@finos_sdk/sdk-ekyc 1.3.2 → 1.3.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -65,7 +65,7 @@ dependencies {
65
65
  implementation 'com.facebook.react:react-android'
66
66
 
67
67
  // Finos eKYC SDK dependencies from GitHub Packages Maven repository
68
- def sdkVersion = "1.3.2"
68
+ def sdkVersion = "1.3.3.1"
69
69
  implementation("finos.sdk.ekyc:ekyc:$sdkVersion")
70
70
  implementation("finos.sdk.ekyc:ekycui:$sdkVersion")
71
71
  implementation("finos.sdk.ekyc:nfc:$sdkVersion")
@@ -1,6 +1,9 @@
1
1
  package finos.sdk.ekyc
2
2
 
3
3
  import android.R
4
+ import android.net.Uri
5
+ import android.os.Handler
6
+ import android.os.Looper
4
7
  import android.util.Base64
5
8
  import android.util.Log
6
9
  import com.facebook.react.bridge.Arguments
@@ -38,9 +41,12 @@ import finos.sdk.smsotp.SdkSmsOtpService
38
41
  import finos.sdk.core.model.sdk.config.SmsOtpConfig
39
42
  import vn.softdreams.easyca.sdk.SdkeSign
40
43
 
44
+ import org.json.JSONArray
41
45
  import org.json.JSONObject
42
46
  import java.io.File
43
47
  import java.io.FileOutputStream
48
+ import java.io.FileInputStream
49
+ import java.io.InputStream
44
50
  import java.util.Date
45
51
 
46
52
  @ReactModule(name = EKYCModule.NAME)
@@ -102,8 +108,20 @@ class EKYCModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaMo
102
108
  promise.resolve(true)
103
109
  }
104
110
  } catch (e: Exception) {
105
- Log.e(TAG, "❌ initSdkEkyc() failed: ${e.message}", e)
106
- promise.reject("INIT_ERROR", "Failed to initialize SDK EKYC: ${e.message}")
111
+ val msg = e.message?.lowercase() ?: ""
112
+ // SDK đã được khởi tạo (Koin/DI already started) → coi là thành công, cho icon pass xanh / text xanh
113
+ if (msg.contains("already been started") || msg.contains("already started") || msg.contains("already initialized")) {
114
+ Log.d(TAG, "✅ initSdkEkyc() – SDK đã khởi tạo, trả success (không coi là lỗi)")
115
+ val params = Arguments.createMap().apply {
116
+ putString("status", "success")
117
+ putString("message", "SDK EKYC already initialized")
118
+ }
119
+ sendEvent("EKYCInitEvent", params)
120
+ promise.resolve(true)
121
+ } else {
122
+ Log.e(TAG, "❌ initSdkEkyc() failed: ${e.message}", e)
123
+ promise.reject("INIT_ERROR", "Failed to initialize SDK EKYC: ${e.message}")
124
+ }
107
125
  }
108
126
  }
109
127
 
@@ -433,48 +451,58 @@ class EKYCModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaMo
433
451
  ) {
434
452
  Log.d(TAG, "▶️ startFaceCompare() called")
435
453
  try {
436
- val currentActivity = reactApplicationContext.currentActivity
437
- if (currentActivity == null) {
438
- Log.e(TAG, "❌ startFaceCompare() failed: Activity not available")
439
- promise.reject("NO_ACTIVITY", "Activity not available")
454
+ // React Native có thể gọi khi currentActivity = null (background / app state).
455
+ // FaceService không cần Activity, nên không block ở đây.
456
+ val selfieFile = resolveImageInputToFile(selfieImage, "selfie")
457
+ val idImageFile = resolveImageInputToFile(idImage, "id")
458
+
459
+ if (!selfieFile.exists() || selfieFile.length() <= 0L) {
460
+ promise.reject("SELFIE_IMAGE_INVALID", "Selfie image is invalid or not found")
440
461
  return
441
462
  }
442
- val imageFile = File(selfieImage.replace("file://", ""))
443
- if (!imageFile.exists()) {
444
- promise.reject("FILE_NOT_FOUND", "Image file does not exist")
463
+ if (!idImageFile.exists() || idImageFile.length() <= 0L) {
464
+ promise.reject("ID_IMAGE_INVALID", "ID image is invalid or not found")
445
465
  return
446
466
  }
447
- val idImageFile = base64ToImageFile(idImage)
448
467
 
449
468
  val faceServiceConfig =
450
469
  FaceServiceConfig(
451
470
  transactionId = transactionId,
452
- selfieImage = imageFile,
471
+ selfieImage = selfieFile,
453
472
  idImage = idImageFile
454
473
  )
455
- val ekycConfig = EKYCConfigSDK(faceServiceConfig = faceServiceConfig)
456
- SdkEkycFaceService.startEkyc(
457
- ekycConfigSDK = ekycConfig,
458
- callbackSuccess = { event, data ->
459
- Log.d(TAG, "✅ startFaceCompare() success")
460
- val (eventMap, promiseMap) = createSeparateMaps { map ->
461
- map.putString("event", event.name.toString())
462
- map.putString("data", data.toString())
463
- }
464
- sendEvent("onFaceCompareSuccess", eventMap)
465
- promise.resolve(promiseMap)
466
- },
467
- callbackError = { event, message ->
468
- Log.e(TAG, "❌ startFaceCompare() failed - Event: $event, Message: $message")
469
- val errorMap =
470
- Arguments.createMap().apply {
471
- putString("event", event.name.toString())
472
- putString("message", message?.toString() ?: "Unknown error")
473
- }
474
- sendEvent("onFaceCompareError", errorMap)
475
- promise.reject(event.name.toString(), message?.toString() ?: "Unknown FaceService error")
476
- }
474
+ // FaceService cần appKeyFaceService. Trước đây RN chỉ truyền 1 key string,
475
+ // nên map key này vào appKeyFaceService (và set appKey luôn để tương thích).
476
+ val ekycConfig = EKYCConfigSDK(
477
+ appKey = AppKeyConfig(appKey = appKey, appKeyFaceService = appKey),
478
+ faceServiceConfig = faceServiceConfig
477
479
  )
480
+
481
+ // Đảm bảo chạy trên main thread để tránh crash trên một số devices/SDK versions.
482
+ Handler(Looper.getMainLooper()).post {
483
+ SdkEkycFaceService.startEkyc(
484
+ ekycConfigSDK = ekycConfig,
485
+ callbackSuccess = { event, data ->
486
+ Log.d(TAG, "✅ startFaceCompare() success")
487
+ val (eventMap, promiseMap) = createSeparateMaps { map ->
488
+ map.putString("event", event.name.toString())
489
+ map.putString("data", data.toString())
490
+ }
491
+ sendEvent("onFaceCompareSuccess", eventMap)
492
+ promise.resolve(promiseMap)
493
+ },
494
+ callbackError = { event, message ->
495
+ Log.e(TAG, "❌ startFaceCompare() failed - Event: $event, Message: $message")
496
+ val errorMap =
497
+ Arguments.createMap().apply {
498
+ putString("event", event.name.toString())
499
+ putString("message", message?.toString() ?: "Unknown error")
500
+ }
501
+ sendEvent("onFaceCompareError", errorMap)
502
+ promise.reject(event.name.toString(), message?.toString() ?: "Unknown FaceService error")
503
+ }
504
+ )
505
+ }
478
506
  } catch (e: Exception) {
479
507
  Log.e(TAG, "❌ startFaceCompare() exception: ${e.message}", e)
480
508
  promise.reject("FACE_COMPARE_ERROR", e.message)
@@ -502,7 +530,56 @@ class EKYCModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaMo
502
530
  }
503
531
  }
504
532
 
505
- private fun base64ToImageFile(base64Image: String): File {
533
+ /**
534
+ * Resolve input image string to a local file.
535
+ * - Supports: file path (`/...`), file URI (`file://...`), content URI (`content://...`), base64 (with/without data URI).
536
+ */
537
+ private fun resolveImageInputToFile(input: String, prefix: String): File {
538
+ val value = input.trim()
539
+ if (value.isEmpty()) {
540
+ // Return empty file to trigger validation error upstream
541
+ return File(reactApplicationContext.cacheDir, "empty_${prefix}_${Date().time}.jpg").apply {
542
+ createNewFile()
543
+ }
544
+ }
545
+
546
+ // file://...
547
+ if (value.startsWith("file://")) {
548
+ val file = File(value.removePrefix("file://"))
549
+ if (file.exists()) return file
550
+ }
551
+
552
+ // content://...
553
+ if (value.startsWith("content://")) {
554
+ val copied = copyContentUriToCacheFile(value, prefix)
555
+ if (copied != null && copied.exists()) return copied
556
+ }
557
+
558
+ // raw path
559
+ val rawFile = File(value)
560
+ if (rawFile.exists()) return rawFile
561
+
562
+ // fallback: treat as base64
563
+ return base64ToImageFile(value, prefix)
564
+ }
565
+
566
+ private fun copyContentUriToCacheFile(uriString: String, prefix: String): File? {
567
+ return try {
568
+ val uri = Uri.parse(uriString)
569
+ val resolver = reactApplicationContext.contentResolver
570
+ val inputStream: InputStream = resolver.openInputStream(uri) ?: return null
571
+ val outFile = File(reactApplicationContext.cacheDir, "${prefix}_${Date().time}.jpg")
572
+ FileOutputStream(outFile).use { out ->
573
+ inputStream.use { ins -> ins.copyTo(out) }
574
+ }
575
+ outFile
576
+ } catch (e: Exception) {
577
+ Log.w(TAG, "copyContentUriToCacheFile failed: ${e.message}")
578
+ null
579
+ }
580
+ }
581
+
582
+ private fun base64ToImageFile(base64Image: String, prefix: String = "image"): File {
506
583
  try {
507
584
  // Remove data URI prefix if present (like "data:image/png;base64,")
508
585
  var processedBase64 = base64Image
@@ -515,7 +592,7 @@ class EKYCModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaMo
515
592
 
516
593
  // Create a unique filename with timestamp
517
594
  val timestamp = Date().time.toString()
518
- val fileName = "image_$timestamp.jpg"
595
+ val fileName = "${prefix}_$timestamp.jpg"
519
596
 
520
597
  // Get temporary directory and create file path
521
598
  val directory = reactApplicationContext.cacheDir
@@ -527,14 +604,28 @@ class EKYCModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaMo
527
604
 
528
605
  return imageFile
529
606
  } catch (e: Exception) {
530
- Log.e("LivenessFragment", "Error converting base64 to image file: ${e.message}", e)
607
+ Log.e(TAG, "Error converting base64 to image file: ${e.message}", e)
531
608
  // In case of error, create an empty file to avoid null returns
532
- val errorFile = File(reactApplicationContext.cacheDir, "error_image_${Date().time}.jpg")
609
+ val errorFile = File(reactApplicationContext.cacheDir, "error_${prefix}_${Date().time}.jpg")
533
610
  errorFile.createNewFile()
534
611
  return errorFile
535
612
  }
536
613
  }
537
614
 
615
+ /**
616
+ * Convert File to base64 string for passing to React Native.
617
+ * Mirrors Android SDK: data?.ekycStateModel?.eKYCFileModel?.imageFace
618
+ */
619
+ private fun fileToBase64(file: File?): String? {
620
+ if (file == null || !file.exists()) return null
621
+ return try {
622
+ Base64.encodeToString(file.readBytes(), Base64.NO_WRAP)
623
+ } catch (e: Exception) {
624
+ Log.e(TAG, "fileToBase64 error: ${e.message}", e)
625
+ null
626
+ }
627
+ }
628
+
538
629
  @ReactMethod
539
630
  fun startEkycUI(
540
631
  appKey: String,
@@ -574,15 +665,26 @@ class EKYCModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaMo
574
665
  // Parse StyleConfig JSON from React Native
575
666
  val styleConfig = parseStyleConfig(styleConfigJson)
576
667
 
577
- // Extract switchFrontCamera from optionConfigJson
668
+ // LivenessConfig theo SDKeKYCActivity (157-169): isActiveLiveness, autoCapture, forceCaptureTimeout, isShowCameraFont, customActions, activeActionCount
578
669
  val switchFrontCamera = extractBooleanValue(optionConfigJson, "switchFrontCamera") ?: false
670
+ val isActiveLiveness = extractBooleanValue(optionConfigJson, "isActiveLiveness") ?: true
671
+ val autoCapture = extractBooleanValue(optionConfigJson, "autoCapture") ?: true
672
+ val forceCaptureTimeoutSec = extractIntValue(optionConfigJson, "forceCaptureTimeout") ?: 30
673
+ val customActionsList = parseCustomActionsFromJson(optionConfigJson)
674
+ val activeActionCount = extractIntValue(optionConfigJson, "activeActionCount") ?: customActionsList?.size ?: 2
579
675
 
580
- // Create LivenessConfig if LIVENESS is in the flow and switchFrontCamera is set
581
- val livenessConfig = if (finalFlow.contains(SDKType.LIVENESS) && switchFrontCamera) {
582
- LivenessConfig(isShowCameraFont = true)
676
+ val livenessConfig = if (finalFlow.contains(SDKType.LIVENESS)) {
677
+ LivenessConfig(
678
+ isActiveLiveness = isActiveLiveness,
679
+ autoCapture = autoCapture,
680
+ forceCaptureTimeout = (forceCaptureTimeoutSec * 1000L).coerceAtLeast(0),
681
+ isShowCameraFont = switchFrontCamera,
682
+ customActions = customActionsList?.takeIf { it.isNotEmpty() },
683
+ activeActionCount = activeActionCount,
684
+ )
583
685
  } else null
584
686
 
585
- // Create EKYCConfigSDK like MainActivity.kt
687
+ // Create EKYCConfigSDK like SDKeKYCActivity / MainActivity.kt
586
688
  val ekycConfigSDK = EKYCConfigSDK(
587
689
  appKey = appKeyConfig,
588
690
  optionConfig = optionConfig.copy(language = if (language == "en") "en" else "vi"),
@@ -596,14 +698,31 @@ class EKYCModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaMo
596
698
  activity = currentActivity,
597
699
  ekycConfigSDK = ekycConfigSDK,
598
700
  callbackSuccess = { event, data ->
599
- Log.d(TAG, "✅ startEkycUI() success")
600
- val (eventMap, promiseMap) = createSeparateMaps { map ->
601
- map.putString("status", "success")
602
- map.putString("event", event.name.toString())
603
- map.putString("data", data.toString())
701
+ Log.d(TAG, "✅ startEkycUI() callback - event=${event.name}")
702
+ val result = data as? SDKEkycResult
703
+ val hasRealData = result?.ekycStateModel != null
704
+ // Chỉ resolve promise khi flow hoàn thành có data (SDK_END_SUCCESS); SDK_START_SUCCESS = activity vừa mở, bỏ qua
705
+ if (event == EKYCEvent.SDK_START_SUCCESS && !hasRealData) {
706
+ Log.d(TAG, "⏳ startEkycUI() SDK_START_SUCCESS – chờ SDK_END_SUCCESS")
707
+ sendEvent("onEkycUISuccess", Arguments.createMap().apply {
708
+ putString("status", "started")
709
+ putString("event", event.name.toString())
710
+ })
711
+ } else {
712
+ val ekycFiles = result?.ekycStateModel?.eKYCFileModel
713
+ val ekycTransactionId = result?.ekycStateModel?.transactionId ?: ""
714
+ val (eventMap, promiseMap) = createSeparateMaps { map ->
715
+ map.putString("status", "success")
716
+ map.putString("event", event.name.toString())
717
+ map.putString("data", data.toString())
718
+ map.putString("transactionId", ekycTransactionId)
719
+ ekycFiles?.imageFace?.absolutePath?.let { map.putString("imageFacePath", it) }
720
+ ekycFiles?.imageOcrFront?.absolutePath?.let { map.putString("imageOcrFrontPath", it) }
721
+ ekycFiles?.imageOcrBack?.absolutePath?.let { map.putString("imageOcrBackPath", it) }
722
+ }
723
+ sendEvent("onEkycUISuccess", eventMap)
724
+ promise.resolve(promiseMap)
604
725
  }
605
- sendEvent("onEkycUISuccess", eventMap)
606
- promise.resolve(promiseMap)
607
726
  },
608
727
  callbackError = { event, errorResult ->
609
728
  Log.e(TAG, "❌ startEkycUI() failed - Event: $event, Message: ${errorResult.message}")
@@ -703,6 +822,37 @@ class EKYCModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaMo
703
822
  }
704
823
  }
705
824
 
825
+ /** Parse customActions array from optionConfig JSON. Maps như SDKeKYCActivity getSelectedCustomActions. */
826
+ private fun parseCustomActionsFromJson(optionConfigJson: String): List<SDKFaceDetectStatus>? {
827
+ return try {
828
+ val obj = JSONObject(optionConfigJson)
829
+ val arr = obj.optJSONArray("customActions") ?: return null
830
+ if (arr.length() == 0) return null
831
+ val actions = mutableListOf<SDKFaceDetectStatus>()
832
+ for (i in 0 until arr.length()) {
833
+ val s = arr.optString(i, "")
834
+ when (s) {
835
+ "LEFT" -> actions.add(SDKFaceDetectStatus.LEFT)
836
+ "RIGHT" -> actions.add(SDKFaceDetectStatus.RIGHT)
837
+ "UP" -> actions.add(SDKFaceDetectStatus.UP)
838
+ "DOWN" -> actions.add(SDKFaceDetectStatus.DOWN)
839
+ "SMILE" -> actions.add(SDKFaceDetectStatus.SMILE)
840
+ "BLINK" -> actions.add(SDKFaceDetectStatus.BLINK)
841
+ "TILT_LEFT" -> actions.add(SDKFaceDetectStatus.TILT_LEFT)
842
+ "TILT_RIGHT" -> actions.add(SDKFaceDetectStatus.TILT_RIGHT)
843
+ "WINK_LEFT" -> actions.add(SDKFaceDetectStatus.WINK_LEFT)
844
+ "WINK_RIGHT" -> actions.add(SDKFaceDetectStatus.WINK_RIGHT)
845
+ "STRAIGHT" -> actions.add(SDKFaceDetectStatus.STRAIGHT)
846
+ else -> if (s.isNotEmpty()) Log.w(TAG, "parseCustomActionsFromJson: unknown action=$s")
847
+ }
848
+ }
849
+ if (actions.isNotEmpty()) actions else null
850
+ } catch (e: Exception) {
851
+ Log.w(TAG, "parseCustomActionsFromJson: ${e.message}")
852
+ null
853
+ }
854
+ }
855
+
706
856
  private fun parseAppKeyConfig(appKeyConfigJson: String): AppKeyConfig {
707
857
  return try {
708
858
  if (appKeyConfigJson.isBlank() || appKeyConfigJson == "{}") {
@@ -1019,14 +1169,26 @@ class EKYCModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaMo
1019
1169
  promise.resolve(promiseMap)
1020
1170
  },
1021
1171
  callbackError = { event, error ->
1022
- Log.e(TAG, "❌ initializeESign() failed - Event: $event, Message: ${error.message}")
1023
- val errorMap = Arguments.createMap().apply {
1024
- putString("event", event.name.toString())
1025
- putString("message", error.message ?: "")
1026
- putString("code", error.code.toString())
1172
+ val msg = (error.message ?: "").lowercase()
1173
+ // eSign đã được khởi tạo → coi là thành công, cho icon pass xanh / text xanh
1174
+ if (msg.contains("đã được khởi tạo") || msg.contains("already") || msg.contains("already initialized")) {
1175
+ Log.d(TAG, "✅ initializeESign() – eSign đã khởi tạo, trả success (không coi là lỗi)")
1176
+ val (eventMap, promiseMap) = createSeparateMaps { map ->
1177
+ map.putString("code", "0")
1178
+ map.putString("message", "eSign already initialized")
1179
+ }
1180
+ sendEvent("onESignInitSuccess", eventMap)
1181
+ promise.resolve(promiseMap)
1182
+ } else {
1183
+ Log.e(TAG, "❌ initializeESign() failed - Event: $event, Message: ${error.message}")
1184
+ val errorMap = Arguments.createMap().apply {
1185
+ putString("event", event.name.toString())
1186
+ putString("message", error.message ?: "")
1187
+ putString("code", error.code.toString())
1188
+ }
1189
+ sendEvent("onESignError", errorMap)
1190
+ promise.reject(event.name.toString(), error.message ?: "Unknown Error")
1027
1191
  }
1028
- sendEvent("onESignError", errorMap)
1029
- promise.reject(event.name.toString(), error.message ?: "Unknown Error")
1030
1192
  }
1031
1193
  )
1032
1194
  } catch (e: Exception) {
@@ -1053,9 +1215,14 @@ class EKYCModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaMo
1053
1215
  promise.resolve(token)
1054
1216
  },
1055
1217
  callbackError = { event, error ->
1056
- Log.e(TAG, "❌ getSdkToken() failed: $error")
1057
- // error might be String or Object, toString() covers both
1058
- promise.reject("GET_SDK_TOKEN_ERROR", error.toString())
1218
+ Log.e(TAG, "❌ getSdkToken() failed - Event: $event, Message: ${error.message}")
1219
+ val errorMap = Arguments.createMap().apply {
1220
+ putString("event", event.name.toString())
1221
+ putString("message", error.message ?: "")
1222
+ putString("code", error.code.toString())
1223
+ }
1224
+ sendEvent("onESignError", errorMap)
1225
+ promise.reject(event.name.toString(), error.message ?: "Get SDK Token failed")
1059
1226
  }
1060
1227
  )
1061
1228
  } catch (e: Exception) {
@@ -1420,26 +1587,14 @@ class EKYCModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaMo
1420
1587
  callbackError = { event, error ->
1421
1588
  val errorMessage = error.message ?: ""
1422
1589
  val errorCode = error.code.toString()
1423
-
1424
- // Check if message contains "thành công" or code == "0" -> treat as success
1425
- if (errorMessage.contains("thành công", ignoreCase = true) || errorCode == "0") {
1426
- Log.d(TAG, "✅ registerRemoteSigning() success (async operation) - Message: $errorMessage")
1427
- val (eventMap, promiseMap) = createSeparateMaps { map ->
1428
- map.putString("response", "{\"status\":1,\"msg\":\"$errorMessage\"}")
1429
- map.putString("message", errorMessage)
1430
- }
1431
- sendEvent("onESignRegisterRemoteSigningSuccess", eventMap)
1432
- promise.resolve(promiseMap)
1433
- } else {
1434
- Log.e(TAG, "❌ registerRemoteSigning() failed - Event: $event, Message: $errorMessage")
1435
- val errorMap = Arguments.createMap().apply {
1436
- putString("event", event.name.toString())
1437
- putString("message", errorMessage)
1438
- putString("code", errorCode)
1439
- }
1440
- sendEvent("onESignError", errorMap)
1441
- promise.reject(event.name.toString(), errorMessage)
1590
+ Log.e(TAG, "❌ registerRemoteSigning() failed - Event: $event, Message: $errorMessage")
1591
+ val errorMap = Arguments.createMap().apply {
1592
+ putString("event", event.name.toString())
1593
+ putString("message", errorMessage)
1594
+ putString("code", errorCode)
1442
1595
  }
1596
+ sendEvent("onESignError", errorMap)
1597
+ promise.reject(event.name.toString(), errorMessage)
1443
1598
  }
1444
1599
  )
1445
1600
  } catch (e: Exception) {
@@ -1448,6 +1603,48 @@ class EKYCModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaMo
1448
1603
  }
1449
1604
  }
1450
1605
 
1606
+ /**
1607
+ * Composite API: Register Remote Signing + Send Confirmation Document
1608
+ * Align với SdkeSignImpl.registerAndConfirm: gọi registerRemoteSigning -> parse sessionId -> sendConfirmationDocument
1609
+ */
1610
+ @ReactMethod
1611
+ fun registerAndConfirm(
1612
+ requestJson: String,
1613
+ confirmationDocBase64: String,
1614
+ promise: Promise
1615
+ ) {
1616
+ Log.d(TAG, "▶️ registerAndConfirm() called")
1617
+ try {
1618
+ SdkeSign.registerAndConfirm(
1619
+ requestJson = requestJson,
1620
+ confirmationDocBase64 = confirmationDocBase64,
1621
+ callbackSuccess = { rawResponse ->
1622
+ Log.d(TAG, "✅ registerAndConfirm() success")
1623
+ val (eventMap, promiseMap) = createSeparateMaps { map ->
1624
+ map.putString("response", rawResponse)
1625
+ }
1626
+ sendEvent("onESignRegisterAndConfirmSuccess", eventMap)
1627
+ promise.resolve(promiseMap)
1628
+ },
1629
+ callbackError = { event, error ->
1630
+ val errorMessage = error.message ?: ""
1631
+ val errorCode = error.code.toString()
1632
+ Log.e(TAG, "❌ registerAndConfirm() failed - Event: $event, Message: $errorMessage")
1633
+ val errorMap = Arguments.createMap().apply {
1634
+ putString("event", event.name.toString())
1635
+ putString("message", errorMessage)
1636
+ putString("code", errorCode)
1637
+ }
1638
+ sendEvent("onESignError", errorMap)
1639
+ promise.reject(event.name.toString(), errorMessage)
1640
+ }
1641
+ )
1642
+ } catch (e: Exception) {
1643
+ Log.e(TAG, "❌ registerAndConfirm() exception: ${e.message}", e)
1644
+ promise.reject("ESIGN_EXCEPTION", e.message, e)
1645
+ }
1646
+ }
1647
+
1451
1648
  @ReactMethod
1452
1649
  fun signPdf(
1453
1650
  requestJson: String,
@@ -1514,25 +1711,13 @@ class EKYCModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaMo
1514
1711
  callbackError = { event, error ->
1515
1712
  val errorMessage = error.message ?: ""
1516
1713
  val errorCode = error.code.toString()
1517
-
1518
- // Check if message contains "thành công" or code == "0" -> treat as success
1519
- if (errorMessage.contains("thành công", ignoreCase = true) || errorCode == "0") {
1520
- Log.d(TAG, "✅ sendConfirmationDocument() success (async operation) - Message: $errorMessage")
1521
- val (eventMap, promiseMap) = createSeparateMaps { map ->
1522
- map.putString("response", "{\"status\":1,\"msg\":\"$errorMessage\"}")
1523
- map.putString("message", errorMessage)
1524
- }
1525
- sendEvent("onESignSendConfirmationDocumentSuccess", eventMap)
1526
- promise.resolve(promiseMap)
1527
- } else {
1528
- val errorMap = Arguments.createMap().apply {
1529
- putString("event", event.name.toString())
1530
- putString("message", errorMessage)
1531
- putString("code", errorCode)
1532
- }
1533
- sendEvent("onESignError", errorMap)
1534
- promise.reject(event.name.toString(), errorMessage)
1714
+ val errorMap = Arguments.createMap().apply {
1715
+ putString("event", event.name.toString())
1716
+ putString("message", errorMessage)
1717
+ putString("code", errorCode)
1535
1718
  }
1719
+ sendEvent("onESignError", errorMap)
1720
+ promise.reject(event.name.toString(), errorMessage)
1536
1721
  }
1537
1722
  )
1538
1723
  } catch (e: Exception) {
@@ -1,6 +1,6 @@
1
1
  import { EmitterSubscription } from 'react-native';
2
2
  import { NfcConfig, NfcError } from './src/types/ekycNFCType';
3
- import { SDKEkycResultWithEvent, SDKEkycResultStringWithEvent } from './src/types/ekycType';
3
+ import { SDKEkycResultWithEvent, SDKEkycResultStringWithEvent, StartEkycUIResult } from './src/types/ekycType';
4
4
  import { C06Config } from './src/types/ekycC06Type';
5
5
  import { OcrConfig } from './src/types/ekycOCRType';
6
6
  import { LivenessConfig, SDKFaceDetectStatus } from './src/types/ekycLivenessType';
@@ -82,6 +82,13 @@ declare class SDKeKYC {
82
82
  sendConfirmationDocument(requestJson: string): Promise<{
83
83
  response: string;
84
84
  }>;
85
+ /**
86
+ * Composite API: Register Remote Signing + Send Confirmation Document
87
+ * Align với SdkeSignImpl.registerAndConfirm
88
+ */
89
+ registerAndConfirm(requestJson: string, confirmationDocBase64: string): Promise<{
90
+ response: string;
91
+ }>;
85
92
  onESignInitSuccess(callback: (data: ESignInitResult) => void): EmitterSubscription | null;
86
93
  onESignOpenSessionSuccess(callback: (data: ESignOpenSessionResult) => void): EmitterSubscription | null;
87
94
  onESignRegisterDeviceSuccess(callback: (data: {
@@ -111,7 +118,15 @@ declare class SDKeKYC {
111
118
  onESignSendConfirmationDocumentSuccess(callback: (data: {
112
119
  response: string;
113
120
  }) => void): EmitterSubscription | null;
121
+ onESignRegisterAndConfirmSuccess(callback: (data: {
122
+ response: string;
123
+ }) => void): EmitterSubscription | null;
114
124
  onESignError(callback: (error: ESignError) => void): EmitterSubscription | null;
125
+ /**
126
+ * Start eKYC UI flow. On success, result contains the same data as Android:
127
+ * data.ekycStateModel.eKYCFileModel.imageFace → result.imageFace (base64),
128
+ * result.imageFacePath (file path), result.imageOcrFront / imageOcrBack, result.transactionId.
129
+ */
115
130
  startEkycUI(appKey: string, flowSDK: string[], language: string, transactionId: string, appKeyConfig: {
116
131
  appKey: string;
117
132
  appKeyNfc: string;
@@ -159,7 +174,7 @@ declare class SDKeKYC {
159
174
  textFont?: string;
160
175
  textColor?: number;
161
176
  };
162
- }): Promise<any>;
177
+ }): Promise<StartEkycUIResult>;
163
178
  }
164
179
  declare const sdkEKYC: SDKeKYC;
165
180
  export { SDKeKYC };
@@ -221,10 +221,12 @@ class SDKeKYC {
221
221
  }
222
222
  async startFaceCompare(config) {
223
223
  var _a;
224
- this.validateConfig(config, ['appKey', 'selfieImage', 'idImage']);
224
+ // Support both `appKey` and docs' `appKeyFaceService`
225
+ const normalized = Object.assign(Object.assign({}, config), { appKey: (config.appKeyFaceService || config.appKey) });
226
+ this.validateConfig(normalized, ['appKey', 'selfieImage', 'idImage']);
225
227
  const nativeModule = this.ensureNativeModule();
226
228
  try {
227
- return await nativeModule.startFaceCompare(config.appKey, (_a = config.transactionId) !== null && _a !== void 0 ? _a : '', config.selfieImage, config.idImage);
229
+ return await nativeModule.startFaceCompare(normalized.appKey, (_a = normalized.transactionId) !== null && _a !== void 0 ? _a : '', normalized.selfieImage, normalized.idImage);
228
230
  }
229
231
  catch (error) {
230
232
  console.error('Face Compare Error:', error);
@@ -700,6 +702,21 @@ class SDKeKYC {
700
702
  throw error;
701
703
  }
702
704
  }
705
+ /**
706
+ * Composite API: Register Remote Signing + Send Confirmation Document
707
+ * Align với SdkeSignImpl.registerAndConfirm
708
+ */
709
+ async registerAndConfirm(requestJson, confirmationDocBase64) {
710
+ const nativeModule = this.ensureNativeModule();
711
+ try {
712
+ const result = await nativeModule.registerAndConfirm(requestJson, confirmationDocBase64);
713
+ return result;
714
+ }
715
+ catch (error) {
716
+ console.error('eSign Register And Confirm Error:', error);
717
+ throw error;
718
+ }
719
+ }
703
720
  // eSign Event Listeners
704
721
  onESignInitSuccess(callback) {
705
722
  try {
@@ -861,6 +878,22 @@ class SDKeKYC {
861
878
  return null;
862
879
  }
863
880
  }
881
+ onESignRegisterAndConfirmSuccess(callback) {
882
+ try {
883
+ const emitter = this.ensureEventEmitter();
884
+ if (!emitter) {
885
+ console.error('❌ Event emitter not available for onESignRegisterAndConfirmSuccess');
886
+ return null;
887
+ }
888
+ const listener = emitter.addListener('onESignRegisterAndConfirmSuccess', callback);
889
+ this.listeners.push(listener);
890
+ return listener;
891
+ }
892
+ catch (error) {
893
+ console.error('Failed to add onESignRegisterAndConfirmSuccess listener:', error);
894
+ return null;
895
+ }
896
+ }
864
897
  onESignError(callback) {
865
898
  try {
866
899
  const emitter = this.ensureEventEmitter();
@@ -877,7 +910,11 @@ class SDKeKYC {
877
910
  return null;
878
911
  }
879
912
  }
880
- // SdkEkycUI method
913
+ /**
914
+ * Start eKYC UI flow. On success, result contains the same data as Android:
915
+ * data.ekycStateModel.eKYCFileModel.imageFace → result.imageFace (base64),
916
+ * result.imageFacePath (file path), result.imageOcrFront / imageOcrBack, result.transactionId.
917
+ */
881
918
  async startEkycUI(appKey, flowSDK, language, transactionId, appKeyConfig, optionConfig, styleConfig) {
882
919
  if (!this.isInitialized) {
883
920
  throw new Error('SDK is not initialized. Please call initSdkEkyc() first.');
@@ -892,7 +929,7 @@ class SDKeKYC {
892
929
  const appKeyConfigJson = JSON.stringify(appKeyConfig);
893
930
  const styleConfigJson = styleConfig ? JSON.stringify(styleConfig) : '{}';
894
931
  const nativeModule = this.ensureNativeModule();
895
- const result = await nativeModule.startEkycUI(appKey, flowSDKJson, language, transactionId, optionConfigJson, appKeyConfigJson, styleConfigJson);
932
+ const result = (await nativeModule.startEkycUI(appKey, flowSDKJson, language, transactionId, optionConfigJson, appKeyConfigJson, styleConfigJson));
896
933
  console.log('✅ SdkEkycUI started successfully');
897
934
  return result;
898
935
  }