Commit f790805c authored by Javokhir's avatar Javokhir
Browse files

feat(android): implement MyID turbo module against capture SDK 3.1.9

Maps the JS config onto MyIdConfig.Builder, launches the SDK via
startActivityForResult and settles a single pending promise from the
activity-result callback.

handleActivityResult does not inspect the request code, so the listener filters
on its own code first to avoid settling the promise from an unrelated result.

MyID artifacts are not on Maven Central, and a consuming app re-resolves
transitive dependencies against its own repository set, so the repositories are
registered on all projects rather than only on this module. An opt-out property
is provided for apps that manage repositories centrally.
parent 567d3cbb
...@@ -2,7 +2,10 @@ buildscript { ...@@ -2,7 +2,10 @@ buildscript {
ext.MyidSdk = [ ext.MyidSdk = [
kotlinVersion: "2.0.21", kotlinVersion: "2.0.21",
minSdkVersion: 24, minSdkVersion: 24,
compileSdkVersion: 36 compileSdkVersion: 36,
// Override from the app's root build.gradle:
// ext { myIdCaptureSdkVersion = "3.1.9" }
myIdCaptureSdkVersion: "3.1.9"
] ]
ext.getExtOrDefault = { prop -> ext.getExtOrDefault = { prop ->
...@@ -50,6 +53,42 @@ android { ...@@ -50,6 +53,42 @@ android {
} }
} }
// MyID artifacts live outside Maven Central:
// artifactory.aigroup.uz - the SDK itself, public read access, no credentials
// developer.huawei.com - com.huawei.hms:safetydetect, pulled in by
// myid-integrity-sdk, which the capture SDK depends on
//
// These are registered on every project, not just this one, because a consuming
// app resolves our transitive dependencies against ITS OWN repository set when
// building its runtime classpath. A module-level repositories block here is
// enough to compile this library in isolation but still fails the app build
// with "Could not find uz.myid.sdk.capture:myid-capture-sdk".
//
// Apps that manage repositories centrally (dependencyResolutionManagement with
// FAIL_ON_PROJECT_REPOS) must set myIdSkipRepositoryInjection=true in
// gradle.properties and declare the two URLs themselves — see the README.
if (!rootProject.hasProperty("myIdSkipRepositoryInjection")) {
rootProject.allprojects {
repositories {
maven { url "https://artifactory.aigroup.uz/artifactory/myid/" }
maven { url "https://developer.huawei.com/repo/" }
}
}
}
repositories {
google()
mavenCentral()
maven { url "https://artifactory.aigroup.uz/artifactory/myid/" }
maven { url "https://developer.huawei.com/repo/" }
}
dependencies { dependencies {
implementation "com.facebook.react:react-android" implementation "com.facebook.react:react-android"
// The debug artifact is a separate, non-obfuscated build of the same SDK.
// Apps that define build types other than debug/release must declare
// matchingFallbacks for them, otherwise resolution fails — see the README.
debugImplementation "uz.myid.sdk.capture:myid-capture-sdk-debug:${getExtOrDefault('myIdCaptureSdkVersion')}"
releaseImplementation "uz.myid.sdk.capture:myid-capture-sdk:${getExtOrDefault('myIdCaptureSdkVersion')}"
} }
package com.myidsdk
import android.content.Context
import com.facebook.react.bridge.ReadableMap
import uz.myid.android.sdk.capture.MyIdConfig
import uz.myid.android.sdk.capture.model.MyIdCameraResolution
import uz.myid.android.sdk.capture.model.MyIdCameraSelector
import uz.myid.android.sdk.capture.model.MyIdCameraShape
import uz.myid.android.sdk.capture.model.MyIdEntryType
import uz.myid.android.sdk.capture.model.MyIdEnvironment
import uz.myid.android.sdk.capture.model.MyIdImageFormat
import uz.myid.android.sdk.capture.model.MyIdLocale
import uz.myid.android.sdk.capture.model.MyIdOrganizationDetails
import uz.myid.android.sdk.capture.model.MyIdResidency
import uz.myid.android.sdk.capture.model.MyIdScreenOrientation
/**
* Translates the JS config payload into a [MyIdConfig].
*
* A `withX(...)` call only happens when the key is actually present, so an
* omitted option keeps the SDK's own default instead of being overwritten with
* a value invented here.
*
* Unrecognised enum strings fall back to the SDK default rather than throwing —
* a typo should not hard-fail a verification the user already started.
*/
internal object MyidSdkConfigMapper {
fun build(config: ReadableMap, context: Context): MyIdConfig {
val builder = MyIdConfig.Builder(config.requireString("sessionId"))
.withClientHash(
config.requireString("clientHash"),
config.requireString("clientHashId")
)
config.optInt("minAge")?.let { builder.withMinAge(it) }
config.optDouble("distance")?.let { builder.withDistance(it.toFloat()) }
config.optBoolean("withSoundGuides")?.let { builder.withSoundGuides(it) }
config.optBoolean("showErrorScreen")?.let { builder.withErrorScreen(it) }
config.optString("huaweiAppId")?.let { builder.withHuaweiAppId(it) }
config.optEnum("environment")?.let {
builder.withEnvironment(
when (it) {
"DEBUG" -> MyIdEnvironment.Debug
else -> MyIdEnvironment.Production
}
)
}
config.optEnum("entryType")?.let {
builder.withEntryType(
when (it) {
"FACE_DETECTION" -> MyIdEntryType.FaceDetection
"VIDEO_IDENTIFICATION" -> MyIdEntryType.VideoIdentification
else -> MyIdEntryType.Identification
}
)
}
config.optEnum("residency")?.let {
builder.withResidency(
when (it) {
"USER_DEFINED" -> MyIdResidency.UserDefined
"NON_RESIDENT" -> MyIdResidency.NonResident
else -> MyIdResidency.Resident
}
)
}
config.optEnum("locale")?.let {
builder.withLocale(
when (it) {
"UZBEK_CYRILLIC" -> MyIdLocale.UzbekCyrillic
"KARAKALPAK" -> MyIdLocale.Karakalpak
"TAJIK" -> MyIdLocale.Tajik
"ENGLISH" -> MyIdLocale.English
"RUSSIAN" -> MyIdLocale.Russian
else -> MyIdLocale.Uzbek
}
)
}
config.optEnum("cameraShape")?.let {
builder.withCameraShape(
when (it) {
"ELLIPSE" -> MyIdCameraShape.Ellipse
else -> MyIdCameraShape.Circle
}
)
}
config.optEnum("cameraSelector")?.let {
builder.withCameraSelector(
when (it) {
"BACK" -> MyIdCameraSelector.Back
else -> MyIdCameraSelector.Front
}
)
}
config.optEnum("cameraResolution")?.let {
builder.withCameraResolution(
when (it) {
"HIGH" -> MyIdCameraResolution.High
else -> MyIdCameraResolution.Low
}
)
}
config.optEnum("imageFormat")?.let {
builder.withImageFormat(
when (it) {
"JPEG" -> MyIdImageFormat.JPEG
else -> MyIdImageFormat.PNG
}
)
}
config.optEnum("screenOrientation")?.let {
builder.withScreenOrientation(
when (it) {
"LANDSCAPE" -> MyIdScreenOrientation.Landscape
"FULL" -> MyIdScreenOrientation.Full
else -> MyIdScreenOrientation.Portrait
}
)
}
config.optMap("organizationDetails")?.let { details ->
builder.withOrganizationDetails(
MyIdOrganizationDetails(
phoneNumber = details.optString("phone"),
logo = details.optString("logo")?.let { context.drawableIdOrNull(it) }
)
)
}
return builder.build()
}
private fun ReadableMap.requireString(key: String): String =
optString(key) ?: throw IllegalArgumentException("Missing required config field: $key")
private fun ReadableMap.optString(key: String): String? =
if (hasKey(key) && !isNull(key)) getString(key) else null
/** Enum values travel as strings; normalise case so 'debug' works like 'DEBUG'. */
private fun ReadableMap.optEnum(key: String): String? = optString(key)?.uppercase()
private fun ReadableMap.optInt(key: String): Int? =
if (hasKey(key) && !isNull(key)) getDouble(key).toInt() else null
private fun ReadableMap.optDouble(key: String): Double? =
if (hasKey(key) && !isNull(key)) getDouble(key) else null
private fun ReadableMap.optBoolean(key: String): Boolean? =
if (hasKey(key) && !isNull(key)) getBoolean(key) else null
private fun ReadableMap.optMap(key: String): ReadableMap? =
if (hasKey(key) && !isNull(key)) getMap(key) else null
/** Resolves a drawable by name; a missing asset degrades to no logo. */
private fun Context.drawableIdOrNull(name: String): Int? =
resources.getIdentifier(name, "drawable", packageName).takeIf { it != 0 }
}
package com.myidsdk package com.myidsdk
import android.app.Activity
import android.content.Intent
import android.graphics.Bitmap
import android.util.Base64
import com.facebook.react.bridge.Arguments
import com.facebook.react.bridge.BaseActivityEventListener
import com.facebook.react.bridge.Promise
import com.facebook.react.bridge.ReactApplicationContext import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.bridge.ReadableMap
import com.facebook.react.bridge.WritableMap
import uz.myid.android.sdk.capture.MyIdClient
import uz.myid.android.sdk.capture.MyIdException
import uz.myid.android.sdk.capture.MyIdResult
import uz.myid.android.sdk.capture.MyIdResultListener
import uz.myid.android.sdk.capture.model.MyIdEvent
import uz.myid.android.sdk.capture.model.MyIdGraphicFieldType
import java.io.ByteArrayOutputStream
class MyidSdkModule(reactContext: ReactApplicationContext) : class MyidSdkModule(reactContext: ReactApplicationContext) :
NativeMyidSdkSpec(reactContext) { NativeMyidSdkSpec(reactContext) {
override fun multiply(a: Double, b: Double): Double { private val client = MyIdClient()
return a * b
/**
* The SDK owns the screen for the whole flow, so at most one verification can
* be in flight. Guarded rather than queued: a second concurrent call is a
* caller bug, not something to silently serialise.
*/
private var pendingPromise: Promise? = null
private val activityEventListener = object : BaseActivityEventListener() {
override fun onActivityResult(
activity: Activity,
requestCode: Int,
resultCode: Int,
data: Intent?
) {
// handleActivityResult does not check the request code itself, so
// filtering here keeps unrelated results from settling our promise.
if (requestCode != MYID_REQUEST_CODE) return
client.handleActivityResult(resultCode, resultListener)
}
}
private val resultListener = object : MyIdResultListener {
override fun onSuccess(result: MyIdResult) {
val promise = takePending() ?: return
promise.resolve(result.toPayload())
}
override fun onError(exception: MyIdException) {
takePending()?.reject(exception.code.toString(), exception.message)
}
override fun onUserExited() {
takePending()?.reject(CODE_USER_CANCELLED, "User canceled flow")
}
override fun onEvent(event: MyIdEvent) {
// Progress telemetry (camera opened, face captured, ...). Not surfaced:
// the JS API is a single promise, and the SDK renders its own progress.
}
}
init {
reactContext.addActivityEventListener(activityEventListener)
}
/**
* @param appearance accepted for bridge symmetry; theming on Android is done
* by overriding the SDK's XML resources, not at runtime.
*/
override fun start(config: ReadableMap, appearance: ReadableMap, promise: Promise) {
if (pendingPromise != null) {
promise.reject(CODE_FAILURE, "A MyID verification is already in progress")
return
}
val activity = currentActivity
if (activity == null) {
promise.reject(CODE_FAILURE, "Cannot start MyID: no foreground activity")
return
}
pendingPromise = promise
try {
val myIdConfig = MyidSdkConfigMapper.build(config, activity)
client.startActivityForResult(
activity = activity,
requestCode = MYID_REQUEST_CODE,
config = myIdConfig,
listener = resultListener
)
} catch (error: Exception) {
// Never leak the config into the message — it carries clientHash.
takePending()?.reject(CODE_FAILURE, "Failed to start MyID: ${error.message}")
}
}
override fun invalidate() {
reactApplicationContext.removeActivityEventListener(activityEventListener)
takePending()?.reject(CODE_FAILURE, "MyID was interrupted because the app reloaded")
super.invalidate()
}
/**
* Claims the pending promise so it can be settled exactly once — the SDK may
* invoke a listener callback more than once in edge cases.
*/
private fun takePending(): Promise? {
val promise = pendingPromise
pendingPromise = null
return promise
}
private fun MyIdResult.toPayload(): WritableMap = Arguments.createMap().apply {
putString("code", code)
val portrait = try {
getGraphicFieldImageByType(MyIdGraphicFieldType.FacePortrait)
} catch (error: Exception) {
// A missing portrait must not fail an otherwise successful verification.
null
}
portrait?.toBase64Jpeg()?.let { putString("base64", it) }
}
private fun Bitmap.toBase64Jpeg(): String? = try {
ByteArrayOutputStream().use { stream ->
compress(Bitmap.CompressFormat.JPEG, JPEG_QUALITY, stream)
Base64.encodeToString(stream.toByteArray(), Base64.NO_WRAP)
}
} catch (error: Exception) {
null
} }
companion object { companion object {
const val NAME = NativeMyidSdkSpec.NAME const val NAME = NativeMyidSdkSpec.NAME
private const val MYID_REQUEST_CODE = 1771
private const val JPEG_QUALITY = 100
private const val CODE_USER_CANCELLED = "101"
private const val CODE_FAILURE = "103"
} }
} }
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment