wc-stripe-future-payments
Design or audit WooCommerce Stripe payment flows that save a reusable method and charge it later. Covers SetupIntent versus PaymentIntent, charge-now-and-save, deposits and installment series, `setup_future_usage=off_session`, Stripe Customer ownership, explicit consent/mandates,
Install
npx skills add https://github.com/Lonsdale201/wp-agent-skills/tree/main/woocommerce/wc-stripe-future-payments
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install lonsdale201-wp-agent-skills@llmmart
git clone https://github.com/Lonsdale201/wp-agent-skills.git
The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole lonsdale201/wp-agent-skills collection as a plugin from our marketplace. Git is the plain clone.
Skill manifest
WooCommerce Stripe future payments
Saving a payment method, charging now, and charging later are three distinct operations. Assign each operation one owner and an explicit state machine.
Choose the correct Stripe object
| Required outcome | Stripe flow |
|---|---|
| Save now, charge nothing | Confirm a SetupIntent with usage=off_session |
| Charge now and prepare the same method for later | Confirm a PaymentIntent associated with the Customer and set setup_future_usage=off_session |
| Charge an already saved method without the shopper present | Create a new PaymentIntent with the Customer, PaymentMethod, off_session=true, and confirm=true |
| First amount is zero, later amounts are scheduled | SetupIntent first; create a separate PaymentIntent for every due charge |
A SetupIntent never creates a charge. Do not describe “SetupIntent saves the card and deducts the first installment” as one operation. Use a PaymentIntent for charge-now-and-save, or use a SetupIntent followed by a separate PaymentIntent when the product flow genuinely needs two operations.
setup_future_usage improves authentication/optimization for later use; it does not make every payment-method type reusable and does not guarantee that every future off-session attempt succeeds without authentication.
Do not conflate merchant installments with BNPL
In a merchant-managed installment plan, the merchant saves an eligible method and initiates later charges. Third-party BNPL methods such as Affirm, Afterpay/Clearpay, or Klarna commonly use provider approval/redirect flows: the merchant receives the purchase amount under the BNPL settlement contract, while the shopper repays the BNPL provider. That is not the same as saving a merchant-owned card for later PaymentIntents. A subscription gateway may use cards, debits, wallets, hosted checkout, or mandates; there is no universal “every serious gateway does this” implementation.
Decide who owns Stripe checkout
Extend the official stripe gateway when it owns the payment
If checkout already uses WooCommerce Stripe Gateway, keep the selected Woo gateway as stripe and model the plan/deposit choice as separate cart/order data. Let the official gateway own Payment Element, PaymentIntent/SetupIntent creation, SCA, token creation, and its webhooks.
This is usually safer than displaying a second Stripe card form under a fictional installment gateway. The installed plugin already supports charge-and-save for reusable methods and both classic and Checkout Block flows.
Important boundaries:
- The official gateway charges the authoritative Woo order total. Do not patch a private request array to charge a smaller hidden amount.
- Direct Stripe gateway classes and service methods are plugin internals. Prefer documented filters/actions; version-guard and integration-test any unavoidable direct use.
- A plan-specific payment method must still be reusable and enabled. Link, card-through-Link, cards, bank redirects, and debits do not all have the same token or reuse behavior.
Build a separate Stripe-backed gateway only when it owns the whole payment contract
A separate gateway must implement Stripe Elements/Payment Element, Customer and intent creation, confirmation/redirect handling, token projection, signed webhooks, idempotency, refunds, recovery, classic checkout, and Checkout Block integration. Do not reuse raw DOM or JavaScript internals from the official gateway.
Use wc-checkout-block-payment-method for the Blocks adapter. Link support is an additional payment-method/token concern, not a substitute for that adapter.
Force saving through Woo Stripe carefully
Woo Stripe 10.9.0 exposes:
add_filter(
'wc_stripe_force_save_payment_method',
static function ( bool $force, $order_id ): bool {
if ( ! is_user_logged_in() ) {
return $force;
}
return myplugin_future_payment_is_selected( $order_id ) ? true : $force;
},
10,
2
);
Use it only after the shopper has explicitly accepted the future-payment terms. Scope the predicate to the exact plan/order and preserve another integration's true value.
The plugin also calls this filter without an order ID while preparing checkout JavaScript. For Blocks and confirmation-token flows, the pre-order cart/session decision and final order decision must agree. A callback that returns true only after an order exists can prepare the client flow incorrectly.
The helper refuses force-save for logged-out users. Saving also remains disabled when saved cards are off or the selected Stripe method is not reusable. Require login/account creation for long-lived plans, or design and test a deliberate durable guest-to-Customer ownership/recovery model.
A shopper selecting “create account” during final checkout is still logged out when the Block payment UI is initially configured. Therefore the force-save filter alone does not solve a first-purchase guest flow. If first-time shoppers must qualify, authenticate/create the durable account before mounting the payment step, use a recurring system whose gateway integration explicitly owns that lifecycle, or implement the complete custom Customer/intent/token flow. Test this path with a shopper who has no prior Woo token or Stripe Customer.
Do not use wc_stripe_display_save_payment_method_checkbox merely as a cosmetic hide. In the classic Stripe path, hiding an otherwise available checkbox is also interpreted as a forced-save situation. Consent must not be inferred from a missing control.
Opt incompatible plans out of Adaptive Pricing
Adaptive Pricing uses a Stripe Checkout Session and is a different orchestration path from an ordinary PaymentIntent checkout. Stripe already rejects carts containing WCS subscriptions, charge-upon-release pre-orders, and WooCommerce Deposits. If a custom installment, deposit, or future-payment cart cannot preserve its amount, currency, token, or order lifecycle through that path, opt it out explicitly:
add_filter(
'wc_stripe_is_adaptive_pricing_supported',
static function ( bool $supported, $cart ): bool {
if ( ! $supported || ! $cart instanceof WC_Cart ) {
return $supported;
}
return myplugin_cart_has_future_payment_plan( $cart ) ? false : $supported;
},
10,
2
);
This 10.9+ filter runs only after the gateway's own availability and cart checks pass, so it can opt out but must not be treated as an opt-in override. The older wc_stripe_is_checkout_sessions_available filter has been removed; do not use it as a replacement.
Consent and mandate are part of the payment contract
Before saving for off-session use, obtain explicit agreement covering at least:
- permission to initiate the future payment or series;
- expected timing/frequency;
- the amount or an objective method for determining it;
- cancellation/refund terms where applicable.
Store a durable consent snapshot: plan/version, displayed terms version, time, customer/order, schedule, currency, and amount rule. Never set setup_future_usage=off_session silently because it improves conversion.
The Customer/PaymentMethod relationship is also mandatory. On every later attempt, resolve the server-owned plan, Customer, and PaymentMethod; verify that the method belongs to the expected Customer and is still allowed. Never accept a browser-supplied pm_... identifier as ownership proof.
Model the Woo accounting before charging
The immediate Stripe amount, Woo order total, taxes, refunds, fulfillment state, and remaining liability must tell the same story.
Choose an explicit model, for example:
- A full-value parent/order plus auditable child payment/renewal orders for each installment.
- An initial order whose total is the immediate amount plus an owned plan entity that creates later installment orders.
- WooCommerce Subscriptions when its recurring-product and lifecycle semantics actually match the product.
Do not charge only a deposit against a full-value order and call payment_complete() as if the full balance was captured. Do not create later Stripe charges with no Woo-side auditable payment/order/refund record. Define cancellation, partial/full refund allocation, failed installment, chargeback, tax document, fulfillment, and over/underpayment behavior before implementation.
Create every later charge as a new operation
For a due installment, create a new server-side PaymentIntent with:
amount=<authoritative due amount in Stripe minor units>
currency=<plan currency>
customer=<owned Stripe Customer>
payment_method=<owned reusable PaymentMethod>
off_session=true
confirm=true
Use an idempotency key derived from an immutable installment/payment-record ID and attempt policy, not from the current timestamp. Persist the PaymentIntent ID before considering the attempt dispatched. Reconcile timeouts by retrieving provider state before retrying.
Scheduling is delivery, not exactly-once execution. Action Scheduler jobs may be delayed, repeated, or fail after Stripe succeeded. Make the remote charge idempotent and make local settlement safe under duplicate and out-of-order webhooks.
Handle SCA and failure as normal states
Upfront off-session setup reduces later authentication friction but cannot remove it. Model at least:
scheduled -> attempting -> processing/succeeded
-> requires_customer_action
-> declined/retryable
-> terminal_failed/cancelled
When Stripe requires authentication, do not retry the same off-session request indefinitely. Notify the customer through a signed, expiring return flow, bring them on-session, confirm/replace the method, and resume only after verified success. Webhooks or active reconciliation, not the browser return alone, own final settlement.
Woo token boundary
A WC_Payment_Token is a local, user-owned projection of a provider method. Create/update it only after Stripe confirms a reusable method and the expected Customer relationship. Validate local token ownership before resolving its provider ID.
Do not assume every reusable Stripe method is WC_Payment_Token_CC:
- native Link is
type=linkand uses a Link token class; - a card funded through the Link wallet can remain
type=cardwithwallet.type=link; - some redirect methods save a different reusable debit method;
- some enabled methods cannot be reused at all.
Use provider object type/capabilities, not an ID prefix or checkout label, to classify the method.
Review checklist
- Verify SetupIntent versus PaymentIntent choice and that exactly one operation owns the initial charge.
- Verify Customer association, reusable method capability, explicit off-session consent, and durable shopper identity.
- Compare Stripe amount/currency with the authoritative Woo payment record on initial and later attempts.
- Verify provider idempotency, unique local installment records, signed webhooks, event deduplication, and timeout reconciliation.
- Test first-time shopper with no Customer/token, saved method, guest rejection/account creation, classic checkout, Checkout Block, Link/card, SCA now, SCA later, decline, async processing, duplicate job, duplicate webhook, refund, cancellation, and method replacement.
- Test Adaptive Pricing eligibility and opt-out for every custom deposit/installment cart shape.
- Redact secrets, client secrets, payment identifiers where unnecessary, raw provider bodies, and billing data from logs.
Cross-references
wc-stripe-add-payment-methodfor the no-charge My Account SetupIntent flow.wc-checkout-block-payment-methodfor a custom gateway's Blocks adapter.wc-stripe-link-paymentsfor Link-specific representations and consent.wc-stripe-webhooksfor verified asynchronous settlement.wc-action-scheduler-jobsfor delivery, retries, and remote idempotency.wc-stripe-subscriptionswhen WooCommerce Subscriptions owns the recurring contract.- See references/stripe-future-payment-lifecycle.md for intent parameters, installed gateway contracts, and state/recovery details.
References
- Stripe SetupIntents: https://docs.stripe.com/payments/setup-intents
- Stripe save-during-payment: https://docs.stripe.com/payments/save-during-payment?payment-ui=elements
- Stripe BNPL model: https://docs.stripe.com/payments/buy-now-pay-later
- Verified source paths:
wp-content/plugins/woocommerce-gateway-stripe/includes/class-wc-stripe-intent-controller.phpwp-content/plugins/woocommerce-gateway-stripe/includes/class-wc-stripe-helper.phpwp-content/plugins/woocommerce-gateway-stripe/includes/class-wc-stripe-blocks-support.phpwp-content/plugins/woocommerce-gateway-stripe/includes/payment-methods/class-wc-stripe-upe-payment-gateway.php
Files (wp-agent-skills)
-
agents
-
openai.yaml 266 B
interface: display_name: "WooCommerce Stripe Future Payments" short_description: "Charge now and reuse Stripe methods safely" default_prompt: "Use $wc-stripe-future-payments to design or audit a WooCommerce Stripe charge-and-save or off-session payment flow."
-
-
references
-
stripe-future-payment-lifecycle.md 8.4 KB
# Stripe future-payment lifecycle reference Load this reference when implementing charge-and-save, deposits, installments, or later off-session collection. This reference describes merchant-initiated future collection. A third-party BNPL provider usually owns the shopper's repayment plan and pays the merchant according to its own settlement contract; it is not a saved-card/off-session plan owned by the merchant. See <https://docs.stripe.com/payments/buy-now-pay-later>. ## Intent decision matrix ### Save without charging Create/confirm a SetupIntent with: ```text customer=cus_... usage=off_session payment_method=pm_... or collect through Stripe.js ``` Success means the method is prepared for the declared usage. It does not mean money moved. ### Charge now and save Create/confirm a PaymentIntent with: ```text amount=<server amount> currency=<server currency> customer=cus_... payment_method=pm_... setup_future_usage=off_session confirm=true or confirm through Stripe.js ``` For an integration offering reusable and non-reusable types together, apply future usage only to a compatible type, such as `payment_method_options[card][setup_future_usage]=off_session`, rather than assuming every automatic method is reusable. The method becomes reusable only after the relevant provider flow succeeds. Persist local token/state from verified provider results, not from the mere creation of an intent. ### Charge later off-session Create a new PaymentIntent for each due payment: ```text amount=<due amount> currency=<plan currency> customer=cus_... payment_method=pm_... off_session=true confirm=true ``` Never reuse the first successful PaymentIntent as the next installment. It represents one payment lifecycle and one amount/state history. ## Installed Woo Stripe Gateway 10.9.0 contract The installed gateway's behavior is source-verified as follows: - `WC_Stripe_Intent_Controller::create_and_confirm_setup_intent()` creates a SetupIntent with a Customer, PaymentMethod, confirmation, return URL when needed, and mandate options. It has no amount and creates no charge. - Its PaymentIntent request sets `setup_future_usage=off_session` when the shopper saves a reusable method or an automatic-renewal subscription needs it, with confirmation-token/manual-renewal exceptions. - `WC_Stripe_UPE_Payment_Gateway::should_save_payment_method_from_request()` rejects unknown/non-reusable types and already-saved methods; automatic subscriptions save when supported; ordinary checkout requires saved cards plus checkbox or force-save. - Classic save-checkbox values are `true`; Checkout Block values are `1`. - `WC_Stripe_Helper::should_force_save_payment_method()` returns false when logged out and applies `wc_stripe_force_save_payment_method` for logged-in users. - The force-save filter is evaluated both before an order exists for checkout JavaScript and later with an order ID. Keep the result consistent across those phases. - Selecting account creation during checkout does not make the shopper logged in when the payment UI is first configured. Do not present the filter alone as a first-purchase guest solution. - The deprecated `wc_stripe_force_save_source` filter is still bridged, but new integrations must use `wc_stripe_force_save_payment_method`. - `handle_saving_payment_method()` classifies the provider object, rejects non-reusable types, reconciles duplicates, creates/updates the appropriate Woo token, and updates relevant order/subscription data. - Adaptive Pricing is automatically unavailable for WCS subscriptions, charge-upon-release pre-orders, and WooCommerce Deposits. The 10.9+ `wc_stripe_is_adaptive_pricing_supported` filter is a final opt-out for other incompatible carts; the removed `wc_stripe_is_checkout_sessions_available` filter is not a supported fallback. Treat these as version-pinned integration contracts. Test again when the Stripe Gateway version changes. ## Official-gateway extension pattern Use this pattern only when the official `stripe` gateway should own the initial checkout payment: 1. Store the selected plan in validated cart/session state and copy an immutable snapshot to the order. 2. Make the Woo order/payment record's payable amount match the immediate Stripe charge through a deliberate accounting design. 3. Require account/login if the plan needs a local saved token and recovery UI. 4. Enable saved cards and ensure the chosen provider method is reusable. 5. Force save only for the selected/accepted plan, with the same predicate before and after order creation. 6. After verified initial payment success, create the schedule and immutable installment records. 7. Resolve the saved Customer/PaymentMethod from server-owned state for every later charge. Do not register a second card gateway solely to get a separate radio button. If a distinct payment method is a business requirement, it must own a complete provider integration rather than parasitize the official gateway's private markup or JavaScript state. ## Schedule and idempotency model Recommended owned records: ```text plan_id customer/user/order relationship currency and immutable amount rule consent snapshot/version/time installment_id and sequence due_at amount status provider_payment_intent_id attempt counter and last safe error category paid_at / cancelled_at ``` Enforce a unique key on the logical installment identity. Use a provider idempotency key such as: ```text myplugin:installment:<immutable-installment-id>:charge ``` Do not append a random timestamp to retries of the same logical charge. Use a new operation/key only when product policy deliberately creates a new charge attempt after the previous provider operation is known terminal. Action Scheduler delivery sequence: 1. Atomically claim a due installment or observe that it is already owned/complete. 2. If a provider intent ID exists, retrieve/reconcile it before creating anything. 3. Create/confirm with the stable idempotency key. 4. Store the provider ID and observed state. 5. Let signed webhooks/reconciliation transition final state idempotently. 6. Schedule a bounded retry or customer-action workflow according to categorized failure. ## Authentication recovery A future PaymentIntent can require customer action even after correct off-session setup. Record a recoverable state and send the customer to a signed, expiring, order/plan-bound page. On-session recovery must: - authenticate/authorize the shopper; - retrieve the exact server-owned PaymentIntent or create a deliberate replacement; - use Stripe.js with a scoped client secret; - verify final state on the server; - update the reusable method only with explicit consent if replacement is offered; - avoid leaking the client secret through logs, analytics, referrers, or cache. Differentiate insufficient funds, expired/detached method, authentication required, transient API failure, and permanent plan cancellation. They do not share a retry policy. ## Webhook settlement Verify the Stripe signature against the exact raw body. Resolve the local payment/installment through a stored provider ID and compare Customer, amount, currency, and allowed transition. Atomically deduplicate event IDs, while keeping the state transition itself idempotent because distinct Stripe events can describe the same final state. Do not mark an installment paid from: - a successful browser redirect alone; - a client-provided PaymentIntent status; - a SetupIntent success; - a queued Action Scheduler job finishing without verified provider state. ## Test scenarios Use Stripe test methods to cover: - initial payment succeeds and later reuse succeeds; - initial authentication is required; - later off-session authentication is required despite setup; - setup/initial charge decline; - later insufficient funds and expired/detached method; - delayed/asynchronous payment types if allowed; - browser response loss after provider success; - duplicate checkout, duplicate job, duplicate webhook, and out-of-order webhook; - changed plan amount, cancellation race, refund, chargeback, and payment-method replacement; - logged-out shopper, new account, existing saved method, native Link, and card-through-Link. ## Primary documentation - SetupIntent lifecycle and off-session consent: <https://docs.stripe.com/payments/setup-intents> - Save during a PaymentIntent and charge later: <https://docs.stripe.com/payments/save-during-payment?payment-ui=elements> - PaymentIntent API: <https://docs.stripe.com/api/payment_intents> - SetupIntent API: <https://docs.stripe.com/api/setup_intents>
-
-
SKILL.md 13.7 KB
--- name: wc-stripe-future-payments description: Design or audit WooCommerce Stripe payment flows that save a reusable method and charge it later. Covers SetupIntent versus PaymentIntent, charge-now-and-save, deposits and installment series, `setup_future_usage=off_session`, Stripe Customer ownership, explicit consent/mandates, later off-session PaymentIntents, SCA recovery, idempotent scheduling, Woo token projection, guest/account policy, official Woo Stripe Gateway reuse versus a custom Stripe-backed gateway, Blocks/classic checkout, Link polymorphism, webhooks, accounting, and tests. Use for installments, deposits, subscriptions outside WCS, merchant-initiated charges, future payments, saved cards, or claims that a SetupIntent also takes the first payment. metadata: wp-skills-author: "Soczó Kristóf" wp-skills-contact: "mailto:lonsdale201@hotmail.com" wp-skills-plugin: "woocommerce-gateway-stripe" wp-skills-plugin-version-tested: "10.9.0" wp-skills-woocommerce-version-tested: "11.0.1" wp-skills-php-min: "7.4" wp-skills-last-updated: "2026-08-19" --- # WooCommerce Stripe future payments Saving a payment method, charging now, and charging later are three distinct operations. Assign each operation one owner and an explicit state machine. ## Choose the correct Stripe object | Required outcome | Stripe flow | |---|---| | Save now, charge nothing | Confirm a `SetupIntent` with `usage=off_session` | | Charge now and prepare the same method for later | Confirm a `PaymentIntent` associated with the Customer and set `setup_future_usage=off_session` | | Charge an already saved method without the shopper present | Create a new `PaymentIntent` with the Customer, PaymentMethod, `off_session=true`, and `confirm=true` | | First amount is zero, later amounts are scheduled | SetupIntent first; create a separate PaymentIntent for every due charge | A SetupIntent never creates a charge. Do not describe “SetupIntent saves the card and deducts the first installment” as one operation. Use a PaymentIntent for charge-now-and-save, or use a SetupIntent followed by a separate PaymentIntent when the product flow genuinely needs two operations. `setup_future_usage` improves authentication/optimization for later use; it does not make every payment-method type reusable and does not guarantee that every future off-session attempt succeeds without authentication. ## Do not conflate merchant installments with BNPL In a merchant-managed installment plan, the merchant saves an eligible method and initiates later charges. Third-party BNPL methods such as Affirm, Afterpay/Clearpay, or Klarna commonly use provider approval/redirect flows: the merchant receives the purchase amount under the BNPL settlement contract, while the shopper repays the BNPL provider. That is not the same as saving a merchant-owned card for later PaymentIntents. A subscription gateway may use cards, debits, wallets, hosted checkout, or mandates; there is no universal “every serious gateway does this” implementation. ## Decide who owns Stripe checkout ### Extend the official `stripe` gateway when it owns the payment If checkout already uses WooCommerce Stripe Gateway, keep the selected Woo gateway as `stripe` and model the plan/deposit choice as separate cart/order data. Let the official gateway own Payment Element, PaymentIntent/SetupIntent creation, SCA, token creation, and its webhooks. This is usually safer than displaying a second Stripe card form under a fictional installment gateway. The installed plugin already supports charge-and-save for reusable methods and both classic and Checkout Block flows. Important boundaries: - The official gateway charges the authoritative Woo order total. Do not patch a private request array to charge a smaller hidden amount. - Direct Stripe gateway classes and service methods are plugin internals. Prefer documented filters/actions; version-guard and integration-test any unavoidable direct use. - A plan-specific payment method must still be reusable and enabled. Link, card-through-Link, cards, bank redirects, and debits do not all have the same token or reuse behavior. ### Build a separate Stripe-backed gateway only when it owns the whole payment contract A separate gateway must implement Stripe Elements/Payment Element, Customer and intent creation, confirmation/redirect handling, token projection, signed webhooks, idempotency, refunds, recovery, classic checkout, and Checkout Block integration. Do not reuse raw DOM or JavaScript internals from the official gateway. Use `wc-checkout-block-payment-method` for the Blocks adapter. Link support is an additional payment-method/token concern, not a substitute for that adapter. ## Force saving through Woo Stripe carefully Woo Stripe 10.9.0 exposes: ```php add_filter( 'wc_stripe_force_save_payment_method', static function ( bool $force, $order_id ): bool { if ( ! is_user_logged_in() ) { return $force; } return myplugin_future_payment_is_selected( $order_id ) ? true : $force; }, 10, 2 ); ``` Use it only after the shopper has explicitly accepted the future-payment terms. Scope the predicate to the exact plan/order and preserve another integration's `true` value. The plugin also calls this filter without an order ID while preparing checkout JavaScript. For Blocks and confirmation-token flows, the pre-order cart/session decision and final order decision must agree. A callback that returns true only after an order exists can prepare the client flow incorrectly. The helper refuses force-save for logged-out users. Saving also remains disabled when saved cards are off or the selected Stripe method is not reusable. Require login/account creation for long-lived plans, or design and test a deliberate durable guest-to-Customer ownership/recovery model. A shopper selecting “create account” during final checkout is still logged out when the Block payment UI is initially configured. Therefore the force-save filter alone does not solve a first-purchase guest flow. If first-time shoppers must qualify, authenticate/create the durable account before mounting the payment step, use a recurring system whose gateway integration explicitly owns that lifecycle, or implement the complete custom Customer/intent/token flow. Test this path with a shopper who has no prior Woo token or Stripe Customer. Do not use `wc_stripe_display_save_payment_method_checkbox` merely as a cosmetic hide. In the classic Stripe path, hiding an otherwise available checkbox is also interpreted as a forced-save situation. Consent must not be inferred from a missing control. ## Opt incompatible plans out of Adaptive Pricing Adaptive Pricing uses a Stripe Checkout Session and is a different orchestration path from an ordinary PaymentIntent checkout. Stripe already rejects carts containing WCS subscriptions, charge-upon-release pre-orders, and WooCommerce Deposits. If a custom installment, deposit, or future-payment cart cannot preserve its amount, currency, token, or order lifecycle through that path, opt it out explicitly: ```php add_filter( 'wc_stripe_is_adaptive_pricing_supported', static function ( bool $supported, $cart ): bool { if ( ! $supported || ! $cart instanceof WC_Cart ) { return $supported; } return myplugin_cart_has_future_payment_plan( $cart ) ? false : $supported; }, 10, 2 ); ``` This 10.9+ filter runs only after the gateway's own availability and cart checks pass, so it can opt out but must not be treated as an opt-in override. The older `wc_stripe_is_checkout_sessions_available` filter has been removed; do not use it as a replacement. ## Consent and mandate are part of the payment contract Before saving for off-session use, obtain explicit agreement covering at least: - permission to initiate the future payment or series; - expected timing/frequency; - the amount or an objective method for determining it; - cancellation/refund terms where applicable. Store a durable consent snapshot: plan/version, displayed terms version, time, customer/order, schedule, currency, and amount rule. Never set `setup_future_usage=off_session` silently because it improves conversion. The Customer/PaymentMethod relationship is also mandatory. On every later attempt, resolve the server-owned plan, Customer, and PaymentMethod; verify that the method belongs to the expected Customer and is still allowed. Never accept a browser-supplied `pm_...` identifier as ownership proof. ## Model the Woo accounting before charging The immediate Stripe amount, Woo order total, taxes, refunds, fulfillment state, and remaining liability must tell the same story. Choose an explicit model, for example: 1. A full-value parent/order plus auditable child payment/renewal orders for each installment. 2. An initial order whose total is the immediate amount plus an owned plan entity that creates later installment orders. 3. WooCommerce Subscriptions when its recurring-product and lifecycle semantics actually match the product. Do not charge only a deposit against a full-value order and call `payment_complete()` as if the full balance was captured. Do not create later Stripe charges with no Woo-side auditable payment/order/refund record. Define cancellation, partial/full refund allocation, failed installment, chargeback, tax document, fulfillment, and over/underpayment behavior before implementation. ## Create every later charge as a new operation For a due installment, create a new server-side PaymentIntent with: ```text amount=<authoritative due amount in Stripe minor units> currency=<plan currency> customer=<owned Stripe Customer> payment_method=<owned reusable PaymentMethod> off_session=true confirm=true ``` Use an idempotency key derived from an immutable installment/payment-record ID and attempt policy, not from the current timestamp. Persist the PaymentIntent ID before considering the attempt dispatched. Reconcile timeouts by retrieving provider state before retrying. Scheduling is delivery, not exactly-once execution. Action Scheduler jobs may be delayed, repeated, or fail after Stripe succeeded. Make the remote charge idempotent and make local settlement safe under duplicate and out-of-order webhooks. ## Handle SCA and failure as normal states Upfront off-session setup reduces later authentication friction but cannot remove it. Model at least: ```text scheduled -> attempting -> processing/succeeded -> requires_customer_action -> declined/retryable -> terminal_failed/cancelled ``` When Stripe requires authentication, do not retry the same off-session request indefinitely. Notify the customer through a signed, expiring return flow, bring them on-session, confirm/replace the method, and resume only after verified success. Webhooks or active reconciliation, not the browser return alone, own final settlement. ## Woo token boundary A `WC_Payment_Token` is a local, user-owned projection of a provider method. Create/update it only after Stripe confirms a reusable method and the expected Customer relationship. Validate local token ownership before resolving its provider ID. Do not assume every reusable Stripe method is `WC_Payment_Token_CC`: - native Link is `type=link` and uses a Link token class; - a card funded through the Link wallet can remain `type=card` with `wallet.type=link`; - some redirect methods save a different reusable debit method; - some enabled methods cannot be reused at all. Use provider object type/capabilities, not an ID prefix or checkout label, to classify the method. ## Review checklist 1. Verify SetupIntent versus PaymentIntent choice and that exactly one operation owns the initial charge. 2. Verify Customer association, reusable method capability, explicit off-session consent, and durable shopper identity. 3. Compare Stripe amount/currency with the authoritative Woo payment record on initial and later attempts. 4. Verify provider idempotency, unique local installment records, signed webhooks, event deduplication, and timeout reconciliation. 5. Test first-time shopper with no Customer/token, saved method, guest rejection/account creation, classic checkout, Checkout Block, Link/card, SCA now, SCA later, decline, async processing, duplicate job, duplicate webhook, refund, cancellation, and method replacement. 6. Test Adaptive Pricing eligibility and opt-out for every custom deposit/installment cart shape. 7. Redact secrets, client secrets, payment identifiers where unnecessary, raw provider bodies, and billing data from logs. ## Cross-references - `wc-stripe-add-payment-method` for the no-charge My Account SetupIntent flow. - `wc-checkout-block-payment-method` for a custom gateway's Blocks adapter. - `wc-stripe-link-payments` for Link-specific representations and consent. - `wc-stripe-webhooks` for verified asynchronous settlement. - `wc-action-scheduler-jobs` for delivery, retries, and remote idempotency. - `wc-stripe-subscriptions` when WooCommerce Subscriptions owns the recurring contract. - See [references/stripe-future-payment-lifecycle.md](references/stripe-future-payment-lifecycle.md) for intent parameters, installed gateway contracts, and state/recovery details. ## References - Stripe SetupIntents: <https://docs.stripe.com/payments/setup-intents> - Stripe save-during-payment: <https://docs.stripe.com/payments/save-during-payment?payment-ui=elements> - Stripe BNPL model: <https://docs.stripe.com/payments/buy-now-pay-later> - Verified source paths: - `wp-content/plugins/woocommerce-gateway-stripe/includes/class-wc-stripe-intent-controller.php` - `wp-content/plugins/woocommerce-gateway-stripe/includes/class-wc-stripe-helper.php` - `wp-content/plugins/woocommerce-gateway-stripe/includes/class-wc-stripe-blocks-support.php` - `wp-content/plugins/woocommerce-gateway-stripe/includes/payment-methods/class-wc-stripe-upe-payment-gateway.php`
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.