@funnelsgrove/cli 0.1.14 → 0.1.18

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.
Files changed (293) hide show
  1. package/contracts/step-contract-v2.json +733 -0
  2. package/dist/analyticsOutput.d.ts +30 -15
  3. package/dist/analyticsOutput.js +45 -28
  4. package/dist/apiClient.d.ts +85 -0
  5. package/dist/apiClient.js +225 -8
  6. package/dist/cli.d.ts +149 -1
  7. package/dist/cli.js +1232 -71
  8. package/dist/diagnosticOutput.d.ts +42 -0
  9. package/dist/diagnosticOutput.js +83 -0
  10. package/dist/docsOutput.d.ts +19 -0
  11. package/dist/docsOutput.js +73 -0
  12. package/dist/docsTransaction.d.ts +81 -0
  13. package/dist/docsTransaction.js +1755 -0
  14. package/dist/funnelValidation.d.ts +14 -0
  15. package/dist/funnelValidation.js +297 -0
  16. package/dist/localSync.d.ts +79 -0
  17. package/dist/localSync.js +428 -53
  18. package/dist/projectValidator.d.ts +27 -0
  19. package/dist/projectValidator.js +208 -0
  20. package/dist/reskin.js +27 -1
  21. package/dist/stepContractMigration.d.ts +71 -0
  22. package/dist/stepContractMigration.js +1323 -0
  23. package/dist/templateDocs.js +8 -25
  24. package/docs-release-history.json +66 -0
  25. package/legacy-docs-catalog.json +1012 -0
  26. package/package.json +13 -4
  27. package/template_docs/.funnelsgrove-docs.json +144 -0
  28. package/template_docs/AGENTS.md +30 -58
  29. package/template_docs/CLAUDE.md +0 -2
  30. package/template_docs/docs/funnelsgrove/START-HERE.md +73 -0
  31. package/template_docs/docs/funnelsgrove/contracts/analytics-events.md +59 -0
  32. package/template_docs/docs/funnelsgrove/contracts/content-answers.md +44 -0
  33. package/template_docs/docs/funnelsgrove/contracts/flow-routing.md +41 -0
  34. package/template_docs/docs/funnelsgrove/contracts/payments.md +42 -0
  35. package/template_docs/docs/funnelsgrove/contracts/step-metadata.md +57 -0
  36. package/template_docs/docs/funnelsgrove/migrations/step-contract-v3.md +39 -0
  37. package/template_docs/docs/funnelsgrove/qa/analytics.md +43 -0
  38. package/template_docs/docs/funnelsgrove/qa/local.md +42 -0
  39. package/template_docs/docs/funnelsgrove/qa/paywall-checkout.md +55 -0
  40. package/template_docs/docs/funnelsgrove/qa/publish.md +32 -0
  41. package/template_docs/docs/funnelsgrove/recipes/add-experiment.md +34 -0
  42. package/template_docs/docs/funnelsgrove/recipes/add-step.md +33 -0
  43. package/template_docs/docs/funnelsgrove/recipes/edit-step.md +31 -0
  44. package/template_docs/docs/funnelsgrove/steps/INDEX.md +26 -0
  45. package/template_docs/docs/funnelsgrove/steps/cancellation_offer.md +112 -0
  46. package/template_docs/docs/funnelsgrove/steps/checkout.md +120 -0
  47. package/template_docs/docs/funnelsgrove/steps/form_input.md +115 -0
  48. package/template_docs/docs/funnelsgrove/steps/intro_hero.md +110 -0
  49. package/template_docs/docs/funnelsgrove/steps/multi_select_choice.md +123 -0
  50. package/template_docs/docs/funnelsgrove/steps/paywall_offer.md +115 -0
  51. package/template_docs/docs/funnelsgrove/steps/progress_interstitial.md +111 -0
  52. package/template_docs/docs/funnelsgrove/steps/purchase_completed.md +117 -0
  53. package/template_docs/docs/funnelsgrove/steps/single_step_choice.md +122 -0
  54. package/template_docs/docs/funnelsgrove/steps/single_step_choice_emoji.md +122 -0
  55. package/template_docs/docs/funnelsgrove/steps/social_proof.md +110 -0
  56. package/template_docs/docs/funnelsgrove/steps/subscription_handoff.md +113 -0
  57. package/template_docs/docs/funnelsgrove/steps/subscription_management.md +112 -0
  58. package/template_docs/docs/funnelsgrove/steps/summary_confirmation.md +110 -0
  59. package/template_docs/docs/funnelsgrove/steps/upsell_offer.md +112 -0
  60. package/template_docs/docs/funnelsgrove/steps/value_prop_story.md +110 -0
  61. package/template_docs/funnel-docs.config.json +43 -0
  62. package/template_scaffold/.env.example +41 -0
  63. package/template_scaffold/.funnelsgrove-contract-tools.json +19 -0
  64. package/template_scaffold/.funnelsgrove-docs.json +144 -0
  65. package/template_scaffold/.funnelsgrove-scaffold.json +1078 -0
  66. package/template_scaffold/AGENTS.md +42 -0
  67. package/template_scaffold/CLAUDE.md +1 -0
  68. package/template_scaffold/PLAN.md +81 -0
  69. package/template_scaffold/PRODUCT_SENSE.md +28 -0
  70. package/template_scaffold/context/build-context.md +34 -0
  71. package/template_scaffold/docs/AGENT_FUNNEL_IMPLEMENTATION_SPEC.md +5 -0
  72. package/template_scaffold/docs/ARCHITECTURE.md +5 -0
  73. package/template_scaffold/docs/FLOW_CONFIG_AND_ROUTING.md +5 -0
  74. package/template_scaffold/docs/PAYWALL_AND_PAYMENTS.md +5 -0
  75. package/template_scaffold/docs/README.md +5 -0
  76. package/template_scaffold/docs/funnelsgrove/START-HERE.md +73 -0
  77. package/template_scaffold/docs/funnelsgrove/contracts/analytics-events.md +59 -0
  78. package/template_scaffold/docs/funnelsgrove/contracts/content-answers.md +44 -0
  79. package/template_scaffold/docs/funnelsgrove/contracts/flow-routing.md +41 -0
  80. package/template_scaffold/docs/funnelsgrove/contracts/payments.md +42 -0
  81. package/template_scaffold/docs/funnelsgrove/contracts/step-metadata.md +57 -0
  82. package/template_scaffold/docs/funnelsgrove/migrations/step-contract-v3.md +39 -0
  83. package/template_scaffold/docs/funnelsgrove/qa/analytics.md +43 -0
  84. package/template_scaffold/docs/funnelsgrove/qa/local.md +42 -0
  85. package/template_scaffold/docs/funnelsgrove/qa/paywall-checkout.md +55 -0
  86. package/template_scaffold/docs/funnelsgrove/qa/publish.md +32 -0
  87. package/template_scaffold/docs/funnelsgrove/recipes/add-experiment.md +34 -0
  88. package/template_scaffold/docs/funnelsgrove/recipes/add-step.md +33 -0
  89. package/template_scaffold/docs/funnelsgrove/recipes/edit-step.md +31 -0
  90. package/template_scaffold/docs/funnelsgrove/steps/INDEX.md +26 -0
  91. package/template_scaffold/docs/funnelsgrove/steps/cancellation_offer.md +112 -0
  92. package/template_scaffold/docs/funnelsgrove/steps/checkout.md +120 -0
  93. package/template_scaffold/docs/funnelsgrove/steps/form_input.md +115 -0
  94. package/template_scaffold/docs/funnelsgrove/steps/intro_hero.md +110 -0
  95. package/template_scaffold/docs/funnelsgrove/steps/multi_select_choice.md +123 -0
  96. package/template_scaffold/docs/funnelsgrove/steps/paywall_offer.md +115 -0
  97. package/template_scaffold/docs/funnelsgrove/steps/progress_interstitial.md +111 -0
  98. package/template_scaffold/docs/funnelsgrove/steps/purchase_completed.md +117 -0
  99. package/template_scaffold/docs/funnelsgrove/steps/single_step_choice.md +122 -0
  100. package/template_scaffold/docs/funnelsgrove/steps/single_step_choice_emoji.md +122 -0
  101. package/template_scaffold/docs/funnelsgrove/steps/social_proof.md +110 -0
  102. package/template_scaffold/docs/funnelsgrove/steps/subscription_handoff.md +113 -0
  103. package/template_scaffold/docs/funnelsgrove/steps/subscription_management.md +112 -0
  104. package/template_scaffold/docs/funnelsgrove/steps/summary_confirmation.md +110 -0
  105. package/template_scaffold/docs/funnelsgrove/steps/upsell_offer.md +112 -0
  106. package/template_scaffold/docs/funnelsgrove/steps/value_prop_story.md +110 -0
  107. package/template_scaffold/docs/references/funnel-sdk-user-answers.md +5 -0
  108. package/template_scaffold/eslint.config.mjs +16 -0
  109. package/template_scaffold/funnel-agent-docs.test.ts +699 -0
  110. package/template_scaffold/funnel-docs.config.json +43 -0
  111. package/template_scaffold/funnel.config.json +5 -0
  112. package/template_scaffold/gitignore.template +5 -0
  113. package/template_scaffold/next-env.d.ts +6 -0
  114. package/template_scaffold/next.config.ts +53 -0
  115. package/template_scaffold/package-lock.json +7737 -0
  116. package/template_scaffold/package.json +37 -0
  117. package/template_scaffold/package.test.ts +23 -0
  118. package/template_scaffold/public/claimbee/bee.png +0 -0
  119. package/template_scaffold/public/claimbee/device-iphone.png +0 -0
  120. package/template_scaffold/public/claimbee/device-macbook.png +0 -0
  121. package/template_scaffold/public/claimbee/device-none.png +0 -0
  122. package/template_scaffold/public/claimbee/device-watch.png +0 -0
  123. package/template_scaffold/public/claimbee/glow.svg +3 -0
  124. package/template_scaffold/public/claimbee/steps-sprite-alt.png +0 -0
  125. package/template_scaffold/public/claimbee/steps-sprite-default.png +0 -0
  126. package/template_scaffold/public/claimbee/testimonial-avatar.png +0 -0
  127. package/template_scaffold/public/claimbee/testimonial-star.svg +3 -0
  128. package/template_scaffold/public/paywall/ai-claim-assistant.png +0 -0
  129. package/template_scaffold/public/paywall/claim-history.png +0 -0
  130. package/template_scaffold/public/paywall/clear-progress.png +0 -0
  131. package/template_scaffold/public/paywall/hero.png +0 -0
  132. package/template_scaffold/public/paywall/money-alerts.png +0 -0
  133. package/template_scaffold/public/paywall/money-back-badge.png +0 -0
  134. package/template_scaffold/public/paywall/paywall-image.png +0 -0
  135. package/template_scaffold/public/paywall/r1.png +0 -0
  136. package/template_scaffold/public/paywall/r2.png +0 -0
  137. package/template_scaffold/public/paywall/r3.png +0 -0
  138. package/template_scaffold/public/paywall/r4.png +0 -0
  139. package/template_scaffold/public/paywall/r5.png +0 -0
  140. package/template_scaffold/public/paywall/r6.png +0 -0
  141. package/template_scaffold/public/paywall/special-offer-gift.png +0 -0
  142. package/template_scaffold/public/please/bee.png +0 -0
  143. package/template_scaffold/public/please/device-iphone.png +0 -0
  144. package/template_scaffold/public/please/device-macbook.png +0 -0
  145. package/template_scaffold/public/please/device-none.png +0 -0
  146. package/template_scaffold/public/please/device-watch.png +0 -0
  147. package/template_scaffold/public/please/glow.svg +3 -0
  148. package/template_scaffold/public/please/steps-sprite-alt.png +0 -0
  149. package/template_scaffold/public/please/steps-sprite-default.png +0 -0
  150. package/template_scaffold/public/please/testimonial-avatar.png +0 -0
  151. package/template_scaffold/public/please/testimonial-star.svg +3 -0
  152. package/template_scaffold/public/upsell/upgrade-hero.png +0 -0
  153. package/template_scaffold/src/app/[stepId]/page.test.tsx +59 -0
  154. package/template_scaffold/src/app/[stepId]/page.tsx +62 -0
  155. package/template_scaffold/src/app/globals.css +49 -0
  156. package/template_scaffold/src/app/layout.test.ts +26 -0
  157. package/template_scaffold/src/app/layout.tsx +61 -0
  158. package/template_scaffold/src/app/page.tsx +8 -0
  159. package/template_scaffold/src/app/sdk/page.tsx +673 -0
  160. package/template_scaffold/src/app/subscription-dashboard/page.tsx +274 -0
  161. package/template_scaffold/src/assets/claimbee/bee.png +0 -0
  162. package/template_scaffold/src/assets/claimbee/device-iphone.png +0 -0
  163. package/template_scaffold/src/assets/claimbee/device-macbook.png +0 -0
  164. package/template_scaffold/src/assets/claimbee/device-none.png +0 -0
  165. package/template_scaffold/src/assets/claimbee/device-watch.png +0 -0
  166. package/template_scaffold/src/assets/claimbee/glow.svg +3 -0
  167. package/template_scaffold/src/assets/claimbee/steps-sprite-alt.png +0 -0
  168. package/template_scaffold/src/assets/claimbee/steps-sprite-default.png +0 -0
  169. package/template_scaffold/src/assets/claimbee/testimonial-avatar.png +0 -0
  170. package/template_scaffold/src/assets/claimbee/testimonial-star.svg +3 -0
  171. package/template_scaffold/src/components/FunnelEditorPanel.tsx +194 -0
  172. package/template_scaffold/src/components/FunnelFlow.test.tsx +17 -0
  173. package/template_scaffold/src/components/FunnelFlow.tsx +114 -0
  174. package/template_scaffold/src/components/FunnelStepImage.test.ts +15 -0
  175. package/template_scaffold/src/components/FunnelStepImage.tsx +28 -0
  176. package/template_scaffold/src/components/step/FunnelStepHeader.tsx +35 -0
  177. package/template_scaffold/src/config/billing.plans.test.ts +143 -0
  178. package/template_scaffold/src/config/billing.plans.ts +69 -0
  179. package/template_scaffold/src/config/billing.test.plans.ts +32 -0
  180. package/template_scaffold/src/config/experiments.generated.ts +4 -0
  181. package/template_scaffold/src/config/experiments.test.ts +12 -0
  182. package/template_scaffold/src/config/experiments.ts +9 -0
  183. package/template_scaffold/src/config/funnel.config.ts +12 -0
  184. package/template_scaffold/src/config/funnel.manifest.test.ts +166 -0
  185. package/template_scaffold/src/config/funnel.manifest.ts +139 -0
  186. package/template_scaffold/src/config/offer-sets.generated.ts +4 -0
  187. package/template_scaffold/src/contract/funnel-project.validation.test.ts +80 -0
  188. package/template_scaffold/src/contract/funnel-project.validation.ts +44 -0
  189. package/template_scaffold/src/contract/funnel-validator.vite.config.ts +11 -0
  190. package/template_scaffold/src/contract/golden/cancellation_offer.fixture.ts +29 -0
  191. package/template_scaffold/src/contract/golden/checkout.fixture.ts +37 -0
  192. package/template_scaffold/src/contract/golden/form_input.fixture.ts +28 -0
  193. package/template_scaffold/src/contract/golden/golden-fixtures.test.ts +190 -0
  194. package/template_scaffold/src/contract/golden/intro_hero.fixture.ts +27 -0
  195. package/template_scaffold/src/contract/golden/multi_select_choice.fixture.ts +37 -0
  196. package/template_scaffold/src/contract/golden/paywall_offer.fixture.ts +30 -0
  197. package/template_scaffold/src/contract/golden/progress_interstitial.fixture.ts +28 -0
  198. package/template_scaffold/src/contract/golden/purchase_completed.fixture.ts +33 -0
  199. package/template_scaffold/src/contract/golden/single_step_choice.fixture.ts +36 -0
  200. package/template_scaffold/src/contract/golden/single_step_choice_emoji.fixture.ts +36 -0
  201. package/template_scaffold/src/contract/golden/social_proof.fixture.ts +27 -0
  202. package/template_scaffold/src/contract/golden/subscription_handoff.fixture.ts +30 -0
  203. package/template_scaffold/src/contract/golden/subscription_management.fixture.ts +29 -0
  204. package/template_scaffold/src/contract/golden/summary_confirmation.fixture.ts +27 -0
  205. package/template_scaffold/src/contract/golden/upsell_offer.fixture.ts +29 -0
  206. package/template_scaffold/src/contract/golden/value_prop_story.fixture.ts +27 -0
  207. package/template_scaffold/src/contract/validate-funnel.cli.test.ts +180 -0
  208. package/template_scaffold/src/contract/validate-funnel.cli.ts +199 -0
  209. package/template_scaffold/src/runtime/checkout-runtime-config.test.ts +60 -0
  210. package/template_scaffold/src/runtime/checkout-runtime-config.ts +26 -0
  211. package/template_scaffold/src/runtime/funnel-runtime.test.ts +25 -0
  212. package/template_scaffold/src/runtime/funnel-runtime.ts +118 -0
  213. package/template_scaffold/src/runtime/step-content-context.test.ts +38 -0
  214. package/template_scaffold/src/runtime/step-content-context.ts +65 -0
  215. package/template_scaffold/src/runtime/step-registry.ts +66 -0
  216. package/template_scaffold/src/runtime/step-variable-values.ts +31 -0
  217. package/template_scaffold/src/runtime/use-funnel-flow-controller.integration.test.ts +203 -0
  218. package/template_scaffold/src/runtime/use-funnel-flow-controller.test.ts +76 -0
  219. package/template_scaffold/src/runtime/use-funnel-flow-controller.ts +53 -0
  220. package/template_scaffold/src/steps/content/email-capture.content.ts +29 -0
  221. package/template_scaffold/src/steps/content/manage-subscription.content.ts +62 -0
  222. package/template_scaffold/src/steps/content/paywall.content.ts +191 -0
  223. package/template_scaffold/src/steps/content/step-01.content.ts +23 -0
  224. package/template_scaffold/src/steps/content/step-02.content.ts +77 -0
  225. package/template_scaffold/src/steps/content/step-03.content.ts +40 -0
  226. package/template_scaffold/src/steps/content/step-04.content.ts +49 -0
  227. package/template_scaffold/src/steps/content/step-32-paywall.content.ts +352 -0
  228. package/template_scaffold/src/steps/content/subscription-started.content.ts +68 -0
  229. package/template_scaffold/src/steps/editor/email-capture.editor.ts +58 -0
  230. package/template_scaffold/src/steps/editor/manage-subscription.editor.ts +179 -0
  231. package/template_scaffold/src/steps/editor/paywall.editor.ts +232 -0
  232. package/template_scaffold/src/steps/editor/step-01.editor.ts +22 -0
  233. package/template_scaffold/src/steps/editor/step-02.editor.ts +41 -0
  234. package/template_scaffold/src/steps/editor/step-03.editor.ts +58 -0
  235. package/template_scaffold/src/steps/editor/step-04.editor.ts +98 -0
  236. package/template_scaffold/src/steps/editor/step-32-paywall.editor.ts +478 -0
  237. package/template_scaffold/src/steps/editor/subscription-started.editor.ts +131 -0
  238. package/template_scaffold/src/steps/index.ts +25 -0
  239. package/template_scaffold/src/steps/paywall-checkout-options.test.ts +104 -0
  240. package/template_scaffold/src/steps/paywall-checkout-options.ts +130 -0
  241. package/template_scaffold/src/steps/pricing/paywall-b.pricing.ts +12 -0
  242. package/template_scaffold/src/steps/pricing/paywall.pricing.ts +12 -0
  243. package/template_scaffold/src/steps/pricing/shared-plan-order.ts +17 -0
  244. package/template_scaffold/src/steps/pricing/upsell-form.pricing.ts +16 -0
  245. package/template_scaffold/src/steps/shared-header-contract.test.ts +27 -0
  246. package/template_scaffold/src/steps/step-01.test.ts +38 -0
  247. package/template_scaffold/src/steps/step-01.tsx +108 -0
  248. package/template_scaffold/src/steps/step-02.test.ts +43 -0
  249. package/template_scaffold/src/steps/step-02.tsx +140 -0
  250. package/template_scaffold/src/steps/step-03.test.ts +15 -0
  251. package/template_scaffold/src/steps/step-03.tsx +262 -0
  252. package/template_scaffold/src/steps/step-04.test.ts +32 -0
  253. package/template_scaffold/src/steps/step-04.tsx +673 -0
  254. package/template_scaffold/src/steps/step-31-email-capture.test.ts +22 -0
  255. package/template_scaffold/src/steps/step-31-email-capture.tsx +195 -0
  256. package/template_scaffold/src/steps/step-32-paywall.test.ts +770 -0
  257. package/template_scaffold/src/steps/step-32-paywall.tsx +2253 -0
  258. package/template_scaffold/src/steps/step-32b-paywall-b.tsx +35 -0
  259. package/template_scaffold/src/steps/step-33-subscription-started.test.ts +26 -0
  260. package/template_scaffold/src/steps/step-33-subscription-started.tsx +86 -0
  261. package/template_scaffold/src/steps/step-34-upsell-form.test.ts +27 -0
  262. package/template_scaffold/src/steps/step-35-manage-subscription.tsx +46 -0
  263. package/template_scaffold/src/steps/step-content.registry.ts +45 -0
  264. package/template_scaffold/src/steps/step-image-migration.test.ts +20 -0
  265. package/template_scaffold/src/steps/styles/index.css +2 -0
  266. package/template_scaffold/src/steps/styles/shared/final-flow-shared.css +55 -0
  267. package/template_scaffold/src/steps/styles/shared/funnel-base.css +543 -0
  268. package/template_scaffold/src/steps/styles/shared/info-shared.css +139 -0
  269. package/template_scaffold/src/steps/styles/shared/progress-shared.css +117 -0
  270. package/template_scaffold/src/steps/styles/shared/question-shared.css +76 -0
  271. package/template_scaffold/src/steps/styles/shared/responsive-container.css +176 -0
  272. package/template_scaffold/src/steps/types.ts +7 -0
  273. package/template_scaffold/src/steps/use-claimbee-step-content.ts +15 -0
  274. package/template_scaffold/src/theme/theme.ts +81 -0
  275. package/template_scaffold/tsconfig.json +38 -0
  276. package/template_scaffold/vitest.config.ts +32 -0
  277. package/template_validation/.funnelsgrove-contract-tools.json +19 -0
  278. package/template_validation/src/contract/funnel-project.validation.ts +44 -0
  279. package/template_validation/src/contract/funnel-validator.vite.config.ts +11 -0
  280. package/template_validation/src/contract/validate-funnel.cli.ts +199 -0
  281. package/template_docs/docs/ab-experiments.md +0 -55
  282. package/template_docs/docs/analytics.md +0 -65
  283. package/template_docs/docs/editing-flow.md +0 -125
  284. package/template_docs/docs/editing-step.md +0 -125
  285. package/template_docs/docs/editor-and-content.md +0 -58
  286. package/template_docs/docs/funnel-runtime-architecture.md +0 -125
  287. package/template_docs/docs/meta-pixel-conversions-api.md +0 -50
  288. package/template_docs/docs/payment-plans-and-discounts.md +0 -106
  289. package/template_docs/docs/publishing-and-versioning.md +0 -117
  290. package/template_docs/docs/qa-checklist.md +0 -80
  291. package/template_docs/docs/sdk-api-endpoints.md +0 -78
  292. package/template_docs/docs/step-ui-guidelines.md +0 -110
  293. package/template_docs/docs/theme.md +0 -35
@@ -0,0 +1,118 @@
1
+ import { funnelManifest, type FunnelStepId } from '@/config/funnel.manifest';
2
+ import type { FunnelManifestExperiment } from '@funnelsgrove/runtime';
3
+ import {
4
+ getChoiceTargetsFromEdges,
5
+ isManifestStepId,
6
+ resolveInitialStepId,
7
+ resolveNextStepId,
8
+ } from '@funnelsgrove/runtime';
9
+
10
+ export { FunnelStepId };
11
+
12
+ export const funnelStepSequence = funnelManifest.steps.map((step) => step.id) as FunnelStepId[];
13
+ export type FunnelExperimentConfig = FunnelManifestExperiment & {
14
+ stepId: FunnelStepId;
15
+ variants: readonly (
16
+ FunnelManifestExperiment['variants'][number] & {
17
+ routeToStepId: FunnelStepId;
18
+ }
19
+ )[];
20
+ };
21
+ export const funnelExperiments: FunnelExperimentConfig[] = funnelManifest.experiments.map(
22
+ (experiment) => ({
23
+ ...experiment,
24
+ stepId: experiment.stepId as FunnelStepId,
25
+ variants: experiment.variants.map((variant) => ({
26
+ ...variant,
27
+ routeToStepId: variant.routeToStepId as FunnelStepId,
28
+ })),
29
+ }),
30
+ );
31
+
32
+ export const defaultFunnelStepId: FunnelStepId = funnelStepSequence[0];
33
+
34
+ const stepIdSet = new Set<string>(funnelStepSequence);
35
+ const stepIndexById = new Map<FunnelStepId, number>(
36
+ funnelStepSequence.map((stepId, index) => [stepId, index] as const),
37
+ );
38
+ const stepPathById = new Map<FunnelStepId, string>(
39
+ funnelManifest.steps.map((step) => [step.id as FunnelStepId, step.path] as const),
40
+ );
41
+
42
+ export const isFunnelStepId = (value: string): value is FunnelStepId => {
43
+ return stepIdSet.has(value);
44
+ };
45
+
46
+ export const getPathForStep = (stepId: FunnelStepId): string => {
47
+ return stepPathById.get(stepId) || `/${stepId}`;
48
+ };
49
+
50
+ export const getStepIdFromPath = (pathname: string): FunnelStepId | null => {
51
+ const segment = pathname
52
+ .split('?')[0]
53
+ .split('#')[0]
54
+ .split('/')
55
+ .map((part) => part.trim())
56
+ .filter(Boolean)[0];
57
+
58
+ if (!segment || !isFunnelStepId(segment)) {
59
+ return null;
60
+ }
61
+
62
+ return segment;
63
+ };
64
+
65
+ export const getSequentialNextStepId = (stepId: FunnelStepId): FunnelStepId | null => {
66
+ const stepIndex = stepIndexById.get(stepId);
67
+ if (stepIndex === undefined) {
68
+ return null;
69
+ }
70
+
71
+ return funnelStepSequence[stepIndex + 1] ?? null;
72
+ };
73
+
74
+ export const getChoiceTargetsForStep = (
75
+ stepId: FunnelStepId,
76
+ ): { yes?: FunnelStepId; no?: FunnelStepId } | null => {
77
+ const choiceTargets = getChoiceTargetsFromEdges(funnelManifest, stepId);
78
+ if (!choiceTargets) {
79
+ return null;
80
+ }
81
+
82
+ return {
83
+ yes: choiceTargets.yes && isFunnelStepId(choiceTargets.yes) ? choiceTargets.yes : undefined,
84
+ no: choiceTargets.no && isFunnelStepId(choiceTargets.no) ? choiceTargets.no : undefined,
85
+ };
86
+ };
87
+
88
+ export const resolveRuntimeInitialStepId = (input: {
89
+ requestedStepId?: string | null;
90
+ }): FunnelStepId => {
91
+ const stepId = resolveInitialStepId({
92
+ manifest: funnelManifest,
93
+ requestedStepId: input.requestedStepId,
94
+ });
95
+
96
+ return isFunnelStepId(stepId) ? stepId : defaultFunnelStepId;
97
+ };
98
+
99
+ export const resolveConfiguredNextStep = (
100
+ stepId: FunnelStepId,
101
+ context: {
102
+ attributes: Record<string, unknown>;
103
+ ignoreExperiment?: boolean;
104
+ resolveExperimentVariantKey: (experiment: FunnelManifestExperiment) => string | null;
105
+ },
106
+ ): FunnelStepId | null => {
107
+ const nextStepId = resolveNextStepId({
108
+ manifest: funnelManifest,
109
+ currentStepId: stepId,
110
+ attributes: context.attributes,
111
+ ignoreExperiment: context.ignoreExperiment,
112
+ resolveExperimentVariantKey: context.resolveExperimentVariantKey,
113
+ });
114
+
115
+ return nextStepId && isManifestStepId(funnelManifest, nextStepId) && isFunnelStepId(nextStepId)
116
+ ? nextStepId
117
+ : null;
118
+ };
@@ -0,0 +1,38 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import {
3
+ getStepContentLocale,
4
+ getStepCountryCode,
5
+ } from './step-content-context';
6
+
7
+ describe('step content context helpers', () => {
8
+ it('prefers stored browser language and explicit country when present', () => {
9
+ expect(
10
+ getStepContentLocale(
11
+ {
12
+ browserLanguage: 'pt-PT',
13
+ language: 'en-US',
14
+ },
15
+ 'de-DE',
16
+ ),
17
+ ).toBe('pt-PT');
18
+
19
+ expect(
20
+ getStepCountryCode(
21
+ {
22
+ country: 'pt',
23
+ },
24
+ 'en-US',
25
+ ),
26
+ ).toBe('PT');
27
+ });
28
+
29
+ it('falls back to the runtime locale and derives the region when no country is stored', () => {
30
+ expect(getStepContentLocale({}, 'en-US')).toBe('en-US');
31
+ expect(getStepCountryCode({}, 'en-US')).toBe('US');
32
+ });
33
+
34
+ it('returns null when no locale or country signal is available', () => {
35
+ expect(getStepContentLocale({}, null)).toBeNull();
36
+ expect(getStepCountryCode({}, null)).toBeNull();
37
+ });
38
+ });
@@ -0,0 +1,65 @@
1
+ import type { FunnelUserAnswers } from '@funnelsgrove/runtime';
2
+
3
+ const asTrimmedStringOrNull = (value: unknown): string | null => {
4
+ if (typeof value !== 'string') {
5
+ return null;
6
+ }
7
+
8
+ const trimmed = value.trim();
9
+ return trimmed || null;
10
+ };
11
+
12
+ const readBrowserLocale = (): string | null => {
13
+ if (typeof navigator === 'undefined') {
14
+ return null;
15
+ }
16
+
17
+ return asTrimmedStringOrNull(navigator.language);
18
+ };
19
+
20
+ const normalizeCountryCode = (countryCode: string | null): string | null => {
21
+ if (!countryCode) {
22
+ return null;
23
+ }
24
+
25
+ if (/^[a-z]{2}$/i.test(countryCode)) {
26
+ return countryCode.toUpperCase();
27
+ }
28
+
29
+ return countryCode;
30
+ };
31
+
32
+ const getCountryFromLocale = (locale: string | null): string | null => {
33
+ if (!locale) {
34
+ return null;
35
+ }
36
+
37
+ const region = locale
38
+ .split('-')[1]
39
+ ?.trim()
40
+ .toUpperCase();
41
+
42
+ return region && /^[A-Z]{2}$/.test(region) ? region : null;
43
+ };
44
+
45
+ export function getStepContentLocale(
46
+ attributes: FunnelUserAnswers,
47
+ browserLocale: string | null = readBrowserLocale(),
48
+ ): string | null {
49
+ return (
50
+ asTrimmedStringOrNull(attributes.browserLanguage) ||
51
+ asTrimmedStringOrNull(attributes.language) ||
52
+ asTrimmedStringOrNull(attributes.locale) ||
53
+ asTrimmedStringOrNull(browserLocale)
54
+ );
55
+ }
56
+
57
+ export function getStepCountryCode(
58
+ attributes: FunnelUserAnswers,
59
+ locale: string | null = getStepContentLocale(attributes),
60
+ ): string | null {
61
+ const explicitCountry =
62
+ asTrimmedStringOrNull(attributes.countryCode) || asTrimmedStringOrNull(attributes.country);
63
+
64
+ return normalizeCountryCode(explicitCountry) || getCountryFromLocale(locale);
65
+ }
@@ -0,0 +1,66 @@
1
+ import type { FunctionComponent } from 'react';
2
+ import { StepFour, stepFour } from '@/steps/step-04';
3
+ import { StepOne, stepOne } from '@/steps/step-01';
4
+ import { StepEmailCapture, stepEmailCapture } from '@/steps/step-31-email-capture';
5
+ import { StepPaywall, stepPaywall } from '@/steps/step-32-paywall';
6
+ import { StepPaywallB, stepPaywallB } from '@/steps/step-32b-paywall-b';
7
+ import {
8
+ StepSubscriptionStarted,
9
+ stepSubscriptionStarted,
10
+ } from '@/steps/step-33-subscription-started';
11
+ import {
12
+ StepManageSubscription,
13
+ stepManageSubscription,
14
+ } from '@/steps/step-35-manage-subscription';
15
+ import { StepThree, stepThree } from '@/steps/step-03';
16
+ import { StepTwo, stepTwo } from '@/steps/step-02';
17
+ import type { FunnelStepComponentKey } from '@/config/funnel.manifest';
18
+ import type { FunnelStepMeta } from '@funnelsgrove/runtime';
19
+
20
+ export type StepRegistryEntry = {
21
+ component: FunctionComponent<Record<string, never>>;
22
+ meta: FunnelStepMeta;
23
+ };
24
+
25
+ export const stepRegistry: Record<FunnelStepComponentKey, StepRegistryEntry> = {
26
+ stepOne: {
27
+ component: StepOne,
28
+ meta: stepOne,
29
+ },
30
+ stepTwo: {
31
+ component: StepTwo,
32
+ meta: stepTwo,
33
+ },
34
+ stepThree: {
35
+ component: StepThree,
36
+ meta: stepThree,
37
+ },
38
+ stepFour: {
39
+ component: StepFour,
40
+ meta: stepFour,
41
+ },
42
+ stepEmailCapture: {
43
+ component: StepEmailCapture,
44
+ meta: stepEmailCapture,
45
+ },
46
+ stepPaywall: {
47
+ component: StepPaywall,
48
+ meta: stepPaywall,
49
+ },
50
+ stepPaywallB: {
51
+ component: StepPaywallB,
52
+ meta: stepPaywallB,
53
+ },
54
+ stepManageSubscription: {
55
+ component: StepManageSubscription,
56
+ meta: stepManageSubscription,
57
+ },
58
+ stepSubscriptionStarted: {
59
+ component: StepSubscriptionStarted,
60
+ meta: stepSubscriptionStarted,
61
+ },
62
+ };
63
+
64
+ export const getStepRegistry = (): Record<FunnelStepComponentKey, StepRegistryEntry> => {
65
+ return stepRegistry;
66
+ };
@@ -0,0 +1,31 @@
1
+ import type { PaywallPlan } from '@funnelsgrove/payments';
2
+ import {
3
+ resolveStepEditorUserVariableValues,
4
+ type StepEditorSection,
5
+ } from '@funnelsgrove/runtime';
6
+
7
+ export const buildPlanVariableValues = (plans: readonly PaywallPlan[]): Record<string, string> => {
8
+ return Object.fromEntries(
9
+ plans.flatMap((plan) => {
10
+ const prefix = `plan.${plan.id}`;
11
+ return [
12
+ [`${prefix}.priceLabel`, plan.priceLabel],
13
+ [`${prefix}.dailyPrice`, plan.comparison?.perDay || `${plan.perDayAmount}/day`],
14
+ [`${prefix}.weeklyPrice`, plan.comparison?.perWeek || ''],
15
+ [`${prefix}.monthlyPrice`, plan.comparison?.perMonth || plan.priceLabel],
16
+ [`${prefix}.yearlyPrice`, plan.comparison?.perYear || ''],
17
+ ];
18
+ }),
19
+ );
20
+ };
21
+
22
+ export const buildStepVariableValues = (
23
+ sections: readonly StepEditorSection[],
24
+ answers: Record<string, unknown>,
25
+ extraValues?: Record<string, string>,
26
+ ): Record<string, string> => {
27
+ return {
28
+ ...resolveStepEditorUserVariableValues(sections, answers),
29
+ ...(extraValues || {}),
30
+ };
31
+ };
@@ -0,0 +1,203 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
3
+
4
+ const { posthogFlagsPath } = vi.hoisted(() => ({
5
+ posthogFlagsPath: new URL(
6
+ '../../../funnel-runtime/src/runtime/posthog-flags.ts',
7
+ import.meta.url,
8
+ ).pathname,
9
+ }));
10
+
11
+ // This suite mocks the runtime's internal posthog-flags module by source path,
12
+ // which only exists inside the funnelsgrove monorepo. Standalone funnels
13
+ // scaffolded from the template resolve @funnelsgrove/runtime from node_modules
14
+ // and cover the same routing logic through use-funnel-flow-controller.test.ts.
15
+ const insideMonorepo = existsSync(posthogFlagsPath);
16
+
17
+ vi.mock(posthogFlagsPath, () => ({
18
+ resolveExperimentVariant: vi.fn(),
19
+ bootstrapPostHog: vi.fn(),
20
+ isPostHogReady: vi.fn(() => true),
21
+ identifyPostHog: vi.fn(),
22
+ getPostHog: vi.fn(() => null),
23
+ }));
24
+
25
+ import type {
26
+ FunnelManifest,
27
+ FunnelManifestExperiment,
28
+ } from '@funnelsgrove/runtime';
29
+ import {
30
+ resolveExperimentAssignment,
31
+ resolveExperimentVariant,
32
+ resolveNextStepId,
33
+ } from '@funnelsgrove/runtime';
34
+ import { computeRenderSuspended } from './use-funnel-flow-controller';
35
+
36
+ const mockedResolveVariant = vi.mocked(resolveExperimentVariant);
37
+
38
+ const heroExperiment: FunnelManifestExperiment = {
39
+ experimentId: 'exp_hero',
40
+ stepId: 'hero',
41
+ variants: [
42
+ { variantKey: 'control', routeToStepId: 'hero_v1' },
43
+ { variantKey: 'variant_a', routeToStepId: 'hero_v2' },
44
+ ],
45
+ };
46
+
47
+ const manifest: FunnelManifest = {
48
+ templateArchitectureVersion: 1,
49
+ stepContractVersion: 2,
50
+ meta: { title: 'Hero Experiment Funnel', description: 'integration fixture' },
51
+ viewport: { width: 390, height: 844 },
52
+ assets: {},
53
+ steps: [
54
+ {
55
+ id: 'hero',
56
+ name: 'hero',
57
+ path: '/hero',
58
+ filePath: 'src/steps/hero.tsx',
59
+ componentKey: 'hero',
60
+ type: 'intro_hero',
61
+ title: 'Hero',
62
+ },
63
+ {
64
+ id: 'hero_v1',
65
+ name: 'hero-v1',
66
+ path: '/hero-v1',
67
+ filePath: 'src/steps/hero-v1.tsx',
68
+ componentKey: 'heroV1',
69
+ type: 'intro_hero',
70
+ title: 'Hero v1',
71
+ },
72
+ {
73
+ id: 'hero_v2',
74
+ name: 'hero-v2',
75
+ path: '/hero-v2',
76
+ filePath: 'src/steps/hero-v2.tsx',
77
+ componentKey: 'heroV2',
78
+ type: 'intro_hero',
79
+ title: 'Hero v2',
80
+ },
81
+ ],
82
+ edgesByStepId: {
83
+ hero: [{ toStepId: 'hero_v1' }],
84
+ },
85
+ experiments: [heroExperiment],
86
+ };
87
+
88
+ // The controller's renderedStepId is derived by passing the active step's
89
+ // experiment through resolveExperimentAssignment and routing to the matched
90
+ // variant's routeToStepId (falling back to control). That is exactly the
91
+ // function under test here. See apps/funnel-template/src/runtime/use-funnel-flow-controller.ts
92
+ // (renderedStepId useMemo).
93
+ const computeRenderedStepIdForActiveExperiment = (
94
+ experiment: FunnelManifestExperiment,
95
+ ): string => {
96
+ const assignment = resolveExperimentAssignment({
97
+ experimentId: experiment.experimentId,
98
+ variants: experiment.variants,
99
+ });
100
+ return assignment?.routeToStepId ?? experiment.stepId;
101
+ };
102
+
103
+ describe.skipIf(!insideMonorepo)('PostHog variant resolution (integration)', () => {
104
+ beforeEach(() => {
105
+ mockedResolveVariant.mockReset();
106
+ });
107
+
108
+ afterEach(() => {
109
+ vi.clearAllMocks();
110
+ });
111
+
112
+ it('routes to the variant_a target when PostHog resolves exp_hero to variant_a', () => {
113
+ mockedResolveVariant.mockReturnValue('variant_a');
114
+
115
+ expect(computeRenderedStepIdForActiveExperiment(heroExperiment)).toBe('hero_v2');
116
+
117
+ expect(
118
+ resolveNextStepId({
119
+ manifest,
120
+ currentStepId: 'hero',
121
+ attributes: {},
122
+ resolveExperimentVariantKey: (experiment) =>
123
+ resolveExperimentVariant(experiment.experimentId) ?? null,
124
+ }),
125
+ ).toBe('hero_v2');
126
+ });
127
+
128
+ it('falls back to control (hero_v1) when PostHog is not ready / returns undefined', () => {
129
+ mockedResolveVariant.mockReturnValue(undefined);
130
+
131
+ expect(computeRenderedStepIdForActiveExperiment(heroExperiment)).toBe('hero_v1');
132
+
133
+ expect(
134
+ resolveNextStepId({
135
+ manifest,
136
+ currentStepId: 'hero',
137
+ attributes: {},
138
+ resolveExperimentVariantKey: (experiment) =>
139
+ resolveExperimentVariant(experiment.experimentId) ?? null,
140
+ }),
141
+ ).toBe('hero_v1');
142
+ });
143
+
144
+ it('falls back to control (hero_v1) when PostHog resolves to an unknown variant key', () => {
145
+ mockedResolveVariant.mockReturnValue('phantom');
146
+
147
+ expect(computeRenderedStepIdForActiveExperiment(heroExperiment)).toBe('hero_v1');
148
+
149
+ expect(
150
+ resolveNextStepId({
151
+ manifest,
152
+ currentStepId: 'hero',
153
+ attributes: {},
154
+ resolveExperimentVariantKey: (experiment) =>
155
+ resolveExperimentVariant(experiment.experimentId) ?? null,
156
+ }),
157
+ ).toBe('hero_v1');
158
+ });
159
+
160
+ it('suspends render on an experiment step while PostHog is not ready', () => {
161
+ expect(
162
+ computeRenderSuspended({
163
+ activeStepId: 'hero',
164
+ postHogReady: false,
165
+ experiments: manifest.experiments,
166
+ }),
167
+ ).toBe(true);
168
+
169
+ expect(
170
+ computeRenderSuspended({
171
+ activeStepId: 'hero',
172
+ postHogReady: true,
173
+ experiments: manifest.experiments,
174
+ }),
175
+ ).toBe(false);
176
+ });
177
+
178
+ it('does not suspend render on an experiment step in preview runtime', () => {
179
+ expect(
180
+ computeRenderSuspended({
181
+ activeStepId: 'hero',
182
+ postHogReady: false,
183
+ experiments: manifest.experiments,
184
+ isPreviewRuntime: true,
185
+ }),
186
+ ).toBe(false);
187
+ });
188
+
189
+ it('renders experiment variants on the source URL before normal continuation', () => {
190
+ const adapterSource = readFileSync(new URL('./use-funnel-flow-controller.ts', import.meta.url), 'utf8');
191
+ const runtimeSource = readFileSync(
192
+ new URL('../../../funnel-runtime/src/runtime/use-funnel-flow-controller.ts', import.meta.url),
193
+ 'utf8',
194
+ );
195
+
196
+ expect(adapterSource).toContain('useFunnelFlowController as useSharedFunnelFlowController');
197
+ expect(runtimeSource).toContain('activeExperimentForStep');
198
+ expect(runtimeSource).not.toContain('goToStep(renderedStepId)');
199
+ expect(runtimeSource).toContain('goToStep(resolveNextStepId(renderedStepId');
200
+ expect(runtimeSource).toContain('ignoreExperiment: Boolean(activeExperimentForStep && renderedStepId === safeActiveStepId)');
201
+ expect(runtimeSource).toContain('if (prevId === renderedStepId)');
202
+ });
203
+ });
@@ -0,0 +1,76 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { readFileSync } from 'node:fs';
3
+ import { computeRenderSuspended } from './use-funnel-flow-controller';
4
+
5
+ describe('computeRenderSuspended', () => {
6
+ const experiments = [
7
+ { stepId: 'step-paywall' },
8
+ { stepId: 'step-headline' },
9
+ ];
10
+
11
+ it('renderSuspended is true when active step has experiment and posthog is not ready', () => {
12
+ expect(
13
+ computeRenderSuspended({
14
+ activeStepId: 'step-paywall',
15
+ postHogReady: false,
16
+ experiments,
17
+ }),
18
+ ).toBe(true);
19
+ });
20
+
21
+ it('renderSuspended is false when active step has no experiment', () => {
22
+ expect(
23
+ computeRenderSuspended({
24
+ activeStepId: 'step-intro',
25
+ postHogReady: false,
26
+ experiments,
27
+ }),
28
+ ).toBe(false);
29
+ });
30
+
31
+ it('renderSuspended is false after posthog becomes ready', () => {
32
+ expect(
33
+ computeRenderSuspended({
34
+ activeStepId: 'step-paywall',
35
+ postHogReady: true,
36
+ experiments,
37
+ }),
38
+ ).toBe(false);
39
+ });
40
+
41
+ it('renderSuspended is false in preview runtime even when posthog is not ready', () => {
42
+ expect(
43
+ computeRenderSuspended({
44
+ activeStepId: 'step-paywall',
45
+ postHogReady: false,
46
+ experiments,
47
+ isPreviewRuntime: true,
48
+ }),
49
+ ).toBe(false);
50
+ });
51
+ });
52
+
53
+ describe('qualified visitor analytics', () => {
54
+ it('delegates flow behavior to the shared runtime controller with template analytics', () => {
55
+ const source = readFileSync(new URL('./use-funnel-flow-controller.ts', import.meta.url), 'utf8');
56
+
57
+ expect(source).toContain('useFunnelFlowController as useSharedFunnelFlowController');
58
+ expect(source).toContain('return useSharedFunnelFlowController<FunnelStepId>({');
59
+ expect(source).toContain('analytics: publicAnalyticsSdk');
60
+ expect(source).toContain('stepContractVersion: funnelManifest.stepContractVersion');
61
+ expect(source).toContain('defaultStepId: defaultFunnelStepId');
62
+ expect(source).toContain('stepSequence: funnelStepSequence');
63
+ expect(source).toContain('stepById');
64
+ expect(source).toContain('stepComponentById');
65
+ expect(source).toContain('funnelExperiments');
66
+ });
67
+
68
+ it('does not carry a local copy of shared controller internals', () => {
69
+ const source = readFileSync(new URL('./use-funnel-flow-controller.ts', import.meta.url), 'utf8');
70
+
71
+ expect(source).not.toContain('FIRST_STEP_ENGAGEMENT_THRESHOLD_MS');
72
+ expect(source).not.toContain('apiService.bootstrapSession');
73
+ expect(source).not.toContain('apiService.pingUserContext');
74
+ expect(source).not.toContain('window.setTimeout');
75
+ });
76
+ });
@@ -0,0 +1,53 @@
1
+ 'use client';
2
+
3
+ import {
4
+ computeRenderSuspended,
5
+ useFunnelFlowController as useSharedFunnelFlowController,
6
+ } from '@funnelsgrove/runtime';
7
+ import { publicAnalyticsSdk } from '@funnelsgrove/analytics';
8
+ import { funnelManifest } from '@/config/funnel.manifest';
9
+ import { stepById, stepComponentById } from '@/steps';
10
+ import {
11
+ defaultFunnelStepId,
12
+ funnelExperiments,
13
+ funnelStepSequence,
14
+ getChoiceTargetsForStep,
15
+ getPathForStep,
16
+ getSequentialNextStepId,
17
+ getStepIdFromPath,
18
+ isFunnelStepId,
19
+ resolveConfiguredNextStep,
20
+ resolveRuntimeInitialStepId,
21
+ type FunnelStepId,
22
+ } from './funnel-runtime';
23
+
24
+ type FunnelFlowControllerInput = {
25
+ initialStepId?: FunnelStepId;
26
+ lockToInitialStep?: boolean;
27
+ };
28
+
29
+ export { computeRenderSuspended };
30
+
31
+ export function useFunnelFlowController({
32
+ initialStepId = defaultFunnelStepId,
33
+ lockToInitialStep = false,
34
+ }: FunnelFlowControllerInput = {}) {
35
+ return useSharedFunnelFlowController<FunnelStepId>({
36
+ analytics: publicAnalyticsSdk,
37
+ stepContractVersion: funnelManifest.stepContractVersion,
38
+ initialStepId,
39
+ lockToInitialStep,
40
+ defaultStepId: defaultFunnelStepId,
41
+ stepSequence: funnelStepSequence,
42
+ stepById,
43
+ stepComponentById,
44
+ funnelExperiments,
45
+ getPathForStep,
46
+ getStepIdFromPath,
47
+ getSequentialNextStepId,
48
+ getChoiceTargetsForStep,
49
+ isFunnelStepId,
50
+ resolveConfiguredNextStep,
51
+ resolveRuntimeInitialStepId,
52
+ });
53
+ }
@@ -0,0 +1,29 @@
1
+ import type { LocalizedStepContent } from '@funnelsgrove/runtime';
2
+
3
+ export type EmailCaptureLocaleContent = {
4
+ kicker: string;
5
+ title: string;
6
+ description: string;
7
+ fieldLabel: string;
8
+ placeholder: string;
9
+ submitLabel: string;
10
+ submittingLabel: string;
11
+ saveErrorMessage: string;
12
+ };
13
+
14
+ export const emailCaptureContent = {
15
+ defaultLocale: 'en',
16
+ locales: {
17
+ en: {
18
+ kicker: 'Before we unlock your plan',
19
+ title: 'Where should we send your access link?',
20
+ description:
21
+ 'Enter the email address you want to use for access details, updates, and recovery.',
22
+ fieldLabel: 'Email address',
23
+ placeholder: 'you@example.com',
24
+ submitLabel: 'Continue',
25
+ submittingLabel: 'Saving...',
26
+ saveErrorMessage: 'Unable to save email',
27
+ },
28
+ },
29
+ } as const satisfies LocalizedStepContent<EmailCaptureLocaleContent>;