@funnelsgrove/cli 0.1.10 → 0.1.12

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,33 +1,53 @@
1
1
  # A/B Experiments
2
2
 
3
- Experiments live in `src/config/funnel.manifest.ts`. They are routing rules, not ad hoc conditionals inside step components.
3
+ Experiments are config, not code: declared in `src/config/experiments.ts`, exported into the manifest, resolved by the shared runtime. Never write variant conditionals inside step components. If the variant step already exists, setting up the experiment is a one-file edit that takes under a minute.
4
4
 
5
- ## Runtime Contract
5
+ ## Recipe: Step or Paywall Test
6
6
 
7
- An experiment attaches to a source step and maps variant keys to routable step ids:
7
+ Open `src/config/experiments.ts` and add one entry inside `defineFunnelExperiments([...])`:
8
8
 
9
9
  ```ts
10
- {
11
- experimentId: 'paywall-ab',
12
- stepId: 'paywall',
13
- variants: [
14
- { variantKey: 'control', routeToStepId: 'paywall' },
15
- { variantKey: 'variant_b', routeToStepId: 'paywall-b' },
16
- ],
17
- }
10
+ export const experiments = defineFunnelExperiments([
11
+ {
12
+ id: 'paywall-ab', // durable, readable, never reused
13
+ name: 'Paywall copy test',
14
+ type: 'paywall', // 'step' for quiz steps, 'paywall' for paywall tests
15
+ status: 'running', // 'paused' | 'stopped' removes it from the manifest
16
+ launchDate: '2026-06-12T00:00:00.000Z',
17
+ control: { stepId: 'paywall', label: 'paywall-ab-control', trafficPercent: 50 },
18
+ variant: { stepId: 'paywall-b', label: 'paywall-ab-variant-b', trafficPercent: 50 },
19
+ },
20
+ ] as const);
18
21
  ```
19
22
 
20
- The flow controller resolves the experiment assignment through the shared analytics/runtime integration and routes to the matched variant. `@funnelsgrove/analytics` encapsulates api for experiment tracking. If the assignment is not ready on an experiment step, rendering is suspended to avoid flashing the control before the variant resolves.
23
+ That's it `toManifestExperiments(experiments)` in the same file feeds the manifest, traffic percents are validated to sum to 100, and non-`running` entries are filtered out automatically. (Older funnels may declare a plain array and map it manually follow the local file's existing shape there.) Preconditions (each is its own task if missing):
21
24
 
22
- ## Editor and Preview
25
+ 1. Both `control.stepId` and `variant.stepId` exist in the manifest with normal `edgesByStepId` exits ([editing-step.md](editing-step.md) to create a variant step — usually a copy of the control step with one deliberate change).
26
+ 2. Both steps route to the same next step, unless the experiment is explicitly about the flow.
23
27
 
24
- When `?editor=true` is enabled, the editor panel can force runtime mode and select experiment variants. Builder preview can also navigate steps through the preview bridge. This lets agents verify each variant without changing the manifest.
28
+ ## How the Runtime Behaves
29
+
30
+ When a visitor opens the source step, the runtime suspends rendering until the assignment from `@funnelsgrove/analytics` is ready, then routes to the assigned `stepId` and syncs the URL. After assignment, navigation continues through the assigned step's normal edges — the experiment is not re-evaluated mid-flow. Assignments are sticky per visitor.
31
+
32
+ The paywall runtime is experiment-ready out of the box: paywall variants can differ in copy, layout, plan presentation, or pricing source, and checkout/discount state stays scoped per funnel.
33
+
34
+ ## Verify (part of the < 1 min setup)
35
+
36
+ In local preview or builder preview, force each side with the editor override:
37
+
38
+ ```text
39
+ /paywall?editor=true&experimentVariant=control
40
+ /paywall?editor=true&experimentVariant=variant_b
41
+ ```
42
+
43
+ Check: opening the source URL promotes to the assigned route, each variant renders, Continue advances to the step after the variant. Then run a normal (non-editor) open to confirm the suspension → assignment → route flow works.
25
44
 
26
45
  ## Rules
27
46
 
28
- - Keep `control` stable.
29
- - Keep variant keys durable and readable.
30
- - Test the attachment step and every variant route.
31
- - Do not put unrelated changes in a variant; isolate one major decision.
32
- - Do not remove a live variant until analysis and rollout are approved.
33
- - If a variant changes paywall plans or checkout behavior, update `@funnelsgrove/analytics` event expectations too.
47
+ - One deliberate change per experiment. Don't bundle unrelated edits into a variant.
48
+ - `control` stays stable; never edit the control step as part of launching a variant.
49
+ - Add stable `control.label` and `variant.label` values in the `<experiment-id>-control` / `<experiment-id>-variant-b` format so builder labels and analytics views stay readable.
50
+ - Variant keys and experiment ids are durable they flow into analytics. Never recycle an id for a different hypothesis.
51
+ - Keep every variant step's outgoing edge in `edgesByStepId`, or assigned visitors strand.
52
+ - If a paywall variant changes plans or pricing, follow [payment-plans-and-discounts.md](payment-plans-and-discounts.md) for the plan/discount sync rules and QA both variants' checkout ([qa-checklist.md](qa-checklist.md)).
53
+ - Don't remove a running variant until the user confirms the analysis is done; route all traffic to the winner by setting `trafficPercent` rather than deleting history.
@@ -18,15 +18,15 @@ Analytics should describe meaningful user behavior. `@funnelsgrove/analytics` is
18
18
 
19
19
  Experiment assignments are attached as feature flag properties when available. Preview runtime skips normal analytics delivery.
20
20
 
21
- ## Custom Events
21
+ ## Standard Checkout Events
22
22
 
23
- Use `publicAnalyticsSdk.track(...)` from `@funnelsgrove/analytics`:
23
+ Use named helpers from `@funnelsgrove/analytics` for checkout lifecycle events:
24
24
 
25
25
  ```ts
26
- publicAnalyticsSdk.track({
27
- eventType: 'checkout_started',
26
+ publicAnalyticsSdk.trackCheckoutStarted({
28
27
  stepId: stepPaywallId,
29
28
  stepName: stepPaywall.name || stepPaywall.title,
29
+ stepType: stepPaywall.type,
30
30
  metadata: {
31
31
  planId,
32
32
  providerPlanId,
@@ -37,6 +37,8 @@ publicAnalyticsSdk.track({
37
37
  });
38
38
  ```
39
39
 
40
+ Use `trackPaymentInfoSubmitted(...)` for payment details submitted, `trackCheckoutCompleted(...)` after a confirmed checkout, and `trackPaymentCheckoutSucceeded(...)` / `trackPaymentCheckoutReturned(...)` on redirect return screens.
41
+
40
42
  Flush before redirects or payment handoff when losing the page would drop the event:
41
43
 
42
44
  ```ts
@@ -60,3 +62,4 @@ Provider-specific behavior is documented separately. Read [Meta Pixel and Conver
60
62
  - Use `@funnelsgrove/analytics` for custom tracking; do not call the provider SDK directly.
61
63
  - When changing flow, verify step start/end still fires on the new path.
62
64
  - When changing offers, verify checkout events include plan id, provider plan id, coupon id, amount, and mode.
65
+ - Use `stepType: 'paywall_offer'` on paywall checkout events so analytics rollups do not depend on step names.
@@ -6,15 +6,24 @@ Flow is product logic. The source of truth is `src/config/funnel.manifest.ts`; r
6
6
 
7
7
  The manifest defines:
8
8
 
9
- - `viewport`: the designed shell size, usually `430 x 932`.
10
- - `assets`: image metadata used by runtime/build tooling. Funnel shells may use
11
- this data to warm likely next-step images, but first-viewport images should
12
- still use the framework's normal priority/preload mechanism.
13
- - `steps`: every routable step with `id`, `path`, `filePath`, `componentKey`, `type`, optional `kind`, and optional `assetIds`.
9
+ - `viewport`: the designed shell size. New funnels also declare default QA
10
+ `breakpoints`: small 375x667, medium 393x852, large 402x874, and
11
+ desktop-small 1280x800.
12
+ - `assets`: image metadata used by runtime/build tooling. Declare every
13
+ funnel-critical raster image here with stable `src`, `width`, and `height`
14
+ so publish can reduce image size during the build and generate AVIF/WebP
15
+ variants. Funnel shells may use this data to warm likely next-step images,
16
+ but first-viewport images should still use the framework's normal
17
+ priority/preload mechanism.
18
+ - `steps`: every routable step with `id`, `path`, `filePath`, `componentKey`, `type`, optional `kind`, optional `tags`, and optional `assetIds`. New `path` values must be meaningful public route slugs, not `/step-1` style URLs. Sequential ids are acceptable when the funnel uses them internally.
14
19
  - `edgesByStepId`: graph edges between steps.
20
+ - `branches`: builder metadata for conditional paths that own one or more steps before reconverging.
15
21
  - `experiments`: optional variant routing.
16
22
 
17
23
  Keep `steps[].id`, `path`, and answer keys stable unless the request is a migration.
24
+ When adding a step, prefer a semantic path like `/motivation`, `/fitness-goal`,
25
+ or `/email-capture`; do not expose internal ordering through `/step-1` or
26
+ `/step-07` routes.
18
27
 
19
28
  ## Routing Rules
20
29
 
@@ -37,6 +46,36 @@ eligibility: [
37
46
 
38
47
  The flow controller resolves the next step from configured edges first, then falls back to the sequential order. It also keeps the URL in sync through `getPathForStep(...)` and browser history.
39
48
 
49
+ ## Branch Labels
50
+
51
+ A branch starts when one step has different next steps depending on an answer. If a direction owns one or more branch-only steps before all directions share a common next step again, add a `branches` entry:
52
+
53
+ ```ts
54
+ branches: [
55
+ {
56
+ id: 'device-android',
57
+ name: 'Device type',
58
+ sourceStepId: 'eligibility',
59
+ conditionId: 'eligibility:yes',
60
+ label: 'yes-branch',
61
+ tags: ['android-branch', 'yes-branch'],
62
+ stepIds: ['active-google-claim', 'subscriptions'],
63
+ },
64
+ ]
65
+ ```
66
+
67
+ `name` is the readable branch group shown in builder. `label` should come from the answer that opened the path, such as `yes-branch`, `no-branch`, or `family-branch`. `stepIds` includes only the steps owned by that direction; stop before the common next step. Directions that jump directly to the common next step do not need a branch entry.
68
+
69
+ Keep branch and edge metadata builder-readable, following the ClaimBee pattern:
70
+ `branches` should be an inline array on the manifest, and `edgesByStepId`
71
+ should use literal string keys and `toStepId` values. Avoid hiding builder
72
+ metadata behind constants such as `branches: flowBranches`, computed keys such
73
+ as `[emailCaptureManifestStep.id]`, or targets such as
74
+ `toStepId: emailCaptureManifestStep.id`; the builder reads these fields from
75
+ source text without executing the module.
76
+
77
+ Use `steps[].tags` for stable builder labels that belong to one step, including current experiment labels such as `paywall-test-control`. Keep stopped A/B variants and other intentional inactive screens tagged too; builder derives `unused` automatically when those steps are not reachable from the default entry point or any active experiment path.
78
+
40
79
  ## Step Navigation
41
80
 
42
81
  Use the funnel context:
@@ -62,12 +101,25 @@ Experiments attach to a step and route to variant steps:
62
101
  }
63
102
  ```
64
103
 
65
- The runtime resolves assignments through the shared analytics/runtime integration outside preview and uses editor overrides inside preview/editor mode. Keep the control variant stable and do not remove a running variant until analytics have been reviewed.
104
+ The runtime resolves assignments through the shared analytics/runtime integration outside preview and uses editor overrides inside preview/editor mode. Opening the source step waits for the assignment, then opens the assigned route and keeps the URL in sync. Continue then advances from the assigned route's normal graph edge, with the already-applied experiment ignored for that continuation.
105
+
106
+ Keep experiment redirects out of step components. Add normal `edgesByStepId` entries for the source/control step and every variant step, then let `goNext()` or the shell Continue button use the shared runtime.
107
+
108
+ Keep the control variant stable and do not remove a running variant until analytics have been reviewed.
66
109
 
67
110
  ## Checklist
68
111
 
69
112
  - Add or update the manifest step.
70
113
  - Register the component in `src/runtime/step-registry.ts`.
71
- - Update `edgesByStepId`, `entryPoints`, and `assetIds` if needed.
114
+ - Update `edgesByStepId`, `entryPoints`, `branches`, step `tags`, and `assetIds` if needed.
115
+ - Label every branch-owned path with `branches[].name`, answer-derived `label`, useful `tags`, and owned `stepIds`.
116
+ - Keep builder metadata inline/literal so groups, labels, and `unused` badges are visible in builder.
117
+ - Label every running experiment variant with stable tags/labels like `<experiment-id>-control` and `<experiment-id>-variant-b`.
118
+ - Confirm intentionally inactive/unreachable steps will show as `unused` in builder.
119
+ - Use a meaningful public `path` for every new step. Internal ids can be
120
+ sequential, but URLs should describe the screen.
121
+ - Keep image preloading manifest-driven: step images belong in
122
+ `funnelManifest.assets` and step `assetIds`; the shell should warm only likely
123
+ next-step images at low priority, not the entire funnel.
72
124
  - Make sure skipped steps do not own required answers.
73
125
  - Verify the step before, the edited step, and the step after.
@@ -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 small 375x667, medium 393x852, large 402x874, and desktop-small 1280x800, 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:
@@ -6,10 +6,53 @@ Local edits are not public until they are synced and published. Sync creates or
6
6
 
7
7
  ```bash
8
8
  fgrove status
9
+ git status --short
10
+ fgrove github status
11
+ fgrove publish --env preview --message '<summary>'
12
+ ```
13
+
14
+ Before syncing down over an existing synced directory, check local changes. If
15
+ `git status --short` shows local edits, checkpoint them first. `fgrove sync
16
+ down` refuses to overwrite a dirty synced directory by default; use `--force`
17
+ only when discarding local changes is intentional.
18
+
19
+ When GitHub is connected and its branch is ahead, run `fgrove github pull` to
20
+ pull GitHub into the hosted draft, then poll `fgrove github status` until the
21
+ pull job is completed or skipped. Sync that latest draft into a clean directory
22
+ and merge local changes intentionally before continuing.
23
+
24
+ For GitHub-connected funnels, source changes must be pushed to GitHub first,
25
+ then pulled into the hosted draft:
26
+
27
+ ```bash
28
+ git push
29
+ fgrove github pull
30
+ fgrove github status
31
+ fgrove publish --env preview --message '<summary>'
32
+ ```
33
+
34
+ Poll `fgrove github status` until the pull job is completed or skipped before
35
+ publishing. Do not run `fgrove sync up` for the same source diff; the CLI and
36
+ API reject draft source sync for GitHub-connected funnels.
37
+
38
+ When GitHub is not connected, the hosted draft is the remote source of truth.
39
+ Download it into a temporary clean directory with `fgrove sync down --funnel
40
+ <id-or-slug> --dir <temp-dir>` or an already selected `fgrove use` context,
41
+ compare it with local changes, merge intentionally, rerun checks, then `fgrove
42
+ sync up`.
43
+
44
+ For funnels without GitHub, sync local source directly to the hosted draft:
45
+
46
+ ```bash
9
47
  fgrove sync up --message '<summary>'
10
48
  fgrove publish --env preview --message '<summary>'
11
49
  ```
12
50
 
51
+ If `fgrove sync up` reports that the remote draft changed since the local
52
+ directory was synced, do not retry blindly. Download the current draft into a
53
+ temporary clean directory, merge local changes against it, rerun checks, and
54
+ sync again.
55
+
13
56
  Refresh the ignored local `.env` from the remote project when project settings change:
14
57
 
15
58
  ```bash
@@ -26,9 +69,24 @@ fgrove publish --env production --domain <domain> --message '<summary>'
26
69
 
27
70
  Production publish requires a domain. Preview publish returns a deployment URL, version sequence, and version id.
28
71
 
72
+ ## Publish Image Optimization
73
+
74
+ Publish builds reduce raster image size before upload. Keep this path enabled:
75
+ PNG/JPEG sources are compressed, AVIF/WebP variants are generated for supported
76
+ browsers, and the original image remains the fallback. Do not bypass this with
77
+ remote funnel-critical image URLs.
78
+
79
+ After publishing image edits or a production candidate, check the deployment
80
+ metadata, CLI output, or `publishBuild.stageTimings.imageVariants`. If the
81
+ image-variant stage is missing or unavailable, report that explicitly before
82
+ calling the publish ready.
83
+
29
84
  ## Local Sync Contract
30
85
 
31
- 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.
86
+ The CLI writes `.funnelsgrove-sync.json` into the local tree. Keep it there. It
87
+ records workspace id, funnel id, current draft version id, and source hashes so
88
+ later `sync up` can patch only changed/deleted files and detect stale remote
89
+ drafts before overwriting them.
32
90
 
33
91
  Local-only files are excluded from upload:
34
92
 
@@ -45,8 +103,15 @@ Do not edit generated build output as the source of truth.
45
103
  ## Agent Rules
46
104
 
47
105
  - Run `fgrove status` before syncing.
106
+ - Check local changes before `sync down`; checkpoint or merge instead of
107
+ overwriting.
108
+ - For GitHub-connected funnels, push with normal git, then run `fgrove github
109
+ pull`; do not run `fgrove sync up` for the same source change.
48
110
  - Use clear sync and publish messages.
49
- - Sync before publishing.
111
+ - Sync the hosted draft before publishing: GitHub-connected funnels use normal
112
+ `git push` plus `fgrove github pull`; funnels without GitHub use `fgrove sync
113
+ up`.
50
114
  - Publish preview first and verify the returned URL.
51
- - If preview verification fails, fix locally, run checks, sync up again, and publish a new preview.
115
+ - If preview verification fails, fix locally, run checks, sync the hosted draft
116
+ again through the correct source path, and publish a new preview.
52
117
  - Do not production publish without explicit instruction and target domain.
@@ -0,0 +1,80 @@
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
+ Run every visual pass at all four default breakpoints (rules in [step-ui-guidelines.md](step-ui-guidelines.md)):
26
+
27
+ | Breakpoint | Size |
28
+ | --- | --- |
29
+ | small | **375x667** |
30
+ | medium | **393x852** |
31
+ | large | **402x874** |
32
+ | desktop-small | **1280x800** |
33
+
34
+ - Nothing intersects or overlaps: text never collides with images, cards, badges, or the action bar; modals and dialogs fit the viewport.
35
+ - Continue button sits on an opaque bar at the bottom and stays visible on every step, including while content scrolls.
36
+ - No horizontal scroll, no clipped or truncated content, no broken images.
37
+ - Disabled CTA states render correctly and enable when valid.
38
+
39
+ ## 3. Image Performance
40
+
41
+ For image edits and every preview-to-production candidate:
42
+
43
+ - Confirm edited step images are listed in `funnelManifest.assets` and attached
44
+ to the relevant manifest steps with `assetIds`.
45
+ - Confirm first-viewport images use the framework's priority/preload mechanism.
46
+ - Confirm the shell warms only likely next-step images at low priority, not the
47
+ full funnel image set.
48
+ - After preview or production publish, confirm build metadata includes the
49
+ `imageVariants` stage or name why it is unavailable.
50
+ - In the browser/network panel, confirm AVIF/WebP variants are served when the
51
+ browser advertises support, with the original raster image as fallback.
52
+
53
+ ## 4. Paywall and Checkout (most important)
54
+
55
+ Test the paywall on every QA run that touches it, pricing, discounts, or experiments — on the paywall step **and** every live paywall variant:
56
+
57
+ 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.
58
+ 2. **Countdown / discount state** — first-stage discount applies on open; timer counts down.
59
+ 3. **Checkout opens** — CTA opens the checkout with the selected plan; itemized totals match the selected plan and applied coupon.
60
+ 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.
61
+ **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.
62
+ 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.
63
+ 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.
64
+ 7. **Success state** — subscription-started renders, registration/next-step handoff works.
65
+
66
+ If checkout, payment mode, or test credentials are unavailable, name the skipped item explicitly — a silent skip counts as a fail.
67
+
68
+ ## 5. Registration and Links
69
+
70
+ - Complete the registration page with valid test data; confirm validation errors on invalid data.
71
+ - Click every legal/support/account link on paywall, checkout, and registration: terms, privacy, subscription policy, money-back, support email.
72
+
73
+ ## 6. Subscription Management
74
+
75
+ - Open `/manage-subscription?user_id=<test-user>` (or with `stripe_customer_id`).
76
+ - Confirm the subscription list loads and the cancellation flow completes when a test subscription is available; otherwise name it as a skipped item.
77
+
78
+ ## Reporting
79
+
80
+ Every QA run ends with a short report: stage + URLs tested, steps/branches/variants covered, breakpoint results for small 375x667, medium 393x852, large 402x874, and desktop-small 1280x800, 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.