DEV Community

Gulshan Yadav
Gulshan Yadav

Posted on • Originally published at misar.blog

Apple Pay in Flutter: The Easiest Implementation

So, in this article, I will be showing you how you can integrate Apple Pay into your Flutter app — and yes, this one is genuinely the easiest of the mobile wallets, because Apple has wrapped the entire flow into a native payment sheet. No card form, no bank list, no OTP. The user double-clicks the side button, confirms with Face ID, and the payment token is out.

The reason Apple Pay integration is easy is that Apple does almost everything for you: the card vault, the biometrics, the tokenization, the UI. What is not easy — and what stops most people for a full day — is the Apple Developer setup on the way in. This article covers both: the configuration you have to get right before Flutter, and the minimal Dart you need after.

Let's jump into the coding part.

Adding the Dependencies

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

dependencies:
  flutter:
    sdk: flutter
  pay_ios: ^1.0.0
Enter fullscreen mode Exit fullscreen mode

pay_ios is Apple's official Flutter plugin for Apple Pay (part of the flutter-pay-plugins). It wraps PassKit's PKPaymentAuthorizationViewController, so you never touch Swift. That single package is the whole dependency story — one line, no extra UI packages, no separate button package.

Step 1: Apple Developer Setup (The Part Everyone Misses)

Before you write a single line of Dart, three things must exist on the Apple side, or your payment sheet will silently refuse to show:

  1. A Merchant ID. In the Apple Developer portal, go to Certificates, Identifiers & Profiles → Identifiers, create a Merchant ID like merchant.com.yourcompany.yourname. This is the ID your code references, and it must be enabled for your App ID.
  2. The Apple Pay capability. In Xcode, open the Runner target → Signing & Capabilities → add "Apple Pay" and select your merchant ID. If you use flutter build, verify this in the generated Xcode project before building the app.
  3. A Payment Processing Certificate. In the Merchant ID settings, create a merchant identity certificate. Apple uses this to encrypt the payment token. If you use a payment provider (Stripe, Adyen, Braintree), they generate this certificate for you; otherwise you create a CSR from Apple.

The classic failure: everything works in code, and the payment sheet says "Apple Pay is not available." Nine times out of ten it is the entitlement or the merchant ID mismatch, not your Dart.

Step 2: The Minimal Dart Implementation

Here is the entire Flutter side. Create a checkout page and present the Apple Pay sheet:

import 'package:flutter/material.dart';
import 'package:pay_ios/pay_ios.dart';

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

class _CheckoutPageState extends State<CheckoutPage> {
  late final ApplePayClient _client;

  @override
  void initState() {
    super.initState();
    _client = ApplePayClient(
      paymentConfiguration: PaymentConfiguration.fromJsonString(
        '''
        {
          "merchantId": "merchant.com.yourcompany.yourname",
          "merchantName": "Your App Name",
          "countryCode": "US",
          "currencyCode": "USD"
        }
        ''',
      ),
    );
  }

  Future<void> _payWithApplePay() async {
    try {
      final result = await _client.presentApplePay(
        displayItems: const [
          ApplePayItem(
            label: 'Premium Plan',
            amount: '9.99',
            type: ApplePayItemType.final_,
          ),
        ],
        merchantCapabilities: const [
          MerchantCapability.threeDSecure,
        ],
        supportedNetworks: const [
          ApplePayCardNetwork.visa,
          ApplePayCardNetwork.mastercard,
          ApplePayCardNetwork.amex,
          ApplePayCardNetwork.discover,
        ],
        requiredBillingContactFields: const [
          ApplePayContactField.postalAddress,
        ],
        requiredShippingContactFields: const [
          ApplePayContactField.email,
          ApplePayContactField.phone,
        ],
      );

      if (result is ApplePayResult.success) {
        // Send result.token to YOUR backend for verification.
        await _verifyOnServer(result.token);
      } else if (result is ApplePayResult.canceled) {
        ScaffoldMessenger.of(context).showSnackBar(
          const SnackBar(content: Text('Payment cancelled')),
        );
      }
    } catch (e) {
      debugPrint('Apple Pay error: $e');
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Checkout')),
      body: Center(
        child: ElevatedButton(
          onPressed: _payWithApplePay,
          child: const Text('Pay with Apple Pay'),
        ),
      ),
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

That is the whole implementation. presentApplePay launches the native sheet, the user confirms with Face ID or Touch ID, and you receive an ApplePayResult with the token.

Step 3: Verify the Token on Your Backend

Same rule as every wallet integration, and it is non-negotiable: the device gives you a token, not money. Send it to your backend, and let your backend decrypt and charge it through your payment provider. On the backend, with Stripe it looks roughly like this:

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

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

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

Confirm success from the gateway's response (or its webhook), never from the app. The app can always lie; the gateway cannot.

The Full Flow in Plain English

Here is the sequence end to end, so you know what happens between the double-click and the money landing:

  1. The user taps your Pay button.
  2. Your app calls presentApplePay, and iOS builds the payment sheet natively — the card, the merchant name, the price, the shipping and contact fields you requested.
  3. The user authenticates with Face ID, Touch ID, or their device passcode.
  4. Apple wraps the selected card into an encrypted, single-use token and returns it to your app.
  5. Your app sends the token to your backend.
  6. Your backend forwards it to your payment provider.
  7. The provider decrypts the token (using the payment processing certificate), charges the card, and confirms — plus fires a webhook.
  8. Your backend marks the order paid and tells the app.

Steps 5 through 8 are where the actual money moves, and they must never be skipped. If steps 3 and 4 work but step 6 never happens, the user believes they paid while you hold nothing.

Adding Line Items Properly

A small detail that trips people: Apple Pay wants every item you show in the sheet to match what you eventually charge. If you show '9.99' but your backend computes tax and charges 10.74, the user approved a different number than the one they paid. The clean pattern:

  1. Build the full order server-side — items, shipping, tax — before presenting the sheet.
  2. Pass the computed total as your ApplePayItem amount.
  3. Send the same order reference to your backend when you forward the token, so the charge uses exactly what was quoted.

If you cannot finalize the amount upfront (variable shipping, tips), use ApplePayItemType.pending, then update the charge server-side to the final value. Keeping the quoted number and the charged number identical avoids a whole category of refund requests and chargeback disputes.

Alternative Approaches

Three variations are worth knowing before you commit:

  1. Button-only, without the manual client. If your checkout is a fixed set of items, some payment providers' Flutter SDKs (Stripe, Adyen) expose a one-call Apple Pay button that handles the sheet and the provider confirmation together. Less code, but it ties you to that provider's SDK for both the UI and the charging.
  2. Stripe/Adyen Flutter SDK with Apple Pay. Rather than managing the token and the backend forward yourself, these SDKs accept your merchant configuration and return a confirmed PaymentMethod you then charge server-side. If you already use one of those providers, this is usually the pragmatic choice — fewer moving parts, one vendor.
  3. Manual PKPaymentRequest via a channel. You can write the PassKit integration yourself in Swift and call it over a MethodChannel. This gives you total control but also total responsibility — entitlement handling, the full authorization delegate, error mapping. The pay_ios plugin exists precisely so you do not have to do this.

Handling the Failure Paths

The sheet succeeds or cancels, and both are real user behavior you must render. Handle them explicitly:

final result = await _client.presentApplePay(...);
if (result is ApplePayResult.success) {
  await _verifyOnServer(result.token);           // confirmed server-side
} else if (result is ApplePayResult.canceled) {
  ScaffoldMessenger.of(context).showSnackBar(
    const SnackBar(content: Text('Payment cancelled')),
  );
} else {
  ScaffoldMessenger.of(context).showSnackBar(
    const SnackBar(content: Text('Payment failed. Please try again.')),
  );
}
Enter fullscreen mode Exit fullscreen mode

A subtle trap: users cancel the sheet by swiping it away or locking the phone — a surprising share of "failed" payments are actually cancels. Handle cancel distinctly, and never show an error screen for a cancel. And never treat the app-level result as proof of charge; only the provider's confirmation counts.

Important Notes & Pitfalls

  1. Real device required. Apple Pay does not work in the iOS Simulator for card payments. Test on a physical iPhone with a sandbox card. This is the #1 thing that makes people think their code is broken.
  2. Sandbox cards. Add a test card (e.g., 4242 4242 4242 4242) in Settings → Wallet on your device. It passes the full flow with no real charge.
  3. Merchant ID must match everywhere. The ID in your code, the ID in Xcode's entitlement, and the ID in Apple's portal must be identical. A single typo and the sheet never appears.
  4. Region availability. Apple Pay only works in supported countries, and your app's region settings matter. An App Store region without Apple Pay will fail gracefully — handle it.
  5. amount is a string with the decimal point. '9.99', not 9.99. Keep the string format consistent with your currency rules.
  6. Token is single-use and short-lived. Do not cache or log it. Forward it to your backend immediately and only once.
  7. ApplePayItemType.final_ vs pending. Use final_ for a fixed charge. Use pending if you'll finalize the amount later (e.g., tips or variable delivery), then update it server-side.
  8. Handle the canceled case explicitly. Users abort the sheet all the time; your UI should return to a clean state, not show a spinner forever.

Testing Checklist

Before you ship, run through this list:

  • [ ] Sandbox card completes the full flow on a physical device
  • [ ] Cancel returns the user to a clean state
  • [ ] Token reaches the backend and the provider confirms the charge
  • [ ] Webhook updates order status server-side
  • [ ] Merchant ID is identical in code, entitlement, and portal
  • [ ] Fallback payment method exists for regions/unsupported devices
  • [ ] No secrets or merchant IDs committed to the repo

FAQ

Is pay_ios maintained? Yes — it is part of the official Flutter pay plugins maintained by Apple, alongside pay_android for Google Pay.

Do I need my own payment processing infrastructure? No. You need a payment provider agreement (Stripe, Adyen, Braintree) to decrypt the token. Apple Pay itself has no integration fee; you pay your provider's processing fee.

Can I use the same backend as Google Pay? Mostly, yes — both give you a token your backend forwards to the same gateway. That is why a single checkout backend can serve both wallets.

Why does the sheet show "Apple Pay is not available"? Almost always one of: no entitlement in the Xcode target, a merchant ID mismatch between the code and the portal, a payment processing certificate missing, or running on the Simulator. Walk the Step 1 checklist in order — it is a configuration problem, not a code problem.

Is a development device enough for testing? Yes. A physical iPhone with a sandbox card exercises the entire flow. Just remember the sandbox card only works in sandbox mode with the test entitlement — keep that pairing consistent in your build config.

Does Apple Pay work in every country? No. It is available in a growing list of countries, and availability is tied to both the App Store region and the card issuer. Test on a device configured for a supported region, and provide a fallback for everything else.

That's it — a complete Apple Pay integration in Flutter. The Dart is genuinely this short; the configuration is where you will lose time, and I hope the setup section here saves you that day.

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


*Gulshan Yad

Top comments (0)