@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,770 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import path from 'node:path';
3
+ import { describe, expect, it } from 'vitest';
4
+
5
+ describe('ClaimBee paywall source', () => {
6
+ it('uses the local checkout runtime config when creating subscription-backed custom checkout intents', () => {
7
+ const stepSource = readFileSync(
8
+ path.resolve(__dirname, './step-32-paywall.tsx'),
9
+ 'utf8',
10
+ );
11
+
12
+ expect(stepSource).toContain("from '@/runtime/checkout-runtime-config'");
13
+ expect(stepSource).toContain('runtimeConfig: checkoutRuntimeConfig');
14
+ expect(stepSource).toContain('useStripeSubscriptionCheckoutSession');
15
+ expect(stepSource).not.toContain('Boolean(checkoutRuntimeConfig.funnelSdkPublishableKey)');
16
+ expect(stepSource).not.toContain('createStripeSubscriptionCheckout');
17
+ expect(stepSource).not.toContain('createStripePaymentIntent');
18
+ expect(stepSource).not.toContain('redirectToStripeCheckout');
19
+ });
20
+
21
+ it('renders the custom checkout trigger and separate claim CTA', () => {
22
+ const stepSource = readFileSync(
23
+ path.resolve(__dirname, './step-32-paywall.tsx'),
24
+ 'utf8',
25
+ );
26
+ const contentSource = readFileSync(
27
+ path.resolve(__dirname, './content/step-32-paywall.content.ts'),
28
+ 'utf8',
29
+ );
30
+
31
+ expect(stepSource).toContain('SharedCheckoutSpecialOfferDialog');
32
+ expect(stepSource).toContain('ApplePaySubscriptionCheckoutSlot');
33
+ expect(stepSource).toContain('GooglePaySubscriptionCheckoutSlot');
34
+ expect(stepSource).toContain('SharedStripeCheckoutV2Dialog');
35
+ expect(stepSource).not.toContain('StripeExpressCheckoutElement');
36
+ expect(stepSource).not.toContain('createStripeSubscriptionCheckout');
37
+ expect(stepSource).not.toContain('redirectToStripeCheckout');
38
+ expect(stepSource).not.toContain('createStripePaymentIntent');
39
+ expect(stepSource).toContain("from '@/steps/content/step-32-paywall.content'");
40
+ expect(stepSource).toContain('useClaimbeeStepContent(stepPaywallId, stepPaywallContent)');
41
+ expect(contentSource).toContain("topBarDiscountLabel: '⏰ Discount Expires in'");
42
+ expect(contentSource).toContain("topBarCtaLabel: 'GET MY PLAN'");
43
+ expect(contentSource).toContain("cardButtonLabel: 'GET MY PLAN'");
44
+ expect(contentSource).toContain("title: 'Checkout'");
45
+ expect(contentSource).toContain("walletButtonLabel: 'Apple Pay'");
46
+ expect(contentSource).toContain("googlePayPlaceholderLabel: 'Subscribe with Google Pay'");
47
+ expect(contentSource).toContain("paypalButtonLabel: 'PayPal Buy Now'");
48
+ expect(contentSource).toContain("originalPriceLabel: 'Regular Price'");
49
+ expect(contentSource).toContain("discountLabelTemplate: '{percent}% OFF'");
50
+ expect(contentSource).toContain("promoCodeLabel: 'Applied promo code:'");
51
+ expect(contentSource).toContain("specialOffer:");
52
+ expect(contentSource).toContain("src: '/paywall/special-offer-gift.png'");
53
+ expect(stepSource).toContain('imageSrc={content.checkout.specialOffer.image.src}');
54
+ expect(stepSource).toContain("className='paywall-v2-scroll-cta paywall-v2-vibe-cta'");
55
+ expect(stepSource).toContain("className='paywall-v2-card-button paywall-v2-vibe-cta'");
56
+ expect(stepSource).toContain("className='paywall-v2-apple-pay-button'");
57
+ expect(stepSource).toContain("className='paywall-v2-google-pay-button'");
58
+ expect(stepSource).not.toContain('Continue with other payment options');
59
+ });
60
+
61
+ it('keeps the timer and top CTA in a sticky bar with minute and second labels under blue digits', () => {
62
+ const stepSource = readFileSync(
63
+ path.resolve(__dirname, './step-32-paywall.tsx'),
64
+ 'utf8',
65
+ );
66
+ const contentSource = readFileSync(
67
+ path.resolve(__dirname, './content/step-32-paywall.content.ts'),
68
+ 'utf8',
69
+ );
70
+
71
+ const topBarStylesStart = stepSource.indexOf('.paywall-v2-top-row {');
72
+ const topBarStylesEnd = stepSource.indexOf('.paywall-v2-head {');
73
+ const topBarStyles = stepSource.slice(topBarStylesStart, topBarStylesEnd);
74
+ const compactTopBarStylesStart = stepSource.indexOf('@container (max-width: 429px) {');
75
+ const compactTopBarStylesEnd = stepSource.indexOf(' .paywall-v2-plan-row {', compactTopBarStylesStart);
76
+ const compactTopBarStyles = stepSource.slice(compactTopBarStylesStart, compactTopBarStylesEnd);
77
+
78
+ expect(topBarStylesStart).toBeGreaterThan(-1);
79
+ expect(topBarStyles).toContain('position: sticky;');
80
+ expect(topBarStyles).toContain('top: 0;');
81
+ expect(topBarStyles).toContain('z-index: 20;');
82
+ expect(topBarStyles).toContain('min-height: 92px;');
83
+ expect(topBarStyles).toContain('margin: 0 -24px 0;');
84
+ expect(topBarStyles).toContain('padding: 8px 24px 10px;');
85
+ expect(topBarStyles).toContain('background: #ecf7ff;');
86
+ expect(topBarStyles).toContain('border-bottom: 1px solid rgb(63 81 181 / 10%);');
87
+ expect(topBarStyles).toContain('.paywall-v2-top-label {');
88
+ expect(topBarStyles).toContain('font-size: 13px;');
89
+ expect(topBarStyles).toContain('.paywall-v2-top-controls {');
90
+ expect(topBarStyles).toContain('font-size: 40px;');
91
+ expect(topBarStyles).toContain('grid-template-columns: minmax(0, 1fr) 14px minmax(0, 1fr);');
92
+ expect(topBarStyles).toContain('color: var(--color-secondary-text);');
93
+ expect(topBarStyles).toContain('max-width: 188px;');
94
+ expect(topBarStyles).toContain('min-height: 46px;');
95
+ expect(topBarStyles).toContain('padding: 0 14px;');
96
+ expect(topBarStyles).toContain('font-size: 16px;');
97
+ expect(topBarStyles).toContain('background: var(--color-secondary-text);');
98
+ expect(topBarStyles).toContain('border-radius: 16px;');
99
+ expect(stepSource).toContain('.paywall-v2-vibe-cta:not(:disabled) {');
100
+ expect(stepSource).toContain('animation: paywall-v2-cta-beat 2400ms ease-in-out infinite;');
101
+ expect(stepSource).toContain('animation: paywall-v2-cta-wave 2400ms ease-out infinite;');
102
+ expect(stepSource).toContain('@keyframes paywall-v2-cta-beat {');
103
+ expect(stepSource).toContain('@keyframes paywall-v2-cta-wave {');
104
+ expect(stepSource).toContain('@media (prefers-reduced-motion: reduce) {');
105
+ expect(stepSource).toContain("className='paywall-v2-countdown-units'");
106
+ expect(stepSource).toContain("className='paywall-v2-top-label'");
107
+ expect(stepSource).toContain("className='paywall-v2-top-controls'");
108
+ expect(contentSource).toContain("countdownUnits: ['minutes', 'seconds']");
109
+ expect(stepSource).not.toContain('Discount expired');
110
+ expect(stepSource).not.toContain("className='paywall-v2-countdown-label'");
111
+ expect(topBarStyles).not.toContain('background: rgb(148 165 255 / 20%);');
112
+ expect(topBarStyles).not.toContain('backdrop-filter: blur(12px);');
113
+ expect(stepSource).toContain('padding: 0 24px 64px;');
114
+ expect(stepSource).toContain('.paywall-v2-head {');
115
+ expect(stepSource).toContain('margin-top: 16px;');
116
+ expect(compactTopBarStylesStart).toBeGreaterThan(-1);
117
+ expect(compactTopBarStylesEnd).toBeGreaterThan(compactTopBarStylesStart);
118
+ expect(compactTopBarStyles).toContain('min-height: 92px;');
119
+ expect(compactTopBarStyles).toContain('padding: 8px 16px 10px;');
120
+ expect(compactTopBarStyles).toContain('font-size: 40px;');
121
+ expect(compactTopBarStyles).toContain('min-height: 46px;');
122
+ expect(compactTopBarStyles).not.toContain('min-height: 108px;');
123
+ });
124
+
125
+ it('scrolls the header CTA to the selected plan card and opens the custom subscription checkout', () => {
126
+ const stepSource = readFileSync(
127
+ path.resolve(__dirname, './step-32-paywall.tsx'),
128
+ 'utf8',
129
+ );
130
+ const contentSource = readFileSync(
131
+ path.resolve(__dirname, './content/step-32-paywall.content.ts'),
132
+ 'utf8',
133
+ );
134
+
135
+ expect(stepSource).toContain('useStripeSubscriptionCheckoutSession');
136
+ expect(stepSource).toContain('plan: selectedPlan');
137
+ expect(stepSource).toContain('couponId: checkoutPromoActive ? selectedDisplayPlan?.couponId ?? null : null');
138
+ expect(stepSource).toContain('checkoutSession.prepareCardCheckout()');
139
+ expect(stepSource).toContain("successUrl.searchParams.set('source', 'stripe-elements');");
140
+ expect(stepSource).not.toContain('publicAnalyticsSdk.trackCheckoutStarted({');
141
+ expect(stepSource).toContain('stepContractVersion: funnelManifest.stepContractVersion');
142
+ expect(stepSource).toContain("successUrl.searchParams.set('user_id', user.id);");
143
+ expect(stepSource).toContain("onClick={() => void startCheckout()}");
144
+ expect(stepSource).toContain("document.getElementById(`paywall-v2-plan-${currentPlanId}`)");
145
+ expect(stepSource).toContain("scrollIntoView({ behavior: 'smooth', block: 'center' })");
146
+ expect(stepSource).toContain("id={`paywall-v2-plan-${plan.id}`}");
147
+ expect(contentSource).toContain("walletPlaceholderLabel: 'Subscribe with Apple Pay'");
148
+ expect(stepSource).not.toContain('window.open(');
149
+ expect(stepSource).not.toContain("className='paywall-v2-checkout-overlay'");
150
+ });
151
+
152
+ it('uses subscription-backed Payment Element checkout instead of a one-time PaymentIntent checkout path', () => {
153
+ const stepSource = readFileSync(
154
+ path.resolve(__dirname, './step-32-paywall.tsx'),
155
+ 'utf8',
156
+ );
157
+
158
+ expect(stepSource).toContain('useStripeSubscriptionCheckoutSession');
159
+ expect(stepSource).toContain('checkoutSession.activeClientSecret');
160
+ expect(stepSource).toContain('<SharedStripeCheckoutV2Dialog');
161
+ expect(stepSource).toContain('<ApplePaySubscriptionCheckoutSlot');
162
+ expect(stepSource).not.toContain('<StripeExpressCheckoutElement');
163
+ expect(stepSource).not.toContain('createStripePaymentIntent');
164
+ expect(stepSource).not.toContain('createStripeSubscriptionCheckout');
165
+ expect(stepSource).not.toContain('redirectToStripeCheckout');
166
+ expect(stepSource).not.toContain('/sdk/public/payments/intents');
167
+ expect(stepSource).toContain('if (checkoutSession.loading.card) {');
168
+ expect(stepSource).toContain("className='paywall-v2-loading-screen'");
169
+ expect(stepSource).toContain("aria-label='Loading checkout'");
170
+ });
171
+
172
+ it('claims paywall completion only when payment information is submitted', () => {
173
+ const stepSource = readFileSync(
174
+ path.resolve(__dirname, './step-32-paywall.tsx'),
175
+ 'utf8',
176
+ );
177
+ const paymentInfoHandler = stepSource.slice(
178
+ stepSource.indexOf('const trackPaymentInfoSubmitted = () => {'),
179
+ stepSource.indexOf('const preparePaywallWalletCheckout'),
180
+ );
181
+ const checkoutStartHandler = stepSource.slice(
182
+ stepSource.indexOf('const startCheckout = async () => {'),
183
+ stepSource.indexOf('const trackPaymentInfoSubmitted'),
184
+ );
185
+
186
+ expect(stepSource).toContain('const { user, setUser, attributes, featureFlags, completeStep } = useFunnel();');
187
+ expect(paymentInfoHandler).toContain('completeStep(analyticsStepId, {');
188
+ expect(paymentInfoHandler).toContain('planId: selectedDisplayPlan.id');
189
+ expect(checkoutStartHandler).not.toContain('completeStep(');
190
+ });
191
+
192
+ it('tracks the staged discount timer, returns first checkout closes to the special offer, and forwards the active coupon into subscription checkout', () => {
193
+ const stepSource = readFileSync(
194
+ path.resolve(__dirname, './step-32-paywall.tsx'),
195
+ 'utf8',
196
+ );
197
+
198
+ expect(stepSource).toContain("from '@/config/billing.plans'");
199
+ expect(stepSource).toContain('billingDiscounts');
200
+ expect(stepSource).not.toContain("from '@/config/claimbee-paywall.config'");
201
+ expect(stepSource).toContain('CLAIMBEE_PAYWALL_SECOND_COUPON_ID');
202
+ expect(stepSource).toContain('resolvePaywallDiscountState');
203
+ expect(stepSource).toContain('advancePaywallDiscountState');
204
+ expect(stepSource).toContain('serializePaywallDiscountState');
205
+ expect(stepSource).toContain('readPaywallStateValue');
206
+ expect(stepSource).toContain('updatePaywallStateValue');
207
+ expect(stepSource).toContain('CLAIMBEE_PAYWALL_DISCOUNT_STORAGE_KEYS');
208
+ expect(stepSource).toContain('const [specialOfferDialogOpen, setSpecialOfferDialogOpen] = useState(() => {');
209
+ expect(stepSource).toContain('activateSecondPaywallDiscount');
210
+ expect(stepSource).toContain("searchParams.get('checkout_offer') === 'first'");
211
+ expect(stepSource).toContain('closeCheckoutDialog');
212
+ expect(stepSource).toContain('handleSpecialOfferAccept');
213
+ expect(stepSource).toContain('SharedCheckoutSpecialOfferDialog');
214
+ expect(stepSource).toContain('const paywallStepRef = useRef<HTMLDivElement | null>(null);');
215
+ expect(stepSource).toContain("paywallStepRef.current?.scrollTo({ top: 0, behavior: 'smooth' });");
216
+ expect(stepSource).toContain("ref={paywallStepRef}");
217
+ expect(stepSource).toContain('discountState.firstStartedAtMs');
218
+ expect(stepSource).toContain('discountState.secondStartedAtMs');
219
+ expect(stepSource).not.toContain('readWindowStorageValue');
220
+ expect(stepSource).not.toContain('writeWindowStorageValue');
221
+ expect(stepSource).toContain('couponId: checkoutPromoActive ? selectedDisplayPlan?.couponId ?? null : null');
222
+ expect(stepSource).not.toContain('couponId: selectedDisplayPlan.hasActiveDiscount');
223
+ expect(stepSource).toContain('setSpecialOfferDialogOpen(false);');
224
+ expect(stepSource).not.toContain("claimbeeCheckout");
225
+ expect(stepSource).not.toContain('window.history.replaceState');
226
+ expect(stepSource).not.toContain('discountState.remainingSeconds % 15 === 0');
227
+ });
228
+
229
+ it('waits for the stored discount state before rendering the paywall and shows a white loading screen first', () => {
230
+ const stepSource = readFileSync(
231
+ path.resolve(__dirname, './step-32-paywall.tsx'),
232
+ 'utf8',
233
+ );
234
+ const contentSource = readFileSync(
235
+ path.resolve(__dirname, './content/step-32-paywall.content.ts'),
236
+ 'utf8',
237
+ );
238
+
239
+ expect(stepSource).toContain('const [discountStateReady, setDiscountStateReady] = useState(false);');
240
+ expect(stepSource).toContain('setDiscountStateReady(true);');
241
+ expect(stepSource).toContain('if (!discountStateReady) {');
242
+ expect(stepSource).toContain("className='paywall-v2-loading-screen'");
243
+ expect(stepSource).toContain("className='paywall-v2-loading-spinner'");
244
+ expect(stepSource).toContain('background: #fff;');
245
+ expect(contentSource).toContain("loadingLabel: 'Loading...'");
246
+ });
247
+
248
+ it('keeps checkout dialog copy in the step content contract and editor', () => {
249
+ const contentSource = readFileSync(
250
+ path.resolve(__dirname, './content/step-32-paywall.content.ts'),
251
+ 'utf8',
252
+ );
253
+ const editorSource = readFileSync(
254
+ path.resolve(__dirname, './editor/step-32-paywall.editor.ts'),
255
+ 'utf8',
256
+ );
257
+
258
+ expect(contentSource).toContain("title: 'Checkout'");
259
+ expect(contentSource).toContain("cardSubmitLabel: 'Confirm payment'");
260
+ expect(contentSource).toContain("specialOffer:");
261
+ expect(editorSource).toContain("id: 'checkout'");
262
+ expect(editorSource).toContain("path: 'topBarDiscountLabel'");
263
+ expect(editorSource).toContain("path: 'checkout.title'");
264
+ expect(editorSource).toContain("path: 'checkout.originalPriceLabel'");
265
+ expect(editorSource).toContain("path: 'checkout.promoCodeLabel'");
266
+ expect(editorSource).toContain("path: 'checkout.specialOffer.image'");
267
+ expect(editorSource).toContain("path: 'checkout.specialOffer.buttonLabel'");
268
+ });
269
+
270
+ it('passes customer email and active discount state into subscription checkout', () => {
271
+ const stepSource = readFileSync(
272
+ path.resolve(__dirname, './step-32-paywall.tsx'),
273
+ 'utf8',
274
+ );
275
+
276
+ expect(stepSource).toContain('customerEmail: checkoutEmail || user.email');
277
+ expect(stepSource).toContain('customerEmailLabel={content.checkout.customerEmailLabel}');
278
+ expect(stepSource).toContain('customerEmailPlaceholder={content.checkout.customerEmailPlaceholder}');
279
+ expect(stepSource).toContain('apiService.updateUser');
280
+ expect(stepSource).toContain('collectCurrentFunnelAttribution');
281
+ expect(stepSource).toContain(
282
+ 'const { user, setUser, attributes, featureFlags, completeStep } = useFunnel();',
283
+ );
284
+ expect(stepSource).toContain('const checkoutPromoActive =');
285
+ expect(stepSource).toContain("discountState.status === 'active'");
286
+ expect(stepSource).toContain('summaryLabel: checkoutSummaryLabel');
287
+ });
288
+
289
+ it('renders the checkout CTAs immediately after the plans and before the feature grid', () => {
290
+ const stepSource = readFileSync(
291
+ path.resolve(__dirname, './step-32-paywall.tsx'),
292
+ 'utf8',
293
+ );
294
+
295
+ const plansIndex = stepSource.indexOf("id='paywall-v2-plans'");
296
+ const paymentStackIndex = stepSource.indexOf("className='paywall-v2-payment-stack'");
297
+ const featureGridIndex = stepSource.indexOf("className='paywall-v2-feature-grid'");
298
+
299
+ expect(plansIndex).toBeGreaterThan(-1);
300
+ expect(paymentStackIndex).toBeGreaterThan(plansIndex);
301
+ expect(featureGridIndex).toBeGreaterThan(paymentStackIndex);
302
+ expect(stepSource).toContain('ApplePaySubscriptionCheckoutSlot');
303
+ expect(stepSource).toContain('GooglePaySubscriptionCheckoutSlot');
304
+ expect(stepSource).toContain('.paywall-v2-apple-pay-button,');
305
+ expect(stepSource).toContain('.paywall-v2-google-pay-button {');
306
+ expect(stepSource).toContain('min-height: 64px;');
307
+ expect(stepSource).toContain('border-radius: 16px;');
308
+ expect(stepSource).not.toContain('paywallApplePayAvailable');
309
+ expect(stepSource).not.toContain('APPLE_PAY_QR_CODE_URL');
310
+ expect(stepSource).toContain('beforeConfirm={preparePaywallWalletCheckout}');
311
+ expect(stepSource).not.toContain("className='paywall-v2-apple-pay-express'");
312
+ expect(stepSource).not.toContain("className='paywall-v2-plan-image-card'");
313
+ expect(stepSource).not.toContain("src='/paywall/paywall-image.png'");
314
+ });
315
+
316
+ it('keeps the mobile paywall geometry inside the viewport without horizontal scroll', () => {
317
+ const stepSource = readFileSync(
318
+ path.resolve(__dirname, './step-32-paywall.tsx'),
319
+ 'utf8',
320
+ );
321
+
322
+ expect(stepSource).toContain('overflow-x: hidden;');
323
+ expect(stepSource).toContain('grid-template-columns: minmax(128px, 164px) minmax(132px, 1fr);');
324
+ expect(stepSource).toContain('width: min(100%, 390px);');
325
+ expect(stepSource).toContain('margin: -12px auto 0;');
326
+ expect(stepSource).toContain('min-width: 0;');
327
+ expect(stepSource).toContain('width: min(34%, 118px);');
328
+ expect(stepSource).toContain('@container (max-width: 429px) {');
329
+ expect(stepSource).toContain('padding: 0 16px 48px;');
330
+ expect(stepSource).toContain('margin: 0 -16px 0;');
331
+ });
332
+
333
+ it('routes Apple Pay and Google Pay controls through subscription-backed Express Checkout', () => {
334
+ const stepSource = readFileSync(
335
+ path.resolve(__dirname, './step-32-paywall.tsx'),
336
+ 'utf8',
337
+ );
338
+
339
+ expect(stepSource).toContain("<div className='paywall-v2-apple-pay-shell'>");
340
+ expect(stepSource).toContain("<div className='paywall-v2-google-pay-shell'>");
341
+ expect(stepSource).toContain('<ApplePaySubscriptionCheckoutSlot');
342
+ expect(stepSource).toContain('<GooglePaySubscriptionCheckoutSlot');
343
+ expect(stepSource).toContain('onUnavailableClick={openCardCheckoutFromUnavailableWallet}');
344
+ expect(stepSource).toContain('session={checkoutSession}');
345
+ expect(stepSource).not.toContain('onClick={() => void startWalletCheckout()}');
346
+ expect(stepSource).not.toContain('StripeExpressCheckoutElement');
347
+ expect(stepSource).not.toContain('createStripeSubscriptionCheckout');
348
+ expect(stepSource).not.toContain('redirectToStripeCheckout');
349
+ expect(stepSource).not.toContain('applepaycode/index.html');
350
+ expect(stepSource).not.toContain('href={paywallApplePayQrFallbackAllowed');
351
+ });
352
+
353
+ it('carries paywall wallet availability into checkout so reopened checkout keeps known wallets visible', () => {
354
+ const stepSource = readFileSync(
355
+ path.resolve(__dirname, './step-32-paywall.tsx'),
356
+ 'utf8',
357
+ );
358
+
359
+ expect(stepSource).toContain('type PaywallWalletAvailabilityState = {');
360
+ expect(stepSource).toContain('const [paywallWalletAvailability, setPaywallWalletAvailability] = useState<PaywallWalletAvailabilityState>');
361
+ expect(stepSource).toContain("updatePaywallWalletAvailability('applePay', available)");
362
+ expect(stepSource).toContain("updatePaywallWalletAvailability('googlePay', available)");
363
+ expect(stepSource).toContain('const checkoutInitialWalletAvailable = walletPaymentMethods.some(');
364
+ expect(stepSource).toContain('initialWalletAvailable={checkoutInitialWalletAvailable}');
365
+ expect(stepSource).toContain('onAvailabilityChange={handleApplePayAvailabilityChange}');
366
+ expect(stepSource).toContain('onAvailabilityChange={handleGooglePayAvailabilityChange}');
367
+ });
368
+
369
+ it('prepares the real Apple Pay element on load instead of waiting for the placeholder tap', () => {
370
+ const stepSource = readFileSync(
371
+ path.resolve(__dirname, './step-32-paywall.tsx'),
372
+ 'utf8',
373
+ );
374
+
375
+ expect(stepSource).toContain('useStripeSubscriptionCheckoutSession');
376
+ expect(stepSource).toContain('ApplePaySubscriptionCheckoutSlot');
377
+ expect(stepSource).toContain('checkoutSession.prepareCardCheckout');
378
+ expect(stepSource).toContain('checkoutSession.activeClientSecret');
379
+ expect(stepSource).not.toContain('const autoWalletIntentKeyRef = useRef<string | null>(null);');
380
+ expect(stepSource).not.toContain('createStripeSubscriptionCheckout');
381
+ expect(stepSource).not.toContain('StripeExpressCheckoutElement');
382
+ expect(stepSource).not.toContain('if (!available && clientSecret) {');
383
+ });
384
+
385
+ it('keeps the paywall Apple Pay control visible and confirms the subscription client secret', () => {
386
+ const stepSource = readFileSync(
387
+ path.resolve(__dirname, './step-32-paywall.tsx'),
388
+ 'utf8',
389
+ );
390
+
391
+ expect(stepSource).toContain('useStripeSubscriptionCheckoutSession');
392
+ expect(stepSource).toContain('plan: selectedPlan');
393
+ expect(stepSource).toContain('customerEmail: checkoutEmail || user.email');
394
+ expect(stepSource).toContain('couponId: checkoutPromoActive ? selectedDisplayPlan?.couponId ?? null : null');
395
+ expect(stepSource).toContain('summaryLabel: checkoutSummaryLabel');
396
+ expect(stepSource).toContain('checkoutSession.activeClientSecret');
397
+ expect(stepSource).toContain('<ApplePaySubscriptionCheckoutSlot');
398
+ expect(stepSource).toContain('<GooglePaySubscriptionCheckoutSlot');
399
+ expect(stepSource).not.toContain('StripeExpressCheckoutElement');
400
+ expect(stepSource).not.toContain('preparePaywallWalletSubscription');
401
+ expect(stepSource).not.toContain('const handlePaywallApplePayPlaceholderClick = () => {');
402
+ expect(stepSource).not.toContain('onClick={handlePaywallApplePayPlaceholderClick}');
403
+ });
404
+
405
+ it('leaves checkout-start ownership to the shared session-aware payment helpers', () => {
406
+ const stepSource = readFileSync(
407
+ path.resolve(__dirname, './step-32-paywall.tsx'),
408
+ 'utf8',
409
+ );
410
+
411
+ expect(stepSource).toContain('const trackPaymentInfoSubmitted = () => {');
412
+ expect(stepSource).not.toContain('publicAnalyticsSdk.trackCheckoutStarted({');
413
+ expect(stepSource).toContain('publicAnalyticsSdk.trackPaymentInfoSubmitted({');
414
+ expect(stepSource).toContain('stepType: stepPaywall.type');
415
+ expect(stepSource).toContain('value: selectedDisplayPlan.analyticsPurchaseValue');
416
+ expect(stepSource).toContain("currency: 'USD'");
417
+ expect(stepSource).toContain('analyticsMetadata: checkoutAnalyticsMetadata');
418
+ expect(stepSource).toContain('const checkoutAnalyticsContext = useMemo(');
419
+ expect(stepSource).toContain('checkoutAnalytics={checkoutAnalyticsContext}');
420
+ expect(stepSource).toContain('checkoutSessionId={checkoutSession.checkoutSessionId}');
421
+ expect(stepSource).toContain('onPaymentInfoSubmitted={trackPaymentInfoSubmitted}');
422
+ expect(stepSource).toContain('await checkoutSession.prepareCardCheckout();');
423
+ expect(stepSource).not.toContain('const trackCheckoutCompleted = () => {');
424
+ expect(stepSource).not.toContain('publicAnalyticsSdk.trackCheckoutCompleted({');
425
+ expect(stepSource).not.toContain('onSuccess={trackCheckoutCompleted}');
426
+ expect(stepSource).not.toContain("eventType: 'checkout_started'");
427
+ expect(stepSource).not.toContain("eventType: 'checkout_completed'");
428
+ });
429
+
430
+ it('emits payment-info analytics only after a stable Checkout Session exists', () => {
431
+ const stepSource = readFileSync(
432
+ path.resolve(__dirname, './step-32-paywall.tsx'),
433
+ 'utf8',
434
+ );
435
+ const analyticsMetadata = stepSource.slice(
436
+ stepSource.indexOf('const checkoutAnalyticsMetadata = useMemo(() => {'),
437
+ stepSource.indexOf('const checkoutAnalyticsContext = useMemo(() => {'),
438
+ );
439
+ const checkoutAnalyticsContext = stepSource.slice(
440
+ stepSource.indexOf('const checkoutAnalyticsContext = useMemo(() => {'),
441
+ stepSource.indexOf('const successReturnUrl = useMemo(() => {'),
442
+ );
443
+ const paymentInfoHandler = stepSource.slice(
444
+ stepSource.indexOf('const trackPaymentInfoSubmitted = () => {'),
445
+ stepSource.indexOf('const preparePaywallWalletCheckout'),
446
+ );
447
+
448
+ expect(analyticsMetadata).toContain("currency: 'USD'");
449
+ expect(checkoutAnalyticsContext).toContain('metadata: checkoutAnalyticsMetadata');
450
+ expect(paymentInfoHandler).toContain(
451
+ 'const checkoutSessionId = checkoutSession.checkoutSessionId?.trim();',
452
+ );
453
+ expect(paymentInfoHandler).toContain('if (!checkoutSessionId) {');
454
+ expect(paymentInfoHandler.indexOf('if (!checkoutSessionId) {')).toBeLessThan(
455
+ paymentInfoHandler.indexOf('completeStep(analyticsStepId, {'),
456
+ );
457
+ expect(paymentInfoHandler).toContain(
458
+ 'eventId: `add_payment_info:${checkoutSessionId}`',
459
+ );
460
+ expect(paymentInfoHandler).toContain('metadata: checkoutAnalyticsMetadata');
461
+ expect(paymentInfoHandler).toContain('void publicAnalyticsSdk.flush().catch(() => 0);');
462
+ });
463
+
464
+ it('uses a local three-plan catalog so the paywall shows weekly, monthly, and yearly offers', () => {
465
+ const billingConfigPath = path.resolve(__dirname, '../config/billing.plans.ts');
466
+ const paywallPlansHookPath = path.resolve(__dirname, './use-claimbee-paywall-plans.ts');
467
+ const stepPaywallPlansPath = path.resolve(__dirname, './step-32-paywall.plans.ts');
468
+ const stepPaywallOfferPath = path.resolve(__dirname, './step-32-paywall.offer.ts');
469
+ const paywallConfigPath = path.resolve(__dirname, '../config/claimbee-paywall.config.ts');
470
+ const billingTestConfigPath = path.resolve(__dirname, '../config/billing.test.plans.ts');
471
+ const stepSource = readFileSync(
472
+ path.resolve(__dirname, './step-32-paywall.tsx'),
473
+ 'utf8',
474
+ );
475
+
476
+ expect(existsSync(billingConfigPath)).toBe(true);
477
+ expect(existsSync(paywallPlansHookPath)).toBe(false);
478
+ expect(existsSync(stepPaywallPlansPath)).toBe(false);
479
+ expect(existsSync(stepPaywallOfferPath)).toBe(false);
480
+ expect(existsSync(paywallConfigPath)).toBe(false);
481
+ expect(existsSync(billingTestConfigPath)).toBe(true);
482
+
483
+ const billingSource = existsSync(billingConfigPath)
484
+ ? readFileSync(billingConfigPath, 'utf8')
485
+ : '';
486
+
487
+ expect(stepSource).toContain("from '@/config/billing.plans'");
488
+ expect(stepSource).not.toContain("from '@/config/claimbee-paywall.config'");
489
+ expect(stepSource).toContain('resolveClaimbeeCheckoutMode(runtimeMode)');
490
+ expect(stepSource).toContain('const availablePlans = useMemo(');
491
+ expect(stepSource).toContain('buildConfigPaywallPlans');
492
+ expect(stepSource).toContain("['primary', 'secondary', 'tertiary']");
493
+ expect(stepSource).toContain('[checkoutMode]');
494
+ expect(stepSource).toContain("const defaultSelectedPlan = 'secondary'");
495
+ expect(stepSource).not.toContain('CLAIMBEE_PAYWALL_PLAN_ORDER');
496
+ expect(stepSource).not.toContain('getOrderedPaywallPlans');
497
+ expect(stepSource).toContain("from '@funnelsgrove/payments'");
498
+ expect(stepSource).not.toContain('useClaimbeePaywallPlans');
499
+ expect(stepSource).not.toContain("from './step-32-paywall.offer'");
500
+ expect(stepSource).not.toContain('/sdk/public/payments/plans');
501
+ expect(stepSource).not.toContain('claimbeeBillingPlans');
502
+ expect(billingSource).toContain('createRuntimeModeBillingPlanCatalog');
503
+ expect(billingSource).toContain('claimbeePlansByMode');
504
+ expect(billingSource).not.toContain('runtimeEnvConfig.stripeActiveMode');
505
+ expect(billingSource).toContain("title: '1-Week Plan'");
506
+ expect(billingSource).toContain("title: '1-Month Plan'");
507
+ expect(billingSource).toContain("title: '1-Year Plan'");
508
+ expect(billingSource).toContain("featuredTag: '🔥 Most Popular Choice'");
509
+ expect(billingSource).toContain("stage: 'first'");
510
+ expect(billingSource).toContain("couponId: 'fg_30_off_one'");
511
+ expect(billingSource).toContain('discountPercent: 30');
512
+ expect(billingSource).toContain("stage: 'second'");
513
+ expect(billingSource).toContain("couponId: 'fg_46_off_once'");
514
+ expect(billingSource).toContain('discountPercent: 46');
515
+ expect(billingSource).not.toContain('claimbeePaywallDiscounts');
516
+ expect(billingSource).not.toContain('oldPriceLabel');
517
+ expect(billingSource).not.toContain('perDayLabel');
518
+ expect(billingSource).not.toContain('billingInterval');
519
+ expect(billingSource).not.toContain('isDefault');
520
+ });
521
+
522
+ it('selects the monthly paywall plan by default', () => {
523
+ const stepSource = readFileSync(
524
+ path.resolve(__dirname, './step-32-paywall.tsx'),
525
+ 'utf8',
526
+ );
527
+
528
+ expect(stepSource).toContain("const defaultSelectedPlan = 'secondary'");
529
+ expect(stepSource).toContain('availablePlans.find((plan) => plan.id === defaultSelectedPlan)?.id');
530
+ expect(stepSource).not.toContain('availablePlans.find((plan) => Boolean(plan.featuredTag))?.id');
531
+ });
532
+
533
+ it('renders the FAQ items as a collapsed accordion by default', () => {
534
+ const stepSource = readFileSync(
535
+ path.resolve(__dirname, './step-32-paywall.tsx'),
536
+ 'utf8',
537
+ );
538
+ const contentSource = readFileSync(
539
+ path.resolve(__dirname, './content/step-32-paywall.content.ts'),
540
+ 'utf8',
541
+ );
542
+
543
+ expect(stepSource).toContain('const [openFaqIds, setOpenFaqIds] = useState<string[]>([])');
544
+ expect(stepSource).toContain('paywall-v2-faq');
545
+ expect(stepSource).toContain('paywall-v2-faq-toggle');
546
+ expect(stepSource).toContain('aria-expanded={isOpen}');
547
+ expect(contentSource).toContain(
548
+ 'Do you only cover the big, famous lawsuits, or do you track smaller ones too?',
549
+ );
550
+ expect(stepSource).not.toContain("index === 0 ? 'paywall-v2-plan--featured' : ''");
551
+ expect(stepSource).toContain("plan.featuredTag ? 'paywall-v2-plan--featured' : ''");
552
+ });
553
+
554
+ it('renders the gold promo-applied strip below the hero image', () => {
555
+ const stepSource = readFileSync(
556
+ path.resolve(__dirname, './step-32-paywall.tsx'),
557
+ 'utf8',
558
+ );
559
+ const contentSource = readFileSync(
560
+ path.resolve(__dirname, './content/step-32-paywall.content.ts'),
561
+ 'utf8',
562
+ );
563
+
564
+ const heroIndex = stepSource.indexOf("className='paywall-v2-hero'");
565
+ const promoIndex = stepSource.indexOf("'paywall-v2-promo-banner'");
566
+ const highlightIndex = stepSource.indexOf("className='paywall-v2-highlight'");
567
+
568
+ expect(heroIndex).toBeGreaterThan(-1);
569
+ expect(promoIndex).toBeGreaterThan(heroIndex);
570
+ expect(highlightIndex).toBeGreaterThan(promoIndex);
571
+ expect(contentSource).toContain("initialLabel: '% Your promo code applied!'");
572
+ expect(contentSource).toContain("upgradedLabel: 'New promo code applied!'");
573
+ expect(stepSource).toContain('getPaywallPromoDisplayName');
574
+ expect(stepSource).toContain('promoDisplayName');
575
+ expect(stepSource).toContain('discountState.previousDiscountPercent');
576
+ });
577
+
578
+ it('matches the Figma promo strip treatment with a notched split card', () => {
579
+ const stepSource = readFileSync(
580
+ path.resolve(__dirname, './step-32-paywall.tsx'),
581
+ 'utf8',
582
+ );
583
+
584
+ const promoStylesStart = stepSource.indexOf('.paywall-v2-promo-banner {');
585
+ const promoStylesEnd = stepSource.indexOf('.paywall-v2-highlight {');
586
+ const promoStyles = stepSource.slice(promoStylesStart, promoStylesEnd);
587
+
588
+ expect(promoStylesStart).toBeGreaterThan(-1);
589
+ expect(promoStyles).toContain('max-width: 382px;');
590
+ expect(promoStyles).toContain('min-height: 86px;');
591
+ expect(promoStyles).toContain('position: relative;');
592
+ expect(promoStyles).toContain('display: flex;');
593
+ expect(promoStyles).toContain('flex-direction: column;');
594
+ expect(promoStyles).toContain('overflow: hidden;');
595
+ expect(promoStyles).toContain('margin: -20px auto 0;');
596
+ expect(promoStyles).toContain('.paywall-v2-promo-banner::before,');
597
+ expect(promoStyles).toContain('.paywall-v2-promo-banner::after {');
598
+ expect(promoStyles).toContain('background: #ecf7ff;');
599
+ expect(promoStyles).toContain('repeating-linear-gradient(');
600
+ expect(promoStyles).toContain('.paywall-v2-promo-label {');
601
+ expect(promoStyles).toContain(
602
+ "font-family: var(--font-family-base, Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif);",
603
+ );
604
+ expect(promoStyles).toContain('font-size: 16px;');
605
+ expect(promoStyles).toContain('font-weight: 600;');
606
+ expect(promoStyles).toContain('line-height: 1.35;');
607
+ expect(promoStyles).toContain('text-align: center;');
608
+ expect(promoStyles).toContain('justify-content: center;');
609
+ expect(promoStyles).toContain('.paywall-v2-promo-code {');
610
+ expect(promoStyles).toContain('font-size: 14px;');
611
+ expect(promoStyles).toContain('font-weight: 700;');
612
+ expect(promoStyles).toContain('--paywall-v2-promo-code-height: 41px;');
613
+ expect(promoStyles).toContain('width: 100%;');
614
+ expect(promoStyles).toContain('.paywall-v2-promo-banner.is-upgraded {');
615
+ expect(promoStyles).toContain('.paywall-v2-promo-upgrade-row {');
616
+ expect(promoStyles).toContain('.paywall-v2-promo-upgrade-badge {');
617
+ expect(promoStyles).toContain('.paywall-v2-promo-upgrade-label {');
618
+ expect(promoStyles).not.toContain('.paywall-v2-promo-glyph {');
619
+ expect(readFileSync(path.resolve(__dirname, './content/step-32-paywall.content.ts'), 'utf8')).toContain(
620
+ "initialLabel: '% Your promo code applied!'",
621
+ );
622
+ expect(stepSource).not.toContain("className='paywall-v2-promo-glyph'");
623
+ });
624
+
625
+ it('renders the discounted per-day row and the auto-renew disclaimer below the buttons', () => {
626
+ const stepSource = readFileSync(
627
+ path.resolve(__dirname, './step-32-paywall.tsx'),
628
+ 'utf8',
629
+ );
630
+
631
+ expect(stepSource).toContain("'paywall-v2-plan-indicator'");
632
+ expect(stepSource).toContain("className='paywall-v2-plan-divider'");
633
+ expect(stepSource).toContain("className='paywall-v2-plan-per-day-price'");
634
+ expect(stepSource).toContain("className='paywall-v2-plan-per-day-old'");
635
+ expect(stepSource).toContain("className='paywall-v2-disclaimer'");
636
+ expect(stepSource).toContain('buildPaywallRenewalDisclaimer');
637
+ expect(stepSource).toContain("className='paywall-v2-disclaimer'");
638
+ });
639
+
640
+ it('renders paywall support from the runtime project support email', () => {
641
+ const stepSource = readFileSync(
642
+ path.resolve(__dirname, './step-32-paywall.tsx'),
643
+ 'utf8',
644
+ );
645
+ const contentSource = readFileSync(
646
+ path.resolve(__dirname, './content/step-32-paywall.content.ts'),
647
+ 'utf8',
648
+ );
649
+ const editorSource = readFileSync(
650
+ path.resolve(__dirname, './editor/step-32-paywall.editor.ts'),
651
+ 'utf8',
652
+ );
653
+
654
+ expect(stepSource).toContain('runtimePublicConfig.supportEmail');
655
+ expect(stepSource).toContain('const supportEmail = runtimePublicConfig.supportEmail;');
656
+ expect(stepSource).toContain('content.checkout.supportText');
657
+ expect(stepSource).toContain("className='paywall-v2-support'");
658
+ expect(stepSource).toContain('href={`mailto:${supportEmail}`}');
659
+ expect(stepSource).not.toContain('mailto:hello@claimbee.app');
660
+ expect(contentSource).not.toContain('supportEmail:');
661
+ expect(editorSource).not.toContain("path: 'checkout.supportEmail'");
662
+ });
663
+
664
+ it('matches the Figma plan-card spacing so the per-day label stays aligned', () => {
665
+ const stepSource = readFileSync(
666
+ path.resolve(__dirname, './step-32-paywall.tsx'),
667
+ 'utf8',
668
+ );
669
+
670
+ const planStylesStart = stepSource.indexOf('.paywall-v2-plan {');
671
+ const planStylesEnd = stepSource.indexOf('.paywall-v2-payment-stack {');
672
+ const planStyles = stepSource.slice(planStylesStart, planStylesEnd);
673
+
674
+ expect(planStylesStart).toBeGreaterThan(-1);
675
+ expect(planStyles).toContain('padding: 16px 14px;');
676
+ expect(planStyles).toContain('justify-content: flex-start;');
677
+ expect(planStyles).toContain('align-items: center;');
678
+ expect(planStyles).toContain('width: 22px;');
679
+ expect(planStyles).toContain('height: 22px;');
680
+ expect(planStyles).toContain('width: 7px;');
681
+ expect(planStyles).toContain('height: 14px;');
682
+ expect(planStyles).toContain('.paywall-v2-plan-indicator.is-selected {');
683
+ expect(planStyles).toContain('background: var(--color-primary);');
684
+ expect(planStyles).toContain('border-left: 1px solid rgb(31 16 0 / 8%);');
685
+ expect(planStyles).toContain('text-align: center;');
686
+ expect(planStyles).toContain('width: min(34%, 118px);');
687
+ expect(planStyles).toContain('width: 100%;');
688
+ expect(planStyles).toContain('gap: 6px;');
689
+ expect(planStyles).toContain('gap: 2px;');
690
+ expect(planStyles).toContain('min-height: 32px;');
691
+ expect(planStyles).toContain('min-height: 92px;');
692
+ expect(planStyles).toContain('font-size: 22px;');
693
+ expect(planStyles).toContain('font-size: 16px;');
694
+ expect(planStyles).toContain('font-size: 15px;');
695
+ expect(planStyles).toContain('font-size: 30px;');
696
+ expect(planStyles).toContain('color: rgba(31, 16, 0, 0.72);');
697
+ expect(planStyles).toContain('color: rgba(31, 16, 0, 0.44);');
698
+ expect(planStyles).toContain('font-weight: 500;');
699
+ expect(planStyles).toContain('.paywall-v2-plan.is-selected .paywall-v2-plan-title,');
700
+ expect(planStyles).toContain('.paywall-v2-plan.is-selected .paywall-v2-plan-per-day-amount {');
701
+ expect(planStyles).toContain('color: var(--color-secondary-text);');
702
+ expect(planStyles).not.toContain('align-items: flex-start;');
703
+ expect(planStyles).not.toContain('.paywall-v2-plan--featured .paywall-v2-plan-row {');
704
+ });
705
+
706
+ it('keeps the featured plan taller so the badge sits above a full-height body', () => {
707
+ const stepSource = readFileSync(
708
+ path.resolve(__dirname, './step-32-paywall.tsx'),
709
+ 'utf8',
710
+ );
711
+
712
+ const planStylesStart = stepSource.indexOf('.paywall-v2-plan {');
713
+ const planStylesEnd = stepSource.indexOf('.paywall-v2-payment-stack {');
714
+ const planStyles = stepSource.slice(planStylesStart, planStylesEnd);
715
+
716
+ expect(planStyles).toContain('.paywall-v2-plan-tag {');
717
+ expect(planStyles).toContain('min-height: 32px;');
718
+ expect(planStyles).toContain('.paywall-v2-plan-row {');
719
+ expect(planStyles).toContain('min-height: 92px;');
720
+ expect(planStyles).not.toContain('min-height: 70px;');
721
+ });
722
+
723
+ it('keeps the feature grid below checkout and ends the paywall with the guarantee', () => {
724
+ const stepSource = readFileSync(
725
+ path.resolve(__dirname, './step-32-paywall.tsx'),
726
+ 'utf8',
727
+ );
728
+
729
+ const paymentStackIndex = stepSource.indexOf("className='paywall-v2-payment-stack'");
730
+ const featureGridIndex = stepSource.indexOf("className='paywall-v2-feature-grid'");
731
+ const feedbackIndex = stepSource.indexOf("className='paywall-v2-feedback'");
732
+ const faqIndex = stepSource.indexOf("className='paywall-v2-faq'");
733
+ const guaranteeIndex = stepSource.indexOf("className='paywall-v2-guarantee-card'");
734
+ const styleIndex = stepSource.lastIndexOf('<style>{stepPaywallStyles}</style>');
735
+
736
+ expect(paymentStackIndex).toBeGreaterThan(-1);
737
+ expect(featureGridIndex).toBeGreaterThan(paymentStackIndex);
738
+ expect(feedbackIndex).toBeGreaterThan(featureGridIndex);
739
+ expect(faqIndex).toBeGreaterThan(feedbackIndex);
740
+ expect(guaranteeIndex).toBeGreaterThan(faqIndex);
741
+ expect(styleIndex).toBeGreaterThan(guaranteeIndex);
742
+ expect(stepSource.slice(guaranteeIndex, styleIndex)).not.toContain("className='paywall-v2-feature-grid'");
743
+ expect(stepSource.slice(guaranteeIndex, styleIndex)).toContain('width={184}');
744
+ expect(stepSource).toContain('width: 184px;');
745
+ expect(stepSource).toContain('height: 184px;');
746
+ expect(stepSource).toContain('object-fit: cover;');
747
+ expect(stepSource).toContain('object-position: center 54%;');
748
+ expect(stepSource).toContain('font-size: 22px;');
749
+ expect(stepSource).toContain('white-space: nowrap;');
750
+ });
751
+
752
+ it('keeps the rotating reviews card at a fixed height so the paywall does not jump between stories', () => {
753
+ const stepSource = readFileSync(
754
+ path.resolve(__dirname, './step-32-paywall.tsx'),
755
+ 'utf8',
756
+ );
757
+
758
+ const feedbackCardStylesStart = stepSource.indexOf('.paywall-v2-feedback-card {');
759
+ const feedbackCardStylesEnd = stepSource.indexOf('.paywall-v2-feedback-quote {');
760
+ const feedbackCardStyles = stepSource.slice(
761
+ feedbackCardStylesStart,
762
+ feedbackCardStylesEnd,
763
+ );
764
+
765
+ expect(feedbackCardStylesStart).toBeGreaterThan(-1);
766
+ expect(feedbackCardStyles).toContain('box-sizing: border-box;');
767
+ expect(feedbackCardStyles).toContain('height: 372px;');
768
+ expect(feedbackCardStyles).not.toContain('min-height: 228px;');
769
+ });
770
+ });