diff --git a/packages/stripe_ios/ios/Classes/Stripe Sdk/ApplePayViewController.swift b/packages/stripe_ios/ios/Classes/Stripe Sdk/ApplePayViewController.swift index bc5da251e..57e1e472b 100644 --- a/packages/stripe_ios/ios/Classes/Stripe Sdk/ApplePayViewController.swift +++ b/packages/stripe_ios/ios/Classes/Stripe Sdk/ApplePayViewController.swift @@ -21,9 +21,13 @@ extension StripeSdk : PKPaymentAuthorizationViewControllerDelegate, STPApplePayC if let error = error { self.createPlatformPayPaymentMethodResolver?(Errors.createError(ErrorType.Failed, error)) } else { - let promiseResult = [ - "token": token != nil ? Mappers.mapFromToken(token: token!.splitApplePayAddressByNewline()) : [:] + var promiseResult = [ + "token": token != nil ? Mappers.mapFromToken(token: token!.splitApplePayAddressByNewline()) : [:], ] + if let shippingContact = payment.shippingContact { + promiseResult["shippingContact"] = Mappers.mapFromShippingContact(shippingContact: shippingContact) + } + self.createPlatformPayPaymentMethodResolver?(promiseResult) } completion(PKPaymentAuthorizationResult.init(status: .success, errors: nil)) @@ -33,9 +37,13 @@ extension StripeSdk : PKPaymentAuthorizationViewControllerDelegate, STPApplePayC if let error = error { self.createPlatformPayPaymentMethodResolver?(Errors.createError(ErrorType.Failed, error)) } else { - let promiseResult = [ + var promiseResult = [ "paymentMethod": Mappers.mapFromPaymentMethod(paymentMethod?.splitApplePayAddressByNewline()) ?? [:] ] + if let shippingContact = payment.shippingContact { + promiseResult["shippingContact"] = Mappers.mapFromShippingContact(shippingContact: shippingContact) + } + self.createPlatformPayPaymentMethodResolver?(promiseResult) } completion(PKPaymentAuthorizationResult.init(status: .success, errors: nil)) @@ -157,6 +165,7 @@ extension StripeSdk : PKPaymentAuthorizationViewControllerDelegate, STPApplePayC paymentInformation: PKPayment, completion: @escaping STPIntentClientSecretCompletionBlock ) { + self.confirmApplePayPaymentMethod = paymentMethod if let clientSecret = self.confirmApplePayPaymentClientSecret { completion(clientSecret, nil) } else if let clientSecret = self.confirmApplePaySetupClientSecret { @@ -199,10 +208,15 @@ extension StripeSdk : PKPaymentAuthorizationViewControllerDelegate, STPApplePayC } if let paymentIntent = paymentIntent { - resolve(Mappers.createResult("paymentIntent", Mappers.mapFromPaymentIntent(paymentIntent: paymentIntent))) + let result = Mappers.mapFromPaymentIntent(paymentIntent: paymentIntent) + if (paymentIntent.paymentMethod == nil) { + result.setValue(Mappers.mapFromPaymentMethod(self.confirmApplePayPaymentMethod), forKey: "paymentMethod") + } + resolve(Mappers.createResult("paymentIntent", result)) } else { resolve(Mappers.createResult("paymentIntent", nil)) } + self.confirmApplePayPaymentMethod = nil } } else if let clientSecret = self.confirmApplePaySetupClientSecret { STPAPIClient.shared.retrieveSetupIntent(withClientSecret: clientSecret) { (setupIntent, error) in @@ -216,10 +230,15 @@ extension StripeSdk : PKPaymentAuthorizationViewControllerDelegate, STPApplePayC } if let setupIntent = setupIntent { - resolve(Mappers.createResult("setupIntent", Mappers.mapFromSetupIntent(setupIntent: setupIntent))) + let result = Mappers.mapFromSetupIntent(setupIntent: setupIntent) + if (setupIntent.paymentMethod == nil) { + result.setValue(Mappers.mapFromPaymentMethod(self.confirmApplePayPaymentMethod), forKey: "paymentMethod") + } + resolve(Mappers.createResult("setupIntent", result)) } else { resolve(Mappers.createResult("setupIntent", nil)) } + self.confirmApplePayPaymentMethod = nil } } } diff --git a/packages/stripe_ios/ios/Classes/Stripe Sdk/CustomerSheet/CustomerSheetUtils.swift b/packages/stripe_ios/ios/Classes/Stripe Sdk/CustomerSheet/CustomerSheetUtils.swift new file mode 100644 index 000000000..3b71ebc33 --- /dev/null +++ b/packages/stripe_ios/ios/Classes/Stripe Sdk/CustomerSheet/CustomerSheetUtils.swift @@ -0,0 +1,181 @@ +// +// CustomerSheetUtils.swift +// stripe-react-native +// +// Created by Charles Cruzan on 08/28/23. +// + +import Foundation +@_spi(PrivateBetaCustomerSheet) import StripePaymentSheet + +class CustomerSheetUtils { + internal class func buildCustomerSheetConfiguration( + appearance: PaymentSheet.Appearance, + style: PaymentSheet.UserInterfaceStyle, + removeSavedPaymentMethodMessage: String?, + returnURL: String?, + headerTextForSelectionScreen: String?, + applePayEnabled: Bool?, + merchantDisplayName: String?, + billingDetailsCollectionConfiguration: NSDictionary?, + defaultBillingDetails: NSDictionary? + ) -> CustomerSheet.Configuration { + var config = CustomerSheet.Configuration() + config.appearance = appearance + config.style = style + config.removeSavedPaymentMethodMessage = removeSavedPaymentMethodMessage + config.returnURL = returnURL + config.headerTextForSelectionScreen = headerTextForSelectionScreen + config.applePayEnabled = applePayEnabled ?? false + if let merchantDisplayName = merchantDisplayName { + config.merchantDisplayName = merchantDisplayName + } + if let billingConfigParams = billingDetailsCollectionConfiguration { + config.billingDetailsCollectionConfiguration.name = StripeSdk.mapToCollectionMode(str: billingConfigParams["name"] as? String) + config.billingDetailsCollectionConfiguration.phone = StripeSdk.mapToCollectionMode(str: billingConfigParams["phone"] as? String) + config.billingDetailsCollectionConfiguration.email = StripeSdk.mapToCollectionMode(str: billingConfigParams["email"] as? String) + config.billingDetailsCollectionConfiguration.address = StripeSdk.mapToAddressCollectionMode(str: billingConfigParams["address"] as? String) + config.billingDetailsCollectionConfiguration.attachDefaultsToPaymentMethod = billingConfigParams["attachDefaultsToPaymentMethod"] as? Bool == true + } + if let defaultBillingDetails = defaultBillingDetails { + config.defaultBillingDetails.name = defaultBillingDetails["name"] as? String + config.defaultBillingDetails.email = defaultBillingDetails["email"] as? String + config.defaultBillingDetails.phone = defaultBillingDetails["phone"] as? String + if let address = defaultBillingDetails["address"] as? [String: String] { + config.defaultBillingDetails.address = .init(city: address["city"], + country: address["country"], + line1: address["line1"], + line2: address["line2"], + postalCode: address["postalCode"], + state: address["state"]) + } + } + return config + } + + internal class func buildStripeCustomerAdapter( + customerId: String, + ephemeralKeySecret: String, + setupIntentClientSecret: String?, + customerAdapter: NSDictionary, + stripeSdk: StripeSdk + ) -> StripeCustomerAdapter { + if (customerAdapter.count > 0) { + return buildCustomerAdapterOverride( + customerAdapter: customerAdapter, + customerId: customerId, + ephemeralKeySecret: ephemeralKeySecret, + setupIntentClientSecret: setupIntentClientSecret, + stripeSdk: stripeSdk + ) + } + + if let setupIntentClientSecret = setupIntentClientSecret { + return StripeCustomerAdapter( + customerEphemeralKeyProvider: { + return CustomerEphemeralKey(customerId: customerId, ephemeralKeySecret: ephemeralKeySecret) + }, + setupIntentClientSecretProvider: { + return setupIntentClientSecret + } + ) + } + + return StripeCustomerAdapter( + customerEphemeralKeyProvider: { + return CustomerEphemeralKey(customerId: customerId, ephemeralKeySecret: ephemeralKeySecret) + } + ) + } + + internal class func buildCustomerAdapterOverride( + customerAdapter: NSDictionary, + customerId: String, + ephemeralKeySecret: String, + setupIntentClientSecret: String?, + stripeSdk: StripeSdk + ) -> StripeCustomerAdapter { + return ReactNativeCustomerAdapter( + fetchPaymentMethods: customerAdapter["fetchPaymentMethods"] as? Bool ?? false, + attachPaymentMethod: customerAdapter["attachPaymentMethod"] as? Bool ?? false, + detachPaymentMethod: customerAdapter["detachPaymentMethod"] as? Bool ?? false, + setSelectedPaymentOption: customerAdapter["setSelectedPaymentOption"] as? Bool ?? false, + fetchSelectedPaymentOption: customerAdapter["fetchSelectedPaymentOption"] as? Bool ?? false, + setupIntentClientSecretForCustomerAttach: customerAdapter["setupIntentClientSecretForCustomerAttach"] as? Bool ?? false, + customerId: customerId, + ephemeralKeySecret: ephemeralKeySecret, + setupIntentClientSecret: setupIntentClientSecret, + stripeSdk: stripeSdk + ) + } + + internal class func getModalPresentationStyle(_ string: String?) -> UIModalPresentationStyle { + switch (string) { + case "fullscreen": + return .fullScreen + case "popover": + fallthrough + default: + return .popover + } + } + + internal class func getModalTransitionStyle(_ string: String?) -> UIModalTransitionStyle { + switch (string) { + case "flip": + return .flipHorizontal + case "curl": + return .partialCurl + case "dissolve": + return .crossDissolve + case "slide": + fallthrough + default: + return .coverVertical + } + } + + + internal class func buildPaymentOptionResult(label: String, imageData: String?, paymentMethod: STPPaymentMethod?) -> NSMutableDictionary { + let result: NSMutableDictionary = [:] + let paymentOption: NSMutableDictionary = [:] + paymentOption.setValue(label, forKey: "label") + if (imageData != nil) { + paymentOption.setValue(imageData, forKey: "image") + } + result.setValue(paymentOption, forKey: "paymentOption") + if (paymentMethod != nil) { + result.setValue(Mappers.mapFromPaymentMethod(paymentMethod), forKey: "paymentMethod") + } + return result + } + + internal class func interpretResult(result: CustomerSheet.CustomerSheetResult) -> NSDictionary { + var payload: NSMutableDictionary = [:] + switch result { + case .error(let error): + return Errors.createError(ErrorType.Failed, error as NSError) + case .selected(let paymentOption): + switch paymentOption { + case .applePay(let paymentOptionDisplayData): + payload = CustomerSheetUtils.buildPaymentOptionResult(label: paymentOptionDisplayData.label, imageData: paymentOptionDisplayData.image.pngData()?.base64EncodedString(), paymentMethod: nil) + case .paymentMethod(let paymentMethod, let paymentOptionDisplayData): + payload = CustomerSheetUtils.buildPaymentOptionResult(label: paymentOptionDisplayData.label, imageData: paymentOptionDisplayData.image.pngData()?.base64EncodedString(), paymentMethod: paymentMethod) + case .none: + break + } + case .canceled(let paymentOption): + switch paymentOption { + case .applePay(let paymentOptionDisplayData): + payload = CustomerSheetUtils.buildPaymentOptionResult(label: paymentOptionDisplayData.label, imageData: paymentOptionDisplayData.image.pngData()?.base64EncodedString(), paymentMethod: nil) + case .paymentMethod(let paymentMethod, let paymentOptionDisplayData): + payload = CustomerSheetUtils.buildPaymentOptionResult(label: paymentOptionDisplayData.label, imageData: paymentOptionDisplayData.image.pngData()?.base64EncodedString(), paymentMethod: paymentMethod) + case .none: + break + } + payload.setValue(["code": ErrorType.Canceled], forKey: "error") + } + return payload + } + +} diff --git a/packages/stripe_ios/ios/Classes/Stripe Sdk/CustomerSheet/ReactNativeCustomerAdapter.swift b/packages/stripe_ios/ios/Classes/Stripe Sdk/CustomerSheet/ReactNativeCustomerAdapter.swift new file mode 100644 index 000000000..c1ccf81f4 --- /dev/null +++ b/packages/stripe_ios/ios/Classes/Stripe Sdk/CustomerSheet/ReactNativeCustomerAdapter.swift @@ -0,0 +1,173 @@ +// +// ReactNativeCustomerAdapter.swift +// stripe-react-native +// +// Created by Charlie Cruzan on 9/5/23. +// + +import Foundation +@_spi(PrivateBetaCustomerSheet) @_spi(STP) import StripePaymentSheet + +class ReactNativeCustomerAdapter: StripeCustomerAdapter { + var overridesFetchPaymentMethods: Bool + var overridesAttachPaymentMethod: Bool + var overridesDetachPaymentMethod: Bool + var overridesSetSelectedPaymentOption: Bool + var overridesFetchSelectedPaymentOption: Bool + var overridesSetupIntentClientSecretForCustomerAttach: Bool + var stripeSdk: StripeSdk + + init( + fetchPaymentMethods: Bool, + attachPaymentMethod: Bool, + detachPaymentMethod: Bool, + setSelectedPaymentOption: Bool, + fetchSelectedPaymentOption: Bool, + setupIntentClientSecretForCustomerAttach: Bool, + customerId: String, + ephemeralKeySecret: String, + setupIntentClientSecret: String?, + stripeSdk: StripeSdk + ) { + self.overridesFetchPaymentMethods = fetchPaymentMethods + self.overridesAttachPaymentMethod = attachPaymentMethod + self.overridesDetachPaymentMethod = detachPaymentMethod + self.overridesSetSelectedPaymentOption = setSelectedPaymentOption + self.overridesFetchSelectedPaymentOption = fetchSelectedPaymentOption + self.overridesSetupIntentClientSecretForCustomerAttach = setupIntentClientSecretForCustomerAttach + self.stripeSdk = stripeSdk + + if let setupIntentClientSecret = setupIntentClientSecret { + super.init( + customerEphemeralKeyProvider: { + return CustomerEphemeralKey(customerId: customerId, ephemeralKeySecret: ephemeralKeySecret) + }, + setupIntentClientSecretProvider: { + return setupIntentClientSecret + } + ) + } else { + super.init( + customerEphemeralKeyProvider: { + return CustomerEphemeralKey(customerId: customerId, ephemeralKeySecret: ephemeralKeySecret) + } + ) + } + } + + override func fetchPaymentMethods() async throws -> [STPPaymentMethod] { + if (self.overridesFetchPaymentMethods) { + return await withCheckedContinuation({ continuation in + fetchPaymentMethods { paymentMethods in + continuation.resume(returning: paymentMethods) + } + }) + } else { + return try await super.fetchPaymentMethods() + } + } + + override func attachPaymentMethod(_ paymentMethodId: String) async throws { + if (self.overridesAttachPaymentMethod) { + return await withCheckedContinuation({ continuation in + attachPaymentMethod(paymentMethodId) { + continuation.resume(returning: ()) + } + }) + } else { + return try await super.attachPaymentMethod(paymentMethodId) + } + } + + override func detachPaymentMethod(paymentMethodId: String) async throws { + if (self.overridesDetachPaymentMethod) { + return await withCheckedContinuation({ continuation in + detachPaymentMethod(paymentMethodId) { + continuation.resume(returning: ()) + } + }) + } else { + return try await super.detachPaymentMethod(paymentMethodId: paymentMethodId) + } + } + + override func setSelectedPaymentOption(paymentOption: CustomerPaymentOption?) async throws { + if (self.overridesSetSelectedPaymentOption) { + return await withCheckedContinuation({ continuation in + setSelectedPaymentOption(paymentOption) { + continuation.resume(returning: ()) + } + }) + } else { + return try await super.setSelectedPaymentOption(paymentOption: paymentOption) + } + } + + override func fetchSelectedPaymentOption() async throws -> CustomerPaymentOption? { + if (self.overridesFetchSelectedPaymentOption) { + return await withCheckedContinuation({ continuation in + fetchSelectedPaymentOption { paymentOption in + continuation.resume(returning: paymentOption) + } + }) + } else { + return try await super.fetchSelectedPaymentOption() + } + } + + override func setupIntentClientSecretForCustomerAttach() async throws -> String { + if (self.overridesSetupIntentClientSecretForCustomerAttach) { + return await withCheckedContinuation({ continuation in + setupIntentClientSecretForCustomerAttach { clientSecret in + continuation.resume(returning: clientSecret) + } + }) + } else { + return try await super.setupIntentClientSecretForCustomerAttach() + } + } +} + +extension ReactNativeCustomerAdapter { + func fetchPaymentMethods(completion: @escaping ([STPPaymentMethod]) -> Void) { + DispatchQueue.main.async { + self.stripeSdk.fetchPaymentMethodsCallback = completion + self.stripeSdk.sendEvent(withName: "onCustomerAdapterFetchPaymentMethodsCallback", body: [:]) + } + } + + func attachPaymentMethod(_ paymentMethodId: String, completion: @escaping () -> Void) { + DispatchQueue.main.async { + self.stripeSdk.attachPaymentMethodCallback = completion + self.stripeSdk.sendEvent(withName: "onCustomerAdapterAttachPaymentMethodCallback", body: ["paymentMethodId": paymentMethodId]) + } + } + + func detachPaymentMethod(_ paymentMethodId: String, completion: @escaping () -> Void) { + DispatchQueue.main.async { + self.stripeSdk.detachPaymentMethodCallback = completion + self.stripeSdk.sendEvent(withName: "onCustomerAdapterDetachPaymentMethodCallback", body: ["paymentMethodId": paymentMethodId]) + } + } + + func setSelectedPaymentOption(_ paymentOption: CustomerPaymentOption?, completion: @escaping () -> Void) { + DispatchQueue.main.async { + self.stripeSdk.setSelectedPaymentOptionCallback = completion + self.stripeSdk.sendEvent(withName: "onCustomerAdapterSetSelectedPaymentOptionCallback", body: ["paymentOption": paymentOption?.value]) + } + } + + func fetchSelectedPaymentOption(completion: @escaping (CustomerPaymentOption?) -> Void) { + DispatchQueue.main.async { + self.stripeSdk.fetchSelectedPaymentOptionCallback = completion + self.stripeSdk.sendEvent(withName: "onCustomerAdapterFetchSelectedPaymentOptionCallback", body: [:]) + } + } + + func setupIntentClientSecretForCustomerAttach(completion: @escaping (String) -> Void) { + DispatchQueue.main.async { + self.stripeSdk.setupIntentClientSecretForCustomerAttachCallback = completion + self.stripeSdk.sendEvent(withName: "onCustomerAdapterSetupIntentClientSecretForCustomerAttachCallback", body: [:]) + } + } +} diff --git a/packages/stripe_ios/ios/Classes/Stripe Sdk/Mappers.swift b/packages/stripe_ios/ios/Classes/Stripe Sdk/Mappers.swift index fc1dba808..88880cab4 100644 --- a/packages/stripe_ios/ios/Classes/Stripe Sdk/Mappers.swift +++ b/packages/stripe_ios/ios/Classes/Stripe Sdk/Mappers.swift @@ -451,14 +451,16 @@ class Mappers { "voucherURL": it.oxxoDisplayDetails?.hostedVoucherURL.absoluteString ?? NSNull(), "voucherNumber": it.oxxoDisplayDetails?.number ?? NSNull(), ] -// TODO: Not supported on Android -// case .boletoDisplayDetails: -// return [ -// "type": "boletoVoucher", -// "expiration": it.boletoDisplayDetails?.expiresAt.timeIntervalSince1970.description ?? NSNull(), -// "voucherURL": it.boletoDisplayDetails?.hostedVoucherURL.absoluteString ?? NSNull(), -// "voucherNumber": it.boletoDisplayDetails?.number ?? NSNull(), -// ] + case .boletoDisplayDetails: + return [ + "type": "boletoVoucher", + "voucherURL": it.boletoDisplayDetails?.hostedVoucherURL.absoluteString ?? NSNull(), + ] + case .konbiniDisplayDetails: + return [ + "type": "konbiniVoucher", + "voucherURL": it.konbiniDisplayDetails?.hostedVoucherURL.absoluteString ?? NSNull(), + ] default: // .useStripeSDK, .BLIKAuthorize, .unknown return nil } @@ -754,7 +756,7 @@ class Mappers { } @available(iOS 13.0, *) - class func mapToUserInterfaceStyle(_ style: String) -> PaymentSheet.UserInterfaceStyle { + class func mapToUserInterfaceStyle(_ style: String?) -> PaymentSheet.UserInterfaceStyle { switch style { case "alwaysDark": return PaymentSheet.UserInterfaceStyle.alwaysDark case "alwaysLight": return PaymentSheet.UserInterfaceStyle.alwaysLight diff --git a/packages/stripe_ios/ios/Classes/Stripe Sdk/StripeSdk+CustomerSheet.swift b/packages/stripe_ios/ios/Classes/Stripe Sdk/StripeSdk+CustomerSheet.swift new file mode 100644 index 000000000..2afc2d1ff --- /dev/null +++ b/packages/stripe_ios/ios/Classes/Stripe Sdk/StripeSdk+CustomerSheet.swift @@ -0,0 +1,166 @@ +// +// CustomerSheetView.swift +// stripe-react-native +// +// Created by Charles Cruzan on 08/28/23. +// + +import Foundation +@_spi(PrivateBetaCustomerSheet) @_spi(STP) import StripePaymentSheet + +extension StripeSdk { + @objc(initCustomerSheet:customerAdapterOverrides:resolver:rejecter:) + func initCustomerSheet(params: NSDictionary, customerAdapterOverrides: NSDictionary, resolver resolve: @escaping RCTPromiseResolveBlock, + rejecter reject: @escaping RCTPromiseRejectBlock) -> Void { + do { + customerSheetConfiguration = CustomerSheetUtils.buildCustomerSheetConfiguration( + appearance: try PaymentSheetAppearance.buildAppearanceFromParams(userParams: params["appearance"] as? NSDictionary), + style: Mappers.mapToUserInterfaceStyle(params["style"] as? String), + removeSavedPaymentMethodMessage: params["removeSavedPaymentMethodMessage"] as? String, + returnURL: params["returnURL"] as? String, + headerTextForSelectionScreen: params["headerTextForSelectionScreen"] as? String, + applePayEnabled: params["applePayEnabled"] as? Bool, + merchantDisplayName: params["merchantDisplayName"] as? String, + billingDetailsCollectionConfiguration: params["billingDetailsCollectionConfiguration"] as? NSDictionary, + defaultBillingDetails: params["defaultBillingDetails"] as? NSDictionary) + } catch { + resolve( + Errors.createError(ErrorType.Failed, error.localizedDescription) + ) + return + } + + guard let customerId = params["customerId"] as? String else { + resolve(Errors.createError(ErrorType.Failed, "You must provide `customerId`")) + return + } + guard let customerEphemeralKeySecret = params["customerEphemeralKeySecret"] as? String else { + resolve(Errors.createError(ErrorType.Failed, "You must provide `customerEphemeralKeySecret`")) + return + } + + customerAdapter = CustomerSheetUtils.buildStripeCustomerAdapter( + customerId: customerId, + ephemeralKeySecret: customerEphemeralKeySecret, + setupIntentClientSecret: params["setupIntentClientSecret"] as? String, + customerAdapter: customerAdapterOverrides, + stripeSdk: self + ) + + customerSheet = CustomerSheet(configuration: customerSheetConfiguration, customer: customerAdapter!) + + resolve([]) + } + + @objc(presentCustomerSheet:resolver:rejecter:) + func presentCustomerSheet(params: NSDictionary, resolver resolve: @escaping RCTPromiseResolveBlock, + rejecter reject: @escaping RCTPromiseRejectBlock) -> Void { + if (STPAPIClient.shared.publishableKey == nil) { + resolve( + Errors.createError(ErrorType.Failed, "No publishable key set. Stripe has not been initialized. Initialize Stripe in your app with the StripeProvider component or the initStripe method.") + ) + return + } + + if let timeout = params["timeout"] as? Double { + DispatchQueue.main.asyncAfter(deadline: .now() + timeout/1000) { + if let customerSheetViewController = self.customerSheetViewController { + customerSheetViewController.dismiss(animated: true) + resolve(Errors.createError(ErrorType.Timeout, "The payment has timed out.")) + } + } + } + + DispatchQueue.main.async { + self.customerSheetViewController = findViewControllerPresenter(from: UIApplication.shared.delegate?.window??.rootViewController ?? UIViewController()) + if let customerSheetViewController = self.customerSheetViewController { + customerSheetViewController.modalPresentationStyle = CustomerSheetUtils.getModalPresentationStyle(params["presentationStyle"] as? String) + customerSheetViewController.modalTransitionStyle = CustomerSheetUtils.getModalTransitionStyle(params["animationStyle"] as? String) + if let customerSheet = self.customerSheet { + customerSheet.present( + from: customerSheetViewController, completion: { result in + resolve(CustomerSheetUtils.interpretResult(result: result)) + }) + } else { + resolve(Errors.createError(ErrorType.Failed, "CustomerSheet has not been properly initialized.")) + return + } + } + } + } + + @objc(retrieveCustomerSheetPaymentOptionSelection:rejecter:) + func retrieveCustomerSheetPaymentOptionSelection(resolver resolve: @escaping RCTPromiseResolveBlock, + rejecter reject: @escaping RCTPromiseRejectBlock) -> Void { + guard let customerAdapter = customerAdapter else { + resolve(Errors.createError(ErrorType.Failed, "CustomerSheet has not been properly initialized.")) + return + } + + Task { + var payload: NSDictionary = [:] + var paymentMethodOption: CustomerSheet.PaymentOptionSelection? = nil + do { + paymentMethodOption = try await customerAdapter.retrievePaymentOptionSelection() + } catch { + resolve(Errors.createError(ErrorType.Failed, error as NSError)) + return + } + + switch paymentMethodOption { + case .applePay(let paymentOptionDisplayData): + payload = CustomerSheetUtils.buildPaymentOptionResult(label: paymentOptionDisplayData.label, imageData: paymentOptionDisplayData.image.pngData()?.base64EncodedString(), paymentMethod: nil) + case .paymentMethod(let paymentMethod, let paymentOptionDisplayData): + payload = CustomerSheetUtils.buildPaymentOptionResult(label: paymentOptionDisplayData.label, imageData: paymentOptionDisplayData.image.pngData()?.base64EncodedString(), paymentMethod: paymentMethod) + case .none: + break + } + resolve(payload) + } + } + + @objc(customerAdapterFetchPaymentMethodsCallback:resolver:rejecter:) + func customerAdapterFetchPaymentMethodsCallback(paymentMethods: [NSDictionary], resolver resolve: @escaping RCTPromiseResolveBlock, + rejecter reject: @escaping RCTPromiseRejectBlock) -> Void { + let decodedPaymentMethods = paymentMethods.compactMap { STPPaymentMethod.decodedObject(fromAPIResponse: $0 as? [AnyHashable : Any]) } + self.fetchPaymentMethodsCallback?(decodedPaymentMethods) + resolve([]) + } + + @objc(customerAdapterAttachPaymentMethodCallback:resolver:rejecter:) + func customerAdapterAttachPaymentMethodCallback(unusedPaymentMethod: NSDictionary, resolver resolve: @escaping RCTPromiseResolveBlock, + rejecter reject: @escaping RCTPromiseRejectBlock) -> Void { + self.attachPaymentMethodCallback?() + resolve([]) + } + + @objc(customerAdapterDetachPaymentMethodCallback:resolver:rejecter:) + func customerAdapterDetachPaymentMethodCallback(unusedPaymentMethod: NSDictionary, resolver resolve: @escaping RCTPromiseResolveBlock, + rejecter reject: @escaping RCTPromiseRejectBlock) -> Void { + self.detachPaymentMethodCallback?() + resolve([]) + } + + @objc(customerAdapterSetSelectedPaymentOptionCallback:rejecter:) + func customerAdapterSetSelectedPaymentOptionCallback(resolver resolve: @escaping RCTPromiseResolveBlock, + rejecter reject: @escaping RCTPromiseRejectBlock) -> Void { + self.setSelectedPaymentOptionCallback?() + resolve([]) + } + + @objc(customerAdapterFetchSelectedPaymentOptionCallback:resolver:rejecter:) + func customerAdapterFetchSelectedPaymentOptionCallback(paymentOption: String?, resolver resolve: @escaping RCTPromiseResolveBlock, rejecter reject: @escaping RCTPromiseRejectBlock) -> Void { + if let paymentOption = paymentOption { + self.fetchSelectedPaymentOptionCallback?(CustomerPaymentOption.init(value: paymentOption)) + } else { + self.fetchSelectedPaymentOptionCallback?(nil) + } + resolve([]) + } + + @objc(customerAdapterSetupIntentClientSecretForCustomerAttachCallback:resolver:rejecter:) + func customerAdapterSetupIntentClientSecretForCustomerAttachCallback(clientSecret: String, resolver resolve: @escaping RCTPromiseResolveBlock, rejecter reject: @escaping RCTPromiseRejectBlock) -> Void { + self.setupIntentClientSecretForCustomerAttachCallback?(clientSecret) + resolve([]) + } +} diff --git a/packages/stripe_ios/ios/Classes/Stripe Sdk/StripeSdk+PaymentSheet.swift b/packages/stripe_ios/ios/Classes/Stripe Sdk/StripeSdk+PaymentSheet.swift index 13b70f550..b7dd3b1ff 100644 --- a/packages/stripe_ios/ios/Classes/Stripe Sdk/StripeSdk+PaymentSheet.swift +++ b/packages/stripe_ios/ios/Classes/Stripe Sdk/StripeSdk+PaymentSheet.swift @@ -50,6 +50,10 @@ extension StripeSdk { configuration.allowsDelayedPaymentMethods = allowsDelayedPaymentMethods } + if let removeSavedPaymentMethodMessage = params["removeSavedPaymentMethodMessage"] as? String { + configuration.removeSavedPaymentMethodMessage = removeSavedPaymentMethodMessage + } + if let billingConfigParams = params["billingDetailsCollectionConfiguration"] as? [String: Any?] { configuration.billingDetailsCollectionConfiguration.name = StripeSdk.mapToCollectionMode(str: billingConfigParams["name"] as? String) configuration.billingDetailsCollectionConfiguration.phone = StripeSdk.mapToCollectionMode(str: billingConfigParams["phone"] as? String) @@ -262,7 +266,7 @@ extension StripeSdk { }) } - private static func mapToCollectionMode(str: String?) -> PaymentSheet.BillingDetailsCollectionConfiguration.CollectionMode { + internal static func mapToCollectionMode(str: String?) -> PaymentSheet.BillingDetailsCollectionConfiguration.CollectionMode { switch str { case "automatic": return .automatic @@ -275,7 +279,7 @@ extension StripeSdk { } } - private static func mapToAddressCollectionMode(str: String?) -> PaymentSheet.BillingDetailsCollectionConfiguration.AddressCollectionMode { + internal static func mapToAddressCollectionMode(str: String?) -> PaymentSheet.BillingDetailsCollectionConfiguration.AddressCollectionMode { switch str { case "automatic": return .automatic diff --git a/packages/stripe_ios/ios/Classes/Stripe Sdk/StripeSdk.swift b/packages/stripe_ios/ios/Classes/Stripe Sdk/StripeSdk.swift index c5254aff0..6c8390680 100644 --- a/packages/stripe_ios/ios/Classes/Stripe Sdk/StripeSdk.swift +++ b/packages/stripe_ios/ios/Classes/Stripe Sdk/StripeSdk.swift @@ -22,6 +22,7 @@ class StripeSdk: RCTEventEmitter, STPBankSelectionViewControllerDelegate, UIAdap var confirmApplePayResolver: RCTPromiseResolveBlock? = nil var confirmApplePayPaymentClientSecret: String? = nil var confirmApplePaySetupClientSecret: String? = nil + var confirmApplePayPaymentMethod: STPPaymentMethod? = nil var applePaymentAuthorizationController: PKPaymentAuthorizationViewController? = nil var createPlatformPayPaymentMethodResolver: RCTPromiseResolveBlock? = nil @@ -48,6 +49,17 @@ class StripeSdk: RCTEventEmitter, STPBankSelectionViewControllerDelegate, UIAdap var applePayShippingAddressErrors: [Error]? = nil var applePayCouponCodeErrors: [Error]? = nil + var customerSheetConfiguration = CustomerSheet.Configuration() + var customerSheet: CustomerSheet? = nil + var customerAdapter: StripeCustomerAdapter? = nil + var customerSheetViewController: UIViewController? + var fetchPaymentMethodsCallback: (([STPPaymentMethod]) -> Void)? = nil + var attachPaymentMethodCallback: (() -> Void)? = nil + var detachPaymentMethodCallback: (() -> Void)? = nil + var setSelectedPaymentOptionCallback: (() -> Void)? = nil + var fetchSelectedPaymentOptionCallback: ((CustomerPaymentOption?) -> Void)? = nil + var setupIntentClientSecretForCustomerAttachCallback: ((String) -> Void)? = nil + var hasEventListeners = false override func startObserving() { hasEventListeners = true @@ -57,7 +69,9 @@ class StripeSdk: RCTEventEmitter, STPBankSelectionViewControllerDelegate, UIAdap } override func supportedEvents() -> [String]! { - return ["onOrderTrackingCallback", "onConfirmHandlerCallback"] + return ["onOrderTrackingCallback", "onConfirmHandlerCallback", "onCustomerAdapterFetchPaymentMethodsCallback", "onCustomerAdapterAttachPaymentMethodCallback", + "onCustomerAdapterDetachPaymentMethodCallback", "onCustomerAdapterSetSelectedPaymentOptionCallback", "onCustomerAdapterFetchSelectedPaymentOptionCallback", + "onCustomerAdapterSetupIntentClientSecretForCustomerAttachCallback"] } @objc override static func requiresMainQueueSetup() -> Bool { @@ -252,20 +266,20 @@ class StripeSdk: RCTEventEmitter, STPBankSelectionViewControllerDelegate, UIAdap if (paymentMethodType == .USBankAccount && paymentMethodData == nil) { return STPSetupIntentConfirmParams(clientSecret: setupIntentClientSecret, paymentMethodType: .USBankAccount) } else { + let factory = PaymentMethodFactory.init(paymentMethodData: paymentMethodData, options: options, cardFieldView: cardFieldView, cardFormView: cardFormView) let parameters = STPSetupIntentConfirmParams(clientSecret: setupIntentClientSecret) if let paymentMethodId = paymentMethodData?["paymentMethodId"] as? String { parameters.paymentMethodID = paymentMethodId } else { - let factory = PaymentMethodFactory.init(paymentMethodData: paymentMethodData, options: options, cardFieldView: cardFieldView, cardFormView: cardFormView) do { - let paymentMethodParams = try factory.createParams(paymentMethodType: paymentMethodType) - parameters.paymentMethodParams = paymentMethodParams - parameters.mandateData = factory.createMandateData() + parameters.paymentMethodParams = try factory.createParams(paymentMethodType: paymentMethodType) } catch { err = Errors.createError(ErrorType.Failed, error as NSError?) } } + + parameters.mandateData = factory.createMandateData() return parameters } @@ -829,7 +843,6 @@ class StripeSdk: RCTEventEmitter, STPBankSelectionViewControllerDelegate, UIAdap paymentMethodData: NSDictionary?, options: NSDictionary ) -> (NSDictionary?, STPPaymentIntentParams) { - let factory = PaymentMethodFactory.init(paymentMethodData: paymentMethodData, options: options, cardFieldView: cardFieldView, cardFormView: cardFormView) var err: NSDictionary? = nil let paymentIntentParams: STPPaymentIntentParams = { @@ -838,7 +851,7 @@ class StripeSdk: RCTEventEmitter, STPBankSelectionViewControllerDelegate, UIAdap return STPPaymentIntentParams(clientSecret: paymentIntentClientSecret, paymentMethodType: .USBankAccount) } else { guard let paymentMethodType = paymentMethodType else { return STPPaymentIntentParams(clientSecret: paymentIntentClientSecret) } - + let factory = PaymentMethodFactory.init(paymentMethodData: paymentMethodData, options: options, cardFieldView: cardFieldView, cardFormView: cardFormView) let paymentMethodId = paymentMethodData?["paymentMethodId"] as? String let parameters = STPPaymentIntentParams(clientSecret: paymentIntentClientSecret) @@ -846,15 +859,19 @@ class StripeSdk: RCTEventEmitter, STPBankSelectionViewControllerDelegate, UIAdap parameters.paymentMethodId = paymentMethodId } else { do { - let paymentMethodParams = try factory.createParams(paymentMethodType: paymentMethodType) - let paymentMethodOptions = try factory.createOptions(paymentMethodType: paymentMethodType) - parameters.paymentMethodParams = paymentMethodParams - parameters.paymentMethodOptions = paymentMethodOptions - parameters.mandateData = factory.createMandateData() + parameters.paymentMethodParams = try factory.createParams(paymentMethodType: paymentMethodType) } catch { err = Errors.createError(ErrorType.Failed, error as NSError?) } } + + do { + parameters.paymentMethodOptions = try factory.createOptions(paymentMethodType: paymentMethodType) + parameters.mandateData = factory.createMandateData() + } catch { + err = Errors.createError(ErrorType.Failed, error as NSError?) + } + return parameters } }() diff --git a/packages/stripe_ios/ios/Classes/StripePlugin.swift b/packages/stripe_ios/ios/Classes/StripePlugin.swift index 1615e40a8..30052c3e2 100644 --- a/packages/stripe_ios/ios/Classes/StripePlugin.swift +++ b/packages/stripe_ios/ios/Classes/StripePlugin.swift @@ -117,6 +117,93 @@ class StripePlugin: StripeSdk, FlutterPlugin, ViewManagerDelegate { return removeListener(call, result: result) case "intentCreationCallback": return intentCreationCallback(call, result: result) + case "initCustomerSheet": + guard let arguments = call.arguments as? FlutterMap, + let customerAdapterOverrides = arguments["customerAdapterOverrides"] as? NSDictionary, + let params = arguments["params"] as? NSDictionary else { + result(FlutterError.invalidParams) + return + } + return initCustomerSheet( + params: params, + customerAdapterOverrides: customerAdapterOverrides, + resolver: resolver(for: result), + rejecter: rejecter(for: result) + ) + case "presentCustomerSheet": + guard let arguments = call.arguments as? FlutterMap, + let params = arguments["params"] as? NSDictionary else { + result(FlutterError.invalidParams) + return + } + return presentCustomerSheet( + params: params, + resolver: resolver(for: result), + rejecter: rejecter(for: result) + ) + case "retrieveCustomerSheetPaymentOptionSelection": + return retrieveCustomerSheetPaymentOptionSelection( + resolver: resolver(for: result), + rejecter: rejecter(for: result) + ) + case "customerAdapterFetchPaymentMethodsCallback": + guard let arguments = call.arguments as? FlutterMap, + let paymentMethods = arguments["paymentMethods"] as? [NSDictionary] else { + result(FlutterError.invalidParams) + return + } + return customerAdapterFetchPaymentMethodsCallback( + paymentMethods: paymentMethods, + resolver: resolver(for: result), + rejecter: rejecter(for: result) + ) + case "customerAdapterAttachPaymentMethodCallback": + guard let arguments = call.arguments as? FlutterMap, + let unusedPaymentMethod = arguments["unusedPaymentMethod"] as? NSDictionary else { + result(FlutterError.invalidParams) + return + } + return customerAdapterAttachPaymentMethodCallback( + unusedPaymentMethod: unusedPaymentMethod, + resolver: resolver(for: result), + rejecter: rejecter(for: result) + ) + case "customerAdapterDetachPaymentMethodCallback": + guard let arguments = call.arguments as? FlutterMap, + let unusedPaymentMethod = arguments["unusedPaymentMethod"] as? NSDictionary else { + result(FlutterError.invalidParams) + return + } + return customerAdapterDetachPaymentMethodCallback( + unusedPaymentMethod: unusedPaymentMethod, + resolver: resolver(for: result), + rejecter: rejecter(for: result) + ) + case "customerAdapterSetSelectedPaymentOptionCallback": + return customerAdapterSetSelectedPaymentOptionCallback( + resolver: resolver(for: result), + rejecter: rejecter(for: result) + ) + case "customerAdapterFetchSelectedPaymentOptionCallback": + let arguments = call.arguments as? FlutterMap + let paymentOption = arguments?["paymentOption"] as? String + + return customerAdapterFetchSelectedPaymentOptionCallback( + paymentOption: paymentOption, + resolver: resolver(for: result), + rejecter: rejecter(for: result) + ) + case "customerAdapterSetupIntentClientSecretForCustomerAttachCallback": + guard let arguments = call.arguments as? FlutterMap, + let clientSecret = arguments["clientSecret"] as? String else { + result(FlutterError.invalidParams) + return + } + return customerAdapterSetupIntentClientSecretForCustomerAttachCallback( + clientSecret: clientSecret, + resolver: resolver(for: result), + rejecter: rejecter(for: result) + ) default: result(FlutterMethodNotImplemented) } diff --git a/packages/stripe_ios/ios/stripe_ios.podspec b/packages/stripe_ios/ios/stripe_ios.podspec index 389c92ecf..dff2f77ad 100644 --- a/packages/stripe_ios/ios/stripe_ios.podspec +++ b/packages/stripe_ios/ios/stripe_ios.podspec @@ -2,7 +2,7 @@ # To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html. # Run `pod lib lint stripe_ios.podspec' to validate before publishing. # -stripe_version = '~> 23.12.0' +stripe_version = '~> 23.16.0' Pod::Spec.new do |s| s.name = 'stripe_ios' s.version = '0.0.1'