From 031fab8458923ff0ffbf17e977764cdbd74f92ce Mon Sep 17 00:00:00 2001 From: Andrii Vysotskyi Date: Fri, 2 Jan 2026 16:44:07 +0100 Subject: [PATCH 1/2] Support deep links --- ...ernativePaymentTokenizationRequestV2.swift | 5 ++ ...rnativePaymentAuthorizationRequestV2.swift | 7 ++- ...veAlternativePaymentRedirectResultV2.swift | 16 ++++++ ...PONativeAlternativePaymentRedirectV2.swift | 34 +++++++++++++ ...tiveAlternativePaymentServiceAdapter.swift | 2 + ...ernativePaymentServiceAdapterRequest.swift | 5 ++ ...eAlternativePaymentDefaultInteractor.swift | 50 +++++++++++++------ 7 files changed, 104 insertions(+), 15 deletions(-) create mode 100644 Sources/ProcessOut/Sources/Repositories/Shared/Requests/AlternativePaymentV2/PONativeAlternativePaymentRedirectResultV2.swift diff --git a/Sources/ProcessOut/Sources/Repositories/CustomerTokens/Requests/PONativeAlternativePaymentTokenizationRequestV2.swift b/Sources/ProcessOut/Sources/Repositories/CustomerTokens/Requests/PONativeAlternativePaymentTokenizationRequestV2.swift index e75101fb1..ef6fcc1fa 100644 --- a/Sources/ProcessOut/Sources/Repositories/CustomerTokens/Requests/PONativeAlternativePaymentTokenizationRequestV2.swift +++ b/Sources/ProcessOut/Sources/Repositories/CustomerTokens/Requests/PONativeAlternativePaymentTokenizationRequestV2.swift @@ -19,6 +19,9 @@ public struct PONativeAlternativePaymentTokenizationRequestV2: Sendable, Encodab /// Payment request parameters. public let submitData: PONativeAlternativePaymentSubmitDataV2? + /// Redirect result. + public let redirect: PONativeAlternativePaymentRedirectResultV2? + /// Customer's locale identifier override. @POExcludedEncodable public private(set) var localeIdentifier: String? @@ -28,12 +31,14 @@ public struct PONativeAlternativePaymentTokenizationRequestV2: Sendable, Encodab customerTokenId: String, gatewayConfigurationId: String, submitData: PONativeAlternativePaymentSubmitDataV2? = nil, + redirect: PONativeAlternativePaymentRedirectResultV2? = nil, localeIdentifier: String? = nil ) { self.customerId = customerId self.customerTokenId = customerTokenId self.gatewayConfigurationId = gatewayConfigurationId 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 e623fca4e..6f20b1655 100644 --- a/Sources/ProcessOut/Sources/Repositories/Invoices/Requests/AlternativePaymentV2/PONativeAlternativePaymentAuthorizationRequestV2.swift +++ b/Sources/ProcessOut/Sources/Repositories/Invoices/Requests/AlternativePaymentV2/PONativeAlternativePaymentAuthorizationRequestV2.swift @@ -19,6 +19,9 @@ public struct PONativeAlternativePaymentAuthorizationRequestV2: Sendable, Encoda /// Payment request parameters. public let submitData: PONativeAlternativePaymentSubmitDataV2? + /// Redirect result. + public let redirect: PONativeAlternativePaymentRedirectResultV2? + /// Customer's locale identifier override. @POExcludedEncodable public private(set) var localeIdentifier: String? @@ -27,13 +30,15 @@ public struct PONativeAlternativePaymentAuthorizationRequestV2: Sendable, Encoda invoiceId: String, gatewayConfigurationId: String, source: String? = nil, - submitData: PONativeAlternativePaymentSubmitDataV2?, + submitData: PONativeAlternativePaymentSubmitDataV2? = nil, + redirect: PONativeAlternativePaymentRedirectResultV2? = nil, localeIdentifier: String? = nil ) { self.invoiceId = invoiceId self.gatewayConfigurationId = gatewayConfigurationId self.source = source self.submitData = submitData + self.redirect = redirect self.localeIdentifier = localeIdentifier } } diff --git a/Sources/ProcessOut/Sources/Repositories/Shared/Requests/AlternativePaymentV2/PONativeAlternativePaymentRedirectResultV2.swift b/Sources/ProcessOut/Sources/Repositories/Shared/Requests/AlternativePaymentV2/PONativeAlternativePaymentRedirectResultV2.swift new file mode 100644 index 000000000..af6aae0ac --- /dev/null +++ b/Sources/ProcessOut/Sources/Repositories/Shared/Requests/AlternativePaymentV2/PONativeAlternativePaymentRedirectResultV2.swift @@ -0,0 +1,16 @@ +// +// PONativeAlternativePaymentRedirectResultV2.swift +// ProcessOut +// +// Created by Andrii Vysotskyi on 23.12.2025. +// + +public struct PONativeAlternativePaymentRedirectResultV2: Sendable, Encodable { + + public init(success: Bool) { + self.success = success + } + + /// indicates whether customer was redirected successfully. + public let success: Bool +} diff --git a/Sources/ProcessOut/Sources/Repositories/Shared/Responses/AlternativePaymentV2/PONativeAlternativePaymentRedirectV2.swift b/Sources/ProcessOut/Sources/Repositories/Shared/Responses/AlternativePaymentV2/PONativeAlternativePaymentRedirectV2.swift index 378b27302..88c06c186 100644 --- a/Sources/ProcessOut/Sources/Repositories/Shared/Responses/AlternativePaymentV2/PONativeAlternativePaymentRedirectV2.swift +++ b/Sources/ProcessOut/Sources/Repositories/Shared/Responses/AlternativePaymentV2/PONativeAlternativePaymentRedirectV2.swift @@ -10,9 +10,43 @@ import Foundation /// Redirect details. public struct PONativeAlternativePaymentRedirectV2: Decodable, Sendable { + public struct RedirectType: Hashable, RawRepresentable, Sendable { + + public init(rawValue: String) { + self.rawValue = rawValue + } + + /// Raw value. + public let rawValue: String + } + /// Destination URL. public let url: URL /// Display hint describing redirect purpose. public let hint: String + + /// Redirect type. + public let type: RedirectType + + /// Boolean value indicating whether backend expects redirect confirmation after customer + /// is redirected to url. + public let redirectConfirmationRequired: Bool +} + +extension PONativeAlternativePaymentRedirectV2.RedirectType: Decodable { + + public init(from decoder: any Decoder) throws { + let container = try decoder.singleValueContainer() + rawValue = try container.decode(String.self) + } +} + +extension PONativeAlternativePaymentRedirectV2.RedirectType { + + /// Web redirect. + public static let web = Self(rawValue: "web") + + /// Deep link redirect. + public static let deepLink = Self(rawValue: "deep_link") } diff --git a/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/Interactor/Adapter/DefaultNativeAlternativePaymentServiceAdapter.swift b/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/Interactor/Adapter/DefaultNativeAlternativePaymentServiceAdapter.swift index 38ad95676..616edef8a 100644 --- a/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/Interactor/Adapter/DefaultNativeAlternativePaymentServiceAdapter.swift +++ b/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/Interactor/Adapter/DefaultNativeAlternativePaymentServiceAdapter.swift @@ -32,6 +32,7 @@ final class DefaultNativeAlternativePaymentServiceAdapter: NativeAlternativePaym gatewayConfigurationId: flow.gatewayConfigurationId, source: flow.customerTokenId, submitData: request.submitData, + redirect: request.redirect, localeIdentifier: request.localeIdentifier ) let authorizationResponse = try await invoicesService.authorizeInvoice(request: authorizationRequest) @@ -42,6 +43,7 @@ final class DefaultNativeAlternativePaymentServiceAdapter: NativeAlternativePaym customerTokenId: flow.customerTokenId, gatewayConfigurationId: flow.gatewayConfigurationId, submitData: request.submitData, + redirect: request.redirect, localeIdentifier: request.localeIdentifier ) let tokenizationResponse = try await tokensService.tokenize(request: tokenizationRequest) diff --git a/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/Interactor/Adapter/NativeAlternativePaymentServiceAdapterRequest.swift b/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/Interactor/Adapter/NativeAlternativePaymentServiceAdapterRequest.swift index e0ca92fbd..3c9df85b5 100644 --- a/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/Interactor/Adapter/NativeAlternativePaymentServiceAdapterRequest.swift +++ b/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/Interactor/Adapter/NativeAlternativePaymentServiceAdapterRequest.swift @@ -12,10 +12,12 @@ struct NativeAlternativePaymentServiceAdapterRequest { init( flow: PONativeAlternativePaymentConfiguration.Flow, submitData: PONativeAlternativePaymentSubmitDataV2? = nil, + redirect: PONativeAlternativePaymentRedirectResultV2? = nil, localeIdentifier: String? ) { self.flow = flow self.submitData = submitData + self.redirect = redirect self.localeIdentifier = localeIdentifier } @@ -25,6 +27,9 @@ struct NativeAlternativePaymentServiceAdapterRequest { /// Data to submit if any. let submitData: PONativeAlternativePaymentSubmitDataV2? + /// Redirect result. + let redirect: PONativeAlternativePaymentRedirectResultV2? + /// Customer's locale identifier override. let localeIdentifier: String? } diff --git a/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/Interactor/NativeAlternativePaymentDefaultInteractor.swift b/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/Interactor/NativeAlternativePaymentDefaultInteractor.swift index 4dd97a80a..3d5c58084 100644 --- a/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/Interactor/NativeAlternativePaymentDefaultInteractor.swift +++ b/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/Interactor/NativeAlternativePaymentDefaultInteractor.swift @@ -130,15 +130,25 @@ final class NativeAlternativePaymentDefaultInteractor: } let task = Task { do { - let authenticationRequest = POAlternativePaymentAuthenticationRequest( - url: currentState.redirect.url, - callback: configuration.redirect.callback, - prefersEphemeralSession: configuration.redirect.prefersEphemeralSession - ) - _ = try await alternativePaymentsService.authenticate(request: authenticationRequest) + let didOpenUrl: Bool + switch currentState.redirect.type { + case .deepLink: + didOpenUrl = await UIApplication.shared.open(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.redirectConfirmationRequired ? .init(success: didOpenUrl) : nil, localeIdentifier: configuration.localization.localeOverride?.identifier ) ) @@ -214,15 +224,27 @@ final class NativeAlternativePaymentDefaultInteractor: logger.error("Attempted to handle headless redirect while not in starting state. Ignoring.") return } - let authenticationRequest = POAlternativePaymentAuthenticationRequest( - url: redirect.url, - callback: configuration.redirect.callback, - prefersEphemeralSession: configuration.redirect.prefersEphemeralSession - ) - _ = try await alternativePaymentsService.authenticate(request: authenticationRequest) - let localeIdentifier = configuration.localization.localeOverride?.identifier + let didOpenUrl: Bool + switch redirect.type { + case .deepLink: + didOpenUrl = await UIApplication.shared.open(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, localeIdentifier: localeIdentifier) + with: .init( + flow: configuration.flow, + redirect: redirect.redirectConfirmationRequired ? .init(success: didOpenUrl) : nil, + localeIdentifier: configuration.localization.localeOverride?.identifier + ) ) try await setState(with: response) } From d379dc2596c9a08e698214d06da86427cc132da7 Mon Sep 17 00:00:00 2001 From: Andrii Vysotskyi Date: Tue, 13 Jan 2026 14:15:58 +0100 Subject: [PATCH 2/2] Update props --- .../PONativeAlternativePaymentRedirectV2.swift | 2 +- .../NativeAlternativePaymentDefaultInteractor.swift | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/ProcessOut/Sources/Repositories/Shared/Responses/AlternativePaymentV2/PONativeAlternativePaymentRedirectV2.swift b/Sources/ProcessOut/Sources/Repositories/Shared/Responses/AlternativePaymentV2/PONativeAlternativePaymentRedirectV2.swift index 88c06c186..3e1f7d740 100644 --- a/Sources/ProcessOut/Sources/Repositories/Shared/Responses/AlternativePaymentV2/PONativeAlternativePaymentRedirectV2.swift +++ b/Sources/ProcessOut/Sources/Repositories/Shared/Responses/AlternativePaymentV2/PONativeAlternativePaymentRedirectV2.swift @@ -31,7 +31,7 @@ public struct PONativeAlternativePaymentRedirectV2: Decodable, Sendable { /// Boolean value indicating whether backend expects redirect confirmation after customer /// is redirected to url. - public let redirectConfirmationRequired: Bool + public let confirmationRequired: Bool } extension PONativeAlternativePaymentRedirectV2.RedirectType: Decodable { diff --git a/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/Interactor/NativeAlternativePaymentDefaultInteractor.swift b/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/Interactor/NativeAlternativePaymentDefaultInteractor.swift index 3d5c58084..f1d64029d 100644 --- a/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/Interactor/NativeAlternativePaymentDefaultInteractor.swift +++ b/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/Interactor/NativeAlternativePaymentDefaultInteractor.swift @@ -148,7 +148,7 @@ final class NativeAlternativePaymentDefaultInteractor: let response = try await serviceAdapter.continuePayment( with: .init( flow: configuration.flow, - redirect: currentState.redirect.redirectConfirmationRequired ? .init(success: didOpenUrl) : nil, + redirect: currentState.redirect.confirmationRequired ? .init(success: didOpenUrl) : nil, localeIdentifier: configuration.localization.localeOverride?.identifier ) ) @@ -242,7 +242,7 @@ final class NativeAlternativePaymentDefaultInteractor: let response = try await serviceAdapter.continuePayment( with: .init( flow: configuration.flow, - redirect: redirect.redirectConfirmationRequired ? .init(success: didOpenUrl) : nil, + redirect: redirect.confirmationRequired ? .init(success: didOpenUrl) : nil, localeIdentifier: configuration.localization.localeOverride?.identifier ) )