Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

import UIKit

@MainActor
final class AlternativePaymentDataBuilder {

init(completion: @escaping ([String: String]) -> Void) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

import Foundation

@MainActor
protocol AlternativePaymentDataViewModelType: ViewModelType<AlternativePaymentDataViewModelState> {

/// Submits items and continues payment.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

import UIKit

@MainActor
final class AlternativePaymentDataEntryBuilder {

init(completion: @escaping (_ key: String, _ value: String) -> Void) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import UIKit
import ProcessOut

@MainActor
final class AlternativePaymentMethodsBuilder {

init(filter: POAllGatewayConfigurationsRequest.Filter) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

import UIKit

@MainActor
final class AuthorizationAmountBuilder {

init(completion: @escaping (_ amount: Decimal, _ currencyCode: String) -> Void) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import ProcessOut
import ProcessOutUI
import ProcessOutCheckout3DS

@MainActor
final class CardPaymentBuilder {

init(threeDSService: CardPayment3DSService = .test, completion: @escaping (Result<POCard, POFailure>) -> Void) {
Expand All @@ -21,12 +22,9 @@ final class CardPaymentBuilder {
let threeDSService: PO3DSService
switch self.threeDSService {
case .test:
threeDSService = POTest3DSService(returnUrl: Constants.returnUrl)
threeDSService = POTest3DSService()
case .checkout:
threeDSService = POCheckout3DSServiceBuilder()
.with(delegate: CardPaymentCheckout3DSServiceDelegate())
.with(environment: .sandbox)
.build()
threeDSService = POCheckout3DSService(environment: .sandbox)
}
let delegate = CardPaymentDelegate(
invoicesService: ProcessOut.shared.invoices, threeDSService: threeDSService
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import UIKit
import ProcessOut

@MainActor
final class FeaturesBuilder {

func build() -> UIViewController {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ extension FeaturesViewModel: PODynamicCheckoutDelegate {
func dynamicCheckout(
willAuthorizeInvoiceWith request: inout POInvoiceAuthorizationRequest
) async -> any PO3DSService {
POTest3DSService(returnUrl: Constants.returnUrl)
POTest3DSService()
}

func dynamicCheckout(willAuthorizeInvoiceWith request: PKPaymentRequest) async {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
// Created by Andrii Vysotskyi on 23.10.2022.
//

@MainActor
protocol RouterType<RouteType>: AnyObject {

/// `RouteType` defines which routes can be triggered in a certain implementation.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
// Created by Andrii Vysotskyi on 21.10.2022.
//

@MainActor
protocol ViewModelType<State>: AnyObject {

associatedtype State
Expand Down
4 changes: 3 additions & 1 deletion Example/Example/Sources/UI/Shared/View/ViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,9 @@ class ViewController<ViewModel: ViewModelType>: UIViewController {
// as a workaround, configuration is postponed to a point when tracking ends.
if RunLoop.current.currentMode == .tracking {
RunLoop.current.perform {
self.configure(with: self.viewModel.state)
MainActor.assumeIsolated {
self.configure(with: self.viewModel.state)
}
}
} else {
configure(with: viewModel.state)
Expand Down
2 changes: 1 addition & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import PackageDescription

let swiftSettings: [SwiftSetting] = [
.enableExperimentalFeature("IsolatedAny"),
.enableUpcomingFeature("StrictConcurrency")
]

Expand All @@ -27,7 +28,6 @@ let package = Package(
dependencies: [
.target(name: "cmark")
],
exclude: ["swiftgen.yml"],
resources: [
.process("Resources")
],
Expand Down
3 changes: 2 additions & 1 deletion Sources/ProcessOut/Sources/Api/ProcessOut.swift
Original file line number Diff line number Diff line change
Expand Up @@ -154,12 +154,13 @@ public final class ProcessOut: @unchecked Sendable {
}

private static func create3DSService() -> DefaultThreeDSService {
let webSession = WebAuthenticationSession()
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .useDefaultKeys
let encoder = JSONEncoder()
encoder.dataEncodingStrategy = .base64
encoder.keyEncodingStrategy = .useDefaultKeys
return DefaultThreeDSService(decoder: decoder, encoder: encoder)
return DefaultThreeDSService(decoder: decoder, encoder: encoder, webSession: webSession)
}

private func createConnector(
Expand Down
16 changes: 9 additions & 7 deletions Sources/ProcessOut/Sources/Core/Utils/AsyncUtils.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ import Foundation
/// - Warning: operation should support cancellation, otherwise calling this method has no effect.
func withTimeout<T: Sendable>(
_ timeout: TimeInterval,
error timeoutError: @autoclosure () -> Error,
perform operation: @escaping @Sendable () async throws -> T
error timeoutError: @autoclosure @Sendable () -> Error,
perform operation: @escaping @Sendable @isolated(any) () async throws -> T
) async throws -> T {
let isTimedOut = POUnfairlyLocked(wrappedValue: false)
let task = Task(operation: operation)
Expand Down Expand Up @@ -48,10 +48,10 @@ func withTimeout<T: Sendable>(
// MARK: - Retry

func retry<T: Sendable>(
operation: @escaping @Sendable () async throws -> T,
operation: @escaping @Sendable @isolated(any) () async throws -> T,
while condition: @escaping @Sendable (Result<T, Error>) -> Bool,
timeout: TimeInterval,
timeoutError: @autoclosure () -> Error,
timeoutError: @autoclosure @Sendable () -> Error,
retryStrategy: RetryStrategy? = nil
) async throws -> T {
let operationBox = { @Sendable in
Expand All @@ -67,7 +67,7 @@ func retry<T: Sendable>(
}

private func retry<T: Sendable>(
operation: @escaping @Sendable () async throws -> T,
operation: @escaping @Sendable @isolated(any) () async throws -> T,
after result: Result<T, Error>,
while condition: @escaping @Sendable (Result<T, Error>) -> Bool,
retryStrategy: RetryStrategy?,
Expand All @@ -91,11 +91,13 @@ private func retry<T: Sendable>(
)
}

extension Result where Failure == Error {
extension Result where Failure == Error, Success: Sendable {

/// Creates a new result by evaluating a throwing closure, capturing the
/// returned value as a success, or any thrown error as a failure.
fileprivate init(catching body: () async throws -> Success) async { // swiftlint:disable:this strict_fileprivate
fileprivate init( // swiftlint:disable:this strict_fileprivate
catching body: @isolated(any) () async throws -> Success
) async {
do {
let success = try await body()
self = .success(success)
Expand Down
4 changes: 2 additions & 2 deletions Sources/ProcessOut/Sources/Core/Utils/Batcher.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@

import Foundation

final class Batcher<Task>: Sendable {
final class Batcher<Task: Sendable>: Sendable {

typealias Executor = @Sendable (Array<Task>) async -> Bool
typealias Executor = @Sendable @isolated(any) (Array<Task>) async -> Bool

init(executionInterval: TimeInterval = 10, executor: @escaping Executor) {
self.executionInterval = executionInterval
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
//
// WebAuthenticationSession.swift
// ProcessOut
//
// Created by Andrii Vysotskyi on 01.08.2024.
//

import AuthenticationServices

@MainActor
final class WebAuthenticationSession: NSObject, Sendable, ASWebAuthenticationPresentationContextProviding {

override nonisolated init() {
// Ignored
}

func authenticate(
using url: URL,
callbackScheme: String? = nil,
additionalHeaderFields: [String: String]? = nil
) async throws -> URL {
let sessionProxy = WebAuthenticationSessionProxy()
return try await withTaskCancellationHandler(
operation: {
try await withCheckedThrowingContinuation { continuation in
guard !Task.isCancelled else {
let failure = POFailure(message: "Authentication session was cancelled.", code: .cancelled)
continuation.resume(throwing: failure)
return
}
let session = ASWebAuthenticationSession(
url: url,
callbackURLScheme: callbackScheme,
completionHandler: { url, error in
sessionProxy.invalidate()
if let error {
continuation.resume(throwing: Self.converted(error: error))
} else if let url {
continuation.resume(returning: url)
} else {
preconditionFailure("Unexpected ASWebAuthenticationSession completion result.")
}
}
)
session.prefersEphemeralWebBrowserSession = true
session.presentationContextProvider = self
if #available(iOS 17.4, *) {
session.additionalHeaderFields = additionalHeaderFields
}
sessionProxy.setSession(session, continuation: continuation)
session.start()
}
},
onCancel: {
sessionProxy.cancel()
}
)
}

// MARK: - ASWebAuthenticationPresentationContextProviding

func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor {
let application = UIApplication.shared
let scene = application.connectedScenes.first { $0 is UIWindowScene } as? UIWindowScene
let window = scene?.windows.first(where: \.isKeyWindow)
return window ?? ASPresentationAnchor()
}

// MARK: - Private Methods

private static func converted(error: Error) -> POFailure {
guard let error = error as? ASWebAuthenticationSessionError else {
return POFailure(code: .generic(.mobile), underlyingError: error)
}
let poCode: POFailure.Code
switch error.code {
case .canceledLogin:
poCode = .cancelled
case .presentationContextNotProvided, .presentationContextInvalid:
poCode = .internal(.mobile)
@unknown default:
poCode = .generic(.mobile)
}
return POFailure(code: poCode, underlyingError: error)
}
}

@MainActor
private final class WebAuthenticationSessionProxy: Sendable {

func setSession(_ session: ASWebAuthenticationSession, continuation: CheckedContinuation<URL, Error>) {
self.session = session
self.continuation = continuation
}

func invalidate() {
session = nil
continuation = nil
}

nonisolated func cancel() {
Task { @MainActor in
_cancel()
}
}

// MARK: - Private Properties

private var session: ASWebAuthenticationSession?
private var continuation: CheckedContinuation<URL, Error>?

// MARK: - Private Methods

private func _cancel() {
let failure = POFailure(message: "Authentication session was cancelled.", code: .cancelled)
session?.cancel()
continuation?.resume(throwing: failure)
invalidate()
}
}
Loading