Claude Skill

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

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-link-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-link-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 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

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:

$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 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
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.

No comments yet.

Reviews (0)

No reviews yet.

Related