wc-stripe-link-payments
Implement or audit Stripe Link behavior in the WooCommerce Stripe Gateway, especially code that assumes every `pm_...` or `stripe` token is a card. Distinguishes native Stripe PaymentMethod `type=link` and `WC_Payment_Token_Link` from `type=card` with `card.wallet.type=link`, and
Install
npx skills add https://github.com/Lonsdale201/wp-agent-skills/tree/main/woocommerce/wc-stripe-link-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 Link payments
Do not model Link as a card brand or a separate WooCommerce gateway. Determine the representation from the Stripe PaymentMethod object and the hydrated Woo token.
Distinguish the two Link representations
| Stripe object | Woo token | Durable fields | Meaning |
|---|---|---|---|
type = link, link.email |
WC_Payment_Token_Link |
type link, gateway stripe, token pm_..., email meta |
Native reusable Link PaymentMethod |
type = card, card.wallet.type = link |
WC_Stripe_Payment_Token_CC |
type CC, gateway stripe, pm_..., card/fingerprint fields, wallet_type=link |
A card-shaped PaymentMethod used through Link |
Both can have a pm_... ID. Never infer card shape from that prefix. The plugin deliberately does not expose Link branding for the second case: get_wallet_brand_label() returns a label only for Apple Pay and Google Pay.
Keep the identifier layers separate
- Stripe method type:
linkorcard. - Woo token type:
linkorCC. - Woo token class:
WC_Payment_Token_LinkorWC_Stripe_Payment_Token_CC. - Woo gateway ID on the token, order, and subscription:
stripe. - Saved-token form field:
wc-stripe-payment-token. - Gateway intent marker:
express_payment_type=link. - Express Checkout/WCS bookkeeping marker:
express_checkout_type=link. - Shopper-facing order title:
Link.
WC_Stripe_UPE_Payment_Method_Link::get_id() returns the Stripe method type link, not a Woo gateway ID, and is_available() deliberately returns false. The helper is not a standalone checkout gateway; there is no registered stripe_link gateway to store on an order.
Inspect tokens polymorphically
function myplugin_describe_stripe_token( WC_Payment_Token $token ): array {
if ( 'stripe' !== $token->get_gateway_id() ) {
return array();
}
$type = strtolower( $token->get_type() );
if ( 'link' === $type && method_exists( $token, 'get_email' ) ) {
return array(
'kind' => 'link',
'display' => $token->get_display_name(),
'email' => sanitize_email( $token->get_email() ),
);
}
if ( $token instanceof WC_Payment_Token_CC ) {
return array(
'kind' => 'card',
'display' => $token->get_display_name(),
'last4' => $token->get_last4(),
);
}
return array(
'kind' => $type,
'display' => $token->get_display_name(),
);
}
Use get_type() and capabilities such as method_exists() before type-specific getters. Do not call get_last4(), expiry, card brand, or fingerprint methods on a Link token.
Verified 10.9.0 quirk
WC_Payment_Token_Link::set_payment_method_type() calls set_prop( 'payment_method_type', ... ), but that property is absent from the class's extra_data. Consequently get_payment_method_type() still returns null in 10.9.0. Do not use it to classify Link; use get_type() === 'link'. Version-guard and retest if upstream adds the property.
Let the gateway create tokens
Preserve the plugin's Payment Element and intent orchestration. Add payment method and subscription change-payment use SetupIntents because they save without taking a purchase payment; normal paid checkout uses a PaymentIntent and, when future reuse is required and supported, setup_future_usage=off_session. The gateway retrieves the final Stripe PaymentMethod, selects its method handler, and creates the matching Woo token:
- native Link stores
link.emailand the PaymentMethod ID; - card stores safe card display fields and fingerprint;
- both use gateway ID
stripe.
Do not construct a partial Link token from an email or browser-submitted pm_.... Link email is display/deduplication data, not authentication or proof of ownership. If direct integration is unavoidable, retrieve the PaymentMethod server-side and verify its Stripe Customer, type, usable state, and current Woo user before invoking a version-pinned gateway service.
The plugin deduplicates native Link tokens by link.email, not PaymentMethod ID. A replacement remote pm_... for the same Link email can update the existing local token. Therefore neither Link email nor the local Woo token ID is a safe immutable business identifier.
Preserve Link's consent boundary
Link saving and WooCommerce-account tokenization are separate concepts:
- Link collects consent for the shopper's Link wallet inside Stripe's UI;
- a Woo saved method is a merchant-side local projection of a PaymentMethod attached to the Stripe Customer;
- having a Link wallet does not imply that a Woo token exists;
- deleting a Woo token does not delete the shopper's Link account.
When Link is enabled, the gateway hides the store-level save checkbox for card and Link because the Payment Element owns Link consent. Do not re-add or force that checkbox merely because custom UI expects wc-stripe-new-payment-method. Subscription and Add payment method paths have their own forced/setup logic.
Preserve the payment surface contracts
Payment Element
Link is offered inside the main Stripe/card surface rather than as a standalone Woo gateway. When card is selected and Link is enabled, the gateway requests both card and link intent types. Keep that pair; forcing only card breaks Link, SetupIntent, mandate, and some subscription paths.
Express Checkout Element
Express Link sends express_payment_type=link into the gateway intent path and express_checkout_type=link for Express Checkout/WCS bookkeeping, but the order gateway remains stripe. The final PaymentMethod may still need server-side inspection; do not treat either request marker as the provider token type.
Stripe 10.9 gives Link its own link_button_locations and link_button_size settings instead of inheriting Apple Pay/Google Pay appearance. Existing stores migrate the previous Express Checkout locations when the Link location option is absent. Read Link placement through WC_Stripe_Express_Checkout_Helper::get_button_locations( 'link' ) and height through get_link_button_height() only in version-pinned integration code; do not read the generic express_checkout_button_* options and assume they control Link. Supported locations include product, cart, checkout, and the WCS change-payment page when available.
The 10.9 Express Checkout client uses Woo Store API calls for shipping and variable-product cart mutations. Do not intercept removed legacy Stripe shipping/add-to-cart AJAX requests. Integrate custom cart data and checkout fields through Woo's Store API/classic-checkout extension surfaces, then test Link Express Checkout separately from Apple Pay/Google Pay.
Optimized Checkout
Multiple methods share the consolidated stripe gateway. Use the resolved PaymentMethod type and hydrated Woo token, not the selected container slug, as the type authority.
Validate saved-token requests
Prefer the installed gateway's normal checkout flow. In custom authenticated endpoints, treat the posted value as a local Woo token ID:
$token = WC_Payment_Tokens::get( absint( $request['token_id'] ?? 0 ) );
if (
! $token instanceof WC_Payment_Token ||
(int) $token->get_user_id() !== get_current_user_id() ||
'stripe' !== $token->get_gateway_id() ||
! in_array( strtolower( $token->get_type() ), array( 'cc', 'link' ), true )
) {
return new WP_Error( 'invalid_payment_method', __( 'Invalid payment method.', 'myplugin' ), array( 'status' => 403 ) );
}
$payment_method_id = $token->get_token( 'edit' ); // Server-side only.
Then let the gateway retrieve/use the PaymentMethod. Do not expose the pm_..., Link email, SetupIntent client secret, or Stripe Customer ID in logs or general REST output.
Account for reconciliation-on-read
The Stripe plugin filters WC_Payment_Tokens::get_customer_tokens() for logged-in requests. A token-list read can therefore:
- call Stripe for active reusable PaymentMethod types, or all reusable types under Optimized Checkout;
- create missing local Woo tokens;
- update a duplicate token's remote
pm_...; - delete local methods no longer returned for the active type set.
CLI/cron without a logged-in user does not take this synchronization path. The remote list is cached, and synchronization is skipped when the initial local token list already reaches the configured posts_per_page limit. Under Optimized Checkout, a remotely present but disabled or temporarily unavailable method is excluded from the returned list while its local token row is preserved. Outside Optimized Checkout, a disabled type can be outside the remote fetch and its local projection can still be cleaned up. Do not treat visibility as proof of remote detach, and do not assume token enumeration is pure, context-independent, complete, or cheap.
Depending on Optimized Checkout state, disabling and re-enabling Link can either preserve the hidden local row or recreate a cleaned-up projection with another Woo token ID. Store durable domain relationships against the order/subscription and remote PaymentMethod purpose, not a permanently stable local token-row ID. Load references/link-contract.md for the full sync, deletion/default, order, and subscription contracts.
Handle orders and subscriptions through stripe
For a Link payment, the gateway stores:
- Woo payment method ID
stripe; - title
Link; - Stripe Customer ID in gateway-owned metadata;
- native or card-shaped
pm_...source/payment-method ID.
Subscriptions also renew through gateway stripe; _stripe_source_id can contain a native Link pm_.... Do not switch the WCS gateway to stripe_link, infer Link from the gateway ID, or write _stripe_source_id directly.
On Express Checkout change-payment, the plugin replaces the subscription's attached Woo payment-token IDs with the local token matching the new Stripe PaymentMethod. Keep the WCS + Stripe orchestration so this, update-all consent, SetupIntent/SCA, titles, and hooks remain consistent.
Test matrix
- Native
type=linkversustype=card+wallet.type=link. - Classic Payment Element, Blocks, Optimized Checkout, and Express Checkout.
- Guest, logged-in, Add payment method, and one-time checkout save behavior.
- Existing saved Link selection through
wc-stripe-payment-token. - Duplicate Link email with the same and a replacement
pm_.... - Remote detach, Woo deletion, default change, disabled/re-enabled Link with Optimized Checkout on and off, cache refresh, and CLI versus logged-in listing.
- Subscription signup, off-session renewal, standard and Express change-payment, update-all consent, and 3DS return.
- Plugin disabled/missing custom token class; code must fail closed rather than assuming a CC token.
Cross-references
wc-stripe-add-payment-method: complete My Account form and SetupIntent contract.wc-stripe-subscriptions: renewal, WCS change-payment, SCA, and detached-token behavior.wc-stripe-webhooks: asynchronous settlement and idempotent order transitions.
References
- Verified source paths:
wp-content/plugins/woocommerce-gateway-stripe/includes/payment-methods/class-wc-stripe-upe-payment-method-link.phpwp-content/plugins/woocommerce-gateway-stripe/includes/payment-methods/class-wc-stripe-upe-payment-method-cc.phpwp-content/plugins/woocommerce-gateway-stripe/includes/payment-methods/class-wc-stripe-upe-payment-gateway.phpwp-content/plugins/woocommerce-gateway-stripe/includes/payment-methods/class-wc-stripe-express-checkout-element.phpwp-content/plugins/woocommerce-gateway-stripe/includes/payment-methods/class-wc-stripe-express-checkout-helper.phpwp-content/plugins/woocommerce-gateway-stripe/includes/admin/class-wc-stripe-link-controller.phpwp-content/plugins/woocommerce-gateway-stripe/includes/admin/stripe-settings.phpwp-content/plugins/woocommerce-gateway-stripe/includes/migrations/class-wc-stripe-migrate-link-button-locations.phpwp-content/plugins/woocommerce-gateway-stripe/includes/payment-tokens/class-wc-stripe-link-payment-token.phpwp-content/plugins/woocommerce-gateway-stripe/includes/payment-tokens/class-wc-stripe-cc-payment-token.phpwp-content/plugins/woocommerce-gateway-stripe/includes/payment-tokens/class-wc-stripe-payment-tokens.phpwp-content/plugins/woocommerce-gateway-stripe/includes/class-wc-stripe-customer.phpwp-content/plugins/woocommerce-gateway-stripe/includes/class-wc-stripe-intent-controller.phpwp-content/plugins/woocommerce-gateway-stripe/includes/compat/trait-wc-stripe-subscriptions.php
Files (wp-agent-skills)
-
agents
-
openai.yaml 251 B
interface: display_name: "WooCommerce Stripe Link Payments" short_description: "Handle Stripe Link tokens and payment flows" default_prompt: "Use $wc-stripe-link-payments to implement or audit Stripe Link payment-token handling in WooCommerce."
-
-
references
-
link-contract.md 12.2 KB
# Stripe Link integration contract Version scope: WooCommerce Stripe Gateway 10.9.0 with WooCommerce 11.0.1. Use this reference when a custom integration lists, stores, selects, deletes, defaults, or migrates Link methods, or changes a subscription to Link. ## Contents 1. [Object and identifier matrix](#object-and-identifier-matrix) 2. [Why Link is hidden behind the main gateway](#why-link-is-hidden-behind-the-main-gateway) 3. [Token construction and duplicate rules](#token-construction-and-duplicate-rules) 4. [Remote reconciliation](#remote-reconciliation) 5. [Checkout and intent behavior](#checkout-and-intent-behavior) 6. [Deletion and default behavior](#deletion-and-default-behavior) 7. [Orders and subscriptions](#orders-and-subscriptions) 8. [Security and compatibility](#security-and-compatibility) 9. [Regression checklist](#regression-checklist) ## Object and identifier matrix | Layer | Native Link | Card used through Link | |---|---|---| | Stripe PaymentMethod ID | `pm_...` | `pm_...` | | Stripe `type` | `link` | `card` | | Stripe details | `link.email` | `card.brand`, `last4`, expiry, fingerprint, `wallet.type=link` | | Woo class | `WC_Payment_Token_Link` | `WC_Stripe_Payment_Token_CC` | | Woo token type | `link` | `CC` | | Woo gateway ID | `stripe` | `stripe` | | Duplicate key | Link email | Stripe card fingerprint | | Display | `Stripe Link (email)` | ordinary card display in 10.9.0 | The local Link class extends base `WC_Payment_Token`, not `WC_Payment_Token_CC`. Its extra data contains only `email`. It has no card brand, last4, expiry, or fingerprint contract. The plugin maps lowercase token type `link` explicitly through `woocommerce_payment_token_class`. Without the active Stripe plugin/classmap/filter, core would derive a different class name and cannot reliably hydrate this provider-specific token. Treat plugin availability as a runtime requirement. ### `payment_method_type` accessor caveat The Link class defines `set_payment_method_type()` and `get_payment_method_type()`, and creation code calls the setter. However, `payment_method_type` is absent from the class's `extra_data`, while `WC_Data::set_prop()` ignores undeclared properties. Runtime verification on 10.9.0 therefore yields: ```text get_type() => "link" get_payment_method_type() => null ``` Use `get_type()`. Treat the accessor as a version-specific upstream inconsistency, not a usable contract. ## Why Link is hidden behind the main gateway `WC_Stripe_UPE_Payment_Method_Link::get_id()` returns the provider method type `link`. The helper sets itself reusable but returns false from `is_available()`. Link appears inside Stripe-owned UI: - the standard Payment Element associated with the card/main gateway; - the Express Checkout Element; - Optimized Checkout's consolidated element. The method helper exists so the main gateway can describe, validate, save, and title Link. Its `link` ID is not a Woo gateway registration, and no `stripe_link` payment gateway is registered. Consequently: - token gateway: `stripe`; - order/subscription gateway: `stripe`; - standard saved-token input: `wc-stripe-payment-token`; - title may be `Link`; - `stripe_link` must not be used as a business discriminator. Determine Link from the final Stripe PaymentMethod/Woo token or from the trusted order title/type metadata maintained by the gateway, depending on the task. ## Token construction and duplicate rules The Link method's creation path stores: ```text class WC_Payment_Token_Link type link gateway_id stripe token Stripe pm_... ID user_id WordPress user ID email Stripe payment_method.link.email ``` Creation must follow a server-retrieved Stripe PaymentMethod. Do not build this tuple from customer POST data. `WC_Stripe_Payment_Tokens::get_duplicate_token()` loads up to 100 local tokens for the customer/gateway and delegates comparison to each token. Link equality requires: ```text payment_method.type == link payment_method.link.email == token.email ``` It does not compare PaymentMethod IDs. During remote synchronization, if a matching email is found and the old local `pm_...` is absent from the returned remote IDs, the local row is updated to the new PaymentMethod ID. This makes the email a reconciliation key, not an authorization key or globally unique domain identity. ## Remote reconciliation `WC_Stripe_Payment_Tokens` filters `woocommerce_get_customer_payment_tokens`. On a logged-in request it can reconcile the local token list with the Stripe Customer: 1. accept only reusable Stripe gateway IDs; 2. stop when the initial local token count reaches `posts_per_page`; 3. categorize stored and deprecated local tokens by provider ID; 4. fetch all remote Stripe Customer PaymentMethods, cached under an all-methods transient; 5. filter to active reusable types, or inspect all reusable types under Optimized Checkout; 6. preserve matching `pm_...` rows; 7. create missing type-specific Woo tokens; 8. collapse duplicates using the type-specific comparator; 9. delete local rows not represented in the active remote result, except that Optimized Checkout preserves remotely present rows for disabled or temporarily unavailable methods while excluding them from the returned list. The cleanup temporarily removes the normal remote-detach deletion action. Thus local reconciliation cleanup does not detach the PaymentMethod from Stripe. Practical consequences: - `get_customer_tokens()` can perform network I/O and local writes; - CLI/cron without a logged-in user sees local state only; - transient state can delay remote changes until cleared by gateway operations/expiry; - outside Optimized Checkout, disabled Link can be outside the active type set, so its local row can be removed and later recreated; under Optimized Checkout the remotely present row is preserved but hidden from the result; - local token IDs are projections, not permanent external identifiers; - repeated listing in a loop risks expensive provider synchronization. Use `WC_Payment_Tokens::get_tokens()` with explicit arguments when the task intentionally needs raw local rows and must avoid the customer-token filter. Use the gateway's normal UI/service when reconciliation is desired; direct Stripe internal service calls require version pinning. ## Checkout and intent behavior ### Dedicated Link appearance settings in 10.9 Express Link now uses `link_button_locations` and `link_button_size`, separate from Apple Pay/Google Pay's `express_checkout_button_locations` and `express_checkout_button_size`. On upgrade, the migration initializes a missing Link location setting from the prior Express Checkout locations. The helper routes `get_button_locations( 'link' )` to the Link option and `get_link_button_height()` to Link's size. Do not assume changing the generic Express Checkout appearance changes Link. If a version-pinned integration reads these internal helpers, test product, cart, checkout, and WCS change-payment locations. Stripe 10.9 also moved Express Checkout shipping and variable-product cart mutations to Woo Store API calls, so custom field/cart extensions must use Woo extension points rather than legacy Stripe AJAX interception. ### Standard Payment Element The submitted Woo gateway remains `stripe`, so the selected internal type initially normalizes to `card`. When Link is enabled, intent creation expands that selection to: ```php array( 'card', 'link' ) ``` The final PaymentMethod object is authoritative. `set_payment_method_title_for_order()` detects `type=link`, forces order gateway `stripe`, and stores title `Link`. The intent controller also treats a card selection as requiring Link mandate data when Link is enabled. A custom intent builder that strips `link` or bypasses the gateway's mandate handling is not equivalent. ### Express Checkout The gateway intent request carries `express_payment_type=link`; the Express Checkout and WCS change-payment orchestration also uses `express_checkout_type=link`. For intent creation Link again permits both card and Link types. These markers express the UI route, not a promise that every returned provider object has identical shape. ### Save behavior The gateway hides its own save checkbox for card/Link while Link is enabled because Stripe's Payment Element owns Link wallet consent. That does not guarantee a Woo token after every Link payment. Woo token creation occurs when the gateway's merchant-side save path is active, including Add payment method SetupIntent, an automatic subscription requirement, an explicit supported save path, or later remote reconciliation of an attached PaymentMethod. ## Deletion and default behavior Normal Woo token deletion invokes the Stripe token manager. For reusable Stripe gateways, including native Link, it detaches the stored `pm_...` from the Stripe Customer when provider-detach policy allows it. Do not detach manually and then delete the Woo token; that duplicates the remote operation. Reconciliation cleanup intentionally disables that detach listener because a local stale/disabled projection should not necessarily delete the remote method. Setting a Woo token as default triggers Stripe synchronization. Since Link token values begin with `pm_`, the plugin sets the Stripe Customer's default PaymentMethod. Keep the core ownership/nonce checks and normal token action rather than updating `is_default` or Stripe customer fields independently. Deleting a local Link token does not delete the shopper's consumer Link account. It removes/detaches the merchant's Stripe Customer PaymentMethod relationship according to the gateway behavior. ## Orders and subscriptions Orders store the main gateway `stripe` and title `Link`. Gateway-owned metadata stores the Stripe Customer and PaymentMethod/source ID. The PaymentMethod ID alone does not encode whether its object type is Link or card. For WooCommerce Subscriptions: - subscription gateway remains `stripe`; - `_stripe_customer_id` identifies the Stripe Customer; - `_stripe_source_id` can be a native Link or card-shaped `pm_...`; - scheduled renewals use the normal `woocommerce_scheduled_subscription_payment_stripe` handler; - display logic retrieves the provider object and renders `Via Stripe Link (email)` for native Link; - payment-method changes must run WCS and Stripe orchestration, not raw meta updates. Express change-payment has extra bookkeeping. It forces the old saved-token selector to `new`, clears implicit update-all consent, records the express type/payment-method ID across redirects, restores the `Link` title, and replaces the subscription's attached Woo token IDs through the order data store. Bypassing this can leave `_stripe_source_id`, `_payment_tokens`, visible title, and renewal behavior inconsistent. ## Security and compatibility - Treat local token ID, provider `pm_...`, Link email, and Stripe Customer ID as separate identifiers. - Verify local token ownership and gateway before use. - Retrieve the provider object server-side when its actual type/customer matters. - Do not authorize by Link email; it is PII and a mutable display/deduplication field. - Do not expose raw provider IDs, customer IDs, client secrets, or full token objects through custom REST responses/logs. - Avoid hard type hints to plugin classes before confirming Stripe is active and the class is loadable. - Fail closed for unknown custom token types; do not cast them into a fake CC token. - Pin and retest direct calls into Stripe gateway classes because they are extension internals. ## Regression checklist - Hydrate `link` through the token-class filter and verify `get_type()`/`get_email()`. - Verify `get_payment_method_type()` behavior against the installed plugin version. - Verify native Link versus card wallet Link without calling the wrong getters. - Exercise same-email duplicate and replacement-`pm_...` reconciliation. - Compare logged-in customer listing with CLI/raw-local enumeration. - Toggle Link off/on and observe local projection/recreated token ID. - Test normal delete, reconciliation cleanup, and default selection separately. - Test Payment Element and Express Checkout across classic, Blocks, and Optimized Checkout. - Test one-time, Add payment method, subscription signup, renewal, change-payment, update-all, and 3DS return. - Disable the Stripe plugin and verify custom code handles unhydratable provider-specific tokens safely.
-
-
SKILL.md 13.3 KB
--- name: wc-stripe-link-payments description: Implement or audit Stripe Link behavior in the WooCommerce Stripe Gateway, especially code that assumes every `pm_...` or `stripe` token is a card. Distinguishes native Stripe PaymentMethod `type=link` and `WC_Payment_Token_Link` from `type=card` with `card.wallet.type=link`, and covers gateway/type identifiers, Payment Element and Express Checkout, dedicated Link button settings, save consent, SetupIntents, remote-to-Woo token reconciliation, duplicate detection, checkout request validation, deletion/defaulting, orders, subscriptions, and Link-specific tests. Use for Link by Stripe, `WC_Payment_Token_Link`, `link.email`, `wallet_type=link`, `link_button_locations`, saved Link methods, or Stripe token type errors. 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 Link payments Do not model Link as a card brand or a separate WooCommerce gateway. Determine the representation from the Stripe PaymentMethod object and the hydrated Woo token. ## Distinguish the two Link representations | Stripe object | Woo token | Durable fields | Meaning | |---|---|---|---| | `type = link`, `link.email` | `WC_Payment_Token_Link` | type `link`, gateway `stripe`, token `pm_...`, email meta | Native reusable Link PaymentMethod | | `type = card`, `card.wallet.type = link` | `WC_Stripe_Payment_Token_CC` | type `CC`, gateway `stripe`, `pm_...`, card/fingerprint fields, `wallet_type=link` | A card-shaped PaymentMethod used through Link | Both can have a `pm_...` ID. Never infer card shape from that prefix. The plugin deliberately does not expose Link branding for the second case: `get_wallet_brand_label()` returns a label only for Apple Pay and Google Pay. ## Keep the identifier layers separate - Stripe method type: `link` or `card`. - Woo token type: `link` or `CC`. - Woo token class: `WC_Payment_Token_Link` or `WC_Stripe_Payment_Token_CC`. - Woo gateway ID on the token, order, and subscription: `stripe`. - Saved-token form field: `wc-stripe-payment-token`. - Gateway intent marker: `express_payment_type=link`. - Express Checkout/WCS bookkeeping marker: `express_checkout_type=link`. - Shopper-facing order title: `Link`. `WC_Stripe_UPE_Payment_Method_Link::get_id()` returns the Stripe method type `link`, not a Woo gateway ID, and `is_available()` deliberately returns false. The helper is not a standalone checkout gateway; there is no registered `stripe_link` gateway to store on an order. ## Inspect tokens polymorphically ```php function myplugin_describe_stripe_token( WC_Payment_Token $token ): array { if ( 'stripe' !== $token->get_gateway_id() ) { return array(); } $type = strtolower( $token->get_type() ); if ( 'link' === $type && method_exists( $token, 'get_email' ) ) { return array( 'kind' => 'link', 'display' => $token->get_display_name(), 'email' => sanitize_email( $token->get_email() ), ); } if ( $token instanceof WC_Payment_Token_CC ) { return array( 'kind' => 'card', 'display' => $token->get_display_name(), 'last4' => $token->get_last4(), ); } return array( 'kind' => $type, 'display' => $token->get_display_name(), ); } ``` Use `get_type()` and capabilities such as `method_exists()` before type-specific getters. Do not call `get_last4()`, expiry, card brand, or fingerprint methods on a Link token. ### Verified 10.9.0 quirk `WC_Payment_Token_Link::set_payment_method_type()` calls `set_prop( 'payment_method_type', ... )`, but that property is absent from the class's `extra_data`. Consequently `get_payment_method_type()` still returns `null` in 10.9.0. Do not use it to classify Link; use `get_type() === 'link'`. Version-guard and retest if upstream adds the property. ## Let the gateway create tokens Preserve the plugin's Payment Element and intent orchestration. Add payment method and subscription change-payment use SetupIntents because they save without taking a purchase payment; normal paid checkout uses a PaymentIntent and, when future reuse is required and supported, `setup_future_usage=off_session`. The gateway retrieves the final Stripe PaymentMethod, selects its method handler, and creates the matching Woo token: - native Link stores `link.email` and the PaymentMethod ID; - card stores safe card display fields and fingerprint; - both use gateway ID `stripe`. Do not construct a partial Link token from an email or browser-submitted `pm_...`. Link email is display/deduplication data, not authentication or proof of ownership. If direct integration is unavoidable, retrieve the PaymentMethod server-side and verify its Stripe Customer, type, usable state, and current Woo user before invoking a version-pinned gateway service. The plugin deduplicates native Link tokens by `link.email`, not PaymentMethod ID. A replacement remote `pm_...` for the same Link email can update the existing local token. Therefore neither Link email nor the local Woo token ID is a safe immutable business identifier. ## Preserve Link's consent boundary Link saving and WooCommerce-account tokenization are separate concepts: - Link collects consent for the shopper's Link wallet inside Stripe's UI; - a Woo saved method is a merchant-side local projection of a PaymentMethod attached to the Stripe Customer; - having a Link wallet does not imply that a Woo token exists; - deleting a Woo token does not delete the shopper's Link account. When Link is enabled, the gateway hides the store-level save checkbox for card and Link because the Payment Element owns Link consent. Do not re-add or force that checkbox merely because custom UI expects `wc-stripe-new-payment-method`. Subscription and Add payment method paths have their own forced/setup logic. ## Preserve the payment surface contracts ### Payment Element Link is offered inside the main Stripe/card surface rather than as a standalone Woo gateway. When card is selected and Link is enabled, the gateway requests both `card` and `link` intent types. Keep that pair; forcing only `card` breaks Link, SetupIntent, mandate, and some subscription paths. ### Express Checkout Element Express Link sends `express_payment_type=link` into the gateway intent path and `express_checkout_type=link` for Express Checkout/WCS bookkeeping, but the order gateway remains `stripe`. The final PaymentMethod may still need server-side inspection; do not treat either request marker as the provider token type. Stripe 10.9 gives Link its own `link_button_locations` and `link_button_size` settings instead of inheriting Apple Pay/Google Pay appearance. Existing stores migrate the previous Express Checkout locations when the Link location option is absent. Read Link placement through `WC_Stripe_Express_Checkout_Helper::get_button_locations( 'link' )` and height through `get_link_button_height()` only in version-pinned integration code; do not read the generic `express_checkout_button_*` options and assume they control Link. Supported locations include product, cart, checkout, and the WCS change-payment page when available. The 10.9 Express Checkout client uses Woo Store API calls for shipping and variable-product cart mutations. Do not intercept removed legacy Stripe shipping/add-to-cart AJAX requests. Integrate custom cart data and checkout fields through Woo's Store API/classic-checkout extension surfaces, then test Link Express Checkout separately from Apple Pay/Google Pay. ### Optimized Checkout Multiple methods share the consolidated `stripe` gateway. Use the resolved PaymentMethod type and hydrated Woo token, not the selected container slug, as the type authority. ## Validate saved-token requests Prefer the installed gateway's normal checkout flow. In custom authenticated endpoints, treat the posted value as a local Woo token ID: ```php $token = WC_Payment_Tokens::get( absint( $request['token_id'] ?? 0 ) ); if ( ! $token instanceof WC_Payment_Token || (int) $token->get_user_id() !== get_current_user_id() || 'stripe' !== $token->get_gateway_id() || ! in_array( strtolower( $token->get_type() ), array( 'cc', 'link' ), true ) ) { return new WP_Error( 'invalid_payment_method', __( 'Invalid payment method.', 'myplugin' ), array( 'status' => 403 ) ); } $payment_method_id = $token->get_token( 'edit' ); // Server-side only. ``` Then let the gateway retrieve/use the PaymentMethod. Do not expose the `pm_...`, Link email, SetupIntent client secret, or Stripe Customer ID in logs or general REST output. ## Account for reconciliation-on-read The Stripe plugin filters `WC_Payment_Tokens::get_customer_tokens()` for logged-in requests. A token-list read can therefore: 1. call Stripe for active reusable PaymentMethod types, or all reusable types under Optimized Checkout; 2. create missing local Woo tokens; 3. update a duplicate token's remote `pm_...`; 4. delete local methods no longer returned for the active type set. CLI/cron without a logged-in user does not take this synchronization path. The remote list is cached, and synchronization is skipped when the initial local token list already reaches the configured `posts_per_page` limit. Under Optimized Checkout, a remotely present but disabled or temporarily unavailable method is excluded from the returned list while its local token row is preserved. Outside Optimized Checkout, a disabled type can be outside the remote fetch and its local projection can still be cleaned up. Do not treat visibility as proof of remote detach, and do not assume token enumeration is pure, context-independent, complete, or cheap. Depending on Optimized Checkout state, disabling and re-enabling Link can either preserve the hidden local row or recreate a cleaned-up projection with another Woo token ID. Store durable domain relationships against the order/subscription and remote PaymentMethod purpose, not a permanently stable local token-row ID. Load [references/link-contract.md](references/link-contract.md) for the full sync, deletion/default, order, and subscription contracts. ## Handle orders and subscriptions through `stripe` For a Link payment, the gateway stores: - Woo payment method ID `stripe`; - title `Link`; - Stripe Customer ID in gateway-owned metadata; - native or card-shaped `pm_...` source/payment-method ID. Subscriptions also renew through gateway `stripe`; `_stripe_source_id` can contain a native Link `pm_...`. Do not switch the WCS gateway to `stripe_link`, infer Link from the gateway ID, or write `_stripe_source_id` directly. On Express Checkout change-payment, the plugin replaces the subscription's attached Woo payment-token IDs with the local token matching the new Stripe PaymentMethod. Keep the WCS + Stripe orchestration so this, update-all consent, SetupIntent/SCA, titles, and hooks remain consistent. ## Test matrix 1. Native `type=link` versus `type=card` + `wallet.type=link`. 2. Classic Payment Element, Blocks, Optimized Checkout, and Express Checkout. 3. Guest, logged-in, Add payment method, and one-time checkout save behavior. 4. Existing saved Link selection through `wc-stripe-payment-token`. 5. Duplicate Link email with the same and a replacement `pm_...`. 6. Remote detach, Woo deletion, default change, disabled/re-enabled Link with Optimized Checkout on and off, cache refresh, and CLI versus logged-in listing. 7. Subscription signup, off-session renewal, standard and Express change-payment, update-all consent, and 3DS return. 8. Plugin disabled/missing custom token class; code must fail closed rather than assuming a CC token. ## Cross-references - `wc-stripe-add-payment-method`: complete My Account form and SetupIntent contract. - `wc-stripe-subscriptions`: renewal, WCS change-payment, SCA, and detached-token behavior. - `wc-stripe-webhooks`: asynchronous settlement and idempotent order transitions. ## References - Verified source paths: - `wp-content/plugins/woocommerce-gateway-stripe/includes/payment-methods/class-wc-stripe-upe-payment-method-link.php` - `wp-content/plugins/woocommerce-gateway-stripe/includes/payment-methods/class-wc-stripe-upe-payment-method-cc.php` - `wp-content/plugins/woocommerce-gateway-stripe/includes/payment-methods/class-wc-stripe-upe-payment-gateway.php` - `wp-content/plugins/woocommerce-gateway-stripe/includes/payment-methods/class-wc-stripe-express-checkout-element.php` - `wp-content/plugins/woocommerce-gateway-stripe/includes/payment-methods/class-wc-stripe-express-checkout-helper.php` - `wp-content/plugins/woocommerce-gateway-stripe/includes/admin/class-wc-stripe-link-controller.php` - `wp-content/plugins/woocommerce-gateway-stripe/includes/admin/stripe-settings.php` - `wp-content/plugins/woocommerce-gateway-stripe/includes/migrations/class-wc-stripe-migrate-link-button-locations.php` - `wp-content/plugins/woocommerce-gateway-stripe/includes/payment-tokens/class-wc-stripe-link-payment-token.php` - `wp-content/plugins/woocommerce-gateway-stripe/includes/payment-tokens/class-wc-stripe-cc-payment-token.php` - `wp-content/plugins/woocommerce-gateway-stripe/includes/payment-tokens/class-wc-stripe-payment-tokens.php` - `wp-content/plugins/woocommerce-gateway-stripe/includes/class-wc-stripe-customer.php` - `wp-content/plugins/woocommerce-gateway-stripe/includes/class-wc-stripe-intent-controller.php` - `wp-content/plugins/woocommerce-gateway-stripe/includes/compat/trait-wc-stripe-subscriptions.php`
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.