@funnelsgrove/runtime 0.7.1 → 0.7.3
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.js +20 -13
- 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]);
|
|
@@ -40,26 +40,33 @@ const getLegacyProviderMapping = (plan, mode) => {
|
|
|
40
40
|
const buildPlanModeConfig = (item, mode, legacyPlan = null) => {
|
|
41
41
|
var _a, _b;
|
|
42
42
|
const metadata = isRecord(item.metadata) ? item.metadata : {};
|
|
43
|
+
const directMappings = isRecord(item.providerMappingsByMode)
|
|
44
|
+
? item.providerMappingsByMode
|
|
45
|
+
: {};
|
|
43
46
|
const metadataMappings = isRecord(metadata.providerMappingsByMode)
|
|
44
47
|
? metadata.providerMappingsByMode
|
|
45
48
|
: {};
|
|
46
|
-
const
|
|
49
|
+
const modeMapping = isRecord(directMappings[mode])
|
|
50
|
+
? directMappings[mode]
|
|
51
|
+
: isRecord(metadataMappings[mode])
|
|
52
|
+
? metadataMappings[mode]
|
|
53
|
+
: {};
|
|
47
54
|
const legacyMapping = getLegacyProviderMapping(legacyPlan, mode);
|
|
48
55
|
const prefix = mode === 'test' ? 'test' : 'live';
|
|
49
56
|
const providerPlanId = (pickString(item, `${prefix}ProviderPlanId`, `${prefix}_provider_plan_id`)
|
|
50
|
-
|| pickString(
|
|
57
|
+
|| pickString(modeMapping, 'providerPlanId')
|
|
51
58
|
|| (legacyMapping ? pickString(legacyMapping, 'providerPlanId', 'provider_plan_id') : null));
|
|
52
59
|
if (!providerPlanId) {
|
|
53
60
|
return null;
|
|
54
61
|
}
|
|
55
|
-
const amountMinor = (_a = pickNumber(
|
|
56
|
-
const interval = pickString(
|
|
62
|
+
const amountMinor = (_a = pickNumber(modeMapping, 'amountCents')) !== null && _a !== void 0 ? _a : (legacyMapping ? pickNumber(legacyMapping, 'amountMinor', 'amount_minor') : null);
|
|
63
|
+
const interval = pickString(modeMapping, 'billingInterval')
|
|
57
64
|
|| (legacyMapping ? pickString(legacyMapping, 'interval') : null);
|
|
58
|
-
const intervalCount = (_b = pickNumber(
|
|
59
|
-
const currency = pickString(
|
|
65
|
+
const intervalCount = (_b = pickNumber(modeMapping, 'billingIntervalCount')) !== null && _b !== void 0 ? _b : (legacyMapping ? pickNumber(legacyMapping, 'intervalCount', 'interval_count') : null);
|
|
66
|
+
const currency = pickString(modeMapping, 'currency')
|
|
60
67
|
|| (legacyMapping ? pickString(legacyMapping, 'currency') : null);
|
|
61
68
|
const amountMajor = amountMinor === null ? null : amountMinor / 100;
|
|
62
|
-
const priceLabel = pickString(
|
|
69
|
+
const priceLabel = pickString(modeMapping, 'priceLabel') || (amountMajor === null
|
|
63
70
|
? null
|
|
64
71
|
: new Intl.NumberFormat('en-US', {
|
|
65
72
|
style: 'currency',
|
|
@@ -67,12 +74,12 @@ const buildPlanModeConfig = (item, mode, legacyPlan = null) => {
|
|
|
67
74
|
minimumFractionDigits: 2,
|
|
68
75
|
maximumFractionDigits: 2,
|
|
69
76
|
}).format(amountMajor) + (interval ? `/${interval}` : ''));
|
|
70
|
-
return Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({ providerPlanId }, (priceLabel ? { priceLabel } : {})), (pickString(
|
|
71
|
-
? { perDayAmount: pickString(
|
|
72
|
-
: {})), (pickString(
|
|
73
|
-
? { perDayLabel: pickString(
|
|
74
|
-
: {})), (amountMinor === null ? {} : { amountCents: amountMinor })), (pickString(
|
|
75
|
-
? { checkoutSummaryLabel: pickString(
|
|
77
|
+
return Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({ providerPlanId }, (priceLabel ? { priceLabel } : {})), (pickString(modeMapping, 'perDayAmount')
|
|
78
|
+
? { perDayAmount: pickString(modeMapping, 'perDayAmount') }
|
|
79
|
+
: {})), (pickString(modeMapping, 'perDayLabel')
|
|
80
|
+
? { perDayLabel: pickString(modeMapping, 'perDayLabel') }
|
|
81
|
+
: {})), (amountMinor === null ? {} : { amountCents: amountMinor })), (pickString(modeMapping, 'checkoutSummaryLabel')
|
|
82
|
+
? { checkoutSummaryLabel: pickString(modeMapping, 'checkoutSummaryLabel') }
|
|
76
83
|
: amountMajor === null
|
|
77
84
|
? {}
|
|
78
85
|
: { checkoutSummaryLabel: `${new Intl.NumberFormat('en-US', {
|
|
@@ -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,
|