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
-
pay_androidis 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_buttonrenders 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:
- Your app's
minSdkVersionmust be 21 or higher (inandroid/app/build.gradle). - Add the Google Pay meta-data to your
AndroidManifest.xml:
<meta-data
android:name="com.google.android.gms.wallet.api.enabled"
android:value="true" />
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_..."
}
}
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. -
merchantIdis your Google Pay merchant ID from the Google Pay & Wallet Console. You can leave it empty in TEST, but production requires it. -
allowedAuthMethods:PAN_ONLYis the card on file,CRYPTOGRAM_3DSis the encrypted token. Using both maximizes the number of users who can pay. - The
gatewaysection 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
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,
),
),
);
}
}
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
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,
});
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:
- The user taps the Google Pay button.
- Your app calls
isReadyToPayto confirm the device has Google Pay set up with an eligible card. - Your app calls
presentPaymentSheet, and Google's native sheet opens — the card list, the price, the pay button. - The user authenticates (fingerprint, face unlock, or PIN) and approves.
- Google returns an encrypted, single-use payment token to your app.
- Your app sends the token to your backend.
- Your backend forwards it to the payment gateway.
- The gateway decrypts the token, charges the card, and returns a confirmation (and fires a webhook).
- 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:
-
Button-only, without the manual
PaymentsClient. TheGooglePayButtonwidget can present the sheet and return the result through its ownonPaymentResultcallback, skipping the explicitisReadyToPaycheck and the manualpresentPaymentSheetcall. 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. -
WebView fallback with Google Pay's JavaScript API. You can render the Google Pay web experience inside a
webview_flutterand 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.')),
);
}
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
-
Never hardcode
Environment.testin release. Read the environment from a config flag, not from editing the file per build. - The token is single-use. Do not cache it, do not log it to console, do not display it. One charge attempt per token.
- Google Pay is Android-only. This plugin does nothing on iOS. For iOS you integrate Apple Pay separately.
- 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.
-
PaymentItemamounts are strings, not numbers.'9.99', not9.99. Round to the currency's smallest unit display — a missing decimal place has shipped real-world pricing bugs. -
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. -
Handle
isReadyToPay == false. Some devices/browsers won't support Google Pay. Show a fallback payment method instead of a dead button. -
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.jsonor 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 == falsefalls 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)