3

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

noctifer20
  • 188
  • 6

1 Answers1

1

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

noctifer20
  • 188
  • 6