Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
41250fb
fix: normalize getFromResponse cookie handling
Preeternal Jul 20, 2026
13835c4
docs: axios changes
Preeternal Jul 20, 2026
9664b85
fix: docs typo
Preeternal Jul 20, 2026
02eafbd
fix(android): resolve cookie mutations after flush
Preeternal Jul 20, 2026
8c16236
fix(ios): await WebKit clearByName deletions
Preeternal Jul 20, 2026
6216083
fix(android): return stored cookie attributes from get
Preeternal Jul 21, 2026
c2088b3
Merge branch 'master' into v.6.3.4
Preeternal Jul 21, 2026
6763cfd
fix: make removeSessionCookies cross-platform and deterministic
Preeternal Jul 21, 2026
93ba0f0
feat: add deterministic clearAllStores
Preeternal Jul 21, 2026
2adadae
fix(android): persist getFromResponse cookies before resolve
Preeternal Jul 21, 2026
ae09f61
fix(ios): correct case and leading-dot domain matching
Preeternal Jul 21, 2026
cf02afb
feat: add duplicate-preserving cookie array APIs
Preeternal Jul 21, 2026
431e939
ci: run Android unit tests
Preeternal Jul 21, 2026
d0ba8e9
test(ios): add cookie store integration coverage
Preeternal Jul 21, 2026
c114ba1
docs: polish useWebKit description
Preeternal Jul 21, 2026
463b0fe
feat: add getCookieHeader API
Preeternal Jul 21, 2026
80513e5
feat(android): support clearByName
Preeternal Jul 21, 2026
ec09311
feat(android): make AndroidX WebKit version configurable
Preeternal Jul 21, 2026
ec866e7
feat: support SameSite and Max-Age cookie attributes
Preeternal Jul 21, 2026
d161230
docs: polish api table
Preeternal Jul 21, 2026
efa73ed
docs: bold font for api methods
Preeternal Jul 21, 2026
fa0a97b
tests: add device checks
Preeternal Jul 21, 2026
ebbc7ab
fix(android): preserve ISO cookie expiry across restarts
Preeternal Jul 22, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 45 additions & 11 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ jobs:
- name: Typecheck files
run: yarn typecheck

- name: Test Expo config plugin
run: yarn test:plugin

build-library:
runs-on: ubuntu-latest
Expand Down Expand Up @@ -73,6 +75,16 @@ jobs:
npm install
npm install --ignore-scripts "$GITHUB_WORKSPACE/$PACKAGE_TGZ"

node -e '
const fs = require("node:fs");
const app = JSON.parse(fs.readFileSync("app.json", "utf8"));
app.expo.plugins = [
...(app.expo.plugins || []),
["@preeternal/react-native-cookie-manager", { androidWebkitVersion: "1.15.0" }]
];
fs.writeFileSync("app.json", JSON.stringify(app, null, 2) + "\n");
'

CI=1 npx expo config --json > "$EXPO_WORK_DIR/expo-config.json" 2> "$EXPO_WORK_DIR/expo-config.stderr.log" || {
echo "::error::expo config failed"
if [[ -f "$EXPO_WORK_DIR/expo-config.stderr.log" ]]; then
Expand All @@ -91,6 +103,8 @@ jobs:

CI=1 npx expo prebuild --platform android --clean --no-install 2>&1 | tee "$EXPO_WORK_DIR/prebuild.log"

grep -Fx "react_native_cookie_manager_webkit_version=1.15.0" android/gradle.properties

- name: Print Expo prebuild diagnostics
if: always()
run: |
Expand Down Expand Up @@ -158,20 +172,16 @@ jobs:
fi

- name: Install JDK
if: env.turbo_cache_hit != 1
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
with:
distribution: 'zulu'
java-version: '17'

- name: Finalize Android SDK
if: env.turbo_cache_hit != 1
run: |
/bin/bash -c "yes | $ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager --licenses > /dev/null"


- name: Cache Gradle
if: env.turbo_cache_hit != 1
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
with:
path: |
Expand All @@ -181,6 +191,11 @@ jobs:
restore-keys: |
${{ runner.os }}-gradle-

- name: Run Android unit tests
working-directory: example/android
run: |
./gradlew :preeternal_react-native-cookie-manager:testDebugUnitTest --no-daemon --console=plain

- name: Build example for Android
env:
JAVA_OPTS: "-XX:MaxHeapSize=6g"
Expand All @@ -203,12 +218,6 @@ jobs:
- name: Setup
uses: ./.github/actions/setup

- name: Run Swift domain tests
run: |
CLANG_MODULE_CACHE_PATH="$RUNNER_TEMP/clang-module-cache" \
SWIFT_MODULECACHE_PATH="$RUNNER_TEMP/swift-module-cache" \
swift test

- name: Cache turborepo for iOS
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
with:
Expand All @@ -226,11 +235,36 @@ jobs:
fi

- name: Use appropriate Xcode version
if: env.turbo_cache_hit != 1
uses: maxim-lobanov/setup-xcode@60606e260d2fc5762a71e64e74b2174e8ea3c8bd # v1.6.0
with:
xcode-version: ${{ env.XCODE_VERSION }}

- name: Run Swift tests
run: |
CLANG_MODULE_CACHE_PATH="$RUNNER_TEMP/clang-module-cache" \
SWIFT_MODULECACHE_PATH="$RUNNER_TEMP/swift-module-cache" \
swift test

- name: Run iOS simulator tests
run: |
set -euo pipefail

SIMULATOR_UDID=$(xcrun simctl list devices available --json | ruby -rjson -e '
data = JSON.parse(STDIN.read)
device = data.fetch("devices").values.flatten.find do |item|
item["isAvailable"] && item["name"].start_with?("iPhone")
end
abort "No available iPhone simulator" unless device
puts device.fetch("udid")
')

xcodebuild \
-scheme CookieManagerSwiftTests-Package \
-destination "platform=iOS Simulator,id=$SIMULATOR_UDID" \
-derivedDataPath "$RUNNER_TEMP/swift-package-ios" \
-quiet \
test

- name: Install cocoapods
if: env.turbo_cache_hit != 1 && steps.cocoapods-cache.outputs.cache-hit != 'true'
run: |
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -87,3 +87,6 @@ android/generated

# React Native Nitro Modules
nitrogen/

# Local notes
/docs/local-*.md
60 changes: 60 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,66 @@

---

## v6.4.0: Reliable native cookies and modern attributes (Unreleased)

### Added

- Added `clearAllStores()` for deterministic full-store cleanup. It clears Foundation and the default WebKit store on iOS, or the shared cookie store on Android, and resolves only after native cleanup (including Android persistence) completes. Existing `clearAll(useWebKit?)` behavior is unchanged.
- Added `getAsArray()` on both platforms and iOS-only `getAllAsArray()` to preserve cookies that share a name but differ by domain or path. Existing `get()` and `getAll()` retain their upstream-compatible last-cookie-wins object shape.
- Added `getCookieHeader(url, useWebKit?)` to read matching cookies as a ready-to-use `Cookie` request-header value. It preserves duplicate names and returns an empty string when no cookies match.
- Added Android support for `clearByName(url, name)`. When the device's Android System WebView provider supports `GET_COOKIE_INFO`, it expires every same-name domain/path variant applicable to the supplied URL, waits for all deletion callbacks, then flushes persistence. Devices with an older provider reject with `not_supported` instead of risking incomplete deletion.
- Added AndroidX WebKit version configuration for bare React Native and Expo builds through `react_native_cookie_manager_webkit_version` / `androidWebkitVersion`. The shared `rootProject.ext.webkitVersion` override used by `react-native-webview` is also honored.
- Added structured `sameSite` and `maxAge` fields to `set()` on both platforms. `maxAge` is a relative lifetime in whole seconds and takes precedence over `expires`; `SameSite=None` requires `Secure`. Reads return `sameSite` when the selected native store exposes it and continue to return the effective absolute `expires` date rather than the original `maxAge`.

### Fixed

- Android `getFromResponse()` now performs the inherited GET request instead of returning the input URL.
- Android applies `Set-Cookie` before each redirect and selects stored cookies for every destination URL instead of forwarding the original `Cookie` header.
- Both platforms return the declared `Cookies` object shape and update their native cookie store.
- On iOS, `getFromResponse()` values are now `Cookie` objects instead of raw strings; read the cookie value through `.value`. This aligns runtime behavior with the existing TypeScript contract.
- Awaiting Android `getFromResponse()` now guarantees that cookies stored from the final response or any redirect have been flushed to persistent storage; the blocking persistence work runs on a worker thread.
- Awaiting Android `set()`, `setFromResponse()`, or `clearAll()` now guarantees that its automatic `flush()` has completed, so an immediate app shutdown or restart cannot leave the previous cookie state on disk. The blocking persistence work runs on a worker thread.
- Awaiting iOS `clearByName(url, name, true)` now waits for every matching WebKit deletion, preventing a following request or WebView load from observing cookies that were still being removed.
- iOS `get(url, true)` and `clearByName()` now match cookie domains case-insensitively in both Foundation and WebKit flows. Leading-dot domains apply to their root host as well as subdomains, while strict domain boundaries remain enforced.
- Android `get(url)` now returns stored `domain`, `path`, `expires`, `secure`, `httpOnly`, and `sameSite` attributes when the device's Android System WebView provider supports `GET_COOKIE_INFO`. Devices with an older provider transparently retain the previous name/value-only behavior.
- Android `set()` now serializes `expires` directly as an absolute `Expires` attribute instead of temporarily storing an absolute timestamp in `HttpCookie.maxAge`. Its ISO parser now accepts the `Z` and offset forms returned by JavaScript dates on Android instead of silently treating them as session cookies. This preserves future and past-date behavior while allowing the new relative `maxAge` field to use its standards-defined seconds contract without accidentally turning deletion cookies into session cookies.
- `removeSessionCookies()` now works on iOS and resolves only after session cookies have been removed from the selected stores; persistent cookies remain untouched. Foundation and the default WebKit store are selected by default. On Android, awaiting it now also guarantees that its automatic `flush()` has completed, so an immediate shutdown cannot leave removed session cookies on disk.
- iOS cookie cleanup no longer invokes the unrelated deprecated `UserDefaults.synchronize()` API.

### Tests

- Added Android unit coverage for the detailed cookie read, both fallback paths, empty results, attribute parsing, expiration precedence/conversion, and legacy behavior.
- Added Swift coverage for Foundation-only, WebKit-only, and both-store session cleanup, persistent-cookie preservation, and asynchronous deletion completion ordering.
- Added Swift coverage for full-store cleanup ordering and WebKit completion.
- Added Swift regression coverage for mixed-case, leading-dot root, parent-domain, and substring-rejection matching.
- Added Swift and Kotlin regression coverage for duplicate-name array reads and legacy object collapsing.
- Added Foundation and WebKit store integration tests on macOS and iOS Simulator, covering duplicate-name round trips, store selection, and completion-aware deletion/cleanup.
- Added Swift coverage for request-header formatting and WebKit domain/path/secure/expiry matching, plus Android coverage for raw header passthrough and empty stores.
- Added Android coverage for host-only, domain, path, prefixed, and partitioned cookie deletion, unsupported providers, callback ordering, and rejected writes.
- Added unit coverage for the Expo config plugin and extended the Expo prebuild smoke test to verify its generated Gradle property.
- Added Swift and Kotlin coverage for `sameSite`, relative `maxAge`, `maxAge`/`expires` precedence, immediate deletion, invalid values, concrete ISO timestamps and time-zone offsets, Android RFC serialization, and Foundation/WebKit store round trips.
- Added example app device checks: a one-tap public API smoke test and a two-phase prepare → force-stop → verify flow for persistent-cookie restoration without an extra manual `flush()`.

### Compatibility

- Android defaults to `androidx.webkit:webkit:1.16.0`; applications may request another full version through the package Gradle/Expo option or shared `rootProject.ext.webkitVersion`. Versions below `1.6.0` are rejected, while normal Gradle conflict resolution may select a higher version. The library defaults remain `minSdk 24`, `compileSdk 36`, and AGP 8.7.2.

### Deprecated

- `getFromResponse()` is deprecated but retained for compatibility with `@react-native-cookies/cookies`; no removal version is scheduled.
- Prefer making requests with Fetch/Axios and reading the shared native store with `get(url)`. This avoids a duplicate request and leaves request configuration with the application.

### Documentation

- Documented the network and cookie-store side effects of the legacy API.
- Added a concise Fetch/Axios response flow and clarified the advanced `setFromResponse()` use case.
- Clarified that direct Android `flush()` calls are redundant after library mutations and are intended only as a persistence barrier for external shared-store changes.
- Documented native persistence and expiration behavior, including Android WebView session-cookie restoration and the decision not to maintain a separate automatic cookie backup.
- Documented AndroidX WebKit overrides for bare React Native, `react-native-webview` interoperability, and Expo prebuild.
- Documented modern cookie attributes, read-back limitations, and why `partitioned` is not exposed without a reliable top-level partition context.

---

## v6.3.3: Strict cookie domain validation

This release fixes inherited cookie domain validation behavior that accepted substring matches instead of RFC-style domain matches.
Expand Down
139 changes: 138 additions & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,157 @@ import PackageDescription
let package = Package(
name: "CookieManagerSwiftTests",
platforms: [
.iOS(.v15),
.macOS(.v12),
],
products: [],
targets: [
.target(
name: "CookieDomainLogic",
path: "ios",
exclude: ["CookieManager.h", "CookieManager.mm", "CookieManager.swift"],
exclude: [
"CookieAttributeLogic.swift",
"CookieHeaderLogic.swift",
"CookieCollectionLogic.swift",
"CookieManager.h",
"CookieManager.mm",
"CookieManager.swift",
"CookieStoreAccess.swift",
"CookieStoreClearLogic.swift",
"CookieSessionLogic.swift",
],
sources: ["CookieDomainLogic.swift"]
),
.target(
name: "CookieSessionLogic",
path: "ios",
exclude: [
"CookieAttributeLogic.swift",
"CookieHeaderLogic.swift",
"CookieCollectionLogic.swift",
"CookieDomainLogic.swift",
"CookieManager.h",
"CookieManager.mm",
"CookieManager.swift",
"CookieStoreAccess.swift",
"CookieStoreClearLogic.swift",
],
sources: ["CookieSessionLogic.swift"]
),
.target(
name: "CookieStoreClearLogic",
path: "ios",
exclude: [
"CookieAttributeLogic.swift",
"CookieHeaderLogic.swift",
"CookieCollectionLogic.swift",
"CookieDomainLogic.swift",
"CookieManager.h",
"CookieManager.mm",
"CookieManager.swift",
"CookieStoreAccess.swift",
"CookieSessionLogic.swift",
],
sources: ["CookieStoreClearLogic.swift"]
),
.target(
name: "CookieCollectionLogic",
path: "ios",
exclude: [
"CookieAttributeLogic.swift",
"CookieHeaderLogic.swift",
"CookieDomainLogic.swift",
"CookieManager.h",
"CookieManager.mm",
"CookieManager.swift",
"CookieSessionLogic.swift",
"CookieStoreAccess.swift",
"CookieStoreClearLogic.swift",
],
sources: ["CookieCollectionLogic.swift"]
),
.target(
name: "CookieStoreAccess",
path: "ios",
exclude: [
"CookieAttributeLogic.swift",
"CookieHeaderLogic.swift",
"CookieCollectionLogic.swift",
"CookieDomainLogic.swift",
"CookieManager.h",
"CookieManager.mm",
"CookieManager.swift",
"CookieSessionLogic.swift",
"CookieStoreClearLogic.swift",
],
sources: ["CookieStoreAccess.swift"]
),
.target(
name: "CookieHeaderLogic",
path: "ios",
exclude: [
"CookieAttributeLogic.swift",
"CookieCollectionLogic.swift",
"CookieDomainLogic.swift",
"CookieManager.h",
"CookieManager.mm",
"CookieManager.swift",
"CookieSessionLogic.swift",
"CookieStoreAccess.swift",
"CookieStoreClearLogic.swift",
],
sources: ["CookieHeaderLogic.swift"]
),
.target(
name: "CookieAttributeLogic",
path: "ios",
exclude: [
"CookieCollectionLogic.swift",
"CookieDomainLogic.swift",
"CookieHeaderLogic.swift",
"CookieManager.h",
"CookieManager.mm",
"CookieManager.swift",
"CookieSessionLogic.swift",
"CookieStoreAccess.swift",
"CookieStoreClearLogic.swift",
],
sources: ["CookieAttributeLogic.swift"]
),
.testTarget(
name: "CookieDomainLogicTests",
dependencies: ["CookieDomainLogic"],
path: "swift-tests/CookieDomainLogicTests"
),
.testTarget(
name: "CookieSessionLogicTests",
dependencies: ["CookieSessionLogic"],
path: "swift-tests/CookieSessionLogicTests"
),
.testTarget(
name: "CookieStoreClearLogicTests",
dependencies: ["CookieStoreClearLogic"],
path: "swift-tests/CookieStoreClearLogicTests"
),
.testTarget(
name: "CookieCollectionLogicTests",
dependencies: ["CookieCollectionLogic"],
path: "swift-tests/CookieCollectionLogicTests"
),
.testTarget(
name: "CookieStoreAccessIntegrationTests",
dependencies: ["CookieStoreAccess"],
path: "swift-tests/CookieStoreAccessIntegrationTests"
),
.testTarget(
name: "CookieHeaderLogicTests",
dependencies: ["CookieHeaderLogic"],
path: "swift-tests/CookieHeaderLogicTests"
),
.testTarget(
name: "CookieAttributeLogicTests",
dependencies: ["CookieAttributeLogic"],
path: "swift-tests/CookieAttributeLogicTests"
),
]
)
Loading
Loading