@sendoracloud/sdk-react-native 0.10.4 → 0.12.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/README.md CHANGED
@@ -86,6 +86,18 @@ SendoraCloud.reset();
86
86
  environment?: "prod" | "staging" | "dev"; // Routing hint.
87
87
  projectId?: string; // Optional project scoping.
88
88
  consentedByDefault?: boolean; // Defaults to true.
89
+ /**
90
+ * Auto-collect lifecycle events. Default true. Object form:
91
+ * { appOpen?, sessionStart?, appBackground? } for granular control.
92
+ * - appOpen — `app.opened` once per init.
93
+ * - sessionStart — `session.start` once per session (idle >sessionIdleMs).
94
+ * - appBackground — AppState 'change' fires `app.foregrounded` /
95
+ * `app.backgrounded`.
96
+ * Note: `screen.viewed` is NOT auto — call `SendoraCloud.screen(name)`
97
+ * from your navigation listener (RN has no canonical router).
98
+ */
99
+ autoTrack?: boolean | { appOpen?: boolean; sessionStart?: boolean; appBackground?: boolean };
100
+ sessionIdleMs?: number; // Idle threshold for session expiry. Default 30 min.
89
101
  }
90
102
  ```
91
103
 
package/dist/index.cjs CHANGED
@@ -799,8 +799,90 @@ function decodeJwtPayload(token) {
799
799
  }
800
800
  }
801
801
 
802
+ // src/auto-track.ts
803
+ var DEFAULT_IDLE_MS = 30 * 60 * 1e3;
804
+ function resolveFlags(cfg) {
805
+ if (cfg === false) {
806
+ return { appOpen: false, sessionStart: false, appBackground: false };
807
+ }
808
+ if (cfg === true || cfg === void 0) {
809
+ return { appOpen: true, sessionStart: true, appBackground: true };
810
+ }
811
+ return {
812
+ appOpen: cfg.appOpen ?? true,
813
+ sessionStart: cfg.sessionStart ?? true,
814
+ appBackground: cfg.appBackground ?? true
815
+ };
816
+ }
817
+ function installAutoTrack(args) {
818
+ const flags = resolveFlags(args.config.autoTrack);
819
+ const idleMs = args.config.sessionIdleMs ?? DEFAULT_IDLE_MS;
820
+ let lastActivity = Date.now();
821
+ const cleanups = [];
822
+ function newSessionId() {
823
+ return `sess_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`;
824
+ }
825
+ function ensureSession(emitStart) {
826
+ const now = Date.now();
827
+ let id = args.getSessionId();
828
+ if (!id || now - lastActivity > idleMs) {
829
+ id = newSessionId();
830
+ args.setSessionId(id);
831
+ if (emitStart && flags.sessionStart) {
832
+ args.fire("session.start", { sessionId: id });
833
+ }
834
+ }
835
+ lastActivity = now;
836
+ return id;
837
+ }
838
+ const sessionId = ensureSession(
839
+ /* emitStart */
840
+ flags.sessionStart
841
+ );
842
+ if (flags.appOpen) {
843
+ args.fire("app.opened", { sessionId });
844
+ }
845
+ if (flags.appBackground) {
846
+ let AppState = null;
847
+ try {
848
+ const dyn = globalThis.require;
849
+ const mod = dyn ? dyn("react-native") : null;
850
+ AppState = mod?.AppState ?? null;
851
+ } catch {
852
+ AppState = null;
853
+ }
854
+ if (AppState && typeof AppState.addEventListener === "function") {
855
+ const handler = (state) => {
856
+ ensureSession(true);
857
+ if (state === "background" || state === "inactive") {
858
+ args.fire("app.backgrounded", { sessionId: args.getSessionId() });
859
+ } else if (state === "active") {
860
+ args.fire("app.foregrounded", { sessionId: args.getSessionId() });
861
+ }
862
+ };
863
+ const sub = AppState.addEventListener("change", handler);
864
+ cleanups.push(() => {
865
+ if (typeof sub === "function") {
866
+ return;
867
+ }
868
+ if (sub && typeof sub.remove === "function") {
869
+ sub.remove();
870
+ }
871
+ });
872
+ }
873
+ }
874
+ return () => {
875
+ for (const fn of cleanups) {
876
+ try {
877
+ fn();
878
+ } catch {
879
+ }
880
+ }
881
+ };
882
+ }
883
+
802
884
  // src/index.ts
803
- var SDK_VERSION = "0.10.4";
885
+ var SDK_VERSION = "0.11.0";
804
886
  var DEFAULT_API_URL = "https://api.sendoracloud.com";
805
887
  var ANON_KEY = "anon_id";
806
888
  var USER_ID_KEY = "user_id";
@@ -864,6 +946,8 @@ var SendoraSDK = class {
864
946
  this.userId = null;
865
947
  this.consentGranted = true;
866
948
  this.debug = false;
949
+ this.sessionId = "";
950
+ this.autoTrackCleanup = null;
867
951
  }
868
952
  /** Initialize the SDK. Must be awaited at app startup before identify/track. */
869
953
  async init(config) {
@@ -938,6 +1022,20 @@ var SendoraSDK = class {
938
1022
  });
939
1023
  this.auth.hydrate();
940
1024
  this.ready = true;
1025
+ if (config.autoTrack !== false) {
1026
+ this.autoTrackCleanup = installAutoTrack({
1027
+ config: this.config,
1028
+ fire: (eventType, properties) => this.track(eventType, properties),
1029
+ getSessionId: () => this.sessionId,
1030
+ setSessionId: (id) => {
1031
+ this.sessionId = id;
1032
+ }
1033
+ });
1034
+ }
1035
+ }
1036
+ /** Current logical session id, if auto-track is enabled. */
1037
+ getSessionId() {
1038
+ return this.sessionId;
941
1039
  }
942
1040
  /** Stable anonymous identifier for this install — survives app restarts via AsyncStorage. */
943
1041
  getAnonymousId() {
@@ -1037,6 +1135,28 @@ var SendoraSDK = class {
1037
1135
  }
1038
1136
  return { tokenId: data.tokenId, created: data.created === true };
1039
1137
  }
1138
+ /**
1139
+ * Push namespace — mirrors the iOS / Android / Web SDK shape so
1140
+ * `sendora.push.registerToken(...)` works consistently across all 5 SDKs.
1141
+ * Backed by the same registerPushToken() implementation; kept top-level
1142
+ * too for backwards compatibility with the s47 release.
1143
+ */
1144
+ get push() {
1145
+ const self = this;
1146
+ return {
1147
+ registerToken: (reg) => self.registerPushToken(reg),
1148
+ trackOpen: async (sendId, clickAction) => {
1149
+ if (!self.config) throw new Error("[sendoracloud] init() must complete before push.trackOpen().");
1150
+ const body = { sendId };
1151
+ if (clickAction !== void 0) body.clickAction = clickAction;
1152
+ await post(
1153
+ { apiUrl: self.config.apiUrl, publicKey: self.config.publicKey, debug: self.debug },
1154
+ "/push/track-open",
1155
+ body
1156
+ );
1157
+ }
1158
+ };
1159
+ }
1040
1160
  /**
1041
1161
  * Clear identity + rotate the anonymous id. Call on logout. Awaits
1042
1162
  * the AsyncStorage writes so a caller who kills the app immediately
@@ -1045,9 +1165,17 @@ var SendoraSDK = class {
1045
1165
  async reset() {
1046
1166
  this.userId = null;
1047
1167
  this.anonId = uuid();
1168
+ this.sessionId = "";
1048
1169
  await this.storage.clearAll();
1049
1170
  this.storage.set(ANON_KEY, this.anonId);
1050
1171
  }
1172
+ /** Tear down auto-track listeners. Call in test harnesses. */
1173
+ destroy() {
1174
+ if (this.autoTrackCleanup) {
1175
+ this.autoTrackCleanup();
1176
+ this.autoTrackCleanup = null;
1177
+ }
1178
+ }
1051
1179
  async sendEvent(kind, payload) {
1052
1180
  if (!this.config) return;
1053
1181
  if (!this.consentGranted) return;
package/dist/index.d.cts CHANGED
@@ -309,6 +309,27 @@ interface SendoraConfig {
309
309
  consentedByDefault?: boolean;
310
310
  /** Verbose error logging (auth-error codes + Zod field errors). Default false. */
311
311
  debug?: boolean;
312
+ /**
313
+ * Auto-collect lifecycle events. Mirrors Firebase Analytics' auto-collected
314
+ * surface. `true` = all enabled (default). Pass an object for granular
315
+ * control or `false` to disable.
316
+ *
317
+ * - `appOpen` — fire `app.opened` on init. Default true.
318
+ * - `sessionStart` — emit `session.start` once per logical session
319
+ * (idle >`sessionIdleMs` closes session). Default true.
320
+ * - `appBackground` — emit `app.backgrounded`/`app.foregrounded` on
321
+ * AppState transitions. Default true.
322
+ *
323
+ * Note: screen view tracking is NOT auto — RN has no canonical router.
324
+ * Call `SendoraCloud.screen(name)` from your navigation listener.
325
+ */
326
+ autoTrack?: boolean | {
327
+ appOpen?: boolean;
328
+ sessionStart?: boolean;
329
+ appBackground?: boolean;
330
+ };
331
+ /** Idle threshold (ms) for session expiry; default 30 minutes. */
332
+ sessionIdleMs?: number;
312
333
  }
313
334
  type TraitValue = string | number | boolean | null | undefined;
314
335
  interface IdentifyTraits {
@@ -377,10 +398,14 @@ declare class SendoraSDK {
377
398
  private userId;
378
399
  private consentGranted;
379
400
  private debug;
401
+ private sessionId;
402
+ private autoTrackCleanup;
380
403
  /** Lazy-initialised in init() once we know the apiUrl + publicKey. */
381
404
  auth: Auth;
382
405
  /** Initialize the SDK. Must be awaited at app startup before identify/track. */
383
406
  init(config: SendoraConfig): Promise<void>;
407
+ /** Current logical session id, if auto-track is enabled. */
408
+ getSessionId(): string;
384
409
  /** Stable anonymous identifier for this install — survives app restarts via AsyncStorage. */
385
410
  getAnonymousId(): string;
386
411
  /** Current identified user id, if any. */
@@ -400,12 +425,24 @@ declare class SendoraSDK {
400
425
  * `@react-native-firebase/messaging`.
401
426
  */
402
427
  registerPushToken(reg: PushTokenRegistration): Promise<PushTokenReceipt>;
428
+ /**
429
+ * Push namespace — mirrors the iOS / Android / Web SDK shape so
430
+ * `sendora.push.registerToken(...)` works consistently across all 5 SDKs.
431
+ * Backed by the same registerPushToken() implementation; kept top-level
432
+ * too for backwards compatibility with the s47 release.
433
+ */
434
+ get push(): {
435
+ registerToken: (reg: PushTokenRegistration) => Promise<PushTokenReceipt>;
436
+ trackOpen: (sendId: string, clickAction?: string) => Promise<void>;
437
+ };
403
438
  /**
404
439
  * Clear identity + rotate the anonymous id. Call on logout. Awaits
405
440
  * the AsyncStorage writes so a caller who kills the app immediately
406
441
  * after doesn't leave stale identity on disk.
407
442
  */
408
443
  reset(): Promise<void>;
444
+ /** Tear down auto-track listeners. Call in test harnesses. */
445
+ destroy(): void;
409
446
  private sendEvent;
410
447
  private assertReady;
411
448
  }
package/dist/index.d.ts CHANGED
@@ -309,6 +309,27 @@ interface SendoraConfig {
309
309
  consentedByDefault?: boolean;
310
310
  /** Verbose error logging (auth-error codes + Zod field errors). Default false. */
311
311
  debug?: boolean;
312
+ /**
313
+ * Auto-collect lifecycle events. Mirrors Firebase Analytics' auto-collected
314
+ * surface. `true` = all enabled (default). Pass an object for granular
315
+ * control or `false` to disable.
316
+ *
317
+ * - `appOpen` — fire `app.opened` on init. Default true.
318
+ * - `sessionStart` — emit `session.start` once per logical session
319
+ * (idle >`sessionIdleMs` closes session). Default true.
320
+ * - `appBackground` — emit `app.backgrounded`/`app.foregrounded` on
321
+ * AppState transitions. Default true.
322
+ *
323
+ * Note: screen view tracking is NOT auto — RN has no canonical router.
324
+ * Call `SendoraCloud.screen(name)` from your navigation listener.
325
+ */
326
+ autoTrack?: boolean | {
327
+ appOpen?: boolean;
328
+ sessionStart?: boolean;
329
+ appBackground?: boolean;
330
+ };
331
+ /** Idle threshold (ms) for session expiry; default 30 minutes. */
332
+ sessionIdleMs?: number;
312
333
  }
313
334
  type TraitValue = string | number | boolean | null | undefined;
314
335
  interface IdentifyTraits {
@@ -377,10 +398,14 @@ declare class SendoraSDK {
377
398
  private userId;
378
399
  private consentGranted;
379
400
  private debug;
401
+ private sessionId;
402
+ private autoTrackCleanup;
380
403
  /** Lazy-initialised in init() once we know the apiUrl + publicKey. */
381
404
  auth: Auth;
382
405
  /** Initialize the SDK. Must be awaited at app startup before identify/track. */
383
406
  init(config: SendoraConfig): Promise<void>;
407
+ /** Current logical session id, if auto-track is enabled. */
408
+ getSessionId(): string;
384
409
  /** Stable anonymous identifier for this install — survives app restarts via AsyncStorage. */
385
410
  getAnonymousId(): string;
386
411
  /** Current identified user id, if any. */
@@ -400,12 +425,24 @@ declare class SendoraSDK {
400
425
  * `@react-native-firebase/messaging`.
401
426
  */
402
427
  registerPushToken(reg: PushTokenRegistration): Promise<PushTokenReceipt>;
428
+ /**
429
+ * Push namespace — mirrors the iOS / Android / Web SDK shape so
430
+ * `sendora.push.registerToken(...)` works consistently across all 5 SDKs.
431
+ * Backed by the same registerPushToken() implementation; kept top-level
432
+ * too for backwards compatibility with the s47 release.
433
+ */
434
+ get push(): {
435
+ registerToken: (reg: PushTokenRegistration) => Promise<PushTokenReceipt>;
436
+ trackOpen: (sendId: string, clickAction?: string) => Promise<void>;
437
+ };
403
438
  /**
404
439
  * Clear identity + rotate the anonymous id. Call on logout. Awaits
405
440
  * the AsyncStorage writes so a caller who kills the app immediately
406
441
  * after doesn't leave stale identity on disk.
407
442
  */
408
443
  reset(): Promise<void>;
444
+ /** Tear down auto-track listeners. Call in test harnesses. */
445
+ destroy(): void;
409
446
  private sendEvent;
410
447
  private assertReady;
411
448
  }
package/dist/index.js CHANGED
@@ -759,8 +759,90 @@ function decodeJwtPayload(token) {
759
759
  }
760
760
  }
761
761
 
762
+ // src/auto-track.ts
763
+ var DEFAULT_IDLE_MS = 30 * 60 * 1e3;
764
+ function resolveFlags(cfg) {
765
+ if (cfg === false) {
766
+ return { appOpen: false, sessionStart: false, appBackground: false };
767
+ }
768
+ if (cfg === true || cfg === void 0) {
769
+ return { appOpen: true, sessionStart: true, appBackground: true };
770
+ }
771
+ return {
772
+ appOpen: cfg.appOpen ?? true,
773
+ sessionStart: cfg.sessionStart ?? true,
774
+ appBackground: cfg.appBackground ?? true
775
+ };
776
+ }
777
+ function installAutoTrack(args) {
778
+ const flags = resolveFlags(args.config.autoTrack);
779
+ const idleMs = args.config.sessionIdleMs ?? DEFAULT_IDLE_MS;
780
+ let lastActivity = Date.now();
781
+ const cleanups = [];
782
+ function newSessionId() {
783
+ return `sess_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`;
784
+ }
785
+ function ensureSession(emitStart) {
786
+ const now = Date.now();
787
+ let id = args.getSessionId();
788
+ if (!id || now - lastActivity > idleMs) {
789
+ id = newSessionId();
790
+ args.setSessionId(id);
791
+ if (emitStart && flags.sessionStart) {
792
+ args.fire("session.start", { sessionId: id });
793
+ }
794
+ }
795
+ lastActivity = now;
796
+ return id;
797
+ }
798
+ const sessionId = ensureSession(
799
+ /* emitStart */
800
+ flags.sessionStart
801
+ );
802
+ if (flags.appOpen) {
803
+ args.fire("app.opened", { sessionId });
804
+ }
805
+ if (flags.appBackground) {
806
+ let AppState = null;
807
+ try {
808
+ const dyn = globalThis.require;
809
+ const mod = dyn ? dyn("react-native") : null;
810
+ AppState = mod?.AppState ?? null;
811
+ } catch {
812
+ AppState = null;
813
+ }
814
+ if (AppState && typeof AppState.addEventListener === "function") {
815
+ const handler = (state) => {
816
+ ensureSession(true);
817
+ if (state === "background" || state === "inactive") {
818
+ args.fire("app.backgrounded", { sessionId: args.getSessionId() });
819
+ } else if (state === "active") {
820
+ args.fire("app.foregrounded", { sessionId: args.getSessionId() });
821
+ }
822
+ };
823
+ const sub = AppState.addEventListener("change", handler);
824
+ cleanups.push(() => {
825
+ if (typeof sub === "function") {
826
+ return;
827
+ }
828
+ if (sub && typeof sub.remove === "function") {
829
+ sub.remove();
830
+ }
831
+ });
832
+ }
833
+ }
834
+ return () => {
835
+ for (const fn of cleanups) {
836
+ try {
837
+ fn();
838
+ } catch {
839
+ }
840
+ }
841
+ };
842
+ }
843
+
762
844
  // src/index.ts
763
- var SDK_VERSION = "0.10.4";
845
+ var SDK_VERSION = "0.11.0";
764
846
  var DEFAULT_API_URL = "https://api.sendoracloud.com";
765
847
  var ANON_KEY = "anon_id";
766
848
  var USER_ID_KEY = "user_id";
@@ -824,6 +906,8 @@ var SendoraSDK = class {
824
906
  this.userId = null;
825
907
  this.consentGranted = true;
826
908
  this.debug = false;
909
+ this.sessionId = "";
910
+ this.autoTrackCleanup = null;
827
911
  }
828
912
  /** Initialize the SDK. Must be awaited at app startup before identify/track. */
829
913
  async init(config) {
@@ -898,6 +982,20 @@ var SendoraSDK = class {
898
982
  });
899
983
  this.auth.hydrate();
900
984
  this.ready = true;
985
+ if (config.autoTrack !== false) {
986
+ this.autoTrackCleanup = installAutoTrack({
987
+ config: this.config,
988
+ fire: (eventType, properties) => this.track(eventType, properties),
989
+ getSessionId: () => this.sessionId,
990
+ setSessionId: (id) => {
991
+ this.sessionId = id;
992
+ }
993
+ });
994
+ }
995
+ }
996
+ /** Current logical session id, if auto-track is enabled. */
997
+ getSessionId() {
998
+ return this.sessionId;
901
999
  }
902
1000
  /** Stable anonymous identifier for this install — survives app restarts via AsyncStorage. */
903
1001
  getAnonymousId() {
@@ -997,6 +1095,28 @@ var SendoraSDK = class {
997
1095
  }
998
1096
  return { tokenId: data.tokenId, created: data.created === true };
999
1097
  }
1098
+ /**
1099
+ * Push namespace — mirrors the iOS / Android / Web SDK shape so
1100
+ * `sendora.push.registerToken(...)` works consistently across all 5 SDKs.
1101
+ * Backed by the same registerPushToken() implementation; kept top-level
1102
+ * too for backwards compatibility with the s47 release.
1103
+ */
1104
+ get push() {
1105
+ const self = this;
1106
+ return {
1107
+ registerToken: (reg) => self.registerPushToken(reg),
1108
+ trackOpen: async (sendId, clickAction) => {
1109
+ if (!self.config) throw new Error("[sendoracloud] init() must complete before push.trackOpen().");
1110
+ const body = { sendId };
1111
+ if (clickAction !== void 0) body.clickAction = clickAction;
1112
+ await post(
1113
+ { apiUrl: self.config.apiUrl, publicKey: self.config.publicKey, debug: self.debug },
1114
+ "/push/track-open",
1115
+ body
1116
+ );
1117
+ }
1118
+ };
1119
+ }
1000
1120
  /**
1001
1121
  * Clear identity + rotate the anonymous id. Call on logout. Awaits
1002
1122
  * the AsyncStorage writes so a caller who kills the app immediately
@@ -1005,9 +1125,17 @@ var SendoraSDK = class {
1005
1125
  async reset() {
1006
1126
  this.userId = null;
1007
1127
  this.anonId = uuid();
1128
+ this.sessionId = "";
1008
1129
  await this.storage.clearAll();
1009
1130
  this.storage.set(ANON_KEY, this.anonId);
1010
1131
  }
1132
+ /** Tear down auto-track listeners. Call in test harnesses. */
1133
+ destroy() {
1134
+ if (this.autoTrackCleanup) {
1135
+ this.autoTrackCleanup();
1136
+ this.autoTrackCleanup = null;
1137
+ }
1138
+ }
1011
1139
  async sendEvent(kind, payload) {
1012
1140
  if (!this.config) return;
1013
1141
  if (!this.consentGranted) return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sendoracloud/sdk-react-native",
3
- "version": "0.10.4",
3
+ "version": "0.12.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",