@sendoracloud/sdk-react-native 1.6.1 → 1.7.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
@@ -219,6 +219,82 @@ async function request(opts, method, path, body, extraHeaders, reqOpts) {
219
219
  return res;
220
220
  }
221
221
 
222
+ // package.json
223
+ var package_default = {
224
+ name: "@sendoracloud/sdk-react-native",
225
+ version: "1.7.0",
226
+ 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
+ type: "module",
228
+ main: "./dist/index.cjs",
229
+ module: "./dist/index.js",
230
+ types: "./dist/index.d.ts",
231
+ exports: {
232
+ ".": {
233
+ types: "./dist/index.d.ts",
234
+ import: "./dist/index.js",
235
+ require: "./dist/index.cjs"
236
+ },
237
+ "./contact-widget": {
238
+ types: "./dist/contact-widget.d.ts",
239
+ import: "./dist/contact-widget.js",
240
+ require: "./dist/contact-widget.cjs"
241
+ }
242
+ },
243
+ files: [
244
+ "dist",
245
+ "examples",
246
+ "README.md",
247
+ "LICENSE"
248
+ ],
249
+ keywords: [
250
+ "sendora",
251
+ "react-native",
252
+ "expo",
253
+ "analytics",
254
+ "tracking",
255
+ "customer-engagement"
256
+ ],
257
+ dependencies: {
258
+ "react-native-get-random-values": "^1.11.0"
259
+ },
260
+ peerDependencies: {
261
+ "@react-native-async-storage/async-storage": ">=1.17.0",
262
+ "react-native": ">=0.70.0",
263
+ "react-native-webview": ">=13.0.0"
264
+ },
265
+ peerDependenciesMeta: {
266
+ "@react-native-async-storage/async-storage": {
267
+ optional: false
268
+ },
269
+ "react-native": {
270
+ optional: false
271
+ },
272
+ "react-native-webview": {
273
+ optional: true
274
+ }
275
+ },
276
+ scripts: {
277
+ build: "tsup src/index.ts src/contact-widget.tsx --format esm,cjs --dts --clean --external react --external react-native --external react-native-webview",
278
+ typecheck: "tsc --noEmit"
279
+ },
280
+ devDependencies: {
281
+ "@types/react": "^19.2.16",
282
+ "react-native": "^0.85.3",
283
+ "react-native-webview": "^13.16.1",
284
+ tsup: "^8.0.0",
285
+ typescript: "^5.0.0"
286
+ },
287
+ license: "MIT",
288
+ repository: {
289
+ type: "git",
290
+ url: "https://github.com/sendoracloud/sdk-react-native"
291
+ },
292
+ homepage: "https://sendoracloud.com/sdks"
293
+ };
294
+
295
+ // src/version.ts
296
+ var SDK_VERSION = package_default.version;
297
+
222
298
  // src/auth.ts
223
299
  var import_react_native = require("react-native");
224
300
  var TOKEN_KEY = "auth_access_token";
@@ -277,6 +353,14 @@ var Auth = class {
277
353
  this.refreshCooldownUntil = 0;
278
354
  this.takeoverListeners = /* @__PURE__ */ new Set();
279
355
  this.lastTakeover = null;
356
+ /**
357
+ * Anon refresh token captured at `signIn()` time, stashed keyed to the
358
+ * issued `mfaChallengeToken` so the later `challengeMfa()` can forward it
359
+ * for device-takeover (s58.111). Before this, the MFA branch wiped the anon
360
+ * identity before the challenge resolved → the takeover hint was lost → one
361
+ * device ended with two user_ids + duplicate pushes (audit s58.203 fix).
362
+ */
363
+ this.pendingAnonTakeover = null;
280
364
  /**
281
365
  * Proactive refresh (s58.72). Fires when access-token age crosses
282
366
  * ~80% of TTL. Pairs with the AppState foreground listener so a
@@ -496,12 +580,7 @@ var Auth = class {
496
580
  */
497
581
  signIn(email, password) {
498
582
  return this.serialize(async () => {
499
- let prevAnonRefreshToken;
500
- if (this.user?.isAnonymous) {
501
- const stashed = this.hooks.storage.get(REFRESH_KEY);
502
- if (stashed) prevAnonRefreshToken = stashed;
503
- }
504
- if (this.user !== null) await this.wipeLocalIdentity();
583
+ const prevAnonRefreshToken = this.readPrevAnonRefreshToken();
505
584
  const res = await post(
506
585
  { apiUrl: this.hooks.apiUrl, publicKey: this.hooks.publicKey, debug: this.hooks.debug },
507
586
  "/api/v1/auth-service/login",
@@ -519,12 +598,16 @@ var Auth = class {
519
598
  }
520
599
  const r = parsed.data;
521
600
  if (r.mfaRequired === true && typeof r.mfaChallengeToken === "string") {
601
+ if (prevAnonRefreshToken) {
602
+ this.pendingAnonTakeover = { challengeToken: r.mfaChallengeToken, prevAnonRefreshToken };
603
+ }
522
604
  const u = r.user;
523
605
  return { mfaRequired: true, mfaChallengeToken: r.mfaChallengeToken, userId: u?.id ?? "" };
524
606
  }
525
607
  if (!isAuthApiResponse(parsed.data)) {
526
608
  throw new AuthError(parsed.error?.code ?? "AUTH_ERROR", parsed.error?.message ?? "Login failed");
527
609
  }
610
+ if (this.user !== null) await this.wipeLocalIdentity();
528
611
  this.persist(parsed.data);
529
612
  return parsed.data.user;
530
613
  });
@@ -606,13 +689,15 @@ var Auth = class {
606
689
  */
607
690
  challengeMfa(mfaChallengeToken, code) {
608
691
  return this.serialize(async () => {
609
- const prevAnonRefreshToken = this.readPrevAnonRefreshToken();
692
+ const stashed = this.pendingAnonTakeover?.challengeToken === mfaChallengeToken ? this.pendingAnonTakeover.prevAnonRefreshToken : void 0;
693
+ const prevAnonRefreshToken = stashed ?? this.readPrevAnonRefreshToken();
610
694
  const body = {
611
695
  challengeToken: mfaChallengeToken,
612
696
  code
613
697
  };
614
698
  if (prevAnonRefreshToken) body.prevAnonRefreshToken = prevAnonRefreshToken;
615
699
  const data = await this.callAuth("/api/v1/auth-service/mfa/challenge", body);
700
+ if (this.user !== null) await this.wipeLocalIdentity();
616
701
  this.persist(data);
617
702
  return data.user;
618
703
  });
@@ -1152,6 +1237,7 @@ var Auth = class {
1152
1237
  this.accessToken = null;
1153
1238
  this.accessExpiresAt = 0;
1154
1239
  this.lastTakeover = null;
1240
+ this.pendingAnonTakeover = null;
1155
1241
  this.hooks.storage.remove(USER_KEY);
1156
1242
  this.hooks.storage.remove(TOKEN_KEY);
1157
1243
  this.hooks.storage.remove(TOKEN_EXPIRES_KEY);
@@ -1549,6 +1635,7 @@ var Links = class {
1549
1635
  async doCreate(input) {
1550
1636
  const body = { title: input.title };
1551
1637
  if (input.fallbackUrl) body.fallbackUrl = input.fallbackUrl;
1638
+ if (input.noAppMode) body.noAppMode = input.noAppMode;
1552
1639
  if (input.iosDeepLinkPath) body.iosDeepLinkPath = input.iosDeepLinkPath;
1553
1640
  if (input.androidDeepLinkPath) body.androidDeepLinkPath = input.androidDeepLinkPath;
1554
1641
  if (input.linkData) body.linkData = input.linkData;
@@ -1839,7 +1926,6 @@ function installAutoTrack(args) {
1839
1926
  }
1840
1927
 
1841
1928
  // src/index.ts
1842
- var SDK_VERSION = "1.0.0";
1843
1929
  var DEFAULT_API_URL = "https://api.sendoracloud.com";
1844
1930
  var ANON_KEY = "anon_id";
1845
1931
  var USER_ID_KEY = "user_id";
@@ -2383,11 +2469,12 @@ var SendoraSDK = class {
2383
2469
  const token = await self.auth.getAccessToken();
2384
2470
  if (!token) return 0;
2385
2471
  try {
2386
- const res = await fetch(
2387
- `${self.config.apiUrl}/api/v1/widgets/${encodeURIComponent(widgetId)}/my-tickets/unread-count`,
2388
- { headers: { Authorization: `Bearer ${token}` } }
2472
+ const res = await getJson(
2473
+ { apiUrl: self.config.apiUrl, publicKey: self.config.publicKey, debug: self.debug },
2474
+ `/api/v1/widgets/${encodeURIComponent(widgetId)}/my-tickets/unread-count`,
2475
+ { Authorization: `Bearer ${token}` }
2389
2476
  );
2390
- if (!res.ok) return 0;
2477
+ if (!res || !res.ok) return 0;
2391
2478
  const parsed = await res.json();
2392
2479
  return parsed?.data?.count ?? 0;
2393
2480
  } catch {
@@ -2416,12 +2503,11 @@ var SendoraSDK = class {
2416
2503
  if (!input.query || input.query.trim().length === 0) return [];
2417
2504
  const params = new URLSearchParams({ query: input.query });
2418
2505
  if (input.limit) params.set("limit", String(input.limit));
2419
- const url = `${self.config.apiUrl}/api/v1/kb/search?${params.toString()}`;
2420
- const res = await fetch(url, {
2421
- method: "GET",
2422
- headers: { "x-api-key": self.config.publicKey }
2423
- });
2424
- if (!res.ok) throw new Error(`[sendoracloud] kb.search failed (${res.status})`);
2506
+ const res = await getJson(
2507
+ { apiUrl: self.config.apiUrl, publicKey: self.config.publicKey, debug: self.debug },
2508
+ `/api/v1/kb/search?${params.toString()}`
2509
+ );
2510
+ if (!res || !res.ok) throw new Error(`[sendoracloud] kb.search failed (${res?.status ?? "network"})`);
2425
2511
  const parsed = await res.json();
2426
2512
  if (!parsed.success || !Array.isArray(parsed.data)) {
2427
2513
  throw new Error("[sendoracloud] kb.search: bad response");
@@ -2507,11 +2593,11 @@ var SendoraSDK = class {
2507
2593
  fetchConfig: async (surveyId) => {
2508
2594
  if (!self.config) throw new Error("[sendoracloud] init() must complete before surveys.fetchConfig().");
2509
2595
  if (typeof surveyId !== "string" || surveyId.length === 0) return null;
2510
- const url = `${self.config.apiUrl}/api/v1/surveys/${encodeURIComponent(surveyId)}/config`;
2511
- const res = await fetch(url, {
2512
- method: "GET",
2513
- headers: { "x-api-key": self.config.publicKey }
2514
- });
2596
+ const res = await getJson(
2597
+ { apiUrl: self.config.apiUrl, publicKey: self.config.publicKey, debug: self.debug },
2598
+ `/api/v1/surveys/${encodeURIComponent(surveyId)}/config`
2599
+ );
2600
+ if (!res) return null;
2515
2601
  if (res.status === 404) return null;
2516
2602
  if (!res.ok) throw new Error(`[sendoracloud] surveys.fetchConfig failed (${res.status})`);
2517
2603
  const parsed = await res.json();
@@ -2556,12 +2642,11 @@ var SendoraSDK = class {
2556
2642
  return {
2557
2643
  fetchActive: async () => {
2558
2644
  if (!self.config) throw new Error("[sendoracloud] init() must complete before messages.fetchActive().");
2559
- const url = `${self.config.apiUrl}/api/v1/in-app-messages/active`;
2560
- const res = await fetch(url, {
2561
- method: "GET",
2562
- headers: { "x-api-key": self.config.publicKey }
2563
- });
2564
- if (!res.ok) throw new Error(`[sendoracloud] messages.fetchActive failed (${res.status})`);
2645
+ const res = await getJson(
2646
+ { apiUrl: self.config.apiUrl, publicKey: self.config.publicKey, debug: self.debug },
2647
+ "/api/v1/in-app-messages/active"
2648
+ );
2649
+ if (!res || !res.ok) throw new Error(`[sendoracloud] messages.fetchActive failed (${res?.status ?? "network"})`);
2565
2650
  const parsed = await res.json();
2566
2651
  if (!parsed.success || !Array.isArray(parsed.data)) {
2567
2652
  throw new Error("[sendoracloud] messages.fetchActive: bad response");
package/dist/index.d.cts CHANGED
@@ -50,9 +50,16 @@ declare class Storage {
50
50
  * so a UI double-submit can't mint two anonymous users or interleave
51
51
  * a signIn + signOut.
52
52
  *
53
- * Tokens persist via the configured secure-storage adapter (defaults
54
- * to AsyncStorage; consumers handling sensitive accounts should pass
55
- * a Keychain / SecureStore-backed adapter via `secureStorage`).
53
+ * Tokens (including the refresh token) persist in AsyncStorage, not
54
+ * the iOS Keychain / Android Keystore. AsyncStorage is unencrypted
55
+ * app-sandbox storage: readable by anyone with filesystem/backup
56
+ * access to a jailbroken/rooted or unlocked device, but isolated from
57
+ * other apps by the OS sandbox. There is NO `secureStorage` adapter
58
+ * option today — earlier docs claimed a Keychain/SecureStore-backed
59
+ * adapter that was never implemented. If you ship a follow-up that
60
+ * moves the refresh token to expo-secure-store / react-native-keychain
61
+ * (optional peer dep, Metro-resolvable static import — never an inline
62
+ * require), keep the `auth_refresh_token` storage key for back-compat.
56
63
  *
57
64
  * Response payloads are validated non-empty before persisting so a
58
65
  * malformed/MITM'd response can't install an `id = ""` user.
@@ -134,6 +141,14 @@ declare class Auth {
134
141
  private refreshCooldownUntil;
135
142
  private takeoverListeners;
136
143
  private lastTakeover;
144
+ /**
145
+ * Anon refresh token captured at `signIn()` time, stashed keyed to the
146
+ * issued `mfaChallengeToken` so the later `challengeMfa()` can forward it
147
+ * for device-takeover (s58.111). Before this, the MFA branch wiped the anon
148
+ * identity before the challenge resolved → the takeover hint was lost → one
149
+ * device ended with two user_ids + duplicate pushes (audit s58.203 fix).
150
+ */
151
+ private pendingAnonTakeover;
137
152
  /**
138
153
  * Resolves when the parent SDK has finished its async init() —
139
154
  * AsyncStorage hydrated + auth.hydrate() restored the cached
@@ -503,6 +518,14 @@ interface LinkCreateInput<T extends LinkData = LinkData> {
503
518
  * Play Store URL). Pass explicitly for per-link override.
504
519
  */
505
520
  fallbackUrl?: string;
521
+ /**
522
+ * How a **mobile visitor without the app installed** is routed (Adjust /
523
+ * Branch parity). `"auto"` (default) = open the app store when one is
524
+ * registered for the platform, else the web `fallbackUrl`. `"store"` =
525
+ * prefer the store. `"web"` = force the web `fallbackUrl` even when a
526
+ * store URL exists. Omit to inherit the project default. Desktop is
527
+ * always web. */
528
+ noAppMode?: "auto" | "store" | "web";
506
529
  /** Custom typed JSON delivered to onLinkOpened. Max 2KB serialized. */
507
530
  linkData?: T;
508
531
  /** Optional OG preview overrides for richer share cards. */
package/dist/index.d.ts CHANGED
@@ -50,9 +50,16 @@ declare class Storage {
50
50
  * so a UI double-submit can't mint two anonymous users or interleave
51
51
  * a signIn + signOut.
52
52
  *
53
- * Tokens persist via the configured secure-storage adapter (defaults
54
- * to AsyncStorage; consumers handling sensitive accounts should pass
55
- * a Keychain / SecureStore-backed adapter via `secureStorage`).
53
+ * Tokens (including the refresh token) persist in AsyncStorage, not
54
+ * the iOS Keychain / Android Keystore. AsyncStorage is unencrypted
55
+ * app-sandbox storage: readable by anyone with filesystem/backup
56
+ * access to a jailbroken/rooted or unlocked device, but isolated from
57
+ * other apps by the OS sandbox. There is NO `secureStorage` adapter
58
+ * option today — earlier docs claimed a Keychain/SecureStore-backed
59
+ * adapter that was never implemented. If you ship a follow-up that
60
+ * moves the refresh token to expo-secure-store / react-native-keychain
61
+ * (optional peer dep, Metro-resolvable static import — never an inline
62
+ * require), keep the `auth_refresh_token` storage key for back-compat.
56
63
  *
57
64
  * Response payloads are validated non-empty before persisting so a
58
65
  * malformed/MITM'd response can't install an `id = ""` user.
@@ -134,6 +141,14 @@ declare class Auth {
134
141
  private refreshCooldownUntil;
135
142
  private takeoverListeners;
136
143
  private lastTakeover;
144
+ /**
145
+ * Anon refresh token captured at `signIn()` time, stashed keyed to the
146
+ * issued `mfaChallengeToken` so the later `challengeMfa()` can forward it
147
+ * for device-takeover (s58.111). Before this, the MFA branch wiped the anon
148
+ * identity before the challenge resolved → the takeover hint was lost → one
149
+ * device ended with two user_ids + duplicate pushes (audit s58.203 fix).
150
+ */
151
+ private pendingAnonTakeover;
137
152
  /**
138
153
  * Resolves when the parent SDK has finished its async init() —
139
154
  * AsyncStorage hydrated + auth.hydrate() restored the cached
@@ -503,6 +518,14 @@ interface LinkCreateInput<T extends LinkData = LinkData> {
503
518
  * Play Store URL). Pass explicitly for per-link override.
504
519
  */
505
520
  fallbackUrl?: string;
521
+ /**
522
+ * How a **mobile visitor without the app installed** is routed (Adjust /
523
+ * Branch parity). `"auto"` (default) = open the app store when one is
524
+ * registered for the platform, else the web `fallbackUrl`. `"store"` =
525
+ * prefer the store. `"web"` = force the web `fallbackUrl` even when a
526
+ * store URL exists. Omit to inherit the project default. Desktop is
527
+ * always web. */
528
+ noAppMode?: "auto" | "store" | "web";
506
529
  /** Custom typed JSON delivered to onLinkOpened. Max 2KB serialized. */
507
530
  linkData?: T;
508
531
  /** Optional OG preview overrides for richer share cards. */
package/dist/index.js CHANGED
@@ -176,6 +176,82 @@ async function request(opts, method, path, body, extraHeaders, reqOpts) {
176
176
  return res;
177
177
  }
178
178
 
179
+ // package.json
180
+ var package_default = {
181
+ name: "@sendoracloud/sdk-react-native",
182
+ version: "1.7.0",
183
+ 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
+ type: "module",
185
+ main: "./dist/index.cjs",
186
+ module: "./dist/index.js",
187
+ types: "./dist/index.d.ts",
188
+ exports: {
189
+ ".": {
190
+ types: "./dist/index.d.ts",
191
+ import: "./dist/index.js",
192
+ require: "./dist/index.cjs"
193
+ },
194
+ "./contact-widget": {
195
+ types: "./dist/contact-widget.d.ts",
196
+ import: "./dist/contact-widget.js",
197
+ require: "./dist/contact-widget.cjs"
198
+ }
199
+ },
200
+ files: [
201
+ "dist",
202
+ "examples",
203
+ "README.md",
204
+ "LICENSE"
205
+ ],
206
+ keywords: [
207
+ "sendora",
208
+ "react-native",
209
+ "expo",
210
+ "analytics",
211
+ "tracking",
212
+ "customer-engagement"
213
+ ],
214
+ dependencies: {
215
+ "react-native-get-random-values": "^1.11.0"
216
+ },
217
+ peerDependencies: {
218
+ "@react-native-async-storage/async-storage": ">=1.17.0",
219
+ "react-native": ">=0.70.0",
220
+ "react-native-webview": ">=13.0.0"
221
+ },
222
+ peerDependenciesMeta: {
223
+ "@react-native-async-storage/async-storage": {
224
+ optional: false
225
+ },
226
+ "react-native": {
227
+ optional: false
228
+ },
229
+ "react-native-webview": {
230
+ optional: true
231
+ }
232
+ },
233
+ scripts: {
234
+ build: "tsup src/index.ts src/contact-widget.tsx --format esm,cjs --dts --clean --external react --external react-native --external react-native-webview",
235
+ typecheck: "tsc --noEmit"
236
+ },
237
+ devDependencies: {
238
+ "@types/react": "^19.2.16",
239
+ "react-native": "^0.85.3",
240
+ "react-native-webview": "^13.16.1",
241
+ tsup: "^8.0.0",
242
+ typescript: "^5.0.0"
243
+ },
244
+ license: "MIT",
245
+ repository: {
246
+ type: "git",
247
+ url: "https://github.com/sendoracloud/sdk-react-native"
248
+ },
249
+ homepage: "https://sendoracloud.com/sdks"
250
+ };
251
+
252
+ // src/version.ts
253
+ var SDK_VERSION = package_default.version;
254
+
179
255
  // src/auth.ts
180
256
  import { AppState as RnAppState } from "react-native";
181
257
  var TOKEN_KEY = "auth_access_token";
@@ -234,6 +310,14 @@ var Auth = class {
234
310
  this.refreshCooldownUntil = 0;
235
311
  this.takeoverListeners = /* @__PURE__ */ new Set();
236
312
  this.lastTakeover = null;
313
+ /**
314
+ * Anon refresh token captured at `signIn()` time, stashed keyed to the
315
+ * issued `mfaChallengeToken` so the later `challengeMfa()` can forward it
316
+ * for device-takeover (s58.111). Before this, the MFA branch wiped the anon
317
+ * identity before the challenge resolved → the takeover hint was lost → one
318
+ * device ended with two user_ids + duplicate pushes (audit s58.203 fix).
319
+ */
320
+ this.pendingAnonTakeover = null;
237
321
  /**
238
322
  * Proactive refresh (s58.72). Fires when access-token age crosses
239
323
  * ~80% of TTL. Pairs with the AppState foreground listener so a
@@ -453,12 +537,7 @@ var Auth = class {
453
537
  */
454
538
  signIn(email, password) {
455
539
  return this.serialize(async () => {
456
- let prevAnonRefreshToken;
457
- if (this.user?.isAnonymous) {
458
- const stashed = this.hooks.storage.get(REFRESH_KEY);
459
- if (stashed) prevAnonRefreshToken = stashed;
460
- }
461
- if (this.user !== null) await this.wipeLocalIdentity();
540
+ const prevAnonRefreshToken = this.readPrevAnonRefreshToken();
462
541
  const res = await post(
463
542
  { apiUrl: this.hooks.apiUrl, publicKey: this.hooks.publicKey, debug: this.hooks.debug },
464
543
  "/api/v1/auth-service/login",
@@ -476,12 +555,16 @@ var Auth = class {
476
555
  }
477
556
  const r = parsed.data;
478
557
  if (r.mfaRequired === true && typeof r.mfaChallengeToken === "string") {
558
+ if (prevAnonRefreshToken) {
559
+ this.pendingAnonTakeover = { challengeToken: r.mfaChallengeToken, prevAnonRefreshToken };
560
+ }
479
561
  const u = r.user;
480
562
  return { mfaRequired: true, mfaChallengeToken: r.mfaChallengeToken, userId: u?.id ?? "" };
481
563
  }
482
564
  if (!isAuthApiResponse(parsed.data)) {
483
565
  throw new AuthError(parsed.error?.code ?? "AUTH_ERROR", parsed.error?.message ?? "Login failed");
484
566
  }
567
+ if (this.user !== null) await this.wipeLocalIdentity();
485
568
  this.persist(parsed.data);
486
569
  return parsed.data.user;
487
570
  });
@@ -563,13 +646,15 @@ var Auth = class {
563
646
  */
564
647
  challengeMfa(mfaChallengeToken, code) {
565
648
  return this.serialize(async () => {
566
- const prevAnonRefreshToken = this.readPrevAnonRefreshToken();
649
+ const stashed = this.pendingAnonTakeover?.challengeToken === mfaChallengeToken ? this.pendingAnonTakeover.prevAnonRefreshToken : void 0;
650
+ const prevAnonRefreshToken = stashed ?? this.readPrevAnonRefreshToken();
567
651
  const body = {
568
652
  challengeToken: mfaChallengeToken,
569
653
  code
570
654
  };
571
655
  if (prevAnonRefreshToken) body.prevAnonRefreshToken = prevAnonRefreshToken;
572
656
  const data = await this.callAuth("/api/v1/auth-service/mfa/challenge", body);
657
+ if (this.user !== null) await this.wipeLocalIdentity();
573
658
  this.persist(data);
574
659
  return data.user;
575
660
  });
@@ -1109,6 +1194,7 @@ var Auth = class {
1109
1194
  this.accessToken = null;
1110
1195
  this.accessExpiresAt = 0;
1111
1196
  this.lastTakeover = null;
1197
+ this.pendingAnonTakeover = null;
1112
1198
  this.hooks.storage.remove(USER_KEY);
1113
1199
  this.hooks.storage.remove(TOKEN_KEY);
1114
1200
  this.hooks.storage.remove(TOKEN_EXPIRES_KEY);
@@ -1511,6 +1597,7 @@ var Links = class {
1511
1597
  async doCreate(input) {
1512
1598
  const body = { title: input.title };
1513
1599
  if (input.fallbackUrl) body.fallbackUrl = input.fallbackUrl;
1600
+ if (input.noAppMode) body.noAppMode = input.noAppMode;
1514
1601
  if (input.iosDeepLinkPath) body.iosDeepLinkPath = input.iosDeepLinkPath;
1515
1602
  if (input.androidDeepLinkPath) body.androidDeepLinkPath = input.androidDeepLinkPath;
1516
1603
  if (input.linkData) body.linkData = input.linkData;
@@ -1801,7 +1888,6 @@ function installAutoTrack(args) {
1801
1888
  }
1802
1889
 
1803
1890
  // src/index.ts
1804
- var SDK_VERSION = "1.0.0";
1805
1891
  var DEFAULT_API_URL = "https://api.sendoracloud.com";
1806
1892
  var ANON_KEY = "anon_id";
1807
1893
  var USER_ID_KEY = "user_id";
@@ -2345,11 +2431,12 @@ var SendoraSDK = class {
2345
2431
  const token = await self.auth.getAccessToken();
2346
2432
  if (!token) return 0;
2347
2433
  try {
2348
- const res = await fetch(
2349
- `${self.config.apiUrl}/api/v1/widgets/${encodeURIComponent(widgetId)}/my-tickets/unread-count`,
2350
- { headers: { Authorization: `Bearer ${token}` } }
2434
+ const res = await getJson(
2435
+ { apiUrl: self.config.apiUrl, publicKey: self.config.publicKey, debug: self.debug },
2436
+ `/api/v1/widgets/${encodeURIComponent(widgetId)}/my-tickets/unread-count`,
2437
+ { Authorization: `Bearer ${token}` }
2351
2438
  );
2352
- if (!res.ok) return 0;
2439
+ if (!res || !res.ok) return 0;
2353
2440
  const parsed = await res.json();
2354
2441
  return parsed?.data?.count ?? 0;
2355
2442
  } catch {
@@ -2378,12 +2465,11 @@ var SendoraSDK = class {
2378
2465
  if (!input.query || input.query.trim().length === 0) return [];
2379
2466
  const params = new URLSearchParams({ query: input.query });
2380
2467
  if (input.limit) params.set("limit", String(input.limit));
2381
- const url = `${self.config.apiUrl}/api/v1/kb/search?${params.toString()}`;
2382
- const res = await fetch(url, {
2383
- method: "GET",
2384
- headers: { "x-api-key": self.config.publicKey }
2385
- });
2386
- if (!res.ok) throw new Error(`[sendoracloud] kb.search failed (${res.status})`);
2468
+ const res = await getJson(
2469
+ { apiUrl: self.config.apiUrl, publicKey: self.config.publicKey, debug: self.debug },
2470
+ `/api/v1/kb/search?${params.toString()}`
2471
+ );
2472
+ if (!res || !res.ok) throw new Error(`[sendoracloud] kb.search failed (${res?.status ?? "network"})`);
2387
2473
  const parsed = await res.json();
2388
2474
  if (!parsed.success || !Array.isArray(parsed.data)) {
2389
2475
  throw new Error("[sendoracloud] kb.search: bad response");
@@ -2469,11 +2555,11 @@ var SendoraSDK = class {
2469
2555
  fetchConfig: async (surveyId) => {
2470
2556
  if (!self.config) throw new Error("[sendoracloud] init() must complete before surveys.fetchConfig().");
2471
2557
  if (typeof surveyId !== "string" || surveyId.length === 0) return null;
2472
- const url = `${self.config.apiUrl}/api/v1/surveys/${encodeURIComponent(surveyId)}/config`;
2473
- const res = await fetch(url, {
2474
- method: "GET",
2475
- headers: { "x-api-key": self.config.publicKey }
2476
- });
2558
+ const res = await getJson(
2559
+ { apiUrl: self.config.apiUrl, publicKey: self.config.publicKey, debug: self.debug },
2560
+ `/api/v1/surveys/${encodeURIComponent(surveyId)}/config`
2561
+ );
2562
+ if (!res) return null;
2477
2563
  if (res.status === 404) return null;
2478
2564
  if (!res.ok) throw new Error(`[sendoracloud] surveys.fetchConfig failed (${res.status})`);
2479
2565
  const parsed = await res.json();
@@ -2518,12 +2604,11 @@ var SendoraSDK = class {
2518
2604
  return {
2519
2605
  fetchActive: async () => {
2520
2606
  if (!self.config) throw new Error("[sendoracloud] init() must complete before messages.fetchActive().");
2521
- const url = `${self.config.apiUrl}/api/v1/in-app-messages/active`;
2522
- const res = await fetch(url, {
2523
- method: "GET",
2524
- headers: { "x-api-key": self.config.publicKey }
2525
- });
2526
- if (!res.ok) throw new Error(`[sendoracloud] messages.fetchActive failed (${res.status})`);
2607
+ const res = await getJson(
2608
+ { apiUrl: self.config.apiUrl, publicKey: self.config.publicKey, debug: self.debug },
2609
+ "/api/v1/in-app-messages/active"
2610
+ );
2611
+ if (!res || !res.ok) throw new Error(`[sendoracloud] messages.fetchActive failed (${res?.status ?? "network"})`);
2527
2612
  const parsed = await res.json();
2528
2613
  if (!parsed.success || !Array.isArray(parsed.data)) {
2529
2614
  throw new Error("[sendoracloud] messages.fetchActive: bad response");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sendoracloud/sdk-react-native",
3
- "version": "1.6.1",
3
+ "version": "1.7.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",