@usergist/feedback-react-native 0.2.0-beta.0 → 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 +6 -0
- package/README.md +60 -0
- package/dist/Provider.d.ts.map +1 -1
- package/dist/Provider.js +51 -23
- 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 +41 -2
- package/dist/Push.js.map +1 -1
- package/dist/UserGist.d.ts +11 -2
- package/dist/UserGist.d.ts.map +1 -1
- package/dist/UserGist.js +301 -65
- package/dist/UserGist.js.map +1 -1
- package/dist/internal/context.d.ts +1 -1
- package/dist/internal/context.js +1 -1
- package/dist/internal/engine.d.ts +10 -2
- package/dist/internal/engine.d.ts.map +1 -1
- package/dist/internal/engine.js +195 -44
- package/dist/internal/engine.js.map +1 -1
- package/dist/internal/events.d.ts +6 -1
- package/dist/internal/events.d.ts.map +1 -1
- package/dist/internal/events.js +39 -18
- package/dist/internal/events.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/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/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 +10 -2
- package/dist/internal/transport.d.ts.map +1 -1
- package/dist/internal/transport.js +15 -12
- 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/ui/SurveyView.d.ts.map +1 -1
- package/dist/ui/SurveyView.js +5 -5
- package/dist/ui/SurveyView.js.map +1 -1
- package/package.json +2 -2
package/dist/UserGist.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { userPropertiesUpdateSchema } from '@usergist/sdk-core/mobile';
|
|
1
2
|
import { hydratePushDedupe } from './internal/push-dedupe.js';
|
|
2
3
|
import { REQUEST_COMMENTED_EVENT_NAME, REQUEST_COMMENT_EDITED_EVENT_NAME, REQUEST_COMMENT_DELETED_EVENT_NAME, } from '@usergist/sdk-core/mobile';
|
|
3
4
|
import { asEventProps, clearAllState, createEngine, emitAppOpenWhenConsentReady, emitAppVersionChanged, ensureHydrated, enqueueAndEvaluate, flushNow, flushMutations, pollInstructions, refreshTargetingRules, recordServerBackedEventLocally, submitResponse, } from './internal/engine.js';
|
|
@@ -7,7 +8,7 @@ import { createSurveyStore } from './internal/survey-store.js';
|
|
|
7
8
|
import { UserGistPushNative, onTokenReceived, onNotificationReceived, onNotificationOpened, onNotificationDisplayed, onNotificationDismissed, onTokenError, } from './native/push-bridge.js';
|
|
8
9
|
import { Push } from './Push.js';
|
|
9
10
|
import { AppState, Platform } from 'react-native';
|
|
10
|
-
import { generateEventId, validateIdentifyTransition } from './internal/identity.js';
|
|
11
|
+
import { generateEventId, validateIdentifyTransition, } from './internal/identity.js';
|
|
11
12
|
import { configureStorageAdapter, STORAGE_KEYS, } from './internal/storage.js';
|
|
12
13
|
import { surveyCompletionOutcome } from './internal/survey-completion.js';
|
|
13
14
|
import { USERGIST_SDK_VERSION } from './internal/context.js';
|
|
@@ -21,6 +22,7 @@ function serializePushRegistration(operation) {
|
|
|
21
22
|
let runtimeStarted = false;
|
|
22
23
|
let runtimeStartPromise = null;
|
|
23
24
|
let resetPromise = null;
|
|
25
|
+
const surveyStarts = new Map();
|
|
24
26
|
let surveyStore = null;
|
|
25
27
|
let surveyHandlers = {};
|
|
26
28
|
let inAppHandlers = {};
|
|
@@ -39,8 +41,10 @@ function requestSessionIsCurrent(e) {
|
|
|
39
41
|
const generation = e.resetGeneration;
|
|
40
42
|
const consentVersion = e.consent.get().version;
|
|
41
43
|
const identity = e.identity.get();
|
|
42
|
-
return () => !e.resetting &&
|
|
43
|
-
|
|
44
|
+
return () => !e.resetting &&
|
|
45
|
+
generation === e.resetGeneration &&
|
|
46
|
+
consentVersion === e.consent.get().version &&
|
|
47
|
+
e.consent.allowsFeedback() &&
|
|
44
48
|
identity.anonymousId === e.identity.get().anonymousId &&
|
|
45
49
|
identity.externalId === e.identity.get().externalId;
|
|
46
50
|
}
|
|
@@ -49,9 +53,16 @@ async function syncNativePushState(e, push = e.consent.allowsPush()) {
|
|
|
49
53
|
const generation = e.resetGeneration;
|
|
50
54
|
const version = e.consent.get().version;
|
|
51
55
|
const operation = nativeStateQueue.then(async () => {
|
|
52
|
-
if (generation !== e.resetGeneration ||
|
|
56
|
+
if (generation !== e.resetGeneration ||
|
|
57
|
+
anonymousId !== e.identity.get().anonymousId ||
|
|
58
|
+
version !== e.consent.get().version)
|
|
53
59
|
return;
|
|
54
|
-
await UserGistPushNative.syncState({
|
|
60
|
+
await UserGistPushNative.syncState({
|
|
61
|
+
writeKey: e.config.writeKey,
|
|
62
|
+
apiUrl: e.config.apiUrl,
|
|
63
|
+
anonymousId,
|
|
64
|
+
push: push && !e.resetting,
|
|
65
|
+
});
|
|
55
66
|
});
|
|
56
67
|
nativeStateQueue = operation.catch(() => undefined);
|
|
57
68
|
await operation;
|
|
@@ -75,8 +86,10 @@ function requireEngine() {
|
|
|
75
86
|
function ensureEngine(config) {
|
|
76
87
|
if (engine)
|
|
77
88
|
return engine;
|
|
78
|
-
|
|
79
|
-
|
|
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());
|
|
80
93
|
return engine;
|
|
81
94
|
}
|
|
82
95
|
async function startRuntime(e) {
|
|
@@ -145,7 +158,9 @@ function attachEnginePushListeners() {
|
|
|
145
158
|
try {
|
|
146
159
|
const platform = (p.platform === 'ios' ? 'ios' : 'android');
|
|
147
160
|
await UserGist.registerPushToken(p.token, platform, {
|
|
148
|
-
environment: lastEnablePushOptions?.environment ??
|
|
161
|
+
environment: lastEnablePushOptions?.environment ??
|
|
162
|
+
(await UserGistPushNative.defaultEnvironment()) ??
|
|
163
|
+
defaultEnvironment(),
|
|
149
164
|
});
|
|
150
165
|
}
|
|
151
166
|
catch (err) {
|
|
@@ -159,8 +174,8 @@ function attachEnginePushListeners() {
|
|
|
159
174
|
onNotificationReceived((p) => {
|
|
160
175
|
forwardToPushHandler(p, 'received');
|
|
161
176
|
});
|
|
162
|
-
onNotificationDisplayed(p => forwardToPushHandler(p, 'displayed'));
|
|
163
|
-
onNotificationDismissed(p => forwardToPushHandler(p, 'dismissed'));
|
|
177
|
+
onNotificationDisplayed((p) => forwardToPushHandler(p, 'displayed'));
|
|
178
|
+
onNotificationDismissed((p) => forwardToPushHandler(p, 'dismissed'));
|
|
164
179
|
onNotificationOpened((p) => {
|
|
165
180
|
forwardToPushHandler(p, 'opened');
|
|
166
181
|
});
|
|
@@ -169,7 +184,8 @@ function forwardToPushHandler(p, kind) {
|
|
|
169
184
|
if (!engine?.consent.allowsPush() || engine.resetting)
|
|
170
185
|
return;
|
|
171
186
|
const data = p.data ?? {};
|
|
172
|
-
if (data.usergist_anonymous_id &&
|
|
187
|
+
if (data.usergist_anonymous_id &&
|
|
188
|
+
data.usergist_anonymous_id !== engine.identity.get().anonymousId)
|
|
173
189
|
return;
|
|
174
190
|
if (p.deliveryId) {
|
|
175
191
|
;
|
|
@@ -186,7 +202,10 @@ function forwardToPushHandler(p, kind) {
|
|
|
186
202
|
};
|
|
187
203
|
Object.assign(userInfo, data);
|
|
188
204
|
if (kind === 'received') {
|
|
189
|
-
Push.handleReceived({
|
|
205
|
+
Push.handleReceived({
|
|
206
|
+
userInfo,
|
|
207
|
+
notification: { title: p.title, body: p.body },
|
|
208
|
+
});
|
|
190
209
|
}
|
|
191
210
|
else if (kind === 'displayed') {
|
|
192
211
|
Push.handleDisplayed({ userInfo });
|
|
@@ -204,7 +223,10 @@ function forwardToPushHandler(p, kind) {
|
|
|
204
223
|
dataStr[k] = String(v);
|
|
205
224
|
}
|
|
206
225
|
if (kind === 'received') {
|
|
207
|
-
Push.handleReceived({
|
|
226
|
+
Push.handleReceived({
|
|
227
|
+
data: dataStr,
|
|
228
|
+
notification: { title: p.title, body: p.body },
|
|
229
|
+
});
|
|
208
230
|
}
|
|
209
231
|
else if (kind === 'displayed') {
|
|
210
232
|
Push.handleDisplayed({ data: dataStr });
|
|
@@ -240,6 +262,11 @@ export const UserGist = {
|
|
|
240
262
|
init(config) {
|
|
241
263
|
void UserGist.initAsync(config);
|
|
242
264
|
},
|
|
265
|
+
/** Defer campaign UI without interrupting analytics or an active modal. */
|
|
266
|
+
pausePresentation() { engine?.presentation.setPaused(true); },
|
|
267
|
+
/** Call after the host's loaded screen and navigation are ready. */
|
|
268
|
+
resumePresentation() { engine?.presentation.setPaused(false); },
|
|
269
|
+
__internal_presentationGate() { return requireEngine().presentation; },
|
|
243
270
|
async initAsync(config, initialIdentity) {
|
|
244
271
|
try {
|
|
245
272
|
const e = ensureEngine(config);
|
|
@@ -274,12 +301,16 @@ export const UserGist = {
|
|
|
274
301
|
return 'rejected';
|
|
275
302
|
const identity = e.identity.get();
|
|
276
303
|
const pendingIdentity = e.mutations.peek();
|
|
277
|
-
const transition = validateIdentifyTransition(identity.externalId, pendingIdentity?.kind === 'identify'
|
|
304
|
+
const transition = validateIdentifyTransition(identity.externalId, pendingIdentity?.kind === 'identify'
|
|
305
|
+
? pendingIdentity.payload.externalId
|
|
306
|
+
: null, userId);
|
|
278
307
|
if (transition === 'reset_required') {
|
|
279
308
|
reportError('identify rejected: call reset() before switching users');
|
|
280
309
|
return 'rejected';
|
|
281
310
|
}
|
|
282
|
-
const cleanProps = e.consent.allowsAnalytics()
|
|
311
|
+
const cleanProps = e.consent.allowsAnalytics()
|
|
312
|
+
? asEventProps(properties)
|
|
313
|
+
: undefined;
|
|
283
314
|
if (transition === 'already_identified' &&
|
|
284
315
|
(!cleanProps || Object.keys(cleanProps).length === 0)) {
|
|
285
316
|
return 'synced';
|
|
@@ -312,6 +343,33 @@ export const UserGist = {
|
|
|
312
343
|
return 'rejected';
|
|
313
344
|
}
|
|
314
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
|
+
},
|
|
315
373
|
track(eventName, properties) {
|
|
316
374
|
try {
|
|
317
375
|
if (typeof eventName !== 'string' || eventName.length === 0) {
|
|
@@ -329,6 +387,10 @@ export const UserGist = {
|
|
|
329
387
|
async setConsent(purposes) {
|
|
330
388
|
try {
|
|
331
389
|
const e = requireEngine();
|
|
390
|
+
if (purposes.feedback === false)
|
|
391
|
+
e.presentation.invalidate('feedback');
|
|
392
|
+
if (purposes.survey === false)
|
|
393
|
+
e.presentation.invalidate('survey');
|
|
332
394
|
await ensureHydrated(e);
|
|
333
395
|
if (e.resetting)
|
|
334
396
|
return false;
|
|
@@ -392,6 +454,7 @@ export const UserGist = {
|
|
|
392
454
|
return resetPromise ?? Promise.resolve();
|
|
393
455
|
e.resetting = true;
|
|
394
456
|
e.resetGeneration += 1;
|
|
457
|
+
e.presentation.invalidate();
|
|
395
458
|
requestsCache = null;
|
|
396
459
|
lastEnablePushOptions = null;
|
|
397
460
|
e.events.emit('resetSurfaces', undefined);
|
|
@@ -403,6 +466,7 @@ export const UserGist = {
|
|
|
403
466
|
await UserGist.invalidatePushToken(e.lastPushToken);
|
|
404
467
|
await UserGistPushNative.disablePush().catch(() => undefined);
|
|
405
468
|
await clearAllState(e);
|
|
469
|
+
await surveyStore?.clear();
|
|
406
470
|
await hydratePushDedupe(e.config.writeKey, e.identity.get().anonymousId);
|
|
407
471
|
})();
|
|
408
472
|
const finalized = operation
|
|
@@ -526,10 +590,13 @@ export const UserGist = {
|
|
|
526
590
|
return;
|
|
527
591
|
const generation = e.resetGeneration;
|
|
528
592
|
await serializePushRegistration(async () => {
|
|
529
|
-
if (e.resetting ||
|
|
593
|
+
if (e.resetting ||
|
|
594
|
+
generation !== e.resetGeneration ||
|
|
595
|
+
!e.consent.allowsPush())
|
|
530
596
|
return;
|
|
531
597
|
const id = e.identity.get();
|
|
532
|
-
const environment = opts?.environment ??
|
|
598
|
+
const environment = opts?.environment ??
|
|
599
|
+
(await UserGistPushNative.defaultEnvironment()) ??
|
|
533
600
|
(e.config.environment === 'production' ? 'production' : 'sandbox');
|
|
534
601
|
const registrationKey = [
|
|
535
602
|
id.anonymousId,
|
|
@@ -541,7 +608,9 @@ export const UserGist = {
|
|
|
541
608
|
if (e.lastPushRegistrationKey === registrationKey &&
|
|
542
609
|
Date.now() - e.lastPushRegistrationAt < 24 * 60 * 60_000)
|
|
543
610
|
return;
|
|
544
|
-
if (e.resetting ||
|
|
611
|
+
if (e.resetting ||
|
|
612
|
+
generation !== e.resetGeneration ||
|
|
613
|
+
!e.consent.allowsPush())
|
|
545
614
|
return;
|
|
546
615
|
// Persist before the request so logout can invalidate a token even when
|
|
547
616
|
// the response is lost or the process exits after the server accepts it.
|
|
@@ -603,7 +672,10 @@ export const UserGist = {
|
|
|
603
672
|
return;
|
|
604
673
|
const id = e.identity.get();
|
|
605
674
|
const token = e.lastPushToken;
|
|
606
|
-
if (!token ||
|
|
675
|
+
if (!token ||
|
|
676
|
+
e.resetting ||
|
|
677
|
+
!e.consent.allowsPush() ||
|
|
678
|
+
id.externalId !== externalId)
|
|
607
679
|
return;
|
|
608
680
|
await e.transport.pushRebind({
|
|
609
681
|
anonymousId: id.anonymousId,
|
|
@@ -703,12 +775,30 @@ export const UserGist = {
|
|
|
703
775
|
const e = requireEngine();
|
|
704
776
|
await ensureHydrated(e);
|
|
705
777
|
if (!e.consent.allowsPush() || e.resetting)
|
|
706
|
-
return {
|
|
778
|
+
return {
|
|
779
|
+
granted: false,
|
|
780
|
+
status: 'not_determined',
|
|
781
|
+
platform: Platform.OS === 'ios' ? 'ios' : 'android',
|
|
782
|
+
error: 'consent_required',
|
|
783
|
+
};
|
|
707
784
|
const generation = e.resetGeneration;
|
|
708
785
|
const consentVersion = e.consent.get().version;
|
|
709
|
-
const current = () => !e.resetting &&
|
|
710
|
-
|
|
711
|
-
|
|
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
|
+
};
|
|
712
802
|
await syncNativePushState(e);
|
|
713
803
|
if (!current())
|
|
714
804
|
return cancelled();
|
|
@@ -731,7 +821,7 @@ export const UserGist = {
|
|
|
731
821
|
if (result.granted && result.token) {
|
|
732
822
|
const platform = (result.platform === 'ios' ? 'ios' : 'android');
|
|
733
823
|
await UserGist.registerPushToken(result.token, platform, {
|
|
734
|
-
environment: opts?.environment ??
|
|
824
|
+
environment: opts?.environment ?? defaultEnvironment(),
|
|
735
825
|
});
|
|
736
826
|
}
|
|
737
827
|
return result;
|
|
@@ -811,10 +901,20 @@ export const UserGist = {
|
|
|
811
901
|
debugLog('openSurvey: survey consent not granted; ignoring');
|
|
812
902
|
return;
|
|
813
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;
|
|
814
913
|
e.events.emit('showSurvey', {
|
|
815
914
|
surveyId,
|
|
816
|
-
source: context?.source ?? 'on_demand',
|
|
915
|
+
source: context?.source ?? invitation?.source ?? 'on_demand',
|
|
817
916
|
...(context?.language ? { language: context.language } : {}),
|
|
917
|
+
...(invitation ? { survey: invitation.survey, attempt: invitation.attempt } : {}),
|
|
818
918
|
});
|
|
819
919
|
}
|
|
820
920
|
catch (err) {
|
|
@@ -832,21 +932,28 @@ export const UserGist = {
|
|
|
832
932
|
const e = requireEngine();
|
|
833
933
|
await ensureHydrated(e);
|
|
834
934
|
// Accept URLs of the shape `*://*/s/<token>` or `*survey=<token>`.
|
|
835
|
-
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._-]+)/);
|
|
836
937
|
if (!match || !match[1])
|
|
837
938
|
return false;
|
|
838
939
|
const token = match[1];
|
|
940
|
+
const presentationIsValid = e.presentation.validator('survey');
|
|
839
941
|
const id = e.identity.get();
|
|
840
942
|
const resolved = await e.transport.surveyResolveLink({
|
|
841
943
|
token,
|
|
842
944
|
anonymousId: id.anonymousId,
|
|
843
945
|
externalId: id.externalId ?? null,
|
|
844
946
|
});
|
|
947
|
+
if (!presentationIsValid() || !e.consent.allowsSurvey())
|
|
948
|
+
return false;
|
|
845
949
|
if (resolved.consentRequired) {
|
|
846
950
|
debugLog('survey link resolved but consent required — awaiting consent');
|
|
847
951
|
return false;
|
|
848
952
|
}
|
|
849
|
-
e.events.emit('showSurvey', {
|
|
953
|
+
e.events.emit('showSurvey', {
|
|
954
|
+
surveyId: resolved.surveyId,
|
|
955
|
+
source: 'link',
|
|
956
|
+
});
|
|
850
957
|
return true;
|
|
851
958
|
}
|
|
852
959
|
catch (err) {
|
|
@@ -899,6 +1006,9 @@ export const UserGist = {
|
|
|
899
1006
|
reportError('reportSurveyShown failed', err);
|
|
900
1007
|
}
|
|
901
1008
|
},
|
|
1009
|
+
__internal_surveyOpenFailed(surveyId) {
|
|
1010
|
+
engine?.surveyMatcher.cancelPending(surveyId);
|
|
1011
|
+
},
|
|
902
1012
|
__internal_armedSurveyById(surveyId) {
|
|
903
1013
|
// Local-fire fast-path. If the survey-rules-cache already has the
|
|
904
1014
|
// full survey content (because the matcher just fired), the
|
|
@@ -919,6 +1029,11 @@ export const UserGist = {
|
|
|
919
1029
|
const e = requireEngine();
|
|
920
1030
|
await ensureHydrated(e);
|
|
921
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;
|
|
922
1037
|
return await e.transport.surveyGet({
|
|
923
1038
|
surveyId,
|
|
924
1039
|
anonymousId: id.anonymousId,
|
|
@@ -931,52 +1046,169 @@ export const UserGist = {
|
|
|
931
1046
|
return null;
|
|
932
1047
|
}
|
|
933
1048
|
},
|
|
934
|
-
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);
|
|
935
1185
|
try {
|
|
936
|
-
|
|
937
|
-
await ensureHydrated(e);
|
|
938
|
-
// Give a previously deferred completion a chance to reach the server
|
|
939
|
-
// before asking it to create or resume another attempt.
|
|
940
|
-
await flushMutations(e);
|
|
941
|
-
const id = e.identity.get();
|
|
942
|
-
const res = await e.transport.surveyCreateAttempt(surveyId, {
|
|
943
|
-
anonymousId: id.anonymousId,
|
|
944
|
-
externalId: id.externalId ?? null,
|
|
945
|
-
source,
|
|
946
|
-
...(language ? { language } : {}),
|
|
947
|
-
sdkVersion: `rn-${USERGIST_SDK_VERSION}`,
|
|
948
|
-
});
|
|
949
|
-
await surveyStore?.upsert({
|
|
950
|
-
surveyId,
|
|
951
|
-
attemptId: res.attemptId,
|
|
952
|
-
startedAt: Date.now(),
|
|
953
|
-
currentQuestionId: res.currentQuestionId,
|
|
954
|
-
snapshot: (res.progressSnapshot ?? {}),
|
|
955
|
-
language: language ?? null,
|
|
956
|
-
});
|
|
957
|
-
return {
|
|
958
|
-
attemptId: res.attemptId,
|
|
959
|
-
startQuestionId: res.startQuestionId,
|
|
960
|
-
currentQuestionId: res.currentQuestionId,
|
|
961
|
-
snapshot: (res.progressSnapshot ?? {}),
|
|
962
|
-
resumed: res.resumed,
|
|
963
|
-
};
|
|
1186
|
+
return await operation;
|
|
964
1187
|
}
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
1188
|
+
finally {
|
|
1189
|
+
if (surveyStarts.get(key) === operation)
|
|
1190
|
+
surveyStarts.delete(key);
|
|
968
1191
|
}
|
|
969
1192
|
},
|
|
970
1193
|
async __internal_saveProgress(attemptId, currentQuestionId, snapshot) {
|
|
971
1194
|
// Persist the resume point before attempting the network. Progress is
|
|
972
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;
|
|
973
1203
|
await surveyStore?.updateProgress(attemptId, currentQuestionId, snapshot);
|
|
974
1204
|
try {
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
1205
|
+
if (e.resetting || e.resetGeneration !== generation)
|
|
1206
|
+
return;
|
|
1207
|
+
await e.mutations.enqueue('survey-progress', 'survey', {
|
|
1208
|
+
attemptId,
|
|
1209
|
+
body: { currentQuestionId, progressSnapshot: snapshot },
|
|
979
1210
|
});
|
|
1211
|
+
void flushMutations(e);
|
|
980
1212
|
}
|
|
981
1213
|
catch (err) {
|
|
982
1214
|
// Completion can win a race with an in-flight progress PATCH, making a
|
|
@@ -1016,7 +1248,9 @@ export const UserGist = {
|
|
|
1016
1248
|
throw new Error('Survey submission was rejected by the server');
|
|
1017
1249
|
}
|
|
1018
1250
|
if (outcome === 'deferred') {
|
|
1019
|
-
debugLog('survey completion saved locally; delivery deferred', {
|
|
1251
|
+
debugLog('survey completion saved locally; delivery deferred', {
|
|
1252
|
+
attemptId,
|
|
1253
|
+
});
|
|
1020
1254
|
}
|
|
1021
1255
|
// Once the completion is durably queued, the attempt must not reopen as
|
|
1022
1256
|
// resumable work. The mutation queue owns delivery from this point.
|
|
@@ -1096,7 +1330,9 @@ export const UserGist = {
|
|
|
1096
1330
|
submitRequest(title, description, callback) {
|
|
1097
1331
|
try {
|
|
1098
1332
|
const e = requireEngine();
|
|
1099
|
-
if (typeof title !== 'string' ||
|
|
1333
|
+
if (typeof title !== 'string' ||
|
|
1334
|
+
title.length === 0 ||
|
|
1335
|
+
title.length > 120) {
|
|
1100
1336
|
callback?.(new Error('title required, max 120 chars'));
|
|
1101
1337
|
return;
|
|
1102
1338
|
}
|