@funnelsgrove/cli 0.1.68 → 0.1.73

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 (26) hide show
  1. package/funnel-contract-compatibility.json +58 -58
  2. package/package.json +4 -4
  3. package/template_docs/.funnelsgrove-docs.json +9 -9
  4. package/template_docs/docs/funnelsgrove/contracts/analytics-events.md +2 -1
  5. package/template_docs/docs/funnelsgrove/contracts/payments.md +6 -1
  6. package/template_docs/docs/funnelsgrove/migrations/step-contract-v3.md +1 -1
  7. package/template_docs/docs/funnelsgrove/qa/analytics.md +4 -0
  8. package/template_docs/docs/funnelsgrove/qa/paywall-checkout.md +3 -0
  9. package/template_docs/docs/funnelsgrove/steps/paywall_offer.md +4 -2
  10. package/template_docs/docs/funnelsgrove/steps/upsell_offer.md +3 -2
  11. package/template_docs/funnel-docs.config.json +1 -1
  12. package/template_scaffold/.funnelsgrove-docs.json +9 -9
  13. package/template_scaffold/.funnelsgrove-scaffold.json +16 -16
  14. package/template_scaffold/docs/funnelsgrove/contracts/analytics-events.md +2 -1
  15. package/template_scaffold/docs/funnelsgrove/contracts/payments.md +6 -1
  16. package/template_scaffold/docs/funnelsgrove/migrations/step-contract-v3.md +1 -1
  17. package/template_scaffold/docs/funnelsgrove/qa/analytics.md +4 -0
  18. package/template_scaffold/docs/funnelsgrove/qa/paywall-checkout.md +3 -0
  19. package/template_scaffold/docs/funnelsgrove/steps/paywall_offer.md +4 -2
  20. package/template_scaffold/docs/funnelsgrove/steps/upsell_offer.md +3 -2
  21. package/template_scaffold/funnel-docs.config.json +1 -1
  22. package/template_scaffold/package-lock.json +15 -15
  23. package/template_scaffold/package.json +3 -3
  24. package/template_scaffold/src/steps/step-32-paywall.tsx +89 -106
  25. package/template_scaffold/tests/funnel-agent-docs.test.ts +1 -1
  26. package/template_scaffold/tests/src/steps/step-32-paywall.test.ts +37 -51
@@ -14,7 +14,6 @@ import {
14
14
  } from '@/config/billing.plans';
15
15
  import { stepPaywallContent } from '@/steps/content/step-32-paywall.content';
16
16
  import { useClaimbeeStepContent } from '@/steps/use-claimbee-step-content';
17
- import { publicAnalyticsSdk } from '@funnelsgrove/analytics';
18
17
  import {
19
18
  PaymentRuntimeBoundary,
20
19
  apiService,
@@ -31,10 +30,9 @@ import {
31
30
  } from '@funnelsgrove/runtime';
32
31
  import { getPathForStep } from '@/runtime/funnel-runtime';
33
32
  import {
34
- ApplePaySubscriptionCheckoutSlot,
35
- GooglePaySubscriptionCheckoutSlot,
36
33
  SharedCheckoutSpecialOfferDialog,
37
34
  SharedStripeCheckoutV2Dialog,
35
+ StripeSubscriptionWalletSurface,
38
36
  activateSecondPaywallDiscount,
39
37
  advancePaywallDiscountState,
40
38
  buildBillingDiscountCatalog,
@@ -45,12 +43,13 @@ import {
45
43
  getPaywallPromoDisplayName,
46
44
  resolvePaywallDiscountState,
47
45
  serializePaywallDiscountState,
48
- usePlatformWalletPaymentMethods,
49
46
  useStripeSubscriptionCheckoutSession,
50
47
  type BillingFallbackPlanConfig,
51
48
  type BillingPlanCatalog,
52
49
  type PaywallDiscountState,
53
50
  type PlatformWalletPaymentMethod,
51
+ type StripeSubscriptionWalletSurfaceAvailability,
52
+ type StripeSubscriptionWalletSurfaceCheckoutInput,
54
53
  } from '@funnelsgrove/payments';
55
54
  import { stepSubscriptionStartedId } from '@/steps/step-33-subscription-started';
56
55
  import { funnelManifest } from '@/config/funnel.manifest';
@@ -76,9 +75,18 @@ type StoryOption = {
76
75
  backgroundColor: string;
77
76
  };
78
77
 
79
- type PaywallWalletAvailabilityState = {
80
- intentKey: string;
81
- methods: Partial<Record<PlatformWalletPaymentMethod, boolean>>;
78
+ const PAYWALL_WALLET_SURFACE_ID = 'paywall-primary';
79
+
80
+ type PaywallWalletAvailabilityState = Partial<Record<string, boolean>>;
81
+
82
+ const paywallWalletMethodClassNames: Record<PlatformWalletPaymentMethod, string> = {
83
+ applePay: 'paywall-v2-apple-pay-button',
84
+ googlePay: 'paywall-v2-google-pay-button',
85
+ };
86
+
87
+ const paywallWalletMethodWrapperClassNames: Record<PlatformWalletPaymentMethod, string> = {
88
+ applePay: 'paywall-v2-apple-pay-shell',
89
+ googlePay: 'paywall-v2-google-pay-shell',
82
90
  };
83
91
 
84
92
  const claimbeeStoryBackgroundColors = ['var(--color-secondary-text)', 'var(--color-primary)', 'var(--color-secondary)'] as const;
@@ -141,7 +149,6 @@ function StepPaywallRuntime({
141
149
  const publishedRuntime = useFunnelRuntimeConfig();
142
150
  const content = useClaimbeeStepContent(stepPaywallId, stepPaywallContent);
143
151
  const supportEmail = runtimePublicConfig.supportEmail;
144
- const walletPaymentMethods = usePlatformWalletPaymentMethods();
145
152
  const publishedOfferSetRuntime = useMemo(
146
153
  () => publishedRuntime.config && publishedRuntime.config.offerSets.length > 0
147
154
  ? resolveGeneratedOfferSetRuntime<BillingFallbackPlanConfig>({
@@ -208,10 +215,8 @@ function StepPaywallRuntime({
208
215
  const [activeStoryIndex, setActiveStoryIndex] = useState(0);
209
216
  const [openFaqIds, setOpenFaqIds] = useState<string[]>([]);
210
217
  const [checkoutEmail, setCheckoutEmail] = useState(user.email);
211
- const [paywallWalletAvailability, setPaywallWalletAvailability] = useState<PaywallWalletAvailabilityState>({
212
- intentKey: '',
213
- methods: {},
214
- });
218
+ const [paywallWalletAvailability, setPaywallWalletAvailability] =
219
+ useState<PaywallWalletAvailabilityState>({});
215
220
  const paywallStepRef = useRef<HTMLDivElement | null>(null);
216
221
 
217
222
  const activeCouponId = discountState.couponId;
@@ -363,21 +368,40 @@ function StepPaywallRuntime({
363
368
  runtimeConfig: checkoutRuntimeConfig,
364
369
  userId: user.id,
365
370
  });
371
+ const walletCheckoutInput = useMemo<StripeSubscriptionWalletSurfaceCheckoutInput>(
372
+ () => ({
373
+ analyticsMetadata: checkoutAnalyticsMetadata,
374
+ checkoutMode,
375
+ couponId: checkoutPromoActive ? selectedDisplayPlan?.couponId ?? null : null,
376
+ customerEmail: checkoutEmail || user.email,
377
+ customerName: user.name,
378
+ displayPlan: selectedDisplayPlan,
379
+ plan: selectedPlan,
380
+ returnUrl: successReturnUrl,
381
+ runtimeConfig: checkoutRuntimeConfig,
382
+ userId: user.id,
383
+ }),
384
+ [
385
+ checkoutAnalyticsMetadata,
386
+ checkoutEmail,
387
+ checkoutMode,
388
+ checkoutPromoActive,
389
+ selectedDisplayPlan,
390
+ selectedPlan,
391
+ successReturnUrl,
392
+ user.email,
393
+ user.id,
394
+ user.name,
395
+ ],
396
+ );
366
397
  const checkoutDisabled = !checkoutSession.configured || !selectedPlan || !selectedDisplayPlan;
367
398
  const checkoutDialogReady =
368
399
  checkoutDialogOpen &&
369
400
  Boolean(checkoutSession.activeClientSecret) &&
370
401
  Boolean(checkoutSession.stripePromise) &&
371
402
  Boolean(selectedDisplayPlan);
372
- const currentPaywallWalletAvailability: PaywallWalletAvailabilityState['methods'] =
373
- paywallWalletAvailability.intentKey === checkoutSession.intentKey
374
- ? paywallWalletAvailability.methods
375
- : {};
376
- const checkoutInitialWalletAvailable = walletPaymentMethods.some(
377
- (method) => currentPaywallWalletAvailability[method] === true,
378
- )
379
- ? true
380
- : null;
403
+ const checkoutInitialWalletAvailable =
404
+ paywallWalletAvailability[PAYWALL_WALLET_SURFACE_ID] ?? null;
381
405
  const checkoutTotalValue = useMemo(() => {
382
406
  if (!selectedDisplayPlan) {
383
407
  return '';
@@ -468,31 +492,22 @@ function StepPaywallRuntime({
468
492
  );
469
493
  }, [discountStateReady, serializedDiscountSnapshot]);
470
494
 
471
- const updatePaywallWalletAvailability = (
472
- method: PlatformWalletPaymentMethod,
473
- available: boolean,
495
+ const handleWalletSurfaceAvailabilityChange = (
496
+ surfaceId: string,
497
+ availability: StripeSubscriptionWalletSurfaceAvailability,
474
498
  ) => {
475
- setPaywallWalletAvailability((current) => {
476
- const currentMethods =
477
- current.intentKey === checkoutSession.intentKey
478
- ? current.methods
479
- : {};
480
-
481
- return {
482
- intentKey: checkoutSession.intentKey,
483
- methods: {
484
- ...currentMethods,
485
- [method]: available,
486
- },
487
- };
488
- });
499
+ setPaywallWalletAvailability((current) =>
500
+ current[surfaceId] === availability.anyAvailable
501
+ ? current
502
+ : { ...current, [surfaceId]: availability.anyAvailable },
503
+ );
489
504
  };
490
505
 
491
- const handleApplePayAvailabilityChange = (available: boolean) =>
492
- updatePaywallWalletAvailability('applePay', available);
506
+ const resolvePaywallWalletMethodClassName = (method: PlatformWalletPaymentMethod) =>
507
+ paywallWalletMethodClassNames[method];
493
508
 
494
- const handleGooglePayAvailabilityChange = (available: boolean) =>
495
- updatePaywallWalletAvailability('googlePay', available);
509
+ const resolvePaywallWalletMethodWrapperClassName = (method: PlatformWalletPaymentMethod) =>
510
+ paywallWalletMethodWrapperClassNames[method];
496
511
 
497
512
  const startCheckout = async () => {
498
513
  if (checkoutDisabled || checkoutSession.loading.card) {
@@ -510,29 +525,13 @@ function StepPaywallRuntime({
510
525
  }
511
526
  };
512
527
 
513
- const trackPaymentInfoSubmitted = () => {
514
- if (!selectedDisplayPlan || !checkoutAnalyticsMetadata) {
515
- return;
516
- }
517
-
518
- const checkoutSessionId = checkoutSession.checkoutSessionId?.trim();
519
- if (!checkoutSessionId) {
528
+ const handleCheckoutCompleted = () => {
529
+ if (!selectedDisplayPlan) {
520
530
  return;
521
531
  }
522
-
523
532
  completeStep(analyticsStepId, {
524
533
  planId: selectedDisplayPlan.id,
525
534
  });
526
- publicAnalyticsSdk.trackPaymentInfoSubmitted({
527
- eventId: `add_payment_info:${checkoutSessionId}`,
528
- stepId: analyticsStepId,
529
- stepName: analyticsStepName,
530
- stepType: stepPaywall.type,
531
- stepContractVersion: funnelManifest.stepContractVersion,
532
- featureFlags,
533
- metadata: checkoutAnalyticsMetadata,
534
- });
535
- void publicAnalyticsSdk.flush().catch(() => 0);
536
535
  };
537
536
 
538
537
  const preparePaywallWalletCheckout = async (): Promise<string | null> => {
@@ -676,7 +675,7 @@ function StepPaywallRuntime({
676
675
  onCustomerEmailChange={setCheckoutEmail}
677
676
  onCustomerEmailCommit={commitCheckoutEmail}
678
677
  onError={checkoutSession.setError}
679
- onPaymentInfoSubmitted={trackPaymentInfoSubmitted}
678
+ onSuccess={handleCheckoutCompleted}
680
679
  paymentMethodLabels={content.checkout.paymentMethodLabels}
681
680
  paypalButtonLabel={content.checkout.paypalButtonLabel}
682
681
  paypalLabel={content.checkout.paypalLabel}
@@ -884,51 +883,28 @@ function StepPaywallRuntime({
884
883
  </section>
885
884
 
886
885
  <section className='paywall-v2-payment-stack'>
887
- {walletPaymentMethods.includes('applePay') ? (
888
- <div className='paywall-v2-apple-pay-shell'>
889
- {selectedDisplayPlan ? (
890
- <ApplePaySubscriptionCheckoutSlot
891
- amountCents={selectedDisplayPlan.amountCents}
892
- beforeConfirm={preparePaywallWalletCheckout}
893
- checkoutAnalytics={checkoutAnalyticsContext}
894
- className='paywall-v2-apple-pay-button'
895
- customerEmail={checkoutEmail || user.email}
896
- customerName={user.name}
897
- disabled={checkoutDisabled}
898
- onAvailabilityChange={handleApplePayAvailabilityChange}
899
- onPaymentInfoSubmitted={trackPaymentInfoSubmitted}
900
- onUnavailableClick={openCardCheckoutFromUnavailableWallet}
901
- placeholderLabel={content.checkout.walletPlaceholderLabel}
902
- returnUrl={successReturnUrl}
903
- session={checkoutSession}
904
- summaryLabel={checkoutSummaryLabel}
905
- suspended={checkoutDialogReady || specialOfferDialogOpen}
906
- />
907
- ) : null}
908
- </div>
909
- ) : null}
910
- {walletPaymentMethods.includes('googlePay') ? (
911
- <div className='paywall-v2-google-pay-shell'>
912
- {selectedDisplayPlan ? (
913
- <GooglePaySubscriptionCheckoutSlot
914
- amountCents={selectedDisplayPlan.amountCents}
915
- beforeConfirm={preparePaywallWalletCheckout}
916
- checkoutAnalytics={checkoutAnalyticsContext}
917
- className='paywall-v2-google-pay-button'
918
- customerEmail={checkoutEmail || user.email}
919
- customerName={user.name}
920
- disabled={checkoutDisabled}
921
- onAvailabilityChange={handleGooglePayAvailabilityChange}
922
- onPaymentInfoSubmitted={trackPaymentInfoSubmitted}
923
- onUnavailableClick={openCardCheckoutFromUnavailableWallet}
924
- placeholderLabel={content.checkout.googlePayPlaceholderLabel}
925
- returnUrl={successReturnUrl}
926
- session={checkoutSession}
927
- summaryLabel={checkoutSummaryLabel}
928
- suspended={checkoutDialogReady || specialOfferDialogOpen}
929
- />
930
- ) : null}
931
- </div>
886
+ {selectedDisplayPlan ? (
887
+ <StripeSubscriptionWalletSurface
888
+ amountCents={selectedDisplayPlan.amountCents}
889
+ applePayPlaceholderLabel={content.checkout.walletPlaceholderLabel}
890
+ beforeConfirm={preparePaywallWalletCheckout}
891
+ checkout={walletCheckoutInput}
892
+ checkoutAnalytics={checkoutAnalyticsContext}
893
+ containerClassName='paywall-v2-wallet-surface'
894
+ disabled={checkoutDisabled}
895
+ googlePayPlaceholderLabel={content.checkout.googlePayPlaceholderLabel}
896
+ onAvailabilityChange={(availability) =>
897
+ handleWalletSurfaceAvailabilityChange(PAYWALL_WALLET_SURFACE_ID, availability)
898
+ }
899
+ onError={setError}
900
+ onSuccess={handleCheckoutCompleted}
901
+ onUnavailableClick={openCardCheckoutFromUnavailableWallet}
902
+ resolveMethodClassName={resolvePaywallWalletMethodClassName}
903
+ resolveMethodWrapperClassName={resolvePaywallWalletMethodWrapperClassName}
904
+ summaryLabel={checkoutSummaryLabel}
905
+ surfaceId={PAYWALL_WALLET_SURFACE_ID}
906
+ suspended={checkoutDialogReady || specialOfferDialogOpen}
907
+ />
932
908
  ) : null}
933
909
  <button
934
910
  type='button'
@@ -1721,6 +1697,13 @@ const stepPaywallStyles = `
1721
1697
  width: 100%;
1722
1698
  }
1723
1699
 
1700
+ .paywall-v2-wallet-surface {
1701
+ display: flex;
1702
+ flex-direction: column;
1703
+ gap: 12px;
1704
+ width: 100%;
1705
+ }
1706
+
1724
1707
  .paywall-v2-card-button {
1725
1708
  width: 100%;
1726
1709
  min-height: 64px;
@@ -363,7 +363,7 @@ describe('funnel agent documentation supply', () => {
363
363
 
364
364
  expect(manifest).toMatchObject({
365
365
  schemaVersion: 1,
366
- bundleVersion: '2.0.57',
366
+ bundleVersion: '2.0.62',
367
367
  stepContractVersion: contract.stepContractVersion,
368
368
  contractHash: contract.contractHash,
369
369
  });
@@ -43,8 +43,7 @@ describe('ClaimBee paywall source', () => {
43
43
  );
44
44
 
45
45
  expect(stepSource).toContain('SharedCheckoutSpecialOfferDialog');
46
- expect(stepSource).toContain('ApplePaySubscriptionCheckoutSlot');
47
- expect(stepSource).toContain('GooglePaySubscriptionCheckoutSlot');
46
+ expect(stepSource).toContain('StripeSubscriptionWalletSurface');
48
47
  expect(stepSource).toContain('SharedStripeCheckoutV2Dialog');
49
48
  expect(stepSource).not.toContain('StripeExpressCheckoutElement');
50
49
  expect(stepSource).not.toContain('createStripeSubscriptionCheckout');
@@ -67,8 +66,8 @@ describe('ClaimBee paywall source', () => {
67
66
  expect(stepSource).toContain('imageSrc={content.checkout.specialOffer.image.src}');
68
67
  expect(stepSource).toContain("className='paywall-v2-scroll-cta paywall-v2-vibe-cta'");
69
68
  expect(stepSource).toContain("className='paywall-v2-card-button paywall-v2-vibe-cta'");
70
- expect(stepSource).toContain("className='paywall-v2-apple-pay-button'");
71
- expect(stepSource).toContain("className='paywall-v2-google-pay-button'");
69
+ expect(stepSource).toContain("applePay: 'paywall-v2-apple-pay-button'");
70
+ expect(stepSource).toContain("googlePay: 'paywall-v2-google-pay-button'");
72
71
  expect(stepSource).not.toContain('Continue with other payment options');
73
72
  });
74
73
 
@@ -172,7 +171,7 @@ describe('ClaimBee paywall source', () => {
172
171
  expect(stepSource).toContain('useStripeSubscriptionCheckoutSession');
173
172
  expect(stepSource).toContain('checkoutSession.activeClientSecret');
174
173
  expect(stepSource).toContain('<SharedStripeCheckoutV2Dialog');
175
- expect(stepSource).toContain('<ApplePaySubscriptionCheckoutSlot');
174
+ expect(stepSource).toContain('<StripeSubscriptionWalletSurface');
176
175
  expect(stepSource).not.toContain('<StripeExpressCheckoutElement');
177
176
  expect(stepSource).not.toContain('createStripePaymentIntent');
178
177
  expect(stepSource).not.toContain('createStripeSubscriptionCheckout');
@@ -183,23 +182,25 @@ describe('ClaimBee paywall source', () => {
183
182
  expect(stepSource).toContain("aria-label='Loading checkout'");
184
183
  });
185
184
 
186
- it('claims paywall completion only when payment information is submitted', () => {
185
+ it('claims paywall completion only after verified checkout success', () => {
187
186
  const stepSource = readFileSync(
188
187
  path.resolve(__dirname, '../../../src/steps/step-32-paywall.tsx'),
189
188
  'utf8',
190
189
  );
191
- const paymentInfoHandler = stepSource.slice(
192
- stepSource.indexOf('const trackPaymentInfoSubmitted = () => {'),
190
+ const completionHandler = stepSource.slice(
191
+ stepSource.indexOf('const handleCheckoutCompleted = () => {'),
193
192
  stepSource.indexOf('const preparePaywallWalletCheckout'),
194
193
  );
195
194
  const checkoutStartHandler = stepSource.slice(
196
195
  stepSource.indexOf('const startCheckout = async () => {'),
197
- stepSource.indexOf('const trackPaymentInfoSubmitted'),
196
+ stepSource.indexOf('const handleCheckoutCompleted'),
198
197
  );
199
198
 
200
199
  expect(stepSource).toContain('const { user, setUser, attributes, featureFlags, completeStep } = useFunnel();');
201
- expect(paymentInfoHandler).toContain('completeStep(analyticsStepId, {');
202
- expect(paymentInfoHandler).toContain('planId: selectedDisplayPlan.id');
200
+ expect(completionHandler).toContain('completeStep(analyticsStepId, {');
201
+ expect(completionHandler).toContain('planId: selectedDisplayPlan.id');
202
+ expect(stepSource).toContain('onSuccess={handleCheckoutCompleted}');
203
+ expect(stepSource).not.toContain('onPaymentInfoSubmitted=');
203
204
  expect(checkoutStartHandler).not.toContain('completeStep(');
204
205
  });
205
206
 
@@ -315,8 +316,7 @@ describe('ClaimBee paywall source', () => {
315
316
  expect(plansIndex).toBeGreaterThan(-1);
316
317
  expect(paymentStackIndex).toBeGreaterThan(plansIndex);
317
318
  expect(featureGridIndex).toBeGreaterThan(paymentStackIndex);
318
- expect(stepSource).toContain('ApplePaySubscriptionCheckoutSlot');
319
- expect(stepSource).toContain('GooglePaySubscriptionCheckoutSlot');
319
+ expect(stepSource).toContain('StripeSubscriptionWalletSurface');
320
320
  expect(stepSource).toContain('.paywall-v2-apple-pay-button,');
321
321
  expect(stepSource).toContain('.paywall-v2-google-pay-button {');
322
322
  expect(stepSource).toContain('min-height: 64px;');
@@ -352,12 +352,13 @@ describe('ClaimBee paywall source', () => {
352
352
  'utf8',
353
353
  );
354
354
 
355
- expect(stepSource).toContain("<div className='paywall-v2-apple-pay-shell'>");
356
- expect(stepSource).toContain("<div className='paywall-v2-google-pay-shell'>");
357
- expect(stepSource).toContain('<ApplePaySubscriptionCheckoutSlot');
358
- expect(stepSource).toContain('<GooglePaySubscriptionCheckoutSlot');
355
+ expect(stepSource).toContain('<StripeSubscriptionWalletSurface');
356
+ expect(stepSource).toContain("surfaceId={PAYWALL_WALLET_SURFACE_ID}");
357
+ expect(stepSource).toContain('checkout={walletCheckoutInput}');
358
+ expect(stepSource).toContain('resolveMethodClassName={resolvePaywallWalletMethodClassName}');
359
+ expect(stepSource).toContain('resolveMethodWrapperClassName={resolvePaywallWalletMethodWrapperClassName}');
359
360
  expect(stepSource).toContain('onUnavailableClick={openCardCheckoutFromUnavailableWallet}');
360
- expect(stepSource).toContain('session={checkoutSession}');
361
+ expect(stepSource).not.toContain('session={checkoutSession}');
361
362
  expect(stepSource).not.toContain('onClick={() => void startWalletCheckout()}');
362
363
  expect(stepSource).not.toContain('StripeExpressCheckoutElement');
363
364
  expect(stepSource).not.toContain('createStripeSubscriptionCheckout');
@@ -372,14 +373,13 @@ describe('ClaimBee paywall source', () => {
372
373
  'utf8',
373
374
  );
374
375
 
375
- expect(stepSource).toContain('type PaywallWalletAvailabilityState = {');
376
- expect(stepSource).toContain('const [paywallWalletAvailability, setPaywallWalletAvailability] = useState<PaywallWalletAvailabilityState>');
377
- expect(stepSource).toContain("updatePaywallWalletAvailability('applePay', available)");
378
- expect(stepSource).toContain("updatePaywallWalletAvailability('googlePay', available)");
379
- expect(stepSource).toContain('const checkoutInitialWalletAvailable = walletPaymentMethods.some(');
376
+ expect(stepSource).toContain('type PaywallWalletAvailabilityState = Partial<Record<string, boolean>>;');
377
+ expect(stepSource).toContain('useState<PaywallWalletAvailabilityState>({});');
378
+ expect(stepSource).toContain('const handleWalletSurfaceAvailabilityChange = (');
379
+ expect(stepSource).toContain('availability: StripeSubscriptionWalletSurfaceAvailability');
380
+ expect(stepSource).toContain('paywallWalletAvailability[PAYWALL_WALLET_SURFACE_ID] ?? null');
380
381
  expect(stepSource).toContain('initialWalletAvailable={checkoutInitialWalletAvailable}');
381
- expect(stepSource).toContain('onAvailabilityChange={handleApplePayAvailabilityChange}');
382
- expect(stepSource).toContain('onAvailabilityChange={handleGooglePayAvailabilityChange}');
382
+ expect(stepSource).toContain('handleWalletSurfaceAvailabilityChange(PAYWALL_WALLET_SURFACE_ID, availability)');
383
383
  });
384
384
 
385
385
  it('prepares the real Apple Pay element on load instead of waiting for the placeholder tap', () => {
@@ -389,7 +389,7 @@ describe('ClaimBee paywall source', () => {
389
389
  );
390
390
 
391
391
  expect(stepSource).toContain('useStripeSubscriptionCheckoutSession');
392
- expect(stepSource).toContain('ApplePaySubscriptionCheckoutSlot');
392
+ expect(stepSource).toContain('StripeSubscriptionWalletSurface');
393
393
  expect(stepSource).toContain('checkoutSession.prepareCardCheckout');
394
394
  expect(stepSource).toContain('checkoutSession.activeClientSecret');
395
395
  expect(stepSource).not.toContain('const autoWalletIntentKeyRef = useRef<string | null>(null);');
@@ -410,8 +410,8 @@ describe('ClaimBee paywall source', () => {
410
410
  expect(stepSource).toContain('couponId: checkoutPromoActive ? selectedDisplayPlan?.couponId ?? null : null');
411
411
  expect(stepSource).toContain('summaryLabel: checkoutSummaryLabel');
412
412
  expect(stepSource).toContain('checkoutSession.activeClientSecret');
413
- expect(stepSource).toContain('<ApplePaySubscriptionCheckoutSlot');
414
- expect(stepSource).toContain('<GooglePaySubscriptionCheckoutSlot');
413
+ expect(stepSource).toContain('<StripeSubscriptionWalletSurface');
414
+ expect(stepSource).toContain('const walletCheckoutInput = useMemo<StripeSubscriptionWalletSurfaceCheckoutInput>');
415
415
  expect(stepSource).not.toContain('StripeExpressCheckoutElement');
416
416
  expect(stepSource).not.toContain('preparePaywallWalletSubscription');
417
417
  expect(stepSource).not.toContain('const handlePaywallApplePayPlaceholderClick = () => {');
@@ -424,9 +424,8 @@ describe('ClaimBee paywall source', () => {
424
424
  'utf8',
425
425
  );
426
426
 
427
- expect(stepSource).toContain('const trackPaymentInfoSubmitted = () => {');
427
+ expect(stepSource).not.toContain('publicAnalyticsSdk');
428
428
  expect(stepSource).not.toContain('publicAnalyticsSdk.trackCheckoutStarted({');
429
- expect(stepSource).toContain('publicAnalyticsSdk.trackPaymentInfoSubmitted({');
430
429
  expect(stepSource).toContain('stepType: stepPaywall.type');
431
430
  expect(stepSource).toContain('value: selectedDisplayPlan.analyticsPurchaseValue');
432
431
  expect(stepSource).toContain("currency: 'USD'");
@@ -434,16 +433,16 @@ describe('ClaimBee paywall source', () => {
434
433
  expect(stepSource).toContain('const checkoutAnalyticsContext = useMemo(');
435
434
  expect(stepSource).toContain('checkoutAnalytics={checkoutAnalyticsContext}');
436
435
  expect(stepSource).toContain('checkoutSessionId={checkoutSession.checkoutSessionId}');
437
- expect(stepSource).toContain('onPaymentInfoSubmitted={trackPaymentInfoSubmitted}');
436
+ expect(stepSource).not.toContain('onPaymentInfoSubmitted=');
438
437
  expect(stepSource).toContain("await checkoutSession.prepareCardCheckout({ checkoutStartSource: 'checkout_render' });");
439
- expect(stepSource).not.toContain('const trackCheckoutCompleted = () => {');
438
+ expect(stepSource).toContain('const handleCheckoutCompleted = () => {');
440
439
  expect(stepSource).not.toContain('publicAnalyticsSdk.trackCheckoutCompleted({');
441
- expect(stepSource).not.toContain('onSuccess={trackCheckoutCompleted}');
440
+ expect(stepSource).toContain('onSuccess={handleCheckoutCompleted}');
442
441
  expect(stepSource).not.toContain("eventType: 'checkout_started'");
443
442
  expect(stepSource).not.toContain("eventType: 'checkout_completed'");
444
443
  });
445
444
 
446
- it('emits payment-info analytics only after a stable Checkout Session exists', () => {
445
+ it('delegates payment-info analytics to shared checkout components with session context', () => {
447
446
  const stepSource = readFileSync(
448
447
  path.resolve(__dirname, '../../../src/steps/step-32-paywall.tsx'),
449
448
  'utf8',
@@ -456,25 +455,12 @@ describe('ClaimBee paywall source', () => {
456
455
  stepSource.indexOf('const checkoutAnalyticsContext = useMemo(() => {'),
457
456
  stepSource.indexOf('const successReturnUrl = useMemo(() => {'),
458
457
  );
459
- const paymentInfoHandler = stepSource.slice(
460
- stepSource.indexOf('const trackPaymentInfoSubmitted = () => {'),
461
- stepSource.indexOf('const preparePaywallWalletCheckout'),
462
- );
463
-
464
458
  expect(analyticsMetadata).toContain("currency: 'USD'");
465
459
  expect(checkoutAnalyticsContext).toContain('metadata: checkoutAnalyticsMetadata');
466
- expect(paymentInfoHandler).toContain(
467
- 'const checkoutSessionId = checkoutSession.checkoutSessionId?.trim();',
468
- );
469
- expect(paymentInfoHandler).toContain('if (!checkoutSessionId) {');
470
- expect(paymentInfoHandler.indexOf('if (!checkoutSessionId) {')).toBeLessThan(
471
- paymentInfoHandler.indexOf('completeStep(analyticsStepId, {'),
472
- );
473
- expect(paymentInfoHandler).toContain(
474
- 'eventId: `add_payment_info:${checkoutSessionId}`',
475
- );
476
- expect(paymentInfoHandler).toContain('metadata: checkoutAnalyticsMetadata');
477
- expect(paymentInfoHandler).toContain('void publicAnalyticsSdk.flush().catch(() => 0);');
460
+ expect(stepSource.match(/checkoutAnalytics=\{checkoutAnalyticsContext\}/g)).toHaveLength(2);
461
+ expect(stepSource).toContain('checkoutSessionId={checkoutSession.checkoutSessionId}');
462
+ expect(stepSource).not.toContain('trackPaymentInfoSubmitted');
463
+ expect(stepSource).not.toContain('add_payment_info:');
478
464
  });
479
465
 
480
466
  it('uses a local three-plan catalog so the paywall shows weekly, monthly, and yearly offers', () => {