@funnelsgrove/runtime 0.1.18 → 0.1.19

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.
@@ -5,6 +5,7 @@ export declare const BUILDER_PREVIEW_RUNTIME_MODE_CHANGED = "builder.preview.run
5
5
  export declare const BUILDER_PREVIEW_DEFINITION_PATCH = "builder.preview.definitionPatch";
6
6
  export declare const BUILDER_PREVIEW_VARIABLE_VALUES_CHANGED = "builder.preview.variableValuesChanged";
7
7
  export declare const BUILDER_PREVIEW_PAYWALL_PLANS_CHANGED = "builder.preview.paywallPlansChanged";
8
+ export declare const BUILDER_PREVIEW_THEME_CHANGED = "builder.preview.themeChanged";
8
9
  export type BuilderPreviewReadyMessage = {
9
10
  type: typeof BUILDER_PREVIEW_READY;
10
11
  stepId: string;
@@ -75,3 +76,7 @@ export type BuilderPreviewPaywallPlansChangedMessage = {
75
76
  stepId: string;
76
77
  plans: BuilderPreviewPaywallPlan[];
77
78
  };
79
+ export type BuilderPreviewThemeChangedMessage = {
80
+ type: typeof BUILDER_PREVIEW_THEME_CHANGED;
81
+ cssVariables: Record<string, string>;
82
+ };
@@ -5,3 +5,4 @@ export const BUILDER_PREVIEW_RUNTIME_MODE_CHANGED = 'builder.preview.runtimeMode
5
5
  export const BUILDER_PREVIEW_DEFINITION_PATCH = 'builder.preview.definitionPatch';
6
6
  export const BUILDER_PREVIEW_VARIABLE_VALUES_CHANGED = 'builder.preview.variableValuesChanged';
7
7
  export const BUILDER_PREVIEW_PAYWALL_PLANS_CHANGED = 'builder.preview.paywallPlansChanged';
8
+ export const BUILDER_PREVIEW_THEME_CHANGED = 'builder.preview.themeChanged';
@@ -41,12 +41,16 @@ type PreviewBridgeMessage = {
41
41
  kind: 'paywallPlansChanged';
42
42
  stepId: string;
43
43
  plans: BuilderPreviewPaywallPlan[];
44
+ } | {
45
+ kind: 'themeChanged';
46
+ cssVariables: Record<string, string>;
44
47
  } | {
45
48
  kind: 'goToStep';
46
49
  stepId: string;
47
50
  };
48
51
  export declare const parsePreviewQuickEditPatch: (value: unknown) => PreviewQuickEditPatch | null;
49
52
  export declare const parsePreviewBridgeMessage: (value: unknown) => PreviewBridgeMessage | null;
53
+ export declare const applyPreviewThemeVariables: (cssVariables: Record<string, string>) => void;
50
54
  export declare const applyPreviewQuickEditPatch: (patch: PreviewQuickEditPatch) => void;
51
55
  export declare function emitPreviewVariableValues(stepId: FunnelStepId, values: Record<string, string>): void;
52
56
  export declare function usePreviewVariableValues(stepId: FunnelStepId, values: Record<string, string>): void;
@@ -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_GO_TO_STEP, BUILDER_PREVIEW_PAYWALL_PLANS_CHANGED, BUILDER_PREVIEW_READY, BUILDER_PREVIEW_RUNTIME_MODE_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_GO_TO_STEP, BUILDER_PREVIEW_PAYWALL_PLANS_CHANGED, BUILDER_PREVIEW_READY, BUILDER_PREVIEW_RUNTIME_MODE_CHANGED, 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';
@@ -107,6 +107,13 @@ export const parsePreviewQuickEditPatch = (value) => {
107
107
  }
108
108
  return null;
109
109
  };
110
+ const parseThemeCssVariables = (value) => {
111
+ if (!isRecord(value)) {
112
+ return null;
113
+ }
114
+ const cssVariables = Object.fromEntries(Object.entries(value).filter((entry) => entry[0].startsWith('--') && typeof entry[1] === 'string'));
115
+ return Object.keys(cssVariables).length > 0 ? cssVariables : null;
116
+ };
110
117
  export const parsePreviewBridgeMessage = (value) => {
111
118
  if (!isRecord(value)) {
112
119
  return null;
@@ -149,12 +156,33 @@ export const parsePreviewBridgeMessage = (value) => {
149
156
  }
150
157
  : null;
151
158
  }
159
+ if (messageType === BUILDER_PREVIEW_THEME_CHANGED) {
160
+ const cssVariables = parseThemeCssVariables(value.cssVariables);
161
+ return cssVariables ? { kind: 'themeChanged', cssVariables } : null;
162
+ }
152
163
  if (messageType === BUILDER_PREVIEW_GO_TO_STEP) {
153
164
  const stepId = typeof value.stepId === 'string' ? value.stepId.trim() : '';
154
165
  return stepId ? { kind: 'goToStep', stepId } : null;
155
166
  }
156
167
  return null;
157
168
  };
169
+ export const applyPreviewThemeVariables = (cssVariables) => {
170
+ if (typeof document === 'undefined') {
171
+ return;
172
+ }
173
+ const targets = [
174
+ document.documentElement,
175
+ document.body,
176
+ ...Array.from(document.querySelectorAll('.page-root')),
177
+ ].filter((target) => Boolean(target));
178
+ for (const target of targets) {
179
+ for (const [key, value] of Object.entries(cssVariables)) {
180
+ if (key.startsWith('--')) {
181
+ target.style.setProperty(key, value);
182
+ }
183
+ }
184
+ }
185
+ };
158
186
  export const applyPreviewQuickEditPatch = (patch) => {
159
187
  if (typeof document === 'undefined') {
160
188
  return;
@@ -291,6 +319,10 @@ export function usePreviewBridge({ activeStepId, onGoToStep, resolveRenderableSt
291
319
  applyPreviewPaywallPlansPatch(action.stepId, action.plans);
292
320
  return;
293
321
  }
322
+ if (action.kind === 'themeChanged') {
323
+ applyPreviewThemeVariables(action.cssVariables);
324
+ return;
325
+ }
294
326
  if (action.kind === 'runtimeModeChanged') {
295
327
  setRuntimeMode(action.mode);
296
328
  return;
@@ -9,7 +9,16 @@ export type FunnelFlowControllerExperiment<StepId extends string> = FunnelManife
9
9
  })[];
10
10
  };
11
11
  type StepComponentRegistry = Record<string, unknown>;
12
+ type FunnelFlowAnalyticsAdapter = {
13
+ trackFirstStepViewed?: (input: {
14
+ stepId: string;
15
+ stepName: string;
16
+ occurredAt: string;
17
+ featureFlags?: Record<string, string>;
18
+ }) => string | null;
19
+ };
12
20
  type UseFunnelFlowControllerInput<StepId extends string> = {
21
+ analytics?: FunnelFlowAnalyticsAdapter;
13
22
  initialStepId?: StepId;
14
23
  lockToInitialStep?: boolean;
15
24
  defaultStepId: StepId;
@@ -63,5 +72,5 @@ export declare function computeRenderSuspended(input: {
63
72
  }>;
64
73
  isPreviewRuntime?: boolean;
65
74
  }): boolean;
66
- export declare function useFunnelFlowController<StepId extends string>({ initialStepId, lockToInitialStep, defaultStepId, stepSequence, stepById, stepComponentById, funnelExperiments, getPathForStep, getStepIdFromPath, getSequentialNextStepId, getChoiceTargetsForStep, isFunnelStepId, resolveConfiguredNextStep, resolveRuntimeInitialStepId, }: UseFunnelFlowControllerInput<StepId>): UseFunnelFlowControllerResult<StepId>;
75
+ export declare function useFunnelFlowController<StepId extends string>({ analytics, initialStepId, lockToInitialStep, defaultStepId, stepSequence, stepById, stepComponentById, funnelExperiments, getPathForStep, getStepIdFromPath, getSequentialNextStepId, getChoiceTargetsForStep, isFunnelStepId, resolveConfiguredNextStep, resolveRuntimeInitialStepId, }: UseFunnelFlowControllerInput<StepId>): UseFunnelFlowControllerResult<StepId>;
67
76
  export {};
@@ -54,7 +54,7 @@ export function computeRenderSuspended(input) {
54
54
  }
55
55
  return input.experiments.some((experiment) => experiment.stepId === input.activeStepId);
56
56
  }
57
- export function useFunnelFlowController({ initialStepId, lockToInitialStep = false, defaultStepId, stepSequence, stepById, stepComponentById, funnelExperiments, getPathForStep, getStepIdFromPath, getSequentialNextStepId, getChoiceTargetsForStep, isFunnelStepId, resolveConfiguredNextStep, resolveRuntimeInitialStepId, }) {
57
+ export function useFunnelFlowController({ analytics, initialStepId, lockToInitialStep = false, defaultStepId, stepSequence, stepById, stepComponentById, funnelExperiments, getPathForStep, getStepIdFromPath, getSequentialNextStepId, getChoiceTargetsForStep, isFunnelStepId, resolveConfiguredNextStep, resolveRuntimeInitialStepId, }) {
58
58
  var _a, _b;
59
59
  const [runtimeMode, setRuntimeMode] = useRuntimeMode();
60
60
  const [editorModeEnabled, setEditorModeEnabled] = useState(false);
@@ -106,6 +106,7 @@ export function useFunnelFlowController({ initialStepId, lockToInitialStep = fal
106
106
  const attributesRef = useRef(attributes);
107
107
  const postHogFeatureFlagsRef = useRef({});
108
108
  const prevStepIdRef = useRef(null);
109
+ const firstStepViewedTrackedRef = useRef(false);
109
110
  const stepStartedAtByIdRef = useRef({});
110
111
  const engagedStepIdsRef = useRef(new Set());
111
112
  const currentUserIdRef = useRef(user.id);
@@ -368,6 +369,7 @@ export function useFunnelFlowController({ initialStepId, lockToInitialStep = fal
368
369
  recordStepCompletion(stepId, choices);
369
370
  }, [recordStepCompletion]);
370
371
  useEffect(() => {
372
+ var _a;
371
373
  if (safeActiveStepId !== activeStepId) {
372
374
  logger.warn(`[FunnelFlow] Unknown or non-renderable step "${activeStepId}", falling back to "${safeActiveStepId}".`);
373
375
  setActiveStepId(safeActiveStepId);
@@ -391,6 +393,15 @@ export function useFunnelFlowController({ initialStepId, lockToInitialStep = fal
391
393
  startedAt,
392
394
  });
393
395
  capturePostHogStepEvent('step_started', Object.assign({ distinct_id: currentUserIdRef.current, funnel_id: FUNNEL_ID || undefined, funnel_version_id: FUNNEL_VERSION_ID || undefined, project_id: PROJECT_ID || undefined, environment: runtimeMode, step_id: renderedStepId, step_name: stepName, started_at: startedAt }, buildFeatureFlagProperties(postHogFeatureFlagsRef.current)));
396
+ if (!firstStepViewedTrackedRef.current && safeActiveStepId === defaultStepId) {
397
+ firstStepViewedTrackedRef.current = true;
398
+ (_a = analytics === null || analytics === void 0 ? void 0 : analytics.trackFirstStepViewed) === null || _a === void 0 ? void 0 : _a.call(analytics, {
399
+ stepId: renderedStepId,
400
+ stepName,
401
+ occurredAt: startedAt,
402
+ featureFlags: postHogFeatureFlagsRef.current,
403
+ });
404
+ }
394
405
  }
395
406
  attributesAtStepStart.current = Object.assign({}, attributesRef.current);
396
407
  prevStepIdRef.current = renderedStepId;
@@ -425,6 +436,7 @@ export function useFunnelFlowController({ initialStepId, lockToInitialStep = fal
425
436
  return () => window.clearTimeout(timer);
426
437
  }, [
427
438
  activeStepId,
439
+ analytics,
428
440
  defaultStepId,
429
441
  isPreviewRuntime,
430
442
  recordStepCompletion,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@funnelsgrove/runtime",
3
- "version": "0.1.18",
3
+ "version": "0.1.19",
4
4
  "type": "module",
5
5
  "private": false,
6
6
  "main": "./dist/index.js",