// Copyright © 2022 Olo Inc. All rights reserved. // This software is made available under the Olo Pay SDK License (See LICENSE.md file) package com.olopaysdkreactnative import android.util.Log import android.view.View import androidx.appcompat.app.AppCompatActivity import com.facebook.react.bridge.Arguments import com.facebook.react.bridge.ReactApplicationContext import com.facebook.react.bridge.ReactContextBaseJavaModule import com.facebook.react.bridge.ReactMethod import com.facebook.react.bridge.Promise import com.facebook.react.bridge.ReadableMap import com.facebook.react.modules.core.DeviceEventManagerModule import com.facebook.react.uimanager.UIManagerModule import com.olo.olopay.api.IOloPayApiInitializer import com.olo.olopay.api.OloPayApiInitializer import com.olo.olopay.data.* import com.olo.olopay.googlepay.* import com.olopaysdkreactnative.paymentcarddetailsview.PaymentCardDetailsView import com.olopaysdkreactnative.data.DataKeys import com.olopaysdkreactnative.data.ErrorCodes import com.olopaysdkreactnative.extensions.* import com.olopaysdkreactnative.googlepay.GooglePayFragment import com.olopaysdkreactnative.googlepay.ReactNativeGooglePayResultCallback import com.olopaysdkreactnative.helpers.backgroundOperation import com.olopaysdkreactnative.helpers.uiOperation import com.olopaysdkreactnative.paymentcardcvvview.PaymentCardCvvView import com.olopaysdkreactnative.paymentcarddetailsform.PaymentCardDetailsForm import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.sync.Semaphore import kotlinx.coroutines.sync.withPermit class OlopaysdkReactNativeModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext) { private var _sdkInitializingSemaphore = Semaphore(1) private var _googlePaySemaphore = Semaphore(1) private var _sdkInitializedSemaphore = Semaphore(1) private var _googlePayReadySemaphore = Semaphore(1) private var _googlePayInitializedSemaphore = Semaphore(1) private var _initializeInternalCalled = false //WARNING: DO NOT ACCESS OR MODIFY THESE DIRECTLY... USE THREAD-SAFE GETTERS/SETTERS private var _sdkInitialized = false private var _googlePayReady = false override fun getName(): String { return "OlopaysdkReactNative" } @ReactMethod fun initializeOloPay(args: ReadableMap, promise: Promise) = backgroundOperation { _sdkInitializingSemaphore.withPermit { setSdkInitialized(false) val productionEnv = args.getBoolean(DataKeys.ProductionEnvironmentKey, DefaultProductionEnvironment) val params = SetupParameters( if (productionEnv) OloPayEnvironment.Production else OloPayEnvironment.Test, IOloPayApiInitializer.googlePayConfig ) val initializer = OloPayApiInitializer() initializer.setup(reactApplicationContext, params) setSdkInitialized(true) promise.resolve(null) } } @ReactMethod fun initializeMetadata(args: ReadableMap, promise: Promise) { // First call happens while the plugin is getting initialized... Subsequent calls should be ignored if (!_initializeInternalCalled) { _initializeInternalCalled = true val hybridVersion = args.getString(DataKeys.HybridSdkVersionKey, DefaultVersionString)!!.trim() val hybridBuildType = args.getString(DataKeys.HybridBuildTypeKey, DataKeys.HybridBuildTypeInternalValue)!!.trim() setSdkWrapperInfo(hybridVersion, hybridBuildType) } promise.resolve(null) } @ReactMethod fun initializeGooglePay(args: ReadableMap, promise: Promise) = googlePayLockingOperation { val baseError = "Unable to initialize Google Pay" if (!isSdkInitialized()) { promise.reject(ErrorCodes.UninitializedSdk, "${baseError}: Olo Pay SDK has not been initialized") return@googlePayLockingOperation } val countryCode = args.getString(DataKeys.GPayCountryCodeKey) if (countryCode.isNullOrEmpty()) { promise.reject(ErrorCodes.MissingParameter, "${baseError}: Missing parameter ${DataKeys.GPayCountryCodeKey}") return@googlePayLockingOperation } val merchantName = args.getString(DataKeys.GPayMerchantNameKey) if (merchantName.isNullOrEmpty()) { promise.reject(ErrorCodes.MissingParameter, "${baseError}: Missing parameter ${DataKeys.GPayMerchantNameKey}") return@googlePayLockingOperation } // Clean up in case digital wallets have been initialized multiple times initializeGooglePay(null) removeGooglePayFragment() val productionEnvironment = if (args.getBoolean(DataKeys.GPayProductionEnvironmentKey, DefaultGooglePayProductionEnvironment)) { Environment.Production } else { Environment.Test } val addressFormat = if (args.getBoolean(DataKeys.GPayFullAddressFormatKey, DefaultFullAddressFormat)) { Config.AddressFormat.Full } else { Config.AddressFormat.Min } val googlePayConfig = Config( productionEnvironment, merchantName, countryCode, args.getBoolean(DataKeys.GPayExistingPaymentMethodRequiredKey, DefaultExistingPaymentMethodRequired), args.getBoolean(DataKeys.GPayEmailRequiredKey, DefaultEmailRequired), args.getBoolean(DataKeys.GPayPhoneNumberRequiredKey, DefaultPhoneNumberRequired), addressFormat ) initializeGooglePay(googlePayConfig) changeDigitalWalletCountry(countryCode, merchantName) promise.resolve(null) } @ReactMethod fun changeGooglePayVendor(args: ReadableMap, promise: Promise) = googlePayLockingOperation { val baseError = "Unable to change Google Pay Country" if (!isSdkInitialized()) { promise.reject(ErrorCodes.UninitializedSdk, "${baseError}: Olo Pay SDK has not been initialized") return@googlePayLockingOperation } if (!isGooglePayInitialized()) { promise.reject(ErrorCodes.GooglePayUninitialized, "${baseError}: Google Pay not initialized") return@googlePayLockingOperation } val countryCode = args.getString(DataKeys.GPayCountryCodeKey) if (countryCode.isNullOrEmpty()) { promise.reject(ErrorCodes.MissingParameter, "${baseError}: Missing parameter ${DataKeys.GPayCountryCodeKey}") return@googlePayLockingOperation } val merchantName = args.getString(DataKeys.GPayMerchantNameKey) if (merchantName.isNullOrEmpty()) { promise.reject(ErrorCodes.MissingParameter, "${baseError}: Missing parameter ${DataKeys.GPayMerchantNameKey}") return@googlePayLockingOperation } changeDigitalWalletCountry(countryCode, merchantName) promise.resolve(null) } @ReactMethod fun getDigitalWalletPaymentMethod(args: ReadableMap, promise: Promise) = uiOperation { // Unable to use googlePayLockingOperation because this call waits for a callback method... we need to unlock // the semaphore manually val baseError = "Unable to create payment method" if (!isSdkInitialized()) { promise.reject(ErrorCodes.UninitializedSdk, "${baseError}: Olo Pay SDK has not been initialized") return@uiOperation } if (!isGooglePayReady()) { promise.reject(ErrorCodes.GooglePayNotReady, "${baseError}: Google Pay isn't ready yet.") return@uiOperation } val amount = args.getNullableDouble(DataKeys.GPayAmountKey) if (amount == null) { promise.reject(ErrorCodes.MissingParameter, "${baseError}: Missing parameter ${DataKeys.GPayAmountKey}") return@uiOperation } else if (amount < 0){ promise.reject(ErrorCodes.InvalidParameter, "${baseError}: ${DataKeys.GPayAmountKey} cannot be negative") } _googlePaySemaphore.acquire() val fragment = getGooglePayFragment() if (fragment == null) { promise.reject(ErrorCodes.GooglePayUninitialized, "${baseError}: Google Pay not initialized") _googlePaySemaphore.safeRelease() return@uiOperation } if (!fragment.isReady) { val reason = if (fragment.countryCode.isNullOrEmpty()) { "Google Pay not initialized - country code not set" } else if (fragment.merchantName.isNullOrEmpty()) { "Google Pay not initialized - merchant name not set" } else { "Google Pay isn't ready yet" } promise.reject(ErrorCodes.GooglePayNotReady, "${baseError}: $reason") _googlePaySemaphore.safeRelease() return@uiOperation } val currencyCode = args.getString(DataKeys.GPayCurrencyCodeKey, DefaultCurrencyCode) val currencyMultiplier = args.getInt(DataKeys.GPayCurrencyMultiplierKey, DefaultCurrencyMultiplier) val amountInSmallestCurrencyUnit = (amount * currencyMultiplier).toInt() fragment.resultCallback = ReactNativeGooglePayResultCallback{ result, promise -> onGooglePayResult(result, promise) } fragment.present(currencyCode, amountInSmallestCurrencyUnit, promise) } @ReactMethod fun isDigitalWalletReady(promise: Promise) = backgroundOperation { val data = Arguments.createMap() data.putBoolean(DataKeys.GPayIsReadyKey, isGooglePayReady()) promise.resolve(data) } @ReactMethod fun isOloPayInitialized(promise: Promise) = backgroundOperation { val data = Arguments.createMap() data.putBoolean(DataKeys.IsInitializedKey, isSdkInitialized()) promise.resolve(data) } @ReactMethod fun isDigitalWalletInitialized(promise: Promise) = backgroundOperation { val data = Arguments.createMap() data.putBoolean(DataKeys.IsInitializedKey, isGooglePayInitialized()) promise.resolve(data) } @ReactMethod fun paymentCardDetailsViewCreatePaymentMethod(reactTag: Int, promise: Promise) = uiOperation { findViewOrReject(reactTag, promise)?.createPaymentMethod(promise) } @ReactMethod fun paymentCardDetailsViewFocus(reactTag: Int, promise: Promise) = uiOperation { findViewOrReject(reactTag, promise)?.requestFocusFromJS(CardField.CardNumber, true) } @ReactMethod fun paymentCardDetailsViewBlur(reactTag: Int, promise: Promise) = uiOperation { findViewOrReject(reactTag, promise)?.clearFocusFromJS() } @ReactMethod fun paymentCardDetailsViewClear(reactTag: Int, promise: Promise) = uiOperation { findViewOrReject(reactTag, promise)?.clearCardDetails() } @ReactMethod fun paymentCardDetailsFormCreatePaymentMethod(reactTag: Int, promise: Promise) = uiOperation { findViewOrReject(reactTag, promise)?.createPaymentMethod(promise) } @ReactMethod fun paymentCardDetailsFormFocus(reactTag: Int, promise: Promise) = uiOperation { findViewOrReject(reactTag, promise)?.requestFocusFromJS(CardField.CardNumber, true) } @ReactMethod fun paymentCardDetailsFormBlur(reactTag: Int, promise: Promise) = uiOperation { findViewOrReject(reactTag, promise)?.clearFocusFromJS() } @ReactMethod fun paymentCardDetailsFormClear(reactTag: Int, promise: Promise) = uiOperation { findViewOrReject(reactTag, promise)?.clearCardDetails() } @ReactMethod fun paymentCardCvvViewCreateCvvUpdateToken(reactTag: Int, promise: Promise) = uiOperation { findViewOrReject(reactTag, promise)?.createCvvUpdateToken(promise) } @ReactMethod fun paymentCardCvvViewFocus(reactTag: Int, promise: Promise) = uiOperation { findViewOrReject(reactTag, promise)?.requestFocusFromJS(true) } @ReactMethod fun paymentCardCvvViewBlur(reactTag: Int, promise: Promise) = uiOperation { findViewOrReject(reactTag, promise)?.clearFocusFromJS() } @ReactMethod fun paymentCardCvvViewClear(reactTag: Int, promise: Promise) = uiOperation { findViewOrReject(reactTag, promise)?.clearCvvDetails() } private fun findViewOrReject(reactTag: Int, promise: Promise): T? { val view = try { val managerModule = reactApplicationContext.getNativeModule(UIManagerModule::class.java) managerModule?.resolveView(reactTag) as? T } catch (e: Exception) { null } if (view == null) { promise.reject(ErrorCodes.ViewNotFound, "Native view not found") } return view } private suspend fun isSdkInitialized(): Boolean { _sdkInitializedSemaphore.withPermit { return _sdkInitialized } } private suspend fun setSdkInitialized(initialized: Boolean) { _sdkInitializedSemaphore.withPermit { _sdkInitialized = initialized } } private suspend fun isGooglePayInitialized(): Boolean { _googlePayInitializedSemaphore.withPermit { return IOloPayApiInitializer.googlePayConfig != null } } private suspend fun initializeGooglePay(config: Config?) { _googlePayInitializedSemaphore.withPermit { IOloPayApiInitializer.googlePayConfig = config } } private suspend fun isGooglePayReady() : Boolean { _googlePayReadySemaphore.withPermit { return _googlePayReady } } private suspend fun setGooglePayReady(ready: Boolean) { _googlePayReadySemaphore.withPermit { _googlePayReady = ready } } private suspend fun changeDigitalWalletCountry(countryCode: String, merchantName: String) { getGooglePayFragment(countryCode, merchantName, true) } private suspend fun getGooglePayFragment(countryCode: String? = null, merchantName: String? = null, createIfNeeded: Boolean = false): GooglePayFragment? { if (!isGooglePayInitialized()) { return null } if (activity == null) { return null } val fragmentManager = activity!!.supportFragmentManager var fragment = fragmentManager.findFragmentByTag(GooglePayFragment.Tag) as GooglePayFragment? //If the fragment isn't null, determine if we need a new instance var forceCreation = false if (fragment != null) { val invalidCountryCode = !countryCode.isNullOrEmpty() && fragment.countryCode != countryCode val invalidMerchantName = !merchantName.isNullOrEmpty() && fragment.merchantName != merchantName if (invalidCountryCode || invalidMerchantName) { removeGooglePayFragment(fragment) fragment = null forceCreation = true } } if (fragment == null && (createIfNeeded || forceCreation)) { fragment = GooglePayFragment() fragment.countryCode = countryCode fragment.merchantName = merchantName fragmentManager.beginTransaction().add(fragment, GooglePayFragment.Tag).commitAllowingStateLoss() } fragment?.also { it.readyCallback = ReadyCallback { isReady -> onGooglePayReady(isReady) } } return fragment } private suspend fun removeGooglePayFragment() { getGooglePayFragment(createIfNeeded = false)?.let { it -> removeGooglePayFragment(it) } } private fun removeGooglePayFragment(fragment: GooglePayFragment) { activity?.supportFragmentManager?.beginTransaction()?.remove(fragment)?.commitAllowingStateLoss() emitDigitalWalletReadyEvent(false) } private fun onGooglePayReady(isReady: Boolean) { emitDigitalWalletReadyEvent(isReady) } private fun onGooglePayResult(result: Result, promise: Promise) { _googlePaySemaphore.safeRelease() when (result) { is Result.Completed -> { val data = Arguments.createMap() data.putMap(DataKeys.PaymentMethodKey, result.paymentMethod.toMap()) promise.resolve(data) } is Result.Canceled -> { promise.resolve(null) } is Result.Failed -> { val data = Arguments.createMap() data.putMap(DataKeys.PaymentMethodErrorKey, result.error.toMap()) promise.resolve(data) } } } private fun emitDigitalWalletReadyEvent(isReady: Boolean) = backgroundOperation { if (isGooglePayReady() == isReady) { return@backgroundOperation } setGooglePayReady(isReady) val module = reactApplicationContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java) if (module == null) { Log.d("OloPaySDK", "Unable to emit ${DataKeys.DigitalWalletReadyEvent}. RCTDeviceEventEmitter JS Module instance is null") return@backgroundOperation } val data = Arguments.createMap() data.putBoolean(DataKeys.GPayIsReadyKey, isReady) module.emit(DataKeys.DigitalWalletReadyEvent, data) } private fun googlePayLockingOperation(operation: suspend() -> Unit) { CoroutineScope(Dispatchers.Main).launch { _googlePaySemaphore.withPermit { operation() } } } private fun setSdkWrapperInfo(version: String, buildType: String) { val versionStrings = version.split(".") val wrapperInfo = SdkWrapperInfo( (versionStrings.getOrElse(MajorVersionIndex) { "0" }).toIntOrNull() ?: 0, (versionStrings.getOrElse(MinorVersionIndex) { "0" }).toIntOrNull() ?: 0, (versionStrings.getOrElse(BuildVersionIndex) { "0" }).toIntOrNull() ?: 0, when (buildType) { DataKeys.HybridBuildTypePublicValue -> SdkBuildType.Public else -> SdkBuildType.Internal }, SdkWrapperPlatform.ReactNative ) IOloPayApiInitializer.sdkWrapperInfo = wrapperInfo } private val activity: AppCompatActivity? get() = currentActivity as AppCompatActivity? companion object { // Default Initialization Options const val DefaultProductionEnvironment = true // Default Google Pay Initialization Options const val DefaultGooglePayProductionEnvironment = true const val DefaultExistingPaymentMethodRequired = true const val DefaultEmailRequired = false const val DefaultPhoneNumberRequired = false const val DefaultFullAddressFormat = false // Default Digital Wallet Payment Method Request Options const val DefaultCurrencyMultiplier = 100 const val DefaultCurrencyCode = "USD" const val MajorVersionIndex = 0 const val MinorVersionIndex = 1 const val BuildVersionIndex = 2 const val DefaultVersionString = "0.0.0" } }