@smileid/usesmileid_mlkit_face 12.0.1 → 12.1.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.
- package/CHANGELOG.md +11 -0
- package/README.md +30 -2
- package/android/build.gradle +21 -16
- package/android/src/main/java/com/usesmileid/mlkit/face/UseSmileIDMlkitFaceKameraAnalysisTick.kt +42 -0
- package/android/src/main/java/com/usesmileid/mlkit/face/UseSmileIDMlkitFaceModule.kt +6 -28
- package/android/src/main/java/com/usesmileid/mlkit/face/UseSmileIDMlkitFaceSessionHolder.kt +2 -3
- package/android/src/main/java/com/usesmileid/rn/analyzer/mlkitface/UseSmileIDAnalyzerExecutor.kt +28 -0
- package/app.plugin.js +3 -0
- package/dist/index.d.ts +4 -4
- package/dist/index.js +1 -38
- package/package.json +19 -11
- package/plugin/build/index.d.ts +11 -0
- package/plugin/build/index.js +1 -0
- package/plugin/build/lib/mlkit_manifest.d.ts +13 -0
- package/plugin/build/lib/mlkit_manifest.js +1 -0
- package/plugin/build/lib/root_project.d.ts +27 -0
- package/plugin/build/lib/root_project.js +4 -0
- package/android/src/main/java/com/usesmileid/mlkit/face/UseSmileIDMlkitFaceFrameProcessorPlugin.kt +0 -79
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/src/__tests__/use_smile_id_mlkit_face.test.ts +0 -101
- package/src/index.ts +0 -46
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,14 @@
|
|
|
1
|
+
Full release notes: [Smile ID docs](https://docs.usesmileid.com/developer-resources/sdks/mobile/release-notes/react-native-expo).
|
|
2
|
+
|
|
3
|
+
## 12.1.0
|
|
4
|
+
|
|
5
|
+
See the repository CHANGELOG for the 12.1.0 release notes.
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
## 12.0.2
|
|
9
|
+
|
|
10
|
+
See the repository CHANGELOG for the 12.0.2 release notes.
|
|
11
|
+
|
|
1
12
|
## 12.0.1
|
|
2
13
|
|
|
3
14
|
See the repository CHANGELOG for the 12.0.1 release notes.
|
package/README.md
CHANGED
|
@@ -20,8 +20,36 @@ No ML logic is reimplemented in this package — it's a pure passthrough.
|
|
|
20
20
|
pnpm add @smileid/usesmileid_mlkit_face
|
|
21
21
|
```
|
|
22
22
|
|
|
23
|
-
Expo's autolinking picks up `expo-module.config.json` at app build time
|
|
24
|
-
|
|
23
|
+
Expo's autolinking picks up `expo-module.config.json` at app build time, but the
|
|
24
|
+
build settings the AAR needs belong to the root Gradle project, so add the
|
|
25
|
+
package to `expo.plugins` in `app.json` and run `npx expo prebuild --clean`:
|
|
26
|
+
|
|
27
|
+
```json
|
|
28
|
+
{
|
|
29
|
+
"expo": {
|
|
30
|
+
"plugins": [
|
|
31
|
+
[
|
|
32
|
+
"@smileid/usesmileid_mlkit_face",
|
|
33
|
+
{ "kspVersion": "2.3.6", "additionalModelDependencies": ["barcode_ui"] }
|
|
34
|
+
]
|
|
35
|
+
]
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
The plugin pins the Kotlin the AAR was compiled with (2.3.20) and the
|
|
41
|
+
`androidx.core` version that resolves under Expo's Android Gradle Plugin.
|
|
42
|
+
It also claims the `com.google.mlkit.vision.DEPENDENCIES` meta-data on your
|
|
43
|
+
`<application>` with `tools:replace`: this AAR declares that key as `face`, so
|
|
44
|
+
without the override a second ML Kit consumer declaring its own value (for
|
|
45
|
+
instance `expo-camera`'s barcode scanner, which declares `barcode_ui`) fails the
|
|
46
|
+
Android manifest merger. `additionalModelDependencies` is optional — name the
|
|
47
|
+
hints your other consumers declare so their models are prefetched at install
|
|
48
|
+
time rather than downloaded on first use.
|
|
49
|
+
|
|
50
|
+
Expo's own KSP lookup carries no entry for that Kotlin and falls back to a KSP
|
|
51
|
+
the compiler cannot load, so pass `kspVersion` as shown — the samples use
|
|
52
|
+
`2.3.6`. `kotlinVersion` overrides the Kotlin pin.
|
|
25
53
|
|
|
26
54
|
## Usage
|
|
27
55
|
|
package/android/build.gradle
CHANGED
|
@@ -17,7 +17,7 @@ plugins {
|
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
group = 'com.usesmileid.mlkit.face'
|
|
20
|
-
version = '12.0
|
|
20
|
+
version = '12.1.0'
|
|
21
21
|
|
|
22
22
|
android {
|
|
23
23
|
namespace 'com.usesmileid.mlkit.face'
|
|
@@ -29,7 +29,7 @@ android {
|
|
|
29
29
|
// android/build.gradle and the rest of the @expo/* modules.
|
|
30
30
|
defaultConfig {
|
|
31
31
|
versionCode 1
|
|
32
|
-
versionName '12.0
|
|
32
|
+
versionName '12.1.0'
|
|
33
33
|
// R8 keep-rules to bundle into partner apps that depend on this
|
|
34
34
|
// module. Without these, R8 may rename the Expo module entry
|
|
35
35
|
// point and the JS-bridged member functions, breaking autolinking.
|
|
@@ -38,21 +38,26 @@ android {
|
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
dependencies {
|
|
41
|
+
// kamera native core: the analyzer executor takes analysis frames from
|
|
42
|
+
// kamera's KameraFrameHub so pixels never cross the JS bridge. Resolves from
|
|
43
|
+
// Maven Central; a local publishToMavenLocal build wins (see kamera.md).
|
|
44
|
+
implementation 'com.usesmileid:kamera:1.0.5'
|
|
45
|
+
|
|
41
46
|
// Existing Smile ID native AAR — the entire ML pipeline lives here.
|
|
42
47
|
// This Expo Module is a thin Kotlin shim that constructs the
|
|
43
48
|
// session and bridges its four methods to JS via Expo Modules.
|
|
44
|
-
implementation 'com.usesmileid:mlkit-face:12.0
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
49
|
+
implementation 'com.usesmileid:mlkit-face:12.1.0'
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// kamera resolves from Maven Central. mavenLocal stays injected — content-filtered to
|
|
55
|
+
// com.usesmileid — the same way kamera's own glue does it, so a locally published kamera
|
|
56
|
+
// build takes precedence during the kamera dev loop without host apps editing repos.
|
|
57
|
+
rootProject.allprojects {
|
|
58
|
+
repositories {
|
|
59
|
+
mavenLocal {
|
|
60
|
+
content { includeGroup 'com.usesmileid' }
|
|
61
|
+
}
|
|
62
|
+
}
|
|
58
63
|
}
|
package/android/src/main/java/com/usesmileid/mlkit/face/UseSmileIDMlkitFaceKameraAnalysisTick.kt
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
package com.usesmileid.mlkit.face
|
|
2
|
+
|
|
3
|
+
import com.usesmileid.bridge.model.YuvFrame as NativeYuvFrame
|
|
4
|
+
import com.usesmileid.kamera.integration.KameraFrameHub
|
|
5
|
+
import com.usesmileid.rn.analyzer.mlkitface.toNativeYuvFrame
|
|
6
|
+
import kotlin.coroutines.cancellation.CancellationException
|
|
7
|
+
import kotlinx.coroutines.runBlocking
|
|
8
|
+
import kotlinx.coroutines.withContext
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Scans the latest [KameraFrameHub] frame natively; no pending frame returns the `no_frame` wire error.
|
|
12
|
+
* The frame's rotationDegrees already uprights the buffer to the locked interface axis.
|
|
13
|
+
*/
|
|
14
|
+
internal fun analyzeKameraFrameTick(arguments: Map<String, Any?>?): Any {
|
|
15
|
+
val frame = KameraFrameHub.take() ?: return mapOf("error" to "no_frame")
|
|
16
|
+
val yuvFrame = try {
|
|
17
|
+
frame.toNativeYuvFrame()
|
|
18
|
+
} finally {
|
|
19
|
+
// Closing releases the frame to the pipeline; the converted copy owns its own bytes.
|
|
20
|
+
frame.close()
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Device-tilt context (0 = upright portrait) for the validators.
|
|
24
|
+
val jsRotation = (arguments?.get("rotation") as? Number)?.toInt() ?: 0
|
|
25
|
+
|
|
26
|
+
return runBlocking {
|
|
27
|
+
withContext(UseSmileIDMlkitFaceSessionHolder.sessionDispatcher) {
|
|
28
|
+
val activeSession = UseSmileIDMlkitFaceSessionHolder.session
|
|
29
|
+
?: return@withContext mapOf("error" to "session_not_configured")
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
val scanResult = activeSession.scan(yuvFrame, jsRotation)
|
|
33
|
+
val events = activeSession.drainCaptureReadyEvents()
|
|
34
|
+
UseSmileIDMlkitFaceModule.buildWireResult(scanResult, events)
|
|
35
|
+
} catch (cancellation: CancellationException) {
|
|
36
|
+
throw cancellation
|
|
37
|
+
} catch (e: Exception) {
|
|
38
|
+
mapOf("error" to ("scan_failed: " + (e.message ?: e.javaClass.simpleName)))
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -14,7 +14,6 @@ import com.usesmileid.bridge.session.UseSmileIDFaceSession
|
|
|
14
14
|
import com.usesmileid.bridge.session.UseSmileIDScanState
|
|
15
15
|
import com.usesmileid.bridge.session.UseSmileIDScanStateType
|
|
16
16
|
import com.usesmileid.bridge.storage.FrameStorage
|
|
17
|
-
import com.mrousavy.camera.frameprocessors.FrameProcessorPluginRegistry
|
|
18
17
|
import com.usesmileid.rn.analyzer.mlkitface.UseSmileIDAnalyzerExecutor
|
|
19
18
|
import expo.modules.kotlin.Promise
|
|
20
19
|
import expo.modules.kotlin.modules.Module
|
|
@@ -31,10 +30,7 @@ import expo.modules.kotlin.modules.ModuleDefinition
|
|
|
31
30
|
* subclass hooks (session construction, wire conversion) and forwards
|
|
32
31
|
* the async functions through it.
|
|
33
32
|
*
|
|
34
|
-
*
|
|
35
|
-
* which is registered with VisionCamera's frame-processor runtime in
|
|
36
|
-
* [OnCreate]. Partners call `VisionCameraProxy.initFrameProcessorPlugin`
|
|
37
|
-
* from their JS worklet instead of the old `AsyncFunction("analyze")`.
|
|
33
|
+
* Frames reach `analyzeKameraFrame` through kamera's native hub; pixels never cross the JS bridge.
|
|
38
34
|
*
|
|
39
35
|
* Lifecycle: Expo instantiates exactly one `Module` per app process;
|
|
40
36
|
* the executor is owned for the module's lifetime.
|
|
@@ -118,20 +114,17 @@ class UseSmileIDMlkitFaceModule : Module() {
|
|
|
118
114
|
?: throw IllegalStateException("Module not attached to a React context")
|
|
119
115
|
executor.onAttach(ctx)
|
|
120
116
|
|
|
121
|
-
// Guard against duplicate registration on dev-client hot reloads
|
|
122
|
-
// where OnCreate can fire more than once in the same process.
|
|
123
|
-
if (!pluginRegistered) {
|
|
124
|
-
pluginRegistered = true
|
|
125
|
-
FrameProcessorPluginRegistry.addFrameProcessorPlugin(
|
|
126
|
-
FRAME_PROCESSOR_PLUGIN_NAME,
|
|
127
|
-
) { _, _ -> UseSmileIDMlkitFaceFrameProcessorPlugin() }
|
|
128
|
-
}
|
|
129
117
|
}
|
|
130
118
|
|
|
131
119
|
AsyncFunction("configure") { config: Map<String, Any?> ->
|
|
132
120
|
executor.handleConfigure(config)
|
|
133
121
|
}
|
|
134
122
|
|
|
123
|
+
AsyncFunction("analyzeKameraFrame") { arguments: Map<String, Any?>? ->
|
|
124
|
+
// Blocks this module's async queue so only one scan is in flight at a time.
|
|
125
|
+
analyzeKameraFrameTick(arguments)
|
|
126
|
+
}
|
|
127
|
+
|
|
135
128
|
AsyncFunction("reset") {
|
|
136
129
|
executor.handleReset()
|
|
137
130
|
}
|
|
@@ -158,21 +151,6 @@ class UseSmileIDMlkitFaceModule : Module() {
|
|
|
158
151
|
/** Wire-format key partners pass via `FaceConfig.analyzerFactoryKey`. */
|
|
159
152
|
const val KEY: String = "mlkit-face"
|
|
160
153
|
|
|
161
|
-
/**
|
|
162
|
-
* Must match the JS worklet's `VisionCameraProxy.initFrameProcessorPlugin(...)`
|
|
163
|
-
* call exactly — drift is a runtime "plugin not found" error.
|
|
164
|
-
*/
|
|
165
|
-
const val FRAME_PROCESSOR_PLUGIN_NAME: String = "UseSmileIDMlkitFaceFrameProcessor"
|
|
166
|
-
|
|
167
|
-
/**
|
|
168
|
-
* [FrameProcessorPluginRegistry.addFrameProcessorPlugin] asserts uniqueness
|
|
169
|
-
* and crashes on duplicate registration; this guard prevents that on dev-client
|
|
170
|
-
* hot reloads where [OnCreate] fires more than once per process.
|
|
171
|
-
*/
|
|
172
|
-
@Volatile
|
|
173
|
-
private var pluginRegistered: Boolean = false
|
|
174
|
-
|
|
175
|
-
/** Wire-result builder shared with [UseSmileIDMlkitFaceFrameProcessorPlugin]. */
|
|
176
154
|
internal fun buildWireResult(
|
|
177
155
|
scanResult: UseSmileIDFaceScanResult,
|
|
178
156
|
events: List<NativeCaptureReadyEvent>,
|
|
@@ -10,9 +10,8 @@ import kotlinx.coroutines.asCoroutineDispatcher
|
|
|
10
10
|
*
|
|
11
11
|
* The session's state machine is not thread-safe; all operations —
|
|
12
12
|
* scan+drain as one atomic unit, flush, reset, close — are serialized
|
|
13
|
-
* through [sessionDispatcher] (a single-thread executor). The
|
|
14
|
-
*
|
|
15
|
-
* arrive while a scan is in flight rather than queuing them.
|
|
13
|
+
* through [sessionDispatcher] (a single-thread executor). The analysis tick blocks its
|
|
14
|
+
* caller via [runBlocking]; kamera's hub keeps only the latest frame, so frames never queue.
|
|
16
15
|
*/
|
|
17
16
|
internal object UseSmileIDMlkitFaceSessionHolder {
|
|
18
17
|
|
package/android/src/main/java/com/usesmileid/rn/analyzer/mlkitface/UseSmileIDAnalyzerExecutor.kt
CHANGED
|
@@ -12,6 +12,8 @@ import com.usesmileid.bridge.model.CaptureType
|
|
|
12
12
|
import com.usesmileid.bridge.model.YuvFrame as NativeYuvFrame
|
|
13
13
|
import com.usesmileid.bridge.storage.DocumentSide
|
|
14
14
|
import com.usesmileid.bridge.storage.FrameStorage
|
|
15
|
+
import com.usesmileid.kamera.frame.KameraFrame
|
|
16
|
+
import com.usesmileid.kamera.integration.KameraFrameHub
|
|
15
17
|
import java.io.File
|
|
16
18
|
import kotlinx.coroutines.CoroutineDispatcher
|
|
17
19
|
import kotlinx.coroutines.CoroutineScope
|
|
@@ -171,6 +173,7 @@ abstract class UseSmileIDAnalyzerExecutor<
|
|
|
171
173
|
|
|
172
174
|
// Shared implementation behind the Expo Module's async functions -----
|
|
173
175
|
|
|
176
|
+
|
|
174
177
|
/**
|
|
175
178
|
* Implementation behind `configure`. Closes any prior session,
|
|
176
179
|
* allocates a fresh on-disk session directory, and delegates session
|
|
@@ -281,3 +284,28 @@ abstract class UseSmileIDAnalyzerExecutor<
|
|
|
281
284
|
return dir
|
|
282
285
|
}
|
|
283
286
|
}
|
|
287
|
+
|
|
288
|
+
/** Copies the YUV planes so the frame can close at once; rotation stays metadata for the detector. */
|
|
289
|
+
internal fun KameraFrame.toNativeYuvFrame(): NativeYuvFrame {
|
|
290
|
+
val yPlane = planes[0]
|
|
291
|
+
val uPlane = planes[1]
|
|
292
|
+
val vPlane = planes[2]
|
|
293
|
+
fun copyBytes(buffer: java.nio.ByteBuffer): ByteArray {
|
|
294
|
+
val source = buffer.duplicate()
|
|
295
|
+
source.rewind()
|
|
296
|
+
return ByteArray(source.remaining()).also { source.get(it) }
|
|
297
|
+
}
|
|
298
|
+
return NativeYuvFrame(
|
|
299
|
+
planeY = copyBytes(yPlane.buffer),
|
|
300
|
+
planeU = copyBytes(uPlane.buffer),
|
|
301
|
+
planeV = copyBytes(vPlane.buffer),
|
|
302
|
+
yRowStride = yPlane.rowStride,
|
|
303
|
+
uRowStride = uPlane.rowStride,
|
|
304
|
+
vRowStride = vPlane.rowStride,
|
|
305
|
+
uPixelStride = uPlane.pixelStride,
|
|
306
|
+
vPixelStride = vPlane.pixelStride,
|
|
307
|
+
width = width,
|
|
308
|
+
height = height,
|
|
309
|
+
rotation = rotationDegrees,
|
|
310
|
+
)
|
|
311
|
+
}
|
package/app.plugin.js
ADDED
package/dist/index.d.ts
CHANGED
|
@@ -20,9 +20,10 @@ import type { FaceConfig, FaceScanResult } from "@smileid/usesmileid_bridge";
|
|
|
20
20
|
* `FaceAnalyzer` for the canonical contract.
|
|
21
21
|
*/
|
|
22
22
|
/**
|
|
23
|
-
* Must match the Kotlin Expo Module's `Name("UseSmileIDMlkitFace")`
|
|
24
|
-
*
|
|
25
|
-
*
|
|
23
|
+
* Must match the Kotlin Expo Module's `Name("UseSmileIDMlkitFace")` — drift is a runtime
|
|
24
|
+
* "module not found" error, not a compile error.
|
|
25
|
+
*
|
|
26
|
+
* `frameProcessorPluginName` is inert; kept only for API compatibility.
|
|
26
27
|
*/
|
|
27
28
|
export declare const UseSmileIDMlkitFaceIdentifiers: {
|
|
28
29
|
readonly nativeModuleName: "UseSmileIDMlkitFace";
|
|
@@ -30,4 +31,3 @@ export declare const UseSmileIDMlkitFaceIdentifiers: {
|
|
|
30
31
|
};
|
|
31
32
|
export declare const useSmileIDMlkitFace: import("@smileid/usesmileid_platform_interface").FaceAnalyzer;
|
|
32
33
|
export type { FaceConfig, FaceScanResult };
|
|
33
|
-
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
CHANGED
|
@@ -1,38 +1 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { useSmileIDAnalyzerExecutor } from "@smileid/usesmileid_platform_interface";
|
|
3
|
-
/**
|
|
4
|
-
* Google ML Kit face analyzer (Android-only).
|
|
5
|
-
*
|
|
6
|
-
* Thin shim around the existing native session
|
|
7
|
-
* `com.usesmileid.bridge.session.UseSmileIDFaceSession(FaceDetectorAnalyzer())`
|
|
8
|
-
* shipped in the `com.smileidentity:mlkit-face` AAR. The native side
|
|
9
|
-
* runs the entire pipeline (analyzer + validator + transitioner +
|
|
10
|
-
* state machine); this TS layer only routes calls and validates wire
|
|
11
|
-
* payloads at the channel boundary.
|
|
12
|
-
*
|
|
13
|
-
* The canonical wiring lives in [useSmileIDAnalyzerExecutor] in
|
|
14
|
-
* `usesmileid_platform_interface`. This file only supplies the
|
|
15
|
-
* provider-specific identity so all six provider modules share one set
|
|
16
|
-
* of correctness properties by construction.
|
|
17
|
-
*
|
|
18
|
-
* Lifecycle: `configure()` → per-frame analysis via the native frame
|
|
19
|
-
* processor plugin → `reset()`? → `close()`. Calling
|
|
20
|
-
* `close()` twice is idempotent. See `usesmileid_platform_interface`
|
|
21
|
-
* `FaceAnalyzer` for the canonical contract.
|
|
22
|
-
*/
|
|
23
|
-
/**
|
|
24
|
-
* Must match the Kotlin Expo Module's `Name("UseSmileIDMlkitFace")` and its
|
|
25
|
-
* VisionCamera frame-processor-plugin registration — drift is a runtime
|
|
26
|
-
* "module/plugin not found" error, not a compile error.
|
|
27
|
-
*/
|
|
28
|
-
export const UseSmileIDMlkitFaceIdentifiers = {
|
|
29
|
-
nativeModuleName: "UseSmileIDMlkitFace",
|
|
30
|
-
frameProcessorPluginName: "UseSmileIDMlkitFaceFrameProcessor",
|
|
31
|
-
};
|
|
32
|
-
export const useSmileIDMlkitFace = useSmileIDAnalyzerExecutor({
|
|
33
|
-
nativeModuleName: UseSmileIDMlkitFaceIdentifiers.nativeModuleName,
|
|
34
|
-
frameProcessorPluginName: UseSmileIDMlkitFaceIdentifiers.frameProcessorPluginName,
|
|
35
|
-
kind: "face",
|
|
36
|
-
key: AnalyzerFactoryKey.MLKitFace,
|
|
37
|
-
});
|
|
38
|
-
//# sourceMappingURL=index.js.map
|
|
1
|
+
"use strict";import{AnalyzerFactoryKey as e}from"@smileid/usesmileid_bridge";import{useSmileIDAnalyzerExecutor as r}from"@smileid/usesmileid_platform_interface";export const UseSmileIDMlkitFaceIdentifiers={nativeModuleName:"UseSmileIDMlkitFace",frameProcessorPluginName:"UseSmileIDMlkitFaceFrameProcessor"},useSmileIDMlkitFace=r({nativeModuleName:UseSmileIDMlkitFaceIdentifiers.nativeModuleName,frameProcessorPluginName:UseSmileIDMlkitFaceIdentifiers.frameProcessorPluginName,kind:"face",key:e.MLKitFace});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@smileid/usesmileid_mlkit_face",
|
|
3
|
-
"version": "12.0
|
|
3
|
+
"version": "12.1.0",
|
|
4
4
|
"description": "Android MLKit face detection implementation for the UseSmileID React Native (Expo) SDK.",
|
|
5
5
|
"homepage": "https://usesmileid.com",
|
|
6
6
|
"repository": {
|
|
@@ -31,20 +31,24 @@
|
|
|
31
31
|
"types": "./dist/index.d.ts",
|
|
32
32
|
"import": "./dist/index.js",
|
|
33
33
|
"require": "./dist/index.js"
|
|
34
|
-
}
|
|
34
|
+
},
|
|
35
|
+
"./app.plugin.js": "./app.plugin.js"
|
|
35
36
|
},
|
|
36
37
|
"files": [
|
|
37
38
|
"dist",
|
|
38
|
-
"
|
|
39
|
-
"android",
|
|
39
|
+
"android/build.gradle",
|
|
40
|
+
"android/consumer-rules.pro",
|
|
41
|
+
"android/src/main",
|
|
42
|
+
"plugin/build",
|
|
43
|
+
"app.plugin.js",
|
|
40
44
|
"expo-module.config.json",
|
|
41
45
|
"README.md",
|
|
42
46
|
"LICENSE",
|
|
43
47
|
"CHANGELOG.md"
|
|
44
48
|
],
|
|
45
49
|
"dependencies": {
|
|
46
|
-
"@smileid/
|
|
47
|
-
"@smileid/
|
|
50
|
+
"@smileid/usesmileid_platform_interface": "12.1.0",
|
|
51
|
+
"@smileid/usesmileid_bridge": "12.1.0"
|
|
48
52
|
},
|
|
49
53
|
"peerDependencies": {
|
|
50
54
|
"expo": ">=56.0.0",
|
|
@@ -52,18 +56,22 @@
|
|
|
52
56
|
"react-native": ">=0.83.0"
|
|
53
57
|
},
|
|
54
58
|
"devDependencies": {
|
|
55
|
-
"
|
|
56
|
-
"
|
|
59
|
+
"@types/node": "^26.4.1",
|
|
60
|
+
"expo": "~57.0.20",
|
|
61
|
+
"expo-modules-core": "^57.0.16",
|
|
62
|
+
"react-native": "^0.86.3",
|
|
57
63
|
"typescript": "^6.0.3"
|
|
58
64
|
},
|
|
59
65
|
"publishConfig": {
|
|
60
66
|
"access": "public"
|
|
61
67
|
},
|
|
62
68
|
"scripts": {
|
|
63
|
-
"build": "
|
|
64
|
-
"
|
|
69
|
+
"build": "pnpm run build:lib && pnpm run build:plugin && node ../Scripts/copy-assets.mjs",
|
|
70
|
+
"build:lib": "tsc -p tsconfig.json",
|
|
71
|
+
"build:plugin": "tsc -p plugin/tsconfig.json",
|
|
72
|
+
"typecheck": "tsc -b && tsc -p plugin/tsconfig.json --noEmit",
|
|
65
73
|
"test": "vitest run",
|
|
66
74
|
"test:watch": "vitest",
|
|
67
|
-
"clean": "rm -rf dist .turbo coverage"
|
|
75
|
+
"clean": "rm -rf dist plugin/build .turbo coverage tsconfig.tsbuildinfo"
|
|
68
76
|
}
|
|
69
77
|
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type ConfigPlugin } from "expo/config-plugins";
|
|
2
|
+
import { type MlkitManifestMergeOptions } from "./lib/mlkit_manifest";
|
|
3
|
+
export interface MlkitFaceOptions extends MlkitManifestMergeOptions {
|
|
4
|
+
/** Overrides the default Kotlin pin (must read the AARs' metadata). */
|
|
5
|
+
kotlinVersion?: string;
|
|
6
|
+
/** Opt-in: only when Expo's lookup table lacks the pinned Kotlin. */
|
|
7
|
+
kspVersion?: string;
|
|
8
|
+
}
|
|
9
|
+
declare const withMlkitFace: ConfigPlugin<MlkitFaceOptions | void>;
|
|
10
|
+
export default withMlkitFace;
|
|
11
|
+
export type { MlkitManifestMergeOptions };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";Object.defineProperty(exports,"__esModule",{value:!0});const root_project_1=require("./lib/root_project"),mlkit_manifest_1=require("./lib/mlkit_manifest"),DEFAULT_KOTLIN_VERSION="2.3.20",FORCED_DEPENDENCIES=["androidx.core:core:1.18.0","androidx.core:core-ktx:1.18.0"],withMlkitFace=(t,o)=>{const{kotlinVersion:i,kspVersion:e,...r}=o??{};return(0,mlkit_manifest_1.withMlkitManifestMerge)((0,root_project_1.withAndroidRootProjectPins)(t,{kotlinVersion:i??DEFAULT_KOTLIN_VERSION,...e!==void 0?{kspVersion:e}:{},forcedDependencies:FORCED_DEPENDENCIES}),r)};exports.default=withMlkitFace;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { AndroidConfig, type ConfigPlugin } from "expo/config-plugins";
|
|
2
|
+
type AndroidManifest = AndroidConfig.Manifest.AndroidManifest;
|
|
3
|
+
export interface MlkitManifestMergeOptions {
|
|
4
|
+
/** Hints your app's other ML Kit consumers declare, so their models prefetch at install time rather than on first use. */
|
|
5
|
+
readonly additionalModelDependencies?: readonly string[];
|
|
6
|
+
}
|
|
7
|
+
/** Provider hint first, then the caller's, deduplicated and order-stable. */
|
|
8
|
+
export declare function mergedModelDependencies(additional: readonly string[]): string;
|
|
9
|
+
/** Declares the merged `com.google.mlkit.vision.DEPENDENCIES` on the host `<application>` with `tools:replace`. */
|
|
10
|
+
export declare const withMlkitManifestMerge: ConfigPlugin<MlkitManifestMergeOptions | void>;
|
|
11
|
+
/** Pure form of the edit. Idempotent; a re-run replaces a stale value. */
|
|
12
|
+
export declare function applyMlkitManifestMerge(manifest: AndroidManifest, value: string): AndroidManifest;
|
|
13
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.withMlkitManifestMerge=void 0,exports.mergedModelDependencies=mergedModelDependencies,exports.applyMlkitManifestMerge=applyMlkitManifestMerge;const config_plugins_1=require("expo/config-plugins"),META_DATA_NAME="com.google.mlkit.vision.DEPENDENCIES",TOOLS_NAMESPACE="http://schemas.android.com/tools",PROVIDER_MODEL_DEPENDENCY="face",MODEL_DEPENDENCY=/^[a-z0-9_]+$/;function mergedModelDependencies(e){for(const t of e)if(!MODEL_DEPENDENCY.test(t))throw new Error(`[smileid-android] '${t}' is not a valid ML Kit model dependency. Expected a lowercase hint such as 'barcode_ui', as printed in the manifest-merger error.`);return[...new Set([PROVIDER_MODEL_DEPENDENCY,...e])].join(",")}const withMlkitManifestMerge=(e,t)=>(0,config_plugins_1.withAndroidManifest)(e,n=>(n.modResults=applyMlkitManifestMerge(n.modResults,mergedModelDependencies(t?.additionalModelDependencies??[])),n));exports.withMlkitManifestMerge=withMlkitManifestMerge;function applyMlkitManifestMerge(e,t){const n=config_plugins_1.AndroidConfig.Manifest.getMainApplicationOrThrow(e);n["meta-data"]??=[];const i=n["meta-data"],o=i.find(s=>s?.$?.["android:name"]===META_DATA_NAME);return o?(o.$["android:value"]=t,o.$["tools:replace"]="android:value"):i.push({$:{"android:name":META_DATA_NAME,"android:value":t,"tools:replace":"android:value"}}),e.manifest.$??={},e.manifest.$["xmlns:tools"]??=TOOLS_NAMESPACE,e}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { type AndroidConfig, type ConfigPlugin } from "expo/config-plugins";
|
|
2
|
+
type PropertiesItem = AndroidConfig.Properties.PropertiesItem;
|
|
3
|
+
export interface AndroidRootProjectPins {
|
|
4
|
+
/**
|
|
5
|
+
* Opt-in: only for hosts whose Kotlin predates the AARs' compiler. Current
|
|
6
|
+
* Expo defaults read the metadata, and forcing a Kotlin that Expo ships no
|
|
7
|
+
* compiler-plugin artifacts for breaks expo-modules-core.
|
|
8
|
+
*/
|
|
9
|
+
kotlinVersion?: string;
|
|
10
|
+
/** Only alongside `kotlinVersion` — Expo's own lookup table may carry no entry for it. */
|
|
11
|
+
kspVersion?: string;
|
|
12
|
+
/** Opt-in: only for a host that must compile above React Native's default. The AARs declare their own consumer floor. */
|
|
13
|
+
compileSdkVersion?: number;
|
|
14
|
+
/** `group:name:version` coordinates forced everywhere, for deps whose newest release the host toolchain cannot consume. */
|
|
15
|
+
forcedDependencies: readonly string[];
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* AGP matches this against the resolved platform, which may or may not carry a minor
|
|
19
|
+
* (`android-37` vs `android-37.0`), so both forms are listed.
|
|
20
|
+
*/
|
|
21
|
+
export declare function suppressUnsupportedCompileSdkValue(compileSdkVersion: number): string;
|
|
22
|
+
export declare function suppressAdvisoryProperties(items: PropertiesItem[], compileSdkVersion: number | undefined): PropertiesItem[];
|
|
23
|
+
/** Declares the pins ahead of `apply plugin: "expo-root-project"`. */
|
|
24
|
+
export declare const withAndroidRootProjectPins: ConfigPlugin<AndroidRootProjectPins>;
|
|
25
|
+
/** Pure form of the edit. Idempotent; a re-run replaces stale values rather than appending. */
|
|
26
|
+
export declare function applyAndroidRootProjectPins(contents: string, pins: AndroidRootProjectPins): string;
|
|
27
|
+
export {};
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
"use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.withAndroidRootProjectPins=void 0,exports.suppressUnsupportedCompileSdkValue=suppressUnsupportedCompileSdkValue,exports.suppressAdvisoryProperties=suppressAdvisoryProperties,exports.applyAndroidRootProjectPins=applyAndroidRootProjectPins;const config_plugins_1=require("expo/config-plugins"),SUPPRESS_KEY="android.suppressUnsupportedCompileSdk",BEGIN_MARKER="// === BEGIN smileid root-project pins ===",END_MARKER="// === END smileid root-project pins ===",APPLY_ROOT_PROJECT=/^apply\s+plugin:\s*["']expo-root-project["']/m,KGP_CLASSPATH=/classpath\s*(?:\(\s*)?['"]org\.jetbrains\.kotlin:kotlin-gradle-plugin(?::[^'"]*)?['"](?:\s*\))?/;function suppressUnsupportedCompileSdkValue(e){return`${e},${e}.0`}function suppressAdvisoryProperties(e,o){if(o===void 0)return e;const r={type:"property",key:SUPPRESS_KEY,value:suppressUnsupportedCompileSdkValue(o)},n=e.findIndex(t=>t.type==="property"&&t.key===SUPPRESS_KEY);return n===-1?[...e,r]:e.map((t,i)=>i===n?r:t)}const withSuppressedCompileSdkAdvisory=(e,o)=>(0,config_plugins_1.withGradleProperties)(e,r=>(r.modResults=suppressAdvisoryProperties(r.modResults,o),r)),withAndroidRootProjectPins=(e,o)=>withSuppressedCompileSdkAdvisory((0,config_plugins_1.withProjectBuildGradle)(e,r=>{if(r.modResults.language!=="groovy")throw new Error(`[smileid-android] Expected android/build.gradle to be Groovy DSL; refusing to edit a ${r.modResults.language} build script.`);return r.modResults.contents=applyAndroidRootProjectPins(r.modResults.contents,o),r}),o.compileSdkVersion);exports.withAndroidRootProjectPins=withAndroidRootProjectPins;function applyAndroidRootProjectPins(e,o){if(o.kspVersion!==void 0&&o.kotlinVersion===void 0)throw new Error("[smileid-android] `kspVersion` only makes sense alongside `kotlinVersion`.");validateGroovySafe(o);const r=o.kotlinVersion===void 0?e:pinKotlinGradlePlugin(e,o.kotlinVersion);return upsertPinBlock(r,renderPinBlock(o))}function validateGroovySafe(e){const o=[e.kotlinVersion,e.kspVersion,...e.forcedDependencies].filter(r=>r!==void 0);for(const r of o)if(/['\\\n\r\t$]/.test(r))throw new Error(`[smileid-android] Refusing to edit android/build.gradle: '${r}' contains an unsafe character.`)}function pinKotlinGradlePlugin(e,o){const r=`classpath('org.jetbrains.kotlin:kotlin-gradle-plugin:${o}')`;if(!KGP_CLASSPATH.test(e))throw new Error(`[smileid-android] Could not locate the kotlin-gradle-plugin classpath line in android/build.gradle. Refusing to silently no-op \u2014 the Smile ID analyzer modules do not compile unless the Kotlin compiler is pinned to ${o}.`);return e.replace(KGP_CLASSPATH,r)}function renderPinBlock(e){const o=[];e.kotlinVersion!==void 0&&o.push(["kotlinVersion",`'${e.kotlinVersion}'`]),e.kspVersion!==void 0&&o.push(["kspVersion",`'${e.kspVersion}'`]),e.compileSdkVersion!==void 0&&o.push(["compileSdkVersion",String(e.compileSdkVersion)]);const r=o.map(([n,t])=>`ext.${n} = ${t}`);return[BEGIN_MARKER,...r,...renderForcedDependencies(e.forcedDependencies),END_MARKER].join(`
|
|
2
|
+
`)}function renderForcedDependencies(e){return e.length===0?[]:["","// Forced to the newest release that resolves under the AGP Expo generates.","allprojects {"," configurations.all {"," resolutionStrategy {",...e.map(o=>` force '${o}'`)," }"," }","}"]}function upsertPinBlock(e,o){const r=new RegExp(`${escapeRegExp(BEGIN_MARKER)}[\\s\\S]*?${escapeRegExp(END_MARKER)}`,"m");if(r.test(e))return e.replace(r,o);if(!APPLY_ROOT_PROJECT.test(e))throw new Error('[smileid-android] Could not locate `apply plugin: "expo-root-project"` in android/build.gradle. Refusing to silently no-op \u2014 these pins only take effect when declared before that plugin applies.');return e.replace(APPLY_ROOT_PROJECT,n=>`${o}
|
|
3
|
+
|
|
4
|
+
${n}`)}function escapeRegExp(e){return e.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}
|
package/android/src/main/java/com/usesmileid/mlkit/face/UseSmileIDMlkitFaceFrameProcessorPlugin.kt
DELETED
|
@@ -1,79 +0,0 @@
|
|
|
1
|
-
package com.usesmileid.mlkit.face
|
|
2
|
-
|
|
3
|
-
import androidx.camera.core.ExperimentalGetImage
|
|
4
|
-
import com.mrousavy.camera.core.FrameInvalidError
|
|
5
|
-
import com.mrousavy.camera.frameprocessors.Frame
|
|
6
|
-
import com.mrousavy.camera.frameprocessors.FrameProcessorPlugin
|
|
7
|
-
import com.usesmileid.bridge.model.YuvFrame as NativeYuvFrame
|
|
8
|
-
import kotlin.coroutines.cancellation.CancellationException
|
|
9
|
-
import kotlinx.coroutines.runBlocking
|
|
10
|
-
import kotlinx.coroutines.withContext
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* VisionCamera Frame Processor Plugin for the ML Kit face backend.
|
|
14
|
-
* Registered under [UseSmileIDMlkitFaceModule.FRAME_PROCESSOR_PLUGIN_NAME].
|
|
15
|
-
* Reads raw [ImageProxy] planes directly (no JPEG encoding) and runs the
|
|
16
|
-
* native session pipeline. Blocks the FP thread via [runBlocking] —
|
|
17
|
-
* VisionCamera drops frames that arrive while a scan is in flight.
|
|
18
|
-
*/
|
|
19
|
-
@OptIn(ExperimentalGetImage::class)
|
|
20
|
-
internal class UseSmileIDMlkitFaceFrameProcessorPlugin : FrameProcessorPlugin() {
|
|
21
|
-
|
|
22
|
-
override fun callback(frame: Frame, arguments: Map<String, Any>?): Any? {
|
|
23
|
-
val imageProxy = try {
|
|
24
|
-
frame.imageProxy
|
|
25
|
-
} catch (e: FrameInvalidError) {
|
|
26
|
-
return mapOf("error" to "frame_conversion_failed")
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
val image = imageProxy.image
|
|
30
|
-
?: return mapOf("error" to "frame_conversion_failed")
|
|
31
|
-
|
|
32
|
-
// Pixel-orientation for ML Kit — always from ImageProxy metadata, never from JS.
|
|
33
|
-
val sensorRotation = imageProxy.imageInfo.rotationDegrees
|
|
34
|
-
|
|
35
|
-
// Device-tilt context (0 = upright portrait) for head-pose math.
|
|
36
|
-
// Must not fall back to sensorRotation — sensor angle != device tilt.
|
|
37
|
-
val jsRotation = (arguments?.get("rotation") as? Number)?.toInt() ?: 0
|
|
38
|
-
|
|
39
|
-
val planes = image.planes
|
|
40
|
-
if (planes.size < 3) return mapOf("error" to "frame_conversion_failed")
|
|
41
|
-
|
|
42
|
-
fun extractPlane(index: Int): ByteArray {
|
|
43
|
-
val buffer = planes[index].buffer
|
|
44
|
-
buffer.rewind()
|
|
45
|
-
return ByteArray(buffer.remaining()).also { buffer.get(it) }
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
val yuvFrame = NativeYuvFrame(
|
|
49
|
-
planeY = extractPlane(0),
|
|
50
|
-
planeU = extractPlane(1),
|
|
51
|
-
planeV = extractPlane(2),
|
|
52
|
-
yRowStride = planes[0].rowStride,
|
|
53
|
-
uRowStride = planes[1].rowStride,
|
|
54
|
-
vRowStride = planes[2].rowStride,
|
|
55
|
-
uPixelStride = planes[1].pixelStride,
|
|
56
|
-
vPixelStride = planes[2].pixelStride,
|
|
57
|
-
width = image.width,
|
|
58
|
-
height = image.height,
|
|
59
|
-
rotation = sensorRotation,
|
|
60
|
-
)
|
|
61
|
-
|
|
62
|
-
return runBlocking {
|
|
63
|
-
withContext(UseSmileIDMlkitFaceSessionHolder.sessionDispatcher) {
|
|
64
|
-
val activeSession = UseSmileIDMlkitFaceSessionHolder.session
|
|
65
|
-
?: return@withContext mapOf("error" to "session_not_configured")
|
|
66
|
-
|
|
67
|
-
try {
|
|
68
|
-
val scanResult = activeSession.scan(yuvFrame, jsRotation)
|
|
69
|
-
val events = activeSession.drainCaptureReadyEvents()
|
|
70
|
-
UseSmileIDMlkitFaceModule.buildWireResult(scanResult, events)
|
|
71
|
-
} catch (cancellation: CancellationException) {
|
|
72
|
-
throw cancellation
|
|
73
|
-
} catch (e: Exception) {
|
|
74
|
-
mapOf("error" to ("scan_failed: " + (e.message ?: e.javaClass.simpleName)))
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
}
|
package/dist/index.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,4BAA4B,CAAC;AAG7E;;;;;;;;;;;;;;;;;;;GAmBG;AACH;;;;GAIG;AACH,eAAO,MAAM,8BAA8B;;;CAGjC,CAAC;AAEX,eAAO,MAAM,mBAAmB,+DAM9B,CAAC;AAKH,YAAY,EAAE,UAAU,EAAE,cAAc,EAAE,CAAC"}
|
package/dist/index.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,MAAM,4BAA4B,CAAC;AAEhE,OAAO,EAAE,0BAA0B,EAAE,MAAM,wCAAwC,CAAC;AAEpF;;;;;;;;;;;;;;;;;;;GAmBG;AACH;;;;GAIG;AACH,MAAM,CAAC,MAAM,8BAA8B,GAAG;IAC5C,gBAAgB,EAAE,qBAAqB;IACvC,wBAAwB,EAAE,mCAAmC;CACrD,CAAC;AAEX,MAAM,CAAC,MAAM,mBAAmB,GAAG,0BAA0B,CAAC;IAC5D,gBAAgB,EAAE,8BAA8B,CAAC,gBAAgB;IACjE,wBAAwB,EACtB,8BAA8B,CAAC,wBAAwB;IACzD,IAAI,EAAE,MAAM;IACZ,GAAG,EAAE,kBAAkB,CAAC,SAAS;CAClC,CAAC,CAAC"}
|
|
@@ -1,101 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it, vi, beforeEach, type Mock } from "vitest";
|
|
2
|
-
|
|
3
|
-
// Hoist the mock so `requireNativeModule` resolves to a stub before
|
|
4
|
-
// the index module's top-level `requireNativeModule(...)` call runs.
|
|
5
|
-
// Without `vi.hoisted(...)`, Vitest evaluates the import statement
|
|
6
|
-
// before the mock is in scope and the wrapper grabs the real native
|
|
7
|
-
// module — which doesn't exist in a Node test environment.
|
|
8
|
-
const native = vi.hoisted(() => ({
|
|
9
|
-
configure: vi.fn(),
|
|
10
|
-
reset: vi.fn(),
|
|
11
|
-
close: vi.fn(),
|
|
12
|
-
}));
|
|
13
|
-
|
|
14
|
-
vi.mock("expo-modules-core", () => ({
|
|
15
|
-
requireNativeModule: vi.fn(() => native),
|
|
16
|
-
}));
|
|
17
|
-
|
|
18
|
-
// Imports happen *after* the mock is registered.
|
|
19
|
-
import { requireNativeModule } from "expo-modules-core";
|
|
20
|
-
import { useSmileIDMlkitFace, UseSmileIDMlkitFaceIdentifiers } from "../index";
|
|
21
|
-
|
|
22
|
-
describe("useSmileIDMlkitFace — TS wrapper", () => {
|
|
23
|
-
beforeEach(() => {
|
|
24
|
-
Object.values(native).forEach((m) => (m as Mock).mockReset());
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
it("looks up the native module under the contract-stable name", () => {
|
|
28
|
-
// The Kotlin Expo Module declares `Name("UseSmileIDMlkitFace")`.
|
|
29
|
-
// If the JS wrapper passes a different string, partner apps fail
|
|
30
|
-
// at runtime with "Cannot find native module 'X'".
|
|
31
|
-
expect(requireNativeModule).toHaveBeenCalledWith("UseSmileIDMlkitFace");
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
it("exposes the lifecycle surface — no per-frame analyze method", () => {
|
|
35
|
-
expect(typeof useSmileIDMlkitFace.configure).toBe("function");
|
|
36
|
-
expect(typeof useSmileIDMlkitFace.reset).toBe("function");
|
|
37
|
-
expect(typeof useSmileIDMlkitFace.close).toBe("function");
|
|
38
|
-
// Per-frame analysis lives in the native VisionCamera frame
|
|
39
|
-
// processor plugin — raw pixels never cross the JS bridge.
|
|
40
|
-
expect("analyze" in useSmileIDMlkitFace).toBe(false);
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
it("exposes the kind discriminator and frame processor plugin name", () => {
|
|
44
|
-
expect(useSmileIDMlkitFace.kind).toBe("face");
|
|
45
|
-
expect(useSmileIDMlkitFace.frameProcessorPluginName).toBe(
|
|
46
|
-
UseSmileIDMlkitFaceIdentifiers.frameProcessorPluginName,
|
|
47
|
-
);
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
describe("configure", () => {
|
|
51
|
-
it("passes the validated config through to native", async () => {
|
|
52
|
-
native.configure.mockResolvedValueOnce(undefined);
|
|
53
|
-
await useSmileIDMlkitFace.configure({ selfieCaptureTimeout: 60_000 });
|
|
54
|
-
expect(native.configure).toHaveBeenCalledTimes(1);
|
|
55
|
-
expect(native.configure).toHaveBeenCalledWith({
|
|
56
|
-
selfieCaptureTimeout: 60_000,
|
|
57
|
-
});
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
it("rejects malformed config at the JS boundary before crossing the bridge", async () => {
|
|
61
|
-
// Concrete scenario: a partner builds a config from float math
|
|
62
|
-
// and passes `selfieCaptureTimeout: 60_000.5`. Without the
|
|
63
|
-
// boundary parse, the value would marshal to a Kotlin `Int`
|
|
64
|
-
// with silent truncation. With it, the JS layer throws here
|
|
65
|
-
// and `native.configure` is never called.
|
|
66
|
-
await expect(
|
|
67
|
-
useSmileIDMlkitFace.configure({
|
|
68
|
-
// @ts-expect-error — deliberately wrong: int field with float value
|
|
69
|
-
selfieCaptureTimeout: 60_000.5,
|
|
70
|
-
}),
|
|
71
|
-
).rejects.toThrow();
|
|
72
|
-
expect(native.configure).not.toHaveBeenCalled();
|
|
73
|
-
});
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
describe("reset", () => {
|
|
77
|
-
it("routes to native reset", async () => {
|
|
78
|
-
native.reset.mockResolvedValueOnce(undefined);
|
|
79
|
-
await useSmileIDMlkitFace.reset();
|
|
80
|
-
expect(native.reset).toHaveBeenCalledTimes(1);
|
|
81
|
-
});
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
describe("close", () => {
|
|
85
|
-
it("routes to native close", async () => {
|
|
86
|
-
native.close.mockResolvedValueOnce(undefined);
|
|
87
|
-
await useSmileIDMlkitFace.close();
|
|
88
|
-
expect(native.close).toHaveBeenCalledTimes(1);
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
it("is idempotent — partner can call it twice safely", async () => {
|
|
92
|
-
// Idempotency contract from FaceAnalyzer doc-comment. The native
|
|
93
|
-
// side handles the second close as a no-op; the wrapper just
|
|
94
|
-
// needs to not throw on re-entry.
|
|
95
|
-
native.close.mockResolvedValue(undefined);
|
|
96
|
-
await useSmileIDMlkitFace.close();
|
|
97
|
-
await useSmileIDMlkitFace.close();
|
|
98
|
-
expect(native.close).toHaveBeenCalledTimes(2);
|
|
99
|
-
});
|
|
100
|
-
});
|
|
101
|
-
});
|
package/src/index.ts
DELETED
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
import { AnalyzerFactoryKey } from "@smileid/usesmileid_bridge";
|
|
2
|
-
import type { FaceConfig, FaceScanResult } from "@smileid/usesmileid_bridge";
|
|
3
|
-
import { useSmileIDAnalyzerExecutor } from "@smileid/usesmileid_platform_interface";
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Google ML Kit face analyzer (Android-only).
|
|
7
|
-
*
|
|
8
|
-
* Thin shim around the existing native session
|
|
9
|
-
* `com.usesmileid.bridge.session.UseSmileIDFaceSession(FaceDetectorAnalyzer())`
|
|
10
|
-
* shipped in the `com.smileidentity:mlkit-face` AAR. The native side
|
|
11
|
-
* runs the entire pipeline (analyzer + validator + transitioner +
|
|
12
|
-
* state machine); this TS layer only routes calls and validates wire
|
|
13
|
-
* payloads at the channel boundary.
|
|
14
|
-
*
|
|
15
|
-
* The canonical wiring lives in [useSmileIDAnalyzerExecutor] in
|
|
16
|
-
* `usesmileid_platform_interface`. This file only supplies the
|
|
17
|
-
* provider-specific identity so all six provider modules share one set
|
|
18
|
-
* of correctness properties by construction.
|
|
19
|
-
*
|
|
20
|
-
* Lifecycle: `configure()` → per-frame analysis via the native frame
|
|
21
|
-
* processor plugin → `reset()`? → `close()`. Calling
|
|
22
|
-
* `close()` twice is idempotent. See `usesmileid_platform_interface`
|
|
23
|
-
* `FaceAnalyzer` for the canonical contract.
|
|
24
|
-
*/
|
|
25
|
-
/**
|
|
26
|
-
* Must match the Kotlin Expo Module's `Name("UseSmileIDMlkitFace")` and its
|
|
27
|
-
* VisionCamera frame-processor-plugin registration — drift is a runtime
|
|
28
|
-
* "module/plugin not found" error, not a compile error.
|
|
29
|
-
*/
|
|
30
|
-
export const UseSmileIDMlkitFaceIdentifiers = {
|
|
31
|
-
nativeModuleName: "UseSmileIDMlkitFace",
|
|
32
|
-
frameProcessorPluginName: "UseSmileIDMlkitFaceFrameProcessor",
|
|
33
|
-
} as const;
|
|
34
|
-
|
|
35
|
-
export const useSmileIDMlkitFace = useSmileIDAnalyzerExecutor({
|
|
36
|
-
nativeModuleName: UseSmileIDMlkitFaceIdentifiers.nativeModuleName,
|
|
37
|
-
frameProcessorPluginName:
|
|
38
|
-
UseSmileIDMlkitFaceIdentifiers.frameProcessorPluginName,
|
|
39
|
-
kind: "face",
|
|
40
|
-
key: AnalyzerFactoryKey.MLKitFace,
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
// Re-export the contract types so consumers don't need a separate
|
|
44
|
-
// import line for `FaceConfig` / `FaceScanResult` when they're using
|
|
45
|
-
// this provider.
|
|
46
|
-
export type { FaceConfig, FaceScanResult };
|