@finos_sdk/sdk-ekyc 1.4.6 → 1.4.8
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.
- package/README.md +1 -1
- package/android/build.gradle +1 -1
- package/android/src/main/java/finos/sdk/ekyc/EKYCModule.kt +627 -116
- package/android/src/main/java/finos/sdk/ekyc/SDKeKYCExitHandlerManager.kt +28 -0
- package/dist/EKYCModule.d.ts +11 -8
- package/dist/EKYCModule.js +58 -4
- package/dist/index.d.ts +5 -5
- package/dist/package.json +1 -1
- package/dist/src/modules/FinosEKYCModule.d.ts +38 -10
- package/dist/src/modules/FinosEKYCModule.js +75 -1
- package/dist/src/modules/FinosESignModule.d.ts +7 -7
- package/dist/src/types/ekycC06Type.d.ts +9 -0
- package/dist/src/types/ekycFaceType.d.ts +9 -0
- package/dist/src/types/ekycLivenessType.d.ts +9 -13
- package/dist/src/types/ekycNFCType.d.ts +10 -1
- package/dist/src/types/ekycOCRType.d.ts +9 -0
- package/dist/src/types/finos-ekyc.d.ts +1 -1
- package/dist/src/utils/utils.d.ts +2 -2
- package/dist/src/utils/utils.js +1 -1
- package/ios/EKYCModule.swift +27 -3
- package/ios/EKYCModuleBridge.m +8 -0
- package/package.json +1 -1
- package/src/modules/FinosEKYCModule.ts +91 -12
- package/src/modules/FinosESignModule.ts +6 -6
- package/src/types/ekycC06Type.ts +10 -0
- package/src/types/ekycFaceType.ts +10 -0
- package/src/types/ekycLivenessType.ts +10 -15
- package/src/types/ekycNFCType.ts +12 -2
- package/src/types/ekycOCRType.ts +11 -1
- package/src/types/finos-ekyc.ts +1 -1
- package/src/utils/utils.ts +3 -3
|
@@ -13,10 +13,14 @@ import com.facebook.react.bridge.ReactApplicationContext
|
|
|
13
13
|
import com.facebook.react.bridge.ReactContextBaseJavaModule
|
|
14
14
|
import com.facebook.react.bridge.ReactMethod
|
|
15
15
|
import com.facebook.react.bridge.ReadableArray
|
|
16
|
+
import com.facebook.react.bridge.ReadableMap
|
|
16
17
|
import com.facebook.react.bridge.WritableArray
|
|
17
18
|
import com.facebook.react.bridge.WritableMap
|
|
18
19
|
import com.facebook.react.module.annotations.ReactModule
|
|
19
20
|
import com.facebook.react.modules.core.DeviceEventManagerModule
|
|
21
|
+
import com.facebook.react.ReactRootView
|
|
22
|
+
import com.facebook.react.ReactApplication
|
|
23
|
+
import com.facebook.react.bridge.ReactContext
|
|
20
24
|
import finos.sdk.c06.eKYCFinOSC06
|
|
21
25
|
import finos.sdk.core.define.EKYCErrorResult
|
|
22
26
|
import finos.sdk.core.define.EKYCEvent
|
|
@@ -44,6 +48,15 @@ import finos.sdk.smsotp.OTPFinOS
|
|
|
44
48
|
import finos.sdk.core.model.sdk.config.SmsOtpConfig
|
|
45
49
|
import vn.softdreams.easyca.sdk.eSignFinOS
|
|
46
50
|
import vn.softdreams.easyca.sdk.esign.ESignModels
|
|
51
|
+
import com.google.android.material.bottomsheet.BottomSheetDialog
|
|
52
|
+
import android.view.LayoutInflater
|
|
53
|
+
import android.view.View
|
|
54
|
+
import android.widget.Button
|
|
55
|
+
import android.widget.TextView
|
|
56
|
+
import android.widget.LinearLayout
|
|
57
|
+
import android.view.Gravity
|
|
58
|
+
import android.graphics.Color
|
|
59
|
+
import android.util.TypedValue
|
|
47
60
|
|
|
48
61
|
import org.json.JSONArray
|
|
49
62
|
import org.json.JSONObject
|
|
@@ -52,6 +65,8 @@ import java.io.FileOutputStream
|
|
|
52
65
|
import java.io.FileInputStream
|
|
53
66
|
import java.io.InputStream
|
|
54
67
|
import java.util.Date
|
|
68
|
+
import android.app.Activity
|
|
69
|
+
import android.view.ViewGroup
|
|
55
70
|
|
|
56
71
|
@ReactModule(name = EKYCModule.NAME)
|
|
57
72
|
class EKYCModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext) {
|
|
@@ -63,6 +78,362 @@ class EKYCModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaMo
|
|
|
63
78
|
|
|
64
79
|
override fun getName(): String = NAME
|
|
65
80
|
|
|
81
|
+
/**
|
|
82
|
+
* Reference tới BottomSheetDialog hiện đang hiển thị.
|
|
83
|
+
* @Volatile: resolveExit() chạy trên JS bridge thread, đọc field này được viết bởi UI thread.
|
|
84
|
+
* Không có @Volatile → JVM cache → bridge thread thấy stale null → dismiss bị bỏ qua.
|
|
85
|
+
*/
|
|
86
|
+
@Volatile
|
|
87
|
+
private var currentExitSheet: BottomSheetDialog? = null
|
|
88
|
+
|
|
89
|
+
// Đánh dấu khi dismiss được trigger bởi resolveExit (button), không phải backdrop tap.
|
|
90
|
+
// @Volatile vì được set từ bridge thread, đọc từ UI thread trong setOnDismissListener.
|
|
91
|
+
@Volatile
|
|
92
|
+
private var exitSheetResolvedViaButton = false
|
|
93
|
+
|
|
94
|
+
private fun getTrueCurrentActivity(): Activity? {
|
|
95
|
+
// Try React Native's currentActivity first
|
|
96
|
+
val rnActivity = currentActivity
|
|
97
|
+
if (rnActivity != null && rnActivity.javaClass.simpleName != "MainActivity") {
|
|
98
|
+
return rnActivity
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// If RN gives us MainActivity or null, but we know an SDK is showing,
|
|
102
|
+
// we must find the actual top activity via reflection or ActivityThread
|
|
103
|
+
try {
|
|
104
|
+
val activityThreadClass = Class.forName("android.app.ActivityThread")
|
|
105
|
+
val activityThread = activityThreadClass.getMethod("currentActivityThread").invoke(null)
|
|
106
|
+
val activitiesField = activityThreadClass.getDeclaredField("mActivities")
|
|
107
|
+
activitiesField.isAccessible = true
|
|
108
|
+
|
|
109
|
+
val activities = activitiesField.get(activityThread) as Map<Any, Any>
|
|
110
|
+
Log.d(TAG, "🔍 Scanning ${activities.size} activities...")
|
|
111
|
+
|
|
112
|
+
var topActivity: Activity? = null
|
|
113
|
+
|
|
114
|
+
for (activityRecord in activities.values) {
|
|
115
|
+
val activityRecordClass = activityRecord.javaClass
|
|
116
|
+
|
|
117
|
+
val activityField = activityRecordClass.getDeclaredField("activity")
|
|
118
|
+
activityField.isAccessible = true
|
|
119
|
+
val activity = activityField.get(activityRecord) as Activity
|
|
120
|
+
|
|
121
|
+
val pausedField = activityRecordClass.getDeclaredField("paused")
|
|
122
|
+
pausedField.isAccessible = true
|
|
123
|
+
val isPaused = pausedField.get(activityRecord) as Boolean
|
|
124
|
+
|
|
125
|
+
val stoppedField = activityRecordClass.getDeclaredField("stopped")
|
|
126
|
+
stoppedField.isAccessible = true
|
|
127
|
+
val isStopped = stoppedField.get(activityRecord) as Boolean
|
|
128
|
+
|
|
129
|
+
Log.d(TAG, " - Found: ${activity.javaClass.simpleName} (Paused: $isPaused, Stopped: $isStopped, Finishing: ${activity.isFinishing})")
|
|
130
|
+
|
|
131
|
+
if (!activity.isFinishing && !isStopped) {
|
|
132
|
+
topActivity = activity
|
|
133
|
+
if (!isPaused) break // Found the resumed one
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return topActivity ?: rnActivity
|
|
137
|
+
} catch (e: Exception) {
|
|
138
|
+
Log.e(TAG, "Error finding true current activity", e)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return rnActivity
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
@ReactMethod
|
|
145
|
+
fun showRNExitSheet(bundleName: String, initialProps: ReadableMap, promise: Promise) {
|
|
146
|
+
val activity = getTrueCurrentActivity() ?: run {
|
|
147
|
+
Log.e(TAG, "❌ showRNExitSheet failed: No visible activity found")
|
|
148
|
+
promise.reject("NO_ACTIVITY", "No visible activity found")
|
|
149
|
+
return
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (activity.isFinishing || activity.isDestroyed) {
|
|
153
|
+
Log.e(TAG, "❌ showRNExitSheet failed: Activity is finishing/destroyed")
|
|
154
|
+
promise.reject("ACTIVITY_INVALID", "Activity is finishing or destroyed")
|
|
155
|
+
return
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Guard chống double-resolve/reject promise (vì có nhiều entry point: sync error, async listener, exception)
|
|
159
|
+
val promiseSettled = java.util.concurrent.atomic.AtomicBoolean(false)
|
|
160
|
+
fun safeResolve(value: Any?) {
|
|
161
|
+
if (promiseSettled.compareAndSet(false, true)) promise.resolve(value)
|
|
162
|
+
}
|
|
163
|
+
fun safeReject(code: String, msg: String, e: Throwable? = null) {
|
|
164
|
+
if (promiseSettled.compareAndSet(false, true)) {
|
|
165
|
+
if (e != null) promise.reject(code, msg, e) else promise.reject(code, msg)
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
activity.runOnUiThread {
|
|
170
|
+
try {
|
|
171
|
+
val activityName = activity.javaClass.simpleName
|
|
172
|
+
Log.d(TAG, "▶️ showRNExitSheet: Top Activity=$activityName, bundle=$bundleName")
|
|
173
|
+
|
|
174
|
+
val reactApplication = activity.application as? ReactApplication
|
|
175
|
+
if (reactApplication == null) {
|
|
176
|
+
Log.e(TAG, "❌ Application does not implement ReactApplication")
|
|
177
|
+
safeReject("NO_REACT_APP", "Application does not implement ReactApplication")
|
|
178
|
+
return@runOnUiThread
|
|
179
|
+
}
|
|
180
|
+
val reactInstanceManager = reactApplication.reactNativeHost.reactInstanceManager
|
|
181
|
+
|
|
182
|
+
if (reactInstanceManager.currentReactContext != null) {
|
|
183
|
+
presentRNBottomSheet(activity, reactInstanceManager, bundleName, initialProps,
|
|
184
|
+
::safeResolve, ::safeReject)
|
|
185
|
+
return@runOnUiThread
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Context chưa sẵn sàng: chờ listener
|
|
189
|
+
Log.w(TAG, "⚠️ ReactContext chưa ready, chờ init…")
|
|
190
|
+
val listener = object : com.facebook.react.ReactInstanceEventListener {
|
|
191
|
+
override fun onReactContextInitialized(context: ReactContext) {
|
|
192
|
+
reactInstanceManager.removeReactInstanceEventListener(this)
|
|
193
|
+
activity.runOnUiThread {
|
|
194
|
+
// Re-validate activity vì có thể đã destroy trong lúc chờ
|
|
195
|
+
if (activity.isFinishing || activity.isDestroyed) {
|
|
196
|
+
Log.w(TAG, "⚠️ Activity đã destroy trong lúc chờ ReactContext")
|
|
197
|
+
safeReject("ACTIVITY_INVALID", "Activity destroyed while waiting for RN context")
|
|
198
|
+
return@runOnUiThread
|
|
199
|
+
}
|
|
200
|
+
presentRNBottomSheet(activity, reactInstanceManager, bundleName, initialProps,
|
|
201
|
+
::safeResolve, ::safeReject)
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
reactInstanceManager.addReactInstanceEventListener(listener)
|
|
206
|
+
|
|
207
|
+
// FIX RACE: re-check sau khi add listener vì context có thể đã init
|
|
208
|
+
// giữa null-check ở trên và lúc add listener (listener không fire cho event đã xảy ra)
|
|
209
|
+
val ctxAfterAdd = reactInstanceManager.currentReactContext
|
|
210
|
+
if (ctxAfterAdd != null) {
|
|
211
|
+
reactInstanceManager.removeReactInstanceEventListener(listener)
|
|
212
|
+
Log.d(TAG, "✅ ReactContext init xong giữa lúc add listener — present luôn")
|
|
213
|
+
presentRNBottomSheet(activity, reactInstanceManager, bundleName, initialProps,
|
|
214
|
+
::safeResolve, ::safeReject)
|
|
215
|
+
return@runOnUiThread
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Chỉ trigger background create nếu chưa start (tránh IllegalStateException)
|
|
219
|
+
if (!reactInstanceManager.hasStartedCreatingInitialContext()) {
|
|
220
|
+
try {
|
|
221
|
+
reactInstanceManager.createReactContextInBackground()
|
|
222
|
+
} catch (e: Exception) {
|
|
223
|
+
reactInstanceManager.removeReactInstanceEventListener(listener)
|
|
224
|
+
Log.e(TAG, "❌ Failed createReactContextInBackground", e)
|
|
225
|
+
safeReject("RN_CONTEXT_INIT_FAILED", e.message ?: "createReactContextInBackground failed", e)
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
} catch (e: Exception) {
|
|
229
|
+
Log.e(TAG, "❌ Exception in showRNExitSheet", e)
|
|
230
|
+
safeReject("RN_SHEET_ERROR", e.message ?: "unknown", e)
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Hiển thị BottomSheetDialog chứa ReactRootView. Phải gọi trên UI thread.
|
|
237
|
+
*
|
|
238
|
+
* Quan trọng:
|
|
239
|
+
* - Set BottomSheetBehavior.peekHeight = sheetHeight và state = STATE_EXPANDED
|
|
240
|
+
* để sheet xuất hiện đầy đủ (mặc định peek nhỏ → sheet "không hiển thị" cảm giác).
|
|
241
|
+
* - Auto-dismiss khi activity host bị detach (memory leak guard).
|
|
242
|
+
* - safeResolve/safeReject để tránh promise resolve/reject 2 lần.
|
|
243
|
+
*/
|
|
244
|
+
private fun presentRNBottomSheet(
|
|
245
|
+
activity: Activity,
|
|
246
|
+
reactInstanceManager: com.facebook.react.ReactInstanceManager,
|
|
247
|
+
bundleName: String,
|
|
248
|
+
initialProps: ReadableMap,
|
|
249
|
+
safeResolve: (Any?) -> Unit,
|
|
250
|
+
safeReject: (String, String, Throwable?) -> Unit
|
|
251
|
+
) {
|
|
252
|
+
try {
|
|
253
|
+
val container = LinearLayout(activity).apply {
|
|
254
|
+
orientation = LinearLayout.VERTICAL
|
|
255
|
+
setBackgroundColor(Color.TRANSPARENT)
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
val rootView = ReactRootView(activity)
|
|
259
|
+
rootView.layoutParams = LinearLayout.LayoutParams(
|
|
260
|
+
LinearLayout.LayoutParams.MATCH_PARENT,
|
|
261
|
+
LinearLayout.LayoutParams.WRAP_CONTENT
|
|
262
|
+
)
|
|
263
|
+
container.addView(rootView)
|
|
264
|
+
|
|
265
|
+
// Bug 3 guard: nếu có sheet cũ chưa dismiss, dismiss nó trước để tránh leak
|
|
266
|
+
currentExitSheet?.let { old ->
|
|
267
|
+
if (old.isShowing) {
|
|
268
|
+
try { old.dismiss() } catch (e: Exception) {
|
|
269
|
+
Log.w(TAG, "presentRNBottomSheet: error dismissing old sheet: ${e.message}")
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
val bottomSheetDialog = BottomSheetDialog(activity)
|
|
275
|
+
currentExitSheet = bottomSheetDialog
|
|
276
|
+
bottomSheetDialog.setContentView(container)
|
|
277
|
+
bottomSheetDialog.setCancelable(true)
|
|
278
|
+
bottomSheetDialog.setCanceledOnTouchOutside(true)
|
|
279
|
+
|
|
280
|
+
// isFitToContents=true → sheet height = wrap_content của ReactRootView,
|
|
281
|
+
// không expand fullscreen. setExitSheetHeight() từ JS sẽ fine-tune nếu cần.
|
|
282
|
+
bottomSheetDialog.setOnShowListener { dlg ->
|
|
283
|
+
val d = dlg as BottomSheetDialog
|
|
284
|
+
val bs = d.findViewById<View>(com.google.android.material.R.id.design_bottom_sheet)
|
|
285
|
+
?: return@setOnShowListener
|
|
286
|
+
val behavior = com.google.android.material.bottomsheet.BottomSheetBehavior.from(bs)
|
|
287
|
+
behavior.isFitToContents = true
|
|
288
|
+
behavior.skipCollapsed = true
|
|
289
|
+
behavior.state = com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_EXPANDED
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// Auto-dismiss & unmount khi activity decor view bị detach (activity destroy)
|
|
293
|
+
val decorView = activity.window.decorView
|
|
294
|
+
val attachListener = object : View.OnAttachStateChangeListener {
|
|
295
|
+
override fun onViewAttachedToWindow(v: View) {}
|
|
296
|
+
override fun onViewDetachedFromWindow(v: View) {
|
|
297
|
+
decorView.removeOnAttachStateChangeListener(this)
|
|
298
|
+
if (bottomSheetDialog.isShowing) {
|
|
299
|
+
try { bottomSheetDialog.dismiss() } catch (_: Exception) {}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
decorView.addOnAttachStateChangeListener(attachListener)
|
|
304
|
+
|
|
305
|
+
bottomSheetDialog.setOnDismissListener {
|
|
306
|
+
currentExitSheet = null
|
|
307
|
+
decorView.removeOnAttachStateChangeListener(attachListener)
|
|
308
|
+
// Nếu dismiss KHÔNG phải từ button (backdrop tap / activity destroy),
|
|
309
|
+
// tự resolve CANCEL để SDK không bị kẹt và lần sau back vẫn trigger sheet lại.
|
|
310
|
+
if (!exitSheetResolvedViaButton && SDKeKYCExitHandlerManager.pendingCancel != null) {
|
|
311
|
+
SDKeKYCExitHandlerManager.resolve("CANCEL")
|
|
312
|
+
Log.d(TAG, "🔙 Sheet dismissed by backdrop — auto resolved CANCEL")
|
|
313
|
+
}
|
|
314
|
+
try {
|
|
315
|
+
rootView.unmountReactApplication()
|
|
316
|
+
Log.d(TAG, "🧹 RN sheet dismissed, root unmounted")
|
|
317
|
+
} catch (e: Exception) {
|
|
318
|
+
Log.w(TAG, "Error unmounting ReactRootView: ${e.message}")
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// Start RN trước show — view sẽ measure khi attached vào dialog window
|
|
323
|
+
rootView.startReactApplication(
|
|
324
|
+
reactInstanceManager,
|
|
325
|
+
bundleName,
|
|
326
|
+
Arguments.toBundle(initialProps)
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
bottomSheetDialog.show()
|
|
330
|
+
|
|
331
|
+
Log.d(TAG, "✅ showRNExitSheet: BottomSheetDialog shown (wrap content), bundle=$bundleName")
|
|
332
|
+
safeResolve(true)
|
|
333
|
+
} catch (e: Exception) {
|
|
334
|
+
Log.e(TAG, "❌ Exception in presentRNBottomSheet", e)
|
|
335
|
+
safeReject("RN_SHEET_ERROR", e.message ?: "unknown", e)
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
@ReactMethod
|
|
340
|
+
fun showNativeExitDialog(config: ReadableMap, promise: Promise) {
|
|
341
|
+
val activity = currentActivity ?: run {
|
|
342
|
+
promise.reject("NO_ACTIVITY", "Current activity is null")
|
|
343
|
+
return
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
val title = if (config.hasKey("title")) config.getString("title") else "Xác nhận thoát"
|
|
347
|
+
val message = if (config.hasKey("message")) config.getString("message") else "Bạn có chắc chắn muốn thoát khỏi quá trình eKYC?"
|
|
348
|
+
val confirmText = if (config.hasKey("confirmText")) config.getString("confirmText") else "Thoát ra"
|
|
349
|
+
val cancelText = if (config.hasKey("cancelText")) config.getString("cancelText") else "Hủy bỏ"
|
|
350
|
+
|
|
351
|
+
activity.runOnUiThread {
|
|
352
|
+
try {
|
|
353
|
+
val bottomSheetDialog = BottomSheetDialog(activity)
|
|
354
|
+
|
|
355
|
+
// Create a simple programmatic layout for the Bottom Sheet
|
|
356
|
+
val context = activity
|
|
357
|
+
val root = LinearLayout(context).apply {
|
|
358
|
+
orientation = LinearLayout.VERTICAL
|
|
359
|
+
setPadding(60, 40, 60, 80)
|
|
360
|
+
setBackgroundColor(Color.WHITE)
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// Title
|
|
364
|
+
val titleView = TextView(context).apply {
|
|
365
|
+
text = title
|
|
366
|
+
textSize = 20f
|
|
367
|
+
setTextColor(Color.BLACK)
|
|
368
|
+
setTypeface(null, android.graphics.Typeface.BOLD)
|
|
369
|
+
gravity = Gravity.CENTER
|
|
370
|
+
setPadding(0, 0, 0, 20)
|
|
371
|
+
}
|
|
372
|
+
root.addView(titleView)
|
|
373
|
+
|
|
374
|
+
// Message
|
|
375
|
+
val messageView = TextView(context).apply {
|
|
376
|
+
text = message
|
|
377
|
+
textSize = 16f
|
|
378
|
+
setTextColor(Color.DKGRAY)
|
|
379
|
+
gravity = Gravity.CENTER
|
|
380
|
+
setPadding(0, 0, 0, 60)
|
|
381
|
+
}
|
|
382
|
+
root.addView(messageView)
|
|
383
|
+
|
|
384
|
+
// Buttons Container
|
|
385
|
+
val buttonsContainer = LinearLayout(context).apply {
|
|
386
|
+
orientation = LinearLayout.HORIZONTAL
|
|
387
|
+
layoutParams = LinearLayout.LayoutParams(
|
|
388
|
+
LinearLayout.LayoutParams.MATCH_PARENT,
|
|
389
|
+
LinearLayout.LayoutParams.WRAP_CONTENT
|
|
390
|
+
)
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// Cancel Button
|
|
394
|
+
val btnCancel = Button(context).apply {
|
|
395
|
+
text = cancelText
|
|
396
|
+
layoutParams = LinearLayout.LayoutParams(0, 140, 1f).apply {
|
|
397
|
+
setMargins(0, 0, 10, 0)
|
|
398
|
+
}
|
|
399
|
+
setBackgroundColor(Color.parseColor("#F2F2F7"))
|
|
400
|
+
setTextColor(Color.BLACK)
|
|
401
|
+
setOnClickListener {
|
|
402
|
+
SDKeKYCExitHandlerManager.resolve("CANCEL")
|
|
403
|
+
bottomSheetDialog.dismiss()
|
|
404
|
+
promise.resolve("CANCEL")
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
buttonsContainer.addView(btnCancel)
|
|
408
|
+
|
|
409
|
+
// Confirm Button
|
|
410
|
+
val btnConfirm = Button(context).apply {
|
|
411
|
+
text = confirmText
|
|
412
|
+
layoutParams = LinearLayout.LayoutParams(0, 140, 1f).apply {
|
|
413
|
+
setMargins(10, 0, 0, 0)
|
|
414
|
+
}
|
|
415
|
+
setBackgroundColor(Color.parseColor("#FF3B30"))
|
|
416
|
+
setTextColor(Color.WHITE)
|
|
417
|
+
setOnClickListener {
|
|
418
|
+
SDKeKYCExitHandlerManager.resolve("CONFIRM")
|
|
419
|
+
bottomSheetDialog.dismiss()
|
|
420
|
+
promise.resolve("CONFIRM")
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
buttonsContainer.addView(btnConfirm)
|
|
424
|
+
|
|
425
|
+
root.addView(buttonsContainer)
|
|
426
|
+
bottomSheetDialog.setContentView(root)
|
|
427
|
+
bottomSheetDialog.setCancelable(false)
|
|
428
|
+
bottomSheetDialog.show()
|
|
429
|
+
|
|
430
|
+
} catch (e: Exception) {
|
|
431
|
+
Log.e(TAG, "❌ showNativeExitDialog error: ${e.message}")
|
|
432
|
+
promise.reject("DIALOG_ERROR", e.message)
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
66
437
|
private fun sendEvent(eventName: String, params: WritableMap?) {
|
|
67
438
|
reactApplicationContext
|
|
68
439
|
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
|
|
@@ -97,6 +468,69 @@ class EKYCModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaMo
|
|
|
97
468
|
return Pair(eventMap, promiseMap)
|
|
98
469
|
}
|
|
99
470
|
|
|
471
|
+
/**
|
|
472
|
+
* Convert snake_case (match_score) and kebab-case (to-be-reviewed) keys to camelCase.
|
|
473
|
+
* Native SDK responses use these formats via Gson @SerializedName, but TS API expects camelCase.
|
|
474
|
+
* Examples:
|
|
475
|
+
* - "match_score" → "matchScore"
|
|
476
|
+
* - "to-be-reviewed" → "toBeReviewed"
|
|
477
|
+
* - "ageRange" → "ageRange" (already camelCase, unchanged)
|
|
478
|
+
*/
|
|
479
|
+
private fun normalizeKeyToCamelCase(key: String): String {
|
|
480
|
+
if (!key.contains('_') && !key.contains('-')) return key
|
|
481
|
+
val parts = key.split('_', '-')
|
|
482
|
+
if (parts.isEmpty()) return key
|
|
483
|
+
val sb = StringBuilder(parts[0])
|
|
484
|
+
for (i in 1 until parts.size) {
|
|
485
|
+
val p = parts[i]
|
|
486
|
+
if (p.isEmpty()) continue
|
|
487
|
+
sb.append(p[0].uppercaseChar())
|
|
488
|
+
if (p.length > 1) sb.append(p.substring(1))
|
|
489
|
+
}
|
|
490
|
+
return sb.toString()
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
/** Recursively convert org.json.JSONObject → WritableMap (normalizes keys to camelCase). */
|
|
494
|
+
private fun jsonObjectToWritableMap(jsonObject: JSONObject): WritableMap {
|
|
495
|
+
val map = Arguments.createMap()
|
|
496
|
+
val keys = jsonObject.keys()
|
|
497
|
+
while (keys.hasNext()) {
|
|
498
|
+
val rawKey = keys.next()
|
|
499
|
+
val key = normalizeKeyToCamelCase(rawKey)
|
|
500
|
+
when (val value = jsonObject.get(rawKey)) {
|
|
501
|
+
is JSONObject -> map.putMap(key, jsonObjectToWritableMap(value))
|
|
502
|
+
is JSONArray -> map.putArray(key, jsonArrayToWritableArray(value))
|
|
503
|
+
is Boolean -> map.putBoolean(key, value)
|
|
504
|
+
is Int -> map.putInt(key, value)
|
|
505
|
+
is Long -> map.putDouble(key, value.toDouble())
|
|
506
|
+
is Double -> map.putDouble(key, value)
|
|
507
|
+
is String -> map.putString(key, value)
|
|
508
|
+
JSONObject.NULL -> map.putNull(key)
|
|
509
|
+
else -> map.putString(key, value.toString())
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
return map
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
/** Recursively convert org.json.JSONArray → WritableArray. */
|
|
516
|
+
private fun jsonArrayToWritableArray(jsonArray: JSONArray): WritableArray {
|
|
517
|
+
val array = Arguments.createArray()
|
|
518
|
+
for (i in 0 until jsonArray.length()) {
|
|
519
|
+
when (val value = jsonArray.get(i)) {
|
|
520
|
+
is JSONObject -> array.pushMap(jsonObjectToWritableMap(value))
|
|
521
|
+
is JSONArray -> array.pushArray(jsonArrayToWritableArray(value))
|
|
522
|
+
is Boolean -> array.pushBoolean(value)
|
|
523
|
+
is Int -> array.pushInt(value)
|
|
524
|
+
is Long -> array.pushDouble(value.toDouble())
|
|
525
|
+
is Double -> array.pushDouble(value)
|
|
526
|
+
is String -> array.pushString(value)
|
|
527
|
+
JSONObject.NULL -> array.pushNull()
|
|
528
|
+
else -> array.pushString(value.toString())
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
return array
|
|
532
|
+
}
|
|
533
|
+
|
|
100
534
|
/** Convert ESignOpenSessionResult to WritableMap for RN. */
|
|
101
535
|
private fun eSignOpenSessionResultToWritableMap(result: ESignModels.ESignOpenSessionResult): WritableMap =
|
|
102
536
|
Arguments.createMap().apply {
|
|
@@ -430,8 +864,6 @@ class EKYCModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaMo
|
|
|
430
864
|
activeActionCount: Int?,
|
|
431
865
|
forceCaptureTimeout: Double?,
|
|
432
866
|
isActiveLivenessColor: Boolean?,
|
|
433
|
-
isShowBackConfirmation: Boolean?,
|
|
434
|
-
backConfirmConfigJson: String?,
|
|
435
867
|
promise: Promise
|
|
436
868
|
) {
|
|
437
869
|
Log.d(TAG, "▶️ startLiveness() called")
|
|
@@ -465,7 +897,7 @@ class EKYCModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaMo
|
|
|
465
897
|
null
|
|
466
898
|
}
|
|
467
899
|
|
|
468
|
-
|
|
900
|
+
|
|
469
901
|
val livenessConfig =
|
|
470
902
|
LivenessConfig(
|
|
471
903
|
isActiveLiveness = isActiveLiveness ?: false,
|
|
@@ -480,9 +912,7 @@ class EKYCModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaMo
|
|
|
480
912
|
activeActionCount = activeActionCount ?: 2,
|
|
481
913
|
forceCaptureTimeout = (forceCaptureTimeout ?: 0.0).toLong(),
|
|
482
914
|
selfieImage = imageFile,
|
|
483
|
-
transactionId = transactionId
|
|
484
|
-
isShowBackConfirmation = isShowBackConfirmation ?: false,
|
|
485
|
-
backConfirmConfig = backConfirmConfig,
|
|
915
|
+
transactionId = transactionId
|
|
486
916
|
)
|
|
487
917
|
val ekycConfig =
|
|
488
918
|
EKYCConfigSDK(
|
|
@@ -493,24 +923,25 @@ class EKYCModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaMo
|
|
|
493
923
|
eKYCFinOSLiveness.startEkyc(
|
|
494
924
|
ekycConfigSDK = ekycConfig,
|
|
495
925
|
callbackSuccess = { event, data ->
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
926
|
+
Log.d(TAG, "🔥 NATIVE CALLBACK (Liveness): event=${event.name}")
|
|
927
|
+
val response = (data as? SDKEkycResult)?.checkLivenessResponse
|
|
928
|
+
val jsonStr = data?.customData?.let { Gson().toJson(it) } ?: Gson().toJson(response ?: data)
|
|
929
|
+
|
|
930
|
+
// Helper to create fresh data map to avoid 'Map already consumed' error
|
|
931
|
+
fun createWritableData() = try { jsonObjectToWritableMap(JSONObject(jsonStr)) } catch (e: Exception) { Arguments.createMap() }
|
|
932
|
+
|
|
933
|
+
// Send the event directly to React Native
|
|
934
|
+
sendEvent("onLivenessSuccess", Arguments.createMap().apply {
|
|
935
|
+
putString("event", event.name)
|
|
936
|
+
putMap("data", createWritableData())
|
|
937
|
+
})
|
|
938
|
+
|
|
939
|
+
// Always attempt to resolve the promise.
|
|
940
|
+
// RN safely ignores subsequent resolves if the native SDK fires multiple events.
|
|
941
|
+
promise.resolve(Arguments.createMap().apply {
|
|
942
|
+
putString("event", event.name)
|
|
943
|
+
putMap("data", createWritableData())
|
|
944
|
+
})
|
|
514
945
|
},
|
|
515
946
|
callbackError = { event, errorResult ->
|
|
516
947
|
Log.e(TAG, "❌ startLiveness() failed - Event: $event, Code: ${errorResult.code}, Message: ${errorResult.message}")
|
|
@@ -577,40 +1008,21 @@ class EKYCModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaMo
|
|
|
577
1008
|
eKYCFinOSFaceService.startEkyc(
|
|
578
1009
|
ekycConfigSDK = ekycConfig,
|
|
579
1010
|
callbackSuccess = { event, data ->
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
map.putString("event", event.name.toString())
|
|
596
|
-
val resultMap = Arguments.createMap().apply {
|
|
597
|
-
putDouble("conf", faceResponse?.result?.conf?.toString()?.toDoubleOrNull() ?: 0.0)
|
|
598
|
-
putString("match", faceResponse?.result?.match?.toString())
|
|
599
|
-
putInt("matchScore", faceResponse?.result?.matchScore?.toString()?.toIntOrNull() ?: 0)
|
|
600
|
-
putString("toBeReviewed", faceResponse?.result?.toBeReviewed?.toString())
|
|
601
|
-
}
|
|
602
|
-
val faceMap = Arguments.createMap().apply {
|
|
603
|
-
putString("requestId", faceResponse?.requestId?.toString())
|
|
604
|
-
putMap("result", resultMap)
|
|
605
|
-
putString("status", faceResponse?.status?.toString())
|
|
606
|
-
putInt("statusCode", faceResponse?.statusCode?.toString()?.toIntOrNull() ?: 0)
|
|
607
|
-
putString("error", faceResponse?.error?.toString())
|
|
608
|
-
putString("type", faceResponse?.type?.toString())
|
|
609
|
-
}
|
|
610
|
-
map.putMap("data", faceMap)
|
|
611
|
-
}
|
|
612
|
-
sendEvent("onFaceCompareSuccess", eventMap)
|
|
613
|
-
promise.resolve(promiseMap)
|
|
1011
|
+
Log.d(TAG, "🔥 NATIVE CALLBACK (Face Compare): event=${event.name}")
|
|
1012
|
+
val response = (data as? SDKEkycResult)?.checkFaceResponse
|
|
1013
|
+
val jsonStr = data?.customData?.let { Gson().toJson(it) } ?: Gson().toJson(response ?: data)
|
|
1014
|
+
fun createWritableData() = try { jsonObjectToWritableMap(JSONObject(jsonStr)) } catch (e: Exception) { Arguments.createMap() }
|
|
1015
|
+
|
|
1016
|
+
sendEvent("onFaceCompareSuccess", Arguments.createMap().apply {
|
|
1017
|
+
putString("event", event.name)
|
|
1018
|
+
putMap("data", createWritableData())
|
|
1019
|
+
})
|
|
1020
|
+
|
|
1021
|
+
// Always attempt to resolve the promise.
|
|
1022
|
+
promise.resolve(Arguments.createMap().apply {
|
|
1023
|
+
putString("event", event.name)
|
|
1024
|
+
putMap("data", createWritableData())
|
|
1025
|
+
})
|
|
614
1026
|
},
|
|
615
1027
|
callbackError = { event, errorResult ->
|
|
616
1028
|
Log.e(TAG, "❌ startFaceCompare() failed - Event: $event, Code: ${errorResult.code}, Message: ${errorResult.message}")
|
|
@@ -806,18 +1218,13 @@ class EKYCModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaMo
|
|
|
806
1218
|
else -> AppIDType.NONE
|
|
807
1219
|
}
|
|
808
1220
|
|
|
809
|
-
val isShowBackConfirmation = extractBooleanValue(optionConfigJson, "isShowBackConfirmation") ?: false
|
|
810
|
-
val backConfirmConfig = parseBackConfirmConfig(extractObjectJson(optionConfigJson, "backConfirmConfig"))
|
|
811
|
-
|
|
812
1221
|
val livenessConfig = if (finalFlow.contains(SDKType.LIVENESS)) {
|
|
813
1222
|
LivenessConfig(
|
|
814
1223
|
isActiveLiveness = isActiveLiveness,
|
|
815
1224
|
forceCaptureTimeout = (forceCaptureTimeoutSec * 1000L).coerceAtLeast(0),
|
|
816
1225
|
isShowCameraFont = switchFrontCamera,
|
|
817
1226
|
customActions = customActionsList?.takeIf { it.isNotEmpty() },
|
|
818
|
-
activeActionCount = activeActionCount
|
|
819
|
-
isShowBackConfirmation = isShowBackConfirmation,
|
|
820
|
-
backConfirmConfig = backConfirmConfig,
|
|
1227
|
+
activeActionCount = activeActionCount
|
|
821
1228
|
)
|
|
822
1229
|
} else null
|
|
823
1230
|
|
|
@@ -835,41 +1242,71 @@ class EKYCModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaMo
|
|
|
835
1242
|
activity = currentActivity,
|
|
836
1243
|
ekycConfigSDK = ekycConfigSDK,
|
|
837
1244
|
callbackSuccess = { event, data ->
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
val logDataStr = data?.customData?.let { Gson().toJson(it) } ?: Gson().toJson(data)
|
|
841
|
-
Log.d(TAG, "LOG_SUCCESS [EkycUI]: $logDataStr")
|
|
842
|
-
sendEvent("onEkycUISuccess", Arguments.createMap().apply {
|
|
843
|
-
putString("status", "log")
|
|
844
|
-
putString("event", event.name.toString())
|
|
845
|
-
putString("data", logDataStr)
|
|
846
|
-
})
|
|
847
|
-
return@startEkyc
|
|
848
|
-
}
|
|
849
|
-
Log.d(TAG, "✅ startEkycUI() callback - event=${event.name}")
|
|
1245
|
+
Log.d(TAG, "🔥 NATIVE CALLBACK (EkycUI): event=${event.name}")
|
|
1246
|
+
|
|
850
1247
|
val result = data as? SDKEkycResult
|
|
851
|
-
val
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
1248
|
+
val response = result?.ekycStateModel
|
|
1249
|
+
val jsonStr = data?.customData?.let { Gson().toJson(it) } ?: Gson().toJson(response ?: data)
|
|
1250
|
+
|
|
1251
|
+
// Helper to create fresh data map to avoid 'Map already consumed' error
|
|
1252
|
+
fun createWritableData() = try { jsonObjectToWritableMap(JSONObject(jsonStr)) } catch (e: Exception) { Arguments.createMap() }
|
|
1253
|
+
|
|
1254
|
+
val statusStr = when (event) {
|
|
1255
|
+
EKYCEvent.LOG_SUCCESS -> "log"
|
|
1256
|
+
EKYCEvent.SDK_START_SUCCESS -> "started"
|
|
1257
|
+
else -> "success"
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
// Send EVERY event directly to React Native Event Listener (Pass-through)
|
|
1261
|
+
sendEvent("onEkycUISuccess", Arguments.createMap().apply {
|
|
1262
|
+
putString("status", statusStr)
|
|
1263
|
+
putString("event", event.name)
|
|
1264
|
+
|
|
1265
|
+
if (event == EKYCEvent.SDK_START_SUCCESS && response == null) {
|
|
1266
|
+
// Bỏ qua data nếu là SDK_START_SUCCESS (chỉ có event/status) để giống code cũ
|
|
1267
|
+
} else {
|
|
1268
|
+
putMap("data", createWritableData())
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
// Retain the convenient root properties cho backward compatibility
|
|
860
1272
|
val ekycFiles = result?.ekycStateModel?.eKYCFileModel
|
|
861
1273
|
val ekycTransactionId = result?.ekycStateModel?.transactionId ?: ""
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
1274
|
+
if (ekycTransactionId.isNotEmpty()) putString("transactionId", ekycTransactionId)
|
|
1275
|
+
ekycFiles?.imageFace?.absolutePath?.let { putString("imageFacePath", it) }
|
|
1276
|
+
ekycFiles?.imageOcrFront?.absolutePath?.let { putString("imageOcrFrontPath", it) }
|
|
1277
|
+
ekycFiles?.imageOcrBack?.absolutePath?.let { putString("imageOcrBackPath", it) }
|
|
1278
|
+
})
|
|
1279
|
+
|
|
1280
|
+
// Re-emit onLivenessSuccess: dùng checkLivenessResponse (đúng format
|
|
1281
|
+
// CheckLivenessResponse) thay vì ekycStateModel để JS nhận đủ fields.
|
|
1282
|
+
if (event == EKYCEvent.LIVENESS_SUCCESS || event == EKYCEvent.LOG_SUCCESS) {
|
|
1283
|
+
val lrJson = result?.checkLivenessResponse
|
|
1284
|
+
?.let { Gson().toJson(it) }
|
|
1285
|
+
?: data?.customData?.let { Gson().toJson(it) }
|
|
1286
|
+
?: jsonStr
|
|
1287
|
+
fun createLivenessData() = try {
|
|
1288
|
+
jsonObjectToWritableMap(JSONObject(lrJson))
|
|
1289
|
+
} catch (e: Exception) { Arguments.createMap() }
|
|
1290
|
+
sendEvent("onLivenessSuccess", Arguments.createMap().apply {
|
|
1291
|
+
putString("event", event.name)
|
|
1292
|
+
putMap("data", createLivenessData())
|
|
1293
|
+
})
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1296
|
+
// IMPORTANT: Chỉ resolve promise ở SDK_END_SUCCESS để `await FinosEKYC.startEkycUI()` không bị kết thúc sớm
|
|
1297
|
+
if (event == EKYCEvent.SDK_END_SUCCESS) {
|
|
1298
|
+
promise.resolve(Arguments.createMap().apply {
|
|
1299
|
+
putString("status", "success")
|
|
1300
|
+
putString("event", event.name)
|
|
1301
|
+
putMap("data", createWritableData())
|
|
1302
|
+
|
|
1303
|
+
val ekycFiles = result?.ekycStateModel?.eKYCFileModel
|
|
1304
|
+
val ekycTransactionId = result?.ekycStateModel?.transactionId ?: ""
|
|
1305
|
+
if (ekycTransactionId.isNotEmpty()) putString("transactionId", ekycTransactionId)
|
|
1306
|
+
ekycFiles?.imageFace?.absolutePath?.let { putString("imageFacePath", it) }
|
|
1307
|
+
ekycFiles?.imageOcrFront?.absolutePath?.let { putString("imageOcrFrontPath", it) }
|
|
1308
|
+
ekycFiles?.imageOcrBack?.absolutePath?.let { putString("imageOcrBackPath", it) }
|
|
1309
|
+
})
|
|
873
1310
|
}
|
|
874
1311
|
},
|
|
875
1312
|
callbackError = { event, errorResult ->
|
|
@@ -881,14 +1318,14 @@ class EKYCModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaMo
|
|
|
881
1318
|
putString("customMessage", errorResult.message)
|
|
882
1319
|
putString("message", errorResult.message)
|
|
883
1320
|
}
|
|
1321
|
+
sendEvent("onEkycUIError", errorMap)
|
|
1322
|
+
|
|
884
1323
|
val promiseErrorMap = Arguments.createMap().apply {
|
|
885
1324
|
putString("status", "error")
|
|
886
1325
|
putString("event", event.name.toString())
|
|
887
1326
|
putString("customCode", errorResult.code)
|
|
888
1327
|
putString("customMessage", errorResult.message)
|
|
889
|
-
putString("message", errorResult.message)
|
|
890
1328
|
}
|
|
891
|
-
sendEvent("onEkycUIError", errorMap)
|
|
892
1329
|
promise.reject(event.name.toString(), errorResult.message, null, promiseErrorMap)
|
|
893
1330
|
}
|
|
894
1331
|
)
|
|
@@ -943,24 +1380,7 @@ class EKYCModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaMo
|
|
|
943
1380
|
}
|
|
944
1381
|
}
|
|
945
1382
|
|
|
946
|
-
|
|
947
|
-
if (json.isNullOrBlank() || json == "{}") return null
|
|
948
|
-
return try {
|
|
949
|
-
LivenessBackConfirmConfig(
|
|
950
|
-
titleVi = extractStringValue(json, "titleVi"),
|
|
951
|
-
titleEn = extractStringValue(json, "titleEn"),
|
|
952
|
-
bodyVi = extractStringValue(json, "bodyVi"),
|
|
953
|
-
bodyEn = extractStringValue(json, "bodyEn"),
|
|
954
|
-
confirmButtonVi = extractStringValue(json, "confirmButtonVi"),
|
|
955
|
-
confirmButtonEn = extractStringValue(json, "confirmButtonEn"),
|
|
956
|
-
cancelButtonVi = extractStringValue(json, "cancelButtonVi"),
|
|
957
|
-
cancelButtonEn = extractStringValue(json, "cancelButtonEn"),
|
|
958
|
-
)
|
|
959
|
-
} catch (e: Exception) {
|
|
960
|
-
Log.e(TAG, "Error parsing backConfirmConfig: ${e.message}", e)
|
|
961
|
-
null
|
|
962
|
-
}
|
|
963
|
-
}
|
|
1383
|
+
|
|
964
1384
|
|
|
965
1385
|
private fun extractIntValue(json: String, key: String): Int? {
|
|
966
1386
|
return try {
|
|
@@ -2174,4 +2594,95 @@ class EKYCModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaMo
|
|
|
2174
2594
|
}
|
|
2175
2595
|
}
|
|
2176
2596
|
|
|
2597
|
+
@ReactMethod
|
|
2598
|
+
fun registerExitHandler(promise: Promise) {
|
|
2599
|
+
Log.d(TAG, "▶️ registerExitHandler() called")
|
|
2600
|
+
try {
|
|
2601
|
+
SDKeKYCExitHandlerManager.uiListener = { fm ->
|
|
2602
|
+
sendEvent("onShowExitConfirm", Arguments.createMap())
|
|
2603
|
+
}
|
|
2604
|
+
SDKeKYCExitHandlerManager.register()
|
|
2605
|
+
promise.resolve(true)
|
|
2606
|
+
} catch (e: Exception) {
|
|
2607
|
+
Log.e(TAG, "❌ registerExitHandler() exception: ${e.message}", e)
|
|
2608
|
+
promise.reject("REGISTER_EXIT_HANDLER_EXCEPTION", e.message, e)
|
|
2609
|
+
}
|
|
2610
|
+
}
|
|
2611
|
+
|
|
2612
|
+
@ReactMethod
|
|
2613
|
+
fun resolveExit(action: String, promise: Promise) {
|
|
2614
|
+
Log.d(TAG, "▶️ resolveExit() called with action: $action")
|
|
2615
|
+
try {
|
|
2616
|
+
val sheet = currentExitSheet
|
|
2617
|
+
val activity = getTrueCurrentActivity()
|
|
2618
|
+
|
|
2619
|
+
if (sheet != null && sheet.isShowing && activity != null) {
|
|
2620
|
+
// Đánh dấu trước khi dismiss để setOnDismissListener biết đây là button-triggered,
|
|
2621
|
+
// không phải backdrop tap → tránh auto-resolve CANCEL sai.
|
|
2622
|
+
exitSheetResolvedViaButton = true
|
|
2623
|
+
val latch = java.util.concurrent.CountDownLatch(1)
|
|
2624
|
+
activity.runOnUiThread {
|
|
2625
|
+
try {
|
|
2626
|
+
sheet.dismiss()
|
|
2627
|
+
} catch (e: Exception) {
|
|
2628
|
+
Log.w(TAG, "resolveExit: error dismissing sheet: ${e.message}")
|
|
2629
|
+
} finally {
|
|
2630
|
+
latch.countDown()
|
|
2631
|
+
}
|
|
2632
|
+
}
|
|
2633
|
+
latch.await(500, java.util.concurrent.TimeUnit.MILLISECONDS)
|
|
2634
|
+
exitSheetResolvedViaButton = false
|
|
2635
|
+
}
|
|
2636
|
+
|
|
2637
|
+
// Gọi SDK callback SAU KHI sheet đã dismiss
|
|
2638
|
+
SDKeKYCExitHandlerManager.resolve(action)
|
|
2639
|
+
promise.resolve(true)
|
|
2640
|
+
} catch (e: Exception) {
|
|
2641
|
+
Log.e(TAG, "❌ resolveExit() exception: ${e.message}", e)
|
|
2642
|
+
promise.reject("RESOLVE_EXIT_EXCEPTION", e.message, e)
|
|
2643
|
+
}
|
|
2644
|
+
}
|
|
2645
|
+
|
|
2646
|
+
@ReactMethod
|
|
2647
|
+
fun setExitSheetHeight(heightDp: Float) {
|
|
2648
|
+
val activity = getTrueCurrentActivity() ?: return
|
|
2649
|
+
val heightPx = (heightDp * activity.resources.displayMetrics.density).toInt()
|
|
2650
|
+
Log.d(TAG, "▶️ setExitSheetHeight: ${heightDp}dp → ${heightPx}px")
|
|
2651
|
+
activity.runOnUiThread {
|
|
2652
|
+
val sheet = currentExitSheet ?: return@runOnUiThread
|
|
2653
|
+
if (!sheet.isShowing) return@runOnUiThread
|
|
2654
|
+
val bs = sheet.findViewById<View>(com.google.android.material.R.id.design_bottom_sheet)
|
|
2655
|
+
?: return@runOnUiThread
|
|
2656
|
+
val lp = bs.layoutParams ?: return@runOnUiThread
|
|
2657
|
+
lp.height = heightPx
|
|
2658
|
+
bs.layoutParams = lp
|
|
2659
|
+
val behavior = com.google.android.material.bottomsheet.BottomSheetBehavior.from(bs)
|
|
2660
|
+
behavior.peekHeight = heightPx
|
|
2661
|
+
behavior.state = com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_EXPANDED
|
|
2662
|
+
}
|
|
2663
|
+
}
|
|
2664
|
+
|
|
2665
|
+
@ReactMethod
|
|
2666
|
+
fun addListener(eventName: String) {
|
|
2667
|
+
// Keep: Required for RN built-in Event Emitter Calls.
|
|
2668
|
+
}
|
|
2669
|
+
|
|
2670
|
+
@ReactMethod
|
|
2671
|
+
fun removeListeners(count: Int) {
|
|
2672
|
+
// Keep: Required for RN built-in Event Emitter Calls.
|
|
2673
|
+
}
|
|
2674
|
+
|
|
2675
|
+
override fun onCatalystInstanceDestroy() {
|
|
2676
|
+
super.onCatalystInstanceDestroy()
|
|
2677
|
+
SDKeKYCExitHandlerManager.uiListener = null
|
|
2678
|
+
// Dismiss sheet nếu vẫn đang hiển thị khi RN catalyst instance destroy
|
|
2679
|
+
val sheet = currentExitSheet
|
|
2680
|
+
currentExitSheet = null
|
|
2681
|
+
if (sheet != null && sheet.isShowing) {
|
|
2682
|
+
val activity = currentActivity
|
|
2683
|
+
activity?.runOnUiThread {
|
|
2684
|
+
try { sheet.dismiss() } catch (_: Exception) {}
|
|
2685
|
+
}
|
|
2686
|
+
}
|
|
2687
|
+
}
|
|
2177
2688
|
}
|