整合 Apple Pay 至 iOS App

Photo by Julia Kadel on Unsplash
Photo by Julia Kadel on Unsplash
Apple Pay 是 Apple 的行動支付服務。我們可以用 Apple Pay 在實體店家的 NFC 刷卡,也可以在 apps 中購買商品。另外,iPhone 裡的 Wallet app 可以管理信用卡,所以我們不需要隨身攜帶數張信用卡。

Apple Pay 是 Apple 的行動支付服務。我們可以用 Apple Pay 在實體店家的 NFC 刷卡,也可以在 apps 中購買商品。另外,iPhone 裡的 Wallet app 可以管理信用卡,所以我們不需要隨身攜帶數張信用卡。本文章將介紹,如何將 Apple Pay 整合至 iOS app。

完整程式碼可以在 下載。

建立 Merchant ID

在開始寫程式之前,我們要先建立一個 merchant ID。登入 Apple developer,並進入 Identifiers 頁面。在頁面的右上角,選擇 Merchant IDs。點擊 Register a Merchant ID

Merchant IDs.
Merchant IDs.

選擇 Merchant IDs

Register a new merchant ID.
Register a new merchant ID.

在右邊的欄位中,輸入你的 merchant ID。Apple 建議 merchant ID 的格式為 reverse-domain name style。

Input a merchant ID.
Input a merchant ID.

確認無誤後,點擊 Register

Confirm the merchant ID.
Confirm the merchant ID.

現在 merchant IDs 列表中有我們剛剛建立好的 merchant ID。點擊它來看它的詳情。

Created a merchant ID.
Created a merchant ID.

點擊在 Apple Pay Payment Processing Certificate 下的 Create Certificate

Edit the merchant ID.
Edit the merchant ID.

選擇 Yes

Select Yes to exclusive China mainland.
Select Yes to exclusive China mainland.

這邊要我們上傳一個 Certificate Signing Request。

Create a new certificate.
Create a new certificate.

打開你 Mac OS 裡的 Keychain Access,選擇 Certificate Assistant -> Request a Certificate From a Certificate Authority...

Keychain Access -> Request a Certificate From a Certificate Authority...
Keychain Access -> Request a Certificate From a Certificate Authority…

User Email Address 中輸入你的 email。CA Email Address 可以留白。點擊 Continue 後,會儲存一個 CertificateSigningRequest.certSigningRequest。

Input user email, and leave CA Email Address empty.
Input user email, and leave CA Email Address empty.

點擊 Choose File,並上傳剛剛建立的 CertificateSigningRequest.certSigningRequest。

Upload the CSR.
Upload the CSR.

最後,Apple Pay Payment Processing Certificate 已經建立好了。點擊 Download 來下載 apple_pay.cer。

Download the certificate.
Download the certificate.

回到 merchant ID 的詳情,可以看到 Apple Pay Payment Processing Certificate 已經被建立。

Payment Processing Certificate is created.
Payment Processing Certificate is created.

新增 Apple Pay 至 Capabilities

用 Xcode 打開專案,並打開 Signing & Capabilities

Xcode -> Capabilities.
Xcode -> Capabilities.

點擊右上角的 + 來新增 Apple Pay 至 Capabilities。

Add Apple Pay capability.
Add Apple Pay capability.

新增好 Apple Pay 後,Xcode 會自動同步已經建立好的 merchant IDs。選取我們剛剛建立的 merchant ID。

Choose the merchant ID.
Choose the merchant ID.

以上就設定好了專案。打開專案中的 entitlements 檔案,可以看到 Xcode 幫我們將選取的 merchant ID 新增至 entitlements 檔案裡。

Project's entitlements.
Project’s entitlements.

初始化 PKPaymentButton

在 ViewController 中宣告 PKPaymentButton,並選擇你想要的樣式。然後,將它放到 view 上。

import UIKit
import PassKit

class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        
        initPaymentButton()
    }

    private func initPaymentButton() {
        var button: UIButton? = PKPaymentButton(paymentButtonType: .buy, paymentButtonStyle: .white)
        button?.addTarget(self, action: #selector(ViewController.onPayPressed), for: .touchUpInside)
        
        if let button = button {
            let constraints = [
                button.centerXAnchor.constraint(equalTo: view.centerXAnchor),
                button.centerYAnchor.constraint(equalTo: view.centerYAnchor)
            ]
            button.translatesAutoresizingMaskIntoConstraints = false
            view.addSubview(button)
            NSLayoutConstraint.activate(constraints)
        }
    }

    @objc private func onPayPressed(sender: AnyObject) {
        // TODO: start payment
    }
}

檢查 Apple Pay 是否可用

設定好 PKPaymentButton 後,我們要檢查是否可以使用 Apple Pay。如果可以使用 Apple Pay 的話,我們才顯示 PKPaymentButton。

class ViewController: UIViewController {
    private let viewModel = ViewModel()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        initPaymentButton()
    }

    private func initPaymentButton() {
        var button: UIButton?
        
        if viewModel.canMakePayment() {
            button = PKPaymentButton(paymentButtonType: .buy, paymentButtonStyle: .white)
            button?.addTarget(self, action: #selector(ViewController.onPayPressed), for: .touchUpInside)
        }
        
        if let button = button {
            let constraints = [
                button.centerXAnchor.constraint(equalTo: view.centerXAnchor),
                button.centerYAnchor.constraint(equalTo: view.centerYAnchor)
            ]
            button.translatesAutoresizingMaskIntoConstraints = false
            view.addSubview(button)
            NSLayoutConstraint.activate(constraints)
        }
    }
    
    @objc private func onPayPressed(sender: AnyObject) {
        // TODO: start payment
    }
}

利用 PKPaymentAuthorizationController.canMakePayments(usingNetworks:) 來檢查此裝置是否可以使用 Apple Pay。

class ViewModel {
    private static let supportedNetworks: [PKPaymentNetwork] = [
        .amex,
        .discover,
        .masterCard,
        .visa
    ]

    func canMakePayment() -> Bool {
        return PKPaymentAuthorizationController.canMakePayments(usingNetworks: Self.supportedNetworks)
    }
}

執行 Payment

最後一步就是付款了。我們必須要透過 PKPaymentAuthorizationController 來顯示 Apple Pay 的付款畫面。因此,我們要告訴它付款的金額、貨幣、運送資訊、merchant ID 等。

此外,我們還要設定 PKPaymentAuthorizationControllerDelegate。當付款完成後,我們會收到一個 PKPayment。我們可以從 PKPayment 中取得 token,並將 token 送給 server。

最後,當付款完成後, Apple Pay 付款畫面不會自動離開。我們要手動呼叫 PKPaymentAuthorizationController.dismiss()。

import UIKit
import PassKit

class ViewModel: NSObject {
    private static let merchantID = "merchant.com.waynestalk"
    private static let supportedNetworks: [PKPaymentNetwork] = [
        .amex,
        .discover,
        .masterCard,
        .visa
    ]
    
    var paymentController: PKPaymentAuthorizationController?
    var paymentSummaryItems = [PKPaymentSummaryItem]()
    var paymentStatus = PKPaymentAuthorizationStatus.failure
    var completion: ((Bool) -> Void)?

    ...
    
    func startPayment(completion: @escaping (Bool) -> Void) {
        self.completion = completion
        
        let ticket = PKPaymentSummaryItem(label: "Cloth", amount: NSDecimalNumber(string: "9.99"), type: .final)
        let tax = PKPaymentSummaryItem(label: "Tax", amount: NSDecimalNumber(string: "1.00"), type: .final)
        let total = PKPaymentSummaryItem(label: "Total", amount: NSDecimalNumber(string: "10.99"), type: .final)
        paymentSummaryItems = [ticket, tax, total]
        
        let paymentRequest = PKPaymentRequest()
        paymentRequest.paymentSummaryItems = paymentSummaryItems
        paymentRequest.merchantIdentifier = Self.merchantID
        paymentRequest.merchantCapabilities = .threeDSecure
        paymentRequest.countryCode = "US"
        paymentRequest.currencyCode = "USD"
        paymentRequest.supportedNetworks = Self.supportedNetworks
        paymentRequest.shippingType = .storePickup
        
        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.completion?(false)
            }
        })
    }
}

extension ViewModel: PKPaymentAuthorizationControllerDelegate {
    func paymentAuthorizationController(_ controller: PKPaymentAuthorizationController,
                                        didAuthorizePayment payment: PKPayment,
                                        handler completion: @escaping (PKPaymentAuthorizationResult) -> Void) {
        // Perform 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 available in the United States")
            let countryError = PKPaymentRequest.paymentShippingAddressInvalidError(withKey: CNPostalAddressCountryKey, localizedDescription: "Invalid country")
            errors.append(pickupError)
            errors.append(countryError)
            status = .failure
        } else if let token = String(data: payment.token.paymentData, encoding: .utf8) {
            // Send the payment token to your server or payment provider to process here.
            // Once processed, return an appropriate status in the completion handler (success, failure, and so on).
            debugPrint(token)
        }
        
        self.paymentStatus = status
        completion(PKPaymentAuthorizationResult(status: status, errors: errors))
    }
    
    func paymentAuthorizationControllerDidFinish(_ controller: PKPaymentAuthorizationController) {
        controller.dismiss {
            DispatchQueue.main.async {
                if self.paymentStatus == .success {
                    self.completion?(true)
                } else {
                    self.completion?(false)
                }
            }
        }
    }
}

import UIKit
import PassKit

class ViewController: UIViewController {
    private let viewModel = ViewModel()

    ...
    
    @objc private func onPayPressed(sender: AnyObject) {
        viewModel.startPayment {
            if $0 {
                debugPrint("payment has succeeded")
            }
        }
    }
}

結語

Apple Pay 的整合並不難。反而是建立 merchant ID 的步驟比較麻煩。還有,我們無法直接在 Storyboard 中新增 PKPaymentButton。當畫面比較複雜時,手動將 PKPaymentButton 加入到 view 裡並不是那麼地容易。

參考

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *

You May Also Like
Photo by Alex Alvarez on Unsplash
Read More

Dispatch Queue 教學

GCD 提供有效率的並行處理,讓我們不需要直接管理多執行緒。它的 Dispatch Queues 可以循序地(serially)或是並行地(concurrently)執行任務。我們只需要將要並行的程式當作任務提交到 dispatch queues 就可以了。
Read More
Photo by Florinel Gorgan on Unsplash
Read More

如何製作一個 XCFramework

XCFramework 讓你可以將 iPhone、iPhone 模擬器等多的不同平台的二進位碼打包到一個可發佈的 .xcframework 檔。你只需要為你的 Framework 產生出一個 .xcframework 檔,就可以支援多種平台。
Read More
Photo by Fabian Gieske on Unsplash
Read More

SwiftUI @State & @Binding 教學

SwiftUI 推出了兩個 Property Wrapper – @State and @Binding。利用它們可以達到變數的 Two-way Binding 功能。也就是當變數的值改變時,它會重新被顯示。本章藉由製作一個 Custom View 來展示如何使用 @State 和 @Binding。
Read More
Photo by Svitlana on Unsplash
Read More

iOS:禁止螢幕截圖

基於一些理由,我們可能會想要禁止使用者對我們的 app 做螢幕截圖。然而,iOS 並沒有提供這樣的功能。所幸,我們可以利用 UITextField 來達到此效果。
Read More