init
This commit is contained in:
394
ios/RNBraintreeDropIn.swift
Normal file
394
ios/RNBraintreeDropIn.swift
Normal file
@@ -0,0 +1,394 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import PassKit
|
||||
import BraintreeCore
|
||||
import BraintreeDropIn
|
||||
import BraintreeCard
|
||||
import BraintreeDataCollector
|
||||
import BraintreeApplePay
|
||||
import BraintreeVenmo
|
||||
|
||||
@objc(RNBraintreeDropIn)
|
||||
class RNBraintreeDropIn: NSObject {
|
||||
|
||||
private var dataCollector: BTDataCollector?
|
||||
private var braintreeClient: BTAPIClient?
|
||||
private var paymentRequest: PKPaymentRequest?
|
||||
private var viewController: PKPaymentAuthorizationViewController?
|
||||
private var deviceDataCollector: String = ""
|
||||
private var currentResolve: RCTPromiseResolveBlock?
|
||||
private var currentReject: RCTPromiseRejectBlock?
|
||||
private var applePayAuthorized: Bool = false
|
||||
|
||||
override init() {
|
||||
super.init()
|
||||
}
|
||||
|
||||
@objc
|
||||
static func requiresMainQueueSetup() -> Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
@objc
|
||||
func show(_ options: NSDictionary,
|
||||
resolve: @escaping RCTPromiseResolveBlock,
|
||||
reject: @escaping RCTPromiseRejectBlock) {
|
||||
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
guard let self = self else { return }
|
||||
|
||||
self.currentResolve = resolve
|
||||
self.currentReject = reject
|
||||
self.applePayAuthorized = false
|
||||
|
||||
guard let clientToken = options["clientToken"] as? String else {
|
||||
reject("NO_CLIENT_TOKEN", "You must provide a client token", nil)
|
||||
return
|
||||
}
|
||||
|
||||
// Setup color scheme
|
||||
var colorScheme: BTDropInUICustomization.ColorScheme = .light
|
||||
if let darkTheme = options["darkTheme"] as? Bool, darkTheme {
|
||||
if #available(iOS 13.0, *) {
|
||||
colorScheme = .dynamic
|
||||
} else {
|
||||
colorScheme = .dark
|
||||
}
|
||||
}
|
||||
|
||||
let uiCustomization = BTDropInUICustomization(colorScheme: colorScheme)
|
||||
|
||||
if let fontFamily = options["fontFamily"] as? String {
|
||||
uiCustomization.fontFamily = fontFamily
|
||||
}
|
||||
if let boldFontFamily = options["boldFontFamily"] as? String {
|
||||
uiCustomization.boldFontFamily = boldFontFamily
|
||||
}
|
||||
|
||||
let request = BTDropInRequest()
|
||||
request.uiCustomization = uiCustomization
|
||||
|
||||
// 3D Secure setup
|
||||
if let threeDSecureOptions = options["threeDSecure"] as? NSDictionary {
|
||||
guard let amount = threeDSecureOptions["amount"] as? NSNumber else {
|
||||
reject("NO_3DS_AMOUNT", "You must provide an amount for 3D Secure", nil)
|
||||
return
|
||||
}
|
||||
|
||||
let threeDSecureRequest = BTThreeDSecureRequest()
|
||||
threeDSecureRequest.amount = NSDecimalNumber(value: amount.doubleValue)
|
||||
request.threeDSecureRequest = threeDSecureRequest
|
||||
}
|
||||
|
||||
// Initialize API client and data collector
|
||||
let apiClient = BTAPIClient(authorization: clientToken)!
|
||||
self.dataCollector = BTDataCollector(apiClient: apiClient)
|
||||
self.dataCollector?.collectDeviceData { [weak self] deviceData in
|
||||
self?.deviceDataCollector = deviceData
|
||||
}
|
||||
|
||||
// Vault manager
|
||||
if let vaultManager = options["vaultManager"] as? Bool, vaultManager {
|
||||
request.vaultManager = true
|
||||
}
|
||||
|
||||
// Card disabled
|
||||
if let cardDisabled = options["cardDisabled"] as? Bool, cardDisabled {
|
||||
request.cardDisabled = true
|
||||
}
|
||||
|
||||
// Apple Pay setup
|
||||
if let applePay = options["applePay"] as? Bool, applePay {
|
||||
self.setupApplePay(options: options, clientToken: clientToken, reject: reject)
|
||||
// Don't disable Apple Pay in request
|
||||
} else {
|
||||
request.applePayDisabled = true
|
||||
}
|
||||
|
||||
// Venmo setup
|
||||
if let venmo = options["venmo"] as? Bool, venmo {
|
||||
request.venmoDisabled = false
|
||||
} else {
|
||||
request.venmoDisabled = true
|
||||
}
|
||||
|
||||
// PayPal setup
|
||||
if let payPal = options["payPal"] as? Bool, !payPal {
|
||||
request.paypalDisabled = true
|
||||
}
|
||||
|
||||
// Initialize Drop-In controller
|
||||
let dropIn = BTDropInController(authorization: clientToken, request: request) { [weak self] (controller, result, error) in
|
||||
guard let self = self else { return }
|
||||
|
||||
controller.dismiss(animated: true) {
|
||||
if let error = error {
|
||||
self.currentReject?("DROP_IN_ERROR", error.localizedDescription, error)
|
||||
} else if result?.isCanceled == true {
|
||||
self.currentReject?("USER_CANCELLATION", "The user cancelled", nil)
|
||||
} else if let result = result {
|
||||
self.handleDropInResult(result, threeDSecureOptions: options["threeDSecure"] as? NSDictionary)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let dropIn = dropIn {
|
||||
guard let rootViewController = self.getRootViewController() else {
|
||||
reject("NO_ROOT_VC", "Could not find root view controller", nil)
|
||||
return
|
||||
}
|
||||
rootViewController.present(dropIn, animated: true, completion: nil)
|
||||
} else {
|
||||
reject("INVALID_CLIENT_TOKEN", "The client token seems invalid", nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func setupApplePay(options: NSDictionary, clientToken: String, reject: @escaping RCTPromiseRejectBlock) {
|
||||
guard let merchantIdentifier = options["merchantIdentifier"] as? String,
|
||||
let countryCode = options["countryCode"] as? String,
|
||||
let currencyCode = options["currencyCode"] as? String,
|
||||
let merchantName = options["merchantName"] as? String,
|
||||
let orderTotal = options["orderTotal"] as? NSNumber else {
|
||||
reject("MISSING_OPTIONS", "Not all required Apple Pay options were provided", nil)
|
||||
return
|
||||
}
|
||||
|
||||
self.braintreeClient = BTAPIClient(authorization: clientToken)
|
||||
|
||||
let paymentRequest = PKPaymentRequest()
|
||||
paymentRequest.merchantIdentifier = merchantIdentifier
|
||||
paymentRequest.merchantCapabilities = .capability3DS
|
||||
paymentRequest.countryCode = countryCode
|
||||
paymentRequest.currencyCode = currencyCode
|
||||
paymentRequest.supportedNetworks = [.amex, .visa, .masterCard, .discover, .chinaUnionPay]
|
||||
|
||||
let paymentSummaryItem = PKPaymentSummaryItem(
|
||||
label: merchantName,
|
||||
amount: NSDecimalNumber(value: orderTotal.doubleValue)
|
||||
)
|
||||
paymentRequest.paymentSummaryItems = [paymentSummaryItem]
|
||||
|
||||
self.paymentRequest = paymentRequest
|
||||
|
||||
if let vc = PKPaymentAuthorizationViewController(paymentRequest: paymentRequest) {
|
||||
vc.delegate = self
|
||||
self.viewController = vc
|
||||
}
|
||||
}
|
||||
|
||||
private func handleDropInResult(_ result: BTDropInResult, threeDSecureOptions: NSDictionary?) {
|
||||
// Check for 3D Secure
|
||||
if let threeDSecureOptions = threeDSecureOptions,
|
||||
let cardNonce = result.paymentMethod as? BTCardNonce {
|
||||
|
||||
if let threeDSecureInfo = cardNonce.threeDSecureInfo {
|
||||
if !threeDSecureInfo.liabilityShiftPossible && threeDSecureInfo.wasVerified {
|
||||
self.currentReject?("3DSECURE_NOT_ABLE_TO_SHIFT_LIABILITY", "3D Secure liability cannot be shifted", nil)
|
||||
return
|
||||
} else if !threeDSecureInfo.liabilityShifted && threeDSecureInfo.wasVerified {
|
||||
self.currentReject?("3DSECURE_LIABILITY_NOT_SHIFTED", "3D Secure liability was not shifted", nil)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check for Apple Pay
|
||||
if result.paymentMethod == nil,
|
||||
let paymentMethodType = result.paymentMethodType?.rawValue,
|
||||
(16...18).contains(paymentMethodType) {
|
||||
// Apple Pay flow
|
||||
if let viewController = self.viewController,
|
||||
let rootViewController = self.getRootViewController() {
|
||||
rootViewController.present(viewController, animated: true, completion: nil)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Check for Venmo
|
||||
if let venmoNonce = result.paymentMethod as? BTVenmoAccountNonce {
|
||||
let resultDict: [String: Any] = [
|
||||
"nonce": venmoNonce.nonce,
|
||||
"type": "Venmo",
|
||||
"description": "Venmo \(venmoNonce.username)",
|
||||
"isDefault": false,
|
||||
"deviceData": self.deviceDataCollector
|
||||
]
|
||||
self.currentResolve?(resultDict)
|
||||
return
|
||||
}
|
||||
|
||||
// Default payment method handling
|
||||
self.resolvePayment(result: result)
|
||||
}
|
||||
|
||||
@objc
|
||||
func fetchMostRecentPaymentMethod(_ clientToken: String,
|
||||
resolve: @escaping RCTPromiseResolveBlock,
|
||||
reject: @escaping RCTPromiseRejectBlock) {
|
||||
|
||||
BTDropInResult.mostRecentPaymentMethod(forClientToken: clientToken) { [weak self] (result, error) in
|
||||
if let error = error {
|
||||
reject("FETCH_ERROR", error.localizedDescription, error)
|
||||
} else if result?.isCanceled == true {
|
||||
reject("USER_CANCELLATION", "The user cancelled", nil)
|
||||
} else if let result = result {
|
||||
self?.resolvePayment(result: result)
|
||||
resolve(self?.buildResultDictionary(from: result))
|
||||
} else {
|
||||
resolve(nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@objc
|
||||
func tokenizeCard(_ clientToken: String,
|
||||
cardInfo: NSDictionary,
|
||||
resolve: @escaping RCTPromiseResolveBlock,
|
||||
reject: @escaping RCTPromiseRejectBlock) {
|
||||
|
||||
let cvv = cardInfo["cvv"] as? String
|
||||
let onlyCVV = cardInfo["onlyCVV"] as? Bool ?? false
|
||||
|
||||
if onlyCVV {
|
||||
guard cvv != nil else {
|
||||
reject("INVALID_CARD_INFO", "Please enter cvv", nil)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
guard let _ = cardInfo["number"] as? String,
|
||||
let _ = cardInfo["expirationMonth"] as? String,
|
||||
let _ = cardInfo["expirationYear"] as? String,
|
||||
cvv != nil,
|
||||
let _ = cardInfo["postalCode"] as? String else {
|
||||
reject("INVALID_CARD_INFO", "Invalid card info", nil)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
let braintreeClient = BTAPIClient(authorization: clientToken)!
|
||||
let cardClient = BTCardClient(apiClient: braintreeClient)
|
||||
let card = BTCard()
|
||||
|
||||
if !onlyCVV {
|
||||
card.number = cardInfo["number"] as? String
|
||||
card.expirationMonth = cardInfo["expirationMonth"] as? String
|
||||
card.expirationYear = cardInfo["expirationYear"] as? String
|
||||
card.postalCode = cardInfo["postalCode"] as? String
|
||||
}
|
||||
card.cvv = cvv
|
||||
|
||||
cardClient.tokenize(card) { [weak self] (tokenizedCard, error) in
|
||||
if let error = error {
|
||||
reject("TOKENIZE_ERROR", "Error tokenizing card", error)
|
||||
} else if let tokenizedCard = tokenizedCard {
|
||||
var jsResult: [String: Any] = [
|
||||
"nonce": tokenizedCard.nonce,
|
||||
"type": "card"
|
||||
]
|
||||
|
||||
let dataCollector = BTDataCollector(apiClient: braintreeClient)
|
||||
dataCollector.collectDeviceData { deviceData in
|
||||
if !deviceData.isEmpty {
|
||||
jsResult["deviceData"] = deviceData
|
||||
resolve(jsResult)
|
||||
} else {
|
||||
reject("DEVICE_DATA_ERROR", "Failed to collect device data", nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@objc
|
||||
func collectDeviceData(_ clientToken: String,
|
||||
resolve: @escaping RCTPromiseResolveBlock,
|
||||
reject: @escaping RCTPromiseRejectBlock) {
|
||||
|
||||
let apiClient = BTAPIClient(authorization: clientToken)!
|
||||
let dataCollector = BTDataCollector(apiClient: apiClient)
|
||||
|
||||
dataCollector.collectDeviceData { deviceData in
|
||||
if !deviceData.isEmpty {
|
||||
resolve(deviceData)
|
||||
} else {
|
||||
reject("DEVICE_DATA_ERROR", "Failed to collect device data", nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func resolvePayment(result: BTDropInResult) {
|
||||
guard let resolve = self.currentResolve else { return }
|
||||
let resultDict = buildResultDictionary(from: result)
|
||||
resolve(resultDict)
|
||||
}
|
||||
|
||||
private func buildResultDictionary(from result: BTDropInResult) -> [String: Any]? {
|
||||
guard let paymentMethod = result.paymentMethod else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return [
|
||||
"nonce": paymentMethod.nonce,
|
||||
"type": paymentMethod.type,
|
||||
"description": result.paymentDescription ?? "",
|
||||
"isDefault": paymentMethod.isDefault,
|
||||
"deviceData": self.deviceDataCollector
|
||||
]
|
||||
}
|
||||
|
||||
private func getRootViewController() -> UIViewController? {
|
||||
guard let window = UIApplication.shared.windows.first(where: { $0.isKeyWindow }) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
var rootViewController = window.rootViewController
|
||||
if let presented = rootViewController?.presentedViewController {
|
||||
rootViewController = presented
|
||||
}
|
||||
|
||||
return rootViewController
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - PKPaymentAuthorizationViewControllerDelegate
|
||||
extension RNBraintreeDropIn: PKPaymentAuthorizationViewControllerDelegate {
|
||||
|
||||
func paymentAuthorizationViewController(_ controller: PKPaymentAuthorizationViewController,
|
||||
didAuthorizePayment payment: PKPayment,
|
||||
handler completion: @escaping (PKPaymentAuthorizationResult) -> Void) {
|
||||
|
||||
guard let braintreeClient = self.braintreeClient else {
|
||||
completion(PKPaymentAuthorizationResult(status: .failure, errors: nil))
|
||||
return
|
||||
}
|
||||
|
||||
let applePayClient = BTApplePayClient(apiClient: braintreeClient)
|
||||
applePayClient.tokenize(payment) { [weak self] (tokenizedPayment, error) in
|
||||
if let tokenizedPayment = tokenizedPayment {
|
||||
completion(PKPaymentAuthorizationResult(status: .success, errors: nil))
|
||||
self?.applePayAuthorized = true
|
||||
|
||||
let result: [String: Any] = [
|
||||
"nonce": tokenizedPayment.nonce,
|
||||
"type": "Apple Pay",
|
||||
"description": "Apple Pay \(tokenizedPayment.type ?? "")",
|
||||
"isDefault": false,
|
||||
"deviceData": self?.deviceDataCollector ?? ""
|
||||
]
|
||||
|
||||
self?.currentResolve?(result)
|
||||
} else {
|
||||
completion(PKPaymentAuthorizationResult(status: .failure, errors: nil))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func paymentAuthorizationViewControllerDidFinish(_ controller: PKPaymentAuthorizationViewController) {
|
||||
controller.dismiss(animated: true) { [weak self] in
|
||||
if self?.applePayAuthorized == false {
|
||||
self?.currentReject?("USER_CANCELLATION", "The user cancelled", nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user