From 0c31ad8a8a1ab5a9a17e1f519f952e40b7346c00 Mon Sep 17 00:00:00 2001 From: Andrii Vysotskyi Date: Wed, 7 Aug 2024 15:40:31 +0200 Subject: [PATCH 01/28] Support throwing methods in lock --- .../Sources/Core/Utils/UnfairlyLocked/UnfairLock.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/ProcessOut/Sources/Core/Utils/UnfairlyLocked/UnfairLock.swift b/Sources/ProcessOut/Sources/Core/Utils/UnfairlyLocked/UnfairLock.swift index e9e0c59cc..0c58c9d12 100644 --- a/Sources/ProcessOut/Sources/Core/Utils/UnfairlyLocked/UnfairLock.swift +++ b/Sources/ProcessOut/Sources/Core/Utils/UnfairlyLocked/UnfairLock.swift @@ -15,12 +15,12 @@ final class UnfairLock: Sendable { unfairLock.initialize(to: os_unfair_lock()) } - func withLock(_ body: () -> R) -> R { + func withLock(_ body: () throws -> R) rethrows -> R { defer { os_unfair_lock_unlock(unfairLock) } os_unfair_lock_lock(unfairLock) - return body() + return try body() } deinit { From fe1b0c7dfc0730e4a4d7e9c7ac0745bc0a359edd Mon Sep 17 00:00:00 2001 From: Andrii Vysotskyi Date: Wed, 7 Aug 2024 15:42:32 +0200 Subject: [PATCH 02/28] Commit generated changes --- Sources/ProcessOut/Sources/Generated/Sourcery+Generated.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Sources/ProcessOut/Sources/Generated/Sourcery+Generated.swift b/Sources/ProcessOut/Sources/Generated/Sourcery+Generated.swift index f74d9e2de..b2da9612d 100644 --- a/Sources/ProcessOut/Sources/Generated/Sourcery+Generated.swift +++ b/Sources/ProcessOut/Sources/Generated/Sourcery+Generated.swift @@ -50,6 +50,7 @@ extension POCreateCustomerTokenRequest { enum CodingKeys: String, CodingKey { case verify case invoiceReturnUrl + case returnUrl } } From 3c597b8aa850b5defb0cc835ad8fb927b5d064bc Mon Sep 17 00:00:00 2001 From: Andrii Vysotskyi Date: Wed, 7 Aug 2024 15:43:07 +0200 Subject: [PATCH 03/28] Fix 3DS challenge response --- .../ProcessOut/Sources/Services/3DS/DefaultThreeDSService.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ProcessOut/Sources/Services/3DS/DefaultThreeDSService.swift b/Sources/ProcessOut/Sources/Services/3DS/DefaultThreeDSService.swift index a8cf84259..f9181bf30 100644 --- a/Sources/ProcessOut/Sources/Services/3DS/DefaultThreeDSService.swift +++ b/Sources/ProcessOut/Sources/Services/3DS/DefaultThreeDSService.swift @@ -84,7 +84,7 @@ final class DefaultThreeDSService: ThreeDSService { throw POFailure(message: message, code: .internal(.mobile), underlyingError: error) } let response = AuthenticationResponse(url: nil, body: encodedChallengeResult) - return try Constants.tokenPrefix + encode(authenticationResponse: response) + return try encode(authenticationResponse: response) } // MARK: - Web Based 3DS From ce80614ad98aae481f4e16a5a01f34358b691cf7 Mon Sep 17 00:00:00 2001 From: Andrii Vysotskyi Date: Wed, 7 Aug 2024 15:43:40 +0200 Subject: [PATCH 04/28] Expose API to create redirect URL --- .../DefaultAlternativePaymentsService.swift | 16 +++++++++++----- .../POAlternativePaymentsService.swift | 6 ++++++ 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/Sources/ProcessOut/Sources/Services/AlternativePayments/DefaultAlternativePaymentsService.swift b/Sources/ProcessOut/Sources/Services/AlternativePayments/DefaultAlternativePaymentsService.swift index b5548c440..e3150c576 100644 --- a/Sources/ProcessOut/Sources/Services/AlternativePayments/DefaultAlternativePaymentsService.swift +++ b/Sources/ProcessOut/Sources/Services/AlternativePayments/DefaultAlternativePaymentsService.swift @@ -22,18 +22,24 @@ final class DefaultAlternativePaymentsService: POAlternativePaymentsService { // MARK: - POAlternativePaymentsService func tokenize(request: POAlternativePaymentTokenizationRequest) async throws -> POAlternativePaymentResponse { - let pathComponents = [request.customerId, request.tokenId, "redirect", request.gatewayConfigurationId] - let redirectUrl = try url(with: pathComponents, additionalData: request.additionalData) - return try await authenticate(using: redirectUrl) + try await authenticate(using: url(for: request)) } func authorize(request: POAlternativePaymentAuthorizationRequest) async throws -> POAlternativePaymentResponse { + try await authenticate(using: url(for: request)) + } + + func url(for request: POAlternativePaymentTokenizationRequest) throws -> URL { + let pathComponents = [request.customerId, request.tokenId, "redirect", request.gatewayConfigurationId] + return try url(with: pathComponents, additionalData: request.additionalData) + } + + func url(for request: POAlternativePaymentAuthorizationRequest) throws -> URL { var pathComponents = [request.invoiceId, "redirect", request.gatewayConfigurationId] if let tokenId = request.tokenId { pathComponents += ["tokenized", tokenId] } - let redirectUrl = try url(with: pathComponents, additionalData: request.additionalData) - return try await authenticate(using: redirectUrl) + return try url(with: pathComponents, additionalData: request.additionalData) } func authenticate(using url: URL) async throws -> POAlternativePaymentResponse { diff --git a/Sources/ProcessOut/Sources/Services/AlternativePayments/POAlternativePaymentsService.swift b/Sources/ProcessOut/Sources/Services/AlternativePayments/POAlternativePaymentsService.swift index 18c78cd5b..ec2e2c616 100644 --- a/Sources/ProcessOut/Sources/Services/AlternativePayments/POAlternativePaymentsService.swift +++ b/Sources/ProcessOut/Sources/Services/AlternativePayments/POAlternativePaymentsService.swift @@ -16,6 +16,12 @@ public protocol POAlternativePaymentsService: POService { /// Authorizes invoice using given request. func authorize(request: POAlternativePaymentAuthorizationRequest) async throws -> POAlternativePaymentResponse + /// Creates redirect URL for given tokenization request. + func url(for request: POAlternativePaymentTokenizationRequest) throws -> URL + + /// Creates redirect URL for given authorization request. + func url(for request: POAlternativePaymentAuthorizationRequest) throws -> URL + /// Authenticates alternative payment using given raw URL. func authenticate(using url: URL) async throws -> POAlternativePaymentResponse } From c64d9f59d020c37e8a3156e3a4214b70897a9a47 Mon Sep 17 00:00:00 2001 From: Andrii Vysotskyi Date: Wed, 7 Aug 2024 15:47:08 +0200 Subject: [PATCH 05/28] Move tests to UI module --- .../MockPhoneNumberMetadataProvider.swift | 6 +++--- .../CardExpirationFormatterTests.swift | 0 .../CardNumber/CardNumberFormatterTests.swift | 0 ...faultPhoneNumberMetadataProviderTests.swift | 0 .../PhoneNumberFormatterTests.swift | 0 .../Unit/Core/Utils/FormattingUtilsTests.swift | 18 +++++++++--------- 6 files changed, 12 insertions(+), 12 deletions(-) rename Tests/{ProcessOutTests => ProcessOutUITests}/Sources/Mocks/PhoneNumberMetadataProvider/MockPhoneNumberMetadataProvider.swift (57%) rename Tests/{ProcessOutTests => ProcessOutUITests}/Sources/Unit/Core/Formatters/CardExpiration/CardExpirationFormatterTests.swift (100%) rename Tests/{ProcessOutTests => ProcessOutUITests}/Sources/Unit/Core/Formatters/CardNumber/CardNumberFormatterTests.swift (100%) rename Tests/{ProcessOutTests => ProcessOutUITests}/Sources/Unit/Core/Formatters/PhoneNumber/DefaultPhoneNumberMetadataProviderTests.swift (100%) rename Tests/{ProcessOutTests => ProcessOutUITests}/Sources/Unit/Core/Formatters/PhoneNumber/PhoneNumberFormatterTests.swift (100%) rename Tests/{ProcessOutTests => ProcessOutUITests}/Sources/Unit/Core/Utils/FormattingUtilsTests.swift (81%) diff --git a/Tests/ProcessOutTests/Sources/Mocks/PhoneNumberMetadataProvider/MockPhoneNumberMetadataProvider.swift b/Tests/ProcessOutUITests/Sources/Mocks/PhoneNumberMetadataProvider/MockPhoneNumberMetadataProvider.swift similarity index 57% rename from Tests/ProcessOutTests/Sources/Mocks/PhoneNumberMetadataProvider/MockPhoneNumberMetadataProvider.swift rename to Tests/ProcessOutUITests/Sources/Mocks/PhoneNumberMetadataProvider/MockPhoneNumberMetadataProvider.swift index f4319bdb2..0bbb40581 100644 --- a/Tests/ProcessOutTests/Sources/Mocks/PhoneNumberMetadataProvider/MockPhoneNumberMetadataProvider.swift +++ b/Tests/ProcessOutUITests/Sources/Mocks/PhoneNumberMetadataProvider/MockPhoneNumberMetadataProvider.swift @@ -5,14 +5,14 @@ // Created by Andrii Vysotskyi on 12.05.2023. // -@testable @_spi(PO) import ProcessOut +@testable import ProcessOutUI -final class MockPhoneNumberMetadataProvider: POPhoneNumberMetadataProvider { +final class MockPhoneNumberMetadataProvider: PhoneNumberMetadataProvider { var metadataCallsCount = 0 var metadata: POPhoneNumberMetadata? - func metadata(for countryCode: String) -> POPhoneNumberMetadata? { + func metadata(for countryCode: String) -> PhoneNumberMetadata? { metadataCallsCount += 1 return metadata } diff --git a/Tests/ProcessOutTests/Sources/Unit/Core/Formatters/CardExpiration/CardExpirationFormatterTests.swift b/Tests/ProcessOutUITests/Sources/Unit/Core/Formatters/CardExpiration/CardExpirationFormatterTests.swift similarity index 100% rename from Tests/ProcessOutTests/Sources/Unit/Core/Formatters/CardExpiration/CardExpirationFormatterTests.swift rename to Tests/ProcessOutUITests/Sources/Unit/Core/Formatters/CardExpiration/CardExpirationFormatterTests.swift diff --git a/Tests/ProcessOutTests/Sources/Unit/Core/Formatters/CardNumber/CardNumberFormatterTests.swift b/Tests/ProcessOutUITests/Sources/Unit/Core/Formatters/CardNumber/CardNumberFormatterTests.swift similarity index 100% rename from Tests/ProcessOutTests/Sources/Unit/Core/Formatters/CardNumber/CardNumberFormatterTests.swift rename to Tests/ProcessOutUITests/Sources/Unit/Core/Formatters/CardNumber/CardNumberFormatterTests.swift diff --git a/Tests/ProcessOutTests/Sources/Unit/Core/Formatters/PhoneNumber/DefaultPhoneNumberMetadataProviderTests.swift b/Tests/ProcessOutUITests/Sources/Unit/Core/Formatters/PhoneNumber/DefaultPhoneNumberMetadataProviderTests.swift similarity index 100% rename from Tests/ProcessOutTests/Sources/Unit/Core/Formatters/PhoneNumber/DefaultPhoneNumberMetadataProviderTests.swift rename to Tests/ProcessOutUITests/Sources/Unit/Core/Formatters/PhoneNumber/DefaultPhoneNumberMetadataProviderTests.swift diff --git a/Tests/ProcessOutTests/Sources/Unit/Core/Formatters/PhoneNumber/PhoneNumberFormatterTests.swift b/Tests/ProcessOutUITests/Sources/Unit/Core/Formatters/PhoneNumber/PhoneNumberFormatterTests.swift similarity index 100% rename from Tests/ProcessOutTests/Sources/Unit/Core/Formatters/PhoneNumber/PhoneNumberFormatterTests.swift rename to Tests/ProcessOutUITests/Sources/Unit/Core/Formatters/PhoneNumber/PhoneNumberFormatterTests.swift diff --git a/Tests/ProcessOutTests/Sources/Unit/Core/Utils/FormattingUtilsTests.swift b/Tests/ProcessOutUITests/Sources/Unit/Core/Utils/FormattingUtilsTests.swift similarity index 81% rename from Tests/ProcessOutTests/Sources/Unit/Core/Utils/FormattingUtilsTests.swift rename to Tests/ProcessOutUITests/Sources/Unit/Core/Utils/FormattingUtilsTests.swift index 21d96518c..552c6b133 100644 --- a/Tests/ProcessOutTests/Sources/Unit/Core/Utils/FormattingUtilsTests.swift +++ b/Tests/ProcessOutUITests/Sources/Unit/Core/Utils/FormattingUtilsTests.swift @@ -6,13 +6,13 @@ // import XCTest -@testable @_spi(PO) import ProcessOut +@testable import ProcessOutUI final class FormattingUtilsTests: XCTestCase { func test_adjustedCursorOffset_whenNotGreedy_doesNotIncludeSignificants() { // When - let offset = POFormattingUtils.adjustedCursorOffset( + let offset = FormattingUtils.adjustedCursorOffset( in: "1 2", source: "12", sourceCursorOffset: 1, significantCharacters: .decimalDigits, greedy: false ) @@ -22,7 +22,7 @@ final class FormattingUtilsTests: XCTestCase { func test_adjustedCursorOffset_whenGreedy_includesNonSignificants() { // When - let offset = POFormattingUtils.adjustedCursorOffset( + let offset = FormattingUtils.adjustedCursorOffset( in: "1 2", source: "12", sourceCursorOffset: 1, significantCharacters: .decimalDigits ) @@ -32,7 +32,7 @@ final class FormattingUtilsTests: XCTestCase { func test_adjustedCursorOffset_whenCursorPrefixChanges_returnValidOffset() { // When - let offset = POFormattingUtils.adjustedCursorOffset( + let offset = FormattingUtils.adjustedCursorOffset( in: "+12", source: "12", sourceCursorOffset: 1, significantCharacters: .decimalDigits ) @@ -42,7 +42,7 @@ final class FormattingUtilsTests: XCTestCase { func test_adjustedCursorOffset_whenCursorSuffixChanges_returnEndOfTarget() { // When - let offset = POFormattingUtils.adjustedCursorOffset( + let offset = FormattingUtils.adjustedCursorOffset( in: "2", source: "1", sourceCursorOffset: 0, significantCharacters: .decimalDigits ) @@ -52,7 +52,7 @@ final class FormattingUtilsTests: XCTestCase { func test_adjustedCursorOffset_whenNewCharacterIsAddedToCursorSuffix_returnEndOfTarget() { // When - let offset = POFormattingUtils.adjustedCursorOffset( + let offset = FormattingUtils.adjustedCursorOffset( in: "123", source: "12", sourceCursorOffset: 1, significantCharacters: .decimalDigits ) @@ -62,7 +62,7 @@ final class FormattingUtilsTests: XCTestCase { func test_adjustedCursorOffset_whenSourceIsSameAsTarget_returnsSameOffset() { // When - let offset = POFormattingUtils.adjustedCursorOffset( + let offset = FormattingUtils.adjustedCursorOffset( in: "1", source: "1", sourceCursorOffset: 0, significantCharacters: .decimalDigits ) @@ -72,7 +72,7 @@ final class FormattingUtilsTests: XCTestCase { func test_adjustedCursorOffset_whenSourceIsEmpty_returnEndOfTarget() { // When - let offset = POFormattingUtils.adjustedCursorOffset( + let offset = FormattingUtils.adjustedCursorOffset( in: "1", source: "", sourceCursorOffset: 0, significantCharacters: .decimalDigits ) @@ -82,7 +82,7 @@ final class FormattingUtilsTests: XCTestCase { func test_adjustedCursorOffset_whenTargetIsEmpty_returnStartOfTarget() { // When - let offset = POFormattingUtils.adjustedCursorOffset( + let offset = FormattingUtils.adjustedCursorOffset( in: "", source: "0", sourceCursorOffset: 1, significantCharacters: .decimalDigits ) From 5f31e4153c983879eb4ac187b28304217a3dc7ca Mon Sep 17 00:00:00 2001 From: Andrii Vysotskyi Date: Wed, 7 Aug 2024 15:48:01 +0200 Subject: [PATCH 06/28] Remove tests for non-existing types --- .../CodingUtils/FallbackDecodableTests.swift | 54 ------------------- .../ImmutableExcludedCodableTests.swift | 30 ----------- .../Core/Utils/UIImage+DynamicTests.swift | 8 --- 3 files changed, 92 deletions(-) delete mode 100644 Tests/ProcessOutTests/Sources/Unit/Core/CodingUtils/FallbackDecodableTests.swift delete mode 100644 Tests/ProcessOutTests/Sources/Unit/Core/CodingUtils/ImmutableExcludedCodableTests.swift delete mode 100644 Tests/ProcessOutTests/Sources/Unit/Core/Utils/UIImage+DynamicTests.swift diff --git a/Tests/ProcessOutTests/Sources/Unit/Core/CodingUtils/FallbackDecodableTests.swift b/Tests/ProcessOutTests/Sources/Unit/Core/CodingUtils/FallbackDecodableTests.swift deleted file mode 100644 index 65682cc01..000000000 --- a/Tests/ProcessOutTests/Sources/Unit/Core/CodingUtils/FallbackDecodableTests.swift +++ /dev/null @@ -1,54 +0,0 @@ -// -// FallbackDecodableTests.swift -// ProcessOutTests -// -// Created by Andrii Vysotskyi on 24.11.2023. -// - -import XCTest -@testable import ProcessOut - -final class FallbackDecodableTests: XCTestCase { - - func test_fallbackDecodable_whenValueIsNotPresent_decodesEmptyString() throws { - // Given - let decoder = JSONDecoder() - let data = Data("{}".utf8) - - // When - let container = try decoder.decode(Container.self, from: data) - - // Then - XCTAssertTrue(container.value.isEmpty) - } - - func test_fallbackDecodable_whenValueIsNull_decodesEmptyString() throws { - // Given - let decoder = JSONDecoder() - let data = Data(#"{ "value": null }"#.utf8) - - // When - let container = try decoder.decode(Container.self, from: data) - - // Then - XCTAssertTrue(container.value.isEmpty) - } - - func test_fallbackDecodable_whenValueIsAvailable_decodesIt() throws { - // Given - let decoder = JSONDecoder() - let data = Data(#"{ "value": "1" }"#.utf8) - - // When - let container = try decoder.decode(Container.self, from: data) - - // Then - XCTAssertEqual(container.value, "1") - } -} - -private struct Container: Decodable { - - @POFallbackDecodable - var value: String -} diff --git a/Tests/ProcessOutTests/Sources/Unit/Core/CodingUtils/ImmutableExcludedCodableTests.swift b/Tests/ProcessOutTests/Sources/Unit/Core/CodingUtils/ImmutableExcludedCodableTests.swift deleted file mode 100644 index 4a39bf7c2..000000000 --- a/Tests/ProcessOutTests/Sources/Unit/Core/CodingUtils/ImmutableExcludedCodableTests.swift +++ /dev/null @@ -1,30 +0,0 @@ -// -// ImmutableExcludedCodableTests.swift -// ProcessOutTests -// -// Created by Andrii Vysotskyi on 29.03.2023. -// - -import XCTest -@testable import ProcessOut - -final class ImmutableExcludedCodableTests: XCTestCase { - - func test_excludedCodable_whenWrappedInContainer_isNotEncoded() throws { - // Given - let encoder = JSONEncoder() - let value = Container(value: POImmutableExcludedCodable(value: 1)) - - // When - let encodeData = try encoder.encode(value) - - // Then - XCTAssertEqual(Data("{}".utf8), encodeData) - } -} - -private struct Container: Encodable { - - @POImmutableExcludedCodable - var value: Int -} diff --git a/Tests/ProcessOutTests/Sources/Unit/Core/Utils/UIImage+DynamicTests.swift b/Tests/ProcessOutTests/Sources/Unit/Core/Utils/UIImage+DynamicTests.swift deleted file mode 100644 index ace38d5c6..000000000 --- a/Tests/ProcessOutTests/Sources/Unit/Core/Utils/UIImage+DynamicTests.swift +++ /dev/null @@ -1,8 +0,0 @@ -// -// UIImage+Dynamic+Tests.swift -// ProcessOutTests -// -// Created by Andrii Vysotskyi on 04.04.2024. -// - -import Foundation From 1275737f158ba33696b7d95c70cc93b8092bd582 Mon Sep 17 00:00:00 2001 From: Andrii Vysotskyi Date: Wed, 7 Aug 2024 15:49:59 +0200 Subject: [PATCH 07/28] Fix utils tests --- ...> StringCodableOptionalDecimalTests.swift} | 16 ++++----- .../Unit/Core/Utils/AsyncUtilsTests.swift | 34 +++++++++---------- 2 files changed, 25 insertions(+), 25 deletions(-) rename Tests/ProcessOutTests/Sources/Unit/Core/CodingUtils/{ImmutableStringCodableOptionalDecimalTests.swift => StringCodableOptionalDecimalTests.swift} (74%) diff --git a/Tests/ProcessOutTests/Sources/Unit/Core/CodingUtils/ImmutableStringCodableOptionalDecimalTests.swift b/Tests/ProcessOutTests/Sources/Unit/Core/CodingUtils/StringCodableOptionalDecimalTests.swift similarity index 74% rename from Tests/ProcessOutTests/Sources/Unit/Core/CodingUtils/ImmutableStringCodableOptionalDecimalTests.swift rename to Tests/ProcessOutTests/Sources/Unit/Core/CodingUtils/StringCodableOptionalDecimalTests.swift index 7d1048fd9..2d3b4515f 100644 --- a/Tests/ProcessOutTests/Sources/Unit/Core/CodingUtils/ImmutableStringCodableOptionalDecimalTests.swift +++ b/Tests/ProcessOutTests/Sources/Unit/Core/CodingUtils/StringCodableOptionalDecimalTests.swift @@ -1,5 +1,5 @@ // -// ImmutableStringCodableOptionalDecimalTests.swift +// StringCodableOptionalDecimalTests.swift // ProcessOutTests // // Created by Andrii Vysotskyi on 18.10.2022. @@ -22,7 +22,7 @@ final class ImmutableStringCodableOptionalDecimalTests: XCTestCase { let data = Data(#""1""#.utf8) // When - let decimal = try decoder.decode(POImmutableStringCodableOptionalDecimal.self, from: data) + let decimal = try decoder.decode(POStringCodableOptionalDecimal.self, from: data) // Then XCTAssertEqual(decimal.wrappedValue?.description, "1") @@ -33,7 +33,7 @@ final class ImmutableStringCodableOptionalDecimalTests: XCTestCase { let data = Data(#""1234.25""#.utf8) // When - let decimal = try decoder.decode(POImmutableStringCodableOptionalDecimal.self, from: data) + let decimal = try decoder.decode(POStringCodableOptionalDecimal.self, from: data) // Then XCTAssertEqual(decimal.wrappedValue?.description, "1234.25") @@ -55,7 +55,7 @@ final class ImmutableStringCodableOptionalDecimalTests: XCTestCase { let data = Data("1".utf8) // Then - XCTAssertThrowsError(try decoder.decode(POImmutableStringCodableOptionalDecimal.self, from: data)) + XCTAssertThrowsError(try decoder.decode(POStringCodableOptionalDecimal.self, from: data)) } func test_init_whenInputHasComma_fails() throws { @@ -63,12 +63,12 @@ final class ImmutableStringCodableOptionalDecimalTests: XCTestCase { let data = Data(#""1,2""#.utf8) // Then - XCTAssertThrowsError(try decoder.decode(POImmutableStringCodableOptionalDecimal.self, from: data)) + XCTAssertThrowsError(try decoder.decode(POStringCodableOptionalDecimal.self, from: data)) } func test_encode_returnsStringData() throws { // Given - let decimal = POImmutableStringCodableOptionalDecimal(value: Decimal(1234)) + let decimal = POStringCodableOptionalDecimal(value: Decimal(1234)) // When let data = try encoder.encode(decimal) @@ -80,7 +80,7 @@ final class ImmutableStringCodableOptionalDecimalTests: XCTestCase { func test_encode_whenInContainer_encodesString() throws { // Given - let value = Container(number: POImmutableStringCodableOptionalDecimal(value: Decimal(1234))) + let value = Container(number: POStringCodableOptionalDecimal(value: Decimal(1234))) // When let data = try encoder.encode(value) @@ -97,6 +97,6 @@ final class ImmutableStringCodableOptionalDecimalTests: XCTestCase { private struct Container: Codable { - @POImmutableStringCodableOptionalDecimal + @POStringCodableOptionalDecimal var number: Decimal? } diff --git a/Tests/ProcessOutTests/Sources/Unit/Core/Utils/AsyncUtilsTests.swift b/Tests/ProcessOutTests/Sources/Unit/Core/Utils/AsyncUtilsTests.swift index b3321e36f..551feb743 100644 --- a/Tests/ProcessOutTests/Sources/Unit/Core/Utils/AsyncUtilsTests.swift +++ b/Tests/ProcessOutTests/Sources/Unit/Core/Utils/AsyncUtilsTests.swift @@ -105,12 +105,12 @@ final class AsyncUtilsTests: XCTestCase { func test_retry_whenTimeoutIsZero_executesOperationOnce() async throws { // Given - @POUnfairlyLocked var isOperationExecuted = false + let isOperationExecuted = POUnfairlyLocked(wrappedValue: false) // When try await retry( operation: { - $isOperationExecuted.withLock { $0 = true } + isOperationExecuted.withLock { $0 = true } }, while: { _ in false @@ -120,7 +120,7 @@ final class AsyncUtilsTests: XCTestCase { ) // Then - XCTAssertTrue(isOperationExecuted) + XCTAssertTrue(isOperationExecuted.wrappedValue) } func test_retry_whenTimesOut_throwsTimeoutError() async { @@ -145,7 +145,7 @@ final class AsyncUtilsTests: XCTestCase { func test_retry_checksRetryCondition_whenRetryStrategyIsSet() async throws { // Given - @POUnfairlyLocked var isConditionChecked = false + let isConditionChecked = POUnfairlyLocked(wrappedValue: false) // When _ = try await retry( @@ -153,7 +153,7 @@ final class AsyncUtilsTests: XCTestCase { "" }, while: { _ in - $isConditionChecked.withLock { $0 = true } + isConditionChecked.withLock { $0 = true } return false }, timeout: 10, @@ -162,17 +162,17 @@ final class AsyncUtilsTests: XCTestCase { ) // Then - XCTAssertTrue(isConditionChecked) + XCTAssertTrue(isConditionChecked.wrappedValue) } func test_retry_retriesOperation_whenRetryStrategyIsSet() async throws { // Given - @POUnfairlyLocked var operationStartsCount = 0 + let operationStartsCount = POUnfairlyLocked(wrappedValue: 0) // When _ = try await retry( operation: { - $operationStartsCount.withLock { $0 += 1 } + operationStartsCount.withLock { $0 += 1 } }, while: { _ in true @@ -183,7 +183,7 @@ final class AsyncUtilsTests: XCTestCase { ) // Then - XCTAssertEqual(operationStartsCount, 2) + XCTAssertEqual(operationStartsCount.wrappedValue, 2) } // swiftlint:disable:next line_length @@ -217,13 +217,13 @@ final class AsyncUtilsTests: XCTestCase { func test_retry_whenRetryCountIsExceeded_completesWithRecentResult() async throws { // Given - @POUnfairlyLocked var recentOperationValue = "" + let recentOperationValue = POUnfairlyLocked(wrappedValue: "") // When let value = try await retry( operation: { - $recentOperationValue.withLock { $0 = UUID().uuidString } - return recentOperationValue + recentOperationValue.withLock { $0 = UUID().uuidString } + return recentOperationValue.wrappedValue }, while: { _ in true @@ -234,18 +234,18 @@ final class AsyncUtilsTests: XCTestCase { ) // Then - XCTAssertEqual(recentOperationValue, value) + XCTAssertEqual(recentOperationValue.wrappedValue, value) } func test_retry_whenRetryConditionResolvesFalse_completesWithRecentResult() async throws { // Given - @POUnfairlyLocked var recentOperationValue = "" + let recentOperationValue = POUnfairlyLocked(wrappedValue: "") // When let value = try await retry( operation: { - $recentOperationValue.withLock { $0 = UUID().uuidString } - return recentOperationValue + recentOperationValue.withLock { $0 = UUID().uuidString } + return recentOperationValue.wrappedValue }, while: { _ in false @@ -256,7 +256,7 @@ final class AsyncUtilsTests: XCTestCase { ) // Then - XCTAssertEqual(recentOperationValue, value) + XCTAssertEqual(recentOperationValue.wrappedValue, value) } func test_retry_whenCancelledImmediately_completesWithCancellationError() async throws { From e2bbfc24ce32471d2dbc2188297be4c06a23de2a Mon Sep 17 00:00:00 2001 From: Andrii Vysotskyi Date: Wed, 7 Aug 2024 15:50:35 +0200 Subject: [PATCH 08/28] Enable experimental feature --- project.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/project.yml b/project.yml index b81d6838d..da3eda5fd 100644 --- a/project.yml +++ b/project.yml @@ -23,6 +23,7 @@ targets: deploymentTarget: "13.0" settings: PRODUCT_BUNDLE_IDENTIFIER: com.processout.core-ui + OTHER_SWIFT_FLAGS: "-enable-experimental-feature IsolatedAny" TARGET_ROOT: $(PROJECT_DIR)/Sources/ProcessOutCoreUI preBuildScripts: - path: Scripts/Lint.sh @@ -56,7 +57,7 @@ targets: PRODUCT_BUNDLE_IDENTIFIER: com.processout.processout-ios MARKETING_VERSION: ${CURRENT_VERSION} CURRENT_PROJECT_VERSION: 1 - OTHER_SWIFT_FLAGS: "-Xfrontend -module-interface-preserve-types-as-written" + OTHER_SWIFT_FLAGS: "-Xfrontend -module-interface-preserve-types-as-written -enable-experimental-feature IsolatedAny" TARGET_ROOT: $(PROJECT_DIR)/Sources/ProcessOut preBuildScripts: - path: Scripts/Lint.sh From f81387f6527cd75fa2c0ddfaddf4a416257cffdb Mon Sep 17 00:00:00 2001 From: Andrii Vysotskyi Date: Wed, 7 Aug 2024 15:51:45 +0200 Subject: [PATCH 09/28] Resolve concurrency warnings --- .../StubDeviceMetadataProvider.swift | 8 ++++---- .../Sources/Mocks/Logger/POLogger+Extensions.swift | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Tests/ProcessOutTests/Sources/Mocks/DeviceMetadataProvider/StubDeviceMetadataProvider.swift b/Tests/ProcessOutTests/Sources/Mocks/DeviceMetadataProvider/StubDeviceMetadataProvider.swift index 55e782cff..374fb6b12 100644 --- a/Tests/ProcessOutTests/Sources/Mocks/DeviceMetadataProvider/StubDeviceMetadataProvider.swift +++ b/Tests/ProcessOutTests/Sources/Mocks/DeviceMetadataProvider/StubDeviceMetadataProvider.swift @@ -11,10 +11,10 @@ struct StubDeviceMetadataProvider: DeviceMetadataProvider { var deviceMetadata: DeviceMetadata { DeviceMetadata( - id: .init(value: ""), - installationId: .init(value: nil), - systemVersion: .init(value: "4"), - model: .init(value: "model"), + id: "", + installationId: nil, + systemVersion: "4", + model: "model", appLanguage: "en", appScreenWidth: 1, appScreenHeight: 2, diff --git a/Tests/ProcessOutTests/Sources/Mocks/Logger/POLogger+Extensions.swift b/Tests/ProcessOutTests/Sources/Mocks/Logger/POLogger+Extensions.swift index c318c2477..4b0b53ce9 100644 --- a/Tests/ProcessOutTests/Sources/Mocks/Logger/POLogger+Extensions.swift +++ b/Tests/ProcessOutTests/Sources/Mocks/Logger/POLogger+Extensions.swift @@ -10,5 +10,5 @@ extension POLogger { /// Stub logger. - static var stub = POLogger(category: "") + static let stub = POLogger(category: "") } From 386f9d2e0027b7a7916d7016387a3c9d82465537 Mon Sep 17 00:00:00 2001 From: Andrii Vysotskyi Date: Wed, 7 Aug 2024 16:42:28 +0200 Subject: [PATCH 10/28] Fix UI tests --- .../DefaultCardUpdateInteractorTests.swift | 14 ++++++++- .../CardUpdate/CardUpdateDelegateMock.swift | 29 +++++++++++++++---- .../Mocks/Logger/Logger+Extensions.swift | 2 +- .../MockPhoneNumberMetadataProvider.swift | 26 +++++++++++++---- .../CardExpirationFormatterTests.swift | 8 ++--- .../CardNumber/CardNumberFormatterTests.swift | 8 ++--- ...aultPhoneNumberMetadataProviderTests.swift | 4 +-- .../PhoneNumberFormatterTests.swift | 28 +++++++++--------- 8 files changed, 83 insertions(+), 36 deletions(-) diff --git a/Tests/ProcessOutUITests/Sources/Integration/CardUpdate/DefaultCardUpdateInteractorTests.swift b/Tests/ProcessOutUITests/Sources/Integration/CardUpdate/DefaultCardUpdateInteractorTests.swift index 01f5911ad..d0b225ce4 100644 --- a/Tests/ProcessOutUITests/Sources/Integration/CardUpdate/DefaultCardUpdateInteractorTests.swift +++ b/Tests/ProcessOutUITests/Sources/Integration/CardUpdate/DefaultCardUpdateInteractorTests.swift @@ -11,14 +11,16 @@ import XCTest final class DefaultCardUpdateInteractorTests: XCTestCase { + @MainActor override func setUp() { super.setUp() - ProcessOut.configure(configuration: .production(projectId: Constants.projectId), force: true) + ProcessOut.configure(configuration: .init(projectId: Constants.projectId), force: true) cardsService = ProcessOut.shared.cards } // MARK: - Start + @MainActor func test_start_whenCardInfoIsNotSet_setsStartingState() { // Given let delegate = CardUpdateDelegateMock() @@ -41,6 +43,7 @@ final class DefaultCardUpdateInteractorTests: XCTestCase { // MARK: - Scheme Resolve + @MainActor func test_start_whenCardSchemeIsSetInConfiguration_setsStartedStateWithIt() { // Given let configuration = POCardUpdateConfiguration(cardId: "", cardInformation: .init(scheme: "visa")) @@ -57,6 +60,7 @@ final class DefaultCardUpdateInteractorTests: XCTestCase { XCTAssertEqual(startedState.scheme, .visa) } + @MainActor func test_start_whenPreferredCardSchemeIsAvailable_setsStartedStateWithIt() { // Given let configuration = POCardUpdateConfiguration( @@ -76,6 +80,7 @@ final class DefaultCardUpdateInteractorTests: XCTestCase { XCTAssertEqual(startedState.preferredScheme, .carteBancaire) } + @MainActor func test_start_whenCardSchemeIsNotSetAndIinIsSet_attemptsToResolve() { // Given let configuration = POCardUpdateConfiguration(cardId: "", cardInformation: .init(iin: "424242")) @@ -94,6 +99,7 @@ final class DefaultCardUpdateInteractorTests: XCTestCase { wait(for: [expectation]) } + @MainActor func test_start_whenCardSchemeIsNotSetAndMaskedNumberIsSet_attemptsToResolve() { // Given let configuration = POCardUpdateConfiguration( @@ -116,6 +122,7 @@ final class DefaultCardUpdateInteractorTests: XCTestCase { // MARK: - Cancel + @MainActor func test_cancel_whenStarted() { // Given let configuration = POCardUpdateConfiguration(cardId: "", cardInformation: .init(scheme: "visa")) @@ -131,6 +138,7 @@ final class DefaultCardUpdateInteractorTests: XCTestCase { // MARK: - Update CVC + @MainActor func test_updateCvc_whenStarting_isIgnored() { // Given let configuration = POCardUpdateConfiguration(cardId: "") @@ -145,6 +153,7 @@ final class DefaultCardUpdateInteractorTests: XCTestCase { XCTAssertEqual(sut.state, oldState) } + @MainActor func test_updateCvc_whenStarted_updatesState() { // Given let configuration = POCardUpdateConfiguration(cardId: "", cardInformation: .init(scheme: "visa")) @@ -163,6 +172,7 @@ final class DefaultCardUpdateInteractorTests: XCTestCase { // MARK: - Submit + @MainActor func test_submit_whenCvcIsNotSet_causesError() { // Given let configuration = POCardUpdateConfiguration(cardId: "", cardInformation: .init(scheme: "visa")) @@ -182,6 +192,7 @@ final class DefaultCardUpdateInteractorTests: XCTestCase { wait(for: [expectation]) } + @MainActor func test_submit_whenValidCvcIsSet_completes() { // Given let configuration = POCardUpdateConfiguration( @@ -210,6 +221,7 @@ final class DefaultCardUpdateInteractorTests: XCTestCase { // MARK: - Private Methods + @MainActor private func createSut( configuration: POCardUpdateConfiguration, delegate: POCardUpdateDelegate? = nil ) -> any CardUpdateInteractor { diff --git a/Tests/ProcessOutUITests/Sources/Mocks/CardUpdate/CardUpdateDelegateMock.swift b/Tests/ProcessOutUITests/Sources/Mocks/CardUpdate/CardUpdateDelegateMock.swift index ad1ff415a..13e18362b 100644 --- a/Tests/ProcessOutUITests/Sources/Mocks/CardUpdate/CardUpdateDelegateMock.swift +++ b/Tests/ProcessOutUITests/Sources/Mocks/CardUpdate/CardUpdateDelegateMock.swift @@ -5,14 +5,25 @@ // Created by Andrii Vysotskyi on 14.11.2023. // -import ProcessOut +@_spi(PO) import ProcessOut @testable import ProcessOutUI -final class CardUpdateDelegateMock: POCardUpdateDelegate { +final class CardUpdateDelegateMock: POCardUpdateDelegate, Sendable { - var cardUpdateDidEmitEventFromClosure: ((POCardUpdateEvent) -> Void)? - var cardInformationFromClosure: ((String) -> POCardUpdateInformation?)? - var shouldContinueUpdateFromClosure: ((POFailure) -> Bool)? + var cardUpdateDidEmitEventFromClosure: ((POCardUpdateEvent) -> Void)? { + get { lock.withLock { _cardUpdateDidEmitEventFromClosure } } + set { lock.withLock { _cardUpdateDidEmitEventFromClosure = newValue } } + } + + var cardInformationFromClosure: ((String) -> POCardUpdateInformation?)? { + get { lock.withLock { _cardInformationFromClosure } } + set { lock.withLock { _cardInformationFromClosure = newValue } } + } + + var shouldContinueUpdateFromClosure: ((POFailure) -> Bool)? { + get { lock.withLock { _shouldContinueUpdateFromClosure } } + set { lock.withLock { _shouldContinueUpdateFromClosure = newValue } } + } // MARK: - PO3DSService @@ -27,4 +38,12 @@ final class CardUpdateDelegateMock: POCardUpdateDelegate { func shouldContinueUpdate(after failure: POFailure) -> Bool { shouldContinueUpdateFromClosure?(failure) ?? false } + + // MARK: - Private Properties + + private let lock = POUnfairlyLocked() + + private nonisolated(unsafe) var _cardUpdateDidEmitEventFromClosure: ((POCardUpdateEvent) -> Void)? + private nonisolated(unsafe) var _cardInformationFromClosure: ((String) -> POCardUpdateInformation?)? + private nonisolated(unsafe) var _shouldContinueUpdateFromClosure: ((POFailure) -> Bool)? } diff --git a/Tests/ProcessOutUITests/Sources/Mocks/Logger/Logger+Extensions.swift b/Tests/ProcessOutUITests/Sources/Mocks/Logger/Logger+Extensions.swift index 03502f7ae..5dd72ee62 100644 --- a/Tests/ProcessOutUITests/Sources/Mocks/Logger/Logger+Extensions.swift +++ b/Tests/ProcessOutUITests/Sources/Mocks/Logger/Logger+Extensions.swift @@ -10,5 +10,5 @@ extension POLogger { /// Stub logger. - static var stub = POLogger(category: "") + static let stub = POLogger(category: "") } diff --git a/Tests/ProcessOutUITests/Sources/Mocks/PhoneNumberMetadataProvider/MockPhoneNumberMetadataProvider.swift b/Tests/ProcessOutUITests/Sources/Mocks/PhoneNumberMetadataProvider/MockPhoneNumberMetadataProvider.swift index 0bbb40581..9e39ab294 100644 --- a/Tests/ProcessOutUITests/Sources/Mocks/PhoneNumberMetadataProvider/MockPhoneNumberMetadataProvider.swift +++ b/Tests/ProcessOutUITests/Sources/Mocks/PhoneNumberMetadataProvider/MockPhoneNumberMetadataProvider.swift @@ -5,15 +5,31 @@ // Created by Andrii Vysotskyi on 12.05.2023. // +@_spi(PO) import ProcessOut @testable import ProcessOutUI -final class MockPhoneNumberMetadataProvider: PhoneNumberMetadataProvider { +final class MockPhoneNumberMetadataProvider: PhoneNumberMetadataProvider, Sendable { - var metadataCallsCount = 0 - var metadata: POPhoneNumberMetadata? + var metadataCallsCount: Int { + lock.withLock { _metadataCallsCount } + } + + var metadata: PhoneNumberMetadata? { + get { lock.withLock { _metadata } } + set { lock.withLock { _metadata = newValue } } + } func metadata(for countryCode: String) -> PhoneNumberMetadata? { - metadataCallsCount += 1 - return metadata + lock.withLock { + _metadataCallsCount += 1 + return _metadata + } } + + // MARK: - Private Properties + + private let lock = POUnfairlyLocked() + + private nonisolated(unsafe) var _metadataCallsCount = 0 + private nonisolated(unsafe) var _metadata: PhoneNumberMetadata? } diff --git a/Tests/ProcessOutUITests/Sources/Unit/Core/Formatters/CardExpiration/CardExpirationFormatterTests.swift b/Tests/ProcessOutUITests/Sources/Unit/Core/Formatters/CardExpiration/CardExpirationFormatterTests.swift index 73368c78a..eded9fb7e 100644 --- a/Tests/ProcessOutUITests/Sources/Unit/Core/Formatters/CardExpiration/CardExpirationFormatterTests.swift +++ b/Tests/ProcessOutUITests/Sources/Unit/Core/Formatters/CardExpiration/CardExpirationFormatterTests.swift @@ -1,18 +1,18 @@ // // CardExpirationFormatterTests.swift -// ProcessOut +// ProcessOutUITests // // Created by Andrii Vysotskyi on 21.07.2023. // import XCTest -@testable @_spi(PO) import ProcessOut +@testable import ProcessOutUI final class CardExpirationFormatterTests: XCTestCase { override func setUp() { super.setUp() - sut = POCardExpirationFormatter() + sut = CardExpirationFormatter() } func test_string() { @@ -105,5 +105,5 @@ final class CardExpirationFormatterTests: XCTestCase { // MARK: - Private Properties - private var sut: POCardExpirationFormatter! + private var sut: CardExpirationFormatter! } diff --git a/Tests/ProcessOutUITests/Sources/Unit/Core/Formatters/CardNumber/CardNumberFormatterTests.swift b/Tests/ProcessOutUITests/Sources/Unit/Core/Formatters/CardNumber/CardNumberFormatterTests.swift index a815f62de..4d1c09f32 100644 --- a/Tests/ProcessOutUITests/Sources/Unit/Core/Formatters/CardNumber/CardNumberFormatterTests.swift +++ b/Tests/ProcessOutUITests/Sources/Unit/Core/Formatters/CardNumber/CardNumberFormatterTests.swift @@ -1,18 +1,18 @@ // // CardNumberFormatterTests.swift -// ProcessOut +// ProcessOutUITests // // Created by Andrii Vysotskyi on 19.07.2023. // import XCTest -@testable @_spi(PO) import ProcessOut +@testable import ProcessOutUI final class CardNumberFormatterTests: XCTestCase { override func setUp() { super.setUp() - sut = POCardNumberFormatter() + sut = CardNumberFormatter() } func test_normalized_retainsDigits() { @@ -57,5 +57,5 @@ final class CardNumberFormatterTests: XCTestCase { // MARK: - Private Properties - private var sut: POCardNumberFormatter! + private var sut: CardNumberFormatter! } diff --git a/Tests/ProcessOutUITests/Sources/Unit/Core/Formatters/PhoneNumber/DefaultPhoneNumberMetadataProviderTests.swift b/Tests/ProcessOutUITests/Sources/Unit/Core/Formatters/PhoneNumber/DefaultPhoneNumberMetadataProviderTests.swift index ddd3d22a6..c14e4ea45 100644 --- a/Tests/ProcessOutUITests/Sources/Unit/Core/Formatters/PhoneNumber/DefaultPhoneNumberMetadataProviderTests.swift +++ b/Tests/ProcessOutUITests/Sources/Unit/Core/Formatters/PhoneNumber/DefaultPhoneNumberMetadataProviderTests.swift @@ -6,7 +6,7 @@ // import XCTest -@testable @_spi(PO) import ProcessOut +@testable import ProcessOutUI final class DefaultPhoneNumberMetadataProviderTests: XCTestCase { @@ -33,5 +33,5 @@ final class DefaultPhoneNumberMetadataProviderTests: XCTestCase { // MARK: - Private Properties - private var sut: PODefaultPhoneNumberMetadataProvider! + private var sut: DefaultPhoneNumberMetadataProvider! } diff --git a/Tests/ProcessOutUITests/Sources/Unit/Core/Formatters/PhoneNumber/PhoneNumberFormatterTests.swift b/Tests/ProcessOutUITests/Sources/Unit/Core/Formatters/PhoneNumber/PhoneNumberFormatterTests.swift index ef13cab0f..ee742d8c8 100644 --- a/Tests/ProcessOutUITests/Sources/Unit/Core/Formatters/PhoneNumber/PhoneNumberFormatterTests.swift +++ b/Tests/ProcessOutUITests/Sources/Unit/Core/Formatters/PhoneNumber/PhoneNumberFormatterTests.swift @@ -1,12 +1,12 @@ // // PhoneNumberFormatterTests.swift -// ProcessOut +// ProcessOutUITests // // Created by Andrii Vysotskyi on 12.05.2023. // import XCTest -@testable @_spi(PO) import ProcessOut +@testable import ProcessOutUI final class PhoneNumberFormatterTests: XCTestCase { @@ -14,7 +14,7 @@ final class PhoneNumberFormatterTests: XCTestCase { super.setUp() metadataProvider = MockPhoneNumberMetadataProvider() metadataProvider.metadata = nil - sut = POPhoneNumberFormatter(metadataProvider: metadataProvider) + sut = PhoneNumberFormatter(metadataProvider: metadataProvider) } func test_normalized_retainsDigitsAndPlus() { @@ -70,8 +70,8 @@ final class PhoneNumberFormatterTests: XCTestCase { func test_string_whenNumberIsComplete_returnsFormattedNumber() { // Given - let format = POPhoneNumberFormat(pattern: "(\\d)(\\d)", leading: [".*"], format: "$1-$2") - metadataProvider.metadata = POPhoneNumberMetadata(countryCode: "1", formats: [format]) + let format = PhoneNumberFormat(pattern: "(\\d)(\\d)", leading: [".*"], format: "$1-$2") + metadataProvider.metadata = PhoneNumberMetadata(countryCode: "1", formats: [format]) // When let formattedNumber = sut.string(from: "123#") @@ -82,8 +82,8 @@ final class PhoneNumberFormatterTests: XCTestCase { func test_string_whenNationalNumberLeadingDigitsAreUnknown_formatsCountryCode() { // Given - let format = POPhoneNumberFormat(pattern: "", leading: [""], format: "") - metadataProvider.metadata = POPhoneNumberMetadata(countryCode: "1", formats: [format]) + let format = PhoneNumberFormat(pattern: "", leading: [""], format: "") + metadataProvider.metadata = PhoneNumberMetadata(countryCode: "1", formats: [format]) // When let formattedNumber = sut.string(from: "123") @@ -94,8 +94,8 @@ final class PhoneNumberFormatterTests: XCTestCase { func test_string_whenNationalNumberLengthExceedsMaximumLength_formatsCountryCode() { // Given - let format = POPhoneNumberFormat(pattern: "", leading: [], format: "") - metadataProvider.metadata = POPhoneNumberMetadata(countryCode: "1", formats: [format]) + let format = PhoneNumberFormat(pattern: "", leading: [], format: "") + metadataProvider.metadata = PhoneNumberMetadata(countryCode: "1", formats: [format]) // When let formattedNumber = sut.string(from: "1123456789123456") @@ -106,8 +106,8 @@ final class PhoneNumberFormatterTests: XCTestCase { func test_string_whenNumberContainsEasternArabicNumerals_returnsFormattedNumberWithLatinNumerals() { // Given - let format = POPhoneNumberFormat(pattern: "(\\d)(\\d+)", leading: [".*"], format: "$1-$2") - metadataProvider.metadata = POPhoneNumberMetadata(countryCode: "1", formats: [format]) + let format = PhoneNumberFormat(pattern: "(\\d)(\\d+)", leading: [".*"], format: "$1-$2") + metadataProvider.metadata = PhoneNumberMetadata(countryCode: "1", formats: [format]) // When let formattedNumber = sut.string(from: "١٢٣") @@ -118,8 +118,8 @@ final class PhoneNumberFormatterTests: XCTestCase { func test_string_whenNumberIsPartial_returnsFormattedNumberWithoutTrailingSeparators() { // Given - let format = POPhoneNumberFormat(pattern: "(\\d)(\\d)(\\d)", leading: [".*"], format: "$1-$2-$3") - metadataProvider.metadata = POPhoneNumberMetadata(countryCode: "1", formats: [format]) + let format = PhoneNumberFormat(pattern: "(\\d)(\\d)(\\d)", leading: [".*"], format: "$1-$2-$3") + metadataProvider.metadata = PhoneNumberMetadata(countryCode: "1", formats: [format]) // When let formattedNumber = sut.string(from: "123") @@ -185,5 +185,5 @@ final class PhoneNumberFormatterTests: XCTestCase { // MARK: - Private Properties private var metadataProvider: MockPhoneNumberMetadataProvider! - private var sut: POPhoneNumberFormatter! + private var sut: PhoneNumberFormatter! } From 909da72a0bd3dcd8cde839f0d65179ba2a95e13c Mon Sep 17 00:00:00 2001 From: Andrii Vysotskyi Date: Wed, 7 Aug 2024 16:43:11 +0200 Subject: [PATCH 11/28] Add unfair lock extensions --- .../UnfairlyLocked/POUnfairlyLocked.swift | 22 ++++++++++++------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/Sources/ProcessOut/Sources/Core/Utils/UnfairlyLocked/POUnfairlyLocked.swift b/Sources/ProcessOut/Sources/Core/Utils/UnfairlyLocked/POUnfairlyLocked.swift index 43429859e..df7289c3a 100644 --- a/Sources/ProcessOut/Sources/Core/Utils/UnfairlyLocked/POUnfairlyLocked.swift +++ b/Sources/ProcessOut/Sources/Core/Utils/UnfairlyLocked/POUnfairlyLocked.swift @@ -20,14 +20,8 @@ public final class POUnfairlyLocked: @unchecked Sendable { lock.withLock { value } } - public var projectedValue: POUnfairlyLocked { - self - } - - public func withLock(_ body: (inout Value) -> R) -> R { - lock.withLock { - body(&value) - } + public func withLock(_ body: (inout Value) throws -> R) rethrows -> R { + try lock.withLock { try body(&value) } } // MARK: - Private Properties @@ -35,3 +29,15 @@ public final class POUnfairlyLocked: @unchecked Sendable { private let lock = UnfairLock() private var value: Value } + +extension POUnfairlyLocked where Value == Void { + + /// Convenience to create lock when value type is `Void`. + public convenience init() where Value == Void { + self.init(wrappedValue: ()) + } + + public func withLock(_ body: () throws -> R) rethrows -> R { + try withLock { _ in try body() } + } +} From c7f51526fa864086149b3417ff576119a1739677 Mon Sep 17 00:00:00 2001 From: Andrii Vysotskyi Date: Wed, 7 Aug 2024 17:36:43 +0200 Subject: [PATCH 12/28] Ensure mocks are sendable --- .../Sources/Mocks/3DS/Mock3DSService.swift | 65 ++++++++++++------- .../MockHttpConnectorRequestMapper.swift | 29 +++++++-- .../Mocks/UrlProtocol/MockUrlProtocol.swift | 26 +++----- 3 files changed, 75 insertions(+), 45 deletions(-) diff --git a/Tests/ProcessOutTests/Sources/Mocks/3DS/Mock3DSService.swift b/Tests/ProcessOutTests/Sources/Mocks/3DS/Mock3DSService.swift index 26e42f207..1df988250 100644 --- a/Tests/ProcessOutTests/Sources/Mocks/3DS/Mock3DSService.swift +++ b/Tests/ProcessOutTests/Sources/Mocks/3DS/Mock3DSService.swift @@ -5,39 +5,58 @@ // Created by Andrii Vysotskyi on 10.04.2023. // -// swiftlint:disable line_length +// swiftlint:disable identifier_name line_length import Foundation -@testable import ProcessOut +@testable @_spi(PO) import ProcessOut -final class Mock3DSService: PO3DSService { +final class Mock3DSService: PO3DSService, Sendable { - var authenticationRequestCallsCount = 0 - var authenticationRequestFromClosure: ((PO3DS2Configuration, @escaping (Result) -> Void) -> Void)! - var handleChallengeCallsCount = 0 - var handleChallengeFromClosure: ((PO3DS2Challenge, @escaping (Result) -> Void) -> Void)! - var handleRedirectCallsCount = 0 - var handleRedirectFromClosure: ((PO3DSRedirect, @escaping (Result) -> Void) -> Void)! + var authenticationRequestParametersCallsCount: Int { + lock.withLock { _authenticationRequestParametersCallsCount } + } - // MARK: - PO3DSService + var authenticationRequestParametersFromClosure: ((PO3DS2Configuration) throws -> PO3DS2AuthenticationRequestParameters)! { + get { lock.withLock { _authenticationRequestParametersFromClosure } } + set { lock.withLock { _authenticationRequestParametersFromClosure = newValue } } + } - func authenticationRequest( - configuration: PO3DS2Configuration, - completion: @escaping (Result) -> Void - ) { - authenticationRequestCallsCount += 1 - authenticationRequestFromClosure!(configuration, completion) + var performChallengeCallsCount: Int { + lock.withLock { _performChallengeCallsCount } } - func handle(challenge: PO3DS2Challenge, completion: @escaping (Result) -> Void) { - handleChallengeCallsCount += 1 - handleChallengeFromClosure!(challenge, completion) + var performChallengeFromClosure: ((PO3DS2ChallengeParameters) throws -> PO3DS2ChallengeResult)! { + get { lock.withLock { _performChallengeFromClosure } } + set { lock.withLock { _performChallengeFromClosure = newValue } } } - func handle(redirect: PO3DSRedirect, completion: @escaping (Result) -> Void) { - handleRedirectCallsCount += 1 - handleRedirectFromClosure!(redirect, completion) + // MARK: - PO3DSService + + func authenticationRequestParameters( + configuration: PO3DS2Configuration + ) async throws -> PO3DS2AuthenticationRequestParameters { + try lock.withLock { + _authenticationRequestParametersCallsCount += 1 + return try _authenticationRequestParametersFromClosure(configuration) + } } + + func performChallenge(with parameters: PO3DS2ChallengeParameters) async throws -> PO3DS2ChallengeResult { + try lock.withLock { + _performChallengeCallsCount += 1 + return try _performChallengeFromClosure(parameters) + } + } + + // MARK: - Private Properties + + private let lock = POUnfairlyLocked() + + private nonisolated(unsafe) var _authenticationRequestParametersCallsCount = 0 + private nonisolated(unsafe) var _authenticationRequestParametersFromClosure: ((PO3DS2Configuration) throws -> PO3DS2AuthenticationRequestParameters)! + + private nonisolated(unsafe) var _performChallengeCallsCount = 0 + private nonisolated(unsafe) var _performChallengeFromClosure: ((PO3DS2ChallengeParameters) throws -> PO3DS2ChallengeResult)! } -// swiftlint:enable line_length +// swiftlint:enable identifier_name line_length diff --git a/Tests/ProcessOutTests/Sources/Mocks/HttpConnectorRequestMapper/MockHttpConnectorRequestMapper.swift b/Tests/ProcessOutTests/Sources/Mocks/HttpConnectorRequestMapper/MockHttpConnectorRequestMapper.swift index 5dcdb7694..bb21b6505 100644 --- a/Tests/ProcessOutTests/Sources/Mocks/HttpConnectorRequestMapper/MockHttpConnectorRequestMapper.swift +++ b/Tests/ProcessOutTests/Sources/Mocks/HttpConnectorRequestMapper/MockHttpConnectorRequestMapper.swift @@ -6,15 +6,32 @@ // import Foundation -@testable import ProcessOut +@testable @_spi(PO) import ProcessOut -final class MockHttpConnectorRequestMapper: HttpConnectorRequestMapper { +final class MockHttpConnectorRequestMapper: HttpConnectorRequestMapper, Sendable { - var urlRequestFromCallsCount = 0 - var urlRequestFromClosure: (() throws -> URLRequest)! + var urlRequestFromCallsCount: Int { + lock.withLock { _urlRequestFromCallsCount } + } + + var urlRequestFromClosure: (() throws -> URLRequest)! { + get { lock.withLock { _urlRequestFromClosure } } + set { lock.withLock { _urlRequestFromClosure = newValue } } + } + + // MARK: - HttpConnectorRequestMapper func urlRequest(from request: HttpConnectorRequest) throws -> URLRequest { - urlRequestFromCallsCount += 1 - return try urlRequestFromClosure() + try lock.withLock { + _urlRequestFromCallsCount += 1 + return try _urlRequestFromClosure() + } } + + // MARK: - Private Properties + + private let lock = POUnfairlyLocked() + + private nonisolated(unsafe) var _urlRequestFromCallsCount = 0 + private nonisolated(unsafe) var _urlRequestFromClosure: (() throws -> URLRequest)! } diff --git a/Tests/ProcessOutTests/Sources/Mocks/UrlProtocol/MockUrlProtocol.swift b/Tests/ProcessOutTests/Sources/Mocks/UrlProtocol/MockUrlProtocol.swift index 10a200f2c..368790069 100644 --- a/Tests/ProcessOutTests/Sources/Mocks/UrlProtocol/MockUrlProtocol.swift +++ b/Tests/ProcessOutTests/Sources/Mocks/UrlProtocol/MockUrlProtocol.swift @@ -6,8 +6,9 @@ // import Foundation +@_spi(PO) import ProcessOut -final class MockUrlProtocol: URLProtocol { +final class MockUrlProtocol: URLProtocol, @unchecked Sendable { /// Method doesn't validate whether handler for given method/path is already registered. static func register( @@ -16,15 +17,11 @@ final class MockUrlProtocol: URLProtocol { handler: @escaping (URLRequest) async throws -> (URLResponse, Data) ) { let route = MockUrlProtocolRoute(method: method, path: path, handler: handler) - lock.withLock { - routes.append(route) - } + routes.withLock { $0.append(route) } } static func removeRegistrations() { - lock.withLock { - routes = [] - } + routes.withLock { $0 = [] } } /// Implementation raises an assertion failure is given request can't be handled. @@ -32,10 +29,7 @@ final class MockUrlProtocol: URLProtocol { guard let urlAbsoluteString = request.url?.absoluteString else { fatalError("Invalid request") } - var availableRoutes: [MockUrlProtocolRoute] = [] - lock.withLock { - availableRoutes = routes - } + let availableRoutes = routes.wrappedValue for route in availableRoutes { if let method = route.method, method != request.httpMethod { continue @@ -55,8 +49,7 @@ final class MockUrlProtocol: URLProtocol { // MARK: - Private Properties - private static var routes: [MockUrlProtocolRoute] = [] - private static var lock = NSLock() + private static let routes = POUnfairlyLocked<[MockUrlProtocolRoute]>(wrappedValue: []) // MARK: - URLProtocol @@ -69,18 +62,19 @@ final class MockUrlProtocol: URLProtocol { } override func startLoading() { - currentTask = Task { [weak self] in + let task = Task { [weak self] in await self?.startLoadingAsync() } + currentTask.withLock { $0 = task } } override func stopLoading() { - currentTask?.cancel() + currentTask.withLock { $0?.cancel() } } // MARK: - Private Properties - private var currentTask: Task? + private let currentTask = POUnfairlyLocked?>(wrappedValue: nil) // MARK: - Private Methods From b1ee77f71f159ee4c5a3fd2d2c43169639c93bbc Mon Sep 17 00:00:00 2001 From: Andrii Vysotskyi Date: Wed, 7 Aug 2024 17:38:39 +0200 Subject: [PATCH 13/28] Replace NSLock with UnfairLock --- .../Logger/Destinations/SystemLoggerDestination.swift | 8 +++----- Sources/ProcessOut/Sources/Core/Logger/POLogger.swift | 4 ++-- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/Sources/ProcessOut/Sources/Core/Logger/Destinations/SystemLoggerDestination.swift b/Sources/ProcessOut/Sources/Core/Logger/Destinations/SystemLoggerDestination.swift index e48772e63..318239716 100644 --- a/Sources/ProcessOut/Sources/Core/Logger/Destinations/SystemLoggerDestination.swift +++ b/Sources/ProcessOut/Sources/Core/Logger/Destinations/SystemLoggerDestination.swift @@ -12,8 +12,7 @@ final class SystemLoggerDestination: LoggerDestination { init(subsystem: String) { self.subsystem = subsystem - lock = NSLock() - logs = [:] + logs = POUnfairlyLocked(wrappedValue: [:]) } func log(event: LogEvent) { @@ -30,8 +29,7 @@ final class SystemLoggerDestination: LoggerDestination { // MARK: - Private Properties private let subsystem: String - private let lock: NSLock - private nonisolated(unsafe) var logs: [String: OSLog] + private let logs: POUnfairlyLocked<[String: OSLog]> // MARK: - Private Methods @@ -49,7 +47,7 @@ final class SystemLoggerDestination: LoggerDestination { } private func osLog(category: String) -> OSLog { - let log = lock.withLock { + let log = logs.withLock { logs in if let log = logs[category] { return log } diff --git a/Sources/ProcessOut/Sources/Core/Logger/POLogger.swift b/Sources/ProcessOut/Sources/Core/Logger/POLogger.swift index f01fde5eb..b6b337374 100644 --- a/Sources/ProcessOut/Sources/Core/Logger/POLogger.swift +++ b/Sources/ProcessOut/Sources/Core/Logger/POLogger.swift @@ -16,7 +16,7 @@ public struct POLogger: Sendable { self.category = category self.minimumLevel = minimumLevel self.attributes = [:] - lock = NSLock() + lock = POUnfairlyLocked() } init(destinations: [LoggerDestination] = [], category: String) { @@ -84,7 +84,7 @@ public struct POLogger: Sendable { private let destinations: [LoggerDestination] private let minimumLevel: @Sendable () -> LogLevel - private let lock: NSLock + private let lock: POUnfairlyLocked private var attributes: [POLogAttributeKey: String] // MARK: - Private Methods From 08d9b39ec1bec11b8e70f70422bafa584696bdd8 Mon Sep 17 00:00:00 2001 From: Andrii Vysotskyi Date: Wed, 7 Aug 2024 17:42:48 +0200 Subject: [PATCH 14/28] Add web authentication session protocol --- .../ProcessOut/Sources/Api/ProcessOut.swift | 4 +- .../DefaultWebAuthenticationSession.swift | 123 ++++++++++++++++++ .../WebAuthenticationSession.swift | 116 ++--------------- 3 files changed, 136 insertions(+), 107 deletions(-) create mode 100644 Sources/ProcessOut/Sources/Core/WebAuthenticationSession/DefaultWebAuthenticationSession.swift diff --git a/Sources/ProcessOut/Sources/Api/ProcessOut.swift b/Sources/ProcessOut/Sources/Api/ProcessOut.swift index d40821695..f030d1c16 100644 --- a/Sources/ProcessOut/Sources/Api/ProcessOut.swift +++ b/Sources/ProcessOut/Sources/Api/ProcessOut.swift @@ -132,7 +132,7 @@ public final class ProcessOut: @unchecked Sendable { let configuration = self.configuration return .init(projectId: configuration.projectId, baseUrl: configuration.checkoutBaseUrl) } - let webSession = WebAuthenticationSession() + let webSession = DefaultWebAuthenticationSession() return DefaultAlternativePaymentsService( configuration: serviceConfiguration, webSession: webSession, logger: logger ) @@ -144,7 +144,7 @@ public final class ProcessOut: @unchecked Sendable { let encoder = JSONEncoder() encoder.dataEncodingStrategy = .base64 encoder.keyEncodingStrategy = .useDefaultKeys - let webSession = WebAuthenticationSession() + let webSession = DefaultWebAuthenticationSession() return DefaultThreeDSService(decoder: decoder, encoder: encoder, webSession: webSession) } diff --git a/Sources/ProcessOut/Sources/Core/WebAuthenticationSession/DefaultWebAuthenticationSession.swift b/Sources/ProcessOut/Sources/Core/WebAuthenticationSession/DefaultWebAuthenticationSession.swift new file mode 100644 index 000000000..dc39ea490 --- /dev/null +++ b/Sources/ProcessOut/Sources/Core/WebAuthenticationSession/DefaultWebAuthenticationSession.swift @@ -0,0 +1,123 @@ +// +// DefaultWebAuthenticationSession.swift +// ProcessOut +// +// Created by Andrii Vysotskyi on 07.08.2024. +// + +import AuthenticationServices + +@MainActor +final class DefaultWebAuthenticationSession: + NSObject, WebAuthenticationSession, ASWebAuthenticationPresentationContextProviding { + + override nonisolated init() { + // Ignored + } + + // MARK: - WebAuthenticationSession + + func authenticate( + using url: URL, + callbackScheme: String? = nil, + additionalHeaderFields: [String: String]? = nil + ) async throws -> URL { + let sessionProxy = WebAuthenticationSessionProxy() + return try await withTaskCancellationHandler( + operation: { + try await withCheckedThrowingContinuation { continuation in + guard !Task.isCancelled else { + let failure = POFailure(message: "Authentication session was cancelled.", code: .cancelled) + continuation.resume(throwing: failure) + return + } + let session = ASWebAuthenticationSession( + url: url, + callbackURLScheme: callbackScheme, + completionHandler: { url, error in + sessionProxy.invalidate() + if let error { + continuation.resume(throwing: Self.converted(error: error)) + } else if let url { + continuation.resume(returning: url) + } else { + preconditionFailure("Unexpected ASWebAuthenticationSession completion result.") + } + } + ) + session.prefersEphemeralWebBrowserSession = true + session.presentationContextProvider = self + if #available(iOS 17.4, *) { + session.additionalHeaderFields = additionalHeaderFields + } + sessionProxy.setSession(session, continuation: continuation) + session.start() + } + }, + onCancel: { + sessionProxy.cancel() + } + ) + } + + // MARK: - ASWebAuthenticationPresentationContextProviding + + func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor { + let application = UIApplication.shared + let scene = application.connectedScenes.first { $0 is UIWindowScene } as? UIWindowScene + let window = scene?.windows.first(where: \.isKeyWindow) + return window ?? ASPresentationAnchor() + } + + // MARK: - Private Methods + + private static func converted(error: Error) -> POFailure { + guard let error = error as? ASWebAuthenticationSessionError else { + return POFailure(code: .generic(.mobile), underlyingError: error) + } + let poCode: POFailure.Code + switch error.code { + case .canceledLogin: + poCode = .cancelled + case .presentationContextNotProvided, .presentationContextInvalid: + poCode = .internal(.mobile) + @unknown default: + poCode = .generic(.mobile) + } + return POFailure(code: poCode, underlyingError: error) + } +} + +@MainActor +private final class WebAuthenticationSessionProxy: Sendable { + + func setSession(_ session: ASWebAuthenticationSession, continuation: CheckedContinuation) { + self.session = session + self.continuation = continuation + } + + func invalidate() { + session = nil + continuation = nil + } + + nonisolated func cancel() { + Task { @MainActor in + _cancel() + } + } + + // MARK: - Private Properties + + private var session: ASWebAuthenticationSession? + private var continuation: CheckedContinuation? + + // MARK: - Private Methods + + private func _cancel() { + let failure = POFailure(message: "Authentication session was cancelled.", code: .cancelled) + session?.cancel() + continuation?.resume(throwing: failure) + invalidate() + } +} diff --git a/Sources/ProcessOut/Sources/Core/WebAuthenticationSession/WebAuthenticationSession.swift b/Sources/ProcessOut/Sources/Core/WebAuthenticationSession/WebAuthenticationSession.swift index 7ccbbf187..9ce8f6457 100644 --- a/Sources/ProcessOut/Sources/Core/WebAuthenticationSession/WebAuthenticationSession.swift +++ b/Sources/ProcessOut/Sources/Core/WebAuthenticationSession/WebAuthenticationSession.swift @@ -5,116 +5,22 @@ // Created by Andrii Vysotskyi on 01.08.2024. // -import AuthenticationServices +import Foundation -@MainActor -final class WebAuthenticationSession: NSObject, Sendable, ASWebAuthenticationPresentationContextProviding { - - override nonisolated init() { - // Ignored - } +protocol WebAuthenticationSession: Sendable { + /// Begins a web authentication session. func authenticate( - using url: URL, - callbackScheme: String? = nil, - additionalHeaderFields: [String: String]? = nil - ) async throws -> URL { - let sessionProxy = WebAuthenticationSessionProxy() - return try await withTaskCancellationHandler( - operation: { - try await withCheckedThrowingContinuation { continuation in - guard !Task.isCancelled else { - let failure = POFailure(message: "Authentication session was cancelled.", code: .cancelled) - continuation.resume(throwing: failure) - return - } - let session = ASWebAuthenticationSession( - url: url, - callbackURLScheme: callbackScheme, - completionHandler: { url, error in - sessionProxy.invalidate() - if let error { - continuation.resume(throwing: Self.converted(error: error)) - } else if let url { - continuation.resume(returning: url) - } else { - preconditionFailure("Unexpected ASWebAuthenticationSession completion result.") - } - } - ) - session.prefersEphemeralWebBrowserSession = true - session.presentationContextProvider = self - if #available(iOS 17.4, *) { - session.additionalHeaderFields = additionalHeaderFields - } - sessionProxy.setSession(session, continuation: continuation) - session.start() - } - }, - onCancel: { - sessionProxy.cancel() - } - ) - } - - // MARK: - ASWebAuthenticationPresentationContextProviding - - func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor { - let application = UIApplication.shared - let scene = application.connectedScenes.first { $0 is UIWindowScene } as? UIWindowScene - let window = scene?.windows.first(where: \.isKeyWindow) - return window ?? ASPresentationAnchor() - } - - // MARK: - Private Methods - - private static func converted(error: Error) -> POFailure { - guard let error = error as? ASWebAuthenticationSessionError else { - return POFailure(code: .generic(.mobile), underlyingError: error) - } - let poCode: POFailure.Code - switch error.code { - case .canceledLogin: - poCode = .cancelled - case .presentationContextNotProvided, .presentationContextInvalid: - poCode = .internal(.mobile) - @unknown default: - poCode = .generic(.mobile) - } - return POFailure(code: poCode, underlyingError: error) - } + using url: URL, callbackScheme: String?, additionalHeaderFields: [String: String]? + ) async throws -> URL } -@MainActor -private final class WebAuthenticationSessionProxy: Sendable { +extension WebAuthenticationSession { - func setSession(_ session: ASWebAuthenticationSession, continuation: CheckedContinuation) { - self.session = session - self.continuation = continuation - } - - func invalidate() { - session = nil - continuation = nil - } - - nonisolated func cancel() { - Task { @MainActor in - _cancel() - } - } - - // MARK: - Private Properties - - private var session: ASWebAuthenticationSession? - private var continuation: CheckedContinuation? - - // MARK: - Private Methods - - private func _cancel() { - let failure = POFailure(message: "Authentication session was cancelled.", code: .cancelled) - session?.cancel() - continuation?.resume(throwing: failure) - invalidate() + /// Begins a web authentication session. + func authenticate( + using url: URL, callbackScheme: String? = nil, additionalHeaderFields headerFields: [String: String]? = nil + ) async throws -> URL { + try await authenticate(using: url, callbackScheme: callbackScheme, additionalHeaderFields: headerFields) } } From e24c5fe31790c5775d4e6dd63449b3a7f6a33ffb Mon Sep 17 00:00:00 2001 From: Andrii Vysotskyi Date: Thu, 8 Aug 2024 12:46:05 +0200 Subject: [PATCH 15/28] Add mock web authentication session --- .../MockWebAuthenticationSession.swift | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 Tests/ProcessOutTests/Sources/Mocks/WebAuthenticationSession/MockWebAuthenticationSession.swift diff --git a/Tests/ProcessOutTests/Sources/Mocks/WebAuthenticationSession/MockWebAuthenticationSession.swift b/Tests/ProcessOutTests/Sources/Mocks/WebAuthenticationSession/MockWebAuthenticationSession.swift new file mode 100644 index 000000000..2c20acb1a --- /dev/null +++ b/Tests/ProcessOutTests/Sources/Mocks/WebAuthenticationSession/MockWebAuthenticationSession.swift @@ -0,0 +1,40 @@ +// +// MockWebAuthenticationSession.swift +// ProcessOutTests +// +// Created by Andrii Vysotskyi on 07.08.2024. +// + +import Foundation +@testable @_spi(PO) import ProcessOut + +final class MockWebAuthenticationSession: WebAuthenticationSession { + + var authenticateCallsCount: Int { + lock.withLock { _authenticateCallsCount } + } + + var authenticateFromClosure: ((URL, String?, [String: String]?) async throws -> URL)! { + get { lock.withLock { _authenticateFromClosure } } + set { lock.withLock { _authenticateFromClosure = newValue } } + } + + // MARK: - + + func authenticate( + using url: URL, callbackScheme: String?, additionalHeaderFields: [String: String]? + ) async throws -> URL { + let authenticate = lock.withLock { + _authenticateCallsCount += 1 + return _authenticateFromClosure + } + return try await authenticate!(url, callbackScheme, additionalHeaderFields) + } + + // MARK: - Private Properties + + private let lock = POUnfairlyLocked() + + private nonisolated(unsafe) var _authenticateCallsCount = 0 + private nonisolated(unsafe) var _authenticateFromClosure: ((URL, String?, [String: String]?) async throws -> URL)! +} From e7b771db43a66d5bb05852ef06cb259e2ddbb526 Mon Sep 17 00:00:00 2001 From: Andrii Vysotskyi Date: Thu, 8 Aug 2024 12:46:24 +0200 Subject: [PATCH 16/28] Remove redundant defaults --- .../DefaultWebAuthenticationSession.swift | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Sources/ProcessOut/Sources/Core/WebAuthenticationSession/DefaultWebAuthenticationSession.swift b/Sources/ProcessOut/Sources/Core/WebAuthenticationSession/DefaultWebAuthenticationSession.swift index dc39ea490..8ee950b81 100644 --- a/Sources/ProcessOut/Sources/Core/WebAuthenticationSession/DefaultWebAuthenticationSession.swift +++ b/Sources/ProcessOut/Sources/Core/WebAuthenticationSession/DefaultWebAuthenticationSession.swift @@ -18,9 +18,7 @@ final class DefaultWebAuthenticationSession: // MARK: - WebAuthenticationSession func authenticate( - using url: URL, - callbackScheme: String? = nil, - additionalHeaderFields: [String: String]? = nil + using url: URL, callbackScheme: String?, additionalHeaderFields: [String: String]? ) async throws -> URL { let sessionProxy = WebAuthenticationSessionProxy() return try await withTaskCancellationHandler( From f0eeac1796794bae78589a87241749efb22c97c8 Mon Sep 17 00:00:00 2001 From: Andrii Vysotskyi Date: Thu, 8 Aug 2024 13:17:52 +0200 Subject: [PATCH 17/28] Update error utility --- Tests/ProcessOutTests/Sources/Core/Utils.swift | 9 ++++++--- .../Unit/Core/Utils/AsyncUtilsTests.swift | 17 ++++++----------- 2 files changed, 12 insertions(+), 14 deletions(-) diff --git a/Tests/ProcessOutTests/Sources/Core/Utils.swift b/Tests/ProcessOutTests/Sources/Core/Utils.swift index 689aa270e..60ccef50c 100644 --- a/Tests/ProcessOutTests/Sources/Core/Utils.swift +++ b/Tests/ProcessOutTests/Sources/Core/Utils.swift @@ -12,16 +12,19 @@ import XCTest /// - expression: An expression that can throw an error. /// - message: An optional description of a failure. @discardableResult -func assertThrowsError( +func assertThrowsError( _ expression: @autoclosure () async throws -> T, _ message: @autoclosure () -> String = "", + errorType: E.Type = Error.self, file: StaticString = #filePath, line: UInt = #line -) async -> Error? { +) async -> E? { do { _ = try await expression() - } catch { + } catch let error as E { return error + } catch { + XCTFail("Unexpected error type") } XCTFail(message(), file: file, line: line) return nil diff --git a/Tests/ProcessOutTests/Sources/Unit/Core/Utils/AsyncUtilsTests.swift b/Tests/ProcessOutTests/Sources/Unit/Core/Utils/AsyncUtilsTests.swift index 551feb743..ba9421359 100644 --- a/Tests/ProcessOutTests/Sources/Unit/Core/Utils/AsyncUtilsTests.swift +++ b/Tests/ProcessOutTests/Sources/Unit/Core/Utils/AsyncUtilsTests.swift @@ -36,14 +36,11 @@ final class AsyncUtilsTests: XCTestCase { // When let error = await assertThrowsError( - try await withTimeout(timeout, error: Failure.timeout, perform: operation) + try await withTimeout(timeout, error: Failure.timeout, perform: operation), errorType: Failure.self ) // Then - if let failure = error as? Failure, failure == .timeout { - return - } - XCTFail("Expected timeout failure.") + XCTAssertEqual(error, .timeout, "Expected timeout failure.") } func test_withTimeout_whenNonCancellableOperationTimesOut_ignoresTimeout() async throws { @@ -133,14 +130,12 @@ final class AsyncUtilsTests: XCTestCase { while: { _ in false }, timeout: 1, timeoutError: Failure.timeout - ) + ), + errorType: Failure.self ) // Then - if let failure = error as? Failure, failure == .timeout { - return - } - XCTFail("Expected timeout failure.") + XCTAssertEqual(error, .timeout, "Expected timeout failure.") } func test_retry_checksRetryCondition_whenRetryStrategyIsSet() async throws { @@ -286,7 +281,7 @@ final class AsyncUtilsTests: XCTestCase { // MARK: - Private Nested Types - private enum Failure: Error { + private enum Failure: Error, Equatable { case timeout, generic, cancel } } From 696f24b72a52740910fe03e15d947388c170e634 Mon Sep 17 00:00:00 2001 From: Andrii Vysotskyi Date: Thu, 8 Aug 2024 13:18:25 +0200 Subject: [PATCH 18/28] Fix APM tests --- ...lternativePaymentMethodsServiceTests.swift | 50 ++++++------------- 1 file changed, 14 insertions(+), 36 deletions(-) diff --git a/Tests/ProcessOutTests/Sources/Unit/Service/AlternativePaymentMethods/DefaultAlternativePaymentMethodsServiceTests.swift b/Tests/ProcessOutTests/Sources/Unit/Service/AlternativePaymentMethods/DefaultAlternativePaymentMethodsServiceTests.swift index 3b85cf1a9..a44d19104 100644 --- a/Tests/ProcessOutTests/Sources/Unit/Service/AlternativePaymentMethods/DefaultAlternativePaymentMethodsServiceTests.swift +++ b/Tests/ProcessOutTests/Sources/Unit/Service/AlternativePaymentMethods/DefaultAlternativePaymentMethodsServiceTests.swift @@ -13,21 +13,24 @@ final class DefaultAlternativePaymentMethodsServiceTests: XCTestCase { override func setUp() { super.setUp() - let configuration = AlternativePaymentMethodsServiceConfiguration( - projectId: "proj_test", baseUrl: URL(string: "https://example.com")! + sut = DefaultAlternativePaymentsService( + configuration: { + .init(projectId: "proj_test", baseUrl: URL(string: "https://example.com")!) + }, + webSession: MockWebAuthenticationSession(), + logger: .stub ) - sut = DefaultAlternativePaymentMethodsService(configuration: { configuration }, logger: .stub) } func test_alternativePaymentMethodUrl_authorizationWithAdditionalData_succeeds() throws { - let request = POAlternativePaymentMethodRequest( + let request = POAlternativePaymentAuthorizationRequest( invoiceId: "iv_test", gatewayConfigurationId: "gway_conf_test", additionalData: ["field1": "test", "field2": "test2"] ) // When - let url = sut.alternativePaymentMethodUrl(request: request) + let url = try sut.url(for: request) // Then let expectedUrls = [ @@ -41,14 +44,14 @@ final class DefaultAlternativePaymentMethodsServiceTests: XCTestCase { } func test_alternativePaymentMethodUrl_tokenization_succeeds() throws { - let request = POAlternativePaymentMethodRequest( + let request = POAlternativePaymentTokenizationRequest( customerId: "cust_test", tokenId: "tok_test", gatewayConfigurationId: "gway_conf_test" ) // When - let url = sut.alternativePaymentMethodUrl(request: request) + let url = try sut.url(for: request) // Then let expectedUrl = "https://example.com/proj_test/cust_test/tok_test/redirect/gway_conf_test" @@ -56,44 +59,19 @@ final class DefaultAlternativePaymentMethodsServiceTests: XCTestCase { } func test_alternativePaymentMethodUrl_authorizationWithToken_succeeds() throws { - let request = POAlternativePaymentMethodRequest( + let request = POAlternativePaymentAuthorizationRequest( invoiceId: "iv_test", gatewayConfigurationId: "gway_conf_test", tokenId: "tok_test" ) // When - let url = sut.alternativePaymentMethodUrl(request: request) + let url = try sut.url(for: request) // Then let expectedUrl = "https://example.com/proj_test/iv_test/redirect/gway_conf_test/tokenized/tok_test" XCTAssertEqual(url.absoluteString, expectedUrl) } - func test_alternativePaymentMethodResponse_withOnlyGatewayToken_succeeds() throws { - let result: POAlternativePaymentMethodResponse? = try sut.alternativePaymentMethodResponse( - url: URL(string: "https://processout.return?token=gway_req_test")! - ) - - XCTAssertEqual(result?.gatewayToken, "gway_req_test") - } - - func test_alternativePaymentMethodResponse_withCustomerToken_succeeds() throws { - let result: POAlternativePaymentMethodResponse? = try sut.alternativePaymentMethodResponse( - url: URL(string: "https://processout.return?token=gway_req_test&token_id=tok_test&customer_id=cust_test")! - ) - - XCTAssertEqual(result?.gatewayToken, "gway_req_test") - XCTAssertEqual(result?.tokenId, "tok_test") - XCTAssertEqual(result?.customerId, "cust_test") - } - - func test_alternativePaymentMethodResponse_whenTokenIsNotSet_succeeds() throws { - // When - let url = URL(string: "test://return")! - let result = try sut.alternativePaymentMethodResponse(url: url) - - // Then - XCTAssertTrue(result.gatewayToken.isEmpty) - } + // MARK: - Private Properties - private var sut: DefaultAlternativePaymentMethodsService! + private var sut: DefaultAlternativePaymentsService! } From 41efb995643c519890106048b7cfdeab9f148f55 Mon Sep 17 00:00:00 2001 From: Andrii Vysotskyi Date: Thu, 8 Aug 2024 13:18:39 +0200 Subject: [PATCH 19/28] Fix 3DS tests --- .../3DS/DefaultThreeDSServiceTests.swift | 239 ++++++++---------- 1 file changed, 102 insertions(+), 137 deletions(-) diff --git a/Tests/ProcessOutTests/Sources/Unit/Service/3DS/DefaultThreeDSServiceTests.swift b/Tests/ProcessOutTests/Sources/Unit/Service/3DS/DefaultThreeDSServiceTests.swift index 766720fc4..5ccb8daf3 100644 --- a/Tests/ProcessOutTests/Sources/Unit/Service/3DS/DefaultThreeDSServiceTests.swift +++ b/Tests/ProcessOutTests/Sources/Unit/Service/3DS/DefaultThreeDSServiceTests.swift @@ -15,8 +15,9 @@ final class DefaultThreeDSServiceTests: XCTestCase { super.setUp() let encoder = JSONEncoder() encoder.outputFormatting = .sortedKeys + webSession = MockWebAuthenticationSession() sut = DefaultThreeDSService( - decoder: JSONDecoder(), encoder: encoder, jsonWritingOptions: [.sortedKeys] + decoder: JSONDecoder(), encoder: encoder, jsonWritingOptions: [.sortedKeys], webSession: webSession ) delegate = Mock3DSService() } @@ -31,17 +32,12 @@ final class DefaultThreeDSServiceTests: XCTestCase { let customerAction = ThreeDSCustomerAction(type: .fingerprintMobile, value: value) // When - let handlingError = await assertThrowsError( - try await sut.handle(action: customerAction, delegate: delegate) + let failure = await assertThrowsError( + try await sut.handle(action: customerAction, delegate: delegate), errorType: POFailure.self ) // Then - switch handlingError { - case let failure as POFailure: - XCTAssertEqual(failure.code, .internal(.mobile)) - default: - XCTFail("Unexpected result") - } + XCTAssertEqual(failure?.code, .internal(.mobile)) } } @@ -56,17 +52,17 @@ final class DefaultThreeDSServiceTests: XCTestCase { directoryServerPublicKey: "2", directoryServerRootCertificates: ["3"], directoryServerTransactionId: "4", - scheme: .unknown("5"), + scheme: .init(rawValue: "5"), messageVersion: "6" ) var delegateCallsCount = 0 for customerAction in customerActions { - delegate.authenticationRequestFromClosure = { configuration, completion in + delegate.authenticationRequestParametersFromClosure = { configuration in // Then XCTAssertEqual(configuration, expectedConfiguration) delegateCallsCount += 1 - completion(.failure(.init(code: .generic(.mobile)))) + throw POFailure(code: .generic(.mobile)) } // When @@ -77,65 +73,53 @@ final class DefaultThreeDSServiceTests: XCTestCase { func test_handle_whenDelegateAuthenticationRequestFails_propagatesFailure() async { // Given - let error = POFailure(code: .unknown(rawValue: "test-error")) - delegate.authenticationRequestFromClosure = { _, completion in - completion(.failure(error)) + let expectedError = POFailure(code: .unknown(rawValue: "test-error")) + delegate.authenticationRequestParametersFromClosure = { _ in + throw expectedError } let customerAction = defaultFingerprintMobileCustomerAction() // When - let handlingError = await assertThrowsError( - try await sut.handle(action: customerAction, delegate: delegate) + let failure = await assertThrowsError( + try await sut.handle(action: customerAction, delegate: delegate), errorType: POFailure.self ) // Then - switch handlingError { - case let failure as POFailure: - XCTAssertEqual(failure.code, error.code) - default: - XCTFail("Unexpected result") - } + XCTAssertEqual(failure?.code, expectedError.code) } func test_handle_whenAuthenticationRequestPublicKeyIsEmpty_fails() async { // Given var isDelegateCalled = false - delegate.authenticationRequestFromClosure = { _, completion in - let invalidAuthenticationRequest = PO3DS2AuthenticationRequest( + delegate.authenticationRequestParametersFromClosure = { _ in + isDelegateCalled = true + return .init( deviceData: "", sdkAppId: "", sdkEphemeralPublicKey: "", sdkReferenceNumber: "", sdkTransactionId: "" ) - isDelegateCalled = true - completion(.success(invalidAuthenticationRequest)) } let customerAction = defaultFingerprintMobileCustomerAction() // When - let error = await assertThrowsError( - try await sut.handle(action: customerAction, delegate: delegate) + let failure = await assertThrowsError( + try await sut.handle(action: customerAction, delegate: delegate), errorType: POFailure.self ) // Then - switch error { - case let failure as POFailure: - XCTAssertEqual(failure.code, .internal(.mobile)) - default: - XCTFail("Unexpected result") - } + XCTAssertEqual(failure?.code, .internal(.mobile)) XCTAssertTrue(isDelegateCalled) } func test_handle_whenAuthenticationRequestIsValid_succeeds() async throws { // Given let customerAction = defaultFingerprintMobileCustomerAction() - delegate.authenticationRequestFromClosure = { _, completion in - let authenticationRequest = PO3DS2AuthenticationRequest( + delegate.authenticationRequestParametersFromClosure = { _ in + PO3DS2AuthenticationRequestParameters( deviceData: "1", sdkAppId: "2", sdkEphemeralPublicKey: #"{"kty": "EC"}"#, sdkReferenceNumber: "3", sdkTransactionId: "4" ) - completion(.success(authenticationRequest)) } // When @@ -157,33 +141,28 @@ final class DefaultThreeDSServiceTests: XCTestCase { let customerAction = ThreeDSCustomerAction(type: .challengeMobile, value: "") // When - let error = await assertThrowsError( - try await sut.handle(action: customerAction, delegate: delegate) + let failure = await assertThrowsError( + try await sut.handle(action: customerAction, delegate: delegate), errorType: POFailure.self ) // Then - switch error { - case let failure as POFailure: - XCTAssertEqual(failure.code, .internal(.mobile)) - default: - XCTFail("Unexpected result") - } + XCTAssertEqual(failure?.code, .internal(.mobile)) } func test_handle_whenChallengeMobileValueIsValid_callsDelegateDoChallenge() async throws { // Given - let expectedChallenge = PO3DS2Challenge( + let expectedChallenge = PO3DS2ChallengeParameters( acsTransactionId: "1", acsReferenceNumber: "2", acsSignedContent: "3", threeDSServerTransactionId: "4" ) var isDelegateCalled = false - delegate.handleChallengeFromClosure = { challenge, completion in + delegate.performChallengeFromClosure = { challenge in // Then XCTAssertEqual(challenge, expectedChallenge) isDelegateCalled = true - completion(.success(true)) + return .init(transactionStatus: "Y") } // When @@ -193,60 +172,53 @@ final class DefaultThreeDSServiceTests: XCTestCase { func test_handle_whenDelegateDoChallengeFails_propagatesFailure() async { // Given - let error = POFailure(code: .unknown(rawValue: "test-error")) - delegate.handleChallengeFromClosure = { _, completion in - completion(.failure(error)) + let expectedError = POFailure(code: .unknown(rawValue: "test-error")) + delegate.performChallengeFromClosure = { _ in + throw expectedError } // When - let handlingError = await assertThrowsError( - try await sut.handle(action: defaultChallengeMobileCustomerAction, delegate: delegate) + let failure = await assertThrowsError( + try await sut.handle(action: defaultChallengeMobileCustomerAction, delegate: delegate), + errorType: POFailure.self ) // Then - switch handlingError { - case let failure as POFailure: - XCTAssertEqual(failure.code, error.code) - default: - XCTFail("Unexpected result") - } + XCTAssertEqual(failure?.code, expectedError.code) } func test_handle_whenDelegateDoChallengeCompletesWithTrue_succeeds() async throws { // Given - delegate.handleChallengeFromClosure = { _, completion in - completion(.success(true)) + delegate.performChallengeFromClosure = { _ in + .init(transactionStatus: "Y") } // When let token = try await sut.handle(action: defaultChallengeMobileCustomerAction, delegate: delegate) // Then - XCTAssertEqual(token, "gway_req_eyJib2R5IjoieyBcInRyYW5zU3RhdHVzXCI6IFwiWVwiIH0ifQ==") + XCTAssertEqual(token, "gway_req_eyJib2R5Ijoie1widHJhbnNTdGF0dXNcIjpcIllcIn0ifQ==") } func test_handle_whenDelegateDoChallengeCompletesWithFalse_succeeds() async throws { // Given - delegate.handleChallengeFromClosure = { _, completion in - completion(.success(false)) + delegate.performChallengeFromClosure = { _ in + .init(transactionStatus: "N") } // When let token = try await sut.handle(action: defaultChallengeMobileCustomerAction, delegate: delegate) // Then - XCTAssertEqual(token, "gway_req_eyJib2R5IjoieyBcInRyYW5zU3RhdHVzXCI6IFwiTlwiIH0ifQ==") + XCTAssertEqual(token, "gway_req_eyJib2R5Ijoie1widHJhbnNTdGF0dXNcIjpcIk5cIn0ifQ==") } // MARK: - Redirect - func test_handle_whenActionTypeIsUrlOrFingerprint_callsDelegateRedirect() async throws { + func test_handle_whenActionTypeIsUrlOrFingerprint_callsWebSession() async throws { // Given - var delegateCallsCount = 0 - delegate.handleRedirectFromClosure = { _, completion in - // Then - delegateCallsCount += 1 - completion(.success("")) + webSession.authenticateFromClosure = { _, _, _ in + URL(string: "example.com")! } let actionTypes: [ThreeDSCustomerAction.ActionType] = [.url, .fingerprint] @@ -256,7 +228,9 @@ final class DefaultThreeDSServiceTests: XCTestCase { // When _ = try await sut.handle(action: customerAction, delegate: delegate) } - XCTAssertEqual(delegateCallsCount, actionTypes.count) + + // Then + XCTAssertEqual(webSession.authenticateCallsCount, actionTypes.count) } func test_handle_whenRedirectOrFingerprintValueIsNotValidUrl_fails() async { @@ -264,46 +238,35 @@ final class DefaultThreeDSServiceTests: XCTestCase { let actionTypes: [ThreeDSCustomerAction.ActionType] = [.redirect, .url, .fingerprint] for actionType in actionTypes { - let action = ThreeDSCustomerAction(type: actionType, value: "http://:-1") + let action = ThreeDSCustomerAction(type: actionType, value: "") // When - let error = await assertThrowsError( - try await sut.handle(action: action, delegate: delegate) + let failure = await assertThrowsError( + try await sut.handle(action: action, delegate: delegate), errorType: POFailure.self ) // Then - switch error { - case let failure as POFailure: - XCTAssertEqual(failure.code, .internal(.mobile)) - default: - XCTFail("Unexpected result") - } + XCTAssertEqual(failure?.code, .internal(.mobile)) } } - func test_handle_whenRedirectValueIsValidUrl_callsDelegateRedirect() async throws { + func test_handle_whenRedirectValueIsValidUrl_callsWebSession() async throws { // Given - let expectedRedirect = PO3DSRedirect( - url: URL(string: "example.com")!, timeout: nil - ) - var isDelegateCalled = false - delegate.handleRedirectFromClosure = { redirect, completion in - // Then - XCTAssertEqual(redirect, expectedRedirect) - isDelegateCalled = true - completion(.success("")) + webSession.authenticateFromClosure = { url, _, _ in + XCTAssertEqual(URL(string: "example.com"), url) + return URL(string: "test://return")! } let customerAction = ThreeDSCustomerAction(type: .redirect, value: "example.com") // When _ = try await sut.handle(action: customerAction, delegate: delegate) - XCTAssertTrue(isDelegateCalled) + XCTAssertEqual(webSession.authenticateCallsCount, 1) } func test_handle_whenRedirectCompletesWithNewToken_propagatesToken() async throws { // Given - delegate.handleRedirectFromClosure = { _, completion in - completion(.success("test")) + webSession.authenticateFromClosure = { _, _, _ in + URL(string: "test://return?token=test")! } let customerAction = ThreeDSCustomerAction(type: .redirect, value: "example.com") @@ -316,51 +279,41 @@ final class DefaultThreeDSServiceTests: XCTestCase { func test_handle_whenRedirectFails_propagatesError() async { // Given - delegate.handleRedirectFromClosure = { _, completion in - let failure = POFailure(code: .unknown(rawValue: "test-error")) - completion(.failure(failure)) + webSession.authenticateFromClosure = { _, _, _ in + throw POFailure(code: .unknown(rawValue: "test-error")) } + let customerAction = ThreeDSCustomerAction(type: .redirect, value: "example.com") // When - let error = await assertThrowsError( - try await sut.handle(action: customerAction, delegate: delegate) + let failure = await assertThrowsError( + try await sut.handle(action: customerAction, delegate: delegate), errorType: POFailure.self ) // Then - switch error { - case let failure as POFailure: - XCTAssertEqual(failure.code, .unknown(rawValue: "test-error")) - default: - XCTFail("Unexpected result") - } + XCTAssertEqual(failure?.code, .unknown(rawValue: "test-error")) } // MARK: - Fingerprint - func test_handle_whenFingerprintValueIsValidUrl_callsDelegateRedirect() async throws { + func test_handle_whenFingerprintValueIsValidUrl_callsWebSession() async throws { // Given - let expectedRedirect = PO3DSRedirect( - url: URL(string: "example.com")!, timeout: 10 - ) - var isDelegateCalled = false - delegate.handleRedirectFromClosure = { redirect, completion in - // Then - XCTAssertEqual(redirect, expectedRedirect) - isDelegateCalled = true - completion(.success("")) + let expectedRedirectUrl = URL(string: "example.com")! + webSession.authenticateFromClosure = { url, _, _ in + XCTAssertEqual(url, expectedRedirectUrl) + return URL(string: "test://return")! } - let customerAction = ThreeDSCustomerAction(type: .fingerprint, value: "example.com") + let customerAction = ThreeDSCustomerAction(type: .fingerprint, value: expectedRedirectUrl.absoluteString) // When _ = try await sut.handle(action: customerAction, delegate: delegate) - XCTAssertTrue(isDelegateCalled) + XCTAssertEqual(webSession.authenticateCallsCount, 1) } func test_handle_whenFingerprintCompletesWithNewToken_propagatesToken() async throws { // Given - delegate.handleRedirectFromClosure = { _, completion in - completion(.success("test")) + webSession.authenticateFromClosure = { _, _, _ in + URL(string: "test://return?token=test")! } let customerAction = ThreeDSCustomerAction(type: .fingerprint, value: "example.com") @@ -373,31 +326,24 @@ final class DefaultThreeDSServiceTests: XCTestCase { func test_handle_whenFingerprintFails_propagatesError() async { // Given - delegate.handleRedirectFromClosure = { _, completion in - let failure = POFailure(code: .unknown(rawValue: "test-error")) - completion(.failure(failure)) + webSession.authenticateFromClosure = { _, _, _ in + throw POFailure(code: .unknown(rawValue: "test-error")) } let customerAction = ThreeDSCustomerAction(type: .fingerprint, value: "example.com") // When - let error = await assertThrowsError( - try await sut.handle(action: customerAction, delegate: delegate) + let failure = await assertThrowsError( + try await sut.handle(action: customerAction, delegate: delegate), errorType: POFailure.self ) // Then - switch error { - case let failure as POFailure: - XCTAssertEqual(failure.code, .unknown(rawValue: "test-error")) - default: - XCTFail("Unexpected result") - } + XCTAssertEqual(failure?.code, .unknown(rawValue: "test-error")) } func test_handle_whenFingerprintFailsWithTimeoutError_succeeds() async throws { // Given - delegate.handleRedirectFromClosure = { _, completion in - let failure = POFailure(code: .timeout(.mobile)) - completion(.failure(failure)) + webSession.authenticateFromClosure = { _, _, _ in + throw POFailure(code: .timeout(.mobile)) } let customerAction = ThreeDSCustomerAction(type: .fingerprint, value: "example.com") @@ -406,17 +352,36 @@ final class DefaultThreeDSServiceTests: XCTestCase { // Then let expectedValue = """ - gway_req_eyJib2R5IjoieyBcInRocmVlRFMyRmluZ2VycHJpbnRUaW1\ - lb3V0XCI6IHRydWUgfSIsInVybCI6ImV4YW1wbGUuY29tIn0= + gway_req_eyJib2R5IjoieyBcInRocmVlRFMyRmluZ2VycHJpbnRUaW1lb3V0XCI6IHRydWUgfSIsInVybCI6ImV4YW1wbGUuY29tIn0= + """ + XCTAssertEqual(value, expectedValue) + } + + func test_handle_whenFingerprintTakesTooLong_succeedsWithTimeout() async throws { + // Given + webSession.authenticateFromClosure = { _, _, _ in + try await Task.sleep(seconds: 15) + return URL(string: "test://return")! + } + let customerAction = ThreeDSCustomerAction(type: .fingerprint, value: "example.com") + + // When + let value = try await sut.handle(action: customerAction, delegate: delegate) + + // Then + let expectedValue = """ + gway_req_eyJib2R5IjoieyBcInRocmVlRFMyRmluZ2VycHJpbnRUaW1lb3V0XCI6IHRydWUgfSIsInVybCI6ImV4YW1wbGUuY29tIn0= """ XCTAssertEqual(value, expectedValue) } // MARK: - Private Properties - private var delegate: Mock3DSService! private var sut: DefaultThreeDSService! + private var delegate: Mock3DSService! + private var webSession: MockWebAuthenticationSession! + // MARK: - Private Methods private func defaultFingerprintMobileCustomerAction(padded: Bool = false) -> ThreeDSCustomerAction { From 065673da55768d34f9b46da7ac11976509af4450 Mon Sep 17 00:00:00 2001 From: Andrii Vysotskyi Date: Thu, 8 Aug 2024 13:19:01 +0200 Subject: [PATCH 20/28] Minor improvements --- .../Sources/Integration/CardsServiceTests.swift | 7 ++++--- .../Integration/CustomerTokensServiceTests.swift | 12 ++++++------ .../GatewayConfigurationsRepositoryTests.swift | 3 ++- .../Http/UrlSessionHttpConnectorTests.swift | 10 +++++----- 4 files changed, 17 insertions(+), 15 deletions(-) diff --git a/Tests/ProcessOutTests/Sources/Integration/CardsServiceTests.swift b/Tests/ProcessOutTests/Sources/Integration/CardsServiceTests.swift index 853504d14..83f7e1918 100644 --- a/Tests/ProcessOutTests/Sources/Integration/CardsServiceTests.swift +++ b/Tests/ProcessOutTests/Sources/Integration/CardsServiceTests.swift @@ -11,9 +11,10 @@ import XCTest final class CardsServiceTests: XCTestCase { + @MainActor override func setUp() { super.setUp() - ProcessOut.configure(configuration: .production(projectId: Constants.projectId), force: true) + ProcessOut.configure(configuration: .init(projectId: Constants.projectId), force: true) sut = ProcessOut.shared.cards } @@ -27,7 +28,7 @@ final class CardsServiceTests: XCTestCase { XCTAssertEqual(information.bankName, "UNITED CITIZENS BANK OF SOUTHERN KENTUCKY") XCTAssertEqual(information.brand, "visa business") XCTAssertEqual(information.category, "commercial") - XCTAssertEqual(information.$scheme.typed, .visa) + XCTAssertEqual(information.scheme, .visa) XCTAssertEqual(information.type, "debit") } @@ -99,7 +100,7 @@ final class CardsServiceTests: XCTestCase { ) // Then - XCTAssertEqual(updatedCard.$preferredScheme.typed, "test") + XCTAssertEqual(updatedCard.preferredScheme?.rawValue, "test") } func test_tokenize_whenPreferredSchemeIsSet() async throws { diff --git a/Tests/ProcessOutTests/Sources/Integration/CustomerTokensServiceTests.swift b/Tests/ProcessOutTests/Sources/Integration/CustomerTokensServiceTests.swift index 3b31e5eb3..549c884a2 100644 --- a/Tests/ProcessOutTests/Sources/Integration/CustomerTokensServiceTests.swift +++ b/Tests/ProcessOutTests/Sources/Integration/CustomerTokensServiceTests.swift @@ -10,9 +10,10 @@ import XCTest final class CustomerTokensServiceTests: XCTestCase { + @MainActor override func setUp() { super.setUp() - let configuration = ProcessOutConfiguration.production( + let configuration = ProcessOutConfiguration( projectId: Constants.projectId, privateKey: Constants.projectPrivateKey ) ProcessOut.configure(configuration: configuration, force: true) @@ -60,19 +61,18 @@ final class CustomerTokensServiceTests: XCTestCase { customerId: Constants.customerId, tokenId: try await createToken(verify: true).id, source: card.id, - verify: true, - enableThreeDS2: true + verify: true ) let threeDSService = Mock3DSService() - threeDSService.authenticationRequestFromClosure = { _, completion in - completion(.failure(.init(code: .cancelled))) + threeDSService.authenticationRequestParametersFromClosure = { _ in + throw POFailure(code: .cancelled) } // When _ = try? await sut.assignCustomerToken(request: request, threeDSService: threeDSService) // Then - XCTAssertEqual(threeDSService.authenticationRequestCallsCount, 1) + XCTAssertEqual(threeDSService.authenticationRequestParametersCallsCount, 1) } // MARK: - Private Properties diff --git a/Tests/ProcessOutTests/Sources/Integration/GatewayConfigurationsRepositoryTests.swift b/Tests/ProcessOutTests/Sources/Integration/GatewayConfigurationsRepositoryTests.swift index 3bb418ada..d257f268c 100644 --- a/Tests/ProcessOutTests/Sources/Integration/GatewayConfigurationsRepositoryTests.swift +++ b/Tests/ProcessOutTests/Sources/Integration/GatewayConfigurationsRepositoryTests.swift @@ -11,9 +11,10 @@ import XCTest final class GatewayConfigurationsRepositoryTests: XCTestCase { + @MainActor override func setUp() { super.setUp() - ProcessOut.configure(configuration: .production(projectId: Constants.projectId), force: true) + ProcessOut.configure(configuration: .init(projectId: Constants.projectId), force: true) sut = ProcessOut.shared.gatewayConfigurations } diff --git a/Tests/ProcessOutTests/Sources/Unit/Connectors/Http/UrlSessionHttpConnectorTests.swift b/Tests/ProcessOutTests/Sources/Unit/Connectors/Http/UrlSessionHttpConnectorTests.swift index a8835e573..ec0ac5dbb 100644 --- a/Tests/ProcessOutTests/Sources/Unit/Connectors/Http/UrlSessionHttpConnectorTests.swift +++ b/Tests/ProcessOutTests/Sources/Unit/Connectors/Http/UrlSessionHttpConnectorTests.swift @@ -40,11 +40,11 @@ final class UrlSessionHttpConnectorTests: XCTestCase { // When let error = await assertThrowsError( - try await sut.execute(request: defaultRequest) + try await sut.execute(request: defaultRequest), errorType: HttpConnectorFailure.self ) // Then - if let failure = error as? HttpConnectorFailure, case .encoding(let encodingError) = failure { + if case .encoding(let encodingError) = error { XCTAssertEqual(encodingError as NSError, codingError) return } @@ -215,8 +215,8 @@ final class UrlSessionHttpConnectorTests: XCTestCase { requestMapper.urlRequestFromClosure = defaultUrlRequest // When - let task = Task { - _ = try await sut.execute(request: defaultRequest) + let task = Task { [sut, defaultRequest] in + _ = try await sut!.execute(request: defaultRequest) } DispatchQueue.main.async { task.cancel() @@ -243,7 +243,7 @@ final class UrlSessionHttpConnectorTests: XCTestCase { // MARK: - Private Methods - private var defaultRequest: HttpConnectorRequest { + private var defaultRequest: HttpConnectorRequest { HttpConnectorRequest.get(path: "") } From 25a17488b4e9ccbe9c9fce2c70890cae0148820c Mon Sep 17 00:00:00 2001 From: Andrii Vysotskyi Date: Thu, 8 Aug 2024 13:34:26 +0200 Subject: [PATCH 21/28] Use beta xcode --- .github/actions/bootstrap-project/action.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/actions/bootstrap-project/action.yml b/.github/actions/bootstrap-project/action.yml index 1a2713ac0..e8f774099 100644 --- a/.github/actions/bootstrap-project/action.yml +++ b/.github/actions/bootstrap-project/action.yml @@ -16,7 +16,8 @@ runs: echo "$CONSTANTS" > Example/Example/Resources/Constants.yml shell: bash - name: Select Xcode Version - run: sudo xcode-select -s '/Applications/Xcode_15.4.app/Contents/Developer' + # Migrate to public release once available + run: sudo xcode-select -s '/Applications/Xcode_16_beta_4.app/Contents/Developer' shell: bash - name: Bootstrap Project run: ./Scripts/BootstrapProject.sh From 6208da2feb3466d63adbd1561fe2bbb055ec932a Mon Sep 17 00:00:00 2001 From: Andrii Vysotskyi Date: Thu, 8 Aug 2024 13:34:51 +0200 Subject: [PATCH 22/28] Update comment --- .github/actions/bootstrap-project/action.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/actions/bootstrap-project/action.yml b/.github/actions/bootstrap-project/action.yml index e8f774099..58b5cca45 100644 --- a/.github/actions/bootstrap-project/action.yml +++ b/.github/actions/bootstrap-project/action.yml @@ -16,7 +16,7 @@ runs: echo "$CONSTANTS" > Example/Example/Resources/Constants.yml shell: bash - name: Select Xcode Version - # Migrate to public release once available + # todo(andrii-vysotskyi): Migrate to public release once available run: sudo xcode-select -s '/Applications/Xcode_16_beta_4.app/Contents/Developer' shell: bash - name: Bootstrap Project From b3a048ce8b62cec088b6c151cb5017553291856b Mon Sep 17 00:00:00 2001 From: Andrii Vysotskyi Date: Thu, 8 Aug 2024 14:18:42 +0200 Subject: [PATCH 23/28] Fix dev project swift settings --- project.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/project.yml b/project.yml index da3eda5fd..681d64431 100644 --- a/project.yml +++ b/project.yml @@ -10,6 +10,7 @@ settings: LOCALIZATION_PREFERS_STRING_CATALOGS: true SWIFT_STRICT_CONCURRENCY: complete ASSETCATALOG_COMPILER_GENERATE_ASSET_SYMBOLS: false + OTHER_SWIFT_FLAGS: "-enable-experimental-feature IsolatedAny -enable-upcoming-feature StrictConcurrency" options: transitivelyLinkDependencies: true packages: @@ -23,7 +24,6 @@ targets: deploymentTarget: "13.0" settings: PRODUCT_BUNDLE_IDENTIFIER: com.processout.core-ui - OTHER_SWIFT_FLAGS: "-enable-experimental-feature IsolatedAny" TARGET_ROOT: $(PROJECT_DIR)/Sources/ProcessOutCoreUI preBuildScripts: - path: Scripts/Lint.sh @@ -57,7 +57,7 @@ targets: PRODUCT_BUNDLE_IDENTIFIER: com.processout.processout-ios MARKETING_VERSION: ${CURRENT_VERSION} CURRENT_PROJECT_VERSION: 1 - OTHER_SWIFT_FLAGS: "-Xfrontend -module-interface-preserve-types-as-written -enable-experimental-feature IsolatedAny" + OTHER_SWIFT_FLAGS: "$(inherited) -Xfrontend -module-interface-preserve-types-as-written" TARGET_ROOT: $(PROJECT_DIR)/Sources/ProcessOut preBuildScripts: - path: Scripts/Lint.sh From f794aa7eb4a66006a22dfb373df220780a6ba2e3 Mon Sep 17 00:00:00 2001 From: Andrii Vysotskyi Date: Thu, 8 Aug 2024 14:19:01 +0200 Subject: [PATCH 24/28] Temporarily disable CKO target tests --- Scripts/Test.sh | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Scripts/Test.sh b/Scripts/Test.sh index dc5d50b12..8c2780eac 100755 --- a/Scripts/Test.sh +++ b/Scripts/Test.sh @@ -5,8 +5,11 @@ set -euo pipefail PROJECT='ProcessOut.xcodeproj' DESTINATION=$(./Scripts/TestDestination.swift) +# todo(andrii-vysotskyi): reenable "ProcessOutCheckout3DS" tests +# when Swift6 compatibility is fixed + # Run Tests -for PRODUCT in "ProcessOut" "ProcessOutUI" "ProcessOutCheckout3DS"; do +for PRODUCT in "ProcessOut" "ProcessOutUI"; do xcodebuild clean test \ -destination "$DESTINATION" \ -project $PROJECT \ From 7676c2334604853b3336f1f4de7d006405697c96 Mon Sep 17 00:00:00 2001 From: Andrii Vysotskyi Date: Thu, 8 Aug 2024 15:10:12 +0200 Subject: [PATCH 25/28] Resolve build issue --- .../POPassKitPaymentAuthorizationController.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ProcessOutUI/Sources/Modules/PassKitPaymentAuthorization/POPassKitPaymentAuthorizationController.swift b/Sources/ProcessOutUI/Sources/Modules/PassKitPaymentAuthorization/POPassKitPaymentAuthorizationController.swift index 3acc4cb55..d89344311 100644 --- a/Sources/ProcessOutUI/Sources/Modules/PassKitPaymentAuthorization/POPassKitPaymentAuthorizationController.swift +++ b/Sources/ProcessOutUI/Sources/Modules/PassKitPaymentAuthorization/POPassKitPaymentAuthorizationController.swift @@ -59,7 +59,7 @@ public final class POPassKitPaymentAuthorizationController: NSObject { /// Presents the payment sheet modally over your app. public func present() async -> Bool { await withUnsafeContinuation { continuation in - present(completion: continuation.resume) + present { continuation.resume(returning: $0) } } } From b172ec8d58278d59a73151443078091f35031d48 Mon Sep 17 00:00:00 2001 From: Andrii Vysotskyi Date: Thu, 8 Aug 2024 17:00:37 +0200 Subject: [PATCH 26/28] Update dependencies --- Package.swift | 17 +++-------------- ProcessOut.podspec | 5 ++--- ProcessOutCheckout3DS.podspec | 4 ++-- ProcessOutCoreUI.podspec | 4 ++-- ProcessOutUI.podspec | 2 +- .../Sources/Core/Markdown/MarkdownParser.swift | 1 - project.yml | 11 ++++------- 7 files changed, 14 insertions(+), 30 deletions(-) diff --git a/Package.swift b/Package.swift index 4c5ebd5a3..e37d94c12 100644 --- a/Package.swift +++ b/Package.swift @@ -2,11 +2,6 @@ import PackageDescription -let swiftSettings: [SwiftSetting] = [ - .enableExperimentalFeature("IsolatedAny"), - .enableUpcomingFeature("StrictConcurrency") -] - let package = Package( name: "ProcessOut", defaultLocalization: "en", @@ -25,13 +20,9 @@ let package = Package( targets: [ .target( name: "ProcessOut", - dependencies: [ - .target(name: "cmark") - ], resources: [ .process("Resources") - ], - swiftSettings: swiftSettings + ] ), .target( name: "ProcessOutCheckout3DS", @@ -50,8 +41,7 @@ let package = Package( ], resources: [ .process("Resources") - ], - swiftSettings: swiftSettings + ] ), .target( name: "ProcessOutCoreUI", @@ -60,8 +50,7 @@ let package = Package( ], resources: [ .process("Resources") - ], - swiftSettings: swiftSettings + ] ), .binaryTarget(name: "cmark", path: "Vendor/cmark.xcframework") ] diff --git a/ProcessOut.podspec b/ProcessOut.podspec index 31ece0de1..ac0eea650 100644 --- a/ProcessOut.podspec +++ b/ProcessOut.podspec @@ -1,15 +1,14 @@ Pod::Spec.new do |s| s.name = 'ProcessOut' s.version = '4.19.0' - s.swift_versions = ['5.9'] + s.swift_versions = ['6.0'] s.license = { :type => 'MIT', :file => 'LICENSE' } s.homepage = 'https://github.com/processout/processout-ios' s.author = 'ProcessOut' s.summary = 'The smart router for payments. Smartly route each transaction to the relevant payment providers.' s.source = { :git => 'https://github.com/processout/processout-ios.git', :tag => s.version.to_s } s.frameworks = 'Foundation', 'UIKit' - s.ios.deployment_target = '13.0' - s.vendored_frameworks = "Vendor/cmark.xcframework" + s.ios.deployment_target = '14.0' s.ios.resources = 'Sources/ProcessOut/Resources/**/*' s.source_files = 'Sources/ProcessOut/**/*.swift' s.pod_target_xcconfig = { 'OTHER_SWIFT_FLAGS' => '-Xfrontend -module-interface-preserve-types-as-written' } diff --git a/ProcessOutCheckout3DS.podspec b/ProcessOutCheckout3DS.podspec index 302ab1da0..658e9f47b 100644 --- a/ProcessOutCheckout3DS.podspec +++ b/ProcessOutCheckout3DS.podspec @@ -1,14 +1,14 @@ Pod::Spec.new do |s| s.name = 'ProcessOutCheckout3DS' s.version = '4.19.0' - s.swift_versions = ['5.9'] + s.swift_versions = ['6.0'] s.license = { :type => 'MIT', :file => 'LICENSE' } s.homepage = 'https://github.com/processout/processout-ios' s.author = 'ProcessOut' s.summary = 'Integration with Checkout.com 3D Secure (3DS) mobile SDK.' s.source = { :git => 'https://github.com/processout/processout-ios.git', :tag => s.version.to_s } s.frameworks = 'Foundation' - s.ios.deployment_target = '13.0' + s.ios.deployment_target = '14.0' s.ios.resources = 'Sources/ProcessOutCheckout3DS/Resources/**/*' s.source_files = 'Sources/ProcessOutCheckout3DS/**/*.swift' s.pod_target_xcconfig = { 'EXCLUDED_ARCHS' => 'x86_64' } diff --git a/ProcessOutCoreUI.podspec b/ProcessOutCoreUI.podspec index 139911ccc..f97e99b90 100644 --- a/ProcessOutCoreUI.podspec +++ b/ProcessOutCoreUI.podspec @@ -1,15 +1,15 @@ Pod::Spec.new do |s| s.name = 'ProcessOutCoreUI' s.version = '4.19.0' - s.swift_versions = ['5.9'] + s.swift_versions = ['6.0'] s.license = { :type => 'MIT', :file => 'LICENSE' } s.homepage = 'https://github.com/processout/processout-ios' s.author = 'ProcessOut' s.summary = 'Reusable UI components and logic. Pod is meant to be used only with other ProcessOut pods.' s.source = { :git => 'https://github.com/processout/processout-ios.git', :tag => s.version.to_s } s.frameworks = 'Foundation', 'SwiftUI' + s.vendored_frameworks = "Vendor/cmark.xcframework" s.ios.deployment_target = '14.0' s.ios.resources = 'Sources/ProcessOutCoreUI/Resources/**/*' s.source_files = 'Sources/ProcessOutCoreUI/**/*.swift' - s.dependency 'ProcessOut' # todo(andrii-vysotskyi): vendor cmark.xcframework instead after UI migration is completed end diff --git a/ProcessOutUI.podspec b/ProcessOutUI.podspec index 5226d9460..1b762cc0e 100644 --- a/ProcessOutUI.podspec +++ b/ProcessOutUI.podspec @@ -1,7 +1,7 @@ Pod::Spec.new do |s| s.name = 'ProcessOutUI' s.version = '4.19.0' - s.swift_versions = ['5.9'] + s.swift_versions = ['6.0'] s.license = { :type => 'MIT', :file => 'LICENSE' } s.homepage = 'https://github.com/processout/processout-ios' s.author = 'ProcessOut' diff --git a/Sources/ProcessOut/Sources/Core/Markdown/MarkdownParser.swift b/Sources/ProcessOut/Sources/Core/Markdown/MarkdownParser.swift index bace98f06..cfad9257d 100644 --- a/Sources/ProcessOut/Sources/Core/Markdown/MarkdownParser.swift +++ b/Sources/ProcessOut/Sources/Core/Markdown/MarkdownParser.swift @@ -6,7 +6,6 @@ // import Foundation -@_implementationOnly import cmark enum MarkdownParser { diff --git a/project.yml b/project.yml index 681d64431..bfa073813 100644 --- a/project.yml +++ b/project.yml @@ -10,7 +10,6 @@ settings: LOCALIZATION_PREFERS_STRING_CATALOGS: true SWIFT_STRICT_CONCURRENCY: complete ASSETCATALOG_COMPILER_GENERATE_ASSET_SYMBOLS: false - OTHER_SWIFT_FLAGS: "-enable-experimental-feature IsolatedAny -enable-upcoming-feature StrictConcurrency" options: transitivelyLinkDependencies: true packages: @@ -21,7 +20,7 @@ targets: ProcessOutCoreUI: type: framework platform: iOS - deploymentTarget: "13.0" + deploymentTarget: "14.0" settings: PRODUCT_BUNDLE_IDENTIFIER: com.processout.core-ui TARGET_ROOT: $(PROJECT_DIR)/Sources/ProcessOutCoreUI @@ -36,7 +35,7 @@ targets: ProcessOutUI: type: framework platform: iOS - deploymentTarget: "13.0" + deploymentTarget: "14.0" settings: PRODUCT_BUNDLE_IDENTIFIER: com.processout.ui TARGET_ROOT: $(PROJECT_DIR)/Sources/ProcessOutUI @@ -52,7 +51,7 @@ targets: ProcessOut: type: framework platform: iOS - deploymentTarget: "13.0" + deploymentTarget: "14.0" settings: PRODUCT_BUNDLE_IDENTIFIER: com.processout.processout-ios MARKETING_VERSION: ${CURRENT_VERSION} @@ -70,8 +69,6 @@ targets: - path: Sources/ProcessOut excludes: - swiftgen.yml - dependencies: - - framework: Vendor/cmark.xcframework ProcessOutTests: type: bundle.unit-test platform: iOS @@ -111,7 +108,7 @@ targets: ProcessOutCheckout3DS: type: framework platform: iOS - deploymentTarget: "13.0" + deploymentTarget: "14.0" settings: PRODUCT_BUNDLE_IDENTIFIER: com.processout.checkout-3ds-ios EXCLUDED_ARCHS: x86_64 # Checkout3DS doesn't support x86_64 so are we From dab098a52c1dcd02f6fb7f1795d9b77d76fbaa72 Mon Sep 17 00:00:00 2001 From: Andrii Vysotskyi Date: Thu, 8 Aug 2024 17:00:54 +0200 Subject: [PATCH 27/28] Resolve warning --- .../Core/DeviceMetadata/DefaultDeviceMetadataProvider.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ProcessOut/Sources/Core/DeviceMetadata/DefaultDeviceMetadataProvider.swift b/Sources/ProcessOut/Sources/Core/DeviceMetadata/DefaultDeviceMetadataProvider.swift index f42464c3f..bc58aacd1 100644 --- a/Sources/ProcessOut/Sources/Core/DeviceMetadata/DefaultDeviceMetadataProvider.swift +++ b/Sources/ProcessOut/Sources/Core/DeviceMetadata/DefaultDeviceMetadataProvider.swift @@ -54,7 +54,7 @@ actor DefaultDeviceMetadataProvider: DeviceMetadataProvider { let description = withUnsafePointer(to: &systemInfo.machine) { pointer in let capacity = Int(_SYS_NAMELEN) return pointer.withMemoryRebound(to: CChar.self, capacity: capacity) { charPointer in - String(validatingUTF8: charPointer) + String(validatingCString: charPointer) } } return description From ba8da0fdb1c39c0308b95b60643c0af504015c46 Mon Sep 17 00:00:00 2001 From: Andrii Vysotskyi Date: Thu, 8 Aug 2024 17:10:37 +0200 Subject: [PATCH 28/28] Fix build issues --- Example/Example/Sources/Application/AppDelegate.swift | 2 +- .../View/Sections/NativeAlternativePaymentContentView.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Example/Example/Sources/Application/AppDelegate.swift b/Example/Example/Sources/Application/AppDelegate.swift index a3157eecd..e052ec712 100644 --- a/Example/Example/Sources/Application/AppDelegate.swift +++ b/Example/Example/Sources/Application/AppDelegate.swift @@ -36,7 +36,7 @@ final class AppDelegate: UIResponder, UIApplicationDelegate { // Please note that implementation is using factory method (part of private interface) that creates // configuration with private key. It is only done for demonstration/testing purposes to avoid setting // up test server and shouldn't be shipped with production code. - let configuration = ProcessOutConfiguration.production( + let configuration = ProcessOutConfiguration( projectId: Constants.projectId, privateKey: Constants.projectPrivateKey ) ProcessOut.configure(configuration: configuration) diff --git a/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/View/Sections/NativeAlternativePaymentContentView.swift b/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/View/Sections/NativeAlternativePaymentContentView.swift index 51fa62817..5a11f0abb 100644 --- a/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/View/Sections/NativeAlternativePaymentContentView.swift +++ b/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/View/Sections/NativeAlternativePaymentContentView.swift @@ -72,7 +72,7 @@ struct NativeAlternativePaymentContentView: View { return (top: sections, center: []) } let index = sections.firstIndex { section in - section.items.contains(where: shouldCenter) + section.items.contains { shouldCenter(item: $0) } } guard let index else { return (top: sections, center: [])