The Journey of Apple Pay at JUST EAT

written in apple, apple pay, ios, just eat

The original post is published on the JUST EAT tech blog at the following URL http://tech.just-eat.com/2015/07/14/the-journey-of-apple-pay-at-just-eat/

Introduction

Apple Pay has recently been released in UK and at JUST EAT we worked on the integration in the iOS app to better support all of our customers and to ease the experience to both existing and new users. Until version 10 of our iOS UK app, the checkout for completing an order was wrapped into a webview and the flow was as follows:

Since Apple pushes developers to implement Apple Pay in a way that the checkout doesn’t force the user to log in, the checkout flow had to be reworked, and we took the opportunity to make the majority of the checkout flow native. This enabled us to support both checkout flows:

  • standard checkout (now with a more native flavour)

  • Apple Pay checkout

The latter is clearly a fantastic solution for completing the checkout in very few steps with a great and simple UX. Thanks to the information provided by Apple Pay (inserted by the user when registering a debit/credit card) the user details native screen is no longer necessary and more importantly for the user, there is no need to log in to the platform.

A further detail on the checkout is that we support two different so-called “service types” for the orders: delivery and collection. Defined as so:

1
2
3
4
5
6
typedef NS_ENUM(NSUInteger, JEServiceType)
{
    JEServiceTypeUnknown = 0,
    JEServiceTypeDelivery,
    JEServiceTypeCollection
};

On a side note, these changes soon became a challenge during the development as JUST EAT need to treat Apple Pay users (guest users) in a similar manner to users that have registered previously to our service.

How we designed around Apple Pay

At the time of writing there are already a few very good articles about a basic integration with Apple Pay. Probably the best reference worth mentioning is the NSHipster post.

Clearly also the Apple Documentation is a great start and the “Apple Pay Within Apps” video from WWDC 2015 explains really clearly all the relevant steps to have your app ready for Apple Pay.

Rather than discussing the basic concepts (creating the merchant ID, configuring the PKPaymentRequest object, handling the presentation of the PKPaymentAuthorizationViewController, sending the token to the Payment Service Provider, etc.), we think it’d be more useful to walk you through the architectural aspects we considered when designing the solution on iOS using Objective-C.

In the architecture we are proposing, the relevant components for handling an Apple Pay payment are the following:

  • ApplePayService
  • ApplePayPaymentHandler
  • ApplePayPaymentRequestFactory

Some additional components are also present in the big picture:

  • CheckoutService
  • ABRecordRefConverter
  • PaymentFlowController

N.B. We haven’t used the iOS SDK provided by our PSP (Payment Service Provider) to communicate directly to it from the iOS app, but rather we rely on a payment API to complete this communication.

At JUST EAT we like dependency injection and composition when possible. Developing this new feature with these concepts in mind helped to develop components that are isolated, easily pluggable, easy to test and (sometimes) reusable.

The above components have well-defined responsibilities. We’ll provide simplified code for the interfaces. Let’s go through them in a constructive order:

N.B. Don’t be alarmed if you see the usage of the JEFuture or the JEProgress symbols. Lots of parts in our codebase rely on JustPromises (the library about Future and Promises we open sourced on GitHub). You’ll also see the usage of some DTOs and the JE prefix.

  • CheckoutService: responsible for handling the basic flow for the checkout. This is very much platform dependant. It could include the logic to perform the necessary actions in the backend to prepare the order to be completed with Apple Pay. In our case we need to store the user delivery notes (things like “The door bell doesn’t work please call me when you arrive.”) and the preferred time for the delivery.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@interface JECheckoutService : NSObject

/**
 *  Composition of operations that are needed to prepare the order to be payed.
 */
- (JEFuture *)checkoutWithProgress:(JEProgress *)progress
                          basketID:(NSString *)basketID
               orderContactDetails:(JEOrderContactDetailsDTO *)orderContactDetails
                      deliveryDate:(NSDate *)deliveryDate
                     deliveryNotes:(NSString *)deliveryNotes;

- (void)cancelCheckoutForBasketWithID:(NSString *)basketID;

@end
  • ApplePayPaymentRequestFactory: responsible for creating the PKPaymentRequest objects representing a transaction. In our case objects of this kind are initialised with a delivery method and a card fee. The input parameter for the method returning a PKPaymentRequest is a representation of the basket.
1
2
3
4
5
6
7
8
9
10
@interface JEApplePayPaymentRequestFactory : NSObject

- (instancetype)initWithServiceType:(JEServiceType)serviceType
                            cardFee:(NSDecimalNumber *)cardFee NS_DESIGNATED_INITIALIZER;

- (PKPaymentRequest *)paymentRequestForBasket:(JEBasketDTO *)basketDTO;

- (NSArray *)summaryItemsForBasket:(JEBasketDTO *)basketDTO;

@end

You might wonder why summaryItemsForBasket: is public rather than keep it private. The reason is related to the fact that the block parameter ofpaymentAuthorizationViewController:didSelectShippingAddress:completion: has the following signature:

1
2
3
4
(void (^)(
PKPaymentAuthorizationStatus status,
NSArray *shippingMethods,
NSArray *summaryItems))

It might very well be that some items are not available to be shipped to a specific address, and therefore we need a way to provide the updated list of summary items for the new shipping address the user selected.

  • ApplePayPaymentHandler: objects of this class are responsible for handling the payment from beginning to end. It’s initialised with a CheckoutService that covers the initial part of the flow (i.e. the steps that happen with the standard checkout). A PKPaymentRequest and a basket representation are provided (along with some other minor details) to objects of this class when asked to actually process a payment.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@interface JEApplePayPaymentHandler : NSObject

- (instancetype)initWithServiceType:(JEServiceType)serviceType
                    checkoutService:(JECheckoutService *)checkoutService NS_DESIGNATED_INITIALIZER;

/**
 *  Handle an authorised PKPayment payment.
 *
 *  @param payment         The payment object provided by Apple Pay via the PassKit framework after the user authorised it.
 *  @param basket          The basket representation to use for creating the order.
 *  @param paymentProvider The payment provider.
 *  @param deliveryDate    The delivery time of the order.
 *  @param deliveryNotes   THe delivery notes of the order.
 *
 *  @return A future for the payment handling. Future can be successful, failed or canceled if 'cancelLastPaymentAttempt' is called during the initial state of the payment and it can therefore be aborted.
 */
- (JEFuture *)handlePayment:(PKPayment *)payment
         forOrderWithBasket:(JEBasketDTO *)basket
            paymentProvider:(NSString *)paymentProvider
               deliveryDate:(NSDate *)deliveryDate
              deliveryNotes:(NSString *)deliveryNotes;

/**
 *  Attempt to stop the payment process for a given payment.
 */
- (JEFuture *)attemptPaymentCancellation:(PKPayment *)payment;

@end
  • ApplePayService: this service is responsible for implementing the PKPaymentAuthorizationViewControllerDelegate protocol, for checking if Apple Pay is enabled on the device and for cancelling the payment (if it’s not too late). It’s initialised with a view controller (used to display the PKPaymentAuthorizationViewController), a JEApplePayPaymentRequestFactory and a JEApplePayPaymentHandler. The logic for handling the selection of shipping address or the shipping method is here.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
@protocol JEApplePayServiceDelegate <NSObject>

@optional
- (void)applePayServiceDidPresentApplePaySheet:(JEApplePayService *)service;
- (void)applePayService:(JEApplePayService *)service didCompletePaymentProcessWithSuccessForOrderWithID:(NSString *)orderID;
- (void)applePayService:(JEApplePayService *)service didCompletePaymentProcessWithError:(NSError *)error;
- (void)applePayService:(JEApplePayService *)service didFinishWithPaymentStatus:(JEApplePayServicePaymentStatus)paymentStatus;

@end

@interface JEApplePayService : NSObject

@property (nonatomic, weak) id<JEApplePayServiceDelegate> delegate;

- (instancetype)initWithPresentingViewController:(UIViewController *)presentingViewController
                           paymentRequestFactory:(JEApplePayPaymentRequestFactory *)paymentRequestFactory
                                  paymentHandler:(JEApplePayPaymentHandler *)paymentHandler NS_DESIGNATED_INITIALIZER;

+ (BOOL)isApplePayAvailableOnDevice;

/**
 *  Request the receiver to handle an Apple Pay payment.
 *
 *  @param basket          The basket representation for the order to be paid.
 *  @param paymentProvider The payment provider from the retrieved payment option.
 *  @param deliveryDate    The delivery date of the order.
 *  @param deliveryNotes   The delivery notes of the order.
 */
- (void)handlePaymentForOrderWithBasket:(JEBasketDTO *)basket
                        paymentProvider:(NSString *)paymentProvider
                           deliveryDate:(NSDate *)deliveryDate
                          deliveryNotes:(NSString *)deliveryNotes;

/**
 *  Attempt to stop the payment process for a given basket.
 */
- (void)attemptPaymentCancellationForBasket:(JEBasketDTO *)basket;

@end
  • ABRecordRefConverter: just a bunch of class methods for isolating the logic for transforming the ABRecordRef to easy-to-use DTOs. Until iOS 8.4, the delegate method paymentAuthorizationViewController:didSelectShippingAddress:completion: of PKPaymentAuthorizationViewControllerDelegate provides an ABRecordRef. Starting with iOS 9.0, this method is deprecated and a similar one using a wrapper object (PKContact) on top of the Contacts framework is used. ABRecordRefConverter basically does the work that Apple did for us in iOS 9.
1
2
3
4
5
6
7
8
9
@interface JEABRecordRefConverter : NSObject

+ (JEOrderContactDetailsDTO *)orderContactDetailsForABRecordRef:(ABRecordRef)recordRef
                                         requireShippingDetails:(BOOL)requireShippingDetails
                                                          error:(NSError **)error;

+ (JEAddressDTO *)addressForABRecordRef:(ABRecordRef)recordRef error:(NSError **)error;

@end

Similar to what happened in theApplePayPaymentRequestFactory, you might wonder why the addressForABRecordRef: method is necessary. The reason is that the second parameter ofpaymentAuthorizationViewController:didSelectShippingAddress:completion: is an ABRecordRef populated with only the address information for privacy reasons.

  • PaymentFlowController: it is responsible for creating the necessary stack and interactions between of all the above components. It acts as the delegate of the ApplePayServiceto handle the navigation flow and it should be intended to be the starting point and glue around our standard checkout flow and the Apple Pay one.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/* simplified code from JEPaymentFlowController */

JEApplePayPaymentRequestFactory *paymentRequestFactory =
[[JEApplePayPaymentRequestFactory alloc] initWithServiceType:self.serviceType
                                                     cardFee:self.applePayPaymentOption.fee];

self.checkoutService = [JECheckoutService new];

JEApplePayPaymentHandler *paymentHandler =
[[JEApplePayPaymentHandler alloc] initWithServiceType:self.serviceType
                                      checkoutService:self.checkoutService];

self.applePayService =
[[JEApplePayService alloc] initWithPresentingViewController:self.navigationController
                                      paymentRequestFactory:paymentRequestFactory
                                             paymentHandler:paymentHandler];

self.applePayService.delegate = self;

[self.applePayService
handlePaymentForOrderWithBasketID:self.basketID
                  paymentProvider:self.applePayPaymentOption.paymentProvider
                     deliveryDate:deliveryDate
                    deliveryNotes:deliveryNotes];

We strongly believe in unit testing (and automation and integration testing as well) and we strive to cover our code with the necessary tests for every new feature we develop. Payments are clearly a hot topic and a crucial part of our business, therefore structuring this feature in small and separated components allowed us to easily keep the code coverage for the above components close to 100%.

PKPaymentRequest’s pitfalls

The way the payment request is populated dictates what the Apple Pay sheet will display. How to populate the shipping and billing address properties is not completely straightforward. An excerpt of code we have in production is as follows:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/* basic PKPaymentRequest creation */

PKPaymentRequest *paymentRequest = [[PKPaymentRequest alloc] init];
paymentRequest.supportedNetworks = @[PKPaymentNetworkMasterCard, PKPaymentNetworkVisa];
paymentRequest.merchantCapabilities = PKMerchantCapability3DS;
paymentRequest.countryCode = @"GB";    // ISO 3166-1 alpha-2 country code
paymentRequest.currencyCode = @"GBP";  // ISO 4217 currency code
paymentRequest.requiredBillingAddressFields = PKAddressFieldAll;

if (__ORDER_IS_FOR_DELIVERY__)
{
    paymentRequest.requiredShippingAddressFields = PKAddressFieldAll;
    paymentRequest.shippingType = PKShippingTypeDelivery;
}
else if (__ORDER_IS_FOR_COLLECTION__)
{
    paymentRequest.requiredShippingAddressFields = PKAddressFieldPhone | PKAddressFieldEmail;
}

return paymentRequest;

Using the above configuration the Apple Pay sheets for delivery and collection orders appear like so:

Note that, in the case of collection orders, even if we set the requiredShippingAddressFields property to something meaningful (which isn’t PKAddressFieldNone), the associated cell in the sheet is not displayed. At JUST EAT we need to know upfront if the order is for delivery or collection in order to let the user fill the basket accordingly (e.g. some items might not be available for delivery) and for this reason we couldn’t leverage the built-in capabilities of Apple Pay to handle different shipping methods. Moreover, since Apple defines the shipping type like so:

1
2
3
4
5
6
typedef NS_ENUM(NSUInteger, PKShippingType) {
    PKShippingTypeShipping,
    PKShippingTypeDelivery,
    PKShippingTypeStorePickup,
    PKShippingTypeServicePickup
} NS_ENUM_AVAILABLE(NA, 8_3);

for collection orders the correct value to use would be PKShippingTypeStorePickup but the address of the store must be present in the list of addresses the user has entered on the device. This isn’t practical.

Going back to how the payment request is configured, it’s critical for JUST EAT to have the phone number and the email of the customer in order for customer services to contact them in case something goes wrong with the order. This applies to delivery orders as well as collection orders. At first we thought that, since for collection orders the shipping address was not necessary, the property requiredShippingAddressFieldsof the PKPaymentRequestcould be set to PKAddressFieldNone and we could grab the phone number and email from the billingAddress property. Unfortunately, even setting the requiredBillingAddressFields to PKAddressFieldAll when fetching the info from the billingAddress property of the PKPayment the phone number and email values are not there. Crucially, to grab the necessary order details info we had to merge the information provided by the two properties (billingAddress and shippingAddress) as so:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
@interface JEAddressDTO : NSObject

@property (nonatomic, copy, readonly) NSString *line1;
@property (nonatomic, copy, readonly) NSString *line2;
@property (nonatomic, copy, readonly) NSString *line3;
@property (nonatomic, copy, readonly) NSString *city;
@property (nonatomic, copy, readonly) NSString *postCode;

+ (instancetype)addressWithLine1:(NSString *)line1
                           line2:(NSString *)line2
                           line3:(NSString *)line3
                            city:(NSString *)city
                        postCode:(NSString *)postCode;

@end

@interface JEOrderContactDetailsDTO : NSObject

@property (nonatomic, copy, readonly) NSString *fullName;
@property (nonatomic, copy, readonly) NSString *phoneNumber;
@property (nonatomic, copy, readonly) NSString *email;
@property (nonatomic, strong, readonly) JEAddressDTO *address;

+ (instancetype)orderContactDetailsWithFullName:(NSString *)fullName
                                    phoneNumber:(NSString *)phoneNumber
                                          email:(NSString *)email
                                        address:(JEAddressDTO *)address;

@end
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
/* 
 * simplified code for composing the necessary order details info from
 * billing and shipping addresses within the `JEApplePayPaymentHandler`
 */

- (JEOrderContactDetailsDTO *)orderContactDetailsForPayment:(PKPayment *)payment
                                                serviceType:(BOOL)serviceType
                                                      error:(NSError **)error
{
    BOOL isDeliveryOrder = (serviceType == JEServiceTypeDelivery);
    NSError *shippingAddressError = nil;
    JEOrderContactDetailsDTO *contactDetailsFromShippingAddress =
    [JEABRecordRefConverter orderContactDetailsForABRecordRef:payment.shippingAddress
                                       requireShippingDetails:isDeliveryOrder
                                                        error:&shippingAddressError];

    // at this point `contactDetailsFromShippingAddress` contains
    if (shippingAddressError && error != NULL)
    {
        *error = shippingAddressError;
        return nil;
    }

    JEOrderContactDetailsDTO *orderContactDetails = contactDetailsFromShippingAddress;

    if (!isDeliveryOrder) // collection orders
    {
        NSError *billingAddressError = nil;
        JEOrderContactDetailsDTO *contactDetailsFromBillingAddress =
        [JEABRecordRefConverter orderContactDetailsForABRecordRef:payment.billingAddress
                                           requireShippingDetails:NO
                                                            error:&billingAddressError];

        if (billingAddressError && error != NULL)
        {
            *error = billingAddressError;
            return nil;
        }

        // compose the order contact details
        orderContactDetails =
        [JEOrderContactDetailsDTO orderContactDetailsWithFullName:contactDetailsFromBillingAddress.fullName
                    phoneNumber:contactDetailsFromShippingAddress.phoneNumber
                          email:contactDetailsFromShippingAddress.email
                        address:contactDetailsFromBillingAddress.address];
    }

    return orderContactDetails;
}

Integration considerations

From our journey into the Apple Pay world we learned that Apple pushes a lot for the Apple Pay button to be prominent in your app’s UI. Unless one starts an iOS application from scratch and provides only Apple Pay as a payment method, other methods are usually already available (cards, PayPal, etc.): showing the Apple Pay button as big as the other buttons leading to different payments is almost a mandatory requirement from Apple. Showing the button as a first option is equally important.

What is not so mandatory, but still very nice to have, is to provide the user with the ability to complete the checkout without logging in. This is a good thing for a few reasons:

  • remove the friction and enable happy paths to help your customers pay quicker
  • no need to collect data from the user any more, leveraging what’s already provided by Apple Pay
  • Apple and customer happiness

As in our case, this is far from being trivial and logic in the backend usually needs to be tweaked accordingly to support what we call “guest” or “anonymous” users.

On a side note, it wasn’t completely clear from the beginning that the ABRecordRef object provided in the paymentAuthorizationViewController:didSelectShippingAddress:completion: delegate method does not contain the full user data but just the address, but that is sufficient information to calculate the shipping cost and the availability of the items to a specific address. The entire user information is provided in the PKPayment object via the shippingAddressand billingAddressproperties once the user authorised the payment.

In iOS 9 the API slightly changed mainly due to introduction of the Contacts framework and the deprecation of the AddressBook one. PassKit provides a new class PKContact that is a wrapper around objects from the Contacts framework. This means that the following properties of PKPaymentRequest objects:

1
2
@property (nonatomic, assign, nullable) ABRecordRef billingAddress NS_DEPRECATED_IOS(8_0, 9_0, "Use billingContact instead");
@property (nonatomic, assign, nullable) ABRecordRef shippingAddress NS_DEPRECATED_IOS(8_0, 9_0, "Use shippingContact instead");

are deprecated in favour of the following new ones:

1
2
@property (nonatomic, retain, nullable) PKContact *billingContact NS_AVAILABLE_IOS(9_0);
@property (nonatomic, retain, nullable) PKContact *shippingContact NS_AVAILABLE_IOS(9_0);

This will help a lot since the old ABRecordRef is defined as a CFTypeRef and the related APIs are not easy to consume in an object-oriented world.


Comments