Apple Pay 是 Apple 的行動支付服務。我們可以用 Apple Pay 在實體店家的 NFC 刷卡,也可以在 apps 中購買商品。另外,iPhone 裡的 Wallet app 可以管理信用卡,所以我們不需要隨身攜帶數張信用卡。本文章將介紹,如何將 Apple Pay 整合至 iOS app。
Table of Contents
建立 Merchant ID
在開始寫程式之前,我們要先建立一個 merchant ID。登入 Apple developer,並進入 Identifiers
頁面。在頁面的右上角,選擇 Merchant IDs
。點擊 Register a Merchant ID
。
選擇 Merchant IDs
。
在右邊的欄位中,輸入你的 merchant ID。Apple 建議 merchant ID 的格式為 reverse-domain name style。
確認無誤後,點擊 Register
。
現在 merchant IDs 列表中有我們剛剛建立好的 merchant ID。點擊它來看它的詳情。
點擊在 Apple Pay Payment Processing Certificate 下的 Create Certificate
。
選擇 Yes
。
這邊要我們上傳一個 Certificate Signing Request。
打開你 Mac OS 裡的 Keychain Access
,選擇 Certificate Assistant -> Request a Certificate From a Certificate Authority...
。
在 User Email Address
中輸入你的 email。CA Email Address
可以留白。點擊 Continue
後,會儲存一個 CertificateSigningRequest.certSigningRequest。
點擊 Choose File
,並上傳剛剛建立的 CertificateSigningRequest.certSigningRequest。
最後,Apple Pay Payment Processing Certificate 已經建立好了。點擊 Download
來下載 apple_pay.cer。
回到 merchant ID 的詳情,可以看到 Apple Pay Payment Processing Certificate 已經被建立。
新增 Apple Pay 至 Capabilities
用 Xcode 打開專案,並打開 Signing & Capabilities
。
點擊右上角的 +
來新增 Apple Pay 至 Capabilities。
新增好 Apple Pay 後,Xcode 會自動同步已經建立好的 merchant IDs。選取我們剛剛建立的 merchant ID。
以上就設定好了專案。打開專案中的 entitlements 檔案,可以看到 Xcode 幫我們將選取的 merchant ID 新增至 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 裡並不是那麼地容易。
參考
- PassKit (Apple Pay and Wallet), Apple Developer.