Skip to content

feat(connector): [STRIPE] Google Pay decryption, and route predecrypted wallets through /v1/tokens - #2120

Open
pixincreate wants to merge 10 commits into
mainfrom
feat/stripe-wallet-decrypt-recurring
Open

feat(connector): [STRIPE] Google Pay decryption, and route predecrypted wallets through /v1/tokens#2120
pixincreate wants to merge 10 commits into
mainfrom
feat/stripe-wallet-decrypt-recurring

Conversation

@pixincreate

@pixincreate pixincreate commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

What

Two things: adding Google Pay decryption support for Stripe, and fixing a pre-existing bug that meant no predecrypted wallet ever worked on this connector.

1. Google Pay decryption was missing

Stripe only accepts a Google Pay credential for later merchant-initiated transactions as a network token (PAN + cryptogram). TryFrom<&GooglePayWalletData> read only the encrypted token and errored on GpayTokenizationData::Decrypted, so Google Pay with setup_future_usage could not be set up at all.

Adds StripeWallet::GooglePayPredecryptToken / StripeGooglePayPredecrypt (tokenization_method = android_pay). A PAN_ONLY token — no cryptogram or ECI — falls back to a plain card. auth_type is threaded through the wallet conversions so that fallback can set request_three_d_secure.

2. Predecrypted wallets were posted to the wrong endpoint

Found while testing the above. Apple Pay predecrypt was equally broken and predates this branch.

The predecrypt structs serialize a top-level card[number] / card[cryptogram] / card[eci] / card[tokenization_method] block, which is only legal on /v1/tokens. Authorize flattened it straight into /v1/payment_intents. Replaying the exact bytes:

Endpoint Result
POST /v1/tokens 200 — returns tok_..., card.tokenization_method: "android_pay"
POST /v1/payment_methods 400 — unknown parameters tokenization_method, eci, cryptogram
POST /v1/payment_intents (what this connector did) 400 — parameter_unknown, param: "card"

Fix:

  • The PaymentMethodToken flow's get_url splits by payment method: Walletv1/tokens, everything else stays v1/payment_methods. Card tokenization already round-trips coherently (/v1/payment_methodspm_payment_method=pm_) and is left alone.
  • New StripeCardTokenPayment (payment_method_data[card][token]). Authorize splits an incoming PaymentMethodToken on the tok_ prefix — a token goes to payment_method_data[card][token], a pm_ still goes to payment_method. The prefix determines the Stripe object type, which determines the legal parameter.
  • A decrypted wallet passed directly to Authorize fails fast with an actionable message instead of a request Stripe is guaranteed to reject.

3. Predecrypted Google Pay was excluded from tokenization

should_do_payment_method_token returned false for Google Pay unconditionally, so the composite service skipped tokenization and the decrypted payload hit the guard above — the feature was unreachable through the normal flow. Apple Pay was unaffected.

Removing the exclusion outright would break encrypted Google Pay, which must go direct to Authorize as GooglepayToken. The trait signature could not express the difference, so should_do_payment_method_token gains an is_wallet_pre_decrypted flag (default false, so the nine other implementors are behaviourally untouched), and the composite service derives it from the request payload's wallet oneof. Stripe's rule becomes: wallet and (not Google Pay or the payload is predecrypted).

Net routing, which matches the connector's documented contract:

Case Tokenized first?
Apple Pay yes
Google Pay encrypted no — direct to Authorize as GooglepayToken
Google Pay decrypted yes — /v1/tokens then payment_method_data[card][token]

No inline tokenization from Authorize. Stripe tokens are single-use and short-lived, so an inline mint would leak a token on every Authorize retry, and the composite Authorize has no tokenize step to chain through.

Testing — real Stripe sandbox

Driven through the composite path (the one that consults should_do_payment_method_token), not by calling Tokenize by hand — calling it manually is what masked bug 3 the first time.

# Check Endpoint(s) Result
a Card Authorize baseline /v1/payment_intents, payment_method_data[card][number] CHARGED pi_3U4Gj8D5R7gDAGff1MD8kEfX
b Google Pay decrypted, cryptogram + ECI /v1/tokenstok_, then payment_method_data[card][token] CHARGED pi_3U4Gk2D5R7gDAGff0Y7Ey1yj — was 400
c Google Pay encrypted no tokenization call; direct Authorize as GooglepayToken CHARGED pi_3U4GknD5R7gDAGff0zLUkNMQ
d Apple Pay decrypted /v1/tokenstok_, then payment_method_data[card][token] CHARGED pi_3U4Gl6D5R7gDAGff0fho2ZIv — was 400
e Google Pay decrypted + setup_future_usage=OFF_SESSION as (b) plus setup_future_usage=off_session CHARGED pi_3U4GlgD5R7gDAGff1ypGbKez, mandate pm_1U4GlgD5R7gDAGffNEmYd3Ow
f Card tokenization regression /v1/payment_methodspm_, payment_method=pm_ CHARGED pi_3U4CDrD5R7gDAGff0r5570px
g Guard decrypted wallet posted straight to Authorize rejected locally, no HTTP emitted

Case (c) is the regression risk of change 3 and is clean in both directions.

Outgoing body for (b), POST /v1/tokens:

card[number]=...&card[exp_year]=2030&card[exp_month]=08
&card[cryptogram]=...&card[eci]=05&card[tokenization_method]=android_pay

then POST /v1/payment_intents:

payment_method_data[type]=card&payment_method_data[card][token]=tok_...
&capture_method=automatic&expand[0]=latest_charge

cargo clippy -p connector-integration --all-targets, -p composite-service -p interfaces -p grpc-server, cargo check --workspace --all-targets and cargo fmt --check all clean.

Known adjacent issue, deliberately not changed

RepeatPayment has the same payment_method = token assignment and would mishandle a tok_. Unreachable in practice — MIT uses a stored mandate id — so it was left rather than changing an untested path, but it is the next place this bug class surfaces.

🤖 Generated with Claude Code

…token

Stripe only accepts a Google Pay credential for later merchant-initiated
transactions when it is presented as a network token (PAN + cryptogram).
The connector previously read only the encrypted wallet token and errored
on GpayTokenizationData::Decrypted, so a Google Pay payment with
setup_future_usage could not be set up at all.

Add StripeGooglePayPredecrypt and route the decrypted token to it, falling
back to a plain card when the token carries no cryptogram/ECI. auth_type is
threaded through the wallet conversions so that fallback can set
request_three_d_secure. Apple Pay predecrypt already worked and is unchanged.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@pixincreate pixincreate added rust Pull requests that update rust code enhancement New feature or request PMT Add new payment method labels Aug 13, 2026
@pixincreate pixincreate self-assigned this Aug 13, 2026
pixincreate and others added 2 commits August 13, 2026 23:34
Record why this arm builds the non-generic card variant where hyperswitch
builds StripeCardData, and that the two serialize identically, so the
divergence does not read as an unexplained drift from OSS.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Predecrypted Apple Pay and Google Pay have never worked on Stripe in UCS.
`StripeApplePayPredecrypt` / `StripeGooglePayPredecrypt` serialize a top-level
`card[number]` + `card[cryptogram]` + `card[eci]` + `card[tokenization_method]`
block, and Authorize flattened that straight into `/v1/payment_intents`, which
answers `parameter_unknown` on `card`. Replaying the exact bytes UCS produces
against the sandbox: `/v1/tokens` 200s and returns a `tok_` with
`card.tokenization_method` set, `/v1/payment_methods` 400s on
`tokenization_method`/`eci`/`cryptogram`, `/v1/payment_intents` 400s on `card`.
Apple Pay predecrypt was broken the same way and predates the Google Pay work.

Mirror hyperswitch OSS:

* The `PaymentMethodToken` flow now posts wallets to `/v1/tokens` and keeps raw
  cards on `/v1/payment_methods`. The two endpoints mint different object
  families (`tok_` vs `pm_`) that Authorize spends on different parameters, so
  the split has to be per payment method rather than a blanket switch. Card
  tokenization keeps the `pm_` contract UCS already ships.
* Authorize now recognises a `tok_` and sends it as
  `payment_method_data[card][token]` (the shape `GooglePayToken` /
  `ApplepayPayment` already used) instead of `payment_method=`, which only
  accepts a PaymentMethod id.
* Authorize refuses a decrypted wallet credential passed directly, with a
  message pointing at `PaymentMethodService/Tokenize`, rather than posting a
  request Stripe is guaranteed to reject.

Tokenization stays caller-driven — Authorize does not mint a token inline. Same
call we made on Checkout: Stripe tokens are single-use and short-lived, so an
inline second network call turns every Authorize retry into a fresh token mint
and leaks a token per attempt. UCS also has no connector-level flow chaining;
composite Authorize orchestrates access-token/customer/order/authenticate but
has no tokenize step, so inline minting would need proto and composite-service
changes well outside the connector.

Verified against the Stripe sandbox: card Authorize CHARGED (unchanged), Google
Pay decrypted with cryptogram+ECI now CHARGED via `tok_`, Google Pay PAN_ONLY
still CHARGED through the plain-card fallback, Apple Pay decrypted now CHARGED
via `tok_`, Google Pay decrypted with `setup_future_usage=off_session` still
emits `setup_future_usage=off_session` and CHARGED, and card tokenize -> pm_ ->
Authorize still CHARGED.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@pixincreate pixincreate changed the title feat(connector): [STRIPE] send decrypted Google Pay token as network token feat(connector): [STRIPE] Google Pay decryption, and route predecrypted wallets through /v1/tokens Aug 14, 2026
…crypt-recurring

* origin/main:
  chore(version): 2026.08.14.0
  feat(observability): add typed connector request/response fields (#2036)
  fix(connectors): [tsys_transit] add support for psync and rsync (#2100)
  chore(version): 2026.08.13.1
  fix(grabpay,maya): added superposition urls (#2118)
@pixincreate
pixincreate marked this pull request as ready for review August 14, 2026 06:24
@pixincreate
pixincreate requested a review from a team as a code owner August 14, 2026 06:24
Auto-applied by CI:
- cargo +nightly fmt --all
- make -C sdk generate (if applicable)
- make docs (if applicable)

This commit was automatically generated by GitHub Actions.
@hyperswitch-bot
hyperswitch-bot Bot requested a review from a team as a code owner August 14, 2026 06:45
Comment thread crates/integrations/connector-integration/src/connectors/stripe.rs Outdated
pixincreate and others added 2 commits August 14, 2026 14:21
…e flow

`should_do_payment_method_token` excluded Google Pay outright, so a decrypted
Google Pay payload never reached Tokenize: the composite flow sent it straight
to Authorize, where the `is_tokens_endpoint_only` guard refused it. The Google
Pay decryption support was therefore unreachable through the normal flow.

Deleting the exclusion would have been wrong in the other direction. An
*encrypted* Google Pay payload is Google's own token and Stripe consumes it
inline on `/v1/payment_intents` as `payment_method_data[card][token]`; only the
*decrypted* credential (PAN + cryptogram + ECI) is restricted to `/v1/tokens`.
The two shapes collapse onto the same `Wallet` + `GooglePay` pair, which is why
the old signature could not express the rule.

`should_do_payment_method_token` now also receives whether the wallet payload
arrived pre-decrypted, computed at the composite call site from the request's
`google_pay_sdk` / `apple_pay_sdk` oneof. Stripe answers wallet AND (not Google
Pay OR pre-decrypted), which is what hyperswitch's config expresses as
`payment_method = "wallet"` + `payment_method_type = { list = "google_pay",
type = "disable_only" }` + `google_pay_pre_decrypt_flow =
"connector_tokenization"`. Every other implementor ignores the new parameter and
is behaviourally unchanged; the default impl still returns false.

Verified against the Stripe sandbox through `/composite/payment_methods/get`:
decrypted Google Pay and Apple Pay now mint a `tok_…` on `/v1/tokens` and the
follow-up Authorize charges it, encrypted Google Pay performs no tokenization
call at all and charges directly on `/v1/payment_intents`, and a decrypted
credential posted straight to Authorize is still refused locally without any
outbound request.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…erences

Addresses review feedback on #2120.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Shubhodip900
Shubhodip900 previously approved these changes Aug 14, 2026
pixincreate and others added 2 commits August 14, 2026 19:01
…guard every flow

Three fixes to the decrypted-wallet routing added in this branch.

1. A cryptogram-less decrypted wallet is no longer treated as tokenizable.
   `is_wallet_payload_pre_decrypted` reported `true` for any decrypted wallet
   payload, so Stripe's `should_do_payment_method_token` demanded Tokenize for a
   PAN_ONLY Google Pay credential. The Tokenize builder can only produce the
   PaymentIntent card block (`payment_method_data[card][…]` /
   `payment_method_options[card][…]`) for it, and `/v1/tokens` accepts only
   `card[number]` / `card[exp_month]` / `card[exp_year]` / `card[cvc]` — a
   guaranteed 400 on exactly the path the flag routed traffic onto. The flag now
   reports the narrower, honest fact — the payload carries a decrypted *network
   token*, i.e. a PAN with a cryptogram — and is renamed
   `is_wallet_payload_decrypted_network_token` so that "pre-decrypted" is no
   longer conflated with "tokenizable". A PAN_ONLY credential stays on the inline
   Authorize path it already worked on.

2. The `/v1/tokens`-only guard now covers every flow that flattens a
   `StripePaymentMethodData` into a non-token request body, not just Authorize.
   The same `card[cryptogram]` + `card[eci]` block was still reachable through
   the SetupMandate wallet arm (`/v1/setup_intents`) and RepeatPayment's
   `NetworkTokenWithNTI` arm (`/v1/payment_intents`). Both now fail fast via a
   shared `reject_if_tokens_endpoint_only`, naming the endpoint and the tokenize
   step, instead of shipping a request Stripe answers with `parameter_unknown`.

3. `StripeGooglePayPredecrypt::eci` becomes `Option`, matching
   `StripeApplePayPredecrypt`. A cryptogram-bearing token with no ECI used to
   fall into the PAN-only arm and charge as a plain unauthenticated card,
   silently discarding the cryptogram and the liability shift. The cryptogram
   alone now decides the shape. This intentionally diverges from OSS, which
   matches `(Some(cryptogram), Some(eci))` and drops the cryptogram otherwise.

Verified against the Stripe sandbox, wallet cases driven through
CompositePaymentMethodService/Get so `should_do_payment_method_token` is actually
consulted:

  card Authorize                     -> /v1/payment_intents, CHARGED
  GPay decrypted, cryptogram + ECI   -> /v1/tokens then /v1/payment_intents,
                                        CHARGED, charge wallet.type=google_pay
  GPay decrypted, PAN only           -> no Tokenize call at all, inline
                                        /v1/payment_intents card, CHARGED
  GPay decrypted, cryptogram no ECI  -> /v1/tokens with card[cryptogram] and no
                                        card[eci], CHARGED, wallet.type=google_pay
  Apple Pay decrypted                -> unchanged, /v1/tokens, CHARGED
  GPay encrypted                     -> no Tokenize, inline
                                        payment_method_data[card][token]
  SetupMandate w/ decrypted wallet   -> local refusal, zero HTTP emitted
  RepeatPayment NetworkTokenWithNTI  -> local refusal, zero HTTP emitted

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…crypt-recurring

* origin/main:
  docs(grace): require superposition URL registration + URL patching for new connectors (#2123)
  chore(version): 2026.08.14.1
  fix(connector): read calida shop name from connector specific config (#2106)
Auto-applied by CI:
- cargo +nightly fmt --all
- make -C sdk generate (if applicable)
- make docs (if applicable)

This commit was automatically generated by GitHub Actions.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request PMT Add new payment method rust Pull requests that update rust code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants