Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 46 additions & 8 deletions Adyen.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -428,6 +428,9 @@
A0F4559B295F0F58001742C7 /* MealVoucherPaymentMethod.swift in Sources */ = {isa = PBXBuildFile; fileRef = A0F4559A295F0F58001742C7 /* MealVoucherPaymentMethod.swift */; };
A0F455A12968472B001742C7 /* PartialPaymentMethodDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = A0F455A02968472B001742C7 /* PartialPaymentMethodDetails.swift */; };
A0FA143F26D65A5300627127 /* InstallmentPickerElement.swift in Sources */ = {isa = PBXBuildFile; fileRef = A0FA143E26D65A5300627127 /* InstallmentPickerElement.swift */; };
B605AC0B2D897A930084D583 /* CardScannerControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B605AC092D897A930084D583 /* CardScannerControllerTests.swift */; };
B605AC112D8D988D0084D583 /* AdyenCardScanner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C9FC2C3D2D50D0B700C9D0F3 /* AdyenCardScanner.framework */; };
B605AC122D8D988D0084D583 /* AdyenCardScanner.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = C9FC2C3D2D50D0B700C9D0F3 /* AdyenCardScanner.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
B62A075C2D71FB43006072E7 /* FormCardNumberItemView+ScanCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = B62A075B2D71FB43006072E7 /* FormCardNumberItemView+ScanCard.swift */; };
B62D48B42BBE8DBE001EF01A /* AnalyticsFlavorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C97C16AB280702B200534419 /* AnalyticsFlavorTests.swift */; };
B62D48B52BBE8DBE001EF01A /* AnalyticsEventTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C97C16A928059A5A00534419 /* AnalyticsEventTests.swift */; };
Expand All @@ -438,8 +441,6 @@
B62D48C62BBE8F45001EF01A /* XCTestCase+Wait.swift in Sources */ = {isa = PBXBuildFile; fileRef = B62D48C22BBE8ED6001EF01A /* XCTestCase+Wait.swift */; };
B62D48C82BBE8F47001EF01A /* XCTestCase+Wait.swift in Sources */ = {isa = PBXBuildFile; fileRef = B62D48C22BBE8ED6001EF01A /* XCTestCase+Wait.swift */; };
B639C0822D8039F600472EBB /* CardScannerController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B639C0812D80399800472EBB /* CardScannerController.swift */; };
B639C0832D80456300472EBB /* AdyenCardScanner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C9FC2C3D2D50D0B700C9D0F3 /* AdyenCardScanner.framework */; };
B639C0842D80456300472EBB /* AdyenCardScanner.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = C9FC2C3D2D50D0B700C9D0F3 /* AdyenCardScanner.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
B6ABA1C32CA6B058003514E5 /* ListItemTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6ABA1C22CA6B058003514E5 /* ListItemTests.swift */; };
B6C9DA792D0C3F62005D65C7 /* DualBrandView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6C9DA782D0C3F62005D65C7 /* DualBrandView.swift */; };
B6C9DA7B2D102C91005D65C7 /* DualBrandViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6C9DA7A2D102C91005D65C7 /* DualBrandViewTests.swift */; };
Expand Down Expand Up @@ -1283,7 +1284,14 @@
remoteGlobalIDString = E2C0E03222097917008616F6;
remoteInfo = Adyen;
};
B639C0852D80456300472EBB /* PBXContainerItemProxy */ = {
B605AC0E2D897F960084D583 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = E2C0E02A22097917008616F6 /* Project object */;
proxyType = 1;
remoteGlobalIDString = C9FC2C3C2D50D0B700C9D0F3;
remoteInfo = AdyenCardScanner;
};
B605AC132D8D988D0084D583 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = E2C0E02A22097917008616F6 /* Project object */;
proxyType = 1;
Expand Down Expand Up @@ -1496,6 +1504,17 @@
/* End PBXContainerItemProxy section */

/* Begin PBXCopyFilesBuildPhase section */
B605AC102D897F960084D583 /* Embed Frameworks */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 10;
files = (
B605AC0D2D897F960084D583 /* AdyenCardScanner.framework in Embed Frameworks */,
);
name = "Embed Frameworks";
runOnlyForDeploymentPostprocessing = 0;
};
E2C0E0A8220B0827008616F6 /* Embed Frameworks */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
Expand All @@ -1514,7 +1533,7 @@
E2C0E0AE220B0840008616F6 /* AdyenCard.framework in Embed Frameworks */,
F92980AD27CE2B33000CA5CA /* AdyenSession.framework in Embed Frameworks */,
A020EC4929E6EC4B0050B2FE /* AdyenCashAppPay.framework in Embed Frameworks */,
B639C0842D80456300472EBB /* AdyenCardScanner.framework in Embed Frameworks */,
B605AC122D8D988D0084D583 /* AdyenCardScanner.framework in Embed Frameworks */,
);
name = "Embed Frameworks";
runOnlyForDeploymentPostprocessing = 0;
Expand Down Expand Up @@ -1879,6 +1898,7 @@
A0F4559A295F0F58001742C7 /* MealVoucherPaymentMethod.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MealVoucherPaymentMethod.swift; sourceTree = "<group>"; };
A0F455A02968472B001742C7 /* PartialPaymentMethodDetails.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PartialPaymentMethodDetails.swift; sourceTree = "<group>"; };
A0FA143E26D65A5300627127 /* InstallmentPickerElement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstallmentPickerElement.swift; sourceTree = "<group>"; };
B605AC092D897A930084D583 /* CardScannerControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardScannerControllerTests.swift; sourceTree = "<group>"; };
B62A075B2D71FB43006072E7 /* FormCardNumberItemView+ScanCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FormCardNumberItemView+ScanCard.swift"; sourceTree = "<group>"; };
B62D48AC2BBE8D79001EF01A /* UnitTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = UnitTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
B62D48C22BBE8ED6001EF01A /* XCTestCase+Wait.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "XCTestCase+Wait.swift"; sourceTree = "<group>"; };
Expand Down Expand Up @@ -2640,6 +2660,7 @@
8182AABD2B974E2B0087568E /* AdyenTwint.framework in Frameworks */,
E97B971322980CBA00505476 /* Adyen3DS2.framework in Frameworks */,
81B8036B2BE0E714003D037F /* AdyenWeChatPayInternal in Frameworks */,
B605AC0C2D897F960084D583 /* AdyenCardScanner.framework in Frameworks */,
81B8036B2BE0E714003D037F /* AdyenWeChatPayInternal in Frameworks */,
E97B970E22980C8400505476 /* AdyenDropIn.framework in Frameworks */,
E97B970D22980C7900505476 /* AdyenCard.framework in Frameworks */,
Expand Down Expand Up @@ -2668,7 +2689,7 @@
81088A1F2BDBACB7007FCDB9 /* AdyenTwint.framework in Frameworks */,
F9175EEE2593955C00D653BE /* AdyenComponents.framework in Frameworks */,
F92327C425A46EF0002C5BC4 /* AdyenEncryption.framework in Frameworks */,
B639C0832D80456300472EBB /* AdyenCardScanner.framework in Frameworks */,
B605AC112D8D988D0084D583 /* AdyenCardScanner.framework in Frameworks */,
81A91AC22BEC12A2001E00C8 /* TwintSDK.xcframework in Frameworks */,
F9A2D01225DBF104008944BE /* PassKit.framework in Frameworks */,
F973A9192791C3B0005AA753 /* AdyenActions.framework in Frameworks */,
Expand Down Expand Up @@ -3414,6 +3435,14 @@
path = Installment;
sourceTree = "<group>";
};
B605AC0A2D897A930084D583 /* Card Scanner */ = {
isa = PBXGroup;
children = (
B605AC092D897A930084D583 /* CardScannerControllerTests.swift */,
);
path = "Card Scanner";
sourceTree = "<group>";
};
B62D48AD2BBE8D79001EF01A /* UnitTests */ = {
isa = PBXGroup;
children = (
Expand Down Expand Up @@ -4914,6 +4943,7 @@
F9EDB799239664B500CFB3C9 /* StoredPaymentMethodComponentTests.swift */,
F919DF9824EA64680027976E /* CardPublicKeyProviderTests.swift */,
81D7EC8F2CD24143005159F6 /* FormViewControllerTests.swift */,
B605AC0A2D897A930084D583 /* Card Scanner */,
);
path = "Card Tests";
sourceTree = "<group>";
Expand Down Expand Up @@ -6222,6 +6252,7 @@
E2C0E03822097917008616F6 /* Sources */,
E2C0E03922097917008616F6 /* Frameworks */,
E2C0E03A22097917008616F6 /* Resources */,
B605AC102D897F960084D583 /* Embed Frameworks */,
);
buildRules = (
);
Expand All @@ -6230,6 +6261,7 @@
E97B971222980C8B00505476 /* PBXTargetDependency */,
E2C0E03F22097917008616F6 /* PBXTargetDependency */,
F9D57522237C345C009C18B5 /* PBXTargetDependency */,
B605AC0F2D897F960084D583 /* PBXTargetDependency */,
);
name = IntegrationUIKitTests;
packageProductDependencies = (
Expand Down Expand Up @@ -6285,7 +6317,7 @@
F94D65EA2B036A450095D61E /* PBXTargetDependency */,
81088A222BDBACB7007FCDB9 /* PBXTargetDependency */,
819BFB3E2BDBED960018DC9B /* PBXTargetDependency */,
B639C0862D80456300472EBB /* PBXTargetDependency */,
B605AC142D8D988D0084D583 /* PBXTargetDependency */,
);
name = AdyenUIHost;
packageProductDependencies = (
Expand Down Expand Up @@ -7457,6 +7489,7 @@
F9AC61C0243750D80062A00D /* AppLauncherMock.swift in Sources */,
C9BB460927622F4100E6730B /* BACSConfirmationPresenterTests.swift in Sources */,
C96688BF26A6FC1C00DC7297 /* AffirmComponentTests.swift in Sources */,
B605AC0B2D897A930084D583 /* CardScannerControllerTests.swift in Sources */,
81FC2C8F2BB18F0F007F1316 /* ImageLoaderMock.swift in Sources */,
A0BDF3F22BD29E69001FF7E5 /* PostalCodeValidatorTests.swift in Sources */,
81DA70872BDA6075006CE5D5 /* Twint+Spy.swift in Sources */,
Expand Down Expand Up @@ -8204,10 +8237,15 @@
target = E2C0E03222097917008616F6 /* Adyen */;
targetProxy = A04F8C2D29E5957B00F3F62B /* PBXContainerItemProxy */;
};
B639C0862D80456300472EBB /* PBXTargetDependency */ = {
B605AC0F2D897F960084D583 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = C9FC2C3C2D50D0B700C9D0F3 /* AdyenCardScanner */;
targetProxy = B605AC0E2D897F960084D583 /* PBXContainerItemProxy */;
};
B605AC142D8D988D0084D583 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = C9FC2C3C2D50D0B700C9D0F3 /* AdyenCardScanner */;
targetProxy = B639C0852D80456300472EBB /* PBXContainerItemProxy */;
targetProxy = B605AC132D8D988D0084D583 /* PBXContainerItemProxy */;
};
B6EE0F482BBECBEF00B9810D /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
Expand Down
133 changes: 109 additions & 24 deletions AdyenCard/Components/Card/CardScannerController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,71 +7,156 @@
import Foundation
import UIKit

internal protocol CardScannerControlling {
typealias CardModel = (String?, Date?)

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?
}

init(presenter: UIViewController)
internal protocol CardScannerControlling: CardScannerAvailability {

init(presenter: UIViewController, availabilityProvider: CardScannerAvailability, cardScannerProvider: CardScannerProviding)
func openCardScanner()

var onScanComplete: ((Result<CardModel, Error>) -> Void)? { get set }

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
internal var onScanComplete: ((Result<(String?, Date?), any Error>) -> Void)?

internal init(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, *), AdyenCardScanner.CardScanner.isAvailable { true } else { false }
if #available(iOS 13.0, *), availabilityProvider.isScannerAvailable { true } else { false }
}

internal func openCardScanner() {
let scannerNavigationController = UINavigationController()
guard let scannerViewController = AdyenCardScanner.CardScanner.createCardScanner(completion: { [weak self] result in
let scannerNavigationController = makeNavigationController()
guard let scannerViewController = cardScannerProvider.createCardScanner(completion: { [weak self] result in
guard let self else { return }
self.onScanComplete?(self.map(result))
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<AdyenCardScanner.CardScanDetails, AdyenCardScanner.CardScannerError>) -> Result<CardModel, Error> {

private func map(_ result: Result<CardScanDetails, Error>) -> Result<CardScanDetails, Error> {
switch result {
case let .success(card):
.success((card.number, card.expirationDate))
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<CardModel, any Error>) -> Void)?
internal var onScanComplete: ((Result<CardScanDetails, any Error>) -> Void)?
internal var title: String?
internal func openCardScanner() {}

internal init(presenter: UIViewController) {}
internal init(
presenter: UIViewController,
availabilityProvider: CardScannerAvailability = DummyCardScannerAvailability(),
cardScannerProvider: CardScannerProviding = DummyCardScannerProvider()
) {}

// MARK: - Helpers

internal struct DummyCardScannerAvailability: CardScannerAvailability {
var isScannerAvailable: Bool { false }
}

internal struct DummyCardScannerProvider: CardScannerProviding {
internal func createCardScanner(
completion: @escaping (Result<CardScanDetails, any Error>) -> Void
) -> UIViewController? {
UIViewController()
}
}
}

#endif // canImport(AdyenCardScanner)
3 changes: 2 additions & 1 deletion AdyenCard/Components/Card/CardViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ internal class CardViewController: FormViewController {
private let cardLogos: [FormCardLogosItem.CardTypeLogo]
private lazy var cardScannerController: CardScannerControlling = {
let controller = CardScannerController(presenter: self)
controller.title = localizedString(.scanYourCardButton, localizationParameters)
controller.onScanComplete = { [weak self] result in
self?.handleCardScanningResult(result)
}
Expand Down Expand Up @@ -409,7 +410,7 @@ extension CardViewController: CardViewControllerProtocol {
// MARK: - Card scanner

extension CardViewController {
private func handleCardScanningResult(_ result: Result<(String?, Date?), Error>) {
private func handleCardScanningResult(_ result: Result<CardScanDetails, Error>) {
switch result {
case let .success((number, expiryDate)):
items.numberContainerItem.setCardNumber(number ?? "")
Expand Down
Loading
Loading