@funnelsgrove/runtime 0.7.6 → 0.7.7
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/dist/components/FunnelContext.d.ts +1 -0
- package/dist/runtime/submit-email-capture.d.ts +4 -1
- package/dist/runtime/submit-email-capture.js +8 -1
- package/dist/runtime/use-funnel-flow-controller.d.ts +1 -1
- package/dist/runtime/use-funnel-flow-controller.js +25 -2
- package/dist/services/api.service.d.ts +4 -0
- package/dist/services/api.service.js +42 -3
- package/dist/services/funnel-sdk.service.d.ts +5 -0
- package/dist/testing/funnel-contract-journey-driver.js +1 -0
- package/package.json +1 -1
|
@@ -21,6 +21,7 @@ export type FunnelContextValue = {
|
|
|
21
21
|
activeStepId: FunnelStepId;
|
|
22
22
|
featureFlags: Record<string, string>;
|
|
23
23
|
isBuilder: boolean;
|
|
24
|
+
isReturningSubscriber: boolean;
|
|
24
25
|
goToStep: (stepId: string, outcome: FunnelNavigationOutcome) => void;
|
|
25
26
|
goNext: () => void;
|
|
26
27
|
goChoice: (choice: 'yes' | 'no', stepId?: string) => void;
|
|
@@ -2,6 +2,8 @@ export type EmailCapturePersistenceResult<User> = {
|
|
|
2
2
|
user: User;
|
|
3
3
|
eventId: string;
|
|
4
4
|
eventCreated: boolean;
|
|
5
|
+
canonicalUser: User | null;
|
|
6
|
+
activeSubscription: boolean;
|
|
5
7
|
};
|
|
6
8
|
export type EmailCaptureSingleFlight = {
|
|
7
9
|
current: Promise<void> | null;
|
|
@@ -13,10 +15,11 @@ export type SubmitEmailCaptureInput<User> = {
|
|
|
13
15
|
retryPendingNavigation?: () => boolean | Promise<boolean>;
|
|
14
16
|
persist: (email: string) => Promise<EmailCapturePersistenceResult<User>>;
|
|
15
17
|
applyUser: (user: User) => void;
|
|
18
|
+
adoptCanonicalUser: (user: User) => void;
|
|
16
19
|
applyPreviewEmail: (email: string) => void;
|
|
17
20
|
fanOutBrowserDestinations: (eventId: string) => void | Promise<void>;
|
|
18
21
|
completeVisit: () => boolean;
|
|
19
|
-
resolveNextStep: () => string | null;
|
|
22
|
+
resolveNextStep: (canonicalUser: User | null, activeSubscription: boolean) => string | null;
|
|
20
23
|
navigate: (stepId: string) => void;
|
|
21
24
|
};
|
|
22
25
|
export declare const createEmailCaptureSingleFlight: () => EmailCaptureSingleFlight;
|
|
@@ -15,11 +15,15 @@ const runEmailCapture = async (input) => {
|
|
|
15
15
|
return;
|
|
16
16
|
}
|
|
17
17
|
const email = normalizeEmail(input.email);
|
|
18
|
+
let canonicalUser = null;
|
|
19
|
+
let activeSubscription = false;
|
|
18
20
|
if (input.isPreview) {
|
|
19
21
|
input.applyPreviewEmail(email);
|
|
20
22
|
}
|
|
21
23
|
else {
|
|
22
24
|
const result = await input.persist(email);
|
|
25
|
+
canonicalUser = result.canonicalUser;
|
|
26
|
+
activeSubscription = result.activeSubscription;
|
|
23
27
|
input.applyUser(result.user);
|
|
24
28
|
if (result.eventCreated) {
|
|
25
29
|
try {
|
|
@@ -33,7 +37,10 @@ const runEmailCapture = async (input) => {
|
|
|
33
37
|
if (!input.completeVisit()) {
|
|
34
38
|
return;
|
|
35
39
|
}
|
|
36
|
-
|
|
40
|
+
if (canonicalUser) {
|
|
41
|
+
input.adoptCanonicalUser(canonicalUser);
|
|
42
|
+
}
|
|
43
|
+
const nextStepId = input.resolveNextStep(canonicalUser, activeSubscription);
|
|
37
44
|
if (nextStepId) {
|
|
38
45
|
input.navigate(nextStepId);
|
|
39
46
|
}
|
|
@@ -105,7 +105,7 @@ type FunnelFlowAnalyticsAdapter = {
|
|
|
105
105
|
featureFlags?: Record<string, string>;
|
|
106
106
|
}) => string | null;
|
|
107
107
|
};
|
|
108
|
-
export type FunnelFlowApiAdapter = Pick<typeof apiService, 'getOrCreateClientUserId' | 'getBootstrapCandidateUserId' | 'bootstrapSession' | 'pingUserContext' | 'updateUser' | 'captureEmail'>;
|
|
108
|
+
export type FunnelFlowApiAdapter = Pick<typeof apiService, 'getOrCreateClientUserId' | 'getBootstrapCandidateUserId' | 'bootstrapSession' | 'pingUserContext' | 'persistCanonicalUserId' | 'updateUser' | 'captureEmail'>;
|
|
109
109
|
type UseFunnelFlowControllerInput<StepId extends string> = {
|
|
110
110
|
api?: FunnelFlowApiAdapter;
|
|
111
111
|
analytics?: FunnelFlowAnalyticsAdapter;
|
|
@@ -319,6 +319,7 @@ export function useFunnelFlowController({ api = apiService, analytics, stepContr
|
|
|
319
319
|
completedSteps: [],
|
|
320
320
|
}));
|
|
321
321
|
const [userBootstrapped, setUserBootstrapped] = useState(isPreviewRuntime);
|
|
322
|
+
const [isReturningSubscriber, setIsReturningSubscriber] = useState(false);
|
|
322
323
|
const [attributionReady, setAttributionReady] = useState(isPreviewRuntime);
|
|
323
324
|
const attributesAtStepStart = useRef({});
|
|
324
325
|
const attributesRef = useRef(attributes);
|
|
@@ -624,6 +625,7 @@ export function useFunnelFlowController({ api = apiService, analytics, stepContr
|
|
|
624
625
|
document: (_b = updatedUser.document) !== null && _b !== void 0 ? _b : {},
|
|
625
626
|
}, {
|
|
626
627
|
attribution: collectCurrentFunnelAttribution(),
|
|
628
|
+
persistClientIdentity: false,
|
|
627
629
|
})
|
|
628
630
|
.catch((error) => {
|
|
629
631
|
logger.error('Failed to persist step completion:', error);
|
|
@@ -761,9 +763,15 @@ export function useFunnelFlowController({ api = apiService, analytics, stepContr
|
|
|
761
763
|
environment: getRuntimeMode(),
|
|
762
764
|
}),
|
|
763
765
|
applyUser: (capturedUser) => {
|
|
766
|
+
setIsReturningSubscriber(false);
|
|
764
767
|
currentUserIdRef.current = capturedUser.id;
|
|
765
768
|
setUser((prev) => (Object.assign(Object.assign(Object.assign({}, prev), capturedUser), { completedSteps: prev.completedSteps })));
|
|
766
769
|
},
|
|
770
|
+
adoptCanonicalUser: (canonicalUser) => {
|
|
771
|
+
const canonicalUserId = api.persistCanonicalUserId(canonicalUser.id);
|
|
772
|
+
currentUserIdRef.current = canonicalUserId;
|
|
773
|
+
setUser((prev) => (Object.assign(Object.assign({}, prev), { id: canonicalUserId, email: canonicalUser.email, completedSteps: prev.completedSteps })));
|
|
774
|
+
},
|
|
767
775
|
applyPreviewEmail: (normalizedEmail) => {
|
|
768
776
|
setUser((prev) => (Object.assign(Object.assign({}, prev), { email: normalizedEmail })));
|
|
769
777
|
},
|
|
@@ -785,10 +793,21 @@ export function useFunnelFlowController({ api = apiService, analytics, stepContr
|
|
|
785
793
|
&& ((_a = activeVisitTokenRef.current) === null || _a === void 0 ? void 0 : _a.visitId) === submittedVisitId
|
|
786
794
|
&& claimActiveVisit(submittedStepId, { type: 'complete' }));
|
|
787
795
|
},
|
|
788
|
-
resolveNextStep: () => {
|
|
789
|
-
|
|
796
|
+
resolveNextStep: (_canonicalUser, activeSubscription) => {
|
|
797
|
+
var _a;
|
|
798
|
+
const manifestTargetStepId = resolveNextStepId(submittedStepId, {
|
|
790
799
|
ignoreExperiment: Boolean(activeExperimentForStep && submittedStepId === safeActiveStepId),
|
|
791
800
|
});
|
|
801
|
+
const handoffStepId = activeSubscription
|
|
802
|
+
? (_a = stepSequence.find((stepId) => {
|
|
803
|
+
const step = stepById[stepId];
|
|
804
|
+
return (step === null || step === void 0 ? void 0 : step.type) === 'purchase_completed'
|
|
805
|
+
&& step.kind === 'subscription-handoff'
|
|
806
|
+
&& Boolean(stepComponentById[stepId]);
|
|
807
|
+
})) !== null && _a !== void 0 ? _a : null
|
|
808
|
+
: null;
|
|
809
|
+
const targetStepId = handoffStepId || manifestTargetStepId;
|
|
810
|
+
setIsReturningSubscriber(Boolean(activeSubscription && handoffStepId));
|
|
792
811
|
if (submittedVisitId !== null) {
|
|
793
812
|
pendingEmailCaptureNavigationRef.current = {
|
|
794
813
|
visitId: submittedVisitId,
|
|
@@ -816,6 +835,8 @@ export function useFunnelFlowController({ api = apiService, analytics, stepContr
|
|
|
816
835
|
resolveNextStepId,
|
|
817
836
|
safeActiveStepId,
|
|
818
837
|
stepById,
|
|
838
|
+
stepComponentById,
|
|
839
|
+
stepSequence,
|
|
819
840
|
]);
|
|
820
841
|
useEffect(() => {
|
|
821
842
|
const abandonCurrentVisit = (event) => {
|
|
@@ -1296,6 +1317,7 @@ export function useFunnelFlowController({ api = apiService, analytics, stepContr
|
|
|
1296
1317
|
activeStepId: renderedStepId,
|
|
1297
1318
|
featureFlags: postHogFeatureFlags,
|
|
1298
1319
|
isBuilder: isPreviewRuntime,
|
|
1320
|
+
isReturningSubscriber,
|
|
1299
1321
|
goToStep: goToStepFromContext,
|
|
1300
1322
|
goNext,
|
|
1301
1323
|
goChoice,
|
|
@@ -1331,6 +1353,7 @@ export function useFunnelFlowController({ api = apiService, analytics, stepContr
|
|
|
1331
1353
|
goNext,
|
|
1332
1354
|
goToStepFromContext,
|
|
1333
1355
|
isPreviewRuntime,
|
|
1356
|
+
isReturningSubscriber,
|
|
1334
1357
|
postHogFeatureFlags,
|
|
1335
1358
|
renderedStepId,
|
|
1336
1359
|
resolveRenderableStepId,
|
|
@@ -55,10 +55,13 @@ export type EmailCaptureResult = {
|
|
|
55
55
|
user: AppUser;
|
|
56
56
|
eventId: string;
|
|
57
57
|
eventCreated: boolean;
|
|
58
|
+
canonicalUser: AppUser | null;
|
|
59
|
+
activeSubscription: boolean;
|
|
58
60
|
};
|
|
59
61
|
declare class ApiService {
|
|
60
62
|
private readFileAsDataUrl;
|
|
61
63
|
getOrCreateClientUserId(): string;
|
|
64
|
+
persistCanonicalUserId(userId: string): string;
|
|
62
65
|
getSubscriptionManagementUserId(): string | null;
|
|
63
66
|
getSubscriptionManagementStripeCustomerId(): string | null;
|
|
64
67
|
getBootstrapCandidateUserId(fallbackUserId?: string | null): string | null;
|
|
@@ -72,6 +75,7 @@ declare class ApiService {
|
|
|
72
75
|
}): Promise<AppUser>;
|
|
73
76
|
updateUser(user: AppUser, options?: {
|
|
74
77
|
attribution?: FunnelUserAttribution;
|
|
78
|
+
persistClientIdentity?: boolean;
|
|
75
79
|
}): Promise<AppUser>;
|
|
76
80
|
captureEmail(input: {
|
|
77
81
|
userId: string;
|
|
@@ -214,6 +214,13 @@ class ApiService {
|
|
|
214
214
|
}
|
|
215
215
|
return persistUserId(generateUserId(), FUNNEL_ID);
|
|
216
216
|
}
|
|
217
|
+
persistCanonicalUserId(userId) {
|
|
218
|
+
const normalizedUserId = normalizeUserId(userId);
|
|
219
|
+
if (!normalizedUserId) {
|
|
220
|
+
throw new Error('Invalid canonical user id');
|
|
221
|
+
}
|
|
222
|
+
return persistUserId(normalizedUserId, FUNNEL_ID);
|
|
223
|
+
}
|
|
217
224
|
getSubscriptionManagementUserId() {
|
|
218
225
|
const locationUserId = resolveUrlUserAttributes().userId;
|
|
219
226
|
if (locationUserId) {
|
|
@@ -313,7 +320,10 @@ class ApiService {
|
|
|
313
320
|
});
|
|
314
321
|
}
|
|
315
322
|
async updateUser(user, options) {
|
|
316
|
-
const
|
|
323
|
+
const userId = normalizeUserId(user.id) || this.getOrCreateClientUserId();
|
|
324
|
+
const persistedUserId = (options === null || options === void 0 ? void 0 : options.persistClientIdentity) === false
|
|
325
|
+
? userId
|
|
326
|
+
: persistUserId(userId, FUNNEL_ID);
|
|
317
327
|
const publishableKey = getFunnelSdkPublishableKey();
|
|
318
328
|
if (!publishableKey) {
|
|
319
329
|
return toAppUser({
|
|
@@ -347,7 +357,7 @@ class ApiService {
|
|
|
347
357
|
});
|
|
348
358
|
}
|
|
349
359
|
async captureEmail(input) {
|
|
350
|
-
var _a, _b;
|
|
360
|
+
var _a, _b, _c, _d;
|
|
351
361
|
const userId = input.userId.trim();
|
|
352
362
|
const email = input.email.trim().toLowerCase();
|
|
353
363
|
const payload = await funnelSdkService.captureEmail({
|
|
@@ -359,15 +369,42 @@ class ApiService {
|
|
|
359
369
|
const responseUser = isRecord(payload.user) ? payload.user : null;
|
|
360
370
|
const responseUserId = responseUser === null || responseUser === void 0 ? void 0 : responseUser.user_id;
|
|
361
371
|
const responseEmail = (_b = (_a = asString(responseUser === null || responseUser === void 0 ? void 0 : responseUser.email)) === null || _a === void 0 ? void 0 : _a.toLowerCase()) !== null && _b !== void 0 ? _b : null;
|
|
372
|
+
const canonicalUserValue = payload.canonicalUser;
|
|
373
|
+
const canonicalUserRecord = isRecord(canonicalUserValue)
|
|
374
|
+
? canonicalUserValue
|
|
375
|
+
: null;
|
|
376
|
+
const canonicalUserId = asString(canonicalUserRecord === null || canonicalUserRecord === void 0 ? void 0 : canonicalUserRecord.user_id);
|
|
377
|
+
const canonicalUserEmail = (_d = (_c = asString(canonicalUserRecord === null || canonicalUserRecord === void 0 ? void 0 : canonicalUserRecord.email)) === null || _c === void 0 ? void 0 : _c.toLowerCase()) !== null && _d !== void 0 ? _d : null;
|
|
378
|
+
const hasCanonicalUser = canonicalUserValue !== undefined
|
|
379
|
+
&& canonicalUserValue !== null;
|
|
380
|
+
const canonicalUserKeys = canonicalUserRecord
|
|
381
|
+
? Object.keys(canonicalUserRecord)
|
|
382
|
+
: [];
|
|
362
383
|
if (!eventId
|
|
363
384
|
|| typeof payload.eventCreated !== 'boolean'
|
|
385
|
+
|| typeof payload.activeSubscription !== 'boolean'
|
|
364
386
|
|| !responseUser
|
|
365
387
|
|| typeof responseUserId !== 'string'
|
|
366
388
|
|| responseUserId !== userId
|
|
367
|
-
|| responseEmail !== email
|
|
389
|
+
|| responseEmail !== email
|
|
390
|
+
|| (hasCanonicalUser
|
|
391
|
+
&& (!canonicalUserRecord
|
|
392
|
+
|| !canonicalUserId
|
|
393
|
+
|| canonicalUserEmail !== email
|
|
394
|
+
|| canonicalUserKeys.length !== 2
|
|
395
|
+
|| canonicalUserKeys.some((key) => key !== 'user_id' && key !== 'email')))) {
|
|
368
396
|
throw new Error('Invalid email capture response');
|
|
369
397
|
}
|
|
370
398
|
const persistedUserId = persistUserId(userId, FUNNEL_ID);
|
|
399
|
+
const canonicalUser = canonicalUserRecord && canonicalUserId
|
|
400
|
+
? {
|
|
401
|
+
id: canonicalUserId,
|
|
402
|
+
name: '',
|
|
403
|
+
email,
|
|
404
|
+
attributes: {},
|
|
405
|
+
document: {},
|
|
406
|
+
}
|
|
407
|
+
: null;
|
|
371
408
|
return {
|
|
372
409
|
user: toAppUser({
|
|
373
410
|
apiUser: Object.assign(Object.assign({}, responseUser), { user_id: userId, email }),
|
|
@@ -376,6 +413,8 @@ class ApiService {
|
|
|
376
413
|
}),
|
|
377
414
|
eventId,
|
|
378
415
|
eventCreated: payload.eventCreated,
|
|
416
|
+
canonicalUser,
|
|
417
|
+
activeSubscription: payload.activeSubscription,
|
|
379
418
|
};
|
|
380
419
|
}
|
|
381
420
|
async syncUrlUserAttributes(input) {
|
|
@@ -36,6 +36,11 @@ export type FunnelSdkCaptureEmailResponse = {
|
|
|
36
36
|
email?: unknown;
|
|
37
37
|
document?: unknown;
|
|
38
38
|
};
|
|
39
|
+
canonicalUser?: {
|
|
40
|
+
user_id?: unknown;
|
|
41
|
+
email?: unknown;
|
|
42
|
+
} | null;
|
|
43
|
+
activeSubscription: boolean;
|
|
39
44
|
eventId: string;
|
|
40
45
|
eventCreated: boolean;
|
|
41
46
|
};
|
|
@@ -193,6 +193,7 @@ export const runFunnelContractJourney = async (options) => {
|
|
|
193
193
|
getBootstrapCandidateUserId: () => journeyUser.id,
|
|
194
194
|
bootstrapSession: async () => journeyUser,
|
|
195
195
|
pingUserContext: async () => null,
|
|
196
|
+
persistCanonicalUserId: (userId) => userId,
|
|
196
197
|
updateUser: async (user) => user,
|
|
197
198
|
captureEmail: async (captureInput) => {
|
|
198
199
|
emailCaptureCalls.push(captureInput);
|