@sendoracloud/sdk-react-native 0.19.0 → 0.20.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
@@ -236,6 +236,15 @@ var Auth = class {
236
236
  this.accessExpiresAt = 0;
237
237
  this.inflight = Promise.resolve();
238
238
  this.refreshInflight = null;
239
+ /**
240
+ * Proactive refresh (s58.72). Fires when access-token age crosses
241
+ * ~80% of TTL. Pairs with the AppState foreground listener so a
242
+ * backgrounded app coming back to active state immediately runs
243
+ * a tick — much better UX than waiting 60s for the next interval.
244
+ * Stopped on signOut() / wipeLocalIdentity().
245
+ */
246
+ this.proactiveRefreshTimer = null;
247
+ this.proactiveRefreshAppStateUnsub = null;
239
248
  this.hooks = hooks;
240
249
  this.readyPromise = new Promise((res) => {
241
250
  this.resolveReady = res;
@@ -784,12 +793,25 @@ var Auth = class {
784
793
  */
785
794
  async refreshAccessToken() {
786
795
  if (this.refreshInflight) return this.refreshInflight;
787
- const refresh = this.hooks.storage.get(REFRESH_KEY);
788
- if (!refresh) return null;
796
+ const refreshSnapshot = this.hooks.storage.get(REFRESH_KEY);
797
+ if (!refreshSnapshot) return null;
789
798
  this.refreshInflight = (async () => {
790
799
  try {
800
+ const current = this.hooks.storage.get(REFRESH_KEY);
801
+ if (current && current !== refreshSnapshot) {
802
+ const freshAccess = this.hooks.storage.get(TOKEN_KEY);
803
+ const freshExpiresStr = this.hooks.storage.get(TOKEN_EXPIRES_KEY);
804
+ if (freshAccess && freshExpiresStr) {
805
+ const freshExpires = Number(freshExpiresStr);
806
+ if (Number.isFinite(freshExpires) && freshExpires > Date.now()) {
807
+ this.accessToken = freshAccess;
808
+ this.accessExpiresAt = freshExpires;
809
+ return freshAccess;
810
+ }
811
+ }
812
+ }
791
813
  const data = await this.callAuth("/api/v1/auth-service/token/refresh", {
792
- refreshToken: refresh
814
+ refreshToken: current ?? refreshSnapshot
793
815
  });
794
816
  this.accessToken = data.tokens.accessToken;
795
817
  this.accessExpiresAt = Date.now() + data.tokens.expiresIn * 1e3;
@@ -808,6 +830,46 @@ var Auth = class {
808
830
  })();
809
831
  return this.refreshInflight;
810
832
  }
833
+ startProactiveRefreshCron() {
834
+ if (this.proactiveRefreshTimer) return;
835
+ const PROACTIVE_TARGET_PCT = 0.8;
836
+ const TICK_MS = 6e4;
837
+ const JITTER_MS = 3e4;
838
+ const tick = async () => {
839
+ if (!this.accessToken || !this.accessExpiresAt) return;
840
+ const now = Date.now();
841
+ const lifetimeMs = this.accessExpiresAt - now;
842
+ if (lifetimeMs <= 0) return;
843
+ const guessOriginalMs = Math.max(lifetimeMs, 5 * 6e4);
844
+ const fireWhenRemainingMs = guessOriginalMs * (1 - PROACTIVE_TARGET_PCT) + (Math.random() * JITTER_MS * 2 - JITTER_MS);
845
+ if (lifetimeMs <= fireWhenRemainingMs) {
846
+ await this.refreshAccessToken();
847
+ }
848
+ };
849
+ this.proactiveRefreshTimer = setInterval(() => {
850
+ void tick();
851
+ }, TICK_MS);
852
+ try {
853
+ const RN = require("react-native");
854
+ const subscription = RN.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
  };
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
@@ -1,3 +1,10 @@
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
+
1
8
  // src/index.ts
2
9
  import "react-native-get-random-values";
3
10
 
@@ -193,6 +200,15 @@ var Auth = class {
193
200
  this.accessExpiresAt = 0;
194
201
  this.inflight = Promise.resolve();
195
202
  this.refreshInflight = null;
203
+ /**
204
+ * Proactive refresh (s58.72). Fires when access-token age crosses
205
+ * ~80% of TTL. Pairs with the AppState foreground listener so a
206
+ * backgrounded app coming back to active state immediately runs
207
+ * a tick — much better UX than waiting 60s for the next interval.
208
+ * Stopped on signOut() / wipeLocalIdentity().
209
+ */
210
+ this.proactiveRefreshTimer = null;
211
+ this.proactiveRefreshAppStateUnsub = null;
196
212
  this.hooks = hooks;
197
213
  this.readyPromise = new Promise((res) => {
198
214
  this.resolveReady = res;
@@ -741,12 +757,25 @@ var Auth = class {
741
757
  */
742
758
  async refreshAccessToken() {
743
759
  if (this.refreshInflight) return this.refreshInflight;
744
- const refresh = this.hooks.storage.get(REFRESH_KEY);
745
- if (!refresh) return null;
760
+ const refreshSnapshot = this.hooks.storage.get(REFRESH_KEY);
761
+ if (!refreshSnapshot) return null;
746
762
  this.refreshInflight = (async () => {
747
763
  try {
764
+ const current = this.hooks.storage.get(REFRESH_KEY);
765
+ if (current && current !== refreshSnapshot) {
766
+ const freshAccess = this.hooks.storage.get(TOKEN_KEY);
767
+ const freshExpiresStr = this.hooks.storage.get(TOKEN_EXPIRES_KEY);
768
+ if (freshAccess && freshExpiresStr) {
769
+ const freshExpires = Number(freshExpiresStr);
770
+ if (Number.isFinite(freshExpires) && freshExpires > Date.now()) {
771
+ this.accessToken = freshAccess;
772
+ this.accessExpiresAt = freshExpires;
773
+ return freshAccess;
774
+ }
775
+ }
776
+ }
748
777
  const data = await this.callAuth("/api/v1/auth-service/token/refresh", {
749
- refreshToken: refresh
778
+ refreshToken: current ?? refreshSnapshot
750
779
  });
751
780
  this.accessToken = data.tokens.accessToken;
752
781
  this.accessExpiresAt = Date.now() + data.tokens.expiresIn * 1e3;
@@ -765,6 +794,46 @@ var Auth = class {
765
794
  })();
766
795
  return this.refreshInflight;
767
796
  }
797
+ startProactiveRefreshCron() {
798
+ if (this.proactiveRefreshTimer) return;
799
+ const PROACTIVE_TARGET_PCT = 0.8;
800
+ const TICK_MS = 6e4;
801
+ const JITTER_MS = 3e4;
802
+ const tick = async () => {
803
+ if (!this.accessToken || !this.accessExpiresAt) return;
804
+ const now = Date.now();
805
+ const lifetimeMs = this.accessExpiresAt - now;
806
+ if (lifetimeMs <= 0) return;
807
+ const guessOriginalMs = Math.max(lifetimeMs, 5 * 6e4);
808
+ const fireWhenRemainingMs = guessOriginalMs * (1 - PROACTIVE_TARGET_PCT) + (Math.random() * JITTER_MS * 2 - JITTER_MS);
809
+ if (lifetimeMs <= fireWhenRemainingMs) {
810
+ await this.refreshAccessToken();
811
+ }
812
+ };
813
+ this.proactiveRefreshTimer = setInterval(() => {
814
+ void tick();
815
+ }, TICK_MS);
816
+ try {
817
+ const RN = __require("react-native");
818
+ const subscription = RN.AppState?.addEventListener?.("change", (state) => {
819
+ if (state === "active") void tick();
820
+ });
821
+ if (subscription) {
822
+ this.proactiveRefreshAppStateUnsub = () => subscription.remove();
823
+ }
824
+ } catch {
825
+ }
826
+ }
827
+ stopProactiveRefreshCron() {
828
+ if (this.proactiveRefreshTimer) {
829
+ clearInterval(this.proactiveRefreshTimer);
830
+ this.proactiveRefreshTimer = null;
831
+ }
832
+ if (this.proactiveRefreshAppStateUnsub) {
833
+ this.proactiveRefreshAppStateUnsub();
834
+ this.proactiveRefreshAppStateUnsub = null;
835
+ }
836
+ }
768
837
  /**
769
838
  * Returns the currently-stored refresh token (or null when signed out).
770
839
  * Primary use: capture the anonymous refresh token BEFORE calling
@@ -866,6 +935,7 @@ var Auth = class {
866
935
  this.hooks.storage.set(TOKEN_EXPIRES_KEY, String(this.accessExpiresAt));
867
936
  this.hooks.storage.set(REFRESH_KEY, data.tokens.refreshToken);
868
937
  this.hooks.onIdentityChange(data.user.id);
938
+ this.startProactiveRefreshCron();
869
939
  }
870
940
  async wipeLocalIdentity() {
871
941
  this.user = null;
@@ -875,6 +945,7 @@ var Auth = class {
875
945
  this.hooks.storage.remove(TOKEN_KEY);
876
946
  this.hooks.storage.remove(TOKEN_EXPIRES_KEY);
877
947
  this.hooks.storage.remove(REFRESH_KEY);
948
+ this.stopProactiveRefreshCron();
878
949
  await this.hooks.onAnonymousWipe();
879
950
  }
880
951
  };
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.0",
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",