@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.
- package/CHANGELOG.md +3 -0
- package/LICENSE +21 -0
- package/README.md +50 -0
- package/app.plugin.js +3 -0
- package/dist/index.d.ts +31 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +36 -0
- package/dist/index.js.map +1 -0
- package/expo-module.config.json +6 -0
- package/ios/UseSmileIDVisionFaceExpo.podspec +61 -0
- package/ios/UseSmileIDVisionFaceModule/ResizingFrameStorage.swift +181 -0
- package/ios/UseSmileIDVisionFaceModule/UseSmileIDAnalyzerExecutor.swift +119 -0
- package/ios/UseSmileIDVisionFaceModule/UseSmileIDVisionFaceFrameProcessorPlugin.swift +96 -0
- package/ios/UseSmileIDVisionFaceModule/UseSmileIDVisionFaceModule.swift +280 -0
- package/ios/UseSmileIDVisionFaceModule/UseSmileIDVisionFaceSessionHolder.swift +105 -0
- package/package.json +79 -0
- package/plugin/build/index.d.ts +3 -0
- package/plugin/build/index.js +115 -0
- package/plugin/build/index.js.map +1 -0
- package/plugin/build/lib/podfile.d.ts +17 -0
- package/plugin/build/lib/podfile.js +180 -0
- package/plugin/build/lib/podfile.js.map +1 -0
- package/plugin/build/lib/spm.d.ts +33 -0
- package/plugin/build/lib/spm.js +196 -0
- package/plugin/build/lib/spm.js.map +1 -0
- package/plugin/src/index.ts +96 -0
- package/plugin/src/lib/podfile.ts +209 -0
- package/plugin/src/lib/spm.ts +314 -0
- package/plugin/tsconfig.json +19 -0
- package/src/__tests__/use_smile_id_vision_face.test.ts +79 -0
- package/src/index.ts +41 -0
|
@@ -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,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;
|