@funnelsgrove/runtime 0.1.63 → 0.3.0

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.
@@ -1,4 +1,3 @@
1
- import type { RuntimeMode } from '../services/runtime-mode.service.js';
2
1
  export type FunnelEditorPanelExperiment = {
3
2
  experimentId: string;
4
3
  variants: readonly {
@@ -10,11 +9,9 @@ type FunnelEditorPanelProps<Experiment extends FunnelEditorPanelExperiment> = {
10
9
  editorPanelExpanded: boolean;
11
10
  experimentAssignmentForEditor: (experiment: Experiment) => string;
12
11
  funnelExperiments: readonly Experiment[];
13
- onRuntimeModeChange: (mode: RuntimeMode) => void;
14
12
  onSelectVariant: (experiment: Experiment, variantKey: string) => void;
15
13
  onToggleExpanded: () => void;
16
- runtimeMode: RuntimeMode;
17
14
  };
18
- export declare function FunnelEditorPanel<Experiment extends FunnelEditorPanelExperiment>({ editorModeEnabled, editorPanelExpanded, experimentAssignmentForEditor, funnelExperiments, onRuntimeModeChange, onSelectVariant, onToggleExpanded, runtimeMode, }: FunnelEditorPanelProps<Experiment>): import("react/jsx-runtime").JSX.Element | null;
19
- export declare const funnelEditorPanelStyles = "\n.funnel-editor-panel {\n position: fixed;\n top: 12px;\n right: 12px;\n z-index: 1200;\n border-radius: 12px;\n border: 1px solid rgba(33, 41, 82, 0.25);\n background: rgba(255, 255, 255, 0.98);\n color: #202538;\n box-shadow: 0 18px 40px rgba(0, 0, 0, 0.18);\n}\n\n.funnel-editor-panel.is-collapsed {\n width: auto;\n}\n\n.funnel-editor-panel.is-expanded {\n width: 184px;\n}\n\n.funnel-editor-panel-toggle {\n width: 100%;\n border: 0;\n background: transparent;\n color: inherit;\n font-size: 13px;\n font-weight: 700;\n line-height: 1;\n padding: 10px 12px;\n cursor: pointer;\n text-align: left;\n}\n\n.funnel-editor-panel-content {\n border-top: 1px solid rgba(33, 41, 82, 0.15);\n padding: 10px 12px 12px;\n}\n\n.funnel-editor-panel-label {\n margin: 0 0 8px;\n font-size: 11px;\n font-weight: 700;\n letter-spacing: 0.08em;\n color: #53608a;\n}\n\n.funnel-editor-panel-options {\n display: grid;\n grid-template-columns: repeat(2, minmax(0, 1fr));\n gap: 8px;\n}\n\n.funnel-editor-panel-options button {\n border: 1px solid rgba(63, 81, 181, 0.3);\n border-radius: 8px;\n background: #eef1ff;\n color: #3f51b5;\n font-size: 12px;\n font-weight: 700;\n line-height: 1;\n padding: 10px 0;\n cursor: pointer;\n}\n\n.funnel-editor-panel-options button.is-active {\n border-color: #3f51b5;\n background: #3f51b5;\n color: #ffffff;\n}\n\n.funnel-editor-panel-tests {\n margin-top: 10px;\n}\n\n.funnel-editor-panel-helper {\n margin: 0;\n font-size: 11px;\n color: #5a6387;\n}\n\n.funnel-editor-panel-test-list {\n display: grid;\n gap: 8px;\n}\n\n.funnel-editor-panel-test-item {\n display: grid;\n gap: 6px;\n font-size: 11px;\n color: #445073;\n}\n\n.funnel-editor-panel-test-item select {\n height: 28px;\n border: 1px solid rgba(63, 81, 181, 0.25);\n border-radius: 8px;\n background: #f5f7ff;\n color: #26315a;\n font-size: 12px;\n padding: 0 8px;\n}\n";
15
+ export declare function FunnelEditorPanel<Experiment extends FunnelEditorPanelExperiment>({ editorModeEnabled, editorPanelExpanded, experimentAssignmentForEditor, funnelExperiments, onSelectVariant, onToggleExpanded, }: FunnelEditorPanelProps<Experiment>): import("react/jsx-runtime").JSX.Element | null;
16
+ export declare const funnelEditorPanelStyles = "\n.funnel-editor-panel {\n position: fixed;\n top: 12px;\n right: 12px;\n z-index: 1200;\n border-radius: 12px;\n border: 1px solid rgba(33, 41, 82, 0.25);\n background: rgba(255, 255, 255, 0.98);\n color: #202538;\n box-shadow: 0 18px 40px rgba(0, 0, 0, 0.18);\n}\n\n.funnel-editor-panel.is-collapsed {\n width: auto;\n}\n\n.funnel-editor-panel.is-expanded {\n width: 184px;\n}\n\n.funnel-editor-panel-toggle {\n width: 100%;\n border: 0;\n background: transparent;\n color: inherit;\n font-size: 13px;\n font-weight: 700;\n line-height: 1;\n padding: 10px 12px;\n cursor: pointer;\n text-align: left;\n}\n\n.funnel-editor-panel-content {\n border-top: 1px solid rgba(33, 41, 82, 0.15);\n padding: 10px 12px 12px;\n}\n\n.funnel-editor-panel-label {\n margin: 0 0 8px;\n font-size: 11px;\n font-weight: 700;\n letter-spacing: 0.08em;\n color: #53608a;\n}\n\n.funnel-editor-panel-tests {\n margin-top: 0;\n}\n\n.funnel-editor-panel-helper {\n margin: 0;\n font-size: 11px;\n color: #5a6387;\n}\n\n.funnel-editor-panel-test-list {\n display: grid;\n gap: 8px;\n}\n\n.funnel-editor-panel-test-item {\n display: grid;\n gap: 6px;\n font-size: 11px;\n color: #445073;\n}\n\n.funnel-editor-panel-test-item select {\n height: 28px;\n border: 1px solid rgba(63, 81, 181, 0.25);\n border-radius: 8px;\n background: #f5f7ff;\n color: #26315a;\n font-size: 12px;\n padding: 0 8px;\n}\n";
20
17
  export {};
@@ -1,15 +1,15 @@
1
1
  'use client';
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
- export function FunnelEditorPanel({ editorModeEnabled, editorPanelExpanded, experimentAssignmentForEditor, funnelExperiments, onRuntimeModeChange, onSelectVariant, onToggleExpanded, runtimeMode, }) {
3
+ export function FunnelEditorPanel({ editorModeEnabled, editorPanelExpanded, experimentAssignmentForEditor, funnelExperiments, onSelectVariant, onToggleExpanded, }) {
4
4
  if (!editorModeEnabled) {
5
5
  return null;
6
6
  }
7
- return (_jsxs("aside", { className: `funnel-editor-panel ${editorPanelExpanded ? 'is-expanded' : 'is-collapsed'}`, children: [_jsx("button", { type: 'button', className: 'funnel-editor-panel-toggle', onClick: onToggleExpanded, children: editorPanelExpanded ? 'Hide editor' : 'Editor' }), editorPanelExpanded ? (_jsxs("div", { className: 'funnel-editor-panel-content', children: [_jsx("p", { className: 'funnel-editor-panel-label', children: "MODE" }), _jsxs("div", { className: 'funnel-editor-panel-options', children: [_jsx("button", { type: 'button', className: runtimeMode === 'test' ? 'is-active' : '', onClick: () => onRuntimeModeChange('test'), children: "Test" }), _jsx("button", { type: 'button', className: runtimeMode === 'live' ? 'is-active' : '', onClick: () => onRuntimeModeChange('live'), children: "Live" })] }), _jsxs("div", { className: 'funnel-editor-panel-tests', children: [_jsx("p", { className: 'funnel-editor-panel-label', children: "A/B TESTS" }), funnelExperiments.length === 0 ? (_jsx("p", { className: 'funnel-editor-panel-helper', children: "No configured tests." })) : (_jsx("div", { className: 'funnel-editor-panel-test-list', children: funnelExperiments.map((experiment) => {
8
- const assignedVariantKey = experimentAssignmentForEditor(experiment);
9
- return (_jsxs("label", { className: 'funnel-editor-panel-test-item', children: [_jsx("span", { children: experiment.experimentId }), _jsx("select", { value: assignedVariantKey, onChange: (event) => {
10
- onSelectVariant(experiment, event.target.value);
11
- }, children: experiment.variants.map((variant) => (_jsx("option", { value: variant.variantKey, children: variant.variantKey }, variant.variantKey))) })] }, experiment.experimentId));
12
- }) }))] })] })) : null] }));
7
+ return (_jsxs("aside", { className: `funnel-editor-panel ${editorPanelExpanded ? 'is-expanded' : 'is-collapsed'}`, children: [_jsx("button", { type: 'button', className: 'funnel-editor-panel-toggle', onClick: onToggleExpanded, children: editorPanelExpanded ? 'Hide editor' : 'Editor' }), editorPanelExpanded ? (_jsx("div", { className: 'funnel-editor-panel-content', children: _jsxs("div", { className: 'funnel-editor-panel-tests', children: [_jsx("p", { className: 'funnel-editor-panel-label', children: "A/B TESTS" }), funnelExperiments.length === 0 ? (_jsx("p", { className: 'funnel-editor-panel-helper', children: "No configured tests." })) : (_jsx("div", { className: 'funnel-editor-panel-test-list', children: funnelExperiments.map((experiment) => {
8
+ const assignedVariantKey = experimentAssignmentForEditor(experiment);
9
+ return (_jsxs("label", { className: 'funnel-editor-panel-test-item', children: [_jsx("span", { children: experiment.experimentId }), _jsx("select", { value: assignedVariantKey, onChange: (event) => {
10
+ onSelectVariant(experiment, event.target.value);
11
+ }, children: experiment.variants.map((variant) => (_jsx("option", { value: variant.variantKey, children: variant.variantKey }, variant.variantKey))) })] }, experiment.experimentId));
12
+ }) }))] }) })) : null] }));
13
13
  }
14
14
  export const funnelEditorPanelStyles = `
15
15
  .funnel-editor-panel {
@@ -58,32 +58,8 @@ export const funnelEditorPanelStyles = `
58
58
  color: #53608a;
59
59
  }
60
60
 
61
- .funnel-editor-panel-options {
62
- display: grid;
63
- grid-template-columns: repeat(2, minmax(0, 1fr));
64
- gap: 8px;
65
- }
66
-
67
- .funnel-editor-panel-options button {
68
- border: 1px solid rgba(63, 81, 181, 0.3);
69
- border-radius: 8px;
70
- background: #eef1ff;
71
- color: #3f51b5;
72
- font-size: 12px;
73
- font-weight: 700;
74
- line-height: 1;
75
- padding: 10px 0;
76
- cursor: pointer;
77
- }
78
-
79
- .funnel-editor-panel-options button.is-active {
80
- border-color: #3f51b5;
81
- background: #3f51b5;
82
- color: #ffffff;
83
- }
84
-
85
61
  .funnel-editor-panel-tests {
86
- margin-top: 10px;
62
+ margin-top: 0;
87
63
  }
88
64
 
89
65
  .funnel-editor-panel-helper {
@@ -0,0 +1,7 @@
1
+ import type { ReactNode } from 'react';
2
+ type PaymentRuntimeBoundaryProps = {
3
+ children: ReactNode;
4
+ fallback?: ReactNode;
5
+ };
6
+ export declare function PaymentRuntimeBoundary({ children, fallback, }: PaymentRuntimeBoundaryProps): ReactNode;
7
+ export {};
@@ -0,0 +1,16 @@
1
+ 'use client';
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import { useRuntimeEnvironmentReady } from '../services/runtime-mode.service.js';
4
+ const defaultFallback = (_jsx("section", { role: 'status', "aria-live": 'polite', "aria-busy": 'true', style: {
5
+ alignItems: 'center',
6
+ background: 'inherit',
7
+ color: 'inherit',
8
+ display: 'grid',
9
+ justifyItems: 'center',
10
+ minHeight: '100dvh',
11
+ width: '100%',
12
+ }, children: "Loading secure checkout" }));
13
+ export function PaymentRuntimeBoundary({ children, fallback = defaultFallback, }) {
14
+ const runtimeEnvironmentReady = useRuntimeEnvironmentReady();
15
+ return runtimeEnvironmentReady ? children : fallback;
16
+ }
@@ -9,7 +9,6 @@ export type RuntimeDevInfoValue = {
9
9
  type RuntimeDevInfoBoxProps = {
10
10
  runtimeMode: RuntimeMode;
11
11
  user: Pick<FunnelUser, 'id' | 'email'>;
12
- onSwitchToLiveMode: () => void;
13
12
  configValues?: readonly RuntimeDevInfoValue[];
14
13
  initialConfigOpen?: boolean;
15
14
  initialExpanded?: boolean;
@@ -18,6 +17,6 @@ type RuntimeDevInfoBoxProps = {
18
17
  title?: string;
19
18
  };
20
19
  export declare const copyRuntimeDevInfoValue: (value: string) => Promise<boolean>;
21
- export declare function RuntimeDevInfoBox({ runtimeMode, user, onSwitchToLiveMode, configValues, initialConfigOpen, initialExpanded, paywallStateOptions, reloadOnReset, title, }: RuntimeDevInfoBoxProps): import("react/jsx-runtime").JSX.Element | null;
22
- export declare const runtimeDevInfoBoxStyles = "\n.runtime-dev-info {\n position: fixed;\n right: 16px;\n bottom: 16px;\n z-index: 100000;\n width: min(392px, calc(100vw - 32px));\n color: #f8fafc;\n font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;\n pointer-events: none;\n}\n\n.runtime-dev-info * {\n box-sizing: border-box;\n}\n\n.runtime-dev-info-pill,\n.runtime-dev-info-panel {\n pointer-events: auto;\n}\n\n.runtime-dev-info.is-collapsed {\n width: auto;\n}\n\n.runtime-dev-info-pill {\n min-height: 56px;\n display: inline-flex;\n align-items: center;\n gap: 10px;\n border: 1px solid rgb(255 255 255 / 12%);\n border-radius: 28px;\n background: #11161d;\n color: inherit;\n padding: 0 20px;\n box-shadow: 0 18px 42px rgb(15 23 42 / 24%);\n cursor: pointer;\n}\n\n.runtime-dev-info-wordmark {\n font-size: 17px;\n font-weight: 800;\n letter-spacing: 0;\n}\n\n.runtime-dev-info-chevron {\n color: #cbd5e1;\n font-size: 18px;\n font-weight: 700;\n line-height: 1;\n}\n\n.runtime-dev-info-panel {\n margin-top: 10px;\n border: 1px solid rgb(255 255 255 / 12%);\n border-radius: 20px;\n background: #11161d;\n box-shadow: 0 22px 50px rgb(15 23 42 / 30%);\n overflow: hidden;\n}\n\n.runtime-dev-info-status-row,\n.runtime-dev-info-config-head {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 12px;\n}\n\n.runtime-dev-info-status-row {\n padding: 18px 20px 14px;\n}\n\n.runtime-dev-info-title,\n.runtime-dev-info-config-head p,\n.runtime-dev-info-help p {\n margin: 0;\n}\n\n.runtime-dev-info-title {\n color: #e5e7eb;\n font-size: 16px;\n font-weight: 750;\n line-height: 1.2;\n}\n\n.runtime-dev-info-badge {\n border: 1px solid #3b82f6;\n border-radius: 999px;\n color: #60a5fa;\n font-size: 12px;\n font-weight: 750;\n line-height: 1;\n padding: 7px 10px;\n}\n\n.runtime-dev-info-identities {\n display: grid;\n gap: 10px;\n padding: 0 20px 18px;\n}\n\n.runtime-dev-info-identity-card {\n min-width: 0;\n border: 1px solid rgb(255 255 255 / 10%);\n border-radius: 10px;\n background: rgb(255 255 255 / 4%);\n padding: 10px;\n}\n\n.runtime-dev-info-identity-main {\n display: flex;\n align-items: center;\n gap: 8px;\n}\n\n.runtime-dev-info-identity-value {\n min-width: 0;\n flex: 1;\n color: #f8fafc;\n overflow-wrap: anywhere;\n white-space: normal;\n}\n\n.runtime-dev-info-identity-label,\n.runtime-dev-info-config-name {\n display: block;\n margin-top: 5px;\n color: #94a3b8;\n font-size: 12px;\n line-height: 1.25;\n}\n\n.runtime-dev-info-identity-value,\n.runtime-dev-info-config-value,\n.runtime-dev-info-help code {\n font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', monospace;\n font-size: 12px;\n letter-spacing: 0;\n}\n\n.runtime-dev-info-config-value,\n.runtime-dev-info-help code {\n color: #dbeafe;\n}\n\n.runtime-dev-info-identity-main button,\n.runtime-dev-info-actions button,\n.runtime-dev-info-config-head button {\n min-height: 30px;\n border: 1px solid rgb(255 255 255 / 14%);\n border-radius: 8px;\n background: rgb(255 255 255 / 6%);\n color: #e5e7eb;\n font: inherit;\n font-size: 12px;\n font-weight: 700;\n cursor: pointer;\n}\n\n.runtime-dev-info-identity-main button {\n flex: 0 0 auto;\n padding: 0 9px;\n}\n\n.runtime-dev-info-actions {\n display: grid;\n grid-template-columns: repeat(3, minmax(0, 1fr));\n gap: 8px;\n border-top: 1px solid rgb(255 255 255 / 10%);\n padding: 14px 20px;\n}\n\n.runtime-dev-info-actions button {\n width: 100%;\n padding: 0 8px;\n}\n\n.runtime-dev-info-actions button:hover,\n.runtime-dev-info-identity-main button:hover,\n.runtime-dev-info-config-head button:hover {\n background: rgb(255 255 255 / 12%);\n}\n\n.runtime-dev-info-config {\n border-top: 1px solid rgb(255 255 255 / 10%);\n padding: 14px 20px;\n}\n\n.runtime-dev-info-config-head {\n margin-bottom: 10px;\n color: #e5e7eb;\n font-size: 13px;\n font-weight: 750;\n}\n\n.runtime-dev-info-config-head button {\n padding: 0 10px;\n}\n\n.runtime-dev-info-config-list {\n display: grid;\n gap: 5px;\n max-height: 220px;\n overflow: auto;\n}\n\n.runtime-dev-info-config-row {\n min-width: 0;\n border-bottom: 1px solid rgb(255 255 255 / 8%);\n padding: 7px 0;\n}\n\n.runtime-dev-info-config-value {\n display: block;\n min-width: 0;\n overflow-wrap: anywhere;\n white-space: normal;\n}\n\n.runtime-dev-info-config-name {\n font-size: 11px;\n}\n\n.runtime-dev-info-help {\n display: grid;\n gap: 6px;\n border-top: 1px solid rgb(255 255 255 / 10%);\n padding: 14px 20px 18px;\n color: #94a3b8;\n font-size: 12px;\n line-height: 1.45;\n}\n\n@media (max-width: 430px) {\n .runtime-dev-info {\n right: 10px;\n bottom: 10px;\n width: min(360px, calc(100vw - 20px));\n }\n\n .runtime-dev-info-actions {\n grid-template-columns: 1fr;\n }\n\n .runtime-dev-info-identity-main {\n align-items: flex-start;\n }\n}\n";
20
+ export declare function RuntimeDevInfoBox({ runtimeMode, user, configValues, initialConfigOpen, initialExpanded, paywallStateOptions, reloadOnReset, title, }: RuntimeDevInfoBoxProps): import("react/jsx-runtime").JSX.Element | null;
21
+ export declare const runtimeDevInfoBoxStyles = "\n.runtime-dev-info {\n position: fixed;\n right: 16px;\n bottom: 16px;\n z-index: 100000;\n width: min(392px, calc(100vw - 32px));\n color: #f8fafc;\n font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;\n pointer-events: none;\n}\n\n.runtime-dev-info * {\n box-sizing: border-box;\n}\n\n.runtime-dev-info-pill,\n.runtime-dev-info-panel {\n pointer-events: auto;\n}\n\n.runtime-dev-info.is-collapsed {\n width: auto;\n}\n\n.runtime-dev-info-pill {\n min-height: 56px;\n display: inline-flex;\n align-items: center;\n gap: 10px;\n border: 1px solid rgb(255 255 255 / 12%);\n border-radius: 28px;\n background: #11161d;\n color: inherit;\n padding: 0 20px;\n box-shadow: 0 18px 42px rgb(15 23 42 / 24%);\n cursor: pointer;\n}\n\n.runtime-dev-info-wordmark {\n font-size: 17px;\n font-weight: 800;\n letter-spacing: 0;\n}\n\n.runtime-dev-info-chevron {\n color: #cbd5e1;\n font-size: 18px;\n font-weight: 700;\n line-height: 1;\n}\n\n.runtime-dev-info-panel {\n margin-top: 10px;\n border: 1px solid rgb(255 255 255 / 12%);\n border-radius: 20px;\n background: #11161d;\n box-shadow: 0 22px 50px rgb(15 23 42 / 30%);\n overflow: hidden;\n}\n\n.runtime-dev-info-status-row,\n.runtime-dev-info-config-head {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 12px;\n}\n\n.runtime-dev-info-status-row {\n padding: 18px 20px 14px;\n}\n\n.runtime-dev-info-title,\n.runtime-dev-info-config-head p {\n margin: 0;\n}\n\n.runtime-dev-info-title {\n color: #e5e7eb;\n font-size: 16px;\n font-weight: 750;\n line-height: 1.2;\n}\n\n.runtime-dev-info-badge {\n border: 1px solid #3b82f6;\n border-radius: 999px;\n color: #60a5fa;\n font-size: 12px;\n font-weight: 750;\n line-height: 1;\n padding: 7px 10px;\n}\n\n.runtime-dev-info-identities {\n display: grid;\n gap: 10px;\n padding: 0 20px 18px;\n}\n\n.runtime-dev-info-identity-card {\n min-width: 0;\n border: 1px solid rgb(255 255 255 / 10%);\n border-radius: 10px;\n background: rgb(255 255 255 / 4%);\n padding: 10px;\n}\n\n.runtime-dev-info-identity-main {\n display: flex;\n align-items: center;\n gap: 8px;\n}\n\n.runtime-dev-info-identity-value {\n min-width: 0;\n flex: 1;\n color: #f8fafc;\n overflow-wrap: anywhere;\n white-space: normal;\n}\n\n.runtime-dev-info-identity-label,\n.runtime-dev-info-config-name {\n display: block;\n margin-top: 5px;\n color: #94a3b8;\n font-size: 12px;\n line-height: 1.25;\n}\n\n.runtime-dev-info-identity-value,\n.runtime-dev-info-config-value {\n font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', monospace;\n font-size: 12px;\n letter-spacing: 0;\n}\n\n.runtime-dev-info-config-value {\n color: #dbeafe;\n}\n\n.runtime-dev-info-identity-main button,\n.runtime-dev-info-actions button,\n.runtime-dev-info-config-head button {\n min-height: 30px;\n border: 1px solid rgb(255 255 255 / 14%);\n border-radius: 8px;\n background: rgb(255 255 255 / 6%);\n color: #e5e7eb;\n font: inherit;\n font-size: 12px;\n font-weight: 700;\n cursor: pointer;\n}\n\n.runtime-dev-info-identity-main button {\n flex: 0 0 auto;\n padding: 0 9px;\n}\n\n.runtime-dev-info-actions {\n display: grid;\n grid-template-columns: repeat(2, minmax(0, 1fr));\n gap: 8px;\n border-top: 1px solid rgb(255 255 255 / 10%);\n padding: 14px 20px;\n}\n\n.runtime-dev-info-actions button {\n width: 100%;\n padding: 0 8px;\n}\n\n.runtime-dev-info-actions button:hover,\n.runtime-dev-info-identity-main button:hover,\n.runtime-dev-info-config-head button:hover {\n background: rgb(255 255 255 / 12%);\n}\n\n.runtime-dev-info-config {\n border-top: 1px solid rgb(255 255 255 / 10%);\n padding: 14px 20px;\n}\n\n.runtime-dev-info-config-head {\n margin-bottom: 10px;\n color: #e5e7eb;\n font-size: 13px;\n font-weight: 750;\n}\n\n.runtime-dev-info-config-head button {\n padding: 0 10px;\n}\n\n.runtime-dev-info-config-list {\n display: grid;\n gap: 5px;\n max-height: 220px;\n overflow: auto;\n}\n\n.runtime-dev-info-config-row {\n min-width: 0;\n border-bottom: 1px solid rgb(255 255 255 / 8%);\n padding: 7px 0;\n}\n\n.runtime-dev-info-config-value {\n display: block;\n min-width: 0;\n overflow-wrap: anywhere;\n white-space: normal;\n}\n\n.runtime-dev-info-config-name {\n font-size: 11px;\n}\n\n@media (max-width: 430px) {\n .runtime-dev-info {\n right: 10px;\n bottom: 10px;\n width: min(360px, calc(100vw - 20px));\n }\n\n .runtime-dev-info-actions {\n grid-template-columns: 1fr;\n }\n\n .runtime-dev-info-identity-main {\n align-items: flex-start;\n }\n}\n";
23
22
  export {};
@@ -73,7 +73,7 @@ export const copyRuntimeDevInfoValue = async (value) => {
73
73
  return false;
74
74
  }
75
75
  };
76
- export function RuntimeDevInfoBox({ runtimeMode, user, onSwitchToLiveMode, configValues = [], initialConfigOpen = false, initialExpanded = false, paywallStateOptions, reloadOnReset = true, title = 'Funnel sandbox', }) {
76
+ export function RuntimeDevInfoBox({ runtimeMode, user, configValues = [], initialConfigOpen = false, initialExpanded = false, paywallStateOptions, reloadOnReset = true, title = 'Funnel sandbox', }) {
77
77
  const [expanded, setExpanded] = useState(initialExpanded);
78
78
  const [configOpen, setConfigOpen] = useState(initialConfigOpen);
79
79
  const [copiedKey, setCopiedKey] = useState(null);
@@ -114,12 +114,12 @@ export function RuntimeDevInfoBox({ runtimeMode, user, onSwitchToLiveMode, confi
114
114
  value: normalizeDisplayValue(user.email),
115
115
  },
116
116
  ];
117
- return (_jsxs("aside", { className: `runtime-dev-info ${expanded ? 'is-expanded' : 'is-collapsed'}`, children: [_jsxs("button", { type: 'button', className: 'runtime-dev-info-pill', onClick: () => setExpanded((current) => !current), "aria-expanded": expanded, children: [_jsx("span", { className: 'runtime-dev-info-wordmark', children: "funnelgrove" }), _jsx("span", { className: 'runtime-dev-info-chevron', "aria-hidden": 'true', children: expanded ? 'v' : '>' })] }), expanded ? (_jsxs("div", { className: 'runtime-dev-info-panel', children: [_jsxs("div", { className: 'runtime-dev-info-status-row', children: [_jsx("p", { className: 'runtime-dev-info-title', children: title }), _jsx("span", { className: 'runtime-dev-info-badge', children: "Test" })] }), _jsx("div", { className: 'runtime-dev-info-identities', children: userRows.map((row) => (_jsxs("div", { className: 'runtime-dev-info-identity-card', children: [_jsxs("div", { className: 'runtime-dev-info-identity-main', children: [_jsx("code", { className: 'runtime-dev-info-identity-value', children: row.value }), _jsx("button", { type: 'button', onClick: () => copyValue(row.value, row.key), children: copiedKey === row.key ? 'Copied' : 'Copy' })] }), _jsx("span", { className: 'runtime-dev-info-identity-label', children: row.label })] }, row.key))) }), _jsxs("div", { className: 'runtime-dev-info-actions', children: [_jsx("button", { type: 'button', onClick: () => setConfigOpen((current) => !current), children: configOpen ? 'Hide config' : 'View config' }), _jsx("button", { type: 'button', onClick: onSwitchToLiveMode, children: "Switch to live mode" }), _jsx("button", { type: 'button', onClick: handleResetState, children: resetDone ? 'Timers reset' : 'Reset timers' })] }), configOpen ? (_jsxs("div", { className: 'runtime-dev-info-config', children: [_jsxs("div", { className: 'runtime-dev-info-config-head', children: [_jsx("p", { children: "Runtime config" }), _jsx("button", { type: 'button', onClick: () => {
117
+ return (_jsxs("aside", { className: `runtime-dev-info ${expanded ? 'is-expanded' : 'is-collapsed'}`, children: [_jsxs("button", { type: 'button', className: 'runtime-dev-info-pill', onClick: () => setExpanded((current) => !current), "aria-expanded": expanded, children: [_jsx("span", { className: 'runtime-dev-info-wordmark', children: "funnelgrove" }), _jsx("span", { className: 'runtime-dev-info-chevron', "aria-hidden": 'true', children: expanded ? 'v' : '>' })] }), expanded ? (_jsxs("div", { className: 'runtime-dev-info-panel', children: [_jsxs("div", { className: 'runtime-dev-info-status-row', children: [_jsx("p", { className: 'runtime-dev-info-title', children: title }), _jsx("span", { className: 'runtime-dev-info-badge', children: "Test" })] }), _jsx("div", { className: 'runtime-dev-info-identities', children: userRows.map((row) => (_jsxs("div", { className: 'runtime-dev-info-identity-card', children: [_jsxs("div", { className: 'runtime-dev-info-identity-main', children: [_jsx("code", { className: 'runtime-dev-info-identity-value', children: row.value }), _jsx("button", { type: 'button', onClick: () => copyValue(row.value, row.key), children: copiedKey === row.key ? 'Copied' : 'Copy' })] }), _jsx("span", { className: 'runtime-dev-info-identity-label', children: row.label })] }, row.key))) }), _jsxs("div", { className: 'runtime-dev-info-actions', children: [_jsx("button", { type: 'button', onClick: () => setConfigOpen((current) => !current), children: configOpen ? 'Hide config' : 'View config' }), _jsx("button", { type: 'button', onClick: handleResetState, children: resetDone ? 'Timers reset' : 'Reset timers' })] }), configOpen ? (_jsxs("div", { className: 'runtime-dev-info-config', children: [_jsxs("div", { className: 'runtime-dev-info-config-head', children: [_jsx("p", { children: "Runtime config" }), _jsx("button", { type: 'button', onClick: () => {
118
118
  const value = configItems
119
119
  .map((item) => `${item.source || item.label}=${normalizeDisplayValue(item.value)}`)
120
120
  .join('\n');
121
121
  copyValue(value, 'config');
122
- }, children: copiedKey === 'config' ? 'Copied' : 'Copy all' })] }), _jsx("div", { className: 'runtime-dev-info-config-list', children: configItems.map((item) => (_jsxs("div", { className: 'runtime-dev-info-config-row', children: [_jsx("code", { className: 'runtime-dev-info-config-value', children: normalizeDisplayValue(item.value) }), _jsx("span", { className: 'runtime-dev-info-config-name', children: item.source || item.label })] }, `${item.source || item.label}:${item.label}`))) })] })) : null, _jsxs("div", { className: 'runtime-dev-info-help', children: [_jsxs("p", { children: ["Use ", _jsx("code", { children: "?mode=test" }), " to force test mode."] }), _jsxs("p", { children: ["Use ", _jsx("code", { children: "?mode=prod" }), " or ", _jsx("code", { children: "?mode=production" }), " for live mode."] })] })] })) : null] }));
122
+ }, children: copiedKey === 'config' ? 'Copied' : 'Copy all' })] }), _jsx("div", { className: 'runtime-dev-info-config-list', children: configItems.map((item) => (_jsxs("div", { className: 'runtime-dev-info-config-row', children: [_jsx("code", { className: 'runtime-dev-info-config-value', children: normalizeDisplayValue(item.value) }), _jsx("span", { className: 'runtime-dev-info-config-name', children: item.source || item.label })] }, `${item.source || item.label}:${item.label}`))) })] })) : null] })) : null] }));
123
123
  }
124
124
  export const runtimeDevInfoBoxStyles = `
125
125
  .runtime-dev-info {
@@ -195,8 +195,7 @@ export const runtimeDevInfoBoxStyles = `
195
195
  }
196
196
 
197
197
  .runtime-dev-info-title,
198
- .runtime-dev-info-config-head p,
199
- .runtime-dev-info-help p {
198
+ .runtime-dev-info-config-head p {
200
199
  margin: 0;
201
200
  }
202
201
 
@@ -255,15 +254,13 @@ export const runtimeDevInfoBoxStyles = `
255
254
  }
256
255
 
257
256
  .runtime-dev-info-identity-value,
258
- .runtime-dev-info-config-value,
259
- .runtime-dev-info-help code {
257
+ .runtime-dev-info-config-value {
260
258
  font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', monospace;
261
259
  font-size: 12px;
262
260
  letter-spacing: 0;
263
261
  }
264
262
 
265
- .runtime-dev-info-config-value,
266
- .runtime-dev-info-help code {
263
+ .runtime-dev-info-config-value {
267
264
  color: #dbeafe;
268
265
  }
269
266
 
@@ -288,7 +285,7 @@ export const runtimeDevInfoBoxStyles = `
288
285
 
289
286
  .runtime-dev-info-actions {
290
287
  display: grid;
291
- grid-template-columns: repeat(3, minmax(0, 1fr));
288
+ grid-template-columns: repeat(2, minmax(0, 1fr));
292
289
  gap: 8px;
293
290
  border-top: 1px solid rgb(255 255 255 / 10%);
294
291
  padding: 14px 20px;
@@ -345,16 +342,6 @@ export const runtimeDevInfoBoxStyles = `
345
342
  font-size: 11px;
346
343
  }
347
344
 
348
- .runtime-dev-info-help {
349
- display: grid;
350
- gap: 6px;
351
- border-top: 1px solid rgb(255 255 255 / 10%);
352
- padding: 14px 20px 18px;
353
- color: #94a3b8;
354
- font-size: 12px;
355
- line-height: 1.45;
356
- }
357
-
358
345
  @media (max-width: 430px) {
359
346
  .runtime-dev-info {
360
347
  right: 10px;
@@ -1,7 +1,6 @@
1
1
  export declare const BUILDER_PREVIEW_READY = "builder.preview.ready";
2
2
  export declare const BUILDER_PREVIEW_ACTIVE_STEP_CHANGED = "builder.preview.activeStepChanged";
3
3
  export declare const BUILDER_PREVIEW_GO_TO_STEP = "builder.preview.goToStep";
4
- export declare const BUILDER_PREVIEW_RUNTIME_MODE_CHANGED = "builder.preview.runtimeModeChanged";
5
4
  export declare const BUILDER_PREVIEW_DEFINITION_PATCH = "builder.preview.definitionPatch";
6
5
  export declare const BUILDER_PREVIEW_VARIABLE_VALUES_CHANGED = "builder.preview.variableValuesChanged";
7
6
  export declare const BUILDER_PREVIEW_PAYWALL_PLANS_CHANGED = "builder.preview.paywallPlansChanged";
@@ -16,10 +15,6 @@ export type BuilderPreviewActiveStepChangedMessage = {
16
15
  type: typeof BUILDER_PREVIEW_ACTIVE_STEP_CHANGED;
17
16
  stepId: string;
18
17
  };
19
- export type BuilderPreviewRuntimeModeChangedMessage = {
20
- type: typeof BUILDER_PREVIEW_RUNTIME_MODE_CHANGED;
21
- mode: 'test' | 'live';
22
- };
23
18
  export type BuilderPreviewGoToStepMessage = {
24
19
  type: typeof BUILDER_PREVIEW_GO_TO_STEP;
25
20
  stepId: string;
@@ -1,7 +1,6 @@
1
1
  export const BUILDER_PREVIEW_READY = 'builder.preview.ready';
2
2
  export const BUILDER_PREVIEW_ACTIVE_STEP_CHANGED = 'builder.preview.activeStepChanged';
3
3
  export const BUILDER_PREVIEW_GO_TO_STEP = 'builder.preview.goToStep';
4
- export const BUILDER_PREVIEW_RUNTIME_MODE_CHANGED = 'builder.preview.runtimeModeChanged';
5
4
  export const BUILDER_PREVIEW_DEFINITION_PATCH = 'builder.preview.definitionPatch';
6
5
  export const BUILDER_PREVIEW_VARIABLE_VALUES_CHANGED = 'builder.preview.variableValuesChanged';
7
6
  export const BUILDER_PREVIEW_PAYWALL_PLANS_CHANGED = 'builder.preview.paywallPlansChanged';
package/dist/index.d.ts CHANGED
@@ -41,6 +41,7 @@ export * from './sdk/userAnswers.js';
41
41
  export * from './components/FunnelContext.js';
42
42
  export * from './components/FunnelEditorPanel.js';
43
43
  export * from './components/ManageSubscriptionScreen.js';
44
+ export * from './components/PaymentRuntimeBoundary.js';
44
45
  export * from './components/RuntimeDevInfoBox.js';
45
46
  export * from './components/SubscriptionHandoffScreen.js';
46
47
  export * from './components/shared/PrimaryButton.js';
package/dist/index.js CHANGED
@@ -44,6 +44,7 @@ export * from './sdk/userAnswers.js';
44
44
  export * from './components/FunnelContext.js';
45
45
  export * from './components/FunnelEditorPanel.js';
46
46
  export * from './components/ManageSubscriptionScreen.js';
47
+ export * from './components/PaymentRuntimeBoundary.js';
47
48
  export * from './components/RuntimeDevInfoBox.js';
48
49
  export * from './components/SubscriptionHandoffScreen.js';
49
50
  export * from './components/shared/PrimaryButton.js';
@@ -55,6 +55,7 @@ export type GeneratedOfferSetDiscount = {
55
55
  readonly metadata?: Record<string, unknown> | null;
56
56
  };
57
57
  export type GeneratedOfferSet = {
58
+ readonly id?: string | null;
58
59
  readonly key: string;
59
60
  readonly isActive?: boolean | null;
60
61
  readonly metadata?: Record<string, unknown> | null;
@@ -74,6 +75,7 @@ export type OfferSetRuntimeDiscount = {
74
75
  readonly previousDiscountPercent: number;
75
76
  };
76
77
  export type OfferSetRuntimePlanOverrides = {
78
+ offerSetId?: string;
77
79
  offerSetKey?: string;
78
80
  projectPlanId?: string;
79
81
  providerPlanId?: string;
@@ -159,7 +159,7 @@ const buildGeneratedPlanOverrides = (item, mode, isDefault) => {
159
159
  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(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({}, (projectPlanId ? { projectPlanId } : {})), (providerPlanId ? { providerPlanId } : {})), (paymentMode ? { paymentMode } : {})), (analyticsPurchaseValue !== null ? { analyticsPurchaseValue } : {})), (title ? { title } : {})), (description ? { description } : {})), (priceLabel ? { priceLabel } : {})), (perDayAmount ? { perDayAmount } : {})), (perDayLabel ? { perDayLabel } : {})), (amountCents !== null ? { amountCents: Math.max(0, Math.round(amountCents)) } : {})), (checkoutSummaryLabel ? { checkoutSummaryLabel } : {})), (billingInterval ? { billingInterval } : {})), (billingIntervalCount !== null ? { billingIntervalCount: Math.max(1, Math.round(billingIntervalCount)) } : {})), (followUpProviderPlanId ? { followUpProviderPlanId } : {})), (followUpPlanTitle ? { followUpPlanTitle } : {})), (followUpBillingInterval ? { followUpBillingInterval } : {})), (followUpBillingIntervalCount !== null ? { followUpBillingIntervalCount: Math.max(1, Math.round(followUpBillingIntervalCount)) } : {})), (introIterations !== null ? { introIterations: Math.max(1, Math.round(introIterations)) } : {})), (featuredTag ? { featuredTag } : {})), (oldPriceLabel ? { oldPriceLabel } : {})), (isDefault ? { isDefault: true } : {}));
160
160
  };
161
161
  export const applyGeneratedOfferSetToPlanCatalog = (baseCatalog, mode, offerSet) => {
162
- var _a, _b, _c;
162
+ var _a, _b, _c, _d;
163
163
  if (!offerSet) {
164
164
  return Object.assign({}, baseCatalog);
165
165
  }
@@ -179,7 +179,7 @@ export const applyGeneratedOfferSetToPlanCatalog = (baseCatalog, mode, offerSet)
179
179
  if (!key) {
180
180
  continue;
181
181
  }
182
- nextCatalog[key] = Object.assign(Object.assign(Object.assign({}, ((_c = nextCatalog[key]) !== null && _c !== void 0 ? _c : {})), { offerSetKey: offerSet.key }), buildGeneratedPlanOverrides(item, mode, getOfferSetItemKind(item) === 'plan' && key === defaultPlanKey));
182
+ nextCatalog[key] = Object.assign(Object.assign(Object.assign(Object.assign({}, ((_c = nextCatalog[key]) !== null && _c !== void 0 ? _c : {})), (((_d = offerSet.id) === null || _d === void 0 ? void 0 : _d.trim()) ? { offerSetId: offerSet.id.trim() } : {})), { offerSetKey: offerSet.key }), buildGeneratedPlanOverrides(item, mode, getOfferSetItemKind(item) === 'plan' && key === defaultPlanKey));
183
183
  }
184
184
  return nextCatalog;
185
185
  };
@@ -1,5 +1,4 @@
1
1
  import { type BuilderPreviewDefinitionPatch, type BuilderPreviewPaywallPlan } from '../config/builder-preview.protocol.js';
2
- import type { RuntimeMode } from '../services/runtime-mode.service.js';
3
2
  import type { FunnelStepId } from './funnel-runtime.js';
4
3
  export type PreviewQuickEditPatch = {
5
4
  kind: 'tagText';
@@ -34,9 +33,6 @@ type PreviewBridgeMessage = {
34
33
  } | {
35
34
  kind: 'quickEditPatch';
36
35
  patch: PreviewQuickEditPatch;
37
- } | {
38
- kind: 'runtimeModeChanged';
39
- mode: RuntimeMode;
40
36
  } | {
41
37
  kind: 'paywallPlansChanged';
42
38
  stepId: string;
@@ -62,8 +58,7 @@ export type PreviewBridgeOptions = {
62
58
  activeStepId: FunnelStepId;
63
59
  onGoToStep: (stepId: FunnelStepId) => void;
64
60
  resolveRenderableStepId: (stepId: string) => FunnelStepId | null;
65
- setRuntimeMode: (mode: RuntimeMode) => void;
66
61
  shouldLockToInitialStep: boolean;
67
62
  };
68
- export declare function usePreviewBridge({ activeStepId, onGoToStep, resolveRenderableStepId, setRuntimeMode, shouldLockToInitialStep, }: PreviewBridgeOptions): void;
63
+ export declare function usePreviewBridge({ activeStepId, onGoToStep, resolveRenderableStepId, shouldLockToInitialStep, }: PreviewBridgeOptions): void;
69
64
  export {};
@@ -1,6 +1,6 @@
1
1
  'use client';
2
2
  import { useEffect, useRef } from 'react';
3
- import { BUILDER_PREVIEW_ACTIVE_STEP_CHANGED, BUILDER_PREVIEW_DEFINITION_PATCH, BUILDER_PREVIEW_EDIT_MODE_CHANGED, BUILDER_PREVIEW_GO_TO_STEP, BUILDER_PREVIEW_PAYWALL_PLANS_CHANGED, BUILDER_PREVIEW_READY, BUILDER_PREVIEW_RUNTIME_MODE_CHANGED, BUILDER_PREVIEW_TEXT_EDITED, BUILDER_PREVIEW_THEME_CHANGED, BUILDER_PREVIEW_VARIABLE_VALUES_CHANGED, } from '../config/builder-preview.protocol.js';
3
+ import { BUILDER_PREVIEW_ACTIVE_STEP_CHANGED, BUILDER_PREVIEW_DEFINITION_PATCH, BUILDER_PREVIEW_EDIT_MODE_CHANGED, BUILDER_PREVIEW_GO_TO_STEP, BUILDER_PREVIEW_PAYWALL_PLANS_CHANGED, BUILDER_PREVIEW_READY, BUILDER_PREVIEW_TEXT_EDITED, BUILDER_PREVIEW_THEME_CHANGED, BUILDER_PREVIEW_VARIABLE_VALUES_CHANGED, } from '../config/builder-preview.protocol.js';
4
4
  import { isPreviewFrameRuntime } from '../services/preview-frame.service.js';
5
5
  import { logger } from '../services/logger.js';
6
6
  import { applyPreviewDefinitionPatch, applyPreviewPaywallPlansPatch } from './preview-definition-overrides.js';
@@ -130,12 +130,6 @@ export const parsePreviewBridgeMessage = (value) => {
130
130
  const patch = parsePreviewDefinitionPatch(value.patch);
131
131
  return patch ? { kind: 'definitionPatch', patch } : null;
132
132
  }
133
- if (messageType === BUILDER_PREVIEW_RUNTIME_MODE_CHANGED) {
134
- return {
135
- kind: 'runtimeModeChanged',
136
- mode: value.mode === 'live' ? 'live' : 'test',
137
- };
138
- }
139
133
  if (messageType === BUILDER_PREVIEW_PAYWALL_PLANS_CHANGED) {
140
134
  const stepId = typeof value.stepId === 'string' ? value.stepId.trim() : '';
141
135
  const plans = Array.isArray(value.plans)
@@ -422,7 +416,7 @@ export function usePreviewVariableValues(stepId, values) {
422
416
  emitPreviewVariableValues(stepId, values);
423
417
  }, [serializedValues, stepId, values]);
424
418
  }
425
- export function usePreviewBridge({ activeStepId, onGoToStep, resolveRenderableStepId, setRuntimeMode, shouldLockToInitialStep, }) {
419
+ export function usePreviewBridge({ activeStepId, onGoToStep, resolveRenderableStepId, shouldLockToInitialStep, }) {
426
420
  const previewReadySentRef = useRef(false);
427
421
  const activeStepIdRef = useRef(activeStepId);
428
422
  useEffect(() => {
@@ -470,10 +464,6 @@ export function usePreviewBridge({ activeStepId, onGoToStep, resolveRenderableSt
470
464
  setPreviewInlineEditMode(action.enabled, () => activeStepIdRef.current);
471
465
  return;
472
466
  }
473
- if (action.kind === 'runtimeModeChanged') {
474
- setRuntimeMode(action.mode);
475
- return;
476
- }
477
467
  if (shouldLockToInitialStep) {
478
468
  return;
479
469
  }
@@ -494,7 +484,6 @@ export function usePreviewBridge({ activeStepId, onGoToStep, resolveRenderableSt
494
484
  activeStepId,
495
485
  onGoToStep,
496
486
  resolveRenderableStepId,
497
- setRuntimeMode,
498
487
  shouldLockToInitialStep,
499
488
  ]);
500
489
  }
@@ -154,7 +154,6 @@ type UseFunnelFlowControllerResult<StepId extends string> = {
154
154
  runtimeMode: 'test' | 'live';
155
155
  setEditorPanelExpanded: Dispatch<SetStateAction<boolean>>;
156
156
  setEditorVariantSelection: (experiment: FunnelFlowControllerExperiment<StepId>, variantKey: string) => void;
157
- setRuntimeMode: (mode: 'test' | 'live') => void;
158
157
  showShellContinue: boolean;
159
158
  };
160
159
  export declare function resolveFunnelEventAttribution(document: unknown, provisionalAttribution: FunnelUserAttribution | null): FunnelUserAttribution | null;
@@ -271,7 +271,7 @@ export function resolveRenderedExperimentStepId(input) {
271
271
  }
272
272
  export function useFunnelFlowController({ api = apiService, analytics, stepContractVersion, initialStepId, initialAttributes, lockToInitialStep = false, defaultStepId, stepSequence, stepById, stepComponentById, funnelExperiments, getPathForStep, getStepIdFromPath, getSequentialNextStepId, getChoiceTargetsForStep, isFunnelStepId, resolveConfiguredNextStep, resolveRuntimeInitialStepId, }) {
273
273
  var _a, _b;
274
- const [runtimeMode, setRuntimeMode] = useRuntimeMode();
274
+ const [runtimeMode] = useRuntimeMode();
275
275
  const [editorModeEnabled, setEditorModeEnabled] = useState(() => isEditorEnabled());
276
276
  const [editorPanelExpanded, setEditorPanelExpanded] = useState(false);
277
277
  const [postHogReady, setPostHogReady] = useState(() => isPostHogReady());
@@ -1267,7 +1267,6 @@ export function useFunnelFlowController({ api = apiService, analytics, stepContr
1267
1267
  }
1268
1268
  },
1269
1269
  resolveRenderableStepId,
1270
- setRuntimeMode,
1271
1270
  shouldLockToInitialStep,
1272
1271
  });
1273
1272
  const setEditorVariantSelection = useCallback((experiment, variantKey) => {
@@ -1374,7 +1373,6 @@ export function useFunnelFlowController({ api = apiService, analytics, stepContr
1374
1373
  runtimeMode,
1375
1374
  setEditorPanelExpanded,
1376
1375
  setEditorVariantSelection,
1377
- setRuntimeMode,
1378
1376
  showShellContinue: !hideActionBar && !isAutoAdvanceStep && !choiceTargets,
1379
1377
  };
1380
1378
  }
@@ -2,60 +2,12 @@ import { buildSdkHeaders, FUNNEL_ID, FUNNEL_VERSION_ID, getFunnelSdkPublishableK
2
2
  import { funnelSdkService } from './funnel-sdk.service.js';
3
3
  import { mergeFunnelUserAttribution, } from '../runtime/funnel-attribution.js';
4
4
  import { applyUrlUserAttributesToUser, hasUrlUserProfileAttributes, resolveUrlUserAttributes, } from '../runtime/url-user-attributes.js';
5
+ import { getRuntimeMode } from './runtime-mode.service.js';
5
6
  const DEFAULT_USER_ID_STORAGE_KEY = 'funnel:user-id';
6
7
  const DEFAULT_BOOTSTRAP_IDEMPOTENCY_KEY_STORAGE_KEY = 'funnel:bootstrap-idempotency-key';
7
- const DEFAULT_FUNNEL_DOMAIN_SIGNAL = 'funnelsgrove';
8
8
  const canUseDom = () => {
9
9
  return typeof window !== 'undefined';
10
10
  };
11
- const normalizeRuntimeEnvironment = (value) => {
12
- if (typeof value !== 'string') {
13
- return null;
14
- }
15
- const normalized = value.trim().toLowerCase();
16
- if (normalized === 'test') {
17
- return 'test';
18
- }
19
- if (normalized === 'live' || normalized === 'prod' || normalized === 'production') {
20
- return 'live';
21
- }
22
- return null;
23
- };
24
- const readCookieValue = (name) => {
25
- var _a, _b;
26
- if (!canUseDom() || typeof document === 'undefined') {
27
- return null;
28
- }
29
- return (((_b = (_a = document.cookie) === null || _a === void 0 ? void 0 : _a.split(';').map((part) => part.trim()).find((part) => part.startsWith(`${name}=`))) === null || _b === void 0 ? void 0 : _b.slice(name.length + 1)) || null);
30
- };
31
- const resolveRuntimeAnalyticsEnvironment = () => {
32
- var _a, _b, _c, _d;
33
- if (!canUseDom()) {
34
- return 'live';
35
- }
36
- try {
37
- const queryMode = normalizeRuntimeEnvironment(new URLSearchParams(((_a = window.location) === null || _a === void 0 ? void 0 : _a.search) || '').get('mode'));
38
- if (queryMode) {
39
- return queryMode;
40
- }
41
- const cookieMode = normalizeRuntimeEnvironment(readCookieValue('funnel-mode'));
42
- if (cookieMode) {
43
- return cookieMode;
44
- }
45
- const hostname = ((_c = (_b = window.location) === null || _b === void 0 ? void 0 : _b.hostname) === null || _c === void 0 ? void 0 : _c.trim().toLowerCase()) || '';
46
- if (hostname.includes(DEFAULT_FUNNEL_DOMAIN_SIGNAL)) {
47
- return 'test';
48
- }
49
- const storageMode = normalizeRuntimeEnvironment((_d = window.localStorage) === null || _d === void 0 ? void 0 : _d.getItem('funnel-mode'));
50
- if (storageMode) {
51
- return storageMode;
52
- }
53
- }
54
- catch (_e) {
55
- return 'live';
56
- }
57
- return 'live';
58
- };
59
11
  const isRecord = (value) => {
60
12
  return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
61
13
  };
@@ -329,7 +281,7 @@ class ApiService {
329
281
  }
330
282
  const metadata = {
331
283
  source: 'funnel-session',
332
- environment: resolveRuntimeAnalyticsEnvironment(),
284
+ environment: getRuntimeMode(),
333
285
  };
334
286
  const fingerprint = await digestBootstrapRequest({
335
287
  funnelVersionId: asString(FUNNEL_VERSION_ID),
@@ -381,7 +333,7 @@ class ApiService {
381
333
  },
382
334
  metadata: {
383
335
  source: 'funnel',
384
- environment: resolveRuntimeAnalyticsEnvironment(),
336
+ environment: getRuntimeMode(),
385
337
  },
386
338
  attribution: options === null || options === void 0 ? void 0 : options.attribution,
387
339
  });
@@ -10,6 +10,8 @@ export type FunnelSdkJsonObject = Record<string, unknown>;
10
10
  export type FunnelSdkListPaymentPlansResponse = {
11
11
  funnelId?: string;
12
12
  environment?: FunnelSdkRuntimeMode;
13
+ offerSetId?: string;
14
+ offerSetKey?: string;
13
15
  plans: FunnelSdkPaymentPlan[];
14
16
  };
15
17
  export type FunnelSdkBootstrapUserInput = {
@@ -72,6 +74,7 @@ export type FunnelSdkUploadPhotoInput = {
72
74
  };
73
75
  export type FunnelSdkCreateHostedCheckoutSessionInput = {
74
76
  planId: string;
77
+ offerSetId?: string | null;
75
78
  offerSetKey?: string | null;
76
79
  amountCents: number;
77
80
  successUrl: string;
@@ -95,6 +98,7 @@ export type FunnelSdkCreateHostedCheckoutSessionResponse = {
95
98
  };
96
99
  export type FunnelSdkCreateSubscriptionCheckoutInput = {
97
100
  planId: string;
101
+ offerSetId?: string | null;
98
102
  offerSetKey?: string | null;
99
103
  amountCents: number;
100
104
  returnUrl: string;
@@ -124,6 +128,7 @@ export type FunnelSdkCreateSubscriptionCheckoutResponse = {
124
128
  };
125
129
  export type FunnelSdkCreateOneTimeCheckoutInput = {
126
130
  planId: string;
131
+ offerSetId?: string | null;
127
132
  offerSetKey?: string | null;
128
133
  amountCents: number;
129
134
  returnUrl: string;
@@ -146,6 +151,7 @@ export type FunnelSdkCreateOneTimeCheckoutResponse = {
146
151
  export type FunnelSdkUpdateSubscriptionCheckoutPlanInput = {
147
152
  checkoutSessionId: string;
148
153
  planId: string;
154
+ offerSetId?: string | null;
149
155
  offerSetKey?: string | null;
150
156
  amountCents: number;
151
157
  analyticsMetadata?: Record<string, unknown> | null;
@@ -187,6 +193,8 @@ export type FunnelSdkVerifySubscriptionCheckoutResponse = {
187
193
  */
188
194
  export type FunnelSdkCreateOneTimePaymentIntentInput = {
189
195
  planId: string;
196
+ offerSetId?: string | null;
197
+ offerSetKey?: string | null;
190
198
  amountCents: number;
191
199
  analyticsMetadata?: Record<string, unknown> | null;
192
200
  couponId?: string | null;
@@ -250,6 +258,8 @@ declare class FunnelSdkService {
250
258
  uploadPhoto<ResponsePayload = FunnelSdkJsonObject>(input: FunnelSdkUploadPhotoInput): Promise<ResponsePayload>;
251
259
  listPaymentPlans(input?: {
252
260
  environment?: FunnelSdkRuntimeMode;
261
+ offerSetId?: string | null;
262
+ offerSetKey?: string | null;
253
263
  runtimeConfig?: FunnelSdkRuntimeConfigOverrides;
254
264
  }): Promise<FunnelSdkListPaymentPlansResponse>;
255
265
  createHostedCheckoutSession(input: FunnelSdkCreateHostedCheckoutSessionInput): Promise<FunnelSdkCreateHostedCheckoutSessionResponse>;
@@ -193,6 +193,8 @@ class FunnelSdkService {
193
193
  searchParams: {
194
194
  environment: input.environment,
195
195
  funnelId,
196
+ offerSetId: input.offerSetId,
197
+ offerSetKey: input.offerSetKey,
196
198
  },
197
199
  errorMessage: 'Unable to load payment plans',
198
200
  });
@@ -201,6 +203,7 @@ class FunnelSdkService {
201
203
  return this.postJson('/sdk/public/payments/checkout-session', {
202
204
  runtimeConfig: input.runtimeConfig,
203
205
  body: {
206
+ offerSetId: input.offerSetId,
204
207
  offerSetKey: input.offerSetKey,
205
208
  planId: input.planId,
206
209
  providerPlanId: input.providerPlanId,
@@ -224,6 +227,7 @@ class FunnelSdkService {
224
227
  return this.postJson('/sdk/public/payments/subscriptions', {
225
228
  runtimeConfig: input.runtimeConfig,
226
229
  body: {
230
+ offerSetId: input.offerSetId,
227
231
  offerSetKey: input.offerSetKey,
228
232
  planId: input.planId,
229
233
  providerPlanId: input.providerPlanId,
@@ -251,6 +255,7 @@ class FunnelSdkService {
251
255
  return this.postJson('/sdk/public/payments/one-time-checkout', {
252
256
  runtimeConfig: input.runtimeConfig,
253
257
  body: {
258
+ offerSetId: input.offerSetId,
254
259
  offerSetKey: input.offerSetKey,
255
260
  planId: input.planId,
256
261
  providerPlanId: input.providerPlanId,
@@ -272,6 +277,7 @@ class FunnelSdkService {
272
277
  runtimeConfig: input.runtimeConfig,
273
278
  body: {
274
279
  checkoutSessionId: input.checkoutSessionId,
280
+ offerSetId: input.offerSetId,
275
281
  offerSetKey: input.offerSetKey,
276
282
  planId: input.planId,
277
283
  providerPlanId: input.providerPlanId,
@@ -306,6 +312,8 @@ class FunnelSdkService {
306
312
  return this.postJson('/sdk/public/payments/intents', {
307
313
  runtimeConfig: input.runtimeConfig,
308
314
  body: {
315
+ offerSetId: input.offerSetId,
316
+ offerSetKey: input.offerSetKey,
309
317
  planId: input.planId,
310
318
  amountCents: input.amountCents,
311
319
  couponId: input.couponId,
@@ -1,8 +1,8 @@
1
1
  export type RuntimeMode = 'test' | 'live';
2
2
  export type FunnelMode = 'test' | 'production';
3
+ export declare const isSystemPreviewHostname: (value: string) => boolean;
3
4
  export declare const isEditorEnabled: () => boolean;
4
5
  export declare const getFunnelMode: () => FunnelMode;
5
6
  export declare const getRuntimeMode: () => RuntimeMode;
6
- export declare const setFunnelMode: (mode: FunnelMode) => void;
7
- export declare const setRuntimeMode: (mode: RuntimeMode) => void;
8
- export declare const useRuntimeMode: () => [RuntimeMode, (mode: RuntimeMode) => void];
7
+ export declare const useRuntimeMode: () => readonly [RuntimeMode];
8
+ export declare const useRuntimeEnvironmentReady: () => boolean;
@@ -1,48 +1,40 @@
1
1
  'use client';
2
- import { useCallback, useEffect, useState } from 'react';
3
- const EDITOR_MODE_STORAGE_KEY = 'funnel:editor:mode';
4
- const FUNNEL_MODE_STORAGE_KEY = 'funnel-mode';
5
- const FUNNEL_MODE_COOKIE_NAME = 'funnel-mode';
6
- const MODE_CHANGE_EVENT = 'funnel:editor-mode-changed';
7
- const DEFAULT_FUNNEL_DOMAIN_SIGNAL = 'funnelsgrove';
8
- const FUNNEL_MODE_COOKIE_MAX_AGE_SECONDS = 60 * 60 * 24 * 365;
2
+ import { useSyncExternalStore } from 'react';
3
+ const SYSTEM_PREVIEW_DOMAINS = ['funnelsgrove.org', 'funnelsgrove.com'];
9
4
  const canUseDom = () => {
10
5
  return typeof window !== 'undefined';
11
6
  };
12
- const toFunnelMode = (value) => {
13
- if (typeof value !== 'string') {
14
- return null;
15
- }
16
- const normalized = value.trim().toLowerCase();
17
- if (normalized === 'test') {
18
- return 'test';
19
- }
20
- if (normalized === 'prod' || normalized === 'production' || normalized === 'live') {
21
- return 'production';
22
- }
23
- return null;
24
- };
25
- const toRuntimeMode = (value) => {
26
- return toFunnelMode(value) === 'production' ? 'live' : 'test';
7
+ const normalizeHostname = (value) => {
8
+ return value.trim().toLowerCase().replace(/^\[|\]$/g, '').replace(/\.+$/, '');
27
9
  };
28
- const toRuntimeModeFromFunnelMode = (mode) => {
29
- return mode === 'production' ? 'live' : 'test';
10
+ const isExactOrSubdomain = (hostname, domain) => {
11
+ return hostname === domain || hostname.endsWith(`.${domain}`);
30
12
  };
31
- const toFunnelModeFromRuntimeMode = (mode) => {
32
- return mode === 'live' ? 'production' : 'test';
13
+ const isIpv4Loopback = (hostname) => {
14
+ const match = /^127((?:\.\d{1,3}){3})$/.exec(hostname);
15
+ return match
16
+ ? match[1].slice(1).split('.').every((octet) => Number(octet) <= 255)
17
+ : false;
33
18
  };
34
- const normalizeHostname = (value) => {
35
- return value.trim().toLowerCase().replace(/\.+$/, '');
19
+ const isMappedIpv4Loopback = (hostname) => {
20
+ const mappedIpv4 = /^::ffff:([0-9a-f]{1,4}):[0-9a-f]{1,4}$/.exec(hostname);
21
+ return Boolean(mappedIpv4
22
+ && Number.parseInt(mappedIpv4[1], 16) >= 0x7f00
23
+ && Number.parseInt(mappedIpv4[1], 16) <= 0x7fff);
36
24
  };
37
- const isDefaultPublishedDomain = () => {
38
- if (!canUseDom()) {
39
- return false;
40
- }
41
- const hostname = normalizeHostname(window.location.hostname || '');
25
+ export const isSystemPreviewHostname = (value) => {
26
+ const hostname = normalizeHostname(value);
42
27
  if (!hostname) {
43
28
  return false;
44
29
  }
45
- return hostname.includes(DEFAULT_FUNNEL_DOMAIN_SIGNAL);
30
+ if (hostname === 'localhost' ||
31
+ hostname.endsWith('.localhost') ||
32
+ isIpv4Loopback(hostname) ||
33
+ hostname === '::1' ||
34
+ isMappedIpv4Loopback(hostname)) {
35
+ return true;
36
+ }
37
+ return SYSTEM_PREVIEW_DOMAINS.some((domain) => isExactOrSubdomain(hostname, domain));
46
38
  };
47
39
  export const isEditorEnabled = () => {
48
40
  if (!canUseDom()) {
@@ -55,197 +47,31 @@ export const isEditorEnabled = () => {
55
47
  return false;
56
48
  }
57
49
  };
58
- const readStoredEditorMode = () => {
59
- if (!canUseDom()) {
60
- return null;
61
- }
62
- try {
63
- const storedValue = window.localStorage.getItem(EDITOR_MODE_STORAGE_KEY);
64
- if (!storedValue) {
65
- return null;
66
- }
67
- return toRuntimeMode(storedValue);
68
- }
69
- catch (_a) {
70
- return null;
71
- }
72
- };
73
- const readCookieValue = (name) => {
74
- if (typeof document === 'undefined') {
75
- return null;
76
- }
77
- try {
78
- const target = `${encodeURIComponent(name)}=`;
79
- const entry = document.cookie
80
- .split(';')
81
- .map((item) => item.trim())
82
- .find((item) => item.startsWith(target));
83
- return entry ? decodeURIComponent(entry.slice(target.length)) : null;
84
- }
85
- catch (_a) {
86
- return null;
87
- }
88
- };
89
- const writeFunnelModeCookie = (mode) => {
90
- if (typeof document === 'undefined') {
91
- return;
92
- }
93
- try {
94
- document.cookie = [
95
- `${encodeURIComponent(FUNNEL_MODE_COOKIE_NAME)}=${encodeURIComponent(mode)}`,
96
- 'Path=/',
97
- `Max-Age=${FUNNEL_MODE_COOKIE_MAX_AGE_SECONDS}`,
98
- 'SameSite=Lax',
99
- ].join('; ');
100
- }
101
- catch (_a) {
102
- // ignore cookie failures
103
- }
104
- };
105
- const readFunnelModeCookie = () => {
106
- return toFunnelMode(readCookieValue(FUNNEL_MODE_COOKIE_NAME));
107
- };
108
- const readFunnelModeStorage = () => {
109
- if (!canUseDom()) {
110
- return null;
111
- }
112
- try {
113
- return toFunnelMode(window.localStorage.getItem(FUNNEL_MODE_STORAGE_KEY));
114
- }
115
- catch (_a) {
116
- return null;
117
- }
118
- };
119
- const persistFunnelMode = (mode, options = {}) => {
120
- if (!canUseDom()) {
121
- return;
122
- }
123
- try {
124
- window.localStorage.setItem(FUNNEL_MODE_STORAGE_KEY, mode);
125
- }
126
- catch (_a) {
127
- // ignore storage failures
128
- }
129
- writeFunnelModeCookie(mode);
130
- if (options.emit === false) {
131
- return;
132
- }
133
- emitModeChange(toRuntimeModeFromFunnelMode(mode));
134
- };
135
- const readQueryFunnelMode = () => {
136
- if (!canUseDom()) {
137
- return null;
138
- }
139
- try {
140
- return toFunnelMode(new URLSearchParams(window.location.search).get('mode'));
141
- }
142
- catch (_a) {
143
- return null;
144
- }
145
- };
146
- const emitModeChange = (mode) => {
147
- if (!canUseDom()) {
148
- return;
149
- }
150
- window.dispatchEvent(new CustomEvent(MODE_CHANGE_EVENT, {
151
- detail: {
152
- mode,
153
- },
154
- }));
155
- };
156
50
  export const getFunnelMode = () => {
157
51
  if (!canUseDom()) {
158
52
  return 'production';
159
53
  }
160
- const queryMode = readQueryFunnelMode();
161
- if (queryMode) {
162
- persistFunnelMode(queryMode, { emit: false });
163
- return queryMode;
164
- }
165
- const cookieMode = readFunnelModeCookie();
166
- if (cookieMode) {
167
- return cookieMode;
168
- }
169
- if (isDefaultPublishedDomain()) {
170
- return 'test';
171
- }
172
- return readFunnelModeStorage() || 'production';
54
+ return isSystemPreviewHostname(window.location.hostname || '') ? 'test' : 'production';
173
55
  };
174
56
  export const getRuntimeMode = () => {
175
- if (!canUseDom()) {
176
- return 'live';
177
- }
178
- const queryMode = readQueryFunnelMode();
179
- if (queryMode) {
180
- persistFunnelMode(queryMode, { emit: false });
181
- return toRuntimeModeFromFunnelMode(queryMode);
182
- }
183
- if (!isEditorEnabled()) {
184
- return toRuntimeModeFromFunnelMode(getFunnelMode());
185
- }
186
- return readStoredEditorMode() || 'test';
57
+ return getFunnelMode() === 'test' ? 'test' : 'live';
187
58
  };
188
- export const setFunnelMode = (mode) => {
189
- if (!canUseDom()) {
190
- return;
191
- }
192
- persistFunnelMode(mode);
59
+ const subscribeRuntimeEnvironment = () => {
60
+ return () => undefined;
193
61
  };
194
- export const setRuntimeMode = (mode) => {
195
- if (!canUseDom()) {
196
- return;
197
- }
198
- const normalizedMode = toRuntimeMode(mode);
199
- try {
200
- window.localStorage.setItem(EDITOR_MODE_STORAGE_KEY, normalizedMode);
201
- }
202
- catch (_a) {
203
- // ignore storage failures
204
- }
205
- persistFunnelMode(toFunnelModeFromRuntimeMode(normalizedMode));
62
+ const getServerRuntimeMode = () => {
63
+ return 'live';
206
64
  };
207
- const subscribeRuntimeMode = (listener) => {
208
- if (!canUseDom()) {
209
- return () => undefined;
210
- }
211
- const onModeChanged = (event) => {
212
- var _a;
213
- const customEvent = event;
214
- listener(toRuntimeMode((_a = customEvent.detail) === null || _a === void 0 ? void 0 : _a.mode));
215
- };
216
- const onStorageChanged = (event) => {
217
- if (event.key !== EDITOR_MODE_STORAGE_KEY && event.key !== FUNNEL_MODE_STORAGE_KEY) {
218
- return;
219
- }
220
- listener(getRuntimeMode());
221
- };
222
- window.addEventListener(MODE_CHANGE_EVENT, onModeChanged);
223
- window.addEventListener('storage', onStorageChanged);
224
- return () => {
225
- window.removeEventListener(MODE_CHANGE_EVENT, onModeChanged);
226
- window.removeEventListener('storage', onStorageChanged);
227
- };
65
+ const getRuntimeEnvironmentReady = () => {
66
+ return true;
67
+ };
68
+ const getServerRuntimeEnvironmentReady = () => {
69
+ return false;
228
70
  };
229
71
  export const useRuntimeMode = () => {
230
- const [mode, setMode] = useState('live');
231
- useEffect(() => {
232
- const syncMode = () => {
233
- setMode(getRuntimeMode());
234
- };
235
- syncMode();
236
- const unsubscribe = subscribeRuntimeMode((nextMode) => {
237
- setMode(nextMode);
238
- });
239
- window.addEventListener('popstate', syncMode);
240
- return () => {
241
- unsubscribe();
242
- window.removeEventListener('popstate', syncMode);
243
- };
244
- }, []);
245
- const updateMode = useCallback((nextMode) => {
246
- const normalizedMode = toRuntimeMode(nextMode);
247
- setRuntimeMode(normalizedMode);
248
- setMode(normalizedMode);
249
- }, []);
250
- return [mode, updateMode];
72
+ const mode = useSyncExternalStore(subscribeRuntimeEnvironment, getRuntimeMode, getServerRuntimeMode);
73
+ return [mode];
74
+ };
75
+ export const useRuntimeEnvironmentReady = () => {
76
+ return useSyncExternalStore(subscribeRuntimeEnvironment, getRuntimeEnvironmentReady, getServerRuntimeEnvironmentReady);
251
77
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@funnelsgrove/runtime",
3
- "version": "0.1.63",
3
+ "version": "0.3.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/The-Solid-Grove/funnelsgrove.git",