package com.myfatoorahreactnative.cardviewv3 import android.app.Activity import android.content.Context import android.util.Log import android.widget.FrameLayout import com.facebook.react.bridge.Arguments import com.facebook.react.bridge.ReactApplicationContext import com.facebook.react.bridge.WritableMap import com.facebook.react.modules.core.DeviceEventManagerModule import com.myfatoorah.sdk.v3.entity.MFCardConfigurationV3 import com.myfatoorah.sdk.v3.entity.session.MFSessionData import com.myfatoorah.sdk.v3.views.embeddedpayment.MFCardViewV3 import com.myfatoorahreactnative.utils.MFCardV3Constants /** * MFCardV3View - React Native host view for the V3 embedded card. * * A thin FrameLayout wrapping a fresh SDK `MFCardViewV3` per mounted RN component * (no singleton). Each view registers itself under its JS-assigned `cardId` so * `MFCardV3Module`'s promise methods can reach the exact instance. * * @see MFCardV3Module Drives this view's load/submit/verify via the cardId registry. */ class MFCardV3View( context: Context, private val reactContext: ReactApplicationContext ) : FrameLayout(context) { private val TAG = MFCardV3Constants.MFCardViewV3ModuleNAME // The SDK casts the view's context to Activity to launch the 3DS / VERIFY challenge // (MFCardViewV3.open3DSWebView -> context as? Activity). RN hands us a // ThemedReactContext (NOT an Activity); without the host Activity the redirect Intent // never starts and submit/verify hang forever. The bridge tries to rebuild the // SDK view with currentActivity once RN exposes it. private var currentConfiguration = MFCardConfigurationV3() private var loadedSession: MFSessionData? = null var cardView = createSdkCardView() private set /** Unique id assigned by the JS component; routes module calls + events. */ private var cardId: String? = null init { if (reactContext.currentActivity == null) { Log.e(TAG, "currentActivity is null - 3DS/VERIFY redirects will not work for this view") } addView(cardView, LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)) wireCallbacks() } /** * Register (or unregister) this view under the given cardId so MFCardV3Module * can resolve it for load/submit/verify calls. */ fun setCardId(id: String?) { val old = cardId cardId = id synchronized(registry) { if (old != null && registry[old] === this) registry.remove(old) if (id != null) registry[id] = this } if (id != null && !ensureActivityBackedCardView()) { val params = Arguments.createMap() params.putString("cardId", id) params.putString("code", "no_activity_context") params.putString("message", "MFCardViewV3 could not obtain a host Activity; 3DS/VERIFY redirects will not work.") emit(MFCardV3Constants.V3ErrorEventName, params) } } fun getCardId(): String? = cardId /** Applies the reused JS `MFCardViewStyle` to the SDK view before load(). */ fun applyStyle(style: com.facebook.react.bridge.ReadableMap?) { currentConfiguration = MFCardV3ConfigTranslator.configuration(style) cardView.configuration = currentConfiguration } fun loadSession(sessionData: MFSessionData) { ensureActivityBackedCardView() loadedSession = sessionData cardView.load(sessionData) } /** * The embedded SDK launches 3DS by casting its own Context to Activity. React * Native can mount views before currentActivity is populated, so recover by * rebuilding the SDK view with the Activity once it is available. */ fun ensureActivityBackedCardView(): Boolean { if (cardView.context is Activity) return true val activity = reactContext.currentActivity ?: return false val params = cardView.layoutParams ?: LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT) removeView(cardView) cardView = MFCardViewV3(activity) cardView.configuration = currentConfiguration addView(cardView, params) wireCallbacks() loadedSession?.let { cardView.load(it) } return true } override fun onDetachedFromWindow() { super.onDetachedFromWindow() val id = cardId if (id != null) { synchronized(registry) { if (registry[id] === this) registry.remove(id) } } } //#region SDK callbacks -> tagged RN events private fun createSdkCardView(): MFCardViewV3 = MFCardViewV3(reactContext.currentActivity ?: context).apply { configuration = currentConfiguration } private fun wireCallbacks() { cardView.onHeightChanged = { height -> val cid = cardId if (cid != null) { val params = Arguments.createMap() params.putString("cardId", cid) params.putDouble("height", height.toDouble()) emit(MFCardV3Constants.V3CardResizedEventName, params) } } cardView.onWidgetEvent = { event -> val cid = cardId if (cid != null) { Log.d(TAG, "onWidgetEvent: ${event.name}") val params = Arguments.createMap() params.putString("cardId", cid) params.putString("name", event.name) params.putInt("id", event.id) event.paymentMethodName?.let { params.putString("paymentMethodName", it) } emit(MFCardV3Constants.V3WidgetEventName, params) } } cardView.onLoadFailed = { error -> val cid = cardId if (cid != null) { Log.e(TAG, "onLoadFailed: " + error.message) val params = Arguments.createMap() params.putString("cardId", cid) params.putString("code", error.code.toString()) params.putString("message", error.message ?: "") emit(MFCardV3Constants.V3ErrorEventName, params) } } } private fun emit(eventName: String, params: WritableMap) { reactContext .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java) .emit(eventName, params) } //#endregion companion object { /** cardId -> view. Touched only under lock; entries cleared on detach. */ private val registry = mutableMapOf() fun viewFor(cardId: String): MFCardV3View? = synchronized(registry) { registry[cardId] } } }