From ca9bde04602bd45a944d1e44c21c59c7f2ef5a57 Mon Sep 17 00:00:00 2001 From: Andrii Vysotskyi Date: Thu, 26 Mar 2026 20:48:24 +0100 Subject: [PATCH 01/10] Support app2app flow --- .../xcshareddata/swiftpm/Package.resolved | 20 ++-- .../ProcessOut/Sources/Api/ProcessOut.swift | 19 +++- .../Core/EventEmitter/LocalEventEmitter.swift | 8 ++ .../Core/EventEmitter/POEventEmitter.swift | 4 + .../Responses/POCustomerToken.swift | 2 +- .../Invoices/Responses/POInvoice.swift | 2 +- .../Invoices/DefaultInvoicesService.swift | 46 +++++++++- ...InvoiceDeepLinkResolutionFailedEvent.swift | 18 ++++ .../POInvoiceDeepLinkResolvedEvent.swift | 16 ++++ ...eckoutInteractorDefaultChildProvider.swift | 1 + .../PONativeAlternativePaymentComponent.swift | 1 + ...eAlternativePaymentDefaultInteractor.swift | 92 +++++++++++++++++++ 12 files changed, 211 insertions(+), 18 deletions(-) create mode 100644 Sources/ProcessOut/Sources/Services/Invoices/Events/POInvoiceDeepLinkResolutionFailedEvent.swift create mode 100644 Sources/ProcessOut/Sources/Services/Invoices/Events/POInvoiceDeepLinkResolvedEvent.swift diff --git a/Example/Example.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Example/Example.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index f289e84b9..d4f1c495a 100644 --- a/Example/Example.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Example/Example.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "797eaf02a4c0b4b477a991945fa6c684312886a42f6d1845fd6cf70d1c09d2fd", + "originHash" : "8e33b04f0b89dff26f8d44e81a930f3ee2acdbc19d658677d0158262f897fc59", "pins" : [ { "identity" : "checkout-3ds-sdk-ios", @@ -19,6 +19,15 @@ "version" : "1.2.4" } }, + { + "identity" : "ios-3ds-sdk-spm", + "kind" : "remoteSourceControl", + "location" : "https://github.com/netceteragroup/ios-3ds-sdk-spm", + "state" : { + "revision" : "0cabeb92c66959ab922cd1e872e8dbaecc66174a", + "version" : "2.6.0" + } + }, { "identity" : "joseswift", "kind" : "remoteSourceControl", @@ -37,15 +46,6 @@ "version" : "1.12.0" } }, - { - "identity" : "spm", - "kind" : "remoteSourceControl", - "location" : "https://github.com/ios-3ds-sdk/SPM", - "state" : { - "revision" : "bcd39004106ca322809b14337043b478e070e6c6", - "version" : "2.6.0" - } - }, { "identity" : "swift-cmark", "kind" : "remoteSourceControl", diff --git a/Sources/ProcessOut/Sources/Api/ProcessOut.swift b/Sources/ProcessOut/Sources/Api/ProcessOut.swift index 9475a98da..1e293fb54 100644 --- a/Sources/ProcessOut/Sources/Api/ProcessOut.swift +++ b/Sources/ProcessOut/Sources/Api/ProcessOut.swift @@ -5,7 +5,7 @@ // Created by Andrii Vysotskyi on 14.10.2024. // -// swiftlint:disable force_unwrapping type_body_length +// swiftlint:disable force_unwrapping type_body_length file_length import Foundation import UIKit @@ -143,7 +143,10 @@ public final class ProcessOut: @unchecked Sendable { ) gatewayConfigurations = HttpGatewayConfigurationsRepository(connector: httpConnector) invoices = Self.createInvoicesService( - httpConnector: httpConnector, customerActionsService: customerActionsService, logger: serviceLogger + httpConnector: httpConnector, + customerActionsService: customerActionsService, + eventEmitter: eventEmitter, + logger: serviceLogger ) _alternativePayments = Self.createAlternativePaymentsService( configuration: configuration, webAuthenticationSession: webAuthenticationSession, logger: serviceLogger @@ -170,11 +173,17 @@ public final class ProcessOut: @unchecked Sendable { // MARK: - Services private static func createInvoicesService( - httpConnector: HttpConnector, customerActionsService: CustomerActionsService, logger: POLogger + httpConnector: HttpConnector, + customerActionsService: CustomerActionsService, + eventEmitter: POEventEmitter, + logger: POLogger ) -> POInvoicesService { let repository = HttpInvoicesRepository(connector: httpConnector) return DefaultInvoicesService( - repository: repository, customerActionsService: customerActionsService, logger: logger + repository: repository, + customerActionsService: customerActionsService, + eventEmitter: eventEmitter, + logger: logger ) } @@ -393,4 +402,4 @@ extension ProcessOut { } } -// swiftlint:enable force_unwrapping type_body_length +// swiftlint:enable force_unwrapping type_body_length file_length diff --git a/Sources/ProcessOut/Sources/Core/EventEmitter/LocalEventEmitter.swift b/Sources/ProcessOut/Sources/Core/EventEmitter/LocalEventEmitter.swift index 6593ce84b..9b7044ae2 100644 --- a/Sources/ProcessOut/Sources/Core/EventEmitter/LocalEventEmitter.swift +++ b/Sources/ProcessOut/Sources/Core/EventEmitter/LocalEventEmitter.swift @@ -64,6 +64,14 @@ final class LocalEventEmitter: POEventEmitter, @unchecked Sendable { return cancellable } + func hasListeners(of eventType: Event.Type) -> Bool { + lock.lock() + defer { + lock.unlock() + } + return subscriptions.keys.contains(Event.name) ?? false + } + // MARK: - Private Nested Types private struct Subscription { diff --git a/Sources/ProcessOut/Sources/Core/EventEmitter/POEventEmitter.swift b/Sources/ProcessOut/Sources/Core/EventEmitter/POEventEmitter.swift index 7d0e35d30..d3ae4fe60 100644 --- a/Sources/ProcessOut/Sources/Core/EventEmitter/POEventEmitter.swift +++ b/Sources/ProcessOut/Sources/Core/EventEmitter/POEventEmitter.swift @@ -18,4 +18,8 @@ public protocol POEventEmitter: Sendable { func on( _ eventType: Event.Type, listener: @escaping @Sendable (Event) -> Bool ) -> AnyObject + + /// Returns boolean value indicating whether there are currently any listeners + /// of event with given type. + func hasListeners(of eventType: Event.Type) -> Bool } diff --git a/Sources/ProcessOut/Sources/Repositories/CustomerTokens/Responses/POCustomerToken.swift b/Sources/ProcessOut/Sources/Repositories/CustomerTokens/Responses/POCustomerToken.swift index cb99227e3..5b1caf0a3 100644 --- a/Sources/ProcessOut/Sources/Repositories/CustomerTokens/Responses/POCustomerToken.swift +++ b/Sources/ProcessOut/Sources/Repositories/CustomerTokens/Responses/POCustomerToken.swift @@ -9,7 +9,7 @@ import Foundation /// Customer tokens (usually just called tokens for short) are objects that associate a payment source such as a /// card or APM token with a customer. -public struct POCustomerToken: Codable, Sendable { +public struct POCustomerToken: Identifiable, Codable, Sendable { /// Customer token verification status. public enum VerificationStatus: String, Codable, Sendable { diff --git a/Sources/ProcessOut/Sources/Repositories/Invoices/Responses/POInvoice.swift b/Sources/ProcessOut/Sources/Repositories/Invoices/Responses/POInvoice.swift index 3691dcd1a..a64831fbd 100644 --- a/Sources/ProcessOut/Sources/Repositories/Invoices/Responses/POInvoice.swift +++ b/Sources/ProcessOut/Sources/Repositories/Invoices/Responses/POInvoice.swift @@ -8,7 +8,7 @@ import Foundation /// Invoice details. -public struct POInvoice: Codable, Sendable { +public struct POInvoice: Identifiable, Codable, Sendable { /// String value that uniquely identifies this invoice. public let id: String diff --git a/Sources/ProcessOut/Sources/Services/Invoices/DefaultInvoicesService.swift b/Sources/ProcessOut/Sources/Services/Invoices/DefaultInvoicesService.swift index a4696d91f..f5534860c 100644 --- a/Sources/ProcessOut/Sources/Services/Invoices/DefaultInvoicesService.swift +++ b/Sources/ProcessOut/Sources/Services/Invoices/DefaultInvoicesService.swift @@ -9,10 +9,17 @@ import Foundation final class DefaultInvoicesService: POInvoicesService { - init(repository: InvoicesRepository, customerActionsService: CustomerActionsService, logger: POLogger) { + init( + repository: InvoicesRepository, + customerActionsService: CustomerActionsService, + eventEmitter: POEventEmitter, + logger: POLogger, + ) { self.repository = repository self.customerActionsService = customerActionsService + self.eventEmitter = eventEmitter self.logger = logger + commonInit() } // MARK: - POInvoicesService @@ -95,10 +102,15 @@ final class DefaultInvoicesService: POInvoicesService { private let repository: InvoicesRepository private let customerActionsService: CustomerActionsService + private let eventEmitter: POEventEmitter private let logger: POLogger // 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 @@ -123,6 +135,38 @@ 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 { + guard event.url.queryParameters?.keys.contains("po_token") ?? false else { + logger.debug("Deep link url \(event.url) is not supported, ignored.") + return false + } + let shouldResolveDeepLink = eventEmitter.hasListeners(of: POInvoiceDeepLinkResolvedEvent.self) + || eventEmitter.hasListeners(of: POInvoiceDeepLinkResolutionFailedEvent.self) + guard shouldResolveDeepLink else { + logger.debug("Deep link resolution is not requested, ignored.") + return false + } + Task { + do throws(POFailure) { + // TODO: Resolve deep link + } catch { + eventEmitter.emit(event: POInvoiceDeepLinkResolutionFailedEvent(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/POInvoiceDeepLinkResolutionFailedEvent.swift b/Sources/ProcessOut/Sources/Services/Invoices/Events/POInvoiceDeepLinkResolutionFailedEvent.swift new file mode 100644 index 000000000..4f91db982 --- /dev/null +++ b/Sources/ProcessOut/Sources/Services/Invoices/Events/POInvoiceDeepLinkResolutionFailedEvent.swift @@ -0,0 +1,18 @@ +// +// POInvoiceDeepLinkResolutionFailedEvent.swift +// ProcessOut +// +// Created by Andrii Vysotskyi on 25.03.2026. +// + +import Foundation + +@_spi(PO) +public struct POInvoiceDeepLinkResolutionFailedEvent: POEventEmitterEvent { + + /// Original deep link URL. + public let url: URL + + /// Resolution error. + public let error: POFailure +} diff --git a/Sources/ProcessOut/Sources/Services/Invoices/Events/POInvoiceDeepLinkResolvedEvent.swift b/Sources/ProcessOut/Sources/Services/Invoices/Events/POInvoiceDeepLinkResolvedEvent.swift new file mode 100644 index 000000000..32dc4dd29 --- /dev/null +++ b/Sources/ProcessOut/Sources/Services/Invoices/Events/POInvoiceDeepLinkResolvedEvent.swift @@ -0,0 +1,16 @@ +// +// POInvoiceDeepLinkResolvedEvent.swift +// ProcessOut +// +// Created by Andrii Vysotskyi on 25.03.2026. +// + +@_spi(PO) +public struct POInvoiceDeepLinkResolvedEvent: POEventEmitterEvent { + + /// Resolved invoice ID. + public let invoiceId: POInvoice.ID + + /// Customer token ID. + public let customerTokenId: POCustomerToken.ID? +} diff --git a/Sources/ProcessOutUI/Sources/Modules/DynamicCheckout/Utils/ChildProvider/DynamicCheckoutInteractorDefaultChildProvider.swift b/Sources/ProcessOutUI/Sources/Modules/DynamicCheckout/Utils/ChildProvider/DynamicCheckoutInteractorDefaultChildProvider.swift index b092089d9..52d7940d0 100644 --- a/Sources/ProcessOutUI/Sources/Modules/DynamicCheckout/Utils/ChildProvider/DynamicCheckoutInteractorDefaultChildProvider.swift +++ b/Sources/ProcessOutUI/Sources/Modules/DynamicCheckout/Utils/ChildProvider/DynamicCheckoutInteractorDefaultChildProvider.swift @@ -63,6 +63,7 @@ final class DynamicCheckoutInteractorDefaultChildProvider: DynamicCheckoutIntera alternativePaymentsService: ProcessOut.shared.alternativePayments, imagesRepository: imagesRepository, barcodeImageProvider: DefaultBarcodeImageProvider(logger: logger), + eventEmitter: ProcessOut.shared.eventEmitter, logger: logger, completion: { _ in } ) diff --git a/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/Component/PONativeAlternativePaymentComponent.swift b/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/Component/PONativeAlternativePaymentComponent.swift index 2cc7b908f..1e392776e 100644 --- a/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/Component/PONativeAlternativePaymentComponent.swift +++ b/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/Component/PONativeAlternativePaymentComponent.swift @@ -37,6 +37,7 @@ public final class PONativeAlternativePaymentComponent { alternativePaymentsService: ProcessOut.shared.alternativePayments, imagesRepository: ProcessOut.shared.images, barcodeImageProvider: DefaultBarcodeImageProvider(logger: logger), + eventEmitter: ProcessOut.shared.eventEmitter, logger: logger, completion: completion ) diff --git a/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/Interactor/NativeAlternativePaymentDefaultInteractor.swift b/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/Interactor/NativeAlternativePaymentDefaultInteractor.swift index ac06d555a..dc3f369dd 100644 --- a/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/Interactor/NativeAlternativePaymentDefaultInteractor.swift +++ b/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/Interactor/NativeAlternativePaymentDefaultInteractor.swift @@ -21,6 +21,7 @@ final class NativeAlternativePaymentDefaultInteractor: alternativePaymentsService: POAlternativePaymentsService, imagesRepository: POImagesRepository, barcodeImageProvider: BarcodeImageProvider, + eventEmitter: POEventEmitter, logger: POLogger, completion: @escaping (Result) -> Void ) { @@ -29,6 +30,7 @@ final class NativeAlternativePaymentDefaultInteractor: self.alternativePaymentsService = alternativePaymentsService self.imagesRepository = imagesRepository self.barcodeImageProvider = barcodeImageProvider + self.eventEmitter = eventEmitter self.logger = logger self.completion = completion super.init(state: .idle) @@ -63,6 +65,7 @@ final class NativeAlternativePaymentDefaultInteractor: } } state = .starting(.init(task: task)) + observeEvents() _ = await task.result } @@ -200,6 +203,7 @@ final class NativeAlternativePaymentDefaultInteractor: private let alternativePaymentsService: POAlternativePaymentsService private let imagesRepository: POImagesRepository private let barcodeImageProvider: BarcodeImageProvider + private let eventEmitter: POEventEmitter private let logger: POLogger private let completion: (Result) -> Void @@ -418,6 +422,30 @@ 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 { @@ -492,6 +520,10 @@ final class NativeAlternativePaymentDefaultInteractor: // MARK: - Failure State private func setFailureState(error: Error) { + guard !Task.isCancelled else { + logger.debug("Task is cancelled, ignoring attempt to set failure state with: \(error).") + return + } guard !state.isSink else { logger.debug("Already in a sink state, ignoring attempt to set failure state with: \(error).") return @@ -914,6 +946,66 @@ final class NativeAlternativePaymentDefaultInteractor: } return await UIApplication.shared.open(url, options: options) } + + // MARK: - External Events + + private func observeEvents() { + let deepLinkResolvedEventsListener = eventEmitter.on( + POInvoiceDeepLinkResolvedEvent.self, + listener: { [weak self] event in + self?.didReceive(event: event) ?? false + } + ) + eventListeners.append(deepLinkResolvedEventsListener) + let deepLinkResolutionFailedEventListener = eventEmitter.on( + POInvoiceDeepLinkResolutionFailedEvent.self, + listener: { [weak self] event in + self?.didReceive(event: event) ?? false + } + ) + eventListeners.append(deepLinkResolutionFailedEventListener) + } + + private nonisolated func didReceive(event: POInvoiceDeepLinkResolvedEvent) -> Bool { + switch configuration.flow { + case .authorization(let flow) where flow.invoiceId != event.invoiceId: + return false + case .tokenization(let flow) where flow.customerTokenId != event.customerTokenId: + return false + default: + break + } + Task { @MainActor in + switch state { + case .awaitingRedirect: + setRedirectingState(didOpenUrl: true) + case .awaitingCompletion: + confirmPayment() + default: + return + } + } + return true + } + + private nonisolated func didReceive(event: POInvoiceDeepLinkResolutionFailedEvent) -> Bool { + 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() + default: + break + } + setFailureState(error: event.error) + } + return true + } + + private var eventListeners: [AnyObject] = [] } // swiftlint:enable file_length type_body_length From cb366c32a868d74fcc939dacd3ae2a5824049cfb Mon Sep 17 00:00:00 2001 From: Andrii Vysotskyi Date: Fri, 27 Mar 2026 11:17:06 +0100 Subject: [PATCH 02/10] Support alternative payment redirect customization --- ...ernativePaymentTokenizationRequestV2.swift | 7 ++++ ...rnativePaymentAuthorizationRequestV2.swift | 7 ++++ ...iveAlternativePaymentConfigurationV2.swift | 41 +++++++++++++++++++ ...tiveAlternativePaymentServiceAdapter.swift | 2 + 4 files changed, 57 insertions(+) create mode 100644 Sources/ProcessOut/Sources/Repositories/Shared/Requests/AlternativePaymentV2/PONativeAlternativePaymentConfigurationV2.swift diff --git a/Sources/ProcessOut/Sources/Repositories/CustomerTokens/Requests/PONativeAlternativePaymentTokenizationRequestV2.swift b/Sources/ProcessOut/Sources/Repositories/CustomerTokens/Requests/PONativeAlternativePaymentTokenizationRequestV2.swift index ef6fcc1fa..8b4942eb5 100644 --- a/Sources/ProcessOut/Sources/Repositories/CustomerTokens/Requests/PONativeAlternativePaymentTokenizationRequestV2.swift +++ b/Sources/ProcessOut/Sources/Repositories/CustomerTokens/Requests/PONativeAlternativePaymentTokenizationRequestV2.swift @@ -16,6 +16,11 @@ public struct PONativeAlternativePaymentTokenizationRequestV2: Sendable, Encodab /// Gateway configuration identifier. public let gatewayConfigurationId: String + /// Alternative payment configuration. + /// + /// - WARNING: Only has effect when passed with first call. + public let configuration: PONativeAlternativePaymentConfigurationV2 + /// Payment request parameters. public let submitData: PONativeAlternativePaymentSubmitDataV2? @@ -30,6 +35,7 @@ public struct PONativeAlternativePaymentTokenizationRequestV2: Sendable, Encodab customerId: String, customerTokenId: String, gatewayConfigurationId: String, + configuration: PONativeAlternativePaymentConfigurationV2 = .init(), submitData: PONativeAlternativePaymentSubmitDataV2? = nil, redirect: PONativeAlternativePaymentRedirectResultV2? = nil, localeIdentifier: String? = nil @@ -37,6 +43,7 @@ public struct PONativeAlternativePaymentTokenizationRequestV2: Sendable, Encodab self.customerId = customerId self.customerTokenId = customerTokenId self.gatewayConfigurationId = gatewayConfigurationId + self.configuration = configuration self.submitData = submitData self.redirect = redirect self.localeIdentifier = localeIdentifier diff --git a/Sources/ProcessOut/Sources/Repositories/Invoices/Requests/AlternativePaymentV2/PONativeAlternativePaymentAuthorizationRequestV2.swift b/Sources/ProcessOut/Sources/Repositories/Invoices/Requests/AlternativePaymentV2/PONativeAlternativePaymentAuthorizationRequestV2.swift index 6f20b1655..c6a240db9 100644 --- a/Sources/ProcessOut/Sources/Repositories/Invoices/Requests/AlternativePaymentV2/PONativeAlternativePaymentAuthorizationRequestV2.swift +++ b/Sources/ProcessOut/Sources/Repositories/Invoices/Requests/AlternativePaymentV2/PONativeAlternativePaymentAuthorizationRequestV2.swift @@ -13,6 +13,11 @@ public struct PONativeAlternativePaymentAuthorizationRequestV2: Sendable, Encoda /// Gateway configuration identifier. public let gatewayConfigurationId: String + /// Alternative payment configuration. + /// + /// - WARNING: Only has effect when passed with first call. + public let configuration: PONativeAlternativePaymentConfigurationV2 + /// Payment source. public let source: String? @@ -29,6 +34,7 @@ public struct PONativeAlternativePaymentAuthorizationRequestV2: Sendable, Encoda public init( invoiceId: String, gatewayConfigurationId: String, + configuration: PONativeAlternativePaymentConfigurationV2 = .init(), source: String? = nil, submitData: PONativeAlternativePaymentSubmitDataV2? = nil, redirect: PONativeAlternativePaymentRedirectResultV2? = nil, @@ -36,6 +42,7 @@ public struct PONativeAlternativePaymentAuthorizationRequestV2: Sendable, Encoda ) { self.invoiceId = invoiceId self.gatewayConfigurationId = gatewayConfigurationId + self.configuration = configuration self.source = source self.submitData = submitData self.redirect = redirect diff --git a/Sources/ProcessOut/Sources/Repositories/Shared/Requests/AlternativePaymentV2/PONativeAlternativePaymentConfigurationV2.swift b/Sources/ProcessOut/Sources/Repositories/Shared/Requests/AlternativePaymentV2/PONativeAlternativePaymentConfigurationV2.swift new file mode 100644 index 000000000..925feacbe --- /dev/null +++ b/Sources/ProcessOut/Sources/Repositories/Shared/Requests/AlternativePaymentV2/PONativeAlternativePaymentConfigurationV2.swift @@ -0,0 +1,41 @@ +// +// PONativeAlternativePaymentConfigurationV2.swift +// ProcessOut +// +// Created by Andrii Vysotskyi on 27.03.2026. +// + +/// Payment configuration. +public struct PONativeAlternativePaymentConfigurationV2: Sendable, Encodable { + + public struct ReturnRedirectType: Sendable { + + /// Redirect type raw value. + let rawValue: String + } + + /// Return redirect type. + public let returnRedirectType: ReturnRedirectType + + public init(returnRedirectType: ReturnRedirectType = .automatic) { + self.returnRedirectType = returnRedirectType + } +} + +extension PONativeAlternativePaymentConfigurationV2.ReturnRedirectType { + + /// Redirect result is handled automatically. + public static let automatic = Self(rawValue: "automatic") + + /// Redirect result is not processed automatically and should be resolved explicitly. + @_spi(PO) + public static let manual = Self(rawValue: "manual") +} + +extension PONativeAlternativePaymentConfigurationV2.ReturnRedirectType: Encodable { + + public func encode(to encoder: any Encoder) throws { + var container = encoder.singleValueContainer() + try container.encode(rawValue) + } +} diff --git a/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/Interactor/Adapter/DefaultNativeAlternativePaymentServiceAdapter.swift b/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/Interactor/Adapter/DefaultNativeAlternativePaymentServiceAdapter.swift index 616edef8a..ca3918a1b 100644 --- a/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/Interactor/Adapter/DefaultNativeAlternativePaymentServiceAdapter.swift +++ b/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/Interactor/Adapter/DefaultNativeAlternativePaymentServiceAdapter.swift @@ -30,6 +30,7 @@ final class DefaultNativeAlternativePaymentServiceAdapter: NativeAlternativePaym let authorizationRequest = PONativeAlternativePaymentAuthorizationRequestV2( invoiceId: flow.invoiceId, gatewayConfigurationId: flow.gatewayConfigurationId, + configuration: .init(returnRedirectType: .manual), source: flow.customerTokenId, submitData: request.submitData, redirect: request.redirect, @@ -42,6 +43,7 @@ final class DefaultNativeAlternativePaymentServiceAdapter: NativeAlternativePaym customerId: flow.customerId, customerTokenId: flow.customerTokenId, gatewayConfigurationId: flow.gatewayConfigurationId, + configuration: .init(returnRedirectType: .manual), submitData: request.submitData, redirect: request.redirect, localeIdentifier: request.localeIdentifier From 9efd98ac4446ec9032b5149c7e6228b10ee12cdc Mon Sep 17 00:00:00 2001 From: Andrii Vysotskyi Date: Fri, 27 Mar 2026 11:19:23 +0100 Subject: [PATCH 03/10] Update comment --- .../PONativeAlternativePaymentTokenizationRequestV2.swift | 3 ++- .../PONativeAlternativePaymentAuthorizationRequestV2.swift | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/Sources/ProcessOut/Sources/Repositories/CustomerTokens/Requests/PONativeAlternativePaymentTokenizationRequestV2.swift b/Sources/ProcessOut/Sources/Repositories/CustomerTokens/Requests/PONativeAlternativePaymentTokenizationRequestV2.swift index 8b4942eb5..392fb36a3 100644 --- a/Sources/ProcessOut/Sources/Repositories/CustomerTokens/Requests/PONativeAlternativePaymentTokenizationRequestV2.swift +++ b/Sources/ProcessOut/Sources/Repositories/CustomerTokens/Requests/PONativeAlternativePaymentTokenizationRequestV2.swift @@ -18,7 +18,8 @@ public struct PONativeAlternativePaymentTokenizationRequestV2: Sendable, Encodab /// Alternative payment configuration. /// - /// - WARNING: Only has effect when passed with first call. + /// - WARNING: Configuration is respected only with the **FIRST** request for the payment, ignored for + /// subsequent ones. public let configuration: PONativeAlternativePaymentConfigurationV2 /// Payment request parameters. diff --git a/Sources/ProcessOut/Sources/Repositories/Invoices/Requests/AlternativePaymentV2/PONativeAlternativePaymentAuthorizationRequestV2.swift b/Sources/ProcessOut/Sources/Repositories/Invoices/Requests/AlternativePaymentV2/PONativeAlternativePaymentAuthorizationRequestV2.swift index c6a240db9..ada325fe6 100644 --- a/Sources/ProcessOut/Sources/Repositories/Invoices/Requests/AlternativePaymentV2/PONativeAlternativePaymentAuthorizationRequestV2.swift +++ b/Sources/ProcessOut/Sources/Repositories/Invoices/Requests/AlternativePaymentV2/PONativeAlternativePaymentAuthorizationRequestV2.swift @@ -15,7 +15,8 @@ public struct PONativeAlternativePaymentAuthorizationRequestV2: Sendable, Encoda /// Alternative payment configuration. /// - /// - WARNING: Only has effect when passed with first call. + /// - WARNING: Configuration is respected only with the **FIRST** request for the payment, ignored for + /// subsequent ones. public let configuration: PONativeAlternativePaymentConfigurationV2 /// Payment source. From 6c18812bac737916f09666d21102a82772b22b25 Mon Sep 17 00:00:00 2001 From: Andrii Vysotskyi Date: Fri, 27 Mar 2026 13:43:43 +0100 Subject: [PATCH 04/10] Apply API changes --- .../Invoices/HttpInvoicesRepository.swift | 9 +++++ .../Invoices/InvoicesRepository.swift | 7 ++++ ...rnativePaymentUrlResolutionRequestV2.swift | 31 +++++++++++++++ ...nativePaymentUrlResolutionResponseV2.swift | 39 +++++++++++++++++++ ...veAlternativePaymentRedirectResultV2.swift | 2 +- .../Invoices/DefaultInvoicesService.swift | 12 +++++- .../POInvoiceDeepLinkResolvedEvent.swift | 7 +--- ...eAlternativePaymentDefaultInteractor.swift | 28 ++++++++----- 8 files changed, 118 insertions(+), 17 deletions(-) create mode 100644 Sources/ProcessOut/Sources/Repositories/Invoices/Requests/AlternativePaymentV2/PONativeAlternativePaymentUrlResolutionRequestV2.swift create mode 100644 Sources/ProcessOut/Sources/Repositories/Invoices/Responses/AlternativePaymentV2/PONativeAlternativePaymentUrlResolutionResponseV2.swift diff --git a/Sources/ProcessOut/Sources/Repositories/Invoices/HttpInvoicesRepository.swift b/Sources/ProcessOut/Sources/Repositories/Invoices/HttpInvoicesRepository.swift index ae4f2133d..4981245e3 100644 --- a/Sources/ProcessOut/Sources/Repositories/Invoices/HttpInvoicesRepository.swift +++ b/Sources/ProcessOut/Sources/Repositories/Invoices/HttpInvoicesRepository.swift @@ -76,6 +76,15 @@ final class HttpInvoicesRepository: InvoicesRepository { return try await connector.execute(request: httpRequest) } + func resolveUrl( + request: PONativeAlternativePaymentUrlResolutionRequestV2 + ) async throws -> PONativeAlternativePaymentUrlResolutionResponseV2 { + let httpRequest = HttpConnectorRequest.post( + path: "/apm-payments", body: request + ) + return try await connector.execute(request: httpRequest) + } + // MARK: - Deprecated func nativeAlternativePaymentMethodTransactionDetails( diff --git a/Sources/ProcessOut/Sources/Repositories/Invoices/InvoicesRepository.swift b/Sources/ProcessOut/Sources/Repositories/Invoices/InvoicesRepository.swift index c618efa62..e54fb68c8 100644 --- a/Sources/ProcessOut/Sources/Repositories/Invoices/InvoicesRepository.swift +++ b/Sources/ProcessOut/Sources/Repositories/Invoices/InvoicesRepository.swift @@ -5,6 +5,8 @@ // Created by Andrii Vysotskyi on 17.10.2022. // +import Foundation + protocol InvoicesRepository: PORepository { /// Creates invoice with given parameters. @@ -23,6 +25,11 @@ protocol InvoicesRepository: PORepository { request: PONativeAlternativePaymentAuthorizationRequestV2 ) async throws -> PONativeAlternativePaymentAuthorizationResponseV2 + /// Resolves native alternative payment return URL. + func resolveUrl( + request: PONativeAlternativePaymentUrlResolutionRequestV2 + ) async throws -> PONativeAlternativePaymentUrlResolutionResponseV2 + // MARK: - Alternative Payment (Deprecated) /// Requests information needed to continue existing payment or start new one. diff --git a/Sources/ProcessOut/Sources/Repositories/Invoices/Requests/AlternativePaymentV2/PONativeAlternativePaymentUrlResolutionRequestV2.swift b/Sources/ProcessOut/Sources/Repositories/Invoices/Requests/AlternativePaymentV2/PONativeAlternativePaymentUrlResolutionRequestV2.swift new file mode 100644 index 000000000..2694229f0 --- /dev/null +++ b/Sources/ProcessOut/Sources/Repositories/Invoices/Requests/AlternativePaymentV2/PONativeAlternativePaymentUrlResolutionRequestV2.swift @@ -0,0 +1,31 @@ +// +// PONativeAlternativePaymentUrlResolutionRequestV2.swift +// ProcessOut +// +// Created by Andrii Vysotskyi on 27.03.2026. +// + +import Foundation + +@_spi(PO) +public struct PONativeAlternativePaymentUrlResolutionRequestV2: Sendable, Encodable { + + public struct Redirect: Sendable, Encodable { + + public struct Result: Sendable, Encodable { // swiftlint:disable:this nesting + + /// Result URL. + public let url: URL + } + + /// Redirect result. + public let result: Result + } + + /// Redirect information. + public let redirect: Redirect + + public init(redirect: Redirect) { + self.redirect = redirect + } +} diff --git a/Sources/ProcessOut/Sources/Repositories/Invoices/Responses/AlternativePaymentV2/PONativeAlternativePaymentUrlResolutionResponseV2.swift b/Sources/ProcessOut/Sources/Repositories/Invoices/Responses/AlternativePaymentV2/PONativeAlternativePaymentUrlResolutionResponseV2.swift new file mode 100644 index 000000000..9a59b4fbf --- /dev/null +++ b/Sources/ProcessOut/Sources/Repositories/Invoices/Responses/AlternativePaymentV2/PONativeAlternativePaymentUrlResolutionResponseV2.swift @@ -0,0 +1,39 @@ +// +// PONativeAlternativePaymentUrlResolutionResponseV2.swift +// ProcessOut +// +// Created by Andrii Vysotskyi on 27.03.2026. +// + +import Foundation + +@_spi(PO) +public struct PONativeAlternativePaymentUrlResolutionResponseV2: Sendable, Decodable { + + public struct Payment: Sendable, Decodable { + + /// Invoice ID. + public let invoiceId: POInvoice.ID + + /// Customer token ID if any. + public let customerTokenId: POCustomerToken.ID? + } + + /// Payment state. + public let state: PONativeAlternativePaymentStateV2 + + /// Payment method information. + public let paymentMethod: PONativeAlternativePaymentMethodV2 + + /// Invoice information if available. + public let invoice: PONativeAlternativePaymentInvoiceV2? + + /// UI elements to display to user. + public let elements: [PONativeAlternativePaymentElementV2]? + + /// Redirect details. + public let redirect: PONativeAlternativePaymentRedirectV2? + + /// Payment information. + public let payment: Payment +} diff --git a/Sources/ProcessOut/Sources/Repositories/Shared/Requests/AlternativePaymentV2/PONativeAlternativePaymentRedirectResultV2.swift b/Sources/ProcessOut/Sources/Repositories/Shared/Requests/AlternativePaymentV2/PONativeAlternativePaymentRedirectResultV2.swift index af6aae0ac..ca0ac8433 100644 --- a/Sources/ProcessOut/Sources/Repositories/Shared/Requests/AlternativePaymentV2/PONativeAlternativePaymentRedirectResultV2.swift +++ b/Sources/ProcessOut/Sources/Repositories/Shared/Requests/AlternativePaymentV2/PONativeAlternativePaymentRedirectResultV2.swift @@ -11,6 +11,6 @@ public struct PONativeAlternativePaymentRedirectResultV2: Sendable, Encodable { self.success = success } - /// indicates whether customer was redirected successfully. + /// Indicates whether customer was redirected successfully. public let success: Bool } diff --git a/Sources/ProcessOut/Sources/Services/Invoices/DefaultInvoicesService.swift b/Sources/ProcessOut/Sources/Services/Invoices/DefaultInvoicesService.swift index f5534860c..e2f7eb59e 100644 --- a/Sources/ProcessOut/Sources/Services/Invoices/DefaultInvoicesService.swift +++ b/Sources/ProcessOut/Sources/Services/Invoices/DefaultInvoicesService.swift @@ -157,9 +157,17 @@ final class DefaultInvoicesService: POInvoicesService { return false } Task { - do throws(POFailure) { - // TODO: Resolve deep link + do { + let response = try await repository.resolveUrl( + request: .init(redirect: .init(result: .init(url: event.url))) + ) + eventEmitter.emit(event: POInvoiceDeepLinkResolvedEvent(resolutionResponse: response)) + } catch let error as POFailure { + eventEmitter.emit(event: POInvoiceDeepLinkResolutionFailedEvent(url: event.url, error: error)) } catch { + let error = POFailure( + message: "Unable to resolve deep link URL.", code: .Mobile.generic, underlyingError: error + ) eventEmitter.emit(event: POInvoiceDeepLinkResolutionFailedEvent(url: event.url, error: error)) } } diff --git a/Sources/ProcessOut/Sources/Services/Invoices/Events/POInvoiceDeepLinkResolvedEvent.swift b/Sources/ProcessOut/Sources/Services/Invoices/Events/POInvoiceDeepLinkResolvedEvent.swift index 32dc4dd29..39458f68a 100644 --- a/Sources/ProcessOut/Sources/Services/Invoices/Events/POInvoiceDeepLinkResolvedEvent.swift +++ b/Sources/ProcessOut/Sources/Services/Invoices/Events/POInvoiceDeepLinkResolvedEvent.swift @@ -8,9 +8,6 @@ @_spi(PO) public struct POInvoiceDeepLinkResolvedEvent: POEventEmitterEvent { - /// Resolved invoice ID. - public let invoiceId: POInvoice.ID - - /// Customer token ID. - public let customerTokenId: POCustomerToken.ID? + /// Native alternative payment URL resolution response. + public let resolutionResponse: PONativeAlternativePaymentUrlResolutionResponseV2 } diff --git a/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/Interactor/NativeAlternativePaymentDefaultInteractor.swift b/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/Interactor/NativeAlternativePaymentDefaultInteractor.swift index dc3f369dd..7f5f528d4 100644 --- a/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/Interactor/NativeAlternativePaymentDefaultInteractor.swift +++ b/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/Interactor/NativeAlternativePaymentDefaultInteractor.swift @@ -968,21 +968,31 @@ final class NativeAlternativePaymentDefaultInteractor: private nonisolated func didReceive(event: POInvoiceDeepLinkResolvedEvent) -> Bool { switch configuration.flow { - case .authorization(let flow) where flow.invoiceId != event.invoiceId: + case .authorization(let flow) where flow.invoiceId != event.resolutionResponse.payment.invoiceId: return false - case .tokenization(let flow) where flow.customerTokenId != event.customerTokenId: + case .tokenization(let flow) where flow.customerTokenId != event.resolutionResponse.payment.customerTokenId: return false default: break } Task { @MainActor in - switch state { - case .awaitingRedirect: - setRedirectingState(didOpenUrl: true) - case .awaitingCompletion: - confirmPayment() - default: - return + 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, .awaitingCompletion: + try await setState(with: adapterResponse) + default: + logger.warn("Unable to apply resolve deep link to current state: \(state).") + return + } + } catch { + setFailureState(error: error) } } return true From 8a19988045b31b39a348cc114fcaf5984c73c511 Mon Sep 17 00:00:00 2001 From: Andrii Vysotskyi Date: Fri, 27 Mar 2026 13:45:29 +0100 Subject: [PATCH 05/10] Update naming --- .../Services/Invoices/DefaultInvoicesService.swift | 10 +++++----- ...ernativePaymentDeepLinkResolutionFailedEvent.swift} | 4 ++-- ...ativeAlternativePaymentDeepLinkResolvedEvent.swift} | 4 ++-- .../NativeAlternativePaymentDefaultInteractor.swift | 8 ++++---- 4 files changed, 13 insertions(+), 13 deletions(-) rename Sources/ProcessOut/Sources/Services/Invoices/Events/{POInvoiceDeepLinkResolutionFailedEvent.swift => PONativeAlternativePaymentDeepLinkResolutionFailedEvent.swift} (57%) rename Sources/ProcessOut/Sources/Services/Invoices/Events/{POInvoiceDeepLinkResolvedEvent.swift => PONativeAlternativePaymentDeepLinkResolvedEvent.swift} (61%) diff --git a/Sources/ProcessOut/Sources/Services/Invoices/DefaultInvoicesService.swift b/Sources/ProcessOut/Sources/Services/Invoices/DefaultInvoicesService.swift index e2f7eb59e..5895df0bd 100644 --- a/Sources/ProcessOut/Sources/Services/Invoices/DefaultInvoicesService.swift +++ b/Sources/ProcessOut/Sources/Services/Invoices/DefaultInvoicesService.swift @@ -150,8 +150,8 @@ final class DefaultInvoicesService: POInvoicesService { logger.debug("Deep link url \(event.url) is not supported, ignored.") return false } - let shouldResolveDeepLink = eventEmitter.hasListeners(of: POInvoiceDeepLinkResolvedEvent.self) - || eventEmitter.hasListeners(of: POInvoiceDeepLinkResolutionFailedEvent.self) + 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 @@ -161,14 +161,14 @@ final class DefaultInvoicesService: POInvoicesService { let response = try await repository.resolveUrl( request: .init(redirect: .init(result: .init(url: event.url))) ) - eventEmitter.emit(event: POInvoiceDeepLinkResolvedEvent(resolutionResponse: response)) + eventEmitter.emit(event: PONativeAlternativePaymentDeepLinkResolvedEvent(resolutionResponse: response)) } catch let error as POFailure { - eventEmitter.emit(event: POInvoiceDeepLinkResolutionFailedEvent(url: event.url, error: error)) + 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: POInvoiceDeepLinkResolutionFailedEvent(url: event.url, error: error)) + eventEmitter.emit(event: PONativeAlternativePaymentDeepLinkResolutionFailedEvent(url: event.url, error: error)) } } return true diff --git a/Sources/ProcessOut/Sources/Services/Invoices/Events/POInvoiceDeepLinkResolutionFailedEvent.swift b/Sources/ProcessOut/Sources/Services/Invoices/Events/PONativeAlternativePaymentDeepLinkResolutionFailedEvent.swift similarity index 57% rename from Sources/ProcessOut/Sources/Services/Invoices/Events/POInvoiceDeepLinkResolutionFailedEvent.swift rename to Sources/ProcessOut/Sources/Services/Invoices/Events/PONativeAlternativePaymentDeepLinkResolutionFailedEvent.swift index 4f91db982..9c708382a 100644 --- a/Sources/ProcessOut/Sources/Services/Invoices/Events/POInvoiceDeepLinkResolutionFailedEvent.swift +++ b/Sources/ProcessOut/Sources/Services/Invoices/Events/PONativeAlternativePaymentDeepLinkResolutionFailedEvent.swift @@ -1,5 +1,5 @@ // -// POInvoiceDeepLinkResolutionFailedEvent.swift +// PONativeAlternativePaymentDeepLinkResolutionFailedEvent.swift // ProcessOut // // Created by Andrii Vysotskyi on 25.03.2026. @@ -8,7 +8,7 @@ import Foundation @_spi(PO) -public struct POInvoiceDeepLinkResolutionFailedEvent: POEventEmitterEvent { +public struct PONativeAlternativePaymentDeepLinkResolutionFailedEvent: POEventEmitterEvent { /// Original deep link URL. public let url: URL diff --git a/Sources/ProcessOut/Sources/Services/Invoices/Events/POInvoiceDeepLinkResolvedEvent.swift b/Sources/ProcessOut/Sources/Services/Invoices/Events/PONativeAlternativePaymentDeepLinkResolvedEvent.swift similarity index 61% rename from Sources/ProcessOut/Sources/Services/Invoices/Events/POInvoiceDeepLinkResolvedEvent.swift rename to Sources/ProcessOut/Sources/Services/Invoices/Events/PONativeAlternativePaymentDeepLinkResolvedEvent.swift index 39458f68a..50d5b20b3 100644 --- a/Sources/ProcessOut/Sources/Services/Invoices/Events/POInvoiceDeepLinkResolvedEvent.swift +++ b/Sources/ProcessOut/Sources/Services/Invoices/Events/PONativeAlternativePaymentDeepLinkResolvedEvent.swift @@ -1,12 +1,12 @@ // -// POInvoiceDeepLinkResolvedEvent.swift +// PONativeAlternativePaymentDeepLinkResolvedEvent.swift // ProcessOut // // Created by Andrii Vysotskyi on 25.03.2026. // @_spi(PO) -public struct POInvoiceDeepLinkResolvedEvent: POEventEmitterEvent { +public struct PONativeAlternativePaymentDeepLinkResolvedEvent: POEventEmitterEvent { /// Native alternative payment URL resolution response. public let resolutionResponse: PONativeAlternativePaymentUrlResolutionResponseV2 diff --git a/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/Interactor/NativeAlternativePaymentDefaultInteractor.swift b/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/Interactor/NativeAlternativePaymentDefaultInteractor.swift index 7f5f528d4..edd1783ea 100644 --- a/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/Interactor/NativeAlternativePaymentDefaultInteractor.swift +++ b/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/Interactor/NativeAlternativePaymentDefaultInteractor.swift @@ -951,14 +951,14 @@ final class NativeAlternativePaymentDefaultInteractor: private func observeEvents() { let deepLinkResolvedEventsListener = eventEmitter.on( - POInvoiceDeepLinkResolvedEvent.self, + PONativeAlternativePaymentDeepLinkResolvedEvent.self, listener: { [weak self] event in self?.didReceive(event: event) ?? false } ) eventListeners.append(deepLinkResolvedEventsListener) let deepLinkResolutionFailedEventListener = eventEmitter.on( - POInvoiceDeepLinkResolutionFailedEvent.self, + PONativeAlternativePaymentDeepLinkResolutionFailedEvent.self, listener: { [weak self] event in self?.didReceive(event: event) ?? false } @@ -966,7 +966,7 @@ final class NativeAlternativePaymentDefaultInteractor: eventListeners.append(deepLinkResolutionFailedEventListener) } - private nonisolated func didReceive(event: POInvoiceDeepLinkResolvedEvent) -> Bool { + private nonisolated func didReceive(event: PONativeAlternativePaymentDeepLinkResolvedEvent) -> Bool { switch configuration.flow { case .authorization(let flow) where flow.invoiceId != event.resolutionResponse.payment.invoiceId: return false @@ -998,7 +998,7 @@ final class NativeAlternativePaymentDefaultInteractor: return true } - private nonisolated func didReceive(event: POInvoiceDeepLinkResolutionFailedEvent) -> Bool { + private nonisolated func didReceive(event: PONativeAlternativePaymentDeepLinkResolutionFailedEvent) -> Bool { Task { @MainActor in switch state { case .starting(let currentState): From 27357beca44f8e4808de67f43d3bc45001f99a1c Mon Sep 17 00:00:00 2001 From: Andrii Vysotskyi Date: Fri, 27 Mar 2026 13:46:18 +0100 Subject: [PATCH 06/10] Fix linter issues --- .../Services/Invoices/DefaultInvoicesService.swift | 8 ++++++-- ...eAlternativePaymentDeepLinkResolutionFailedEvent.swift | 2 +- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/Sources/ProcessOut/Sources/Services/Invoices/DefaultInvoicesService.swift b/Sources/ProcessOut/Sources/Services/Invoices/DefaultInvoicesService.swift index 5895df0bd..b2a502245 100644 --- a/Sources/ProcessOut/Sources/Services/Invoices/DefaultInvoicesService.swift +++ b/Sources/ProcessOut/Sources/Services/Invoices/DefaultInvoicesService.swift @@ -163,12 +163,16 @@ final class DefaultInvoicesService: POInvoicesService { ) eventEmitter.emit(event: PONativeAlternativePaymentDeepLinkResolvedEvent(resolutionResponse: response)) } catch let error as POFailure { - eventEmitter.emit(event: PONativeAlternativePaymentDeepLinkResolutionFailedEvent(url: event.url, error: error)) + 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)) + eventEmitter.emit( + event: PONativeAlternativePaymentDeepLinkResolutionFailedEvent(url: event.url, error: error) + ) } } return true diff --git a/Sources/ProcessOut/Sources/Services/Invoices/Events/PONativeAlternativePaymentDeepLinkResolutionFailedEvent.swift b/Sources/ProcessOut/Sources/Services/Invoices/Events/PONativeAlternativePaymentDeepLinkResolutionFailedEvent.swift index 9c708382a..be76e0985 100644 --- a/Sources/ProcessOut/Sources/Services/Invoices/Events/PONativeAlternativePaymentDeepLinkResolutionFailedEvent.swift +++ b/Sources/ProcessOut/Sources/Services/Invoices/Events/PONativeAlternativePaymentDeepLinkResolutionFailedEvent.swift @@ -7,7 +7,7 @@ import Foundation -@_spi(PO) +@_spi(PO) // swiftlint:disable:next type_name public struct PONativeAlternativePaymentDeepLinkResolutionFailedEvent: POEventEmitterEvent { /// Original deep link URL. From 89fc2e99a05d6d511365aa29737cd307102d6187 Mon Sep 17 00:00:00 2001 From: Andrii Vysotskyi Date: Mon, 30 Mar 2026 10:23:04 +0200 Subject: [PATCH 07/10] Fix query items precondition --- .../Sources/Services/Invoices/DefaultInvoicesService.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ProcessOut/Sources/Services/Invoices/DefaultInvoicesService.swift b/Sources/ProcessOut/Sources/Services/Invoices/DefaultInvoicesService.swift index b2a502245..b403c6508 100644 --- a/Sources/ProcessOut/Sources/Services/Invoices/DefaultInvoicesService.swift +++ b/Sources/ProcessOut/Sources/Services/Invoices/DefaultInvoicesService.swift @@ -146,7 +146,7 @@ final class DefaultInvoicesService: POInvoicesService { } private func didReceive(deepLinkEvent event: PODeepLinkReceivedEvent) -> Bool { - guard event.url.queryParameters?.keys.contains("po_token") ?? false else { + guard event.url.queryParameters?.keys.contains(where: { $0.starts(with: "processout") }) ?? false else { logger.debug("Deep link url \(event.url) is not supported, ignored.") return false } From fd2cc1ad886da5eab058e08124bc35234155ebca Mon Sep 17 00:00:00 2001 From: Andrii Vysotskyi Date: Mon, 30 Mar 2026 15:39:23 +0200 Subject: [PATCH 08/10] Update API --- ...lternativePaymentUrlResolutionResponseV2.swift | 15 ++++++--------- .../PONativeAlternativePaymentInvoiceV2.swift | 3 +++ ...ativeAlternativePaymentDefaultInteractor.swift | 4 ++-- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/Sources/ProcessOut/Sources/Repositories/Invoices/Responses/AlternativePaymentV2/PONativeAlternativePaymentUrlResolutionResponseV2.swift b/Sources/ProcessOut/Sources/Repositories/Invoices/Responses/AlternativePaymentV2/PONativeAlternativePaymentUrlResolutionResponseV2.swift index 9a59b4fbf..9b246a9c8 100644 --- a/Sources/ProcessOut/Sources/Repositories/Invoices/Responses/AlternativePaymentV2/PONativeAlternativePaymentUrlResolutionResponseV2.swift +++ b/Sources/ProcessOut/Sources/Repositories/Invoices/Responses/AlternativePaymentV2/PONativeAlternativePaymentUrlResolutionResponseV2.swift @@ -10,13 +10,10 @@ import Foundation @_spi(PO) public struct PONativeAlternativePaymentUrlResolutionResponseV2: Sendable, Decodable { - public struct Payment: Sendable, Decodable { + public struct CustomerToken: Sendable, Decodable { - /// Invoice ID. - public let invoiceId: POInvoice.ID - - /// Customer token ID if any. - public let customerTokenId: POCustomerToken.ID? + /// Customer token ID. + public let id: POCustomerToken.ID } /// Payment state. @@ -28,12 +25,12 @@ public struct PONativeAlternativePaymentUrlResolutionResponseV2: Sendable, Decod /// Invoice information if available. public let invoice: PONativeAlternativePaymentInvoiceV2? + /// Customer token information if any. + public let customerToken: CustomerToken? + /// UI elements to display to user. public let elements: [PONativeAlternativePaymentElementV2]? /// Redirect details. public let redirect: PONativeAlternativePaymentRedirectV2? - - /// Payment information. - public let payment: Payment } diff --git a/Sources/ProcessOut/Sources/Repositories/Shared/Responses/AlternativePaymentV2/PONativeAlternativePaymentInvoiceV2.swift b/Sources/ProcessOut/Sources/Repositories/Shared/Responses/AlternativePaymentV2/PONativeAlternativePaymentInvoiceV2.swift index 4ad473c69..296b7b80c 100644 --- a/Sources/ProcessOut/Sources/Repositories/Shared/Responses/AlternativePaymentV2/PONativeAlternativePaymentInvoiceV2.swift +++ b/Sources/ProcessOut/Sources/Repositories/Shared/Responses/AlternativePaymentV2/PONativeAlternativePaymentInvoiceV2.swift @@ -10,6 +10,9 @@ import Foundation /// Native alternative payment invoice information. public struct PONativeAlternativePaymentInvoiceV2: Sendable, Decodable { + /// String value that uniquely identifies this invoice. + public let id: POInvoice.ID + /// Invoice amount. @POImmutableStringCodableDecimal public var amount: Decimal diff --git a/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/Interactor/NativeAlternativePaymentDefaultInteractor.swift b/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/Interactor/NativeAlternativePaymentDefaultInteractor.swift index edd1783ea..883599ee4 100644 --- a/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/Interactor/NativeAlternativePaymentDefaultInteractor.swift +++ b/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/Interactor/NativeAlternativePaymentDefaultInteractor.swift @@ -968,9 +968,9 @@ final class NativeAlternativePaymentDefaultInteractor: private nonisolated func didReceive(event: PONativeAlternativePaymentDeepLinkResolvedEvent) -> Bool { switch configuration.flow { - case .authorization(let flow) where flow.invoiceId != event.resolutionResponse.payment.invoiceId: + case .authorization(let flow) where flow.invoiceId != event.resolutionResponse.invoice?.id: return false - case .tokenization(let flow) where flow.customerTokenId != event.resolutionResponse.payment.customerTokenId: + case .tokenization(let flow) where flow.customerTokenId != event.resolutionResponse.customerToken?.id: return false default: break From 9aee6ce5d2e20926b20a5b0a6ad8d7f0e7e18c5f Mon Sep 17 00:00:00 2001 From: Andrii Vysotskyi Date: Wed, 1 Apr 2026 14:15:38 +0200 Subject: [PATCH 09/10] Drop redundant check --- .../Sources/Services/Invoices/DefaultInvoicesService.swift | 4 ---- 1 file changed, 4 deletions(-) diff --git a/Sources/ProcessOut/Sources/Services/Invoices/DefaultInvoicesService.swift b/Sources/ProcessOut/Sources/Services/Invoices/DefaultInvoicesService.swift index b403c6508..270210ac2 100644 --- a/Sources/ProcessOut/Sources/Services/Invoices/DefaultInvoicesService.swift +++ b/Sources/ProcessOut/Sources/Services/Invoices/DefaultInvoicesService.swift @@ -146,10 +146,6 @@ final class DefaultInvoicesService: POInvoicesService { } private func didReceive(deepLinkEvent event: PODeepLinkReceivedEvent) -> Bool { - guard event.url.queryParameters?.keys.contains(where: { $0.starts(with: "processout") }) ?? false else { - logger.debug("Deep link url \(event.url) is not supported, ignored.") - return false - } let shouldResolveDeepLink = eventEmitter.hasListeners(of: PONativeAlternativePaymentDeepLinkResolvedEvent.self) || eventEmitter.hasListeners(of: PONativeAlternativePaymentDeepLinkResolutionFailedEvent.self) guard shouldResolveDeepLink else { From bdffa7eb220eccde8c3f7ddf326f1e227128c2d2 Mon Sep 17 00:00:00 2001 From: Andrii Vysotskyi Date: Wed, 1 Apr 2026 14:17:01 +0200 Subject: [PATCH 10/10] Lower logger level --- .../Interactor/NativeAlternativePaymentDefaultInteractor.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/Interactor/NativeAlternativePaymentDefaultInteractor.swift b/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/Interactor/NativeAlternativePaymentDefaultInteractor.swift index 883599ee4..6e4073f99 100644 --- a/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/Interactor/NativeAlternativePaymentDefaultInteractor.swift +++ b/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/Interactor/NativeAlternativePaymentDefaultInteractor.swift @@ -988,7 +988,7 @@ final class NativeAlternativePaymentDefaultInteractor: case .idle, .started, .awaitingRedirect, .awaitingCompletion: try await setState(with: adapterResponse) default: - logger.warn("Unable to apply resolve deep link to current state: \(state).") + logger.info("Unable to apply resolve deep link to current state: \(state).") return } } catch {