Integrating Apple Pay with Flutter Bloc in a shopping app

Mohamed Elsheikh
7 min readAug 9, 2023

--

In the preceding tutorials, we took an existing shopping cart example that utilized the BLOC pattern and incorporated several new functionalities. In this guide, our focus will be on comprehending the process of enabling Apple Pay, setting up the Apple Pay button, managing successful payment transactions, emptying the cart, and navigating back to the Catalog page.

To configure Apple Pay, you should proceed with the instructions provided in this article’s steps 1 to 5 within your Apple Developer account.

Hungrimind has already published a detailed guide on this topic, and you can find it at the following link: https://www.hungrimind.com/flutter/apple_pay.

Once you finish the above steps , in the pubspec.yaml file , make sure you add the latest version of https://pub.dev/packages/pay

Then hit flutter run

Regular issue :

if you encountered this one Go to Target -> Signing & capabilties

Scroll down to Apple pay and uncheck the Merchant Id then check it again.

This should recreate and add the entitelment files needed for building.

Now hit flutter run again , and the code should compile successfully , if Not please write me in the comments and I’ll do my best to cover other issues

Create a directory called ‘pay,’ and within it, craft a file named ‘payment_configuration.dart.’

const String defaultApplePay = '''{
"provider": "apple_pay",
"data": {
"merchantIdentifier": "merchant.thinkode.fluttershoppingapp",
"displayName": "Bloc shopping app ",
"merchantCapabilities": ["3DS", "debit", "credit"],
"supportedNetworks": ["visa", "discover", "masterCard"],
"countryCode": "US",
"currencyCode": "USD",
"requiredBillingContactFields": ["emailAddress", "name", "phoneNumber", "postalAddress"],
"requiredShippingContactFields": [],
"shippingMethods": [
{
"amount": "0.00",
"detail": "Available within an hour",
"identifier": "in_store_pickup",
"label": "In-Store Pickup"
},
{
"amount": "4.99",
"detail": "5-8 Business Days",
"identifier": "flat_rate_shipping_id_2",
"label": "UPS Ground"
},
{
"amount": "29.99",
"detail": "1-3 Business Days",
"identifier": "flat_rate_shipping_id_1",
"label": "FedEx Priority Mail"
}
]
}
}''';

Inside the file named ‘cart_page.dart’ under the ‘View’ directory, let’s proceed to construct the ‘ApplePayButton.’

ApplePayButton(
onError: (error) => debugPrint('error ${error.toString()}'),
paymentConfiguration: PaymentConfiguration.fromJsonString(
payment_configurations.defaultApplePay),
paymentItems: [
PaymentItem(
label: 'Total',
amount:'0',
status: PaymentItemStatus.final_price,
type: PaymentItemType.total
),

],
style: ApplePayButtonStyle.black,
type: ApplePayButtonType.buy,
margin: const EdgeInsets.only(top: 15.0),
onPaymentResult: onApplePayResult,
loadingIndicator: const Center(
child: CircularProgressIndicator(),
),
)

Our primary focus here will be on two parameters ‘paymentItems and onPaymentResult , since the rest has the default values and no need to reconfigure in this tutorial

First : ‘paymentItems’ parameter. This parameter encapsulates the items for which the user will make a payment, encompassing the ultimate total price.

for the amount , we will need to reflect the total value of items in the cart ,

How can we get this ??

Exactly from the Cart state=>cart.totalprice

so let’s create a handy variable that has the state

Widget build(BuildContext context) {
final hugeStyle =
Theme.of(context).textTheme.displayLarge?.copyWith(fontSize: 48);
var cartState = context.read<CartBloc>().state;

then we will only compute the amount after the bloc state is loaded

amount: cartState is CartLoaded ? cartState.cart.totalPrice.toString():'0',

Here’s the full ApplePayButton

ApplePayButton(
onError: (error) => debugPrint('error111 ${error.toString()}'),
paymentConfiguration: PaymentConfiguration.fromJsonString(
payment_configurations.defaultApplePay),
paymentItems: [
PaymentItem(
label: 'Total',
amount: cartState is CartLoaded ? cartState.cart.totalPrice.toString():'0';
status: PaymentItemStatus.final_price,
type: PaymentItemType.total
),

],
style: ApplePayButtonStyle.black,
type: ApplePayButtonType.buy,
margin: const EdgeInsets.only(top: 15.0),
onPaymentResult: onApplePayResult,
loadingIndicator: const Center(
child: CircularProgressIndicator(),
),
)

Second onPaymentResult : Thats the call back when the user press the buy button

let’s then define it inside the CartTotal Stateless widget before the build function

class CartTotal extends StatelessWidget {
CartTotal({super.key});
void onApplePayResult(paymentResult) {
debugPrint(paymentResult.toString());
}

@override
Widget build(BuildContext context) {

While running this on the simulator, please note that the cards are already simulated for testing purposes. If you intend to run this on an actual device, it’s essential to set up a sandbox account. Afterward, log in to iCloud on the real device using this created account. This step is necessary to facilitate testing on a physical device.

Once You the user pay you should expect a result

 {transactionIdentifier: Simulated Identifier, paymentMethod: {type: 0, displayName: Simulated Instrument, network: Visa}, billingContact: {name: {namePrefix: , nameSuffix: , familyName: Elsheukh, middleName: , givenName: Mohamed, nickname: , phoneticRepresentation: {nameSuffix: null, phoneticRepresentation: null, givenName: , nickname: null, familyName: , middleName: , namePrefix: null}}, postalAddress: {state: Merseyside, postalCode: L6 8JH, street: 27 Hsha Street, isoCountryCode: GB, subLocality: , city: Liverpool, country: United Kingdom, subAdministrativeArea: }}, token: }

let’s then create a new method to check if the transaction is successful based on this result

bool isTransactionSuccessful (Map<String, dynamic> paymentResult)
{
if (paymentResult.containsKey('billingContact') )
return true;
else return false;
}

then let’s call it inside the onApplePayResult

 void onApplePayResult(Map<String, dynamic> paymentResult) {

if (isTransactionSuccessful(paymentResult))
{}
}

Next, our objective is to exhibit a user-friendly message that indicates whether the transaction was successful or not. To achieve this, include the following dependency in the pubspec.yaml file:

https://pub.dev/packages/fluttertoast

fluttertoast: ^8.2.2

We need to show the toast few seconds after the pay button is payed to give some time for the payment to be processed

        
Future.delayed(Duration(seconds: 3), () {
Fluttertoast.showToast(
msg: "Payment successful - Order submitted",
toastLength: Toast.LENGTH_SHORT,
gravity: ToastGravity.BOTTOM,
timeInSecForIosWeb: 1,
backgroundColor: Colors.green,
textColor: Colors.white,
fontSize: 16.0
);
});

So Here is the full method

void onApplePayResult(Map<String, dynamic> paymentResult) {

if (isTransactionSuccessful(paymentResult))
{
Future.delayed(Duration(seconds: 3), () {
Fluttertoast.showToast(
msg: "Payment successful - Order submitted",
toastLength: Toast.LENGTH_SHORT,
gravity: ToastGravity.BOTTOM,
timeInSecForIosWeb: 1,
backgroundColor: Colors.green,
textColor: Colors.white,
fontSize: 16.0
);
});

}
}

What is next :

Go back to the Catalog screen and clear the shopping cart

To be able to Navigate back to the Catalog screen we will need to call the context

1. First define a global key for navigator and call it navigator_key.dart

import 'package:flutter/material.dart';
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();

2- In the app.dart

MaterialApp(
title: 'Flutter Bloc Shopping Cart',
initialRoute: '/',
navigatorKey: navigatorKey,
routes: {
'/': (_) => const CatalogPage(),
'/cart': (_) => const CartPage(),
},
),

3- Back to onApplePayResult , let’s add

Navigator.of(navigatorKey.currentContext!).pushNamed('/');

here’s the full code

void onApplePayResult(Map<String, dynamic> paymentResult) {

if (isTransactionSuccessful(paymentResult))
{
Future.delayed(Duration(seconds: 3), () {
Fluttertoast.showToast(
msg: "Payment successful - Order submitted",
toastLength: Toast.LENGTH_SHORT,
gravity: ToastGravity.BOTTOM,
timeInSecForIosWeb: 1,
backgroundColor: Colors.green,
textColor: Colors.white,
fontSize: 16.0
);
});
}

Navigator.of(navigatorKey.currentContext!).pushNamed('/');
}

Now let’s clear the cart right after the payment is successful

To do that we will need to addto modify our cart_bloc.dart by the following :

1- Adding a new cart Event “CartItemsCleared”

2- Adding an event handler “_onClearCart”

3-Adding a repository function for clearing the cart clearCartItems

1- Inside cart_event.dart we will define our clearing event

final class CartItemsCleared extends CartEvent {
@override
List<Object> get props => [];
}

2.cart_bloc.dart , let’s set up our new event listener for clearing the cart

class CartBloc extends Bloc<CartEvent, CartState> {
CartBloc({required this.shoppingRepository}) : super(CartLoading()) {
on<CartStarted>(_onStarted);
on<CartItemAdded>(_onItemAdded);
on<CartItemRemoved>(_onItemRemoved);
on<CartItemsCleared>(_onClearCart);
}

3.in the shopping_repository.dart , let’s add a method for clearing the cart

we will clear both list and map that we use for items

Future<List<Item>> clearCartItems() {
_items.clear();
itemsWithQuantity.clear();
return Future.delayed(_delay, () => _items);

}

4. in the cart_bloc.dart

We will set the cart to be in loading state => Clear it => then set the state to be Loaded again

Future<void> _onClearCart(CartItemsCleared event, Emitter<CartState> emit) async {
emit(CartLoading());
try {
final items = await shoppingRepository.clearCartItems();
final itemsWithQuantity = await shoppingRepository.loadCartItemsWithQuantity();
emit(CartLoaded(cart: Cart(items: [...items] , itemsWithQuantity: itemsWithQuantity )));
} catch (_) {
emit(CartError());
}
}

Our very final step is to add a CartItemsCleared event once the payment is successful just before we switch back to the Catalog page

void onApplePayResult(Map<String, dynamic> paymentResult) {

if (isTransactionSuccessful(paymentResult))
{
Future.delayed(Duration(seconds: 3), () {
Fluttertoast.showToast(
msg: "Payment successful - Order submitted",
toastLength: Toast.LENGTH_SHORT,
gravity: ToastGravity.BOTTOM,
timeInSecForIosWeb: 3,
backgroundColor: Colors.green,
textColor: Colors.white,
fontSize: 16.0
);
navigatorKey.currentContext!.read<CartBloc>().add(CartItemsCleared());
Navigator.of(navigatorKey.currentContext!).pushNamed('/');

});

}

Here’s our end result , You can see the cart is cleared right after a successful payment.

To access the full code

--

--