# Payments and subscription contract

Commerce steps use shared plan, checkout, wallet, handoff, and subscription helpers. Components own presentation and user intent, not provider integration.

<!-- funnelsgrove:generated:start contract-v3/contract/payments -->
Step contract version: `3`

Contract hash: `d761e91d5ac6ff9e72c6d49c5bcd014270912473a66f99c49d726c65998085cf`

| Type | Family | Action owner | Required kind | Terminal | Meaning |
| --- | --- | --- | --- | --- | --- |
| `cancellation_offer` | `commerce` | `step` | `cancellation` | `false` | A cancellation retention or downsell surface. |
| `checkout` | `commerce` | `step` | — | `false` | A dedicated payment-entry screen. |
| `paywall_offer` | `commerce` | `step` | `paywall` | `false` | A monetization offer that presents purchasable plans. |
| `subscription_handoff` | `commerce` | `step` | `subscription-handoff` | `false` | A non-terminal app, store, or deep-link handoff. |
| `subscription_management` | `commerce` | `step` | `manage-subscription` | `false` | A screen for managing or inspecting an existing subscription. |
| `upsell_offer` | `commerce` | `step` | `upsell` | `false` | A post-purchase or upgrade offer. |

| Capability | Allowed types | Allowed identities | Required owner |
| --- | --- | --- | --- |
| `checkout` | `paywall_offer`, `upsell_offer`, `checkout` | `any` | `named-payment-helpers` |
| `subscription-handoff` | `subscription_handoff`, `purchase_completed` | `any` | `runtime-handoff-helper` |
| `subscription-management` | `subscription_management`, `cancellation_offer` | `any` | `runtime-subscription-helpers` |
<!-- funnelsgrove:generated:end contract-v3/contract/payments -->

## Paywall definition

A paywall presents purchasable plans. A reveal, loading state, calculation, or “your plan is ready” bridge is a progress interstitial even when it precedes a paywall.

## Payment rules

- Resolve plans, prices, discounts, currencies, provider IDs, and payment mode from shared configuration.
- Use `SharedStripeCheckoutV2Dialog` for card checkout. Render one `StripeSubscriptionWalletSurface` per simultaneously visible subscription wallet placement and one `StripeOneTimeWalletSurface` per one-time placement. Never share a checkout hook result between card and wallet surfaces or between two visible wallet placements.
- Pass `checkoutAnalytics` to every shared dialog and wallet surface. The payments package owns `checkout_started`, `add_payment_info`, and verified `checkout_completed`; do not call the corresponding `publicAnalyticsSdk` methods in funnel components.
- Treat `onSuccess` as the first valid place to call `completeStep` for a purchase flow. Payment-info submission, dialog open, session creation, browser return, and provider-pending state are not lifecycle completion.
- Use `completeStripeOneClickPayment` for one-click upsells. Do not duplicate the public one-click endpoint, Stripe loader, or `confirmPayment` inside a funnel.
- Reuse the same event ID when retrying the same logical conversion.
- Keep secrets and trusted provider calls server-side.
- Route post-purchase behavior through the manifest and runtime handoff helper.

On a Stripe return screen, call `trackStripePaymentCheckoutReturn` for browser diagnostics. Query parameters never prove `checkout_completed` or `purchase_completed`.

## Payments API V2 selection

Runtime/payments 0.12.0 and later route every payment through V2, including
hosted checkout, one-time Checkout Sessions and one-click upsells. Forward the published
`runtimeConfigRevisionId`, `offerSetId`, `paymentProfileId`, `provider: 'stripe'`,
`providerPlanId`, and `planKey`; for the one-time hook, supply the resolved plan
with its `offerSetId` and `funnelPlanKey`. Pass revision/profile/provider to each
wallet placement as well as the card hook.

With runtime/payments 0.11.1 and the compatible API, `runtimeConfigRevisionId`
is optional. A deployment does not invalidate an unchanged offer: the server
checks the active offer set, profile and provider price rather than revision
equality. Deploy the API change before upgrading callers that omit revision;
keep forwarding it when available for older APIs. The other selection fields
remain required, and removed or changed offers still return 409.

One-click additionally requires the original paid `checkoutSessionId`, external
`userId`, and a stable `idempotencyKey` for the logical upsell attempt. The server
uses the stored checkout customer and published price, not a browser-supplied
customer, amount, or currency. Preserve that key on network retries; never retry
an uncertain V2 write through V1. Missing selection fails explicitly. Upgrade earlier bundles before server
retirement: unversioned V1 endpoints return 404.

With runtime `0.11.6`, a V2 subscription or one-time Checkout Session request
that fails with a browser transport error is retried once only when it has a
non-empty `idempotencyKey`. The retry reuses the identical checkout identity and
ownership seed. HTTP errors are not transport-retried, and a failed transport
retry is not followed by identity refresh or another checkout request.

SolidGate uses the same V2 checkout-session contract. Forward the published
`offerSetId`, `paymentProfileId`, `provider: 'solidgate'`, `providerPlanId`,
`planKey`, `environment`, and `checkoutType`. Status polling uses the shared V2
checkout-session resource. The old provider-specific SolidGate routes are removed.

With payments `0.11.24`, `StripeSubscriptionWalletSurface` and
`StripeOneTimeWalletSurface` pass `{ checkoutSessionId }` to `onSuccess` from the
confirmed wallet session. Save that result before navigating to an upsell; a
separate card hook does not own the wallet payment. Existing zero-argument
handlers remain compatible. If the result has no session ID, require a new
checkout instead of attempting a V2 one-click charge.

## Purchase completion separation

Quiz completed counts each unique user who reaches a paywall step. `purchase_completed` remains trusted-server-only purchase verification and does not define Quiz completed. Browser terminal reach and `funnel_completed` remain lifecycle-only and are not purchase conversions.

The canonical `subscription-started` route keeps its stable identity and remains the `purchase_completed` terminal type in contract v3. Only the trusted backend emits the purchase event after verifying an initial subscription or one-time payment; renewals, pending payments, and browser checkout returns do not qualify. `registration_completed` remains the later app-link conversion.

Before publishing, complete [paywall/checkout QA](../qa/paywall-checkout.md) and [publish QA](../qa/publish.md).
