From a05fa24a4fd8d6b3d69bb99b59730c32a560ce5f Mon Sep 17 00:00:00 2001
From: Dima Khludkov <57712402+dmytrokhl@users.noreply.github.com>
Date: Wed, 18 Jan 2023 16:21:27 +0200
Subject: [PATCH] Public Release 1.12.0
Public Release 1.12.0
---
MIGRATING.md | 4 +
Package.resolved | 16 +-
Package.swift | 49 +-
README.md | 21 +-
.../VGSBlinkCardExpirationDate.swift | 61 +++
.../VGSBlinkCardController.swift | 70 +++
.../VGSBlinkCardControllerDelegate.swift | 65 +++
.../VGSBlinkCardHandler.swift | 137 ++++++
.../VGSBouncerDataMapUtils.swift | 183 --------
.../VGSBouncerExpirationDate.swift | 17 -
.../VGSCardScanController.swift | 71 ---
.../VGSCardScanControllerDelegate.swift | 65 ---
.../VGSCardScanHandler.swift | 127 ------
.../Text Field/VGSCVCTextField.swift | 2 -
.../Utils/Extensions/Utils.swift | 3 +-
.../Text Fields Tests/ExpDateTextField.swift | 25 +-
.../ExpDateTextFieldTests.swift | 5 +-
.../Text Fields Tests/VGSTextFieldTests.swift | 3 +
.../VGSTokenizationConfigurationTests.swift | 162 +++++++
.../VGSTokenizationResponseMappingTests.swift | 8 +-
Tests/FrameworkTests/VGSCollectTests.swift | 19 +-
.../Info.plist | 0
VGSBlinkCardCollector/VGSBlinkCardCollector.h | 19 +
VGSCardScanCollector/VGSCardScanCollector.h | 19 -
VGSCardScanCollectorTests/Info.plist | 22 -
.../VGSBouncerDataMapUtilsTests.swift | 116 -----
VGSCollectSDK.podspec | 12 +-
VGSCollectSDK.xcodeproj/project.pbxproj | 422 ++++++------------
.../contents.xcworkspacedata | 2 +-
...cscheme => VGSBlinkCardCollector.xcscheme} | 24 +-
...cheme => VGSCardIOCollectorTests.xcscheme} | 8 +-
demoapp/Podfile | 2 +
demoapp/demoapp.xcodeproj/project.pbxproj | 36 +-
.../demoapp/AppCollectorConfiguration.swift | 3 +
demoapp/demoapp/Info.plist | 5 +
.../CardsDataCollectingViewController.swift | 134 +++---
demoapp/demoapp/demoapp.entitlements | 7 +-
images/VGSCollect_CardScan_SPM_2.png | Bin 55497 -> 193622 bytes
VGSZeroData.png => images/VGSZeroData.png | Bin
add-card.gif => images/add-card.gif | Bin
card-scan.gif => images/card-scan.gif | Bin
cardTextField.gif => images/cardTextField.gif | Bin
file-picker.gif => images/file-picker.gif | Bin
state.gif => images/state.gif | Bin
.../vgs-collect-ios-response.png | Bin
.../vgs-collect-ios-state.png | Bin
46 files changed, 853 insertions(+), 1091 deletions(-)
create mode 100644 Sources/VGSBlinkCardCollector/CardDataMappers/VGSBlinkCardExpirationDate.swift
create mode 100644 Sources/VGSBlinkCardCollector/VGSBlinkCardController.swift
create mode 100644 Sources/VGSBlinkCardCollector/VGSBlinkCardControllerDelegate.swift
create mode 100644 Sources/VGSBlinkCardCollector/VGSBlinkCardHandler.swift
delete mode 100644 Sources/VGSCardScanCollector/CardDataMappers/VGSBouncerDataMapUtils.swift
delete mode 100644 Sources/VGSCardScanCollector/CardDataMappers/VGSBouncerExpirationDate.swift
delete mode 100644 Sources/VGSCardScanCollector/VGSCardScanController.swift
delete mode 100644 Sources/VGSCardScanCollector/VGSCardScanControllerDelegate.swift
delete mode 100644 Sources/VGSCardScanCollector/VGSCardScanHandler.swift
create mode 100644 Tests/FrameworkTests/TokenizationTests/VGSTokenizationConfigurationTests.swift
rename {VGSCardScanCollector => VGSBlinkCardCollector}/Info.plist (100%)
create mode 100644 VGSBlinkCardCollector/VGSBlinkCardCollector.h
delete mode 100644 VGSCardScanCollector/VGSCardScanCollector.h
delete mode 100644 VGSCardScanCollectorTests/Info.plist
delete mode 100644 VGSCardScanCollectorTests/MapExpirationDateTests/VGSBouncerDataMapUtilsTests.swift
rename VGSCollectSDK.xcodeproj/xcshareddata/xcschemes/{VGSCardScanCollector.xcscheme => VGSBlinkCardCollector.xcscheme} (70%)
rename VGSCollectSDK.xcodeproj/xcshareddata/xcschemes/{VGSCardScanCollectorTests.xcscheme => VGSCardIOCollectorTests.xcscheme} (88%)
rename VGSZeroData.png => images/VGSZeroData.png (100%)
rename add-card.gif => images/add-card.gif (100%)
rename card-scan.gif => images/card-scan.gif (100%)
rename cardTextField.gif => images/cardTextField.gif (100%)
rename file-picker.gif => images/file-picker.gif (100%)
rename state.gif => images/state.gif (100%)
rename vgs-collect-ios-response.png => images/vgs-collect-ios-response.png (100%)
rename vgs-collect-ios-state.png => images/vgs-collect-ios-state.png (100%)
diff --git a/MIGRATING.md b/MIGRATING.md
index 253b4b16..64c8d7de 100644
--- a/MIGRATING.md
+++ b/MIGRATING.md
@@ -1,5 +1,9 @@
## Migration Guides
+### Migrating from versions < v1.12.0
+#### `VGSCollectSDK/CardScan` scan module deprecated.
+Use `VGSCollectSDK/CardIO` or `VGSCollectSDK/BlinkCard` as card scan solution instead.
+
### Migrating from versions < v1.11.3
#### Rename enum
`HTTPMethod` -> `VGSCollectHTTPMethod`
diff --git a/Package.resolved b/Package.resolved
index 4f13ac25..9e670d73 100644
--- a/Package.resolved
+++ b/Package.resolved
@@ -2,21 +2,21 @@
"object": {
"pins": [
{
- "package": "CardIOSDK",
- "repositoryURL": "https://github.com/verygoodsecurity/CardIOSDK-iOS.git",
+ "package": "BlinkCard",
+ "repositoryURL": "https://github.com/blinkcard/blinkcard-swift-package",
"state": {
"branch": null,
- "revision": "9f21bec2d2f12d14ffbe8305c44ceff9b60e35af",
- "version": "5.5.7"
+ "revision": "e74689f8e0df9c4fbebf46b87c4ab716325516a8",
+ "version": "2.7.0"
}
},
{
- "package": "CardScan",
- "repositoryURL": "https://github.com/getbouncer/cardscan-ios.git",
+ "package": "CardIOSDK",
+ "repositoryURL": "https://github.com/verygoodsecurity/CardIOSDK-iOS.git",
"state": {
"branch": null,
- "revision": "84e7d1e7805675274edd85bc786b2dd1e4d864e0",
- "version": "2.0.9"
+ "revision": "9f21bec2d2f12d14ffbe8305c44ceff9b60e35af",
+ "version": "5.5.7"
}
}
]
diff --git a/Package.swift b/Package.swift
index 8d9426f4..ac4d3f77 100644
--- a/Package.swift
+++ b/Package.swift
@@ -13,25 +13,24 @@ let package = Package(
.library(
name: "VGSCollectSDK",
targets: ["VGSCollectSDK"]),
- .library(
- name: "VGSCardScanCollector",
- targets: ["VGSCardScanCollector"]
- ),
- .library(
- name: "VGSCardIOCollector",
- targets: ["VGSCardIOCollector"])
+ .library(
+ name: "VGSCardIOCollector",
+ targets: ["VGSCardIOCollector"]),
+ .library(
+ name: "VGSBlinkCardCollector",
+ targets: ["VGSBlinkCardCollector"])
],
dependencies: [
- .package(
- name: "CardScan",
- url: "https://github.com/getbouncer/cardscan-ios.git",
- .exact("2.0.9")
- ),
.package(
name: "CardIOSDK",
url: "https://github.com/verygoodsecurity/CardIOSDK-iOS.git",
.exact("5.5.7")
- )
+ ),
+ .package(
+ name: "BlinkCard",
+ url: "https://github.com/blinkcard/blinkcard-swift-package",
+ .exact("2.7.0")
+ )
],
targets: [
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
@@ -49,18 +48,16 @@ let package = Package(
"Info.plist",
"FrameworkTests.xctestplan"
],
- resources: [.process("Resources")]
- ),
- .target(
- name: "VGSCardScanCollector",
- dependencies: ["VGSCollectSDK",
- .product(name: "CardScan", package: "CardScan")],
- path: "Sources/VGSCardScanCollector/"
- ),
- .target(
- name: "VGSCardIOCollector",
- dependencies: ["VGSCollectSDK",
- .product(name: "CardIOSDK", package: "CardIOSDK")],
- path: "Sources/VGSCardIOCollector/")
+ resources: [.process("Resources")]),
+ .target(
+ name: "VGSCardIOCollector",
+ dependencies: ["VGSCollectSDK",
+ .product(name: "CardIOSDK", package: "CardIOSDK")],
+ path: "Sources/VGSCardIOCollector/"),
+ .target(
+ name: "VGSBlinkCardCollector",
+ dependencies: ["VGSCollectSDK",
+ .product(name: "BlinkCard", package: "BlinkCard")],
+ path: "Sources/VGSBlinkCardCollector/")
]
)
diff --git a/README.md b/README.md
index 4bfbd179..833e5662 100644
--- a/README.md
+++ b/README.md
@@ -4,7 +4,7 @@
[![Platform](https://img.shields.io/cocoapods/p/VGSCollectSDK.svg?style=flat)](https://github.com/verygoodsecurity/vgs-collect-ios)
[![swift](https://img.shields.io/badge/swift-5-orange)]()
[![Cocoapods Compatible](https://img.shields.io/cocoapods/v/VGSCollectSDK.svg?style=flat)](https://cocoapods.org/pods/VGSCollectSDK)
-
+
# VGS Collect iOS SDK
@@ -30,8 +30,8 @@ Table of contents
-
-
+
+
@@ -93,7 +93,7 @@ Use your `` to initialize VGSCollect instance. You can get it in your [
Customize VGSTextFields... |
- |
+ |
@@ -131,7 +131,7 @@ Use your `` to initialize VGSCollect instance. You can get it in your [
|
... observe filed states |
- |
+ |
@@ -213,14 +213,16 @@ Add 'VGSCollectSDK' alongside with one of scan modules pod:
```ruby
pod 'VGSCollectSDK'
-# Add CardIO module to use Card.io as scan provider
-pod 'VGSCollectSDK/CardIO'
+# Add one of available scan providers
+pod 'VGSCollectSDK/CardIO'
+pod 'VGSCollectSDK/BlinkCard'
```
#### Integrate with Swift Package Manager
Starting with the 1.7.11 release, `VGSCollectSDK` supports [CardIO](https://github.com/verygoodsecurity/card.io-iOS-source) integration via Swift PM.
To use **CardIO** add `VGSCollectSDK`, `VGSCardIOCollector` packages to your target.
+To use **BlinkCard** add `VGSCollectSDK`, `VGSBlinkCardCollector` packages to your target.
#### Code Example
@@ -231,7 +233,7 @@ To use **CardIO** add `VGSCollectSDK`, `VGSCardIOCollector` packages to your tar
|
Setup VGSCardIOScanController... |
- |
+ |
@@ -392,7 +394,7 @@ You can add a file uploading functionality to your application with **VGSFilePic
}
|
- |
+ |
... send file to your Vault |
@@ -453,6 +455,7 @@ VGSAnalyticsClient.shared.shouldCollectAnalytics = false
- Swift 5
- Optional 3rd party libraries:
- [CardIO](https://github.com/card-io/card.io-iOS-SDK)
+ - [BlinkCard](https://github.com/blinkcard/blinkcard-ios)
## License
diff --git a/Sources/VGSBlinkCardCollector/CardDataMappers/VGSBlinkCardExpirationDate.swift b/Sources/VGSBlinkCardCollector/CardDataMappers/VGSBlinkCardExpirationDate.swift
new file mode 100644
index 00000000..e806fa1b
--- /dev/null
+++ b/Sources/VGSBlinkCardCollector/CardDataMappers/VGSBlinkCardExpirationDate.swift
@@ -0,0 +1,61 @@
+
+import Foundation
+#if !COCOAPODS
+import VGSCollectSDK
+#endif
+
+/// Holds scanned expiration date data.
+/// BlinkCard holds scanned data expiration date with two separate Ints.
+internal struct VGSBlinkCardExpirationDate {
+ /// Scanned month.
+ let month: Int
+ /// Month converted to String: "01"
+ let monthString: String
+ /// Scanned year: YYYY
+ let year: Int
+ /// Scanned year short: YY
+ let shortYear: Int
+
+ init(_ month: Int, year: Int) {
+ self.month = month
+ // Normalize month to format "01"
+ self.monthString = Self.formatMonthString(from: month)
+ self.year = year
+ self.shortYear = year - 2000
+ }
+
+ // MARK: - Helpers
+
+ /// Maps scanned exp month and year to valid format (MM/YY).
+ /// - Returns: `String`, composed text or nil if scanned info is invalid.
+ func mapDefaultExpirationDate() -> String {
+ return "\(monthString)\(shortYear)"
+ }
+
+ /// Maps scanned exp month and year to long expiration date format (MM/YYYY).
+ /// - Returns: `String`, composed text or nil if scanned info is invalid.
+ func mapLongExpirationDate() -> String {
+ return "\(monthString)\(year)"
+ }
+
+ /// Maps scanned exp month and year to valid format starting with year (YY/MM).
+ /// - Returns: `String`, composed text or nil if scanned info is invalid.
+ func mapExpirationDateWithShortYearFirst() -> String {
+ return "\(shortYear)\(monthString)"
+ }
+
+ /// Maps scanned exp month and year to long expiration date format starting with year (YYYY/MM).
+ /// - Returns: `String`, composed text or nil if scanned info is invalid.
+ func mapLongExpirationDateWithLongYearFirst() -> String {
+ return "\(year)\(monthString)"
+ }
+
+ /// Formats month int.
+ /// - Parameter monthInt: `Int`, should be month.
+ /// - Returns: `String` object, formatted month.
+ private static func formatMonthString(from monthInt: Int) -> String {
+ // Add `0` for month less than 10.
+ let monthString = monthInt < 10 ? "0\(monthInt)" : "\(monthInt)"
+ return monthString
+ }
+}
diff --git a/Sources/VGSBlinkCardCollector/VGSBlinkCardController.swift b/Sources/VGSBlinkCardCollector/VGSBlinkCardController.swift
new file mode 100644
index 00000000..e134c01d
--- /dev/null
+++ b/Sources/VGSBlinkCardCollector/VGSBlinkCardController.swift
@@ -0,0 +1,70 @@
+//
+// VGSBlinkCardController.swift
+// VGSBlinkCardCollector
+//
+
+import Foundation
+#if !COCOAPODS
+import VGSCollectSDK
+#endif
+#if os(iOS)
+import UIKit
+#endif
+
+/// Controller responsible for managing `BlinkCard` scanner.
+@available(iOS 13.0, *)
+public class VGSBlinkCardController {
+
+ // MARK: - Attributes
+
+ /// Handle card scanner events.
+ internal var scanHandler: VGSBlinkCardHandler?
+
+ /// `VGSBlinkCardControllerDelegate` - handle user interaction with `BlinkCard` scanner.
+ public var delegate: VGSBlinkCardControllerDelegate? {
+ set {
+ scanHandler?.delegate = newValue
+ }
+ get {
+ return scanHandler?.delegate
+ }
+ }
+
+ // MARK: - Initialization
+
+ /// Initialization
+ /// - Parameters:
+ /// - licenseKey: key required for BlinkCard SDK usage.
+ /// - delegate: `VGSBlinkCardControllerDelegate`. Default is `nil`.
+ /// - errorCallback: Error callback with Int error code(represents `MBCLicenseError` enum), triggered only when error occured.
+ public required init(licenseKey: String, delegate: VGSBlinkCardControllerDelegate? = nil, onError errorCallback: @escaping ((NSInteger) -> Void)) {
+ self.scanHandler = VGSBlinkCardHandler(licenseKey: licenseKey, errorCallback: errorCallback)
+ self.delegate = delegate
+
+ }
+
+ // MARK: - Methods
+
+ /// Present `BlinkCard` scanner.
+ /// - Parameters:
+ /// - viewController: `UIViewController` that will present card scanner.
+ /// - animated: pass `true` to animate the presentation; otherwise, pass `false`.
+ /// - modalPresentationStyle: `UIModalPresentationStyle` object, modal presentation style. Default is `.overCurrentContext`.
+ /// - completion: the block to execute after the presentation finishes.
+ public func presentCardScanner(on viewController: UIViewController, animated: Bool, modalPresentationStyle: UIModalPresentationStyle = .overCurrentContext, completion: (() -> Void)?) {
+ scanHandler?.presentScanVC(on: viewController, animated: animated, modalPresentationStyle: modalPresentationStyle, completion: completion)
+ }
+
+ /// Dismiss `BlinkCard` scanner.
+ /// - Parameters:
+ /// - animated: pass `true` to animate the dismiss of presented viewcontroller; otherwise, pass `false`.
+ /// - completion: the block to execute after the dismiss finishes.
+ public func dismissCardScanner(animated: Bool, completion: (() -> Void)?) {
+ scanHandler?.dismissScanVC(animated: animated, completion: completion)
+ }
+
+ /// Set custom localization fileName.
+ public static func setCustomLocalization(fileName: String) {
+ VGSBlinkCardHandler.setCustomLocalization(fileName: fileName)
+ }
+}
diff --git a/Sources/VGSBlinkCardCollector/VGSBlinkCardControllerDelegate.swift b/Sources/VGSBlinkCardCollector/VGSBlinkCardControllerDelegate.swift
new file mode 100644
index 00000000..39e2f50e
--- /dev/null
+++ b/Sources/VGSBlinkCardCollector/VGSBlinkCardControllerDelegate.swift
@@ -0,0 +1,65 @@
+//
+// VGSBlinkCardControllerDelegate.swift
+// VGSBlinkCardCollector
+//
+
+import Foundation
+#if !COCOAPODS
+import VGSCollectSDK
+#endif
+#if os(iOS)
+import UIKit
+#endif
+
+/// Supported scan data fields by BlinkCard.
+@objc
+public enum VGSBlinkCardDataType: Int {
+
+ /// Credit Card Number. Digits string.
+ case cardNumber
+
+ /// Card holder name displayed on card.
+ case name
+
+ /// Card cvc.
+ case cvc
+
+ /// Credit Card Expiration Date. String in format "mm/yy", e.g: "01/21".
+ case expirationDate
+
+ /// Credit Card Expiration Month. String in format "mm", e.g:"01".
+ case expirationMonth
+
+ /// Credit Card Expiration Year. String in format "yy", e.g: "21".
+ case expirationYear
+
+ /// Credit Card Expiration Year. String in format "yyyy", e.g:"2021".
+ case expirationYearLong
+
+ /// Credit Card Expiration Date. String in format "mm/yyyy", e.g:"01/2021".
+ case expirationDateLong
+
+ /// Credit Card Expiration Date. String in format "yy/mm", e.g:"21/01".
+ case expirationDateShortYearThenMonth
+
+ /// Credit Card Expiration Date. String in format "yyyy/mm", e.g:"2021/01".
+ case expirationDateLongYearThenMonth
+}
+
+/// Delegates produced by `VGSBlinkCardController` instance.
+@objc
+public protocol VGSBlinkCardControllerDelegate {
+
+ // MARK: - Handle user ineraction with `BlinkCard`
+
+ /// On user confirm scanned data by selecting Done button on `BlinkCard` screen.
+ @objc func userDidFinishScan()
+
+ /// On user press Cancel buttonn on `BlinkCard` screen.
+ @objc func userDidCancelScan()
+
+ // MARK: - Manage scanned data
+
+ /// Asks `VGSTextField` where scanned data with `VGSConfiguration.FieldType` need to be set. Called after user select Done button, just before userDidFinishScan() delegate.
+ @objc func textFieldForScannedData(type: VGSBlinkCardDataType) -> VGSTextField?
+}
diff --git a/Sources/VGSBlinkCardCollector/VGSBlinkCardHandler.swift b/Sources/VGSBlinkCardCollector/VGSBlinkCardHandler.swift
new file mode 100644
index 00000000..850446d1
--- /dev/null
+++ b/Sources/VGSBlinkCardCollector/VGSBlinkCardHandler.swift
@@ -0,0 +1,137 @@
+//
+// VGSBlinkCardHandler.swift
+// VGSBlinkCardCollector
+//
+
+
+import Foundation
+import BlinkCard
+#if !COCOAPODS
+import VGSCollectSDK
+#endif
+#if os(iOS)
+import UIKit
+#endif
+
+/// BlinkCard wrapper, manages communication between public API and BlinkCard.
+@available(iOS 13.0, *)
+internal class VGSBlinkCardHandler: NSObject, VGSScanHandlerProtocol {
+ /// VGS BlinkCard Controller Delegate.
+ weak var delegate: VGSBlinkCardControllerDelegate?
+ /// BlinkCard scanner UIViewController.
+ weak var view: UIViewController?
+ /// MBCBlinkCardRecognizer.
+ lazy var cardRecognizer: MBCBlinkCardRecognizer = {
+ return MBCBlinkCardRecognizer()
+ }()
+
+ /// Initialization
+ /// - Parameters:
+ /// - licenseKey: key required for BlinkCard SDK usage.
+ /// - errorCallback: Error callback with Int error code(represents `MBCLicenseError` enum), triggered only when error occured.
+ required init(licenseKey: String, errorCallback: @escaping ((NSInteger) -> Void)) {
+ super.init()
+ MBCMicroblinkSDK.shared().setLicenseKey(licenseKey) { error in
+ VGSAnalyticsClient.shared.trackEvent(.scan, status: .failed, extraData: ["scannerType": "BlinkCard", "errorCode": error])
+ errorCallback(error.rawValue)
+ }
+ }
+
+ /// Setup BlinlCard params and present scanner.
+ func presentScanVC(on viewController: UIViewController, animated: Bool, modalPresentationStyle: UIModalPresentationStyle, completion: (() -> Void)?) {
+ // Create BlinkCard settings
+ let settings : MBCBlinkCardOverlaySettings = MBCBlinkCardOverlaySettings()
+ settings.enableEditScreen = false
+ // Crate recognizer collection
+ let recognizerList = [cardRecognizer]
+ let recognizerCollection : MBCRecognizerCollection = MBCRecognizerCollection(recognizers: recognizerList)
+ // Create overlay view controller
+ let blinkCardOverlayViewController = MBCBlinkCardOverlayViewController(settings: settings, recognizerCollection: recognizerCollection, delegate: self)
+ // Create recognizer view controller with wanted overlay view controller
+ let recognizerRunneViewController : UIViewController = MBCViewControllerFactory.recognizerRunnerViewController(withOverlayViewController: blinkCardOverlayViewController)!
+ recognizerRunneViewController.modalPresentationStyle = modalPresentationStyle
+ self.view = recognizerRunneViewController
+ // Present the recognizer runner view controller
+ viewController.present(recognizerRunneViewController, animated: true, completion: completion)
+ }
+
+ /// Setup BlinkCard params and present scanner.
+ func dismissScanVC(animated: Bool, completion: (() -> Void)?) {
+ view?.dismiss(animated: animated, completion: completion)
+ }
+
+ /// Set custom localization fileName.
+ static func setCustomLocalization(fileName: String) {
+ MBCMicroblinkApp.shared().customLocalizationFileName = fileName
+ }
+}
+
+/// :nodoc:
+@available(iOS 13.0, *)
+extension VGSBlinkCardHandler: MBCBlinkCardOverlayViewControllerDelegate {
+
+ /// When BlinkCard finish card recognition.
+ func blinkCardOverlayViewControllerDidFinishScanning(_ blinkCardOverlayViewController: MBCBlinkCardOverlayViewController, state: MBCRecognizerResultState) {
+ // pause scanning
+ blinkCardOverlayViewController.recognizerRunnerViewController?.pauseScanning()
+ // send results in main thread
+ DispatchQueue.main.async { [weak self] in
+ // 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"])
+ }
+ 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()
+ }
+ }
+
+ /// When user tap close button.
+ func blinkCardOverlayViewControllerDidTapClose(_ blinkCardOverlayViewController: MBCBlinkCardOverlayViewController) {
+ VGSAnalyticsClient.shared.trackEvent(.scan, status: .cancel, extraData: [ "scannerType": "BlinkCard"])
+ delegate?.userDidCancelScan()
+ }
+}
diff --git a/Sources/VGSCardScanCollector/CardDataMappers/VGSBouncerDataMapUtils.swift b/Sources/VGSCardScanCollector/CardDataMappers/VGSBouncerDataMapUtils.swift
deleted file mode 100644
index 3bd895f3..00000000
--- a/Sources/VGSCardScanCollector/CardDataMappers/VGSBouncerDataMapUtils.swift
+++ /dev/null
@@ -1,183 +0,0 @@
-//
-// VGSBouncerDataMapUtils.swift
-// VGSCollectSDK
-//
-
-import Foundation
-#if !COCOAPODS
-import VGSCollectSDK
-#endif
-
-/// Holds mapping utils for scanned card data.
-internal final class VGSBouncerDataMapUtils {
-
- // MARK: - Constants
-
- /// Valid months range.
- private static let monthsRange = 1...12
-
- // MARK: - Interface
-
- /// Maps scanned expiration data to expected format.
- /// - Parameters:
- /// - data: `VGSBouncerExpirationDate` object, scanned expiry date data.
- /// - format: `CradScanDataType` object, card data type.
- /// - Returns: `String?`, formatted string or `nil`.
- internal static func mapCardExpirationData(_ data: VGSBouncerExpirationDate, scannedDataType: CradScanDataType) -> String? {
- switch scannedDataType {
- case .cardNumber, .name:
- return nil
- case .expirationDate:
- return mapDefaultExpirationDate(data.monthString, scannedExpYear: data.yearString)
- case .expirationDateLong:
- return mapLongExpirationDate(data.monthString, scannedExpYear: data.yearString)
- case .expirationMonth:
- return mapMonth(data.monthString)
- case .expirationYear:
- return mapYear(data.yearString)
- case .expirationYearLong:
- return mapYearLong(data.yearString)
- case .expirationDateShortYearThenMonth:
- return mapExpirationDateWithShortYearFirst(data.monthString, scannedExpYear: data.yearString)
- case .expirationDateLongYearThenMonth:
- return mapLongExpirationDateWithLongYearFirst(data.monthString, scannedExpYear: data.yearString)
- }
- }
-
- // MARK: - Helpers
-
- /// Maps scanned exp month and year to valid format (MM/YY).
- /// - Parameters:
- /// - scannedExpMonth: `String` object, scanned expiry month.
- /// - scannedExpYear: `String` object, scanned expiry year.
- /// - Returns: `String?`, composed text or nil if scanned info is invalid.
- private static func mapDefaultExpirationDate(_ scannedExpMonth: String?, scannedExpYear: String?) -> String? {
- guard let month = mapMonth(scannedExpMonth), let year = mapYear(scannedExpYear) else {
- return nil
- }
-
- return "\(month)\(year)"
- }
-
- /// Maps scanned exp month and year to long expiration date format (MM/YYYY).
- /// - Parameters:
- /// - scannedExpMonth: `String` object, scanned expiry month.
- /// - scannedExpYear: `String` object, scanned expiry year.
- /// - Returns: `String?`, composed text or nil if scanned info is invalid.
- private static func mapLongExpirationDate(_ scannedExpMonth: String?, scannedExpYear: String?) -> String? {
- guard let month = mapMonth(scannedExpMonth), let longYear = mapYearLong(scannedExpYear) else {
- return nil
- }
-
- return "\(month)\(longYear)"
- }
-
- /// Maps scanned exp month and year to valid format starting with year (YY/MM).
- /// - Parameters:
- /// - scannedExpMonth: `UInt` object, scanned expiry month.
- /// - scannedExpYear: `UInt` object, scanned expiry year.
- /// - Returns: `String?`, composed text or nil if scanned info is invalid.
- private static func mapExpirationDateWithShortYearFirst(_ scannedExpMonth: String?, scannedExpYear: String?) -> String? {
- guard let month = mapMonth(scannedExpMonth), let year = mapYear(scannedExpYear) else {
- return nil
- }
-
- return "\(year)\(month)"
- }
-
- /// Maps scanned exp month and year to long expiration date format starting with year (YYYY/MM).
- /// - Parameters:
- /// - scannedExpMonth: `UInt` object, scanned expiry month.
- /// - scannedExpYear: `UInt` object, scanned expiry year.
- /// - Returns: `String?`, composed text or nil if scanned info is invalid.
- private static func mapLongExpirationDateWithLongYearFirst(_ scannedExpMonth: String?, scannedExpYear: String?) -> String? {
- guard let month = mapMonth(scannedExpMonth), let longYear = mapYearLong(scannedExpYear) else {
- return nil
- }
-
- return "\(longYear)\(month)"
- }
-
- /// Maps scanned expiry month to short format (YY) string.
- /// - Parameter scannedExpYear: `String?` object, scanned expiry year.
- /// - Returns: `String?`, year text or nil if scanned info is invalid.
- private static func mapMonth(_ scannedExpMonth: String?) -> String? {
- guard let month = monthInt(from: scannedExpMonth) else {return nil}
-
- let formattedMonthString = formatMonthString(from: month)
- return formattedMonthString
- }
-
- /// Maps scanned expiry year to short format (YY) string.
- /// - Parameter scannedExpYear: `String?` object, scanned expiry year.
- /// - Returns: `String?`, year text or nil if scanned info is invalid.
- private static func mapYear(_ scannedExpYear: String?) -> String? {
- guard let year = yearInt(from: scannedExpYear) else {return nil}
-
- return "\(year)"
- }
-
- /// Maps scanned expiry year to long format (YYYY) string.
- /// - Parameter scannedExpYear: `String?` object, scanned expiry year.
- /// - Returns: `String?`, year text or nil if scanned info is invalid.
- private static func mapYearLong(_ scannedExpYear: String?) -> String? {
- guard let year = yearInt(from: scannedExpYear) else {return nil}
-
- return longYearString(from: year)
- }
-
- /// Converts year to long format string.
- /// - Parameter shortYear: `Int` object, should be short year.
- /// - Returns: `String` with long year format.
- private static func longYearString(from shortYear: Int) -> String {
- return "20\(shortYear)"
- }
-
- /// Checks if month (Int) is valid.
- /// - Parameter month: `Int` object, month to verify.
- /// - Returns: `Bool` object, `true` if is valid.
- private static func isMonthValid(_ month: Int) -> Bool {
- return monthsRange ~= month
- }
-
- /// Checks if year (Int) is valid.
- /// - Parameter year: `Int` object, year to verify.
- /// - Returns: `Bool` object, `true` if is valid.
- private static func isYearValid(_ year: Int) -> Bool {
- // CardScan returns year in short format: `2024` -> `24`.
- return year >= VGSCalendarUtils.currentYearShort
- }
-
- /// Provides month Int from text.
- /// - Parameter monthString: `String` object, month as text.
- /// - Returns: `Int?`, valid month or `nil`.
- private static func monthInt(from monthString: String?) -> Int? {
- guard let month = monthString, !month.isEmpty,
- let monthInt = Int(month), isMonthValid(monthInt) else {
- return nil
- }
-
- return monthInt
- }
-
- /// Provides year Int from text.
- /// - Parameter yearString: `String` object, year as text.
- /// - Returns: `Int?`, valid year or `nil`.
- private static func yearInt(from yearString: String?) -> Int? {
- guard let year = yearString, !year.isEmpty,
- let yearInt = Int(year), isYearValid(yearInt) else {
- return nil
- }
-
- return yearInt
- }
-
- /// Formats month int.
- /// - Parameter monthInt: `Int`, should be month.
- /// - Returns: `String` object, formatted month.
- private static func formatMonthString(from monthInt: Int) -> String {
- // Add `0` for month less than 10.
- let monthString = monthInt < 10 ? "0\(monthInt)" : "\(monthInt)"
- return monthString
- }
-}
diff --git a/Sources/VGSCardScanCollector/CardDataMappers/VGSBouncerExpirationDate.swift b/Sources/VGSCardScanCollector/CardDataMappers/VGSBouncerExpirationDate.swift
deleted file mode 100644
index 3eaed19f..00000000
--- a/Sources/VGSCardScanCollector/CardDataMappers/VGSBouncerExpirationDate.swift
+++ /dev/null
@@ -1,17 +0,0 @@
-//
-// VGSBouncerExpirationDate.swift
-// VGSCardScanCollector
-//
-
-import Foundation
-
-/// Holds scanned expiration date data.
-/// Bouncer holds scanned data expiration date with two separate strings.
-/// `03/25` on card -> `03` for month, `25` for year. Year is always `YY` in short format.
-internal struct VGSBouncerExpirationDate {
- /// Scanned month.
- internal let monthString: String?
-
- /// Scanned year.
- internal let yearString: String?
-}
diff --git a/Sources/VGSCardScanCollector/VGSCardScanController.swift b/Sources/VGSCardScanCollector/VGSCardScanController.swift
deleted file mode 100644
index afabe558..00000000
--- a/Sources/VGSCardScanCollector/VGSCardScanController.swift
+++ /dev/null
@@ -1,71 +0,0 @@
-//
-// VGSCardScanController.swift
-// VGSCardScanCollector
-//
-// Created by Dima on 18.08.2020.
-// Copyright © 2020 VGS. All rights reserved.
-//
-
-import Foundation
-#if !COCOAPODS
-import VGSCollectSDK
-#endif
-#if os(iOS)
-import UIKit
-#endif
-
-/// Controller responsible for managing CardScan scanner
-public class VGSCardScanController {
-
- // MARK: - Attributes
-
- internal var scanHandler: VGSCardScanHandler?
-
- /// `VGSCardScanControllerDelegate` - handle user interaction with `CardScan` scanner
- public var delegate: VGSCardScanControllerDelegate? {
- set {
- scanHandler?.delegate = newValue
- }
- get {
- return scanHandler?.delegate
- }
- }
-
- // MARK: - Initialization
- /// Initialization
- ///
- /// - Parameters:
- /// - apiKey: key required for CardScan SDK usage.
- /// - delegate: `VGSCardScanControllerDelegate`. Default is `nil`.
- @available(*, deprecated, message: "CardScan support will be droped in future versions, migrate to card.io.")
- public required init(apiKey: String, delegate: VGSCardScanControllerDelegate? = nil) {
-
- self.scanHandler = VGSCardScanHandler(apiKey: apiKey)
- self.delegate = delegate
- }
-
- // MARK: - Methods
-
- /// Present `CardScan` scanner.
- /// - Parameters:
- /// - viewController: `UIViewController` that will present card scanner.
- /// - animated: pass `true` to animate the presentation; otherwise, pass `false`.
- /// - completion: the block to execute after the presentation finishes.
- public func presentCardScanner(on viewController: UIViewController, animated: Bool, completion: (() -> Void)?) {
- scanHandler?.presentScanVC(on: viewController, animated: animated, completion: completion)
- }
-
- /// Dismiss `CardScan` scanner.
- ///
- /// - Parameters:
- /// - animated: pass `true` to animate the dismiss of presented viewcontroller; otherwise, pass `false`.
- /// - completion: the block to execute after the dismiss finishes.
- public func dismissCardScanner(animated: Bool, completion: (() -> Void)?) {
- scanHandler?.dismissScanVC(animated: animated, completion: completion)
- }
-
- /// Check if CardScan can run on current device.
- static public func isCompatible() -> Bool {
- return VGSCardScanHandler.isCompatible()
- }
-}
diff --git a/Sources/VGSCardScanCollector/VGSCardScanControllerDelegate.swift b/Sources/VGSCardScanCollector/VGSCardScanControllerDelegate.swift
deleted file mode 100644
index a009711f..00000000
--- a/Sources/VGSCardScanCollector/VGSCardScanControllerDelegate.swift
+++ /dev/null
@@ -1,65 +0,0 @@
-//
-// VGSCardScanControllerDelegate.swift
-// VGSCardScanCollector
-//
-// Created by Dima on 31.08.2020.
-// Copyright © 2020 VGS. All rights reserved.
-//
-
-import Foundation
-#if !COCOAPODS
-import VGSCollectSDK
-#endif
-#if os(iOS)
-import UIKit
-#endif
-
-/// Supported scan data fields by CardScan
-@objc
-public enum CradScanDataType: Int {
-
- /// Credit Card Number. Digits string.
- case cardNumber
-
- /// Credit Card Expiration Date. String in format "01/21".
- case expirationDate
-
- /// Credit Card Expiration Month. String in format "01".
- case expirationMonth
-
- /// Credit Card Expiration Year. String in format "21".
- case expirationYear
-
- /// Credit Card Expiration Date. String in format "01/2021".
- case expirationDateLong
-
- /// Credit Card Expiration Year. String in format "2021".
- case expirationYearLong
-
- /// Card holder name displayed on card.
- case name
-
- /// Credit Card Expiration Date. String in format "21/01".
- case expirationDateShortYearThenMonth
-
- /// Credit Card Expiration Date. String in format "2021/01".
- case expirationDateLongYearThenMonth
-}
-
-/// Delegates produced by `VGSCardScanController` instance.
-@objc
-public protocol VGSCardScanControllerDelegate {
-
- // MARK: - Handle user ineraction with `CardScan`
-
- /// On user confirm scanned data by selecting Done button on `CardScan` screen.
- @objc func userDidFinishScan()
-
- /// On user press Cancel buttonn on `CardScan` screen.
- @objc func userDidCancelScan()
-
- // MARK: - Manage scanned data
-
- /// Asks `VGSTextField` where scanned data with `VGSConfiguration.FieldType` need to be set. Called after user select Done button, just before userDidFinishScan() delegate.
- @objc func textFieldForScannedData(type: CradScanDataType) -> VGSTextField?
-}
diff --git a/Sources/VGSCardScanCollector/VGSCardScanHandler.swift b/Sources/VGSCardScanCollector/VGSCardScanHandler.swift
deleted file mode 100644
index 3f666641..00000000
--- a/Sources/VGSCardScanCollector/VGSCardScanHandler.swift
+++ /dev/null
@@ -1,127 +0,0 @@
-//
-// VGSCardScanHandler.swift
-// VGSCardScanCollector
-//
-// Created by Dima on 18.08.2020.
-// Copyright © 2020 VGS. All rights reserved.
-//
-
-import Foundation
-import CardScan
-#if !COCOAPODS
-import VGSCollectSDK
-#endif
-#if os(iOS)
-import UIKit
-#endif
-
-internal class VGSCardScanHandler: NSObject, VGSScanHandlerProtocol {
-
- weak var delegate: VGSCardScanControllerDelegate?
- weak var view: UIViewController?
-
- required init(apiKey: String) {
- super.init()
-
- if #available(iOS 11.2, *) {
- ScanViewController.configure(apiKey: apiKey)
- } else {
- print("⚠️ Unsupported iOS version, should be iOS 11.2 or higher.")
- }
- }
-
- func presentScanVC(on viewController: UIViewController, animated: Bool, modalPresentationStyle: UIModalPresentationStyle = .overCurrentContext, completion: (() -> Void)?) {
-
- if #available(iOS 11.2, *) {
- guard let vc = ScanViewController.createViewController(withDelegate: self) else {
- print("⚠️ This device is incompatible with CardScan.")
- return
- }
- print("ScanViewController.version(): \(ScanViewController.version())")
- self.view = vc
- vc.scanDelegate = self
- viewController.present(vc, animated: animated, completion: completion)
- } else {
- print("⚠️ Unsupported iOS version, should be iOS 11.2 or higher.")
- }
- }
-
- func dismissScanVC(animated: Bool, completion: (() -> Void)?) {
- view?.dismiss(animated: animated, completion: completion)
- }
-
- static func isCompatible() -> Bool {
- if #available(iOS 11.2, *) {
- return ScanViewController.isCompatible()
- } else {
- print("⚠️ Unsupported iOS version, should be iOS 11.2 or higher.")
- return false
- }
- }
-}
-
-/// :nodoc:
-
-@available(iOS 11.2, *)
-extension VGSCardScanHandler: ScanDelegate {
- func userDidCancel(_ scanViewController: ScanViewController) {
- VGSAnalyticsClient.shared.trackEvent(.scan, status: .cancel, extraData: [ "scannerType": "Bouncer"])
- delegate?.userDidCancelScan()
- }
-
- /// :nodoc:
- func userDidScanCard(_ scanViewController: ScanViewController, creditCard: CreditCard) {
- guard let cardScanDelegate = delegate else {
- return
- }
-
- if !creditCard.number.isEmpty, let textfield = cardScanDelegate.textFieldForScannedData(type: .cardNumber) {
-
- if let form = textfield.configuration?.vgsCollector {
- VGSAnalyticsClient.shared.trackFormEvent(form.formAnalyticsDetails, type: .scan, status: .success, extraData: [ "scannerType": "Bouncer"])
- }
- textfield.setText(creditCard.number)
- }
- if let name = creditCard.name, !name.isEmpty, let textfield =
- cardScanDelegate.textFieldForScannedData(type: .name) {
- textfield.setText(name)
- }
-
- let expiryDateData = VGSBouncerExpirationDate(monthString: creditCard.expiryMonth, yearString: creditCard.expiryYear)
-
- if let defaultExpirationDate = VGSBouncerDataMapUtils.mapCardExpirationData(expiryDateData, scannedDataType: .expirationDate), let textfield = cardScanDelegate.textFieldForScannedData(type: .expirationDate) {
- textfield.setText(defaultExpirationDate)
- }
-
- if let longExpirationDate = VGSBouncerDataMapUtils.mapCardExpirationData(expiryDateData, scannedDataType: .expirationDateLong), let textfield = cardScanDelegate.textFieldForScannedData(type: .expirationDateLong) {
- textfield.setText(longExpirationDate)
- }
-
- if let shortExpirationDateWithYearFirst = VGSBouncerDataMapUtils.mapCardExpirationData(expiryDateData, scannedDataType: .expirationDateShortYearThenMonth), let textfield = cardScanDelegate.textFieldForScannedData(type: .expirationDateShortYearThenMonth) {
- textfield.setText(shortExpirationDateWithYearFirst)
- }
-
- if let longExpirationDateWithYearFirst = VGSBouncerDataMapUtils.mapCardExpirationData(expiryDateData, scannedDataType: .expirationDateLongYearThenMonth), let textfield = cardScanDelegate.textFieldForScannedData(type: .expirationDateLongYearThenMonth) {
- textfield.setText(longExpirationDateWithYearFirst)
- }
-
- if let expiryMonth = VGSBouncerDataMapUtils.mapCardExpirationData(expiryDateData, scannedDataType: .expirationMonth), let textfield = cardScanDelegate.textFieldForScannedData(type: .expirationMonth) {
- textfield.setText(expiryMonth)
- }
-
- if let expiryYear = VGSBouncerDataMapUtils.mapCardExpirationData(expiryDateData, scannedDataType: .expirationYear), let textfield = cardScanDelegate.textFieldForScannedData(type: .expirationYear) {
- textfield.setText(expiryYear)
- }
-
- if let expiryYearLong = VGSBouncerDataMapUtils.mapCardExpirationData(expiryDateData, scannedDataType: .expirationYearLong), let textfield = cardScanDelegate.textFieldForScannedData(type: .expirationYearLong) {
- textfield.setText(expiryYearLong)
- }
-
- cardScanDelegate.userDidFinishScan()
- }
-
- /// :nodoc:
- func userDidSkip(_ scanViewController: ScanViewController) {
- delegate?.userDidCancelScan()
- }
-}
diff --git a/Sources/VGSCollectSDK/UIElements/Text Field/VGSCVCTextField.swift b/Sources/VGSCollectSDK/UIElements/Text Field/VGSCVCTextField.swift
index 056b826d..4c8e5d1b 100644
--- a/Sources/VGSCollectSDK/UIElements/Text Field/VGSCVCTextField.swift
+++ b/Sources/VGSCollectSDK/UIElements/Text Field/VGSCVCTextField.swift
@@ -138,11 +138,9 @@ internal extension VGSCVCTextField {
func updatecvcIconViewSize() {
if let widthConstraint = cvcIconImageView.constraints.filter({ $0.identifier == "widthConstraint" }).first {
widthConstraint.constant = cvcIconSize.width
- print("widthConstraint.constant: \(widthConstraint.constant)")
}
if let heightConstraint = cvcIconImageView.constraints.filter({ $0.identifier == "heightConstraint" }).first {
heightConstraint.constant = cvcIconSize.height
- print("heightConstraint.constant: \(heightConstraint.constant)")
}
}
diff --git a/Sources/VGSCollectSDK/Utils/Extensions/Utils.swift b/Sources/VGSCollectSDK/Utils/Extensions/Utils.swift
index 469d4008..9cc3dacb 100644
--- a/Sources/VGSCollectSDK/Utils/Extensions/Utils.swift
+++ b/Sources/VGSCollectSDK/Utils/Extensions/Utils.swift
@@ -7,7 +7,6 @@
//
import Foundation
-@testable import VGSCollectSDK
/// Merge two objects and their nested values. Returns [String: Any]. Values in d2 will override values in d1 if keys are same!!!!
func deepMerge(_ d1: [String: Any], _ d2: [String: Any]) -> [String: Any] {
@@ -47,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.11.3"
+ static let vgsCollectVersion: String = "1.12.0"
}
extension Dictionary {
diff --git a/Tests/FrameworkTests/Satellite Tests/Text Fields Tests/ExpDateTextField.swift b/Tests/FrameworkTests/Satellite Tests/Text Fields Tests/ExpDateTextField.swift
index a98a0350..61c0a477 100644
--- a/Tests/FrameworkTests/Satellite Tests/Text Fields Tests/ExpDateTextField.swift
+++ b/Tests/FrameworkTests/Satellite Tests/Text Fields Tests/ExpDateTextField.swift
@@ -2,9 +2,6 @@
// ExpDateTextField.swift
// FrameworkTests
//
-// Created by Vitalii Obertynskyi on 17.06.2020.
-// Copyright © 2020 VGS. All rights reserved.
-//
import XCTest
@testable import VGSCollectSDK
@@ -83,4 +80,26 @@ class ExpDateTextField: VGSCollectBaseTestCase {
XCTAssertTrue(Int(monthComponent) == currentMonth)
XCTAssertTrue(Int(yearComponent) == currentYear)
}
+
+ func testExpDateKeyboardConfiguration() {
+ let customExpDateConfiguration = VGSExpDateConfiguration(collector: collector, fieldName: "textField")
+ customExpDateConfiguration.inputSource = .keyboard
+ customExpDateConfiguration.keyboardType = .namePhonePad
+ customExpDateConfiguration.returnKeyType = .go
+ customExpDateConfiguration.keyboardAppearance = .dark
+ textField.configuration = customExpDateConfiguration
+
+ XCTAssertTrue(textField.textField.keyboardType == customExpDateConfiguration.keyboardType, "Wrong keyboardType!")
+ XCTAssertTrue(textField.textField.returnKeyType == customExpDateConfiguration.returnKeyType, "Wrong returnKeyType!")
+ XCTAssertTrue(textField.textField.keyboardAppearance == customExpDateConfiguration.keyboardAppearance, "Wrong keyboardAppearance!")
+ }
+
+ func testExpDateConfigurationWithDatePicker() {
+ let customExpDateConfiguration = VGSExpDateConfiguration(collector: collector, fieldName: "textField")
+ customExpDateConfiguration.inputSource = .datePicker
+ textField.configuration = customExpDateConfiguration
+
+ XCTAssertTrue(textField.textField.inputView != nil, "Date picker not set!")
+ XCTAssertTrue(textField.textField.inputView is UIPickerView, "Wrong date picker view!")
+ }
}
diff --git a/Tests/FrameworkTests/Satellite Tests/Text Fields Tests/ExpDateTextFieldTests.swift b/Tests/FrameworkTests/Satellite Tests/Text Fields Tests/ExpDateTextFieldTests.swift
index 0b07f69c..c26ab187 100644
--- a/Tests/FrameworkTests/Satellite Tests/Text Fields Tests/ExpDateTextFieldTests.swift
+++ b/Tests/FrameworkTests/Satellite Tests/Text Fields Tests/ExpDateTextFieldTests.swift
@@ -2,10 +2,6 @@
// ExpDateTextFieldTests.swift
// FrameworkTests
//
-// Created by Vitalii Obertynskyi on 9/19/19.
-// Copyright © 2019 Vitalii Obertynskyi. All rights reserved.
-//
-
import XCTest
@testable import VGSCollectSDK
@@ -140,4 +136,5 @@ class ExpDateTextFieldTests: VGSCollectBaseTestCase {
XCTAssertFalse(expDateTextField.state.isEmpty)
}
}
+
}
diff --git a/Tests/FrameworkTests/Satellite Tests/Text Fields Tests/VGSTextFieldTests.swift b/Tests/FrameworkTests/Satellite Tests/Text Fields Tests/VGSTextFieldTests.swift
index 36f7b6fa..010ea659 100644
--- a/Tests/FrameworkTests/Satellite Tests/Text Fields Tests/VGSTextFieldTests.swift
+++ b/Tests/FrameworkTests/Satellite Tests/Text Fields Tests/VGSTextFieldTests.swift
@@ -40,9 +40,12 @@ class VGSTextFieldTests: VGSCollectBaseTestCase {
}
func testSetText() {
+ let placeholder = "card numner"
configuration.type = .cardNumber
textfield.configuration = configuration
+ textfield.placeholder = placeholder
textfield.setText("4111111111111111")
+ XCTAssertTrue(textfield.textField.placeholder == placeholder)
XCTAssertTrue(textfield.textField.secureText == "4111 1111 1111 1111")
XCTAssertTrue(textfield.state.inputLength == 16)
XCTAssertTrue(textfield.state.isDirty == true)
diff --git a/Tests/FrameworkTests/TokenizationTests/VGSTokenizationConfigurationTests.swift b/Tests/FrameworkTests/TokenizationTests/VGSTokenizationConfigurationTests.swift
new file mode 100644
index 00000000..7dbc37f9
--- /dev/null
+++ b/Tests/FrameworkTests/TokenizationTests/VGSTokenizationConfigurationTests.swift
@@ -0,0 +1,162 @@
+//
+// VGSTokenizationConfigurationsTest.swift
+// FrameworkTests
+//
+
+import Foundation
+
+import Foundation
+import XCTest
+@testable import VGSCollectSDK
+
+class VGSTokenizationConfigurationsTest: VGSCollectBaseTestCase {
+
+ var textField: VGSTextField!
+ var collector = VGSCollect(id: "testVaultId")
+
+
+ override func tearDown() {
+ super.tearDown()
+ textField = nil
+ collector.unregisterAllFiles()
+ }
+
+ func testTokenizationConfiguration() {
+ // set configuration
+ let configuration = VGSTokenizationConfiguration(collector: collector, fieldName: "fieldName")
+ textField = VGSTextField()
+ textField.configuration = configuration
+
+ // Test field type is correct
+ XCTAssertTrue(textField.fieldType == configuration.type, "Error: wrong field type: \(textField.fieldType). Should be: \(configuration.type)")
+
+ // Test tokenization parameters
+ guard let tokenizationConfiguration = textField.configuration as? VGSTokenizationConfiguration else {
+ XCTFail("Wrong configuration type, should be `VGSTokenizationConfiguration`")
+ return
+ }
+ let defaultTokenizationParams = VGSTokenizationParameters()
+ XCTAssertTrue(tokenizationConfiguration.tokenizationParameters.format == defaultTokenizationParams.format, "Error: wrong tokenization format!")
+ XCTAssertTrue(tokenizationConfiguration.tokenizationParameters.storage == defaultTokenizationParams.storage, "Error: wrong tokenization storage!")
+
+ // Test custom tokenization parameters
+ var customTokenizationParams = VGSTokenizationParameters()
+ customTokenizationParams.storage = VGSVaultStorageType.VOLATILE.rawValue
+ customTokenizationParams.format = VGSVaultAliasFormat.FPE_T_FOUR.rawValue
+ configuration.tokenizationParameters = customTokenizationParams
+ textField.configuration = configuration
+
+ guard let customTokenizationConfiguration = textField.configuration as? VGSTokenizationConfiguration else {
+ XCTFail("Wrong configuration type, should be `VGSTokenizationConfiguration`")
+ return
+ }
+ XCTAssertTrue(customTokenizationConfiguration.tokenizationParameters.format == customTokenizationParams.format, "Error: wrong custom tokenization format!")
+ XCTAssertTrue(tokenizationConfiguration.tokenizationParameters.storage == customTokenizationParams.storage, "Error: wrong custom tokenization storage!")
+ }
+
+ func testCVCTokenizationConfiguration() {
+ // set configuration
+ let configuration = VGSCVCTokenizationConfiguration(collector: collector, fieldName: "fieldName")
+ textField = VGSCVCTextField()
+ textField.configuration = configuration
+
+ // Test field type is correct
+ XCTAssertTrue(textField.fieldType == configuration.type, "Error: wrong field type: \(textField.fieldType). Should be: \(configuration.type)")
+
+ // Test tokenization parameters
+ guard let tokenizationConfiguration = textField.configuration as? VGSCVCTokenizationConfiguration else {
+ XCTFail("Wrong configuration type, should be `VGSCVCTokenizationConfiguration`")
+ return
+ }
+ let defaultTokenizationParams = VGSCVCTokenizationParameters()
+ XCTAssertTrue(tokenizationConfiguration.tokenizationParameters.format == defaultTokenizationParams.format, "Error: wrong tokenization format!")
+ XCTAssertTrue(tokenizationConfiguration.tokenizationParameters.storage == defaultTokenizationParams.storage, "Error: wrong tokenization storage!")
+ }
+
+ func testExpDateTokenizationConfiguration() {
+ textField = VGSExpDateTextField()
+
+ // set configuration
+ let configuration = VGSExpDateTokenizationConfiguration(collector: collector, fieldName: "fieldName")
+ configuration.inputSource = .keyboard
+ configuration.keyboardType = .namePhonePad
+ configuration.returnKeyType = .go
+ configuration.keyboardAppearance = .dark
+
+ textField.configuration = configuration
+
+ // Test field type is correct
+ XCTAssertTrue(textField.fieldType == configuration.type, "Error: wrong field type: \(textField.fieldType). Should be: \(configuration.type)")
+
+ // Test tokenization parameters
+ guard let tokenizationConfiguration = textField.configuration as? VGSExpDateTokenizationConfiguration else {
+ XCTFail("Wrong configuration type, should be `VGSExpDateTokenizationConfiguration`")
+ return
+ }
+ let defaultTokenizationParams = VGSExpDateTokenizationParameters()
+ XCTAssertTrue(tokenizationConfiguration.tokenizationParameters.format == defaultTokenizationParams.format, "Error: wrong tokenization format!")
+ XCTAssertTrue(tokenizationConfiguration.tokenizationParameters.storage == defaultTokenizationParams.storage, "Error: wrong tokenization storage!")
+
+ // Test configuration params
+ XCTAssertTrue(textField.textField.keyboardType == configuration.keyboardType, "Wrong keyboardType!")
+ XCTAssertTrue(textField.textField.returnKeyType == configuration.returnKeyType, "Wrong returnKeyType!")
+ XCTAssertTrue(textField.textField.keyboardAppearance == configuration.keyboardAppearance, "Wrong keyboardAppearance!")
+ }
+
+ func testCardNumberTokenizationConfiguration() {
+ // set configuration
+ let configuration = VGSCardNumberTokenizationConfiguration(collector: collector, fieldName: "fieldName")
+ textField = VGSCardTextField()
+ textField.configuration = configuration
+
+ // Test field type is correct
+ XCTAssertTrue(textField.fieldType == configuration.type, "Error: wrong field type: \(textField.fieldType). Should be: \(configuration.type)")
+
+ // Test tokenization parameters
+ guard let tokenizationConfiguration = textField.configuration as? VGSCardNumberTokenizationConfiguration else {
+ XCTFail("Wrong configuration type, should be `VGSCardNumberTokenizationConfiguration`")
+ return
+ }
+ let defaultTokenizationParams = VGSCardNumberTokenizationParameters()
+ XCTAssertTrue(tokenizationConfiguration.tokenizationParameters.format == defaultTokenizationParams.format, "Error: wrong tokenization format!")
+ XCTAssertTrue(tokenizationConfiguration.tokenizationParameters.storage == defaultTokenizationParams.storage, "Error: wrong tokenization storage!")
+ }
+
+ func testCardHolderNameTokenizationConfiguration() {
+ // set configuration
+ let configuration = VGSCardHolderNameTokenizationConfiguration(collector: collector, fieldName: "fieldName")
+ textField = VGSTextField()
+ textField.configuration = configuration
+
+ // Test field type is correct
+ XCTAssertTrue(textField.fieldType == configuration.type, "Error: wrong field type: \(textField.fieldType). Should be: \(configuration.type)")
+
+ // Test tokenization parameters
+ guard let tokenizationConfiguration = textField.configuration as? VGSCardHolderNameTokenizationConfiguration else {
+ XCTFail("Wrong configuration type, should be `VGSCardHolderNameTokenizationConfiguration`")
+ return
+ }
+ let defaultTokenizationParams = VGSCardHolderNameTokenizationParameters()
+ XCTAssertTrue(tokenizationConfiguration.tokenizationParameters.format == defaultTokenizationParams.format, "Error: wrong tokenization format!")
+ XCTAssertTrue(tokenizationConfiguration.tokenizationParameters.storage == defaultTokenizationParams.storage, "Error: wrong tokenization storage!")
+ }
+
+ func testSSNTokenizationConfiguration() {
+ // set configuration
+ let configuration = VGSSSNTokenizationConfiguration(collector: collector, fieldName: "fieldName")
+ textField = VGSTextField()
+ textField.configuration = configuration
+
+ // Test field type is correct
+ XCTAssertTrue(textField.fieldType == configuration.type, "Error: wrong field type: \(textField.fieldType). Should be: \(configuration.type)")
+
+ // Test tokenization parameters
+ guard let tokenizationConfiguration = textField.configuration as? VGSSSNTokenizationConfiguration else {
+ XCTFail("Wrong configuration type, should be `VGSSSNTokenizationConfiguration`")
+ return
+ }
+ let defaultTokenizationParams = VGSSSNTokenizationParameters()
+ XCTAssertTrue(tokenizationConfiguration.tokenizationParameters.format == defaultTokenizationParams.format, "Error: wrong tokenization format!")
+ XCTAssertTrue(tokenizationConfiguration.tokenizationParameters.storage == defaultTokenizationParams.storage, "Error: wrong tokenization storage!")
+ }
+}
diff --git a/Tests/FrameworkTests/TokenizationTests/VGSTokenizationResponseMappingTests.swift b/Tests/FrameworkTests/TokenizationTests/VGSTokenizationResponseMappingTests.swift
index 5d07bca6..75ce8a44 100644
--- a/Tests/FrameworkTests/TokenizationTests/VGSTokenizationResponseMappingTests.swift
+++ b/Tests/FrameworkTests/TokenizationTests/VGSTokenizationResponseMappingTests.swift
@@ -127,13 +127,13 @@ class VGSTokenizationResponseMappingTests: VGSCollectBaseTestCase {
let fieldName = textFieldData.fieldName
switch fieldName {
case "card_number":
- var config = VGSCardNumberTokenizationConfiguration(collector: collector, fieldName: fieldName)
+ let config = VGSCardNumberTokenizationConfiguration(collector: collector, fieldName: fieldName)
config.tokenizationParameters.format = textFieldData.storage
config.tokenizationParameters.format = textFieldData.format
cardNumberTextField.configuration = config
cardNumberTextField.setText(textFieldData.inputValue)
case "exp_date":
- var config = VGSExpDateTokenizationConfiguration(collector: collector, fieldName: fieldName)
+ let config = VGSExpDateTokenizationConfiguration(collector: collector, fieldName: fieldName)
if textFieldData.isSerializationEnabled {
config.serializers = [VGSExpDateSeparateSerializer(monthFieldName: "month", yearFieldName: "year")]
@@ -147,7 +147,7 @@ class VGSTokenizationResponseMappingTests: VGSCollectBaseTestCase {
expDateTextField.configuration = config
expDateTextField.setText(textFieldData.inputValue)
case "cvc":
- var config = VGSCVCTokenizationConfiguration(collector: collector, fieldName: fieldName)
+ let config = VGSCVCTokenizationConfiguration(collector: collector, fieldName: fieldName)
config.tokenizationParameters.format = textFieldData.storage
config.tokenizationParameters.format = textFieldData.format
cvcTextField.configuration = config
@@ -171,7 +171,7 @@ class VGSTokenizationResponseMappingTests: VGSCollectBaseTestCase {
return
}
- XCTAssertTrue(json == expectedJSON, "Tokenization data mapping error:\n - Index: \(index)\n - Actual JSON: \(actualJSON)\n - Expected: \(expectedJSON)")
+ XCTAssertTrue(json == expectedJSON, "Tokenization data mapping error:\n - Index: \(index)\n - Actual JSON: \(String(describing: actualJSON))\n - Expected: \(expectedJSON)")
}
}
diff --git a/Tests/FrameworkTests/VGSCollectTests.swift b/Tests/FrameworkTests/VGSCollectTests.swift
index fdb21971..a0d27dc2 100644
--- a/Tests/FrameworkTests/VGSCollectTests.swift
+++ b/Tests/FrameworkTests/VGSCollectTests.swift
@@ -2,9 +2,6 @@
// FormTests.swift
// FrameworkTests
//
-// Created by Vitalii Obertynskyi on 9/17/19.
-// Copyright © 2019 Vitalii Obertynskyi. All rights reserved.
-//
import XCTest
@testable import VGSCollectSDK
@@ -180,6 +177,22 @@ class VGSCollectTests: VGSCollectBaseTestCase {
XCTAssertTrue(collector.storage.textFields.count == 0)
XCTAssertTrue(collector.textFields.count == 0)
}
+
+ func testUnassignAllTextFields() {
+ let config = VGSConfiguration(collector: collector, fieldName: "test")
+ let tf1 = VGSTextField()
+ tf1.configuration = config
+
+ let tf2 = VGSExpDateTextField()
+ tf2.configuration = config
+
+ XCTAssertTrue(collector.storage.textFields.count == 2)
+ XCTAssertTrue(collector.textFields.count == 2)
+
+ collector.unsubscribeAllTextFields()
+ XCTAssertTrue(collector.storage.textFields.count == 0)
+ XCTAssertTrue(collector.textFields.count == 0)
+ }
func testCustomJsonMapping() {
let cardConfiguration = VGSConfiguration(collector: collector, fieldName: "user.card_data.card_number")
diff --git a/VGSCardScanCollector/Info.plist b/VGSBlinkCardCollector/Info.plist
similarity index 100%
rename from VGSCardScanCollector/Info.plist
rename to VGSBlinkCardCollector/Info.plist
diff --git a/VGSBlinkCardCollector/VGSBlinkCardCollector.h b/VGSBlinkCardCollector/VGSBlinkCardCollector.h
new file mode 100644
index 00000000..c20d34e9
--- /dev/null
+++ b/VGSBlinkCardCollector/VGSBlinkCardCollector.h
@@ -0,0 +1,19 @@
+//
+// VGSBlinkCardCollector.h
+// VGSBlinkCardCollector
+//
+// Created by Dmytro on 26.09.2022.
+// Copyright © 2022 VGS. All rights reserved.
+//
+
+#import
+
+//! Project version number for VGSBlinkCardCollector.
+FOUNDATION_EXPORT double VGSBlinkCardCollectorVersionNumber;
+
+//! Project version string for VGSBlinkCardCollector.
+FOUNDATION_EXPORT const unsigned char VGSBlinkCardCollectorVersionString[];
+
+// In this header, you should import all the public headers of your framework using statements like #import
+
+
diff --git a/VGSCardScanCollector/VGSCardScanCollector.h b/VGSCardScanCollector/VGSCardScanCollector.h
deleted file mode 100644
index 5ce4fd54..00000000
--- a/VGSCardScanCollector/VGSCardScanCollector.h
+++ /dev/null
@@ -1,19 +0,0 @@
-//
-// VGSCardScanCollector.h
-// VGSCardScanCollector
-//
-// Created by Dima on 18.08.2020.
-// Copyright © 2020 VGS. All rights reserved.
-//
-
-#import
-
-//! Project version number for VGSCardScanCollector.
-FOUNDATION_EXPORT double VGSCardScanCollectorVersionNumber;
-
-//! Project version string for VGSCardScanCollector.
-FOUNDATION_EXPORT const unsigned char VGSCardScanCollectorVersionString[];
-
-// In this header, you should import all the public headers of your framework using statements like #import
-
-
diff --git a/VGSCardScanCollectorTests/Info.plist b/VGSCardScanCollectorTests/Info.plist
deleted file mode 100644
index 64d65ca4..00000000
--- a/VGSCardScanCollectorTests/Info.plist
+++ /dev/null
@@ -1,22 +0,0 @@
-
-
-
-
- CFBundleDevelopmentRegion
- $(DEVELOPMENT_LANGUAGE)
- CFBundleExecutable
- $(EXECUTABLE_NAME)
- CFBundleIdentifier
- $(PRODUCT_BUNDLE_IDENTIFIER)
- CFBundleInfoDictionaryVersion
- 6.0
- CFBundleName
- $(PRODUCT_NAME)
- CFBundlePackageType
- $(PRODUCT_BUNDLE_PACKAGE_TYPE)
- CFBundleShortVersionString
- 1.0
- CFBundleVersion
- 1
-
-
diff --git a/VGSCardScanCollectorTests/MapExpirationDateTests/VGSBouncerDataMapUtilsTests.swift b/VGSCardScanCollectorTests/MapExpirationDateTests/VGSBouncerDataMapUtilsTests.swift
deleted file mode 100644
index 807f0269..00000000
--- a/VGSCardScanCollectorTests/MapExpirationDateTests/VGSBouncerDataMapUtilsTests.swift
+++ /dev/null
@@ -1,116 +0,0 @@
-//
-// VGSBouncerDataMapUtilsTests.swift
-// VGSCardScanCollectorTests
-//
-
-import Foundation
-import XCTest
-@testable import VGSCollectSDK
-@testable import VGSCardScanCollector
-
-/// Tests for Bouncer data mapping to VGS Collect.
-class VGSBouncerDataMapUtilsTests: XCTestCase {
-
- /// Holds test data.
- struct TestDataMappingItem {
- let format: CradScanDataType
- let expectedText: String
- }
-
- /// Holds set of data to test specific use case.
- struct TestDataItem {
- let items: [TestDataMappingItem]
- let scannedData: VGSBouncerExpirationDate
- }
-
- private let testDataItemLongMonth = TestDataItem(items: [
- TestDataMappingItem(format: .expirationDate, expectedText: "1024"),
-
- TestDataMappingItem(format: .expirationDateLong, expectedText: "102024"),
-
- TestDataMappingItem(format: .expirationMonth, expectedText: "10"),
-
- TestDataMappingItem(format: .expirationYear, expectedText: "24"),
-
- TestDataMappingItem(format: .expirationYearLong, expectedText: "2024"),
- TestDataMappingItem(format: .expirationDateShortYearThenMonth, expectedText: "2410"),
- TestDataMappingItem(format: .expirationDateLongYearThenMonth, expectedText: "202410")],
- scannedData: VGSBouncerExpirationDate(monthString: "10", yearString: "24"))
-
- private let testDataItemShortMonth = TestDataItem(items: [
- TestDataMappingItem(format: .expirationDate, expectedText: "0325"),
-
- TestDataMappingItem(format: .expirationDateLong, expectedText: "032025"),
-
- TestDataMappingItem(format: .expirationMonth, expectedText: "03"),
-
- TestDataMappingItem(format: .expirationYear, expectedText: "25"),
-
- TestDataMappingItem(format: .expirationYearLong, expectedText: "2025"),
- TestDataMappingItem(format: .expirationDateShortYearThenMonth, expectedText: "2503"),
- TestDataMappingItem(format: .expirationDateLongYearThenMonth, expectedText: "202503")
- ],
- scannedData: VGSBouncerExpirationDate(monthString: "03", yearString: "25"))
-
- private let testDataItemMonthFirst = TestDataItem(items: [
- TestDataMappingItem(format: .expirationDate, expectedText: "0125"),
-
- TestDataMappingItem(format: .expirationDateLong, expectedText: "012025"),
-
- TestDataMappingItem(format: .expirationMonth, expectedText: "01"),
-
- TestDataMappingItem(format: .expirationYear, expectedText: "25"),
-
- TestDataMappingItem(format: .expirationYearLong, expectedText: "2025"),
- TestDataMappingItem(format: .expirationDateShortYearThenMonth, expectedText: "2501"),
- TestDataMappingItem(format: .expirationDateLongYearThenMonth, expectedText: "202501")
- ],
- scannedData: VGSBouncerExpirationDate(monthString: "01", yearString: "25"))
-
- private let testDataItemMonthLast = TestDataItem(items: [
- TestDataMappingItem(format: .expirationDate, expectedText: "1225"),
-
- TestDataMappingItem(format: .expirationDateLong, expectedText: "122025"),
-
- TestDataMappingItem(format: .expirationMonth, expectedText: "12"),
-
- TestDataMappingItem(format: .expirationYear, expectedText: "25"),
-
- TestDataMappingItem(format: .expirationYearLong, expectedText: "2025"),
- TestDataMappingItem(format: .expirationDateShortYearThenMonth, expectedText: "2512"),
- TestDataMappingItem(format: .expirationDateLongYearThenMonth, expectedText: "202512")],
- scannedData: VGSBouncerExpirationDate(monthString: "12", yearString: "25"))
-
- /// Tests scan data.
- func testScannedData() {
- verifyScanDataItem(testDataItemShortMonth)
- verifyScanDataItem(testDataItemLongMonth)
- verifyScanDataItem(testDataItemMonthFirst)
- verifyScanDataItem(testDataItemMonthLast)
- }
-
- // MARK: - Helpers
-
- /// Verifies scanned data mapping.
- /// - Parameter testDataItem: `TestDataItem` object, holds test data.
- private func verifyScanDataItem(_ testDataItem: TestDataItem) {
- let scannedData = testDataItem.scannedData
- let testData = testDataItem.items
-
- for index in 0..
+ location = "self:">
diff --git a/VGSCollectSDK.xcodeproj/xcshareddata/xcschemes/VGSCardScanCollector.xcscheme b/VGSCollectSDK.xcodeproj/xcshareddata/xcschemes/VGSBlinkCardCollector.xcscheme
similarity index 70%
rename from VGSCollectSDK.xcodeproj/xcshareddata/xcschemes/VGSCardScanCollector.xcscheme
rename to VGSCollectSDK.xcodeproj/xcshareddata/xcschemes/VGSBlinkCardCollector.xcscheme
index 6a550248..38446ca3 100644
--- a/VGSCollectSDK.xcodeproj/xcshareddata/xcschemes/VGSCardScanCollector.xcscheme
+++ b/VGSCollectSDK.xcodeproj/xcshareddata/xcschemes/VGSBlinkCardCollector.xcscheme
@@ -1,6 +1,6 @@
@@ -28,16 +28,6 @@
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
-
-
-
-
diff --git a/VGSCollectSDK.xcodeproj/xcshareddata/xcschemes/VGSCardScanCollectorTests.xcscheme b/VGSCollectSDK.xcodeproj/xcshareddata/xcschemes/VGSCardIOCollectorTests.xcscheme
similarity index 88%
rename from VGSCollectSDK.xcodeproj/xcshareddata/xcschemes/VGSCardScanCollectorTests.xcscheme
rename to VGSCollectSDK.xcodeproj/xcshareddata/xcschemes/VGSCardIOCollectorTests.xcscheme
index 11da164b..c0e38cdc 100644
--- a/VGSCollectSDK.xcodeproj/xcshareddata/xcschemes/VGSCardScanCollectorTests.xcscheme
+++ b/VGSCollectSDK.xcodeproj/xcshareddata/xcschemes/VGSCardIOCollectorTests.xcscheme
@@ -1,6 +1,6 @@
diff --git a/demoapp/Podfile b/demoapp/Podfile
index b666a5df..09d2926a 100644
--- a/demoapp/Podfile
+++ b/demoapp/Podfile
@@ -11,6 +11,8 @@ target 'demoapp' do
# pod 'VGSCollectSDK'
pod 'VGSCollectSDK', :path => "../"
pod 'VGSCollectSDK/CardIO', :path => "../"
+ pod 'VGSCollectSDK/BlinkCard', :path => "../"
+
end
diff --git a/demoapp/demoapp.xcodeproj/project.pbxproj b/demoapp/demoapp.xcodeproj/project.pbxproj
index 4225d009..35a95d5a 100644
--- a/demoapp/demoapp.xcodeproj/project.pbxproj
+++ b/demoapp/demoapp.xcodeproj/project.pbxproj
@@ -337,10 +337,10 @@
isa = PBXGroup;
children = (
449FFCA728A505A100FE4A1F /* PayOptIntegration */,
- 44D8691226205F670014645F /* CustomPaymentCardsViewController.swift */,
44D8691326205F670014645F /* CardsDataCollectingViewController.swift */,
- 0351799E2858D53C00394BFC /* CardsDataTokenizationViewController.swift */,
+ 44D8691226205F670014645F /* CustomPaymentCardsViewController.swift */,
44D8691426205F670014645F /* SSNCollectingViewController.swift */,
+ 0351799E2858D53C00394BFC /* CardsDataTokenizationViewController.swift */,
44D8691526205F670014645F /* CustomDataCollectingViewController.swift */,
44D8691926205F670014645F /* FilePickerViewController.swift */,
03DBB7B3292FBFDA00F4DCA2 /* CollectApplePayDataViewController.swift */,
@@ -479,7 +479,7 @@
FD12B9702304616C00B670DD /* Sources */,
FD12B9712304616C00B670DD /* Frameworks */,
FD12B9722304616C00B670DD /* Resources */,
- 2D7DDBAB213ED83AEB98C86F /* [CP] Embed Pods Frameworks */,
+ AE13066AE7DCCC2EC779B27C /* [CP] Embed Pods Frameworks */,
);
buildRules = (
);
@@ -625,43 +625,43 @@
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0;
};
- 2D7DDBAB213ED83AEB98C86F /* [CP] Embed Pods Frameworks */ = {
+ A283A473324A83860BE00B1D /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
- "${PODS_ROOT}/Target Support Files/Pods-demoapp/Pods-demoapp-frameworks-${CONFIGURATION}-input-files.xcfilelist",
);
- name = "[CP] Embed Pods Frameworks";
+ inputPaths = (
+ "${PODS_PODFILE_DIR_PATH}/Podfile.lock",
+ "${PODS_ROOT}/Manifest.lock",
+ );
+ name = "[CP] Check Pods Manifest.lock";
outputFileListPaths = (
- "${PODS_ROOT}/Target Support Files/Pods-demoapp/Pods-demoapp-frameworks-${CONFIGURATION}-output-files.xcfilelist",
+ );
+ outputPaths = (
+ "$(DERIVED_FILE_DIR)/Pods-demoappUITests-checkManifestLockResult.txt",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
- shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-demoapp/Pods-demoapp-frameworks.sh\"\n";
+ shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0;
};
- A283A473324A83860BE00B1D /* [CP] Check Pods Manifest.lock */ = {
+ AE13066AE7DCCC2EC779B27C /* [CP] Embed Pods Frameworks */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
+ "${PODS_ROOT}/Target Support Files/Pods-demoapp/Pods-demoapp-frameworks-${CONFIGURATION}-input-files.xcfilelist",
);
- inputPaths = (
- "${PODS_PODFILE_DIR_PATH}/Podfile.lock",
- "${PODS_ROOT}/Manifest.lock",
- );
- name = "[CP] Check Pods Manifest.lock";
+ name = "[CP] Embed Pods Frameworks";
outputFileListPaths = (
- );
- outputPaths = (
- "$(DERIVED_FILE_DIR)/Pods-demoappUITests-checkManifestLockResult.txt",
+ "${PODS_ROOT}/Target Support Files/Pods-demoapp/Pods-demoapp-frameworks-${CONFIGURATION}-output-files.xcfilelist",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
- shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
+ shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-demoapp/Pods-demoapp-frameworks.sh\"\n";
showEnvVarsInLog = 0;
};
C3637F86567C1421694D51C5 /* [CP] Check Pods Manifest.lock */ = {
diff --git a/demoapp/demoapp/AppCollectorConfiguration.swift b/demoapp/demoapp/AppCollectorConfiguration.swift
index 1589f4f6..e083ecee 100644
--- a/demoapp/demoapp/AppCollectorConfiguration.swift
+++ b/demoapp/demoapp/AppCollectorConfiguration.swift
@@ -19,6 +19,9 @@ class AppCollectorConfiguration {
/// Set environment - `.sandbox` for testing or `.live` for production
var environment = Environment.sandbox
+
+ /// Set BlinkCard license key to test card scanner
+ var blinkCardLicenseKey: String? = nil
var paymentOrchestrationDefaultRouteId = "4880868f-d88b-4333-ab70-d9deecdbffc4"
diff --git a/demoapp/demoapp/Info.plist b/demoapp/demoapp/Info.plist
index 95c0446f..e4b47a02 100644
--- a/demoapp/demoapp/Info.plist
+++ b/demoapp/demoapp/Info.plist
@@ -31,6 +31,11 @@
<><><><><><><>
NSPhotoLibraryUsageDescription
<><><><><><><>
+ UIApplicationSceneManifest
+
+ UISceneConfigurations
+
+
UILaunchStoryboardName
LaunchScreen
UIMainStoryboardFile
diff --git a/demoapp/demoapp/UseCases/CardsDataCollectingViewController.swift b/demoapp/demoapp/UseCases/CardsDataCollectingViewController.swift
index 129d8018..de1cace1 100644
--- a/demoapp/demoapp/UseCases/CardsDataCollectingViewController.swift
+++ b/demoapp/demoapp/UseCases/CardsDataCollectingViewController.swift
@@ -11,7 +11,7 @@ import VGSCollectSDK
/// A class that demonstrates how to collect data from VGSTextFields and upload it to VGS
class CardsDataCollectingViewController: UIViewController {
-
+
@IBOutlet weak var cardDataStackView: UIStackView!
@IBOutlet weak var consoleStatusLabel: UILabel!
@IBOutlet weak var consoleLabel: UILabel!
@@ -25,65 +25,49 @@ class CardsDataCollectingViewController: UIViewController {
var cvcCardNum = VGSCVCTextField()
var cardHolderName = VGSTextField()
+ /// BlinkCard Card Scanner
+ var scanController: VGSBlinkCardController?
+
var consoleMessage: String = "" {
didSet { consoleLabel.text = consoleMessage }
}
-
- // Init CardScan controller with API KEY. Details: https://cardscan.io
- var scanController = VGSCardIOScanController()
override func viewDidLoad() {
- super.viewDidLoad()
-
- setupUI()
- setupElementsConfiguration()
-
- // set custom headers
- vgsCollect.customHeaders = [
- "my custome header": "some custom data"
- ]
-
- // set VGSCardScanDelegate
- scanController.delegate = self
+ super.viewDidLoad()
- // Observing text fields. The call back return all textfields with updated states. You also can use VGSTextFieldDelegate
- vgsCollect.observeStates = { [weak self] form in
+ setupUI()
+ setupElementsConfiguration()
- self?.consoleMessage = ""
- self?.consoleStatusLabel.text = "STATE"
+ // set custom headers
+ vgsCollect.customHeaders = [
+ "my custome header": "some custom data"
+ ]
+
+ // Observing text fields. The call back return all textfields with updated states. You also can use VGSTextFieldDelegate
+ vgsCollect.observeStates = { [weak self] form in
- form.forEach({ textField in
- self?.consoleMessage.append(textField.state.description)
- self?.consoleMessage.append("\n")
- })
- }
+ self?.consoleMessage = ""
+ self?.consoleStatusLabel.text = "STATE"
+
+ form.forEach({ textField in
+ self?.consoleMessage.append(textField.state.description)
+ self?.consoleMessage.append("\n")
+ })
+ }
-// // If you need to set your own card brand icons
-//
-// VGSPaymentCards.visa.brandIcon = UIImage(named: "my visa icon")
-// VGSPaymentCards.unknown.brandIcon = UIImage(named: "my unknown brand icon")
-//
-// //OR
-//
-// // use the closure below
-// cardNumber.cardsIconSource = { cardBrand in
-// switch cardBrand {
-// case .mastercard:
-// return UIImage(named: "bank_card")
-// case .custom(brandName: let name):
-// switch name {
-// case "Visa2":
-// return UIImage(named: "bank_card")
-// default:
-// return nil
-// }
-// default:
-// return UIImage(named: "cloud-upload")
-//
-// }
-// }
-//
+ // Init VGSBlinkCardController with BlinkCard license key
+ if let licenseKey = AppCollectorConfiguration.shared.blinkCardLicenseKey {
+ scanController = VGSBlinkCardController(licenseKey: licenseKey, delegate: self, onError: { errorCode in
+ print("BlinkCard license error, code: \(errorCode)")
+ })
+ } else {
+ print("⚠️ VGSBlinkCardController not initialized. Check license key!")
+ }
+ // // If you need to set your own card brand icons
+ //
+ // VGSPaymentCards.visa.brandIcon = UIImage(named: "my visa icon")
+ // VGSPaymentCards.unknown.brandIcon = UIImage(named: "my unknown brand icon")
}
override func awakeFromNib() {
@@ -124,7 +108,7 @@ class CardsDataCollectingViewController: UIViewController {
cardNumber.textAlignment = .natural
cardNumber.cardIconLocation = .right
- cardNumber.becomeFirstResponder()
+// cardNumber.becomeFirstResponder()
/// Use `VGSExpDateConfiguration` if you need to convert output date format
let expDateConfiguration = VGSExpDateConfiguration(collector: vgsCollect, fieldName: "card_expirationDate")
expDateConfiguration.type = .expDate
@@ -172,9 +156,13 @@ class CardsDataCollectingViewController: UIViewController {
}
}
- // Start CardIO scanning
+ // Start BlinkCard scanning
@IBAction func scanAction(_ sender: Any) {
- scanController.presentCardScanner(on: self, animated: true, completion: nil)
+ guard let scanController = scanController else {
+ print("⚠️ VGSBlinkCardController not initialized. Check license key!")
+ return
+ }
+ scanController.presentCardScanner(on: self, animated: true, modalPresentationStyle: .fullScreen, completion: nil)
}
// Upload data from TextFields to VGS
@@ -227,7 +215,6 @@ class CardsDataCollectingViewController: UIViewController {
// MARK: - VGSTextFieldDelegate
extension CardsDataCollectingViewController: VGSTextFieldDelegate {
func vgsTextFieldDidChange(_ textField: VGSTextField) {
- print(textField.state.description)
textField.borderColor = textField.state.isValid ? .gray : .red
/// Update CVC field UI in case if valid cvc digits change, e.g.: input card number brand changed form Visa(3 digints CVC) to Amex(4 digits CVC) )
@@ -242,33 +229,32 @@ extension CardsDataCollectingViewController: VGSTextFieldDelegate {
}
}
-// MARK: - VGSCardIOScanControllerDelegate
-extension CardsDataCollectingViewController: VGSCardIOScanControllerDelegate {
-
- //When user press Done button on CardIO screen
- func userDidFinishScan() {
- scanController.dismissCardScanner(animated: true, completion: {
- // add actions on scan controller dismiss completion
- })
- }
-
- //When user press Cancel button on CardIO screen
- func userDidCancelScan() {
- scanController.dismissCardScanner(animated: true, completion: nil)
- }
-
- //Asks VGSTextField where scanned data with type need to be set.
- func textFieldForScannedData(type: CradIODataType) -> VGSTextField? {
+extension CardsDataCollectingViewController: VGSBlinkCardControllerDelegate {
+ func textFieldForScannedData(type: VGSBlinkCardDataType) -> VGSTextField? {
+ // match VGSTextField with scanned data
switch type {
case .expirationDateLong:
return expCardDate
- case .cvc:
- return cvcCardNum
case .cardNumber:
return cardNumber
+ case .cvc:
+ return cvcCardNum
+ case .name:
+ return cardHolderName
default:
return nil
}
}
+
+ func userDidFinishScan() {
+ scanController?.dismissCardScanner(animated: true, completion: {
+ // add actions on scan controller dismiss completion
+ })
+ }
+
+ func userDidCancelScan() {
+ scanController?.dismissCardScanner(animated: true, completion: {
+ // add actions on scan controller dismiss completion
+ })
+ }
}
-
diff --git a/demoapp/demoapp/demoapp.entitlements b/demoapp/demoapp/demoapp.entitlements
index 39542ced..0c67376e 100644
--- a/demoapp/demoapp/demoapp.entitlements
+++ b/demoapp/demoapp/demoapp.entitlements
@@ -1,10 +1,5 @@
-
- com.apple.developer.in-app-payments
-
- merchant.com.vgs-checkout-ios-test
-
-
+
diff --git a/images/VGSCollect_CardScan_SPM_2.png b/images/VGSCollect_CardScan_SPM_2.png
index 7000a385e1c78e0db711ece88d9ebb18eebb93c1..2d8f0ee829968566dc06afa95c7e8db7d9980662 100644
GIT binary patch
literal 193622
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≻eih;hCPgnj
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