@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 +114 -29
- package/dist/index.d.cts +26 -3
- package/dist/index.d.ts +26 -3
- package/dist/index.js +114 -29
- package/package.json +1 -1
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
|
-
|
|
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
|
|
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
|
|
2387
|
-
|
|
2388
|
-
{
|
|
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
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
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
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
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
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
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
|
|
54
|
-
*
|
|
55
|
-
*
|
|
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
|
|
54
|
-
*
|
|
55
|
-
*
|
|
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
|
-
|
|
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
|
|
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
|
|
2349
|
-
|
|
2350
|
-
{
|
|
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
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
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
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
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
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
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.
|
|
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",
|