Skip to content

Commit

Permalink
Public release 1.16.3
Browse files Browse the repository at this point in the history
Public release 1.16.3
  • Loading branch information
dmytrokhl authored Aug 16, 2024
2 parents cfa8dbc + bf6d31c commit 0af809e
Show file tree
Hide file tree
Showing 18 changed files with 384 additions and 77 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
//
// VGSBlinkCardControllerRepresentable.swift
// VGSBlinkCardCollector
//

import Foundation
#if os(iOS)
import SwiftUI
import UIKit
#endif

#if canImport(BlinkCard)
import BlinkCard
#if !COCOAPODS
import VGSCollectSDK
#endif

/// UIViewControllerRepresentable responsible for managing `BlinkCard` scanner.
public struct VGSBlinkCardControllerRepresentable: UIViewControllerRepresentable {

public typealias UIViewControllerType = UIViewController

internal lazy var cardRecognizer: MBCBlinkCardRecognizer = {
return MBCBlinkCardRecognizer()
}()

// MARK: - Actions handlers
/// On Scaner finish card recognition.
public var onCardScanned: (() -> Void)?
/// On card scanning canceled by user.
public var onCardScanCanceled: (() -> Void)?

// MARK: - MBCBlinkCardRecognizer params
/// https://blinkcard.github.io/blinkcard-ios/Classes/MBCBlinkCardRecognizer.html
/// Should extract the card owner information.
public var extractOwner: Bool = true
/// Should extract the payment card’s month of expiry.
public var extractExpiryDate: Bool = true
/// Should extract CVV.
public var extractCvv: Bool = true
/// Should extract the payment card’s IBAN.
public var extractIban: Bool = true
/// Whether invalid card number is accepted.
public var allowInvalidCardNumber: Bool = false

/// Card scan data coordinators dict connects scanned data with VGSTextFields.
private var dataCoordinators = [VGSBlinkCardDataType: VGSCardScanCoordinator]()

// MARK: - Initialization

/// Initialization
/// - Parameters:
/// - licenseKey: key required for BlinkCard SDK usage.
/// - dataCoordinators: `[VGSBlinkCardDataType: VGSCardScanCoordinator]` represents connection between scanned data and VGSTextFields.
/// - errorCallback: Error callback with Int error code(represents `MBCLicenseError` enum), triggered only when error occured.
public init(licenseKey: String, dataCoordinators: [VGSBlinkCardDataType: VGSCardScanCoordinator], errorCallback: @escaping ((NSInteger) -> Void)) {
MBCMicroblinkSDK.shared().setLicenseKey(licenseKey) { error in
VGSAnalyticsClient.shared.trackEvent(.scan, status: .failed, extraData: ["scannerType": "BlinkCard", "errorCode": error])
errorCallback(error.rawValue)
}
self.dataCoordinators = dataCoordinators
}

public func makeCoordinator() -> Coordinator {
Coordinator(self)
}

public func makeUIViewController(context: Context) -> UIViewController {

// Create BlinkCard settings
let settings: MBCBlinkCardOverlaySettings = MBCBlinkCardOverlaySettings()
settings.enableEditScreen = false
// Set card recognizer
context.coordinator.parent.cardRecognizer.extractOwner = extractOwner
context.coordinator.parent.cardRecognizer.extractOwner = extractCvv
context.coordinator.parent.cardRecognizer.extractOwner = extractExpiryDate
context.coordinator.parent.cardRecognizer.extractOwner = extractIban
context.coordinator.parent.cardRecognizer.allowInvalidCardNumber = allowInvalidCardNumber
// Crate recognizer collection
let recognizerList = [context.coordinator.parent.cardRecognizer]
let recognizerCollection: MBCRecognizerCollection = MBCRecognizerCollection(recognizers: recognizerList)
// Create overlay view controller
let blinkCardOverlayViewController = MBCBlinkCardOverlayViewController(settings: settings, recognizerCollection: recognizerCollection, delegate: context.coordinator)
// Create recognizer view controller with wanted overlay view controller
let recognizerRunneViewController: UIViewController = MBCViewControllerFactory.recognizerRunnerViewController(withOverlayViewController: blinkCardOverlayViewController)!
// recognizerRunneViewController.modalPresentationStyle = modalPresentationStyle

return recognizerRunneViewController
}

public func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) {

}
// MARK: - Scan interaction events
/// On Scaner finish card recognition.
public func onCardScanned(_ action: (() -> Void)?) -> VGSBlinkCardControllerRepresentable {
var newRepresentable = self
newRepresentable.onCardScanned = action
return newRepresentable
}
/// On card scanning canceled by user.
public func onCardScanCanceled(_ action: (() -> Void)?) -> VGSBlinkCardControllerRepresentable {
var newRepresentable = self
newRepresentable.onCardScanCanceled = action
return newRepresentable
}
// MARK: - MBCBlinkCardRecognizer params setting
/// https://blinkcard.github.io/blinkcard-ios/Classes/MBCBlinkCardRecognizer.html
/// Set if should `extractOwner` value.
public func extractOwner(_ extract: Bool) -> VGSBlinkCardControllerRepresentable {
var newRepresentable = self
newRepresentable.extractOwner = extract
return newRepresentable
}
/// Set if should `extractExpiryDate` value.
public func extractExpiryDate(_ extract: Bool) -> VGSBlinkCardControllerRepresentable {
var newRepresentable = self
newRepresentable.extractExpiryDate = extract
return newRepresentable
}
/// Set if should `extractCvv` value.
public func extractCvv(_ extract: Bool) -> VGSBlinkCardControllerRepresentable {
var newRepresentable = self
newRepresentable.extractCvv = extract
return newRepresentable
}
/// Set if should `extractIban` value.
public func extractIban(_ extract: Bool) -> VGSBlinkCardControllerRepresentable {
var newRepresentable = self
newRepresentable.extractIban = extract
return newRepresentable
}
/// Set if should `allowInvalidCardNumber` value.
public func allowInvalidCardNumber(_ allow: Bool) -> VGSBlinkCardControllerRepresentable {
var newRepresentable = self
newRepresentable.allowInvalidCardNumber = allow
return newRepresentable
}

// MARK: - Coordinator
public class Coordinator: NSObject, MBCBlinkCardOverlayViewControllerDelegate {
var parent: VGSBlinkCardControllerRepresentable

init(_ parent: VGSBlinkCardControllerRepresentable) {
self.parent = parent
}

/// When BlinkCard finish card recognition.
public func blinkCardOverlayViewControllerDidFinishScanning(_ blinkCardOverlayViewController: MBCBlinkCardOverlayViewController, state: MBCRecognizerResultState) {
// pause scanning
blinkCardOverlayViewController.recognizerRunnerViewController?.pauseScanning()
// send results in main thread
DispatchQueue.main.async { [weak self] in
guard let strongSelf = self else {return}
// get scan result and pass to fields coordinator
let result = strongSelf.parent.cardRecognizer.result
let dataDict = VGSBlinkCardHandler.mapScanResult(result)

for (dataType, coordinator) in strongSelf.parent.dataCoordinators {
if let scanData = dataDict[dataType] {
coordinator.setText(scanData)
}
if dataType == .cardNumber {
coordinator.trackAnalyticsEvent(scannerType: "BlinkCard")
}
}
// notify scan is finished
strongSelf.parent.onCardScanned?()
}
}

/// When user tap close button.
public func blinkCardOverlayViewControllerDidTapClose(_ blinkCardOverlayViewController: MBCBlinkCardOverlayViewController) {
VGSAnalyticsClient.shared.trackEvent(.scan, status: .cancel, extraData: [ "scannerType": "BlinkCard"])
parent.onCardScanCanceled?()
}
}
}
#endif
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import UIKit

/// Supported scan data fields by BlinkCard.
@objc
public enum VGSBlinkCardDataType: Int {
public enum VGSBlinkCardDataType: Int, CaseIterable {

/// Credit Card Number. Digits string.
case cardNumber
Expand Down
81 changes: 38 additions & 43 deletions Sources/VGSBlinkCardCollector/VGSBlinkCardHandler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -80,50 +80,19 @@ extension VGSBlinkCardHandler: MBCBlinkCardOverlayViewControllerDelegate {
// get scan result and delegate
guard let result = self?.cardRecognizer.result,
let blinkCardDelegate = self?.delegate else { return }
// card number
let number = result.cardNumber.trimmingCharacters(in: .whitespacesAndNewlines)
if !number.isEmpty, let textfield = blinkCardDelegate.textFieldForScannedData(type: .cardNumber) {
if let form = textfield.configuration?.vgsCollector {
VGSAnalyticsClient.shared.trackFormEvent(form.formAnalyticsDetails, type: .scan, status: .success, extraData: [ "scannerType": "BlinkCard"])

let dataDict = Self.mapScanResult(result)

for dataType in VGSBlinkCardDataType.allCases {
if let textfield = blinkCardDelegate.textFieldForScannedData(type: dataType), let value = dataDict[dataType] {
textfield.setText(value)
/// analytics event, send once
if dataType == .cardNumber {
if let form = textfield.configuration?.vgsCollector {
VGSAnalyticsClient.shared.trackFormEvent(form.formAnalyticsDetails, type: .scan, status: .success, extraData: [ "scannerType": "BlinkCard"])
}
}
}
textfield.setText(number)
}
// card holder name
let name = result.owner
if !name.isEmpty, let textfield =
blinkCardDelegate.textFieldForScannedData(type: .name) {
textfield.setText(name)
}
// cvv
let cvv = result.cvv
if !cvv.isEmpty, let textfield =
blinkCardDelegate.textFieldForScannedData(type: .cvc) {
textfield.setText(cvv)
}
// exp.date
let month = result.expiryDate.month
let year = result.expiryDate.year
let date = VGSBlinkCardExpirationDate(month, year: year)
if let textField = blinkCardDelegate.textFieldForScannedData(type: .expirationDate) {
textField.setText(date.mapDefaultExpirationDate())
}
if let textField = blinkCardDelegate.textFieldForScannedData(type: .expirationDateLong) {
textField.setText(date.mapLongExpirationDate())
}
if let textField = blinkCardDelegate.textFieldForScannedData(type: .expirationDateShortYearThenMonth) {
textField.setText(date.mapExpirationDateWithShortYearFirst())
}
if let textField = blinkCardDelegate.textFieldForScannedData(type: .expirationDateLongYearThenMonth) {
textField.setText(date.mapLongExpirationDateWithLongYearFirst())
}
if let textField = blinkCardDelegate.textFieldForScannedData(type: .expirationYear) {
textField.setText(String(date.shortYear))
}
if let textField = blinkCardDelegate.textFieldForScannedData(type: .expirationYearLong) {
textField.setText(String(date.year))
}
if let textField = blinkCardDelegate.textFieldForScannedData(type: .expirationMonth) {
textField.setText(date.monthString)
}
// notify scan is finished
self?.delegate?.userDidFinishScan()
Expand All @@ -135,5 +104,31 @@ extension VGSBlinkCardHandler: MBCBlinkCardOverlayViewControllerDelegate {
VGSAnalyticsClient.shared.trackEvent(.scan, status: .cancel, extraData: [ "scannerType": "BlinkCard"])
delegate?.userDidCancelScan()
}

static func mapScanResult(_ result: MBCBlinkCardRecognizerResult) -> [VGSBlinkCardDataType: String] {
var data = [VGSBlinkCardDataType: String]()

let number = result.cardNumber.trimmingCharacters(in: .whitespacesAndNewlines)
if !number.isEmpty {data[.cardNumber] = number}

let name = result.owner
if !name.isEmpty {data[.name] = name}

let cvv = result.cvv
if !cvv.isEmpty {data[.cvc] = cvv}

let month = result.expiryDate.month
let year = result.expiryDate.year
let date = VGSBlinkCardExpirationDate(month, year: year)

data[.expirationDate] = date.mapDefaultExpirationDate()
data[.expirationDateLong] = date.mapLongExpirationDate()
data[.expirationDateShortYearThenMonth] = date.mapExpirationDateWithShortYearFirst()
data[.expirationDateLongYearThenMonth] = date.mapLongExpirationDateWithLongYearFirst()
data[.expirationYear] = String(date.shortYear)
data[.expirationYearLong] = String(date.year)
data[.expirationMonth] = (date.monthString)
return data
}
}
#endif
1 change: 1 addition & 0 deletions Sources/VGSCardIOCollector/VGSCardIOScanController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import UIKit
import AVFoundation.AVCaptureDevice

/// Controller responsible for managing Card.io scanner
@available(*, deprecated, message: "use VGSBlinkCardController or VGSBlinkCardControllerRepresentable instead")
public class VGSCardIOScanController {

// MARK: - Attributes
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,8 @@ public struct VGSCVCTextFieldRepresentable: UIViewRepresentable, VGSCVCTextField
var borderColor: UIColor?
/// Field border line width.
var bodrerWidth: CGFloat?

/// Coordinates connection between scan data and text field.
var cardScanCoordinator: VGSCardScanCoordinator?
// MARK: - Accessibility attributes
/// A succinct label in a localized string that identifies the accessibility text field.
var textFieldAccessibilityLabel: String?
Expand Down Expand Up @@ -97,7 +98,7 @@ public struct VGSCVCTextFieldRepresentable: UIViewRepresentable, VGSCVCTextField
if let lineWidth = bodrerWidth {vgsTextField.borderWidth = lineWidth}
if !attributedPlaceholder.isNilOrEmpty { vgsTextField.attributedPlaceholder = attributedPlaceholder }
if !placeholder.isNilOrEmpty { vgsTextField.placeholder = placeholder}

cardScanCoordinator?.registerTextField(vgsTextField)
vgsTextField.statePublisher
.receive(on: DispatchQueue.main)
.sink { newState in
Expand Down Expand Up @@ -188,6 +189,12 @@ public struct VGSCVCTextFieldRepresentable: UIViewRepresentable, VGSCVCTextField
newRepresentable.bodrerWidth = lineWidth
return newRepresentable
}
/// Coordinates connection between scan data and text field.
public func cardScanCoordinator(_ coordinator: VGSCardScanCoordinator) -> VGSCVCTextFieldRepresentable {
var newRepresentable = self
newRepresentable.cardScanCoordinator = coordinator
return newRepresentable
}
// MARK: - VGSCVCTextField specific methods
/// Set size of CVC icon.
public func cvcIconSize(_ size: CGSize) -> VGSCVCTextFieldRepresentable {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
//
// VGSCardScanCoordinator.swift
// VGSCollectSDK
//

import Foundation
import SwiftUI

@available(iOS 13.0, *)
public class VGSCardScanCoordinator: ObservableObject {
private weak var textField: VGSTextField?

public init() {}

// Register the UITextField with the coordinator
internal func registerTextField(_ textField: VGSTextField) {
self.textField = textField
}

// Set text for the text field
internal func setText(_ text: String) {
textField?.setText(text)
}
}
@available(iOS 13.0, *)
internal extension VGSCardScanCoordinator {
func trackAnalyticsEvent(scannerType: String) {
if let form = textField?.configuration?.vgsCollector {
VGSAnalyticsClient.shared.trackFormEvent(form.formAnalyticsDetails, type: .scan, status: .success, extraData: [ "scannerType": scannerType])
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,7 @@ import SwiftUI
import Combine

@available(iOS 14.0, *)
public struct VGSCardTextFieldRepresentable: UIViewRepresentable, VGSCardTextFieldRepresentableProtocol, VGSCardTextFieldEditingRepresentableProtocol {

public struct VGSCardTextFieldRepresentable: UIViewRepresentable, VGSCardTextFieldRepresentableProtocol, VGSCardTextFieldEditingRepresentableProtocol {
/// A class responsible for configuration VGSCardTextFieldRepresentable.
var configuration: VGSConfiguration
/// `VGSCardTextFieldRepresentable` text font.
Expand Down Expand Up @@ -42,7 +41,8 @@ public struct VGSCardTextFieldRepresentable: UIViewRepresentable, VGSCardTextFie
var borderColor: UIColor?
/// Field border line width.
var bodrerWidth: CGFloat?

/// Coordinates connection between scan data and text field.
var cardScanCoordinator: VGSCardScanCoordinator?
// MARK: - Accessibility attributes
/// A succinct label in a localized string that identifies the accessibility text field.
var textFieldAccessibilityLabel: String?
Expand Down Expand Up @@ -96,7 +96,7 @@ public struct VGSCardTextFieldRepresentable: UIViewRepresentable, VGSCardTextFie
if let lineWidth = bodrerWidth {vgsTextField.borderWidth = lineWidth}
if !attributedPlaceholder.isNilOrEmpty { vgsTextField.attributedPlaceholder = attributedPlaceholder }
if !placeholder.isNilOrEmpty { vgsTextField.placeholder = placeholder}

cardScanCoordinator?.registerTextField(vgsTextField)
vgsTextField.statePublisher
.receive(on: DispatchQueue.main)
.compactMap { state -> VGSCardState? in
Expand Down Expand Up @@ -193,6 +193,13 @@ public struct VGSCardTextFieldRepresentable: UIViewRepresentable, VGSCardTextFie
return newRepresentable
}

/// Coordinates connection between scan data and text field.
public func cardScanCoordinator(_ coordinator: VGSCardScanCoordinator) -> VGSCardTextFieldRepresentable {
var newRepresentable = self
newRepresentable.cardScanCoordinator = coordinator
return newRepresentable
}

// MARK: - VGSCardTextField specific methods
/// Set `size` of card icon.
public func cardIconSize(_ size: CGSize) -> VGSCardTextFieldRepresentable {
Expand Down
Loading

0 comments on commit 0af809e

Please sign in to comment.