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/Requests/PONativeAlternativePaymentTokenizationRequestV2.swift b/Sources/ProcessOut/Sources/Repositories/CustomerTokens/Requests/PONativeAlternativePaymentTokenizationRequestV2.swift index ef6fcc1fa..392fb36a3 100644 --- a/Sources/ProcessOut/Sources/Repositories/CustomerTokens/Requests/PONativeAlternativePaymentTokenizationRequestV2.swift +++ b/Sources/ProcessOut/Sources/Repositories/CustomerTokens/Requests/PONativeAlternativePaymentTokenizationRequestV2.swift @@ -16,6 +16,12 @@ public struct PONativeAlternativePaymentTokenizationRequestV2: Sendable, Encodab /// Gateway configuration identifier. public let gatewayConfigurationId: String + /// Alternative payment configuration. + /// + /// - WARNING: Configuration is respected only with the **FIRST** request for the payment, ignored for + /// subsequent ones. + public let configuration: PONativeAlternativePaymentConfigurationV2 + /// Payment request parameters. public let submitData: PONativeAlternativePaymentSubmitDataV2? @@ -30,6 +36,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 +44,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/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/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/PONativeAlternativePaymentAuthorizationRequestV2.swift b/Sources/ProcessOut/Sources/Repositories/Invoices/Requests/AlternativePaymentV2/PONativeAlternativePaymentAuthorizationRequestV2.swift index 6f20b1655..ada325fe6 100644 --- a/Sources/ProcessOut/Sources/Repositories/Invoices/Requests/AlternativePaymentV2/PONativeAlternativePaymentAuthorizationRequestV2.swift +++ b/Sources/ProcessOut/Sources/Repositories/Invoices/Requests/AlternativePaymentV2/PONativeAlternativePaymentAuthorizationRequestV2.swift @@ -13,6 +13,12 @@ public struct PONativeAlternativePaymentAuthorizationRequestV2: Sendable, Encoda /// Gateway configuration identifier. public let gatewayConfigurationId: String + /// Alternative payment configuration. + /// + /// - WARNING: Configuration is respected only with the **FIRST** request for the payment, ignored for + /// subsequent ones. + public let configuration: PONativeAlternativePaymentConfigurationV2 + /// Payment source. public let source: String? @@ -29,6 +35,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 +43,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/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..9b246a9c8 --- /dev/null +++ b/Sources/ProcessOut/Sources/Repositories/Invoices/Responses/AlternativePaymentV2/PONativeAlternativePaymentUrlResolutionResponseV2.swift @@ -0,0 +1,36 @@ +// +// PONativeAlternativePaymentUrlResolutionResponseV2.swift +// ProcessOut +// +// Created by Andrii Vysotskyi on 27.03.2026. +// + +import Foundation + +@_spi(PO) +public struct PONativeAlternativePaymentUrlResolutionResponseV2: Sendable, Decodable { + + public struct CustomerToken: Sendable, Decodable { + + /// Customer token ID. + public let id: POCustomerToken.ID + } + + /// Payment state. + public let state: PONativeAlternativePaymentStateV2 + + /// Payment method information. + public let paymentMethod: PONativeAlternativePaymentMethodV2 + + /// 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? +} 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/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/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/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/ProcessOut/Sources/Services/Invoices/DefaultInvoicesService.swift b/Sources/ProcessOut/Sources/Services/Invoices/DefaultInvoicesService.swift index a4696d91f..270210ac2 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,46 @@ 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 new file mode 100644 index 000000000..be76e0985 --- /dev/null +++ b/Sources/ProcessOut/Sources/Services/Invoices/Events/PONativeAlternativePaymentDeepLinkResolutionFailedEvent.swift @@ -0,0 +1,18 @@ +// +// 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 new file mode 100644 index 000000000..50d5b20b3 --- /dev/null +++ b/Sources/ProcessOut/Sources/Services/Invoices/Events/PONativeAlternativePaymentDeepLinkResolvedEvent.swift @@ -0,0 +1,13 @@ +// +// 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/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/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 diff --git a/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/Interactor/NativeAlternativePaymentDefaultInteractor.swift b/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/Interactor/NativeAlternativePaymentDefaultInteractor.swift index ac06d555a..6e4073f99 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,76 @@ final class NativeAlternativePaymentDefaultInteractor: } return await UIApplication.shared.open(url, options: options) } + + // 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 + } + ) + eventListeners.append(deepLinkResolutionFailedEventListener) + } + + private nonisolated func didReceive(event: PONativeAlternativePaymentDeepLinkResolvedEvent) -> Bool { + 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 + default: + break + } + 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, .awaitingCompletion: + try await setState(with: adapterResponse) + default: + logger.info("Unable to apply resolve deep link to current state: \(state).") + return + } + } catch { + setFailureState(error: error) + } + } + return true + } + + private nonisolated func didReceive(event: PONativeAlternativePaymentDeepLinkResolutionFailedEvent) -> 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