Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
42a6f76
Add Cancel button and title to card scanning screen navigation bar
atmamont Mar 17, 2025
8a0ac0b
Code cleanup
atmamont Mar 17, 2025
1da6587
Use variable instead of init parameter for Card scanner screen title
atmamont Mar 20, 2025
cc8f01c
Add tests for CardScannerController
atmamont Mar 20, 2025
0ae27a2
Merge branch 'COIOS-826_OCR_feature' into feature/OCR-add-cancel-button
atmamont Mar 20, 2025
32f65bf
Fix the no-framework-available version
atmamont Mar 20, 2025
a48c79f
Merge branch 'COIOS-826_OCR_feature' of github.com:Adyen/adyen-ios in…
atmamont Mar 21, 2025
5f13795
Add integration tests, accomodate scanned card data type change
atmamont Mar 21, 2025
1224f08
Increase debug output level for xcodebuild
atmamont Mar 24, 2025
893cd08
Remove xcpretty verbose flag
atmamont Mar 24, 2025
725f2e8
Increase failing test timeout (it passes locally)
atmamont Mar 24, 2025
a1c6cf2
Merge branch 'COIOS-826_OCR_feature' into feature/OCR-add-cancel-button
nauaros Mar 24, 2025
980a37c
Increase failing test timeout
atmamont Mar 24, 2025
335cb07
Replace setupRootViewController with loadViewIfNeeded
atmamont Mar 24, 2025
8f121e0
Switch another failing test to loadViewIfNeeded
atmamont Mar 24, 2025
08ab48a
Use loadViewIfNeeded in all other cashappcomponent tests
atmamont Mar 24, 2025
044b4e9
Experiment: increase waiting time for root view controller to setup
atmamont Mar 25, 2025
07fc27e
Wrap scanner tests in ifCanImport statement
atmamont Mar 25, 2025
e55a966
Use loadViewIfNeeded in DocumentComponentTests
atmamont Mar 25, 2025
45ae7b0
Adjust DokuCOmponentTests
atmamont Mar 25, 2025
6e57e90
Remove state from CardScannerControllerTests
atmamont Mar 26, 2025
ef4eeae
Add missing scope
atmamont Mar 26, 2025
f2a7c7b
Merge branch 'feature/OCR-add-cancel-button' of github.com:Adyen/adye…
atmamont Mar 26, 2025
70df08a
Add conditional check for library import
atmamont Mar 26, 2025
d84845e
Update adyen-networking version in Cartfile
atmamont Mar 26, 2025
497cc6c
Apply swiftformat
atmamont Mar 26, 2025
8e033ae
Remove linked frameworks for test target
atmamont Mar 26, 2025
030f78e
Merge branch 'COIOS-826_OCR_feature' into feature/OCR-add-cancel-button
atmamont Mar 26, 2025
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
53 changes: 44 additions & 9 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 @@ -1286,7 +1287,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 @@ -1499,6 +1507,16 @@
/* End PBXContainerItemProxy section */

/* Begin PBXCopyFilesBuildPhase section */
B605AC102D897F960084D583 /* Embed Frameworks */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 10;
files = (
);
name = "Embed Frameworks";
runOnlyForDeploymentPostprocessing = 0;
};
E2C0E0A8220B0827008616F6 /* Embed Frameworks */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
Expand All @@ -1517,7 +1535,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 @@ -1882,6 +1900,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 @@ -2646,7 +2665,6 @@
8182AABD2B974E2B0087568E /* AdyenTwint.framework in Frameworks */,
E97B971322980CBA00505476 /* Adyen3DS2.framework in Frameworks */,
81B8036B2BE0E714003D037F /* AdyenWeChatPayInternal in Frameworks */,
81B8036B2BE0E714003D037F /* AdyenWeChatPayInternal in Frameworks */,
E97B970E22980C8400505476 /* AdyenDropIn.framework in Frameworks */,
E97B970D22980C7900505476 /* AdyenCard.framework in Frameworks */,
E2C0E03D22097917008616F6 /* Adyen.framework in Frameworks */,
Expand Down Expand Up @@ -2674,7 +2692,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 @@ -3420,6 +3438,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 @@ -4923,6 +4949,7 @@
F9EDB799239664B500CFB3C9 /* StoredPaymentMethodComponentTests.swift */,
F919DF9824EA64680027976E /* CardPublicKeyProviderTests.swift */,
81D7EC8F2CD24143005159F6 /* FormViewControllerTests.swift */,
B605AC0A2D897A930084D583 /* Card Scanner */,
);
path = "Card Tests";
sourceTree = "<group>";
Expand Down Expand Up @@ -6231,6 +6258,7 @@
E2C0E03822097917008616F6 /* Sources */,
E2C0E03922097917008616F6 /* Frameworks */,
E2C0E03A22097917008616F6 /* Resources */,
B605AC102D897F960084D583 /* Embed Frameworks */,
);
buildRules = (
);
Expand All @@ -6239,6 +6267,7 @@
E97B971222980C8B00505476 /* PBXTargetDependency */,
E2C0E03F22097917008616F6 /* PBXTargetDependency */,
F9D57522237C345C009C18B5 /* PBXTargetDependency */,
B605AC0F2D897F960084D583 /* PBXTargetDependency */,
);
name = IntegrationUIKitTests;
packageProductDependencies = (
Expand Down Expand Up @@ -6294,7 +6323,7 @@
F94D65EA2B036A450095D61E /* PBXTargetDependency */,
81088A222BDBACB7007FCDB9 /* PBXTargetDependency */,
819BFB3E2BDBED960018DC9B /* PBXTargetDependency */,
B639C0862D80456300472EBB /* PBXTargetDependency */,
B605AC142D8D988D0084D583 /* PBXTargetDependency */,
);
name = AdyenUIHost;
packageProductDependencies = (
Expand Down Expand Up @@ -7469,6 +7498,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 @@ -8216,10 +8246,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()
Comment thread
atmamont marked this conversation as resolved.

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 {
internal 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
2 changes: 1 addition & 1 deletion Cartfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
github "adyen/adyen-3ds2-ios" == 2.4.2
github "adyen/adyen-networking-ios" == 2.0.0
github "adyen/adyen-networking-ios" == 3.0.1
github "adyen/adyen-wechatpay-ios" == 2.1.0
github "adyen/adyen-authentication-ios" == 3.1.0
Loading
Loading