Skip to main content
Start your own AI-powered blog — freeGet started →

Google Pay Integration in Flutter with One Button

Podcast episode2 voices
4:40
Google Pay Integration in Flutter with One Button
Photo by Julio Lopez on pexels

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:

yaml
dependencies:
  flutter:
    sdk: flutter
  pay_android: ^1.0.0
  google_pay_button: ^0.1.0
  • 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:
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:

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.
  • 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:

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:

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

code
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:

javascript
// 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:

  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:

dart
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

  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

Overview of Google Pay in Flutter

Google Pay is a unified payment solution that lets users pay with a single tap, eliminating the need to enter card details manually. In Flutter, the integration is streamlined by the pay package, which bridges the native Google Pay SDK to Dart code. By defining a payment configuration JSON file, you specify your merchant ID, supported card networks, and transaction parameters. Once the configuration is in place, the package automatically detects whether Google Pay is available on the device and displays a consistent checkout UI. This approach keeps your codebase minimal while delivering a native‑looking payment experience.

The architecture of a typical Flutter Google Pay flow consists of three core layers: the UI layer, the payment configuration layer, and the backend layer. The UI layer contains a single button that triggers the payment request. The configuration layer holds the merchant credentials and transaction details in a JSON asset. The backend layer receives a payment token from the client, verifies it with your payment gateway, and completes the charge. This separation of concerns simplifies maintenance and testing.

Before you start coding, ensure you have a Google Cloud project with the Google Pay API enabled and a merchant ID issued. The merchant ID is a unique string that validates your identity to Google Pay. Additionally, you need a Firebase project if you plan to use Firebase Cloud Functions for your backend. Once those prerequisites are in place, you can add the pay package to your pubspec.yaml and proceed with the next steps.

Setting Up Firebase and Google Pay API

Creating a Firebase project is the first step to secure the backend communication. Log into the Firebase console, create a new project, and register your Android and iOS apps by providing the bundle identifiers. Download the google-services.json for Android and GoogleService-Info.plist for iOS, and place them in the respective platform directories. These files enable Firebase Authentication, Cloud Functions, and Realtime Database if you need them.

Once Firebase is configured, move to the Google Cloud console. Enable the Google Pay API under the “Library” section. After activation, open the “Credentials” tab and generate a new API key. This key is used by the Flutter app to authenticate requests to Google Pay. Store the API key securely, for example, in a .env file that is excluded from version control. The pay package will read the key from the payment configuration JSON.

With both Firebase and Google Pay API set up, you can create the payment configuration file. In the assets folder, add a file named google_pay.json. Inside, define the merchantId, environment, and supported card networks. The file might look like this:

json
{
  "merchantId": "01234567890123456789",
  "environment": "TEST",
  "allowedAuthMethods": ["PAN_ONLY", "CRYPTOGRAM_3DS"],
  "allowedCardNetworks": ["AMEX", "VISA", "MASTERCARD"],
  "transactionInfo": {
    "totalPriceStatus": "FINAL",
    "totalPrice": "1.00",
    "currencyCode": "USD"
  }
}

After creating the JSON, reference it in the pubspec.yaml under the assets section and run flutter pub get to make it available at runtime.

Implementing the One‑Button Checkout Flow

With the configuration in place, you can add a single button to your UI that launches the Google Pay dialog. Use the Pay widget from the pay package, passing the asset path and a callback for the payment data. The widget automatically displays the appropriate icon and handles user interaction.

Here is a concise example of how to wire the button:

dart
Pay(
  paymentConfigurationAsset: 'assets/google_pay.json',
  paymentItems: [
    PaymentItem(
      label: 'Total',
      amount: '1.00',
      status: PaymentItemStatus.final_price,
    ),
  ],
  onWillPop: () => Future.value(false),
  onPaymentResult: (paymentResult) {
    // Send paymentResult['paymentMethodData']['tokenizationData']['token']
    // to your backend for processing.
  },
  styleParameters: const ButtonStyleParameters(
    buttonColor: '#FFFFFF',
    shape: ButtonShape.rounded_rectangle,
  ),
)

The onPaymentResult callback receives a map containing the payment token. This token is a short‑lived reference that your server can use to charge the customer via your payment gateway. Keep the token on the client only long enough to pass it over HTTPS; discard it immediately thereafter.

The button’s styleParameters allow you to match your app’s branding while still complying with Google Pay’s visual guidelines. The onWillPop callback prevents accidental dismissal of the payment sheet, ensuring a smooth user experience.

Handling Payment Data and Security

Security is paramount when dealing with payment tokens. The Google Pay SDK returns a token that is encrypted and signed by Google. Your server must validate this token with the payment gateway’s API before attempting to capture funds. Never attempt to decrypt or inspect the token on the client side; the token is opaque and designed for server‑side usage.

Store any non‑sensitive metadata in secure storage. For example, you might keep the transaction ID or order number in the device’s keychain or secure enclave. Use the flutter_secure_storage package to avoid exposing sensitive information in plain text. Ensure that your backend uses HTTPS with a valid TLS certificate to protect data in transit.

PCI DSS compliance is achieved by not storing card numbers or CVV codes on your servers. The token returned by Google Pay is a one‑time credential that satisfies this requirement. Document your token handling process, and periodically audit your backend for compliance with the latest PCI standards.

Testing and Debugging

Testing is critical before you go live. The pay package offers a debug flag that, when set to true, logs detailed information about the payment flow. In the test environment, use Google’s provided test card numbers such as 4111 1111 1111 1111 for VISA or 3782 822463 10005 for AMEX. These cards simulate successful and declined transactions.

Run your app on a real device or emulator that has Google Play services installed. Tap the checkout button and observe the Google Pay dialog. If the dialog does not appear, check the console for errors related to missing merchant ID or unsupported card networks. The Pay widget will throw a PlatformException if the device cannot support Google Pay, so wrap the call in a try/catch block to handle this gracefully.

Common pitfalls include mismatched currency codes, incorrect total price status, or using a production merchant ID in the test environment. Verify that the environment field in your JSON matches the API key’s configuration. If you encounter a “merchant not verified” error, ensure that your merchant ID has been approved by Google Pay and that your Firebase project’s SHA‑1 fingerprint matches the one registered in the Google Cloud console.

Going Live and Compliance

When you’re ready to ship, switch the environment field in your google_pay.json to PRODUCTION. This tells the SDK to use your live merchant credentials. Additionally, sign your Android app with the production keystore and upload the app to the Play Store with the correct SHA‑1 fingerprint. For iOS, use the production provisioning profile.

Before the first live transaction, submit your merchant ID for verification. Google will review your business information and confirm that you meet the eligibility criteria. Once approved, the SDK will allow real payments.

After launch, monitor transaction logs on your backend. Keep an eye on failed charges, failed token validations, or unusual activity. Implement rate limiting and fraud detection on the server side to protect against abuse. Finally, maintain clear documentation for your support team so they can troubleshoot payment issues reported by users.

Future Enhancements and Best Practices

As your app scales, consider integrating additional payment methods such as Apple Pay or local payment networks. The pay package supports multiple providers; you can configure additional payment configurations and present a unified button that adapts to the user’s device.

Automate the testing of the payment flow using integration tests. The integration_test package can simulate tapping the checkout button and verifying that the callback receives a valid token. Mock the backend API in your test environment to validate end‑to‑end logic.

Keep your dependencies up to date and monitor the pay package’s changelog for security patches. Regularly audit your app’s use of sensitive data and enforce strict network security policies, such as HSTS and certificate pinning, to mitigate man‑in‑the‑middle attacks.

By following these steps, you can deliver a seamless, secure, and compliant Google Pay experience in Flutter with a single, intuitive checkout button.

Key Takeaways

  • Integrate Google Pay with a single Flutter button by leveraging the google_pay package and a concise payment data request.
  • Ensure your Android and iOS projects are correctly configured with the appropriate minSdkVersion, merchant ID, and test credentials before deploying.
  • Use a lightweight widget that triggers the payment flow on tap, keeping the UI clean and the user experience fast.
  • Validate the payment token on a secure server to keep sensitive transaction data off the client and meet PCI compliance.
  • Handle both success and error callbacks to provide clear feedback and fallback options for users.

Frequently Asked Questions

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.

G
Gulshan Yadav

1 followers

AI systems builder · 7 years in production. RAG, self-hosted infra, agent architecture. 📬 Deep-dives → mrgulshanyadav.substack.com

Comments

Sign in to join the conversation

No comments yet. Be the first to share your thoughts!

More from Gulshan Yadav

Recommended for you