@funnelsgrove/runtime 0.1.4 → 0.1.6
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/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/runtime/url-user-attributes.d.ts +6 -0
- package/dist/runtime/url-user-attributes.js +30 -5
- package/dist/runtime/use-funnel-flow-controller.js +4 -0
- package/dist/runtime/use-url-user-attributes-sync.d.ts +13 -0
- package/dist/runtime/use-url-user-attributes-sync.js +54 -0
- package/dist/services/api.service.d.ts +4 -1
- package/dist/services/api.service.js +17 -51
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -17,6 +17,7 @@ export * from './runtime/preview-definition-overrides.js';
|
|
|
17
17
|
export * from './runtime/route-resolver.js';
|
|
18
18
|
export * from './runtime/subscription-handoff.js';
|
|
19
19
|
export * from './runtime/url-user-attributes.js';
|
|
20
|
+
export * from './runtime/use-url-user-attributes-sync.js';
|
|
20
21
|
export * from './services/api.service.js';
|
|
21
22
|
export * from './services/funnel-state.service.js';
|
|
22
23
|
export * from './services/logger.js';
|
package/dist/index.js
CHANGED
|
@@ -17,6 +17,7 @@ export * from './runtime/preview-definition-overrides.js';
|
|
|
17
17
|
export * from './runtime/route-resolver.js';
|
|
18
18
|
export * from './runtime/subscription-handoff.js';
|
|
19
19
|
export * from './runtime/url-user-attributes.js';
|
|
20
|
+
export * from './runtime/use-url-user-attributes-sync.js';
|
|
20
21
|
export * from './services/api.service.js';
|
|
21
22
|
export * from './services/funnel-state.service.js';
|
|
22
23
|
export * from './services/logger.js';
|
|
@@ -1,15 +1,21 @@
|
|
|
1
1
|
import type { FunnelUserAnswers } from '../sdk/userAnswers.js';
|
|
2
2
|
export type UrlUserAttributes = {
|
|
3
|
+
userId: string | null;
|
|
3
4
|
email: string | null;
|
|
5
|
+
name: string | null;
|
|
6
|
+
stripeCustomerId: string | null;
|
|
4
7
|
};
|
|
5
8
|
export type UrlUserAttributeUser = {
|
|
6
9
|
email: string;
|
|
10
|
+
name: string;
|
|
7
11
|
attributes?: FunnelUserAnswers;
|
|
8
12
|
document?: Record<string, unknown>;
|
|
9
13
|
};
|
|
10
14
|
export declare function normalizeUrlUserEmail(value: string | null | undefined): string | null;
|
|
15
|
+
export declare function normalizeUrlUserAttribute(value: string | null | undefined): string | null;
|
|
11
16
|
export declare function resolveUrlUserAttributes(search?: string): UrlUserAttributes;
|
|
12
17
|
export declare function hasUrlUserAttributes(urlAttributes: UrlUserAttributes | null | undefined): boolean;
|
|
18
|
+
export declare function hasUrlUserProfileAttributes(urlAttributes: UrlUserAttributes | null | undefined): boolean;
|
|
13
19
|
export declare function applyUrlUserAttributesToUser<TUser extends UrlUserAttributeUser>(input: {
|
|
14
20
|
user: TUser;
|
|
15
21
|
attributes?: FunnelUserAnswers;
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
const EMAIL_QUERY_KEY = 'email';
|
|
2
|
+
const USER_ID_QUERY_KEY = 'user_id';
|
|
3
|
+
const STRIPE_CUSTOMER_ID_QUERY_KEY = 'stripe_customer_id';
|
|
4
|
+
const NAME_QUERY_KEYS = ['name', 'fullName', 'full_name'];
|
|
2
5
|
const EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
3
6
|
export function normalizeUrlUserEmail(value) {
|
|
4
7
|
const email = value === null || value === void 0 ? void 0 : value.trim();
|
|
@@ -7,32 +10,54 @@ export function normalizeUrlUserEmail(value) {
|
|
|
7
10
|
}
|
|
8
11
|
return email;
|
|
9
12
|
}
|
|
13
|
+
export function normalizeUrlUserAttribute(value) {
|
|
14
|
+
const normalized = value === null || value === void 0 ? void 0 : value.trim();
|
|
15
|
+
return normalized || null;
|
|
16
|
+
}
|
|
10
17
|
export function resolveUrlUserAttributes(search) {
|
|
18
|
+
var _a;
|
|
11
19
|
if (typeof window === 'undefined' && typeof search !== 'string') {
|
|
12
20
|
return {
|
|
21
|
+
userId: null,
|
|
13
22
|
email: null,
|
|
23
|
+
name: null,
|
|
24
|
+
stripeCustomerId: null,
|
|
14
25
|
};
|
|
15
26
|
}
|
|
16
27
|
try {
|
|
17
28
|
const params = new URLSearchParams(search !== null && search !== void 0 ? search : window.location.search);
|
|
29
|
+
const name = (_a = NAME_QUERY_KEYS.map((key) => normalizeUrlUserAttribute(params.get(key))).find(Boolean)) !== null && _a !== void 0 ? _a : null;
|
|
18
30
|
return {
|
|
31
|
+
userId: normalizeUrlUserAttribute(params.get(USER_ID_QUERY_KEY)),
|
|
19
32
|
email: normalizeUrlUserEmail(params.get(EMAIL_QUERY_KEY)),
|
|
33
|
+
name,
|
|
34
|
+
stripeCustomerId: normalizeUrlUserAttribute(params.get(STRIPE_CUSTOMER_ID_QUERY_KEY)),
|
|
20
35
|
};
|
|
21
36
|
}
|
|
22
|
-
catch (
|
|
37
|
+
catch (_b) {
|
|
23
38
|
return {
|
|
39
|
+
userId: null,
|
|
24
40
|
email: null,
|
|
41
|
+
name: null,
|
|
42
|
+
stripeCustomerId: null,
|
|
25
43
|
};
|
|
26
44
|
}
|
|
27
45
|
}
|
|
28
46
|
export function hasUrlUserAttributes(urlAttributes) {
|
|
29
|
-
return Boolean(urlAttributes === null || urlAttributes === void 0 ? void 0 : urlAttributes.
|
|
47
|
+
return Boolean((urlAttributes === null || urlAttributes === void 0 ? void 0 : urlAttributes.userId) ||
|
|
48
|
+
(urlAttributes === null || urlAttributes === void 0 ? void 0 : urlAttributes.email) ||
|
|
49
|
+
(urlAttributes === null || urlAttributes === void 0 ? void 0 : urlAttributes.name) ||
|
|
50
|
+
(urlAttributes === null || urlAttributes === void 0 ? void 0 : urlAttributes.stripeCustomerId));
|
|
51
|
+
}
|
|
52
|
+
export function hasUrlUserProfileAttributes(urlAttributes) {
|
|
53
|
+
return Boolean((urlAttributes === null || urlAttributes === void 0 ? void 0 : urlAttributes.email) || (urlAttributes === null || urlAttributes === void 0 ? void 0 : urlAttributes.name));
|
|
30
54
|
}
|
|
31
55
|
export function applyUrlUserAttributesToUser(input) {
|
|
32
|
-
var _a, _b, _c, _d;
|
|
56
|
+
var _a, _b, _c, _d, _e;
|
|
33
57
|
const email = (_a = input.urlAttributes) === null || _a === void 0 ? void 0 : _a.email;
|
|
34
|
-
|
|
58
|
+
const name = (_b = input.urlAttributes) === null || _b === void 0 ? void 0 : _b.name;
|
|
59
|
+
if (!email && !name) {
|
|
35
60
|
return input.user;
|
|
36
61
|
}
|
|
37
|
-
return Object.assign(Object.assign({}, input.user), { email, attributes: Object.assign(Object.assign({}, ((
|
|
62
|
+
return Object.assign(Object.assign({}, input.user), { email: email !== null && email !== void 0 ? email : input.user.email, name: name !== null && name !== void 0 ? name : input.user.name, attributes: Object.assign(Object.assign({}, ((_c = input.attributes) !== null && _c !== void 0 ? _c : {})), ((_d = input.user.attributes) !== null && _d !== void 0 ? _d : {})), document: (_e = input.user.document) !== null && _e !== void 0 ? _e : {} });
|
|
38
63
|
}
|
|
@@ -9,6 +9,7 @@ import { isPreviewStepLockRequested, resolveNextStepFromContext, shouldRunAutoAd
|
|
|
9
9
|
import { usePreviewBridge } from './preview-bridge.js';
|
|
10
10
|
import { resolveExperimentAssignment } from './experiment-assignment.js';
|
|
11
11
|
import { collectCurrentFunnelAttribution } from './funnel-attribution.js';
|
|
12
|
+
import { resolveUrlUserAttributes } from './url-user-attributes.js';
|
|
12
13
|
import { bootstrapPostHog, getPostHog, identifyPostHog, isPostHogReady, resolveExperimentVariant, } from './posthog-flags.js';
|
|
13
14
|
import { isEditorEnabled, useRuntimeMode } from '../services/runtime-mode.service.js';
|
|
14
15
|
import { isPreviewFrameRuntime } from '../services/preview-frame.service.js';
|
|
@@ -244,6 +245,7 @@ export function useFunnelFlowController({ initialStepId, lockToInitialStep = fal
|
|
|
244
245
|
}, [goToStep, resolveNextStepId, safeActiveStepId]);
|
|
245
246
|
useEffect(() => {
|
|
246
247
|
const localUserId = apiService.getOrCreateClientUserId();
|
|
248
|
+
const urlUserAttributes = resolveUrlUserAttributes();
|
|
247
249
|
const bootstrapCandidateUserId = apiService.getBootstrapCandidateUserId(localUserId);
|
|
248
250
|
const initialAttribution = collectCurrentFunnelAttribution();
|
|
249
251
|
setUser((prev) => (Object.assign(Object.assign({}, prev), { id: localUserId })));
|
|
@@ -263,6 +265,8 @@ export function useFunnelFlowController({ initialStepId, lockToInitialStep = fal
|
|
|
263
265
|
const sessionBootstrap = apiService
|
|
264
266
|
.bootstrapSession({
|
|
265
267
|
userId: bootstrapCandidateUserId || localUserId,
|
|
268
|
+
email: urlUserAttributes.email || undefined,
|
|
269
|
+
name: urlUserAttributes.name || undefined,
|
|
266
270
|
attribution: initialAttribution,
|
|
267
271
|
})
|
|
268
272
|
.then((sessionUser) => {
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { FunnelUser } from '../components/FunnelContext.js';
|
|
2
|
+
import type { FunnelUserAnswers } from '../sdk/userAnswers.js';
|
|
3
|
+
import { type FunnelUserAttribution } from './funnel-attribution.js';
|
|
4
|
+
import { type UrlUserAttributes } from './url-user-attributes.js';
|
|
5
|
+
export type UseUrlUserAttributesSyncInput = {
|
|
6
|
+
user: FunnelUser;
|
|
7
|
+
attributes: FunnelUserAnswers;
|
|
8
|
+
setUser: (user: FunnelUser) => void;
|
|
9
|
+
disabled?: boolean;
|
|
10
|
+
urlAttributes?: UrlUserAttributes | null;
|
|
11
|
+
getAttribution?: () => FunnelUserAttribution;
|
|
12
|
+
};
|
|
13
|
+
export declare function useUrlUserAttributesSync({ user, attributes, setUser, disabled, urlAttributes, getAttribution, }: UseUrlUserAttributesSyncInput): UrlUserAttributes;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { useEffect, useMemo, useRef } from 'react';
|
|
3
|
+
import { apiService } from '../services/api.service.js';
|
|
4
|
+
import { logger } from '../services/logger.js';
|
|
5
|
+
import { collectCurrentFunnelAttribution, } from './funnel-attribution.js';
|
|
6
|
+
import { applyUrlUserAttributesToUser, hasUrlUserProfileAttributes, resolveUrlUserAttributes, } from './url-user-attributes.js';
|
|
7
|
+
export function useUrlUserAttributesSync({ user, attributes, setUser, disabled = false, urlAttributes, getAttribution = collectCurrentFunnelAttribution, }) {
|
|
8
|
+
const resolvedUrlAttributes = useMemo(() => urlAttributes !== null && urlAttributes !== void 0 ? urlAttributes : resolveUrlUserAttributes(), [urlAttributes]);
|
|
9
|
+
const syncedUrlUserAttributesRef = useRef(null);
|
|
10
|
+
useEffect(() => {
|
|
11
|
+
var _a, _b;
|
|
12
|
+
if (disabled || !hasUrlUserProfileAttributes(resolvedUrlAttributes)) {
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
const nextUser = applyUrlUserAttributesToUser({
|
|
16
|
+
user,
|
|
17
|
+
attributes,
|
|
18
|
+
urlAttributes: resolvedUrlAttributes,
|
|
19
|
+
});
|
|
20
|
+
const currentEmail = user.email.trim();
|
|
21
|
+
const nextEmail = nextUser.email.trim();
|
|
22
|
+
const currentName = user.name.trim();
|
|
23
|
+
const nextName = nextUser.name.trim();
|
|
24
|
+
if (currentEmail !== nextEmail || currentName !== nextName) {
|
|
25
|
+
setUser(nextUser);
|
|
26
|
+
}
|
|
27
|
+
const syncKey = [
|
|
28
|
+
nextUser.id,
|
|
29
|
+
(_a = resolvedUrlAttributes.email) !== null && _a !== void 0 ? _a : '',
|
|
30
|
+
(_b = resolvedUrlAttributes.name) !== null && _b !== void 0 ? _b : '',
|
|
31
|
+
].join(':');
|
|
32
|
+
if (syncedUrlUserAttributesRef.current === syncKey) {
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
syncedUrlUserAttributesRef.current = syncKey;
|
|
36
|
+
apiService
|
|
37
|
+
.syncUrlUserAttributes({
|
|
38
|
+
user: nextUser,
|
|
39
|
+
attributes,
|
|
40
|
+
urlAttributes: resolvedUrlAttributes,
|
|
41
|
+
attribution: getAttribution(),
|
|
42
|
+
})
|
|
43
|
+
.then((updatedUser) => {
|
|
44
|
+
if (updatedUser) {
|
|
45
|
+
setUser(updatedUser);
|
|
46
|
+
}
|
|
47
|
+
})
|
|
48
|
+
.catch((error) => {
|
|
49
|
+
syncedUrlUserAttributesRef.current = null;
|
|
50
|
+
logger.error('Failed to persist URL attributes onto funnel user:', error);
|
|
51
|
+
});
|
|
52
|
+
}, [attributes, disabled, getAttribution, resolvedUrlAttributes, setUser, user]);
|
|
53
|
+
return resolvedUrlAttributes;
|
|
54
|
+
}
|
|
@@ -8,6 +8,9 @@ export type AppUser = {
|
|
|
8
8
|
attributes: FunnelUserAnswers;
|
|
9
9
|
document: Record<string, unknown>;
|
|
10
10
|
};
|
|
11
|
+
type AppUserInput = Omit<AppUser, 'document'> & {
|
|
12
|
+
document?: Record<string, unknown>;
|
|
13
|
+
};
|
|
11
14
|
export type FunnelEvent = {
|
|
12
15
|
userId: string;
|
|
13
16
|
eventType: string;
|
|
@@ -76,7 +79,7 @@ declare class ApiService {
|
|
|
76
79
|
attribution?: FunnelUserAttribution;
|
|
77
80
|
}): Promise<AppUser>;
|
|
78
81
|
syncUrlUserAttributes(input: {
|
|
79
|
-
user:
|
|
82
|
+
user: AppUserInput;
|
|
80
83
|
attributes?: FunnelUserAnswers;
|
|
81
84
|
urlAttributes?: UrlUserAttributes | null;
|
|
82
85
|
attribution?: FunnelUserAttribution;
|
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
import { buildMainApiUrl, buildSdkHeaders, FUNNEL_ID, FUNNEL_VERSION_ID, getFunnelSdkPublishableKey, } from './runtime-api.config.js';
|
|
2
2
|
import { mergeFunnelUserAttribution, } from '../runtime/funnel-attribution.js';
|
|
3
|
-
import { applyUrlUserAttributesToUser,
|
|
3
|
+
import { applyUrlUserAttributesToUser, hasUrlUserProfileAttributes, resolveUrlUserAttributes, } from '../runtime/url-user-attributes.js';
|
|
4
4
|
const DEFAULT_USER_ID_STORAGE_KEY = 'funnel:user-id';
|
|
5
|
-
const LOCATION_USER_ID_QUERY_KEYS = ['user_id'];
|
|
6
|
-
const LOCATION_STRIPE_CUSTOMER_ID_QUERY_KEYS = ['stripe_customer_id'];
|
|
7
5
|
const canUseDom = () => {
|
|
8
6
|
return typeof window !== 'undefined';
|
|
9
7
|
};
|
|
@@ -63,42 +61,6 @@ const persistUserId = (value, funnelId = FUNNEL_ID) => {
|
|
|
63
61
|
}
|
|
64
62
|
return normalized;
|
|
65
63
|
};
|
|
66
|
-
const readLocationUserId = () => {
|
|
67
|
-
if (!canUseDom()) {
|
|
68
|
-
return null;
|
|
69
|
-
}
|
|
70
|
-
try {
|
|
71
|
-
const searchParams = new URLSearchParams(window.location.search);
|
|
72
|
-
for (const key of LOCATION_USER_ID_QUERY_KEYS) {
|
|
73
|
-
const candidate = normalizeUserId(searchParams.get(key));
|
|
74
|
-
if (candidate) {
|
|
75
|
-
return candidate;
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
catch (_a) {
|
|
80
|
-
return null;
|
|
81
|
-
}
|
|
82
|
-
return null;
|
|
83
|
-
};
|
|
84
|
-
const readLocationStripeCustomerId = () => {
|
|
85
|
-
if (!canUseDom()) {
|
|
86
|
-
return null;
|
|
87
|
-
}
|
|
88
|
-
try {
|
|
89
|
-
const searchParams = new URLSearchParams(window.location.search);
|
|
90
|
-
for (const key of LOCATION_STRIPE_CUSTOMER_ID_QUERY_KEYS) {
|
|
91
|
-
const candidate = asString(searchParams.get(key));
|
|
92
|
-
if (candidate) {
|
|
93
|
-
return candidate;
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
catch (_a) {
|
|
98
|
-
return null;
|
|
99
|
-
}
|
|
100
|
-
return null;
|
|
101
|
-
};
|
|
102
64
|
const buildSdkEvent = (event) => {
|
|
103
65
|
return {
|
|
104
66
|
eventType: event.eventType,
|
|
@@ -178,7 +140,7 @@ class ApiService {
|
|
|
178
140
|
return persistUserId(generateUserId(), FUNNEL_ID);
|
|
179
141
|
}
|
|
180
142
|
getSubscriptionManagementUserId() {
|
|
181
|
-
const locationUserId =
|
|
143
|
+
const locationUserId = resolveUrlUserAttributes().userId;
|
|
182
144
|
if (locationUserId) {
|
|
183
145
|
return persistUserId(locationUserId, FUNNEL_ID);
|
|
184
146
|
}
|
|
@@ -188,10 +150,10 @@ class ApiService {
|
|
|
188
150
|
return this.getOrCreateClientUserId();
|
|
189
151
|
}
|
|
190
152
|
getSubscriptionManagementStripeCustomerId() {
|
|
191
|
-
return
|
|
153
|
+
return resolveUrlUserAttributes().stripeCustomerId;
|
|
192
154
|
}
|
|
193
155
|
getBootstrapCandidateUserId(fallbackUserId) {
|
|
194
|
-
return
|
|
156
|
+
return resolveUrlUserAttributes().userId || normalizeUserId(fallbackUserId);
|
|
195
157
|
}
|
|
196
158
|
async fetchCurrentUserState(userId) {
|
|
197
159
|
const publishableKey = getFunnelSdkPublishableKey();
|
|
@@ -236,13 +198,16 @@ class ApiService {
|
|
|
236
198
|
}
|
|
237
199
|
async bootstrapSession(input) {
|
|
238
200
|
var _a;
|
|
239
|
-
const
|
|
201
|
+
const urlUserAttributes = resolveUrlUserAttributes();
|
|
202
|
+
const inputEmail = (input === null || input === void 0 ? void 0 : input.email) || urlUserAttributes.email || undefined;
|
|
203
|
+
const inputName = (input === null || input === void 0 ? void 0 : input.name) || urlUserAttributes.name || undefined;
|
|
204
|
+
const userId = await this.resolveBootstrapUserId((input === null || input === void 0 ? void 0 : input.userId) || urlUserAttributes.userId);
|
|
240
205
|
const publishableKey = getFunnelSdkPublishableKey();
|
|
241
206
|
if (!publishableKey) {
|
|
242
207
|
return toAppUser({
|
|
243
208
|
userId,
|
|
244
|
-
fallbackName:
|
|
245
|
-
fallbackEmail:
|
|
209
|
+
fallbackName: inputName,
|
|
210
|
+
fallbackEmail: inputEmail,
|
|
246
211
|
fallbackDocument: withMergedAttributionDocument({}, input === null || input === void 0 ? void 0 : input.attribution),
|
|
247
212
|
});
|
|
248
213
|
}
|
|
@@ -256,8 +221,8 @@ class ApiService {
|
|
|
256
221
|
funnelId: FUNNEL_ID || undefined,
|
|
257
222
|
funnelVersionId: FUNNEL_VERSION_ID || undefined,
|
|
258
223
|
user_id: userId,
|
|
259
|
-
email:
|
|
260
|
-
fullName:
|
|
224
|
+
email: inputEmail,
|
|
225
|
+
fullName: inputName,
|
|
261
226
|
metadata: {
|
|
262
227
|
source: 'funnel-session',
|
|
263
228
|
},
|
|
@@ -275,8 +240,8 @@ class ApiService {
|
|
|
275
240
|
return toAppUser({
|
|
276
241
|
apiUser: payload.user,
|
|
277
242
|
userId: persistedUserId,
|
|
278
|
-
fallbackName:
|
|
279
|
-
fallbackEmail:
|
|
243
|
+
fallbackName: inputName,
|
|
244
|
+
fallbackEmail: inputEmail,
|
|
280
245
|
fallbackDocument: withMergedAttributionDocument({}, input === null || input === void 0 ? void 0 : input.attribution),
|
|
281
246
|
});
|
|
282
247
|
}
|
|
@@ -328,7 +293,8 @@ class ApiService {
|
|
|
328
293
|
});
|
|
329
294
|
}
|
|
330
295
|
async syncUrlUserAttributes(input) {
|
|
331
|
-
|
|
296
|
+
var _a;
|
|
297
|
+
if (!hasUrlUserProfileAttributes(input.urlAttributes)) {
|
|
332
298
|
return null;
|
|
333
299
|
}
|
|
334
300
|
const nextUser = applyUrlUserAttributesToUser({
|
|
@@ -336,7 +302,7 @@ class ApiService {
|
|
|
336
302
|
attributes: input.attributes,
|
|
337
303
|
urlAttributes: input.urlAttributes,
|
|
338
304
|
});
|
|
339
|
-
const updatedUser = await this.updateUser(nextUser, {
|
|
305
|
+
const updatedUser = await this.updateUser(Object.assign(Object.assign({}, nextUser), { document: (_a = nextUser.document) !== null && _a !== void 0 ? _a : {} }), {
|
|
340
306
|
attribution: input.attribution,
|
|
341
307
|
});
|
|
342
308
|
return applyUrlUserAttributesToUser({
|