From 185028fc4863a43eaf91690c7ba7689be83c1e41 Mon Sep 17 00:00:00 2001 From: Dmytro Khludkov Date: Thu, 15 Aug 2024 17:16:11 +0300 Subject: [PATCH 1/4] Add BlinkCard support in SwiftUI. --- .../VGSBlinkCardControllerRepresentable.swift | 179 ++++++++++++++++++ .../VGSBlinkCardControllerDelegate.swift | 2 +- .../VGSBlinkCardHandler.swift | 81 ++++---- .../VGSCVCTextFieldRepresentable.swift | 11 +- .../VGSCardScanCoordinator.swift | 32 ++++ .../VGSCardTextFieldRepresentable.swift | 15 +- .../VGSDateTextFieldRepresentable.swift | 11 +- .../VGSExpDateTextFieldRepresentable.swift | 10 +- .../VGSTextFieldRepresentable.swift | 15 +- .../VGSTextFieldRepresentableProtocol.swift | 3 + VGSCollectSDK.xcodeproj/project.pbxproj | 16 ++ 11 files changed, 319 insertions(+), 56 deletions(-) create mode 100644 Sources/VGSBlinkCardCollector/SwiftUI/VGSBlinkCardControllerRepresentable.swift create mode 100644 Sources/VGSCollectSDK/UIElements/Text Field/SwiftUIElements/VGSCardScanCoordinator.swift diff --git a/Sources/VGSBlinkCardCollector/SwiftUI/VGSBlinkCardControllerRepresentable.swift b/Sources/VGSBlinkCardCollector/SwiftUI/VGSBlinkCardControllerRepresentable.swift new file mode 100644 index 00000000..70aa09ce --- /dev/null +++ b/Sources/VGSBlinkCardCollector/SwiftUI/VGSBlinkCardControllerRepresentable.swift @@ -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 diff --git a/Sources/VGSBlinkCardCollector/VGSBlinkCardControllerDelegate.swift b/Sources/VGSBlinkCardCollector/VGSBlinkCardControllerDelegate.swift index 39e2f50e..029e5f18 100644 --- a/Sources/VGSBlinkCardCollector/VGSBlinkCardControllerDelegate.swift +++ b/Sources/VGSBlinkCardCollector/VGSBlinkCardControllerDelegate.swift @@ -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 diff --git a/Sources/VGSBlinkCardCollector/VGSBlinkCardHandler.swift b/Sources/VGSBlinkCardCollector/VGSBlinkCardHandler.swift index f1cd2ef1..dee76c51 100644 --- a/Sources/VGSBlinkCardCollector/VGSBlinkCardHandler.swift +++ b/Sources/VGSBlinkCardCollector/VGSBlinkCardHandler.swift @@ -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() @@ -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 diff --git a/Sources/VGSCollectSDK/UIElements/Text Field/SwiftUIElements/VGSCVCTextFieldRepresentable.swift b/Sources/VGSCollectSDK/UIElements/Text Field/SwiftUIElements/VGSCVCTextFieldRepresentable.swift index 5317d9a9..848c66d7 100644 --- a/Sources/VGSCollectSDK/UIElements/Text Field/SwiftUIElements/VGSCVCTextFieldRepresentable.swift +++ b/Sources/VGSCollectSDK/UIElements/Text Field/SwiftUIElements/VGSCVCTextFieldRepresentable.swift @@ -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? @@ -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 @@ -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 { diff --git a/Sources/VGSCollectSDK/UIElements/Text Field/SwiftUIElements/VGSCardScanCoordinator.swift b/Sources/VGSCollectSDK/UIElements/Text Field/SwiftUIElements/VGSCardScanCoordinator.swift new file mode 100644 index 00000000..7a163a3e --- /dev/null +++ b/Sources/VGSCollectSDK/UIElements/Text Field/SwiftUIElements/VGSCardScanCoordinator.swift @@ -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]) + } + } +} diff --git a/Sources/VGSCollectSDK/UIElements/Text Field/SwiftUIElements/VGSCardTextFieldRepresentable.swift b/Sources/VGSCollectSDK/UIElements/Text Field/SwiftUIElements/VGSCardTextFieldRepresentable.swift index 60357b76..f3a2e53c 100644 --- a/Sources/VGSCollectSDK/UIElements/Text Field/SwiftUIElements/VGSCardTextFieldRepresentable.swift +++ b/Sources/VGSCollectSDK/UIElements/Text Field/SwiftUIElements/VGSCardTextFieldRepresentable.swift @@ -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. @@ -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? @@ -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 @@ -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 { diff --git a/Sources/VGSCollectSDK/UIElements/Text Field/SwiftUIElements/VGSDateTextFieldRepresentable.swift b/Sources/VGSCollectSDK/UIElements/Text Field/SwiftUIElements/VGSDateTextFieldRepresentable.swift index d4f236cb..3cfcf1a4 100644 --- a/Sources/VGSCollectSDK/UIElements/Text Field/SwiftUIElements/VGSDateTextFieldRepresentable.swift +++ b/Sources/VGSCollectSDK/UIElements/Text Field/SwiftUIElements/VGSDateTextFieldRepresentable.swift @@ -40,7 +40,8 @@ public struct VGSDateTextFieldRepresentable: UIViewRepresentable, VGSDateTextFie 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? @@ -90,7 +91,7 @@ public struct VGSDateTextFieldRepresentable: UIViewRepresentable, VGSDateTextFie 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 @@ -181,6 +182,12 @@ public struct VGSDateTextFieldRepresentable: UIViewRepresentable, VGSDateTextFie newRepresentable.bodrerWidth = lineWidth return newRepresentable } + /// Coordinates connection between scan data and text field. + public func cardScanCoordinator(_ coordinator: VGSCardScanCoordinator) -> VGSDateTextFieldRepresentable { + var newRepresentable = self + newRepresentable.cardScanCoordinator = coordinator + return newRepresentable + } // MARK: - VGSDateTextField specific methods /// Set `VGSDateTextField.MonthFormat` UIPicker format. Default is `.shortSymbols`. public func monthPickerFormat(_ format: VGSDateTextField.MonthFormat) -> VGSDateTextFieldRepresentable { diff --git a/Sources/VGSCollectSDK/UIElements/Text Field/SwiftUIElements/VGSExpDateTextFieldRepresentable.swift b/Sources/VGSCollectSDK/UIElements/Text Field/SwiftUIElements/VGSExpDateTextFieldRepresentable.swift index 78646dd1..35edd737 100644 --- a/Sources/VGSCollectSDK/UIElements/Text Field/SwiftUIElements/VGSExpDateTextFieldRepresentable.swift +++ b/Sources/VGSCollectSDK/UIElements/Text Field/SwiftUIElements/VGSExpDateTextFieldRepresentable.swift @@ -41,6 +41,8 @@ public struct VGSExpDateTextFieldRepresentable: UIViewRepresentable, VGSExpDateT 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. @@ -94,7 +96,7 @@ public struct VGSExpDateTextFieldRepresentable: UIViewRepresentable, VGSExpDateT 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 @@ -185,6 +187,12 @@ public struct VGSExpDateTextFieldRepresentable: UIViewRepresentable, VGSExpDateT newRepresentable.bodrerWidth = lineWidth return newRepresentable } + /// Coordinates connection between scan data and text field. + public func cardScanCoordinator(_ coordinator: VGSCardScanCoordinator) -> VGSExpDateTextFieldRepresentable { + var newRepresentable = self + newRepresentable.cardScanCoordinator = coordinator + return newRepresentable + } // MARK: - VGSExpDateTextField specific methods /// Set `VGSExpDateTextField.MonthFormat` UIPicker format. Default is `.longSymbols`. public func monthPickerFormat(_ format: VGSExpDateTextField.MonthFormat) -> VGSExpDateTextFieldRepresentable { diff --git a/Sources/VGSCollectSDK/UIElements/Text Field/SwiftUIElements/VGSTextFieldRepresentable.swift b/Sources/VGSCollectSDK/UIElements/Text Field/SwiftUIElements/VGSTextFieldRepresentable.swift index a884ad50..b9165c16 100644 --- a/Sources/VGSCollectSDK/UIElements/Text Field/SwiftUIElements/VGSTextFieldRepresentable.swift +++ b/Sources/VGSCollectSDK/UIElements/Text Field/SwiftUIElements/VGSTextFieldRepresentable.swift @@ -8,7 +8,7 @@ import Combine @available(iOS 14.0, *) public struct VGSTextFieldRepresentable: UIViewRepresentable, VGSTextFieldRepresentableProtocol, VGSTextFieldEditingRepresentableProtocol { - /// A class responsible for configuration VGSTextFieldRepresentable. + /// A class responsible for configuration VGSTextFieldRepresentable. var configuration: VGSConfiguration /// `VGSTextFieldRepresentable` text font. var font: UIFont? @@ -41,6 +41,8 @@ public struct VGSTextFieldRepresentable: UIViewRepresentable, VGSTextFieldRepres 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. @@ -58,7 +60,7 @@ public struct VGSTextFieldRepresentable: UIViewRepresentable, VGSTextFieldRepres /// Returns new `VGSTextFieldState` object on change. public var onStateChange: ((VGSTextFieldState) -> Void)? - + // MARK: - Initialization /// Initialization @@ -89,7 +91,7 @@ public struct VGSTextFieldRepresentable: UIViewRepresentable, VGSTextFieldRepres 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 @@ -206,6 +208,13 @@ public struct VGSTextFieldRepresentable: UIViewRepresentable, VGSTextFieldRepres newRepresentable.onStateChange = action return newRepresentable } + + /// Coordinates connection between scan data and text field. + public func cardScanCoordinator(_ coordinator: VGSCardScanCoordinator) -> VGSTextFieldRepresentable { + var newRepresentable = self + newRepresentable.cardScanCoordinator = coordinator + return newRepresentable + } public class Coordinator: NSObject, VGSTextFieldDelegate { var parent: VGSTextFieldRepresentable diff --git a/Sources/VGSCollectSDK/UIElements/Text Field/SwiftUIElements/VGSTextFieldRepresentableProtocol.swift b/Sources/VGSCollectSDK/UIElements/Text Field/SwiftUIElements/VGSTextFieldRepresentableProtocol.swift index 06301f75..d1202255 100644 --- a/Sources/VGSCollectSDK/UIElements/Text Field/SwiftUIElements/VGSTextFieldRepresentableProtocol.swift +++ b/Sources/VGSCollectSDK/UIElements/Text Field/SwiftUIElements/VGSTextFieldRepresentableProtocol.swift @@ -40,6 +40,9 @@ internal protocol VGSTextFieldRepresentableProtocol { // MARK: - Accessibility attributes /// A succinct label in a localized string that identifies the accessibility text field. var textFieldAccessibilityLabel: String? {get set} + // MARK: - Card Scan integration + @available(iOS 13.0, *) + var cardScanCoordinator: VGSCardScanCoordinator? {get set} } /// A base set of optional methods to manage editing text in a text field object. diff --git a/VGSCollectSDK.xcodeproj/project.pbxproj b/VGSCollectSDK.xcodeproj/project.pbxproj index 7a463fc3..1a6e182c 100644 --- a/VGSCollectSDK.xcodeproj/project.pbxproj +++ b/VGSCollectSDK.xcodeproj/project.pbxproj @@ -10,6 +10,7 @@ 0306C3AB2B4EE0B20099E45D /* VGSTextFieldRepresentable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0306C3AA2B4EE0B20099E45D /* VGSTextFieldRepresentable.swift */; }; 0306C3AF2B5AF05D0099E45D /* VGSCardTextFieldRepresentable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0306C3AE2B5AF05D0099E45D /* VGSCardTextFieldRepresentable.swift */; }; 0306C3B12B5E8F430099E45D /* VGSTextFieldRepresentableProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0306C3B02B5E8F430099E45D /* VGSTextFieldRepresentableProtocol.swift */; }; + 0310EBC92C6E240F00329345 /* VGSCardScanCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0310EBC82C6E240F00329345 /* VGSCardScanCoordinator.swift */; }; 0313A1202975A22700DB2F2C /* VGSTokenizationConfigurationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0313A11F2975A22700DB2F2C /* VGSTokenizationConfigurationTests.swift */; }; 031795D629DF00EA00BD1215 /* VGSTextFieldStatePublisherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 031795D529DF00EA00BD1215 /* VGSTextFieldStatePublisherTests.swift */; }; 033C979729D5BDC60088E045 /* VGSTextField+statePublisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 033C979629D5BDC60088E045 /* VGSTextField+statePublisher.swift */; }; @@ -20,6 +21,7 @@ 035179A52859BB7D00394BFC /* VGSExpDateTokenizationConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 035179A42859BB7D00394BFC /* VGSExpDateTokenizationConfiguration.swift */; }; 035179A72859BCC400394BFC /* VGSCardHolderNameTokenizationConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 035179A62859BCC400394BFC /* VGSCardHolderNameTokenizationConfiguration.swift */; }; 035179A92859BD5500394BFC /* VGSTokenizationConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 035179A82859BD5500394BFC /* VGSTokenizationConfiguration.swift */; }; + 036907D02C6CB12A005C1CF5 /* VGSBlinkCardControllerRepresentable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 036907CF2C6CB12A005C1CF5 /* VGSBlinkCardControllerRepresentable.swift */; }; 038EDA0D286F2EFF00AF3CF2 /* TokenizationApiTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 038EDA0C286F2EFF00AF3CF2 /* TokenizationApiTests.swift */; }; 039AB3482B5EB0C50048CF15 /* VGSExpDateTextFieldRepresentable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 039AB3472B5EB0C50048CF15 /* VGSExpDateTextFieldRepresentable.swift */; }; 039AB34A2B5EBB810048CF15 /* VGSDateTextFieldRepresentable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 039AB3492B5EBB810048CF15 /* VGSDateTextFieldRepresentable.swift */; }; @@ -229,6 +231,7 @@ 0306C3AA2B4EE0B20099E45D /* VGSTextFieldRepresentable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VGSTextFieldRepresentable.swift; sourceTree = ""; }; 0306C3AE2B5AF05D0099E45D /* VGSCardTextFieldRepresentable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VGSCardTextFieldRepresentable.swift; sourceTree = ""; }; 0306C3B02B5E8F430099E45D /* VGSTextFieldRepresentableProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VGSTextFieldRepresentableProtocol.swift; sourceTree = ""; }; + 0310EBC82C6E240F00329345 /* VGSCardScanCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VGSCardScanCoordinator.swift; sourceTree = ""; }; 0313A11F2975A22700DB2F2C /* VGSTokenizationConfigurationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VGSTokenizationConfigurationTests.swift; sourceTree = ""; }; 031795D529DF00EA00BD1215 /* VGSTextFieldStatePublisherTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VGSTextFieldStatePublisherTests.swift; sourceTree = ""; }; 033C979629D5BDC60088E045 /* VGSTextField+statePublisher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "VGSTextField+statePublisher.swift"; sourceTree = ""; }; @@ -239,6 +242,7 @@ 035179A42859BB7D00394BFC /* VGSExpDateTokenizationConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VGSExpDateTokenizationConfiguration.swift; sourceTree = ""; }; 035179A62859BCC400394BFC /* VGSCardHolderNameTokenizationConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VGSCardHolderNameTokenizationConfiguration.swift; sourceTree = ""; }; 035179A82859BD5500394BFC /* VGSTokenizationConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VGSTokenizationConfiguration.swift; sourceTree = ""; }; + 036907CF2C6CB12A005C1CF5 /* VGSBlinkCardControllerRepresentable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VGSBlinkCardControllerRepresentable.swift; sourceTree = ""; }; 038EDA0C286F2EFF00AF3CF2 /* TokenizationApiTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TokenizationApiTests.swift; sourceTree = ""; }; 039AB3472B5EB0C50048CF15 /* VGSExpDateTextFieldRepresentable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VGSExpDateTextFieldRepresentable.swift; sourceTree = ""; }; 039AB3492B5EBB810048CF15 /* VGSDateTextFieldRepresentable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VGSDateTextFieldRepresentable.swift; sourceTree = ""; }; @@ -494,6 +498,7 @@ 039AB3472B5EB0C50048CF15 /* VGSExpDateTextFieldRepresentable.swift */, 039AB3492B5EBB810048CF15 /* VGSDateTextFieldRepresentable.swift */, 039AB34B2B5EBB950048CF15 /* VGSCVCTextFieldRepresentable.swift */, + 0310EBC82C6E240F00329345 /* VGSCardScanCoordinator.swift */, ); path = SwiftUIElements; sourceTree = ""; @@ -513,6 +518,14 @@ path = TokenizationConfiguration; sourceTree = ""; }; + 036907CE2C6CB0D9005C1CF5 /* SwiftUI */ = { + isa = PBXGroup; + children = ( + 036907CF2C6CB12A005C1CF5 /* VGSBlinkCardControllerRepresentable.swift */, + ); + path = SwiftUI; + sourceTree = ""; + }; 038EDA0E286F2F8700AF3CF2 /* APIClient Tests */ = { isa = PBXGroup; children = ( @@ -535,6 +548,7 @@ 03B10BE228E2E36E009B409B /* VGSBlinkCardCollector */ = { isa = PBXGroup; children = ( + 036907CE2C6CB0D9005C1CF5 /* SwiftUI */, 03B10BE328E2E36E009B409B /* CardDataMappers */, 03B10BE628E2E36E009B409B /* VGSBlinkCardControllerDelegate.swift */, 03B10BE728E2E36E009B409B /* VGSBlinkCardController.swift */, @@ -1560,6 +1574,7 @@ 03B10C0E28E31B74009B409B /* VGSBlinkCardControllerDelegate.swift in Sources */, 03B10C0D28E31B74009B409B /* VGSBlinkCardExpirationDate.swift in Sources */, 03B10C1028E31B74009B409B /* VGSBlinkCardController.swift in Sources */, + 036907D02C6CB12A005C1CF5 /* VGSBlinkCardControllerRepresentable.swift in Sources */, 03B10C0C28E31B74009B409B /* VGSBlinkCardHandler.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -1688,6 +1703,7 @@ 0306C3B12B5E8F430099E45D /* VGSTextFieldRepresentableProtocol.swift in Sources */, 324B5E6D24A23CC50036867E /* VGSValidationError.swift in Sources */, 035179A32859BABC00394BFC /* VGSSSNTokenizationConfiguration.swift in Sources */, + 0310EBC92C6E240F00329345 /* VGSCardScanCoordinator.swift in Sources */, 321300192417F2E80062FEF0 /* VGSCollect+internal.swift in Sources */, 32E4989C23FAEF8300863E61 /* VGSFileInfo.swift in Sources */, 32093D7C25CD9F88006CD242 /* VGSCollectPrintingLogger.swift in Sources */, From 38aa7750cd6cfdf9a7b771e8de900580fcd50a05 Mon Sep 17 00:00:00 2001 From: Dmytro Khludkov Date: Thu, 15 Aug 2024 17:16:34 +0300 Subject: [PATCH 2/4] Update demo app. --- demoapp/demoapp.xcodeproj/project.pbxproj | 4 +- .../xcshareddata/xcschemes/demoapp.xcscheme | 7 +++ .../CardsDataCollectingViewController.swift | 2 +- .../SwiftUI/CardDataCollectionSwiftUI.swift | 60 +++++++++++++++---- 4 files changed, 58 insertions(+), 15 deletions(-) diff --git a/demoapp/demoapp.xcodeproj/project.pbxproj b/demoapp/demoapp.xcodeproj/project.pbxproj index 9d4f73c3..8d42e5ea 100644 --- a/demoapp/demoapp.xcodeproj/project.pbxproj +++ b/demoapp/demoapp.xcodeproj/project.pbxproj @@ -849,7 +849,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 35; - DEVELOPMENT_TEAM = 7S7622HN85; + DEVELOPMENT_TEAM = 4445Y664U5; INFOPLIST_FILE = demoapp/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 14.0; LD_RUNPATH_SEARCH_PATHS = ( @@ -875,7 +875,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 35; - DEVELOPMENT_TEAM = 7S7622HN85; + DEVELOPMENT_TEAM = 4445Y664U5; INFOPLIST_FILE = demoapp/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 14.0; LD_RUNPATH_SEARCH_PATHS = ( diff --git a/demoapp/demoapp.xcodeproj/xcshareddata/xcschemes/demoapp.xcscheme b/demoapp/demoapp.xcodeproj/xcshareddata/xcschemes/demoapp.xcscheme index c60672dd..321dbedc 100644 --- a/demoapp/demoapp.xcodeproj/xcshareddata/xcschemes/demoapp.xcscheme +++ b/demoapp/demoapp.xcodeproj/xcshareddata/xcschemes/demoapp.xcscheme @@ -89,6 +89,13 @@ isEnabled = "NO"> + + + + VGSTextField? { // match VGSTextField with scanned data switch type { - case .expirationDateLong: + case .expirationDate: return expCardDate case .cardNumber: return cardNumber diff --git a/demoapp/demoapp/UseCases/SwiftUI/CardDataCollectionSwiftUI.swift b/demoapp/demoapp/UseCases/SwiftUI/CardDataCollectionSwiftUI.swift index b9e3bfc2..18b4a1d1 100644 --- a/demoapp/demoapp/UseCases/SwiftUI/CardDataCollectionSwiftUI.swift +++ b/demoapp/demoapp/UseCases/SwiftUI/CardDataCollectionSwiftUI.swift @@ -14,8 +14,15 @@ struct CardDataCollectionSwiftUI: View { @State private var cardTextFieldState: VGSCardState? @State private var expDateTextFieldState: VGSTextFieldState? @State private var cvcTextFieldState: VGSTextFieldState? - + @State private var showingBlinkCardScanner = false @State private var consoleMessage = "" + /// Match scanned data with apropriate text fields. + @State private var scanedDataCoordinators: [VGSBlinkCardDataType: VGSCardScanCoordinator] = [ + .cardNumber: VGSCardScanCoordinator(), + .name: VGSCardScanCoordinator(), + .cvc: VGSCardScanCoordinator(), + .expirationDate: VGSCardScanCoordinator() + ] // MARK: - Textfield UI attributes let paddings = UIEdgeInsets(top: 2, left: 8, bottom: 2, right: 8) @@ -52,6 +59,7 @@ struct CardDataCollectionSwiftUI: View { return VStack(spacing: 8) { VGSTextFieldRepresentable(configuration: holderNameConfiguration) .placeholder("Cardholder Name") + .cardScanCoordinator(scanedDataCoordinators[.name]!) .onEditingStart { print("- Cardholder name onEditingStart") } @@ -63,6 +71,7 @@ struct CardDataCollectionSwiftUI: View { .frame(height: 54) VGSCardTextFieldRepresentable(configuration: cardNumConfiguration) .placeholder("4111 1111 1111 1111") + .cardScanCoordinator(scanedDataCoordinators[.cardNumber]!) .onStateChange { newState in cardTextFieldState = newState print(newState.isValid) @@ -75,28 +84,55 @@ struct CardDataCollectionSwiftUI: View { HStack(spacing: 20) { VGSExpDateTextFieldRepresentable(configuration: expDateConfiguration) .placeholder("MM/YY") + .cardScanCoordinator(scanedDataCoordinators[.expirationDate]!) .textFieldPadding(paddings) .border(color: (expDateTextFieldState?.isValid ?? true) ? validColor : invalidColor, lineWidth: 1) .frame(height: 54) VGSCVCTextFieldRepresentable(configuration: cvcConfiguration) .placeholder("CVC") + .cardScanCoordinator(scanedDataCoordinators[.cvc]!) .setSecureTextEntry(true) .cvcIconSize(CGSize(width: 30, height: 20)) .textFieldPadding(paddings) .border(color: (cvcTextFieldState?.isValid ?? true) ? validColor : invalidColor, lineWidth: 1) .frame(height: 54) } - Button(action: { - UIApplication.shared.endEditing() - sendData() - }) { - Text("UPLOAD") - .padding() - .cornerRadius(8) - .overlay( - RoundedRectangle(cornerRadius: 10) - .stroke(Color.blue, lineWidth: 2) - ) + HStack(spacing: 20) { + Button(action: { + UIApplication.shared.endEditing() + showingBlinkCardScanner = true + }) { + Text("SCAN") + .padding() + .cornerRadius(8) + .overlay( + RoundedRectangle(cornerRadius: 10) + .stroke(Color.blue, lineWidth: 2) + ) + } + .fullScreenCover(isPresented: $showingBlinkCardScanner) { + VGSBlinkCardControllerRepresentable(licenseKey: AppCollectorConfiguration.shared.blinkCardLicenseKey!, dataCoordinators: scanedDataCoordinators) { (errorCode) in + print(errorCode) + }.allowInvalidCardNumber(true) + .onCardScanned({ + showingBlinkCardScanner = false + }) + .onCardScanCanceled({ + showingBlinkCardScanner = false + }) + } + Button(action: { + UIApplication.shared.endEditing() + sendData() + }) { + Text("UPLOAD") + .padding() + .cornerRadius(8) + .overlay( + RoundedRectangle(cornerRadius: 10) + .stroke(Color.blue, lineWidth: 2) + ) + } }.padding(.top, 50) Text("\(consoleMessage)") }.padding(.leading, 20) From b9bad0a5f66d090f8eddb250c15aafbf685ebfaa Mon Sep 17 00:00:00 2001 From: Dmytro Khludkov Date: Thu, 15 Aug 2024 17:25:59 +0300 Subject: [PATCH 3/4] Add Card.io deprecation warning. --- Sources/VGSCardIOCollector/VGSCardIOScanController.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Sources/VGSCardIOCollector/VGSCardIOScanController.swift b/Sources/VGSCardIOCollector/VGSCardIOScanController.swift index 975b88ac..ab51a060 100644 --- a/Sources/VGSCardIOCollector/VGSCardIOScanController.swift +++ b/Sources/VGSCardIOCollector/VGSCardIOScanController.swift @@ -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 From bf6d31c73e150122661a699938a9a825882747d5 Mon Sep 17 00:00:00 2001 From: Dmytro Khludkov Date: Fri, 16 Aug 2024 14:19:19 +0300 Subject: [PATCH 4/4] Bump SDK version. --- Sources/VGSCollectSDK/Utils/Extensions/Utils.swift | 2 +- VGSCollectSDK.podspec | 2 +- VGSCollectSDK.xcodeproj/project.pbxproj | 8 ++++---- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Sources/VGSCollectSDK/Utils/Extensions/Utils.swift b/Sources/VGSCollectSDK/Utils/Extensions/Utils.swift index bc9f2712..826711a4 100644 --- a/Sources/VGSCollectSDK/Utils/Extensions/Utils.swift +++ b/Sources/VGSCollectSDK/Utils/Extensions/Utils.swift @@ -46,7 +46,7 @@ internal class Utils { /// VGS Collect SDK Version. /// Necessary since SPM doesn't track info plist correctly: https://forums.swift.org/t/add-info-plist-on-spm-bundle/40274/5 - static let vgsCollectVersion: String = "1.16.2" + static let vgsCollectVersion: String = "1.16.3" } extension Dictionary { diff --git a/VGSCollectSDK.podspec b/VGSCollectSDK.podspec index 3bea7920..94d0d59d 100644 --- a/VGSCollectSDK.podspec +++ b/VGSCollectSDK.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |spec| spec.name = 'VGSCollectSDK' - spec.version = '1.16.2' + spec.version = '1.16.3' spec.summary = 'VGS Collect - is a product suite that allows customers to collect information securely without possession of it.' spec.swift_version = '5.0' spec.description = <<-DESC diff --git a/VGSCollectSDK.xcodeproj/project.pbxproj b/VGSCollectSDK.xcodeproj/project.pbxproj index 1a6e182c..756805c5 100644 --- a/VGSCollectSDK.xcodeproj/project.pbxproj +++ b/VGSCollectSDK.xcodeproj/project.pbxproj @@ -2004,7 +2004,7 @@ GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 12.0; - MARKETING_VERSION = 1.16.2; + MARKETING_VERSION = 1.16.3; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; @@ -2066,7 +2066,7 @@ GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 12.0; - MARKETING_VERSION = 1.16.2; + MARKETING_VERSION = 1.16.3; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; SDKROOT = iphoneos; @@ -2099,7 +2099,7 @@ "@executable_path/Frameworks", "@loader_path/Frameworks", ); - MARKETING_VERSION = 1.16.2; + MARKETING_VERSION = 1.16.3; PRODUCT_BUNDLE_IDENTIFIER = com.vgs.framework; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -2131,7 +2131,7 @@ "@executable_path/Frameworks", "@loader_path/Frameworks", ); - MARKETING_VERSION = 1.16.2; + MARKETING_VERSION = 1.16.3; PRODUCT_BUNDLE_IDENTIFIER = com.vgs.framework; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; PROVISIONING_PROFILE_SPECIFIER = "";