@funnelsgrove/runtime 0.7.27 → 0.7.29
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/runtime/funnel-answer-draft.d.ts +11 -0
- package/dist/runtime/funnel-answer-draft.js +82 -0
- package/dist/runtime/use-funnel-flow-controller.js +70 -36
- package/dist/services/api.service.d.ts +1 -1
- package/dist/services/api.service.js +7 -20
- package/dist/services/funnel-sdk.service.d.ts +24 -0
- package/dist/services/funnel-sdk.service.js +43 -0
- package/package.json +1 -1
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { FunnelUserAnswers } from '../sdk/userAnswers.js';
|
|
2
|
+
type FunnelAnswerDraftScope = {
|
|
3
|
+
funnelId: string | null | undefined;
|
|
4
|
+
funnelVersionId: string | null | undefined;
|
|
5
|
+
userId: string | null | undefined;
|
|
6
|
+
};
|
|
7
|
+
export declare const buildFunnelAnswerDraftStorageKey: (scope: FunnelAnswerDraftScope) => string | null;
|
|
8
|
+
export declare const readFunnelAnswerDraft: (scope: FunnelAnswerDraftScope, nowMs?: number) => FunnelUserAnswers | null;
|
|
9
|
+
export declare const writeFunnelAnswerDraft: (scope: FunnelAnswerDraftScope, answers: FunnelUserAnswers, nowMs?: number) => boolean;
|
|
10
|
+
export declare const clearFunnelAnswerDraft: (scope: FunnelAnswerDraftScope) => void;
|
|
11
|
+
export {};
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
const ANSWER_DRAFT_VERSION = 1;
|
|
2
|
+
const ANSWER_DRAFT_TTL_MS = 14 * 24 * 60 * 60 * 1000;
|
|
3
|
+
const MAX_ANSWER_DRAFT_BYTES = 65536;
|
|
4
|
+
const asNonEmptyPart = (value) => {
|
|
5
|
+
const normalized = value === null || value === void 0 ? void 0 : value.trim();
|
|
6
|
+
return normalized || null;
|
|
7
|
+
};
|
|
8
|
+
const isRecord = (value) => (Boolean(value) && typeof value === 'object' && !Array.isArray(value));
|
|
9
|
+
export const buildFunnelAnswerDraftStorageKey = (scope) => {
|
|
10
|
+
const funnelId = asNonEmptyPart(scope.funnelId);
|
|
11
|
+
const funnelVersionId = asNonEmptyPart(scope.funnelVersionId);
|
|
12
|
+
const userId = asNonEmptyPart(scope.userId);
|
|
13
|
+
if (!funnelId || !funnelVersionId || !userId) {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
return `funnel:${funnelId}:${funnelVersionId}:${userId}:answer-draft`;
|
|
17
|
+
};
|
|
18
|
+
export const readFunnelAnswerDraft = (scope, nowMs = Date.now()) => {
|
|
19
|
+
const key = buildFunnelAnswerDraftStorageKey(scope);
|
|
20
|
+
if (!key || typeof window === 'undefined') {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
try {
|
|
24
|
+
const raw = window.localStorage.getItem(key);
|
|
25
|
+
if (!raw) {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
const parsed = JSON.parse(raw);
|
|
29
|
+
if (parsed.version !== ANSWER_DRAFT_VERSION
|
|
30
|
+
|| typeof parsed.updatedAt !== 'number'
|
|
31
|
+
|| !Number.isFinite(parsed.updatedAt)
|
|
32
|
+
|| parsed.updatedAt > nowMs
|
|
33
|
+
|| nowMs - parsed.updatedAt > ANSWER_DRAFT_TTL_MS
|
|
34
|
+
|| !isRecord(parsed.answers)) {
|
|
35
|
+
window.localStorage.removeItem(key);
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
return parsed.answers;
|
|
39
|
+
}
|
|
40
|
+
catch (_a) {
|
|
41
|
+
try {
|
|
42
|
+
window.localStorage.removeItem(key);
|
|
43
|
+
}
|
|
44
|
+
catch (_b) {
|
|
45
|
+
// Storage may be unavailable in privacy-constrained browser contexts.
|
|
46
|
+
}
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
export const writeFunnelAnswerDraft = (scope, answers, nowMs = Date.now()) => {
|
|
51
|
+
const key = buildFunnelAnswerDraftStorageKey(scope);
|
|
52
|
+
if (!key || typeof window === 'undefined' || !isRecord(answers)) {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
try {
|
|
56
|
+
const serialized = JSON.stringify({
|
|
57
|
+
version: ANSWER_DRAFT_VERSION,
|
|
58
|
+
updatedAt: nowMs,
|
|
59
|
+
answers,
|
|
60
|
+
});
|
|
61
|
+
if (new TextEncoder().encode(serialized).byteLength > MAX_ANSWER_DRAFT_BYTES) {
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
window.localStorage.setItem(key, serialized);
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
catch (_a) {
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
export const clearFunnelAnswerDraft = (scope) => {
|
|
72
|
+
const key = buildFunnelAnswerDraftStorageKey(scope);
|
|
73
|
+
if (!key || typeof window === 'undefined') {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
try {
|
|
77
|
+
window.localStorage.removeItem(key);
|
|
78
|
+
}
|
|
79
|
+
catch (_a) {
|
|
80
|
+
// Storage may be unavailable in privacy-constrained browser contexts.
|
|
81
|
+
}
|
|
82
|
+
};
|
|
@@ -4,7 +4,7 @@ import { useFunnelRuntimeConfig } from '../components/FunnelRuntimeConfigBoundar
|
|
|
4
4
|
import { writeStepChoice } from '../sdk/userAnswers.js';
|
|
5
5
|
import { apiService } from '../services/api.service.js';
|
|
6
6
|
import { logger } from '../services/logger.js';
|
|
7
|
-
import { FUNNEL_ID, POSTHOG_API_HOST, POSTHOG_PROJECT_API_KEY, PROJECT_ID, } from '../services/runtime-api.config.js';
|
|
7
|
+
import { FUNNEL_ID, FUNNEL_VERSION_ID, POSTHOG_API_HOST, POSTHOG_PROJECT_API_KEY, PROJECT_ID, } from '../services/runtime-api.config.js';
|
|
8
8
|
import { buildHostedStepLocation, dispatchWindowCustomEvent, updateFunnelHistory, } from './browser-helpers.js';
|
|
9
9
|
import { isPreviewStepLockRequested, resolveNextStepFromContext, shouldRunAutoAdvanceTimer, } from './funnel-flow.js';
|
|
10
10
|
import { usePreviewBridge } from './preview-bridge.js';
|
|
@@ -19,6 +19,7 @@ import { isDevelopmentRuntime } from '../config/env.config.js';
|
|
|
19
19
|
import { consumeChoiceCompletionMarker, enforceChoiceBypassGuard, registerInternalChoiceCommit, runAtomicChoiceCommit, } from './use-step-choices.js';
|
|
20
20
|
import { createFunnelStepLifecycleState, finishVisit, startVisit, } from './funnel-step-lifecycle.js';
|
|
21
21
|
import { createEmailCaptureSingleFlight, submitEmailCapture as submitEmailCapturePrimitive, } from './submit-email-capture.js';
|
|
22
|
+
import { clearFunnelAnswerDraft, readFunnelAnswerDraft, writeFunnelAnswerDraft, } from './funnel-answer-draft.js';
|
|
22
23
|
const isTerminalCompletion = (stepMeta, outcome) => {
|
|
23
24
|
var _a;
|
|
24
25
|
return (outcome.type === 'complete'
|
|
@@ -322,14 +323,25 @@ export function useFunnelFlowController({ api = apiService, analytics, stepContr
|
|
|
322
323
|
}, [initialStepId, resolveFallbackStepId, resolveRuntimeInitialStepId]);
|
|
323
324
|
const isSubscriptionManagementRuntime = ((_g = stepById[safeInitialStepId]) === null || _g === void 0 ? void 0 : _g.kind) === 'manage-subscription';
|
|
324
325
|
const lifecycleEventsSuppressed = isPreviewRuntime || editorModeEnabled || isSubscriptionManagementRuntime;
|
|
326
|
+
const [initialRuntimeIdentity] = useState(() => {
|
|
327
|
+
const urlAttributes = resolveUrlUserAttributes();
|
|
328
|
+
const clientUserId = isSubscriptionManagementRuntime
|
|
329
|
+
? urlAttributes.userId || ''
|
|
330
|
+
: api.getOrCreateClientUserId();
|
|
331
|
+
return { clientUserId, urlAttributes };
|
|
332
|
+
});
|
|
325
333
|
const [activeStepId, setActiveStepId] = useState(safeInitialStepId);
|
|
326
|
-
const [attributes, setAttributes] = useState(() => (Object.assign({}, (
|
|
334
|
+
const [attributes, setAttributes] = useState(() => (Object.assign(Object.assign({}, (isSubscriptionManagementRuntime || isPreviewRuntime
|
|
335
|
+
? {}
|
|
336
|
+
: readFunnelAnswerDraft({
|
|
337
|
+
funnelId: FUNNEL_ID,
|
|
338
|
+
funnelVersionId: FUNNEL_VERSION_ID,
|
|
339
|
+
userId: initialRuntimeIdentity.clientUserId,
|
|
340
|
+
}) || {})), (initialAttributes || {}))));
|
|
327
341
|
const [user, setUser] = useState(() => ({
|
|
328
|
-
id:
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
name: '',
|
|
332
|
-
email: '',
|
|
342
|
+
id: initialRuntimeIdentity.clientUserId,
|
|
343
|
+
name: initialRuntimeIdentity.urlAttributes.name || '',
|
|
344
|
+
email: initialRuntimeIdentity.urlAttributes.email || '',
|
|
333
345
|
attributes: {},
|
|
334
346
|
document: {},
|
|
335
347
|
completedSteps: [],
|
|
@@ -353,6 +365,7 @@ export function useFunnelFlowController({ api = apiService, analytics, stepContr
|
|
|
353
365
|
const stepLifecycleStateRef = useRef(createFunnelStepLifecycleState());
|
|
354
366
|
const activeVisitTokenRef = useRef(null);
|
|
355
367
|
const emailCaptureSingleFlightRef = useRef(createEmailCaptureSingleFlight());
|
|
368
|
+
const answersCommittedRef = useRef(false);
|
|
356
369
|
const pendingEmailCaptureNavigationRef = useRef(null);
|
|
357
370
|
const appliedInitialStepIdRef = useRef(safeInitialStepId);
|
|
358
371
|
const safeActiveStepId = useMemo(() => { var _a; return (_a = resolveRenderableStepId(activeStepId)) !== null && _a !== void 0 ? _a : safeInitialStepId; }, [activeStepId, resolveRenderableStepId, safeInitialStepId]);
|
|
@@ -474,6 +487,16 @@ export function useFunnelFlowController({ api = apiService, analytics, stepContr
|
|
|
474
487
|
useEffect(() => {
|
|
475
488
|
attributesRef.current = attributes;
|
|
476
489
|
}, [attributes]);
|
|
490
|
+
useEffect(() => {
|
|
491
|
+
if (isPreviewRuntime || isSubscriptionManagementRuntime || answersCommittedRef.current) {
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
494
|
+
writeFunnelAnswerDraft({
|
|
495
|
+
funnelId: FUNNEL_ID,
|
|
496
|
+
funnelVersionId: FUNNEL_VERSION_ID,
|
|
497
|
+
userId: user.id,
|
|
498
|
+
}, attributes);
|
|
499
|
+
}, [attributes, isPreviewRuntime, isSubscriptionManagementRuntime, user.id]);
|
|
477
500
|
useEffect(() => {
|
|
478
501
|
postHogFeatureFlagsRef.current = postHogFeatureFlags;
|
|
479
502
|
}, [postHogFeatureFlags]);
|
|
@@ -522,12 +545,12 @@ export function useFunnelFlowController({ api = apiService, analytics, stepContr
|
|
|
522
545
|
setPostHogReady(true);
|
|
523
546
|
return;
|
|
524
547
|
}
|
|
525
|
-
const localUserId =
|
|
548
|
+
const localUserId = initialRuntimeIdentity.clientUserId;
|
|
526
549
|
const bootstrapCandidateUserId = api.getBootstrapCandidateUserId(localUserId);
|
|
527
550
|
const initialAttribution = collectCurrentFunnelAttribution();
|
|
528
551
|
initialAttributionRef.current = initialAttribution;
|
|
529
552
|
setUserBootstrapped(isPreviewRuntime);
|
|
530
|
-
setUser((prev) => (Object.assign(Object.assign({}, prev), { id: localUserId })));
|
|
553
|
+
setUser((prev) => (Object.assign(Object.assign({}, prev), { id: localUserId, name: prev.name || urlUserAttributes.name || '', email: prev.email || urlUserAttributes.email || '' })));
|
|
531
554
|
if (isPreviewRuntime) {
|
|
532
555
|
return;
|
|
533
556
|
}
|
|
@@ -551,8 +574,6 @@ export function useFunnelFlowController({ api = apiService, analytics, stepContr
|
|
|
551
574
|
const sessionBootstrap = api
|
|
552
575
|
.bootstrapSession({
|
|
553
576
|
userId: bootstrapCandidateUserId || localUserId,
|
|
554
|
-
email: urlUserAttributes.email || undefined,
|
|
555
|
-
name: urlUserAttributes.name || undefined,
|
|
556
577
|
attribution: initialAttribution,
|
|
557
578
|
})
|
|
558
579
|
.then((sessionUser) => {
|
|
@@ -560,8 +581,13 @@ export function useFunnelFlowController({ api = apiService, analytics, stepContr
|
|
|
560
581
|
const sessionAttributes = Object.keys(sessionUser.attributes || {}).length > 0
|
|
561
582
|
? sessionUser.attributes
|
|
562
583
|
: null;
|
|
563
|
-
|
|
564
|
-
|
|
584
|
+
const sessionDraft = readFunnelAnswerDraft({
|
|
585
|
+
funnelId: FUNNEL_ID,
|
|
586
|
+
funnelVersionId: FUNNEL_VERSION_ID,
|
|
587
|
+
userId: sessionUser.id,
|
|
588
|
+
});
|
|
589
|
+
if (sessionAttributes || sessionDraft) {
|
|
590
|
+
setAttributes((prev) => (Object.assign(Object.assign(Object.assign({}, (sessionAttributes || {})), (sessionDraft || {})), prev)));
|
|
565
591
|
}
|
|
566
592
|
initialAttributionRef.current = resolveFunnelEventAttribution(sessionUser.document, initialAttribution);
|
|
567
593
|
setUser((prev) => (Object.assign(Object.assign({}, prev), { id: sessionUser.id || prev.id, name: sessionUser.name || prev.name, email: sessionUser.email || prev.email, document: sessionUser.document, attributes: sessionAttributes ? Object.assign(Object.assign({}, sessionAttributes), prev.attributes) : prev.attributes })));
|
|
@@ -602,6 +628,7 @@ export function useFunnelFlowController({ api = apiService, analytics, stepContr
|
|
|
602
628
|
return cancelAttributionFailOpen;
|
|
603
629
|
}, [
|
|
604
630
|
api,
|
|
631
|
+
initialRuntimeIdentity.clientUserId,
|
|
605
632
|
isPreviewRuntime,
|
|
606
633
|
isSubscriptionManagementRuntime,
|
|
607
634
|
sessionReplayEnabled,
|
|
@@ -640,25 +667,9 @@ export function useFunnelFlowController({ api = apiService, analytics, stepContr
|
|
|
640
667
|
const completionAttributes = Object.assign({}, attributesRef.current);
|
|
641
668
|
dispatchWindowCustomEvent('funnel:step-completed', record);
|
|
642
669
|
setUser((prev) => {
|
|
643
|
-
var _a
|
|
670
|
+
var _a;
|
|
644
671
|
const updatedSteps = [...((_a = prev.completedSteps) !== null && _a !== void 0 ? _a : []), record];
|
|
645
672
|
const updatedUser = Object.assign(Object.assign({}, prev), { attributes: Object.assign(Object.assign({}, prev.attributes), completionAttributes), document: prev.document, completedSteps: updatedSteps });
|
|
646
|
-
if (!isPreviewRuntime) {
|
|
647
|
-
api
|
|
648
|
-
.updateUser({
|
|
649
|
-
id: updatedUser.id,
|
|
650
|
-
name: updatedUser.name,
|
|
651
|
-
email: updatedUser.email,
|
|
652
|
-
attributes: Object.assign(Object.assign({}, updatedUser.attributes), { completedSteps: updatedSteps }),
|
|
653
|
-
document: (_b = updatedUser.document) !== null && _b !== void 0 ? _b : {},
|
|
654
|
-
}, {
|
|
655
|
-
attribution: collectCurrentFunnelAttribution(),
|
|
656
|
-
persistClientIdentity: false,
|
|
657
|
-
})
|
|
658
|
-
.catch((error) => {
|
|
659
|
-
logger.error('Failed to persist step completion:', error);
|
|
660
|
-
});
|
|
661
|
-
}
|
|
662
673
|
return updatedUser;
|
|
663
674
|
});
|
|
664
675
|
if (!isPreviewRuntime) {
|
|
@@ -670,7 +681,7 @@ export function useFunnelFlowController({ api = apiService, analytics, stepContr
|
|
|
670
681
|
logger.error('Failed to track step completion:', error);
|
|
671
682
|
}
|
|
672
683
|
}
|
|
673
|
-
}, [analytics,
|
|
684
|
+
}, [analytics, attributionReady, isPreviewRuntime, lifecycleEventsSuppressed, stepById, stepContractVersion]);
|
|
674
685
|
const emitStepExit = useCallback((intent) => {
|
|
675
686
|
var _a;
|
|
676
687
|
const meta = stepById[intent.visit.stepId];
|
|
@@ -785,11 +796,33 @@ export function useFunnelFlowController({ api = apiService, analytics, stepContr
|
|
|
785
796
|
pendingEmailCaptureNavigationRef.current = null;
|
|
786
797
|
return true;
|
|
787
798
|
},
|
|
788
|
-
persist: (normalizedEmail) =>
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
799
|
+
persist: async (normalizedEmail) => {
|
|
800
|
+
const result = await api.captureEmail({
|
|
801
|
+
userId: submittedUserId,
|
|
802
|
+
email: normalizedEmail,
|
|
803
|
+
environment: getRuntimeMode(),
|
|
804
|
+
answers: Object.assign({}, attributesRef.current),
|
|
805
|
+
});
|
|
806
|
+
answersCommittedRef.current = true;
|
|
807
|
+
clearFunnelAnswerDraft({
|
|
808
|
+
funnelId: FUNNEL_ID,
|
|
809
|
+
funnelVersionId: FUNNEL_VERSION_ID,
|
|
810
|
+
userId: submittedUserId,
|
|
811
|
+
});
|
|
812
|
+
clearFunnelAnswerDraft({
|
|
813
|
+
funnelId: FUNNEL_ID,
|
|
814
|
+
funnelVersionId: FUNNEL_VERSION_ID,
|
|
815
|
+
userId: initialRuntimeIdentity.clientUserId,
|
|
816
|
+
});
|
|
817
|
+
if (result.canonicalUser) {
|
|
818
|
+
clearFunnelAnswerDraft({
|
|
819
|
+
funnelId: FUNNEL_ID,
|
|
820
|
+
funnelVersionId: FUNNEL_VERSION_ID,
|
|
821
|
+
userId: result.canonicalUser.id,
|
|
822
|
+
});
|
|
823
|
+
}
|
|
824
|
+
return result;
|
|
825
|
+
},
|
|
793
826
|
applyUser: (capturedUser) => {
|
|
794
827
|
setIsReturningSubscriber(false);
|
|
795
828
|
currentUserIdRef.current = capturedUser.id;
|
|
@@ -855,6 +888,7 @@ export function useFunnelFlowController({ api = apiService, analytics, stepContr
|
|
|
855
888
|
api,
|
|
856
889
|
analytics,
|
|
857
890
|
claimActiveVisit,
|
|
891
|
+
initialRuntimeIdentity.clientUserId,
|
|
858
892
|
isFunnelStepId,
|
|
859
893
|
isPreviewRuntime,
|
|
860
894
|
navigateToStep,
|
|
@@ -59,7 +59,6 @@ export type EmailCaptureResult = {
|
|
|
59
59
|
activeSubscription: boolean;
|
|
60
60
|
};
|
|
61
61
|
declare class ApiService {
|
|
62
|
-
private readFileAsDataUrl;
|
|
63
62
|
getOrCreateClientUserId(): string;
|
|
64
63
|
persistCanonicalUserId(userId: string): string;
|
|
65
64
|
getSubscriptionManagementUserId(): string | null;
|
|
@@ -81,6 +80,7 @@ declare class ApiService {
|
|
|
81
80
|
userId: string;
|
|
82
81
|
email: string;
|
|
83
82
|
environment: 'test' | 'live';
|
|
83
|
+
answers?: FunnelUserAnswers;
|
|
84
84
|
}): Promise<EmailCaptureResult>;
|
|
85
85
|
syncUrlUserAttributes(input: {
|
|
86
86
|
user: AppUserInput;
|
|
@@ -194,20 +194,6 @@ const toAppUser = (input) => {
|
|
|
194
194
|
};
|
|
195
195
|
};
|
|
196
196
|
class ApiService {
|
|
197
|
-
readFileAsDataUrl(file) {
|
|
198
|
-
return new Promise((resolve, reject) => {
|
|
199
|
-
const reader = new FileReader();
|
|
200
|
-
reader.onload = () => {
|
|
201
|
-
if (typeof reader.result !== 'string' || !reader.result.trim()) {
|
|
202
|
-
reject(new Error('Failed to read image'));
|
|
203
|
-
return;
|
|
204
|
-
}
|
|
205
|
-
resolve(reader.result);
|
|
206
|
-
};
|
|
207
|
-
reader.onerror = () => reject(new Error('Failed to read image'));
|
|
208
|
-
reader.readAsDataURL(file);
|
|
209
|
-
});
|
|
210
|
-
}
|
|
211
197
|
getOrCreateClientUserId() {
|
|
212
198
|
const existing = readStoredUserId(FUNNEL_ID);
|
|
213
199
|
if (existing) {
|
|
@@ -267,8 +253,8 @@ class ApiService {
|
|
|
267
253
|
async bootstrapSession(input) {
|
|
268
254
|
var _a, _b, _c;
|
|
269
255
|
const urlUserAttributes = resolveUrlUserAttributes();
|
|
270
|
-
const inputEmail = (input === null || input === void 0 ? void 0 : input.email) ||
|
|
271
|
-
const inputName = (input === null || input === void 0 ? void 0 : input.name) ||
|
|
256
|
+
const inputEmail = (input === null || input === void 0 ? void 0 : input.email) || undefined;
|
|
257
|
+
const inputName = (input === null || input === void 0 ? void 0 : input.name) || undefined;
|
|
272
258
|
const candidateUserId = (input === null || input === void 0 ? void 0 : input.userId) || urlUserAttributes.userId;
|
|
273
259
|
const userId = await this.resolveBootstrapUserId(candidateUserId);
|
|
274
260
|
const publishableKey = getFunnelSdkPublishableKey();
|
|
@@ -358,6 +344,7 @@ class ApiService {
|
|
|
358
344
|
userId,
|
|
359
345
|
email,
|
|
360
346
|
environment: input.environment,
|
|
347
|
+
answers: input.answers,
|
|
361
348
|
});
|
|
362
349
|
const eventId = asString(payload.eventId);
|
|
363
350
|
const responseUser = isRecord(payload.user) ? payload.user : null;
|
|
@@ -465,8 +452,8 @@ class ApiService {
|
|
|
465
452
|
async uploadTempPhoto(input) {
|
|
466
453
|
const userId = persistUserId(input.userId || this.getOrCreateClientUserId(), FUNNEL_ID);
|
|
467
454
|
const publishableKey = getFunnelSdkPublishableKey();
|
|
468
|
-
const imageDataUrl = await this.readFileAsDataUrl(input.file);
|
|
469
455
|
if (!publishableKey) {
|
|
456
|
+
const imageDataUrl = URL.createObjectURL(input.file);
|
|
470
457
|
return {
|
|
471
458
|
key: `local_${userId}_${Date.now()}`,
|
|
472
459
|
publicUrl: imageDataUrl,
|
|
@@ -475,11 +462,11 @@ class ApiService {
|
|
|
475
462
|
uploadedAt: new Date().toISOString(),
|
|
476
463
|
};
|
|
477
464
|
}
|
|
478
|
-
const payload = await funnelSdkService.
|
|
465
|
+
const payload = await funnelSdkService.uploadPhotoFile({
|
|
479
466
|
userId,
|
|
480
467
|
fileName: input.file.name || 'capture.jpg',
|
|
481
|
-
|
|
482
|
-
|
|
468
|
+
contentType: input.file.type || undefined,
|
|
469
|
+
file: input.file,
|
|
483
470
|
source: input.source || 'upload',
|
|
484
471
|
});
|
|
485
472
|
const publicUrl = asString(payload.publicUrl);
|
|
@@ -27,6 +27,7 @@ export type FunnelSdkCaptureEmailInput = {
|
|
|
27
27
|
userId: string;
|
|
28
28
|
email: string;
|
|
29
29
|
environment: FunnelSdkRuntimeMode;
|
|
30
|
+
answers?: FunnelSdkJsonObject;
|
|
30
31
|
runtimeConfig?: FunnelSdkRuntimeConfigOverrides;
|
|
31
32
|
};
|
|
32
33
|
export type FunnelSdkCaptureEmailResponse = {
|
|
@@ -77,6 +78,25 @@ export type FunnelSdkUploadPhotoInput = {
|
|
|
77
78
|
userId?: string | null;
|
|
78
79
|
runtimeConfig?: FunnelSdkRuntimeConfigOverrides;
|
|
79
80
|
};
|
|
81
|
+
export type FunnelSdkUploadPhotoFileInput = {
|
|
82
|
+
file: Blob;
|
|
83
|
+
fileName: string;
|
|
84
|
+
contentType?: string | null;
|
|
85
|
+
source?: 'camera' | 'upload' | string | null;
|
|
86
|
+
userId?: string | null;
|
|
87
|
+
runtimeConfig?: FunnelSdkRuntimeConfigOverrides;
|
|
88
|
+
};
|
|
89
|
+
export type FunnelSdkPreparedPhotoUpload = {
|
|
90
|
+
key: string;
|
|
91
|
+
publicUrl: string;
|
|
92
|
+
uploadUrl: string;
|
|
93
|
+
uploadHeaders: Record<string, string>;
|
|
94
|
+
contentType: string;
|
|
95
|
+
sizeBytes: number;
|
|
96
|
+
};
|
|
97
|
+
export type FunnelSdkUploadedPhoto = Omit<FunnelSdkPreparedPhotoUpload, 'uploadUrl' | 'uploadHeaders'> & {
|
|
98
|
+
uploadedAt: string;
|
|
99
|
+
};
|
|
80
100
|
export type FunnelSdkCreateHostedCheckoutSessionInput = {
|
|
81
101
|
planId: string;
|
|
82
102
|
offerSetId?: string | null;
|
|
@@ -263,6 +283,10 @@ declare class FunnelSdkService {
|
|
|
263
283
|
claimUserSubscription<ResponsePayload = FunnelSdkJsonObject>(input: FunnelSdkClaimUserSubscriptionInput): Promise<ResponsePayload>;
|
|
264
284
|
updateFunnelUser<ResponsePayload = FunnelSdkJsonObject>(input: FunnelSdkUpdateFunnelUserInput): Promise<ResponsePayload>;
|
|
265
285
|
uploadPhoto<ResponsePayload = FunnelSdkJsonObject>(input: FunnelSdkUploadPhotoInput): Promise<ResponsePayload>;
|
|
286
|
+
preparePhotoUpload(input: Omit<FunnelSdkUploadPhotoFileInput, 'file'> & {
|
|
287
|
+
sizeBytes: number;
|
|
288
|
+
}): Promise<FunnelSdkPreparedPhotoUpload>;
|
|
289
|
+
uploadPhotoFile(input: FunnelSdkUploadPhotoFileInput): Promise<FunnelSdkUploadedPhoto>;
|
|
266
290
|
listPaymentPlans(input?: {
|
|
267
291
|
environment?: FunnelSdkRuntimeMode;
|
|
268
292
|
offerSetId?: string | null;
|
|
@@ -122,6 +122,7 @@ class FunnelSdkService {
|
|
|
122
122
|
user_id: input.userId,
|
|
123
123
|
email: input.email,
|
|
124
124
|
environment: input.environment,
|
|
125
|
+
answers: input.answers,
|
|
125
126
|
stepContractVersion: CURRENT_STEP_CONTRACT_VERSION,
|
|
126
127
|
},
|
|
127
128
|
errorMessage: 'Failed to capture email',
|
|
@@ -186,6 +187,48 @@ class FunnelSdkService {
|
|
|
186
187
|
errorMessage: 'Failed to upload photo',
|
|
187
188
|
});
|
|
188
189
|
}
|
|
190
|
+
async preparePhotoUpload(input) {
|
|
191
|
+
const { funnelId } = resolveRuntimeConfig(input.runtimeConfig);
|
|
192
|
+
return this.postJson('/sdk/public/uploads/photos/prepare', {
|
|
193
|
+
runtimeConfig: input.runtimeConfig,
|
|
194
|
+
body: {
|
|
195
|
+
funnelId,
|
|
196
|
+
userId: input.userId,
|
|
197
|
+
fileName: input.fileName,
|
|
198
|
+
contentType: input.contentType,
|
|
199
|
+
sizeBytes: input.sizeBytes,
|
|
200
|
+
source: input.source,
|
|
201
|
+
},
|
|
202
|
+
includeRuntimeFields: false,
|
|
203
|
+
includePublishableKey: false,
|
|
204
|
+
errorMessage: 'Failed to prepare photo upload',
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
async uploadPhotoFile(input) {
|
|
208
|
+
const prepared = await this.preparePhotoUpload({
|
|
209
|
+
fileName: input.fileName,
|
|
210
|
+
contentType: input.contentType,
|
|
211
|
+
sizeBytes: input.file.size,
|
|
212
|
+
source: input.source,
|
|
213
|
+
userId: input.userId,
|
|
214
|
+
runtimeConfig: input.runtimeConfig,
|
|
215
|
+
});
|
|
216
|
+
const response = await fetch(prepared.uploadUrl, {
|
|
217
|
+
method: 'PUT',
|
|
218
|
+
headers: prepared.uploadHeaders,
|
|
219
|
+
body: input.file,
|
|
220
|
+
});
|
|
221
|
+
if (!response.ok) {
|
|
222
|
+
throw new Error(`Failed to upload photo (${response.status})`);
|
|
223
|
+
}
|
|
224
|
+
return {
|
|
225
|
+
key: prepared.key,
|
|
226
|
+
publicUrl: prepared.publicUrl,
|
|
227
|
+
contentType: prepared.contentType,
|
|
228
|
+
sizeBytes: prepared.sizeBytes,
|
|
229
|
+
uploadedAt: new Date().toISOString(),
|
|
230
|
+
};
|
|
231
|
+
}
|
|
189
232
|
async listPaymentPlans(input = {}) {
|
|
190
233
|
const { funnelId } = resolveRuntimeConfig(input.runtimeConfig);
|
|
191
234
|
return this.getJson('/sdk/public/payments/plans', {
|