@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 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
- no manual native registration required.
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
 
@@ -17,7 +17,7 @@ plugins {
17
17
  }
18
18
 
19
19
  group = 'com.usesmileid.mlkit.face'
20
- version = '12.0.1'
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.1'
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.1'
45
-
46
- // VisionCamera frame-processor plugin classes. The host app provides
47
- // this dependency via RN autolinking — react-native-vision-camera >= 4.7.3
48
- // must be present in the partner app's package.json.
49
- implementation project(':react-native-vision-camera')
50
-
51
- // CameraX types (`ImageProxy`, `ExperimentalGetImage`) referenced by the
52
- // frame processor plugin. VisionCamera declares camera-core as
53
- // `implementation`, so it is NOT on this module's compile classpath
54
- // transitively — compileOnly pins the compile-time API while the host
55
- // app's VisionCamera provides the runtime classes (its camera-core is
56
- // 1.5.0-alpha03; the `ImageProxy` surface used here is stable since 1.0).
57
- compileOnly 'androidx.camera:camera-core:1.5.0-alpha03'
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
  }
@@ -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
- * Per-frame work is handled by [UseSmileIDMlkitFaceFrameProcessorPlugin],
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 FP plugin
14
- * blocks its thread via [runBlocking]; VisionCamera drops frames that
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
 
@@ -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
@@ -0,0 +1,3 @@
1
+ // Entry point Expo discovers when an app references this package in
2
+ // `expo.plugins`. The compiled plugin lives at `plugin/build/index.js`.
3
+ module.exports = require("./plugin/build");
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")` and its
24
- * VisionCamera frame-processor-plugin registration — drift is a runtime
25
- * "module/plugin not found" error, not a compile error.
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 { AnalyzerFactoryKey } from "@smileid/usesmileid_bridge";
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.1",
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
- "src",
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/usesmileid_bridge": "12.0.1",
47
- "@smileid/usesmileid_platform_interface": "12.0.1"
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
- "expo-modules-core": "^56.0.15",
56
- "react-native": "^0.85.3",
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": "tsc -p tsconfig.json",
64
- "typecheck": "tsc -b",
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,"\\$&")}
@@ -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
- }
@@ -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 };