Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
0c31ad8
Support throwing methods in lock
andrii-vysotskyi-cko Aug 7, 2024
fe1b0c7
Commit generated changes
andrii-vysotskyi-cko Aug 7, 2024
3c597b8
Fix 3DS challenge response
andrii-vysotskyi-cko Aug 7, 2024
ce80614
Expose API to create redirect URL
andrii-vysotskyi-cko Aug 7, 2024
c64d9f5
Move tests to UI module
andrii-vysotskyi-cko Aug 7, 2024
5f31e41
Remove tests for non-existing types
andrii-vysotskyi-cko Aug 7, 2024
1275737
Fix utils tests
andrii-vysotskyi-cko Aug 7, 2024
e2bbfc2
Enable experimental feature
andrii-vysotskyi-cko Aug 7, 2024
f81387f
Resolve concurrency warnings
andrii-vysotskyi-cko Aug 7, 2024
386f9d2
Fix UI tests
andrii-vysotskyi-cko Aug 7, 2024
909da72
Add unfair lock extensions
andrii-vysotskyi-cko Aug 7, 2024
c7f5152
Ensure mocks are sendable
andrii-vysotskyi-cko Aug 7, 2024
b1ee77f
Replace NSLock with UnfairLock
andrii-vysotskyi-cko Aug 7, 2024
08d9b39
Add web authentication session protocol
andrii-vysotskyi-cko Aug 7, 2024
e24c5fe
Add mock web authentication session
andrii-vysotskyi-cko Aug 8, 2024
e7b771d
Remove redundant defaults
andrii-vysotskyi-cko Aug 8, 2024
f0eeac1
Update error utility
andrii-vysotskyi-cko Aug 8, 2024
696f24b
Fix APM tests
andrii-vysotskyi-cko Aug 8, 2024
41efb99
Fix 3DS tests
andrii-vysotskyi-cko Aug 8, 2024
065673d
Minor improvements
andrii-vysotskyi-cko Aug 8, 2024
25a1748
Use beta xcode
andrii-vysotskyi-cko Aug 8, 2024
6208da2
Update comment
andrii-vysotskyi-cko Aug 8, 2024
b3a048c
Fix dev project swift settings
andrii-vysotskyi-cko Aug 8, 2024
f794aa7
Temporarily disable CKO target tests
andrii-vysotskyi-cko Aug 8, 2024
7676c23
Resolve build issue
andrii-vysotskyi-cko Aug 8, 2024
b172ec8
Update dependencies
andrii-vysotskyi-cko Aug 8, 2024
dab098a
Resolve warning
andrii-vysotskyi-cko Aug 8, 2024
ba8da0f
Fix build issues
andrii-vysotskyi-cko Aug 8, 2024
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
3 changes: 2 additions & 1 deletion .github/actions/bootstrap-project/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ runs:
echo "$CONSTANTS" > Example/Example/Resources/Constants.yml
shell: bash
- name: Select Xcode Version
run: sudo xcode-select -s '/Applications/Xcode_15.4.app/Contents/Developer'
# todo(andrii-vysotskyi): Migrate to public release once available
run: sudo xcode-select -s '/Applications/Xcode_16_beta_4.app/Contents/Developer'
shell: bash
- name: Bootstrap Project
run: ./Scripts/BootstrapProject.sh
Expand Down
2 changes: 1 addition & 1 deletion Example/Example/Sources/Application/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ final class AppDelegate: UIResponder, UIApplicationDelegate {
// Please note that implementation is using factory method (part of private interface) that creates
// configuration with private key. It is only done for demonstration/testing purposes to avoid setting
// up test server and shouldn't be shipped with production code.
let configuration = ProcessOutConfiguration.production(
let configuration = ProcessOutConfiguration(
projectId: Constants.projectId, privateKey: Constants.projectPrivateKey
)
ProcessOut.configure(configuration: configuration)
Expand Down
17 changes: 3 additions & 14 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,6 @@

import PackageDescription

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

let package = Package(
name: "ProcessOut",
defaultLocalization: "en",
Expand All @@ -25,13 +20,9 @@ let package = Package(
targets: [
.target(
name: "ProcessOut",
dependencies: [
.target(name: "cmark")
],
resources: [
.process("Resources")
],
swiftSettings: swiftSettings
]
),
.target(
name: "ProcessOutCheckout3DS",
Expand All @@ -50,8 +41,7 @@ let package = Package(
],
resources: [
.process("Resources")
],
swiftSettings: swiftSettings
]
),
.target(
name: "ProcessOutCoreUI",
Expand All @@ -60,8 +50,7 @@ let package = Package(
],
resources: [
.process("Resources")
],
swiftSettings: swiftSettings
]
),
.binaryTarget(name: "cmark", path: "Vendor/cmark.xcframework")
]
Expand Down
5 changes: 2 additions & 3 deletions ProcessOut.podspec
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
Pod::Spec.new do |s|
s.name = 'ProcessOut'
s.version = '4.19.0'
s.swift_versions = ['5.9']
s.swift_versions = ['6.0']
s.license = { :type => 'MIT', :file => 'LICENSE' }
s.homepage = 'https://github.com/processout/processout-ios'
s.author = 'ProcessOut'
s.summary = 'The smart router for payments. Smartly route each transaction to the relevant payment providers.'
s.source = { :git => 'https://github.com/processout/processout-ios.git', :tag => s.version.to_s }
s.frameworks = 'Foundation', 'UIKit'
s.ios.deployment_target = '13.0'
s.vendored_frameworks = "Vendor/cmark.xcframework"
s.ios.deployment_target = '14.0'
s.ios.resources = 'Sources/ProcessOut/Resources/**/*'
s.source_files = 'Sources/ProcessOut/**/*.swift'
s.pod_target_xcconfig = { 'OTHER_SWIFT_FLAGS' => '-Xfrontend -module-interface-preserve-types-as-written' }
Expand Down
4 changes: 2 additions & 2 deletions ProcessOutCheckout3DS.podspec
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
Pod::Spec.new do |s|
s.name = 'ProcessOutCheckout3DS'
s.version = '4.19.0'
s.swift_versions = ['5.9']
s.swift_versions = ['6.0']
s.license = { :type => 'MIT', :file => 'LICENSE' }
s.homepage = 'https://github.com/processout/processout-ios'
s.author = 'ProcessOut'
s.summary = 'Integration with Checkout.com 3D Secure (3DS) mobile SDK.'
s.source = { :git => 'https://github.com/processout/processout-ios.git', :tag => s.version.to_s }
s.frameworks = 'Foundation'
s.ios.deployment_target = '13.0'
s.ios.deployment_target = '14.0'
s.ios.resources = 'Sources/ProcessOutCheckout3DS/Resources/**/*'
s.source_files = 'Sources/ProcessOutCheckout3DS/**/*.swift'
s.pod_target_xcconfig = { 'EXCLUDED_ARCHS' => 'x86_64' }
Expand Down
4 changes: 2 additions & 2 deletions ProcessOutCoreUI.podspec
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
Pod::Spec.new do |s|
s.name = 'ProcessOutCoreUI'
s.version = '4.19.0'
s.swift_versions = ['5.9']
s.swift_versions = ['6.0']
s.license = { :type => 'MIT', :file => 'LICENSE' }
s.homepage = 'https://github.com/processout/processout-ios'
s.author = 'ProcessOut'
s.summary = 'Reusable UI components and logic. Pod is meant to be used only with other ProcessOut pods.'
s.source = { :git => 'https://github.com/processout/processout-ios.git', :tag => s.version.to_s }
s.frameworks = 'Foundation', 'SwiftUI'
s.vendored_frameworks = "Vendor/cmark.xcframework"
s.ios.deployment_target = '14.0'
s.ios.resources = 'Sources/ProcessOutCoreUI/Resources/**/*'
s.source_files = 'Sources/ProcessOutCoreUI/**/*.swift'
s.dependency 'ProcessOut' # todo(andrii-vysotskyi): vendor cmark.xcframework instead after UI migration is completed
end
2 changes: 1 addition & 1 deletion ProcessOutUI.podspec
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
Pod::Spec.new do |s|
s.name = 'ProcessOutUI'
s.version = '4.19.0'
s.swift_versions = ['5.9']
s.swift_versions = ['6.0']
s.license = { :type => 'MIT', :file => 'LICENSE' }
s.homepage = 'https://github.com/processout/processout-ios'
s.author = 'ProcessOut'
Expand Down
5 changes: 4 additions & 1 deletion Scripts/Test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,11 @@ set -euo pipefail
PROJECT='ProcessOut.xcodeproj'
DESTINATION=$(./Scripts/TestDestination.swift)

# todo(andrii-vysotskyi): reenable "ProcessOutCheckout3DS" tests
# when Swift6 compatibility is fixed

# Run Tests
for PRODUCT in "ProcessOut" "ProcessOutUI" "ProcessOutCheckout3DS"; do
for PRODUCT in "ProcessOut" "ProcessOutUI"; do
xcodebuild clean test \
-destination "$DESTINATION" \
-project $PROJECT \
Expand Down
4 changes: 2 additions & 2 deletions Sources/ProcessOut/Sources/Api/ProcessOut.swift
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ public final class ProcessOut: @unchecked Sendable {
let configuration = self.configuration
return .init(projectId: configuration.projectId, baseUrl: configuration.checkoutBaseUrl)
}
let webSession = WebAuthenticationSession()
let webSession = DefaultWebAuthenticationSession()
return DefaultAlternativePaymentsService(
configuration: serviceConfiguration, webSession: webSession, logger: logger
)
Expand All @@ -144,7 +144,7 @@ public final class ProcessOut: @unchecked Sendable {
let encoder = JSONEncoder()
encoder.dataEncodingStrategy = .base64
encoder.keyEncodingStrategy = .useDefaultKeys
let webSession = WebAuthenticationSession()
let webSession = DefaultWebAuthenticationSession()
return DefaultThreeDSService(decoder: decoder, encoder: encoder, webSession: webSession)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ actor DefaultDeviceMetadataProvider: DeviceMetadataProvider {
let description = withUnsafePointer(to: &systemInfo.machine) { pointer in
let capacity = Int(_SYS_NAMELEN)
return pointer.withMemoryRebound(to: CChar.self, capacity: capacity) { charPointer in
String(validatingUTF8: charPointer)
String(validatingCString: charPointer)
}
}
return description
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,7 @@ final class SystemLoggerDestination: LoggerDestination {

init(subsystem: String) {
self.subsystem = subsystem
lock = NSLock()
logs = [:]
logs = POUnfairlyLocked(wrappedValue: [:])
}

func log(event: LogEvent) {
Expand All @@ -30,8 +29,7 @@ final class SystemLoggerDestination: LoggerDestination {
// MARK: - Private Properties

private let subsystem: String
private let lock: NSLock
private nonisolated(unsafe) var logs: [String: OSLog]
private let logs: POUnfairlyLocked<[String: OSLog]>

// MARK: - Private Methods

Expand All @@ -49,7 +47,7 @@ final class SystemLoggerDestination: LoggerDestination {
}

private func osLog(category: String) -> OSLog {
let log = lock.withLock {
let log = logs.withLock { logs in
if let log = logs[category] {
return log
}
Expand Down
4 changes: 2 additions & 2 deletions Sources/ProcessOut/Sources/Core/Logger/POLogger.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ public struct POLogger: Sendable {
self.category = category
self.minimumLevel = minimumLevel
self.attributes = [:]
lock = NSLock()
lock = POUnfairlyLocked()
}

init(destinations: [LoggerDestination] = [], category: String) {
Expand Down Expand Up @@ -84,7 +84,7 @@ public struct POLogger: Sendable {

private let destinations: [LoggerDestination]
private let minimumLevel: @Sendable () -> LogLevel
private let lock: NSLock
private let lock: POUnfairlyLocked<Void>
private var attributes: [POLogAttributeKey: String]

// MARK: - Private Methods
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
//

import Foundation
@_implementationOnly import cmark

enum MarkdownParser {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,18 +20,24 @@ public final class POUnfairlyLocked<Value>: @unchecked Sendable {
lock.withLock { value }
}

public var projectedValue: POUnfairlyLocked<Value> {
self
}

public func withLock<R>(_ body: (inout Value) -> R) -> R {
lock.withLock {
body(&value)
}
public func withLock<R>(_ body: (inout Value) throws -> R) rethrows -> R {
try lock.withLock { try body(&value) }
}

// MARK: - Private Properties

private let lock = UnfairLock()
private var value: Value
}

extension POUnfairlyLocked where Value == Void {

/// Convenience to create lock when value type is `Void`.
public convenience init() where Value == Void {
self.init(wrappedValue: ())
}

public func withLock<R>(_ body: () throws -> R) rethrows -> R {
try withLock { _ in try body() }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,12 @@ final class UnfairLock: Sendable {
unfairLock.initialize(to: os_unfair_lock())
}

func withLock<R>(_ body: () -> R) -> R {
func withLock<R>(_ body: () throws -> R) rethrows -> R {
defer {
os_unfair_lock_unlock(unfairLock)
}
os_unfair_lock_lock(unfairLock)
return body()
return try body()
}

deinit {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
//
// DefaultWebAuthenticationSession.swift
// ProcessOut
//
// Created by Andrii Vysotskyi on 07.08.2024.
//

import AuthenticationServices

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

override nonisolated init() {
// Ignored
}

// MARK: - WebAuthenticationSession

func authenticate(
using url: URL, callbackScheme: String?, additionalHeaderFields: [String: String]?
) 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