@sendoracloud/sdk-react-native 0.19.0 → 0.20.1

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
@@ -195,6 +195,7 @@ async function request(opts, method, path, body, extraHeaders) {
195
195
  }
196
196
 
197
197
  // src/auth.ts
198
+ var import_react_native = require("react-native");
198
199
  var TOKEN_KEY = "auth_access_token";
199
200
  var TOKEN_EXPIRES_KEY = "auth_access_expires";
200
201
  var REFRESH_KEY = "auth_refresh_token";
@@ -236,6 +237,15 @@ var Auth = class {
236
237
  this.accessExpiresAt = 0;
237
238
  this.inflight = Promise.resolve();
238
239
  this.refreshInflight = null;
240
+ /**
241
+ * Proactive refresh (s58.72). Fires when access-token age crosses
242
+ * ~80% of TTL. Pairs with the AppState foreground listener so a
243
+ * backgrounded app coming back to active state immediately runs
244
+ * a tick — much better UX than waiting 60s for the next interval.
245
+ * Stopped on signOut() / wipeLocalIdentity().
246
+ */
247
+ this.proactiveRefreshTimer = null;
248
+ this.proactiveRefreshAppStateUnsub = null;
239
249
  this.hooks = hooks;
240
250
  this.readyPromise = new Promise((res) => {
241
251
  this.resolveReady = res;
@@ -784,12 +794,25 @@ var Auth = class {
784
794
  */
785
795
  async refreshAccessToken() {
786
796
  if (this.refreshInflight) return this.refreshInflight;
787
- const refresh = this.hooks.storage.get(REFRESH_KEY);
788
- if (!refresh) return null;
797
+ const refreshSnapshot = this.hooks.storage.get(REFRESH_KEY);
798
+ if (!refreshSnapshot) return null;
789
799
  this.refreshInflight = (async () => {
790
800
  try {
801
+ const current = this.hooks.storage.get(REFRESH_KEY);
802
+ if (current && current !== refreshSnapshot) {
803
+ const freshAccess = this.hooks.storage.get(TOKEN_KEY);
804
+ const freshExpiresStr = this.hooks.storage.get(TOKEN_EXPIRES_KEY);
805
+ if (freshAccess && freshExpiresStr) {
806
+ const freshExpires = Number(freshExpiresStr);
807
+ if (Number.isFinite(freshExpires) && freshExpires > Date.now()) {
808
+ this.accessToken = freshAccess;
809
+ this.accessExpiresAt = freshExpires;
810
+ return freshAccess;
811
+ }
812
+ }
813
+ }
791
814
  const data = await this.callAuth("/api/v1/auth-service/token/refresh", {
792
- refreshToken: refresh
815
+ refreshToken: current ?? refreshSnapshot
793
816
  });
794
817
  this.accessToken = data.tokens.accessToken;
795
818
  this.accessExpiresAt = Date.now() + data.tokens.expiresIn * 1e3;
@@ -808,6 +831,45 @@ var Auth = class {
808
831
  })();
809
832
  return this.refreshInflight;
810
833
  }
834
+ startProactiveRefreshCron() {
835
+ if (this.proactiveRefreshTimer) return;
836
+ const PROACTIVE_TARGET_PCT = 0.8;
837
+ const TICK_MS = 6e4;
838
+ const JITTER_MS = 3e4;
839
+ const tick = async () => {
840
+ if (!this.accessToken || !this.accessExpiresAt) return;
841
+ const now = Date.now();
842
+ const lifetimeMs = this.accessExpiresAt - now;
843
+ if (lifetimeMs <= 0) return;
844
+ const guessOriginalMs = Math.max(lifetimeMs, 5 * 6e4);
845
+ const fireWhenRemainingMs = guessOriginalMs * (1 - PROACTIVE_TARGET_PCT) + (Math.random() * JITTER_MS * 2 - JITTER_MS);
846
+ if (lifetimeMs <= fireWhenRemainingMs) {
847
+ await this.refreshAccessToken();
848
+ }
849
+ };
850
+ this.proactiveRefreshTimer = setInterval(() => {
851
+ void tick();
852
+ }, TICK_MS);
853
+ try {
854
+ const subscription = import_react_native.AppState?.addEventListener?.("change", (state) => {
855
+ if (state === "active") void tick();
856
+ });
857
+ if (subscription) {
858
+ this.proactiveRefreshAppStateUnsub = () => subscription.remove();
859
+ }
860
+ } catch {
861
+ }
862
+ }
863
+ stopProactiveRefreshCron() {
864
+ if (this.proactiveRefreshTimer) {
865
+ clearInterval(this.proactiveRefreshTimer);
866
+ this.proactiveRefreshTimer = null;
867
+ }
868
+ if (this.proactiveRefreshAppStateUnsub) {
869
+ this.proactiveRefreshAppStateUnsub();
870
+ this.proactiveRefreshAppStateUnsub = null;
871
+ }
872
+ }
811
873
  /**
812
874
  * Returns the currently-stored refresh token (or null when signed out).
813
875
  * Primary use: capture the anonymous refresh token BEFORE calling
@@ -909,6 +971,7 @@ var Auth = class {
909
971
  this.hooks.storage.set(TOKEN_EXPIRES_KEY, String(this.accessExpiresAt));
910
972
  this.hooks.storage.set(REFRESH_KEY, data.tokens.refreshToken);
911
973
  this.hooks.onIdentityChange(data.user.id);
974
+ this.startProactiveRefreshCron();
912
975
  }
913
976
  async wipeLocalIdentity() {
914
977
  this.user = null;
@@ -918,6 +981,7 @@ var Auth = class {
918
981
  this.hooks.storage.remove(TOKEN_KEY);
919
982
  this.hooks.storage.remove(TOKEN_EXPIRES_KEY);
920
983
  this.hooks.storage.remove(REFRESH_KEY);
984
+ this.stopProactiveRefreshCron();
921
985
  await this.hooks.onAnonymousWipe();
922
986
  }
923
987
  };
@@ -940,7 +1004,7 @@ function decodeJwtPayload(token) {
940
1004
  }
941
1005
 
942
1006
  // src/links.ts
943
- var import_react_native = require("react-native");
1007
+ var import_react_native2 = require("react-native");
944
1008
  var LinkError = class extends Error {
945
1009
  constructor(code, message, statusCode = 0, details) {
946
1010
  super(message);
@@ -1019,10 +1083,10 @@ function dynRequire(id) {
1019
1083
  }
1020
1084
  function loadRn() {
1021
1085
  return {
1022
- Platform: import_react_native.Platform,
1023
- Dimensions: import_react_native.Dimensions,
1024
- NativeModules: import_react_native.NativeModules,
1025
- Linking: import_react_native.Linking
1086
+ Platform: import_react_native2.Platform,
1087
+ Dimensions: import_react_native2.Dimensions,
1088
+ NativeModules: import_react_native2.NativeModules,
1089
+ Linking: import_react_native2.Linking
1026
1090
  };
1027
1091
  }
1028
1092
  function readTimezone() {
@@ -1504,7 +1568,7 @@ var Links = class {
1504
1568
  };
1505
1569
 
1506
1570
  // src/auto-track.ts
1507
- var import_react_native2 = require("react-native");
1571
+ var import_react_native3 = require("react-native");
1508
1572
  var DEFAULT_IDLE_MS = 30 * 60 * 1e3;
1509
1573
  function resolveFlags(cfg) {
1510
1574
  if (cfg === false) {
@@ -1548,7 +1612,7 @@ function installAutoTrack(args) {
1548
1612
  args.fire("app.opened", { sessionId });
1549
1613
  }
1550
1614
  if (flags.appBackground) {
1551
- const AppState = import_react_native2.AppState;
1615
+ const AppState = import_react_native3.AppState;
1552
1616
  if (AppState && typeof AppState.addEventListener === "function") {
1553
1617
  const handler = (state) => {
1554
1618
  ensureSession(true);
package/dist/index.d.cts CHANGED
@@ -312,6 +312,17 @@ declare class Auth {
312
312
  * surface NOT_SIGNED_IN to the host app instead of looping.
313
313
  */
314
314
  private refreshAccessToken;
315
+ /**
316
+ * Proactive refresh (s58.72). Fires when access-token age crosses
317
+ * ~80% of TTL. Pairs with the AppState foreground listener so a
318
+ * backgrounded app coming back to active state immediately runs
319
+ * a tick — much better UX than waiting 60s for the next interval.
320
+ * Stopped on signOut() / wipeLocalIdentity().
321
+ */
322
+ private proactiveRefreshTimer;
323
+ private proactiveRefreshAppStateUnsub;
324
+ private startProactiveRefreshCron;
325
+ private stopProactiveRefreshCron;
315
326
  /**
316
327
  * Returns the currently-stored refresh token (or null when signed out).
317
328
  * Primary use: capture the anonymous refresh token BEFORE calling
package/dist/index.d.ts CHANGED
@@ -312,6 +312,17 @@ declare class Auth {
312
312
  * surface NOT_SIGNED_IN to the host app instead of looping.
313
313
  */
314
314
  private refreshAccessToken;
315
+ /**
316
+ * Proactive refresh (s58.72). Fires when access-token age crosses
317
+ * ~80% of TTL. Pairs with the AppState foreground listener so a
318
+ * backgrounded app coming back to active state immediately runs
319
+ * a tick — much better UX than waiting 60s for the next interval.
320
+ * Stopped on signOut() / wipeLocalIdentity().
321
+ */
322
+ private proactiveRefreshTimer;
323
+ private proactiveRefreshAppStateUnsub;
324
+ private startProactiveRefreshCron;
325
+ private stopProactiveRefreshCron;
315
326
  /**
316
327
  * Returns the currently-stored refresh token (or null when signed out).
317
328
  * Primary use: capture the anonymous refresh token BEFORE calling
package/dist/index.js CHANGED
@@ -152,6 +152,7 @@ async function request(opts, method, path, body, extraHeaders) {
152
152
  }
153
153
 
154
154
  // src/auth.ts
155
+ import { AppState as RnAppState } from "react-native";
155
156
  var TOKEN_KEY = "auth_access_token";
156
157
  var TOKEN_EXPIRES_KEY = "auth_access_expires";
157
158
  var REFRESH_KEY = "auth_refresh_token";
@@ -193,6 +194,15 @@ var Auth = class {
193
194
  this.accessExpiresAt = 0;
194
195
  this.inflight = Promise.resolve();
195
196
  this.refreshInflight = null;
197
+ /**
198
+ * Proactive refresh (s58.72). Fires when access-token age crosses
199
+ * ~80% of TTL. Pairs with the AppState foreground listener so a
200
+ * backgrounded app coming back to active state immediately runs
201
+ * a tick — much better UX than waiting 60s for the next interval.
202
+ * Stopped on signOut() / wipeLocalIdentity().
203
+ */
204
+ this.proactiveRefreshTimer = null;
205
+ this.proactiveRefreshAppStateUnsub = null;
196
206
  this.hooks = hooks;
197
207
  this.readyPromise = new Promise((res) => {
198
208
  this.resolveReady = res;
@@ -741,12 +751,25 @@ var Auth = class {
741
751
  */
742
752
  async refreshAccessToken() {
743
753
  if (this.refreshInflight) return this.refreshInflight;
744
- const refresh = this.hooks.storage.get(REFRESH_KEY);
745
- if (!refresh) return null;
754
+ const refreshSnapshot = this.hooks.storage.get(REFRESH_KEY);
755
+ if (!refreshSnapshot) return null;
746
756
  this.refreshInflight = (async () => {
747
757
  try {
758
+ const current = this.hooks.storage.get(REFRESH_KEY);
759
+ if (current && current !== refreshSnapshot) {
760
+ const freshAccess = this.hooks.storage.get(TOKEN_KEY);
761
+ const freshExpiresStr = this.hooks.storage.get(TOKEN_EXPIRES_KEY);
762
+ if (freshAccess && freshExpiresStr) {
763
+ const freshExpires = Number(freshExpiresStr);
764
+ if (Number.isFinite(freshExpires) && freshExpires > Date.now()) {
765
+ this.accessToken = freshAccess;
766
+ this.accessExpiresAt = freshExpires;
767
+ return freshAccess;
768
+ }
769
+ }
770
+ }
748
771
  const data = await this.callAuth("/api/v1/auth-service/token/refresh", {
749
- refreshToken: refresh
772
+ refreshToken: current ?? refreshSnapshot
750
773
  });
751
774
  this.accessToken = data.tokens.accessToken;
752
775
  this.accessExpiresAt = Date.now() + data.tokens.expiresIn * 1e3;
@@ -765,6 +788,45 @@ var Auth = class {
765
788
  })();
766
789
  return this.refreshInflight;
767
790
  }
791
+ startProactiveRefreshCron() {
792
+ if (this.proactiveRefreshTimer) return;
793
+ const PROACTIVE_TARGET_PCT = 0.8;
794
+ const TICK_MS = 6e4;
795
+ const JITTER_MS = 3e4;
796
+ const tick = async () => {
797
+ if (!this.accessToken || !this.accessExpiresAt) return;
798
+ const now = Date.now();
799
+ const lifetimeMs = this.accessExpiresAt - now;
800
+ if (lifetimeMs <= 0) return;
801
+ const guessOriginalMs = Math.max(lifetimeMs, 5 * 6e4);
802
+ const fireWhenRemainingMs = guessOriginalMs * (1 - PROACTIVE_TARGET_PCT) + (Math.random() * JITTER_MS * 2 - JITTER_MS);
803
+ if (lifetimeMs <= fireWhenRemainingMs) {
804
+ await this.refreshAccessToken();
805
+ }
806
+ };
807
+ this.proactiveRefreshTimer = setInterval(() => {
808
+ void tick();
809
+ }, TICK_MS);
810
+ try {
811
+ const subscription = RnAppState?.addEventListener?.("change", (state) => {
812
+ if (state === "active") void tick();
813
+ });
814
+ if (subscription) {
815
+ this.proactiveRefreshAppStateUnsub = () => subscription.remove();
816
+ }
817
+ } catch {
818
+ }
819
+ }
820
+ stopProactiveRefreshCron() {
821
+ if (this.proactiveRefreshTimer) {
822
+ clearInterval(this.proactiveRefreshTimer);
823
+ this.proactiveRefreshTimer = null;
824
+ }
825
+ if (this.proactiveRefreshAppStateUnsub) {
826
+ this.proactiveRefreshAppStateUnsub();
827
+ this.proactiveRefreshAppStateUnsub = null;
828
+ }
829
+ }
768
830
  /**
769
831
  * Returns the currently-stored refresh token (or null when signed out).
770
832
  * Primary use: capture the anonymous refresh token BEFORE calling
@@ -866,6 +928,7 @@ var Auth = class {
866
928
  this.hooks.storage.set(TOKEN_EXPIRES_KEY, String(this.accessExpiresAt));
867
929
  this.hooks.storage.set(REFRESH_KEY, data.tokens.refreshToken);
868
930
  this.hooks.onIdentityChange(data.user.id);
931
+ this.startProactiveRefreshCron();
869
932
  }
870
933
  async wipeLocalIdentity() {
871
934
  this.user = null;
@@ -875,6 +938,7 @@ var Auth = class {
875
938
  this.hooks.storage.remove(TOKEN_KEY);
876
939
  this.hooks.storage.remove(TOKEN_EXPIRES_KEY);
877
940
  this.hooks.storage.remove(REFRESH_KEY);
941
+ this.stopProactiveRefreshCron();
878
942
  await this.hooks.onAnonymousWipe();
879
943
  }
880
944
  };
@@ -1466,7 +1530,7 @@ var Links = class {
1466
1530
  };
1467
1531
 
1468
1532
  // src/auto-track.ts
1469
- import { AppState as RnAppState } from "react-native";
1533
+ import { AppState as RnAppState2 } from "react-native";
1470
1534
  var DEFAULT_IDLE_MS = 30 * 60 * 1e3;
1471
1535
  function resolveFlags(cfg) {
1472
1536
  if (cfg === false) {
@@ -1510,7 +1574,7 @@ function installAutoTrack(args) {
1510
1574
  args.fire("app.opened", { sessionId });
1511
1575
  }
1512
1576
  if (flags.appBackground) {
1513
- const AppState = RnAppState;
1577
+ const AppState = RnAppState2;
1514
1578
  if (AppState && typeof AppState.addEventListener === "function") {
1515
1579
  const handler = (state) => {
1516
1580
  ensureSession(true);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sendoracloud/sdk-react-native",
3
- "version": "0.19.0",
3
+ "version": "0.20.1",
4
4
  "description": "Sendora Cloud React Native + Expo SDK — analytics, identity, push-token registration, auth, deep links (Branch / Firebase Dynamic Links parity). Auth + analytics + deep links work in Expo Go; push token registration requires a Dev Client (EAS Build or `npx expo prebuild`).",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",