Apple Pay with SwiftUI

0

Full article

This is in my opinion the most important part of having an app. Besides fulfillment, we all want to get paid so we can keep working on things we love, but without proper documentation this task can become very tedious...

All the code information on this Article has been extracted and summarized from the official apple documentation.
Up to this point (Jun 23, 2020) Apple has not yet release the documentation to implement Apple Pay in SwiftUI, you can rise a request for documentation and design guidelines in the Feedback Assistant website.

In this guide I'll try to implement as simple as possible Apple Pay in the new framework SwiftUI. The page follows the guide from the Official Documentation with faster steps.

Requirements

I'll use Xcode to do this since it's way easier and faster. This can also be done on the web

To set up your environment to implement Apple Pay in your apps, you must complete three steps:

  • Enable Apple Pay in Xcode and Create a merchant ID.

Go to Xcode and click on the top of your project name, select Signing & Capabilities, then Targets and add the new Capability Apple Pay.

  1. Below the Merchant IDs table, click the Add button (+).  
  2. In the dialog that appears, enter the merchant identifier name.
  3. An app group container ID begins with merchant. followed by a string in reverse DNS notation.
  4. Click OK.

The merchant ID appears selected in the table.

A payment processing certificate is associated with your merchant identifier and used to encrypt payment information. The payment processing certificate expires every 25 months. If the certificate is revoked, you can recreate it.

  1. In Certificates, Identifiers & Profiles, select Identifiers from the sidebar.
  2. Under Identifiers, select Merchant IDs using the filter in the top-right.
  3. On the right, select your merchant identifier.

Note: If a banner appears at the top of the page saying that you need to accept an agreement, click the Review Agreement button and follow the instructions before continuing.

Under Apple Pay Payment Processing Certificate, click Create Certificate. You will need to upload the certificate you'll create below:

Create a certificate signing request on your Mac.

Now back to the previous page, Click Choose File and select the certificated created above (a file with a .certSigningRequest file extension).

Click Continue, Download it on your Mac and double click to install it in your Key Chain.

In-App Purchase Overview

Offer customers extra content and features using in-app purchases — including premium content, digital goods, and subscriptions — directly within your app. You can even promote and offer in-app purchases directly on the App Store.

Official Doc:
In-App Purchase, StoreKit & Workflow for configuring in-app purchases

Each developer account can create up to 10,000 in-app purchase products across all the apps in the account. You can use the same in-app purchases for all supported platforms (iOS, macOS, tvOS) of your app if they are part of the same app record in App Store Connect.

There are four In-App Purchase types you can offer:

  • Consumables are a type that are depleted after one use. Customers can purchase them multiple times.
  • Non-consumables are a type that customers purchase once. They don't expire.
  • Auto-renewable subscriptions to services or content are a type that customers purchase once and that renew automatically on a recurring basis until customers decide to cancel.
  • Non-renewing subscriptions to services or content provide access over a limited duration and don't renew automatically. Customers can purchase them again.

You can sync and restore non-consumables and auto-renewable subscriptions across devices using StoreKit. When a user purchases an auto-renewable or non-renewing subscription, your app is responsible for making it available across all the user's devices, and for enabling users to restore past purchases.

Configure In-App Purchase

To offer in-app purchases inside your app, you must add in-app purchase information to your app in App Store Connect.

Official Doc:
Configuring in-app purchases

Required role: You must have the Account Holder, Admin, App Manager, Developer, or Marketing role to add and edit in-app purchases. See Role permissions.

1- Go to App Store Connect and from My Apps, select your app.
2- In the sidebar under In-App Purchases, click Manage.
3- To add an in-app purchase, go to In-App Purchases and click the Add button (+).

4- Select Consumable, Non-Consumable, or Non-Renewing Subscriptions and click Create.
5- Add:
  - The reference name (this is the name of the product you are selling)
  - Product ID (this can be a sequence of numbers, 1, 2, 3, ...)
  - A localized display name.
6- Click Save, or Submit for Review.

Add App Review information

Add Review Notes and an image to help Apple review your in-app purchase.

Host non-consumables with Apple

For example: Premium Pass that never expires and can be restored.

You can have Apple host non-consumable in-app purchase products when you first create the products in App Store Connect, or convert content that you are currently hosting on your own servers to be hosted by Apple.

Apple provides a reliable and familiar experience for users and handles distributing products to their devices and restoring products. When you host content with Apple, Apple stores your app’s content using the same infrastructure that supports other large-scale operations. Additionally, Apple automatically downloads Apple-hosted content in the background even if your app isn’t running.

To host content with Apple:

1- From My Apps, select your app.
2- In the sidebar under In-App Purchases, click Manage.
3- Click on the in-app purchase you want to view or edit.
4- Scroll down to the Content Hosting section.
5- Select Turn on Content Hosting or Turn off Content Hosting, then click Save.

Setting up Code

Time to Configure In-App Purchases in App Store Connect using through code.

  1. Setting Up the Transaction Observer for the Payment Queue
    Enable your app to receive and handle transactions by adding an observer.

Create an Observer:
Create and build out a custom observer class to handle changes to the payment queue:

Reveal Code

class StoreObserver: NSObject, SKPaymentTransactionObserver {
    //Initialize the store observer.
    override init() {
        super.init()
        //Other initialization here.
    }


    //Observe transaction updates.
    func paymentQueue(_ queue: SKPaymentQueue,updatedTransactions transactions: [SKPaymentTransaction]) {
        //Handle transaction states here.
    }
}

In AppDelegate.swift, we need to do the registers and removes of the observer from the payment queue. StoreKit can notify your SKPaymentTransactionObserver instance automatically when the content of the payment queue changes upon resuming or while running your app:

Reveal Code

import UIKit
import StoreKit

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

    let iapObserver = StoreObserver()

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        // Attach an observer to the payment queue.
        SKPaymentQueue.default().add(iapObserver)
        return true
    }
    
    // Called when the application is about to terminate.
    func applicationWillTerminate(_ application: UIApplication) {
        // Remove the observer.
        SKPaymentQueue.default().remove(iapObserver)
    }
    ...
}

Offering, Completing, and Restoring In-App Purchases

Fetch, complete, and restore transactions in your app.

- Head over to your project in Xcode. Select the nameOfProject  in the Project navigator, then select it again under Targets. Select the General tab, and make sure your Team is set to your correct team, and check the bundle ID.
- Next select the Capabilities ta and add new In-App Purchase Capability:

Upload in-app purchase content to App Store Connect

Upload the in-app purchase content to App Store Connect using Xcode.

1- Select your project, click on the first left + under target, and create a new in-app purchase content project, select the In-App Purchase Content template under Cross-platform.
2- Upload in-app purchase content: choose the in-app purchase content target from the scheme menu in the toolbar. Choose Product > Archive.
3- In the Archives organizer, select the in-app purchase content archive, then click “Distribute Content.”

4- Follow the submit steps...

Configure auto-renewable subscription

Setting up auto-renewable subscriptions differs from setting up other in-app purchase types. Each auto-renewable subscription product will need to be created as part of a subscription group and assigned a level. How you set up your subscription group or groups will determine how customers can subscribe to your content or services, how they move between subscriptions, when they are billed, and your proceeds rate. For guidance on the subscription business model, see Offering Subscriptions.

Official Doc:
Configuring in-app purchases

More coming soon...

Restore In-App Purchase

Official Doc:
Restoring In-App Purchases

More coming soon...

Create a Sandbox Tester Account

The Apple Pay sandbox environment allows merchants and developers to test their implementation of Apple Pay with test credit and debit cards.

Official Doc:
Sandbox Testing
Note: It is also important to test Apple Pay in your production environment. Real cards must be used in the production environment. Test cards will not work.

You’ll need the following to test Apple Pay in the sandbox:

  • iPhone 6 or later, iPad mini 3 or later, iPad Air 2, iPad Pro, or Apple Watch
  • App Store Connect sandbox tester account

To create a sandbox tester account, follow these steps:

  1. Sign in to App Store Connect.
  2. On the homepage, click Users and Access.
  3. Under Sandbox, click Testers.
  4. Click “+” to set up your tester accounts.
  5. Complete the tester information form and click Invite.
  6. Sign out of your Apple ID on all testing devices and sign back in with your new sandbox tester account.

Important: If you mistakenly use a sandbox tester account to sign in to a production environment, like iTunes, on your test device instead of your test environment, the sandbox account becomes invalid and can’t be used again. If this happens, create a new sandbox tester account with a new email address.

Adding a Test Card Number

  • In your signed in Tester Account, go to Wallet and tap Add Credit or Debit Card.
  • Choose A Debit/Credit Card from here.
Note: To provision test cards on your device and use the sandbox, you will need to make sure that your device’s region is set to a country or region that supports Apple Pay.

Code Implementation (Apple Connect rejected it, currently working on a better implementation)

Part of the code below has been taken from the official documentation

You can structure it however you'd like, here's how I did it, create 3 classes:

1. PaymentHandler.swift

Reveal Code

//A shared class for handling payment across an app and its related extensions

 import UIKit
import PassKit

typealias PaymentCompletionHandler = (Bool) -> Void

class PaymentHandler: NSObject {

    var paymentController: PKPaymentAuthorizationController?
    var paymentSummaryItems = [PKPaymentSummaryItem]()
    var paymentStatus = PKPaymentAuthorizationStatus.failure
    var completionHandler: PaymentCompletionHandler!

    static let supportedNetworks: [PKPaymentNetwork] = [
        .amex,
        .discover,
        .masterCard,
        .visa
    ]

    class func applePayStatus() -> (canMakePayments: Bool, canSetupCards: Bool) {
        return (PKPaymentAuthorizationController.canMakePayments(),
                PKPaymentAuthorizationController.canMakePayments(usingNetworks: supportedNetworks))
    }
    
    func startPayment(completion: @escaping PaymentCompletionHandler) {

        completionHandler = completion
        
        let fare = PKPaymentSummaryItem(label: "Minimum Fare", amount: NSDecimalNumber(string: "3.99"), type: .final)
        let tax = PKPaymentSummaryItem(label: "Tax", amount: NSDecimalNumber(string: "1.00"), type: .final)
        let total = PKPaymentSummaryItem(label: "Total", amount: NSDecimalNumber(string: "5.99"), type: .pending)
        paymentSummaryItems = [fare, tax, total]

        // Create our payment request
        let paymentRequest = PKPaymentRequest()
        paymentRequest.paymentSummaryItems = paymentSummaryItems
        paymentRequest.merchantIdentifier = ApplePayConfiguration.Merchant.identifier
        paymentRequest.merchantCapabilities = .capability3DS
        paymentRequest.countryCode = "US"
        paymentRequest.currencyCode = "USD"
        paymentRequest.requiredShippingContactFields = [.postalAddress, .phoneNumber]
        paymentRequest.supportedNetworks = PaymentHandler.supportedNetworks
        
        // Display our payment request
        paymentController = PKPaymentAuthorizationController(paymentRequest: paymentRequest)
        paymentController?.delegate = self
        paymentController?.present(completion: { (presented: Bool) in
            if presented {
                debugPrint("Presented payment controller")
            } else {
                debugPrint("Failed to present payment controller")
                self.completionHandler(false)
            }
        })
    }
}

/*
    PKPaymentAuthorizationControllerDelegate conformance.
 */
extension PaymentHandler: PKPaymentAuthorizationControllerDelegate {

    func paymentAuthorizationController(_ controller: PKPaymentAuthorizationController, didAuthorizePayment payment: PKPayment, handler completion: @escaping (PKPaymentAuthorizationResult) -> Void) {
        
        // Perform some very basic validation on the provided contact information
        var errors = [Error]()
        var status = PKPaymentAuthorizationStatus.success
        if payment.shippingContact?.postalAddress?.isoCountryCode != "US" {
            let pickupError = PKPaymentRequest.paymentShippingAddressUnserviceableError(withLocalizedDescription: "Sample App only picks up in the United States")
            let countryError = PKPaymentRequest.paymentShippingAddressInvalidError(withKey: CNPostalAddressCountryKey, localizedDescription: "Invalid country")
            errors.append(pickupError)
            errors.append(countryError)
            status = .failure
        } else {
            // Here you would send the payment token to your server or payment provider to process
            // Once processed, return an appropriate status in the completion handler (success, failure, etc)
        }
        
        self.paymentStatus = status
        completion(PKPaymentAuthorizationResult(status: status, errors: errors))
    }
    
    func paymentAuthorizationControllerDidFinish(_ controller: PKPaymentAuthorizationController) {
        controller.dismiss {
            // We are responsible for dismissing the payment sheet once it has finished
            DispatchQueue.main.async {
                if self.paymentStatus == .success {
                    self.completionHandler!(true)
                } else {
                    self.completionHandler!(false)
                }
            }
        }
    }
    
    func paymentAuthorizationController(_ controller: PKPaymentAuthorizationController, didSelectPaymentMethod paymentMethod: PKPaymentMethod, handler completion: @escaping (PKPaymentRequestPaymentMethodUpdate) -> Void) {
        // The didSelectPaymentMethod delegate method allows you to make changes when the user updates their payment card
        // Here we're applying a $2 discount when a debit card is selected
        guard paymentMethod.type == .debit else {
            completion(PKPaymentRequestPaymentMethodUpdate(paymentSummaryItems: paymentSummaryItems))
            return
        }

        var discountedSummaryItems = paymentSummaryItems
        let discount = PKPaymentSummaryItem(label: "Debit Card Discount", amount: NSDecimalNumber(string: "-2.00"))
        discountedSummaryItems.insert(discount, at: paymentSummaryItems.count - 1)
        if let total = paymentSummaryItems.last {
            total.amount = total.amount.subtracting(NSDecimalNumber(string: "2.00"))
        }
        completion(PKPaymentRequestPaymentMethodUpdate(paymentSummaryItems: discountedSummaryItems))
    }
}

2. ApplePayConfiguration.swift

Reveal Code

// Handles configuration logic for the Apple Pay merchant identifier

import Foundation

public class ApplePayConfiguration {
    /*
     The value of the `OFFERING_APPLE_PAY_BUNDLE_PREFIX` user-defined build
     setting is written to the Info.plist file of every target in Swift
     version of the sample project. Specifically, the value of
     `OFFERING_APPLE_PAY_BUNDLE_PREFIX` is used as the string value for a
     key of `AAPLOfferingApplePayBundlePrefix`. This value is loaded from the
     target's bundle by the lazily evaluated static variable "prefix" from
     the nested "Bundle" struct below the first time that "Bundle.prefix"
     is accessed. This avoids the need for developers to edit both
     `OFFERING_APPLE_PAY_BUNDLE_PREFIX` and the code below. The value of
     `Bundle.prefix` is then used as part of an interpolated string to insert
     the user-defined value of `OFFERING_APPLE_PAY_BUNDLE_PREFIX` into several
     static string constants below.
     */

    private struct MainBundle {
        static var prefix: String = {
            guard let prefix = Bundle.main.object(forInfoDictionaryKey: "AAPLOfferingApplePayBundlePrefix") as? String else {
                return ""
            }
            return prefix
        }()
    }

    struct Merchant {
        static let identifier = "this is your mercant id for Apple Pay"
    }
}

3. ApplePayButton.swift

The button has to be in line with Apple's human-interface-guidelines.

Reveal Code

import SwiftUI
import PassKit

struct ApplePayButtonView: View {
    
    let paymentHandler = PaymentHandler()
    
    var body: some View {
        Button( action: {
            self.paymentHandler.startPayment { (success) in
                if success {
                    print("Success")
                } else {
                    print("Failed")
                }
            }
        }, label: { Text("")} )
            .frame(width: 212, height: 38, alignment: .center)
            .buttonStyle(ApplePayButtonStyle())
    }
}

struct ApplePayButton: UIViewRepresentable {
    func updateUIView(_ uiView: PKPaymentButton, context: Context) {
        
    }
    func makeUIView(context: Context) -> PKPaymentButton {
        return PKPaymentButton(paymentButtonType: .plain, paymentButtonStyle: .black)
    }
}
struct ApplePayButtonStyle: ButtonStyle {
    func makeBody(configuration: Self.Configuration) -> some View {
        return ApplePayButton()
    }
}

Now you can call ApplePayButtonView() in any view and walah! Magic.

I hope this was useful to you. If there is something wrong or an updated way to accomplish the above please leave a comment below.

Comments