接入工程
创建一个工程设置其 Bundle ID
必须和描述文件中的 Bundle ID
一致。
在 Capability
标签中把 ApplePay
选项打开,并且将 Merchant ID
选中。
项目会多了一个 ApplePay
的配置文件 ApplePayDemo.entitlements
。
在工程中导入对应的头文件 PassKit/PassKit.h
和 AddressBook/AddressBook.h
,并且遵循 PKPaymentAuthorizationViewControllerDelegate
协议。
#import <PassKit/PassKit.h> // 用户绑定的银行卡信息
#import <PassKit/PKPaymentAuthorizationViewController.h> // Apple pay的展示控件
#import <AddressBook/AddressBook.h> // 用户联系信息相关
创建支付请求
支付请求是 PKPayementRequest 类的一个实列。一个支持请求包含用户支付的物品概要清单、可选配送方式列表、用户需提供的配送信息、商家的信息以及支付处理机构。
判断用户是否能够支付
创建支付请求前,可以先通过调用 PKPaymentAuthorizationViewController 类的方法 canMakePaymentsUsingNetworks 判断用户是否能使用你支持的支付网络完成付款。canMakePayments 方法可以判断当前设备的硬件是否支持 Apple Pay
以及家长控制是否允许使用 Apple Pay
。
如果 canMakePayments
返回 NO
,则设备不支持 Apple Pay
。不要显示 Apple Pay
按扭,你可以选择使用其它的支付方式。
如果 canMakePayments
返回 YES
,但 canMakePayementsUsingNetworks:
返回 NO
,则表示设备支持 Apple Pay
,但是用户并没有为任何请求的支付网络添加银行卡。你可以选择显示一个支付设置按扭,引导用户添加银行卡。如果用户点击该按扭,则开始设置新的银行卡流程 (例如,通过调用 openPaymentSetup 方法)。
在 iOS 8.3 以及以后的系统中,你可以选择使用 PKPayementButton 方法在初始化支付请求时创建带商标的 Apple Pay 按扭。对于 iOS 8.2 以及更早的系统,你可以使用《Apple Pay 标志指南》 中提示的方法。 其它关于使用 Apple Pay 按扭以及支付标志的指南请参考《iOS 人机界面准则》 中的 Apple Pay 相关部分。
包含货币以及地区信息的支付请求
// 设置币种、国家码及merchant标识符等基本信息
PKPaymentRequest *payRequest = [[PKPaymentRequest alloc]init];
payRequest.countryCode = @"CN"; //国家代码
payRequest.currencyCode = @"CNY"; //RMB的币种代码
payRequest.merchantIdentifier = @"merchant.ApplePayDemoYasin"; //申请的merchantID
payRequest.supportedNetworks = supportedNetworks; //用户可进行支付的银行卡
payRequest.merchantCapabilities = PKMerchantCapability3DS|PKMerchantCapabilityEMV; //设置支持的交易处理协议,3DS必须支持,EMV为可选,目前国内的话还是使用两者吧
在同一个支付请求中的所有汇总金额使用相同的货币。所使用的币种可以通过 PKPaymentRequest
的 currencyCode属性指定。币种由三个字符的 ISO
货币代码指定,例如 USD
表示美元。
支付请求中的国家代码表明支付发生的国家或者支付将在哪个国家处理。由三个字符的 ISO
国家代码指定该属性,例如 US
。
在请求中指定的商户 ID
必须是应用程序有授权的商户 ID
中的某一个。
支付请求包括一系列的支付汇总项
由 PKPaymentSummaryItem 类表示支付请求中的不同部分。一个支付请求包括多个支付汇总项,一般包括:小计、折扣、配送费用、税以及总计。如果你没有其它任何额外的费用 (例如,配送或税),那么支付的总额直接是所有购买商品费用 的总和。关于每一项商品的费用的详细信息你需要在应用程序的其它合适位置显示。
每一个汇总项都有标签和金额两个部分。标签是对该项的可读描述。金额对应于所需支付的金额。一个支付请求中的所有金额都使用该请求中指定的支付货币类型。对于折扣和优惠券,其金额被设置为负值。
某些场景下,如果在支付授权的时候还不能获取应当支付的费用(例如,出租车收费),则使用 PKPaymentSummaryItemTypePending 类型做小计项,并将其金额值设置为 0.0
。系统随后会设置该项的金额值。
// 12.75 subtotal
NSDecimalNumber *subtotalAmount = [NSDecimalNumber decimalNumberWithMantissa:1275 exponent:-2 isNegative:NO];
self.subtotal = [PKPaymentSummaryItem summaryItemWithLabel:@"Subtotal" amount:subtotalAmount];
// 2.00 discount
NSDecimalNumber *discountAmount = [NSDecimalNumber decimalNumberWithMantissa:200 exponent:-2 isNegative:YES];
self.discount = [PKPaymentSummaryItem summaryItemWithLabel:@"Discount" amount:discountAmount];
汇总项使用 NSDecimalNumber 类存储金额,并且金额使用 10 进制数表示。如示例代码演示的一样,可以通过显示地指定小数部分与指数部分创建该类的实例,也可以直接使用字符串的方式指定金额。在财务计算中绝大部分情况下都是使用的 10 进制数进行计算的,例如,计算 5% 的折扣。
尽管 IEEE 浮点数 float 或 double 计算更加方便一些,但是它们并不适用于财务计算中,因为这些数字使用 2 进制表示。这意味着有些符点数不能被准确的表示,例如 0.42 只能被近似的表示为 0.41999...。这样的近似可能导致财务计算返回错误的结果。
汇总项列表中最后一项是总计项。总计项的金额是其它所有汇总项的金额的和。总计项的显示不同用于其它项。在该项中,你应该使用你的公司名称作为其标签,使用所有其它项的金额之和作为其金额值。最后,使用 paymentSummaryItems 属性将所有汇总项都添加到支付请求中。
// 10.75 grand total
NSDecimalNumber *totalAmount = [NSDecimalNumber zero];
totalAmount = [totalAmount decimalNumberByAdding:subtotalAmount];
totalAmount = [totalAmount decimalNumberByAdding:discountAmount];
self.total = [PKPaymentSummaryItem summaryItemWithLabel:@"My Company Name" amount:totalAmount];
self.summaryItems = @[self.subtotal, self.discount, self.total];
request.paymentSummaryItems = self.summaryItems;
配送方式是一个特殊的支付汇总项
// 设置两种配送方式
PKShippingMethod *freeShipping = [PKShippingMethod summaryItemWithLabel:@"包邮" amount:[NSDecimalNumber zero]];
freeShipping.identifier = @"freeshipping";
freeShipping.detail = @"6-8 天 送达";
PKShippingMethod *expressShipping = [PKShippingMethod summaryItemWithLabel:@"极速送达" amount:[NSDecimalNumber decimalNumberWithString:@"10.00"]];
expressShipping.identifier = @"expressshipping";
expressShipping.detail = @"2-3 小时 送达";
payRequest.shippingMethods = @[freeShipping, expressShipping];
为每一个可选的配送方式创建一个 PKShippingMethod 实例。与其它支付汇总项一些,配送方式也有一个用户可读的标签,例如标准配送或者可隔天配送,和一个配送金额值。与其它汇总项不同的时,配送方法有一个 detail 属性值,例如,7 月 29 日送达或者 24 小时之内送达等等。该属性值说明不同配送方式之间的区别。
为了在委托方法中区分不同的配送方式,你可以使用 identifier 属性。这个属性只被该应用使用,它对于支付框架是不可见。同样,它也不会出现在 UI
中。在创建每个配送方式的时候为其分配一个唯一的标识符。为了便于调试,推荐使用简短字符串或者字符串缩写,例如 “discount”、“standard”、“next-day” 等等。
有些配送方式并不是在所有地区都是可以使用的,或者它们费用会根据配送地址的不同而发生变化。你需要在用户选择配送地址或方法时更新其信息,详情请见 委托方法更新配送方法与费用。
指定应用程序支持的支付处理机制
supportedNetworks 属性是一个字符串常量,通过设置该值可以指定应用所支持的支付网络。 merchantCapabilities属性值说明应用程序支持的支付处理协议。3DS 协议是须支持的支付处理协议, EMV 是可选的支付处理协议。
request.supportedNetworks = @[PKPaymentNetworkAmex, PKPaymentNetworkDiscover, PKPaymentNetworkMasterCard, PKPaymentNetworkVisa];
// Supports 3DS only
request.merchantCapabilities = PKMerchantCapability3DS;
// Supports both 3DS and EMV
request.merchantCapabilities = PKMerchantCapability3DS | PKMerchantCapabilityEMV;
说明所需的配送信息和账单信息
修改支付授权视图控制器的 requiredBillingAddressFields 属性和 requiredShippingAddressFields 属性可以设置所需的账单信息和配送信息。当你显示视图控制器时,它会提示用户输入必需的账单信息和配送信息。这个域的值是通过这些属性组合而成的,如下所示:
request.requiredBillingAddressFields = PKAddressFieldEmail;
request.requiredBillingAddressFields = PKAddressFieldEmail | PKAddressFieldPostalAddress;
请求只包括处理支付和配送商品或服务的必需的账单信息和配送信息。额外的非必需信息都会增加处理事务的复杂度。每个多余的步骤都可能会增加用户取消支付的风险。
如果已有最新账单信息以及配送联系信息,你可以直接为支付请求设置这些值。Apple Pay
会默认使用这些信息。但是,用户仍然可以选择在本次支付中使用其它联系信息。
PKContact *contact = [[PKContact alloc] init];
NSPersonNameComponents *name = [[NSPersonNameComponents alloc] init];
name.givenName = @"John";
name.familyName = @"Appleseed";
contact.name = name;
CNMutablePostalAddress *address = [[CNMutablePostalAddress alloc] init];
address.street = @"1234 Laurel Street";
address.city = @"Atlanta";
address.state = @"GA";
address.postalCode = @"30303";
contact.postalAddress = address;
request.shippingContact = contact;
地址信息可以从 iOS 中很多地方获取到,请在使用它之前确保该信息是有效的。
保存其它信息
保存支付中其它与应用相关的信息,例如购物车标识,你可以使用 applicationData 属性。这个属性对于系统来说是不可见的。用户授权支付后,应用数据的哈希值也会成为支付令牌的一部分。
授权支付
支付授权过程是由支付授权视图控制器与其委托合作完成的。支付授权视图控制器做了两件事:
- 让用户选择支付请求所需的账单信息与配送信息。
- 让用户授权支付操作。
用户与视图控制器交互时,委托方法会被系统调用,所以在这些方法中你的应用可以更新所要显示的信息。例如在配送地址修改后更新配送价格。在用户授权支付请求后此方法还会被调用一次。
在实现这些委托方法时,你应该谨记它们会被多次调用并且这些方法调用的顺序是取决与用户的操作顺序的。
所有的这些委托方法在授权过程中都会被调用,传入该方法的其中一个参数是一个完成块 completion block
。支付授权视图控制器等待一个委托完成相应的方法后 (通过调用完成块) 再依次调用其它的委托方法。paymentAuthorizationViewControllerDidFinish: 方法是唯一例外:它并不需要一个完成块作为参数,它可以在任何时候被调用。
完成块接受一个输入参数,该参数为应用程序根据信息判断得到的支付事务的当前状态。如果支付事务一切正常,则应传入值 PKPaymentAuthorizationStatusSuccess
。否则,可以传入能识别出错误的值。
创建 PKPaymentAuthorizationViewController 类的实例时,你需要将已初始化后的支付请求传递给视图控制器初始化函数。接着,设置视图控制器的委托;然后再显示它:
PKPaymentAuthorizationViewController *viewController = [[PKPaymentAuthorizationViewController alloc] initWithPaymentRequest:request];
if (!viewController) { /* ... Handle error ... */ }
viewController.delegate = self;
[self presentViewController:viewController animated:YES completion:nil];
当用户与视图控制器交互时,视图控制器就会调用其委托方法:
在 Xcode 7.0 及其后的版本中,你可以在模拟器中测试支付授权视图控制器。这些版本的模拟器提供了支持所有支付网络的虚拟卡,它会以纯文本的方式返回虚拟支付数据。在设备上时,这些数据会使用商户 ID 进行加密。
虽然模拟器可以方便快捷地测试支付代码,但是你仍然需要在物理设备上测试你的支付功能。
如果你使用的是较早版本的 Xcode,那么你就只能在物理设备上测试你的支付功能了。
使用委托方法更新配送方式与配送费用
当用户输入配送信息时,授权视图控制器会调用委托的 paymentAuthorizationViewController:didSelectShippingContact:completion: 方法和 paymentAuthorizationViewController:didSelectShippingMethod:completion: 方法。你可以实现这两个方法来更新你的支付请求。
- (void)paymentAuthorizationViewController:(PKPaymentAuthorizationViewController *)controller
didSelectShippingContact:(CNContact *)contact
completion:(void (^)(PKPaymentAuthorizationStatus, NSArray *, NSArray *))completion
{
self.selectedContact = contact;
[self updateShippingCost];
NSArray *shippingMethods = [self shippingMethodsForContact:contact];
completion(PKPaymentAuthorizationStatusSuccess, shippingMethods, self.summaryItems);
}
- (void) paymentAuthorizationViewController:(PKPaymentAuthorizationViewController *)controller
didSelectShippingMethod:(PKShippingMethod *)shippingMethod
completion:(void (^)(PKPaymentAuthorizationStatus, NSArray *))completion
{
self.selectedShippingMethod = shippingMethod;
[self updateShippingCost];
completion(PKPaymentAuthorizationStatusSuccess, self.summaryItems);
}
为了保护用户隐私,提供给方法 paymentAuthorizationViewController:didSelectShippingContact:completion: 的配送信息是经过匿名化处理后的数据。返回的 contact 包含了计算配送费用的所有信息同时隐藏了用户的敏感信息。只有在用户授权支付后,你才能得到用户完整的配送信息。此外, contact 中的数据会随着国家的不同而不同,同时还会随着版本的更新而变化。请仔细测试你的应用程序。
支付被授权时创建了一个支付令牌
当用户授权一个支付请求时,支付框架的 Apple
服务器与安全模块会协作创建一个支付令牌。你可以在委托方法 paymentAuthorizationViewController:didAuthorizePayment:completion: 中将支付信息以及其它你需要处理的信息,例如配送地址和购物车标识符,一起发送至你的服务器。这个过程如下所示:
- 支付框架将支付请求发送至安全模块。只有安全模块会访问令牌化后的设备相关的支付卡号。
- 安全模块将特定卡的支付数据和商家信息一起加密(加密后的数据只有
Apple
可以访问),然后将加密后的数据发送至支付框架。支付框架再将这些数据发送至Apple
的服务器。 -
Apple
服务器使用商家标识证书将这些支付数据重新加密。这些令牌只能由你以及那些与你共享商户标识证书的人读取。随后服务器生成支付令牌再将其发送至设备。 - 支付框架调用 paymentAuthorizationViewController:didAuthorizePayment:completion: 方法将令牌发送至你的委托。你在委托方法中再将其发送至你的服务器。
在服务器上的处理操作取决于你是自己处理支付还是使用其它支付平台。不过,在两种情况下服务器都得处理订单再将处理结果返回给设备。在设备上,委托再将处理结果传入完成处理方法中,详细过程请参阅 处理支付
- (void) paymentAuthorizationViewController:(PKPaymentAuthorizationViewController *)controller
didAuthorizePayment:(PKPayment *)payment
completion:(void (^)(PKPaymentAuthorizationStatus))completion
{
NSError *error;
ABMultiValueRef addressMultiValue = ABRecordCopyValue(payment.billingAddress, kABPersonAddressProperty);
NSDictionary *addressDictionary = (__bridge_transfer NSDictionary *) ABMultiValueCopyValueAtIndex(addressMultiValue, 0);
NSData *json = [NSJSONSerialization dataWithJSONObject:addressDictionary options:NSJSONWritingPrettyPrinted error: &error];
// ... Send payment token, shipping and billing address, and order information to your server ...
PKPaymentAuthorizationStatus status; // From your server
completion(status);
}
委托方法中释放支付授权视图控制器
支付框架显示完支付事务状态后,授权视图控制器会调用委托的 aymentAuthorizationViewControllerDidFinish: 方法。在此方法的实现中,你应该释放授权视图控制器然后再显示与应用相关的支付信息界面。
- (void)paymentAuthorizationViewControllerDidFinish:(PKPaymentAuthorizationViewController *)controller
{
[controller dismissViewControllerAnimated:YES completion:nil];
}
处理支付
处理一次付款事务一般包括以下几个步骤:
- 将付款信息与其它处理订单的必需信息一起发送至你的服务器。
- 验证付款数据的散列值与签名。
- 解密出支付数据。
- 将支付数据提交给付款处理网络。
- 将订单信息提交至你的订单跟踪系统。
你有两种可选的方式处理付款过程:
- 利用已有的支付平台来处理付款。
- 自己实现付款过程。一次付款的处理过程通常情况下包括上述的大部分步骤。