diff --git a/Example/Example/Sources/Application/SceneDelegate.swift b/Example/Example/Sources/Application/SceneDelegate.swift index 0619074f0..5c87f8763 100644 --- a/Example/Example/Sources/Application/SceneDelegate.swift +++ b/Example/Example/Sources/Application/SceneDelegate.swift @@ -22,10 +22,4 @@ final class SceneDelegate: UIResponder, UIWindowSceneDelegate { window?.rootViewController = FeaturesBuilder().build() window?.makeKeyAndVisible() } - - func scene(_ scene: UIScene, openURLContexts urlContexts: Set) { - if let url = urlContexts.first?.url { - ProcessOut.shared.processDeepLink(url: url) - } - } } diff --git a/Example/Example/Sources/UI/Modules/AlternativePayment/Methods/AlternativePaymentMethodsBuilder.swift b/Example/Example/Sources/UI/Modules/AlternativePayment/Methods/AlternativePaymentMethodsBuilder.swift index 0e015c28d..92b3e4829 100644 --- a/Example/Example/Sources/UI/Modules/AlternativePayment/Methods/AlternativePaymentMethodsBuilder.swift +++ b/Example/Example/Sources/UI/Modules/AlternativePayment/Methods/AlternativePaymentMethodsBuilder.swift @@ -19,6 +19,7 @@ final class AlternativePaymentMethodsBuilder { let interactor = AlternativePaymentMethodsInteractor( gatewayConfigurationsRepository: ProcessOut.shared.gatewayConfigurations, invoicesService: ProcessOut.shared.invoices, + alternativePaymentsService: ProcessOut.shared.alternativePayments, filter: filter ) let router = AlternativePaymentMethodsRouter() diff --git a/Example/Example/Sources/UI/Modules/AlternativePayment/Methods/Interactor/AlternativePaymentMethodsInteractor.swift b/Example/Example/Sources/UI/Modules/AlternativePayment/Methods/Interactor/AlternativePaymentMethodsInteractor.swift index 17583eb1a..88ba3c440 100644 --- a/Example/Example/Sources/UI/Modules/AlternativePayment/Methods/Interactor/AlternativePaymentMethodsInteractor.swift +++ b/Example/Example/Sources/UI/Modules/AlternativePayment/Methods/Interactor/AlternativePaymentMethodsInteractor.swift @@ -14,10 +14,12 @@ final class AlternativePaymentMethodsInteractor: init( gatewayConfigurationsRepository: POGatewayConfigurationsRepository, invoicesService: POInvoicesService, + alternativePaymentsService: POAlternativePaymentsService, filter: POAllGatewayConfigurationsRequest.Filter? ) { self.gatewayConfigurationsRepository = gatewayConfigurationsRepository self.invoicesService = invoicesService + self.alternativePaymentsService = alternativePaymentsService self.filter = filter super.init(state: .idle) } @@ -111,6 +113,12 @@ final class AlternativePaymentMethodsInteractor: } } + func authorize(request: POAlternativePaymentAuthorizationRequest) { + Task { + try await alternativePaymentsService.authorize(request: request) + } + } + // MARK: - Private Nested Types private enum Constants { @@ -121,6 +129,7 @@ final class AlternativePaymentMethodsInteractor: private let gatewayConfigurationsRepository: POGatewayConfigurationsRepository private let invoicesService: POInvoicesService + private let alternativePaymentsService: POAlternativePaymentsService private let filter: POAllGatewayConfigurationsRequest.Filter? // MARK: - State Management diff --git a/Example/Example/Sources/UI/Modules/AlternativePayment/Methods/Interactor/AlternativePaymentMethodsInteractorType.swift b/Example/Example/Sources/UI/Modules/AlternativePayment/Methods/Interactor/AlternativePaymentMethodsInteractorType.swift index 40e1eea68..7a528041b 100644 --- a/Example/Example/Sources/UI/Modules/AlternativePayment/Methods/Interactor/AlternativePaymentMethodsInteractorType.swift +++ b/Example/Example/Sources/UI/Modules/AlternativePayment/Methods/Interactor/AlternativePaymentMethodsInteractorType.swift @@ -16,6 +16,9 @@ protocol AlternativePaymentMethodsInteractorType: InteractorType Void) + /// Authorizes alternative payment using given request. + func authorize(request: POAlternativePaymentAuthorizationRequest) + /// Loads more data if possible. func loadMore() } diff --git a/Example/Example/Sources/UI/Modules/AlternativePayment/Methods/Router/AlternativePaymentMethodsRoute.swift b/Example/Example/Sources/UI/Modules/AlternativePayment/Methods/Router/AlternativePaymentMethodsRoute.swift index 576b8988c..986668454 100644 --- a/Example/Example/Sources/UI/Modules/AlternativePayment/Methods/Router/AlternativePaymentMethodsRoute.swift +++ b/Example/Example/Sources/UI/Modules/AlternativePayment/Methods/Router/AlternativePaymentMethodsRoute.swift @@ -25,9 +25,6 @@ enum AlternativePaymentMethodsRoute: RouteType { /// Alternative payment executed natively. case nativeAlternativePayment(NativeAlternativePayment) - /// Alternative payment. - case alternativePayment(request: POAlternativePaymentMethodRequest) - /// Asks user for authorisation amount and currency. case authorizationtAmount(completion: (Decimal, String) -> Void) diff --git a/Example/Example/Sources/UI/Modules/AlternativePayment/Methods/Router/AlternativePaymentMethodsRouter.swift b/Example/Example/Sources/UI/Modules/AlternativePayment/Methods/Router/AlternativePaymentMethodsRouter.swift index 97af6bc21..fb93edaf6 100644 --- a/Example/Example/Sources/UI/Modules/AlternativePayment/Methods/Router/AlternativePaymentMethodsRouter.swift +++ b/Example/Example/Sources/UI/Modules/AlternativePayment/Methods/Router/AlternativePaymentMethodsRouter.swift @@ -36,11 +36,6 @@ final class AlternativePaymentMethodsRouter: RouterType { ) viewController.isModalInPresentation = true self.viewController?.present(viewController, animated: true) - case let .alternativePayment(request): - let session = POWebAuthenticationSession(request: request, returnUrl: Constants.returnUrl) { _ in } - Task { - _ = await session.start() - } case let .authorizationtAmount(completion): let viewController = AuthorizationAmountBuilder(completion: completion).build() self.viewController?.present(viewController, animated: true) diff --git a/Example/Example/Sources/UI/Modules/AlternativePayment/Methods/ViewModel/AlternativePaymentMethodsViewModel.swift b/Example/Example/Sources/UI/Modules/AlternativePayment/Methods/ViewModel/AlternativePaymentMethodsViewModel.swift index 4cd287012..6df0d5258 100644 --- a/Example/Example/Sources/UI/Modules/AlternativePayment/Methods/ViewModel/AlternativePaymentMethodsViewModel.swift +++ b/Example/Example/Sources/UI/Modules/AlternativePayment/Methods/ViewModel/AlternativePaymentMethodsViewModel.swift @@ -130,12 +130,12 @@ final class AlternativePaymentMethodsViewModel: route = .nativeAlternativePayment(paymentRoute) } else { route = AlternativePaymentMethodsRoute.additionalData { [weak self] additionalData in - let request = POAlternativePaymentMethodRequest( + let request = POAlternativePaymentAuthorizationRequest( invoiceId: invoice.id, gatewayConfigurationId: gatewayConfiguration.id, additionalData: additionalData ) - self?.router.trigger(route: .alternativePayment(request: request)) + self?.interactor.authorize(request: request) } } self?.router.trigger(route: route) diff --git a/Example/Example/Sources/UI/Modules/Features/ViewModel/FeaturesViewModel.swift b/Example/Example/Sources/UI/Modules/Features/ViewModel/FeaturesViewModel.swift index 3ed8d4511..670a3b85e 100644 --- a/Example/Example/Sources/UI/Modules/Features/ViewModel/FeaturesViewModel.swift +++ b/Example/Example/Sources/UI/Modules/Features/ViewModel/FeaturesViewModel.swift @@ -102,7 +102,6 @@ final class FeaturesViewModel: BaseViewModel, FeaturesVi } let configuration = PODynamicCheckoutConfiguration( invoiceRequest: .init(invoiceId: invoice.id, clientSecret: invoice.clientSecret), - alternativePayment: .init(returnUrl: Constants.returnUrl), cancelButton: .init(confirmation: .init()) ) self.router.trigger(route: .dynamicCheckout(configuration: configuration, delegate: self)) diff --git a/Sources/ProcessOut/Sources/Api/Models/PODeepLinkReceivedEvent.swift b/Sources/ProcessOut/Sources/Api/Models/PODeepLinkReceivedEvent.swift deleted file mode 100644 index 6a474d5da..000000000 --- a/Sources/ProcessOut/Sources/Api/Models/PODeepLinkReceivedEvent.swift +++ /dev/null @@ -1,15 +0,0 @@ -// -// PODeepLinkReceivedEvent.swift -// ProcessOut -// -// Created by Andrii Vysotskyi on 10.05.2023. -// - -import Foundation - -@_spi(PO) -public struct PODeepLinkReceivedEvent: POEventEmitterEvent { - - /// Url representing deep link or universal link. - public let url: URL -} diff --git a/Sources/ProcessOut/Sources/Api/ProcessOut.swift b/Sources/ProcessOut/Sources/Api/ProcessOut.swift index a6d80fa7e..54c782b68 100644 --- a/Sources/ProcessOut/Sources/Api/ProcessOut.swift +++ b/Sources/ProcessOut/Sources/Api/ProcessOut.swift @@ -28,8 +28,8 @@ public final class ProcessOut: @unchecked Sendable { /// Invoices service. public private(set) var invoices: POInvoicesService! - /// Alternative payment methods service. - public private(set) var alternativePaymentMethods: POAlternativePaymentMethodsService! + /// Alternative payments service. + public private(set) var alternativePayments: POAlternativePaymentsService! /// Cards service. public private(set) var cards: POCardsService! @@ -37,26 +37,12 @@ public final class ProcessOut: @unchecked Sendable { /// Returns customer tokens service. public private(set) var customerTokens: POCustomerTokensService! - /// Call this method in your app or scene delegate whenever your implementation receives incoming URL. Only deep - /// links are supported. - /// - /// - Returns: `true` if the URL is expected and will be handled by SDK. `false` otherwise. - @discardableResult - public func processDeepLink(url: URL) -> Bool { - logger.debug("Will process deep link: \(url)") - return eventEmitter.emit(event: PODeepLinkReceivedEvent(url: url)) - } - // MARK: - SPI /// Logger with application category. @_spi(PO) public private(set) var logger: POLogger! - /// Event emitter to use for events exchange. - @_spi(PO) - public private(set) var eventEmitter: POEventEmitter! - /// Images repository. @_spi(PO) public let images: POImagesRepository = UrlSessionImagesRepository(session: .shared) @@ -107,14 +93,13 @@ public final class ProcessOut: @unchecked Sendable { invoices = Self.createInvoicesService( httpConnector: httpConnector, threeDSService: threeDSService, logger: logger ) - alternativePaymentMethods = createAlternativePaymentsService() + alternativePayments = createAlternativePaymentsService() cards = Self.createCardsService( httpConnector: httpConnector, logger: logger ) customerTokens = Self.createCustomerTokensService( httpConnector: httpConnector, threeDSService: threeDSService, logger: logger ) - eventEmitter = LocalEventEmitter(logger: logger) } // MARK: - @@ -145,21 +130,24 @@ public final class ProcessOut: @unchecked Sendable { return DefaultCustomerTokensService(repository: repository, threeDSService: threeDSService, logger: logger) } - private func createAlternativePaymentsService() -> POAlternativePaymentMethodsService { - let serviceConfiguration = { @Sendable [unowned self] () -> AlternativePaymentMethodsServiceConfiguration in + private func createAlternativePaymentsService() -> POAlternativePaymentsService { + let serviceConfiguration = { @Sendable [unowned self] () -> AlternativePaymentsServiceConfiguration in let configuration = self.configuration return .init(projectId: configuration.projectId, baseUrl: configuration.checkoutBaseUrl) } - return DefaultAlternativePaymentMethodsService(configuration: serviceConfiguration, logger: logger) + let webSession = WebAuthenticationSession() + return DefaultAlternativePaymentsService( + configuration: serviceConfiguration, webSession: webSession, logger: logger + ) } private static func create3DSService() -> DefaultThreeDSService { - let webSession = WebAuthenticationSession() let decoder = JSONDecoder() decoder.keyDecodingStrategy = .useDefaultKeys let encoder = JSONEncoder() encoder.dataEncodingStrategy = .base64 encoder.keyEncodingStrategy = .useDefaultKeys + let webSession = WebAuthenticationSession() return DefaultThreeDSService(decoder: decoder, encoder: encoder, webSession: webSession) } diff --git a/Sources/ProcessOut/Sources/Core/EventEmitter/LocalEventEmitter.swift b/Sources/ProcessOut/Sources/Core/EventEmitter/LocalEventEmitter.swift deleted file mode 100644 index 4f51bf8cb..000000000 --- a/Sources/ProcessOut/Sources/Core/EventEmitter/LocalEventEmitter.swift +++ /dev/null @@ -1,91 +0,0 @@ -// -// LocalEventEmitter.swift -// ProcessOut -// -// Created by Andrii Vysotskyi on 10.05.2023. -// - -import Foundation - -final class LocalEventEmitter: POEventEmitter, @unchecked Sendable { - - init(logger: POLogger) { - self.logger = logger - lock = NSLock() - subscriptions = [:] - } - - // MARK: - POEventEmitter - - func emit(event: Event) -> Bool { - lock.lock() - guard let eventSubscriptions = subscriptions[Event.name]?.values, !eventSubscriptions.isEmpty else { - lock.unlock() - logger.debug("No subscribers for '\(Event.name)' event, ignored") - return false - } - lock.unlock() - var isHandled = false - for subscription in eventSubscriptions { - // Event should be delivered to all subscribers. - isHandled = subscription.listener(event) || isHandled - } - if !isHandled { - logger.debug("Subscribers refused to handle '\(Event.name)' event") - } - return isHandled - } - - func on(_ eventType: Event.Type, listener: @escaping (Event) -> Bool) -> AnyObject { - let subscription = Subscription { event in - guard let event = event as? Event else { - return false - } - return listener(event) - } - let subscriptionId = UUID().uuidString - lock.lock() - if subscriptions[Event.name] != nil { - subscriptions[Event.name]?[subscriptionId] = subscription - } else { - subscriptions[Event.name] = [subscriptionId: subscription] - } - lock.unlock() - let cancellable = Cancellable { [weak self] in - guard let self = self else { - return - } - self.lock.lock() - self.subscriptions[Event.name]?[subscriptionId] = nil - self.lock.unlock() - } - return cancellable - } - - // MARK: - Private Nested Types - - private struct Subscription { - - /// Type erased listener. - let listener: (Any) -> Bool - } - - private final class Cancellable { - - let didCancel: () -> Void - - init(didCancel: @escaping () -> Void) { - self.didCancel = didCancel - } - - deinit { - didCancel() - } - } - - // MARK: - Private Properties - - private let logger: POLogger - private let lock: NSLock - private var subscriptions: [String: [AnyHashable: Subscription]] -} diff --git a/Sources/ProcessOut/Sources/Core/EventEmitter/POEventEmitter.swift b/Sources/ProcessOut/Sources/Core/EventEmitter/POEventEmitter.swift deleted file mode 100644 index 4c03b1878..000000000 --- a/Sources/ProcessOut/Sources/Core/EventEmitter/POEventEmitter.swift +++ /dev/null @@ -1,17 +0,0 @@ -// -// POEventEmitter.swift -// ProcessOut -// -// Created by Andrii Vysotskyi on 10.05.2023. -// - -import Foundation - -@_spi(PO) public protocol POEventEmitter: Sendable { - - /// Emits given event. - func emit(event: Event) -> Bool - - /// Adds subscription for given event. - func on(_ eventType: Event.Type, listener: @escaping (Event) -> Bool) -> AnyObject -} diff --git a/Sources/ProcessOut/Sources/Core/EventEmitter/POEventEmitterEvent.swift b/Sources/ProcessOut/Sources/Core/EventEmitter/POEventEmitterEvent.swift deleted file mode 100644 index 1f96154df..000000000 --- a/Sources/ProcessOut/Sources/Core/EventEmitter/POEventEmitterEvent.swift +++ /dev/null @@ -1,20 +0,0 @@ -// -// POEventEmitterEvent.swift -// ProcessOut -// -// Created by Andrii Vysotskyi on 10.05.2023. -// - -@_spi(PO) -public protocol POEventEmitterEvent: Sendable { - - /// Event name. - static var name: String { get } -} - -extension POEventEmitterEvent { - - public static var name: String { - String(describing: self) - } -} diff --git a/Sources/ProcessOut/Sources/Repositories/CustomerTokens/Requests/POCreateCustomerTokenRequest.swift b/Sources/ProcessOut/Sources/Repositories/CustomerTokens/Requests/POCreateCustomerTokenRequest.swift index db3a39304..5e3da407c 100644 --- a/Sources/ProcessOut/Sources/Repositories/CustomerTokens/Requests/POCreateCustomerTokenRequest.swift +++ b/Sources/ProcessOut/Sources/Repositories/CustomerTokens/Requests/POCreateCustomerTokenRequest.swift @@ -20,9 +20,13 @@ public struct POCreateCustomerTokenRequest: Encodable, Sendable { /// Return URL to assign to verification invoice. public let invoiceReturnUrl: URL? - public init(customerId: String, verify: Bool = false, invoiceReturnUrl: URL? = nil) { + /// Return URL. + public let returnUrl: URL? + + public init(customerId: String, verify: Bool = false, returnUrl: URL? = nil) { self._customerId = .init(value: customerId) self.verify = verify - self.invoiceReturnUrl = invoiceReturnUrl + self.invoiceReturnUrl = returnUrl + self.returnUrl = returnUrl } } diff --git a/Sources/ProcessOut/Sources/Services/AlternativePaymentMethods/POAlternativePaymentMethodsService.swift b/Sources/ProcessOut/Sources/Services/AlternativePaymentMethods/POAlternativePaymentMethodsService.swift deleted file mode 100644 index fc95f9c03..000000000 --- a/Sources/ProcessOut/Sources/Services/AlternativePaymentMethods/POAlternativePaymentMethodsService.swift +++ /dev/null @@ -1,26 +0,0 @@ -// -// POAlternativePaymentMethodsService.swift -// ProcessOut -// -// Created by Simeon Kostadinov on 27/10/2022. -// - -import Foundation - -@available(*, deprecated, renamed: "POAlternativePaymentMethodsService") -public typealias POAlternativePaymentMethodsServiceType = POAlternativePaymentMethodsService - -/// Service that provides set of methods to work with alternative payments. -public protocol POAlternativePaymentMethodsService: POService { - - /// Creates the redirection URL for APM Payments and APM token creation. - /// - /// - Parameter request: request containing information needed to build the URL. - func alternativePaymentMethodUrl(request: POAlternativePaymentMethodRequest) -> URL - - /// Convert given APMs response URL into response object. - /// - /// - Parameter url: url response that our checkout service sends back when the customer gets redirected. - /// - Returns: response parsed from given url. - func alternativePaymentMethodResponse(url: URL) throws -> POAlternativePaymentMethodResponse -} diff --git a/Sources/ProcessOut/Sources/Services/AlternativePaymentMethods/Requests/POAlternativePaymentMethodRequest.swift b/Sources/ProcessOut/Sources/Services/AlternativePaymentMethods/Requests/POAlternativePaymentMethodRequest.swift deleted file mode 100644 index 20087ddfd..000000000 --- a/Sources/ProcessOut/Sources/Services/AlternativePaymentMethods/Requests/POAlternativePaymentMethodRequest.swift +++ /dev/null @@ -1,79 +0,0 @@ -// -// POAlternativePaymentMethodRequest.swift -// ProcessOut -// -// Created by Simeon Kostadinov on 27/10/2022. -// - -// todo(andrii-vysotskyi): consider splitting request into separate tokenization and authorization requests - -import Foundation - -/// Request describing parameters that are used to create URL that user can be redirected to initiate -/// alternative payment. -/// -/// - NOTE: Make sure to supply proper `additionalData` specific for particular payment -/// method. -public struct POAlternativePaymentMethodRequest: Sendable { - - /// Invoice identifier to to perform APM payment for. - public let invoiceId: String - - /// Gateway Configuration ID of the APM the payment will be made on. - public let gatewayConfigurationId: String - - /// Customer ID that may be used for creating APM recurring token. - public let customerId: String? - - /// Customer token ID that may be used for creating APM recurring token. - public let tokenId: String? - - /// Additional Data that will be supplied to the APM. - public let additionalData: [String: String]? - - @_disfavoredOverload - @available(*, deprecated, message: "Use other init that creates either tokenization or payment request explicitly.") - public init( - invoiceId: String, - gatewayConfigurationId: String, - additionalData: [String: String]? = nil, - customerId: String? = nil, - tokenId: String? = nil - ) { - self.invoiceId = invoiceId - self.gatewayConfigurationId = gatewayConfigurationId - self.additionalData = additionalData - self.customerId = customerId - self.tokenId = tokenId - } - - /// Creates a request that can be used to tokenize APM. - public init( - customerId: String, - tokenId: String, - gatewayConfigurationId: String, - additionalData: [String: String]? = nil - ) { - self.invoiceId = "" - self.customerId = customerId - self.tokenId = tokenId - self.gatewayConfigurationId = gatewayConfigurationId - self.additionalData = additionalData - } - - /// Creates a request that can be used to authorize APM. - /// - Parameters: - /// - tokenId: when value is set invoice is being authorized with previously tokenized APM. - public init( - invoiceId: String, - gatewayConfigurationId: String, - tokenId: String? = nil, - additionalData: [String: String]? = nil - ) { - self.invoiceId = invoiceId - self.gatewayConfigurationId = gatewayConfigurationId - self.customerId = nil - self.tokenId = tokenId - self.additionalData = additionalData - } -} diff --git a/Sources/ProcessOut/Sources/Services/AlternativePaymentMethods/Responses/POAlternativePaymentMethodResponse.swift b/Sources/ProcessOut/Sources/Services/AlternativePaymentMethods/Responses/POAlternativePaymentMethodResponse.swift deleted file mode 100644 index b849ad69e..000000000 --- a/Sources/ProcessOut/Sources/Services/AlternativePaymentMethods/Responses/POAlternativePaymentMethodResponse.swift +++ /dev/null @@ -1,28 +0,0 @@ -// -// POAlternativePaymentMethodResponse.swift -// ProcessOut -// -// Created by Simeon Kostadinov on 27/10/2022. -// - -import Foundation - -/// Result of alternative payment. -public struct POAlternativePaymentMethodResponse: Sendable { - - public enum APMReturnType: Sendable { - case authorization, createToken - } - - /// Gateway token starting with prefix gway_req_ that can be used to perform a sale call. - public let gatewayToken: String - - /// Customer ID that may be used for creating APM recurring token. - public let customerId: String? - - /// Customer token ID that may be used for creating APM recurring token. - public let tokenId: String? - - /// returnType informs if this was an APM token creation or a payment creation response. - public let returnType: APMReturnType -} diff --git a/Sources/ProcessOut/Sources/Services/AlternativePaymentMethods/AlternativePaymentMethodsServiceConfiguration.swift b/Sources/ProcessOut/Sources/Services/AlternativePayments/AlternativePaymentsServiceConfiguration.swift similarity index 59% rename from Sources/ProcessOut/Sources/Services/AlternativePaymentMethods/AlternativePaymentMethodsServiceConfiguration.swift rename to Sources/ProcessOut/Sources/Services/AlternativePayments/AlternativePaymentsServiceConfiguration.swift index 2b32d59c0..93915eebc 100644 --- a/Sources/ProcessOut/Sources/Services/AlternativePaymentMethods/AlternativePaymentMethodsServiceConfiguration.swift +++ b/Sources/ProcessOut/Sources/Services/AlternativePayments/AlternativePaymentsServiceConfiguration.swift @@ -1,5 +1,5 @@ // -// AlternativePaymentMethodsServiceConfiguration.swift +// AlternativePaymentsServiceConfiguration.swift // ProcessOut // // Created by Andrii Vysotskyi on 13.02.2024. @@ -7,7 +7,7 @@ import Foundation -struct AlternativePaymentMethodsServiceConfiguration: Sendable { +struct AlternativePaymentsServiceConfiguration: Sendable { /// Project ID. let projectId: String diff --git a/Sources/ProcessOut/Sources/Services/AlternativePaymentMethods/DefaultAlternativePaymentMethodsService.swift b/Sources/ProcessOut/Sources/Services/AlternativePayments/DefaultAlternativePaymentsService.swift similarity index 51% rename from Sources/ProcessOut/Sources/Services/AlternativePaymentMethods/DefaultAlternativePaymentMethodsService.swift rename to Sources/ProcessOut/Sources/Services/AlternativePayments/DefaultAlternativePaymentsService.swift index c0f4dd64d..b5548c440 100644 --- a/Sources/ProcessOut/Sources/Services/AlternativePaymentMethods/DefaultAlternativePaymentMethodsService.swift +++ b/Sources/ProcessOut/Sources/Services/AlternativePayments/DefaultAlternativePaymentsService.swift @@ -1,5 +1,5 @@ // -// DefaultAlternativePaymentMethodsService.swift +// DefaultAlternativePaymentsService.swift // ProcessOut // // Created by Simeon Kostadinov on 27/10/2022. @@ -7,41 +7,68 @@ import Foundation -final class DefaultAlternativePaymentMethodsService: POAlternativePaymentMethodsService { +final class DefaultAlternativePaymentsService: POAlternativePaymentsService { - init(configuration: @escaping @Sendable () -> AlternativePaymentMethodsServiceConfiguration, logger: POLogger) { + init( + configuration: @escaping @Sendable () -> AlternativePaymentsServiceConfiguration, + webSession: WebAuthenticationSession, + logger: POLogger + ) { self.configuration = configuration + self.webSession = webSession self.logger = logger } - // MARK: - POAlternativePaymentMethodsService + // MARK: - POAlternativePaymentsService - func alternativePaymentMethodUrl(request: POAlternativePaymentMethodRequest) -> URL { + func tokenize(request: POAlternativePaymentTokenizationRequest) async throws -> POAlternativePaymentResponse { + let pathComponents = [request.customerId, request.tokenId, "redirect", request.gatewayConfigurationId] + let redirectUrl = try url(with: pathComponents, additionalData: request.additionalData) + return try await authenticate(using: redirectUrl) + } + + func authorize(request: POAlternativePaymentAuthorizationRequest) async throws -> POAlternativePaymentResponse { + var pathComponents = [request.invoiceId, "redirect", request.gatewayConfigurationId] + if let tokenId = request.tokenId { + pathComponents += ["tokenized", tokenId] + } + let redirectUrl = try url(with: pathComponents, additionalData: request.additionalData) + return try await authenticate(using: redirectUrl) + } + + func authenticate(using url: URL) async throws -> POAlternativePaymentResponse { + let returnUrl = try await webSession.authenticate(using: url) + return try response(from: returnUrl) + } + + // MARK: - Private + + private let configuration: @Sendable () -> AlternativePaymentsServiceConfiguration + private let logger: POLogger + private let webSession: WebAuthenticationSession + + // MARK: - Request + + /// - NOTE: Method prepends project ID to path components automatically. + private func url(with additionalPathComponents: [String], additionalData: [String: String]?) throws -> URL { let configuration = self.configuration() guard var components = URLComponents(url: configuration.baseUrl, resolvingAgainstBaseURL: true) else { - preconditionFailure("Failed to create components from base url.") - } - var pathComponents: [String] - if let customerId = request.customerId, let tokenId = request.tokenId { - pathComponents = [configuration.projectId, customerId, tokenId, "redirect", request.gatewayConfigurationId] - } else { - precondition(!request.invoiceId.isEmpty, "Invoice ID must be set.") - pathComponents = [configuration.projectId, request.invoiceId, "redirect", request.gatewayConfigurationId] - if let tokenId = request.tokenId { - pathComponents += ["tokenized", tokenId] - } + preconditionFailure("Invalid base URL.") } + let pathComponents = [configuration.projectId] + additionalPathComponents components.path = "/" + pathComponents.joined(separator: "/") - components.queryItems = request.additionalData?.map { data in + components.queryItems = additionalData?.map { data in URLQueryItem(name: "additional_data[" + data.key + "]", value: data.value) } - guard let url = components.url else { - preconditionFailure("Failed to create APM redirection URL.") + if let url = components.url { + return url } - return url + throw POFailure(message: "Unable to create redirect URL.", code: .generic(.mobile)) } - func alternativePaymentMethodResponse(url: URL) throws -> POAlternativePaymentMethodResponse { + // MARK: - Response + + private func response(from url: URL) throws -> POAlternativePaymentResponse { guard let components = URLComponents(url: url, resolvingAgainstBaseURL: true) else { let message = "Invalid or malformed Alternative Payment Method URL response provided." throw POFailure(message: message, code: .generic(.mobile), underlyingError: nil) @@ -54,20 +81,9 @@ final class DefaultAlternativePaymentMethodsService: POAlternativePaymentMethods if gatewayToken.isEmpty { logger.debug("Gateway 'token' is not set in \(url), this may be an error.") } - let tokenId = queryItems.queryItemValue(name: "token_id") - if let customerId = queryItems.queryItemValue(name: "customer_id"), let tokenId { - return .init(gatewayToken: gatewayToken, customerId: customerId, tokenId: tokenId, returnType: .createToken) - } - return .init(gatewayToken: gatewayToken, customerId: nil, tokenId: tokenId, returnType: .authorization) + return .init(gatewayToken: gatewayToken) } - // MARK: - Private - - private let configuration: @Sendable () -> AlternativePaymentMethodsServiceConfiguration - private let logger: POLogger - - // MARK: - Private Methods - private func createFailureCode(rawValue: String) -> POFailure.Code { if let code = POFailure.AuthenticationCode(rawValue: rawValue) { return .authentication(code) diff --git a/Sources/ProcessOut/Sources/Services/AlternativePayments/POAlternativePaymentsService.swift b/Sources/ProcessOut/Sources/Services/AlternativePayments/POAlternativePaymentsService.swift new file mode 100644 index 000000000..65dd0990f --- /dev/null +++ b/Sources/ProcessOut/Sources/Services/AlternativePayments/POAlternativePaymentsService.swift @@ -0,0 +1,24 @@ +// +// POAlternativePaymentsService.swift +// ProcessOut +// +// Created by Simeon Kostadinov on 27/10/2022. +// + +import Foundation + +@available(*, deprecated, renamed: "POAlternativePaymentsService") +public typealias POAlternativePaymentMethodsServiceType = POAlternativePaymentsService + +/// Service that provides set of methods to work with alternative payments. +public protocol POAlternativePaymentsService: POService { + + /// Attempts to tokenize APM using given request. + func tokenize(request: POAlternativePaymentTokenizationRequest) async throws -> POAlternativePaymentResponse + + /// Authorizes invoice using given request. + func authorize(request: POAlternativePaymentAuthorizationRequest) async throws -> POAlternativePaymentResponse + + /// Authenticates alternative payment using given raw URL. + func authenticate(using url: URL) async throws -> POAlternativePaymentResponse +} diff --git a/Sources/ProcessOut/Sources/Services/AlternativePayments/Requests/POAlternativePaymentAuthorizationRequest.swift b/Sources/ProcessOut/Sources/Services/AlternativePayments/Requests/POAlternativePaymentAuthorizationRequest.swift new file mode 100644 index 000000000..91ff4692d --- /dev/null +++ b/Sources/ProcessOut/Sources/Services/AlternativePayments/Requests/POAlternativePaymentAuthorizationRequest.swift @@ -0,0 +1,38 @@ +// +// POAlternativePaymentAuthorizationRequest.swift +// ProcessOut +// +// Created by Andrii Vysotskyi on 05.08.2024. +// + +/// Invoice authorization request. +/// +/// - NOTE: Make sure to supply proper `additionalData` specific for particular payment +/// method. +public struct POAlternativePaymentAuthorizationRequest: Sendable { + + /// Invoice identifier to to perform APM payment for. + public let invoiceId: String + + /// Gateway Configuration ID of the APM the payment will be made on. + public let gatewayConfigurationId: String + + /// When value is set invoice is being authorized with previously tokenized APM. + public let tokenId: String? + + /// Additional Data that will be supplied to the APM. + public let additionalData: [String: String]? + + /// Creates authorization request. + public init( + invoiceId: String, + gatewayConfigurationId: String, + tokenId: String? = nil, + additionalData: [String: String]? = nil + ) { + self.invoiceId = invoiceId + self.gatewayConfigurationId = gatewayConfigurationId + self.tokenId = tokenId + self.additionalData = additionalData + } +} diff --git a/Sources/ProcessOut/Sources/Services/AlternativePayments/Requests/POAlternativePaymentTokenizationRequest.swift b/Sources/ProcessOut/Sources/Services/AlternativePayments/Requests/POAlternativePaymentTokenizationRequest.swift new file mode 100644 index 000000000..eccf408c9 --- /dev/null +++ b/Sources/ProcessOut/Sources/Services/AlternativePayments/Requests/POAlternativePaymentTokenizationRequest.swift @@ -0,0 +1,38 @@ +// +// POAlternativePaymentTokenizationRequest.swift +// ProcessOut +// +// Created by Andrii Vysotskyi on 05.08.2024. +// + +/// APM tokenization request. +/// +/// - NOTE: Make sure to supply proper `additionalData` specific for particular payment +/// method. +public struct POAlternativePaymentTokenizationRequest: Sendable { + + /// Customer ID that may be used for creating APM recurring token. + public let customerId: String + + /// Customer token ID that may be used for creating APM recurring token. + public let tokenId: String + + /// Gateway Configuration ID of the APM the payment will be made on. + public let gatewayConfigurationId: String + + /// Additional data that will be supplied to the APM. + public let additionalData: [String: String]? + + /// Creates tokenization request. + public init( + customerId: String, + tokenId: String, + gatewayConfigurationId: String, + additionalData: [String: String]? = nil + ) { + self.customerId = customerId + self.tokenId = tokenId + self.gatewayConfigurationId = gatewayConfigurationId + self.additionalData = additionalData + } +} diff --git a/Sources/ProcessOut/Sources/Services/AlternativePayments/Responses/POAlternativePaymentResponse.swift b/Sources/ProcessOut/Sources/Services/AlternativePayments/Responses/POAlternativePaymentResponse.swift new file mode 100644 index 000000000..3097180b8 --- /dev/null +++ b/Sources/ProcessOut/Sources/Services/AlternativePayments/Responses/POAlternativePaymentResponse.swift @@ -0,0 +1,19 @@ +// +// POAlternativePaymentResponse.swift +// ProcessOut +// +// Created by Simeon Kostadinov on 27/10/2022. +// + +import Foundation + +/// Generic alternative payment response. +public struct POAlternativePaymentResponse: Sendable { + + /// Represents a gateway token. + /// + /// - Authorization: The token can be used to capture the payment on your server. + /// - Tokenization: The token is a gateway request token, which can only be used to + /// generate the eventual customer token. It should not be used as a payment source. + public let gatewayToken: String +} diff --git a/Sources/ProcessOutUI/Sources/Modules/AlternativePayment/POWebAuthenticationSession+AlternativePayment.swift b/Sources/ProcessOutUI/Sources/Modules/AlternativePayment/POWebAuthenticationSession+AlternativePayment.swift deleted file mode 100644 index 8e3127e70..000000000 --- a/Sources/ProcessOutUI/Sources/Modules/AlternativePayment/POWebAuthenticationSession+AlternativePayment.swift +++ /dev/null @@ -1,54 +0,0 @@ -// -// POWebAuthenticationSession+AlternativePayment.swift -// ProcessOutUI -// -// Created by Andrii Vysotskyi on 18.03.2024. -// - -import Foundation -import ProcessOut - -extension POWebAuthenticationSession { - - /// Creates session that is capable of handling alternative payment. - /// - /// - Parameters: - /// - request: Alternative payment request. - /// - returnUrl: Return URL specified when creating invoice. - /// - completion: Completion to invoke when APM flow completes. - public convenience init( - request: POAlternativePaymentMethodRequest, - returnUrl: URL, - completion: @escaping @Sendable (Result) -> Void - ) { - let url = ProcessOut.shared.alternativePaymentMethods.alternativePaymentMethodUrl(request: request) - self.init(alternativePaymentMethodUrl: url, returnUrl: returnUrl, completion: completion) - } - - /// Creates session that is capable of handling alternative payment. - /// - /// - Parameters: - /// - url: initial URL instead of **request**. Implementation does not validate - /// whether given value is valid to actually start APM flow. - /// - returnUrl: Return URL specified when creating invoice. - /// - completion: Completion to invoke when APM flow completes. - public convenience init( - alternativePaymentMethodUrl url: URL, - returnUrl: URL, - completion: @escaping @Sendable (Result) -> Void - ) { - let completionBox: Completion = { result in - completion(result.flatMap(Self.response)) - } - self.init(url: url, callback: .customScheme(returnUrl.scheme ?? ""), completion: completionBox) - } - - // MARK: - Private Methods - - private static nonisolated func response(with url: URL) -> Result { - let result = Result { - try ProcessOut.shared.alternativePaymentMethods.alternativePaymentMethodResponse(url: url) - } - return result.mapError { $0 as! POFailure } // swiftlint:disable:this force_cast - } -} diff --git a/Sources/ProcessOutUI/Sources/Modules/AlternativePayment/SFSafariViewController+AlternativePayment.swift b/Sources/ProcessOutUI/Sources/Modules/AlternativePayment/SFSafariViewController+AlternativePayment.swift deleted file mode 100644 index fd53eb53d..000000000 --- a/Sources/ProcessOutUI/Sources/Modules/AlternativePayment/SFSafariViewController+AlternativePayment.swift +++ /dev/null @@ -1,76 +0,0 @@ -// -// SFSafariViewController+AlternativePayment.swift -// ProcessOutUI -// -// Created by Andrii Vysotskyi on 17.11.2023. -// - -import Foundation -import SafariServices -@_spi(PO) import ProcessOut - -extension SFSafariViewController { - - /// Creates view controller that is capable of handling Alternative Payment. - /// - /// - Note: Caller should dismiss view controller after completion is called. - /// - Note: Object's delegate shouldn't be modified. - /// - /// - Parameters: - /// - returnUrl: Return URL specified when creating invoice. - /// - safariConfiguration: The configuration for the new view controller. - /// - completion: Completion to invoke when APM flow completes. - public convenience init( - request: POAlternativePaymentMethodRequest, - returnUrl: URL, - safariConfiguration: SFSafariViewController.Configuration = Configuration(), - completion: @escaping @Sendable (Result) -> Void - ) { - let url = ProcessOut.shared.alternativePaymentMethods.alternativePaymentMethodUrl(request: request) - self.init( - alternativePaymentMethodUrl: url, - returnUrl: returnUrl, - safariConfiguration: safariConfiguration, - completion: completion - ) - } - - /// Creates view controller that is capable of handling Alternative Payment. - /// - /// - Note: Caller should dismiss view controller after completion is called. - /// - Note: Object's delegate shouldn't be modified. - /// - /// - Parameters: - /// - url: initial URL instead of **request**. Implementation does not validate - /// whether given value is valid to actually start APM flow. - /// - returnUrl: Return URL specified when creating invoice. - /// - safariConfiguration: The configuration for the new view controller. - /// - completion: Completion to invoke when APM flow completes. - public convenience init( - alternativePaymentMethodUrl url: URL, - returnUrl: URL, - safariConfiguration: SFSafariViewController.Configuration = Configuration(), - completion: @escaping @Sendable (Result) -> Void - ) { - self.init(url: url, configuration: safariConfiguration) - let viewModel = DefaultSafariViewModel( - callback: .customScheme(returnUrl.scheme ?? ""), - eventEmitter: ProcessOut.shared.eventEmitter, - logger: ProcessOut.shared.logger, - completion: { result in - completion(result.flatMap(Self.response)) - } - ) - self.setViewModel(viewModel) - viewModel.start() - } - - // MARK: - Private Methods - - private nonisolated static func response(with url: URL) -> Result { - let result = Result { - try ProcessOut.shared.alternativePaymentMethods.alternativePaymentMethodResponse(url: url) - } - return result.mapError { $0 as! POFailure } // swiftlint:disable:this force_cast - } -} diff --git a/Sources/ProcessOutUI/Sources/Modules/DynamicCheckout/Configuration/PODynamicCheckoutAlternativePaymentConfiguration.swift b/Sources/ProcessOutUI/Sources/Modules/DynamicCheckout/Configuration/PODynamicCheckoutAlternativePaymentConfiguration.swift index 5d92b2d49..8ac943bbd 100644 --- a/Sources/ProcessOutUI/Sources/Modules/DynamicCheckout/Configuration/PODynamicCheckoutAlternativePaymentConfiguration.swift +++ b/Sources/ProcessOutUI/Sources/Modules/DynamicCheckout/Configuration/PODynamicCheckoutAlternativePaymentConfiguration.swift @@ -59,9 +59,6 @@ public struct PODynamicCheckoutAlternativePaymentConfiguration: Sendable { } } - /// Return URL to expect when handling OOB or web based payments. - public let returnUrl: URL? - /// For parameters where user should select single option from multiple values defines /// maximum number of options that framework will display inline (e.g. using radio buttons). /// @@ -72,12 +69,7 @@ public struct PODynamicCheckoutAlternativePaymentConfiguration: Sendable { public let paymentConfirmation: PaymentConfirmation /// Creates configuration. - public init( - returnUrl: URL? = nil, - inlineSingleSelectValuesLimit: Int = 5, - paymentConfirmation: PaymentConfirmation = .init() - ) { - self.returnUrl = returnUrl + public init(inlineSingleSelectValuesLimit: Int = 5, paymentConfirmation: PaymentConfirmation = .init()) { self.inlineSingleSelectValuesLimit = inlineSingleSelectValuesLimit self.paymentConfirmation = paymentConfirmation } diff --git a/Sources/ProcessOutUI/Sources/Modules/DynamicCheckout/Interactor/DynamicCheckout/DynamicCheckoutDefaultInteractor.swift b/Sources/ProcessOutUI/Sources/Modules/DynamicCheckout/Interactor/DynamicCheckout/DynamicCheckoutDefaultInteractor.swift index 70257cbf9..e27accac9 100644 --- a/Sources/ProcessOutUI/Sources/Modules/DynamicCheckout/Interactor/DynamicCheckout/DynamicCheckoutDefaultInteractor.swift +++ b/Sources/ProcessOutUI/Sources/Modules/DynamicCheckout/Interactor/DynamicCheckout/DynamicCheckoutDefaultInteractor.swift @@ -19,18 +19,18 @@ final class DynamicCheckoutDefaultInteractor: configuration: PODynamicCheckoutConfiguration, delegate: PODynamicCheckoutDelegate?, passKitPaymentSession: DynamicCheckoutPassKitPaymentSession, - alternativePaymentSession: DynamicCheckoutAlternativePaymentSession, childProvider: DynamicCheckoutInteractorChildProvider, invoicesService: POInvoicesService, + alternativePaymentsService: POAlternativePaymentsService, logger: POLogger, completion: @escaping (Result) -> Void ) { self.configuration = configuration self.delegate = delegate self.passKitPaymentSession = passKitPaymentSession - self.alternativePaymentSession = alternativePaymentSession self.childProvider = childProvider self.invoicesService = invoicesService + self.alternativePaymentsService = alternativePaymentsService self.logger = logger self.completion = completion super.init(state: .idle) @@ -138,7 +138,7 @@ final class DynamicCheckoutDefaultInteractor: // MARK: - Private Properties private let passKitPaymentSession: DynamicCheckoutPassKitPaymentSession - private let alternativePaymentSession: DynamicCheckoutAlternativePaymentSession + private let alternativePaymentsService: POAlternativePaymentsService private let childProvider: DynamicCheckoutInteractorChildProvider private let invoicesService: POInvoicesService private let completion: (Result) -> Void @@ -431,7 +431,7 @@ final class DynamicCheckoutDefaultInteractor: state = .paymentProcessing(paymentProcessingState) Task { do { - _ = try await alternativePaymentSession.start(url: method.configuration.redirectUrl) + _ = try await alternativePaymentsService.authenticate(using: method.configuration.redirectUrl) setSuccessState() } catch { recoverPaymentProcessing(error: error) @@ -523,7 +523,7 @@ final class DynamicCheckoutDefaultInteractor: Task { @MainActor in do { if let redirectUrl = method.configuration.redirectUrl { - _ = try await alternativePaymentSession.start(url: redirectUrl) + _ = try await alternativePaymentsService.authenticate(using: redirectUrl) } else { try await authorizeInvoice(source: method.configuration.customerTokenId, startedState: startedState) } diff --git a/Sources/ProcessOutUI/Sources/Modules/DynamicCheckout/Sessions/AlternativePayment/DynamicCheckoutAlternativePaymentDefaultSession.swift b/Sources/ProcessOutUI/Sources/Modules/DynamicCheckout/Sessions/AlternativePayment/DynamicCheckoutAlternativePaymentDefaultSession.swift deleted file mode 100644 index ff1f34a45..000000000 --- a/Sources/ProcessOutUI/Sources/Modules/DynamicCheckout/Sessions/AlternativePayment/DynamicCheckoutAlternativePaymentDefaultSession.swift +++ /dev/null @@ -1,38 +0,0 @@ -// -// DynamicCheckoutAlternativePaymentDefaultSession.swift -// ProcessOutUI -// -// Created by Andrii Vysotskyi on 25.03.2024. -// - -import Foundation -import ProcessOut - -final class DynamicCheckoutAlternativePaymentDefaultSession: DynamicCheckoutAlternativePaymentSession { - - init(configuration: PODynamicCheckoutAlternativePaymentConfiguration) { - self.configuration = configuration - } - - func start(url: URL) async throws -> POAlternativePaymentMethodResponse { - guard let returnUrl = configuration.returnUrl else { - throw POFailure(message: "Return URL must be set.", code: .generic(.mobile)) - } - return try await withCheckedThrowingContinuation { continuation in - let session = POWebAuthenticationSession(alternativePaymentMethodUrl: url, returnUrl: returnUrl) { result in - continuation.resume(with: result) - } - Task { - guard await !session.start() else { - return - } - let failure = POFailure(message: "Unable to start alternative payment.", code: .generic(.mobile)) - continuation.resume(throwing: failure) - } - } - } - - // MARK: - Private Properties - - private let configuration: PODynamicCheckoutAlternativePaymentConfiguration -} diff --git a/Sources/ProcessOutUI/Sources/Modules/DynamicCheckout/Sessions/AlternativePayment/DynamicCheckoutAlternativePaymentSession.swift b/Sources/ProcessOutUI/Sources/Modules/DynamicCheckout/Sessions/AlternativePayment/DynamicCheckoutAlternativePaymentSession.swift deleted file mode 100644 index 82ae0e29b..000000000 --- a/Sources/ProcessOutUI/Sources/Modules/DynamicCheckout/Sessions/AlternativePayment/DynamicCheckoutAlternativePaymentSession.swift +++ /dev/null @@ -1,16 +0,0 @@ -// -// DynamicCheckoutAlternativePaymentSession.swift -// ProcessOutUI -// -// Created by Andrii Vysotskyi on 17.03.2024. -// - -import Foundation -import ProcessOut - -@MainActor -protocol DynamicCheckoutAlternativePaymentSession { - - /// Starts alternative payment. - func start(url: URL) async throws -> POAlternativePaymentMethodResponse -} diff --git a/Sources/ProcessOutUI/Sources/Modules/DynamicCheckout/View/PODynamicCheckoutView+Init.swift b/Sources/ProcessOutUI/Sources/Modules/DynamicCheckout/View/PODynamicCheckoutView+Init.swift index a20dcc315..a8942f8b0 100644 --- a/Sources/ProcessOutUI/Sources/Modules/DynamicCheckout/View/PODynamicCheckoutView+Init.swift +++ b/Sources/ProcessOutUI/Sources/Modules/DynamicCheckout/View/PODynamicCheckoutView+Init.swift @@ -28,9 +28,6 @@ extension PODynamicCheckoutView { passKitPaymentSession: DynamicCheckoutPassKitPaymentDefaultSession( delegate: delegate, invoicesService: ProcessOut.shared.invoices ), - alternativePaymentSession: DynamicCheckoutAlternativePaymentDefaultSession( - configuration: configuration.alternativePayment - ), childProvider: DynamicCheckoutInteractorDefaultChildProvider( configuration: configuration, cardsService: ProcessOut.shared.cards, @@ -39,6 +36,7 @@ extension PODynamicCheckoutView { logger: logger ), invoicesService: ProcessOut.shared.invoices, + alternativePaymentsService: ProcessOut.shared.alternativePayments, logger: logger, completion: completion ) diff --git a/Sources/ProcessOutUI/Sources/Modules/WebAuthentication/DefaultSafariViewModel.swift b/Sources/ProcessOutUI/Sources/Modules/WebAuthentication/DefaultSafariViewModel.swift deleted file mode 100644 index 005e8ef38..000000000 --- a/Sources/ProcessOutUI/Sources/Modules/WebAuthentication/DefaultSafariViewModel.swift +++ /dev/null @@ -1,129 +0,0 @@ -// -// DefaultSafariViewModel.swift -// ProcessOutUI -// -// Created by Andrii Vysotskyi on 10.05.2023. -// - -import Foundation -import SafariServices -@_spi(PO) import ProcessOut - -@MainActor -final class DefaultSafariViewModel: NSObject, Sendable, @preconcurrency SFSafariViewControllerDelegate { - - init( - callback: POWebAuthenticationSessionCallback, - timeout: TimeInterval? = nil, - eventEmitter: POEventEmitter, - logger: POLogger, - completion: @escaping @Sendable (Result) -> Void - ) { - self.callback = callback - self.timeout = timeout - self.eventEmitter = eventEmitter - self.logger = logger - self.completion = completion - state = .idle - } - - func start() { - guard case .idle = state else { - return - } - if let timeout { - timeoutTimer = Timer.scheduledTimer(withTimeInterval: timeout, repeats: false) { [weak self] _ in - MainActor.assumeIsolated { - self?.setCompletedState(with: POFailure(code: .timeout(.mobile))) - } - } - } - deepLinkObserver = eventEmitter.on(PODeepLinkReceivedEvent.self) { [weak self] event in - self?.setCompletedState(with: event.url) ?? false - } - state = .started - } - - // MARK: - SFSafariViewControllerDelegate - - func safariViewControllerDidFinish(_ controller: SFSafariViewController) { - if state != .completed { - logger.debug("Safari did finish, but state is not completed, handling as cancelation") - let failure = POFailure(code: .cancelled) - setCompletedState(with: failure) - } - } - - func safariViewController(_ controller: SFSafariViewController, didCompleteInitialLoad didLoadSuccessfully: Bool) { - if !didLoadSuccessfully { - logger.debug("Safari failed to load initial url, aborting") - let failure = POFailure(code: .generic(.mobile)) - setCompletedState(with: failure) - } - } - - nonisolated func safariViewController(_ controller: SFSafariViewController, initialLoadDidRedirectTo url: URL) { - logger.debug("Safari did redirect to url: \(url)") - } - - // MARK: - Private Nested Types - - private enum State { - - /// View model is currently idle and waiting for start. - case idle - - /// View model has been started and is currently operating. - case started - - /// View model did complete with either success or failure. - case completed - } - - // MARK: - Private Properties - - private let callback: POWebAuthenticationSessionCallback - private let timeout: TimeInterval? - private let eventEmitter: POEventEmitter - private let logger: POLogger - private let completion: (Result) -> Void - - private var state: State - private var deepLinkObserver: AnyObject? - private var timeoutTimer: Timer? - - // MARK: - Private Methods - - private func setCompletedState(with url: URL) -> Bool { - if case .completed = state { - logger.info("Can't change state to completed because already in sink state.") - return false - } - // todo(andrii-vysotskyi): consider validating whether url is related to initial request if possible - guard callback.matchesURL(url) else { - logger.debug("Ignoring unrelated url: \(url)") - return false - } - invalidateObservers() - state = .completed - logger.info("Did complete with url: \(url)") - completion(.success(url)) - return true - } - - private func setCompletedState(with failure: POFailure) { - if case .completed = state { - logger.info("Can't change state to completed because already in a sink state.") - return - } - invalidateObservers() - state = .completed - logger.debug("Did complete with error: \(failure)") - completion(.failure(failure)) - } - - private func invalidateObservers() { - timeoutTimer?.invalidate() - deepLinkObserver = nil - } -} diff --git a/Sources/ProcessOutUI/Sources/Modules/WebAuthentication/POWebAuthenticationSession.swift b/Sources/ProcessOutUI/Sources/Modules/WebAuthentication/POWebAuthenticationSession.swift deleted file mode 100644 index 33bb00a9a..000000000 --- a/Sources/ProcessOutUI/Sources/Modules/WebAuthentication/POWebAuthenticationSession.swift +++ /dev/null @@ -1,118 +0,0 @@ -// -// POWebAuthenticationSession.swift -// ProcessOutUI -// -// Created by Andrii Vysotskyi on 29.05.2024. -// - -import SafariServices -import AuthenticationServices -@_spi(PO) import ProcessOut - -/// A session that an app uses to authenticate a payment. -@MainActor -public final class POWebAuthenticationSession: Sendable { - - /// A completion handler for the web authentication session. - typealias Completion = @Sendable (Result) -> Void - - /// Only call this method once for a given POWebAuthenticationSession instance after initialization. - /// Calling the start() method on a canceled session results in a failure. - /// - /// After you call start(), the session instance stores a strong reference to itself. To avoid deallocation during - /// the authentication process, the session keeps the reference until after it calls the completion handler. - public func start() async -> Bool { - guard state == nil else { - preconditionFailure("Session start must be attempted only once.") - } - guard let presentingViewController = PresentingViewControllerProvider.find() else { - return false - } - let viewController = createViewController() - state = .started(viewController: viewController) - await withCheckedContinuation { continuation in - presentingViewController.present(viewController, animated: true, completion: continuation.resume) - } - associate(controller: self, with: viewController) - return true - } - - /// Cancels a web authentication session. - /// - /// If the session has already presented a view with the authentication webpage, calling this method dismisses - /// that view. Calling cancel() on an already canceled/completed session has no effect. - public func cancel() async { - guard case .started(let viewController) = state else { - return - } - // Break retain cycle to allow de-initialization of self. - await withCheckedContinuation { continuation in - viewController.dismiss(animated: true, completion: continuation.resume) - } - state = .cancelling - associate(controller: nil, with: viewController) - } - - // MARK: - - - init( - url: URL, - callback: POWebAuthenticationSessionCallback, - timeout: TimeInterval? = nil, - completion: @escaping Completion - ) { - self.url = url - self.callback = callback - self.timeout = timeout - self.completion = completion - } - - // MARK: - Private Nested Types - - private enum AssociatedKeys { - nonisolated(unsafe) static var controller: UInt8 = 0 - } - - private enum State { - case started(viewController: SFSafariViewController), cancelling, completed - } - - // MARK: - Private Properties - - private let url: URL - private let callback: POWebAuthenticationSessionCallback - private let timeout: TimeInterval? - private let completion: Completion - private var state: State? - - // MARK: - Utils - - private func createViewController() -> SFSafariViewController { - let viewController = SFSafariViewController(url: url) - viewController.dismissButtonStyle = .cancel - let viewModel = DefaultSafariViewModel( - callback: callback, - timeout: timeout, - eventEmitter: ProcessOut.shared.eventEmitter, - logger: ProcessOut.shared.logger, - completion: { [weak self] result in - self?.complete(with: result) - } - ) - viewController.setViewModel(viewModel) - viewModel.start() - return viewController - } - - private nonisolated func complete(with result: Result) { - Task { @MainActor in - await self.cancel() - state = .completed - completion(result) - } - } - - private func associate(controller: POWebAuthenticationSession?, with object: Any) { - objc_setAssociatedObject(object, &AssociatedKeys.controller, controller, .OBJC_ASSOCIATION_RETAIN) - } -} diff --git a/Sources/ProcessOutUI/Sources/Modules/WebAuthentication/POWebAuthenticationSessionCallback.swift b/Sources/ProcessOutUI/Sources/Modules/WebAuthentication/POWebAuthenticationSessionCallback.swift deleted file mode 100644 index 1ced4cbe8..000000000 --- a/Sources/ProcessOutUI/Sources/Modules/WebAuthentication/POWebAuthenticationSessionCallback.swift +++ /dev/null @@ -1,21 +0,0 @@ -// -// POWebAuthenticationSessionCallback.swift -// ProcessOutUI -// -// Created by Andrii Vysotskyi on 29.05.2024. -// - -import Foundation - -/// An object used to evaluate navigation events in an authentication session. -public struct POWebAuthenticationSessionCallback: Sendable { - - /// Creates a callback object that matches against URLs with the given custom scheme. - /// - Parameter customScheme: The custom scheme that the app expects in the callback URL. - public static func customScheme(_ customScheme: String) -> Self { - Self { $0.scheme == customScheme } - } - - /// Check whether a given main-frame navigation URL matches the callback expected by the client app. - let matchesURL: @Sendable (_ url: URL) -> Bool -} diff --git a/Sources/ProcessOutUI/Sources/Modules/WebAuthentication/SafariViewController+Extensions.swift b/Sources/ProcessOutUI/Sources/Modules/WebAuthentication/SafariViewController+Extensions.swift deleted file mode 100644 index f6a2beb39..000000000 --- a/Sources/ProcessOutUI/Sources/Modules/WebAuthentication/SafariViewController+Extensions.swift +++ /dev/null @@ -1,22 +0,0 @@ -// -// SafariViewController+Extensions.swift -// ProcessOutUI -// -// Created by Andrii Vysotskyi on 10.05.2023. -// - -import SafariServices - -extension SFSafariViewController { - - func setViewModel(_ viewModel: DefaultSafariViewModel) { - objc_setAssociatedObject(self, &Keys.viewModel, viewModel, .OBJC_ASSOCIATION_RETAIN) - delegate = viewModel - } - - // MARK: - Private Nested Types - - private enum Keys { - nonisolated(unsafe) static var viewModel: UInt8 = 0 - } -}