@usergist/feedback-react-native 0.1.1 → 0.2.0-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/EXPO-ACCEPTANCE.md +57 -0
  3. package/EXPO.md +103 -0
  4. package/README.md +10 -2
  5. package/UserGistFeedback.podspec +4 -1
  6. package/UserGistFeedbackExtension.podspec +1 -1
  7. package/android/build.gradle +3 -1
  8. package/android/src/main/AndroidManifest.xml +1 -0
  9. package/android/src/main/java/studio/usergist/feedback/UserGistFirebaseMessagingService.kt +78 -12
  10. package/android/src/main/java/studio/usergist/feedback/UserGistNotificationDismissReceiver.kt +17 -0
  11. package/android/src/main/java/studio/usergist/feedback/UserGistPushModule.kt +46 -5
  12. package/android/src/main/java/studio/usergist/feedback/UserGistPushState.kt +69 -0
  13. package/app.plugin.js +2 -0
  14. package/dist/NativeUserGistPush.d.ts +10 -0
  15. package/dist/NativeUserGistPush.d.ts.map +1 -1
  16. package/dist/NativeUserGistPush.js.map +1 -1
  17. package/dist/Push.d.ts.map +1 -1
  18. package/dist/Push.js +9 -0
  19. package/dist/Push.js.map +1 -1
  20. package/dist/UserGist.d.ts +1 -0
  21. package/dist/UserGist.d.ts.map +1 -1
  22. package/dist/UserGist.js +218 -79
  23. package/dist/UserGist.js.map +1 -1
  24. package/dist/expo/index.d.ts +68 -0
  25. package/dist/expo/index.d.ts.map +1 -0
  26. package/dist/expo/index.js +120 -0
  27. package/dist/expo/index.js.map +1 -0
  28. package/dist/internal/consent.d.ts.map +1 -1
  29. package/dist/internal/consent.js +4 -1
  30. package/dist/internal/consent.js.map +1 -1
  31. package/dist/internal/context.d.ts +1 -1
  32. package/dist/internal/context.d.ts.map +1 -1
  33. package/dist/internal/context.js +1 -1
  34. package/dist/internal/context.js.map +1 -1
  35. package/dist/internal/engine.d.ts.map +1 -1
  36. package/dist/internal/engine.js +6 -1
  37. package/dist/internal/engine.js.map +1 -1
  38. package/dist/internal/events.d.ts +1 -0
  39. package/dist/internal/events.d.ts.map +1 -1
  40. package/dist/internal/events.js +6 -0
  41. package/dist/internal/events.js.map +1 -1
  42. package/dist/internal/push-dedupe.d.ts +3 -0
  43. package/dist/internal/push-dedupe.d.ts.map +1 -0
  44. package/dist/internal/push-dedupe.js +33 -0
  45. package/dist/internal/push-dedupe.js.map +1 -0
  46. package/dist/internal/storage.d.ts +1 -0
  47. package/dist/internal/storage.d.ts.map +1 -1
  48. package/dist/internal/storage.js +1 -0
  49. package/dist/internal/storage.js.map +1 -1
  50. package/dist/internal/transport.d.ts +5 -1
  51. package/dist/internal/transport.d.ts.map +1 -1
  52. package/dist/internal/transport.js +2 -2
  53. package/dist/internal/transport.js.map +1 -1
  54. package/dist/native/events.d.ts +2 -0
  55. package/dist/native/events.d.ts.map +1 -1
  56. package/dist/native/events.js +2 -0
  57. package/dist/native/events.js.map +1 -1
  58. package/dist/native/push-adapter.d.ts +11 -0
  59. package/dist/native/push-adapter.d.ts.map +1 -0
  60. package/dist/native/push-adapter.js +10 -0
  61. package/dist/native/push-adapter.js.map +1 -0
  62. package/dist/native/push-bridge.d.ts +9 -0
  63. package/dist/native/push-bridge.d.ts.map +1 -1
  64. package/dist/native/push-bridge.js +39 -4
  65. package/dist/native/push-bridge.js.map +1 -1
  66. package/dist/ui/requests/RequestsHost.d.ts.map +1 -1
  67. package/dist/ui/requests/RequestsHost.js +8 -0
  68. package/dist/ui/requests/RequestsHost.js.map +1 -1
  69. package/expo-module.config.json +8 -0
  70. package/expo-support/UserGistExpoAppDelegateSubscriber.swift +34 -0
  71. package/expo-support/UserGistFeedbackExpo.podspec +17 -0
  72. package/ios/Extension/UserGistNotificationService.swift +52 -5
  73. package/ios/Shared/UserGistNotificationActions.swift +27 -0
  74. package/ios/Shared/UserGistPushState.swift +110 -0
  75. package/ios/UserGistPush.h +1 -6
  76. package/ios/UserGistPush.mm +18 -0
  77. package/ios/UserGistPushImpl.swift +46 -4
  78. package/ios/UserGistPushInterop.h +5 -0
  79. package/package.json +33 -7
  80. package/plugin/index.cjs +269 -0
  81. package/plugin/index.d.cts +14 -0
  82. package/plugin/templates/UserGistExpoMessagingService.kt +20 -0
package/dist/UserGist.js CHANGED
@@ -1,25 +1,23 @@
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 { hydratePushDedupe } from './internal/push-dedupe.js';
10
2
  import { REQUEST_COMMENTED_EVENT_NAME, REQUEST_COMMENT_EDITED_EVENT_NAME, REQUEST_COMMENT_DELETED_EVENT_NAME, } from '@usergist/sdk-core/mobile';
11
3
  import { asEventProps, clearAllState, createEngine, emitAppOpenWhenConsentReady, emitAppVersionChanged, ensureHydrated, enqueueAndEvaluate, flushNow, flushMutations, pollInstructions, refreshTargetingRules, recordServerBackedEventLocally, submitResponse, } from './internal/engine.js';
12
4
  import { setDebugEnabled, reportError, debugLog, logTrace, getTraces, setDiagnosticHandler, } from './internal/debug.js';
13
5
  import { DEFAULT_THEME, mergeTheme } from './ui/theme.js';
14
6
  import { createSurveyStore } from './internal/survey-store.js';
15
- import { UserGistPushNative, onTokenReceived, onNotificationReceived, onNotificationOpened, onTokenError, } from './native/push-bridge.js';
7
+ import { UserGistPushNative, onTokenReceived, onNotificationReceived, onNotificationOpened, onNotificationDisplayed, onNotificationDismissed, onTokenError, } from './native/push-bridge.js';
16
8
  import { Push } from './Push.js';
17
9
  import { AppState, Platform } from 'react-native';
18
10
  import { generateEventId, validateIdentifyTransition } from './internal/identity.js';
19
- import { configureStorageAdapter, } from './internal/storage.js';
11
+ import { configureStorageAdapter, STORAGE_KEYS, } from './internal/storage.js';
20
12
  import { surveyCompletionOutcome } from './internal/survey-completion.js';
21
13
  import { USERGIST_SDK_VERSION } from './internal/context.js';
22
14
  let engine = null;
15
+ let pushRegistrationQueue = Promise.resolve();
16
+ function serializePushRegistration(operation) {
17
+ const task = pushRegistrationQueue.then(operation);
18
+ pushRegistrationQueue = task.catch(() => undefined);
19
+ return task;
20
+ }
23
21
  let runtimeStarted = false;
24
22
  let runtimeStartPromise = null;
25
23
  let resetPromise = null;
@@ -37,6 +35,37 @@ function ensureRequestsCache() {
37
35
  }
38
36
  return requestsCache;
39
37
  }
38
+ function requestSessionIsCurrent(e) {
39
+ const generation = e.resetGeneration;
40
+ const consentVersion = e.consent.get().version;
41
+ const identity = e.identity.get();
42
+ return () => !e.resetting && generation === e.resetGeneration &&
43
+ consentVersion === e.consent.get().version && e.consent.allowsFeedback() &&
44
+ identity.anonymousId === e.identity.get().anonymousId &&
45
+ identity.externalId === e.identity.get().externalId;
46
+ }
47
+ async function syncNativePushState(e, push = e.consent.allowsPush()) {
48
+ const anonymousId = e.identity.get().anonymousId;
49
+ const generation = e.resetGeneration;
50
+ const version = e.consent.get().version;
51
+ const operation = nativeStateQueue.then(async () => {
52
+ if (generation !== e.resetGeneration || anonymousId !== e.identity.get().anonymousId || version !== e.consent.get().version)
53
+ return;
54
+ await UserGistPushNative.syncState({ writeKey: e.config.writeKey, apiUrl: e.config.apiUrl, anonymousId, push: push && !e.resetting });
55
+ });
56
+ nativeStateQueue = operation.catch(() => undefined);
57
+ await operation;
58
+ }
59
+ let nativeStateQueue = Promise.resolve();
60
+ async function disableNativePushForConsent(e, version) {
61
+ const operation = nativeStateQueue.then(async () => {
62
+ if (e.consent.get().version !== version || e.consent.allowsPush())
63
+ return;
64
+ await UserGistPushNative.disablePush();
65
+ });
66
+ nativeStateQueue = operation.catch(() => undefined);
67
+ await operation;
68
+ }
40
69
  function requireEngine() {
41
70
  if (!engine) {
42
71
  throw new Error('UserGist.init must be called before using the SDK');
@@ -116,7 +145,7 @@ function attachEnginePushListeners() {
116
145
  try {
117
146
  const platform = (p.platform === 'ios' ? 'ios' : 'android');
118
147
  await UserGist.registerPushToken(p.token, platform, {
119
- environment: lastEnablePushOptions?.environment ?? defaultEnvironment(),
148
+ environment: lastEnablePushOptions?.environment ?? await UserGistPushNative.defaultEnvironment() ?? defaultEnvironment(),
120
149
  });
121
150
  }
122
151
  catch (err) {
@@ -130,12 +159,18 @@ function attachEnginePushListeners() {
130
159
  onNotificationReceived((p) => {
131
160
  forwardToPushHandler(p, 'received');
132
161
  });
162
+ onNotificationDisplayed(p => forwardToPushHandler(p, 'displayed'));
163
+ onNotificationDismissed(p => forwardToPushHandler(p, 'dismissed'));
133
164
  onNotificationOpened((p) => {
134
165
  forwardToPushHandler(p, 'opened');
135
166
  });
136
167
  }
137
168
  function forwardToPushHandler(p, kind) {
169
+ if (!engine?.consent.allowsPush() || engine.resetting)
170
+ return;
138
171
  const data = p.data ?? {};
172
+ if (data.usergist_anonymous_id && data.usergist_anonymous_id !== engine.identity.get().anonymousId)
173
+ return;
139
174
  if (p.deliveryId) {
140
175
  ;
141
176
  data.usergist_delivery_id = p.deliveryId;
@@ -153,6 +188,12 @@ function forwardToPushHandler(p, kind) {
153
188
  if (kind === 'received') {
154
189
  Push.handleReceived({ userInfo, notification: { title: p.title, body: p.body } });
155
190
  }
191
+ else if (kind === 'displayed') {
192
+ Push.handleDisplayed({ userInfo });
193
+ }
194
+ else if (kind === 'dismissed') {
195
+ Push.handleDismissed({ userInfo });
196
+ }
156
197
  else {
157
198
  Push.handleOpened({ userInfo, actionIdentifier: p.actionIdentifier });
158
199
  }
@@ -165,6 +206,12 @@ function forwardToPushHandler(p, kind) {
165
206
  if (kind === 'received') {
166
207
  Push.handleReceived({ data: dataStr, notification: { title: p.title, body: p.body } });
167
208
  }
209
+ else if (kind === 'displayed') {
210
+ Push.handleDisplayed({ data: dataStr });
211
+ }
212
+ else if (kind === 'dismissed') {
213
+ Push.handleDismissed({ data: dataStr });
214
+ }
168
215
  else {
169
216
  Push.handleOpened({ data: dataStr, actionIdentifier: p.actionIdentifier });
170
217
  }
@@ -202,6 +249,8 @@ export const UserGist = {
202
249
  if (result !== 'synced')
203
250
  return result;
204
251
  }
252
+ await hydratePushDedupe(e.config.writeKey, e.identity.get().anonymousId);
253
+ await syncNativePushState(e);
205
254
  await startRuntime(e);
206
255
  return 'synced';
207
256
  }
@@ -221,6 +270,8 @@ export const UserGist = {
221
270
  }
222
271
  const e = requireEngine();
223
272
  await ensureHydrated(e);
273
+ if (e.resetting)
274
+ return 'rejected';
224
275
  const identity = e.identity.get();
225
276
  const pendingIdentity = e.mutations.peek();
226
277
  const transition = validateIdentifyTransition(identity.externalId, pendingIdentity?.kind === 'identify' ? pendingIdentity.payload.externalId : null, userId);
@@ -252,6 +303,7 @@ export const UserGist = {
252
303
  debugLog('identify is saved locally and pending retry');
253
304
  return 'queued';
254
305
  }
306
+ await syncNativePushState(e);
255
307
  void UserGist.rebindPushToken(userId);
256
308
  return 'synced';
257
309
  }
@@ -278,7 +330,29 @@ export const UserGist = {
278
330
  try {
279
331
  const e = requireEngine();
280
332
  await ensureHydrated(e);
333
+ if (e.resetting)
334
+ return false;
281
335
  const next = await e.consent.set(purposes);
336
+ if (e.resetting || next.version !== e.consent.get().version)
337
+ return false;
338
+ if (!next.feedback) {
339
+ requestsCache = null;
340
+ e.events.emit('dismissRequests', undefined);
341
+ }
342
+ await syncNativePushState(e);
343
+ if (e.resetting || next.version !== e.consent.get().version)
344
+ return false;
345
+ if (purposes.push === false) {
346
+ lastEnablePushOptions = null;
347
+ await pushRegistrationQueue;
348
+ if (e.resetting || next.version !== e.consent.get().version)
349
+ return false;
350
+ if (e.lastPushToken)
351
+ await UserGist.invalidatePushToken(e.lastPushToken);
352
+ await disableNativePushForConsent(e, next.version);
353
+ }
354
+ if (e.resetting || next.version !== e.consent.get().version)
355
+ return false;
282
356
  const id = e.identity.get();
283
357
  try {
284
358
  await e.transport.consent({
@@ -318,13 +392,18 @@ export const UserGist = {
318
392
  return resetPromise ?? Promise.resolve();
319
393
  e.resetting = true;
320
394
  e.resetGeneration += 1;
395
+ requestsCache = null;
396
+ lastEnablePushOptions = null;
321
397
  e.events.emit('resetSurfaces', undefined);
322
398
  const operation = (async () => {
323
399
  await ensureHydrated(e);
400
+ await syncNativePushState(e, false);
401
+ await pushRegistrationQueue;
324
402
  if (e.lastPushToken)
325
403
  await UserGist.invalidatePushToken(e.lastPushToken);
326
404
  await UserGistPushNative.disablePush().catch(() => undefined);
327
405
  await clearAllState(e);
406
+ await hydratePushDedupe(e.config.writeKey, e.identity.get().anonymousId);
328
407
  })();
329
408
  const finalized = operation
330
409
  .catch((err) => reportError('reset failed', err))
@@ -443,42 +522,48 @@ export const UserGist = {
443
522
  return;
444
523
  const e = requireEngine();
445
524
  await ensureHydrated(e);
446
- if (!e.consent.allowsPush())
447
- return;
448
- // Consent is enforced server-side (the register-token route reads
449
- // consent_log and refuses if push was explicitly declined). We no
450
- // longer short-circuit here because the host app typically calls
451
- // setConsent and registerPushToken in parallel; racing the local
452
- // consent hydration is not our problem to solve.
453
- const id = e.identity.get();
454
- const environment = opts?.environment ??
455
- (e.config.environment === 'production' ? 'production' : 'sandbox');
456
- const registrationKey = [
457
- id.anonymousId,
458
- id.externalId ?? '',
459
- platform,
460
- environment,
461
- token,
462
- ].join(':');
463
- if (e.lastPushRegistrationKey === registrationKey &&
464
- Date.now() - e.lastPushRegistrationAt < 24 * 60 * 60_000)
525
+ if (!e.consent.allowsPush() || e.resetting)
465
526
  return;
466
- await e.transport.pushRegisterToken({
467
- anonymousId: id.anonymousId,
468
- externalId: id.externalId ?? null,
469
- token,
470
- platform,
471
- environment,
472
- language: undefined,
473
- timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
474
- appVersion: undefined,
475
- sdkVersion: `rn-${USERGIST_SDK_VERSION}`,
476
- optIn: true,
527
+ const generation = e.resetGeneration;
528
+ await serializePushRegistration(async () => {
529
+ if (e.resetting || generation !== e.resetGeneration || !e.consent.allowsPush())
530
+ return;
531
+ const id = e.identity.get();
532
+ const environment = opts?.environment ?? await UserGistPushNative.defaultEnvironment() ??
533
+ (e.config.environment === 'production' ? 'production' : 'sandbox');
534
+ const registrationKey = [
535
+ id.anonymousId,
536
+ id.externalId ?? '',
537
+ platform,
538
+ environment,
539
+ token,
540
+ ].join(':');
541
+ if (e.lastPushRegistrationKey === registrationKey &&
542
+ Date.now() - e.lastPushRegistrationAt < 24 * 60 * 60_000)
543
+ return;
544
+ if (e.resetting || generation !== e.resetGeneration || !e.consent.allowsPush())
545
+ return;
546
+ // Persist before the request so logout can invalidate a token even when
547
+ // the response is lost or the process exits after the server accepts it.
548
+ e.lastPushToken = token;
549
+ await e.storage.setJson(STORAGE_KEYS.pushToken, token);
550
+ await e.transport.pushRegisterToken({
551
+ anonymousId: id.anonymousId,
552
+ externalId: id.externalId ?? null,
553
+ token,
554
+ platform,
555
+ environment,
556
+ language: undefined,
557
+ timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
558
+ appVersion: undefined,
559
+ sdkVersion: `rn-${USERGIST_SDK_VERSION}`,
560
+ optIn: true,
561
+ });
562
+ e.lastPushToken = token;
563
+ e.lastPushRegistrationKey = registrationKey;
564
+ e.lastPushRegistrationAt = Date.now();
565
+ debugLog('push token registered with server');
477
566
  });
478
- e.lastPushToken = token;
479
- e.lastPushRegistrationKey = registrationKey;
480
- e.lastPushRegistrationAt = Date.now();
481
- debugLog('push token registered with server');
482
567
  }
483
568
  catch (err) {
484
569
  reportError('registerPushToken failed', err);
@@ -491,6 +576,8 @@ export const UserGist = {
491
576
  const e = requireEngine();
492
577
  await ensureHydrated(e);
493
578
  const id = e.identity.get();
579
+ e.lastPushRegistrationKey = null;
580
+ e.lastPushRegistrationAt = 0;
494
581
  await e.transport.pushInvalidateToken({
495
582
  anonymousId: id.anonymousId,
496
583
  token,
@@ -510,14 +597,19 @@ export const UserGist = {
510
597
  try {
511
598
  const e = requireEngine();
512
599
  await ensureHydrated(e);
513
- const id = e.identity.get();
514
- const token = e.lastPushToken;
515
- if (!token)
516
- return;
517
- await e.transport.pushRebind({
518
- anonymousId: id.anonymousId,
519
- externalId,
520
- token,
600
+ const generation = e.resetGeneration;
601
+ await serializePushRegistration(async () => {
602
+ if (generation !== e.resetGeneration)
603
+ return;
604
+ const id = e.identity.get();
605
+ const token = e.lastPushToken;
606
+ if (!token || e.resetting || !e.consent.allowsPush() || id.externalId !== externalId)
607
+ return;
608
+ await e.transport.pushRebind({
609
+ anonymousId: id.anonymousId,
610
+ externalId,
611
+ token,
612
+ });
521
613
  });
522
614
  }
523
615
  catch (err) {
@@ -608,6 +700,18 @@ export const UserGist = {
608
700
  // react-native-onesignal, WonderPush, or any other push SDK.
609
701
  async enablePush(opts) {
610
702
  try {
703
+ const e = requireEngine();
704
+ await ensureHydrated(e);
705
+ if (!e.consent.allowsPush() || e.resetting)
706
+ return { granted: false, status: 'not_determined', platform: Platform.OS === 'ios' ? 'ios' : 'android', error: 'consent_required' };
707
+ const generation = e.resetGeneration;
708
+ const consentVersion = e.consent.get().version;
709
+ const current = () => !e.resetting && generation === e.resetGeneration && consentVersion === e.consent.get().version && e.consent.allowsPush();
710
+ const cancelled = () => ({ granted: false, status: 'not_determined', platform: Platform.OS === 'ios' ? 'ios' : 'android', error: 'session_changed' });
711
+ opts = { ...opts, environment: opts?.environment ?? await UserGistPushNative.defaultEnvironment() ?? defaultEnvironment() };
712
+ await syncNativePushState(e);
713
+ if (!current())
714
+ return cancelled();
611
715
  if (!enginePushListenersAttached)
612
716
  attachEnginePushListeners();
613
717
  // Snapshot the caller's options so the AppState foreground listener
@@ -618,6 +722,10 @@ export const UserGist = {
618
722
  installDelegateProxy: opts?.installDelegateProxy,
619
723
  };
620
724
  const result = await UserGistPushNative.enablePush(opts ?? {});
725
+ // Reset/withdrawal owns native cleanup. An old permission callback must
726
+ // not disable a new account's subsequently enabled notification adapter.
727
+ if (!current())
728
+ return cancelled();
621
729
  // Server-side registration runs on the tokenReceived event; if the
622
730
  // native module already had a cached token it surfaces here.
623
731
  if (result.granted && result.token) {
@@ -632,23 +740,19 @@ export const UserGist = {
632
740
  reportError('enablePush failed', err);
633
741
  return {
634
742
  granted: false,
635
- status: 'denied',
636
- platform: (typeof navigator === 'object' && navigator.product === 'ReactNative' ? 'ios' : 'android'),
743
+ status: 'not_determined',
744
+ error: 'push_setup_failed',
745
+ platform: Platform.OS === 'ios' ? 'ios' : 'android',
637
746
  };
638
747
  }
639
748
  },
640
749
  async disablePush() {
750
+ lastEnablePushOptions = null;
641
751
  try {
642
- const e = engine;
643
- if (e?.lastPushToken)
644
- await UserGist.invalidatePushToken(e.lastPushToken);
645
- await UserGistPushNative.disablePush();
646
- if (e) {
647
- e.lastPushToken = null;
648
- e.lastPushRegistrationKey = null;
649
- e.lastPushRegistrationAt = 0;
650
- void UserGist.setConsent({ push: false });
651
- }
752
+ if (engine)
753
+ await UserGist.setConsent({ push: false });
754
+ else
755
+ await UserGistPushNative.disablePush();
652
756
  }
653
757
  catch (err) {
654
758
  reportError('disablePush failed', err);
@@ -949,6 +1053,9 @@ export const UserGist = {
949
1053
  try {
950
1054
  const e = requireEngine();
951
1055
  await ensureHydrated(e);
1056
+ if (e.resetting || !e.consent.allowsFeedback())
1057
+ return { items: [], nextCursor: null };
1058
+ const current = requestSessionIsCurrent(e);
952
1059
  const id = e.identity.get();
953
1060
  const result = await e.transport.requestsList({
954
1061
  anonymousId: id.anonymousId,
@@ -960,6 +1067,8 @@ export const UserGist = {
960
1067
  cursor: options.cursor ?? null,
961
1068
  limit: options.limit,
962
1069
  });
1070
+ if (!current())
1071
+ return { items: [], nextCursor: null };
963
1072
  ensureRequestsCache().upsertList(result.items.map((s) => ({
964
1073
  id: s.id,
965
1074
  appId: '',
@@ -1000,10 +1109,11 @@ export const UserGist = {
1000
1109
  void (async () => {
1001
1110
  try {
1002
1111
  await ensureHydrated(e);
1003
- if (!e.consent.allowsFeedback()) {
1112
+ if (e.resetting || !e.consent.allowsFeedback()) {
1004
1113
  callback?.(new Error('feedback consent required'));
1005
1114
  return;
1006
1115
  }
1116
+ const current = requestSessionIsCurrent(e);
1007
1117
  const id = e.identity.get();
1008
1118
  const req = await e.transport.requestSubmit({
1009
1119
  idempotencyKey: generateEventId(),
@@ -1012,6 +1122,10 @@ export const UserGist = {
1012
1122
  title,
1013
1123
  description,
1014
1124
  });
1125
+ if (!current()) {
1126
+ callback?.(new Error('Request cancelled by identity or consent change'));
1127
+ return;
1128
+ }
1015
1129
  ensureRequestsCache().upsert(req);
1016
1130
  recordServerBackedEventLocally(e, '$request_submitted');
1017
1131
  requestsHandlers.onSubmit?.(req);
@@ -1031,11 +1145,12 @@ export const UserGist = {
1031
1145
  try {
1032
1146
  const e = requireEngine();
1033
1147
  await ensureHydrated(e);
1034
- if (!e.consent.allowsFeedback())
1148
+ if (e.resetting || !e.consent.allowsFeedback())
1035
1149
  return;
1036
1150
  const cache = ensureRequestsCache();
1037
1151
  const rollback = cache.applyOptimisticVote(requestId, vote);
1038
1152
  try {
1153
+ const current = requestSessionIsCurrent(e);
1039
1154
  const id = e.identity.get();
1040
1155
  const result = await e.transport.requestVote({
1041
1156
  requestId,
@@ -1043,6 +1158,10 @@ export const UserGist = {
1043
1158
  externalId: id.externalId ?? null,
1044
1159
  vote,
1045
1160
  });
1161
+ if (!current()) {
1162
+ rollback();
1163
+ return;
1164
+ }
1046
1165
  cache.commitVote(requestId, result);
1047
1166
  recordServerBackedEventLocally(e, vote ? '$request_upvoted' : '$request_unupvoted');
1048
1167
  requestsHandlers.onVote?.(result);
@@ -1060,11 +1179,12 @@ export const UserGist = {
1060
1179
  try {
1061
1180
  const e = requireEngine();
1062
1181
  await ensureHydrated(e);
1063
- if (!e.consent.allowsFeedback())
1182
+ if (e.resetting || !e.consent.allowsFeedback())
1064
1183
  return;
1065
1184
  const cache = ensureRequestsCache();
1066
1185
  const rollback = cache.applyOptimisticFollow(requestId, follow);
1067
1186
  try {
1187
+ const current = requestSessionIsCurrent(e);
1068
1188
  const id = e.identity.get();
1069
1189
  const result = await e.transport.requestFollow({
1070
1190
  requestId,
@@ -1072,6 +1192,10 @@ export const UserGist = {
1072
1192
  externalId: id.externalId ?? null,
1073
1193
  follow,
1074
1194
  });
1195
+ if (!current()) {
1196
+ rollback();
1197
+ return;
1198
+ }
1075
1199
  cache.commitFollow(requestId, result);
1076
1200
  recordServerBackedEventLocally(e, follow ? '$request_followed' : '$request_unfollowed');
1077
1201
  requestsHandlers.onFollow?.(result);
@@ -1095,9 +1219,11 @@ export const UserGist = {
1095
1219
  try {
1096
1220
  const e = requireEngine();
1097
1221
  await ensureHydrated(e);
1098
- if (!e.consent.allowsFeedback())
1222
+ if (e.resetting || !e.consent.allowsFeedback())
1099
1223
  return null;
1100
- return await e.transport.requestBranding();
1224
+ const current = requestSessionIsCurrent(e);
1225
+ const branding = await e.transport.requestBranding();
1226
+ return current() ? branding : null;
1101
1227
  }
1102
1228
  catch (err) {
1103
1229
  reportError('getRequestBranding failed', err);
@@ -1114,14 +1240,17 @@ export const UserGist = {
1114
1240
  try {
1115
1241
  const e = requireEngine();
1116
1242
  await ensureHydrated(e);
1117
- if (!e.consent.allowsFeedback())
1243
+ if (e.resetting || !e.consent.allowsFeedback())
1118
1244
  return null;
1245
+ const current = requestSessionIsCurrent(e);
1119
1246
  const id = e.identity.get();
1120
1247
  const req = await e.transport.requestGet({
1121
1248
  requestId,
1122
1249
  anonymousId: id.anonymousId,
1123
1250
  externalId: id.externalId ?? null,
1124
1251
  });
1252
+ if (!current())
1253
+ return null;
1125
1254
  ensureRequestsCache().upsert(req);
1126
1255
  return req;
1127
1256
  }
@@ -1133,7 +1262,7 @@ export const UserGist = {
1133
1262
  openRequestsBoard() {
1134
1263
  try {
1135
1264
  const e = requireEngine();
1136
- if (!e.consent.allowsFeedback())
1265
+ if (e.resetting || !e.consent.allowsFeedback())
1137
1266
  return;
1138
1267
  e.events.emit('showRequestsBoard', undefined);
1139
1268
  }
@@ -1144,7 +1273,7 @@ export const UserGist = {
1144
1273
  openRequestDetail(requestId) {
1145
1274
  try {
1146
1275
  const e = requireEngine();
1147
- if (!e.consent.allowsFeedback())
1276
+ if (e.resetting || !e.consent.allowsFeedback())
1148
1277
  return;
1149
1278
  e.events.emit('showRequestDetail', { requestId });
1150
1279
  }
@@ -1161,15 +1290,16 @@ export const UserGist = {
1161
1290
  try {
1162
1291
  const e = requireEngine();
1163
1292
  await ensureHydrated(e);
1164
- if (!e.consent.allowsFeedback())
1293
+ if (e.resetting || !e.consent.allowsFeedback())
1165
1294
  return [];
1295
+ const current = requestSessionIsCurrent(e);
1166
1296
  const id = e.identity.get();
1167
1297
  const result = await e.transport.requestCommentsList({
1168
1298
  requestId,
1169
1299
  anonymousId: id.anonymousId,
1170
1300
  externalId: id.externalId ?? null,
1171
1301
  });
1172
- return result.items;
1302
+ return current() ? result.items : [];
1173
1303
  }
1174
1304
  catch (err) {
1175
1305
  reportError('getComments failed', err);
@@ -1182,8 +1312,9 @@ export const UserGist = {
1182
1312
  const e = requireEngine();
1183
1313
  validateCommentBody(body);
1184
1314
  await ensureHydrated(e);
1185
- if (!e.consent.allowsFeedback())
1315
+ if (e.resetting || !e.consent.allowsFeedback())
1186
1316
  return null;
1317
+ const current = requestSessionIsCurrent(e);
1187
1318
  const id = e.identity.get();
1188
1319
  const comment = await e.transport.requestCommentPost({
1189
1320
  idempotencyKey: generateEventId(),
@@ -1192,6 +1323,8 @@ export const UserGist = {
1192
1323
  externalId: id.externalId ?? null,
1193
1324
  body,
1194
1325
  });
1326
+ if (!current())
1327
+ return null;
1195
1328
  recordServerBackedEventLocally(e, REQUEST_COMMENTED_EVENT_NAME);
1196
1329
  return comment;
1197
1330
  }
@@ -1206,8 +1339,9 @@ export const UserGist = {
1206
1339
  const e = requireEngine();
1207
1340
  validateCommentBody(body);
1208
1341
  await ensureHydrated(e);
1209
- if (!e.consent.allowsFeedback())
1342
+ if (e.resetting || !e.consent.allowsFeedback())
1210
1343
  return null;
1344
+ const current = requestSessionIsCurrent(e);
1211
1345
  const id = e.identity.get();
1212
1346
  const comment = await e.transport.requestCommentEdit({
1213
1347
  requestId,
@@ -1215,6 +1349,8 @@ export const UserGist = {
1215
1349
  anonymousId: id.anonymousId,
1216
1350
  body,
1217
1351
  });
1352
+ if (!current())
1353
+ return null;
1218
1354
  recordServerBackedEventLocally(e, REQUEST_COMMENT_EDITED_EVENT_NAME);
1219
1355
  return comment;
1220
1356
  }
@@ -1228,14 +1364,17 @@ export const UserGist = {
1228
1364
  try {
1229
1365
  const e = requireEngine();
1230
1366
  await ensureHydrated(e);
1231
- if (!e.consent.allowsFeedback())
1367
+ if (e.resetting || !e.consent.allowsFeedback())
1232
1368
  return;
1369
+ const current = requestSessionIsCurrent(e);
1233
1370
  const id = e.identity.get();
1234
1371
  await e.transport.requestCommentDelete({
1235
1372
  requestId,
1236
1373
  commentId,
1237
1374
  anonymousId: id.anonymousId,
1238
1375
  });
1376
+ if (!current())
1377
+ return;
1239
1378
  recordServerBackedEventLocally(e, REQUEST_COMMENT_DELETED_EVENT_NAME);
1240
1379
  }
1241
1380
  catch (err) {