capacitor-microblink 0.2.0 → 0.3.1

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.
@@ -1,332 +1,257 @@
1
1
  import Foundation
2
2
  import Capacitor
3
+ import Combine
4
+ import SwiftUI
5
+ import AVFoundation
3
6
  import BlinkCard
4
- import MicroblinkPlatform
7
+ import BlinkCardUX
5
8
 
6
9
  @objc(MicroblinkPlugin)
7
- public class MicroblinkPlugin: CAPPlugin, CAPBridgedPlugin, MicroblinkPlatformSDKDelegate, MBCBlinkCardOverlayViewControllerDelegate {
10
+ public class MicroblinkPlugin: CAPPlugin, CAPBridgedPlugin {
8
11
  public let identifier = "MicroblinkPlugin"
9
12
  public let jsName = "Microblink"
10
13
  public let pluginMethods: [CAPPluginMethod] = [
11
- CAPPluginMethod(name: "startVerification", returnType: CAPPluginReturnPromise),
12
- CAPPluginMethod(name: "scanCard", returnType: CAPPluginReturnPromise)
14
+ CAPPluginMethod(name: "initializeBlinkCard", returnType: CAPPluginReturnPromise),
15
+ CAPPluginMethod(name: "scanCard", returnType: CAPPluginReturnPromise),
16
+ CAPPluginMethod(name: "terminateBlinkCard", returnType: CAPPluginReturnPromise)
13
17
  ]
14
- private var pendingVerificationCallId: String?
15
- private var verificationSDK: MicroblinkPlatformSDK?
16
- private var pendingVerificationCardScanResult: [String: Any]?
17
18
 
18
- private var pendingBlinkCardCallId: String?
19
- private var blinkCardRecognizer: MBCBlinkCardRecognizer?
20
- private weak var blinkCardRunnerViewController: UIViewController?
19
+ private enum PluginError: LocalizedError {
20
+ case sdkNotInitialized
21
21
 
22
- @objc public func startVerification(_ call: CAPPluginCall) {
23
- if pendingVerificationCallId != nil || pendingBlinkCardCallId != nil {
24
- call.reject("A native flow is already running.")
25
- return
26
- }
27
- guard let workflowId = call.getString("workflowId"), !workflowId.isEmpty else {
28
- call.reject("Missing required field: workflowId.")
29
- return
30
- }
31
- guard let url = call.getString("url"), !url.isEmpty else {
32
- call.reject("Missing required field: url.")
33
- return
34
- }
35
- guard let userId = call.getString("userId"), !userId.isEmpty else {
36
- call.reject("Missing required field: userId.")
37
- return
38
- }
39
- guard let isProcessingStoringAllowed = call.getBool("isProcessingStoringAllowed") else {
40
- call.reject("Missing required field: isProcessingStoringAllowed.")
41
- return
42
- }
43
- guard let isTrainingAllowed = call.getBool("isTrainingAllowed") else {
44
- call.reject("Missing required field: isTrainingAllowed.")
45
- return
46
- }
47
- guard let vc = bridge?.viewController else {
48
- call.reject("No view controller available")
49
- return
22
+ var errorDescription: String? {
23
+ switch self {
24
+ case .sdkNotInitialized:
25
+ return "BlinkCard is not initialized. Call initializeBlinkCard first or provide licenseKey in scanCard."
26
+ }
50
27
  }
28
+ }
51
29
 
52
- let givenOnMs = call.getDouble("givenOn") ?? Date().timeIntervalSince1970 * 1000
53
- let givenOn = Date(timeIntervalSince1970: givenOnMs / 1000)
54
- let consent = MicroblinkPlatformConsent(
55
- userId: userId,
56
- isProcessingStoringAllowed: isProcessingStoringAllowed,
57
- isTrainingAllowed: isTrainingAllowed,
58
- isGivenOn: givenOn,
59
- note: call.getString("note")
60
- )
30
+ private var pendingBlinkCardCallId: String?
31
+ private weak var blinkCardHostingController: UIViewController?
32
+ private var blinkCardResultObserver: AnyCancellable?
61
33
 
62
- let serviceSettings = MicroblinkPlatformServiceSettings(
63
- workflowId: workflowId,
64
- url: url,
65
- consent: consent,
66
- additionalRequestHeaders: call.getObject("additionalRequestHeaders") as? [String: String]
67
- )
68
- if let startTransaction = call.getString("startTransactionPath"), !startTransaction.isEmpty {
69
- serviceSettings.startTransaction = startTransaction
70
- }
71
- if let cancelWorkflow = call.getString("cancelWorkflowPath"), !cancelWorkflow.isEmpty {
72
- serviceSettings.cancelWorkflow = cancelWorkflow
73
- }
74
- if let workflowInfo = call.getString("workflowInfoPath"), !workflowInfo.isEmpty {
75
- serviceSettings.getWorkflowInfo = workflowInfo
34
+ private var blinkCardSdk: BlinkCardSdk?
35
+ private var isBlinkCardInitialized = false
36
+
37
+ @objc public func initializeBlinkCard(_ call: CAPPluginCall) {
38
+ guard let licenseKey = call.getString("licenseKey"), !licenseKey.isEmpty else {
39
+ call.reject("Missing required field: licenseKey.")
40
+ return
76
41
  }
77
42
 
78
- pendingVerificationCardScanResult = nil
79
- bridge?.saveCall(call)
80
- pendingVerificationCallId = call.callbackId
81
- verificationSDK = MicroblinkPlatformSDK(serviceSettings: serviceSettings, delegate: self)
43
+ let licensee = normalized(call.getString("licensee"))
82
44
 
83
- let sdkViewController = verificationSDK?.startSDK()
84
- guard let sdkViewController else {
85
- clearPendingVerificationState()
86
- call.reject("Unable to start verification.")
87
- return
45
+ Task {
46
+ do {
47
+ let sdk = try await createBlinkCardSdk(licenseKey: licenseKey, licensee: licensee)
48
+ await MainActor.run {
49
+ self.blinkCardSdk = sdk
50
+ self.isBlinkCardInitialized = true
51
+ call.resolve(["initialized": true])
52
+ }
53
+ } catch {
54
+ await MainActor.run {
55
+ self.isBlinkCardInitialized = false
56
+ call.reject("Failed to initialize BlinkCard SDK: \(error.localizedDescription)")
57
+ }
58
+ }
88
59
  }
89
- vc.present(sdkViewController, animated: true)
90
60
  }
91
61
 
92
62
  @objc public func scanCard(_ call: CAPPluginCall) {
93
- if pendingVerificationCallId != nil || pendingBlinkCardCallId != nil {
94
- call.reject("A native flow is already running.")
95
- return
63
+ ensureCameraPermission(call) { [weak self] in
64
+ self?.performScanCard(call)
96
65
  }
97
- guard let licenseKey = call.getString("licenseKey"), !licenseKey.isEmpty else {
98
- call.reject("Missing required field: licenseKey.")
66
+ }
67
+
68
+ private func performScanCard(_ call: CAPPluginCall) {
69
+ if pendingBlinkCardCallId != nil {
70
+ call.reject("A BlinkCard flow is already running.")
99
71
  return
100
72
  }
101
- guard let vc = bridge?.viewController else {
73
+
74
+ guard let viewController = bridge?.viewController else {
102
75
  call.reject("No view controller available")
103
76
  return
104
77
  }
105
78
 
106
- bridge?.saveCall(call)
107
- pendingBlinkCardCallId = call.callbackId
79
+ let providedLicenseKey = normalized(call.getString("licenseKey"))
80
+ let providedLicensee = normalized(call.getString("licensee"))
108
81
 
109
- let licenseErrorHandler: MBCLicenseErrorBlock = { [weak self] licenseError in
110
- guard let self, let pendingBlinkCardCallId, let pendingCall = self.bridge?.savedCall(withID: pendingBlinkCardCallId) else {
111
- return
112
- }
113
- self.clearPendingBlinkCardState()
114
- pendingCall.reject("Failed to initialize BlinkCard license (\(licenseError.rawValue)).")
115
- self.bridge?.releaseCall(withID: pendingBlinkCardCallId)
116
- }
117
-
118
- if let licensee = call.getString("licensee"), !licensee.isEmpty {
119
- MBCMicroblinkSDK.shared().setLicenseKey(licenseKey, andLicensee: licensee, errorCallback: licenseErrorHandler)
120
- } else {
121
- MBCMicroblinkSDK.shared().setLicenseKey(licenseKey, errorCallback: licenseErrorHandler)
122
- }
82
+ Task {
83
+ do {
84
+ let sdk: BlinkCardSdk
85
+ if let providedLicenseKey {
86
+ sdk = try await createBlinkCardSdk(licenseKey: providedLicenseKey, licensee: providedLicensee)
87
+ await MainActor.run {
88
+ self.blinkCardSdk = sdk
89
+ self.isBlinkCardInitialized = true
90
+ }
91
+ } else {
92
+ guard let existingSdk = await MainActor.run(body: { self.blinkCardSdk }), self.isBlinkCardInitialized else {
93
+ throw PluginError.sdkNotInitialized
94
+ }
95
+ sdk = existingSdk
96
+ }
123
97
 
124
- let recognizer = MBCBlinkCardRecognizer()
125
- recognizer.extractOwner = call.getBool("extractOwner") ?? true
126
- recognizer.extractExpiryDate = call.getBool("extractExpiryDate") ?? true
127
- recognizer.extractCvv = call.getBool("extractCvv") ?? true
128
- recognizer.extractIban = call.getBool("extractIban") ?? true
129
- recognizer.allowInvalidCardNumber = call.getBool("allowInvalidCardNumber") ?? false
130
-
131
- let recognizerCollection = MBCRecognizerCollection(recognizers: [recognizer])
132
- let overlaySettings = MBCBlinkCardOverlaySettings()
133
- overlaySettings.enableEditScreen = call.getBool("enableEditScreen") ?? true
134
-
135
- let overlayViewController = MBCBlinkCardOverlayViewController(
136
- settings: overlaySettings,
137
- recognizerCollection: recognizerCollection,
138
- delegate: self
139
- )
140
- guard let runnerViewController = MBCViewControllerFactory.recognizerRunnerViewController(withOverlayViewController: overlayViewController) else {
141
- clearPendingBlinkCardState()
142
- bridge?.releaseCall(withID: call.callbackId)
143
- call.reject("Unable to create BlinkCard runner view controller.")
144
- return
98
+ try await startBlinkCardScan(call: call, sdk: sdk, presentingViewController: viewController)
99
+ } catch let error as PluginError {
100
+ await MainActor.run {
101
+ call.reject(error.localizedDescription)
102
+ }
103
+ } catch {
104
+ await MainActor.run {
105
+ call.reject("Failed to start BlinkCard scan: \(error.localizedDescription)")
106
+ }
107
+ }
145
108
  }
146
-
147
- blinkCardRecognizer = recognizer
148
- blinkCardRunnerViewController = runnerViewController
149
-
150
- vc.present(runnerViewController, animated: true)
151
109
  }
152
110
 
153
- public func microblinkPlatformSDKDidFinish(
154
- viewController: UIViewController,
155
- result: MicroblinkPlatformResult
156
- ) {
157
- var payload: [String: Any] = [
158
- "canceled": false,
159
- "transactionId": result.transactionId,
160
- "status": mapStatus(result.status)
161
- ]
162
- if let pendingVerificationCardScanResult {
163
- payload["cardScanResult"] = pendingVerificationCardScanResult
111
+ private func ensureCameraPermission(_ call: CAPPluginCall, onGranted: @escaping () -> Void) {
112
+ let status = AVCaptureDevice.authorizationStatus(for: .video)
113
+ switch status {
114
+ case .authorized:
115
+ onGranted()
116
+ case .notDetermined:
117
+ AVCaptureDevice.requestAccess(for: .video) { granted in
118
+ DispatchQueue.main.async {
119
+ if granted {
120
+ onGranted()
121
+ } else {
122
+ call.reject("Camera permission is required to scan cards.")
123
+ }
124
+ }
125
+ }
126
+ case .denied, .restricted:
127
+ call.reject("Camera permission is required to scan cards. Enable it in iOS Settings.")
128
+ @unknown default:
129
+ call.reject("Unable to determine camera permission status.")
164
130
  }
165
- finalize(viewController: viewController, payload: payload)
166
131
  }
167
132
 
168
- public func microblinkPlatformSDKDidClose(
169
- viewController: UIViewController,
170
- cancelState: MicroblinkPlatformCancelState
171
- ) {
172
- var payload: [String: Any] = [
173
- "canceled": true,
174
- "transactionId": cancelState.transactionId as Any,
175
- "cancelReason": mapCancelReason(cancelState.cancelReason)
176
- ]
177
- if let pendingVerificationCardScanResult {
178
- payload["cardScanResult"] = pendingVerificationCardScanResult
133
+ @objc public func terminateBlinkCard(_ call: CAPPluginCall) {
134
+ Task {
135
+ await BlinkCardSdk.terminateBlinkCardSdk()
136
+ await MainActor.run {
137
+ self.blinkCardSdk = nil
138
+ self.isBlinkCardInitialized = false
139
+ call.resolve(["terminated": true])
140
+ }
179
141
  }
180
- finalize(viewController: viewController, payload: payload)
181
142
  }
182
143
 
183
- public func microblinkPlatformSDKDidFinishCardScanStep(
184
- viewController: UIViewController,
185
- cardScanResult: MicroblinkPlatformResultCardScanResult
186
- ) {
187
- pendingVerificationCardScanResult = [
188
- "cardNumber": cardScanResult.cardNumber,
189
- "expiryDate": cardScanResult.expiryDate,
190
- "owner": cardScanResult.owner,
191
- "cvv": cardScanResult.cvv
192
- ]
193
- }
144
+ private func startBlinkCardScan(call: CAPPluginCall, sdk: BlinkCardSdk, presentingViewController: UIViewController) async throws {
145
+ let extractionSettings = ExtractionSettings(
146
+ extractIban: call.getBool("extractIban") ?? true,
147
+ extractExpiryDate: call.getBool("extractExpiryDate") ?? true,
148
+ extractCardholderName: call.getBool("extractOwner") ?? true,
149
+ extractCvv: call.getBool("extractCvv") ?? true,
150
+ extractInvalidCardNumber: call.getBool("allowInvalidCardNumber") ?? false
151
+ )
152
+ let scanningSettings = ScanningSettings(extractionSettings: extractionSettings)
153
+ let sessionSettings = BlinkCardSessionSettings(inputImageSource: .video, scanningSettings: scanningSettings)
194
154
 
195
- public func blinkCardOverlayViewControllerDidFinishScanning(
196
- _ blinkCardOverlayViewController: MBCBlinkCardOverlayViewController,
197
- state: MBCRecognizerResultState
198
- ) {
199
- let result = blinkCardRecognizer?.result
200
- var payload: [String: Any] = [
201
- "canceled": false,
202
- "resultState": mapBlinkCardResultState(state)
203
- ]
204
- if let result {
205
- payload["processingStatus"] = mapBlinkCardProcessingStatus(result.processingStatus)
206
- payload["cardNumber"] = result.cardNumber
207
- payload["cardNumberValid"] = result.cardNumberValid
208
- payload["cardNumberPrefix"] = result.cardNumberPrefix
209
- payload["owner"] = result.owner
210
- payload["cvv"] = result.cvv
211
- payload["iban"] = result.iban
212
- payload["expiryDate"] = mapBlinkCardDate(result.expiryDate)
213
- }
214
- finalizeBlinkCard(payload: payload)
215
- }
155
+ let analyzer = try await BlinkCardAnalyzer(
156
+ sdk: sdk,
157
+ blinkCardSessionSettings: sessionSettings,
158
+ eventStream: BlinkCardEventStream()
159
+ )
216
160
 
217
- public func blinkCardOverlayViewControllerDidTapClose(
218
- _ blinkCardOverlayViewController: MBCBlinkCardOverlayViewController
219
- ) {
220
- finalizeBlinkCard(payload: ["canceled": true])
221
- }
161
+ await MainActor.run {
162
+ let viewModel = BlinkCardUXModel(analyzer: analyzer)
163
+ let hostingController = UIHostingController(rootView: BlinkCardUXView(viewModel: viewModel))
164
+ hostingController.modalPresentationStyle = .fullScreen
222
165
 
223
- private func mapStatus(_ status: MicroblinkPlatformResultStatus) -> String {
224
- switch status {
225
- case .accept:
226
- return "accept"
227
- case .review:
228
- return "review"
229
- case .reject:
230
- return "reject"
231
- @unknown default:
232
- return "review"
233
- }
234
- }
166
+ self.bridge?.saveCall(call)
167
+ self.pendingBlinkCardCallId = call.callbackId
168
+ self.blinkCardHostingController = hostingController
235
169
 
236
- private func mapCancelReason(_ reason: MicroblinkPlatformCancelReason) -> String {
237
- switch reason {
238
- case .userCanceled:
239
- return "userCanceled"
240
- case .consentDenied:
241
- return "consentDenied"
242
- @unknown default:
243
- return "userCanceled"
170
+ self.blinkCardResultObserver = viewModel.$result
171
+ .dropFirst()
172
+ .sink { [weak self] resultState in
173
+ self?.handleBlinkCardResult(resultState)
174
+ }
175
+
176
+ presentingViewController.present(hostingController, animated: true)
244
177
  }
245
178
  }
246
179
 
247
- private func finalize(viewController: UIViewController, payload: [String: Any]) {
248
- viewController.dismiss(animated: true)
249
- guard let pendingVerificationCallId, let call = bridge?.savedCall(withID: pendingVerificationCallId) else {
250
- clearPendingVerificationState()
180
+ private func handleBlinkCardResult(_ resultState: BlinkCardResultState?) {
181
+ guard let scanningResult = resultState?.scanningResult else {
182
+ finalizeBlinkCard(payload: ["canceled": true])
251
183
  return
252
184
  }
253
- clearPendingVerificationState()
254
- call.resolve(payload)
255
- bridge?.releaseCall(withID: pendingVerificationCallId)
256
- }
257
185
 
258
- private func clearPendingVerificationState() {
259
- pendingVerificationCallId = nil
260
- pendingVerificationCardScanResult = nil
261
- verificationSDK = nil
262
- }
186
+ var payload: [String: Any] = [
187
+ "canceled": false,
188
+ "resultState": "valid",
189
+ "processingStatus": "success",
190
+ "owner": scanningResult.cardholderName as Any,
191
+ "iban": scanningResult.iban as Any
192
+ ]
263
193
 
264
- private func clearPendingBlinkCardState() {
265
- pendingBlinkCardCallId = nil
266
- blinkCardRecognizer = nil
267
- blinkCardRunnerViewController = nil
194
+ if let cardAccount = scanningResult.cardAccounts.first {
195
+ payload["cardNumber"] = cardAccount.cardNumber
196
+ payload["cardNumberValid"] = cardAccount.cardNumberValid
197
+ payload["cardNumberPrefix"] = cardAccount.cardNumberPrefix as Any
198
+ payload["cvv"] = cardAccount.cvv as Any
199
+ payload["expiryDate"] = mapBlinkCardDate(cardAccount.expiryDate) as Any
200
+ }
201
+
202
+ finalizeBlinkCard(payload: payload)
268
203
  }
269
204
 
270
205
  private func finalizeBlinkCard(payload: [String: Any]) {
271
206
  DispatchQueue.main.async {
272
207
  let pendingCallId = self.pendingBlinkCardCallId
273
- self.blinkCardRunnerViewController?.dismiss(animated: true)
208
+ self.blinkCardHostingController?.dismiss(animated: true)
209
+
274
210
  guard let pendingCallId, let call = self.bridge?.savedCall(withID: pendingCallId) else {
275
211
  self.clearPendingBlinkCardState()
276
212
  return
277
213
  }
214
+
278
215
  self.clearPendingBlinkCardState()
279
216
  call.resolve(payload)
280
217
  self.bridge?.releaseCall(withID: pendingCallId)
281
218
  }
282
219
  }
283
220
 
284
- private func mapBlinkCardResultState(_ state: MBCRecognizerResultState) -> String {
285
- switch state {
286
- case .empty:
287
- return "empty"
288
- case .uncertain:
289
- return "uncertain"
290
- case .valid:
291
- return "valid"
292
- case .stageValid:
293
- return "stageValid"
294
- @unknown default:
295
- return "empty"
296
- }
221
+ private func clearPendingBlinkCardState() {
222
+ pendingBlinkCardCallId = nil
223
+ blinkCardHostingController = nil
224
+ blinkCardResultObserver = nil
297
225
  }
298
226
 
299
- private func mapBlinkCardProcessingStatus(_ status: MBCBlinkCardProcessingStatus) -> String {
300
- switch status {
301
- case .success:
302
- return "success"
303
- case .detectionFailed:
304
- return "detectionFailed"
305
- case .imagePreprocessingFailed:
306
- return "imagePreprocessingFailed"
307
- case .stabilityTestFailed:
308
- return "stabilityTestFailed"
309
- case .scanningWrongSide:
310
- return "scanningWrongSide"
311
- case .fieldIdentificationFailed:
312
- return "fieldIdentificationFailed"
313
- case .imageReturnFailed:
314
- return "imageReturnFailed"
315
- case .unsupportedCard:
316
- return "unsupportedCard"
317
- @unknown default:
318
- return "unsupportedCard"
227
+ private func normalized(_ value: String?) -> String? {
228
+ guard let value, !value.isEmpty else {
229
+ return nil
319
230
  }
231
+ return value
320
232
  }
321
233
 
322
- private func mapBlinkCardDate(_ date: MBCDate?) -> [String: Int]? {
323
- guard let date else {
234
+ private func mapBlinkCardDate(_ date: DateResult?) -> [String: Int]? {
235
+ guard let date,
236
+ let day = date.day,
237
+ let month = date.month,
238
+ let year = date.year else {
324
239
  return nil
325
240
  }
326
241
  return [
327
- "day": Int(date.day),
328
- "month": Int(date.month),
329
- "year": Int(date.year)
242
+ "day": day,
243
+ "month": month,
244
+ "year": year
330
245
  ]
331
246
  }
247
+
248
+ private func createBlinkCardSdk(licenseKey: String, licensee: String?) async throws -> BlinkCardSdk {
249
+ await BlinkCardSdk.terminateBlinkCardSdk()
250
+ let settings = BlinkCardSdkSettings(
251
+ licenseKey: licenseKey,
252
+ licensee: licensee,
253
+ downloadResources: true
254
+ )
255
+ return try await BlinkCardSdk.createBlinkCardSdk(withSettings: settings)
256
+ }
332
257
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "capacitor-microblink",
3
- "version": "0.2.0",
3
+ "version": "0.3.1",
4
4
  "description": "Capacitor plugin for microblink",
5
5
  "main": "dist/plugin.cjs.js",
6
6
  "module": "dist/esm/index.js",
@@ -31,7 +31,7 @@
31
31
  ],
32
32
  "scripts": {
33
33
  "verify": "npm run verify:ios && npm run verify:android && npm run verify:web",
34
- "verify:ios": "xcodebuild -scheme CapacitorMicroblink -destination generic/platform=iOS",
34
+ "verify:ios": "xcodebuild -scheme CapacitorMicroblink -destination generic/platform=iOS -derivedDataPath /tmp/capacitor-microblink-deriveddata clean build",
35
35
  "verify:android": "cd android && ./gradlew clean build test && cd ..",
36
36
  "verify:web": "npm run build",
37
37
  "lint": "npm run eslint && npm run prettier -- --check && npm run swiftlint -- lint",