@finos_sdk/sdk-ekyc 1.4.9 → 1.5.1

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.
@@ -66,7 +66,10 @@ import java.io.FileInputStream
66
66
  import java.io.InputStream
67
67
  import java.util.Date
68
68
  import android.app.Activity
69
+ import android.app.Application
70
+ import android.os.Bundle
69
71
  import android.view.ViewGroup
72
+ import java.lang.ref.WeakReference
70
73
 
71
74
  @ReactModule(name = EKYCModule.NAME)
72
75
  class EKYCModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext) {
@@ -74,10 +77,71 @@ class EKYCModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaMo
74
77
  companion object {
75
78
  const val NAME = "EKYCModule"
76
79
  private const val TAG = "EKYCModule"
80
+
81
+ // Full class name của SDK activity duy nhất
82
+ private const val SDK_EKYC_ACTIVITY = "finos.sdk.ekyc.sdkui.SDKeKYCActivity"
77
83
  }
78
84
 
79
85
  override fun getName(): String = NAME
80
86
 
87
+ /**
88
+ * WeakReference tới SDK activity đang foreground.
89
+ * Được set/clear bởi ActivityLifecycleCallbacks khi SDK activity resume/pause.
90
+ * @Volatile: viết từ UI thread (lifecycle callback), đọc từ JS bridge thread (showRNExitSheet).
91
+ * WeakReference để tránh memory leak nếu activity bị destroy mà chưa clear.
92
+ */
93
+ @Volatile
94
+ private var activeSdkActivity: WeakReference<Activity>? = null
95
+
96
+ private val sdkActivityLifecycleCallbacks = object : Application.ActivityLifecycleCallbacks {
97
+ override fun onActivityResumed(activity: Activity) {
98
+ if (isSDKActivity(activity)) {
99
+ activeSdkActivity = WeakReference(activity)
100
+ Log.d(TAG, "📌 SDK Activity resumed: ${activity.javaClass.simpleName}")
101
+ }
102
+ }
103
+ override fun onActivityPaused(activity: Activity) {
104
+ if (activeSdkActivity?.get() == activity) {
105
+ activeSdkActivity = null
106
+ Log.d(TAG, "📌 SDK Activity paused: ${activity.javaClass.simpleName}")
107
+ }
108
+ }
109
+ override fun onActivityDestroyed(activity: Activity) {
110
+ if (activeSdkActivity?.get() == activity) {
111
+ activeSdkActivity = null
112
+ Log.d(TAG, "📌 SDK Activity destroyed: ${activity.javaClass.simpleName}")
113
+ }
114
+ }
115
+ override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {}
116
+ override fun onActivityStarted(activity: Activity) {}
117
+ override fun onActivityStopped(activity: Activity) {}
118
+ override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {}
119
+ }
120
+
121
+ private fun isSDKActivity(activity: Activity): Boolean {
122
+ return activity.javaClass.name == SDK_EKYC_ACTIVITY
123
+ }
124
+
125
+ init {
126
+ // Đăng ký lifecycle callback để track SDK activity tự động
127
+ val app = reactContext.applicationContext as? Application
128
+ app?.registerActivityLifecycleCallbacks(sdkActivityLifecycleCallbacks)
129
+ ?: Log.w(TAG, "⚠️ Could not register ActivityLifecycleCallbacks")
130
+ }
131
+
132
+ /**
133
+ * Lấy SDK activity đang active, fallback về getTrueCurrentActivity() nếu không có.
134
+ */
135
+ private fun getSDKActivity(): Activity? {
136
+ val sdk = activeSdkActivity?.get()
137
+ if (sdk != null && !sdk.isFinishing && !sdk.isDestroyed) {
138
+ Log.d(TAG, "✅ Using tracked SDK Activity: ${sdk.javaClass.simpleName}")
139
+ return sdk
140
+ }
141
+ Log.d(TAG, "⚠️ No active SDK activity tracked, falling back to getTrueCurrentActivity")
142
+ return getTrueCurrentActivity()
143
+ }
144
+
81
145
  /**
82
146
  * Reference tới BottomSheetDialog hiện đang hiển thị.
83
147
  * @Volatile: resolveExit() chạy trên JS bridge thread, đọc field này được viết bởi UI thread.
@@ -92,97 +156,153 @@ class EKYCModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaMo
92
156
  private var exitSheetResolvedViaButton = false
93
157
 
94
158
  private fun getTrueCurrentActivity(): Activity? {
95
- // Try React Native's currentActivity first
159
+ // Try React Native's currentActivity first as a baseline
96
160
  val rnActivity = currentActivity
97
- if (rnActivity != null && rnActivity.javaClass.simpleName != "MainActivity") {
161
+
162
+ Log.d(TAG, "🔍 getTrueCurrentActivity check: rnActivity=${rnActivity?.javaClass?.simpleName}")
163
+
164
+ // If rnActivity is already an SDK activity, we can likely trust it
165
+ if (rnActivity != null && rnActivity.javaClass.name.startsWith("finos.sdk.")) {
166
+ Log.d(TAG, "✅ Current activity is already an SDK activity: ${rnActivity.javaClass.simpleName}")
98
167
  return rnActivity
99
168
  }
100
169
 
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
170
+ // Use reflection to find the absolute top resumed activity.
171
+ // This is crucial when the SDK is running in a native Activity on top of the RN Activity,
172
+ // and currentActivity might still point to the background Activity.
173
+ try {
174
+ val topActivity = findTopActivityViaReflection()
175
+ if (topActivity != null) {
176
+ Log.d(TAG, "✅ Found top activity via reflection: ${topActivity.javaClass.simpleName}")
177
+ return topActivity
178
+ }
179
+ } catch (e: Exception) {
180
+ Log.e(TAG, "Failed to find top activity via reflection", e)
181
+ }
182
+
183
+ Log.d(TAG, "⚠️ Fallback to rnActivity: ${rnActivity?.javaClass?.simpleName}")
184
+ return rnActivity
185
+ }
186
+
187
+ /**
188
+ * Finds the top-most resumed activity using ActivityThread's internal records.
189
+ */
190
+ private fun findTopActivityViaReflection(): Activity? {
103
191
  try {
104
192
  val activityThreadClass = Class.forName("android.app.ActivityThread")
105
- val activityThread = activityThreadClass.getMethod("currentActivityThread").invoke(null)
193
+ val activityThread = activityThreadClass.getMethod("currentActivityThread").invoke(null) ?: return null
106
194
  val activitiesField = activityThreadClass.getDeclaredField("mActivities")
107
195
  activitiesField.isAccessible = true
108
196
 
109
- val activities = activitiesField.get(activityThread) as Map<Any, Any>
110
- Log.d(TAG, "🔍 Scanning ${activities.size} activities...")
111
-
112
- var topActivity: Activity? = null
197
+ val activities = activitiesField.get(activityThread) as? Map<*, *> ?: return null
198
+ Log.d(TAG, "🔍 Scanning ${activities.size} activities via reflection...")
113
199
 
200
+ var bestCandidate: Activity? = null
201
+ var highestPriority = -1
202
+
114
203
  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
204
+ if (activityRecord == null) continue
205
+ try {
206
+ val activityRecordClass = activityRecord.javaClass
207
+ val activityField = activityRecordClass.getDeclaredField("activity")
208
+ activityField.isAccessible = true
209
+ val activity = activityField.get(activityRecord) as? Activity ?: continue
210
+
211
+ if (activity.isFinishing) continue
212
+
213
+ val pausedField = activityRecordClass.getDeclaredField("paused")
214
+ pausedField.isAccessible = true
215
+ val isPaused = pausedField.get(activityRecord) as Boolean
216
+
217
+ val stoppedField = activityRecordClass.getDeclaredField("stopped")
218
+ stoppedField.isAccessible = true
219
+ val isStopped = stoppedField.get(activityRecord) as Boolean
220
+
221
+ if (isStopped) continue
222
+
223
+ // Identity check: prioritize known SDK activity patterns
224
+ val className = activity.javaClass.name
225
+ val isSDK = className.startsWith("finos.sdk.") ||
226
+ className.contains("EKYC", ignoreCase = true) ||
227
+ className.contains("Liveness", ignoreCase = true) ||
228
+ className.contains("OCR", ignoreCase = true) ||
229
+ className.contains("NFC", ignoreCase = true)
230
+
231
+ // Priority scoring:
232
+ // 3: Resumed SDK Activity
233
+ // 2: Resumed App Activity
234
+ // 1: Paused SDK Activity
235
+ // 0: Paused App Activity
236
+ val currentPriority = (if (isSDK) 1 else 0) + (if (!isPaused) 2 else 0)
237
+
238
+ Log.v(TAG, " [Audit] ${activity.javaClass.simpleName} -> Prio: $currentPriority (SDK=$isSDK, Paused=$isPaused)")
239
+
240
+ if (currentPriority > highestPriority) {
241
+ highestPriority = currentPriority
242
+ bestCandidate = activity
243
+ }
244
+
245
+ // Optimization: Found the absolute best target
246
+ if (highestPriority == 3) break
247
+
248
+ } catch (e: Exception) {
249
+ // Silently continue for individual activity record failures
134
250
  }
135
251
  }
136
- return topActivity ?: rnActivity
252
+
253
+ if (bestCandidate != null) {
254
+ Log.d(TAG, "✅ Best candidate found: ${bestCandidate.javaClass.simpleName} (Prio: $highestPriority)")
255
+ }
256
+
257
+ return bestCandidate
137
258
  } catch (e: Exception) {
138
- Log.e(TAG, "Error finding true current activity", e)
259
+ Log.e(TAG, " Critical error in findTopActivityViaReflection", e)
260
+ return null
139
261
  }
140
-
141
- return rnActivity
142
262
  }
143
263
 
144
264
  @ReactMethod
145
265
  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
- }
266
+ Handler(Looper.getMainLooper()).post {
267
+ val activity = getSDKActivity() ?: run {
268
+ Log.e(TAG, "❌ showRNExitSheet failed: No visible activity found")
269
+ promise.reject("NO_ACTIVITY", "No visible activity found")
270
+ return@post
271
+ }
157
272
 
158
- // Guard chống double-resolve/reject promise ( 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)
273
+ if (activity.isFinishing || (android.os.Build.VERSION.SDK_INT >= 17 && activity.isDestroyed)) {
274
+ Log.e(TAG, "❌ showRNExitSheet failed: Activity is finishing or destroyed")
275
+ promise.reject("ACTIVITY_INVALID", "Activity is finishing or destroyed")
276
+ return@post
166
277
  }
167
- }
168
278
 
169
- activity.runOnUiThread {
279
+ // Guard chống double-resolve/reject promise
280
+ val promiseSettled = java.util.concurrent.atomic.AtomicBoolean(false)
281
+ fun safeResolve(value: Any?) {
282
+ if (promiseSettled.compareAndSet(false, true)) promise.resolve(value)
283
+ }
284
+ fun safeReject(code: String, msg: String, e: Throwable? = null) {
285
+ if (promiseSettled.compareAndSet(false, true)) {
286
+ if (e != null) promise.reject(code, msg, e) else promise.reject(code, msg)
287
+ }
288
+ }
170
289
  try {
171
290
  val activityName = activity.javaClass.simpleName
172
291
  Log.d(TAG, "▶️ showRNExitSheet: Top Activity=$activityName, bundle=$bundleName")
173
292
 
174
293
  val reactApplication = activity.application as? ReactApplication
175
294
  if (reactApplication == null) {
176
- Log.e(TAG, "❌ Application does not implement ReactApplication")
295
+ Log.e(TAG, "❌ showRNExitSheet failed: Application class (${activity.application.javaClass.name}) does not implement ReactApplication. Please ensure your Application class implements ReactApplication or provide a way for the SDK to access the ReactInstanceManager.")
177
296
  safeReject("NO_REACT_APP", "Application does not implement ReactApplication")
178
- return@runOnUiThread
297
+ return@post
179
298
  }
180
299
  val reactInstanceManager = reactApplication.reactNativeHost.reactInstanceManager
181
300
 
182
301
  if (reactInstanceManager.currentReactContext != null) {
302
+ Log.d(TAG, "✅ ReactContext is ready, presenting sheet...")
183
303
  presentRNBottomSheet(activity, reactInstanceManager, bundleName, initialProps,
184
304
  ::safeResolve, ::safeReject)
185
- return@runOnUiThread
305
+ return@post
186
306
  }
187
307
 
188
308
  // Context chưa sẵn sàng: chờ listener
@@ -212,7 +332,7 @@ class EKYCModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaMo
212
332
  Log.d(TAG, "✅ ReactContext init xong giữa lúc add listener — present luôn")
213
333
  presentRNBottomSheet(activity, reactInstanceManager, bundleName, initialProps,
214
334
  ::safeResolve, ::safeReject)
215
- return@runOnUiThread
335
+ return@post
216
336
  }
217
337
 
218
338
  // Chỉ trigger background create nếu chưa start (tránh IllegalStateException)
@@ -250,12 +370,17 @@ class EKYCModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaMo
250
370
  safeReject: (String, String, Throwable?) -> Unit
251
371
  ) {
252
372
  try {
373
+ val minHeightPx = (400 * activity.resources.displayMetrics.density).toInt()
253
374
  val container = LinearLayout(activity).apply {
254
375
  orientation = LinearLayout.VERTICAL
255
376
  setBackgroundColor(Color.TRANSPARENT)
377
+ minimumHeight = minHeightPx
256
378
  }
257
379
 
258
380
  val rootView = ReactRootView(activity)
381
+ // Hardening: Set a minimum height to ensure the sheet is visible even before RN finishes layout
382
+ rootView.minimumHeight = minHeightPx
383
+
259
384
  rootView.layoutParams = LinearLayout.LayoutParams(
260
385
  LinearLayout.LayoutParams.MATCH_PARENT,
261
386
  LinearLayout.LayoutParams.WRAP_CONTENT
@@ -328,7 +453,7 @@ class EKYCModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaMo
328
453
 
329
454
  bottomSheetDialog.show()
330
455
 
331
- Log.d(TAG, "✅ showRNExitSheet: BottomSheetDialog shown (wrap content), bundle=$bundleName")
456
+ Log.d(TAG, "✅ showRNExitSheet: BottomSheetDialog shown on ${activity.javaClass.simpleName}, bundle=$bundleName")
332
457
  safeResolve(true)
333
458
  } catch (e: Exception) {
334
459
  Log.e(TAG, "❌ Exception in presentRNBottomSheet", e)
@@ -903,12 +1028,7 @@ class EKYCModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaMo
903
1028
  isActiveLiveness = isActiveLiveness ?: false,
904
1029
  isActiveLivenessColor = isActiveLivenessColor ?: false,
905
1030
  isShowCameraFont = isShowCameraFont ?: true,
906
- // Custom actions từ checkboxes (nếu có chọn)
907
- // Nếu có actions được chọn → sử dụng customActions
908
- // Nếu không có actions nào được chọn → sử dụng random actions với activeActionCount
909
1031
  customActions = customActions,
910
- // Number of random actions (1-10), only used when customActions = null
911
- // activeActionCount = 2 → 2 random actions + STRAIGHT
912
1032
  activeActionCount = activeActionCount ?: 2,
913
1033
  forceCaptureTimeout = (forceCaptureTimeout ?: 0.0).toLong(),
914
1034
  selfieImage = imageFile,
@@ -2645,15 +2765,16 @@ class EKYCModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaMo
2645
2765
 
2646
2766
  @ReactMethod
2647
2767
  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
2768
+ Handler(Looper.getMainLooper()).post {
2769
+ val activity = getTrueCurrentActivity() ?: return@post
2770
+ val heightPx = (heightDp * activity.resources.displayMetrics.density).toInt()
2771
+ Log.d(TAG, "▶️ setExitSheetHeight: ${heightDp}dp → ${heightPx}px")
2772
+
2773
+ val sheet = currentExitSheet ?: return@post
2774
+ if (!sheet.isShowing) return@post
2654
2775
  val bs = sheet.findViewById<View>(com.google.android.material.R.id.design_bottom_sheet)
2655
- ?: return@runOnUiThread
2656
- val lp = bs.layoutParams ?: return@runOnUiThread
2776
+ ?: return@post
2777
+ val lp = bs.layoutParams ?: return@post
2657
2778
  lp.height = heightPx
2658
2779
  bs.layoutParams = lp
2659
2780
  val behavior = com.google.android.material.bottomsheet.BottomSheetBehavior.from(bs)
@@ -2675,6 +2796,10 @@ class EKYCModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaMo
2675
2796
  override fun onCatalystInstanceDestroy() {
2676
2797
  super.onCatalystInstanceDestroy()
2677
2798
  SDKeKYCExitHandlerManager.uiListener = null
2799
+ // Unregister lifecycle callback để tránh memory leak
2800
+ val app = reactApplicationContext.applicationContext as? Application
2801
+ app?.unregisterActivityLifecycleCallbacks(sdkActivityLifecycleCallbacks)
2802
+ activeSdkActivity = null
2678
2803
  // Dismiss sheet nếu vẫn đang hiển thị khi RN catalyst instance destroy
2679
2804
  val sheet = currentExitSheet
2680
2805
  currentExitSheet = null
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@finos_sdk/sdk-ekyc",
3
- "version": "1.4.9",
3
+ "version": "1.5.1",
4
4
  "description": "React Native SDK for eKYC - Vietnamese CCCD NFC reading, OCR, Liveness detection, Face matching, and C06, eSign, SmsOTP residence verification",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -5,7 +5,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  const react_1 = __importDefault(require("react"));
7
7
  const react_native_1 = require("react-native");
8
- const FinosEKYCModule_1 = require("../modules/FinosEKYCModule");
8
+ // Dùng NativeModules trực tiếp để tránh circular dependency với FinosEKYCModule
9
+ // (ExitConfirmSheet được load qua ExitSheetWrapper được import bởi FinosEKYCModule)
9
10
  const BellIcon = () => (<react_native_1.View style={styles.bellWrapper}>
10
11
  <react_native_1.View style={styles.bellMount}/>
11
12
  <react_native_1.View style={styles.bellBody}/>
@@ -17,10 +18,12 @@ const ExitConfirmSheet = (props) => {
17
18
  const confirmText = props.confirmText || 'Đồng ý';
18
19
  const cancelText = props.cancelText || 'Ở lại';
19
20
  const handleConfirm = async () => {
20
- await FinosEKYCModule_1.FinosEKYC.resolveExit('CONFIRM');
21
+ var _a, _b;
22
+ await ((_b = (_a = react_native_1.NativeModules.EKYCModule) === null || _a === void 0 ? void 0 : _a.resolveExit) === null || _b === void 0 ? void 0 : _b.call(_a, 'CONFIRM'));
21
23
  };
22
24
  const handleCancel = async () => {
23
- await FinosEKYCModule_1.FinosEKYC.resolveExit('CANCEL');
25
+ var _a, _b;
26
+ await ((_b = (_a = react_native_1.NativeModules.EKYCModule) === null || _a === void 0 ? void 0 : _a.resolveExit) === null || _b === void 0 ? void 0 : _b.call(_a, 'CANCEL'));
24
27
  };
25
28
  return (<react_native_1.View style={styles.container} onLayout={(e) => {
26
29
  var _a, _b;
@@ -826,7 +826,11 @@ const isMethod = (prop) => {
826
826
  prop === 'checkC06' ||
827
827
  prop === 'startOcr' ||
828
828
  prop === 'startLiveness' ||
829
- prop === 'startFaceCompare';
829
+ prop === 'startFaceCompare' ||
830
+ prop === 'registerExitHandler' ||
831
+ prop === 'resolveExit' ||
832
+ prop === 'showRNExitSheet' ||
833
+ prop === 'setExitSheetComponent';
830
834
  };
831
835
  // Create a comprehensive stub object with all methods to prevent undefined errors
832
836
  const createFinosEKYCStub = () => {
@@ -856,6 +860,7 @@ const createFinosEKYCStub = () => {
856
860
  'startEkycUI', 'sendOtp', 'verifyOtp', 'resendOtp', 'initializeESign', 'openSessionId',
857
861
  'registerDevice', 'listCerts', 'verifyCert', 'listSignRequest', 'confirmSign',
858
862
  'registerRemoteSigning', 'signPdf', 'sendConfirmationDocument',
863
+ 'registerExitHandler', 'resolveExit', 'showRNExitSheet', 'setExitSheetComponent',
859
864
  'onResume', 'onPause', 'isSDKReady', 'getSDKInfo'
860
865
  ];
861
866
  otherMethods.forEach(method => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@finos_sdk/sdk-ekyc",
3
- "version": "1.4.9",
3
+ "version": "1.5.1",
4
4
  "description": "React Native SDK for eKYC - Vietnamese CCCD NFC reading, OCR, Liveness detection, Face matching, and C06, eSign, SmsOTP residence verification",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -6,7 +6,9 @@ import {
6
6
  TouchableOpacity,
7
7
  NativeModules,
8
8
  } from 'react-native';
9
- import { FinosEKYC } from '../modules/FinosEKYCModule';
9
+
10
+ // Dùng NativeModules trực tiếp để tránh circular dependency với FinosEKYCModule
11
+ // (ExitConfirmSheet được load qua ExitSheetWrapper được import bởi FinosEKYCModule)
10
12
 
11
13
  const BellIcon = () => (
12
14
  <View style={styles.bellWrapper}>
@@ -23,11 +25,11 @@ const ExitConfirmSheet = (props: any) => {
23
25
  const cancelText = props.cancelText || 'Ở lại';
24
26
 
25
27
  const handleConfirm = async () => {
26
- await FinosEKYC.resolveExit('CONFIRM');
28
+ await NativeModules.EKYCModule?.resolveExit?.('CONFIRM');
27
29
  };
28
30
 
29
31
  const handleCancel = async () => {
30
- await FinosEKYC.resolveExit('CANCEL');
32
+ await NativeModules.EKYCModule?.resolveExit?.('CANCEL');
31
33
  };
32
34
 
33
35
  return (
@@ -1001,7 +1001,11 @@ const isMethod = (prop: string | symbol): boolean => {
1001
1001
  prop === 'checkC06' ||
1002
1002
  prop === 'startOcr' ||
1003
1003
  prop === 'startLiveness' ||
1004
- prop === 'startFaceCompare';
1004
+ prop === 'startFaceCompare' ||
1005
+ prop === 'registerExitHandler' ||
1006
+ prop === 'resolveExit' ||
1007
+ prop === 'showRNExitSheet' ||
1008
+ prop === 'setExitSheetComponent';
1005
1009
  };
1006
1010
 
1007
1011
  // Create a comprehensive stub object with all methods to prevent undefined errors
@@ -1035,6 +1039,7 @@ const createFinosEKYCStub = (): FinosEKYCModule => {
1035
1039
  'startEkycUI', 'sendOtp', 'verifyOtp', 'resendOtp', 'initializeESign', 'openSessionId',
1036
1040
  'registerDevice', 'listCerts', 'verifyCert', 'listSignRequest', 'confirmSign',
1037
1041
  'registerRemoteSigning', 'signPdf', 'sendConfirmationDocument',
1042
+ 'registerExitHandler', 'resolveExit', 'showRNExitSheet', 'setExitSheetComponent',
1038
1043
  'onResume', 'onPause', 'isSDKReady', 'getSDKInfo'
1039
1044
  ];
1040
1045