@finos_sdk/sdk-ekyc 1.4.8 → 1.5.0

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.
@@ -92,97 +92,154 @@ class EKYCModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaMo
92
92
  private var exitSheetResolvedViaButton = false
93
93
 
94
94
  private fun getTrueCurrentActivity(): Activity? {
95
- // Try React Native's currentActivity first
95
+ // Try React Native's currentActivity first as a baseline
96
96
  val rnActivity = currentActivity
97
- if (rnActivity != null && rnActivity.javaClass.simpleName != "MainActivity") {
97
+
98
+ Log.d(TAG, "🔍 getTrueCurrentActivity check: rnActivity=${rnActivity?.javaClass?.simpleName}")
99
+
100
+ // If rnActivity is already an SDK activity, we can likely trust it
101
+ if (rnActivity != null && rnActivity.javaClass.name.startsWith("finos.sdk.")) {
102
+ Log.d(TAG, "✅ Current activity is already an SDK activity: ${rnActivity.javaClass.simpleName}")
98
103
  return rnActivity
99
104
  }
100
105
 
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
106
+ // Use reflection to find the absolute top resumed activity.
107
+ // This is crucial when the SDK is running in a native Activity on top of the RN Activity,
108
+ // and currentActivity might still point to the background Activity.
109
+ try {
110
+ val topActivity = findTopActivityViaReflection()
111
+ if (topActivity != null) {
112
+ Log.d(TAG, "✅ Found top activity via reflection: ${topActivity.javaClass.simpleName}")
113
+ return topActivity
114
+ }
115
+ } catch (e: Exception) {
116
+ Log.e(TAG, "Failed to find top activity via reflection", e)
117
+ }
118
+
119
+ Log.d(TAG, "⚠️ Fallback to rnActivity: ${rnActivity?.javaClass?.simpleName}")
120
+ return rnActivity
121
+ }
122
+
123
+ /**
124
+ * Finds the top-most resumed activity using ActivityThread's internal records.
125
+ */
126
+ private fun findTopActivityViaReflection(): Activity? {
103
127
  try {
104
128
  val activityThreadClass = Class.forName("android.app.ActivityThread")
105
- val activityThread = activityThreadClass.getMethod("currentActivityThread").invoke(null)
129
+ val activityThread = activityThreadClass.getMethod("currentActivityThread").invoke(null) ?: return null
106
130
  val activitiesField = activityThreadClass.getDeclaredField("mActivities")
107
131
  activitiesField.isAccessible = true
108
132
 
109
- val activities = activitiesField.get(activityThread) as Map<Any, Any>
110
- Log.d(TAG, "🔍 Scanning ${activities.size} activities...")
111
-
112
- var topActivity: Activity? = null
133
+ val activities = activitiesField.get(activityThread) as? Map<*, *> ?: return null
134
+ Log.d(TAG, "🔍 Scanning ${activities.size} activities via reflection...")
113
135
 
136
+ var bestCandidate: Activity? = null
137
+ var highestPriority = -1
138
+
114
139
  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
140
+ if (activityRecord == null) continue
141
+ try {
142
+ val activityRecordClass = activityRecord.javaClass
143
+ val activityField = activityRecordClass.getDeclaredField("activity")
144
+ activityField.isAccessible = true
145
+ val activity = activityField.get(activityRecord) as? Activity ?: continue
146
+
147
+ if (activity.isFinishing) continue
148
+
149
+ val pausedField = activityRecordClass.getDeclaredField("paused")
150
+ pausedField.isAccessible = true
151
+ val isPaused = pausedField.get(activityRecord) as Boolean
152
+
153
+ val stoppedField = activityRecordClass.getDeclaredField("stopped")
154
+ stoppedField.isAccessible = true
155
+ val isStopped = stoppedField.get(activityRecord) as Boolean
156
+
157
+ if (isStopped) continue
158
+
159
+ // Identity check: prioritize known SDK activity patterns
160
+ val className = activity.javaClass.name
161
+ val isSDK = className.startsWith("finos.sdk.") ||
162
+ className.contains("EKYC", ignoreCase = true) ||
163
+ className.contains("Liveness", ignoreCase = true) ||
164
+ className.contains("OCR", ignoreCase = true) ||
165
+ className.contains("NFC", ignoreCase = true)
166
+
167
+ // Priority scoring:
168
+ // 3: Resumed SDK Activity
169
+ // 2: Resumed App Activity
170
+ // 1: Paused SDK Activity
171
+ // 0: Paused App Activity
172
+ val currentPriority = (if (isSDK) 1 else 0) + (if (!isPaused) 2 else 0)
173
+
174
+ Log.v(TAG, " [Audit] ${activity.javaClass.simpleName} -> Prio: $currentPriority (SDK=$isSDK, Paused=$isPaused)")
175
+
176
+ if (currentPriority > highestPriority) {
177
+ highestPriority = currentPriority
178
+ bestCandidate = activity
179
+ }
180
+
181
+ // Optimization: Found the absolute best target
182
+ if (highestPriority == 3) break
183
+
184
+ } catch (e: Exception) {
185
+ // Silently continue for individual activity record failures
134
186
  }
135
187
  }
136
- return topActivity ?: rnActivity
188
+
189
+ if (bestCandidate != null) {
190
+ Log.d(TAG, "✅ Best candidate found: ${bestCandidate.javaClass.simpleName} (Prio: $highestPriority)")
191
+ }
192
+
193
+ return bestCandidate
137
194
  } catch (e: Exception) {
138
- Log.e(TAG, "Error finding true current activity", e)
195
+ Log.e(TAG, " Critical error in findTopActivityViaReflection", e)
196
+ return null
139
197
  }
140
-
141
- return rnActivity
142
198
  }
143
199
 
144
200
  @ReactMethod
145
201
  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
- }
202
+ // Fix: Move activity detection to UI thread to avoid race conditions and ensure correct window attachment
203
+ Handler(Looper.getMainLooper()).post {
204
+ val activity = getTrueCurrentActivity() ?: run {
205
+ Log.e(TAG, "❌ showRNExitSheet failed: No visible activity found")
206
+ promise.reject("NO_ACTIVITY", "No visible activity found")
207
+ return@post
208
+ }
157
209
 
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)
210
+ if (activity.isFinishing || (android.os.Build.VERSION.SDK_INT >= 17 && activity.isDestroyed)) {
211
+ Log.e(TAG, "❌ showRNExitSheet failed: Activity is finishing or destroyed")
212
+ promise.reject("ACTIVITY_INVALID", "Activity is finishing or destroyed")
213
+ return@post
166
214
  }
167
- }
168
215
 
169
- activity.runOnUiThread {
216
+ // Guard chống double-resolve/reject promise
217
+ val promiseSettled = java.util.concurrent.atomic.AtomicBoolean(false)
218
+ fun safeResolve(value: Any?) {
219
+ if (promiseSettled.compareAndSet(false, true)) promise.resolve(value)
220
+ }
221
+ fun safeReject(code: String, msg: String, e: Throwable? = null) {
222
+ if (promiseSettled.compareAndSet(false, true)) {
223
+ if (e != null) promise.reject(code, msg, e) else promise.reject(code, msg)
224
+ }
225
+ }
170
226
  try {
171
227
  val activityName = activity.javaClass.simpleName
172
228
  Log.d(TAG, "▶️ showRNExitSheet: Top Activity=$activityName, bundle=$bundleName")
173
229
 
174
230
  val reactApplication = activity.application as? ReactApplication
175
231
  if (reactApplication == null) {
176
- Log.e(TAG, "❌ Application does not implement ReactApplication")
232
+ 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
233
  safeReject("NO_REACT_APP", "Application does not implement ReactApplication")
178
- return@runOnUiThread
234
+ return@post
179
235
  }
180
236
  val reactInstanceManager = reactApplication.reactNativeHost.reactInstanceManager
181
237
 
182
238
  if (reactInstanceManager.currentReactContext != null) {
239
+ Log.d(TAG, "✅ ReactContext is ready, presenting sheet...")
183
240
  presentRNBottomSheet(activity, reactInstanceManager, bundleName, initialProps,
184
241
  ::safeResolve, ::safeReject)
185
- return@runOnUiThread
242
+ return@post
186
243
  }
187
244
 
188
245
  // Context chưa sẵn sàng: chờ listener
@@ -212,7 +269,7 @@ class EKYCModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaMo
212
269
  Log.d(TAG, "✅ ReactContext init xong giữa lúc add listener — present luôn")
213
270
  presentRNBottomSheet(activity, reactInstanceManager, bundleName, initialProps,
214
271
  ::safeResolve, ::safeReject)
215
- return@runOnUiThread
272
+ return@post
216
273
  }
217
274
 
218
275
  // Chỉ trigger background create nếu chưa start (tránh IllegalStateException)
@@ -250,12 +307,17 @@ class EKYCModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaMo
250
307
  safeReject: (String, String, Throwable?) -> Unit
251
308
  ) {
252
309
  try {
310
+ val minHeightPx = (400 * activity.resources.displayMetrics.density).toInt()
253
311
  val container = LinearLayout(activity).apply {
254
312
  orientation = LinearLayout.VERTICAL
255
313
  setBackgroundColor(Color.TRANSPARENT)
314
+ minimumHeight = minHeightPx
256
315
  }
257
316
 
258
317
  val rootView = ReactRootView(activity)
318
+ // Hardening: Set a minimum height to ensure the sheet is visible even before RN finishes layout
319
+ rootView.minimumHeight = minHeightPx
320
+
259
321
  rootView.layoutParams = LinearLayout.LayoutParams(
260
322
  LinearLayout.LayoutParams.MATCH_PARENT,
261
323
  LinearLayout.LayoutParams.WRAP_CONTENT
@@ -328,7 +390,7 @@ class EKYCModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaMo
328
390
 
329
391
  bottomSheetDialog.show()
330
392
 
331
- Log.d(TAG, "✅ showRNExitSheet: BottomSheetDialog shown (wrap content), bundle=$bundleName")
393
+ Log.d(TAG, "✅ showRNExitSheet: BottomSheetDialog shown on ${activity.javaClass.simpleName}, bundle=$bundleName")
332
394
  safeResolve(true)
333
395
  } catch (e: Exception) {
334
396
  Log.e(TAG, "❌ Exception in presentRNBottomSheet", e)
@@ -903,12 +965,7 @@ class EKYCModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaMo
903
965
  isActiveLiveness = isActiveLiveness ?: false,
904
966
  isActiveLivenessColor = isActiveLivenessColor ?: false,
905
967
  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
968
  customActions = customActions,
910
- // Number of random actions (1-10), only used when customActions = null
911
- // activeActionCount = 2 → 2 random actions + STRAIGHT
912
969
  activeActionCount = activeActionCount ?: 2,
913
970
  forceCaptureTimeout = (forceCaptureTimeout ?: 0.0).toLong(),
914
971
  selfieImage = imageFile,
@@ -2645,15 +2702,16 @@ class EKYCModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaMo
2645
2702
 
2646
2703
  @ReactMethod
2647
2704
  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
2705
+ Handler(Looper.getMainLooper()).post {
2706
+ val activity = getTrueCurrentActivity() ?: return@post
2707
+ val heightPx = (heightDp * activity.resources.displayMetrics.density).toInt()
2708
+ Log.d(TAG, "▶️ setExitSheetHeight: ${heightDp}dp → ${heightPx}px")
2709
+
2710
+ val sheet = currentExitSheet ?: return@post
2711
+ if (!sheet.isShowing) return@post
2654
2712
  val bs = sheet.findViewById<View>(com.google.android.material.R.id.design_bottom_sheet)
2655
- ?: return@runOnUiThread
2656
- val lp = bs.layoutParams ?: return@runOnUiThread
2713
+ ?: return@post
2714
+ val lp = bs.layoutParams ?: return@post
2657
2715
  lp.height = heightPx
2658
2716
  bs.layoutParams = lp
2659
2717
  val behavior = com.google.android.material.bottomsheet.BottomSheetBehavior.from(bs)
package/dist/index.js CHANGED
@@ -32,10 +32,14 @@ var __importStar = (this && this.__importStar) || (function () {
32
32
  return result;
33
33
  };
34
34
  })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
35
38
  Object.defineProperty(exports, "__esModule", { value: true });
36
39
  exports.OCR_USE_NEW_ID_DOCUMENT_2 = exports.OCR_USE_NEW_ID_DOCUMENT_1 = exports.OCR_FAKE_MRZ = exports.OCR_INVALID_GENDER_CODE = exports.OCR_INVALID_ID_CARD = exports.OCR_GLARE_ID_CARD = exports.OCR_BLURRY_ID_CARD = exports.OCR_CUT_CORNER_ID_CARD = exports.OCR_CANNOT_RECOGNIZE_PORTRAIT = exports.OCR_ID_CARD_FROM_OTHER_DEVICE = exports.OCR_NOT_ORIGINAL_ID_CARD = exports.OCR_MISSING_ID_CARD_PART = exports.OCR_CANNOT_GET_ISSUE_PLACE = exports.OCR_CANNOT_GET_GENDER = exports.OCR_CANNOT_GET_HOMETOWN = exports.OCR_CANNOT_GET_RESIDENCE = exports.OCR_CANNOT_GET_ISSUE_DATE = exports.OCR_CANNOT_GET_EXPIRY_DATE = exports.OCR_CANNOT_GET_BIRTH_YEAR = exports.OCR_CANNOT_GET_NAME = exports.OCR_CANNOT_GET_ID_NUMBER = exports.OCR_WRONG_ID_CARD_SIDE = exports.OCR_UNRECOGNIZED_ID_CARD = exports.SDK_MISS_KEY = exports.ERROR_UNKNOWN = exports.SDK_START_ERROR = exports.SDK_START_FLOW_ERROR = exports.getMessage = exports.getErrorResultFromDetails = exports.createCustom = exports.fromCode = exports.getLocalizedMessage = exports.parseNfcResponse = exports.AuthorizationStatus = exports.AppIDType = exports.getEkycError = exports.EKYCErrorEvent = exports.EKYCEvent = exports.customActionsToStrings = exports.SDK_LIVENESS_ACTIONS = exports.SDKFaceDetectStatus = exports.flowToStrings = exports.SDK_FLOW_OPTIONS = exports.SDKFlowType = exports.FinosESignModule = exports.FinosESign = exports.FinosEKYCModule = exports.FinosEKYC = exports.SDKeKYC = exports.sdkEKYC = void 0;
37
40
  exports.ESIGN_INVALID_USER_ACCOUNT = exports.ESIGN_APP_LIMITED = exports.ESIGN_DEVICE_NOT_INIT = exports.ESIGN_INVALID_LICENSE_CONTENT = exports.ESIGN_INVALID_LICENSE = exports.ESIGN_DEVICE_ALREADY_INIT = exports.ESIGN_SESSION_INVALID = exports.FETCH_HISTORY_ERROR = exports.QRCODE_ERROR = exports.SMS_OTP_RATE_LIMIT = exports.SMS_OTP_NOT_FOUND = exports.SMS_OTP_MAX_ATTEMPTS = exports.SMS_OTP_INVALID_PHONE = exports.SMS_OTP_ERROR = exports.NFC_CHIP_AUTH_FAILED = exports.NFC_USER_CANCEL = exports.NFC_UNKNOWN_ERROR = exports.NFC_IO_ERROR = exports.NFC_INVALID_MRZ_KEY = exports.NFC_CONNECTION_LOST = exports.NFC_MUTUAL_AUTH_FAILED = exports.C06_ERROR = exports.SCAN_NFC_ENABLE = exports.SCAN_NFC_CHECK = exports.SCAN_NFC_ERROR = exports.HEAD_IS_TURNED_IN_SELFIE = exports.NUDITY_DETECTED_IN_SELFIE = exports.READING_GLASSES_DETECTED_IN_SELFIE = exports.EYEWEAR_DETECTED_IN_SELFIE = exports.FACE_OCCLUDED_IN_SELFIE = exports.FACE_IS_BLURRED = exports.MASK_PRESENT_IN_SELFIE = exports.EYES_CLOSED_IN_SELFIE = exports.MULTIPLE_FACES_IN_SELFIE = exports.LIVENESS_FAIL = exports.LIVENESS_ERROR = exports.FACE_ERROR = exports.FACE_HAT_ERROR = exports.OCR_ERROR = exports.OCR_FONT_BACK_NOT_MATCH = exports.OCR_PHOTOCOPY_ID_CARD = exports.OCR_FAKE_PORTRAIT_DETECTED = exports.OCR_FAKE_CHARACTERS_DETECTED_2 = exports.OCR_FAKE_CHARACTERS_DETECTED_1 = exports.OCR_UNKNOWN_ID_NUMBER_LENGTH = exports.OCR_MODIFIED_BIRTH_DATE_DETECTED_CMND = exports.OCR_MODIFIED_SYMBOL_DETECTED_CMND = exports.OCR_FAKE_BIRTH_DATE_DETECTED_CMND = exports.OCR_FAKE_PORTRAIT_DETECTED_CMND = exports.OCR_FAKE_CHARACTERS_DETECTED_CMND = void 0;
38
41
  exports.SDK_NAME = exports.SDK_VERSION = exports.USER_CANCEL = exports.ESIGN_INVALID_PIN_CODE = exports.ESIGN_INVALID_RECOVERY_CODE = exports.ESIGN_INVALID_LICENSE_CODE = exports.ESIGN_INVALID_CONTEXT = exports.ESIGN_MISSING_REQUEST_ID = exports.ESIGN_MISSING_SERIAL = exports.ESIGN_MISSING_IDENTITY = exports.ESIGN_MISSING_ACCESS_TOKEN = exports.ESIGN_NO_SESSION_ID = exports.ESIGN_MISSING_CONFIRMATION_DOC = exports.ESIGN_MISSING_REQUEST_JSON = exports.ESIGN_MISSING_CCCD = exports.ESIGN_MISSING_TOKEN = exports.ESIGN_ERROR_UNKNOWN = exports.ESIGN_AUTH_REQUEST_EXISTS = exports.ESIGN_INVALID_CERT_FOR_AUTH = exports.ESIGN_INVALID_SIGN_COUNT_OR_TIME = exports.ESIGN_AUTH_EXISTS = exports.ESIGN_SESSION_INVALID_LIST_CERT = exports.ESIGN_INVALID_RECOVERY_OR_PIN = exports.ESIGN_SESSION_INVALID_REGISTER = void 0;
42
+ const react_native_1 = require("react-native");
39
43
  const EKYCModule_1 = __importStar(require("./EKYCModule"));
40
44
  exports.sdkEKYC = EKYCModule_1.default;
41
45
  Object.defineProperty(exports, "SDKeKYC", { enumerable: true, get: function () { return EKYCModule_1.SDKeKYC; } });
@@ -47,6 +51,9 @@ Object.defineProperty(exports, "FinosEKYCModule", { enumerable: true, get: funct
47
51
  const FinosESignModule_1 = require("./src/modules/FinosESignModule");
48
52
  Object.defineProperty(exports, "FinosESign", { enumerable: true, get: function () { return FinosESignModule_1.FinosESign; } });
49
53
  Object.defineProperty(exports, "FinosESignModule", { enumerable: true, get: function () { return FinosESignModule_1.FinosESignModule; } });
54
+ const ExitSheetWrapper_1 = __importDefault(require("./src/components/ExitSheetWrapper"));
55
+ // Auto-register exit sheet wrapper — khách hàng không cần khai báo AppRegistry
56
+ react_native_1.AppRegistry.registerComponent('SDKExitSheetWrapper', () => ExitSheetWrapper_1.default);
50
57
  console.log('✅ SDK modules loaded successfully');
51
58
  // Export main SDK instance and class (legacy)
52
59
  exports.default = EKYCModule_1.default;
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@finos_sdk/sdk-ekyc",
3
- "version": "1.4.8",
3
+ "version": "1.5.0",
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",
@@ -0,0 +1,3 @@
1
+ import React from 'react';
2
+ declare const ExitConfirmSheet: (props: any) => React.JSX.Element;
3
+ export default ExitConfirmSheet;
@@ -0,0 +1,164 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const react_1 = __importDefault(require("react"));
7
+ const react_native_1 = require("react-native");
8
+ const FinosEKYCModule_1 = require("../modules/FinosEKYCModule");
9
+ const BellIcon = () => (<react_native_1.View style={styles.bellWrapper}>
10
+ <react_native_1.View style={styles.bellMount}/>
11
+ <react_native_1.View style={styles.bellBody}/>
12
+ <react_native_1.View style={styles.bellSkirt}/>
13
+ <react_native_1.View style={styles.bellClapper}/>
14
+ </react_native_1.View>);
15
+ const ExitConfirmSheet = (props) => {
16
+ const message = props.message || 'Giao dịch bị hủy khi điều hướng sang tính năng tiếp theo. Quý khách có chắc chắn hủy giao dịch?';
17
+ const confirmText = props.confirmText || 'Đồng ý';
18
+ const cancelText = props.cancelText || 'Ở lại';
19
+ const handleConfirm = async () => {
20
+ await FinosEKYCModule_1.FinosEKYC.resolveExit('CONFIRM');
21
+ };
22
+ const handleCancel = async () => {
23
+ await FinosEKYCModule_1.FinosEKYC.resolveExit('CANCEL');
24
+ };
25
+ return (<react_native_1.View style={styles.container} onLayout={(e) => {
26
+ var _a, _b;
27
+ const h = e.nativeEvent.layout.height;
28
+ if (h > 0)
29
+ (_b = (_a = react_native_1.NativeModules.EKYCModule) === null || _a === void 0 ? void 0 : _a.setExitSheetHeight) === null || _b === void 0 ? void 0 : _b.call(_a, h);
30
+ }}>
31
+ <react_native_1.View style={styles.handle}/>
32
+
33
+ <react_native_1.View style={styles.content}>
34
+ <BellIcon />
35
+ <react_native_1.Text style={styles.message}>{message}</react_native_1.Text>
36
+ </react_native_1.View>
37
+
38
+ <react_native_1.View style={styles.footer}>
39
+ <react_native_1.TouchableOpacity style={styles.confirmButton} onPress={handleConfirm} activeOpacity={0.8}>
40
+ <react_native_1.View style={styles.gradientContainer}>
41
+ <react_native_1.View style={[styles.gradientStep, { backgroundColor: '#E53935' }]}/>
42
+ <react_native_1.View style={[styles.gradientStep, { backgroundColor: '#EA4F28' }]}/>
43
+ <react_native_1.View style={[styles.gradientStep, { backgroundColor: '#EF6519' }]}/>
44
+ <react_native_1.View style={[styles.gradientStep, { backgroundColor: '#F47A0B' }]}/>
45
+ <react_native_1.View style={[styles.gradientStep, { backgroundColor: '#F98F00' }]}/>
46
+ <react_native_1.View style={[styles.gradientStep, { backgroundColor: '#FCA300' }]}/>
47
+ <react_native_1.View style={[styles.gradientStep, { backgroundColor: '#FFB800' }]}/>
48
+ <react_native_1.View style={[styles.gradientStep, { backgroundColor: '#FFCC00' }]}/>
49
+ <react_native_1.View style={[styles.gradientStep, { backgroundColor: '#FFD600' }]}/>
50
+ </react_native_1.View>
51
+ <react_native_1.Text style={styles.confirmText}>{confirmText}</react_native_1.Text>
52
+ </react_native_1.TouchableOpacity>
53
+
54
+ <react_native_1.TouchableOpacity style={styles.cancelButton} onPress={handleCancel} activeOpacity={0.7}>
55
+ <react_native_1.Text style={styles.cancelText}>{cancelText}</react_native_1.Text>
56
+ </react_native_1.TouchableOpacity>
57
+ </react_native_1.View>
58
+ </react_native_1.View>);
59
+ };
60
+ const styles = react_native_1.StyleSheet.create({
61
+ container: {
62
+ backgroundColor: 'white',
63
+ paddingHorizontal: 24,
64
+ paddingBottom: 32,
65
+ borderTopLeftRadius: 32,
66
+ borderTopRightRadius: 32,
67
+ },
68
+ handle: {
69
+ width: 40,
70
+ height: 4,
71
+ backgroundColor: '#CCCCCC',
72
+ borderRadius: 2,
73
+ alignSelf: 'center',
74
+ marginTop: 12,
75
+ marginBottom: 20,
76
+ },
77
+ content: {
78
+ alignItems: 'center',
79
+ marginBottom: 28,
80
+ },
81
+ bellWrapper: {
82
+ alignItems: 'center',
83
+ width: 72,
84
+ height: 72,
85
+ marginBottom: 20,
86
+ },
87
+ bellMount: {
88
+ width: 9,
89
+ height: 7,
90
+ borderRadius: 4,
91
+ backgroundColor: '#E53935',
92
+ marginTop: 7,
93
+ },
94
+ bellBody: {
95
+ width: 36,
96
+ height: 34,
97
+ borderTopLeftRadius: 18,
98
+ borderTopRightRadius: 18,
99
+ backgroundColor: '#E53935',
100
+ marginTop: -1,
101
+ },
102
+ bellSkirt: {
103
+ width: 48,
104
+ height: 9,
105
+ borderBottomLeftRadius: 5,
106
+ borderBottomRightRadius: 5,
107
+ backgroundColor: '#E53935',
108
+ },
109
+ bellClapper: {
110
+ width: 12,
111
+ height: 7,
112
+ borderBottomLeftRadius: 6,
113
+ borderBottomRightRadius: 6,
114
+ backgroundColor: '#E53935',
115
+ marginTop: 2,
116
+ },
117
+ message: {
118
+ fontSize: 15,
119
+ color: '#1A1A1A',
120
+ textAlign: 'center',
121
+ lineHeight: 19.5,
122
+ },
123
+ footer: {
124
+ width: '100%',
125
+ gap: 12,
126
+ },
127
+ confirmButton: {
128
+ width: '100%',
129
+ height: 52,
130
+ borderRadius: 24,
131
+ alignItems: 'center',
132
+ justifyContent: 'center',
133
+ overflow: 'hidden',
134
+ elevation: 4,
135
+ shadowColor: '#E53935',
136
+ shadowOffset: { width: 0, height: 4 },
137
+ shadowOpacity: 0.3,
138
+ shadowRadius: 8,
139
+ },
140
+ gradientContainer: Object.assign(Object.assign({}, react_native_1.StyleSheet.absoluteFillObject), { flexDirection: 'row' }),
141
+ gradientStep: {
142
+ flex: 1,
143
+ },
144
+ confirmText: {
145
+ fontSize: 16,
146
+ fontWeight: '700',
147
+ color: '#fff',
148
+ },
149
+ cancelButton: {
150
+ width: '100%',
151
+ height: 52,
152
+ borderRadius: 24,
153
+ borderWidth: 1.5,
154
+ borderColor: '#CCCCCC',
155
+ alignItems: 'center',
156
+ justifyContent: 'center',
157
+ },
158
+ cancelText: {
159
+ fontSize: 16,
160
+ fontWeight: '600',
161
+ color: '#555555',
162
+ },
163
+ });
164
+ exports.default = ExitConfirmSheet;
@@ -0,0 +1,12 @@
1
+ import React from 'react';
2
+ /**
3
+ * Set custom exit sheet component.
4
+ * Gọi trước khi showRNExitSheet để override default ExitConfirmSheet.
5
+ */
6
+ export declare function setCustomExitComponent(component: React.ComponentType<any> | null): void;
7
+ /**
8
+ * Wrapper component được SDK auto-register với tên 'SDKExitSheetWrapper'.
9
+ * Render custom component nếu được set, fallback về ExitConfirmSheet mặc định.
10
+ */
11
+ declare const ExitSheetWrapper: (props: any) => React.JSX.Element;
12
+ export default ExitSheetWrapper;
@@ -0,0 +1,26 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.setCustomExitComponent = setCustomExitComponent;
7
+ const react_1 = __importDefault(require("react"));
8
+ const ExitConfirmSheet_1 = __importDefault(require("./ExitConfirmSheet"));
9
+ // Module-level variable — set bởi FinosEKYC.setExitSheetComponent()
10
+ let _customComponent = null;
11
+ /**
12
+ * Set custom exit sheet component.
13
+ * Gọi trước khi showRNExitSheet để override default ExitConfirmSheet.
14
+ */
15
+ function setCustomExitComponent(component) {
16
+ _customComponent = component;
17
+ }
18
+ /**
19
+ * Wrapper component được SDK auto-register với tên 'SDKExitSheetWrapper'.
20
+ * Render custom component nếu được set, fallback về ExitConfirmSheet mặc định.
21
+ */
22
+ const ExitSheetWrapper = (props) => {
23
+ const Component = _customComponent || ExitConfirmSheet_1.default;
24
+ return <Component {...props}/>;
25
+ };
26
+ exports.default = ExitSheetWrapper;
@@ -1,3 +1,4 @@
1
+ import React from 'react';
1
2
  import { SDK_VERSION, SDK_NAME } from '../../EKYCModule';
2
3
  import { NfcConfig } from '../types/ekycNFCType';
3
4
  import { SDKEkycResultWithEvent, SDKEkycResultStringWithEvent, EKYCError, AppIDType } from '../types/ekycType';
@@ -110,13 +111,31 @@ export declare class FinosEKYCModule {
110
111
  */
111
112
  resolveExit(action: 'CONFIRM' | 'CANCEL' | 'CLOSE'): Promise<boolean>;
112
113
  /**
113
- * Show a React Native component as a Bottom Sheet on top of the current activity.
114
- * Use this to show a custom RN UI over SDK screens.
114
+ * Set custom component to render inside the exit bottom sheet.
115
+ * Thay thế ExitConfirmSheet mặc định bằng component tùy chỉnh.
116
+ * Không cần AppRegistry — SDK tự lo phần registration.
115
117
  *
116
- * @param componentName The name registered with AppRegistry.registerComponent
117
- * @param props Initial props to pass to the component
118
+ * @param component React component để hiển thị, hoặc null để về default
119
+ *
120
+ * @example
121
+ * FinosEKYC.setExitSheetComponent(WarningBottomSheet);
122
+ */
123
+ setExitSheetComponent(component: React.ComponentType<any> | null): void;
124
+ /**
125
+ * Show the exit bottom sheet on top of the current SDK screen.
126
+ * Mặc định dùng 'SDKExitSheetWrapper' (đã register sẵn trong SDK).
127
+ *
128
+ * @param props Props truyền vào component (optional)
129
+ *
130
+ * @example
131
+ * // Dùng default ExitConfirmSheet
132
+ * FinosEKYC.showRNExitSheet();
133
+ *
134
+ * // Dùng custom component (set trước bằng setExitSheetComponent)
135
+ * FinosEKYC.setExitSheetComponent(WarningBottomSheet);
136
+ * FinosEKYC.showRNExitSheet();
118
137
  */
119
- showRNExitSheet(componentName: string, props?: any): Promise<boolean>;
138
+ showRNExitSheet(props?: any): Promise<boolean>;
120
139
  /**
121
140
  * Show a native alert dialog on top of the current activity (useful for SDK screens)
122
141
  * @param config Dialog configuration { title, message, confirmText, cancelText }
@@ -38,6 +38,7 @@ const react_native_1 = require("react-native");
38
38
  const EKYCModule_1 = __importStar(require("../../EKYCModule"));
39
39
  Object.defineProperty(exports, "SDK_VERSION", { enumerable: true, get: function () { return EKYCModule_1.SDK_VERSION; } });
40
40
  Object.defineProperty(exports, "SDK_NAME", { enumerable: true, get: function () { return EKYCModule_1.SDK_NAME; } });
41
+ const ExitSheetWrapper_1 = require("../components/ExitSheetWrapper");
41
42
  const ekycFlowType_1 = require("../types/ekycFlowType");
42
43
  /**
43
44
  * Finos eKYC SDK Module
@@ -313,16 +314,36 @@ class FinosEKYCModule {
313
314
  }
314
315
  }
315
316
  /**
316
- * Show a React Native component as a Bottom Sheet on top of the current activity.
317
- * Use this to show a custom RN UI over SDK screens.
317
+ * Set custom component to render inside the exit bottom sheet.
318
+ * Thay thế ExitConfirmSheet mặc định bằng component tùy chỉnh.
319
+ * Không cần AppRegistry — SDK tự lo phần registration.
318
320
  *
319
- * @param componentName The name registered with AppRegistry.registerComponent
320
- * @param props Initial props to pass to the component
321
+ * @param component React component để hiển thị, hoặc null để về default
322
+ *
323
+ * @example
324
+ * FinosEKYC.setExitSheetComponent(WarningBottomSheet);
325
+ */
326
+ setExitSheetComponent(component) {
327
+ (0, ExitSheetWrapper_1.setCustomExitComponent)(component);
328
+ }
329
+ /**
330
+ * Show the exit bottom sheet on top of the current SDK screen.
331
+ * Mặc định dùng 'SDKExitSheetWrapper' (đã register sẵn trong SDK).
332
+ *
333
+ * @param props Props truyền vào component (optional)
334
+ *
335
+ * @example
336
+ * // Dùng default ExitConfirmSheet
337
+ * FinosEKYC.showRNExitSheet();
338
+ *
339
+ * // Dùng custom component (set trước bằng setExitSheetComponent)
340
+ * FinosEKYC.setExitSheetComponent(WarningBottomSheet);
341
+ * FinosEKYC.showRNExitSheet();
321
342
  */
322
- async showRNExitSheet(componentName, props = {}) {
343
+ async showRNExitSheet(props = {}) {
323
344
  this.validateSDKReady();
324
345
  try {
325
- return await this.sdk.showRNExitSheet(componentName, props);
346
+ return await this.sdk.showRNExitSheet('SDKExitSheetWrapper', props);
326
347
  }
327
348
  catch (error) {
328
349
  console.error('❌ Failed to show RN exit sheet:', error);
@@ -805,7 +826,11 @@ const isMethod = (prop) => {
805
826
  prop === 'checkC06' ||
806
827
  prop === 'startOcr' ||
807
828
  prop === 'startLiveness' ||
808
- prop === 'startFaceCompare';
829
+ prop === 'startFaceCompare' ||
830
+ prop === 'registerExitHandler' ||
831
+ prop === 'resolveExit' ||
832
+ prop === 'showRNExitSheet' ||
833
+ prop === 'setExitSheetComponent';
809
834
  };
810
835
  // Create a comprehensive stub object with all methods to prevent undefined errors
811
836
  const createFinosEKYCStub = () => {
@@ -835,6 +860,7 @@ const createFinosEKYCStub = () => {
835
860
  'startEkycUI', 'sendOtp', 'verifyOtp', 'resendOtp', 'initializeESign', 'openSessionId',
836
861
  'registerDevice', 'listCerts', 'verifyCert', 'listSignRequest', 'confirmSign',
837
862
  'registerRemoteSigning', 'signPdf', 'sendConfirmationDocument',
863
+ 'registerExitHandler', 'resolveExit', 'showRNExitSheet', 'setExitSheetComponent',
838
864
  'onResume', 'onPause', 'isSDKReady', 'getSDKInfo'
839
865
  ];
840
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.8",
3
+ "version": "1.5.0",
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",
@@ -0,0 +1,180 @@
1
+ import React from 'react';
2
+ import {
3
+ View,
4
+ Text,
5
+ StyleSheet,
6
+ TouchableOpacity,
7
+ NativeModules,
8
+ } from 'react-native';
9
+ import { FinosEKYC } from '../modules/FinosEKYCModule';
10
+
11
+ const BellIcon = () => (
12
+ <View style={styles.bellWrapper}>
13
+ <View style={styles.bellMount} />
14
+ <View style={styles.bellBody} />
15
+ <View style={styles.bellSkirt} />
16
+ <View style={styles.bellClapper} />
17
+ </View>
18
+ );
19
+
20
+ const ExitConfirmSheet = (props: any) => {
21
+ const message = props.message || 'Giao dịch bị hủy khi điều hướng sang tính năng tiếp theo. Quý khách có chắc chắn hủy giao dịch?';
22
+ const confirmText = props.confirmText || 'Đồng ý';
23
+ const cancelText = props.cancelText || 'Ở lại';
24
+
25
+ const handleConfirm = async () => {
26
+ await FinosEKYC.resolveExit('CONFIRM');
27
+ };
28
+
29
+ const handleCancel = async () => {
30
+ await FinosEKYC.resolveExit('CANCEL');
31
+ };
32
+
33
+ return (
34
+ <View
35
+ style={styles.container}
36
+ onLayout={(e) => {
37
+ const h = e.nativeEvent.layout.height;
38
+ if (h > 0) NativeModules.EKYCModule?.setExitSheetHeight?.(h);
39
+ }}
40
+ >
41
+ <View style={styles.handle} />
42
+
43
+ <View style={styles.content}>
44
+ <BellIcon />
45
+ <Text style={styles.message}>{message}</Text>
46
+ </View>
47
+
48
+ <View style={styles.footer}>
49
+ <TouchableOpacity style={styles.confirmButton} onPress={handleConfirm} activeOpacity={0.8}>
50
+ <View style={styles.gradientContainer}>
51
+ <View style={[styles.gradientStep, { backgroundColor: '#E53935' }]} />
52
+ <View style={[styles.gradientStep, { backgroundColor: '#EA4F28' }]} />
53
+ <View style={[styles.gradientStep, { backgroundColor: '#EF6519' }]} />
54
+ <View style={[styles.gradientStep, { backgroundColor: '#F47A0B' }]} />
55
+ <View style={[styles.gradientStep, { backgroundColor: '#F98F00' }]} />
56
+ <View style={[styles.gradientStep, { backgroundColor: '#FCA300' }]} />
57
+ <View style={[styles.gradientStep, { backgroundColor: '#FFB800' }]} />
58
+ <View style={[styles.gradientStep, { backgroundColor: '#FFCC00' }]} />
59
+ <View style={[styles.gradientStep, { backgroundColor: '#FFD600' }]} />
60
+ </View>
61
+ <Text style={styles.confirmText}>{confirmText}</Text>
62
+ </TouchableOpacity>
63
+
64
+ <TouchableOpacity style={styles.cancelButton} onPress={handleCancel} activeOpacity={0.7}>
65
+ <Text style={styles.cancelText}>{cancelText}</Text>
66
+ </TouchableOpacity>
67
+ </View>
68
+ </View>
69
+ );
70
+ };
71
+
72
+ const styles = StyleSheet.create({
73
+ container: {
74
+ backgroundColor: 'white',
75
+ paddingHorizontal: 24,
76
+ paddingBottom: 32,
77
+ borderTopLeftRadius: 32,
78
+ borderTopRightRadius: 32,
79
+ },
80
+ handle: {
81
+ width: 40,
82
+ height: 4,
83
+ backgroundColor: '#CCCCCC',
84
+ borderRadius: 2,
85
+ alignSelf: 'center',
86
+ marginTop: 12,
87
+ marginBottom: 20,
88
+ },
89
+ content: {
90
+ alignItems: 'center',
91
+ marginBottom: 28,
92
+ },
93
+ bellWrapper: {
94
+ alignItems: 'center',
95
+ width: 72,
96
+ height: 72,
97
+ marginBottom: 20,
98
+ },
99
+ bellMount: {
100
+ width: 9,
101
+ height: 7,
102
+ borderRadius: 4,
103
+ backgroundColor: '#E53935',
104
+ marginTop: 7,
105
+ },
106
+ bellBody: {
107
+ width: 36,
108
+ height: 34,
109
+ borderTopLeftRadius: 18,
110
+ borderTopRightRadius: 18,
111
+ backgroundColor: '#E53935',
112
+ marginTop: -1,
113
+ },
114
+ bellSkirt: {
115
+ width: 48,
116
+ height: 9,
117
+ borderBottomLeftRadius: 5,
118
+ borderBottomRightRadius: 5,
119
+ backgroundColor: '#E53935',
120
+ },
121
+ bellClapper: {
122
+ width: 12,
123
+ height: 7,
124
+ borderBottomLeftRadius: 6,
125
+ borderBottomRightRadius: 6,
126
+ backgroundColor: '#E53935',
127
+ marginTop: 2,
128
+ },
129
+ message: {
130
+ fontSize: 15,
131
+ color: '#1A1A1A',
132
+ textAlign: 'center',
133
+ lineHeight: 19.5,
134
+ },
135
+ footer: {
136
+ width: '100%',
137
+ gap: 12,
138
+ },
139
+ confirmButton: {
140
+ width: '100%',
141
+ height: 52,
142
+ borderRadius: 24,
143
+ alignItems: 'center',
144
+ justifyContent: 'center',
145
+ overflow: 'hidden',
146
+ elevation: 4,
147
+ shadowColor: '#E53935',
148
+ shadowOffset: { width: 0, height: 4 },
149
+ shadowOpacity: 0.3,
150
+ shadowRadius: 8,
151
+ },
152
+ gradientContainer: {
153
+ ...StyleSheet.absoluteFillObject,
154
+ flexDirection: 'row',
155
+ },
156
+ gradientStep: {
157
+ flex: 1,
158
+ },
159
+ confirmText: {
160
+ fontSize: 16,
161
+ fontWeight: '700',
162
+ color: '#fff',
163
+ },
164
+ cancelButton: {
165
+ width: '100%',
166
+ height: 52,
167
+ borderRadius: 24,
168
+ borderWidth: 1.5,
169
+ borderColor: '#CCCCCC',
170
+ alignItems: 'center',
171
+ justifyContent: 'center',
172
+ },
173
+ cancelText: {
174
+ fontSize: 16,
175
+ fontWeight: '600',
176
+ color: '#555555',
177
+ },
178
+ });
179
+
180
+ export default ExitConfirmSheet;
@@ -0,0 +1,24 @@
1
+ import React from 'react';
2
+ import ExitConfirmSheet from './ExitConfirmSheet';
3
+
4
+ // Module-level variable — set bởi FinosEKYC.setExitSheetComponent()
5
+ let _customComponent: React.ComponentType<any> | null = null;
6
+
7
+ /**
8
+ * Set custom exit sheet component.
9
+ * Gọi trước khi showRNExitSheet để override default ExitConfirmSheet.
10
+ */
11
+ export function setCustomExitComponent(component: React.ComponentType<any> | null) {
12
+ _customComponent = component;
13
+ }
14
+
15
+ /**
16
+ * Wrapper component được SDK auto-register với tên 'SDKExitSheetWrapper'.
17
+ * Render custom component nếu được set, fallback về ExitConfirmSheet mặc định.
18
+ */
19
+ const ExitSheetWrapper = (props: any) => {
20
+ const Component = _customComponent || ExitConfirmSheet;
21
+ return <Component {...props} />;
22
+ };
23
+
24
+ export default ExitSheetWrapper;
@@ -1,5 +1,7 @@
1
+ import React from 'react';
1
2
  import { Platform, DeviceEventEmitter } from 'react-native';
2
3
  import sdkEKYC, { SDKeKYC, SDK_VERSION, SDK_NAME } from '../../EKYCModule';
4
+ import { setCustomExitComponent } from '../components/ExitSheetWrapper';
3
5
  import { NfcConfig, NfcError } from '../types/ekycNFCType';
4
6
  import { SDKEkycResultWithEvent, SDKEkycResultStringWithEvent, EKYCError, getEkycError, AppIDType } from '../types/ekycType';
5
7
  import { C06Config } from '../types/ekycC06Type';
@@ -321,16 +323,37 @@ export class FinosEKYCModule {
321
323
  }
322
324
 
323
325
  /**
324
- * Show a React Native component as a Bottom Sheet on top of the current activity.
325
- * Use this to show a custom RN UI over SDK screens.
326
- *
327
- * @param componentName The name registered with AppRegistry.registerComponent
328
- * @param props Initial props to pass to the component
326
+ * Set custom component to render inside the exit bottom sheet.
327
+ * Thay thế ExitConfirmSheet mặc định bằng component tùy chỉnh.
328
+ * Không cần AppRegistry — SDK tự lo phần registration.
329
+ *
330
+ * @param component React component để hiển thị, hoặc null để về default
331
+ *
332
+ * @example
333
+ * FinosEKYC.setExitSheetComponent(WarningBottomSheet);
329
334
  */
330
- public async showRNExitSheet(componentName: string, props: any = {}): Promise<boolean> {
335
+ public setExitSheetComponent(component: React.ComponentType<any> | null): void {
336
+ setCustomExitComponent(component);
337
+ }
338
+
339
+ /**
340
+ * Show the exit bottom sheet on top of the current SDK screen.
341
+ * Mặc định dùng 'SDKExitSheetWrapper' (đã register sẵn trong SDK).
342
+ *
343
+ * @param props Props truyền vào component (optional)
344
+ *
345
+ * @example
346
+ * // Dùng default ExitConfirmSheet
347
+ * FinosEKYC.showRNExitSheet();
348
+ *
349
+ * // Dùng custom component (set trước bằng setExitSheetComponent)
350
+ * FinosEKYC.setExitSheetComponent(WarningBottomSheet);
351
+ * FinosEKYC.showRNExitSheet();
352
+ */
353
+ public async showRNExitSheet(props: any = {}): Promise<boolean> {
331
354
  this.validateSDKReady();
332
355
  try {
333
- return await this.sdk.showRNExitSheet(componentName, props);
356
+ return await this.sdk.showRNExitSheet('SDKExitSheetWrapper', props);
334
357
  } catch (error) {
335
358
  console.error('❌ Failed to show RN exit sheet:', error);
336
359
  throw error;
@@ -978,7 +1001,11 @@ const isMethod = (prop: string | symbol): boolean => {
978
1001
  prop === 'checkC06' ||
979
1002
  prop === 'startOcr' ||
980
1003
  prop === 'startLiveness' ||
981
- prop === 'startFaceCompare';
1004
+ prop === 'startFaceCompare' ||
1005
+ prop === 'registerExitHandler' ||
1006
+ prop === 'resolveExit' ||
1007
+ prop === 'showRNExitSheet' ||
1008
+ prop === 'setExitSheetComponent';
982
1009
  };
983
1010
 
984
1011
  // Create a comprehensive stub object with all methods to prevent undefined errors
@@ -1012,6 +1039,7 @@ const createFinosEKYCStub = (): FinosEKYCModule => {
1012
1039
  'startEkycUI', 'sendOtp', 'verifyOtp', 'resendOtp', 'initializeESign', 'openSessionId',
1013
1040
  'registerDevice', 'listCerts', 'verifyCert', 'listSignRequest', 'confirmSign',
1014
1041
  'registerRemoteSigning', 'signPdf', 'sendConfirmationDocument',
1042
+ 'registerExitHandler', 'resolveExit', 'showRNExitSheet', 'setExitSheetComponent',
1015
1043
  'onResume', 'onPause', 'isSDKReady', 'getSDKInfo'
1016
1044
  ];
1017
1045