Apple Pay and Google Pay in Flutter with the pay Package
Add Apple Pay and Google Pay to Flutter with the pay package: merchant IDs, tokenized payments, and PSP integration via Stripe PaymentSheet.
Published on • November 10, 2026
AI Assistant

Wallet buttons promise one-tap checkout, but the setup is split across Apple and Google consoles, Xcode, and a payment service provider. This post explains what Apple Pay and Google Pay actually return, how to configure both merchants, and how the pay package plus flutter_stripe turn a sheet tap into a captured payment.
What the wallets actually return
Apple Pay and Google Pay are tokenized card-network payments: the buyer picks a card already stored in their wallet, and instead of your app ever seeing the primary account number, the card network or wallet returns a single-use payment token. On Google Pay, the result map carries that token under paymentMethodData.tokenizationData.token, which you send to your backend along with the purchase details; your backend passes it to the payment processor to execute the charge. Apple Pay works the same way through PassKit - the wallet returns an encrypted PKPayment token that only your payment provider can decrypt. The design point is the same for both: your servers handle tokens, never raw card data.
Merchant setup on both platforms
Both wallets need merchant registration before a button can render. For Apple Pay, the pay package README points at Apple’s setup requirements: create a merchant identifier (merchant.com.your.company) in the Developer account and a payment processing certificate used to encrypt payment information. Then enable the capability in Xcode:
- Open the project, select the target, go to “Signing & Capabilities”.
- Click ”+ Capability” and add “Apple Pay”.
- Enable the merchant IDs you created.
For Google Pay, sign up at the Google Pay Business Console (pay.google.com/business/console), complete the business profile, and review the integration requirements. On the web, production additionally requires entering a MerchantInfo.merchantId (an ID like BCR2DN6TSXXXZ000) and requesting production access from the console; your integration type is either Gateway (using a participating processor) or Direct - and Google restricts Direct integration to merchants that are PCI DSS compliant as validated by a Qualified Security Assessor, including annual public-key rotation and PCI Attestation.
Buttons and payment results with the pay
The pay package (published by google.dev, currently 4.0.0) bundles pay_android and pay_ios behind one API. You declare each provider’s configuration as JSON and drop in a button:
import 'package:pay/pay.dart';
const _paymentItems = [
PaymentItem(label: 'Total', amount: '99.99',
status: PaymentItemStatus.final_price),
];
ApplePayButton(
paymentConfiguration:
PaymentConfiguration.fromJsonString(defaultApplePayConfigString),
paymentItems: _paymentItems,
style: ApplePayButtonStyle.black,
type: ApplePayButtonType.buy,
onPaymentResult: onApplePayResult,
loadingIndicator: const Center(child: CircularProgressIndicator()),
),
GooglePayButton(
paymentConfiguration:
PaymentConfiguration.fromJsonString(defaultGooglePayConfigString),
paymentItems: _paymentItems,
type: GooglePayButtonType.buy,
onPaymentResult: onGooglePayResult,
),
Send both results straight to your backend - onApplePayResult(paymentResult) and onGooglePayResult(paymentResult) hand you the token payload, nothing to charge directly from the device. For more control, instantiate Pay({PayProvider.google_pay: ..., PayProvider.apple_pay: ...}), gate the UI on userCanPay(PayProvider.apple_pay), and trigger the sheet with showPaymentSelector(provider, paymentItems). One Android-only trap: Pay.showPaymentSelector starts the flow but does not return the result - subscribe to the EventChannel('plugins.flutter.io/pay/payment_result') stream instead, and cancel that subscription when you are done.
Platform availability, including Google Pay on the web
The pay plugin’s platform table is explicit: Google Pay on Android, Apple Pay on iOS. Apple Pay itself exists on iOS, macOS, and watchOS through PassKit (and in Safari on the web), but the plugin only implements the iOS side, so a Flutter macOS app needs another route. Google Pay’s web surface is separate: load https://pay.google.com/gp/p/js/pay.js, create new google.payments.api.PaymentsClient({environment: 'TEST'}), and only show the button after isReadyToPay() resolves true:
paymentsClient.isReadyToPay(getGoogleIsReadyToPayRequest())
.then(function (response) {
if (response.result) addGooglePayButton();
});
Keep existingPaymentMethodRequired out of that decision - Google warns it depends on cookies and cross-site tracking, so paymentMethodPresent may simply be absent. When you go live, switch the environment to 'PRODUCTION' and add the merchantId.
Tokenization and PCI scope
The whole point of wallets is PCI scope reduction: because your app and server only ever handle a network-issued token, card data never touches your infrastructure, which is what keeps most integrations in the lightest compliance categories. Google’s own checklist reinforces this - apply your existing risk controls and 3-D Secure triggers to Google Pay PAN_ONLY transactions exactly as you would to normal card charges, and use Google Pay data only to process transactions. The scope you do take on is the DIRECT path: bypassing a processor means annual key rotation plus a PCI Attestation, which is why the overwhelming majority of Flutter apps should pick the Gateway route or a hosted sheet.
Going through a PSP instead of raw wallets
The cheapest correct integration is to let a payment service provider run the wallet sheet. With flutter_stripe, configure the SDK once and let its platform-pay API own the flow:
void main() async {
Stripe.publishableKey = stripePublishableKey;
Stripe.merchantIdentifier = 'merchant.com.your.app';
runApp(const App());
}
// later, in a widget:
if (Stripe.instance.isPlatformPaySupportedListenable.value)
PlatformPayButton(
type: PlatformButtonType.buy,
onPressed: startApplePay,
);
Future<void> startApplePay() async {
final clientSecret = await fetchPaymentIntentClientSecret();
await Stripe.instance.confirmPlatformPayPaymentIntent(
clientSecret: clientSecret,
confirmParams: PlatformPayConfirmParams.applePay(
applePay: ApplePayParams(
cartItems: [ApplePayCartSummaryItem.immediate(
label: 'Product Test', amount: '0.01')],
merchantCountryCode: 'US',
currencyCode: 'USD',
),
),
);
}
Here Stripe handles the token, the PaymentIntent, and SCA step-up; you register the Apple Merchant ID in the Stripe dashboard and keep the Google Pay side behind the same PaymentSheet. The pay plugin remains useful when you want the native sheet without a hosted UI - you then take its result, call Stripe.instance.createApplePayToken(paymentResult) (or extract the Google Pay token), and confirm the PaymentIntent yourself. Pick raw pay only when you already have a processor and a backend ready to redeem tokens; otherwise start with the PSP’s sheet and add raw wallets later.
Takeaway: Wallets return tokens, never card numbers - register an Apple Merchant ID plus processing certificate and a Google Pay merchant profile, render buttons with pay (Android/iOS) or Google’s pay.js (web), and route every token through a PSP like Stripe’s PaymentSheet so your servers stay out of raw card data.
Sources
- https://pub.dev/packages/pay
- https://pub.dev/packages/flutter_stripe
- https://developers.google.com/pay/api/android/overview
- https://developers.google.com/pay/api/web/guides/test-and-deploy/integration-checklist
- https://developers.google.com/pay/api/web/reference/client
- https://developer.apple.com/documentation/passkit/apple_pay/setting_up_apple_pay_requirements
- https://docs.page/flutter-stripe/flutter_stripe/apple_pay
- https://docs.page/flutter-stripe/flutter_stripe/platform_pay_migration