@usergist/feedback-react-native 0.1.1 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +19 -0
- package/README.md +14 -1
- package/dist/Provider.d.ts.map +1 -1
- package/dist/Provider.js +29 -12
- package/dist/Provider.js.map +1 -1
- package/dist/UserGist.d.ts +14 -0
- package/dist/UserGist.d.ts.map +1 -1
- package/dist/UserGist.js +320 -104
- package/dist/UserGist.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/internal/context.d.ts +1 -1
- package/dist/internal/context.js +1 -1
- package/dist/internal/engine.d.ts +8 -0
- package/dist/internal/engine.d.ts.map +1 -1
- package/dist/internal/engine.js +116 -39
- package/dist/internal/engine.js.map +1 -1
- package/dist/internal/events.d.ts +7 -1
- package/dist/internal/events.d.ts.map +1 -1
- package/dist/internal/events.js +43 -16
- package/dist/internal/events.js.map +1 -1
- package/dist/internal/identity-lifecycle.d.ts +16 -0
- package/dist/internal/identity-lifecycle.d.ts.map +1 -0
- package/dist/internal/identity-lifecycle.js +96 -0
- package/dist/internal/identity-lifecycle.js.map +1 -0
- 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 +19 -2
- package/dist/internal/mutation-queue.js.map +1 -1
- package/dist/internal/push-subscription.d.ts +11 -0
- package/dist/internal/push-subscription.d.ts.map +1 -0
- package/dist/internal/push-subscription.js +20 -0
- package/dist/internal/push-subscription.js.map +1 -0
- package/dist/internal/session-revocations.d.ts +5 -0
- package/dist/internal/session-revocations.d.ts.map +1 -0
- package/dist/internal/session-revocations.js +58 -0
- package/dist/internal/session-revocations.js.map +1 -0
- package/dist/internal/storage.d.ts +3 -0
- package/dist/internal/storage.d.ts.map +1 -1
- package/dist/internal/storage.js +76 -60
- package/dist/internal/storage.js.map +1 -1
- package/dist/internal/transport.d.ts +14 -2
- package/dist/internal/transport.d.ts.map +1 -1
- package/dist/internal/transport.js +31 -14
- package/dist/internal/transport.js.map +1 -1
- package/dist/internal/user-state.d.ts +2 -1
- package/dist/internal/user-state.d.ts.map +1 -1
- package/dist/internal/user-state.js +3 -2
- package/dist/internal/user-state.js.map +1 -1
- package/package.json +2 -2
package/dist/UserGist.js
CHANGED
|
@@ -1,12 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
// bind a server-proven identity before any lifecycle event is emitted.
|
|
6
|
-
// - All public methods catch their own errors and report via debug. The
|
|
7
|
-
// SDK never throws across its boundary.
|
|
8
|
-
// - Track is synchronous for the caller: it enqueues, schedules a flush,
|
|
9
|
-
// and runs the trigger matcher — zero awaits.
|
|
1
|
+
import { drainRevocations, hasPendingRevocations } from './internal/session-revocations.js';
|
|
2
|
+
import { setPushSubscriptionStateHandler, pushSubscriptionChanged } from './internal/push-subscription.js';
|
|
3
|
+
import { cancelIdentityRecovery, identityChanged, setIdentityStateHandler, setSubjectTokenProvider, getIdentityState } from './internal/identity-lifecycle.js';
|
|
4
|
+
import { userPropertiesUpdateSchema } from '@usergist/sdk-core/mobile';
|
|
10
5
|
import { REQUEST_COMMENTED_EVENT_NAME, REQUEST_COMMENT_EDITED_EVENT_NAME, REQUEST_COMMENT_DELETED_EVENT_NAME, } from '@usergist/sdk-core/mobile';
|
|
11
6
|
import { asEventProps, clearAllState, createEngine, emitAppOpenWhenConsentReady, emitAppVersionChanged, ensureHydrated, enqueueAndEvaluate, flushNow, flushMutations, pollInstructions, refreshTargetingRules, recordServerBackedEventLocally, submitResponse, } from './internal/engine.js';
|
|
12
7
|
import { setDebugEnabled, reportError, debugLog, logTrace, getTraces, setDiagnosticHandler, } from './internal/debug.js';
|
|
@@ -16,10 +11,16 @@ import { UserGistPushNative, onTokenReceived, onNotificationReceived, onNotifica
|
|
|
16
11
|
import { Push } from './Push.js';
|
|
17
12
|
import { AppState, Platform } from 'react-native';
|
|
18
13
|
import { generateEventId, validateIdentifyTransition } from './internal/identity.js';
|
|
19
|
-
import { configureStorageAdapter, } from './internal/storage.js';
|
|
14
|
+
import { STORAGE_KEYS, configureStorageAdapter, } from './internal/storage.js';
|
|
20
15
|
import { surveyCompletionOutcome } from './internal/survey-completion.js';
|
|
21
16
|
import { USERGIST_SDK_VERSION } from './internal/context.js';
|
|
22
17
|
let engine = null;
|
|
18
|
+
let pushRegistrationQueue = Promise.resolve();
|
|
19
|
+
function serializePushRegistration(operation) {
|
|
20
|
+
const task = pushRegistrationQueue.then(operation);
|
|
21
|
+
pushRegistrationQueue = task.catch(() => undefined);
|
|
22
|
+
return task;
|
|
23
|
+
}
|
|
23
24
|
let runtimeStarted = false;
|
|
24
25
|
let runtimeStartPromise = null;
|
|
25
26
|
let resetPromise = null;
|
|
@@ -37,6 +38,17 @@ function ensureRequestsCache() {
|
|
|
37
38
|
}
|
|
38
39
|
return requestsCache;
|
|
39
40
|
}
|
|
41
|
+
function requestSessionIsCurrent(e) {
|
|
42
|
+
const generation = e.resetGeneration;
|
|
43
|
+
const consentVersion = e.consent.get().version;
|
|
44
|
+
const identity = e.identity.get();
|
|
45
|
+
return () => !e.resetting &&
|
|
46
|
+
generation === e.resetGeneration &&
|
|
47
|
+
consentVersion === e.consent.get().version &&
|
|
48
|
+
e.consent.allowsFeedback() &&
|
|
49
|
+
identity.anonymousId === e.identity.get().anonymousId &&
|
|
50
|
+
identity.externalId === e.identity.get().externalId;
|
|
51
|
+
}
|
|
40
52
|
function requireEngine() {
|
|
41
53
|
if (!engine) {
|
|
42
54
|
throw new Error('UserGist.init must be called before using the SDK');
|
|
@@ -47,6 +59,20 @@ function ensureEngine(config) {
|
|
|
47
59
|
if (engine)
|
|
48
60
|
return engine;
|
|
49
61
|
engine = createEngine(config);
|
|
62
|
+
const pushEngine = engine;
|
|
63
|
+
pushEngine.retryPushRegistration = async () => {
|
|
64
|
+
if (pushEngine.resetting)
|
|
65
|
+
return;
|
|
66
|
+
const saved = await pushEngine.storage.getJson(STORAGE_KEYS.pushDevice);
|
|
67
|
+
if (!saved)
|
|
68
|
+
return;
|
|
69
|
+
pushEngine.pushTokenAvailable = true;
|
|
70
|
+
if (pushEngine !== engine || pushEngine.resetting)
|
|
71
|
+
return;
|
|
72
|
+
pushSubscriptionChanged(pushEngine);
|
|
73
|
+
if (pushEngine.consent.allowsPush())
|
|
74
|
+
await registerPushDevice(saved.token, saved.platform, { environment: saved.environment }, true);
|
|
75
|
+
};
|
|
50
76
|
surveyStore = createSurveyStore(config.writeKey);
|
|
51
77
|
return engine;
|
|
52
78
|
}
|
|
@@ -135,6 +161,10 @@ function attachEnginePushListeners() {
|
|
|
135
161
|
});
|
|
136
162
|
}
|
|
137
163
|
function forwardToPushHandler(p, kind) {
|
|
164
|
+
if (!engine?.consent.allowsPush() || engine.resetting)
|
|
165
|
+
return;
|
|
166
|
+
if (p.data?.usergist_anonymous_id && p.data.usergist_anonymous_id !== engine.identity.get().anonymousId)
|
|
167
|
+
return;
|
|
138
168
|
const data = p.data ?? {};
|
|
139
169
|
if (p.deliveryId) {
|
|
140
170
|
;
|
|
@@ -193,6 +223,11 @@ export const UserGist = {
|
|
|
193
223
|
init(config) {
|
|
194
224
|
void UserGist.initAsync(config);
|
|
195
225
|
},
|
|
226
|
+
/** Defer campaign UI without stopping analytics or closing active UI. */
|
|
227
|
+
pausePresentation() { engine?.presentation.setPaused(true); },
|
|
228
|
+
/** Call after startup loading and navigation have completed. */
|
|
229
|
+
resumePresentation() { engine?.presentation.setPaused(false); },
|
|
230
|
+
__internal_presentationGate() { return requireEngine().presentation; },
|
|
196
231
|
async initAsync(config, initialIdentity) {
|
|
197
232
|
try {
|
|
198
233
|
const e = ensureEngine(config);
|
|
@@ -210,6 +245,10 @@ export const UserGist = {
|
|
|
210
245
|
return 'rejected';
|
|
211
246
|
}
|
|
212
247
|
},
|
|
248
|
+
setPushSubscriptionStateHandler,
|
|
249
|
+
setIdentityStateHandler,
|
|
250
|
+
setSubjectTokenProvider,
|
|
251
|
+
getIdentityState,
|
|
213
252
|
identify(userId, properties, subjectToken) {
|
|
214
253
|
void UserGist.identifyAsync(userId, properties, subjectToken);
|
|
215
254
|
},
|
|
@@ -220,7 +259,10 @@ export const UserGist = {
|
|
|
220
259
|
return 'rejected';
|
|
221
260
|
}
|
|
222
261
|
const e = requireEngine();
|
|
262
|
+
const generation = e.resetGeneration;
|
|
223
263
|
await ensureHydrated(e);
|
|
264
|
+
if (e.resetting || e.resetGeneration !== generation)
|
|
265
|
+
return 'rejected';
|
|
224
266
|
const identity = e.identity.get();
|
|
225
267
|
const pendingIdentity = e.mutations.peek();
|
|
226
268
|
const transition = validateIdentifyTransition(identity.externalId, pendingIdentity?.kind === 'identify' ? pendingIdentity.payload.externalId : null, userId);
|
|
@@ -228,8 +270,13 @@ export const UserGist = {
|
|
|
228
270
|
reportError('identify rejected: call reset() before switching users');
|
|
229
271
|
return 'rejected';
|
|
230
272
|
}
|
|
231
|
-
const cleanProps = e.consent.allowsAnalytics()
|
|
273
|
+
const cleanProps = e.consent.allowsAnalytics()
|
|
274
|
+
? (properties && Object.keys(properties).length
|
|
275
|
+
? userPropertiesUpdateSchema.parse({ mutationId: generateEventId(), set: properties }).set
|
|
276
|
+
: undefined)
|
|
277
|
+
: undefined;
|
|
232
278
|
if (transition === 'already_identified' &&
|
|
279
|
+
(!subjectToken || subjectToken === e.subjectToken) &&
|
|
233
280
|
(!cleanProps || Object.keys(cleanProps).length === 0)) {
|
|
234
281
|
return 'synced';
|
|
235
282
|
}
|
|
@@ -237,6 +284,7 @@ export const UserGist = {
|
|
|
237
284
|
reportError('identify requires a server-minted subject token');
|
|
238
285
|
return 'rejected';
|
|
239
286
|
}
|
|
287
|
+
identityChanged(e, 'identifying');
|
|
240
288
|
const mutationId = await e.mutations.enqueue('identify', 'essential', {
|
|
241
289
|
subjectToken,
|
|
242
290
|
anonymousId: identity.anonymousId,
|
|
@@ -244,7 +292,10 @@ export const UserGist = {
|
|
|
244
292
|
...(cleanProps ? { properties: cleanProps } : {}),
|
|
245
293
|
}, `identify:${userId}`);
|
|
246
294
|
const result = await flushMutations(e);
|
|
295
|
+
if (e.resetting || e.resetGeneration !== generation)
|
|
296
|
+
return 'rejected';
|
|
247
297
|
if (result.permanentlyRejectedIds.has(mutationId)) {
|
|
298
|
+
identityChanged(e, 'rejected');
|
|
248
299
|
reportError('identify was rejected by the server');
|
|
249
300
|
return 'rejected';
|
|
250
301
|
}
|
|
@@ -260,6 +311,33 @@ export const UserGist = {
|
|
|
260
311
|
return 'rejected';
|
|
261
312
|
}
|
|
262
313
|
},
|
|
314
|
+
/** Update the current user, including anonymous users. Durable and retried on reconnect. */
|
|
315
|
+
async setUserProperties(properties, unset = []) {
|
|
316
|
+
try {
|
|
317
|
+
const e = requireEngine();
|
|
318
|
+
const generation = e.resetGeneration;
|
|
319
|
+
await ensureHydrated(e);
|
|
320
|
+
if (e !== engine ||
|
|
321
|
+
e.resetting ||
|
|
322
|
+
e.resetGeneration !== generation ||
|
|
323
|
+
!e.consent.allowsAnalytics())
|
|
324
|
+
return 'rejected';
|
|
325
|
+
const update = userPropertiesUpdateSchema.parse({
|
|
326
|
+
mutationId: generateEventId(),
|
|
327
|
+
set: properties,
|
|
328
|
+
unset: [...unset],
|
|
329
|
+
});
|
|
330
|
+
const mutationId = await e.mutations.enqueue('user-properties', 'analytics', { ...update, anonymousId: e.identity.get().anonymousId });
|
|
331
|
+
const result = await flushMutations(e);
|
|
332
|
+
if (result.permanentlyRejectedIds.has(mutationId))
|
|
333
|
+
return 'rejected';
|
|
334
|
+
return e.mutations.has(mutationId) ? 'queued' : 'synced';
|
|
335
|
+
}
|
|
336
|
+
catch (error) {
|
|
337
|
+
reportError('setUserProperties failed', error);
|
|
338
|
+
return 'rejected';
|
|
339
|
+
}
|
|
340
|
+
},
|
|
263
341
|
track(eventName, properties) {
|
|
264
342
|
try {
|
|
265
343
|
if (typeof eventName !== 'string' || eventName.length === 0) {
|
|
@@ -277,8 +355,34 @@ export const UserGist = {
|
|
|
277
355
|
async setConsent(purposes) {
|
|
278
356
|
try {
|
|
279
357
|
const e = requireEngine();
|
|
358
|
+
if (purposes.feedback === false)
|
|
359
|
+
e.presentation.invalidate('feedback');
|
|
360
|
+
if (purposes.survey === false)
|
|
361
|
+
e.presentation.invalidate('survey');
|
|
280
362
|
await ensureHydrated(e);
|
|
363
|
+
if (e.resetting)
|
|
364
|
+
return false;
|
|
281
365
|
const next = await e.consent.set(purposes);
|
|
366
|
+
if (e.resetting || next.version !== e.consent.get().version)
|
|
367
|
+
return false;
|
|
368
|
+
if (!next.feedback) {
|
|
369
|
+
requestsCache = null;
|
|
370
|
+
e.events.emit('dismissRequests', undefined);
|
|
371
|
+
}
|
|
372
|
+
if (e.resetting || next.version !== e.consent.get().version)
|
|
373
|
+
return false;
|
|
374
|
+
if (purposes.push === false) {
|
|
375
|
+
lastEnablePushOptions = null;
|
|
376
|
+
await pushRegistrationQueue;
|
|
377
|
+
if (e.resetting || next.version !== e.consent.get().version)
|
|
378
|
+
return false;
|
|
379
|
+
if (e.lastPushToken)
|
|
380
|
+
await invalidatePushDevice(e.lastPushToken, true);
|
|
381
|
+
if (!e.consent.allowsPush())
|
|
382
|
+
await UserGistPushNative.disablePush();
|
|
383
|
+
}
|
|
384
|
+
if (e.resetting || next.version !== e.consent.get().version)
|
|
385
|
+
return false;
|
|
282
386
|
const id = e.identity.get();
|
|
283
387
|
try {
|
|
284
388
|
await e.transport.consent({
|
|
@@ -298,6 +402,9 @@ export const UserGist = {
|
|
|
298
402
|
reportError('transport.consent failed', err);
|
|
299
403
|
return false;
|
|
300
404
|
}
|
|
405
|
+
pushSubscriptionChanged(e);
|
|
406
|
+
if (next.push)
|
|
407
|
+
void e.retryPushRegistration?.().catch(error => reportError('push registration deferred', error));
|
|
301
408
|
if (next.feedback || next.survey) {
|
|
302
409
|
await refreshTargetingRules(e, true);
|
|
303
410
|
}
|
|
@@ -314,22 +421,27 @@ export const UserGist = {
|
|
|
314
421
|
reset() {
|
|
315
422
|
try {
|
|
316
423
|
const e = requireEngine();
|
|
317
|
-
if (
|
|
318
|
-
return resetPromise
|
|
424
|
+
if (resetPromise)
|
|
425
|
+
return resetPromise;
|
|
319
426
|
e.resetting = true;
|
|
427
|
+
identityChanged(e, 'resetting');
|
|
428
|
+
pushSubscriptionChanged(e);
|
|
429
|
+
lastEnablePushOptions = null;
|
|
320
430
|
e.resetGeneration += 1;
|
|
431
|
+
e.transport.cancelAll();
|
|
432
|
+
cancelIdentityRecovery(e);
|
|
433
|
+
e.presentation.invalidate();
|
|
321
434
|
e.events.emit('resetSurfaces', undefined);
|
|
322
435
|
const operation = (async () => {
|
|
323
436
|
await ensureHydrated(e);
|
|
324
|
-
|
|
325
|
-
await UserGist.invalidatePushToken(e.lastPushToken);
|
|
326
|
-
await UserGistPushNative.disablePush().catch(() => undefined);
|
|
437
|
+
void UserGistPushNative.disablePush().catch(() => undefined);
|
|
327
438
|
await clearAllState(e);
|
|
439
|
+
requestsCache = null;
|
|
328
440
|
})();
|
|
329
441
|
const finalized = operation
|
|
330
|
-
.
|
|
442
|
+
.then(() => { e.resetting = false; pushSubscriptionChanged(e); })
|
|
443
|
+
.catch((err) => { identityChanged(e, 'reset-failed'); reportError('reset failed', err); throw err; })
|
|
331
444
|
.finally(() => {
|
|
332
|
-
e.resetting = false;
|
|
333
445
|
if (resetPromise === finalized)
|
|
334
446
|
resetPromise = null;
|
|
335
447
|
});
|
|
@@ -438,67 +550,10 @@ export const UserGist = {
|
|
|
438
550
|
}
|
|
439
551
|
},
|
|
440
552
|
async registerPushToken(token, platform, opts) {
|
|
441
|
-
|
|
442
|
-
if (!token)
|
|
443
|
-
return;
|
|
444
|
-
const e = requireEngine();
|
|
445
|
-
await ensureHydrated(e);
|
|
446
|
-
if (!e.consent.allowsPush())
|
|
447
|
-
return;
|
|
448
|
-
// Consent is enforced server-side (the register-token route reads
|
|
449
|
-
// consent_log and refuses if push was explicitly declined). We no
|
|
450
|
-
// longer short-circuit here because the host app typically calls
|
|
451
|
-
// setConsent and registerPushToken in parallel; racing the local
|
|
452
|
-
// consent hydration is not our problem to solve.
|
|
453
|
-
const id = e.identity.get();
|
|
454
|
-
const environment = opts?.environment ??
|
|
455
|
-
(e.config.environment === 'production' ? 'production' : 'sandbox');
|
|
456
|
-
const registrationKey = [
|
|
457
|
-
id.anonymousId,
|
|
458
|
-
id.externalId ?? '',
|
|
459
|
-
platform,
|
|
460
|
-
environment,
|
|
461
|
-
token,
|
|
462
|
-
].join(':');
|
|
463
|
-
if (e.lastPushRegistrationKey === registrationKey &&
|
|
464
|
-
Date.now() - e.lastPushRegistrationAt < 24 * 60 * 60_000)
|
|
465
|
-
return;
|
|
466
|
-
await e.transport.pushRegisterToken({
|
|
467
|
-
anonymousId: id.anonymousId,
|
|
468
|
-
externalId: id.externalId ?? null,
|
|
469
|
-
token,
|
|
470
|
-
platform,
|
|
471
|
-
environment,
|
|
472
|
-
language: undefined,
|
|
473
|
-
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
|
474
|
-
appVersion: undefined,
|
|
475
|
-
sdkVersion: `rn-${USERGIST_SDK_VERSION}`,
|
|
476
|
-
optIn: true,
|
|
477
|
-
});
|
|
478
|
-
e.lastPushToken = token;
|
|
479
|
-
e.lastPushRegistrationKey = registrationKey;
|
|
480
|
-
e.lastPushRegistrationAt = Date.now();
|
|
481
|
-
debugLog('push token registered with server');
|
|
482
|
-
}
|
|
483
|
-
catch (err) {
|
|
484
|
-
reportError('registerPushToken failed', err);
|
|
485
|
-
}
|
|
553
|
+
await registerPushDevice(token, platform, opts);
|
|
486
554
|
},
|
|
487
555
|
async invalidatePushToken(token) {
|
|
488
|
-
|
|
489
|
-
if (!token)
|
|
490
|
-
return;
|
|
491
|
-
const e = requireEngine();
|
|
492
|
-
await ensureHydrated(e);
|
|
493
|
-
const id = e.identity.get();
|
|
494
|
-
await e.transport.pushInvalidateToken({
|
|
495
|
-
anonymousId: id.anonymousId,
|
|
496
|
-
token,
|
|
497
|
-
});
|
|
498
|
-
}
|
|
499
|
-
catch (err) {
|
|
500
|
-
reportError('invalidatePushToken failed', err);
|
|
501
|
-
}
|
|
556
|
+
await invalidatePushDevice(token);
|
|
502
557
|
},
|
|
503
558
|
/**
|
|
504
559
|
* Re-bind the latest registered device token to a newly-identified user.
|
|
@@ -608,6 +663,34 @@ export const UserGist = {
|
|
|
608
663
|
// react-native-onesignal, WonderPush, or any other push SDK.
|
|
609
664
|
async enablePush(opts) {
|
|
610
665
|
try {
|
|
666
|
+
const e = requireEngine();
|
|
667
|
+
await ensureHydrated(e);
|
|
668
|
+
if (!e.consent.allowsPush() || e.resetting)
|
|
669
|
+
return {
|
|
670
|
+
granted: false,
|
|
671
|
+
status: 'not_determined',
|
|
672
|
+
platform: Platform.OS === 'ios' ? 'ios' : 'android',
|
|
673
|
+
error: 'consent_required',
|
|
674
|
+
};
|
|
675
|
+
const generation = e.resetGeneration;
|
|
676
|
+
const consentVersion = e.consent.get().version;
|
|
677
|
+
const current = () => !e.resetting &&
|
|
678
|
+
generation === e.resetGeneration &&
|
|
679
|
+
consentVersion === e.consent.get().version &&
|
|
680
|
+
e.consent.allowsPush();
|
|
681
|
+
const cancelled = () => ({
|
|
682
|
+
granted: false,
|
|
683
|
+
status: 'not_determined',
|
|
684
|
+
platform: Platform.OS === 'ios' ? 'ios' : 'android',
|
|
685
|
+
error: 'session_changed',
|
|
686
|
+
});
|
|
687
|
+
opts = {
|
|
688
|
+
...opts,
|
|
689
|
+
environment: opts?.environment ??
|
|
690
|
+
defaultEnvironment(),
|
|
691
|
+
};
|
|
692
|
+
if (!current())
|
|
693
|
+
return cancelled();
|
|
611
694
|
if (!enginePushListenersAttached)
|
|
612
695
|
attachEnginePushListeners();
|
|
613
696
|
// Snapshot the caller's options so the AppState foreground listener
|
|
@@ -618,12 +701,16 @@ export const UserGist = {
|
|
|
618
701
|
installDelegateProxy: opts?.installDelegateProxy,
|
|
619
702
|
};
|
|
620
703
|
const result = await UserGistPushNative.enablePush(opts ?? {});
|
|
704
|
+
// Reset/withdrawal owns native cleanup. An old permission callback must
|
|
705
|
+
// not disable a new account's subsequently enabled notification adapter.
|
|
706
|
+
if (!current())
|
|
707
|
+
return cancelled();
|
|
621
708
|
// Server-side registration runs on the tokenReceived event; if the
|
|
622
709
|
// native module already had a cached token it surfaces here.
|
|
623
710
|
if (result.granted && result.token) {
|
|
624
711
|
const platform = (result.platform === 'ios' ? 'ios' : 'android');
|
|
625
712
|
await UserGist.registerPushToken(result.token, platform, {
|
|
626
|
-
environment: opts?.environment ??
|
|
713
|
+
environment: opts?.environment ?? defaultEnvironment(),
|
|
627
714
|
});
|
|
628
715
|
}
|
|
629
716
|
return result;
|
|
@@ -632,23 +719,19 @@ export const UserGist = {
|
|
|
632
719
|
reportError('enablePush failed', err);
|
|
633
720
|
return {
|
|
634
721
|
granted: false,
|
|
635
|
-
status: '
|
|
636
|
-
|
|
722
|
+
status: 'not_determined',
|
|
723
|
+
error: 'push_setup_failed',
|
|
724
|
+
platform: Platform.OS === 'ios' ? 'ios' : 'android',
|
|
637
725
|
};
|
|
638
726
|
}
|
|
639
727
|
},
|
|
640
728
|
async disablePush() {
|
|
729
|
+
lastEnablePushOptions = null;
|
|
641
730
|
try {
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
if (e) {
|
|
647
|
-
e.lastPushToken = null;
|
|
648
|
-
e.lastPushRegistrationKey = null;
|
|
649
|
-
e.lastPushRegistrationAt = 0;
|
|
650
|
-
void UserGist.setConsent({ push: false });
|
|
651
|
-
}
|
|
731
|
+
if (engine)
|
|
732
|
+
await UserGist.setConsent({ push: false });
|
|
733
|
+
else
|
|
734
|
+
await UserGistPushNative.disablePush();
|
|
652
735
|
}
|
|
653
736
|
catch (err) {
|
|
654
737
|
reportError('disablePush failed', err);
|
|
@@ -732,12 +815,15 @@ export const UserGist = {
|
|
|
732
815
|
if (!match || !match[1])
|
|
733
816
|
return false;
|
|
734
817
|
const token = match[1];
|
|
818
|
+
const presentationIsValid = e.presentation.validator('survey');
|
|
735
819
|
const id = e.identity.get();
|
|
736
820
|
const resolved = await e.transport.surveyResolveLink({
|
|
737
821
|
token,
|
|
738
822
|
anonymousId: id.anonymousId,
|
|
739
823
|
externalId: id.externalId ?? null,
|
|
740
824
|
});
|
|
825
|
+
if (!presentationIsValid() || !e.consent.allowsSurvey())
|
|
826
|
+
return false;
|
|
741
827
|
if (resolved.consentRequired) {
|
|
742
828
|
debugLog('survey link resolved but consent required — awaiting consent');
|
|
743
829
|
return false;
|
|
@@ -949,6 +1035,9 @@ export const UserGist = {
|
|
|
949
1035
|
try {
|
|
950
1036
|
const e = requireEngine();
|
|
951
1037
|
await ensureHydrated(e);
|
|
1038
|
+
if (e.resetting || !e.consent.allowsFeedback())
|
|
1039
|
+
return { items: [], nextCursor: null };
|
|
1040
|
+
const current = requestSessionIsCurrent(e);
|
|
952
1041
|
const id = e.identity.get();
|
|
953
1042
|
const result = await e.transport.requestsList({
|
|
954
1043
|
anonymousId: id.anonymousId,
|
|
@@ -960,6 +1049,8 @@ export const UserGist = {
|
|
|
960
1049
|
cursor: options.cursor ?? null,
|
|
961
1050
|
limit: options.limit,
|
|
962
1051
|
});
|
|
1052
|
+
if (!current())
|
|
1053
|
+
return { items: [], nextCursor: null };
|
|
963
1054
|
ensureRequestsCache().upsertList(result.items.map((s) => ({
|
|
964
1055
|
id: s.id,
|
|
965
1056
|
appId: '',
|
|
@@ -987,7 +1078,9 @@ export const UserGist = {
|
|
|
987
1078
|
submitRequest(title, description, callback) {
|
|
988
1079
|
try {
|
|
989
1080
|
const e = requireEngine();
|
|
990
|
-
if (typeof title !== 'string' ||
|
|
1081
|
+
if (typeof title !== 'string' ||
|
|
1082
|
+
title.length === 0 ||
|
|
1083
|
+
title.length > 120) {
|
|
991
1084
|
callback?.(new Error('title required, max 120 chars'));
|
|
992
1085
|
return;
|
|
993
1086
|
}
|
|
@@ -1000,10 +1093,11 @@ export const UserGist = {
|
|
|
1000
1093
|
void (async () => {
|
|
1001
1094
|
try {
|
|
1002
1095
|
await ensureHydrated(e);
|
|
1003
|
-
if (!e.consent.allowsFeedback()) {
|
|
1096
|
+
if (e.resetting || !e.consent.allowsFeedback()) {
|
|
1004
1097
|
callback?.(new Error('feedback consent required'));
|
|
1005
1098
|
return;
|
|
1006
1099
|
}
|
|
1100
|
+
const current = requestSessionIsCurrent(e);
|
|
1007
1101
|
const id = e.identity.get();
|
|
1008
1102
|
const req = await e.transport.requestSubmit({
|
|
1009
1103
|
idempotencyKey: generateEventId(),
|
|
@@ -1012,6 +1106,10 @@ export const UserGist = {
|
|
|
1012
1106
|
title,
|
|
1013
1107
|
description,
|
|
1014
1108
|
});
|
|
1109
|
+
if (!current()) {
|
|
1110
|
+
callback?.(new Error('Request cancelled by identity or consent change'));
|
|
1111
|
+
return;
|
|
1112
|
+
}
|
|
1015
1113
|
ensureRequestsCache().upsert(req);
|
|
1016
1114
|
recordServerBackedEventLocally(e, '$request_submitted');
|
|
1017
1115
|
requestsHandlers.onSubmit?.(req);
|
|
@@ -1031,11 +1129,12 @@ export const UserGist = {
|
|
|
1031
1129
|
try {
|
|
1032
1130
|
const e = requireEngine();
|
|
1033
1131
|
await ensureHydrated(e);
|
|
1034
|
-
if (!e.consent.allowsFeedback())
|
|
1132
|
+
if (e.resetting || !e.consent.allowsFeedback())
|
|
1035
1133
|
return;
|
|
1036
1134
|
const cache = ensureRequestsCache();
|
|
1037
1135
|
const rollback = cache.applyOptimisticVote(requestId, vote);
|
|
1038
1136
|
try {
|
|
1137
|
+
const current = requestSessionIsCurrent(e);
|
|
1039
1138
|
const id = e.identity.get();
|
|
1040
1139
|
const result = await e.transport.requestVote({
|
|
1041
1140
|
requestId,
|
|
@@ -1043,6 +1142,10 @@ export const UserGist = {
|
|
|
1043
1142
|
externalId: id.externalId ?? null,
|
|
1044
1143
|
vote,
|
|
1045
1144
|
});
|
|
1145
|
+
if (!current()) {
|
|
1146
|
+
rollback();
|
|
1147
|
+
return;
|
|
1148
|
+
}
|
|
1046
1149
|
cache.commitVote(requestId, result);
|
|
1047
1150
|
recordServerBackedEventLocally(e, vote ? '$request_upvoted' : '$request_unupvoted');
|
|
1048
1151
|
requestsHandlers.onVote?.(result);
|
|
@@ -1060,11 +1163,12 @@ export const UserGist = {
|
|
|
1060
1163
|
try {
|
|
1061
1164
|
const e = requireEngine();
|
|
1062
1165
|
await ensureHydrated(e);
|
|
1063
|
-
if (!e.consent.allowsFeedback())
|
|
1166
|
+
if (e.resetting || !e.consent.allowsFeedback())
|
|
1064
1167
|
return;
|
|
1065
1168
|
const cache = ensureRequestsCache();
|
|
1066
1169
|
const rollback = cache.applyOptimisticFollow(requestId, follow);
|
|
1067
1170
|
try {
|
|
1171
|
+
const current = requestSessionIsCurrent(e);
|
|
1068
1172
|
const id = e.identity.get();
|
|
1069
1173
|
const result = await e.transport.requestFollow({
|
|
1070
1174
|
requestId,
|
|
@@ -1072,6 +1176,10 @@ export const UserGist = {
|
|
|
1072
1176
|
externalId: id.externalId ?? null,
|
|
1073
1177
|
follow,
|
|
1074
1178
|
});
|
|
1179
|
+
if (!current()) {
|
|
1180
|
+
rollback();
|
|
1181
|
+
return;
|
|
1182
|
+
}
|
|
1075
1183
|
cache.commitFollow(requestId, result);
|
|
1076
1184
|
recordServerBackedEventLocally(e, follow ? '$request_followed' : '$request_unfollowed');
|
|
1077
1185
|
requestsHandlers.onFollow?.(result);
|
|
@@ -1095,9 +1203,11 @@ export const UserGist = {
|
|
|
1095
1203
|
try {
|
|
1096
1204
|
const e = requireEngine();
|
|
1097
1205
|
await ensureHydrated(e);
|
|
1098
|
-
if (!e.consent.allowsFeedback())
|
|
1206
|
+
if (e.resetting || !e.consent.allowsFeedback())
|
|
1099
1207
|
return null;
|
|
1100
|
-
|
|
1208
|
+
const current = requestSessionIsCurrent(e);
|
|
1209
|
+
const branding = await e.transport.requestBranding();
|
|
1210
|
+
return current() ? branding : null;
|
|
1101
1211
|
}
|
|
1102
1212
|
catch (err) {
|
|
1103
1213
|
reportError('getRequestBranding failed', err);
|
|
@@ -1114,14 +1224,17 @@ export const UserGist = {
|
|
|
1114
1224
|
try {
|
|
1115
1225
|
const e = requireEngine();
|
|
1116
1226
|
await ensureHydrated(e);
|
|
1117
|
-
if (!e.consent.allowsFeedback())
|
|
1227
|
+
if (e.resetting || !e.consent.allowsFeedback())
|
|
1118
1228
|
return null;
|
|
1229
|
+
const current = requestSessionIsCurrent(e);
|
|
1119
1230
|
const id = e.identity.get();
|
|
1120
1231
|
const req = await e.transport.requestGet({
|
|
1121
1232
|
requestId,
|
|
1122
1233
|
anonymousId: id.anonymousId,
|
|
1123
1234
|
externalId: id.externalId ?? null,
|
|
1124
1235
|
});
|
|
1236
|
+
if (!current())
|
|
1237
|
+
return null;
|
|
1125
1238
|
ensureRequestsCache().upsert(req);
|
|
1126
1239
|
return req;
|
|
1127
1240
|
}
|
|
@@ -1133,7 +1246,7 @@ export const UserGist = {
|
|
|
1133
1246
|
openRequestsBoard() {
|
|
1134
1247
|
try {
|
|
1135
1248
|
const e = requireEngine();
|
|
1136
|
-
if (!e.consent.allowsFeedback())
|
|
1249
|
+
if (e.resetting || !e.consent.allowsFeedback())
|
|
1137
1250
|
return;
|
|
1138
1251
|
e.events.emit('showRequestsBoard', undefined);
|
|
1139
1252
|
}
|
|
@@ -1144,7 +1257,7 @@ export const UserGist = {
|
|
|
1144
1257
|
openRequestDetail(requestId) {
|
|
1145
1258
|
try {
|
|
1146
1259
|
const e = requireEngine();
|
|
1147
|
-
if (!e.consent.allowsFeedback())
|
|
1260
|
+
if (e.resetting || !e.consent.allowsFeedback())
|
|
1148
1261
|
return;
|
|
1149
1262
|
e.events.emit('showRequestDetail', { requestId });
|
|
1150
1263
|
}
|
|
@@ -1161,15 +1274,16 @@ export const UserGist = {
|
|
|
1161
1274
|
try {
|
|
1162
1275
|
const e = requireEngine();
|
|
1163
1276
|
await ensureHydrated(e);
|
|
1164
|
-
if (!e.consent.allowsFeedback())
|
|
1277
|
+
if (e.resetting || !e.consent.allowsFeedback())
|
|
1165
1278
|
return [];
|
|
1279
|
+
const current = requestSessionIsCurrent(e);
|
|
1166
1280
|
const id = e.identity.get();
|
|
1167
1281
|
const result = await e.transport.requestCommentsList({
|
|
1168
1282
|
requestId,
|
|
1169
1283
|
anonymousId: id.anonymousId,
|
|
1170
1284
|
externalId: id.externalId ?? null,
|
|
1171
1285
|
});
|
|
1172
|
-
return result.items;
|
|
1286
|
+
return current() ? result.items : [];
|
|
1173
1287
|
}
|
|
1174
1288
|
catch (err) {
|
|
1175
1289
|
reportError('getComments failed', err);
|
|
@@ -1182,8 +1296,9 @@ export const UserGist = {
|
|
|
1182
1296
|
const e = requireEngine();
|
|
1183
1297
|
validateCommentBody(body);
|
|
1184
1298
|
await ensureHydrated(e);
|
|
1185
|
-
if (!e.consent.allowsFeedback())
|
|
1299
|
+
if (e.resetting || !e.consent.allowsFeedback())
|
|
1186
1300
|
return null;
|
|
1301
|
+
const current = requestSessionIsCurrent(e);
|
|
1187
1302
|
const id = e.identity.get();
|
|
1188
1303
|
const comment = await e.transport.requestCommentPost({
|
|
1189
1304
|
idempotencyKey: generateEventId(),
|
|
@@ -1192,6 +1307,8 @@ export const UserGist = {
|
|
|
1192
1307
|
externalId: id.externalId ?? null,
|
|
1193
1308
|
body,
|
|
1194
1309
|
});
|
|
1310
|
+
if (!current())
|
|
1311
|
+
return null;
|
|
1195
1312
|
recordServerBackedEventLocally(e, REQUEST_COMMENTED_EVENT_NAME);
|
|
1196
1313
|
return comment;
|
|
1197
1314
|
}
|
|
@@ -1206,8 +1323,9 @@ export const UserGist = {
|
|
|
1206
1323
|
const e = requireEngine();
|
|
1207
1324
|
validateCommentBody(body);
|
|
1208
1325
|
await ensureHydrated(e);
|
|
1209
|
-
if (!e.consent.allowsFeedback())
|
|
1326
|
+
if (e.resetting || !e.consent.allowsFeedback())
|
|
1210
1327
|
return null;
|
|
1328
|
+
const current = requestSessionIsCurrent(e);
|
|
1211
1329
|
const id = e.identity.get();
|
|
1212
1330
|
const comment = await e.transport.requestCommentEdit({
|
|
1213
1331
|
requestId,
|
|
@@ -1215,6 +1333,8 @@ export const UserGist = {
|
|
|
1215
1333
|
anonymousId: id.anonymousId,
|
|
1216
1334
|
body,
|
|
1217
1335
|
});
|
|
1336
|
+
if (!current())
|
|
1337
|
+
return null;
|
|
1218
1338
|
recordServerBackedEventLocally(e, REQUEST_COMMENT_EDITED_EVENT_NAME);
|
|
1219
1339
|
return comment;
|
|
1220
1340
|
}
|
|
@@ -1228,14 +1348,17 @@ export const UserGist = {
|
|
|
1228
1348
|
try {
|
|
1229
1349
|
const e = requireEngine();
|
|
1230
1350
|
await ensureHydrated(e);
|
|
1231
|
-
if (!e.consent.allowsFeedback())
|
|
1351
|
+
if (e.resetting || !e.consent.allowsFeedback())
|
|
1232
1352
|
return;
|
|
1353
|
+
const current = requestSessionIsCurrent(e);
|
|
1233
1354
|
const id = e.identity.get();
|
|
1234
1355
|
await e.transport.requestCommentDelete({
|
|
1235
1356
|
requestId,
|
|
1236
1357
|
commentId,
|
|
1237
1358
|
anonymousId: id.anonymousId,
|
|
1238
1359
|
});
|
|
1360
|
+
if (!current())
|
|
1361
|
+
return;
|
|
1239
1362
|
recordServerBackedEventLocally(e, REQUEST_COMMENT_DELETED_EVENT_NAME);
|
|
1240
1363
|
}
|
|
1241
1364
|
catch (err) {
|
|
@@ -1243,4 +1366,97 @@ export const UserGist = {
|
|
|
1243
1366
|
}
|
|
1244
1367
|
},
|
|
1245
1368
|
};
|
|
1369
|
+
/** Registration and invalidation share an ordered lane. Automatic retries may
|
|
1370
|
+
* consume a retained token, but must never recreate an invalidated candidate. */
|
|
1371
|
+
async function registerPushDevice(token, platform, opts, retry = false) {
|
|
1372
|
+
try {
|
|
1373
|
+
if (!token)
|
|
1374
|
+
return;
|
|
1375
|
+
const e = requireEngine();
|
|
1376
|
+
if (e.resetting)
|
|
1377
|
+
return;
|
|
1378
|
+
const generation = e.resetGeneration;
|
|
1379
|
+
await serializePushRegistration(async () => {
|
|
1380
|
+
await ensureHydrated(e);
|
|
1381
|
+
if (e.resetting || generation !== e.resetGeneration)
|
|
1382
|
+
return;
|
|
1383
|
+
const environment = opts?.environment ??
|
|
1384
|
+
(e.config.environment === 'production' ? 'production' : 'sandbox');
|
|
1385
|
+
if (e.resetting || generation !== e.resetGeneration)
|
|
1386
|
+
return;
|
|
1387
|
+
const descriptor = { token, platform, environment };
|
|
1388
|
+
const stored = await e.storage.getJson(STORAGE_KEYS.pushDevice);
|
|
1389
|
+
if (JSON.stringify(stored) !== JSON.stringify(descriptor)) {
|
|
1390
|
+
if (retry)
|
|
1391
|
+
return;
|
|
1392
|
+
await e.storage.setJsonStrict(STORAGE_KEYS.pushDevice, descriptor);
|
|
1393
|
+
}
|
|
1394
|
+
e.pushTokenAvailable = true;
|
|
1395
|
+
pushSubscriptionChanged(e);
|
|
1396
|
+
if (!e.consent.allowsPush())
|
|
1397
|
+
return;
|
|
1398
|
+
await drainRevocations(e);
|
|
1399
|
+
if (await hasPendingRevocations(e) || e.resetting || generation !== e.resetGeneration || !e.consent.allowsPush())
|
|
1400
|
+
return;
|
|
1401
|
+
const id = e.identity.get();
|
|
1402
|
+
const registrationKey = [id.anonymousId, id.externalId ?? '', platform, environment, token].join(':');
|
|
1403
|
+
if (e.lastPushRegistrationKey === registrationKey && Date.now() - e.lastPushRegistrationAt < 24 * 60 * 60_000)
|
|
1404
|
+
return;
|
|
1405
|
+
// Persist before delivery so logout still owns a token if its response is lost.
|
|
1406
|
+
e.lastPushToken = token;
|
|
1407
|
+
await e.storage.setJson(STORAGE_KEYS.pushToken, token);
|
|
1408
|
+
if (e.resetting || generation !== e.resetGeneration || !e.consent.allowsPush())
|
|
1409
|
+
return;
|
|
1410
|
+
const registration = await e.transport.pushRegisterToken({
|
|
1411
|
+
anonymousId: id.anonymousId, externalId: id.externalId ?? null,
|
|
1412
|
+
token, platform, environment, timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
|
1413
|
+
sdkVersion: `rn-${USERGIST_SDK_VERSION}`, optIn: true,
|
|
1414
|
+
});
|
|
1415
|
+
if (e.resetting || generation !== e.resetGeneration || !e.consent.allowsPush())
|
|
1416
|
+
return;
|
|
1417
|
+
if (!(registration && typeof registration === 'object' && 'registered' in registration && registration.registered === true)) {
|
|
1418
|
+
pushSubscriptionChanged(e);
|
|
1419
|
+
return;
|
|
1420
|
+
}
|
|
1421
|
+
e.lastPushRegistrationKey = registrationKey;
|
|
1422
|
+
e.lastPushRegistrationAt = Date.now();
|
|
1423
|
+
pushSubscriptionChanged(e);
|
|
1424
|
+
debugLog('push token registered with server');
|
|
1425
|
+
});
|
|
1426
|
+
}
|
|
1427
|
+
catch (err) {
|
|
1428
|
+
reportError('registerPushToken failed', err);
|
|
1429
|
+
}
|
|
1430
|
+
}
|
|
1431
|
+
async function invalidatePushDevice(token, retainDevice = false) {
|
|
1432
|
+
try {
|
|
1433
|
+
if (!token)
|
|
1434
|
+
return;
|
|
1435
|
+
const e = requireEngine();
|
|
1436
|
+
const generation = e.resetGeneration;
|
|
1437
|
+
await serializePushRegistration(async () => {
|
|
1438
|
+
await ensureHydrated(e);
|
|
1439
|
+
if (e.resetting || generation !== e.resetGeneration)
|
|
1440
|
+
return;
|
|
1441
|
+
const saved = await e.storage.getJson(STORAGE_KEYS.pushDevice);
|
|
1442
|
+
if (saved?.token === token) {
|
|
1443
|
+
// Retire the automatic retry candidate before invalidating it remotely.
|
|
1444
|
+
if (!retainDevice)
|
|
1445
|
+
await e.storage.setJsonStrict(STORAGE_KEYS.pushDevice, null);
|
|
1446
|
+
e.pushTokenAvailable = retainDevice;
|
|
1447
|
+
e.lastPushRegistrationKey = null;
|
|
1448
|
+
e.lastPushRegistrationAt = 0;
|
|
1449
|
+
}
|
|
1450
|
+
if (e.lastPushToken === token) {
|
|
1451
|
+
await e.storage.setJsonStrict(STORAGE_KEYS.pushToken, null);
|
|
1452
|
+
e.lastPushToken = null;
|
|
1453
|
+
}
|
|
1454
|
+
pushSubscriptionChanged(e);
|
|
1455
|
+
await e.transport.pushInvalidateToken({ anonymousId: e.identity.get().anonymousId, token });
|
|
1456
|
+
});
|
|
1457
|
+
}
|
|
1458
|
+
catch (err) {
|
|
1459
|
+
reportError('invalidatePushToken failed', err);
|
|
1460
|
+
}
|
|
1461
|
+
}
|
|
1246
1462
|
//# sourceMappingURL=UserGist.js.map
|