Claude Skill

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,

LLM Mart · 0 points · 0 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download lonsdale201-wp-agent-skills-woocommerce_wc-stripe-future-payments-52f6020.zip · 9 KB
Part of lonsdale201/wp-agent-skills — 226 skills

Install

skills CLI npx skills add https://github.com/Lonsdale201/wp-agent-skills/tree/main/woocommerce/wc-stripe-future-payments
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install lonsdale201-wp-agent-skills@llmmart
Git 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:

  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:

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=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 for intent parameters, installed gateway contracts, and state/recovery details.

References

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.

No comments yet.

Reviews (0)

No reviews yet.

Related