import AuthenticationServices
import Foundation
import PassKit
@_spi(DashboardOnly) @_spi(STP) import Stripe
@_spi(STP) @_spi(ReactNativeSDK) import StripeCore
import StripeFinancialConnections
@_spi(STP) @_spi(ConfirmationTokensPublicPreview) import StripePayments
import StripePaymentsUI
import UIKit
#if canImport(StripeCryptoOnramp)
@_spi(CryptoOnrampAlpha) import StripeCryptoOnramp

@_spi(STP)
@_spi(EmbeddedPaymentElementPrivateBeta)
@_spi(CustomerSessionBetaAccess)
@_spi(AppearanceAPIAdditionsPreview)
import StripePaymentSheet
#else
@_spi(EmbeddedPaymentElementPrivateBeta) @_spi(CustomerSessionBetaAccess) import StripePaymentSheet
#endif

@available(iOS 13.0, *)
class ASWebAuthenticationPresentationContextProvider: NSObject, ASWebAuthenticationPresentationContextProviding {
    func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor {
        return RCTKeyWindow() ?? ASPresentationAnchor()
    }
}

// Helper to get device type identifier
private func getDeviceType() -> String {
    var systemInfo = utsname()
    uname(&systemInfo)
    let machineMirror = Mirror(reflecting: systemInfo.machine)
    let identifier = machineMirror.children.reduce("") { identifier, element in
        guard let value = element.value as? Int8, value != 0 else { return identifier }
        return identifier + String(UnicodeScalar(UInt8(value)))
    }
    return identifier.isEmpty ? UIDevice.current.model : identifier
}

@objc(StripeSdkImpl)
public class StripeSdkImpl: NSObject, UIAdaptivePresentationControllerDelegate {
    @objc public static let shared = StripeSdkImpl()

    static var isNewArchitecture: Bool {
        #if RCT_NEW_ARCH_ENABLED
        return true
        #else
        return false
        #endif
    }

    static var reactNativeVersion: String {
        let version = RCTGetReactNativeVersion()
        let major = version?["major"] ?? 0
        let minor = version?["minor"] ?? 0
        let patch = version?["patch"] ?? 0
        return "\(major).\(minor).\(patch)"
    }

    @objc public weak var emitter: StripeSdkEmitter?
    @objc public weak var onrampEmitter: StripeOnrampSdkEmitter?
    weak var cardFieldView: CardFieldView?
    weak var cardFormView: CardFormView?

    var merchantIdentifier: String?

    internal var paymentSheet: PaymentSheet?
    internal var paymentSheetFlowController: PaymentSheet.FlowController?
    var paymentSheetIntentCreationCallback: ((Result<String, Error>) -> Void)?
    var paymentSheetConfirmationTokenIntentCreationCallback: ((Result<String, Error>) -> Void)?

    var urlScheme: String?

    var confirmPaymentResolver: RCTPromiseResolveBlock?

    var confirmApplePayResolver: RCTPromiseResolveBlock?
    var confirmApplePayPaymentClientSecret: String?
    var confirmApplePaySetupClientSecret: String?
    var confirmApplePayPaymentMethod: STPPaymentMethod?

    var applePaymentAuthorizationController: PKPaymentAuthorizationViewController?
    var createPlatformPayPaymentMethodResolver: RCTPromiseResolveBlock?
    var platformPayUsesDeprecatedTokenFlow = false
    var applePaymentMethodFlowCanBeCanceled = false

    var confirmPaymentClientSecret: String?

    var shippingMethodUpdateCompletion: ((PKPaymentRequestShippingMethodUpdate) -> Void)?
    var shippingContactUpdateCompletion: ((PKPaymentRequestShippingContactUpdate) -> Void)?
    @available(iOS 15.0, *)
    var couponCodeUpdateCompletion: ((PKPaymentRequestCouponCodeUpdate) -> Void)? {
        get { _couponCodeUpdateCompletion as? ((PKPaymentRequestCouponCodeUpdate) -> Void) }
        set { _couponCodeUpdateCompletion = newValue }
    }
    private var _couponCodeUpdateCompletion: Any?
    var orderTrackingHandler: (result: PKPaymentAuthorizationResult, handler: ((PKPaymentAuthorizationResult) -> Void))?
    var shippingMethodUpdateJSCallback: RCTDirectEventBlock?
    var shippingContactUpdateJSCallback: RCTDirectEventBlock?
    var couponCodeEnteredJSCallback: RCTDirectEventBlock?
    var platformPayOrderTrackingJSCallback: RCTDirectEventBlock?
    var applePaySummaryItems: [PKPaymentSummaryItem] = []
    var applePayShippingMethods: [PKShippingMethod] = []
    var applePayShippingAddressErrors: [Error]?
    var applePayCouponCodeErrors: [Error]?

    var customerSheetConfiguration = CustomerSheet.Configuration()
    var customerSheet: CustomerSheet?
    var customerAdapter: StripeCustomerAdapter?
    var customerSheetViewController: UIViewController?
    var fetchPaymentMethodsCallback: (([STPPaymentMethod]) -> Void)?
    var attachPaymentMethodCallback: (() -> Void)?
    var detachPaymentMethodCallback: (() -> Void)?
    var setSelectedPaymentOptionCallback: (() -> Void)?
    var fetchSelectedPaymentOptionCallback: ((CustomerPaymentOption?) -> Void)?
    var setupIntentClientSecretForCustomerAttachCallback: ((String) -> Void)?
    var customPaymentMethodResultCallback: ((PaymentSheetResult) -> Void)?
    var clientSecretProviderSetupIntentClientSecretCallback: ((String) -> Void)?
    var clientSecretProviderCustomerSessionClientSecretCallback: ((CustomerSessionClientSecret) -> Void)?

#if canImport(StripeCryptoOnramp)
    var cryptoOnrampCoordinator: CryptoOnrampCoordinator?
    var cryptoOnrampCheckoutClientSecretContinuation: CheckedContinuation<String, Error>?
#endif

    var embeddedInstance: EmbeddedPaymentElement?
    lazy var embeddedInstanceDelegate = StripeSdkEmbeddedPaymentElementDelegate(sdkImpl: self)

    var authenticationSession: ASWebAuthenticationSession?
    var authenticationContextProvider: Any?

    @objc public func getConstants() -> [AnyHashable: Any] {
        return [
            "API_VERSIONS": [
                "CORE": STPAPIClient.apiVersion,
                "ISSUING": STPAPIClient.apiVersion,
            ],
            "SYSTEM_INFO": [
                "sdkVersion": StripeAPIConfiguration.STPSDKVersion,
                "osVersion": UIDevice.current.systemVersion,
                "deviceType": getDeviceType(),
                "appName": Bundle.stp_applicationName() ?? "",
                "appVersion": Bundle.stp_applicationVersion() ?? "",
                "isNewArchitecture": Self.isNewArchitecture,
                "reactNativeVersion": Self.reactNativeVersion,
            ],
        ]
    }

    @objc(initialise:resolver:rejecter:)
    public func initialise(params: NSDictionary, resolver resolve: @escaping RCTPromiseResolveBlock, rejecter reject: @escaping RCTPromiseRejectBlock) {
        let publishableKey = params["publishableKey"] as! String
        let appInfo = params["appInfo"] as! NSDictionary
        let stripeAccountId = params["stripeAccountId"] as? String
        let params3ds = params["threeDSecureParams"] as? NSDictionary
        let urlScheme = params["urlScheme"] as? String
        let merchantIdentifier = params["merchantIdentifier"] as? String

        if let params3ds = params3ds {
            configure3dSecure(params3ds)
        }

        self.urlScheme = urlScheme

        STPAPIClient.shared.publishableKey = publishableKey
        StripeAPI.defaultPublishableKey = publishableKey
        STPAPIClient.shared.stripeAccount = stripeAccountId

        if STPAPIClient.shared.publishableKeyIsUserKey {
            STPAPIClient.shared.userKeyLiveMode = UserDefaults.standard.object(forKey: "stripe_userKeyLiveMode") as? Bool ?? true
        }

        let name = appInfo["name"] as? String ?? ""
        let partnerId = appInfo["partnerId"] as? String ?? ""
        let version = appInfo["version"] as? String ?? ""
        let url = appInfo["url"] as? String ?? ""

        STPAPIClient.shared.appInfo = STPAppInfo(name: name, partnerId: partnerId, version: version, url: url)
        ReactNativeAnalytics.isNewArchitecture = Self.isNewArchitecture
        ReactNativeAnalytics.reactNativeVersion = Self.reactNativeVersion
        self.merchantIdentifier = merchantIdentifier
        resolve(NSNull())
    }

    @objc(initPaymentSheet:resolver:rejecter:)
    public func initPaymentSheet(params: NSDictionary,
                                 resolver resolve: @escaping RCTPromiseResolveBlock,
                                 rejecter reject: @escaping RCTPromiseRejectBlock) {
        let (error, configuration) = buildPaymentSheetConfiguration(params: params)
        guard let configuration = configuration else {
            resolve(error)
            return
        }

        preparePaymentSheetInstance(params: params, configuration: configuration, resolve: resolve)
    }

    @objc(intentCreationCallback:resolver:rejecter:)
    @MainActor public func intentCreationCallback(result: NSDictionary,
                                                  resolver resolve: @escaping RCTPromiseResolveBlock,
                                                  rejecter reject: @escaping RCTPromiseRejectBlock) {
        guard let paymentSheetIntentCreationCallback = self.paymentSheetIntentCreationCallback else {
            resolve(Errors.createError(ErrorType.Failed, "No intent creation callback was set"))
            return
        }
        if let clientSecret = result["clientSecret"] as? String {
            paymentSheetIntentCreationCallback(.success(clientSecret))
        } else {
            let errorParams = result["error"] as? NSDictionary
            let error = ConfirmationError.init(errorMessage: errorParams?["localizedMessage"] as? String ?? "An unknown error occurred.")
            paymentSheetIntentCreationCallback(.failure(error))
        }
    }

    @objc(confirmationTokenCreationCallback:resolver:rejecter:)
    @MainActor public func confirmationTokenCreationCallback(result: NSDictionary,
                                                             resolver resolve: @escaping RCTPromiseResolveBlock,
                                                             rejecter reject: @escaping RCTPromiseRejectBlock) {
        guard let paymentSheetConfirmationTokenIntentCreationCallback = self.paymentSheetConfirmationTokenIntentCreationCallback else {
            resolve(Errors.createError(ErrorType.Failed, "No confirmation token intent creation callback was set"))
            return
        }
        if let clientSecret = result["clientSecret"] as? String {
            paymentSheetConfirmationTokenIntentCreationCallback(.success(clientSecret))
        } else {
            let errorParams = result["error"] as? NSDictionary
            let error = ConfirmationError.init(errorMessage: errorParams?["localizedMessage"] as? String ?? "An unknown error occurred.")
            paymentSheetConfirmationTokenIntentCreationCallback(.failure(error))
        }
    }

    @objc(customPaymentMethodResultCallback:resolver:rejecter:)
    @MainActor public func customPaymentMethodResultCallback(result: NSDictionary,
                                                             resolver resolve: @escaping RCTPromiseResolveBlock,
                                                             rejecter reject: @escaping RCTPromiseRejectBlock) {
        guard let customPaymentMethodResultCallback = self.customPaymentMethodResultCallback else {
            resolve(Errors.createError(ErrorType.Failed, "Internal error: no custom payment method callback"))
            return
        }

        let status = result["status"] as? String ?? ""
        let errorMessage = result["error"] as? String

        switch status {
        case "completed":
            customPaymentMethodResultCallback(.completed)
        case "canceled":
            customPaymentMethodResultCallback(.canceled)
        case "failed":
            let error = ConfirmationError.init(errorMessage: errorMessage ?? "An unknown error occurred.")
            customPaymentMethodResultCallback(.failed(error: error))
        default:
            let error = ConfirmationError.init(errorMessage: "Unknown custom payment method result status")
            customPaymentMethodResultCallback(.failed(error: error))
        }

        resolve(NSNull())
    }

    @objc(confirmPaymentSheetPayment:rejecter:)
    public func confirmPaymentSheetPayment(resolver resolve: @escaping RCTPromiseResolveBlock,
                                           rejecter reject: @escaping RCTPromiseRejectBlock) {
        DispatchQueue.main.async {
            if self.paymentSheetFlowController != nil {
                self.paymentSheetFlowController?.confirm(from: RCTKeyWindow()?.rootViewController ?? UIViewController()) { paymentResult in
                    switch paymentResult {
                    case .completed:
                        resolve([])
                        self.paymentSheetFlowController = nil
                    case .canceled:
                        resolve(Errors.createError(ErrorType.Canceled, "The payment flow has been canceled"))
                    case .failed(let error):
                        resolve(Errors.createError(ErrorType.Failed, error))
                    }
                }
            } else {
                resolve(Errors.createError(ErrorType.Failed, "No payment sheet has been initialized yet"))
            }
        }
    }

    @objc(resetPaymentSheetCustomer:rejecter:)
    public func resetPaymentSheetCustomer(resolver resolve: @escaping RCTPromiseResolveBlock,
                                          rejecter reject: @escaping RCTPromiseRejectBlock) {
        PaymentSheet.resetCustomer()
        resolve(nil)
    }

    @objc(presentPaymentSheet:resolver:rejecter:)
    public func presentPaymentSheet(options: NSDictionary,
                                    resolver resolve: @escaping RCTPromiseResolveBlock,
                                    rejecter reject: @escaping RCTPromiseRejectBlock) {
        var paymentSheetViewController: UIViewController?

        if let timeout = options["timeout"] as? Double {
            DispatchQueue.main.asyncAfter(deadline: .now() + timeout/1000) {
                if let paymentSheetViewController = paymentSheetViewController {
                    paymentSheetViewController.dismiss(animated: true)
                    resolve(Errors.createError(ErrorType.Timeout, "The payment has timed out."))
                }
            }
        }
        DispatchQueue.main.async {
            paymentSheetViewController = RCTKeyWindow()?.rootViewController ?? UIViewController()
            if let paymentSheetFlowController = self.paymentSheetFlowController {
                paymentSheetFlowController.presentPaymentOptions(from: findViewControllerPresenter(from: paymentSheetViewController!)
                ) { didCancel in
                    paymentSheetViewController = nil
                    if let paymentOption = self.paymentSheetFlowController?.paymentOption {
                        let option: NSDictionary = [
                            "label": paymentOption.label,
                            "image": paymentOption.image.pngData()?.base64EncodedString() ?? "",
                        ]
                        resolve(Mappers.createResult("paymentOption", option, additionalFields: ["didCancel": didCancel]))
                    } else {
                        resolve(Errors.createError(ErrorType.Canceled, "The payment option selection flow has been canceled"))
                    }
                }
            } else if let paymentSheet = self.paymentSheet {
                paymentSheet.present(from: findViewControllerPresenter(from: paymentSheetViewController!)
                ) { paymentResult in
                    paymentSheetViewController = nil
                    switch paymentResult {
                    case .completed:
                        resolve([])
                        self.paymentSheet = nil
                    case .canceled:
                        resolve(Errors.createError(ErrorType.Canceled, "The payment has been canceled"))
                    case .failed(let error):
                        resolve(Errors.createError(ErrorType.Failed, error as NSError))
                    }
                }
            } else {
                resolve(Errors.createError(ErrorType.Failed, "No payment sheet has been initialized yet. You must call `initPaymentSheet` before `presentPaymentSheet`."))
            }
        }
    }

    @objc(createTokenForCVCUpdate:resolver:rejecter:)
    public func createTokenForCVCUpdate(cvc: String?, resolver resolve: @escaping RCTPromiseResolveBlock, rejecter reject: @escaping RCTPromiseRejectBlock) {
        guard let cvc = cvc else {
            resolve(Errors.createError(ErrorType.Failed, "You must provide CVC"))
            return
        }

        STPAPIClient.shared.createToken(forCVCUpdate: cvc) { (token, error) in
            if error != nil || token == nil {
                resolve(Errors.createError(ErrorType.Failed, error as? NSError))
            } else {
                let tokenId = token?.tokenId
                resolve(["tokenId": tokenId])
            }
        }
    }

    @objc(confirmSetupIntent:data:options:resolver:rejecter:)
    public func confirmSetupIntent (setupIntentClientSecret: String,
                                    params: NSDictionary,
                                    options: NSDictionary,
                                    resolver resolve: @escaping RCTPromiseResolveBlock,
                                    rejecter reject: @escaping RCTPromiseRejectBlock) {
        let paymentMethodData = params["paymentMethodData"] as? NSDictionary
        let type = Mappers.mapToPaymentMethodType(type: params["paymentMethodType"] as? String)
        guard let paymentMethodType = type else {
            resolve(Errors.createError(ErrorType.Failed, "You must provide paymentMethodType."))
            return
        }

        var err: NSDictionary?
        let setupIntentParams: STPSetupIntentConfirmParams = {
            // If payment method data is not supplied, assume payment method was attached through via collectBankAccount
            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 {
                    do {
                        parameters.paymentMethodParams = try factory.createParams(paymentMethodType: paymentMethodType)
                    } catch  {
                        err = Errors.createError(ErrorType.Failed, error as NSError?)
                    }
                }

                parameters.mandateData = factory.createMandateData()

                return parameters
            }
        }()

        if err != nil {
            resolve(err)
            return
        }

        if let urlScheme = urlScheme {
            setupIntentParams.returnURL = Mappers.mapToReturnURL(urlScheme: urlScheme)
        }

        let paymentHandler = STPPaymentHandler.shared()
        paymentHandler.confirmSetupIntent(setupIntentParams, with: self) { status, setupIntent, error in
            switch status {
            case .failed:
                resolve(Errors.createError(ErrorType.Failed, error))
            case .canceled:
                if let lastError = setupIntent?.lastSetupError {
                    resolve(Errors.createError(ErrorType.Canceled, lastError))
                } else {
                    resolve(Errors.createError(ErrorType.Canceled, "The payment has been canceled"))
                }
            case .succeeded:
                let intent = Mappers.mapFromSetupIntent(setupIntent: setupIntent!)
                resolve(Mappers.createResult("setupIntent", intent))
            @unknown default:
                resolve(Errors.createError(ErrorType.Unknown, error))
            }
        }
    }

    @objc(updatePlatformPaySheet:shippingMethods:errors:resolver:rejecter:)
    public func updatePlatformPaySheet(summaryItems: NSArray,
                                       shippingMethods: NSArray,
                                       errors: [NSDictionary],
                                       resolver resolve: @escaping RCTPromiseResolveBlock,
                                       rejecter reject: @escaping RCTPromiseRejectBlock)
    {
        let couponUpdateHandlerIsNil: Bool = {
            if #available(iOS 15.0, *), self.couponCodeUpdateCompletion == nil {
                return true
            }
            return false
        }()

        if shippingMethodUpdateCompletion == nil && shippingContactUpdateCompletion == nil && couponUpdateHandlerIsNil {
            resolve(Errors.createError(ErrorType.Failed, "You can use this method only after either onShippingContactSelected, onShippingMethodSelected, or onCouponCodeEntered callbacks are triggered"))
            return
        }

        do {
            applePaySummaryItems = try ApplePayUtils.buildPaymentSummaryItems(items: summaryItems as? [[String: Any]])
        } catch {
            resolve(Errors.createError(ErrorType.Failed, error.localizedDescription))
            return
        }

        applePayShippingMethods = ApplePayUtils.buildShippingMethods(items: shippingMethods as? [[String: Any]])

        do {
            (applePayShippingAddressErrors, applePayCouponCodeErrors) = try ApplePayUtils.buildApplePayErrors(errorItems: errors)
        } catch {
            resolve(Errors.createError(ErrorType.Failed, error.localizedDescription))
            return
        }

        shippingMethodUpdateCompletion?(PKPaymentRequestShippingMethodUpdate.init(paymentSummaryItems: applePaySummaryItems))
        shippingContactUpdateCompletion?(PKPaymentRequestShippingContactUpdate.init(errors: applePayShippingAddressErrors, paymentSummaryItems: applePaySummaryItems, shippingMethods: applePayShippingMethods))
        if #available(iOS 15.0, *) {
            couponCodeUpdateCompletion?(PKPaymentRequestCouponCodeUpdate.init(errors: applePayCouponCodeErrors, paymentSummaryItems: applePaySummaryItems, shippingMethods: applePayShippingMethods))
            self.couponCodeUpdateCompletion = nil
        }
        self.shippingMethodUpdateCompletion = nil
        self.shippingContactUpdateCompletion = nil
        resolve([])
    }

    @objc(openApplePaySetup:rejecter:)
    public func openApplePaySetup(resolver resolve: @escaping RCTPromiseResolveBlock, rejecter reject: @escaping RCTPromiseRejectBlock) {
        PKPassLibrary.init().openPaymentSetup()
        resolve([])
    }

    @objc(handleURLCallback:resolver:rejecter:)
    public func handleURLCallback(url: String?, resolver resolve: @escaping RCTPromiseResolveBlock, rejecter reject: @escaping RCTPromiseRejectBlock) {
        guard let url = url else {
            resolve(false)
            return
        }
        let urlObj = URL(string: url)
        if urlObj == nil {
            resolve(false)
        } else {
            DispatchQueue.main.async {
                let stripeHandled = StripeAPI.handleURLCallback(with: urlObj!)
                resolve(stripeHandled)
            }
        }
    }

    @objc(isPlatformPaySupported:resolver:rejecter:)
    public func isPlatformPaySupported(params: NSDictionary,
                                       resolver resolve: @escaping RCTPromiseResolveBlock,
                                       rejecter reject: @escaping RCTPromiseRejectBlock) {
        resolve(StripeAPI.deviceSupportsApplePay())
    }

    @objc(createPlatformPayPaymentMethod:usesDeprecatedTokenFlow:resolver:rejecter:)
    public func createPlatformPayPaymentMethod(params: NSDictionary,
                                               usesDeprecatedTokenFlow: Bool,
                                               resolver resolve: @escaping RCTPromiseResolveBlock,
                                               rejecter reject: @escaping RCTPromiseRejectBlock) {
        guard let applePayPatams = params["applePay"] as? NSDictionary else {
            resolve(Errors.createError(ErrorType.Failed, "You must provide the `applePay` parameter."))
            return
        }
        let (error, paymentRequest) = ApplePayUtils.createPaymentRequest(merchantIdentifier: merchantIdentifier, params: applePayPatams)
        guard let paymentRequest = paymentRequest else {
            resolve(error)
            return
        }

        self.applePaySummaryItems = paymentRequest.paymentSummaryItems
        self.applePayShippingMethods = paymentRequest.shippingMethods ?? []
        self.applePayShippingAddressErrors = nil
        self.applePayCouponCodeErrors = nil
        platformPayUsesDeprecatedTokenFlow = usesDeprecatedTokenFlow
        applePaymentMethodFlowCanBeCanceled = true
        createPlatformPayPaymentMethodResolver = resolve
        self.applePaymentAuthorizationController = PKPaymentAuthorizationViewController(paymentRequest: paymentRequest)
        if let applePaymentAuthorizationController = self.applePaymentAuthorizationController {
            applePaymentAuthorizationController.delegate = self
            DispatchQueue.main.async {
                let vc = findViewControllerPresenter(from: RCTKeyWindow()?.rootViewController ?? UIViewController())
                vc.present(
                    applePaymentAuthorizationController,
                    animated: true,
                    completion: nil
                )
            }
        } else {
            resolve(Errors.createError(ErrorType.Failed, "Invalid in-app payment request. Search the iOS logs for `NSUnderlyingError` to get more information."))
        }
    }

    @objc(dismissPlatformPay:rejecter:)
    public func dismissPlatformPay(resolver resolve: @escaping RCTPromiseResolveBlock, rejecter reject: @escaping RCTPromiseRejectBlock) {
        let didDismiss = maybeDismissApplePay()
        resolve(didDismiss)
    }

    @objc(confirmPlatformPay:params:isPaymentIntent:resolver:rejecter:)
    public func confirmPlatformPay(
        clientSecret: String?,
        params: NSDictionary,
        isPaymentIntent: Bool,
        resolver resolve: @escaping RCTPromiseResolveBlock,
        rejecter reject: @escaping RCTPromiseRejectBlock
    ) {
        guard let applePayPatams = params["applePay"] as? NSDictionary else {
            resolve(Errors.createError(ErrorType.Failed, "You must provide the `applePay` parameter."))
            return
        }
        let (error, paymentRequest) = ApplePayUtils.createPaymentRequest(merchantIdentifier: merchantIdentifier, params: applePayPatams)
        guard let paymentRequest = paymentRequest else {
            resolve(error)
            return
        }

        // Prevent multiple simultaneous Apple Pay presentations
        if self.confirmApplePayResolver != nil {
            resolve(Errors.createError(ErrorType.Failed, "Apple Pay is already in progress"))
            return
        }

        self.applePaySummaryItems = paymentRequest.paymentSummaryItems
        self.applePayShippingMethods = paymentRequest.shippingMethods ?? []
        self.applePayShippingAddressErrors = nil
        self.applePayCouponCodeErrors = nil
        self.orderTrackingHandler = nil
        self.confirmApplePayResolver = resolve
        if isPaymentIntent {
            self.confirmApplePayPaymentClientSecret = clientSecret
        } else {
            self.confirmApplePaySetupClientSecret = clientSecret
        }
        if let applePayContext = STPApplePayContext(paymentRequest: paymentRequest, delegate: self) {
            DispatchQueue.main.async {
                applePayContext.presentApplePay(completion: nil)
            }
        } else {
            resolve(Errors.createError(ErrorType.Failed, "Payment not completed"))
        }
    }

    func configure3dSecure(_ params: NSDictionary) {
        let threeDSCustomizationSettings = STPPaymentHandler.shared().threeDSCustomizationSettings
        let uiCustomization = Mappers.mapUICustomization(params)

        threeDSCustomizationSettings.uiCustomization = uiCustomization
    }

    @objc(createPaymentMethod:options:resolver:rejecter:)
    public func createPaymentMethod(
        params: NSDictionary,
        options: NSDictionary,
        resolver resolve: @escaping RCTPromiseResolveBlock,
        rejecter reject: @escaping RCTPromiseRejectBlock
    ) {
        let type = Mappers.mapToPaymentMethodType(type: params["paymentMethodType"] as? String)
        guard let paymentMethodType = type else {
            resolve(Errors.createError(ErrorType.Failed, "You must provide paymentMethodType"))
            return
        }

        var paymentMethodParams: STPPaymentMethodParams?
        let factory = PaymentMethodFactory.init(
            paymentMethodData: params["paymentMethodData"] as? NSDictionary,
            options: options,
            cardFieldView: cardFieldView,
            cardFormView: cardFormView
        )
        do {
            paymentMethodParams = try factory.createParams(paymentMethodType: paymentMethodType)
        } catch  {
            resolve(Errors.createError(ErrorType.Failed, error.localizedDescription))
            return
        }

        if let paymentMethodParams = paymentMethodParams {
            STPAPIClient.shared.createPaymentMethod(with: paymentMethodParams) { paymentMethod, error in
                if let createError = error {
                    resolve(Errors.createError(ErrorType.Failed, createError as NSError))
                } else {
                    resolve(
                        Mappers.createResult("paymentMethod", Mappers.mapFromPaymentMethod(paymentMethod))
                    )
                }
            }
        } else {
            resolve(Errors.createError(ErrorType.Unknown, "Unhandled error occured"))
        }
    }

    @objc(createToken:resolver:rejecter:)
    public func createToken(
        params: NSDictionary,
        resolver resolve: @escaping RCTPromiseResolveBlock,
        rejecter reject: @escaping RCTPromiseRejectBlock
    ) {
        guard let type = params["type"] as? String else {
            resolve(Errors.createError(ErrorType.Failed, "type parameter is required"))
            return
        }

        // TODO: Consider moving this to its own class when more types are supported.
        switch type {
        case "BankAccount":
            createTokenFromBankAccount(params: params, resolver: resolve, rejecter: reject)
        case "Card":
            createTokenFromCard(params: params, resolver: resolve, rejecter: reject)
        case "Pii":
            createTokenFromPii(params: params, resolver: resolve, rejecter: reject)
        default:
            resolve(Errors.createError(ErrorType.Failed, type + " type is not supported yet"))
        }
    }

    func createTokenFromBankAccount(
        params: NSDictionary,
        resolver resolve: @escaping RCTPromiseResolveBlock,
        rejecter reject: @escaping RCTPromiseRejectBlock
    ) {
        let accountHolderName = params["accountHolderName"] as? String
        let accountHolderType = params["accountHolderType"] as? String
        let accountNumber = params["accountNumber"] as? String
        let country = params["country"] as? String
        let currency = params["currency"] as? String
        let routingNumber = params["routingNumber"] as? String

        let bankAccountParams = STPBankAccountParams()
        bankAccountParams.accountHolderName = accountHolderName
        bankAccountParams.accountNumber = accountNumber
        bankAccountParams.country = country
        bankAccountParams.currency = currency
        bankAccountParams.routingNumber = routingNumber
        bankAccountParams.accountHolderType = Mappers.mapToBankAccountHolderType(accountHolderType)

        STPAPIClient.shared.createToken(withBankAccount: bankAccountParams) { token, error in
            if let token = token {
                resolve(Mappers.createResult("token", Mappers.mapFromToken(token: token)))
            } else {
                resolve(Errors.createError(ErrorType.Failed, error as NSError?))
            }
        }
    }

    func createTokenFromPii(
        params: NSDictionary,
        resolver resolve: @escaping RCTPromiseResolveBlock,
        rejecter reject: @escaping RCTPromiseRejectBlock
    ) {
        guard let personalId = params["personalId"] as? String else {
            resolve(Errors.createError(ErrorType.Failed, "personalId parameter is required"))
            return
        }

        STPAPIClient.shared.createToken(withPersonalIDNumber: personalId) { token, error in
            if let token = token {
                resolve(Mappers.createResult("token", Mappers.mapFromToken(token: token)))
            } else {
                resolve(Errors.createError(ErrorType.Failed, error as NSError?))
            }
        }
    }

    func createTokenFromCard(
        params: NSDictionary,
        resolver resolve: @escaping RCTPromiseResolveBlock,
        rejecter reject: @escaping RCTPromiseRejectBlock
    ) {
        let address = params["address"] as? NSDictionary
        let cardSourceParams = STPCardParams()
        if let params = cardFieldView?.cardParams as? STPPaymentMethodParams {
            cardSourceParams.number = params.card!.number
            cardSourceParams.cvc = params.card!.cvc
            cardSourceParams.expMonth = UInt(truncating: params.card!.expMonth ?? 0)
            cardSourceParams.expYear = UInt(truncating: params.card!.expYear ?? 0)
        } else if let params = cardFormView?.cardParams as? STPPaymentMethodCardParams {
            cardSourceParams.number = params.number
            cardSourceParams.cvc = params.cvc
            cardSourceParams.expMonth = UInt(truncating: params.expMonth ?? 0)
            cardSourceParams.expYear = UInt(truncating: params.expYear ?? 0)
        } else {
            resolve(Errors.createError(ErrorType.Failed, "Card details not complete"))
            return
        }
        cardSourceParams.address = Mappers.mapToAddress(address: address)
        cardSourceParams.name = params["name"] as? String
        cardSourceParams.currency = params["currency"] as? String

        STPAPIClient.shared.createToken(withCard: cardSourceParams) { token, error in
            if let token = token {
                resolve(Mappers.createResult("token", Mappers.mapFromToken(token: token)))
            } else {
                resolve(Errors.createError(ErrorType.Failed, error as NSError?))
            }
        }
    }

    @objc(handleNextAction:returnURL:resolver:rejecter:)
    public func handleNextAction(
        paymentIntentClientSecret: String,
        returnURL: String?,
        resolver resolve: @escaping RCTPromiseResolveBlock,
        rejecter reject: @escaping RCTPromiseRejectBlock
    ){
        let paymentHandler = STPPaymentHandler.shared()
        paymentHandler.handleNextAction(forPayment: paymentIntentClientSecret, with: self, returnURL: returnURL) { status, paymentIntent, handleActionError in
            switch status {
            case .failed:
                resolve(Errors.createError(ErrorType.Failed, handleActionError))
            case .canceled:
                if let lastError = paymentIntent?.lastPaymentError {
                    resolve(Errors.createError(ErrorType.Canceled, lastError))
                } else {
                    resolve(Errors.createError(ErrorType.Canceled, "The payment has been canceled"))
                }
            case .succeeded:
                if let paymentIntent = paymentIntent {
                    resolve(Mappers.createResult("paymentIntent", Mappers.mapFromPaymentIntent(paymentIntent: paymentIntent)))
                }
            @unknown default:
                resolve(Errors.createError(ErrorType.Unknown, "Cannot complete payment"))
            }
        }
    }

    @objc(handleNextActionForSetup:returnURL:resolver:rejecter:)
    public func handleNextActionForSetup(
        setupIntentClientSecret: String,
        returnURL: String?,
        resolver resolve: @escaping RCTPromiseResolveBlock,
        rejecter reject: @escaping RCTPromiseRejectBlock
    ){
        let paymentHandler = STPPaymentHandler.shared()
        paymentHandler.handleNextAction(forSetupIntent: setupIntentClientSecret, with: self, returnURL: returnURL) { status, setupIntent, handleActionError in
            switch status {
            case .failed:
                resolve(Errors.createError(ErrorType.Failed, handleActionError))
            case .canceled:
                if let lastError = setupIntent?.lastSetupError {
                    resolve(Errors.createError(ErrorType.Canceled, lastError))
                } else {
                    resolve(Errors.createError(ErrorType.Canceled, "The setup intent has been canceled"))
                }
            case .succeeded:
                if let setupIntent = setupIntent {
                    resolve(Mappers.createResult("setupIntent", Mappers.mapFromSetupIntent(setupIntent: setupIntent)))
                }
            @unknown default:
                resolve(Errors.createError(ErrorType.Unknown, "Cannot complete setup"))
            }
        }
    }

    @objc(collectBankAccount:clientSecret:params:resolver:rejecter:)
    public func collectBankAccount(
        isPaymentIntent: Bool,
        clientSecret: NSString,
        params: NSDictionary,
        resolver resolve: @escaping RCTPromiseResolveBlock,
        rejecter reject: @escaping RCTPromiseRejectBlock
    ) {
        let paymentMethodData = params["paymentMethodData"] as? NSDictionary
        let type = Mappers.mapToPaymentMethodType(type: params["paymentMethodType"] as? String)
        if type != STPPaymentMethodType.USBankAccount {
            resolve(Errors.createError(ErrorType.Failed, "collectBankAccount currently only accepts the USBankAccount payment method type."))
            return
        }

        guard let billingDetails = paymentMethodData?["billingDetails"] as? [String: Any?], let name = billingDetails["name"] as? String else {
            resolve(Errors.createError(ErrorType.Failed, "You must provide a name when collecting US bank account details."))
            return
        }

        if name.isEmpty {
            resolve(Errors.createError(ErrorType.Failed, "You must provide a name when collecting US bank account details."))
            return
        }

        let collectParams = STPCollectBankAccountParams.collectUSBankAccountParams(
            with: name,
            email: billingDetails["email"] as? String
        )

        let connectionsReturnURL: String?
        if let urlScheme = urlScheme {
            connectionsReturnURL = Mappers.mapToFinancialConnectionsReturnURL(urlScheme: urlScheme)
        } else {
            connectionsReturnURL = nil
        }

        let onEvent = { [weak self] (event: FinancialConnectionsEvent) in
            let mappedEvent = Mappers.financialConnectionsEventToMap(event)
            self?.emitter?.emitOnFinancialConnectionsEvent(mappedEvent)
        }

        let style = STPBankAccountCollectorUserInterfaceStyle(from: params)
        let bankAccountCollector = STPBankAccountCollector(style: style)

        if isPaymentIntent {
            DispatchQueue.main.async {
                bankAccountCollector.collectBankAccountForPayment(
                    clientSecret: clientSecret as String,
                    returnURL: connectionsReturnURL,
                    params: collectParams,
                    from: findViewControllerPresenter(from: RCTKeyWindow()?.rootViewController ?? UIViewController()),
                    onEvent: onEvent
                ) { intent, error in
                    if let error = error {
                        resolve(Errors.createError(ErrorType.Failed, error as NSError))
                        return
                    }

                    if let intent = intent {
                        if intent.status == .requiresPaymentMethod {
                            resolve(Errors.createError(ErrorType.Canceled, "Bank account collection was canceled."))
                        } else {
                            resolve(
                                Mappers.createResult("paymentIntent", Mappers.mapFromPaymentIntent(paymentIntent: intent))
                            )
                        }
                    } else {
                        resolve(Errors.createError(ErrorType.Unknown, "There was unexpected error while collecting bank account information."))
                    }
                }
            }
        } else {
            DispatchQueue.main.async {
                bankAccountCollector.collectBankAccountForSetup(
                    clientSecret: clientSecret as String,
                    returnURL: connectionsReturnURL,
                    params: collectParams,
                    from: findViewControllerPresenter(from: RCTKeyWindow()?.rootViewController ?? UIViewController()),
                    onEvent: onEvent
                ) { intent, error in
                    if let error = error {
                        resolve(Errors.createError(ErrorType.Failed, error as NSError))
                        return
                    }

                    if let intent = intent {
                        if intent.status == .requiresPaymentMethod {
                            resolve(Errors.createError(ErrorType.Canceled, "Bank account collection was canceled."))
                        } else {
                            resolve(
                                Mappers.createResult("setupIntent", Mappers.mapFromSetupIntent(setupIntent: intent))
                            )
                        }
                    } else {
                        resolve(Errors.createError(ErrorType.Unknown, "There was unexpected error while collecting bank account information."))
                    }
                }
            }
        }
    }

    @objc(confirmPayment:data:options:resolver:rejecter:)
    public func confirmPayment(
        paymentIntentClientSecret: String,
        params: NSDictionary?,
        options: NSDictionary,
        resolver resolve: @escaping RCTPromiseResolveBlock,
        rejecter reject: @escaping RCTPromiseRejectBlock
    ) {
        self.confirmPaymentResolver = resolve
        self.confirmPaymentClientSecret = paymentIntentClientSecret

        let paymentMethodData = params?["paymentMethodData"] as? NSDictionary
        let (missingPaymentMethodError, paymentMethodType) = getPaymentMethodType(params: params)
        if missingPaymentMethodError != nil {
            resolve(missingPaymentMethodError)
            return
        }

        let (error, paymentIntentParams) = createPaymentIntentParams(paymentIntentClientSecret: paymentIntentClientSecret, paymentMethodType: paymentMethodType, paymentMethodData: paymentMethodData, options: options)

        if error != nil {
            resolve(error)
        } else {
            STPPaymentHandler.shared().confirmPayment(paymentIntentParams, with: self, completion: onCompleteConfirmPayment)
        }
    }

    func getPaymentMethodType(
        params: NSDictionary?
    ) -> (NSDictionary?, STPPaymentMethodType?) {
        if let params = params {
            guard let paymentMethodType = Mappers.mapToPaymentMethodType(type: params["paymentMethodType"] as? String) else {
                return (Errors.createError(ErrorType.Failed, "You must provide paymentMethodType"), nil)
            }
            return (nil, paymentMethodType)
        } else {
            // If params aren't provided, it means we expect that the payment method was attached on the server side
            return (nil, nil)
        }
    }

    func createPaymentIntentParams(
        paymentIntentClientSecret: String,
        paymentMethodType: STPPaymentMethodType?,
        paymentMethodData: NSDictionary?,
        options: NSDictionary
    ) -> (NSDictionary?, STPPaymentIntentParams) {
        var err: NSDictionary?

        let paymentIntentParams: STPPaymentIntentParams = {
            // If payment method data is not supplied, assume payment method was attached through via collectBankAccount
            if paymentMethodType == .USBankAccount && paymentMethodData == nil {
                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)

                if paymentMethodId != nil {
                    parameters.paymentMethodId = paymentMethodId
                } else {
                    do {
                        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
            }
        }()

        if let setupFutureUsage = options["setupFutureUsage"] as? String {
            paymentIntentParams.setupFutureUsage = Mappers.mapToPaymentIntentFutureUsage(usage: setupFutureUsage)
        }
        if let urlScheme = urlScheme {
            paymentIntentParams.returnURL = Mappers.mapToReturnURL(urlScheme: urlScheme)
        }
        paymentIntentParams.shipping = Mappers.mapToShippingDetails(shippingDetails: paymentMethodData?["shippingDetails"] as? NSDictionary)

        return (err, paymentIntentParams)
    }

    @objc(retrievePaymentIntent:resolver:rejecter:)
    public func retrievePaymentIntent(
        clientSecret: String,
        resolver resolve: @escaping RCTPromiseResolveBlock,
        rejecter reject: @escaping RCTPromiseRejectBlock
    ) {
        STPAPIClient.shared.retrievePaymentIntent(withClientSecret: clientSecret) { (paymentIntent, error) in
            guard error == nil else {
                if let lastPaymentError = paymentIntent?.lastPaymentError {
                    resolve(Errors.createError(ErrorType.Unknown, lastPaymentError))
                } else {
                    resolve(Errors.createError(ErrorType.Unknown, error as? NSError))
                }
                return
            }

            if let paymentIntent = paymentIntent {
                resolve(Mappers.createResult("paymentIntent", Mappers.mapFromPaymentIntent(paymentIntent: paymentIntent)))
            } else {
                resolve(Errors.createError(ErrorType.Unknown, "Failed to retrieve the PaymentIntent"))
            }
        }
    }

    @objc(retrieveSetupIntent:resolver:rejecter:)
    public func retrieveSetupIntent(
        clientSecret: String,
        resolver resolve: @escaping RCTPromiseResolveBlock,
        rejecter reject: @escaping RCTPromiseRejectBlock
    ) {
        STPAPIClient.shared.retrieveSetupIntent(withClientSecret: clientSecret) { (setupIntent, error) in
            guard error == nil else {
                if let lastSetupError = setupIntent?.lastSetupError {
                    resolve(Errors.createError(ErrorType.Unknown, lastSetupError))
                } else {
                    resolve(Errors.createError(ErrorType.Unknown, error as? NSError))
                }
                return
            }

            if let setupIntent = setupIntent {
                resolve(Mappers.createResult("setupIntent", Mappers.mapFromSetupIntent(setupIntent: setupIntent)))
            } else {
                resolve(Errors.createError(ErrorType.Unknown, "Failed to retrieve the SetupIntent"))
            }
        }
    }

    @objc(verifyMicrodeposits:clientSecret:params:resolver:rejecter:)
    public func verifyMicrodeposits(
        isPaymentIntent: Bool,
        clientSecret: NSString,
        params: NSDictionary,
        resolver resolve: @escaping RCTPromiseResolveBlock,
        rejecter reject: @escaping RCTPromiseRejectBlock
    ) {
        let amounts = params["amounts"] as? NSArray
        let descriptorCode = params["descriptorCode"] as? String

        if amounts != nil && descriptorCode != nil || amounts == nil && descriptorCode == nil {
            resolve(Errors.createError(ErrorType.Failed, "You must provide either amounts OR descriptorCode, not both."))
            return
        }

        if let amounts = amounts {
            if amounts.count != 2 {
                resolve(Errors.createError(ErrorType.Failed, "Expected 2 integers in the amounts array, but received " + String(amounts.count)))
                return
            }
            if isPaymentIntent {
                STPAPIClient.shared.verifyPaymentIntentWithMicrodeposits(
                    clientSecret: clientSecret as String,
                    firstAmount: amounts[0] as! Int,
                    secondAmount: amounts[1] as! Int,
                    completion: onCompletePaymentVerification
                )
            } else {
                STPAPIClient.shared.verifySetupIntentWithMicrodeposits(
                    clientSecret: clientSecret as String,
                    firstAmount: amounts[0] as! Int,
                    secondAmount: amounts[1] as! Int,
                    completion: onCompleteSetupVerification
                )
            }
        } else if let descriptorCode = descriptorCode {
            if isPaymentIntent {
                STPAPIClient.shared.verifyPaymentIntentWithMicrodeposits(
                    clientSecret: clientSecret as String,
                    descriptorCode: descriptorCode,
                    completion: onCompletePaymentVerification
                )
            } else {
                STPAPIClient.shared.verifySetupIntentWithMicrodeposits(
                    clientSecret: clientSecret as String,
                    descriptorCode: descriptorCode,
                    completion: onCompleteSetupVerification
                )
            }
        }

        func onCompletePaymentVerification(intent: STPPaymentIntent?, error: Error?) {
            if error != nil {
                resolve(Errors.createError(ErrorType.Failed, error as NSError?))
            } else {
                resolve(Mappers.createResult("paymentIntent", Mappers.mapFromPaymentIntent(paymentIntent: intent!)))
            }
        }
        func onCompleteSetupVerification(intent: STPSetupIntent?, error: Error?) {
            if error != nil {
                resolve(Errors.createError(ErrorType.Failed, error as NSError?))
            } else {
                resolve(Mappers.createResult("setupIntent", Mappers.mapFromSetupIntent(setupIntent: intent!)))
            }
        }
    }

    @objc(canAddCardToWallet:resolver:rejecter:)
    public func canAddCardToWallet(
        params: NSDictionary,
        resolver resolve: @escaping RCTPromiseResolveBlock,
        rejecter reject: @escaping RCTPromiseRejectBlock
    ) {
        PushProvisioningUtils.canAddCardToWallet(
            primaryAccountIdentifier: params["primaryAccountIdentifier"] as? String ?? "",
            testEnv: params["testEnv"] as? Bool ?? false,
            hasPairedAppleWatch: params["hasPairedAppleWatch"]  as? Bool ?? false)
        { canAddCard, status in
            resolve([
                "canAddCard": canAddCard,
                "details": ["status": status?.rawValue],
            ])
        }
    }

    @objc(isCardInWallet:resolver:rejecter:)
    public func isCardInWallet(
        params: NSDictionary,
        resolver resolve: @escaping RCTPromiseResolveBlock,
        rejecter reject: @escaping RCTPromiseRejectBlock
    ) {
        guard let last4 = params["cardLastFour"] as? String else {
            resolve(Errors.createError(ErrorType.Failed, "You must provide `cardLastFour`"))
            return
        }
        resolve(["isInWallet": PushProvisioningUtils.getPassLocation(last4: last4) != nil])
    }

    @objc(collectBankAccountToken:params:resolver:rejecter:)
    public func collectBankAccountToken(
        clientSecret: String,
        params: NSDictionary,
        resolver resolve: @escaping RCTPromiseResolveBlock,
        rejecter reject: @escaping RCTPromiseRejectBlock
    ) {
        if STPAPIClient.shared.publishableKey == nil {
            resolve(Errors.MISSING_INIT_ERROR)
            return
        }
        let returnURL: String?
        if let urlScheme = urlScheme {
            returnURL = Mappers.mapToFinancialConnectionsReturnURL(urlScheme: urlScheme)
        } else {
            returnURL = nil
        }

        let configuration = FinancialConnectionsSheet.Configuration(from: params)

        let onEvent = { [weak self] (event: FinancialConnectionsEvent) in
            let mappedEvent = Mappers.financialConnectionsEventToMap(event)
            self?.emitter?.emitOnFinancialConnectionsEvent(mappedEvent)
        }

        // Use connectedAccountId from params if provided
        let originalStripeAccount = STPAPIClient.shared.stripeAccount
        if let connectedAccountId = params["connectedAccountId"] as? String {
            STPAPIClient.shared.stripeAccount = connectedAccountId
        }

        let wrappedResolve: RCTPromiseResolveBlock = { result in
            // Restore original stripeAccount after completion
            STPAPIClient.shared.stripeAccount = originalStripeAccount
            resolve(result)
        }

        FinancialConnections.presentForToken(
            withClientSecret: clientSecret,
            returnURL: returnURL,
            configuration: configuration,
            onEvent: onEvent,
            resolve: wrappedResolve
        )
    }

    @objc(collectFinancialConnectionsAccounts:params:resolver:rejecter:)
    public func collectFinancialConnectionsAccounts(
        clientSecret: String,
        params: NSDictionary,
        resolver resolve: @escaping RCTPromiseResolveBlock,
        rejecter reject: @escaping RCTPromiseRejectBlock
    ) {
        if STPAPIClient.shared.publishableKey == nil {
            resolve(Errors.MISSING_INIT_ERROR)
            return
        }
        let returnURL: String?
        if let urlScheme = urlScheme {
            returnURL = Mappers.mapToFinancialConnectionsReturnURL(urlScheme: urlScheme)
        } else {
            returnURL = nil
        }

        let configuration = FinancialConnectionsSheet.Configuration(from: params)

        let onEvent = { [weak self] (event: FinancialConnectionsEvent) in
            let mappedEvent = Mappers.financialConnectionsEventToMap(event)
            self?.emitter?.emitOnFinancialConnectionsEvent(mappedEvent)
        }

        // Use connectedAccountId from params if provided
        let originalStripeAccount = STPAPIClient.shared.stripeAccount
        if let connectedAccountId = params["connectedAccountId"] as? String {
            STPAPIClient.shared.stripeAccount = connectedAccountId
        }

        let wrappedResolve: RCTPromiseResolveBlock = { result in
            // Restore original stripeAccount after completion
            STPAPIClient.shared.stripeAccount = originalStripeAccount
            resolve(result)
        }

        FinancialConnections.present(
            withClientSecret: clientSecret,
            returnURL: returnURL,
            configuration: configuration,
            onEvent: onEvent,
            resolve: wrappedResolve
        )
    }

    @objc(configureOrderTracking:orderIdentifier:webServiceUrl:authenticationToken:resolver:rejecter:)
    public func configureOrderTracking(
        orderTypeIdentifier: String,
        orderIdentifier: String,
        webServiceUrl: String,
        authenticationToken: String,
        resolver resolve: @escaping RCTPromiseResolveBlock,
        rejecter reject: @escaping RCTPromiseRejectBlock
    ) {
        #if compiler(>=5.7)
        if #available(iOS 16.0, *) {
            if let orderTrackingHandler = self.orderTrackingHandler {
                if let url = URL(string: webServiceUrl) {
                    orderTrackingHandler.result.orderDetails = PKPaymentOrderDetails(
                        orderTypeIdentifier: orderTypeIdentifier,
                        orderIdentifier: orderIdentifier,
                        webServiceURL: url,
                        authenticationToken: authenticationToken)
                }
                orderTrackingHandler.handler(orderTrackingHandler.result)
                self.orderTrackingHandler = nil
            }
        }
        #endif
    }

    @objc(createRadarSession:rejecter:)
    public func createRadarSession(
        resolver resolve: @escaping RCTPromiseResolveBlock,
        rejecter reject: @escaping RCTPromiseRejectBlock
    ) {
        STPAPIClient.shared.createRadarSession { (session, error) in
            if let error = error as NSError? {
                resolve(Errors.createError(ErrorType.Failed, error))
                return
            }

            guard let session else {
                resolve(Errors.createError(ErrorType.Unknown, "Radar session not available"))
                return
            }

            resolve(["id": session.id])
        }
    }

#if canImport(StripeCryptoOnramp)
    @objc(configureOnramp:resolver:rejecter:)
    public func configureOnramp(
        config: NSDictionary,
        resolver resolve: @escaping RCTPromiseResolveBlock,
        rejecter reject: @escaping RCTPromiseRejectBlock
    ) {
        guard isPublishableKeyAvailable(resolve) else {
            return
        }

        let appearance: LinkAppearance = if let appearanceParams = config["appearance"] as? [String: Any?] {
            Mappers.mapToLinkAppearance(appearanceParams)
        } else {
            LinkAppearance()
        }

        let cryptoCustomerId = config["cryptoCustomerId"] as? String

        Task {
            do {
                cryptoOnrampCoordinator = try await CryptoOnrampCoordinator.create(appearance: appearance, cryptoCustomerID: cryptoCustomerId)
                resolve([:])  // Return empty object on success
            } catch {
                let errorResult = Errors.createError(ErrorType.Failed, error)
                resolve(["error": errorResult["error"]!])
            }
        }
    }

    @objc(hasLinkAccount:resolver:rejecter:)
    public func hasLinkAccount(
        email: String,
        resolver resolve: @escaping RCTPromiseResolveBlock,
        rejecter reject: @escaping RCTPromiseRejectBlock
    ) {
        guard isPublishableKeyAvailable(resolve), let coordinator = requireOnrampCoordinator(resolve) else {
            return
        }

        Task {
            do {
                let hasLinkAccount = try await coordinator.hasLinkAccount(with: email)
                resolve(["hasLinkAccount": hasLinkAccount])
            } catch {
                let errorResult = Errors.createError(ErrorType.Failed, error)
                resolve(["error": errorResult["error"]!])
            }
        }
    }

    @objc(registerLinkUser:resolver:rejecter:)
    public func registerLinkUser(
        info: NSDictionary,
        resolver resolve: @escaping RCTPromiseResolveBlock,
        rejecter reject: @escaping RCTPromiseRejectBlock
    ) {
        guard isPublishableKeyAvailable(resolve), let coordinator = requireOnrampCoordinator(resolve) else {
            return
        }

        let email = info["email"] as? String ?? ""
        let phone = info["phone"] as? String ?? ""
        let country = info["country"] as? String ?? ""
        let fullName = info["fullName"] as? String

        Task {
            do {
                let customerId = try await coordinator.registerLinkUser(email: email, fullName: fullName, phone: phone, country: country)
                resolve(["customerId": customerId])
            } catch {
                let errorResult = Errors.createError(ErrorType.Failed, error)
                resolve(["error": errorResult["error"]!])
            }
        }
    }

    @objc(authenticateUserWithToken:resolver:rejecter:)
    public func authenticateUserWithToken(
        _ linkAuthTokenClientSecret: String,
        resolver resolve: @escaping RCTPromiseResolveBlock,
        rejecter reject: @escaping RCTPromiseRejectBlock
    ) {
        guard isPublishableKeyAvailable(resolve), let coordinator = requireOnrampCoordinator(resolve) else {
            return
        }

        Task {
            do {
                try await coordinator.authenticateUserWithToken(linkAuthTokenClientSecret)
                resolve([:])  // Return empty object on success
            } catch {
                if let onrampError = error as? CryptoOnrampCoordinator.Error,
                   case let .seamlessSignInTokenInvalid(reason) = onrampError {
                    let errorResult = Errors.createError(ErrorType.Failed, reason ?? onrampError.localizedDescription)
                    resolve(["error": errorResult["error"]!])
                    return
                }

                let errorResult = Errors.createError(ErrorType.Failed, error)
                resolve(["error": errorResult["error"]!])
            }
        }
    }

    @objc(registerWalletAddress:network:resolver:rejecter:)
    public func registerWalletAddress(
        address: String,
        network: String,
        resolver resolve: @escaping RCTPromiseResolveBlock,
        rejecter reject: @escaping RCTPromiseRejectBlock
    ) {
        guard isPublishableKeyAvailable(resolve), let coordinator = requireOnrampCoordinator(resolve) else {
            return
        }

        guard let cryptoNetwork = CryptoNetwork(rawValue: network as String) else {
            let errorResult = Errors.createError(ErrorType.Unknown, "Invalid network: \(network)")
            resolve(["error": errorResult["error"]!])
            return
        }

        Task {
            do {
                try await coordinator.registerWalletAddress(walletAddress: address, network: cryptoNetwork)
                resolve([:])  // Return empty object on success
            } catch {
                let errorResult = Errors.createError(ErrorType.Failed, error)
                resolve(["error": errorResult["error"]!])
            }
        }
    }

    @objc(attachKycInfo:resolver:rejecter:)
    public func attachKycInfo(
        info: NSDictionary,
        resolver resolve: @escaping RCTPromiseResolveBlock,
        rejecter reject: @escaping RCTPromiseRejectBlock
    ) {
        guard isPublishableKeyAvailable(resolve), let coordinator = requireOnrampCoordinator(resolve) else {
            return
        }

        guard let kycInfoDictionary = info as? [String: Any] else {
            let errorResult = Errors.createError(ErrorType.Failed, "Unexpected format of KYC info dictionary. Expected String keys.")
            resolve(["error": errorResult["error"]!])
            return
        }

        Task {
            do {
                let kycInfo = try Mappers.mapToKycInfo(kycInfoDictionary)
                try await coordinator.attachKYCInfo(info: kycInfo)
                resolve([:])  // Return empty object on success
            } catch {
                if let kycInfoError = error as? Mappers.KycInfoError, case let .invalidField(field) = kycInfoError {
                    let errorResult = Errors.createError(ErrorType.Unknown, "Invalid format for field: \(field)")
                    resolve(["error": errorResult["error"]!])
                } else {
                    let errorResult = Errors.createError(ErrorType.Failed, error)
                    resolve(["error": errorResult["error"]!])
                }
            }
        }
    }

    @objc(presentKycInfoVerification:resolver:rejecter:)
    public func presentKycInfoVerification(
        updatedAddressDictionary: NSDictionary?,
        resolver resolve: @escaping RCTPromiseResolveBlock,
        rejecter reject: @escaping RCTPromiseRejectBlock
    ) {
        guard isPublishableKeyAvailable(resolve), let coordinator = requireOnrampCoordinator(resolve) else {
            return
        }

        let updatedAddress: Address?
        if let updatedAddressDictionary {
            guard let typedDictionary = updatedAddressDictionary as? [String: String] else {
                let errorResult = Errors.createError(ErrorType.Failed, "Unexpected format of address dictionary. Expected String keys and values.")
                resolve(["error": errorResult["error"]!])
                return
            }

            updatedAddress = Mappers.mapToKycAddress(typedDictionary)
        } else {
            updatedAddress = nil
        }

        Task {
            do {
                let presentingViewController = await MainActor.run {
                    findViewControllerPresenter(from: RCTKeyWindow()?.rootViewController ?? UIViewController())
                }
                let result = try await coordinator.verifyKYCInfo(updatedAddress: updatedAddress, from: presentingViewController)
                switch result {
                case .confirmed:
                    resolve(["status": "Confirmed"])
                case .updateAddress:
                    resolve(["status": "UpdateAddress"])
                case .canceled:
                    let errorResult = Errors.createError(ErrorType.Canceled, "KYC info verification was cancelled")
                    resolve(["error": errorResult["error"]!])
                }
            } catch {
                let errorResult = Errors.createError(ErrorType.Failed, error)
                resolve(["error": errorResult["error"]!])
            }
        }
    }

    @objc(updatePhoneNumber:resolver:rejecter:)
    public func updatePhoneNumber(
        phone: String,
        resolver resolve: @escaping RCTPromiseResolveBlock,
        rejecter reject: @escaping RCTPromiseRejectBlock
    ) {
        guard isPublishableKeyAvailable(resolve), let coordinator = requireOnrampCoordinator(resolve) else {
            return
        }

        Task {
            do {
                try await coordinator.updatePhoneNumber(to: phone)
                resolve([:]) // Return empty object on success
            } catch {
                let errorResult = Errors.createError(ErrorType.Failed, error)
                resolve(["error": errorResult["error"]!])
            }
        }
    }

    @objc(logout:rejecter:)
    public func logout(
        resolver resolve: @escaping RCTPromiseResolveBlock,
        rejecter reject: @escaping RCTPromiseRejectBlock
    ) {
        guard isPublishableKeyAvailable(resolve), let coordinator = requireOnrampCoordinator(resolve) else {
            return
        }

        Task {
            do {
                try await coordinator.logOut()
                resolve([:]) // Return empty object on success
            } catch {
                let errorResult = Errors.createError(ErrorType.Failed, error)
                resolve(["error": errorResult["error"]!])
            }
        }
    }

    @objc(verifyIdentity:rejecter:)
    public func verifyIdentity(
        resolver resolve: @escaping RCTPromiseResolveBlock,
        rejecter reject: @escaping RCTPromiseRejectBlock
    ) {
        guard isPublishableKeyAvailable(resolve), let coordinator = requireOnrampCoordinator(resolve) else {
            return
        }

        Task {
            do {
                let presentingViewController = await MainActor.run {
                    findViewControllerPresenter(from: RCTKeyWindow()?.rootViewController ?? UIViewController())
                }
                let result = try await coordinator.verifyIdentity(from: presentingViewController)
                switch result {
                case .completed:
                    resolve([:])  // Return empty object on success
                case .canceled:
                    resolve(["error": Errors.createError(ErrorType.Canceled, "Identity verification was cancelled")["error"]!])
                }
            } catch {
                let errorResult = Errors.createError(ErrorType.Failed, error)
                resolve(["error": errorResult["error"]!])
            }
        }
    }

    @objc(collectPaymentMethod:platformPayParams:resolver:rejecter:)
    public func collectPaymentMethod(
        paymentMethod: String,
        platformPayParams: NSDictionary,
        resolver resolve: @escaping RCTPromiseResolveBlock,
        rejecter reject: @escaping RCTPromiseRejectBlock
    ) {
        guard isPublishableKeyAvailable(resolve), let coordinator = requireOnrampCoordinator(resolve) else {
            return
        }

        var paymentMethodType: PaymentMethodType?
        switch paymentMethod {
        case "Card":
            paymentMethodType = .card
        case "BankAccount":
            paymentMethodType = .bankAccount
        case "CardAndBankAccount":
            paymentMethodType = .cardAndBankAccount
        case "PlatformPay":
            guard let applePayParams = platformPayParams["applePay"] as? NSDictionary else {
                resolve(Errors.createError(ErrorType.Failed, "You must provide the `applePay` parameter."))
                return
        }

            let (error, paymentRequest) = ApplePayUtils.createPaymentRequest(merchantIdentifier: merchantIdentifier, params: applePayParams)
            if let paymentRequest {
                paymentMethodType = .applePay(paymentRequest: paymentRequest)
            } else {
                resolve(Errors.createError(ErrorType.Failed, "Unable to create Apple Pay payment request: \(String(describing: error))"))
                return
            }
        default:
            resolve(Errors.createError(ErrorType.Failed, "Unsupported payment method: \(paymentMethod)"))
            return
        }

        guard let paymentMethodType else {
            // In all non-assignment branches above, we've already called `resolve` with error, so simply return.
            return
        }

        Task {
            do {
                let presentingViewController = await MainActor.run {
                    findViewControllerPresenter(from: RCTKeyWindow()?.rootViewController ?? UIViewController())
                }
                let result = try await coordinator.collectPaymentMethod(type: paymentMethodType, from: presentingViewController)
                switch result {
                case .canceled:
                    let errorResult = Errors.createError(ErrorType.Canceled, "Payment collection was cancelled")
                    resolve(["error": errorResult["error"]!])
                case .completed(let displayData, let kycInfo):
                    var response: [String: Any] = ["displayData": Mappers.paymentMethodDisplayDataToMap(displayData)]

                    if let kycInfo {
                        response["kycInfo"] = Mappers.mapFromKycInfo(kycInfo)
                    }

                    resolve(response)
                @unknown default:
                    let errorResult = Errors.createError(ErrorType.Failed, "Received an unexpected payment collection result")
                    resolve(["error": errorResult["error"]!])
                }
            } catch {
                let errorResult = Errors.createError(ErrorType.Failed, error)
                resolve(["error": errorResult["error"]!])
            }
        }
    }

    @objc(createCryptoPaymentToken:rejecter:)
    public func createCryptoPaymentToken(
        resolver resolve: @escaping RCTPromiseResolveBlock,
        rejecter reject: @escaping RCTPromiseRejectBlock
    ) {
        guard isPublishableKeyAvailable(resolve), let coordinator = requireOnrampCoordinator(resolve) else {
            return
        }

        Task {
            do {
                let token = try await coordinator.createCryptoPaymentToken()
                resolve(["cryptoPaymentToken": token])
            } catch {
                let errorResult = Errors.createError(ErrorType.Failed, error)
                resolve(["error": errorResult["error"]!])
            }
        }
    }

    @objc(performCheckout:resolver:rejecter:)
    public func performCheckout(
        onrampSessionId: String,
        resolver resolve: @escaping RCTPromiseResolveBlock,
        rejecter reject: @escaping RCTPromiseRejectBlock
    ) {
        guard isPublishableKeyAvailable(resolve), let coordinator = requireOnrampCoordinator(resolve) else {
            return
        }

        Task {
            do {
                let result = try await coordinator.performCheckout(onrampSessionId: onrampSessionId, authenticationContext: self) { [weak self] onrampSessionId in
                    self?.onrampEmitter?.emitOnCheckoutClientSecretRequested(["onrampSessionId": onrampSessionId])

                    let clientSecret = try await withCheckedThrowingContinuation { [weak self] continuation in
                        self?.cryptoOnrampCheckoutClientSecretContinuation = continuation
                    }

                    return clientSecret
                }
                switch result {
                case .completed:
                    resolve([:]) // Return empty object on success
                case .canceled:
                    let errorResult = Errors.createError(ErrorType.Canceled, "Checkout was cancelled")
                    resolve(["error": errorResult["error"]!])
                }
            } catch {
                let errorResult = Errors.createError(ErrorType.Failed, error)
                resolve(["error": errorResult["error"]!])
            }
        }
    }

    @objc(provideCheckoutClientSecret:)
    public func provideCheckoutClientSecret(clientSecret: String?) {
        if let clientSecret {
            cryptoOnrampCheckoutClientSecretContinuation?.resume(returning: clientSecret)
        } else {
            let error = NSError(
                domain: ErrorType.Failed,
                code: -1,
                userInfo: [NSLocalizedDescriptionKey: "Failed to provide checkout client secret"]
            )
            cryptoOnrampCheckoutClientSecretContinuation?.resume(throwing: error)
        }
        cryptoOnrampCheckoutClientSecretContinuation = nil
    }

    @objc(onrampAuthorize:resolver:rejecter:)
    public func onrampAuthorize(
        linkAuthIntentId: String,
        resolver resolve: @escaping RCTPromiseResolveBlock,
        rejecter reject: @escaping RCTPromiseRejectBlock
    ) {
        guard isPublishableKeyAvailable(resolve), let coordinator = requireOnrampCoordinator(resolve) else {
            return
        }

        Task {
            do {
                let presentingViewController = await MainActor.run {
                    findViewControllerPresenter(from: RCTKeyWindow()?.rootViewController ?? UIViewController())
                }
                let authorizationResult = try await coordinator.authorize(linkAuthIntentId: linkAuthIntentId, from: presentingViewController)
                switch authorizationResult {
                case let .consented(customerId):
                    resolve(["status": "Consented", "customerId": customerId])
                case .denied:
                    resolve(["status": "Denied"])
                case.canceled:
                    let errorResult = Errors.createError(ErrorType.Canceled, "Authorization was cancelled")
                    resolve(["error": errorResult["error"]!])
                }
            } catch {
                let errorResult = Errors.createError(ErrorType.Failed, error)
                resolve(["error": errorResult["error"]!])
            }
        }
    }

    @objc(getCryptoTokenDisplayData:resolver:rejecter:)
    public func getCryptoTokenDisplayData(
        token: NSDictionary,
        resolver resolve: @escaping RCTPromiseResolveBlock,
        rejecter reject: @escaping RCTPromiseRejectBlock
    ) {
        let label = STPPaymentMethodType.link.displayName

        if let cardDetails = token["card"] as? [String: Any] {
            let brand = cardDetails["brand"] as? String ?? ""
            let funding = cardDetails["funding"] as? String ?? ""
            let last4 = cardDetails["last4"] as? String ?? ""

            let cardBrand = STPCard.brand(from: brand)
            let icon = STPImageLibrary.cardBrandImage(for: cardBrand)
            let brandName = STPCardBrandUtilities.stringFrom(cardBrand)

            let mappedFunding = STPCardFundingType(funding)
            let formattedBrandName = String(format: mappedFunding.displayNameWithBrand, brandName ?? "")
            let sublabel = "\(formattedBrandName) •••• \(last4)"

            let result = PaymentMethodDisplayData(paymentMethodType: .card, icon: icon, label: label, sublabel: sublabel)
            let displayData = Mappers.paymentMethodDisplayDataToMap(result)

            resolve(["displayData": displayData])
        } else if let bankDetails = token["us_bank_account"] as? [String: Any] {
            let bankName = bankDetails["bank_name"] as? String ?? ""
            let last4 = bankDetails["last4"] as? String ?? ""

            let iconCode = PaymentSheetImageLibrary.bankIconCode(for: bankName)
            let icon = PaymentSheetImageLibrary.bankIcon(for: iconCode, iconStyle: .filled)
            let sublabel = "\(bankName) •••• \(last4)"

            let result = PaymentMethodDisplayData(paymentMethodType: .bankAccount, icon: icon, label: label, sublabel: sublabel)
            let displayData = Mappers.paymentMethodDisplayDataToMap(result)

            resolve(["displayData": displayData])
        } else {
            let errorResult = Errors.createError(ErrorType.Unknown, "'type' parameter not unknown.")
            resolve(["error": errorResult["error"]!])
        }
    }

    /// Checks for a `publishableKey`. Calls the resolve block with an error when one doesn’t exist.
    /// - Parameter resolve: The resolve block that is called with an error if no `publishableKey` is found.
    /// - Returns: `true` if a `publishableKey` was found. `false` otherwise.
    private func isPublishableKeyAvailable(_ resolve: @escaping RCTPromiseResolveBlock) -> Bool {
        if STPAPIClient.shared.publishableKey == nil {
            resolve(["error": Errors.MISSING_INIT_ERROR["error"]!])
            return false
        } else {
            return true
        }
    }

    /// Returns the shared `CryptoOnrampCoordinator`, calling the resolve block with an error if CryptoOnramp has not yet been configured.
    /// - Parameter resolve: The resolve block that is called with an error if CryptoOnramp has not yet been configured.
    /// - Returns: The shared `CryptoOnrampCoordinator`, nor `nil` if CryptoOnramp has not yet been configured.
    private func requireOnrampCoordinator(_ resolve: @escaping RCTPromiseResolveBlock) -> CryptoOnrampCoordinator? {
        guard let coordinator = cryptoOnrampCoordinator else {
            let errorResult = Errors.createError(ErrorType.Failed, "CryptoOnramp not configured. Call -configureOnramp:resolver:rejecter: successfully first")
            resolve(["error": errorResult["error"]!])
            return nil
        }

        return coordinator
    }
#else
    @objc(configureOnramp:resolver:rejecter:)
    public func configureOnramp(config: NSDictionary, resolver resolve: @escaping RCTPromiseResolveBlock, rejecter reject: @escaping RCTPromiseRejectBlock) {
        resolveWithCryptoOnrampNotAvailableError(resolve)
    }

    @objc(hasLinkAccount:resolver:rejecter:)
    public func hasLinkAccount(email: String, resolver resolve: @escaping RCTPromiseResolveBlock, rejecter reject: @escaping RCTPromiseRejectBlock) {
        resolveWithCryptoOnrampNotAvailableError(resolve)
    }

    @objc(registerLinkUser:resolver:rejecter:)
    public func registerLinkUser(info: NSDictionary, resolver resolve: @escaping RCTPromiseResolveBlock, rejecter reject: @escaping RCTPromiseRejectBlock) {
        resolveWithCryptoOnrampNotAvailableError(resolve)
    }

    @objc(authenticateUserWithToken:resolver:rejecter:)
    public func authenticateUserWithToken(
        _ linkAuthTokenClientSecret: String,
        resolver resolve: @escaping RCTPromiseResolveBlock,
        rejecter reject: @escaping RCTPromiseRejectBlock
    ) {
        resolveWithCryptoOnrampNotAvailableError(resolve)
    }

    @objc(registerWalletAddress:network:resolver:rejecter:)
    public func registerWalletAddress(address: String, network: String, resolver resolve: @escaping RCTPromiseResolveBlock, rejecter reject: @escaping RCTPromiseRejectBlock) {
        resolveWithCryptoOnrampNotAvailableError(resolve)
    }

    @objc(attachKycInfo:resolver:rejecter:)
    public func attachKycInfo(info: NSDictionary, resolver resolve: @escaping RCTPromiseResolveBlock, rejecter reject: @escaping RCTPromiseRejectBlock) {
        resolveWithCryptoOnrampNotAvailableError(resolve)
    }

    @objc(presentKycInfoVerification:resolver:rejecter:)
    public func presentKycInfoVerification(
        updatedAddressDictionary: NSDictionary?,
        resolver resolve: @escaping RCTPromiseResolveBlock,
        rejecter reject: @escaping RCTPromiseRejectBlock
    ) {
        resolveWithCryptoOnrampNotAvailableError(resolve)
    }

    @objc(updatePhoneNumber:resolver:rejecter:)
    public func updatePhoneNumber(phone: String, resolver resolve: @escaping RCTPromiseResolveBlock, rejecter reject: @escaping RCTPromiseRejectBlock) {
        resolveWithCryptoOnrampNotAvailableError(resolve)
    }

    @objc(logout:rejecter:)
    public func logout(resolver resolve: @escaping RCTPromiseResolveBlock, rejecter reject: @escaping RCTPromiseRejectBlock) {
        resolveWithCryptoOnrampNotAvailableError(resolve)
    }

    @objc(verifyIdentity:rejecter:)
    public func verifyIdentity(resolver resolve: @escaping RCTPromiseResolveBlock, rejecter reject: @escaping RCTPromiseRejectBlock) {
        resolveWithCryptoOnrampNotAvailableError(resolve)
    }

    @objc(collectPaymentMethod:platformPayParams:resolver:rejecter:)
    public func collectPaymentMethod(paymentMethod: String, platformPayParams: NSDictionary, resolver resolve: @escaping RCTPromiseResolveBlock, rejecter reject: @escaping RCTPromiseRejectBlock) {
        resolveWithCryptoOnrampNotAvailableError(resolve)
    }

    @objc(createCryptoPaymentToken:rejecter:)
    public func createCryptoPaymentToken(resolver resolve: @escaping RCTPromiseResolveBlock, rejecter reject: @escaping RCTPromiseRejectBlock) {
        resolveWithCryptoOnrampNotAvailableError(resolve)
    }

    @objc(performCheckout:resolver:rejecter:)
    public func performCheckout(onrampSessionId: String, resolver resolve: @escaping RCTPromiseResolveBlock, rejecter reject: @escaping RCTPromiseRejectBlock) {
        resolveWithCryptoOnrampNotAvailableError(resolve)
    }

    @objc(provideCheckoutClientSecret:)
    public func provideCheckoutClientSecret(clientSecret: String?) {
        // no-op when Onramp is unavailable
    }

    @objc(onrampAuthorize:resolver:rejecter:)
    public func onrampAuthorize(linkAuthIntentId: String, resolver resolve: @escaping RCTPromiseResolveBlock, rejecter reject: @escaping RCTPromiseRejectBlock) {
        resolveWithCryptoOnrampNotAvailableError(resolve)
    }

    @objc(getCryptoTokenDisplayData:resolver:rejecter:)
    public func getCryptoTokenDisplayData(token: NSDictionary, resolver resolve: @escaping RCTPromiseResolveBlock, rejecter reject: @escaping RCTPromiseRejectBlock) {
        resolveWithCryptoOnrampNotAvailableError(resolve)
    }

    private func resolveWithCryptoOnrampNotAvailableError(_ resolver: @escaping RCTPromiseResolveBlock) {
        resolver(Errors.createError(ErrorType.Failed, "StripeCryptoOnramp is not available. To enable, add the 'stripe-react-native/Onramp' subspec to your Podfile."))
    }
#endif

    @objc(setFinancialConnectionsForceNativeFlow:resolver:rejecter:)
    public func setFinancialConnectionsForceNativeFlow(
        enabled: Bool,
        resolver resolve: @escaping RCTPromiseResolveBlock,
        rejecter reject: @escaping RCTPromiseRejectBlock
    ) {
        UserDefaults.standard.set(enabled, forKey: "FINANCIAL_CONNECTIONS_EXAMPLE_APP_ENABLE_NATIVE")
        resolve(nil)
    }

    @objc(openAuthenticatedWebView:url:resolver:rejecter:)
    public func openAuthenticatedWebView(
        id: String,
        url: String,
        resolver resolve: @escaping RCTPromiseResolveBlock,
        rejecter reject: @escaping RCTPromiseRejectBlock
    ) {
        guard let url = URL(string: url) else {
            resolve(Errors.createError(ErrorType.Failed, "Invalid URL"))
            return
        }

        DispatchQueue.main.async { [weak self] in
            guard let self = self else {
                resolve(Errors.createError(ErrorType.Failed, "StripeSdkImpl instance deallocated"))
                return
            }

            // Create the authentication session with the configured URL scheme
            self.authenticationSession = ASWebAuthenticationSession(
                url: url,
                callbackURLScheme: nil
            ) { callbackURL, error in
                if let error = error {
                    // User canceled or an error occurred
                    if (error as NSError).code == ASWebAuthenticationSessionError.canceledLogin.rawValue {
                        // User canceled - resolve successfully as this is expected behavior
                        resolve([])
                    } else {
                        resolve(Errors.createError(ErrorType.Failed, error as NSError))
                    }
                } else if let callbackURL = callbackURL {
                    // Return the callback URL
                    let result: [String: Any] = ["url": callbackURL.absoluteString]
                    resolve(result)
                } else {
                    // Session completed successfully without a callback URL
                    resolve([])
                }

                // Clean up the session and context provider
                self.authenticationSession = nil
                self.authenticationContextProvider = nil
            }

            // Configure the session for iOS 13+
            if #available(iOS 13.0, *) {
                let contextProvider = ASWebAuthenticationPresentationContextProvider()
                self.authenticationContextProvider = contextProvider
                self.authenticationSession?.presentationContextProvider = contextProvider
                self.authenticationSession?.prefersEphemeralWebBrowserSession = false
            }

            // Start the session
            guard self.authenticationSession?.start() == true else {
                resolve(Errors.createError(ErrorType.Failed, "Failed to start authentication session"))
                self.authenticationSession = nil
                self.authenticationContextProvider = nil
                return
            }
        }
    }

    @objc(downloadAndShareFile:filename:resolver:rejecter:)
    public func downloadAndShareFile(
        url: String,
        filename: String?,
        resolver resolve: @escaping RCTPromiseResolveBlock,
        rejecter reject: @escaping RCTPromiseRejectBlock
    ) {
        guard let url = URL(string: url) else {
            resolve(["success": false, "error": "InvalidURL"])
            return
        }

        let task = URLSession.shared.dataTask(with: url) { [weak self] data, _, error in
            if let error = error {
                resolve(["success": false, "error": "NetworkError", "message": error.localizedDescription])
                return
            }

            guard let data = data else {
                resolve(["success": false, "error": "NoData"])
                return
            }

            // Save to temp directory
            let tempDir = FileManager.default.temporaryDirectory
            let fileName = filename ?? "export.csv"
            let fileURL = tempDir
                .appendingPathComponent(fileName.replacingOccurrences(of: " ", with: "-"))
                .deletingPathExtension()
                .appendingPathExtension("csv")

            do {
                try data.write(to: fileURL)

                // Present share sheet on main thread
                DispatchQueue.main.async { [weak self] in
                    guard let self = self else {
                        // Avoid leaving temp files behind if module is gone
                        try? FileManager.default.removeItem(at: fileURL)
                        resolve(["success": false, "error": "ModuleDeallocated"])
                        return
                    }

                    self.presentShareSheet(fileURL: fileURL) { success in
                        resolve([
                            "success": success
                        ])
                    }
                }
            } catch {
                resolve(["success": false, "error": "FileSystemError", "message": error.localizedDescription])
            }
        }
        task.resume()
    }

    private func presentShareSheet(fileURL: URL, completion: @escaping (Bool) -> Void) {
        guard let rootViewController = RCTKeyWindow()?.rootViewController else {
            // Clean up temp file
            try? FileManager.default.removeItem(at: fileURL)
            completion(false)
            return
        }

        let activityVC = UIActivityViewController(
            activityItems: [fileURL],
            applicationActivities: nil
        )

        // iPad support
        if let popover = activityVC.popoverPresentationController {
            popover.sourceView = rootViewController.view
            popover.sourceRect = CGRect(
                x: rootViewController.view.bounds.midX,
                y: rootViewController.view.bounds.midY,
                width: 0,
                height: 0
            )
            popover.permittedArrowDirections = []
        }

        activityVC.completionWithItemsHandler = { _, completed, _, _ in
            // Clean up temp file after sharing is complete
            try? FileManager.default.removeItem(at: fileURL)
            completion(completed)
        }

        rootViewController.present(activityVC, animated: true)
    }

    public func presentationControllerDidDismiss(_ presentationController: UIPresentationController) {
        confirmPaymentResolver?(Errors.createError(ErrorType.Canceled, "FPX Payment has been canceled"))
    }

    func onCompleteConfirmPayment(status: STPPaymentHandlerActionStatus, paymentIntent: STPPaymentIntent?, error: NSError?) {
        self.confirmPaymentClientSecret = nil
        switch status {
        case .failed:
            confirmPaymentResolver?(Errors.createError(ErrorType.Failed, error))
        case .canceled:
            let statusCode: String
            if paymentIntent?.status == STPPaymentIntentStatus.requiresPaymentMethod {
                statusCode = ErrorType.Failed
            } else {
                statusCode = ErrorType.Canceled
            }
            if let lastPaymentError = paymentIntent?.lastPaymentError {
                confirmPaymentResolver?(Errors.createError(statusCode, lastPaymentError))
            } else {
                confirmPaymentResolver?(Errors.createError(statusCode, "The payment has been canceled"))
            }
        case .succeeded:
            if let paymentIntent = paymentIntent {
                let intent = Mappers.mapFromPaymentIntent(paymentIntent: paymentIntent)
                confirmPaymentResolver?(Mappers.createResult("paymentIntent", intent))
            }
        @unknown default:
            confirmPaymentResolver?(Errors.createError(ErrorType.Unknown, "Cannot complete the payment"))
        }
    }

    struct ConfirmationError: Error, LocalizedError {
        private var errorMessage: String
        init(errorMessage: String) {
            self.errorMessage = errorMessage
        }
        public var errorDescription: String? {
            return errorMessage
        }
    }
}

func findViewControllerPresenter(from uiViewController: UIViewController) -> UIViewController {
    // Note: creating a UIViewController inside here results in a nil window
    // This is a bit of a hack: We traverse the view hierarchy looking for the most reasonable VC to present from.
    // A VC hosted within a SwiftUI cell, for example, doesn't have a parent, so we need to find the UIWindow.
    var presentingViewController: UIViewController =
        uiViewController.view.window?.rootViewController ?? uiViewController

    // Find the most-presented UIViewController
    while let presented = presentingViewController.presentedViewController {
        presentingViewController = presented
    }

    return presentingViewController
}

extension StripeSdkImpl: STPAuthenticationContext {
    public func authenticationPresentingViewController() -> UIViewController {
        return findViewControllerPresenter(from: RCTKeyWindow()?.rootViewController ?? UIViewController())
    }
}

extension STPBankAccountCollectorUserInterfaceStyle {
    init(from params: NSDictionary) {
        guard let styleString = params["style"] as? String else {
            self = .automatic
            return
        }

        switch styleString {
        case "automatic":
            self = .automatic
        case "alwaysLight":
            self = .alwaysLight
        case "alwaysDark":
            self = .alwaysDark
        default:
            self = .automatic
        }
    }
}

extension FinancialConnectionsSheet.Configuration {
    init(from params: NSDictionary) {
        let style: FinancialConnectionsSheet.Configuration.UserInterfaceStyle = {
            guard let styleString = params["style"] as? String else {
                return .automatic
            }

            switch styleString {
            case "automatic":
                return .automatic
            case "alwaysLight":
                return .alwaysLight
            case "alwaysDark":
                return .alwaysDark
            default:
                return .automatic
            }
        }()
        self.init(style: style)
    }
}

private extension STPCardFundingType {
    var displayNameWithBrand: String {
        switch self {
        case .credit: String.Localized.Funding.credit
        case .debit: String.Localized.Funding.debit
        case .prepaid: String.Localized.Funding.prepaid
        case .other: String.Localized.Funding.default
        }
    }

    init(_ typeString: String) {
        self = switch typeString {
        case "debit": .debit
        case "credit": .credit
        case "prepaid": .prepaid
        default: .other
        }
    }
}
