@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.
Files changed (43) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +4 -1
  3. package/dist/UserGist.d.ts +9 -0
  4. package/dist/UserGist.d.ts.map +1 -1
  5. package/dist/UserGist.js +307 -104
  6. package/dist/UserGist.js.map +1 -1
  7. package/dist/index.d.ts +2 -0
  8. package/dist/index.d.ts.map +1 -1
  9. package/dist/internal/context.d.ts +1 -1
  10. package/dist/internal/context.js +1 -1
  11. package/dist/internal/engine.d.ts +6 -0
  12. package/dist/internal/engine.d.ts.map +1 -1
  13. package/dist/internal/engine.js +107 -36
  14. package/dist/internal/engine.js.map +1 -1
  15. package/dist/internal/identity-lifecycle.d.ts +16 -0
  16. package/dist/internal/identity-lifecycle.d.ts.map +1 -0
  17. package/dist/internal/identity-lifecycle.js +96 -0
  18. package/dist/internal/identity-lifecycle.js.map +1 -0
  19. package/dist/internal/mutation-queue.d.ts +2 -2
  20. package/dist/internal/mutation-queue.d.ts.map +1 -1
  21. package/dist/internal/mutation-queue.js +19 -2
  22. package/dist/internal/mutation-queue.js.map +1 -1
  23. package/dist/internal/push-subscription.d.ts +11 -0
  24. package/dist/internal/push-subscription.d.ts.map +1 -0
  25. package/dist/internal/push-subscription.js +20 -0
  26. package/dist/internal/push-subscription.js.map +1 -0
  27. package/dist/internal/session-revocations.d.ts +5 -0
  28. package/dist/internal/session-revocations.d.ts.map +1 -0
  29. package/dist/internal/session-revocations.js +58 -0
  30. package/dist/internal/session-revocations.js.map +1 -0
  31. package/dist/internal/storage.d.ts +3 -0
  32. package/dist/internal/storage.d.ts.map +1 -1
  33. package/dist/internal/storage.js +76 -60
  34. package/dist/internal/storage.js.map +1 -1
  35. package/dist/internal/transport.d.ts +14 -2
  36. package/dist/internal/transport.d.ts.map +1 -1
  37. package/dist/internal/transport.js +31 -14
  38. package/dist/internal/transport.js.map +1 -1
  39. package/dist/internal/user-state.d.ts +2 -1
  40. package/dist/internal/user-state.d.ts.map +1 -1
  41. package/dist/internal/user-state.js +3 -2
  42. package/dist/internal/user-state.js.map +1 -1
  43. package/package.json +2 -2
package/dist/UserGist.js CHANGED
@@ -1,12 +1,7 @@
1
- // UserGist — public singleton facade.
2
- //
3
- // Philosophy:
4
- // - `init()` remains synchronous and backwards compatible. `initAsync()` can
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() ? asEventProps(properties) : undefined;
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 (e.resetting)
327
- return resetPromise ?? Promise.resolve();
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
- if (e.lastPushToken)
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
- .catch((err) => reportError('reset failed', err))
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
- try {
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
- try {
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 ?? (defaultEnvironment()),
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: 'denied',
646
- platform: (typeof navigator === 'object' && navigator.product === 'ReactNative' ? 'ios' : 'android'),
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
- const e = engine;
653
- if (e?.lastPushToken)
654
- await UserGist.invalidatePushToken(e.lastPushToken);
655
- await UserGistPushNative.disablePush();
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' || title.length === 0 || title.length > 120) {
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
- return await e.transport.requestBranding();
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