package com.myfatoorahreactnative.cardviewv3 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.ReadableArray import com.facebook.react.bridge.ReadableMap import com.facebook.react.bridge.ReadableType import com.facebook.react.module.annotations.ReactModule import com.google.gson.Gson import com.google.gson.JsonObject import com.myfatoorah.sdk.v3.entity.session.MFSessionData import com.myfatoorah.sdk.v3.views.MFPaymentModeV3 import com.myfatoorah.sdk.v3.views.MFSDKV3 import com.myfatoorah.sdk.views.MFResult import com.myfatoorahreactnative.models.MFV3CreateSessionRequest import com.myfatoorahreactnative.models.MFV3CreateVerifySessionRequest import com.myfatoorahreactnative.utils.MFCardV3Constants import com.myfatoorahreactnative.utils.handleReadableMap /** * MFCardV3Module - NativeModule for the V3 embedded card view. * * Exposes promise methods that create sessions (SDK path) and drive a specific * `MFCardV3View` resolved by its JS-assigned `cardId`. Resize / widget / error * events are emitted by the view itself via RCTDeviceEventEmitter, not by this module. * * Requires `MFSDK.init(...)` to have already run - the V3 repo reuses the shared * `Const.apiKey` + `ConfigManager` set up by that init. * * @see MFCardV3View Holds the actual SDK card view, keyed by cardId. */ @ReactModule(name = MFCardV3Constants.MFCardV3ModuleNAME) class MFCardV3Module(reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext) { private val TAG = MFCardV3Constants.MFCardV3ModuleNAME override fun getName(): String = MFCardV3Constants.MFCardV3ModuleNAME //#region Session creation /** * Create a PAY / COLLECT_DETAILS session via the SDK path. * * @param request ReadableMap {Amount:number, Currency?:string, Mode?:string, Require3DS?:bool, SaveCard?:bool, CustomerReference?:string} * @param promise Resolves with the session JSON string on success. */ @ReactMethod fun create(request: ReadableMap, promise: Promise) { val req = handleReadableMap(request, MFV3CreateSessionRequest::class.java) val amount = req.Amount if (amount == null) { Log.e(TAG, "create: Amount is required") promise.reject("invalid_request", "Amount is required") return } MFSDKV3.createSession( amount = amount, currency = req.Currency, mode = req.Mode ?: MFPaymentModeV3.COMPLETE_PAYMENT, require3DS = req.Require3DS ?: true, saveCard = req.SaveCard ?: false, customerReference = req.CustomerReference ) { result -> resolveSession(result, promise) } } /** * Create a VERIFY session via the SDK path (no charge). * * @param request ReadableMap {CustomerReference:string, Currency?:string, Require3DS?:bool} * @param promise Resolves with the session JSON string on success. */ @ReactMethod fun createVerify(request: ReadableMap, promise: Promise) { val req = handleReadableMap(request, MFV3CreateVerifySessionRequest::class.java) val customerReference = req.CustomerReference if (customerReference == null) { Log.e(TAG, "createVerify: CustomerReference is required") promise.reject("invalid_request", "CustomerReference is required for verify") return } MFSDKV3.createVerifySession( customerReference = customerReference, currency = req.Currency, require3DS = req.Require3DS ?: true ) { result -> resolveSession(result, promise) } } //#endregion //#region View-bound methods (resolved by cardId) /** * Load a session into the mounted card view. Must be called before submit/verify. * * @param cardId JS-assigned id of the target MFCardViewV3. * @param session Session ReadableMap, as returned by create()/createVerify() or built via fromBackend(). * @param promise Resolves with null once load() has been dispatched. */ @ReactMethod fun load(cardId: String, session: ReadableMap, promise: Promise) { val sessionData = try { Gson().fromJson(readableMapToJson(session), MFSessionData::class.java) } catch (e: Exception) { Log.e(TAG, "load: could not parse session", e) promise.reject("invalid_session", "Could not parse session") return } val view = MFCardV3View.viewFor(cardId) if (view == null) { Log.e(TAG, "load: MFCardViewV3 not mounted for id $cardId") promise.reject("view_not_found", "MFCardViewV3 not mounted for id $cardId", null) return } view.post { view.loadSession(sessionData) promise.resolve(null) } } /** * Submit the entered card (pay / collect, per the session mode). * * @param cardId JS-assigned id of the target MFCardViewV3. * @param currency ISO currency code used to charge the card. * @param promise Resolves with the payment-result JSON string on success. */ @ReactMethod fun submit(cardId: String, currency: String, promise: Promise) { val view = MFCardV3View.viewFor(cardId) if (view == null) { Log.e(TAG, "submit: MFCardViewV3 not mounted for id $cardId") promise.reject("view_not_found", "MFCardViewV3 not mounted for id $cardId", null) return } view.post { if (!view.ensureActivityBackedCardView()) { promise.reject("no_activity_context", "MFCardViewV3 could not obtain a host Activity; 3DS redirects cannot be started.") return@post } view.cardView.submit(currency) { result -> when (result) { is MFResult.Success -> { Log.d(TAG, "submit: success") promise.resolve(MFV3Serializers.paymentResult(result.response)) } is MFResult.Fail -> { Log.e(TAG, "submit: " + result.error.message) promise.reject(result.error.code.toString(), result.error.message) } else -> { Log.e(TAG, "submit: unhandled MFResult variant: $result") promise.reject("unknown_result", "Unhandled MFResult variant: $result") } } } } } /** * Verify the entered card (VERIFY session, no charge). * * @param cardId JS-assigned id of the target MFCardViewV3. * @param promise Resolves with the verify-result JSON string on success. */ @ReactMethod fun verify(cardId: String, promise: Promise) { val view = MFCardV3View.viewFor(cardId) if (view == null) { Log.e(TAG, "verify: MFCardViewV3 not mounted for id $cardId") promise.reject("view_not_found", "MFCardViewV3 not mounted for id $cardId", null) return } view.post { if (!view.ensureActivityBackedCardView()) { promise.reject("no_activity_context", "MFCardViewV3 could not obtain a host Activity; VERIFY redirects cannot be started.") return@post } view.cardView.verify { result -> when (result) { is MFResult.Success -> { Log.d(TAG, "verify: success") promise.resolve(MFV3Serializers.verifyResult(result.response)) } is MFResult.Fail -> { Log.e(TAG, "verify: " + result.error.message) promise.reject(result.error.code.toString(), result.error.message) } else -> { Log.e(TAG, "verify: unhandled MFResult variant: $result") promise.reject("unknown_result", "Unhandled MFResult variant: $result") } } } } } // 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+ } //#endregion //#region Helpers private fun resolveSession(result: MFResult, promise: Promise) { when (result) { is MFResult.Success -> { Log.d(TAG, "resolveSession: success") promise.resolve(MFV3Serializers.session(result.response)) } is MFResult.Fail -> { Log.e(TAG, "resolveSession: " + result.error.message) promise.reject(result.error.code.toString(), result.error.message) } else -> { Log.e(TAG, "resolveSession: unhandled MFResult variant: $result") promise.reject("unknown_result", "Unhandled MFResult variant: $result") } } } /** * Recursively convert a ReadableMap to a JsonObject. * * Used only to decode the session passed back into load() - its PascalCase keys * already match the MFSessionData @SerializedName annotations. create()/createVerify() * go through handleReadableMap() instead, since their request shape is a flat, fixed model. */ private fun readableMapToJson(map: ReadableMap): JsonObject { val obj = JsonObject() val iterator = map.keySetIterator() while (iterator.hasNextKey()) { val key = iterator.nextKey() if (map.isNull(key)) continue when (map.getType(key)) { ReadableType.Null -> {} ReadableType.Boolean -> obj.addProperty(key, map.getBoolean(key)) ReadableType.Number -> obj.addProperty(key, map.getDouble(key)) ReadableType.String -> map.getString(key)?.let { obj.addProperty(key, it) } ReadableType.Map -> map.getMap(key)?.let { obj.add(key, readableMapToJson(it)) } ReadableType.Array -> map.getArray(key)?.let { obj.add(key, readableArrayToJson(it)) } } } return obj } private fun readableArrayToJson(array: ReadableArray): com.google.gson.JsonArray { val json = com.google.gson.JsonArray() for (i in 0 until array.size()) { when (array.getType(i)) { ReadableType.Null -> {} ReadableType.Boolean -> json.add(array.getBoolean(i)) ReadableType.Number -> json.add(array.getDouble(i)) ReadableType.String -> array.getString(i)?.let { json.add(it) } ReadableType.Map -> array.getMap(i)?.let { json.add(readableMapToJson(it)) } ReadableType.Array -> array.getArray(i)?.let { json.add(readableArrayToJson(it)) } } } return json } //#endregion }