@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 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
  };
@@ -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 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
  };
@@ -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.18.9",
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",