@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.
- package/CHANGELOG.md +11 -4
- package/EXPO-ACCEPTANCE.md +57 -0
- package/EXPO.md +103 -0
- package/README.md +69 -11
- package/UserGistFeedback.podspec +4 -1
- package/UserGistFeedbackExtension.podspec +1 -1
- package/android/build.gradle +3 -1
- package/android/src/main/AndroidManifest.xml +1 -0
- package/android/src/main/java/studio/usergist/feedback/UserGistFirebaseMessagingService.kt +78 -12
- package/android/src/main/java/studio/usergist/feedback/UserGistNotificationDismissReceiver.kt +17 -0
- package/android/src/main/java/studio/usergist/feedback/UserGistPushModule.kt +46 -5
- package/android/src/main/java/studio/usergist/feedback/UserGistPushState.kt +69 -0
- package/app.plugin.js +2 -0
- package/dist/NativeUserGistPush.d.ts +10 -0
- package/dist/NativeUserGistPush.d.ts.map +1 -1
- package/dist/NativeUserGistPush.js.map +1 -1
- package/dist/Provider.d.ts.map +1 -1
- package/dist/Provider.js +28 -17
- package/dist/Provider.js.map +1 -1
- package/dist/Push.d.ts +2 -0
- package/dist/Push.d.ts.map +1 -1
- package/dist/Push.js +50 -2
- package/dist/Push.js.map +1 -1
- package/dist/UserGist.d.ts +9 -4
- package/dist/UserGist.d.ts.map +1 -1
- package/dist/UserGist.js +492 -130
- package/dist/UserGist.js.map +1 -1
- package/dist/expo/index.d.ts +68 -0
- package/dist/expo/index.d.ts.map +1 -0
- package/dist/expo/index.js +120 -0
- package/dist/expo/index.js.map +1 -0
- package/dist/internal/consent.d.ts.map +1 -1
- package/dist/internal/consent.js +4 -1
- package/dist/internal/consent.js.map +1 -1
- package/dist/internal/context.d.ts +1 -1
- package/dist/internal/context.d.ts.map +1 -1
- package/dist/internal/context.js +1 -1
- package/dist/internal/context.js.map +1 -1
- package/dist/internal/engine.d.ts +8 -2
- package/dist/internal/engine.d.ts.map +1 -1
- package/dist/internal/engine.js +189 -39
- package/dist/internal/engine.js.map +1 -1
- package/dist/internal/inapp-rules-cache.d.ts +1 -0
- package/dist/internal/inapp-rules-cache.d.ts.map +1 -1
- package/dist/internal/inapp-rules-cache.js +7 -0
- package/dist/internal/inapp-rules-cache.js.map +1 -1
- package/dist/internal/mutation-queue.d.ts +2 -2
- package/dist/internal/mutation-queue.d.ts.map +1 -1
- package/dist/internal/mutation-queue.js +32 -4
- package/dist/internal/mutation-queue.js.map +1 -1
- package/dist/internal/push-dedupe.d.ts +3 -0
- package/dist/internal/push-dedupe.d.ts.map +1 -0
- package/dist/internal/push-dedupe.js +33 -0
- package/dist/internal/push-dedupe.js.map +1 -0
- package/dist/internal/rules-cache.d.ts +1 -0
- package/dist/internal/rules-cache.d.ts.map +1 -1
- package/dist/internal/rules-cache.js +7 -0
- package/dist/internal/rules-cache.js.map +1 -1
- package/dist/internal/storage.d.ts +1 -0
- package/dist/internal/storage.d.ts.map +1 -1
- package/dist/internal/storage.js +1 -0
- package/dist/internal/storage.js.map +1 -1
- package/dist/internal/survey-invitations.d.ts +25 -0
- package/dist/internal/survey-invitations.d.ts.map +1 -0
- package/dist/internal/survey-invitations.js +54 -0
- package/dist/internal/survey-invitations.js.map +1 -0
- package/dist/internal/survey-matcher.d.ts +1 -0
- package/dist/internal/survey-matcher.d.ts.map +1 -1
- package/dist/internal/survey-matcher.js +3 -0
- package/dist/internal/survey-matcher.js.map +1 -1
- package/dist/internal/survey-rules-cache.d.ts +1 -0
- package/dist/internal/survey-rules-cache.d.ts.map +1 -1
- package/dist/internal/survey-rules-cache.js +7 -0
- package/dist/internal/survey-rules-cache.js.map +1 -1
- package/dist/internal/survey-store.d.ts +9 -2
- package/dist/internal/survey-store.d.ts.map +1 -1
- package/dist/internal/survey-store.js +37 -16
- package/dist/internal/survey-store.js.map +1 -1
- package/dist/internal/transport.d.ts +15 -3
- package/dist/internal/transport.d.ts.map +1 -1
- package/dist/internal/transport.js +16 -13
- package/dist/internal/transport.js.map +1 -1
- package/dist/internal/types.d.ts +2 -0
- package/dist/internal/types.d.ts.map +1 -1
- package/dist/native/events.d.ts +2 -0
- package/dist/native/events.d.ts.map +1 -1
- package/dist/native/events.js +2 -0
- package/dist/native/events.js.map +1 -1
- package/dist/native/push-adapter.d.ts +11 -0
- package/dist/native/push-adapter.d.ts.map +1 -0
- package/dist/native/push-adapter.js +10 -0
- package/dist/native/push-adapter.js.map +1 -0
- package/dist/native/push-bridge.d.ts +9 -0
- package/dist/native/push-bridge.d.ts.map +1 -1
- package/dist/native/push-bridge.js +39 -4
- package/dist/native/push-bridge.js.map +1 -1
- package/dist/ui/SurveyView.d.ts.map +1 -1
- package/dist/ui/SurveyView.js +5 -5
- package/dist/ui/SurveyView.js.map +1 -1
- package/dist/ui/requests/RequestsHost.d.ts.map +1 -1
- package/dist/ui/requests/RequestsHost.js +8 -0
- package/dist/ui/requests/RequestsHost.js.map +1 -1
- package/expo-module.config.json +8 -0
- package/expo-support/UserGistExpoAppDelegateSubscriber.swift +34 -0
- package/expo-support/UserGistFeedbackExpo.podspec +17 -0
- package/ios/Extension/UserGistNotificationService.swift +52 -5
- package/ios/Shared/UserGistNotificationActions.swift +27 -0
- package/ios/Shared/UserGistPushState.swift +110 -0
- package/ios/UserGistPush.h +1 -6
- package/ios/UserGistPush.mm +18 -0
- package/ios/UserGistPushImpl.swift +46 -4
- package/ios/UserGistPushInterop.h +5 -0
- package/package.json +33 -7
- package/plugin/index.cjs +269 -0
- package/plugin/index.d.cts +14 -0
- package/plugin/templates/UserGistExpoMessagingService.kt +20 -0
package/dist/UserGist.js
CHANGED
|
@@ -1,28 +1,28 @@
|
|
|
1
|
-
|
|
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
|
-
|
|
50
|
-
|
|
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 ??
|
|
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({
|
|
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({
|
|
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
|
|
265
|
+
/** Defer campaign UI without interrupting analytics or an active modal. */
|
|
197
266
|
pausePresentation() { engine?.presentation.setPaused(true); },
|
|
198
|
-
/** Call after
|
|
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'
|
|
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()
|
|
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
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
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
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
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 ??
|
|
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: '
|
|
646
|
-
|
|
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
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
if (e) {
|
|
657
|
-
e.lastPushToken = null;
|
|
658
|
-
e.lastPushRegistrationKey = null;
|
|
659
|
-
e.lastPushRegistrationAt = 0;
|
|
660
|
-
void UserGist.setConsent({ push: false });
|
|
661
|
-
}
|
|
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._-]+)/) ??
|
|
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', {
|
|
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
|
-
|
|
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
|
-
|
|
875
|
-
|
|
876
|
-
|
|
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
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
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', {
|
|
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' ||
|
|
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
|
-
|
|
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) {
|