package br.com.expo.modules.oitiexpoliveness3d import expo.modules.kotlin.modules.Module import expo.modules.kotlin.modules.ModuleDefinition import java.net.URL import android.Manifest import android.app.Activity import android.content.Intent import android.content.pm.PackageManager import android.util.Log import androidx.core.app.ActivityCompat import androidx.core.content.ContextCompat import br.com.oiti.liveness3d.app.ui.HybridLiveness3DActivity import br.com.oiti.liveness3d.data.model.Liveness3DUser import br.com.oiti.liveness3d.data.model.ENVIRONMENT3D import br.com.oiti.liveness3d.data.model.Liveness3DFontsKey import br.com.oiti.liveness3d.data.model.LoadingType3D import com.facebook.react.modules.core.PermissionAwareActivity import expo.modules.kotlin.Promise import expo.modules.kotlin.events.OnActivityResultPayload import br.com.expo.modules.oitiexpoliveness3d.theme.Liveness3DText import br.com.expo.modules.oitiexpoliveness3d.theme.Liveness3DTheme import com.facebook.react.modules.core.PermissionListener import org.json.JSONObject class OitiExpoLiveness3dModule : Module() { private var livenessPromise: Promise? = null private var permissionPromise: Promise? = null override fun definition() = ModuleDefinition { Name("OitiExpoLiveness3d") Constants("PI" to Math.PI) Events("onChange", "onLoad") AsyncFunction("setValueAsync") { value: String -> sendEvent("onChange", mapOf("value" to value)) } AsyncFunction("checkCameraPermission") { val activity = appContext.currentActivity ?: throw IllegalStateException("Activity doesn't exist") ContextCompat.checkSelfPermission(activity, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED } AsyncFunction("askCameraPermission") { promise: Promise -> val activity: Activity = appContext.currentActivity ?: run { Log.e(TAG, "No current Activity") promise.reject("NO_ACTIVITY", "Nenhuma Activity disponível", null) return@AsyncFunction } if (ContextCompat.checkSelfPermission(activity, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED) { Log.i(TAG, "CAMERA permission already granted") promise.resolve(true) return@AsyncFunction } permissionPromise = promise val paActivity = activity as? PermissionAwareActivity if (paActivity == null) { Log.e(TAG, "Activity não implementa PermissionAwareActivity") promise.reject( "NO_PERMISSION_AWARE_ACTIVITY", "Activity não implementa PermissionAwareActivity", null ) return@AsyncFunction } paActivity.requestPermissions( arrayOf(Manifest.permission.CAMERA), CAMERA_PERMISSION_REQUEST_CODE, object : PermissionListener { override fun onRequestPermissionsResult( requestCode: Int, permissions: Array, grantResults: IntArray ): Boolean { Log.d(TAG, "onRequestPermissionsResult: requestCode=$requestCode") if (requestCode == CAMERA_PERMISSION_REQUEST_CODE) { val granted = grantResults.firstOrNull() == PackageManager.PERMISSION_GRANTED Log.i(TAG, "CAMERA permission granted? $granted") permissionPromise?.resolve(granted) permissionPromise = null return true } return false } } ) } AsyncFunction("startliveness3d") { args: Map, promise: Promise -> val activity = appContext.currentActivity ?: throw IllegalStateException("Activity doesn't exist") Log.d(TAG, "startliveness3d called") livenessPromise = promise val appKey = args["appkey"] as? String ?: "" val ticket = args["ticket"] as? String val envStr = (args["environment"] as? String ?: "HML").uppercase() val loading = args["loading"] as? Map ?: emptyMap() val typeLoading = loading["type"]?.toString()?.ifBlank { null } ?: "default" val sizeLoadingRaw = (loading["size"] as? Number)?.toInt() ?: (loading["size"] as? String)?.toIntOrNull() ?: 10 val sizeLoading = normalizeLoadingSize(sizeLoadingRaw) val bgColor = loading["backgroundColor"]?.toString()?.ifBlank { null } ?: "#FFFFFF" val loadingColor = loading["loadingColor"]?.toString()?.ifBlank { null } ?: "#000000" val textsJson = args["liveness3Dtext"] val themeJson = args["theme"] as? Map val fontsJson = args["fonts"] as? Map val environment = if (envStr == "PRD") ENVIRONMENT3D.PRD else ENVIRONMENT3D.HML val themeMap = themeJson?.toStringMap() val themeBuilder = themeMap ?.takeIf { it.isNotEmpty() } ?.let { Liveness3DTheme(it).apply() } val fonts = fontsJson?.let { toFontMap(it) } val texts = (textsJson as? Map)?.let { Liveness3DText().getTexts(it) } val user = Liveness3DUser(appKey = appKey, environment, themeBuilder) val intent = Intent(activity, HybridLiveness3DActivity::class.java).apply { putExtra(HybridLiveness3DActivity.PARAM_LIVENESS3D_USER, user) ticket?.let { putExtra(HybridLiveness3DActivity.PARAM_TICKET, it) } putExtra(HybridLiveness3DActivity.PARAM_TEXTS, texts) putExtra(HybridLiveness3DActivity.PARAM_FONTS, fonts) putExtra(HybridLiveness3DActivity.PARAM_CUSTOM_LOADING_BACKGROUND, bgColor) putExtra(HybridLiveness3DActivity.PARAM_CUSTOM_LOADING_SPINNER_COLOR, loadingColor) putExtra(HybridLiveness3DActivity.PARAM_CUSTOM_LOADING_SIZE, sizeLoading) val loadingType = if (typeLoading == "default") LoadingType3D.ACTIVITY_INDICATOR else LoadingType3D.SPINNER putExtra(HybridLiveness3DActivity.PARAM_CUSTOM_LOADING_TYPE, loadingType) } activity.startActivityForResult(intent, LIVENESS3D_REQUEST) } OnActivityResult { _, payload: OnActivityResultPayload -> if (payload.requestCode == LIVENESS3D_REQUEST && livenessPromise != null) { val promise = livenessPromise!! if (payload.resultCode == Activity.RESULT_OK) { val data = payload.data val resultMap = mapOf( "cause" to data?.getStringExtra(HybridLiveness3DActivity.PARAM_RESULT_CAUSE).orEmpty(), "codId" to data?.getStringExtra(HybridLiveness3DActivity.PARAM_RESULT_COD_ID).orEmpty(), "protocol" to data?.getStringExtra(HybridLiveness3DActivity.PARAM_RESULT_PROTOCOL).orEmpty(), "blob" to data?.getStringExtra(HybridLiveness3DActivity.PARAM_RESULT_SCAN).orEmpty(), "valid" to (data?.getBooleanExtra(HybridLiveness3DActivity.PARAM_RESULT_VALID, false) ?: false) ) promise.resolve(JSONObject(resultMap as Map<*, *>).toString()) } else { val errorMsg = payload.data ?.getStringExtra(HybridLiveness3DActivity.PARAM_RESULT_ERROR) ?: "User canceled" promise.reject("RESULT_CANCELED", errorMsg, null) } livenessPromise = null } } View(OitiExpoLiveness3dView::class) { Prop("url") { view: OitiExpoLiveness3dView, url: URL -> view.webView.loadUrl(url.toString()) } Events("onLoad") } } private fun toFontMap(raw: Map): HashMap { val result = HashMap() for ((key, value) in raw) { (value as? String)?.takeIf { it.isNotBlank() }?.let { fontName -> getFontKey(key)?.let { fontKey -> result[fontKey] = "fonts/${fontName.lowercase()}.ttf" } } } return result } private fun Map.toStringMap(): Map { val result = HashMap() for ((key, value) in this) { when (value) { is String -> result[key] = value is Number -> result[key] = value.toString() is Boolean -> result[key] = value.toString() } } return result } private fun getFontKey(id: String): Liveness3DFontsKey? = when (id) { "guidanceCustomizationHeaderFont" -> Liveness3DFontsKey.GUIDANCE_CUSTOMIZATION_HEADER_FONT "guidanceCustomizationSubtextFont" -> Liveness3DFontsKey.GUIDANCE_CUSTOMIZATION_SUBTEXT_FONT "guidanceCustomizationButtonFont" -> Liveness3DFontsKey.GUIDANCE_CUSTOMIZATION_BUTTON_FONT "readyScreenCustomizationHeaderFont" -> Liveness3DFontsKey.GUIDANCE_CUSTOMIZATION_READY_SCREEN_HEADER_FONT "readyScreenCustomizationSubtextFont" -> Liveness3DFontsKey.GUIDANCE_CUSTOMIZATION_READY_SCREEN_SUBTEXT_FONT "retryScreenCustomizationHeaderFont" -> Liveness3DFontsKey.GUIDANCE_CUSTOMIZATION_RETRY_SCREEN_HEADER_FONT "retryScreenCustomizationSubtextFont" -> Liveness3DFontsKey.GUIDANCE_CUSTOMIZATION_RETRY_SCREEN_SUBTEXT_FONT "resultScreenCustomizationMessageFont" -> Liveness3DFontsKey.RESULT_SCREEN_CUSTOMIZATION_MESSAGE_FONT "feedbackCustomizationTextFont" -> Liveness3DFontsKey.FEEDBACK_CUSTOMIZATION_TEXT_FONT else -> null } private fun normalizeLoadingSize(rawSize: Int): Int { val safeSize = rawSize.coerceAtLeast(0) return if (safeSize <= 10) safeSize * 30 else safeSize } companion object { private const val LIVENESS3D_REQUEST = 999 private const val CAMERA_PERMISSION_REQUEST_CODE = 1111 private const val TAG = "OitiExpoLiveness3dModule" } }