@funnelsgrove/runtime 0.7.2 → 0.7.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/README.md +4 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/runtime/browser-helpers.d.ts +2 -0
- package/dist/runtime/browser-helpers.js +9 -0
- package/dist/runtime/use-browser-location-search.d.ts +2 -0
- package/dist/runtime/use-browser-location-search.js +20 -0
- package/dist/runtime/use-funnel-flow-controller.js +2 -1
- package/dist/services/funnel-runtime-config.d.ts +2 -4
- package/dist/services/funnel-runtime-config.js +13 -35
- package/dist/services/funnel-sdk.service.d.ts +2 -0
- package/dist/services/funnel-sdk.service.js +2 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -20,6 +20,7 @@ documented in [`docs/product-specs/funnel-template-runtime.md`](../../docs/produ
|
|
|
20
20
|
- funnel context, flow controller, and generic runtime UI primitives
|
|
21
21
|
- funnel-scoped state/storage helpers
|
|
22
22
|
- browser-safe API client helpers used by funnels
|
|
23
|
+
- reactive browser query-string access through `useBrowserLocationSearch`
|
|
23
24
|
- subscription handoff and subscription management screens whose copy stays funnel-local
|
|
24
25
|
- test-mode developer info surfaces for funnel preview tooling
|
|
25
26
|
|
|
@@ -54,6 +55,9 @@ documented in [`docs/product-specs/funnel-template-runtime.md`](../../docs/produ
|
|
|
54
55
|
- Do not import `@funnelsgrove/payments` or `@funnelsgrove/analytics` here for checkout or transport orchestration.
|
|
55
56
|
- Runtime UI must be generic and copy-injectable.
|
|
56
57
|
- Browser-only values must resolve after mount when SSR hydration can be affected.
|
|
58
|
+
- Funnel code must not copy `useSyncExternalStore`/`popstate` query-string hooks. Use
|
|
59
|
+
`useBrowserLocationSearch`; the shared controller also notifies it after
|
|
60
|
+
`history.pushState` and `history.replaceState` navigation.
|
|
57
61
|
- Keep env mapping centralized in `config/env.config.ts`.
|
|
58
62
|
|
|
59
63
|
## Build, Test, Publish
|
package/dist/index.d.ts
CHANGED
|
@@ -18,6 +18,7 @@ export * from './runtime/funnel-runtime.js';
|
|
|
18
18
|
export type { FunnelNavigationOutcome, FunnelStepExitReason, } from './runtime/funnel-step-lifecycle.js';
|
|
19
19
|
export * from './runtime/posthog-flags.js';
|
|
20
20
|
export * from './runtime/use-funnel-flow-controller.js';
|
|
21
|
+
export * from './runtime/use-browser-location-search.js';
|
|
21
22
|
export { useStepChoices } from './runtime/use-step-choices.js';
|
|
22
23
|
export type { FunnelStepChoices } from './runtime/use-step-choices.js';
|
|
23
24
|
export * from './runtime/preview-bridge.js';
|
package/dist/index.js
CHANGED
|
@@ -20,6 +20,7 @@ export * from './runtime/funnel-step-metadata.validation.js';
|
|
|
20
20
|
export * from './runtime/funnel-runtime.js';
|
|
21
21
|
export * from './runtime/posthog-flags.js';
|
|
22
22
|
export * from './runtime/use-funnel-flow-controller.js';
|
|
23
|
+
export * from './runtime/use-browser-location-search.js';
|
|
23
24
|
export { useStepChoices } from './runtime/use-step-choices.js';
|
|
24
25
|
export * from './runtime/preview-bridge.js';
|
|
25
26
|
export * from './runtime/preview-definition-overrides.js';
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
export declare const canUseDom: () => boolean;
|
|
2
|
+
export declare const FUNNEL_LOCATION_CHANGE_EVENT = "funnel:location-change";
|
|
2
3
|
export declare const readWindowStorageValue: (storageKey: string) => string | null;
|
|
3
4
|
export declare const writeWindowStorageValue: (storageKey: string, value: string) => void;
|
|
4
5
|
export declare const removeWindowStorageValue: (storageKey: string) => void;
|
|
5
6
|
export declare const dispatchWindowCustomEvent: <Detail>(eventType: string, detail: Detail) => void;
|
|
7
|
+
export declare const dispatchFunnelLocationChange: () => void;
|
|
6
8
|
export declare const buildHostedStepLocation: (input: {
|
|
7
9
|
currentHref: string;
|
|
8
10
|
stepPath: string;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export const canUseDom = () => {
|
|
2
2
|
return typeof window !== 'undefined';
|
|
3
3
|
};
|
|
4
|
+
export const FUNNEL_LOCATION_CHANGE_EVENT = 'funnel:location-change';
|
|
4
5
|
export const readWindowStorageValue = (storageKey) => {
|
|
5
6
|
if (!canUseDom()) {
|
|
6
7
|
return null;
|
|
@@ -42,6 +43,14 @@ export const dispatchWindowCustomEvent = (eventType, detail) => {
|
|
|
42
43
|
detail,
|
|
43
44
|
}));
|
|
44
45
|
};
|
|
46
|
+
export const dispatchFunnelLocationChange = () => {
|
|
47
|
+
if (!canUseDom()) {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
dispatchWindowCustomEvent(FUNNEL_LOCATION_CHANGE_EVENT, {
|
|
51
|
+
href: window.location.href,
|
|
52
|
+
});
|
|
53
|
+
};
|
|
45
54
|
const getHostedFunnelBaseSegments = (pathname) => {
|
|
46
55
|
const pathSegments = pathname.split('/').filter(Boolean);
|
|
47
56
|
if (pathSegments[0] === 'published' && pathSegments[1] === 'f' && pathSegments[2]) {
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { useSyncExternalStore } from 'react';
|
|
3
|
+
import { FUNNEL_LOCATION_CHANGE_EVENT } from './browser-helpers.js';
|
|
4
|
+
const subscribeToBrowserLocationSearch = (onStoreChange) => {
|
|
5
|
+
if (typeof window === 'undefined') {
|
|
6
|
+
return () => undefined;
|
|
7
|
+
}
|
|
8
|
+
window.addEventListener('popstate', onStoreChange);
|
|
9
|
+
window.addEventListener(FUNNEL_LOCATION_CHANGE_EVENT, onStoreChange);
|
|
10
|
+
return () => {
|
|
11
|
+
window.removeEventListener('popstate', onStoreChange);
|
|
12
|
+
window.removeEventListener(FUNNEL_LOCATION_CHANGE_EVENT, onStoreChange);
|
|
13
|
+
};
|
|
14
|
+
};
|
|
15
|
+
export const getBrowserLocationSearch = () => {
|
|
16
|
+
return typeof window === 'undefined' ? '' : window.location.search;
|
|
17
|
+
};
|
|
18
|
+
export const useBrowserLocationSearch = () => {
|
|
19
|
+
return useSyncExternalStore(subscribeToBrowserLocationSearch, getBrowserLocationSearch, () => '');
|
|
20
|
+
};
|
|
@@ -4,7 +4,7 @@ import { writeStepChoice } from '../sdk/userAnswers.js';
|
|
|
4
4
|
import { apiService } from '../services/api.service.js';
|
|
5
5
|
import { logger } from '../services/logger.js';
|
|
6
6
|
import { FUNNEL_ID, POSTHOG_API_HOST, POSTHOG_PROJECT_API_KEY, PROJECT_ID, } from '../services/runtime-api.config.js';
|
|
7
|
-
import { buildHostedStepLocation, dispatchWindowCustomEvent, } from './browser-helpers.js';
|
|
7
|
+
import { buildHostedStepLocation, dispatchFunnelLocationChange, dispatchWindowCustomEvent, } from './browser-helpers.js';
|
|
8
8
|
import { isPreviewStepLockRequested, resolveNextStepFromContext, shouldRunAutoAdvanceTimer, } from './funnel-flow.js';
|
|
9
9
|
import { usePreviewBridge } from './preview-bridge.js';
|
|
10
10
|
import { resolveExperimentAssignment, resolveForcedExperimentAssignment, } from './experiment-assignment.js';
|
|
@@ -422,6 +422,7 @@ export function useFunnelFlowController({ api = apiService, analytics, stepContr
|
|
|
422
422
|
else {
|
|
423
423
|
window.history.pushState({ stepId: safeStepId }, '', nextLocation);
|
|
424
424
|
}
|
|
425
|
+
dispatchFunnelLocationChange();
|
|
425
426
|
}
|
|
426
427
|
setActiveStepId(safeStepId);
|
|
427
428
|
}, [getPathForStep]);
|
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
import type { FunnelExperimentDefinition } from '../config/funnel.experiments.types.js';
|
|
2
2
|
import type { GeneratedOfferSet } from '../runtime/offer-set-runtime.js';
|
|
3
3
|
export type FunnelRuntimeConfig = {
|
|
4
|
-
schemaVersion:
|
|
4
|
+
schemaVersion: 2;
|
|
5
5
|
funnelId: string;
|
|
6
6
|
projectId: string;
|
|
7
|
-
/** Present only on historical schema-v1 artifacts. */
|
|
8
|
-
plans?: unknown[];
|
|
9
7
|
offerSets: unknown[];
|
|
10
8
|
experiments: unknown[];
|
|
11
9
|
};
|
|
@@ -21,7 +19,7 @@ export type PublishedFunnelRuntimeConfig = {
|
|
|
21
19
|
revisionId: string;
|
|
22
20
|
sourceDeploymentId: string;
|
|
23
21
|
sourceVersionId: string;
|
|
24
|
-
schemaVersion:
|
|
22
|
+
schemaVersion: 2;
|
|
25
23
|
publishedAt: string;
|
|
26
24
|
config: FunnelRuntimeConfig;
|
|
27
25
|
};
|
|
@@ -25,20 +25,7 @@ const deepFreeze = (value) => {
|
|
|
25
25
|
}
|
|
26
26
|
return value;
|
|
27
27
|
};
|
|
28
|
-
const
|
|
29
|
-
var _a;
|
|
30
|
-
return (projectBillingPlanId
|
|
31
|
-
? (_a = plans.find((plan) => pickString(plan, 'id') === projectBillingPlanId)) !== null && _a !== void 0 ? _a : null
|
|
32
|
-
: null);
|
|
33
|
-
};
|
|
34
|
-
const getLegacyProviderMapping = (plan, mode) => {
|
|
35
|
-
var _a;
|
|
36
|
-
return ((_a = readRecordArray(plan === null || plan === void 0 ? void 0 : plan.providerMappings)
|
|
37
|
-
.find((mapping) => (pickString(mapping, 'environment') === mode
|
|
38
|
-
&& pickBoolean(mapping, 'isActive', 'is_active') !== false))) !== null && _a !== void 0 ? _a : null);
|
|
39
|
-
};
|
|
40
|
-
const buildPlanModeConfig = (item, mode, legacyPlan = null) => {
|
|
41
|
-
var _a, _b;
|
|
28
|
+
const buildPlanModeConfig = (item, mode) => {
|
|
42
29
|
const metadata = isRecord(item.metadata) ? item.metadata : {};
|
|
43
30
|
const directMappings = isRecord(item.providerMappingsByMode)
|
|
44
31
|
? item.providerMappingsByMode
|
|
@@ -51,20 +38,16 @@ const buildPlanModeConfig = (item, mode, legacyPlan = null) => {
|
|
|
51
38
|
: isRecord(metadataMappings[mode])
|
|
52
39
|
? metadataMappings[mode]
|
|
53
40
|
: {};
|
|
54
|
-
const legacyMapping = getLegacyProviderMapping(legacyPlan, mode);
|
|
55
41
|
const prefix = mode === 'test' ? 'test' : 'live';
|
|
56
42
|
const providerPlanId = (pickString(item, `${prefix}ProviderPlanId`, `${prefix}_provider_plan_id`)
|
|
57
|
-
|| pickString(modeMapping, 'providerPlanId')
|
|
58
|
-
|| (legacyMapping ? pickString(legacyMapping, 'providerPlanId', 'provider_plan_id') : null));
|
|
43
|
+
|| pickString(modeMapping, 'providerPlanId'));
|
|
59
44
|
if (!providerPlanId) {
|
|
60
45
|
return null;
|
|
61
46
|
}
|
|
62
|
-
const amountMinor =
|
|
63
|
-
const interval = pickString(modeMapping, 'billingInterval')
|
|
64
|
-
|
|
65
|
-
const
|
|
66
|
-
const currency = pickString(modeMapping, 'currency')
|
|
67
|
-
|| (legacyMapping ? pickString(legacyMapping, 'currency') : null);
|
|
47
|
+
const amountMinor = pickNumber(modeMapping, 'amountCents');
|
|
48
|
+
const interval = pickString(modeMapping, 'billingInterval');
|
|
49
|
+
const intervalCount = pickNumber(modeMapping, 'billingIntervalCount');
|
|
50
|
+
const currency = pickString(modeMapping, 'currency');
|
|
68
51
|
const amountMajor = amountMinor === null ? null : amountMinor / 100;
|
|
69
52
|
const priceLabel = pickString(modeMapping, 'priceLabel') || (amountMajor === null
|
|
70
53
|
? null
|
|
@@ -90,7 +73,6 @@ const buildPlanModeConfig = (item, mode, legacyPlan = null) => {
|
|
|
90
73
|
}).format(amountMajor)} today` })), (interval ? { billingInterval: interval } : {})), (intervalCount === null ? {} : { billingIntervalCount: intervalCount }));
|
|
91
74
|
};
|
|
92
75
|
const normalizePublishedOfferSets = (config) => {
|
|
93
|
-
const legacyPlans = readRecordArray(config.plans);
|
|
94
76
|
return readRecordArray(config.offerSets).map((offerSet) => {
|
|
95
77
|
var _a;
|
|
96
78
|
const key = pickString(offerSet, 'key');
|
|
@@ -105,15 +87,12 @@ const normalizePublishedOfferSets = (config) => {
|
|
|
105
87
|
throw new Error(`Published runtime offer set "${key}" has an item without a plan key`);
|
|
106
88
|
}
|
|
107
89
|
const projectBillingPlanId = pickString(item, 'projectBillingPlanId', 'project_billing_plan_id');
|
|
108
|
-
const
|
|
109
|
-
const
|
|
110
|
-
const liveMapping = buildPlanModeConfig(item, 'live', legacyPlan);
|
|
90
|
+
const testMapping = buildPlanModeConfig(item, 'test');
|
|
91
|
+
const liveMapping = buildPlanModeConfig(item, 'live');
|
|
111
92
|
const title = pickString(item, 'title', 'displayNameOverride', 'display_name_override')
|
|
112
|
-
|| pickString(metadata, 'title', 'publicPlanName', 'public_plan_name')
|
|
113
|
-
|| (legacyPlan ? pickString(legacyPlan, 'displayName', 'display_name') : null);
|
|
93
|
+
|| pickString(metadata, 'title', 'publicPlanName', 'public_plan_name');
|
|
114
94
|
const description = pickString(item, 'description')
|
|
115
|
-
|| pickString(metadata, 'description')
|
|
116
|
-
|| (legacyPlan ? pickString(legacyPlan, 'description') : null);
|
|
95
|
+
|| pickString(metadata, 'description');
|
|
117
96
|
return Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({ funnelPlanKey }, (pickString(metadata, 'runtimePlanKey', 'planKey', 'billingPlanKey')
|
|
118
97
|
? { runtimePlanKey: pickString(metadata, 'runtimePlanKey', 'planKey', 'billingPlanKey') }
|
|
119
98
|
: {})), (projectBillingPlanId ? { projectBillingPlanId } : {})), (pickString(item, 'testProviderPlanId', 'test_provider_plan_id')
|
|
@@ -251,14 +230,13 @@ const parsePublishedRuntimeConfig = (value) => {
|
|
|
251
230
|
if (typeof value.revisionId !== 'string'
|
|
252
231
|
|| typeof value.sourceDeploymentId !== 'string'
|
|
253
232
|
|| typeof value.sourceVersionId !== 'string'
|
|
254
|
-
||
|
|
233
|
+
|| value.schemaVersion !== 2
|
|
255
234
|
|| typeof value.publishedAt !== 'string'
|
|
256
|
-
||
|
|
235
|
+
|| schemaVersion !== 2
|
|
257
236
|
|| typeof config.funnelId !== 'string'
|
|
258
237
|
|| typeof config.projectId !== 'string'
|
|
259
238
|
|| !Array.isArray(config.offerSets)
|
|
260
|
-
|| !Array.isArray(config.experiments)
|
|
261
|
-
|| (schemaVersion === 1 && !Array.isArray(config.plans))) {
|
|
239
|
+
|| !Array.isArray(config.experiments)) {
|
|
262
240
|
throw new Error('Funnel runtime config response is invalid');
|
|
263
241
|
}
|
|
264
242
|
return value;
|
|
@@ -212,10 +212,12 @@ export type FunnelSdkCreateOneTimePaymentIntentResponse = {
|
|
|
212
212
|
};
|
|
213
213
|
export type FunnelSdkChargeOneClickPaymentInput = {
|
|
214
214
|
planId: string;
|
|
215
|
+
providerPlanId?: string | null;
|
|
215
216
|
amountCents: number;
|
|
216
217
|
analyticsMetadata?: Record<string, unknown> | null;
|
|
217
218
|
checkoutSessionId?: string | null;
|
|
218
219
|
currency?: string | null;
|
|
220
|
+
description?: string | null;
|
|
219
221
|
environment?: FunnelSdkRuntimeMode;
|
|
220
222
|
idempotencyKey?: string | null;
|
|
221
223
|
runtimeConfig?: FunnelSdkRuntimeConfigOverrides;
|
|
@@ -329,8 +329,10 @@ class FunnelSdkService {
|
|
|
329
329
|
runtimeConfig: input.runtimeConfig,
|
|
330
330
|
body: {
|
|
331
331
|
planId: input.planId,
|
|
332
|
+
providerPlanId: input.providerPlanId,
|
|
332
333
|
amountCents: input.amountCents,
|
|
333
334
|
currency: input.currency,
|
|
335
|
+
description: input.description,
|
|
334
336
|
analyticsMetadata: input.analyticsMetadata,
|
|
335
337
|
user_id: input.userId,
|
|
336
338
|
stripe_customer_id: input.stripeCustomerId,
|