Pay
注意:截止 2015 年 1 月,Apple Pay 仅在美国可用。
你要在网上买东西的那一刻你会发现有一种现代化带来的独有的焦虑感。那种感觉不能用语言形容,大概是这样的:”我的信用卡去哪了?卡号是多少?我好想买这个啊卡去哪了!”
当你用 iOS 设备的时候,这种困难又加剧了一层:很有可能你的卡并不在身边,也有可能你会一手拿着信用卡另一手同时在手机上输入,这样的壮举最好还是留给体操运动员和宇航员吧。(开个玩笑,但我打赌苹果应该在某个实验室里面这样实验过了。)
如果你刚好在开发一款能够接受信用卡付款的应用,这种不幸的现象直接会导致你收入的减少。
Apple Pay 改变了这一切。大多数人可能还在关注它发布时提到的在实体店的功用(消费者可以用 iPhone NFC 功能付款),与此同时这也为开发者提升在应用内付款体验的巨大机会。
提示:如果你在应用内卖电子化商品或虚拟货币,那么应该用应用内支付而不是 Apple Pay。(请看 App Store Review Guidelines 的这一章 section 11.2)。Apple Pay 可以用来销售实体商品和服务。
获取一个 Apple Merchant ID
测试支付之前需要注册一个 Apple Merchant ID。还要先选择一个能够控制信用卡支付流程的服务提供商。苹果提供了一个这类提供商的推荐列表:Apple Pay Developer Page (作者自曝就职于这个推荐列表中的 Stripe 公司,但本文涉及到的代码和选择什么服务提供商是无关的)。你的服务提供商会给你一份特别说明指导如何在他们的平台上使用 Apple Pay 来支付,整个流程类似于这样:
- 前往 Apple Developer Center
Certificates, Identifiers, and Profiles
部分 创建一个 merchant ID。 - 接下来,前往 Certificates 部分 新建一个 Apple Pay Certificate。这步需要上传一个 Certificate Signing Request。注册服务提供商的时候他们会给你一个可用的 CSR 文件,你也可以用自己生成的 CSR 文件走完整个流程,但你的服务提供商不能用你生成的 CSR 来完成支付的解谜流程。
- 在 Xcode 里,在工程设置的 “Capabilities” 部分打开 “Apple Pay”。这步可能会要求你选择之前创建的 merchant ID。
拿到第一桶金
Apple Pay 只能在部分 iOS 设备上工作(包括 iPhone6/6+、iPad Mini 3、iPad Air 2)。另外,测试时需要添加 Apple Pay entitlement(在“获取一个 Apple Merchant ID”章节中提到过)。如果想在模拟器中测试,可以用这个库来模拟支付功能(带有测试用的信用卡信息) https://github.com/stripe/ApplePayStubs 。
一旦有了 merchant 账号,使用 Apple Pay 进行收款就近在咫尺了。接下来,需要先检查用户的设备十分支持 Apple Pay 以及用户可使用的信用卡种类:
let payment Networks = [PKPayment Network Amex, PKPayment Network Master Card, PKPayment Network Visa]
if PKPayment Authorization View Controller.can Make Payments Using Networks(payment Networks) {
// Pay is available!
} else {
// Show your own credit card form.
}
假设此时 Apple Pay 是可使用状态,下一步就是构建一个 PKPayment
。这是一个描述用户扣款信息的对象。如果付款行为发生在美国(当然目前 Apple Pay 仅在美国可用),还有其他一些你需要配置的常量:
let request = PKPayment Request()
request.supported Networks = [PKPayment Network Amex, PKPayment Network Master Card, PKPayment Network Visa]
request.country Code = "US"
request.currency Code = "USD"
request.merchant Identifier = "Replace me with your Apple Merchant ID"
request.merchant Capabilities = .Capability3DS
接下来,用 payment
属性来描述消费者购买的商品。这个属性可以接受一个 PKPayment
数组,PKPayment
有 label
和 amount
两个属性。这些属性和收据(马上就能看到收据了)上显示的条目相关。
let wax = PKPayment Summary Item(label: "Mustache Wax", amount: NSDecimal Number(string: "10.00"))
let discount = PKPayment Summary Item(label: "Discount", amount: NSDecimal Number(string: "-1.00"))
let total Amount = wax.amount.decimal Number By Adding(discount.amount)
.decimal Number By Adding(shipping.amount)
let total = PKPayment Summary Item(label: "NSHipster", amount: total Amount)
request.payment Summary Items = [wax, discount, shipping, total]
注意可以通过给条目价格赋予 0 值或负值来给消费者发放优惠或提示补充信息。然而一个收款的总金额必须大于 0。这里我们用一个 PKShipping
(继承自 PKPayment
)来描述我们发货的商品。之后会加以详述。
然后需要将付款单展示给消费者,通过 PKPayment
创建一个 PKPayment
然后 present 出来。(假设本样例中所有的代码都在一个置于付款页面后面的 UIView
中编写)。
let view Controller = PKPayment Authorization View Controller(payment Request: request)
view Controller.delegate = self
present View Controller(view Controller, animated: true, completion: nil)
小提示:
- 这个 View controller 没有占满屏幕 (这时看到的蓝色背景是我们应用中的一部分)。可以在
PKPayment
可见时随时更改背景色。Authorization View Controller - 所有的文字信息都是自动大写的。
- 最后横线下的条目和其他的是分开的,这里用来表示总金额。文字会自动以 “PAY” 开头,所以这里用公司名字作为其
label
属性开起来会比较合理一些。 - 整套 UI 是通过一个 Remote View Controller present 出来的。这意味着除了传入的
PKPayment
之外是不能修改这个 view 的其他内容或样式的。Request
PKPaymentAuthorizationViewControllerDelegate
为了捕获 PKPayment
返回的付款信息需要实现 PKPayment
接口。它有两个必须实现的方法:-(void)payment
和 -(void)payment
。
为了便于理解这些步骤都是如何工作的,我们来看一下 Apple Pay 付款的顺序流程:
- 先像如上所述显示一个
PKPayment
。Authorization View Controller - 用户通过 Touch ID(如果三次失败之后需要通过密码来验证)授权支付。
- 指纹图案变成一个旋转的加载图案,并且显示 “Processing” 字样。
- Delegate 收到
payment
回调。Authorization View Controller:did Authorize Payment:completion: - 应用同步地和付款服务商以及网站后端进行信息交换,根据付款详情进行扣款。完成之后,你要根据付款结果调用
completion
方法并传入PKPayment
或者Authorization Status.Success PKPayment
参数。Authorization Status.Failure -
PKPayment
的加载动画变成成功或者失败的图案。如果付款成功了,PassBook 会收到一个通知来对用户的信用卡进行扣款。Authorization View Controller - Delegate 收到
payment
回调,然后就可以调用Authorization View Controller Did Finish: dismiss
方法来关闭付款页面了。View Controller Animated:completion
具体代码如下:
// MARK: - PKPayment Authorization View Controller Delegate
func payment Authorization View Controller(controller: PKPayment Authorization View Controller!, did Authorize Payment payment: PKPayment!, completion: ((PKPayment Authorization Status) -> Void)!) {
// Use your payment processor's SDK to finish charging your customer.
// When this is done, call completion(PKPayment Authorization Status.Success)
}
func payment Authorization View Controller Did Finish(controller: PKPayment Authorization View Controller!) {
dismiss View Controller Animated(true, completion: nil)
}
这里的 process
方法是在你自己代码中编写的,这个方法会让扣款服务商的 SDK 结束本次支付。
动态显示物流信息和价格
如果用户通过 Apple Pay 购买了实体商品,你可能需要需要提供给他们多种物流选项。这个可以在 PKPayment
的 shipping
属性中设置。然后可以通过实现 PKPayment
中的 payment
方法来对用户做出的选择进行反馈。这个方法遵循和 did
类似的方式,在这个方法里可以同步地做一些事情然后调用回调去更新包含用户付款信息的 PKPayment
数组。(还记得吗我们之前提过由 PKPayment
继承来的 PKShipping
,这东西能帮上大忙!)
此处对上面的代码做了一点小改动,实现了一个通过计算得出的物流属性:
var payment Request: PKPayment Request {
let request = ... // initialize as before
let free Shipping = PKShipping Method(label: "Free Shipping", amount: NSDecimal Number(string: "0"))
free Shipping.identifier = "freeshipping"
free Shipping.detail = "Arrives in 6-8 weeks"
let express Shipping = PKShipping Method(label: "Express Shipping", amount: NSDecimal Number(string: "10.00"))
express Shipping.identifier = "expressshipping"
express Shipping.detail = "Arrives in 2-3 days"
request.shipping Methods = [free Shipping, express Shipping]
request.payment Summary Items = payment Summary Items For Shipping Method(free Shipping)
return request
}
func payment Summary Items For Shipping Method(shipping: PKShipping Method) -> ([PKPayment Summary Item]) {
let wax = PKPayment Summary Item(label: "Mustache Wax", amount: NSDecimal Number(string: "10.00"))
let discount = PKPayment Summary Item(label: "Discount", amount: NSDecimal Number(string: "-1.00"))
let total Amount = wax.amount.decimal Number By Adding(discount.amount)
.decimal Number By Adding(shipping.amount)
let total = PKPayment Summary Item(label: "NSHipster", amount: total Amount)
return [wax, discount, shipping, total]
}
// MARK: - PKPayment Authorization View Controller Delegate
func payment Authorization View Controller(controller: PKPayment Authorization View Controller!, did Select Shipping Method shipping Method: PKShipping Method!, completion: ((PKPayment Authorization Status, [Any Object]!) -> Void)!) {
completion(PKPayment Authorization Status.Success, payment Summary Items For Shipping Method(shipping Method))
}
在这个样例中,用户可以选择包邮和 express 发货两种选项,两种方法的费用会根据用户的选择自动调整。
别急,还有更多的东西呢
相对于提供一堆统一费率的物流选项,其实你可以让消费者选择运送地址然后动态计算物流费用。这需要设置 PKPayment
的 required
属性。这个属性含有 PKAddress
、Phone
、Postal
信息。
如果你不需要用户的具体运送地址但是需要一些联系方式(比如说需要寄送电子发票的邮箱),也可以通过这个方式来实现。
当设置了这个属性时,新的 “Shipping Address” 就会显示在付款的 UI 界面上,用户可以依次选择他们保存过的寄送地址。每当用户选择一个地址时,PKPayment
中的 payment
(顾名思义)方法就会调用。
此时你应当通过用户选择的地址信息计算出物流费用,之后调用 completion
回调并携带 3 个参数:
- 调用结果
-
PKPayment
代表成功Authorization Status.Success -
.Failure
代表网络连接出错 -
.Invalid
代表 API 返回了一个空数组(例如填入了一个不可送达的地址)Shipping Postal Address
-
- 用
PKShipping
类型的数组表示用户的可用物流选项Method - 含有物流信息的新
PKPayment
数组Summary Item
我写了一个使用 EasyPost API 通过地址能够查询邮费的简单 web 后台。源码见 https://github.com/jflinter/example-shipping-api 。
这里有一个使用 Alamofire 查询的样例:
import Address Book
import Pass Kit
import Alamofire
func addresses For Record(record: ABRecord) -> [[String: String]] {
var addresses: [[String: String]] = []
let values: ABMulti Value = ABRecord Copy Value(record, k ABPerson Address Property).take Retained Value()
for index in 0..<ABMulti Value Get Count(values) {
if let address = ABMulti Value Copy Value At Index(values, index).take Retained Value() as? [String: String] {
addresses.append(address)
}
}
return addresses
}
func fetch Shipping Methods For Address(address: [String: String], completion: ([PKShipping Method]?) -> Void) {
let parameters = [
"street": address[k ABPerson Address Street Key] ?? "",
"city": address[k ABPerson Address City Key] ?? "",
"state": address[k ABPerson Address State Key] ?? "",
"zip": address[k ABPerson Address ZIPKey] ?? "",
"country": address[k ABPerson Address Country Key] ?? ""
]
Alamofire.request(.GET, "http://example.com", parameters: parameters)
.response JSON { (_, _, JSON, _) in
if let rates = JSON as? [[String: String]] {
let shipping Methods = map(rates) { (rate) -> PKShipping Method in
let identifier = rate["id"]
let carrier = rate["carrier"] ?? "Unknown Carrier"
let service = rate["service"] ?? "Unknown Service"
let amount = NSDecimal Number(string: rate["amount"])
let arrival = rate["formatted_arrival_date"] ?? "Unknown Arrival"
let shipping Method = PKShipping Method(label: "\(carrier) \(service)", amount: amount)
shipping Method.identifier = identifier
shipping Method.detail = arrival
return shipping Method
}
}
}
}
通过这种方法,可用简单地实现 PKPayment
:
func payment Authorization View Controller(controller: PKPayment Authorization View Controller!, did Select Shipping Address record: ABRecord!, completion: ((PKPayment Authorization Status, [Any Object]!, [Any Object]!) -> Void)!) {
if let address = addresses For Record(record).first {
fetch Shipping Methods For Address(address) { (shipping Methods) in
switch shipping Methods?.count {
case .None:
completion(PKPayment Authorization Status.Failure, nil, nil)
case .Some(0):
completion(PKPayment Authorization Status.Invalid Shipping Postal Address, nil, nil)
default:
completion(PKPayment Authorization Status.Success, shipping Methods, self.payment Summary Items For Shipping Method(shipping Methods!.first!))
}
}
} else {
completion(PKPayment Authorization Status.Failure, nil, nil)
}
}
至此,用户可以选择寄送地址以及基于其居住地得到的可用物流方式。用户最终选择的 shipping
和 shipping
会成为回调到 delegate 方法 payment
中 PKPayment
对象的某些属性。
文中提到的所有源码可以在这个项目中找到:https://github.com/jflinter/ApplePayExample
虽然 Apple Pay 只开放了很少的 API,但很多 应用还是借此实现了多种多样的功能,你可以通过定制化应用到自己的应用中。其实它开启了一个购买的新篇章,比如说,用户根本不用先注册就可以买东西。
随着越来越多的应用开始使用 Apple Pay(也随着越来越多的用户拥有支持 Apple Pay 的设备),很快这就会成为 iOS 应用中普遍存在的支付方式。我很乐意看一看你们都如何用 Apple Pay 来丰富的你的产品 - 如果你有任何问题,或者想向我展示你的成果,请联系我!