Search code examples
iosswiftreact-nativepasskitapple-wallet

PKAddPaymentPassViewController dismiss on cancel


Currently I'm presenting the PKAddPaymentPassViewController in my react-native application with the following code

let delegate = PKAddPaymentPassDelegate();
let pkAddPaymentPassViewController = PKAddPaymentPassViewController.init(requestConfiguration: pkAddPaymentPassRequestConfiguration!, delegate:delegate );
DispatchQueue.main.async {
  RCTPresentedViewController()?.present(pkAddPaymentPassViewController!, animated: true, completion: nil);
}

The problem is, that when I'm taping the cancel button on the left-top, the View is not disappearing.

Has anyone faced this problem ? any help would be appreciated


Solution

  • I managed to solve this by adding dismiss functionality to error handler of PKAddPaymentPassViewControllerDelegate implementation, something like this

    func addPaymentPassViewController(
      _ controller: PKAddPaymentPassViewController,
      didFinishAdding pass: PKPaymentPass?,
      error: Error?) {
        RCTPresentedViewController()?.dismiss(animated: true, completion: nil)
    }
    

    here is whole PKAddPaymentPassViewControllerDelegate implementation, I can't explain what's going on here, nor this is a best implementation as I wrote it over a year ago with 0 swift experience whatsoever. But this is a working solution.

    //  RNPasskit.swift
    import PassKit
    import React
    
    extension Data {
      struct HexEncodingOptions: OptionSet {
        let rawValue: Int
        static let upperCase = HexEncodingOptions(rawValue: 1 << 0)
      }
    
      func hexEncodedString(options: HexEncodingOptions = []) -> String {
        let format = options.contains(.upperCase) ? "%02hhX" : "%02hhx"
        return self.map { String(format: format, $0) }.joined()
      }
    }
    
    extension String {
      var hexadecimal: Data? {
        var data = Data(capacity: count / 2)
    
        let regex = try! NSRegularExpression(pattern: "[0-9a-f]{1,2}", options: .caseInsensitive)
        regex.enumerateMatches(in: self, range: NSRange(startIndex..., in: self)) { match, _, _ in
          let byteString = (self as NSString).substring(with: match!.range)
          let num = UInt8(byteString, radix: 16)!
          data.append(num)
        }
    
        guard data.count > 0 else { return nil }
    
        return data
      }
    }
    
    
    @available(iOS 12.3, *)
    @objc(RNPassKit)
    class RNPassKit: NSObject, PKAddPaymentPassViewControllerDelegate {
    
      var accessToken: String = "";
      var cardRef: String = "";
      var isMaster: Bool = false;
    
      @objc public static var isApplePayAvailableForDevice: Bool {
          return PKAddPaymentPassViewController.canAddPaymentPass()
      }
    
      @objc(checkSupportApplePay:resolver:rejecter:)
      public func checkSupportApplePay(config: [String: String], resolve: @escaping RCTPromiseResolveBlock, rejecter reject: RCTPromiseRejectBlock) {
        let cardSuffix = config["cardSuffix"]!;
        let canAddToApplePay = PassKitCardDetector.checkSupportApplePay(cardSuffix: cardSuffix, bankName: nil);
        resolve("\(canAddToApplePay)");
      }
    
      @objc  func initToken(_ token: String) {
        Http.accessToken = token;
      }
    
      @objc(presentAddPass:resolver:rejecter:)
      func presentAddPass(config: [String: String], resolve: @escaping RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) -> Void {
        let cardNumber = config["cardNumber"]!;
    
        let json: [String: Any] = [
          "requestId": "inappprov",
          "cardNumbers": [cardNumber],
        ];
    
        Http.post(path: "/GetPanReferenceIds", json: json) { res in
          self.isMaster = config["isMaster"] == "true" ? true : false;
    
          if(res != nil && res.count > 0){
            let responseJSON = res as! [String: Any];
    
            let refList = responseJSON["refList"]! as! [[String: String]];
            let panReferenceId = refList[0]["panReferenceId"]!
    
            let pkAddPaymentPassRequestConfiguration = PKAddPaymentPassRequestConfiguration.init(encryptionScheme: .ECC_V2);
            pkAddPaymentPassRequestConfiguration?.cardholderName = config["cardholderName"];
            pkAddPaymentPassRequestConfiguration?.style = PKAddPaymentPassStyle.payment;
            pkAddPaymentPassRequestConfiguration?.primaryAccountIdentifier = panReferenceId;
            pkAddPaymentPassRequestConfiguration?.primaryAccountSuffix = config["primaryAccountSuffix"];
    
            self.cardRef = config["cardRef"]!;
    
            DispatchQueue.main.async {
              let pkAddPaymentPassViewController = PKAddPaymentPassViewController.init(requestConfiguration: pkAddPaymentPassRequestConfiguration!, delegate: self);
    
              pkAddPaymentPassViewController?.modalPresentationStyle = UIModalPresentationStyle.pageSheet;
              RCTPresentedViewController()?.present(pkAddPaymentPassViewController!, animated: true, completion: nil);
            }
    
          }
    
          resolve(nil);
        }
    
      }
    
      func addPaymentPassViewController(_ controller: PKAddPaymentPassViewController, generateRequestWithCertificateChain certificates: [Data], nonce: Data, nonceSignature: Data, completionHandler handler: @escaping (PKAddPaymentPassRequest) -> Void) {
    
        let stringCertificates = certificates.map {
          $0.hexEncodedString(options: .upperCase)
        };
    
        let json: [String: Any] = [
          "requestId": "inappprov" + self.cardRef,
          "cardRef": self.cardRef,
          "certificates": certificates.map {$0.hexEncodedString(options: .upperCase)},
          "nonce": nonce.hexEncodedString(options: .upperCase),
          "nonceSignature": nonceSignature.hexEncodedString(options: .upperCase),
        ];
    
        Http.post(path: "/GetDigitizationRequest", json: json) { res in
          if(res != nil && res.count > 0){
            let responseJSON = res as! [String: String];
    
            let encryptedPassDataString = responseJSON["encryptedPassData"]!;
            let activationDataString = responseJSON["activationData"]!;
            let ephemeralPublicKeyString = responseJSON["ephemeralPublicKey"]!;
    
            let pkAddPaymentPassRequest = PKAddPaymentPassRequest.init();
    
            if(self.isMaster){
              pkAddPaymentPassRequest.activationData = Data.init(base64Encoded: activationDataString, options: []);
            } else {
              pkAddPaymentPassRequest.activationData = activationDataString.data(using: .utf8);
            }
            pkAddPaymentPassRequest.encryptedPassData = encryptedPassDataString.hexadecimal;
            pkAddPaymentPassRequest.ephemeralPublicKey =  ephemeralPublicKeyString.hexadecimal;
            handler(pkAddPaymentPassRequest);
          }
        }
    
    
      }
    
      func addPaymentPassViewController(_ controller: PKAddPaymentPassViewController, didFinishAdding pass: PKPaymentPass?, error: Error?) {
        RCTPresentedViewController()?.dismiss(animated: true, completion: nil)
        RNPassKitEvents.emitter.sendEvent(withName: "onAppleScreen", body: [])
      }
    }
    
    

    PassKitCardDetector implementation was taken from here

    I was using RCTEventEmitter to talk back with javascript with implementation as simple as follows:

    //  RNPasskitEvents.swift
    import Foundation
    import React
    
    @objc(RNPassKitEvents)
    open class RNPassKitEvents: RCTEventEmitter {
      public static var emitter: RCTEventEmitter!
      
      override init() {
        super.init()
        RNPassKitEvents.emitter = self
      }
      
      open override func supportedEvents() -> [String] {
        ["onAppleScreen"]
      }
    }
    

    To present the provisioning screen I was calling presentAddPass as follows:

    NativeModules.RNPassKit.presentAddPass(...).then(...).catch(...)
    

    to track unsuccessful provisioning

    const eventEmitter = new NativeEventEmitter(NativeModules.RNPassKitEvents);
    eventEmitter.addListener('onAppleScreen', callback);
    

    PS. Don't forget to export methods to js

    // RNPassKit.m
    
    #import "React/RCTBridgeModule.h"
    
    @interface RCT_EXTERN_REMAP_MODULE(RNPassKit, RNPassKit, NSObject)
    RCT_EXTERN_METHOD(checkSupportApplePay: (NSDictionary *)data
                      resolver:(RCTPromiseResolveBlock)resolve
                      rejecter:(RCTPromiseRejectBlock)reject)
    RCT_EXTERN_METHOD(initToken: (NSString *)token)
    RCT_EXTERN_METHOD(presentAddPass: (NSDictionary *)data
                      resolver:(RCTPromiseResolveBlock)resolve
                      rejecter:(RCTPromiseRejectBlock)reject)
    @end
    
    
    //  RNPasskitEvents.m
    
    #import <Foundation/Foundation.h>
    #import <React/RCTBridgeModule.h>
    #import <React/RCTEventEmitter.h>
    
    @interface RCT_EXTERN_MODULE(RNPassKitEvents, RCTEventEmitter)
      RCT_EXTERN_METHOD(supportedEvents)
    @end