@funnelsgrove/payments
v0.25.0
Published
Shared billing and checkout helpers for funnels.
Downloads
10,925
Readme
@funnelsgrove/payments
Shared billing and checkout helpers for funnels.
Read this file before editing checkout code. Deeper implementation docs:
Owns provider-backed checkout primitives
@funnelsgrove/payments owns reusable payment contracts, provider adapters, checkout-session helpers, wallet slots, checkout UI primitives, and Checkout Session-scoped semantic analytics. Funnel code owns paywall layout, copy, when an offer opens, post-payment navigation, and the lifecycle completion callback after verified payment success.
Current Checkout Path
Render every visible wallet placement with StripeSubscriptionWalletSurface or StripeOneTimeWalletSurface, matching the resolved plan's payment mode. They are the normal current path for Apple Pay and Google Pay:
- Each surface owns one private Checkout Session per simultaneously visible wallet placement, isolated by its own
surfaceId. - Simultaneously visible placements may use the same plan and coupon; nothing is shared because each surface keeps its own session.
- Changing the selected plan replaces the surface’s Checkout Session and
clientSecret. A stablesurfaceIdidentifies the placement; it does not guarantee that the Stripe iframe survives a plan change. - If payment confirmation is already in flight when the selection changes, the success callback remains bound to the original confirmed Checkout Session.
Use these for new subscription paywalls:
StripeSubscriptionWalletSurfaceuseStripeSubscriptionCheckoutSessionSharedStripeCheckoutV2DialogtrackPaidStripeSubscriptionCheckoutCompletedtrackStripeSubscriptionPaymentInfoSubmitted
Use these for new one-time paywall plans:
StripeOneTimeWalletSurfaceuseStripeOneTimeCheckoutSessioncreateStripeOneTimeCheckoutSharedStripeCheckoutV2Dialog
SharedStripeCheckoutV2Dialog defaults to the existing full-screen standard surface. Pass
variant='compact' for a centered desktop modal with the same Stripe-owned wallet/card flow;
mobile remains full-screen and all checkout analytics and verified-success behavior are unchanged.
Pass presentation='inline' to embed a card-only form in the paywall. The host owns the offer summary and renewal disclosure. Inline checkout has no modal semantics, close button, Escape dismissal, or shared wallet session; analytics and verified-success callbacks are unchanged.
Use completeStripeOneClickPayment for normal post-checkout one-click upsells. It delegates the charge to funnelSdkService.chargeOneClickPayment and owns any required Stripe confirmation. Pass its optional checkoutAnalytics context to let the shared package emit checkout_completed only after succeeded, including after required confirmation. The context uses StripeSubscriptionCheckoutAnalyticsContext; completion is deduplicated by PaymentIntent ID, and analytics failures do not fail the payment. Funnel code supplies the context and completes its step; the shared package owns the analytics callback. chargeStripeOneClickPayment remains the lower-level charge-only primitive.
Solidgate browser checkout is available for controlled provider-routed funnels through
SolidgateCheckoutDialog, prepareSolidgateCheckout, and pollSolidgateCheckoutStatus. The dialog
mounts one official Solidgate form per attempt, destroys the remote form on replacement/unmount,
and never treats iframe success as funnel completion. Its onSuccess callback runs only after the
provider-neutral status endpoint finds canonical shared-ledger payment evidence.
Localized funnel copy
Funnel content files own translated strings. Pass a complete SharedStripeCheckoutV2Copy
as the dialog's copy prop to translate its method tabs, country label and payment
status/fallback messages. Existing callers keep the English defaults. Stripe-owned
fields and provider errors use the stripeLocale option on both checkout-session
hooks; wallet surfaces accept it through their checkout input. Stripe clients are
cached by publishable key and locale so changing language cannot reuse another
language's client. Locale does not change a plan, currency or billing interval.
ManageSubscriptionContent exported by this package extends the runtime content
with optional labels: ManageSubscriptionLabels. A localized funnel should make
labels required in its own content type and provide the complete English and
translated objects. The labels own section/status text, date locale and interval
templates ({count}), while subscription data and cancellation behavior remain
unchanged. Set errorMessagePolicy: 'content' to render load/cancel failures from
the content's errorMessages. The default 'server' policy preserves existing
error rendering independently of the selected labels.
Checkout email prompt
When the access email is missing or invalid, wallet surfaces render nothing.
The funnel's primary payment CTA (such as Get My Plan) opens CheckoutEmailDialog
and saves the email through the existing caller-owned identity update. Once the
caller adopts the saved email, wallet controls can mount. Provider email rejections
still open the surface's repair dialog; saving never confirms the old session.
CheckoutEmailDialog defaults to a light surface regardless of the browser or
ancestor theme. It inherits the funnel font. Set inherited --checkout-email-*
CSS variables on the funnel root or .checkout-email-dialog to customize it:
background, foreground, muted, border, input-background,
button-background, button-foreground, focus, and error.
For example, --checkout-email-button-background: var(--color-primary) uses the
funnel accent. Set foreground/background pairs together for custom dark surfaces.
Existing dialog.checkout-email-dialog CSS overrides continue to work.
Advanced Wallet Primitives
ApplePaySubscriptionCheckoutSlot, GooglePaySubscriptionCheckoutSlot, and WalletSubscriptionCheckoutSlot stay exported and runtime-compatible. They are backward-compatible advanced low-level primitives: reach for them only when a placement needs wiring the shared wallet surfaces do not cover. Never share one checkout hook result across placements; give each visible placement its own session.
Compatibility-Only Surfaces
Keep these exports available for saved drafts, old published artifacts, and controlled migrations. Do not use them for new subscription checkout work:
SharedStripeCheckoutDialogApplePaySubscribeButtonGooglePaySubscribeButtonAPPLE_PAY_QR_CODE_URL
When a live funnel still uses one of these, migrate the funnel deliberately with its own tests before removing the export.
Use This Package For
- billing catalog types and normalization helpers
- runtime-mode-aware plan catalog selection
- Stripe SDK and public payment API client helpers
- subscription checkout-session preparation, reuse, update, and recovery
- reusable plan-selector UI
- shared embedded checkout dialogs
- Stripe Express Checkout wallet slots and lower-level wallet primitives
- generic timed-offer display helpers
- checkout completion analytics handoff to
@funnelsgrove/analytics
Do Not Use This Package For
- funnel routing
- manifest validation
- funnel-owned billing catalog entries
- funnel-specific paywall layout or copy
- funnel-specific decisions about when to activate, upgrade, or recover an offer
- analytics transport internals
- direct provider SDK calls inside funnel paywall steps; add provider adapters here first, then wire the funnel to the shared session or slot
Public Surfaces
config/billing.config: billing catalog, mapping, plan, and discount contracts.services/publishedBillingRuntime.service: resolves one published runtime artifact into component-ready plans, defaults, upsells, downsells, discounts, and flow experiments.services/planCatalog.service: plan resolution and selection helpers.services/runtimeBillingPlanCatalog.service: test/live runtime catalog selection.services/paywallOffer.service: timed discount and display-plan helpers.providers/paymentProvider.types: provider-neutral checkout state shared by every adapter.providers/stripe/services/*: Stripe loader, Checkout Session, hosted checkout, one-click, analytics, and one-click completion.providers/stripe/hooks/*: Stripe-backed plan resolution and isolated card/wallet checkout-session state.providers/stripe/components/*: plan selector, checkout dialogs, Express Checkout wrappers, wallet slots, placeholders, and trust assets.providers/stripe/testing/*: Stripe wallet checkout smoke assertions.providers/solidgate/services/*: strict Solidgate checkout preparation, status, and bounded polling clients.providers/solidgate/components/*: the official-SDK checkout dialog and canonical verification lifecycle.components/ManageSubscriptionScreen: provider-neutral subscription management UI.
Funnels should call resolvePublishedBillingRuntime once at their runtime-provider boundary. Screens consume its resolved view and should not parse offer sets or pricing-experiment variants themselves. Pass the resolved stripePublishableKey into shared Stripe checkout hooks so Stripe.js can load while the Checkout Session request is still in flight; the key returned by that request remains the compatibility fallback.
Client implementation structure
Provider-neutral plan types, selection and recurrence live in
services/paywallPlans/, exposed through paywallPlans.service.ts.
publishedBillingRuntime and timed-offer helpers must not import a provider
service just to resolve or display plans. Shared config/services are checked for
provider dependencies in paymentBoundaries.test.ts.
Stripe browser operations live in providers/stripe/services/checkout/,
hostedCheckout/ and oneClick/.
Keep request mappers (requests.ts) beside the short async operation functions;
common validation and metadata normalization belong in the local shared/ folder.
stripeClient.ts owns the Stripe.js cache. One-click orchestration calls the
charge and confirmation functions in its own folder, without a workflow layer.
stripe.service.ts is the public export facade. From 0.12.0, all payment
transport uses V2. createStripePaymentIntent is removed: migrate to
createStripeOneTimeCheckout and its Checkout Session UI. Keep legacy published
bundles on their pinned releases until their own migration passes checkout QA.
Hosted checkout uses createStripeCheckoutSession with published profile,
provider and plan selection plus successUrl and cancelUrl. Deploy hosted V2
support before releasing this SDK. Subscription mutation callers forward the
listed subscriptionRef; no provider ownership is reconstructed in the browser.
HTTP transport belongs to the runtime SDK. No charge retry, V1 fallback or extra
conversion event is introduced.
Engineering Rules
- Keep every provider-specific component, hook, service, and test helper under
src/providers/<provider>. Add future providers as sibling folders; do not place provider SDK imports in rootcomponents,hooks, orservices. - Stripe owns real card fields and wallet controls. Local wallet buttons are placeholders until Stripe confirms availability.
- Keep
planIdas the funnel plan key and send Stripe's price id only asproviderPlanId; the API uses both fields to resolve the active test/live offer mapping. - Wallet unavailable state must fall back to card/manual checkout or disappear.
- Count
checkout_startedonly at shared UI boundaries: when a shared card dialog opens or when Stripe reports Apple Pay / Google Pay intent. Checkout Sessions use confirmation or cancellation as a fallback because Stripe omits their click callback. Cancellation recovers the start withcheckout_start_source: wallet_cancel; its timestamp is dismissal time, not wallet-open time. Closing the browser before either callback is still unobservable. Cancel has no payment-method field, so the method is included only when the element enables exactly one method. - Pass
checkoutAnalyticsto every current shared dialog and wallet surface. The package then ownscheckout_started,add_payment_info, and verifiedcheckout_completed, all deduplicated by the actual Checkout Session. - Payment submission triggers, validation, declines, and retry identity follow AE-BR-001–004. Keep that behavior inside shared checkout surfaces.
- Call funnel lifecycle
completeSteponly from the shared checkoutonSuccesscallback. Payment-info submission is not step completion. - Wallet slots may create render-only Checkout Sessions on mount so Apple Pay and Google Pay render before the visitor taps. Session creation is technical preparation and must never emit
checkout_started. - Shared checkout analytics deduplicates
checkout_startedby Checkout Session id. Wallet confirmation and cancellation may retry the start signal when the click callback is unavailable, without creating a duplicate. Cancellation never emits payment-info or purchase completion events. - Do not call
publicAnalyticsSdk.trackCheckoutStarted,trackPaymentInfoSubmitted, ortrackCheckoutCompleteddirectly from funnel checkout components. - Funnel paywalls should style wallet buttons through the shared slot API:
classNamefor the Stripe button/placeholder,slotClassNamefor the outer slot,appearancefor Stripe Elements appearance, andoptionsfor Stripe Express Checkout options. - Keep discount math reusable here, but keep offer activation timing in funnel code.
- Checkout metadata must avoid sensitive raw query params and use
@funnelsgrove/analyticsmetadata helpers.
Build, Test, Publish
npm run test:run --workspace @funnelsgrove/payments
npm run build --workspace @funnelsgrove/payments
npm publish --workspace @funnelsgrove/payments --access publicThe build normalizes generated relative ESM imports to explicit .js files for published package consumers.
Checkout email repair
Checkout email recovery
owns the behavior. Pass onCustomerEmailCommit to wallet surfaces and the card
checkout dialog. It must save the access email, adopt the returned Funnel User, and
update the customerEmail and userId inputs used by checkout. A wallet surface
renders a Continue control and CheckoutEmailDialog when repair is required.
onResolveCustomerEmail can resolve a known identity before asking for input;
its caller must also adopt the resolved identity. emailDialogContent customizes
prompt copy. Keep beforeConfirm for immediate preparation only, without awaiting
human input or changing the access identity.
CheckoutEmailDialog is also exported for funnel-owned checkout entry controls,
including legacy wallet launchers. Mount it before wallet launch, persist through
onSave, and close through onClose; its completion never launches a wallet.
isValidCheckoutEmail and isCheckoutEmailError share validation and recovery
classification. onCustomerEmailChange in the card dialog reports a committed
email, not each draft keystroke, so typing cannot prepare sessions for unsaved input.
Offer price money
Published plans expose optional money: ProviderMoney; new mappings supply it. Discount presentation and purchase analytics use its amount, currency and exponent. amountCents remains a legacy wire alias and does not define the scale of structured money. Independent plans can use different currencies. Currency is selected by the provider price, not by an Offer Set setting.
Published offer selection and checkout attempts
Use resolvePublishedBillingRuntimeState with configuration, browser locale and experiment assignments that are ready. Its result is pending, resolved or error. Pending/error states must not prepare Stripe card/wallet or Solidgate checkout; the maintained templates render an inert zero-plan state rather than a starter catalog.
usePinnedPublishedBillingRuntime holds the complete resolved catalog at the first payment preparation. Wire its pin callback to preparation and release both the payment session and this snapshot on explicit reset. Displayed prices, provider, revision, source version, locale and experiment assignment then remain consistent through background updates. The visitor can still choose another plan within the pinned offer set. A new attempt after reset can adopt the latest configuration.
