Skip to main content
Drop this file in the root of your Flutter app and ask Claude Code:
Validate my Yuno VTEX headless wallet integration following CLAUDE.md.
It audits the app against the deferred preflight contract for wallets on VTEX headless (Google Pay, Apple Pay, Click to Pay), described in the flow below, and reports what is wrong, with file and line. It only reads code — it changes nothing unless you ask.

The flow being validated

The order matters: the VTEX order (Steps 6–8) is placed only after /session/preflight/payments has stored the body with the token. The payment does not exist in Yuno until Step 8, so the SDK listener never reports a payment status in this flow (status=null after the token is expected). The outcome comes from VTEX (check 16). Endpoints live on the VTEX store host: https://<store-host>/_v/<connector-app>/v4.

Checks

Report each one as PASS / FAIL / N/A with the evidence (file:line). Do not assume a check passes because the code “looks right” — quote the line that proves it.

1. The hand-off happens only after the OTT

The single most common defect. /session/preflight/payments must be called from inside the listener callback, with a non-empty token, never before startPaymentLite and never on a timer.
  • Find the YunoPaymentListener (or Yuno.startPaymentLite caller) and follow where state.token goes.
  • FAIL if the preflight-payments call is reachable without a token, sends token: ''/null, or runs in the same function that calls startPaymentLite, before any event.
  • FAIL if the VTEX order (Steps 6–8) can be placed before the hand-off returns { deferred: true }, or if the token is sent to /payments/yuno (the Payment App route) instead of /session/preflight/payments.
  • Why: the connector accepts a token-less body and answers { deferred: true }; the failure only surfaces at authorization, as a denied order with Deferred preflight payment creation failed. An app that places the order first, sends a token-less hand-off and then posts the token to /payments/yuno can complete a payment, but only while the authorization happens to arrive before the hand-off; the day it arrives after, the connector uses the token-less body and the order is denied.

2. createPaymentInAuth: true

Must be in the /session/preflight/payments body. Without it the payment is created immediately, before a VTEX order exists.

3. The SDK is initialized with exactly the key preflight returned

  • Yuno.init(apiKey: …) must receive the publicApiKey from that same preflight response.
  • FAIL on: a hardcoded key, a key from .env/remote config/build flavor, or YunoSdkAndroidPlugin.initSdk(...) in an Application class with a different key.
  • Search every initialization point: grep -rn "Yuno.init\|initSdk\|YunoSDK.initialize" lib android ios. When the SDK is initialized more than once, the first one usually wins on iOS — a stale key there makes the session’s first calls fail 401 and the wallet closes with no message.

4. Payment method type matches what VTEX authorizes

payment_method.type (APPLE_PAY / GOOGLE_PAY / CLICK_TO_PAY) must correspond to the VTEX payment method the order is placed with (Apple Pay / Google Pay / C2P). Check the paymentSystem used in Step 5 / Step 7 against the type sent in the hand-off. A mismatch makes the connector ignore the stored payment.

5. Amount, currency and country are identical in preflight and hand-off

  • The amount is a JSON number in decimal units (845.90), not cents (84590) and not a string.
  • The same triple amount:currency:country goes to both calls; it is fingerprinted. A mismatch is 409.
  • FAIL if the amount is read from a different source in the two calls (e.g. cart total vs order total), or if either call happens before the cart total is final.

6. The total is locked before preflight, and the flow restarts if it changes

Shipping, coupons and installments must all be resolved before /session/preflight. If the total changes after it, the app must tear the SDK down and rerun from preflight with the new session. FAIL if the code can change the cart while a session is live without recreating it.

7. One store host per order

Preflight, preflight/payments and the order placement must use the same VTEX account host. FAIL if the connector base URL and the checkout base URL point at different accounts.

8. showPaymentStatus is off

startPaymentLite(..., showPaymentStatus: false). The outcome lives in the VTEX order, not in the SDK’s screen (check 16).

9. The listener is mounted before the wallet starts, and stays mounted

YunoPaymentListener must wrap a widget that is alive for the whole flow. FAIL if it is created inside a dialog/bottom sheet that the wallet sheet can dispose, or if startPaymentLite can run before the widget is built. Log every event, not just the token:
state.substatus and state.message need yuno >= 1.0.19 and are what a support ticket should carry.

10. Split orders are not offered with wallets

A cart fulfilled by more than one seller becomes several VTEX orders, and one wallet token cannot cover them. FAIL if wallets are offered without checking the cart can produce a single order.

11. Installments (only if offered, Apple Pay / Google Pay)

  • /installments/apm is called after preflight, with that session’s checkoutSession + publicApiKey, once per wallet, with country and a full locale (pt-BR).
  • The chosen count travels as payment_method.detail.card.installments in the hand-off body — never with CLICK_TO_PAY, and the amount stays the order total.
  • Empty list, error or timeout must all render no selector and let the wallet proceed. FAIL if the checkout is blocked by this call.

12. Native setup

  • Android: MainActivity extends FlutterFragmentActivity; the Yuno Maven repo is in allprojects; on Kotlin 2.x the Compose compiler plugin is applied (extra["kotlin_version"] set for the plugin to detect it) and the :yuno module’s compileSdk is 35+.
  • iOS: deployment target 14+, ios/Runner/Runner.entitlements has com.apple.developer.in-app-payments listing the merchant ID registered with Yuno for this affiliation, and the App ID has the Apple Pay capability. Report the merchant ID found so it can be compared with the Yuno side.

13. Testing on real builds

Wallets cannot be validated on a simulator/emulator or a debug build: Google Pay needs the release keystore registered in the Google Pay & Wallet Console, Apple Pay a signed build with the entitlement above. FAIL if the test instructions in the repo say otherwise.

14. SDK version

yuno in pubspec.yaml. Report the resolved version and the native versions it pins (~/.pub-cache/hosted/pub.dev/yuno-<v>/android/build.gradle and ios/yuno.podspec), since wallet behavior is decided by the native SDK, not the Dart layer.

15. The native SDK reaches the token service (runtime, signed build)

Nothing in the app code can prove this: the one-time token is requested by the native SDK, not by Dart. Run one attempt on a signed build with the device traffic captured (Proxyman / Charles with the iPhone or Android device proxied, SSL proxying enabled for *.y.uno), complete the wallet sheet, and report which of these calls to the Yuno API appear after startPaymentLite, in order:
  1. the wallet setup call — Apple Pay: .../payments/transactions/APPLE_PAY (merchant session); Google Pay: the config/sdk calls before the sheet opens
  2. .../wallet/decrypt — the wallet payload came back from the sheet and was handed to Yuno
  3. the token creation call (.../token or .../one-time-token) — the OTT exists
Report the last call seen and what the sheet showed when it closed. Reading the result:
  • Only 1 → the sheet never returned a payment: entitlement / merchant ID mismatch (check 12) or the user cancelled. Not an SDK defect.
  • 1 and 2, no 3 → the native SDK stopped between decrypt and token. Report the version pair (check 14).
  • 1, 2 and 3, but the listener logged no token (check 9) → the plugin’s MethodChannel dropped it.
  • All three and the listener logged the token → the SDK side is fine; the problem is in the hand-off (check 1).
enable_logs_ios / enable_logs_android on the Yuno account also print these calls from the SDK, but they are off by default, so the proxy capture is the reliable option. Never paste the capture in a public channel: the decrypt and token bodies carry card data.

16. The outcome is read from VTEX, not from the listener

After the hand-off, the app must not wait for state.paymentStatus: it stays null in this flow. The result comes from Step 8 and the order: After a 2xx, GET /api/checkout/pub/orders/order-group/{orderGroup} and decide from each order’s status: payment-approved (or any later state) is success, payment-pending means the payment is still being processed.
  • FAIL if the success/failure screen depends on a listener status, or if a 2xx is treated as approved without reading the order.
  • FAIL if the app can leave the shopper on a loader after the result is known: with keepLoader: true in the SDK config, Yuno.hideLoader() must be called once the outcome is decided.

Report format

Finish with the failures ordered by impact, each with the one-line fix.

Diagnostics to capture from a failing device run

When the checks pass and the wallet still does not complete, capture and send:
  1. The checkoutSession from the preflight response, and the UTC timestamp of the attempt.
  2. Every listener line from check 9 — paymentStatus, substatus and the full message map.
  3. The device model and OS version, the app build (release/debug) and the yuno version.
  4. Whether the native wallet sheet appeared, and what it showed when it closed.
  5. The VTEX orderGroup, the gatewayCallback HTTP status and body, and the order status read afterwards.
That is enough to line the client events up with the server trace for the session on the Yuno side.

SDK versions

No native SDK version is known to break this flow. A September 2026 suspicion about native iOS 2.18.0–2.23.1 not delivering the Apple Pay token was ruled out: iOS 2.23.1 created the token and completed an approved payment once the app’s hand-off was fixed (check 1). If the token never reaches the listener, run check 15 before suspecting the SDK.