@sendoracloud/sdk-react-native 0.18.9 → 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 +157 -3
- package/dist/index.d.cts +55 -0
- package/dist/index.d.ts +55 -0
- package/dist/index.js +164 -3
- package/package.json +1 -1
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
|
|
788
|
-
if (!
|
|
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:
|
|
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
|
};
|
|
@@ -1858,6 +1922,14 @@ var SendoraSDK = class {
|
|
|
1858
1922
|
* `sendora.push.registerToken(...)` works consistently across all 5 SDKs.
|
|
1859
1923
|
* Backed by the same registerPushToken() implementation; kept top-level
|
|
1860
1924
|
* too for backwards compatibility with the s47 release.
|
|
1925
|
+
*
|
|
1926
|
+
* Tier 3 (s58.70): added `liveActivities.startToken()` and
|
|
1927
|
+
* `geofences.{list,recordEvent}()`. These are JS-side shims around
|
|
1928
|
+
* the backend wire calls — the native bridge work (ActivityKit on
|
|
1929
|
+
* iOS, CoreLocation geofencing, Android NotificationCompat + Geo
|
|
1930
|
+
* APIs) still lives in your app code. Once your native side has
|
|
1931
|
+
* the activity push token / geofence trigger in hand, hand it to
|
|
1932
|
+
* the SDK and we'll persist + dispatch server-side.
|
|
1861
1933
|
*/
|
|
1862
1934
|
get push() {
|
|
1863
1935
|
const self = this;
|
|
@@ -1872,6 +1944,88 @@ var SendoraSDK = class {
|
|
|
1872
1944
|
"/push/track-open",
|
|
1873
1945
|
body
|
|
1874
1946
|
);
|
|
1947
|
+
},
|
|
1948
|
+
liveActivities: {
|
|
1949
|
+
/**
|
|
1950
|
+
* Report a freshly-started iOS Live Activity (or Android Live
|
|
1951
|
+
* Update on Android 14+) push token to Sendora so server-side
|
|
1952
|
+
* `PATCH /push/live-activities/:id/update` calls can dispatch
|
|
1953
|
+
* via APNs (iOS) or FCM data-only (Android).
|
|
1954
|
+
*
|
|
1955
|
+
* Host app calls this once per Activity lifecycle, right
|
|
1956
|
+
* after `Activity.pushTokenUpdates` (iOS) or your Android
|
|
1957
|
+
* NotificationCompat setup hands you the push token.
|
|
1958
|
+
*/
|
|
1959
|
+
startToken: async (input) => {
|
|
1960
|
+
if (!self.config) throw new Error("[sendoracloud] init() must complete before push.liveActivities.startToken().");
|
|
1961
|
+
const res = await post(
|
|
1962
|
+
{ apiUrl: self.config.apiUrl, publicKey: self.config.publicKey, debug: self.debug },
|
|
1963
|
+
"/push/live-activities/start-token",
|
|
1964
|
+
{
|
|
1965
|
+
pushToken: input.pushToken,
|
|
1966
|
+
activityType: input.activityType,
|
|
1967
|
+
platform: input.platform ?? "ios",
|
|
1968
|
+
externalId: input.externalId,
|
|
1969
|
+
userId: input.userId,
|
|
1970
|
+
attributes: input.attributes ?? {},
|
|
1971
|
+
contentState: input.contentState ?? {},
|
|
1972
|
+
bundleId: input.bundleId
|
|
1973
|
+
}
|
|
1974
|
+
);
|
|
1975
|
+
if (!res || !res.ok) {
|
|
1976
|
+
throw new Error(`[sendoracloud] live-activity start-token failed (HTTP ${res?.status ?? "network"}).`);
|
|
1977
|
+
}
|
|
1978
|
+
const parsed = await res.json();
|
|
1979
|
+
const data = parsed?.data;
|
|
1980
|
+
if (!data?.id) throw new Error("[sendoracloud] live-activity start-token: server returned no id.");
|
|
1981
|
+
return { id: data.id, status: data.status ?? "active" };
|
|
1982
|
+
}
|
|
1983
|
+
},
|
|
1984
|
+
geofences: {
|
|
1985
|
+
/**
|
|
1986
|
+
* Fetch the enabled geofences for this device. The SDK doesn't
|
|
1987
|
+
* register them with the OS — that's app-side native work
|
|
1988
|
+
* (CLLocationManager.startMonitoring on iOS, GeofencingClient
|
|
1989
|
+
* on Android). Sort order is server-controlled by priority
|
|
1990
|
+
* asc so the caller can clamp to the OS cap (iOS 20, Android
|
|
1991
|
+
* 100) by truncating the list.
|
|
1992
|
+
*/
|
|
1993
|
+
list: async () => {
|
|
1994
|
+
if (!self.config) throw new Error("[sendoracloud] init() must complete before push.geofences.list().");
|
|
1995
|
+
const res = await getJson(
|
|
1996
|
+
{ apiUrl: self.config.apiUrl, publicKey: self.config.publicKey, debug: self.debug },
|
|
1997
|
+
"/push/geofences/list-for-device"
|
|
1998
|
+
);
|
|
1999
|
+
if (!res || !res.ok) {
|
|
2000
|
+
throw new Error(`[sendoracloud] geofences list failed (HTTP ${res?.status ?? "network"}).`);
|
|
2001
|
+
}
|
|
2002
|
+
const parsed = await res.json();
|
|
2003
|
+
return parsed?.data ?? [];
|
|
2004
|
+
},
|
|
2005
|
+
/**
|
|
2006
|
+
* Report a geofence enter / exit / dwell event. Backend writes
|
|
2007
|
+
* a `geofence.<kind>` event row + fires any workflows triggered
|
|
2008
|
+
* on that geofence id. Call from the native handler that
|
|
2009
|
+
* receives the OS region notification.
|
|
2010
|
+
*/
|
|
2011
|
+
recordEvent: async (input) => {
|
|
2012
|
+
if (!self.config) throw new Error("[sendoracloud] init() must complete before push.geofences.recordEvent().");
|
|
2013
|
+
const res = await post(
|
|
2014
|
+
{ apiUrl: self.config.apiUrl, publicKey: self.config.publicKey, debug: self.debug },
|
|
2015
|
+
"/push/geofences/event",
|
|
2016
|
+
{
|
|
2017
|
+
geofenceId: input.geofenceId,
|
|
2018
|
+
kind: input.kind,
|
|
2019
|
+
lat: input.lat,
|
|
2020
|
+
lng: input.lng,
|
|
2021
|
+
accuracyMeters: input.accuracyMeters,
|
|
2022
|
+
dwellSeconds: input.dwellSeconds
|
|
2023
|
+
}
|
|
2024
|
+
);
|
|
2025
|
+
if (!res || !res.ok) {
|
|
2026
|
+
throw new Error(`[sendoracloud] geofence event failed (HTTP ${res?.status ?? "network"}).`);
|
|
2027
|
+
}
|
|
2028
|
+
}
|
|
1875
2029
|
}
|
|
1876
2030
|
};
|
|
1877
2031
|
}
|
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
|
|
@@ -759,10 +770,54 @@ declare class SendoraSDK {
|
|
|
759
770
|
* `sendora.push.registerToken(...)` works consistently across all 5 SDKs.
|
|
760
771
|
* Backed by the same registerPushToken() implementation; kept top-level
|
|
761
772
|
* too for backwards compatibility with the s47 release.
|
|
773
|
+
*
|
|
774
|
+
* Tier 3 (s58.70): added `liveActivities.startToken()` and
|
|
775
|
+
* `geofences.{list,recordEvent}()`. These are JS-side shims around
|
|
776
|
+
* the backend wire calls — the native bridge work (ActivityKit on
|
|
777
|
+
* iOS, CoreLocation geofencing, Android NotificationCompat + Geo
|
|
778
|
+
* APIs) still lives in your app code. Once your native side has
|
|
779
|
+
* the activity push token / geofence trigger in hand, hand it to
|
|
780
|
+
* the SDK and we'll persist + dispatch server-side.
|
|
762
781
|
*/
|
|
763
782
|
get push(): {
|
|
764
783
|
registerToken: (reg: PushTokenRegistration) => Promise<PushTokenReceipt>;
|
|
765
784
|
trackOpen: (sendId: string, clickAction?: string) => Promise<void>;
|
|
785
|
+
liveActivities: {
|
|
786
|
+
startToken: (input: {
|
|
787
|
+
pushToken: string;
|
|
788
|
+
activityType: string;
|
|
789
|
+
platform?: "ios" | "android";
|
|
790
|
+
externalId?: string;
|
|
791
|
+
userId?: string;
|
|
792
|
+
attributes?: Record<string, unknown>;
|
|
793
|
+
contentState?: Record<string, unknown>;
|
|
794
|
+
bundleId?: string;
|
|
795
|
+
}) => Promise<{
|
|
796
|
+
id: string;
|
|
797
|
+
status: string;
|
|
798
|
+
}>;
|
|
799
|
+
};
|
|
800
|
+
geofences: {
|
|
801
|
+
list: () => Promise<Array<{
|
|
802
|
+
id: string;
|
|
803
|
+
name: string;
|
|
804
|
+
lat: number;
|
|
805
|
+
lng: number;
|
|
806
|
+
radiusMeters: number;
|
|
807
|
+
priority: number;
|
|
808
|
+
enabled: boolean;
|
|
809
|
+
triggerOn: string[];
|
|
810
|
+
properties?: Record<string, unknown>;
|
|
811
|
+
}>>;
|
|
812
|
+
recordEvent: (input: {
|
|
813
|
+
geofenceId: string;
|
|
814
|
+
kind: "enter" | "exit" | "dwell";
|
|
815
|
+
lat?: number;
|
|
816
|
+
lng?: number;
|
|
817
|
+
accuracyMeters?: number;
|
|
818
|
+
dwellSeconds?: number;
|
|
819
|
+
}) => Promise<void>;
|
|
820
|
+
};
|
|
766
821
|
};
|
|
767
822
|
/**
|
|
768
823
|
* Attribution namespace — mobile install + deferred deep-link reporting.
|
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
|
|
@@ -759,10 +770,54 @@ declare class SendoraSDK {
|
|
|
759
770
|
* `sendora.push.registerToken(...)` works consistently across all 5 SDKs.
|
|
760
771
|
* Backed by the same registerPushToken() implementation; kept top-level
|
|
761
772
|
* too for backwards compatibility with the s47 release.
|
|
773
|
+
*
|
|
774
|
+
* Tier 3 (s58.70): added `liveActivities.startToken()` and
|
|
775
|
+
* `geofences.{list,recordEvent}()`. These are JS-side shims around
|
|
776
|
+
* the backend wire calls — the native bridge work (ActivityKit on
|
|
777
|
+
* iOS, CoreLocation geofencing, Android NotificationCompat + Geo
|
|
778
|
+
* APIs) still lives in your app code. Once your native side has
|
|
779
|
+
* the activity push token / geofence trigger in hand, hand it to
|
|
780
|
+
* the SDK and we'll persist + dispatch server-side.
|
|
762
781
|
*/
|
|
763
782
|
get push(): {
|
|
764
783
|
registerToken: (reg: PushTokenRegistration) => Promise<PushTokenReceipt>;
|
|
765
784
|
trackOpen: (sendId: string, clickAction?: string) => Promise<void>;
|
|
785
|
+
liveActivities: {
|
|
786
|
+
startToken: (input: {
|
|
787
|
+
pushToken: string;
|
|
788
|
+
activityType: string;
|
|
789
|
+
platform?: "ios" | "android";
|
|
790
|
+
externalId?: string;
|
|
791
|
+
userId?: string;
|
|
792
|
+
attributes?: Record<string, unknown>;
|
|
793
|
+
contentState?: Record<string, unknown>;
|
|
794
|
+
bundleId?: string;
|
|
795
|
+
}) => Promise<{
|
|
796
|
+
id: string;
|
|
797
|
+
status: string;
|
|
798
|
+
}>;
|
|
799
|
+
};
|
|
800
|
+
geofences: {
|
|
801
|
+
list: () => Promise<Array<{
|
|
802
|
+
id: string;
|
|
803
|
+
name: string;
|
|
804
|
+
lat: number;
|
|
805
|
+
lng: number;
|
|
806
|
+
radiusMeters: number;
|
|
807
|
+
priority: number;
|
|
808
|
+
enabled: boolean;
|
|
809
|
+
triggerOn: string[];
|
|
810
|
+
properties?: Record<string, unknown>;
|
|
811
|
+
}>>;
|
|
812
|
+
recordEvent: (input: {
|
|
813
|
+
geofenceId: string;
|
|
814
|
+
kind: "enter" | "exit" | "dwell";
|
|
815
|
+
lat?: number;
|
|
816
|
+
lng?: number;
|
|
817
|
+
accuracyMeters?: number;
|
|
818
|
+
dwellSeconds?: number;
|
|
819
|
+
}) => Promise<void>;
|
|
820
|
+
};
|
|
766
821
|
};
|
|
767
822
|
/**
|
|
768
823
|
* Attribution namespace — mobile install + deferred deep-link reporting.
|
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
|
|
745
|
-
if (!
|
|
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:
|
|
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
|
};
|
|
@@ -1820,6 +1891,14 @@ var SendoraSDK = class {
|
|
|
1820
1891
|
* `sendora.push.registerToken(...)` works consistently across all 5 SDKs.
|
|
1821
1892
|
* Backed by the same registerPushToken() implementation; kept top-level
|
|
1822
1893
|
* too for backwards compatibility with the s47 release.
|
|
1894
|
+
*
|
|
1895
|
+
* Tier 3 (s58.70): added `liveActivities.startToken()` and
|
|
1896
|
+
* `geofences.{list,recordEvent}()`. These are JS-side shims around
|
|
1897
|
+
* the backend wire calls — the native bridge work (ActivityKit on
|
|
1898
|
+
* iOS, CoreLocation geofencing, Android NotificationCompat + Geo
|
|
1899
|
+
* APIs) still lives in your app code. Once your native side has
|
|
1900
|
+
* the activity push token / geofence trigger in hand, hand it to
|
|
1901
|
+
* the SDK and we'll persist + dispatch server-side.
|
|
1823
1902
|
*/
|
|
1824
1903
|
get push() {
|
|
1825
1904
|
const self = this;
|
|
@@ -1834,6 +1913,88 @@ var SendoraSDK = class {
|
|
|
1834
1913
|
"/push/track-open",
|
|
1835
1914
|
body
|
|
1836
1915
|
);
|
|
1916
|
+
},
|
|
1917
|
+
liveActivities: {
|
|
1918
|
+
/**
|
|
1919
|
+
* Report a freshly-started iOS Live Activity (or Android Live
|
|
1920
|
+
* Update on Android 14+) push token to Sendora so server-side
|
|
1921
|
+
* `PATCH /push/live-activities/:id/update` calls can dispatch
|
|
1922
|
+
* via APNs (iOS) or FCM data-only (Android).
|
|
1923
|
+
*
|
|
1924
|
+
* Host app calls this once per Activity lifecycle, right
|
|
1925
|
+
* after `Activity.pushTokenUpdates` (iOS) or your Android
|
|
1926
|
+
* NotificationCompat setup hands you the push token.
|
|
1927
|
+
*/
|
|
1928
|
+
startToken: async (input) => {
|
|
1929
|
+
if (!self.config) throw new Error("[sendoracloud] init() must complete before push.liveActivities.startToken().");
|
|
1930
|
+
const res = await post(
|
|
1931
|
+
{ apiUrl: self.config.apiUrl, publicKey: self.config.publicKey, debug: self.debug },
|
|
1932
|
+
"/push/live-activities/start-token",
|
|
1933
|
+
{
|
|
1934
|
+
pushToken: input.pushToken,
|
|
1935
|
+
activityType: input.activityType,
|
|
1936
|
+
platform: input.platform ?? "ios",
|
|
1937
|
+
externalId: input.externalId,
|
|
1938
|
+
userId: input.userId,
|
|
1939
|
+
attributes: input.attributes ?? {},
|
|
1940
|
+
contentState: input.contentState ?? {},
|
|
1941
|
+
bundleId: input.bundleId
|
|
1942
|
+
}
|
|
1943
|
+
);
|
|
1944
|
+
if (!res || !res.ok) {
|
|
1945
|
+
throw new Error(`[sendoracloud] live-activity start-token failed (HTTP ${res?.status ?? "network"}).`);
|
|
1946
|
+
}
|
|
1947
|
+
const parsed = await res.json();
|
|
1948
|
+
const data = parsed?.data;
|
|
1949
|
+
if (!data?.id) throw new Error("[sendoracloud] live-activity start-token: server returned no id.");
|
|
1950
|
+
return { id: data.id, status: data.status ?? "active" };
|
|
1951
|
+
}
|
|
1952
|
+
},
|
|
1953
|
+
geofences: {
|
|
1954
|
+
/**
|
|
1955
|
+
* Fetch the enabled geofences for this device. The SDK doesn't
|
|
1956
|
+
* register them with the OS — that's app-side native work
|
|
1957
|
+
* (CLLocationManager.startMonitoring on iOS, GeofencingClient
|
|
1958
|
+
* on Android). Sort order is server-controlled by priority
|
|
1959
|
+
* asc so the caller can clamp to the OS cap (iOS 20, Android
|
|
1960
|
+
* 100) by truncating the list.
|
|
1961
|
+
*/
|
|
1962
|
+
list: async () => {
|
|
1963
|
+
if (!self.config) throw new Error("[sendoracloud] init() must complete before push.geofences.list().");
|
|
1964
|
+
const res = await getJson(
|
|
1965
|
+
{ apiUrl: self.config.apiUrl, publicKey: self.config.publicKey, debug: self.debug },
|
|
1966
|
+
"/push/geofences/list-for-device"
|
|
1967
|
+
);
|
|
1968
|
+
if (!res || !res.ok) {
|
|
1969
|
+
throw new Error(`[sendoracloud] geofences list failed (HTTP ${res?.status ?? "network"}).`);
|
|
1970
|
+
}
|
|
1971
|
+
const parsed = await res.json();
|
|
1972
|
+
return parsed?.data ?? [];
|
|
1973
|
+
},
|
|
1974
|
+
/**
|
|
1975
|
+
* Report a geofence enter / exit / dwell event. Backend writes
|
|
1976
|
+
* a `geofence.<kind>` event row + fires any workflows triggered
|
|
1977
|
+
* on that geofence id. Call from the native handler that
|
|
1978
|
+
* receives the OS region notification.
|
|
1979
|
+
*/
|
|
1980
|
+
recordEvent: async (input) => {
|
|
1981
|
+
if (!self.config) throw new Error("[sendoracloud] init() must complete before push.geofences.recordEvent().");
|
|
1982
|
+
const res = await post(
|
|
1983
|
+
{ apiUrl: self.config.apiUrl, publicKey: self.config.publicKey, debug: self.debug },
|
|
1984
|
+
"/push/geofences/event",
|
|
1985
|
+
{
|
|
1986
|
+
geofenceId: input.geofenceId,
|
|
1987
|
+
kind: input.kind,
|
|
1988
|
+
lat: input.lat,
|
|
1989
|
+
lng: input.lng,
|
|
1990
|
+
accuracyMeters: input.accuracyMeters,
|
|
1991
|
+
dwellSeconds: input.dwellSeconds
|
|
1992
|
+
}
|
|
1993
|
+
);
|
|
1994
|
+
if (!res || !res.ok) {
|
|
1995
|
+
throw new Error(`[sendoracloud] geofence event failed (HTTP ${res?.status ?? "network"}).`);
|
|
1996
|
+
}
|
|
1997
|
+
}
|
|
1837
1998
|
}
|
|
1838
1999
|
};
|
|
1839
2000
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sendoracloud/sdk-react-native",
|
|
3
|
-
"version": "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",
|