package com.checkoutreactnativecomponents import android.content.Context import com.checkout.components.core.CheckoutComponentsFactory import com.checkout.components.core.logError import com.checkout.components.interfaces.Environment import com.checkout.components.interfaces.api.CheckoutComponents import com.checkout.components.interfaces.api.PaymentMethodComponent import com.checkout.components.interfaces.component.CheckoutComponentConfiguration import com.checkout.components.interfaces.component.ComponentCallback import com.checkout.components.interfaces.component.ComponentOption import com.checkout.components.interfaces.component.PaymentButtonAction import com.checkout.components.interfaces.error.CheckoutError import com.checkout.components.interfaces.localisation.Locale import com.checkout.components.interfaces.localisation.Translations import com.checkout.components.interfaces.model.ApiCallResult import com.checkout.components.interfaces.model.CallbackResult import com.checkout.components.interfaces.model.ComponentName import com.checkout.components.interfaces.model.PaymentMethodName import com.checkout.components.interfaces.model.PaymentSessionResponse import com.checkout.components.interfaces.uicustomisation.designtoken.DesignTokens import com.checkout.components.wallet.wrapper.GooglePayFlowCoordinator import com.checkoutreactnativecomponents.utils.AppearanceBuilder import com.checkoutreactnativecomponents.utils.Constants import com.checkoutreactnativecomponents.utils.Constants.HANDLE_SUBMIT_ID import com.checkoutreactnativecomponents.utils.Constants.TOKENIZED_ID import com.checkoutreactnativecomponents.utils.Event import com.checkoutreactnativecomponents.utils.EventEmitter import com.checkoutreactnativecomponents.utils.LocaleBuilder import com.checkoutreactnativecomponents.utils.ManifestUtils import com.checkoutreactnativecomponents.utils.TranslationsBuilder import com.checkoutreactnativecomponents.utils.hasRegisterCallback import com.checkoutreactnativecomponents.utils.toWritableMap import com.facebook.react.bridge.Arguments import com.facebook.react.bridge.ReactApplicationContext import com.facebook.react.bridge.ReadableMap import com.facebook.react.bridge.WritableMap import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import java.util.concurrent.ConcurrentHashMap public object CheckoutManager { internal var eventEmitter: EventEmitter? = null public var checkoutComponentsFactory: CheckoutComponentsFactory? = null public var checkoutComponents: CheckoutComponents? = null internal var paymentSessionResponse: PaymentSessionResponse? = null internal var publicKey: String = Constants.EMPTY_STRING internal var environment: Environment = Environment.PRODUCTION internal var appearance: DesignTokens? = null internal var locale: Locale? = null internal var translations: Translations? = null internal var callbacks = emptyList() internal val pendingCallbacks = ConcurrentHashMap>() public fun setEventEmitter(context: ReactApplicationContext) { this.eventEmitter = EventEmitter(context) } public fun setPaymentSession(paymentSessionId: String, paymentSessionSecret: String) { if (paymentSessionId.isNotEmpty() && paymentSessionSecret.isNotEmpty()) { paymentSessionResponse = PaymentSessionResponse(paymentSessionId, paymentSessionSecret) } } public fun setPublicKey(value: String) { publicKey = value } public fun setEnvironment(value: String) { val env = value.lowercase() environment = if (env == Constants.SANDBOX) { Environment.SANDBOX } else { Environment.PRODUCTION } } public fun setAppearance(data: ReadableMap) { appearance = AppearanceBuilder.fromReadableMap(data) } public fun setLocale(value: String) { locale = LocaleBuilder.fromString(value) } public fun setTranslations(data: ReadableMap) { translations = TranslationsBuilder.fromReadableMap(data) } public suspend fun initializeSDK(context: Context) { withContext(Dispatchers.Main) { if (publicKey.isEmpty()) { throw Exception("Initialize Error: Missing public key") } try { val configuration = if (paymentSessionResponse !== null) { val flowCoordinators = createFlowCoordinators(context) standardConfiguration(context, flowCoordinators) } else { sessionLessConfiguration(context) } checkoutComponentsFactory = CheckoutComponentsFactory(configuration) checkoutComponents = checkoutComponentsFactory?.create() } catch (checkoutError: CheckoutError) { throw checkoutError } } } internal fun sessionLessConfiguration(context: Context): CheckoutComponentConfiguration { return CheckoutComponentConfiguration( context = context, publicKey = publicKey, environment = environment, locale = locale, translations = translations, appearance = appearance, ) } internal fun standardConfiguration( context: Context, flowCoordinators: Map? = null, ): CheckoutComponentConfiguration { return CheckoutComponentConfiguration( context = context, paymentSession = paymentSessionResponse!!, publicKey = publicKey, environment = environment, locale = locale, translations = translations, appearance = appearance, componentCallback = createCallbacks(), flowCoordinators = flowCoordinators.orEmpty(), ) } internal fun extractCardConfig(config: HashMap): Pair { val showPayButton = config[Constants.SHOW_PAY_BUTTON] as Boolean? ?: true val paymentButtonActionValue = config[Constants.PAYMENT_BUTTON_ACTION] as? String ?: Constants.PAYMENT val paymentButtonAction: PaymentButtonAction = if (paymentButtonActionValue == Constants.TOKENIZE) { PaymentButtonAction.TOKENIZE } else { PaymentButtonAction.PAYMENT } return Pair(showPayButton, paymentButtonAction) } public suspend fun createFlow(config: HashMap): PaymentMethodComponent = withContext(Dispatchers.Main) { checkNotNull(checkoutComponents).run { val (showPayButton, paymentButtonAction) = extractCardConfig(config) val flowComponent = create( ComponentName.Flow, ComponentOption( showPayButton = showPayButton, paymentButtonAction = paymentButtonAction, ), ) if (!flowComponent.isAvailable()) { val error = "${Constants.CHECKOUT_MANAGER}: Failed to create Checkout Flow Component" emitError(error) throw Exception(error) } flowComponent } } public suspend fun createCard(config: HashMap): PaymentMethodComponent = withContext(Dispatchers.Main) { checkNotNull(checkoutComponents).run { val (showPayButton, paymentButtonAction) = extractCardConfig(config) val cardComponent = create( PaymentMethodName.Card, ComponentOption( showPayButton = showPayButton, paymentButtonAction = paymentButtonAction, ), ) if (!cardComponent.isAvailable()) { val error = "${Constants.CHECKOUT_MANAGER}: Failed to create Checkout Card Component" emitError(error) throw Exception(error) } cardComponent } } public suspend fun createGooglePay(config: HashMap): PaymentMethodComponent = withContext(Dispatchers.Main) { checkNotNull(checkoutComponents).run { val googlePayComponent = create(PaymentMethodName.GooglePay, null) if (!googlePayComponent.isAvailable()) { val error = "${Constants.CHECKOUT_MANAGER}: Failed to create Google Pay Component" emitError(error) throw Exception(error) } googlePayComponent } } public fun clear() { paymentSessionResponse = null publicKey = Constants.EMPTY_STRING environment = Environment.PRODUCTION appearance = null locale = null translations = null pendingCallbacks.clear() } public fun emitAndLogError(message: String, error: Throwable) { logError(message, error.message ?: "Empty error message", error.stackTraceToString()) emitError(message) } public fun logError(name: String, message: String, stack: String?) { checkoutComponentsFactory?.logError(message, name, message, stack) } public fun emitError(message: String) { val emitter = requireNotNull(eventEmitter) { "${Constants.CHECKOUT_MANAGER}: EventEmitter is null" } val data: WritableMap = Arguments.createMap() data.putString(Constants.MESSAGE, message) emitter.sendEvent(Event.OnError, data) } private fun createFlowCoordinators(context: Context): Map? { val isWalletEnabled = ManifestUtils.getBooleanMetaData( context = context, keyName = Constants.MANIFEST_WALLET_API, emitAndLogError = ::emitAndLogError ) return if (isWalletEnabled) { val coordinator = GooglePayFlowCoordinator( context = context, handleActivityResult = { resultCode, data -> try { checkoutComponents?.handleActivityResult(resultCode, data) } catch (e: Exception) { emitAndLogError("${Constants.CHECKOUT_MANAGER}: handleActivityResult error", e) } }, ) mapOf(PaymentMethodName.GooglePay to coordinator) } else { null } } internal fun resolveCallback(callbackId: String, result: Any) { pendingCallbacks[callbackId]?.complete(result) pendingCallbacks.remove(callbackId) } internal fun cancelCallback(callbackId: String) { pendingCallbacks[callbackId]?.cancel() pendingCallbacks.remove(callbackId) } @Suppress("unchecked_cast") internal suspend inline fun waitForResolution( callbackId: String, eventName: Event, eventData: WritableMap, defaultValue: T, ): T { val deferred = CompletableDeferred() pendingCallbacks[callbackId]?.let { callback -> callback.cancel() pendingCallbacks.remove(callbackId) } pendingCallbacks[callbackId] = deferred as CompletableDeferred eventData.putString(Constants.CALLBACK_ID, callbackId) eventEmitter?.sendEvent(eventName, eventData) return runCatching { deferred.await() }.onFailure { if (it is CancellationException) { throw it } emitAndLogError(Constants.CHECKOUT_MANAGER, it) }.getOrDefault(defaultValue) } internal fun createCallbacks(): ComponentCallback { val emitter = requireNotNull(eventEmitter) { "${Constants.CHECKOUT_MANAGER}: EventEmitter is null" } return ComponentCallback( onReady = { component -> val data: WritableMap = Arguments.createMap() data.putString(Constants.PAYMENT_METHOD, component.name.toString()) emitter.sendEvent(Event.OnReady, data) }, onChange = { component -> CoroutineScope(context = Dispatchers.Main).launch { val data: WritableMap = Arguments.createMap() data.putString(Constants.PAYMENT_METHOD, component.name.toString()) data.putBoolean(Constants.IS_VALID, component.isValid()) data.putBoolean(Constants.IS_AVAILABLE, component.isAvailable()) emitter.sendEvent(Event.OnChange, data) } }, onSubmit = { component -> val data: WritableMap = Arguments.createMap() data.putString(Constants.PAYMENT_METHOD, component.name.toString()) emitter.sendEvent(Event.OnSubmit, data) }, onTokenized = if (callbacks.hasRegisterCallback(Event.OnTokenized)) { { tokenizationResult -> val tokenizationData: WritableMap = Arguments.createMap() tokenizationData.putString(Constants.PAYMENT_TYPE, tokenizationResult.type) tokenizationData.putMap(Constants.DATA, tokenizationResult.data.toWritableMap()) if (tokenizationResult.cardMetadata !== null) { tokenizationData.putMap( Constants.CARD_METADATA, tokenizationResult.cardMetadata?.toWritableMap(), ) } if (tokenizationResult.preferredScheme !== null) { tokenizationData.putString(Constants.PREFERRED_SCHEME, tokenizationResult.preferredScheme) } val eventData: WritableMap = Arguments.createMap() eventData.putMap(Constants.TOKENIZATION_RESULT, tokenizationData) val result = waitForResolution( callbackId = TOKENIZED_ID, eventName = Event.OnTokenized, eventData = eventData, defaultValue = CallbackResult.Rejected(null), ) result } } else null, onSuccess = { component, paymentID -> val data: WritableMap = Arguments.createMap() data.putString(Constants.PAYMENT_METHOD, component.name.toString()) data.putString(Constants.PAYMENT_ID, paymentID) emitter.sendEvent(Event.OnSuccess, data) }, onError = { _, error -> val data: WritableMap = Arguments.createMap() data.putString(Constants.ERROR_CODE, error.code.toString()) data.putMap(Constants.DETAILS, error.details.toWritableMap()) data.putString(Constants.MESSAGE, error.message) emitter.sendEvent(Event.OnError, data) }, handleSubmit = if (callbacks.hasRegisterCallback(Event.HandleSubmit)) { { sessionData -> val eventData: WritableMap = Arguments.createMap() eventData.putString(Constants.SESSION_DATA, sessionData) val result = waitForResolution( callbackId = HANDLE_SUBMIT_ID, eventName = Event.HandleSubmit, eventData = eventData, defaultValue = ApiCallResult.Failure, ) result } } else null, ) } internal const val NAME = Constants.CHECKOUT_MANAGER }