Skip to content
 
 

Latest commit

 

History

183 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

@preeternal/react-native-cookie-manager

npm version npm downloads

A maintained, New Architecture–only cookie manager for React Native, implemented as a TurboModule with Swift on iOS and Kotlin on Android. It is a successor to @react-native-cookies/cookies: legacy method names and call signatures remain available, while v7 intentionally changes architecture support, validation, and error codes.

Starting with v7.0.0, this package supports only React Native's New Architecture. Projects that still require the legacy bridge should stay on v6.x.

The package works in bare React Native apps and in Expo Dev Builds (custom native builds).

Upstream / credits

This package is based on the public API and behavior of @react-native-cookies/cookies. Big thanks to the upstream maintainers and contributors for the original implementation and long-term work on the project.

Installation

Install with your package manager:

# Bun
bun add @preeternal/react-native-cookie-manager

# Yarn
yarn add @preeternal/react-native-cookie-manager

# npm
npm install @preeternal/react-native-cookie-manager

CocoaPods (default)

React Native still selects CocoaPods by default. This library keeps its podspec, so existing apps and React Native versions before 0.87 continue to install it normally:

cd ios && bundle exec pod install

Swift Package Manager (React Native 0.87+)

React Native 0.87 added experimental, opt-in SwiftPM integration. This library ships a compatible Package.swift; CocoaPods remains the default and supported production path.

To migrate an app once:

cd ios
npx react-native spm --deintegrate

After a fresh clone or in CI, generate the SwiftPM workspace before building:

cd ios
npx react-native spm

Every native dependency must have a compatible Package.swift. If a dependency does not provide one, generate it with npx react-native spm scaffold and keep the manifest in a package-manager patch.

The SwiftPM commands and generated layout are experimental in React Native 0.87 and may change in later releases. Do not use this integration in production yet. See the React Native 0.87 release notes.

Usage

After a network request

React Native stores response cookies automatically. Make the request with your HTTP client, then read cookies matching the URL:

import CookieManager from '@preeternal/react-native-cookie-manager';

const url = 'https://example.com/login';
await fetch(url);
// axios alternative: await axios(url);
const cookies = await CookieManager.get(url);

Standard React Native networking handles cookies by default. Credentials options are only needed if your client configuration explicitly disables cookie handling. get() only reads the native cookie store; it does not make a request.

If a custom client or Axios adapter does not use React Native's native cookie handling, getCookieHeader(url) returns a ready-to-use Cookie request-header value. Do not add it to standard Fetch/Axios requests, where native networking already attaches cookies.

The upstream-compatible getFromResponse(url) remains available but is deprecated: it performs a separate GET, follows redirects, and updates the cookie store without options for headers, authentication, timeout, or cancellation. Prefer the flow above to avoid a duplicate request and its side effects.

Manage the cookie store

On iOS, “the WebKit store” below means WKWebsiteDataStore.default().httpCookieStore, the app's default persistent store.

// Foundation on iOS; options are optional
await CookieManager.set('https://example.com', {
  name: 'session',
  value: 'abc123',
  domain: 'example.com',
  path: '/',
  secure: true,
  httpOnly: true,
  sameSite: 'lax',
  maxAge: 60 * 60 * 24 * 7,
});

// iOS: write to the WebKit store
await CookieManager.set(
  'https://example.com',
  { name: 'web_session', value: 'abc123', path: '/', secure: true },
  { iosCookieStore: 'webKit' }
);

const cookies = await CookieManager.get('https://example.com');

// Preserve cookies that share a name but differ by domain or path
const cookieVariants = await CookieManager.getAsArray('https://example.com');

// iOS only: get all cookies
const allCookies = await CookieManager.getAll();

// Clear cookies named "session"
await CookieManager.clearByName('https://example.com', 'session');

// Clear Foundation on iOS; clear the shared store on Android
await CookieManager.clearAll();

// Clear Foundation and the WebKit store on iOS
await CookieManager.clearAllStores();

// Remove session cookies from both iOS stores; shared Android store
await CookieManager.removeSessionCookies();
await CookieManager.removeSessionCookies({ iosCookieStore: 'both' }); // explicit equivalent

// iOS: limit session cleanup to one store when needed
await CookieManager.removeSessionCookies({ iosCookieStore: 'webKit' });

Observe iOS cookie-store changes

Use the change listener to invalidate application state after Foundation or the WebKit store changes—for example, when a login completes inside a WebView:

import CookieManager, {
  type IOSCookieStore,
} from '@preeternal/react-native-cookie-manager';
import { useEffect } from 'react';
import { Platform } from 'react-native';

const url = 'https://example.com/account';

useEffect(() => {
  if (Platform.OS !== 'ios') {
    return;
  }

  const subscription = CookieManager.addCookieChangeListener(
    ({ iosCookieStore }) => {
      refreshCookies(iosCookieStore)
        .then((snapshot) => {
          updateAuthState(snapshot);
        })
        .catch((error) => {
          console.error('Failed to refresh cookies after invalidation', error);
        });
    }
  );

  return () => subscription.remove();
}, []);

async function refreshCookies(iosCookieStore: IOSCookieStore) {
  // Read cookies matching one URL from the store that changed.
  const matchingCookies = await CookieManager.get(url, { iosCookieStore });
  const sessionCookie = matchingCookies.session;

  // Or read every cookie in that store.
  const allCookiesInChangedStore = await CookieManager.getAll({
    iosCookieStore,
  });

  // When application state depends on both stores, read them separately.
  const [allFoundationCookies, allWebKitCookies] = await Promise.all([
    CookieManager.getAll({ iosCookieStore: 'foundation' }),
    CookieManager.getAll({ iosCookieStore: 'webKit' }),
  ]);

  return {
    sessionCookie,
    allCookiesInChangedStore,
    allFoundationCookies,
    allWebKitCookies,
  };
}

The event payload is only { iosCookieStore: 'foundation' | 'webKit' }. It is an invalidation signal: native stores may coalesce notifications, so one event is not guaranteed for every cookie mutation and no cookie delta is provided. Choose the narrowest useful follow-up read: select one cookie from get(), preserve same-name variants with getAsArray(), read the changed store with getAll() / getAllAsArray(), or call those methods once per store when both snapshots are required. Results from Foundation and WebKit remain separate. The listener observes Foundation and the default persistent WebKit store; custom or non-persistent WKWebsiteDataStore instances are outside its scope.

Cookie change subscriptions are iOS-only. Calling addCookieChangeListener() on Android throws an error with code: 'not_supported' because the public Android WebView cookie store has no global change observer.

Structured validation and v6 migration

set() validates the complete structured cookie before touching a native store. Validation is enabled by default on both platforms. It rejects malformed names, paths, and expiry dates, together with control characters or field delimiters that could change the cookie's structure. Printable values do not require percent-encoding or base64url merely because they contain spaces, Unicode, quotes, commas, backslashes, or =.

During migration from v6, { validate: false } temporarily restores native-store handling for compatibility-sensitive input. For example, v6 treated an unparseable expires value as a session cookie:

import CookieManager, {
  isCookieManagerError,
  type Cookie,
} from '@preeternal/react-native-cookie-manager';

const cookieFromBackend: Cookie = {
  name: 'session',
  value: '<token supplied by the backend>',
  path: '/',
  // set() requires an ISO 8601 expiry date.
  expires: '2032-06-09 10:18:14 UTC',
};

try {
  await CookieManager.set('https://example.com', cookieFromBackend);
} catch (error) {
  if (isCookieManagerError(error) && error.code === 'invalid_cookie') {
    console.warn('Structured cookie rejected', {
      code: error.code,
      cookieName: cookieFromBackend.name,
      path: cookieFromBackend.path,
      hasExpires: cookieFromBackend.expires !== undefined,
      backendOperation: 'create-session',
    });
  }
}

// Use only after confirming that validation caused the migration failure.
await CookieManager.set('https://example.com', cookieFromBackend, {
  validate: false,
});

The validate field is deprecated and will be removed in the next major version, when validation becomes unconditional. Do not automatically retry every invalid_cookie rejection with validation disabled. Log the stable code and safe source context, fix the producer or backend, and then remove the escape hatch. Never log a cookie value, raw Set-Cookie header, authentication token, or session identifier. Messages are diagnostic text and must not be used for branching.

Structural safety checks remain active when validate: false. CR, LF, NUL, other ASCII controls, and ; in a structured value still reject. Android WebView accepts only a raw Set-Cookie string and treats the first ; as an attribute separator, even inside quotes. A literal semicolon therefore cannot be preserved without encoding agreed by the application and backend; the library never encodes values implicitly.

Import a Set-Cookie header

setFromResponse() is an advanced API for importing a raw Set-Cookie header from a custom HTTP client that does not share React Native's cookie store. It is normally unnecessary with Fetch or Axios. Call it once for each Set-Cookie header value.

await CookieManager.setFromResponse(
  'https://example.com',
  'session=abc123; Path=/; Secure; HttpOnly'
);

Semicolons in this raw API delimit real attributes; they do not escape a literal semicolon inside a value. Empty headers and headers containing CR, LF, or NUL reject before reaching the native store.

API

Legacy method names and positional boolean overloads remain source-compatible with @react-native-cookies/cookies. The New Architecture requirement, default validation, and stable error taxonomy are intentional v7 behavior changes.

Method Platforms Description
addCookieChangeListener(listener): EventSubscription iOS Subscribes to invalidations from Foundation and the WebKit store. Native observers are shared across JS subscribers and stop after the last subscription is removed. Android throws not_supported.
set(url, cookie, options?): Promise<boolean> iOS, Android Validates and stores a cookie, including sameSite and relative maxAge. On iOS, omitted options select Foundation; { iosCookieStore: 'webKit' } selects the WebKit store. options.validate is a temporary deprecated v7 migration escape hatch.
get(url, options?): Promise<Cookies> iOS, Android Reads matching cookies without making a request. On iOS, omitted options select Foundation; { iosCookieStore: 'webKit' } selects the WebKit store.
getAsArray(url, options?): Promise<ReadonlyArray<Cookie>> iOS, Android Reads matching cookies without collapsing cookies that share a name. Store selection matches get().
getCookieHeader(url, options?): Promise<string> iOS, Android Returns the selected store's matching cookies as a Cookie request-header value, or an empty string.
clearAll(options?): Promise<boolean> iOS, Android Clears the shared Android store or the selected iOS store.
clearAllStores(): Promise<boolean> iOS, Android Clears the shared Android store, or Foundation and the WebKit store on iOS; resolves true after native completion.
getAll(options?): Promise<Cookies> iOS Reads all cookies from the selected iOS store.
getAllAsArray(options?): Promise<ReadonlyArray<Cookie>> iOS Reads the selected iOS store without collapsing cookies that share a name.
clearByName(url, name, options?): Promise<boolean> iOS, Android Clears same-name cookies from the selected iOS store, or variants applicable to url in the shared Android store.
flush(): Promise<void> iOS, Android Explicit Android persistence barrier for external shared-store changes. Android mutations already flush automatically; this method is a no-op on iOS.
removeSessionCookies(options?): Promise<boolean> iOS, Android Removes cookies without an expiry date and reports whether any were removed; includes both iOS stores by default.
setFromResponse(url, cookieHeader): Promise<boolean> iOS, Android Imports one raw Set-Cookie header value; uses Foundation on iOS.
getFromResponse(url): Promise<Cookies> iOS, Android Deprecated; performs a GET and updates Foundation on iOS.

All single-store methods use the same optional { iosCookieStore } selector. Omitting options or omitting the field selects Foundation. The legacy positional useWebKit overloads remain available throughout v7 but are deprecated; replace true with { iosCookieStore: 'webKit' } and false with { iosCookieStore: 'foundation' } or omitted options. On iOS each call selects one store and never combines them. Android ignores iosCookieStore because WebView and native share a single store.

removeSessionCookies() clears both iOS stores by default. Pass { iosCookieStore: 'both' } to state that scope explicitly, or select 'foundation' / 'webKit' to limit cleanup to one store. Android ignores this iOS-only option.

On Android, clearByName() relies on GET_COOKIE_INFO support in the device's Android System WebView provider. It rejects with not_supported on devices with an older provider. The method clears every same-name domain/path variant visible to the supplied URL. A cookie restricted to /account is not visible from a / URL, so use a matching path (and multiple calls for unrelated paths). On iOS, the method clears same-domain variants across all paths in the selected store.

Error handling

Native failures use a small platform-neutral set of stable codes. Use isCookieManagerError() before reading code:

import CookieManager, {
  isCookieManagerError,
} from '@preeternal/react-native-cookie-manager';

try {
  await CookieManager.set(url, cookie);
} catch (error) {
  if (isCookieManagerError(error)) {
    switch (error.code) {
      case 'domain_mismatch':
        // The cookie domain cannot be set from this URL.
        break;
      case 'invalid_cookie':
        // Fix the structured cookie input.
        break;
    }
  }
}
Code Meaning
invalid_url The supplied URL failed the parsing or host check required by that operation.
invalid_cookie Cookie input cannot be represented or accepted as a cookie.
domain_mismatch The cookie domain does not match the URL host or one of its parent domains.
not_supported The requested capability is unavailable on this platform or native provider.
storage_error The native cookie store could not complete a read, write, deletion, or persistence operation.
network_error The deprecated getFromResponse() request failed. No other method performs network I/O.

The human-readable message and any native cause are diagnostic details and are not stable API. Do not branch on their contents.

v7 does not add a global HTTP(S)-only check to every store operation. URL acceptance otherwise remains delegated to the platform store for compatibility with v6. The deprecated network method still requires an HTTP(S) URL.

Cookie shape

type Cookie = {
  name: string;
  value: string;
  path?: string;
  domain?: string;
  version?: string;
  expires?: string; // ISO 8601 string, e.g. 2015-05-30T12:30:00.00-05:00
  secure?: boolean;
  httpOnly?: boolean;
  sameSite?: 'lax' | 'strict' | 'none';
  maxAge?: number; // set() only: relative lifetime in whole seconds
};

type IOSCookieStore = 'foundation' | 'webKit';

type IOSCookieStoreOptions = {
  iosCookieStore?: IOSCookieStore;
};

type SetCookieOptions = IOSCookieStoreOptions & {
  /** @deprecated Temporary v7 migration escape hatch. */
  validate?: boolean;
};

type RemoveSessionCookiesOptions = {
  // Defaults to 'both'.
  iosCookieStore?: IOSCookieStore | 'both';
};

maxAge takes precedence over expires; 0 or a negative value expires the cookie immediately. Native stores expose the resulting absolute expires date when reading, not the original maxAge. sameSite: 'none' requires secure: true. The iOS HTTPCookie model represents this unrestricted policy as no explicit SameSite value, so reads may omit sameSite after setting 'none'.

partitioned is intentionally not a structured field: creating a partitioned cookie requires top-level site context that this API's cookie URL cannot express consistently across Android and iOS. Prefer receiving it from the server or setting it inside the relevant WebView context.

Cookies is keyed by cookie name, so get() and getAll() retain only the last item when multiple cookies share a name. This legacy behavior is preserved for upstream compatibility. Use getAsArray() or getAllAsArray() when domain/path variants must remain separate.

On Android, metadata is populated when the device's Android System WebView provider supports GET_COOKIE_INFO. Devices with an older provider fall back to legacy name/value parsing, so domain, path, expires, and sameSite may be unavailable, while secure and httpOnly should not be treated as authoritative.

WebKit on iOS

  • iOS has two stores: NSHTTPCookieStorage (used by URLSession) and WKHTTPCookieStore (used by WKWebView / react-native-webview).
  • Pass { iosCookieStore: 'webKit' } to set() to use the default WKWebView cookie store. For network-only flows, omit options or select 'foundation' to use NSHTTPCookieStorage.
  • To apply a single-store method to both stores, call it once with { iosCookieStore: 'foundation' } (or omitted options) and once with { iosCookieStore: 'webKit' }. Results are returned separately and are not merged.
  • getCookieHeader(url, { iosCookieStore: 'webKit' }) filters cookies from the WebKit store by domain, path, Secure, and expiry. A URL alone cannot reproduce WebKit's SameSite, partition, or third-party request context, so do not treat it as the exact header of an embedded WebView request.
  • Use clearAllStores() when logout must clear both app-accessible stores. The library cannot access a non-persistent or custom store owned by a specific WebView.
  • On Android the flag is ignored; WebView and native use the same store.

Warning

On Android, react-native-webview's incognito mode currently clears the shared app-wide cookie store, including cookies used by React Native networking. Avoid it when your app relies on authenticated native requests. See react-native-webview#3988.

AndroidX WebKit version

Android uses androidx.webkit:webkit:1.16.0 by default. Most applications do not need to configure it. Bare React Native apps can override the requested version in android/gradle.properties:

react_native_cookie_manager_webkit_version=1.16.0

An existing shared override in the root android/build.gradle is also honored, including when it configures react-native-webview:

rootProject.ext.webkitVersion = "1.16.0"

The package-specific gradle.properties value takes precedence when both are present. Expo apps can set it during prebuild:

{
  "expo": {
    "plugins": [
      [
        "@preeternal/react-native-cookie-manager",
        { "androidWebkitVersion": "1.16.0" }
      ]
    ]
  }
}

Versions older than 1.6.0 are unsupported because the library compiles against CookieManagerCompat.getCookieInfo(). Gradle may select a higher compatible version when another dependency requires it.

Persistence and expiration

  • A cookie is persistent only when the server supplies Expires/Max-Age, or when set() receives expires/maxAge. Native stores enforce expiration; flush() does not extend a cookie's lifetime or turn a session cookie into a persistent one.
  • On iOS, Foundation persistent cookies survive without a WebView. There is no public iOS flush API, so flush() is a no-op.
  • On Android, the library automatically flushes the shared WebView cookie store after its mutations. Current WebView implementations may also restore session cookies—cookies without an expiry—after a process restart.

A persistent cookie written with { iosCookieStore: 'webKit' } survives process termination only if a normal, non-incognito WKWebView using the default data store was mounted before the process ended. A normally mounted react-native-webview satisfies this requirement; installing the package without mounting a WebView does not.

After a cold start, mount the WebView before reading cookies from the previous app session with get(), getAsArray(), getCookieHeader(), getAll(), or getAllAsArray() using { iosCookieStore: 'webKit' }. Do the same before clearByName(url, name, { iosCookieStore: 'webKit' }) when deleting a persistent cookie from the previous session, because this method first reads the store to find matching cookies.

Full cleanup with clearAll({ iosCookieStore: 'webKit' }) or clearAllStores() can run before a WebView is mounted because it clears WebKit website data directly. removeSessionCookies() can also run before mounting; iOS session cookies are process-scoped and are not expected to survive a restart.

If the app never creates a WebView, use the Foundation store instead by omitting options or selecting { iosCookieStore: 'foundation' }.

On Android, mutation methods automatically flush before their Promises resolve. Calling flush() immediately after awaiting set(), setFromResponse(), getFromResponse(), clearByName(), clearAll(), clearAllStores(), or removeSessionCookies() is redundant. Use it only as an explicit persistence barrier after the shared Android store was changed outside this library.

The library intentionally does not maintain a separate cookie backup or silently replay cookies on startup. That could resurrect expired or logged-out authentication state and would require the application to choose appropriate secure storage. Prefer server-defined persistent cookies; call removeSessionCookies() before the first request or WebView load when the application requires a clean session on launch.

Examples

  • example/ uses CocoaPods on iOS and is also the Android example.
  • example-spm/ uses React Native 0.87 SwiftPM autolinking without CocoaPods.

Both apps demonstrate store selection, iOS invalidations, stable errors, structured validation, native smoke checks, and persistence across an app restart.

Contributing

License

MIT


Made with create-react-native-library

About

🍪 Cookie Manager for React Native

Resources

Code of conduct

Contributing

Stars

29 stars

Watchers

1 watching

Forks

Releases

Sponsor this project

Packages

Contributors

Languages