@usergist/feedback-react-native 0.1.2 → 0.2.0-beta.1

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 (116) hide show
  1. package/CHANGELOG.md +11 -4
  2. package/EXPO-ACCEPTANCE.md +57 -0
  3. package/EXPO.md +103 -0
  4. package/README.md +69 -11
  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/Provider.d.ts.map +1 -1
  18. package/dist/Provider.js +28 -17
  19. package/dist/Provider.js.map +1 -1
  20. package/dist/Push.d.ts +2 -0
  21. package/dist/Push.d.ts.map +1 -1
  22. package/dist/Push.js +50 -2
  23. package/dist/Push.js.map +1 -1
  24. package/dist/UserGist.d.ts +9 -4
  25. package/dist/UserGist.d.ts.map +1 -1
  26. package/dist/UserGist.js +492 -130
  27. package/dist/UserGist.js.map +1 -1
  28. package/dist/expo/index.d.ts +68 -0
  29. package/dist/expo/index.d.ts.map +1 -0
  30. package/dist/expo/index.js +120 -0
  31. package/dist/expo/index.js.map +1 -0
  32. package/dist/internal/consent.d.ts.map +1 -1
  33. package/dist/internal/consent.js +4 -1
  34. package/dist/internal/consent.js.map +1 -1
  35. package/dist/internal/context.d.ts +1 -1
  36. package/dist/internal/context.d.ts.map +1 -1
  37. package/dist/internal/context.js +1 -1
  38. package/dist/internal/context.js.map +1 -1
  39. package/dist/internal/engine.d.ts +8 -2
  40. package/dist/internal/engine.d.ts.map +1 -1
  41. package/dist/internal/engine.js +189 -39
  42. package/dist/internal/engine.js.map +1 -1
  43. package/dist/internal/inapp-rules-cache.d.ts +1 -0
  44. package/dist/internal/inapp-rules-cache.d.ts.map +1 -1
  45. package/dist/internal/inapp-rules-cache.js +7 -0
  46. package/dist/internal/inapp-rules-cache.js.map +1 -1
  47. package/dist/internal/mutation-queue.d.ts +2 -2
  48. package/dist/internal/mutation-queue.d.ts.map +1 -1
  49. package/dist/internal/mutation-queue.js +32 -4
  50. package/dist/internal/mutation-queue.js.map +1 -1
  51. package/dist/internal/push-dedupe.d.ts +3 -0
  52. package/dist/internal/push-dedupe.d.ts.map +1 -0
  53. package/dist/internal/push-dedupe.js +33 -0
  54. package/dist/internal/push-dedupe.js.map +1 -0
  55. package/dist/internal/rules-cache.d.ts +1 -0
  56. package/dist/internal/rules-cache.d.ts.map +1 -1
  57. package/dist/internal/rules-cache.js +7 -0
  58. package/dist/internal/rules-cache.js.map +1 -1
  59. package/dist/internal/storage.d.ts +1 -0
  60. package/dist/internal/storage.d.ts.map +1 -1
  61. package/dist/internal/storage.js +1 -0
  62. package/dist/internal/storage.js.map +1 -1
  63. package/dist/internal/survey-invitations.d.ts +25 -0
  64. package/dist/internal/survey-invitations.d.ts.map +1 -0
  65. package/dist/internal/survey-invitations.js +54 -0
  66. package/dist/internal/survey-invitations.js.map +1 -0
  67. package/dist/internal/survey-matcher.d.ts +1 -0
  68. package/dist/internal/survey-matcher.d.ts.map +1 -1
  69. package/dist/internal/survey-matcher.js +3 -0
  70. package/dist/internal/survey-matcher.js.map +1 -1
  71. package/dist/internal/survey-rules-cache.d.ts +1 -0
  72. package/dist/internal/survey-rules-cache.d.ts.map +1 -1
  73. package/dist/internal/survey-rules-cache.js +7 -0
  74. package/dist/internal/survey-rules-cache.js.map +1 -1
  75. package/dist/internal/survey-store.d.ts +9 -2
  76. package/dist/internal/survey-store.d.ts.map +1 -1
  77. package/dist/internal/survey-store.js +37 -16
  78. package/dist/internal/survey-store.js.map +1 -1
  79. package/dist/internal/transport.d.ts +15 -3
  80. package/dist/internal/transport.d.ts.map +1 -1
  81. package/dist/internal/transport.js +16 -13
  82. package/dist/internal/transport.js.map +1 -1
  83. package/dist/internal/types.d.ts +2 -0
  84. package/dist/internal/types.d.ts.map +1 -1
  85. package/dist/native/events.d.ts +2 -0
  86. package/dist/native/events.d.ts.map +1 -1
  87. package/dist/native/events.js +2 -0
  88. package/dist/native/events.js.map +1 -1
  89. package/dist/native/push-adapter.d.ts +11 -0
  90. package/dist/native/push-adapter.d.ts.map +1 -0
  91. package/dist/native/push-adapter.js +10 -0
  92. package/dist/native/push-adapter.js.map +1 -0
  93. package/dist/native/push-bridge.d.ts +9 -0
  94. package/dist/native/push-bridge.d.ts.map +1 -1
  95. package/dist/native/push-bridge.js +39 -4
  96. package/dist/native/push-bridge.js.map +1 -1
  97. package/dist/ui/SurveyView.d.ts.map +1 -1
  98. package/dist/ui/SurveyView.js +5 -5
  99. package/dist/ui/SurveyView.js.map +1 -1
  100. package/dist/ui/requests/RequestsHost.d.ts.map +1 -1
  101. package/dist/ui/requests/RequestsHost.js +8 -0
  102. package/dist/ui/requests/RequestsHost.js.map +1 -1
  103. package/expo-module.config.json +8 -0
  104. package/expo-support/UserGistExpoAppDelegateSubscriber.swift +34 -0
  105. package/expo-support/UserGistFeedbackExpo.podspec +17 -0
  106. package/ios/Extension/UserGistNotificationService.swift +52 -5
  107. package/ios/Shared/UserGistNotificationActions.swift +27 -0
  108. package/ios/Shared/UserGistPushState.swift +110 -0
  109. package/ios/UserGistPush.h +1 -6
  110. package/ios/UserGistPush.mm +18 -0
  111. package/ios/UserGistPushImpl.swift +46 -4
  112. package/ios/UserGistPushInterop.h +5 -0
  113. package/package.json +33 -7
  114. package/plugin/index.cjs +269 -0
  115. package/plugin/index.d.cts +14 -0
  116. package/plugin/templates/UserGistExpoMessagingService.kt +20 -0
package/dist/UserGist.js CHANGED
@@ -1,28 +1,28 @@
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 { userPropertiesUpdateSchema } from '@usergist/sdk-core/mobile';
2
+ import { hydratePushDedupe } from './internal/push-dedupe.js';
10
3
  import { REQUEST_COMMENTED_EVENT_NAME, REQUEST_COMMENT_EDITED_EVENT_NAME, REQUEST_COMMENT_DELETED_EVENT_NAME, } from '@usergist/sdk-core/mobile';
11
4
  import { asEventProps, clearAllState, createEngine, emitAppOpenWhenConsentReady, emitAppVersionChanged, ensureHydrated, enqueueAndEvaluate, flushNow, flushMutations, pollInstructions, refreshTargetingRules, recordServerBackedEventLocally, submitResponse, } from './internal/engine.js';
12
5
  import { setDebugEnabled, reportError, debugLog, logTrace, getTraces, setDiagnosticHandler, } from './internal/debug.js';
13
6
  import { DEFAULT_THEME, mergeTheme } from './ui/theme.js';
14
7
  import { createSurveyStore } from './internal/survey-store.js';
15
- import { UserGistPushNative, onTokenReceived, onNotificationReceived, onNotificationOpened, onTokenError, } from './native/push-bridge.js';
8
+ import { UserGistPushNative, onTokenReceived, onNotificationReceived, onNotificationOpened, onNotificationDisplayed, onNotificationDismissed, onTokenError, } from './native/push-bridge.js';
16
9
  import { Push } from './Push.js';
17
10
  import { AppState, Platform } from 'react-native';
18
- import { generateEventId, validateIdentifyTransition } from './internal/identity.js';
19
- import { configureStorageAdapter, } from './internal/storage.js';
11
+ import { generateEventId, validateIdentifyTransition, } from './internal/identity.js';
12
+ import { configureStorageAdapter, STORAGE_KEYS, } from './internal/storage.js';
20
13
  import { surveyCompletionOutcome } from './internal/survey-completion.js';
21
14
  import { USERGIST_SDK_VERSION } from './internal/context.js';
22
15
  let engine = null;
16
+ let pushRegistrationQueue = Promise.resolve();
17
+ function serializePushRegistration(operation) {
18
+ const task = pushRegistrationQueue.then(operation);
19
+ pushRegistrationQueue = task.catch(() => undefined);
20
+ return task;
21
+ }
23
22
  let runtimeStarted = false;
24
23
  let runtimeStartPromise = null;
25
24
  let resetPromise = null;
25
+ const surveyStarts = new Map();
26
26
  let surveyStore = null;
27
27
  let surveyHandlers = {};
28
28
  let inAppHandlers = {};
@@ -37,6 +37,46 @@ function ensureRequestsCache() {
37
37
  }
38
38
  return requestsCache;
39
39
  }
40
+ function requestSessionIsCurrent(e) {
41
+ const generation = e.resetGeneration;
42
+ const consentVersion = e.consent.get().version;
43
+ const identity = e.identity.get();
44
+ return () => !e.resetting &&
45
+ generation === e.resetGeneration &&
46
+ consentVersion === e.consent.get().version &&
47
+ e.consent.allowsFeedback() &&
48
+ identity.anonymousId === e.identity.get().anonymousId &&
49
+ identity.externalId === e.identity.get().externalId;
50
+ }
51
+ async function syncNativePushState(e, push = e.consent.allowsPush()) {
52
+ const anonymousId = e.identity.get().anonymousId;
53
+ const generation = e.resetGeneration;
54
+ const version = e.consent.get().version;
55
+ const operation = nativeStateQueue.then(async () => {
56
+ if (generation !== e.resetGeneration ||
57
+ anonymousId !== e.identity.get().anonymousId ||
58
+ version !== e.consent.get().version)
59
+ return;
60
+ await UserGistPushNative.syncState({
61
+ writeKey: e.config.writeKey,
62
+ apiUrl: e.config.apiUrl,
63
+ anonymousId,
64
+ push: push && !e.resetting,
65
+ });
66
+ });
67
+ nativeStateQueue = operation.catch(() => undefined);
68
+ await operation;
69
+ }
70
+ let nativeStateQueue = Promise.resolve();
71
+ async function disableNativePushForConsent(e, version) {
72
+ const operation = nativeStateQueue.then(async () => {
73
+ if (e.consent.get().version !== version || e.consent.allowsPush())
74
+ return;
75
+ await UserGistPushNative.disablePush();
76
+ });
77
+ nativeStateQueue = operation.catch(() => undefined);
78
+ await operation;
79
+ }
40
80
  function requireEngine() {
41
81
  if (!engine) {
42
82
  throw new Error('UserGist.init must be called before using the SDK');
@@ -46,8 +86,10 @@ function requireEngine() {
46
86
  function ensureEngine(config) {
47
87
  if (engine)
48
88
  return engine;
49
- engine = createEngine(config);
50
- surveyStore = createSurveyStore(config.writeKey);
89
+ // A host-owned invitation is not a survey start. Only automatic rendering
90
+ // requests a session bundled with the delivery authorization.
91
+ engine = createEngine(config, () => !surveyHandlers.onInvite);
92
+ surveyStore = createSurveyStore(config.writeKey, () => engine.identity.get());
51
93
  return engine;
52
94
  }
53
95
  async function startRuntime(e) {
@@ -116,7 +158,9 @@ function attachEnginePushListeners() {
116
158
  try {
117
159
  const platform = (p.platform === 'ios' ? 'ios' : 'android');
118
160
  await UserGist.registerPushToken(p.token, platform, {
119
- environment: lastEnablePushOptions?.environment ?? defaultEnvironment(),
161
+ environment: lastEnablePushOptions?.environment ??
162
+ (await UserGistPushNative.defaultEnvironment()) ??
163
+ defaultEnvironment(),
120
164
  });
121
165
  }
122
166
  catch (err) {
@@ -130,12 +174,19 @@ function attachEnginePushListeners() {
130
174
  onNotificationReceived((p) => {
131
175
  forwardToPushHandler(p, 'received');
132
176
  });
177
+ onNotificationDisplayed((p) => forwardToPushHandler(p, 'displayed'));
178
+ onNotificationDismissed((p) => forwardToPushHandler(p, 'dismissed'));
133
179
  onNotificationOpened((p) => {
134
180
  forwardToPushHandler(p, 'opened');
135
181
  });
136
182
  }
137
183
  function forwardToPushHandler(p, kind) {
184
+ if (!engine?.consent.allowsPush() || engine.resetting)
185
+ return;
138
186
  const data = p.data ?? {};
187
+ if (data.usergist_anonymous_id &&
188
+ data.usergist_anonymous_id !== engine.identity.get().anonymousId)
189
+ return;
139
190
  if (p.deliveryId) {
140
191
  ;
141
192
  data.usergist_delivery_id = p.deliveryId;
@@ -151,7 +202,16 @@ function forwardToPushHandler(p, kind) {
151
202
  };
152
203
  Object.assign(userInfo, data);
153
204
  if (kind === 'received') {
154
- Push.handleReceived({ userInfo, notification: { title: p.title, body: p.body } });
205
+ Push.handleReceived({
206
+ userInfo,
207
+ notification: { title: p.title, body: p.body },
208
+ });
209
+ }
210
+ else if (kind === 'displayed') {
211
+ Push.handleDisplayed({ userInfo });
212
+ }
213
+ else if (kind === 'dismissed') {
214
+ Push.handleDismissed({ userInfo });
155
215
  }
156
216
  else {
157
217
  Push.handleOpened({ userInfo, actionIdentifier: p.actionIdentifier });
@@ -163,7 +223,16 @@ function forwardToPushHandler(p, kind) {
163
223
  dataStr[k] = String(v);
164
224
  }
165
225
  if (kind === 'received') {
166
- Push.handleReceived({ data: dataStr, notification: { title: p.title, body: p.body } });
226
+ Push.handleReceived({
227
+ data: dataStr,
228
+ notification: { title: p.title, body: p.body },
229
+ });
230
+ }
231
+ else if (kind === 'displayed') {
232
+ Push.handleDisplayed({ data: dataStr });
233
+ }
234
+ else if (kind === 'dismissed') {
235
+ Push.handleDismissed({ data: dataStr });
167
236
  }
168
237
  else {
169
238
  Push.handleOpened({ data: dataStr, actionIdentifier: p.actionIdentifier });
@@ -193,9 +262,9 @@ export const UserGist = {
193
262
  init(config) {
194
263
  void UserGist.initAsync(config);
195
264
  },
196
- /** Defer campaign UI without stopping analytics or closing active UI. */
265
+ /** Defer campaign UI without interrupting analytics or an active modal. */
197
266
  pausePresentation() { engine?.presentation.setPaused(true); },
198
- /** Call after startup loading and navigation have completed. */
267
+ /** Call after the host's loaded screen and navigation are ready. */
199
268
  resumePresentation() { engine?.presentation.setPaused(false); },
200
269
  __internal_presentationGate() { return requireEngine().presentation; },
201
270
  async initAsync(config, initialIdentity) {
@@ -207,6 +276,8 @@ export const UserGist = {
207
276
  if (result !== 'synced')
208
277
  return result;
209
278
  }
279
+ await hydratePushDedupe(e.config.writeKey, e.identity.get().anonymousId);
280
+ await syncNativePushState(e);
210
281
  await startRuntime(e);
211
282
  return 'synced';
212
283
  }
@@ -226,14 +297,20 @@ export const UserGist = {
226
297
  }
227
298
  const e = requireEngine();
228
299
  await ensureHydrated(e);
300
+ if (e.resetting)
301
+ return 'rejected';
229
302
  const identity = e.identity.get();
230
303
  const pendingIdentity = e.mutations.peek();
231
- const transition = validateIdentifyTransition(identity.externalId, pendingIdentity?.kind === 'identify' ? pendingIdentity.payload.externalId : null, userId);
304
+ const transition = validateIdentifyTransition(identity.externalId, pendingIdentity?.kind === 'identify'
305
+ ? pendingIdentity.payload.externalId
306
+ : null, userId);
232
307
  if (transition === 'reset_required') {
233
308
  reportError('identify rejected: call reset() before switching users');
234
309
  return 'rejected';
235
310
  }
236
- const cleanProps = e.consent.allowsAnalytics() ? asEventProps(properties) : undefined;
311
+ const cleanProps = e.consent.allowsAnalytics()
312
+ ? asEventProps(properties)
313
+ : undefined;
237
314
  if (transition === 'already_identified' &&
238
315
  (!cleanProps || Object.keys(cleanProps).length === 0)) {
239
316
  return 'synced';
@@ -257,6 +334,7 @@ export const UserGist = {
257
334
  debugLog('identify is saved locally and pending retry');
258
335
  return 'queued';
259
336
  }
337
+ await syncNativePushState(e);
260
338
  void UserGist.rebindPushToken(userId);
261
339
  return 'synced';
262
340
  }
@@ -265,6 +343,33 @@ export const UserGist = {
265
343
  return 'rejected';
266
344
  }
267
345
  },
346
+ /** Update the current user, including anonymous users. Durable and retried on reconnect. */
347
+ async setUserProperties(properties, unset = []) {
348
+ try {
349
+ const e = requireEngine();
350
+ const generation = e.resetGeneration;
351
+ await ensureHydrated(e);
352
+ if (e !== engine ||
353
+ e.resetting ||
354
+ e.resetGeneration !== generation ||
355
+ !e.consent.allowsAnalytics())
356
+ return 'rejected';
357
+ const update = userPropertiesUpdateSchema.parse({
358
+ mutationId: generateEventId(),
359
+ set: properties,
360
+ unset: [...unset],
361
+ });
362
+ const mutationId = await e.mutations.enqueue('user-properties', 'analytics', { ...update, anonymousId: e.identity.get().anonymousId });
363
+ const result = await flushMutations(e);
364
+ if (result.permanentlyRejectedIds.has(mutationId))
365
+ return 'rejected';
366
+ return e.mutations.has(mutationId) ? 'queued' : 'synced';
367
+ }
368
+ catch (error) {
369
+ reportError('setUserProperties failed', error);
370
+ return 'rejected';
371
+ }
372
+ },
268
373
  track(eventName, properties) {
269
374
  try {
270
375
  if (typeof eventName !== 'string' || eventName.length === 0) {
@@ -287,7 +392,29 @@ export const UserGist = {
287
392
  if (purposes.survey === false)
288
393
  e.presentation.invalidate('survey');
289
394
  await ensureHydrated(e);
395
+ if (e.resetting)
396
+ return false;
290
397
  const next = await e.consent.set(purposes);
398
+ if (e.resetting || next.version !== e.consent.get().version)
399
+ return false;
400
+ if (!next.feedback) {
401
+ requestsCache = null;
402
+ e.events.emit('dismissRequests', undefined);
403
+ }
404
+ await syncNativePushState(e);
405
+ if (e.resetting || next.version !== e.consent.get().version)
406
+ return false;
407
+ if (purposes.push === false) {
408
+ lastEnablePushOptions = null;
409
+ await pushRegistrationQueue;
410
+ if (e.resetting || next.version !== e.consent.get().version)
411
+ return false;
412
+ if (e.lastPushToken)
413
+ await UserGist.invalidatePushToken(e.lastPushToken);
414
+ await disableNativePushForConsent(e, next.version);
415
+ }
416
+ if (e.resetting || next.version !== e.consent.get().version)
417
+ return false;
291
418
  const id = e.identity.get();
292
419
  try {
293
420
  await e.transport.consent({
@@ -328,13 +455,19 @@ export const UserGist = {
328
455
  e.resetting = true;
329
456
  e.resetGeneration += 1;
330
457
  e.presentation.invalidate();
458
+ requestsCache = null;
459
+ lastEnablePushOptions = null;
331
460
  e.events.emit('resetSurfaces', undefined);
332
461
  const operation = (async () => {
333
462
  await ensureHydrated(e);
463
+ await syncNativePushState(e, false);
464
+ await pushRegistrationQueue;
334
465
  if (e.lastPushToken)
335
466
  await UserGist.invalidatePushToken(e.lastPushToken);
336
467
  await UserGistPushNative.disablePush().catch(() => undefined);
337
468
  await clearAllState(e);
469
+ await surveyStore?.clear();
470
+ await hydratePushDedupe(e.config.writeKey, e.identity.get().anonymousId);
338
471
  })();
339
472
  const finalized = operation
340
473
  .catch((err) => reportError('reset failed', err))
@@ -453,42 +586,53 @@ export const UserGist = {
453
586
  return;
454
587
  const e = requireEngine();
455
588
  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)
589
+ if (!e.consent.allowsPush() || e.resetting)
475
590
  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,
591
+ const generation = e.resetGeneration;
592
+ await serializePushRegistration(async () => {
593
+ if (e.resetting ||
594
+ generation !== e.resetGeneration ||
595
+ !e.consent.allowsPush())
596
+ return;
597
+ const id = e.identity.get();
598
+ const environment = opts?.environment ??
599
+ (await UserGistPushNative.defaultEnvironment()) ??
600
+ (e.config.environment === 'production' ? 'production' : 'sandbox');
601
+ const registrationKey = [
602
+ id.anonymousId,
603
+ id.externalId ?? '',
604
+ platform,
605
+ environment,
606
+ token,
607
+ ].join(':');
608
+ if (e.lastPushRegistrationKey === registrationKey &&
609
+ Date.now() - e.lastPushRegistrationAt < 24 * 60 * 60_000)
610
+ return;
611
+ if (e.resetting ||
612
+ generation !== e.resetGeneration ||
613
+ !e.consent.allowsPush())
614
+ return;
615
+ // Persist before the request so logout can invalidate a token even when
616
+ // the response is lost or the process exits after the server accepts it.
617
+ e.lastPushToken = token;
618
+ await e.storage.setJson(STORAGE_KEYS.pushToken, token);
619
+ await e.transport.pushRegisterToken({
620
+ anonymousId: id.anonymousId,
621
+ externalId: id.externalId ?? null,
622
+ token,
623
+ platform,
624
+ environment,
625
+ language: undefined,
626
+ timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
627
+ appVersion: undefined,
628
+ sdkVersion: `rn-${USERGIST_SDK_VERSION}`,
629
+ optIn: true,
630
+ });
631
+ e.lastPushToken = token;
632
+ e.lastPushRegistrationKey = registrationKey;
633
+ e.lastPushRegistrationAt = Date.now();
634
+ debugLog('push token registered with server');
487
635
  });
488
- e.lastPushToken = token;
489
- e.lastPushRegistrationKey = registrationKey;
490
- e.lastPushRegistrationAt = Date.now();
491
- debugLog('push token registered with server');
492
636
  }
493
637
  catch (err) {
494
638
  reportError('registerPushToken failed', err);
@@ -501,6 +645,8 @@ export const UserGist = {
501
645
  const e = requireEngine();
502
646
  await ensureHydrated(e);
503
647
  const id = e.identity.get();
648
+ e.lastPushRegistrationKey = null;
649
+ e.lastPushRegistrationAt = 0;
504
650
  await e.transport.pushInvalidateToken({
505
651
  anonymousId: id.anonymousId,
506
652
  token,
@@ -520,14 +666,22 @@ export const UserGist = {
520
666
  try {
521
667
  const e = requireEngine();
522
668
  await ensureHydrated(e);
523
- const id = e.identity.get();
524
- const token = e.lastPushToken;
525
- if (!token)
526
- return;
527
- await e.transport.pushRebind({
528
- anonymousId: id.anonymousId,
529
- externalId,
530
- token,
669
+ const generation = e.resetGeneration;
670
+ await serializePushRegistration(async () => {
671
+ if (generation !== e.resetGeneration)
672
+ return;
673
+ const id = e.identity.get();
674
+ const token = e.lastPushToken;
675
+ if (!token ||
676
+ e.resetting ||
677
+ !e.consent.allowsPush() ||
678
+ id.externalId !== externalId)
679
+ return;
680
+ await e.transport.pushRebind({
681
+ anonymousId: id.anonymousId,
682
+ externalId,
683
+ token,
684
+ });
531
685
  });
532
686
  }
533
687
  catch (err) {
@@ -618,6 +772,36 @@ export const UserGist = {
618
772
  // react-native-onesignal, WonderPush, or any other push SDK.
619
773
  async enablePush(opts) {
620
774
  try {
775
+ const e = requireEngine();
776
+ await ensureHydrated(e);
777
+ if (!e.consent.allowsPush() || e.resetting)
778
+ return {
779
+ granted: false,
780
+ status: 'not_determined',
781
+ platform: Platform.OS === 'ios' ? 'ios' : 'android',
782
+ error: 'consent_required',
783
+ };
784
+ const generation = e.resetGeneration;
785
+ const consentVersion = e.consent.get().version;
786
+ const current = () => !e.resetting &&
787
+ generation === e.resetGeneration &&
788
+ consentVersion === e.consent.get().version &&
789
+ e.consent.allowsPush();
790
+ const cancelled = () => ({
791
+ granted: false,
792
+ status: 'not_determined',
793
+ platform: Platform.OS === 'ios' ? 'ios' : 'android',
794
+ error: 'session_changed',
795
+ });
796
+ opts = {
797
+ ...opts,
798
+ environment: opts?.environment ??
799
+ (await UserGistPushNative.defaultEnvironment()) ??
800
+ defaultEnvironment(),
801
+ };
802
+ await syncNativePushState(e);
803
+ if (!current())
804
+ return cancelled();
621
805
  if (!enginePushListenersAttached)
622
806
  attachEnginePushListeners();
623
807
  // Snapshot the caller's options so the AppState foreground listener
@@ -628,12 +812,16 @@ export const UserGist = {
628
812
  installDelegateProxy: opts?.installDelegateProxy,
629
813
  };
630
814
  const result = await UserGistPushNative.enablePush(opts ?? {});
815
+ // Reset/withdrawal owns native cleanup. An old permission callback must
816
+ // not disable a new account's subsequently enabled notification adapter.
817
+ if (!current())
818
+ return cancelled();
631
819
  // Server-side registration runs on the tokenReceived event; if the
632
820
  // native module already had a cached token it surfaces here.
633
821
  if (result.granted && result.token) {
634
822
  const platform = (result.platform === 'ios' ? 'ios' : 'android');
635
823
  await UserGist.registerPushToken(result.token, platform, {
636
- environment: opts?.environment ?? (defaultEnvironment()),
824
+ environment: opts?.environment ?? defaultEnvironment(),
637
825
  });
638
826
  }
639
827
  return result;
@@ -642,23 +830,19 @@ export const UserGist = {
642
830
  reportError('enablePush failed', err);
643
831
  return {
644
832
  granted: false,
645
- status: 'denied',
646
- platform: (typeof navigator === 'object' && navigator.product === 'ReactNative' ? 'ios' : 'android'),
833
+ status: 'not_determined',
834
+ error: 'push_setup_failed',
835
+ platform: Platform.OS === 'ios' ? 'ios' : 'android',
647
836
  };
648
837
  }
649
838
  },
650
839
  async disablePush() {
840
+ lastEnablePushOptions = null;
651
841
  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
- }
842
+ if (engine)
843
+ await UserGist.setConsent({ push: false });
844
+ else
845
+ await UserGistPushNative.disablePush();
662
846
  }
663
847
  catch (err) {
664
848
  reportError('disablePush failed', err);
@@ -717,10 +901,20 @@ export const UserGist = {
717
901
  debugLog('openSurvey: survey consent not granted; ignoring');
718
902
  return;
719
903
  }
904
+ const generation = e.resetGeneration;
905
+ const identity = e.identity.get();
906
+ const consentVersion = e.consent.get().version;
907
+ const invitation = await e.surveyInvitations.find(surveyId);
908
+ if (e.resetting || e.resetGeneration !== generation ||
909
+ !e.consent.allowsSurvey() || e.consent.get().version !== consentVersion ||
910
+ e.identity.get().anonymousId !== identity.anonymousId ||
911
+ e.identity.get().externalId !== identity.externalId)
912
+ return;
720
913
  e.events.emit('showSurvey', {
721
914
  surveyId,
722
- source: context?.source ?? 'on_demand',
915
+ source: context?.source ?? invitation?.source ?? 'on_demand',
723
916
  ...(context?.language ? { language: context.language } : {}),
917
+ ...(invitation ? { survey: invitation.survey, attempt: invitation.attempt } : {}),
724
918
  });
725
919
  }
726
920
  catch (err) {
@@ -738,7 +932,8 @@ export const UserGist = {
738
932
  const e = requireEngine();
739
933
  await ensureHydrated(e);
740
934
  // Accept URLs of the shape `*://*/s/<token>` or `*survey=<token>`.
741
- const match = url.match(/\/s\/([A-Za-z0-9._-]+)/) ?? url.match(/[?&]survey=([A-Za-z0-9._-]+)/);
935
+ const match = url.match(/\/s\/([A-Za-z0-9._-]+)/) ??
936
+ url.match(/[?&]survey=([A-Za-z0-9._-]+)/);
742
937
  if (!match || !match[1])
743
938
  return false;
744
939
  const token = match[1];
@@ -755,7 +950,10 @@ export const UserGist = {
755
950
  debugLog('survey link resolved but consent required — awaiting consent');
756
951
  return false;
757
952
  }
758
- e.events.emit('showSurvey', { surveyId: resolved.surveyId, source: 'link' });
953
+ e.events.emit('showSurvey', {
954
+ surveyId: resolved.surveyId,
955
+ source: 'link',
956
+ });
759
957
  return true;
760
958
  }
761
959
  catch (err) {
@@ -808,6 +1006,9 @@ export const UserGist = {
808
1006
  reportError('reportSurveyShown failed', err);
809
1007
  }
810
1008
  },
1009
+ __internal_surveyOpenFailed(surveyId) {
1010
+ engine?.surveyMatcher.cancelPending(surveyId);
1011
+ },
811
1012
  __internal_armedSurveyById(surveyId) {
812
1013
  // Local-fire fast-path. If the survey-rules-cache already has the
813
1014
  // full survey content (because the matcher just fired), the
@@ -828,6 +1029,11 @@ export const UserGist = {
828
1029
  const e = requireEngine();
829
1030
  await ensureHydrated(e);
830
1031
  const id = e.identity.get();
1032
+ const pending = await surveyStore?.findForSurvey(surveyId);
1033
+ if (pending?.survey &&
1034
+ Date.now() - pending.startedAt <
1035
+ pending.survey.saveResumeWindowSeconds * 1000)
1036
+ return pending.survey;
831
1037
  return await e.transport.surveyGet({
832
1038
  surveyId,
833
1039
  anonymousId: id.anonymousId,
@@ -840,52 +1046,169 @@ export const UserGist = {
840
1046
  return null;
841
1047
  }
842
1048
  },
843
- async __internal_createAttempt(surveyId, source, language) {
1049
+ async __internal_createAttempt(surveyId, source, language, presentationId, content, preparedAttempt) {
1050
+ const active = requireEngine();
1051
+ const identity = active.identity.get();
1052
+ const key = JSON.stringify([
1053
+ active.resetGeneration,
1054
+ identity.anonymousId,
1055
+ identity.externalId,
1056
+ surveyId,
1057
+ language,
1058
+ presentationId,
1059
+ preparedAttempt?.attemptId,
1060
+ ]);
1061
+ const pending = surveyStarts.get(key);
1062
+ if (pending)
1063
+ return pending;
1064
+ const operation = (async () => {
1065
+ try {
1066
+ const e = requireEngine();
1067
+ await ensureHydrated(e);
1068
+ const id = e.identity.get();
1069
+ const generation = e.resetGeneration;
1070
+ const consentVersion = e.consent.get().version;
1071
+ const current = () => !e.resetting &&
1072
+ e.resetGeneration === generation &&
1073
+ e.consent.allowsSurvey() &&
1074
+ e.consent.get().version === consentVersion &&
1075
+ e.identity.get().anonymousId === id.anonymousId &&
1076
+ e.identity.get().externalId === id.externalId;
1077
+ if (!current())
1078
+ return null;
1079
+ const saved = await surveyStore?.findForSurvey(surveyId);
1080
+ if (!current())
1081
+ return null;
1082
+ // Resuming a durable device session preserves its original content and
1083
+ // answers. Never replace it with a stale network snapshot.
1084
+ if (!preparedAttempt &&
1085
+ (!presentationId || saved?.attemptId === presentationId) &&
1086
+ saved?.survey &&
1087
+ Date.now() - saved.startedAt <
1088
+ saved.survey.saveResumeWindowSeconds * 1000) {
1089
+ if (saved.startRequest)
1090
+ await e.mutations.enqueue('survey-start', 'survey', { surveyId, body: saved.startRequest }, `survey-start:${saved.attemptId}`);
1091
+ if (!current())
1092
+ return null;
1093
+ void flushMutations(e);
1094
+ return {
1095
+ attemptId: saved.attemptId,
1096
+ startQuestionId: saved.survey.flow.startQuestionId,
1097
+ currentQuestionId: saved.currentQuestionId,
1098
+ snapshot: saved.snapshot,
1099
+ resumed: true,
1100
+ resolvedContent: saved.survey,
1101
+ };
1102
+ }
1103
+ const armed = !language && !presentationId
1104
+ ? e.surveyRules.getById(surveyId)
1105
+ : null;
1106
+ const grant = armed?.localStart &&
1107
+ Date.parse(armed.localStart.expiresAt) > Date.now()
1108
+ ? armed.localStart
1109
+ : null;
1110
+ const body = {
1111
+ presentationId,
1112
+ anonymousId: id.anonymousId,
1113
+ externalId: id.externalId ?? null,
1114
+ source,
1115
+ ...(language ? { language } : {}),
1116
+ sdkVersion: `rn-${USERGIST_SDK_VERSION}`,
1117
+ platform: e.context.platform(),
1118
+ ...(presentationId || grant || source === 'triggered' || source === 'scheduled'
1119
+ ? { resume: false, clientAttemptId: presentationId ?? generateEventId() }
1120
+ : { resume: true }),
1121
+ ...(grant
1122
+ ? {
1123
+ localStart: {
1124
+ token: grant.token,
1125
+ startedAt: new Date().toISOString(),
1126
+ },
1127
+ }
1128
+ : {}),
1129
+ };
1130
+ const res = preparedAttempt ??
1131
+ (grant && armed
1132
+ ? {
1133
+ attemptId: body.clientAttemptId,
1134
+ startQuestionId: armed.survey.flow.startQuestionId,
1135
+ currentQuestionId: armed.survey.flow.startQuestionId,
1136
+ progressSnapshot: {},
1137
+ resumed: false,
1138
+ resolvedContent: armed.survey,
1139
+ }
1140
+ : await e.transport.surveyCreateAttempt(surveyId, body));
1141
+ if (!current())
1142
+ return null;
1143
+ await surveyStore?.upsert({
1144
+ anonymousId: id.anonymousId,
1145
+ externalId: id.externalId,
1146
+ survey: res.resolvedContent ?? content,
1147
+ ...(grant && !preparedAttempt ? { startRequest: body } : {}),
1148
+ surveyId,
1149
+ attemptId: res.attemptId,
1150
+ startedAt: Date.now(),
1151
+ currentQuestionId: res.currentQuestionId,
1152
+ snapshot: (res.progressSnapshot ??
1153
+ {}),
1154
+ language: language ?? null,
1155
+ });
1156
+ if (!current())
1157
+ return null;
1158
+ if (presentationId) {
1159
+ await e.surveyInvitations.remove(presentationId);
1160
+ if (!current())
1161
+ return null;
1162
+ }
1163
+ if (grant && !preparedAttempt) {
1164
+ await e.mutations.enqueue('survey-start', 'survey', { surveyId, body }, `survey-start:${res.attemptId}`);
1165
+ if (!current())
1166
+ return null;
1167
+ void flushMutations(e);
1168
+ }
1169
+ return {
1170
+ attemptId: res.attemptId,
1171
+ startQuestionId: res.startQuestionId,
1172
+ currentQuestionId: res.currentQuestionId,
1173
+ snapshot: (res.progressSnapshot ??
1174
+ {}),
1175
+ resumed: res.resumed,
1176
+ resolvedContent: res.resolvedContent,
1177
+ };
1178
+ }
1179
+ catch (err) {
1180
+ reportError('createAttempt failed', err);
1181
+ return null;
1182
+ }
1183
+ })();
1184
+ surveyStarts.set(key, operation);
844
1185
  try {
845
- const e = requireEngine();
846
- await ensureHydrated(e);
847
- // Give a previously deferred completion a chance to reach the server
848
- // before asking it to create or resume another attempt.
849
- await flushMutations(e);
850
- const id = e.identity.get();
851
- const res = await e.transport.surveyCreateAttempt(surveyId, {
852
- anonymousId: id.anonymousId,
853
- externalId: id.externalId ?? null,
854
- source,
855
- ...(language ? { language } : {}),
856
- sdkVersion: `rn-${USERGIST_SDK_VERSION}`,
857
- });
858
- await surveyStore?.upsert({
859
- surveyId,
860
- attemptId: res.attemptId,
861
- startedAt: Date.now(),
862
- currentQuestionId: res.currentQuestionId,
863
- snapshot: (res.progressSnapshot ?? {}),
864
- language: language ?? null,
865
- });
866
- return {
867
- attemptId: res.attemptId,
868
- startQuestionId: res.startQuestionId,
869
- currentQuestionId: res.currentQuestionId,
870
- snapshot: (res.progressSnapshot ?? {}),
871
- resumed: res.resumed,
872
- };
1186
+ return await operation;
873
1187
  }
874
- catch (err) {
875
- reportError('createAttempt failed', err);
876
- return null;
1188
+ finally {
1189
+ if (surveyStarts.get(key) === operation)
1190
+ surveyStarts.delete(key);
877
1191
  }
878
1192
  },
879
1193
  async __internal_saveProgress(attemptId, currentQuestionId, snapshot) {
880
1194
  // Persist the resume point before attempting the network. Progress is
881
1195
  // best-effort transport; losing connectivity must not lose local answers.
1196
+ const e = requireEngine();
1197
+ if (e.resetting || !e.consent.allowsSurvey())
1198
+ return;
1199
+ const generation = e.resetGeneration;
1200
+ const pending = (await surveyStore?.list())?.find((a) => a.attemptId === attemptId);
1201
+ if (!pending || e.resetting || e.resetGeneration !== generation)
1202
+ return;
882
1203
  await surveyStore?.updateProgress(attemptId, currentQuestionId, snapshot);
883
1204
  try {
884
- const e = requireEngine();
885
- await e.transport.surveyUpdateProgress(attemptId, {
886
- currentQuestionId,
887
- progressSnapshot: snapshot,
1205
+ if (e.resetting || e.resetGeneration !== generation)
1206
+ return;
1207
+ await e.mutations.enqueue('survey-progress', 'survey', {
1208
+ attemptId,
1209
+ body: { currentQuestionId, progressSnapshot: snapshot },
888
1210
  });
1211
+ void flushMutations(e);
889
1212
  }
890
1213
  catch (err) {
891
1214
  // Completion can win a race with an in-flight progress PATCH, making a
@@ -925,7 +1248,9 @@ export const UserGist = {
925
1248
  throw new Error('Survey submission was rejected by the server');
926
1249
  }
927
1250
  if (outcome === 'deferred') {
928
- debugLog('survey completion saved locally; delivery deferred', { attemptId });
1251
+ debugLog('survey completion saved locally; delivery deferred', {
1252
+ attemptId,
1253
+ });
929
1254
  }
930
1255
  // Once the completion is durably queued, the attempt must not reopen as
931
1256
  // resumable work. The mutation queue owns delivery from this point.
@@ -962,6 +1287,9 @@ export const UserGist = {
962
1287
  try {
963
1288
  const e = requireEngine();
964
1289
  await ensureHydrated(e);
1290
+ if (e.resetting || !e.consent.allowsFeedback())
1291
+ return { items: [], nextCursor: null };
1292
+ const current = requestSessionIsCurrent(e);
965
1293
  const id = e.identity.get();
966
1294
  const result = await e.transport.requestsList({
967
1295
  anonymousId: id.anonymousId,
@@ -973,6 +1301,8 @@ export const UserGist = {
973
1301
  cursor: options.cursor ?? null,
974
1302
  limit: options.limit,
975
1303
  });
1304
+ if (!current())
1305
+ return { items: [], nextCursor: null };
976
1306
  ensureRequestsCache().upsertList(result.items.map((s) => ({
977
1307
  id: s.id,
978
1308
  appId: '',
@@ -1000,7 +1330,9 @@ export const UserGist = {
1000
1330
  submitRequest(title, description, callback) {
1001
1331
  try {
1002
1332
  const e = requireEngine();
1003
- if (typeof title !== 'string' || title.length === 0 || title.length > 120) {
1333
+ if (typeof title !== 'string' ||
1334
+ title.length === 0 ||
1335
+ title.length > 120) {
1004
1336
  callback?.(new Error('title required, max 120 chars'));
1005
1337
  return;
1006
1338
  }
@@ -1013,10 +1345,11 @@ export const UserGist = {
1013
1345
  void (async () => {
1014
1346
  try {
1015
1347
  await ensureHydrated(e);
1016
- if (!e.consent.allowsFeedback()) {
1348
+ if (e.resetting || !e.consent.allowsFeedback()) {
1017
1349
  callback?.(new Error('feedback consent required'));
1018
1350
  return;
1019
1351
  }
1352
+ const current = requestSessionIsCurrent(e);
1020
1353
  const id = e.identity.get();
1021
1354
  const req = await e.transport.requestSubmit({
1022
1355
  idempotencyKey: generateEventId(),
@@ -1025,6 +1358,10 @@ export const UserGist = {
1025
1358
  title,
1026
1359
  description,
1027
1360
  });
1361
+ if (!current()) {
1362
+ callback?.(new Error('Request cancelled by identity or consent change'));
1363
+ return;
1364
+ }
1028
1365
  ensureRequestsCache().upsert(req);
1029
1366
  recordServerBackedEventLocally(e, '$request_submitted');
1030
1367
  requestsHandlers.onSubmit?.(req);
@@ -1044,11 +1381,12 @@ export const UserGist = {
1044
1381
  try {
1045
1382
  const e = requireEngine();
1046
1383
  await ensureHydrated(e);
1047
- if (!e.consent.allowsFeedback())
1384
+ if (e.resetting || !e.consent.allowsFeedback())
1048
1385
  return;
1049
1386
  const cache = ensureRequestsCache();
1050
1387
  const rollback = cache.applyOptimisticVote(requestId, vote);
1051
1388
  try {
1389
+ const current = requestSessionIsCurrent(e);
1052
1390
  const id = e.identity.get();
1053
1391
  const result = await e.transport.requestVote({
1054
1392
  requestId,
@@ -1056,6 +1394,10 @@ export const UserGist = {
1056
1394
  externalId: id.externalId ?? null,
1057
1395
  vote,
1058
1396
  });
1397
+ if (!current()) {
1398
+ rollback();
1399
+ return;
1400
+ }
1059
1401
  cache.commitVote(requestId, result);
1060
1402
  recordServerBackedEventLocally(e, vote ? '$request_upvoted' : '$request_unupvoted');
1061
1403
  requestsHandlers.onVote?.(result);
@@ -1073,11 +1415,12 @@ export const UserGist = {
1073
1415
  try {
1074
1416
  const e = requireEngine();
1075
1417
  await ensureHydrated(e);
1076
- if (!e.consent.allowsFeedback())
1418
+ if (e.resetting || !e.consent.allowsFeedback())
1077
1419
  return;
1078
1420
  const cache = ensureRequestsCache();
1079
1421
  const rollback = cache.applyOptimisticFollow(requestId, follow);
1080
1422
  try {
1423
+ const current = requestSessionIsCurrent(e);
1081
1424
  const id = e.identity.get();
1082
1425
  const result = await e.transport.requestFollow({
1083
1426
  requestId,
@@ -1085,6 +1428,10 @@ export const UserGist = {
1085
1428
  externalId: id.externalId ?? null,
1086
1429
  follow,
1087
1430
  });
1431
+ if (!current()) {
1432
+ rollback();
1433
+ return;
1434
+ }
1088
1435
  cache.commitFollow(requestId, result);
1089
1436
  recordServerBackedEventLocally(e, follow ? '$request_followed' : '$request_unfollowed');
1090
1437
  requestsHandlers.onFollow?.(result);
@@ -1108,9 +1455,11 @@ export const UserGist = {
1108
1455
  try {
1109
1456
  const e = requireEngine();
1110
1457
  await ensureHydrated(e);
1111
- if (!e.consent.allowsFeedback())
1458
+ if (e.resetting || !e.consent.allowsFeedback())
1112
1459
  return null;
1113
- return await e.transport.requestBranding();
1460
+ const current = requestSessionIsCurrent(e);
1461
+ const branding = await e.transport.requestBranding();
1462
+ return current() ? branding : null;
1114
1463
  }
1115
1464
  catch (err) {
1116
1465
  reportError('getRequestBranding failed', err);
@@ -1127,14 +1476,17 @@ export const UserGist = {
1127
1476
  try {
1128
1477
  const e = requireEngine();
1129
1478
  await ensureHydrated(e);
1130
- if (!e.consent.allowsFeedback())
1479
+ if (e.resetting || !e.consent.allowsFeedback())
1131
1480
  return null;
1481
+ const current = requestSessionIsCurrent(e);
1132
1482
  const id = e.identity.get();
1133
1483
  const req = await e.transport.requestGet({
1134
1484
  requestId,
1135
1485
  anonymousId: id.anonymousId,
1136
1486
  externalId: id.externalId ?? null,
1137
1487
  });
1488
+ if (!current())
1489
+ return null;
1138
1490
  ensureRequestsCache().upsert(req);
1139
1491
  return req;
1140
1492
  }
@@ -1146,7 +1498,7 @@ export const UserGist = {
1146
1498
  openRequestsBoard() {
1147
1499
  try {
1148
1500
  const e = requireEngine();
1149
- if (!e.consent.allowsFeedback())
1501
+ if (e.resetting || !e.consent.allowsFeedback())
1150
1502
  return;
1151
1503
  e.events.emit('showRequestsBoard', undefined);
1152
1504
  }
@@ -1157,7 +1509,7 @@ export const UserGist = {
1157
1509
  openRequestDetail(requestId) {
1158
1510
  try {
1159
1511
  const e = requireEngine();
1160
- if (!e.consent.allowsFeedback())
1512
+ if (e.resetting || !e.consent.allowsFeedback())
1161
1513
  return;
1162
1514
  e.events.emit('showRequestDetail', { requestId });
1163
1515
  }
@@ -1174,15 +1526,16 @@ export const UserGist = {
1174
1526
  try {
1175
1527
  const e = requireEngine();
1176
1528
  await ensureHydrated(e);
1177
- if (!e.consent.allowsFeedback())
1529
+ if (e.resetting || !e.consent.allowsFeedback())
1178
1530
  return [];
1531
+ const current = requestSessionIsCurrent(e);
1179
1532
  const id = e.identity.get();
1180
1533
  const result = await e.transport.requestCommentsList({
1181
1534
  requestId,
1182
1535
  anonymousId: id.anonymousId,
1183
1536
  externalId: id.externalId ?? null,
1184
1537
  });
1185
- return result.items;
1538
+ return current() ? result.items : [];
1186
1539
  }
1187
1540
  catch (err) {
1188
1541
  reportError('getComments failed', err);
@@ -1195,8 +1548,9 @@ export const UserGist = {
1195
1548
  const e = requireEngine();
1196
1549
  validateCommentBody(body);
1197
1550
  await ensureHydrated(e);
1198
- if (!e.consent.allowsFeedback())
1551
+ if (e.resetting || !e.consent.allowsFeedback())
1199
1552
  return null;
1553
+ const current = requestSessionIsCurrent(e);
1200
1554
  const id = e.identity.get();
1201
1555
  const comment = await e.transport.requestCommentPost({
1202
1556
  idempotencyKey: generateEventId(),
@@ -1205,6 +1559,8 @@ export const UserGist = {
1205
1559
  externalId: id.externalId ?? null,
1206
1560
  body,
1207
1561
  });
1562
+ if (!current())
1563
+ return null;
1208
1564
  recordServerBackedEventLocally(e, REQUEST_COMMENTED_EVENT_NAME);
1209
1565
  return comment;
1210
1566
  }
@@ -1219,8 +1575,9 @@ export const UserGist = {
1219
1575
  const e = requireEngine();
1220
1576
  validateCommentBody(body);
1221
1577
  await ensureHydrated(e);
1222
- if (!e.consent.allowsFeedback())
1578
+ if (e.resetting || !e.consent.allowsFeedback())
1223
1579
  return null;
1580
+ const current = requestSessionIsCurrent(e);
1224
1581
  const id = e.identity.get();
1225
1582
  const comment = await e.transport.requestCommentEdit({
1226
1583
  requestId,
@@ -1228,6 +1585,8 @@ export const UserGist = {
1228
1585
  anonymousId: id.anonymousId,
1229
1586
  body,
1230
1587
  });
1588
+ if (!current())
1589
+ return null;
1231
1590
  recordServerBackedEventLocally(e, REQUEST_COMMENT_EDITED_EVENT_NAME);
1232
1591
  return comment;
1233
1592
  }
@@ -1241,14 +1600,17 @@ export const UserGist = {
1241
1600
  try {
1242
1601
  const e = requireEngine();
1243
1602
  await ensureHydrated(e);
1244
- if (!e.consent.allowsFeedback())
1603
+ if (e.resetting || !e.consent.allowsFeedback())
1245
1604
  return;
1605
+ const current = requestSessionIsCurrent(e);
1246
1606
  const id = e.identity.get();
1247
1607
  await e.transport.requestCommentDelete({
1248
1608
  requestId,
1249
1609
  commentId,
1250
1610
  anonymousId: id.anonymousId,
1251
1611
  });
1612
+ if (!current())
1613
+ return;
1252
1614
  recordServerBackedEventLocally(e, REQUEST_COMMENT_DELETED_EVENT_NAME);
1253
1615
  }
1254
1616
  catch (err) {