@funnelsgrove/runtime 0.1.62 → 0.2.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';
@@ -74,6 +74,7 @@ export type OfferSetRuntimeDiscount = {
74
74
  readonly previousDiscountPercent: number;
75
75
  };
76
76
  export type OfferSetRuntimePlanOverrides = {
77
+ offerSetKey?: string;
77
78
  projectPlanId?: string;
78
79
  providerPlanId?: string;
79
80
  paymentMode?: 'one_time';
@@ -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({}, ((_c = nextCatalog[key]) !== null && _c !== void 0 ? _c : {})), buildGeneratedPlanOverrides(item, mode, getOfferSetItemKind(item) === 'plan' && key === defaultPlanKey));
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));
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
  });
@@ -72,6 +72,7 @@ export type FunnelSdkUploadPhotoInput = {
72
72
  };
73
73
  export type FunnelSdkCreateHostedCheckoutSessionInput = {
74
74
  planId: string;
75
+ offerSetKey?: string | null;
75
76
  amountCents: number;
76
77
  successUrl: string;
77
78
  cancelUrl: string;
@@ -94,6 +95,7 @@ export type FunnelSdkCreateHostedCheckoutSessionResponse = {
94
95
  };
95
96
  export type FunnelSdkCreateSubscriptionCheckoutInput = {
96
97
  planId: string;
98
+ offerSetKey?: string | null;
97
99
  amountCents: number;
98
100
  returnUrl: string;
99
101
  analyticsMetadata?: Record<string, unknown> | null;
@@ -122,6 +124,7 @@ export type FunnelSdkCreateSubscriptionCheckoutResponse = {
122
124
  };
123
125
  export type FunnelSdkCreateOneTimeCheckoutInput = {
124
126
  planId: string;
127
+ offerSetKey?: string | null;
125
128
  amountCents: number;
126
129
  returnUrl: string;
127
130
  analyticsMetadata?: Record<string, unknown> | null;
@@ -143,6 +146,7 @@ export type FunnelSdkCreateOneTimeCheckoutResponse = {
143
146
  export type FunnelSdkUpdateSubscriptionCheckoutPlanInput = {
144
147
  checkoutSessionId: string;
145
148
  planId: string;
149
+ offerSetKey?: string | null;
146
150
  amountCents: number;
147
151
  analyticsMetadata?: Record<string, unknown> | null;
148
152
  billingInterval?: string | null;
@@ -201,6 +201,7 @@ class FunnelSdkService {
201
201
  return this.postJson('/sdk/public/payments/checkout-session', {
202
202
  runtimeConfig: input.runtimeConfig,
203
203
  body: {
204
+ offerSetKey: input.offerSetKey,
204
205
  planId: input.planId,
205
206
  providerPlanId: input.providerPlanId,
206
207
  title: input.title,
@@ -223,6 +224,7 @@ class FunnelSdkService {
223
224
  return this.postJson('/sdk/public/payments/subscriptions', {
224
225
  runtimeConfig: input.runtimeConfig,
225
226
  body: {
227
+ offerSetKey: input.offerSetKey,
226
228
  planId: input.planId,
227
229
  providerPlanId: input.providerPlanId,
228
230
  title: input.title,
@@ -249,6 +251,7 @@ class FunnelSdkService {
249
251
  return this.postJson('/sdk/public/payments/one-time-checkout', {
250
252
  runtimeConfig: input.runtimeConfig,
251
253
  body: {
254
+ offerSetKey: input.offerSetKey,
252
255
  planId: input.planId,
253
256
  providerPlanId: input.providerPlanId,
254
257
  title: input.title,
@@ -269,6 +272,7 @@ class FunnelSdkService {
269
272
  runtimeConfig: input.runtimeConfig,
270
273
  body: {
271
274
  checkoutSessionId: input.checkoutSessionId,
275
+ offerSetKey: input.offerSetKey,
272
276
  planId: input.planId,
273
277
  providerPlanId: input.providerPlanId,
274
278
  title: input.title,
@@ -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.62",
3
+ "version": "0.2.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/The-Solid-Grove/funnelsgrove.git",