From 42a6f768b449107fc4e18dba95b24abd74b6972d Mon Sep 17 00:00:00 2001 From: Andrii Mamchenko Date: Mon, 17 Mar 2025 15:43:28 +0100 Subject: [PATCH 01/23] Add Cancel button and title to card scanning screen navigation bar --- .../Card/CardScannerController.swift | 37 ++++++++++++++++--- .../Components/Card/CardViewController.swift | 3 +- 2 files changed, 34 insertions(+), 6 deletions(-) diff --git a/AdyenCard/Components/Card/CardScannerController.swift b/AdyenCard/Components/Card/CardScannerController.swift index de345eba82..d55b6fa9de 100644 --- a/AdyenCard/Components/Card/CardScannerController.swift +++ b/AdyenCard/Components/Card/CardScannerController.swift @@ -13,8 +13,8 @@ internal protocol CardScannerControlling { var isScannerAvailable: Bool { get } init(presenter: UIViewController) - func openCardScanner() - + func openCardScanner(title: String?) + var onScanComplete: ((Result) -> Void)? { get set } } @@ -27,6 +27,7 @@ internal protocol CardScannerControlling { } private let presenter: UIViewController + internal var onScanComplete: ((Result<(String?, Date?), any Error>) -> Void)? internal init(presenter: UIViewController) { @@ -37,14 +38,17 @@ internal protocol CardScannerControlling { if #available(iOS 13.0, *), AdyenCardScanner.CardScanner.isAvailable { true } else { false } } - internal func openCardScanner() { - let scannerNavigationController = UINavigationController() + internal func openCardScanner(title: String?) { + let scannerNavigationController = makeNavigationController(title: title) guard let scannerViewController = AdyenCardScanner.CardScanner.createCardScanner(completion: { [weak self] result in guard let self else { return } self.onScanComplete?(self.map(result)) scannerNavigationController.dismiss(animated: true) }) else { return } - + + scannerViewController.navigationItem.leftBarButtonItem = makeCancelBarButton() + scannerViewController.title = title + scannerNavigationController.setViewControllers( [scannerViewController], animated: false @@ -62,6 +66,29 @@ internal protocol CardScannerControlling { .failure(CardScannerError.scanningError) } } + + private func makeNavigationController(title: String?) -> 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 + navigationController.title = title + + return navigationController + } + + private func makeCancelBarButton() -> UIBarButtonItem { + UIBarButtonItem(barButtonSystemItem: .cancel, target: self, action: #selector(handleCardScanningCancelation)) + } + + @objc private func handleCardScanningCancelation(sender: NSObject) { + presenter.navigationController?.topViewController?.dismiss(animated: true) + } } #else // canImport(AdyenCardScanner) diff --git a/AdyenCard/Components/Card/CardViewController.swift b/AdyenCard/Components/Card/CardViewController.swift index eec252a0d4..c84bc294b1 100644 --- a/AdyenCard/Components/Card/CardViewController.swift +++ b/AdyenCard/Components/Card/CardViewController.swift @@ -40,7 +40,8 @@ internal class CardViewController: FormViewController { let scanCardHandler: (() -> Void)? if isCardScannerAvailable { - scanCardHandler = { [weak self] in self?.cardScannerController.openCardScanner() } + let scanCardTitle = localizedString(.scanYourCardButton, localizationParameters) + scanCardHandler = { [weak self] in self?.cardScannerController.openCardScanner(title: scanCardTitle) } } else { scanCardHandler = nil } From 8a0ac0be8a59276af2d7d8ed7c9a6e8c413a6f2f Mon Sep 17 00:00:00 2001 From: Andrii Mamchenko Date: Mon, 17 Mar 2025 15:50:03 +0100 Subject: [PATCH 02/23] Code cleanup --- AdyenCard/Components/Card/CardScannerController.swift | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/AdyenCard/Components/Card/CardScannerController.swift b/AdyenCard/Components/Card/CardScannerController.swift index d55b6fa9de..d9ef25eacb 100644 --- a/AdyenCard/Components/Card/CardScannerController.swift +++ b/AdyenCard/Components/Card/CardScannerController.swift @@ -39,7 +39,7 @@ internal protocol CardScannerControlling { } internal func openCardScanner(title: String?) { - let scannerNavigationController = makeNavigationController(title: title) + let scannerNavigationController = makeNavigationController() guard let scannerViewController = AdyenCardScanner.CardScanner.createCardScanner(completion: { [weak self] result in guard let self else { return } self.onScanComplete?(self.map(result)) @@ -67,7 +67,7 @@ internal protocol CardScannerControlling { } } - private func makeNavigationController(title: String?) -> UINavigationController { + private func makeNavigationController() -> UINavigationController { guard #available(iOS 13.0, *) else { return UINavigationController() } let appearance = UINavigationBarAppearance() @@ -77,7 +77,6 @@ internal protocol CardScannerControlling { navigationController.navigationBar.standardAppearance = appearance navigationController.navigationBar.compactAppearance = appearance navigationController.navigationBar.scrollEdgeAppearance = appearance - navigationController.title = title return navigationController } @@ -96,7 +95,7 @@ internal protocol CardScannerControlling { internal final class CardScannerController: CardScannerControlling { internal var isScannerAvailable: Bool { false } internal var onScanComplete: ((Result) -> Void)? - internal func openCardScanner() {} + internal func openCardScanner(title: String?) {} internal init(presenter: UIViewController) {} } From 1da6587098fb7b365f93d5c448b4ff0a2440ccda Mon Sep 17 00:00:00 2001 From: Andrii Mamchenko Date: Thu, 20 Mar 2025 08:12:42 +0100 Subject: [PATCH 03/23] Use variable instead of init parameter for Card scanner screen title --- AdyenCard/Components/Card/CardViewController.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/AdyenCard/Components/Card/CardViewController.swift b/AdyenCard/Components/Card/CardViewController.swift index c84bc294b1..35ae611d60 100644 --- a/AdyenCard/Components/Card/CardViewController.swift +++ b/AdyenCard/Components/Card/CardViewController.swift @@ -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) } @@ -40,8 +41,7 @@ internal class CardViewController: FormViewController { let scanCardHandler: (() -> Void)? if isCardScannerAvailable { - let scanCardTitle = localizedString(.scanYourCardButton, localizationParameters) - scanCardHandler = { [weak self] in self?.cardScannerController.openCardScanner(title: scanCardTitle) } + scanCardHandler = { [weak self] in self?.cardScannerController.openCardScanner() } } else { scanCardHandler = nil } From cc8f01c27f07b187a77afa7495adb749ff91a9df Mon Sep 17 00:00:00 2001 From: Andrii Mamchenko Date: Thu, 20 Mar 2025 10:39:20 +0100 Subject: [PATCH 04/23] Add tests for CardScannerController --- Adyen.xcodeproj/project.pbxproj | 40 +++++ .../Card/CardScannerController.swift | 48 ++++-- .../CardScannerControllerTests.swift | 143 ++++++++++++++++++ 3 files changed, 220 insertions(+), 11 deletions(-) create mode 100644 Tests/IntegrationTests/Card Tests/Card Scanner/CardScannerControllerTests.swift diff --git a/Adyen.xcodeproj/project.pbxproj b/Adyen.xcodeproj/project.pbxproj index 0ef255bfee..0b54b12302 100644 --- a/Adyen.xcodeproj/project.pbxproj +++ b/Adyen.xcodeproj/project.pbxproj @@ -426,6 +426,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 */; }; + B605AC0C2D897F960084D583 /* AdyenCardScanner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C9FC2C3D2D50D0B700C9D0F3 /* AdyenCardScanner.framework */; }; + B605AC0D2D897F960084D583 /* 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 */; }; @@ -1281,6 +1284,13 @@ remoteGlobalIDString = E2C0E03222097917008616F6; remoteInfo = Adyen; }; + B605AC0E2D897F960084D583 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = E2C0E02A22097917008616F6 /* Project object */; + proxyType = 1; + remoteGlobalIDString = C9FC2C3C2D50D0B700C9D0F3; + remoteInfo = AdyenCardScanner; + }; B639C0852D80456300472EBB /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = E2C0E02A22097917008616F6 /* Project object */; @@ -1494,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; @@ -1875,6 +1896,7 @@ A0F4559A295F0F58001742C7 /* MealVoucherPaymentMethod.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MealVoucherPaymentMethod.swift; sourceTree = ""; }; A0F455A02968472B001742C7 /* PartialPaymentMethodDetails.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PartialPaymentMethodDetails.swift; sourceTree = ""; }; A0FA143E26D65A5300627127 /* InstallmentPickerElement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstallmentPickerElement.swift; sourceTree = ""; }; + B605AC092D897A930084D583 /* CardScannerControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardScannerControllerTests.swift; sourceTree = ""; }; B62A075B2D71FB43006072E7 /* FormCardNumberItemView+ScanCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FormCardNumberItemView+ScanCard.swift"; sourceTree = ""; }; 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 = ""; }; @@ -2636,6 +2658,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 */, @@ -3410,6 +3433,14 @@ path = Installment; sourceTree = ""; }; + B605AC0A2D897A930084D583 /* Card Scanner */ = { + isa = PBXGroup; + children = ( + B605AC092D897A930084D583 /* CardScannerControllerTests.swift */, + ); + path = "Card Scanner"; + sourceTree = ""; + }; B62D48AD2BBE8D79001EF01A /* UnitTests */ = { isa = PBXGroup; children = ( @@ -4909,6 +4940,7 @@ F9EDB799239664B500CFB3C9 /* StoredPaymentMethodComponentTests.swift */, F919DF9824EA64680027976E /* CardPublicKeyProviderTests.swift */, 81D7EC8F2CD24143005159F6 /* FormViewControllerTests.swift */, + B605AC0A2D897A930084D583 /* Card Scanner */, ); path = "Card Tests"; sourceTree = ""; @@ -6216,6 +6248,7 @@ E2C0E03822097917008616F6 /* Sources */, E2C0E03922097917008616F6 /* Frameworks */, E2C0E03A22097917008616F6 /* Resources */, + B605AC102D897F960084D583 /* Embed Frameworks */, ); buildRules = ( ); @@ -6224,6 +6257,7 @@ E97B971222980C8B00505476 /* PBXTargetDependency */, E2C0E03F22097917008616F6 /* PBXTargetDependency */, F9D57522237C345C009C18B5 /* PBXTargetDependency */, + B605AC0F2D897F960084D583 /* PBXTargetDependency */, ); name = IntegrationUIKitTests; packageProductDependencies = ( @@ -7451,6 +7485,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 */, @@ -8196,6 +8231,11 @@ target = E2C0E03222097917008616F6 /* Adyen */; targetProxy = A04F8C2D29E5957B00F3F62B /* PBXContainerItemProxy */; }; + B605AC0F2D897F960084D583 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = C9FC2C3C2D50D0B700C9D0F3 /* AdyenCardScanner */; + targetProxy = B605AC0E2D897F960084D583 /* PBXContainerItemProxy */; + }; B639C0862D80456300472EBB /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = C9FC2C3C2D50D0B700C9D0F3 /* AdyenCardScanner */; diff --git a/AdyenCard/Components/Card/CardScannerController.swift b/AdyenCard/Components/Card/CardScannerController.swift index d9ef25eacb..dbcaebef1f 100644 --- a/AdyenCard/Components/Card/CardScannerController.swift +++ b/AdyenCard/Components/Card/CardScannerController.swift @@ -7,13 +7,19 @@ import Foundation import UIKit -internal protocol CardScannerControlling { - typealias CardModel = (String?, Date?) - +internal protocol CardScannerAvailability { var isScannerAvailable: Bool { get } +} + +internal protocol CardScannerProviding { + func createCardScanner(completion: @escaping (Result) -> Void) -> UIViewController? +} + +internal protocol CardScannerControlling: CardScannerAvailability { + typealias CardModel = (String?, Date?) - init(presenter: UIViewController) - func openCardScanner(title: String?) + init(presenter: UIViewController, availabilityProvider: CardScannerAvailability, cardScannerProvider: CardScannerProviding) + func openCardScanner() var onScanComplete: ((Result) -> Void)? { get set } } @@ -21,26 +27,45 @@ internal protocol CardScannerControlling { #if canImport(AdyenCardScanner) import AdyenCardScanner + private struct CardScannerAvailabilityWrapper: CardScannerAvailability { + var isScannerAvailable: Bool { AdyenCardScanner.CardScanner.isAvailable } + } + + private struct CardScannerProviderWrapper: CardScannerProviding { + func createCardScanner(completion: @escaping (Result) -> Void) -> UIViewController? { + AdyenCardScanner.CardScanner.createCardScanner(completion: completion) + } + } + 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<(String?, Date?), any Error>) -> Void)? - internal init(presenter: UIViewController) { + 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(title: String?) { + internal func openCardScanner() { let scannerNavigationController = makeNavigationController() - guard let scannerViewController = AdyenCardScanner.CardScanner.createCardScanner(completion: { [weak self] result in + guard let scannerViewController = cardScannerProvider.createCardScanner(completion: { [weak self] result in guard let self else { return } self.onScanComplete?(self.map(result)) scannerNavigationController.dismiss(animated: true) @@ -85,8 +110,9 @@ internal protocol CardScannerControlling { UIBarButtonItem(barButtonSystemItem: .cancel, target: self, action: #selector(handleCardScanningCancelation)) } - @objc private func handleCardScanningCancelation(sender: NSObject) { - presenter.navigationController?.topViewController?.dismiss(animated: true) + @objc internal func handleCardScanningCancelation(completion: (() -> Void)? = nil) { + presenter.presentedViewController?.dismiss(animated: true, completion: completion) +// presenter.navigationController?.topViewController?.dismiss(animated: true) } } diff --git a/Tests/IntegrationTests/Card Tests/Card Scanner/CardScannerControllerTests.swift b/Tests/IntegrationTests/Card Tests/Card Scanner/CardScannerControllerTests.swift new file mode 100644 index 0000000000..ed6f3915b1 --- /dev/null +++ b/Tests/IntegrationTests/Card Tests/Card Scanner/CardScannerControllerTests.swift @@ -0,0 +1,143 @@ +// +// Copyright (c) 2025 Adyen N.V. +// +// This file is open source and available under the MIT license. See the LICENSE file for more info. +// + +@testable import AdyenCard +@testable import AdyenCardScanner +import XCTest + +class CardScannerControllerTests: XCTestCase { + + var sut: CardScannerController! + var mockPresenter: UIViewController! + private var mockCardScanner: CardScannerProviderSpy! + + override func setUpWithError() throws { + mockPresenter = UIViewController() + mockCardScanner = CardScannerProviderSpy() + + sut = CardScannerController( + presenter: mockPresenter, + availabilityProvider: CardScannerAvailalabilityMock(), + cardScannerProvider: mockCardScanner + ) + + let window = UIWindow(frame: UIScreen.main.bounds) + window.rootViewController = mockPresenter + window.makeKeyAndVisible() + } + + override func tearDownWithError() throws { + sut = nil + mockPresenter = nil + mockCardScanner = nil + } + + // This test requires AdyenCardScanner framework to be imported for the test target + func test_scannerIsAvailable() { + XCTAssertTrue(sut.isScannerAvailable) + } + + func test_openCardScanner_withTitle_presentsCorrectTitle() throws { + let expectation = XCTestExpectation(description: "Card scanner should complete the flow") + sut.onScanComplete = { result in + expectation.fulfill() + } + + let expectedTitle = "Scan your card" + sut.title = expectedTitle + sut.openCardScanner() + + let scannerNavigationController = mockPresenter.presentedViewController as? UINavigationController + let scannerViewController = scannerNavigationController?.topViewController + XCTAssertEqual(scannerViewController?.title, expectedTitle) + + sut.onScanComplete?(.success((nil, Date()))) + wait(for: [expectation], timeout: 3.0) + } + + func testHandleCardScanningCancelation() throws { + sut.openCardScanner() + + sut.handleCardScanningCancelation { + XCTAssertNil(self.mockPresenter.presentedViewController) + } + } + + func test_controller_returnsScannedCardValue() { + // Given + let expectation = XCTestExpectation(description: "Card scanner should complete the flow") + + let cardNumber = "1111 2222 3333 4444" + let expiryDate = Date(timeIntervalSince1970: 1742456818) + + let expectedResultTuple: (String?, Date?) = (cardNumber, expiryDate) + let mockCard = AdyenCardScanner.CreditCard(number: cardNumber, expirationDate: expiryDate) + + sut.onScanComplete = { result in + // Then + self.expect(result, toMatch: .success(expectedResultTuple)) + expectation.fulfill() + } + + // When + sut.openCardScanner() + mockCardScanner.onScanComplete(result: .success(mockCard)) + + wait(for: [expectation], timeout: 1.0) + } + + func test_controller_returnsSimplifiedScannerError() { + // Given + let expectation = XCTestExpectation(description: "Card scanner should complete the flow") + let mockError = AdyenCardScanner.CardScannerError(kind: .authorizationDenied) + let expectedError = CardScannerController.CardScannerError.scanningError + + sut.onScanComplete = { result in + // Then + self.expect(result, toMatch: .failure(expectedError)) + expectation.fulfill() + } + + // When + sut.openCardScanner() + mockCardScanner.onScanComplete(result: .failure(mockError)) + + wait(for: [expectation], timeout: 1.0) + } + + // MARK: - Helpers + + private func expect(_ result: Result<(String?, Date?), Error>, toMatch expectedResult: Result<(String?, Date?), Error>) { + switch (result, expectedResult) { + case (.success(let (receivedCard, receivedDate)), .success(let (expectedCard, expectedDate))): + XCTAssertEqual(receivedCard, expectedCard) + XCTAssertEqual(receivedDate, expectedDate) + case let (.failure(receivedError as NSError), .failure(expectedError as NSError)): + XCTAssertEqual(receivedError, expectedError) + default: + XCTFail() + } + } + + private struct CardScannerAvailalabilityMock: CardScannerAvailability { + var isScannerAvailable: Bool { true } + } + + private class CardScannerProviderSpy: CardScannerProviding { + private var completion: ((Result) -> Void)? = nil + + func createCardScanner( + completion: @escaping (Result) -> Void + ) -> UIViewController? { + self.completion = completion + return UIViewController() + } + + func onScanComplete(result: Result) { + self.completion?(result) + } + } +} From 32f65bffe30addfc648146a8c20f2b43840aeb9c Mon Sep 17 00:00:00 2001 From: Andrii Mamchenko Date: Thu, 20 Mar 2025 11:47:09 +0100 Subject: [PATCH 05/23] Fix the no-framework-available version --- Adyen.xcodeproj/project.pbxproj | 17 ------------ .../Card/CardScannerController.swift | 26 ++++++++++++++++--- 2 files changed, 23 insertions(+), 20 deletions(-) diff --git a/Adyen.xcodeproj/project.pbxproj b/Adyen.xcodeproj/project.pbxproj index fc39c1b32f..156210fde2 100644 --- a/Adyen.xcodeproj/project.pbxproj +++ b/Adyen.xcodeproj/project.pbxproj @@ -439,8 +439,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 */; }; @@ -1291,13 +1289,6 @@ remoteGlobalIDString = C9FC2C3C2D50D0B700C9D0F3; remoteInfo = AdyenCardScanner; }; - B639C0852D80456300472EBB /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = E2C0E02A22097917008616F6 /* Project object */; - proxyType = 1; - remoteGlobalIDString = C9FC2C3C2D50D0B700C9D0F3; - remoteInfo = AdyenCardScanner; - }; B6EE0F472BBECBEF00B9810D /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = E2C0E02A22097917008616F6 /* Project object */; @@ -1533,7 +1524,6 @@ E2C0E0AE220B0840008616F6 /* AdyenCard.framework in Embed Frameworks */, F92980AD27CE2B33000CA5CA /* AdyenSession.framework in Embed Frameworks */, A020EC4929E6EC4B0050B2FE /* AdyenCashAppPay.framework in Embed Frameworks */, - B639C0842D80456300472EBB /* AdyenCardScanner.framework in Embed Frameworks */, ); name = "Embed Frameworks"; runOnlyForDeploymentPostprocessing = 0; @@ -2687,7 +2677,6 @@ 81088A1F2BDBACB7007FCDB9 /* AdyenTwint.framework in Frameworks */, F9175EEE2593955C00D653BE /* AdyenComponents.framework in Frameworks */, F92327C425A46EF0002C5BC4 /* AdyenEncryption.framework in Frameworks */, - B639C0832D80456300472EBB /* AdyenCardScanner.framework in Frameworks */, 81A91AC22BEC12A2001E00C8 /* TwintSDK.xcframework in Frameworks */, F9A2D01225DBF104008944BE /* PassKit.framework in Frameworks */, F973A9192791C3B0005AA753 /* AdyenActions.framework in Frameworks */, @@ -6313,7 +6302,6 @@ F94D65EA2B036A450095D61E /* PBXTargetDependency */, 81088A222BDBACB7007FCDB9 /* PBXTargetDependency */, 819BFB3E2BDBED960018DC9B /* PBXTargetDependency */, - B639C0862D80456300472EBB /* PBXTargetDependency */, ); name = AdyenUIHost; packageProductDependencies = ( @@ -8236,11 +8224,6 @@ target = C9FC2C3C2D50D0B700C9D0F3 /* AdyenCardScanner */; targetProxy = B605AC0E2D897F960084D583 /* PBXContainerItemProxy */; }; - B639C0862D80456300472EBB /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = C9FC2C3C2D50D0B700C9D0F3 /* AdyenCardScanner */; - targetProxy = B639C0852D80456300472EBB /* PBXContainerItemProxy */; - }; B6EE0F482BBECBEF00B9810D /* PBXTargetDependency */ = { isa = PBXTargetDependency; platformFilter = ios; diff --git a/AdyenCard/Components/Card/CardScannerController.swift b/AdyenCard/Components/Card/CardScannerController.swift index dbcaebef1f..f6f6b2e8d2 100644 --- a/AdyenCard/Components/Card/CardScannerController.swift +++ b/AdyenCard/Components/Card/CardScannerController.swift @@ -12,7 +12,7 @@ internal protocol CardScannerAvailability { } internal protocol CardScannerProviding { - func createCardScanner(completion: @escaping (Result) -> Void) -> UIViewController? + func createCardScanner(completion: @escaping (Result<(String?, Date?), Error>) -> Void) -> UIViewController? } internal protocol CardScannerControlling: CardScannerAvailability { @@ -21,6 +21,7 @@ internal protocol CardScannerControlling: CardScannerAvailability { init(presenter: UIViewController, availabilityProvider: CardScannerAvailability, cardScannerProvider: CardScannerProviding) func openCardScanner() + var title: String? { get set } var onScanComplete: ((Result) -> Void)? { get set } } @@ -121,9 +122,28 @@ internal protocol CardScannerControlling: CardScannerAvailability { internal final class CardScannerController: CardScannerControlling { internal var isScannerAvailable: Bool { false } internal var onScanComplete: ((Result) -> Void)? - internal func openCardScanner(title: String?) {} + var title: String? + internal func openCardScanner() {} - internal init(presenter: UIViewController) {} + internal init( + presenter: UIViewController, + availabilityProvider: CardScannerAvailability = DummyCardScannerAvailability(), + cardScannerProvider: CardScannerProviding = DummyCardScannerProvider() + ) {} + + // MARK: - Helpers + + struct DummyCardScannerAvailability: CardScannerAvailability { + var isScannerAvailable: Bool { false } + } + + struct DummyCardScannerProvider: CardScannerProviding { + func createCardScanner( + completion: @escaping (Result<(String?, Date?), any Error>) -> Void + ) -> UIViewController? { + UIViewController() + } + } } #endif // canImport(AdyenCardScanner) From 5f137952ff90d96831eb8ec45fa371a295c11d9d Mon Sep 17 00:00:00 2001 From: Andrii Mamchenko Date: Fri, 21 Mar 2025 16:27:50 +0100 Subject: [PATCH 06/23] Add integration tests, accomodate scanned card data type change --- Adyen.xcodeproj/project.pbxproj | 17 +++++ .../Card/CardScannerController.swift | 63 +++++++++++-------- .../Components/Card/CardViewController.swift | 2 +- .../CardScannerControllerTests.swift | 27 ++++---- 4 files changed, 72 insertions(+), 37 deletions(-) diff --git a/Adyen.xcodeproj/project.pbxproj b/Adyen.xcodeproj/project.pbxproj index 156210fde2..862b812f74 100644 --- a/Adyen.xcodeproj/project.pbxproj +++ b/Adyen.xcodeproj/project.pbxproj @@ -429,6 +429,8 @@ B605AC0B2D897A930084D583 /* CardScannerControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B605AC092D897A930084D583 /* CardScannerControllerTests.swift */; }; B605AC0C2D897F960084D583 /* AdyenCardScanner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C9FC2C3D2D50D0B700C9D0F3 /* AdyenCardScanner.framework */; }; B605AC0D2D897F960084D583 /* AdyenCardScanner.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = C9FC2C3D2D50D0B700C9D0F3 /* AdyenCardScanner.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 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 */; }; @@ -1289,6 +1291,13 @@ remoteGlobalIDString = C9FC2C3C2D50D0B700C9D0F3; remoteInfo = AdyenCardScanner; }; + B605AC132D8D988D0084D583 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = E2C0E02A22097917008616F6 /* Project object */; + proxyType = 1; + remoteGlobalIDString = C9FC2C3C2D50D0B700C9D0F3; + remoteInfo = AdyenCardScanner; + }; B6EE0F472BBECBEF00B9810D /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = E2C0E02A22097917008616F6 /* Project object */; @@ -1524,6 +1533,7 @@ E2C0E0AE220B0840008616F6 /* AdyenCard.framework in Embed Frameworks */, F92980AD27CE2B33000CA5CA /* AdyenSession.framework in Embed Frameworks */, A020EC4929E6EC4B0050B2FE /* AdyenCashAppPay.framework in Embed Frameworks */, + B605AC122D8D988D0084D583 /* AdyenCardScanner.framework in Embed Frameworks */, ); name = "Embed Frameworks"; runOnlyForDeploymentPostprocessing = 0; @@ -2677,6 +2687,7 @@ 81088A1F2BDBACB7007FCDB9 /* AdyenTwint.framework in Frameworks */, F9175EEE2593955C00D653BE /* AdyenComponents.framework in Frameworks */, F92327C425A46EF0002C5BC4 /* AdyenEncryption.framework in Frameworks */, + B605AC112D8D988D0084D583 /* AdyenCardScanner.framework in Frameworks */, 81A91AC22BEC12A2001E00C8 /* TwintSDK.xcframework in Frameworks */, F9A2D01225DBF104008944BE /* PassKit.framework in Frameworks */, F973A9192791C3B0005AA753 /* AdyenActions.framework in Frameworks */, @@ -6302,6 +6313,7 @@ F94D65EA2B036A450095D61E /* PBXTargetDependency */, 81088A222BDBACB7007FCDB9 /* PBXTargetDependency */, 819BFB3E2BDBED960018DC9B /* PBXTargetDependency */, + B605AC142D8D988D0084D583 /* PBXTargetDependency */, ); name = AdyenUIHost; packageProductDependencies = ( @@ -8224,6 +8236,11 @@ target = C9FC2C3C2D50D0B700C9D0F3 /* AdyenCardScanner */; targetProxy = B605AC0E2D897F960084D583 /* PBXContainerItemProxy */; }; + B605AC142D8D988D0084D583 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = C9FC2C3C2D50D0B700C9D0F3 /* AdyenCardScanner */; + targetProxy = B605AC132D8D988D0084D583 /* PBXContainerItemProxy */; + }; B6EE0F482BBECBEF00B9810D /* PBXTargetDependency */ = { isa = PBXTargetDependency; platformFilter = ios; diff --git a/AdyenCard/Components/Card/CardScannerController.swift b/AdyenCard/Components/Card/CardScannerController.swift index c82181e62b..cbfaf66c4f 100644 --- a/AdyenCard/Components/Card/CardScannerController.swift +++ b/AdyenCard/Components/Card/CardScannerController.swift @@ -11,30 +11,38 @@ internal protocol CardScannerAvailability { var isScannerAvailable: Bool { get } } +internal typealias CardScanDetails = (number: String?, expirationDate: Date?) + internal protocol CardScannerProviding { - func createCardScanner(completion: @escaping (Result<(String?, Date?), Error>) -> Void) -> UIViewController? + func createCardScanner(completion: @escaping (Result) -> Void) -> UIViewController? } internal protocol CardScannerControlling: CardScannerAvailability { - typealias CardModel = (String?, Date?) init(presenter: UIViewController, availabilityProvider: CardScannerAvailability, cardScannerProvider: CardScannerProviding) func openCardScanner() var title: String? { get set } - var onScanComplete: ((Result) -> Void)? { get set } + var onScanComplete: ((Result) -> Void)? { get set } } #if canImport(AdyenCardScanner) import AdyenCardScanner private struct CardScannerAvailabilityWrapper: CardScannerAvailability { - var isScannerAvailable: Bool { AdyenCardScanner.CardScanner.isAvailable } + var isScannerAvailable: Bool { + AdyenCardScanner.CardScanner.isAvailable + } } private struct CardScannerProviderWrapper: CardScannerProviding { - func createCardScanner(completion: @escaping (Result) -> Void) -> UIViewController? { - AdyenCardScanner.CardScanner.createCardScanner(completion: completion) + func createCardScanner(completion: @escaping (Result) -> 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)) + } + } } } @@ -42,14 +50,14 @@ internal protocol CardScannerControlling: CardScannerAvailability { 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<(String?, Date?), any Error>) -> Void)? - + internal var onScanComplete: ((Result) -> Void)? + internal init( presenter: UIViewController, availabilityProvider: CardScannerAvailability = CardScannerAvailabilityWrapper(), @@ -59,16 +67,16 @@ internal protocol CardScannerControlling: CardScannerAvailability { 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?(self.map(result)) + self.onScanComplete?(map(result)) scannerNavigationController.dismiss(animated: true) }) else { return } @@ -81,13 +89,13 @@ internal protocol CardScannerControlling: CardScannerAvailability { ) presenter.present(scannerNavigationController, animated: true) } - + // MARK: - Private - - private func map(_ result: Result) -> Result { + + private func map(_ result: Result) -> Result { switch result { - case let .success(card): - .success((card.number, card.expirationDate)) + case let .success(cardScanDetails): + .success(cardScanDetails) case .failure: .failure(CardScannerError.scanningError) } @@ -111,9 +119,14 @@ internal protocol CardScannerControlling: CardScannerAvailability { UIBarButtonItem(barButtonSystemItem: .cancel, target: self, action: #selector(handleCardScanningCancelation)) } - @objc internal func handleCardScanningCancelation(completion: (() -> Void)? = nil) { + @objc + private func handleCardScanningCancelation() { + handleCardScanningCancelationWithCompletion(nil) + } + + @objc + internal func handleCardScanningCancelationWithCompletion(_ completion: (() -> Void)?) { presenter.presentedViewController?.dismiss(animated: true, completion: completion) -// presenter.navigationController?.topViewController?.dismiss(animated: true) } } @@ -121,8 +134,8 @@ internal protocol CardScannerControlling: CardScannerAvailability { internal final class CardScannerController: CardScannerControlling { internal var isScannerAvailable: Bool { false } - internal var onScanComplete: ((Result) -> Void)? - var title: String? + internal var onScanComplete: ((Result) -> Void)? + internal var title: String? internal func openCardScanner() {} internal init( @@ -133,13 +146,13 @@ internal protocol CardScannerControlling: CardScannerAvailability { // MARK: - Helpers - struct DummyCardScannerAvailability: CardScannerAvailability { + internal struct DummyCardScannerAvailability: CardScannerAvailability { var isScannerAvailable: Bool { false } } - struct DummyCardScannerProvider: CardScannerProviding { - func createCardScanner( - completion: @escaping (Result<(String?, Date?), any Error>) -> Void + internal struct DummyCardScannerProvider: CardScannerProviding { + internal func createCardScanner( + completion: @escaping (Result) -> Void ) -> UIViewController? { UIViewController() } diff --git a/AdyenCard/Components/Card/CardViewController.swift b/AdyenCard/Components/Card/CardViewController.swift index 35ae611d60..fbd767f259 100644 --- a/AdyenCard/Components/Card/CardViewController.swift +++ b/AdyenCard/Components/Card/CardViewController.swift @@ -410,7 +410,7 @@ extension CardViewController: CardViewControllerProtocol { // MARK: - Card scanner extension CardViewController { - private func handleCardScanningResult(_ result: Result<(String?, Date?), Error>) { + private func handleCardScanningResult(_ result: Result) { switch result { case let .success((number, expiryDate)): items.numberContainerItem.setCardNumber(number ?? "") diff --git a/Tests/IntegrationTests/Card Tests/Card Scanner/CardScannerControllerTests.swift b/Tests/IntegrationTests/Card Tests/Card Scanner/CardScannerControllerTests.swift index ed6f3915b1..e15fea6317 100644 --- a/Tests/IntegrationTests/Card Tests/Card Scanner/CardScannerControllerTests.swift +++ b/Tests/IntegrationTests/Card Tests/Card Scanner/CardScannerControllerTests.swift @@ -61,7 +61,7 @@ class CardScannerControllerTests: XCTestCase { func testHandleCardScanningCancelation() throws { sut.openCardScanner() - sut.handleCardScanningCancelation { + sut.handleCardScanningCancelationWithCompletion { XCTAssertNil(self.mockPresenter.presentedViewController) } } @@ -73,12 +73,12 @@ class CardScannerControllerTests: XCTestCase { let cardNumber = "1111 2222 3333 4444" let expiryDate = Date(timeIntervalSince1970: 1742456818) - let expectedResultTuple: (String?, Date?) = (cardNumber, expiryDate) - let mockCard = AdyenCardScanner.CreditCard(number: cardNumber, expirationDate: expiryDate) + let expectedResult: CardScanDetails = (cardNumber, expiryDate) + let mockCard = CardScanDetails(cardNumber, expiryDate) sut.onScanComplete = { result in // Then - self.expect(result, toMatch: .success(expectedResultTuple)) + self.expect(result, toMatch: .success(expectedResult)) expectation.fulfill() } @@ -110,13 +110,18 @@ class CardScannerControllerTests: XCTestCase { // MARK: - Helpers - private func expect(_ result: Result<(String?, Date?), Error>, toMatch expectedResult: Result<(String?, Date?), Error>) { + private func expect( + _ result: Result, + toMatch expectedResult: Result, + file: StaticString = #file, + line: UInt = #line + ) { switch (result, expectedResult) { case (.success(let (receivedCard, receivedDate)), .success(let (expectedCard, expectedDate))): - XCTAssertEqual(receivedCard, expectedCard) - XCTAssertEqual(receivedDate, expectedDate) + XCTAssertEqual(receivedCard, expectedCard, file: file, line: line) + XCTAssertEqual(receivedDate, expectedDate, file: file, line: line) case let (.failure(receivedError as NSError), .failure(expectedError as NSError)): - XCTAssertEqual(receivedError, expectedError) + XCTAssertEqual(receivedError, expectedError, file: file, line: line) default: XCTFail() } @@ -127,16 +132,16 @@ class CardScannerControllerTests: XCTestCase { } private class CardScannerProviderSpy: CardScannerProviding { - private var completion: ((Result) -> Void)? = nil + private var completion: ((Result) -> Void)? = nil func createCardScanner( - completion: @escaping (Result) -> Void + completion: @escaping (Result) -> Void ) -> UIViewController? { self.completion = completion return UIViewController() } - func onScanComplete(result: Result) { + func onScanComplete(result: Result) { self.completion?(result) } } From 1224f0829821db8517885ae8109628dc9d6440be Mon Sep 17 00:00:00 2001 From: Andrii Mamchenko Date: Mon, 24 Mar 2025 09:28:28 +0100 Subject: [PATCH 07/23] Increase debug output level for xcodebuild --- .github/workflows/pr_scan.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pr_scan.yml b/.github/workflows/pr_scan.yml index a6da2b7a72..fd9d29cb9a 100644 --- a/.github/workflows/pr_scan.yml +++ b/.github/workflows/pr_scan.yml @@ -56,7 +56,7 @@ jobs: -enableCodeCoverage YES \ -resultBundlePath "${resultPath}" \ ${params} \ - -destination "${{env.destination}}" | xcpretty --utf --color && exit ${PIPESTATUS[0]} + -destination "${{env.destination}}" | xcpretty --test --verbose --utf --color && exit ${PIPESTATUS[0]} env: params: '${{env.params}}' scheme: 'AdyenUIHost' From 893cd0891a417f708cafbf9b3dae5269709600a2 Mon Sep 17 00:00:00 2001 From: Andrii Mamchenko Date: Mon, 24 Mar 2025 09:50:50 +0100 Subject: [PATCH 08/23] Remove xcpretty verbose flag --- .github/workflows/pr_scan.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pr_scan.yml b/.github/workflows/pr_scan.yml index fd9d29cb9a..fae01ec5cc 100644 --- a/.github/workflows/pr_scan.yml +++ b/.github/workflows/pr_scan.yml @@ -56,7 +56,7 @@ jobs: -enableCodeCoverage YES \ -resultBundlePath "${resultPath}" \ ${params} \ - -destination "${{env.destination}}" | xcpretty --test --verbose --utf --color && exit ${PIPESTATUS[0]} + -destination "${{env.destination}}" | xcpretty --test --utf --color && exit ${PIPESTATUS[0]} env: params: '${{env.params}}' scheme: 'AdyenUIHost' From 725f2e8892bd24200d3bae7cb12d33b9e66a20c1 Mon Sep 17 00:00:00 2001 From: Andrii Mamchenko Date: Mon, 24 Mar 2025 10:19:30 +0100 Subject: [PATCH 09/23] Increase failing test timeout (it passes locally) --- .github/workflows/pr_scan.yml | 2 +- .../Cash App Pay/CashAppPayComponentTests.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pr_scan.yml b/.github/workflows/pr_scan.yml index fae01ec5cc..a6da2b7a72 100644 --- a/.github/workflows/pr_scan.yml +++ b/.github/workflows/pr_scan.yml @@ -56,7 +56,7 @@ jobs: -enableCodeCoverage YES \ -resultBundlePath "${resultPath}" \ ${params} \ - -destination "${{env.destination}}" | xcpretty --test --utf --color && exit ${PIPESTATUS[0]} + -destination "${{env.destination}}" | xcpretty --utf --color && exit ${PIPESTATUS[0]} env: params: '${{env.params}}' scheme: 'AdyenUIHost' diff --git a/Tests/IntegrationTests/Components Tests/Cash App Pay/CashAppPayComponentTests.swift b/Tests/IntegrationTests/Components Tests/Cash App Pay/CashAppPayComponentTests.swift index 99c738b13a..aac50b3dd0 100644 --- a/Tests/IntegrationTests/Components Tests/Cash App Pay/CashAppPayComponentTests.swift +++ b/Tests/IntegrationTests/Components Tests/Cash App Pay/CashAppPayComponentTests.swift @@ -261,7 +261,7 @@ import XCTest delegateExpectation.fulfill() } - wait(for: .milliseconds(300)) + wait(for: .milliseconds(500)) sut.submitApprovedRequest(with: [oneTimeGrant, onFileGrant], profile: .init(id: "testId", cashtag: "testtag")) From 980a37cd3e7df6b926abb082ed4ddb415e8cbe8b Mon Sep 17 00:00:00 2001 From: Andrii Mamchenko Date: Mon, 24 Mar 2025 15:29:03 +0100 Subject: [PATCH 10/23] Increase failing test timeout --- .../Cash App Pay/CashAppPayComponentTests.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/IntegrationTests/Components Tests/Cash App Pay/CashAppPayComponentTests.swift b/Tests/IntegrationTests/Components Tests/Cash App Pay/CashAppPayComponentTests.swift index aac50b3dd0..c20196e2b2 100644 --- a/Tests/IntegrationTests/Components Tests/Cash App Pay/CashAppPayComponentTests.swift +++ b/Tests/IntegrationTests/Components Tests/Cash App Pay/CashAppPayComponentTests.swift @@ -261,8 +261,8 @@ import XCTest delegateExpectation.fulfill() } - wait(for: .milliseconds(500)) - + wait(for: .milliseconds(1500)) + sut.submitApprovedRequest(with: [oneTimeGrant, onFileGrant], profile: .init(id: "testId", cashtag: "testtag")) waitForExpectations(timeout: 10, handler: nil) From 335cb07b13a4e5152657010ccad3a721e209ea54 Mon Sep 17 00:00:00 2001 From: Andrii Mamchenko Date: Mon, 24 Mar 2025 15:43:07 +0100 Subject: [PATCH 11/23] Replace setupRootViewController with loadViewIfNeeded --- .../Cash App Pay/CashAppPayComponentTests.swift | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/Tests/IntegrationTests/Components Tests/Cash App Pay/CashAppPayComponentTests.swift b/Tests/IntegrationTests/Components Tests/Cash App Pay/CashAppPayComponentTests.swift index c20196e2b2..efd8e434b8 100644 --- a/Tests/IntegrationTests/Components Tests/Cash App Pay/CashAppPayComponentTests.swift +++ b/Tests/IntegrationTests/Components Tests/Cash App Pay/CashAppPayComponentTests.swift @@ -241,8 +241,8 @@ import XCTest let delegate = PaymentComponentDelegateMock() sut.delegate = delegate - setupRootViewController(sut.viewController) - + sut.viewController.loadViewIfNeeded() + let delegateExpectation = expectation(description: "PaymentComponentDelegate must be called when submit button is clicked.") let finalizationExpectation = expectation(description: "Component should finalize.") delegate.onDidSubmit = { data, component in @@ -261,8 +261,6 @@ import XCTest delegateExpectation.fulfill() } - wait(for: .milliseconds(1500)) - sut.submitApprovedRequest(with: [oneTimeGrant, onFileGrant], profile: .init(id: "testId", cashtag: "testtag")) waitForExpectations(timeout: 10, handler: nil) From 8f121e0f1e867c75d47a9ba010bfa5c9b5d4756f Mon Sep 17 00:00:00 2001 From: Andrii Mamchenko Date: Mon, 24 Mar 2025 16:03:15 +0100 Subject: [PATCH 12/23] Switch another failing test to loadViewIfNeeded --- .../Cash App Pay/CashAppPayComponentTests.swift | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Tests/IntegrationTests/Components Tests/Cash App Pay/CashAppPayComponentTests.swift b/Tests/IntegrationTests/Components Tests/Cash App Pay/CashAppPayComponentTests.swift index efd8e434b8..25971b731a 100644 --- a/Tests/IntegrationTests/Components Tests/Cash App Pay/CashAppPayComponentTests.swift +++ b/Tests/IntegrationTests/Components Tests/Cash App Pay/CashAppPayComponentTests.swift @@ -208,7 +208,7 @@ import XCTest let delegate = PaymentComponentDelegateMock() sut.delegate = delegate - setupRootViewController(sut.viewController) + sut.viewController.loadViewIfNeeded() let delegateExpectation = expectation(description: "PaymentComponentDelegate must be called when submit button is clicked.") let finalizationExpectation = expectation(description: "Component should finalize.") @@ -228,8 +228,6 @@ import XCTest delegateExpectation.fulfill() } - wait(for: .milliseconds(300)) - sut.submitApprovedRequest(with: [oneTimeGrant], profile: .init(id: "testId", cashtag: "testtag")) waitForExpectations(timeout: 10, handler: nil) From 08ab48aa67122b795ca34a2f938bd88232ed9a6f Mon Sep 17 00:00:00 2001 From: Andrii Mamchenko Date: Mon, 24 Mar 2025 19:41:46 +0100 Subject: [PATCH 13/23] Use loadViewIfNeeded in all other cashappcomponent tests --- .../CashAppPayComponentTests.swift | 29 +++++++------------ 1 file changed, 11 insertions(+), 18 deletions(-) diff --git a/Tests/IntegrationTests/Components Tests/Cash App Pay/CashAppPayComponentTests.swift b/Tests/IntegrationTests/Components Tests/Cash App Pay/CashAppPayComponentTests.swift index 25971b731a..d635e3400e 100644 --- a/Tests/IntegrationTests/Components Tests/Cash App Pay/CashAppPayComponentTests.swift +++ b/Tests/IntegrationTests/Components Tests/Cash App Pay/CashAppPayComponentTests.swift @@ -100,9 +100,8 @@ import XCTest let config = CashAppPayConfiguration(redirectURL: URL(string: "test")!, showsStorePaymentMethodField: true, style: componentStyle) let sut = CashAppPayComponent(paymentMethod: paymentMethod, context: context, configuration: config) - setupRootViewController(sut.viewController) - wait(for: .milliseconds(300)) - + sut.viewController.loadViewIfNeeded() + let storeDetailsItemView: FormToggleItemView? = sut.viewController.view.findView(with: "AdyenCashAppPay.CashAppPayComponent.storeDetailsItem") let storeDetailsItemTitleLabel: UILabel? = sut.viewController.view.findView(with: "AdyenCashAppPay.CashAppPayComponent.storeDetailsItem.titleLabel") @@ -121,9 +120,8 @@ import XCTest let config = CashAppPayConfiguration(redirectURL: URL(string: "test")!, showsStorePaymentMethodField: true) let sut = CashAppPayComponent(paymentMethod: paymentMethod, context: context, configuration: config) - setupRootViewController(sut.viewController) - wait(for: .milliseconds(300)) - + sut.viewController.loadViewIfNeeded() + let storeDetailsToggleView: UIView? = sut.viewController.view.findView(with: "AdyenCashAppPay.CashAppPayComponent.storeDetailsItem") XCTAssertNotNil(storeDetailsToggleView) @@ -134,9 +132,8 @@ import XCTest let config = CashAppPayConfiguration(redirectURL: URL(string: "test")!, showsStorePaymentMethodField: false) let sut = CashAppPayComponent(paymentMethod: paymentMethod, context: context, configuration: config) - setupRootViewController(sut.viewController) - wait(for: .milliseconds(300)) - + sut.viewController.loadViewIfNeeded() + let storeDetailsToggleView: UIView? = sut.viewController.view.findView(with: "AdyenCashAppPay.CashAppPayComponent.storeDetailsItem") XCTAssertNil(storeDetailsToggleView) @@ -146,12 +143,8 @@ import XCTest let config = CashAppPayConfiguration(redirectURL: URL(string: "test")!, showsStorePaymentMethodField: true) let sut = CashAppPayComponent(paymentMethod: paymentMethod, context: context, configuration: config) - setupRootViewController(sut.viewController) - wait(for: .milliseconds(300)) + sut.viewController.loadViewIfNeeded() - setupRootViewController(sut.viewController) - wait(for: .milliseconds(300)) - XCTAssertFalse(sut.cashAppPayButton.showsActivityIndicator) sut.cashAppPayButton.showsActivityIndicator = true sut.stopLoadingIfNeeded() @@ -209,7 +202,7 @@ import XCTest let delegate = PaymentComponentDelegateMock() sut.delegate = delegate sut.viewController.loadViewIfNeeded() - + let delegateExpectation = expectation(description: "PaymentComponentDelegate must be called when submit button is clicked.") let finalizationExpectation = expectation(description: "Component should finalize.") delegate.onDidSubmit = { data, component in @@ -272,7 +265,7 @@ import XCTest context: context, configuration: configuration ) - setupRootViewController(sut.viewController) + sut.viewController.loadViewIfNeeded() let paymentDelegateMock = PaymentComponentDelegateMock() sut.delegate = paymentDelegateMock @@ -305,7 +298,7 @@ import XCTest configuration: config ) - setupRootViewController(sut.viewController) + sut.viewController.loadViewIfNeeded() let paymentDelegateMock = PaymentComponentDelegateMock() sut.delegate = paymentDelegateMock @@ -455,7 +448,7 @@ import XCTest context: context, configuration: configuration ) - setupRootViewController(sut.viewController) + sut.viewController.loadViewIfNeeded() let formViewController = try XCTUnwrap((sut.viewController as? SecuredViewController)?.childViewController) let expectedResult = formViewController.validate() From 044b4e96680392f1b51044329612e2a6f3834988 Mon Sep 17 00:00:00 2001 From: Andrii Mamchenko Date: Tue, 25 Mar 2025 10:34:34 +0100 Subject: [PATCH 14/23] Experiment: increase waiting time for root view controller to setup --- .../Helpers/XCTestCase+RootViewController.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/IntegrationTests/Helpers/XCTestCase+RootViewController.swift b/Tests/IntegrationTests/Helpers/XCTestCase+RootViewController.swift index 294d2ea86e..ef47f3228f 100644 --- a/Tests/IntegrationTests/Helpers/XCTestCase+RootViewController.swift +++ b/Tests/IntegrationTests/Helpers/XCTestCase+RootViewController.swift @@ -59,7 +59,7 @@ extension XCTestCase { extension DispatchTimeInterval { /// .milliseconds(30) - static var aMoment: Self { .milliseconds(30) } + static var aMoment: Self { .milliseconds(300) } } extension XCTestCase { From 07fc27e30e204edf83ab0328fe4fd087d2239c3d Mon Sep 17 00:00:00 2001 From: Andrii Mamchenko Date: Tue, 25 Mar 2025 12:20:02 +0100 Subject: [PATCH 15/23] Wrap scanner tests in ifCanImport statement --- Adyen.xcodeproj/project.pbxproj | 2 - .../CardScannerControllerTests.swift | 228 +++++++++--------- 2 files changed, 115 insertions(+), 115 deletions(-) diff --git a/Adyen.xcodeproj/project.pbxproj b/Adyen.xcodeproj/project.pbxproj index 2035f9f5c6..56102a0dc8 100644 --- a/Adyen.xcodeproj/project.pbxproj +++ b/Adyen.xcodeproj/project.pbxproj @@ -429,8 +429,6 @@ 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 */; }; - B605AC0C2D897F960084D583 /* AdyenCardScanner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C9FC2C3D2D50D0B700C9D0F3 /* AdyenCardScanner.framework */; }; - B605AC0D2D897F960084D583 /* AdyenCardScanner.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = C9FC2C3D2D50D0B700C9D0F3 /* AdyenCardScanner.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 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 */; }; diff --git a/Tests/IntegrationTests/Card Tests/Card Scanner/CardScannerControllerTests.swift b/Tests/IntegrationTests/Card Tests/Card Scanner/CardScannerControllerTests.swift index e15fea6317..d1d77bbe5b 100644 --- a/Tests/IntegrationTests/Card Tests/Card Scanner/CardScannerControllerTests.swift +++ b/Tests/IntegrationTests/Card Tests/Card Scanner/CardScannerControllerTests.swift @@ -4,145 +4,147 @@ // This file is open source and available under the MIT license. See the LICENSE file for more info. // -@testable import AdyenCard -@testable import AdyenCardScanner -import XCTest +#if canImport(AdyenCardScanner) + @testable import AdyenCard + @testable import AdyenCardScanner + import XCTest + + class CardScannerControllerTests: XCTestCase { + + var sut: CardScannerController! + var mockPresenter: UIViewController! + private var mockCardScanner: CardScannerProviderSpy! + + override func setUpWithError() throws { + mockPresenter = UIViewController() + mockCardScanner = CardScannerProviderSpy() + + sut = CardScannerController( + presenter: mockPresenter, + availabilityProvider: CardScannerAvailalabilityMock(), + cardScannerProvider: mockCardScanner + ) + + let window = UIWindow(frame: UIScreen.main.bounds) + window.rootViewController = mockPresenter + window.makeKeyAndVisible() + } -class CardScannerControllerTests: XCTestCase { + override func tearDownWithError() throws { + sut = nil + mockPresenter = nil + mockCardScanner = nil + } - var sut: CardScannerController! - var mockPresenter: UIViewController! - private var mockCardScanner: CardScannerProviderSpy! + // This test requires AdyenCardScanner framework to be imported for the test target + func test_scannerIsAvailable() { + XCTAssertTrue(sut.isScannerAvailable) + } - override func setUpWithError() throws { - mockPresenter = UIViewController() - mockCardScanner = CardScannerProviderSpy() + func test_openCardScanner_withTitle_presentsCorrectTitle() throws { + let expectation = XCTestExpectation(description: "Card scanner should complete the flow") + sut.onScanComplete = { result in + expectation.fulfill() + } - sut = CardScannerController( - presenter: mockPresenter, - availabilityProvider: CardScannerAvailalabilityMock(), - cardScannerProvider: mockCardScanner - ) + let expectedTitle = "Scan your card" + sut.title = expectedTitle + sut.openCardScanner() - let window = UIWindow(frame: UIScreen.main.bounds) - window.rootViewController = mockPresenter - window.makeKeyAndVisible() - } + let scannerNavigationController = mockPresenter.presentedViewController as? UINavigationController + let scannerViewController = scannerNavigationController?.topViewController + XCTAssertEqual(scannerViewController?.title, expectedTitle) - override func tearDownWithError() throws { - sut = nil - mockPresenter = nil - mockCardScanner = nil - } + sut.onScanComplete?(.success((nil, Date()))) + wait(for: [expectation], timeout: 3.0) + } - // This test requires AdyenCardScanner framework to be imported for the test target - func test_scannerIsAvailable() { - XCTAssertTrue(sut.isScannerAvailable) - } + func testHandleCardScanningCancelation() throws { + sut.openCardScanner() - func test_openCardScanner_withTitle_presentsCorrectTitle() throws { - let expectation = XCTestExpectation(description: "Card scanner should complete the flow") - sut.onScanComplete = { result in - expectation.fulfill() + sut.handleCardScanningCancelationWithCompletion { + XCTAssertNil(self.mockPresenter.presentedViewController) + } } - let expectedTitle = "Scan your card" - sut.title = expectedTitle - sut.openCardScanner() - - let scannerNavigationController = mockPresenter.presentedViewController as? UINavigationController - let scannerViewController = scannerNavigationController?.topViewController - XCTAssertEqual(scannerViewController?.title, expectedTitle) + func test_controller_returnsScannedCardValue() { + // Given + let expectation = XCTestExpectation(description: "Card scanner should complete the flow") - sut.onScanComplete?(.success((nil, Date()))) - wait(for: [expectation], timeout: 3.0) - } + let cardNumber = "1111 2222 3333 4444" + let expiryDate = Date(timeIntervalSince1970: 1742456818) - func testHandleCardScanningCancelation() throws { - sut.openCardScanner() + let expectedResult: CardScanDetails = (cardNumber, expiryDate) + let mockCard = CardScanDetails(cardNumber, expiryDate) - sut.handleCardScanningCancelationWithCompletion { - XCTAssertNil(self.mockPresenter.presentedViewController) - } - } + sut.onScanComplete = { result in + // Then + self.expect(result, toMatch: .success(expectedResult)) + expectation.fulfill() + } - func test_controller_returnsScannedCardValue() { - // Given - let expectation = XCTestExpectation(description: "Card scanner should complete the flow") + // When + sut.openCardScanner() + mockCardScanner.onScanComplete(result: .success(mockCard)) - let cardNumber = "1111 2222 3333 4444" - let expiryDate = Date(timeIntervalSince1970: 1742456818) - - let expectedResult: CardScanDetails = (cardNumber, expiryDate) - let mockCard = CardScanDetails(cardNumber, expiryDate) - - sut.onScanComplete = { result in - // Then - self.expect(result, toMatch: .success(expectedResult)) - expectation.fulfill() + wait(for: [expectation], timeout: 1.0) } - // When - sut.openCardScanner() - mockCardScanner.onScanComplete(result: .success(mockCard)) + func test_controller_returnsSimplifiedScannerError() { + // Given + let expectation = XCTestExpectation(description: "Card scanner should complete the flow") + let mockError = AdyenCardScanner.CardScannerError(kind: .authorizationDenied) + let expectedError = CardScannerController.CardScannerError.scanningError - wait(for: [expectation], timeout: 1.0) - } + sut.onScanComplete = { result in + // Then + self.expect(result, toMatch: .failure(expectedError)) + expectation.fulfill() + } - func test_controller_returnsSimplifiedScannerError() { - // Given - let expectation = XCTestExpectation(description: "Card scanner should complete the flow") - let mockError = AdyenCardScanner.CardScannerError(kind: .authorizationDenied) - let expectedError = CardScannerController.CardScannerError.scanningError + // When + sut.openCardScanner() + mockCardScanner.onScanComplete(result: .failure(mockError)) - sut.onScanComplete = { result in - // Then - self.expect(result, toMatch: .failure(expectedError)) - expectation.fulfill() + wait(for: [expectation], timeout: 1.0) } - // When - sut.openCardScanner() - mockCardScanner.onScanComplete(result: .failure(mockError)) - - wait(for: [expectation], timeout: 1.0) - } - - // MARK: - Helpers - - private func expect( - _ result: Result, - toMatch expectedResult: Result, - file: StaticString = #file, - line: UInt = #line - ) { - switch (result, expectedResult) { - case (.success(let (receivedCard, receivedDate)), .success(let (expectedCard, expectedDate))): - XCTAssertEqual(receivedCard, expectedCard, file: file, line: line) - XCTAssertEqual(receivedDate, expectedDate, file: file, line: line) - case let (.failure(receivedError as NSError), .failure(expectedError as NSError)): - XCTAssertEqual(receivedError, expectedError, file: file, line: line) - default: - XCTFail() + // MARK: - Helpers + + private func expect( + _ result: Result, + toMatch expectedResult: Result, + file: StaticString = #file, + line: UInt = #line + ) { + switch (result, expectedResult) { + case (.success(let (receivedCard, receivedDate)), .success(let (expectedCard, expectedDate))): + XCTAssertEqual(receivedCard, expectedCard, file: file, line: line) + XCTAssertEqual(receivedDate, expectedDate, file: file, line: line) + case let (.failure(receivedError as NSError), .failure(expectedError as NSError)): + XCTAssertEqual(receivedError, expectedError, file: file, line: line) + default: + XCTFail() + } } - } - private struct CardScannerAvailalabilityMock: CardScannerAvailability { - var isScannerAvailable: Bool { true } - } + private struct CardScannerAvailalabilityMock: CardScannerAvailability { + var isScannerAvailable: Bool { true } + } - private class CardScannerProviderSpy: CardScannerProviding { - private var completion: ((Result) -> Void)? = nil + private class CardScannerProviderSpy: CardScannerProviding { + private var completion: ((Result) -> Void)? = nil - func createCardScanner( - completion: @escaping (Result) -> Void - ) -> UIViewController? { - self.completion = completion - return UIViewController() - } + func createCardScanner( + completion: @escaping (Result) -> Void + ) -> UIViewController? { + self.completion = completion + return UIViewController() + } - func onScanComplete(result: Result) { - self.completion?(result) + func onScanComplete(result: Result) { + self.completion?(result) + } } } -} +#endif From e55a966eab5b1c2364169d63e9dfda4add985ec8 Mon Sep 17 00:00:00 2001 From: Andrii Mamchenko Date: Tue, 25 Mar 2025 15:29:32 +0100 Subject: [PATCH 16/23] Use loadViewIfNeeded in DocumentComponentTests --- .../BACS Direct Debit/DocumentComponentTests.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Tests/IntegrationTests/Components Tests/BACS Direct Debit/DocumentComponentTests.swift b/Tests/IntegrationTests/Components Tests/BACS Direct Debit/DocumentComponentTests.swift index 644f91d6e3..c777ca2ff4 100644 --- a/Tests/IntegrationTests/Components Tests/BACS Direct Debit/DocumentComponentTests.swift +++ b/Tests/IntegrationTests/Components Tests/BACS Direct Debit/DocumentComponentTests.swift @@ -28,12 +28,12 @@ class DocumentComponentTests: XCTestCase { sut.presentationDelegate = presentationDelegate sut.configuration.localizationParameters = LocalizationParameters(tableName: "test_table") - presentationDelegate.doPresent = { [self] component in + presentationDelegate.doPresent = { component in XCTAssertNotNil(component.viewController as? ADYViewController) let viewController = component.viewController as! ADYViewController - setupRootViewController(viewController) - + viewController.loadViewIfNeeded() + let pdfButton: UIButton? = viewController.view.findView(by: "mainButton") let messageLabel: UILabel? = viewController.view.findView(by: "messageLabel") let logo: UIImageView? = viewController.view.findView(by: "icon") From 45ae7b0e4f48bb976af740707f35cff1d83489da Mon Sep 17 00:00:00 2001 From: Andrii Mamchenko Date: Tue, 25 Mar 2025 15:57:29 +0100 Subject: [PATCH 17/23] Adjust DokuCOmponentTests --- .../Components Tests/Doku/DokuComponentTests.swift | 2 +- .../Helpers/XCTestCase+RootViewController.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/IntegrationTests/Components Tests/Doku/DokuComponentTests.swift b/Tests/IntegrationTests/Components Tests/Doku/DokuComponentTests.swift index 83bc235217..4f9e90a78f 100644 --- a/Tests/IntegrationTests/Components Tests/Doku/DokuComponentTests.swift +++ b/Tests/IntegrationTests/Components Tests/Doku/DokuComponentTests.swift @@ -90,7 +90,7 @@ class DokuComponentTests: XCTestCase { configuration: DokuComponent.Configuration() ) - setupRootViewController(sut.viewController) + sut.viewController.loadViewIfNeeded() wait(for: .milliseconds(300)) diff --git a/Tests/IntegrationTests/Helpers/XCTestCase+RootViewController.swift b/Tests/IntegrationTests/Helpers/XCTestCase+RootViewController.swift index ef47f3228f..294d2ea86e 100644 --- a/Tests/IntegrationTests/Helpers/XCTestCase+RootViewController.swift +++ b/Tests/IntegrationTests/Helpers/XCTestCase+RootViewController.swift @@ -59,7 +59,7 @@ extension XCTestCase { extension DispatchTimeInterval { /// .milliseconds(30) - static var aMoment: Self { .milliseconds(300) } + static var aMoment: Self { .milliseconds(30) } } extension XCTestCase { From 6e57e905c510c756fdcf54c86986c3147339a00a Mon Sep 17 00:00:00 2001 From: Andrii Mamchenko Date: Wed, 26 Mar 2025 13:18:12 +0100 Subject: [PATCH 18/23] Remove state from CardScannerControllerTests --- .../CardScannerControllerTests.swift | 56 +++++++++---------- 1 file changed, 27 insertions(+), 29 deletions(-) diff --git a/Tests/IntegrationTests/Card Tests/Card Scanner/CardScannerControllerTests.swift b/Tests/IntegrationTests/Card Tests/Card Scanner/CardScannerControllerTests.swift index e15fea6317..275bfe00af 100644 --- a/Tests/IntegrationTests/Card Tests/Card Scanner/CardScannerControllerTests.swift +++ b/Tests/IntegrationTests/Card Tests/Card Scanner/CardScannerControllerTests.swift @@ -10,38 +10,20 @@ import XCTest class CardScannerControllerTests: XCTestCase { - var sut: CardScannerController! - var mockPresenter: UIViewController! - private var mockCardScanner: CardScannerProviderSpy! - - override func setUpWithError() throws { - mockPresenter = UIViewController() - mockCardScanner = CardScannerProviderSpy() - - sut = CardScannerController( - presenter: mockPresenter, - availabilityProvider: CardScannerAvailalabilityMock(), - cardScannerProvider: mockCardScanner - ) - - let window = UIWindow(frame: UIScreen.main.bounds) - window.rootViewController = mockPresenter - window.makeKeyAndVisible() - } - - override func tearDownWithError() throws { - sut = nil - mockPresenter = nil - mockCardScanner = nil - } - // This test requires AdyenCardScanner framework to be imported for the test target func test_scannerIsAvailable() { + let (sut, _, _) = makeSUT() XCTAssertTrue(sut.isScannerAvailable) } func test_openCardScanner_withTitle_presentsCorrectTitle() throws { let expectation = XCTestExpectation(description: "Card scanner should complete the flow") + let (sut, presenter, _) = makeSUT() + + let window = UIWindow(frame: UIScreen.main.bounds) + window.rootViewController = presenter + window.makeKeyAndVisible() + sut.onScanComplete = { result in expectation.fulfill() } @@ -50,7 +32,7 @@ class CardScannerControllerTests: XCTestCase { sut.title = expectedTitle sut.openCardScanner() - let scannerNavigationController = mockPresenter.presentedViewController as? UINavigationController + let scannerNavigationController = presenter.presentedViewController as? UINavigationController let scannerViewController = scannerNavigationController?.topViewController XCTAssertEqual(scannerViewController?.title, expectedTitle) @@ -59,10 +41,12 @@ class CardScannerControllerTests: XCTestCase { } func testHandleCardScanningCancelation() throws { + let (sut, presenter, _) = makeSUT() + sut.openCardScanner() sut.handleCardScanningCancelationWithCompletion { - XCTAssertNil(self.mockPresenter.presentedViewController) + XCTAssertNil(presenter.presentedViewController) } } @@ -76,6 +60,7 @@ class CardScannerControllerTests: XCTestCase { let expectedResult: CardScanDetails = (cardNumber, expiryDate) let mockCard = CardScanDetails(cardNumber, expiryDate) + let (sut, presenter, cardScanner) = makeSUT() sut.onScanComplete = { result in // Then self.expect(result, toMatch: .success(expectedResult)) @@ -84,7 +69,7 @@ class CardScannerControllerTests: XCTestCase { // When sut.openCardScanner() - mockCardScanner.onScanComplete(result: .success(mockCard)) + cardScanner.onScanComplete(result: .success(mockCard)) wait(for: [expectation], timeout: 1.0) } @@ -94,6 +79,7 @@ class CardScannerControllerTests: XCTestCase { let expectation = XCTestExpectation(description: "Card scanner should complete the flow") let mockError = AdyenCardScanner.CardScannerError(kind: .authorizationDenied) let expectedError = CardScannerController.CardScannerError.scanningError + let (sut, presenter, cardScanner) = makeSUT() sut.onScanComplete = { result in // Then @@ -103,13 +89,25 @@ class CardScannerControllerTests: XCTestCase { // When sut.openCardScanner() - mockCardScanner.onScanComplete(result: .failure(mockError)) + cardScanner.onScanComplete(result: .failure(mockError)) wait(for: [expectation], timeout: 1.0) } // MARK: - Helpers + private func makeSUT() -> (CardScannerController, UIViewController, CardScannerProviderSpy) { + let presenter = UIViewController() + let cardScanner = CardScannerProviderSpy() + + let sut = CardScannerController( + presenter: presenter, + availabilityProvider: CardScannerAvailalabilityMock(), + cardScannerProvider: cardScanner + ) + return (sut, presenter, cardScanner) + } + private func expect( _ result: Result, toMatch expectedResult: Result, From ef4eeaeab1484156f33561c1375eae5858fbebdd Mon Sep 17 00:00:00 2001 From: Andrii Mamchenko Date: Wed, 26 Mar 2025 13:18:43 +0100 Subject: [PATCH 19/23] Add missing scope --- AdyenCard/Components/Card/CardScannerController.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AdyenCard/Components/Card/CardScannerController.swift b/AdyenCard/Components/Card/CardScannerController.swift index cbfaf66c4f..d160fcad75 100644 --- a/AdyenCard/Components/Card/CardScannerController.swift +++ b/AdyenCard/Components/Card/CardScannerController.swift @@ -147,7 +147,7 @@ internal protocol CardScannerControlling: CardScannerAvailability { // MARK: - Helpers internal struct DummyCardScannerAvailability: CardScannerAvailability { - var isScannerAvailable: Bool { false } + internal var isScannerAvailable: Bool { false } } internal struct DummyCardScannerProvider: CardScannerProviding { From 70df08a0cdb0df07d9ebf819a812d4fcd9ad834f Mon Sep 17 00:00:00 2001 From: Andrii Mamchenko Date: Wed, 26 Mar 2025 13:24:41 +0100 Subject: [PATCH 20/23] Add conditional check for library import --- .../Card Tests/Card Scanner/CardScannerControllerTests.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Tests/IntegrationTests/Card Tests/Card Scanner/CardScannerControllerTests.swift b/Tests/IntegrationTests/Card Tests/Card Scanner/CardScannerControllerTests.swift index 275bfe00af..1652abf0cb 100644 --- a/Tests/IntegrationTests/Card Tests/Card Scanner/CardScannerControllerTests.swift +++ b/Tests/IntegrationTests/Card Tests/Card Scanner/CardScannerControllerTests.swift @@ -4,6 +4,7 @@ // This file is open source and available under the MIT license. See the LICENSE file for more info. // +#if canImport(AdyenCardScanner) @testable import AdyenCard @testable import AdyenCardScanner import XCTest @@ -144,3 +145,4 @@ class CardScannerControllerTests: XCTestCase { } } } +#endif From d84845eec9348cf38c01a786bcaad03888679d9f Mon Sep 17 00:00:00 2001 From: Andrii Mamchenko Date: Wed, 26 Mar 2025 14:44:28 +0100 Subject: [PATCH 21/23] Update adyen-networking version in Cartfile --- Cartfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cartfile b/Cartfile index f8c62fbbb3..ffc9192740 100644 --- a/Cartfile +++ b/Cartfile @@ -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 \ No newline at end of file From 497cc6ce2be5cbf6288cc8c300d2878b60b7e7c0 Mon Sep 17 00:00:00 2001 From: Andrii Mamchenko Date: Wed, 26 Mar 2025 14:44:53 +0100 Subject: [PATCH 22/23] Apply swiftformat --- .../CardScannerControllerTests.swift | 216 +++++++++--------- 1 file changed, 108 insertions(+), 108 deletions(-) diff --git a/Tests/IntegrationTests/Card Tests/Card Scanner/CardScannerControllerTests.swift b/Tests/IntegrationTests/Card Tests/Card Scanner/CardScannerControllerTests.swift index 1652abf0cb..cfbd6583de 100644 --- a/Tests/IntegrationTests/Card Tests/Card Scanner/CardScannerControllerTests.swift +++ b/Tests/IntegrationTests/Card Tests/Card Scanner/CardScannerControllerTests.swift @@ -5,144 +5,144 @@ // #if canImport(AdyenCardScanner) -@testable import AdyenCard -@testable import AdyenCardScanner -import XCTest + @testable import AdyenCard + @testable import AdyenCardScanner + import XCTest -class CardScannerControllerTests: XCTestCase { + class CardScannerControllerTests: XCTestCase { - // This test requires AdyenCardScanner framework to be imported for the test target - func test_scannerIsAvailable() { - let (sut, _, _) = makeSUT() - XCTAssertTrue(sut.isScannerAvailable) - } + // This test requires AdyenCardScanner framework to be imported for the test target + func test_scannerIsAvailable() { + let (sut, _, _) = makeSUT() + XCTAssertTrue(sut.isScannerAvailable) + } - func test_openCardScanner_withTitle_presentsCorrectTitle() throws { - let expectation = XCTestExpectation(description: "Card scanner should complete the flow") - let (sut, presenter, _) = makeSUT() + func test_openCardScanner_withTitle_presentsCorrectTitle() throws { + let expectation = XCTestExpectation(description: "Card scanner should complete the flow") + let (sut, presenter, _) = makeSUT() - let window = UIWindow(frame: UIScreen.main.bounds) - window.rootViewController = presenter - window.makeKeyAndVisible() + let window = UIWindow(frame: UIScreen.main.bounds) + window.rootViewController = presenter + window.makeKeyAndVisible() - sut.onScanComplete = { result in - expectation.fulfill() - } + sut.onScanComplete = { result in + expectation.fulfill() + } - let expectedTitle = "Scan your card" - sut.title = expectedTitle - sut.openCardScanner() + let expectedTitle = "Scan your card" + sut.title = expectedTitle + sut.openCardScanner() - let scannerNavigationController = presenter.presentedViewController as? UINavigationController - let scannerViewController = scannerNavigationController?.topViewController - XCTAssertEqual(scannerViewController?.title, expectedTitle) + let scannerNavigationController = presenter.presentedViewController as? UINavigationController + let scannerViewController = scannerNavigationController?.topViewController + XCTAssertEqual(scannerViewController?.title, expectedTitle) - sut.onScanComplete?(.success((nil, Date()))) - wait(for: [expectation], timeout: 3.0) - } + sut.onScanComplete?(.success((nil, Date()))) + wait(for: [expectation], timeout: 3.0) + } - func testHandleCardScanningCancelation() throws { - let (sut, presenter, _) = makeSUT() + func testHandleCardScanningCancelation() throws { + let (sut, presenter, _) = makeSUT() - sut.openCardScanner() + sut.openCardScanner() - sut.handleCardScanningCancelationWithCompletion { - XCTAssertNil(presenter.presentedViewController) + sut.handleCardScanningCancelationWithCompletion { + XCTAssertNil(presenter.presentedViewController) + } } - } - func test_controller_returnsScannedCardValue() { - // Given - let expectation = XCTestExpectation(description: "Card scanner should complete the flow") + func test_controller_returnsScannedCardValue() { + // Given + let expectation = XCTestExpectation(description: "Card scanner should complete the flow") - let cardNumber = "1111 2222 3333 4444" - let expiryDate = Date(timeIntervalSince1970: 1742456818) + let cardNumber = "1111 2222 3333 4444" + let expiryDate = Date(timeIntervalSince1970: 1742456818) - let expectedResult: CardScanDetails = (cardNumber, expiryDate) - let mockCard = CardScanDetails(cardNumber, expiryDate) + let expectedResult: CardScanDetails = (cardNumber, expiryDate) + let mockCard = CardScanDetails(cardNumber, expiryDate) - let (sut, presenter, cardScanner) = makeSUT() - sut.onScanComplete = { result in - // Then - self.expect(result, toMatch: .success(expectedResult)) - expectation.fulfill() - } + let (sut, presenter, cardScanner) = makeSUT() + sut.onScanComplete = { result in + // Then + self.expect(result, toMatch: .success(expectedResult)) + expectation.fulfill() + } - // When - sut.openCardScanner() - cardScanner.onScanComplete(result: .success(mockCard)) + // When + sut.openCardScanner() + cardScanner.onScanComplete(result: .success(mockCard)) - wait(for: [expectation], timeout: 1.0) - } - - func test_controller_returnsSimplifiedScannerError() { - // Given - let expectation = XCTestExpectation(description: "Card scanner should complete the flow") - let mockError = AdyenCardScanner.CardScannerError(kind: .authorizationDenied) - let expectedError = CardScannerController.CardScannerError.scanningError - let (sut, presenter, cardScanner) = makeSUT() - - sut.onScanComplete = { result in - // Then - self.expect(result, toMatch: .failure(expectedError)) - expectation.fulfill() + wait(for: [expectation], timeout: 1.0) } - // When - sut.openCardScanner() - cardScanner.onScanComplete(result: .failure(mockError)) + func test_controller_returnsSimplifiedScannerError() { + // Given + let expectation = XCTestExpectation(description: "Card scanner should complete the flow") + let mockError = AdyenCardScanner.CardScannerError(kind: .authorizationDenied) + let expectedError = CardScannerController.CardScannerError.scanningError + let (sut, presenter, cardScanner) = makeSUT() - wait(for: [expectation], timeout: 1.0) - } + sut.onScanComplete = { result in + // Then + self.expect(result, toMatch: .failure(expectedError)) + expectation.fulfill() + } - // MARK: - Helpers + // When + sut.openCardScanner() + cardScanner.onScanComplete(result: .failure(mockError)) - private func makeSUT() -> (CardScannerController, UIViewController, CardScannerProviderSpy) { - let presenter = UIViewController() - let cardScanner = CardScannerProviderSpy() + wait(for: [expectation], timeout: 1.0) + } - let sut = CardScannerController( - presenter: presenter, - availabilityProvider: CardScannerAvailalabilityMock(), - cardScannerProvider: cardScanner - ) - return (sut, presenter, cardScanner) - } + // MARK: - Helpers - private func expect( - _ result: Result, - toMatch expectedResult: Result, - file: StaticString = #file, - line: UInt = #line - ) { - switch (result, expectedResult) { - case (.success(let (receivedCard, receivedDate)), .success(let (expectedCard, expectedDate))): - XCTAssertEqual(receivedCard, expectedCard, file: file, line: line) - XCTAssertEqual(receivedDate, expectedDate, file: file, line: line) - case let (.failure(receivedError as NSError), .failure(expectedError as NSError)): - XCTAssertEqual(receivedError, expectedError, file: file, line: line) - default: - XCTFail() - } - } + private func makeSUT() -> (CardScannerController, UIViewController, CardScannerProviderSpy) { + let presenter = UIViewController() + let cardScanner = CardScannerProviderSpy() - private struct CardScannerAvailalabilityMock: CardScannerAvailability { - var isScannerAvailable: Bool { true } - } + let sut = CardScannerController( + presenter: presenter, + availabilityProvider: CardScannerAvailalabilityMock(), + cardScannerProvider: cardScanner + ) + return (sut, presenter, cardScanner) + } - private class CardScannerProviderSpy: CardScannerProviding { - private var completion: ((Result) -> Void)? = nil + private func expect( + _ result: Result, + toMatch expectedResult: Result, + file: StaticString = #file, + line: UInt = #line + ) { + switch (result, expectedResult) { + case (.success(let (receivedCard, receivedDate)), .success(let (expectedCard, expectedDate))): + XCTAssertEqual(receivedCard, expectedCard, file: file, line: line) + XCTAssertEqual(receivedDate, expectedDate, file: file, line: line) + case let (.failure(receivedError as NSError), .failure(expectedError as NSError)): + XCTAssertEqual(receivedError, expectedError, file: file, line: line) + default: + XCTFail() + } + } - func createCardScanner( - completion: @escaping (Result) -> Void - ) -> UIViewController? { - self.completion = completion - return UIViewController() + private struct CardScannerAvailalabilityMock: CardScannerAvailability { + var isScannerAvailable: Bool { true } } - func onScanComplete(result: Result) { - self.completion?(result) + private class CardScannerProviderSpy: CardScannerProviding { + private var completion: ((Result) -> Void)? = nil + + func createCardScanner( + completion: @escaping (Result) -> Void + ) -> UIViewController? { + self.completion = completion + return UIViewController() + } + + func onScanComplete(result: Result) { + self.completion?(result) + } } } -} #endif From 8e033ae609faffa2470bfe076bbf6c36cb6413ca Mon Sep 17 00:00:00 2001 From: Andrii Mamchenko Date: Wed, 26 Mar 2025 16:04:05 +0100 Subject: [PATCH 23/23] Remove linked frameworks for test target --- Adyen.xcodeproj/project.pbxproj | 3 --- 1 file changed, 3 deletions(-) diff --git a/Adyen.xcodeproj/project.pbxproj b/Adyen.xcodeproj/project.pbxproj index 56102a0dc8..6d200d2d16 100644 --- a/Adyen.xcodeproj/project.pbxproj +++ b/Adyen.xcodeproj/project.pbxproj @@ -1510,7 +1510,6 @@ dstPath = ""; dstSubfolderSpec = 10; files = ( - B605AC0D2D897F960084D583 /* AdyenCardScanner.framework in Embed Frameworks */, ); name = "Embed Frameworks"; runOnlyForDeploymentPostprocessing = 0; @@ -2660,8 +2659,6 @@ 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 */, E2C0E03D22097917008616F6 /* Adyen.framework in Frameworks */,