@sendoracloud/sdk-react-native 1.6.1 → 1.6.2
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 +113 -29
- package/dist/index.d.cts +18 -3
- package/dist/index.d.ts +18 -3
- package/dist/index.js +113 -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.6.2",
|
|
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);
|
|
@@ -1839,7 +1925,6 @@ function installAutoTrack(args) {
|
|
|
1839
1925
|
}
|
|
1840
1926
|
|
|
1841
1927
|
// src/index.ts
|
|
1842
|
-
var SDK_VERSION = "1.0.0";
|
|
1843
1928
|
var DEFAULT_API_URL = "https://api.sendoracloud.com";
|
|
1844
1929
|
var ANON_KEY = "anon_id";
|
|
1845
1930
|
var USER_ID_KEY = "user_id";
|
|
@@ -2383,11 +2468,12 @@ var SendoraSDK = class {
|
|
|
2383
2468
|
const token = await self.auth.getAccessToken();
|
|
2384
2469
|
if (!token) return 0;
|
|
2385
2470
|
try {
|
|
2386
|
-
const res = await
|
|
2387
|
-
|
|
2388
|
-
{
|
|
2471
|
+
const res = await getJson(
|
|
2472
|
+
{ apiUrl: self.config.apiUrl, publicKey: self.config.publicKey, debug: self.debug },
|
|
2473
|
+
`/api/v1/widgets/${encodeURIComponent(widgetId)}/my-tickets/unread-count`,
|
|
2474
|
+
{ Authorization: `Bearer ${token}` }
|
|
2389
2475
|
);
|
|
2390
|
-
if (!res.ok) return 0;
|
|
2476
|
+
if (!res || !res.ok) return 0;
|
|
2391
2477
|
const parsed = await res.json();
|
|
2392
2478
|
return parsed?.data?.count ?? 0;
|
|
2393
2479
|
} catch {
|
|
@@ -2416,12 +2502,11 @@ var SendoraSDK = class {
|
|
|
2416
2502
|
if (!input.query || input.query.trim().length === 0) return [];
|
|
2417
2503
|
const params = new URLSearchParams({ query: input.query });
|
|
2418
2504
|
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})`);
|
|
2505
|
+
const res = await getJson(
|
|
2506
|
+
{ apiUrl: self.config.apiUrl, publicKey: self.config.publicKey, debug: self.debug },
|
|
2507
|
+
`/api/v1/kb/search?${params.toString()}`
|
|
2508
|
+
);
|
|
2509
|
+
if (!res || !res.ok) throw new Error(`[sendoracloud] kb.search failed (${res?.status ?? "network"})`);
|
|
2425
2510
|
const parsed = await res.json();
|
|
2426
2511
|
if (!parsed.success || !Array.isArray(parsed.data)) {
|
|
2427
2512
|
throw new Error("[sendoracloud] kb.search: bad response");
|
|
@@ -2507,11 +2592,11 @@ var SendoraSDK = class {
|
|
|
2507
2592
|
fetchConfig: async (surveyId) => {
|
|
2508
2593
|
if (!self.config) throw new Error("[sendoracloud] init() must complete before surveys.fetchConfig().");
|
|
2509
2594
|
if (typeof surveyId !== "string" || surveyId.length === 0) return null;
|
|
2510
|
-
const
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2595
|
+
const res = await getJson(
|
|
2596
|
+
{ apiUrl: self.config.apiUrl, publicKey: self.config.publicKey, debug: self.debug },
|
|
2597
|
+
`/api/v1/surveys/${encodeURIComponent(surveyId)}/config`
|
|
2598
|
+
);
|
|
2599
|
+
if (!res) return null;
|
|
2515
2600
|
if (res.status === 404) return null;
|
|
2516
2601
|
if (!res.ok) throw new Error(`[sendoracloud] surveys.fetchConfig failed (${res.status})`);
|
|
2517
2602
|
const parsed = await res.json();
|
|
@@ -2556,12 +2641,11 @@ var SendoraSDK = class {
|
|
|
2556
2641
|
return {
|
|
2557
2642
|
fetchActive: async () => {
|
|
2558
2643
|
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})`);
|
|
2644
|
+
const res = await getJson(
|
|
2645
|
+
{ apiUrl: self.config.apiUrl, publicKey: self.config.publicKey, debug: self.debug },
|
|
2646
|
+
"/api/v1/in-app-messages/active"
|
|
2647
|
+
);
|
|
2648
|
+
if (!res || !res.ok) throw new Error(`[sendoracloud] messages.fetchActive failed (${res?.status ?? "network"})`);
|
|
2565
2649
|
const parsed = await res.json();
|
|
2566
2650
|
if (!parsed.success || !Array.isArray(parsed.data)) {
|
|
2567
2651
|
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
|
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
|
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.6.2",
|
|
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);
|
|
@@ -1801,7 +1887,6 @@ function installAutoTrack(args) {
|
|
|
1801
1887
|
}
|
|
1802
1888
|
|
|
1803
1889
|
// src/index.ts
|
|
1804
|
-
var SDK_VERSION = "1.0.0";
|
|
1805
1890
|
var DEFAULT_API_URL = "https://api.sendoracloud.com";
|
|
1806
1891
|
var ANON_KEY = "anon_id";
|
|
1807
1892
|
var USER_ID_KEY = "user_id";
|
|
@@ -2345,11 +2430,12 @@ var SendoraSDK = class {
|
|
|
2345
2430
|
const token = await self.auth.getAccessToken();
|
|
2346
2431
|
if (!token) return 0;
|
|
2347
2432
|
try {
|
|
2348
|
-
const res = await
|
|
2349
|
-
|
|
2350
|
-
{
|
|
2433
|
+
const res = await getJson(
|
|
2434
|
+
{ apiUrl: self.config.apiUrl, publicKey: self.config.publicKey, debug: self.debug },
|
|
2435
|
+
`/api/v1/widgets/${encodeURIComponent(widgetId)}/my-tickets/unread-count`,
|
|
2436
|
+
{ Authorization: `Bearer ${token}` }
|
|
2351
2437
|
);
|
|
2352
|
-
if (!res.ok) return 0;
|
|
2438
|
+
if (!res || !res.ok) return 0;
|
|
2353
2439
|
const parsed = await res.json();
|
|
2354
2440
|
return parsed?.data?.count ?? 0;
|
|
2355
2441
|
} catch {
|
|
@@ -2378,12 +2464,11 @@ var SendoraSDK = class {
|
|
|
2378
2464
|
if (!input.query || input.query.trim().length === 0) return [];
|
|
2379
2465
|
const params = new URLSearchParams({ query: input.query });
|
|
2380
2466
|
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})`);
|
|
2467
|
+
const res = await getJson(
|
|
2468
|
+
{ apiUrl: self.config.apiUrl, publicKey: self.config.publicKey, debug: self.debug },
|
|
2469
|
+
`/api/v1/kb/search?${params.toString()}`
|
|
2470
|
+
);
|
|
2471
|
+
if (!res || !res.ok) throw new Error(`[sendoracloud] kb.search failed (${res?.status ?? "network"})`);
|
|
2387
2472
|
const parsed = await res.json();
|
|
2388
2473
|
if (!parsed.success || !Array.isArray(parsed.data)) {
|
|
2389
2474
|
throw new Error("[sendoracloud] kb.search: bad response");
|
|
@@ -2469,11 +2554,11 @@ var SendoraSDK = class {
|
|
|
2469
2554
|
fetchConfig: async (surveyId) => {
|
|
2470
2555
|
if (!self.config) throw new Error("[sendoracloud] init() must complete before surveys.fetchConfig().");
|
|
2471
2556
|
if (typeof surveyId !== "string" || surveyId.length === 0) return null;
|
|
2472
|
-
const
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2557
|
+
const res = await getJson(
|
|
2558
|
+
{ apiUrl: self.config.apiUrl, publicKey: self.config.publicKey, debug: self.debug },
|
|
2559
|
+
`/api/v1/surveys/${encodeURIComponent(surveyId)}/config`
|
|
2560
|
+
);
|
|
2561
|
+
if (!res) return null;
|
|
2477
2562
|
if (res.status === 404) return null;
|
|
2478
2563
|
if (!res.ok) throw new Error(`[sendoracloud] surveys.fetchConfig failed (${res.status})`);
|
|
2479
2564
|
const parsed = await res.json();
|
|
@@ -2518,12 +2603,11 @@ var SendoraSDK = class {
|
|
|
2518
2603
|
return {
|
|
2519
2604
|
fetchActive: async () => {
|
|
2520
2605
|
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})`);
|
|
2606
|
+
const res = await getJson(
|
|
2607
|
+
{ apiUrl: self.config.apiUrl, publicKey: self.config.publicKey, debug: self.debug },
|
|
2608
|
+
"/api/v1/in-app-messages/active"
|
|
2609
|
+
);
|
|
2610
|
+
if (!res || !res.ok) throw new Error(`[sendoracloud] messages.fetchActive failed (${res?.status ?? "network"})`);
|
|
2527
2611
|
const parsed = await res.json();
|
|
2528
2612
|
if (!parsed.success || !Array.isArray(parsed.data)) {
|
|
2529
2613
|
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.
|
|
3
|
+
"version": "1.6.2",
|
|
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",
|