@funnelsgrove/runtime 0.1.5 → 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 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 (_a) {
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.email);
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
- if (!email) {
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({}, ((_b = input.attributes) !== null && _b !== void 0 ? _b : {})), ((_c = input.user.attributes) !== null && _c !== void 0 ? _c : {})), document: (_d = input.user.document) !== null && _d !== void 0 ? _d : {} });
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
+ }
@@ -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, hasUrlUserAttributes, } from '../runtime/url-user-attributes.js';
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 = readLocationUserId();
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 readLocationStripeCustomerId();
153
+ return resolveUrlUserAttributes().stripeCustomerId;
192
154
  }
193
155
  getBootstrapCandidateUserId(fallbackUserId) {
194
- return readLocationUserId() || normalizeUserId(fallbackUserId);
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 userId = await this.resolveBootstrapUserId(input === null || input === void 0 ? void 0 : input.userId);
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: input === null || input === void 0 ? void 0 : input.name,
245
- fallbackEmail: input === null || input === void 0 ? void 0 : input.email,
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: (input === null || input === void 0 ? void 0 : input.email) || undefined,
260
- fullName: (input === null || input === void 0 ? void 0 : input.name) || undefined,
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: input === null || input === void 0 ? void 0 : input.name,
279
- fallbackEmail: input === null || input === void 0 ? void 0 : input.email,
243
+ fallbackName: inputName,
244
+ fallbackEmail: inputEmail,
280
245
  fallbackDocument: withMergedAttributionDocument({}, input === null || input === void 0 ? void 0 : input.attribution),
281
246
  });
282
247
  }
@@ -329,7 +294,7 @@ class ApiService {
329
294
  }
330
295
  async syncUrlUserAttributes(input) {
331
296
  var _a;
332
- if (!hasUrlUserAttributes(input.urlAttributes)) {
297
+ if (!hasUrlUserProfileAttributes(input.urlAttributes)) {
333
298
  return null;
334
299
  }
335
300
  const nextUser = applyUrlUserAttributesToUser({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@funnelsgrove/runtime",
3
- "version": "0.1.5",
3
+ "version": "0.1.6",
4
4
  "type": "module",
5
5
  "private": false,
6
6
  "main": "./dist/index.js",