@funnelsgrove/cli 0.1.2 → 0.1.4
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.
- package/dist/apiClient.js +11 -1
- package/dist/cli.js +64 -15
- package/dist/localSync.d.ts +12 -0
- package/dist/localSync.js +53 -1
- package/package.json +2 -2
- package/template_docs/AGENTS.md +102 -0
- package/template_docs/CLAUDE.md +3 -0
- package/template_docs/docs/ab-experiments.md +31 -8
- package/template_docs/docs/analytics.md +56 -8
- package/template_docs/docs/editing-flow.md +69 -8
- package/template_docs/docs/editing-step.md +64 -8
- package/template_docs/docs/editor-and-content.md +56 -8
- package/template_docs/docs/payment-plans-and-discounts.md +51 -8
- package/template_docs/docs/publishing-and-versioning.md +38 -6
- package/template_docs/docs/sdk-api-endpoints.md +44 -8
- package/template_docs/docs/theme.md +33 -8
- package/template_docs/agent.md +0 -24
package/dist/apiClient.js
CHANGED
|
@@ -46,6 +46,16 @@ export async function callTrpcProcedure(input) {
|
|
|
46
46
|
? `${baseUrl}?input=${encodeURIComponent(JSON.stringify(input.input))}`
|
|
47
47
|
: baseUrl;
|
|
48
48
|
const response = await fetchFn(url, init);
|
|
49
|
-
const
|
|
49
|
+
const responseText = await response.text();
|
|
50
|
+
let json;
|
|
51
|
+
try {
|
|
52
|
+
json = JSON.parse(responseText);
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
const contentType = response.headers.get('content-type') || 'unknown content type';
|
|
56
|
+
const snippet = responseText.replace(/\s+/g, ' ').trim().slice(0, 500);
|
|
57
|
+
const details = snippet ? `: ${snippet}` : '';
|
|
58
|
+
throw new Error(`FunnelsGrove API returned ${response.status} ${response.statusText || 'Unknown status'} with ${contentType}${details}`);
|
|
59
|
+
}
|
|
50
60
|
return parseTrpcJsonResponse(json);
|
|
51
61
|
}
|
package/dist/cli.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
2
3
|
import { cp, mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
3
4
|
import { createInterface } from 'node:readline/promises';
|
|
4
5
|
import path from 'node:path';
|
|
@@ -6,10 +7,22 @@ import { fileURLToPath } from 'node:url';
|
|
|
6
7
|
import { Command } from 'commander';
|
|
7
8
|
import { callTrpcProcedure } from './apiClient.js';
|
|
8
9
|
import { clearActiveContext, getDefaultAuthConfigPath, loadActiveContext, loadAuthToken, saveActiveContext, saveAuthToken, } from './authStore.js';
|
|
9
|
-
import { buildSyncManifest, collectSourceFiles, readSyncManifest, writeSourceFiles, writeSyncManifest, } from './localSync.js';
|
|
10
|
+
import { buildSyncManifest, chunkChangedSourceFiles, collectChangedSourceFiles, collectSourceFiles, readSyncManifest, writeSourceFiles, writeSyncManifest, } from './localSync.js';
|
|
10
11
|
import { isKnownTemplate, KNOWN_TEMPLATE_SLUGS, reskinFunnel } from './reskin.js';
|
|
11
12
|
import { syncTemplateDocs, TEMPLATE_DOCS_DIR } from './templateDocs.js';
|
|
12
13
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
14
|
+
const readCliVersion = () => {
|
|
15
|
+
try {
|
|
16
|
+
const packageJson = JSON.parse(readFileSync(path.resolve(__dirname, '..', 'package.json'), 'utf8'));
|
|
17
|
+
if (typeof packageJson.version === 'string' && packageJson.version.trim()) {
|
|
18
|
+
return packageJson.version;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
// Fall through to the packaged fallback below.
|
|
23
|
+
}
|
|
24
|
+
return '0.1.3';
|
|
25
|
+
};
|
|
13
26
|
const toKebabCase = (value) => value
|
|
14
27
|
.trim()
|
|
15
28
|
.replace(/([a-z])([A-Z])/g, '$1-$2')
|
|
@@ -205,7 +218,7 @@ const program = new Command();
|
|
|
205
218
|
program
|
|
206
219
|
.name('fgrove')
|
|
207
220
|
.description('FunnelsGrove CLI for editing, syncing, and publishing funnels')
|
|
208
|
-
.version(
|
|
221
|
+
.version(readCliVersion())
|
|
209
222
|
.option('--api-url <url>', 'FunnelsGrove tRPC API URL', process.env.FUNNELSGROVE_API_URL || DEFAULT_API_URL)
|
|
210
223
|
.option('--config <path>', 'Auth config path', process.env.FUNNELSGROVE_CONFIG || getDefaultAuthConfigPath());
|
|
211
224
|
addExamples(program, [
|
|
@@ -434,24 +447,60 @@ addExamples(syncCommand
|
|
|
434
447
|
funnel: options.funnel,
|
|
435
448
|
dir: options.dir,
|
|
436
449
|
});
|
|
437
|
-
const
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
450
|
+
const changes = target.manifest
|
|
451
|
+
? await collectChangedSourceFiles(target.sourceDir, target.manifest)
|
|
452
|
+
: null;
|
|
453
|
+
if (changes && changes.files.length === 0 && changes.deletedPaths.length === 0) {
|
|
454
|
+
console.log('No local changes to sync.');
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
let result = null;
|
|
458
|
+
let syncedFileCount = 0;
|
|
459
|
+
let deletedFileCount = 0;
|
|
460
|
+
if (changes) {
|
|
461
|
+
const batches = chunkChangedSourceFiles(changes);
|
|
462
|
+
for (const batch of batches) {
|
|
463
|
+
result = await callApi({
|
|
464
|
+
path: 'funnels.patchSource',
|
|
465
|
+
type: 'mutation',
|
|
466
|
+
token,
|
|
467
|
+
data: {
|
|
468
|
+
workspaceId: target.workspaceId,
|
|
469
|
+
funnelId: target.funnelId,
|
|
470
|
+
message: options.message,
|
|
471
|
+
files: batch.files,
|
|
472
|
+
deletedPaths: batch.deletedPaths,
|
|
473
|
+
},
|
|
474
|
+
});
|
|
475
|
+
syncedFileCount += result.syncedFiles.length;
|
|
476
|
+
deletedFileCount += result.deletedFiles?.length || 0;
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
else {
|
|
480
|
+
result = await callApi({
|
|
481
|
+
path: 'funnels.importSource',
|
|
482
|
+
type: 'mutation',
|
|
483
|
+
token,
|
|
484
|
+
data: {
|
|
485
|
+
workspaceId: target.workspaceId,
|
|
486
|
+
funnelId: target.funnelId,
|
|
487
|
+
message: options.message,
|
|
488
|
+
files: await collectSourceFiles(target.sourceDir),
|
|
489
|
+
},
|
|
490
|
+
});
|
|
491
|
+
syncedFileCount = result.syncedFiles.length;
|
|
492
|
+
deletedFileCount = result.deletedFiles?.length || 0;
|
|
493
|
+
}
|
|
494
|
+
if (!result) {
|
|
495
|
+
throw new Error('No sync result returned.');
|
|
496
|
+
}
|
|
449
497
|
await writeSyncManifest(target.sourceDir, await buildSyncManifest(target.sourceDir, {
|
|
450
498
|
workspaceId: target.workspaceId,
|
|
451
499
|
funnelId: target.funnelId,
|
|
452
500
|
draftVersionId: result.versionId,
|
|
453
501
|
}));
|
|
454
|
-
|
|
502
|
+
const deletedSummary = deletedFileCount > 0 ? ` and removed ${deletedFileCount} files` : '';
|
|
503
|
+
console.log(`Synced ${syncedFileCount} files${deletedSummary} to draft v${result.versionSeq} (${result.versionId})`);
|
|
455
504
|
});
|
|
456
505
|
addExamples(program
|
|
457
506
|
.command('publish')
|
package/dist/localSync.d.ts
CHANGED
|
@@ -16,10 +16,22 @@ export type SourceFile = {
|
|
|
16
16
|
content: string;
|
|
17
17
|
contentType?: string;
|
|
18
18
|
};
|
|
19
|
+
export type ChangedSourceFiles = {
|
|
20
|
+
currentManifest: SyncManifest;
|
|
21
|
+
deletedPaths: string[];
|
|
22
|
+
files: SourceFile[];
|
|
23
|
+
};
|
|
24
|
+
export type SourceFilePatchBatch = {
|
|
25
|
+
deletedPaths: string[];
|
|
26
|
+
files: SourceFile[];
|
|
27
|
+
};
|
|
28
|
+
export declare const DEFAULT_PATCH_BATCH_CONTENT_CHARS = 12000000;
|
|
19
29
|
export declare function normalizeSyncPath(filePath: string): string;
|
|
20
30
|
export declare function shouldSyncFile(filePath: string): boolean;
|
|
21
31
|
export declare function buildSyncManifest(rootDir: string, input: SyncManifestInput): Promise<SyncManifest>;
|
|
22
32
|
export declare function writeSyncManifest(rootDir: string, manifest: SyncManifest): Promise<void>;
|
|
23
33
|
export declare function readSyncManifest(rootDir: string): Promise<SyncManifest | null>;
|
|
24
34
|
export declare function collectSourceFiles(rootDir: string): Promise<SourceFile[]>;
|
|
35
|
+
export declare function collectChangedSourceFiles(rootDir: string, previousManifest: SyncManifest): Promise<ChangedSourceFiles>;
|
|
36
|
+
export declare function chunkChangedSourceFiles(changes: Pick<ChangedSourceFiles, 'deletedPaths' | 'files'>, maxContentChars?: number): SourceFilePatchBatch[];
|
|
25
37
|
export declare function writeSourceFiles(rootDir: string, files: SourceFile[]): Promise<void>;
|
package/dist/localSync.js
CHANGED
|
@@ -2,6 +2,7 @@ import { createHash } from 'node:crypto';
|
|
|
2
2
|
import { mkdir, readdir, readFile, stat, writeFile } from 'node:fs/promises';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
export const SYNC_MANIFEST_FILE = '.funnelsgrove-sync.json';
|
|
5
|
+
export const DEFAULT_PATCH_BATCH_CONTENT_CHARS = 12_000_000;
|
|
5
6
|
const EXCLUDED_PATH_PARTS = new Set(['node_modules', '.next', 'out']);
|
|
6
7
|
export function normalizeSyncPath(filePath) {
|
|
7
8
|
const normalized = path.posix.normalize(filePath.replaceAll('\\', '/'));
|
|
@@ -76,7 +77,58 @@ export async function collectSourceFiles(rootDir) {
|
|
|
76
77
|
funnelId: '',
|
|
77
78
|
draftVersionId: '',
|
|
78
79
|
});
|
|
79
|
-
|
|
80
|
+
return readSourceFiles(rootDir, manifest.files);
|
|
81
|
+
}
|
|
82
|
+
export async function collectChangedSourceFiles(rootDir, previousManifest) {
|
|
83
|
+
const currentManifest = await buildSyncManifest(rootDir, {
|
|
84
|
+
workspaceId: previousManifest.workspaceId,
|
|
85
|
+
funnelId: previousManifest.funnelId,
|
|
86
|
+
draftVersionId: previousManifest.draftVersionId,
|
|
87
|
+
});
|
|
88
|
+
const previousHashByPath = new Map(previousManifest.files.map((file) => [file.path, file.hash]));
|
|
89
|
+
const currentHashByPath = new Map(currentManifest.files.map((file) => [file.path, file.hash]));
|
|
90
|
+
const changedManifestFiles = currentManifest.files.filter((file) => previousHashByPath.get(file.path) !== file.hash);
|
|
91
|
+
const deletedPaths = previousManifest.files
|
|
92
|
+
.filter((file) => !currentHashByPath.has(file.path))
|
|
93
|
+
.map((file) => file.path)
|
|
94
|
+
.sort((left, right) => left.localeCompare(right));
|
|
95
|
+
return {
|
|
96
|
+
currentManifest,
|
|
97
|
+
deletedPaths,
|
|
98
|
+
files: await readSourceFiles(rootDir, changedManifestFiles),
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
export function chunkChangedSourceFiles(changes, maxContentChars = DEFAULT_PATCH_BATCH_CONTENT_CHARS) {
|
|
102
|
+
const batches = [];
|
|
103
|
+
let currentBatch = {
|
|
104
|
+
deletedPaths: changes.deletedPaths,
|
|
105
|
+
files: [],
|
|
106
|
+
};
|
|
107
|
+
let currentContentChars = 0;
|
|
108
|
+
const pushCurrentBatch = () => {
|
|
109
|
+
if (currentBatch.files.length === 0 && currentBatch.deletedPaths.length === 0) {
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
batches.push(currentBatch);
|
|
113
|
+
currentBatch = {
|
|
114
|
+
deletedPaths: [],
|
|
115
|
+
files: [],
|
|
116
|
+
};
|
|
117
|
+
currentContentChars = 0;
|
|
118
|
+
};
|
|
119
|
+
for (const file of changes.files) {
|
|
120
|
+
if (currentBatch.files.length > 0 &&
|
|
121
|
+
currentContentChars + file.content.length > maxContentChars) {
|
|
122
|
+
pushCurrentBatch();
|
|
123
|
+
}
|
|
124
|
+
currentBatch.files.push(file);
|
|
125
|
+
currentContentChars += file.content.length;
|
|
126
|
+
}
|
|
127
|
+
pushCurrentBatch();
|
|
128
|
+
return batches;
|
|
129
|
+
}
|
|
130
|
+
async function readSourceFiles(rootDir, manifestFiles) {
|
|
131
|
+
const files = await Promise.all(manifestFiles.map(async (file) => {
|
|
80
132
|
const absolutePath = path.join(rootDir, assertSafeSyncPath(file.path));
|
|
81
133
|
const imageContentType = inferImageContentType(file.path);
|
|
82
134
|
const buffer = await readFile(absolutePath);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@funnelsgrove/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"description": "FunnelsGrove command-line tools for editing, syncing, and publishing funnels",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
"access": "public"
|
|
17
17
|
},
|
|
18
18
|
"scripts": {
|
|
19
|
-
"build": "rm -rf dist && tsc",
|
|
19
|
+
"build": "rm -rf dist && tsc && chmod +x dist/cli.js",
|
|
20
20
|
"prepack": "npm run build",
|
|
21
21
|
"test": "vitest run"
|
|
22
22
|
},
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# AGENTS.md
|
|
2
|
+
|
|
3
|
+
Behavioral guidelines to reduce common LLM coding mistakes. Merge these rules with project-specific instructions as needed.
|
|
4
|
+
|
|
5
|
+
**Tradeoff:** These guidelines bias toward caution over speed. For trivial tasks, use judgment.
|
|
6
|
+
|
|
7
|
+
## Read Docs Before Editing
|
|
8
|
+
|
|
9
|
+
Before changing any funnel file, read the relevant topic docs in this folder and follow their contracts. Make the edit according to those docs first; if the requested change conflicts with the docs, stop and explain the conflict before editing.
|
|
10
|
+
|
|
11
|
+
## 1. Think Before Coding
|
|
12
|
+
|
|
13
|
+
**Don't assume. Don't hide confusion. Surface tradeoffs.**
|
|
14
|
+
|
|
15
|
+
Before implementing:
|
|
16
|
+
- State your assumptions explicitly. If uncertain, ask.
|
|
17
|
+
- If multiple interpretations exist, present them. Do not pick silently.
|
|
18
|
+
- If a simpler approach exists, say so. Push back when warranted.
|
|
19
|
+
- If something is unclear, stop. Name what's confusing. Ask.
|
|
20
|
+
|
|
21
|
+
## 2. Simplicity First
|
|
22
|
+
|
|
23
|
+
**Minimum code that solves the problem. Nothing speculative.**
|
|
24
|
+
|
|
25
|
+
- No features beyond what was asked.
|
|
26
|
+
- No abstractions for single-use code.
|
|
27
|
+
- No flexibility or configurability that was not requested.
|
|
28
|
+
- No error handling for impossible scenarios.
|
|
29
|
+
- If you write 200 lines and it could be 50, rewrite it.
|
|
30
|
+
|
|
31
|
+
Ask yourself: "Would a senior engineer say this is overcomplicated?" If yes, simplify.
|
|
32
|
+
|
|
33
|
+
## 3. Surgical Changes
|
|
34
|
+
|
|
35
|
+
**Touch only what you must. Clean up only your own mess.**
|
|
36
|
+
|
|
37
|
+
When editing existing code:
|
|
38
|
+
- Do not improve adjacent code, comments, or formatting.
|
|
39
|
+
- Do not refactor things that are not broken.
|
|
40
|
+
- Match existing style, even if you would do it differently.
|
|
41
|
+
- If you notice unrelated dead code, mention it. Do not delete it.
|
|
42
|
+
|
|
43
|
+
When your changes create orphans:
|
|
44
|
+
- Remove imports, variables, functions, and files that your changes made unused.
|
|
45
|
+
- Do not remove pre-existing dead code unless asked.
|
|
46
|
+
|
|
47
|
+
The test: every changed line should trace directly to the user's request.
|
|
48
|
+
|
|
49
|
+
## 4. Goal-Driven Execution
|
|
50
|
+
|
|
51
|
+
**Define success criteria. Loop until verified.**
|
|
52
|
+
|
|
53
|
+
Transform tasks into verifiable goals:
|
|
54
|
+
- "Add validation" -> "Write tests for invalid inputs, then make them pass"
|
|
55
|
+
- "Fix the bug" -> "Write a test that reproduces it, then make it pass"
|
|
56
|
+
- "Refactor X" -> "Ensure tests pass before and after"
|
|
57
|
+
|
|
58
|
+
For multi-step tasks, state a brief plan:
|
|
59
|
+
|
|
60
|
+
```text
|
|
61
|
+
1. [Step] -> verify: [check]
|
|
62
|
+
2. [Step] -> verify: [check]
|
|
63
|
+
3. [Step] -> verify: [check]
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Strong success criteria let you loop independently. Weak criteria like "make it work" require constant clarification.
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
**These guidelines are working if:** fewer unnecessary changes in diffs, fewer rewrites due to overcomplication, and clarifying questions come before implementation rather than after mistakes.
|
|
71
|
+
|
|
72
|
+
## Documentation Index
|
|
73
|
+
|
|
74
|
+
Use this folder as the local source-of-truth guide when editing a synced FunnelsGrove funnel.
|
|
75
|
+
|
|
76
|
+
Current funnel templates are small Next.js apps built on shared runtime modules:
|
|
77
|
+
|
|
78
|
+
- `@funnelsgrove/runtime` owns the funnel contract: routing, user state, content localization, builder preview patches, subscriptions, analytics helpers, runtime env, and theme variables.
|
|
79
|
+
- `@funnelsgrove/analytics` owns analytics tracking and A/B experiment tracking. It encapsulates the PostHog integration so funnel steps do not call PostHog directly.
|
|
80
|
+
- `@funnelsgrove/payments` owns checkout plans, discounts, Stripe sessions/intents, wallet slots, and shared checkout UI.
|
|
81
|
+
- The local funnel owns product decisions: step views, content files, editor fields, flow manifest, theme, checked-in assets, and billing plan ids.
|
|
82
|
+
|
|
83
|
+
Keep changes simple and local. Most edits should touch one step file plus its matching `content/` and `editor/` files, or one manifest/config file for flow, theme, or billing changes.
|
|
84
|
+
|
|
85
|
+
## Funnel Workflow
|
|
86
|
+
|
|
87
|
+
1. Run `fgrove status` and confirm the active project and funnel.
|
|
88
|
+
2. Keep edits inside the synced funnel tree.
|
|
89
|
+
3. Run the funnel's local checks before syncing.
|
|
90
|
+
4. Run `fgrove sync up --message '<summary>'`.
|
|
91
|
+
5. Run `fgrove publish --env preview --message '<summary>'` and verify the preview.
|
|
92
|
+
6. Publish production only when explicitly requested.
|
|
93
|
+
|
|
94
|
+
- [Editing or Creating a Step](docs/editing-step.md)
|
|
95
|
+
- [Editing Flow](docs/editing-flow.md)
|
|
96
|
+
- [Editor and Content](docs/editor-and-content.md)
|
|
97
|
+
- [Payment Plans and Discounts](docs/payment-plans-and-discounts.md)
|
|
98
|
+
- [SDK API Endpoints](docs/sdk-api-endpoints.md)
|
|
99
|
+
- [Analytics](docs/analytics.md)
|
|
100
|
+
- [A/B Experiments](docs/ab-experiments.md)
|
|
101
|
+
- [Theme](docs/theme.md)
|
|
102
|
+
- [Publishing and Versioning](docs/publishing-and-versioning.md)
|
|
@@ -1,10 +1,33 @@
|
|
|
1
1
|
# A/B Experiments
|
|
2
2
|
|
|
3
|
-
Experiments
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
3
|
+
Experiments live in `src/config/funnel.manifest.ts`. They are routing rules, not ad hoc conditionals inside step components.
|
|
4
|
+
|
|
5
|
+
## Runtime Contract
|
|
6
|
+
|
|
7
|
+
An experiment attaches to a source step and maps variant keys to routable step ids:
|
|
8
|
+
|
|
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
|
+
}
|
|
18
|
+
```
|
|
19
|
+
|
|
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.
|
|
21
|
+
|
|
22
|
+
## Editor and Preview
|
|
23
|
+
|
|
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.
|
|
25
|
+
|
|
26
|
+
## Rules
|
|
27
|
+
|
|
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.
|
|
@@ -1,10 +1,58 @@
|
|
|
1
1
|
# Analytics
|
|
2
2
|
|
|
3
|
-
Analytics should describe meaningful user behavior
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
-
|
|
10
|
-
-
|
|
3
|
+
Analytics should describe meaningful user behavior. `@funnelsgrove/analytics` is the tracking boundary for funnel code: it encapsulates the analytics integration for event delivery and experiment metadata.
|
|
4
|
+
|
|
5
|
+
## Standard Events
|
|
6
|
+
|
|
7
|
+
`FunnelFlow` tracks:
|
|
8
|
+
|
|
9
|
+
- `step_start` when a step becomes active.
|
|
10
|
+
- `step_end` when the user leaves a step, including the answer diff selected during that step.
|
|
11
|
+
|
|
12
|
+
`@funnelsgrove/analytics` maps canonical runtime event types to provider event names:
|
|
13
|
+
|
|
14
|
+
- `step_start` -> `step_started`.
|
|
15
|
+
- `step_end` -> `step_completed`.
|
|
16
|
+
- `funnel_start` -> `funnel_started`.
|
|
17
|
+
|
|
18
|
+
Experiment assignments are attached as feature flag properties when available. Preview runtime skips normal analytics delivery.
|
|
19
|
+
|
|
20
|
+
## Custom Events
|
|
21
|
+
|
|
22
|
+
Use `publicAnalyticsSdk.track(...)` from `@funnelsgrove/analytics`:
|
|
23
|
+
|
|
24
|
+
```ts
|
|
25
|
+
publicAnalyticsSdk.track({
|
|
26
|
+
eventType: 'checkout_started',
|
|
27
|
+
stepId: stepPaywallId,
|
|
28
|
+
stepName: stepPaywall.name || stepPaywall.title,
|
|
29
|
+
metadata: {
|
|
30
|
+
planId,
|
|
31
|
+
providerPlanId,
|
|
32
|
+
couponId,
|
|
33
|
+
amountCents,
|
|
34
|
+
environment,
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Flush before redirects or payment handoff when losing the page would drop the event:
|
|
40
|
+
|
|
41
|
+
```ts
|
|
42
|
+
await publicAnalyticsSdk.flush().catch(() => 0);
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Experiment Tracking
|
|
46
|
+
|
|
47
|
+
Flow and experiment assignment are configured in `src/config/funnel.manifest.ts`, while tracking uses the analytics package. When an experiment is active, the runtime passes assignment metadata into `publicAnalyticsSdk.trackStepStarted(...)`, `trackStepCompleted(...)`, and custom events through feature flag properties.
|
|
48
|
+
|
|
49
|
+
Agents should only document and verify the analytics package surface: stable event names, step ids, selected answers, checkout metadata, and experiment metadata. Treat the underlying provider as an implementation detail.
|
|
50
|
+
|
|
51
|
+
## Agent Rules
|
|
52
|
+
|
|
53
|
+
- Keep event names stable.
|
|
54
|
+
- Use `metadata`, `payload`, `context`, and `selected` for small analysis fields only.
|
|
55
|
+
- Do not send personal data, secrets, full payment objects, SDK keys, or raw Stripe responses.
|
|
56
|
+
- Use `@funnelsgrove/analytics` for custom tracking; do not call the provider SDK directly.
|
|
57
|
+
- When changing flow, verify step start/end still fires on the new path.
|
|
58
|
+
- When changing offers, verify checkout events include plan id, provider plan id, coupon id, amount, and mode.
|
|
@@ -1,10 +1,71 @@
|
|
|
1
1
|
# Editing Flow
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
-
|
|
10
|
-
-
|
|
3
|
+
Flow is product logic. The source of truth is `src/config/funnel.manifest.ts`; runtime helpers in `@funnelsgrove/runtime` resolve paths, edges, entry points, experiments, and browser history.
|
|
4
|
+
|
|
5
|
+
## Manifest Contract
|
|
6
|
+
|
|
7
|
+
The manifest defines:
|
|
8
|
+
|
|
9
|
+
- `viewport`: the designed shell size, usually `430 x 932`.
|
|
10
|
+
- `assets`: preloaded image metadata used by runtime/build tooling.
|
|
11
|
+
- `steps`: every routable step with `id`, `path`, `filePath`, `componentKey`, `type`, optional `kind`, and optional `assetIds`.
|
|
12
|
+
- `edgesByStepId`: graph edges between steps.
|
|
13
|
+
- `experiments`: optional variant routing.
|
|
14
|
+
|
|
15
|
+
Keep `steps[].id`, `path`, and answer keys stable unless the request is a migration.
|
|
16
|
+
|
|
17
|
+
## Routing Rules
|
|
18
|
+
|
|
19
|
+
Sequential routes use:
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
edgesByStepId: {
|
|
23
|
+
claim: [{ toStepId: 'profile' }],
|
|
24
|
+
}
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Conditional routes use `conditionId` values written by `goChoice(...)`. For yes/no choices the runtime expects ids like:
|
|
28
|
+
|
|
29
|
+
```ts
|
|
30
|
+
eligibility: [
|
|
31
|
+
{ toStepId: 'active-google-claim', conditionId: 'eligibility:yes' },
|
|
32
|
+
{ toStepId: 'subscriptions', conditionId: 'eligibility:no' },
|
|
33
|
+
]
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
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.
|
|
37
|
+
|
|
38
|
+
## Step Navigation
|
|
39
|
+
|
|
40
|
+
Use the funnel context:
|
|
41
|
+
|
|
42
|
+
- `goNext()` for normal manifest progression.
|
|
43
|
+
- `goChoice('yes' | 'no')` when the current branch is represented by conditional edges.
|
|
44
|
+
- `goToStep(stepId)` for explicit jumps such as paywall success or manage-subscription return.
|
|
45
|
+
|
|
46
|
+
Do not hardcode route strings in step logic when `getPathForStep(...)` or context navigation is available.
|
|
47
|
+
|
|
48
|
+
## Experiments
|
|
49
|
+
|
|
50
|
+
Experiments attach to a step and route to variant steps:
|
|
51
|
+
|
|
52
|
+
```ts
|
|
53
|
+
{
|
|
54
|
+
experimentId: 'paywall-ab',
|
|
55
|
+
stepId: 'paywall',
|
|
56
|
+
variants: [
|
|
57
|
+
{ variantKey: 'control', routeToStepId: 'paywall' },
|
|
58
|
+
{ variantKey: 'variant_b', routeToStepId: 'paywall-b' },
|
|
59
|
+
],
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
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.
|
|
64
|
+
|
|
65
|
+
## Checklist
|
|
66
|
+
|
|
67
|
+
- Add or update the manifest step.
|
|
68
|
+
- Register the component in `src/runtime/step-registry.ts`.
|
|
69
|
+
- Update `edgesByStepId`, `entryPoints`, and `assetIds` if needed.
|
|
70
|
+
- Make sure skipped steps do not own required answers.
|
|
71
|
+
- Verify the step before, the edited step, and the step after.
|
|
@@ -1,10 +1,66 @@
|
|
|
1
|
-
# Editing a Step
|
|
1
|
+
# Editing or Creating a Step
|
|
2
2
|
|
|
3
|
-
A step is one screen or decision point in the
|
|
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.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
-
|
|
10
|
-
-
|
|
5
|
+
## Source Files
|
|
6
|
+
|
|
7
|
+
For an existing step, start from `src/config/funnel.manifest.ts` and find:
|
|
8
|
+
|
|
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`.
|
|
13
|
+
|
|
14
|
+
Most steps also have:
|
|
15
|
+
|
|
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.
|
|
19
|
+
|
|
20
|
+
## View Pattern
|
|
21
|
+
|
|
22
|
+
Step files usually export:
|
|
23
|
+
|
|
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(...)`.
|
|
27
|
+
|
|
28
|
+
Use `useFunnel()` for runtime state:
|
|
29
|
+
|
|
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.
|
|
34
|
+
|
|
35
|
+
Do not bypass the flow controller with raw `window.location` for normal funnel navigation.
|
|
36
|
+
|
|
37
|
+
## CSS Pattern
|
|
38
|
+
|
|
39
|
+
Keep one-off step styling at the bottom of the step file:
|
|
40
|
+
|
|
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
|
+
`;
|
|
52
|
+
```
|
|
53
|
+
|
|
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.
|
|
55
|
+
|
|
56
|
+
## Creating a Step
|
|
57
|
+
|
|
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.
|
|
65
|
+
|
|
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.
|
|
@@ -1,10 +1,58 @@
|
|
|
1
1
|
# Editor and Content
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
3
|
+
Editable step content is a runtime contract. The step view should read from typed content, not from scattered hardcoded strings, when the value is copy, image, legal text, pricing label, FAQ, testimonial, or builder-editable UI.
|
|
4
|
+
|
|
5
|
+
## Content Files
|
|
6
|
+
|
|
7
|
+
Each step content file exports a typed `LocalizedStepContent<T>`:
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
export const stepContent = {
|
|
11
|
+
defaultLocale: 'en',
|
|
12
|
+
locales: {
|
|
13
|
+
en: {
|
|
14
|
+
headline: ['People getting', 'free money', 'know one secret'],
|
|
15
|
+
artwork: { src: '/figma/example.png', alt: '' },
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
} as const satisfies LocalizedStepContent<StepLocaleContent>;
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
The runtime validates that a default locale exists and resolves locale from funnel attributes (`browserLanguage`, `language`, `locale`) or browser locale. In preview, builder content patches are merged before the step renders.
|
|
22
|
+
|
|
23
|
+
Use runtime item types where possible:
|
|
24
|
+
|
|
25
|
+
- `StepImage` for images.
|
|
26
|
+
- `ChoiceItem`, `InfoItem`, `QuoteItem`, `QaItem`, `LinkItem`, `ReasonItem`, `StoreLinkItem`, `PlanPresentationItem` for lists.
|
|
27
|
+
- `CountryPricingProfile` for country-specific plan ordering/defaults.
|
|
28
|
+
|
|
29
|
+
## Editor Files
|
|
30
|
+
|
|
31
|
+
Each editor file exports `readonly StepEditorSection[]`. A field's `path` must match the content object path:
|
|
32
|
+
|
|
33
|
+
```ts
|
|
34
|
+
{
|
|
35
|
+
id: 'hero.image',
|
|
36
|
+
label: 'Hero image',
|
|
37
|
+
kind: 'image',
|
|
38
|
+
path: 'hero.image',
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Supported field kinds are `text`, `textLines`, `textarea`, `image`, `boolean`, `select`, `list`, and `pricingProfile`. List fields must use a preset such as `choiceItems`, `infoItems`, `quoteItems`, `qaItems`, or `linkItems`.
|
|
43
|
+
|
|
44
|
+
Only expose fields the builder should edit. Do not add spare content fields for possible future use.
|
|
45
|
+
|
|
46
|
+
## Variables
|
|
47
|
+
|
|
48
|
+
For builder variables, set `supportsVariables: true` on text-capable fields only. Editor variables map `user.*` tokens to answer paths, then runtime interpolation replaces `{{ user.token }}` style values in supported fields. Universal variables also support simple `{user_id}` replacement at render time.
|
|
49
|
+
|
|
50
|
+
Keep variable use obvious. If a step depends on a value from an earlier step, make sure that earlier step writes a stable answer key.
|
|
51
|
+
|
|
52
|
+
## Agent Rules
|
|
53
|
+
|
|
54
|
+
- Keep copy, images, FAQ, testimonials, legal strings, and checkout labels in content files.
|
|
55
|
+
- Keep layout decisions in the step file.
|
|
56
|
+
- Keep editor sections aligned with the content shape.
|
|
57
|
+
- Keep `src/steps/step-content.registry.ts` in sync when adding or renaming content/editor files.
|
|
58
|
+
- Use checked-in public assets (`/paywall/...`, `/figma/...`, etc.); do not leave temporary localhost asset URLs.
|
|
@@ -1,10 +1,53 @@
|
|
|
1
1
|
# Payment Plans and Discounts
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
-
|
|
10
|
-
-
|
|
3
|
+
Payments are handled through `@funnelsgrove/payments`. Funnel steps should use the shared payment module instead of building raw Stripe calls.
|
|
4
|
+
|
|
5
|
+
## Plan Sources
|
|
6
|
+
|
|
7
|
+
Local plans live in `src/config/billing.plans.ts`. A plan catalog is keyed by stable funnel keys (`primary`, `secondary`, `tertiary`) and each plan includes:
|
|
8
|
+
|
|
9
|
+
- `projectPlanId`: FunnelsGrove project plan id.
|
|
10
|
+
- `providerPlanId`: Stripe price id.
|
|
11
|
+
- `title`, `priceLabel`, `perDayAmount`, `amountCents`.
|
|
12
|
+
- Optional `featuredTag`, `oldPriceLabel`, `checkoutSummaryLabel`, `billingInterval`, `billingIntervalCount`, `isDefault`.
|
|
13
|
+
|
|
14
|
+
ClaimBee-style funnels may keep separate test/live catalogs with `createRuntimeModeBillingPlanCatalog(...)` and choose the active catalog through `useRuntimeMode()` plus checkout mode resolution.
|
|
15
|
+
|
|
16
|
+
## Runtime Resolution
|
|
17
|
+
|
|
18
|
+
Use the shared helpers:
|
|
19
|
+
|
|
20
|
+
- `buildConfigPaywallPlans(...)` for local catalogs.
|
|
21
|
+
- `useResolvedPaywallPlans(...)` when the funnel can load synced project Stripe plans.
|
|
22
|
+
- `getDefaultPlanId(...)`, `findPaywallPlan(...)`, and `getPaywallPlanSelectionValue(...)` for selection.
|
|
23
|
+
- `usePreviewStepPaywallPlans(stepId)` so builder paywall plan edits appear in preview.
|
|
24
|
+
|
|
25
|
+
If a catalog is only a mapping of `{ projectPlanId }`, the payment module can load remote project plans from `/sdk/public/payments/plans` and map them back to funnel keys.
|
|
26
|
+
|
|
27
|
+
## Discounts
|
|
28
|
+
|
|
29
|
+
Discounts use `BillingDiscountList` or `BillingDiscountCatalog` and are resolved by `buildBillingDiscountCatalog(...)`. The current ClaimBee pattern has two stages:
|
|
30
|
+
|
|
31
|
+
- First stage: coupon id, percent, duration.
|
|
32
|
+
- Second stage: stronger coupon id, percent, duration, and previous percent.
|
|
33
|
+
|
|
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
|
+
|
|
36
|
+
## Checkout
|
|
37
|
+
|
|
38
|
+
Prefer shared Stripe surfaces:
|
|
39
|
+
|
|
40
|
+
- `useStripeSubscriptionCheckoutSession(...)` for subscription checkout sessions.
|
|
41
|
+
- `SharedStripeCheckoutV2Dialog` for card checkout UI.
|
|
42
|
+
- `ApplePaySubscriptionCheckoutSlot` and `GooglePaySubscriptionCheckoutSlot` for wallet buttons.
|
|
43
|
+
- `StripeExpressCheckoutButton` / `StripePlanSelector` for older paywall variants.
|
|
44
|
+
|
|
45
|
+
Runtime config should come from `runtimePublicConfig` through a local `src/runtime/checkout-runtime-config.ts`. Include `apiBaseUrl`, `funnelId`, and the SDK publishable key. Preview seed keys may resolve to the preview payment key and force test checkout mode.
|
|
46
|
+
|
|
47
|
+
## Agent Rules
|
|
48
|
+
|
|
49
|
+
- Never change a live `projectPlanId`, `providerPlanId`, coupon id, or amount casually.
|
|
50
|
+
- Keep displayed price, discounted price, amount cents, coupon id, checkout payload, and analytics metadata in sync.
|
|
51
|
+
- Track checkout starts/completions with plan id, provider plan id, coupon id, amount, and environment.
|
|
52
|
+
- Keep legal, renewal, guarantee, support, and button labels in content files.
|
|
53
|
+
- Test test-mode and live-mode plan resolution separately when a payment change affects both.
|
|
@@ -1,10 +1,42 @@
|
|
|
1
1
|
# Publishing and Versioning
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Local edits are not public until they are synced and published. Sync creates or patches a draft version. Publish deploys that draft.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
## CLI Flow
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
fgrove status
|
|
9
|
+
fgrove sync up --message '<summary>'
|
|
10
|
+
fgrove publish --env preview --message '<summary>'
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Only publish production when explicitly requested:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
fgrove publish --env production --domain <domain> --message '<summary>'
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Production publish requires a domain. Preview publish returns a deployment URL, version sequence, and version id.
|
|
20
|
+
|
|
21
|
+
## Local Sync Contract
|
|
22
|
+
|
|
23
|
+
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.
|
|
24
|
+
|
|
25
|
+
Local-only files are excluded from upload:
|
|
26
|
+
|
|
27
|
+
- `.env`, `.env.local`, `.env.*` except `.env.example`.
|
|
28
|
+
- `.funnelsgrove-sync.json`.
|
|
29
|
+
- `node_modules`.
|
|
30
|
+
- `.next`.
|
|
31
|
+
- `out`.
|
|
32
|
+
|
|
33
|
+
Do not edit generated build output as the source of truth.
|
|
34
|
+
|
|
35
|
+
## Agent Rules
|
|
36
|
+
|
|
37
|
+
- Run `fgrove status` before syncing.
|
|
38
|
+
- Use clear sync and publish messages.
|
|
39
|
+
- Sync before publishing.
|
|
7
40
|
- Publish preview first and verify the returned URL.
|
|
8
|
-
-
|
|
9
|
-
-
|
|
10
|
-
- If preview verification fails, fix locally, sync up again, and publish a new preview.
|
|
41
|
+
- If preview verification fails, fix locally, run checks, sync up again, and publish a new preview.
|
|
42
|
+
- Do not production publish without explicit instruction and target domain.
|
|
@@ -1,10 +1,46 @@
|
|
|
1
1
|
# SDK API Endpoints
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
-
|
|
10
|
-
-
|
|
3
|
+
Funnel code should call shared runtime/payment/analytics services, not raw endpoints, unless a new shared helper is being built.
|
|
4
|
+
|
|
5
|
+
## Runtime Config
|
|
6
|
+
|
|
7
|
+
`@funnelsgrove/runtime` reads public config from env aliases and exposes `runtimePublicConfig`:
|
|
8
|
+
|
|
9
|
+
- SDK API base URL.
|
|
10
|
+
- funnel id and funnel version id.
|
|
11
|
+
- SDK publishable key.
|
|
12
|
+
- project id and analytics provider key.
|
|
13
|
+
- support/legal/app-store/deep-link metadata.
|
|
14
|
+
|
|
15
|
+
`buildMainApiUrl(...)` and `buildSdkHeaders(...)` add the correct SDK base URL and `x-sdk-publishable-key`. Preview frame runtime intentionally suppresses normal SDK calls.
|
|
16
|
+
|
|
17
|
+
## User and State APIs
|
|
18
|
+
|
|
19
|
+
Use `apiService` from `@funnelsgrove/runtime`:
|
|
20
|
+
|
|
21
|
+
- `bootstrapSession(...)`: create/restore the visitor using local user id, URL `user_id`, attribution, email, or name.
|
|
22
|
+
- `updateUser(...)`: persist email/name/progress attributes.
|
|
23
|
+
- `uploadTempPhoto(...)`: upload or locally preview image captures.
|
|
24
|
+
- `getManageSubscriptions()` and `updateSubscription(...)`: subscription management by `user_id` or `stripe_customer_id`.
|
|
25
|
+
- `trackFunnelEvent(...)`, `trackStepStarted(...)`, `trackStepCompleted(...)`: legacy runtime event delivery.
|
|
26
|
+
|
|
27
|
+
The flow controller already bootstraps the user, persists answer attributes, records completed steps, and connects analytics identity through the shared analytics/runtime integration. Steps should only call `apiService.updateUser(...)` directly when they collect profile data such as checkout email.
|
|
28
|
+
|
|
29
|
+
## Payment APIs
|
|
30
|
+
|
|
31
|
+
Use `@funnelsgrove/payments`:
|
|
32
|
+
|
|
33
|
+
- `getPaywallPlans(...)` / `useResolvedPaywallPlans(...)` for `/sdk/public/payments/plans`.
|
|
34
|
+
- `createStripePaymentIntent(...)` for payment intents.
|
|
35
|
+
- `createStripeSubscriptionCheckout(...)` for subscription Elements checkout.
|
|
36
|
+
- `createStripeCheckoutSession(...)` or `redirectToStripeCheckout(...)` for hosted checkout.
|
|
37
|
+
|
|
38
|
+
Always pass runtime checkout config so published funnels, preview funnels, and local builds resolve the correct funnel id and SDK key.
|
|
39
|
+
|
|
40
|
+
## Rules
|
|
41
|
+
|
|
42
|
+
- Keep payloads minimal and typed.
|
|
43
|
+
- Do not log personal data, payment details, SDK keys, or Stripe secrets.
|
|
44
|
+
- Handle loading, unavailable config, API failure, and final failure states in UI.
|
|
45
|
+
- Use local fallbacks only where the shared service already supports them.
|
|
46
|
+
- If an endpoint contract changes, update the shared service, callers, tests, and these docs together.
|
|
@@ -1,10 +1,35 @@
|
|
|
1
1
|
# Theme
|
|
2
2
|
|
|
3
|
-
Theme
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
-
|
|
10
|
-
-
|
|
3
|
+
Theme is a runtime contract in `src/theme/theme.ts`. It is converted to CSS variables by `createThemeCssVariables(...)` from `@funnelsgrove/runtime`.
|
|
4
|
+
|
|
5
|
+
## Theme Contract
|
|
6
|
+
|
|
7
|
+
The theme owns:
|
|
8
|
+
|
|
9
|
+
- Interface colors: `background`, `primary`, `primaryText`, `secondary`, `secondaryText`, `text`.
|
|
10
|
+
- Semantic colors: `button`, `buttonText`, `surface`, `border`, `warning`, `danger`.
|
|
11
|
+
- Derived colors: muted/success/warning/danger/neutral backgrounds, muted text, soft accents.
|
|
12
|
+
- Typography: font family, weights, sizes, line heights.
|
|
13
|
+
- Device shell: active preset, shell width, max width, padding, corner radius.
|
|
14
|
+
- Safe area offsets.
|
|
15
|
+
|
|
16
|
+
The generated CSS vars include `--color-bg`, `--color-primary`, `--color-secondary`, `--color-text`, `--font-family-base`, `--padding`, `--corner-radius`, and shell sizing vars.
|
|
17
|
+
|
|
18
|
+
## Styling Rules
|
|
19
|
+
|
|
20
|
+
- Use CSS variables in step CSS and shared CSS.
|
|
21
|
+
- Use `src/app/globals.css` only for global reset, fonts, and body/root behavior.
|
|
22
|
+
- Use `src/steps/styles/shared/` for repeated step patterns.
|
|
23
|
+
- Keep one-off styles inside the step file at the bottom.
|
|
24
|
+
- Keep the designed shell stable unless the request is explicitly about device size.
|
|
25
|
+
|
|
26
|
+
ClaimBee-style funnels use a 430px mobile shell, Inter, and product colors through theme variables. If changing brand colors, update `theme.ts` first and only add local CSS overrides when the layout genuinely needs them.
|
|
27
|
+
|
|
28
|
+
## Verification
|
|
29
|
+
|
|
30
|
+
After theme edits, check:
|
|
31
|
+
|
|
32
|
+
- Main happy path.
|
|
33
|
+
- Paywall and checkout states.
|
|
34
|
+
- Mobile shell and desktop centered preview.
|
|
35
|
+
- Contrast for primary buttons, secondary accents, disabled/loading states, and error text.
|
package/template_docs/agent.md
DELETED
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
# Funnel Editing Guide
|
|
2
|
-
|
|
3
|
-
Use this folder as the local source-of-truth guide when editing a synced FunnelsGrove funnel.
|
|
4
|
-
|
|
5
|
-
## Workflow
|
|
6
|
-
|
|
7
|
-
1. Run `fgrove status` and confirm the active project and funnel.
|
|
8
|
-
2. Keep edits inside the synced funnel tree.
|
|
9
|
-
3. Run the funnel's local checks before syncing.
|
|
10
|
-
4. Run `fgrove sync up --message '<summary>'`.
|
|
11
|
-
5. Run `fgrove publish --env preview --message '<summary>'` and verify the preview.
|
|
12
|
-
6. Publish production only when explicitly requested.
|
|
13
|
-
|
|
14
|
-
## Topics
|
|
15
|
-
|
|
16
|
-
- [Editing a Step](docs/editing-step.md)
|
|
17
|
-
- [Editing Flow](docs/editing-flow.md)
|
|
18
|
-
- [Editor and Content](docs/editor-and-content.md)
|
|
19
|
-
- [Payment Plans and Discounts](docs/payment-plans-and-discounts.md)
|
|
20
|
-
- [SDK API Endpoints](docs/sdk-api-endpoints.md)
|
|
21
|
-
- [Analytics](docs/analytics.md)
|
|
22
|
-
- [A/B Experiments](docs/ab-experiments.md)
|
|
23
|
-
- [Theme](docs/theme.md)
|
|
24
|
-
- [Publishing and Versioning](docs/publishing-and-versioning.md)
|