@sendoracloud/sdk-react-native 1.36.0 → 1.38.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -228,7 +228,7 @@ var Storage = class {
228
228
  // package.json
229
229
  var package_default = {
230
230
  name: "@sendoracloud/sdk-react-native",
231
- version: "1.36.0",
231
+ version: "1.38.0",
232
232
  description: "Sendora Cloud React Native + Expo SDK \u2014 analytics, identity, push-token registration, auth (incl. Game Center / Play Games), deep links (Branch / Firebase Dynamic Links parity). Auth + analytics + deep links work in Expo Go; push-token registration and the no-arg Game Center / Play Games sign-in require a Dev Client (EAS Build or `npx expo prebuild`).",
233
233
  type: "module",
234
234
  main: "./dist/index.cjs",
@@ -284,7 +284,7 @@ var package_default = {
284
284
  scripts: {
285
285
  build: "tsup src/index.ts src/contact-widget.tsx --format esm,cjs --dts --clean --external react --external react-native --external react-native-webview",
286
286
  typecheck: "tsc --noEmit",
287
- test: "tsx test/secure-storage.test.ts && tsx test/integration-storage.test.ts && tsx test/plugin.test.ts && tsx test/game-auth.test.ts && tsx test/link.test.ts && tsx test/session-preservation.test.ts && tsx test/credential-collision.test.ts && tsx test/token-expiry.test.ts && tsx test/unlink.test.ts"
287
+ test: "tsx test/secure-storage.test.ts && tsx test/integration-storage.test.ts && tsx test/plugin.test.ts && tsx test/game-auth.test.ts && tsx test/link.test.ts && tsx test/session-preservation.test.ts && tsx test/credential-collision.test.ts && tsx test/token-expiry.test.ts && tsx test/unlink.test.ts && tsx test/deferred-match.test.ts && tsx test/anon-id-stability.test.ts"
288
288
  },
289
289
  devDependencies: {
290
290
  "@types/react": "^19.2.16",
@@ -2156,7 +2156,7 @@ var Auth = class {
2156
2156
  this.hooks.storage.remove(TOKEN_EXPIRES_KEY);
2157
2157
  this.hooks.storage.remove(REFRESH_KEY);
2158
2158
  this.stopProactiveRefreshCron();
2159
- await this.hooks.onAnonymousWipe();
2159
+ await this.hooks.onAnonymousWipe(reason);
2160
2160
  if (reason !== "replaced") {
2161
2161
  this.emitAuthState({ event: "signed_out", user: null, reason });
2162
2162
  }
@@ -2627,33 +2627,31 @@ var Links = class {
2627
2627
  }
2628
2628
  }
2629
2629
  /**
2630
- * Cold-launch deferred deep link match. When neither `fingerprintHash` nor
2631
- * `installReferrer` is supplied, the SDK auto-discovers:
2632
- * - Android: probes `react-native-play-install-referrer` (preferred — 100%
2633
- * accurate when present).
2634
- * - Otherwise: computes the canonical device fingerprint hash.
2630
+ * Cold-launch deferred deep-link match.
2631
+ *
2632
+ * ⚠ NO IMPLICIT FINGERPRINTING (compliance). This used to compute a device
2633
+ * fingerprint on iOS when you passed no signal; it no longer does — Apple
2634
+ * prohibits deriving a device identifier from device signals for attribution
2635
+ * (App Store 5.1.2 / DPLA §3.3.9), regardless of ATT, and doing it on-device is
2636
+ * exactly that. Signal resolution now:
2637
+ * - Android: probes `react-native-play-install-referrer` — deterministic, the
2638
+ * preferred and compliant path.
2639
+ * - iOS: no deterministic signal exists (the App Store has no install referrer).
2640
+ * With `allowProbabilisticMatch: true` the server matches on a coarse
2641
+ * IP + time window (OFF by default — see `LinkMatchInput`). Otherwise this
2642
+ * returns `null` and you route the user through your normal onboarding UX —
2643
+ * which is the expected iOS cold-launch outcome, not an error.
2635
2644
  *
2636
2645
  * Fires `onLinkOpened` with `isDeferred: true` on success.
2637
2646
  */
2638
2647
  async matchDeferred(input = {}) {
2639
- let fingerprintHash = input.fingerprintHash;
2648
+ const fingerprintHash = input.fingerprintHash;
2640
2649
  let installReferrer = input.installReferrer;
2641
2650
  if (!fingerprintHash && !installReferrer) {
2642
2651
  installReferrer = await getPlayInstallReferrer() ?? void 0;
2643
- if (!installReferrer) {
2644
- try {
2645
- fingerprintHash = await computeDeviceFingerprint();
2646
- } catch (err) {
2647
- if (this.deps.debug) console.warn("[sendoracloud] fingerprint compute failed:", err);
2648
- }
2649
- }
2650
- }
2651
- if (!fingerprintHash && !installReferrer) {
2652
- throw new LinkError(
2653
- "INVALID_INPUT",
2654
- "links.matchDeferred: install `react-native-play-install-referrer` for Android, or ensure web crypto / expo-crypto is available for iOS fingerprint compute."
2655
- );
2656
2652
  }
2653
+ const canMatch = !!fingerprintHash || !!installReferrer || input.allowProbabilisticMatch === true;
2654
+ if (!canMatch) return null;
2657
2655
  const body = {};
2658
2656
  if (fingerprintHash) body.fingerprintHash = fingerprintHash;
2659
2657
  if (installReferrer) body.installReferrer = installReferrer;
@@ -3101,9 +3099,11 @@ var SendoraSDK = class {
3101
3099
  if (userId) this.storage.set(USER_ID_KEY, userId);
3102
3100
  else this.storage.remove(USER_ID_KEY);
3103
3101
  },
3104
- onAnonymousWipe: async () => {
3105
- this.anonId = uuid();
3106
- this.storage.set(ANON_KEY, this.anonId);
3102
+ onAnonymousWipe: async (reason) => {
3103
+ if (reason === "user" || reason === "account_deleted") {
3104
+ this.anonId = uuid();
3105
+ this.storage.set(ANON_KEY, this.anonId);
3106
+ }
3107
3107
  this.userId = null;
3108
3108
  this.storage.remove(USER_ID_KEY);
3109
3109
  }
package/dist/index.d.cts CHANGED
@@ -497,12 +497,15 @@ interface AuthHooks {
497
497
  * identity (analytics userId, push token registration, etc). */
498
498
  onIdentityChange: (userId: string | null) => void;
499
499
  /**
500
- * Called when the SDK switches from one identity to another via
501
- * signIn() or non-anonymous signUp(). The parent rotates anonId
502
- * + drops any pending events so the new user can't carry over
503
- * orphaned attribution.
504
- */
505
- onAnonymousWipe: () => Promise<void>;
500
+ * Called from `wipeLocalIdentity` whenever the local session is cleared. The
501
+ * `reason` lets the parent decide whether to rotate the anonymous analytics id:
502
+ * it rotates ONLY at a person-boundary ("user" = explicit signOut,
503
+ * "account_deleted"), never on a successful sign-in/upgrade ("replaced") or an
504
+ * involuntary session death ("session_expired") — those are the same person, and
505
+ * the anon id must survive so pre-auth analytics link to them (Firebase-FID
506
+ * semantics; the industry norm).
507
+ */
508
+ onAnonymousWipe: (reason: WipeReason) => Promise<void>;
506
509
  apiUrl: string;
507
510
  publicKey: string;
508
511
  debug: boolean;
@@ -565,6 +568,12 @@ type AuthStateChange = {
565
568
  user: AuthUser | null;
566
569
  };
567
570
  type AuthStateListener = (change: AuthStateChange) => void;
571
+ /**
572
+ * Internal marker for a wipe that is immediately followed by a `persist()` of
573
+ * a NEW session (the sign-in success path). Not a sign-out — emitting one
574
+ * would make every successful sign-in look like a logout to subscribers.
575
+ */
576
+ type WipeReason = AuthSignedOutReason | "replaced";
568
577
  declare class Auth {
569
578
  private hooks;
570
579
  private user;
@@ -1400,11 +1409,33 @@ interface LinkOpenedEvent<T extends LinkData = LinkData> {
1400
1409
  }
1401
1410
  type LinkOpenedHandler<T extends LinkData = LinkData> = (event: LinkOpenedEvent<T>) => void;
1402
1411
  interface LinkMatchInput {
1403
- /** Hex-encoded SHA-256 — pass output of `computeDeviceFingerprint()`. */
1412
+ /**
1413
+ * Hex-encoded SHA-256 device fingerprint.
1414
+ *
1415
+ * ⚠ **Do not supply this on iOS.** Apple prohibits deriving a device identifier
1416
+ * from device signals for attribution (App Store 5.1.2 / DPLA §3.3.9), and it
1417
+ * does not care which side of the network computed it — passing a hash you built
1418
+ * on-device is the same practice. The SDK no longer computes one for you (it used
1419
+ * to, implicitly, on iOS). Left here for the rare Android/self-hosted caller that
1420
+ * has a legitimate deterministic hash; on iOS, omit it.
1421
+ */
1404
1422
  fingerprintHash?: string;
1405
1423
  /** Raw Play Install Referrer. Sendora probes
1406
1424
  * `react-native-play-install-referrer` automatically when this is omitted. */
1407
1425
  installReferrer?: string;
1426
+ /**
1427
+ * Opt in to probabilistic matching on iOS.
1428
+ *
1429
+ * iOS has no deterministic deferred-install signal (no install referrer), and the
1430
+ * SDK will not fingerprint the device. The only remaining matcher is the server's
1431
+ * coarse IP + timestamp window — the same class of practice Apple scrutinises, and
1432
+ * it collides across a shared NAT / iCloud Private Relay. So it is **off by
1433
+ * default**: without this flag, `matchDeferred()` on iOS matches nothing and
1434
+ * returns `null` (route the user through your normal onboarding UX). Set it only
1435
+ * if you accept the accuracy and compliance trade-off. No effect on Android, where
1436
+ * the Play Install Referrer is deterministic.
1437
+ */
1438
+ allowProbabilisticMatch?: boolean;
1408
1439
  iosBundleId?: string;
1409
1440
  androidPackageName?: string;
1410
1441
  }
@@ -1437,12 +1468,14 @@ interface LinkStats {
1437
1468
  */
1438
1469
  declare function extractShortcodeFromUrl(url: string, linkDomain?: string): string | null;
1439
1470
  /**
1440
- * Canonical device-fingerprint recipe. Identical byte-for-byte across the
1441
- * RN / iOS / Android SDKs so a click logged from one platform and an
1442
- * install completing on another won't accidentally match.
1471
+ * Canonical device-fingerprint recipe (`${platform}|${screenW}x${screenH}|${timezone}|${locale}`
1472
+ * → lowercase hex SHA-256), identical across the RN / iOS / Android SDKs.
1443
1473
  *
1444
- * Input: `${platform}|${screenW}x${screenH}|${timezone}|${locale}`
1445
- * Output: lowercase hex SHA-256.
1474
+ * @deprecated ⚠ Do not use on iOS. Apple prohibits deriving a device identifier
1475
+ * from device signals for attribution (App Store 5.1.2 / DPLA §3.3.9), regardless
1476
+ * of ATT — and this is exactly that. `matchDeferred()` no longer calls it
1477
+ * implicitly. It is retained only for an Android/self-hosted caller with a
1478
+ * legitimate deterministic use, and is slated for removal in a future major.
1446
1479
  */
1447
1480
  declare function computeDeviceFingerprint(): Promise<string>;
1448
1481
  /** Probe optional `react-native-play-install-referrer` peer. Returns the
@@ -1511,11 +1544,20 @@ declare class Links {
1511
1544
  */
1512
1545
  handleUniversalLink(url: string): Promise<boolean>;
1513
1546
  /**
1514
- * Cold-launch deferred deep link match. When neither `fingerprintHash` nor
1515
- * `installReferrer` is supplied, the SDK auto-discovers:
1516
- * - Android: probes `react-native-play-install-referrer` (preferred — 100%
1517
- * accurate when present).
1518
- * - Otherwise: computes the canonical device fingerprint hash.
1547
+ * Cold-launch deferred deep-link match.
1548
+ *
1549
+ * ⚠ NO IMPLICIT FINGERPRINTING (compliance). This used to compute a device
1550
+ * fingerprint on iOS when you passed no signal; it no longer does — Apple
1551
+ * prohibits deriving a device identifier from device signals for attribution
1552
+ * (App Store 5.1.2 / DPLA §3.3.9), regardless of ATT, and doing it on-device is
1553
+ * exactly that. Signal resolution now:
1554
+ * - Android: probes `react-native-play-install-referrer` — deterministic, the
1555
+ * preferred and compliant path.
1556
+ * - iOS: no deterministic signal exists (the App Store has no install referrer).
1557
+ * With `allowProbabilisticMatch: true` the server matches on a coarse
1558
+ * IP + time window (OFF by default — see `LinkMatchInput`). Otherwise this
1559
+ * returns `null` and you route the user through your normal onboarding UX —
1560
+ * which is the expected iOS cold-launch outcome, not an error.
1519
1561
  *
1520
1562
  * Fires `onLinkOpened` with `isDeferred: true` on success.
1521
1563
  */
package/dist/index.d.ts CHANGED
@@ -497,12 +497,15 @@ interface AuthHooks {
497
497
  * identity (analytics userId, push token registration, etc). */
498
498
  onIdentityChange: (userId: string | null) => void;
499
499
  /**
500
- * Called when the SDK switches from one identity to another via
501
- * signIn() or non-anonymous signUp(). The parent rotates anonId
502
- * + drops any pending events so the new user can't carry over
503
- * orphaned attribution.
504
- */
505
- onAnonymousWipe: () => Promise<void>;
500
+ * Called from `wipeLocalIdentity` whenever the local session is cleared. The
501
+ * `reason` lets the parent decide whether to rotate the anonymous analytics id:
502
+ * it rotates ONLY at a person-boundary ("user" = explicit signOut,
503
+ * "account_deleted"), never on a successful sign-in/upgrade ("replaced") or an
504
+ * involuntary session death ("session_expired") — those are the same person, and
505
+ * the anon id must survive so pre-auth analytics link to them (Firebase-FID
506
+ * semantics; the industry norm).
507
+ */
508
+ onAnonymousWipe: (reason: WipeReason) => Promise<void>;
506
509
  apiUrl: string;
507
510
  publicKey: string;
508
511
  debug: boolean;
@@ -565,6 +568,12 @@ type AuthStateChange = {
565
568
  user: AuthUser | null;
566
569
  };
567
570
  type AuthStateListener = (change: AuthStateChange) => void;
571
+ /**
572
+ * Internal marker for a wipe that is immediately followed by a `persist()` of
573
+ * a NEW session (the sign-in success path). Not a sign-out — emitting one
574
+ * would make every successful sign-in look like a logout to subscribers.
575
+ */
576
+ type WipeReason = AuthSignedOutReason | "replaced";
568
577
  declare class Auth {
569
578
  private hooks;
570
579
  private user;
@@ -1400,11 +1409,33 @@ interface LinkOpenedEvent<T extends LinkData = LinkData> {
1400
1409
  }
1401
1410
  type LinkOpenedHandler<T extends LinkData = LinkData> = (event: LinkOpenedEvent<T>) => void;
1402
1411
  interface LinkMatchInput {
1403
- /** Hex-encoded SHA-256 — pass output of `computeDeviceFingerprint()`. */
1412
+ /**
1413
+ * Hex-encoded SHA-256 device fingerprint.
1414
+ *
1415
+ * ⚠ **Do not supply this on iOS.** Apple prohibits deriving a device identifier
1416
+ * from device signals for attribution (App Store 5.1.2 / DPLA §3.3.9), and it
1417
+ * does not care which side of the network computed it — passing a hash you built
1418
+ * on-device is the same practice. The SDK no longer computes one for you (it used
1419
+ * to, implicitly, on iOS). Left here for the rare Android/self-hosted caller that
1420
+ * has a legitimate deterministic hash; on iOS, omit it.
1421
+ */
1404
1422
  fingerprintHash?: string;
1405
1423
  /** Raw Play Install Referrer. Sendora probes
1406
1424
  * `react-native-play-install-referrer` automatically when this is omitted. */
1407
1425
  installReferrer?: string;
1426
+ /**
1427
+ * Opt in to probabilistic matching on iOS.
1428
+ *
1429
+ * iOS has no deterministic deferred-install signal (no install referrer), and the
1430
+ * SDK will not fingerprint the device. The only remaining matcher is the server's
1431
+ * coarse IP + timestamp window — the same class of practice Apple scrutinises, and
1432
+ * it collides across a shared NAT / iCloud Private Relay. So it is **off by
1433
+ * default**: without this flag, `matchDeferred()` on iOS matches nothing and
1434
+ * returns `null` (route the user through your normal onboarding UX). Set it only
1435
+ * if you accept the accuracy and compliance trade-off. No effect on Android, where
1436
+ * the Play Install Referrer is deterministic.
1437
+ */
1438
+ allowProbabilisticMatch?: boolean;
1408
1439
  iosBundleId?: string;
1409
1440
  androidPackageName?: string;
1410
1441
  }
@@ -1437,12 +1468,14 @@ interface LinkStats {
1437
1468
  */
1438
1469
  declare function extractShortcodeFromUrl(url: string, linkDomain?: string): string | null;
1439
1470
  /**
1440
- * Canonical device-fingerprint recipe. Identical byte-for-byte across the
1441
- * RN / iOS / Android SDKs so a click logged from one platform and an
1442
- * install completing on another won't accidentally match.
1471
+ * Canonical device-fingerprint recipe (`${platform}|${screenW}x${screenH}|${timezone}|${locale}`
1472
+ * → lowercase hex SHA-256), identical across the RN / iOS / Android SDKs.
1443
1473
  *
1444
- * Input: `${platform}|${screenW}x${screenH}|${timezone}|${locale}`
1445
- * Output: lowercase hex SHA-256.
1474
+ * @deprecated ⚠ Do not use on iOS. Apple prohibits deriving a device identifier
1475
+ * from device signals for attribution (App Store 5.1.2 / DPLA §3.3.9), regardless
1476
+ * of ATT — and this is exactly that. `matchDeferred()` no longer calls it
1477
+ * implicitly. It is retained only for an Android/self-hosted caller with a
1478
+ * legitimate deterministic use, and is slated for removal in a future major.
1446
1479
  */
1447
1480
  declare function computeDeviceFingerprint(): Promise<string>;
1448
1481
  /** Probe optional `react-native-play-install-referrer` peer. Returns the
@@ -1511,11 +1544,20 @@ declare class Links {
1511
1544
  */
1512
1545
  handleUniversalLink(url: string): Promise<boolean>;
1513
1546
  /**
1514
- * Cold-launch deferred deep link match. When neither `fingerprintHash` nor
1515
- * `installReferrer` is supplied, the SDK auto-discovers:
1516
- * - Android: probes `react-native-play-install-referrer` (preferred — 100%
1517
- * accurate when present).
1518
- * - Otherwise: computes the canonical device fingerprint hash.
1547
+ * Cold-launch deferred deep-link match.
1548
+ *
1549
+ * ⚠ NO IMPLICIT FINGERPRINTING (compliance). This used to compute a device
1550
+ * fingerprint on iOS when you passed no signal; it no longer does — Apple
1551
+ * prohibits deriving a device identifier from device signals for attribution
1552
+ * (App Store 5.1.2 / DPLA §3.3.9), regardless of ATT, and doing it on-device is
1553
+ * exactly that. Signal resolution now:
1554
+ * - Android: probes `react-native-play-install-referrer` — deterministic, the
1555
+ * preferred and compliant path.
1556
+ * - iOS: no deterministic signal exists (the App Store has no install referrer).
1557
+ * With `allowProbabilisticMatch: true` the server matches on a coarse
1558
+ * IP + time window (OFF by default — see `LinkMatchInput`). Otherwise this
1559
+ * returns `null` and you route the user through your normal onboarding UX —
1560
+ * which is the expected iOS cold-launch outcome, not an error.
1519
1561
  *
1520
1562
  * Fires `onLinkOpened` with `isDeferred: true` on success.
1521
1563
  */
package/dist/index.js CHANGED
@@ -183,7 +183,7 @@ var Storage = class {
183
183
  // package.json
184
184
  var package_default = {
185
185
  name: "@sendoracloud/sdk-react-native",
186
- version: "1.36.0",
186
+ version: "1.38.0",
187
187
  description: "Sendora Cloud React Native + Expo SDK \u2014 analytics, identity, push-token registration, auth (incl. Game Center / Play Games), deep links (Branch / Firebase Dynamic Links parity). Auth + analytics + deep links work in Expo Go; push-token registration and the no-arg Game Center / Play Games sign-in require a Dev Client (EAS Build or `npx expo prebuild`).",
188
188
  type: "module",
189
189
  main: "./dist/index.cjs",
@@ -239,7 +239,7 @@ var package_default = {
239
239
  scripts: {
240
240
  build: "tsup src/index.ts src/contact-widget.tsx --format esm,cjs --dts --clean --external react --external react-native --external react-native-webview",
241
241
  typecheck: "tsc --noEmit",
242
- test: "tsx test/secure-storage.test.ts && tsx test/integration-storage.test.ts && tsx test/plugin.test.ts && tsx test/game-auth.test.ts && tsx test/link.test.ts && tsx test/session-preservation.test.ts && tsx test/credential-collision.test.ts && tsx test/token-expiry.test.ts && tsx test/unlink.test.ts"
242
+ test: "tsx test/secure-storage.test.ts && tsx test/integration-storage.test.ts && tsx test/plugin.test.ts && tsx test/game-auth.test.ts && tsx test/link.test.ts && tsx test/session-preservation.test.ts && tsx test/credential-collision.test.ts && tsx test/token-expiry.test.ts && tsx test/unlink.test.ts && tsx test/deferred-match.test.ts && tsx test/anon-id-stability.test.ts"
243
243
  },
244
244
  devDependencies: {
245
245
  "@types/react": "^19.2.16",
@@ -2111,7 +2111,7 @@ var Auth = class {
2111
2111
  this.hooks.storage.remove(TOKEN_EXPIRES_KEY);
2112
2112
  this.hooks.storage.remove(REFRESH_KEY);
2113
2113
  this.stopProactiveRefreshCron();
2114
- await this.hooks.onAnonymousWipe();
2114
+ await this.hooks.onAnonymousWipe(reason);
2115
2115
  if (reason !== "replaced") {
2116
2116
  this.emitAuthState({ event: "signed_out", user: null, reason });
2117
2117
  }
@@ -2587,33 +2587,31 @@ var Links = class {
2587
2587
  }
2588
2588
  }
2589
2589
  /**
2590
- * Cold-launch deferred deep link match. When neither `fingerprintHash` nor
2591
- * `installReferrer` is supplied, the SDK auto-discovers:
2592
- * - Android: probes `react-native-play-install-referrer` (preferred — 100%
2593
- * accurate when present).
2594
- * - Otherwise: computes the canonical device fingerprint hash.
2590
+ * Cold-launch deferred deep-link match.
2591
+ *
2592
+ * ⚠ NO IMPLICIT FINGERPRINTING (compliance). This used to compute a device
2593
+ * fingerprint on iOS when you passed no signal; it no longer does — Apple
2594
+ * prohibits deriving a device identifier from device signals for attribution
2595
+ * (App Store 5.1.2 / DPLA §3.3.9), regardless of ATT, and doing it on-device is
2596
+ * exactly that. Signal resolution now:
2597
+ * - Android: probes `react-native-play-install-referrer` — deterministic, the
2598
+ * preferred and compliant path.
2599
+ * - iOS: no deterministic signal exists (the App Store has no install referrer).
2600
+ * With `allowProbabilisticMatch: true` the server matches on a coarse
2601
+ * IP + time window (OFF by default — see `LinkMatchInput`). Otherwise this
2602
+ * returns `null` and you route the user through your normal onboarding UX —
2603
+ * which is the expected iOS cold-launch outcome, not an error.
2595
2604
  *
2596
2605
  * Fires `onLinkOpened` with `isDeferred: true` on success.
2597
2606
  */
2598
2607
  async matchDeferred(input = {}) {
2599
- let fingerprintHash = input.fingerprintHash;
2608
+ const fingerprintHash = input.fingerprintHash;
2600
2609
  let installReferrer = input.installReferrer;
2601
2610
  if (!fingerprintHash && !installReferrer) {
2602
2611
  installReferrer = await getPlayInstallReferrer() ?? void 0;
2603
- if (!installReferrer) {
2604
- try {
2605
- fingerprintHash = await computeDeviceFingerprint();
2606
- } catch (err) {
2607
- if (this.deps.debug) console.warn("[sendoracloud] fingerprint compute failed:", err);
2608
- }
2609
- }
2610
- }
2611
- if (!fingerprintHash && !installReferrer) {
2612
- throw new LinkError(
2613
- "INVALID_INPUT",
2614
- "links.matchDeferred: install `react-native-play-install-referrer` for Android, or ensure web crypto / expo-crypto is available for iOS fingerprint compute."
2615
- );
2616
2612
  }
2613
+ const canMatch = !!fingerprintHash || !!installReferrer || input.allowProbabilisticMatch === true;
2614
+ if (!canMatch) return null;
2617
2615
  const body = {};
2618
2616
  if (fingerprintHash) body.fingerprintHash = fingerprintHash;
2619
2617
  if (installReferrer) body.installReferrer = installReferrer;
@@ -3061,9 +3059,11 @@ var SendoraSDK = class {
3061
3059
  if (userId) this.storage.set(USER_ID_KEY, userId);
3062
3060
  else this.storage.remove(USER_ID_KEY);
3063
3061
  },
3064
- onAnonymousWipe: async () => {
3065
- this.anonId = uuid();
3066
- this.storage.set(ANON_KEY, this.anonId);
3062
+ onAnonymousWipe: async (reason) => {
3063
+ if (reason === "user" || reason === "account_deleted") {
3064
+ this.anonId = uuid();
3065
+ this.storage.set(ANON_KEY, this.anonId);
3066
+ }
3067
3067
  this.userId = null;
3068
3068
  this.storage.remove(USER_ID_KEY);
3069
3069
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sendoracloud/sdk-react-native",
3
- "version": "1.36.0",
3
+ "version": "1.38.0",
4
4
  "description": "Sendora Cloud React Native + Expo SDK — analytics, identity, push-token registration, auth (incl. Game Center / Play Games), deep links (Branch / Firebase Dynamic Links parity). Auth + analytics + deep links work in Expo Go; push-token registration and the no-arg Game Center / Play Games sign-in require a Dev Client (EAS Build or `npx expo prebuild`).",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -56,7 +56,7 @@
56
56
  "scripts": {
57
57
  "build": "tsup src/index.ts src/contact-widget.tsx --format esm,cjs --dts --clean --external react --external react-native --external react-native-webview",
58
58
  "typecheck": "tsc --noEmit",
59
- "test": "tsx test/secure-storage.test.ts && tsx test/integration-storage.test.ts && tsx test/plugin.test.ts && tsx test/game-auth.test.ts && tsx test/link.test.ts && tsx test/session-preservation.test.ts && tsx test/credential-collision.test.ts && tsx test/token-expiry.test.ts && tsx test/unlink.test.ts"
59
+ "test": "tsx test/secure-storage.test.ts && tsx test/integration-storage.test.ts && tsx test/plugin.test.ts && tsx test/game-auth.test.ts && tsx test/link.test.ts && tsx test/session-preservation.test.ts && tsx test/credential-collision.test.ts && tsx test/token-expiry.test.ts && tsx test/unlink.test.ts && tsx test/deferred-match.test.ts && tsx test/anon-id-stability.test.ts"
60
60
  },
61
61
  "devDependencies": {
62
62
  "@types/react": "^19.2.16",