package com.checkoutreactnativecomponents.components import android.content.Context import android.util.AttributeSet import android.widget.FrameLayout import androidx.compose.runtime.mutableStateOf import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.platform.ViewCompositionStrategy import androidx.compose.ui.unit.Dp import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.setViewTreeLifecycleOwner import androidx.lifecycle.findViewTreeLifecycleOwner import com.checkoutreactnativecomponents.CheckoutManager import com.checkoutreactnativecomponents.interfaces.CheckoutComponent import com.checkoutreactnativecomponents.utils.Arch import com.checkoutreactnativecomponents.utils.Constants import kotlinx.coroutines.MainScope import kotlinx.coroutines.cancel import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch import com.facebook.react.bridge.Arguments import com.facebook.react.bridge.WritableMap import com.facebook.react.uimanager.ThemedReactContext import com.facebook.react.uimanager.UIManagerHelper import com.facebook.react.uimanager.events.Event import com.facebook.react.uimanager.events.RCTEventEmitter import com.facebook.react.uimanager.events.RCTModernEventEmitter public abstract class CheckoutComposeComponent : FrameLayout { protected val composeView: ComposeView = ComposeView(context) protected val scope: CoroutineScope = MainScope() private var componentConfig: HashMap? = null private var currentComponent: CheckoutComponent? = null private val componentState = mutableStateOf(null) public constructor(context: Context) : super(context) { init() } public constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) { init() } public constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { init() } override fun requestLayout() { super.requestLayout() // This is a workaround to ensure that the ComposeView is measured and laid out correctly. @Suppress("SENSELESS_COMPARISON") if (!Arch.isNewArch) { post(measureAndLayout) } } private val measureAndLayout = Runnable { measure( MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY) ) layout(left, top, right, bottom) } private fun init() { composeView.setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) ensureLifecycleOwner() composeView.setContent { MeasuringBox( onSizeChanged = { size -> emitOnDimensionsChanged(size.width, size.height) }, ) { componentState.value?.Render() } } addView(composeView) } /** * Abstract method to create the Compose component based on the current configuration. * Implementations should create their specific component using CheckoutManager. */ protected abstract suspend fun createComponent(config: HashMap): CheckoutComponent private fun renderComponent() { scope.launch { try { val component = createComponent(componentConfig ?: hashMapOf()) currentComponent = component componentState.value = component } catch (e: Exception) { componentState.value = null currentComponent = null val error = "Render Error" CheckoutManager.emitAndLogError(error, e) } } } private fun ensureLifecycleOwner() { if (composeView.findViewTreeLifecycleOwner() == null) { val reactContext = context as? ThemedReactContext val activity = reactContext?.currentActivity as? LifecycleOwner if (activity != null) { composeView.setViewTreeLifecycleOwner(activity) } } } override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { val parentWidth = MeasureSpec.getSize(widthMeasureSpec) val parentWidthMode = MeasureSpec.getMode(widthMeasureSpec) val widthSpec = MeasureSpec.makeMeasureSpec(parentWidth, parentWidthMode) val heightSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED) composeView.measure(widthSpec, heightSpec) val measuredHeight = composeView.measuredHeight val measuredWidth = composeView.measuredWidth setMeasuredDimension(measuredWidth, measuredHeight) } override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) { composeView.layout(0, 0, right - left, bottom - top) } override fun onDetachedFromWindow() { super.onDetachedFromWindow() cleanup() } private fun cleanup() { componentState.value = null currentComponent = null componentConfig = null scope.cancel() composeView.disposeComposition() if (!Arch.isNewArch) { removeCallbacks(measureAndLayout) } } public fun setConfig(value: HashMap) { if (value != componentConfig || currentComponent == null) { componentConfig = value renderComponent() } } protected fun emitOnDimensionsChanged(width: Dp, height: Dp) { val reactContext = context as ThemedReactContext val surfaceId = reactContext.surfaceId val eventData = Arguments.createMap().apply { putDouble("width", width.value.toDouble()) putDouble("height", height.value.toDouble()) } val eventDispatcher = UIManagerHelper.getEventDispatcherForReactTag(reactContext, id) eventDispatcher?.dispatchEvent( OnDimensionsChangedEvent( surfaceId, id, eventData ) ) } /** * Abstract method to get the component name for logging and identification. */ protected abstract fun getComponentName(): String /** * Event class for dimension changes that works with both Fabric and Paper architectures. */ private class OnDimensionsChangedEvent( surfaceId: Int, viewId: Int, private val payload: WritableMap, ) : Event(surfaceId, viewId) { override fun getEventName(): String = Constants.ON_DIMENSIONS_CHANGED // Legacy Paper event emission override fun dispatch(rctEventEmitter: RCTEventEmitter) { rctEventEmitter.receiveEvent(viewTag, eventName, payload) } override fun dispatchModern(rctEventEmitter: RCTModernEventEmitter) { rctEventEmitter.receiveEvent(surfaceId, viewTag, eventName, payload) } override fun canCoalesce(): Boolean = true } }