@funnelsgrove/cli 0.1.53 → 0.1.62
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/cli.d.ts +1 -1
- package/dist/cli.js +24 -2
- package/dist/experimentCreate.d.ts +3 -8
- package/dist/experimentCreate.js +19 -18
- package/funnel-contract-compatibility.json +35 -35
- package/package.json +4 -4
- package/template_docs/.funnelsgrove-docs.json +4 -4
- package/template_docs/docs/funnelsgrove/migrations/step-contract-v3.md +1 -1
- package/template_docs/docs/funnelsgrove/recipes/add-experiment.md +11 -10
- package/template_docs/funnel-docs.config.json +1 -1
- package/template_scaffold/.funnelsgrove-contract-tools.json +1 -1
- package/template_scaffold/.funnelsgrove-docs.json +4 -4
- package/template_scaffold/.funnelsgrove-scaffold.json +19 -34
- package/template_scaffold/docs/funnelsgrove/migrations/step-contract-v3.md +1 -1
- package/template_scaffold/docs/funnelsgrove/recipes/add-experiment.md +11 -10
- package/template_scaffold/funnel-docs.config.json +1 -1
- package/template_scaffold/package-lock.json +14 -14
- package/template_scaffold/package.json +3 -3
- package/template_scaffold/src/components/FunnelFlow.tsx +26 -0
- package/template_scaffold/src/config/funnel.manifest.ts +2 -2
- package/template_scaffold/src/contract/funnel-project.validation.ts +1 -2
- package/template_scaffold/src/runtime/funnel-runtime.ts +2 -10
- package/template_scaffold/src/runtime/use-funnel-flow-controller.ts +15 -1
- package/template_scaffold/src/steps/step-32-paywall.tsx +47 -15
- package/template_scaffold/tests/funnel-agent-docs.test.ts +5 -4
- package/template_scaffold/tests/src/components/FunnelFlow.test.tsx +4 -0
- package/template_scaffold/tests/src/contract/funnel-project.validation.test.ts +1 -2
- package/template_scaffold/tests/src/steps/step-32-paywall.test.ts +6 -3
- package/template_validation/.funnelsgrove-contract-tools.json +1 -1
- package/template_validation/src/contract/funnel-project.validation.ts +1 -2
- package/template_scaffold/src/config/experiments.generated.ts +0 -4
- package/template_scaffold/src/config/experiments.ts +0 -9
- package/template_scaffold/tests/src/config/experiments.test.ts +0 -12
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
} from '@/runtime/checkout-runtime-config';
|
|
10
10
|
import {
|
|
11
11
|
billingDiscountStorageKeys,
|
|
12
|
-
|
|
12
|
+
billingDiscountList,
|
|
13
13
|
claimbeePlansByMode,
|
|
14
14
|
} from '@/config/billing.plans';
|
|
15
15
|
import { stepPaywallContent } from '@/steps/content/step-32-paywall.content';
|
|
@@ -21,9 +21,12 @@ import {
|
|
|
21
21
|
collectCurrentFunnelAttribution,
|
|
22
22
|
readPaywallStateValue,
|
|
23
23
|
runtimePublicConfig,
|
|
24
|
+
resolveGeneratedOfferSetRuntime,
|
|
24
25
|
updatePaywallStateValue,
|
|
25
26
|
useFunnel,
|
|
27
|
+
useFunnelRuntimeConfig,
|
|
26
28
|
useRuntimeMode,
|
|
29
|
+
type GeneratedOfferSet,
|
|
27
30
|
type FunnelStepMeta,
|
|
28
31
|
} from '@funnelsgrove/runtime';
|
|
29
32
|
import { getPathForStep } from '@/runtime/funnel-runtime';
|
|
@@ -34,6 +37,7 @@ import {
|
|
|
34
37
|
SharedStripeCheckoutV2Dialog,
|
|
35
38
|
activateSecondPaywallDiscount,
|
|
36
39
|
advancePaywallDiscountState,
|
|
40
|
+
buildBillingDiscountCatalog,
|
|
37
41
|
buildConfigPaywallPlans,
|
|
38
42
|
buildDiscountedPaywallPlans,
|
|
39
43
|
buildPaywallRenewalDisclaimer,
|
|
@@ -43,6 +47,8 @@ import {
|
|
|
43
47
|
serializePaywallDiscountState,
|
|
44
48
|
usePlatformWalletPaymentMethods,
|
|
45
49
|
useStripeSubscriptionCheckoutSession,
|
|
50
|
+
type BillingFallbackPlanConfig,
|
|
51
|
+
type BillingPlanCatalog,
|
|
46
52
|
type PaywallDiscountState,
|
|
47
53
|
type PlatformWalletPaymentMethod,
|
|
48
54
|
} from '@funnelsgrove/payments';
|
|
@@ -93,7 +99,6 @@ const paywallDetailImageDimensions = {
|
|
|
93
99
|
},
|
|
94
100
|
} as const;
|
|
95
101
|
const defaultSelectedPlan = 'secondary';
|
|
96
|
-
const CLAIMBEE_PAYWALL_SECOND_COUPON_ID = billingDiscounts.second.couponId;
|
|
97
102
|
export const CLAIMBEE_PAYWALL_DISCOUNT_STORAGE_KEYS = billingDiscountStorageKeys;
|
|
98
103
|
const deferPaywallStateSync = (syncState: () => void): (() => void) => {
|
|
99
104
|
const timeoutId = window.setTimeout(syncState, 0);
|
|
@@ -133,16 +138,43 @@ function StepPaywallRuntime({
|
|
|
133
138
|
[runtimeMode],
|
|
134
139
|
);
|
|
135
140
|
const { user, setUser, attributes, featureFlags, completeStep } = useFunnel();
|
|
141
|
+
const publishedRuntime = useFunnelRuntimeConfig();
|
|
136
142
|
const content = useClaimbeeStepContent(stepPaywallId, stepPaywallContent);
|
|
137
143
|
const supportEmail = runtimePublicConfig.supportEmail;
|
|
138
144
|
const walletPaymentMethods = usePlatformWalletPaymentMethods();
|
|
145
|
+
const publishedOfferSetRuntime = useMemo(
|
|
146
|
+
() => publishedRuntime.config && publishedRuntime.config.offerSets.length > 0
|
|
147
|
+
? resolveGeneratedOfferSetRuntime<BillingFallbackPlanConfig>({
|
|
148
|
+
offerSets: publishedRuntime.config.offerSets as readonly GeneratedOfferSet[],
|
|
149
|
+
baseCatalogsByMode: claimbeePlansByMode,
|
|
150
|
+
fallbackDiscounts: billingDiscountList,
|
|
151
|
+
})
|
|
152
|
+
: null,
|
|
153
|
+
[publishedRuntime.config],
|
|
154
|
+
);
|
|
155
|
+
const activeBillingDiscounts = useMemo(
|
|
156
|
+
() => buildBillingDiscountCatalog(
|
|
157
|
+
publishedOfferSetRuntime?.getDiscountList(checkoutMode) ?? billingDiscountList,
|
|
158
|
+
),
|
|
159
|
+
[checkoutMode, publishedOfferSetRuntime],
|
|
160
|
+
);
|
|
161
|
+
const activePlanCatalog = useMemo<BillingPlanCatalog>(
|
|
162
|
+
() => publishedOfferSetRuntime?.getPlanCatalog(checkoutMode) as BillingPlanCatalog
|
|
163
|
+
|| (checkoutMode === 'test' ? claimbeePlansByMode.test : claimbeePlansByMode.live),
|
|
164
|
+
[checkoutMode, publishedOfferSetRuntime],
|
|
165
|
+
);
|
|
166
|
+
const activePlanKeys = useMemo(
|
|
167
|
+
() => publishedOfferSetRuntime?.planKeys.length
|
|
168
|
+
? publishedOfferSetRuntime.planKeys
|
|
169
|
+
: ['primary', 'secondary', 'tertiary'],
|
|
170
|
+
[publishedOfferSetRuntime],
|
|
171
|
+
);
|
|
139
172
|
const availablePlans = useMemo(
|
|
140
|
-
() =>
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
[checkoutMode],
|
|
173
|
+
() => buildConfigPaywallPlans(
|
|
174
|
+
activePlanCatalog,
|
|
175
|
+
activePlanKeys,
|
|
176
|
+
),
|
|
177
|
+
[activePlanCatalog, activePlanKeys],
|
|
146
178
|
);
|
|
147
179
|
const initialPlanId = useMemo(
|
|
148
180
|
() =>
|
|
@@ -168,7 +200,7 @@ function StepPaywallRuntime({
|
|
|
168
200
|
const [discountStateReady, setDiscountStateReady] = useState(false);
|
|
169
201
|
const [discountState, setDiscountState] = useState<PaywallDiscountState>(() =>
|
|
170
202
|
resolvePaywallDiscountState({
|
|
171
|
-
discounts:
|
|
203
|
+
discounts: activeBillingDiscounts,
|
|
172
204
|
nowMs: Date.now(),
|
|
173
205
|
storedValue: null,
|
|
174
206
|
}),
|
|
@@ -229,7 +261,7 @@ function StepPaywallRuntime({
|
|
|
229
261
|
const activeStory = storyOptions[activeStoryIndex] ?? storyOptions[0] ?? null;
|
|
230
262
|
const minutes = Math.floor(discountState.remainingSeconds / 60);
|
|
231
263
|
const seconds = discountState.remainingSeconds % 60;
|
|
232
|
-
const upgradedDiscountActive = activeCouponId ===
|
|
264
|
+
const upgradedDiscountActive = activeCouponId === activeBillingDiscounts.second.couponId;
|
|
233
265
|
const activeDiscountStartedAtMs =
|
|
234
266
|
discountState.stage === 'second'
|
|
235
267
|
? discountState.secondStartedAtMs
|
|
@@ -371,7 +403,7 @@ function StepPaywallRuntime({
|
|
|
371
403
|
return deferPaywallStateSync(() => {
|
|
372
404
|
setDiscountState(
|
|
373
405
|
resolvePaywallDiscountState({
|
|
374
|
-
discounts:
|
|
406
|
+
discounts: activeBillingDiscounts,
|
|
375
407
|
nowMs: Date.now(),
|
|
376
408
|
storedValue: readPaywallStateValue({
|
|
377
409
|
legacyKeys: CLAIMBEE_PAYWALL_DISCOUNT_STORAGE_KEYS,
|
|
@@ -380,7 +412,7 @@ function StepPaywallRuntime({
|
|
|
380
412
|
);
|
|
381
413
|
setDiscountStateReady(true);
|
|
382
414
|
});
|
|
383
|
-
}, []);
|
|
415
|
+
}, [activeBillingDiscounts]);
|
|
384
416
|
|
|
385
417
|
useEffect(() => {
|
|
386
418
|
return deferPaywallStateSync(() => {
|
|
@@ -396,7 +428,7 @@ function StepPaywallRuntime({
|
|
|
396
428
|
const timer = window.setInterval(() => {
|
|
397
429
|
setDiscountState((current) => {
|
|
398
430
|
const nextState = advancePaywallDiscountState({
|
|
399
|
-
discounts:
|
|
431
|
+
discounts: activeBillingDiscounts,
|
|
400
432
|
state: current,
|
|
401
433
|
nowMs: Date.now(),
|
|
402
434
|
});
|
|
@@ -409,7 +441,7 @@ function StepPaywallRuntime({
|
|
|
409
441
|
}, 1000);
|
|
410
442
|
|
|
411
443
|
return () => window.clearInterval(timer);
|
|
412
|
-
}, [discountStateReady]);
|
|
444
|
+
}, [activeBillingDiscounts, discountStateReady]);
|
|
413
445
|
|
|
414
446
|
useEffect(() => {
|
|
415
447
|
if (storyOptions.length < 2) {
|
|
@@ -567,7 +599,7 @@ function StepPaywallRuntime({
|
|
|
567
599
|
paywallStepRef.current?.scrollTo({ top: 0, behavior: 'smooth' });
|
|
568
600
|
setDiscountState((current) =>
|
|
569
601
|
activateSecondPaywallDiscount({
|
|
570
|
-
discounts:
|
|
602
|
+
discounts: activeBillingDiscounts,
|
|
571
603
|
state: current,
|
|
572
604
|
nowMs: Date.now(),
|
|
573
605
|
}),
|
|
@@ -254,11 +254,12 @@ describe('funnel agent documentation supply', () => {
|
|
|
254
254
|
expect(content).toContain('fgrove experiments create --spec experiment.json --dir . --json');
|
|
255
255
|
expect(content).toContain('`experimentId`, `experimentKey`, `draftVersionId`, and `writtenPaths`');
|
|
256
256
|
expect(content).toContain(
|
|
257
|
-
'
|
|
257
|
+
'Pricing variants additionally require `offerSetId` as a UUID.',
|
|
258
258
|
);
|
|
259
|
+
expect(content).not.toContain('offerSetKey');
|
|
259
260
|
expect(content).toContain('does not create or activate a PostHog flag');
|
|
260
|
-
expect(content).toContain('
|
|
261
|
-
expect(content).toContain('
|
|
261
|
+
expect(content).toContain('included in the version-bound runtime artifact');
|
|
262
|
+
expect(content).toContain('deprecated compatibility files');
|
|
262
263
|
expect(content).toContain('[FG-EXPERIMENT-STALE-DRAFT]');
|
|
263
264
|
expect(content).toContain('[FG-EXPERIMENT-DRAFT-CONFLICT]');
|
|
264
265
|
expect(content).toContain('[FG-EXPERIMENT-SPEC]');
|
|
@@ -362,7 +363,7 @@ describe('funnel agent documentation supply', () => {
|
|
|
362
363
|
|
|
363
364
|
expect(manifest).toMatchObject({
|
|
364
365
|
schemaVersion: 1,
|
|
365
|
-
bundleVersion: '2.0.
|
|
366
|
+
bundleVersion: '2.0.51',
|
|
366
367
|
stepContractVersion: contract.stepContractVersion,
|
|
367
368
|
contractHash: contract.contractHash,
|
|
368
369
|
});
|
|
@@ -13,6 +13,10 @@ describe('FunnelFlow lifecycle wiring', () => {
|
|
|
13
13
|
expect(flowSource).not.toContain('publicAnalyticsSdk');
|
|
14
14
|
expect(adapterSource.match(/analytics: publicAnalyticsSdk/g)).toHaveLength(1);
|
|
15
15
|
expect(adapterSource.match(/stepContractVersion: funnelManifest\.stepContractVersion/g)).toHaveLength(1);
|
|
16
|
+
expect(flowSource).toContain('<FunnelRuntimeConfigBoundary');
|
|
17
|
+
expect(flowSource).toContain('disabled={isPreviewRuntime}');
|
|
18
|
+
expect(adapterSource).toContain('toManifestExperiments(publishedRuntime.config.experiments)');
|
|
19
|
+
expect(adapterSource).toContain(': funnelExperiments');
|
|
16
20
|
});
|
|
17
21
|
|
|
18
22
|
it('keeps A/B editor controls without wiring a manual payment mode switch', () => {
|
|
@@ -6,7 +6,6 @@ import {
|
|
|
6
6
|
type FunnelStepContractProjectSnapshot,
|
|
7
7
|
} from '@funnelsgrove/runtime';
|
|
8
8
|
import docsManifest from '../../../.funnelsgrove-docs.json';
|
|
9
|
-
import { manifestExperiments } from '@/config/experiments';
|
|
10
9
|
import { funnelManifest, rawFunnelManifest } from '@/config/funnel.manifest';
|
|
11
10
|
import { stepRegistry } from '@/runtime/step-registry';
|
|
12
11
|
import { contentOptionsByStepId } from '@/steps/step-content.registry';
|
|
@@ -30,7 +29,7 @@ describe('funnel project validation', () => {
|
|
|
30
29
|
registeredComponentKeys: Object.keys(stepRegistry),
|
|
31
30
|
contentOptionsByStepId,
|
|
32
31
|
edgesByStepId: rawFunnelManifest.edgesByStepId,
|
|
33
|
-
experiments:
|
|
32
|
+
experiments: rawFunnelManifest.experiments,
|
|
34
33
|
docs: {
|
|
35
34
|
schemaVersion: docsManifest.schemaVersion,
|
|
36
35
|
bundleVersion: docsManifest.bundleVersion,
|
|
@@ -210,9 +210,11 @@ describe('ClaimBee paywall source', () => {
|
|
|
210
210
|
);
|
|
211
211
|
|
|
212
212
|
expect(stepSource).toContain("from '@/config/billing.plans'");
|
|
213
|
-
expect(stepSource).toContain('
|
|
213
|
+
expect(stepSource).toContain('activeBillingDiscounts');
|
|
214
|
+
expect(stepSource).toContain('useFunnelRuntimeConfig');
|
|
215
|
+
expect(stepSource).toContain('resolveGeneratedOfferSetRuntime');
|
|
214
216
|
expect(stepSource).not.toContain("from '@/config/claimbee-paywall.config'");
|
|
215
|
-
expect(stepSource).toContain('
|
|
217
|
+
expect(stepSource).toContain('activeBillingDiscounts.second.couponId');
|
|
216
218
|
expect(stepSource).toContain('resolvePaywallDiscountState');
|
|
217
219
|
expect(stepSource).toContain('advancePaywallDiscountState');
|
|
218
220
|
expect(stepSource).toContain('serializePaywallDiscountState');
|
|
@@ -504,7 +506,8 @@ describe('ClaimBee paywall source', () => {
|
|
|
504
506
|
expect(stepSource).toContain('const availablePlans = useMemo(');
|
|
505
507
|
expect(stepSource).toContain('buildConfigPaywallPlans');
|
|
506
508
|
expect(stepSource).toContain("['primary', 'secondary', 'tertiary']");
|
|
507
|
-
expect(stepSource).toContain('[checkoutMode]');
|
|
509
|
+
expect(stepSource).toContain('[checkoutMode, publishedOfferSetRuntime]');
|
|
510
|
+
expect(stepSource).toContain('publishedOfferSetRuntime?.getPlanCatalog(checkoutMode)');
|
|
508
511
|
expect(stepSource).toContain("const defaultSelectedPlan = 'secondary'");
|
|
509
512
|
expect(stepSource).not.toContain('CLAIMBEE_PAYWALL_PLAN_ORDER');
|
|
510
513
|
expect(stepSource).not.toContain('getOrderedPaywallPlans');
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
"managedFiles": [
|
|
6
6
|
{
|
|
7
7
|
"path": "src/contract/funnel-project.validation.ts",
|
|
8
|
-
"sha256": "
|
|
8
|
+
"sha256": "99299cfb96414dd24272439cd1c57191a0863749c920a5eb03ff786f7972ab0e"
|
|
9
9
|
},
|
|
10
10
|
{
|
|
11
11
|
"path": "src/contract/funnel-validator.vite.config.ts",
|
|
@@ -6,7 +6,6 @@ import {
|
|
|
6
6
|
type FunnelStepMeta,
|
|
7
7
|
} from '@funnelsgrove/runtime';
|
|
8
8
|
import docsManifest from '../../.funnelsgrove-docs.json';
|
|
9
|
-
import { manifestExperiments } from '@/config/experiments';
|
|
10
9
|
import { rawFunnelManifest } from '@/config/funnel.manifest';
|
|
11
10
|
import { stepRegistry } from '@/runtime/step-registry';
|
|
12
11
|
import { contentOptionsByStepId } from '@/steps/step-content.registry';
|
|
@@ -24,7 +23,7 @@ export const buildFunnelProjectSnapshot = (): FunnelStepContractProjectSnapshot
|
|
|
24
23
|
registeredComponentKeys: Object.keys(stepRegistry),
|
|
25
24
|
contentOptionsByStepId,
|
|
26
25
|
edgesByStepId: rawFunnelManifest.edgesByStepId,
|
|
27
|
-
experiments:
|
|
26
|
+
experiments: rawFunnelManifest.experiments,
|
|
28
27
|
docs: {
|
|
29
28
|
schemaVersion: docsManifest.schemaVersion,
|
|
30
29
|
bundleVersion: docsManifest.bundleVersion,
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
defineFunnelExperiments,
|
|
3
|
-
toManifestExperiments,
|
|
4
|
-
} from '@funnelsgrove/runtime';
|
|
5
|
-
import { generatedExperiments } from './experiments.generated';
|
|
6
|
-
|
|
7
|
-
export const experiments = defineFunnelExperiments(generatedExperiments);
|
|
8
|
-
|
|
9
|
-
export const manifestExperiments = toManifestExperiments(experiments);
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest';
|
|
2
|
-
import { experiments, manifestExperiments } from '../../../src/config/experiments';
|
|
3
|
-
|
|
4
|
-
describe('funnel experiments config', () => {
|
|
5
|
-
it('ships the generic template without brand-specific experiments', () => {
|
|
6
|
-
expect(experiments).toEqual([]);
|
|
7
|
-
});
|
|
8
|
-
|
|
9
|
-
it('maps code-defined experiments into the runtime manifest shape', () => {
|
|
10
|
-
expect(manifestExperiments).toEqual([]);
|
|
11
|
-
});
|
|
12
|
-
});
|