package com.myfatoorahreactnative.googlepay import android.util.Log import com.facebook.react.bridge.Promise import com.facebook.react.bridge.ReactApplicationContext import com.facebook.react.bridge.ReactContextBaseJavaModule import com.facebook.react.bridge.ReactMethod import com.facebook.react.bridge.ReadableMap import com.facebook.react.module.annotations.ReactModule import com.facebook.react.modules.core.DeviceEventManagerModule import com.google.gson.Gson import com.google.gson.JsonObject import com.myfatoorah.sdk.entity.MFError import com.myfatoorah.sdk.entity.executepayment.MFExecutePaymentRequest import com.myfatoorah.sdk.entity.googlepay.GooglePayRequest import com.myfatoorah.sdk.views.MFResult import com.myfatoorah.sdk.views.embeddedpayment.googlepay.ExecutionMode import com.myfatoorah.sdk.views.embeddedpayment.googlepay.MFGooglePayLauncher import com.myfatoorahreactnative.models.MFGooglePayRequest import com.myfatoorahreactnative.utils.MFGPayConstants import com.myfatoorahreactnative.utils.convertJsonStringToUppercase import com.myfatoorahreactnative.utils.handleReadableMap import kotlin.toString /** * MFGPayModule - Modern Google Pay integration using MFGooglePayLauncher. * * This module replaces the deprecated Google Pay methods in MFModule. * It uses the current MFGooglePayLauncher from the native SDK which provides: * - Better lifecycle handling via Activity Result API * - Cleaner config() API * - Additional methods like isGooglePayAvailable() and updateRequestAmount() * * @see MFModule Deprecated Google Pay methods (SetupGooglePayHelper, etc.) */ @ReactModule(name = MFGPayConstants.MFGPayModuleNAME) class MFGPayModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext) { private val TAG = MFGPayConstants.MFGPayModuleNAME var mfGooglePayLauncher: MFGooglePayLauncher? = null override fun getName(): String = MFGPayConstants.MFGPayModuleNAME override fun invalidate() { super.invalidate() mfGooglePayLauncher = null } /** * Get or initialize the Google Pay Launcher instance. * Uses currentActivity which implements ActivityResultCaller in React Native. */ private fun getLauncher(): MFGooglePayLauncher? { val activity = this.reactApplicationContext.currentActivity if (activity == null) { Log.e(TAG, "Current activity is null") return null } // Use the launcher pre-registered before STARTED by MFGPayInitProvider MFGooglePayLauncherHolder.launcherFor(activity)?.let { mfGooglePayLauncher = it return it } Log.e(TAG, "Google Pay launcher was not pre-registered for this activity") return null } /** * Convert ReadableMap to GooglePayRequest. */ private fun getGooglePayRequest(map: ReadableMap): GooglePayRequest { val mfRequest = handleReadableMap(map, MFGooglePayRequest::class.java) val googlePayRequest = GooglePayRequest( mfRequest.TotalPrice, mfRequest.MerchantId, mfRequest.MerchantName, mfRequest.CountryCode, mfRequest.CurrencyIso ) return googlePayRequest } /** * Send event to JavaScript. */ private fun sendEvent(eventName: String, data: Any?) { reactApplicationContext .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java) .emit(eventName, data) } /** * Resolve promise with JSON data (uppercase keys). */ private fun onSuccess(promise: Promise, response: T?) { Log.d(TAG, "Success: $response") val gson = Gson() val json = gson.toJson(response) val uppercaseJson = convertJsonStringToUppercase(json) promise.resolve(uppercaseJson) } /** * Reject promise with error. */ private fun onError(promise: Promise, error: MFError) { Log.e(TAG, "Error: " + error.message) promise.reject(error.code.toString(), error.message) } // ==================== PUBLIC METHODS ==================== /** * Setup Google Pay with Auto execution mode. * * In this mode, the SDK handles the entire flow automatically: * 1. Opens Google Pay sheet * 2. Updates session with token * 3. Creates invoice * 4. Executes payment * * @param sessionId MyFatoorah session ID * @param googlePayRequest Google Pay configuration * @param promise Resolves with MFGetPaymentStatusResponse on success */ @ReactMethod fun setupWithAutoExecute( sessionId: String, googlePayRequest: ReadableMap, promise: Promise ) { val button = MFGooglePayManager.googlePayButton if (button == null) { onError(promise, MFError("010", "googlePayButton must be initialized")) return } val request = getGooglePayRequest(googlePayRequest) val launcher = getLauncher() ?: run { onError(promise, MFError("016", "Failed to initialize Google Pay Launcher")) return } val executionMode = ExecutionMode.Auto( onInvoiceCreated = { invoiceId -> sendEvent(MFGPayConstants.GPayInvoiceCreatedEventName, invoiceId) }, onExecutePaymentSuccess = { invoiceId, response -> sendEvent(MFGPayConstants.GPayExecutePaymentSuccessEventName, Gson().toJson(response)) } ) launcher.config( googlePayRequest = request, executionMode = executionMode, sessionId = sessionId, onError = { error -> sendEvent(MFGPayConstants.GPayErrorEventName, Gson().toJson(error)) } ) button.button?.let { launcher.setGooglePayButton(it) } val json = JsonObject() json.addProperty("isReady", true) onSuccess(promise, json) } /** * Setup Google Pay with Manual execution mode. * * In this mode, the SDK only updates the session. * You must manually call executePayment() later with the updated session ID. * * @param sessionId MyFatoorah session ID * @param googlePayRequest Google Pay configuration * @param promise Resolves with { sessionId: string } on success */ @ReactMethod fun setupWithManualExecute( sessionId: String, googlePayRequest: ReadableMap, promise: Promise ) { val button = MFGooglePayManager.googlePayButton if (button == null) { onError(promise, MFError("010", "googlePayButton must be initialized")) return } val request = getGooglePayRequest(googlePayRequest) val launcher = getLauncher() ?: run { onError(promise, MFError("016", "Failed to initialize Google Pay Launcher")) return } val executionMode = ExecutionMode.Manual { updatedSessionId -> sendEvent(MFGPayConstants.GPaySessionUpdatedEventName, updatedSessionId) } launcher.config( googlePayRequest = request, executionMode = executionMode, sessionId = sessionId, onError = { error -> sendEvent(MFGPayConstants.GPayErrorEventName, Gson().toJson(error)) } ) button.button?.let { launcher.setGooglePayButton(it) } val json = JsonObject() json.addProperty("isReady", true) onSuccess(promise, json) } /** * Setup Google Pay with TokenOnly mode. * * In this mode, only the Google Pay token is retrieved. * No session update or payment execution occurs. * Useful for backend processing or recurring payments. * * @param googlePayRequest Google Pay configuration * @param promise Resolves with token string on success */ @ReactMethod fun setupTokenOnly( googlePayRequest: ReadableMap, promise: Promise ) { val button = MFGooglePayManager.googlePayButton if (button == null) { onError(promise, MFError("010", "googlePayButton must be initialized")) return } val request = getGooglePayRequest(googlePayRequest) val launcher = getLauncher() ?: run { onError(promise, MFError("016", "Failed to initialize Google Pay Launcher")) return } val executionMode = ExecutionMode.TokenOnly { token -> sendEvent(MFGPayConstants.GPayReceivedTokenEventName, token) } launcher.config( googlePayRequest = request, executionMode = executionMode, sessionId = null, onError = { error -> sendEvent(MFGPayConstants.GPayErrorEventName, Gson().toJson(error)) } ) button.button?.let { launcher.setGooglePayButton(it) } val json = JsonObject() json.addProperty("isReady", true) onSuccess(promise, json) } //#endregion /** * Check if Google Pay is available on the current device. * * This is a NEW feature not available in the deprecated MFModule methods. * * @param promise Resolves with boolean indicating availability */ @ReactMethod fun isGooglePayAvailable(promise: Promise) { val launcher = getLauncher() ?: run { onError(promise, MFError("016", "Failed to initialize Google Pay Launcher")) return } launcher.isGooglePayAvailable { isAvailable -> promise.resolve(isAvailable) } } /** * Update the payment amount dynamically. * * This is useful when the final amount is determined at runtime * (e.g., after shipping selection or discount application). * * This is a NEW feature not available in the deprecated MFModule methods. * * @param amount New total price as string (e.g., "10.500") * @param promise Resolves with { isUpdated: true } on success */ @ReactMethod fun updateRequestAmount(amount: String, promise: Promise) { val launcher = getLauncher() ?: run { onError(promise, MFError("017", "Google Pay Launcher not configured")) return } launcher.updateRequestAmount(amount) val json = JsonObject() json.addProperty("isUpdated", true) onSuccess(promise, json) } /** * Open Google Pay sheet programmatically. * * Use this when you have a custom UI trigger instead of the standard button. * Must call setup method first. * * @param promise Resolves with { isInvoked: true } on success */ @ReactMethod fun openSheet(promise: Promise) { val launcher = getLauncher() ?: run { onError(promise, MFError("017", "Google Pay Launcher not configured")) return } launcher.openGooglePaySheet() val json = JsonObject() json.addProperty("isInvoked", true) onSuccess(promise, json) } /** * Execute payment with Google Pay (for Manual mode). * * Call this after setupWithManualExecute() when you receive the updated session ID. * * @param executePaymentRequest Payment execution request * @param lang API language * @param promise Resolves with MFGetPaymentStatusResponse on success */ @ReactMethod fun executePayment( executePaymentRequest: ReadableMap, lang: String, promise: Promise ) { val launcher = getLauncher() ?: run { onError(promise, MFError("017", "Google Pay Launcher not configured")) return } val request = handleReadableMap( executePaymentRequest, MFExecutePaymentRequest::class.java ) launcher.executePayment( activity = this.reactApplicationContext.currentActivity!!, request = request, apiLang = lang, onInvoiceCreated = { invoiceId -> sendEvent(MFGPayConstants.GPayInvoiceCreatedEventName, invoiceId) } ) { _, result -> when (result) { is MFResult.Success -> { onSuccess(promise, result.response) } is MFResult.Fail -> { onError(promise, result.error) } else -> {} } } } // Required for EventEmitter support @ReactMethod fun addListener(eventName: String) { // Required: Deprecated in React Native 0.65+ } @ReactMethod fun removeListeners(count: Int) { // Required: Deprecated in React Native 0.65+ } }