-
Notifications
You must be signed in to change notification settings - Fork 135
Expand file tree
/
Copy pathCardScannerController.swift
More file actions
162 lines (128 loc) · 5.97 KB
/
CardScannerController.swift
File metadata and controls
162 lines (128 loc) · 5.97 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
//
// Copyright (c) 2025 Adyen N.V.
//
// This file is open source and available under the MIT license. See the LICENSE file for more info.
//
import Foundation
import UIKit
internal protocol CardScannerAvailability {
var isScannerAvailable: Bool { get }
}
internal typealias CardScanDetails = (number: String?, expirationDate: Date?)
internal protocol CardScannerProviding {
func createCardScanner(completion: @escaping (Result<CardScanDetails, Error>) -> Void) -> UIViewController?
}
internal protocol CardScannerControlling: CardScannerAvailability {
init(presenter: UIViewController, availabilityProvider: CardScannerAvailability, cardScannerProvider: CardScannerProviding)
func openCardScanner()
var title: String? { get set }
var onScanComplete: ((Result<CardScanDetails, Error>) -> Void)? { get set }
}
#if canImport(AdyenCardScanner)
import AdyenCardScanner
private struct CardScannerAvailabilityWrapper: CardScannerAvailability {
var isScannerAvailable: Bool {
AdyenCardScanner.CardScanner.isAvailable
}
}
private struct CardScannerProviderWrapper: CardScannerProviding {
func createCardScanner(completion: @escaping (Result<CardScanDetails, Error>) -> Void) -> UIViewController? {
AdyenCardScanner.CardScanner.createCardScanner { result in
switch result {
case let .success(details): completion(.success((details.number, details.expirationDate)))
case let .failure(error): completion(.failure(error))
}
}
}
}
internal final class CardScannerController: CardScannerControlling {
internal enum CardScannerError: Error {
case scanningError
}
private let presenter: UIViewController
private let availabilityProvider: CardScannerAvailability
private let cardScannerProvider: CardScannerProviding
internal var title: String?
internal var onScanComplete: ((Result<CardScanDetails, Error>) -> Void)?
internal init(
presenter: UIViewController,
availabilityProvider: CardScannerAvailability = CardScannerAvailabilityWrapper(),
cardScannerProvider: CardScannerProviding = CardScannerProviderWrapper()
) {
self.availabilityProvider = availabilityProvider
self.cardScannerProvider = cardScannerProvider
self.presenter = presenter
}
internal var isScannerAvailable: Bool {
if #available(iOS 13.0, *), availabilityProvider.isScannerAvailable { true } else { false }
}
internal func openCardScanner() {
let scannerNavigationController = makeNavigationController()
guard let scannerViewController = cardScannerProvider.createCardScanner(completion: { [weak self] result in
guard let self else { return }
self.onScanComplete?(map(result))
scannerNavigationController.dismiss(animated: true)
}) else { return }
scannerViewController.navigationItem.leftBarButtonItem = makeCancelBarButton()
scannerViewController.title = title
scannerNavigationController.setViewControllers(
[scannerViewController],
animated: false
)
presenter.present(scannerNavigationController, animated: true)
}
// MARK: - Private
private func map(_ result: Result<CardScanDetails, Error>) -> Result<CardScanDetails, Error> {
switch result {
case let .success(cardScanDetails):
.success(cardScanDetails)
case .failure:
.failure(CardScannerError.scanningError)
}
}
private func makeNavigationController() -> UINavigationController {
guard #available(iOS 13.0, *) else { return UINavigationController() }
let appearance = UINavigationBarAppearance()
appearance.configureWithDefaultBackground()
let navigationController = UINavigationController()
navigationController.navigationBar.standardAppearance = appearance
navigationController.navigationBar.compactAppearance = appearance
navigationController.navigationBar.scrollEdgeAppearance = appearance
return navigationController
}
private func makeCancelBarButton() -> UIBarButtonItem {
UIBarButtonItem(barButtonSystemItem: .cancel, target: self, action: #selector(handleCardScanningCancelation))
}
@objc
private func handleCardScanningCancelation() {
handleCardScanningCancelationWithCompletion(nil)
}
@objc
internal func handleCardScanningCancelationWithCompletion(_ completion: (() -> Void)?) {
presenter.presentedViewController?.dismiss(animated: true, completion: completion)
}
}
#else // canImport(AdyenCardScanner)
internal final class CardScannerController: CardScannerControlling {
internal var isScannerAvailable: Bool { false }
internal var onScanComplete: ((Result<CardScanDetails, any Error>) -> Void)?
internal var title: String?
internal func openCardScanner() {}
internal init(
presenter: UIViewController,
availabilityProvider: CardScannerAvailability = DummyCardScannerAvailability(),
cardScannerProvider: CardScannerProviding = DummyCardScannerProvider()
) {}
// MARK: - Helpers
internal struct DummyCardScannerAvailability: CardScannerAvailability {
internal var isScannerAvailable: Bool { false }
}
internal struct DummyCardScannerProvider: CardScannerProviding {
internal func createCardScanner(
completion: @escaping (Result<CardScanDetails, any Error>) -> Void
) -> UIViewController? {
UIViewController()
}
}
}
#endif // canImport(AdyenCardScanner)