@funnelsgrove/cli 0.1.9 → 0.1.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,66 +1,125 @@
1
- # Editing or Creating a Step
1
+ # Adding or Editing a Step
2
2
 
3
- A step is one screen or decision point. Keep its view, local state, validation, and step-specific CSS in the step file. Keep editable copy/images in a matching content file and builder fields in a matching editor file.
3
+ A step is one screen, one decision. Target: a new step compiles and renders in local preview in under 3 minutes. Exactly six files are involved no others.
4
4
 
5
- ## Source Files
5
+ | # | File | What goes there |
6
+ | --- | --- | --- |
7
+ | 1 | `src/steps/step-NN-<name>.tsx` | View, step meta, local state, one-off CSS |
8
+ | 2 | `src/steps/content/step-NN-<name>.content.ts` | All copy and image refs (localized) |
9
+ | 3 | `src/steps/editor/step-NN-<name>.editor.ts` | Which content fields the builder may edit |
10
+ | 4 | `src/runtime/step-registry.ts` | `componentKey` → component + meta |
11
+ | 5 | `src/steps/step-content.registry.ts` | step id → content/editor file paths |
12
+ | 6 | `src/config/funnel.manifest.ts` | Step entry + `edgesByStepId` routing |
6
13
 
7
- For an existing step, start from `src/config/funnel.manifest.ts` and find:
14
+ Before writing UI, read [step-ui-guidelines.md](step-ui-guidelines.md) — it defines the layout shell, the sticky action bar, and the viewport rules every step must pass.
8
15
 
9
- - `steps[].id`: stable route/runtime id, such as `paywall`.
10
- - `steps[].path`: URL path, such as `/paywall`.
11
- - `steps[].filePath`: React view file, such as `src/steps/step-32-paywall.tsx`.
12
- - `steps[].componentKey`: key registered in `src/runtime/step-registry.ts`.
16
+ ## Naming and URLs
13
17
 
14
- Most steps also have:
18
+ Files may use ordered names like `step-07-motivation.tsx`, and step ids may stay
19
+ sequential if the existing funnel uses sequential ids. Public route paths must
20
+ be meaningful product slugs. Use `/motivation`, `/fitness-goal`, or
21
+ `/email-capture`; do not create routes like `/step-1`, `/step-07`, or
22
+ `/question-3`.
15
23
 
16
- - `src/steps/content/<step>.content.ts`: localized content using `LocalizedStepContent`.
17
- - `src/steps/editor/<step>.editor.ts`: builder editor fields using `StepEditorSection`.
18
- - `src/steps/step-content.registry.ts`: maps the runtime step id to those content/editor files.
24
+ ## Recipe: New Step
19
25
 
20
- ## View Pattern
26
+ Copy the closest existing step as the starting point (`step-02` for selections, `step-03` for interstitials). Then:
21
27
 
22
- Step files usually export:
28
+ **1. View** `src/steps/step-07-motivation.tsx`:
23
29
 
24
- - A stable `stepXId`.
25
- - A `FunnelStepMeta` object with `id`, `type`, `title`, optional `kind`, and optional `actionBar`.
26
- - A React component that reads content with `usePreviewStepLocalizedContent(...)` or the funnel's wrapper, such as `useClaimbeeStepContent(...)`.
30
+ ```tsx
31
+ export const stepMotivationId = 'motivation';
32
+
33
+ export const stepMotivation: FunnelStepMeta = {
34
+ id: stepMotivationId,
35
+ name: 'motivation',
36
+ type: 'single_step_choice',
37
+ title: 'Motivation',
38
+ description: 'Asks the user what motivates them.',
39
+ actionBar: { buttonText: 'Continue' }, // omit for auto-advance; { hidden: true } if the step owns its CTA
40
+ };
41
+
42
+ export function StepMotivation() {
43
+ const { attributes, setAnswer, goNext } = useFunnel();
44
+ const content = usePreviewStepLocalizedContent(
45
+ stepMotivationId,
46
+ stepMotivationContent,
47
+ getStepContentLocale(attributes),
48
+ );
49
+ // render options; on select: setAnswer('motivation', id) then goNext() for auto-advance
50
+ return (
51
+ <>
52
+ <section className='motivation-step'>...</section>
53
+ <style>{stepStyles}</style>
54
+ </>
55
+ );
56
+ }
57
+
58
+ const stepStyles = `...one-off CSS only...`;
59
+ ```
27
60
 
28
- Use `useFunnel()` for runtime state:
61
+ **2. Content** — `src/steps/content/step-07-motivation.content.ts`: export a `LocalizedStepContent` object holding every user-visible string and image. The view must not contain literal copy.
29
62
 
30
- - `answers` / `attributes`: current funnel answers.
31
- - `setAnswer(key, value)` or `setAttribute(key, value)`: store data another step needs.
32
- - `goNext()`, `goToStep(stepId)`, or `goChoice('yes' | 'no')`: navigate through the manifest.
33
- - `user`, `setUser`, `completeStep`: only when the step explicitly needs user/session behavior.
63
+ **3. Editor** — `src/steps/editor/step-07-motivation.editor.ts`: export `StepEditorSection[]` exposing only the fields a non-developer should edit (see an existing editor file for the field kinds).
34
64
 
35
- Do not bypass the flow controller with raw `window.location` for normal funnel navigation.
65
+ **4. Register the component** `src/runtime/step-registry.ts`:
36
66
 
37
- ## CSS Pattern
67
+ ```ts
68
+ stepMotivation: { component: StepMotivation, meta: stepMotivation },
69
+ ```
38
70
 
39
- Keep one-off step styling at the bottom of the step file:
71
+ **5. Register content/editor** `src/steps/step-content.registry.ts`:
40
72
 
41
- ```tsx
42
- return (
43
- <>
44
- <section className='claimbee-step'>...</section>
45
- <style>{stepStyles}</style>
46
- </>
47
- );
48
-
49
- const stepStyles = `
50
- .claimbee-step { ... }
51
- `;
73
+ ```ts
74
+ motivation: {
75
+ contentPath: 'src/steps/content/step-07-motivation.content.ts',
76
+ editorPath: 'src/steps/editor/step-07-motivation.editor.ts',
77
+ },
52
78
  ```
53
79
 
54
- Use shared CSS under `src/steps/styles/shared/` only for patterns reused by multiple steps. Do not move one-off Figma parity or paywall-specific styling into shared files.
80
+ **6. Manifest + routing** `src/config/funnel.manifest.ts`:
81
+
82
+ ```ts
83
+ // steps[]
84
+ {
85
+ id: 'motivation',
86
+ path: '/motivation', // meaningful public URL, not '/step-07'
87
+ filePath: 'src/steps/step-07-motivation.tsx',
88
+ componentKey: 'stepMotivation',
89
+ type: 'single_step_choice',
90
+ title: 'Motivation',
91
+ },
92
+ // edgesByStepId — wire it in AND give it an exit
93
+ 'step-2': [{ toStepId: 'motivation' }],
94
+ motivation: [{ toStepId: 'step-3' }],
95
+ ```
96
+
97
+ **Verify:** `npm run dev`, open `/motivation`, confirm it renders at 430x932 and 390x844, Continue (or auto-advance) lands on the next step, and `npm run test:run && npm run lint` pass.
98
+
99
+ ## Editing an Existing Step
100
+
101
+ Find the step in `src/config/funnel.manifest.ts` by `id` — `filePath` points at the view, `componentKey` at the registry entry. Copy changes go in the `content/` file, never inline in the view. Behavior changes go in the view file. Re-run the verify line above for the edited step.
102
+
103
+ ## Navigation Inside a Step
104
+
105
+ Use `useFunnel()` only:
106
+
107
+ - `setAnswer(key, value)` / `setAttribute(key, value)` — store data another step needs.
108
+ - `goNext()` — normal manifest progression.
109
+ - `goChoice('yes' | 'no')` — when the manifest has conditional edges for this step.
110
+ - `goToStep(stepId)` — explicit jumps (paywall success, manage-subscription return).
111
+
112
+ Never navigate with raw `window.location`, and never encode experiment routing in the step ([ab-experiments.md](ab-experiments.md) handles that in config).
113
+
114
+ ## CSS
55
115
 
56
- ## Creating a Step
116
+ One-off styling lives in a `const stepStyles` string at the bottom of the step file. Shared styling under `src/steps/styles/shared/` only when several steps already use the same pattern. Do not move paywall-specific styling into shared files.
57
117
 
58
- 1. Add the view file in `src/steps/`.
59
- 2. Add `content/<step>.content.ts` with the exact content shape the view needs.
60
- 3. Add `editor/<step>.editor.ts` exposing only fields that should be editable.
61
- 4. Register the component in `src/runtime/step-registry.ts`.
62
- 5. Add content/editor paths in `src/steps/step-content.registry.ts`.
63
- 6. Add the step and route edge in `src/config/funnel.manifest.ts`.
64
- 7. Add focused tests when changing routing, state, checkout, parsing, or non-trivial UI behavior.
118
+ ## Rules
65
119
 
66
- Preserve existing ids unless the work is an explicit migration. A renamed step id changes routes, persisted answers, analytics, builder preview patches, and publish history.
120
+ - Keep `id`, `path`, and answer keys stable renames break routes, persisted answers, analytics, and publish history.
121
+ - For new steps, use a meaningful `path`. Sequential ids and filenames are fine
122
+ when they match the project, but public URLs should not be `/step-1` style
123
+ routes.
124
+ - Every step needs an outgoing edge (except terminal steps like subscription handoff).
125
+ - Add a focused test when changing routing, state, checkout, parsing, or non-trivial UI logic.
@@ -33,6 +33,17 @@ Discounts use `BillingDiscountList` or `BillingDiscountCatalog` and are resolved
33
33
 
34
34
  Use `resolvePaywallDiscountState(...)`, `advancePaywallDiscountState(...)`, `activateSecondPaywallDiscount(...)`, `serializePaywallDiscountState(...)`, and `buildDiscountedPaywallPlans(...)`. Store discount state through runtime paywall state helpers (`readPaywallStateValue`, `updatePaywallStateValue`) so it is scoped by funnel id.
35
35
 
36
+ ### Discount on Closing Checkout
37
+
38
+ The standard two-stage flow, wired in the paywall step:
39
+
40
+ 1. Paywall opens with the first-stage discount applied to plan cards.
41
+ 2. The checkout controller's `onFirstCheckoutClosed` callback fires when the visitor closes the card checkout without paying. The paywall opens `SharedCheckoutSpecialOfferDialog` (image, discount label, accept button — copy lives in the paywall content file).
42
+ 3. Accepting calls `activateSecondPaywallDiscount(...)`, which swaps to the stronger second-stage coupon, restarts its window, and re-renders plans via `buildDiscountedPaywallPlans(...)`.
43
+ 4. The next checkout open carries the second-stage coupon id in its payload.
44
+
45
+ Keep both stages testable: QA must verify stage one on open and stage two after a checkout close ([qa-checklist.md](qa-checklist.md), paywall item 5).
46
+
36
47
  ## Checkout
37
48
 
38
49
  Prefer shared Stripe surfaces:
@@ -26,6 +26,18 @@ fgrove publish --env production --domain <domain> --message '<summary>'
26
26
 
27
27
  Production publish requires a domain. Preview publish returns a deployment URL, version sequence, and version id.
28
28
 
29
+ ## Publish Image Optimization
30
+
31
+ Publish builds reduce raster image size before upload. Keep this path enabled:
32
+ PNG/JPEG sources are compressed, AVIF/WebP variants are generated for supported
33
+ browsers, and the original image remains the fallback. Do not bypass this with
34
+ remote funnel-critical image URLs.
35
+
36
+ After publishing image edits or a production candidate, check the deployment
37
+ metadata, CLI output, or `publishBuild.stageTimings.imageVariants`. If the
38
+ image-variant stage is missing or unavailable, report that explicitly before
39
+ calling the publish ready.
40
+
29
41
  ## Local Sync Contract
30
42
 
31
43
  The CLI writes `.funnelsgrove-sync.json` into the local tree. Keep it there. It records workspace id, funnel id, and current draft version id so later `sync up` can patch only changed/deleted files.
@@ -0,0 +1,73 @@
1
+ # QA Checklist
2
+
3
+ What to test, where to test it, and what blocks a publish. QA can take as long as it needs — it is the gate before anything user-facing ships. Scale the scope to the edit: a copy tweak needs the affected step only; anything touching flow, paywall, checkout, pricing, identity, or experiments needs the full list.
4
+
5
+ ## Where
6
+
7
+ | Stage | URL | When |
8
+ | --- | --- | --- |
9
+ | Local | `npm run dev` → `http://localhost:3000` | Always, before sync |
10
+ | Preview | URL returned by `fgrove publish --env preview` | Before any production publish |
11
+ | Production | Live domain | After production publish |
12
+
13
+ A production publish without a QA-passed matching preview build is a blocker unless the user explicitly accepts the risk.
14
+
15
+ ## 1. Flow Coverage
16
+
17
+ - Open the first step and walk every step to the end.
18
+ - Exercise every branch (`goChoice` yes/no paths) — check both sides.
19
+ - For each experiment in `src/config/experiments.ts`, force both variants (`?editor=true&experimentVariant=control|variant_b`) and confirm each renders and advances.
20
+ - Submit email/identity capture with a test address and confirm advancement.
21
+ - Keep the browser console open the whole walk: zero uncaught errors and no failed requests (ignore third-party analytics noise).
22
+
23
+ ## 2. Visual Pass
24
+
25
+ At **430x932**, then **390x844** (rules in [step-ui-guidelines.md](step-ui-guidelines.md)):
26
+
27
+ - Nothing intersects or overlaps: text never collides with images, cards, badges, or the action bar; modals and dialogs fit the viewport.
28
+ - Continue button sits on an opaque bar at the bottom and stays visible on every step, including while content scrolls.
29
+ - No horizontal scroll, no clipped or truncated content, no broken images.
30
+ - Disabled CTA states render correctly and enable when valid.
31
+
32
+ ## 3. Image Performance
33
+
34
+ For image edits and every preview-to-production candidate:
35
+
36
+ - Confirm edited step images are listed in `funnelManifest.assets` and attached
37
+ to the relevant manifest steps with `assetIds`.
38
+ - Confirm first-viewport images use the framework's priority/preload mechanism.
39
+ - Confirm the shell warms only likely next-step images at low priority, not the
40
+ full funnel image set.
41
+ - After preview or production publish, confirm build metadata includes the
42
+ `imageVariants` stage or name why it is unavailable.
43
+ - In the browser/network panel, confirm AVIF/WebP variants are served when the
44
+ browser advertises support, with the original raster image as fallback.
45
+
46
+ ## 4. Paywall and Checkout (most important)
47
+
48
+ Test the paywall on every QA run that touches it, pricing, discounts, or experiments — on the paywall step **and** every live paywall variant:
49
+
50
+ 1. **Plans render correctly** — titles, struck-through old price, discounted price, per-day price, featured/preselected plan match `src/config/billing.plans.ts` exactly. Verify discount math.
51
+ 2. **Countdown / discount state** — first-stage discount applies on open; timer counts down.
52
+ 3. **Checkout opens** — CTA opens the checkout with the selected plan; itemized totals match the selected plan and applied coupon.
53
+ 4. **Apple Pay / Google Pay buttons** — wallet slots render (Apple Pay needs Safari/iOS context, Google Pay needs Chrome; on unsupported browsers expect the documented fallback, not a broken gap). If wallets never appear in a supported context, treat as a defect.
54
+ **Note:** wallet buttons silently fail when the domain and checkout return URLs are not configured in the Stripe dashboard (payment method domains + return URL allowlist). New funnels and new domains must have Stripe configured before wallet QA can pass — if it is not configured, report it as a named blocker, not a pass.
55
+ 5. **Close-checkout discount** — close the checkout without paying: the special-offer dialog must appear; accepting it must apply the second-stage (larger) discount to the plan cards and the next checkout's payload. Verify the new prices and coupon id.
56
+ 6. **Test payment** — complete a payment with Stripe test card `4242 4242 4242 4242` (test mode) or the approved payment path. Confirm redirect to the subscription-started step with `user_id` preserved.
57
+ 7. **Success state** — subscription-started renders, registration/next-step handoff works.
58
+
59
+ If checkout, payment mode, or test credentials are unavailable, name the skipped item explicitly — a silent skip counts as a fail.
60
+
61
+ ## 5. Registration and Links
62
+
63
+ - Complete the registration page with valid test data; confirm validation errors on invalid data.
64
+ - Click every legal/support/account link on paywall, checkout, and registration: terms, privacy, subscription policy, money-back, support email.
65
+
66
+ ## 6. Subscription Management
67
+
68
+ - Open `/manage-subscription?user_id=<test-user>` (or with `stripe_customer_id`).
69
+ - Confirm the subscription list loads and the cancellation flow completes when a test subscription is available; otherwise name it as a skipped item.
70
+
71
+ ## Reporting
72
+
73
+ Every QA run ends with a short report: stage + URLs tested, steps/branches/variants covered, viewports checked, image optimization/preload result when relevant, paywall items 1–7 pass/fail, payment method used, console findings, and named blockers or explicitly skipped items with the reason. Blockers block production unless the user accepts the risk in so many words.
@@ -0,0 +1,106 @@
1
+ # Step UI Guidelines
2
+
3
+ Every step must pass these rules before it counts as done. They are distilled from high-performing quiz funnels (BetterMe-class) and apply to new steps, edited steps, and paywalls alike.
4
+
5
+ ## Viewports
6
+
7
+ Funnels are mobile-first. Build and verify at:
8
+
9
+ | Viewport | Size | Role |
10
+ | --- | --- | --- |
11
+ | iPhone 16 Pro | **430 x 932** | Default. Matches `viewport` in `funnel.manifest.ts`. |
12
+ | iPhone 12 / 13 / 14 | **390 x 844** | Required spot-check on every created or edited step. |
13
+
14
+ Pass criteria at both sizes: no horizontal scroll, no clipped or overlapping content, the primary CTA visible without scrolling on selection/input steps, and tap targets at least 44px tall.
15
+
16
+ ## Layout Shell Contract
17
+
18
+ The shell (`src/components/FunnelFlow.tsx`) owns the frame; steps fill the middle:
19
+
20
+ 1. **Fixed top bar** — opaque background, back arrow, brand or section title, optional progress. Thin bottom hairline.
21
+ 2. **Scrollable content** — the step body. Must add bottom padding so the last element clears the action bar.
22
+ 3. **Fixed bottom action bar** — the Continue button area. **The bar is opaque, never transparent** — content scrolling under a floating button is a defect. Reference implementation:
23
+
24
+ ```css
25
+ position: fixed;
26
+ bottom: 0;
27
+ left: 0;
28
+ right: 0;
29
+ background: var(--color-surface); /* solid — no alpha */
30
+ border-top: 1px solid rgba(23, 23, 23, 0.08);
31
+ padding: 16px 20px calc(16px + env(safe-area-inset-bottom));
32
+ z-index: 11;
33
+ ```
34
+
35
+ The button itself: full width, ~50px tall, pill radius, theme primary color. Disabled state stays visible (washed-out), it never disappears.
36
+
37
+ Steps control the shell bar through `FunnelStepMeta.actionBar`:
38
+
39
+ - omit → default Continue button;
40
+ - `{ buttonText: '...' }` → custom label;
41
+ - `{ hidden: true }` → step owns its CTAs (paywalls);
42
+ - auto-advance steps (single-select) need no bar at all.
43
+
44
+ ## Step Patterns
45
+
46
+ One decision per screen. Pick the matching pattern; do not invent hybrids.
47
+
48
+ **Single-select** — tap an option → store answer → `goNext()` immediately. No Continue button. Option cards: icon or image + label + radio indicator, ~16px gap, selected state with tinted background.
49
+
50
+ **Multi-select** — "Choose all that apply" subtitle, checkbox cards, sticky Continue **disabled until at least one selection**. A "None of the above" option clears and excludes the others.
51
+
52
+ **Scale question** — statement in quotes as the headline, 1–5 buttons in a row, anchored labels ("Strongly disagree" / "Strongly agree") at the ends. Auto-advance on tap.
53
+
54
+ **Numeric input** (height, weight, age) — large centered input, unit toggle (kg/lbs, cm/ft) where relevant, visible validation range hint ("enter a value from 90 cm to 243 cm"), Continue disabled until valid. When the value enables personalization, show instant inline feedback below the input (e.g. a BMI callout: amber card, icon, bold key phrase, supportive copy that says how the data will be used). Hard-stop ineligible values with a clear message, not a silent disable.
55
+
56
+ **Info interstitial** — headline, hero image card, 2–3 short supporting lines, sticky Continue. Insert one every 3–5 questions to re-sell the promise and break monotony; tie it to the answer just given ("Keep your back and knees strong → your plan will include…").
57
+
58
+ **Personalized summary** — mirror the user's data back (profile card, gauge with the user's position, attribute list, projection chart with goal badge and date). Charts that predict results need a compliance footnote ("illustrative purposes only / results vary").
59
+
60
+ **Loading / plan-building** — animated percent ring or bar plus rotating social proof (ratings, testimonials) during the wait. Auto-advance on completion.
61
+
62
+ **Email capture** — single email field, nothing else on screen. Headline names the deliverable ("Enter your email to get your plan"). Padlock icon + one privacy reassurance line + Privacy Policy link. Marketing opt-in is its own subsequent step, never a pre-checked box.
63
+
64
+ ## Paywall Anatomy
65
+
66
+ The proven block order, top to bottom:
67
+
68
+ 1. **Sticky offer header** — compact bar with countdown timer ("Reserved price for 09:59") and a small CTA, always visible while scrolling.
69
+ 2. **Before/after comparison** — "Now" vs "Your Goal" with attribute deltas.
70
+ 3. **Applied-discount card** — promo code chip, "applied automatically" copy, countdown.
71
+ 4. **Plan cards** — usually three; mark one "MOST POPULAR" and preselect it; show struck-through old total → discounted total and a large per-day price anchor.
72
+ 5. **Primary CTA** directly under the cards.
73
+ 6. **Auto-renewal disclosure** — full renewal price, interval, cancellation path. Required, directly below the CTA.
74
+ 7. **Plan highlights** — icon + bold benefit + one-line explanation.
75
+ 8. **Trust blocks** — press logos, testimonials with concrete results, results disclaimer.
76
+ 9. **FAQ** ("What happens after payment?") and **money-back guarantee** with conditions.
77
+ 10. Repeat plan cards + CTA at the bottom for long pages.
78
+
79
+ Checkout opens as a modal over the paywall: itemized breakdown (regular price, discount line, promo chip, VAT, total, "you save" line), wallet buttons first (Apple Pay / Google Pay / PayPal as available), card entry collapsed behind a radio. Closing the checkout without paying triggers the second-stage discount dialog ([payment-plans-and-discounts.md](payment-plans-and-discounts.md)) — verify both discount stages whenever you touch the paywall.
80
+
81
+ ## Image Performance
82
+
83
+ Use the ClaimBee/Blessly image loading pattern:
84
+
85
+ - Keep raster artwork on the build-time optimization path so publish can shrink
86
+ PNG/JPEG sources and create AVIF/WebP variants.
87
+ - Put step image metadata in `funnelManifest.assets` and reference those images
88
+ from each step with `assetIds`.
89
+ - Above-the-fold images on the current step use the framework's priority/preload
90
+ mechanism.
91
+ - The flow shell may warm likely next-step images at low priority shortly after
92
+ the active step loads. Do not preload the whole funnel image set on first load.
93
+ - Any image-heavy funnel should keep a contract test that every `assetId`
94
+ resolves and the shell uses manifest-driven next-step preloads.
95
+
96
+ ## Content-Fit Audit
97
+
98
+ Run on every created or edited step, at 430x932 then 390x844:
99
+
100
+ 1. Open the step in local preview (`npm run dev`).
101
+ 2. Check: nothing clipped, nothing overlapping, no text truncated mid-word, images loaded with correct aspect, CTA fully visible above the fold on selection/input steps.
102
+ 3. Scroll to both ends — last content clears the action bar; nothing hides under the top bar.
103
+ 4. Check disabled→enabled CTA transition where applicable.
104
+ 5. Fix and re-check before moving to another step.
105
+
106
+ Report the audit (both viewports, pass/fail per step) in your summary.