DEV Community

Gulshan Yadav
Gulshan Yadav

Posted on • Originally published at misar.blog

Google Pay Integration in Flutter with One Button

So, in this article, I will be showing you how you can add Google Pay to your Flutter app with literally one button. No card-number form, no CVV field, no "add a payment method" screen. The user taps the Google Pay button, picks a card that Google Pay already knows, confirms with their fingerprint, and the payment data is on its way.

I keep getting asked about payments in Flutter, and after PayPal and Stripe, Google Pay is the one people want next. The good news: it is the easiest of the three, because Google has done the hard work. In this article I'll show you the exact dependencies, the button, the flow, and the pitfalls I hit when I first shipped it — so you don't lose a day to the same setup trap.

Let's jump into the coding part.

Adding the Dependencies

For this purpose, we need to add these dependencies in your pubspec.yaml file:

dependencies:
  flutter:
    sdk: flutter
  pay_android: ^1.0.0
  google_pay_button: ^0.1.0
Enter fullscreen mode Exit fullscreen mode
  • pay_android is the official Google-maintained Flutter plugin for Google Pay. It wraps the native Android Google Pay API, so you don't need to write Kotlin.
  • google_pay_button renders the actual branded Google Pay button, so you comply with Google's button guidelines without hand-drawing the G logo.

If you only want the button and prefer to write the payment logic yourself, skip google_pay_button. But if you want the one-tap experience described above, take both.

Step 1: Configure Android

Before any Dart runs, the Android side needs two things:

  1. Your app's minSdkVersion must be 21 or higher (in android/app/build.gradle).
  2. Add the Google Pay meta-data to your AndroidManifest.xml:
<meta-data
    android:name="com.google.android.gms.wallet.api.enabled"
    android:value="true" />
Enter fullscreen mode Exit fullscreen mode

Now add your payment configuration as an asset. This is a JSON file that tells Google Pay which networks, auth methods, and gateway to use. Create assets/google_pay.json:

{
  "provider": "google_pay",
  "environment": "TEST",
  "merchantName": "Your App Name",
  "merchantId": "BCR2DN4TXXXXXXXXXX",
  "allowedCardNetworks": ["VISA", "MASTERCARD", "AMEX"],
  "allowedAuthMethods": ["PAN_ONLY", "CRYPTOGRAM_3DS"],
  "gateway": {
    "gateway": "stripe",
    "gatewayMerchantId": "pk_test_..."
  }
}
Enter fullscreen mode Exit fullscreen mode

A few important notes on this file:

  • environment: "TEST" means you can run the whole flow with test cards and no real money moves. Switch it to "PRODUCTION" only for release builds.
  • merchantId is your Google Pay merchant ID from the Google Pay & Wallet Console. You can leave it empty in TEST, but production requires it.
  • allowedAuthMethods: PAN_ONLY is the card on file, CRYPTOGRAM_3DS is the encrypted token. Using both maximizes the number of users who can pay.
  • The gateway section tells Google which payment processor will decrypt the token — Stripe, Adyen, Braintree, etc. You need a live account with that gateway before production.

Register the asset in pubspec.yaml:

flutter:
  assets:
    - assets/google_pay.json
Enter fullscreen mode Exit fullscreen mode

Step 2: The One Button

Here is the whole Flutter side. Create a checkout page and drop the button in:

import 'package:flutter/material.dart';
import 'package:google_pay_button/google_pay_button.dart';
import 'package:pay_android/pay_android.dart';

class CheckoutPage extends StatefulWidget {
  const CheckoutPage({super.key});
  @override
  State<CheckoutPage> createState() => _CheckoutPageState();
}

class _CheckoutPageState extends State<CheckoutPage> {
  late final PaymentConfiguration _config;

  @override
  void initState() {
    super.initState();
    PaymentConfiguration.fromAsset('assets/google_pay.json')
        .then((config) => setState(() => _config = config));
  }

  Future<void> _onGooglePayPressed() async {
    // 1. Confirm the device can actually pay.
    final client = PaymentsClient(environment: Environment.test);
    final isReady = await client.isReadyToPay(
      paymentConfiguration: _config,
      allowedCardNetworks: const [CardNetwork.visa, CardNetwork.mastercard],
      allowedAuthMethods: const [AuthMethod.cryptogram3ds, AuthMethod.panOnly],
    );
    if (!isReady) return;

    // 2. Present the Google Pay sheet.
    final result = await client.presentPaymentSheet(
      merchantName: 'Your App Name',
      paymentConfiguration: _config,
      paymentItems: const [
        PaymentItem(
          label: 'Premium Plan',
          amount: '9.99',
          status: PaymentItemStatus.finalPrice,
        ),
      ],
    );

    // 3. Handle the result.
    if (result is PaymentResult.success) {
      // Send result.token to YOUR backend for verification.
      await _verifyPaymentOnServer(result.token.paymentData);
    } else if (result is PaymentResult.canceled) {
      ScaffoldMessenger.of(context).showSnackBar(
        const SnackBar(content: Text('Payment cancelled')),
      );
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Checkout')),
      body: Center(
        child: GooglePayButton(
          paymentConfiguration: _config,
          onPressed: _onGooglePayPressed,
          type: GooglePayButtonType.buy,
        ),
      ),
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

That's it. That is the entire integration. The GooglePayButton widget handles the tap; presentPaymentSheet opens Google's native sheet where the user picks a card and confirms with biometrics; and you get a PaymentResult to react to.

Step 3: Verify the Token on Your Backend

Here is the part most beginners get wrong, and it matters: the Google Pay token is not money collected. It is a one-time encrypted payment credential. Your app must send it to your own backend, and your backend must pass it to your payment gateway (Stripe, Adyen, etc.) which decrypts it and actually charges the card.

Flutter app ──▶ Your backend ──▶ Payment gateway (decrypt + charge)
                    ▲
                    └── webhook: payment.succeeded ──▶ mark order paid
Enter fullscreen mode Exit fullscreen mode

Never trust the app to tell you the payment succeeded. Always confirm the charge server-side, ideally via the gateway's webhook. On the backend, for Stripe it looks roughly like this:

// POST /api/verify-payment
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);

const paymentMethod = await stripe.paymentMethods.create({
  type: 'card',
  card: { token: req.body.googlePayToken },
});

const intent = await stripe.paymentIntents.create({
  amount: 999,
  currency: 'usd',
  payment_method: paymentMethod.id,
  confirmation_method: 'manual',
  confirm: true,
});
Enter fullscreen mode Exit fullscreen mode

Different gateways have different calls, but the pattern is identical: forward the token, wait for the gateway to confirm, then mark the order paid.

The Full Flow in Plain English

Here is the whole sequence, so you know exactly what happens between the tap and the charge landing in your account:

  1. The user taps the Google Pay button.
  2. Your app calls isReadyToPay to confirm the device has Google Pay set up with an eligible card.
  3. Your app calls presentPaymentSheet, and Google's native sheet opens — the card list, the price, the pay button.
  4. The user authenticates (fingerprint, face unlock, or PIN) and approves.
  5. Google returns an encrypted, single-use payment token to your app.
  6. Your app sends the token to your backend.
  7. Your backend forwards it to the payment gateway.
  8. The gateway decrypts the token, charges the card, and returns a confirmation (and fires a webhook).
  9. Your backend marks the order paid and tells the app.

Steps 6 through 9 are the ones you must never skip. If steps 4 and 5 work but step 7 never happens, the user believes they paid and you never receive the money. That is the exact failure mode that looks like a successful integration in the demo and collapses in production.

Alternative Approaches

Two variations are worth knowing before you commit:

  1. Button-only, without the manual PaymentsClient. The GooglePayButton widget can present the sheet and return the result through its own onPaymentResult callback, skipping the explicit isReadyToPay check and the manual presentPaymentSheet call. It is less code, but you lose fine-grained control — per-run payment items and explicit readiness handling. For a single fixed product price, the button-only path is fine; for dynamic carts, keep the explicit client.
  2. WebView fallback with Google Pay's JavaScript API. You can render the Google Pay web experience inside a webview_flutter and bridge the token back to Dart. This is useful when your backend already runs the web checkout and you want to reuse the same flow on mobile. The cost: no native sheet, a JavaScript bridge you own, and message-passing and error handling that live in your code. I have used this for a legacy app with an existing web checkout. For a new build, I would take the native plugin every time.

Handling the Failure Paths

The happy path is one line of code; the failure paths are where payments are actually won or lost. Cover at least these cases in your UI:

final result = await client.presentPaymentSheet(...);
if (result is PaymentResult.success) {
  await _verifyPaymentOnServer(result.token.paymentData);
} else if (result is PaymentResult.canceled) {
  // User closed the sheet — return them to a clean cart state.
  ScaffoldMessenger.of(context).showSnackBar(
    const SnackBar(content: Text('Payment cancelled')),
  );
} else {
  // Any other failure — do NOT silently stay on a spinner.
  ScaffoldMessenger.of(context).showSnackBar(
    const SnackBar(content: Text('Payment failed. Please try again.')),
  );
}
Enter fullscreen mode Exit fullscreen mode

Never leave the button in a loading state after a failure, and never show success based on the app's result alone — success must be confirmed by your backend.

Important Notes & Pitfalls

  1. Never hardcode Environment.test in release. Read the environment from a config flag, not from editing the file per build.
  2. The token is single-use. Do not cache it, do not log it to console, do not display it. One charge attempt per token.
  3. Google Pay is Android-only. This plugin does nothing on iOS. For iOS you integrate Apple Pay separately.
  4. Requires Google Play Services. Test on an emulator with Play Services enabled, or on a real device. On a bare emulator the button will simply not work.
  5. PaymentItem amounts are strings, not numbers. '9.99', not 9.99. Round to the currency's smallest unit display — a missing decimal place has shipped real-world pricing bugs.
  6. Test with real test cards. In TEST mode use your gateway's test cards (e.g., Stripe's 4242 4242 4242 4242). The flow is 100% real except no money moves.
  7. Handle isReadyToPay == false. Some devices/browsers won't support Google Pay. Show a fallback payment method instead of a dead button.
  8. Don't forget the gateway agreement. The token is encrypted to a gateway. You must have a live account with the gateway listed in your google_pay.json or production decryption fails with a confusing error.

Testing Checklist

Before you ship, walk through this list:

  • [ ] TEST environment payment with a test card completes end-to-end
  • [ ] Cancellation flow returns the user to the app with a clear message
  • [ ] Token reaches your backend and the gateway confirms the charge
  • [ ] Webhook updates the order status server-side (not just the app)
  • [ ] isReadyToPay == false falls back to another payment method
  • [ ] PRODUCTION environment switch is config-driven, not manual
  • [ ] No secret keys or merchant IDs committed to the repo

FAQ

Is pay_android still maintained? Yes. It is part of the official Flutter pay plugins maintained by Google, with active releases.

Can I use this with any backend language? Yes. The Flutter side just sends the token to your backend; the backend can be Node, Python, Go, anything that talks to your gateway.

Does Google Pay support recurring subscriptions? Google Pay returns a single-payment token. For recurring billing, use that token to create a customer and a subscription in your gateway (Stripe, Razorpay, etc.) on your backend, then charge it on your own schedule. Do not expect Google Pay itself to manage recurring charges.

Why did TEST work but production fails? The three classic causes, in order: the environment is still TEST, the merchant ID is missing or wrong, or the gateway listed in google_pay.json does not match the gateway actually processing the token. The token is encrypted to a specific gateway, so a mismatch fails at decryption time with a confusing error.

Does Google Pay cost money to integrate? No integration fee from Google. You pay the standard processing fee to your payment gateway.

That's it — a complete Google Pay integration in Flutter, with one button. The whole thing took me less than an hour once I had the google_pay.json right, and I hope this saves you the setup trap I hit on my first attempt.

I have also written integrations for PayPal, Stripe, and UPI/Razorpay — comment below with your payment gateway and I'll cover it next.


*Gulshan Yad

Top comments (0)