@hexar/biometric-identity-sdk-react-native 1.24.0 → 1.25.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hexar/biometric-identity-sdk-react-native",
3
- "version": "1.24.0",
3
+ "version": "1.25.1",
4
4
  "description": "React Native wrapper for Biometric Identity SDK",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -8,7 +8,8 @@
8
8
  "scripts": {
9
9
  "build": "tsc && cp -r src/assets dist/",
10
10
  "dev": "tsc --watch",
11
- "clean": "rm -rf dist"
11
+ "clean": "rm -rf dist",
12
+ "postinstall": "node scripts/postinstall.js"
12
13
  },
13
14
  "peerDependencies": {
14
15
  "@hexar/biometric-identity-sdk-core": ">=1.8.0",
@@ -0,0 +1,147 @@
1
+ diff --git a/node_modules/react-native-vision-camera/android/src/main/java/com/mrousavy/camera/core/utils/runOnUiThread.kt b/node_modules/react-native-vision-camera/android/src/main/java/com/mrousavy/camera/core/utils/runOnUiThread.kt
2
+ index e96e78e..c840643 100644
3
+ --- a/node_modules/react-native-vision-camera/android/src/main/java/com/mrousavy/camera/core/utils/runOnUiThread.kt
4
+ +++ b/node_modules/react-native-vision-camera/android/src/main/java/com/mrousavy/camera/core/utils/runOnUiThread.kt
5
+ @@ -3,6 +3,7 @@ package com.mrousavy.camera.core.utils
6
+ import com.facebook.react.bridge.UiThreadUtil
7
+ import kotlin.coroutines.cancellation.CancellationException
8
+ import kotlin.coroutines.resume
9
+ +import kotlin.coroutines.resumeWithException
10
+ import kotlinx.coroutines.suspendCancellableCoroutine
11
+
12
+ suspend inline fun <T> runOnUiThreadAndWait(crossinline function: () -> T): T {
13
+ @@ -13,9 +14,13 @@ suspend inline fun <T> runOnUiThreadAndWait(crossinline function: () -> T): T {
14
+
15
+ return suspendCancellableCoroutine { continuation ->
16
+ UiThreadUtil.runOnUiThread {
17
+ - if (continuation.isCancelled) throw CancellationException()
18
+ - val result = function()
19
+ - continuation.resume(result)
20
+ + try {
21
+ + if (continuation.isCancelled) throw CancellationException()
22
+ + val result = function()
23
+ + continuation.resume(result)
24
+ + } catch (e: Throwable) {
25
+ + continuation.resumeWithException(e)
26
+ + }
27
+ }
28
+ }
29
+ }
30
+ diff --git a/node_modules/react-native-vision-camera/android/src/main/java/com/mrousavy/camera/react/CameraViewModule.kt b/node_modules/react-native-vision-camera/android/src/main/java/com/mrousavy/camera/react/CameraViewModule.kt
31
+ index 059f677..54ead5a 100644
32
+ --- a/node_modules/react-native-vision-camera/android/src/main/java/com/mrousavy/camera/react/CameraViewModule.kt
33
+ +++ b/node_modules/react-native-vision-camera/android/src/main/java/com/mrousavy/camera/react/CameraViewModule.kt
34
+ @@ -91,7 +91,12 @@ class CameraViewModule(reactContext: ReactApplicationContext) : ReactContextBase
35
+ @ReactMethod
36
+ fun takePhoto(viewTag: Int, options: ReadableMap, promise: Promise) {
37
+ backgroundCoroutineScope.launch {
38
+ - val view = findCameraView(viewTag)
39
+ + val view = try {
40
+ + findCameraView(viewTag)
41
+ + } catch (e: Throwable) {
42
+ + promise.reject("camera/view-not-found", "Camera view has been removed", e)
43
+ + return@launch
44
+ + }
45
+ withPromise(promise) {
46
+ view.takePhoto(options)
47
+ }
48
+ @@ -101,7 +106,12 @@ class CameraViewModule(reactContext: ReactApplicationContext) : ReactContextBase
49
+ @ReactMethod
50
+ fun takeSnapshot(viewTag: Int, jsOptions: ReadableMap, promise: Promise) {
51
+ backgroundCoroutineScope.launch {
52
+ - val view = findCameraView(viewTag)
53
+ + val view = try {
54
+ + findCameraView(viewTag)
55
+ + } catch (e: Throwable) {
56
+ + promise.reject("camera/view-not-found", "Camera view has been removed", e)
57
+ + return@launch
58
+ + }
59
+ runOnUiThread {
60
+ try {
61
+ val options = TakeSnapshotOptions.fromJSValue(reactApplicationContext, jsOptions)
62
+ @@ -118,7 +128,13 @@ class CameraViewModule(reactContext: ReactApplicationContext) : ReactContextBase
63
+ @ReactMethod
64
+ fun startRecording(viewTag: Int, jsOptions: ReadableMap, onRecordCallback: Callback) {
65
+ backgroundCoroutineScope.launch {
66
+ - val view = findCameraView(viewTag)
67
+ + val view = try {
68
+ + findCameraView(viewTag)
69
+ + } catch (e: Throwable) {
70
+ + val map = makeErrorMap("camera/view-not-found", "Camera view has been removed", e)
71
+ + onRecordCallback(null, map)
72
+ + return@launch
73
+ + }
74
+ try {
75
+ val options = RecordVideoOptions.fromJSValue(reactApplicationContext, jsOptions)
76
+ view.startRecording(options, onRecordCallback)
77
+ @@ -136,8 +152,13 @@ class CameraViewModule(reactContext: ReactApplicationContext) : ReactContextBase
78
+ @ReactMethod
79
+ fun pauseRecording(viewTag: Int, promise: Promise) {
80
+ backgroundCoroutineScope.launch {
81
+ + val view = try {
82
+ + findCameraView(viewTag)
83
+ + } catch (e: Throwable) {
84
+ + promise.reject("camera/view-not-found", "Camera view has been removed", e)
85
+ + return@launch
86
+ + }
87
+ withPromise(promise) {
88
+ - val view = findCameraView(viewTag)
89
+ view.pauseRecording()
90
+ return@withPromise null
91
+ }
92
+ @@ -147,7 +168,12 @@ class CameraViewModule(reactContext: ReactApplicationContext) : ReactContextBase
93
+ @ReactMethod
94
+ fun resumeRecording(viewTag: Int, promise: Promise) {
95
+ backgroundCoroutineScope.launch {
96
+ - val view = findCameraView(viewTag)
97
+ + val view = try {
98
+ + findCameraView(viewTag)
99
+ + } catch (e: Throwable) {
100
+ + promise.reject("camera/view-not-found", "Camera view has been removed", e)
101
+ + return@launch
102
+ + }
103
+ withPromise(promise) {
104
+ view.resumeRecording()
105
+ return@withPromise null
106
+ @@ -158,7 +184,12 @@ class CameraViewModule(reactContext: ReactApplicationContext) : ReactContextBase
107
+ @ReactMethod
108
+ fun stopRecording(viewTag: Int, promise: Promise) {
109
+ backgroundCoroutineScope.launch {
110
+ - val view = findCameraView(viewTag)
111
+ + val view = try {
112
+ + findCameraView(viewTag)
113
+ + } catch (e: Throwable) {
114
+ + promise.reject("camera/view-not-found", "Camera view has been removed", e)
115
+ + return@launch
116
+ + }
117
+ withPromise(promise) {
118
+ view.stopRecording()
119
+ return@withPromise null
120
+ @@ -169,7 +200,12 @@ class CameraViewModule(reactContext: ReactApplicationContext) : ReactContextBase
121
+ @ReactMethod
122
+ fun cancelRecording(viewTag: Int, promise: Promise) {
123
+ backgroundCoroutineScope.launch {
124
+ - val view = findCameraView(viewTag)
125
+ + val view = try {
126
+ + findCameraView(viewTag)
127
+ + } catch (e: Throwable) {
128
+ + promise.reject("camera/view-not-found", "Camera view has been removed", e)
129
+ + return@launch
130
+ + }
131
+ withPromise(promise) {
132
+ view.cancelRecording()
133
+ return@withPromise null
134
+ @@ -180,7 +216,12 @@ class CameraViewModule(reactContext: ReactApplicationContext) : ReactContextBase
135
+ @ReactMethod
136
+ fun focus(viewTag: Int, point: ReadableMap, promise: Promise) {
137
+ backgroundCoroutineScope.launch {
138
+ - val view = findCameraView(viewTag)
139
+ + val view = try {
140
+ + findCameraView(viewTag)
141
+ + } catch (e: Throwable) {
142
+ + promise.reject("camera/view-not-found", "Camera view has been removed", e)
143
+ + return@launch
144
+ + }
145
+ withPromise(promise) {
146
+ view.focus(point)
147
+ return@withPromise null
@@ -0,0 +1,176 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Postinstall script for @hexar/biometric-identity-sdk-react-native
5
+ *
6
+ * Patches react-native-vision-camera to prevent IllegalViewOperationException
7
+ * crashes on Android when CameraViewModule.findCameraView is called after
8
+ * the React Native view has been unmounted.
9
+ *
10
+ * The patch makes two changes:
11
+ * 1. runOnUiThreadAndWait: wraps the UI-thread lambda in try/catch so
12
+ * exceptions route back to the coroutine via resumeWithException instead
13
+ * of crashing the main thread Looper.
14
+ * 2. CameraViewModule: wraps every findCameraView call in try/catch so
15
+ * the exception becomes a Promise rejection (catchable from JS) instead
16
+ * of an uncaught native crash.
17
+ */
18
+
19
+ const { execSync } = require('child_process');
20
+ const fs = require('fs');
21
+ const path = require('path');
22
+
23
+ // Walk up from this package to find the consumer's node_modules root.
24
+ // This script runs from:
25
+ // <consumer>/node_modules/@hexar/biometric-identity-sdk-react-native/scripts/
26
+ // We need:
27
+ // <consumer>/node_modules/react-native-vision-camera/
28
+ function findConsumerRoot() {
29
+ let dir = __dirname;
30
+ // Walk up until we find a node_modules parent that contains react-native-vision-camera
31
+ for (let i = 0; i < 10; i++) {
32
+ dir = path.dirname(dir);
33
+ const candidate = path.join(dir, 'react-native-vision-camera');
34
+ if (fs.existsSync(candidate)) {
35
+ return path.dirname(dir); // consumer project root
36
+ }
37
+ // Also check if we're at a node_modules boundary
38
+ if (path.basename(dir) === 'node_modules') {
39
+ return path.dirname(dir); // consumer project root
40
+ }
41
+ }
42
+ return null;
43
+ }
44
+
45
+ function applyPatch() {
46
+ const consumerRoot = findConsumerRoot();
47
+ if (!consumerRoot) {
48
+ // Not installed in a consumer project (e.g. local dev) — skip silently
49
+ return;
50
+ }
51
+
52
+ const visionCameraDir = path.join(consumerRoot, 'node_modules', 'react-native-vision-camera');
53
+ if (!fs.existsSync(visionCameraDir)) {
54
+ // vision-camera not installed — nothing to patch
55
+ return;
56
+ }
57
+
58
+ // Target files to patch
59
+ const runOnUiThreadFile = path.join(
60
+ visionCameraDir,
61
+ 'android/src/main/java/com/mrousavy/camera/core/utils/runOnUiThread.kt'
62
+ );
63
+ const cameraViewModuleFile = path.join(
64
+ visionCameraDir,
65
+ 'android/src/main/java/com/mrousavy/camera/react/CameraViewModule.kt'
66
+ );
67
+
68
+ if (!fs.existsSync(runOnUiThreadFile)) {
69
+ // Different version layout — skip
70
+ return;
71
+ }
72
+
73
+ // Check if already patched (idempotent)
74
+ const runOnUiThreadContent = fs.readFileSync(runOnUiThreadFile, 'utf8');
75
+ if (runOnUiThreadContent.includes('resumeWithException')) {
76
+ // Already patched
77
+ return;
78
+ }
79
+
80
+ console.log('[biometric-sdk] Patching react-native-vision-camera to fix Android IllegalViewOperationException crash...');
81
+
82
+ // Patch 1: runOnUiThread.kt — wrap UI thread lambda in try/catch
83
+ const patchedRunOnUiThread = runOnUiThreadContent.replace(
84
+ ` return suspendCancellableCoroutine { continuation ->
85
+ UiThreadUtil.runOnUiThread {
86
+ if (continuation.isCancelled) throw CancellationException()
87
+ val result = function()
88
+ continuation.resume(result)
89
+ }
90
+ }`,
91
+ ` return suspendCancellableCoroutine { continuation ->
92
+ UiThreadUtil.runOnUiThread {
93
+ try {
94
+ if (continuation.isCancelled) throw CancellationException()
95
+ val result = function()
96
+ continuation.resume(result)
97
+ } catch (e: Throwable) {
98
+ continuation.resumeWithException(e)
99
+ }
100
+ }
101
+ }`
102
+ );
103
+
104
+ if (patchedRunOnUiThread !== runOnUiThreadContent) {
105
+ // Add the missing import for resumeWithException
106
+ let finalContent = patchedRunOnUiThread;
107
+ if (!finalContent.includes('import kotlin.coroutines.resumeWithException')) {
108
+ finalContent = finalContent.replace(
109
+ 'import kotlin.coroutines.resume',
110
+ 'import kotlin.coroutines.resume\nimport kotlin.coroutines.resumeWithException'
111
+ );
112
+ }
113
+ fs.writeFileSync(runOnUiThreadFile, finalContent, 'utf8');
114
+ console.log('[biometric-sdk] ✓ Patched runOnUiThread.kt');
115
+ }
116
+
117
+ // Patch 2: CameraViewModule.kt — wrap findCameraView in try/catch for all methods
118
+ let moduleContent = fs.readFileSync(cameraViewModuleFile, 'utf8');
119
+ const original = moduleContent;
120
+
121
+ // Helper: replace "val view = findCameraView(viewTag)" with try/catch version
122
+ // that rejects the promise. Handle both promise and callback patterns.
123
+
124
+ // Pattern for promise-based methods (takePhoto, takeSnapshot, pauseRecording,
125
+ // resumeRecording, stopRecording, cancelRecording, focus)
126
+ // Match: "val view = findCameraView(viewTag)" inside a launch block that has a promise
127
+ const promiseMethods = [
128
+ 'takePhoto', 'takeSnapshot', 'resumeRecording',
129
+ 'stopRecording', 'cancelRecording', 'focus'
130
+ ];
131
+
132
+ for (const method of promiseMethods) {
133
+ const pattern = new RegExp(
134
+ `(fun ${method}\\(viewTag: Int,.*?promise: Promise\\).*?backgroundCoroutineScope\\.launch \\{\\n)(\\s*val view = findCameraView\\(viewTag\\))`,
135
+ 's'
136
+ );
137
+ moduleContent = moduleContent.replace(pattern, (match, before, viewLine) => {
138
+ const indent = viewLine.match(/^(\s*)/)[1];
139
+ return `${before}${indent}val view = try {\n${indent} findCameraView(viewTag)\n${indent}} catch (e: Throwable) {\n${indent} promise.reject("camera/view-not-found", "Camera view has been removed", e)\n${indent} return@launch\n${indent}}`;
140
+ });
141
+ }
142
+
143
+ // pauseRecording has findCameraView inside withPromise — different pattern
144
+ moduleContent = moduleContent.replace(
145
+ /fun pauseRecording\(viewTag: Int, promise: Promise\).*?backgroundCoroutineScope\.launch \{\n(\s*)withPromise\(promise\) \{\n(\s*)val view = findCameraView\(viewTag\)/s,
146
+ (match, indent1, indent2) => {
147
+ return match.replace(
148
+ `${indent1}withPromise(promise) {\n${indent2}val view = findCameraView(viewTag)`,
149
+ `${indent1}val view = try {\n${indent1} findCameraView(viewTag)\n${indent1}} catch (e: Throwable) {\n${indent1} promise.reject("camera/view-not-found", "Camera view has been removed", e)\n${indent1} return@launch\n${indent1}}\n${indent1}withPromise(promise) {`
150
+ );
151
+ }
152
+ );
153
+
154
+ // startRecording uses Callback, not Promise
155
+ moduleContent = moduleContent.replace(
156
+ /(fun startRecording\(viewTag: Int,.*?onRecordCallback: Callback\).*?backgroundCoroutineScope\.launch \{\n)(\s*val view = findCameraView\(viewTag\))/s,
157
+ (match, before, viewLine) => {
158
+ const indent = viewLine.match(/^(\s*)/)[1];
159
+ return `${before}${indent}val view = try {\n${indent} findCameraView(viewTag)\n${indent}} catch (e: Throwable) {\n${indent} val map = makeErrorMap("camera/view-not-found", "Camera view has been removed", e)\n${indent} onRecordCallback(null, map)\n${indent} return@launch\n${indent}}`;
160
+ }
161
+ );
162
+
163
+ if (moduleContent !== original) {
164
+ fs.writeFileSync(cameraViewModuleFile, moduleContent, 'utf8');
165
+ console.log('[biometric-sdk] ✓ Patched CameraViewModule.kt');
166
+ }
167
+
168
+ console.log('[biometric-sdk] Done.');
169
+ }
170
+
171
+ try {
172
+ applyPatch();
173
+ } catch (e) {
174
+ // Never fail the install — the patch is a safety improvement, not a requirement
175
+ console.warn('[biometric-sdk] Warning: could not apply vision-camera patch:', e.message);
176
+ }