@smileid/usesmileid_vision_face 12.0.0-beta01

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.
@@ -0,0 +1,280 @@
1
+ import ExpoModulesCore
2
+ import UIKit
3
+ import UseSmileIDBridge
4
+ import UseSmileIDVisionFace
5
+ import VisionCamera
6
+
7
+ /// Expo Module bridging `UseSmileIDFaceSession` to the JS side.
8
+ /// No ML logic lives here — the native session owns the full pipeline.
9
+ /// Publishes the active session to `UseSmileIDVisionFaceSessionHolder` so
10
+ /// the frame processor plugin can feed frames without crossing the JS thread.
11
+ public class UseSmileIDVisionFaceModule: Module {
12
+ /// Wire-format key partners pass via `FaceConfig.analyzerFactoryKey`.
13
+ static let KEY = "vision-face"
14
+
15
+ /// Prevents double-registration of the frame processor plugin when
16
+ /// Expo Go / dev-client fast-refreshes the JS bundle and `OnCreate`
17
+ /// runs again in the same native process.
18
+ private static var isFrameProcessorRegistered = false
19
+
20
+ private var session: UseSmileIDFaceSession?
21
+
22
+ private static func freshSessionDirectory() -> URL {
23
+ // Application Support — not Caches — so the OS cannot evict
24
+ // in-flight capture frames between capture and submission under
25
+ // memory pressure. The root is flagged out of iCloud / iTunes
26
+ // backup so biometric frames never leave the device.
27
+ let support = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first
28
+ ?? FileManager.default.temporaryDirectory
29
+ var root = support.appendingPathComponent("UseSmileID", isDirectory: true)
30
+ try? FileManager.default.createDirectory(at: root, withIntermediateDirectories: true)
31
+ var resourceValues = URLResourceValues()
32
+ resourceValues.isExcludedFromBackup = true
33
+ try? root.setResourceValues(resourceValues)
34
+ let dir = root.appendingPathComponent(String(Int(Date().timeIntervalSince1970 * 1000)), isDirectory: true)
35
+ try? FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true)
36
+ return dir
37
+ }
38
+
39
+ public func definition() -> ModuleDefinition {
40
+ Name("UseSmileIDVisionFace")
41
+
42
+ OnCreate {
43
+ FaceAnalyzerFactoryRegistry.shared.register(key: Self.KEY) { @Sendable in
44
+ let analyzer = FaceDetectorAnalyzer.Factory().newInstance()!
45
+ return { @Sendable input, state in await analyzer.analyze(data: input, state: state) }
46
+ }
47
+
48
+ // Guard prevents double-registration on dev-client fast-refresh reloads.
49
+ if !Self.isFrameProcessorRegistered {
50
+ FrameProcessorPluginRegistry.addFrameProcessorPlugin("UseSmileIDVisionFaceFrameProcessor") { proxy, options in
51
+ UseSmileIDVisionFaceFrameProcessorPlugin(proxy: proxy, options: options)
52
+ }
53
+ Self.isFrameProcessorRegistered = true
54
+ }
55
+ }
56
+
57
+ AsyncFunction("configure") { (config: [String: Any?]) in
58
+ let key = (config["analyzerFactoryKey"] as? String) ?? Self.KEY
59
+ let analyzeFrame = try FaceAnalyzerFactoryRegistry.shared.newAnalyzeFrame(key: key)
60
+ let storage = config.toResizingFrameStorage(sessionDirectory: Self.freshSessionDirectory())
61
+ let newSession = UseSmileIDFaceSession(
62
+ analyzeFrame: analyzeFrame,
63
+ config: config.toFaceScanConfig(),
64
+ frameStorage: storage
65
+ )
66
+ self.session = newSession
67
+ // Publish the session through the holder's operation queue so the
68
+ // write is serialised with any concurrent scan on the FP thread.
69
+ UseSmileIDVisionFaceSessionHolder.shared.setSession(newSession)
70
+ }
71
+
72
+ AsyncFunction("reset") {
73
+ UseSmileIDVisionFaceSessionHolder.shared.reset()
74
+ }
75
+
76
+ AsyncFunction("flush") { (promise: Promise) in
77
+ // Hop onto a detached task so the Expo queue is not blocked while
78
+ // flush waits behind any in-flight scan on the operation queue.
79
+ Task.detached(priority: .userInitiated) {
80
+ do {
81
+ try UseSmileIDVisionFaceSessionHolder.shared.flush()
82
+ promise.resolve(nil)
83
+ } catch {
84
+ promise.reject("FLUSH_FAILED", error.localizedDescription)
85
+ }
86
+ }
87
+ }
88
+
89
+ AsyncFunction("close") {
90
+ UseSmileIDVisionFaceSessionHolder.shared.setSession(nil)
91
+ self.session = nil
92
+ }
93
+
94
+ OnDestroy {
95
+ UseSmileIDVisionFaceSessionHolder.shared.setSession(nil)
96
+ self.session = nil
97
+ FaceAnalyzerFactoryRegistry.shared.unregister(key: Self.KEY)
98
+ }
99
+ }
100
+ }
101
+
102
+ /// Adapt the partner-supplied JS object (a `[String: Any?]` after
103
+ /// Expo Modules' bridge marshalling) into a `ResizingFrameStorage`
104
+ /// configured with the token-resolved capture dimensions and JPEG
105
+ /// quality values. All fields fall back to SDK defaults when absent.
106
+ private extension Dictionary where Key == String, Value == Any? {
107
+ func toResizingFrameStorage(sessionDirectory: URL) -> ResizingFrameStorage {
108
+ // iOS quality is CGFloat 0..1; wire keys carry an integer 1..100.
109
+ let selfieTargetWidth = (self["selfieImageWidth"] as? NSNumber)?.intValue ?? 480
110
+ let selfieTargetHeight = (self["selfieImageHeight"] as? NSNumber)?.intValue ?? 640
111
+ let livenessTargetWidth = (self["livenessImageWidth"] as? NSNumber)?.intValue ?? 480
112
+ let livenessTargetHeight = (self["livenessImageHeight"] as? NSNumber)?.intValue ?? 640
113
+ let selfieQuality = ((self["selfieJpegQuality"] as? NSNumber)?.intValue).map { CGFloat($0) / 100.0 } ?? 0.95
114
+ let livenessQuality = ((self["livenessJpegQuality"] as? NSNumber)?.intValue).map { CGFloat($0) / 100.0 } ?? 0.80
115
+ return ResizingFrameStorage(
116
+ sessionDirectory: sessionDirectory,
117
+ documentSide: .front,
118
+ selfieTargetWidth: selfieTargetWidth,
119
+ selfieTargetHeight: selfieTargetHeight,
120
+ livenessTargetWidth: livenessTargetWidth,
121
+ livenessTargetHeight: livenessTargetHeight,
122
+ selfieQuality: selfieQuality,
123
+ livenessQuality: livenessQuality
124
+ )
125
+ }
126
+ }
127
+
128
+ /// Adapt the partner-supplied JS object (a `[String: Any?]` after
129
+ /// Expo Modules' bridge marshalling) into the strongly-typed
130
+ /// `FaceScanConfig` the native session expects. Every field is
131
+ /// nullable on the wire; the fallbacks below are the production
132
+ /// defaults.
133
+ private extension Dictionary where Key == String, Value == Any? {
134
+ // JS numbers cross the Expo bridge as `NSNumber`; direct `Int`/`Double` casts
135
+ // silently fail when the bit pattern doesn't match — funnel through `NSNumber`.
136
+ func toFaceScanConfig() -> FaceScanConfig {
137
+ let base = FaceDetectorMode.from(token: self["faceDetectorMode"] as? String).config
138
+ return FaceScanConfig(
139
+ selfieCaptureTimeout: (self["selfieCaptureTimeout"] as? NSNumber)?.intValue ?? 100_000,
140
+ // Default yields 1 selfie + 7 liveness frames — the server requires
141
+ // `liveness_images >= 6`, so anything below 7 causes submission rejection.
142
+ numSamples: (self["numSamples"] as? NSNumber)?.intValue ?? 8,
143
+ sampleInterval: (self["sampleInterval"] as? NSNumber)?.intValue ?? 350,
144
+ stayInFoundDuration: (self["stayInFoundDuration"] as? NSNumber)?.intValue ?? 3_000,
145
+ faceDetectorConfig: FaceDetectorConfig(
146
+ minFillThreshold: (self["minFillThreshold"] as? NSNumber)?.floatValue ?? base.minFillThreshold,
147
+ maxFillThreshold: (self["maxFillThreshold"] as? NSNumber)?.floatValue ?? base.maxFillThreshold,
148
+ faceDetectorMinScore: (self["faceDetectorMinScore"] as? NSNumber)?.floatValue ?? base.faceDetectorMinScore,
149
+ centerToleranceX: (self["centerToleranceX"] as? NSNumber)?.floatValue ?? base.centerToleranceX,
150
+ centerToleranceY: (self["centerToleranceY"] as? NSNumber)?.floatValue ?? base.centerToleranceY,
151
+ minFaceBoundsThreshold: (self["minFaceBoundsThreshold"] as? NSNumber)?.floatValue ?? base.minFaceBoundsThreshold,
152
+ maxFaceBoundsThreshold: (self["maxFaceBoundsThreshold"] as? NSNumber)?.floatValue ?? base.maxFaceBoundsThreshold,
153
+ motionBlurMinIou: (self["motionBlurMinIou"] as? NSNumber)?.floatValue ?? base.motionBlurMinIou,
154
+ motionBlurMinDuration: (self["motionBlurMinDuration"] as? NSNumber)?.intValue ?? base.motionBlurMinDuration,
155
+ minTopPadding: (self["minTopPadding"] as? NSNumber)?.floatValue ?? base.minTopPadding,
156
+ minSmileProbability: (self["minSmileProbability"] as? NSNumber)?.floatValue ?? base.minSmileProbability
157
+ ),
158
+ enableEnhancedLiveness: (self["enableEnhancedLiveness"] as? NSNumber)?.boolValue ?? false
159
+ )
160
+ }
161
+ }
162
+
163
+ // Wire-format converters for the native session result types. Marked
164
+ // `internal` (no access modifier) so both this file and
165
+ // `UseSmileIDVisionFaceSessionHolder` — which builds the same wire dict
166
+ // inside `scanAndDrain` — share one canonical set. The plugin file uses
167
+ // the holder's `scanAndDrain` return value and never calls these directly.
168
+
169
+ /// Keys must match `faceDetectorOutputSchema` field-for-field.
170
+ extension UseSmileIDBridge.FaceDetectorOutput {
171
+ func toWireMap() -> [String: Any] {
172
+ let primaryFace = faces.max { $0.boundingBox.area < $1.boundingBox.area }
173
+ var map: [String: Any] = [
174
+ "faces": faces.map { $0.boundingBox.toWireMap() },
175
+ "brightness": Double(primaryFace?.brightness ?? 0)
176
+ ]
177
+ if let headPose = primaryFace?.headPose {
178
+ map["headPose"] = headPose.toWireMap()
179
+ }
180
+ if let smilingProbability = primaryFace?.smilingProbability {
181
+ map["smilingProbability"] = Double(smilingProbability)
182
+ }
183
+ return map
184
+ }
185
+ }
186
+
187
+ extension UseSmileIDBridge.BoundingBox {
188
+ func toWireMap() -> [String: Any] {
189
+ [
190
+ "left": Double(left),
191
+ "top": Double(top),
192
+ "width": Double(width),
193
+ "height": Double(height),
194
+ "score": Double(score)
195
+ ]
196
+ }
197
+ }
198
+
199
+ extension UseSmileIDBridge.HeadPoseResult {
200
+ func toWireMap() -> [String: Any] {
201
+ [
202
+ "yaw": Double(yaw),
203
+ "pitch": Double(pitch),
204
+ "roll": Double(roll)
205
+ ]
206
+ }
207
+ }
208
+
209
+ extension UseSmileIDBridge.CaptureReadyEvent {
210
+ func toWireMap() -> [String: Any] {
211
+ let kindString: String
212
+ let captureIndex: Int
213
+ let urlString: String
214
+ switch self {
215
+ case .selfieFrame(let url):
216
+ kindString = "selfie"
217
+ captureIndex = 0
218
+ urlString = url.absoluteString
219
+ case .livenessFrame(let index, let url):
220
+ kindString = "liveness"
221
+ captureIndex = index
222
+ urlString = url.absoluteString
223
+ case .documentFrame(let url):
224
+ kindString = "document"
225
+ captureIndex = 0
226
+ urlString = url.absoluteString
227
+ }
228
+ return [
229
+ "kind": kindString,
230
+ "captureIndex": captureIndex,
231
+ "url": urlString
232
+ ]
233
+ }
234
+ }
235
+
236
+ /// Map the native state-machine type into the wire-string the JS-side
237
+ /// `scanStateTypeSchema` enum accepts. Keep in lock-step with the
238
+ /// `ScanStateType` enum order so cross-platform analytics/dashboards
239
+ /// see consistent values.
240
+ extension UseSmileIDBridge.UseSmileIDScanState {
241
+ func toWireMap() -> [String: Any] {
242
+ let typeString: String
243
+ switch type {
244
+ case .initial: typeString = "initial"
245
+ case .found: typeString = "found"
246
+ case .satisfied: typeString = "satisfied"
247
+ case .unsatisfied: typeString = "unsatisfied"
248
+ case .finished: typeString = "finished"
249
+ case .timeout: typeString = "timeout"
250
+ case .headTurn: typeString = "headTurn"
251
+ }
252
+ let directionString: String?
253
+ switch currentDirection {
254
+ case .left: directionString = "left"
255
+ case .right: directionString = "right"
256
+ case .up: directionString = "up"
257
+ case nil: directionString = nil
258
+ }
259
+ var map: [String: Any] = ["type": typeString]
260
+ if let feedbackName {
261
+ map["feedback"] = feedbackName
262
+ }
263
+ if let unsatisfiedReason {
264
+ map["reason"] = unsatisfiedReason
265
+ }
266
+ if let direction = directionString {
267
+ map["currentDirection"] = direction
268
+ }
269
+ if let captureProgress {
270
+ map["captureProgress"] = Double(captureProgress)
271
+ }
272
+ // Latched pose-match flag for the ESS head turn — drives the grey→green
273
+ // direction arrow. Emit only while matched (omit ⇒ false on the JS side),
274
+ // matching the optional-field omission convention above.
275
+ if poseMatched {
276
+ map["poseMatched"] = true
277
+ }
278
+ return map
279
+ }
280
+ }
@@ -0,0 +1,105 @@
1
+ import Foundation
2
+ import UIKit
3
+ import UseSmileIDBridge
4
+ import UseSmileIDVisionFace
5
+
6
+ /// Process-wide holder for the active `UseSmileIDFaceSession`.
7
+ ///
8
+ /// All session ops (scan+drain, reset, flush, setSession) serialise through one
9
+ /// `operationQueue`. Deadlock invariant: `Task.detached` bodies inside
10
+ /// `scanAndDrain`/`flush` run on the cooperative pool and never re-enter the holder.
11
+ final class UseSmileIDVisionFaceSessionHolder {
12
+ static let shared = UseSmileIDVisionFaceSessionHolder()
13
+
14
+ /// Single serial queue serialising every session operation.
15
+ private let operationQueue = DispatchQueue(
16
+ label: "com.usesmileid.vision.face.session.operations",
17
+ qos: .userInitiated
18
+ )
19
+
20
+ private var session: UseSmileIDFaceSession?
21
+
22
+ private init() {}
23
+
24
+ /// Replace or clear the active session. Called from the Expo module's
25
+ /// `configure`, `close`, and `OnDestroy` paths.
26
+ func setSession(_ newSession: UseSmileIDFaceSession?) {
27
+ operationQueue.sync {
28
+ session = newSession
29
+ }
30
+ }
31
+
32
+ /// Run `session.reset()` serialised with any in-flight scan. Called from
33
+ /// the Expo module's `reset` AsyncFunction.
34
+ func reset() {
35
+ operationQueue.sync {
36
+ session?.reset()
37
+ }
38
+ }
39
+
40
+ /// Flush the active session, blocking the caller until completion.
41
+ /// Semaphore wait runs inside `operationQueue.sync` — blocks concurrent
42
+ /// scans for the flush duration. Task body runs on cooperative pool; no deadlock.
43
+ func flush() throws {
44
+ var flushError: Error?
45
+ operationQueue.sync {
46
+ guard let currentSession = session else {
47
+ return
48
+ }
49
+ let errorBox = FlushErrorBox()
50
+ let semaphore = DispatchSemaphore(value: 0)
51
+ Task.detached(priority: .userInitiated) {
52
+ do {
53
+ try await currentSession.flush()
54
+ } catch {
55
+ errorBox.error = error
56
+ }
57
+ semaphore.signal()
58
+ }
59
+ semaphore.wait()
60
+ flushError = errorBox.error
61
+ }
62
+ if let error = flushError {
63
+ throw error
64
+ }
65
+ }
66
+
67
+ /// Run `session.scan` + `drainCaptureReadyEvents` as one atomic unit, blocking the
68
+ /// caller. Returns `nil` when no session is configured; never throws.
69
+ func scanAndDrain(image: UIImage, rotation: Int) -> [String: Any]? {
70
+ var result: [String: Any]?
71
+ operationQueue.sync {
72
+ guard let currentSession = session else {
73
+ return
74
+ }
75
+ let resultBox = ScanResultBox()
76
+ let semaphore = DispatchSemaphore(value: 0)
77
+ Task.detached(priority: .userInitiated) {
78
+ let scanResult = await currentSession.scan(image: image, rotation: rotation)
79
+ let nativeOutput = scanResult.output as? UseSmileIDBridge.FaceDetectorOutput
80
+ let state = scanResult.state
81
+ let events = currentSession.drainCaptureReadyEvents()
82
+ resultBox.value = [
83
+ "output": nativeOutput?.toWireMap() ?? (["faces": [], "brightness": 0.0] as [String: Any]),
84
+ "state": state.toWireMap(),
85
+ "capturedEvents": events.map { $0.toWireMap() }
86
+ ]
87
+ semaphore.signal()
88
+ }
89
+ semaphore.wait()
90
+ result = resultBox.value
91
+ }
92
+ return result
93
+ }
94
+ }
95
+
96
+ /// Shuttles a scan result from the cooperative-pool Task to the semaphore-blocked caller.
97
+ private final class ScanResultBox: @unchecked Sendable {
98
+ var value: [String: Any] = [:]
99
+ }
100
+
101
+ /// Reference-type container used to propagate a flush error across the
102
+ /// same semaphore boundary as `ScanResultBox`.
103
+ private final class FlushErrorBox: @unchecked Sendable {
104
+ var error: Error?
105
+ }
package/package.json ADDED
@@ -0,0 +1,79 @@
1
+ {
2
+ "name": "@smileid/usesmileid_vision_face",
3
+ "version": "12.0.0-beta01",
4
+ "description": "UseSmileID v12 — Apple Vision face provider (iOS-only). Thin Expo Module shim around the existing Swift Package `UseSmileIDVisionFace`.",
5
+ "homepage": "https://usesmileid.com",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/smileidentity/react-native-expo.git",
9
+ "directory": "usesmileid_vision_face"
10
+ },
11
+ "bugs": {
12
+ "url": "https://github.com/smileidentity/react-native-expo/issues"
13
+ },
14
+ "license": "MIT",
15
+ "author": "Smile Identity <hello@usesmileid.com>",
16
+ "keywords": [
17
+ "react-native",
18
+ "expo",
19
+ "expo-module",
20
+ "biometric",
21
+ "kyc",
22
+ "identity-verification",
23
+ "face-detection",
24
+ "document-capture",
25
+ "smile-id"
26
+ ],
27
+ "main": "./dist/index.js",
28
+ "types": "./dist/index.d.ts",
29
+ "exports": {
30
+ ".": {
31
+ "types": "./dist/index.d.ts",
32
+ "import": "./dist/index.js",
33
+ "require": "./dist/index.js"
34
+ },
35
+ "./app.plugin.js": "./app.plugin.js"
36
+ },
37
+ "files": [
38
+ "dist",
39
+ "src",
40
+ "ios",
41
+ "plugin/build",
42
+ "plugin/src",
43
+ "plugin/tsconfig.json",
44
+ "app.plugin.js",
45
+ "expo-module.config.json",
46
+ "README.md",
47
+ "LICENSE",
48
+ "CHANGELOG.md"
49
+ ],
50
+ "dependencies": {
51
+ "@smileid/usesmileid_bridge": "12.0.0-beta01",
52
+ "@smileid/usesmileid_platform_interface": "12.0.0-beta01"
53
+ },
54
+ "peerDependencies": {
55
+ "expo": ">=56.0.0",
56
+ "expo-modules-core": ">=56.0.0",
57
+ "react-native": ">=0.83.0"
58
+ },
59
+ "devDependencies": {
60
+ "@expo/config-plugins": "^56.0.8",
61
+ "@types/node": "^25.9.1",
62
+ "expo-modules-core": "^56.0.15",
63
+ "react-native": "^0.85.3",
64
+ "typescript": "^6.0.3"
65
+ },
66
+ "publishConfig": {
67
+ "access": "public",
68
+ "provenance": false
69
+ },
70
+ "scripts": {
71
+ "build": "pnpm run build:lib && pnpm run build:plugin",
72
+ "build:lib": "tsc -p tsconfig.json",
73
+ "build:plugin": "tsc -p plugin/tsconfig.json",
74
+ "typecheck": "tsc -b && tsc -p plugin/tsconfig.json --noEmit",
75
+ "test": "vitest run",
76
+ "test:watch": "vitest",
77
+ "clean": "rm -rf dist plugin/build .turbo coverage"
78
+ }
79
+ }
@@ -0,0 +1,3 @@
1
+ import { ConfigPlugin } from "@expo/config-plugins";
2
+ declare const withVisionFaceSpm: ConfigPlugin;
3
+ export default withVisionFaceSpm;
@@ -0,0 +1,115 @@
1
+ "use strict";
2
+ // Expo config plugin that wires the Smile Identity Swift Package
3
+ // (`https://github.com/smileidentity/ios-spm`) into both:
4
+ // 1. The consuming app's Xcode project — so the .xcframeworks ship
5
+ // and embed in the final .app bundle.
6
+ // 2. The autogenerated Pods.xcodeproj at `pod install` time — so
7
+ // the Expo Module bridge (`UseSmileIDVisionFaceExpo`) compiles
8
+ // with `import UseSmileIDVisionFace` / `import UseSmileIDBridge`.
9
+ //
10
+ // Why a plugin: `expo-modules-autolinking@55.0.x`'s
11
+ // `RawModuleConfigApple` schema has no `dependencies` field, so the
12
+ // `apple.dependencies` block in `expo-module.config.json` is silently
13
+ // ignored. Without this plugin, neither the bridge nor the host app
14
+ // know about the SPM package.
15
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
16
+ if (k2 === undefined) k2 = k;
17
+ var desc = Object.getOwnPropertyDescriptor(m, k);
18
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
19
+ desc = { enumerable: true, get: function() { return m[k]; } };
20
+ }
21
+ Object.defineProperty(o, k2, desc);
22
+ }) : (function(o, m, k, k2) {
23
+ if (k2 === undefined) k2 = k;
24
+ o[k2] = m[k];
25
+ }));
26
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
27
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
28
+ }) : function(o, v) {
29
+ o["default"] = v;
30
+ });
31
+ var __importStar = (this && this.__importStar) || (function () {
32
+ var ownKeys = function(o) {
33
+ ownKeys = Object.getOwnPropertyNames || function (o) {
34
+ var ar = [];
35
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
36
+ return ar;
37
+ };
38
+ return ownKeys(o);
39
+ };
40
+ return function (mod) {
41
+ if (mod && mod.__esModule) return mod;
42
+ var result = {};
43
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
44
+ __setModuleDefault(result, mod);
45
+ return result;
46
+ };
47
+ })();
48
+ Object.defineProperty(exports, "__esModule", { value: true });
49
+ const config_plugins_1 = require("@expo/config-plugins");
50
+ const fs = __importStar(require("fs"));
51
+ const path = __importStar(require("path"));
52
+ const podfile_1 = require("./lib/podfile");
53
+ const spm_1 = require("./lib/spm");
54
+ // Pinned to the exact `12.0.0-beta01` release tag — `from: 12.0.0`
55
+ // would not resolve because `ios-spm` publishes pre-release tags (e.g.
56
+ // `12.0.0-beta01`), and SPM excludes pre-release tags from open-ended
57
+ // major-version requirements.
58
+ const SMILE_ID_PACKAGE = {
59
+ url: "https://github.com/smileidentity/ios-spm",
60
+ requirement: {
61
+ kind: "exactVersion",
62
+ version: "12.0.0-beta01",
63
+ },
64
+ };
65
+ const POD_TARGET = "UseSmileIDVisionFaceExpo";
66
+ // `UseSmileIDBridge` is also listed explicitly even though the Vision
67
+ // library product wraps it as a transitive target — empirically this
68
+ // is what makes `import UseSmileIDBridge` resolve in the bridge under
69
+ // SPM-via-CocoaPods. A cleaner config that drops the redundant
70
+ // listing is plausible (the SPM Package.swift declares
71
+ // `targets: ["UseSmileIDVisionFace", "UseSmileIDBridge"]` for the Vision
72
+ // library product) but unverified, and the duplicate produces only a
73
+ // benign "Conflicting parseable interfaces" warning at link time.
74
+ const SPM_PRODUCTS = ["UseSmileIDVisionFace", "UseSmileIDBridge"];
75
+ const PODFILE_MARKER = "usesmileid_vision_face";
76
+ const withVisionFaceSpm = (config) => {
77
+ // 1. Add the package + products to the host app's Xcode project so
78
+ // Xcode resolves the binary xcframeworks and embeds them in the
79
+ // .app at link time.
80
+ config = (0, config_plugins_1.withXcodeProject)(config, (config) => {
81
+ const appTarget = config.modRequest.projectName;
82
+ if (!appTarget) {
83
+ throw new Error("[usesmileid_vision_face] Unable to resolve host app native target name from Expo config.");
84
+ }
85
+ (0, spm_1.linkSpmPackage)(config.modResults, SMILE_ID_PACKAGE, {
86
+ targetName: appTarget,
87
+ products: [...SPM_PRODUCTS],
88
+ });
89
+ return config;
90
+ });
91
+ // 2. Inject a Ruby block into the Podfile's post_install hook that
92
+ // re-applies the same wiring to Pods.xcodeproj when CocoaPods
93
+ // materializes it. The bridge pod target needs the products
94
+ // visible at compile time; the host-app step alone isn't enough.
95
+ config = (0, config_plugins_1.withDangerousMod)(config, [
96
+ "ios",
97
+ (config) => {
98
+ const podfilePath = path.join(config.modRequest.platformProjectRoot, "Podfile");
99
+ const original = fs.readFileSync(podfilePath, "utf-8");
100
+ const next = (0, podfile_1.injectPodfileSpmBlock)(original, {
101
+ marker: PODFILE_MARKER,
102
+ package: SMILE_ID_PACKAGE,
103
+ podTarget: POD_TARGET,
104
+ products: [...SPM_PRODUCTS],
105
+ });
106
+ if (next !== original) {
107
+ fs.writeFileSync(podfilePath, next);
108
+ }
109
+ return config;
110
+ },
111
+ ]);
112
+ return config;
113
+ };
114
+ exports.default = withVisionFaceSpm;
115
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAAA,iEAAiE;AACjE,0DAA0D;AAC1D,qEAAqE;AACrE,2CAA2C;AAC3C,mEAAmE;AACnE,oEAAoE;AACpE,uEAAuE;AACvE,EAAE;AACF,oDAAoD;AACpD,oEAAoE;AACpE,sEAAsE;AACtE,oEAAoE;AACpE,8BAA8B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAE9B,yDAI8B;AAC9B,uCAAyB;AACzB,2CAA6B;AAE7B,2CAAsD;AACtD,mCAAuD;AAEvD,mEAAmE;AACnE,uEAAuE;AACvE,sEAAsE;AACtE,8BAA8B;AAC9B,MAAM,gBAAgB,GAAe;IACnC,GAAG,EAAE,0CAA0C;IAC/C,WAAW,EAAE;QACX,IAAI,EAAE,cAAc;QACpB,OAAO,EAAE,eAAe;KACzB;CACF,CAAC;AAEF,MAAM,UAAU,GAAG,0BAA0B,CAAC;AAC9C,sEAAsE;AACtE,qEAAqE;AACrE,sEAAsE;AACtE,+DAA+D;AAC/D,uDAAuD;AACvD,yEAAyE;AACzE,qEAAqE;AACrE,kEAAkE;AAClE,MAAM,YAAY,GAAG,CAAC,sBAAsB,EAAE,kBAAkB,CAAU,CAAC;AAC3E,MAAM,cAAc,GAAG,wBAAwB,CAAC;AAEhD,MAAM,iBAAiB,GAAiB,CAAC,MAAM,EAAE,EAAE;IACjD,mEAAmE;IACnE,mEAAmE;IACnE,wBAAwB;IACxB,MAAM,GAAG,IAAA,iCAAgB,EAAC,MAAM,EAAE,CAAC,MAAM,EAAE,EAAE;QAC3C,MAAM,SAAS,GAAG,MAAM,CAAC,UAAU,CAAC,WAAW,CAAC;QAChD,IAAI,CAAC,SAAS,EAAE,CAAC;YACf,MAAM,IAAI,KAAK,CACb,0FAA0F,CAC3F,CAAC;QACJ,CAAC;QACD,IAAA,oBAAc,EAAC,MAAM,CAAC,UAAU,EAAE,gBAAgB,EAAE;YAClD,UAAU,EAAE,SAAS;YACrB,QAAQ,EAAE,CAAC,GAAG,YAAY,CAAC;SAC5B,CAAC,CAAC;QACH,OAAO,MAAM,CAAC;IAChB,CAAC,CAAC,CAAC;IAEH,mEAAmE;IACnE,iEAAiE;IACjE,+DAA+D;IAC/D,oEAAoE;IACpE,MAAM,GAAG,IAAA,iCAAgB,EAAC,MAAM,EAAE;QAChC,KAAK;QACL,CAAC,MAAM,EAAE,EAAE;YACT,MAAM,WAAW,GAAG,IAAI,CAAC,IAAI,CAC3B,MAAM,CAAC,UAAU,CAAC,mBAAmB,EACrC,SAAS,CACV,CAAC;YACF,MAAM,QAAQ,GAAG,EAAE,CAAC,YAAY,CAAC,WAAW,EAAE,OAAO,CAAC,CAAC;YACvD,MAAM,IAAI,GAAG,IAAA,+BAAqB,EAAC,QAAQ,EAAE;gBAC3C,MAAM,EAAE,cAAc;gBACtB,OAAO,EAAE,gBAAgB;gBACzB,SAAS,EAAE,UAAU;gBACrB,QAAQ,EAAE,CAAC,GAAG,YAAY,CAAC;aAC5B,CAAC,CAAC;YACH,IAAI,IAAI,KAAK,QAAQ,EAAE,CAAC;gBACtB,EAAE,CAAC,aAAa,CAAC,WAAW,EAAE,IAAI,CAAC,CAAC;YACtC,CAAC;YACD,OAAO,MAAM,CAAC;QAChB,CAAC;KACF,CAAC,CAAC;IAEH,OAAO,MAAM,CAAC;AAChB,CAAC,CAAC;AAEF,kBAAe,iBAAiB,CAAC"}
@@ -0,0 +1,17 @@
1
+ import type { SpmPackage } from "./spm";
2
+ export interface PodfileLinkOptions {
3
+ /** Marker key — distinct per bridge so each plugin owns its own block. */
4
+ marker: string;
5
+ package: SpmPackage;
6
+ /** Pod target name (e.g. `UseSmileIDVisionFaceExpo`). */
7
+ podTarget: string;
8
+ /** SPM library product names to link to the pod target. */
9
+ products: string[];
10
+ }
11
+ /**
12
+ * Idempotently injects an SPM-linking Ruby block into the Podfile's
13
+ * top-level `post_install` block. Re-running on an already-injected
14
+ * Podfile rewrites the same marker block in place (so version /
15
+ * product changes flow through cleanly on the next prebuild).
16
+ */
17
+ export declare function injectPodfileSpmBlock(podfile: string, options: PodfileLinkOptions): string;