@sendoracloud/sdk-react-native 0.11.0 → 0.13.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
@@ -719,6 +719,20 @@ var Auth = class {
719
719
  this.inflight = next.catch(() => void 0);
720
720
  return next;
721
721
  }
722
+ /**
723
+ * Hot-path token refresh. Single-flight via `refreshInflight` so a
724
+ * UI batch firing 10 simultaneous reads never produces 10 /refresh
725
+ * round-trips. Returns null on any failure so callers can decide
726
+ * whether to surface a sign-in prompt or proceed anonymously.
727
+ *
728
+ * s58.46 — on a `INVALID_REFRESH_TOKEN` (new backend error code) or
729
+ * HTTP 401 we WIPE the stored refresh token. Pre-s58.46 we caught
730
+ * the error and returned null but kept the stored token intact, so
731
+ * the very next caller would re-attempt the same dead token in an
732
+ * infinite loop (Pulse News iOS hammered /refresh ~1×/s for hours).
733
+ * The wipe forces the next op to fall through to /anonymous or
734
+ * surface NOT_SIGNED_IN to the host app instead of looping.
735
+ */
722
736
  async refreshAccessToken() {
723
737
  if (this.refreshInflight) return this.refreshInflight;
724
738
  const refresh = this.hooks.storage.get(REFRESH_KEY);
@@ -727,14 +741,18 @@ var Auth = class {
727
741
  try {
728
742
  const data = await this.callAuth("/api/v1/auth-service/token/refresh", {
729
743
  refreshToken: refresh
730
- }).catch(() => null);
731
- if (!data) return null;
744
+ });
732
745
  this.accessToken = data.tokens.accessToken;
733
746
  this.accessExpiresAt = Date.now() + data.tokens.expiresIn * 1e3;
734
747
  this.hooks.storage.set(TOKEN_KEY, data.tokens.accessToken);
735
748
  this.hooks.storage.set(TOKEN_EXPIRES_KEY, String(this.accessExpiresAt));
736
749
  this.hooks.storage.set(REFRESH_KEY, data.tokens.refreshToken);
737
750
  return data.tokens.accessToken;
751
+ } catch (err) {
752
+ if (err instanceof AuthError && isDeadRefreshError(err.code)) {
753
+ await this.wipeLocalIdentity().catch(() => void 0);
754
+ }
755
+ return null;
738
756
  } finally {
739
757
  this.refreshInflight = null;
740
758
  }
@@ -785,6 +803,10 @@ var Auth = class {
785
803
  await this.hooks.onAnonymousWipe();
786
804
  }
787
805
  };
806
+ function isDeadRefreshError(code) {
807
+ if (!code) return false;
808
+ return code === "INVALID_REFRESH_TOKEN" || code === "HTTP_401" || code === "UNAUTHORIZED" || code === "RATE_LIMIT_EXCEEDED" || code === "RATE_LIMIT";
809
+ }
788
810
  function decodeJwtPayload(token) {
789
811
  try {
790
812
  const parts = token.split(".");
@@ -845,7 +867,9 @@ function installAutoTrack(args) {
845
867
  if (flags.appBackground) {
846
868
  let AppState = null;
847
869
  try {
848
- AppState = require("react-native").AppState;
870
+ const dyn = globalThis.require;
871
+ const mod = dyn ? dyn("react-native") : null;
872
+ AppState = mod?.AppState ?? null;
849
873
  } catch {
850
874
  AppState = null;
851
875
  }
@@ -1133,6 +1157,28 @@ var SendoraSDK = class {
1133
1157
  }
1134
1158
  return { tokenId: data.tokenId, created: data.created === true };
1135
1159
  }
1160
+ /**
1161
+ * Push namespace — mirrors the iOS / Android / Web SDK shape so
1162
+ * `sendora.push.registerToken(...)` works consistently across all 5 SDKs.
1163
+ * Backed by the same registerPushToken() implementation; kept top-level
1164
+ * too for backwards compatibility with the s47 release.
1165
+ */
1166
+ get push() {
1167
+ const self = this;
1168
+ return {
1169
+ registerToken: (reg) => self.registerPushToken(reg),
1170
+ trackOpen: async (sendId, clickAction) => {
1171
+ if (!self.config) throw new Error("[sendoracloud] init() must complete before push.trackOpen().");
1172
+ const body = { sendId };
1173
+ if (clickAction !== void 0) body.clickAction = clickAction;
1174
+ await post(
1175
+ { apiUrl: self.config.apiUrl, publicKey: self.config.publicKey, debug: self.debug },
1176
+ "/push/track-open",
1177
+ body
1178
+ );
1179
+ }
1180
+ };
1181
+ }
1136
1182
  /**
1137
1183
  * Clear identity + rotate the anonymous id. Call on logout. Awaits
1138
1184
  * the AsyncStorage writes so a caller who kills the app immediately
package/dist/index.d.cts CHANGED
@@ -270,6 +270,20 @@ declare class Auth {
270
270
  */
271
271
  signOut(): Promise<void>;
272
272
  private serialize;
273
+ /**
274
+ * Hot-path token refresh. Single-flight via `refreshInflight` so a
275
+ * UI batch firing 10 simultaneous reads never produces 10 /refresh
276
+ * round-trips. Returns null on any failure so callers can decide
277
+ * whether to surface a sign-in prompt or proceed anonymously.
278
+ *
279
+ * s58.46 — on a `INVALID_REFRESH_TOKEN` (new backend error code) or
280
+ * HTTP 401 we WIPE the stored refresh token. Pre-s58.46 we caught
281
+ * the error and returned null but kept the stored token intact, so
282
+ * the very next caller would re-attempt the same dead token in an
283
+ * infinite loop (Pulse News iOS hammered /refresh ~1×/s for hours).
284
+ * The wipe forces the next op to fall through to /anonymous or
285
+ * surface NOT_SIGNED_IN to the host app instead of looping.
286
+ */
273
287
  private refreshAccessToken;
274
288
  private callAuth;
275
289
  private persist;
@@ -425,6 +439,16 @@ declare class SendoraSDK {
425
439
  * `@react-native-firebase/messaging`.
426
440
  */
427
441
  registerPushToken(reg: PushTokenRegistration): Promise<PushTokenReceipt>;
442
+ /**
443
+ * Push namespace — mirrors the iOS / Android / Web SDK shape so
444
+ * `sendora.push.registerToken(...)` works consistently across all 5 SDKs.
445
+ * Backed by the same registerPushToken() implementation; kept top-level
446
+ * too for backwards compatibility with the s47 release.
447
+ */
448
+ get push(): {
449
+ registerToken: (reg: PushTokenRegistration) => Promise<PushTokenReceipt>;
450
+ trackOpen: (sendId: string, clickAction?: string) => Promise<void>;
451
+ };
428
452
  /**
429
453
  * Clear identity + rotate the anonymous id. Call on logout. Awaits
430
454
  * the AsyncStorage writes so a caller who kills the app immediately
package/dist/index.d.ts CHANGED
@@ -270,6 +270,20 @@ declare class Auth {
270
270
  */
271
271
  signOut(): Promise<void>;
272
272
  private serialize;
273
+ /**
274
+ * Hot-path token refresh. Single-flight via `refreshInflight` so a
275
+ * UI batch firing 10 simultaneous reads never produces 10 /refresh
276
+ * round-trips. Returns null on any failure so callers can decide
277
+ * whether to surface a sign-in prompt or proceed anonymously.
278
+ *
279
+ * s58.46 — on a `INVALID_REFRESH_TOKEN` (new backend error code) or
280
+ * HTTP 401 we WIPE the stored refresh token. Pre-s58.46 we caught
281
+ * the error and returned null but kept the stored token intact, so
282
+ * the very next caller would re-attempt the same dead token in an
283
+ * infinite loop (Pulse News iOS hammered /refresh ~1×/s for hours).
284
+ * The wipe forces the next op to fall through to /anonymous or
285
+ * surface NOT_SIGNED_IN to the host app instead of looping.
286
+ */
273
287
  private refreshAccessToken;
274
288
  private callAuth;
275
289
  private persist;
@@ -425,6 +439,16 @@ declare class SendoraSDK {
425
439
  * `@react-native-firebase/messaging`.
426
440
  */
427
441
  registerPushToken(reg: PushTokenRegistration): Promise<PushTokenReceipt>;
442
+ /**
443
+ * Push namespace — mirrors the iOS / Android / Web SDK shape so
444
+ * `sendora.push.registerToken(...)` works consistently across all 5 SDKs.
445
+ * Backed by the same registerPushToken() implementation; kept top-level
446
+ * too for backwards compatibility with the s47 release.
447
+ */
448
+ get push(): {
449
+ registerToken: (reg: PushTokenRegistration) => Promise<PushTokenReceipt>;
450
+ trackOpen: (sendId: string, clickAction?: string) => Promise<void>;
451
+ };
428
452
  /**
429
453
  * Clear identity + rotate the anonymous id. Call on logout. Awaits
430
454
  * the AsyncStorage writes so a caller who kills the app immediately
package/dist/index.js CHANGED
@@ -1,10 +1,3 @@
1
- var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
2
- get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
3
- }) : x)(function(x) {
4
- if (typeof require !== "undefined") return require.apply(this, arguments);
5
- throw Error('Dynamic require of "' + x + '" is not supported');
6
- });
7
-
8
1
  // src/storage.ts
9
2
  var PREFIX = "sendora_";
10
3
  var asyncStoragePromise = null;
@@ -686,6 +679,20 @@ var Auth = class {
686
679
  this.inflight = next.catch(() => void 0);
687
680
  return next;
688
681
  }
682
+ /**
683
+ * Hot-path token refresh. Single-flight via `refreshInflight` so a
684
+ * UI batch firing 10 simultaneous reads never produces 10 /refresh
685
+ * round-trips. Returns null on any failure so callers can decide
686
+ * whether to surface a sign-in prompt or proceed anonymously.
687
+ *
688
+ * s58.46 — on a `INVALID_REFRESH_TOKEN` (new backend error code) or
689
+ * HTTP 401 we WIPE the stored refresh token. Pre-s58.46 we caught
690
+ * the error and returned null but kept the stored token intact, so
691
+ * the very next caller would re-attempt the same dead token in an
692
+ * infinite loop (Pulse News iOS hammered /refresh ~1×/s for hours).
693
+ * The wipe forces the next op to fall through to /anonymous or
694
+ * surface NOT_SIGNED_IN to the host app instead of looping.
695
+ */
689
696
  async refreshAccessToken() {
690
697
  if (this.refreshInflight) return this.refreshInflight;
691
698
  const refresh = this.hooks.storage.get(REFRESH_KEY);
@@ -694,14 +701,18 @@ var Auth = class {
694
701
  try {
695
702
  const data = await this.callAuth("/api/v1/auth-service/token/refresh", {
696
703
  refreshToken: refresh
697
- }).catch(() => null);
698
- if (!data) return null;
704
+ });
699
705
  this.accessToken = data.tokens.accessToken;
700
706
  this.accessExpiresAt = Date.now() + data.tokens.expiresIn * 1e3;
701
707
  this.hooks.storage.set(TOKEN_KEY, data.tokens.accessToken);
702
708
  this.hooks.storage.set(TOKEN_EXPIRES_KEY, String(this.accessExpiresAt));
703
709
  this.hooks.storage.set(REFRESH_KEY, data.tokens.refreshToken);
704
710
  return data.tokens.accessToken;
711
+ } catch (err) {
712
+ if (err instanceof AuthError && isDeadRefreshError(err.code)) {
713
+ await this.wipeLocalIdentity().catch(() => void 0);
714
+ }
715
+ return null;
705
716
  } finally {
706
717
  this.refreshInflight = null;
707
718
  }
@@ -752,6 +763,10 @@ var Auth = class {
752
763
  await this.hooks.onAnonymousWipe();
753
764
  }
754
765
  };
766
+ function isDeadRefreshError(code) {
767
+ if (!code) return false;
768
+ return code === "INVALID_REFRESH_TOKEN" || code === "HTTP_401" || code === "UNAUTHORIZED" || code === "RATE_LIMIT_EXCEEDED" || code === "RATE_LIMIT";
769
+ }
755
770
  function decodeJwtPayload(token) {
756
771
  try {
757
772
  const parts = token.split(".");
@@ -812,7 +827,9 @@ function installAutoTrack(args) {
812
827
  if (flags.appBackground) {
813
828
  let AppState = null;
814
829
  try {
815
- AppState = __require("react-native").AppState;
830
+ const dyn = globalThis.require;
831
+ const mod = dyn ? dyn("react-native") : null;
832
+ AppState = mod?.AppState ?? null;
816
833
  } catch {
817
834
  AppState = null;
818
835
  }
@@ -1100,6 +1117,28 @@ var SendoraSDK = class {
1100
1117
  }
1101
1118
  return { tokenId: data.tokenId, created: data.created === true };
1102
1119
  }
1120
+ /**
1121
+ * Push namespace — mirrors the iOS / Android / Web SDK shape so
1122
+ * `sendora.push.registerToken(...)` works consistently across all 5 SDKs.
1123
+ * Backed by the same registerPushToken() implementation; kept top-level
1124
+ * too for backwards compatibility with the s47 release.
1125
+ */
1126
+ get push() {
1127
+ const self = this;
1128
+ return {
1129
+ registerToken: (reg) => self.registerPushToken(reg),
1130
+ trackOpen: async (sendId, clickAction) => {
1131
+ if (!self.config) throw new Error("[sendoracloud] init() must complete before push.trackOpen().");
1132
+ const body = { sendId };
1133
+ if (clickAction !== void 0) body.clickAction = clickAction;
1134
+ await post(
1135
+ { apiUrl: self.config.apiUrl, publicKey: self.config.publicKey, debug: self.debug },
1136
+ "/push/track-open",
1137
+ body
1138
+ );
1139
+ }
1140
+ };
1141
+ }
1103
1142
  /**
1104
1143
  * Clear identity + rotate the anonymous id. Call on logout. Awaits
1105
1144
  * the AsyncStorage writes so a caller who kills the app immediately
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sendoracloud/sdk-react-native",
3
- "version": "0.11.0",
3
+ "version": "0.13.0",
4
4
  "description": "Sendora Cloud React Native + Expo SDK — analytics, identity, push-token registration, auth. Expo Go compatible, no native modules beyond AsyncStorage.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",