@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.
Files changed (33) hide show
  1. package/dist/cli.d.ts +1 -1
  2. package/dist/cli.js +24 -2
  3. package/dist/experimentCreate.d.ts +3 -8
  4. package/dist/experimentCreate.js +19 -18
  5. package/funnel-contract-compatibility.json +35 -35
  6. package/package.json +4 -4
  7. package/template_docs/.funnelsgrove-docs.json +4 -4
  8. package/template_docs/docs/funnelsgrove/migrations/step-contract-v3.md +1 -1
  9. package/template_docs/docs/funnelsgrove/recipes/add-experiment.md +11 -10
  10. package/template_docs/funnel-docs.config.json +1 -1
  11. package/template_scaffold/.funnelsgrove-contract-tools.json +1 -1
  12. package/template_scaffold/.funnelsgrove-docs.json +4 -4
  13. package/template_scaffold/.funnelsgrove-scaffold.json +19 -34
  14. package/template_scaffold/docs/funnelsgrove/migrations/step-contract-v3.md +1 -1
  15. package/template_scaffold/docs/funnelsgrove/recipes/add-experiment.md +11 -10
  16. package/template_scaffold/funnel-docs.config.json +1 -1
  17. package/template_scaffold/package-lock.json +14 -14
  18. package/template_scaffold/package.json +3 -3
  19. package/template_scaffold/src/components/FunnelFlow.tsx +26 -0
  20. package/template_scaffold/src/config/funnel.manifest.ts +2 -2
  21. package/template_scaffold/src/contract/funnel-project.validation.ts +1 -2
  22. package/template_scaffold/src/runtime/funnel-runtime.ts +2 -10
  23. package/template_scaffold/src/runtime/use-funnel-flow-controller.ts +15 -1
  24. package/template_scaffold/src/steps/step-32-paywall.tsx +47 -15
  25. package/template_scaffold/tests/funnel-agent-docs.test.ts +5 -4
  26. package/template_scaffold/tests/src/components/FunnelFlow.test.tsx +4 -0
  27. package/template_scaffold/tests/src/contract/funnel-project.validation.test.ts +1 -2
  28. package/template_scaffold/tests/src/steps/step-32-paywall.test.ts +6 -3
  29. package/template_validation/.funnelsgrove-contract-tools.json +1 -1
  30. package/template_validation/src/contract/funnel-project.validation.ts +1 -2
  31. package/template_scaffold/src/config/experiments.generated.ts +0 -4
  32. package/template_scaffold/src/config/experiments.ts +0 -9
  33. 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
- billingDiscounts,
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
- buildConfigPaywallPlans(
142
- checkoutMode === 'test' ? claimbeePlansByMode.test : claimbeePlansByMode.live,
143
- ['primary', 'secondary', 'tertiary'],
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: billingDiscounts,
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 === CLAIMBEE_PAYWALL_SECOND_COUPON_ID;
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: billingDiscounts,
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: billingDiscounts,
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: billingDiscounts,
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
- '`variantKey` is at most 120 characters. `id`, `name`, `stepId`, `label`, `routeToStepId`, and `offerSetKey` are each at most 200 characters after trimming.',
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('immediately visible in the FunnelsGrove UI');
261
- expect(content).toContain('src/config/experiments.generated.ts');
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.41',
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: manifestExperiments,
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('billingDiscounts');
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('CLAIMBEE_PAYWALL_SECOND_COUPON_ID');
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": "b0ec02eac1bd56812eef773be71628ae4e94f80fcd88837bbc5c8bf04e73a62c"
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: manifestExperiments,
26
+ experiments: rawFunnelManifest.experiments,
28
27
  docs: {
29
28
  schemaVersion: docsManifest.schemaVersion,
30
29
  bundleVersion: docsManifest.bundleVersion,
@@ -1,4 +0,0 @@
1
- // Generated by FunnelsGrove. Do not edit by hand.
2
-
3
- export const generatedExperiments = [
4
- ] as const;
@@ -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
- });