美文网首页AppleWatch
ApplePay In-App Provisioning记录

ApplePay In-App Provisioning记录

作者: 小小棒棒糖 | 来源:发表于2020-07-02 15:07 被阅读0次

    [TOC]

    In-App是什么?

    ​ 全称:Apple Pay In-App Provisioning。就是在应用内配置信息,把用户的银行卡直接绑定到用户手机的Apple Wallet中,而不需要用户手动输入信息,提供了良好的用户体验。无需跳出应用,直接把用户信息通过Passkit SDK提供给苹果,达到绑卡的目的。 starts and finishes 整个流程都是在App中。

    ​ 我们要做的,也就是ApplePay的应用内绑卡功能。和使用AppleWallet绑卡是一样的,绑卡后可以通过ApplePay购物支付等。

    绑卡流程

    关键角色

    1. Bank Client:对接用户
    2. Bank Server:提供用户/卡信息
    3. PKPass:提供Apple Wallet相关查询/绑卡接口
    4. Apple Server:接收PKPass数据
    5. Visa:移动支付运营商(PNO:Payment Network Operator)。
    6. FD:卡信息提供商,负责发卡

    解释一下Visa与FD他们的区别:

    Visa是支付运营商,他们定义了一套支付的协议,不同的银行可以对接其协议,达成支付能力。他们会给每个银行一个编号,支付过程首先流转到Visa,再根据编号识别属于什么银行,然后做数据流转。

    类似的还有美国运通,

    FD是卡信息提供商,Bank的卡片生成也是FD生成的,包括cvv2,日期,卡号等信息。支付过程中卡信息的确认也发生在FD。

    Bank银行负责记录账户与卡之间的关系,卡余额也是记录在Bank方。

    关键短句

    1. DPAN:Device Primary Account Number 设备主账号(与银行卡号唯一对应的一串号码,如:V-999916888641233333222)
    2. FPAN:Funding Primary Account Number 资金主账号(银行卡号)
    3. SEID:苹果手机的一个序列号(NFC模块的序列号)
    4. ECC:Elliptic Curve Cryptography(椭圆曲线加密),苹果绑卡就是经过ECC-V2加密传输的。
    5. PNO:Payment Network Operator,支付网络运营商,我们目前PNO就是Visa。
    6. regular provisioning flow:常规认证流程。无论是苹果支付,或华为/小米/谷歌,有的只是加解密过程不一样,最终解密完成之后的拿到明文Payload,执行绑定操作。

    绑卡流转流程

    image

    苹果文档上介绍的,是单独的绑卡过程。下面讲述的,是我们Bank绑卡的实际流程,包括绑卡入口的判定。

    1. Bank App判断是否显示绑卡按钮。

    想要显示绑卡入口,需要满足两点:

    1. 设备及系统支持。(iphone6 ios9.0+)
    2. Bank的绑卡功能开关。Bank添加了开关功能,基于此项开的情况下,才去判断PKPassLibrary。
    3. 未被添加到当前设备(连接iwatch的情况下,需要两者都被绑定才会不显示)。

    App向Bank后台请求ApplePay相关数据,根据拿到的DPAN,放到PKPassLibrary的canAddPaymentPass接口判断是否已绑定,如已绑定则不展示。

    2. Bank用户点击Add To Apple Wallet按钮,触发绑卡流程。

    首先,Bank App会向Bank后台发起一个请求,后台返回启动绑卡的config信息,包含的关键信息有:

    1. cardHolderName 持卡人姓名
    2. paymentNetwork 支付运营商
    3. primaryAccountNumberSuffix 主账户后4位
    4. primaryAccountIdentifier(DPAN)PassKit可以根据其判断是否展示AppleWatch绑卡。

    3. App生成Config,并通过PKAddPaymentPassViewController调起In-App界面。

    PKAddPaymentPassViewController这个VC的生成有条件限制,不符合条件会返回nil,后面会细说。

    添加到Wallet入口 In-App绑卡界面 绑卡失败示例
    image image image

    4. 点击下一步,会触发PassKit代理,待App上传交易加密字段

    加密发生在Bank后台,这也是苹果推荐的一种方式,保证了数据的安全。

    苹果回调返回certificates/nonce/nonceSignature,这三个数据发送给Bank后台,后台验证证书链的合法性,如正确,把绑卡相关用户信息两重加密,回传给App。

    App把从Bank后台收到的encryptedPassData/ephemeralPublicKey/activationData,经base64反解,组装成PKAddPaymentPassRequest,通过PassKit代理中的handler传送给苹果。

    5. 苹果解密ECCV2数据,并把解密后数据K传给Visa

    加解密是整个In-App绑卡中最重要和关键的部分,也是最容易出错的部分。比较难排查,需要有苹果人员配合分析日志。

    Note:苹果对于加解密,有一份Test Vector,可以邮件苹果或者直接给苹果对接人要。有了这个Test Vector,后台就能准确对比加密过程中每一步的结果值,确保加密无误。

    Note:在我们多次的异常解决过程中,苹果给的邮件回复往往能很直接的命中要害,所以要多邮件沟通。

    6. Visa解密K得到原始JSON,进入常规认证流程。

    encryptedPassData中Payload一般是这个样子的,外面再套两层加密(for Visa,for Apple),也保证了数据传输的安全性。

    {
    "primaryAccountNumberPrefix":"496626", "encryptedPrimaryAccountNumber":"TUJQQUQtMS1GSy00MjYwNjQuMS0tVERFQS1BOEZFOEVGRTdFNzlFN",
    "nonceSignature":"408089C255A06E59FEF1702BA74715D96BC1C5CBD7CD6C90A6F06B94ED67D231765D", "networkName":"Visa",
    "name":"John Appleseed",
    "nonce":"08643998"
    }
    

    绿色/橙色流程

    对于每个绑卡认证请求,苹果都会给出对应的风险等级建议。

    • 绿色流程:大部分苹果给出的建议都是绿色流程,可以直接认证绑卡成功,不需要其它用户身份验证
    • 黄色流程:必须先验证身用户身份,由发卡行提供验证选项,具体方式可以通过(SMS/EMAIL/phone-call)
    • 橙色流程:需要更严格的身份验证,苹果推断出可能存在欺诈行为(Apple账户/历史记录),需要上报反欺诈团队严格验证。
    • 红色流程:拒绝认证

    苹果Must条款

    1. 发卡行必须支持应用内绑卡,包括iPad(提供安全/无缝的用户体验)。
    2. 提交审核资料时,必须附带应用内绑卡的相关视频。
    3. 不能自定义“Add To Apple Wallet”按钮,否则苹果会拒绝。
    4. 必须支持远程启用/禁用功能。
    5. 必须支持推送通知(后台逻辑)。
    6. 必须支持卡生命周期管理(后台逻辑)。

    iOS端接入PassKit

    1. 提供原生“Add To Apple Wallet”按钮

    因为苹果不支持自定义按钮,所以需要把原生按钮,桥接到Flutter/RN,具体代码不再展示,遵守其规则就行。

    2. 是否展示“Add To Apple Wallet”按钮

    RN写在了RNApplePayService,Flutter写在了ApplePayFlutterService中,功能包括4个:

    1. 是否需要展示按钮
    2. 是否包含此卡
    3. 卡片激活状态
    4. 开始绑卡流程
    //RNApplePayService
    //MARK: 获取AppleWallet状态: 0.不支持 1.已绑定完成(iPhone/当前连接iWatch) 2.可绑定
    @objc public func appleWalletState(_ primaryAccountIdentifier: String,
                                                _ resolve: RCTPromiseResolveBlock,
                                                _: RCTPromiseRejectBlock) {
            /// 检查是否应该显示添加到Wallet按钮
            /// @param primaryAccountIdentifier 账户标识
            guard PKAddPaymentPassViewController.canAddPaymentPass() else {
                print("客户端不能进行ApplePay的设备卡加载")
                resolve(0)
                return
            }
            // 从服务器缓存取applePaySwitch状态
            if (!SingleInstanceSettings.applePaySwitch) {
                resolve(0)
                return
            }
            // 从SDK取结果
            let library = PKPassLibrary()
            let can = library.canAddPaymentPass(withPrimaryAccountIdentifier: primaryAccountIdentifier)
            resolve(can ? 2 : 1)
        }
    

    2. 绑卡流程

    import Foundation
    import PassKit
    import RxSwift
    import XCGLogger
    
    public typealias BankAddToWalletCallback = (_ finished: Bool, _ error: NSError?) -> Void
    
    public class BankApplePayUtil: NSObject, PKAddPaymentPassViewControllerDelegate {
        public var callback: BankAddToWalletCallback?
        @objc public var cardNo = "" // 需要用户传入
        @objc public func addToWalletStart() {
            BankApplePayAPI.fetchPaymentConfiguration(cardNo: cardNo) { [weak self] result, error in
                guard let params = result else {
                    XCGLogger.default.info("接口返回有误,请检查:\(error ?? "")")
                    return
                }
                guard let config = self?.parseConfig(params) else {
                    XCGLogger.default.info("生成PKAddPaymentPassRequestConfiguration失败")
                    return
                }
                // 主线程调用UI
                DispatchQueue.main.async {
                    guard let addPaymentVC = PKAddPaymentPassViewController(requestConfiguration: config, delegate: self) else {
                        XCGLogger.default.info("AddPaymentVC生成失败,请检查!")
                        return
                    }
                    if #available(iOS 13.0, *) {
                        addPaymentVC.overrideUserInterfaceStyle = .light
                    }
                    self?.topVC?.present(addPaymentVC, animated: true, completion: nil)
                }
            }
        }
    
        private var topVC: UIViewController? {
            var controller = UIApplication.shared.keyWindow?.rootViewController
            if let rootVC = controller {
                var presentedController = rootVC.presentedViewController
                if let presentVC = presentedController, !presentVC.isBeingDismissed {
                    controller = presentedController
                    presentedController = controller?.presentedViewController
                }
                return controller
            }
            return controller
        }
    
        private func parseConfig(_ params: [String: Any]) -> PKAddPaymentPassRequestConfiguration? {
            guard let config = PKAddPaymentPassRequestConfiguration(encryptionScheme: .ECC_V2) else {
                XCGLogger.default.info("PKAddPaymentPassRequestConfiguration生成失败!")
                return nil
            }
            if #available(iOS 12.0, *) {
                config.style = .payment
            }
            config.cardholderName = params["cardHolderName"] as? String
            config.paymentNetwork = PKPaymentNetwork(params["paymentNetwork"] as? String ?? "Visa")
            config.primaryAccountSuffix = params["primaryAccountNumberSuffix"] as? String
            config.primaryAccountIdentifier = params["primaryAccountIdentifier"] as? String
            config.localizedDescription = params["localizedDescription"] as? String
            return config
        }
    
        // MARK: PKAddPaymentPassViewControllerDelegate
    
        /// 向发卡方提供证书链、nOnce, nOnceSignature等信息
        /// 重要:回调20s未执行, 则会视为失败
        /// - Parameters:
        ///   - controller: VC
        ///   - certificates: 证书链
        ///   - nonce: nonce
        ///   - nonceSignature: nonceSignature
        ///   - handler:
        ///   - activationData: ⼀次性加密OTP,⽤于确保加载请求的安全合法,由发卡方生成和验证(可省略)
        ///   - encryptedPassData: 数据加密后的JSON⽂件
        ///   - ephemeralPublicKey: ECC算法使用,发卡方生成的随机公钥
        ///   - wrappedKey
        public func addPaymentPassViewController(_: PKAddPaymentPassViewController,
                                                 generateRequestWithCertificateChain certificates: [Data],
                                                 nonce: Data,
                                                 nonceSignature: Data,
                                                 completionHandler handler: @escaping (PKAddPaymentPassRequest) -> Void) {
            // Data -> String
            func stringfy(_ data: Data) -> String {
                return data.base64EncodedString()
            }
            BankApplePayAPI.fetchPaymentData(cardNo: cardNo,
                                 certificates: certificates.map { stringfy($0) },
                                 nonce: stringfy(nonce),
                                 nonceSignature: stringfy(nonceSignature)) { result, error in
                guard let params = result else {
                    XCGLogger.default.info("接口返回有误,请检查:\(error ?? "")")
                    return
                }
                guard let data = params["encryptedPassData"] as? String,
                    let key = params["ephemeralPublicKey"] as? String,
                    let otp = params["activationData"] as? String else {
                    XCGLogger.default.info("接口返回参数有误"); return
                }
                let encryptedPassData = Data(base64Encoded: data)
                let ephemeralPublicKey = Data(base64Encoded: key)
                let activationData = Data(base64Encoded: otp)
                let request = PKAddPaymentPassRequest()
                request.activationData = activationData
    
                request.encryptedPassData = encryptedPassData
                request.ephemeralPublicKey = ephemeralPublicKey
                handler(request)
            }
        }
    
        /// 加载完成结果
        /// - Parameters:
        ///   - controller: VC
        ///   - pass: 申请得到的pass
        ///   - error: 失败参数
        public func addPaymentPassViewController(_ controller: PKAddPaymentPassViewController, didFinishAdding pass: PKPaymentPass?, error: Error?) {
            XCGLogger.default.info("\(error?.localizedDescription)")
            controller.dismiss(animated: true, completion: nil)
            if let c = self.callback {
                if pass != nil {
                    c(true, nil)
                } else if error != nil {
                    c(false, NSError.init(domain: error!.domain, code: error!.code, userInfo: nil))
                }
            }
        }
    }
    
    public class BankApplePayAPI: NSObject {
        /// 查询支付Configuration
        /// @param cardNum 卡号
        public static func fetchPaymentConfiguration(cardNo: String,
                                                    callback: @escaping (_ result: [String: Any]?, _ error: String?) -> Void) {
            var bag: DisposeBag? = DisposeBag()
            APIFetch.fetch(host: host,
                          path: path,
                          parameters: ["cardNumber": cardNo],
                          options: nil,
                          method: Post,
                          disposeBag: bag!)
                .subscribe(onNext: { json in
                    guard let dict = json as? [String: Any] else {
                        callback(nil, "返回字段非字典类型,请检查"); return
                    }
                    callback(dict["value"] as? [String: Any], nil)
                }, onError: { error in
                    callback(nil, error.localizedDescription)
                }) {
                    bag = nil; print("清理")
                }.disposed(by: bag!)
        }
    
        /// 查询支付数据
        /// @param cardNumber 卡号
        /// @param certificates 证书文件的base64字符串 0    叶子证书 1    中级证书 2    root证书  (这个没有可不传入)
        /// @param nonce 随机数
        /// @param nonceSignature 加密后随机数
        /// /mb/nmm33g/debit-card/apple/encrypt
        public static func fetchPaymentData(cardNo: String,
                                     certificates: [String],
                                     nonce: String,
                                     nonceSignature: String,
                                     callback: @escaping (_ result: [String: Any]?, _ error: String?) -> Void) {
            var bag: DisposeBag? = DisposeBag()
            BankFetch.fetch(host: host,
                          path: path,
                          parameters: [
                              "cardNumber": cardNo,
                              "certificates": certificates,
                              "nonce": nonce,
                              "nonceSignature": nonceSignature,
                          ],
                          options: nil,
                          method: Post,
                          disposeBag: bag!)
                .subscribe(onNext: { json in
                    guard let dict = json as? [String: Any] else {
                        callback(nil, "返回字段非字典类型,请检查"); return
                    }
                    callback(dict["value"] as? [String: Any], nil)
                }, onError: { error in
                    callback(nil, error.localizedDescription)
                }) {
                    bag = nil; print("清理")
                }.disposed(by: bag!)
        }
    }
    

    如何测试?

    测试必须是production环境

    苹果有文档指出可以以下3种方式:

    1. Sandbox

    2. TestFlight

    但苹果一直强调,sandbox不稳定,推荐TestFlight。Visa方不支持sandbox,只有线上环境。所以我们选择直接在TestFlight测试。

    3. AppStore

    苹果推荐,在TestFlight通过后,上线时在AppStore验证。

    注意

    1. TestFlight测试,ios最低版本必须选择>=10.3,否则调不起in-app流程。
    2. 出现绑卡失败问题,需要提供机器的SEID给苹果,苹果可以协助查找原因。

    遇到的问题点

    1. PKAddPaymentPassViewController返回nil,无法调起in-app流程

    1. 首先,苹果要给开通In-app权限,需要在develop.apple.com中,编辑并勾选权限关联到证书中。
    2. 需要在Xcode中配置entitlements文件,添加com.apple.developer.payment-pass-provisioning为YES
    3. TestFlight测试,ios最低版本必须选择>=10.3,否则调不起来。

    2. 调起in-app后,绑卡失败

    这个问题点就多了,大多失败在Visa及FD,我简单总结几点

    1. App是否开了代理。苹果能检测到抓包,直接报网络失败。我调试时是先切抓包,map显示卡tab,然后切非抓包网络,点击进入in-app流程。
    2. 白名单(卡ID + SEID)
    3. 银行后台准备JSON字段错误
    4. 银行后台加密存在错误(苹果加密一层,Visa加密一层。Bank传给苹果,苹果解密后发给Visa,Visa解密后拿到初始JSON)(1. ephemeralPublicKey 65bytes 2. ephemeralPublicKey需要转为Hex)
    5. 需要在TestFlight测试,并且testFlight要求iOS >= 10.3(很奇怪,上线却只要求>=9.0)

    3. 绑卡后,PKPassLibrary().passes()找不到对应卡片

    检查VCMM(Visa提供的录入用户数据的平台)上associatedApplicationIdentifiers字段,他是由两段组成“teamID.bundleId”,需要填入App对应的数据。如果填错,PKPassLibrary内方法将返回不准确,及拿不到passes。

    4. 绑卡后,PKPassLibrary().canAddPaymentPass仍返回true

    同上

    5. 无法智能提示iPhone或iWatch去绑卡

    当设备同时有iPhone及iWatch时,如果我的iPhone已经添加绑定,此时再次点击“Add To Apple Wallet”按钮,希望直接走iWatch的绑定(而不是出现选择界面)。

    检查PKAddPaymentPassRequestConfiguration的primaryAccountIdentifier配置,是否有传入DPAN值,它就是苹果用来筛选设备的。我们就是因为后台返回了空导致filter无效。

    6. Flutter中,“Add To Apple Wallet”的桥接UI覆盖住了Alert

    Flutter的绘制原理,就是在bitmap上绘制合成完成,才进行渲染。对于原生来说,无论你Flutter在哪个页面,页面包含多少元素,在原生调试就是薄薄的一层界面。

    而我们的首页Alert也是Flutter实现的,所以原生按钮无法被夹心,浮在了Alert的上方,导致挡住Alert。

    解决办法是:

    在初始化FlutterPlatformView时,按钮应在init中实例化,在获取View时直接return此实例。Flutter会自动判断被原生组件盖住的部分,然后再原生层上层再绘制被覆盖的区域,看上去仿佛原生被夹心。

    required init(frame: CGRect, viewID: Int64, args: Any?, binaryMessenger: FlutterBinaryMessenger) {
      button = PKAddPassButton(addPassButtonStyle: .blackOutline)
    }
    func view() -> UIView {
        return button
    }
    

    难点

    1. 调试

    相比其它需求开发,调试相对是困难的。

    1. ---。
    2. release环境,无法抓包,无法调试,很多次都通过上TestFlight,Toast报调试信息。
    3. 测试必须发到TestFlight,我使用Adhoc证书试,都不可以。
    4. 想要有卡table入口,必须加白名单。
    5. 无测试环境,只能发布生产验证bug,及bug修改后是否修复。

    2. 英文沟通

    无论是苹果还是FD,对话都是英语。

    苹果给出的官方文档,是全部英文的; 挺多次的视频通话是全英文,很考验听力; 出问题时,咨询苹果也要英文对话,全靠大能的谷歌翻译。

    相关文章

      网友评论

        本文标题:ApplePay In-App Provisioning记录

        本文链接:https://www.haomeiwen.com/subject/jycrqktx.html