Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
12 changes: 12 additions & 0 deletions Adyen.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -533,6 +533,7 @@
C930FB6E269D79E0006A26D2 /* AffirmDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = C930FB6D269D79E0006A26D2 /* AffirmDetails.swift */; };
C933801E276A06E1005B66CD /* BACSDirectDebitComponentTracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = C933801D276A06E1005B66CD /* BACSDirectDebitComponentTracker.swift */; };
C9338020276A09E7005B66CD /* BACSDirectDebitComponentTrackerProtocolMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = C933801F276A09E7005B66CD /* BACSDirectDebitComponentTrackerProtocolMock.swift */; };
C933BE592D91659600EBFF7E /* ExpirationDateFormatterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C933BE582D91659600EBFF7E /* ExpirationDateFormatterTests.swift */; };
C93844732D79DF3B00B1E747 /* CardScannerViewModelMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = C93844722D79DF3B00B1E747 /* CardScannerViewModelMock.swift */; };
C93844752D7B1C4100B1E747 /* CardScannerViewControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C93844742D7B1C4100B1E747 /* CardScannerViewControllerTests.swift */; };
C93B01B72760B03400D311A1 /* BACSConfirmationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C93B01B62760B03400D311A1 /* BACSConfirmationViewController.swift */; };
Expand All @@ -541,6 +542,8 @@
C9454C38276A34150086C218 /* BACSDirectDebitPresentationDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9454C35276A33A00086C218 /* BACSDirectDebitPresentationDelegate.swift */; };
C94632BE27BA6985003DD81F /* AnalyticsProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = C94632BD27BA6985003DD81F /* AnalyticsProvider.swift */; };
C94639D02D5F68BC004A1B0C /* CaptureSessionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C94639CF2D5F68BC004A1B0C /* CaptureSessionManager.swift */; };
C948B6672D8AC5C9000331AA /* CardImageParserTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C948B6662D8AC5C9000331AA /* CardImageParserTests.swift */; };
C948B6692D8AD0CA000331AA /* ExpirationDateFormattingMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = C948B6682D8AD0CA000331AA /* ExpirationDateFormattingMock.swift */; };
C95903DE275A48D000E7D3BC /* BACSDirectDebitComponentTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C95903DD275A48D000E7D3BC /* BACSDirectDebitComponentTests.swift */; };
C96688BF26A6FC1C00DC7297 /* AffirmComponentTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C96688BE26A6FC1C00DC7297 /* AffirmComponentTests.swift */; };
C9680BE62D6CAD590076B93D /* CardScannerAssembler.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9680BE52D6CAD590076B93D /* CardScannerAssembler.swift */; };
Expand Down Expand Up @@ -1910,13 +1913,16 @@
C930FB6D269D79E0006A26D2 /* AffirmDetails.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AffirmDetails.swift; sourceTree = "<group>"; };
C933801D276A06E1005B66CD /* BACSDirectDebitComponentTracker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BACSDirectDebitComponentTracker.swift; sourceTree = "<group>"; };
C933801F276A09E7005B66CD /* BACSDirectDebitComponentTrackerProtocolMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BACSDirectDebitComponentTrackerProtocolMock.swift; sourceTree = "<group>"; };
C933BE582D91659600EBFF7E /* ExpirationDateFormatterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExpirationDateFormatterTests.swift; sourceTree = "<group>"; };
C93844722D79DF3B00B1E747 /* CardScannerViewModelMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardScannerViewModelMock.swift; sourceTree = "<group>"; };
C93844742D7B1C4100B1E747 /* CardScannerViewControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardScannerViewControllerTests.swift; sourceTree = "<group>"; };
C93B01B62760B03400D311A1 /* BACSConfirmationViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BACSConfirmationViewController.swift; sourceTree = "<group>"; };
C93B01B82760B06300D311A1 /* BACSConfirmationPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BACSConfirmationPresenter.swift; sourceTree = "<group>"; };
C9454C35276A33A00086C218 /* BACSDirectDebitPresentationDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BACSDirectDebitPresentationDelegate.swift; sourceTree = "<group>"; };
C94632BD27BA6985003DD81F /* AnalyticsProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsProvider.swift; sourceTree = "<group>"; };
C94639CF2D5F68BC004A1B0C /* CaptureSessionManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CaptureSessionManager.swift; sourceTree = "<group>"; };
C948B6662D8AC5C9000331AA /* CardImageParserTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardImageParserTests.swift; sourceTree = "<group>"; };
C948B6682D8AD0CA000331AA /* ExpirationDateFormattingMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExpirationDateFormattingMock.swift; sourceTree = "<group>"; };
C95903DD275A48D000E7D3BC /* BACSDirectDebitComponentTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BACSDirectDebitComponentTests.swift; sourceTree = "<group>"; };
C95C89312BF63A3500C47296 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
C96688BE26A6FC1C00DC7297 /* AffirmComponentTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AffirmComponentTests.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -3502,6 +3508,7 @@
C91BF2572D771667001F19DE /* CardImageParsingMock.swift */,
C91BF2592D7716D4001F19DE /* CaptureSessionManagingMock.swift */,
C93844722D79DF3B00B1E747 /* CardScannerViewModelMock.swift */,
C948B6682D8AD0CA000331AA /* ExpirationDateFormattingMock.swift */,
);
path = Mocks;
sourceTree = "<group>";
Expand Down Expand Up @@ -3610,6 +3617,8 @@
C96844922D771328001DB7F1 /* CardScannerViewModelTests.swift */,
C91BF25B2D772233001F19DE /* Assets.xcassets */,
C93844742D7B1C4100B1E747 /* CardScannerViewControllerTests.swift */,
C948B6662D8AC5C9000331AA /* CardImageParserTests.swift */,
C933BE582D91659600EBFF7E /* ExpirationDateFormatterTests.swift */,
);
path = AdyenCardScannerTests;
sourceTree = "<group>";
Expand Down Expand Up @@ -7094,7 +7103,10 @@
buildActionMask = 2147483647;
files = (
C91BF2582D771667001F19DE /* CardImageParsingMock.swift in Sources */,
C933BE592D91659600EBFF7E /* ExpirationDateFormatterTests.swift in Sources */,
C948B6692D8AD0CA000331AA /* ExpirationDateFormattingMock.swift in Sources */,
C93844752D7B1C4100B1E747 /* CardScannerViewControllerTests.swift in Sources */,
C948B6672D8AC5C9000331AA /* CardImageParserTests.swift in Sources */,
C93844732D79DF3B00B1E747 /* CardScannerViewModelMock.swift in Sources */,
C91BF25A2D7716D4001F19DE /* CaptureSessionManagingMock.swift in Sources */,
C96844932D771328001DB7F1 /* CardScannerViewModelTests.swift in Sources */,
Expand Down
6 changes: 1 addition & 5 deletions AdyenCardScanner/Sources/CardScannerAssembler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,7 @@ internal class CardScannerAssembler: CardScannerAssembling {
internal func resolveCardScannerViewController(
completion: @escaping (Result<CardScanDetails, CardScannerError>) -> Void
) -> UIViewController? {
guard let captureDevice else {
// TODO: Remove after error handling implementation
print("Card scanner: cannot resolve view controller, missing capture device")
return nil
}
guard let captureDevice else { return nil }

let expireDateFormatter = ExpirationDateFormatter()
let cardImageParser = CardImageParser(expirationDateFormatter: expireDateFormatter)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"images" : [
{
"filename" : "test-card-number-1.jpg",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"images" : [
{
"filename" : "test-card-number-2.jpg",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"images" : [
{
"filename" : "test-card-number-3.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
158 changes: 158 additions & 0 deletions AdyenCardScannerTests/CardImageParserTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
//
// 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 AdyenCardScanner
import XCTest


final class CardImageParserTests: XCTestCase {

var sut: CardImageParser!
var expirationDateFormatter: ExpirationDateFormatter!

override func setUpWithError() throws {
try super.setUpWithError()
expirationDateFormatter = ExpirationDateFormatter()
}

override func tearDownWithError() throws {
sut = nil
expirationDateFormatter = nil
try super.tearDownWithError()
}

func testParseImageWithHighContrastCardImage() throws {
// Given
let expirationDateFormatter = ExpirationDateFormatter()
sut = CardImageParser(expirationDateFormatter: expirationDateFormatter)

let testCreditCard = try XCTUnwrap(testCreditCardHighContrast)
let expectation = expectation(description: "Image should be parsed")

// When
sut.parse(image: testCreditCard.image) { receivedCreditCard in
expectation.fulfill()

let expectedCreditCard = testCreditCard.creditCard
XCTAssertEqual(expectedCreditCard.number, receivedCreditCard.number)
XCTAssertEqual(expectedCreditCard.expirationDate, receivedCreditCard.expirationDate)
}

// Then
wait(for: [expectation], timeout: 0.1)
}

func testParseImageWithLowContrastCardImage() throws {
// Given
let expirationDateFormatter = ExpirationDateFormatter()
sut = CardImageParser(expirationDateFormatter: expirationDateFormatter)

let testCreditCard = try XCTUnwrap(testCreditCardLowContrast)
let expectation = expectation(description: "Image should be parsed")

// When
sut.parse(image: testCreditCard.image) { receivedCreditCard in
expectation.fulfill()

let expectedCreditCard = testCreditCard.creditCard
XCTAssertEqual(expectedCreditCard.number, receivedCreditCard.number)
XCTAssertEqual(expectedCreditCard.expirationDate, receivedCreditCard.expirationDate)
}

// Then
wait(for: [expectation], timeout: 0.1)
}

func testParseImageWithInvalidLuhnCheck() throws {
// Given
let expirationDateFormatter = ExpirationDateFormatter()
sut = CardImageParser(expirationDateFormatter: expirationDateFormatter)

let testCreditCard = try XCTUnwrap(testCreditCardInvalidLuhn)
let expectation = expectation(description: "Image should be parsed")
expectation.isInverted = true

// When
sut.parse(image: testCreditCard.image) { receivedCreditCard in
expectation.fulfill()

let expectedCreditCard = testCreditCard.creditCard
XCTAssertEqual(expectedCreditCard.number, receivedCreditCard.number)
XCTAssertEqual(expectedCreditCard.expirationDate, receivedCreditCard.expirationDate)
}

// Then
wait(for: [expectation], timeout: 0.1)
}

// MARK: - Private

private struct TestCreditCard {
let image: CIImage
let creditCard: CreditCard
}

private var testCreditCardHighContrast: TestCreditCard? {
let image = UIImage(
named: "test-card-number-1",
in: Bundle(for: type(of: self)),
compatibleWith: nil
)
guard let cgImage = image?.cgImage else { return nil }
let originalImage = CIImage(cgImage: cgImage)

let creditCard = CreditCard(
number: "4111110003920001",
expirationDate: dateFrom("03/30")
)
return TestCreditCard(
image: originalImage,
creditCard: creditCard
)
}

private var testCreditCardLowContrast: TestCreditCard? {
let image = UIImage(
named: "test-card-number-2",
in: Bundle(for: type(of: self)),
compatibleWith: nil
)
guard let cgImage = image?.cgImage else { return nil }
let originalImage = CIImage(cgImage: cgImage)

let creditCard = CreditCard(
number: "5413330089099999",
expirationDate: dateFrom("02/28")
)
return TestCreditCard(
image: originalImage,
creditCard: creditCard
)
}

private var testCreditCardInvalidLuhn: TestCreditCard? {
let image = UIImage(
named: "test-card-number-3",
in: Bundle(for: type(of: self)),
compatibleWith: nil
)
guard let cgImage = image?.cgImage else { return nil }
let originalImage = CIImage(cgImage: cgImage)

let creditCard = CreditCard(
number: "5412751234123456",
expirationDate: dateFrom("12/23")
)
return TestCreditCard(
image: originalImage,
creditCard: creditCard
)
}

private func dateFrom(_ string: String) -> Date? {
return expirationDateFormatter.date(from: string)
}
}
69 changes: 69 additions & 0 deletions AdyenCardScannerTests/ExpirationDateFormatterTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
//
// Copyright (c) 2025 Adyen N.V.
//
// This file is open source and available under the MIT license. See the LICENSE file for more info.
//

import XCTest
@testable import AdyenCardScanner

final class ExpirationDateFormatterTests: XCTestCase {

var sut: ExpirationDateFormatter!
var dateFormatter = DateFormatter()

override func setUpWithError() throws {
try super.setUpWithError()
dateFormatter.dateFormat = "MM/yyyy"
dateFormatter.locale = Locale.current
dateFormatter.timeZone = TimeZone(secondsFromGMT: 0)
}

override func tearDownWithError() throws {
sut = nil
try super.tearDownWithError()
}

func testDateFromStringWithShortFormatDate() throws {
// Given
let stringDate = "03/2028"
let expectedDate = dateFormatter.date(from: stringDate)

let shortFormatDate = "03/28"
sut = ExpirationDateFormatter()

// When
let receivedDate = try XCTUnwrap(sut.date(from: shortFormatDate))

// Then
XCTAssertEqual(receivedDate, expectedDate)
}

func testDateFromStringWithLongFormatDate() throws {
// Given
let stringDate = "03/2028"
let expectedDate = dateFormatter.date(from: stringDate)

let longFormatDate = "03/2028"
sut = ExpirationDateFormatter()

// When
let receivedDate = try XCTUnwrap(sut.date(from: longFormatDate))

// Then
XCTAssertEqual(receivedDate, expectedDate)
}

func testDateFromStringWithInvalidFormatDateShouldReturnNil() throws {
// Given
let longFormatDate = "2028/03"
sut = ExpirationDateFormatter()

// When
let receivedDate = sut.date(from: longFormatDate)

// Then
XCTAssertNil(receivedDate)
}

}
29 changes: 29 additions & 0 deletions AdyenCardScannerTests/Mocks/ExpirationDateFormattingMock.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
//
// 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 AdyenCardScanner
import Foundation

class ExpirationDateFormattingMock: ExpirationDateFormatting {
Comment thread
nauaros marked this conversation as resolved.
// MARK: - date(from:)

var dateCallsCount = 0
var dateCalled: Bool {
dateCallsCount > 0
}

var dateReceivedString: String?
var dateReceivedInvocations: [String] = []
var dateReturnValue: Date?
var dateClosure: ((String) -> Date?)?

func date(from string: String) -> Date? {
dateCallsCount += 1
dateReceivedString = string
dateReceivedInvocations.append(string)
return dateClosure?(string) ?? dateReturnValue
}
}
Loading