'use client'; import { useCallback, useEffect, useMemo, useRef, useState, type FormEvent } from 'react'; import Image from 'next/image'; import { localizePaywallPlanLabels } from '@/components/paywall/localize-paywall-plan-labels'; import { PaywallRenewalDisclaimer } from '@/components/paywall/PaywallRenewalDisclaimer'; import { checkoutRuntimeConfig } from '@/runtime/checkout-runtime-config'; import { billingDiscountStorageKeys, billingDiscountList, starterPlansByMode, starterBillingRuntimeFallback, } from '@/config/billing.plans'; import { stepPaywallContent } from '@/steps/content/step-32-paywall.content'; import { PaymentRuntimeBoundary, apiService, collectCurrentFunnelAttribution, readPaywallStateValue, runtimePublicConfig, updatePaywallStateValue, useFunnel, useFunnelRuntimeConfig, usePreviewStepLocalizedContent, useRuntimeMode, type FunnelStepMeta, } from '@funnelsgrove/runtime'; import { getPathForStep } from '@/runtime/funnel-runtime'; import { SharedCheckoutSpecialOfferDialog, SolidgateCheckoutDialog, SharedStripeCheckoutV2Dialog, StripeSubscriptionWalletSurface, activateSecondPaywallDiscount, advancePaywallDiscountState, buildBillingDiscountCatalog, buildDiscountedPaywallPlans, getDefaultPlanId, getPaywallPromoDisplayName, resolvePaywallDiscountState, resolvePublishedBillingRuntime, serializePaywallDiscountState, useStripeSubscriptionCheckoutSession, type PaywallDiscountState, type PlatformWalletPaymentMethod, type StripeSubscriptionWalletSurfaceAvailability, type StripeSubscriptionWalletSurfaceCheckoutInput, } from '@funnelsgrove/payments'; import { stepSubscriptionStartedId } from '@/steps/step-33-subscription-started'; import { funnelManifest } from '@/config/funnel.manifest'; import { getStepContentLocale } from '@/runtime/step-content-context'; export const stepPaywallId = 'paywall'; const moneyBackBadgeAsset = funnelManifest.assets.moneyBackBadge; export const stepPaywall = { id: stepPaywallId, name: 'paywall', type: 'paywall_offer', kind: 'paywall', figmaNodeId: '6992-6210', title: 'Paywall', description: 'Starter paywall with a local three-plan catalog and FAQ accordion.', actionBar: { hidden: true, }, } as const satisfies FunnelStepMeta; type StoryOption = { quote: string; author: string; backgroundColor: string; }; const PAYWALL_WALLET_SURFACE_ID = 'paywall-primary'; const STRIPE_INVALID_EMAIL_ERROR = 'Enter a valid email address to continue.'; type PaywallWalletAvailabilityState = Partial>; const paywallWalletMethodClassNames: Record = { applePay: 'paywall-v2-apple-pay-button', googlePay: 'paywall-v2-google-pay-button', }; const paywallWalletMethodWrapperClassNames: Record = { applePay: 'paywall-v2-apple-pay-shell', googlePay: 'paywall-v2-google-pay-shell', }; const storyBackgroundColors = ['var(--color-secondary-text)', 'var(--color-primary)', 'var(--color-secondary)'] as const; export const PAYWALL_DISCOUNT_STORAGE_KEYS = billingDiscountStorageKeys; const deferPaywallStateSync = (syncState: () => void): (() => void) => { const timeoutId = window.setTimeout(syncState, 0); return () => window.clearTimeout(timeoutId); }; const checkoutCurrencyFormatter = new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD', minimumFractionDigits: 2, maximumFractionDigits: 2, }); const inactiveBillingDiscounts = buildBillingDiscountCatalog([ { stage: 'first', couponId: '__inactive_first__', discountPercent: 0, durationSeconds: 0, previousDiscountPercent: null, }, { stage: 'second', couponId: '__inactive_second__', discountPercent: 0, durationSeconds: 0, previousDiscountPercent: 0, }, ]); function formatCheckoutAmount(amountCents: number): string { return checkoutCurrencyFormatter.format(amountCents / 100); } type StepPaywallProps = { analyticsStepId?: string; analyticsStepName?: string; }; export function StepPaywall(props: StepPaywallProps = {}) { return ( ); } function StepPaywallRuntime({ analyticsStepId = stepPaywallId, analyticsStepName = stepPaywall.name || stepPaywall.title, }: StepPaywallProps = {}) { const [runtimeMode] = useRuntimeMode(); const checkoutMode = runtimeMode; const { user, setUser, attributes, featureFlags, completeStep, userBootstrapped } = useFunnel(); const publishedRuntime = useFunnelRuntimeConfig(); const content = usePreviewStepLocalizedContent( stepPaywallId, stepPaywallContent, getStepContentLocale(attributes), ); const supportEmail = runtimePublicConfig.supportEmail; const paywallPreviewAsset = funnelManifest.assets.paywallPreview; const activeBillingRuntime = useMemo( () => resolvePublishedBillingRuntime({ config: publishedRuntime.config?.offerSets.length ? publishedRuntime.config : starterBillingRuntimeFallback, featureFlags, mode: checkoutMode, search: typeof window === 'undefined' ? '' : window.location.search, baseCatalogsByMode: starterPlansByMode, fallbackDiscounts: billingDiscountList, }), [checkoutMode, featureFlags, publishedRuntime.config], ); const activeCheckoutRuntimeConfig = useMemo(() => ({ ...checkoutRuntimeConfig, funnelVersionId: publishedRuntime.config?.sourceVersionId ?? null, paymentsApiVersion: activeBillingRuntime.paymentsApiVersion, }), [ activeBillingRuntime.paymentsApiVersion, checkoutRuntimeConfig, publishedRuntime.config?.sourceVersionId, ]); const activeBillingDiscounts = useMemo( () => activeBillingRuntime.discounts ?? ( activeBillingRuntime.provider === 'stripe' ? buildBillingDiscountCatalog(billingDiscountList) : inactiveBillingDiscounts ), [activeBillingRuntime.discounts, activeBillingRuntime.provider], ); const availablePlans = activeBillingRuntime.plans; const initialPlanId = useMemo( () => availablePlans.find((plan) => plan.id === activeBillingRuntime.defaultPlanKey)?.id ?? getDefaultPlanId(availablePlans) ?? availablePlans[0]?.id ?? null, [activeBillingRuntime.defaultPlanKey, availablePlans], ); const [selectedPlanId, setSelectedPlanId] = useState( initialPlanId, ); const [checkoutDialogOpen, setCheckoutDialogOpen] = useState(false); const [solidgateAttemptKey, setSolidgateAttemptKey] = useState(null); const [specialOfferDialogOpen, setSpecialOfferDialogOpen] = useState(() => { if (typeof window === 'undefined') { return false; } const searchParams = new URLSearchParams(window.location.search); return searchParams.get('checkout_offer') === 'first'; }); const [error, setError] = useState(null); const [discountStateReady, setDiscountStateReady] = useState(false); const [discountState, setDiscountState] = useState(() => resolvePaywallDiscountState({ discounts: activeBillingDiscounts, nowMs: Date.now(), storedValue: null, }), ); const [activeStoryIndex, setActiveStoryIndex] = useState(0); const [openFaqIds, setOpenFaqIds] = useState([]); const [checkoutEmail, setCheckoutEmail] = useState(user.email); const [checkoutEmailNeedsReview, setCheckoutEmailNeedsReview] = useState(false); const [checkoutEmailPromptOpen, setCheckoutEmailPromptOpen] = useState(false); const [checkoutEmailPromptDraft, setCheckoutEmailPromptDraft] = useState(''); const [checkoutEmailPromptError, setCheckoutEmailPromptError] = useState(null); const [paywallWalletAvailability, setPaywallWalletAvailability] = useState({}); const paywallStepRef = useRef(null); const activeCouponId = discountState.couponId; const displayPlans = useMemo( () => buildDiscountedPaywallPlans({ plans: availablePlans, couponId: activeCouponId, discountPercent: discountState.discountPercent, }).map((plan) => localizePaywallPlanLabels(plan, content.catalogLabels)), [activeCouponId, availablePlans, discountState.discountPercent, content.catalogLabels], ); const selectedPlan = useMemo(() => { if (availablePlans.length === 0) { return null; } if (!selectedPlanId) { return availablePlans.find((plan) => plan.id === initialPlanId) ?? availablePlans[0]; } return availablePlans.find((plan) => plan.id === selectedPlanId) ?? availablePlans[0]; }, [availablePlans, initialPlanId, selectedPlanId]); const selectedDisplayPlan = useMemo(() => { if (displayPlans.length === 0) { return null; } if (!selectedPlanId) { return displayPlans.find((plan) => plan.id === initialPlanId) ?? displayPlans[0] ?? null; } return displayPlans.find((plan) => plan.id === selectedPlanId) ?? displayPlans[0]; }, [displayPlans, initialPlanId, selectedPlanId]); const checkoutSummaryLabel = selectedDisplayPlan?.checkoutSummaryLabel?.trim() || selectedDisplayPlan?.title || content.selectedPlanLabel; const checkoutProvider = activeBillingRuntime.provider; const storyOptions = useMemo( () => content.stories.items.map((item, index) => ({ quote: item.quote, author: item.author, backgroundColor: storyBackgroundColors[index % storyBackgroundColors.length], })), [content.stories.items], ); const activeStory = storyOptions[activeStoryIndex] ?? storyOptions[0] ?? null; const minutes = Math.floor(discountState.remainingSeconds / 60); const seconds = discountState.remainingSeconds % 60; const upgradedDiscountActive = activeCouponId === activeBillingDiscounts.second.couponId; const activeDiscountStartedAtMs = discountState.stage === 'second' ? discountState.secondStartedAtMs : discountState.firstStartedAtMs; const serializedDiscountSnapshot = useMemo( () => serializePaywallDiscountState(discountState), [discountState], ); const promoDisplayName = useMemo(() => { if ( discountState.status !== 'active' || typeof activeDiscountStartedAtMs !== 'number' ) { return ''; } return getPaywallPromoDisplayName(activeDiscountStartedAtMs); }, [activeDiscountStartedAtMs, discountState.status]); const appliedDiscountAmountCents = useMemo(() => { if (!selectedDisplayPlan) { return 0; } return Math.max(0, selectedDisplayPlan.baseAmountCents - selectedDisplayPlan.amountCents); }, [selectedDisplayPlan]); const checkoutDiscountPercent = discountState.discountPercent ?? 0; const checkoutPromoActive = Boolean(selectedDisplayPlan?.hasActiveDiscount) && discountState.status === 'active' && checkoutDiscountPercent > 0 && appliedDiscountAmountCents > 0; const checkoutAnalyticsMetadata = useMemo(() => { if (!selectedDisplayPlan) { return null; } return { planId: selectedDisplayPlan.id, providerPlanId: selectedDisplayPlan.providerPlanId || null, couponId: checkoutPromoActive ? selectedDisplayPlan.couponId : null, summaryLabel: checkoutSummaryLabel, amountCents: selectedDisplayPlan.amountCents, currency: 'USD', ...(typeof selectedDisplayPlan.analyticsPurchaseValue === 'number' ? { value: selectedDisplayPlan.analyticsPurchaseValue } : {}), environment: checkoutMode, }; }, [checkoutMode, checkoutPromoActive, checkoutSummaryLabel, selectedDisplayPlan]); const checkoutAnalyticsContext = useMemo(() => { if (!checkoutAnalyticsMetadata) { return null; } return { environment: checkoutMode, featureFlags, metadata: checkoutAnalyticsMetadata, runtimeConfig: activeCheckoutRuntimeConfig, stepId: analyticsStepId, stepName: analyticsStepName, stepType: stepPaywall.type, stepContractVersion: funnelManifest.stepContractVersion, }; }, [ activeCheckoutRuntimeConfig, analyticsStepId, analyticsStepName, checkoutAnalyticsMetadata, checkoutMode, featureFlags, ]); const successReturnUrl = useMemo(() => { if (typeof window === 'undefined') { return ''; } const successUrl = new URL(`${window.location.origin}${getPathForStep(stepSubscriptionStartedId)}`); if (user.id) { successUrl.searchParams.set('user_id', user.id); } successUrl.searchParams.set( 'source', checkoutProvider === 'solidgate' ? 'solidgate' : 'stripe-elements', ); return successUrl.toString(); }, [checkoutProvider, user.id]); const checkoutPageUrl = useMemo( () => typeof window === 'undefined' ? '' : window.location.href, [], ); const openCheckoutEmailPrompt = useCallback((showInvalidError: boolean) => { setCheckoutEmailPromptDraft(checkoutEmail.trim() || user.email.trim()); setCheckoutEmailPromptError( showInvalidError ? content.checkout.customerEmailInvalidMessage : null, ); setCheckoutEmailPromptOpen(true); }, [checkoutEmail, content.checkout.customerEmailInvalidMessage, user.email]); const handleStripeCheckoutError = useCallback((message: string | null) => { setError(message); if (message !== STRIPE_INVALID_EMAIL_ERROR) { return; } setCheckoutEmailNeedsReview(true); openCheckoutEmailPrompt(true); }, [openCheckoutEmailPrompt]); const checkoutSession = useStripeSubscriptionCheckoutSession({ analyticsMetadata: checkoutAnalyticsMetadata, checkoutMode, couponId: checkoutPromoActive ? selectedDisplayPlan?.couponId ?? null : null, customerEmail: checkoutEmail || user.email, displayPlan: selectedDisplayPlan, onError: handleStripeCheckoutError, plan: checkoutProvider === 'stripe' ? selectedPlan : null, returnUrl: successReturnUrl, runtimeConfig: activeCheckoutRuntimeConfig, runtimeConfigRevisionId: activeBillingRuntime.revisionId, paymentProfileId: activeBillingRuntime.paymentProfileId, provider: checkoutProvider === 'stripe' ? 'stripe' : null, stripePublishableKey: activeBillingRuntime.stripePublishableKey, userId: user.id, }); const checkoutCardClientSecret = checkoutSession.activeClientSecret; const checkoutCardLoading = checkoutSession.loading.card; const prepareCardCheckout = checkoutSession.prepareCardCheckout; const walletCheckoutInput = useMemo( () => ({ analyticsMetadata: checkoutAnalyticsMetadata, checkoutMode, couponId: checkoutPromoActive ? selectedDisplayPlan?.couponId ?? null : null, customerEmail: checkoutEmail || user.email, customerName: user.name, displayPlan: selectedDisplayPlan, plan: selectedPlan, returnUrl: successReturnUrl, runtimeConfig: activeCheckoutRuntimeConfig, runtimeConfigRevisionId: activeBillingRuntime.revisionId, paymentProfileId: activeBillingRuntime.paymentProfileId, provider: checkoutProvider === 'stripe' ? 'stripe' : null, stripePublishableKey: activeBillingRuntime.stripePublishableKey, userId: user.id, }), [ checkoutAnalyticsMetadata, checkoutEmail, checkoutMode, checkoutPromoActive, activeBillingRuntime.stripePublishableKey, activeBillingRuntime.paymentProfileId, activeBillingRuntime.revisionId, activeCheckoutRuntimeConfig, checkoutProvider, selectedDisplayPlan, selectedPlan, successReturnUrl, user.email, user.id, user.name, ], ); const solidgateCheckoutConfigured = Boolean( user.id.trim() && activeBillingRuntime.offerSetId.trim() && selectedPlan?.funnelPlanKey?.trim(), ); const checkoutDisabled = !userBootstrapped || !selectedPlan || !selectedDisplayPlan || ( checkoutProvider === 'solidgate' ? !solidgateCheckoutConfigured : !checkoutSession.configured ); const checkoutDialogReady = checkoutProvider === 'stripe' && checkoutDialogOpen && Boolean(checkoutSession.activeClientSecret) && Boolean(checkoutSession.stripePromise) && Boolean(selectedDisplayPlan); const checkoutInitialWalletAvailable = paywallWalletAvailability[PAYWALL_WALLET_SURFACE_ID] ?? null; const checkoutTotalValue = useMemo(() => { if (!selectedDisplayPlan) { return ''; } return selectedDisplayPlan.priceLabel.split('/')[0]?.trim() || selectedDisplayPlan.priceLabel; }, [selectedDisplayPlan]); const checkoutDiscountLabel = checkoutPromoActive ? content.checkout.discountRowLabelTemplate.replace( '{percent}', String(checkoutDiscountPercent), ) : ''; const checkoutDiscountAmountLabel = checkoutPromoActive ? `-${formatCheckoutAmount(appliedDiscountAmountCents)}` : ''; const checkoutSavedLabel = checkoutPromoActive ? content.checkout.savedLabelTemplate .replace('{amount}', formatCheckoutAmount(appliedDiscountAmountCents)) .replace('{percent}', String(checkoutDiscountPercent)) : ''; useEffect(() => { return deferPaywallStateSync(() => { setDiscountState( resolvePaywallDiscountState({ discounts: activeBillingDiscounts, nowMs: Date.now(), storedValue: readPaywallStateValue({ legacyKeys: PAYWALL_DISCOUNT_STORAGE_KEYS, }), }), ); setDiscountStateReady(true); }); }, [activeBillingDiscounts]); useEffect(() => { return deferPaywallStateSync(() => { setCheckoutEmail(user.email); }); }, [user.email]); useEffect(() => { setCheckoutDialogOpen(false); setSolidgateAttemptKey(null); setError(null); }, [selectedPlanId]); useEffect(() => { if ( !discountStateReady || checkoutProvider !== 'stripe' || checkoutDisabled || checkoutEmailNeedsReview || checkoutCardClientSecret || checkoutCardLoading ) { return; } void prepareCardCheckout({ checkoutStartSource: 'checkout_render' }); }, [ checkoutDisabled, checkoutEmailNeedsReview, checkoutProvider, checkoutCardClientSecret, checkoutCardLoading, discountStateReady, prepareCardCheckout, ]); useEffect(() => { if (!discountStateReady) { return; } const timer = window.setInterval(() => { setDiscountState((current) => { const nextState = advancePaywallDiscountState({ discounts: activeBillingDiscounts, state: current, nowMs: Date.now(), }); return nextState.status === current.status && nextState.remainingSeconds === current.remainingSeconds && nextState.expiresAtMs === current.expiresAtMs ? current : nextState; }); }, 1000); return () => window.clearInterval(timer); }, [activeBillingDiscounts, discountStateReady]); useEffect(() => { if (storyOptions.length < 2) { return; } const storyTimer = window.setInterval(() => { setActiveStoryIndex((current) => (current + 1) % storyOptions.length); }, 2500); return () => window.clearInterval(storyTimer); }, [storyOptions.length]); useEffect(() => { if (!discountStateReady) { return; } updatePaywallStateValue( () => serializedDiscountSnapshot, { legacyKeys: PAYWALL_DISCOUNT_STORAGE_KEYS, }, ); }, [discountStateReady, serializedDiscountSnapshot]); const handleWalletSurfaceAvailabilityChange = ( surfaceId: string, availability: StripeSubscriptionWalletSurfaceAvailability, ) => { setPaywallWalletAvailability((current) => current[surfaceId] === availability.anyAvailable ? current : { ...current, [surfaceId]: availability.anyAvailable }, ); }; const resolvePaywallWalletMethodClassName = (method: PlatformWalletPaymentMethod) => paywallWalletMethodClassNames[method]; const resolvePaywallWalletMethodWrapperClassName = (method: PlatformWalletPaymentMethod) => paywallWalletMethodWrapperClassNames[method]; const startCheckout = async () => { if (checkoutDisabled) { return; } if (checkoutProvider === 'stripe' && checkoutEmailNeedsReview) { openCheckoutEmailPrompt(true); return; } if (checkoutProvider === 'solidgate') { setSolidgateAttemptKey(crypto.randomUUID()); setCheckoutDialogOpen(true); return; } if (checkoutSession.activeClientSecret) { setCheckoutDialogOpen(true); return; } const readyClientSecret = await checkoutSession.prepareCardCheckout({ checkoutStartSource: 'card_click' }); if (readyClientSecret) { setCheckoutDialogOpen(true); } }; const handleCheckoutCompleted = () => { if (!selectedDisplayPlan) { return; } completeStep(analyticsStepId, { planId: selectedDisplayPlan.id, }); }; const preparePaywallWalletCheckout = async (): Promise => { if (checkoutEmailNeedsReview) { openCheckoutEmailPrompt(true); return null; } const normalizedEmail = checkoutEmail.trim() || user.email.trim(); if (!normalizedEmail) { setCheckoutDialogOpen(true); return null; } return normalizedEmail; }; const openCardCheckoutFromUnavailableWallet = async () => { if (checkoutDisabled) { return; } const readyClientSecret = checkoutSession.activeClientSecret ?? await checkoutSession.prepareCardCheckout({ checkoutStartSource: 'card_click' }); if (readyClientSecret) { setCheckoutDialogOpen(true); } }; const commitCheckoutEmail = async (draftEmail: string): Promise => { const normalizedEmail = draftEmail.trim(); if ( !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(normalizedEmail) || ( checkoutEmailNeedsReview && normalizedEmail.toLowerCase() === checkoutEmail.trim().toLowerCase() ) ) { throw new Error(content.checkout.customerEmailInvalidMessage); } if (normalizedEmail === user.email.trim()) { setCheckoutEmail(normalizedEmail); return normalizedEmail; } const updatedUser = await apiService.updateUser({ ...user, email: normalizedEmail, document: user.document ?? {}, attributes: { ...attributes, ...user.attributes, }, }, { attribution: collectCurrentFunnelAttribution(), }); const syncedEmail = updatedUser.email.trim() || normalizedEmail; setUser(updatedUser); setCheckoutEmail(syncedEmail); return syncedEmail; }; const submitCheckoutEmail = async (event: FormEvent) => { event.preventDefault(); try { await commitCheckoutEmail(checkoutEmailPromptDraft); setCheckoutEmailNeedsReview(false); setCheckoutEmailPromptOpen(false); setCheckoutEmailPromptError(null); checkoutSession.setError(null); setError(null); } catch (submitError) { setCheckoutEmailPromptError( submitError instanceof Error && submitError.message ? submitError.message : content.checkout.customerEmailInvalidMessage, ); } }; const cancelCheckoutEmailPrompt = () => { setCheckoutEmailPromptOpen(false); setCheckoutEmailPromptError(null); }; const closeCheckoutDialog = () => { setCheckoutDialogOpen(false); setSolidgateAttemptKey(null); setError(null); if (discountState.stage === 'first' && discountState.status === 'active') { setSpecialOfferDialogOpen(true); } }; const handleSpecialOfferAccept = () => { setSpecialOfferDialogOpen(false); paywallStepRef.current?.scrollTo({ top: 0, behavior: 'smooth' }); setDiscountState((current) => activateSecondPaywallDiscount({ discounts: activeBillingDiscounts, state: current, nowMs: Date.now(), }), ); }; const toggleFaqId = (faqId: string) => { setOpenFaqIds((current) => current.includes(faqId) ? current.filter((currentId) => currentId !== faqId) : [...current, faqId], ); }; if (!discountStateReady) { return ( <>
); } return ( <> {checkoutProvider === 'stripe' && checkoutEmailPromptOpen ? (
void submitCheckoutEmail(event)} >

{content.checkout.emailPromptTitle}

{content.checkout.emailPromptDescription}

{checkoutEmailPromptError ? (

{checkoutEmailPromptError}

) : null}
) : null} {checkoutProvider === 'solidgate' && checkoutDialogOpen && solidgateAttemptKey && selectedPlan?.funnelPlanKey ? ( ) : null} {checkoutDialogOpen && checkoutProvider === 'stripe' && checkoutSession.activeClientSecret && checkoutSession.stripePromise && selectedDisplayPlan ? ( ) : null} {specialOfferDialogOpen ? ( ) : null}

{content.topBarDiscountLabel}

{String(minutes).padStart(2, '0')} : {String(seconds).padStart(2, '0')}

{content.hero.pills.map((line, index) => (

{line}

))}
{activeCouponId ? (
{upgradedDiscountActive ? ( <>
{discountState.previousDiscountPercent ? ( {discountState.previousDiscountPercent}% ) : null} {content.checkout.discountLabelTemplate.replace('{percent}', String(discountState.discountPercent))}

{content.promo.upgradedLabel}

) : null}

{content.highlight.title.map((line) => ( {line}
))}

{content.highlight.description}

{content.plans.title}

{displayPlans.map((plan) => ( ))}
{checkoutProvider === 'stripe' && selectedDisplayPlan ? ( handleWalletSurfaceAvailabilityChange(PAYWALL_WALLET_SURFACE_ID, availability) } onError={handleStripeCheckoutError} onSuccess={handleCheckoutCompleted} onUnavailableClick={openCardCheckoutFromUnavailableWallet} resolveMethodClassName={resolvePaywallWalletMethodClassName} resolveMethodWrapperClassName={resolvePaywallWalletMethodWrapperClassName} summaryLabel={checkoutSummaryLabel} surfaceId={PAYWALL_WALLET_SURFACE_ID} suspended={checkoutDialogReady || checkoutEmailPromptOpen || specialOfferDialogOpen} /> ) : null}

{content.plans.metaLabels.join(' · ')}

{content.checkout.supportText}{' '} {supportEmail}

{checkoutProvider === 'stripe' && !checkoutSession.configured ? (

{content.plans.stripeUnavailableNote}

) : null} {error ?

{error}

: null}

{content.featureGrid.title.map((line) => ( {line}
))}

{content.featureGrid.subtitle}

{[content.featureGrid.assistant, ...content.featureGrid.quickFeatures].map((feature) => (

{(feature.title || '').split('\n').map((line) => ( {line} ))}

))}

{content.stories.title}

{activeStory?.quote}

- {activeStory?.author}

{content.faq.title}

{content.faq.items.map((item) => { const isOpen = openFaqIds.includes(item.id); return (
{item.answer && isOpen ? (

{item.answer}

) : null}
); })}
{content.guarantee.image.alt}

{content.guarantee.title}

{content.guarantee.description}{' '} {content.guarantee.links[0]?.label || content.refundPolicyLabel} {' '} {content.guarantee.detailsSuffix}

); } const stepPaywallStyles = ` .paywall-v2-loading-screen { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; background: #fff; } .paywall-v2-loading-content { display: flex; flex-direction: column; align-items: center; gap: 12px; } .paywall-v2-loading-spinner { width: 26px; height: 26px; border: 3px solid rgb(63 81 181 / 14%); border-top-color: var(--color-secondary-text); border-radius: 999px; animation: paywall-v2-loading-spin 720ms linear infinite; } .paywall-v2-loading-label { margin: 0; color: #5f6790; font-size: 14px; font-weight: 600; line-height: 1.25; text-align: center; } .paywall-v2-step { position: absolute; inset: 0; box-sizing: border-box; max-width: 100%; overflow-y: auto; overflow-x: hidden; background: #ecf7ff; padding: 0 24px 64px; container-type: inline-size; } .paywall-v2-top-row { position: sticky; top: 0; z-index: 20; min-height: 92px; box-sizing: border-box; display: flex; flex-direction: column; justify-content: center; gap: 6px; margin: 0 -24px 0; padding: 8px 24px 10px; background: #ecf7ff; border-bottom: 1px solid rgb(63 81 181 / 10%); } .paywall-v2-top-label { margin: 0; color: var(--color-secondary-text); font-size: 13px; font-weight: 700; line-height: 1.15; letter-spacing: 0; } .paywall-v2-top-controls { display: grid; grid-template-columns: minmax(128px, 164px) minmax(132px, 1fr); align-items: center; gap: 12px; } .paywall-v2-countdown { width: 100%; min-width: 0; display: flex; flex-direction: column; justify-content: center; gap: 4px; } .paywall-v2-countdown-value { margin: 0; display: grid; grid-template-columns: minmax(0, 1fr) 14px minmax(0, 1fr); align-items: end; color: var(--color-secondary-text); font-size: 40px; font-weight: 700; line-height: 1; letter-spacing: -0.08em; text-align: left; } .paywall-v2-countdown-segment { color: var(--color-secondary-text); } .paywall-v2-countdown-separator { color: #b8b0a5; text-align: center; } .paywall-v2-countdown-units { margin: 0; display: grid; grid-template-columns: minmax(0, 1fr) 14px minmax(0, 1fr); align-items: start; color: #2f230f; font-size: 11px; font-weight: 500; line-height: 1.15; } .paywall-v2-countdown-unit { white-space: nowrap; text-align: center; } .paywall-v2-scroll-cta { width: 100%; max-width: 188px; min-height: 46px; padding: 0 14px; border: 0; border-radius: 16px; background: var(--color-secondary-text); display: flex; align-items: center; justify-content: center; color: #fff; font-size: 16px; font-weight: 800; line-height: 1; letter-spacing: -0.03em; cursor: pointer; justify-self: end; } .paywall-v2-head { margin-top: 16px; display: flex; flex-direction: column; align-items: center; gap: 4px; } .paywall-v2-pill-line { margin: 0; max-width: 100%; box-sizing: border-box; display: inline-flex; align-items: center; justify-content: center; border-radius: 999px; background: var(--color-secondary-text); padding: 4px 16px; color: #fff; font-size: 28px; font-weight: 600; line-height: 1.25; text-align: center; } .paywall-v2-pill-line.is-green { color: #bcfbb4; } .paywall-v2-hero { position: relative; display: grid; place-items: center; width: min(100%, 390px); height: 230px; margin: 20px auto 8px; overflow: hidden; border-radius: 32px; background: linear-gradient(145deg, #d8edff, #f7fbff); } .paywall-v2-hero-image { width: 160px; height: 120px; object-fit: contain; } .paywall-v2-promo-banner { --paywall-v2-promo-code-height: 41px; position: relative; display: flex; flex-direction: column; width: 100%; max-width: 382px; min-height: 86px; margin: -20px auto 0; box-sizing: border-box; overflow: hidden; border-radius: 16px; background: #f4c249; box-shadow: 0 2px 5px rgb(40 58 128 / 10%), 0 9px 9px rgb(40 58 128 / 9%), 0 20px 12px rgb(40 58 128 / 5%), 0 36px 14px rgb(40 58 128 / 1%); text-align: center; } .paywall-v2-promo-banner::before, .paywall-v2-promo-banner::after { content: ''; position: absolute; top: calc(100% - var(--paywall-v2-promo-code-height) - 1px); width: 24px; height: 24px; border-radius: 999px; background: #ecf7ff; transform: translateY(-50%); } .paywall-v2-promo-banner::before { left: -12px; } .paywall-v2-promo-banner::after { right: -12px; } .paywall-v2-promo-label { margin: 0; display: flex; min-height: 44px; align-items: center; justify-content: center; padding: 0 16px; color: #56330c; font-family: var(--font-family-base, Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif); font-size: 16px; font-weight: 600; line-height: 1.35; text-align: center; } .paywall-v2-promo-divider { height: 1px; margin: 0 13px; background-image: repeating-linear-gradient( to right, rgb(255 255 255 / 92%) 0 10px, transparent 10px 16px ); } .paywall-v2-promo-code { margin: 0; display: flex; min-height: var(--paywall-v2-promo-code-height); align-items: center; justify-content: center; width: 100%; box-sizing: border-box; background: #dca63a; color: #fff; font-family: var(--font-family-base, Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif); font-size: 14px; font-weight: 700; line-height: 1.35; text-align: center; padding: 0 16px; text-transform: uppercase; } .paywall-v2-promo-banner.is-upgraded { min-height: 126px; background: #edf4d4; } .paywall-v2-promo-banner.is-upgraded .paywall-v2-promo-code { background: #dbe8b1; color: #5f780d; } .paywall-v2-promo-upgrade-row { display: flex; align-items: center; gap: 16px; padding: 12px 16px 14px 12px; } .paywall-v2-promo-upgrade-badge { position: relative; display: flex; min-width: 110px; min-height: 80px; flex-direction: column; align-items: center; justify-content: center; gap: 6px; border-radius: 24px; background: #76aa10; color: #fff; flex-shrink: 0; } .paywall-v2-promo-upgrade-badge::after { content: ''; position: absolute; left: 50%; bottom: -12px; width: 0; height: 0; border-left: 20px solid transparent; border-right: 20px solid transparent; border-top: 12px solid #76aa10; transform: translateX(-50%); } .paywall-v2-promo-upgrade-badge-old { color: rgb(255 255 255 / 78%); font-size: 17px; font-weight: 600; line-height: 1; text-decoration: line-through; } .paywall-v2-promo-upgrade-badge-main { font-size: 18px; font-weight: 800; line-height: 1.05; text-align: center; } .paywall-v2-promo-upgrade-label { margin: 0; color: #2f230f; font-family: var(--font-family-base, Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif); font-size: 18px; font-weight: 700; line-height: 1.05; letter-spacing: -0.03em; text-align: left; } .paywall-v2-highlight { margin-top: 16px; border-radius: 16px; background: #fff; padding: 16px; text-align: center; } .paywall-v2-highlight-title { margin: 0; color: var(--color-primary); font-size: 28px; font-weight: 600; line-height: 1.25; letter-spacing: -0.036em; } .paywall-v2-highlight-copy { margin: 12px 0 0; color: #1b286f; font-size: 14px; font-weight: 500; line-height: 1.25; } .paywall-v2-plan-list { margin-top: 24px; display: flex; flex-direction: column; gap: 16px; } .paywall-v2-title { margin: 0; color: var(--color-secondary-text); font-size: 28px; font-weight: 600; line-height: 1.25; text-align: center; } .paywall-v2-plan { box-sizing: border-box; display: flex; flex-direction: column; width: 100%; min-height: 92px; border: 1px solid #dce2ff; border-radius: 16px; background: #fff; padding: 0; text-align: left; cursor: pointer; } .paywall-v2-plan--featured { overflow: hidden; border-color: var(--color-primary); } .paywall-v2-plan.is-selected { border-color: var(--color-primary); box-shadow: inset 0 0 0 1px var(--color-primary); } .paywall-v2-plan-tag { display: flex; min-height: 32px; align-items: center; justify-content: center; background: var(--color-primary); color: #fff; font-size: 14px; font-weight: 600; line-height: 1.25; text-align: center; } .paywall-v2-plan-row { display: flex; min-height: 92px; align-items: center; gap: 12px; padding: 16px 14px; box-sizing: border-box; } .paywall-v2-plan-indicator { width: 22px; height: 22px; border: 1.5px solid rgb(31 16 0 / 24%); border-radius: 999px; display: flex; align-items: center; justify-content: center; flex-shrink: 0; box-sizing: border-box; } .paywall-v2-plan-indicator.is-selected { border-color: var(--color-primary); background: var(--color-primary); } .paywall-v2-plan-indicator-check { width: 7px; height: 14px; border-right: 2px solid #fff; border-bottom: 2px solid #fff; transform: rotate(45deg) translate(-1px, -2px); box-sizing: border-box; } .paywall-v2-plan-body { display: flex; align-items: center; justify-content: flex-start; gap: 12px; width: 100%; min-width: 0; } .paywall-v2-plan-left, .paywall-v2-plan-right { display: flex; flex-direction: column; justify-content: center; gap: 6px; } .paywall-v2-plan-left { flex: 1 1 0; width: auto; min-width: 0; } .paywall-v2-plan-right { width: min(34%, 118px); min-width: 86px; align-items: center; text-align: center; flex-shrink: 0; } .paywall-v2-plan-divider { width: 1px; align-self: stretch; background: rgb(31 16 0 / 8%); border-left: 1px solid rgb(31 16 0 / 8%); flex-shrink: 0; } .paywall-v2-plan-title { margin: 0; color: rgba(31, 16, 0, 0.44); font-size: 22px; font-weight: 700; line-height: 1.25; overflow-wrap: anywhere; } .paywall-v2-plan-price { margin: 0; display: flex; flex-wrap: wrap; align-items: center; gap: 4px; color: rgba(31, 16, 0, 0.72); font-size: 16px; font-weight: 500; line-height: 1.25; } .paywall-v2-plan-price-old { text-decoration: line-through; } .paywall-v2-plan-price-main { color: rgba(31, 16, 0, 0.72); font-size: 16px; font-weight: 500; line-height: 1.25; min-width: 0; } .paywall-v2-plan-per-day-price { margin: 0; display: flex; flex-direction: column; align-items: center; width: 100%; gap: 2px; } .paywall-v2-plan-per-day-old { margin: 0; color: rgba(31, 16, 0, 0.72); font-size: 15px; font-weight: 500; line-height: 1.25; text-decoration: line-through; } .paywall-v2-plan-per-day-amount { margin: 0; color: rgba(31, 16, 0, 0.44); font-size: 30px; font-weight: 700; line-height: 1.05; } .paywall-v2-plan-per-day-label { margin: 0; color: rgba(31, 16, 0, 0.72); font-size: 16px; font-weight: 500; line-height: 1.25; overflow-wrap: anywhere; } .paywall-v2-plan.is-selected .paywall-v2-plan-title, .paywall-v2-plan.is-selected .paywall-v2-plan-per-day-amount { color: var(--color-secondary-text); } .paywall-v2-payment-stack { margin-top: 24px; display: flex; flex-direction: column; align-items: stretch; gap: 12px; } .paywall-v2-apple-pay-button, .paywall-v2-google-pay-button { min-height: 64px; border-radius: 16px; } .paywall-v2-apple-pay-shell, .paywall-v2-google-pay-shell { width: 100%; } .paywall-v2-wallet-surface { display: flex; flex-direction: column; gap: 12px; width: 100%; } .paywall-v2-card-button { width: 100%; min-height: 64px; border: 0; border-radius: 16px; cursor: pointer; } .paywall-v2-card-button, .paywall-v2-scroll-cta { transition: opacity 160ms ease, transform 180ms ease, box-shadow 180ms ease, background-color 180ms ease; } .paywall-v2-card-button:disabled, .paywall-v2-scroll-cta:disabled { cursor: default; opacity: 0.65; } .paywall-v2-vibe-cta { position: relative; isolation: isolate; overflow: visible; transform-origin: center; } .paywall-v2-vibe-cta::after { content: ''; position: absolute; inset: -7px; border: 2px solid rgb(63 81 181 / 0%); border-radius: 22px; opacity: 0; pointer-events: none; } .paywall-v2-vibe-cta:not(:disabled) { animation: paywall-v2-cta-beat 2400ms ease-in-out infinite; } .paywall-v2-vibe-cta:not(:disabled)::after { animation: paywall-v2-cta-wave 2400ms ease-out infinite; } @keyframes paywall-v2-cta-beat { 0%, 100% { transform: scale(1); } 10% { transform: scale(1.035); } 18% { transform: scale(1); } 28% { transform: scale(1.018); } 38% { transform: scale(1); } } @keyframes paywall-v2-cta-wave { 0%, 17% { opacity: 0; transform: scale(0.98); border-color: rgb(63 81 181 / 0%); } 24% { opacity: 0.42; transform: scale(1.02); border-color: rgb(63 81 181 / 28%); } 48% { opacity: 0; transform: scale(1.14); border-color: rgb(63 81 181 / 0%); } 100% { opacity: 0; transform: scale(1.14); border-color: rgb(63 81 181 / 0%); } } .paywall-v2-card-button { background: var(--color-secondary-text); color: #fff; font-size: 18px; font-weight: 700; line-height: 1.25; } .paywall-v2-alt-meta { margin: 0; color: var(--color-secondary-text); font-size: 14px; font-weight: 500; line-height: 1.25; text-align: center; } .paywall-v2-disclaimer { margin: 0; border: 1px solid rgb(63 81 181 / 12%); border-radius: 24px; background: rgb(255 255 255 / 72%); padding: 20px 18px; color: #766f66; font-size: 15px; font-weight: 500; line-height: 1.45; text-align: center; } .paywall-v2-support { margin: 0; color: #4d5470; font-size: 14px; font-weight: 500; line-height: 1.35; text-align: center; } .paywall-v2-support a { color: var(--color-secondary-text); font-weight: 600; text-decoration: none; } .paywall-v2-note, .paywall-v2-error { margin: 0; font-size: 14px; font-weight: 600; line-height: 1.35; text-align: center; } .paywall-v2-note { color: #4d5470; } .paywall-v2-error { color: #d33; } .paywall-v2-email-prompt-backdrop { position: fixed; inset: 0; z-index: 1000; display: flex; align-items: center; justify-content: center; padding: 20px; background: rgb(13 17 32 / 42%); } .paywall-v2-email-prompt { box-sizing: border-box; width: min(100%, 360px); border-radius: 20px; background: var(--color-surface, #fff); padding: 22px; box-shadow: 0 18px 48px rgb(19 27 47 / 26%); color: var(--color-text, #111827); } .paywall-v2-email-prompt h2, .paywall-v2-email-prompt p { margin: 0; } .paywall-v2-email-prompt h2 { font-size: 22px; font-weight: 800; line-height: 1.15; } .paywall-v2-email-prompt > p { margin-top: 10px; color: var(--color-text, #4b5563); font-size: 15px; font-weight: 500; line-height: 1.45; } .paywall-v2-email-prompt label { display: flex; flex-direction: column; gap: 8px; margin-top: 18px; font-size: 13px; font-weight: 700; } .paywall-v2-email-prompt input { box-sizing: border-box; min-height: 48px; border: 1px solid var(--color-border, rgb(31 16 0 / 18%)); border-radius: 12px; background: var(--color-surface, #fff); padding: 0 14px; color: var(--color-text, #111827); font: inherit; } .paywall-v2-email-prompt-error { margin-top: 10px !important; color: var(--color-danger, #b42318); font-size: 13px; font-weight: 600; line-height: 1.35; } .paywall-v2-email-prompt-actions { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-top: 18px; } .paywall-v2-email-prompt-actions button { min-height: 44px; border: 0; border-radius: 12px; background: var(--color-bg, #eef1f6); color: var(--color-text, #111827); font-weight: 700; cursor: pointer; } .paywall-v2-email-prompt-actions button[type='submit'] { background: var(--color-button); color: var(--color-button-text); } @media (prefers-reduced-motion: reduce) { .paywall-v2-vibe-cta:not(:disabled), .paywall-v2-vibe-cta:not(:disabled)::after { animation: none; } } .paywall-v2-feedback { margin: 32px -24px 0; } .paywall-v2-feedback-title { margin: 0; color: var(--color-secondary-text); font-size: 28px; font-weight: 600; line-height: 1.25; text-align: center; } .paywall-v2-feedback-card { margin-top: 24px; box-sizing: border-box; height: 372px; padding: 48px 48px 40px; display: flex; flex-direction: column; justify-content: center; gap: 24px; text-align: center; transition: background-color 240ms ease; } .paywall-v2-feedback-quote { margin: 0; color: #fff; font-size: 24px; font-weight: 600; line-height: 1.25; } .paywall-v2-feedback-author { margin: 0; color: #fff; font-size: 14px; font-weight: 600; line-height: 1.25; } .paywall-v2-faq { margin-top: 32px; } .paywall-v2-faq-title { margin: 0; color: var(--color-secondary-text); font-size: 28px; font-weight: 600; line-height: 1.25; text-align: center; } .paywall-v2-faq-list { margin-top: 24px; display: flex; flex-direction: column; gap: 16px; } .paywall-v2-faq-item { border-radius: 16px; background: #fff; padding: 24px 16px; } .paywall-v2-faq-toggle { width: 100%; border: 0; background: transparent; padding: 0; display: flex; align-items: flex-start; justify-content: space-between; gap: 10px; text-align: left; cursor: pointer; } .paywall-v2-faq-question, .paywall-v2-faq-answer { margin: 0; } .paywall-v2-faq-question { color: var(--color-secondary-text); font-size: 18px; font-weight: 600; line-height: 1.25; } .paywall-v2-faq-answer { margin-top: 18px; color: #585a5e; font-size: 16px; font-weight: 500; line-height: 1.25; white-space: pre-line; } .paywall-v2-faq-icon { position: relative; flex-shrink: 0; width: 28px; height: 28px; border-radius: 999px; background: #ecf7ff; } .paywall-v2-faq-icon::before, .paywall-v2-faq-icon::after { content: ''; position: absolute; top: 50%; left: 50%; width: 10px; height: 2px; border-radius: 999px; background: var(--color-secondary-text); transform: translate(-50%, -50%); transition: opacity 160ms ease; } .paywall-v2-faq-icon::after { transform: translate(-50%, -50%) rotate(90deg); } .paywall-v2-faq-icon.is-open::after { opacity: 0; } .paywall-v2-guarantee-card { margin-top: 32px; border-radius: 16px; background: #fff; padding: 11px 16px 16px; display: flex; flex-direction: column; align-items: center; gap: 12px; } .paywall-v2-badge { width: 184px; height: 184px; object-fit: cover; } .paywall-v2-guarantee-copy { width: 100%; display: flex; flex-direction: column; gap: 8px; text-align: center; } .paywall-v2-guarantee-title { margin: 0; color: var(--color-secondary-text); font-size: 22px; font-weight: 600; line-height: 1.2; white-space: nowrap; } .paywall-v2-guarantee-text { margin: 0; color: #6074e2; font-size: 14px; font-weight: 500; line-height: 1.25; } .paywall-v2-guarantee-text a { color: #3d59f8; font-weight: 600; text-decoration: none; } .paywall-v2-feature-grid { margin-top: 48px; } .paywall-v2-feature-grid-head { text-align: center; } .paywall-v2-feature-grid-title { margin: 0; color: var(--color-secondary-text); font-size: 28px; font-weight: 600; line-height: 1.25; letter-spacing: -0.036em; } .paywall-v2-feature-grid-subtitle { margin: 16px 0 0; color: var(--color-secondary-text); font-size: 16px; font-weight: 500; line-height: 1.25; } .paywall-v2-feature-card { border-radius: 16px; background: #fff; } .paywall-v2-feature-card--large { margin-top: 24px; min-height: 220px; padding: 16px; display: flex; flex-direction: column; align-items: center; gap: 16px; } .paywall-v2-feature-row { margin-top: 24px; display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 14px; } .paywall-v2-feature-card--small { min-height: 122px; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 10px; padding: 10px; } .paywall-v2-feature-row--light { grid-template-columns: repeat(2, minmax(0, 1fr)); } .paywall-v2-feature-check { display: grid; width: 32px; height: 32px; place-items: center; border-radius: 999px; background: color-mix(in srgb, var(--color-primary) 14%, white); color: var(--color-primary); font-weight: 800; } .paywall-v2-feature-tile-title { margin: 0; display: flex; flex-direction: column; } .paywall-v2-feature-tile-title { font-size: 18px; font-weight: 600; line-height: 1.25; text-align: center; } @media (max-width: 429px) { .paywall-v2-step { padding: 0 16px 48px; } } @container (max-width: 429px) { .paywall-v2-top-row { min-height: 92px; margin: 0 -16px 0; padding: 8px 16px 10px; } .paywall-v2-top-controls { grid-template-columns: minmax(120px, 150px) minmax(128px, 1fr); gap: 10px; } .paywall-v2-countdown-value { font-size: 40px; } .paywall-v2-scroll-cta { min-height: 46px; padding: 0 10px; font-size: 15px; } .paywall-v2-plan-row { gap: 10px; padding: 14px 12px; } .paywall-v2-plan-body { gap: 10px; } .paywall-v2-plan-right { min-width: 80px; } .paywall-v2-feedback { margin-left: -16px; margin-right: -16px; } } `;