// Copyright © 2022 Olo Inc. All rights reserved. // This software is made available under the Olo Pay SDK License (See LICENSE.md file) package com.olo.olopaysdk.capacitorplugin import com.getcapacitor.annotation.CapacitorPlugin import com.getcapacitor.PluginMethod import com.getcapacitor.PluginCall import com.getcapacitor.JSObject import com.getcapacitor.Plugin import com.olo.olopay.api.IOloPayApiInitializer import com.olo.olopay.api.OloPayApiInitializer import com.olo.olopay.data.OloPayEnvironment import com.olo.olopay.data.SdkBuildType import com.olo.olopay.data.SdkWrapperInfo import com.olo.olopay.data.SdkWrapperPlatform import com.olo.olopay.data.SetupParameters import com.olo.olopay.googlepay.Config import com.olo.olopay.googlepay.Environment import com.olo.olopay.googlepay.ReadyCallback import com.olo.olopay.googlepay.Result import com.olo.olopaysdk.capacitorplugin.data.DataKeys import com.olo.olopaysdk.capacitorplugin.data.safeRelease import com.olo.olopaysdk.capacitorplugin.data.toJSObject import com.olo.olopaysdk.capacitorplugin.fragments.CapacitorGooglePayResultCallback import com.olo.olopaysdk.capacitorplugin.fragments.GooglePayFragment import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.sync.Semaphore import kotlinx.coroutines.sync.withPermit @CapacitorPlugin(name = "OloPaySDK") class OloPaySDKPlugin : Plugin() { 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 @PluginMethod fun initializeInternal(call: PluginCall) { // First call happens while the plugin is getting initialized... Subsequent calls should be ignored if (!_initializeInternalCalled) { _initializeInternalCalled = true val hybridVersion = call.getString(DataKeys.HybridSdkVersionKey, DefaultVersionString)!!.trim() val hybridBuildType = call.getString(DataKeys.HybridBuildTypeKey, DataKeys.HybridBuildTypeInternalValue)!!.trim() setSdkWrapperInfo(hybridVersion, hybridBuildType) } call.resolve() } @PluginMethod fun initialize(call: PluginCall) = backgroundOperation { _sdkInitializingSemaphore.withPermit { setSdkInitialized(false) val productionEnv = call.getBoolean(DataKeys.ProductionEnvironmentKey, DefaultProductionEnvironment)!! val params = SetupParameters( if (productionEnv) OloPayEnvironment.Production else OloPayEnvironment.Test, IOloPayApiInitializer.googlePayConfig ) val initializer = OloPayApiInitializer() initializer.setup(activity.applicationContext, params) setSdkInitialized(true) call.resolve() } } @PluginMethod fun initializeGooglePay(call: PluginCall) = googlePayLockingOperation { val baseError = "Unable to initialize Google Pay" if (!isSdkInitialized()) { call.reject("${baseError}: Olo Pay SDK has not been initialized", DataKeys.UninitializedSdkErrorCode) return@googlePayLockingOperation } val countryCode = call.getString(DataKeys.GPayCountryCodeKey) if (countryCode.isNullOrEmpty()) { call.reject("${baseError}: Missing parameter ${DataKeys.GPayCountryCodeKey}", DataKeys.MissingParameterErrorCode) return@googlePayLockingOperation } val merchantName = call.getString(DataKeys.GPayMerchantNameKey) if (merchantName.isNullOrEmpty()) { call.reject("${baseError}: Missing parameter ${DataKeys.GPayMerchantNameKey}", DataKeys.MissingParameterErrorCode) return@googlePayLockingOperation } // Clean up in case digital wallets have been initialized multiple times initializeGooglePay(null) removeGooglePayFragment() val productionEnvironment = if (call.getBoolean(DataKeys.GPayProductionEnvironmentKey, DefaultGooglePayProductionEnvironment)!!) { Environment.Production } else { Environment.Test } val addressFormat = if (call.getBoolean(DataKeys.GPayFullAddressFormatKey, DefaultFullAddressFormat)!!) { Config.AddressFormat.Full } else { Config.AddressFormat.Min } val googlePayConfig = Config( productionEnvironment, merchantName, countryCode, call.getBoolean(DataKeys.GPayExistingPaymentMethodRequiredKey, DefaultExistingPaymentMethodRequired)!!, call.getBoolean(DataKeys.GPayEmailRequiredKey, DefaultEmailRequired)!!, call.getBoolean(DataKeys.GPayPhoneNumberRequiredKey, DefaultPhoneNumberRequired)!!, addressFormat ) initializeGooglePay(googlePayConfig) changeDigitalWalletCountry(countryCode, merchantName) call.resolve() } @PluginMethod fun changeGooglePayVendor(call: PluginCall) = googlePayLockingOperation { val baseError = "Unable to change Google Pay Country" if (!isSdkInitialized()) { call.reject("${baseError}: Olo Pay SDK has not been initialized", DataKeys.UninitializedSdkErrorCode) return@googlePayLockingOperation } val countryCode = call.getString(DataKeys.GPayCountryCodeKey) if (countryCode.isNullOrEmpty()) { call.reject("${baseError}: Missing parameter ${DataKeys.GPayCountryCodeKey}", DataKeys.MissingParameterErrorCode) return@googlePayLockingOperation } val merchantName = call.getString(DataKeys.GPayMerchantNameKey) if (merchantName.isNullOrEmpty()) { call.reject("${baseError}: Missing parameter ${DataKeys.GPayMerchantNameKey}", DataKeys.MissingParameterErrorCode) return@googlePayLockingOperation } if (getGooglePayFragment() == null) { call.reject("${baseError}: Google Pay not initialized", DataKeys.GooglePayUninitialized) return@googlePayLockingOperation } changeDigitalWalletCountry(countryCode, merchantName) call.resolve() } @PluginMethod fun getDigitalWalletPaymentMethod(call: PluginCall) = 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()) { call.reject("${baseError}: Olo Pay SDK has not been initialized", DataKeys.UninitializedSdkErrorCode) return@uiOperation } if (!isGooglePayReady()) { call.reject("${baseError}: Google Pay isn't ready yet.", DataKeys.GooglePayNotReady) return@uiOperation } val amount = call.getDouble(DataKeys.GPayAmountKey) if (amount == null) { call.reject("${baseError}: Missing parameter ${DataKeys.GPayAmountKey}", DataKeys.MissingParameterErrorCode) return@uiOperation } else if (amount < 0) { call.reject("${baseError}: ${DataKeys.GPayAmountKey} cannot be negative", DataKeys.InvalidParameterErrorCode) return@uiOperation } _googlePaySemaphore.acquire() val fragment = getGooglePayFragment() if (fragment == null) { call.reject("${baseError}: Google Pay not initialized", DataKeys.GooglePayUninitialized) _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" } call.reject("${baseError}: $reason", DataKeys.GooglePayNotReady) _googlePaySemaphore.safeRelease() return@uiOperation } val currencyCode = call.getString(DataKeys.GPayCurrencyCodeKey, DefaultCurrencyCode)!! val currencyMultiplier = call.getInt(DataKeys.GPayCurrencyMultiplierKey, DefaultCurrencyMultiplier)!! val amountInSmallestCurrencyUnit = (amount * currencyMultiplier).toInt() fragment.resultCallback = CapacitorGooglePayResultCallback{ result, call -> onGooglePayResult(result, call) } fragment.present(currencyCode, amountInSmallestCurrencyUnit, call) } @PluginMethod fun isDigitalWalletReady(call: PluginCall) = backgroundOperation { val data = JSObject() data.put(DataKeys.GPayIsReadyKey, isGooglePayReady()) call.resolve(data) } @PluginMethod fun isInitialized(call: PluginCall) = backgroundOperation { val data = JSObject() data.put(DataKeys.IsInitializedKey, isSdkInitialized()) call.resolve(data) } @PluginMethod fun isDigitalWalletInitialized(call: PluginCall) = backgroundOperation { val data = JSObject() data.put(DataKeys.IsInitializedKey, isGooglePayInitialized()) call.resolve(data) } 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 } 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).commit() } 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, call: PluginCall) { _googlePaySemaphore.safeRelease() when (result) { is Result.Completed -> { val data = JSObject() data.put(DataKeys.PaymentMethodKey, result.paymentMethod.toJSObject()) call.resolve(data) } is Result.Canceled -> { call.resolve() } is Result.Failed -> { val data = JSObject() data.put(DataKeys.PaymentMethodErrorKey, result.error.toJSObject()) call.resolve(data) } } } private fun emitDigitalWalletReadyEvent(isReady: Boolean) = backgroundOperation { if (isGooglePayReady() == isReady) { return@backgroundOperation } setGooglePayReady(isReady) val data = JSObject() data.put(DataKeys.GPayIsReadyKey, isReady) notifyListeners(DataKeys.DigitalWalletReadyEvent, data) } private fun googlePayLockingOperation(operation: suspend() -> Unit) { CoroutineScope(Dispatchers.Main).launch { _googlePaySemaphore.withPermit { operation() } } } private fun backgroundOperation(operation: suspend() -> Unit) { CoroutineScope(Dispatchers.IO).launch { operation() } } private fun uiOperation(operation: suspend() -> Unit) { CoroutineScope(Dispatchers.Main).launch { 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.Capacitor ) IOloPayApiInitializer.sdkWrapperInfo = wrapperInfo } 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" } }