@smileid/usesmileid_vision_document 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,221 @@
1
+ import ExpoModulesCore
2
+ import UIKit
3
+ import UseSmileIDBridge
4
+ import UseSmileIDVisionDocument
5
+ import VisionCamera
6
+
7
+ /// Expo Module bridging the existing native session
8
+ /// `UseSmileIDDocumentSession(analyzeFrame:)` (shipped in the
9
+ /// `UseSmileIDVisionDocument` Swift Package) to the JS side.
10
+ ///
11
+ /// No ML logic lives here — the native session owns the entire
12
+ /// pipeline. This module adapts the four contract methods (`configure /
13
+ /// reset / flush / close`) and publishes the active session to
14
+ /// `UseSmileIDVisionDocumentSessionHolder` so the frame processor plugin
15
+ /// can feed camera frames directly without crossing the JS thread.
16
+ ///
17
+ /// All session operations (configure, reset, flush, close, per-frame scan)
18
+ /// are serialised through `UseSmileIDVisionDocumentSessionHolder.shared`,
19
+ /// which owns the single `operationQueue` serialisation point.
20
+ ///
21
+ /// Lifecycle: Expo instantiates exactly one `Module` per app process;
22
+ /// `session` is owned for the module's lifetime.
23
+ public class UseSmileIDVisionDocumentModule: Module {
24
+ private lazy var analyzer: DocumentDetectorAnalyzer = DocumentDetectorAnalyzer.Factory().newInstance()!
25
+
26
+ /// Prevents double-registration of the frame processor plugin when
27
+ /// Expo Go / dev-client fast-refreshes the JS bundle and `OnCreate`
28
+ /// runs again in the same native process.
29
+ private static var isFrameProcessorRegistered = false
30
+
31
+ private var session: UseSmileIDDocumentSession?
32
+
33
+ public func definition() -> ModuleDefinition {
34
+ Name("UseSmileIDVisionDocument")
35
+
36
+ OnCreate {
37
+ // Guard prevents double-registration on dev-client fast-refresh reloads.
38
+ if !Self.isFrameProcessorRegistered {
39
+ FrameProcessorPluginRegistry.addFrameProcessorPlugin("UseSmileIDVisionDocumentFrameProcessor") { proxy, options in
40
+ UseSmileIDVisionDocumentFrameProcessorPlugin(proxy: proxy, options: options)
41
+ }
42
+ Self.isFrameProcessorRegistered = true
43
+ }
44
+ }
45
+
46
+ AsyncFunction("configure") { (config: [String: Any?]) in
47
+ // The active side selects the `document_front.jpg` /
48
+ // `document_back.jpg` filename; two-sided flows re-`configure` with
49
+ // `back` so the back image doesn't overwrite the front.
50
+ let documentSide: DocumentSide =
51
+ (config["documentSide"] as? String) == "back" ? .back : .front
52
+ // iOS quality is CGFloat 0..1; the wire key carries an integer 1..100.
53
+ let documentQuality = ((config["documentJpegQuality"] as? NSNumber)?.intValue)
54
+ .map { CGFloat($0) / 100.0 } ?? 0.85
55
+ let storage = ResizingFrameStorage(
56
+ sessionDirectory: Self.freshSessionDirectory(),
57
+ documentSide: documentSide,
58
+ documentQuality: documentQuality
59
+ )
60
+ let captured = self.analyzer
61
+ let newSession = UseSmileIDDocumentSession(
62
+ analyzeFrame: { input, state in
63
+ await captured.analyze(data: input, state: state)
64
+ },
65
+ config: config.toDocumentScanConfig(),
66
+ frameStorage: storage
67
+ )
68
+ self.session = newSession
69
+ // Publish the session through the holder's operation queue so the
70
+ // write is serialised with any concurrent scan on the FP thread.
71
+ UseSmileIDVisionDocumentSessionHolder.shared.setSession(newSession)
72
+ }
73
+
74
+ AsyncFunction("reset") {
75
+ UseSmileIDVisionDocumentSessionHolder.shared.reset()
76
+ }
77
+
78
+ AsyncFunction("flush") { (promise: Promise) in
79
+ // Hop onto a detached task so the Expo queue is not blocked while
80
+ // flush waits behind any in-flight scan on the operation queue.
81
+ Task.detached(priority: .userInitiated) {
82
+ do {
83
+ try UseSmileIDVisionDocumentSessionHolder.shared.flush()
84
+ promise.resolve(nil)
85
+ } catch {
86
+ promise.reject("FLUSH_FAILED", error.localizedDescription)
87
+ }
88
+ }
89
+ }
90
+
91
+ AsyncFunction("close") {
92
+ UseSmileIDVisionDocumentSessionHolder.shared.setSession(nil)
93
+ self.session = nil
94
+ }
95
+
96
+ OnDestroy {
97
+ UseSmileIDVisionDocumentSessionHolder.shared.setSession(nil)
98
+ self.session = nil
99
+ }
100
+ }
101
+
102
+ private static func freshSessionDirectory() -> URL {
103
+ // Application Support — not Caches — so the OS cannot evict
104
+ // in-flight capture frames between capture and submission under
105
+ // memory pressure. The root is flagged out of iCloud / iTunes
106
+ // backup so biometric frames never leave the device.
107
+ let support = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first
108
+ ?? FileManager.default.temporaryDirectory
109
+ var root = support.appendingPathComponent("UseSmileID", isDirectory: true)
110
+ try? FileManager.default.createDirectory(at: root, withIntermediateDirectories: true)
111
+ var resourceValues = URLResourceValues()
112
+ resourceValues.isExcludedFromBackup = true
113
+ try? root.setResourceValues(resourceValues)
114
+ let dir = root.appendingPathComponent(String(Int(Date().timeIntervalSince1970 * 1000)), isDirectory: true)
115
+ try? FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true)
116
+ return dir
117
+ }
118
+ }
119
+
120
+ private extension Dictionary where Key == String, Value == Any? {
121
+ // JS numbers cross the Expo bridge as `NSNumber`; direct `Int`/`Double` casts
122
+ // silently fail when the bit pattern doesn't match — funnel through `NSNumber`.
123
+ func toDocumentScanConfig() -> DocumentScanConfig {
124
+ DocumentScanConfig(
125
+ documentDetectorConfig: DocumentDetectorConfig(
126
+ timeout: (self["timeout"] as? NSNumber)?.intValue ?? 15_000,
127
+ iouThreshold: (self["iouThreshold"] as? NSNumber)?.floatValue ?? 0.85,
128
+ timeRequired: (self["timeRequired"] as? NSNumber)?.intValue ?? 500,
129
+ minCoverageThreshold: (self["minCoverageThreshold"] as? NSNumber)?.floatValue ?? 0.18,
130
+ maxCoverageThreshold: (self["maxCoverageThreshold"] as? NSNumber)?.floatValue ?? 0.78,
131
+ centerToleranceX: (self["centerToleranceX"] as? NSNumber)?.floatValue ?? 0.05,
132
+ centerToleranceY: (self["centerToleranceY"] as? NSNumber)?.floatValue ?? 0.05
133
+ ),
134
+ stayInFoundDuration: (self["stayInFoundDuration"] as? NSNumber)?.intValue ?? 1_000
135
+ )
136
+ }
137
+ }
138
+
139
+ // Wire-format converters for the native session result types. Marked
140
+ // `internal` (no access modifier) so both this file and
141
+ // `UseSmileIDVisionDocumentSessionHolder` — which builds the same wire dict
142
+ // inside `scanAndDrain` — share one canonical set. The plugin file uses
143
+ // the holder's `scanAndDrain` return value and never calls these directly.
144
+
145
+ extension UseSmileIDBridge.DocumentDetectorOutput {
146
+ func toWireMap() -> [String: Any] {
147
+ [
148
+ "documents": documents.map { $0.toWireMap() },
149
+ "brightness": Double(brightness)
150
+ ]
151
+ }
152
+ }
153
+
154
+ extension UseSmileIDBridge.BoundingBox {
155
+ func toWireMap() -> [String: Any] {
156
+ [
157
+ "left": Double(left),
158
+ "top": Double(top),
159
+ "width": Double(width),
160
+ "height": Double(height),
161
+ "score": Double(score)
162
+ ]
163
+ }
164
+ }
165
+
166
+ extension UseSmileIDBridge.CaptureReadyEvent {
167
+ func toWireMap() -> [String: Any] {
168
+ let kindString: String
169
+ let captureIndex: Int
170
+ let urlString: String
171
+ switch self {
172
+ case .selfieFrame(let url):
173
+ kindString = "selfie"
174
+ captureIndex = 0
175
+ urlString = url.absoluteString
176
+ case .livenessFrame(let index, let url):
177
+ kindString = "liveness"
178
+ captureIndex = index
179
+ urlString = url.absoluteString
180
+ case .documentFrame(let url):
181
+ kindString = "document"
182
+ captureIndex = 0
183
+ urlString = url.absoluteString
184
+ }
185
+ return [
186
+ "kind": kindString,
187
+ "captureIndex": captureIndex,
188
+ "url": urlString
189
+ ]
190
+ }
191
+ }
192
+
193
+ /// Map the native state-machine type into the wire-string the JS-side
194
+ /// `scanStateTypeSchema` enum accepts. Keep in lock-step with the
195
+ /// `ScanStateType` enum order so cross-platform analytics/dashboards
196
+ /// see consistent values.
197
+ extension UseSmileIDBridge.UseSmileIDScanState {
198
+ func toWireMap() -> [String: Any] {
199
+ let typeString: String
200
+ switch type {
201
+ case .initial: typeString = "initial"
202
+ case .found: typeString = "found"
203
+ case .satisfied: typeString = "satisfied"
204
+ case .unsatisfied: typeString = "unsatisfied"
205
+ case .finished: typeString = "finished"
206
+ case .timeout: typeString = "timeout"
207
+ case .headTurn: typeString = "headTurn"
208
+ }
209
+ var map: [String: Any] = ["type": typeString]
210
+ if let feedbackName {
211
+ map["feedback"] = feedbackName
212
+ }
213
+ if let unsatisfiedReason {
214
+ map["reason"] = unsatisfiedReason
215
+ }
216
+ if let captureProgress {
217
+ map["captureProgress"] = Double(captureProgress)
218
+ }
219
+ return map
220
+ }
221
+ }
@@ -0,0 +1,105 @@
1
+ import Foundation
2
+ import UIKit
3
+ import UseSmileIDBridge
4
+ import UseSmileIDVisionDocument
5
+
6
+ /// Process-wide holder for the active `UseSmileIDDocumentSession`.
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 UseSmileIDVisionDocumentSessionHolder {
12
+ static let shared = UseSmileIDVisionDocumentSessionHolder()
13
+
14
+ /// Single serial queue serialising every session operation.
15
+ private let operationQueue = DispatchQueue(
16
+ label: "com.usesmileid.vision.document.session.operations",
17
+ qos: .userInitiated
18
+ )
19
+
20
+ private var session: UseSmileIDDocumentSession?
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: UseSmileIDDocumentSession?) {
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.DocumentDetectorOutput
80
+ let state = scanResult.state
81
+ let events = currentSession.drainCaptureReadyEvents()
82
+ resultBox.value = [
83
+ "output": nativeOutput?.toWireMap() ?? (["documents": [], "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_document",
3
+ "version": "12.0.0-beta01",
4
+ "description": "UseSmileID v12 — Apple Vision document provider (iOS-only). Thin Expo Module shim around the existing Swift Package `UseSmileIDVisionDocument`.",
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_document"
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 withVisionDocumentSpm: ConfigPlugin;
3
+ export default withVisionDocumentSpm;
@@ -0,0 +1,107 @@
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 (`UseSmileIDVisionDocumentExpo`) compiles
8
+ // with `import UseSmileIDVisionDocument` / `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 = "UseSmileIDVisionDocumentExpo";
66
+ const SPM_PRODUCTS = ["UseSmileIDVisionDocument", "UseSmileIDBridge"];
67
+ const PODFILE_MARKER = "usesmileid_vision_document";
68
+ const withVisionDocumentSpm = (config) => {
69
+ // 1. Add the package + products to the host app's Xcode project so
70
+ // Xcode resolves the binary xcframeworks and embeds them in the
71
+ // .app at link time.
72
+ config = (0, config_plugins_1.withXcodeProject)(config, (config) => {
73
+ const appTarget = config.modRequest.projectName;
74
+ if (!appTarget) {
75
+ throw new Error("[usesmileid_vision_document] Unable to resolve host app native target name from Expo config.");
76
+ }
77
+ (0, spm_1.linkSpmPackage)(config.modResults, SMILE_ID_PACKAGE, {
78
+ targetName: appTarget,
79
+ products: [...SPM_PRODUCTS],
80
+ });
81
+ return config;
82
+ });
83
+ // 2. Inject a Ruby block into the Podfile's post_install hook that
84
+ // re-applies the same wiring to Pods.xcodeproj when CocoaPods
85
+ // materializes it. The bridge pod target needs the products
86
+ // visible at compile time; the host-app step alone isn't enough.
87
+ config = (0, config_plugins_1.withDangerousMod)(config, [
88
+ "ios",
89
+ (config) => {
90
+ const podfilePath = path.join(config.modRequest.platformProjectRoot, "Podfile");
91
+ const original = fs.readFileSync(podfilePath, "utf-8");
92
+ const next = (0, podfile_1.injectPodfileSpmBlock)(original, {
93
+ marker: PODFILE_MARKER,
94
+ package: SMILE_ID_PACKAGE,
95
+ podTarget: POD_TARGET,
96
+ products: [...SPM_PRODUCTS],
97
+ });
98
+ if (next !== original) {
99
+ fs.writeFileSync(podfilePath, next);
100
+ }
101
+ return config;
102
+ },
103
+ ]);
104
+ return config;
105
+ };
106
+ exports.default = withVisionDocumentSpm;
107
+ //# 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,wEAAwE;AACxE,2EAA2E;AAC3E,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,8BAA8B,CAAC;AAClD,MAAM,YAAY,GAAG,CAAC,0BAA0B,EAAE,kBAAkB,CAAU,CAAC;AAC/E,MAAM,cAAc,GAAG,4BAA4B,CAAC;AAEpD,MAAM,qBAAqB,GAAiB,CAAC,MAAM,EAAE,EAAE;IACrD,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,8FAA8F,CAC/F,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,qBAAqB,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;