From f805e1c89195e72bea13f09668359c451ef77b14 Mon Sep 17 00:00:00 2001 From: Andrii Vysotskyi Date: Fri, 24 Apr 2026 16:06:00 +0200 Subject: [PATCH 01/13] Refactor interactor --- ...eAlternativePaymentDefaultInteractor.swift | 98 ++++++++----------- 1 file changed, 40 insertions(+), 58 deletions(-) diff --git a/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/Interactor/NativeAlternativePaymentDefaultInteractor.swift b/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/Interactor/NativeAlternativePaymentDefaultInteractor.swift index d15dcc90c..b6e41ff41 100644 --- a/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/Interactor/NativeAlternativePaymentDefaultInteractor.swift +++ b/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/Interactor/NativeAlternativePaymentDefaultInteractor.swift @@ -142,29 +142,7 @@ final class NativeAlternativePaymentDefaultInteractor: } let task = Task { do { - let didOpenUrl: Bool - switch currentState.redirect.type { - case .deepLink: - didOpenUrl = await openDeepLink(url: currentState.redirect.url) - case .web: - let authenticationRequest = POAlternativePaymentAuthenticationRequest( - url: currentState.redirect.url, - callback: configuration.redirect.callback, - prefersEphemeralSession: configuration.redirect.prefersEphemeralSession - ) - _ = try await alternativePaymentsService.authenticate(request: authenticationRequest) - didOpenUrl = true - default: - throw POFailure(errorDescription: "Unknown redirect type.", code: .Mobile.internal) - } - let response = try await serviceAdapter.continuePayment( - with: .init( - flow: configuration.flow, - redirect: currentState.redirect.confirmationRequired ? .init(success: didOpenUrl) : nil, - localeIdentifier: configuration.localization.localeOverride?.identifier - ) - ) - try await setState(with: response) + try await uncheckedRedirect(to: currentState.redirect) } catch { setFailureState(error: error) } @@ -237,29 +215,7 @@ final class NativeAlternativePaymentDefaultInteractor: logger.error("Attempted to handle headless redirect while not in starting state. Ignoring.") return } - let didOpenUrl: Bool - switch redirect.type { - case .deepLink: - didOpenUrl = await openDeepLink(url: redirect.url) - case .web: - let authenticationRequest = POAlternativePaymentAuthenticationRequest( - url: redirect.url, - callback: configuration.redirect.callback, - prefersEphemeralSession: configuration.redirect.prefersEphemeralSession - ) - _ = try await alternativePaymentsService.authenticate(request: authenticationRequest) - didOpenUrl = true - default: - throw POFailure(errorDescription: "Unknown redirect type.", code: .Mobile.internal) - } - let response = try await serviceAdapter.continuePayment( - with: .init( - flow: configuration.flow, - redirect: redirect.confirmationRequired ? .init(success: didOpenUrl) : nil, - localeIdentifier: configuration.localization.localeOverride?.identifier - ) - ) - try await setState(with: response) + try await uncheckedRedirect(to: redirect) } // MARK: - Started State @@ -326,6 +282,44 @@ final class NativeAlternativePaymentDefaultInteractor: private var cancelationEnablingTask: Task? + // MARK: - Redirecting State + + private func uncheckedRedirect(to redirect: PONativeAlternativePaymentRedirectV2) async throws { + let didOpenUrl: Bool + switch redirect.type { + case .deepLink: + didOpenUrl = await openDeepLink(url: redirect.url) + case .web: + let authenticationRequest = POAlternativePaymentAuthenticationRequest( + url: redirect.url, + callback: configuration.redirect.callback, + prefersEphemeralSession: configuration.redirect.prefersEphemeralSession + ) + _ = try await alternativePaymentsService.authenticate(request: authenticationRequest) + didOpenUrl = true + default: + throw POFailure(errorDescription: "Unknown redirect type.", code: .Mobile.internal) + } + let response = try await serviceAdapter.continuePayment( + with: .init( + flow: configuration.flow, + redirect: redirect.confirmationRequired ? .init(success: didOpenUrl) : nil, + localeIdentifier: configuration.localization.localeOverride?.identifier + ) + ) + try await setState(with: response) + } + + private func openDeepLink(url: URL) async -> Bool { + let options: [UIApplication.OpenExternalURLOptionsKey: Any] + if url.scheme == "https" || url.scheme == "http" { // Determines whether link could be universal + options = [.universalLinksOnly: true] + } else { + options = [:] + } + return await UIApplication.shared.open(url, options: options) + } + // MARK: - Awaiting Completion State private func setAwaitingCompletionState(response: NativeAlternativePaymentServiceAdapterResponse) async { @@ -935,18 +929,6 @@ final class NativeAlternativePaymentDefaultInteractor: return nil } - // MARK: - Redirect Utils - - private func openDeepLink(url: URL) async -> Bool { - let options: [UIApplication.OpenExternalURLOptionsKey: Any] - if url.scheme == "https" || url.scheme == "http" { // Determines whether link could be universal - options = [.universalLinksOnly: true] - } else { - options = [:] - } - return await UIApplication.shared.open(url, options: options) - } - // MARK: - External Events private func observeEvents() { From 7a3a5c4b61fd04a5c72c55ed402700aece5e85d6 Mon Sep 17 00:00:00 2001 From: Andrii Vysotskyi Date: Fri, 24 Apr 2026 16:11:57 +0200 Subject: [PATCH 02/13] Expose web authentication session --- Sources/ProcessOut/Sources/Api/ProcessOut.swift | 10 +++++++--- .../DefaultAlternativePaymentsService.swift | 4 ++-- .../DefaultCustomerActionsService.swift | 4 ++-- .../DefaultWebAuthenticationSession.swift | 8 +++++--- ...est.swift => POWebAuthenticationRequest.swift} | 11 ++++++----- .../POWebAuthenticationSession.swift | 15 +++++++++++++++ ...rottledWebAuthenticationSessionDecorator.swift | 8 ++++---- .../WebAuthenticationSession.swift | 14 -------------- 8 files changed, 41 insertions(+), 33 deletions(-) rename Sources/ProcessOut/Sources/Sessions/WebAuthentication/{WebAuthenticationRequest.swift => POWebAuthenticationRequest.swift} (57%) create mode 100644 Sources/ProcessOut/Sources/Sessions/WebAuthentication/POWebAuthenticationSession.swift delete mode 100644 Sources/ProcessOut/Sources/Sessions/WebAuthentication/WebAuthenticationSession.swift diff --git a/Sources/ProcessOut/Sources/Api/ProcessOut.swift b/Sources/ProcessOut/Sources/Api/ProcessOut.swift index 1e293fb54..d7138dc64 100644 --- a/Sources/ProcessOut/Sources/Api/ProcessOut.swift +++ b/Sources/ProcessOut/Sources/Api/ProcessOut.swift @@ -66,6 +66,10 @@ public final class ProcessOut: @unchecked Sendable { @_spi(PO) public let eventEmitter: POEventEmitter + /// Web authentication session. + @_spi(PO) + public let webAuthenticationSession: POWebAuthenticationSession + @_spi(PO) public func replace(configuration newConfiguration: ProcessOutConfiguration) { _configuration.withLock { configuration in @@ -135,7 +139,7 @@ public final class ProcessOut: @unchecked Sendable { logger: connectorLogger ) eventEmitter = LocalEventEmitter(logger: serviceLogger) - let webAuthenticationSession = ThrottledWebAuthenticationSessionDecorator( + webAuthenticationSession = ThrottledWebAuthenticationSessionDecorator( session: DefaultWebAuthenticationSession(eventEmitter: eventEmitter) ) let customerActionsService = Self.createCustomerActionsService( @@ -189,7 +193,7 @@ public final class ProcessOut: @unchecked Sendable { private static func createAlternativePaymentsService( configuration: ProcessOutConfiguration, - webAuthenticationSession webSession: WebAuthenticationSession, + webAuthenticationSession webSession: POWebAuthenticationSession, logger: POLogger ) -> DefaultAlternativePaymentsService { let serviceConfiguration = Self.alternativePaymentsConfiguration(with: configuration) @@ -234,7 +238,7 @@ public final class ProcessOut: @unchecked Sendable { } private static func createCustomerActionsService( - webAuthenticationSession webSession: WebAuthenticationSession, + webAuthenticationSession webSession: POWebAuthenticationSession, logger: POLogger ) -> CustomerActionsService { let decoder = JSONDecoder(), encoder = JSONEncoder() diff --git a/Sources/ProcessOut/Sources/Services/AlternativePayments/DefaultAlternativePaymentsService.swift b/Sources/ProcessOut/Sources/Services/AlternativePayments/DefaultAlternativePaymentsService.swift index b7d43e5b8..0b4b30ab5 100644 --- a/Sources/ProcessOut/Sources/Services/AlternativePayments/DefaultAlternativePaymentsService.swift +++ b/Sources/ProcessOut/Sources/Services/AlternativePayments/DefaultAlternativePaymentsService.swift @@ -11,7 +11,7 @@ final class DefaultAlternativePaymentsService: POAlternativePaymentsService { init( configuration: AlternativePaymentsServiceConfiguration, - webSession: WebAuthenticationSession, + webSession: POWebAuthenticationSession, logger: POLogger ) { self.configuration = .init(wrappedValue: configuration) @@ -130,7 +130,7 @@ final class DefaultAlternativePaymentsService: POAlternativePaymentsService { private let configuration: POUnfairlyLocked private let logger: POLogger - private let webSession: WebAuthenticationSession + private let webSession: POWebAuthenticationSession // MARK: - Request diff --git a/Sources/ProcessOut/Sources/Services/CustomerActions/DefaultCustomerActionsService.swift b/Sources/ProcessOut/Sources/Services/CustomerActions/DefaultCustomerActionsService.swift index ee662be6a..4936276fc 100644 --- a/Sources/ProcessOut/Sources/Services/CustomerActions/DefaultCustomerActionsService.swift +++ b/Sources/ProcessOut/Sources/Services/CustomerActions/DefaultCustomerActionsService.swift @@ -13,7 +13,7 @@ final class DefaultCustomerActionsService: CustomerActionsService { decoder: JSONDecoder, encoder: JSONEncoder, jsonWritingOptions: JSONSerialization.WritingOptions = [], - webSession: WebAuthenticationSession, + webSession: POWebAuthenticationSession, logger: POLogger ) { self.decoder = decoder @@ -80,7 +80,7 @@ final class DefaultCustomerActionsService: CustomerActionsService { private let decoder: JSONDecoder private let encoder: JSONEncoder private let jsonWritingOptions: JSONSerialization.WritingOptions - private let webSession: WebAuthenticationSession + private let webSession: POWebAuthenticationSession private let logger: POLogger private let semaphore: AsyncSemaphore diff --git a/Sources/ProcessOut/Sources/Sessions/WebAuthentication/DefaultWebAuthenticationSession.swift b/Sources/ProcessOut/Sources/Sessions/WebAuthentication/DefaultWebAuthenticationSession.swift index 4c8e9c8cf..e65026309 100644 --- a/Sources/ProcessOut/Sources/Sessions/WebAuthentication/DefaultWebAuthenticationSession.swift +++ b/Sources/ProcessOut/Sources/Sessions/WebAuthentication/DefaultWebAuthenticationSession.swift @@ -9,7 +9,7 @@ import AuthenticationServices @MainActor final class DefaultWebAuthenticationSession: - NSObject, WebAuthenticationSession, ASWebAuthenticationPresentationContextProviding { + NSObject, POWebAuthenticationSession, ASWebAuthenticationPresentationContextProviding { nonisolated init(eventEmitter: POEventEmitter) { self.eventEmitter = eventEmitter @@ -18,7 +18,7 @@ final class DefaultWebAuthenticationSession: // MARK: - WebAuthenticationSession - func authenticate(using request: WebAuthenticationRequest) async throws -> URL { + func authenticate(using request: POWebAuthenticationRequest) async throws -> URL { let operationProxy = WebAuthenticationOperationProxy( callback: request.callback, eventEmitter: eventEmitter ) @@ -75,7 +75,9 @@ final class DefaultWebAuthenticationSession: // MARK: - Private Methods private static func createAuthenticationSession( - with request: WebAuthenticationRequest, redirectUrl: URL, completion: @escaping (Result) -> Void + with request: POWebAuthenticationRequest, + redirectUrl: URL, + completion: @escaping (Result) -> Void ) -> ASWebAuthenticationSession { let completionHandler = { (url: URL?, error: Error?) in if let url { diff --git a/Sources/ProcessOut/Sources/Sessions/WebAuthentication/WebAuthenticationRequest.swift b/Sources/ProcessOut/Sources/Sessions/WebAuthentication/POWebAuthenticationRequest.swift similarity index 57% rename from Sources/ProcessOut/Sources/Sessions/WebAuthentication/WebAuthenticationRequest.swift rename to Sources/ProcessOut/Sources/Sessions/WebAuthentication/POWebAuthenticationRequest.swift index 5e6d2d39e..171fe34d2 100644 --- a/Sources/ProcessOut/Sources/Sessions/WebAuthentication/WebAuthenticationRequest.swift +++ b/Sources/ProcessOut/Sources/Sessions/WebAuthentication/POWebAuthenticationRequest.swift @@ -1,5 +1,5 @@ // -// WebAuthenticationRequest.swift +// POWebAuthenticationRequest.swift // ProcessOut // // Created by Andrii Vysotskyi on 31.10.2024. @@ -7,15 +7,16 @@ import Foundation -struct WebAuthenticationRequest: Sendable { +@_spi(PO) +public struct POWebAuthenticationRequest: Sendable { /// A URL pointing to the authentication webpage. - let url: URL + public let url: URL /// Callback. - let callback: POWebAuthenticationCallback? + public let callback: POWebAuthenticationCallback? /// A boolean value that indicates whether the session should ask the browser for /// a private authentication session. - let prefersEphemeralSession: Bool + public let prefersEphemeralSession: Bool } diff --git a/Sources/ProcessOut/Sources/Sessions/WebAuthentication/POWebAuthenticationSession.swift b/Sources/ProcessOut/Sources/Sessions/WebAuthentication/POWebAuthenticationSession.swift new file mode 100644 index 000000000..631396be6 --- /dev/null +++ b/Sources/ProcessOut/Sources/Sessions/WebAuthentication/POWebAuthenticationSession.swift @@ -0,0 +1,15 @@ +// +// POWebAuthenticationSession.swift +// ProcessOut +// +// Created by Andrii Vysotskyi on 01.08.2024. +// + +import Foundation + +@_spi(PO) +public protocol POWebAuthenticationSession: Sendable { + + /// Begins a web authentication session. + func authenticate(using request: POWebAuthenticationRequest) async throws -> URL +} diff --git a/Sources/ProcessOut/Sources/Sessions/WebAuthentication/ThrottledWebAuthenticationSessionDecorator.swift b/Sources/ProcessOut/Sources/Sessions/WebAuthentication/ThrottledWebAuthenticationSessionDecorator.swift index 9bd2146f3..7a89e69e2 100644 --- a/Sources/ProcessOut/Sources/Sessions/WebAuthentication/ThrottledWebAuthenticationSessionDecorator.swift +++ b/Sources/ProcessOut/Sources/Sessions/WebAuthentication/ThrottledWebAuthenticationSessionDecorator.swift @@ -7,16 +7,16 @@ import Foundation -actor ThrottledWebAuthenticationSessionDecorator: WebAuthenticationSession { +actor ThrottledWebAuthenticationSessionDecorator: POWebAuthenticationSession { - init(session: WebAuthenticationSession) { + init(session: POWebAuthenticationSession) { self.session = session semaphore = AsyncSemaphore(value: 1) } // MARK: - WebAuthenticationSession - func authenticate(using request: WebAuthenticationRequest) async throws -> URL { + func authenticate(using request: POWebAuthenticationRequest) async throws -> URL { try await semaphore.waitUnlessCancelled( cancellationError: POFailure(message: "Authentication session was cancelled.", code: .Mobile.cancelled) ) @@ -30,7 +30,7 @@ actor ThrottledWebAuthenticationSessionDecorator: WebAuthenticationSession { // MARK: - Private Properties - private let session: WebAuthenticationSession + private let session: POWebAuthenticationSession private let semaphore: AsyncSemaphore private var lastAuthenticationTime: DispatchTime? diff --git a/Sources/ProcessOut/Sources/Sessions/WebAuthentication/WebAuthenticationSession.swift b/Sources/ProcessOut/Sources/Sessions/WebAuthentication/WebAuthenticationSession.swift deleted file mode 100644 index 8a3df52fe..000000000 --- a/Sources/ProcessOut/Sources/Sessions/WebAuthentication/WebAuthenticationSession.swift +++ /dev/null @@ -1,14 +0,0 @@ -// -// WebAuthenticationSession.swift -// ProcessOut -// -// Created by Andrii Vysotskyi on 01.08.2024. -// - -import Foundation - -protocol WebAuthenticationSession: Sendable { - - /// Begins a web authentication session. - func authenticate(using request: WebAuthenticationRequest) async throws -> URL -} From a23cfbecac27f3b619b9c499629d19bbad7ea7a0 Mon Sep 17 00:00:00 2001 From: Andrii Vysotskyi Date: Sat, 25 Apr 2026 11:20:22 +0200 Subject: [PATCH 03/13] Support web redirect result resolution --- ...veAlternativePaymentRedirectResultV2.swift | 28 +++++++++++++++++-- .../POWebAuthenticationRequest.swift | 6 ++++ ...eckoutInteractorDefaultChildProvider.swift | 2 +- .../PONativeAlternativePaymentComponent.swift | 2 +- ...eAlternativePaymentDefaultInteractor.swift | 19 +++++++------ 5 files changed, 44 insertions(+), 13 deletions(-) diff --git a/Sources/ProcessOut/Sources/Repositories/Shared/Requests/AlternativePaymentV2/PONativeAlternativePaymentRedirectResultV2.swift b/Sources/ProcessOut/Sources/Repositories/Shared/Requests/AlternativePaymentV2/PONativeAlternativePaymentRedirectResultV2.swift index ca0ac8433..eaabe85fd 100644 --- a/Sources/ProcessOut/Sources/Repositories/Shared/Requests/AlternativePaymentV2/PONativeAlternativePaymentRedirectResultV2.swift +++ b/Sources/ProcessOut/Sources/Repositories/Shared/Requests/AlternativePaymentV2/PONativeAlternativePaymentRedirectResultV2.swift @@ -5,12 +5,36 @@ // Created by Andrii Vysotskyi on 23.12.2025. // +import Foundation + public struct PONativeAlternativePaymentRedirectResultV2: Sendable, Encodable { - public init(success: Bool) { - self.success = success + @_spi(PO) + public struct Result: Sendable, Encodable { + + /// Result URL. + public let url: URL + + public init(url: URL) { + self.url = url + } } /// Indicates whether customer was redirected successfully. public let success: Bool + + /// Redirect result. + @_spi(PO) + public let result: Result? + + @_spi(PO) + public init(success: Bool, result: Result?) { + self.success = success + self.result = result + } + + public init(success: Bool) { + self.success = success + self.result = nil + } } diff --git a/Sources/ProcessOut/Sources/Sessions/WebAuthentication/POWebAuthenticationRequest.swift b/Sources/ProcessOut/Sources/Sessions/WebAuthentication/POWebAuthenticationRequest.swift index 171fe34d2..4298a8d62 100644 --- a/Sources/ProcessOut/Sources/Sessions/WebAuthentication/POWebAuthenticationRequest.swift +++ b/Sources/ProcessOut/Sources/Sessions/WebAuthentication/POWebAuthenticationRequest.swift @@ -19,4 +19,10 @@ public struct POWebAuthenticationRequest: Sendable { /// A boolean value that indicates whether the session should ask the browser for /// a private authentication session. public let prefersEphemeralSession: Bool + + public init(url: URL, callback: POWebAuthenticationCallback?, prefersEphemeralSession: Bool) { + self.url = url + self.callback = callback + self.prefersEphemeralSession = prefersEphemeralSession + } } diff --git a/Sources/ProcessOutUI/Sources/Modules/DynamicCheckout/Utils/ChildProvider/DynamicCheckoutInteractorDefaultChildProvider.swift b/Sources/ProcessOutUI/Sources/Modules/DynamicCheckout/Utils/ChildProvider/DynamicCheckoutInteractorDefaultChildProvider.swift index 52d7940d0..e221ae16d 100644 --- a/Sources/ProcessOutUI/Sources/Modules/DynamicCheckout/Utils/ChildProvider/DynamicCheckoutInteractorDefaultChildProvider.swift +++ b/Sources/ProcessOutUI/Sources/Modules/DynamicCheckout/Utils/ChildProvider/DynamicCheckoutInteractorDefaultChildProvider.swift @@ -60,7 +60,7 @@ final class DynamicCheckoutInteractorDefaultChildProvider: DynamicCheckoutIntera tokensService: ProcessOut.shared.customerTokens, paymentConfirmationTimeout: configuration.paymentConfirmation.timeout ), - alternativePaymentsService: ProcessOut.shared.alternativePayments, + webAuthenticationSession: ProcessOut.shared.webAuthenticationSession, imagesRepository: imagesRepository, barcodeImageProvider: DefaultBarcodeImageProvider(logger: logger), eventEmitter: ProcessOut.shared.eventEmitter, diff --git a/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/Component/PONativeAlternativePaymentComponent.swift b/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/Component/PONativeAlternativePaymentComponent.swift index 1e392776e..a838ab825 100644 --- a/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/Component/PONativeAlternativePaymentComponent.swift +++ b/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/Component/PONativeAlternativePaymentComponent.swift @@ -34,7 +34,7 @@ public final class PONativeAlternativePaymentComponent { tokensService: ProcessOut.shared.customerTokens, paymentConfirmationTimeout: configuration.paymentConfirmation.timeout ), - alternativePaymentsService: ProcessOut.shared.alternativePayments, + webAuthenticationSession: ProcessOut.shared.webAuthenticationSession, imagesRepository: ProcessOut.shared.images, barcodeImageProvider: DefaultBarcodeImageProvider(logger: logger), eventEmitter: ProcessOut.shared.eventEmitter, diff --git a/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/Interactor/NativeAlternativePaymentDefaultInteractor.swift b/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/Interactor/NativeAlternativePaymentDefaultInteractor.swift index b6e41ff41..1e69c1cb4 100644 --- a/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/Interactor/NativeAlternativePaymentDefaultInteractor.swift +++ b/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/Interactor/NativeAlternativePaymentDefaultInteractor.swift @@ -18,7 +18,7 @@ final class NativeAlternativePaymentDefaultInteractor: init( configuration: PONativeAlternativePaymentConfiguration, serviceAdapter: NativeAlternativePaymentServiceAdapter, - alternativePaymentsService: POAlternativePaymentsService, + webAuthenticationSession: ProcessOut::POWebAuthenticationSession, imagesRepository: POImagesRepository, barcodeImageProvider: BarcodeImageProvider, eventEmitter: POEventEmitter, @@ -27,7 +27,7 @@ final class NativeAlternativePaymentDefaultInteractor: ) { self.configuration = configuration self.serviceAdapter = serviceAdapter - self.alternativePaymentsService = alternativePaymentsService + self.webAuthenticationSession = webAuthenticationSession self.imagesRepository = imagesRepository self.barcodeImageProvider = barcodeImageProvider self.eventEmitter = eventEmitter @@ -178,7 +178,7 @@ final class NativeAlternativePaymentDefaultInteractor: // MARK: - Private Properties private let serviceAdapter: NativeAlternativePaymentServiceAdapter - private let alternativePaymentsService: POAlternativePaymentsService + private let webAuthenticationSession: ProcessOut::POWebAuthenticationSession private let imagesRepository: POImagesRepository private let barcodeImageProvider: BarcodeImageProvider private let eventEmitter: POEventEmitter @@ -285,25 +285,26 @@ final class NativeAlternativePaymentDefaultInteractor: // MARK: - Redirecting State private func uncheckedRedirect(to redirect: PONativeAlternativePaymentRedirectV2) async throws { - let didOpenUrl: Bool + let redirectResult: PONativeAlternativePaymentRedirectResultV2 switch redirect.type { case .deepLink: - didOpenUrl = await openDeepLink(url: redirect.url) + let didOpenUrl = await openDeepLink(url: redirect.url) + redirectResult = .init(success: didOpenUrl) case .web: - let authenticationRequest = POAlternativePaymentAuthenticationRequest( + let authenticationRequest = POWebAuthenticationRequest( url: redirect.url, callback: configuration.redirect.callback, prefersEphemeralSession: configuration.redirect.prefersEphemeralSession ) - _ = try await alternativePaymentsService.authenticate(request: authenticationRequest) - didOpenUrl = true + let returnUrl = try await webAuthenticationSession.authenticate(using: authenticationRequest) + redirectResult = .init(success: true, result: .init(url: returnUrl)) default: throw POFailure(errorDescription: "Unknown redirect type.", code: .Mobile.internal) } let response = try await serviceAdapter.continuePayment( with: .init( flow: configuration.flow, - redirect: redirect.confirmationRequired ? .init(success: didOpenUrl) : nil, + redirect: redirect.confirmationRequired ? redirectResult : nil, localeIdentifier: configuration.localization.localeOverride?.identifier ) ) From 9dcebe4903961efe27bee2504db2fac56b651faf Mon Sep 17 00:00:00 2001 From: Andrii Vysotskyi Date: Sun, 26 Apr 2026 14:25:23 +0200 Subject: [PATCH 04/13] Move deep link resolution to interactor --- ...rnativePaymentUrlResolutionRequestV2.swift | 8 ++ .../Invoices/DefaultInvoicesService.swift | 51 ++------- ...PaymentDeepLinkResolutionFailedEvent.swift | 18 ---- ...ernativePaymentDeepLinkResolvedEvent.swift | 13 --- .../Services/Invoices/POInvoicesService.swift | 13 +++ ...tiveAlternativePaymentServiceAdapter.swift | 6 ++ ...tiveAlternativePaymentServiceAdapter.swift | 6 ++ ...eAlternativePaymentDefaultInteractor.swift | 101 ++++++++++-------- 8 files changed, 94 insertions(+), 122 deletions(-) delete mode 100644 Sources/ProcessOut/Sources/Services/Invoices/Events/PONativeAlternativePaymentDeepLinkResolutionFailedEvent.swift delete mode 100644 Sources/ProcessOut/Sources/Services/Invoices/Events/PONativeAlternativePaymentDeepLinkResolvedEvent.swift diff --git a/Sources/ProcessOut/Sources/Repositories/Invoices/Requests/AlternativePaymentV2/PONativeAlternativePaymentUrlResolutionRequestV2.swift b/Sources/ProcessOut/Sources/Repositories/Invoices/Requests/AlternativePaymentV2/PONativeAlternativePaymentUrlResolutionRequestV2.swift index 2694229f0..5dcd75e08 100644 --- a/Sources/ProcessOut/Sources/Repositories/Invoices/Requests/AlternativePaymentV2/PONativeAlternativePaymentUrlResolutionRequestV2.swift +++ b/Sources/ProcessOut/Sources/Repositories/Invoices/Requests/AlternativePaymentV2/PONativeAlternativePaymentUrlResolutionRequestV2.swift @@ -16,10 +16,18 @@ public struct PONativeAlternativePaymentUrlResolutionRequestV2: Sendable, Encoda /// Result URL. public let url: URL + + public init(url: URL) { + self.url = url + } } /// Redirect result. public let result: Result + + public init(result: Result) { + self.result = result + } } /// Redirect information. diff --git a/Sources/ProcessOut/Sources/Services/Invoices/DefaultInvoicesService.swift b/Sources/ProcessOut/Sources/Services/Invoices/DefaultInvoicesService.swift index 270210ac2..383a8a25b 100644 --- a/Sources/ProcessOut/Sources/Services/Invoices/DefaultInvoicesService.swift +++ b/Sources/ProcessOut/Sources/Services/Invoices/DefaultInvoicesService.swift @@ -19,7 +19,6 @@ final class DefaultInvoicesService: POInvoicesService { self.customerActionsService = customerActionsService self.eventEmitter = eventEmitter self.logger = logger - commonInit() } // MARK: - POInvoicesService @@ -48,6 +47,12 @@ final class DefaultInvoicesService: POInvoicesService { try await repository.authorizeInvoice(request: request) } + func resolveUrl( + request: PONativeAlternativePaymentUrlResolutionRequestV2 + ) async throws -> PONativeAlternativePaymentUrlResolutionResponseV2 { + try await repository.resolveUrl(request: request) + } + // MARK: - Deprecated func nativeAlternativePaymentMethodTransactionDetails( @@ -107,10 +112,6 @@ final class DefaultInvoicesService: POInvoicesService { // MARK: - Private Methods - private func commonInit() { - observeEvents() - } - private func _authorizeInvoice(request: POInvoiceAuthorizationRequest, threeDSService: PO3DS2Service) async throws { let request = request.replacing( thirdPartySdkVersion: request.thirdPartySdkVersion ?? threeDSService.version @@ -135,46 +136,6 @@ final class DefaultInvoicesService: POInvoicesService { } try await _authorizeInvoice(request: newRequest, threeDSService: threeDSService) } - - // MARK: - Events - - private func observeEvents() { - let deepLinkEventsListener = eventEmitter.on(PODeepLinkReceivedEvent.self) { [weak self] event in - self?.didReceive(deepLinkEvent: event) ?? false - } - eventListeners.append(deepLinkEventsListener) - } - - private func didReceive(deepLinkEvent event: PODeepLinkReceivedEvent) -> Bool { - let shouldResolveDeepLink = eventEmitter.hasListeners(of: PONativeAlternativePaymentDeepLinkResolvedEvent.self) - || eventEmitter.hasListeners(of: PONativeAlternativePaymentDeepLinkResolutionFailedEvent.self) - guard shouldResolveDeepLink else { - logger.debug("Deep link resolution is not requested, ignored.") - return false - } - Task { - do { - let response = try await repository.resolveUrl( - request: .init(redirect: .init(result: .init(url: event.url))) - ) - eventEmitter.emit(event: PONativeAlternativePaymentDeepLinkResolvedEvent(resolutionResponse: response)) - } catch let error as POFailure { - eventEmitter.emit( - event: PONativeAlternativePaymentDeepLinkResolutionFailedEvent(url: event.url, error: error) - ) - } catch { - let error = POFailure( - message: "Unable to resolve deep link URL.", code: .Mobile.generic, underlyingError: error - ) - eventEmitter.emit( - event: PONativeAlternativePaymentDeepLinkResolutionFailedEvent(url: event.url, error: error) - ) - } - } - return true - } - - private nonisolated(unsafe) var eventListeners: [AnyObject] = [] } private extension POInvoiceAuthorizationRequest { // swiftlint:disable:this no_extension_access_modifier diff --git a/Sources/ProcessOut/Sources/Services/Invoices/Events/PONativeAlternativePaymentDeepLinkResolutionFailedEvent.swift b/Sources/ProcessOut/Sources/Services/Invoices/Events/PONativeAlternativePaymentDeepLinkResolutionFailedEvent.swift deleted file mode 100644 index be76e0985..000000000 --- a/Sources/ProcessOut/Sources/Services/Invoices/Events/PONativeAlternativePaymentDeepLinkResolutionFailedEvent.swift +++ /dev/null @@ -1,18 +0,0 @@ -// -// PONativeAlternativePaymentDeepLinkResolutionFailedEvent.swift -// ProcessOut -// -// Created by Andrii Vysotskyi on 25.03.2026. -// - -import Foundation - -@_spi(PO) // swiftlint:disable:next type_name -public struct PONativeAlternativePaymentDeepLinkResolutionFailedEvent: POEventEmitterEvent { - - /// Original deep link URL. - public let url: URL - - /// Resolution error. - public let error: POFailure -} diff --git a/Sources/ProcessOut/Sources/Services/Invoices/Events/PONativeAlternativePaymentDeepLinkResolvedEvent.swift b/Sources/ProcessOut/Sources/Services/Invoices/Events/PONativeAlternativePaymentDeepLinkResolvedEvent.swift deleted file mode 100644 index 50d5b20b3..000000000 --- a/Sources/ProcessOut/Sources/Services/Invoices/Events/PONativeAlternativePaymentDeepLinkResolvedEvent.swift +++ /dev/null @@ -1,13 +0,0 @@ -// -// PONativeAlternativePaymentDeepLinkResolvedEvent.swift -// ProcessOut -// -// Created by Andrii Vysotskyi on 25.03.2026. -// - -@_spi(PO) -public struct PONativeAlternativePaymentDeepLinkResolvedEvent: POEventEmitterEvent { - - /// Native alternative payment URL resolution response. - public let resolutionResponse: PONativeAlternativePaymentUrlResolutionResponseV2 -} diff --git a/Sources/ProcessOut/Sources/Services/Invoices/POInvoicesService.swift b/Sources/ProcessOut/Sources/Services/Invoices/POInvoicesService.swift index 8ae539825..82db45bba 100644 --- a/Sources/ProcessOut/Sources/Services/Invoices/POInvoicesService.swift +++ b/Sources/ProcessOut/Sources/Services/Invoices/POInvoicesService.swift @@ -27,6 +27,12 @@ public protocol POInvoicesService: POService { // sourcery: AutoCompletion request: PONativeAlternativePaymentAuthorizationRequestV2 ) async throws -> PONativeAlternativePaymentAuthorizationResponseV2 + /// Resolves native alternative payment return URL. + @_spi(PO) + func resolveUrl( + request: PONativeAlternativePaymentUrlResolutionRequestV2 + ) async throws -> PONativeAlternativePaymentUrlResolutionResponseV2 + // MARK: - Alternative Payment (Deprecated) /// Requests information needed to continue existing payment or start new one. @@ -52,4 +58,11 @@ extension POInvoicesService { public func createInvoice(request: POInvoiceCreationRequest) async throws -> POInvoice { throw POFailure(code: .Mobile.generic) } + + @_spi(PO) + public func resolveUrl( + request: PONativeAlternativePaymentUrlResolutionRequestV2 + ) async throws -> PONativeAlternativePaymentUrlResolutionResponseV2 { + throw POFailure(code: .Mobile.generic) + } } diff --git a/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/Interactor/Adapter/DefaultNativeAlternativePaymentServiceAdapter.swift b/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/Interactor/Adapter/DefaultNativeAlternativePaymentServiceAdapter.swift index 03d74de1d..59f37e761 100644 --- a/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/Interactor/Adapter/DefaultNativeAlternativePaymentServiceAdapter.swift +++ b/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/Interactor/Adapter/DefaultNativeAlternativePaymentServiceAdapter.swift @@ -81,6 +81,12 @@ final class DefaultNativeAlternativePaymentServiceAdapter: NativeAlternativePaym ) } + func resolveUrl( + with request: PONativeAlternativePaymentUrlResolutionRequestV2 + ) async throws -> PONativeAlternativePaymentUrlResolutionResponseV2 { + try await invoicesService.resolveUrl(request: request) + } + // MARK: - Private Properties private let invoicesService: POInvoicesService diff --git a/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/Interactor/Adapter/NativeAlternativePaymentServiceAdapter.swift b/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/Interactor/Adapter/NativeAlternativePaymentServiceAdapter.swift index 0f07ceeab..40ae16bec 100644 --- a/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/Interactor/Adapter/NativeAlternativePaymentServiceAdapter.swift +++ b/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/Interactor/Adapter/NativeAlternativePaymentServiceAdapter.swift @@ -5,6 +5,8 @@ // Created by Andrii Vysotskyi on 10.06.2025. // +@_spi(PO) import ProcessOut + protocol NativeAlternativePaymentServiceAdapter { func continuePayment( @@ -14,4 +16,8 @@ protocol NativeAlternativePaymentServiceAdapter { func expectPaymentCompletion( with request: NativeAlternativePaymentServiceAdapterRequest ) async throws -> NativeAlternativePaymentServiceAdapterResponse + + func resolveUrl( + with request: PONativeAlternativePaymentUrlResolutionRequestV2 + ) async throws -> PONativeAlternativePaymentUrlResolutionResponseV2 } diff --git a/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/Interactor/NativeAlternativePaymentDefaultInteractor.swift b/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/Interactor/NativeAlternativePaymentDefaultInteractor.swift index 1e69c1cb4..dbafe21df 100644 --- a/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/Interactor/NativeAlternativePaymentDefaultInteractor.swift +++ b/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/Interactor/NativeAlternativePaymentDefaultInteractor.swift @@ -933,39 +933,45 @@ final class NativeAlternativePaymentDefaultInteractor: // MARK: - External Events private func observeEvents() { - let deepLinkResolvedEventsListener = eventEmitter.on( - PONativeAlternativePaymentDeepLinkResolvedEvent.self, - listener: { [weak self] event in - self?.didReceive(event: event) ?? false - } - ) - eventListeners.append(deepLinkResolvedEventsListener) - let deepLinkResolutionFailedEventListener = eventEmitter.on( - PONativeAlternativePaymentDeepLinkResolutionFailedEvent.self, - listener: { [weak self] event in - self?.didReceive(event: event) ?? false + let deepLinkEventsListener = eventEmitter.on(PODeepLinkReceivedEvent.self) { [weak self] event in + self?.didReceive(deepLinkEvent: event) ?? false + } + eventListeners.append(deepLinkEventsListener) + } + + private nonisolated func didReceive(deepLinkEvent event: PODeepLinkReceivedEvent) -> Bool { + Task { @MainActor in + do { + let response = try await serviceAdapter.resolveUrl( + with: .init(redirect: .init(result: .init(url: event.url))) + ) + didResolveDeepLinkReturnUrl(response: response) + } catch { + didFailToResolveDeepLinkReturnUrl(error: error) } - ) - eventListeners.append(deepLinkResolutionFailedEventListener) + } + return true } - private nonisolated func didReceive(event: PONativeAlternativePaymentDeepLinkResolvedEvent) -> Bool { + private func didResolveDeepLinkReturnUrl(response: PONativeAlternativePaymentUrlResolutionResponseV2) { switch configuration.flow { - case .authorization(let flow) where flow.invoiceId != event.resolutionResponse.invoice?.id: - return false - case .tokenization(let flow) where flow.customerTokenId != event.resolutionResponse.customerToken?.id: - return false + case .authorization(let flow) where flow.invoiceId != response.invoice?.id: + logger.debug("Ignoring unrelated deep link resolution.") + return + case .tokenization(let flow) where flow.customerTokenId != response.customerToken?.id: + logger.debug("Ignoring unrelated deep link resolution.") + return default: break } + let adapterResponse = NativeAlternativePaymentServiceAdapterResponse( + state: response.state, + paymentMethod: response.paymentMethod, + invoice: response.invoice, + elements: response.elements, + redirect: response.redirect + ) Task { @MainActor in - let adapterResponse = NativeAlternativePaymentServiceAdapterResponse( - state: event.resolutionResponse.state, - paymentMethod: event.resolutionResponse.paymentMethod, - invoice: event.resolutionResponse.invoice, - elements: event.resolutionResponse.elements, - redirect: event.resolutionResponse.redirect - ) do { switch state { case .idle, .started, .awaitingRedirect: @@ -981,31 +987,34 @@ final class NativeAlternativePaymentDefaultInteractor: setFailureState(error: error) } } - return true } - private nonisolated func didReceive(event: PONativeAlternativePaymentDeepLinkResolutionFailedEvent) -> Bool { - guard event.error.failureCode != .RequestValidation.redirectResultInvalid else { - return false + private func didFailToResolveDeepLinkReturnUrl(error: Error) { + let failure: POFailure + if let error = error as? POFailure { + failure = error + } else { + logger.error("Unexpected error type: \(error)") + failure = .init(message: "Unable to resolve deep link URL.", code: .Mobile.generic, underlyingError: error) } - Task { @MainActor in - switch state { - case .starting(let currentState): - currentState.task.cancel() - case .submitting(let currentState): - currentState.task.cancel() - case .redirecting(let currentState): - currentState.task.cancel() - case .awaitingCompletion(let currentState): - currentState.task?.cancel() - case .failure, .completed: - return - default: - break - } - setFailureState(error: event.error) + guard failure.failureCode != .RequestValidation.redirectResultInvalid else { + return } - return true + switch state { + case .starting(let currentState): + currentState.task.cancel() + case .submitting(let currentState): + currentState.task.cancel() + case .redirecting(let currentState): + currentState.task.cancel() + case .awaitingCompletion(let currentState): + currentState.task?.cancel() + case .failure, .completed: + return + default: + break + } + setFailureState(error: failure) } private var eventListeners: [AnyObject] = [] From 71e4b9594806f09c9471c93ebdfdc4b75dcebf15 Mon Sep 17 00:00:00 2001 From: Andrii Vysotskyi Date: Sun, 26 Apr 2026 14:26:30 +0200 Subject: [PATCH 05/13] Add todo --- .../Interactor/NativeAlternativePaymentDefaultInteractor.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/Interactor/NativeAlternativePaymentDefaultInteractor.swift b/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/Interactor/NativeAlternativePaymentDefaultInteractor.swift index dbafe21df..30e500328 100644 --- a/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/Interactor/NativeAlternativePaymentDefaultInteractor.swift +++ b/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/Interactor/NativeAlternativePaymentDefaultInteractor.swift @@ -940,6 +940,7 @@ final class NativeAlternativePaymentDefaultInteractor: } private nonisolated func didReceive(deepLinkEvent event: PODeepLinkReceivedEvent) -> Bool { + // TODO: Disregard deep link in state other than pending or starting Task { @MainActor in do { let response = try await serviceAdapter.resolveUrl( From 3ad3ed5f15d3fa43208d51bfd84150c959115656 Mon Sep 17 00:00:00 2001 From: Andrii Vysotskyi Date: Sun, 26 Apr 2026 14:30:44 +0200 Subject: [PATCH 06/13] Remove unused method --- ...eAlternativePaymentDefaultInteractor.swift | 24 ------------------- 1 file changed, 24 deletions(-) diff --git a/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/Interactor/NativeAlternativePaymentDefaultInteractor.swift b/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/Interactor/NativeAlternativePaymentDefaultInteractor.swift index 30e500328..f3443401f 100644 --- a/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/Interactor/NativeAlternativePaymentDefaultInteractor.swift +++ b/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/Interactor/NativeAlternativePaymentDefaultInteractor.swift @@ -417,30 +417,6 @@ final class NativeAlternativePaymentDefaultInteractor: enableCancellationAfterDelay() } - /// Requests state change to redirecting while bypassing actual redirect assuming it was performed elsewhere. - private func setRedirectingState(didOpenUrl: Bool) { - guard case .awaitingRedirect(let currentState) = state else { - logger.debug("Ignoring redirect confirmation in unsupported state \(state).") - return - } - let task = Task { - do { - let response = try await serviceAdapter.continuePayment( - with: .init( - flow: configuration.flow, - redirect: currentState.redirect.confirmationRequired ? .init(success: didOpenUrl) : nil, - localeIdentifier: configuration.localization.localeOverride?.identifier - ) - ) - try await setState(with: response) - } catch { - setFailureState(error: error) - } - } - let newState = State.Redirecting(task: task, snapshot: currentState) - state = .redirecting(newState) - } - // MARK: - Completed State private func setCompletedState(response: NativeAlternativePaymentServiceAdapterResponse) async { From 2acefee1d1eec1e82c1d579dec2bafcee284452d Mon Sep 17 00:00:00 2001 From: Andrii Vysotskyi Date: Sun, 26 Apr 2026 14:34:42 +0200 Subject: [PATCH 07/13] Disregard deep link if already redirecting --- .../NativeAlternativePaymentDefaultInteractor.swift | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/Interactor/NativeAlternativePaymentDefaultInteractor.swift b/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/Interactor/NativeAlternativePaymentDefaultInteractor.swift index f3443401f..5c58b3c8a 100644 --- a/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/Interactor/NativeAlternativePaymentDefaultInteractor.swift +++ b/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/Interactor/NativeAlternativePaymentDefaultInteractor.swift @@ -916,7 +916,10 @@ final class NativeAlternativePaymentDefaultInteractor: } private nonisolated func didReceive(deepLinkEvent event: PODeepLinkReceivedEvent) -> Bool { - // TODO: Disregard deep link in state other than pending or starting + let stateSnapshot = MainActor.assumeIsolated { state } // Deep link as expected to arrive on main actor + if case .redirecting = stateSnapshot { + return false + } Task { @MainActor in do { let response = try await serviceAdapter.resolveUrl( From 64afbbb9cd03888797f6fcd18f0ccbbd75f34994 Mon Sep 17 00:00:00 2001 From: Andrii Vysotskyi Date: Sun, 26 Apr 2026 14:36:51 +0200 Subject: [PATCH 08/13] Revert "Disregard deep link if already redirecting" This reverts commit 2acefee1d1eec1e82c1d579dec2bafcee284452d. --- .../NativeAlternativePaymentDefaultInteractor.swift | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/Interactor/NativeAlternativePaymentDefaultInteractor.swift b/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/Interactor/NativeAlternativePaymentDefaultInteractor.swift index 5c58b3c8a..f3443401f 100644 --- a/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/Interactor/NativeAlternativePaymentDefaultInteractor.swift +++ b/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/Interactor/NativeAlternativePaymentDefaultInteractor.swift @@ -916,10 +916,7 @@ final class NativeAlternativePaymentDefaultInteractor: } private nonisolated func didReceive(deepLinkEvent event: PODeepLinkReceivedEvent) -> Bool { - let stateSnapshot = MainActor.assumeIsolated { state } // Deep link as expected to arrive on main actor - if case .redirecting = stateSnapshot { - return false - } + // TODO: Disregard deep link in state other than pending or starting Task { @MainActor in do { let response = try await serviceAdapter.resolveUrl( From 24057253bd82878b52581ac5c7164fbb821d12f2 Mon Sep 17 00:00:00 2001 From: Andrii Vysotskyi Date: Wed, 29 Apr 2026 10:38:08 +0200 Subject: [PATCH 09/13] Disregard deep links in unrelated state --- .../NativeAlternativePaymentDefaultInteractor.swift | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/Interactor/NativeAlternativePaymentDefaultInteractor.swift b/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/Interactor/NativeAlternativePaymentDefaultInteractor.swift index f3443401f..b01079d3b 100644 --- a/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/Interactor/NativeAlternativePaymentDefaultInteractor.swift +++ b/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/Interactor/NativeAlternativePaymentDefaultInteractor.swift @@ -916,7 +916,15 @@ final class NativeAlternativePaymentDefaultInteractor: } private nonisolated func didReceive(deepLinkEvent event: PODeepLinkReceivedEvent) -> Bool { - // TODO: Disregard deep link in state other than pending or starting + let stateSnapshot = MainActor.assumeIsolated { + state + } + switch stateSnapshot { + case .starting, .redirecting: + return false + default: + break + } Task { @MainActor in do { let response = try await serviceAdapter.resolveUrl( From b5ebf281d9fd0625529d21a2c9333471d92ca426 Mon Sep 17 00:00:00 2001 From: Andrii Vysotskyi Date: Mon, 25 May 2026 11:32:14 +0200 Subject: [PATCH 10/13] Prevent generation --- .../Sources/Services/Invoices/POInvoicesService.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ProcessOut/Sources/Services/Invoices/POInvoicesService.swift b/Sources/ProcessOut/Sources/Services/Invoices/POInvoicesService.swift index 82db45bba..0457fc7fd 100644 --- a/Sources/ProcessOut/Sources/Services/Invoices/POInvoicesService.swift +++ b/Sources/ProcessOut/Sources/Services/Invoices/POInvoicesService.swift @@ -29,7 +29,7 @@ public protocol POInvoicesService: POService { // sourcery: AutoCompletion /// Resolves native alternative payment return URL. @_spi(PO) - func resolveUrl( + func resolveUrl( // sourcery:completion: skip request: PONativeAlternativePaymentUrlResolutionRequestV2 ) async throws -> PONativeAlternativePaymentUrlResolutionResponseV2 From 8226a7fc4f032e36f58601aed705dd8336b2960e Mon Sep 17 00:00:00 2001 From: Andrii Vysotskyi Date: Mon, 25 May 2026 11:33:00 +0200 Subject: [PATCH 11/13] Fix tests --- .../MockWebAuthenticationSession.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Tests/ProcessOutTests/Sources/Mocks/WebAuthenticationSession/MockWebAuthenticationSession.swift b/Tests/ProcessOutTests/Sources/Mocks/WebAuthenticationSession/MockWebAuthenticationSession.swift index 7bdfdaa24..11accc6df 100644 --- a/Tests/ProcessOutTests/Sources/Mocks/WebAuthenticationSession/MockWebAuthenticationSession.swift +++ b/Tests/ProcessOutTests/Sources/Mocks/WebAuthenticationSession/MockWebAuthenticationSession.swift @@ -8,20 +8,20 @@ import Foundation @testable @_spi(PO) import ProcessOut -final class MockWebAuthenticationSession: WebAuthenticationSession { +final class MockWebAuthenticationSession: POWebAuthenticationSession { var authenticateCallsCount: Int { lock.withLock { _authenticateCallsCount } } - var authenticateFromClosure: ((WebAuthenticationRequest) async throws -> URL)! { + var authenticateFromClosure: ((POWebAuthenticationRequest) async throws -> URL)! { get { lock.withLock { _authenticateFromClosure } } set { lock.withLock { _authenticateFromClosure = newValue } } } // MARK: - - func authenticate(using request: WebAuthenticationRequest) async throws -> URL { + func authenticate(using request: POWebAuthenticationRequest) async throws -> URL { let authenticate = lock.withLock { _authenticateCallsCount += 1 return _authenticateFromClosure @@ -34,5 +34,5 @@ final class MockWebAuthenticationSession: WebAuthenticationSession { private let lock = POUnfairlyLocked() private nonisolated(unsafe) var _authenticateCallsCount = 0 - private nonisolated(unsafe) var _authenticateFromClosure: ((WebAuthenticationRequest) async throws -> URL)! + private nonisolated(unsafe) var _authenticateFromClosure: ((POWebAuthenticationRequest) async throws -> URL)! } From f7f1b4fe81e3b9d62525c0d88298b0e370a6e107 Mon Sep 17 00:00:00 2001 From: Andrii Vysotskyi Date: Tue, 2 Jun 2026 11:13:23 +0200 Subject: [PATCH 12/13] Improve redirect handling --- .../NativeAlternativePaymentDefaultInteractor.swift | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/Interactor/NativeAlternativePaymentDefaultInteractor.swift b/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/Interactor/NativeAlternativePaymentDefaultInteractor.swift index 23509aac9..140d8a8e7 100644 --- a/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/Interactor/NativeAlternativePaymentDefaultInteractor.swift +++ b/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/Interactor/NativeAlternativePaymentDefaultInteractor.swift @@ -384,11 +384,11 @@ final class NativeAlternativePaymentDefaultInteractor: } private func uncheckedRedirect(to redirect: PONativeAlternativePaymentRedirectV2) async throws { - let redirectResult: PONativeAlternativePaymentRedirectResultV2 + let redirectResult: PONativeAlternativePaymentRedirectResultV2? switch redirect.type { case .deepLink: let didOpenUrl = await openDeepLink(url: redirect.url) - redirectResult = .init(success: didOpenUrl) + redirectResult = redirect.confirmationRequired ? .init(success: didOpenUrl) : nil case .web: let authenticationRequest = POWebAuthenticationRequest( url: redirect.url, @@ -403,7 +403,7 @@ final class NativeAlternativePaymentDefaultInteractor: let response = try await serviceAdapter.continuePayment( with: .init( flow: configuration.flow, - redirect: redirect.confirmationRequired ? redirectResult : nil, + redirect: redirectResult, localeIdentifier: configuration.localization.localeOverride?.identifier ) ) @@ -923,10 +923,10 @@ final class NativeAlternativePaymentDefaultInteractor: state } switch stateSnapshot { - case .starting, .redirecting: - return false - default: + case .awaitingCompletion: break + default: + return false } Task { @MainActor in do { From cfef2eb659cea2ac69e8e9a4b21bbef23b2ba350 Mon Sep 17 00:00:00 2001 From: Andrii Vysotskyi Date: Tue, 2 Jun 2026 11:33:18 +0200 Subject: [PATCH 13/13] Avoid using :: for compatibility reasons --- .../Utils/WebAuthenticationSessionShim.swift | 16 ++++++++++++++++ ...tiveAlternativePaymentDefaultInteractor.swift | 4 ++-- 2 files changed, 18 insertions(+), 2 deletions(-) create mode 100644 Sources/ProcessOutUI/Sources/Core/Utils/WebAuthenticationSessionShim.swift diff --git a/Sources/ProcessOutUI/Sources/Core/Utils/WebAuthenticationSessionShim.swift b/Sources/ProcessOutUI/Sources/Core/Utils/WebAuthenticationSessionShim.swift new file mode 100644 index 000000000..92445e3b2 --- /dev/null +++ b/Sources/ProcessOutUI/Sources/Core/Utils/WebAuthenticationSessionShim.swift @@ -0,0 +1,16 @@ +// +// WebAuthenticationSessionShim.swift +// ProcessOut +// +// Created by Andrii Vysotskyi on 02.06.2026. +// + +@_spi(PO) import protocol ProcessOut.POWebAuthenticationSession + +// TODO: Remove after upgrading to a Swift version with `::` module disambiguation. +// +// `ProcessOutUI.POWebAuthenticationSession` shadows `ProcessOut.POWebAuthenticationSession`, and +// `ProcessOut.POWebAuthenticationSession` cannot be used directly because `ProcessOut` is both the +// module name and a type exported by that module. This alias provides an unambiguous reference to +// the imported type. +typealias WebAuthenticationSessionShim = ProcessOut.POWebAuthenticationSession diff --git a/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/Interactor/NativeAlternativePaymentDefaultInteractor.swift b/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/Interactor/NativeAlternativePaymentDefaultInteractor.swift index 140d8a8e7..d3919f71f 100644 --- a/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/Interactor/NativeAlternativePaymentDefaultInteractor.swift +++ b/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/Interactor/NativeAlternativePaymentDefaultInteractor.swift @@ -18,7 +18,7 @@ final class NativeAlternativePaymentDefaultInteractor: init( configuration: PONativeAlternativePaymentConfiguration, serviceAdapter: NativeAlternativePaymentServiceAdapter, - webAuthenticationSession: ProcessOut::POWebAuthenticationSession, + webAuthenticationSession: WebAuthenticationSessionShim, imagesRepository: POImagesRepository, barcodeImageProvider: BarcodeImageProvider, eventEmitter: POEventEmitter, @@ -178,7 +178,7 @@ final class NativeAlternativePaymentDefaultInteractor: // MARK: - Private Properties private let serviceAdapter: NativeAlternativePaymentServiceAdapter - private let webAuthenticationSession: ProcessOut::POWebAuthenticationSession + private let webAuthenticationSession: WebAuthenticationSessionShim private let imagesRepository: POImagesRepository private let barcodeImageProvider: BarcodeImageProvider private let eventEmitter: POEventEmitter