capacitor-microblink 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,332 +1,216 @@
1
1
  import Foundation
2
2
  import Capacitor
3
+ import Combine
4
+ import SwiftUI
3
5
  import BlinkCard
4
- import MicroblinkPlatform
6
+ import BlinkCardUX
5
7
 
6
8
  @objc(MicroblinkPlugin)
7
- public class MicroblinkPlugin: CAPPlugin, CAPBridgedPlugin, MicroblinkPlatformSDKDelegate, MBCBlinkCardOverlayViewControllerDelegate {
9
+ public class MicroblinkPlugin: CAPPlugin, CAPBridgedPlugin {
8
10
  public let identifier = "MicroblinkPlugin"
9
11
  public let jsName = "Microblink"
10
12
  public let pluginMethods: [CAPPluginMethod] = [
11
- CAPPluginMethod(name: "startVerification", returnType: CAPPluginReturnPromise),
13
+ CAPPluginMethod(name: "initializeBlinkCard", returnType: CAPPluginReturnPromise),
12
14
  CAPPluginMethod(name: "scanCard", returnType: CAPPluginReturnPromise)
13
15
  ]
14
- private var pendingVerificationCallId: String?
15
- private var verificationSDK: MicroblinkPlatformSDK?
16
- private var pendingVerificationCardScanResult: [String: Any]?
17
16
 
18
- private var pendingBlinkCardCallId: String?
19
- private var blinkCardRecognizer: MBCBlinkCardRecognizer?
20
- private weak var blinkCardRunnerViewController: UIViewController?
17
+ private enum PluginError: LocalizedError {
18
+ case sdkNotInitialized
21
19
 
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
20
+ var errorDescription: String? {
21
+ switch self {
22
+ case .sdkNotInitialized:
23
+ return "BlinkCard is not initialized. Call initializeBlinkCard first or provide licenseKey in scanCard."
24
+ }
50
25
  }
26
+ }
51
27
 
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
- )
61
-
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
76
- }
28
+ private var pendingBlinkCardCallId: String?
29
+ private weak var blinkCardHostingController: UIViewController?
30
+ private var blinkCardResultObserver: AnyCancellable?
77
31
 
78
- pendingVerificationCardScanResult = nil
79
- bridge?.saveCall(call)
80
- pendingVerificationCallId = call.callbackId
81
- verificationSDK = MicroblinkPlatformSDK(serviceSettings: serviceSettings, delegate: self)
32
+ private var blinkCardSdk: BlinkCardSdk?
33
+ private var isBlinkCardInitialized = false
82
34
 
83
- let sdkViewController = verificationSDK?.startSDK()
84
- guard let sdkViewController else {
85
- clearPendingVerificationState()
86
- call.reject("Unable to start verification.")
35
+ @objc public func initializeBlinkCard(_ call: CAPPluginCall) {
36
+ guard let licenseKey = call.getString("licenseKey"), !licenseKey.isEmpty else {
37
+ call.reject("Missing required field: licenseKey.")
87
38
  return
88
39
  }
89
- vc.present(sdkViewController, animated: true)
40
+
41
+ let licensee = normalized(call.getString("licensee"))
42
+
43
+ Task {
44
+ do {
45
+ let sdk = try await createBlinkCardSdk(licenseKey: licenseKey, licensee: licensee)
46
+ await MainActor.run {
47
+ self.blinkCardSdk = sdk
48
+ self.isBlinkCardInitialized = true
49
+ call.resolve(["initialized": true])
50
+ }
51
+ } catch {
52
+ await MainActor.run {
53
+ self.isBlinkCardInitialized = false
54
+ call.reject("Failed to initialize BlinkCard SDK: \(error.localizedDescription)")
55
+ }
56
+ }
57
+ }
90
58
  }
91
59
 
92
60
  @objc public func scanCard(_ call: CAPPluginCall) {
93
- if pendingVerificationCallId != nil || pendingBlinkCardCallId != nil {
94
- call.reject("A native flow is already running.")
61
+ if pendingBlinkCardCallId != nil {
62
+ call.reject("A BlinkCard flow is already running.")
95
63
  return
96
64
  }
97
- guard let licenseKey = call.getString("licenseKey"), !licenseKey.isEmpty else {
98
- call.reject("Missing required field: licenseKey.")
99
- return
100
- }
101
- guard let vc = bridge?.viewController else {
65
+
66
+ guard let viewController = bridge?.viewController else {
102
67
  call.reject("No view controller available")
103
68
  return
104
69
  }
105
70
 
106
- bridge?.saveCall(call)
107
- pendingBlinkCardCallId = call.callbackId
108
-
109
- let licenseErrorHandler: MBCLicenseErrorBlock = { [weak self] licenseError in
110
- guard let self, let pendingBlinkCardCallId, let pendingCall = self.bridge?.savedCall(withID: pendingBlinkCardCallId) else {
111
- return
71
+ let providedLicenseKey = normalized(call.getString("licenseKey"))
72
+ let providedLicensee = normalized(call.getString("licensee"))
73
+
74
+ Task {
75
+ do {
76
+ let sdk: BlinkCardSdk
77
+ if let providedLicenseKey {
78
+ sdk = try await createBlinkCardSdk(licenseKey: providedLicenseKey, licensee: providedLicensee)
79
+ await MainActor.run {
80
+ self.blinkCardSdk = sdk
81
+ self.isBlinkCardInitialized = true
82
+ }
83
+ } else {
84
+ guard let existingSdk = await MainActor.run(body: { self.blinkCardSdk }), self.isBlinkCardInitialized else {
85
+ throw PluginError.sdkNotInitialized
86
+ }
87
+ sdk = existingSdk
88
+ }
89
+
90
+ try await startBlinkCardScan(call: call, sdk: sdk, presentingViewController: viewController)
91
+ } catch let error as PluginError {
92
+ await MainActor.run {
93
+ call.reject(error.localizedDescription)
94
+ }
95
+ } catch {
96
+ await MainActor.run {
97
+ call.reject("Failed to start BlinkCard scan: \(error.localizedDescription)")
98
+ }
112
99
  }
113
- self.clearPendingBlinkCardState()
114
- pendingCall.reject("Failed to initialize BlinkCard license (\(licenseError.rawValue)).")
115
- self.bridge?.releaseCall(withID: pendingBlinkCardCallId)
116
100
  }
101
+ }
117
102
 
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
- }
123
-
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
103
+ private func startBlinkCardScan(call: CAPPluginCall, sdk: BlinkCardSdk, presentingViewController: UIViewController) async throws {
104
+ let extractionSettings = ExtractionSettings(
105
+ extractIban: call.getBool("extractIban") ?? true,
106
+ extractExpiryDate: call.getBool("extractExpiryDate") ?? true,
107
+ extractCardholderName: call.getBool("extractOwner") ?? true,
108
+ extractCvv: call.getBool("extractCvv") ?? true,
109
+ extractInvalidCardNumber: call.getBool("allowInvalidCardNumber") ?? false
110
+ )
111
+ let scanningSettings = ScanningSettings(extractionSettings: extractionSettings)
112
+ let sessionSettings = BlinkCardSessionSettings(inputImageSource: .video, scanningSettings: scanningSettings)
134
113
 
135
- let overlayViewController = MBCBlinkCardOverlayViewController(
136
- settings: overlaySettings,
137
- recognizerCollection: recognizerCollection,
138
- delegate: self
114
+ let analyzer = try await BlinkCardAnalyzer(
115
+ sdk: sdk,
116
+ blinkCardSessionSettings: sessionSettings,
117
+ eventStream: BlinkCardEventStream()
139
118
  )
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
145
- }
146
119
 
147
- blinkCardRecognizer = recognizer
148
- blinkCardRunnerViewController = runnerViewController
120
+ await MainActor.run {
121
+ let viewModel = BlinkCardUXModel(analyzer: analyzer)
122
+ let hostingController = UIHostingController(rootView: BlinkCardUXView(viewModel: viewModel))
123
+ hostingController.modalPresentationStyle = .fullScreen
149
124
 
150
- vc.present(runnerViewController, animated: true)
151
- }
125
+ self.bridge?.saveCall(call)
126
+ self.pendingBlinkCardCallId = call.callbackId
127
+ self.blinkCardHostingController = hostingController
152
128
 
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
164
- }
165
- finalize(viewController: viewController, payload: payload)
166
- }
129
+ self.blinkCardResultObserver = viewModel.$result
130
+ .dropFirst()
131
+ .sink { [weak self] resultState in
132
+ self?.handleBlinkCardResult(resultState)
133
+ }
167
134
 
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
135
+ presentingViewController.present(hostingController, animated: true)
179
136
  }
180
- finalize(viewController: viewController, payload: payload)
181
137
  }
182
138
 
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
- }
139
+ private func handleBlinkCardResult(_ resultState: BlinkCardResultState?) {
140
+ guard let scanningResult = resultState?.scanningResult else {
141
+ finalizeBlinkCard(payload: ["canceled": true])
142
+ return
143
+ }
194
144
 
195
- public func blinkCardOverlayViewControllerDidFinishScanning(
196
- _ blinkCardOverlayViewController: MBCBlinkCardOverlayViewController,
197
- state: MBCRecognizerResultState
198
- ) {
199
- let result = blinkCardRecognizer?.result
200
145
  var payload: [String: Any] = [
201
146
  "canceled": false,
202
- "resultState": mapBlinkCardResultState(state)
147
+ "resultState": "valid",
148
+ "processingStatus": "success",
149
+ "owner": scanningResult.cardholderName as Any,
150
+ "iban": scanningResult.iban as Any
203
151
  ]
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
- }
216
-
217
- public func blinkCardOverlayViewControllerDidTapClose(
218
- _ blinkCardOverlayViewController: MBCBlinkCardOverlayViewController
219
- ) {
220
- finalizeBlinkCard(payload: ["canceled": true])
221
- }
222
152
 
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"
153
+ if let cardAccount = scanningResult.cardAccounts.first {
154
+ payload["cardNumber"] = cardAccount.cardNumber
155
+ payload["cardNumberValid"] = cardAccount.cardNumberValid
156
+ payload["cardNumberPrefix"] = cardAccount.cardNumberPrefix as Any
157
+ payload["cvv"] = cardAccount.cvv as Any
158
+ payload["expiryDate"] = mapBlinkCardDate(cardAccount.expiryDate) as Any
233
159
  }
234
- }
235
160
 
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"
244
- }
245
- }
246
-
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()
251
- return
252
- }
253
- clearPendingVerificationState()
254
- call.resolve(payload)
255
- bridge?.releaseCall(withID: pendingVerificationCallId)
256
- }
257
-
258
- private func clearPendingVerificationState() {
259
- pendingVerificationCallId = nil
260
- pendingVerificationCardScanResult = nil
261
- verificationSDK = nil
262
- }
263
-
264
- private func clearPendingBlinkCardState() {
265
- pendingBlinkCardCallId = nil
266
- blinkCardRecognizer = nil
267
- blinkCardRunnerViewController = nil
161
+ finalizeBlinkCard(payload: payload)
268
162
  }
269
163
 
270
164
  private func finalizeBlinkCard(payload: [String: Any]) {
271
165
  DispatchQueue.main.async {
272
166
  let pendingCallId = self.pendingBlinkCardCallId
273
- self.blinkCardRunnerViewController?.dismiss(animated: true)
167
+ self.blinkCardHostingController?.dismiss(animated: true)
168
+
274
169
  guard let pendingCallId, let call = self.bridge?.savedCall(withID: pendingCallId) else {
275
170
  self.clearPendingBlinkCardState()
276
171
  return
277
172
  }
173
+
278
174
  self.clearPendingBlinkCardState()
279
175
  call.resolve(payload)
280
176
  self.bridge?.releaseCall(withID: pendingCallId)
281
177
  }
282
178
  }
283
179
 
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
- }
180
+ private func clearPendingBlinkCardState() {
181
+ pendingBlinkCardCallId = nil
182
+ blinkCardHostingController = nil
183
+ blinkCardResultObserver = nil
297
184
  }
298
185
 
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"
186
+ private func normalized(_ value: String?) -> String? {
187
+ guard let value, !value.isEmpty else {
188
+ return nil
319
189
  }
190
+ return value
320
191
  }
321
192
 
322
- private func mapBlinkCardDate(_ date: MBCDate?) -> [String: Int]? {
323
- guard let date else {
193
+ private func mapBlinkCardDate(_ date: DateResult?) -> [String: Int]? {
194
+ guard let date,
195
+ let day = date.day,
196
+ let month = date.month,
197
+ let year = date.year else {
324
198
  return nil
325
199
  }
326
200
  return [
327
- "day": Int(date.day),
328
- "month": Int(date.month),
329
- "year": Int(date.year)
201
+ "day": day,
202
+ "month": month,
203
+ "year": year
330
204
  ]
331
205
  }
206
+
207
+ private func createBlinkCardSdk(licenseKey: String, licensee: String?) async throws -> BlinkCardSdk {
208
+ await BlinkCardSdk.terminateBlinkCardSdk()
209
+ let settings = BlinkCardSdkSettings(
210
+ licenseKey: licenseKey,
211
+ licensee: licensee,
212
+ downloadResources: true
213
+ )
214
+ return try await BlinkCardSdk.createBlinkCardSdk(withSettings: settings)
215
+ }
332
216
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "capacitor-microblink",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
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",