@usergist/feedback-react-native 0.1.2 → 0.1.4
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/CHANGELOG.md +12 -0
- package/README.md +4 -1
- package/dist/UserGist.d.ts +9 -0
- package/dist/UserGist.d.ts.map +1 -1
- package/dist/UserGist.js +307 -104
- package/dist/UserGist.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/internal/context.d.ts +1 -1
- package/dist/internal/context.js +1 -1
- package/dist/internal/engine.d.ts +6 -0
- package/dist/internal/engine.d.ts.map +1 -1
- package/dist/internal/engine.js +107 -36
- package/dist/internal/engine.js.map +1 -1
- package/dist/internal/identity-lifecycle.d.ts +16 -0
- package/dist/internal/identity-lifecycle.d.ts.map +1 -0
- package/dist/internal/identity-lifecycle.js +96 -0
- package/dist/internal/identity-lifecycle.js.map +1 -0
- package/dist/internal/mutation-queue.d.ts +2 -2
- package/dist/internal/mutation-queue.d.ts.map +1 -1
- package/dist/internal/mutation-queue.js +19 -2
- package/dist/internal/mutation-queue.js.map +1 -1
- package/dist/internal/push-subscription.d.ts +11 -0
- package/dist/internal/push-subscription.d.ts.map +1 -0
- package/dist/internal/push-subscription.js +20 -0
- package/dist/internal/push-subscription.js.map +1 -0
- package/dist/internal/session-revocations.d.ts +5 -0
- package/dist/internal/session-revocations.d.ts.map +1 -0
- package/dist/internal/session-revocations.js +58 -0
- package/dist/internal/session-revocations.js.map +1 -0
- package/dist/internal/storage.d.ts +3 -0
- package/dist/internal/storage.d.ts.map +1 -1
- package/dist/internal/storage.js +76 -60
- package/dist/internal/storage.js.map +1 -1
- package/dist/internal/transport.d.ts +14 -2
- package/dist/internal/transport.d.ts.map +1 -1
- package/dist/internal/transport.js +31 -14
- package/dist/internal/transport.js.map +1 -1
- package/dist/internal/user-state.d.ts +2 -1
- package/dist/internal/user-state.d.ts.map +1 -1
- package/dist/internal/user-state.js +3 -2
- package/dist/internal/user-state.js.map +1 -1
- package/package.json +2 -2
package/dist/UserGist.js
CHANGED
|
@@ -1,12 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
// bind a server-proven identity before any lifecycle event is emitted.
|
|
6
|
-
// - All public methods catch their own errors and report via debug. The
|
|
7
|
-
// SDK never throws across its boundary.
|
|
8
|
-
// - Track is synchronous for the caller: it enqueues, schedules a flush,
|
|
9
|
-
// and runs the trigger matcher — zero awaits.
|
|
1
|
+
import { drainRevocations, hasPendingRevocations } from './internal/session-revocations.js';
|
|
2
|
+
import { setPushSubscriptionStateHandler, pushSubscriptionChanged } from './internal/push-subscription.js';
|
|
3
|
+
import { cancelIdentityRecovery, identityChanged, setIdentityStateHandler, setSubjectTokenProvider, getIdentityState } from './internal/identity-lifecycle.js';
|
|
4
|
+
import { userPropertiesUpdateSchema } from '@usergist/sdk-core/mobile';
|
|
10
5
|
import { REQUEST_COMMENTED_EVENT_NAME, REQUEST_COMMENT_EDITED_EVENT_NAME, REQUEST_COMMENT_DELETED_EVENT_NAME, } from '@usergist/sdk-core/mobile';
|
|
11
6
|
import { asEventProps, clearAllState, createEngine, emitAppOpenWhenConsentReady, emitAppVersionChanged, ensureHydrated, enqueueAndEvaluate, flushNow, flushMutations, pollInstructions, refreshTargetingRules, recordServerBackedEventLocally, submitResponse, } from './internal/engine.js';
|
|
12
7
|
import { setDebugEnabled, reportError, debugLog, logTrace, getTraces, setDiagnosticHandler, } from './internal/debug.js';
|
|
@@ -16,10 +11,16 @@ import { UserGistPushNative, onTokenReceived, onNotificationReceived, onNotifica
|
|
|
16
11
|
import { Push } from './Push.js';
|
|
17
12
|
import { AppState, Platform } from 'react-native';
|
|
18
13
|
import { generateEventId, validateIdentifyTransition } from './internal/identity.js';
|
|
19
|
-
import { configureStorageAdapter, } from './internal/storage.js';
|
|
14
|
+
import { STORAGE_KEYS, configureStorageAdapter, } from './internal/storage.js';
|
|
20
15
|
import { surveyCompletionOutcome } from './internal/survey-completion.js';
|
|
21
16
|
import { USERGIST_SDK_VERSION } from './internal/context.js';
|
|
22
17
|
let engine = null;
|
|
18
|
+
let pushRegistrationQueue = Promise.resolve();
|
|
19
|
+
function serializePushRegistration(operation) {
|
|
20
|
+
const task = pushRegistrationQueue.then(operation);
|
|
21
|
+
pushRegistrationQueue = task.catch(() => undefined);
|
|
22
|
+
return task;
|
|
23
|
+
}
|
|
23
24
|
let runtimeStarted = false;
|
|
24
25
|
let runtimeStartPromise = null;
|
|
25
26
|
let resetPromise = null;
|
|
@@ -37,6 +38,17 @@ function ensureRequestsCache() {
|
|
|
37
38
|
}
|
|
38
39
|
return requestsCache;
|
|
39
40
|
}
|
|
41
|
+
function requestSessionIsCurrent(e) {
|
|
42
|
+
const generation = e.resetGeneration;
|
|
43
|
+
const consentVersion = e.consent.get().version;
|
|
44
|
+
const identity = e.identity.get();
|
|
45
|
+
return () => !e.resetting &&
|
|
46
|
+
generation === e.resetGeneration &&
|
|
47
|
+
consentVersion === e.consent.get().version &&
|
|
48
|
+
e.consent.allowsFeedback() &&
|
|
49
|
+
identity.anonymousId === e.identity.get().anonymousId &&
|
|
50
|
+
identity.externalId === e.identity.get().externalId;
|
|
51
|
+
}
|
|
40
52
|
function requireEngine() {
|
|
41
53
|
if (!engine) {
|
|
42
54
|
throw new Error('UserGist.init must be called before using the SDK');
|
|
@@ -47,6 +59,20 @@ function ensureEngine(config) {
|
|
|
47
59
|
if (engine)
|
|
48
60
|
return engine;
|
|
49
61
|
engine = createEngine(config);
|
|
62
|
+
const pushEngine = engine;
|
|
63
|
+
pushEngine.retryPushRegistration = async () => {
|
|
64
|
+
if (pushEngine.resetting)
|
|
65
|
+
return;
|
|
66
|
+
const saved = await pushEngine.storage.getJson(STORAGE_KEYS.pushDevice);
|
|
67
|
+
if (!saved)
|
|
68
|
+
return;
|
|
69
|
+
pushEngine.pushTokenAvailable = true;
|
|
70
|
+
if (pushEngine !== engine || pushEngine.resetting)
|
|
71
|
+
return;
|
|
72
|
+
pushSubscriptionChanged(pushEngine);
|
|
73
|
+
if (pushEngine.consent.allowsPush())
|
|
74
|
+
await registerPushDevice(saved.token, saved.platform, { environment: saved.environment }, true);
|
|
75
|
+
};
|
|
50
76
|
surveyStore = createSurveyStore(config.writeKey);
|
|
51
77
|
return engine;
|
|
52
78
|
}
|
|
@@ -135,6 +161,10 @@ function attachEnginePushListeners() {
|
|
|
135
161
|
});
|
|
136
162
|
}
|
|
137
163
|
function forwardToPushHandler(p, kind) {
|
|
164
|
+
if (!engine?.consent.allowsPush() || engine.resetting)
|
|
165
|
+
return;
|
|
166
|
+
if (p.data?.usergist_anonymous_id && p.data.usergist_anonymous_id !== engine.identity.get().anonymousId)
|
|
167
|
+
return;
|
|
138
168
|
const data = p.data ?? {};
|
|
139
169
|
if (p.deliveryId) {
|
|
140
170
|
;
|
|
@@ -215,6 +245,10 @@ export const UserGist = {
|
|
|
215
245
|
return 'rejected';
|
|
216
246
|
}
|
|
217
247
|
},
|
|
248
|
+
setPushSubscriptionStateHandler,
|
|
249
|
+
setIdentityStateHandler,
|
|
250
|
+
setSubjectTokenProvider,
|
|
251
|
+
getIdentityState,
|
|
218
252
|
identify(userId, properties, subjectToken) {
|
|
219
253
|
void UserGist.identifyAsync(userId, properties, subjectToken);
|
|
220
254
|
},
|
|
@@ -225,7 +259,10 @@ export const UserGist = {
|
|
|
225
259
|
return 'rejected';
|
|
226
260
|
}
|
|
227
261
|
const e = requireEngine();
|
|
262
|
+
const generation = e.resetGeneration;
|
|
228
263
|
await ensureHydrated(e);
|
|
264
|
+
if (e.resetting || e.resetGeneration !== generation)
|
|
265
|
+
return 'rejected';
|
|
229
266
|
const identity = e.identity.get();
|
|
230
267
|
const pendingIdentity = e.mutations.peek();
|
|
231
268
|
const transition = validateIdentifyTransition(identity.externalId, pendingIdentity?.kind === 'identify' ? pendingIdentity.payload.externalId : null, userId);
|
|
@@ -233,8 +270,13 @@ export const UserGist = {
|
|
|
233
270
|
reportError('identify rejected: call reset() before switching users');
|
|
234
271
|
return 'rejected';
|
|
235
272
|
}
|
|
236
|
-
const cleanProps = e.consent.allowsAnalytics()
|
|
273
|
+
const cleanProps = e.consent.allowsAnalytics()
|
|
274
|
+
? (properties && Object.keys(properties).length
|
|
275
|
+
? userPropertiesUpdateSchema.parse({ mutationId: generateEventId(), set: properties }).set
|
|
276
|
+
: undefined)
|
|
277
|
+
: undefined;
|
|
237
278
|
if (transition === 'already_identified' &&
|
|
279
|
+
(!subjectToken || subjectToken === e.subjectToken) &&
|
|
238
280
|
(!cleanProps || Object.keys(cleanProps).length === 0)) {
|
|
239
281
|
return 'synced';
|
|
240
282
|
}
|
|
@@ -242,6 +284,7 @@ export const UserGist = {
|
|
|
242
284
|
reportError('identify requires a server-minted subject token');
|
|
243
285
|
return 'rejected';
|
|
244
286
|
}
|
|
287
|
+
identityChanged(e, 'identifying');
|
|
245
288
|
const mutationId = await e.mutations.enqueue('identify', 'essential', {
|
|
246
289
|
subjectToken,
|
|
247
290
|
anonymousId: identity.anonymousId,
|
|
@@ -249,7 +292,10 @@ export const UserGist = {
|
|
|
249
292
|
...(cleanProps ? { properties: cleanProps } : {}),
|
|
250
293
|
}, `identify:${userId}`);
|
|
251
294
|
const result = await flushMutations(e);
|
|
295
|
+
if (e.resetting || e.resetGeneration !== generation)
|
|
296
|
+
return 'rejected';
|
|
252
297
|
if (result.permanentlyRejectedIds.has(mutationId)) {
|
|
298
|
+
identityChanged(e, 'rejected');
|
|
253
299
|
reportError('identify was rejected by the server');
|
|
254
300
|
return 'rejected';
|
|
255
301
|
}
|
|
@@ -265,6 +311,33 @@ export const UserGist = {
|
|
|
265
311
|
return 'rejected';
|
|
266
312
|
}
|
|
267
313
|
},
|
|
314
|
+
/** Update the current user, including anonymous users. Durable and retried on reconnect. */
|
|
315
|
+
async setUserProperties(properties, unset = []) {
|
|
316
|
+
try {
|
|
317
|
+
const e = requireEngine();
|
|
318
|
+
const generation = e.resetGeneration;
|
|
319
|
+
await ensureHydrated(e);
|
|
320
|
+
if (e !== engine ||
|
|
321
|
+
e.resetting ||
|
|
322
|
+
e.resetGeneration !== generation ||
|
|
323
|
+
!e.consent.allowsAnalytics())
|
|
324
|
+
return 'rejected';
|
|
325
|
+
const update = userPropertiesUpdateSchema.parse({
|
|
326
|
+
mutationId: generateEventId(),
|
|
327
|
+
set: properties,
|
|
328
|
+
unset: [...unset],
|
|
329
|
+
});
|
|
330
|
+
const mutationId = await e.mutations.enqueue('user-properties', 'analytics', { ...update, anonymousId: e.identity.get().anonymousId });
|
|
331
|
+
const result = await flushMutations(e);
|
|
332
|
+
if (result.permanentlyRejectedIds.has(mutationId))
|
|
333
|
+
return 'rejected';
|
|
334
|
+
return e.mutations.has(mutationId) ? 'queued' : 'synced';
|
|
335
|
+
}
|
|
336
|
+
catch (error) {
|
|
337
|
+
reportError('setUserProperties failed', error);
|
|
338
|
+
return 'rejected';
|
|
339
|
+
}
|
|
340
|
+
},
|
|
268
341
|
track(eventName, properties) {
|
|
269
342
|
try {
|
|
270
343
|
if (typeof eventName !== 'string' || eventName.length === 0) {
|
|
@@ -287,7 +360,29 @@ export const UserGist = {
|
|
|
287
360
|
if (purposes.survey === false)
|
|
288
361
|
e.presentation.invalidate('survey');
|
|
289
362
|
await ensureHydrated(e);
|
|
363
|
+
if (e.resetting)
|
|
364
|
+
return false;
|
|
290
365
|
const next = await e.consent.set(purposes);
|
|
366
|
+
if (e.resetting || next.version !== e.consent.get().version)
|
|
367
|
+
return false;
|
|
368
|
+
if (!next.feedback) {
|
|
369
|
+
requestsCache = null;
|
|
370
|
+
e.events.emit('dismissRequests', undefined);
|
|
371
|
+
}
|
|
372
|
+
if (e.resetting || next.version !== e.consent.get().version)
|
|
373
|
+
return false;
|
|
374
|
+
if (purposes.push === false) {
|
|
375
|
+
lastEnablePushOptions = null;
|
|
376
|
+
await pushRegistrationQueue;
|
|
377
|
+
if (e.resetting || next.version !== e.consent.get().version)
|
|
378
|
+
return false;
|
|
379
|
+
if (e.lastPushToken)
|
|
380
|
+
await invalidatePushDevice(e.lastPushToken, true);
|
|
381
|
+
if (!e.consent.allowsPush())
|
|
382
|
+
await UserGistPushNative.disablePush();
|
|
383
|
+
}
|
|
384
|
+
if (e.resetting || next.version !== e.consent.get().version)
|
|
385
|
+
return false;
|
|
291
386
|
const id = e.identity.get();
|
|
292
387
|
try {
|
|
293
388
|
await e.transport.consent({
|
|
@@ -307,6 +402,9 @@ export const UserGist = {
|
|
|
307
402
|
reportError('transport.consent failed', err);
|
|
308
403
|
return false;
|
|
309
404
|
}
|
|
405
|
+
pushSubscriptionChanged(e);
|
|
406
|
+
if (next.push)
|
|
407
|
+
void e.retryPushRegistration?.().catch(error => reportError('push registration deferred', error));
|
|
310
408
|
if (next.feedback || next.survey) {
|
|
311
409
|
await refreshTargetingRules(e, true);
|
|
312
410
|
}
|
|
@@ -323,23 +421,27 @@ export const UserGist = {
|
|
|
323
421
|
reset() {
|
|
324
422
|
try {
|
|
325
423
|
const e = requireEngine();
|
|
326
|
-
if (
|
|
327
|
-
return resetPromise
|
|
424
|
+
if (resetPromise)
|
|
425
|
+
return resetPromise;
|
|
328
426
|
e.resetting = true;
|
|
427
|
+
identityChanged(e, 'resetting');
|
|
428
|
+
pushSubscriptionChanged(e);
|
|
429
|
+
lastEnablePushOptions = null;
|
|
329
430
|
e.resetGeneration += 1;
|
|
431
|
+
e.transport.cancelAll();
|
|
432
|
+
cancelIdentityRecovery(e);
|
|
330
433
|
e.presentation.invalidate();
|
|
331
434
|
e.events.emit('resetSurfaces', undefined);
|
|
332
435
|
const operation = (async () => {
|
|
333
436
|
await ensureHydrated(e);
|
|
334
|
-
|
|
335
|
-
await UserGist.invalidatePushToken(e.lastPushToken);
|
|
336
|
-
await UserGistPushNative.disablePush().catch(() => undefined);
|
|
437
|
+
void UserGistPushNative.disablePush().catch(() => undefined);
|
|
337
438
|
await clearAllState(e);
|
|
439
|
+
requestsCache = null;
|
|
338
440
|
})();
|
|
339
441
|
const finalized = operation
|
|
340
|
-
.
|
|
442
|
+
.then(() => { e.resetting = false; pushSubscriptionChanged(e); })
|
|
443
|
+
.catch((err) => { identityChanged(e, 'reset-failed'); reportError('reset failed', err); throw err; })
|
|
341
444
|
.finally(() => {
|
|
342
|
-
e.resetting = false;
|
|
343
445
|
if (resetPromise === finalized)
|
|
344
446
|
resetPromise = null;
|
|
345
447
|
});
|
|
@@ -448,67 +550,10 @@ export const UserGist = {
|
|
|
448
550
|
}
|
|
449
551
|
},
|
|
450
552
|
async registerPushToken(token, platform, opts) {
|
|
451
|
-
|
|
452
|
-
if (!token)
|
|
453
|
-
return;
|
|
454
|
-
const e = requireEngine();
|
|
455
|
-
await ensureHydrated(e);
|
|
456
|
-
if (!e.consent.allowsPush())
|
|
457
|
-
return;
|
|
458
|
-
// Consent is enforced server-side (the register-token route reads
|
|
459
|
-
// consent_log and refuses if push was explicitly declined). We no
|
|
460
|
-
// longer short-circuit here because the host app typically calls
|
|
461
|
-
// setConsent and registerPushToken in parallel; racing the local
|
|
462
|
-
// consent hydration is not our problem to solve.
|
|
463
|
-
const id = e.identity.get();
|
|
464
|
-
const environment = opts?.environment ??
|
|
465
|
-
(e.config.environment === 'production' ? 'production' : 'sandbox');
|
|
466
|
-
const registrationKey = [
|
|
467
|
-
id.anonymousId,
|
|
468
|
-
id.externalId ?? '',
|
|
469
|
-
platform,
|
|
470
|
-
environment,
|
|
471
|
-
token,
|
|
472
|
-
].join(':');
|
|
473
|
-
if (e.lastPushRegistrationKey === registrationKey &&
|
|
474
|
-
Date.now() - e.lastPushRegistrationAt < 24 * 60 * 60_000)
|
|
475
|
-
return;
|
|
476
|
-
await e.transport.pushRegisterToken({
|
|
477
|
-
anonymousId: id.anonymousId,
|
|
478
|
-
externalId: id.externalId ?? null,
|
|
479
|
-
token,
|
|
480
|
-
platform,
|
|
481
|
-
environment,
|
|
482
|
-
language: undefined,
|
|
483
|
-
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
|
484
|
-
appVersion: undefined,
|
|
485
|
-
sdkVersion: `rn-${USERGIST_SDK_VERSION}`,
|
|
486
|
-
optIn: true,
|
|
487
|
-
});
|
|
488
|
-
e.lastPushToken = token;
|
|
489
|
-
e.lastPushRegistrationKey = registrationKey;
|
|
490
|
-
e.lastPushRegistrationAt = Date.now();
|
|
491
|
-
debugLog('push token registered with server');
|
|
492
|
-
}
|
|
493
|
-
catch (err) {
|
|
494
|
-
reportError('registerPushToken failed', err);
|
|
495
|
-
}
|
|
553
|
+
await registerPushDevice(token, platform, opts);
|
|
496
554
|
},
|
|
497
555
|
async invalidatePushToken(token) {
|
|
498
|
-
|
|
499
|
-
if (!token)
|
|
500
|
-
return;
|
|
501
|
-
const e = requireEngine();
|
|
502
|
-
await ensureHydrated(e);
|
|
503
|
-
const id = e.identity.get();
|
|
504
|
-
await e.transport.pushInvalidateToken({
|
|
505
|
-
anonymousId: id.anonymousId,
|
|
506
|
-
token,
|
|
507
|
-
});
|
|
508
|
-
}
|
|
509
|
-
catch (err) {
|
|
510
|
-
reportError('invalidatePushToken failed', err);
|
|
511
|
-
}
|
|
556
|
+
await invalidatePushDevice(token);
|
|
512
557
|
},
|
|
513
558
|
/**
|
|
514
559
|
* Re-bind the latest registered device token to a newly-identified user.
|
|
@@ -618,6 +663,34 @@ export const UserGist = {
|
|
|
618
663
|
// react-native-onesignal, WonderPush, or any other push SDK.
|
|
619
664
|
async enablePush(opts) {
|
|
620
665
|
try {
|
|
666
|
+
const e = requireEngine();
|
|
667
|
+
await ensureHydrated(e);
|
|
668
|
+
if (!e.consent.allowsPush() || e.resetting)
|
|
669
|
+
return {
|
|
670
|
+
granted: false,
|
|
671
|
+
status: 'not_determined',
|
|
672
|
+
platform: Platform.OS === 'ios' ? 'ios' : 'android',
|
|
673
|
+
error: 'consent_required',
|
|
674
|
+
};
|
|
675
|
+
const generation = e.resetGeneration;
|
|
676
|
+
const consentVersion = e.consent.get().version;
|
|
677
|
+
const current = () => !e.resetting &&
|
|
678
|
+
generation === e.resetGeneration &&
|
|
679
|
+
consentVersion === e.consent.get().version &&
|
|
680
|
+
e.consent.allowsPush();
|
|
681
|
+
const cancelled = () => ({
|
|
682
|
+
granted: false,
|
|
683
|
+
status: 'not_determined',
|
|
684
|
+
platform: Platform.OS === 'ios' ? 'ios' : 'android',
|
|
685
|
+
error: 'session_changed',
|
|
686
|
+
});
|
|
687
|
+
opts = {
|
|
688
|
+
...opts,
|
|
689
|
+
environment: opts?.environment ??
|
|
690
|
+
defaultEnvironment(),
|
|
691
|
+
};
|
|
692
|
+
if (!current())
|
|
693
|
+
return cancelled();
|
|
621
694
|
if (!enginePushListenersAttached)
|
|
622
695
|
attachEnginePushListeners();
|
|
623
696
|
// Snapshot the caller's options so the AppState foreground listener
|
|
@@ -628,12 +701,16 @@ export const UserGist = {
|
|
|
628
701
|
installDelegateProxy: opts?.installDelegateProxy,
|
|
629
702
|
};
|
|
630
703
|
const result = await UserGistPushNative.enablePush(opts ?? {});
|
|
704
|
+
// Reset/withdrawal owns native cleanup. An old permission callback must
|
|
705
|
+
// not disable a new account's subsequently enabled notification adapter.
|
|
706
|
+
if (!current())
|
|
707
|
+
return cancelled();
|
|
631
708
|
// Server-side registration runs on the tokenReceived event; if the
|
|
632
709
|
// native module already had a cached token it surfaces here.
|
|
633
710
|
if (result.granted && result.token) {
|
|
634
711
|
const platform = (result.platform === 'ios' ? 'ios' : 'android');
|
|
635
712
|
await UserGist.registerPushToken(result.token, platform, {
|
|
636
|
-
environment: opts?.environment ??
|
|
713
|
+
environment: opts?.environment ?? defaultEnvironment(),
|
|
637
714
|
});
|
|
638
715
|
}
|
|
639
716
|
return result;
|
|
@@ -642,23 +719,19 @@ export const UserGist = {
|
|
|
642
719
|
reportError('enablePush failed', err);
|
|
643
720
|
return {
|
|
644
721
|
granted: false,
|
|
645
|
-
status: '
|
|
646
|
-
|
|
722
|
+
status: 'not_determined',
|
|
723
|
+
error: 'push_setup_failed',
|
|
724
|
+
platform: Platform.OS === 'ios' ? 'ios' : 'android',
|
|
647
725
|
};
|
|
648
726
|
}
|
|
649
727
|
},
|
|
650
728
|
async disablePush() {
|
|
729
|
+
lastEnablePushOptions = null;
|
|
651
730
|
try {
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
if (e) {
|
|
657
|
-
e.lastPushToken = null;
|
|
658
|
-
e.lastPushRegistrationKey = null;
|
|
659
|
-
e.lastPushRegistrationAt = 0;
|
|
660
|
-
void UserGist.setConsent({ push: false });
|
|
661
|
-
}
|
|
731
|
+
if (engine)
|
|
732
|
+
await UserGist.setConsent({ push: false });
|
|
733
|
+
else
|
|
734
|
+
await UserGistPushNative.disablePush();
|
|
662
735
|
}
|
|
663
736
|
catch (err) {
|
|
664
737
|
reportError('disablePush failed', err);
|
|
@@ -962,6 +1035,9 @@ export const UserGist = {
|
|
|
962
1035
|
try {
|
|
963
1036
|
const e = requireEngine();
|
|
964
1037
|
await ensureHydrated(e);
|
|
1038
|
+
if (e.resetting || !e.consent.allowsFeedback())
|
|
1039
|
+
return { items: [], nextCursor: null };
|
|
1040
|
+
const current = requestSessionIsCurrent(e);
|
|
965
1041
|
const id = e.identity.get();
|
|
966
1042
|
const result = await e.transport.requestsList({
|
|
967
1043
|
anonymousId: id.anonymousId,
|
|
@@ -973,6 +1049,8 @@ export const UserGist = {
|
|
|
973
1049
|
cursor: options.cursor ?? null,
|
|
974
1050
|
limit: options.limit,
|
|
975
1051
|
});
|
|
1052
|
+
if (!current())
|
|
1053
|
+
return { items: [], nextCursor: null };
|
|
976
1054
|
ensureRequestsCache().upsertList(result.items.map((s) => ({
|
|
977
1055
|
id: s.id,
|
|
978
1056
|
appId: '',
|
|
@@ -1000,7 +1078,9 @@ export const UserGist = {
|
|
|
1000
1078
|
submitRequest(title, description, callback) {
|
|
1001
1079
|
try {
|
|
1002
1080
|
const e = requireEngine();
|
|
1003
|
-
if (typeof title !== 'string' ||
|
|
1081
|
+
if (typeof title !== 'string' ||
|
|
1082
|
+
title.length === 0 ||
|
|
1083
|
+
title.length > 120) {
|
|
1004
1084
|
callback?.(new Error('title required, max 120 chars'));
|
|
1005
1085
|
return;
|
|
1006
1086
|
}
|
|
@@ -1013,10 +1093,11 @@ export const UserGist = {
|
|
|
1013
1093
|
void (async () => {
|
|
1014
1094
|
try {
|
|
1015
1095
|
await ensureHydrated(e);
|
|
1016
|
-
if (!e.consent.allowsFeedback()) {
|
|
1096
|
+
if (e.resetting || !e.consent.allowsFeedback()) {
|
|
1017
1097
|
callback?.(new Error('feedback consent required'));
|
|
1018
1098
|
return;
|
|
1019
1099
|
}
|
|
1100
|
+
const current = requestSessionIsCurrent(e);
|
|
1020
1101
|
const id = e.identity.get();
|
|
1021
1102
|
const req = await e.transport.requestSubmit({
|
|
1022
1103
|
idempotencyKey: generateEventId(),
|
|
@@ -1025,6 +1106,10 @@ export const UserGist = {
|
|
|
1025
1106
|
title,
|
|
1026
1107
|
description,
|
|
1027
1108
|
});
|
|
1109
|
+
if (!current()) {
|
|
1110
|
+
callback?.(new Error('Request cancelled by identity or consent change'));
|
|
1111
|
+
return;
|
|
1112
|
+
}
|
|
1028
1113
|
ensureRequestsCache().upsert(req);
|
|
1029
1114
|
recordServerBackedEventLocally(e, '$request_submitted');
|
|
1030
1115
|
requestsHandlers.onSubmit?.(req);
|
|
@@ -1044,11 +1129,12 @@ export const UserGist = {
|
|
|
1044
1129
|
try {
|
|
1045
1130
|
const e = requireEngine();
|
|
1046
1131
|
await ensureHydrated(e);
|
|
1047
|
-
if (!e.consent.allowsFeedback())
|
|
1132
|
+
if (e.resetting || !e.consent.allowsFeedback())
|
|
1048
1133
|
return;
|
|
1049
1134
|
const cache = ensureRequestsCache();
|
|
1050
1135
|
const rollback = cache.applyOptimisticVote(requestId, vote);
|
|
1051
1136
|
try {
|
|
1137
|
+
const current = requestSessionIsCurrent(e);
|
|
1052
1138
|
const id = e.identity.get();
|
|
1053
1139
|
const result = await e.transport.requestVote({
|
|
1054
1140
|
requestId,
|
|
@@ -1056,6 +1142,10 @@ export const UserGist = {
|
|
|
1056
1142
|
externalId: id.externalId ?? null,
|
|
1057
1143
|
vote,
|
|
1058
1144
|
});
|
|
1145
|
+
if (!current()) {
|
|
1146
|
+
rollback();
|
|
1147
|
+
return;
|
|
1148
|
+
}
|
|
1059
1149
|
cache.commitVote(requestId, result);
|
|
1060
1150
|
recordServerBackedEventLocally(e, vote ? '$request_upvoted' : '$request_unupvoted');
|
|
1061
1151
|
requestsHandlers.onVote?.(result);
|
|
@@ -1073,11 +1163,12 @@ export const UserGist = {
|
|
|
1073
1163
|
try {
|
|
1074
1164
|
const e = requireEngine();
|
|
1075
1165
|
await ensureHydrated(e);
|
|
1076
|
-
if (!e.consent.allowsFeedback())
|
|
1166
|
+
if (e.resetting || !e.consent.allowsFeedback())
|
|
1077
1167
|
return;
|
|
1078
1168
|
const cache = ensureRequestsCache();
|
|
1079
1169
|
const rollback = cache.applyOptimisticFollow(requestId, follow);
|
|
1080
1170
|
try {
|
|
1171
|
+
const current = requestSessionIsCurrent(e);
|
|
1081
1172
|
const id = e.identity.get();
|
|
1082
1173
|
const result = await e.transport.requestFollow({
|
|
1083
1174
|
requestId,
|
|
@@ -1085,6 +1176,10 @@ export const UserGist = {
|
|
|
1085
1176
|
externalId: id.externalId ?? null,
|
|
1086
1177
|
follow,
|
|
1087
1178
|
});
|
|
1179
|
+
if (!current()) {
|
|
1180
|
+
rollback();
|
|
1181
|
+
return;
|
|
1182
|
+
}
|
|
1088
1183
|
cache.commitFollow(requestId, result);
|
|
1089
1184
|
recordServerBackedEventLocally(e, follow ? '$request_followed' : '$request_unfollowed');
|
|
1090
1185
|
requestsHandlers.onFollow?.(result);
|
|
@@ -1108,9 +1203,11 @@ export const UserGist = {
|
|
|
1108
1203
|
try {
|
|
1109
1204
|
const e = requireEngine();
|
|
1110
1205
|
await ensureHydrated(e);
|
|
1111
|
-
if (!e.consent.allowsFeedback())
|
|
1206
|
+
if (e.resetting || !e.consent.allowsFeedback())
|
|
1112
1207
|
return null;
|
|
1113
|
-
|
|
1208
|
+
const current = requestSessionIsCurrent(e);
|
|
1209
|
+
const branding = await e.transport.requestBranding();
|
|
1210
|
+
return current() ? branding : null;
|
|
1114
1211
|
}
|
|
1115
1212
|
catch (err) {
|
|
1116
1213
|
reportError('getRequestBranding failed', err);
|
|
@@ -1127,14 +1224,17 @@ export const UserGist = {
|
|
|
1127
1224
|
try {
|
|
1128
1225
|
const e = requireEngine();
|
|
1129
1226
|
await ensureHydrated(e);
|
|
1130
|
-
if (!e.consent.allowsFeedback())
|
|
1227
|
+
if (e.resetting || !e.consent.allowsFeedback())
|
|
1131
1228
|
return null;
|
|
1229
|
+
const current = requestSessionIsCurrent(e);
|
|
1132
1230
|
const id = e.identity.get();
|
|
1133
1231
|
const req = await e.transport.requestGet({
|
|
1134
1232
|
requestId,
|
|
1135
1233
|
anonymousId: id.anonymousId,
|
|
1136
1234
|
externalId: id.externalId ?? null,
|
|
1137
1235
|
});
|
|
1236
|
+
if (!current())
|
|
1237
|
+
return null;
|
|
1138
1238
|
ensureRequestsCache().upsert(req);
|
|
1139
1239
|
return req;
|
|
1140
1240
|
}
|
|
@@ -1146,7 +1246,7 @@ export const UserGist = {
|
|
|
1146
1246
|
openRequestsBoard() {
|
|
1147
1247
|
try {
|
|
1148
1248
|
const e = requireEngine();
|
|
1149
|
-
if (!e.consent.allowsFeedback())
|
|
1249
|
+
if (e.resetting || !e.consent.allowsFeedback())
|
|
1150
1250
|
return;
|
|
1151
1251
|
e.events.emit('showRequestsBoard', undefined);
|
|
1152
1252
|
}
|
|
@@ -1157,7 +1257,7 @@ export const UserGist = {
|
|
|
1157
1257
|
openRequestDetail(requestId) {
|
|
1158
1258
|
try {
|
|
1159
1259
|
const e = requireEngine();
|
|
1160
|
-
if (!e.consent.allowsFeedback())
|
|
1260
|
+
if (e.resetting || !e.consent.allowsFeedback())
|
|
1161
1261
|
return;
|
|
1162
1262
|
e.events.emit('showRequestDetail', { requestId });
|
|
1163
1263
|
}
|
|
@@ -1174,15 +1274,16 @@ export const UserGist = {
|
|
|
1174
1274
|
try {
|
|
1175
1275
|
const e = requireEngine();
|
|
1176
1276
|
await ensureHydrated(e);
|
|
1177
|
-
if (!e.consent.allowsFeedback())
|
|
1277
|
+
if (e.resetting || !e.consent.allowsFeedback())
|
|
1178
1278
|
return [];
|
|
1279
|
+
const current = requestSessionIsCurrent(e);
|
|
1179
1280
|
const id = e.identity.get();
|
|
1180
1281
|
const result = await e.transport.requestCommentsList({
|
|
1181
1282
|
requestId,
|
|
1182
1283
|
anonymousId: id.anonymousId,
|
|
1183
1284
|
externalId: id.externalId ?? null,
|
|
1184
1285
|
});
|
|
1185
|
-
return result.items;
|
|
1286
|
+
return current() ? result.items : [];
|
|
1186
1287
|
}
|
|
1187
1288
|
catch (err) {
|
|
1188
1289
|
reportError('getComments failed', err);
|
|
@@ -1195,8 +1296,9 @@ export const UserGist = {
|
|
|
1195
1296
|
const e = requireEngine();
|
|
1196
1297
|
validateCommentBody(body);
|
|
1197
1298
|
await ensureHydrated(e);
|
|
1198
|
-
if (!e.consent.allowsFeedback())
|
|
1299
|
+
if (e.resetting || !e.consent.allowsFeedback())
|
|
1199
1300
|
return null;
|
|
1301
|
+
const current = requestSessionIsCurrent(e);
|
|
1200
1302
|
const id = e.identity.get();
|
|
1201
1303
|
const comment = await e.transport.requestCommentPost({
|
|
1202
1304
|
idempotencyKey: generateEventId(),
|
|
@@ -1205,6 +1307,8 @@ export const UserGist = {
|
|
|
1205
1307
|
externalId: id.externalId ?? null,
|
|
1206
1308
|
body,
|
|
1207
1309
|
});
|
|
1310
|
+
if (!current())
|
|
1311
|
+
return null;
|
|
1208
1312
|
recordServerBackedEventLocally(e, REQUEST_COMMENTED_EVENT_NAME);
|
|
1209
1313
|
return comment;
|
|
1210
1314
|
}
|
|
@@ -1219,8 +1323,9 @@ export const UserGist = {
|
|
|
1219
1323
|
const e = requireEngine();
|
|
1220
1324
|
validateCommentBody(body);
|
|
1221
1325
|
await ensureHydrated(e);
|
|
1222
|
-
if (!e.consent.allowsFeedback())
|
|
1326
|
+
if (e.resetting || !e.consent.allowsFeedback())
|
|
1223
1327
|
return null;
|
|
1328
|
+
const current = requestSessionIsCurrent(e);
|
|
1224
1329
|
const id = e.identity.get();
|
|
1225
1330
|
const comment = await e.transport.requestCommentEdit({
|
|
1226
1331
|
requestId,
|
|
@@ -1228,6 +1333,8 @@ export const UserGist = {
|
|
|
1228
1333
|
anonymousId: id.anonymousId,
|
|
1229
1334
|
body,
|
|
1230
1335
|
});
|
|
1336
|
+
if (!current())
|
|
1337
|
+
return null;
|
|
1231
1338
|
recordServerBackedEventLocally(e, REQUEST_COMMENT_EDITED_EVENT_NAME);
|
|
1232
1339
|
return comment;
|
|
1233
1340
|
}
|
|
@@ -1241,14 +1348,17 @@ export const UserGist = {
|
|
|
1241
1348
|
try {
|
|
1242
1349
|
const e = requireEngine();
|
|
1243
1350
|
await ensureHydrated(e);
|
|
1244
|
-
if (!e.consent.allowsFeedback())
|
|
1351
|
+
if (e.resetting || !e.consent.allowsFeedback())
|
|
1245
1352
|
return;
|
|
1353
|
+
const current = requestSessionIsCurrent(e);
|
|
1246
1354
|
const id = e.identity.get();
|
|
1247
1355
|
await e.transport.requestCommentDelete({
|
|
1248
1356
|
requestId,
|
|
1249
1357
|
commentId,
|
|
1250
1358
|
anonymousId: id.anonymousId,
|
|
1251
1359
|
});
|
|
1360
|
+
if (!current())
|
|
1361
|
+
return;
|
|
1252
1362
|
recordServerBackedEventLocally(e, REQUEST_COMMENT_DELETED_EVENT_NAME);
|
|
1253
1363
|
}
|
|
1254
1364
|
catch (err) {
|
|
@@ -1256,4 +1366,97 @@ export const UserGist = {
|
|
|
1256
1366
|
}
|
|
1257
1367
|
},
|
|
1258
1368
|
};
|
|
1369
|
+
/** Registration and invalidation share an ordered lane. Automatic retries may
|
|
1370
|
+
* consume a retained token, but must never recreate an invalidated candidate. */
|
|
1371
|
+
async function registerPushDevice(token, platform, opts, retry = false) {
|
|
1372
|
+
try {
|
|
1373
|
+
if (!token)
|
|
1374
|
+
return;
|
|
1375
|
+
const e = requireEngine();
|
|
1376
|
+
if (e.resetting)
|
|
1377
|
+
return;
|
|
1378
|
+
const generation = e.resetGeneration;
|
|
1379
|
+
await serializePushRegistration(async () => {
|
|
1380
|
+
await ensureHydrated(e);
|
|
1381
|
+
if (e.resetting || generation !== e.resetGeneration)
|
|
1382
|
+
return;
|
|
1383
|
+
const environment = opts?.environment ??
|
|
1384
|
+
(e.config.environment === 'production' ? 'production' : 'sandbox');
|
|
1385
|
+
if (e.resetting || generation !== e.resetGeneration)
|
|
1386
|
+
return;
|
|
1387
|
+
const descriptor = { token, platform, environment };
|
|
1388
|
+
const stored = await e.storage.getJson(STORAGE_KEYS.pushDevice);
|
|
1389
|
+
if (JSON.stringify(stored) !== JSON.stringify(descriptor)) {
|
|
1390
|
+
if (retry)
|
|
1391
|
+
return;
|
|
1392
|
+
await e.storage.setJsonStrict(STORAGE_KEYS.pushDevice, descriptor);
|
|
1393
|
+
}
|
|
1394
|
+
e.pushTokenAvailable = true;
|
|
1395
|
+
pushSubscriptionChanged(e);
|
|
1396
|
+
if (!e.consent.allowsPush())
|
|
1397
|
+
return;
|
|
1398
|
+
await drainRevocations(e);
|
|
1399
|
+
if (await hasPendingRevocations(e) || e.resetting || generation !== e.resetGeneration || !e.consent.allowsPush())
|
|
1400
|
+
return;
|
|
1401
|
+
const id = e.identity.get();
|
|
1402
|
+
const registrationKey = [id.anonymousId, id.externalId ?? '', platform, environment, token].join(':');
|
|
1403
|
+
if (e.lastPushRegistrationKey === registrationKey && Date.now() - e.lastPushRegistrationAt < 24 * 60 * 60_000)
|
|
1404
|
+
return;
|
|
1405
|
+
// Persist before delivery so logout still owns a token if its response is lost.
|
|
1406
|
+
e.lastPushToken = token;
|
|
1407
|
+
await e.storage.setJson(STORAGE_KEYS.pushToken, token);
|
|
1408
|
+
if (e.resetting || generation !== e.resetGeneration || !e.consent.allowsPush())
|
|
1409
|
+
return;
|
|
1410
|
+
const registration = await e.transport.pushRegisterToken({
|
|
1411
|
+
anonymousId: id.anonymousId, externalId: id.externalId ?? null,
|
|
1412
|
+
token, platform, environment, timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
|
1413
|
+
sdkVersion: `rn-${USERGIST_SDK_VERSION}`, optIn: true,
|
|
1414
|
+
});
|
|
1415
|
+
if (e.resetting || generation !== e.resetGeneration || !e.consent.allowsPush())
|
|
1416
|
+
return;
|
|
1417
|
+
if (!(registration && typeof registration === 'object' && 'registered' in registration && registration.registered === true)) {
|
|
1418
|
+
pushSubscriptionChanged(e);
|
|
1419
|
+
return;
|
|
1420
|
+
}
|
|
1421
|
+
e.lastPushRegistrationKey = registrationKey;
|
|
1422
|
+
e.lastPushRegistrationAt = Date.now();
|
|
1423
|
+
pushSubscriptionChanged(e);
|
|
1424
|
+
debugLog('push token registered with server');
|
|
1425
|
+
});
|
|
1426
|
+
}
|
|
1427
|
+
catch (err) {
|
|
1428
|
+
reportError('registerPushToken failed', err);
|
|
1429
|
+
}
|
|
1430
|
+
}
|
|
1431
|
+
async function invalidatePushDevice(token, retainDevice = false) {
|
|
1432
|
+
try {
|
|
1433
|
+
if (!token)
|
|
1434
|
+
return;
|
|
1435
|
+
const e = requireEngine();
|
|
1436
|
+
const generation = e.resetGeneration;
|
|
1437
|
+
await serializePushRegistration(async () => {
|
|
1438
|
+
await ensureHydrated(e);
|
|
1439
|
+
if (e.resetting || generation !== e.resetGeneration)
|
|
1440
|
+
return;
|
|
1441
|
+
const saved = await e.storage.getJson(STORAGE_KEYS.pushDevice);
|
|
1442
|
+
if (saved?.token === token) {
|
|
1443
|
+
// Retire the automatic retry candidate before invalidating it remotely.
|
|
1444
|
+
if (!retainDevice)
|
|
1445
|
+
await e.storage.setJsonStrict(STORAGE_KEYS.pushDevice, null);
|
|
1446
|
+
e.pushTokenAvailable = retainDevice;
|
|
1447
|
+
e.lastPushRegistrationKey = null;
|
|
1448
|
+
e.lastPushRegistrationAt = 0;
|
|
1449
|
+
}
|
|
1450
|
+
if (e.lastPushToken === token) {
|
|
1451
|
+
await e.storage.setJsonStrict(STORAGE_KEYS.pushToken, null);
|
|
1452
|
+
e.lastPushToken = null;
|
|
1453
|
+
}
|
|
1454
|
+
pushSubscriptionChanged(e);
|
|
1455
|
+
await e.transport.pushInvalidateToken({ anonymousId: e.identity.get().anonymousId, token });
|
|
1456
|
+
});
|
|
1457
|
+
}
|
|
1458
|
+
catch (err) {
|
|
1459
|
+
reportError('invalidatePushToken failed', err);
|
|
1460
|
+
}
|
|
1461
|
+
}
|
|
1259
1462
|
//# sourceMappingURL=UserGist.js.map
|