@sendoracloud/sdk-react-native 1.8.1 → 1.9.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
@@ -43,6 +43,7 @@ __export(index_exports, {
43
43
  });
44
44
  module.exports = __toCommonJS(index_exports);
45
45
  var import_react_native_get_random_values = require("react-native-get-random-values");
46
+ var import_react_native4 = require("react-native");
46
47
 
47
48
  // src/storage.ts
48
49
  var PREFIX = "sendora_";
@@ -222,7 +223,7 @@ async function request(opts, method, path, body, extraHeaders, reqOpts) {
222
223
  // package.json
223
224
  var package_default = {
224
225
  name: "@sendoracloud/sdk-react-native",
225
- version: "1.8.1",
226
+ version: "1.9.0",
226
227
  description: "Sendora Cloud React Native + Expo SDK \u2014 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`).",
227
228
  type: "module",
228
229
  main: "./dist/index.cjs",
@@ -465,38 +466,44 @@ var Auth = class {
465
466
  }
466
467
  }
467
468
  /**
468
- * Returns an access token or null. Cold-launch hardening (Wave 62):
469
+ * Returns a VALID (non-expired) access token, or null.
469
470
  *
470
- * 1. Non-expired cached return it. (Fast path.)
471
- * 2. Expired AND refresh inflight return stale cached anyway.
472
- * Backend grace window (60s see auth-service refresh-rotation
473
- * middleware) accepts slightly-stale access tokens, so the
474
- * caller's downstream RPC will succeed against grace while
475
- * the in-flight refresh resolves out of band. Distinct from
476
- * pre-Wave-62 behaviour where 5 concurrent callers all shared
477
- * the same in-flight promise when that promise hung (iOS
478
- * NSURLSession cold-launch contention) every caller hung too.
479
- * 3. Expired + no inflight + refresh available + not in cooldown →
480
- * fire refresh, await it.
481
- * 4. Expired + in cooldown → return stale cached. Caller's RPC
482
- * either succeeds against grace or fails fast on 401, which is
483
- * a better UX than blocking the whole bridge waiting on a
484
- * network that we already know is stalled.
471
+ * Hard contract (matches sdk-ios / sdk-android, and the industry
472
+ * token-client pattern Supabase `_callRefreshToken`, Auth0
473
+ * `getTokenSilently`, Firebase `getIdToken`): this method NEVER
474
+ * returns a token past its `exp`. Callers attach the result as a
475
+ * Bearer to arbitrary resource servers including third-party
476
+ * backends (PostgREST/Supabase) that have NO knowledge of Sendora's
477
+ * own refresh grace window so a token must be either fresh or
478
+ * absent. A token expired by hours is useless and yields a 401.
485
479
  *
486
- * Returns null only when there's NO usable token at all (no cached
487
- * + no refresh available).
480
+ * Resolution order:
481
+ * 1. Cached token with comfortable life left (>= REFRESH_SAFETY_MS
482
+ * skew before exp) → return it. Fast path, no I/O.
483
+ * 2. No cached token AND no refresh token in storage → genuinely
484
+ * signed out → null.
485
+ * 3. Expired/near-expiry but a refresh chain exists:
486
+ * a. A refresh is already inflight → AWAIT it and return the
487
+ * fresh token. Concurrent callers coalesce onto the single
488
+ * in-flight `/token/refresh` (single-flight) — nobody is
489
+ * handed a stale token. The inflight fetch is
490
+ * AbortController-bounded (http.ts caps `/auth-service/*` at
491
+ * 10s), so awaiting can NEVER hang the JS bridge. That
492
+ * bounded-fetch guarantee — NOT the old stale-token return —
493
+ * is what actually fixed the Wave-62 cold-launch hang.
494
+ * b. A recent refresh failed and we're in backoff cooldown →
495
+ * null. Honest "no usable token right now" rather than a
496
+ * provably-expired one; the AppState foreground hook clears
497
+ * the cooldown for an immediate retry on resume.
498
+ * c. Otherwise → fire the refresh and await it.
488
499
  */
489
500
  async getAccessToken() {
490
- if (!this.accessToken) return null;
491
- if (this.accessExpiresAt && Date.now() < this.accessExpiresAt - REFRESH_SAFETY_MS) {
492
- return this.accessToken;
493
- }
494
- if (this.refreshInflight) {
495
- return this.accessToken;
496
- }
497
- if (Date.now() < this.refreshCooldownUntil) {
501
+ if (this.accessToken && this.accessExpiresAt && Date.now() < this.accessExpiresAt - REFRESH_SAFETY_MS) {
498
502
  return this.accessToken;
499
503
  }
504
+ if (!this.hooks.storage.get(REFRESH_KEY)) return null;
505
+ if (this.refreshInflight) return this.refreshInflight;
506
+ if (Date.now() < this.refreshCooldownUntil) return null;
500
507
  return this.refreshAccessToken();
501
508
  }
502
509
  /** Sync read — returns whatever's cached even if expired. Prefer the async variant. */
@@ -2029,6 +2036,22 @@ function byteLength(s) {
2029
2036
  return s.length * 4;
2030
2037
  }
2031
2038
  }
2039
+ function buildDeviceContext(appVersion) {
2040
+ try {
2041
+ const os = import_react_native4.Platform?.OS;
2042
+ if (os !== "ios" && os !== "android") return void 0;
2043
+ const isPad = os === "ios" && import_react_native4.Platform.isPad === true;
2044
+ return {
2045
+ type: isPad ? "tablet" : "mobile",
2046
+ os: os === "ios" ? "iOS" : "Android",
2047
+ osVersion: String(import_react_native4.Platform.Version ?? ""),
2048
+ model: "",
2049
+ appVersion: appVersion ?? ""
2050
+ };
2051
+ } catch {
2052
+ return void 0;
2053
+ }
2054
+ }
2032
2055
  var SendoraSDK = class {
2033
2056
  constructor() {
2034
2057
  this.config = null;
@@ -2831,6 +2854,7 @@ var SendoraSDK = class {
2831
2854
  async sendEvent(kind, payload) {
2832
2855
  if (!this.config) return;
2833
2856
  if (!this.consentGranted) return;
2857
+ const device = buildDeviceContext(this.config.appVersion);
2834
2858
  await post(
2835
2859
  { apiUrl: this.config.apiUrl, publicKey: this.config.publicKey, debug: this.debug },
2836
2860
  "/api/v1/events",
@@ -2845,6 +2869,7 @@ var SendoraSDK = class {
2845
2869
  context: {
2846
2870
  platform: "react-native",
2847
2871
  sdk: { name: "@sendoracloud/sdk-react-native", version: SDK_VERSION },
2872
+ ...device ? { device } : {},
2848
2873
  ...this.config.environment ? { environment: this.config.environment } : {},
2849
2874
  ...kind === "identify" ? { traits: payload.traits } : {}
2850
2875
  }
package/dist/index.d.cts CHANGED
@@ -210,26 +210,36 @@ declare class Auth {
210
210
  /** Internal — fan out to subscribers + cache the latest. */
211
211
  private fireDeviceTakeover;
212
212
  /**
213
- * Returns an access token or null. Cold-launch hardening (Wave 62):
213
+ * Returns a VALID (non-expired) access token, or null.
214
214
  *
215
- * 1. Non-expired cached return it. (Fast path.)
216
- * 2. Expired AND refresh inflight return stale cached anyway.
217
- * Backend grace window (60s see auth-service refresh-rotation
218
- * middleware) accepts slightly-stale access tokens, so the
219
- * caller's downstream RPC will succeed against grace while
220
- * the in-flight refresh resolves out of band. Distinct from
221
- * pre-Wave-62 behaviour where 5 concurrent callers all shared
222
- * the same in-flight promise when that promise hung (iOS
223
- * NSURLSession cold-launch contention) every caller hung too.
224
- * 3. Expired + no inflight + refresh available + not in cooldown →
225
- * fire refresh, await it.
226
- * 4. Expired + in cooldown → return stale cached. Caller's RPC
227
- * either succeeds against grace or fails fast on 401, which is
228
- * a better UX than blocking the whole bridge waiting on a
229
- * network that we already know is stalled.
215
+ * Hard contract (matches sdk-ios / sdk-android, and the industry
216
+ * token-client pattern Supabase `_callRefreshToken`, Auth0
217
+ * `getTokenSilently`, Firebase `getIdToken`): this method NEVER
218
+ * returns a token past its `exp`. Callers attach the result as a
219
+ * Bearer to arbitrary resource servers including third-party
220
+ * backends (PostgREST/Supabase) that have NO knowledge of Sendora's
221
+ * own refresh grace window so a token must be either fresh or
222
+ * absent. A token expired by hours is useless and yields a 401.
230
223
  *
231
- * Returns null only when there's NO usable token at all (no cached
232
- * + no refresh available).
224
+ * Resolution order:
225
+ * 1. Cached token with comfortable life left (>= REFRESH_SAFETY_MS
226
+ * skew before exp) → return it. Fast path, no I/O.
227
+ * 2. No cached token AND no refresh token in storage → genuinely
228
+ * signed out → null.
229
+ * 3. Expired/near-expiry but a refresh chain exists:
230
+ * a. A refresh is already inflight → AWAIT it and return the
231
+ * fresh token. Concurrent callers coalesce onto the single
232
+ * in-flight `/token/refresh` (single-flight) — nobody is
233
+ * handed a stale token. The inflight fetch is
234
+ * AbortController-bounded (http.ts caps `/auth-service/*` at
235
+ * 10s), so awaiting can NEVER hang the JS bridge. That
236
+ * bounded-fetch guarantee — NOT the old stale-token return —
237
+ * is what actually fixed the Wave-62 cold-launch hang.
238
+ * b. A recent refresh failed and we're in backoff cooldown →
239
+ * null. Honest "no usable token right now" rather than a
240
+ * provably-expired one; the AppState foreground hook clears
241
+ * the cooldown for an immediate retry on resume.
242
+ * c. Otherwise → fire the refresh and await it.
233
243
  */
234
244
  getAccessToken(): Promise<string | null>;
235
245
  /** Sync read — returns whatever's cached even if expired. Prefer the async variant. */
@@ -806,6 +816,14 @@ interface SendoraConfig {
806
816
  * Added 0.16.0.
807
817
  */
808
818
  linkDomain?: string;
819
+ /**
820
+ * Your app's version (e.g. "2.4.0"). Attached to every event's
821
+ * `context.device.appVersion` so the dashboard can break analytics down by
822
+ * release (ADR-022). Optional — RN has no zero-dep way to auto-read the
823
+ * host app version, so supply it (e.g. from `expo-constants` /
824
+ * `react-native-device-info`) if you want app-version analytics.
825
+ */
826
+ appVersion?: string;
809
827
  }
810
828
  type TraitValue = string | number | boolean | null | undefined;
811
829
  interface IdentifyTraits {
package/dist/index.d.ts CHANGED
@@ -210,26 +210,36 @@ declare class Auth {
210
210
  /** Internal — fan out to subscribers + cache the latest. */
211
211
  private fireDeviceTakeover;
212
212
  /**
213
- * Returns an access token or null. Cold-launch hardening (Wave 62):
213
+ * Returns a VALID (non-expired) access token, or null.
214
214
  *
215
- * 1. Non-expired cached return it. (Fast path.)
216
- * 2. Expired AND refresh inflight return stale cached anyway.
217
- * Backend grace window (60s see auth-service refresh-rotation
218
- * middleware) accepts slightly-stale access tokens, so the
219
- * caller's downstream RPC will succeed against grace while
220
- * the in-flight refresh resolves out of band. Distinct from
221
- * pre-Wave-62 behaviour where 5 concurrent callers all shared
222
- * the same in-flight promise when that promise hung (iOS
223
- * NSURLSession cold-launch contention) every caller hung too.
224
- * 3. Expired + no inflight + refresh available + not in cooldown →
225
- * fire refresh, await it.
226
- * 4. Expired + in cooldown → return stale cached. Caller's RPC
227
- * either succeeds against grace or fails fast on 401, which is
228
- * a better UX than blocking the whole bridge waiting on a
229
- * network that we already know is stalled.
215
+ * Hard contract (matches sdk-ios / sdk-android, and the industry
216
+ * token-client pattern Supabase `_callRefreshToken`, Auth0
217
+ * `getTokenSilently`, Firebase `getIdToken`): this method NEVER
218
+ * returns a token past its `exp`. Callers attach the result as a
219
+ * Bearer to arbitrary resource servers including third-party
220
+ * backends (PostgREST/Supabase) that have NO knowledge of Sendora's
221
+ * own refresh grace window so a token must be either fresh or
222
+ * absent. A token expired by hours is useless and yields a 401.
230
223
  *
231
- * Returns null only when there's NO usable token at all (no cached
232
- * + no refresh available).
224
+ * Resolution order:
225
+ * 1. Cached token with comfortable life left (>= REFRESH_SAFETY_MS
226
+ * skew before exp) → return it. Fast path, no I/O.
227
+ * 2. No cached token AND no refresh token in storage → genuinely
228
+ * signed out → null.
229
+ * 3. Expired/near-expiry but a refresh chain exists:
230
+ * a. A refresh is already inflight → AWAIT it and return the
231
+ * fresh token. Concurrent callers coalesce onto the single
232
+ * in-flight `/token/refresh` (single-flight) — nobody is
233
+ * handed a stale token. The inflight fetch is
234
+ * AbortController-bounded (http.ts caps `/auth-service/*` at
235
+ * 10s), so awaiting can NEVER hang the JS bridge. That
236
+ * bounded-fetch guarantee — NOT the old stale-token return —
237
+ * is what actually fixed the Wave-62 cold-launch hang.
238
+ * b. A recent refresh failed and we're in backoff cooldown →
239
+ * null. Honest "no usable token right now" rather than a
240
+ * provably-expired one; the AppState foreground hook clears
241
+ * the cooldown for an immediate retry on resume.
242
+ * c. Otherwise → fire the refresh and await it.
233
243
  */
234
244
  getAccessToken(): Promise<string | null>;
235
245
  /** Sync read — returns whatever's cached even if expired. Prefer the async variant. */
@@ -806,6 +816,14 @@ interface SendoraConfig {
806
816
  * Added 0.16.0.
807
817
  */
808
818
  linkDomain?: string;
819
+ /**
820
+ * Your app's version (e.g. "2.4.0"). Attached to every event's
821
+ * `context.device.appVersion` so the dashboard can break analytics down by
822
+ * release (ADR-022). Optional — RN has no zero-dep way to auto-read the
823
+ * host app version, so supply it (e.g. from `expo-constants` /
824
+ * `react-native-device-info`) if you want app-version analytics.
825
+ */
826
+ appVersion?: string;
809
827
  }
810
828
  type TraitValue = string | number | boolean | null | undefined;
811
829
  interface IdentifyTraits {
package/dist/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  // src/index.ts
2
2
  import "react-native-get-random-values";
3
+ import { Platform } from "react-native";
3
4
 
4
5
  // src/storage.ts
5
6
  var PREFIX = "sendora_";
@@ -179,7 +180,7 @@ async function request(opts, method, path, body, extraHeaders, reqOpts) {
179
180
  // package.json
180
181
  var package_default = {
181
182
  name: "@sendoracloud/sdk-react-native",
182
- version: "1.8.1",
183
+ version: "1.9.0",
183
184
  description: "Sendora Cloud React Native + Expo SDK \u2014 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`).",
184
185
  type: "module",
185
186
  main: "./dist/index.cjs",
@@ -422,38 +423,44 @@ var Auth = class {
422
423
  }
423
424
  }
424
425
  /**
425
- * Returns an access token or null. Cold-launch hardening (Wave 62):
426
+ * Returns a VALID (non-expired) access token, or null.
426
427
  *
427
- * 1. Non-expired cached return it. (Fast path.)
428
- * 2. Expired AND refresh inflight return stale cached anyway.
429
- * Backend grace window (60s see auth-service refresh-rotation
430
- * middleware) accepts slightly-stale access tokens, so the
431
- * caller's downstream RPC will succeed against grace while
432
- * the in-flight refresh resolves out of band. Distinct from
433
- * pre-Wave-62 behaviour where 5 concurrent callers all shared
434
- * the same in-flight promise when that promise hung (iOS
435
- * NSURLSession cold-launch contention) every caller hung too.
436
- * 3. Expired + no inflight + refresh available + not in cooldown →
437
- * fire refresh, await it.
438
- * 4. Expired + in cooldown → return stale cached. Caller's RPC
439
- * either succeeds against grace or fails fast on 401, which is
440
- * a better UX than blocking the whole bridge waiting on a
441
- * network that we already know is stalled.
428
+ * Hard contract (matches sdk-ios / sdk-android, and the industry
429
+ * token-client pattern Supabase `_callRefreshToken`, Auth0
430
+ * `getTokenSilently`, Firebase `getIdToken`): this method NEVER
431
+ * returns a token past its `exp`. Callers attach the result as a
432
+ * Bearer to arbitrary resource servers including third-party
433
+ * backends (PostgREST/Supabase) that have NO knowledge of Sendora's
434
+ * own refresh grace window so a token must be either fresh or
435
+ * absent. A token expired by hours is useless and yields a 401.
442
436
  *
443
- * Returns null only when there's NO usable token at all (no cached
444
- * + no refresh available).
437
+ * Resolution order:
438
+ * 1. Cached token with comfortable life left (>= REFRESH_SAFETY_MS
439
+ * skew before exp) → return it. Fast path, no I/O.
440
+ * 2. No cached token AND no refresh token in storage → genuinely
441
+ * signed out → null.
442
+ * 3. Expired/near-expiry but a refresh chain exists:
443
+ * a. A refresh is already inflight → AWAIT it and return the
444
+ * fresh token. Concurrent callers coalesce onto the single
445
+ * in-flight `/token/refresh` (single-flight) — nobody is
446
+ * handed a stale token. The inflight fetch is
447
+ * AbortController-bounded (http.ts caps `/auth-service/*` at
448
+ * 10s), so awaiting can NEVER hang the JS bridge. That
449
+ * bounded-fetch guarantee — NOT the old stale-token return —
450
+ * is what actually fixed the Wave-62 cold-launch hang.
451
+ * b. A recent refresh failed and we're in backoff cooldown →
452
+ * null. Honest "no usable token right now" rather than a
453
+ * provably-expired one; the AppState foreground hook clears
454
+ * the cooldown for an immediate retry on resume.
455
+ * c. Otherwise → fire the refresh and await it.
445
456
  */
446
457
  async getAccessToken() {
447
- if (!this.accessToken) return null;
448
- if (this.accessExpiresAt && Date.now() < this.accessExpiresAt - REFRESH_SAFETY_MS) {
449
- return this.accessToken;
450
- }
451
- if (this.refreshInflight) {
452
- return this.accessToken;
453
- }
454
- if (Date.now() < this.refreshCooldownUntil) {
458
+ if (this.accessToken && this.accessExpiresAt && Date.now() < this.accessExpiresAt - REFRESH_SAFETY_MS) {
455
459
  return this.accessToken;
456
460
  }
461
+ if (!this.hooks.storage.get(REFRESH_KEY)) return null;
462
+ if (this.refreshInflight) return this.refreshInflight;
463
+ if (Date.now() < this.refreshCooldownUntil) return null;
457
464
  return this.refreshAccessToken();
458
465
  }
459
466
  /** Sync read — returns whatever's cached even if expired. Prefer the async variant. */
@@ -1991,6 +1998,22 @@ function byteLength(s) {
1991
1998
  return s.length * 4;
1992
1999
  }
1993
2000
  }
2001
+ function buildDeviceContext(appVersion) {
2002
+ try {
2003
+ const os = Platform?.OS;
2004
+ if (os !== "ios" && os !== "android") return void 0;
2005
+ const isPad = os === "ios" && Platform.isPad === true;
2006
+ return {
2007
+ type: isPad ? "tablet" : "mobile",
2008
+ os: os === "ios" ? "iOS" : "Android",
2009
+ osVersion: String(Platform.Version ?? ""),
2010
+ model: "",
2011
+ appVersion: appVersion ?? ""
2012
+ };
2013
+ } catch {
2014
+ return void 0;
2015
+ }
2016
+ }
1994
2017
  var SendoraSDK = class {
1995
2018
  constructor() {
1996
2019
  this.config = null;
@@ -2793,6 +2816,7 @@ var SendoraSDK = class {
2793
2816
  async sendEvent(kind, payload) {
2794
2817
  if (!this.config) return;
2795
2818
  if (!this.consentGranted) return;
2819
+ const device = buildDeviceContext(this.config.appVersion);
2796
2820
  await post(
2797
2821
  { apiUrl: this.config.apiUrl, publicKey: this.config.publicKey, debug: this.debug },
2798
2822
  "/api/v1/events",
@@ -2807,6 +2831,7 @@ var SendoraSDK = class {
2807
2831
  context: {
2808
2832
  platform: "react-native",
2809
2833
  sdk: { name: "@sendoracloud/sdk-react-native", version: SDK_VERSION },
2834
+ ...device ? { device } : {},
2810
2835
  ...this.config.environment ? { environment: this.config.environment } : {},
2811
2836
  ...kind === "identify" ? { traits: payload.traits } : {}
2812
2837
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sendoracloud/sdk-react-native",
3
- "version": "1.8.1",
3
+ "version": "1.9.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",