diff --git a/.github/actions/select-xcode/action.yml b/.github/actions/select-xcode/action.yml index 96ede897f..544dddb3b 100644 --- a/.github/actions/select-xcode/action.yml +++ b/.github/actions/select-xcode/action.yml @@ -4,5 +4,6 @@ runs: using: "composite" steps: - 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_6.app/Contents/Developer' shell: bash diff --git a/Example/Example/Sources/Application/SceneDelegate.swift b/Example/Example/Sources/Application/SceneDelegate.swift index 40337f42c..bb1368231 100644 --- a/Example/Example/Sources/Application/SceneDelegate.swift +++ b/Example/Example/Sources/Application/SceneDelegate.swift @@ -11,10 +11,4 @@ import ProcessOut final class SceneDelegate: UIResponder, UIWindowSceneDelegate { var window: UIWindow? - - 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/AlternativePayments/Interactor/AlternativePaymentsInteractor.swift b/Example/Example/Sources/UI/Modules/AlternativePayments/Interactor/AlternativePaymentsInteractor.swift index 853420274..91135e5aa 100644 --- a/Example/Example/Sources/UI/Modules/AlternativePayments/Interactor/AlternativePaymentsInteractor.swift +++ b/Example/Example/Sources/UI/Modules/AlternativePayments/Interactor/AlternativePaymentsInteractor.swift @@ -14,10 +14,12 @@ final class AlternativePaymentsInteractor { init( gatewayConfigurationsRepository: POGatewayConfigurationsRepository, - invoicesService: POInvoicesService + invoicesService: POInvoicesService, + alternativePaymentsService: POAlternativePaymentsService ) { self.gatewayConfigurationsRepository = gatewayConfigurationsRepository self.invoicesService = invoicesService + self.alternativePaymentsService = alternativePaymentsService state = .idle } @@ -90,28 +92,13 @@ final class AlternativePaymentsInteractor { } func authorize(invoice: POInvoice, gatewayConfigurationId: String) async throws { - let request = POAlternativePaymentMethodRequest( + let request = POAlternativePaymentAuthorizationRequest( invoiceId: invoice.id, gatewayConfigurationId: gatewayConfigurationId ) - let result: POAlternativePaymentMethodResponse = try await withCheckedThrowingContinuation { continuation in - let session = POWebAuthenticationSession( - request: request, - returnUrl: Example.Constants.returnUrl, - completion: { result in - continuation.resume(with: result) - } - ) - Task { @MainActor in - if await session.start() { - return - } - let failure = POFailure(message: "Unable to start alternative payment.", code: .generic(.mobile)) - continuation.resume(throwing: failure) - } - } - let authRequest = POInvoiceAuthorizationRequest(invoiceId: invoice.id, source: result.gatewayToken) - let threeDSService = POTest3DSService(returnUrl: Example.Constants.returnUrl) - try await invoicesService.authorizeInvoice(request: authRequest, threeDSService: threeDSService) + let response = try await alternativePaymentsService.authorize(request: request) + let authorizationRequest = POInvoiceAuthorizationRequest(invoiceId: invoice.id, source: response.gatewayToken) + let threeDSService = POTest3DSService() + try await invoicesService.authorizeInvoice(request: authorizationRequest, threeDSService: threeDSService) } // MARK: - Private Nested Types @@ -124,4 +111,5 @@ final class AlternativePaymentsInteractor { private let gatewayConfigurationsRepository: POGatewayConfigurationsRepository private let invoicesService: POInvoicesService + private let alternativePaymentsService: POAlternativePaymentsService } diff --git a/Example/Example/Sources/UI/Modules/AlternativePayments/ViewModel/AlternativePaymentsViewModel.swift b/Example/Example/Sources/UI/Modules/AlternativePayments/ViewModel/AlternativePaymentsViewModel.swift index 9d0566fce..be95e096e 100644 --- a/Example/Example/Sources/UI/Modules/AlternativePayments/ViewModel/AlternativePaymentsViewModel.swift +++ b/Example/Example/Sources/UI/Modules/AlternativePayments/ViewModel/AlternativePaymentsViewModel.swift @@ -162,10 +162,9 @@ final class AlternativePaymentsViewModel: ObservableObject { let configuration = PONativeAlternativePaymentConfiguration( invoiceId: invoice.id, gatewayConfigurationId: gatewayConfigurationId, - secondaryAction: .cancel(), + cancelButton: .init(), paymentConfirmation: .init( - showProgressIndicatorAfter: 5, - secondaryAction: .cancel(disabledFor: 10) + showProgressIndicatorAfter: 5, cancelButton: .init() ) ) let nativePaymentItem = AlternativePaymentsViewModelState.NativePayment( @@ -187,7 +186,8 @@ extension AlternativePaymentsViewModel { convenience init() { let interactor = AlternativePaymentsInteractor( gatewayConfigurationsRepository: ProcessOut.shared.gatewayConfigurations, - invoicesService: ProcessOut.shared.invoices + invoicesService: ProcessOut.shared.invoices, + alternativePaymentsService: ProcessOut.shared.alternativePayments ) self.init(interactor: interactor) } diff --git a/Example/Example/Sources/UI/Modules/CardPayment/ViewModel/CardPaymentViewModel.swift b/Example/Example/Sources/UI/Modules/CardPayment/ViewModel/CardPaymentViewModel.swift index 9e5594d6e..e9346b6f0 100644 --- a/Example/Example/Sources/UI/Modules/CardPayment/ViewModel/CardPaymentViewModel.swift +++ b/Example/Example/Sources/UI/Modules/CardPayment/ViewModel/CardPaymentViewModel.swift @@ -86,33 +86,14 @@ extension CardPaymentViewModel: POCardTokenizationDelegate { let threeDSService: PO3DSService switch state.authenticationService.selection { case .test: - threeDSService = POTest3DSService(returnUrl: Constants.returnUrl) + threeDSService = POTest3DSService() case .checkout: - threeDSService = POCheckout3DSServiceBuilder() - .with(delegate: self) - .with(environment: .sandbox) - .build() + threeDSService = POCheckout3DSService(environment: .sandbox) } try await invoicesService.authorizeInvoice(request: invoiceAuthorizationRequest, threeDSService: threeDSService) } } -extension CardPaymentViewModel: POCheckout3DSServiceDelegate { - - func handle(redirect: PO3DSRedirect, completion: @escaping (Result) -> Void) { - Task { @MainActor in - let session = POWebAuthenticationSession( - redirect: redirect, returnUrl: Constants.returnUrl, completion: completion - ) - if await session.start() { - return - } - let failure = POFailure(message: "Unable to process redirect", code: .generic(.mobile)) - completion(.failure(failure)) - } - } -} - extension CardPaymentViewModel { /// Convenience initializer that resolves its dependencies automatically. diff --git a/Example/Example/Sources/UI/Modules/DynamicCheckout/ViewModel/DynamicCheckoutViewModel.swift b/Example/Example/Sources/UI/Modules/DynamicCheckout/ViewModel/DynamicCheckoutViewModel.swift index 31a733c48..1eb97b003 100644 --- a/Example/Example/Sources/UI/Modules/DynamicCheckout/ViewModel/DynamicCheckoutViewModel.swift +++ b/Example/Example/Sources/UI/Modules/DynamicCheckout/ViewModel/DynamicCheckoutViewModel.swift @@ -55,7 +55,7 @@ final class DynamicCheckoutViewModel: ObservableObject { private func continueDynamicCheckout(invoice: POInvoice) { let configuration = PODynamicCheckoutConfiguration( invoiceRequest: .init(invoiceId: invoice.id, clientSecret: invoice.clientSecret), - alternativePayment: .init(returnUrl: Constants.returnUrl), + alternativePayment: .init(), cancelButton: .init(confirmation: .init()) ) let item = DynamicCheckoutViewModelState.DynamicCheckout( @@ -95,7 +95,7 @@ extension DynamicCheckoutViewModel: PODynamicCheckoutDelegate { func dynamicCheckout( willAuthorizeInvoiceWith request: inout POInvoiceAuthorizationRequest ) async -> any PO3DSService { - POTest3DSService(returnUrl: Constants.returnUrl) + POTest3DSService() } func dynamicCheckout(willAuthorizeInvoiceWith request: PKPaymentRequest) async { diff --git a/Package.swift b/Package.swift index ca2e6da04..8e000c551 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version: 5.9 +// swift-tools-version: 6.0 import PackageDescription @@ -21,9 +21,6 @@ let package = Package( targets: [ .target( name: "ProcessOut", - dependencies: [ - .product(name: "cmark-gfm", package: "swift-cmark") - ], resources: [ .process("Resources") ] diff --git a/ProcessOut.podspec b/ProcessOut.podspec index 25850b1a6..9767da247 100644 --- a/ProcessOut.podspec +++ b/ProcessOut.podspec @@ -1,15 +1,14 @@ Pod::Spec.new do |s| s.name = 'ProcessOut' s.version = '4.20.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_gfm.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' } diff --git a/ProcessOutCheckout3DS.podspec b/ProcessOutCheckout3DS.podspec index d232fef24..ad6381c87 100644 --- a/ProcessOutCheckout3DS.podspec +++ b/ProcessOutCheckout3DS.podspec @@ -1,14 +1,14 @@ Pod::Spec.new do |s| s.name = 'ProcessOutCheckout3DS' s.version = '4.20.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' } diff --git a/ProcessOutCoreUI.podspec b/ProcessOutCoreUI.podspec index 3305fb44c..99132196a 100644 --- a/ProcessOutCoreUI.podspec +++ b/ProcessOutCoreUI.podspec @@ -1,15 +1,15 @@ Pod::Spec.new do |s| s.name = 'ProcessOutCoreUI' s.version = '4.20.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_gfm.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_gfm.xcframework instead after UI migration is completed end diff --git a/ProcessOutUI.podspec b/ProcessOutUI.podspec index 79f855827..83bfe7e85 100644 --- a/ProcessOutUI.podspec +++ b/ProcessOutUI.podspec @@ -1,7 +1,7 @@ Pod::Spec.new do |s| s.name = 'ProcessOutUI' s.version = '4.20.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' diff --git a/Scripts/Test.sh b/Scripts/Test.sh index dc5d50b12..8c2780eac 100755 --- a/Scripts/Test.sh +++ b/Scripts/Test.sh @@ -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 \ diff --git a/Sources/ProcessOut/ProcessOut.docc/3DS.md b/Sources/ProcessOut/ProcessOut.docc/3DS.md index e5a5ede30..3abd72162 100644 --- a/Sources/ProcessOut/ProcessOut.docc/3DS.md +++ b/Sources/ProcessOut/ProcessOut.docc/3DS.md @@ -4,7 +4,7 @@ Some SDK methods may trigger 3DS flow, those methods could be identified by presence of required parameter `threeDSService` whose argument should conform to ``PO3DSService`` protocol. -``POInvoicesService/authorizeInvoice(request:threeDSService:completion:)`` is an example of such method. +``POInvoicesService/authorizeInvoice(request:threeDSService:)`` is an example of such method. ### 3DS2 @@ -14,15 +14,8 @@ allows to abstract the details of 3DS handling and supply functionality in a con We officially support our own implementation `POTest3DSService` (defined in `ProcessOutUI` package) that emulates the normal 3DS authentication flow. -Also, there is an integration with [Checkout](https://checkout.com) 3DS2 SDK that is available in `ProcessOutCheckout3DS` +Also, there is an integration with [Checkout](https://checkout.com) 3DS2 SDK that is available +in [`ProcessOutCheckout3DS`](https://swiftpackageindex.com/processout/processout-ios/documentation/processoutcheckout3ds) package. -### 3DS Redirect - -Method ``PO3DSService/handle(redirect:completion:)`` is a part of 3DS service that is responsible for handling web -based redirects. - -We provide an extension of `SFSafariViewController` (defined in `ProcessOutUI` package) that can be used to create an -instance, capable of handling redirects. - For additional details on 3DS UI see dedicated [documentation.](https://swiftpackageindex.com/processout/processout-ios/documentation/processoutui/3ds) diff --git a/Sources/ProcessOut/ProcessOut.docc/Localizations.md b/Sources/ProcessOut/ProcessOut.docc/Localizations.md index 6d5054a41..1e29fcb7f 100644 --- a/Sources/ProcessOut/ProcessOut.docc/Localizations.md +++ b/Sources/ProcessOut/ProcessOut.docc/Localizations.md @@ -3,6 +3,6 @@ ## Overview If the main application uses a language that we don't support, the implementation would attempt to use strings from -the main bundle's `ProcessOut.strings`. +the main bundle's `ProcessOut.strings`. You may add translations to `ProcessOut.stringsdict` file if special rules for plurals are desired. diff --git a/Sources/ProcessOut/ProcessOut.docc/MigrationGuides.md b/Sources/ProcessOut/ProcessOut.docc/MigrationGuides.md index 0b265a563..771858e7c 100644 --- a/Sources/ProcessOut/ProcessOut.docc/MigrationGuides.md +++ b/Sources/ProcessOut/ProcessOut.docc/MigrationGuides.md @@ -1,19 +1,131 @@ # Migration Guides +## Migrating from versions < 5.0.0 + +- ``ProcessOutConfiguration`` is no longer created using the `func production(...)` static method. Instead, a regular +`init` should be used. See the example below: + + * Also it is no longer possible to pass the version directly when creating a `ProcessOutConfiguration`. Instead, set the +application object with the version. + +```swift +let configuration = ProcessOutConfiguration( + projectId: "test-proj_XZY", + application: .init(name: "Example", version: "1.0.0"), + isDebug: true, + isTelemetryEnabled: true +) +``` + +- The ``ProcessOut/ProcessOut/shared`` instance must be [configured](``ProcessOut/ProcessOut/configure(configuration:force:)``) +exclusively on the main actor's thread. However, once configured, it is safe to use in multithreaded environment. + +- It is no longer necessary to notify the framework of inbound deep links using +the `ProcessOut.shared.processDeepLink(url: url)` method. This method can be safely removed, as deep link processing +for both 3DS and alternative payments is now handled internally by the framework. + +### Cards and Customer Tokens + +- The card properties `scheme`, `coScheme`, `preferredScheme`, and `cvcCheck`, previously represented as strings, +are now represented by typed values: ``POCardScheme`` and ``POCardCvcCheck``. If you need to access the raw string +representation, use the `rawValue` property. This affects: + + * ``POCardTokenizationRequest`` + * ``POCardUpdateRequest`` + * ``POCard`` + * ``POCardIssuerInformation`` + * ``POAssignCustomerTokenRequest`` + * ``POInvoiceAuthorizationRequest`` + * `POCardTokenizationDelegate` + * `POCardUpdateInformation` + +### 3D Secure + +- ``PO3DSService`` has been migrated to structured concurrency. To adapt your existing implementation, you could wrap it +using `withCheckedThrowingContinuation`. See the example below: + +```swift +func authenticationRequestParameters( + configuration: PO3DS2Configuration +) async throws -> PO3DS2AuthenticationRequestParameters { + try await withCheckedThrowingContinuation { completion in + + } +} +``` + +Additionally: + +- The method `func authenticationRequest(configuration:completion:)` has been renamed to +``PO3DSService/authenticationRequestParameters(configuration:)``. + +- The method `func handle(challenge:completion:)` has been renamed to ``PO3DSService/performChallenge(with:)``. In the +previous implementation, you were required to return a boolean value indicating whether the challenge was performed +successfully. In the new implementation, you should return a ``PO3DS2ChallengeResult``, which can be created using either +a string value or a boolean, depending on your 3DS provider. + +- `POTest3DSService` is no longer part of the `ProcessOut` module. It can now be found in the `ProcessOutUI` module. + +- The `PO3DS2Configuration`'s ``PO3DS2Configuration/scheme`` is no longer represented by `PO3DS2ConfigurationCardScheme`. It is now +represented by ``POCardScheme``, consistent with other card-related entities. + +- Requests that previously exposed the `enableThreeDS2` property no longer do so. This property is now considered an +implementation detail and should always be set to `true` for API requests made from mobile. This affects: + + * ``POAssignCustomerTokenRequest`` + * ``POInvoiceAuthorizationRequest`` + +### Alternative Payments + +- `POAlternativePaymentMethodsService` has been renamed to ``POAlternativePaymentsService`` and can now be accessed +using the ``ProcessOut/ProcessOut/alternativePayments`` method. + + * `POAlternativePaymentTokenizationRequest` has been removed. Instead, use the dedicated `POAlternativePaymentTokenizationRequest` +for tokenizing alternative payment methods (APMs) or `POAlternativePaymentAuthorizationRequest` for authorizing them. + + * `POAlternativePaymentMethodResponse` has been replaced with ``POAlternativePaymentResponse``. + + * `POAlternativePaymentsService` provides functionality to create a URL for tokenization or authorization. However, +the recommended approach is to use the ``POAlternativePaymentsService/authenticate(using:)`` and/or +``POAlternativePaymentsService/tokenize(request:)`` methods that handle the entire request process, including URL +preparation and the actual redirect. + + * It is no longer possible to process alternative payments using view controllers created by +`POAlternativePaymentMethodViewControllerBuilder` or by the `SFSafariViewController`-based handler defined in the +`ProcessOutUI` module. Instead, use one of the methods in `POAlternativePaymentsService` to handle the payment/tokenization. + +### Deprecations + +- Previously deprecated declaration were removed: + + * `POTest3DSService` has been replaced by a new `POTest3DSService` defined in the ProcessOutUI module. + + * Legacy API bindings have been removed. Instead, you should use the services and repositories available in +``ProcessOut/ProcessOut/shared`` to interact with the API. + + * `PO3DSRedirectViewControllerBuilder` has been removed, as 3DS redirects are now handled internally by the SDK. + + * `PONativeAlternativePaymentMethodViewControllerBuilder` has been removed. Instead, `import ProcessOutUI` module +and instantiate `PONativeAlternativePaymentView` (or view controller) directly. For additional details, please refer to +the [documentation](https://swiftpackageindex.com/processout/processout-ios/documentation/processoutui/nativealternativepayment). + + * Deprecated protocol aliases with the `Type` suffix have been removed. Please use their counterparts without the +suffix. For example, refer to `POGatewayConfigurationsRepository` instead of `POGatewayConfigurationsRepositoryType`. + ## Migrating from versions < 4.11.0 - UI available in `ProcessOut` package is deprecated. `ProcessOutUI` package should be imported instead. Please see [documentation](https://swiftpackageindex.com/processout/processout-ios/documentation/processoutui) for details. - `PONativeAlternativePaymentViewController` should be used instead of -``PONativeAlternativePaymentMethodViewControllerBuilder``. Please note that new module is built on top of SwiftUI +`PONativeAlternativePaymentMethodViewControllerBuilder`. Please note that new module is built on top of SwiftUI so existing styling is incompatible since no longer relies on UIKit types. - - ``POAlternativePaymentMethodViewControllerBuilder`` is deprecated, use `SFSafariViewController` init that accepts -``POAlternativePaymentMethodRequest`` directly. + - `POAlternativePaymentMethodViewControllerBuilder` is deprecated, use `SFSafariViewController` init that accepts +`POAlternativePaymentMethodRequest` directly. - - ``PO3DSRedirectViewControllerBuilder`` is deprecated, use `SFSafariViewController` init that accepts -``PO3DSRedirect`` directly. + - `PO3DSRedirectViewControllerBuilder` is deprecated, use `SFSafariViewController` init that accepts +`PO3DSRedirect` directly. ## Migrating from versions < 4.0.0 @@ -24,16 +136,16 @@ shared instance. - `POAnyEncodable` utility was removed. -- ``PO3DSRedirectViewControllerBuilder``, ``PONativeAlternativePaymentMethodViewControllerBuilder`` and -``POAlternativePaymentMethodViewControllerBuilder`` should be created via init and configured with instance methods. +- `PO3DSRedirectViewControllerBuilder`, `PONativeAlternativePaymentMethodViewControllerBuilder` and +`POAlternativePaymentMethodViewControllerBuilder` should be created via init and configured with instance methods. Static factory methods were deprecated. -- `POInputFormStyle` was replaced with ``POInputStyle``. It no longer holds information about title and description +- `POInputFormStyle` was replaced with `POInputStyle`. It no longer holds information about title and description and solely describes input field. - `POBackgroundDecorationStyle` style was removed. -- ``PONativeAlternativePaymentMethodStyle`` was revorked. +- `PONativeAlternativePaymentMethodStyle` was revorked. - Instead of specifing input's title and description style via `POInputFormStyle`, `sectionTitle` and `errorDescription` properties should be used. @@ -41,20 +153,20 @@ and solely describes input field. - `primaryButton` and `secondaryButton` along with other properties describing actions style are now living in `PONativeAlternativePaymentMethodActionsStyle`. - - Background style is defined by ``PONativeAlternativePaymentMethodBackgroundStyle``. + - Background style is defined by `PONativeAlternativePaymentMethodBackgroundStyle`. - It is now mandatory to notify SDK about incoming deep links by calling `ProcessOut.shared.processDeepLink(url: url)` method. -- It is now mandatory to pass return url via `func with(returnUrl: URL)` when using ``PO3DSRedirectViewControllerBuilder`` -or ``POAlternativePaymentMethodViewControllerBuilder``. +- It is now mandatory to pass return url via `func with(returnUrl: URL)` when using `PO3DSRedirectViewControllerBuilder` +or `POAlternativePaymentMethodViewControllerBuilder`. ## Migrating from versions < 3.0.0 - Instead of `ProcessOut.Setup(projectId: String)` there is new method that should be used to configure SDK ``ProcessOut/configure(configuration:force:). -- `ProcessOut` was renamed to ``ProcessOutLegacyApi`` to avoid shadowing module name as it may cause issues. For more +- `ProcessOut` was renamed to `ProcessOutLegacyApi` to avoid shadowing module name as it may cause issues. For more information, see [Swift Issue](https://github.com/apple/swift/issues/56573). - `CustomerRequest`, `CustomerTokenRequest`, `Invoice`, `WebViewReturn`, `ApiResponse` and deprecated declarations diff --git a/Sources/ProcessOut/ProcessOut.docc/NativeAlternativePaymentMethod.md b/Sources/ProcessOut/ProcessOut.docc/NativeAlternativePaymentMethod.md deleted file mode 100644 index 75d1a8b38..000000000 --- a/Sources/ProcessOut/ProcessOut.docc/NativeAlternativePaymentMethod.md +++ /dev/null @@ -1,71 +0,0 @@ -# Getting Started with Native Alternative Payments - -## Overview - -Whenever you decide to initiate a payment, you should use ``PONativeAlternativePaymentMethodViewControllerBuilder`` to -create a view controller. - -The most basic implementation may look like following: - -```swift -let viewController = PONativeAlternativePaymentMethodViewControllerBuilder() - .with(invoiceId: "invoice_id") - .with(gatewayConfigurationId: "gateway_configuration_id") - .with { result in - // TODO: Handle result and hide controller - } - .build() -``` - -It's a caller's responsibility to present created controller in a preferred way. One could do it modally or by pushing -onto navigation stack or any other way. - -## Customization - -View controller could be further customized via builder, this includes both UI, and its behavior. - -In order to tweak controller's styling, you should create an instance of ``PONativeAlternativePaymentMethodStyle`` structure -and pass it to ``PONativeAlternativePaymentMethodViewControllerBuilder/with(style:)`` method when creating controller. - -Let's say you want to change pay button's appearance: - -```swift -let payButtonStyle = POButtonStyle( - normal: .init( - title: .init(color: .black, typography: .init(font: .systemFont(ofSize: 14), textStyle: .body)), - border: .clear(radius: 8), - shadow: .clear, - backgroundColor: .green - ), - highlighted: .init( - title: .init(color: .black, typography: .init(font: .systemFont(ofSize: 14), textStyle: .body)), - border: .clear(radius: 8), - shadow: .clear, - backgroundColor: .yellow - ), - disabled: .init( - title: .init(color: .gray, typography: .init(font: .systemFont(ofSize: 14), textStyle: .body)), - border: .clear(radius: 0), - shadow: .clear, - backgroundColor: .darkGray - ), - activityIndicator: .system(.medium) -) -let style = PONativeAlternativePaymentMethodStyle( - actions: .init(primary: payButtonStyle, axis: .vertical) -) -let viewController = builder.with(style: style).build() -``` - -Rather than changing styling you could change displayed data, for example to change screen's title and action button text -you could do: - -```swift -let configuration = PONativeAlternativePaymentMethodConfiguration( - title: "Pay here", primaryActionTitle: "Submit" -) -let viewController = builder.with(configuration: configuration).build() -``` - -You can also pass delegate that conforms to ``PONativeAlternativePaymentMethodDelegate`` to be notified of events and -alter run-time behaviors, use ``PONativeAlternativePaymentMethodViewControllerBuilder/with(delegate:)`` to do so. diff --git a/Sources/ProcessOut/ProcessOut.docc/ProcessOut.md b/Sources/ProcessOut/ProcessOut.docc/ProcessOut.md index 2ea7ff055..6367a045a 100644 --- a/Sources/ProcessOut/ProcessOut.docc/ProcessOut.md +++ b/Sources/ProcessOut/ProcessOut.docc/ProcessOut.md @@ -26,35 +26,16 @@ All errors that could happen as a result of interaction with the SDK are represe - ``POFailure`` -### 3DS +### 3D Secure - - ``PO3DSService`` - ``PO3DS2Configuration`` -- ``PO3DS2ConfigurationCardScheme`` +- ``PO3DS2AuthenticationRequestParameters`` +- ``PO3DS2ChallengeParameters`` +- ``PO3DS2ChallengeResult`` - ``PO3DS2AuthenticationRequest`` - ``PO3DS2Challenge`` -- ``PO3DSRedirect`` -- ``PO3DSRedirectViewControllerBuilder`` -- ``POTest3DSService`` - -### Native Alternative Payment Method - -- -- ``PONativeAlternativePaymentMethodViewControllerBuilder`` -- ``PONativeAlternativePaymentMethodStyle`` -- ``PONativeAlternativePaymentMethodActionsStyle`` -- ``PONativeAlternativePaymentMethodBackgroundStyle`` -- ``PONativeAlternativePaymentMethodConfiguration`` -- ``PONativeAlternativePaymentMethodEvent`` -- ``PONativeAlternativePaymentMethodDelegate`` - -### Alternative Payment Method - -- ``POAlternativePaymentMethodViewControllerBuilder`` -- ``POAlternativePaymentMethodsService`` -- ``POAlternativePaymentMethodRequest`` -- ``POAlternativePaymentMethodResponse`` ### Cards @@ -101,24 +82,12 @@ All errors that could happen as a result of interaction with the SDK are represe - ``PONativeAlternativePaymentMethodState`` -### Appearance - -Types that describe properties such as shadow and border. And style of higher level components, for example buttons and inputs. - -- ``POShadowStyle`` -- ``POBorderStyle`` -- ``POTypography`` -- ``POTextStyle`` -- ``POInputStyle`` -- ``POInputStateStyle`` -- ``PORadioButtonStyle`` -- ``PORadioButtonStateStyle`` -- ``PORadioButtonKnobStateStyle`` -- ``POButtonStyle`` -- ``POButtonStateStyle`` -- ``POActivityIndicatorStyle`` -- ``POActivityIndicatorView`` -- ``POActionsContainerStyle`` +### Alternative Payments + +- ``POAlternativePaymentsService`` +- ``POAlternativePaymentAuthorizationRequest`` +- ``POAlternativePaymentTokenizationRequest`` +- ``POAlternativePaymentResponse`` ### Images Utils @@ -129,36 +98,9 @@ Types that describe properties such as shadow and border. And style of higher le - ``POPaginationOptions`` - ``POCancellable`` -- ``POImmutableExcludedCodable`` -- ``POImmutableStringCodableDecimal`` -- ``POImmutableStringCodableOptionalDecimal`` -- ``POFallbackDecodable`` -- ``POFallbackValueProvider`` -- ``POEmptyStringProvider`` -- ``POTypedRepresentation`` +- ``POStringCodableDecimal`` +- ``POStringCodableOptionalDecimal`` - ``POStringDecodableMerchantCapability`` - ``POBillingAddressCollectionMode`` - ``PORepository`` - ``POService`` -- ``POAutoAsync`` -- ``POAutoCompletion`` - -### Legacy Declarations - -There are outdated declaration that only exist for backward compatibility with old SDK, they will be removed when -full feature parity is reached. See ``ProcessOutLegacyApi`` for possible methods. - -- ``ProcessOutLegacyApi`` -- ``TokenRequest`` -- ``AuthorizationRequest`` -- ``GatewayConfiguration`` -- ``APMTokenReturn`` -- ``ThreeDSHandler`` -- ``ThreeDSTestHandler`` -- ``ThreeDSFingerprintResponse`` -- ``CustomerAction`` -- ``DirectoryServerData`` -- ``AuthentificationChallengeData`` -- ``ProcessOutWebView`` -- ``FingerPrintWebViewSchemeHandler`` -- ``ProcessOutException`` diff --git a/Sources/ProcessOut/Resources/Colors.xcassets/Action/Border/Contents.json b/Sources/ProcessOut/Resources/Colors.xcassets/Action/Border/Contents.json deleted file mode 100644 index 6e965652d..000000000 --- a/Sources/ProcessOut/Resources/Colors.xcassets/Action/Border/Contents.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - }, - "properties" : { - "provides-namespace" : true - } -} diff --git a/Sources/ProcessOut/Resources/Colors.xcassets/Action/Border/Disabled.colorset/Contents.json b/Sources/ProcessOut/Resources/Colors.xcassets/Action/Border/Disabled.colorset/Contents.json deleted file mode 100644 index 488c95805..000000000 --- a/Sources/ProcessOut/Resources/Colors.xcassets/Action/Border/Disabled.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0xC7", - "green" : "0xC4", - "red" : "0xC4" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0x45", - "green" : "0x39", - "red" : "0x36" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Sources/ProcessOut/Resources/Colors.xcassets/Action/Border/Selected.colorset/Contents.json b/Sources/ProcessOut/Resources/Colors.xcassets/Action/Border/Selected.colorset/Contents.json deleted file mode 100644 index 2b1434bfd..000000000 --- a/Sources/ProcessOut/Resources/Colors.xcassets/Action/Border/Selected.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0x55", - "green" : "0x4E", - "red" : "0x4E" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0xC5", - "green" : "0xBA", - "red" : "0xBA" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Sources/ProcessOut/Resources/Colors.xcassets/Action/Contents.json b/Sources/ProcessOut/Resources/Colors.xcassets/Action/Contents.json deleted file mode 100644 index 6e965652d..000000000 --- a/Sources/ProcessOut/Resources/Colors.xcassets/Action/Contents.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - }, - "properties" : { - "provides-namespace" : true - } -} diff --git a/Sources/ProcessOut/Resources/Colors.xcassets/Action/Primary/Contents.json b/Sources/ProcessOut/Resources/Colors.xcassets/Action/Primary/Contents.json deleted file mode 100644 index 6e965652d..000000000 --- a/Sources/ProcessOut/Resources/Colors.xcassets/Action/Primary/Contents.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - }, - "properties" : { - "provides-namespace" : true - } -} diff --git a/Sources/ProcessOut/Resources/Colors.xcassets/Action/Primary/Default.colorset/Contents.json b/Sources/ProcessOut/Resources/Colors.xcassets/Action/Primary/Default.colorset/Contents.json deleted file mode 100644 index 550d35a6c..000000000 --- a/Sources/ProcessOut/Resources/Colors.xcassets/Action/Primary/Default.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0x93", - "green" : "0x2A", - "red" : "0x2B" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0xD8", - "green" : "0x64", - "red" : "0x6A" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Sources/ProcessOut/Resources/Colors.xcassets/Action/Primary/Disabled.colorset/Contents.json b/Sources/ProcessOut/Resources/Colors.xcassets/Action/Primary/Disabled.colorset/Contents.json deleted file mode 100644 index d39f4721f..000000000 --- a/Sources/ProcessOut/Resources/Colors.xcassets/Action/Primary/Disabled.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0xE7", - "green" : "0xE5", - "red" : "0xE5" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0x3A", - "green" : "0x2D", - "red" : "0x2B" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Sources/ProcessOut/Resources/Colors.xcassets/Action/Primary/Pressed.colorset/Contents.json b/Sources/ProcessOut/Resources/Colors.xcassets/Action/Primary/Pressed.colorset/Contents.json deleted file mode 100644 index 1d51fe8c2..000000000 --- a/Sources/ProcessOut/Resources/Colors.xcassets/Action/Primary/Pressed.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0x76", - "green" : "0x1E", - "red" : "0x1E" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0xC5", - "green" : "0x49", - "red" : "0x4D" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Sources/ProcessOut/Resources/Colors.xcassets/Action/Secondary/Contents.json b/Sources/ProcessOut/Resources/Colors.xcassets/Action/Secondary/Contents.json deleted file mode 100644 index 6e965652d..000000000 --- a/Sources/ProcessOut/Resources/Colors.xcassets/Action/Secondary/Contents.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - }, - "properties" : { - "provides-namespace" : true - } -} diff --git a/Sources/ProcessOut/Resources/Colors.xcassets/Action/Secondary/Default.colorset/Contents.json b/Sources/ProcessOut/Resources/Colors.xcassets/Action/Secondary/Default.colorset/Contents.json deleted file mode 100644 index c7c8eace4..000000000 --- a/Sources/ProcessOut/Resources/Colors.xcassets/Action/Secondary/Default.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0xFF", - "green" : "0xFF", - "red" : "0xFF" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0x21", - "green" : "0x14", - "red" : "0x12" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Sources/ProcessOut/Resources/Colors.xcassets/Action/Secondary/Pressed.colorset/Contents.json b/Sources/ProcessOut/Resources/Colors.xcassets/Action/Secondary/Pressed.colorset/Contents.json deleted file mode 100644 index 97cccf76f..000000000 --- a/Sources/ProcessOut/Resources/Colors.xcassets/Action/Secondary/Pressed.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0xE7", - "green" : "0xE5", - "red" : "0xE5" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0x2A", - "green" : "0x1A", - "red" : "0x18" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Sources/ProcessOut/Resources/Colors.xcassets/Border/Contents.json b/Sources/ProcessOut/Resources/Colors.xcassets/Border/Contents.json deleted file mode 100644 index 6e965652d..000000000 --- a/Sources/ProcessOut/Resources/Colors.xcassets/Border/Contents.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - }, - "properties" : { - "provides-namespace" : true - } -} diff --git a/Sources/ProcessOut/Resources/Colors.xcassets/Border/Default.colorset/Contents.json b/Sources/ProcessOut/Resources/Colors.xcassets/Border/Default.colorset/Contents.json deleted file mode 100644 index 3e249532e..000000000 --- a/Sources/ProcessOut/Resources/Colors.xcassets/Border/Default.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0x95", - "green" : "0x8D", - "red" : "0x8D" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0x9F", - "green" : "0x94", - "red" : "0x93" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Sources/ProcessOut/Resources/Colors.xcassets/Border/Divider.colorset/Contents.json b/Sources/ProcessOut/Resources/Colors.xcassets/Border/Divider.colorset/Contents.json deleted file mode 100644 index 6a820d253..000000000 --- a/Sources/ProcessOut/Resources/Colors.xcassets/Border/Divider.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0xF3", - "green" : "0xF2", - "red" : "0xF2" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0x31", - "green" : "0x24", - "red" : "0x21" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Sources/ProcessOut/Resources/Colors.xcassets/Border/Subtle.colorset/Contents.json b/Sources/ProcessOut/Resources/Colors.xcassets/Border/Subtle.colorset/Contents.json deleted file mode 100644 index d39f4721f..000000000 --- a/Sources/ProcessOut/Resources/Colors.xcassets/Border/Subtle.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0xE7", - "green" : "0xE5", - "red" : "0xE5" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0x3A", - "green" : "0x2D", - "red" : "0x2B" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Sources/ProcessOut/Resources/Colors.xcassets/Contents.json b/Sources/ProcessOut/Resources/Colors.xcassets/Contents.json deleted file mode 100644 index 73c00596a..000000000 --- a/Sources/ProcessOut/Resources/Colors.xcassets/Contents.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Sources/ProcessOut/Resources/Colors.xcassets/Surface/Background.colorset/Contents.json b/Sources/ProcessOut/Resources/Colors.xcassets/Surface/Background.colorset/Contents.json deleted file mode 100644 index c7c8eace4..000000000 --- a/Sources/ProcessOut/Resources/Colors.xcassets/Surface/Background.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0xFF", - "green" : "0xFF", - "red" : "0xFF" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0x21", - "green" : "0x14", - "red" : "0x12" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Sources/ProcessOut/Resources/Colors.xcassets/Surface/Contents.json b/Sources/ProcessOut/Resources/Colors.xcassets/Surface/Contents.json deleted file mode 100644 index 6e965652d..000000000 --- a/Sources/ProcessOut/Resources/Colors.xcassets/Surface/Contents.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - }, - "properties" : { - "provides-namespace" : true - } -} diff --git a/Sources/ProcessOut/Resources/Colors.xcassets/Surface/Error.colorset/Contents.json b/Sources/ProcessOut/Resources/Colors.xcassets/Surface/Error.colorset/Contents.json deleted file mode 100644 index fe234b0de..000000000 --- a/Sources/ProcessOut/Resources/Colors.xcassets/Surface/Error.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0xD4", - "green" : "0xD1", - "red" : "0xFA" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0x2B", - "green" : "0x21", - "red" : "0xC0" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Sources/ProcessOut/Resources/Colors.xcassets/Surface/Level1.colorset/Contents.json b/Sources/ProcessOut/Resources/Colors.xcassets/Surface/Level1.colorset/Contents.json deleted file mode 100644 index d96fa9359..000000000 --- a/Sources/ProcessOut/Resources/Colors.xcassets/Surface/Level1.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0xFD", - "green" : "0xFC", - "red" : "0xFC" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0x22", - "green" : "0x13", - "red" : "0x11" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Sources/ProcessOut/Resources/Colors.xcassets/Surface/Neutral.colorset/Contents.json b/Sources/ProcessOut/Resources/Colors.xcassets/Surface/Neutral.colorset/Contents.json deleted file mode 100644 index 1fb6cc9e5..000000000 --- a/Sources/ProcessOut/Resources/Colors.xcassets/Surface/Neutral.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0xF8", - "green" : "0xF7", - "red" : "0xF7" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0x2A", - "green" : "0x1A", - "red" : "0x18" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Sources/ProcessOut/Resources/Colors.xcassets/Surface/Success.colorset/Contents.json b/Sources/ProcessOut/Resources/Colors.xcassets/Surface/Success.colorset/Contents.json deleted file mode 100644 index f07eb7907..000000000 --- a/Sources/ProcessOut/Resources/Colors.xcassets/Surface/Success.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0xE5", - "green" : "0xF8", - "red" : "0xD8" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0x21", - "green" : "0x4B", - "red" : "0x01" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Sources/ProcessOut/Resources/Colors.xcassets/Surface/Warning.colorset/Contents.json b/Sources/ProcessOut/Resources/Colors.xcassets/Surface/Warning.colorset/Contents.json deleted file mode 100644 index 0f60b378d..000000000 --- a/Sources/ProcessOut/Resources/Colors.xcassets/Surface/Warning.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0xE3", - "green" : "0xEB", - "red" : "0xFD" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0x07", - "green" : "0x38", - "red" : "0xA2" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Sources/ProcessOut/Resources/Colors.xcassets/Text/Contents.json b/Sources/ProcessOut/Resources/Colors.xcassets/Text/Contents.json deleted file mode 100644 index 6e965652d..000000000 --- a/Sources/ProcessOut/Resources/Colors.xcassets/Text/Contents.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - }, - "properties" : { - "provides-namespace" : true - } -} diff --git a/Sources/ProcessOut/Resources/Colors.xcassets/Text/Disabled.colorset/Contents.json b/Sources/ProcessOut/Resources/Colors.xcassets/Text/Disabled.colorset/Contents.json deleted file mode 100644 index e0c50a04b..000000000 --- a/Sources/ProcessOut/Resources/Colors.xcassets/Text/Disabled.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0x95", - "green" : "0x8D", - "red" : "0x8D" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0x5B", - "green" : "0x4F", - "red" : "0x4D" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Sources/ProcessOut/Resources/Colors.xcassets/Text/Error.colorset/Contents.json b/Sources/ProcessOut/Resources/Colors.xcassets/Text/Error.colorset/Contents.json deleted file mode 100644 index 9f7d57fb8..000000000 --- a/Sources/ProcessOut/Resources/Colors.xcassets/Text/Error.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0x17", - "green" : "0x0F", - "red" : "0x7B" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0xEB", - "green" : "0xE9", - "red" : "0xFB" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Sources/ProcessOut/Resources/Colors.xcassets/Text/Muted.colorset/Contents.json b/Sources/ProcessOut/Resources/Colors.xcassets/Text/Muted.colorset/Contents.json deleted file mode 100644 index 0738113d1..000000000 --- a/Sources/ProcessOut/Resources/Colors.xcassets/Text/Muted.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0x6F", - "green" : "0x67", - "red" : "0x67" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0x81", - "green" : "0x75", - "red" : "0x74" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Sources/ProcessOut/Resources/Colors.xcassets/Text/OnColor.colorset/Contents.json b/Sources/ProcessOut/Resources/Colors.xcassets/Text/OnColor.colorset/Contents.json deleted file mode 100644 index fd2a9d1c4..000000000 --- a/Sources/ProcessOut/Resources/Colors.xcassets/Text/OnColor.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0xFC", - "green" : "0xFC", - "red" : "0xFC" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0xFB", - "green" : "0xF8", - "red" : "0xF8" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Sources/ProcessOut/Resources/Colors.xcassets/Text/Primary.colorset/Contents.json b/Sources/ProcessOut/Resources/Colors.xcassets/Text/Primary.colorset/Contents.json deleted file mode 100644 index 86afa8f67..000000000 --- a/Sources/ProcessOut/Resources/Colors.xcassets/Text/Primary.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0x12", - "green" : "0x12", - "red" : "0x12" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0xFB", - "green" : "0xF8", - "red" : "0xF8" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Sources/ProcessOut/Resources/Colors.xcassets/Text/Secondary.colorset/Contents.json b/Sources/ProcessOut/Resources/Colors.xcassets/Text/Secondary.colorset/Contents.json deleted file mode 100644 index 4e739c49b..000000000 --- a/Sources/ProcessOut/Resources/Colors.xcassets/Text/Secondary.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0x35", - "green" : "0x31", - "red" : "0x31" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0xDD", - "green" : "0xD4", - "red" : "0xD4" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Sources/ProcessOut/Resources/Colors.xcassets/Text/Success.colorset/Contents.json b/Sources/ProcessOut/Resources/Colors.xcassets/Text/Success.colorset/Contents.json deleted file mode 100644 index ed0295a14..000000000 --- a/Sources/ProcessOut/Resources/Colors.xcassets/Text/Success.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0x21", - "green" : "0x4B", - "red" : "0x01" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0xE5", - "green" : "0xF8", - "red" : "0xD8" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Sources/ProcessOut/Resources/Colors.xcassets/Text/Tertiary.colorset/Contents.json b/Sources/ProcessOut/Resources/Colors.xcassets/Text/Tertiary.colorset/Contents.json deleted file mode 100644 index 2b1434bfd..000000000 --- a/Sources/ProcessOut/Resources/Colors.xcassets/Text/Tertiary.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0x55", - "green" : "0x4E", - "red" : "0x4E" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0xC5", - "green" : "0xBA", - "red" : "0xBA" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Sources/ProcessOut/Resources/Colors.xcassets/Text/Warning.colorset/Contents.json b/Sources/ProcessOut/Resources/Colors.xcassets/Text/Warning.colorset/Contents.json deleted file mode 100644 index a551f0833..000000000 --- a/Sources/ProcessOut/Resources/Colors.xcassets/Text/Warning.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0x02", - "green" : "0x27", - "red" : "0x74" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0xE3", - "green" : "0xEB", - "red" : "0xFD" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Sources/ProcessOut/Resources/Fonts/WorkSans-Italic.ttf b/Sources/ProcessOut/Resources/Fonts/WorkSans-Italic.ttf deleted file mode 100644 index 03e9671c1..000000000 Binary files a/Sources/ProcessOut/Resources/Fonts/WorkSans-Italic.ttf and /dev/null differ diff --git a/Sources/ProcessOut/Resources/Fonts/WorkSans.ttf b/Sources/ProcessOut/Resources/Fonts/WorkSans.ttf deleted file mode 100644 index 0a2b4de92..000000000 Binary files a/Sources/ProcessOut/Resources/Fonts/WorkSans.ttf and /dev/null differ diff --git a/Sources/ProcessOut/Resources/Images.xcassets/ChevronDown.imageset/ChevronDownDark.pdf b/Sources/ProcessOut/Resources/Images.xcassets/ChevronDown.imageset/ChevronDownDark.pdf deleted file mode 100644 index 482ce80ff..000000000 Binary files a/Sources/ProcessOut/Resources/Images.xcassets/ChevronDown.imageset/ChevronDownDark.pdf and /dev/null differ diff --git a/Sources/ProcessOut/Resources/Images.xcassets/ChevronDown.imageset/ChevronDownLight.pdf b/Sources/ProcessOut/Resources/Images.xcassets/ChevronDown.imageset/ChevronDownLight.pdf deleted file mode 100644 index 4106ba1a6..000000000 Binary files a/Sources/ProcessOut/Resources/Images.xcassets/ChevronDown.imageset/ChevronDownLight.pdf and /dev/null differ diff --git a/Sources/ProcessOut/Resources/Images.xcassets/ChevronDown.imageset/Contents.json b/Sources/ProcessOut/Resources/Images.xcassets/ChevronDown.imageset/Contents.json deleted file mode 100644 index 7ebcb5487..000000000 --- a/Sources/ProcessOut/Resources/Images.xcassets/ChevronDown.imageset/Contents.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "images" : [ - { - "filename" : "ChevronDownLight.pdf", - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "filename" : "ChevronDownDark.pdf", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Sources/ProcessOut/Resources/Images.xcassets/Contents.json b/Sources/ProcessOut/Resources/Images.xcassets/Contents.json deleted file mode 100644 index 73c00596a..000000000 --- a/Sources/ProcessOut/Resources/Images.xcassets/Contents.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Sources/ProcessOut/Resources/Images.xcassets/Success.imageset/Contents.json b/Sources/ProcessOut/Resources/Images.xcassets/Success.imageset/Contents.json deleted file mode 100644 index 7bc8e3ebe..000000000 --- a/Sources/ProcessOut/Resources/Images.xcassets/Success.imageset/Contents.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "images" : [ - { - "filename" : "Success.pdf", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - }, - "properties" : { - "template-rendering-intent" : "template" - } -} diff --git a/Sources/ProcessOut/Resources/Images.xcassets/Success.imageset/Success.pdf b/Sources/ProcessOut/Resources/Images.xcassets/Success.imageset/Success.pdf deleted file mode 100644 index 9473521b6..000000000 Binary files a/Sources/ProcessOut/Resources/Images.xcassets/Success.imageset/Success.pdf and /dev/null differ diff --git a/Sources/ProcessOut/Resources/Localizable.xcstrings b/Sources/ProcessOut/Resources/Localizable.xcstrings index 7a2ae5b9d..c82776e74 100644 --- a/Sources/ProcessOut/Resources/Localizable.xcstrings +++ b/Sources/ProcessOut/Resources/Localizable.xcstrings @@ -1,606 +1,37 @@ { "sourceLanguage" : "en", "strings" : { - "native-alternative-payment.cancel-button.title" : { + "dummy" : { + "extractionState" : "manual", "localizations" : { "ar" : { "stringUnit" : { "state" : "translated", - "value" : "إلغاء" + "value" : "N/A" } }, "en" : { "stringUnit" : { "state" : "translated", - "value" : "Cancel" + "value" : "N/A" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Annuler" + "value" : "N/A" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Anuluj" + "value" : "N/A" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", - "value" : "Cancelar" - } - } - } - }, - "native-alternative-payment.email.placeholder" : { - "localizations" : { - "ar" : { - "stringUnit" : { - "state" : "translated", - "value" : "name@example.com" - } - }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "name@example.com" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "nom@exemple.fr" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "imię@przykład.pl" - } - }, - "pt-PT" : { - "stringUnit" : { - "state" : "translated", - "value" : "nome@exemplo.pt" - } - } - } - }, - "native-alternative-payment.error.invalid-email" : { - "localizations" : { - "ar" : { - "stringUnit" : { - "state" : "translated", - "value" : "البريد الإلكتروني غير صالح." - } - }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Email is not valid." - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Votre adresse e-mail est invalide." - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Niepoprawny adres email." - } - }, - "pt-PT" : { - "stringUnit" : { - "state" : "translated", - "value" : "Endereço de e-mail inválido." - } - } - } - }, - "native-alternative-payment.error.invalid-length-%d" : { - "localizations" : { - "ar" : { - "variations" : { - "plural" : { - "one" : { - "stringUnit" : { - "state" : "translated", - "value" : "إن الطول غير صالح، الرجاء التقيد بحرف واحد فقط." - } - }, - "other" : { - "stringUnit" : { - "state" : "translated", - "value" : "إن الطول غير صالح، الرجاء التقيد بـ %d حرفًا." - } - } - } - } - }, - "en" : { - "variations" : { - "plural" : { - "one" : { - "stringUnit" : { - "state" : "translated", - "value" : "Invalid length, expected %d character." - } - }, - "other" : { - "stringUnit" : { - "state" : "translated", - "value" : "Invalid length, expected %d characters." - } - } - } - } - }, - "fr" : { - "variations" : { - "plural" : { - "one" : { - "stringUnit" : { - "state" : "translated", - "value" : "Longueur incorrecte, %d caractère est attendu." - } - }, - "other" : { - "stringUnit" : { - "state" : "translated", - "value" : "Longueur incorrecte, %d caractères sont attendus." - } - } - } - } - }, - "pl" : { - "variations" : { - "plural" : { - "one" : { - "stringUnit" : { - "state" : "translated", - "value" : "Nieprawidłowa długość. Oczekiwano %d znaku." - } - }, - "other" : { - "stringUnit" : { - "state" : "translated", - "value" : "Nieprawidłowa długość. Oczekiwano %d znaków." - } - } - } - } - }, - "pt-PT" : { - "variations" : { - "plural" : { - "one" : { - "stringUnit" : { - "state" : "translated", - "value" : "Quantidade de caracteres incorreta, espera-se %d caractere." - } - }, - "other" : { - "stringUnit" : { - "state" : "translated", - "value" : "Quantidade de caracteres incorreta, espera-se %d caracteres." - } - } - } - } - } - } - }, - "native-alternative-payment.error.invalid-number" : { - "localizations" : { - "ar" : { - "stringUnit" : { - "state" : "translated", - "value" : "الرقم غير صحيح." - } - }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Number is not valid." - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Numéro invalide." - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Niepoprawny numer." - } - }, - "pt-PT" : { - "stringUnit" : { - "state" : "translated", - "value" : "Número inválido." - } - } - } - }, - "native-alternative-payment.error.invalid-phone" : { - "localizations" : { - "ar" : { - "stringUnit" : { - "state" : "translated", - "value" : "رقم الهاتف غير صحيح." - } - }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Phone number is not valid." - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Votre numéro de téléphone est invalide." - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Niepoprawny numer telefonu." - } - }, - "pt-PT" : { - "stringUnit" : { - "state" : "translated", - "value" : "Número de telemóvel inválido." - } - } - } - }, - "native-alternative-payment.error.invalid-value" : { - "localizations" : { - "ar" : { - "stringUnit" : { - "state" : "translated", - "value" : "القيمة غير صحيحة." - } - }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Value is not valid." - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Valeur invalide." - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Niepoprawna wartość." - } - }, - "pt-PT" : { - "stringUnit" : { - "state" : "translated", - "value" : "Texto inválido." - } - } - } - }, - "native-alternative-payment.error.required-parameter" : { - "localizations" : { - "ar" : { - "stringUnit" : { - "state" : "translated", - "value" : "البيانات مطلوبة." - } - }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Parameter is required." - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Paramètre requis." - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Parametr jest wmagany." - } - }, - "pt-PT" : { - "stringUnit" : { - "state" : "translated", - "value" : "Campo obrigatório." - } - } - } - }, - "native-alternative-payment.phone.placeholder" : { - "localizations" : { - "ar" : { - "stringUnit" : { - "state" : "translated", - "value" : "أدخل رقم الهاتف" - } - }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Enter phone number" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Entrez votre numéro de téléphone" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Twój numer telefonu" - } - }, - "pt-PT" : { - "stringUnit" : { - "state" : "translated", - "value" : "Insira o seu número de telemóvel" - } - } - } - }, - "native-alternative-payment.submit-button.default-title" : { - "localizations" : { - "ar" : { - "stringUnit" : { - "state" : "translated", - "value" : "الدفع" - } - }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Pay" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Payer" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Zapłać" - } - }, - "pt-PT" : { - "stringUnit" : { - "state" : "translated", - "value" : "Pagar" - } - } - } - }, - "native-alternative-payment.submit-button.title" : { - "localizations" : { - "ar" : { - "stringUnit" : { - "state" : "translated", - "value" : "الدفع %@" - } - }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Pay %@" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Payer %@" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Zapłać %@" - } - }, - "pt-PT" : { - "stringUnit" : { - "state" : "translated", - "value" : "Pagar %@" - } - } - } - }, - "native-alternative-payment.success.message" : { - "localizations" : { - "ar" : { - "stringUnit" : { - "state" : "translated", - "value" : "نجاح! تمت الموافقة على الدفع" - } - }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Success!\\nPayment approved." - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Succès !\nPaiement confirmé." - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Sukces!\\nPłatność przyjęta." - } - }, - "pt-PT" : { - "stringUnit" : { - "state" : "translated", - "value" : "Successo!\\nPagamento aprovado." - } - } - } - }, - "native-alternative-payment.title" : { - "localizations" : { - "ar" : { - "stringUnit" : { - "state" : "translated", - "value" : "الدفع بواسطة %@" - } - }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Pay with %@" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Payer avec %@" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Zapłać przy pomocy %@" - } - }, - "pt-PT" : { - "stringUnit" : { - "state" : "translated", - "value" : "Pagar com %@" - } - } - } - }, - "test-3ds.challenge.accept" : { - "localizations" : { - "ar" : { - "stringUnit" : { - "state" : "translated", - "value" : "قبول" - } - }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Accept" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Accepter" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Zaakceptować" - } - }, - "pt-PT" : { - "stringUnit" : { - "state" : "translated", - "value" : "Aceitar" - } - } - } - }, - "test-3ds.challenge.reject" : { - "localizations" : { - "ar" : { - "stringUnit" : { - "state" : "translated", - "value" : "رفض" - } - }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Reject" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Rejeter" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Odrzucić" - } - }, - "pt-PT" : { - "stringUnit" : { - "state" : "translated", - "value" : "Rejeitar" - } - } - } - }, - "test-3ds.challenge.title" : { - "localizations" : { - "ar" : { - "stringUnit" : { - "state" : "translated", - "value" : "هل ترغب في قبول تحقيق 3DS2 ؟" - } - }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Do you want to accept the 3DS2 challenge?" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Souhaitez-vous accepter le défi (challenge) 3DS2 ?" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Czy chcesz przyjąć wyzwanie 3DS2?" - } - }, - "pt-PT" : { - "stringUnit" : { - "state" : "translated", - "value" : "Você quer aceitar o desafio 3DS2?" + "value" : "N/A" } } } diff --git a/Sources/ProcessOut/Sources/Api/Builders/ProcessOutHttpConnectorBuilder.swift b/Sources/ProcessOut/Sources/Api/Builders/ProcessOutHttpConnectorBuilder.swift index 4d53a121b..97c1bb239 100644 --- a/Sources/ProcessOut/Sources/Api/Builders/ProcessOutHttpConnectorBuilder.swift +++ b/Sources/ProcessOut/Sources/Api/Builders/ProcessOutHttpConnectorBuilder.swift @@ -11,7 +11,7 @@ import Foundation final class ProcessOutHttpConnectorBuilder { /// Connector configuration provider. - var configuration: (() -> HttpConnectorRequestMapperConfiguration)? + var configuration: (@Sendable () -> HttpConnectorRequestMapperConfiguration)? /// Retry strategy to use for failing requests. var retryStrategy: RetryStrategy? = .exponential(maximumRetries: 3, interval: 0.1, rate: 3) @@ -89,7 +89,7 @@ final class ProcessOutHttpConnectorBuilder { extension ProcessOutHttpConnectorBuilder { - func with(configuration: @escaping () -> HttpConnectorRequestMapperConfiguration) -> Self { + func with(configuration: @escaping @Sendable () -> HttpConnectorRequestMapperConfiguration) -> Self { self.configuration = configuration return self } diff --git a/Sources/ProcessOut/Sources/Api/Models/PODeepLinkReceivedEvent.swift b/Sources/ProcessOut/Sources/Api/Models/PODeepLinkReceivedEvent.swift deleted file mode 100644 index 9feb2fb01..000000000 --- a/Sources/ProcessOut/Sources/Api/Models/PODeepLinkReceivedEvent.swift +++ /dev/null @@ -1,14 +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/Models/ProcessOutConfiguration.swift b/Sources/ProcessOut/Sources/Api/Models/ProcessOutConfiguration.swift index e033f92d7..d8cc84f82 100644 --- a/Sources/ProcessOut/Sources/Api/Models/ProcessOutConfiguration.swift +++ b/Sources/ProcessOut/Sources/Api/Models/ProcessOutConfiguration.swift @@ -7,15 +7,12 @@ import Foundation -@available(*, deprecated, renamed: "ProcessOutConfiguration") -public typealias ProcessOutApiConfiguration = ProcessOutConfiguration - /// Defines configuration parameters that are used to create API singleton. In order to create instance -/// of this structure one should use ``ProcessOutConfiguration/production(projectId:appVersion:isDebug:)`` +/// of this structure one should use ``ProcessOutConfiguration/init(projectId:application:isDebug:isTelemetryEnabled:)`` /// method. -public struct ProcessOutConfiguration { +public struct ProcessOutConfiguration: Sendable { - public struct Application: Hashable { + public struct Application: Hashable, Sendable { /// Application name. public let name: String? @@ -31,7 +28,7 @@ public struct ProcessOutConfiguration { /// Environment. @_spi(PO) - public struct Environment: Hashable { + public struct Environment: Hashable, Sendable { /// Api base URL. let apiBaseUrl: URL @@ -56,13 +53,6 @@ public struct ProcessOutConfiguration { /// Application name. public let application: Application? - /// Host application version. Providing this value helps ProcessOut to troubleshoot potential - /// issues. - @available(*, deprecated, renamed: "application.version") - public var appVersion: String? { - application?.version - } - /// Session ID is a constant value @_spi(PO) public let sessionId = UUID().uuidString diff --git a/Sources/ProcessOut/Sources/Api/ProcessOut.swift b/Sources/ProcessOut/Sources/Api/ProcessOut.swift index 7eb01556d..1292a7503 100644 --- a/Sources/ProcessOut/Sources/Api/ProcessOut.swift +++ b/Sources/ProcessOut/Sources/Api/ProcessOut.swift @@ -5,88 +5,44 @@ // Created by Andrii Vysotskyi on 07.10.2022. // +// swiftlint:disable implicitly_unwrapped_optional force_unwrapping + import Foundation import UIKit -@available(*, deprecated, renamed: "ProcessOut") -public typealias ProcessOutApi = ProcessOut - /// Provides access to shared api instance and a way to configure it. -/// - NOTE: Methods and properties of this class **must** be only accessed from main thread. -public final class ProcessOut { +/// - NOTE: Instance methods and properties of this class could be access from any thread. +public final class ProcessOut: @unchecked Sendable { /// Current configuration. public var configuration: ProcessOutConfiguration { - _configuration + _configuration.wrappedValue } /// Returns gateway configurations repository. - public private(set) lazy var gatewayConfigurations: POGatewayConfigurationsRepository = { - HttpGatewayConfigurationsRepository(connector: httpConnector) - }() + public private(set) var gatewayConfigurations: POGatewayConfigurationsRepository! - /// Returns invoices service. - public private(set) lazy var invoices: POInvoicesService = { - let repository = HttpInvoicesRepository(connector: httpConnector) - return DefaultInvoicesService(repository: repository, threeDSService: threeDSService, logger: serviceLogger) - }() + /// Invoices service. + public private(set) var invoices: POInvoicesService! - /// Returns alternative payment methods service. - public private(set) lazy var alternativePaymentMethods: POAlternativePaymentMethodsService = { - let serviceConfiguration: () -> AlternativePaymentMethodsServiceConfiguration = { [unowned self] in - let configuration = self.configuration - return .init(projectId: configuration.projectId, baseUrl: configuration.environment.checkoutBaseUrl) - } - return DefaultAlternativePaymentMethodsService(configuration: serviceConfiguration, logger: serviceLogger) - }() + /// Alternative payments service. + public private(set) var alternativePayments: POAlternativePaymentsService! - /// Returns cards repository. - public private(set) lazy var cards: POCardsService = { - let requestMapper = DefaultApplePayCardTokenizationRequestMapper( - contactMapper: DefaultPassKitContactMapper(logger: serviceLogger), - decoder: JSONDecoder(), - logger: serviceLogger - ) - let service = DefaultCardsService( - repository: HttpCardsRepository(connector: httpConnector), - applePayAuthorizationSession: DefaultApplePayAuthorizationSession(), - applePayCardTokenizationRequestMapper: requestMapper, - applePayErrorMapper: PODefaultPassKitPaymentErrorMapper(logger: serviceLogger) - ) - return service - }() + /// Cards service. + public private(set) var cards: POCardsService! /// Returns customer tokens service. - public private(set) lazy var customerTokens: POCustomerTokensService = { - let repository = HttpCustomerTokensRepository(connector: httpConnector) - return DefaultCustomerTokensService( - repository: repository, threeDSService: threeDSService, logger: serviceLogger - ) - }() - - /// 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)) - } + public private(set) var customerTokens: POCustomerTokensService! // MARK: - SPI /// Logger with application category. @_spi(PO) - public private(set) lazy var logger: POLogger = createLogger(for: Constants.applicationLoggerCategory) - - /// Event emitter to use for events exchange. - @_spi(PO) - public private(set) lazy var eventEmitter: POEventEmitter = LocalEventEmitter(logger: logger) + public private(set) var logger: POLogger! /// Images repository. @_spi(PO) - public private(set) lazy var images: POImagesRepository = UrlSessionImagesRepository(session: .shared) + public let images: POImagesRepository = UrlSessionImagesRepository(session: .shared) // MARK: - Private Nested Types @@ -100,58 +56,104 @@ public final class ProcessOut { // MARK: - Private Properties - @POUnfairlyLocked - private var _configuration: ProcessOutConfiguration + private var _configuration: POUnfairlyLocked - private lazy var serviceLogger: POLogger = { - createLogger(for: Constants.serviceLoggerCategory) - }() + // MARK: - Private Methods - private lazy var deviceMetadataProvider: DefaultDeviceMetadataProvider = { - let keychain = Keychain(service: Constants.bundleIdentifier) - return DefaultDeviceMetadataProvider(screen: .main, device: .current, bundle: .main, keychain: keychain) - }() + @MainActor + private init(configuration: ProcessOutConfiguration) { + self._configuration = .init(wrappedValue: configuration) + commonInit() + } + + @MainActor + private func commonInit() { + let deviceMetadataProvider = Self.createDeviceMetadataProvider() + let remoteLoggerDestination = createRemoteLoggerDestination(deviceMetadataProvider: deviceMetadataProvider) + let serviceLogger = createLogger( + for: Constants.serviceLoggerCategory, + additionalDestinations: remoteLoggerDestination + ) + logger = createLogger( + for: Constants.applicationLoggerCategory, + additionalDestinations: remoteLoggerDestination + ) + let httpConnector = createConnector(deviceMetadataProvider: deviceMetadataProvider) + let threeDSService = Self.create3DSService() + initServices(httpConnector: httpConnector, threeDSService: threeDSService, logger: serviceLogger) + } + + private func initServices(httpConnector: HttpConnector, threeDSService: ThreeDSService, logger: POLogger) { + gatewayConfigurations = HttpGatewayConfigurationsRepository( + connector: httpConnector + ) + invoices = Self.createInvoicesService( + httpConnector: httpConnector, threeDSService: threeDSService, logger: logger + ) + alternativePayments = createAlternativePaymentsService() + cards = Self.createCardsService( + httpConnector: httpConnector, logger: logger + ) + customerTokens = Self.createCustomerTokensService( + httpConnector: httpConnector, threeDSService: threeDSService, logger: logger + ) + } + + // MARK: - + + private static func createCardsService(httpConnector: HttpConnector, logger: POLogger) -> POCardsService { + let contactMapper = DefaultPassKitContactMapper(logger: logger) + let requestMapper = DefaultApplePayCardTokenizationRequestMapper( + contactMapper: contactMapper, decoder: JSONDecoder(), logger: logger + ) + let service = DefaultCardsService( + repository: HttpCardsRepository(connector: httpConnector), + applePayAuthorizationSession: DefaultApplePayAuthorizationSession(), + applePayCardTokenizationRequestMapper: requestMapper, + applePayErrorMapper: PODefaultPassKitPaymentErrorMapper(logger: logger) + ) + return service + } + + private static func createInvoicesService( + httpConnector: HttpConnector, threeDSService: ThreeDSService, logger: POLogger + ) -> POInvoicesService { + let repository = HttpInvoicesRepository(connector: httpConnector) + return DefaultInvoicesService(repository: repository, threeDSService: threeDSService, logger: logger) + } - private lazy var httpConnector: HttpConnector = { - createConnector(includeLoggerRemoteDestination: true) - }() + private static func createCustomerTokensService( + httpConnector: HttpConnector, threeDSService: ThreeDSService, logger: POLogger + ) -> POCustomerTokensService { + let repository = HttpCustomerTokensRepository(connector: httpConnector) + return DefaultCustomerTokensService(repository: repository, threeDSService: threeDSService, logger: logger) + } - private lazy var remoteLoggerDestination: LoggerDestination = { - let configuration: () -> TelemetryServiceConfiguration = { [unowned self] in + private func createAlternativePaymentsService() -> POAlternativePaymentsService { + let serviceConfiguration = { @Sendable [unowned self] () -> AlternativePaymentsServiceConfiguration in let configuration = self.configuration - return TelemetryServiceConfiguration( - isTelemetryEnabled: configuration.isTelemetryEnabled, - applicationVersion: configuration.application?.version, - applicationName: configuration.application?.name - ) + return .init(projectId: configuration.projectId, baseUrl: configuration.environment.checkoutBaseUrl) } - // Telemetry service uses repository with "special" connector. Its logs - // are not submitted to backend to avoid recursion. - let repository = DefaultTelemetryRepository( - connector: createConnector(includeLoggerRemoteDestination: false) + let webSession = DefaultWebAuthenticationSession() + return DefaultAlternativePaymentsService( + configuration: serviceConfiguration, webSession: webSession, logger: logger ) - return DefaultTelemetryService( - configuration: configuration, repository: repository, deviceMetadataProvider: deviceMetadataProvider - ) - }() + } - private lazy var threeDSService: ThreeDSService = { + private static func create3DSService() -> DefaultThreeDSService { let decoder = JSONDecoder() decoder.keyDecodingStrategy = .useDefaultKeys let encoder = JSONEncoder() encoder.dataEncodingStrategy = .base64 encoder.keyEncodingStrategy = .useDefaultKeys - return DefaultThreeDSService(decoder: decoder, encoder: encoder) - }() - - // MARK: - Private Methods - - private init(configuration: ProcessOutConfiguration) { - self.__configuration = .init(wrappedValue: configuration) + let webSession = DefaultWebAuthenticationSession() + return DefaultThreeDSService(decoder: decoder, encoder: encoder, webSession: webSession) } - private func createConnector(includeLoggerRemoteDestination: Bool) -> HttpConnector { - let connectorConfiguration = { [unowned self] in + private func createConnector( + deviceMetadataProvider: DeviceMetadataProvider, remoteLoggerDestination: LoggerDestination? = nil + ) -> HttpConnector { + let connectorConfiguration = { @Sendable [unowned self] in let configuration = self.configuration return HttpConnectorRequestMapperConfiguration( baseUrl: configuration.environment.apiBaseUrl, @@ -162,8 +164,7 @@ public final class ProcessOut { ) } let logger = createLogger( - for: Constants.connectorLoggerCategory, - includeRemoteDestination: includeLoggerRemoteDestination + for: Constants.connectorLoggerCategory, additionalDestinations: remoteLoggerDestination ) let connector = ProcessOutHttpConnectorBuilder() .with(configuration: connectorConfiguration) @@ -173,18 +174,47 @@ public final class ProcessOut { return connector } - private func createLogger(for category: String, includeRemoteDestination: Bool = true) -> POLogger { + private func createLogger( + for category: String, additionalDestinations: LoggerDestination?... + ) -> POLogger { var destinations: [LoggerDestination] = [ SystemLoggerDestination(subsystem: Constants.bundleIdentifier) ] - if includeRemoteDestination { - destinations.append(remoteLoggerDestination) - } - let minimumLevel: () -> LogLevel = { [unowned self] in + destinations.append( + contentsOf: additionalDestinations.compactMap { $0 } + ) + let minimumLevel = { @Sendable [unowned self] () -> LogLevel in configuration.isDebug ? .debug : .info } return POLogger(destinations: destinations, category: category, minimumLevel: minimumLevel) } + + private func createRemoteLoggerDestination( + deviceMetadataProvider: DeviceMetadataProvider + ) -> DefaultTelemetryService { + let configuration = { @Sendable [unowned self] () -> TelemetryServiceConfiguration in + let configuration = self.configuration + return TelemetryServiceConfiguration( + isTelemetryEnabled: configuration.isTelemetryEnabled, + applicationVersion: configuration.application?.version, + applicationName: configuration.application?.name + ) + } + // Telemetry service uses repository with "special" connector. Its logs + // are not submitted to backend to avoid recursion. + let repository = DefaultTelemetryRepository( + connector: createConnector(deviceMetadataProvider: deviceMetadataProvider) + ) + return DefaultTelemetryService( + configuration: configuration, repository: repository, deviceMetadataProvider: deviceMetadataProvider + ) + } + + @MainActor + private static func createDeviceMetadataProvider() -> DeviceMetadataProvider { + let keychain = Keychain(service: Constants.bundleIdentifier) + return DefaultDeviceMetadataProvider(screen: .main, device: .current, bundle: .main, keychain: keychain) + } } // MARK: - Singleton @@ -193,44 +223,41 @@ extension ProcessOut { /// Returns boolean value indicating whether SDK is configured and operational. public static var isConfigured: Bool { - _shared != nil + _shared.wrappedValue != nil } /// Shared instance. public static var shared: ProcessOut { precondition(isConfigured, "ProcessOut must be configured before the shared instance is accessed.") - return _shared + return _shared.wrappedValue! } /// Configures ``ProcessOut/shared`` instance. /// - Parameters: + /// - configuration: configuration. /// - force: When set to `false` (the default) only the first invocation takes effect, all /// subsequent calls to this method are ignored. Pass `true` to allow existing shared instance /// reconfiguration (if any). + @MainActor public static func configure(configuration: ProcessOutConfiguration, force: Bool = false) { - assert(Thread.isMainThread, "Method must be called only from main thread") if isConfigured { if force { - shared.$_configuration.withLock { $0 = configuration } + shared._configuration.withLock { $0 = configuration } shared.logger.debug("Did change ProcessOut configuration") } else { shared.logger.debug("ProcessOut can be configured only once, ignored") } } else { - Self.prewarm() - _shared = ProcessOut(configuration: configuration) + _shared.withLock { instance in + instance = ProcessOut(configuration: configuration) + } shared.logger.debug("Did complete ProcessOut configuration") } } // MARK: - Private Properties - private static var _shared: ProcessOut! // swiftlint:disable:this implicitly_unwrapped_optional - - // MARK: - Private Methods - - private static func prewarm() { - FontFamily.registerAllCustomFonts() - PODefaultPhoneNumberMetadataProvider.shared.prewarm() - } + private static let _shared = POUnfairlyLocked(wrappedValue: nil) } + +// swiftlint:enable implicitly_unwrapped_optional force_unwrapping diff --git a/Sources/ProcessOut/Sources/Api/Utils/Test3DS/POTest3DSService.swift b/Sources/ProcessOut/Sources/Api/Utils/Test3DS/POTest3DSService.swift deleted file mode 100644 index 75cd81cf5..000000000 --- a/Sources/ProcessOut/Sources/Api/Utils/Test3DS/POTest3DSService.swift +++ /dev/null @@ -1,71 +0,0 @@ -// -// POTest3DSService.swift -// ProcessOut -// -// Created by Andrii Vysotskyi on 19.04.2023. -// - -import UIKit - -/// Service that emulates the normal 3DS authentication flow but does not actually make any calls to a real Access -/// Control Server (ACS). Should be used only for testing purposes in sandbox environment. -@available(*, deprecated, message: "Use ProcessOutUI.POTest3DSService instead.") -public final class POTest3DSService: PO3DSService { - - /// Creates service instance. - @_disfavoredOverload - public init(returnUrl: URL) { - self.returnUrl = returnUrl - } - - /// View controller to use for presentations. - public unowned var viewController: UIViewController! // swiftlint:disable:this implicitly_unwrapped_optional - - // MARK: - PO3DSService - - public func authenticationRequest( - configuration: PO3DS2Configuration, - completion: @escaping (Result) -> Void - ) { - let request = PO3DS2AuthenticationRequest( - deviceData: "", - sdkAppId: "", - sdkEphemeralPublicKey: "{}", - sdkReferenceNumber: "", - sdkTransactionId: "" - ) - completion(.success(request)) - } - - public func handle(challenge: PO3DS2Challenge, completion: @escaping (Result) -> Void) { - let alertController = UIAlertController( - title: String(resource: .Test3DS.title), message: "", preferredStyle: .alert - ) - let acceptAction = UIAlertAction(title: String(resource: .Test3DS.accept), style: .default) { _ in - completion(.success(true)) - } - alertController.addAction(acceptAction) - let rejectAction = UIAlertAction(title: String(resource: .Test3DS.reject), style: .default) { _ in - completion(.success(false)) - } - alertController.addAction(rejectAction) - viewController.present(alertController, animated: true) - } - - public func handle(redirect: PO3DSRedirect, completion: @escaping (Result) -> Void) { - let viewController = PO3DSRedirectViewControllerBuilder() - .with(redirect: redirect) - .with(returnUrl: returnUrl) - .with { [weak self] result in - self?.viewController.presentedViewController?.dismiss(animated: true) { - completion(result) - } - } - .build() - self.viewController.present(viewController, animated: true) - } - - // MARK: - Private Properties - - private let returnUrl: URL -} diff --git a/Sources/ProcessOut/Sources/Api/Utils/Test3DS/StringResource+Test3DS.swift b/Sources/ProcessOut/Sources/Api/Utils/Test3DS/StringResource+Test3DS.swift deleted file mode 100644 index b735fa5fb..000000000 --- a/Sources/ProcessOut/Sources/Api/Utils/Test3DS/StringResource+Test3DS.swift +++ /dev/null @@ -1,21 +0,0 @@ -// -// StringResource+Test3DS.swift -// ProcessOut -// -// Created by Andrii Vysotskyi on 23.01.2024. -// - -extension POStringResource { - - enum Test3DS { - - /// 3DS challenge title. - static let title = POStringResource("test-3ds.challenge.title", comment: "") - - /// Accept button title. - static let accept = POStringResource("test-3ds.challenge.accept", comment: "") - - /// Reject button title. - static let reject = POStringResource("test-3ds.challenge.reject", comment: "") - } -} diff --git a/Sources/ProcessOut/Sources/Connectors/Http/HttpConnector.swift b/Sources/ProcessOut/Sources/Connectors/Http/HttpConnector.swift index d4c564d2b..05d989c0a 100644 --- a/Sources/ProcessOut/Sources/Connectors/Http/HttpConnector.swift +++ b/Sources/ProcessOut/Sources/Connectors/Http/HttpConnector.swift @@ -5,7 +5,7 @@ // Created by Andrii Vysotskyi on 10.10.2022. // -protocol HttpConnector: AnyObject { +protocol HttpConnector: AnyObject, Sendable { typealias Failure = HttpConnectorFailure diff --git a/Sources/ProcessOut/Sources/Connectors/Http/Implementations/UrlSession/RequestMapper/DefaultHttpConnectorRequestMapper.swift b/Sources/ProcessOut/Sources/Connectors/Http/Implementations/UrlSession/RequestMapper/DefaultHttpConnectorRequestMapper.swift index 865362a37..ec5781955 100644 --- a/Sources/ProcessOut/Sources/Connectors/Http/Implementations/UrlSession/RequestMapper/DefaultHttpConnectorRequestMapper.swift +++ b/Sources/ProcessOut/Sources/Connectors/Http/Implementations/UrlSession/RequestMapper/DefaultHttpConnectorRequestMapper.swift @@ -10,7 +10,7 @@ import Foundation final class DefaultHttpConnectorRequestMapper: HttpConnectorRequestMapper { init( - configuration: @escaping () -> HttpConnectorRequestMapperConfiguration, + configuration: @escaping @Sendable () -> HttpConnectorRequestMapperConfiguration, encoder: JSONEncoder, deviceMetadataProvider: DeviceMetadataProvider, logger: POLogger @@ -51,7 +51,7 @@ final class DefaultHttpConnectorRequestMapper: HttpConnectorRequestMapper { // MARK: - Private Properties - private let configuration: () -> HttpConnectorRequestMapperConfiguration + private let configuration: @Sendable () -> HttpConnectorRequestMapperConfiguration private let encoder: JSONEncoder private let deviceMetadataProvider: DeviceMetadataProvider private let logger: POLogger diff --git a/Sources/ProcessOut/Sources/Connectors/Http/Implementations/UrlSession/RequestMapper/HttpConnectorRequestMapper.swift b/Sources/ProcessOut/Sources/Connectors/Http/Implementations/UrlSession/RequestMapper/HttpConnectorRequestMapper.swift index b2970efac..772fb41b0 100644 --- a/Sources/ProcessOut/Sources/Connectors/Http/Implementations/UrlSession/RequestMapper/HttpConnectorRequestMapper.swift +++ b/Sources/ProcessOut/Sources/Connectors/Http/Implementations/UrlSession/RequestMapper/HttpConnectorRequestMapper.swift @@ -7,7 +7,7 @@ import Foundation -protocol HttpConnectorRequestMapper { +protocol HttpConnectorRequestMapper: Sendable { /// Transforms given `HttpConnectorRequest` to `URLRequest`. func urlRequest(from request: HttpConnectorRequest) async throws -> URLRequest diff --git a/Sources/ProcessOut/Sources/Connectors/Http/Implementations/UrlSession/UrlSessionHttpConnector.swift b/Sources/ProcessOut/Sources/Connectors/Http/Implementations/UrlSession/UrlSessionHttpConnector.swift index 6bb2b7901..f6c1a3845 100644 --- a/Sources/ProcessOut/Sources/Connectors/Http/Implementations/UrlSession/UrlSessionHttpConnector.swift +++ b/Sources/ProcessOut/Sources/Connectors/Http/Implementations/UrlSession/UrlSessionHttpConnector.swift @@ -96,7 +96,7 @@ final class UrlSessionHttpConnector: HttpConnector { } } -private struct Response: Decodable { +private struct Response: Decodable, Sendable { /// Indicates whether request was processed successfully. let success: Bool diff --git a/Sources/ProcessOut/Sources/Connectors/Http/Models/HttpConnectorFailure.swift b/Sources/ProcessOut/Sources/Connectors/Http/Models/HttpConnectorFailure.swift index 83f1e2606..c332bfec4 100644 --- a/Sources/ProcessOut/Sources/Connectors/Http/Models/HttpConnectorFailure.swift +++ b/Sources/ProcessOut/Sources/Connectors/Http/Models/HttpConnectorFailure.swift @@ -5,9 +5,9 @@ // Created by Andrii Vysotskyi on 10.10.2022. // -enum HttpConnectorFailure: Error { +enum HttpConnectorFailure: Error, Sendable { - struct InvalidField: Decodable { + struct InvalidField: Decodable, Sendable { /// Field name. let name: String @@ -16,7 +16,7 @@ enum HttpConnectorFailure: Error { let message: String } - struct Server: Decodable { + struct Server: Decodable, Sendable { /// Error type. let errorType: String diff --git a/Sources/ProcessOut/Sources/Connectors/Http/Models/HttpConnectorRequest.swift b/Sources/ProcessOut/Sources/Connectors/Http/Models/HttpConnectorRequest.swift index 2aee97bac..582f6d73c 100644 --- a/Sources/ProcessOut/Sources/Connectors/Http/Models/HttpConnectorRequest.swift +++ b/Sources/ProcessOut/Sources/Connectors/Http/Models/HttpConnectorRequest.swift @@ -7,12 +7,14 @@ import Foundation -struct HttpConnectorRequest { +struct HttpConnectorRequest: Sendable { enum Method: String { case get, put, post } + typealias Body = Sendable & Encodable + /// Request identifier. let id: String @@ -23,10 +25,10 @@ struct HttpConnectorRequest { let path: String /// Query items. - let query: [String: CustomStringConvertible] + let query: [String: String] /// Parameters. - let body: Encodable? + let body: Body? /// Custom headers. let headers: [String: String] @@ -54,7 +56,7 @@ extension HttpConnectorRequest { id: UUID().uuidString, method: .get, path: path, - query: query, + query: query.mapValues(\.description), body: nil, headers: headers, includesDeviceMetadata: false, @@ -64,7 +66,7 @@ extension HttpConnectorRequest { static func post( path: String, - body: Encodable? = nil, + body: Body? = nil, headers: [String: String] = [:], includesDeviceMetadata: Bool = false, requiresPrivateKey: Bool = false @@ -83,7 +85,7 @@ extension HttpConnectorRequest { static func put( path: String, - body: Encodable? = nil, + body: Body? = nil, headers: [String: String] = [:], includesDeviceMetadata: Bool = false, requiresPrivateKey: Bool = false diff --git a/Sources/ProcessOut/Sources/Core/Cancellable/GroupCancellable.swift b/Sources/ProcessOut/Sources/Core/Cancellable/GroupCancellable.swift deleted file mode 100644 index 71114e843..000000000 --- a/Sources/ProcessOut/Sources/Core/Cancellable/GroupCancellable.swift +++ /dev/null @@ -1,47 +0,0 @@ -// -// GroupCancellable.swift -// ProcessOut -// -// Created by Andrii Vysotskyi on 21.12.2022. -// - -import Foundation - -final class GroupCancellable: POCancellable, @unchecked Sendable { - - init() { - isCancelled = false - lock = NSLock() - cancellables = [] - } - - func add(_ cancellable: POCancellable) { - lock.lock() - if isCancelled { - lock.unlock() - cancellable.cancel() - } else { - cancellables.append(cancellable) - lock.unlock() - } - } - - func cancel() { - lock.lock() - guard !isCancelled else { - lock.unlock() - return - } - isCancelled = true - let cancellables = self.cancellables - self.cancellables = [] - lock.unlock() - cancellables.forEach { $0.cancel() } - } - - // MARK: - Private Properties - - private let lock: NSLock - private var isCancelled: Bool - private var cancellables: [POCancellable] -} diff --git a/Sources/ProcessOut/Sources/Core/Cancellable/POCancellable.swift b/Sources/ProcessOut/Sources/Core/Cancellable/POCancellable.swift index b5debb144..6825f0fa2 100644 --- a/Sources/ProcessOut/Sources/Core/Cancellable/POCancellable.swift +++ b/Sources/ProcessOut/Sources/Core/Cancellable/POCancellable.swift @@ -5,11 +5,8 @@ // Created by Andrii Vysotskyi on 21.12.2022. // -@available(*, deprecated, renamed: "POCancellable") -public typealias POCancellableType = POCancellable - /// A protocol indicating that an activity or action supports cancellation. -public protocol POCancellable { +public protocol POCancellable: Sendable { /// Cancel the activity. func cancel() diff --git a/Sources/ProcessOut/Sources/Core/CodingUtils/POFallbackDecodable/POFallbackDecodable.swift b/Sources/ProcessOut/Sources/Core/CodingUtils/POFallbackDecodable/POFallbackDecodable.swift deleted file mode 100644 index cb0a5c66d..000000000 --- a/Sources/ProcessOut/Sources/Core/CodingUtils/POFallbackDecodable/POFallbackDecodable.swift +++ /dev/null @@ -1,30 +0,0 @@ -// -// POFallbackDecodable.swift -// ProcessOut -// -// Created by Andrii Vysotskyi on 24.11.2023. -// - -import Foundation - -/// Allows decoding to fallback to default when value is not present. -@propertyWrapper -public struct POFallbackDecodable: Decodable where Provider.Value: Decodable { - - public var wrappedValue: Provider.Value - - public init(wrappedValue: Provider.Value) { - self.wrappedValue = wrappedValue - } -} - -extension KeyedDecodingContainer { - - public func decode

( - _ type: POFallbackDecodable

.Type, forKey key: KeyedDecodingContainer.Key - ) throws -> POFallbackDecodable

{ - POFallbackDecodable(wrappedValue: try decodeIfPresent(P.Value.self, forKey: key) ?? P.defaultValue) - } -} - -extension POFallbackDecodable: Hashable, Equatable where Provider.Value: Hashable { } diff --git a/Sources/ProcessOut/Sources/Core/CodingUtils/POFallbackDecodable/POFallbackValueProvider.swift b/Sources/ProcessOut/Sources/Core/CodingUtils/POFallbackDecodable/POFallbackValueProvider.swift deleted file mode 100644 index e9cff7683..000000000 --- a/Sources/ProcessOut/Sources/Core/CodingUtils/POFallbackDecodable/POFallbackValueProvider.swift +++ /dev/null @@ -1,23 +0,0 @@ -// -// POFallbackValueProvider.swift -// ProcessOut -// -// Created by Andrii Vysotskyi on 24.11.2023. -// - -import Foundation - -/// Contract for providing a default value of a Type. -public protocol POFallbackValueProvider { - - associatedtype Value - - /// Default value. - static var defaultValue: Value { get } -} - -/// Provides empty string as a fallback. -public struct POEmptyStringProvider: POFallbackValueProvider { - - public static let defaultValue = "" -} diff --git a/Sources/ProcessOut/Sources/Core/CodingUtils/POImmutableExcludedCodable.swift b/Sources/ProcessOut/Sources/Core/CodingUtils/POImmutableExcludedCodable.swift deleted file mode 100644 index a8ce028e2..000000000 --- a/Sources/ProcessOut/Sources/Core/CodingUtils/POImmutableExcludedCodable.swift +++ /dev/null @@ -1,32 +0,0 @@ -// -// POImmutableExcludedCodable.swift -// ProcessOut -// -// Created by Andrii Vysotskyi on 25.10.2022. -// - -import Foundation - -/// Property wrapper that allows to exclude property from being encoded without forcing owning parent to define -/// custom `CodingKeys`. -/// -/// - NOTE: Wrapped value is immutable. -@propertyWrapper -public struct POImmutableExcludedCodable: Encodable { - - public let wrappedValue: Value - - /// Creates property wrapper instance. - public init(value: Value) { - self.wrappedValue = value - } - - public func encode(to encoder: Encoder) throws { } -} - -extension KeyedEncodingContainer { - - public mutating func encode( - _ value: POImmutableExcludedCodable, forKey key: KeyedEncodingContainer.Key - ) throws { /* Ignored */ } -} diff --git a/Sources/ProcessOut/Sources/Core/CodingUtils/POStringCodableColor.swift b/Sources/ProcessOut/Sources/Core/CodingUtils/POStringCodableColor.swift index ca5e91a3a..b6ee712c1 100644 --- a/Sources/ProcessOut/Sources/Core/CodingUtils/POStringCodableColor.swift +++ b/Sources/ProcessOut/Sources/Core/CodingUtils/POStringCodableColor.swift @@ -10,7 +10,7 @@ import UIKit /// Property wrapper that allows to decode UIColor from string representations. @propertyWrapper -public struct POStringCodableColor: Decodable { +public struct POStringCodableColor: Decodable, Sendable { public var wrappedValue: UIColor diff --git a/Sources/ProcessOut/Sources/Core/CodingUtils/POImmutableStringCodableDecimal.swift b/Sources/ProcessOut/Sources/Core/CodingUtils/POStringCodableDecimal.swift similarity index 80% rename from Sources/ProcessOut/Sources/Core/CodingUtils/POImmutableStringCodableDecimal.swift rename to Sources/ProcessOut/Sources/Core/CodingUtils/POStringCodableDecimal.swift index 70ad98ea8..ded89c1cc 100644 --- a/Sources/ProcessOut/Sources/Core/CodingUtils/POImmutableStringCodableDecimal.swift +++ b/Sources/ProcessOut/Sources/Core/CodingUtils/POStringCodableDecimal.swift @@ -1,5 +1,5 @@ // -// POImmutableStringCodableDecimal.swift +// POStringCodableDecimal.swift // ProcessOut // // Created by Andrii Vysotskyi on 30.11.2022. @@ -12,9 +12,9 @@ import Foundation /// Property wrapper that allows to encode and decode `Decimal` to/from string representation. Value is coded /// in en_US locale. @propertyWrapper -public struct POImmutableStringCodableDecimal: Codable { +public struct POStringCodableDecimal: Codable, Sendable { - public let wrappedValue: Decimal + public var wrappedValue: Decimal public init(from decoder: Decoder) throws { let container = try decoder.singleValueContainer() @@ -50,7 +50,7 @@ public struct POImmutableStringCodableDecimal: Codable { extension KeyedEncodingContainer { public mutating func encode( - _ value: POImmutableStringCodableDecimal, forKey key: KeyedEncodingContainer.Key + _ value: POStringCodableDecimal, forKey key: KeyedEncodingContainer.Key ) throws { try value.encode(to: superEncoder(forKey: key)) } @@ -59,8 +59,8 @@ extension KeyedEncodingContainer { extension KeyedDecodingContainer { public func decode( - _ type: POImmutableStringCodableDecimal.Type, forKey key: KeyedDecodingContainer.Key - ) throws -> POImmutableStringCodableDecimal { + _ type: POStringCodableDecimal.Type, forKey key: KeyedDecodingContainer.Key + ) throws -> POStringCodableDecimal { try type.init(from: try superDecoder(forKey: key)) } } diff --git a/Sources/ProcessOut/Sources/Core/CodingUtils/POImmutableStringCodableOptionalDecimal.swift b/Sources/ProcessOut/Sources/Core/CodingUtils/POStringCodableOptionalDecimal.swift similarity index 80% rename from Sources/ProcessOut/Sources/Core/CodingUtils/POImmutableStringCodableOptionalDecimal.swift rename to Sources/ProcessOut/Sources/Core/CodingUtils/POStringCodableOptionalDecimal.swift index d7364eff8..fcd840376 100644 --- a/Sources/ProcessOut/Sources/Core/CodingUtils/POImmutableStringCodableOptionalDecimal.swift +++ b/Sources/ProcessOut/Sources/Core/CodingUtils/POStringCodableOptionalDecimal.swift @@ -1,5 +1,5 @@ // -// POImmutableStringCodableOptionalDecimal.swift +// POStringCodableOptionalDecimal.swift // ProcessOut // // Created by Andrii Vysotskyi on 18.10.2022. @@ -12,9 +12,9 @@ import Foundation /// Property wrapper that allows to encode and decode optional `Decimal` to/from string representation. Value is coded /// in en_US locale. @propertyWrapper -public struct POImmutableStringCodableOptionalDecimal: Codable { +public struct POStringCodableOptionalDecimal: Codable, Sendable { - public let wrappedValue: Decimal? + public var wrappedValue: Decimal? public init(from decoder: Decoder) throws { let container = try decoder.singleValueContainer() @@ -54,7 +54,7 @@ public struct POImmutableStringCodableOptionalDecimal: Codable { extension KeyedEncodingContainer { public mutating func encode( - _ value: POImmutableStringCodableOptionalDecimal, forKey key: KeyedEncodingContainer.Key + _ value: POStringCodableOptionalDecimal, forKey key: KeyedEncodingContainer.Key ) throws { try value.encode(to: superEncoder(forKey: key)) } @@ -63,8 +63,8 @@ extension KeyedEncodingContainer { extension KeyedDecodingContainer { public func decode( - _ type: POImmutableStringCodableOptionalDecimal.Type, forKey key: KeyedDecodingContainer.Key - ) throws -> POImmutableStringCodableOptionalDecimal { + _ type: POStringCodableOptionalDecimal.Type, forKey key: KeyedDecodingContainer.Key + ) throws -> POStringCodableOptionalDecimal { try type.init(from: try superDecoder(forKey: key)) } } diff --git a/Sources/ProcessOut/Sources/Core/CodingUtils/VoidCodable.swift b/Sources/ProcessOut/Sources/Core/CodingUtils/VoidCodable.swift index 2eeda7d57..a7c4e89e4 100644 --- a/Sources/ProcessOut/Sources/Core/CodingUtils/VoidCodable.swift +++ b/Sources/ProcessOut/Sources/Core/CodingUtils/VoidCodable.swift @@ -5,4 +5,4 @@ // Created by Andrii Vysotskyi on 30.11.2022. // -struct VoidCodable: Codable { } +struct VoidCodable: Codable, Sendable { } diff --git a/Sources/ProcessOut/Sources/Core/DeviceMetadata/DefaultDeviceMetadataProvider.swift b/Sources/ProcessOut/Sources/Core/DeviceMetadata/DefaultDeviceMetadataProvider.swift index a7232cf28..bc58aacd1 100644 --- a/Sources/ProcessOut/Sources/Core/DeviceMetadata/DefaultDeviceMetadataProvider.swift +++ b/Sources/ProcessOut/Sources/Core/DeviceMetadata/DefaultDeviceMetadataProvider.swift @@ -21,10 +21,10 @@ actor DefaultDeviceMetadataProvider: DeviceMetadataProvider { @MainActor var deviceMetadata: DeviceMetadata { get async { let metadata = DeviceMetadata( - id: .init(value: await deviceId), - installationId: .init(value: device.identifierForVendor?.uuidString), - systemVersion: .init(value: device.systemVersion), - model: .init(value: await machineName), + id: await deviceId, + installationId: device.identifierForVendor?.uuidString, + systemVersion: device.systemVersion, + model: await machineName, appLanguage: bundle.preferredLocalizations.first!, // swiftlint:disable:this force_unwrapping appScreenWidth: Int(screen.nativeBounds.width), // Specified in pixels appScreenHeight: Int(screen.nativeBounds.height), @@ -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 diff --git a/Sources/ProcessOut/Sources/Core/DeviceMetadata/DeviceMetadata.swift b/Sources/ProcessOut/Sources/Core/DeviceMetadata/DeviceMetadata.swift index 80da5cd1b..a8baed17a 100644 --- a/Sources/ProcessOut/Sources/Core/DeviceMetadata/DeviceMetadata.swift +++ b/Sources/ProcessOut/Sources/Core/DeviceMetadata/DeviceMetadata.swift @@ -7,23 +7,19 @@ import Foundation -struct DeviceMetadata: Encodable { +struct DeviceMetadata: Encodable, Sendable { // sourcery: AutoCodingKeys /// Current device identifier. - @POImmutableExcludedCodable - var id: String? + let id: String? // sourcery:coding: skip /// Installation identifier. Value changes if host application is reinstalled. - @POImmutableExcludedCodable - var installationId: String? + let installationId: String? // sourcery:coding: skip /// Device system version. - @POImmutableExcludedCodable - var systemVersion: String + let systemVersion: String // sourcery:coding: skip /// Device model. - @POImmutableExcludedCodable - var model: String? + let model: String? // sourcery:coding: skip /// Default app language. let appLanguage: String diff --git a/Sources/ProcessOut/Sources/Core/DeviceMetadata/DeviceMetadataProvider.swift b/Sources/ProcessOut/Sources/Core/DeviceMetadata/DeviceMetadataProvider.swift index 00ef672c6..d61542261 100644 --- a/Sources/ProcessOut/Sources/Core/DeviceMetadata/DeviceMetadataProvider.swift +++ b/Sources/ProcessOut/Sources/Core/DeviceMetadata/DeviceMetadataProvider.swift @@ -5,7 +5,7 @@ // Created by Simeon Kostadinov on 01/11/2022. // -protocol DeviceMetadataProvider { +protocol DeviceMetadataProvider: Sendable { /// Returns device metadata. var deviceMetadata: DeviceMetadata { get async } 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 97ea50358..000000000 --- a/Sources/ProcessOut/Sources/Core/EventEmitter/POEventEmitterEvent.swift +++ /dev/null @@ -1,19 +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/Core/Utils/Task+Sleep.swift b/Sources/ProcessOut/Sources/Core/Extensions/Task+Sleep.swift similarity index 100% rename from Sources/ProcessOut/Sources/Core/Utils/Task+Sleep.swift rename to Sources/ProcessOut/Sources/Core/Extensions/Task+Sleep.swift diff --git a/Sources/ProcessOut/Sources/Core/Utils/UIImage+Dynamic.swift b/Sources/ProcessOut/Sources/Core/Extensions/UIImage+Dynamic.swift similarity index 90% rename from Sources/ProcessOut/Sources/Core/Utils/UIImage+Dynamic.swift rename to Sources/ProcessOut/Sources/Core/Extensions/UIImage+Dynamic.swift index 1bcc88a67..2e68d34da 100644 --- a/Sources/ProcessOut/Sources/Core/Utils/UIImage+Dynamic.swift +++ b/Sources/ProcessOut/Sources/Core/Extensions/UIImage+Dynamic.swift @@ -9,8 +9,9 @@ import UIKit extension UIImage { + @MainActor static func dynamic(lightImage: UIImage?, darkImage: UIImage?) -> UIImage? { - // When image with scale greater than 3 is registed asset created explicitly produced image + // When image with scale greater than 3 is registered asset created explicitly produced image // is malformed and doesn't contain images for light nor dark styles. guard let image = lightImage ?? darkImage else { return nil diff --git a/Sources/ProcessOut/Sources/Core/Extensions/URLSessionTask+Cancellable.swift b/Sources/ProcessOut/Sources/Core/Extensions/URLSessionTask+Cancellable.swift deleted file mode 100644 index 964a45423..000000000 --- a/Sources/ProcessOut/Sources/Core/Extensions/URLSessionTask+Cancellable.swift +++ /dev/null @@ -1,10 +0,0 @@ -// -// URLSessionTask+Cancellable.swift -// ProcessOut -// -// Created by Andrii Vysotskyi on 03.04.2023. -// - -import Foundation - -extension URLSessionTask: POCancellable { } diff --git a/Sources/ProcessOut/Sources/Core/Formatters/PhoneNumber/MetadataProvider/POPhoneNumberMetadataProvider.swift b/Sources/ProcessOut/Sources/Core/Formatters/PhoneNumber/MetadataProvider/POPhoneNumberMetadataProvider.swift deleted file mode 100644 index 761f0f2a8..000000000 --- a/Sources/ProcessOut/Sources/Core/Formatters/PhoneNumber/MetadataProvider/POPhoneNumberMetadataProvider.swift +++ /dev/null @@ -1,12 +0,0 @@ -// -// POPhoneNumberMetadataProvider.swift -// ProcessOut -// -// Created by Andrii Vysotskyi on 23.03.2023. -// - -@_spi(PO) public protocol POPhoneNumberMetadataProvider { - - /// Returns metadata for given country code if any. - func metadata(for countryCode: String) -> POPhoneNumberMetadata? -} diff --git a/Sources/ProcessOut/Sources/Core/Formatters/PhoneNumber/POPhoneNumberFormat.swift b/Sources/ProcessOut/Sources/Core/Formatters/PhoneNumber/POPhoneNumberFormat.swift deleted file mode 100644 index bcd41d63f..000000000 --- a/Sources/ProcessOut/Sources/Core/Formatters/PhoneNumber/POPhoneNumberFormat.swift +++ /dev/null @@ -1,18 +0,0 @@ -// -// POPhoneNumberFormat.swift -// ProcessOut -// -// Created by Andrii Vysotskyi on 16.03.2023. -// - -@_spi(PO) public struct POPhoneNumberFormat: Decodable { - - /// Formatting patern. - public let pattern: String - - /// Leading digits pattern. - public let leading: [String] - - /// Format to use for number. - public let format: String -} diff --git a/Sources/ProcessOut/Sources/Core/Formatters/PhoneNumber/POPhoneNumberMetadata.swift b/Sources/ProcessOut/Sources/Core/Formatters/PhoneNumber/POPhoneNumberMetadata.swift deleted file mode 100644 index be1e30fae..000000000 --- a/Sources/ProcessOut/Sources/Core/Formatters/PhoneNumber/POPhoneNumberMetadata.swift +++ /dev/null @@ -1,15 +0,0 @@ -// -// POPhoneNumberMetadata.swift -// ProcessOut -// -// Created by Andrii Vysotskyi on 16.03.2023. -// - -@_spi(PO) public struct POPhoneNumberMetadata: Decodable { - - /// Country code. - public let countryCode: String - - /// Available formats. - public let formats: [POPhoneNumberFormat] -} diff --git a/Sources/ProcessOut/Sources/Core/Formatters/UrlRequest/UrlRequestFormatter.swift b/Sources/ProcessOut/Sources/Core/Formatters/UrlRequest/UrlRequestFormatter.swift index e85c4ad89..244956dd7 100644 --- a/Sources/ProcessOut/Sources/Core/Formatters/UrlRequest/UrlRequestFormatter.swift +++ b/Sources/ProcessOut/Sources/Core/Formatters/UrlRequest/UrlRequestFormatter.swift @@ -7,7 +7,7 @@ import Foundation -final class UrlRequestFormatter { +final class UrlRequestFormatter: Sendable { init(prettyPrintedBody: Bool = true) { self.prettyPrintedBody = prettyPrintedBody diff --git a/Sources/ProcessOut/Sources/Core/Formatters/UrlResponse/UrlResponseFormatter.swift b/Sources/ProcessOut/Sources/Core/Formatters/UrlResponse/UrlResponseFormatter.swift index 7233f0763..4814da1a8 100644 --- a/Sources/ProcessOut/Sources/Core/Formatters/UrlResponse/UrlResponseFormatter.swift +++ b/Sources/ProcessOut/Sources/Core/Formatters/UrlResponse/UrlResponseFormatter.swift @@ -7,7 +7,7 @@ import Foundation -final class UrlResponseFormatter { +final class UrlResponseFormatter: Sendable { init(includesHeaders: Bool, prettyPrintedBody: Bool = true) { self.includesHeaders = includesHeaders diff --git a/Sources/ProcessOut/Sources/Core/Keychain/Keychain.swift b/Sources/ProcessOut/Sources/Core/Keychain/Keychain.swift index e83a65b6b..5910572b3 100644 --- a/Sources/ProcessOut/Sources/Core/Keychain/Keychain.swift +++ b/Sources/ProcessOut/Sources/Core/Keychain/Keychain.swift @@ -8,7 +8,7 @@ import Foundation import Security -final class Keychain { +final class Keychain: Sendable { init(service: String) { queryBuilder = KeychainQueryBuilder(service: service) diff --git a/Sources/ProcessOut/Sources/Core/Keychain/KeychainItemAccessibility.swift b/Sources/ProcessOut/Sources/Core/Keychain/KeychainItemAccessibility.swift index 6bffcd397..53df357c6 100644 --- a/Sources/ProcessOut/Sources/Core/Keychain/KeychainItemAccessibility.swift +++ b/Sources/ProcessOut/Sources/Core/Keychain/KeychainItemAccessibility.swift @@ -7,13 +7,13 @@ import Security -struct KeychainItemAccessibility: RawRepresentable { +struct KeychainItemAccessibility: RawRepresentable, Sendable { - let rawValue: CFString + let rawValue: String /// The data in the keychain item cannot be accessed after a restart until /// the device has been unlocked once by the user. static let accessibleAfterFirstUnlockThisDeviceOnly = KeychainItemAccessibility( - rawValue: kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly + rawValue: kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly as String ) } diff --git a/Sources/ProcessOut/Sources/Core/Keychain/KeychainItemClass.swift b/Sources/ProcessOut/Sources/Core/Keychain/KeychainItemClass.swift index 40566e251..ffd7b0b97 100644 --- a/Sources/ProcessOut/Sources/Core/Keychain/KeychainItemClass.swift +++ b/Sources/ProcessOut/Sources/Core/Keychain/KeychainItemClass.swift @@ -7,10 +7,10 @@ import Security -struct KeychainItemClass: RawRepresentable { +struct KeychainItemClass: RawRepresentable, Sendable { - let rawValue: CFString + let rawValue: String /// Generic password item. - static let genericPassword = KeychainItemClass(rawValue: kSecClassGenericPassword) + static let genericPassword = KeychainItemClass(rawValue: kSecClassGenericPassword as String) } diff --git a/Sources/ProcessOut/Sources/Core/Logger/Destinations/SystemLoggerDestination.swift b/Sources/ProcessOut/Sources/Core/Logger/Destinations/SystemLoggerDestination.swift index 53f7e878e..318239716 100644 --- a/Sources/ProcessOut/Sources/Core/Logger/Destinations/SystemLoggerDestination.swift +++ b/Sources/ProcessOut/Sources/Core/Logger/Destinations/SystemLoggerDestination.swift @@ -12,8 +12,7 @@ final class SystemLoggerDestination: LoggerDestination { init(subsystem: String) { self.subsystem = subsystem - lock = NSLock() - logs = [:] + logs = POUnfairlyLocked(wrappedValue: [:]) } func log(event: LogEvent) { @@ -30,8 +29,7 @@ final class SystemLoggerDestination: LoggerDestination { // MARK: - Private Properties private let subsystem: String - private let lock: NSLock - private var logs: [String: OSLog] + private let logs: POUnfairlyLocked<[String: OSLog]> // MARK: - Private Methods @@ -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 } diff --git a/Sources/ProcessOut/Sources/Core/Logger/LoggerDestination.swift b/Sources/ProcessOut/Sources/Core/Logger/LoggerDestination.swift index b98ca8fb6..b759d356c 100644 --- a/Sources/ProcessOut/Sources/Core/Logger/LoggerDestination.swift +++ b/Sources/ProcessOut/Sources/Core/Logger/LoggerDestination.swift @@ -5,7 +5,7 @@ // Created by Andrii Vysotskyi on 25.10.2022. // -protocol LoggerDestination { +protocol LoggerDestination: Sendable { /// Logs given event. func log(event: LogEvent) diff --git a/Sources/ProcessOut/Sources/Core/Logger/Models/LogEvent.swift b/Sources/ProcessOut/Sources/Core/Logger/Models/LogEvent.swift index 973d2ebaa..341c33077 100644 --- a/Sources/ProcessOut/Sources/Core/Logger/Models/LogEvent.swift +++ b/Sources/ProcessOut/Sources/Core/Logger/Models/LogEvent.swift @@ -7,7 +7,7 @@ import Foundation -struct LogEvent { +struct LogEvent: Sendable { /// Logging level. let level: LogLevel @@ -22,7 +22,7 @@ struct LogEvent { let timestamp: Date /// DSO handle. - let dso: UnsafeRawPointer? + nonisolated(unsafe) let dso: UnsafeRawPointer? /// File name. let file: String diff --git a/Sources/ProcessOut/Sources/Core/Logger/Models/POLogAttributeKey.swift b/Sources/ProcessOut/Sources/Core/Logger/Models/POLogAttributeKey.swift index 471eb97e8..507958084 100644 --- a/Sources/ProcessOut/Sources/Core/Logger/Models/POLogAttributeKey.swift +++ b/Sources/ProcessOut/Sources/Core/Logger/Models/POLogAttributeKey.swift @@ -8,7 +8,7 @@ import Foundation @_spi(PO) -public struct POLogAttributeKey: RawRepresentable, ExpressibleByStringLiteral, Hashable { +public struct POLogAttributeKey: RawRepresentable, ExpressibleByStringLiteral, Hashable, Sendable { public init(rawValue: String) { self.rawValue = rawValue diff --git a/Sources/ProcessOut/Sources/Core/Logger/POLogger.swift b/Sources/ProcessOut/Sources/Core/Logger/POLogger.swift index 7df74e90b..b6b337374 100644 --- a/Sources/ProcessOut/Sources/Core/Logger/POLogger.swift +++ b/Sources/ProcessOut/Sources/Core/Logger/POLogger.swift @@ -7,16 +7,16 @@ import Foundation -/// An object for writing interpolated string messages to the processout logging system. +/// An object for writing interpolated string messages to the ProcessOut logging system. @_spi(PO) -public struct POLogger { +public struct POLogger: Sendable { - init(destinations: [LoggerDestination] = [], category: String, minimumLevel: @escaping () -> LogLevel) { + init(destinations: [LoggerDestination] = [], category: String, minimumLevel: @escaping @Sendable () -> LogLevel) { self.destinations = destinations self.category = category self.minimumLevel = minimumLevel self.attributes = [:] - lock = NSLock() + lock = POUnfairlyLocked() } init(destinations: [LoggerDestination] = [], category: String) { @@ -83,8 +83,8 @@ public struct POLogger { // MARK: - Private Properties private let destinations: [LoggerDestination] - private let minimumLevel: () -> LogLevel - private let lock: NSLock + private let minimumLevel: @Sendable () -> LogLevel + private let lock: POUnfairlyLocked private var attributes: [POLogAttributeKey: String] // MARK: - Private Methods diff --git a/Sources/ProcessOut/Sources/Core/Markdown/MarkdownNodeFactory.swift b/Sources/ProcessOut/Sources/Core/Markdown/MarkdownNodeFactory.swift deleted file mode 100644 index 578880a0f..000000000 --- a/Sources/ProcessOut/Sources/Core/Markdown/MarkdownNodeFactory.swift +++ /dev/null @@ -1,52 +0,0 @@ -// -// MarkdownNodeFactory.swift -// ProcessOut -// -// Created by Andrii Vysotskyi on 13.06.2023. -// - -import cmark_gfm - -final class MarkdownNodeFactory { - - init(cmarkNode: MarkdownBaseNode.CmarkNode) { - self.cmarkNode = cmarkNode - } - - func create() -> MarkdownBaseNode { - let nodeType = UInt32(cmarkNode.pointee.type) - guard nodeType != CMARK_NODE_NONE.rawValue else { - preconditionFailure("Invalid node") - } - // HTML and images are intentionally not supported. - guard let nodeClass = Self.supportedNodes[nodeType] else { - return MarkdownUnknown(cmarkNode: cmarkNode) - } - return nodeClass.init(cmarkNode: cmarkNode) - } - - // MARK: - Private Properties - - private static let supportedNodes: [UInt32: MarkdownBaseNode.Type] = { - let supportedNodes = [ - MarkdownDocument.self, - MarkdownText.self, - MarkdownParagraph.self, - MarkdownList.self, - MarkdownListItem.self, - MarkdownStrong.self, - MarkdownEmphasis.self, - MarkdownBlockQuote.self, - MarkdownCodeBlock.self, - MarkdownCodeSpan.self, - MarkdownHeading.self, - MarkdownLinebreak.self, - MarkdownSoftbreak.self, - MarkdownThematicBreak.self, - MarkdownLink.self - ] - return Dictionary(grouping: supportedNodes) { $0.cmarkNodeType.rawValue }.compactMapValues(\.first) - }() - - private let cmarkNode: MarkdownBaseNode.CmarkNode -} diff --git a/Sources/ProcessOut/Sources/Core/Markdown/MarkdownParser.swift b/Sources/ProcessOut/Sources/Core/Markdown/MarkdownParser.swift deleted file mode 100644 index 484a0a30e..000000000 --- a/Sources/ProcessOut/Sources/Core/Markdown/MarkdownParser.swift +++ /dev/null @@ -1,42 +0,0 @@ -// -// MarkdownParser.swift -// ProcessOut -// -// Created by Andrii Vysotskyi on 13.06.2023. -// - -import Foundation -import cmark_gfm - -enum MarkdownParser { - - static func parse(string: String) -> MarkdownDocument { - let document = string.withCString { pointer in - cmark_parse_document(pointer, strlen(pointer), CMARK_OPT_SMART) - } - guard let document else { - preconditionFailure("Failed to parse markdown document") - } - return MarkdownDocument(cmarkNode: document) - } - - /// Escapes given plain text so it can be represented as is, in markdown. - static func escaped(plainText: String) -> String { - var markdown = String() - markdown.reserveCapacity(plainText.count) - for character in plainText { - if character.unicodeScalars.allSatisfy(Constants.specialCharacters.contains) { - markdown += Constants.escapeCharacter - } - markdown += String(character) - } - return markdown - } - - // MARK: - Private Nested Types - - private enum Constants { - static let specialCharacters = CharacterSet(charactersIn: "\\`*_{}[]()#+-.!") - static let escapeCharacter = "\\" - } -} diff --git a/Sources/ProcessOut/Sources/Core/Markdown/Nodes/MarkdownBlockQuote.swift b/Sources/ProcessOut/Sources/Core/Markdown/Nodes/MarkdownBlockQuote.swift deleted file mode 100644 index d82275347..000000000 --- a/Sources/ProcessOut/Sources/Core/Markdown/Nodes/MarkdownBlockQuote.swift +++ /dev/null @@ -1,19 +0,0 @@ -// -// MarkdownBlockQuote.swift -// ProcessOut -// -// Created by Andrii Vysotskyi on 15.06.2023. -// - -import cmark_gfm - -final class MarkdownBlockQuote: MarkdownBaseNode { - - override static var cmarkNodeType: cmark_node_type { - CMARK_NODE_BLOCK_QUOTE - } - - override func accept(visitor: V) -> V.Result { - visitor.visit(blockQuote: self) - } -} diff --git a/Sources/ProcessOut/Sources/Core/Markdown/Nodes/MarkdownCodeBlock.swift b/Sources/ProcessOut/Sources/Core/Markdown/Nodes/MarkdownCodeBlock.swift deleted file mode 100644 index cfb5e786e..000000000 --- a/Sources/ProcessOut/Sources/Core/Markdown/Nodes/MarkdownCodeBlock.swift +++ /dev/null @@ -1,34 +0,0 @@ -// -// MarkdownCodeBlock.swift -// ProcessOut -// -// Created by Andrii Vysotskyi on 15.06.2023. -// - -import cmark_gfm - -final class MarkdownCodeBlock: MarkdownBaseNode { - - /// Returns the info string from a fenced code block. - private(set) lazy var info: String? = { - String(cString: cmarkNode.pointee.as.code.info.data) - }() - - private(set) lazy var code: String = { - guard let literal = cmark_node_get_literal(cmarkNode) else { - assertionFailure("Unable to get text node value") - return "" - } - return String(cString: literal) - }() - - // MARK: - MarkdownBaseNode - - override static var cmarkNodeType: cmark_node_type { - CMARK_NODE_CODE_BLOCK - } - - override func accept(visitor: V) -> V.Result { - visitor.visit(codeBlock: self) - } -} diff --git a/Sources/ProcessOut/Sources/Core/Markdown/Nodes/MarkdownCodeSpan.swift b/Sources/ProcessOut/Sources/Core/Markdown/Nodes/MarkdownCodeSpan.swift deleted file mode 100644 index 9ff0087a9..000000000 --- a/Sources/ProcessOut/Sources/Core/Markdown/Nodes/MarkdownCodeSpan.swift +++ /dev/null @@ -1,29 +0,0 @@ -// -// MarkdownCodeSpan.swift -// ProcessOut -// -// Created by Andrii Vysotskyi on 15.06.2023. -// - -import cmark_gfm - -final class MarkdownCodeSpan: MarkdownBaseNode { - - private(set) lazy var code: String = { - guard let literal = cmark_node_get_literal(cmarkNode) else { - assertionFailure("Unable to get text node value") - return "" - } - return String(cString: literal) - }() - - // MARK: - MarkdownBaseNode - - override static var cmarkNodeType: cmark_node_type { - CMARK_NODE_CODE - } - - override func accept(visitor: V) -> V.Result { - visitor.visit(codeSpan: self) - } -} diff --git a/Sources/ProcessOut/Sources/Core/Markdown/Nodes/MarkdownDocument.swift b/Sources/ProcessOut/Sources/Core/Markdown/Nodes/MarkdownDocument.swift deleted file mode 100644 index 80c7703c1..000000000 --- a/Sources/ProcessOut/Sources/Core/Markdown/Nodes/MarkdownDocument.swift +++ /dev/null @@ -1,25 +0,0 @@ -// -// MarkdownDocument.swift -// ProcessOut -// -// Created by Andrii Vysotskyi on 11.06.2023. -// - -import cmark_gfm - -final class MarkdownDocument: MarkdownBaseNode { - - deinit { - cmark_node_free(cmarkNode) - } - - // MARK: - MarkdownBaseNode - - override static var cmarkNodeType: cmark_node_type { - CMARK_NODE_DOCUMENT - } - - override func accept(visitor: V) -> V.Result { - visitor.visit(document: self) - } -} diff --git a/Sources/ProcessOut/Sources/Core/Markdown/Nodes/MarkdownEmphasis.swift b/Sources/ProcessOut/Sources/Core/Markdown/Nodes/MarkdownEmphasis.swift deleted file mode 100644 index 879fe3af8..000000000 --- a/Sources/ProcessOut/Sources/Core/Markdown/Nodes/MarkdownEmphasis.swift +++ /dev/null @@ -1,21 +0,0 @@ -// -// MarkdownEmphasis.swift -// ProcessOut -// -// Created by Andrii Vysotskyi on 12.06.2023. -// - -import cmark_gfm - -final class MarkdownEmphasis: MarkdownBaseNode { - - // MARK: - MarkdownBaseNode - - override static var cmarkNodeType: cmark_node_type { - CMARK_NODE_EMPH - } - - override func accept(visitor: V) -> V.Result { - visitor.visit(emphasis: self) - } -} diff --git a/Sources/ProcessOut/Sources/Core/Markdown/Nodes/MarkdownHeading.swift b/Sources/ProcessOut/Sources/Core/Markdown/Nodes/MarkdownHeading.swift deleted file mode 100644 index 03433f23f..000000000 --- a/Sources/ProcessOut/Sources/Core/Markdown/Nodes/MarkdownHeading.swift +++ /dev/null @@ -1,25 +0,0 @@ -// -// MarkdownHeading.swift -// ProcessOut -// -// Created by Andrii Vysotskyi on 15.06.2023. -// - -import cmark_gfm - -final class MarkdownHeading: MarkdownBaseNode { - - private(set) lazy var level: Int = { - Int(cmarkNode.pointee.as.heading.level) - }() - - // MARK: - MarkdownBaseNode - - override static var cmarkNodeType: cmark_node_type { - CMARK_NODE_HEADING - } - - override func accept(visitor: V) -> V.Result { - visitor.visit(heading: self) - } -} diff --git a/Sources/ProcessOut/Sources/Core/Markdown/Nodes/MarkdownLinebreak.swift b/Sources/ProcessOut/Sources/Core/Markdown/Nodes/MarkdownLinebreak.swift deleted file mode 100644 index 9630c1a7f..000000000 --- a/Sources/ProcessOut/Sources/Core/Markdown/Nodes/MarkdownLinebreak.swift +++ /dev/null @@ -1,19 +0,0 @@ -// -// MarkdownLinebreak.swift -// ProcessOut -// -// Created by Andrii Vysotskyi on 15.06.2023. -// - -import cmark_gfm - -final class MarkdownLinebreak: MarkdownBaseNode { - - override static var cmarkNodeType: cmark_node_type { - CMARK_NODE_LINEBREAK - } - - override func accept(visitor: V) -> V.Result { - visitor.visit(linebreak: self) - } -} diff --git a/Sources/ProcessOut/Sources/Core/Markdown/Nodes/MarkdownLink.swift b/Sources/ProcessOut/Sources/Core/Markdown/Nodes/MarkdownLink.swift deleted file mode 100644 index 6e1356e91..000000000 --- a/Sources/ProcessOut/Sources/Core/Markdown/Nodes/MarkdownLink.swift +++ /dev/null @@ -1,25 +0,0 @@ -// -// MarkdownLink.swift -// ProcessOut -// -// Created by Andrii Vysotskyi on 15.06.2023. -// - -import cmark_gfm - -final class MarkdownLink: MarkdownBaseNode { - - private(set) lazy var url: String? = { - String(cString: cmarkNode.pointee.as.link.url.data) - }() - - // MARK: - MarkdownBaseNode - - override static var cmarkNodeType: cmark_node_type { - CMARK_NODE_LINK - } - - override func accept(visitor: V) -> V.Result { - visitor.visit(link: self) - } -} diff --git a/Sources/ProcessOut/Sources/Core/Markdown/Nodes/MarkdownList.swift b/Sources/ProcessOut/Sources/Core/Markdown/Nodes/MarkdownList.swift deleted file mode 100644 index 642f8fcef..000000000 --- a/Sources/ProcessOut/Sources/Core/Markdown/Nodes/MarkdownList.swift +++ /dev/null @@ -1,58 +0,0 @@ -// -// MarkdownList.swift -// ProcessOut -// -// Created by Andrii Vysotskyi on 12.06.2023. -// - -import cmark_gfm - -final class MarkdownList: MarkdownBaseNode { - - enum ListType { - - /// Ordered - case ordered(delimiter: Character, startIndex: Int) - - /// Bullet aka unordered list. - case bullet(marker: Character) - } - - private(set) lazy var type: ListType = { - let listNode = cmarkNode.pointee.as.list - switch listNode.list_type { - case CMARK_BULLET_LIST: - let marker = Character(Unicode.Scalar(listNode.bullet_char)) - return .bullet(marker: marker) - case CMARK_ORDERED_LIST: - let delimiter: Character - switch cmark_node_get_list_delim(cmarkNode) { - case CMARK_PERIOD_DELIM: - delimiter = "." - case CMARK_PAREN_DELIM: - delimiter = ")" - default: - assertionFailure("Unexpected delimiter type") - delimiter = "." - } - let startIndex = Int(listNode.start) - return .ordered(delimiter: delimiter, startIndex: startIndex) - default: - preconditionFailure("Unsupported list type: \(listNode.list_type)") - } - }() - - private(set) lazy var isTight: Bool = { - cmarkNode.pointee.as.list.tight - }() - - // MARK: - MarkdownBaseNode - - override static var cmarkNodeType: cmark_node_type { - CMARK_NODE_LIST - } - - override func accept(visitor: V) -> V.Result { - visitor.visit(list: self) - } -} diff --git a/Sources/ProcessOut/Sources/Core/Markdown/Nodes/MarkdownListItem.swift b/Sources/ProcessOut/Sources/Core/Markdown/Nodes/MarkdownListItem.swift deleted file mode 100644 index 3cf7ccd60..000000000 --- a/Sources/ProcessOut/Sources/Core/Markdown/Nodes/MarkdownListItem.swift +++ /dev/null @@ -1,21 +0,0 @@ -// -// MarkdownListItem.swift -// ProcessOut -// -// Created by Andrii Vysotskyi on 12.06.2023. -// - -import cmark_gfm - -final class MarkdownListItem: MarkdownBaseNode { - - // MARK: - MarkdownBaseNode - - override static var cmarkNodeType: cmark_node_type { - CMARK_NODE_ITEM - } - - override func accept(visitor: V) -> V.Result { - visitor.visit(listItem: self) - } -} diff --git a/Sources/ProcessOut/Sources/Core/Markdown/Nodes/MarkdownNode.swift b/Sources/ProcessOut/Sources/Core/Markdown/Nodes/MarkdownNode.swift deleted file mode 100644 index 17537fe22..000000000 --- a/Sources/ProcessOut/Sources/Core/Markdown/Nodes/MarkdownNode.swift +++ /dev/null @@ -1,43 +0,0 @@ -// -// MarkdownBaseNode.swift -// ProcessOut -// -// Created by Andrii Vysotskyi on 12.06.2023. -// - -import cmark_gfm - -class MarkdownBaseNode { - - typealias CmarkNode = UnsafeMutablePointer - - class var cmarkNodeType: cmark_node_type { - fatalError("Must be implemented by subclass.") - } - - required init(cmarkNode: CmarkNode, validatesType: Bool = true) { - if validatesType { - assert(cmarkNode.pointee.type == Self.cmarkNodeType.rawValue) - } - self.cmarkNode = cmarkNode - } - - /// Returns node children. - private(set) lazy var children: [MarkdownBaseNode] = { - var cmarkChild = cmarkNode.pointee.first_child - var children: [MarkdownBaseNode] = [] - while let cmarkNode = cmarkChild { - let child = MarkdownNodeFactory(cmarkNode: cmarkNode).create() - children.append(child) - cmarkChild = cmarkNode.pointee.next - } - return children - }() - - let cmarkNode: CmarkNode - - /// Accepts given visitor. - func accept(visitor: V) -> V.Result { // swiftlint:disable:this unavailable_function - fatalError("Must be implemented by subclass.") - } -} diff --git a/Sources/ProcessOut/Sources/Core/Markdown/Nodes/MarkdownParagraph.swift b/Sources/ProcessOut/Sources/Core/Markdown/Nodes/MarkdownParagraph.swift deleted file mode 100644 index 8fb905467..000000000 --- a/Sources/ProcessOut/Sources/Core/Markdown/Nodes/MarkdownParagraph.swift +++ /dev/null @@ -1,21 +0,0 @@ -// -// MarkdownParagraph.swift -// ProcessOut -// -// Created by Andrii Vysotskyi on 12.06.2023. -// - -import cmark_gfm - -final class MarkdownParagraph: MarkdownBaseNode { - - // MARK: - MarkdownBaseNode - - override static var cmarkNodeType: cmark_node_type { - CMARK_NODE_PARAGRAPH - } - - override func accept(visitor: V) -> V.Result { - visitor.visit(paragraph: self) - } -} diff --git a/Sources/ProcessOut/Sources/Core/Markdown/Nodes/MarkdownSoftbreak.swift b/Sources/ProcessOut/Sources/Core/Markdown/Nodes/MarkdownSoftbreak.swift deleted file mode 100644 index eb3a5c23c..000000000 --- a/Sources/ProcessOut/Sources/Core/Markdown/Nodes/MarkdownSoftbreak.swift +++ /dev/null @@ -1,19 +0,0 @@ -// -// MarkdownSoftbreak.swift -// ProcessOut -// -// Created by Andrii Vysotskyi on 15.06.2023. -// - -import cmark_gfm - -final class MarkdownSoftbreak: MarkdownBaseNode { - - override static var cmarkNodeType: cmark_node_type { - CMARK_NODE_SOFTBREAK - } - - override func accept(visitor: V) -> V.Result { - visitor.visit(softbreak: self) - } -} diff --git a/Sources/ProcessOut/Sources/Core/Markdown/Nodes/MarkdownStrong.swift b/Sources/ProcessOut/Sources/Core/Markdown/Nodes/MarkdownStrong.swift deleted file mode 100644 index 00055ad53..000000000 --- a/Sources/ProcessOut/Sources/Core/Markdown/Nodes/MarkdownStrong.swift +++ /dev/null @@ -1,21 +0,0 @@ -// -// MarkdownStrong.swift -// ProcessOut -// -// Created by Andrii Vysotskyi on 12.06.2023. -// - -import cmark_gfm - -final class MarkdownStrong: MarkdownBaseNode { - - // MARK: - MarkdownBaseNode - - override static var cmarkNodeType: cmark_node_type { - CMARK_NODE_STRONG - } - - override func accept(visitor: V) -> V.Result { - visitor.visit(strong: self) - } -} diff --git a/Sources/ProcessOut/Sources/Core/Markdown/Nodes/MarkdownText.swift b/Sources/ProcessOut/Sources/Core/Markdown/Nodes/MarkdownText.swift deleted file mode 100644 index ffbe83004..000000000 --- a/Sources/ProcessOut/Sources/Core/Markdown/Nodes/MarkdownText.swift +++ /dev/null @@ -1,29 +0,0 @@ -// -// MarkdownText.swift -// ProcessOut -// -// Created by Andrii Vysotskyi on 12.06.2023. -// - -import cmark_gfm - -final class MarkdownText: MarkdownBaseNode { - - private(set) lazy var value: String = { - guard let literal = cmark_node_get_literal(cmarkNode) else { - assertionFailure("Unable to get text node value") - return "" - } - return String(cString: literal) - }() - - // MARK: - MarkdownBaseNode - - override static var cmarkNodeType: cmark_node_type { - CMARK_NODE_TEXT - } - - override func accept(visitor: V) -> V.Result { - visitor.visit(text: self) - } -} diff --git a/Sources/ProcessOut/Sources/Core/Markdown/Nodes/MarkdownThematicBreak.swift b/Sources/ProcessOut/Sources/Core/Markdown/Nodes/MarkdownThematicBreak.swift deleted file mode 100644 index a833baf17..000000000 --- a/Sources/ProcessOut/Sources/Core/Markdown/Nodes/MarkdownThematicBreak.swift +++ /dev/null @@ -1,19 +0,0 @@ -// -// MarkdownThematicBreak.swift -// ProcessOut -// -// Created by Andrii Vysotskyi on 15.06.2023. -// - -import cmark_gfm - -final class MarkdownThematicBreak: MarkdownBaseNode { - - override static var cmarkNodeType: cmark_node_type { - CMARK_NODE_THEMATIC_BREAK - } - - override func accept(visitor: V) -> V.Result { - visitor.visit(thematicBreak: self) - } -} diff --git a/Sources/ProcessOut/Sources/Core/Markdown/Nodes/MarkdownUnknown.swift b/Sources/ProcessOut/Sources/Core/Markdown/Nodes/MarkdownUnknown.swift deleted file mode 100644 index 13398216f..000000000 --- a/Sources/ProcessOut/Sources/Core/Markdown/Nodes/MarkdownUnknown.swift +++ /dev/null @@ -1,20 +0,0 @@ -// -// MarkdownUnknown.swift -// ProcessOut -// -// Created by Andrii Vysotskyi on 15.06.2023. -// - -/// Unknown node. -final class MarkdownUnknown: MarkdownBaseNode { - - required init(cmarkNode: CmarkNode, validatesType: Bool = false) { - super.init(cmarkNode: cmarkNode, validatesType: false) - } - - // MARK: - MarkdownBaseNode - - override func accept(visitor: V) -> V.Result { - visitor.visit(node: self) - } -} diff --git a/Sources/ProcessOut/Sources/Core/Markdown/Visitor/MarkdownDebugDescriptionPrinter.swift b/Sources/ProcessOut/Sources/Core/Markdown/Visitor/MarkdownDebugDescriptionPrinter.swift deleted file mode 100644 index e5aea8978..000000000 --- a/Sources/ProcessOut/Sources/Core/Markdown/Visitor/MarkdownDebugDescriptionPrinter.swift +++ /dev/null @@ -1,146 +0,0 @@ -// -// MarkdownDebugDescriptionPrinter.swift -// ProcessOut -// -// Created by Andrii Vysotskyi on 14.06.2023. -// - -import Foundation - -final class MarkdownDebugDescriptionPrinter: MarkdownVisitor { - - init(level: Int = 0) { - self.level = level - } - - // MARK: - MarkdownVisitor - - func visit(node: MarkdownUnknown) -> String { - description(node: node, nodeName: "Unknown") - } - - func visit(document: MarkdownDocument) -> String { - description(node: document, nodeName: "Document") - } - - func visit(emphasis: MarkdownEmphasis) -> String { - description(node: emphasis, nodeName: "Emphasis") - } - - func visit(list: MarkdownList) -> String { - let attributes: [String: CustomStringConvertible] - switch list.type { - case let .ordered(delimiter, startIndex): - attributes = ["start": startIndex, "delimiter": delimiter] - case .bullet(let marker): - attributes = ["marker": marker] - } - return description(node: list, nodeName: "List", attributes: attributes) - } - - func visit(listItem: MarkdownListItem) -> String { - description(node: listItem, nodeName: "Item") - } - - func visit(paragraph: MarkdownParagraph) -> String { - description(node: paragraph, nodeName: "Paragraph") - } - - func visit(strong: MarkdownStrong) -> String { - description(node: strong, nodeName: "Bold") - } - - func visit(text: MarkdownText) -> String { - description(node: text, nodeName: "Text", content: text.value) - } - - func visit(softbreak: MarkdownSoftbreak) -> String { - description(node: softbreak, nodeName: "Softbreak") - } - - func visit(linebreak: MarkdownLinebreak) -> String { - description(node: linebreak, nodeName: "Linebreak") - } - - func visit(heading: MarkdownHeading) -> String { - description(node: heading, nodeName: "Heading", attributes: ["level": heading.level]) - } - - func visit(blockQuote: MarkdownBlockQuote) -> String { - description(node: blockQuote, nodeName: "Block Quote") - } - - func visit(codeBlock: MarkdownCodeBlock) -> String { - var attributes: [String: CustomStringConvertible] = [:] - if let info = codeBlock.info { - attributes["info"] = info - } - return description(node: codeBlock, nodeName: "Code Block", attributes: attributes, content: codeBlock.code) - } - - func visit(thematicBreak: MarkdownThematicBreak) -> String { - description(node: thematicBreak, nodeName: "Thematic Break") - } - - func visit(codeSpan: MarkdownCodeSpan) -> String { - description(node: codeSpan, nodeName: "Code Span", content: codeSpan.code) - } - - func visit(link: MarkdownLink) -> String { - var attributes: [String: CustomStringConvertible] = [:] - if let url = link.url { - attributes["url"] = url - } - return description(node: link, nodeName: "Link", attributes: attributes) - } - - // MARK: - Private Properties - - private let level: Int - - // MARK: - Private Methods - - private func description(node: String, attributes: [String: CustomStringConvertible], content: String?) -> String { - var description = String(repeating: " ", count: 2 * level) + "- " + node - let attributesDescription = attributes - .map { key, value in - [key, value.description].joined(separator: "=") - } - .joined(separator: " ") - if !attributesDescription.isEmpty { - description += " (\(attributesDescription))" - } - if let content, !content.isEmpty { - // Newlines and tabs are visualized for better readability. - let escapedContent = content - .replacingOccurrences(of: "\n", with: "\\n") - .replacingOccurrences(of: "\t", with: "\\t") - description += ": \(escapedContent)" - } - return description - } - - private func description( - node: MarkdownBaseNode, - nodeName: String, - attributes: [String: CustomStringConvertible] = [:], - content: String? = nil - ) -> String { - var descriptionComponents = [ - description(node: nodeName, attributes: attributes, content: content) - ] - let childVisitor = MarkdownDebugDescriptionPrinter(level: level + 1) - node.children.forEach { node in - descriptionComponents.append(node.accept(visitor: childVisitor)) - } - return descriptionComponents.joined(separator: "\n") - } -} - -extension MarkdownBaseNode: CustomDebugStringConvertible { - - var debugDescription: String { - let visitor = MarkdownDebugDescriptionPrinter() - return self.accept(visitor: visitor) - } -} diff --git a/Sources/ProcessOut/Sources/Core/Markdown/Visitor/MarkdownVisitor.swift b/Sources/ProcessOut/Sources/Core/Markdown/Visitor/MarkdownVisitor.swift deleted file mode 100644 index f8b4f408f..000000000 --- a/Sources/ProcessOut/Sources/Core/Markdown/Visitor/MarkdownVisitor.swift +++ /dev/null @@ -1,59 +0,0 @@ -// -// MarkdownVisitor.swift -// ProcessOut -// -// Created by Andrii Vysotskyi on 12.06.2023. -// - -protocol MarkdownVisitor { - - associatedtype Result - - /// Visits unknown node. - func visit(node: MarkdownUnknown) -> Result - - /// Visits document. - func visit(document: MarkdownDocument) -> Result - - /// Visits emphasis node. - func visit(emphasis: MarkdownEmphasis) -> Result - - /// Visits list node. - func visit(list: MarkdownList) -> Result - - /// Visits list item. - func visit(listItem: MarkdownListItem) -> Result - - /// Visits paragraph node. - func visit(paragraph: MarkdownParagraph) -> Result - - /// Visits strong node. - func visit(strong: MarkdownStrong) -> Result - - /// Visits text node. - func visit(text: MarkdownText) -> Result - - /// Visits softbreak node. - func visit(softbreak: MarkdownSoftbreak) -> Result - - /// Visits linebreak node. - func visit(linebreak: MarkdownLinebreak) -> Result - - /// Visits heading node. - func visit(heading: MarkdownHeading) -> Result - - /// Visits block quote. - func visit(blockQuote: MarkdownBlockQuote) -> Result - - /// Visits code block - func visit(codeBlock: MarkdownCodeBlock) -> Result - - /// Visits thematic break. - func visit(thematicBreak: MarkdownThematicBreak) -> Result - - /// Visits code span. - func visit(codeSpan: MarkdownCodeSpan) -> Result - - /// Visits link node. - func visit(link: MarkdownLink) -> Result -} diff --git a/Sources/ProcessOut/Sources/Core/Markers/POAutoAsync.swift b/Sources/ProcessOut/Sources/Core/Markers/POAutoAsync.swift deleted file mode 100644 index d0cc2430b..000000000 --- a/Sources/ProcessOut/Sources/Core/Markers/POAutoAsync.swift +++ /dev/null @@ -1,14 +0,0 @@ -// -// POAutoAsync.swift -// ProcessOut -// -// Created by Andrii Vysotskyi on 19.10.2022. -// - -/// For types that implement this protocol, [Sourcery](https://github.com/krzysztofzablocki/Sourcery) will -/// automatically generate `async` methods for their callback-based counterparts. Only methods where last argument -/// is a closure that matches `(Result) -> Void` type are picked, see `Templates/AutoAsync.stencil` in -/// project's root directory for details. -@available(*, deprecated, message: "No longer used.") -@_marker -public protocol POAutoAsync { } diff --git a/Sources/ProcessOut/Sources/Core/Markers/POAutoCompletion.swift b/Sources/ProcessOut/Sources/Core/Markers/POAutoCompletion.swift deleted file mode 100644 index ca6f1ac18..000000000 --- a/Sources/ProcessOut/Sources/Core/Markers/POAutoCompletion.swift +++ /dev/null @@ -1,12 +0,0 @@ -// -// POAutoCompletion.swift -// ProcessOut -// -// Created by Andrii Vysotskyi on 07.12.2023. -// - -/// For types that implement this protocol, [Sourcery](https://github.com/krzysztofzablocki/Sourcery) will -/// automatically generate methods with completion for their async-based counterparts. -@available(*, deprecated, message: "No longer used.") -@_marker -public protocol POAutoCompletion { } diff --git a/Sources/ProcessOut/Sources/Core/PropertyWrappers/ImmutableNullHashable.swift b/Sources/ProcessOut/Sources/Core/PropertyWrappers/ImmutableNullHashable.swift deleted file mode 100644 index b89550179..000000000 --- a/Sources/ProcessOut/Sources/Core/PropertyWrappers/ImmutableNullHashable.swift +++ /dev/null @@ -1,20 +0,0 @@ -// -// ImmutableNullHashable.swift -// ProcessOut -// -// Created by Andrii Vysotskyi on 27.04.2023. -// - -@propertyWrapper -struct ImmutableNullHashable: Hashable { - - let wrappedValue: Value - - static func == (lhs: Self, rhs: Self) -> Bool { - true - } - - func hash(into hasher: inout Hasher) { - // Ignored - } -} diff --git a/Sources/ProcessOut/Sources/Core/PropertyWrappers/ReferenceWrapper.swift b/Sources/ProcessOut/Sources/Core/PropertyWrappers/ReferenceWrapper.swift deleted file mode 100644 index 0939fb9aa..000000000 --- a/Sources/ProcessOut/Sources/Core/PropertyWrappers/ReferenceWrapper.swift +++ /dev/null @@ -1,77 +0,0 @@ -// -// ReferenceWrapper.swift -// ProcessOut -// -// Created by Andrii Vysotskyi on 27.04.2023. -// - -import Foundation - -@propertyWrapper -final class ReferenceWrapper { - - typealias Observer = (_ value: Value) -> Void - - init(value: Value) { - self.wrappedValue = value - observers = [:] - } - - var wrappedValue: Value { - didSet { wrappedValueDidChange() } - } - - var projectedValue: ReferenceWrapper { - self - } - - func addObserver(_ observer: @escaping Observer) -> AnyObject { - let id = UUID().uuidString - observers[id] = observer - let cancellable = Cancellable { [weak self] in - self?.observers[id] = nil - } - return cancellable - } - - // MARK: - Private Nested Types - - private final class Cancellable { - - let didCancel: () -> Void - - init(didCancel: @escaping () -> Void) { - self.didCancel = didCancel - } - - deinit { - didCancel() - } - } - - // MARK: - Private - - private var observers: [String: Observer] - - private func wrappedValueDidChange() { - observers.values.forEach { $0(wrappedValue) } - } -} - -extension ReferenceWrapper: Hashable, Equatable where Value: Hashable { - - static func == (lhs: ReferenceWrapper, rhs: ReferenceWrapper) -> Bool { - lhs.wrappedValue == rhs.wrappedValue && lhs.observerIds == rhs.observerIds - } - - func hash(into hasher: inout Hasher) { - hasher.combine(wrappedValue) - hasher.combine(observerIds) - } - - // MARK: - Private Properties - - private var observerIds: Set { - Set(observers.keys) - } -} diff --git a/Sources/ProcessOut/Sources/Core/RetryStrategy/RetryStrategy.swift b/Sources/ProcessOut/Sources/Core/RetryStrategy/RetryStrategy.swift index 4883f0bbe..21326cb6f 100644 --- a/Sources/ProcessOut/Sources/Core/RetryStrategy/RetryStrategy.swift +++ b/Sources/ProcessOut/Sources/Core/RetryStrategy/RetryStrategy.swift @@ -7,7 +7,7 @@ import Foundation -struct RetryStrategy { +struct RetryStrategy: Sendable { /// Returns time interval to void for given retry. func interval(for retry: Int) -> TimeInterval { @@ -18,7 +18,7 @@ struct RetryStrategy { let maximumRetries: Int /// Function to use to calculate delay for given attempt number. - let intervalFunction: (_ retry: Int) -> TimeInterval + let intervalFunction: @Sendable (_ retry: Int) -> TimeInterval } extension RetryStrategy { diff --git a/Sources/ProcessOut/Sources/Core/Utils/AsyncUtils.swift b/Sources/ProcessOut/Sources/Core/Utils/AsyncUtils.swift index dd87f55dd..996983b0a 100644 --- a/Sources/ProcessOut/Sources/Core/Utils/AsyncUtils.swift +++ b/Sources/ProcessOut/Sources/Core/Utils/AsyncUtils.swift @@ -12,14 +12,14 @@ import Foundation /// - Warning: operation should support cancellation, otherwise calling this method has no effect. func withTimeout( _ 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 { - @POUnfairlyLocked var isTimedOut = false + let isTimedOut = POUnfairlyLocked(wrappedValue: false) let task = Task(operation: operation) let timeoutTask = Task { try await Task.sleep(seconds: timeout) - $isTimedOut.withLock { value in + isTimedOut.withLock { value in value = true } guard !Task.isCancelled else { @@ -33,7 +33,7 @@ func withTimeout( timeoutTask.cancel() return value } catch { - if task.isCancelled, isTimedOut { + if task.isCancelled, isTimedOut.wrappedValue { throw timeoutError() } timeoutTask.cancel() @@ -48,10 +48,10 @@ func withTimeout( // MARK: - Retry func retry( - operation: @escaping @Sendable () async throws -> T, - while condition: @escaping (Result) -> Bool, + operation: @escaping @Sendable @isolated(any) () async throws -> T, + while condition: @escaping @Sendable (Result) -> Bool, timeout: TimeInterval, - timeoutError: @autoclosure () -> Error, + timeoutError: @autoclosure @Sendable () -> Error, retryStrategy: RetryStrategy? = nil ) async throws -> T { let operationBox = { @Sendable in @@ -67,9 +67,9 @@ func retry( } private func retry( - operation: @escaping @Sendable () async throws -> T, + operation: @escaping @Sendable @isolated(any) () async throws -> T, after result: Result, - while condition: @escaping (Result) -> Bool, + while condition: @escaping @Sendable (Result) -> Bool, retryStrategy: RetryStrategy?, attempt: Int ) async throws -> T { @@ -91,11 +91,13 @@ private func retry( ) } -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) diff --git a/Sources/ProcessOut/Sources/Core/Utils/Batcher.swift b/Sources/ProcessOut/Sources/Core/Utils/Batcher.swift index 2d435250b..6166469fd 100644 --- a/Sources/ProcessOut/Sources/Core/Utils/Batcher.swift +++ b/Sources/ProcessOut/Sources/Core/Utils/Batcher.swift @@ -7,9 +7,9 @@ import Foundation -final class Batcher { +final class Batcher: Sendable { - typealias Executor = (Array) async -> Bool + typealias Executor = @Sendable @isolated(any) (Array) async -> Bool init(executionInterval: TimeInterval = 10, executor: @escaping Executor) { self.executionInterval = executionInterval @@ -38,14 +38,14 @@ final class Batcher { private let executionInterval: TimeInterval private let lock: UnfairLock - private var pendingTasks: [Task] - private var executionTimer: Timer? + private nonisolated(unsafe) var pendingTasks: [Task] + private nonisolated(unsafe) var executionTimer: Timer? // MARK: - Private Methods /// - NOTE: method mutates self but is not thread safe. private func scheduleExecutionUnsafe() { - let timer = Timer(timeInterval: executionInterval, repeats: false) { [weak self] _ in + nonisolated(unsafe) let timer = Timer(timeInterval: executionInterval, repeats: false) { [weak self] _ in guard let self = self else { return } diff --git a/Sources/ProcessOut/Sources/Core/Utils/POTypedRepresentation.swift b/Sources/ProcessOut/Sources/Core/Utils/POTypedRepresentation.swift deleted file mode 100644 index d19ade046..000000000 --- a/Sources/ProcessOut/Sources/Core/Utils/POTypedRepresentation.swift +++ /dev/null @@ -1,104 +0,0 @@ -// -// POTypedRepresentation.swift -// ProcessOut -// -// Created by Andrii Vysotskyi on 11.06.2024. -// - -// todo(andrii-vysotskyi): remove when updating to 5.0.0 - -import Foundation - -/// Introduces typed version of a property in a backward compatible way. -@propertyWrapper -public struct POTypedRepresentation { - - public init(wrappedValue: Wrapped) { - self._wrappedValue = wrappedValue - } - - @available(*, deprecated, message: "Use typed representation accessible via projectedValue.typed instead.") - public var wrappedValue: Wrapped { - get { _wrappedValue } - set { _wrappedValue = newValue } - } - - public var projectedValue: Self { - self - } - - // MARK: - Private Properties - - private var _wrappedValue: Wrapped -} - -extension POTypedRepresentation where Representation.RawValue == Wrapped { - - /// Returns typed representation of self. - public var typed: Representation { - Representation(rawValue: _wrappedValue)! // swiftlint:disable:this force_unwrapping - } -} - -extension POTypedRepresentation where Representation.RawValue? == Wrapped { - - /// Returns typed representation of self. - public var typed: Representation? { - _wrappedValue.flatMap { Representation(rawValue: $0) } - } -} - -extension POTypedRepresentation { - - /// Returns typed representation of self. - public func typed(wrappedType: T.Type = T.self) -> Representation? where Wrapped == T?, T: RawRepresentable, T.RawValue == Representation.RawValue { // swiftlint:disable:this line_length - _wrappedValue.flatMap { Representation(rawValue: $0.rawValue) } - } -} - -extension POTypedRepresentation: Hashable where Wrapped: Hashable { - - public func hash(into hasher: inout Hasher) { - _wrappedValue.hash(into: &hasher) - } -} - -extension POTypedRepresentation: Equatable where Wrapped: Equatable { - - public static func == (lhs: Self, rhs: Self) -> Bool { - lhs._wrappedValue == rhs._wrappedValue - } -} - -extension POTypedRepresentation: Encodable where Wrapped: Encodable { - - public func encode(to encoder: any Encoder) throws { - try _wrappedValue.encode(to: encoder) - } -} - -extension POTypedRepresentation: Decodable where Wrapped: Decodable { - - public init(from decoder: any Decoder) throws { - let wrappedValue = try Wrapped(from: decoder) - self = .init(wrappedValue: wrappedValue) - } -} - -extension KeyedEncodingContainer { - - public mutating func encode( - _ value: POTypedRepresentation, forKey key: KeyedEncodingContainer.Key - ) throws { - try value.encode(to: superEncoder(forKey: key)) - } -} - -extension KeyedDecodingContainer { - - public func decode( - _ type: POTypedRepresentation.Type, forKey key: KeyedDecodingContainer.Key - ) throws -> POTypedRepresentation { - try type.init(from: try superDecoder(forKey: key)) - } -} diff --git a/Sources/ProcessOut/Sources/Core/Utils/Strings+PreferredLocalization.swift b/Sources/ProcessOut/Sources/Core/Utils/Strings+PreferredLocalization.swift index dcd33ece2..9d1f556e0 100644 --- a/Sources/ProcessOut/Sources/Core/Utils/Strings+PreferredLocalization.swift +++ b/Sources/ProcessOut/Sources/Core/Utils/Strings+PreferredLocalization.swift @@ -10,8 +10,9 @@ import Foundation enum Strings { static var preferredLocalization: String { - // todo(andrii-vysotskyi): it should be possible to inject supported localizations - // instead of only relying on languages that are supported by current bundle. + // Supported languages are same for all products. There is "dummy" key defined in + // ProcessOut's Localizable.xcstrings that is translated to all supported languages + // to ensure that resolved value is consistent between targets. let bundle: Bundle if Bundle.main.preferredLocalizations.first == BundleLocator.bundle.preferredLocalizations.first { bundle = BundleLocator.bundle diff --git a/Sources/ProcessOut/Sources/Core/Utils/UnfairlyLocked/POUnfairlyLocked.swift b/Sources/ProcessOut/Sources/Core/Utils/UnfairlyLocked/POUnfairlyLocked.swift index 2fa43725c..df7289c3a 100644 --- a/Sources/ProcessOut/Sources/Core/Utils/UnfairlyLocked/POUnfairlyLocked.swift +++ b/Sources/ProcessOut/Sources/Core/Utils/UnfairlyLocked/POUnfairlyLocked.swift @@ -8,8 +8,8 @@ import os /// A thread-safe wrapper around a value. -@propertyWrapper -@_spi(PO) public final class POUnfairlyLocked: @unchecked Sendable { +@_spi(PO) +public final class POUnfairlyLocked: @unchecked Sendable { public init(wrappedValue: Value) { value = wrappedValue @@ -20,14 +20,8 @@ import os lock.withLock { value } } - public var projectedValue: POUnfairlyLocked { - self - } - - public func withLock(_ body: (inout Value) -> R) -> R { - lock.withLock { - body(&value) - } + public func withLock(_ body: (inout Value) throws -> R) rethrows -> R { + try lock.withLock { try body(&value) } } // MARK: - Private Properties @@ -35,3 +29,15 @@ import os 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(_ body: () throws -> R) rethrows -> R { + try withLock { _ in try body() } + } +} diff --git a/Sources/ProcessOut/Sources/Core/Utils/UnfairlyLocked/UnfairLock.swift b/Sources/ProcessOut/Sources/Core/Utils/UnfairlyLocked/UnfairLock.swift index 5a12c1c42..0c58c9d12 100644 --- a/Sources/ProcessOut/Sources/Core/Utils/UnfairlyLocked/UnfairLock.swift +++ b/Sources/ProcessOut/Sources/Core/Utils/UnfairlyLocked/UnfairLock.swift @@ -8,19 +8,19 @@ import os /// An `os_unfair_lock` wrapper. -final class UnfairLock { +final class UnfairLock: Sendable { init() { unfairLock = .allocate(capacity: 1) unfairLock.initialize(to: os_unfair_lock()) } - func withLock(_ body: () -> R) -> R { + func withLock(_ body: () throws -> R) rethrows -> R { defer { os_unfair_lock_unlock(unfairLock) } os_unfair_lock_lock(unfairLock) - return body() + return try body() } deinit { @@ -30,5 +30,5 @@ final class UnfairLock { // MARK: - Private Properties - private let unfairLock: os_unfair_lock_t + private nonisolated(unsafe) let unfairLock: os_unfair_lock_t } diff --git a/Sources/ProcessOut/Sources/Generated/Files+Generated.swift b/Sources/ProcessOut/Sources/Generated/Files+Generated.swift deleted file mode 100644 index 081b11d23..000000000 --- a/Sources/ProcessOut/Sources/Generated/Files+Generated.swift +++ /dev/null @@ -1,54 +0,0 @@ -// swiftlint:disable all -// Generated using SwiftGen — https://github.com/SwiftGen/SwiftGen -// todo(andrii-vysotskyi): remove before releasing 5.0.0 - -import Foundation - -// swiftlint:disable superfluous_disable_command file_length line_length implicit_return - -// MARK: - Files - -// swiftlint:disable explicit_type_interface identifier_name -// swiftlint:disable nesting type_body_length type_name vertical_whitespace_opening_braces -internal enum Files { - /// PhoneNumberMetadata.json - internal static let phoneNumberMetadata = File(name: "PhoneNumberMetadata", ext: "json", relativePath: "", mimeType: "application/json") -} -// swiftlint:enable explicit_type_interface identifier_name -// swiftlint:enable nesting type_body_length type_name vertical_whitespace_opening_braces - -// MARK: - Implementation Details - -internal struct File { - internal let name: String - internal let ext: String? - internal let relativePath: String - internal let mimeType: String - - internal var url: URL { - return url(locale: nil) - } - - internal func url(locale: Locale?) -> URL { - let bundle = BundleLocator.bundle - let url = bundle.url( - forResource: name, - withExtension: ext, - subdirectory: relativePath, - localization: locale?.identifier - ) - guard let result = url else { - let file = name + (ext.flatMap { ".\($0)" } ?? "") - fatalError("Could not locate file named \(file)") - } - return result - } - - internal var path: String { - return path(locale: nil) - } - - internal func path(locale: Locale?) -> String { - return url(locale: locale).path - } -} diff --git a/Sources/ProcessOut/Sources/Generated/Fonts+Generated.swift b/Sources/ProcessOut/Sources/Generated/Fonts+Generated.swift deleted file mode 100644 index bd6af3e5b..000000000 --- a/Sources/ProcessOut/Sources/Generated/Fonts+Generated.swift +++ /dev/null @@ -1,145 +0,0 @@ -// swiftlint:disable all -// Generated using SwiftGen — https://github.com/SwiftGen/SwiftGen -// todo(andrii-vysotskyi): remove before releasing 5.0.0 - -#if os(macOS) - import AppKit.NSFont -#elseif os(iOS) || os(tvOS) || os(watchOS) - import UIKit.UIFont -#endif -#if canImport(SwiftUI) - import SwiftUI -#endif - -// Deprecated typealiases -@available(*, deprecated, renamed: "FontConvertible.Font", message: "This typealias will be removed in SwiftGen 7.0") -internal typealias Font = FontConvertible.Font - -// swiftlint:disable superfluous_disable_command file_length implicit_return - -// MARK: - Fonts - -// swiftlint:disable identifier_name line_length type_body_length -internal enum FontFamily { - internal enum WorkSans { - internal static let italic = FontConvertible(name: "WorkSans-Italic", family: "Work Sans", path: "WorkSans-Italic.ttf") - internal static let regular = FontConvertible(name: "WorkSans-Regular", family: "Work Sans", path: "WorkSans.ttf") - internal static let blackItalic = FontConvertible(name: "WorkSansItalic-Black", family: "Work Sans", path: "WorkSans-Italic.ttf") - internal static let boldItalic = FontConvertible(name: "WorkSansItalic-Bold", family: "Work Sans", path: "WorkSans-Italic.ttf") - internal static let extraBoldItalic = FontConvertible(name: "WorkSansItalic-ExtraBold", family: "Work Sans", path: "WorkSans-Italic.ttf") - internal static let extraLightItalic = FontConvertible(name: "WorkSansItalic-ExtraLight", family: "Work Sans", path: "WorkSans-Italic.ttf") - internal static let lightItalic = FontConvertible(name: "WorkSansItalic-Light", family: "Work Sans", path: "WorkSans-Italic.ttf") - internal static let mediumItalic = FontConvertible(name: "WorkSansItalic-Medium", family: "Work Sans", path: "WorkSans-Italic.ttf") - internal static let semiBoldItalic = FontConvertible(name: "WorkSansItalic-SemiBold", family: "Work Sans", path: "WorkSans-Italic.ttf") - internal static let thinItalic = FontConvertible(name: "WorkSansItalic-Thin", family: "Work Sans", path: "WorkSans-Italic.ttf") - internal static let black = FontConvertible(name: "WorkSansRoman-Black", family: "Work Sans", path: "WorkSans.ttf") - internal static let bold = FontConvertible(name: "WorkSansRoman-Bold", family: "Work Sans", path: "WorkSans.ttf") - internal static let extraBold = FontConvertible(name: "WorkSansRoman-ExtraBold", family: "Work Sans", path: "WorkSans.ttf") - internal static let extraLight = FontConvertible(name: "WorkSansRoman-ExtraLight", family: "Work Sans", path: "WorkSans.ttf") - internal static let light = FontConvertible(name: "WorkSansRoman-Light", family: "Work Sans", path: "WorkSans.ttf") - internal static let medium = FontConvertible(name: "WorkSansRoman-Medium", family: "Work Sans", path: "WorkSans.ttf") - internal static let semiBold = FontConvertible(name: "WorkSansRoman-SemiBold", family: "Work Sans", path: "WorkSans.ttf") - internal static let thin = FontConvertible(name: "WorkSansRoman-Thin", family: "Work Sans", path: "WorkSans.ttf") - internal static let all: [FontConvertible] = [italic, regular, blackItalic, boldItalic, extraBoldItalic, extraLightItalic, lightItalic, mediumItalic, semiBoldItalic, thinItalic, black, bold, extraBold, extraLight, light, medium, semiBold, thin] - } - internal static let allCustomFonts: [FontConvertible] = [WorkSans.all].flatMap { $0 } - internal static func registerAllCustomFonts() { - allCustomFonts.forEach { $0.register() } - } -} -// swiftlint:enable identifier_name line_length type_body_length - -// MARK: - Implementation Details - -internal struct FontConvertible { - internal let name: String - internal let family: String - internal let path: String - - #if os(macOS) - internal typealias Font = NSFont - #elseif os(iOS) || os(tvOS) || os(watchOS) - internal typealias Font = UIFont - #endif - - internal func font(size: CGFloat) -> Font { - guard let font = Font(font: self, size: size) else { - fatalError("Unable to initialize font '\(name)' (\(family))") - } - return font - } - - #if canImport(SwiftUI) - @available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) - internal func swiftUIFont(size: CGFloat) -> SwiftUI.Font { - return SwiftUI.Font.custom(self, size: size) - } - - @available(iOS 14.0, tvOS 14.0, watchOS 7.0, macOS 11.0, *) - internal func swiftUIFont(fixedSize: CGFloat) -> SwiftUI.Font { - return SwiftUI.Font.custom(self, fixedSize: fixedSize) - } - - @available(iOS 14.0, tvOS 14.0, watchOS 7.0, macOS 11.0, *) - internal func swiftUIFont(size: CGFloat, relativeTo textStyle: SwiftUI.Font.TextStyle) -> SwiftUI.Font { - return SwiftUI.Font.custom(self, size: size, relativeTo: textStyle) - } - #endif - - internal func register() { - // swiftlint:disable:next conditional_returns_on_newline - guard let url = url else { return } - CTFontManagerRegisterFontsForURL(url as CFURL, .process, nil) - } - - fileprivate func registerIfNeeded() { - #if os(iOS) || os(tvOS) || os(watchOS) - if !UIFont.fontNames(forFamilyName: family).contains(name) { - register() - } - #elseif os(macOS) - if let url = url, CTFontManagerGetScopeForURL(url as CFURL) == .none { - register() - } - #endif - } - - fileprivate var url: URL? { - // swiftlint:disable:next implicit_return - return BundleLocator.bundle.url(forResource: path, withExtension: nil) - } -} - -internal extension FontConvertible.Font { - convenience init?(font: FontConvertible, size: CGFloat) { - font.registerIfNeeded() - self.init(name: font.name, size: size) - } -} - -#if canImport(SwiftUI) -@available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) -internal extension SwiftUI.Font { - static func custom(_ font: FontConvertible, size: CGFloat) -> SwiftUI.Font { - font.registerIfNeeded() - return custom(font.name, size: size) - } -} - -@available(iOS 14.0, tvOS 14.0, watchOS 7.0, macOS 11.0, *) -internal extension SwiftUI.Font { - static func custom(_ font: FontConvertible, fixedSize: CGFloat) -> SwiftUI.Font { - font.registerIfNeeded() - return custom(font.name, fixedSize: fixedSize) - } - - static func custom( - _ font: FontConvertible, - size: CGFloat, - relativeTo textStyle: SwiftUI.Font.TextStyle - ) -> SwiftUI.Font { - font.registerIfNeeded() - return custom(font.name, size: size, relativeTo: textStyle) - } -} -#endif diff --git a/Sources/ProcessOut/Sources/Generated/Sourcery+Generated.swift b/Sources/ProcessOut/Sources/Generated/Sourcery+Generated.swift index efd61055b..aba2ed6d7 100644 --- a/Sources/ProcessOut/Sources/Generated/Sourcery+Generated.swift +++ b/Sources/ProcessOut/Sources/Generated/Sourcery+Generated.swift @@ -6,6 +6,24 @@ import UIKit // MARK: - AutoCodingKeys +extension DeviceMetadata { + + enum CodingKeys: String, CodingKey { + case appLanguage + case appScreenWidth + case appScreenHeight + case appTimeZoneOffset + case channel + } +} + +extension NativeAlternativePaymentCaptureRequest { + + enum CodingKeys: String, CodingKey { + case source + } +} + extension POAssignCustomerTokenRequest { enum CodingKeys: String, CodingKey { @@ -13,9 +31,26 @@ extension POAssignCustomerTokenRequest { case preferredScheme case verify case invoiceId - case enableThreeDS2 = "enable_three_d_s_2" case thirdPartySdkVersion case metadata + case enableThreeDS2 = "enable_three_d_s_2" + } +} + +extension POCardUpdateRequest { + + enum CodingKeys: String, CodingKey { + case cvc + case preferredScheme + } +} + +extension POCreateCustomerTokenRequest { + + enum CodingKeys: String, CodingKey { + case verify + case returnUrl + case invoiceReturnUrl } } @@ -58,7 +93,6 @@ extension POInvoiceAuthorizationRequest { case source case saveSource case incremental - case enableThreeDS2 = "enable_three_d_s_2" case preferredScheme case thirdPartySdkVersion case invoiceDetailIds @@ -70,6 +104,7 @@ extension POInvoiceAuthorizationRequest { case allowFallbackToSale case clientSecret case metadata + case enableThreeDS2 = "enable_three_d_s_2" } } @@ -84,7 +119,7 @@ extension POCardsService { @discardableResult public func issuerInformation( iin: String, - completion: @escaping (Result) -> Void + completion: @escaping @Sendable (Result) -> Void ) -> POCancellable { invoke(completion: completion) { try await issuerInformation(iin: iin) @@ -98,7 +133,7 @@ extension POCardsService { @discardableResult public func tokenize( request: POCardTokenizationRequest, - completion: @escaping (Result) -> Void + completion: @escaping @Sendable (Result) -> Void ) -> POCancellable { invoke(completion: completion) { try await tokenize(request: request) @@ -109,7 +144,7 @@ extension POCardsService { @discardableResult public func updateCard( request: POCardUpdateRequest, - completion: @escaping (Result) -> Void + completion: @escaping @Sendable (Result) -> Void ) -> POCancellable { invoke(completion: completion) { try await updateCard(request: request) @@ -120,7 +155,7 @@ extension POCardsService { @discardableResult public func tokenize( request: POApplePayPaymentTokenizationRequest, - completion: @escaping (Result) -> Void + completion: @escaping @Sendable (Result) -> Void ) -> POCancellable { invoke(completion: completion) { try await tokenize(request: request) @@ -132,7 +167,7 @@ extension POCardsService { public func tokenize( request: POApplePayTokenizationRequest, delegate: POApplePayTokenizationDelegate?, - completion: @escaping (Result) -> Void + completion: @escaping @Sendable (Result) -> Void ) -> POCancellable { invoke(completion: completion) { try await tokenize(request: request, delegate: delegate) @@ -143,7 +178,7 @@ extension POCardsService { @discardableResult public func tokenize( request: POApplePayTokenizationRequest, - completion: @escaping (Result) -> Void + completion: @escaping @Sendable (Result) -> Void ) -> POCancellable { invoke(completion: completion) { try await tokenize(request: request) @@ -158,7 +193,7 @@ extension POCustomerTokensService { public func assignCustomerToken( request: POAssignCustomerTokenRequest, threeDSService: PO3DSService, - completion: @escaping (Result) -> Void + completion: @escaping @Sendable (Result) -> Void ) -> POCancellable { invoke(completion: completion) { try await assignCustomerToken(request: request, threeDSService: threeDSService) @@ -170,7 +205,7 @@ extension POCustomerTokensService { @discardableResult public func createCustomerToken( request: POCreateCustomerTokenRequest, - completion: @escaping (Result) -> Void + completion: @escaping @Sendable (Result) -> Void ) -> POCancellable { invoke(completion: completion) { try await createCustomerToken(request: request) @@ -184,7 +219,7 @@ extension POGatewayConfigurationsRepository { @discardableResult public func all( request: POAllGatewayConfigurationsRequest, - completion: @escaping (Result) -> Void + completion: @escaping @Sendable (Result) -> Void ) -> POCancellable { invoke(completion: completion) { try await all(request: request) @@ -195,7 +230,7 @@ extension POGatewayConfigurationsRepository { @discardableResult public func find( request: POFindGatewayConfigurationRequest, - completion: @escaping (Result) -> Void + completion: @escaping @Sendable (Result) -> Void ) -> POCancellable { invoke(completion: completion) { try await find(request: request) @@ -205,7 +240,7 @@ extension POGatewayConfigurationsRepository { /// Returns available gateway configurations. @discardableResult public func all( - completion: @escaping (Result) -> Void + completion: @escaping @Sendable (Result) -> Void ) -> POCancellable { invoke(completion: completion) { try await all() @@ -220,7 +255,7 @@ extension POImagesRepository { public func images( at urls: [URL], scale: CGFloat, - completion: @escaping ([URL: UIImage]) -> Void + completion: @escaping @Sendable ([URL: UIImage]) -> Void ) -> POCancellable { invoke(completion: completion) { await images(at: urls, scale: scale) @@ -231,7 +266,7 @@ extension POImagesRepository { @discardableResult public func images( at urls: [URL], - completion: @escaping ([URL: UIImage]) -> Void + completion: @escaping @Sendable ([URL: UIImage]) -> Void ) -> POCancellable { invoke(completion: completion) { await images(at: urls) @@ -243,7 +278,7 @@ extension POImagesRepository { public func image( at url: URL?, scale: CGFloat = 1, - completion: @escaping (UIImage?) -> Void + completion: @escaping @Sendable (UIImage?) -> Void ) -> POCancellable { invoke(completion: completion) { await image(at: url, scale: scale) @@ -256,7 +291,7 @@ extension POImagesRepository { at url1: URL?, _ url2: URL?, scale: CGFloat = 1, - completion: @escaping ((UIImage?, UIImage?)) -> Void + completion: @escaping @Sendable ((UIImage?, UIImage?)) -> Void ) -> POCancellable { invoke(completion: completion) { await images(at: url1, url2, scale: scale) @@ -268,7 +303,7 @@ extension POImagesRepository { @discardableResult public func image( resource: POImageRemoteResource, - completion: @escaping (UIImage?) -> Void + completion: @escaping @Sendable (UIImage?) -> Void ) -> POCancellable { invoke(completion: completion) { await image(resource: resource) @@ -282,7 +317,7 @@ extension POInvoicesService { @discardableResult public func nativeAlternativePaymentMethodTransactionDetails( request: PONativeAlternativePaymentMethodTransactionDetailsRequest, - completion: @escaping (Result) -> Void + completion: @escaping @Sendable (Result) -> Void ) -> POCancellable { invoke(completion: completion) { try await nativeAlternativePaymentMethodTransactionDetails(request: request) @@ -296,7 +331,7 @@ extension POInvoicesService { @discardableResult public func initiatePayment( request: PONativeAlternativePaymentMethodRequest, - completion: @escaping (Result) -> Void + completion: @escaping @Sendable (Result) -> Void ) -> POCancellable { invoke(completion: completion) { try await initiatePayment(request: request) @@ -307,7 +342,7 @@ extension POInvoicesService { @discardableResult public func invoice( request: POInvoiceRequest, - completion: @escaping (Result) -> Void + completion: @escaping @Sendable (Result) -> Void ) -> POCancellable { invoke(completion: completion) { try await invoice(request: request) @@ -319,7 +354,7 @@ extension POInvoicesService { public func authorizeInvoice( request: POInvoiceAuthorizationRequest, threeDSService: PO3DSService, - completion: @escaping (Result) -> Void + completion: @escaping @Sendable (Result) -> Void ) -> POCancellable { invoke(completion: completion) { try await authorizeInvoice(request: request, threeDSService: threeDSService) @@ -330,7 +365,7 @@ extension POInvoicesService { @discardableResult public func captureNativeAlternativePayment( request: PONativeAlternativePaymentCaptureRequest, - completion: @escaping (Result) -> Void + completion: @escaping @Sendable (Result) -> Void ) -> POCancellable { invoke(completion: completion) { try await captureNativeAlternativePayment(request: request) @@ -342,7 +377,7 @@ extension POInvoicesService { @discardableResult public func createInvoice( request: POInvoiceCreationRequest, - completion: @escaping (Result) -> Void + completion: @escaping @Sendable (Result) -> Void ) -> POCancellable { invoke(completion: completion) { try await createInvoice(request: request) @@ -351,9 +386,9 @@ extension POInvoicesService { } /// Invokes given completion with a result of async operation. -private func invoke( - completion: @escaping (Result) -> Void, - after operation: @escaping () async throws -> T +private func invoke( + completion: @escaping @Sendable (Result) -> Void, + after operation: @escaping @Sendable () async throws -> T ) -> POCancellable { Task { @MainActor in do { @@ -369,7 +404,10 @@ private func invoke( } /// Invokes given completion with a result of async operation. -private func invoke(completion: @escaping (T) -> Void, after operation: @escaping () async -> T) -> Task { +private func invoke( + completion: @escaping @Sendable (T) -> Void, + after operation: @escaping @Sendable () async -> T +) -> Task { Task { @MainActor in completion(await operation()) } diff --git a/Sources/ProcessOut/Sources/Legacy/APMTokenReturn.swift b/Sources/ProcessOut/Sources/Legacy/APMTokenReturn.swift deleted file mode 100644 index e288e99be..000000000 --- a/Sources/ProcessOut/Sources/Legacy/APMTokenReturn.swift +++ /dev/null @@ -1,40 +0,0 @@ -// -// APMTokenReturn.swift -// ProcessOut -// -// Created by Jeremy Lejoux on 31/10/2019. -// - -import Foundation - -@available(*, deprecated, message: "Use ProcessOut.shared.customerTokens.assignCustomerToken() instead.") -public final class APMTokenReturn { - - public enum APMReturnType { - case Authorization - case CreateToken - } - - public var token: String? - public var customerId: String? - public var tokenId: String? - public var returnType: APMReturnType - public var error: ProcessOutException? - - public init(token: String) { - self.token = token - self.returnType = .Authorization - } - - public init(token: String, customerId: String, tokenId: String) { - self.token = token - self.customerId = customerId - self.tokenId = tokenId - self.returnType = .CreateToken - } - - public init(error: ProcessOutException) { - self.error = error - self.returnType = .Authorization - } -} diff --git a/Sources/ProcessOut/Sources/Legacy/ApiResponse.swift b/Sources/ProcessOut/Sources/Legacy/ApiResponse.swift deleted file mode 100644 index 1552e0817..000000000 --- a/Sources/ProcessOut/Sources/Legacy/ApiResponse.swift +++ /dev/null @@ -1,19 +0,0 @@ -// -// ApiResponse.swift -// ProcessOut -// -// Created by Jeremy Lejoux on 18/06/2019. -// - -class ApiResponse: Decodable { - - var success: Bool - var message: String? - var errorType: String? - - private enum CodingKeys: String, CodingKey { - case success = "success" - case message = "message" - case errorType = "error_type" - } -} diff --git a/Sources/ProcessOut/Sources/Legacy/AuthentificationChallengeData.swift b/Sources/ProcessOut/Sources/Legacy/AuthentificationChallengeData.swift deleted file mode 100644 index 7a89be610..000000000 --- a/Sources/ProcessOut/Sources/Legacy/AuthentificationChallengeData.swift +++ /dev/null @@ -1,29 +0,0 @@ -// -// AuthentificationChallengeData.swift -// ProcessOut -// -// Created by Jeremy Lejoux on 18/06/2019. -// - -@available(*, deprecated, message: "Use PO3DSService instead.") -public final class AuthentificationChallengeData: Codable { - - public var acsTransID: String - public var acsReferenceNumber: String - public var acsSignedContent: String - public var threeDSServerTransID: String - - init(acsTransID: String, acsReferenceNumber: String, acsSignedContent: String, threeDSServerTransID: String) { - self.acsTransID = acsTransID - self.acsReferenceNumber = acsReferenceNumber - self.acsSignedContent = acsSignedContent - self.threeDSServerTransID = threeDSServerTransID - } - - private enum CodingKeys: String, CodingKey { - case acsTransID = "acsTransID" - case acsReferenceNumber = "acsReferenceNumber" - case acsSignedContent = "acsSignedContent" - case threeDSServerTransID = "threeDSServerTransID" - } -} diff --git a/Sources/ProcessOut/Sources/Legacy/AuthorizationRequest.swift b/Sources/ProcessOut/Sources/Legacy/AuthorizationRequest.swift deleted file mode 100644 index 42cc5e4eb..000000000 --- a/Sources/ProcessOut/Sources/Legacy/AuthorizationRequest.swift +++ /dev/null @@ -1,33 +0,0 @@ -// -// AuthorizationRequest.swift -// ProcessOut -// -// Created by Jeremy Lejoux on 17/06/2019. -// - -@available(*, deprecated, message: "Use ProcessOut.shared.invoices.authorizeInvoice() instead.") -public final class AuthorizationRequest: Codable { - - var invoiceID: String = "" - var source: String = "" - var incremental: Bool = false - var threeDS2Enabled: Bool = true - - public var thirdPartySDKVersion: String = "" - public var preferredScheme: String = "" - - public init(source: String, incremental: Bool, invoiceID: String) { - self.source = source - self.incremental = incremental - self.invoiceID = invoiceID - } - - private enum CodingKeys: String, CodingKey { - case invoiceID = "invoice_id" - case source = "source" - case thirdPartySDKVersion = "third_party_sdk_version" - case incremental = "incremental" - case threeDS2Enabled = "enable_three_d_s_2" - case preferredScheme = "preferred_scheme" - } -} diff --git a/Sources/ProcessOut/Sources/Legacy/AuthorizationResult.swift b/Sources/ProcessOut/Sources/Legacy/AuthorizationResult.swift deleted file mode 100644 index d5f0af48d..000000000 --- a/Sources/ProcessOut/Sources/Legacy/AuthorizationResult.swift +++ /dev/null @@ -1,22 +0,0 @@ -// -// AuthorizationResult.swift -// ProcessOut -// -// Created by Jeremy Lejoux on 17/06/2019. -// - -@available(*, deprecated) -final class AuthorizationResult: ApiResponse { - - var customerAction: CustomerAction? - - required init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - self.customerAction = try container.decodeIfPresent(CustomerAction.self, forKey: .customerAction) - try super.init(from: decoder) - } - - private enum CodingKeys: String, CodingKey { - case customerAction = "customer_action" - } -} diff --git a/Sources/ProcessOut/Sources/Legacy/CardPaymentWebView.swift b/Sources/ProcessOut/Sources/Legacy/CardPaymentWebView.swift deleted file mode 100644 index 5ebf309a6..000000000 --- a/Sources/ProcessOut/Sources/Legacy/CardPaymentWebView.swift +++ /dev/null @@ -1,20 +0,0 @@ -// -// CardPaymentWebView.swift -// ProcessOut -// -// Created by Jeremy Lejoux on 30/09/2019. -// - -import Foundation - -@available(*, deprecated) -final class CardPaymentWebView: ProcessOutWebView { - - override func onRedirect(url: URL) { - guard let parameters = url.queryParameters, let token = parameters["token"] else { - return - } - - onResult(token) - } -} diff --git a/Sources/ProcessOut/Sources/Legacy/CardTokenWebView.swift b/Sources/ProcessOut/Sources/Legacy/CardTokenWebView.swift deleted file mode 100644 index 7e3c1b805..000000000 --- a/Sources/ProcessOut/Sources/Legacy/CardTokenWebView.swift +++ /dev/null @@ -1,20 +0,0 @@ -// -// CardTokenWebView.swift -// ProcessOut -// -// Created by Jeremy Lejoux on 30/09/2019. -// - -import Foundation - -@available(*, deprecated) -final class CardTokenWebView: ProcessOutWebView { - - override func onRedirect(url: URL) { - guard let parameters = url.queryParameters, let token = parameters["token"] else { - return - } - - onResult(token) - } -} diff --git a/Sources/ProcessOut/Sources/Legacy/CustomerAction.swift b/Sources/ProcessOut/Sources/Legacy/CustomerAction.swift deleted file mode 100644 index eeee17646..000000000 --- a/Sources/ProcessOut/Sources/Legacy/CustomerAction.swift +++ /dev/null @@ -1,28 +0,0 @@ -// -// CustomerAction.swift -// ProcessOut -// -// Created by Jeremy Lejoux on 19/09/2019. -// - -import Foundation - -@available(*, deprecated, message: "Use PO3DSService instead.") -public final class CustomerAction: Codable { - - public enum CustomerActionType: String, Codable { - case fingerPrintMobile = "fingerprint-mobile" - case challengeMobile = "challenge-mobile" - case url = "url" - case redirect = "redirect" - case fingerprint = "fingerprint" - } - - public var type: CustomerActionType = CustomerActionType.redirect - public var value: String = "" - - private enum CodingKeys: String, CodingKey { - case type = "type" - case value = "value" - } -} diff --git a/Sources/ProcessOut/Sources/Legacy/CustomerActionHandler.swift b/Sources/ProcessOut/Sources/Legacy/CustomerActionHandler.swift deleted file mode 100644 index 3cc5de856..000000000 --- a/Sources/ProcessOut/Sources/Legacy/CustomerActionHandler.swift +++ /dev/null @@ -1,159 +0,0 @@ -// -// CustomerActionHandler.swift -// ProcessOut -// -// Created by Jeremy Lejoux on 19/09/2019. -// - -import Foundation -import UIKit -import WebKit - -@available(*, deprecated) -final class CustomerActionHandler { - - var handler: ThreeDSHandler - var with: UIViewController - var processOutWebView: ProcessOutWebView - - init(handler: ThreeDSHandler, processOutWebView: ProcessOutWebView, with: UIViewController) { - self.handler = handler - self.with = with - self.processOutWebView = processOutWebView - } - - - /// Handle a customer action request for an authorization - /// - /// - Parameters: - /// - customerAction: the customerAction returned by the auth request - /// - completion: completion callback - func handleCustomerAction(customerAction: CustomerAction, completion: @escaping (String) -> Void) { - switch customerAction.type{ - // 3DS2 fingerprint request - case .fingerPrintMobile: - performFingerprint(customerAction: customerAction, handler: handler, completion: { (encodedData, error) in - if encodedData != nil { - completion(encodedData!) - return - } - self.handler.onError(error: error!) - }) - // 3DS2 challenge request - case .challengeMobile: - performChallenge(customerAction: customerAction, handler: handler) { (success, error) in - guard success else { - completion(ProcessOutLegacyApi.threeDS2ChallengeError) - return - } - completion(ProcessOutLegacyApi.threeDS2ChallengeSuccess) - } - // 3DS1 web fallback - case .url, .redirect: - guard let url = URL(string: customerAction.value) else { - // Invalid URL - handler.onError(error: ProcessOutException.InternalError) - return - } - - // Loading the url - let request = URLRequest(url: url) - guard let _ = processOutWebView.load(request) else { - handler.onError(error: ProcessOutException.InternalError) - return - } - - // Displaying the webview - handler.doPresentWebView(webView: processOutWebView) - - break - - case .fingerprint: - // Need to open a webview for fingerprinting fallback - - guard let url = URL(string: customerAction.value) else { - // Invalid URL - handler.onError(error: ProcessOutException.InternalError) - return - } - - // Prepare the fingerprint hiddenWebview - let fingerprintWebView = FingerprintWebView(customerAction: customerAction, frame: with.view.frame, onResult: { (newSource) in - completion(newSource) - }) { - self.handler.onError(error: ProcessOutException.BadRequest(errorMessage: "Web authentication failed", errorCode: "")) - } - - // Loading the url - let request = URLRequest(url: url) - guard let _ = fingerprintWebView.load(request) else { - handler.onError(error: ProcessOutException.InternalError) - return - } - - // We force the webView display - with.view.addSubview(fingerprintWebView) - break - } - } - - private func performFingerprint(customerAction: CustomerAction, handler: ThreeDSHandler, completion: @escaping (String?, ProcessOutException?) -> Void) { - do { - var encodedData = customerAction.value - let remainder = encodedData.count % 4 - if remainder > 0 { - encodedData = encodedData.padding(toLength: encodedData.count + 4 - remainder, withPad: "=", startingAt: 0) - } - - guard let decodedData = Data(base64Encoded: encodedData) else { - completion(nil, ProcessOutException.InternalError) - return - } - var directoryServerData: DirectoryServerData - - directoryServerData = try JSONDecoder().decode(DirectoryServerData.self, from: decodedData) - handler.doFingerprint(directoryServerData: directoryServerData) { (response) in - do { - guard let body = String(data: try JSONEncoder().encode(response), encoding: .utf8) else { - completion(nil, ProcessOutException.InternalError) - return - } - - let miscGatewayRequest = MiscGatewayRequest(fingerprintResponse: body) - guard let gatewayToken = miscGatewayRequest.generateToken() else { - completion(nil, ProcessOutException.InternalError) - return - } - - completion(gatewayToken, nil) - } catch { - completion(nil, ProcessOutException.InternalError) - } - } - } catch { - completion(nil, ProcessOutException.InternalError) - return - } - } - - private func performChallenge(customerAction: CustomerAction, handler: ThreeDSHandler, completion: @escaping (Bool, ProcessOutException?) -> Void) { - do { - var encodedData = customerAction.value - let remainder = encodedData.count % 4 - if remainder > 0 { - encodedData = encodedData.padding(toLength: encodedData.count + 4 - remainder, withPad: "=", startingAt: 0) - } - - guard let decodedB64Data = Data(base64Encoded: encodedData) else { - completion(false, ProcessOutException.InternalError) - return - } - let authentificationChallengeData = try JSONDecoder().decode(AuthentificationChallengeData.self, from: decodedB64Data) - handler.doChallenge(authentificationData: authentificationChallengeData) { (success) in - completion(success, nil) - } - } catch { - completion(false, ProcessOutException.InternalError) - } - } -} diff --git a/Sources/ProcessOut/Sources/Legacy/DirectoryServerData.swift b/Sources/ProcessOut/Sources/Legacy/DirectoryServerData.swift deleted file mode 100644 index 428ce048e..000000000 --- a/Sources/ProcessOut/Sources/Legacy/DirectoryServerData.swift +++ /dev/null @@ -1,23 +0,0 @@ -// -// DirectoryServerData.swift -// ProcessOut -// -// Created by Jeremy Lejoux on 18/06/2019. -// - -/// Object passed to the doFingerprint function -@available(*, deprecated, message: "Use PO3DSService instead.") -public final class DirectoryServerData: Codable { - - public var directoryServerID: String = "" - public var directoryServerPublicKey: String = "" - public var threeDSServerTransactionID: String = "" - public var messageVersion: String = "" - - private enum CodingKeys: String, CodingKey { - case directoryServerID = "directoryServerID" - case directoryServerPublicKey = "directoryServerPublicKey" - case threeDSServerTransactionID = "threeDSServerTransID" - case messageVersion = "messageVersion" - } -} diff --git a/Sources/ProcessOut/Sources/Legacy/FingerPrintWebViewSchemeHandler.swift b/Sources/ProcessOut/Sources/Legacy/FingerPrintWebViewSchemeHandler.swift deleted file mode 100644 index f4dc477c4..000000000 --- a/Sources/ProcessOut/Sources/Legacy/FingerPrintWebViewSchemeHandler.swift +++ /dev/null @@ -1,47 +0,0 @@ -// -// FingerPrintWebViewSchemeHandler.swift -// -// Created by Jeremy Lejoux on 27/08/2019. -// - -import Foundation -import WebKit - -@available(*, deprecated) -public final class FingerPrintWebViewSchemeHandler: NSObject, WKURLSchemeHandler { - - private var completion: ((String?, String?, ProcessOutException?) -> Void)? = nil - - public init(completion: @escaping (String?, String?, ProcessOutException?) -> Void) { - self.completion = completion - } - - public func webView(_ webView: WKWebView, start urlSchemeTask: WKURLSchemeTask) { - DispatchQueue.global().async { - if let url = urlSchemeTask.request.url { - var invoice = "" - var token = "" - if let queryItems = URLComponents(url: url, resolvingAgainstBaseURL: true)?.queryItems { - for queryParams in queryItems { - if queryParams.name == "invoice_id", let value = queryParams.value { - invoice = value - } else if queryParams.name == "token", let value = queryParams.value { - token = value - } - } - } - - if invoice == "" || token == "" { - self.completion!(nil, nil, ProcessOutException.InternalError) - return - } - - self.completion!(invoice, token, nil) - } - } - } - - public func webView(_ webView: WKWebView, stop urlSchemeTask: WKURLSchemeTask) { - // Nothing needed here - } -} diff --git a/Sources/ProcessOut/Sources/Legacy/FingerprintWebView.swift b/Sources/ProcessOut/Sources/Legacy/FingerprintWebView.swift deleted file mode 100644 index a23073535..000000000 --- a/Sources/ProcessOut/Sources/Legacy/FingerprintWebView.swift +++ /dev/null @@ -1,61 +0,0 @@ -// -// FingerprintWebView.swift -// ProcessOut -// -// Created by Jeremy Lejoux on 30/09/2019. -// - -import Foundation - -@available(*, deprecated) -final class FingerprintWebView: ProcessOutWebView { - - private var timeOutTimer: Timer? - private let customerAction: CustomerAction - - public init(customerAction: CustomerAction, frame: CGRect, onResult: @escaping (String) -> Void, onAuthenticationError: @escaping () -> Void) { - self.customerAction = customerAction - super.init(frame: frame, onResult: onResult, onAuthenticationError: onAuthenticationError) - self.isHidden = false - - // Start the timeout handler with a 10s timeout - timeOutTimer = Timer.scheduledTimer(timeInterval: 10, target: self, selector: #selector(timeOutTimerDidFire), userInfo: nil, repeats: false) - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - private func generateFallbackFingerprintToken(URL: String) -> (fallbackToken: String?, error: ProcessOutException?) { - let miscGatewayRequest = MiscGatewayRequest(fingerprintResponse: "{\"threeDS2FingerprintTimeout\":true}") - miscGatewayRequest.headers = ["Content-Type": "application/json"] - miscGatewayRequest.url = URL - - guard let gatewayToken = miscGatewayRequest.generateToken() else { - return (nil, ProcessOutException.InternalError) - } - - return (gatewayToken, nil) - } - - override func onRedirect(url: URL) { - guard let parameters = url.queryParameters, let token = parameters["token"] else { - return - } - timeOutTimer?.invalidate() - onResult(token) - } - - @objc private func timeOutTimerDidFire() { - // Remove the webview - self.removeFromSuperview() - - // Fallback to default fingerprint values - let fallback = self.generateFallbackFingerprintToken(URL: customerAction.value) - guard fallback.error == nil else { - onAuthenticationError() - return - } - onResult(fallback.fallbackToken!) - } -} diff --git a/Sources/ProcessOut/Sources/Legacy/GatewayConfiguration.swift b/Sources/ProcessOut/Sources/Legacy/GatewayConfiguration.swift deleted file mode 100644 index 0cd65b6e9..000000000 --- a/Sources/ProcessOut/Sources/Legacy/GatewayConfiguration.swift +++ /dev/null @@ -1,61 +0,0 @@ -// -// AlternativeGateway.swift -// ProcessOut -// -// Created by Jeremy Lejoux on 18/04/2019. -// - -import Foundation - -@available(*, deprecated, message: "Use ProcessOut.shared.gatewayConfigurations instead.") -public class GatewayConfiguration: Decodable { - - enum CodingKeys: String, CodingKey { - case id = "id" - case name = "name" - case gateway = "gateway" - case enabled = "enabled" - } - - public struct Gateway: Decodable { - - enum CodingKeys: String, CodingKey { - case displayName = "display_name" - case logoUrl = "logo_url" - case tags = "tags" - case name = "name" - } - - public var name: String? - public var displayName: String - public var logoUrl: String? - public var tags: [String] - } - - public var id: String - public var name: String? - public var enabled: Bool - public var gateway: Gateway? - - required init(id: String, name: String, enabled: Bool, gateway: Gateway) { - self.id = id - self.name = name - self.enabled = enabled - self.gateway = gateway - } -} - -@available(*, deprecated) -class GatewayConfigurationResult: ApiResponse { - var gatewayConfigurations: [GatewayConfiguration]? - - enum CodingKeys: String, CodingKey { - case gatewayConfigurations = "gateway_configurations" - } - - required init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - self.gatewayConfigurations = try container.decode([GatewayConfiguration].self, forKey: .gatewayConfigurations) - try super.init(from: decoder) - } -} diff --git a/Sources/ProcessOut/Sources/Legacy/IncrementAuthorizationRequest.swift b/Sources/ProcessOut/Sources/Legacy/IncrementAuthorizationRequest.swift deleted file mode 100644 index 8743b5abf..000000000 --- a/Sources/ProcessOut/Sources/Legacy/IncrementAuthorizationRequest.swift +++ /dev/null @@ -1,10 +0,0 @@ -// -// IncrementAuthorizationRequest.swift -// ProcessOut -// -// Created by Seb Preston on 11/08/2021. -// - -struct IncrementAuthorizationRequest: Codable { - var amount: Int -} diff --git a/Sources/ProcessOut/Sources/Legacy/MiscGatewayRequest.swift b/Sources/ProcessOut/Sources/Legacy/MiscGatewayRequest.swift deleted file mode 100644 index 45aed3b4d..000000000 --- a/Sources/ProcessOut/Sources/Legacy/MiscGatewayRequest.swift +++ /dev/null @@ -1,41 +0,0 @@ -// -// MiscGatewayRequest.swift -// ProcessOut -// -// Created by Jeremy Lejoux on 18/06/2019. -// - -import Foundation - -final class MiscGatewayRequest: Codable { - - var url: String = "" - var headers: [String:String] = [:] - var body: String = "" - - init(fingerprintResponse: String) { - self.body = fingerprintResponse - } - - /// Generate corresponding gateway token - /// - /// - Returns: The corresponding gateway token, nil if an error occured - func generateToken() -> String? { - do { - let encodedJson = try JSONEncoder().encode(self) - if let base64Encoded = String(data: encodedJson.base64EncodedData(), encoding: .utf8) { - return "gway_req_" + base64Encoded - } else { - return nil - } - } catch { - return nil - } - } - - private enum CodingKeys: String, CodingKey { - case url = "url" - case headers = "headers" - case body = "body" - } -} diff --git a/Sources/ProcessOut/Sources/Legacy/ProcessOutException.swift b/Sources/ProcessOut/Sources/Legacy/ProcessOutException.swift deleted file mode 100644 index 19d8d4598..000000000 --- a/Sources/ProcessOut/Sources/Legacy/ProcessOutException.swift +++ /dev/null @@ -1,15 +0,0 @@ -// -// ProcessOutException.swift -// ProcessOut -// -// Created by Jeremy Lejoux on 17/06/2019. -// - -@available(*, deprecated, message: "Use POFailure instead.") -public enum ProcessOutException: Error { - case NetworkError - case MissingProjectId - case BadRequest(errorMessage: String, errorCode: String) - case InternalError - case GenericError(error: Error) -} diff --git a/Sources/ProcessOut/Sources/Legacy/ProcessOutLegacyApi.swift b/Sources/ProcessOut/Sources/Legacy/ProcessOutLegacyApi.swift deleted file mode 100644 index 13389f874..000000000 --- a/Sources/ProcessOut/Sources/Legacy/ProcessOutLegacyApi.swift +++ /dev/null @@ -1,908 +0,0 @@ - -import Foundation -import PassKit -import UIKit -import WebKit - -enum HTTPMethod: String { - case options = "OPTIONS" - case get = "GET" - case head = "HEAD" - case post = "POST" - case put = "PUT" - case patch = "PATCH" - case delete = "DELETE" - case trace = "TRACE" - case connect = "CONNECT" -} - -@available(*, deprecated, message: "Use ProcessOut instead.") -public final class ProcessOutLegacyApi { - - public struct Contact { - var Address1: String? - var Address2: String? - var City: String? - var State: String? - var Zip: String? - var CountryCode: String? - - public init(address1: String?, address2: String?, city: String?, state: String?, zip: String?, countryCode: String?) { - self.Address1 = address1 - self.Address2 = address2 - self.City = city - self.State = state - self.Zip = zip - self.CountryCode = countryCode - } - } - - public struct Card { - var CardNumber: String - var ExpMonth: Int - var ExpYear: Int - var CVC: String? - var Name: String - var Contact: Contact? - - public init(cardNumber: String, expMonth: Int, expYear: Int, cvc: String?, name: String) { - self.CardNumber = cardNumber - self.ExpMonth = expMonth - self.ExpYear = expYear - self.CVC = cvc - self.Name = name - } - - public init(cardNumber: String, expMonth: Int, expYear: Int, cvc: String?, name: String, contact: Contact) { - self.CardNumber = cardNumber - self.ExpMonth = expMonth - self.ExpYear = expYear - self.CVC = cvc - self.Name = name - self.Contact = contact - } - } - - @available(*, deprecated, message: "Use POPaginationOptions instead.") - public struct PaginationOptions { - var StartAfter: String? - var EndBefore: String? - var Limit: Int? - var Order: String? - - public init(StartAfter: String? = nil, EndBefore: String? = nil, Limit: Int? = nil, Order: String? = nil) { - self.StartAfter = StartAfter - self.EndBefore = EndBefore - self.Limit = Limit - self.Order = Order - } - } - - private static var ApiUrl: String { - ProcessOut.shared.configuration.environment.apiBaseUrl.absoluteString - } - - internal static var CheckoutUrl: String { - ProcessOut.shared.configuration.environment.checkoutBaseUrl.absoluteString - } - - internal static var ProjectId: String { - ProcessOut.shared.configuration.projectId - } - - internal static let threeDS2ChallengeSuccess: String = "gway_req_eyJib2R5Ijoie1widHJhbnNTdGF0dXNcIjpcIllcIn0ifQ==" - internal static let threeDS2ChallengeError: String = "gway_req_eyJib2R5Ijoie1widHJhbnNTdGF0dXNcIjpcIk5cIn0ifQ==" - - internal static let requestManager = ProcessOutRequestManager(apiUrl: ApiUrl, apiVersion: type(of: ProcessOut.shared).version, defaultUserAgent: defaultUserAgent) - - // Getting the device user agent - private static let defaultUserAgent = "iOS/" + UIDevice.current.systemVersion - - /// Tokenizes a card with metadata - /// - /// - Parameters: - /// - card: The card object to be tokenized - /// - metadata: Optional metadata to apply to the card tokenization - /// - completion: Completion callback - public static func Tokenize(card: Card, metadata: [String: Any]?, completion: @escaping (String?, ProcessOutException?) -> Void) { - var parameters: [String: Any] = [:] - if let metadata = metadata { - parameters["metadata"] = metadata - } - parameters["name"] = card.Name - parameters["number"] = card.CardNumber - parameters["exp_month"] = card.ExpMonth - parameters["exp_year"] = card.ExpYear - - if let contact = card.Contact { - let contactParameters = [ - "address1": contact.Address1, - "address2": contact.Address2, - "city": contact.City, - "state": contact.State, - "zip": contact.Zip, - "country_code": contact.CountryCode - ] - parameters["contact"] = contactParameters - - } - - if let cvc = card.CVC { - parameters["cvc2"] = cvc - } - - HttpRequest(route: "/cards", method: .post, parameters: parameters) { (tokenResponse, error) in - do { - guard error == nil else { - completion(nil, error) - return - } - - let tokenizationResult = try JSONDecoder().decode(TokenizationResult.self, from: tokenResponse!) - if let card = tokenizationResult.card, tokenizationResult.success { - completion(card.id, nil) - } else { - guard let message = tokenizationResult.message, let errorType = tokenizationResult.errorType else { - completion(nil, ProcessOutException.InternalError) - return - } - completion(nil, ProcessOutException.BadRequest(errorMessage: message, errorCode: errorType)) - } - } catch { - completion(nil, ProcessOutException.InternalError) - } - } - } - - - /// ApplePay tokenization - /// - /// - Parameters: - /// - payment: PKPayment object to be tokenize - /// - metadata: Optional metadata - /// - completion: Completion callback - public static func Tokenize(payment: PKPayment, metadata: [String: Any]?, completion: @escaping (String?, ProcessOutException?) -> Void) { - return Tokenize(payment: payment, metadata: metadata, contact: nil, completion: completion) - } - - - /// ApplePay tokenization with contact information - /// - /// - Parameters: - /// - payment: PKPayment object to be tokenize - /// - metadata: Optional metadata - /// - contact: Customer contact information - /// - completion: Completion callback - public static func Tokenize(payment: PKPayment, metadata: [String: Any]?, contact: Contact?, completion: @escaping (String?, ProcessOutException?) -> Void) { - - var parameters: [String: Any] = [:] - if let metadata = metadata { - parameters["metadata"] = metadata - } - - do { - // Serializing the paymentdata object - let paymentDataJson: [String: AnyObject]? = try JSONSerialization.jsonObject(with: payment.token.paymentData, options: []) as? [String: AnyObject] - - var applepayResponse: [String: Any] = [:] - var token: [String: Any] = [:] - - // Retrieving additional information - var paymentMethodType: String - switch payment.token.paymentMethod.type { - case .debit: - paymentMethodType = "debit" - break - case .credit: - paymentMethodType = "credit" - break - case .prepaid: - paymentMethodType = "prepaid" - break - case .store: - paymentMethodType = "store" - break - default: - paymentMethodType = "unknown" - break - } - let paymentMethod: [String: Any] = [ - "displayName":payment.token.paymentMethod.displayName ?? "", - "network": payment.token.paymentMethod.network?.rawValue ?? "", - "type": paymentMethodType - ] - token["paymentMethod"] = paymentMethod - - token["transactionIdentifier"] = payment.token.transactionIdentifier - token["paymentData"] = paymentDataJson - applepayResponse["token"] = token - parameters["applepay_response"] = applepayResponse - parameters["token_type"] = "applepay" - - if contact != nil { - let contactParameters = [ - "address1": contact?.Address1, - "address2": contact?.Address2, - "city": contact?.City, - "state": contact?.State, - "zip": contact?.Zip, - "country_code": contact?.CountryCode - ] - parameters["contact"] = contactParameters - } - - HttpRequest(route: "/cards", method: .post, parameters: parameters) { (tokenResponse, error) in - do { - guard let tokenResponse = tokenResponse else { - throw ProcessOutException.InternalError - } - let tokenizationResult = try JSONDecoder().decode(TokenizationResult.self, from: tokenResponse) - if let card = tokenizationResult.card, tokenizationResult.success { - completion(card.id, nil) - } else { - if let message = tokenizationResult.message, let errorType = tokenizationResult.errorType { - completion(nil, ProcessOutException.BadRequest(errorMessage: message, errorCode: errorType)) - } else { - completion(nil, ProcessOutException.InternalError) - } - } - } catch { - completion(nil, ProcessOutException.InternalError) - } - } - } catch { - // Could not parse the PKPaymentData object - completion(nil, ProcessOutException.GenericError(error: error)) - - } - } - - - /// Update a previously stored card CVC - /// - /// - Parameters: - /// - cardId: Card ID to be updated - /// - newCvc: New CVC - /// - completion: Completion callback - public static func UpdateCvc(cardId: String, newCvc: String, completion: @escaping (ProcessOutException?) -> Void) { - let parameters: [String: Any] = [ - "cvc": newCvc - ] - - HttpRequest(route: "/cards/" + cardId, method: .put, parameters: parameters) { (response, error) in - completion(error) - } - } - - @available(*, deprecated, message: "Use POAllGatewayConfigurationsRequest.Filter instead.") - public enum GatewayConfigurationsFilter: String { - case All = "" - case AlternativePaymentMethods = "alternative-payment-methods" - case AlternativePaymentMethodWithTokenization = " alternative-payment-methods-with-tokenization" - } - - /// List alternative gateway configurations activated on your account - /// - /// - Parameters: - /// - completion: Completion callback - /// - paginationOptions: Pagination options to use - @available(*, deprecated, message: "Use ProcessOut.shared.gatewayConfigurations.all instead.") - public static func fetchGatewayConfigurations(filter: GatewayConfigurationsFilter, completion: @escaping ([GatewayConfiguration]?, ProcessOutException?) -> Void, paginationOptions: PaginationOptions? = nil) { - let paginationParams = paginationOptions != nil ? "&" + generatePaginationParamsString(paginationOptions: paginationOptions!) : "" - - HttpRequest(route: "/gateway-configurations?filter=" + filter.rawValue + "&expand_merchant_accounts=true" + paginationParams, method: .get, parameters: nil) { (gateways - , e) in - guard gateways != nil else { - completion(nil, e) - return - } - - var result: GatewayConfigurationResult - do { - result = try JSONDecoder().decode(GatewayConfigurationResult.self, from: gateways!) - } catch { - completion(nil, ProcessOutException.GenericError(error: error)) - return - } - - if let gConfs = result.gatewayConfigurations { - completion(gConfs, nil) - return - } - completion(nil, ProcessOutException.InternalError) - } - } - - /// Initiate a payment authorization from a previously generated invoice and card token - /// - /// - Parameters: - /// - invoiceId: Invoice generated on your backend - /// - token: Card token to be used for the charge - /// - thirdPartySDKVersion: Version of the 3rd party SDK being used for the calls. Can be blank if unused - /// - handler: Custom 3DS2 handler (please refer to our documentation for this) - /// - with: UIViewController to display webviews and perform fingerprinting - public static func makeCardPayment(invoiceId: String, token: String, thirdPartySDKVersion: String, handler: ThreeDSHandler, with: UIViewController) { - let authRequest = AuthorizationRequest(source: token, incremental: false, invoiceID: invoiceId) - authRequest.thirdPartySDKVersion = thirdPartySDKVersion - guard let body = try? JSONEncoder().encode(authRequest) else { - handler.onError(error: ProcessOutException.InternalError) - return - } - - guard let json = try? JSONSerialization.jsonObject(with: body, options: []) as? [String : Any] else { - handler.onError(error: ProcessOutException.InternalError) - return - } - makeAuthorizationRequest(invoiceId: invoiceId, thirdPartySDKVersion: thirdPartySDKVersion, json: json, handler: handler, with: with, actionHandlerCompletion: { (newSource) in - makeCardPayment(invoiceId: invoiceId, token: newSource, thirdPartySDKVersion: thirdPartySDKVersion, handler: handler, with: with) - }) - } - - /// Initiate a payment authorization from a previously generated invoice and card token - /// - /// - Parameters: - /// - invoiceId: Invoice generated on your backend - /// - token: Card token to be used for the charge - /// - handler: Custom 3DS2 handler (please refer to our documentation for this) - /// - with: UIViewController to display webviews and perform fingerprinting - public static func makeCardPayment(invoiceId: String, token: String, handler: ThreeDSHandler, with: UIViewController) { - let authRequest = AuthorizationRequest(source: token, incremental: false, invoiceID: invoiceId) - guard let body = try? JSONEncoder().encode(authRequest) else { - handler.onError(error: ProcessOutException.InternalError) - return - } - - guard let json = try? JSONSerialization.jsonObject(with: body, options: []) as? [String : Any] else { - handler.onError(error: ProcessOutException.InternalError) - return - } - makeAuthorizationRequest(invoiceId: invoiceId, thirdPartySDKVersion: "", json: json, handler: handler, with: with, actionHandlerCompletion: { (newSource) in - makeCardPayment(invoiceId: invoiceId, token: newSource, handler: handler, with: with) - }) - } - - /// Initiate a payment authorization from a previously generated invoice and card token - /// - /// - Parameters: - /// - AuthorizationRequest: contains all the necessary fields to initiate an authorisation request. - /// - handler: Custom 3DS2 handler (please refer to our documentation for this) - /// - with: UIViewController to display webviews and perform fingerprinting - public static func makeCardPayment(AuthorizationRequest request: AuthorizationRequest, handler: ThreeDSHandler, with: UIViewController) { - // Handle empty source - if (request.source.isEmpty || request.invoiceID.isEmpty) { - handler.onError(error: ProcessOutException.InternalError) - return - } - - guard let body = try? JSONEncoder().encode(request) else { - handler.onError(error: ProcessOutException.InternalError) - return - } - - guard let json = try? JSONSerialization.jsonObject(with: body, options: []) as? [String : Any] else { - handler.onError(error: ProcessOutException.InternalError) - return - } - makeAuthorizationRequest(AuthorizationRequest: request, json: json, handler: handler, with: with, actionHandlerCompletion: { (newSource) in - let updatedRequest = AuthorizationRequest(source: newSource, incremental: request.incremental, invoiceID: request.invoiceID) - updatedRequest.thirdPartySDKVersion = request.thirdPartySDKVersion - updatedRequest.threeDS2Enabled = request.threeDS2Enabled - updatedRequest.preferredScheme = request.preferredScheme - makeCardPayment(AuthorizationRequest: updatedRequest, handler: handler, with: with) - }) - } - - /// Initiate an incremental payment authorization from a previously generated invoice and card token - /// - /// - Parameters: - /// - invoiceId: Invoice generated on your backend - /// - token: Card token to be used for the charge - /// - handler: Custom 3DS2 handler (please refer to our documentation for this) - /// - with: UIViewController to display webviews and perform fingerprinting - public static func makeIncrementalAuthorizationPayment(invoiceId: String, token: String, handler: ThreeDSHandler, with: UIViewController) { - let authRequest = AuthorizationRequest(source: token, incremental: true, invoiceID: invoiceId) - guard let body = try? JSONEncoder().encode(authRequest) else { - handler.onError(error: ProcessOutException.InternalError) - return - } - - guard let json = try? JSONSerialization.jsonObject(with: body, options: []) as? [String : Any] else { - handler.onError(error: ProcessOutException.InternalError) - return - } - makeAuthorizationRequest(invoiceId: invoiceId, thirdPartySDKVersion: "", json: json, handler: handler, with: with, actionHandlerCompletion: { (newSource) in - makeIncrementalAuthorizationPayment(invoiceId: invoiceId, token: newSource, handler: handler, with: with) - }) - } - - /// Initiate an incremental payment authorization from a previously generated invoice and card token - /// - /// - Parameters: - /// - invoiceId: Invoice generated on your backend - /// - token: Card token to be used for the charge - /// - thirdPartySDKVersion: Version of the 3rd party SDK being used for the calls. Can be blank if unused - /// - handler: Custom 3DS2 handler (please refer to our documentation for this) - /// - with: UIViewController to display webviews and perform fingerprinting - public static func makeIncrementalAuthorizationPayment(invoiceId: String, token: String, thirdPartySDKVersion: String, handler: ThreeDSHandler, with: UIViewController) { - let authRequest = AuthorizationRequest(source: token, incremental: true, invoiceID: invoiceId) - authRequest.thirdPartySDKVersion = thirdPartySDKVersion - guard let body = try? JSONEncoder().encode(authRequest) else { - handler.onError(error: ProcessOutException.InternalError) - return - } - - guard let json = try? JSONSerialization.jsonObject(with: body, options: []) as? [String : Any] else { - handler.onError(error: ProcessOutException.InternalError) - return - } - makeAuthorizationRequest(invoiceId: invoiceId, thirdPartySDKVersion: thirdPartySDKVersion, json: json, handler: handler, with: with, actionHandlerCompletion: { (newSource) in - makeIncrementalAuthorizationPayment(invoiceId: invoiceId, token: newSource, thirdPartySDKVersion: thirdPartySDKVersion, handler: handler, with: with) - }) - } - - /// Initiate an incremental payment authorization from a previously generated invoice and card token - /// - /// - Parameters: - /// - AuthorizationRequest: contains all the necessary fields to initiate an authorisation request. - /// - handler: Custom 3DS2 handler (please refer to our documentation for this) - /// - with: UIViewController to display webviews and perform fingerprinting - public static func makeIncrementalAuthorizationPayment(AuthorizationRequest request: AuthorizationRequest, handler: ThreeDSHandler, with: UIViewController) { - // Handle empty source - if (request.source.isEmpty || request.invoiceID.isEmpty) { - handler.onError(error: ProcessOutException.InternalError) - return - } - - guard let body = try? JSONEncoder().encode(request) else { - handler.onError(error: ProcessOutException.InternalError) - return - } - - guard let json = try? JSONSerialization.jsonObject(with: body, options: []) as? [String : Any] else { - handler.onError(error: ProcessOutException.InternalError) - return - } - makeAuthorizationRequest(AuthorizationRequest: request, json: json, handler: handler, with: with, actionHandlerCompletion: { (newSource) in - let updatedRequest = AuthorizationRequest(source: newSource, incremental: request.incremental, invoiceID: request.invoiceID) - updatedRequest.thirdPartySDKVersion = request.thirdPartySDKVersion - updatedRequest.threeDS2Enabled = request.threeDS2Enabled - updatedRequest.preferredScheme = request.preferredScheme - makeIncrementalAuthorizationPayment(AuthorizationRequest: updatedRequest, handler: handler, with: with) - }) - } - - /// Increments the authorization of an applicable invoice by a given amount - /// - /// - Parameters: - /// - invoiceId: Invoice generated on your backend - /// - amount: The amount by which the authorization should be incremented - /// - handler: Custom 3DS2 handler (please refer to our documentation for this) - public static func incrementAuthorizationAmount(invoiceId: String, amount: Int, handler: ThreeDSHandler) { - let incrementRequest = IncrementAuthorizationRequest(amount: amount) - guard let body = try? JSONEncoder().encode(incrementRequest) else { - handler.onError(error: ProcessOutException.InternalError) - return - } - - guard let json = try? JSONSerialization.jsonObject(with: body, options: []) as? [String : Any] else { - handler.onError(error: ProcessOutException.InternalError) - return - } - HttpRequest(route: "/invoices/" + invoiceId + "/increment_authorization", method: .post, parameters: json, completion: {(data, error) -> Void in - guard data != nil else { - handler.onError(error: error!) - return - } - - handler.onSuccess(invoiceId: invoiceId) - }) - - } - - /// Create a customer token from a card ID - /// - /// - Parameters: - /// - cardId: Card ID used for the customer token - /// - customerId: Customer ID created in backend - /// - tokenId: Token ID created in backend - /// - handler: 3DS2 handler - /// - with: UIViewController to display webviews and perform fingerprinting - public static func makeCardToken(source: String, customerId: String, tokenId: String, handler: ThreeDSHandler, with: UIViewController) { - let tokenRequest = TokenRequest(source: source, customerID: customerId, tokenID: tokenId) - guard let body = try? JSONEncoder().encode(tokenRequest) else { - handler.onError(error: ProcessOutException.InternalError) - return - } - do { - let json = try JSONSerialization.jsonObject(with: body, options: []) as! [String: Any] - HttpRequest(route: "/customers/" + customerId + "/tokens/" + tokenId, method: .put, parameters: json) { (data, error) in - guard error == nil, data != nil else { - handler.onError(error: error!) - return - } - - // Try to decode the auth result - guard let result = try? JSONDecoder().decode(AuthorizationResult.self, from: data!) else { - handler.onError(error: ProcessOutException.InternalError) - return - } - - guard let customerAction = result.customerAction else { - // Card token verified - handler.onSuccess(invoiceId: tokenId) - return - } - - // Instantiate the webView - let poWebView = CardTokenWebView(frame: with.view.frame, onResult: { (token) in - // Web authentication completed - makeCardToken(source: token, customerId: customerId, tokenId: tokenId, handler: handler, with: with) - }, onAuthenticationError: {() in - // Error while authenticating - handler.onError(error: ProcessOutException.BadRequest(errorMessage: "Web authentication failed.", errorCode: "")) - }) - - // Instantiate the customer action handler - let actionHandler = CustomerActionHandler(handler: handler, processOutWebView: poWebView, with: with) - - // Start the action handling flow - actionHandler.handleCustomerAction(customerAction: customerAction, completion: { (newSource) in - // Successful, new source available to continue the flow - makeCardToken(source: newSource, customerId: customerId, tokenId: tokenId, handler: handler, with: with) - }) - - } - } catch { - handler.onError(error: ProcessOutException.InternalError) - } - } - - /// Create a customer token from a card ID - /// - /// - Parameters: - /// - cardId: Card ID used for the customer token - /// - customerId: Customer ID created in backend - /// - tokenId: Token ID created in backend - /// - thirdPartySDKVersion: Version of the 3rd party SDK being used for the calls. Can be blank if unused - /// - handler: 3DS2 handler - /// - with: UIViewController to display webviews and perform fingerprinting - public static func makeCardToken(source: String, customerId: String, tokenId: String, thirdPartySDKVersion: String, handler: ThreeDSHandler, with: UIViewController) { - let tokenRequest = TokenRequest(source: source, customerID: customerId, tokenID: tokenId) - tokenRequest.thirdPartySDKVersion = thirdPartySDKVersion - guard let body = try? JSONEncoder().encode(tokenRequest) else { - handler.onError(error: ProcessOutException.InternalError) - return - } - do { - let json = try JSONSerialization.jsonObject(with: body, options: []) as! [String: Any] - HttpRequest(route: "/customers/" + customerId + "/tokens/" + tokenId, method: .put, parameters: json) { (data, error) in - guard error == nil, data != nil else { - handler.onError(error: error!) - return - } - - // Try to decode the auth result - guard let result = try? JSONDecoder().decode(AuthorizationResult.self, from: data!) else { - handler.onError(error: ProcessOutException.InternalError) - return - } - - guard let customerAction = result.customerAction else { - // Card token verified - handler.onSuccess(invoiceId: tokenId) - return - } - - // Instantiate the webView - let poWebView = CardTokenWebView(frame: with.view.frame, onResult: { (token) in - // Web authentication completed - makeCardToken(source: token, customerId: customerId, tokenId: tokenId, thirdPartySDKVersion: thirdPartySDKVersion, handler: handler, with: with) - }, onAuthenticationError: {() in - // Error while authenticating - handler.onError(error: ProcessOutException.BadRequest(errorMessage: "Web authentication failed.", errorCode: "")) - }) - - // Instantiate the customer action handler - let actionHandler = CustomerActionHandler(handler: handler, processOutWebView: poWebView, with: with) - - // Start the action handling flow - actionHandler.handleCustomerAction(customerAction: customerAction, completion: { (newSource) in - // Successful, new source available to continue the flow - makeCardToken(source: newSource, customerId: customerId, tokenId: tokenId, thirdPartySDKVersion: thirdPartySDKVersion, handler: handler, with: with) - }) - - } - } catch { - handler.onError(error: ProcessOutException.InternalError) - } - } - - /// Create a customer token from a card ID - /// - /// - Parameters: - /// - TokenRequest: contains all the fields necessary for the token request - /// - handler: 3DS2 handler - /// - with: UIViewController to display webviews and perform fingerprinting - public static func makeCardToken(TokenRequest request: TokenRequest, handler: ThreeDSHandler, with: UIViewController) { - // Handle empty source - if (request.source.isEmpty || request.customerID.isEmpty || request.tokenID.isEmpty) { - handler.onError(error: ProcessOutException.InternalError) - return - } - - guard let body = try? JSONEncoder().encode(request) else { - handler.onError(error: ProcessOutException.InternalError) - return - } - do { - let json = try JSONSerialization.jsonObject(with: body, options: []) as! [String: Any] - HttpRequest(route: "/customers/" + request.customerID + "/tokens/" + request.tokenID, method: .put, parameters: json) { (data, error) in - guard error == nil, data != nil else { - handler.onError(error: error!) - return - } - - // Try to decode the auth result - guard let result = try? JSONDecoder().decode(AuthorizationResult.self, from: data!) else { - handler.onError(error: ProcessOutException.InternalError) - return - } - - guard let customerAction = result.customerAction else { - // Card token verified - handler.onSuccess(invoiceId: request.tokenID) - return - } - - // Instantiate the webView - let poWebView = CardTokenWebView(frame: with.view.frame, onResult: { (token) in - // Web authentication completed - let updatedRequest = TokenRequest(source: token, customerID: request.customerID, tokenID: request.tokenID) - updatedRequest.verify = request.verify - updatedRequest.enable3DS2 = request.enable3DS2 - updatedRequest.thirdPartySDKVersion = request.thirdPartySDKVersion - updatedRequest.preferredScheme = request.preferredScheme - makeCardToken(TokenRequest: updatedRequest, handler: handler, with: with) - }, onAuthenticationError: {() in - // Error while authenticating - handler.onError(error: ProcessOutException.BadRequest(errorMessage: "Web authentication failed.", errorCode: "")) - }) - - // Instantiate the customer action handler - let actionHandler = CustomerActionHandler(handler: handler, processOutWebView: poWebView, with: with) - - // Start the action handling flow - actionHandler.handleCustomerAction(customerAction: customerAction, completion: { (newSource) in - // Successful, new source available to continue the flow - let updatedRequest = TokenRequest(source: newSource, customerID: request.customerID, tokenID: request.tokenID) - updatedRequest.verify = request.verify - updatedRequest.enable3DS2 = request.enable3DS2 - updatedRequest.thirdPartySDKVersion = request.thirdPartySDKVersion - updatedRequest.preferredScheme = request.preferredScheme - makeCardToken(TokenRequest: updatedRequest, handler: handler, with: with) - }) - - } - } catch { - handler.onError(error: ProcessOutException.InternalError) - } - } - - /// Creates a test 3DS2 handler that lets you integrate and test the 3DS2 flow seamlessly. Only use this while using sandbox API keys - /// - /// - Parameter viewController: UIViewController (needed to display a 3DS2 challenge popup) - /// - Returns: Returns a sandbox ready ThreeDS2Handler - @available(*, deprecated) - public static func createThreeDSTestHandler(viewController: UIViewController, completion: @escaping (String?, ProcessOutException?) -> Void) -> ThreeDSHandler { - return ThreeDSTestHandler(controller: viewController, completion: completion) - } - - /// Parses an intent uri. Either for an APM payment return or after an makeAPMToken call - /// - /// - Parameter url: URI from the deep-link app opening - /// - Returns: nil if the URL is not a ProcessOut return URL, an APMTokenReturn object otherwise - @available(*, deprecated, message: "Use ProcessOut.shared.alternativePaymentMethods.alternativePaymentMethodResponse instead.") - public static func handleAPMURLCallback(url: URL) -> APMTokenReturn? { - // Check for the URL host - guard let host = url.host, host == "processout.return" else { - return nil - } - - // Retrieve the URL parameters - guard let params = url.queryParameters else { - return APMTokenReturn(error: ProcessOutException.InternalError) - } - - // Retrieve the token - guard let token = params["token"], !token.isEmpty else { - return APMTokenReturn(error: ProcessOutException.InternalError) - } - - // Retrieve the customer_id and token_id if available - let customerId = params["customer_id"] - let tokenId = params["token_id"] - - // Check if we're on a tokenization return - if customerId != nil && !customerId!.isEmpty && tokenId != nil && !tokenId!.isEmpty { - return APMTokenReturn(token: token, customerId: customerId!, tokenId: tokenId!) - } - - // Simple APM authorization case - return APMTokenReturn(token: token) - } - - /// Generates an alternative payment method token - /// - /// - Parameters: - /// - gateway: The alternative payment method configuration - /// - customerId: The customer ID - /// - tokenId: The token ID generated on your backend with an empty source - @available(*, deprecated, message: "Use POAlternativePaymentMethodViewControllerBuilder to initiate APM payment.") - public static func makeAPMToken(gateway: GatewayConfiguration, customerId: String, tokenId: String, additionalData: [String: String] = [:]) { - // Generate the redirection URL - let checkout = ProcessOutLegacyApi.ProjectId + "/" + customerId + "/" + tokenId + "/redirect/" + gateway.id - let additionalDataString = generateAdditionalDataString(additionalData: additionalData) - let urlString = ProcessOutLegacyApi.CheckoutUrl + "/" + checkout + additionalDataString - - if let url = URL(string: urlString) { - UIApplication.shared.open(url) - } - } - - /// Returns the URL to initiate an alternative payment method payment - /// - /// - Parameters: - /// - gateway: Gateway to use (previously fetched) - /// - invoiceId: Invoice ID generated on your backend - /// - Returns: Redirect URL that should be displayed in a webview - @available(*, deprecated, message: "Use POAlternativePaymentMethodViewControllerBuilder to initiate APM payment.") - public static func makeAPMPayment(gateway: GatewayConfiguration, invoiceId: String, additionalData: [String: String] = [:]) -> String { - // Generate the redirection URL - let checkout = ProcessOutLegacyApi.ProjectId + "/" + invoiceId + "/redirect/" + gateway.id - let additionalDataString = generateAdditionalDataString(additionalData: additionalData) - let urlString = ProcessOutLegacyApi.CheckoutUrl + "/" + checkout + additionalDataString - - return urlString - } - - /// Generates an additionalData query parameter string - /// - /// - Parameter additionalData: additionalData to send to the APM - /// - Returns: a empty string or a string starting with ? followed by the query value - private static func generateAdditionalDataString(additionalData: [String: String]) -> String { - // Transform the map into an array of additional_data[key]=value - let addData = additionalData.map({ (data) -> String in - let (key, value) = data - - // Try to encode value - let encodedValue = value.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) - - return "additional_data[" + key + "]" + "=" + (encodedValue ?? "") - }) - - // The array is empty we return an empty string - if addData.count == 0 { - return "" - } - - // Reduce the array into a single string - return "?" + addData.reduce("", { (result, current) -> String in - if result.isEmpty { - return result + current - } - - return result + "&" + current - }) - } - - /// Generates a query parameter string to facilitate pagination on many endpoints. - /// For more information, see https://docs.processout.com/refs/#pagination - /// - /// - Parameter paginationOptions: Pagination options to use - /// - Returns: An empty string or a string containing a set of query parameters. - /// Note that the returned string is not prefixed or suffixed with ? or &, so you may need to do this yourself depending on where these parameters will appear in your URL - @available(*, deprecated) - private static func generatePaginationParamsString(paginationOptions: PaginationOptions) -> String { - // Construct the individual query params and store them in an array - let paginationParams: [String?] = [ - paginationOptions.StartAfter != nil ? "start_after=" + paginationOptions.StartAfter! : nil, - paginationOptions.EndBefore != nil ? "end_before=" + paginationOptions.EndBefore! : nil, - paginationOptions.Limit != nil ? "limit=" + String(paginationOptions.Limit!) : nil, - paginationOptions.Order != nil ? "order=" + paginationOptions.Order! : nil - ] - - // Remove any nil values from the array - let filteredPaginationParams = paginationParams.compactMap {$0} - - // Join the array into a single string separated by ampersands and return it - return filteredPaginationParams.joined(separator: "&") - } - - /// Requests an authorization for a specified invoice and initiates web authentication where appropriate. - /// - /// - Parameters: - /// - invoiceId: Invoice generated on your backend - /// - json: The request body - /// - thirdPartySDKVersion: Version of the 3rd party 3ds2 SDK used for the calls. Can be blank if unused. - /// - handler: Custom 3DS2 handler (please refer to our documentation for this) - /// - with: UIViewController to display webviews and perform fingerprinting - /// - actionHandlerCompletion: Callback to pass to action handler to be executed following web authentication - private static func makeAuthorizationRequest(invoiceId: String, thirdPartySDKVersion: String, json: [String: Any]?, handler: ThreeDSHandler, with: UIViewController, actionHandlerCompletion: @escaping (String) -> Void) -> Void { - HttpRequest(route: "/invoices/" + invoiceId + "/authorize", method: .post, parameters: json, completion: {(data, error) -> Void in - guard data != nil else { - handler.onError(error: error ?? ProcessOutException.InternalError) - return - } - - guard let authorizationResult = try? JSONDecoder().decode(AuthorizationResult.self, from: data!) else { - handler.onError(error: ProcessOutException.InternalError) - return - } - guard let customerAction = authorizationResult.customerAction else { - // No customer action required, payment authorized - handler.onSuccess(invoiceId: invoiceId) - return - } - - // Initiate the webView component - let poWebView = CardPaymentWebView(frame: with.view.frame, onResult: {(token) in - actionHandlerCompletion(token) - }, onAuthenticationError: {() in - // Error while authenticating - handler.onError(error: ProcessOutException.BadRequest(errorMessage: "Web authentication failed.", errorCode: "")) - }) - - // Initiate the action handler - let actionHandler = CustomerActionHandler(handler: handler, processOutWebView: poWebView, with: with) - - // Start the action handling - actionHandler.handleCustomerAction(customerAction: customerAction, completion: actionHandlerCompletion) - }) - } - - /// Requests an authorization for a specified invoice and initiates web authentication where appropriate. - /// - /// - Parameters: - /// - AuthorizationRequest: contains all the necessary fields to initiate an authorisation request. - /// - json: The request body - /// - handler: Custom 3DS2 handler (please refer to our documentation for this) - /// - with: UIViewController to display webviews and perform fingerprinting - /// - actionHandlerCompletion: Callback to pass to action handler to be executed following web authentication - private static func makeAuthorizationRequest(AuthorizationRequest request: AuthorizationRequest, json: [String: Any]?, handler: ThreeDSHandler, with: UIViewController, actionHandlerCompletion: @escaping (String) -> Void) -> Void { - HttpRequest(route: "/invoices/" + request.invoiceID + "/authorize", method: .post, parameters: json, completion: {(data, error) -> Void in - guard data != nil else { - handler.onError(error: error ?? ProcessOutException.InternalError) - return - } - - guard let authorizationResult = try? JSONDecoder().decode(AuthorizationResult.self, from: data!) else { - handler.onError(error: ProcessOutException.InternalError) - return - } - guard let customerAction = authorizationResult.customerAction else { - // No customer action required, payment authorized - handler.onSuccess(invoiceId: request.invoiceID) - return - } - - // Initiate the webView component - let poWebView = CardPaymentWebView(frame: with.view.frame, onResult: {(token) in - // Web authentication completed - actionHandlerCompletion(token) - }, onAuthenticationError: {() in - // Error while authenticating - handler.onError(error: ProcessOutException.BadRequest(errorMessage: "Web authentication failed.", errorCode: "")) - }) - - // Initiate the action handler - let actionHandler = CustomerActionHandler(handler: handler, processOutWebView: poWebView, with: with) - - // Start the action handling - actionHandler.handleCustomerAction(customerAction: customerAction, completion: actionHandlerCompletion) - }) - } - - private static func HttpRequest(route: String, method: HTTPMethod, parameters: [String: Any]?, completion: @escaping (Data?, ProcessOutException?) -> Void) { - self.requestManager.HttpRequest(route: route, method: method, parameters: parameters, completion: completion) - } -} diff --git a/Sources/ProcessOut/Sources/Legacy/ProcessOutRequestManager.swift b/Sources/ProcessOut/Sources/Legacy/ProcessOutRequestManager.swift deleted file mode 100644 index 6026771fd..000000000 --- a/Sources/ProcessOut/Sources/Legacy/ProcessOutRequestManager.swift +++ /dev/null @@ -1,129 +0,0 @@ -// -// ProcessOutRequest.swift -// ProcessOut -// -// Created by Mauro Vime Castillo on 15/2/21. -// - -import Foundation - -@available(*, deprecated) -final class ProcessOutRequestManager { - - let apiUrl: String - let apiVersion: String - let defaultUserAgent: String - - let sessionDelegate: SessionDelegate - let retryPolicy: RetryPolicy - let urlSession: URLSession - let urlSessionConfiguration: URLSessionConfiguration - - init(apiUrl: String, apiVersion: String, defaultUserAgent: String) { - self.apiUrl = apiUrl - self.apiVersion = apiVersion - self.defaultUserAgent = defaultUserAgent - self.urlSessionConfiguration = .default - self.urlSessionConfiguration.urlCache = nil - self.urlSessionConfiguration.requestCachePolicy = .reloadIgnoringLocalCacheData - - let retryPolicy = RetryPolicy() - self.retryPolicy = retryPolicy - - let sessionDelegate = SessionDelegate() - sessionDelegate.retrier = retryPolicy - self.sessionDelegate = sessionDelegate - - self.urlSession = URLSession(configuration: self.urlSessionConfiguration, delegate: sessionDelegate, delegateQueue: .main) - } - - func HttpRequest(route: String, method: HTTPMethod, parameters: [String: Any]?, completion: @escaping (Data?, ProcessOutException?) -> Void) { - guard let authorizationHeader = self.authorizationHeader(user: ProcessOutLegacyApi.ProjectId, password: "") else { - completion(nil, ProcessOutException.MissingProjectId) - return - } - - do { - guard let url = NSURL(string: self.apiUrl + route) else { - completion(nil, ProcessOutException.InternalError) - return - } - - var request = URLRequest(url: url as URL) - request.httpMethod = method.rawValue - request.setValue("application/json", forHTTPHeaderField: "Content-Type") - request.setValue(authorizationHeader.value, forHTTPHeaderField: authorizationHeader.key) - request.setValue(self.defaultUserAgent + " ProcessOut iOS-Bindings/" + self.apiVersion, forHTTPHeaderField: "User-Agent") - request.setValue(UUID().uuidString, forHTTPHeaderField: "Idempotency-Key") - request.timeoutInterval = 15 - - if let body = parameters { - request.httpBody = try JSONSerialization.data(withJSONObject: body, options: []) - } - - - self.urlSession.dataTask(with: request) { [weak self] (data, _, _) in - guard let data = data else { - completion(nil, ProcessOutException.NetworkError) - return - } - - self?.handleNetworkResult(data: data, completion: completion) - } - .resume() - } catch { - completion(nil, ProcessOutException.InternalError) - } - } - - private func handleNetworkResult(data: Data, completion: @escaping (Data?, ProcessOutException?) -> Void) { - do { - let result = try JSONDecoder().decode(ApiResponse.self, from: data) - if result.success { - completion(data, nil) - return - } - - if let message = result.message, let errorType = result.errorType { - completion(nil, ProcessOutException.BadRequest(errorMessage: message, errorCode: errorType)) - return - } - - completion(nil, ProcessOutException.NetworkError) - } catch { - completion(nil, ProcessOutException.InternalError) - } - } - - private func authorizationHeader(user: String, password: String) -> (key: String, value: String)? { - guard let data = "\(user):\(password)".data(using: .utf8) else { return nil } - - let credential = data.base64EncodedString(options: []) - - return (key: "Authorization", value: "Basic \(credential)") - } - -} - -final class SessionDelegate: NSObject, URLSessionDelegate { - - var retrier: RequestRetrier? - - func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) { - if let retrier = retrier, let error = error { - retrier.should(session, retry: task, with: error) { [weak task] shouldRetry, timeDelay in - guard shouldRetry else { - return - } - - DispatchQueue.global(qos: .utility).asyncAfter(deadline: .now() + timeDelay) { - guard let request = task?.currentRequest else { - return - } - - session.dataTask(with: request).resume() - } - } - } - } -} diff --git a/Sources/ProcessOut/Sources/Legacy/ProcessOutWebView.swift b/Sources/ProcessOut/Sources/Legacy/ProcessOutWebView.swift deleted file mode 100644 index d1abef5e6..000000000 --- a/Sources/ProcessOut/Sources/Legacy/ProcessOutWebView.swift +++ /dev/null @@ -1,69 +0,0 @@ -// -// ProcessOutWebView.swift -// ProcessOut -// -// Created by Jeremy Lejoux on 30/09/2019. -// - -import Foundation -import WebKit - -@available(*, deprecated, message: "Use PO3DSRedirectViewControllerBuilder or POAlternativePaymentMethodViewControllerBuilder instead.") -public class ProcessOutWebView: WKWebView, WKNavigationDelegate, WKUIDelegate { - - private let REDIRECT_URL_PATTERN = "https:\\/\\/checkout\\.processout\\.(ninja|com)\\/helpers\\/mobile-processout-webview-landing.*" - - internal var onResult: (_ token: String) -> Void - internal var onAuthenticationError: () -> Void - - public init(frame: CGRect, onResult: @escaping (String) -> Void, onAuthenticationError: @escaping () -> Void) { - // Setup up the webview to display the challenge - let config = WKWebViewConfiguration() - let preferences = WKPreferences() - preferences.javaScriptEnabled = true - config.preferences = preferences - - self.onResult = onResult - self.onAuthenticationError = onAuthenticationError - super.init(frame: frame, configuration: config) - self.customUserAgent = "ProcessOut iOS-Webview/" + type(of: ProcessOut.shared).version - self.navigationDelegate = self - self.uiDelegate = self - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - func onRedirect(url: URL) { - fatalError("Must override onRedirect") - } - - public func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { - guard let url = webView.url else { - return - } - - if url.absoluteString.range(of: REDIRECT_URL_PATTERN, options: .regularExpression, range: nil, locale: nil) != nil { - self.onRedirect(url: url) - } - } - - public func webView(_ webView: WKWebView, createWebViewWith configuration: WKWebViewConfiguration, for navigationAction: WKNavigationAction, windowFeatures: WKWindowFeatures)-> WKWebView? { - // Add support for popups/new tabs - webView.load(navigationAction.request) - return nil - } -} - -extension URL { - - var queryParameters: [String: String]? { - guard - let components = URLComponents(url: self, resolvingAgainstBaseURL: true), - let queryItems = components.queryItems else { return nil } - return queryItems.reduce(into: [String: String]()) { (result, item) in - result[item.name] = item.value - } - } -} diff --git a/Sources/ProcessOut/Sources/Legacy/RetryPolicy.swift b/Sources/ProcessOut/Sources/Legacy/RetryPolicy.swift deleted file mode 100644 index 67da20e98..000000000 --- a/Sources/ProcessOut/Sources/Legacy/RetryPolicy.swift +++ /dev/null @@ -1,56 +0,0 @@ -// -// RetryPolicy.swift -// ProcessOut -// -// Created by Jeremy Lejoux on 18/09/2019. -// - -import Foundation - -typealias RequestRetryCompletion = (_ shouldRetry: Bool, _ timeDelay: TimeInterval) -> Void - -protocol RequestRetrier { - - func should(_ session: URLSession, retry task: URLSessionTask, with error: Error, completion: @escaping RequestRetryCompletion) -} - -final class RetryPolicy: RequestRetrier { - - private var currentRetriedRequests: [String: Int] = [:] - private let RETRY_INTERVAL: TimeInterval = 0.1 // Retry after .1s - private let MAXIMUM_RETRIES = 2 - - func should(_ session: URLSession, retry task: URLSessionTask, with error: Error, completion: @escaping RequestRetryCompletion) { - guard task.response == nil, let url = task.currentRequest?.url?.absoluteString else { - clearRetriedForUrl(url: task.currentRequest?.url?.absoluteString) - completion(false, 0.0) // Shouldn't retry - return - } - - guard let retryCount = currentRetriedRequests[url] else { - // Should retry - currentRetriedRequests[url] = 1 - completion(true, RETRY_INTERVAL) - return - } - - if retryCount <= MAXIMUM_RETRIES { - // Should retry - currentRetriedRequests[url] = retryCount + 1 - completion(true, RETRY_INTERVAL) - return - } - - // Shouldn't retry - clearRetriedForUrl(url: url) - completion(false, 0.0) - } - - private func clearRetriedForUrl(url: String?) { - guard let url = url else { - return - } - - currentRetriedRequests.removeValue(forKey: url) - } -} diff --git a/Sources/ProcessOut/Sources/Legacy/ThreeDSFingerprintResponse.swift b/Sources/ProcessOut/Sources/Legacy/ThreeDSFingerprintResponse.swift deleted file mode 100644 index a01c3784c..000000000 --- a/Sources/ProcessOut/Sources/Legacy/ThreeDSFingerprintResponse.swift +++ /dev/null @@ -1,48 +0,0 @@ -// -// ThreeDSFingerprintResponse.swift -// ProcessOut -// -// Created by Jeremy Lejoux on 18/06/2019. -// - -@available(*, deprecated, message: "Use PO3DSService instead.") -public final class ThreeDSFingerprintResponse: Codable { - - public final class SDKEphemPubKey: Codable { - var crv: String = "" - var kty: String = "" - var x: String = "" - var y: String = "" - - private enum CodingKeys: String, CodingKey { - case crv = "crv" - case kty = "kty" - case x = "x" - case y = "y" - } - } - - var sdkEncData: String = "" - var deviceChannel: String = "app" - var sdkAppID: String = "" - var sdkEphemPubKey: SDKEphemPubKey? - var sdkReferenceNumber: String = "" - var sdkTransID: String = "" - - public init(sdkEncData: String, sdkAppID: String, sdkEphemPubKey: SDKEphemPubKey?, sdkReferenceNumber: String, sdkTransID: String) { - self.sdkEncData = sdkEncData - self.sdkAppID = sdkAppID - self.sdkEphemPubKey = sdkEphemPubKey - self.sdkReferenceNumber = sdkReferenceNumber - self.sdkTransID = sdkTransID - } - - private enum CodingKeys: String, CodingKey { - case sdkEncData = "sdkEncData" - case deviceChannel = "deviceChannel" - case sdkAppID = "sdkAppID" - case sdkEphemPubKey = "sdkEphemPubKey" - case sdkReferenceNumber = "sdkReferenceNumber" - case sdkTransID = "sdkTransID" - } -} diff --git a/Sources/ProcessOut/Sources/Legacy/ThreeDSHandler.swift b/Sources/ProcessOut/Sources/Legacy/ThreeDSHandler.swift deleted file mode 100644 index ff55ad91b..000000000 --- a/Sources/ProcessOut/Sources/Legacy/ThreeDSHandler.swift +++ /dev/null @@ -1,42 +0,0 @@ -// -// ThreeDSHandler.swift -// ProcessOut -// -// Created by Jeremy Lejoux on 17/06/2019. -// - -import UIKit - -/// Custom protocol which lets you implement a 3DS2 integration -@available(*, deprecated, message: "Use PO3DSService instead.") -public protocol ThreeDSHandler { - - /// method called when a device fingerprint is required - /// - /// - Parameters: - /// - directoryServerData: Contains information required by the third-party handling the device fingerprinting - /// - completion: Callback containing the fingerprint information - func doFingerprint(directoryServerData: DirectoryServerData, completion: @escaping (ThreeDSFingerprintResponse) -> Void) - - /// Method called when a 3DS2 challenge is required - /// - /// - Parameters: - /// - authentificationData: Authentification data required to present the challenge - /// - completion: Callback specifying wheter or not the challenge was successful - func doChallenge(authentificationData: AuthentificationChallengeData, completion: @escaping (Bool) -> Void) - - /// Method called when a web challenge is required - /// - /// - Parameter webView: The webView to present - func doPresentWebView(webView: ProcessOutWebView) - - /// Called when the authorization was successful - /// - /// - Parameter invoiceId: Invoice id that was authorized - func onSuccess(invoiceId: String) - - /// Called when the authorization process ends up in a failed state. - /// - /// - Parameter error: Error - func onError(error: ProcessOutException) -} diff --git a/Sources/ProcessOut/Sources/Legacy/ThreeDSTestHandler.swift b/Sources/ProcessOut/Sources/Legacy/ThreeDSTestHandler.swift deleted file mode 100644 index 3094f617f..000000000 --- a/Sources/ProcessOut/Sources/Legacy/ThreeDSTestHandler.swift +++ /dev/null @@ -1,58 +0,0 @@ -// -// ThreeDSTestHandler.swift -// ProcessOut -// -// Created by Andrii Vysotskyi on 23.01.2023. -// - -import UIKit - -@available(*, deprecated) -public class ThreeDSTestHandler: ThreeDSHandler { - - var controller: UIViewController - var completion: (String?, ProcessOutException?) -> Void - var webView: ProcessOutWebView? - - public init(controller: UIViewController, completion: @escaping (String?, ProcessOutException?) -> Void) { - self.controller = controller - self.completion = completion - } - - public func doFingerprint(directoryServerData: DirectoryServerData, completion: (ThreeDSFingerprintResponse) -> Void) { - completion(ThreeDSFingerprintResponse(sdkEncData: "", sdkAppID: "", sdkEphemPubKey: ThreeDSFingerprintResponse.SDKEphemPubKey(), sdkReferenceNumber: "", sdkTransID: "")) - - } - - public func doChallenge(authentificationData: AuthentificationChallengeData, completion: @escaping (Bool) -> Void) { - let alert = UIAlertController(title: "Do you want to accept the 3DS2 challenge", message: "", preferredStyle: .alert) - - alert.addAction(UIAlertAction(title: "Accept", style: .default, handler: { (action) in - completion(true) - })) - alert.addAction(UIAlertAction(title: "Reject", style: .default, handler: { (action) in - completion(false) - })) - - self.controller.present(alert, animated: true) - } - - public func doPresentWebView(webView: ProcessOutWebView) { - self.webView = webView - controller.view.addSubview(webView) - } - - public func onSuccess(invoiceId: String) { - if self.webView != nil { - webView!.removeFromSuperview() - } - self.completion(invoiceId, nil) - } - - public func onError(error: ProcessOutException) { - if self.webView != nil { - webView!.removeFromSuperview() - } - self.completion(nil, error) - } -} diff --git a/Sources/ProcessOut/Sources/Legacy/TokenRequest.swift b/Sources/ProcessOut/Sources/Legacy/TokenRequest.swift deleted file mode 100644 index 0810a1701..000000000 --- a/Sources/ProcessOut/Sources/Legacy/TokenRequest.swift +++ /dev/null @@ -1,37 +0,0 @@ -// -// TokenRequest.swift -// ProcessOut -// -// Created by Jeremy Lejoux on 19/09/2019. -// - -import Foundation - -@available(*, deprecated, message: "Use ProcessOut.shared.customerTokens.assignCustomerToken() with POAssignCustomerTokenRequest instead.") -public final class TokenRequest: Encodable { - - var customerID: String = "" - var tokenID: String = "" - var source: String = "" - var verify = true - var enable3DS2 = true - - public var thirdPartySDKVersion: String = "" - public var preferredScheme: String = "" - - public init(source: String, customerID: String, tokenID: String) { - self.source = source - self.customerID = customerID - self.tokenID = tokenID - } - - private enum CodingKeys: String, CodingKey { - case customerID = "customer_id" - case tokenID = "token_id" - case source = "source" - case verify = "verify" - case enable3DS2 = "enable_three_d_s_2" - case thirdPartySDKVersion = "third_party_sdk_version" - case preferredScheme = "preferred_scheme" - } -} diff --git a/Sources/ProcessOut/Sources/Legacy/TokenizationResult.swift b/Sources/ProcessOut/Sources/Legacy/TokenizationResult.swift deleted file mode 100644 index ec1bdd13a..000000000 --- a/Sources/ProcessOut/Sources/Legacy/TokenizationResult.swift +++ /dev/null @@ -1,29 +0,0 @@ -// -// TokenizationResult.swift -// ProcessOut -// -// Created by Jeremy Lejoux on 18/06/2019. -// - -final class TokenizationResult: ApiResponse { - - final class Card: Decodable { - var id: String - - private enum CodingKeys: String, CodingKey { - case id = "id" - } - } - - var card: Card? - - required init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - self.card = try container.decode(Card.self, forKey: .card) - try super.init(from: decoder) - } - - private enum CodingKeys: String, CodingKey { - case card = "card" - } -} diff --git a/Sources/ProcessOut/Sources/Repositories/Cards/HttpCardsRepository.swift b/Sources/ProcessOut/Sources/Repositories/Cards/HttpCardsRepository.swift index 5e3ffe0f4..694c1b4f7 100644 --- a/Sources/ProcessOut/Sources/Repositories/Cards/HttpCardsRepository.swift +++ b/Sources/ProcessOut/Sources/Repositories/Cards/HttpCardsRepository.swift @@ -24,21 +24,30 @@ final class HttpCardsRepository: CardsRepository { } func tokenize(request: POCardTokenizationRequest) async throws -> POCard { - let httpRequest = HttpConnectorRequest.post( + struct Response: Decodable { + let card: POCard + } + let httpRequest = HttpConnectorRequest.post( path: "/cards", body: request, includesDeviceMetadata: true ) return try await connector.execute(request: httpRequest).card } func updateCard(request: POCardUpdateRequest) async throws -> POCard { - let httpRequest = HttpConnectorRequest.put( + struct Response: Decodable { + let card: POCard + } + let httpRequest = HttpConnectorRequest.put( path: "/cards/" + request.cardId, body: request, includesDeviceMetadata: true ) return try await connector.execute(request: httpRequest).card } func tokenize(request: ApplePayCardTokenizationRequest) async throws -> POCard { - let httpRequest = HttpConnectorRequest.post( + struct Response: Decodable { + let card: POCard + } + let httpRequest = HttpConnectorRequest.post( path: "/cards", body: request, includesDeviceMetadata: true ) return try await connector.execute(request: httpRequest).card diff --git a/Sources/ProcessOut/Sources/Repositories/Cards/Requests/ApplePayCardTokenizationRequest.swift b/Sources/ProcessOut/Sources/Repositories/Cards/Requests/ApplePayCardTokenizationRequest.swift index 1ea2c1521..29db1d7c1 100644 --- a/Sources/ProcessOut/Sources/Repositories/Cards/Requests/ApplePayCardTokenizationRequest.swift +++ b/Sources/ProcessOut/Sources/Repositories/Cards/Requests/ApplePayCardTokenizationRequest.swift @@ -7,9 +7,9 @@ import Foundation -struct ApplePayCardTokenizationRequest: Encodable { +struct ApplePayCardTokenizationRequest: Encodable, Sendable { - struct PaymentMethod { + struct PaymentMethod: Sendable { /// Card display name. let displayName: String? @@ -22,7 +22,7 @@ struct ApplePayCardTokenizationRequest: Encodable { } /// Based on [payment token structure.](https://developer.apple.com/documentation/passkit/apple_pay/payment_token_format_reference#3949537) - struct PaymentData: Codable { + struct PaymentData: Codable, Sendable { /// Encrypted payment data. let data: String @@ -37,7 +37,7 @@ struct ApplePayCardTokenizationRequest: Encodable { let version: String } - struct ApplePayToken { + struct ApplePayToken: Sendable { /// Payment data. let paymentData: PaymentData @@ -49,7 +49,7 @@ struct ApplePayCardTokenizationRequest: Encodable { let transactionIdentifier: String } - struct ApplePay: Encodable { + struct ApplePay: Encodable, Sendable { /// Token details. let token: ApplePayToken diff --git a/Sources/ProcessOut/Sources/Repositories/Cards/Requests/POCardTokenizationRequest.swift b/Sources/ProcessOut/Sources/Repositories/Cards/Requests/POCardTokenizationRequest.swift index 64aa46dce..5b4b0825b 100644 --- a/Sources/ProcessOut/Sources/Repositories/Cards/Requests/POCardTokenizationRequest.swift +++ b/Sources/ProcessOut/Sources/Repositories/Cards/Requests/POCardTokenizationRequest.swift @@ -8,7 +8,7 @@ import Foundation /// Card details that should be tokenized. -public struct POCardTokenizationRequest: Encodable { +public struct POCardTokenizationRequest: Encodable, Sendable { /// Number of the card. public let number: String @@ -29,8 +29,7 @@ public struct POCardTokenizationRequest: Encodable { public let contact: POContact? /// Preferred scheme defined by the Customer. - @POTypedRepresentation - public private(set) var preferredScheme: String? + public let preferredScheme: POCardScheme? /// Metadata related to the card. public let metadata: [String: String]? @@ -42,7 +41,7 @@ public struct POCardTokenizationRequest: Encodable { cvc: String? = nil, name: String? = nil, contact: POContact? = nil, - preferredScheme: String? = nil, + preferredScheme: POCardScheme? = nil, metadata: [String: String]? = nil ) { self.number = number @@ -51,7 +50,7 @@ public struct POCardTokenizationRequest: Encodable { self.cvc = cvc self.name = name self.contact = contact - self._preferredScheme = .init(wrappedValue: preferredScheme) + self.preferredScheme = preferredScheme self.metadata = metadata } } diff --git a/Sources/ProcessOut/Sources/Repositories/Cards/Requests/POCardUpdateRequest.swift b/Sources/ProcessOut/Sources/Repositories/Cards/Requests/POCardUpdateRequest.swift index 8d9df5ce4..d75893cdc 100644 --- a/Sources/ProcessOut/Sources/Repositories/Cards/Requests/POCardUpdateRequest.swift +++ b/Sources/ProcessOut/Sources/Repositories/Cards/Requests/POCardUpdateRequest.swift @@ -6,11 +6,10 @@ // /// Updated card details. -public struct POCardUpdateRequest: Encodable { +public struct POCardUpdateRequest: Encodable, Sendable { // sourcery: AutoCodingKeys /// Card id. - @POImmutableExcludedCodable - public var cardId: String + public let cardId: String // sourcery:coding: skip /// New cvc. /// Pass `nil` to keep existing value. @@ -18,13 +17,12 @@ public struct POCardUpdateRequest: Encodable { /// Preferred scheme defined by the Customer. This gets priority when processing the Transaction. /// Pass `nil` to keep existing value. - @POTypedRepresentation - public private(set) var preferredScheme: String? + public let preferredScheme: POCardScheme? /// Creates request instance. - public init(cardId: String, cvc: String? = nil, preferredScheme: String? = nil) { - self._cardId = .init(value: cardId) + public init(cardId: String, cvc: String? = nil, preferredScheme: POCardScheme? = nil) { + self.cardId = cardId self.cvc = cvc - self._preferredScheme = .init(wrappedValue: preferredScheme) + self.preferredScheme = preferredScheme } } diff --git a/Sources/ProcessOut/Sources/Repositories/Cards/Requests/POContact.swift b/Sources/ProcessOut/Sources/Repositories/Cards/Requests/POContact.swift index ab687b2ab..6edc20347 100644 --- a/Sources/ProcessOut/Sources/Repositories/Cards/Requests/POContact.swift +++ b/Sources/ProcessOut/Sources/Repositories/Cards/Requests/POContact.swift @@ -8,7 +8,7 @@ import Foundation /// Cardholder information. -public struct POContact: Encodable { +public struct POContact: Encodable, Sendable { /// First line of cardholder’s address. public let address1: String? diff --git a/Sources/ProcessOut/Sources/Repositories/Cards/Responses/CardTokenizationResponse.swift b/Sources/ProcessOut/Sources/Repositories/Cards/Responses/CardTokenizationResponse.swift deleted file mode 100644 index 163f9463f..000000000 --- a/Sources/ProcessOut/Sources/Repositories/Cards/Responses/CardTokenizationResponse.swift +++ /dev/null @@ -1,12 +0,0 @@ -// -// CardTokenizationResponse.swift -// ProcessOut -// -// Created by Julien.Rodrigues on 20/10/2022. -// - -import Foundation - -struct CardTokenizationResponse: Decodable { - let card: POCard -} diff --git a/Sources/ProcessOut/Sources/Repositories/Cards/Responses/POCard.swift b/Sources/ProcessOut/Sources/Repositories/Cards/Responses/POCard.swift index e6b953921..e014f4943 100644 --- a/Sources/ProcessOut/Sources/Repositories/Cards/Responses/POCard.swift +++ b/Sources/ProcessOut/Sources/Repositories/Cards/Responses/POCard.swift @@ -9,7 +9,7 @@ import Foundation /// A card object represents a credit or debit card. It contains many useful pieces of information about the card but /// it does not contain the full card number and CVC (which are kept securely in the ProcessOut Vault). -public struct POCard: Decodable, Hashable { +public struct POCard: Decodable, Hashable, Sendable { /// Value that uniquely identifies the card. public let id: String @@ -18,20 +18,16 @@ public struct POCard: Decodable, Hashable { public let projectId: String /// Scheme of the card. - @POTypedRepresentation - public private(set) var scheme: String + public let scheme: POCardScheme /// Co-scheme of the card, such as Carte Bancaire. - @POTypedRepresentation - public private(set) var coScheme: String? + public let coScheme: POCardScheme? /// Preferred scheme defined by the Customer. - @POTypedRepresentation - public private(set) var preferredScheme: String? + public let preferredScheme: POCardScheme? /// Card type. - @POFallbackDecodable - public private(set) var type: String + public let type: String? /// Name of the card’s issuing bank. public let bankName: String? @@ -50,8 +46,7 @@ public struct POCard: Decodable, Hashable { /// Hash value that remains the same for this card even if it is tokenized several times. /// - NOTE: fingerprint is empty string for Apple and Google Pay cards. - @POFallbackDecodable - public private(set) var fingerprint: String + public let fingerprint: String? /// Month of the expiration date. public let expMonth: Int @@ -60,8 +55,7 @@ public struct POCard: Decodable, Hashable { public let expYear: Int /// CVC check status. - @POTypedRepresentation - public var cvcCheck: String + public var cvcCheck: POCardCvcCheck /// AVS check status. public let avsCheck: String diff --git a/Sources/ProcessOut/Sources/Repositories/Cards/Responses/POCardIssuerInformation.swift b/Sources/ProcessOut/Sources/Repositories/Cards/Responses/POCardIssuerInformation.swift index 21aa03dfa..65c485c0f 100644 --- a/Sources/ProcessOut/Sources/Repositories/Cards/Responses/POCardIssuerInformation.swift +++ b/Sources/ProcessOut/Sources/Repositories/Cards/Responses/POCardIssuerInformation.swift @@ -6,15 +6,13 @@ // /// Holds information about card issuing institution that issued the card to the card holder. -public struct POCardIssuerInformation: Decodable { +public struct POCardIssuerInformation: Decodable, Sendable { /// Scheme of the card. - @POTypedRepresentation - public private(set) var scheme: String + public let scheme: POCardScheme /// Co-scheme of the card, such as Carte Bancaire. - @POTypedRepresentation - public private(set) var coScheme: String? + public let coScheme: POCardScheme? /// Card type. public let type: String? @@ -36,8 +34,8 @@ public struct POCardIssuerInformation: Decodable { brand: String? = nil, category: String? = nil ) { - self._scheme = .init(wrappedValue: scheme.rawValue) - self._coScheme = .init(wrappedValue: coScheme?.rawValue) + self.scheme = scheme + self.coScheme = coScheme self.type = type self.bankName = bankName self.brand = brand diff --git a/Sources/ProcessOut/Sources/Repositories/Cards/Responses/POCardScheme.swift b/Sources/ProcessOut/Sources/Repositories/Cards/Responses/POCardScheme.swift index f310bc307..b3513f8c1 100644 --- a/Sources/ProcessOut/Sources/Repositories/Cards/Responses/POCardScheme.swift +++ b/Sources/ProcessOut/Sources/Repositories/Cards/Responses/POCardScheme.swift @@ -20,6 +20,7 @@ public struct POCardScheme: Hashable, RawRepresentable, ExpressibleByStringLiter public let rawValue: String } +// Defined values are a subset of values defined in https://github.com/processout/norms/blob/master/card_schemes.go extension POCardScheme { /// Visa is the largest global card network in the world by transaction value, ubiquitous worldwide. @@ -61,6 +62,9 @@ extension POCardScheme { /// The Dankort is the national debit card of Denmark. public static let dankort: POCardScheme = "dankort" + /// A Mir payment card. + public static let mir: POCardScheme = "nspk mir" + /// Verve is Africa's most successful card brand. public static let verve: POCardScheme = "verve" @@ -71,20 +75,11 @@ extension POCardScheme { public static let cielo: POCardScheme = "cielo" /// Domestic debit and credit card brand of Brazil. - public static let elo: POCardScheme = "elp" + public static let elo: POCardScheme = "elo" /// Domestic debit and credit card brand of Brazil. public static let hipercard: POCardScheme = "hipercard" - /// Domestic debit and credit card brand of Brazil. - public static let ourocard: POCardScheme = "ourocard" - - /// Domestic debit and credit card brand of Brazil. - public static let aura: POCardScheme = "aura" - - /// Domestic debit and credit card brand of Brazil. - public static let comprocard: POCardScheme = "comprocard" - /// Cabal is a local credit and debit card payment method based in Argentina. public static let cabal: POCardScheme = "cabal" @@ -92,9 +87,6 @@ extension POCardScheme { /// financial institutions in the United States and Canada. public static let nyce: POCardScheme = "nyce" - /// Mastercard Cirrus is a worldwide interbank network that provides cash to Mastercard cardholders. - public static let cirrus: POCardScheme = "cirrus" - /// TROY (acronym of Türkiye’nin Ödeme Yöntemi) is a Turkish card scheme public static let troy: POCardScheme = "troy" @@ -103,9 +95,24 @@ extension POCardScheme { /// such as the German Girocard. public static let vPay: POCardScheme = "vpay" + /// A private label credit card is a type of credit card that is branded for a specific retailer or brand. + public static let privateLabel: POCardScheme = "private label" + + /// Mastercard Cirrus is a worldwide interbank network that provides cash to Mastercard cardholders. + public static let cirrus: POCardScheme = "cirrus" + + /// Domestic debit and credit card brand of Brazil. + public static let ourocard: POCardScheme = "ourocard" + /// Carnet is a leading brand of Mexican acceptance, with more than 50 years of experience. public static let carnet: POCardScheme = "carnet" + /// A private label credit card that is branded for Atos. + public static let atosPrivateLabel: POCardScheme = "atos private label" + + /// Domestic debit and credit card brand of Brazil. + public static let aura: POCardScheme = "aura" + /// GE Capital is the financial services division of General Electric. public static let geCapital: POCardScheme = "ge capital" @@ -121,65 +128,67 @@ extension POCardScheme { /// DinaCard is a national payment card of the Republic of Serbia. public static let dinaCard: POCardScheme = "dinacard" + /// Domestic debit and credit card brand of Brazil. + public static let comprocard: POCardScheme = "comprocard" + /// Mada is the national payment scheme of Saudi Arabia public static let mada: POCardScheme = "mada" /// Bancontact is the most popular online payment method in Belgium. public static let bancontact: POCardScheme = "bancontact" - /// Giropay is an Internet payment System in Germany - public static let giropay: POCardScheme = "giropay" + /// A Girocard payment method. + public static let girocard: POCardScheme = "girocard" - /// A private label credit card is a type of credit card that is branded for a specific retailer or brand. - public static let privateLabel: POCardScheme = "private label" + /// The Interac payment method. + public static let interac: POCardScheme = "interac" - /// A private label credit card that is branded for Atos. - public static let atosPrivateLabel: POCardScheme = "atos private label" + /// A Meeza payment card. + public static let meeza: POCardScheme = "meeza" - /// An Electron debit card. - public static let electron: POCardScheme = "electron" + /// A Nanaco payment card. + public static let nanaco: POCardScheme = "nanaco" - /// An iD payment card. - public static let idCredit: POCardScheme = "idCredit" + /// A Bancomat payment card. + public static let pagoBancomat: POCardScheme = "pagobancomat" - /// The Interac payment method. - public static let interac: POCardScheme = "interac" + /// A PostFinance AG payment card. + public static let postFinance: POCardScheme = "postfinance" /// A QUICPay payment card. - public static let quicPay: POCardScheme = "quicPay" + public static let quicPay: POCardScheme = "quicpay" /// A Suica payment card. public static let suica: POCardScheme = "suica" - /// A Girocard payment method. - public static let girocard: POCardScheme = "girocard" - - /// A Meeza payment card. - public static let meeza: POCardScheme = "meeza" - - /// A Bancomat payment card. - public static let pagoBancomat: POCardScheme = "pagoBancomat" - /// The TMoney card. public static let tmoney: POCardScheme = "tmoney" - /// A PostFinance AG payment card. - public static let postFinance: POCardScheme = "postFinance" - - /// A Nanaco payment card. - public static let nanaco: POCardScheme = "nanaco" - /// A WAON payment card. public static let waon: POCardScheme = "waon" +} - /// A Mir payment card. - public static let mir: POCardScheme = "nspk mir" +extension POCardScheme { + + /// Giropay is an Internet payment System in Germany + public static let giropay: POCardScheme = "giropay" + + /// An Electron debit card. + public static let electron: POCardScheme = "electron" + + /// An iD payment card. + public static let idCredit: POCardScheme = "idCredit" } -extension POCardScheme: Decodable { +extension POCardScheme: Codable { public init(from decoder: any Decoder) throws { let container = try decoder.singleValueContainer() rawValue = try container.decode(String.self) } + + public func encode(to encoder: any Encoder) throws { + var container = encoder.singleValueContainer() + try container.encode(self.rawValue) + } } diff --git a/Sources/ProcessOut/Sources/Repositories/CustomerTokens/Requests/POAssignCustomerTokenRequest.swift b/Sources/ProcessOut/Sources/Repositories/CustomerTokens/Requests/POAssignCustomerTokenRequest.swift index a59f2abe6..a53ce3f18 100644 --- a/Sources/ProcessOut/Sources/Repositories/CustomerTokens/Requests/POAssignCustomerTokenRequest.swift +++ b/Sources/ProcessOut/Sources/Repositories/CustomerTokens/Requests/POAssignCustomerTokenRequest.swift @@ -8,7 +8,7 @@ import Foundation /// Request to use to assign new source to existing customer token and potentially verify it. -public struct POAssignCustomerTokenRequest: Encodable { // sourcery: AutoCodingKeys +public struct POAssignCustomerTokenRequest: Encodable, Sendable { // sourcery: AutoCodingKeys /// Id of the customer who token belongs to. public let customerId: String // sourcery:coding: skip @@ -21,8 +21,7 @@ public struct POAssignCustomerTokenRequest: Encodable { // sourcery: AutoCodingK public let source: String /// Card scheme or co-scheme that should get priority if it is available. - @POTypedRepresentation - public private(set) var preferredScheme: String? + public let preferredScheme: POCardScheme? /// Boolean value that indicates whether token should be verified. Make sure to also pass valid /// ``POAssignCustomerTokenRequest/invoiceId`` if you want verification to happen. Default value @@ -32,35 +31,33 @@ public struct POAssignCustomerTokenRequest: Encodable { // sourcery: AutoCodingK /// Invoice identifier that will be used for token verification. public let invoiceId: String? - /// Boolean value used as flag that when set to `true` indicates that a request is coming directly - /// from the frontend. It is used to understand if we can instantly step-up to 3DS or not. - /// - /// Value is hardcoded to `true`. - @available(*, deprecated, message: "Property is an implementation detail and shouldn't be used.") - public let enableThreeDS2 = true // sourcery:coding: key="enable_three_d_s_2" - /// Can be used for a 3DS2 request to indicate which third party SDK is used for the call. public let thirdPartySdkVersion: String? /// Additional metadata. public let metadata: [String: String]? + /// Boolean value used as flag that when set to `true` indicates that a request is coming directly + /// from the frontend. It is used to understand if we can instantly step-up to 3DS or not. + /// + /// Value is hardcoded to `true`. + let enableThreeDS2 = true // sourcery:coding: key="enable_three_d_s_2" + /// Creates request instance. public init( customerId: String, tokenId: String, source: String, - preferredScheme: String? = nil, + preferredScheme: POCardScheme? = nil, verify: Bool = false, invoiceId: String? = nil, - enableThreeDS2 _: Bool = true, thirdPartySdkVersion: String? = nil, metadata: [String: String]? = nil ) { self.customerId = customerId self.tokenId = tokenId self.source = source - self._preferredScheme = .init(wrappedValue: preferredScheme) + self.preferredScheme = preferredScheme self.verify = verify self.invoiceId = invoiceId self.thirdPartySdkVersion = thirdPartySdkVersion diff --git a/Sources/ProcessOut/Sources/Repositories/CustomerTokens/Requests/POCreateCustomerTokenRequest.swift b/Sources/ProcessOut/Sources/Repositories/CustomerTokens/Requests/POCreateCustomerTokenRequest.swift index bab358afb..6daab328f 100644 --- a/Sources/ProcessOut/Sources/Repositories/CustomerTokens/Requests/POCreateCustomerTokenRequest.swift +++ b/Sources/ProcessOut/Sources/Repositories/CustomerTokens/Requests/POCreateCustomerTokenRequest.swift @@ -8,11 +8,10 @@ import Foundation @_spi(PO) -public struct POCreateCustomerTokenRequest: Encodable { +public struct POCreateCustomerTokenRequest: Encodable, Sendable { // sourcery: AutoCodingKeys /// Customer id to associate created token with. - @POImmutableExcludedCodable - public var customerId: String + public let customerId: String // sourcery:coding: skip /// Flag if you wish to verify the customer token by making zero value transaction. Applicable for cards only. public let verify: Bool @@ -24,9 +23,9 @@ public struct POCreateCustomerTokenRequest: Encodable { public let invoiceReturnUrl: URL? public init(customerId: String, verify: Bool = false, returnUrl: URL? = nil) { - self._customerId = .init(value: customerId) + self.customerId = customerId self.verify = verify - self.returnUrl = returnUrl self.invoiceReturnUrl = returnUrl + self.returnUrl = returnUrl } } diff --git a/Sources/ProcessOut/Sources/Repositories/CustomerTokens/Responses/AssignCustomerTokenResponse.swift b/Sources/ProcessOut/Sources/Repositories/CustomerTokens/Responses/AssignCustomerTokenResponse.swift index 88ef1f2df..fabfb2c71 100644 --- a/Sources/ProcessOut/Sources/Repositories/CustomerTokens/Responses/AssignCustomerTokenResponse.swift +++ b/Sources/ProcessOut/Sources/Repositories/CustomerTokens/Responses/AssignCustomerTokenResponse.swift @@ -5,7 +5,7 @@ // Created by Andrii Vysotskyi on 27.03.2023. // -struct AssignCustomerTokenResponse: Decodable { +struct AssignCustomerTokenResponse: Decodable, Sendable { /// Optional customer action. let customerAction: ThreeDSCustomerAction? diff --git a/Sources/ProcessOut/Sources/Repositories/CustomerTokens/Responses/POCustomerToken.swift b/Sources/ProcessOut/Sources/Repositories/CustomerTokens/Responses/POCustomerToken.swift index 26ae04ca9..d25f875ef 100644 --- a/Sources/ProcessOut/Sources/Repositories/CustomerTokens/Responses/POCustomerToken.swift +++ b/Sources/ProcessOut/Sources/Repositories/CustomerTokens/Responses/POCustomerToken.swift @@ -9,10 +9,10 @@ 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: Decodable { +public struct POCustomerToken: Decodable, Sendable { /// Customer token verification status. - public enum VerificationStatus: String, Decodable { + public enum VerificationStatus: String, Decodable, Sendable { case success, pending, failed, notRequested = "not-requested", unknown } diff --git a/Sources/ProcessOut/Sources/Repositories/GatewayConfigurations/POGatewayConfigurationsRepository.swift b/Sources/ProcessOut/Sources/Repositories/GatewayConfigurations/POGatewayConfigurationsRepository.swift index 5611c8ca7..1e42879d9 100644 --- a/Sources/ProcessOut/Sources/Repositories/GatewayConfigurations/POGatewayConfigurationsRepository.swift +++ b/Sources/ProcessOut/Sources/Repositories/GatewayConfigurations/POGatewayConfigurationsRepository.swift @@ -7,9 +7,6 @@ import Foundation -@available(*, deprecated, renamed: "POGatewayConfigurationsRepository") -public typealias POGatewayConfigurationsRepositoryType = POGatewayConfigurationsRepository - public protocol POGatewayConfigurationsRepository: PORepository { // sourcery: AutoCompletion /// Returns available gateway configurations. diff --git a/Sources/ProcessOut/Sources/Repositories/GatewayConfigurations/Requests/POAllGatewayConfigurationsRequest.swift b/Sources/ProcessOut/Sources/Repositories/GatewayConfigurations/Requests/POAllGatewayConfigurationsRequest.swift index fed58a4ad..ccc52976f 100644 --- a/Sources/ProcessOut/Sources/Repositories/GatewayConfigurations/Requests/POAllGatewayConfigurationsRequest.swift +++ b/Sources/ProcessOut/Sources/Repositories/GatewayConfigurations/Requests/POAllGatewayConfigurationsRequest.swift @@ -5,9 +5,9 @@ // Created by Andrii Vysotskyi on 12.10.2022. // -public struct POAllGatewayConfigurationsRequest { +public struct POAllGatewayConfigurationsRequest: Sendable { - public enum Filter: String { + public enum Filter: String, Sendable { /// Gateways that allow payments using alternative payment methods that allow tokenization. case alternativePaymentMethodsWithTokenization // swiftlint:disable:this identifier_name diff --git a/Sources/ProcessOut/Sources/Repositories/GatewayConfigurations/Requests/POFindGatewayConfigurationRequest.swift b/Sources/ProcessOut/Sources/Repositories/GatewayConfigurations/Requests/POFindGatewayConfigurationRequest.swift index d6ff8e717..d9aaee978 100644 --- a/Sources/ProcessOut/Sources/Repositories/GatewayConfigurations/Requests/POFindGatewayConfigurationRequest.swift +++ b/Sources/ProcessOut/Sources/Repositories/GatewayConfigurations/Requests/POFindGatewayConfigurationRequest.swift @@ -5,11 +5,9 @@ // Created by Andrii Vysotskyi on 27.10.2022. // -import Foundation +public struct POFindGatewayConfigurationRequest: Sendable { -public struct POFindGatewayConfigurationRequest { - - public enum ExpandedProperty: String, Hashable { + public enum ExpandedProperty: String, Hashable, Sendable { case gateway } diff --git a/Sources/ProcessOut/Sources/Repositories/GatewayConfigurations/Responses/POAllGatewayConfigurationsResponse.swift b/Sources/ProcessOut/Sources/Repositories/GatewayConfigurations/Responses/POAllGatewayConfigurationsResponse.swift index b08149e22..6ebe39edd 100644 --- a/Sources/ProcessOut/Sources/Repositories/GatewayConfigurations/Responses/POAllGatewayConfigurationsResponse.swift +++ b/Sources/ProcessOut/Sources/Repositories/GatewayConfigurations/Responses/POAllGatewayConfigurationsResponse.swift @@ -5,7 +5,7 @@ // Created by Andrii Vysotskyi on 12.10.2022. // -public struct POAllGatewayConfigurationsResponse: Decodable { +public struct POAllGatewayConfigurationsResponse: Decodable, Sendable { /// Boolean flag indicating whether there are more items to fetch. public let hasMore: Bool diff --git a/Sources/ProcessOut/Sources/Repositories/GatewayConfigurations/Responses/POGatewayConfiguration.swift b/Sources/ProcessOut/Sources/Repositories/GatewayConfigurations/Responses/POGatewayConfiguration.swift index 64aed6a3c..59adac0bf 100644 --- a/Sources/ProcessOut/Sources/Repositories/GatewayConfigurations/Responses/POGatewayConfiguration.swift +++ b/Sources/ProcessOut/Sources/Repositories/GatewayConfigurations/Responses/POGatewayConfiguration.swift @@ -7,15 +7,15 @@ import Foundation -public struct POGatewayConfiguration: Decodable { +public struct POGatewayConfiguration: Decodable, Sendable { - public struct NativeAlternativePaymentMethodConfig: Decodable { + public struct NativeAlternativePaymentMethodConfig: Decodable, Sendable { /// Configuration parameters. public let parameters: [PONativeAlternativePaymentMethodParameter] } - public struct Gateway: Decodable { + public struct Gateway: Decodable, Sendable { /// Name is the name of the payment gateway. public let name: String @@ -37,10 +37,6 @@ public struct POGatewayConfiguration: Decodable { /// Boolean flag that indicates whether gateway supports refunds. public let canRefund: Bool - - /// Native alternative payment method configuration. - @available(*, deprecated, message: "Use POInvoicesService/nativeAlternativePaymentMethodTransactionDetails(request:) instead.") // swiftlint:disable:this line_length - public let nativeApmConfig: NativeAlternativePaymentMethodConfig? } /// String value that uniquely identifies the configuration. diff --git a/Sources/ProcessOut/Sources/Repositories/Shared/Responses/POImageRemoteResource.swift b/Sources/ProcessOut/Sources/Repositories/Images/POImageRemoteResource.swift similarity index 79% rename from Sources/ProcessOut/Sources/Repositories/Shared/Responses/POImageRemoteResource.swift rename to Sources/ProcessOut/Sources/Repositories/Images/POImageRemoteResource.swift index a5d99c487..214ec4174 100644 --- a/Sources/ProcessOut/Sources/Repositories/Shared/Responses/POImageRemoteResource.swift +++ b/Sources/ProcessOut/Sources/Repositories/Images/POImageRemoteResource.swift @@ -8,9 +8,9 @@ import Foundation /// Image resource with light/dark image variations. -public struct POImageRemoteResource: Hashable, Decodable { +public struct POImageRemoteResource: Hashable, Decodable, Sendable { - public struct ResourceUrl: Hashable, Decodable { + public struct ResourceUrl: Hashable, Decodable, Sendable { /// Raster asset URLs. public let raster: URL diff --git a/Sources/ProcessOut/Sources/Repositories/Images/POImagesRepository.swift b/Sources/ProcessOut/Sources/Repositories/Images/POImagesRepository.swift index 036bfe10d..686ba6bdc 100644 --- a/Sources/ProcessOut/Sources/Repositories/Images/POImagesRepository.swift +++ b/Sources/ProcessOut/Sources/Repositories/Images/POImagesRepository.swift @@ -8,7 +8,8 @@ import Foundation import UIKit -@_spi(PO) public protocol POImagesRepository { // sourcery: AutoCompletion +@_spi(PO) +public protocol POImagesRepository: Sendable { // sourcery: AutoCompletion /// Attempts to download images at given URLs. func images(at urls: [URL], scale: CGFloat) async -> [URL: UIImage] diff --git a/Sources/ProcessOut/Sources/Repositories/Invoices/HttpInvoicesRepository.swift b/Sources/ProcessOut/Sources/Repositories/Invoices/HttpInvoicesRepository.swift index e802ae39c..1e5c8d717 100644 --- a/Sources/ProcessOut/Sources/Repositories/Invoices/HttpInvoicesRepository.swift +++ b/Sources/ProcessOut/Sources/Repositories/Invoices/HttpInvoicesRepository.swift @@ -30,14 +30,24 @@ final class HttpInvoicesRepository: InvoicesRepository { func initiatePayment( request: PONativeAlternativePaymentMethodRequest ) async throws -> PONativeAlternativePaymentMethodResponse { - let requestBox = NativeAlternativePaymentRequestBox( + struct Request: Encodable { + struct NativeApm: Encodable { // swiftlint:disable:this nesting + let parameterValues: [String: String] + } + let gatewayConfigurationId: String + let nativeApm: NativeApm + } + struct Response: Decodable { + let nativeApm: PONativeAlternativePaymentMethodResponse + } + let requestBox = Request( gatewayConfigurationId: request.gatewayConfigurationId, nativeApm: .init(parameterValues: request.parameters) ) - let httpRequest = HttpConnectorRequest.post( + let httpRequest = HttpConnectorRequest.post( path: "/invoices/\(request.invoiceId)/native-payment", body: requestBox ) - return try await connector.execute(request: httpRequest) + return try await connector.execute(request: httpRequest).nativeApm } func invoice(request: POInvoiceRequest) async throws -> POInvoice { @@ -73,11 +83,17 @@ final class HttpInvoicesRepository: InvoicesRepository { func captureNativeAlternativePayment( request: NativeAlternativePaymentCaptureRequest - ) async throws -> PONativeAlternativePaymentMethodResponse { - let httpRequest = HttpConnectorRequest.post( + ) async throws -> PONativeAlternativePaymentMethodState { + struct Response: Decodable { + struct NativeApm: Decodable { // swiftlint:disable:this nesting + let state: PONativeAlternativePaymentMethodState + } + let nativeApm: NativeApm + } + let httpRequest = HttpConnectorRequest.post( path: "/invoices/\(request.invoiceId)/capture", body: request ) - return try await connector.execute(request: httpRequest) + return try await connector.execute(request: httpRequest).nativeApm.state } func createInvoice(request: POInvoiceCreationRequest) async throws -> POInvoice { @@ -92,16 +108,6 @@ final class HttpInvoicesRepository: InvoicesRepository { return response.value.invoice.replacing(clientSecret: clientSecret) } - // MARK: - Private Nested Types - - private struct NativeAlternativePaymentRequestBox: Encodable { - struct NativeApm: Encodable { // swiftlint:disable:this nesting - let parameterValues: [String: String] - } - let gatewayConfigurationId: String - let nativeApm: NativeApm - } - // MARK: - Private Properties private let connector: HttpConnector diff --git a/Sources/ProcessOut/Sources/Repositories/Invoices/InvoicesRepository.swift b/Sources/ProcessOut/Sources/Repositories/Invoices/InvoicesRepository.swift index 00950168a..281c4ae5a 100644 --- a/Sources/ProcessOut/Sources/Repositories/Invoices/InvoicesRepository.swift +++ b/Sources/ProcessOut/Sources/Repositories/Invoices/InvoicesRepository.swift @@ -29,7 +29,7 @@ protocol InvoicesRepository: PORepository { /// Captures native alternative payment. func captureNativeAlternativePayment( request: NativeAlternativePaymentCaptureRequest - ) async throws -> PONativeAlternativePaymentMethodResponse + ) async throws -> PONativeAlternativePaymentMethodState /// Creates invoice with given parameters. func createInvoice(request: POInvoiceCreationRequest) async throws -> POInvoice diff --git a/Sources/ProcessOut/Sources/Repositories/Invoices/Requests/AlternativePayment/NativeAlternativePaymentCaptureRequest.swift b/Sources/ProcessOut/Sources/Repositories/Invoices/Requests/AlternativePayment/NativeAlternativePaymentCaptureRequest.swift index 557752683..473097984 100644 --- a/Sources/ProcessOut/Sources/Repositories/Invoices/Requests/AlternativePayment/NativeAlternativePaymentCaptureRequest.swift +++ b/Sources/ProcessOut/Sources/Repositories/Invoices/Requests/AlternativePayment/NativeAlternativePaymentCaptureRequest.swift @@ -5,17 +5,11 @@ // Created by Andrii Vysotskyi on 16.12.2022. // -struct NativeAlternativePaymentCaptureRequest: Encodable { +struct NativeAlternativePaymentCaptureRequest: Encodable, Sendable { // sourcery: AutoCodingKeys /// Invoice identifier. - @POImmutableExcludedCodable - var invoiceId: String + let invoiceId: String // sourcery:coding: skip /// Source must be set to gateway configuration id that was used to initiate native alternative payment. let source: String - - init(invoiceId: String, source: String) { - self._invoiceId = .init(value: invoiceId) - self.source = source - } } diff --git a/Sources/ProcessOut/Sources/Repositories/Invoices/Requests/AlternativePayment/PONativeAlternativePaymentMethodRequest.swift b/Sources/ProcessOut/Sources/Repositories/Invoices/Requests/AlternativePayment/PONativeAlternativePaymentMethodRequest.swift index 17b522a6f..1cddcca60 100644 --- a/Sources/ProcessOut/Sources/Repositories/Invoices/Requests/AlternativePayment/PONativeAlternativePaymentMethodRequest.swift +++ b/Sources/ProcessOut/Sources/Repositories/Invoices/Requests/AlternativePayment/PONativeAlternativePaymentMethodRequest.swift @@ -5,7 +5,7 @@ // Created by Andrii Vysotskyi on 17.10.2022. // -public struct PONativeAlternativePaymentMethodRequest { +public struct PONativeAlternativePaymentMethodRequest: Sendable { /// Invoice id. public let invoiceId: String diff --git a/Sources/ProcessOut/Sources/Repositories/Invoices/Requests/AlternativePayment/PONativeAlternativePaymentMethodTransactionDetailsRequest.swift b/Sources/ProcessOut/Sources/Repositories/Invoices/Requests/AlternativePayment/PONativeAlternativePaymentMethodTransactionDetailsRequest.swift index 8fdabeb3b..faf399fc7 100644 --- a/Sources/ProcessOut/Sources/Repositories/Invoices/Requests/AlternativePayment/PONativeAlternativePaymentMethodTransactionDetailsRequest.swift +++ b/Sources/ProcessOut/Sources/Repositories/Invoices/Requests/AlternativePayment/PONativeAlternativePaymentMethodTransactionDetailsRequest.swift @@ -5,9 +5,7 @@ // Created by Andrii Vysotskyi on 01.12.2022. // -import Foundation - -public struct PONativeAlternativePaymentMethodTransactionDetailsRequest { // swiftlint:disable:this type_name +public struct PONativeAlternativePaymentMethodTransactionDetailsRequest: Sendable { // swiftlint:disable:this type_name /// Invoice identifier. public let invoiceId: String diff --git a/Sources/ProcessOut/Sources/Repositories/Invoices/Requests/POInvoiceAuthorizationRequest.swift b/Sources/ProcessOut/Sources/Repositories/Invoices/Requests/POInvoiceAuthorizationRequest.swift index 3a89ba927..f0015b844 100644 --- a/Sources/ProcessOut/Sources/Repositories/Invoices/Requests/POInvoiceAuthorizationRequest.swift +++ b/Sources/ProcessOut/Sources/Repositories/Invoices/Requests/POInvoiceAuthorizationRequest.swift @@ -7,7 +7,7 @@ import Foundation -public struct POInvoiceAuthorizationRequest: Encodable { // sourcery: AutoCodingKeys +public struct POInvoiceAuthorizationRequest: Encodable, Sendable { // sourcery: AutoCodingKeys /// Invoice identifier to to perform authorization for. public let invoiceId: String // sourcery:coding: skip @@ -22,16 +22,8 @@ public struct POInvoiceAuthorizationRequest: Encodable { // sourcery: AutoCoding /// Boolean value indicating if authorization is incremental. Default value is `false`. public let incremental: Bool - /// Boolean value used as flag that when set to `true` indicates that a request is coming directly - /// from the frontend. It is used to understand if we can instantly step-up to 3DS or not. - /// - /// Value is hardcoded to `true`. - @available(*, deprecated, message: "Property is an implementation detail and shouldn't be used.") - public let enableThreeDS2 = true // sourcery:coding: key="enable_three_d_s_2" - /// Card scheme or co-scheme that should get priority if it is available. - @POTypedRepresentation - public private(set) var preferredScheme: String? + public let preferredScheme: POCardScheme? /// Can be used for a 3DS2 request to indicate which third party SDK is used for the call. public let thirdPartySdkVersion: String? @@ -52,8 +44,8 @@ public struct POInvoiceAuthorizationRequest: Encodable { // sourcery: AutoCoding /// Amount of money to capture when partial captures are available. Note that this only applies if you are /// also using the `autoCaptureAt` option. - @POImmutableStringCodableOptionalDecimal - public var captureAmount: Decimal? + @POStringCodableOptionalDecimal + public private(set) var captureAmount: Decimal? /// Set to true if you want to authorize payment without capturing. Note that you must capture the payment on /// the server if you use this option. Default value is `true`. @@ -70,13 +62,18 @@ public struct POInvoiceAuthorizationRequest: Encodable { // sourcery: AutoCoding /// Operation metadata. public let metadata: [String: String]? + /// Boolean value used as flag that when set to `true` indicates that a request is coming directly + /// from the frontend. It is used to understand if we can instantly step-up to 3DS or not. + /// + /// Value is hardcoded to `true`. + let enableThreeDS2 = true // sourcery:coding: key="enable_three_d_s_2" + public init( invoiceId: String, source: String, saveSource: Bool = false, incremental: Bool = false, - enableThreeDS2 _: Bool = true, - preferredScheme: String? = nil, + preferredScheme: POCardScheme? = nil, thirdPartySdkVersion: String? = nil, invoiceDetailIds: [String]? = nil, overrideMacBlocking: Bool = false, @@ -92,7 +89,7 @@ public struct POInvoiceAuthorizationRequest: Encodable { // sourcery: AutoCoding self.source = source self.saveSource = saveSource self.incremental = incremental - self._preferredScheme = .init(wrappedValue: preferredScheme) + self.preferredScheme = preferredScheme self.thirdPartySdkVersion = thirdPartySdkVersion self.invoiceDetailIds = invoiceDetailIds self.overrideMacBlocking = overrideMacBlocking diff --git a/Sources/ProcessOut/Sources/Repositories/Invoices/Requests/POInvoiceCreationRequest.swift b/Sources/ProcessOut/Sources/Repositories/Invoices/Requests/POInvoiceCreationRequest.swift index b2b939775..857f4d3ea 100644 --- a/Sources/ProcessOut/Sources/Repositories/Invoices/Requests/POInvoiceCreationRequest.swift +++ b/Sources/ProcessOut/Sources/Repositories/Invoices/Requests/POInvoiceCreationRequest.swift @@ -8,7 +8,7 @@ import Foundation @_spi(PO) -public struct POInvoiceCreationRequest: Encodable { +public struct POInvoiceCreationRequest: Encodable, Sendable { /// Name of the invoice (often an internal ID code from the merchant’s systems). Maximum 80 characters long. public let name: String diff --git a/Sources/ProcessOut/Sources/Repositories/Invoices/Requests/POInvoiceRequest.swift b/Sources/ProcessOut/Sources/Repositories/Invoices/Requests/POInvoiceRequest.swift index 618892adb..d21cfb41e 100644 --- a/Sources/ProcessOut/Sources/Repositories/Invoices/Requests/POInvoiceRequest.swift +++ b/Sources/ProcessOut/Sources/Repositories/Invoices/Requests/POInvoiceRequest.swift @@ -6,7 +6,7 @@ // /// Request to get single invoice details. -public struct POInvoiceRequest { +public struct POInvoiceRequest: Sendable { /// Requested invoice ID. public let invoiceId: String diff --git a/Sources/ProcessOut/Sources/Repositories/Shared/Responses/PONativeAlternativePaymentMethodParameter.swift b/Sources/ProcessOut/Sources/Repositories/Invoices/Responses/AlternativePayment/PONativeAlternativePaymentMethodParameter.swift similarity index 91% rename from Sources/ProcessOut/Sources/Repositories/Shared/Responses/PONativeAlternativePaymentMethodParameter.swift rename to Sources/ProcessOut/Sources/Repositories/Invoices/Responses/AlternativePayment/PONativeAlternativePaymentMethodParameter.swift index 5dc830e70..e96f25f61 100644 --- a/Sources/ProcessOut/Sources/Repositories/Shared/Responses/PONativeAlternativePaymentMethodParameter.swift +++ b/Sources/ProcessOut/Sources/Repositories/Invoices/Responses/AlternativePayment/PONativeAlternativePaymentMethodParameter.swift @@ -7,9 +7,9 @@ import Foundation -public struct PONativeAlternativePaymentMethodParameter: Decodable { +public struct PONativeAlternativePaymentMethodParameter: Decodable, Sendable { - public enum ParameterType: String, Decodable, Hashable { + public enum ParameterType: String, Decodable, Hashable, Sendable { /// For numeric only fields. case numeric @@ -28,7 +28,7 @@ public struct PONativeAlternativePaymentMethodParameter: Decodable { } /// Describes available value. - public struct AvailableValue: Decodable, Hashable { + public struct AvailableValue: Decodable, Hashable, Sendable { /// Display name of value. public let displayName: String diff --git a/Sources/ProcessOut/Sources/Repositories/Invoices/Responses/AlternativePayment/PONativeAlternativePaymentMethodParameterValues.swift b/Sources/ProcessOut/Sources/Repositories/Invoices/Responses/AlternativePayment/PONativeAlternativePaymentMethodParameterValues.swift index 29eedc589..c7e81108e 100644 --- a/Sources/ProcessOut/Sources/Repositories/Invoices/Responses/AlternativePayment/PONativeAlternativePaymentMethodParameterValues.swift +++ b/Sources/ProcessOut/Sources/Repositories/Invoices/Responses/AlternativePayment/PONativeAlternativePaymentMethodParameterValues.swift @@ -8,7 +8,7 @@ import Foundation /// Native alternative payment parameter values. -public struct PONativeAlternativePaymentMethodParameterValues: Decodable { +public struct PONativeAlternativePaymentMethodParameterValues: Decodable, Sendable { /// Message. public let message: String? diff --git a/Sources/ProcessOut/Sources/Repositories/Invoices/Responses/AlternativePayment/PONativeAlternativePaymentMethodResponse.swift b/Sources/ProcessOut/Sources/Repositories/Invoices/Responses/AlternativePayment/PONativeAlternativePaymentMethodResponse.swift index 665389c00..142ffd3d5 100644 --- a/Sources/ProcessOut/Sources/Repositories/Invoices/Responses/AlternativePayment/PONativeAlternativePaymentMethodResponse.swift +++ b/Sources/ProcessOut/Sources/Repositories/Invoices/Responses/AlternativePayment/PONativeAlternativePaymentMethodResponse.swift @@ -7,24 +7,15 @@ import Foundation -public struct PONativeAlternativePaymentMethodResponse: Decodable { +public struct PONativeAlternativePaymentMethodResponse: Decodable, Sendable { - @available(*, deprecated, message: "Use PONativeAlternativePaymentMethodParameterValues directly.") - public typealias NativeAlternativePaymentMethodParameterValues = PONativeAlternativePaymentMethodParameterValues + /// Payment's state. + public let state: PONativeAlternativePaymentMethodState - public struct NativeApm: Decodable { + /// Contains details about the additional information you need to collect from your customer before creating the + /// payment request. + public let parameterDefinitions: [PONativeAlternativePaymentMethodParameter]? - /// Payment's state. - public let state: PONativeAlternativePaymentMethodState - - /// Contains details about the additional information you need to collect from your customer before creating the - /// payment request. - public let parameterDefinitions: [PONativeAlternativePaymentMethodParameter]? - - /// Additional information about payment step. - public let parameterValues: PONativeAlternativePaymentMethodParameterValues? - } - - /// Details for alternative payment method. - public let nativeApm: NativeApm + /// Additional information about payment step. + public let parameterValues: PONativeAlternativePaymentMethodParameterValues? } diff --git a/Sources/ProcessOut/Sources/Repositories/Invoices/Responses/AlternativePayment/PONativeAlternativePaymentMethodState.swift b/Sources/ProcessOut/Sources/Repositories/Invoices/Responses/AlternativePayment/PONativeAlternativePaymentMethodState.swift index 799df0b66..14971d743 100644 --- a/Sources/ProcessOut/Sources/Repositories/Invoices/Responses/AlternativePayment/PONativeAlternativePaymentMethodState.swift +++ b/Sources/ProcessOut/Sources/Repositories/Invoices/Responses/AlternativePayment/PONativeAlternativePaymentMethodState.swift @@ -7,7 +7,7 @@ import Foundation -public enum PONativeAlternativePaymentMethodState: String, Decodable { +public enum PONativeAlternativePaymentMethodState: String, Decodable, Sendable { /// Additional input is required. case customerInput = "CUSTOMER_INPUT" diff --git a/Sources/ProcessOut/Sources/Repositories/Invoices/Responses/AlternativePayment/PONativeAlternativePaymentMethodTransactionDetails.swift b/Sources/ProcessOut/Sources/Repositories/Invoices/Responses/AlternativePayment/PONativeAlternativePaymentMethodTransactionDetails.swift index 8b3d5abd8..208a759f4 100644 --- a/Sources/ProcessOut/Sources/Repositories/Invoices/Responses/AlternativePayment/PONativeAlternativePaymentMethodTransactionDetails.swift +++ b/Sources/ProcessOut/Sources/Repositories/Invoices/Responses/AlternativePayment/PONativeAlternativePaymentMethodTransactionDetails.swift @@ -7,10 +7,10 @@ import Foundation -public struct PONativeAlternativePaymentMethodTransactionDetails: Decodable { +public struct PONativeAlternativePaymentMethodTransactionDetails: Decodable, Sendable { /// Payment gateway information. - public struct Gateway { + public struct Gateway: Sendable { /// Name of the payment gateway that can be displayed. public let displayName: String @@ -27,11 +27,11 @@ public struct PONativeAlternativePaymentMethodTransactionDetails: Decodable { } /// Invoice details. - public struct Invoice: Decodable { + public struct Invoice: Decodable, Sendable { /// Invoice amount. - @POImmutableStringCodableDecimal - public var amount: Decimal + @POStringCodableDecimal + public private(set) var amount: Decimal /// Invoice currency code. public let currencyCode: String @@ -63,7 +63,7 @@ extension PONativeAlternativePaymentMethodTransactionDetails.Gateway: Decodable // Escapes plain text action message and stores as a markdown. customerActionMessage = try container .decodeIfPresent(String.self, forKey: .customerActionMessage) - .map(MarkdownParser.escaped) + .map(Self.escaped(plainText:)) } // MARK: - Private Nested Types @@ -71,4 +71,20 @@ extension PONativeAlternativePaymentMethodTransactionDetails.Gateway: Decodable private enum CodingKeys: String, CodingKey { case displayName, logoUrl, customerActionImageUrl, customerActionMessage } + + // MARK: - Private Methods + + /// Escapes given plain text so it can be represented as is, in markdown. + private static func escaped(plainText: String) -> String { + let specialCharacters = CharacterSet(charactersIn: "\\`*_{}[]()#+-.!") + var markdown = String() + markdown.reserveCapacity(plainText.count) + for character in plainText { + if character.unicodeScalars.allSatisfy(specialCharacters.contains) { + markdown += "\\" + } + markdown += String(character) + } + return markdown + } } diff --git a/Sources/ProcessOut/Sources/Repositories/Shared/Responses/POBillingAddressCollectionMode.swift b/Sources/ProcessOut/Sources/Repositories/Invoices/Responses/DynamicCheckout/POBillingAddressCollectionMode.swift similarity index 83% rename from Sources/ProcessOut/Sources/Repositories/Shared/Responses/POBillingAddressCollectionMode.swift rename to Sources/ProcessOut/Sources/Repositories/Invoices/Responses/DynamicCheckout/POBillingAddressCollectionMode.swift index f4c8106c1..564807aae 100644 --- a/Sources/ProcessOut/Sources/Repositories/Shared/Responses/POBillingAddressCollectionMode.swift +++ b/Sources/ProcessOut/Sources/Repositories/Invoices/Responses/DynamicCheckout/POBillingAddressCollectionMode.swift @@ -6,7 +6,7 @@ // /// Billing address collection modes. -public enum POBillingAddressCollectionMode: String, Decodable { +public enum POBillingAddressCollectionMode: String, Decodable, Sendable { /// Only collect address components that are needed for particular payment method. case automatic diff --git a/Sources/ProcessOut/Sources/Repositories/Invoices/Responses/PODynamicCheckoutPaymentMethod.swift b/Sources/ProcessOut/Sources/Repositories/Invoices/Responses/DynamicCheckout/PODynamicCheckoutPaymentMethod.swift similarity index 88% rename from Sources/ProcessOut/Sources/Repositories/Invoices/Responses/PODynamicCheckoutPaymentMethod.swift rename to Sources/ProcessOut/Sources/Repositories/Invoices/Responses/DynamicCheckout/PODynamicCheckoutPaymentMethod.swift index 2f01da7c8..8ce253368 100644 --- a/Sources/ProcessOut/Sources/Repositories/Invoices/Responses/PODynamicCheckoutPaymentMethod.swift +++ b/Sources/ProcessOut/Sources/Repositories/Invoices/Responses/DynamicCheckout/PODynamicCheckoutPaymentMethod.swift @@ -13,11 +13,11 @@ import PassKit /// /// - Warning: New cases may be added in future minor releases. @_spi(PO) -public enum PODynamicCheckoutPaymentMethod { +public enum PODynamicCheckoutPaymentMethod: Sendable { // MARK: - Apple Pay - public struct ApplePay: Decodable { // sourcery: AutoCodingKeys + public struct ApplePay: Decodable, Sendable { // sourcery: AutoCodingKeys /// Payment method ID. @_spi(PO) @@ -32,7 +32,7 @@ public enum PODynamicCheckoutPaymentMethod { public let configuration: ApplePayConfiguration // sourcery:coding: key="applepay" } - public struct ApplePayConfiguration: Decodable { + public struct ApplePayConfiguration: Decodable, Sendable { /// Merchant ID. public let merchantId: String @@ -42,7 +42,7 @@ public enum PODynamicCheckoutPaymentMethod { /// Merchant capabilities. @POStringDecodableMerchantCapability - public var merchantCapabilities: PKMerchantCapability + public private(set) var merchantCapabilities: PKMerchantCapability /// The payment methods that are supported. public let supportedNetworks: Set @@ -50,7 +50,7 @@ public enum PODynamicCheckoutPaymentMethod { // MARK: - Native APM - public struct NativeAlternativePayment: Decodable { // sourcery: AutoCodingKeys + public struct NativeAlternativePayment: Decodable, Sendable { // sourcery: AutoCodingKeys /// Payment method ID. @_spi(PO) @@ -65,7 +65,7 @@ public enum PODynamicCheckoutPaymentMethod { public let configuration: NativeAlternativePaymentConfiguration // sourcery:coding: key="apm" } - public struct NativeAlternativePaymentConfiguration: Decodable { + public struct NativeAlternativePaymentConfiguration: Decodable, Sendable { /// Gateway configuration ID. public let gatewayConfigurationId: String @@ -73,7 +73,7 @@ public enum PODynamicCheckoutPaymentMethod { // MARK: - APM - public struct AlternativePayment: Decodable { // sourcery: AutoCodingKeys + public struct AlternativePayment: Decodable, Sendable { // sourcery: AutoCodingKeys /// Payment method ID. @_spi(PO) @@ -91,7 +91,7 @@ public enum PODynamicCheckoutPaymentMethod { public let configuration: AlternativePaymentConfiguration // sourcery:coding: key="apm" } - public struct AlternativePaymentConfiguration: Decodable { + public struct AlternativePaymentConfiguration: Decodable, Sendable { /// Gateway configuration ID. public let gatewayConfigurationId: String @@ -102,7 +102,7 @@ public enum PODynamicCheckoutPaymentMethod { // MARK: - Card - public struct Card: Decodable { // sourcery: AutoCodingKeys + public struct Card: Decodable, Sendable { // sourcery: AutoCodingKeys /// Payment method ID. @_spi(PO) @@ -115,7 +115,7 @@ public enum PODynamicCheckoutPaymentMethod { public let configuration: CardConfiguration // sourcery:coding: key="card" } - public struct CardConfiguration: Decodable { + public struct CardConfiguration: Decodable, Sendable { /// Defines whether user will be asked to select scheme if co-scheme is available. let schemeSelectionAllowed: Bool @@ -134,7 +134,7 @@ public enum PODynamicCheckoutPaymentMethod { public let billingAddress: BillingAddressConfiguration } - public struct BillingAddressConfiguration: Decodable { + public struct BillingAddressConfiguration: Decodable, Sendable { /// List of ISO country codes that is supported for the billing address. When nil, all countries are supported. public let restrictToCountryCodes: Set? @@ -145,7 +145,7 @@ public enum PODynamicCheckoutPaymentMethod { // MARK: - Customer Tokens - public struct CustomerToken { + public struct CustomerToken: Sendable { /// Payment method ID. @_spi(PO) @@ -163,7 +163,7 @@ public enum PODynamicCheckoutPaymentMethod { public let configuration: CustomerTokenConfiguration } - public struct CustomerTokenConfiguration: Decodable { + public struct CustomerTokenConfiguration: Decodable, Sendable { /// Customer token ID. public let customerTokenId: String @@ -174,7 +174,7 @@ public enum PODynamicCheckoutPaymentMethod { // MARK: - Unknown - public struct Unknown { + public struct Unknown: Sendable { /// Transient ID assigned to method during decoding. @_spi(PO) @@ -186,7 +186,7 @@ public enum PODynamicCheckoutPaymentMethod { // MARK: - Common - public struct Display: Decodable { + public struct Display: Decodable, @unchecked Sendable { /// Display name. public let name: String @@ -198,7 +198,7 @@ public enum PODynamicCheckoutPaymentMethod { public private(set) var brandColor: UIColor } - public enum Flow: String, Decodable { + public enum Flow: String, Decodable, Sendable { case express } diff --git a/Sources/ProcessOut/Sources/Repositories/Invoices/Responses/POStringDecodableMerchantCapability.swift b/Sources/ProcessOut/Sources/Repositories/Invoices/Responses/DynamicCheckout/POStringDecodableMerchantCapability.swift similarity index 91% rename from Sources/ProcessOut/Sources/Repositories/Invoices/Responses/POStringDecodableMerchantCapability.swift rename to Sources/ProcessOut/Sources/Repositories/Invoices/Responses/DynamicCheckout/POStringDecodableMerchantCapability.swift index c7c23a735..47c58a0bf 100644 --- a/Sources/ProcessOut/Sources/Repositories/Invoices/Responses/POStringDecodableMerchantCapability.swift +++ b/Sources/ProcessOut/Sources/Repositories/Invoices/Responses/DynamicCheckout/POStringDecodableMerchantCapability.swift @@ -9,9 +9,9 @@ import PassKit /// Property wrapper allowing to decode `PKMerchantCapability`. @propertyWrapper -public struct POStringDecodableMerchantCapability: Decodable { +public struct POStringDecodableMerchantCapability: Decodable, Sendable { - public let wrappedValue: PKMerchantCapability + public var wrappedValue: PKMerchantCapability // MARK: - Decodable diff --git a/Sources/ProcessOut/Sources/Repositories/Invoices/Responses/POInvoice.swift b/Sources/ProcessOut/Sources/Repositories/Invoices/Responses/POInvoice.swift index 5947341cf..0f79cedf2 100644 --- a/Sources/ProcessOut/Sources/Repositories/Invoices/Responses/POInvoice.swift +++ b/Sources/ProcessOut/Sources/Repositories/Invoices/Responses/POInvoice.swift @@ -8,13 +8,14 @@ import Foundation /// Invoice details. -public struct POInvoice: Decodable { +public struct POInvoice: Decodable, Sendable { /// String value that uniquely identifies this invoice. public let id: String - @POImmutableStringCodableDecimal - public var amount: Decimal + /// Invoice amount. + @POStringCodableDecimal + public private(set) var amount: Decimal /// Invoice currency. public let currency: String diff --git a/Sources/ProcessOut/Sources/Repositories/Shared/Responses/ThreeDSCustomerAction.swift b/Sources/ProcessOut/Sources/Repositories/Invoices/Responses/ThreeDSCustomerAction.swift similarity index 85% rename from Sources/ProcessOut/Sources/Repositories/Shared/Responses/ThreeDSCustomerAction.swift rename to Sources/ProcessOut/Sources/Repositories/Invoices/Responses/ThreeDSCustomerAction.swift index 9bc0f87eb..adfff3869 100644 --- a/Sources/ProcessOut/Sources/Repositories/Shared/Responses/ThreeDSCustomerAction.swift +++ b/Sources/ProcessOut/Sources/Repositories/Invoices/Responses/ThreeDSCustomerAction.swift @@ -7,9 +7,9 @@ import Foundation -struct ThreeDSCustomerAction: Decodable { +struct ThreeDSCustomerAction: Decodable, Sendable { - enum ActionType: String, Decodable { + enum ActionType: String, Decodable, Sendable { /// Device fingerprint is required. case fingerprintMobile = "fingerprint-mobile" diff --git a/Sources/ProcessOut/Sources/Repositories/Shared/Decorators/HttpConnectorError/FailureMapper/HttpConnectorFailureMapper.swift b/Sources/ProcessOut/Sources/Repositories/Shared/Decorators/HttpConnectorError/FailureMapper/HttpConnectorFailureMapper.swift index d3d75a2f7..091692af8 100644 --- a/Sources/ProcessOut/Sources/Repositories/Shared/Decorators/HttpConnectorError/FailureMapper/HttpConnectorFailureMapper.swift +++ b/Sources/ProcessOut/Sources/Repositories/Shared/Decorators/HttpConnectorError/FailureMapper/HttpConnectorFailureMapper.swift @@ -5,7 +5,7 @@ // Created by Andrii Vysotskyi on 16.10.2022. // -protocol HttpConnectorFailureMapper { +protocol HttpConnectorFailureMapper: Sendable { /// Creates `POFailure` with given ``HttpConnectorFailure`` instance. func failure(from failure: HttpConnectorFailure) -> POFailure diff --git a/Sources/ProcessOut/Sources/Repositories/Shared/PORepository.swift b/Sources/ProcessOut/Sources/Repositories/Shared/PORepository.swift index 892a71a00..156f2e818 100644 --- a/Sources/ProcessOut/Sources/Repositories/Shared/PORepository.swift +++ b/Sources/ProcessOut/Sources/Repositories/Shared/PORepository.swift @@ -5,11 +5,8 @@ // Created by Andrii Vysotskyi on 12.10.2022. // -@available(*, deprecated, renamed: "PORepository") -public typealias PORepositoryType = PORepository - /// Common protocol that all repositories conform to. -public protocol PORepository { +public protocol PORepository: Sendable { /// Repository's failure type. typealias Failure = POFailure diff --git a/Sources/ProcessOut/Sources/Repositories/Shared/Requests/PaginationOptions/POPaginationOptions.swift b/Sources/ProcessOut/Sources/Repositories/Shared/Requests/PaginationOptions/POPaginationOptions.swift index b02e1360e..7ab96e4b1 100644 --- a/Sources/ProcessOut/Sources/Repositories/Shared/Requests/PaginationOptions/POPaginationOptions.swift +++ b/Sources/ProcessOut/Sources/Repositories/Shared/Requests/PaginationOptions/POPaginationOptions.swift @@ -7,13 +7,13 @@ import Foundation -public struct POPaginationOptions { +public struct POPaginationOptions: Sendable { - public enum Order: String { + public enum Order: String, Sendable { case ascending = "asc", descending = "desc" } - public enum Position { + public enum Position: Sendable { case after(String), before(String) } diff --git a/Sources/ProcessOut/Sources/Repositories/Shared/Responses/POFailure.swift b/Sources/ProcessOut/Sources/Repositories/Shared/Responses/POFailure.swift index 73efe7145..4d3236aa6 100644 --- a/Sources/ProcessOut/Sources/Repositories/Shared/Responses/POFailure.swift +++ b/Sources/ProcessOut/Sources/Repositories/Shared/Responses/POFailure.swift @@ -10,9 +10,9 @@ import Foundation /// Information about an error that occurred. -public struct POFailure: Error { +public struct POFailure: Error, Sendable { - public struct InvalidField: Decodable { + public struct InvalidField: Decodable, Sendable { /// Field name. public let name: String @@ -26,17 +26,17 @@ public struct POFailure: Error { } } - public enum InternalCode: String { + public enum InternalCode: String, Sendable { case gateway = "gateway-internal-error" case mobile = "processout-mobile.internal" } - public enum TimeoutCode: String { + public enum TimeoutCode: String, Sendable { case gateway = "gateway.timeout" case mobile = "processout-mobile.timeout" } - public enum ValidationCode: String { + public enum ValidationCode: String, Sendable { case general = "request.validation.error" case gateway = "gateway.validation-error" case invalidAddress = "request.validation.invalid-address" @@ -88,7 +88,7 @@ public struct POFailure: Error { case missingType = "request.validation.missing-type" } - public enum NotFoundCode: String { + public enum NotFoundCode: String, Sendable { case activity = "resource.activity.not-found" case addon = "resource.addon.not-found" case alert = "resource.alert.not-found" @@ -127,12 +127,12 @@ public struct POFailure: Error { case webhookEndpoint = "resource.webhook-endpoint.not-found" } - public enum AuthenticationCode: String { + public enum AuthenticationCode: String, Sendable { case invalid = "request.authentication.invalid" case invalidProjectId = "request.authentication.invalid-project-id" } - public enum GenericCode: String { + public enum GenericCode: String, Sendable { /// The card limits were reached (ex: amounts, transactions volume) and the customer should contact its bank. case cardExceededLimits = "card.exceeded-limits" @@ -362,7 +362,7 @@ public struct POFailure: Error { case serviceNotSupported = "service.not-supported" } - public enum Code: Hashable { + public enum Code: Hashable, Sendable { /// No network connection. case networkUnreachable @@ -392,7 +392,7 @@ public struct POFailure: Error { case unknown(rawValue: String) } - /// Failure message. Not intented to be used as a user facing string. + /// Failure message. Not intended to be used as a user facing string. public let message: String? /// Failure code. diff --git a/Sources/ProcessOut/Sources/Repositories/Telemetry/Telemetry.swift b/Sources/ProcessOut/Sources/Repositories/Telemetry/Telemetry.swift index 977c547c7..04acabfb2 100644 --- a/Sources/ProcessOut/Sources/Repositories/Telemetry/Telemetry.swift +++ b/Sources/ProcessOut/Sources/Repositories/Telemetry/Telemetry.swift @@ -7,9 +7,9 @@ import Foundation -struct Telemetry: Encodable { +struct Telemetry: Encodable, Sendable { - struct ApplicationMetadata: Encodable { + struct ApplicationMetadata: Encodable, Sendable { /// Host application name. let name: String? @@ -18,7 +18,7 @@ struct Telemetry: Encodable { let version: String? } - struct DeviceMetadata: Encodable { + struct DeviceMetadata: Encodable, Sendable { /// Device system language. let language: String @@ -30,7 +30,7 @@ struct Telemetry: Encodable { let timeZone: Int } - struct Metadata: Encodable { + struct Metadata: Encodable, Sendable { /// App metadata. let application: ApplicationMetadata @@ -39,7 +39,7 @@ struct Telemetry: Encodable { let device: DeviceMetadata } - struct Event: Encodable { + struct Event: Encodable, Sendable { /// Event timestamp. let timestamp: Date diff --git a/Sources/ProcessOut/Sources/Services/3DS/DefaultThreeDSService.swift b/Sources/ProcessOut/Sources/Services/3DS/DefaultThreeDSService.swift index 966605091..f9181bf30 100644 --- a/Sources/ProcessOut/Sources/Services/3DS/DefaultThreeDSService.swift +++ b/Sources/ProcessOut/Sources/Services/3DS/DefaultThreeDSService.swift @@ -7,28 +7,39 @@ import Foundation +@MainActor final class DefaultThreeDSService: ThreeDSService { - init(decoder: JSONDecoder, encoder: JSONEncoder, jsonWritingOptions: JSONSerialization.WritingOptions = []) { + nonisolated init( + decoder: JSONDecoder, + encoder: JSONEncoder, + jsonWritingOptions: JSONSerialization.WritingOptions = [], + webSession: WebAuthenticationSession + ) { self.decoder = decoder self.encoder = encoder self.jsonWritingOptions = jsonWritingOptions + self.webSession = webSession } // MARK: - ThreeDSService func handle(action: ThreeDSCustomerAction, delegate: Delegate) async throws -> String { - // todo(andrii-vysotskyi): when async delegate methods are publicly available ensure - // that thrown errors are mapped to POFailure if needed. - switch action.type { - case .fingerprintMobile: - return try await fingerprint(encodedConfiguration: action.value, delegate: delegate) - case .challengeMobile: - return try await challenge(encodedChallenge: action.value, delegate: delegate) - case .fingerprint: - return try await fingerprint(url: action.value, delegate: delegate) - case .redirect, .url: - return try await redirect(url: action.value, delegate: delegate) + do { + switch action.type { + case .fingerprintMobile: + return try await fingerprint(encodedConfiguration: action.value, delegate: delegate) + case .challengeMobile: + return try await challenge(encodedChallenge: action.value, delegate: delegate) + case .fingerprint: + return try await fingerprint(url: action.value) + case .redirect, .url: + return try await redirect(url: action.value) + } + } catch let error as POFailure { + throw error + } catch { + throw POFailure(code: .generic(.mobile), underlyingError: error) } } @@ -37,8 +48,6 @@ final class DefaultThreeDSService: ThreeDSService { private enum Constants { static let deviceChannel = "app" static let tokenPrefix = "gway_req_" - static let challengeSuccessEncodedResponse = "eyJib2R5IjoieyBcInRyYW5zU3RhdHVzXCI6IFwiWVwiIH0ifQ==" - static let challengeFailureEncodedResponse = "eyJib2R5IjoieyBcInRyYW5zU3RhdHVzXCI6IFwiTlwiIH0ifQ==" static let fingerprintTimeoutResponseBody = #"{ "threeDS2FingerprintTimeout": true }"# static let webFingerprintTimeout: TimeInterval = 10 } @@ -53,33 +62,48 @@ final class DefaultThreeDSService: ThreeDSService { private let decoder: JSONDecoder private let encoder: JSONEncoder private let jsonWritingOptions: JSONSerialization.WritingOptions + private let webSession: WebAuthenticationSession - // MARK: - Private Methods + // MARK: - Native 3DS private func fingerprint(encodedConfiguration: String, delegate: Delegate) async throws -> String { let configuration = try decode(PO3DS2Configuration.self, from: encodedConfiguration) - let request = try await delegate.authenticationRequest(configuration: configuration) - let response = AuthenticationResponse(url: nil, body: try self.encode(request: request)) + let requestParameters = try await delegate.authenticationRequestParameters(configuration: configuration) + let response = AuthenticationResponse(url: nil, body: try self.encode(requestParameters: requestParameters)) return try encode(authenticationResponse: response) } private func challenge(encodedChallenge: String, delegate: Delegate) async throws -> String { - let challenge = try decode(PO3DS2Challenge.self, from: encodedChallenge) - let success = try await delegate.handle(challenge: challenge) - let encodedResponse = success - ? Constants.challengeSuccessEncodedResponse - : Constants.challengeFailureEncodedResponse - return Constants.tokenPrefix + encodedResponse + let parameters = try decode(PO3DS2ChallengeParameters.self, from: encodedChallenge) + let result = try await delegate.performChallenge(with: parameters) + let encodedChallengeResult: String + do { + encodedChallengeResult = try String(decoding: encoder.encode(result), as: UTF8.self) + } catch { + let message = "Did fail to encode CRES result." + throw POFailure(message: message, code: .internal(.mobile), underlyingError: error) + } + let response = AuthenticationResponse(url: nil, body: encodedChallengeResult) + return try encode(authenticationResponse: response) } - private func fingerprint(url: String, delegate: Delegate) async throws -> String { + // MARK: - Web Based 3DS + + private func fingerprint(url: String) async throws -> String { guard let url = URL(string: url) else { let message = "Unable to create URL from string: \(url)." throw POFailure(message: message, code: .internal(.mobile), underlyingError: nil) } - let context = PO3DSRedirect(url: url, timeout: Constants.webFingerprintTimeout) do { - return try await delegate.handle(redirect: context) + let returnUrl = try await withTimeout( + Constants.webFingerprintTimeout, + error: POFailure(code: .timeout(.mobile)), + perform: { + try await self.webSession.authenticate(using: url) + } + ) + let queryItems = URLComponents(string: returnUrl.absoluteString)?.queryItems + return queryItems?.first { $0.name == "token" }?.value ?? "" } catch let failure as POFailure where failure.code == .timeout(.mobile) { // Fingerprinting timeout is treated differently from other errors. let response = AuthenticationResponse(url: url, body: Constants.fingerprintTimeoutResponseBody) @@ -87,13 +111,14 @@ final class DefaultThreeDSService: ThreeDSService { } } - private func redirect(url: String, delegate: Delegate) async throws -> String { + private func redirect(url: String) async throws -> String { guard let url = URL(string: url) else { let message = "Unable to create URL from string: \(url)." throw POFailure(message: message, code: .internal(.mobile), underlyingError: nil) } - let context = PO3DSRedirect(url: url, timeout: nil) - return try await delegate.handle(redirect: context) + let returnUrl = try await self.webSession.authenticate(using: url) + let queryItems = URLComponents(string: returnUrl.absoluteString)?.queryItems + return queryItems?.first { $0.name == "token" }?.value ?? "" } // MARK: - Coding @@ -113,20 +138,20 @@ final class DefaultThreeDSService: ThreeDSService { } } - private func encode(request: PO3DS2AuthenticationRequest) throws -> String { + private func encode(requestParameters parameters: PO3DS2AuthenticationRequestParameters) throws -> String { do { // Using JSONSerialization helps avoid creating boilerplate objects for JSON Web Key for coding. // Implementation doesn't validate JWK correctness and simply re-encodes given value. let sdkEphemeralPublicKey = try JSONSerialization.jsonObject( - with: Data(request.sdkEphemeralPublicKey.utf8) + with: Data(parameters.sdkEphemeralPublicKey.utf8) ) let requestParameters = [ "deviceChannel": Constants.deviceChannel, - "sdkAppID": request.sdkAppId, + "sdkAppID": parameters.sdkAppId, "sdkEphemPubKey": sdkEphemeralPublicKey, - "sdkReferenceNumber": request.sdkReferenceNumber, - "sdkTransID": request.sdkTransactionId, - "sdkEncData": request.deviceData + "sdkReferenceNumber": parameters.sdkReferenceNumber, + "sdkTransID": parameters.sdkTransactionId, + "sdkEncData": parameters.deviceData ] let requestParametersData = try JSONSerialization.data( withJSONObject: requestParameters, options: jsonWritingOptions @@ -138,14 +163,12 @@ final class DefaultThreeDSService: ThreeDSService { } } - // MARK: - Utils - /// Encodes given response and creates token with it. private func encode(authenticationResponse: AuthenticationResponse) throws -> String { do { return try Constants.tokenPrefix + encoder.encode(authenticationResponse).base64EncodedString() } catch { - let message = "Did fail to encode AREQ parameters." + let message = "Did fail to encode authentication result." throw POFailure(message: message, code: .internal(.mobile), underlyingError: error) } } diff --git a/Sources/ProcessOut/Sources/Services/3DS/Models/PO3DS2AuthenticationRequest.swift b/Sources/ProcessOut/Sources/Services/3DS/Models/PO3DS2AuthenticationRequestParameters.swift similarity index 79% rename from Sources/ProcessOut/Sources/Services/3DS/Models/PO3DS2AuthenticationRequest.swift rename to Sources/ProcessOut/Sources/Services/3DS/Models/PO3DS2AuthenticationRequestParameters.swift index 6095d33ca..b19f4afe9 100644 --- a/Sources/ProcessOut/Sources/Services/3DS/Models/PO3DS2AuthenticationRequest.swift +++ b/Sources/ProcessOut/Sources/Services/3DS/Models/PO3DS2AuthenticationRequestParameters.swift @@ -1,12 +1,15 @@ // -// PO3DS2AuthenticationRequest.swift +// PO3DS2AuthenticationRequestParameters.swift // ProcessOut // // Created by Andrii Vysotskyi on 03.11.2022. // +@available(*, deprecated, renamed: "PO3DS2AuthenticationRequestParameters") +public typealias PO3DS2AuthenticationRequest = PO3DS2AuthenticationRequestParameters + /// Holds transaction data that the 3DS Server requires to create the AReq. -public struct PO3DS2AuthenticationRequest: Hashable { +public struct PO3DS2AuthenticationRequestParameters: Hashable, Sendable { /// Encrypted device data as a JWE string. public let deviceData: String diff --git a/Sources/ProcessOut/Sources/Services/3DS/Models/PO3DS2Challenge.swift b/Sources/ProcessOut/Sources/Services/3DS/Models/PO3DS2ChallengeParameters.swift similarity index 80% rename from Sources/ProcessOut/Sources/Services/3DS/Models/PO3DS2Challenge.swift rename to Sources/ProcessOut/Sources/Services/3DS/Models/PO3DS2ChallengeParameters.swift index b44d18597..cba716637 100644 --- a/Sources/ProcessOut/Sources/Services/3DS/Models/PO3DS2Challenge.swift +++ b/Sources/ProcessOut/Sources/Services/3DS/Models/PO3DS2ChallengeParameters.swift @@ -1,13 +1,16 @@ // -// PO3DS2Challenge.swift +// PO3DS2ChallengeParameters.swift // ProcessOut // // Created by Andrii Vysotskyi on 02.11.2022. // +@available(*, deprecated, renamed: "PO3DS2ChallengeParameters") +public typealias PO3DS2Challenge = PO3DS2ChallengeParameters + /// Information from the 3DS Server's authentication response that could be used by the 3DS2 SDK to initiate /// the challenge flow. -public struct PO3DS2Challenge: Decodable, Hashable { +public struct PO3DS2ChallengeParameters: Decodable, Hashable, Sendable { /// Unique transaction identifier assigned by the ACS. public let acsTransactionId: String diff --git a/Sources/ProcessOut/Sources/Services/3DS/Models/PO3DS2ChallengeResult.swift b/Sources/ProcessOut/Sources/Services/3DS/Models/PO3DS2ChallengeResult.swift new file mode 100644 index 000000000..e18167582 --- /dev/null +++ b/Sources/ProcessOut/Sources/Services/3DS/Models/PO3DS2ChallengeResult.swift @@ -0,0 +1,27 @@ +// +// PO3DS2ChallengeResult.swift +// ProcessOut +// +// Created by Andrii Vysotskyi on 01.08.2024. +// + +/// Contains information about completion of the challenge process. +public struct PO3DS2ChallengeResult: Encodable, Sendable { + + /// The transaction status that was received in the final challenge response. + public let transactionStatus: String + + public init(transactionStatus: String) { + self.transactionStatus = transactionStatus + } + + public init(transactionStatus: Bool) { + self.transactionStatus = transactionStatus ? "Y" : "N" + } + + // MARK: - Private Nested Types + + private enum CodingKeys: String, CodingKey { + case transactionStatus = "transStatus" + } +} diff --git a/Sources/ProcessOut/Sources/Services/3DS/Models/PO3DS2Configuration.swift b/Sources/ProcessOut/Sources/Services/3DS/Models/PO3DS2Configuration.swift index dfc08200f..85b2af2ff 100644 --- a/Sources/ProcessOut/Sources/Services/3DS/Models/PO3DS2Configuration.swift +++ b/Sources/ProcessOut/Sources/Services/3DS/Models/PO3DS2Configuration.swift @@ -6,7 +6,7 @@ // /// Represents the configuration parameters that are required by the 3DS SDK for initialization. -public struct PO3DS2Configuration: Decodable, Hashable { +public struct PO3DS2Configuration: Decodable, Hashable, Sendable { /// The identifier of the directory server to use during the transaction creation phase. public let directoryServerId: String @@ -21,8 +21,7 @@ public struct PO3DS2Configuration: Decodable, Hashable { public let directoryServerTransactionId: String /// Card scheme from the card used to initiate the payment. - @POTypedRepresentation - public private(set) var scheme: PO3DS2ConfigurationCardScheme? + public let scheme: POCardScheme? /// 3DS protocol version identifier. public let messageVersion: String diff --git a/Sources/ProcessOut/Sources/Services/3DS/Models/PO3DS2ConfigurationCardScheme.swift b/Sources/ProcessOut/Sources/Services/3DS/Models/PO3DS2ConfigurationCardScheme.swift deleted file mode 100644 index 48dec718e..000000000 --- a/Sources/ProcessOut/Sources/Services/3DS/Models/PO3DS2ConfigurationCardScheme.swift +++ /dev/null @@ -1,75 +0,0 @@ -// -// PO3DS2ConfigurationCardScheme.swift -// ProcessOut -// -// Created by Andrii Vysotskyi on 27.03.2023. -// - -// todo(andrii-vysotskyi): remove when updating to 5.0.0 - -/// Available card schemes. -public enum PO3DS2ConfigurationCardScheme: RawRepresentable, Decodable, Hashable { - - /// Known card schemes. - case visa, mastercard, europay, carteBancaire, jcb, diners, discover, unionpay, americanExpress - - /// Used for schemes unknown to sdk. - case unknown(String) - - public init(rawValue: String) { - self = Self.knownSchemes[rawValue] ?? .unknown(rawValue) - } - - public var rawValue: String { - switch self { - case .visa: - return Constants.visa - case .mastercard: - return Constants.mastercard - case .europay: - return Constants.europay - case .carteBancaire: - return Constants.carteBancaire - case .jcb: - return Constants.jcb - case .diners: - return Constants.diners - case .discover: - return Constants.discover - case .unionpay: - return Constants.unionpay - case .americanExpress: - return Constants.americanExpress - case .unknown(let rawValue): - return rawValue - } - } - - // MARK: - Private Nested Types - - private enum Constants { - static let visa = "visa" - static let mastercard = "mastercard" - static let europay = "europay" - static let carteBancaire = "carte bancaire" - static let jcb = "jcb" - static let diners = "diners" - static let discover = "discover" - static let unionpay = "unionpay" - static let americanExpress = "american express" - } - - // MARK: - Private Properties - - private static let knownSchemes: [String: Self] = [ - Constants.visa: .visa, - Constants.mastercard: .mastercard, - Constants.europay: .europay, - Constants.carteBancaire: .carteBancaire, - Constants.jcb: .jcb, - Constants.diners: .diners, - Constants.discover: .discover, - Constants.unionpay: .unionpay, - Constants.americanExpress: .americanExpress - ] -} diff --git a/Sources/ProcessOut/Sources/Services/3DS/Models/PO3DSRedirect.swift b/Sources/ProcessOut/Sources/Services/3DS/Models/PO3DSRedirect.swift deleted file mode 100644 index 7b50949ee..000000000 --- a/Sources/ProcessOut/Sources/Services/3DS/Models/PO3DSRedirect.swift +++ /dev/null @@ -1,23 +0,0 @@ -// -// PO3DSRedirect.swift -// ProcessOut -// -// Created by Andrii Vysotskyi on 17.01.2023. -// - -import Foundation - -/// Holds information about 3DS redirect. -public struct PO3DSRedirect: Hashable { - - /// Redirect url. - public let url: URL - - /// Boolean value that indicates whether a given URL can be handled in headless mode, meaning - /// without showing any UI for the user. - @available(*, deprecated) - public let isHeadlessModeAllowed = false - - /// Optional timeout interval. - public let timeout: TimeInterval? -} diff --git a/Sources/ProcessOut/Sources/Services/3DS/PO3DSService.swift b/Sources/ProcessOut/Sources/Services/3DS/PO3DSService.swift index a18a6745f..2ca761429 100644 --- a/Sources/ProcessOut/Sources/Services/3DS/PO3DSService.swift +++ b/Sources/ProcessOut/Sources/Services/3DS/PO3DSService.swift @@ -5,54 +5,14 @@ // Created by Andrii Vysotskyi on 03.11.2022. // -@available(*, deprecated, renamed: "PO3DSService") -public typealias PO3DSServiceType = PO3DSService - /// This interface provides methods to process 3-D Secure transactions. -public protocol PO3DSService: AnyObject { - - /// Asks implementation to create request that will be passed to 3DS Server to create the AReq. - func authenticationRequest( - configuration: PO3DS2Configuration, - completion: @escaping (Result) -> Void - ) - - /// Implementation must handle given 3DS2 challenge and call completion with result. Use `true` if challenge - /// was handled successfully, if transaction was denied, pass `false`. In all other cases, call completion - /// with failure indicating what went wrong. - func handle(challenge: PO3DS2Challenge, completion: @escaping (Result) -> Void) - - /// Asks implementation to handle redirect. If value of ``PO3DSRedirect/timeout`` is present it must be - /// respected, meaning if timeout is reached `completion` should be called with instance of ``POFailure`` with - /// ``POFailure/code-swift.property`` set to ``POFailure/TimeoutCode/mobile``. - func handle(redirect: PO3DSRedirect, completion: @escaping (Result) -> Void) -} - -@MainActor -extension PO3DSService { +public protocol PO3DSService: AnyObject, Sendable { /// Asks implementation to create request that will be passed to 3DS Server to create the AReq. - func authenticationRequest(configuration: PO3DS2Configuration) async throws -> PO3DS2AuthenticationRequest { - try await withUnsafeThrowingContinuation { continuation in - authenticationRequest(configuration: configuration, completion: continuation.resume) - } - } - - /// Implementation must handle given 3DS2 challenge and call completion with result. Use `true` if challenge - /// was handled successfully, if transaction was denied, pass `false`. In all other cases, call completion - /// with failure indicating what went wrong. - func handle(challenge: PO3DS2Challenge) async throws -> Bool { - try await withUnsafeThrowingContinuation { continuation in - handle(challenge: challenge, completion: continuation.resume) - } - } + func authenticationRequestParameters( + configuration: PO3DS2Configuration + ) async throws -> PO3DS2AuthenticationRequestParameters - /// Asks implementation to handle redirect. If value of ``PO3DSRedirect/timeout`` is present it must be - /// respected, meaning if timeout is reached `completion` should be called with instance of ``POFailure`` with - /// ``POFailure/code-swift.property`` set to ``POFailure/TimeoutCode/mobile``. - func handle(redirect: PO3DSRedirect) async throws -> String { - try await withUnsafeThrowingContinuation { continuation in - handle(redirect: redirect, completion: continuation.resume) - } - } + /// Implementation must handle given 3DS2 challenge. + func performChallenge(with parameters: PO3DS2ChallengeParameters) async throws -> PO3DS2ChallengeResult } diff --git a/Sources/ProcessOut/Sources/Services/3DS/ThreeDSService.swift b/Sources/ProcessOut/Sources/Services/3DS/ThreeDSService.swift index bc629153d..017ed94af 100644 --- a/Sources/ProcessOut/Sources/Services/3DS/ThreeDSService.swift +++ b/Sources/ProcessOut/Sources/Services/3DS/ThreeDSService.swift @@ -5,7 +5,7 @@ // Created by Andrii Vysotskyi on 02.11.2022. // -protocol ThreeDSService { +protocol ThreeDSService: Sendable { typealias Delegate = PO3DSService diff --git a/Sources/ProcessOut/Sources/Services/AlternativePaymentMethods/DefaultAlternativePaymentMethodsService.swift b/Sources/ProcessOut/Sources/Services/AlternativePaymentMethods/DefaultAlternativePaymentMethodsService.swift deleted file mode 100644 index 3db28b5c4..000000000 --- a/Sources/ProcessOut/Sources/Services/AlternativePaymentMethods/DefaultAlternativePaymentMethodsService.swift +++ /dev/null @@ -1,94 +0,0 @@ -// -// DefaultAlternativePaymentMethodsService.swift -// ProcessOut -// -// Created by Simeon Kostadinov on 27/10/2022. -// - -import Foundation - -final class DefaultAlternativePaymentMethodsService: POAlternativePaymentMethodsService { - - init(configuration: @escaping () -> AlternativePaymentMethodsServiceConfiguration, logger: POLogger) { - self.configuration = configuration - self.logger = logger - } - - // MARK: - POAlternativePaymentMethodsService - - func alternativePaymentMethodUrl(request: POAlternativePaymentMethodRequest) -> 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] - } - } - components.path = "/" + pathComponents.joined(separator: "/") - components.queryItems = request.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.") - } - return url - } - - func alternativePaymentMethodResponse(url: URL) throws -> POAlternativePaymentMethodResponse { - 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) - } - let queryItems = components.queryItems ?? [] - if let errorCode = queryItems.queryItemValue(name: "error_code") { - throw POFailure(code: createFailureCode(rawValue: errorCode)) - } - let gatewayToken = queryItems.queryItemValue(name: "token") ?? "" - 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) - } - - // MARK: - Private - - private let configuration: () -> 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) - } else if let code = POFailure.NotFoundCode(rawValue: rawValue) { - return .notFound(code) - } else if let code = POFailure.ValidationCode(rawValue: rawValue) { - return .validation(code) - } else if let code = POFailure.GenericCode(rawValue: rawValue) { - return .generic(code) - } else if let code = POFailure.TimeoutCode(rawValue: rawValue) { - return .timeout(code) - } else if let code = POFailure.InternalCode(rawValue: rawValue) { - return .internal(code) - } - return .unknown(rawValue: rawValue) - } -} - -private extension Array where Element == URLQueryItem { // swiftlint:disable:this no_extension_access_modifier - - func queryItemValue(name: String) -> String? { - first { $0.name == name }?.value - } -} diff --git a/Sources/ProcessOut/Sources/Services/AlternativePaymentMethods/POAlternativePaymentMethodsService.swift b/Sources/ProcessOut/Sources/Services/AlternativePaymentMethods/POAlternativePaymentMethodsService.swift deleted file mode 100644 index ddf6babfe..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 { - - /// 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 37603bd15..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 { - - /// 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 3c63690a7..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 { - - public enum APMReturnType { - 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/AlternativePayments/DefaultAlternativePaymentsService.swift b/Sources/ProcessOut/Sources/Services/AlternativePayments/DefaultAlternativePaymentsService.swift new file mode 100644 index 000000000..e3150c576 --- /dev/null +++ b/Sources/ProcessOut/Sources/Services/AlternativePayments/DefaultAlternativePaymentsService.swift @@ -0,0 +1,116 @@ +// +// DefaultAlternativePaymentsService.swift +// ProcessOut +// +// Created by Simeon Kostadinov on 27/10/2022. +// + +import Foundation + +final class DefaultAlternativePaymentsService: POAlternativePaymentsService { + + init( + configuration: @escaping @Sendable () -> AlternativePaymentsServiceConfiguration, + webSession: WebAuthenticationSession, + logger: POLogger + ) { + self.configuration = configuration + self.webSession = webSession + self.logger = logger + } + + // MARK: - POAlternativePaymentsService + + func tokenize(request: POAlternativePaymentTokenizationRequest) async throws -> POAlternativePaymentResponse { + try await authenticate(using: url(for: request)) + } + + func authorize(request: POAlternativePaymentAuthorizationRequest) async throws -> POAlternativePaymentResponse { + try await authenticate(using: url(for: request)) + } + + func url(for request: POAlternativePaymentTokenizationRequest) throws -> URL { + let pathComponents = [request.customerId, request.tokenId, "redirect", request.gatewayConfigurationId] + return try url(with: pathComponents, additionalData: request.additionalData) + } + + func url(for request: POAlternativePaymentAuthorizationRequest) throws -> URL { + var pathComponents = [request.invoiceId, "redirect", request.gatewayConfigurationId] + if let tokenId = request.tokenId { + pathComponents += ["tokenized", tokenId] + } + return try url(with: pathComponents, additionalData: request.additionalData) + } + + 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("Invalid base URL.") + } + let pathComponents = [configuration.projectId] + additionalPathComponents + components.path = "/" + pathComponents.joined(separator: "/") + components.queryItems = additionalData?.map { data in + URLQueryItem(name: "additional_data[" + data.key + "]", value: data.value) + } + if let url = components.url { + return url + } + throw POFailure(message: "Unable to create redirect URL.", code: .generic(.mobile)) + } + + // 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) + } + let queryItems = components.queryItems ?? [] + if let errorCode = queryItems.queryItemValue(name: "error_code") { + throw POFailure(code: createFailureCode(rawValue: errorCode)) + } + let gatewayToken = queryItems.queryItemValue(name: "token") ?? "" + if gatewayToken.isEmpty { + logger.debug("Gateway 'token' is not set in \(url), this may be an error.") + } + return .init(gatewayToken: gatewayToken) + } + + private func createFailureCode(rawValue: String) -> POFailure.Code { + if let code = POFailure.AuthenticationCode(rawValue: rawValue) { + return .authentication(code) + } else if let code = POFailure.NotFoundCode(rawValue: rawValue) { + return .notFound(code) + } else if let code = POFailure.ValidationCode(rawValue: rawValue) { + return .validation(code) + } else if let code = POFailure.GenericCode(rawValue: rawValue) { + return .generic(code) + } else if let code = POFailure.TimeoutCode(rawValue: rawValue) { + return .timeout(code) + } else if let code = POFailure.InternalCode(rawValue: rawValue) { + return .internal(code) + } + return .unknown(rawValue: rawValue) + } +} + +private extension Array where Element == URLQueryItem { // swiftlint:disable:this no_extension_access_modifier + + func queryItemValue(name: String) -> String? { + first { $0.name == name }?.value + } +} diff --git a/Sources/ProcessOut/Sources/Services/AlternativePayments/POAlternativePaymentsService.swift b/Sources/ProcessOut/Sources/Services/AlternativePayments/POAlternativePaymentsService.swift new file mode 100644 index 000000000..ec2e2c616 --- /dev/null +++ b/Sources/ProcessOut/Sources/Services/AlternativePayments/POAlternativePaymentsService.swift @@ -0,0 +1,27 @@ +// +// POAlternativePaymentsService.swift +// ProcessOut +// +// Created by Simeon Kostadinov on 27/10/2022. +// + +import Foundation + +/// 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 + + /// Creates redirect URL for given tokenization request. + func url(for request: POAlternativePaymentTokenizationRequest) throws -> URL + + /// Creates redirect URL for given authorization request. + func url(for request: POAlternativePaymentAuthorizationRequest) throws -> URL + + /// 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/ProcessOut/Sources/Services/Cards/Coordinators/ApplePay/ApplePayTokenizationCoordinator.swift b/Sources/ProcessOut/Sources/Services/Cards/Coordinators/ApplePay/ApplePayTokenizationCoordinator.swift index cdbec78e8..5ffb258a2 100644 --- a/Sources/ProcessOut/Sources/Services/Cards/Coordinators/ApplePay/ApplePayTokenizationCoordinator.swift +++ b/Sources/ProcessOut/Sources/Services/Cards/Coordinators/ApplePay/ApplePayTokenizationCoordinator.swift @@ -7,6 +7,7 @@ import PassKit +@MainActor final class ApplePayTokenizationCoordinator: ApplePayAuthorizationSessionDelegate { init( diff --git a/Sources/ProcessOut/Sources/Services/Cards/Coordinators/ApplePay/POApplePayTokenizationDelegate.swift b/Sources/ProcessOut/Sources/Services/Cards/Coordinators/ApplePay/POApplePayTokenizationDelegate.swift index e0c88051c..daf358175 100644 --- a/Sources/ProcessOut/Sources/Services/Cards/Coordinators/ApplePay/POApplePayTokenizationDelegate.swift +++ b/Sources/ProcessOut/Sources/Services/Cards/Coordinators/ApplePay/POApplePayTokenizationDelegate.swift @@ -7,7 +7,7 @@ import PassKit -public protocol POApplePayTokenizationDelegate: AnyObject { +public protocol POApplePayTokenizationDelegate: AnyObject, Sendable { /// Sent to the delegate after the user has acted on the payment request and it was tokenized by ProcessOut. @MainActor diff --git a/Sources/ProcessOut/Sources/Services/Cards/DefaultCardsService.swift b/Sources/ProcessOut/Sources/Services/Cards/DefaultCardsService.swift index 34ae49478..27fddf94f 100644 --- a/Sources/ProcessOut/Sources/Services/Cards/DefaultCardsService.swift +++ b/Sources/ProcessOut/Sources/Services/Cards/DefaultCardsService.swift @@ -37,10 +37,11 @@ final class DefaultCardsService: POCardsService { } func tokenize(request: POApplePayPaymentTokenizationRequest) async throws -> POCard { - let request = try applePayCardTokenizationRequestMapper.tokenizationRequest(from: request) + let request = try await applePayCardTokenizationRequestMapper.tokenizationRequest(from: request) return try await repository.tokenize(request: request) } + @MainActor func tokenize( request: POApplePayTokenizationRequest, delegate: POApplePayTokenizationDelegate? ) async throws -> POCard { diff --git a/Sources/ProcessOut/Sources/Services/Cards/Mappers/ApplePayCardTokenizationRequest/ApplePayCardTokenizationRequestMapper.swift b/Sources/ProcessOut/Sources/Services/Cards/Mappers/ApplePayCardTokenizationRequest/ApplePayCardTokenizationRequestMapper.swift index 36ac0fc2b..612c1d2a7 100644 --- a/Sources/ProcessOut/Sources/Services/Cards/Mappers/ApplePayCardTokenizationRequest/ApplePayCardTokenizationRequestMapper.swift +++ b/Sources/ProcessOut/Sources/Services/Cards/Mappers/ApplePayCardTokenizationRequest/ApplePayCardTokenizationRequestMapper.swift @@ -5,10 +5,10 @@ // Created by Julien.Rodrigues on 25/10/2022. // -protocol ApplePayCardTokenizationRequestMapper { +protocol ApplePayCardTokenizationRequestMapper: Sendable { /// Creates tokenization request with given ``POApplePayCardTokenizationRequest`` instance. func tokenizationRequest( from request: POApplePayPaymentTokenizationRequest - ) throws -> ApplePayCardTokenizationRequest + ) async throws -> ApplePayCardTokenizationRequest } diff --git a/Sources/ProcessOut/Sources/Services/Cards/Mappers/ApplePayCardTokenizationRequest/DefaultApplePayCardTokenizationRequestMapper.swift b/Sources/ProcessOut/Sources/Services/Cards/Mappers/ApplePayCardTokenizationRequest/DefaultApplePayCardTokenizationRequestMapper.swift index c40ce296d..d8c87eb3c 100644 --- a/Sources/ProcessOut/Sources/Services/Cards/Mappers/ApplePayCardTokenizationRequest/DefaultApplePayCardTokenizationRequestMapper.swift +++ b/Sources/ProcessOut/Sources/Services/Cards/Mappers/ApplePayCardTokenizationRequest/DefaultApplePayCardTokenizationRequestMapper.swift @@ -19,6 +19,7 @@ final class DefaultApplePayCardTokenizationRequestMapper: ApplePayCardTokenizati // MARK: - ApplePayCardTokenizationRequestMapper /// - Throws: `POFailure` instance in case of error. + @MainActor func tokenizationRequest( from request: POApplePayPaymentTokenizationRequest ) throws -> ApplePayCardTokenizationRequest { diff --git a/Sources/ProcessOut/Sources/Services/Cards/Mappers/PassKitContact/PassKitContactMapper.swift b/Sources/ProcessOut/Sources/Services/Cards/Mappers/PassKitContact/PassKitContactMapper.swift index 28acf662b..2f6dca09c 100644 --- a/Sources/ProcessOut/Sources/Services/Cards/Mappers/PassKitContact/PassKitContactMapper.swift +++ b/Sources/ProcessOut/Sources/Services/Cards/Mappers/PassKitContact/PassKitContactMapper.swift @@ -7,7 +7,7 @@ import PassKit -protocol PassKitContactMapper { +protocol PassKitContactMapper: Sendable { /// Converts given `PKContact` instance to `POContact`. func map(contact: PKContact) -> POContact diff --git a/Sources/ProcessOut/Sources/Services/Cards/Mappers/PassKitPaymentError/POPassKitPaymentErrorMapper.swift b/Sources/ProcessOut/Sources/Services/Cards/Mappers/PassKitPaymentError/POPassKitPaymentErrorMapper.swift index 93822b019..a9592b1eb 100644 --- a/Sources/ProcessOut/Sources/Services/Cards/Mappers/PassKitPaymentError/POPassKitPaymentErrorMapper.swift +++ b/Sources/ProcessOut/Sources/Services/Cards/Mappers/PassKitPaymentError/POPassKitPaymentErrorMapper.swift @@ -8,7 +8,7 @@ // todo(andrii-vysotskyi): remove public access level when POPassKitPaymentAuthorizationController is removed @_spi(PO) -public protocol POPassKitPaymentErrorMapper { +public protocol POPassKitPaymentErrorMapper: Sendable { /// Converts ProcessOut errors into the appropriate Apple Pay error, for use in /// `PKPaymentAuthorizationResult`. diff --git a/Sources/ProcessOut/Sources/Services/Cards/POCardsService.swift b/Sources/ProcessOut/Sources/Services/Cards/POCardsService.swift index 0422707b4..b744c82d7 100644 --- a/Sources/ProcessOut/Sources/Services/Cards/POCardsService.swift +++ b/Sources/ProcessOut/Sources/Services/Cards/POCardsService.swift @@ -5,9 +5,6 @@ // Created by Andrii Vysotskyi on 17.03.2023. // -@available(*, deprecated, renamed: "POCardsService") -public typealias POCardsServiceType = POCardsService - /// Provides set of methods to tokenize and manipulate cards. public protocol POCardsService: POService { // sourcery: AutoCompletion diff --git a/Sources/ProcessOut/Sources/Services/Cards/Requests/POApplePayCardTokenizationRequest.swift b/Sources/ProcessOut/Sources/Services/Cards/Requests/POApplePayCardTokenizationRequest.swift index fe13a62de..09ed27d35 100644 --- a/Sources/ProcessOut/Sources/Services/Cards/Requests/POApplePayCardTokenizationRequest.swift +++ b/Sources/ProcessOut/Sources/Services/Cards/Requests/POApplePayCardTokenizationRequest.swift @@ -12,6 +12,7 @@ import PassKit public typealias POApplePayCardTokenizationRequest = POApplePayPaymentTokenizationRequest /// Apple Pay payment details. +@MainActor public struct POApplePayPaymentTokenizationRequest { /// Payment information. diff --git a/Sources/ProcessOut/Sources/Services/Cards/Requests/POApplePayTokenizationRequest.swift b/Sources/ProcessOut/Sources/Services/Cards/Requests/POApplePayTokenizationRequest.swift index b2fcf1cdc..5765e82cd 100644 --- a/Sources/ProcessOut/Sources/Services/Cards/Requests/POApplePayTokenizationRequest.swift +++ b/Sources/ProcessOut/Sources/Services/Cards/Requests/POApplePayTokenizationRequest.swift @@ -8,6 +8,7 @@ import PassKit /// Apple Pay tokenization request. +@MainActor public struct POApplePayTokenizationRequest { /// The payment request to be authorized and tokenized. diff --git a/Sources/ProcessOut/Sources/Services/CustomerTokens/POCustomerTokensService.swift b/Sources/ProcessOut/Sources/Services/CustomerTokens/POCustomerTokensService.swift index 311d24520..7373522da 100644 --- a/Sources/ProcessOut/Sources/Services/CustomerTokens/POCustomerTokensService.swift +++ b/Sources/ProcessOut/Sources/Services/CustomerTokens/POCustomerTokensService.swift @@ -5,9 +5,6 @@ // Created by Andrii Vysotskyi on 02.11.2022. // -@available(*, deprecated, renamed: "POCustomerTokensService") -public typealias POCustomerTokensServiceType = POCustomerTokensService - /// Provides an ability to interact with customer tokens. /// /// You can only use a card or APM token once but you can make payments as many times as necessary with a customer diff --git a/Sources/ProcessOut/Sources/Services/Invoices/DefaultInvoicesService.swift b/Sources/ProcessOut/Sources/Services/Invoices/DefaultInvoicesService.swift index 72ce58170..cb5db421b 100644 --- a/Sources/ProcessOut/Sources/Services/Invoices/DefaultInvoicesService.swift +++ b/Sources/ProcessOut/Sources/Services/Invoices/DefaultInvoicesService.swift @@ -59,8 +59,8 @@ final class DefaultInvoicesService: POInvoicesService { }, while: { result in switch result { - case let .success(response): - return response.nativeApm.state != .captured + case let .success(state): + return state != .captured case let .failure(failure as POFailure): let retriableCodes: [POFailure.Code] = [ .networkUnreachable, .timeout(.mobile), .internal(.mobile) diff --git a/Sources/ProcessOut/Sources/Services/Invoices/POInvoicesService.swift b/Sources/ProcessOut/Sources/Services/Invoices/POInvoicesService.swift index 573f4e3d4..d37c5aa28 100644 --- a/Sources/ProcessOut/Sources/Services/Invoices/POInvoicesService.swift +++ b/Sources/ProcessOut/Sources/Services/Invoices/POInvoicesService.swift @@ -7,9 +7,6 @@ // todo(andrii-vysotskyi): potentially make threeDSService optional to allow easier APMs authorization -@available(*, deprecated, renamed: "POInvoicesService") -public typealias POInvoicesServiceType = POInvoicesService - public protocol POInvoicesService: POService { // sourcery: AutoCompletion /// Requests information needed to continue existing payment or start new one. diff --git a/Sources/ProcessOut/Sources/Services/Invoices/Requests/PONativeAlternativePaymentCaptureRequest.swift b/Sources/ProcessOut/Sources/Services/Invoices/Requests/PONativeAlternativePaymentCaptureRequest.swift index db03b5ce8..35e7c2e61 100644 --- a/Sources/ProcessOut/Sources/Services/Invoices/Requests/PONativeAlternativePaymentCaptureRequest.swift +++ b/Sources/ProcessOut/Sources/Services/Invoices/Requests/PONativeAlternativePaymentCaptureRequest.swift @@ -7,7 +7,7 @@ import Foundation -public struct PONativeAlternativePaymentCaptureRequest { +public struct PONativeAlternativePaymentCaptureRequest: Sendable { /// Invoice identifier. public let invoiceId: String diff --git a/Sources/ProcessOut/Sources/Services/Shared/POService.swift b/Sources/ProcessOut/Sources/Services/Shared/POService.swift index a372a6d8d..bc34254e5 100644 --- a/Sources/ProcessOut/Sources/Services/Shared/POService.swift +++ b/Sources/ProcessOut/Sources/Services/Shared/POService.swift @@ -6,11 +6,8 @@ // /// Common protocol that all services conform to. -public protocol POService { +public protocol POService: Sendable { /// Service's failure type. typealias Failure = POFailure } - -@available(*, deprecated, renamed: "POService") -public typealias POServiceType = POService diff --git a/Sources/ProcessOut/Sources/Services/Telemetry/DefaultTelemetryService.swift b/Sources/ProcessOut/Sources/Services/Telemetry/DefaultTelemetryService.swift index f58aa9517..5853df7d6 100644 --- a/Sources/ProcessOut/Sources/Services/Telemetry/DefaultTelemetryService.swift +++ b/Sources/ProcessOut/Sources/Services/Telemetry/DefaultTelemetryService.swift @@ -10,7 +10,7 @@ import Foundation final class DefaultTelemetryService: POService, LoggerDestination { init( - configuration: @escaping () -> TelemetryServiceConfiguration, + configuration: @escaping @Sendable () -> TelemetryServiceConfiguration, repository: TelemetryRepository, deviceMetadataProvider: DeviceMetadataProvider ) { @@ -58,9 +58,10 @@ final class DefaultTelemetryService: POService, LoggerDestination { private let repository: TelemetryRepository private let deviceMetadataProvider: DeviceMetadataProvider - private let configuration: () -> TelemetryServiceConfiguration + private let configuration: @Sendable () -> TelemetryServiceConfiguration - private var batcher: Batcher! // swiftlint:disable:this implicitly_unwrapped_optional + // swiftlint:disable:next implicitly_unwrapped_optional + private nonisolated(unsafe) var batcher: Batcher! // MARK: - Private Methods diff --git a/Sources/ProcessOut/Sources/Services/Telemetry/TelemetryServiceConfiguration.swift b/Sources/ProcessOut/Sources/Services/Telemetry/TelemetryServiceConfiguration.swift index c7692b4eb..7bad576a0 100644 --- a/Sources/ProcessOut/Sources/Services/Telemetry/TelemetryServiceConfiguration.swift +++ b/Sources/ProcessOut/Sources/Services/Telemetry/TelemetryServiceConfiguration.swift @@ -5,7 +5,7 @@ // Created by Andrii Vysotskyi on 09.04.2024. // -struct TelemetryServiceConfiguration { +struct TelemetryServiceConfiguration: Sendable { /// Indicates whether telemetry is enabled. let isTelemetryEnabled: Bool diff --git a/Sources/ProcessOut/Sources/Sessions/ApplePay/ApplePayAuthorizationSession.swift b/Sources/ProcessOut/Sources/Sessions/ApplePay/ApplePayAuthorizationSession.swift index eb1fc71c2..94d04e54d 100644 --- a/Sources/ProcessOut/Sources/Sessions/ApplePay/ApplePayAuthorizationSession.swift +++ b/Sources/ProcessOut/Sources/Sessions/ApplePay/ApplePayAuthorizationSession.swift @@ -7,6 +7,7 @@ import PassKit +@MainActor protocol ApplePayAuthorizationSession: Sendable { /// Begins an Apple Pay payment authorization. diff --git a/Sources/ProcessOut/Sources/Sessions/ApplePay/ApplePayAuthorizationSessionCoordinator.swift b/Sources/ProcessOut/Sources/Sessions/ApplePay/ApplePayAuthorizationSessionCoordinator.swift index 5d4c9880f..a177f49d1 100644 --- a/Sources/ProcessOut/Sources/Sessions/ApplePay/ApplePayAuthorizationSessionCoordinator.swift +++ b/Sources/ProcessOut/Sources/Sessions/ApplePay/ApplePayAuthorizationSessionCoordinator.swift @@ -10,7 +10,7 @@ import PassKit @MainActor final class ApplePayAuthorizationSessionCoordinator: NSObject, PKPaymentAuthorizationControllerDelegate { - init(delegate: ApplePayAuthorizationSessionDelegate?) { + nonisolated init(delegate: ApplePayAuthorizationSessionDelegate?) { self.delegate = delegate didFinish = false super.init() diff --git a/Sources/ProcessOut/Sources/Sessions/ApplePay/ApplePayAuthorizationSessionDelegate.swift b/Sources/ProcessOut/Sources/Sessions/ApplePay/ApplePayAuthorizationSessionDelegate.swift index dd1f26a72..91cee2284 100644 --- a/Sources/ProcessOut/Sources/Sessions/ApplePay/ApplePayAuthorizationSessionDelegate.swift +++ b/Sources/ProcessOut/Sources/Sessions/ApplePay/ApplePayAuthorizationSessionDelegate.swift @@ -8,7 +8,7 @@ import Foundation import PassKit -protocol ApplePayAuthorizationSessionDelegate: AnyObject { +protocol ApplePayAuthorizationSessionDelegate: AnyObject, Sendable { /// Sent to the delegate after the user has acted on the payment request. @MainActor diff --git a/Sources/ProcessOut/Sources/Sessions/ApplePay/DefaultApplePayAuthorizationSession.swift b/Sources/ProcessOut/Sources/Sessions/ApplePay/DefaultApplePayAuthorizationSession.swift index 7f6d68f7b..4deda493e 100644 --- a/Sources/ProcessOut/Sources/Sessions/ApplePay/DefaultApplePayAuthorizationSession.swift +++ b/Sources/ProcessOut/Sources/Sessions/ApplePay/DefaultApplePayAuthorizationSession.swift @@ -5,9 +5,8 @@ // Created by Andrii Vysotskyi on 03.09.2024. // -import PassKit +@preconcurrency import PassKit -@MainActor final class DefaultApplePayAuthorizationSession: ApplePayAuthorizationSession { nonisolated init() { @@ -31,7 +30,9 @@ final class DefaultApplePayAuthorizationSession: ApplePayAuthorizationSession { coordinator.setContinuation(continuation: continuation) } } onCancel: { - controller.dismiss() + Task { @MainActor in + await controller.dismiss() + } } await controller.dismiss() guard let payment = coordinator.payment else { diff --git a/Sources/ProcessOut/Sources/Sessions/WebAuthentication/DefaultWebAuthenticationSession.swift b/Sources/ProcessOut/Sources/Sessions/WebAuthentication/DefaultWebAuthenticationSession.swift new file mode 100644 index 000000000..8ee950b81 --- /dev/null +++ b/Sources/ProcessOut/Sources/Sessions/WebAuthentication/DefaultWebAuthenticationSession.swift @@ -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) { + 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? + + // MARK: - Private Methods + + private func _cancel() { + let failure = POFailure(message: "Authentication session was cancelled.", code: .cancelled) + session?.cancel() + continuation?.resume(throwing: failure) + invalidate() + } +} diff --git a/Sources/ProcessOut/Sources/Sessions/WebAuthentication/WebAuthenticationSession.swift b/Sources/ProcessOut/Sources/Sessions/WebAuthentication/WebAuthenticationSession.swift new file mode 100644 index 000000000..9ce8f6457 --- /dev/null +++ b/Sources/ProcessOut/Sources/Sessions/WebAuthentication/WebAuthenticationSession.swift @@ -0,0 +1,26 @@ +// +// WebAuthenticationSession.swift +// ProcessOut +// +// Created by Andrii Vysotskyi on 01.08.2024. +// + +import Foundation + +protocol WebAuthenticationSession: Sendable { + + /// Begins a web authentication session. + func authenticate( + using url: URL, callbackScheme: String?, additionalHeaderFields: [String: String]? + ) async throws -> URL +} + +extension WebAuthenticationSession { + + /// Begins a web authentication session. + func authenticate( + using url: URL, callbackScheme: String? = nil, additionalHeaderFields headerFields: [String: String]? = nil + ) async throws -> URL { + try await authenticate(using: url, callbackScheme: callbackScheme, additionalHeaderFields: headerFields) + } +} diff --git a/Sources/ProcessOut/Sources/UI/Modules/3DSRedirect/PO3DSRedirectViewControllerBuilder.swift b/Sources/ProcessOut/Sources/UI/Modules/3DSRedirect/PO3DSRedirectViewControllerBuilder.swift deleted file mode 100644 index d356642d8..000000000 --- a/Sources/ProcessOut/Sources/UI/Modules/3DSRedirect/PO3DSRedirectViewControllerBuilder.swift +++ /dev/null @@ -1,87 +0,0 @@ -// -// PO3DSRedirectViewControllerBuilder.swift -// ProcessOut -// -// Created by Andrii Vysotskyi on 04.11.2022. -// - -import UIKit -import SafariServices - -/// Builder that can be used to create view controller that is capable of handling 3DS web redirects. -@available(*, deprecated, message: "Use ProcessOutUI.SFSafariViewController(redirect:returnUrl:safariConfiguration:completion:) instead") // swiftlint:disable:this line_length -public final class PO3DSRedirectViewControllerBuilder { - - /// Creates builder instance with given redirect information. - /// - Parameters: - /// - redirect: redirect information. - @available(*, deprecated, message: "Use non static method instead.") - public static func with(redirect: PO3DSRedirect) -> PO3DSRedirectViewControllerBuilder { - PO3DSRedirectViewControllerBuilder().with(redirect: redirect) - } - - public typealias Completion = (Result) -> Void - - /// Creates builder instance. - public init() { - safariConfiguration = SFSafariViewController.Configuration() - } - - /// Changes redirect information. - public func with(redirect: PO3DSRedirect) -> Self { - self.redirect = redirect - return self - } - - /// Completion to invoke when authorization ends. - public func with(completion: @escaping Completion) -> Self { - self.completion = completion - return self - } - - /// Return URL specified when creating invoice. - public func with(returnUrl: URL) -> Self { - self.returnUrl = returnUrl - return self - } - - /// Allows to inject safari configuration. - public func with(safariConfiguration: SFSafariViewController.Configuration) -> Self { - self.safariConfiguration = safariConfiguration - return self - } - - /// Returns view controller that caller should incorporate into view controllers hierarchy. - /// - /// - Note: Caller should dismiss view controller after completion is called. - /// - Note: Returned object's delegate shouldn't be modified. - /// - Warning: Make sure that `completion`, `redirect` and `returnUrl` - /// are set before calling this method. Otherwise precondition failure is raised. - public func build() -> SFSafariViewController { - guard let completion, let returnUrl, let redirect else { - preconditionFailure("Required parameters are not set.") - } - let api: ProcessOut = ProcessOut.shared // swiftlint:disable:this redundant_type_annotation - let viewController = SFSafariViewController( - url: redirect.url, configuration: safariConfiguration - ) - let delegate = ThreeDSRedirectSafariViewModelDelegate(completion: completion) - let configuration = DefaultSafariViewModelConfiguration( - returnUrl: returnUrl, timeout: redirect.timeout - ) - let viewModel = DefaultSafariViewModel( - configuration: configuration, eventEmitter: api.eventEmitter, logger: api.logger, delegate: delegate - ) - viewController.delegate = viewModel - viewController.setViewModel(viewModel) - viewModel.start() - return viewController - } - - // MARK: - Private Properties - - private var redirect: PO3DSRedirect? - private var returnUrl: URL? - private var completion: Completion? - private var safariConfiguration: SFSafariViewController.Configuration -} diff --git a/Sources/ProcessOut/Sources/UI/Modules/3DSRedirect/ThreeDSRedirectSafariViewModelDelegate.swift b/Sources/ProcessOut/Sources/UI/Modules/3DSRedirect/ThreeDSRedirectSafariViewModelDelegate.swift deleted file mode 100644 index f5eb68c4d..000000000 --- a/Sources/ProcessOut/Sources/UI/Modules/3DSRedirect/ThreeDSRedirectSafariViewModelDelegate.swift +++ /dev/null @@ -1,44 +0,0 @@ -// -// ThreeDSRedirectSafariViewModelDelegate.swift -// ProcessOut -// -// Created by Andrii Vysotskyi on 04.11.2022. -// - -import WebKit - -@available(*, deprecated) -final class ThreeDSRedirectSafariViewModelDelegate: DefaultSafariViewModelDelegate { - - typealias Completion = (Result) -> Void - - init(completion: @escaping Completion) { - self.completion = completion - } - - // MARK: - DefaultSafariViewModelDelegate - - func complete(with url: URL) throws { - guard let components = URLComponents(url: url, resolvingAgainstBaseURL: true) else { - throw POFailure(message: nil, code: .internal(.mobile), underlyingError: nil) - } - let token = components.queryItems?.first { item in - item.name == Constants.tokenQueryItemName - } - completion(.success(token?.value ?? "")) - } - - func complete(with failure: POFailure) { - completion(.failure(failure)) - } - - // MARK: - Private Nested Types - - private enum Constants { - static let tokenQueryItemName = "token" - } - - // MARK: - Private Properties - - private let completion: Completion -} diff --git a/Sources/ProcessOut/Sources/UI/Modules/AlternativePaymentMethod/AlternativePaymentMethodSafariViewModelDelegate.swift b/Sources/ProcessOut/Sources/UI/Modules/AlternativePaymentMethod/AlternativePaymentMethodSafariViewModelDelegate.swift deleted file mode 100644 index d27384139..000000000 --- a/Sources/ProcessOut/Sources/UI/Modules/AlternativePaymentMethod/AlternativePaymentMethodSafariViewModelDelegate.swift +++ /dev/null @@ -1,35 +0,0 @@ -// -// AlternativePaymentMethodSafariViewModelDelegate.swift -// ProcessOut -// -// Created by Andrii Vysotskyi on 07.11.2022. -// - -import WebKit - -@available(*, deprecated) -final class AlternativePaymentMethodSafariViewModelDelegate: DefaultSafariViewModelDelegate { - - typealias Completion = (Result) -> Void - - init(alternativePaymentMethodsService: POAlternativePaymentMethodsService, completion: @escaping Completion) { - self.alternativePaymentMethodsService = alternativePaymentMethodsService - self.completion = completion - } - - // MARK: - DefaultSafariViewModelDelegate - - func complete(with url: URL) throws { - let response = try alternativePaymentMethodsService.alternativePaymentMethodResponse(url: url) - completion(.success(response)) - } - - func complete(with failure: POFailure) { - completion(.failure(failure)) - } - - // MARK: - Private Properties - - private let alternativePaymentMethodsService: POAlternativePaymentMethodsService - private let completion: Completion -} diff --git a/Sources/ProcessOut/Sources/UI/Modules/AlternativePaymentMethod/POAlternativePaymentMethodViewControllerBuilder.swift b/Sources/ProcessOut/Sources/UI/Modules/AlternativePaymentMethod/POAlternativePaymentMethodViewControllerBuilder.swift deleted file mode 100644 index 40e568dda..000000000 --- a/Sources/ProcessOut/Sources/UI/Modules/AlternativePaymentMethod/POAlternativePaymentMethodViewControllerBuilder.swift +++ /dev/null @@ -1,99 +0,0 @@ -// -// POAlternativePaymentMethodViewControllerBuilder.swift -// ProcessOut -// -// Created by Andrii Vysotskyi on 07.11.2022. -// - -import UIKit -import SafariServices - -/// Provides an ability to create view controller that could be used to handle Alternative Payment. Call build() to -/// create view controller’s instance. -@available(*, deprecated, message: "Use ProcessOutUI.SFSafariViewController(request:returnUrl:safariConfiguration:completion:) instead") // swiftlint:disable:this line_length -public final class POAlternativePaymentMethodViewControllerBuilder { - - /// Creates builder instance with given request. - @available(*, deprecated, message: "Use non static method instead.") - public static func with( - request: POAlternativePaymentMethodRequest - ) -> POAlternativePaymentMethodViewControllerBuilder { - POAlternativePaymentMethodViewControllerBuilder().with(request: request) - } - - public typealias Completion = (Result) -> Void - - /// Creates builder instance. - public init() { - safariConfiguration = SFSafariViewController.Configuration() - } - - /// Changes request. - public func with(request: POAlternativePaymentMethodRequest) -> Self { - self.url = ProcessOut.shared.alternativePaymentMethods.alternativePaymentMethodUrl(request: request) - return self - } - - /// Allows to inject initial URL instead of **request**. While doing so please note that - /// implementation does not validate whether given value is valid to actually start APM - /// flow. - public func with(url: URL) -> Self { - self.url = url - return self - } - - /// Completion to invoke when authorization ends. - public func with(completion: @escaping Completion) -> Self { - self.completion = completion - return self - } - - /// Return URL specified when creating invoice. - public func with(returnUrl: URL) -> Self { - self.returnUrl = returnUrl - return self - } - - /// Allows to inject safari configuration. - public func with(safariConfiguration: SFSafariViewController.Configuration) -> Self { - self.safariConfiguration = safariConfiguration - return self - } - - /// Creates and returns view controller that is capable of handling alternative payment request. - /// If instance can't be created precondition failure is triggered. - /// - /// - Note: Caller should dismiss view controller after completion is called. - /// - Note: Returned object's delegate shouldn't be modified. - /// - Warning: Make sure that `completion`, `request` and `returnUrl` are set - /// before calling this method. Otherwise precondition failure is raised. - public func build() -> SFSafariViewController { - guard let completion, let returnUrl, let url else { - preconditionFailure("Completion, return url and request must be set.") - } - let api: ProcessOut = ProcessOut.shared // swiftlint:disable:this redundant_type_annotation - let viewController = SFSafariViewController( - url: url, - configuration: safariConfiguration - ) - let delegate = AlternativePaymentMethodSafariViewModelDelegate( - alternativePaymentMethodsService: api.alternativePaymentMethods, - completion: completion - ) - let configuration = DefaultSafariViewModelConfiguration(returnUrl: returnUrl, timeout: nil) - let viewModel = DefaultSafariViewModel( - configuration: configuration, eventEmitter: api.eventEmitter, logger: api.logger, delegate: delegate - ) - viewController.delegate = viewModel - viewController.setViewModel(viewModel) - viewModel.start() - return viewController - } - - // MARK: - Private Properties - - private var url: URL? - private var completion: Completion? - private var returnUrl: URL? - private var safariConfiguration: SFSafariViewController.Configuration -} diff --git a/Sources/ProcessOut/Sources/UI/Modules/NativeAlternativePaymentMethod/Builder/PONativeAlternativePaymentMethodViewControllerBuilder.swift b/Sources/ProcessOut/Sources/UI/Modules/NativeAlternativePaymentMethod/Builder/PONativeAlternativePaymentMethodViewControllerBuilder.swift deleted file mode 100644 index f637b7c84..000000000 --- a/Sources/ProcessOut/Sources/UI/Modules/NativeAlternativePaymentMethod/Builder/PONativeAlternativePaymentMethodViewControllerBuilder.swift +++ /dev/null @@ -1,107 +0,0 @@ -// -// PONativeAlternativePaymentMethodModuleBuilder.swift -// ProcessOut -// -// Created by Andrii Vysotskyi on 19.10.2022. -// - -import UIKit - -/// Provides an ability to create view controller that could be used to handle Native -/// Alternative Payment. Call ``PONativeAlternativePaymentMethodViewControllerBuilder/build()`` -/// to create view controller's instance. -@available(*, deprecated, message: "Use ProcessOutUI.PONativeAlternativePaymentViewController instead.") -public final class PONativeAlternativePaymentMethodViewControllerBuilder { // swiftlint:disable:this type_name - - @available(*, deprecated, message: "Use non static method instead.") - public static func with( - invoiceId: String, gatewayConfigurationId: String - ) -> PONativeAlternativePaymentMethodViewControllerBuilder { - PONativeAlternativePaymentMethodViewControllerBuilder() - .with(invoiceId: invoiceId) - .with(gatewayConfigurationId: gatewayConfigurationId) - } - - /// Creates builder instance. - public init() { - configuration = .init() - style = PONativeAlternativePaymentMethodStyle() - } - - /// Invoice that that user wants to authorize via native APM. - public func with(invoiceId: String) -> Self { - self.invoiceId = invoiceId - return self - } - - /// Gateway configuration id. - public func with(gatewayConfigurationId: String) -> Self { - self.gatewayConfigurationId = gatewayConfigurationId - return self - } - - /// Completion to invoke after flow is completed. - public func with(completion: @escaping (Result) -> Void) -> Self { - self.completion = completion - return self - } - - /// Module's delegate. - /// - NOTE: Delegate is weakly referenced. - public func with(delegate: PONativeAlternativePaymentMethodDelegate) -> Self { - self.delegate = delegate - return self - } - - /// Sets UI configuration. - public func with(configuration: PONativeAlternativePaymentMethodConfiguration) -> Self { - self.configuration = configuration - return self - } - - /// Sets UI style. - public func with(style: PONativeAlternativePaymentMethodStyle) -> Self { - self.style = style - return self - } - - /// Returns view controller that caller should incorporate into view controllers hierarchy. - /// If instance can't be created assertion failure is triggered. - /// - /// - NOTE: Caller should dismiss view controller after completion is called. - public func build() -> UIViewController { - guard let gatewayConfigurationId, let invoiceId else { - preconditionFailure("Gateway configuration id and invoice id must be set.") - } - let api: ProcessOut = ProcessOut.shared // swiftlint:disable:this redundant_type_annotation - var logger = api.logger - logger[attributeKey: .invoiceId] = invoiceId - logger[attributeKey: .gatewayConfigurationId] = gatewayConfigurationId - let interactor = PODefaultNativeAlternativePaymentMethodInteractor( - invoicesService: api.invoices, - imagesRepository: api.images, - configuration: .init( - gatewayConfigurationId: gatewayConfigurationId, - invoiceId: invoiceId, - waitsPaymentConfirmation: configuration.waitsPaymentConfirmation, - paymentConfirmationTimeout: configuration.paymentConfirmationTimeout, - showPaymentConfirmationProgressIndicatorAfter: nil - ), - logger: logger, - delegate: delegate - ) - let viewModel = DefaultNativeAlternativePaymentMethodViewModel( - interactor: interactor, configuration: configuration, completion: completion - ) - return NativeAlternativePaymentMethodViewController(viewModel: viewModel, style: style, logger: logger) - } - - // MARK: - Private Properties - - private var gatewayConfigurationId: String? - private var invoiceId: String? - private var configuration: PONativeAlternativePaymentMethodConfiguration - private var style: PONativeAlternativePaymentMethodStyle - private var completion: ((Result) -> Void)? - private weak var delegate: PONativeAlternativePaymentMethodDelegate? -} diff --git a/Sources/ProcessOut/Sources/UI/Modules/NativeAlternativePaymentMethod/Interactor/PODefaultNativeAlternativePaymentMethodInteractor.swift b/Sources/ProcessOut/Sources/UI/Modules/NativeAlternativePaymentMethod/Interactor/PODefaultNativeAlternativePaymentMethodInteractor.swift deleted file mode 100644 index 670dbbdb1..000000000 --- a/Sources/ProcessOut/Sources/UI/Modules/NativeAlternativePaymentMethod/Interactor/PODefaultNativeAlternativePaymentMethodInteractor.swift +++ /dev/null @@ -1,563 +0,0 @@ -// -// PODefaultNativeAlternativePaymentMethodInteractor.swift -// ProcessOut -// -// Created by Andrii Vysotskyi on 19.10.2022. -// - -// swiftlint:disable file_length type_body_length - -import Foundation -import UIKit - -@_spi(PO) public final class PODefaultNativeAlternativePaymentMethodInteractor: - PONativeAlternativePaymentMethodInteractor { - - public init( - invoicesService: POInvoicesService, - imagesRepository: POImagesRepository, - configuration: PONativeAlternativePaymentMethodInteractorConfiguration, - logger: POLogger, - delegate: PONativeAlternativePaymentMethodDelegate? - ) { - self.invoicesService = invoicesService - self.imagesRepository = imagesRepository - self.configuration = configuration - self.logger = logger - self.delegate = delegate - state = .idle - } - - deinit { - captureCancellable?.cancel() - } - - // MARK: - NativeAlternativePaymentMethodInteractor - - public private(set) var state: State { - didSet { didChange?() } - } - - public var didChange: (() -> Void)? { - didSet { didChange?() } - } - - public func start() { - guard case .idle = state else { - return - } - logger.debug("Starting native alternative payment") - send(event: .willStart) - state = .starting - let request = PONativeAlternativePaymentMethodTransactionDetailsRequest( - invoiceId: configuration.invoiceId, gatewayConfigurationId: configuration.gatewayConfigurationId - ) - invoicesService.nativeAlternativePaymentMethodTransactionDetails(request: request) { [weak self] result in - switch result { - case let .success(details): - self?.defaultValues(for: details.parameters) { values in - self?.setStartedStateUnchecked(details: details, defaultValues: values) - } - case .failure(let failure): - self?.setFailureStateUnchecked(failure: failure) - } - } - } - - public func formatter(type: PONativeAlternativePaymentMethodParameter.ParameterType) -> Formatter? { - switch type { - case .phone: - return phoneNumberFormatter - default: - return nil - } - } - - public func updateValue(_ value: String?, for key: String) { - guard case let .started(startedState) = state, - let parameter = startedState.parameters.first(where: { $0.key == key }) else { - return - } - let formattedValue = formatted(value: value ?? "", type: parameter.type) - guard startedState.values[key]?.value != formattedValue else { - logger.debug("Ignored the same value for key: \(key)") - return - } - var updatedValues = startedState.values - updatedValues[key] = .init(value: formattedValue, recentErrorMessage: nil) - let updatedStartedState = startedState.replacing( - parameters: startedState.parameters, - values: updatedValues, - isSubmitAllowed: isSubmitAllowed(values: updatedValues) - ) - state = .started(updatedStartedState) - send(event: .parametersChanged(.init(parameter: parameter, value: formattedValue))) - logger.debug("Did update parameter value '\(value ?? "nil")' for '\(key)' key") - } - - public func submit() { - guard case let .started(startedState) = state, startedState.isSubmitAllowed else { - return - } - logger.debug("Will submit payment parameters") - let willSubmitParametersEvent = PONativeAlternativePaymentMethodEvent.WillSubmitParameters( - parameters: startedState.parameters, values: startedState.values.compactMapValues(\.value) - ) - send(event: .willSubmitParameters(willSubmitParametersEvent)) - do { - let values = try validated(values: startedState.values, for: startedState.parameters) - let request = PONativeAlternativePaymentMethodRequest( - invoiceId: configuration.invoiceId, - gatewayConfigurationId: configuration.gatewayConfigurationId, - parameters: values - ) - state = .submitting(snapshot: startedState) - invoicesService.initiatePayment(request: request) { [weak self] result in - switch result { - case let .success(response): - self?.completeSubmissionUnchecked(with: response, startedState: startedState) - case let .failure(failure): - self?.restoreStartedStateAfterSubmissionFailureIfPossible(failure, replaceErrorMessages: true) - } - } - } catch let error as POFailure { - restoreStartedStateAfterSubmissionFailureIfPossible(error) - } catch { - let failure = POFailure(code: .internal(.mobile), underlyingError: error) - restoreStartedStateAfterSubmissionFailureIfPossible(failure) - } - } - - public func cancel() { - logger.debug("Will attempt to cancel payment.") - switch state { - case .started: - setFailureStateUnchecked(failure: POFailure(code: .cancelled)) - case .awaitingCapture: - captureCancellable?.cancel() - default: - logger.info("Ignored cancellation attempt from unsupported state: \(String(describing: state))") - } - } - - // MARK: - Private Nested Types - - private enum Constants { - static let emailRegex = #"^\S+@\S+$"# - static let phoneRegex = #"^\+?\d{1,3}\d*$"# - } - - // MARK: - Private Properties - - private let invoicesService: POInvoicesService - private let imagesRepository: POImagesRepository - private let configuration: PONativeAlternativePaymentMethodInteractorConfiguration - private var logger: POLogger - private weak var delegate: PONativeAlternativePaymentMethodDelegate? - - private lazy var phoneNumberFormatter: POPhoneNumberFormatter = { - POPhoneNumberFormatter() - }() - - private var captureCancellable: POCancellable? - - // MARK: - State Management - - private func setStartedStateUnchecked( - details: PONativeAlternativePaymentMethodTransactionDetails, defaultValues: [String: State.ParameterValue] - ) { - switch details.state { - case .customerInput, nil: - break - case .pendingCapture: - logger.debug("No more parameters to submit, waiting for capture") - setAwaitingCaptureStateUnchecked(gateway: details.gateway, parameterValues: details.parameterValues) - return - case .captured: - setCapturedStateUnchecked(gateway: details.gateway, parameterValues: details.parameterValues) - return - case .failed: - setFailureStateUnchecked(failure: POFailure(code: .generic(.mobile))) - return - } - if details.parameters.isEmpty { - logger.debug("Will set started state with empty inputs, this may be unexpected") - } - let startedState = State.Started( - gateway: details.gateway, - amount: details.invoice.amount, - currencyCode: details.invoice.currencyCode, - parameters: details.parameters, - values: defaultValues, - isSubmitAllowed: isSubmitAllowed(values: defaultValues) - ) - state = .started(startedState) - send(event: .didStart) - logger.debug("Did start payment, waiting for parameters") - } - - // MARK: - Submission - - private func completeSubmissionUnchecked( - with response: PONativeAlternativePaymentMethodResponse, startedState: State.Started - ) { - switch response.nativeApm.state { - case .customerInput: - defaultValues(for: response.nativeApm.parameterDefinitions) { [weak self] values in - self?.restoreStartedStateAfterSubmission(nativeApm: response.nativeApm, defaultValues: values) - } - case .pendingCapture: - send(event: .didSubmitParameters(additionalParametersExpected: false)) - setAwaitingCaptureStateUnchecked( - gateway: startedState.gateway, parameterValues: response.nativeApm.parameterValues - ) - case .captured: - setCapturedStateUnchecked( - gateway: startedState.gateway, parameterValues: response.nativeApm.parameterValues - ) - case .failed: - let failure = POFailure(code: .generic(.mobile)) - setFailureStateUnchecked(failure: failure) - } - } - - // MARK: - Awaiting Capture State - - private func setAwaitingCaptureStateUnchecked( - gateway: PONativeAlternativePaymentMethodTransactionDetails.Gateway, - parameterValues: PONativeAlternativePaymentMethodParameterValues? - ) { - guard configuration.waitsPaymentConfirmation else { - logger.debug("Won't await payment capture because waitsPaymentConfirmation is set to false") - state = .submitted - return - } - let actionMessage = parameterValues?.customerActionMessage ?? gateway.customerActionMessage - let logoUrl = logoUrl(gateway: gateway, parameterValues: parameterValues) - send(event: .willWaitForCaptureConfirmation(additionalActionExpected: actionMessage != nil)) - imagesRepository.images(at: logoUrl, gateway.customerActionImageUrl) { [weak self] logo, actionImage in - guard let self else { - return - } - let request = PONativeAlternativePaymentCaptureRequest( - invoiceId: self.configuration.invoiceId, - gatewayConfigurationId: self.configuration.gatewayConfigurationId, - timeout: self.configuration.paymentConfirmationTimeout - ) - self.captureCancellable = self.invoicesService.captureNativeAlternativePayment( - request: request, - completion: { [weak self] result in - switch result { - case .success: - self?.setCapturedStateUnchecked(gateway: gateway, parameterValues: parameterValues) - case .failure(let failure): - self?.setFailureStateUnchecked(failure: failure) - } - } - ) - let awaitingCaptureState = State.AwaitingCapture( - paymentProviderName: parameterValues?.providerName, - logoImage: logo, - actionMessage: actionMessage, - actionImage: actionImage, - isDelayed: false - ) - self.state = .awaitingCapture(awaitingCaptureState) - self.logger.debug("Waiting for invoice capture confirmation") - self.schedulePaymentConfirmationDelay() - } - } - - private func schedulePaymentConfirmationDelay() { - guard let timeInterval = configuration.showPaymentConfirmationProgressIndicatorAfter else { - return - } - Timer.scheduledTimer(withTimeInterval: timeInterval, repeats: false) { [weak self] _ in - guard let self, case .awaitingCapture(let awaitingCaptureState) = self.state else { - return - } - let updatedState = awaitingCaptureState.replacing(isDelayed: true) - self.state = .awaitingCapture(updatedState) - } - } - - // MARK: - Captured State - - private func setCapturedStateUnchecked( - gateway: PONativeAlternativePaymentMethodTransactionDetails.Gateway, - parameterValues: PONativeAlternativePaymentMethodParameterValues? - ) { - logger.debug("Did receive invoice capture confirmation") - guard configuration.waitsPaymentConfirmation else { - logger.debug("Should't wait for confirmation, so setting submitted state instead of captured.") - state = .submitted - return - } - switch state { - case .awaitingCapture(let awaitingCaptureState): - let capturedState = State.Captured( - paymentProviderName: awaitingCaptureState.paymentProviderName, logoImage: awaitingCaptureState.logoImage - ) - state = .captured(capturedState) - send(event: .didCompletePayment) - default: - let logoUrl = logoUrl(gateway: gateway, parameterValues: parameterValues) - imagesRepository.image(at: logoUrl) { [weak self] logoImage in - let capturedState = State.Captured( - paymentProviderName: parameterValues?.providerName, logoImage: logoImage - ) - self?.state = .captured(capturedState) - self?.send(event: .didCompletePayment) - } - } - } - - // MARK: - Started State Restoration - - private func restoreStartedStateAfterSubmissionFailureIfPossible( - _ failure: POFailure, replaceErrorMessages: Bool = false - ) { - logger.debug("Did fail to submit parameters: \(failure)") - let startedState: State.Started - switch state { - case let .submitting(state), let .started(state): - startedState = state - default: - return - } - let invalidFields = failure.invalidFields.map { invalidFields in - Dictionary(grouping: invalidFields, by: \.name).compactMapValues(\.first) - } - guard let invalidFields = invalidFields, !invalidFields.isEmpty else { - logger.debug("Submission error is not recoverable, aborting") - setFailureStateUnchecked(failure: failure) - return - } - var updatedValues: [String: State.ParameterValue] = [:] - startedState.parameters.forEach { parameter in - let errorMessage: String? - if !replaceErrorMessages { - errorMessage = invalidFields[parameter.key]?.message - } else if invalidFields[parameter.key] != nil { - // Server doesn't support localized error messages, so local generic error - // description is used instead in case particular field is invalid. - // todo(andrii-vysotskyi): remove when backend is updated - switch parameter.type { - case .numeric: - errorMessage = String(resource: .NativeAlternativePayment.Error.invalidNumber) - case .text, .singleSelect: - errorMessage = String(resource: .NativeAlternativePayment.Error.invalidValue) - case .email: - errorMessage = String(resource: .NativeAlternativePayment.Error.invalidEmail) - case .phone: - errorMessage = String(resource: .NativeAlternativePayment.Error.invalidPhone) - } - } else { - errorMessage = nil - } - let value = startedState.values[parameter.key]?.value ?? "" - updatedValues[parameter.key] = .init(value: value, recentErrorMessage: errorMessage) - } - let updatedStartedState = startedState.replacing( - parameters: startedState.parameters, values: updatedValues, isSubmitAllowed: false - ) - self.state = .started(updatedStartedState) - send(event: .didFailToSubmitParameters(failure: failure)) - logger.debug("One or more parameters are not valid: \(invalidFields), waiting for parameters to update") - } - - private func restoreStartedStateAfterSubmission( - nativeApm: PONativeAlternativePaymentMethodResponse.NativeApm, defaultValues: [String: State.ParameterValue] - ) { - guard case let .submitting(startedState) = state else { - return - } - let parameters = nativeApm.parameterDefinitions ?? [] - let updatedStartedState = startedState.replacing( - parameters: parameters, values: defaultValues, isSubmitAllowed: isSubmitAllowed(values: defaultValues) - ) - state = .started(updatedStartedState) - send(event: .didSubmitParameters(additionalParametersExpected: true)) - logger.debug("More parameters are expected, waiting for parameters to update") - } - - // MARK: - Failure State - - private func setFailureStateUnchecked(failure: POFailure) { - logger.warn("Did fail to process native payment: \(failure)") - state = .failure(failure) - send(event: .didFail(failure: failure)) - } - - // MARK: - Utils - - private func isSubmitAllowed(values: [String: State.ParameterValue]) -> Bool { - values.values.allSatisfy { $0.recentErrorMessage == nil } - } - - private func send(event: PONativeAlternativePaymentMethodEvent) { - logger.debug("Did send event: '\(String(describing: event))'") - delegate?.nativeAlternativePaymentMethodDidEmitEvent(event) - } - - private func defaultValues( - for parameters: [PONativeAlternativePaymentMethodParameter]?, - completion: @escaping ([String: State.ParameterValue]) -> Void - ) { - guard let parameters, !parameters.isEmpty else { - completion([:]) - return - } - if let delegate { - delegate.nativeAlternativePaymentMethodDefaultValues(for: parameters) { [self] values in - assert(Thread.isMainThread, "Completion must be called on main thread.") - var defaultValues: [String: State.ParameterValue] = [:] - parameters.forEach { parameter in - let defaultValue: String - if let value = values[parameter.key] { - switch parameter.type { - case .email, .numeric, .phone, .text: - defaultValue = self.formatted(value: value, type: parameter.type) - case .singleSelect: - precondition( - parameter.availableValues?.map(\.value).contains(value) == true, - "Unknown `singleSelect` parameter value." - ) - defaultValue = value - } - } else { - defaultValue = self.defaultValue(for: parameter) - } - defaultValues[parameter.key] = .init(value: defaultValue, recentErrorMessage: nil) - } - completion(defaultValues) - } - } else { - var defaultValues: [String: State.ParameterValue] = [:] - parameters.forEach { parameter in - defaultValues[parameter.key] = .init(value: defaultValue(for: parameter), recentErrorMessage: nil) - } - completion(defaultValues) - } - } - - private func defaultValue(for parameter: PONativeAlternativePaymentMethodParameter) -> String { - switch parameter.type { - case .email, .numeric, .phone, .text: - return formatted(value: "", type: parameter.type) - case .singleSelect: - return parameter.availableValues?.first { $0.default == true }?.value ?? "" - } - } - - private func validated( - values: [String: State.ParameterValue], for parameters: [PONativeAlternativePaymentMethodParameter] - ) throws -> [String: String] { - var validatedValues: [String: String] = [:] - var invalidFields: [POFailure.InvalidField] = [] - parameters.forEach { parameter in - let value = values[parameter.key]?.value - let updatedValue: String? = { - if case .phone = parameter.type, let value { - return phoneNumberFormatter.normalized(number: value) - } - return value - }() - if let updatedValue, value != updatedValue { - logger.debug("Will use updated value '\(updatedValue)' for key '\(parameter.key)'") - } - if let invalidField = validate(value: updatedValue ?? "", for: parameter) { - invalidFields.append(invalidField) - } else { - validatedValues[parameter.key] = updatedValue - } - } - if invalidFields.isEmpty { - return validatedValues - } - throw POFailure(code: .validation(.general), invalidFields: invalidFields) - } - - private func validate( - value: String, for parameter: PONativeAlternativePaymentMethodParameter - ) -> POFailure.InvalidField? { - let message: String? - if value.isEmpty { - if parameter.required { - message = String(resource: .NativeAlternativePayment.Error.requiredParameter) - } else { - message = nil - } - } else if let length = parameter.length, value.count != length { - message = String(resource: .NativeAlternativePayment.Error.invalidLength, replacements: length) - } else { - switch parameter.type { - case .numeric where !CharacterSet(charactersIn: value).isSubset(of: .decimalDigits): - message = String(resource: .NativeAlternativePayment.Error.invalidNumber) - case .email where value.range(of: Constants.emailRegex, options: .regularExpression) == nil: - message = String(resource: .NativeAlternativePayment.Error.invalidEmail) - case .phone where value.range(of: Constants.phoneRegex, options: .regularExpression) == nil: - message = String(resource: .NativeAlternativePayment.Error.invalidPhone) - case .singleSelect where parameter.availableValues?.map(\.value).contains(value) == false: - message = String(resource: .NativeAlternativePayment.Error.invalidValue) - default: - message = nil - } - } - return message.map { POFailure.InvalidField(name: parameter.key, message: $0) } - } - - private func formatted(value: String, type: PONativeAlternativePaymentMethodParameter.ParameterType) -> String { - switch type { - case .phone: - return phoneNumberFormatter.string(from: value) - default: - return value - } - } - - private func logoUrl( - gateway: PONativeAlternativePaymentMethodTransactionDetails.Gateway, - parameterValues: PONativeAlternativePaymentMethodParameterValues? - ) -> URL? { - if parameterValues?.providerName != nil { - return parameterValues?.providerLogoUrl - } - return gateway.logoUrl - } -} - -// swiftlint:disable no_extension_access_modifier - -private extension PONativeAlternativePaymentMethodInteractorState.Started { - - func replacing( - parameters: [PONativeAlternativePaymentMethodParameter], - values: [String: PONativeAlternativePaymentMethodInteractorState.ParameterValue], - isSubmitAllowed: Bool - ) -> Self { - .init( - gateway: gateway, - amount: amount, - currencyCode: currencyCode, - parameters: parameters, - values: values, - isSubmitAllowed: isSubmitAllowed - ) - } -} - -private extension PONativeAlternativePaymentMethodInteractorState.AwaitingCapture { - - func replacing(isDelayed: Bool) -> Self { - .init( - paymentProviderName: paymentProviderName, - logoImage: logoImage, - actionMessage: actionMessage, - actionImage: actionImage, - isDelayed: isDelayed - ) - } -} - -// swiftlint:enable file_length type_body_length no_extension_access_modifier diff --git a/Sources/ProcessOut/Sources/UI/Modules/NativeAlternativePaymentMethod/Interactor/PONativeAlternativePaymentMethodDelegate.swift b/Sources/ProcessOut/Sources/UI/Modules/NativeAlternativePaymentMethod/Interactor/PONativeAlternativePaymentMethodDelegate.swift deleted file mode 100644 index 328476082..000000000 --- a/Sources/ProcessOut/Sources/UI/Modules/NativeAlternativePaymentMethod/Interactor/PONativeAlternativePaymentMethodDelegate.swift +++ /dev/null @@ -1,21 +0,0 @@ -// -// PONativeAlternativePaymentMethodDelegate.swift -// ProcessOut -// -// Created by Andrii Vysotskyi on 08.02.2023. -// - -/// Native alternative payment module delegate definition. -public protocol PONativeAlternativePaymentMethodDelegate: AnyObject { - - /// Invoked when module emits event. - func nativeAlternativePaymentMethodDidEmitEvent(_ event: PONativeAlternativePaymentMethodEvent) - - /// Method provides an ability to supply default values for given parameters. Completion expects dictionary - /// where key is a parameter key, and value is desired default. It is not mandatory to provide defaults for - /// all parameters. - /// - NOTE: completion must be called on `main` thread. - func nativeAlternativePaymentMethodDefaultValues( - for parameters: [PONativeAlternativePaymentMethodParameter], completion: @escaping ([String: String]) -> Void - ) -} diff --git a/Sources/ProcessOut/Sources/UI/Modules/NativeAlternativePaymentMethod/Interactor/PONativeAlternativePaymentMethodInteractor.swift b/Sources/ProcessOut/Sources/UI/Modules/NativeAlternativePaymentMethod/Interactor/PONativeAlternativePaymentMethodInteractor.swift deleted file mode 100644 index 67e1a6df7..000000000 --- a/Sources/ProcessOut/Sources/UI/Modules/NativeAlternativePaymentMethod/Interactor/PONativeAlternativePaymentMethodInteractor.swift +++ /dev/null @@ -1,37 +0,0 @@ -// -// PONativeAlternativePaymentMethodInteractor.swift -// ProcessOut -// -// Created by Andrii Vysotskyi on 19.10.2022. -// - -import Foundation - -// todo(andrii-vysotskyi): migrate interactor and dependencies to UI module when ready -@_spi(PO) public protocol PONativeAlternativePaymentMethodInteractor: AnyObject { - - typealias State = PONativeAlternativePaymentMethodInteractorState - - /// Interactor's state. - var state: State { get } - - /// A closure that is invoked after the object has changed. - var didChange: (() -> Void)? { get set } - - /// Starts interactor. - /// It's expected that implementation of this method should have logic responsible for - /// interactor starting process, e.g. loading initial content. - func start() - - /// Updates value for given key. - func updateValue(_ value: String?, for key: String) - - /// Returns formatter that could be used to format given value type if any. - func formatter(type: PONativeAlternativePaymentMethodParameter.ParameterType) -> Formatter? - - /// Submits parameters. - func submit() - - /// Cancells payment if possible. - func cancel() -} diff --git a/Sources/ProcessOut/Sources/UI/Modules/NativeAlternativePaymentMethod/Interactor/PONativeAlternativePaymentMethodInteractorConfiguration.swift b/Sources/ProcessOut/Sources/UI/Modules/NativeAlternativePaymentMethod/Interactor/PONativeAlternativePaymentMethodInteractorConfiguration.swift deleted file mode 100644 index 908fd5d85..000000000 --- a/Sources/ProcessOut/Sources/UI/Modules/NativeAlternativePaymentMethod/Interactor/PONativeAlternativePaymentMethodInteractorConfiguration.swift +++ /dev/null @@ -1,40 +0,0 @@ -// -// PONativeAlternativePaymentMethodInteractorConfiguration.swift -// ProcessOut -// -// Created by Andrii Vysotskyi on 02.03.2023. -// - -import Foundation - -@_spi(PO) public struct PONativeAlternativePaymentMethodInteractorConfiguration { // swiftlint:disable:this type_name - - /// Gateway configuration id. - public let gatewayConfigurationId: String - - /// Invoice identifier. - public let invoiceId: String - - /// Indicates whether interactor should wait for payment confirmation or not. - public let waitsPaymentConfirmation: Bool - - /// Maximum amount of time to wait for payment confirmation if it is enabled. - public let paymentConfirmationTimeout: TimeInterval - - /// Time to wait before showing progress indicator after payment confirmation starts. - public let showPaymentConfirmationProgressIndicatorAfter: TimeInterval? // swiftlint:disable:this identifier_name - - public init( - gatewayConfigurationId: String, - invoiceId: String, - waitsPaymentConfirmation: Bool, - paymentConfirmationTimeout: TimeInterval, - showPaymentConfirmationProgressIndicatorAfter: TimeInterval? // swiftlint:disable:this identifier_name - ) { - self.gatewayConfigurationId = gatewayConfigurationId - self.invoiceId = invoiceId - self.waitsPaymentConfirmation = waitsPaymentConfirmation - self.paymentConfirmationTimeout = paymentConfirmationTimeout - self.showPaymentConfirmationProgressIndicatorAfter = showPaymentConfirmationProgressIndicatorAfter - } -} diff --git a/Sources/ProcessOut/Sources/UI/Modules/NativeAlternativePaymentMethod/Interactor/PONativeAlternativePaymentMethodInteractorState.swift b/Sources/ProcessOut/Sources/UI/Modules/NativeAlternativePaymentMethod/Interactor/PONativeAlternativePaymentMethodInteractorState.swift deleted file mode 100644 index 22b2e3374..000000000 --- a/Sources/ProcessOut/Sources/UI/Modules/NativeAlternativePaymentMethod/Interactor/PONativeAlternativePaymentMethodInteractorState.swift +++ /dev/null @@ -1,93 +0,0 @@ -// -// PONativeAlternativePaymentMethodInteractorState.swift -// ProcessOut -// -// Created by Andrii Vysotskyi on 23.11.2023. -// - -import UIKit - -@_spi(PO) public enum PONativeAlternativePaymentMethodInteractorState { - - public struct ParameterValue { - - /// Actual parameter value. - public let value: String? - - /// The most recent error message associated with this parameter value. - public let recentErrorMessage: String? - } - - public struct Started { - - /// Name of the payment gateway that can be displayed. - public let gateway: PONativeAlternativePaymentMethodTransactionDetails.Gateway - - /// Invoice amount. - public let amount: Decimal - - /// Invoice currency code. - public let currencyCode: String - - /// Parameters that are expected from user. - public let parameters: [PONativeAlternativePaymentMethodParameter] - - /// Parameter values. - public let values: [String: ParameterValue] - - /// Boolean value indicating whether submit it currently allowed. - public let isSubmitAllowed: Bool - } - - public struct AwaitingCapture { - - /// Payment provider name. - public let paymentProviderName: String? - - /// Payment provider or gateway logo image. - public let logoImage: UIImage? - - /// Messaged describing additional actions that are needed from user in order to capture payment. - public let actionMessage: String? - - /// Action image. - public let actionImage: UIImage? - - /// Boolean value indicating whether capture takes longer than anticipated. - public let isDelayed: Bool - } - - public struct Captured { - - /// Payment provider name. - public let paymentProviderName: String? - - /// Payment provider or gateway logo image. - public let logoImage: UIImage? - } - - /// Initial interactor state. - case idle - - /// Interactor is loading initial content portion. - case starting - - /// Interactor is started and awaits for parameters values. - case started(Started) - - /// Starting failure. This is a sink state. - case failure(POFailure) - - /// Parameter values are being submitted. - case submitting(snapshot: Started) - - /// Parameter values were submitted. - /// - NOTE: This is a sink state and it's only set if user opted out from awaiting capture. - case submitted - - /// Parameters were submitted and accepted. - case awaitingCapture(AwaitingCapture) - - /// Payment is completed. - case captured(Captured) -} diff --git a/Sources/ProcessOut/Sources/UI/Modules/NativeAlternativePaymentMethod/Models/PONativeAlternativePaymentMethodConfiguration.swift b/Sources/ProcessOut/Sources/UI/Modules/NativeAlternativePaymentMethod/Models/PONativeAlternativePaymentMethodConfiguration.swift deleted file mode 100644 index e1177d167..000000000 --- a/Sources/ProcessOut/Sources/UI/Modules/NativeAlternativePaymentMethod/Models/PONativeAlternativePaymentMethodConfiguration.swift +++ /dev/null @@ -1,80 +0,0 @@ -// -// PONativeAlternativePaymentMethodConfiguration.swift -// ProcessOut -// -// Created by Andrii Vysotskyi on 22.11.2022. -// - -import Foundation - -/// A configuration object that defines how a native alternative payment view controller content's. -/// Use `nil` to indicate that default value should be used. -@available(*, deprecated, message: "Use ProcessOutUI.PONativeAlternativePaymentConfiguration instead.") -public struct PONativeAlternativePaymentMethodConfiguration { - - public enum SecondaryAction { - - /// Cancel action. - /// - /// - Parameters: - /// - title: Action title. Pass `nil` title to use default value. - /// - disabledFor: By default user can interact with action immediately after it becomes visible, it is - /// possible to make it initially disabled for given amount of time. - case cancel(title: String? = nil, disabledFor: TimeInterval = 0) - } - - /// Custom title. - public let title: String? - - /// Custom success message to display user when payment completes. - public let successMessage: String? - - /// Primary action text, such as "Pay". - public let primaryActionTitle: String? - - /// Secondary action. To remove secondary action use `nil`, this is default behaviour. - public let secondaryAction: SecondaryAction? - - /// 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). - /// - /// Default value is `5`. - public let inlineSingleSelectValuesLimit: Int - - /// Boolean value that indicates whether capture success screen should be skipped. Default value is `false`. - public let skipSuccessScreen: Bool - - /// Boolean value that specifies whether module should wait for payment confirmation from PSP or will - /// complete right after all user's input is submitted. Default value is `true`. - public let waitsPaymentConfirmation: Bool - - /// Amount of time (in seconds) that module is allowed to wait before receiving final payment confirmation. - /// Default timeout is 3 minutes while maximum value is 15 minutes. - public let paymentConfirmationTimeout: TimeInterval - - /// Action that could be optionally presented to user during payment confirmation stage. To remove action - /// use `nil`, this is default behaviour. - public let paymentConfirmationSecondaryAction: SecondaryAction? - - public init( - title: String? = nil, - successMessage: String? = nil, - primaryActionTitle: String? = nil, - secondaryAction: SecondaryAction? = nil, - inlineSingleSelectValuesLimit: Int = 5, - skipSuccessScreen: Bool = false, - waitsPaymentConfirmation: Bool = true, - paymentConfirmationTimeout: TimeInterval = 180, - paymentConfirmationSecondaryAction: SecondaryAction? = nil - ) { - self.title = title - self.successMessage = successMessage - self.primaryActionTitle = primaryActionTitle - self.secondaryAction = secondaryAction - self.inlineSingleSelectValuesLimit = inlineSingleSelectValuesLimit - self.skipSuccessScreen = skipSuccessScreen - self.waitsPaymentConfirmation = waitsPaymentConfirmation - self.paymentConfirmationTimeout = paymentConfirmationTimeout - self.paymentConfirmationSecondaryAction = paymentConfirmationSecondaryAction - } -} diff --git a/Sources/ProcessOut/Sources/UI/Modules/NativeAlternativePaymentMethod/Models/PONativeAlternativePaymentMethodEvent.swift b/Sources/ProcessOut/Sources/UI/Modules/NativeAlternativePaymentMethod/Models/PONativeAlternativePaymentMethodEvent.swift deleted file mode 100644 index e2ec3a1cf..000000000 --- a/Sources/ProcessOut/Sources/UI/Modules/NativeAlternativePaymentMethod/Models/PONativeAlternativePaymentMethodEvent.swift +++ /dev/null @@ -1,83 +0,0 @@ -// -// PONativeAlternativePaymentMethodEvent.swift -// ProcessOut -// -// Created by Andrii Vysotskyi on 07.02.2023. -// - -/// Describes events that could happen during native alternative payment module lifecycle. -public enum PONativeAlternativePaymentMethodEvent { - - public struct WillSubmitParameters { - - /// Available parameters. - public let parameters: [PONativeAlternativePaymentMethodParameter] - - /// Parameter values. - /// - NOTE: For parameters other than `singleSelect` values are user facing including formatting. - /// - WARNING: Values could include sensitive information so make sure to protect them accordingly. - public let values: [String: String] - - @_spi(PO) - public init(parameters: [PONativeAlternativePaymentMethodParameter], values: [String: String]) { - self.parameters = parameters - self.values = values - } - } - - public struct ParametersChanged { - - /// Parameter definition that the user changed. - public let parameter: PONativeAlternativePaymentMethodParameter - - /// Parameter value. - /// - NOTE: For parameters other than `singleSelect` this is user facing value including formatting - /// - WARNING: Value could include sensitive information so make sure to protect it accordingly. - public let value: String - - @_spi(PO) - public init(parameter: PONativeAlternativePaymentMethodParameter, value: String) { - self.parameter = parameter - self.value = value - } - } - - /// Initial event that is sent prior any other event. - case willStart - - /// Indicates that implementation successfully loaded initial portion of data and currently waiting for user - /// to fulfil needed info. - case didStart - - /// This event is emitted when a user clicks the "Cancel payment" button, prompting the system to display a - /// confirmation dialog. This event signifies the initiation of the cancellation confirmation process. - /// - /// This event can be used for tracking user interactions related to payment cancellations. It helps in - /// understanding user behaviour, particularly the frequency and context in which users consider canceling a payment. - case didRequestCancelConfirmation - - /// Event is sent when the user changes any editable value. - case parametersChanged(ParametersChanged) - - /// Event is sent just before sending user input, this is usually a result of a user action, e.g. button press. - case willSubmitParameters(WillSubmitParameters) - - /// Sent in case parameters were submitted successfully. You could inspect the associated value to understand - /// whether additional input is required. - case didSubmitParameters(additionalParametersExpected: Bool) - - /// Sent in case parameters submission failed and if error is retriable, otherwise expect `didFail` event. - case didFailToSubmitParameters(failure: POFailure) - - /// Event is sent after all information is collected, and implementation is waiting for a PSP to confirm capture. - /// You could check associated value `additionalActionExpected` to understand whether user needs - /// to execute additional action(s) outside application, for example confirming operation in his/her banking app - /// to make capture happen. - case willWaitForCaptureConfirmation(additionalActionExpected: Bool) - - /// Event is sent after payment was confirmed to be captured. This is a final event. - case didCompletePayment - - /// Event is sent in case unretryable error occurs. This is a final event. - case didFail(failure: POFailure) -} diff --git a/Sources/ProcessOut/Sources/UI/Modules/NativeAlternativePaymentMethod/Models/Style/PONativeAlternativePaymentMethodActionsStyle.swift b/Sources/ProcessOut/Sources/UI/Modules/NativeAlternativePaymentMethod/Models/Style/PONativeAlternativePaymentMethodActionsStyle.swift deleted file mode 100644 index 2ba2cb03e..000000000 --- a/Sources/ProcessOut/Sources/UI/Modules/NativeAlternativePaymentMethod/Models/Style/PONativeAlternativePaymentMethodActionsStyle.swift +++ /dev/null @@ -1,9 +0,0 @@ -// -// PONativeAlternativePaymentMethodActionsStyle.swift -// ProcessOut -// -// Created by Andrii Vysotskyi on 04.08.2023. -// - -@available(*, deprecated, renamed: "POActionsContainerStyle") -public typealias PONativeAlternativePaymentMethodActionsStyle = POActionsContainerStyle diff --git a/Sources/ProcessOut/Sources/UI/Modules/NativeAlternativePaymentMethod/Models/Style/PONativeAlternativePaymentMethodBackgroundStyle.swift b/Sources/ProcessOut/Sources/UI/Modules/NativeAlternativePaymentMethod/Models/Style/PONativeAlternativePaymentMethodBackgroundStyle.swift deleted file mode 100644 index 5dbb849c0..000000000 --- a/Sources/ProcessOut/Sources/UI/Modules/NativeAlternativePaymentMethod/Models/Style/PONativeAlternativePaymentMethodBackgroundStyle.swift +++ /dev/null @@ -1,25 +0,0 @@ -// -// PONativeAlternativePaymentBackgroundStyle.swift -// ProcessOut -// -// Created by Andrii Vysotskyi on 23.05.2023. -// - -import UIKit - -/// Native alternative payment method screen background style. -@available(*, deprecated, message: "Use ProcessOutUI.PONativeAlternativePaymentBackgroundStyle instead.") -public struct PONativeAlternativePaymentMethodBackgroundStyle { - - /// Regular background color. - public let regular: UIColor - - /// Background color to use in case of success. - public let success: UIColor - - /// Creates background style instance. - public init(regular: UIColor? = nil, success: UIColor? = nil) { - self.regular = regular ?? UIColor(poResource: .Surface.level1) - self.success = success ?? UIColor(poResource: .Surface.success) - } -} diff --git a/Sources/ProcessOut/Sources/UI/Modules/NativeAlternativePaymentMethod/Models/Style/PONativeAlternativePaymentMethodStyle.swift b/Sources/ProcessOut/Sources/UI/Modules/NativeAlternativePaymentMethod/Models/Style/PONativeAlternativePaymentMethodStyle.swift deleted file mode 100644 index 608481343..000000000 --- a/Sources/ProcessOut/Sources/UI/Modules/NativeAlternativePaymentMethod/Models/Style/PONativeAlternativePaymentMethodStyle.swift +++ /dev/null @@ -1,102 +0,0 @@ -// -// PONativeAlternativePaymentMethodStyle.swift -// ProcessOut -// -// Created by Andrii Vysotskyi on 22.11.2022. -// - -import UIKit - -/// Defines style for native alternative payment method module. -@available(*, deprecated, message: "Use ProcessOutUI.PONativeAlternativePaymentStyle instead.") -public struct PONativeAlternativePaymentMethodStyle { - - /// Title style. - public let title: POTextStyle - - /// Section title text style. - public let sectionTitle: POTextStyle - - /// Input style. - public let input: POInputStyle - - /// Input style. - public let codeInput: POInputStyle - - /// Radio button style. - public let radioButton: PORadioButtonStyle - - /// Error description text style. - public let errorDescription: POTextStyle - - /// Actions style. - public let actions: POActionsContainerStyle - - /// Activity indicator style. Please note that maximum height of activity indicator - /// is limited to 256. - public let activityIndicator: POActivityIndicatorStyle - - /// Message style. - /// - /// - NOTE: This style may be used to render rich attributed text so please make sure that your font's - /// typeface supports different variations. Currently framework may use bold, italic and mono space traits. - public let message: POTextStyle - - /// Success message style. - public let successMessage: POTextStyle - - /// Background style. - public let background: PONativeAlternativePaymentMethodBackgroundStyle - - /// Separator color. - public let separatorColor: UIColor - - public init( - title: POTextStyle? = nil, - sectionTitle: POTextStyle? = nil, - input: POInputStyle? = nil, - codeInput: POInputStyle? = nil, - radioButton: PORadioButtonStyle? = nil, - errorDescription: POTextStyle? = nil, - actions: POActionsContainerStyle? = nil, - activityIndicator: POActivityIndicatorStyle? = nil, - message: POTextStyle? = nil, - successMessage: POTextStyle? = nil, - background: PONativeAlternativePaymentMethodBackgroundStyle? = nil, - separatorColor: UIColor? = nil - ) { - self.title = title ?? Constants.title - self.sectionTitle = sectionTitle ?? Constants.sectionTitle - self.input = input ?? Constants.input - self.codeInput = codeInput ?? Constants.codeInput - self.radioButton = radioButton ?? Constants.radioButton - self.errorDescription = errorDescription ?? Constants.errorDescription - self.actions = actions ?? Constants.actions - self.activityIndicator = activityIndicator ?? Constants.activityIndicator - self.message = message ?? Constants.message - self.successMessage = successMessage ?? Constants.successMessage - self.background = background ?? Constants.background - self.separatorColor = separatorColor ?? Constants.separatorColor - } - - // MARK: - Private Nested Types - - private enum Constants { - static let title = POTextStyle(color: UIColor(poResource: .Text.primary), typography: .Medium.title) - static let sectionTitle = POTextStyle( - color: UIColor(poResource: .Text.secondary), typography: .Fixed.labelHeading - ) - static let input = POInputStyle.default() - static let codeInput = POInputStyle.default(typography: .Medium.title) - static let radioButton = PORadioButtonStyle.default - static let errorDescription = POTextStyle(color: UIColor(poResource: .Text.error), typography: .Fixed.label) - static let actions = POActionsContainerStyle() - static let activityIndicator = POActivityIndicatorStyle.system( - .large, color: UIColor(poResource: .Text.secondary) - ) - static let message = POTextStyle(color: UIColor(poResource: .Text.primary), typography: .Fixed.body) - static let successMessage = POTextStyle(color: UIColor(poResource: .Text.success), typography: .Fixed.body) - static let background = PONativeAlternativePaymentMethodBackgroundStyle() - static let separatorColor = UIColor(poResource: .Border.subtle) - } -} diff --git a/Sources/ProcessOut/Sources/UI/Modules/NativeAlternativePaymentMethod/Symbols/StringResource+NativeAlternativePayment.swift b/Sources/ProcessOut/Sources/UI/Modules/NativeAlternativePaymentMethod/Symbols/StringResource+NativeAlternativePayment.swift deleted file mode 100644 index 5d6c95b3d..000000000 --- a/Sources/ProcessOut/Sources/UI/Modules/NativeAlternativePaymentMethod/Symbols/StringResource+NativeAlternativePayment.swift +++ /dev/null @@ -1,73 +0,0 @@ -// -// StringResource+NativeAlternativePayment.swift -// ProcessOut -// -// Created by Andrii Vysotskyi on 23.01.2024. -// - -import Foundation - -// swiftlint:disable nesting - -extension POStringResource { - - enum NativeAlternativePayment { - - /// Screen title. - static let title = POStringResource("native-alternative-payment.title", comment: "") - - enum Placeholder { - - /// Email placeholder. - static let email = POStringResource("native-alternative-payment.email.placeholder", comment: "") - - /// Phone placeholder. - static let phone = POStringResource("native-alternative-payment.phone.placeholder", comment: "") - } - - enum Button { - - /// Pay. - static let submit = POStringResource("native-alternative-payment.submit-button.default-title", comment: "") - - /// Pay %@ - static let submitAmount = POStringResource("native-alternative-payment.submit-button.title", comment: "") - - /// Cancel button title. - static let cancel = POStringResource("native-alternative-payment.cancel-button.title", comment: "") - } - - enum Error { - - /// Email is not valid. - static let invalidEmail = POStringResource("native-alternative-payment.error.invalid-email", comment: "") - - /// Plural format key: "%#@length@" - static let invalidLength = POStringResource( - "native-alternative-payment.error.invalid-length-%d", comment: "" - ) - - /// Number is not valid. - static let invalidNumber = POStringResource("native-alternative-payment.error.invalid-number", comment: "") - - /// Phone number is not valid. - static let invalidPhone = POStringResource("native-alternative-payment.error.invalid-phone", comment: "") - - /// Value is not valid. - static let invalidValue = POStringResource("native-alternative-payment.error.invalid-value", comment: "") - - /// Parameter is required. - static let requiredParameter = POStringResource( - "native-alternative-payment.error.required-parameter", comment: "" - ) - } - - enum Success { - - /// Success message. - static let message = POStringResource("native-alternative-payment.success.message", comment: "") - } - } -} - -// swiftlint:enable nesting diff --git a/Sources/ProcessOut/Sources/UI/Modules/NativeAlternativePaymentMethod/View/Cells/NativeAlternativePaymentMethodCell.swift b/Sources/ProcessOut/Sources/UI/Modules/NativeAlternativePaymentMethod/View/Cells/NativeAlternativePaymentMethodCell.swift deleted file mode 100644 index a43130564..000000000 --- a/Sources/ProcessOut/Sources/UI/Modules/NativeAlternativePaymentMethod/View/Cells/NativeAlternativePaymentMethodCell.swift +++ /dev/null @@ -1,31 +0,0 @@ -// -// NativeAlternativePaymentMethodCell.swift -// ProcessOut -// -// Created by Andrii Vysotskyi on 27.04.2023. -// - -import UIKit - -@available(*, deprecated) -protocol NativeAlternativePaymentMethodCell: UICollectionViewCell { - - /// Tells the cell that it is about to be displayed. - func willDisplay() - - /// Tells the cell that it was removed from the collection view. - func didEndDisplaying() - - /// Should return input responder if any. - var inputResponder: UIResponder? { get } - - /// Cell delegate. - var delegate: NativeAlternativePaymentMethodCellDelegate? { get set } -} - -@available(*, deprecated) -protocol NativeAlternativePaymentMethodCellDelegate: AnyObject { - - /// Should return boolean value indicating whether cells input should return ie resign first responder. - func nativeAlternativePaymentMethodCellShouldReturn(_ cell: NativeAlternativePaymentMethodCell) -> Bool -} diff --git a/Sources/ProcessOut/Sources/UI/Modules/NativeAlternativePaymentMethod/View/Cells/NativeAlternativePaymentMethodCodeInputCell.swift b/Sources/ProcessOut/Sources/UI/Modules/NativeAlternativePaymentMethod/View/Cells/NativeAlternativePaymentMethodCodeInputCell.swift deleted file mode 100644 index 23b5607d5..000000000 --- a/Sources/ProcessOut/Sources/UI/Modules/NativeAlternativePaymentMethod/View/Cells/NativeAlternativePaymentMethodCodeInputCell.swift +++ /dev/null @@ -1,115 +0,0 @@ -// -// NativeAlternativePaymentMethodCodeInputCell.swift -// ProcessOut -// -// Created by Andrii Vysotskyi on 26.04.2023. -// - -import UIKit - -@available(*, deprecated) -final class NativeAlternativePaymentMethodCodeInputCell: UICollectionViewCell, NativeAlternativePaymentMethodCell { - - override func prepareForReuse() { - super.prepareForReuse() - item = nil - observations = [] - } - - func configure(item: NativeAlternativePaymentMethodViewModelState.CodeInputItem, style: POInputStyle) { - initialize(length: item.length) - codeTextField.configure(isInvalid: item.value.isInvalid, style: style, animated: false) - if codeTextField.text != item.value.text { - codeTextField.text = item.value.text - } - codeTextField.keyboardType = .numberPad - codeTextField.textContentType = .oneTimeCode - codeTextFieldCenterXConstraint.isActive = item.isCentered - self.item = item - self.style = style - } - - // MARK: - NativeAlternativePaymentMethodCell - - func willDisplay() { - guard let item, let style else { - return - } - let isInvalidObserver = item.value.$isInvalid.addObserver { [weak self] isInvalid in - self?.codeTextField.configure(isInvalid: isInvalid, style: style, animated: true) - } - let valueObserver = item.value.$text.addObserver { [weak self] updatedValue in - if self?.codeTextField.text != updatedValue { - self?.codeTextField.text = item.value.text - } - } - self.observations = [isInvalidObserver, valueObserver] - } - - func didEndDisplaying() { - observations = [] - } - - var inputResponder: UIResponder? { - codeTextField - } - - var delegate: NativeAlternativePaymentMethodCellDelegate? - - // MARK: - Private Nested Types - - private enum Constants { - static let accessibilityIdentifier = "native-alternative-payment.code-input" - } - - // MARK: - Private Properties - - // swiftlint:disable implicitly_unwrapped_optional - private var codeTextField: CodeTextField! - private var codeTextFieldCenterXConstraint: NSLayoutConstraint! - // swiftlint:enable implicitly_unwrapped_optional - - private var item: NativeAlternativePaymentMethodViewModelState.CodeInputItem? - private var style: POInputStyle? - private var observations: [AnyObject] = [] - - // MARK: - Private Methods - - private func initialize(length: Int) { - if let codeTextField, codeTextField.length == length { - return - } - codeTextField?.removeFromSuperview() - let codeTextField = CodeTextField(length: length) - codeTextField.accessibilityIdentifier = Constants.accessibilityIdentifier - codeTextField.delegate = self - codeTextField.addTarget(self, action: #selector(textFieldEditingChanged), for: .editingChanged) - contentView.addSubview(codeTextField) - let constraints = [ - codeTextField.leadingAnchor.constraint(equalTo: contentView.leadingAnchor).with(priority: .defaultHigh), - codeTextField.trailingAnchor.constraint(lessThanOrEqualTo: contentView.trailingAnchor), - codeTextField.centerYAnchor.constraint(equalTo: contentView.centerYAnchor) - ] - NSLayoutConstraint.activate(constraints) - self.codeTextField = codeTextField - // Center constraint is disabled by default - self.codeTextFieldCenterXConstraint = codeTextField.centerXAnchor.constraint(equalTo: contentView.centerXAnchor) - } - - @objc - private func textFieldEditingChanged() { - item?.value.text = codeTextField.text ?? "" - } -} - -@available(*, deprecated) -extension NativeAlternativePaymentMethodCodeInputCell: CodeTextFieldDelegate { - - func codeTextFieldShouldBeginEditing(_ textField: CodeTextField) -> Bool { - item?.value.isEditingAllowed ?? false - } - - func codeTextFieldShouldReturn(_ textField: CodeTextField) -> Bool { - delegate?.nativeAlternativePaymentMethodCellShouldReturn(self) ?? true - } -} diff --git a/Sources/ProcessOut/Sources/UI/Modules/NativeAlternativePaymentMethod/View/Cells/NativeAlternativePaymentMethodInputCell.swift b/Sources/ProcessOut/Sources/UI/Modules/NativeAlternativePaymentMethod/View/Cells/NativeAlternativePaymentMethodInputCell.swift deleted file mode 100644 index cdcab8450..000000000 --- a/Sources/ProcessOut/Sources/UI/Modules/NativeAlternativePaymentMethod/View/Cells/NativeAlternativePaymentMethodInputCell.swift +++ /dev/null @@ -1,146 +0,0 @@ -// -// NativeAlternativePaymentMethodInputCell.swift -// ProcessOut -// -// Created by Andrii Vysotskyi on 20.04.2023. -// - -import UIKit - -@available(*, deprecated) -final class NativeAlternativePaymentMethodInputCell: UICollectionViewCell, NativeAlternativePaymentMethodCell { - - override init(frame: CGRect) { - observations = [] - super.init(frame: frame) - commonInit() - } - - @available(*, unavailable) - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override func prepareForReuse() { - super.prepareForReuse() - item = nil - observations = [] - } - - func configure(item: NativeAlternativePaymentMethodViewModelState.InputItem, style: POInputStyle) { - textFieldContainer.configure(isInvalid: item.value.isInvalid, style: style, animated: false) - let textField = textFieldContainer.textField - if textField.text != item.value.text { - textField.text = item.value.text - } - textField.placeholder = item.placeholder - textField.returnKeyType = item.isLast ? .done : .next - textField.keyboardType = keyboardType(parameterType: item.type) - textField.textContentType = textContentType(parameterType: item.type) - self.item = item - self.style = style - } - - // MARK: - NativeAlternativePaymentMethodCell - - func willDisplay() { - guard let item, let style else { - return - } - let isInvalidObserver = item.value.$isInvalid.addObserver { [weak self] isInvalid in - self?.textFieldContainer.configure(isInvalid: isInvalid, style: style, animated: true) - } - let valueObserver = item.value.$text.addObserver { [weak self] updatedValue in - if self?.textFieldContainer.textField.text != updatedValue { - self?.textFieldContainer.textField.text = updatedValue - } - } - self.observations = [isInvalidObserver, valueObserver] - } - - func didEndDisplaying() { - observations = [] - } - - var inputResponder: UIResponder? { - textFieldContainer.textField - } - - weak var delegate: NativeAlternativePaymentMethodCellDelegate? - - // MARK: - Private Nested Types - - private enum Constants { - static let accessibilityIdentifier = "native-alternative-payment.generic-input" - } - - // MARK: - Private Properties - - private lazy var textFieldContainer: TextFieldContainerView = { - let view = TextFieldContainerView() - view.textField.accessibilityIdentifier = Constants.accessibilityIdentifier - view.textField.delegate = self - view.textField.addTarget(self, action: #selector(textFieldEditingChanged), for: .editingChanged) - return view - }() - - private var item: NativeAlternativePaymentMethodViewModelState.InputItem? - private var style: POInputStyle? - private var observations: [AnyObject] - - // MARK: - Private Methods - - private func commonInit() { - contentView.addSubview(textFieldContainer) - let constraints = [ - textFieldContainer.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), - textFieldContainer.centerXAnchor.constraint(equalTo: contentView.centerXAnchor), - textFieldContainer.centerYAnchor.constraint(equalTo: contentView.centerYAnchor) - ] - NSLayoutConstraint.activate(constraints) - } - - private func keyboardType( - parameterType: NativeAlternativePaymentMethodViewModelState.ParameterType - ) -> UIKeyboardType { - let keyboardTypes: [NativeAlternativePaymentMethodViewModelState.ParameterType: UIKeyboardType] = [ - .text: .asciiCapable, .email: .emailAddress, .numeric: .numberPad, .phone: .phonePad - ] - return keyboardTypes[parameterType] ?? .default - } - - private func textContentType( - parameterType: NativeAlternativePaymentMethodViewModelState.ParameterType - ) -> UITextContentType? { - let contentTypes: [NativeAlternativePaymentMethodViewModelState.ParameterType: UITextContentType] = [ - .email: .emailAddress, .numeric: .oneTimeCode, .phone: .telephoneNumber - ] - return contentTypes[parameterType] - } - - @objc - private func textFieldEditingChanged() { - item?.value.text = textFieldContainer.textField.text ?? "" - } -} - -@available(*, deprecated) -extension NativeAlternativePaymentMethodInputCell: UITextFieldDelegate { - - func textFieldShouldBeginEditing(_ textField: UITextField) -> Bool { - item?.value.isEditingAllowed ?? false - } - - func textFieldShouldReturn(_ textField: UITextField) -> Bool { - delegate?.nativeAlternativePaymentMethodCellShouldReturn(self) ?? true - } - - func textField( - _ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String - ) -> Bool { - guard let formatter = item?.formatter else { - return true - } - return TextFieldUtils.changeText(in: range, replacement: string, textField: textField, formatter: formatter) - } -} diff --git a/Sources/ProcessOut/Sources/UI/Modules/NativeAlternativePaymentMethod/View/Cells/NativeAlternativePaymentMethodLoaderCell.swift b/Sources/ProcessOut/Sources/UI/Modules/NativeAlternativePaymentMethod/View/Cells/NativeAlternativePaymentMethodLoaderCell.swift deleted file mode 100644 index 596d47307..000000000 --- a/Sources/ProcessOut/Sources/UI/Modules/NativeAlternativePaymentMethod/View/Cells/NativeAlternativePaymentMethodLoaderCell.swift +++ /dev/null @@ -1,36 +0,0 @@ -// -// NativeAlternativePaymentMethodLoaderCell.swift -// ProcessOut -// -// Created by Andrii Vysotskyi on 25.04.2023. -// - -import UIKit - -@available(*, deprecated) -final class NativeAlternativePaymentMethodLoaderCell: UICollectionViewCell { - - /// Implementation ignores 2nd and all subsequent calls to this method. - func initialize(style: POActivityIndicatorStyle) { - guard !isInitialized else { - return - } - let activityIndicator = ActivityIndicatorViewFactory().create(style: style) - activityIndicator.hidesWhenStopped = false - activityIndicator.setAnimating(true) - activityIndicator.translatesAutoresizingMaskIntoConstraints = false - contentView.addSubview(activityIndicator) - let constraints = [ - activityIndicator.leadingAnchor.constraint(greaterThanOrEqualTo: contentView.leadingAnchor), - activityIndicator.topAnchor.constraint(greaterThanOrEqualTo: contentView.topAnchor), - activityIndicator.centerXAnchor.constraint(equalTo: contentView.centerXAnchor), - activityIndicator.centerYAnchor.constraint(equalTo: contentView.centerYAnchor) - ] - NSLayoutConstraint.activate(constraints) - isInitialized = true - } - - // MARK: - Private Properties - - private var isInitialized = false -} diff --git a/Sources/ProcessOut/Sources/UI/Modules/NativeAlternativePaymentMethod/View/Cells/NativeAlternativePaymentMethodPickerCell.swift b/Sources/ProcessOut/Sources/UI/Modules/NativeAlternativePaymentMethod/View/Cells/NativeAlternativePaymentMethodPickerCell.swift deleted file mode 100644 index 4096cea7a..000000000 --- a/Sources/ProcessOut/Sources/UI/Modules/NativeAlternativePaymentMethod/View/Cells/NativeAlternativePaymentMethodPickerCell.swift +++ /dev/null @@ -1,59 +0,0 @@ -// -// NativeAlternativePaymentMethodPickerCell.swift -// ProcessOut -// -// Created by Andrii Vysotskyi on 03.05.2023. -// - -import UIKit - -@available(*, deprecated) -final class NativeAlternativePaymentMethodPickerCell: UICollectionViewCell { - - override init(frame: CGRect) { - super.init(frame: frame) - commonInit() - } - - @available(*, unavailable) - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - func configure(item: NativeAlternativePaymentMethodViewModelState.PickerItem, style: POInputStyle) { - let viewModel = PickerViewModel( - title: item.value, - isInvalid: item.isInvalid, - options: item.options.map { option in - PickerViewModel.Option(title: option.name, isSelected: option.isSelected, select: option.select) - } - ) - picker.configure(viewModel: viewModel, style: style, animated: false) - } - - // MARK: - Private Nested Types - - private enum Constants { - static let accessibilityIdentifier = "native-alternative-payment.picker" - } - - // MARK: - Private Properties - - private lazy var picker: Picker = { - let picker = Picker() - picker.accessibilityIdentifier = Constants.accessibilityIdentifier - return picker - }() - - // MARK: - Private Methods - - private func commonInit() { - contentView.addSubview(picker) - let constraints = [ - picker.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), - picker.centerXAnchor.constraint(equalTo: contentView.centerXAnchor), - picker.centerYAnchor.constraint(equalTo: contentView.centerYAnchor) - ] - NSLayoutConstraint.activate(constraints) - } -} diff --git a/Sources/ProcessOut/Sources/UI/Modules/NativeAlternativePaymentMethod/View/Cells/NativeAlternativePaymentMethodRadioCell.swift b/Sources/ProcessOut/Sources/UI/Modules/NativeAlternativePaymentMethod/View/Cells/NativeAlternativePaymentMethodRadioCell.swift deleted file mode 100644 index 49af2e399..000000000 --- a/Sources/ProcessOut/Sources/UI/Modules/NativeAlternativePaymentMethod/View/Cells/NativeAlternativePaymentMethodRadioCell.swift +++ /dev/null @@ -1,64 +0,0 @@ -// -// NativeAlternativePaymentMethodRadioCell.swift -// ProcessOut -// -// Created by Andrii Vysotskyi on 06.06.2023. -// - -import UIKit - -@available(*, deprecated) -final class NativeAlternativePaymentMethodRadioCell: UICollectionViewCell { - - override init(frame: CGRect) { - super.init(frame: frame) - commonInit() - } - - @available(*, unavailable) - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - func configure(item: NativeAlternativePaymentMethodViewModelState.RadioButtonItem, style: PORadioButtonStyle) { - let viewModel = RadioButtonViewModel( - isSelected: item.isSelected, isInError: item.isInvalid, value: item.value - ) - radioButton.configure(viewModel: viewModel, style: style, animated: false) - self.item = item - } - - // MARK: - Private Nested Types - - private enum Constants { - static let accessibilityIdentifier = "native-alternative-payment.radio-button" - } - - // MARK: - Private Properties - - private lazy var radioButton: RadioButton = { - let radioButton = RadioButton() - radioButton.addTarget(self, action: #selector(didTouchRadioButton), for: .touchUpInside) - radioButton.accessibilityIdentifier = Constants.accessibilityIdentifier - return radioButton - }() - - private var item: NativeAlternativePaymentMethodViewModelState.RadioButtonItem? - - // MARK: - Private Methods - - @objc private func didTouchRadioButton() { - item?.select() - } - - private func commonInit() { - contentView.addSubview(radioButton) - let constraints = [ - radioButton.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), - radioButton.topAnchor.constraint(equalTo: contentView.topAnchor), - radioButton.centerXAnchor.constraint(equalTo: contentView.centerXAnchor), - radioButton.centerYAnchor.constraint(equalTo: contentView.centerYAnchor) - ] - NSLayoutConstraint.activate(constraints) - } -} diff --git a/Sources/ProcessOut/Sources/UI/Modules/NativeAlternativePaymentMethod/View/Cells/Submitted/NativeAlternativePaymentMethodSubmittedCell.swift b/Sources/ProcessOut/Sources/UI/Modules/NativeAlternativePaymentMethod/View/Cells/Submitted/NativeAlternativePaymentMethodSubmittedCell.swift deleted file mode 100644 index 27fdd30d1..000000000 --- a/Sources/ProcessOut/Sources/UI/Modules/NativeAlternativePaymentMethod/View/Cells/Submitted/NativeAlternativePaymentMethodSubmittedCell.swift +++ /dev/null @@ -1,181 +0,0 @@ -// -// NativeAlternativePaymentMethodSubmittedCell.swift -// ProcessOut -// -// Created by Andrii Vysotskyi on 27.04.2023. -// - -import UIKit - -@available(*, deprecated) -final class NativeAlternativePaymentMethodSubmittedCell: UICollectionViewCell { - - override init(frame: CGRect) { - super.init(frame: frame) - commonInit() - } - - @available(*, unavailable) - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - // swiftlint:disable:next function_body_length - func configure( - item: NativeAlternativePaymentMethodViewModelState.SubmittedItem, - style: NativeAlternativePaymentMethodSubmittedCellStyle - ) { - let isMessageCompact = item.message.count <= Constants.maximumCompactMessageLength - if isMessageCompact { - containerViewTopConstraint.constant = Constants.topContentInset - } else { - containerViewTopConstraint.constant = Constants.compactTopContentInset - } - if let image = item.logoImage { - iconImageView.image = image - iconImageView.setAspectRatio(image.size.width / image.size.height) - iconImageViewWidthConstraint.constant = image.size.width - iconImageView.setHidden(false) - } else { - iconImageView.setHidden(true) - } - let descriptionStyle: POTextStyle - if item.isCaptured { - descriptionStyle = style.successMessage - descriptionTextView.accessibilityIdentifier = "native-alternative-payment.captured.description" - } else { - descriptionStyle = style.message - descriptionTextView.accessibilityIdentifier = "native-alternative-payment.non-captured.description" - } - descriptionTextView.attributedText = AttributedStringBuilder() - .with { builder in - builder.typography = descriptionStyle.typography - builder.textStyle = .body - builder.color = descriptionStyle.color - builder.lineBreakMode = .byWordWrapping - builder.alignment = isMessageCompact ? .center : .natural - builder.text = .markdown(item.message) - } - .build() - if let title = item.title { - titleLabel.attributedText = AttributedStringBuilder() - .with { builder in - builder.typography = style.title.typography - builder.textStyle = .largeTitle - builder.alignment = .center - builder.lineBreakMode = .byWordWrapping - builder.color = descriptionStyle.color - builder.text = .plain(title) - } - .build() - titleLabel.setHidden(false) - } else { - titleLabel.setHidden(true) - } - if let image = item.image { - decorationImageView.image = image - decorationImageView.tintColor = descriptionStyle.color - decorationImageView.setAspectRatio(image.size.width / image.size.height) - decorationImageViewWidthConstraint.constant = image.size.width - decorationImageView.setHidden(false) - } else { - decorationImageView.setHidden(true) - } - containerView.setCustomSpacing( - item.isCaptured ? Constants.descriptionBottomSpacing : Constants.descriptionBottomSmallSpacing, - after: descriptionTextView - ) - } - - // MARK: - Private Nested Types - - private enum Constants { - static let maximumLogoImageHeight: CGFloat = 32 - static let maximumDecorationImageHeight: CGFloat = 260 - static let verticalSpacing: CGFloat = 16 - static let descriptionBottomSpacing: CGFloat = 46 - static let descriptionBottomSmallSpacing: CGFloat = 24 - static let topContentInset: CGFloat = 68 - static let compactTopContentInset: CGFloat = 24 - static let maximumCompactMessageLength = 150 - } - - // MARK: - Private Properties - - private lazy var containerView: UIStackView = { - let view = UIStackView(arrangedSubviews: [titleLabel, iconImageView, descriptionTextView, decorationImageView]) - view.translatesAutoresizingMaskIntoConstraints = false - view.spacing = Constants.verticalSpacing - view.axis = .vertical - view.alignment = .center - return view - }() - - private lazy var titleLabel: UILabel = { - let label = UILabel() - label.translatesAutoresizingMaskIntoConstraints = false - label.adjustsFontForContentSizeCategory = false - label.numberOfLines = 0 - label.setContentCompressionResistancePriority(.required, for: .vertical) - label.setContentHuggingPriority(.required, for: .vertical) - return label - }() - - private lazy var iconImageView: UIImageView = { - let imageView = UIImageView() - imageView.translatesAutoresizingMaskIntoConstraints = false - return imageView - }() - - private lazy var descriptionTextView: UITextView = { - let textView = UITextView() - textView.backgroundColor = .clear - textView.translatesAutoresizingMaskIntoConstraints = false - textView.textContainerInset = .zero - textView.textContainer.lineFragmentPadding = 0 - textView.setContentHuggingPriority(.required, for: .vertical) - textView.setContentCompressionResistancePriority(.required, for: .vertical) - textView.adjustsFontForContentSizeCategory = false - textView.isScrollEnabled = false - textView.isEditable = false - textView.isSelectable = true - return textView - }() - - private lazy var decorationImageView: UIImageView = { - let imageView = UIImageView() - imageView.translatesAutoresizingMaskIntoConstraints = false - return imageView - }() - - private lazy var iconImageViewWidthConstraint - = iconImageView.widthAnchor.constraint(equalToConstant: 0).with(priority: .defaultHigh) - - private lazy var decorationImageViewWidthConstraint - = decorationImageView.widthAnchor.constraint(equalToConstant: 0).with(priority: .defaultHigh) - - private lazy var containerViewTopConstraint - = containerView.topAnchor.constraint(equalTo: contentView.topAnchor) - - // MARK: - Private Methods - - private func commonInit() { - contentView.addSubview(containerView) - let constraints = [ - containerViewTopConstraint, - containerView.centerXAnchor.constraint(equalTo: contentView.centerXAnchor), - containerView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), - containerView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor).with(priority: .defaultHigh), - iconImageView.heightAnchor.constraint(lessThanOrEqualToConstant: Constants.maximumLogoImageHeight), - iconImageView.widthAnchor.constraint(lessThanOrEqualTo: containerView.widthAnchor), - iconImageViewWidthConstraint, - descriptionTextView.widthAnchor.constraint(equalTo: containerView.widthAnchor), - decorationImageView.heightAnchor.constraint( - lessThanOrEqualToConstant: Constants.maximumDecorationImageHeight - ), - decorationImageView.widthAnchor.constraint(lessThanOrEqualTo: containerView.widthAnchor), - decorationImageViewWidthConstraint - ] - NSLayoutConstraint.activate(constraints) - } -} diff --git a/Sources/ProcessOut/Sources/UI/Modules/NativeAlternativePaymentMethod/View/Cells/Submitted/NativeAlternativePaymentMethodSubmittedCellStyle.swift b/Sources/ProcessOut/Sources/UI/Modules/NativeAlternativePaymentMethod/View/Cells/Submitted/NativeAlternativePaymentMethodSubmittedCellStyle.swift deleted file mode 100644 index c5bbb51fd..000000000 --- a/Sources/ProcessOut/Sources/UI/Modules/NativeAlternativePaymentMethod/View/Cells/Submitted/NativeAlternativePaymentMethodSubmittedCellStyle.swift +++ /dev/null @@ -1,19 +0,0 @@ -// -// NativeAlternativePaymentMethodSubmittedCellStyle.swift -// ProcessOut -// -// Created by Andrii Vysotskyi on 27.04.2023. -// - -@available(*, deprecated) -struct NativeAlternativePaymentMethodSubmittedCellStyle { - - /// Title style. - let title: POTextStyle - - /// Message style. - let message: POTextStyle - - /// Success message style. - let successMessage: POTextStyle -} diff --git a/Sources/ProcessOut/Sources/UI/Modules/NativeAlternativePaymentMethod/View/NativeAlternativePaymentMethodViewController.swift b/Sources/ProcessOut/Sources/UI/Modules/NativeAlternativePaymentMethod/View/NativeAlternativePaymentMethodViewController.swift deleted file mode 100644 index 5105cae51..000000000 --- a/Sources/ProcessOut/Sources/UI/Modules/NativeAlternativePaymentMethod/View/NativeAlternativePaymentMethodViewController.swift +++ /dev/null @@ -1,503 +0,0 @@ -// -// NativeAlternativePaymentMethodViewController.swift -// ProcessOut -// -// Created by Andrii Vysotskyi on 20.04.2023. -// - -// swiftlint:disable type_body_length file_length - -import UIKit - -@available(*, deprecated) -final class NativeAlternativePaymentMethodViewController: - BaseViewController, - CollectionViewDelegateCenterLayout, - NativeAlternativePaymentMethodCellDelegate { - - init(viewModel: ViewModel, style: PONativeAlternativePaymentMethodStyle, logger: POLogger) { - self.style = style - self.logger = logger - keyboardHeight = 0 - super.init(viewModel: viewModel, logger: logger) - } - - // MARK: - UIViewController - - override func viewDidLoad() { - configureCollectionView() - super.viewDidLoad() - } - - override func loadView() { - view = UIView() - view.addSubview(collectionView) - view.addSubview(collectionOverlayView) - collectionOverlayView.addSubview(buttonsContainerView) - let constraints = [ - collectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor), - collectionView.centerXAnchor.constraint(equalTo: view.centerXAnchor), - collectionView.topAnchor.constraint(equalTo: view.topAnchor), - collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor), - collectionOverlayView.topAnchor.constraint(equalTo: view.topAnchor), - collectionOverlayView.leadingAnchor.constraint(equalTo: view.leadingAnchor), - collectionOverlayView.centerXAnchor.constraint(equalTo: view.centerXAnchor), - collectionOverlayView.bottomAnchor.constraint(equalTo: view.bottomAnchor), - buttonsContainerView.leadingAnchor.constraint(equalTo: collectionOverlayView.leadingAnchor), - buttonsContainerView.centerXAnchor.constraint(equalTo: collectionOverlayView.centerXAnchor), - buttonsContainerView.bottomAnchor.constraint(equalTo: collectionOverlayView.bottomAnchor) - ] - NSLayoutConstraint.activate(constraints) - } - - override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { - super.traitCollectionDidChange(previousTraitCollection) - guard traitCollection.preferredContentSizeCategory != previousTraitCollection?.preferredContentSizeCategory, - case .started(let startedState) = viewModel.state else { - return - } - configure(with: startedState, reload: true, animated: false) - } - - override func viewSafeAreaInsetsDidChange() { - super.viewSafeAreaInsetsDidChange() - configureCollectionViewBottomInset(state: viewModel.state) - collectionViewLayout.invalidateLayout() - } - - // MARK: - BaseViewController - - override func configure(with state: ViewModel.State, animated: Bool) { - super.configure(with: state, animated: animated) - configureCollectionViewBottomInset(state: state) - switch state { - case .idle: - configureWithIdleState() - case .started(let startedState): - configure(with: startedState, animated: animated) - } - } - - override func keyboardWillChange(newHeight: CGFloat) { - super.keyboardWillChange(newHeight: newHeight) - collectionView.performBatchUpdates { - self.keyboardHeight = newHeight - self.configureCollectionViewBottomInset(state: self.viewModel.state) - } - buttonsContainerView.additionalBottomSafeAreaInset = newHeight - collectionOverlayView.layoutIfNeeded() - } - - // MARK: - CollectionViewDelegateCenterLayout - - func centeredSection(layout: UICollectionViewLayout) -> Int? { - let snapshot = collectionViewDataSource.snapshot() - for (section, sectionId) in snapshot.sectionIdentifiers.enumerated() { - for item in snapshot.itemIdentifiers(inSection: sectionId) { - switch item { - case .loader, .codeInput, .input, .picker, .radio: - return section - default: - break - } - } - } - return nil - } - - func collectionViewLayout(_ layout: UICollectionViewLayout, shouldSeparateCellAt indexPath: IndexPath) -> Bool { - switch collectionViewDataSource.itemIdentifier(for: indexPath) { - case .title: - return true - default: - return false - } - } - - func collectionView(_: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) { - let cell = cell as? NativeAlternativePaymentMethodCell - cell?.willDisplay() - } - - func collectionView( - _: UICollectionView, didEndDisplaying cell: UICollectionViewCell, forItemAt indexPath: IndexPath - ) { - let cell = cell as? NativeAlternativePaymentMethodCell - cell?.didEndDisplaying() - } - - // MARK: - UICollectionViewDelegateFlowLayout - - func collectionView( - _ collectionView: UICollectionView, - layout collectionViewLayout: UICollectionViewLayout, - sizeForItemAt indexPath: IndexPath - ) -> CGSize { - // todo(andrii-vysotskyi): consider migrating to self-sizing to evict boilerplate sizing code - let adjustedBounds = collectionView.bounds.inset(by: collectionView.adjustedContentInset) - let height: CGFloat - switch collectionViewDataSource.itemIdentifier(for: indexPath) { - case .loader: - height = Constants.loaderHeight - case .title(let item): - height = collectionReusableViewSizeProvider.systemLayoutSize( - viewType: CollectionViewTitleCell.self, - preferredWidth: adjustedBounds.width, - configure: { cell in - cell.configure(viewModel: item, style: self.style.title) - } - ).height - case .error(let item): - height = collectionReusableViewSizeProvider.systemLayoutSize( - viewType: CollectionViewErrorCell.self, - preferredWidth: adjustedBounds.width, - configure: { cell in - cell.configure(viewModel: item, style: self.style.errorDescription) - } - ).height - case .submitted(let item): - height = collectionReusableViewSizeProvider.systemLayoutSize( - viewType: NativeAlternativePaymentMethodSubmittedCell.self, - preferredWidth: adjustedBounds.width, - configure: { cell in - let style = NativeAlternativePaymentMethodSubmittedCellStyle( - title: self.style.title, message: self.style.message, successMessage: self.style.successMessage - ) - cell.configure(item: item, style: style) - } - ).height - case .input, .codeInput, .picker: - height = Constants.inputHeight - case .radio(let item): - height = collectionReusableViewSizeProvider.systemLayoutSize( - viewType: NativeAlternativePaymentMethodRadioCell.self, - preferredWidth: adjustedBounds.width, - configure: { cell in - cell.configure(item: item, style: self.style.radioButton) - } - ).height - case nil: - height = .zero - } - return CGSize(width: adjustedBounds.width, height: height) - } - - func collectionView( - _ collectionView: UICollectionView, - layout collectionViewLayout: UICollectionViewLayout, - referenceSizeForHeaderInSection section: Int - ) -> CGSize { - let sectionIdentifier = collectionViewDataSource.snapshot().sectionIdentifiers[section] - guard let sectionHeader = sectionIdentifier.header else { - return .zero - } - let width = collectionView.bounds.inset(by: collectionView.adjustedContentInset).width - return collectionReusableViewSizeProvider.systemLayoutSize( - viewType: CollectionViewSectionHeaderView.self, - preferredWidth: width, - configure: { [self] view in - view.configure(viewModel: sectionHeader, style: style.sectionTitle) - } - ) - } - - func collectionView( - _ collectionView: UICollectionView, - layout collectionViewLayout: UICollectionViewLayout, - insetForSectionAt section: Int - ) -> UIEdgeInsets { - let snapshot = collectionViewDataSource.snapshot() - var sectionInset = Constants.sectionInset - if snapshot.sectionIdentifiers[section].header == nil { - // Top inset purpose is to add spacing between header and items, - // for sections without header instead is 0 - sectionInset.top = 0 - } - if section + 1 == snapshot.numberOfSections { - // Bottom inset purpose is to add spacing between sections, it's - // not needed in last section. - sectionInset.bottom = 0 - } - return sectionInset - } - - func collectionView( - _ collectionView: UICollectionView, layout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int - ) -> CGFloat { - if let identifier = collectionViewDataSource.sectionIdentifier(for: section), identifier.isTight { - return Constants.tightLineSpacing - } - return Constants.lineSpacing - } - - // MARK: - Scroll View Delegate - - func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) { - updateFirstResponder() - } - - // MARK: - NativeAlternativePaymentMethodCellDelegate - - func nativeAlternativePaymentMethodCellShouldReturn(_ cell: NativeAlternativePaymentMethodCell) -> Bool { - let visibleIndexPaths = collectionView.indexPathsForVisibleItems.sorted() - guard let indexPath = collectionView.indexPath(for: cell), - let nextIndex = visibleIndexPaths.firstIndex(of: indexPath)?.advanced(by: 1), - visibleIndexPaths.indices.contains(nextIndex) else { - viewModel.submit() - return true - } - for indexPath in visibleIndexPaths.suffix(from: nextIndex) { - guard let cell = collectionView.cellForItem(at: indexPath) as? NativeAlternativePaymentMethodCell, - let responder = cell.inputResponder else { - continue - } - if responder.becomeFirstResponder() { - collectionView.scrollToItem(at: indexPath, at: .top, animated: true) - } - return true - } - viewModel.submit() - return true - } - - // MARK: - Private Nested Types - - private typealias SectionIdentifier = ViewModel.State.SectionIdentifier - private typealias ItemIdentifier = ViewModel.State.Item - - // MARK: - Private Properties - - private let style: PONativeAlternativePaymentMethodStyle - private let logger: POLogger - - private lazy var collectionOverlayView: UIView = { - let view = PassthroughView() - view.translatesAutoresizingMaskIntoConstraints = false - return view - }() - - private lazy var buttonsContainerView = ActionsContainerView( - style: style.actions, horizontalInset: Constants.contentInset.left - ) - - private lazy var collectionView: UICollectionView = { - let collectionView = UICollectionView(frame: .zero, collectionViewLayout: collectionViewLayout) - collectionView.delegate = self - collectionView.translatesAutoresizingMaskIntoConstraints = false - collectionView.backgroundColor = nil - collectionView.contentInset = Constants.contentInset - collectionView.showsVerticalScrollIndicator = false - return collectionView - }() - - private lazy var collectionViewLayout = CollectionViewCenterLayout() - private lazy var collectionReusableViewSizeProvider = CollectionReusableViewSizeProvider() - - private lazy var collectionViewDataSource: CollectionViewDiffableDataSource = { - let dataSource = CollectionViewDiffableDataSource( - collectionView: collectionView, - cellProvider: { [unowned self] _, indexPath, itemIdentifier in - cell(for: itemIdentifier, at: indexPath) - } - ) - dataSource.supplementaryViewProvider = { [unowned self] _, kind, indexPath in - supplementaryView(ofKind: kind, at: indexPath) - } - return dataSource - }() - - private var keyboardHeight: CGFloat - - // MARK: - State Management - - private func configureWithIdleState() { - buttonsContainerView.configure(viewModel: .init(primary: nil, secondary: nil), animated: false) - let snapshot = DiffableDataSourceSnapshot() - collectionViewDataSource.applySnapshotUsingReloadData(snapshot) - view.backgroundColor = style.background.regular - } - - /// - Parameters: - /// - reload: Allows to force reload even if new data source is not different from current. This is useful if data - /// didn't change but its known that content should change due to external conditions e.g. updated - /// traitCollection. - private func configure(with state: ViewModel.State.Started, reload: Bool = false, animated: Bool) { - var snapshot = DiffableDataSourceSnapshot() - snapshot.appendSections(state.sections.map(\.id)) - for section in state.sections { - snapshot.appendItems(section.items, toSection: section.id) - } - if reload { - snapshot.reloadSections(collectionViewDataSource.snapshot().sectionIdentifiers) - } - collectionViewDataSource.apply(snapshot, animatingDifferences: animated) { [weak self] in - self?.updateFirstResponder() - } - UIView.perform(withAnimation: animated, duration: Constants.animationDuration) { [self] in - view.backgroundColor = state.isCaptured ? style.background.success : style.background.regular - buttonsContainerView.configure(viewModel: state.actions, animated: animated) - collectionOverlayView.layoutIfNeeded() - } - } - - // MARK: - Current Responder Handling - - private func updateFirstResponder() { - if case .started(let startedState) = viewModel.state, !startedState.isEditingAllowed { - logger.debug("Editing is not allowed in current state, will resign first responder") - view.endEditing(true) - return - } - let isEditing = collectionView.indexPathsForVisibleItems.contains { indexPath in - let cell = collectionView.cellForItem(at: indexPath) as? NativeAlternativePaymentMethodCell - return cell?.inputResponder?.isFirstResponder == true - } - guard !isEditing, let indexPath = indexPathForFutureFirstResponderCell() else { - return - } - if collectionView.indexPathsForVisibleItems.contains(indexPath) { - let cell = collectionView.cellForItem(at: indexPath) as? NativeAlternativePaymentMethodCell - cell?.inputResponder?.becomeFirstResponder() - } - collectionView.scrollToItem(at: indexPath, at: .top, animated: true) - } - - private func indexPathForFutureFirstResponderCell() -> IndexPath? { - let snapshot = collectionViewDataSource.snapshot() - var inputsIndexPaths: [IndexPath] = [] - for (section, sectionId) in snapshot.sectionIdentifiers.enumerated() { - for (row, item) in snapshot.itemIdentifiers(inSection: sectionId).enumerated() { - let isInvalid: Bool - switch item { - case .input(let inputItem): - isInvalid = inputItem.value.isInvalid - case .codeInput(let inputItem): - isInvalid = inputItem.value.isInvalid - default: - continue - } - let indexPath = IndexPath(row: row, section: section) - if isInvalid { - return indexPath - } - inputsIndexPaths.append(indexPath) - } - } - return inputsIndexPaths.first - } - - // MARK: - - - private func configureCollectionView() { - _ = collectionViewDataSource - collectionView.registerSupplementaryView( - CollectionViewSectionHeaderView.self, kind: UICollectionView.elementKindSectionHeader - ) - collectionView.registerSupplementaryView( - CollectionViewSeparatorView.self, kind: CollectionViewCenterLayout.elementKindSeparator - ) - collectionView.registerCell(CollectionViewTitleCell.self) - collectionView.registerCell(NativeAlternativePaymentMethodLoaderCell.self) - collectionView.registerCell(NativeAlternativePaymentMethodInputCell.self) - collectionView.registerCell(NativeAlternativePaymentMethodCodeInputCell.self) - collectionView.registerCell(CollectionViewErrorCell.self) - collectionView.registerCell(NativeAlternativePaymentMethodSubmittedCell.self) - collectionView.registerCell(NativeAlternativePaymentMethodPickerCell.self) - collectionView.registerCell(NativeAlternativePaymentMethodRadioCell.self) - } - - private func cell(for item: ItemIdentifier, at indexPath: IndexPath) -> UICollectionViewCell? { - switch item { - case .loader: - let cell = collectionView.dequeueReusableCell(NativeAlternativePaymentMethodLoaderCell.self, for: indexPath) - cell.initialize(style: style.activityIndicator) - return cell - case .title(let item): - let cell = collectionView.dequeueReusableCell(CollectionViewTitleCell.self, for: indexPath) - cell.configure(viewModel: item, style: style.title) - return cell - case .input(let item): - let cell = collectionView.dequeueReusableCell(NativeAlternativePaymentMethodInputCell.self, for: indexPath) - cell.configure(item: item, style: style.input) - cell.delegate = self - return cell - case .codeInput(let item): - let cell = collectionView.dequeueReusableCell( - NativeAlternativePaymentMethodCodeInputCell.self, for: indexPath - ) - cell.configure(item: item, style: style.codeInput) - cell.delegate = self - return cell - case .error(let item): - let cell = collectionView.dequeueReusableCell(CollectionViewErrorCell.self, for: indexPath) - cell.configure(viewModel: item, style: style.errorDescription) - return cell - case .submitted(let item): - let cell = collectionView.dequeueReusableCell( - NativeAlternativePaymentMethodSubmittedCell.self, for: indexPath - ) - let style = NativeAlternativePaymentMethodSubmittedCellStyle( - title: style.title, message: style.message, successMessage: style.successMessage - ) - cell.configure(item: item, style: style) - return cell - case .picker(let item): - let cell = collectionView.dequeueReusableCell(NativeAlternativePaymentMethodPickerCell.self, for: indexPath) - cell.configure(item: item, style: style.input) - return cell - case .radio(let item): - let cell = collectionView.dequeueReusableCell(NativeAlternativePaymentMethodRadioCell.self, for: indexPath) - cell.configure(item: item, style: style.radioButton) - return cell - } - } - - private func supplementaryView(ofKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView? { - guard let sectionIdentifier = collectionViewDataSource.sectionIdentifier(for: indexPath.section) else { - return nil - } - switch kind { - case CollectionViewCenterLayout.elementKindSeparator: - let view = collectionView.dequeueReusableSupplementaryView( - CollectionViewSeparatorView.self, kind: kind, indexPath: indexPath - ) - view.configure(color: style.separatorColor) - return view - case UICollectionView.elementKindSectionHeader: - guard let sectionHeader = sectionIdentifier.header else { - return nil - } - let view = collectionView.dequeueReusableSupplementaryView( - CollectionViewSectionHeaderView.self, kind: kind, indexPath: indexPath - ) - view.configure(viewModel: sectionHeader, style: style.sectionTitle) - return view - default: - return nil - } - } - - /// Adjusts bottom inset based on current state actions and keyboard height. - private func configureCollectionViewBottomInset(state: ViewModel.State) { - // todo(andrii-vysotskyi): consider observing overlay content height instead for better flexibility in future - var bottomInset = Constants.contentInset.bottom + keyboardHeight - if case .started(let startedState) = state { - bottomInset += buttonsContainerView.contentHeight(viewModel: startedState.actions) - } - if bottomInset != collectionView.contentInset.bottom { - collectionView.contentInset.bottom = bottomInset - } - } -} - -private enum Constants { - static let animationDuration: TimeInterval = 0.25 - static let lineSpacing: CGFloat = 8 - static let tightLineSpacing: CGFloat = 4 - static let sectionInset = UIEdgeInsets(top: 8, left: 0, bottom: 32, right: 0) - static let contentInset = UIEdgeInsets(top: 24, left: 24, bottom: 24, right: 24) - static let inputHeight: CGFloat = 44 - static let loaderHeight: CGFloat = 256 -} - -// swiftlint:enable type_body_length file_length diff --git a/Sources/ProcessOut/Sources/UI/Modules/NativeAlternativePaymentMethod/ViewModel/DefaultNativeAlternativePaymentMethodViewModel.swift b/Sources/ProcessOut/Sources/UI/Modules/NativeAlternativePaymentMethod/ViewModel/DefaultNativeAlternativePaymentMethodViewModel.swift deleted file mode 100644 index cec82978b..000000000 --- a/Sources/ProcessOut/Sources/UI/Modules/NativeAlternativePaymentMethod/ViewModel/DefaultNativeAlternativePaymentMethodViewModel.swift +++ /dev/null @@ -1,412 +0,0 @@ -// -// DefaultNativeAlternativePaymentMethodViewModel.swift -// ProcessOut -// -// Created by Andrii Vysotskyi on 20.04.2023. -// - -import Foundation -import UIKit - -// swiftlint:disable type_body_length file_length - -@available(*, deprecated) -final class DefaultNativeAlternativePaymentMethodViewModel: - BaseViewModel, NativeAlternativePaymentMethodViewModel { - - init( - interactor: any PONativeAlternativePaymentMethodInteractor, - configuration: PONativeAlternativePaymentMethodConfiguration, - completion: ((Result) -> Void)? - ) { - self.interactor = interactor - self.configuration = configuration - self.completion = completion - inputValuesObservations = [] - inputValuesCache = [:] - isPaymentCancelDisabled = false - isCaptureCancelDisabled = false - cancelActionTimers = [:] - super.init(state: .idle) - observeInteractorStateChanges() - } - - override func start() { - interactor.start() - } - - func submit() { - interactor.submit() - } - - // MARK: - Private Nested Types - - private typealias InteractorState = PONativeAlternativePaymentMethodInteractorState - - private enum Constants { - static let captureSuccessCompletionDelay: TimeInterval = 3 - static let maximumCodeLength = 6 - } - - // MARK: - NativeAlternativePaymentMethodInteractor - - private let interactor: any PONativeAlternativePaymentMethodInteractor - private let configuration: PONativeAlternativePaymentMethodConfiguration - private let completion: ((Result) -> Void)? - - private lazy var priceFormatter: NumberFormatter = { - let formatter = NumberFormatter() - formatter.numberStyle = .currency - formatter.minimumFractionDigits = 0 - formatter.maximumFractionDigits = 2 - return formatter - }() - - private var inputValuesCache: [String: State.InputValue] - private var inputValuesObservations: [AnyObject] - private var cancelActionTimers: [AnyHashable: Timer] - private var isPaymentCancelDisabled: Bool - private var isCaptureCancelDisabled: Bool - - // MARK: - Private Methods - - private func observeInteractorStateChanges() { - interactor.didChange = { [weak self] in self?.configureWithInteractorState() } - } - - private func configureWithInteractorState() { - switch interactor.state { - case .idle: - state = .idle - case .starting: - configureWithStartingState() - case .started(let startedState): - scheduleCancelActionEnabling( - configuration: configuration.secondaryAction, isDisabled: \.isPaymentCancelDisabled - ) - state = convertToState(startedState: startedState, isSubmitting: false) - case .failure(let failure): - completion?(.failure(failure)) - case .submitting(let startedState): - state = convertToState(startedState: startedState, isSubmitting: true) - case .submitted: - completion?(.success(())) - case .awaitingCapture(let awaitingCaptureState): - scheduleCancelActionEnabling( - configuration: configuration.paymentConfirmationSecondaryAction, isDisabled: \.isCaptureCancelDisabled - ) - state = convertToState(awaitingCaptureState: awaitingCaptureState) - case .captured(let capturedState): - configure(with: capturedState) - } - invalidateCancelActionTimersIfNeeded(state: interactor.state) - } - - private func configureWithStartingState() { - let sections = [ - State.Section(id: .init(id: nil, header: nil, isTight: false), items: [.loader]) - ] - let startedState = State.Started( - sections: sections, - actions: .init(primary: nil, secondary: nil), - isEditingAllowed: false, - isCaptured: false - ) - state = .started(startedState) - } - - // swiftlint:disable:next function_body_length - private func convertToState(startedState: InteractorState.Started, isSubmitting: Bool) -> State { - let titleItem = State.TitleItem( - // swiftlint:disable:next line_length - text: configuration.title ?? String(resource: .NativeAlternativePayment.title, replacements: startedState.gateway.displayName) - ) - var sections = [ - State.Section(id: .init(id: nil, header: nil, isTight: false), items: [.title(titleItem)]) - ] - let shouldCenterCodeInput = startedState.parameters.count == 1 - for (offset, parameter) in startedState.parameters.enumerated() { - let value = startedState.values[parameter.key] ?? .init(value: nil, recentErrorMessage: nil) - var items = createItems( - parameter: parameter, - value: value, - isEditingAllowed: !isSubmitting, - isLast: offset == startedState.parameters.indices.last, - shouldCenterCodeInput: shouldCenterCodeInput - ) - var isCentered = false - if case .codeInput = items.first, shouldCenterCodeInput { - isCentered = true - } - if let message = value.recentErrorMessage { - items.append(.error(State.ErrorItem(description: message, isCentered: isCentered))) - } - let isTight = items.contains { item in - if case .radio = item { - return true - } - return false - } - let section = State.Section( - id: .init( - id: parameter.key, - header: .init(title: parameter.displayName, isCentered: isCentered), - isTight: isTight - ), - items: items - ) - sections.append(section) - } - let startedState = State.Started( - sections: sections, - actions: .init( - primary: submitAction(startedState: startedState, isSubmitting: isSubmitting), - secondary: cancelAction( - configuration: configuration.secondaryAction, - isEnabled: !isSubmitting && !isPaymentCancelDisabled - ) - ), - isEditingAllowed: !isSubmitting, - isCaptured: false - ) - return .started(startedState) - } - - private func convertToState(awaitingCaptureState: InteractorState.AwaitingCapture) -> State { - let item: State.Item - if let expectedActionMessage = awaitingCaptureState.actionMessage { - let submittedItem = State.SubmittedItem( - title: awaitingCaptureState.logoImage == nil ? awaitingCaptureState.paymentProviderName : nil, - logoImage: awaitingCaptureState.logoImage, - message: expectedActionMessage, - image: awaitingCaptureState.actionImage, - isCaptured: false - ) - item = .submitted(submittedItem) - } else { - item = .loader - } - let secondaryAction = cancelAction( - configuration: configuration.paymentConfirmationSecondaryAction, - isEnabled: !isCaptureCancelDisabled - ) - let startedState = State.Started( - sections: [ - .init(id: .init(id: nil, header: nil, isTight: false), items: [item]) - ], - actions: .init(primary: nil, secondary: secondaryAction), - isEditingAllowed: false, - isCaptured: false - ) - return .started(startedState) - } - - private func configure(with capturedState: InteractorState.Captured) { - if configuration.skipSuccessScreen { - completion?(.success(())) - } else { - Timer.scheduledTimer( - withTimeInterval: Constants.captureSuccessCompletionDelay, - repeats: false, - block: { [weak self] _ in - self?.completion?(.success(())) - } - ) - let submittedItem = State.SubmittedItem( - title: capturedState.logoImage == nil ? capturedState.paymentProviderName : nil, - logoImage: capturedState.logoImage, - message: String(resource: .NativeAlternativePayment.Success.message), - image: UIImage(poResource: .success), - isCaptured: true - ) - let startedState = State.Started( - sections: [ - .init(id: .init(id: nil, header: nil, isTight: false), items: [.submitted(submittedItem)]) - ], - actions: .init(primary: nil, secondary: nil), - isEditingAllowed: false, - isCaptured: true - ) - state = .started(startedState) - } - } - - // MARK: - Actions - - private func submitAction(startedState: InteractorState.Started, isSubmitting: Bool) -> State.Action { - let title: String - if let customTitle = configuration.primaryActionTitle { - title = customTitle - } else { - priceFormatter.currencyCode = startedState.currencyCode - // swiftlint:disable:next legacy_objc_type - if let formattedAmount = priceFormatter.string(from: startedState.amount as NSDecimalNumber) { - title = String(resource: .NativeAlternativePayment.Button.submitAmount, replacements: formattedAmount) - } else { - title = String(resource: .NativeAlternativePayment.Button.submit) - } - } - let action = State.Action( - title: title, - isEnabled: startedState.isSubmitAllowed, - isExecuting: isSubmitting, - accessibilityIdentifier: "native-alternative-payment.primary-button", - handler: { [weak self] in - self?.interactor.submit() - } - ) - return action - } - - private func cancelAction( - configuration: PONativeAlternativePaymentMethodConfiguration.SecondaryAction?, isEnabled: Bool - ) -> State.Action? { - guard case let .cancel(title, _) = configuration else { - return nil - } - let action = State.Action( - title: title ?? String(resource: .NativeAlternativePayment.Button.cancel), - isEnabled: isEnabled, - isExecuting: false, - accessibilityIdentifier: "native-alternative-payment.secondary-button", - handler: { [weak self] in - self?.interactor.cancel() - } - ) - return action - } - - // MARK: - Input Items - - private func createItems( - parameter: PONativeAlternativePaymentMethodParameter, - value parameterValue: InteractorState.ParameterValue, - isEditingAllowed: Bool, - isLast: Bool, - shouldCenterCodeInput: Bool - ) -> [State.Item] { - let inputValue: State.InputValue - if let value = inputValuesCache[parameter.key] { - inputValue = value - inputValue.text = parameterValue.value ?? "" - inputValue.isInvalid = parameterValue.recentErrorMessage != nil - inputValue.isEditingAllowed = isEditingAllowed - } else { - inputValue = State.InputValue( - text: .init(value: parameterValue.value ?? ""), - isInvalid: .init(value: parameterValue.recentErrorMessage != nil), - isEditingAllowed: .init(value: isEditingAllowed) - ) - inputValuesCache[parameter.key] = inputValue - let observer = inputValue.$text.addObserver { [weak self] updatedValue in - self?.interactor.updateValue(updatedValue, for: parameter.key) - } - inputValuesObservations.append(observer) - } - switch parameter.type { - case .numeric where (parameter.length ?? .max) <= Constants.maximumCodeLength: - let inputItem = State.CodeInputItem( - // swiftlint:disable:next force_unwrapping - length: parameter.length!, value: inputValue, isCentered: shouldCenterCodeInput - ) - return [.codeInput(inputItem)] - case .singleSelect: - let optionsCount = parameter.availableValues?.count ?? 0 - if optionsCount <= configuration.inlineSingleSelectValuesLimit { - return createRadioButtonItems(parameter: parameter, value: inputValue) - } - return [createPickerItem(parameter: parameter, value: inputValue)] - default: - let inputItem = State.InputItem( - type: parameter.type, - placeholder: placeholder(for: parameter), - value: inputValue, - isLast: isLast, - formatter: interactor.formatter(type: parameter.type) - ) - return [.input(inputItem)] - } - } - - private func createRadioButtonItems( - parameter: PONativeAlternativePaymentMethodParameter, value: State.InputValue - ) -> [State.Item] { - assert(parameter.type == .singleSelect) - let items = parameter.availableValues?.map { option in - let radioItem = State.RadioButtonItem( - value: option.displayName, - isSelected: option.value == value.text, - isInvalid: value.isInvalid, - select: { [weak self] in - self?.interactor.updateValue(option.value, for: parameter.key) - } - ) - return State.Item.radio(radioItem) - } - return items ?? [] - } - - private func createPickerItem( - parameter: PONativeAlternativePaymentMethodParameter, value: State.InputValue - ) -> State.Item { - assert(parameter.type == .singleSelect) - let options = parameter.availableValues?.map { option in - State.PickerOption(name: option.displayName, isSelected: option.value == value.text) { [weak self] in - self?.interactor.updateValue(option.value, for: parameter.key) - } - } - let item = State.PickerItem( - // Value of single select parameter is not user friendly instead display name should be used. - value: parameter.availableValues?.first { $0.value == value.text }?.displayName ?? "", - isInvalid: value.isInvalid, - options: options ?? [] - ) - return .picker(item) - } - - private func placeholder(for parameter: PONativeAlternativePaymentMethodParameter) -> String? { - switch parameter.type { - case .numeric, .text, .singleSelect: - return nil - case .email: - return String(resource: .NativeAlternativePayment.Placeholder.email) - case .phone: - return String(resource: .NativeAlternativePayment.Placeholder.phone) - } - } - - // MARK: - Cancel Actions Enabling - - private func scheduleCancelActionEnabling( - configuration: PONativeAlternativePaymentMethodConfiguration.SecondaryAction?, - isDisabled: ReferenceWritableKeyPath - ) { - let timerKey = AnyHashable(isDisabled) - guard !cancelActionTimers.keys.contains(timerKey), - case .cancel(_, let interval) = configuration, - interval > 0 else { - return - } - self[keyPath: isDisabled] = true - let timer = Timer.scheduledTimer(withTimeInterval: interval, repeats: false) { [weak self] _ in - self?[keyPath: isDisabled] = false - self?.configureWithInteractorState() - } - cancelActionTimers[timerKey] = timer - } - - private func invalidateCancelActionTimersIfNeeded(state interactorState: InteractorState) { - // If interactor is in a sink state timers should be invalidated to ensure that completion - // won't be called multiple times. - switch interactorState { - case .failure, .captured, .submitted: - break - default: - return - } - cancelActionTimers.values.forEach { $0.invalidate() } - cancelActionTimers = [:] - } -} - -// swiftlint:enable type_body_length file_length diff --git a/Sources/ProcessOut/Sources/UI/Modules/NativeAlternativePaymentMethod/ViewModel/NativeAlternativePaymentMethodViewModel.swift b/Sources/ProcessOut/Sources/UI/Modules/NativeAlternativePaymentMethod/ViewModel/NativeAlternativePaymentMethodViewModel.swift deleted file mode 100644 index 9491c39e4..000000000 --- a/Sources/ProcessOut/Sources/UI/Modules/NativeAlternativePaymentMethod/ViewModel/NativeAlternativePaymentMethodViewModel.swift +++ /dev/null @@ -1,13 +0,0 @@ -// -// NativeAlternativePaymentMethodViewModel.swift -// ProcessOut -// -// Created by Andrii Vysotskyi on 20.04.2023. -// - -@available(*, deprecated) -protocol NativeAlternativePaymentMethodViewModel: ViewModel { - - /// Submits parameter values. - func submit() -} diff --git a/Sources/ProcessOut/Sources/UI/Modules/NativeAlternativePaymentMethod/ViewModel/NativeAlternativePaymentMethodViewModelState.swift b/Sources/ProcessOut/Sources/UI/Modules/NativeAlternativePaymentMethod/ViewModel/NativeAlternativePaymentMethodViewModelState.swift deleted file mode 100644 index 947e90251..000000000 --- a/Sources/ProcessOut/Sources/UI/Modules/NativeAlternativePaymentMethod/ViewModel/NativeAlternativePaymentMethodViewModelState.swift +++ /dev/null @@ -1,176 +0,0 @@ -// -// NativeAlternativePaymentMethodViewModelState.swift -// ProcessOut -// -// Created by Andrii Vysotskyi on 27.04.2023. -// - -import UIKit - -@available(*, deprecated) -enum NativeAlternativePaymentMethodViewModelState { - - typealias TitleItem = CollectionViewTitleViewModel - - struct RadioButtonItem: Hashable { - - /// Current value. - let value: String - - /// Indicates whether radio button is selected. - let isSelected: Bool - - /// Boolean value indicating whether value is valid. - let isInvalid: Bool - - /// Closure to invoke when radio button is selected. - @ImmutableNullHashable - var select: () -> Void - } - - struct PickerOption: Hashable { - - /// Option name. - let name: String - - /// Indicates whether option is currently selected. - let isSelected: Bool - - /// Closure to invoke when option is selected. - @ImmutableNullHashable - var select: () -> Void - } - - struct PickerItem: Hashable { - - /// Current value. - let value: String - - /// Boolean value indicating whether value is valid. - let isInvalid: Bool - - /// Available options. - let options: [PickerOption] - } - - struct InputValue: Hashable { - - /// Current parameter's value text. - @ReferenceWrapper - var text: String - - /// Boolean value indicating whether value is valid. - @ReferenceWrapper - var isInvalid: Bool - - /// Boolean value indicating whether editing is allowed. - @ReferenceWrapper - var isEditingAllowed: Bool - } - - struct CodeInputItem: Hashable { - - /// Code input length. - let length: Int - - /// Value details. - let value: InputValue - - /// Indicates whether input should be centered. - let isCentered: Bool - } - - typealias ParameterType = PONativeAlternativePaymentMethodParameter.ParameterType - - struct InputItem: Hashable { - - /// Parameter type. - let type: ParameterType - - /// Parameter's placeholder. - let placeholder: String? - - /// Value details. - let value: InputValue - - /// Boolean value indicating whether parameter is last in a chain. - let isLast: Bool - - /// Formatter to use to format value if any. - let formatter: Formatter? - } - - typealias ErrorItem = CollectionViewErrorViewModel - - struct SubmittedItem: Hashable { - - /// Title text. - let title: String? - - /// Payment provider logo. - let logoImage: UIImage? - - /// Message markdown. - let message: String - - /// Image illustrating action. - let image: UIImage? - - /// Boolean value that indicates whether payment is already captured. - let isCaptured: Bool - } - - enum Item: Hashable { - case loader - case title(TitleItem) - case input(InputItem) - case codeInput(CodeInputItem) - case radio(RadioButtonItem) - case picker(PickerItem) - case error(ErrorItem) - case submitted(SubmittedItem) - } - - typealias SectionHeader = CollectionViewSectionHeaderViewModel - - struct SectionIdentifier: Hashable { - - /// Section id. - let id: String? - - /// Section header if any. - let header: SectionHeader? - - /// Boolean value indicating whether section items should be laid out tightly. - let isTight: Bool - } - - struct Section { - - /// Identifier. - let id: SectionIdentifier - - /// Section items. - let items: [Item] - } - - typealias Action = ActionsContainerActionViewModel - typealias Actions = ActionsContainerViewModel - - struct Started { - - /// Available items. - let sections: [Section] - - /// Available actions. - let actions: Actions - - /// Boolean value indicating whether editing is allowed. - let isEditingAllowed: Bool - - /// Boolean value that indicates whether payment is already captured. - let isCaptured: Bool - } - - case idle, started(Started) -} diff --git a/Sources/ProcessOut/Sources/UI/Modules/Safari/DefaultSafariViewModel.swift b/Sources/ProcessOut/Sources/UI/Modules/Safari/DefaultSafariViewModel.swift deleted file mode 100644 index 52071eb4a..000000000 --- a/Sources/ProcessOut/Sources/UI/Modules/Safari/DefaultSafariViewModel.swift +++ /dev/null @@ -1,135 +0,0 @@ -// -// DefaultSafariViewModel.swift -// ProcessOut -// -// Created by Andrii Vysotskyi on 10.05.2023. -// - -import Foundation -import SafariServices - -@available(*, deprecated) -final class DefaultSafariViewModel: NSObject, SFSafariViewControllerDelegate { - - init( - configuration: DefaultSafariViewModelConfiguration, - eventEmitter: POEventEmitter, - logger: POLogger, - delegate: DefaultSafariViewModelDelegate - ) { - self.configuration = configuration - self.eventEmitter = eventEmitter - self.logger = logger - self.delegate = delegate - state = .idle - } - - func start() { - guard case .idle = state else { - return - } - if let timeout = configuration.timeout { - timeoutTimer = Timer.scheduledTimer(withTimeInterval: timeout, repeats: false) { [weak self] _ in - 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) - } - } - - 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 configuration: DefaultSafariViewModelConfiguration - private let eventEmitter: POEventEmitter - private let logger: POLogger - private let delegate: DefaultSafariViewModelDelegate - - 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 url.scheme == configuration.returnUrl.scheme, - url.host == configuration.returnUrl.host, - url.path == configuration.returnUrl.path else { - logger.debug("Ignoring unrelated url: \(url)") - return false - } - do { - try delegate.complete(with: url) - invalidateObservers() - state = .completed - logger.info("Did complete with url: \(url)") - } catch { - setCompletedState(with: error) - } - return true - } - - private func setCompletedState(with error: Error) { - if case .completed = state { - logger.info("Can't change state to completed because already in a sink state.") - return - } - let failure: POFailure - if let error = error as? POFailure { - failure = error - } else { - failure = POFailure(message: nil, code: .generic(.mobile), underlyingError: error) - } - invalidateObservers() - state = .completed - logger.debug("Did complete with error: \(failure)") - delegate.complete(with: failure) - } - - private func invalidateObservers() { - timeoutTimer?.invalidate() - deepLinkObserver = nil - } -} diff --git a/Sources/ProcessOut/Sources/UI/Modules/Safari/DefaultSafariViewModelConfiguration.swift b/Sources/ProcessOut/Sources/UI/Modules/Safari/DefaultSafariViewModelConfiguration.swift deleted file mode 100644 index 54d1dfe98..000000000 --- a/Sources/ProcessOut/Sources/UI/Modules/Safari/DefaultSafariViewModelConfiguration.swift +++ /dev/null @@ -1,18 +0,0 @@ -// -// DefaultSafariViewModelConfiguration.swift -// ProcessOut -// -// Created by Andrii Vysotskyi on 10.05.2023. -// - -import Foundation - -@available(*, deprecated) -struct DefaultSafariViewModelConfiguration { - - /// Return url specified when creating invoice. - let returnUrl: URL - - /// Optional timeout. - let timeout: TimeInterval? -} diff --git a/Sources/ProcessOut/Sources/UI/Modules/Safari/DefaultSafariViewModelDelegate.swift b/Sources/ProcessOut/Sources/UI/Modules/Safari/DefaultSafariViewModelDelegate.swift deleted file mode 100644 index d63efd039..000000000 --- a/Sources/ProcessOut/Sources/UI/Modules/Safari/DefaultSafariViewModelDelegate.swift +++ /dev/null @@ -1,19 +0,0 @@ -// -// DefaultSafariViewModelDelegate.swift -// ProcessOut -// -// Created by Andrii Vysotskyi on 11.11.2022. -// - -import Foundation - -@available(*, deprecated) -protocol DefaultSafariViewModelDelegate: AnyObject { - - /// Asks delegate to complete with given url. - /// - Throws: error if transform is not possible for some reason. - func complete(with url: URL) throws - - /// Completes with failure. - func complete(with failure: POFailure) -} diff --git a/Sources/ProcessOut/Sources/UI/Modules/Safari/SafariViewController+Extensions.swift b/Sources/ProcessOut/Sources/UI/Modules/Safari/SafariViewController+Extensions.swift deleted file mode 100644 index d31338c31..000000000 --- a/Sources/ProcessOut/Sources/UI/Modules/Safari/SafariViewController+Extensions.swift +++ /dev/null @@ -1,22 +0,0 @@ -// -// SafariViewController+Extensions.swift -// ProcessOut -// -// Created by Andrii Vysotskyi on 10.05.2023. -// - -import SafariServices - -@available(*, deprecated) -extension SFSafariViewController { - - func setViewModel(_ viewModel: DefaultSafariViewModel) { - objc_setAssociatedObject(self, &Keys.viewModel, viewModel, .OBJC_ASSOCIATION_RETAIN) - } - - // MARK: - Private Nested Types - - private enum Keys { - static var viewModel: UInt8 = 0 - } -} diff --git a/Sources/ProcessOut/Sources/UI/ResourceSymbols/ColorResource.swift b/Sources/ProcessOut/Sources/UI/ResourceSymbols/ColorResource.swift deleted file mode 100644 index 1520b0647..000000000 --- a/Sources/ProcessOut/Sources/UI/ResourceSymbols/ColorResource.swift +++ /dev/null @@ -1,143 +0,0 @@ -// -// ColorResource.swift -// ProcessOut -// -// Created by Andrii Vysotskyi on 11.10.2023. -// - -import SwiftUI - -// swiftlint:disable strict_fileprivate nesting - -/// A color resource. -/// - NOTE: Type is prefixed with PO but not public to disambiguate from native `ColorResource`. -struct POColorResource: Hashable, Sendable { - - init(name: String) { - self.name = name - self.bundle = BundleLocator.bundle - } - - /// Resource name. - fileprivate let name: String - - /// Resource bundle. - fileprivate let bundle: Bundle -} - -extension POColorResource { - - /// The "Surface" asset catalog resource namespace. - enum Surface { - - /// The "Surface/Background" asset catalog color resource. - static let background = POColorResource(name: "Surface/Background") - - /// The "Surface/Error" asset catalog color resource. - static let error = POColorResource(name: "Surface/Error") - - /// The "Surface/Level1" asset catalog color resource. - static let level1 = POColorResource(name: "Surface/Level1") - - /// The "Surface/Neutral" asset catalog color resource. - static let neutral = POColorResource(name: "Surface/Neutral") - - /// The "Surface/Success" asset catalog color resource. - static let success = POColorResource(name: "Surface/Success") - - /// The "Surface/Warning" asset catalog color resource. - static let warning = POColorResource(name: "Surface/Warning") - } - - /// The "Action" asset catalog resource namespace. - enum Action { - - /// The "Action/Primary" asset catalog resource namespace. - enum Primary { - - /// The "Action/Primary/Default" asset catalog color resource. - static let `default` = POColorResource(name: "Action/Primary/Default") - - /// The "Action/Primary/Disabled" asset catalog color resource. - static let disabled = POColorResource(name: "Action/Primary/Disabled") - - /// The "Action/Primary/Pressed" asset catalog color resource. - static let pressed = POColorResource(name: "Action/Primary/Pressed") - } - - /// The "Action/Secondary" asset catalog resource namespace. - enum Secondary { - - /// The "Action/Secondary/Default" asset catalog color resource. - static let `default` = POColorResource(name: "Action/Secondary/Default") - - /// The "Action/Secondary/Pressed" asset catalog color resource. - static let pressed = POColorResource(name: "Action/Secondary/Pressed") - } - - /// The "Action/Border" asset catalog resource namespace. - enum Border { - - /// The "Action/Border/Disabled" asset catalog color resource. - static let disabled = POColorResource(name: "Action/Border/Disabled") - - /// The "Action/Border/Selected" asset catalog color resource. - static let selected = POColorResource(name: "Action/Border/Selected") - } - } - - /// The "Border" asset catalog resource namespace. - enum Border { - - /// The "Border/Default" asset catalog color resource. - static let `default` = POColorResource(name: "Border/Default") - - /// The "Border/Divider" asset catalog color resource. - static let divider = POColorResource(name: "Border/Divider") - - /// The "Border/Subtle" asset catalog color resource. - static let subtle = POColorResource(name: "Border/Subtle") - } - - /// The "Text" asset catalog resource namespace. - enum Text { - - /// The "Text/Disabled" asset catalog color resource. - static let disabled = POColorResource(name: "Text/Disabled") - - /// The "Text/Error" asset catalog color resource. - static let error = POColorResource(name: "Text/Error") - - /// The "Text/Muted" asset catalog color resource. - static let muted = POColorResource(name: "Text/Muted") - - /// The "Text/OnColor" asset catalog color resource. - static let on = POColorResource(name: "Text/OnColor") // swiftlint:disable:this identifier_name - - /// The "Text/Primary" asset catalog color resource. - static let primary = POColorResource(name: "Text/Primary") - - /// The "Text/Secondary" asset catalog color resource. - static let secondary = POColorResource(name: "Text/Secondary") - - /// The "Text/Success" asset catalog color resource. - static let success = POColorResource(name: "Text/Success") - - /// The "Text/Tertiary" asset catalog color resource. - static let tertiary = POColorResource(name: "Text/Tertiary") - - /// The "Text/Warning" asset catalog color resource. - static let warning = POColorResource(name: "Text/Warning") - } -} - -extension UIColor { - - /// Initialize an `Image` with an image resource. - convenience init(poResource resource: POColorResource) { - // swiftlint:disable:next force_unwrapping - self.init(named: resource.name, in: resource.bundle, compatibleWith: nil)! - } -} - -// swiftlint:enable strict_fileprivate nesting diff --git a/Sources/ProcessOut/Sources/UI/ResourceSymbols/ImageResource.swift b/Sources/ProcessOut/Sources/UI/ResourceSymbols/ImageResource.swift deleted file mode 100644 index ea35197f0..000000000 --- a/Sources/ProcessOut/Sources/UI/ResourceSymbols/ImageResource.swift +++ /dev/null @@ -1,46 +0,0 @@ -// -// ImageResource.swift -// ProcessOut -// -// Created by Andrii Vysotskyi on 31.07.2024. -// - -import SwiftUI - -// swiftlint:disable strict_fileprivate - -/// An image resource. -/// - NOTE: Type is prefixed with PO but not public to disambiguate from native `POImageResource`. -struct POImageResource: Hashable, Sendable { - - init(name: String) { - self.name = name - self.bundle = BundleLocator.bundle - } - - /// An asset catalog image resource name. - fileprivate let name: String - - /// An asset catalog image resource bundle. - fileprivate let bundle: Bundle -} - -extension POImageResource { - - /// The "ChevronDown" asset catalog image resource. - static let chevronDown = POImageResource(name: "ChevronDown") - - /// The "Success" asset catalog image resource. - static let success = POImageResource(name: "Success") -} - -extension UIImage { - - /// Initialize an `Image` with an image resource. - convenience init(poResource resource: POImageResource) { - // swiftlint:disable:next force_unwrapping - self.init(named: resource.name, in: resource.bundle, compatibleWith: nil)! - } -} - -// swiftlint:enable strict_fileprivate diff --git a/Sources/ProcessOut/Sources/UI/Shared/Architecture/View/BaseViewController.swift b/Sources/ProcessOut/Sources/UI/Shared/Architecture/View/BaseViewController.swift deleted file mode 100644 index cade1a550..000000000 --- a/Sources/ProcessOut/Sources/UI/Shared/Architecture/View/BaseViewController.swift +++ /dev/null @@ -1,122 +0,0 @@ -// -// BaseViewController.swift -// ProcessOut -// -// Created by Andrii Vysotskyi on 25.04.2023. -// - -import UIKit - -@available(*, deprecated) -class BaseViewController: UIViewController where Model: ViewModel { - - init(viewModel: Model, logger: POLogger) { - self.viewModel = viewModel - self.logger = logger - keyboardHeight = 0 - super.init(nibName: nil, bundle: nil) - } - - deinit { - NotificationCenter.default.removeObserver(self) - } - - @available(*, unavailable) - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override func viewDidLoad() { - super.viewDidLoad() - viewModel.start() - } - - override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - observeKeyboardChanges() - viewModel.didChange = { [weak self] in self?.viewModelDidChange() } - } - - override func viewWillDisappear(_ animated: Bool) { - super.viewWillDisappear(animated) - viewModel.didChange = nil - removeKeyboardChangesObserver() - } - - // MARK: - - - func configure(with state: Model.State, animated: Bool) { - // Ignored - } - - func keyboardWillChange(newHeight: CGFloat) { - logger.debug("Keyboard height will change to \(newHeight)") - } - - let viewModel: Model - - // MARK: - Private Properties - - private let logger: POLogger - private var keyboardHeight: CGFloat - - // MARK: - Private Methods - - private func viewModelDidChange() { - // There may be UI glitches if view is updated when being tracked by user. So - // as a workaround, configuration is postponed to a point when tracking ends. - guard RunLoop.current.currentMode != .tracking else { - RunLoop.current.perform(viewModelDidChange) - return - } - // View is configured without animation if it is not yet part of the hierarchy to avoid visual issues. - configure(with: viewModel.state, animated: viewIfLoaded?.window != nil) - } - - // MARK: - Keyboard Handling - - private func observeKeyboardChanges() { - let notificationName = UIResponder.keyboardWillChangeFrameNotification - let selector = #selector(keyboardWillChangeFrame(notification:)) - NotificationCenter.default.addObserver(self, selector: selector, name: notificationName, object: nil) - } - - private func removeKeyboardChangesObserver() { - let notificationName = UIResponder.keyboardWillChangeFrameNotification - NotificationCenter.default.removeObserver(self, name: notificationName, object: nil) - } - - @objc private func keyboardWillChangeFrame(notification: Notification) { - guard let notification = KeyboardNotification(notification: notification) else { - return - } - // Keyboard updates are not always animated so defaults are provided for smoother UI. - let animator = UIViewPropertyAnimator( - duration: notification.animationDuration ?? Constants.keyboardAnimationDuration, - curve: notification.animationCurve ?? .easeInOut, - animations: { [self] in - let coveredSafeAreaHeight = view.bounds.height - - view.convert(notification.frameEnd, from: nil).minY - - view.safeAreaInsets.bottom - let keyboardHeight = max(coveredSafeAreaHeight, 0) - guard self.keyboardHeight != keyboardHeight else { - return - } - keyboardWillChange(newHeight: keyboardHeight) - self.keyboardHeight = keyboardHeight - } - ) - // An implementation of `UICollectionView.performBatchUpdates` resigns first responder if item associated - // with a cell containing it is invalidated, for example moved, deleted or reloaded. And since keyboard - // notification is sent as part of resign operation, we shouldn't call `performBatchUpdates` directly here - // to avoid recursion which causes weird artifacts and inconsistency. To break it, keyboard animation info - // is extracted from notification and update is scheduled for next run loop iteration. Collection layout - // update is needed here in a first place because layout depends on inset, which transitively depends on - // keyboard visibility. - RunLoop.current.perform(animator.startAnimation) - } -} - -private enum Constants { - static let keyboardAnimationDuration: TimeInterval = 0.25 -} diff --git a/Sources/ProcessOut/Sources/UI/Shared/Architecture/ViewModel/BaseViewModel.swift b/Sources/ProcessOut/Sources/UI/Shared/Architecture/ViewModel/BaseViewModel.swift deleted file mode 100644 index 86d8de036..000000000 --- a/Sources/ProcessOut/Sources/UI/Shared/Architecture/ViewModel/BaseViewModel.swift +++ /dev/null @@ -1,28 +0,0 @@ -// -// BaseViewModel.swift -// ProcessOut -// -// Created by Andrii Vysotskyi on 19.10.2022. -// - -@available(*, deprecated) -class BaseViewModel: ViewModel { - - init(state: State) { - self.state = state - } - - /// View model's state. - var state: State { - didSet { didChange?() } - } - - /// A closure that is invoked after the object has changed. - var didChange: (() -> Void)? { - didSet { didChange?() } - } - - func start() { - // Does nothing - } -} diff --git a/Sources/ProcessOut/Sources/UI/Shared/Architecture/ViewModel/ViewModel.swift b/Sources/ProcessOut/Sources/UI/Shared/Architecture/ViewModel/ViewModel.swift deleted file mode 100644 index 629c4e586..000000000 --- a/Sources/ProcessOut/Sources/UI/Shared/Architecture/ViewModel/ViewModel.swift +++ /dev/null @@ -1,23 +0,0 @@ -// -// ViewModel.swift -// ProcessOut -// -// Created by Andrii Vysotskyi on 19.10.2022. -// - -@available(*, deprecated) -protocol ViewModel: AnyObject { - - associatedtype State - - /// View model's state. - var state: State { get } - - /// A closure that is invoked after the object has changed. - var didChange: (() -> Void)? { get set } - - /// Starts view model. - /// It's expected that implementation of this method should have logic responsible for - /// view model starting process, e.g. loading initial content. - func start() -} diff --git a/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/CollectionReusableViews/Error/CollectionViewErrorCell.swift b/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/CollectionReusableViews/Error/CollectionViewErrorCell.swift deleted file mode 100644 index 5b27843c8..000000000 --- a/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/CollectionReusableViews/Error/CollectionViewErrorCell.swift +++ /dev/null @@ -1,58 +0,0 @@ -// -// CollectionViewErrorCell.swift -// ProcessOut -// -// Created by Andrii Vysotskyi on 24.07.2023. -// - -import UIKit - -final class CollectionViewErrorCell: UICollectionViewCell { - - override init(frame: CGRect) { - super.init(frame: frame) - commonInit() - } - - @available(*, unavailable) - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - func configure(viewModel: CollectionViewErrorViewModel, style: POTextStyle) { - descriptionLabel.attributedText = AttributedStringBuilder() - .with { builder in - builder.typography = style.typography - builder.textStyle = .body - builder.color = style.color - builder.alignment = viewModel.isCentered ? .center : .natural - builder.text = .plain(viewModel.description) - } - .build() - } - - // MARK: - Private Properties - - private lazy var descriptionLabel: UILabel = { - let label = UILabel() - label.translatesAutoresizingMaskIntoConstraints = false - label.adjustsFontForContentSizeCategory = false - label.numberOfLines = 0 - label.setContentHuggingPriority(.required, for: .vertical) - label.setContentCompressionResistancePriority(.required, for: .vertical) - return label - }() - - // MARK: - Private Methods - - private func commonInit() { - contentView.addSubview(descriptionLabel) - let constraints = [ - descriptionLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), - descriptionLabel.centerXAnchor.constraint(equalTo: contentView.centerXAnchor), - descriptionLabel.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), - descriptionLabel.topAnchor.constraint(equalTo: contentView.topAnchor) - ] - NSLayoutConstraint.activate(constraints) - } -} diff --git a/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/CollectionReusableViews/Error/CollectionViewErrorViewModel.swift b/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/CollectionReusableViews/Error/CollectionViewErrorViewModel.swift deleted file mode 100644 index e3d5d5e22..000000000 --- a/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/CollectionReusableViews/Error/CollectionViewErrorViewModel.swift +++ /dev/null @@ -1,15 +0,0 @@ -// -// CollectionViewErrorViewModel.swift -// ProcessOut -// -// Created by Andrii Vysotskyi on 08.08.2023. -// - -struct CollectionViewErrorViewModel: Hashable { - - /// Error description. - let description: String - - /// Indicates whether error should be centered. - let isCentered: Bool -} diff --git a/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/CollectionReusableViews/Radio/CollectionViewRadioCell.swift b/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/CollectionReusableViews/Radio/CollectionViewRadioCell.swift deleted file mode 100644 index f057c0516..000000000 --- a/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/CollectionReusableViews/Radio/CollectionViewRadioCell.swift +++ /dev/null @@ -1,57 +0,0 @@ -// -// CollectionViewRadioCell.swift -// ProcessOut -// -// Created by Andrii Vysotskyi on 16.08.2023. -// - -import UIKit - -final class CollectionViewRadioCell: UICollectionViewCell { - - override init(frame: CGRect) { - super.init(frame: frame) - commonInit() - } - - @available(*, unavailable) - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - func configure(viewModel: CollectionViewRadioViewModel, style: PORadioButtonStyle) { - let buttonViewModel = RadioButtonViewModel( - isSelected: viewModel.isSelected, isInError: viewModel.isInvalid, value: viewModel.value - ) - radioButton.configure(viewModel: buttonViewModel, style: style, animated: false) - radioButton.accessibilityIdentifier = viewModel.accessibilityIdentifier - self.viewModel = viewModel - } - - // MARK: - Private Properties - - private lazy var radioButton: RadioButton = { - let radioButton = RadioButton() - radioButton.addTarget(self, action: #selector(didTouchRadioButton), for: .touchUpInside) - return radioButton - }() - - private var viewModel: CollectionViewRadioViewModel? - - // MARK: - Private Methods - - @objc private func didTouchRadioButton() { - viewModel?.select() - } - - private func commonInit() { - contentView.addSubview(radioButton) - let constraints = [ - radioButton.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), - radioButton.topAnchor.constraint(equalTo: contentView.topAnchor), - radioButton.centerXAnchor.constraint(equalTo: contentView.centerXAnchor), - radioButton.centerYAnchor.constraint(equalTo: contentView.centerYAnchor) - ] - NSLayoutConstraint.activate(constraints) - } -} diff --git a/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/CollectionReusableViews/Radio/CollectionViewRadioViewModel.swift b/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/CollectionReusableViews/Radio/CollectionViewRadioViewModel.swift deleted file mode 100644 index e5f40c6e5..000000000 --- a/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/CollectionReusableViews/Radio/CollectionViewRadioViewModel.swift +++ /dev/null @@ -1,25 +0,0 @@ -// -// CollectionViewRadioViewModel.swift -// ProcessOut -// -// Created by Andrii Vysotskyi on 16.08.2023. -// - -struct CollectionViewRadioViewModel: Hashable { - - /// Current value. - let value: String - - /// Indicates whether radio button is selected. - let isSelected: Bool - - /// Boolean value indicating whether value is valid. - let isInvalid: Bool - - /// Radio button's accessibility identifier. - let accessibilityIdentifier: String - - /// Closure to invoke when radio button is selected. - @ImmutableNullHashable - var select: () -> Void -} diff --git a/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/CollectionReusableViews/SectionHeader/CollectionViewSectionHeaderView.swift b/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/CollectionReusableViews/SectionHeader/CollectionViewSectionHeaderView.swift deleted file mode 100644 index f46567e07..000000000 --- a/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/CollectionReusableViews/SectionHeader/CollectionViewSectionHeaderView.swift +++ /dev/null @@ -1,58 +0,0 @@ -// -// CollectionViewSectionHeaderView.swift -// ProcessOut -// -// Created by Andrii Vysotskyi on 24.07.2023. -// - -import UIKit - -final class CollectionViewSectionHeaderView: UICollectionReusableView { - - override init(frame: CGRect) { - super.init(frame: frame) - self.commonInit() - } - - @available(*, unavailable) - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - func configure(viewModel: CollectionViewSectionHeaderViewModel, style: POTextStyle) { - titleLabel.attributedText = AttributedStringBuilder() - .with { builder in - builder.typography = style.typography - builder.textStyle = .title3 - builder.color = style.color - builder.alignment = viewModel.isCentered ? .center : .natural - builder.text = .plain(viewModel.title) - } - .build() - } - - // MARK: - Private Properties - - private lazy var titleLabel: UILabel = { - let label = UILabel() - label.translatesAutoresizingMaskIntoConstraints = false - label.adjustsFontForContentSizeCategory = false - label.numberOfLines = 0 - label.setContentHuggingPriority(.required, for: .vertical) - label.setContentCompressionResistancePriority(.required, for: .vertical) - return label - }() - - // MARK: - Private Methods - - private func commonInit() { - addSubview(titleLabel) - let constraints = [ - titleLabel.leadingAnchor.constraint(equalTo: leadingAnchor), - titleLabel.centerXAnchor.constraint(equalTo: centerXAnchor), - titleLabel.topAnchor.constraint(equalTo: topAnchor), - titleLabel.centerYAnchor.constraint(equalTo: centerYAnchor).with(priority: .defaultHigh) - ] - NSLayoutConstraint.activate(constraints) - } -} diff --git a/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/CollectionReusableViews/SectionHeader/CollectionViewSectionHeaderViewModel.swift b/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/CollectionReusableViews/SectionHeader/CollectionViewSectionHeaderViewModel.swift deleted file mode 100644 index 58ecd56e4..000000000 --- a/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/CollectionReusableViews/SectionHeader/CollectionViewSectionHeaderViewModel.swift +++ /dev/null @@ -1,15 +0,0 @@ -// -// CollectionViewSectionHeaderViewModel.swift -// ProcessOut -// -// Created by Andrii Vysotskyi on 08.08.2023. -// - -struct CollectionViewSectionHeaderViewModel: Hashable { - - /// Section title if any. - let title: String - - /// Indicates whether section header should be centered. - let isCentered: Bool -} diff --git a/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/CollectionReusableViews/Separator/CollectionViewSeparatorView.swift b/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/CollectionReusableViews/Separator/CollectionViewSeparatorView.swift deleted file mode 100644 index 4ca1d3dd6..000000000 --- a/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/CollectionReusableViews/Separator/CollectionViewSeparatorView.swift +++ /dev/null @@ -1,15 +0,0 @@ -// -// CollectionViewSeparatorView.swift -// ProcessOut -// -// Created by Andrii Vysotskyi on 24.07.2023. -// - -import UIKit - -final class CollectionViewSeparatorView: UICollectionReusableView { - - func configure(color: UIColor) { - backgroundColor = color - } -} diff --git a/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/CollectionReusableViews/Title/CollectionViewTitleCell.swift b/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/CollectionReusableViews/Title/CollectionViewTitleCell.swift deleted file mode 100644 index 2309eed77..000000000 --- a/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/CollectionReusableViews/Title/CollectionViewTitleCell.swift +++ /dev/null @@ -1,58 +0,0 @@ -// -// CollectionViewTitleCell.swift -// ProcessOut -// -// Created by Andrii Vysotskyi on 24.07.2023. -// - -import UIKit - -final class CollectionViewTitleCell: UICollectionViewCell { - - override init(frame: CGRect) { - super.init(frame: frame) - commonInit() - } - - @available(*, unavailable) - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - func configure(viewModel: CollectionViewTitleViewModel, style: POTextStyle) { - titleLabel.attributedText = AttributedStringBuilder() - .with { builder in - builder.typography = style.typography - builder.textStyle = .largeTitle - builder.lineBreakMode = .byWordWrapping - builder.color = style.color - builder.text = .plain(viewModel.text) - } - .build() - } - - // MARK: - Private Properties - - private lazy var titleLabel: UILabel = { - let label = UILabel() - label.translatesAutoresizingMaskIntoConstraints = false - label.adjustsFontForContentSizeCategory = false - label.numberOfLines = 0 - label.setContentCompressionResistancePriority(.required, for: .vertical) - label.setContentHuggingPriority(.required, for: .vertical) - return label - }() - - // MARK: - Private Methods - - private func commonInit() { - contentView.addSubview(titleLabel) - let constraints = [ - titleLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), - titleLabel.centerXAnchor.constraint(equalTo: contentView.centerXAnchor), - titleLabel.centerYAnchor.constraint(equalTo: contentView.centerYAnchor).with(priority: .defaultHigh), - titleLabel.topAnchor.constraint(equalTo: contentView.topAnchor) - ] - NSLayoutConstraint.activate(constraints) - } -} diff --git a/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/CollectionReusableViews/Title/CollectionViewTitleViewModel.swift b/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/CollectionReusableViews/Title/CollectionViewTitleViewModel.swift deleted file mode 100644 index ec9e8de9b..000000000 --- a/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/CollectionReusableViews/Title/CollectionViewTitleViewModel.swift +++ /dev/null @@ -1,12 +0,0 @@ -// -// CollectionViewTitleViewModel.swift -// ProcessOut -// -// Created by Andrii Vysotskyi on 08.08.2023. -// - -struct CollectionViewTitleViewModel: Hashable { - - /// Title text. - let text: String -} diff --git a/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Extensions/UIActivityIndicatorView+Extensions.swift b/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Extensions/UIActivityIndicatorView+Extensions.swift deleted file mode 100644 index 849948949..000000000 --- a/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Extensions/UIActivityIndicatorView+Extensions.swift +++ /dev/null @@ -1,19 +0,0 @@ -// -// UIActivityIndicatorView+Extensions.swift -// ProcessOut -// -// Created by Andrii Vysotskyi on 28.11.2022. -// - -import UIKit - -extension UIActivityIndicatorView: POActivityIndicatorView { - - public func setAnimating(_ isAnimating: Bool) { - if isAnimating { - startAnimating() - } else { - stopAnimating() - } - } -} diff --git a/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Extensions/UIView+Style.swift b/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Extensions/UIView+Style.swift deleted file mode 100644 index 16e11db90..000000000 --- a/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Extensions/UIView+Style.swift +++ /dev/null @@ -1,26 +0,0 @@ -// -// UIView+Style.swift -// ProcessOut -// -// Created by Andrii Vysotskyi on 28.11.2022. -// - -import UIKit - -extension UIView { - - /// Applies given border style to view's layer. - func apply(style: POBorderStyle) { - layer.cornerRadius = style.radius - layer.borderWidth = style.width - layer.borderColor = style.color.cgColor - } - - /// Applies given shadow style to view's layer. - func apply(style: POShadowStyle, shadowOpacity: CGFloat = 1) { - layer.shadowColor = style.color.cgColor - layer.shadowOpacity = Float(shadowOpacity) - layer.shadowOffset = style.offset - layer.shadowRadius = style.radius - } -} diff --git a/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Layouts/Center/CollectionViewCenterLayout.swift b/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Layouts/Center/CollectionViewCenterLayout.swift deleted file mode 100644 index cff016764..000000000 --- a/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Layouts/Center/CollectionViewCenterLayout.swift +++ /dev/null @@ -1,212 +0,0 @@ -// -// CollectionViewCenterLayout.swift -// ProcessOut -// -// Created by Andrii Vysotskyi on 26.04.2023. -// - -import UIKit - -final class CollectionViewCenterLayout: UICollectionViewFlowLayout { - - override init() { - centeringOffset = 0 - layoutAttributes = [:] - super.init() - } - - @available(*, unavailable) - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override var sectionHeadersPinToVisibleBounds: Bool { - didSet { assert(!sectionHeadersPinToVisibleBounds) } - } - - override var sectionFootersPinToVisibleBounds: Bool { - didSet { assert(!sectionFootersPinToVisibleBounds) } - } - - override func invalidateLayout(with context: UICollectionViewLayoutInvalidationContext) { - super.invalidateLayout(with: context) - centeredSection = nil - centeringOffset = 0 - layoutAttributes = [:] - } - - override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool { - let isWidthDifferent = abs(collectionView().bounds.width - newBounds.width) > 0.001 - return isWidthDifferent || super.shouldInvalidateLayout(forBoundsChange: newBounds) - } - - override func prepare() { - super.prepare() - prepareCentering() - prepareCellSeparatorAttributes() - } - - override var collectionViewContentSize: CGSize { - // This is a workaround for `layoutAttributesForElementsInRect:` not getting invoked enough - // times if `collectionViewContentSize.width` is not smaller than the width of the collection - // view, minus horizontal insets. - // See https://openradar.appspot.com/radar?id=5025850143539200 for more details. - let width = collectionView().bounds.inset(by: collectionView().adjustedContentInset).width - 0.0001 - return CGSize(width: width, height: super.collectionViewContentSize.height + centeringOffset) - } - - override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? { - // There are no custom item attributes so we are simply centering attributes returned by super. - super.layoutAttributesForItem(at: indexPath).flatMap(centered) - } - - override func layoutAttributesForSupplementaryView( - ofKind kind: String, at indexPath: IndexPath - ) -> UICollectionViewLayoutAttributes? { - let key = LayoutAttributesKey(indexPath: indexPath, category: .supplementaryView, kind: kind) - if let attributes = layoutAttributes[key] { - return attributes - } - return super.layoutAttributesForSupplementaryView(ofKind: kind, at: indexPath).flatMap(centered) - } - - override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? { - // Original rect's origin is shifted by negative centeringOffset and height is increased by same value - // to ensure that super will also return attributes visible after centering. - let centeringAdjustedRect = CGRect( - x: rect.minX, - y: rect.minY - centeringOffset, - width: rect.width, - height: rect.height + centeringOffset - ) - var attributes = super.layoutAttributesForElements(in: centeringAdjustedRect)?.compactMap(centered) ?? [] - let visibleLayoutAttributes = layoutAttributes.values.filter { attributes in - // Here we are using original rect because attributes are already centered. - attributes.frame.intersects(rect) - } - attributes.append(contentsOf: visibleLayoutAttributes) - return attributes - } - - // MARK: - Private Nested Types - - private enum Constants { - static let additionalSectionBackgroundBottomOffset: CGFloat = 160 - static let separatorHeight: CGFloat = 1 - } - - private struct LayoutAttributesKey: Hashable { - - /// Index path. - let indexPath: IndexPath - - /// Attributes category. - let category: UICollectionView.ElementCategory - - /// Kind. - let kind: String? - } - - // MARK: - Private Properties - - private var centeredSection: Int? - private var centeringOffset: CGFloat - private var layoutAttributes: [LayoutAttributesKey: UICollectionViewLayoutAttributes] - - // MARK: - - - private func delegate() -> CollectionViewDelegateCenterLayout { - // swiftlint:disable:next force_cast force_unwrapping - collectionView!.delegate as! CollectionViewDelegateCenterLayout - } - - private func collectionView() -> UICollectionView { - collectionView! // swiftlint:disable:this force_unwrapping - } - - // MARK: - Centering - - private func prepareCentering() { - guard let section = delegate().centeredSection(layout: self) else { - return - } - let updatedHeight = collectionView().bounds.inset(by: collectionView().adjustedContentInset).height - let offset = (updatedHeight - super.collectionViewContentSize.height) / 2 - guard offset > 0 else { - return - } - centeredSection = section - centeringOffset = offset - } - - private func centered(attributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes? { - switch attributes.representedElementCategory { - case .cell, .supplementaryView: - break - default: - // Decorations are not centered. - return attributes - } - guard let centeredSection, - attributes.indexPath.section >= centeredSection, - let attributesCopy = attributes.copy() as? UICollectionViewLayoutAttributes else { - return attributes - } - attributesCopy.frame.origin.y += centeringOffset - return attributesCopy - } - - // MARK: - Cell Separators - - private func prepareCellSeparatorAttributes() { - for section in stride(from: 0, to: collectionView().numberOfSections, by: 1) { - let numberOfItems = collectionView().numberOfItems(inSection: section) - for row in stride(from: 0, to: numberOfItems, by: 1) { - let indexPath = IndexPath(row: row, section: section) - guard delegate().collectionViewLayout(self, shouldSeparateCellAt: indexPath) else { - continue - } - let verticalOffset: CGFloat - if row == numberOfItems - 1 { - let inset = delegate().collectionView?(collectionView(), layout: self, insetForSectionAt: section) - verticalOffset = (inset ?? sectionInset).bottom / 2 - } else { - let spacing = delegate().collectionView?( - collectionView(), layout: self, minimumLineSpacingForSectionAt: section - ) ?? minimumLineSpacing - verticalOffset = spacing / 2 - } - prepareCellSeparatorAttributes(at: indexPath, verticalOffset: verticalOffset) - } - } - } - - private func prepareCellSeparatorAttributes(at indexPath: IndexPath, verticalOffset: CGFloat) { - guard let cellAttributes = layoutAttributesForItem(at: indexPath) else { - assertionFailure("Can't find attributes for item.") - return - } - let attributes = UICollectionViewLayoutAttributes( - forSupplementaryViewOfKind: Self.elementKindSeparator, with: indexPath - ) - attributes.frame = CGRect( - x: -collectionView().adjustedContentInset.left, - y: cellAttributes.frame.maxY - Constants.separatorHeight + verticalOffset, - width: collectionView().bounds.width, - height: Constants.separatorHeight - ) - attributes.zIndex = -1 - let layoutAttributesKey = LayoutAttributesKey( - indexPath: attributes.indexPath, - category: attributes.representedElementCategory, - kind: attributes.representedElementKind - ) - layoutAttributes[layoutAttributesKey] = attributes - } -} - -extension CollectionViewCenterLayout { - - /// Section background element kind. - static let elementKindSeparator = "ElementKindSeparator" -} diff --git a/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Layouts/Center/CollectionViewDelegateCenterLayout.swift b/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Layouts/Center/CollectionViewDelegateCenterLayout.swift deleted file mode 100644 index 678439d74..000000000 --- a/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Layouts/Center/CollectionViewDelegateCenterLayout.swift +++ /dev/null @@ -1,17 +0,0 @@ -// -// CollectionViewDelegateCenterLayout.swift -// ProcessOut -// -// Created by Andrii Vysotskyi on 01.05.2023. -// - -import UIKit - -protocol CollectionViewDelegateCenterLayout: AnyObject, UICollectionViewDelegateFlowLayout { - - /// Should return index of the section that should be centered. - func centeredSection(layout: UICollectionViewLayout) -> Int? - - /// Asks delegate whether cell at given index should be decorated with separator. - func collectionViewLayout(_ layout: UICollectionViewLayout, shouldSeparateCellAt indexPath: IndexPath) -> Bool -} diff --git a/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Styles/Input/POInputStateStyle.swift b/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Styles/Input/POInputStateStyle.swift deleted file mode 100644 index 1a3728988..000000000 --- a/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Styles/Input/POInputStateStyle.swift +++ /dev/null @@ -1,47 +0,0 @@ -// -// POInputStateStyle.swift -// ProcessOut -// -// Created by Andrii Vysotskyi on 24.11.2022. -// - -import UIKit - -/// Defines input's styling information in a specific state. -public struct POInputStateStyle { - - /// Text style. - public let text: POTextStyle - - /// Placeholder text style. - public let placeholder: POTextStyle - - /// Input's background color. - public let backgroundColor: UIColor - - /// Border style. - public let border: POBorderStyle - - /// Shadow style. - public let shadow: POShadowStyle - - /// Tint color that is used by input. - public let tintColor: UIColor - - /// Creates style instance. - public init( - text: POTextStyle, - placeholder: POTextStyle, - backgroundColor: UIColor, - border: POBorderStyle, - shadow: POShadowStyle, - tintColor: UIColor - ) { - self.text = text - self.placeholder = placeholder - self.backgroundColor = backgroundColor - self.border = border - self.shadow = shadow - self.tintColor = tintColor - } -} diff --git a/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Styles/Input/POInputStyle.swift b/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Styles/Input/POInputStyle.swift deleted file mode 100644 index fe793f804..000000000 --- a/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Styles/Input/POInputStyle.swift +++ /dev/null @@ -1,52 +0,0 @@ -// -// POInputStyle.swift -// ProcessOut -// -// Created by Andrii Vysotskyi on 24.05.2023. -// - -import UIKit - -@available(*, deprecated, renamed: "POInputStyle") -public typealias POTextFieldStyle = POInputStyle - -/// Defines input control style in both normal and error states. -public struct POInputStyle { - - /// Style for normal state. - public let normal: POInputStateStyle - - /// Style for error state. - public let error: POInputStateStyle - - /// Creates style instance. - public init(normal: POInputStateStyle, error: POInputStateStyle) { - self.normal = normal - self.error = error - } -} - -extension POInputStyle { - - /// Allows to create default input style with given typography. - public static func `default`(typography: POTypography? = nil) -> POInputStyle { - POInputStyle( - normal: POInputStateStyle( - text: .init(color: UIColor(poResource: .Text.primary), typography: typography ?? .Fixed.label), - placeholder: .init(color: UIColor(poResource: .Text.muted), typography: typography ?? .Fixed.label), - backgroundColor: UIColor(poResource: .Surface.background), - border: .regular(radius: 8, color: UIColor(poResource: .Border.default)), - shadow: .clear, - tintColor: UIColor(poResource: .Text.primary) - ), - error: POInputStateStyle( - text: .init(color: UIColor(poResource: .Text.primary), typography: typography ?? .Fixed.label), - placeholder: .init(color: UIColor(poResource: .Text.muted), typography: typography ?? .Fixed.label), - backgroundColor: UIColor(poResource: .Surface.background), - border: .regular(radius: 8, color: UIColor(poResource: .Text.error)), - shadow: .clear, - tintColor: UIColor(poResource: .Text.error) - ) - ) - } -} diff --git a/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Styles/POBorderStyle.swift b/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Styles/POBorderStyle.swift deleted file mode 100644 index 49c5a00c0..000000000 --- a/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Styles/POBorderStyle.swift +++ /dev/null @@ -1,40 +0,0 @@ -// -// POBorderStyle.swift -// ProcessOut -// -// Created by Andrii Vysotskyi on 28.11.2022. -// - -import UIKit - -/// Style that defines border appearance. Border is always a solid line. -public struct POBorderStyle { - - /// Corner radius. - public let radius: CGFloat - - /// Border width. - public let width: CGFloat - - /// Border color. - public let color: UIColor - - public init(radius: CGFloat, width: CGFloat, color: UIColor) { - self.radius = radius - self.width = width - self.color = color - } -} - -extension POBorderStyle { - - /// Clear border of specified radius. - public static func clear(radius: CGFloat) -> POBorderStyle { - .init(radius: radius, width: 0, color: .clear) - } - - /// Regular width border. - static func regular(radius: CGFloat, color: UIColor) -> POBorderStyle { - .init(radius: radius, width: 1, color: color) - } -} diff --git a/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Styles/POShadowStyle.swift b/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Styles/POShadowStyle.swift deleted file mode 100644 index 2bc326bcb..000000000 --- a/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Styles/POShadowStyle.swift +++ /dev/null @@ -1,57 +0,0 @@ -// -// POShadowStyle.swift -// ProcessOut -// -// Created by Andrii Vysotskyi on 28.11.2022. -// - -import UIKit - -/// Style that defines shadow appearance. -public struct POShadowStyle { - - /// The color of the shadow. - public let color: UIColor - - /// The offset (in points) of the shadow. - public let offset: CGSize - - /// The blur radius (in points) used to render the shadow. - public let radius: CGFloat - - public init(color: UIColor, offset: CGSize, radius: CGFloat) { - self.color = color - self.offset = offset - self.radius = radius - } -} - -extension POShadowStyle { - - /// Value represents no shadow. - public static let clear = Self(color: .clear, offset: .zero, radius: 0) - - static let level1 = POShadowStyle( - color: shadowColor, offset: CGSize(width: 0, height: 4), radius: 16 - ) - - static let level2 = POShadowStyle( - color: shadowColor, offset: CGSize(width: 0, height: 8), radius: 20 - ) - - static let level3 = POShadowStyle( - color: shadowColor, offset: CGSize(width: 0, height: 12), radius: 24 - ) - - static let level4 = POShadowStyle( - color: shadowColor, offset: CGSize(width: 0, height: 16), radius: 32 - ) - - static let level5 = POShadowStyle( - color: shadowColor, offset: CGSize(width: 0, height: 20), radius: 40 - ) - - // MARK: - Private Properties - - private static let shadowColor = UIColor.black.withAlphaComponent(0.08) -} diff --git a/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Typography/AttributedStringBuilder.swift b/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Typography/AttributedStringBuilder.swift deleted file mode 100644 index 629f9272f..000000000 --- a/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Typography/AttributedStringBuilder.swift +++ /dev/null @@ -1,128 +0,0 @@ -// -// AttributedStringBuilder.swift -// ProcessOut -// -// Created by Andrii Vysotskyi on 21.11.2022. -// - -import UIKit - -struct AttributedStringBuilder { - - enum Text { - case plain(String), markdown(String) - } - - /// The text alignment of the paragraph. - var alignment: NSTextAlignment = .natural - - /// The mode for breaking lines in the paragraph. - var lineBreakMode: NSLineBreakMode = .byTruncatingTail - - /// The color of the text. - var color: UIColor? - - /// The typography of the text. - var typography: POTypography? - - /// Constants that describe the preferred styles for fonts. - var textStyle: UIFont.TextStyle? - - /// The maximum point size allowed for the font. Use this value to constrain the font to - /// the specified size when your interface cannot accommodate text that is any larger. - var maximumFontSize: CGFloat? - - /// Allows to alter font with the specified symbolic traits. - var fontSymbolicTraits: UIFontDescriptor.SymbolicTraits = [] - - /// The text tab objects that represent the paragraph’s tab stops. - var tabStops: [NSTextTab] = [] - - /// The indentation of the paragraph’s lines other than the first. - var headIndent: CGFloat = 0 - - /// Contents of the future attributed string. Defaults to empty string. - var text: Text = .plain("") - - func build() -> NSAttributedString { - switch text { - case .markdown(let markdown): - let visitor = AttributedStringMarkdownVisitor(builder: self) - let document = MarkdownParser.parse(string: markdown) - return document.accept(visitor: visitor) - case .plain(let string): - return NSAttributedString(string: string, attributes: buildAttributes()) - } - } - - func buildAttributes() -> [NSAttributedString.Key: Any] { - guard let typography else { - preconditionFailure("Typography must be set.") - } - let font = font( - typography: typography, - symbolicTraits: fontSymbolicTraits, - textStyle: textStyle, - maximumFontSize: maximumFontSize - ) - var attributes: [NSAttributedString.Key: Any] = [:] - let lineHeightMultiple = typography.lineHeight / typography.font.lineHeight - attributes[.font] = font - attributes[.baselineOffset] = baselineOffset(font: font, lineHeightMultiple: lineHeightMultiple) - attributes[.foregroundColor] = color - if #available(iOS 14, *) { - attributes[.tracking] = typography.tracking - } - let paragraphStyle = NSMutableParagraphStyle() - paragraphStyle.maximumLineHeight = font.lineHeight * lineHeightMultiple - paragraphStyle.minimumLineHeight = font.lineHeight * lineHeightMultiple - paragraphStyle.lineHeightMultiple = lineHeightMultiple - paragraphStyle.paragraphSpacing = typography.paragraphSpacing - paragraphStyle.alignment = alignment - paragraphStyle.lineBreakMode = lineBreakMode - paragraphStyle.tabStops = tabStops - paragraphStyle.headIndent = headIndent - attributes[.paragraphStyle] = paragraphStyle - return attributes - } - - // MARK: - Private Methods - - private func baselineOffset(font: UIFont, lineHeightMultiple: CGFloat) -> CGFloat { - let offset = (font.lineHeight * lineHeightMultiple - font.capHeight) / 2 + font.descender - if #available(iOS 16, *) { - return offset - } - // Workaround for bug in UIKit. In order to shift baseline to the top, offset should be divided - // by two on iOS < 16. - return offset < 0 ? offset : offset / 2 - } - - private func font( - typography: POTypography, - symbolicTraits: UIFontDescriptor.SymbolicTraits, - textStyle: UIFont.TextStyle?, - maximumFontSize: CGFloat? - ) -> UIFont { - var font = typography.font - if let textStyle, typography.adjustsFontForContentSizeCategory { - font = UIFontMetrics(forTextStyle: textStyle).scaledFont(for: typography.font) - } - if let maximumFontSize, font.pointSize > maximumFontSize { - font = font.withSize(maximumFontSize) - } - if !symbolicTraits.isEmpty, let descriptor = font.fontDescriptor.withSymbolicTraits(symbolicTraits) { - font = UIFont(descriptor: descriptor, size: 0) - } - return font - } -} - -extension AttributedStringBuilder { - - func with(updates: (inout AttributedStringBuilder) -> Void) -> AttributedStringBuilder { - var builder = self - updates(&builder) - return builder - } -} diff --git a/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Typography/AttributedStringMarkdownVisitor.swift b/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Typography/AttributedStringMarkdownVisitor.swift deleted file mode 100644 index 2b245bb1a..000000000 --- a/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Typography/AttributedStringMarkdownVisitor.swift +++ /dev/null @@ -1,179 +0,0 @@ -// -// AttributedStringMarkdownVisitor.swift -// ProcessOut -// -// Created by Andrii Vysotskyi on 16.06.2023. -// - -import Foundation -import UIKit - -final class AttributedStringMarkdownVisitor: MarkdownVisitor { - - init(builder: AttributedStringBuilder, level: Int = 0) { - self.builder = builder - self.level = level - } - - // MARK: - MarkdownVisitor - - func visit(node: MarkdownUnknown) -> NSAttributedString { - node.children.map { $0.accept(visitor: self) }.joined() - } - - func visit(document: MarkdownDocument) -> NSAttributedString { - let separator = NSAttributedString(string: Constants.paragraphSeparator) - return document.children.map { $0.accept(visitor: self) }.joined(separator: separator) - } - - func visit(emphasis: MarkdownEmphasis) -> NSAttributedString { - var builder = builder - builder.fontSymbolicTraits.formUnion(.traitItalic) - let visitor = AttributedStringMarkdownVisitor(builder: builder, level: level) - return emphasis.children.map { $0.accept(visitor: visitor) }.joined() - } - - func visit(list: MarkdownList) -> NSAttributedString { - var builder = self.builder - let textList = textList(list) - builder.tabStops += listTabStops(textList, itemsCount: list.children.count) - if let tabStop = builder.tabStops.last { - builder.headIndent = tabStop.location - } - let itemsSeparator = NSAttributedString(string: Constants.paragraphSeparator) - let attributedString = list.children - .enumerated() - .map { offset, itemNode in - let childVisitor = AttributedStringMarkdownVisitor(builder: builder, level: self.level + 1) - let attributedItem = itemNode.accept(visitor: childVisitor) - let marker = - String(repeating: Constants.tab, count: level * 2 + 1) + - textList.marker(forItemNumber: textList.startingItemNumber + offset) + - Constants.tab - let attributedMarker = builder.with { $0.text = .plain(marker) }.build() - return [attributedMarker, attributedItem].joined() - } - .joined(separator: itemsSeparator) - return attributedString - } - - func visit(listItem: MarkdownListItem) -> NSAttributedString { - let separator = NSAttributedString(string: Constants.paragraphSeparator) - return listItem.children.map { $0.accept(visitor: self) }.joined(separator: separator) - } - - func visit(paragraph: MarkdownParagraph) -> NSAttributedString { - paragraph.children.map { $0.accept(visitor: self) }.joined() - } - - func visit(strong: MarkdownStrong) -> NSAttributedString { - var builder = builder - builder.fontSymbolicTraits.formUnion(.traitBold) - let visitor = AttributedStringMarkdownVisitor(builder: builder, level: level) - return strong.children.map { $0.accept(visitor: visitor) }.joined() - } - - func visit(text: MarkdownText) -> NSAttributedString { - builder.with { $0.text = .plain(text.value) }.build() - } - - /// - NOTE: Softbreak is rendered with line break. - func visit(softbreak: MarkdownSoftbreak) -> NSAttributedString { - NSAttributedString(string: Constants.lineSeparator) - } - - func visit(linebreak: MarkdownLinebreak) -> NSAttributedString { - NSAttributedString(string: Constants.lineSeparator) - } - - func visit(heading: MarkdownHeading) -> NSAttributedString { - heading.children.map { $0.accept(visitor: self) }.joined() - } - - func visit(blockQuote: MarkdownBlockQuote) -> NSAttributedString { - let separator = NSAttributedString(string: Constants.paragraphSeparator) - return blockQuote.children.map { $0.accept(visitor: self) }.joined(separator: separator) - } - - func visit(codeBlock: MarkdownCodeBlock) -> NSAttributedString { - let code = codeBlock.code - .replacingOccurrences(of: "\n", with: Constants.lineSeparator) - .trimmingCharacters(in: .newlines) - var builder = builder - builder.fontSymbolicTraits.formUnion(.traitMonoSpace) - return builder.with { $0.text = .plain(code) }.build() - } - - func visit(thematicBreak: MarkdownThematicBreak) -> NSAttributedString { - NSAttributedString(string: Constants.lineSeparator) - } - - func visit(codeSpan: MarkdownCodeSpan) -> NSAttributedString { - var builder = builder - builder.fontSymbolicTraits.formUnion(.traitMonoSpace) - return builder.with { $0.text = .plain(codeSpan.code) }.build() - } - - func visit(link: MarkdownLink) -> NSAttributedString { - let attributedString = link.children - .map { $0.accept(visitor: self) } - .joined() - .mutableCopy() as! NSMutableAttributedString // swiftlint:disable:this force_cast - if let url = link.url { - let range = NSRange(location: 0, length: attributedString.length) - attributedString.addAttribute(.link, value: url, range: range) - } - return attributedString - } - - // MARK: - Private Nested Types - - private enum Constants { - static let listMarkerWidthIncrement: CGFloat = 12 - static let listMarkerSpacing: CGFloat = 4 - static let lineSeparator = "\u{2028}" - static let paragraphSeparator = "\u{2029}" - static let tab = "\t" - } - - // MARK: - Private Properties - - private let builder: AttributedStringBuilder - private let level: Int - - // MARK: - Private Methods - - private func textList(_ list: MarkdownList) -> NSTextList { - let textList: NSTextList - switch list.type { - case .ordered(_, let startIndex): - let markerFormat = NSTextList.MarkerFormat("{decimal}.") - textList = NSTextList(markerFormat: markerFormat, options: 0) - textList.startingItemNumber = startIndex - case .bullet: - let markers: [NSTextList.MarkerFormat] = [.disc, .circle] - textList = NSTextList(markerFormat: markers[level % markers.count], options: 0) - } - return textList - } - - private func listTabStops(_ textList: NSTextList, itemsCount: Int) -> [NSTextTab] { - guard itemsCount > 0 else { - return [] - } - // Last item is expected to have the longest marker, but just to be safe, - // we are additionally increasing calculated width. - let marker = textList.marker(forItemNumber: textList.startingItemNumber + itemsCount - 1) - let indentation = builder - .with { $0.text = .plain(marker) } - .build() - .size() - .width + Constants.listMarkerWidthIncrement - let parentIndentation = builder.tabStops.last?.location ?? 0 - let tabStops = [ - NSTextTab(textAlignment: .right, location: parentIndentation + indentation), - NSTextTab(textAlignment: .left, location: parentIndentation + indentation + Constants.listMarkerSpacing) - ] - return tabStops - } -} diff --git a/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Typography/POTextStyle.swift b/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Typography/POTextStyle.swift deleted file mode 100644 index e37cacad5..000000000 --- a/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Typography/POTextStyle.swift +++ /dev/null @@ -1,23 +0,0 @@ -// -// POTextStyle.swift -// ProcessOut -// -// Created by Andrii Vysotskyi on 23.11.2022. -// - -import UIKit - -/// Text style. -public struct POTextStyle { - - /// Text foreground color. - public let color: UIColor - - /// Text typography. - public let typography: POTypography - - public init(color: UIColor, typography: POTypography) { - self.color = color - self.typography = typography - } -} diff --git a/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Typography/POTypography.swift b/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Typography/POTypography.swift deleted file mode 100644 index cb2ee3189..000000000 --- a/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Typography/POTypography.swift +++ /dev/null @@ -1,78 +0,0 @@ -// -// POTypography.swift -// ProcessOut -// -// Created by Andrii Vysotskyi on 21.11.2022. -// - -import UIKit - -/// Holds typesetting information that could be applied to displayed text. -public struct POTypography { - - /// Font associated with given typography. - public let font: UIFont - - /// Line height. If not set explicitly equals to font's line height. - public let lineHeight: CGFloat - - /// Tracking value. - public let tracking: CGFloat? - - /// This property contains the space (measured in points) added at the end of the paragraph to separate - /// it from the following paragraph. This value must be nonnegative. Default value is `0`. - public let paragraphSpacing: CGFloat - - /// A Boolean that indicates whether the font should be updated when the device’s content size category changes. - /// Default value is `true`. - public let adjustsFontForContentSizeCategory: Bool - - /// Creates typography with provided information. - public init( - font: UIFont, - lineHeight: CGFloat? = nil, - tracking: CGFloat? = nil, - paragraphSpacing: CGFloat = 0, - adjustsFontForContentSizeCategory: Bool = true - ) { - self.font = font - if let lineHeight { - assert(lineHeight >= font.lineHeight, "Line height less than font's will cause clipping") - self.lineHeight = max(lineHeight, font.lineHeight) - } else { - self.lineHeight = font.lineHeight - } - self.tracking = tracking - self.paragraphSpacing = paragraphSpacing - self.adjustsFontForContentSizeCategory = adjustsFontForContentSizeCategory - } -} - -extension POTypography { - - enum Fixed { - - /// Use for captions, status labels and tags. - static let caption = POTypography(font: FontFamily.WorkSans.regular.font(size: 12), lineHeight: 16) - - /// Use for buttons. - static let button = POTypography(font: FontFamily.WorkSans.medium.font(size: 14), lineHeight: 18) - - /// Use for body copy on larger screens, or smaller blocks of text. - static let body = POTypography( - font: FontFamily.WorkSans.regular.font(size: 16), lineHeight: 24, paragraphSpacing: 8 - ) - - /// Use for form components, error text and key value data. - static let label = POTypography(font: FontFamily.WorkSans.regular.font(size: 14), lineHeight: 18) - - /// Use for form components, error text and key value data. - static let labelHeading = POTypography(font: FontFamily.WorkSans.medium.font(size: 14), lineHeight: 18) - } - - enum Medium { - - /// Use for page titles. - static let title = POTypography(font: FontFamily.WorkSans.medium.font(size: 20), lineHeight: 28) - } -} diff --git a/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Views/ActionsContainer/ActionsContainerView.swift b/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Views/ActionsContainer/ActionsContainerView.swift deleted file mode 100644 index 53a579b42..000000000 --- a/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Views/ActionsContainer/ActionsContainerView.swift +++ /dev/null @@ -1,137 +0,0 @@ -// -// NativeAlternativePaymentMethodButtonsView.swift -// ProcessOut -// -// Created by Andrii Vysotskyi on 22.02.2023. -// - -import UIKit - -final class ActionsContainerView: UIView { - - init(style: POActionsContainerStyle, horizontalInset: CGFloat) { - self.style = style - self.horizontalInset = horizontalInset - super.init(frame: .zero) - commonInit() - } - - @available(*, unavailable) - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - func configure(viewModel: ActionsContainerViewModel, animated: Bool) { - if viewModel.primary != nil || viewModel.secondary != nil { - let animated = animated && alpha > 0 - configure(button: primaryButton, viewModel: viewModel.primary, animated: animated) - configure(button: secondaryButton, viewModel: viewModel.secondary, animated: animated) - alpha = 1 - } else { - alpha = 0 - } - } - - func contentHeight(viewModel: ActionsContainerViewModel) -> CGFloat { - guard viewModel.primary != nil || viewModel.secondary != nil else { - return 0 - } - let numberOfActions: Int - switch style.axis { - case .horizontal: - numberOfActions = 1 - case .vertical: - numberOfActions = [viewModel.primary, viewModel.secondary].compactMap { $0 }.count - @unknown default: - assertionFailure("Unexpected axis.") - numberOfActions = 1 - } - let buttonsHeight = - Constants.buttonHeight * CGFloat(numberOfActions) + - Constants.spacing * CGFloat(numberOfActions - 1) - return Constants.verticalInset * 2 + buttonsHeight - } - - var additionalBottomSafeAreaInset: CGFloat = 0 { - didSet { bottomConstraint.constant = -(additionalBottomSafeAreaInset + Constants.verticalInset) } - } - - // MARK: - Private Nested Types - - private enum Constants { - static let spacing: CGFloat = 16 - static let verticalInset: CGFloat = 16 - static let buttonHeight: CGFloat = 44 - static let separatorHeight: CGFloat = 1 - } - - // MARK: - Private Properties - - private let style: POActionsContainerStyle - private let horizontalInset: CGFloat - - private lazy var contentView: UIStackView = { - var arrangedSubviews = [primaryButton, secondaryButton] - if case .horizontal = style.axis { - arrangedSubviews.reverse() - } - let view = UIStackView(arrangedSubviews: arrangedSubviews) - view.translatesAutoresizingMaskIntoConstraints = false - view.spacing = Constants.spacing - view.axis = style.axis - view.distribution = .fillEqually - return view - }() - - private lazy var separatorView: UIView = { - let view = UIView() - view.translatesAutoresizingMaskIntoConstraints = false - view.backgroundColor = style.separatorColor - return view - }() - - private lazy var primaryButton = Button(style: style.primary) - private lazy var secondaryButton = Button(style: style.secondary) - - private lazy var bottomConstraint = contentView.bottomAnchor.constraint( - equalTo: safeAreaLayoutGuide.bottomAnchor, constant: -Constants.verticalInset - ) - - // MARK: - Private Methods - - private func commonInit() { - translatesAutoresizingMaskIntoConstraints = false - addSubview(separatorView) - addSubview(contentView) - let constraints = [ - separatorView.leadingAnchor.constraint(equalTo: leadingAnchor), - separatorView.trailingAnchor.constraint(equalTo: trailingAnchor), - separatorView.topAnchor.constraint(equalTo: topAnchor), - separatorView.heightAnchor.constraint(equalToConstant: Constants.separatorHeight), - contentView.leadingAnchor - .constraint(equalTo: safeAreaLayoutGuide.leadingAnchor, constant: horizontalInset) - .with(priority: .defaultHigh), - contentView.centerXAnchor.constraint(equalTo: safeAreaLayoutGuide.centerXAnchor), - contentView.topAnchor.constraint(equalTo: topAnchor, constant: Constants.verticalInset), - contentView.heightAnchor.constraint(equalToConstant: 0).with(priority: .defaultLow), - bottomConstraint - ] - NSLayoutConstraint.activate(constraints) - backgroundColor = style.backgroundColor - } - - private func configure(button: Button, viewModel: ActionsContainerActionViewModel?, animated: Bool) { - if let viewModel { - let buttonViewModel = Button.ViewModel( - title: viewModel.title, isLoading: viewModel.isExecuting, handler: viewModel.handler - ) - button.configure(viewModel: buttonViewModel, isEnabled: viewModel.isEnabled, animated: animated) - button.setHidden(false) - button.alpha = 1 - button.accessibilityIdentifier = viewModel.accessibilityIdentifier - } else { - button.setHidden(true) - button.alpha = 0 - } - } -} diff --git a/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Views/ActionsContainer/ActionsContainerViewModel.swift b/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Views/ActionsContainer/ActionsContainerViewModel.swift deleted file mode 100644 index 8add6aca7..000000000 --- a/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Views/ActionsContainer/ActionsContainerViewModel.swift +++ /dev/null @@ -1,33 +0,0 @@ -// -// ActionsContainerViewModel.swift -// ProcessOut -// -// Created by Andrii Vysotskyi on 04.08.2023. -// - -struct ActionsContainerViewModel { - - /// Primary action. - let primary: ActionsContainerActionViewModel? - - /// Secondary action. - let secondary: ActionsContainerActionViewModel? -} - -struct ActionsContainerActionViewModel { - - /// Action title. - let title: String - - /// Boolean value indicating whether action is enabled. - let isEnabled: Bool - - /// Boolean value indicating whether action associated with button is currently running. - let isExecuting: Bool - - /// Accessibility identifier. - let accessibilityIdentifier: String - - /// Action handler. - let handler: () -> Void -} diff --git a/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Views/ActionsContainer/POActionsContainerStyle.swift b/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Views/ActionsContainer/POActionsContainerStyle.swift deleted file mode 100644 index a86fcb7e4..000000000 --- a/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Views/ActionsContainer/POActionsContainerStyle.swift +++ /dev/null @@ -1,42 +0,0 @@ -// -// POActionsContainerStyle.swift -// ProcessOut -// -// Created by Andrii Vysotskyi on 22.05.2023. -// - -import UIKit - -/// Actions container style. -public struct POActionsContainerStyle { - - /// Style for primary button. - public let primary: POButtonStyle - - /// Style for secondary button. - public let secondary: POButtonStyle - - /// The axis along which the buttons lay out. By default actions are positioned vertically. - public let axis: NSLayoutConstraint.Axis - - /// Container separator color. - public let separatorColor: UIColor - - /// Container background color. - public let backgroundColor: UIColor - - /// Creates style instance. - public init( - primary: POButtonStyle? = nil, - secondary: POButtonStyle? = nil, - axis: NSLayoutConstraint.Axis? = nil, - separatorColor: UIColor? = nil, - backgroundColor: UIColor? = nil - ) { - self.primary = primary ?? .primary - self.secondary = secondary ?? .secondary - self.axis = axis ?? .vertical - self.separatorColor = separatorColor ?? UIColor(poResource: .Border.subtle) - self.backgroundColor = backgroundColor ?? UIColor(poResource: .Surface.level1) - } -} diff --git a/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Views/ActivityIndicator/ActivityIndicatorViewFactory.swift b/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Views/ActivityIndicator/ActivityIndicatorViewFactory.swift deleted file mode 100644 index a07913913..000000000 --- a/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Views/ActivityIndicator/ActivityIndicatorViewFactory.swift +++ /dev/null @@ -1,25 +0,0 @@ -// -// ActivityIndicatorViewFactory.swift -// ProcessOut -// -// Created by Andrii Vysotskyi on 20.12.2022. -// - -import UIKit - -final class ActivityIndicatorViewFactory { - - func create(style: POActivityIndicatorStyle) -> POActivityIndicatorView { - let view: POActivityIndicatorView - switch style { - case .custom(let customView): - view = customView - case let .system(style, color): - let indicatorView = UIActivityIndicatorView(style: style) - indicatorView.color = color - view = indicatorView - } - view.translatesAutoresizingMaskIntoConstraints = false - return view - } -} diff --git a/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Views/ActivityIndicator/POActivityIndicatorStyle.swift b/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Views/ActivityIndicator/POActivityIndicatorStyle.swift deleted file mode 100644 index 7f38e4f27..000000000 --- a/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Views/ActivityIndicator/POActivityIndicatorStyle.swift +++ /dev/null @@ -1,18 +0,0 @@ -// -// POActivityIndicatorStyle.swift -// ProcessOut -// -// Created by Andrii Vysotskyi on 23.11.2022. -// - -import UIKit - -/// Possible activity indicator styles. -public enum POActivityIndicatorStyle { - - /// Custom activity indicator. - case custom(POActivityIndicatorView) - - /// System activity indicator. - case system(UIActivityIndicatorView.Style, color: UIColor? = nil) -} diff --git a/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Views/ActivityIndicator/POActivityIndicatorView.swift b/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Views/ActivityIndicator/POActivityIndicatorView.swift deleted file mode 100644 index b42b82c8d..000000000 --- a/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Views/ActivityIndicator/POActivityIndicatorView.swift +++ /dev/null @@ -1,25 +0,0 @@ -// -// POActivityIndicatorView.swift -// ProcessOut -// -// Created by Andrii Vysotskyi on 23.11.2022. -// - -import UIKit - -@available(*, deprecated, renamed: "POActivityIndicatorView") -public typealias POActivityIndicatorViewType = POActivityIndicatorView - -/// Protocol that activity indicator should conform to in order to be used with -/// ``POActivityIndicatorStyle`` custom style. -public protocol POActivityIndicatorView: UIView { - - /// Changes animation state. - func setAnimating(_ isAnimating: Bool) - - /// A Boolean value that controls whether the receiver is hidden when the animation is stopped. - /// - /// If the value of this property is `true`, the receiver sets its `isHidden` property to `true` - /// when receiver is not animating. Otherwise the receiver is not hidden when animation stops. - var hidesWhenStopped: Bool { get set } -} diff --git a/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Views/Button/Button.swift b/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Views/Button/Button.swift deleted file mode 100644 index c067aa105..000000000 --- a/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Views/Button/Button.swift +++ /dev/null @@ -1,190 +0,0 @@ -// -// Button.swift -// ProcessOut -// -// Created by Andrii Vysotskyi on 22.11.2022. -// - -import UIKit - -final class Button: UIControl { - - struct ViewModel { - - /// Button's title. - let title: String - - /// Boolean flag indicating whether button should display loading indicator. - let isLoading: Bool - - /// Action handler. - let handler: () -> Void - } - - // MARK: - - - init(style: POButtonStyle) { - self.style = style - _isEnabled = true - super.init(frame: .zero) - commonInit() - } - - @available(*, unavailable) - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - func configure(viewModel: ViewModel, isEnabled: Bool, animated: Bool) { - UIView.perform(withAnimation: animated, duration: Constants.animationDuration) { [self] in - let currentStyle = style(isEnabled: isEnabled, isHighlighted: isHighlighted) - if viewModel.isLoading { - titleLabel.alpha = 0 - activityIndicatorView.alpha = 1 - } else { - let currentAttributedText = titleLabel.attributedText - titleLabel.attributedText = AttributedStringBuilder() - .with { builder in - builder.typography = currentStyle.title.typography - builder.textStyle = .body - builder.maximumFontSize = Constants.maximumFontSize - builder.color = currentStyle.title.color - builder.alignment = .center - builder.text = .plain(viewModel.title) - } - .build() - titleLabel.alpha = 1 - if animated, currentAttributedText != titleLabel.attributedText { - titleLabel.addTransitionAnimation() - } - activityIndicatorView.alpha = 0 - } - apply(style: currentStyle.border) - apply(style: currentStyle.shadow) - backgroundColor = currentStyle.backgroundColor - UIView.performWithoutAnimation(layoutIfNeeded) - } - _isEnabled = isEnabled - currentViewModel = viewModel - if isEnabled { - accessibilityTraits = [.button] - } else { - accessibilityTraits = [.button, .notEnabled] - } - accessibilityLabel = viewModel.title - } - - func setEnabled(_ enabled: Bool, animated: Bool) { - _isEnabled = enabled - configureWithCurrentViewModel(animated: animated) - } - - override var isEnabled: Bool { - get { _isEnabled } - set { setEnabled(newValue, animated: false) } - } - - override var isHighlighted: Bool { - didSet { configureWithCurrentViewModel(animated: true) } - } - - override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { - if let currentViewModel, currentViewModel.isLoading { - return nil - } - return super.hitTest(point, with: event) - } - - override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { - super.traitCollectionDidChange(previousTraitCollection) - if traitCollection.isColorAppearanceDifferent(to: previousTraitCollection) { - let style = style(isEnabled: isEnabled, isHighlighted: isHighlighted) - layer.borderColor = style.border.color.cgColor - layer.shadowColor = style.shadow.color.cgColor - } - } - - // MARK: - Private Nested Types - - private enum Constants { - static let height: CGFloat = 44 - static let minimumEdgesSpacing: CGFloat = 4 - static let maximumFontSize: CGFloat = 22 - static let animationDuration: TimeInterval = 0.25 - } - - // MARK: - Private Properties - - private let style: POButtonStyle - - private lazy var titleLabel: UILabel = { - let label = UILabel() - label.translatesAutoresizingMaskIntoConstraints = false - label.adjustsFontForContentSizeCategory = false - label.setContentHuggingPriority(.required, for: .horizontal) - label.setContentCompressionResistancePriority(.required, for: .horizontal) - label.alpha = 0 - return label - }() - - private lazy var activityIndicatorView: POActivityIndicatorView = { - let view = ActivityIndicatorViewFactory().create(style: style.activityIndicator) - view.hidesWhenStopped = false - view.setAnimating(true) - view.alpha = 0 - return view - }() - - private var currentViewModel: ViewModel? - private var _isEnabled: Bool - - // MARK: - Private Methods - - private func commonInit() { - translatesAutoresizingMaskIntoConstraints = false - addSubview(titleLabel) - addSubview(activityIndicatorView) - let constraints = [ - titleLabel.centerXAnchor.constraint(equalTo: centerXAnchor), - titleLabel.leadingAnchor.constraint( - greaterThanOrEqualTo: leadingAnchor, constant: Constants.minimumEdgesSpacing - ), - titleLabel.centerYAnchor.constraint(equalTo: centerYAnchor), - activityIndicatorView.centerXAnchor.constraint(equalTo: centerXAnchor), - activityIndicatorView.centerYAnchor.constraint(equalTo: centerYAnchor), - activityIndicatorView.leadingAnchor.constraint( - greaterThanOrEqualTo: leadingAnchor, constant: Constants.minimumEdgesSpacing - ), - activityIndicatorView.topAnchor.constraint( - greaterThanOrEqualTo: topAnchor, constant: Constants.minimumEdgesSpacing - ), - heightAnchor.constraint(equalToConstant: Constants.height) - ] - NSLayoutConstraint.activate(constraints) - addTarget(self, action: #selector(didTouchUpInside), for: .touchUpInside) - isAccessibilityElement = true - } - - private func configureWithCurrentViewModel(animated: Bool) { - if let currentViewModel { - configure(viewModel: currentViewModel, isEnabled: isEnabled, animated: animated) - } - } - - private func style(isEnabled: Bool, isHighlighted: Bool) -> POButtonStateStyle { - if !isEnabled { - return style.disabled - } - if isHighlighted { - return style.highlighted - } - return style.normal - } - - // MARK: - Actions - - @objc - private func didTouchUpInside() { - currentViewModel?.handler() - } -} diff --git a/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Views/Button/POButtonStateStyle.swift b/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Views/Button/POButtonStateStyle.swift deleted file mode 100644 index d636dd192..000000000 --- a/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Views/Button/POButtonStateStyle.swift +++ /dev/null @@ -1,31 +0,0 @@ -// -// POButtonStateStyle.swift -// ProcessOut -// -// Created by Andrii Vysotskyi on 28.11.2022. -// - -import UIKit - -/// Defines button's styling information in a specific state. -public struct POButtonStateStyle { - - /// Text typography. - public let title: POTextStyle - - /// Border style. - public let border: POBorderStyle - - /// Shadow style. - public let shadow: POShadowStyle - - /// Background color. - public let backgroundColor: UIColor - - public init(title: POTextStyle, border: POBorderStyle, shadow: POShadowStyle, backgroundColor: UIColor) { - self.title = title - self.border = border - self.shadow = shadow - self.backgroundColor = backgroundColor - } -} diff --git a/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Views/Button/POButtonStyle.swift b/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Views/Button/POButtonStyle.swift deleted file mode 100644 index 05e7a91c7..000000000 --- a/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Views/Button/POButtonStyle.swift +++ /dev/null @@ -1,91 +0,0 @@ -// -// POButtonStyle.swift -// ProcessOut -// -// Created by Andrii Vysotskyi on 22.11.2022. -// - -import UIKit - -/// Defines button style in all possible states. -public struct POButtonStyle { - - /// Style for normal state. - public let normal: POButtonStateStyle - - /// Style for highlighted state. - public let highlighted: POButtonStateStyle - - /// Style for disabled state. - public let disabled: POButtonStateStyle - - /// Activity indicator style. Only used with normal state. - public let activityIndicator: POActivityIndicatorStyle - - public init( - normal: POButtonStateStyle, - highlighted: POButtonStateStyle, - disabled: POButtonStateStyle, - activityIndicator: POActivityIndicatorStyle - ) { - self.normal = normal - self.highlighted = highlighted - self.disabled = disabled - self.activityIndicator = activityIndicator - } -} - -extension POButtonStyle { - - /// Default style for primary button. - public static let primary = POButtonStyle( - normal: .init( - title: .init(color: UIColor(poResource: .Text.on), typography: .Fixed.button), - border: .clear(radius: 8), - shadow: .clear, - backgroundColor: UIColor(poResource: .Action.Primary.default) - ), - highlighted: .init( - title: .init(color: UIColor(poResource: .Text.on), typography: .Fixed.button), - border: .clear(radius: 8), - shadow: .clear, - backgroundColor: UIColor(poResource: .Action.Primary.pressed) - ), - disabled: .init( - title: .init(color: UIColor(poResource: .Text.disabled), typography: .Fixed.button), - border: .clear(radius: 8), - shadow: .clear, - backgroundColor: UIColor(poResource: .Action.Primary.disabled) - ), - activityIndicator: activityIndicatorStyle(color: UIColor(poResource: .Text.on)) - ) - - /// Default style for secondary button. - public static let secondary = POButtonStyle( - normal: .init( - title: .init(color: UIColor(poResource: .Text.secondary), typography: .Fixed.button), - border: .regular(radius: 8, color: UIColor(poResource: .Border.default)), - shadow: .clear, - backgroundColor: UIColor(poResource: .Action.Secondary.default) - ), - highlighted: .init( - title: .init(color: UIColor(poResource: .Text.secondary), typography: .Fixed.button), - border: .regular(radius: 8, color: UIColor(poResource: .Border.default)), - shadow: .clear, - backgroundColor: UIColor(poResource: .Action.Secondary.pressed) - ), - disabled: .init( - title: .init(color: UIColor(poResource: .Text.disabled), typography: .Fixed.button), - border: .regular(radius: 8, color: UIColor(poResource: .Action.Border.disabled)), - shadow: .clear, - backgroundColor: .clear - ), - activityIndicator: activityIndicatorStyle(color: UIColor(poResource: .Text.secondary)) - ) - - // MARK: - Private Methods - - private static func activityIndicatorStyle(color: UIColor) -> POActivityIndicatorStyle { - .system(.medium, color: color) - } -} diff --git a/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Views/CodeTextField/CodeTextField.swift b/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Views/CodeTextField/CodeTextField.swift deleted file mode 100644 index 5f44c92e5..000000000 --- a/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Views/CodeTextField/CodeTextField.swift +++ /dev/null @@ -1,502 +0,0 @@ -// -// CodeTextField.swift -// ProcessOut -// -// Created by Andrii Vysotskyi on 23.11.2022. -// - -import UIKit - -// swiftlint:disable file_length type_body_length unused_setter_value - -final class CodeTextField: UIControl, UITextInput { - - init(length: Int) { - self.length = length - groupViews = [] - carretPosition = .before - carretPositionIndex = 0 - characters = Array(repeating: nil, count: length) - keyboardType = .default - returnKeyType = .default - isInvalid = false - super.init(frame: .zero) - commonInit() - } - - @available(*, unavailable) - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - /// Text field length. - let length: Int - - weak var delegate: CodeTextFieldDelegate? - - var text: String? { - get { String(characters.compactMap { $0 }) } - set { setText(newValue, sendActions: false) } - } - - private(set) var isInvalid: Bool - - func configure(isInvalid: Bool, style: POInputStyle, animated: Bool) { - self.style = style - self.isInvalid = isInvalid - configureWithCurrentState(animated: animated) - } - - // MARK: - UIControl - - override var canBecomeFirstResponder: Bool { - true - } - - @discardableResult - override func becomeFirstResponder() -> Bool { - if delegate?.codeTextFieldShouldBeginEditing(self) == false { - return false - } - let didBecomeResponder = super.becomeFirstResponder() - configureWithCurrentState(animated: true) - return didBecomeResponder - } - - @discardableResult - override func resignFirstResponder() -> Bool { - let didResignResponder = super.resignFirstResponder() - configureWithCurrentState(animated: true) - return didResignResponder - } - - override func paste(_ sender: Any?) { - if let string = UIPasteboard.general.string { - setText(string, sendActions: true) - } - } - - // MARK: - UITextInput - - var inputDelegate: UITextInputDelegate? - - // MARK: - Replacing and returning text - - func text(in range: UITextRange) -> String? { - guard let range = range as? TextRange, let text else { - return nil - } - // swiftlint:disable:next legacy_objc_type - return (text as NSString).substring(with: range.range) - } - - func replace(_ range: UITextRange, withText text: String) { - // Not supported - } - - // MARK: - Marked and selected text - - var selectedTextRange: UITextRange? { - get { nil } - set { /* Ignored */ } - } - - private(set) var markedTextRange: UITextRange? - - // Ignored - var markedTextStyle: [NSAttributedString.Key: Any]? - - func setMarkedText(_ markedText: String?, selectedRange: NSRange) { - // Not supported - } - - func unmarkText() { - // Not supported - } - - // MARK: - Text ranges and text positions - - func textRange(from fromPosition: UITextPosition, to toPosition: UITextPosition) -> UITextRange? { - guard let fromPosition = fromPosition as? TextPosition, let toPosition = toPosition as? TextPosition else { - return nil - } - let range = NSRange( - location: min(fromPosition.offset, toPosition.offset), length: abs(toPosition.offset - fromPosition.offset) - ) - return TextRange(range: range) - } - - func position(from position: UITextPosition, offset: Int) -> UITextPosition? { - guard let position = position as? TextPosition else { - return nil - } - let textOffset = min(max(position.offset + offset, 0), text?.count ?? 0) - return TextPosition(offset: textOffset) - } - - func position(from position: UITextPosition, in direction: UITextLayoutDirection, offset: Int) -> UITextPosition? { - switch direction { - case .right: - return self.position(from: position, offset: offset) - case .left: - return self.position(from: position, offset: -offset) - default: - return position - } - } - - var beginningOfDocument: UITextPosition { - TextPosition(offset: 0) - } - - var endOfDocument: UITextPosition { - TextPosition(offset: text?.count ?? 0) - } - - func compare(_ position: UITextPosition, to other: UITextPosition) -> ComparisonResult { - guard let position = position as? TextPosition, let other = other as? TextPosition else { - assertionFailure("Invalid text position class.") - return .orderedSame - } - if position.offset < other.offset { - return .orderedAscending - } else if position.offset > other.offset { - return .orderedDescending - } - return .orderedSame - } - - func offset(from: UITextPosition, to toPosition: UITextPosition) -> Int { - guard let start = from as? TextPosition, let end = toPosition as? TextPosition else { - assertionFailure("Invalid text position class.") - return 0 - } - return end.offset - start.offset - } - - // MARK: - Layout and Writing Direction - - func position(within range: UITextRange, farthestIn direction: UITextLayoutDirection) -> UITextPosition? { - guard let range = range as? TextRange else { - return nil - } - let offset: Int - switch direction { - case .up, .left: - offset = range.range.location - case .right, .down: - offset = range.range.location + range.range.length - @unknown default: - return nil - } - return TextPosition(offset: offset) - } - - func characterRange(byExtending position: UITextPosition, in direction: UITextLayoutDirection) -> UITextRange? { - guard let position = position as? TextPosition else { - return nil - } - let range: NSRange - switch direction { - case .up, .left: - range = NSRange(location: position.offset - 1, length: 1) - case .down, .right: - range = NSRange(location: position.offset, length: 1) - @unknown default: - return nil - } - return TextRange(range: range) - } - - func baseWritingDirection( - for position: UITextPosition, in direction: UITextStorageDirection - ) -> NSWritingDirection { - .leftToRight - } - - func setBaseWritingDirection(_ writingDirection: NSWritingDirection, for range: UITextRange) { - // Ignored - } - - // MARK: - Geometry - - func firstRect(for range: UITextRange) -> CGRect { - bounds - } - - func caretRect(for position: UITextPosition) -> CGRect { - .zero - } - - // MARK: - Hit-testing - - func selectionRects(for range: UITextRange) -> [UITextSelectionRect] { - [] - } - - func closestPosition(to point: CGPoint) -> UITextPosition? { - nil - } - - func closestPosition(to point: CGPoint, within range: UITextRange) -> UITextPosition? { - nil - } - - func characterRange(at point: CGPoint) -> UITextRange? { - nil - } - - // MARK: - Tokenizing input text - - private(set) lazy var tokenizer: UITextInputTokenizer = UITextInputStringTokenizer(textInput: self) - - // MARK: - UIKeyInput - - var hasText: Bool { - characters.contains { $0 != nil } - } - - func insertText(_ text: String) { - if let character = text.last, character.isNewline { - if delegate?.codeTextFieldShouldReturn(self) != false { - resignFirstResponder() - } - return - } - let insertionIndex: Int - if text.count == length { - insertionIndex = 0 - } else if case .after = carretPosition, characters[carretPositionIndex] != nil { - insertionIndex = carretPositionIndex + 1 - } else { - insertionIndex = carretPositionIndex - } - guard characters.indices.contains(insertionIndex) else { - return - } - let insertedText = Array(text.prefix(length - insertionIndex)) - characters.replaceSubrange(insertionIndex ..< insertionIndex + insertedText.count, with: insertedText) - carretPositionIndex = insertionIndex + insertedText.count - if carretPositionIndex > length - 1 { - carretPositionIndex = length - 1 - carretPosition = .after - } else { - carretPosition = .before - } - configureWithCurrentState(animated: false) - didChangeEditing() - } - - func deleteBackward() { - let removalIndex: Int - if case .before = carretPosition { - removalIndex = carretPositionIndex - 1 - } else { - removalIndex = carretPositionIndex - } - guard characters.indices.contains(removalIndex) else { - return - } - characters[removalIndex] = nil - carretPosition = .before - carretPositionIndex = removalIndex - configureWithCurrentState(animated: false) - didChangeEditing() - } - - var keyboardType: UIKeyboardType - var returnKeyType: UIReturnKeyType - var textContentType: UITextContentType? - - // MARK: - Private Nested Types - - private enum Constants { - static let height: CGFloat = 44 - static let spacing: CGFloat = 8 - static let minimumSpacing: CGFloat = 6 - } - - private final class TextPosition: UITextPosition { - - let offset: Int - - init(offset: Int) { - self.offset = offset - } - } - - private final class TextRange: UITextRange { - - let range: NSRange - - override var start: TextPosition { - TextPosition(offset: range.location) - } - - override var end: TextPosition { - TextPosition(offset: range.location + range.length) - } - - override var isEmpty: Bool { - range.length == 0 - } - - init(range: NSRange) { - self.range = range - } - } - - // MARK: - Private Properties - - private lazy var contentView: UIView = { - let view = UIView() - view.backgroundColor = .clear - view.translatesAutoresizingMaskIntoConstraints = false - return view - }() - - private var groupViews: [CodeTextFieldComponentView] - private var carretPosition: CodeTextFieldCarretPosition - private var carretPositionIndex: Int - private var characters: [Character?] - private var style: POInputStyle? - - // MARK: - Private Methods - - private func commonInit() { - translatesAutoresizingMaskIntoConstraints = false - addSubview(contentView) - let fixedLeadingConstraint = contentView.leadingAnchor.constraint(equalTo: leadingAnchor) - fixedLeadingConstraint.priority = .defaultLow - let constraints = [ - contentView.heightAnchor.constraint(equalToConstant: Constants.height), - contentView.topAnchor.constraint(equalTo: topAnchor), - contentView.centerYAnchor.constraint(equalTo: centerYAnchor), - contentView.centerXAnchor.constraint(equalTo: centerXAnchor), - contentView.leadingAnchor.constraint(greaterThanOrEqualTo: leadingAnchor), - fixedLeadingConstraint - ] - NSLayoutConstraint.activate(constraints) - createGroupViews() - addContextMenuGesture() - isAccessibilityElement = true - } - - private func createGroupViews() { - groupViews.forEach { view in - view.removeFromSuperview() - } - groupViews = stride(from: 0, to: length, by: 1).map(createCodeTextFieldComponentView) - var constraints = [ - groupViews[0].leadingAnchor.constraint(equalTo: contentView.leadingAnchor), - groupViews[groupViews.count - 1].trailingAnchor.constraint(equalTo: contentView.trailingAnchor) - ] - groupViews.enumerated().forEach { offset, groupView in - contentView.addSubview(groupView) - var viewConstraints = [ - groupView.topAnchor.constraint(equalTo: contentView.topAnchor), - groupView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor) - ] - if groupViews.indices.contains(offset + 1) { - let nextView = groupViews[offset + 1] - let constraints = [ - nextView.leadingAnchor - .constraint(equalTo: groupView.trailingAnchor, constant: Constants.spacing) - .with(priority: .defaultLow), - nextView.leadingAnchor.constraint( - greaterThanOrEqualTo: groupView.trailingAnchor, constant: Constants.minimumSpacing - ), - nextView.widthAnchor.constraint(equalTo: groupView.widthAnchor) - ] - viewConstraints.append(contentsOf: constraints) - } - constraints.append(contentsOf: viewConstraints) - } - NSLayoutConstraint.activate(constraints) - } - - private func setCarretPosition(position: CodeTextFieldCarretPosition, index: Int) { - if !isFirstResponder { - becomeFirstResponder() - } else if characters.indices.contains(index) { - let updatedCarretPosition: CodeTextFieldCarretPosition - if characters[index] != nil { - updatedCarretPosition = position - } else { - updatedCarretPosition = .before - } - if carretPositionIndex == index, carretPosition == updatedCarretPosition { - showContextMenu() - } else { - carretPositionIndex = index - carretPosition = updatedCarretPosition - } - configureWithCurrentState(animated: true) - } else { - assertionFailure("Invalid index.") - } - } - - private func setText(_ text: String?, sendActions: Bool) { - guard self.text != text else { - return - } - characters = Array(repeating: nil, count: length) - if let text, !text.isEmpty { - let insertedTextCharacters = Array(text.prefix(length)) - characters.replaceSubrange(0 ..< insertedTextCharacters.count, with: insertedTextCharacters) - carretPositionIndex = min(insertedTextCharacters.count, length - 1) - carretPosition = insertedTextCharacters.count == length ? .after : .before - } else { - carretPositionIndex = 0 - carretPosition = .before - } - configureWithCurrentState(animated: false) - didChangeEditing(sendActions: sendActions) - } - - private func createCodeTextFieldComponentView(index: Int) -> CodeTextFieldComponentView { - let size = CGSize(width: Constants.height, height: Constants.height) - let view = CodeTextFieldComponentView(size: size) { [weak self] position in - self?.setCarretPosition(position: position, index: index) - } - return view - } - - private func configureWithCurrentState(animated: Bool) { - guard let style else { - return - } - let stateStyle = isInvalid ? style.error : style.normal - characters.enumerated().forEach { offset, character in - let viewModel = CodeTextFieldComponentView.ViewModel( - value: character, - carretPosition: isFirstResponder && carretPositionIndex == offset ? carretPosition : nil, - style: stateStyle - ) - groupViews[offset].configure(viewModel: viewModel, animated: animated) - } - accessibilityValue = text - } - - private func didChangeEditing(sendActions: Bool = true) { - if sendActions { - self.sendActions(for: .editingChanged) - } - UIMenuController.shared.hideMenu(from: self) - } - - // MARK: - Context Menu - - private func addContextMenuGesture() { - let gesture = UILongPressGestureRecognizer(target: self, action: #selector(showContextMenu)) - contentView.addGestureRecognizer(gesture) - } - - @objc - private func showContextMenu() { - UIMenuController.shared.showMenu(from: self, rect: contentView.frame) - } -} - -// swiftlint:enable file_length type_body_length unused_setter_value diff --git a/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Views/CodeTextField/CodeTextFieldCarretPosition.swift b/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Views/CodeTextField/CodeTextFieldCarretPosition.swift deleted file mode 100644 index 6e8a1bdb0..000000000 --- a/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Views/CodeTextField/CodeTextFieldCarretPosition.swift +++ /dev/null @@ -1,10 +0,0 @@ -// -// CodeTextFieldCarretPosition.swift -// ProcessOut -// -// Created by Andrii Vysotskyi on 24.11.2022. -// - -enum CodeTextFieldCarretPosition { - case before, after -} diff --git a/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Views/CodeTextField/CodeTextFieldComponentView.swift b/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Views/CodeTextField/CodeTextFieldComponentView.swift deleted file mode 100644 index 24e553bdb..000000000 --- a/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Views/CodeTextField/CodeTextFieldComponentView.swift +++ /dev/null @@ -1,172 +0,0 @@ -// -// CodeTextFieldComponentView.swift -// ProcessOut -// -// Created by Andrii Vysotskyi on 24.11.2022. -// - -import UIKit - -final class CodeTextFieldComponentView: UIView { - - struct ViewModel { - - /// Components value. - let value: Character? - - /// Carret position if any. - let carretPosition: CodeTextFieldCarretPosition? - - /// Style. - let style: POInputStateStyle - } - - init(size: CGSize, didSelect: @escaping (CodeTextFieldCarretPosition) -> Void) { - self.size = size - self.didSelect = didSelect - super.init(frame: .zero) - commonInit() - } - - @available(*, unavailable) - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { - super.traitCollectionDidChange(previousTraitCollection) - if let currentViewModel, traitCollection.isColorAppearanceDifferent(to: previousTraitCollection) { - layer.borderColor = currentViewModel.style.border.color.cgColor - layer.shadowColor = currentViewModel.style.shadow.color.cgColor - } - } - - func configure(viewModel: ViewModel, animated: Bool) { - UIView.perform(withAnimation: animated, duration: Constants.animationDuration) { [self] in - switch viewModel.carretPosition { - case .none: - carretView.setHidden(true) - removeCarretAnimation() - case .before: - animateCarretBlinking() - carretView.setHidden(false) - carretCenterConstraint.constant = -Constants.carretOffset - case .after: - animateCarretBlinking() - carretView.setHidden(false) - carretCenterConstraint.constant = Constants.carretOffset - } - let previousAttributedText = valueLabel.attributedText - valueLabel.attributedText = AttributedStringBuilder() - .with { builder in - builder.typography = viewModel.style.text.typography - builder.textStyle = .largeTitle - builder.maximumFontSize = Constants.maximumFontSize - builder.alignment = .center - builder.color = viewModel.style.text.color - builder.text = .plain(viewModel.value.map(String.init) ?? "") - } - .build() - if animated, valueLabel.attributedText != previousAttributedText { - valueLabel.addTransitionAnimation() - } - apply(style: viewModel.style.border) - apply(style: viewModel.style.shadow) - backgroundColor = viewModel.style.backgroundColor - carretView.backgroundColor = viewModel.style.tintColor - UIView.performWithoutAnimation(layoutIfNeeded) - } - currentViewModel = viewModel - accessibilityValue = viewModel.value.map(String.init) - } - - // MARK: - Private Nested Types - - private enum Constants { - static let maximumFontSize: CGFloat = 28 - static let carretSize = CGSize(width: 2, height: 24) - static let carretOffset: CGFloat = 10 - static let animationDuration: TimeInterval = 0.25 - static let carretAnimationDuration: TimeInterval = 0.4 - static let carretAnimationKey = "BlinkingAnimation" - } - - // MARK: - Private Properties - - private let didSelect: (CodeTextFieldCarretPosition) -> Void - private let size: CGSize - - private lazy var valueLabel: UILabel = { - let label = UILabel() - label.translatesAutoresizingMaskIntoConstraints = false - label.adjustsFontForContentSizeCategory = false - label.isUserInteractionEnabled = false - return label - }() - - private lazy var carretView: UIView = { - let view = UIView() - view.translatesAutoresizingMaskIntoConstraints = false - view.isUserInteractionEnabled = false - view.alpha = 1 - return view - }() - - private lazy var carretCenterConstraint: NSLayoutConstraint = { - carretView.centerXAnchor.constraint(equalTo: centerXAnchor) - }() - - private var currentViewModel: ViewModel? - - // MARK: - Private Methods - - private func commonInit() { - translatesAutoresizingMaskIntoConstraints = false - addSubview(valueLabel) - addSubview(carretView) - let constraints = [ - heightAnchor.constraint(equalToConstant: size.height), - widthAnchor.constraint(equalToConstant: size.width), - valueLabel.centerYAnchor.constraint(equalTo: centerYAnchor), - valueLabel.centerXAnchor.constraint(equalTo: centerXAnchor), - carretView.widthAnchor.constraint(equalToConstant: Constants.carretSize.width), - carretView.heightAnchor.constraint(equalToConstant: Constants.carretSize.height), - carretView.centerYAnchor.constraint(equalTo: centerYAnchor), - carretCenterConstraint - ] - NSLayoutConstraint.activate(constraints) - configureGestures() - } - - private func configureGestures() { - let tapGesture = UITapGestureRecognizer(target: self, action: #selector(didRecognizeTapGesture)) - addGestureRecognizer(tapGesture) - } - - private func animateCarretBlinking() { - let animation = CABasicAnimation(keyPath: "opacity") - animation.fromValue = 1 - animation.toValue = 0 - animation.duration = Constants.carretAnimationDuration - animation.autoreverses = true - animation.repeatCount = .greatestFiniteMagnitude - carretView.layer.add(animation, forKey: Constants.carretAnimationKey) - } - - private func removeCarretAnimation() { - carretView.layer.removeAnimation(forKey: Constants.carretAnimationKey) - } - - // MARK: - Actions - - @objc - private func didRecognizeTapGesture(gesture: UITapGestureRecognizer) { - let desiredCarretPosition: CodeTextFieldCarretPosition - if currentViewModel?.value != nil, gesture.location(in: self).x > bounds.midX { - desiredCarretPosition = .after - } else { - desiredCarretPosition = .before - } - didSelect(desiredCarretPosition) - } -} diff --git a/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Views/CodeTextField/CodeTextFieldDelegate.swift b/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Views/CodeTextField/CodeTextFieldDelegate.swift deleted file mode 100644 index 0d81b96d1..000000000 --- a/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Views/CodeTextField/CodeTextFieldDelegate.swift +++ /dev/null @@ -1,22 +0,0 @@ -// -// CodeTextFieldDelegate.swift -// ProcessOut -// -// Created by Andrii Vysotskyi on 29.11.2022. -// - -import UIKit - -protocol CodeTextFieldDelegate: AnyObject { - - /// Asks the delegate whether to begin editing in the specified text field. - /// - /// - Returns: `true` if editing should begin or `false` if it should not. - func codeTextFieldShouldBeginEditing(_ textField: CodeTextField) -> Bool - - /// Asks the delegate whether to process the pressing of the Return button for the text field. - /// - /// - Returns: `true` if the text field should implement its default behaviour for the return button; - /// otherwise, `false`. - func codeTextFieldShouldReturn(_ textField: CodeTextField) -> Bool -} diff --git a/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Views/Picker/Picker.swift b/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Views/Picker/Picker.swift deleted file mode 100644 index 6052c44fc..000000000 --- a/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Views/Picker/Picker.swift +++ /dev/null @@ -1,174 +0,0 @@ -// -// Picker.swift -// ProcessOut -// -// Created by Andrii Vysotskyi on 24.04.2023. -// - -import UIKit - -// todo(andrii-vysotskyi): add placeholder -final class Picker: UIControl { - - init() { - super.init(frame: .zero) - commonInit() - } - - @available(*, unavailable) - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - func configure(viewModel: PickerViewModel, style: POInputStyle, animated: Bool) { - currentViewModel = viewModel - currentStyle = viewModel.isInvalid ? style.error : style.normal - configureWithCurrentState(animated: animated) - } - - override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { - super.traitCollectionDidChange(previousTraitCollection) - guard traitCollection.isColorAppearanceDifferent(to: previousTraitCollection), let currentStyle else { - return - } - layer.borderColor = currentStyle.border.color.cgColor - layer.shadowColor = currentStyle.shadow.color.cgColor - } - - // MARK: - UIContextMenuInteractionDelegate - - @available(iOS 14.0, *) - override func contextMenuInteraction( - _ interaction: UIContextMenuInteraction, configurationForMenuAtLocation location: CGPoint - ) -> UIContextMenuConfiguration? { - guard let currentViewModel else { - return nil - } - let configuration = UIContextMenuConfiguration(identifier: nil, previewProvider: nil) { _ in - let menuChildren = currentViewModel.options.map { option in - UIAction(title: option.title, state: option.isSelected ? .on : .off) { _ in - option.select() - } - } - return UIMenu(children: menuChildren) - } - return configuration - } - - // MARK: - Private Nested Types - - private enum Constants { - static let height: CGFloat = 44 - static let horizontalInset: CGFloat = 12 - static let maximumFontSize: CGFloat = 22 - static let animationDuration: TimeInterval = 0.25 - } - - // MARK: - Private Properties - - private lazy var titleLabel: UILabel = { - let label = UILabel() - label.translatesAutoresizingMaskIntoConstraints = false - label.adjustsFontForContentSizeCategory = false - label.setContentHuggingPriority(.defaultHigh, for: .horizontal) - label.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal) - return label - }() - - private lazy var iconImageView: UIImageView = { - let imageView = UIImageView(image: UIImage(poResource: .chevronDown).withRenderingMode(.alwaysTemplate)) - imageView.translatesAutoresizingMaskIntoConstraints = false - imageView.setContentHuggingPriority(.required, for: .horizontal) - imageView.setContentCompressionResistancePriority(.required, for: .horizontal) - return imageView - }() - - private var currentStyle: POInputStateStyle? - private var currentViewModel: PickerViewModel? - - // MARK: - Private Methods - - private func commonInit() { - accessibilityTraits = [.button] - translatesAutoresizingMaskIntoConstraints = false - addSubview(titleLabel) - addSubview(iconImageView) - let constraints = [ - titleLabel.leadingAnchor.constraint(equalTo: leadingAnchor, constant: Constants.horizontalInset), - titleLabel.centerYAnchor.constraint(equalTo: centerYAnchor), - titleLabel.trailingAnchor.constraint( - equalTo: iconImageView.leadingAnchor, constant: -Constants.horizontalInset - ), - iconImageView.centerYAnchor.constraint(equalTo: centerYAnchor), - iconImageView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -Constants.horizontalInset), - heightAnchor.constraint(equalToConstant: Constants.height) - ] - NSLayoutConstraint.activate(constraints) - if #available(iOS 14, *) { - showsMenuAsPrimaryAction = true - isContextMenuInteractionEnabled = true - } else { - addTarget(self, action: #selector(didTouchUpInside), for: .touchUpInside) - } - isAccessibilityElement = true - } - - private func configureWithCurrentState(animated: Bool) { - guard let currentViewModel, let currentStyle else { - return - } - UIView.perform(withAnimation: animated, duration: Constants.animationDuration) { [self] in - let currentAttributedText = titleLabel.attributedText - titleLabel.attributedText = AttributedStringBuilder() - .with { builder in - builder.typography = currentStyle.text.typography - builder.textStyle = .body - builder.maximumFontSize = Constants.maximumFontSize - builder.color = currentStyle.text.color - builder.alignment = .natural - builder.text = .plain(currentViewModel.title) - } - .build() - if animated, currentAttributedText != titleLabel.attributedText { - titleLabel.addTransitionAnimation() - } - apply(style: currentStyle.border) - apply(style: currentStyle.shadow) - iconImageView.tintColor = currentStyle.tintColor - backgroundColor = currentStyle.backgroundColor - UIView.performWithoutAnimation(layoutIfNeeded) - } - accessibilityLabel = currentViewModel.title - } - - // MARK: - Actions - - @objc - private func didTouchUpInside() { - // Fallback for iOS < 14 - guard let currentViewModel else { - return - } - let actionSheetController = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet) - for option in currentViewModel.options { - let action = UIAlertAction(title: option.title, style: .default) { _ in - option.select() - } - actionSheetController.addAction(action) - } - let viewController: UIViewController? = { - var nextResponder = next - while nextResponder != nil { - if let viewController = nextResponder as? UIViewController { - return viewController - } - nextResponder = nextResponder?.next - } - return nil - }() - guard let viewController else { - return - } - viewController.present(actionSheetController, animated: true) - } -} diff --git a/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Views/Picker/PickerViewModel.swift b/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Views/Picker/PickerViewModel.swift deleted file mode 100644 index a1b7e0afe..000000000 --- a/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Views/Picker/PickerViewModel.swift +++ /dev/null @@ -1,30 +0,0 @@ -// -// PickerViewModel.swift -// ProcessOut -// -// Created by Andrii Vysotskyi on 24.04.2023. -// - -struct PickerViewModel { - - struct Option { - - /// Option title. - let title: String - - /// Indicates whether option is currently selected. - let isSelected: Bool - - /// Closure to invoke when option is selected. - let select: () -> Void - } - - /// Button's title. - let title: String - - /// Boolean flag indicating whether input is invalid. - let isInvalid: Bool - - /// Picker options. - let options: [Option] -} diff --git a/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Views/RadioButton/PORadioButtonKnobStateStyle.swift b/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Views/RadioButton/PORadioButtonKnobStateStyle.swift deleted file mode 100644 index 197fe377e..000000000 --- a/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Views/RadioButton/PORadioButtonKnobStateStyle.swift +++ /dev/null @@ -1,38 +0,0 @@ -// -// PORadioButtonKnobStateStyle.swift -// ProcessOut -// -// Created by Andrii Vysotskyi on 08.06.2023. -// - -import UIKit - -/// Describes radio button knob style in a particular state. -public struct PORadioButtonKnobStateStyle { - - /// Background color. - public let backgroundColor: UIColor - - /// Border style. - /// - NOTE: component ignores specified border radius so your implementation may pass 0. - public let border: POBorderStyle - - /// Color of inner circle displayed in the middle of radio button. - public let innerCircleColor: UIColor - - /// Inner circle radius. - public let innerCircleRadius: CGFloat - - /// Create style instance. - public init( - backgroundColor: UIColor, - border: POBorderStyle, - innerCircleColor: UIColor, - innerCircleRadius: CGFloat - ) { - self.backgroundColor = backgroundColor - self.border = border - self.innerCircleColor = innerCircleColor - self.innerCircleRadius = innerCircleRadius - } -} diff --git a/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Views/RadioButton/PORadioButtonStateStyle.swift b/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Views/RadioButton/PORadioButtonStateStyle.swift deleted file mode 100644 index 0fa7ed2e5..000000000 --- a/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Views/RadioButton/PORadioButtonStateStyle.swift +++ /dev/null @@ -1,24 +0,0 @@ -// -// PORadioButtonStateStyle.swift -// ProcessOut -// -// Created by Andrii Vysotskyi on 06.06.2023. -// - -import UIKit - -/// Describes radio button style in a particular state, for example when selected. -public struct PORadioButtonStateStyle { - - /// Styling of the radio button knob not including value. - public let knob: PORadioButtonKnobStateStyle - - /// Radio button's value style. - public let value: POTextStyle - - /// Creates state style. - public init(knob: PORadioButtonKnobStateStyle, value: POTextStyle) { - self.knob = knob - self.value = value - } -} diff --git a/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Views/RadioButton/PORadioButtonStyle.swift b/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Views/RadioButton/PORadioButtonStyle.swift deleted file mode 100644 index fdc5096bc..000000000 --- a/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Views/RadioButton/PORadioButtonStyle.swift +++ /dev/null @@ -1,84 +0,0 @@ -// -// PORadioButtonStyle.swift -// ProcessOut -// -// Created by Andrii Vysotskyi on 06.06.2023. -// - -import UIKit - -/// Describes radio button style in different states. -public struct PORadioButtonStyle { - - /// Style to use when radio button is in default state ie enabled and not selected. - public let normal: PORadioButtonStateStyle - - /// Style to use when radio button is selected. - public let selected: PORadioButtonStateStyle - - /// Style to use when radio button is highlighted. Note that radio can transition - /// to this state when already selected. - public let highlighted: PORadioButtonStateStyle - - /// Style to use when radio button is in error state. - public let error: PORadioButtonStateStyle - - /// Creates style instance. - public init( - normal: PORadioButtonStateStyle, - selected: PORadioButtonStateStyle, - highlighted: PORadioButtonStateStyle, - error: PORadioButtonStateStyle - ) { - self.normal = normal - self.selected = selected - self.highlighted = highlighted - self.error = error - } -} - -extension PORadioButtonStyle { - - static let `default` = PORadioButtonStyle( - normal: .init( - knob: .init( - backgroundColor: .clear, - border: .regular(radius: 0, color: UIColor(poResource: .Border.default)), - innerCircleColor: .clear, - innerCircleRadius: 0 - ), - value: valueStyle - ), - selected: .init( - knob: .init( - backgroundColor: .clear, - border: .regular(radius: 0, color: UIColor(poResource: .Action.Primary.default)), - innerCircleColor: UIColor(poResource: .Action.Primary.default), - innerCircleRadius: 4 - ), - value: valueStyle - ), - highlighted: .init( - knob: .init( - backgroundColor: .clear, - border: .regular(radius: 0, color: UIColor(poResource: .Text.muted)), - innerCircleColor: .clear, - innerCircleRadius: 0 - ), - value: valueStyle - ), - error: .init( - knob: .init( - backgroundColor: .clear, - border: .regular(radius: 0, color: UIColor(poResource: .Text.error)), - innerCircleColor: .clear, - innerCircleRadius: 0 - ), - value: valueStyle - ) - ) - - // MARK: - Private Properties - - private static let valueStyle = POTextStyle(color: UIColor(poResource: .Text.primary), typography: .Fixed.label) -} diff --git a/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Views/RadioButton/RadioButton.swift b/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Views/RadioButton/RadioButton.swift deleted file mode 100644 index 3192bcce3..000000000 --- a/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Views/RadioButton/RadioButton.swift +++ /dev/null @@ -1,128 +0,0 @@ -// -// RadioButton.swift -// ProcessOut -// -// Created by Andrii Vysotskyi on 06.06.2023. -// - -import Foundation -import UIKit - -// todo(andrii-vysotskyi): respond to `isSelected` changes. -final class RadioButton: UIControl { - - init() { - super.init(frame: .zero) - commonInit() - } - - @available(*, unavailable) - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override var isHighlighted: Bool { - didSet { configureWithCurrentState(animated: true) } - } - - func configure(viewModel: RadioButtonViewModel, style: PORadioButtonStyle, animated: Bool) { - self.viewModel = viewModel - self.style = style - configureWithCurrentState(animated: animated) - } - - // MARK: - Private Nested Types - - private enum Constants { - static let minimumHeight: CGFloat = 44 - static let knobSize: CGFloat = 18 - static let animationDuration: TimeInterval = 0.25 - static let horizontalSpacing: CGFloat = 8 - } - - // MARK: - Private Properties - - private lazy var valueLabel: UILabel = { - let label = UILabel() - label.translatesAutoresizingMaskIntoConstraints = false - label.numberOfLines = 0 - label.setContentHuggingPriority(.required, for: .vertical) - label.setContentCompressionResistancePriority(.required, for: .vertical) - label.adjustsFontForContentSizeCategory = false - return label - }() - - private lazy var knobView = RadioButtonKnobView(size: Constants.knobSize) - private lazy var knobViewCenterYConstraint = knobView.centerYAnchor.constraint(equalTo: valueLabel.topAnchor) - - private var viewModel: RadioButtonViewModel? - private var style: PORadioButtonStyle? - - // MARK: - Private Methods - - private func commonInit() { - translatesAutoresizingMaskIntoConstraints = false - addSubview(knobView) - addSubview(valueLabel) - let constraints = [ - heightAnchor.constraint(equalToConstant: Constants.minimumHeight).with(priority: .defaultHigh), - knobView.leadingAnchor.constraint(equalTo: leadingAnchor), - knobViewCenterYConstraint, - valueLabel.leadingAnchor.constraint( - equalTo: knobView.trailingAnchor, constant: Constants.horizontalSpacing - ), - valueLabel.centerYAnchor.constraint(equalTo: centerYAnchor), - valueLabel.topAnchor.constraint(greaterThanOrEqualTo: topAnchor), - valueLabel.trailingAnchor.constraint(equalTo: trailingAnchor) - ] - NSLayoutConstraint.activate(constraints) - isAccessibilityElement = true - } - - private func configureWithCurrentState(animated: Bool) { - guard let viewModel, let style else { - return - } - let currentStyle = currentStyle(style: style, viewModel: viewModel, isHighlighted: isHighlighted) - let previousAttributedText = valueLabel.attributedText - let attributedText = AttributedStringBuilder() - .with { builder in - builder.typography = currentStyle.value.typography - builder.textStyle = .body - builder.color = currentStyle.value.color - builder.alignment = .natural - builder.text = .plain(viewModel.value) - } - .build() - valueLabel.attributedText = attributedText - UIView.perform(withAnimation: animated, duration: Constants.animationDuration) { [self] in - if animated, valueLabel.attributedText != previousAttributedText { - valueLabel.addTransitionAnimation() - } - knobView.configure(style: currentStyle.knob, animated: animated) - // Ensures that knob and label's first line are verticaly center aligned. - knobViewCenterYConstraint.constant = attributedText.size().height / 2 - } - if viewModel.isSelected { - accessibilityTraits = [.button, .selected] - } else { - accessibilityTraits = [.button] - } - accessibilityLabel = viewModel.value - } - - private func currentStyle( - style: PORadioButtonStyle, viewModel: RadioButtonViewModel, isHighlighted: Bool - ) -> PORadioButtonStateStyle { - if viewModel.isSelected { - return style.selected - } - if isHighlighted { - return style.highlighted - } - if viewModel.isInError { - return style.error - } - return style.normal - } -} diff --git a/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Views/RadioButton/RadioButtonKnobView.swift b/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Views/RadioButton/RadioButtonKnobView.swift deleted file mode 100644 index 3ba377b40..000000000 --- a/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Views/RadioButton/RadioButtonKnobView.swift +++ /dev/null @@ -1,83 +0,0 @@ -// -// RadioButtonKnobView.swift -// ProcessOut -// -// Created by Andrii Vysotskyi on 08.06.2023. -// - -import UIKit - -final class RadioButtonKnobView: UIView { - - init(size: CGFloat) { - self.size = size - super.init(frame: .zero) - commonInit() - } - - @available(*, unavailable) - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { - super.traitCollectionDidChange(previousTraitCollection) - if traitCollection.isColorAppearanceDifferent(to: previousTraitCollection), let style { - configureBorder(style: style.border) - } - } - - func configure(style: PORadioButtonKnobStateStyle, animated: Bool) { - UIView.perform(withAnimation: animated, duration: Constants.animationDuration) { [self] in - circleView.layer.cornerRadius = style.innerCircleRadius - circleViewWidthConstraint.constant = style.innerCircleRadius * 2 - backgroundColor = style.backgroundColor - circleView.backgroundColor = style.innerCircleColor - configureBorder(style: style.border) - } - self.style = style - } - - // MARK: - Private Nested Types - - private enum Constants { - static let animationDuration: TimeInterval = 0.25 - } - - // MARK: - Private Properties - - private let size: CGFloat - - private lazy var circleView: UIView = { - let view = UIView() - view.translatesAutoresizingMaskIntoConstraints = false - view.clipsToBounds = true - return view - }() - - private lazy var circleViewWidthConstraint = circleView.widthAnchor.constraint(equalToConstant: 0) - private var style: PORadioButtonKnobStateStyle? - - // MARK: - Private Methods - - private func commonInit() { - clipsToBounds = true - isUserInteractionEnabled = false - translatesAutoresizingMaskIntoConstraints = false - addSubview(circleView) - let constraints = [ - circleViewWidthConstraint, - circleView.heightAnchor.constraint(equalTo: circleView.widthAnchor), - circleView.centerXAnchor.constraint(equalTo: centerXAnchor), - circleView.centerYAnchor.constraint(equalTo: centerYAnchor), - widthAnchor.constraint(equalToConstant: size), - heightAnchor.constraint(equalTo: widthAnchor) - ] - NSLayoutConstraint.activate(constraints) - } - - private func configureBorder(style: POBorderStyle) { - apply(style: style) - layer.cornerRadius = size / 2 // Knob shape should be always circular - } -} diff --git a/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Views/RadioButton/RadioButtonViewModel.swift b/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Views/RadioButton/RadioButtonViewModel.swift deleted file mode 100644 index 237244221..000000000 --- a/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Views/RadioButton/RadioButtonViewModel.swift +++ /dev/null @@ -1,18 +0,0 @@ -// -// RadioButtonViewModel.swift -// ProcessOut -// -// Created by Andrii Vysotskyi on 06.06.2023. -// - -struct RadioButtonViewModel { - - /// Indicates whether button is currently selected. - let isSelected: Bool - - /// Indicates whether button is currently in error. - let isInError: Bool - - /// Radio button value. - let value: String -} diff --git a/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Views/TextField/TextFieldContainerView.swift b/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Views/TextField/TextFieldContainerView.swift deleted file mode 100644 index 4b791d239..000000000 --- a/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Views/TextField/TextFieldContainerView.swift +++ /dev/null @@ -1,126 +0,0 @@ -// -// TextFieldContainerView.swift -// ProcessOut -// -// Created by Andrii Vysotskyi on 25.11.2022. -// - -import UIKit - -final class TextFieldContainerView: UIView { - - init() { - isInvalid = false - super.init(frame: .zero) - commonInit() - } - - @available(*, unavailable) - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { - super.traitCollectionDidChange(previousTraitCollection) - if let style, traitCollection.isColorAppearanceDifferent(to: previousTraitCollection) { - let stateStyle = isInvalid ? style.error : style.normal - layer.borderColor = stateStyle.border.color.cgColor - layer.shadowColor = stateStyle.shadow.color.cgColor - } - } - - // MARK: - TextFieldContainerView - - private(set) var isInvalid: Bool - - private(set) lazy var textField: UITextField = { - let textField = TextField() - textField.translatesAutoresizingMaskIntoConstraints = false - textField.borderStyle = .none - textField.adjustsFontForContentSizeCategory = false - return textField - }() - - func configure(isInvalid: Bool, style: POInputStyle, animated: Bool) { - self.style = style - self.isInvalid = isInvalid - configureWithCurrentState(animated: animated) - } - - // MARK: - Private Nested Types - - private enum Constants { - static let animationDuration: TimeInterval = 0.35 - static let maximumFontSize: CGFloat = 22 - static let height: CGFloat = 44 - static let horizontalInset: CGFloat = 12 - } - - // MARK: - Private Properties - - private var style: POInputStyle? - private var placeholderObservation: NSKeyValueObservation? - - // MARK: - Private Methods - - private func commonInit() { - translatesAutoresizingMaskIntoConstraints = false - addSubview(textField) - let constraints = [ - heightAnchor.constraint(equalToConstant: Constants.height), - textField.leadingAnchor.constraint(equalTo: leadingAnchor, constant: Constants.horizontalInset), - textField.centerXAnchor.constraint(equalTo: centerXAnchor), - textField.centerYAnchor.constraint(equalTo: centerYAnchor) - ] - NSLayoutConstraint.activate(constraints) - placeholderObservation = textField.observe(\.placeholder, options: .old) { [weak self] textField, value in - if textField.placeholder != value.oldValue { - self?.configureWithCurrentState(animated: false) - } - } - } - - private func configureWithCurrentState(animated: Bool) { - guard let style else { - return - } - let stateStyle = isInvalid ? style.error : style.normal - UIView.perform(withAnimation: animated, duration: Constants.animationDuration) { [self] in - let excludedTextAttributes: Set = [.paragraphStyle, .baselineOffset] - let textAttributes = AttributedStringBuilder() - .with { builder in - builder.typography = stateStyle.text.typography - builder.textStyle = .body - builder.maximumFontSize = Constants.maximumFontSize - builder.color = stateStyle.text.color - } - .buildAttributes() - .filter { !excludedTextAttributes.contains($0.key) } - textField.defaultTextAttributes = textAttributes - // `defaultTextAttributes` overwrites placeholder attributes so `attributedPlaceholder` must be set after. - textField.attributedPlaceholder = AttributedStringBuilder() - .with { builder in - builder.typography = stateStyle.placeholder.typography - builder.textStyle = .body - builder.maximumFontSize = Constants.maximumFontSize - builder.color = stateStyle.placeholder.color - builder.text = .plain(textField.placeholder ?? "") - } - .build() - apply(style: stateStyle.border) - apply(style: stateStyle.shadow) - tintColor = stateStyle.tintColor - backgroundColor = stateStyle.backgroundColor - UIView.performWithoutAnimation(layoutIfNeeded) - } - } -} - -private final class TextField: UITextField { - - override func clearButtonRect(forBounds bounds: CGRect) -> CGRect { - var rect = super.clearButtonRect(forBounds: bounds) - rect.origin.x = bounds.width - rect.width - return rect - } -} diff --git a/Sources/ProcessOut/Sources/UI/Shared/Extensions/Array+NSAttributedString.swift b/Sources/ProcessOut/Sources/UI/Shared/Extensions/Array+NSAttributedString.swift deleted file mode 100644 index a7bb85568..000000000 --- a/Sources/ProcessOut/Sources/UI/Shared/Extensions/Array+NSAttributedString.swift +++ /dev/null @@ -1,25 +0,0 @@ -// -// Array+NSAttributedString.swift -// ProcessOut -// -// Created by Andrii Vysotskyi on 19.06.2023. -// - -import Foundation - -extension Array where Element == NSAttributedString { - - /// Returns a new attributed string by concatenating the elements of the sequence, - /// adding the given separator between each element. - func joined(separator: NSAttributedString = .init()) -> NSAttributedString { - let mutableAttributedString = NSMutableAttributedString() - dropLast().forEach { element in - mutableAttributedString.append(element) - mutableAttributedString.append(separator) - } - if let last { - mutableAttributedString.append(last) - } - return mutableAttributedString - } -} diff --git a/Sources/ProcessOut/Sources/UI/Shared/Extensions/NSLayoutConstraint+Extensions.swift b/Sources/ProcessOut/Sources/UI/Shared/Extensions/NSLayoutConstraint+Extensions.swift deleted file mode 100644 index 248a45af0..000000000 --- a/Sources/ProcessOut/Sources/UI/Shared/Extensions/NSLayoutConstraint+Extensions.swift +++ /dev/null @@ -1,16 +0,0 @@ -// -// NSLayoutConstraint+Extensions.swift -// ProcessOut -// -// Created by Andrii Vysotskyi on 05.12.2022. -// - -import UIKit - -extension NSLayoutConstraint { - - func with(priority: UILayoutPriority) -> NSLayoutConstraint { - self.priority = priority - return self - } -} diff --git a/Sources/ProcessOut/Sources/UI/Shared/Extensions/UICollectionView+Extensions.swift b/Sources/ProcessOut/Sources/UI/Shared/Extensions/UICollectionView+Extensions.swift deleted file mode 100644 index a04ed5d75..000000000 --- a/Sources/ProcessOut/Sources/UI/Shared/Extensions/UICollectionView+Extensions.swift +++ /dev/null @@ -1,39 +0,0 @@ -// -// UICollectionView+Extensions.swift -// ProcessOut -// -// Created by Andrii Vysotskyi on 25.04.2023. -// - -import UIKit - -extension UICollectionView { - - func registerCell(_ cellClass: Cell.Type) { - register(cellClass, forCellWithReuseIdentifier: Cell.reuseIdentifier) - } - - func dequeueReusableCell(_ cellClass: Cell.Type, for indexPath: IndexPath) -> Cell { - // swiftlint:disable:next force_cast - dequeueReusableCell(withReuseIdentifier: Cell.reuseIdentifier, for: indexPath) as! Cell - } - - func registerSupplementaryView(_ viewClass: View.Type, kind: String) { - register(viewClass, forSupplementaryViewOfKind: kind, withReuseIdentifier: View.reuseIdentifier) - } - - func dequeueReusableSupplementaryView( - _ viewClass: View.Type, kind: String, indexPath: IndexPath - ) -> View { - dequeueReusableSupplementaryView( - ofKind: kind, withReuseIdentifier: View.reuseIdentifier, for: indexPath - ) as! View // swiftlint:disable:this force_cast - } -} - -extension UICollectionReusableView: Reusable { - - static var reuseIdentifier: String { - String(describing: Self.self) - } -} diff --git a/Sources/ProcessOut/Sources/UI/Shared/Extensions/UIImageView+Extensions.swift b/Sources/ProcessOut/Sources/UI/Shared/Extensions/UIImageView+Extensions.swift deleted file mode 100644 index 75a6fc048..000000000 --- a/Sources/ProcessOut/Sources/UI/Shared/Extensions/UIImageView+Extensions.swift +++ /dev/null @@ -1,34 +0,0 @@ -// -// UIImageView+Extensions.swift -// ProcessOut -// -// Created by Andrii Vysotskyi on 19.12.2022. -// - -import UIKit - -extension UIImageView { - - func setAspectRatio(_ ratio: CGFloat? = nil) { - widthConstraint?.isActive = false - guard let ratio else { - return - } - let constraint = widthAnchor.constraint(equalTo: heightAnchor, multiplier: ratio) - widthConstraint = constraint - constraint.isActive = true - } - - // MARK: - Private Nested Types - - private enum AssociatedKeys { - static var widthConstraint: UInt8 = 0 - } - - // MARK: - Private Properties - - private var widthConstraint: NSLayoutConstraint? { - get { objc_getAssociatedObject(self, &AssociatedKeys.widthConstraint) as? NSLayoutConstraint } - set { objc_setAssociatedObject(self, &AssociatedKeys.widthConstraint, newValue, .OBJC_ASSOCIATION_ASSIGN) } - } -} diff --git a/Sources/ProcessOut/Sources/UI/Shared/Extensions/UITraitCollection+ColorAppearance.swift b/Sources/ProcessOut/Sources/UI/Shared/Extensions/UITraitCollection+ColorAppearance.swift deleted file mode 100644 index 8488f3b89..000000000 --- a/Sources/ProcessOut/Sources/UI/Shared/Extensions/UITraitCollection+ColorAppearance.swift +++ /dev/null @@ -1,22 +0,0 @@ -// -// UITraitCollection+ColorAppearance.swift -// ProcessOut -// -// Created by Andrii Vysotskyi on 12.12.2022. -// - -import UIKit - -extension UITraitCollection { - - // Return whether this trait collection, compared to a different trait collection, could show a different - // appearance for dynamic colors that are provided by UIKit or are in an asset catalog. - func isColorAppearanceDifferent(to traitCollection: UITraitCollection?) -> Bool { - let isDifferent = displayGamut != traitCollection?.displayGamut - || userInterfaceIdiom != traitCollection?.userInterfaceIdiom - || userInterfaceStyle != traitCollection?.userInterfaceStyle - || accessibilityContrast != traitCollection?.accessibilityContrast - || userInterfaceLevel != traitCollection?.userInterfaceLevel - return isDifferent - } -} diff --git a/Sources/ProcessOut/Sources/UI/Shared/Extensions/UIView+Extensions.swift b/Sources/ProcessOut/Sources/UI/Shared/Extensions/UIView+Extensions.swift deleted file mode 100644 index 7157ed74e..000000000 --- a/Sources/ProcessOut/Sources/UI/Shared/Extensions/UIView+Extensions.swift +++ /dev/null @@ -1,49 +0,0 @@ -// -// UIView+Extensions.swift -// ProcessOut -// -// Created by Andrii Vysotskyi on 05.12.2022. -// - -import UIKit - -extension UIView { - - /// If `withAnimation` is set to `true` runs given actions inside UIKit animation block otherwise performs - /// without animation. - static func perform(withAnimation animated: Bool, duration: TimeInterval, actions: @escaping () -> Void) { - if animated { - UIView.animate(withDuration: duration, animations: actions) - } else { - UIView.performWithoutAnimation(actions) - } - } - - /// Setting the value of this property to true hides the receiver and setting it to false shows - /// the receiver. The default value is false. - /// - /// - Warning: UIKit has a known bug when changing `isHidden` on a subview of - /// UIStackView does not always work. It seems to be caused by fact that `isHidden` - /// is cumulative in `UIStackView`, so we have to ensure to not set it the same value - /// twice http://www.openradar.me/25087688 - func setHidden(_ isHidden: Bool) { - if isHidden != self.isHidden { - self.isHidden = isHidden - } - } - - /// Adds transition animation to receiver's layer. Method must be called inside animation block - /// to make sure that timing properties are properly set. - func addTransitionAnimation(type: CATransitionType = .fade, subtype: CATransitionSubtype? = nil) { - let transition = CATransition() - transition.type = type - transition.subtype = subtype - if let animation = layer.action(forKey: "backgroundColor") as? CAAnimation { - transition.duration = animation.duration - transition.timingFunction = animation.timingFunction - } else { - return - } - layer.add(transition, forKey: "transition") - } -} diff --git a/Sources/ProcessOut/Sources/UI/Shared/Utils/CollectionDiffableDataSource/CollectionViewDiffableDataSource.swift b/Sources/ProcessOut/Sources/UI/Shared/Utils/CollectionDiffableDataSource/CollectionViewDiffableDataSource.swift deleted file mode 100644 index 23ba63033..000000000 --- a/Sources/ProcessOut/Sources/UI/Shared/Utils/CollectionDiffableDataSource/CollectionViewDiffableDataSource.swift +++ /dev/null @@ -1,253 +0,0 @@ -// -// CollectionViewDiffableDataSource.swift -// ProcessOut -// -// Created by Andrii Vysotskyi on 30.04.2023. -// - -import UIKit - -/// Simplified backport of UICollectionViewDiffableDataSource that is available for older iOS versions. -final class CollectionViewDiffableDataSource: - NSObject, UICollectionViewDataSource { - - /// A closure that configures and returns a cell for a collection view from its diffable data source. - typealias CellProvider = (UICollectionView, IndexPath, ItemIdentifier) -> UICollectionViewCell? - - /// A closure that configures and returns a collection view’s supplementary view, such as a header or footer, - /// from a data source. - typealias SupplementaryViewProvider = - (UICollectionView, _ elementKind: String, IndexPath) -> UICollectionReusableView? - - /// The closure that configures and returns the collection view’s supplementary views, such as headers and - /// footers, from the data source. - var supplementaryViewProvider: SupplementaryViewProvider? - - init(collectionView: UICollectionView, cellProvider: @escaping CellProvider) { - self.collectionView = collectionView - self.cellProvider = cellProvider - currentSnapshot = .init() - super.init() - collectionView.dataSource = self - } - - /// Updates the UI to reflect the state of the data in the snapshot, optionally animating the UI changes. - func apply( - _ snapshot: DiffableDataSourceSnapshot, - animatingDifferences: Bool = true, - completion: (() -> Void)? = nil - ) { - let performUpdates = { - self.collectionView.performBatchUpdates { - self.update(with: snapshot) - } completion: { _ in - completion?() - } - } - if animatingDifferences { - performUpdates() - } else { - UIView.performWithoutAnimation(performUpdates) - } - } - - func applySnapshotUsingReloadData(_ snapshot: DiffableDataSourceSnapshot) { - currentSnapshot = snapshot - collectionView.reloadData() - } - - /// Returns an identifier for the section at the index you specify in the collection view. - func sectionIdentifier(for index: Int) -> SectionIdentifier? { - currentSnapshot.sectionIdentifier(for: index) - } - - /// Returns an identifier for the item at the specified index path in the collection view. - /// - /// This method is a constant time operation, O(1), which means you can look up an item identifier from its - /// corresponding index path with no significant overhead. - func itemIdentifier(for indexPath: IndexPath) -> ItemIdentifier? { - guard let identifier = currentSnapshot.sectionIdentifier(for: indexPath.section) else { - return nil - } - let itemIdentifiers = currentSnapshot.itemIdentifiers(inSection: identifier) - guard itemIdentifiers.indices.contains(indexPath.row) else { - return nil - } - return itemIdentifiers[indexPath.row] - } - - func snapshot() -> DiffableDataSourceSnapshot { - currentSnapshot - } - - // MARK: - UICollectionViewDataSource - - func numberOfSections(in collectionView: UICollectionView) -> Int { - currentSnapshot.numberOfSections - } - - func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { - if let identifier = currentSnapshot.sectionIdentifier(for: section) { - return currentSnapshot.numberOfItems(inSection: identifier) - } - return 0 - } - - func collectionView( - _ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath - ) -> UICollectionViewCell { - guard let itemIdentifier = itemIdentifier(for: indexPath), - let cell = cellProvider(collectionView, indexPath, itemIdentifier) else { - assertionFailure("Either index path is invalid or there is not cell to return.") - return UICollectionViewCell() - } - return cell - } - - func collectionView( - _ collectionView: UICollectionView, - viewForSupplementaryElementOfKind kind: String, - at indexPath: IndexPath - ) -> UICollectionReusableView { - guard let view = supplementaryViewProvider?(collectionView, kind, indexPath) else { - assertionFailure("No supplementary view to return.") - return UICollectionReusableView() - } - return view - } - - // MARK: - Private Nested Types - - /// Alias to DataSourceSnapshot. - private typealias Snapshot = DiffableDataSourceSnapshot - - private struct Updates { - - // swiftlint:disable:next nesting - struct Move { - let from, to: Index // swiftlint:disable:this identifier_name - } - - /// Removals in descending order. - let removals: [Index] - - /// Reload operations. - let reloads: [Index] - - /// Insertions in ascending order. - let insertions: [Index] - - /// Moves. - let moves: [Move] - } - - private struct Element { - - /// Item identifier. - let identifier: Identifier - - /// Item index. - let index: Index - } - - // MARK: - Private Properties - - private let collectionView: UICollectionView - private let cellProvider: CellProvider - private var currentSnapshot: Snapshot - - // MARK: - Private Methods - - private func update(with snapshot: Snapshot) { - let sectionUpdates = updateSections(with: snapshot) - let itemUpdates = updates( - from: itemElements( - snapshot: currentSnapshot, excludedSections: IndexSet(sectionUpdates.removals) - ), - to: itemElements(snapshot: snapshot), - reloadedIdentifiers: [] - ) - collectionView.deleteItems(at: itemUpdates.removals) - collectionView.insertItems(at: itemUpdates.insertions) - for move in itemUpdates.moves { - collectionView.moveItem(at: move.from, to: move.to) - } - self.currentSnapshot = snapshot - } - - private func updateSections(with snapshot: Snapshot) -> Updates { - let sectionUpdates = updates( - from: sectionElements(snapshot: currentSnapshot), - to: sectionElements(snapshot: snapshot), - reloadedIdentifiers: snapshot.reloadedSectionIdentifiers - ) - collectionView.deleteSections(IndexSet(sectionUpdates.removals)) - collectionView.reloadSections(IndexSet(sectionUpdates.reloads)) - collectionView.insertSections(IndexSet(sectionUpdates.insertions)) - for move in sectionUpdates.moves { - collectionView.moveSection(move.from, toSection: move.to) - } - return sectionUpdates - } - - /// Transforms section identifiers into section/index pairs suitable for calculating difference. - private func sectionElements(snapshot: Snapshot) -> [Element] { - snapshot.sectionIdentifiers.enumerated().map { Element(identifier: $1, index: $0) } - } - - /// Transforms item identifiers into item/indexPath pairs suitable for calculating difference. - private func itemElements( - snapshot: Snapshot, excludedSections: IndexSet = [] - ) -> [Element] { - var itemElements: [Element] = [] - for (section, sectionIdentifier) in snapshot.sectionIdentifiers.enumerated() { - guard !excludedSections.contains(section) else { - continue - } - let elements = snapshot.itemIdentifiers(inSection: sectionIdentifier).enumerated().map { offset, element in - let indexPath = IndexPath(row: offset, section: section) - return Element(identifier: element, index: indexPath) - } - itemElements.append(contentsOf: elements) - } - return itemElements - } - - /// Calculates needed updates to apply to collection view to transform initial elements into final. - private func updates( - from initial: [Element], - to final: [Element], - reloadedIdentifiers: Set - ) -> Updates { - var finalIndices = [Identifier: Index](minimumCapacity: final.count) - for element in final { - finalIndices[element.identifier] = element.index - } - var removals: [Index] = [] - var reloads: [Index] = [] - var initialIndices = [Identifier: Index](minimumCapacity: initial.count) - for element in initial.reversed() { - if finalIndices.keys.contains(element.identifier) { - if reloadedIdentifiers.contains(element.identifier) { - reloads.append(element.index) - } - // Store element index to it can be used later used to deduce move. - initialIndices[element.identifier] = element.index - } else { - removals.append(element.index) - } - } - var moves: [Updates.Move] = [] - var insertions: [Index] = [] - for element in final { - if let initialIndex = initialIndices[element.identifier] { - if initialIndex != element.index { - moves.append(Updates.Move(from: initialIndex, to: element.index)) - } - } else { - insertions.append(element.index) - } - } - return Updates(removals: removals, reloads: reloads, insertions: insertions, moves: moves) - } -} diff --git a/Sources/ProcessOut/Sources/UI/Shared/Utils/CollectionDiffableDataSource/DiffableDataSourceSnapshot.swift b/Sources/ProcessOut/Sources/UI/Shared/Utils/CollectionDiffableDataSource/DiffableDataSourceSnapshot.swift deleted file mode 100644 index a36b4aa20..000000000 --- a/Sources/ProcessOut/Sources/UI/Shared/Utils/CollectionDiffableDataSource/DiffableDataSourceSnapshot.swift +++ /dev/null @@ -1,53 +0,0 @@ -// -// DiffableDataSourceSnapshot.swift -// ProcessOut -// -// Created by Andrii Vysotskyi on 30.04.2023. -// - -struct DiffableDataSourceSnapshot { - - private(set) var sectionIdentifiers: [SectionIdentifier] = [] - - mutating func appendSections(_ identifiers: [SectionIdentifier]) { - sectionIdentifiers += identifiers - } - - mutating func appendItems(_ identifiers: [ItemIdentifier], toSection sectionIdentifier: SectionIdentifier) { - assert(sectionIdentifiers.contains(sectionIdentifier)) - if let itemIdentifiers = self._itemIdentifiers[sectionIdentifier] { - self._itemIdentifiers[sectionIdentifier] = itemIdentifiers + identifiers - } else { - self._itemIdentifiers[sectionIdentifier] = identifiers - } - } - - private(set) var reloadedSectionIdentifiers: Set = [] - - mutating func reloadSections(_ identifiers: [SectionIdentifier]) { - reloadedSectionIdentifiers.formUnion(identifiers) - } - - var numberOfSections: Int { - sectionIdentifiers.count - } - - func numberOfItems(inSection identifier: SectionIdentifier) -> Int { - _itemIdentifiers[identifier]?.count ?? 0 - } - - func sectionIdentifier(for index: Int) -> SectionIdentifier? { - if sectionIdentifiers.indices.contains(index) { - return sectionIdentifiers[index] - } - return nil - } - - func itemIdentifiers(inSection identifier: SectionIdentifier) -> [ItemIdentifier] { - _itemIdentifiers[identifier] ?? [] - } - - // MARK: - - - private var _itemIdentifiers: [SectionIdentifier: [ItemIdentifier]] = [:] -} diff --git a/Sources/ProcessOut/Sources/UI/Shared/Utils/CollectionReusableViewSizeProvider.swift b/Sources/ProcessOut/Sources/UI/Shared/Utils/CollectionReusableViewSizeProvider.swift deleted file mode 100644 index ca5297700..000000000 --- a/Sources/ProcessOut/Sources/UI/Shared/Utils/CollectionReusableViewSizeProvider.swift +++ /dev/null @@ -1,51 +0,0 @@ -// -// CollectionReusableViewSizeProvider.swift -// ProcessOut -// -// Created by Andrii Vysotskyi on 25.04.2023. -// - -import UIKit - -final class CollectionReusableViewSizeProvider { - - init() { - templateViews = [:] - } - - func systemLayoutSize( - viewType: View.Type, preferredWidth: CGFloat, configure: ((View) -> Void)? = nil - ) -> CGSize { - let view = view(viewType) - view.prepareForReuse() - configure?(view) - view.frame.size.width = preferredWidth - view.setNeedsLayout() - view.layoutIfNeeded() - var contentView: UIView = view - if let cell = view as? UICollectionViewCell { - contentView = cell.contentView - } - let targetSize = CGSize(width: preferredWidth, height: UIView.layoutFittingCompressedSize.height) - let size = contentView.systemLayoutSizeFitting( - targetSize, withHorizontalFittingPriority: .required, verticalFittingPriority: .fittingSizeLevel - ) - return CGSize(width: preferredWidth, height: size.height) - } - - // MARK: - Private Properties - - private var templateViews: [ObjectIdentifier: UICollectionReusableView] - - // MARK: - Private Methods - - private func view(_ viewType: View.Type) -> View { - let identifier = ObjectIdentifier(viewType) - if let view = templateViews[identifier] as? View { - return view - } - let view = View() - templateViews[identifier] = view - return view - } -} diff --git a/Sources/ProcessOut/Sources/UI/Shared/Utils/KeyboardNotification.swift b/Sources/ProcessOut/Sources/UI/Shared/Utils/KeyboardNotification.swift deleted file mode 100644 index 52169fd92..000000000 --- a/Sources/ProcessOut/Sources/UI/Shared/Utils/KeyboardNotification.swift +++ /dev/null @@ -1,31 +0,0 @@ -// -// KeyboardNotification.swift -// ProcessOut -// -// Created by Andrii Vysotskyi on 07.05.2023. -// - -import UIKit - -struct KeyboardNotification { - - /// Keyboard’s frame at the end of its animation. - let frameEnd: CGRect - - /// Animation duration. - let animationDuration: TimeInterval? - - /// Animation curve that the system uses to animate the keyboard onto or off the screen. - let animationCurve: UIView.AnimationCurve? - - /// Extracts keyboard information from given notification. - init?(notification: Notification) { - guard let frameEnd = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect else { - return nil - } - self.frameEnd = frameEnd - animationDuration = notification.userInfo?[UIResponder.keyboardAnimationDurationUserInfoKey] as? TimeInterval - let rawCurve = notification.userInfo?[UIResponder.keyboardAnimationCurveUserInfoKey] as? Int - animationCurve = rawCurve.flatMap(UIView.AnimationCurve.init) - } -} diff --git a/Sources/ProcessOut/Sources/UI/Shared/Utils/PassthroughView.swift b/Sources/ProcessOut/Sources/UI/Shared/Utils/PassthroughView.swift deleted file mode 100644 index a57201b6d..000000000 --- a/Sources/ProcessOut/Sources/UI/Shared/Utils/PassthroughView.swift +++ /dev/null @@ -1,19 +0,0 @@ -// -// PassthroughView.swift -// ProcessOut -// -// Created by Andrii Vysotskyi on 03.05.2023. -// - -import UIKit - -final class PassthroughView: UIView { - - override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { - let view = super.hitTest(point, with: event) - guard view !== self else { - return nil - } - return view - } -} diff --git a/Sources/ProcessOut/Sources/UI/Shared/Utils/Reusable.swift b/Sources/ProcessOut/Sources/UI/Shared/Utils/Reusable.swift deleted file mode 100644 index 2e3a91710..000000000 --- a/Sources/ProcessOut/Sources/UI/Shared/Utils/Reusable.swift +++ /dev/null @@ -1,12 +0,0 @@ -// -// Reusable.swift -// ProcessOut -// -// Created by Andrii Vysotskyi on 27.04.2023. -// - -protocol Reusable: AnyObject { - - /// Reuse identifier. - static var reuseIdentifier: String { get } -} diff --git a/Sources/ProcessOut/Sources/UI/Shared/Utils/TextFieldUtils.swift b/Sources/ProcessOut/Sources/UI/Shared/Utils/TextFieldUtils.swift deleted file mode 100644 index 434a18428..000000000 --- a/Sources/ProcessOut/Sources/UI/Shared/Utils/TextFieldUtils.swift +++ /dev/null @@ -1,42 +0,0 @@ -// -// TextFieldUtils.swift -// ProcessOut -// -// Created by Andrii Vysotskyi on 07.08.2023. -// - -import Foundation -import UIKit - -enum TextFieldUtils { - - static func changeText( - in range: NSRange, replacement string: String, textField: UITextField, formatter: Formatter - ) -> Bool { - // swiftlint:disable legacy_objc_type - let originalString = (textField.text ?? "") as NSString - var updatedString = originalString.replacingCharacters(in: range, with: string) as NSString - // swiftlint:enable legacy_objc_type - var proposedSelectedRange = NSRange(location: updatedString.length, length: 0) - let isReplacementValid = formatter.isPartialStringValid( - &updatedString, - proposedSelectedRange: &proposedSelectedRange, - originalString: originalString as String, - originalSelectedRange: range, - errorDescription: nil - ) - guard isReplacementValid else { - return false - } - textField.text = updatedString as String - // swiftlint:disable:next line_length - if let position = textField.position(from: textField.beginningOfDocument, offset: proposedSelectedRange.lowerBound) { - // fixme(andrii-vysotskyi): when called as a result of paste system changes our selection to wrong value - // based on length of `replacementString` after call textField(:shouldChangeCharactersIn:replacementString:) - // returns, even if this method returns false. - textField.selectedTextRange = textField.textRange(from: position, to: position) - } - textField.sendActions(for: .editingChanged) - return false - } -} diff --git a/Sources/ProcessOutCheckout3DS/ProcessOutCheckout3DS.docc/MigrationGuides.md b/Sources/ProcessOutCheckout3DS/ProcessOutCheckout3DS.docc/MigrationGuides.md index 0b8f5bc3b..94bc0b670 100644 --- a/Sources/ProcessOutCheckout3DS/ProcessOutCheckout3DS.docc/MigrationGuides.md +++ b/Sources/ProcessOutCheckout3DS/ProcessOutCheckout3DS.docc/MigrationGuides.md @@ -1,5 +1,29 @@ # Migration Guides +## Migrating from versions < 5.0.0 + +- `POCheckout3DSServiceBuilder` was removed, instead use ``POCheckout3DSService/init(delegate:environment:)`` to create +service. Delegate could be passed during initialization or injected later. + +- Delegate was migrated to structured concurrency, additionally naming was updated: + + - Method `func configuration(with:)` where delegate was asked to provide service configuration is no longer available. +It was replaced with ``POCheckout3DSServiceDelegate/checkout3DSService(_:willCreateAuthenticationRequestParametersWith:)``. +Passed `configuration` is an inout argument that you could modify to alter styling and behaviour of underlying 3DS +service. + + - Method `func shouldContinue(with warnings:completion:)` became +``POCheckout3DSServiceDelegate/checkout3DSService(_:shouldContinueWith:)``. + + - Method `func didCreateAuthenticationRequest(result:)` was replaced with +``POCheckout3DSServiceDelegate/checkout3DSService(_:didCreateAuthenticationRequestParameters:)``. + + - Method `func willHandle(challenge:)` was replaced with ``POCheckout3DSServiceDelegate/checkout3DSService(_:willPerformChallengeWith:)``. + + - Method `func didHandle3DS2Challenge(result:)` was replaced with ``POCheckout3DSServiceDelegate/checkout3DSService(_:didPerformChallenge:)``. + +- `POCheckout3DSService` now holds a weak reference to its delegate. + ## Migrating from versions < 4.0.0 - ``POCheckout3DSServiceBuilder`` should be created via init and configured with instance methods. Static factory diff --git a/Sources/ProcessOutCheckout3DS/ProcessOutCheckout3DS.docc/ProcessOutCheckout3DS.md b/Sources/ProcessOutCheckout3DS/ProcessOutCheckout3DS.docc/ProcessOutCheckout3DS.md index 18749f020..e0289251d 100644 --- a/Sources/ProcessOutCheckout3DS/ProcessOutCheckout3DS.docc/ProcessOutCheckout3DS.md +++ b/Sources/ProcessOutCheckout3DS/ProcessOutCheckout3DS.docc/ProcessOutCheckout3DS.md @@ -3,12 +3,13 @@ ## Overview Framework wraps Checkout SDK to make it easy to use with ProcessOut when making requests that may trigger 3DS2. See -``POCheckout3DSServiceBuilder`` for more details. +``POCheckout3DSService`` for more details. ## Topics ### 3DS -- ``POCheckout3DSServiceBuilder`` -- ``POCheckout3DSServiceDelegate`` - +- ``POCheckout3DSService`` +- ``POCheckout3DSServiceDelegate`` + diff --git a/Sources/ProcessOutCheckout3DS/Sources/Builder/POCheckout3DSServiceBuilder.swift b/Sources/ProcessOutCheckout3DS/Sources/Builder/POCheckout3DSServiceBuilder.swift deleted file mode 100644 index 06712ca72..000000000 --- a/Sources/ProcessOutCheckout3DS/Sources/Builder/POCheckout3DSServiceBuilder.swift +++ /dev/null @@ -1,56 +0,0 @@ -// -// POCheckout3DSServiceBuilder.swift -// ProcessOutCheckout3DS -// -// Created by Andrii Vysotskyi on 01.03.2023. -// - -import Foundation -import ProcessOut -import Checkout3DS - -/// Builder to configure and create service capable of handling 3DS challenges using Checkout3DS SDK. -public final class POCheckout3DSServiceBuilder { - - /// - NOTE: Delegate will be strongly referenced by created service. - @available(*, deprecated, message: "Use non static method instead.") - public static func with(delegate: POCheckout3DSServiceDelegate) -> POCheckout3DSServiceBuilder { - POCheckout3DSServiceBuilder().with(delegate: delegate) - } - - /// Creates builder instance. - public init() { - environment = .production - } - - /// - NOTE: Delegate will be strongly referenced by created service. - public func with(delegate: POCheckout3DSServiceDelegate) -> Self { - self.delegate = delegate - return self - } - - /// Sets environment used to initialize `Standalone3DSService`. Default value is `production`. - public func with(environment: Checkout3DS.Environment) -> Self { - self.environment = environment - return self - } - - /// Creates service instance. - public func build() -> PO3DSService { - guard let delegate else { - preconditionFailure("Delegate must be set.") - } - let service = Checkout3DSService( - errorMapper: DefaultAuthenticationErrorMapper(), - configurationMapper: DefaultConfigurationMapper(), - delegate: delegate, - environment: environment - ) - return service - } - - // MARK: - Private Properties - - private var delegate: POCheckout3DSServiceDelegate? - private var environment: Checkout3DS.Environment -} diff --git a/Sources/ProcessOutCheckout3DS/Sources/Extensions/Checkout3DSTransaction+Async.swift b/Sources/ProcessOutCheckout3DS/Sources/Extensions/Checkout3DSTransaction+Async.swift new file mode 100644 index 000000000..c8357f327 --- /dev/null +++ b/Sources/ProcessOutCheckout3DS/Sources/Extensions/Checkout3DSTransaction+Async.swift @@ -0,0 +1,27 @@ +// +// Checkout3DSTransaction+Async.swift +// ProcessOutCheckout3DS +// +// Created by Andrii Vysotskyi on 01.08.2024. +// + +import Checkout3DS + +extension Checkout3DS.Transaction { + + /// Returns device and 3DS SDK information to the 3DS Requestor App. + func getAuthenticationRequestParameters() async throws -> AuthenticationRequestParameters { + try await withCheckedThrowingContinuation { continuation in + getAuthenticationRequestParameters(completion: continuation.resume) + } + } + + /// Initiates the challenge process. + func doChallenge( + challengeParameters: ChallengeParameters + ) async throws -> AuthenticationResult { + try await withCheckedThrowingContinuation { continuation in + doChallenge(challengeParameters: challengeParameters, completion: continuation.resume) + } + } +} diff --git a/Sources/ProcessOutCheckout3DS/Sources/Mappers/AuthenticationError/AuthenticationErrorMapper.swift b/Sources/ProcessOutCheckout3DS/Sources/Mappers/AuthenticationError/AuthenticationErrorMapper.swift index e0f45c197..b1fc5199f 100644 --- a/Sources/ProcessOutCheckout3DS/Sources/Mappers/AuthenticationError/AuthenticationErrorMapper.swift +++ b/Sources/ProcessOutCheckout3DS/Sources/Mappers/AuthenticationError/AuthenticationErrorMapper.swift @@ -8,8 +8,8 @@ import ProcessOut import Checkout3DS -protocol AuthenticationErrorMapper { +protocol AuthenticationErrorMapper: Sendable { /// Converts given authentication error to ProcessOut error. - func convert(error: Checkout3DS.AuthenticationError) -> POFailure + func convert(error: AuthenticationError) -> POFailure } diff --git a/Sources/ProcessOutCheckout3DS/Sources/Mappers/AuthenticationError/DefaultAuthenticationErrorMapper.swift b/Sources/ProcessOutCheckout3DS/Sources/Mappers/AuthenticationError/DefaultAuthenticationErrorMapper.swift index f58aff81c..08933491a 100644 --- a/Sources/ProcessOutCheckout3DS/Sources/Mappers/AuthenticationError/DefaultAuthenticationErrorMapper.swift +++ b/Sources/ProcessOutCheckout3DS/Sources/Mappers/AuthenticationError/DefaultAuthenticationErrorMapper.swift @@ -8,7 +8,7 @@ import ProcessOut import Checkout3DS -final class DefaultAuthenticationErrorMapper: AuthenticationErrorMapper { +struct DefaultAuthenticationErrorMapper: AuthenticationErrorMapper { func convert(error: AuthenticationError) -> POFailure { let code: POFailure.Code diff --git a/Sources/ProcessOutCheckout3DS/Sources/Mappers/Configuration/ConfigurationMapper.swift b/Sources/ProcessOutCheckout3DS/Sources/Mappers/Configuration/ConfigurationMapper.swift index ed30d47b0..2c390ec9d 100644 --- a/Sources/ProcessOutCheckout3DS/Sources/Mappers/Configuration/ConfigurationMapper.swift +++ b/Sources/ProcessOutCheckout3DS/Sources/Mappers/Configuration/ConfigurationMapper.swift @@ -8,7 +8,7 @@ import ProcessOut import Checkout3DS -protocol ConfigurationMapper { +protocol ConfigurationMapper: Sendable { /// Converts given ProcessOut configuration to Checkout config parameters. func convert(configuration: PO3DS2Configuration) -> ThreeDS2ServiceConfiguration.ConfigParameters diff --git a/Sources/ProcessOutCheckout3DS/Sources/Mappers/Configuration/DefaultConfigurationMapper.swift b/Sources/ProcessOutCheckout3DS/Sources/Mappers/Configuration/DefaultConfigurationMapper.swift index ed4ef74d4..df970b7cd 100644 --- a/Sources/ProcessOutCheckout3DS/Sources/Mappers/Configuration/DefaultConfigurationMapper.swift +++ b/Sources/ProcessOutCheckout3DS/Sources/Mappers/Configuration/DefaultConfigurationMapper.swift @@ -8,7 +8,7 @@ import ProcessOut import Checkout3DS -final class DefaultConfigurationMapper: ConfigurationMapper { +struct DefaultConfigurationMapper: ConfigurationMapper { func convert(configuration: PO3DS2Configuration) -> ThreeDS2ServiceConfiguration.ConfigParameters { let directoryServerData = ThreeDS2ServiceConfiguration.DirectoryServerData( @@ -19,7 +19,7 @@ final class DefaultConfigurationMapper: ConfigurationMapper { let configParameters = ThreeDS2ServiceConfiguration.ConfigParameters( directoryServerData: directoryServerData, messageVersion: configuration.messageVersion, - scheme: configuration.$scheme.typed().map(self.convert) ?? "" + scheme: configuration.scheme.map(self.convert) ?? "" ) return configParameters } diff --git a/Sources/ProcessOutCheckout3DS/Sources/Service/Checkout3DSService.swift b/Sources/ProcessOutCheckout3DS/Sources/Service/Checkout3DSService.swift deleted file mode 100644 index 309e1b8e8..000000000 --- a/Sources/ProcessOutCheckout3DS/Sources/Service/Checkout3DSService.swift +++ /dev/null @@ -1,177 +0,0 @@ -// -// Checkout3DSService.swift -// ProcessOutCheckout3DS -// -// Created by Andrii Vysotskyi on 28.02.2023. -// - -import ProcessOut -import Checkout3DS - -final class Checkout3DSService: PO3DSService { - - init( - errorMapper: AuthenticationErrorMapper, - configurationMapper: ConfigurationMapper, - delegate: POCheckout3DSServiceDelegate, - environment: Checkout3DS.Environment - ) { - self.errorMapper = errorMapper - self.configurationMapper = configurationMapper - self.delegate = delegate - self.environment = environment - queue = DispatchQueue.global() - state = .idle - } - - deinit { - clean() - } - - // MARK: - PO3DSService - - // swiftlint:disable:next function_body_length - func authenticationRequest( - configuration: PO3DS2Configuration, - completion: @escaping (Result) -> Void - ) { - delegate.willCreateAuthenticationRequest(configuration: configuration) - switch state { - case .idle, .fingerprinted: - clean() - default: - let failure = POFailure(code: .generic(.mobile)) - delegate.didCreateAuthenticationRequest(result: .failure(failure)) - completion(.failure(failure)) - return - } - let configurationParameters = configurationMapper.convert(configuration: configuration) - let configuration = delegate.configuration(with: configurationParameters) - do { - let service = try Standalone3DSService.initialize(with: configuration, environment: environment) - let context = State.Context(service: service, transaction: service.createTransaction()) - state = .fingerprinting(context) - queue.async { [unowned self, errorMapper] in - let warnings = service.getWarnings() - DispatchQueue.main.async { - self.delegate.shouldContinue(with: warnings) { shouldContinue in - assert(Thread.isMainThread, "Completion must be called on main thread.") - if shouldContinue { - context.transaction.getAuthenticationRequestParameters { [unowned self] result in - let mappedResult = result - .mapError(errorMapper.convert) - .map(self.convertToAuthenticationRequest) - switch mappedResult { - case .success: - self.state = .fingerprinted(context) - case .failure: - self.setIdleStateUnchecked() - } - self.delegate.didCreateAuthenticationRequest(result: mappedResult) - completion(mappedResult) - } - } else { - self.setIdleStateUnchecked() - let failure = POFailure(code: .cancelled) - self.delegate.didCreateAuthenticationRequest(result: .failure(failure)) - completion(.failure(failure)) - } - } - } - } - } catch let error as AuthenticationError { - let failure = errorMapper.convert(error: error) - delegate.didCreateAuthenticationRequest(result: .failure(failure)) - completion(.failure(failure)) - } catch { - let failure = POFailure(code: .generic(.mobile), underlyingError: error) - delegate.didCreateAuthenticationRequest(result: .failure(failure)) - completion(.failure(failure)) - } - } - - func handle(challenge: PO3DS2Challenge, completion: @escaping (Result) -> Void) { - delegate.willHandle(challenge: challenge) - guard case let .fingerprinted(context) = state else { - let failure = POFailure(code: .generic(.mobile)) - delegate.didHandle3DS2Challenge(result: .failure(failure)) - completion(.failure(failure)) - return - } - state = .challenging(context) - let parameters = convertToChallengeParameters(data: challenge) - context.transaction.doChallenge(challengeParameters: parameters) { [unowned self, errorMapper] result in - self.setIdleStateUnchecked() - let mappedResult = result.map(extractStatus(authenticationResult:)).mapError(errorMapper.convert) - delegate.didHandle3DS2Challenge(result: mappedResult) - completion(mappedResult) - } - } - - func handle(redirect: PO3DSRedirect, completion: @escaping (Result) -> Void) { - // Redirection is simply forwarded to delegate without additional validations. - delegate.handle(redirect: redirect, completion: completion) - } - - // MARK: - Private Nested Types - - private typealias State = Checkout3DSServiceState - - // MARK: - Private Properties - - private let errorMapper: AuthenticationErrorMapper - private let configurationMapper: ConfigurationMapper - private let queue: DispatchQueue - private let delegate: POCheckout3DSServiceDelegate - private let environment: Checkout3DS.Environment - - private var state: State - - // MARK: - Private Methods - - private func setIdleStateUnchecked() { - clean() - state = .idle - } - - private func clean() { - let currentContext: Checkout3DSServiceState.Context - switch state { - case let .fingerprinting(context), let .fingerprinted(context), let .challenging(context): - currentContext = context - default: - return - } - currentContext.transaction.close() - currentContext.service.cleanUp() - } - - // MARK: - Utils - - private func convertToAuthenticationRequest( - request: AuthenticationRequestParameters - ) -> PO3DS2AuthenticationRequest { - let authenticationRequest = PO3DS2AuthenticationRequest( - deviceData: request.deviceData, - sdkAppId: request.sdkAppID, - sdkEphemeralPublicKey: request.sdkEphemeralPublicKey, - sdkReferenceNumber: request.sdkReferenceNumber, - sdkTransactionId: request.sdkTransactionID - ) - return authenticationRequest - } - - private func convertToChallengeParameters(data: PO3DS2Challenge) -> ChallengeParameters { - let challengeParameters = ChallengeParameters( - threeDSServerTransactionID: data.threeDSServerTransactionId, - acsTransactionID: data.acsTransactionId, - acsRefNumber: data.acsReferenceNumber, - acsSignedContent: data.acsSignedContent - ) - return challengeParameters - } - - private func extractStatus(authenticationResult: AuthenticationResult) -> Bool { - authenticationResult.transactionStatus?.uppercased() == "Y" - } -} diff --git a/Sources/ProcessOutCheckout3DS/Sources/Service/Checkout3DSServiceState.swift b/Sources/ProcessOutCheckout3DS/Sources/Service/Checkout3DSServiceState.swift deleted file mode 100644 index c5f528948..000000000 --- a/Sources/ProcessOutCheckout3DS/Sources/Service/Checkout3DSServiceState.swift +++ /dev/null @@ -1,33 +0,0 @@ -// -// Checkout3DSServiceState.swift -// ProcessOutCheckout3DS -// -// Created by Andrii Vysotskyi on 01.03.2023. -// - -import Foundation -import Checkout3DS - -enum Checkout3DSServiceState { - - struct Context { - - /// Service. - let service: ThreeDS2Service - - /// Transaction. - let transaction: Transaction - } - - /// Idle state. - case idle - - /// Fingerprinting. - case fingerprinting(Context) - - /// Fingerprinting is completed and implementation is now ready for 3DS2 challenge. - case fingerprinted(Context) - - /// Challenge is currently in progress. - case challenging(Context) -} diff --git a/Sources/ProcessOutCheckout3DS/Sources/Service/POCheckout3DSService.swift b/Sources/ProcessOutCheckout3DS/Sources/Service/POCheckout3DSService.swift new file mode 100644 index 000000000..c97456f7b --- /dev/null +++ b/Sources/ProcessOutCheckout3DS/Sources/Service/POCheckout3DSService.swift @@ -0,0 +1,134 @@ +// +// POCheckout3DSService.swift +// ProcessOutCheckout3DS +// +// Created by Andrii Vysotskyi on 28.02.2023. +// + +import ProcessOut +import Checkout3DS + +/// Adapter wraps Checkout's `Standalone3DSService` service so it could be used with `ProcessOut` APIs +/// where instance of `PO3DSService` is expected. +public actor POCheckout3DSService: PO3DSService, Sendable { + + public init(delegate: POCheckout3DSServiceDelegate? = nil, environment: Environment = .production) { + errorMapper = DefaultAuthenticationErrorMapper() + configurationMapper = DefaultConfigurationMapper() + self.delegate = delegate + self.environment = environment + } + + deinit { + service?.cleanUp() + } + + /// Service's delegate. + public weak var delegate: POCheckout3DSServiceDelegate? + + // MARK: - PO3DSService + + public func authenticationRequestParameters( + configuration: PO3DS2Configuration + ) async throws -> PO3DS2AuthenticationRequestParameters { + invalidate() + do { + let service = try Standalone3DSService.initialize( + with: await serviceConfiguration(with: configuration), environment: environment + ) + self.service = service + guard await delegate?.checkout3DSService(self, shouldContinueWith: service.getWarnings()) ?? true else { + throw POFailure(code: .cancelled) + } + let authenticationRequest = authenticationRequest( + with: try await service.createTransaction().getAuthenticationRequestParameters() + ) + await delegate?.checkout3DSService(self, didCreateFingerprintWith: .success(authenticationRequest)) + return authenticationRequest + } catch { + invalidate() + let failure = failure(with: error) + await delegate?.checkout3DSService(self, didCreateFingerprintWith: .failure(failure)) + throw failure + } + } + + public func performChallenge(with parameters: PO3DS2ChallengeParameters) async throws -> PO3DS2ChallengeResult { + defer { + invalidate() + } + do { + await delegate?.checkout3DSService(self, willPerformChallengeWith: parameters) + guard let transaction = service?.createTransaction() else { + throw POFailure(code: .generic(.mobile)) + } + let authenticationResult = try await transaction.doChallenge( + challengeParameters: challengeParameters(with: parameters) + ) + let challengeResult = PO3DS2ChallengeResult( + transactionStatus: authenticationResult.transactionStatus ?? "N" + ) + await delegate?.checkout3DSService(self, didPerformChallenge: .success(challengeResult)) + return challengeResult + } catch { + let failure = failure(with: error) + await delegate?.checkout3DSService(self, didPerformChallenge: .failure(failure)) + throw failure + } + } + + // MARK: - Private Properties + + private let errorMapper: AuthenticationErrorMapper + private let configurationMapper: ConfigurationMapper + private let environment: Checkout3DS.Environment + + private var service: ThreeDS2Service? + + // MARK: - Private Methods + + private func invalidate() { + service?.cleanUp() + service = nil + } + + // MARK: - Utils + + private func serviceConfiguration(with configuration: PO3DS2Configuration) async -> ThreeDS2ServiceConfiguration { + let configParameters = configurationMapper.convert(configuration: configuration) + var serviceConfiguration = ThreeDS2ServiceConfiguration(configParameters: configParameters) + await delegate?.checkout3DSService(self, willCreateAuthenticationRequestParametersWith: &serviceConfiguration) + return serviceConfiguration + } + + private func authenticationRequest( + with request: AuthenticationRequestParameters + ) -> PO3DS2AuthenticationRequestParameters { + PO3DS2AuthenticationRequestParameters( + deviceData: request.deviceData, + sdkAppId: request.sdkAppID, + sdkEphemeralPublicKey: request.sdkEphemeralPublicKey, + sdkReferenceNumber: request.sdkReferenceNumber, + sdkTransactionId: request.sdkTransactionID + ) + } + + private func challengeParameters(with parameters: PO3DS2ChallengeParameters) -> ChallengeParameters { + ChallengeParameters( + threeDSServerTransactionID: parameters.threeDSServerTransactionId, + acsTransactionID: parameters.acsTransactionId, + acsRefNumber: parameters.acsReferenceNumber, + acsSignedContent: parameters.acsSignedContent + ) + } + + private func failure(with error: Error) -> POFailure { + if let failure = error as? POFailure { + return failure + } + if let error = error as? AuthenticationError { + return errorMapper.convert(error: error) + } + return POFailure(code: .generic(.mobile), underlyingError: error) + } +} diff --git a/Sources/ProcessOutCheckout3DS/Sources/Service/POCheckout3DSServiceDelegate.swift b/Sources/ProcessOutCheckout3DS/Sources/Service/POCheckout3DSServiceDelegate.swift index bb4194ac4..cfc2a7187 100644 --- a/Sources/ProcessOutCheckout3DS/Sources/Service/POCheckout3DSServiceDelegate.swift +++ b/Sources/ProcessOutCheckout3DS/Sources/Service/POCheckout3DSServiceDelegate.swift @@ -9,60 +9,78 @@ import ProcessOut import Checkout3DS /// Checkout 3DS service delegate. -public protocol POCheckout3DSServiceDelegate: AnyObject { - - /// Notifies delegate that service is about to fingerprint device. - func willCreateAuthenticationRequest(configuration: PO3DS2Configuration) - - /// Asks implementation to create `ThreeDS2ServiceConfiguration` using `configParameters`. This method - /// could be used to customize underlying 3DS SDK appearance and behavior. - func configuration( - with parameters: Checkout3DS.ThreeDS2ServiceConfiguration.ConfigParameters - ) -> Checkout3DS.ThreeDS2ServiceConfiguration +public protocol POCheckout3DSServiceDelegate: AnyObject, Sendable { /// Asks delegate whether service should continue with given warnings. Default implementation - /// ignores warnings and completes with `true`. - func shouldContinue(with warnings: Set, completion: @escaping (Bool) -> Void) - - /// Notifies delegate that service did complete device fingerprinting. - func didCreateAuthenticationRequest(result: Result) - - /// Notifies delegate that implementation is about to handle 3DS2 challenge. - func willHandle(challenge: PO3DS2Challenge) - - /// Notifies delegate that service did end handling 3DS2 challenge with given result. - func didHandle3DS2Challenge(result: Result) + /// ignores warnings and returns `true`. + func checkout3DSService(_ service: POCheckout3DSService, shouldContinueWith warnings: Set) async -> Bool - /// Asks delegate to handle 3DS redirect. See documentation of `PO3DSService/handle(redirect:completion:)` - /// for more details. - func handle(redirect: PO3DSRedirect, completion: @escaping (Result) -> Void) + /// Notifies delegate that service is about to fingerprint device. + /// + /// Your implementation could change given `configuration` in case you want to + /// customize underlying 3DS SDK appearance and behavior. Please note that + /// `configParameters` should remain unchanged. + @MainActor + func checkout3DSService( + _ service: POCheckout3DSService, + willCreateAuthenticationRequestParametersWith configuration: inout ThreeDS2ServiceConfiguration + ) + + /// Notifies delegate that service failed to produce device fingerprint. + @MainActor + func checkout3DSService( + _ service: POCheckout3DSService, + didCreateAuthenticationRequestParameters result: Result + ) + + /// Notifies delegate that implementation is about to proceed with 3DS2 challenge. + @MainActor + func checkout3DSService( + _ service: POCheckout3DSService, willPerformChallengeWith parameters: PO3DS2ChallengeParameters + ) + + /// Notifies delegate that service did fail to handle 3DS2 challenge. + @MainActor + func checkout3DSService( + _ service: POCheckout3DSService, didPerformChallenge result: Result + ) } extension POCheckout3DSServiceDelegate { - public func willCreateAuthenticationRequest(configuration: PO3DS2Configuration) { - // Ignored + public func checkout3DSService( + _ service: POCheckout3DSService, shouldContinueWith warnings: Set + ) async -> Bool { + true } - public func configuration( - with parameters: Checkout3DS.ThreeDS2ServiceConfiguration.ConfigParameters - ) -> Checkout3DS.ThreeDS2ServiceConfiguration { - ThreeDS2ServiceConfiguration(configParameters: parameters) - } - - public func didCreateAuthenticationRequest(result: Result) { + @MainActor + public func checkout3DSService( + _ service: POCheckout3DSService, + willCreateAuthenticationRequestParametersWith configuration: inout ThreeDS2ServiceConfiguration + ) { // Ignored } - public func willHandle(challenge: PO3DS2Challenge) { + @MainActor + public func checkout3DSService( + _ service: POCheckout3DSService, + didCreateAuthenticationRequestParameters result: Result + ) { // Ignored } - public func didHandle3DS2Challenge(result: Result) { + @MainActor + public func checkout3DSService( + _ service: POCheckout3DSService, willPerformChallengeWith parameters: PO3DS2ChallengeParameters + ) { // Ignored } - public func shouldContinue(with warnings: Set, completion: @escaping (Bool) -> Void) { - completion(true) + @MainActor + public func checkout3DSService( + _ service: POCheckout3DSService, didPerformChallenge result: Result + ) { + // Ignored } } diff --git a/Sources/ProcessOutCoreUI/ProcessOutCoreUI.docc/ProcessOutCoreUI.md b/Sources/ProcessOutCoreUI/ProcessOutCoreUI.docc/ProcessOutCoreUI.md index 5dc3bc900..9a0b2ccb1 100644 --- a/Sources/ProcessOutCoreUI/ProcessOutCoreUI.docc/ProcessOutCoreUI.md +++ b/Sources/ProcessOutCoreUI/ProcessOutCoreUI.docc/ProcessOutCoreUI.md @@ -16,7 +16,7 @@ - ``POButtonStyle`` - ``POButtonStateStyle`` -- ``SwiftUI/EnvironmentValues/isButtonLoading`` +- ``SwiftUICore/EnvironmentValues/isButtonLoading`` - ``POButtonStyle/primary`` - ``POButtonStyle/secondary`` - ``POBrandButtonStyle`` @@ -28,7 +28,7 @@ - ``PORadioButtonStyle`` - ``PORadioButtonStateStyle`` - ``PORadioButtonKnobStateStyle`` -- ``SwiftUI/EnvironmentValues/isRadioButtonSelected`` +- ``SwiftUICore/EnvironmentValues/isRadioButtonSelected`` - ``PORadioButtonStyle/radio`` ### Checkbox diff --git a/Sources/ProcessOutCoreUI/Sources/Backports/FocusState/FocusCoordinator.swift b/Sources/ProcessOutCoreUI/Sources/Backports/FocusState/FocusCoordinator.swift index ceffb827c..22ed0c005 100644 --- a/Sources/ProcessOutCoreUI/Sources/Backports/FocusState/FocusCoordinator.swift +++ b/Sources/ProcessOutCoreUI/Sources/Backports/FocusState/FocusCoordinator.swift @@ -7,10 +7,12 @@ import SwiftUI +@MainActor final class FocusCoordinator: ObservableObject { /// Holds boolean value indicating whether tracked control is currently being edited. - @Published private(set) var isEditing = false + @Published + private(set) var isEditing = false func track(control: UIControl) { guard self.control == nil else { diff --git a/Sources/ProcessOutCoreUI/Sources/Backports/FocusState/View+Focused.swift b/Sources/ProcessOutCoreUI/Sources/Backports/FocusState/View+Focused.swift index b1d5006b6..73000ed70 100644 --- a/Sources/ProcessOutCoreUI/Sources/Backports/FocusState/View+Focused.swift +++ b/Sources/ProcessOutCoreUI/Sources/Backports/FocusState/View+Focused.swift @@ -32,10 +32,14 @@ extension POBackport where Wrapped: View { @available(iOS 14, *) private struct FocusModifier: ViewModifier { - init(binding: Binding, value: Value) { - self._binding = binding - self.value = value - } + /// The state binding to register. + @Binding + private(set) var binding: Value? + + /// The value to match against when determining whether the binding should change. + let value: Value + + // MARK: - ViewModifier func body(content: Content) -> some View { content @@ -63,13 +67,6 @@ private struct FocusModifier: ViewModifier { // MARK: - Private Properties - /// The value to match against when determining whether the binding should change. - private let value: Value - - /// The state binding to register. - @Binding - private var binding: Value? - /// Indicates whether @State private var isVisible = false diff --git a/Sources/ProcessOutCoreUI/Sources/Backports/OnSubmit/View+OnSubmit.swift b/Sources/ProcessOutCoreUI/Sources/Backports/OnSubmit/View+OnSubmit.swift index 23c2927dd..2a9d6a3aa 100644 --- a/Sources/ProcessOutCoreUI/Sources/Backports/OnSubmit/View+OnSubmit.swift +++ b/Sources/ProcessOutCoreUI/Sources/Backports/OnSubmit/View+OnSubmit.swift @@ -7,34 +7,50 @@ import SwiftUI +extension POBackport where Wrapped: Any { + + @MainActor + struct SubmitAction: Sendable { + + typealias Action = () -> Void // swiftlint:disable:this nesting + + nonisolated init() { + actions = [] + } + + func callAsFunction() { + actions.forEach { $0() } + } + + mutating func append(action: @escaping Action) { + actions.append(action) + } + + // MARK: - Private Properties + + private nonisolated(unsafe) var actions: [Action] + } +} + extension POBackport where Wrapped: View { /// Adds an action to perform when the user submits a value to this view. /// - NOTE: Only works with `POTextField`. public func onSubmit(_ action: @escaping () -> Void) -> some View { - wrapped.environment(\.backportSubmitAction, action) + wrapped.transformEnvironment(\.backportSubmitAction) { $0.append(action: action) } } } extension EnvironmentValues { - var backportSubmitAction: (() -> Void)? { - get { - self[Key.self] - } - set { - let oldValue = backportSubmitAction - let box = { - oldValue?() - newValue?() - } - self[Key.self] = box - } + var backportSubmitAction: POBackport.SubmitAction { + get { self[Key.self] } + set { self[Key.self] = newValue } } // MARK: - Private Properties private struct Key: EnvironmentKey { - static let defaultValue: (() -> Void)? = nil + static let defaultValue = POBackport.SubmitAction() } } diff --git a/Sources/ProcessOutCoreUI/Sources/Backports/POBackport.swift b/Sources/ProcessOutCoreUI/Sources/Backports/POBackport.swift index c20bce60b..f3e2d8809 100644 --- a/Sources/ProcessOutCoreUI/Sources/Backports/POBackport.swift +++ b/Sources/ProcessOutCoreUI/Sources/Backports/POBackport.swift @@ -8,7 +8,9 @@ import SwiftUI /// Provides a convenient method for backporting API. -@_spi(PO) public struct POBackport { +@_spi(PO) +@MainActor +public struct POBackport { /// The underlying content this backport represents. public let wrapped: Wrapped @@ -23,7 +25,8 @@ import SwiftUI extension View { /// Wraps a SwiftUI `View` that can be extended to provide backport functionality. - @_spi(PO) public var backport: POBackport { + @_spi(PO) + public var backport: POBackport { .init(self) } } diff --git a/Sources/ProcessOutCoreUI/Sources/Backports/SubmitLabel/View+SubmitLabel.swift b/Sources/ProcessOutCoreUI/Sources/Backports/SubmitLabel/View+SubmitLabel.swift index d45c0ad8c..a1aaa90d5 100644 --- a/Sources/ProcessOutCoreUI/Sources/Backports/SubmitLabel/View+SubmitLabel.swift +++ b/Sources/ProcessOutCoreUI/Sources/Backports/SubmitLabel/View+SubmitLabel.swift @@ -10,7 +10,7 @@ import SwiftUI extension POBackport where Wrapped == Any { /// A semantic label describing the label of submission within a view hierarchy. - public struct SubmitLabel: Equatable { + public struct SubmitLabel: Equatable, Sendable { let returnKeyType: UIReturnKeyType diff --git a/Sources/ProcessOutCoreUI/Sources/Backports/Task/View+Task.swift b/Sources/ProcessOutCoreUI/Sources/Backports/Task/View+Task.swift index 90659d77e..48200ceae 100644 --- a/Sources/ProcessOutCoreUI/Sources/Backports/Task/View+Task.swift +++ b/Sources/ProcessOutCoreUI/Sources/Backports/Task/View+Task.swift @@ -14,7 +14,9 @@ extension POBackport where Wrapped: View { @available(iOS 14, *) @ViewBuilder public func task( - id value: T, priority: TaskPriority = .userInitiated, _ action: @escaping @Sendable () async -> Void + id value: T, + priority: TaskPriority = .userInitiated, + _ action: @escaping @Sendable @isolated(any) () async -> Void ) -> some View where T: Equatable { if #available(iOS 15, *) { wrapped.task(id: value, priority: priority, action) @@ -23,11 +25,11 @@ extension POBackport where Wrapped: View { } } - @ViewBuilder @available(iOS, deprecated: 15) @available(iOS 14, *) + @ViewBuilder public func task( - priority: TaskPriority = .userInitiated, _ action: @escaping @Sendable () async -> Void + priority: TaskPriority = .userInitiated, _ action: @escaping @Sendable @isolated(any) () async -> Void ) -> some View { task(id: 0, priority: priority, action) } diff --git a/Sources/ProcessOutCoreUI/Sources/Core/Containers/HorizontalSizeReader/HorizontalSizeReader.swift b/Sources/ProcessOutCoreUI/Sources/Core/Containers/HorizontalSizeReader/HorizontalSizeReader.swift index 43d25a0ab..640018fca 100644 --- a/Sources/ProcessOutCoreUI/Sources/Core/Containers/HorizontalSizeReader/HorizontalSizeReader.swift +++ b/Sources/ProcessOutCoreUI/Sources/Core/Containers/HorizontalSizeReader/HorizontalSizeReader.swift @@ -33,7 +33,7 @@ struct HorizontalSizeReader: View { private struct WidthPreferenceKey: PreferenceKey, Equatable { - static var defaultValue: CGFloat = 0 + static let defaultValue: CGFloat = 0 static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) { // An empty reduce implementation takes the first value diff --git a/Sources/ProcessOutCoreUI/Sources/Core/MarkdownParser/MarkdownParser.swift b/Sources/ProcessOutCoreUI/Sources/Core/MarkdownParser/MarkdownParser.swift index 1f0478a09..b7d9c97c9 100644 --- a/Sources/ProcessOutCoreUI/Sources/Core/MarkdownParser/MarkdownParser.swift +++ b/Sources/ProcessOutCoreUI/Sources/Core/MarkdownParser/MarkdownParser.swift @@ -17,6 +17,8 @@ enum MarkdownParser { guard let document else { preconditionFailure("Failed to parse markdown document") } - return MarkdownDocument(cmarkNode: document) + let markdownDocument = MarkdownDocument(cmarkNode: document) + cmark_node_free(document) + return markdownDocument } } diff --git a/Sources/ProcessOutCoreUI/Sources/Core/MarkdownParser/Nodes/MarkdownBlockQuote.swift b/Sources/ProcessOutCoreUI/Sources/Core/MarkdownParser/Nodes/MarkdownBlockQuote.swift index 7cc69ffe1..3b36ce5da 100644 --- a/Sources/ProcessOutCoreUI/Sources/Core/MarkdownParser/Nodes/MarkdownBlockQuote.swift +++ b/Sources/ProcessOutCoreUI/Sources/Core/MarkdownParser/Nodes/MarkdownBlockQuote.swift @@ -7,7 +7,7 @@ import cmark_gfm -final class MarkdownBlockQuote: MarkdownBaseNode { +final class MarkdownBlockQuote: MarkdownBaseNode, @unchecked Sendable { override static var cmarkNodeType: cmark_node_type { CMARK_NODE_BLOCK_QUOTE diff --git a/Sources/ProcessOutCoreUI/Sources/Core/MarkdownParser/Nodes/MarkdownCodeBlock.swift b/Sources/ProcessOutCoreUI/Sources/Core/MarkdownParser/Nodes/MarkdownCodeBlock.swift index 484990cbf..f31433ef1 100644 --- a/Sources/ProcessOutCoreUI/Sources/Core/MarkdownParser/Nodes/MarkdownCodeBlock.swift +++ b/Sources/ProcessOutCoreUI/Sources/Core/MarkdownParser/Nodes/MarkdownCodeBlock.swift @@ -7,23 +7,22 @@ import cmark_gfm -final class MarkdownCodeBlock: MarkdownBaseNode { +final class MarkdownCodeBlock: MarkdownBaseNode, @unchecked Sendable { - /// Returns the info string from a fenced code block. - private(set) lazy var info: String? = { - String(cString: cmarkNode.pointee.as.code.info.data) - }() - - private(set) lazy var code: String = { - guard let literal = cmark_node_get_literal(cmarkNode) else { - assertionFailure("Unable to get text node value") - return "" - } - return String(cString: literal) - }() + /// Actual code value. + let code: String // MARK: - MarkdownBaseNode + required init(cmarkNode: MarkdownBaseNode.CmarkNode, validatesType: Bool = true) { + if let literal = cmark_node_get_literal(cmarkNode) { + self.code = String(cString: literal) + } else { + self.code = "" + } + super.init(cmarkNode: cmarkNode, validatesType: validatesType) + } + override static var cmarkNodeType: cmark_node_type { CMARK_NODE_CODE_BLOCK } diff --git a/Sources/ProcessOutCoreUI/Sources/Core/MarkdownParser/Nodes/MarkdownCodeSpan.swift b/Sources/ProcessOutCoreUI/Sources/Core/MarkdownParser/Nodes/MarkdownCodeSpan.swift index 8b25455f5..933e69cff 100644 --- a/Sources/ProcessOutCoreUI/Sources/Core/MarkdownParser/Nodes/MarkdownCodeSpan.swift +++ b/Sources/ProcessOutCoreUI/Sources/Core/MarkdownParser/Nodes/MarkdownCodeSpan.swift @@ -7,18 +7,23 @@ import cmark_gfm -final class MarkdownCodeSpan: MarkdownBaseNode { +final class MarkdownCodeSpan: MarkdownBaseNode, @unchecked Sendable { - private(set) lazy var code: String = { - guard let literal = cmark_node_get_literal(cmarkNode) else { - assertionFailure("Unable to get text node value") - return "" - } - return String(cString: literal) - }() + /// Code. + let code: String // MARK: - MarkdownBaseNode + required init(cmarkNode: MarkdownBaseNode.CmarkNode, validatesType: Bool = true) { + if let literal = cmark_node_get_literal(cmarkNode) { + code = String(cString: literal) + } else { + assertionFailure("Unable to get text node value") + code = "" + } + super.init(cmarkNode: cmarkNode, validatesType: validatesType) + } + override static var cmarkNodeType: cmark_node_type { CMARK_NODE_CODE } diff --git a/Sources/ProcessOutCoreUI/Sources/Core/MarkdownParser/Nodes/MarkdownDocument.swift b/Sources/ProcessOutCoreUI/Sources/Core/MarkdownParser/Nodes/MarkdownDocument.swift index a19aa1cb6..e4ca6e896 100644 --- a/Sources/ProcessOutCoreUI/Sources/Core/MarkdownParser/Nodes/MarkdownDocument.swift +++ b/Sources/ProcessOutCoreUI/Sources/Core/MarkdownParser/Nodes/MarkdownDocument.swift @@ -7,13 +7,7 @@ import cmark_gfm -final class MarkdownDocument: MarkdownBaseNode { - - deinit { - cmark_node_free(cmarkNode) - } - - // MARK: - MarkdownBaseNode +final class MarkdownDocument: MarkdownBaseNode, @unchecked Sendable { override static var cmarkNodeType: cmark_node_type { CMARK_NODE_DOCUMENT diff --git a/Sources/ProcessOutCoreUI/Sources/Core/MarkdownParser/Nodes/MarkdownEmphasis.swift b/Sources/ProcessOutCoreUI/Sources/Core/MarkdownParser/Nodes/MarkdownEmphasis.swift index 854c5397f..046b1a414 100644 --- a/Sources/ProcessOutCoreUI/Sources/Core/MarkdownParser/Nodes/MarkdownEmphasis.swift +++ b/Sources/ProcessOutCoreUI/Sources/Core/MarkdownParser/Nodes/MarkdownEmphasis.swift @@ -7,9 +7,7 @@ import cmark_gfm -final class MarkdownEmphasis: MarkdownBaseNode { - - // MARK: - MarkdownBaseNode +final class MarkdownEmphasis: MarkdownBaseNode, @unchecked Sendable { override static var cmarkNodeType: cmark_node_type { CMARK_NODE_EMPH diff --git a/Sources/ProcessOutCoreUI/Sources/Core/MarkdownParser/Nodes/MarkdownHeading.swift b/Sources/ProcessOutCoreUI/Sources/Core/MarkdownParser/Nodes/MarkdownHeading.swift index 8115ace24..83b681c34 100644 --- a/Sources/ProcessOutCoreUI/Sources/Core/MarkdownParser/Nodes/MarkdownHeading.swift +++ b/Sources/ProcessOutCoreUI/Sources/Core/MarkdownParser/Nodes/MarkdownHeading.swift @@ -7,14 +7,17 @@ import cmark_gfm -final class MarkdownHeading: MarkdownBaseNode { +final class MarkdownHeading: MarkdownBaseNode, @unchecked Sendable { - private(set) lazy var level: Int = { - Int(cmarkNode.pointee.as.heading.level) - }() + let level: Int // MARK: - MarkdownBaseNode + required init(cmarkNode: MarkdownBaseNode.CmarkNode, validatesType: Bool = true) { + level = Int(cmarkNode.pointee.as.heading.level) + super.init(cmarkNode: cmarkNode, validatesType: validatesType) + } + override static var cmarkNodeType: cmark_node_type { CMARK_NODE_HEADING } diff --git a/Sources/ProcessOutCoreUI/Sources/Core/MarkdownParser/Nodes/MarkdownLinebreak.swift b/Sources/ProcessOutCoreUI/Sources/Core/MarkdownParser/Nodes/MarkdownLinebreak.swift index a8b0df8b7..bb1d19043 100644 --- a/Sources/ProcessOutCoreUI/Sources/Core/MarkdownParser/Nodes/MarkdownLinebreak.swift +++ b/Sources/ProcessOutCoreUI/Sources/Core/MarkdownParser/Nodes/MarkdownLinebreak.swift @@ -7,7 +7,7 @@ import cmark_gfm -final class MarkdownLinebreak: MarkdownBaseNode { +final class MarkdownLinebreak: MarkdownBaseNode, @unchecked Sendable { override static var cmarkNodeType: cmark_node_type { CMARK_NODE_LINEBREAK diff --git a/Sources/ProcessOutCoreUI/Sources/Core/MarkdownParser/Nodes/MarkdownLink.swift b/Sources/ProcessOutCoreUI/Sources/Core/MarkdownParser/Nodes/MarkdownLink.swift index c0e07b1c1..0e01d8293 100644 --- a/Sources/ProcessOutCoreUI/Sources/Core/MarkdownParser/Nodes/MarkdownLink.swift +++ b/Sources/ProcessOutCoreUI/Sources/Core/MarkdownParser/Nodes/MarkdownLink.swift @@ -7,14 +7,17 @@ import cmark_gfm -final class MarkdownLink: MarkdownBaseNode { +final class MarkdownLink: MarkdownBaseNode, @unchecked Sendable { - private(set) lazy var url: String? = { - String(cString: cmarkNode.pointee.as.link.url.data) - }() + let url: String? // MARK: - MarkdownBaseNode + required init(cmarkNode: MarkdownBaseNode.CmarkNode, validatesType: Bool = true) { + self.url = String(cString: cmarkNode.pointee.as.link.url.data) + super.init(cmarkNode: cmarkNode, validatesType: validatesType) + } + override static var cmarkNodeType: cmark_node_type { CMARK_NODE_LINK } diff --git a/Sources/ProcessOutCoreUI/Sources/Core/MarkdownParser/Nodes/MarkdownList.swift b/Sources/ProcessOutCoreUI/Sources/Core/MarkdownParser/Nodes/MarkdownList.swift index 1b02b6197..5e8baebab 100644 --- a/Sources/ProcessOutCoreUI/Sources/Core/MarkdownParser/Nodes/MarkdownList.swift +++ b/Sources/ProcessOutCoreUI/Sources/Core/MarkdownParser/Nodes/MarkdownList.swift @@ -7,7 +7,7 @@ import cmark_gfm -final class MarkdownList: MarkdownBaseNode { +final class MarkdownList: MarkdownBaseNode, @unchecked Sendable { enum ListType { @@ -18,7 +18,27 @@ final class MarkdownList: MarkdownBaseNode { case bullet(marker: Character) } - private(set) lazy var type: ListType = { + /// List type. + let type: ListType + + // MARK: - MarkdownBaseNode + + required init(cmarkNode: MarkdownBaseNode.CmarkNode, validatesType: Bool = true) { + type = Self.listType(cmarkNode: cmarkNode) + super.init(cmarkNode: cmarkNode, validatesType: validatesType) + } + + override static var cmarkNodeType: cmark_node_type { + CMARK_NODE_LIST + } + + override func accept(visitor: V) -> V.Result { + visitor.visit(list: self) + } + + // MARK: - Private Methods + + private static func listType(cmarkNode: CmarkNode) -> ListType { let listNode = cmarkNode.pointee.as.list switch listNode.list_type { case CMARK_BULLET_LIST: @@ -40,19 +60,5 @@ final class MarkdownList: MarkdownBaseNode { default: preconditionFailure("Unsupported list type: \(listNode.list_type)") } - }() - - private(set) lazy var isTight: Bool = { - cmarkNode.pointee.as.list.tight - }() - - // MARK: - MarkdownBaseNode - - override static var cmarkNodeType: cmark_node_type { - CMARK_NODE_LIST - } - - override func accept(visitor: V) -> V.Result { - visitor.visit(list: self) } } diff --git a/Sources/ProcessOutCoreUI/Sources/Core/MarkdownParser/Nodes/MarkdownListItem.swift b/Sources/ProcessOutCoreUI/Sources/Core/MarkdownParser/Nodes/MarkdownListItem.swift index dd43dc37a..30659804f 100644 --- a/Sources/ProcessOutCoreUI/Sources/Core/MarkdownParser/Nodes/MarkdownListItem.swift +++ b/Sources/ProcessOutCoreUI/Sources/Core/MarkdownParser/Nodes/MarkdownListItem.swift @@ -7,9 +7,7 @@ import cmark_gfm -final class MarkdownListItem: MarkdownBaseNode { - - // MARK: - MarkdownBaseNode +final class MarkdownListItem: MarkdownBaseNode, @unchecked Sendable { override static var cmarkNodeType: cmark_node_type { CMARK_NODE_ITEM diff --git a/Sources/ProcessOutCoreUI/Sources/Core/MarkdownParser/Nodes/MarkdownNode.swift b/Sources/ProcessOutCoreUI/Sources/Core/MarkdownParser/Nodes/MarkdownNode.swift index cead992b5..51e0dfc90 100644 --- a/Sources/ProcessOutCoreUI/Sources/Core/MarkdownParser/Nodes/MarkdownNode.swift +++ b/Sources/ProcessOutCoreUI/Sources/Core/MarkdownParser/Nodes/MarkdownNode.swift @@ -7,7 +7,7 @@ import cmark_gfm -class MarkdownBaseNode { +class MarkdownBaseNode: @unchecked Sendable { typealias CmarkNode = UnsafeMutablePointer @@ -19,11 +19,20 @@ class MarkdownBaseNode { if validatesType { assert(cmarkNode.pointee.type == Self.cmarkNodeType.rawValue) } - self.cmarkNode = cmarkNode + self.children = Self.children(of: cmarkNode) } /// Returns node children. - private(set) lazy var children: [MarkdownBaseNode] = { + let children: [MarkdownBaseNode] + + /// Accepts given visitor. + func accept(visitor: V) -> V.Result { // swiftlint:disable:this unavailable_function + fatalError("Must be implemented by subclass.") + } + + // MARK: - Private Methods + + private static func children(of cmarkNode: CmarkNode) -> [MarkdownBaseNode] { var cmarkChild = cmarkNode.pointee.first_child var children: [MarkdownBaseNode] = [] while let cmarkNode = cmarkChild { @@ -32,12 +41,5 @@ class MarkdownBaseNode { cmarkChild = cmarkNode.pointee.next } return children - }() - - let cmarkNode: CmarkNode - - /// Accepts given visitor. - func accept(visitor: V) -> V.Result { // swiftlint:disable:this unavailable_function - fatalError("Must be implemented by subclass.") } } diff --git a/Sources/ProcessOutCoreUI/Sources/Core/MarkdownParser/Nodes/MarkdownParagraph.swift b/Sources/ProcessOutCoreUI/Sources/Core/MarkdownParser/Nodes/MarkdownParagraph.swift index 1d98a070d..3e244c83b 100644 --- a/Sources/ProcessOutCoreUI/Sources/Core/MarkdownParser/Nodes/MarkdownParagraph.swift +++ b/Sources/ProcessOutCoreUI/Sources/Core/MarkdownParser/Nodes/MarkdownParagraph.swift @@ -7,9 +7,7 @@ import cmark_gfm -final class MarkdownParagraph: MarkdownBaseNode { - - // MARK: - MarkdownBaseNode +final class MarkdownParagraph: MarkdownBaseNode, @unchecked Sendable { override static var cmarkNodeType: cmark_node_type { CMARK_NODE_PARAGRAPH diff --git a/Sources/ProcessOutCoreUI/Sources/Core/MarkdownParser/Nodes/MarkdownSoftbreak.swift b/Sources/ProcessOutCoreUI/Sources/Core/MarkdownParser/Nodes/MarkdownSoftbreak.swift index cda0c53df..0872f2b08 100644 --- a/Sources/ProcessOutCoreUI/Sources/Core/MarkdownParser/Nodes/MarkdownSoftbreak.swift +++ b/Sources/ProcessOutCoreUI/Sources/Core/MarkdownParser/Nodes/MarkdownSoftbreak.swift @@ -7,7 +7,7 @@ import cmark_gfm -final class MarkdownSoftbreak: MarkdownBaseNode { +final class MarkdownSoftbreak: MarkdownBaseNode, @unchecked Sendable { override static var cmarkNodeType: cmark_node_type { CMARK_NODE_SOFTBREAK diff --git a/Sources/ProcessOutCoreUI/Sources/Core/MarkdownParser/Nodes/MarkdownStrong.swift b/Sources/ProcessOutCoreUI/Sources/Core/MarkdownParser/Nodes/MarkdownStrong.swift index cc86ae389..0d4c73ef9 100644 --- a/Sources/ProcessOutCoreUI/Sources/Core/MarkdownParser/Nodes/MarkdownStrong.swift +++ b/Sources/ProcessOutCoreUI/Sources/Core/MarkdownParser/Nodes/MarkdownStrong.swift @@ -7,9 +7,7 @@ import cmark_gfm -final class MarkdownStrong: MarkdownBaseNode { - - // MARK: - MarkdownBaseNode +final class MarkdownStrong: MarkdownBaseNode, @unchecked Sendable { override static var cmarkNodeType: cmark_node_type { CMARK_NODE_STRONG diff --git a/Sources/ProcessOutCoreUI/Sources/Core/MarkdownParser/Nodes/MarkdownText.swift b/Sources/ProcessOutCoreUI/Sources/Core/MarkdownParser/Nodes/MarkdownText.swift index da9d0e65d..5aff0a530 100644 --- a/Sources/ProcessOutCoreUI/Sources/Core/MarkdownParser/Nodes/MarkdownText.swift +++ b/Sources/ProcessOutCoreUI/Sources/Core/MarkdownParser/Nodes/MarkdownText.swift @@ -7,18 +7,23 @@ import cmark_gfm -final class MarkdownText: MarkdownBaseNode { +final class MarkdownText: MarkdownBaseNode, @unchecked Sendable { - private(set) lazy var value: String = { - guard let literal = cmark_node_get_literal(cmarkNode) else { - assertionFailure("Unable to get text node value") - return "" - } - return String(cString: literal) - }() + /// Text value. + let value: String // MARK: - MarkdownBaseNode + required init(cmarkNode: MarkdownBaseNode.CmarkNode, validatesType: Bool = true) { + if let literal = cmark_node_get_literal(cmarkNode) { + value = String(cString: literal) + } else { + assertionFailure("Unable to get text node value") + value = "" + } + super.init(cmarkNode: cmarkNode, validatesType: validatesType) + } + override static var cmarkNodeType: cmark_node_type { CMARK_NODE_TEXT } diff --git a/Sources/ProcessOutCoreUI/Sources/Core/MarkdownParser/Nodes/MarkdownThematicBreak.swift b/Sources/ProcessOutCoreUI/Sources/Core/MarkdownParser/Nodes/MarkdownThematicBreak.swift index 1da83b521..8dee523cf 100644 --- a/Sources/ProcessOutCoreUI/Sources/Core/MarkdownParser/Nodes/MarkdownThematicBreak.swift +++ b/Sources/ProcessOutCoreUI/Sources/Core/MarkdownParser/Nodes/MarkdownThematicBreak.swift @@ -7,7 +7,7 @@ import cmark_gfm -final class MarkdownThematicBreak: MarkdownBaseNode { +final class MarkdownThematicBreak: MarkdownBaseNode, @unchecked Sendable { override static var cmarkNodeType: cmark_node_type { CMARK_NODE_THEMATIC_BREAK diff --git a/Sources/ProcessOutCoreUI/Sources/Core/MarkdownParser/Nodes/MarkdownUnknown.swift b/Sources/ProcessOutCoreUI/Sources/Core/MarkdownParser/Nodes/MarkdownUnknown.swift index 2fefc5028..2f1526934 100644 --- a/Sources/ProcessOutCoreUI/Sources/Core/MarkdownParser/Nodes/MarkdownUnknown.swift +++ b/Sources/ProcessOutCoreUI/Sources/Core/MarkdownParser/Nodes/MarkdownUnknown.swift @@ -6,7 +6,7 @@ // /// Unknown node. -final class MarkdownUnknown: MarkdownBaseNode { +final class MarkdownUnknown: MarkdownBaseNode, @unchecked Sendable { required init(cmarkNode: CmarkNode, validatesType: Bool = false) { super.init(cmarkNode: cmarkNode, validatesType: false) diff --git a/Sources/ProcessOutCoreUI/Sources/Core/MarkdownParser/Visitor/MarkdownDebugDescriptionPrinter.swift b/Sources/ProcessOutCoreUI/Sources/Core/MarkdownParser/Visitor/MarkdownDebugDescriptionPrinter.swift index a1460bf98..d80bb87cd 100644 --- a/Sources/ProcessOutCoreUI/Sources/Core/MarkdownParser/Visitor/MarkdownDebugDescriptionPrinter.swift +++ b/Sources/ProcessOutCoreUI/Sources/Core/MarkdownParser/Visitor/MarkdownDebugDescriptionPrinter.swift @@ -7,17 +7,7 @@ #if DEBUG -import Foundation - -extension MarkdownBaseNode: CustomDebugStringConvertible { - - var debugDescription: String { - let visitor = MarkdownDebugDescriptionPrinter() - return self.accept(visitor: visitor) - } -} - -private final class MarkdownDebugDescriptionPrinter: MarkdownVisitor { +final class MarkdownDebugDescriptionPrinter: MarkdownVisitor { init(level: Int = 0) { self.level = level @@ -81,11 +71,7 @@ private final class MarkdownDebugDescriptionPrinter: MarkdownVisitor { } func visit(codeBlock: MarkdownCodeBlock) -> String { - var attributes: [String: CustomStringConvertible] = [:] - if let info = codeBlock.info { - attributes["info"] = info - } - return description(node: codeBlock, nodeName: "Code Block", attributes: attributes, content: codeBlock.code) + description(node: codeBlock, nodeName: "Code Block", content: codeBlock.code) } func visit(thematicBreak: MarkdownThematicBreak) -> String { @@ -147,4 +133,12 @@ private final class MarkdownDebugDescriptionPrinter: MarkdownVisitor { } } +extension MarkdownBaseNode: CustomDebugStringConvertible { + + var debugDescription: String { + let visitor = MarkdownDebugDescriptionPrinter() + return self.accept(visitor: visitor) + } +} + #endif diff --git a/Sources/ProcessOutCoreUI/Sources/Core/Modifiers/Blink/View+Blink.swift b/Sources/ProcessOutCoreUI/Sources/Core/Modifiers/Blink/View+Blink.swift index 6563f3c25..65df4394e 100644 --- a/Sources/ProcessOutCoreUI/Sources/Core/Modifiers/Blink/View+Blink.swift +++ b/Sources/ProcessOutCoreUI/Sources/Core/Modifiers/Blink/View+Blink.swift @@ -31,5 +31,6 @@ private struct BlinkViewModifier: ViewModifier { // MARK: - Private Properties - @State private var isVisible = true + @State + private var isVisible = true } diff --git a/Sources/ProcessOutCoreUI/Sources/Core/Modifiers/KeyboardType/View+KeyboardType.swift b/Sources/ProcessOutCoreUI/Sources/Core/Modifiers/KeyboardType/View+KeyboardType.swift index 501489096..761c5eb5b 100644 --- a/Sources/ProcessOutCoreUI/Sources/Core/Modifiers/KeyboardType/View+KeyboardType.swift +++ b/Sources/ProcessOutCoreUI/Sources/Core/Modifiers/KeyboardType/View+KeyboardType.swift @@ -11,7 +11,8 @@ extension View { /// Sets the keyboard type for this view. In addition to calling the native counterpart, /// the implementation also exposes given type as an environment so works with `POTextField`. - @_spi(PO) public func poKeyboardType(_ type: UIKeyboardType) -> some View { + @_spi(PO) + public func poKeyboardType(_ type: UIKeyboardType) -> some View { environment(\.poKeyboardType, type).keyboardType(type) } } diff --git a/Sources/ProcessOutCoreUI/Sources/Core/Modifiers/Modify/View+Modify.swift b/Sources/ProcessOutCoreUI/Sources/Core/Modifiers/Modify/View+Modify.swift index 5836eabc5..40b0c00ec 100644 --- a/Sources/ProcessOutCoreUI/Sources/Core/Modifiers/Modify/View+Modify.swift +++ b/Sources/ProcessOutCoreUI/Sources/Core/Modifiers/Modify/View+Modify.swift @@ -7,7 +7,8 @@ import SwiftUI -@_spi(PO) extension View { +@_spi(PO) +extension View { @ViewBuilder public func modify(when condition: Bool, @ViewBuilder _ transform: (Self) -> some View) -> some View { diff --git a/Sources/ProcessOutCoreUI/Sources/Core/Modifiers/OnSizeChange/View+OnSizeChange.swift b/Sources/ProcessOutCoreUI/Sources/Core/Modifiers/OnSizeChange/View+OnSizeChange.swift index 99d6ca981..1428f4939 100644 --- a/Sources/ProcessOutCoreUI/Sources/Core/Modifiers/OnSizeChange/View+OnSizeChange.swift +++ b/Sources/ProcessOutCoreUI/Sources/Core/Modifiers/OnSizeChange/View+OnSizeChange.swift @@ -35,7 +35,7 @@ private struct SizeModifier: ViewModifier { private struct SizePreferenceKey: PreferenceKey { - static var defaultValue: CGSize = .zero + static let defaultValue: CGSize = .zero static func reduce(value: inout CGSize, nextValue: () -> CGSize) { value = nextValue() diff --git a/Sources/ProcessOutCoreUI/Sources/Core/Modifiers/TextContentType/View+TextContentType.swift b/Sources/ProcessOutCoreUI/Sources/Core/Modifiers/TextContentType/View+TextContentType.swift index 0a666047b..22aeb924a 100644 --- a/Sources/ProcessOutCoreUI/Sources/Core/Modifiers/TextContentType/View+TextContentType.swift +++ b/Sources/ProcessOutCoreUI/Sources/Core/Modifiers/TextContentType/View+TextContentType.swift @@ -11,7 +11,8 @@ extension View { /// Sets the text content type for this view. In addition to calling the native counterpart, /// the implementation also exposes given type as an environment so works with `POTextField`. - @_spi(PO) public func poTextContentType(_ type: UITextContentType?) -> some View { + @_spi(PO) + public func poTextContentType(_ type: UITextContentType?) -> some View { environment(\.poTextContentType, type).textContentType(type) } } diff --git a/Sources/ProcessOutCoreUI/Sources/Core/UIKit+Extensions/UIFont+FontFeatures/FontNumberSpacing.swift b/Sources/ProcessOutCoreUI/Sources/Core/UIKit+Extensions/UIFont+FontFeatures/FontNumberSpacing.swift index a12f1c118..b8519c22a 100644 --- a/Sources/ProcessOutCoreUI/Sources/Core/UIKit+Extensions/UIFont+FontFeatures/FontNumberSpacing.swift +++ b/Sources/ProcessOutCoreUI/Sources/Core/UIKit+Extensions/UIFont+FontFeatures/FontNumberSpacing.swift @@ -8,7 +8,7 @@ import CoreText @_spi(PO) -public struct POFontNumberSpacing { +public struct POFontNumberSpacing: Sendable { let rawValue: Int } diff --git a/Sources/ProcessOutCoreUI/Sources/Core/UIKit+Extensions/UIFont+FontFeatures/POFontFeatureSetting.swift b/Sources/ProcessOutCoreUI/Sources/Core/UIKit+Extensions/UIFont+FontFeatures/POFontFeatureSetting.swift index 7d827113d..b78921a26 100644 --- a/Sources/ProcessOutCoreUI/Sources/Core/UIKit+Extensions/UIFont+FontFeatures/POFontFeatureSetting.swift +++ b/Sources/ProcessOutCoreUI/Sources/Core/UIKit+Extensions/UIFont+FontFeatures/POFontFeatureSetting.swift @@ -5,7 +5,7 @@ // Created by Andrii Vysotskyi on 30.07.2024. // -protocol FontFeatureSetting { +protocol FontFeatureSetting: Sendable { /// Indicates a general class of effect (e.g., ligatures). var featureType: Int { get } diff --git a/Sources/ProcessOutCoreUI/Sources/Core/UIKit+Extensions/UIFont+FontFeatures/POFontFeaturesSettings.swift b/Sources/ProcessOutCoreUI/Sources/Core/UIKit+Extensions/UIFont+FontFeatures/POFontFeaturesSettings.swift index f74f1c9e6..c93c90413 100644 --- a/Sources/ProcessOutCoreUI/Sources/Core/UIKit+Extensions/UIFont+FontFeatures/POFontFeaturesSettings.swift +++ b/Sources/ProcessOutCoreUI/Sources/Core/UIKit+Extensions/UIFont+FontFeatures/POFontFeaturesSettings.swift @@ -6,7 +6,7 @@ // @_spi(PO) -public struct POFontFeaturesSettings { +public struct POFontFeaturesSettings: Sendable { /// The number spacing feature type specifies a choice for the appearance of digits. public var numberSpacing: POFontNumberSpacing = .proportional diff --git a/Sources/ProcessOutCoreUI/Sources/DesignSystem/ActionsContainer/POActionsContainerStyle.swift b/Sources/ProcessOutCoreUI/Sources/DesignSystem/ActionsContainer/POActionsContainerStyle.swift index 8bb2a79d3..d529f0c78 100644 --- a/Sources/ProcessOutCoreUI/Sources/DesignSystem/ActionsContainer/POActionsContainerStyle.swift +++ b/Sources/ProcessOutCoreUI/Sources/DesignSystem/ActionsContainer/POActionsContainerStyle.swift @@ -8,6 +8,7 @@ import SwiftUI /// Actions container style. +@MainActor public struct POActionsContainerStyle { /// Style for primary button. diff --git a/Sources/ProcessOutCoreUI/Sources/DesignSystem/ActionsContainer/View+ActionsContainerStyle.swift b/Sources/ProcessOutCoreUI/Sources/DesignSystem/ActionsContainer/View+ActionsContainerStyle.swift index ce6ca39ca..362775bc5 100644 --- a/Sources/ProcessOutCoreUI/Sources/DesignSystem/ActionsContainer/View+ActionsContainerStyle.swift +++ b/Sources/ProcessOutCoreUI/Sources/DesignSystem/ActionsContainer/View+ActionsContainerStyle.swift @@ -26,7 +26,8 @@ extension EnvironmentValues { // MARK: - Private Nested Types - private struct Key: EnvironmentKey { + @MainActor + private struct Key: @preconcurrency EnvironmentKey { static let defaultValue = POActionsContainerStyle.default } } diff --git a/Sources/ProcessOutCoreUI/Sources/DesignSystem/AsyncImage/POAsyncImage.swift b/Sources/ProcessOutCoreUI/Sources/DesignSystem/AsyncImage/POAsyncImage.swift index 292e1c9cb..d7dcc44c9 100644 --- a/Sources/ProcessOutCoreUI/Sources/DesignSystem/AsyncImage/POAsyncImage.swift +++ b/Sources/ProcessOutCoreUI/Sources/DesignSystem/AsyncImage/POAsyncImage.swift @@ -8,7 +8,6 @@ import SwiftUI @_spi(PO) -@MainActor @available(iOS 14, *) public struct POAsyncImage: View { @@ -17,7 +16,7 @@ public struct POAsyncImage: View { /// and returns the view to display for the specified phase. public init( id: AnyHashable, - image: @Sendable @escaping () async throws -> Image?, + image: @Sendable @escaping @isolated(any) () async throws -> Image?, transaction: Transaction, @ViewBuilder content: @escaping (POAsyncImagePhase) -> Content ) { @@ -36,7 +35,7 @@ public struct POAsyncImage: View { phase = .empty } } - .backport.task(id: id, priority: .userInitiated, resolveImage) + .backport.task(id: id, priority: .userInitiated) { await resolveImage() } } // MARK: - Private Properties @@ -55,7 +54,6 @@ public struct POAsyncImage: View { // MARK: - Private Methods /// Implementation resolves image and updates phase. - @Sendable @MainActor private func resolveImage() async { guard !Task.isCancelled, case .empty = phase else { diff --git a/Sources/ProcessOutCoreUI/Sources/DesignSystem/Border/POBorderStyle.swift b/Sources/ProcessOutCoreUI/Sources/DesignSystem/Border/POBorderStyle.swift index 1e15bbac9..d4e715615 100644 --- a/Sources/ProcessOutCoreUI/Sources/DesignSystem/Border/POBorderStyle.swift +++ b/Sources/ProcessOutCoreUI/Sources/DesignSystem/Border/POBorderStyle.swift @@ -8,7 +8,7 @@ import SwiftUI /// Style that defines border appearance. Border is always a solid line. -public struct POBorderStyle { +public struct POBorderStyle: Sendable { /// Corner radius. public let radius: CGFloat diff --git a/Sources/ProcessOutCoreUI/Sources/DesignSystem/Button/Brand/ButtonStyle+POBrandButtonStyle.swift b/Sources/ProcessOutCoreUI/Sources/DesignSystem/Button/Brand/ButtonStyle+POBrandButtonStyle.swift index f3a0dd6d5..3b36dee47 100644 --- a/Sources/ProcessOutCoreUI/Sources/DesignSystem/Button/Brand/ButtonStyle+POBrandButtonStyle.swift +++ b/Sources/ProcessOutCoreUI/Sources/DesignSystem/Button/Brand/ButtonStyle+POBrandButtonStyle.swift @@ -10,7 +10,7 @@ import SwiftUI @available(iOS 14, *) extension ButtonStyle where Self == POBrandButtonStyle { - /// Simple style that changes its appearance based on brand color ``SwiftUI/EnvironmentValues/poButtonBrandColor``. + /// Simple style that changes its appearance based on brand color ``SwiftUICore/EnvironmentValues/poButtonBrandColor``. @_disfavoredOverload public static var brand: POBrandButtonStyle { POBrandButtonStyle( diff --git a/Sources/ProcessOutCoreUI/Sources/DesignSystem/Button/PassKit/POPassKitPaymentButton.swift b/Sources/ProcessOutCoreUI/Sources/DesignSystem/Button/PassKit/POPassKitPaymentButton.swift index f23ce30c2..929775ec9 100644 --- a/Sources/ProcessOutCoreUI/Sources/DesignSystem/Button/PassKit/POPassKitPaymentButton.swift +++ b/Sources/ProcessOutCoreUI/Sources/DesignSystem/Button/PassKit/POPassKitPaymentButton.swift @@ -67,6 +67,7 @@ private struct ButtonRepresentable: UIViewRepresentable { } } +@MainActor private final class ButtonCoordinator { init(action: @escaping () -> Void) { diff --git a/Sources/ProcessOutCoreUI/Sources/DesignSystem/Button/PassKit/POPassKitPaymentButtonStyle.swift b/Sources/ProcessOutCoreUI/Sources/DesignSystem/Button/PassKit/POPassKitPaymentButtonStyle.swift index 51f71ee25..c6f940158 100644 --- a/Sources/ProcessOutCoreUI/Sources/DesignSystem/Button/PassKit/POPassKitPaymentButtonStyle.swift +++ b/Sources/ProcessOutCoreUI/Sources/DesignSystem/Button/PassKit/POPassKitPaymentButtonStyle.swift @@ -9,7 +9,7 @@ import PassKit /// PassKit button style. @available(iOS 14.0, *) -public struct POPassKitPaymentButtonStyle { +public struct POPassKitPaymentButtonStyle: Sendable { /// Native style value. public let style: PKPaymentButtonStyle diff --git a/Sources/ProcessOutCoreUI/Sources/DesignSystem/Button/Regular/POButtonStateStyle.swift b/Sources/ProcessOutCoreUI/Sources/DesignSystem/Button/Regular/POButtonStateStyle.swift index 72a639b39..520045c2a 100644 --- a/Sources/ProcessOutCoreUI/Sources/DesignSystem/Button/Regular/POButtonStateStyle.swift +++ b/Sources/ProcessOutCoreUI/Sources/DesignSystem/Button/Regular/POButtonStateStyle.swift @@ -8,7 +8,7 @@ import SwiftUI /// Defines button's styling information in a specific state. -public struct POButtonStateStyle { +public struct POButtonStateStyle: Sendable { /// Title typography. public let title: POTextStyle diff --git a/Sources/ProcessOutCoreUI/Sources/DesignSystem/CodeField/CodeFieldViewCoordinator.swift b/Sources/ProcessOutCoreUI/Sources/DesignSystem/CodeField/CodeFieldViewCoordinator.swift index fa0793b18..4156277d9 100644 --- a/Sources/ProcessOutCoreUI/Sources/DesignSystem/CodeField/CodeFieldViewCoordinator.swift +++ b/Sources/ProcessOutCoreUI/Sources/DesignSystem/CodeField/CodeFieldViewCoordinator.swift @@ -7,6 +7,7 @@ import Foundation +@MainActor final class CodeFieldViewCoordinator { var representable: CodeFieldRepresentable! // swiftlint:disable:this implicitly_unwrapped_optional diff --git a/Sources/ProcessOutCoreUI/Sources/DesignSystem/CodeField/POCodeField.swift b/Sources/ProcessOutCoreUI/Sources/DesignSystem/CodeField/POCodeField.swift index f00b5a66b..e6006106e 100644 --- a/Sources/ProcessOutCoreUI/Sources/DesignSystem/CodeField/POCodeField.swift +++ b/Sources/ProcessOutCoreUI/Sources/DesignSystem/CodeField/POCodeField.swift @@ -24,8 +24,7 @@ public struct POCodeField: View { focusCoordinator.beginEditing() textIndex = newIndex } - style - .makeBody(configuration: configuration) + AnyView(style.makeBody(configuration: configuration)) .background( CodeFieldRepresentable( length: length, text: $text, textIndex: $textIndex, isMenuVisible: $isMenuVisible diff --git a/Sources/ProcessOutCoreUI/Sources/DesignSystem/CodeField/Style/AnyCodeFieldStyle.swift b/Sources/ProcessOutCoreUI/Sources/DesignSystem/CodeField/Style/AnyCodeFieldStyle.swift deleted file mode 100644 index 86df19d78..000000000 --- a/Sources/ProcessOutCoreUI/Sources/DesignSystem/CodeField/Style/AnyCodeFieldStyle.swift +++ /dev/null @@ -1,25 +0,0 @@ -// -// AnyCodeFieldStyle.swift -// ProcessOutCoreUI -// -// Created by Andrii Vysotskyi on 13.06.2024. -// - -import SwiftUI - -struct AnyCodeFieldStyle: CodeFieldStyle { - - init(erasing style: some CodeFieldStyle) { - _makeBody = { configuration in - AnyView(style.makeBody(configuration: configuration)) - } - } - - func makeBody(configuration: CodeFieldStyleConfiguration) -> some View { - _makeBody(configuration) - } - - // MARK: - Private Properties - - private let _makeBody: (CodeFieldStyleConfiguration) -> AnyView -} diff --git a/Sources/ProcessOutCoreUI/Sources/DesignSystem/CodeField/Style/CodeFieldStyle.swift b/Sources/ProcessOutCoreUI/Sources/DesignSystem/CodeField/Style/CodeFieldStyle.swift index 0ef91e704..5979d08f1 100644 --- a/Sources/ProcessOutCoreUI/Sources/DesignSystem/CodeField/Style/CodeFieldStyle.swift +++ b/Sources/ProcessOutCoreUI/Sources/DesignSystem/CodeField/Style/CodeFieldStyle.swift @@ -7,6 +7,7 @@ import SwiftUI +@MainActor protocol CodeFieldStyle { /// A view that represents the body of a button. diff --git a/Sources/ProcessOutCoreUI/Sources/DesignSystem/CodeField/Style/CodeFieldStyleConfiguration.swift b/Sources/ProcessOutCoreUI/Sources/DesignSystem/CodeField/Style/CodeFieldStyleConfiguration.swift index f18891d26..addc3d1b7 100644 --- a/Sources/ProcessOutCoreUI/Sources/DesignSystem/CodeField/Style/CodeFieldStyleConfiguration.swift +++ b/Sources/ProcessOutCoreUI/Sources/DesignSystem/CodeField/Style/CodeFieldStyleConfiguration.swift @@ -34,3 +34,6 @@ struct CodeFieldStyleConfiguration { private let _setIndex: (_ index: Index) -> Void } + +@available(*, unavailable) +extension CodeFieldStyleConfiguration: Sendable { } diff --git a/Sources/ProcessOutCoreUI/Sources/DesignSystem/CodeField/Style/View+CodeFieldStyle.swift b/Sources/ProcessOutCoreUI/Sources/DesignSystem/CodeField/Style/View+CodeFieldStyle.swift index f448589d5..ccc195765 100644 --- a/Sources/ProcessOutCoreUI/Sources/DesignSystem/CodeField/Style/View+CodeFieldStyle.swift +++ b/Sources/ProcessOutCoreUI/Sources/DesignSystem/CodeField/Style/View+CodeFieldStyle.swift @@ -11,22 +11,23 @@ extension View { /// Sets the style for picker views within this view. @available(iOS 14.0, *) - func codeFieldStyle(_ style: some CodeFieldStyle) -> some View { - environment(\.codeFieldStyle, AnyCodeFieldStyle(erasing: style)) + func codeFieldStyle(_ style: any CodeFieldStyle) -> some View { + environment(\.codeFieldStyle, style) } } @available(iOS 14.0, *) extension EnvironmentValues { - var codeFieldStyle: AnyCodeFieldStyle { + var codeFieldStyle: any CodeFieldStyle { get { self[Key.self] } set { self[Key.self] = newValue } } // MARK: - Private Properties - private struct Key: EnvironmentKey { - static let defaultValue = AnyCodeFieldStyle(erasing: .default) + @MainActor + private struct Key: @preconcurrency EnvironmentKey { + static let defaultValue: any CodeFieldStyle = .default } } diff --git a/Sources/ProcessOutCoreUI/Sources/DesignSystem/ConfirmationDialog/View+ConfirmationDialog.swift b/Sources/ProcessOutCoreUI/Sources/DesignSystem/ConfirmationDialog/View+ConfirmationDialog.swift index 7f0a4da5b..01df2a11e 100644 --- a/Sources/ProcessOutCoreUI/Sources/DesignSystem/ConfirmationDialog/View+ConfirmationDialog.swift +++ b/Sources/ProcessOutCoreUI/Sources/DesignSystem/ConfirmationDialog/View+ConfirmationDialog.swift @@ -9,8 +9,8 @@ import SwiftUI extension View { - @_spi(PO) @available(iOS 14, *) + @_spi(PO) public func poConfirmationDialog(item: Binding) -> some View { modifier(ContentModifier(confirmationDialog: item)) } diff --git a/Sources/ProcessOutCoreUI/Sources/DesignSystem/InputStyle/POInputStateStyle.swift b/Sources/ProcessOutCoreUI/Sources/DesignSystem/InputStyle/POInputStateStyle.swift index 849f5b784..4b74457b8 100644 --- a/Sources/ProcessOutCoreUI/Sources/DesignSystem/InputStyle/POInputStateStyle.swift +++ b/Sources/ProcessOutCoreUI/Sources/DesignSystem/InputStyle/POInputStateStyle.swift @@ -8,7 +8,7 @@ import SwiftUI /// Defines input's styling information in a specific state. -public struct POInputStateStyle { +public struct POInputStateStyle: Sendable { /// Text style. public let text: POTextStyle diff --git a/Sources/ProcessOutCoreUI/Sources/DesignSystem/InputStyle/POInputStyle.swift b/Sources/ProcessOutCoreUI/Sources/DesignSystem/InputStyle/POInputStyle.swift index c8b42e502..c0fde3520 100644 --- a/Sources/ProcessOutCoreUI/Sources/DesignSystem/InputStyle/POInputStyle.swift +++ b/Sources/ProcessOutCoreUI/Sources/DesignSystem/InputStyle/POInputStyle.swift @@ -8,7 +8,7 @@ import SwiftUI /// Defines input control style in both normal and error states. -public struct POInputStyle { +public struct POInputStyle: Sendable { /// Style for normal state. public let normal: POInputStateStyle diff --git a/Sources/ProcessOutCoreUI/Sources/DesignSystem/InputStyle/View+InputStyle.swift b/Sources/ProcessOutCoreUI/Sources/DesignSystem/InputStyle/View+InputStyle.swift index df5d74fc9..3fb67e693 100644 --- a/Sources/ProcessOutCoreUI/Sources/DesignSystem/InputStyle/View+InputStyle.swift +++ b/Sources/ProcessOutCoreUI/Sources/DesignSystem/InputStyle/View+InputStyle.swift @@ -9,7 +9,8 @@ import SwiftUI extension View { - @_spi(PO) public func inputStyle(_ style: POInputStyle) -> some View { + @_spi(PO) + public func inputStyle(_ style: POInputStyle) -> some View { environment(\.inputStyle, style) } } diff --git a/Sources/ProcessOutCoreUI/Sources/DesignSystem/Message/AnyMessageViewStyle.swift b/Sources/ProcessOutCoreUI/Sources/DesignSystem/Message/AnyMessageViewStyle.swift deleted file mode 100644 index bcf912062..000000000 --- a/Sources/ProcessOutCoreUI/Sources/DesignSystem/Message/AnyMessageViewStyle.swift +++ /dev/null @@ -1,25 +0,0 @@ -// -// AnyMessageViewStyle.swift -// ProcessOutCoreUI -// -// Created by Andrii Vysotskyi on 03.06.2024. -// - -import SwiftUI - -struct AnyMessageViewStyle: POMessageViewStyle { - - init(erasing style: any POMessageViewStyle) { - _makeBody = { configuration in - AnyView(style.makeBody(configuration: configuration)) - } - } - - func makeBody(configuration: Configuration) -> AnyView { - _makeBody(configuration) - } - - // MARK: - Private Properties - - private let _makeBody: (Configuration) -> AnyView -} diff --git a/Sources/ProcessOutCoreUI/Sources/DesignSystem/Message/MessageView+Style.swift b/Sources/ProcessOutCoreUI/Sources/DesignSystem/Message/MessageView+Style.swift index 3e70f3d77..8401ba324 100644 --- a/Sources/ProcessOutCoreUI/Sources/DesignSystem/Message/MessageView+Style.swift +++ b/Sources/ProcessOutCoreUI/Sources/DesignSystem/Message/MessageView+Style.swift @@ -13,21 +13,22 @@ extension View { @_spi(PO) @available(iOS 14.0, *) public func messageViewStyle(_ style: any POMessageViewStyle) -> some View { - environment(\.messageViewStyle, AnyMessageViewStyle(erasing: style)) + environment(\.messageViewStyle, style) } } @available(iOS 14.0, *) extension EnvironmentValues { - var messageViewStyle: AnyMessageViewStyle { + var messageViewStyle: any POMessageViewStyle { get { self[Key.self] } set { self[Key.self] = newValue } } // MARK: - Private Properties - private struct Key: EnvironmentKey { - static let defaultValue = AnyMessageViewStyle(erasing: .toast) + @MainActor + private struct Key: @preconcurrency EnvironmentKey { + static let defaultValue: any POMessageViewStyle = .toast } } diff --git a/Sources/ProcessOutCoreUI/Sources/DesignSystem/Message/POMessageView.swift b/Sources/ProcessOutCoreUI/Sources/DesignSystem/Message/POMessageView.swift index df735f4fb..03e2bbf3e 100644 --- a/Sources/ProcessOutCoreUI/Sources/DesignSystem/Message/POMessageView.swift +++ b/Sources/ProcessOutCoreUI/Sources/DesignSystem/Message/POMessageView.swift @@ -19,7 +19,7 @@ public struct POMessageView: View { public var body: some View { let configuration = POMessageViewStyleConfiguration(label: Text(message.text), severity: message.severity) - style.makeBody(configuration: configuration) + AnyView(style.makeBody(configuration: configuration)) } // MARK: - Private Properties diff --git a/Sources/ProcessOutCoreUI/Sources/DesignSystem/Message/POMessageViewStyle.swift b/Sources/ProcessOutCoreUI/Sources/DesignSystem/Message/POMessageViewStyle.swift index c35adaaa4..9514cbef0 100644 --- a/Sources/ProcessOutCoreUI/Sources/DesignSystem/Message/POMessageViewStyle.swift +++ b/Sources/ProcessOutCoreUI/Sources/DesignSystem/Message/POMessageViewStyle.swift @@ -7,6 +7,7 @@ import SwiftUI +@MainActor public protocol POMessageViewStyle { /// A view that represents the body of a message. diff --git a/Sources/ProcessOutCoreUI/Sources/DesignSystem/Message/POMessageViewStyleConfiguration.swift b/Sources/ProcessOutCoreUI/Sources/DesignSystem/Message/POMessageViewStyleConfiguration.swift index 26d85c7c2..1df272b90 100644 --- a/Sources/ProcessOutCoreUI/Sources/DesignSystem/Message/POMessageViewStyleConfiguration.swift +++ b/Sources/ProcessOutCoreUI/Sources/DesignSystem/Message/POMessageViewStyleConfiguration.swift @@ -21,3 +21,6 @@ public struct POMessageViewStyleConfiguration { self.severity = severity } } + +@available(*, unavailable) +extension POMessageViewStyleConfiguration: Sendable { } diff --git a/Sources/ProcessOutCoreUI/Sources/DesignSystem/Message/POToastMessageStyle.swift b/Sources/ProcessOutCoreUI/Sources/DesignSystem/Message/POToastMessageStyle.swift index 0449e4a7a..7076e0720 100644 --- a/Sources/ProcessOutCoreUI/Sources/DesignSystem/Message/POToastMessageStyle.swift +++ b/Sources/ProcessOutCoreUI/Sources/DesignSystem/Message/POToastMessageStyle.swift @@ -11,7 +11,7 @@ import SwiftUI public struct POToastMessageStyle: POMessageViewStyle { /// Style for specific severity. - public struct Severity { + public struct Severity: Sendable { /// Icon image. public let icon: Image? diff --git a/Sources/ProcessOutCoreUI/Sources/DesignSystem/Picker/POPicker.swift b/Sources/ProcessOutCoreUI/Sources/DesignSystem/Picker/POPicker.swift index 1b07b1032..ad9a7e6f6 100644 --- a/Sources/ProcessOutCoreUI/Sources/DesignSystem/Picker/POPicker.swift +++ b/Sources/ProcessOutCoreUI/Sources/DesignSystem/Picker/POPicker.swift @@ -23,7 +23,7 @@ public struct POPicker: View { let configuration = POPickerStyleConfiguration( elements: data.map(createConfigurationElement), isInvalid: isInvalid ) - style.makeBody(configuration: configuration) + AnyView(style.makeBody(configuration: configuration)) } // MARK: - Private Properties diff --git a/Sources/ProcessOutCoreUI/Sources/DesignSystem/Picker/POPickerStyle.swift b/Sources/ProcessOutCoreUI/Sources/DesignSystem/Picker/POPickerStyle.swift index 66d4e2eea..58355681f 100644 --- a/Sources/ProcessOutCoreUI/Sources/DesignSystem/Picker/POPickerStyle.swift +++ b/Sources/ProcessOutCoreUI/Sources/DesignSystem/Picker/POPickerStyle.swift @@ -9,7 +9,9 @@ import SwiftUI /// A type that specifies the appearance and interaction of all pickers /// within a view hierarchy. -@_spi(PO) public protocol POPickerStyle { +@_spi(PO) +@MainActor +public protocol POPickerStyle { /// A view representing the appearance and interaction of a `POPicker`. associatedtype Body: View @@ -20,7 +22,8 @@ import SwiftUI @ViewBuilder func makeBody(configuration: POPickerStyleConfiguration) -> Self.Body } -@_spi(PO) public struct POPickerStyleConfiguration { +@_spi(PO) +public struct POPickerStyleConfiguration { /// Picker elements. public let elements: [POPickerStyleConfigurationElement] @@ -29,7 +32,8 @@ import SwiftUI public let isInvalid: Bool } -@_spi(PO) public struct POPickerStyleConfigurationElement: Identifiable { +@_spi(PO) +public struct POPickerStyleConfigurationElement: Identifiable { /// The stable identity of the element. public let id: AnyHashable @@ -44,19 +48,5 @@ import SwiftUI public let select: () -> Void } -struct AnyPickerStyle: POPickerStyle { - - init(erasing style: Style) { - _makeBody = { configuration in - AnyView(style.makeBody(configuration: configuration)) - } - } - - func makeBody(configuration: POPickerStyleConfiguration) -> some View { - _makeBody(configuration) - } - - // MARK: - Private Properties - - private let _makeBody: (POPickerStyleConfiguration) -> AnyView -} +@available(*, unavailable) +extension POPickerStyleConfiguration: Sendable { } diff --git a/Sources/ProcessOutCoreUI/Sources/DesignSystem/Picker/View+PickerStyle.swift b/Sources/ProcessOutCoreUI/Sources/DesignSystem/Picker/View+PickerStyle.swift index 0462a10e0..c5bec397d 100644 --- a/Sources/ProcessOutCoreUI/Sources/DesignSystem/Picker/View+PickerStyle.swift +++ b/Sources/ProcessOutCoreUI/Sources/DesignSystem/Picker/View+PickerStyle.swift @@ -12,22 +12,23 @@ extension View { /// Sets the style for picker views within this view. @_spi(PO) @available(iOS 14, *) - public func pickerStyle(_ style: Style) -> some View { - environment(\.pickerStyle, AnyPickerStyle(erasing: style)) + public func pickerStyle(_ style: any POPickerStyle) -> some View { + environment(\.pickerStyle, style) } } @available(iOS 14, *) extension EnvironmentValues { - var pickerStyle: AnyPickerStyle { + var pickerStyle: any POPickerStyle { get { self[Key.self] } set { self[Key.self] = newValue } } // MARK: - Private Properties - private struct Key: EnvironmentKey { - static let defaultValue = AnyPickerStyle(erasing: .radioGroup) + @MainActor + private struct Key: @preconcurrency EnvironmentKey { + static let defaultValue: any POPickerStyle = .radioGroup } } diff --git a/Sources/ProcessOutCoreUI/Sources/DesignSystem/ProgressView/View+ProgressViewStyle.swift b/Sources/ProcessOutCoreUI/Sources/DesignSystem/ProgressView/View+ProgressViewStyle.swift index 96f5433ca..cd4a511ef 100644 --- a/Sources/ProcessOutCoreUI/Sources/DesignSystem/ProgressView/View+ProgressViewStyle.swift +++ b/Sources/ProcessOutCoreUI/Sources/DesignSystem/ProgressView/View+ProgressViewStyle.swift @@ -11,8 +11,8 @@ extension View { /// Sets the style for progress views in this view. This method should be used when /// specific style type is unknown and there is no possibility to use generic. - @_spi(PO) @available(iOS 14, *) + @_spi(PO) public func poProgressViewStyle(_ style: any ProgressViewStyle) -> some View { AnyView(self.progressViewStyle(style)) } diff --git a/Sources/ProcessOutCoreUI/Sources/DesignSystem/RadioButton/PORadioButtonKnobStateStyle.swift b/Sources/ProcessOutCoreUI/Sources/DesignSystem/RadioButton/PORadioButtonKnobStateStyle.swift index 8e287ec57..2562521c2 100644 --- a/Sources/ProcessOutCoreUI/Sources/DesignSystem/RadioButton/PORadioButtonKnobStateStyle.swift +++ b/Sources/ProcessOutCoreUI/Sources/DesignSystem/RadioButton/PORadioButtonKnobStateStyle.swift @@ -8,7 +8,7 @@ import SwiftUI /// Describes radio button knob style in a particular state. -public struct PORadioButtonKnobStateStyle { +public struct PORadioButtonKnobStateStyle: Sendable { /// Background color. public let backgroundColor: Color diff --git a/Sources/ProcessOutCoreUI/Sources/DesignSystem/RadioButton/PORadioButtonStateStyle.swift b/Sources/ProcessOutCoreUI/Sources/DesignSystem/RadioButton/PORadioButtonStateStyle.swift index ab7501801..81eb5dc08 100644 --- a/Sources/ProcessOutCoreUI/Sources/DesignSystem/RadioButton/PORadioButtonStateStyle.swift +++ b/Sources/ProcessOutCoreUI/Sources/DesignSystem/RadioButton/PORadioButtonStateStyle.swift @@ -8,7 +8,7 @@ import UIKit /// Describes radio button style in a particular state, for example when selected. -public struct PORadioButtonStateStyle { +public struct PORadioButtonStateStyle: Sendable { /// Styling of the radio button knob not including value. public let knob: PORadioButtonKnobStateStyle diff --git a/Sources/ProcessOutCoreUI/Sources/DesignSystem/RadioButton/View+RadioButtonSelected.swift b/Sources/ProcessOutCoreUI/Sources/DesignSystem/RadioButton/View+RadioButtonSelected.swift index a494136de..4622c0c85 100644 --- a/Sources/ProcessOutCoreUI/Sources/DesignSystem/RadioButton/View+RadioButtonSelected.swift +++ b/Sources/ProcessOutCoreUI/Sources/DesignSystem/RadioButton/View+RadioButtonSelected.swift @@ -9,7 +9,8 @@ import SwiftUI extension View { - @_spi(PO) public func radioButtonSelected(_ isSelected: Bool) -> some View { + @_spi(PO) + public func radioButtonSelected(_ isSelected: Bool) -> some View { environment(\.isRadioButtonSelected, isSelected) } } diff --git a/Sources/ProcessOutCoreUI/Sources/DesignSystem/Shadow/POShadowStyle.swift b/Sources/ProcessOutCoreUI/Sources/DesignSystem/Shadow/POShadowStyle.swift index aa82aa14a..046333494 100644 --- a/Sources/ProcessOutCoreUI/Sources/DesignSystem/Shadow/POShadowStyle.swift +++ b/Sources/ProcessOutCoreUI/Sources/DesignSystem/Shadow/POShadowStyle.swift @@ -8,7 +8,7 @@ import SwiftUI /// Style that defines shadow appearance. -public struct POShadowStyle { +public struct POShadowStyle: Sendable { /// The color of the shadow. public let color: Color diff --git a/Sources/ProcessOutCoreUI/Sources/DesignSystem/Spacing/POSpacing.swift b/Sources/ProcessOutCoreUI/Sources/DesignSystem/Spacing/POSpacing.swift index c4d858a3b..41ab25f69 100644 --- a/Sources/ProcessOutCoreUI/Sources/DesignSystem/Spacing/POSpacing.swift +++ b/Sources/ProcessOutCoreUI/Sources/DesignSystem/Spacing/POSpacing.swift @@ -7,7 +7,8 @@ import Foundation -@_spi(PO) public enum POSpacing { +@_spi(PO) +public enum POSpacing { /// Extra small spacing. public static let extraSmall: CGFloat = 4 diff --git a/Sources/ProcessOutCoreUI/Sources/DesignSystem/Text/POTextStyle.swift b/Sources/ProcessOutCoreUI/Sources/DesignSystem/Text/POTextStyle.swift index 205beb3ba..a050991a4 100644 --- a/Sources/ProcessOutCoreUI/Sources/DesignSystem/Text/POTextStyle.swift +++ b/Sources/ProcessOutCoreUI/Sources/DesignSystem/Text/POTextStyle.swift @@ -8,7 +8,7 @@ import SwiftUI /// Text style. -public struct POTextStyle { +public struct POTextStyle: Sendable { /// Text foreground color. public let color: Color diff --git a/Sources/ProcessOutCoreUI/Sources/DesignSystem/TextField/POTextField.swift b/Sources/ProcessOutCoreUI/Sources/DesignSystem/TextField/POTextField.swift index 877a08149..745e66425 100644 --- a/Sources/ProcessOutCoreUI/Sources/DesignSystem/TextField/POTextField.swift +++ b/Sources/ProcessOutCoreUI/Sources/DesignSystem/TextField/POTextField.swift @@ -123,7 +123,7 @@ private struct TextFieldRepresentable: UIViewRepresentable { // MARK: - func willReturn() { - submitAction?() + submitAction() } // MARK: - Private Nested Types diff --git a/Sources/ProcessOutCoreUI/Sources/DesignSystem/Typography/POTypography.swift b/Sources/ProcessOutCoreUI/Sources/DesignSystem/Typography/POTypography.swift index dc37fcf41..4c8af8e94 100644 --- a/Sources/ProcessOutCoreUI/Sources/DesignSystem/Typography/POTypography.swift +++ b/Sources/ProcessOutCoreUI/Sources/DesignSystem/Typography/POTypography.swift @@ -8,7 +8,7 @@ import UIKit /// Holds typesetting information that could be applied to displayed text. -public struct POTypography { +public struct POTypography: Sendable { /// Font assosiated with given typography. public let font: UIFont diff --git a/Sources/ProcessOutCoreUI/Sources/ResourceSymbols/FontResource.swift b/Sources/ProcessOutCoreUI/Sources/ResourceSymbols/FontResource.swift index ff134fc52..ab279ba46 100644 --- a/Sources/ProcessOutCoreUI/Sources/ResourceSymbols/FontResource.swift +++ b/Sources/ProcessOutCoreUI/Sources/ResourceSymbols/FontResource.swift @@ -10,7 +10,7 @@ import UIKit /// A font resource. -struct FontResource { +struct FontResource: Sendable { /// Font resource name. fileprivate let weight: UIFont.Weight diff --git a/Sources/ProcessOutUI/ProcessOutUI.docc/3DS.md b/Sources/ProcessOutUI/ProcessOutUI.docc/3DS.md deleted file mode 100644 index 85513b925..000000000 --- a/Sources/ProcessOutUI/ProcessOutUI.docc/3DS.md +++ /dev/null @@ -1,54 +0,0 @@ -# Getting Started with 3DS - -### 3DS2 - -``POTest3DSService`` emulates the normal 3DS authentication flow but does not actually make any calls to a real -Access Control Server (ACS). It is mainly useful during development in our sandbox testing environment. - -### 3DS Redirect - -``SafariServices/SFSafariViewController/init(redirect:returnUrl:safariConfiguration:completion:)`` allows you to create -a view controller that will automatically redirect user to expected url and collect result. - -```swift -func handle(redirect: PO3DSRedirect, completion: @escaping (Result) -> Void) { - let viewController = SFSafariViewController( - redirect: redirect, - returnUrl: Constants.returnUrl, - completion: { [weak sourceViewController] result in - sourceViewController?.dismiss(animated: true) - completion(result) - } - ) - sourceViewController.present(viewController, animated: true) -} -``` - -When using `SFSafariViewController` your application should support deep links. When -application receives incoming URL you should allow ProcessOut SDK to handle it. For example -if you are using scene delegate it may look like following: - -```swift -func scene(_ scene: UIScene, openURLContexts urlContexts: Set) { - guard let url = urlContexts.first?.url else { - return - } - let isHandled = ProcessOut.shared.processDeepLink(url: url) - print(isHandled) -} -``` - -We also provide ``POWebAuthenticationSession`` that can handle 3DS Redirects and does not depend on the UIKit framework. -This means that the controller can be used in places where a view controller cannot (for example, in SwiftUI -applications). - -```swift -let session = POWebAuthenticationSession( - redirect: redirect, returnUrl: Constants.returnUrl, completion: completion -) -if await session.start() { - return -} -let failure = POFailure(message: "Unable to process redirect", code: .generic(.mobile)) -completion(.failure(failure)) -``` diff --git a/Sources/ProcessOutUI/ProcessOutUI.docc/CardUpdate.md b/Sources/ProcessOutUI/ProcessOutUI.docc/CardUpdate.md index b41262d3d..f407633ea 100644 --- a/Sources/ProcessOutUI/ProcessOutUI.docc/CardUpdate.md +++ b/Sources/ProcessOutUI/ProcessOutUI.docc/CardUpdate.md @@ -34,7 +34,7 @@ let configuration = POCardUpdateConfiguration(cardId: "ID", cardInformation: car ``` During lifecycle view also gives a chance to its delegate to resolve card information dynamically. It calls -``POCardUpdateDelegate/cardInformation(cardId:)-7wujo`` to retrieve needed info. Please note that the method +``POCardUpdateDelegate/cardUpdate(informationFor:)`` to retrieve needed info. Please note that the method is asynchronous. See ``POCardUpdateConfiguration`` reference for other available configurations. @@ -43,12 +43,12 @@ See ``POCardUpdateConfiguration`` reference for other available configurations. In order to tweak view’s styling, you should create an instance of ``POCardUpdateStyle`` structure and pass it to view. The way you set style depends on whether you are using SwiftUI or UIKit bindings. For view controller -pass style instance via init, for SwiftUI view you can use ``SwiftUI/View/cardUpdateStyle(_:)`` modifier. +pass style instance via init, for SwiftUI view you can use ``SwiftUICore/View/cardUpdateStyle(_:)`` modifier. ### Errors Recovery By default, implementation allow user to recover from all errors (not including cancellation). If you wish to -change this behavior, you can implement delegate’s ``POCardUpdateDelegate/shouldContinueUpdate(after:)-3ax8v`` +change this behavior, you can implement delegate’s ``POCardUpdateDelegate/cardUpdate(shouldContinueAfter:)`` method. Where based on a given failure, you should decide if user should be allowed to continue or flow should be ended. @@ -67,7 +67,7 @@ func shouldContinueUpdate(after failure: POFailure) -> Bool { ### Lifecycle Events During lifecycle view emits multiple events that allow to understand user behavior, in order to observe them -implement delegate's ``POCardUpdateDelegate/cardUpdateDidEmitEvent(_:)-5mhya`` method. +implement delegate's ``POCardUpdateDelegate/cardUpdate(didEmitEvent:)`` method. ### Localization diff --git a/Sources/ProcessOutUI/ProcessOutUI.docc/MigrationGuides.md b/Sources/ProcessOutUI/ProcessOutUI.docc/MigrationGuides.md new file mode 100644 index 000000000..53136d98b --- /dev/null +++ b/Sources/ProcessOutUI/ProcessOutUI.docc/MigrationGuides.md @@ -0,0 +1,94 @@ +# Migration Guides + +## Migrating from versions < 5.0.0 + +### 3D Secure + +- Web-based 3DS redirects are now handled internally by the SDK, making the related UI components no longer accessible. +This includes: + + * `PO3DSRedirectController`. + + * `SFSafariViewController` extension for creating it with `PO3DSRedirect`. + + * `POWebAuthenticationSession` extension for creating it with `PO3DSRedirect`. + +- The ``POTest3DSService`` no longer requires a `returnUrl`. However, you must ensure that the return URL assigned to +the invoice is a valid deep link that your application can handle. + +### Alternative Payment Method + +Processing alternative payments using `SFSafariViewController` or `POWebAuthenticationSession` created with +`POAlternativePaymentMethodRequest` is no longer supported. + +Instead, you need to create one of the following requests: `POAlternativePaymentTokenizationRequest` or +`POAlternativePaymentAuthorizationRequest`, and then pass it to the `POAlternativePaymentsService`, which is directly +accessible via `ProcessOut.shared.alternativePayments`. + +### Web Authentication + +The `POWebAuthenticationSession` and the related `POWebAuthenticationSessionCallback` have been removed. These were +previously used to handle 3DS redirects and alternative payments, but this functionality is now part of the internal +implementation and is no longer exposed publicly. + +### Native APM + +- Deprecated aliases have been removed. Please use their counterparts without the `Method` suffix. This change affects: + + * `PONativeAlternativePaymentMethodDelegate` + * `PONativeAlternativePaymentMethodConfiguration` + * `PONativeAlternativePaymentMethodEvent` + +- Deprecated `waitsPaymentConfirmation`, `paymentConfirmationTimeout`, and `paymentConfirmationSecondaryAction` are no +longer part of `PONativeAlternativePaymentConfiguration`. Instead these can now be set and accessed via the +``PONativeAlternativePaymentConfiguration/paymentConfirmation`` object. + +- ``PONativeAlternativePaymentDelegate`` methods have also been updated: + + * `func nativeAlternativePaymentDidEmitEvent(_ event:)` has been replaced by +``PONativeAlternativePaymentDelegate/nativeAlternativePayment(didEmitEvent:)``. + + * `func nativeAlternativePaymentDefaultValues(for:completion:)` has been replaced by +``PONativeAlternativePaymentDelegate/nativeAlternativePayment(defaultsFor:)``. Additionally this method no longer +accepts a completion argument and now relies on structured concurrency. + +- `PONativeAlternativePaymentConfirmation` has been renamed to +``PONativeAlternativePaymentConfiguration/PaymentConfirmation``. + +- `PONativeAlternativePaymentConfiguration/SecondaryAction` has been renamed to +``PONativeAlternativePaymentConfiguration/CancelButton``. + +- The `secondaryAction` property in ``PONativeAlternativePaymentConfiguration`` and +``PONativeAlternativePaymentConfiguration/PaymentConfirmation`` has been renamed to `cancelButton`. + +- `PONativeAlternativePaymentConfiguration/primaryActionTitle` has been renamed to +``PONativeAlternativePaymentConfiguration/primaryButtonTitle``. + +### Card Tokenization + +- The method names in ``POCardTokenizationDelegate`` have been updated: + + * `func cardTokenizationDidEmitEvent(_ event:)` is now ``POCardTokenizationDelegate/cardTokenization(didEmitEvent:)``. + + * `func preferredScheme(issuerInformation:)` is now ``POCardTokenizationDelegate/cardTokenization(preferredSchemeFor:)``. +Additionally, this method now expects a typed card scheme to be returned instead of a raw string. + + * `func shouldContinueTokenization(after:)` is now ``POCardTokenizationDelegate/cardTokenization(shouldContinueAfter:)``. + + * `func processTokenizedCard(card:)` is now ``POCardTokenizationDelegate/cardTokenization(didTokenizeCard:)``. + +- The deprecated `POBillingAddressConfiguration/CollectionMode` has been removed. Instead, use +`POBillingAddressCollectionMode` directly. + +### Card Update + +- The method names in ``POCardUpdateDelegate`` have been updated: + + * `func cardInformation(cardId:)` is now ``POCardUpdateDelegate/cardUpdate(informationFor:)``. + + * `func shouldContinueUpdate(after:)` is now ``POCardUpdateDelegate/cardUpdate(shouldContinueAfter:)``. + + * `func cardUpdateDidEmitEvent(:)` is now ``POCardUpdateDelegate/cardUpdate(didEmitEvent:)``. + +- ``POCardUpdateInformation`` no longer uses raw strings to represent `scheme`, `coScheme`, and `preferredScheme`. These +are now represented by `POCardScheme`. diff --git a/Sources/ProcessOutUI/ProcessOutUI.docc/NativeAlternativePayment.md b/Sources/ProcessOutUI/ProcessOutUI.docc/NativeAlternativePayment.md index 52bc7b7d9..b440089c5 100644 --- a/Sources/ProcessOutUI/ProcessOutUI.docc/NativeAlternativePayment.md +++ b/Sources/ProcessOutUI/ProcessOutUI.docc/NativeAlternativePayment.md @@ -40,7 +40,7 @@ and alter run-time behaviors. In order to tweak view’s styling, you should create an instance of ``PONativeAlternativePaymentStyle`` structure and pass it to view. The way you set style depends on whether you are using SwiftUI or UIKit bindings. For view -controller pass style instance via init, for SwiftUI view you should use ``SwiftUI/View/nativeAlternativePaymentStyle(_:)`` +controller pass style instance via init, for SwiftUI view you should use ``SwiftUICore/View/nativeAlternativePaymentStyle(_:)`` modifier. Let's say you want to change pay button's appearance: diff --git a/Sources/ProcessOutUI/ProcessOutUI.docc/ProcessOutUI.md b/Sources/ProcessOutUI/ProcessOutUI.docc/ProcessOutUI.md index 232008121..73280db10 100644 --- a/Sources/ProcessOutUI/ProcessOutUI.docc/ProcessOutUI.md +++ b/Sources/ProcessOutUI/ProcessOutUI.docc/ProcessOutUI.md @@ -5,13 +5,10 @@ ### Framework - ``ProcessOutUI`` +- -### 3DS +### 3D Secure -- -- ``SafariServices/SFSafariViewController/init(redirect:returnUrl:safariConfiguration:completion:)`` -- ``POWebAuthenticationSession/init(redirect:returnUrl:completion:)`` -- ``PO3DSRedirectController`` - ``POTest3DSService`` ### Card Tokenization @@ -19,7 +16,7 @@ - ``POCardTokenizationView`` - ``POCardTokenizationViewController`` - ``POCardTokenizationStyle`` -- ``SwiftUI/View/cardTokenizationStyle(_:)`` +- ``SwiftUICore/View/cardTokenizationStyle(_:)`` - ``POCardTokenizationConfiguration`` - ``POBillingAddressConfiguration`` - ``POCardTokenizationDelegate`` @@ -36,19 +33,12 @@ - ``POCardUpdateView`` - ``POCardUpdateViewController`` - ``POCardUpdateStyle`` -- ``SwiftUI/View/cardUpdateStyle(_:)`` +- ``SwiftUICore/View/cardUpdateStyle(_:)`` - ``POCardUpdateConfiguration`` - ``POCardUpdateDelegate`` - ``POCardUpdateInformation`` - ``POCardUpdateEvent`` -### Alternative Payment Method - -- ``SafariServices/SFSafariViewController/init(request:returnUrl:safariConfiguration:completion:)`` -- ``SafariServices/SFSafariViewController/init(alternativePaymentMethodUrl:returnUrl:safariConfiguration:completion:)`` -- ``POWebAuthenticationSession/init(alternativePaymentMethodUrl:returnUrl:completion:)`` -- ``POWebAuthenticationSession/init(request:returnUrl:completion:)`` - ### Native Alternative Payment Method - @@ -56,7 +46,7 @@ - ``PONativeAlternativePaymentViewController`` - ``PONativeAlternativePaymentStyle`` - ``PONativeAlternativePaymentBackgroundStyle`` -- ``SwiftUI/View/nativeAlternativePaymentStyle(_:)`` +- ``SwiftUICore/View/nativeAlternativePaymentStyle(_:)`` - ``PONativeAlternativePaymentConfiguration`` - ``PONativeAlternativePaymentConfirmationConfiguration`` - ``PONativeAlternativePaymentDelegate`` @@ -67,7 +57,7 @@ - + @@ -76,11 +66,6 @@ -### Web Authentication - -- ``POWebAuthenticationSession`` -- ``POWebAuthenticationSessionCallback`` - ### Utils - ``POConfirmationDialogConfiguration`` diff --git a/Sources/ProcessOut/Resources/PhoneNumberMetadata/PhoneNumberMetadata.json b/Sources/ProcessOutUI/Resources/PhoneNumberMetadata/PhoneNumberMetadata.json similarity index 100% rename from Sources/ProcessOut/Resources/PhoneNumberMetadata/PhoneNumberMetadata.json rename to Sources/ProcessOutUI/Resources/PhoneNumberMetadata/PhoneNumberMetadata.json diff --git a/Sources/ProcessOut/Resources/PhoneNumberMetadata/PhoneNumberMetadata.version b/Sources/ProcessOutUI/Resources/PhoneNumberMetadata/PhoneNumberMetadata.version similarity index 100% rename from Sources/ProcessOut/Resources/PhoneNumberMetadata/PhoneNumberMetadata.version rename to Sources/ProcessOutUI/Resources/PhoneNumberMetadata/PhoneNumberMetadata.version diff --git a/Sources/ProcessOutUI/Sources/Api/ProcessOutUI.swift b/Sources/ProcessOutUI/Sources/Api/ProcessOutUI.swift index 77835ea8f..ff532c9d1 100644 --- a/Sources/ProcessOutUI/Sources/Api/ProcessOutUI.swift +++ b/Sources/ProcessOutUI/Sources/Api/ProcessOutUI.swift @@ -14,8 +14,10 @@ public enum ProcessOutUI { /// Configures UI package and preloads needed resources. + @MainActor public static func configure() { POTypography.registerFonts() AddressSpecificationProvider.shared.prewarm() + DefaultPhoneNumberMetadataProvider.shared.prewarm() } } diff --git a/Sources/ProcessOutUI/Sources/Api/Test3DS/POTest3DSService.swift b/Sources/ProcessOutUI/Sources/Api/Test3DS/POTest3DSService.swift index dd19cbd82..cca4e2e85 100644 --- a/Sources/ProcessOutUI/Sources/Api/Test3DS/POTest3DSService.swift +++ b/Sources/ProcessOutUI/Sources/Api/Test3DS/POTest3DSService.swift @@ -6,64 +6,46 @@ // import UIKit -@_spi(PO) import ProcessOut +import ProcessOut /// Service that emulates the normal 3DS authentication flow but does not actually make any calls to a real Access /// Control Server (ACS). Should be used only for testing purposes in sandbox environment. public final class POTest3DSService: PO3DSService { - /// Creates service instance. - public init(returnUrl: URL) { - self.returnUrl = returnUrl + public nonisolated init() { + // Ignored } - // MARK: - PO3DSService - - public func authenticationRequest( - configuration: PO3DS2Configuration, - completion: @escaping (Result) -> Void - ) { - let request = PO3DS2AuthenticationRequest( + public func authenticationRequestParameters( + configuration: PO3DS2Configuration + ) async throws -> PO3DS2AuthenticationRequestParameters { + PO3DS2AuthenticationRequestParameters( deviceData: "", sdkAppId: "", sdkEphemeralPublicKey: "{}", sdkReferenceNumber: "", sdkTransactionId: "" ) - completion(.success(request)) } - public func handle(challenge: PO3DS2Challenge, completion: @escaping (Result) -> Void) { + @MainActor + public func performChallenge(with parameters: PO3DS2ChallengeParameters) async throws -> PO3DS2ChallengeResult { guard let presentingViewController = PresentingViewControllerProvider.find() else { - completion(.success(false)) - return - } - let alertController = UIAlertController( - title: String(resource: .Test3DS.title), message: "", preferredStyle: .alert - ) - let acceptAction = UIAlertAction(title: String(resource: .Test3DS.accept), style: .default) { _ in - completion(.success(true)) + throw POFailure(code: .generic(.mobile)) } - alertController.addAction(acceptAction) - let rejectAction = UIAlertAction(title: String(resource: .Test3DS.reject), style: .default) { _ in - completion(.success(false)) - } - alertController.addAction(rejectAction) - presentingViewController.present(alertController, animated: true) - } - - public func handle(redirect: PO3DSRedirect, completion: @escaping (Result) -> Void) { - Task { @MainActor in - let session = POWebAuthenticationSession(redirect: redirect, returnUrl: returnUrl, completion: completion) - if await session.start() { - return + return await withCheckedContinuation { continuation in + let alertController = UIAlertController( + title: String(resource: .Test3DS.title), message: "", preferredStyle: .alert + ) + let acceptAction = UIAlertAction(title: String(resource: .Test3DS.accept), style: .default) { _ in + continuation.resume(returning: PO3DS2ChallengeResult(transactionStatus: true)) + } + alertController.addAction(acceptAction) + let rejectAction = UIAlertAction(title: String(resource: .Test3DS.reject), style: .default) { _ in + continuation.resume(returning: PO3DS2ChallengeResult(transactionStatus: false)) } - let failure = POFailure(message: "Unable to process redirect", code: .generic(.mobile)) - completion(.failure(failure)) + alertController.addAction(rejectAction) + presentingViewController.present(alertController, animated: true) } } - - // MARK: - Private Properties - - private let returnUrl: URL } diff --git a/Sources/ProcessOutUI/Sources/Api/Test3DS/StringResource+Test3DS.swift b/Sources/ProcessOutUI/Sources/Api/Test3DS/StringResource+Test3DS.swift index 52eee52d4..860a86f38 100644 --- a/Sources/ProcessOutUI/Sources/Api/Test3DS/StringResource+Test3DS.swift +++ b/Sources/ProcessOutUI/Sources/Api/Test3DS/StringResource+Test3DS.swift @@ -5,19 +5,17 @@ // Created by Andrii Vysotskyi on 21.11.2023. // -@_spi(PO) import ProcessOut - -extension POStringResource { +extension StringResource { enum Test3DS { /// 3DS challenge title. - static let title = POStringResource("test-3ds.challenge.title", comment: "") + static let title = StringResource("test-3ds.challenge.title", comment: "") /// Accept button title. - static let accept = POStringResource("test-3ds.challenge.accept", comment: "") + static let accept = StringResource("test-3ds.challenge.accept", comment: "") /// Reject button title. - static let reject = POStringResource("test-3ds.challenge.reject", comment: "") + static let reject = StringResource("test-3ds.challenge.reject", comment: "") } } diff --git a/Sources/ProcessOut/Sources/Core/Extensions/String+Extensions.swift b/Sources/ProcessOutUI/Sources/Core/Extensions/String+Extensions.swift similarity index 100% rename from Sources/ProcessOut/Sources/Core/Extensions/String+Extensions.swift rename to Sources/ProcessOutUI/Sources/Core/Extensions/String+Extensions.swift diff --git a/Sources/ProcessOut/Sources/Core/Extensions/StringProtocol+Extensions.swift b/Sources/ProcessOutUI/Sources/Core/Extensions/StringProtocol+Extensions.swift similarity index 89% rename from Sources/ProcessOut/Sources/Core/Extensions/StringProtocol+Extensions.swift rename to Sources/ProcessOutUI/Sources/Core/Extensions/StringProtocol+Extensions.swift index 4d3ddd554..8a34e2fe4 100644 --- a/Sources/ProcessOut/Sources/Core/Extensions/StringProtocol+Extensions.swift +++ b/Sources/ProcessOutUI/Sources/Core/Extensions/StringProtocol+Extensions.swift @@ -10,7 +10,7 @@ import Foundation extension StringProtocol { /// A new string made from the string by removing all character in given `characterSet`. - @_spi(PO) public func removingCharacters(in characterSet: CharacterSet) -> String { + func removingCharacters(in characterSet: CharacterSet) -> String { String(String.UnicodeScalarView(unicodeScalars.filter(characterSet.inverted.contains))) } diff --git a/Sources/ProcessOut/Sources/Core/Formatters/CardExpiration/POCardExpirationFormatter.swift b/Sources/ProcessOutUI/Sources/Core/Formatters/CardExpiration/CardExpirationFormatter.swift similarity index 90% rename from Sources/ProcessOut/Sources/Core/Formatters/CardExpiration/POCardExpirationFormatter.swift rename to Sources/ProcessOutUI/Sources/Core/Formatters/CardExpiration/CardExpirationFormatter.swift index 63fec2006..0089eb0ff 100644 --- a/Sources/ProcessOut/Sources/Core/Formatters/CardExpiration/POCardExpirationFormatter.swift +++ b/Sources/ProcessOutUI/Sources/Core/Formatters/CardExpiration/CardExpirationFormatter.swift @@ -1,5 +1,5 @@ // -// POCardExpirationFormatter.swift +// CardExpirationFormatter.swift // ProcessOut // // Created by Andrii Vysotskyi on 21.07.2023. @@ -7,9 +7,9 @@ import Foundation -@_spi(PO) public final class POCardExpirationFormatter: Formatter { +final class CardExpirationFormatter: Formatter { - override public init() { + override init() { regexProvider = RegexProvider.shared super.init() } @@ -20,7 +20,7 @@ import Foundation } /// Returns formatted version of given expiration string. - public func string(from string: String) -> String { + func string(from string: String) -> String { let expiration = self.expiration(from: string) guard !expiration.month.isEmpty else { return "" @@ -28,7 +28,7 @@ import Foundation return formatted(month: expiration.month, year: expiration.year) } - public func expirationMonth(from string: String) -> Int? { + func expirationMonth(from string: String) -> Int? { let monthDescription = expiration(from: string).month guard let month = Int(monthDescription), month > 0, month <= 12 else { return nil @@ -36,21 +36,21 @@ import Foundation return month } - public func expirationYear(from string: String) -> Int? { + func expirationYear(from string: String) -> Int? { let yearDescription = expiration(from: string).year return Int(yearDescription) } // MARK: - Formatter - override public func string(for obj: Any?) -> String? { + override func string(for obj: Any?) -> String? { guard let expiration = obj as? String else { return nil } return string(from: expiration) } - override public func isPartialStringValid( + override func isPartialStringValid( _ partialStringPtr: AutoreleasingUnsafeMutablePointer, // swiftlint:disable:this legacy_objc_type proposedSelectedRange proposedSelRangePtr: NSRangePointer?, originalString origString: String, @@ -59,7 +59,7 @@ import Foundation ) -> Bool { let partialString = partialStringPtr.pointee as String let formatted = string(from: partialString) - let adjustedOffset = POFormattingUtils.adjustedCursorOffset( + let adjustedOffset = FormattingUtils.adjustedCursorOffset( in: formatted, source: partialString, // swiftlint:disable:next line_length diff --git a/Sources/ProcessOut/Sources/Core/Formatters/CardNumber/POCardNumberFormatter.swift b/Sources/ProcessOutUI/Sources/Core/Formatters/CardNumber/CardNumberFormatter.swift similarity index 92% rename from Sources/ProcessOut/Sources/Core/Formatters/CardNumber/POCardNumberFormatter.swift rename to Sources/ProcessOutUI/Sources/Core/Formatters/CardNumber/CardNumberFormatter.swift index 3dad1dff5..af0902cad 100644 --- a/Sources/ProcessOut/Sources/Core/Formatters/CardNumber/POCardNumberFormatter.swift +++ b/Sources/ProcessOutUI/Sources/Core/Formatters/CardNumber/CardNumberFormatter.swift @@ -1,5 +1,5 @@ // -// POCardNumberFormatter.swift +// CardNumberFormatter.swift // ProcessOut // // Created by Andrii Vysotskyi on 18.07.2023. @@ -7,9 +7,9 @@ import Foundation -@_spi(PO) public final class POCardNumberFormatter: Formatter { +final class CardNumberFormatter: Formatter { - public func string(from partialNumber: String) -> String { + func string(from partialNumber: String) -> String { let normalizedNumber = normalized(number: partialNumber).prefix(Constants.maxLength) for format in formats { if let formattedNumber = attemptToFormat(cardNumber: normalizedNumber, format: format) { @@ -19,20 +19,20 @@ import Foundation return attemptToFormat(cardNumber: normalizedNumber, pattern: Constants.defaultPattern) ?? partialNumber } - public func normalized(number: String) -> String { + func normalized(number: String) -> String { number.removingCharacters(in: Constants.significantCharacters.inverted) } // MARK: - Formatter - override public func string(for obj: Any?) -> String? { + override func string(for obj: Any?) -> String? { guard let cardNumber = obj as? String else { return nil } return string(from: cardNumber) } - override public func isPartialStringValid( + override func isPartialStringValid( _ partialStringPtr: AutoreleasingUnsafeMutablePointer, // swiftlint:disable:this legacy_objc_type proposedSelectedRange proposedSelRangePtr: NSRangePointer?, originalString origString: String, @@ -41,7 +41,7 @@ import Foundation ) -> Bool { let partialString = partialStringPtr.pointee as String let formatted = string(from: partialString) - let adjustedOffset = POFormattingUtils.adjustedCursorOffset( + let adjustedOffset = FormattingUtils.adjustedCursorOffset( in: formatted, source: partialString, // swiftlint:disable:next line_length diff --git a/Sources/ProcessOutUI/Sources/Core/Formatters/CardSecurityCode/CardSecurityCodeFormatter.swift b/Sources/ProcessOutUI/Sources/Core/Formatters/CardSecurityCode/CardSecurityCodeFormatter.swift index 9b7b70a7d..d97dc0cce 100644 --- a/Sources/ProcessOutUI/Sources/Core/Formatters/CardSecurityCode/CardSecurityCodeFormatter.swift +++ b/Sources/ProcessOutUI/Sources/Core/Formatters/CardSecurityCode/CardSecurityCodeFormatter.swift @@ -6,7 +6,7 @@ // import Foundation -@_spi(PO) import ProcessOut +import ProcessOut final class CardSecurityCodeFormatter: Formatter { @@ -44,7 +44,7 @@ final class CardSecurityCodeFormatter: Formatter { ) -> Bool { let partialString = partialStringPtr.pointee as String let formatted = string(from: partialString) - let adjustedOffset = POFormattingUtils.adjustedCursorOffset( + let adjustedOffset = FormattingUtils.adjustedCursorOffset( in: formatted, source: partialString, // swiftlint:disable:next line_length diff --git a/Sources/ProcessOut/Sources/Core/Formatters/PhoneNumber/MetadataProvider/PODefaultPhoneNumberMetadataProvider.swift b/Sources/ProcessOutUI/Sources/Core/Formatters/PhoneNumber/MetadataProvider/DefaultPhoneNumberMetadataProvider.swift similarity index 54% rename from Sources/ProcessOut/Sources/Core/Formatters/PhoneNumber/MetadataProvider/PODefaultPhoneNumberMetadataProvider.swift rename to Sources/ProcessOutUI/Sources/Core/Formatters/PhoneNumber/MetadataProvider/DefaultPhoneNumberMetadataProvider.swift index 7be0c4446..cb0e1b3a9 100644 --- a/Sources/ProcessOut/Sources/Core/Formatters/PhoneNumber/MetadataProvider/PODefaultPhoneNumberMetadataProvider.swift +++ b/Sources/ProcessOutUI/Sources/Core/Formatters/PhoneNumber/MetadataProvider/DefaultPhoneNumberMetadataProvider.swift @@ -1,38 +1,37 @@ // -// PODefaultPhoneNumberMetadataProvider.swift +// DefaultPhoneNumberMetadataProvider.swift // ProcessOut // // Created by Andrii Vysotskyi on 16.03.2023. // import Foundation +@_spi(PO) import ProcessOut -@_spi(PO) public final class PODefaultPhoneNumberMetadataProvider: POPhoneNumberMetadataProvider { +final class DefaultPhoneNumberMetadataProvider: PhoneNumberMetadataProvider { - public static let shared = PODefaultPhoneNumberMetadataProvider() + static let shared = DefaultPhoneNumberMetadataProvider() /// - NOTE: Method is asynchronous. - public func prewarm() { + func prewarm() { loadMetadata(sync: false) } // MARK: - PhoneNumberMetadataProvider - public func metadata(for countryCode: String) -> POPhoneNumberMetadata? { + func metadata(for countryCode: String) -> PhoneNumberMetadata? { let transformedCountryCode = countryCode.applyingTransform(.toLatin, reverse: false) ?? countryCode - if let metadata = metadata { + if let metadata = metadata.wrappedValue { return metadata[transformedCountryCode] } loadMetadata(sync: true) - return metadata?[transformedCountryCode] + return metadata.wrappedValue?[transformedCountryCode] } // MARK: - Private Properties private let dispatchQueue: DispatchQueue - - @POUnfairlyLocked - private var metadata: [String: POPhoneNumberMetadata]? + private let metadata = POUnfairlyLocked<[String: PhoneNumberMetadata]?>(wrappedValue: nil) // MARK: - Private Methods @@ -42,22 +41,25 @@ import Foundation private func loadMetadata(sync: Bool) { let dispatchWorkItem = DispatchWorkItem { [weak self] in - guard let self, self.metadata == nil else { + guard let self, self.metadata.wrappedValue == nil else { return } - let groupedMetadata: [String: POPhoneNumberMetadata] + let groupedMetadata: [String: PhoneNumberMetadata] do { - let data = try Data(contentsOf: Files.phoneNumberMetadata.url) - let metadata = try JSONDecoder().decode([POPhoneNumberMetadata].self, from: data) + let data = try Data( + // swiftlint:disable:next force_unwrapping + contentsOf: BundleLocator.bundle.url(forResource: "PhoneNumberMetadata", withExtension: "json")! + ) + let metadata = try JSONDecoder().decode([PhoneNumberMetadata].self, from: data) groupedMetadata = Dictionary(grouping: metadata, by: \.countryCode).compactMapValues { values in let countryCode = values.first!.countryCode // swiftlint:disable:this force_unwrapping - return POPhoneNumberMetadata(countryCode: countryCode, formats: values.flatMap(\.formats)) + return PhoneNumberMetadata(countryCode: countryCode, formats: values.flatMap(\.formats)) } } catch { assertionFailure("Failed to load metadata: \(error)") groupedMetadata = [:] } - self.$metadata.withLock { $0 = groupedMetadata } + self.metadata.withLock { $0 = groupedMetadata } } let executor = sync ? dispatchQueue.sync : dispatchQueue.async executor(dispatchWorkItem) diff --git a/Sources/ProcessOutUI/Sources/Core/Formatters/PhoneNumber/MetadataProvider/PhoneNumberFormat.swift b/Sources/ProcessOutUI/Sources/Core/Formatters/PhoneNumber/MetadataProvider/PhoneNumberFormat.swift new file mode 100644 index 000000000..dd7213a83 --- /dev/null +++ b/Sources/ProcessOutUI/Sources/Core/Formatters/PhoneNumber/MetadataProvider/PhoneNumberFormat.swift @@ -0,0 +1,18 @@ +// +// PhoneNumberFormat.swift +// ProcessOut +// +// Created by Andrii Vysotskyi on 16.03.2023. +// + +struct PhoneNumberFormat: Decodable, Sendable { + + /// Formatting patern. + let pattern: String + + /// Leading digits pattern. + let leading: [String] + + /// Format to use for number. + let format: String +} diff --git a/Sources/ProcessOutUI/Sources/Core/Formatters/PhoneNumber/MetadataProvider/PhoneNumberMetadata.swift b/Sources/ProcessOutUI/Sources/Core/Formatters/PhoneNumber/MetadataProvider/PhoneNumberMetadata.swift new file mode 100644 index 000000000..df627968f --- /dev/null +++ b/Sources/ProcessOutUI/Sources/Core/Formatters/PhoneNumber/MetadataProvider/PhoneNumberMetadata.swift @@ -0,0 +1,15 @@ +// +// PhoneNumberMetadata.swift +// ProcessOut +// +// Created by Andrii Vysotskyi on 16.03.2023. +// + +struct PhoneNumberMetadata: Decodable, Sendable { + + /// Country code. + let countryCode: String + + /// Available formats. + let formats: [PhoneNumberFormat] +} diff --git a/Sources/ProcessOutUI/Sources/Core/Formatters/PhoneNumber/MetadataProvider/PhoneNumberMetadataProvider.swift b/Sources/ProcessOutUI/Sources/Core/Formatters/PhoneNumber/MetadataProvider/PhoneNumberMetadataProvider.swift new file mode 100644 index 000000000..fbf4a204e --- /dev/null +++ b/Sources/ProcessOutUI/Sources/Core/Formatters/PhoneNumber/MetadataProvider/PhoneNumberMetadataProvider.swift @@ -0,0 +1,12 @@ +// +// PhoneNumberMetadataProvider.swift +// ProcessOut +// +// Created by Andrii Vysotskyi on 23.03.2023. +// + +protocol PhoneNumberMetadataProvider: Sendable { + + /// Returns metadata for given country code if any. + func metadata(for countryCode: String) -> PhoneNumberMetadata? +} diff --git a/Sources/ProcessOut/Sources/Core/Formatters/PhoneNumber/POPhoneNumberFormatter.swift b/Sources/ProcessOutUI/Sources/Core/Formatters/PhoneNumber/PhoneNumberFormatter.swift similarity index 88% rename from Sources/ProcessOut/Sources/Core/Formatters/PhoneNumber/POPhoneNumberFormatter.swift rename to Sources/ProcessOutUI/Sources/Core/Formatters/PhoneNumber/PhoneNumberFormatter.swift index e0d4022c3..464d1c523 100644 --- a/Sources/ProcessOut/Sources/Core/Formatters/PhoneNumber/POPhoneNumberFormatter.swift +++ b/Sources/ProcessOutUI/Sources/Core/Formatters/PhoneNumber/PhoneNumberFormatter.swift @@ -1,5 +1,5 @@ // -// POPhoneNumberFormatter.swift +// PhoneNumberFormatter.swift // ProcessOut // // Created by Andrii Vysotskyi on 16.03.2023. @@ -7,9 +7,9 @@ import Foundation -@_spi(PO) public final class POPhoneNumberFormatter: Formatter { +final class PhoneNumberFormatter: Formatter { - public init(metadataProvider: POPhoneNumberMetadataProvider = PODefaultPhoneNumberMetadataProvider.shared) { + init(metadataProvider: PhoneNumberMetadataProvider = DefaultPhoneNumberMetadataProvider.shared) { regexProvider = RegexProvider.shared self.metadataProvider = metadataProvider super.init() @@ -20,7 +20,7 @@ import Foundation fatalError("init(coder:) has not been implemented") } - public func string(from partialNumber: String) -> String { + func string(from partialNumber: String) -> String { let normalizedNumber = normalized(number: partialNumber) guard !normalizedNumber.isEmpty else { return "" @@ -34,7 +34,7 @@ import Foundation !number.isEmpty else { return formatted(countryCode: metadata.countryCode, formattedNationalNumber: number) } - var potentialFormats: [POPhoneNumberFormat] = [] + var potentialFormats: [PhoneNumberFormat] = [] if let formatted = attemptToFormat( nationalNumber: number, metadata: metadata, potentialFormats: &potentialFormats ) { @@ -50,20 +50,20 @@ import Foundation return formatted(countryCode: metadata.countryCode, formattedNationalNumber: number) } - public func normalized(number: String) -> String { + func normalized(number: String) -> String { number.removingCharacters(in: Constants.significantCharacters.inverted) } // MARK: - Formatter - override public func string(for obj: Any?) -> String? { + override func string(for obj: Any?) -> String? { guard let phoneNumber = obj as? String else { return nil } return string(from: phoneNumber) } - override public func isPartialStringValid( + override func isPartialStringValid( _ partialStringPtr: AutoreleasingUnsafeMutablePointer, // swiftlint:disable:this legacy_objc_type proposedSelectedRange proposedSelRangePtr: NSRangePointer?, originalString origString: String, @@ -72,7 +72,7 @@ import Foundation ) -> Bool { let partialString = partialStringPtr.pointee as String let formatted = string(from: partialString) - let adjustedOffset = POFormattingUtils.adjustedCursorOffset( + let adjustedOffset = FormattingUtils.adjustedCursorOffset( in: formatted, source: partialString, // swiftlint:disable:next line_length @@ -101,12 +101,12 @@ import Foundation // MARK: - Private Properties private let regexProvider: RegexProvider - private let metadataProvider: POPhoneNumberMetadataProvider + private let metadataProvider: PhoneNumberMetadataProvider // MARK: - Full National Number Formatting private func attemptToFormat( - nationalNumber: String, metadata: POPhoneNumberMetadata, potentialFormats: inout [POPhoneNumberFormat] + nationalNumber: String, metadata: PhoneNumberMetadata, potentialFormats: inout [PhoneNumberFormat] ) -> String? { let range = NSRange(nationalNumber.startIndex ..< nationalNumber.endIndex, in: nationalNumber) for format in metadata.formats { @@ -123,7 +123,7 @@ import Foundation return nil } - private func shouldAttemptUsing(format: POPhoneNumberFormat, nationalNumber: String, range: NSRange) -> Bool { + private func shouldAttemptUsing(format: PhoneNumberFormat, nationalNumber: String, range: NSRange) -> Bool { for pattern in format.leading { guard let regex = regexProvider.regex(with: pattern) else { continue @@ -135,7 +135,7 @@ import Foundation return false } - private func formatted(nationalNumber: String, countryCode: String, format: POPhoneNumberFormat) -> String { + private func formatted(nationalNumber: String, countryCode: String, format: PhoneNumberFormat) -> String { let formattedNationalNumber: String if let formatRegex = regexProvider.regex(with: format.pattern) { let range = NSRange(nationalNumber.startIndex ..< nationalNumber.endIndex, in: nationalNumber) @@ -151,7 +151,7 @@ import Foundation // MARK: - Partial National Number Formatting private func attemptToFormat( - partialNationalNumber: String, formats: [POPhoneNumberFormat], countryCode: String + partialNationalNumber: String, formats: [PhoneNumberFormat], countryCode: String ) -> String? { let nationalNumber = partialNationalNumber.appending( String( @@ -187,7 +187,7 @@ import Foundation // MARK: - Utils - private func extractMetadata(number: inout String) -> POPhoneNumberMetadata? { + private func extractMetadata(number: inout String) -> PhoneNumberMetadata? { let length = min(Constants.maxCountryPrefixLength, number.count) for i in stride(from: 1, through: length, by: 1) { // swiftlint:disable:this identifier_name let potentialCountryCode = String(number.prefix(i)) diff --git a/Sources/ProcessOut/Sources/Core/RegexProvider/RegexProvider.swift b/Sources/ProcessOutUI/Sources/Core/Formatters/RegexProvider/RegexProvider.swift similarity index 87% rename from Sources/ProcessOut/Sources/Core/RegexProvider/RegexProvider.swift rename to Sources/ProcessOutUI/Sources/Core/Formatters/RegexProvider/RegexProvider.swift index a889270fa..e6196dc3a 100644 --- a/Sources/ProcessOut/Sources/Core/RegexProvider/RegexProvider.swift +++ b/Sources/ProcessOutUI/Sources/Core/Formatters/RegexProvider/RegexProvider.swift @@ -9,7 +9,7 @@ import Foundation // swiftlint:disable legacy_objc_type -final class RegexProvider { +final class RegexProvider: Sendable { static let shared = RegexProvider() @@ -35,7 +35,7 @@ final class RegexProvider { // MARK: - Private Properties - private let cache: NSCache + private nonisolated(unsafe) let cache: NSCache } // swiftlint:enable legacy_objc_type diff --git a/Sources/ProcessOut/Sources/Core/Formatters/Utils/POFormattingUtils.swift b/Sources/ProcessOutUI/Sources/Core/Formatters/Utils/FormattingUtils.swift similarity index 95% rename from Sources/ProcessOut/Sources/Core/Formatters/Utils/POFormattingUtils.swift rename to Sources/ProcessOutUI/Sources/Core/Formatters/Utils/FormattingUtils.swift index b30ec6c1f..696c59c42 100644 --- a/Sources/ProcessOut/Sources/Core/Formatters/Utils/POFormattingUtils.swift +++ b/Sources/ProcessOutUI/Sources/Core/Formatters/Utils/FormattingUtils.swift @@ -1,5 +1,5 @@ // -// POFormattingUtils.swift +// FormattingUtils.swift // ProcessOut // // Created by Andrii Vysotskyi on 12.05.2023. @@ -7,7 +7,7 @@ import Foundation -@_spi(PO) public enum POFormattingUtils { +enum FormattingUtils { /// Returns index in formatted string that matches index in `string`. /// @@ -17,7 +17,7 @@ import Foundation /// Alternative solution would be to compare all substrings starting from end of `target` with suffix /// after cursor in `source` and finding substring with least possible difference (using for example Levenshtein /// distance). Downside of it would be almost cubic complexity. - public static func adjustedCursorOffset( + static func adjustedCursorOffset( in target: String, source: String, sourceCursorOffset: Int, diff --git a/Sources/ProcessOutUI/Sources/Core/Interactor/BaseInteractor.swift b/Sources/ProcessOutUI/Sources/Core/Interactor/BaseInteractor.swift index b9517c273..03dacbb6c 100644 --- a/Sources/ProcessOutUI/Sources/Core/Interactor/BaseInteractor.swift +++ b/Sources/ProcessOutUI/Sources/Core/Interactor/BaseInteractor.swift @@ -23,7 +23,6 @@ class BaseInteractor: Interactor { var didChange: (() -> Void)? var willChange: ((State) -> Void)? - @MainActor func start() { // Does nothing } diff --git a/Sources/ProcessOutUI/Sources/Core/Interactor/Interactor.swift b/Sources/ProcessOutUI/Sources/Core/Interactor/Interactor.swift index edc80e604..ddaf04a36 100644 --- a/Sources/ProcessOutUI/Sources/Core/Interactor/Interactor.swift +++ b/Sources/ProcessOutUI/Sources/Core/Interactor/Interactor.swift @@ -5,6 +5,7 @@ // Created by Andrii Vysotskyi on 19.10.2023. // +@MainActor protocol Interactor: AnyObject { associatedtype State diff --git a/Sources/ProcessOutUI/Sources/Core/Providers/AddressSpecification/AddressSpecification+StringResource.swift b/Sources/ProcessOutUI/Sources/Core/Providers/AddressSpecification/AddressSpecification+StringResource.swift index 495122f9c..04b1fb9ae 100644 --- a/Sources/ProcessOutUI/Sources/Core/Providers/AddressSpecification/AddressSpecification+StringResource.swift +++ b/Sources/ProcessOutUI/Sources/Core/Providers/AddressSpecification/AddressSpecification+StringResource.swift @@ -6,16 +6,15 @@ // import Foundation -@_spi(PO) import ProcessOut extension AddressSpecification.CityUnit { - var stringResource: POStringResource { - let resources: [Self: POStringResource] = [ - .city: POStringResource("address-spec.city", comment: ""), - .district: POStringResource("address-spec.district", comment: ""), - .postTown: POStringResource("address-spec.post-town", comment: ""), - .suburb: POStringResource("address-spec.suburb", comment: "") + var stringResource: StringResource { + let resources: [Self: StringResource] = [ + .city: StringResource("address-spec.city", comment: ""), + .district: StringResource("address-spec.district", comment: ""), + .postTown: StringResource("address-spec.post-town", comment: ""), + .suburb: StringResource("address-spec.suburb", comment: "") ] return resources[self]! // swiftlint:disable:this force_unwrapping } @@ -23,19 +22,19 @@ extension AddressSpecification.CityUnit { extension AddressSpecification.StateUnit { - var stringResource: POStringResource { - let resources: [Self: POStringResource] = [ - .area: POStringResource("address-spec.area", comment: ""), - .county: POStringResource("address-spec.county", comment: ""), - .department: POStringResource("address-spec.department", comment: ""), - .doSi: POStringResource("address-spec.do-si", comment: ""), - .emirate: POStringResource("address-spec.emirate", comment: ""), - .island: POStringResource("address-spec.island", comment: ""), - .oblast: POStringResource("address-spec.oblast", comment: ""), - .parish: POStringResource("address-spec.parish", comment: ""), - .prefecture: POStringResource("address-spec.prefecture", comment: ""), - .province: POStringResource("address-spec.province", comment: ""), - .state: POStringResource("address-spec.state", comment: "") + var stringResource: StringResource { + let resources: [Self: StringResource] = [ + .area: StringResource("address-spec.area", comment: ""), + .county: StringResource("address-spec.county", comment: ""), + .department: StringResource("address-spec.department", comment: ""), + .doSi: StringResource("address-spec.do-si", comment: ""), + .emirate: StringResource("address-spec.emirate", comment: ""), + .island: StringResource("address-spec.island", comment: ""), + .oblast: StringResource("address-spec.oblast", comment: ""), + .parish: StringResource("address-spec.parish", comment: ""), + .prefecture: StringResource("address-spec.prefecture", comment: ""), + .province: StringResource("address-spec.province", comment: ""), + .state: StringResource("address-spec.state", comment: "") ] return resources[self]! // swiftlint:disable:this force_unwrapping } @@ -43,12 +42,12 @@ extension AddressSpecification.StateUnit { extension AddressSpecification.PostcodeUnit { - var stringResource: POStringResource { - let resources: [Self: POStringResource] = [ - .postcode: POStringResource("address-spec.postcode", comment: ""), - .eircode: POStringResource("address-spec.eircode", comment: ""), - .pin: POStringResource("address-spec.pin", comment: ""), - .zip: POStringResource("address-spec.zip", comment: "") + var stringResource: StringResource { + let resources: [Self: StringResource] = [ + .postcode: StringResource("address-spec.postcode", comment: ""), + .eircode: StringResource("address-spec.eircode", comment: ""), + .pin: StringResource("address-spec.pin", comment: ""), + .zip: StringResource("address-spec.zip", comment: "") ] return resources[self]! // swiftlint:disable:this force_unwrapping } diff --git a/Sources/ProcessOutUI/Sources/Core/Providers/AddressSpecification/AddressSpecification.swift b/Sources/ProcessOutUI/Sources/Core/Providers/AddressSpecification/AddressSpecification.swift index 9146443c0..c86aeba78 100644 --- a/Sources/ProcessOutUI/Sources/Core/Providers/AddressSpecification/AddressSpecification.swift +++ b/Sources/ProcessOutUI/Sources/Core/Providers/AddressSpecification/AddressSpecification.swift @@ -5,25 +5,25 @@ // Created by Andrii Vysotskyi on 26.10.2023. // -struct AddressSpecification { +struct AddressSpecification: Sendable { - enum Unit: String, CaseIterable, Decodable { + enum Unit: String, CaseIterable, Decodable, Sendable { case street, city, state, postcode } - enum CityUnit: String, Decodable { + enum CityUnit: String, Decodable, Sendable { case city, district, postTown, suburb } - enum StateUnit: String, Decodable { + enum StateUnit: String, Decodable, Sendable { case area, county, department, doSi, emirate, island, oblast, parish, prefecture, province, state } - enum PostcodeUnit: String, Decodable { + enum PostcodeUnit: String, Decodable, Sendable { case postcode, eircode, pin, zip } - struct State: Decodable { + struct State: Decodable, Sendable { let abbreviation, name: String } diff --git a/Sources/ProcessOutUI/Sources/Core/Providers/AddressSpecification/AddressSpecificationProvider.swift b/Sources/ProcessOutUI/Sources/Core/Providers/AddressSpecification/AddressSpecificationProvider.swift index ad8a8027c..8194cd93a 100644 --- a/Sources/ProcessOutUI/Sources/Core/Providers/AddressSpecification/AddressSpecificationProvider.swift +++ b/Sources/ProcessOutUI/Sources/Core/Providers/AddressSpecification/AddressSpecificationProvider.swift @@ -8,7 +8,7 @@ import Foundation @_spi(PO) import ProcessOut -final class AddressSpecificationProvider { +final class AddressSpecificationProvider: Sendable { static let shared = AddressSpecificationProvider() @@ -20,9 +20,9 @@ final class AddressSpecificationProvider { // MARK: - AddressSpecificationProvider /// Returns supported country codes. - private(set) lazy var countryCodes: [String] = { + var countryCodes: [String] { Array(loadSpecifications().keys) - }() + } /// Returns address spec for given country code or default if country is unknown. func specification(for countryCode: String) -> AddressSpecification { @@ -37,8 +37,7 @@ final class AddressSpecificationProvider { // MARK: - Private Properties - @POUnfairlyLocked - private var specifications: [String: AddressSpecification]? + private let specifications = POUnfairlyLocked<[String: AddressSpecification]?>(wrappedValue: nil) // MARK: - Private Methods @@ -48,7 +47,7 @@ final class AddressSpecificationProvider { @discardableResult private func loadSpecifications() -> [String: AddressSpecification] { - $specifications.withLock { specifications in + specifications.withLock { specifications in if let specifications { return specifications } diff --git a/Sources/ProcessOutUI/Sources/Core/Providers/CardScheme/CardSchemeProvider.swift b/Sources/ProcessOutUI/Sources/Core/Providers/CardScheme/CardSchemeProvider.swift index e72b28ee6..8ab4eea6b 100644 --- a/Sources/ProcessOutUI/Sources/Core/Providers/CardScheme/CardSchemeProvider.swift +++ b/Sources/ProcessOutUI/Sources/Core/Providers/CardScheme/CardSchemeProvider.swift @@ -9,15 +9,15 @@ import Foundation @_spi(PO) import ProcessOut // todo(andrii-vysotskyi): support more schemes -final class CardSchemeProvider { +final class CardSchemeProvider: Sendable { - struct Issuer { + struct Issuer: Sendable { let scheme: POCardScheme let numbers: IssuerNumbers let length: Int } - enum IssuerNumbers { + enum IssuerNumbers: Sendable { case range(ClosedRange), exact(Int), set(Set) } diff --git a/Sources/ProcessOutUI/Sources/Core/Providers/CardSchemeImage/CardSchemeImageProvider.swift b/Sources/ProcessOutUI/Sources/Core/Providers/CardSchemeImage/CardSchemeImageProvider.swift index d13cb6489..f79f99941 100644 --- a/Sources/ProcessOutUI/Sources/Core/Providers/CardSchemeImage/CardSchemeImageProvider.swift +++ b/Sources/ProcessOutUI/Sources/Core/Providers/CardSchemeImage/CardSchemeImageProvider.swift @@ -9,7 +9,7 @@ import SwiftUI import ProcessOut @_spi(PO) import ProcessOutCoreUI -final class CardSchemeImageProvider { +final class CardSchemeImageProvider: Sendable { static let shared = CardSchemeImageProvider() diff --git a/Sources/ProcessOutUI/Sources/Core/Providers/PresentingViewController/PresentingViewControllerProvider.swift b/Sources/ProcessOutUI/Sources/Core/Providers/PresentingViewController/PresentingViewControllerProvider.swift index b57e46664..4216b3e76 100644 --- a/Sources/ProcessOutUI/Sources/Core/Providers/PresentingViewController/PresentingViewControllerProvider.swift +++ b/Sources/ProcessOutUI/Sources/Core/Providers/PresentingViewController/PresentingViewControllerProvider.swift @@ -10,6 +10,7 @@ import UIKit enum PresentingViewControllerProvider { /// Attempts to find view controller that can modally present other view controller. + @MainActor static func find() -> UIViewController? { let rootViewController = UIApplication.shared .connectedScenes diff --git a/Sources/ProcessOutUI/Sources/Core/Utils/POConfirmationDialogConfiguration.swift b/Sources/ProcessOutUI/Sources/Core/Utils/POConfirmationDialogConfiguration.swift index 502a3e84b..51a4ea39f 100644 --- a/Sources/ProcessOutUI/Sources/Core/Utils/POConfirmationDialogConfiguration.swift +++ b/Sources/ProcessOutUI/Sources/Core/Utils/POConfirmationDialogConfiguration.swift @@ -6,7 +6,7 @@ // /// Confirmation dialog configuration. -public struct POConfirmationDialogConfiguration { +public struct POConfirmationDialogConfiguration: Sendable { /// Confirmation title. Use empty string to hide title. public let title: String? diff --git a/Sources/ProcessOutUI/Sources/Core/Utils/POStringResource+Extension.swift b/Sources/ProcessOutUI/Sources/Core/Utils/POStringResource+Extension.swift deleted file mode 100644 index dc29cd160..000000000 --- a/Sources/ProcessOutUI/Sources/Core/Utils/POStringResource+Extension.swift +++ /dev/null @@ -1,15 +0,0 @@ -// -// POStringResource+Extension.swift -// ProcessOutUI -// -// Created by Andrii Vysotskyi on 31.01.2024. -// - -@_spi(PO) import ProcessOut - -extension POStringResource { - - init(_ key: String, comment: String) { - self.init(key, bundle: BundleLocator.bundle, comment: comment) - } -} diff --git a/Sources/ProcessOut/Sources/Core/Utils/POStringResource.swift b/Sources/ProcessOutUI/Sources/Core/Utils/StringResource.swift similarity index 89% rename from Sources/ProcessOut/Sources/Core/Utils/POStringResource.swift rename to Sources/ProcessOutUI/Sources/Core/Utils/StringResource.swift index c3fe28e16..252586ebb 100644 --- a/Sources/ProcessOut/Sources/Core/Utils/POStringResource.swift +++ b/Sources/ProcessOutUI/Sources/Core/Utils/StringResource.swift @@ -1,5 +1,5 @@ // -// POStringResource.swift +// StringResource.swift // ProcessOut // // Created by Andrii Vysotskyi on 23.01.2024. @@ -7,7 +7,7 @@ import Foundation -@_spi(PO) public struct POStringResource { +struct StringResource: Sendable { /// The key to use to look up a localized string. let key: String @@ -15,11 +15,6 @@ import Foundation /// The bundle containing the table’s strings file. let bundle: Bundle - public init(_ key: String, bundle: Bundle, comment: String) { - self.key = key - self.bundle = bundle - } - init(_ key: String, comment: String) { self.key = key self.bundle = BundleLocator.bundle @@ -29,7 +24,7 @@ import Foundation extension String { /// Creates string with given resource and replacements. - @_spi(PO) public init(resource: POStringResource, replacements: CVarArg...) { + init(resource: StringResource, replacements: CVarArg...) { let format = Self.localized(resource.key, bundle: resource.bundle) self = String(format: format, locale: .current, arguments: replacements) } diff --git a/Sources/ProcessOutUI/Sources/Core/ViewModel/AnyViewModel.swift b/Sources/ProcessOutUI/Sources/Core/ViewModel/AnyViewModel.swift index 5b514ee4f..e5e360eba 100644 --- a/Sources/ProcessOutUI/Sources/Core/ViewModel/AnyViewModel.swift +++ b/Sources/ProcessOutUI/Sources/Core/ViewModel/AnyViewModel.swift @@ -23,13 +23,17 @@ final class AnyViewModel: ViewModel { // MARK: - CardTokenizationViewModel + var state: State { + get { base.state } + set { base.state = newValue } + } + func start() { base.start() } - var state: State { - get { base.state } - set { base.state = newValue } + func stop() { + base.stop() } // MARK: - Private Properties @@ -46,13 +50,17 @@ private class ViewModelBox: AnyViewModelBase where T: ViewModel { let base: T + override var state: T.State { + get { base.state } + set { base.state = newValue } + } + override func start() { base.start() } - override var state: T.State { - get { base.state } - set { base.state = newValue } + override func stop() { + base.stop() } } @@ -60,13 +68,17 @@ private class ViewModelBox: AnyViewModelBase where T: ViewModel { private class AnyViewModelBase: ViewModel { + var state: State { + get { fatalError("Not implemented") } + set { fatalError("Not implemented") } + } + func start() { fatalError("Not implemented") } - var state: State { - get { fatalError("Not implemented") } - set { fatalError("Not implemented") } + func stop() { + fatalError("Not implemented") } } diff --git a/Sources/ProcessOutUI/Sources/Core/ViewModel/ViewModel.swift b/Sources/ProcessOutUI/Sources/Core/ViewModel/ViewModel.swift index 313177198..b890ca334 100644 --- a/Sources/ProcessOutUI/Sources/Core/ViewModel/ViewModel.swift +++ b/Sources/ProcessOutUI/Sources/Core/ViewModel/ViewModel.swift @@ -7,6 +7,7 @@ import Combine +@MainActor protocol ViewModel: ObservableObject { associatedtype State @@ -16,4 +17,7 @@ protocol ViewModel: ObservableObject { /// Starts view model. func start() + + /// Stops view model. + func stop() } diff --git a/Sources/ProcessOutUI/Sources/Modules/3DSRedirect/PO3DSRedirectController.swift b/Sources/ProcessOutUI/Sources/Modules/3DSRedirect/PO3DSRedirectController.swift deleted file mode 100644 index 35d064012..000000000 --- a/Sources/ProcessOutUI/Sources/Modules/3DSRedirect/PO3DSRedirectController.swift +++ /dev/null @@ -1,103 +0,0 @@ -// -// PO3DSRedirectController.swift -// ProcessOutUI -// -// Created by Andrii Vysotskyi on 17.11.2023. -// - -import Foundation -import SafariServices -import ProcessOut - -/// An object that presents a screen that allows to handle 3DS redirect. -/// -/// - Important: The PO3DSRedirectController class performs the same role as the SFSafariViewController -/// class initialized with 3DSRedirect, but it does not depend on the UIKit framework. This means that -/// the controller can be used in places where a view controller cannot (for example, in SwiftUI applications). -@available(*, deprecated, message: "Use POWebAuthenticationSession instead.") -public final class PO3DSRedirectController { - - /// - Parameters: - /// - redirect: redirect to handle. - /// - returnUrl: Return URL specified when creating invoice or customer token. - /// - safariConfiguration: The configuration for the new view controller. - public init( - redirect: PO3DSRedirect, - returnUrl: URL, - safariConfiguration: SFSafariViewController.Configuration = SFSafariViewController.Configuration() - ) { - self.redirect = redirect - self.returnUrl = returnUrl - self.safariConfiguration = safariConfiguration - } - - /// Presents the Redirect UI modally over your app. You are responsible for dismissal. - /// - /// - Parameters: - /// - completion: A block that is called after the screen is presented. - /// - success: A Boolean value that indicates whether the screen was successfully presented. - /// - /// - NOTE: Redirect controller is retained for the duration of presentation. - public func present(completion: ((_ success: Bool) -> Void)? = nil) { - guard safariViewController == nil else { - preconditionFailure("Controller is already presented.") - } - if let presentingViewController = PresentingViewControllerProvider.find() { - let safariViewController = SFSafariViewController( - redirect: redirect, - returnUrl: returnUrl, - safariConfiguration: safariConfiguration, - completion: self.completion ?? { _ in } - ) - safariViewController.preferredBarTintColor = preferredBarTintColor - safariViewController.preferredControlTintColor = preferredControlTintColor - safariViewController.dismissButtonStyle = .cancel - presentingViewController.present(safariViewController, animated: true) { - completion?(true) - } - objc_setAssociatedObject( - safariViewController, &AssociatedKeys.redirectController, self, .OBJC_ASSOCIATION_RETAIN - ) - self.safariViewController = safariViewController - } else { - completion?(false) - let failure = POFailure(message: "Unable to present redirect UI.", code: .generic(.mobile)) - self.completion?(.failure(failure)) - } - } - - /// Dismisses the Redirect UI. - public func dismiss(completion: (() -> Void)? = nil) { - // todo(andrii-vysotskyi): automatically dismiss controller so behavior - // matches `POAlternativePaymentMethodController`. - if let safariViewController, safariViewController.presentingViewController != nil { - self.safariViewController = nil - safariViewController.dismiss(animated: true, completion: completion) - } else { - completion?() - } - } - - /// Completion to invoke when redirect handling ends. - public var completion: ((Result) -> Void)? - - /// The preferred color to tint the background of the navigation bar and toolbar. - public var preferredBarTintColor: UIColor? - - /// The preferred color to tint the control buttons on the navigation bar and toolbar. - public var preferredControlTintColor: UIColor? - - // MARK: - Private Nested Types - - private enum AssociatedKeys { - static var redirectController: UInt8 = 0 - } - - // MARK: - Private Properties - - private let redirect: PO3DSRedirect - private let returnUrl: URL - private let safariConfiguration: SFSafariViewController.Configuration - - private weak var safariViewController: SFSafariViewController? -} diff --git a/Sources/ProcessOutUI/Sources/Modules/3DSRedirect/POWebAuthenticationSession+3DSRedirect.swift b/Sources/ProcessOutUI/Sources/Modules/3DSRedirect/POWebAuthenticationSession+3DSRedirect.swift deleted file mode 100644 index 5127ff5f4..000000000 --- a/Sources/ProcessOutUI/Sources/Modules/3DSRedirect/POWebAuthenticationSession+3DSRedirect.swift +++ /dev/null @@ -1,37 +0,0 @@ -// -// POWebAuthenticationSession+3DSRedirect.swift -// ProcessOutUI -// -// Created by Andrii Vysotskyi on 29.05.2024. -// - -import Foundation -import ProcessOut - -extension POWebAuthenticationSession { - - /// Creates POWebAuthenticationSession that is able to handle 3DS redirects. - /// - /// - Parameters: - /// - redirect: redirect to handle. - /// - returnUrl: Return URL specified when creating invoice or customer token. - /// - completion: Completion to invoke when redirect handling ends. - public convenience init( - redirect: PO3DSRedirect, - returnUrl: URL, - completion: @escaping (Result) -> Void - ) { - let completionBox: Completion = { result in - completion(result.map(Self.token(with:))) - } - let callback = POWebAuthenticationSessionCallback.customScheme(returnUrl.scheme ?? "") - self.init(url: redirect.url, callback: callback, timeout: redirect.timeout, completion: completionBox) - } - - // MARK: - Private Methods - - private static func token(with url: URL) -> String { - let components = URLComponents(url: url, resolvingAgainstBaseURL: true) - return components?.queryItems?.first { $0.name == "token" }?.value ?? "" - } -} diff --git a/Sources/ProcessOutUI/Sources/Modules/3DSRedirect/SFSafariViewController+3DSRedirect.swift b/Sources/ProcessOutUI/Sources/Modules/3DSRedirect/SFSafariViewController+3DSRedirect.swift deleted file mode 100644 index b1ecbd305..000000000 --- a/Sources/ProcessOutUI/Sources/Modules/3DSRedirect/SFSafariViewController+3DSRedirect.swift +++ /dev/null @@ -1,50 +0,0 @@ -// -// SFSafariViewController+3DSRedirect.swift -// ProcessOutUI -// -// Created by Andrii Vysotskyi on 17.11.2023. -// - -import SafariServices -@_spi(PO) import ProcessOut - -extension SFSafariViewController { - - /// Creates view controller that can handle 3DS Redirects. - /// - /// - Note: Caller should dismiss view controller after completion is called. - /// - Note: Object's delegate shouldn't be modified. - /// - /// - Parameters: - /// - redirect: redirect to handle. - /// - returnUrl: Return URL specified when creating invoice or customer token. - /// - safariConfiguration: The configuration for the new view controller. - /// - completion: Completion to invoke when redirect handling ends. - public convenience init( - redirect: PO3DSRedirect, - returnUrl: URL, - safariConfiguration: SFSafariViewController.Configuration = .init(), - completion: @escaping (Result) -> Void - ) { - self.init(url: redirect.url, configuration: safariConfiguration) - let api: ProcessOut = ProcessOut.shared // swiftlint:disable:this redundant_type_annotation - let viewModel = DefaultSafariViewModel( - callback: .customScheme(returnUrl.scheme ?? ""), - timeout: redirect.timeout, - eventEmitter: api.eventEmitter, - logger: api.logger, - completion: { result in - completion(result.map(Self.token(with:))) - } - ) - setViewModel(viewModel) - viewModel.start() - } - - // MARK: - Private Methods - - private static func token(with url: URL) -> String { - let components = URLComponents(url: url, resolvingAgainstBaseURL: true) - return components?.queryItems?.first { $0.name == "token" }?.value ?? "" - } -} 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 a094a3dda..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 (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 (Result) -> Void - ) { - let completionBox: Completion = { result in - completion(result.flatMap(Self.response(with:))) - } - self.init(url: url, callback: .customScheme(returnUrl.scheme ?? ""), completion: completionBox) - } - - // MARK: - Private Methods - - private 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/AlternativePayment/SFSafariViewController+AlternativePayment.swift b/Sources/ProcessOutUI/Sources/Modules/AlternativePayment/SFSafariViewController+AlternativePayment.swift deleted file mode 100644 index 02aab1d86..000000000 --- a/Sources/ProcessOutUI/Sources/Modules/AlternativePayment/SFSafariViewController+AlternativePayment.swift +++ /dev/null @@ -1,81 +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 (Result) -> Void - ) { - let url = ProcessOut.shared.alternativePaymentMethods.alternativePaymentMethodUrl(request: request) - self.init(url: url, configuration: safariConfiguration) - commonInit(returnUrl: returnUrl, 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 (Result) -> Void - ) { - self.init(url: url, configuration: safariConfiguration) - commonInit(returnUrl: returnUrl, completion: completion) - } - - // MARK: - Private Nested Types - - private typealias Completion = (Result) -> Void - - // MARK: - Private Methods - - private func commonInit(returnUrl: URL, completion: @escaping Completion) { - let api: ProcessOut = ProcessOut.shared // swiftlint:disable:this redundant_type_annotation - let viewModel = DefaultSafariViewModel( - callback: .customScheme(returnUrl.scheme ?? ""), - eventEmitter: api.eventEmitter, - logger: api.logger, - completion: { result in - completion(result.flatMap(Self.response(with:))) - } - ) - self.setViewModel(viewModel) - viewModel.start() - } - - private 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/CardTokenization/Configuration/POBillingAddressConfiguration.swift b/Sources/ProcessOutUI/Sources/Modules/CardTokenization/Configuration/POBillingAddressConfiguration.swift index 822e7d611..b5f06aa11 100644 --- a/Sources/ProcessOutUI/Sources/Modules/CardTokenization/Configuration/POBillingAddressConfiguration.swift +++ b/Sources/ProcessOutUI/Sources/Modules/CardTokenization/Configuration/POBillingAddressConfiguration.swift @@ -8,10 +8,7 @@ import ProcessOut /// Billing address collection configuration. -public struct POBillingAddressConfiguration { - - @available(*, deprecated, message: "Use POBillingAddressCollectionMode directly.") - public typealias CollectionMode = POBillingAddressCollectionMode +public struct POBillingAddressConfiguration: Sendable { /// Billing address collection mode. public let mode: POBillingAddressCollectionMode diff --git a/Sources/ProcessOutUI/Sources/Modules/CardTokenization/Configuration/POCardTokenizationConfiguration.swift b/Sources/ProcessOutUI/Sources/Modules/CardTokenization/Configuration/POCardTokenizationConfiguration.swift index 8f8c648b5..96f392a0e 100644 --- a/Sources/ProcessOutUI/Sources/Modules/CardTokenization/Configuration/POCardTokenizationConfiguration.swift +++ b/Sources/ProcessOutUI/Sources/Modules/CardTokenization/Configuration/POCardTokenizationConfiguration.swift @@ -10,7 +10,7 @@ import ProcessOut /// A configuration object that defines a card tokenization module behaves. /// Use `nil` as a value for a nullable property to indicate that default value should be used. -public struct POCardTokenizationConfiguration { +public struct POCardTokenizationConfiguration: Sendable { /// Custom title. Use empty string to hide title. public let title: String? diff --git a/Sources/ProcessOutUI/Sources/Modules/CardTokenization/Delegate/POCardTokenizationDelegate.swift b/Sources/ProcessOutUI/Sources/Modules/CardTokenization/Delegate/POCardTokenizationDelegate.swift index 4b972ea7c..12bb78d3f 100644 --- a/Sources/ProcessOutUI/Sources/Modules/CardTokenization/Delegate/POCardTokenizationDelegate.swift +++ b/Sources/ProcessOutUI/Sources/Modules/CardTokenization/Delegate/POCardTokenizationDelegate.swift @@ -8,10 +8,11 @@ import ProcessOut /// Card tokenization module delegate definition. -public protocol POCardTokenizationDelegate: AnyObject { +public protocol POCardTokenizationDelegate: AnyObject, Sendable { /// Invoked when module emits event. - func cardTokenizationDidEmitEvent(_ event: POCardTokenizationEvent) + @MainActor + func cardTokenization(didEmitEvent event: POCardTokenizationEvent) /// Allows delegate to additionally process tokenized card before ending module's lifecycle. For example /// it is possible to authorize an invoice or assign customer token. Default implementation does nothing. @@ -32,16 +33,19 @@ public protocol POCardTokenizationDelegate: AnyObject { /// Allows to choose preferred scheme that will be selected by default based on issuer information. Default /// implementation returns primary scheme. - func preferredScheme(issuerInformation: POCardIssuerInformation) -> String? + @MainActor + func cardTokenization(preferredSchemeFor issuerInformation: POCardIssuerInformation) -> POCardScheme? /// Asks delegate whether user should be allowed to continue after failure or module should complete. /// Default implementation returns `true`. - func shouldContinueTokenization(after failure: POFailure) -> Bool + @MainActor + func cardTokenization(shouldContinueAfter failure: POFailure) -> Bool } extension POCardTokenizationDelegate { - public func cardTokenizationDidEmitEvent(_ event: POCardTokenizationEvent) { + @MainActor + public func cardTokenization(didEmitEvent event: POCardTokenizationEvent) { // Ignored } @@ -54,11 +58,13 @@ extension POCardTokenizationDelegate { // Ignored } - public func preferredScheme(issuerInformation: POCardIssuerInformation) -> String? { + @MainActor + public func cardTokenization(preferredSchemeFor issuerInformation: POCardIssuerInformation) -> POCardScheme? { issuerInformation.scheme } - public func shouldContinueTokenization(after failure: POFailure) -> Bool { + @MainActor + public func cardTokenization(shouldContinueAfter failure: POFailure) -> Bool { true } } diff --git a/Sources/ProcessOutUI/Sources/Modules/CardTokenization/Delegate/POCardTokenizationEvent.swift b/Sources/ProcessOutUI/Sources/Modules/CardTokenization/Delegate/POCardTokenizationEvent.swift index d544d05f9..421dc74cb 100644 --- a/Sources/ProcessOutUI/Sources/Modules/CardTokenization/Delegate/POCardTokenizationEvent.swift +++ b/Sources/ProcessOutUI/Sources/Modules/CardTokenization/Delegate/POCardTokenizationEvent.swift @@ -8,7 +8,7 @@ import ProcessOut /// Describes events that could happen during card tokenization lifecycle. -public enum POCardTokenizationEvent { +public enum POCardTokenizationEvent: Sendable { /// Initial event that is sent prior any other event. case willStart diff --git a/Sources/ProcessOutUI/Sources/Modules/CardTokenization/Interactor/CardTokenizationInteractor.swift b/Sources/ProcessOutUI/Sources/Modules/CardTokenization/Interactor/CardTokenizationInteractor.swift index 962367e9b..00f067366 100644 --- a/Sources/ProcessOutUI/Sources/Modules/CardTokenization/Interactor/CardTokenizationInteractor.swift +++ b/Sources/ProcessOutUI/Sources/Modules/CardTokenization/Interactor/CardTokenizationInteractor.swift @@ -7,6 +7,7 @@ import ProcessOut +@MainActor protocol CardTokenizationInteractor: Interactor { /// Delegate. diff --git a/Sources/ProcessOutUI/Sources/Modules/CardTokenization/Interactor/CardTokenizationInteractorState.swift b/Sources/ProcessOutUI/Sources/Modules/CardTokenization/Interactor/CardTokenizationInteractorState.swift index 5960834e1..82b7a1112 100644 --- a/Sources/ProcessOutUI/Sources/Modules/CardTokenization/Interactor/CardTokenizationInteractorState.swift +++ b/Sources/ProcessOutUI/Sources/Modules/CardTokenization/Interactor/CardTokenizationInteractorState.swift @@ -136,3 +136,6 @@ extension CardTokenizationInteractorState.Started { return parameters.allSatisfy(\.isValid) && address.areParametersValid } } + +@available(*, unavailable) +extension CardTokenizationInteractorState: Sendable { } diff --git a/Sources/ProcessOutUI/Sources/Modules/CardTokenization/Interactor/DefaultCardTokenizationInteractor.swift b/Sources/ProcessOutUI/Sources/Modules/CardTokenization/Interactor/DefaultCardTokenizationInteractor.swift index daa1936c0..11e691ca7 100644 --- a/Sources/ProcessOutUI/Sources/Modules/CardTokenization/Interactor/DefaultCardTokenizationInteractor.swift +++ b/Sources/ProcessOutUI/Sources/Modules/CardTokenization/Interactor/DefaultCardTokenizationInteractor.swift @@ -39,7 +39,7 @@ final class DefaultCardTokenizationInteractor: guard case .idle = state else { return } - delegate?.cardTokenizationDidEmitEvent(.willStart) + delegate?.cardTokenization(didEmitEvent: .willStart) let startedState = State.Started( number: .init(id: \.number, formatter: cardNumberFormatter), expiration: .init(id: \.expiration, formatter: cardExpirationFormatter), @@ -50,7 +50,7 @@ final class DefaultCardTokenizationInteractor: address: defaultAddressParameters ) setStateUnchecked(.started(startedState)) - delegate?.cardTokenizationDidEmitEvent(.didStart) + delegate?.cardTokenization(didEmitEvent: .didStart) logger.debug("Did start card tokenization flow") } @@ -80,7 +80,7 @@ final class DefaultCardTokenizationInteractor: break } setStateUnchecked(.started(startedState)) - delegate?.cardTokenizationDidEmitEvent(.parametersChanged) + delegate?.cardTokenization(didEmitEvent: .parametersChanged) } func setPreferredScheme(_ scheme: POCardScheme) { @@ -88,8 +88,8 @@ final class DefaultCardTokenizationInteractor: return } let supportedSchemes = [ - startedState.issuerInformation?.$scheme.typed, - startedState.issuerInformation?.$coScheme.typed + startedState.issuerInformation?.scheme, + startedState.issuerInformation?.coScheme ] logger.debug("Will change card scheme to \(scheme)") guard supportedSchemes.contains(scheme) else { @@ -100,7 +100,7 @@ final class DefaultCardTokenizationInteractor: } startedState.preferredScheme = scheme setStateUnchecked(.started(startedState)) - delegate?.cardTokenizationDidEmitEvent(.parametersChanged) + delegate?.cardTokenization(didEmitEvent: .parametersChanged) } func setShouldSaveCard(_ shouldSaveCard: Bool) { @@ -110,7 +110,7 @@ final class DefaultCardTokenizationInteractor: logger.debug("Will change card saving selection to \(shouldSaveCard)") startedState.shouldSaveCard = shouldSaveCard setStateUnchecked(.started(startedState)) - delegate?.cardTokenizationDidEmitEvent(.parametersChanged) + delegate?.cardTokenization(didEmitEvent: .parametersChanged) // todo(andrii-vysotskyi): actually save card if requested. } @@ -123,7 +123,7 @@ final class DefaultCardTokenizationInteractor: return } logger.debug("Will tokenize card") - delegate?.cardTokenizationDidEmitEvent(.willTokenizeCard) + delegate?.cardTokenization(didEmitEvent: .willTokenizeCard) setStateUnchecked(.tokenizing(snapshot: startedState)) let request = POCardTokenizationRequest( number: cardNumberFormatter.normalized(number: startedState.number.value), @@ -132,14 +132,14 @@ final class DefaultCardTokenizationInteractor: cvc: startedState.cvc.value, name: startedState.cardholderName.value, contact: convertToContact(addressParameters: startedState.address), - preferredScheme: startedState.preferredScheme?.rawValue, + preferredScheme: startedState.preferredScheme, metadata: configuration.metadata ) - Task { @MainActor in + Task { do { let card = try await cardsService.tokenize(request: request) logger.debug("Did tokenize card: \(String(describing: card))") - delegate?.cardTokenizationDidEmitEvent(.didTokenize(card: card)) + delegate?.cardTokenization(didEmitEvent: .didTokenize(card: card)) try await delegate?.cardTokenization(didTokenizeCard: card, shouldSaveCard: startedState.shouldSaveCard) try await delegate?.processTokenizedCard(card: card) setTokenizedState(card: card) @@ -172,8 +172,8 @@ final class DefaultCardTokenizationInteractor: private let logger: POLogger private let completion: Completion - private lazy var cardNumberFormatter = POCardNumberFormatter() - private lazy var cardExpirationFormatter = POCardExpirationFormatter() + private lazy var cardNumberFormatter = CardNumberFormatter() + private lazy var cardExpirationFormatter = CardExpirationFormatter() private var issuerInformationCancellable: POCancellable? @@ -186,7 +186,7 @@ final class DefaultCardTokenizationInteractor: let tokenizedState = State.Tokenized(card: card, cardNumber: snapshot.number.value) setStateUnchecked(.tokenized(tokenizedState)) logger.info("Did tokenize and process card", attributes: [.cardId: card.id]) - delegate?.cardTokenizationDidEmitEvent(.didComplete) + delegate?.cardTokenization(didEmitEvent: .didComplete) completion(.success(card)) } @@ -194,7 +194,7 @@ final class DefaultCardTokenizationInteractor: private func restoreStartedState(tokenizationFailure failure: POFailure) { guard case .tokenizing(var startedState) = state, - delegate?.shouldContinueTokenization(after: failure) != false else { + delegate?.cardTokenization(shouldContinueAfter: failure) != false else { setFailureStateUnchecked(failure: failure) return } @@ -210,7 +210,7 @@ final class DefaultCardTokenizationInteractor: private func errorMessage(for failure: POFailure, invalidParameterIds: inout [State.ParameterId]) -> String? { // todo(andrii-vysotskyi): remove hardcoded message when backend is updated with localized values - let errorMessage: POStringResource + let errorMessage: StringResource switch failure.code { case .generic(.requestInvalidCard), .generic(.cardInvalid): invalidParameterIds.append(contentsOf: [\.number, \.expiration, \.cvc, \.cardholderName]) @@ -270,20 +270,20 @@ final class DefaultCardTokenizationInteractor: return } logger.debug("Will fetch issuer information", attributes: ["IIN": iin]) - issuerInformationCancellable = cardsService.issuerInformation(iin: iin) { [logger, weak self] result in - guard let self, case .started(var startedState) = self.state else { - return - } - switch result { - case .failure(let failure) where failure.code == .cancelled: - break - case .failure(let failure): - // Inability to select co-scheme is considered minor issue and we still want - // users to be able to continue tokenization. So errors are silently ignored. - logger.info("Did fail to fetch issuer information: \(failure)", attributes: ["IIN": iin]) - case .success(let issuerInformation): + issuerInformationCancellable = Task { + do { + let issuerInformation = try await cardsService.issuerInformation(iin: iin) + guard case .started(var startedState) = self.state else { + return + } update(startedState: &startedState, issuerInformation: issuerInformation, resolvePreferredScheme: true) self.setStateUnchecked(.started(startedState)) + } catch let failure as POFailure where failure.code == .cancelled { + // Ignored + } catch { + // Inability to select co-scheme is considered minor issue and we still want + // users to be able to continue tokenization. So errors are silently ignored. + logger.info("Did fail to fetch issuer information: \(error)", attributes: ["IIN": iin]) } } } @@ -296,13 +296,12 @@ final class DefaultCardTokenizationInteractor: if !resolvePreferredScheme { startedState.preferredScheme = nil } else if let issuerInformation, let delegate = delegate { - let rawScheme = delegate.preferredScheme(issuerInformation: issuerInformation) - startedState.preferredScheme = rawScheme.map(POCardScheme.init) + startedState.preferredScheme = delegate.cardTokenization(preferredSchemeFor: issuerInformation) } else { - startedState.preferredScheme = issuerInformation?.$scheme.typed + startedState.preferredScheme = issuerInformation?.scheme } let securityCodeFormatter = CardSecurityCodeFormatter() - securityCodeFormatter.scheme = issuerInformation?.$scheme.typed + securityCodeFormatter.scheme = issuerInformation?.scheme startedState.cvc.value = securityCodeFormatter.string(from: startedState.cvc.value) startedState.cvc.formatter = securityCodeFormatter } diff --git a/Sources/ProcessOutUI/Sources/Modules/CardTokenization/Style/POCardTokenizationStyle.swift b/Sources/ProcessOutUI/Sources/Modules/CardTokenization/Style/POCardTokenizationStyle.swift index fcf0affcb..36b4aace1 100644 --- a/Sources/ProcessOutUI/Sources/Modules/CardTokenization/Style/POCardTokenizationStyle.swift +++ b/Sources/ProcessOutUI/Sources/Modules/CardTokenization/Style/POCardTokenizationStyle.swift @@ -13,6 +13,7 @@ import SwiftUI /// For more information about styling specific components, see /// [the dedicated documentation.](https://swiftpackageindex.com/processout/processout-ios/documentation/processoutcoreui) @available(iOS 14, *) +@MainActor public struct POCardTokenizationStyle { /// Title style. @@ -69,16 +70,14 @@ public struct POCardTokenizationStyle { extension POCardTokenizationStyle { /// Default card tokenization style. - public static var `default`: POCardTokenizationStyle { - POCardTokenizationStyle( - title: POTextStyle(color: Color(poResource: .Text.primary), typography: .title), - sectionTitle: POTextStyle(color: Color(poResource: .Text.primary), typography: .label1), - input: .medium, - radioButton: .radio, - errorDescription: POTextStyle(color: Color(poResource: .Text.error), typography: .label2), - backgroundColor: Color(poResource: .Surface.default), - actionsContainer: .default, - separatorColor: Color(poResource: .Border.subtle) - ) - } + public static let `default` = POCardTokenizationStyle( + title: POTextStyle(color: Color(poResource: .Text.primary), typography: .title), + sectionTitle: POTextStyle(color: Color(poResource: .Text.primary), typography: .label1), + input: .medium, + radioButton: .radio, + errorDescription: POTextStyle(color: Color(poResource: .Text.error), typography: .label2), + backgroundColor: Color(poResource: .Surface.default), + actionsContainer: .default, + separatorColor: Color(poResource: .Border.subtle) + ) } diff --git a/Sources/ProcessOutUI/Sources/Modules/CardTokenization/Style/View+CardTokenizationStyle.swift b/Sources/ProcessOutUI/Sources/Modules/CardTokenization/Style/View+CardTokenizationStyle.swift index 22808e69a..4c541179f 100644 --- a/Sources/ProcessOutUI/Sources/Modules/CardTokenization/Style/View+CardTokenizationStyle.swift +++ b/Sources/ProcessOutUI/Sources/Modules/CardTokenization/Style/View+CardTokenizationStyle.swift @@ -26,7 +26,8 @@ extension EnvironmentValues { // MARK: - Private Nested Types - private struct Key: EnvironmentKey { + @MainActor + private struct Key: @preconcurrency EnvironmentKey { static let defaultValue = POCardTokenizationStyle.default } } diff --git a/Sources/ProcessOutUI/Sources/Modules/CardTokenization/Symbols/StringResource+CardTokenization.swift b/Sources/ProcessOutUI/Sources/Modules/CardTokenization/Symbols/StringResource+CardTokenization.swift index 91634479a..c405e4407 100644 --- a/Sources/ProcessOutUI/Sources/Modules/CardTokenization/Symbols/StringResource+CardTokenization.swift +++ b/Sources/ProcessOutUI/Sources/Modules/CardTokenization/Symbols/StringResource+CardTokenization.swift @@ -5,32 +5,30 @@ // Created by Andrii Vysotskyi on 17.10.2023. // -@_spi(PO) import ProcessOut - // swiftlint:disable nesting -extension POStringResource { +extension StringResource { enum CardTokenization { /// Card tokenization title. - static let title = POStringResource("card-tokenization.title", comment: "") + static let title = StringResource("card-tokenization.title", comment: "") enum CardDetails { /// Card number placeholder. - static let number = POStringResource("card-tokenization.card-details.number.placeholder", comment: "") + static let number = StringResource("card-tokenization.card-details.number.placeholder", comment: "") /// Card expiration placeholder. - static let expiration = POStringResource( + static let expiration = StringResource( "card-tokenization.card-details.expiration.placeholder", comment: "" ) /// Card CVC placeholder. - static let cvc = POStringResource("card-tokenization.card-details.cvc.placeholder", comment: "") + static let cvc = StringResource("card-tokenization.card-details.cvc.placeholder", comment: "") /// Cardholder name placeholder. - static let cardholder = POStringResource( + static let cardholder = StringResource( "card-tokenization.card-details.cardholder.placeholder", comment: "" ) } @@ -38,53 +36,53 @@ extension POStringResource { enum PreferredScheme { /// Preferred scheme section title. - static let title = POStringResource("card-tokenization.preferred-scheme.title", comment: "") + static let title = StringResource("card-tokenization.preferred-scheme.title", comment: "") } enum BillingAddress { /// Billing address section title. - static let title = POStringResource("card-tokenization.billing-address.title", comment: "") + static let title = StringResource("card-tokenization.billing-address.title", comment: "") /// Billing address street. - static let street = POStringResource("card-tokenization.billing-address.street", comment: "") + static let street = StringResource("card-tokenization.billing-address.street", comment: "") } enum Error { /// Generic card error. - static let card = POStringResource("card-tokenization.error.card", comment: "") + static let card = StringResource("card-tokenization.error.card", comment: "") /// Invalid card number. - static let cardNumber = POStringResource("card-tokenization.error.card-number", comment: "") + static let cardNumber = StringResource("card-tokenization.error.card-number", comment: "") /// Invalid card expiration. - static let cardExpiration = POStringResource("card-tokenization.error.card-expiration", comment: "") + static let cardExpiration = StringResource("card-tokenization.error.card-expiration", comment: "") /// Invalid card track data. - static let trackData = POStringResource("card-tokenization.error.track-data", comment: "") + static let trackData = StringResource("card-tokenization.error.track-data", comment: "") /// Invalid CVC. - static let cvc = POStringResource("card-tokenization.error.cvc", comment: "") + static let cvc = StringResource("card-tokenization.error.cvc", comment: "") /// Invalid cardholder name. - static let cardholderName = POStringResource("card-tokenization.error.cardholder-name", comment: "") + static let cardholderName = StringResource("card-tokenization.error.cardholder-name", comment: "") /// Generic error description. - static let generic = POStringResource("card-tokenization.error.generic", comment: "") + static let generic = StringResource("card-tokenization.error.generic", comment: "") } enum Button { /// Submit button title. - static let submit = POStringResource("card-tokenization.submit-button.title", comment: "") + static let submit = StringResource("card-tokenization.submit-button.title", comment: "") /// Cancel button title. - static let cancel = POStringResource("card-tokenization.cancel-button.title", comment: "") + static let cancel = StringResource("card-tokenization.cancel-button.title", comment: "") } /// Save card message. - static let saveCardMessage = POStringResource("card-tokenization.save-card-message", comment: "") + static let saveCardMessage = StringResource("card-tokenization.save-card-message", comment: "") } } diff --git a/Sources/ProcessOutUI/Sources/Modules/CardTokenization/View/POCardTokenizationView.swift b/Sources/ProcessOutUI/Sources/Modules/CardTokenization/View/POCardTokenizationView.swift index bbb4e5156..c06cbf71a 100644 --- a/Sources/ProcessOutUI/Sources/Modules/CardTokenization/View/POCardTokenizationView.swift +++ b/Sources/ProcessOutUI/Sources/Modules/CardTokenization/View/POCardTokenizationView.swift @@ -31,6 +31,7 @@ public struct POCardTokenizationView: View { style.backgroundColor.ignoresSafeArea() } .onAppear(perform: viewModel.start) + .onDisappear(perform: viewModel.stop) } // MARK: - Private Properties diff --git a/Sources/ProcessOutUI/Sources/Modules/CardTokenization/ViewModel/CardTokenizationViewModelState.swift b/Sources/ProcessOutUI/Sources/Modules/CardTokenization/ViewModel/CardTokenizationViewModelState.swift index 960afad9f..c4f86ed93 100644 --- a/Sources/ProcessOutUI/Sources/Modules/CardTokenization/ViewModel/CardTokenizationViewModelState.swift +++ b/Sources/ProcessOutUI/Sources/Modules/CardTokenization/ViewModel/CardTokenizationViewModelState.swift @@ -131,3 +131,6 @@ extension CardTokenizationViewModelState.Item: Identifiable { } } } + +@available(*, unavailable) +extension CardTokenizationViewModelState: Sendable { } diff --git a/Sources/ProcessOutUI/Sources/Modules/CardTokenization/ViewModel/DefaultCardTokenizationViewModel.swift b/Sources/ProcessOutUI/Sources/Modules/CardTokenization/ViewModel/DefaultCardTokenizationViewModel.swift index e2b7c0593..4bb4fbe7d 100644 --- a/Sources/ProcessOutUI/Sources/Modules/CardTokenization/ViewModel/DefaultCardTokenizationViewModel.swift +++ b/Sources/ProcessOutUI/Sources/Modules/CardTokenization/ViewModel/DefaultCardTokenizationViewModel.swift @@ -28,6 +28,10 @@ final class DefaultCardTokenizationViewModel: ViewModel { $state.performWithoutAnimation(interactor.start) } + func stop() { + interactor.cancel() + } + // MARK: - Private Nested Types private typealias InteractorState = CardTokenizationInteractorState @@ -154,7 +158,7 @@ final class DefaultCardTokenizationViewModel: ViewModel { private func cardNumberIcon(startedState: InteractorState.Started) -> Image? { let scheme = startedState.issuerInformation?.coScheme != nil ? startedState.preferredScheme - : startedState.issuerInformation?.$scheme.typed + : startedState.issuerInformation?.scheme return scheme.flatMap(CardSchemeImageProvider.shared.image) } @@ -171,14 +175,14 @@ final class DefaultCardTokenizationViewModel: ViewModel { let pickerItem = State.PickerItem( id: ItemId.scheme, options: [ - .init(id: issuerInformation.scheme, title: issuerInformation.scheme.capitalized), - .init(id: coScheme, title: coScheme.capitalized) + .init(id: issuerInformation.scheme.rawValue, title: issuerInformation.scheme.rawValue.capitalized), + .init(id: coScheme.rawValue, title: coScheme.rawValue.capitalized) ], selectedOptionId: .init( get: { startedState.preferredScheme?.rawValue }, set: { [weak self] newValue in let newScheme = newValue.map(POCardScheme.init) - self?.interactor.setPreferredScheme(newScheme ?? issuerInformation.$scheme.typed) + self?.interactor.setPreferredScheme(newScheme ?? issuerInformation.scheme) } ), preferrsInline: true diff --git a/Sources/ProcessOutUI/Sources/Modules/CardUpdate/Configuration/POCardUpdateConfiguration.swift b/Sources/ProcessOutUI/Sources/Modules/CardUpdate/Configuration/POCardUpdateConfiguration.swift index 741b5b5b7..37090eda5 100644 --- a/Sources/ProcessOutUI/Sources/Modules/CardUpdate/Configuration/POCardUpdateConfiguration.swift +++ b/Sources/ProcessOutUI/Sources/Modules/CardUpdate/Configuration/POCardUpdateConfiguration.swift @@ -7,13 +7,13 @@ /// A configuration object that defines how a card update module behaves. /// Use `nil` as a value for a nullable property to indicate that default value should be used. -public struct POCardUpdateConfiguration { +public struct POCardUpdateConfiguration: Sendable { /// Card id that needs to be updated. public let cardId: String /// Allows to provide card information that will be visible in UI. It is also possible to inject - /// it dynamically using ``POCardUpdateDelegate/cardInformation(cardId:)``. + /// it dynamically using ``POCardUpdateDelegate/cardUpdate(informationFor:)``. public let cardInformation: POCardUpdateInformation? /// Custom title. Use empty string to hide title. diff --git a/Sources/ProcessOutUI/Sources/Modules/CardUpdate/Delegate/POCardUpdateDelegate.swift b/Sources/ProcessOutUI/Sources/Modules/CardUpdate/Delegate/POCardUpdateDelegate.swift index 4bd63feba..182750f88 100644 --- a/Sources/ProcessOutUI/Sources/Modules/CardUpdate/Delegate/POCardUpdateDelegate.swift +++ b/Sources/ProcessOutUI/Sources/Modules/CardUpdate/Delegate/POCardUpdateDelegate.swift @@ -8,30 +8,34 @@ import ProcessOut /// Card update module delegate definition. -public protocol POCardUpdateDelegate: AnyObject { - - /// Invoked when module emits event. - func cardUpdateDidEmitEvent(_ event: POCardUpdateEvent) +public protocol POCardUpdateDelegate: AnyObject, Sendable { /// Asks delegate to resolve card information based on card id. - func cardInformation(cardId: String) async -> POCardUpdateInformation? + func cardUpdate(informationFor cardId: String) async -> POCardUpdateInformation? + + /// Invoked when module emits event. + @MainActor + func cardUpdate(didEmitEvent event: POCardUpdateEvent) /// Asks delegate whether user should be allowed to continue after failure or module should complete. /// Default implementation returns `true`. - func shouldContinueUpdate(after failure: POFailure) -> Bool + @MainActor + func cardUpdate(shouldContinueAfter failure: POFailure) -> Bool } extension POCardUpdateDelegate { - public func cardUpdateDidEmitEvent(_ event: POCardUpdateEvent) { - // Ignored + public func cardUpdate(informationFor cardId: String) async -> POCardUpdateInformation? { + nil } - public func cardInformation(cardId: String) async -> POCardUpdateInformation? { - nil + @MainActor + public func cardUpdate(didEmitEvent event: POCardUpdateEvent) { + // Ignored } - public func shouldContinueUpdate(after failure: POFailure) -> Bool { + @MainActor + public func cardUpdate(shouldContinueAfter failure: POFailure) -> Bool { true } } diff --git a/Sources/ProcessOutUI/Sources/Modules/CardUpdate/Delegate/POCardUpdateEvent.swift b/Sources/ProcessOutUI/Sources/Modules/CardUpdate/Delegate/POCardUpdateEvent.swift index 008e8a205..07246e657 100644 --- a/Sources/ProcessOutUI/Sources/Modules/CardUpdate/Delegate/POCardUpdateEvent.swift +++ b/Sources/ProcessOutUI/Sources/Modules/CardUpdate/Delegate/POCardUpdateEvent.swift @@ -6,7 +6,7 @@ // /// Describes events that could happen during card update lifecycle. -public enum POCardUpdateEvent { +public enum POCardUpdateEvent: Sendable { /// Initial event that is sent prior any other event. case willStart diff --git a/Sources/ProcessOutUI/Sources/Modules/CardUpdate/Delegate/POCardUpdateInformation.swift b/Sources/ProcessOutUI/Sources/Modules/CardUpdate/Delegate/POCardUpdateInformation.swift index 7284ca7ff..7e423a4fa 100644 --- a/Sources/ProcessOutUI/Sources/Modules/CardUpdate/Delegate/POCardUpdateInformation.swift +++ b/Sources/ProcessOutUI/Sources/Modules/CardUpdate/Delegate/POCardUpdateInformation.swift @@ -8,7 +8,7 @@ import ProcessOut /// Short card information necessary for CVC update. -public struct POCardUpdateInformation { +public struct POCardUpdateInformation: Sendable { /// Masked card number displayed to user as is if set. public let maskedNumber: String? @@ -22,28 +22,25 @@ public struct POCardUpdateInformation { public let iin: String? /// Scheme of the card. - @POTypedRepresentation - public private(set) var scheme: String? + public let scheme: POCardScheme? /// Co-scheme of the card, such as Carte Bancaire. - @POTypedRepresentation - public private(set) var coScheme: String? + public let coScheme: POCardScheme? /// Preferred scheme previously selected by customer if any. - @POTypedRepresentation - public private(set) var preferredScheme: String? + public let preferredScheme: POCardScheme? public init( maskedNumber: String? = nil, iin: String? = nil, - scheme: String? = nil, - coScheme: String? = nil, - preferredScheme: String? = nil + scheme: POCardScheme? = nil, + coScheme: POCardScheme? = nil, + preferredScheme: POCardScheme? = nil ) { self.maskedNumber = maskedNumber self.iin = iin - self._scheme = .init(wrappedValue: scheme) - self._coScheme = .init(wrappedValue: coScheme) - self._preferredScheme = .init(wrappedValue: preferredScheme) + self.scheme = scheme + self.coScheme = coScheme + self.preferredScheme = preferredScheme } } diff --git a/Sources/ProcessOutUI/Sources/Modules/CardUpdate/Interactor/CardUpdateInteractor.swift b/Sources/ProcessOutUI/Sources/Modules/CardUpdate/Interactor/CardUpdateInteractor.swift index 7c08d3256..4b6d512d7 100644 --- a/Sources/ProcessOutUI/Sources/Modules/CardUpdate/Interactor/CardUpdateInteractor.swift +++ b/Sources/ProcessOutUI/Sources/Modules/CardUpdate/Interactor/CardUpdateInteractor.swift @@ -7,6 +7,7 @@ import ProcessOut +@MainActor protocol CardUpdateInteractor: Interactor { /// Updates CVC value. @@ -17,7 +18,4 @@ protocol CardUpdateInteractor: Interactor { /// Attempts to update card with new CVC. func submit() - - /// Cancells update if possible. - func cancel() } diff --git a/Sources/ProcessOutUI/Sources/Modules/CardUpdate/Interactor/CardUpdateInteractorState.swift b/Sources/ProcessOutUI/Sources/Modules/CardUpdate/Interactor/CardUpdateInteractorState.swift index 9f0b8e974..9292873b8 100644 --- a/Sources/ProcessOutUI/Sources/Modules/CardUpdate/Interactor/CardUpdateInteractorState.swift +++ b/Sources/ProcessOutUI/Sources/Modules/CardUpdate/Interactor/CardUpdateInteractorState.swift @@ -52,3 +52,6 @@ enum CardUpdateInteractorState: Equatable { /// Card update has finished. This is a sink state. case completed } + +@available(*, unavailable) +extension CardUpdateInteractorState: Sendable { } diff --git a/Sources/ProcessOutUI/Sources/Modules/CardUpdate/Interactor/DefaultCardUpdateInteractor.swift b/Sources/ProcessOutUI/Sources/Modules/CardUpdate/Interactor/DefaultCardUpdateInteractor.swift index 6674cb770..dcf3266bb 100644 --- a/Sources/ProcessOutUI/Sources/Modules/CardUpdate/Interactor/DefaultCardUpdateInteractor.swift +++ b/Sources/ProcessOutUI/Sources/Modules/CardUpdate/Interactor/DefaultCardUpdateInteractor.swift @@ -31,13 +31,13 @@ final class DefaultCardUpdateInteractor: BaseInteractor POCardScheme? { - if let scheme = cardInfo?.$preferredScheme.typed { + if let scheme = cardInfo?.preferredScheme { return scheme } guard configuration.isSchemeSelectionAllowed else { return nil } - return cardInfo?.$scheme.typed ?? issuerInformation?.$scheme.typed + return cardInfo?.scheme ?? issuerInformation?.scheme } } diff --git a/Sources/ProcessOutUI/Sources/Modules/CardUpdate/Style/POCardUpdateStyle.swift b/Sources/ProcessOutUI/Sources/Modules/CardUpdate/Style/POCardUpdateStyle.swift index bc4ef58ad..28252e56c 100644 --- a/Sources/ProcessOutUI/Sources/Modules/CardUpdate/Style/POCardUpdateStyle.swift +++ b/Sources/ProcessOutUI/Sources/Modules/CardUpdate/Style/POCardUpdateStyle.swift @@ -13,6 +13,7 @@ import SwiftUI /// For more information about styling specific components, see /// [the dedicated documentation.](https://swiftpackageindex.com/processout/processout-ios/documentation/processoutcoreui) @available(iOS 14, *) +@MainActor public struct POCardUpdateStyle { /// Title style. @@ -59,15 +60,13 @@ public struct POCardUpdateStyle { extension POCardUpdateStyle { /// Default card update style. - public static var `default`: POCardUpdateStyle { - POCardUpdateStyle( - title: POTextStyle(color: Color(poResource: .Text.primary), typography: .title), - input: .medium, - errorDescription: POTextStyle(color: Color(poResource: .Text.error), typography: .label2), - backgroundColor: Color(poResource: .Surface.default), - actionsContainer: .default, - progress: .circular, - separatorColor: Color(poResource: .Border.subtle) - ) - } + public static let `default` = POCardUpdateStyle( + title: POTextStyle(color: Color(poResource: .Text.primary), typography: .title), + input: .medium, + errorDescription: POTextStyle(color: Color(poResource: .Text.error), typography: .label2), + backgroundColor: Color(poResource: .Surface.default), + actionsContainer: .default, + progress: .circular, + separatorColor: Color(poResource: .Border.subtle) + ) } diff --git a/Sources/ProcessOutUI/Sources/Modules/CardUpdate/Style/View+CardUpdateStyle.swift b/Sources/ProcessOutUI/Sources/Modules/CardUpdate/Style/View+CardUpdateStyle.swift index 6b766f24c..d3fe8fc7a 100644 --- a/Sources/ProcessOutUI/Sources/Modules/CardUpdate/Style/View+CardUpdateStyle.swift +++ b/Sources/ProcessOutUI/Sources/Modules/CardUpdate/Style/View+CardUpdateStyle.swift @@ -26,7 +26,8 @@ extension EnvironmentValues { // MARK: - Private Nested Types - private struct Key: EnvironmentKey { + @MainActor + private struct Key: @preconcurrency EnvironmentKey { static let defaultValue = POCardUpdateStyle.default } } diff --git a/Sources/ProcessOutUI/Sources/Modules/CardUpdate/Symbols/StringResource+CardUpdate.swift b/Sources/ProcessOutUI/Sources/Modules/CardUpdate/Symbols/StringResource+CardUpdate.swift index fe8fc47c9..e8dd97cde 100644 --- a/Sources/ProcessOutUI/Sources/Modules/CardUpdate/Symbols/StringResource+CardUpdate.swift +++ b/Sources/ProcessOutUI/Sources/Modules/CardUpdate/Symbols/StringResource+CardUpdate.swift @@ -5,45 +5,43 @@ // Created by Andrii Vysotskyi on 03.11.2023. // -@_spi(PO) import ProcessOut - // swiftlint:disable nesting -extension POStringResource { +extension StringResource { enum CardUpdate { /// Card update title. - static let title = POStringResource("card-update.title", comment: "") + static let title = StringResource("card-update.title", comment: "") enum CardDetails { /// Card CVC placeholder. - static let cvc = POStringResource("card-update.cvc", comment: "") + static let cvc = StringResource("card-update.cvc", comment: "") } enum PreferredScheme { /// Preferred scheme section title. - static let title = POStringResource("card-update.preferred-scheme", comment: "") + static let title = StringResource("card-update.preferred-scheme", comment: "") } enum Button { /// Submit button title. - static let submit = POStringResource("card-update.submit-button", comment: "") + static let submit = StringResource("card-update.submit-button", comment: "") /// Cancel button title. - static let cancel = POStringResource("card-update.cancel-button", comment: "") + static let cancel = StringResource("card-update.cancel-button", comment: "") } enum Error { /// Invalid CVC. - static let cvc = POStringResource("card-update.error.cvc", comment: "") + static let cvc = StringResource("card-update.error.cvc", comment: "") /// Generic error description. - static let generic = POStringResource("card-update.error.generic", comment: "") + static let generic = StringResource("card-update.error.generic", comment: "") } } } diff --git a/Sources/ProcessOutUI/Sources/Modules/CardUpdate/View/POCardUpdateView+Init.swift b/Sources/ProcessOutUI/Sources/Modules/CardUpdate/View/POCardUpdateView+Init.swift index 503d188c8..82bb90e5b 100644 --- a/Sources/ProcessOutUI/Sources/Modules/CardUpdate/View/POCardUpdateView+Init.swift +++ b/Sources/ProcessOutUI/Sources/Modules/CardUpdate/View/POCardUpdateView+Init.swift @@ -21,7 +21,7 @@ extension POCardUpdateView { completion: @escaping (Result) -> Void ) { let viewModel = { - var logger = ProcessOut.shared.logger + var logger: POLogger = ProcessOut.shared.logger logger[attributeKey: .cardId] = configuration.cardId let interactor = DefaultCardUpdateInteractor( cardsService: ProcessOut.shared.cards, diff --git a/Sources/ProcessOutUI/Sources/Modules/CardUpdate/ViewModel/CardUpdateViewModel.swift b/Sources/ProcessOutUI/Sources/Modules/CardUpdate/ViewModel/CardUpdateViewModel.swift index fbc555582..524677a3e 100644 --- a/Sources/ProcessOutUI/Sources/Modules/CardUpdate/ViewModel/CardUpdateViewModel.swift +++ b/Sources/ProcessOutUI/Sources/Modules/CardUpdate/ViewModel/CardUpdateViewModel.swift @@ -8,6 +8,7 @@ import Combine @_spi(PO) import ProcessOutCoreUI +@MainActor protocol CardUpdateViewModel: ObservableObject { /// Screen title. diff --git a/Sources/ProcessOutUI/Sources/Modules/CardUpdate/ViewModel/CardUpdateViewModelItem.swift b/Sources/ProcessOutUI/Sources/Modules/CardUpdate/ViewModel/CardUpdateViewModelItem.swift index 5c85830d1..e4bc913de 100644 --- a/Sources/ProcessOutUI/Sources/Modules/CardUpdate/ViewModel/CardUpdateViewModelItem.swift +++ b/Sources/ProcessOutUI/Sources/Modules/CardUpdate/ViewModel/CardUpdateViewModelItem.swift @@ -69,3 +69,6 @@ extension CardUpdateViewModelItem: Identifiable { static let progressId = UUID().uuidString } } + +@available(*, unavailable) +extension CardUpdateViewModelItem: Sendable { } diff --git a/Sources/ProcessOutUI/Sources/Modules/CardUpdate/ViewModel/CardUpdateViewModelSection.swift b/Sources/ProcessOutUI/Sources/Modules/CardUpdate/ViewModel/CardUpdateViewModelSection.swift index 4c0a9589a..8ad176dba 100644 --- a/Sources/ProcessOutUI/Sources/Modules/CardUpdate/ViewModel/CardUpdateViewModelSection.swift +++ b/Sources/ProcessOutUI/Sources/Modules/CardUpdate/ViewModel/CardUpdateViewModelSection.swift @@ -18,3 +18,6 @@ struct CardUpdateViewModelSection: Identifiable { /// Section items. let items: [CardUpdateViewModelItem] } + +@available(*, unavailable) +extension CardUpdateViewModelSection: Sendable { } diff --git a/Sources/ProcessOutUI/Sources/Modules/DynamicCheckout/Configuration/PODynamicCheckoutAlternativePaymentConfiguration.swift b/Sources/ProcessOutUI/Sources/Modules/DynamicCheckout/Configuration/PODynamicCheckoutAlternativePaymentConfiguration.swift index 2c0ea3e7b..b9d3a56ee 100644 --- a/Sources/ProcessOutUI/Sources/Modules/DynamicCheckout/Configuration/PODynamicCheckoutAlternativePaymentConfiguration.swift +++ b/Sources/ProcessOutUI/Sources/Modules/DynamicCheckout/Configuration/PODynamicCheckoutAlternativePaymentConfiguration.swift @@ -9,9 +9,9 @@ import Foundation /// Alternative payment specific dynamic checkout configuration. @_spi(PO) -public struct PODynamicCheckoutAlternativePaymentConfiguration { +public struct PODynamicCheckoutAlternativePaymentConfiguration: Sendable { - public struct PaymentConfirmation { + public struct PaymentConfirmation: Sendable { /// Amount of time (in seconds) that module is allowed to wait before receiving final payment confirmation. /// Default timeout is 3 minutes while maximum value is 15 minutes. @@ -36,7 +36,7 @@ public struct PODynamicCheckoutAlternativePaymentConfiguration { } } - public struct CancelButton { + public struct CancelButton: Sendable { /// Cancel button title. Use `nil` for default title. public let title: String? @@ -50,7 +50,7 @@ public struct PODynamicCheckoutAlternativePaymentConfiguration { public init( title: String? = nil, - disabledFor: TimeInterval, + disabledFor: TimeInterval = 0, confirmation: POConfirmationDialogConfiguration? = nil ) { self.title = title @@ -59,9 +59,6 @@ public struct PODynamicCheckoutAlternativePaymentConfiguration { } } - /// 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 { 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/Configuration/PODynamicCheckoutCardConfiguration.swift b/Sources/ProcessOutUI/Sources/Modules/DynamicCheckout/Configuration/PODynamicCheckoutCardConfiguration.swift index 989df6a93..4cb624ac7 100644 --- a/Sources/ProcessOutUI/Sources/Modules/DynamicCheckout/Configuration/PODynamicCheckoutCardConfiguration.swift +++ b/Sources/ProcessOutUI/Sources/Modules/DynamicCheckout/Configuration/PODynamicCheckoutCardConfiguration.swift @@ -9,10 +9,10 @@ import ProcessOut /// Card specific dynamic checkout configuration. @_spi(PO) -public struct PODynamicCheckoutCardConfiguration { +public struct PODynamicCheckoutCardConfiguration: Sendable { /// Billing address collection configuration. - public struct BillingAddress { + public struct BillingAddress: Sendable { /// Default address information. public let defaultAddress: POContact? diff --git a/Sources/ProcessOutUI/Sources/Modules/DynamicCheckout/Configuration/PODynamicCheckoutConfiguration.swift b/Sources/ProcessOutUI/Sources/Modules/DynamicCheckout/Configuration/PODynamicCheckoutConfiguration.swift index 422b97c0c..676e06623 100644 --- a/Sources/ProcessOutUI/Sources/Modules/DynamicCheckout/Configuration/PODynamicCheckoutConfiguration.swift +++ b/Sources/ProcessOutUI/Sources/Modules/DynamicCheckout/Configuration/PODynamicCheckoutConfiguration.swift @@ -10,9 +10,9 @@ import ProcessOut /// Dynamic checkout configuration. @_spi(PO) -public struct PODynamicCheckoutConfiguration { +public struct PODynamicCheckoutConfiguration: Sendable { - public struct PaymentSuccess { + public struct PaymentSuccess: Sendable { /// Custom success message to display user when payment completes. public let message: String? @@ -27,7 +27,7 @@ public struct PODynamicCheckoutConfiguration { } } - public struct CancelButton { + public struct CancelButton: Sendable { /// Cancel button title. public let title: String? diff --git a/Sources/ProcessOutUI/Sources/Modules/DynamicCheckout/Delegate/PODynamicCheckoutDelegate.swift b/Sources/ProcessOutUI/Sources/Modules/DynamicCheckout/Delegate/PODynamicCheckoutDelegate.swift index 3b41c56b8..640e83718 100644 --- a/Sources/ProcessOutUI/Sources/Modules/DynamicCheckout/Delegate/PODynamicCheckoutDelegate.swift +++ b/Sources/ProcessOutUI/Sources/Modules/DynamicCheckout/Delegate/PODynamicCheckoutDelegate.swift @@ -10,10 +10,11 @@ import ProcessOut /// Dynamic checkout module delegate. @_spi(PO) -public protocol PODynamicCheckoutDelegate: AnyObject { +public protocol PODynamicCheckoutDelegate: AnyObject, Sendable { /// Invoked when module emits dynamic checkout event. /// - NOTE: default implementation does nothing. + @MainActor func dynamicCheckout(didEmitEvent event: PODynamicCheckoutEvent) /// Called when dynamic checkout is about to authorize invoice with given request. @@ -26,6 +27,7 @@ public protocol PODynamicCheckoutDelegate: AnyObject { /// Asks delegate whether user should be allowed to continue after failure or module should complete. /// Default implementation returns `true`. + @MainActor func dynamicCheckout(shouldContinueAfter failure: POFailure) -> Bool /// Your implementation could return a request that will be used to fetch new invoice to replace existing one @@ -39,15 +41,18 @@ public protocol PODynamicCheckoutDelegate: AnyObject { // MARK: - Card Payment /// Invoked when module emits event. + @MainActor func dynamicCheckout(didEmitCardTokenizationEvent event: POCardTokenizationEvent) /// Allows to choose preferred scheme that will be selected by default based on issuer information. Default /// implementation returns primary scheme. - func dynamicCheckout(preferredSchemeFor issuerInformation: POCardIssuerInformation) -> String? + @MainActor + func dynamicCheckout(preferredSchemeFor issuerInformation: POCardIssuerInformation) -> POCardScheme? // MARK: - Alternative Payment /// Invoked when module emits alternative payment event. + @MainActor func dynamicCheckout(didEmitAlternativePaymentEvent event: PONativeAlternativePaymentEvent) /// Method provides an ability to supply default values for given parameters. @@ -61,15 +66,18 @@ public protocol PODynamicCheckoutDelegate: AnyObject { // MARK: - Pass Kit /// Gives implementation an opportunity to modify payment request before it is used to authorize invoice. + @MainActor func dynamicCheckout(willAuthorizeInvoiceWith request: PKPaymentRequest) async } extension PODynamicCheckoutDelegate { + @MainActor public func dynamicCheckout(didEmitEvent event: PODynamicCheckoutEvent) { // Ignored } + @MainActor public func dynamicCheckout(shouldContinueAfter failure: POFailure) -> Bool { true } @@ -80,14 +88,17 @@ extension PODynamicCheckoutDelegate { nil } + @MainActor public func dynamicCheckout(didEmitCardTokenizationEvent event: POCardTokenizationEvent) { // Ignored } - public func dynamicCheckout(preferredSchemeFor issuerInformation: POCardIssuerInformation) -> String? { + @MainActor + public func dynamicCheckout(preferredSchemeFor issuerInformation: POCardIssuerInformation) -> POCardScheme? { issuerInformation.scheme } + @MainActor public func dynamicCheckout(didEmitAlternativePaymentEvent event: PONativeAlternativePaymentEvent) { // Ignored } diff --git a/Sources/ProcessOutUI/Sources/Modules/DynamicCheckout/Delegate/PODynamicCheckoutEvent.swift b/Sources/ProcessOutUI/Sources/Modules/DynamicCheckout/Delegate/PODynamicCheckoutEvent.swift index 7192777eb..3403212e7 100644 --- a/Sources/ProcessOutUI/Sources/Modules/DynamicCheckout/Delegate/PODynamicCheckoutEvent.swift +++ b/Sources/ProcessOutUI/Sources/Modules/DynamicCheckout/Delegate/PODynamicCheckoutEvent.swift @@ -9,7 +9,7 @@ import ProcessOut /// Events emitted by dynamic checkout module during its lifecycle. @_spi(PO) -public enum PODynamicCheckoutEvent { +public enum PODynamicCheckoutEvent: Sendable { /// Initial event that is sent prior any other event. case willStart diff --git a/Sources/ProcessOutUI/Sources/Modules/DynamicCheckout/Interactor/DynamicCheckoutDefaultInteractor.swift b/Sources/ProcessOutUI/Sources/Modules/DynamicCheckout/Interactor/DynamicCheckoutDefaultInteractor.swift index 2f08b11b8..0fc1f9b7f 100644 --- a/Sources/ProcessOutUI/Sources/Modules/DynamicCheckout/Interactor/DynamicCheckoutDefaultInteractor.swift +++ b/Sources/ProcessOutUI/Sources/Modules/DynamicCheckout/Interactor/DynamicCheckoutDefaultInteractor.swift @@ -18,18 +18,18 @@ final class DynamicCheckoutDefaultInteractor: init( configuration: PODynamicCheckoutConfiguration, delegate: PODynamicCheckoutDelegate?, - alternativePaymentSession: DynamicCheckoutAlternativePaymentSession, childProvider: DynamicCheckoutInteractorChildProvider, invoicesService: POInvoicesService, + alternativePaymentsService: POAlternativePaymentsService, cardsService: POCardsService, logger: POLogger, completion: @escaping (Result) -> Void ) { self.configuration = configuration self.delegate = delegate - self.alternativePaymentSession = alternativePaymentSession self.childProvider = childProvider self.invoicesService = invoicesService + self.alternativePaymentsService = alternativePaymentsService self.cardsService = cardsService self.logger = logger self.completion = completion @@ -137,9 +137,9 @@ final class DynamicCheckoutDefaultInteractor: // MARK: - Private Properties - private let alternativePaymentSession: DynamicCheckoutAlternativePaymentSession private let childProvider: DynamicCheckoutInteractorChildProvider private let invoicesService: POInvoicesService + private let alternativePaymentsService: POAlternativePaymentsService private let cardsService: POCardsService private let completion: (Result) -> Void @@ -148,7 +148,6 @@ final class DynamicCheckoutDefaultInteractor: // MARK: - Starting State - @MainActor private func continueStartUnchecked() async { do { let invoice = try await invoicesService.invoice(request: configuration.invoiceRequest) @@ -373,7 +372,7 @@ final class DynamicCheckoutDefaultInteractor: shouldInvalidateInvoice: true ) state = .paymentProcessing(paymentProcessingState) - Task { @MainActor in + Task { do { guard let delegate else { throw POFailure(message: "Delegate must be set to authorize invoice.", code: .generic(.mobile)) @@ -457,9 +456,11 @@ final class DynamicCheckoutDefaultInteractor: shouldInvalidateInvoice: true ) state = .paymentProcessing(paymentProcessingState) - Task { @MainActor in + Task { do { - let response = try await alternativePaymentSession.start(url: method.configuration.redirectUrl) + let response = try await alternativePaymentsService.authenticate( + using: method.configuration.redirectUrl + ) try await authorizeInvoice( source: response.gatewayToken, saveSource: false, @@ -557,7 +558,7 @@ final class DynamicCheckoutDefaultInteractor: do { var source = method.configuration.customerTokenId if let redirectUrl = method.configuration.redirectUrl { - source = try await alternativePaymentSession.start(url: redirectUrl).gatewayToken + source = try await alternativePaymentsService.authenticate(using: redirectUrl).gatewayToken } try await authorizeInvoice(source: source, saveSource: false, startedState: startedState) setSuccessState() @@ -723,7 +724,7 @@ final class DynamicCheckoutDefaultInteractor: } state = .success send(event: .didCompletePayment) - Task { @MainActor in + Task { try? await Task.sleep(seconds: configuration.paymentSuccess?.duration ?? 0) completion(.success(())) } @@ -731,8 +732,8 @@ final class DynamicCheckoutDefaultInteractor: // MARK: - Events + @MainActor private func send(event: PODynamicCheckoutEvent) { - assert(Thread.isMainThread, "Method should be called on main thread.") logger.debug("Did send event: '\(event)'") delegate?.dynamicCheckout(didEmitEvent: event) } @@ -781,7 +782,7 @@ final class DynamicCheckoutDefaultInteractor: @available(iOS 14.0, *) extension DynamicCheckoutDefaultInteractor: POCardTokenizationDelegate { - func cardTokenizationDidEmitEvent(_ event: POCardTokenizationEvent) { + func cardTokenization(didEmitEvent event: POCardTokenizationEvent) { delegate?.dynamicCheckout(didEmitCardTokenizationEvent: event) } @@ -795,11 +796,11 @@ extension DynamicCheckoutDefaultInteractor: POCardTokenizationDelegate { try await authorizeInvoice(source: card.id, saveSource: save, startedState: currentState.snapshot) } - func preferredScheme(issuerInformation: POCardIssuerInformation) -> String? { + func cardTokenization(preferredSchemeFor issuerInformation: POCardIssuerInformation) -> POCardScheme? { delegate?.dynamicCheckout(preferredSchemeFor: issuerInformation) } - func shouldContinueTokenization(after failure: POFailure) -> Bool { + func cardTokenization(shouldContinueAfter failure: POFailure) -> Bool { guard case .paymentProcessing(let currentState) = state else { return false } @@ -810,7 +811,7 @@ extension DynamicCheckoutDefaultInteractor: POCardTokenizationDelegate { @available(iOS 14.0, *) extension DynamicCheckoutDefaultInteractor: PONativeAlternativePaymentDelegate { - func nativeAlternativePaymentMethodDidEmitEvent(_ event: PONativeAlternativePaymentMethodEvent) { + func nativeAlternativePayment(didEmitEvent event: PONativeAlternativePaymentEvent) { switch event { case .willSubmitParameters: invalidateInvoiceIfPossible() @@ -820,13 +821,10 @@ extension DynamicCheckoutDefaultInteractor: PONativeAlternativePaymentDelegate { delegate?.dynamicCheckout(didEmitAlternativePaymentEvent: event) } - func nativeAlternativePaymentMethodDefaultValues( - for parameters: [PONativeAlternativePaymentMethodParameter], completion: @escaping ([String: String]) -> Void - ) { - Task { @MainActor in - let values = await delegate?.dynamicCheckout(alternativePaymentDefaultsFor: parameters) ?? [:] - completion(values) - } + func nativeAlternativePayment( + defaultsFor parameters: [PONativeAlternativePaymentMethodParameter] + ) async -> [String: String] { + await delegate?.dynamicCheckout(alternativePaymentDefaultsFor: parameters) ?? [:] } } diff --git a/Sources/ProcessOutUI/Sources/Modules/DynamicCheckout/Interactor/DynamicCheckoutInteractor.swift b/Sources/ProcessOutUI/Sources/Modules/DynamicCheckout/Interactor/DynamicCheckoutInteractor.swift index c55311625..7f8044cd9 100644 --- a/Sources/ProcessOutUI/Sources/Modules/DynamicCheckout/Interactor/DynamicCheckoutInteractor.swift +++ b/Sources/ProcessOutUI/Sources/Modules/DynamicCheckout/Interactor/DynamicCheckoutInteractor.swift @@ -5,6 +5,7 @@ // Created by Andrii Vysotskyi on 05.03.2024. // +@MainActor protocol DynamicCheckoutInteractor: Interactor { /// Configuration. diff --git a/Sources/ProcessOutUI/Sources/Modules/DynamicCheckout/Style/PODynamicCheckoutStyle.swift b/Sources/ProcessOutUI/Sources/Modules/DynamicCheckout/Style/PODynamicCheckoutStyle.swift index 1ef702a6e..12866ba8c 100644 --- a/Sources/ProcessOutUI/Sources/Modules/DynamicCheckout/Style/PODynamicCheckoutStyle.swift +++ b/Sources/ProcessOutUI/Sources/Modules/DynamicCheckout/Style/PODynamicCheckoutStyle.swift @@ -15,8 +15,10 @@ import SwiftUI /// [the dedicated documentation.](https://swiftpackageindex.com/processout/processout-ios/documentation/processoutcoreui) @available(iOS 14, *) @_spi(PO) +@MainActor public struct PODynamicCheckoutStyle { + @MainActor public struct RegularPaymentMethod { /// Payment method title. @@ -35,6 +37,7 @@ public struct PODynamicCheckoutStyle { } } + @MainActor public struct PaymentSuccess { /// Success message style. diff --git a/Sources/ProcessOutUI/Sources/Modules/DynamicCheckout/Style/View+DynamicCheckoutStyle.swift b/Sources/ProcessOutUI/Sources/Modules/DynamicCheckout/Style/View+DynamicCheckoutStyle.swift index bd5513eb7..b20806db6 100644 --- a/Sources/ProcessOutUI/Sources/Modules/DynamicCheckout/Style/View+DynamicCheckoutStyle.swift +++ b/Sources/ProcessOutUI/Sources/Modules/DynamicCheckout/Style/View+DynamicCheckoutStyle.swift @@ -27,7 +27,8 @@ extension EnvironmentValues { // MARK: - Private Nested Types - private struct Key: EnvironmentKey { + @MainActor + private struct Key: @preconcurrency EnvironmentKey { static let defaultValue = PODynamicCheckoutStyle.default } } diff --git a/Sources/ProcessOutUI/Sources/Modules/DynamicCheckout/Symbols/StringResource+DynamicCheckout.swift b/Sources/ProcessOutUI/Sources/Modules/DynamicCheckout/Symbols/StringResource+DynamicCheckout.swift index c0bc7e7ac..786b3f8b0 100644 --- a/Sources/ProcessOutUI/Sources/Modules/DynamicCheckout/Symbols/StringResource+DynamicCheckout.swift +++ b/Sources/ProcessOutUI/Sources/Modules/DynamicCheckout/Symbols/StringResource+DynamicCheckout.swift @@ -5,52 +5,50 @@ // Created by Andrii Vysotskyi on 17.04.2024. // -@_spi(PO) import ProcessOut - // swiftlint:disable nesting -extension POStringResource { +extension StringResource { enum DynamicCheckout { enum Button { /// Submit button title. - static let pay = POStringResource("dynamic-checkout.pay-button", comment: "") + static let pay = StringResource("dynamic-checkout.pay-button", comment: "") /// Cancel button title. - static let cancel = POStringResource("dynamic-checkout.cancel-button", comment: "") + static let cancel = StringResource("dynamic-checkout.cancel-button", comment: "") } enum CancelConfirmation { /// Success message. - static let title = POStringResource("cancel-confirmation.title", comment: "") + static let title = StringResource("cancel-confirmation.title", comment: "") /// Confirm button title.. - static let confirm = POStringResource("cancel-confirmation.confirm", comment: "") + static let confirm = StringResource("cancel-confirmation.confirm", comment: "") /// Cancel button title. - static let cancel = POStringResource("cancel-confirmation.cancel", comment: "") + static let cancel = StringResource("cancel-confirmation.cancel", comment: "") } enum Error { /// Indicates that implementation is unable to process payment. - static let generic = POStringResource("dynamic-checkout.error.generic", comment: "") + static let generic = StringResource("dynamic-checkout.error.generic", comment: "") /// Indicates that selected payment method is no longer available. - static let methodUnavailable = POStringResource("dynamic-checkout.error.method-unavailable", comment: "") + static let methodUnavailable = StringResource("dynamic-checkout.error.method-unavailable", comment: "") } enum Warning { /// APM redirect information. - static let redirect = POStringResource("dynamic-checkout.redirect-warning", comment: "") + static let redirect = StringResource("dynamic-checkout.redirect-warning", comment: "") } /// Success message. - static let successMessage = POStringResource("dynamic-checkout.success-message", comment: "") + static let successMessage = StringResource("dynamic-checkout.success-message", comment: "") } } diff --git a/Sources/ProcessOutUI/Sources/Modules/DynamicCheckout/Utils/AlternativePayment/DynamicCheckoutAlternativePaymentDefaultSession.swift b/Sources/ProcessOutUI/Sources/Modules/DynamicCheckout/Utils/AlternativePayment/DynamicCheckoutAlternativePaymentDefaultSession.swift deleted file mode 100644 index 198b099b8..000000000 --- a/Sources/ProcessOutUI/Sources/Modules/DynamicCheckout/Utils/AlternativePayment/DynamicCheckoutAlternativePaymentDefaultSession.swift +++ /dev/null @@ -1,36 +0,0 @@ -// -// DynamicCheckoutAlternativePaymentDefaultSession.swift -// ProcessOutUI -// -// Created by Andrii Vysotskyi on 25.03.2024. -// - -import Foundation -import ProcessOut - -@MainActor -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)) - } - // swiftlint:disable:next implicitly_unwrapped_optional - var continuation: UnsafeContinuation! - let session = POWebAuthenticationSession(alternativePaymentMethodUrl: url, returnUrl: returnUrl) { result in - continuation.resume(with: result) - } - guard await session.start() else { - throw POFailure(message: "Unable to start alternative payment.", code: .generic(.mobile)) - } - return try await withUnsafeThrowingContinuation { continuation = $0 } - } - - // MARK: - Private Properties - - private let configuration: PODynamicCheckoutAlternativePaymentConfiguration -} diff --git a/Sources/ProcessOutUI/Sources/Modules/DynamicCheckout/Utils/AlternativePayment/DynamicCheckoutAlternativePaymentSession.swift b/Sources/ProcessOutUI/Sources/Modules/DynamicCheckout/Utils/AlternativePayment/DynamicCheckoutAlternativePaymentSession.swift deleted file mode 100644 index 2f5e32ec2..000000000 --- a/Sources/ProcessOutUI/Sources/Modules/DynamicCheckout/Utils/AlternativePayment/DynamicCheckoutAlternativePaymentSession.swift +++ /dev/null @@ -1,15 +0,0 @@ -// -// DynamicCheckoutAlternativePaymentSession.swift -// ProcessOutUI -// -// Created by Andrii Vysotskyi on 17.03.2024. -// - -import Foundation -import ProcessOut - -protocol DynamicCheckoutAlternativePaymentSession { - - /// Starts alternative payment. - func start(url: URL) async throws -> POAlternativePaymentMethodResponse -} diff --git a/Sources/ProcessOutUI/Sources/Modules/DynamicCheckout/Utils/ApplePay/DynamicCheckoutApplePayTokenizationCoordinator.swift b/Sources/ProcessOutUI/Sources/Modules/DynamicCheckout/Utils/ApplePay/DynamicCheckoutApplePayTokenizationCoordinator.swift index d5e4ae204..59432f693 100644 --- a/Sources/ProcessOutUI/Sources/Modules/DynamicCheckout/Utils/ApplePay/DynamicCheckoutApplePayTokenizationCoordinator.swift +++ b/Sources/ProcessOutUI/Sources/Modules/DynamicCheckout/Utils/ApplePay/DynamicCheckoutApplePayTokenizationCoordinator.swift @@ -9,14 +9,15 @@ import Foundation import PassKit import ProcessOut +@MainActor final class DynamicCheckoutApplePayTokenizationCoordinator: POApplePayTokenizationDelegate { - init(didTokenizeCard: @escaping (POCard) async throws -> Void) { + nonisolated init(didTokenizeCard: @escaping (POCard) async throws -> Void) { self.didTokenizeCard = didTokenizeCard } /// Closure that is called when invoice is authorized. - let didTokenizeCard: (POCard) async throws -> Void + nonisolated(unsafe) private(set) var didTokenizeCard: (POCard) async throws -> Void // MARK: - diff --git a/Sources/ProcessOutUI/Sources/Modules/DynamicCheckout/Utils/ChildProvider/DynamicCheckoutInteractorChildProvider.swift b/Sources/ProcessOutUI/Sources/Modules/DynamicCheckout/Utils/ChildProvider/DynamicCheckoutInteractorChildProvider.swift index 15a470d1b..16922623e 100644 --- a/Sources/ProcessOutUI/Sources/Modules/DynamicCheckout/Utils/ChildProvider/DynamicCheckoutInteractorChildProvider.swift +++ b/Sources/ProcessOutUI/Sources/Modules/DynamicCheckout/Utils/ChildProvider/DynamicCheckoutInteractorChildProvider.swift @@ -9,6 +9,7 @@ /// - NOTE: Your implementation should expect that instances created /// by provider are going to be different every time you call a method. +@MainActor protocol DynamicCheckoutInteractorChildProvider { /// Creates and returns card tokenization interactor. diff --git a/Sources/ProcessOutUI/Sources/Modules/DynamicCheckout/Utils/ChildProvider/DynamicCheckoutInteractorDefaultChildProvider.swift b/Sources/ProcessOutUI/Sources/Modules/DynamicCheckout/Utils/ChildProvider/DynamicCheckoutInteractorDefaultChildProvider.swift index 46c5d7846..6c62f3123 100644 --- a/Sources/ProcessOutUI/Sources/Modules/DynamicCheckout/Utils/ChildProvider/DynamicCheckoutInteractorDefaultChildProvider.swift +++ b/Sources/ProcessOutUI/Sources/Modules/DynamicCheckout/Utils/ChildProvider/DynamicCheckoutInteractorDefaultChildProvider.swift @@ -99,8 +99,8 @@ final class DynamicCheckoutInteractorDefaultChildProvider: DynamicCheckoutIntera title: "", shouldHorizontallyCenterCodeInput: false, successMessage: "", - primaryActionTitle: "", - secondaryAction: nil, + primaryButtonTitle: "", + cancelButton: nil, inlineSingleSelectValuesLimit: configuration.alternativePayment.inlineSingleSelectValuesLimit, skipSuccessScreen: true, paymentConfirmation: alternativePaymentConfirmationConfiguration @@ -108,16 +108,16 @@ final class DynamicCheckoutInteractorDefaultChildProvider: DynamicCheckoutIntera return alternativePaymentConfiguration } - // swiftlint:disable:next identifier_name - private var alternativePaymentConfirmationConfiguration: PONativeAlternativePaymentConfirmationConfiguration { + // swiftlint:disable:next identifier_name line_length + private var alternativePaymentConfirmationConfiguration: PONativeAlternativePaymentConfiguration.PaymentConfirmation { let configuration = self.configuration.alternativePayment.paymentConfirmation - let confirmationConfiguration = PONativeAlternativePaymentConfirmationConfiguration( + let confirmationConfiguration = PONativeAlternativePaymentConfiguration.PaymentConfirmation( waitsConfirmation: true, timeout: configuration.timeout, showProgressIndicatorAfter: configuration.showProgressIndicatorAfter, hideGatewayDetails: true, - secondaryAction: configuration.cancelButton.map { configuration in - .cancel(title: "", disabledFor: configuration.disabledFor, confirmation: nil) + cancelButton: configuration.cancelButton.map { configuration in + .init(title: "", disabledFor: configuration.disabledFor, confirmation: nil) } ) return confirmationConfiguration diff --git a/Sources/ProcessOutUI/Sources/Modules/DynamicCheckout/View/DynamicCheckoutContentView.swift b/Sources/ProcessOutUI/Sources/Modules/DynamicCheckout/View/DynamicCheckoutContentView.swift index 078a2ad3d..da61dedad 100644 --- a/Sources/ProcessOutUI/Sources/Modules/DynamicCheckout/View/DynamicCheckoutContentView.swift +++ b/Sources/ProcessOutUI/Sources/Modules/DynamicCheckout/View/DynamicCheckoutContentView.swift @@ -6,7 +6,6 @@ // import SwiftUI -@_spi(PO) import ProcessOut @_spi(PO) import ProcessOutCoreUI @available(iOS 14, *) diff --git a/Sources/ProcessOutUI/Sources/Modules/DynamicCheckout/View/PODynamicCheckoutView+Init.swift b/Sources/ProcessOutUI/Sources/Modules/DynamicCheckout/View/PODynamicCheckoutView+Init.swift index b51a3724f..88ed3a487 100644 --- a/Sources/ProcessOutUI/Sources/Modules/DynamicCheckout/View/PODynamicCheckoutView+Init.swift +++ b/Sources/ProcessOutUI/Sources/Modules/DynamicCheckout/View/PODynamicCheckoutView+Init.swift @@ -21,13 +21,10 @@ extension PODynamicCheckoutView { completion: @escaping (Result) -> Void ) { let viewModel = { - let logger = ProcessOut.shared.logger + let logger: POLogger = ProcessOut.shared.logger let interactor = DynamicCheckoutDefaultInteractor( configuration: configuration, delegate: delegate, - alternativePaymentSession: DynamicCheckoutAlternativePaymentDefaultSession( - configuration: configuration.alternativePayment - ), childProvider: DynamicCheckoutInteractorDefaultChildProvider( configuration: configuration, cardsService: ProcessOut.shared.cards, @@ -36,6 +33,7 @@ extension PODynamicCheckoutView { logger: logger ), invoicesService: ProcessOut.shared.invoices, + alternativePaymentsService: ProcessOut.shared.alternativePayments, cardsService: ProcessOut.shared.cards, logger: logger, completion: completion diff --git a/Sources/ProcessOutUI/Sources/Modules/DynamicCheckout/View/PODynamicCheckoutView.swift b/Sources/ProcessOutUI/Sources/Modules/DynamicCheckout/View/PODynamicCheckoutView.swift index cad2465f6..350d9ef8b 100644 --- a/Sources/ProcessOutUI/Sources/Modules/DynamicCheckout/View/PODynamicCheckoutView.swift +++ b/Sources/ProcessOutUI/Sources/Modules/DynamicCheckout/View/PODynamicCheckoutView.swift @@ -6,7 +6,6 @@ // import SwiftUI -@_spi(PO) import ProcessOut @_spi(PO) import ProcessOutCoreUI /// Dynamic checkout root view. @@ -48,6 +47,7 @@ public struct PODynamicCheckoutView: View { } .backport.geometryGroup() .onAppear(perform: viewModel.start) + .onDisappear(perform: viewModel.stop) .poConfirmationDialog(item: $viewModel.state.confirmationDialog) } diff --git a/Sources/ProcessOutUI/Sources/Modules/DynamicCheckout/ViewModel/DefaultDynamicCheckoutViewModel.swift b/Sources/ProcessOutUI/Sources/Modules/DynamicCheckout/ViewModel/DefaultDynamicCheckoutViewModel.swift index 8e4e76121..200e50f27 100644 --- a/Sources/ProcessOutUI/Sources/Modules/DynamicCheckout/ViewModel/DefaultDynamicCheckoutViewModel.swift +++ b/Sources/ProcessOutUI/Sources/Modules/DynamicCheckout/ViewModel/DefaultDynamicCheckoutViewModel.swift @@ -28,6 +28,10 @@ final class DefaultDynamicCheckoutViewModel: ViewModel { $state.performWithoutAnimation(interactor.start) } + func stop() { + interactor.cancel() + } + // MARK: - Private Nested Types private enum ButtonId { diff --git a/Sources/ProcessOutUI/Sources/Modules/DynamicCheckout/ViewModel/DynamicCheckoutViewModelItem.swift b/Sources/ProcessOutUI/Sources/Modules/DynamicCheckout/ViewModel/DynamicCheckoutViewModelItem.swift index 397d22941..7620a5f76 100644 --- a/Sources/ProcessOutUI/Sources/Modules/DynamicCheckout/ViewModel/DynamicCheckoutViewModelItem.swift +++ b/Sources/ProcessOutUI/Sources/Modules/DynamicCheckout/ViewModel/DynamicCheckoutViewModelItem.swift @@ -170,3 +170,6 @@ extension DynamicCheckoutViewModelItem: Identifiable, AnimationIdentityProvider static let progressId = UUID().uuidString } } + +@available(*, unavailable) +extension DynamicCheckoutViewModelItem: Sendable { } diff --git a/Sources/ProcessOutUI/Sources/Modules/DynamicCheckout/ViewModel/DynamicCheckoutViewModelState.swift b/Sources/ProcessOutUI/Sources/Modules/DynamicCheckout/ViewModel/DynamicCheckoutViewModelState.swift index 7815bc0bd..87bc98d49 100644 --- a/Sources/ProcessOutUI/Sources/Modules/DynamicCheckout/ViewModel/DynamicCheckoutViewModelState.swift +++ b/Sources/ProcessOutUI/Sources/Modules/DynamicCheckout/ViewModel/DynamicCheckoutViewModelState.swift @@ -45,7 +45,9 @@ extension DynamicCheckoutViewModelState: AnimationIdentityProvider { [sections.map(\.animationIdentity), actions.map(\.id)] } - static let idle = DynamicCheckoutViewModelState(sections: [], actions: [], isCompleted: false) + static var idle: Self { + Self(sections: [], actions: [], isCompleted: false) + } } extension DynamicCheckoutViewModelState.Section: AnimationIdentityProvider { @@ -54,3 +56,6 @@ extension DynamicCheckoutViewModelState.Section: AnimationIdentityProvider { [id, items.map(\.animationIdentity), AnyHashable(isTight), AnyHashable(areBezelsVisible)] } } + +@available(*, unavailable) +extension DynamicCheckoutViewModelState: Sendable { } diff --git a/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/Configuration/PONativeAlternativePaymentConfiguration.swift b/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/Configuration/PONativeAlternativePaymentConfiguration.swift index ad9b4bba8..614632551 100644 --- a/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/Configuration/PONativeAlternativePaymentConfiguration.swift +++ b/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/Configuration/PONativeAlternativePaymentConfiguration.swift @@ -12,18 +12,66 @@ import ProcessOut /// Use `nil` to indicate that default value should be used. public struct PONativeAlternativePaymentConfiguration { - public enum SecondaryAction { - - /// Cancel action. - /// - /// - Parameters: - /// - title: Action title. Pass `nil` title to use default value. - /// - disabledFor: By default user can interact with action immediately after it becomes visible, it is - /// possible to make it initially disabled for given amount of time. - /// - confirmation: When property is set implementation asks user to confirm cancel. - case cancel( - title: String? = nil, disabledFor: TimeInterval = 0, confirmation: POConfirmationDialogConfiguration? = nil - ) + /// Payment confirmation configuration. + public struct PaymentConfirmation { + + /// Boolean value that specifies whether module should wait for payment confirmation from PSP or will + /// complete right after all user's input is submitted. Default value is `true`. + public let waitsConfirmation: Bool + + /// Amount of time (in seconds) that module is allowed to wait before receiving final payment confirmation. + /// Default timeout is 3 minutes while maximum value is 15 minutes. + public let timeout: TimeInterval + + /// A delay before showing progress indicator during payment confirmation. + public let showProgressIndicatorAfter: TimeInterval? + + /// Boolean value indicating whether gateway information (such as name/logo) should stay hidden + /// during payment confirmation even if more specific payment provider details are not available. + /// Default value is `false`. + public let hideGatewayDetails: Bool + + /// Button that could be optionally presented to user during payment confirmation stage. To remove it + /// use `nil`, this is default behaviour. + public let cancelButton: CancelButton? + + /// Creates configuration instance. + public init( + waitsConfirmation: Bool = true, + timeout: TimeInterval = 180, + showProgressIndicatorAfter: TimeInterval? = nil, + hideGatewayDetails: Bool = false, + cancelButton: CancelButton? = nil + ) { + self.waitsConfirmation = waitsConfirmation + self.timeout = timeout + self.showProgressIndicatorAfter = showProgressIndicatorAfter + self.hideGatewayDetails = hideGatewayDetails + self.cancelButton = cancelButton + } + } + + public struct CancelButton: Sendable { + + /// Cancel button title. Use `nil` for default title. + public let title: String? + + /// By default user can interact with action immediately after it becomes visible, it is + /// possible to make it initially disabled for given amount of time. + public let disabledFor: TimeInterval + + /// When property is set implementation asks user to confirm cancel. + public let confirmation: POConfirmationDialogConfiguration? + + public init( + title: String? = nil, + disabledFor: TimeInterval = 0, + confirmation: POConfirmationDialogConfiguration? = nil + ) { + self.title = title + self.disabledFor = disabledFor + self.confirmation = confirmation + } } /// Invoice that should be authorized/captured. @@ -43,11 +91,11 @@ public struct PONativeAlternativePaymentConfiguration { /// Custom success message **markdown** to display user when payment completes. public let successMessage: String? - /// Primary action text, such as "Pay". - public let primaryActionTitle: String? + /// Primary button text, such as "Pay". + public let primaryButtonTitle: String? - /// Secondary action. To remove secondary action use `nil`, this is default behaviour. - public let secondaryAction: SecondaryAction? + /// Cancel button. To remove cancel button use `nil`, this is default behaviour. + public let cancelButton: CancelButton? /// 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). @@ -59,59 +107,7 @@ public struct PONativeAlternativePaymentConfiguration { public let skipSuccessScreen: Bool /// Payment confirmation configuration. - public let paymentConfirmation: PONativeAlternativePaymentConfirmationConfiguration - - /// Boolean value that specifies whether module should wait for payment confirmation from PSP or will - /// complete right after all user's input is submitted. Default value is `true`. - @available(*, deprecated, renamed: "paymentConfirmation.waitsConfirmation") - public var waitsPaymentConfirmation: Bool { - paymentConfirmation.waitsConfirmation - } - - /// Amount of time (in seconds) that module is allowed to wait before receiving final payment confirmation. - /// Default timeout is 3 minutes while maximum value is 15 minutes. - @available(*, deprecated, renamed: "paymentConfirmation.timeout") - public var paymentConfirmationTimeout: TimeInterval { - paymentConfirmation.timeout - } - - /// Action that could be optionally presented to user during payment confirmation stage. To remove action - /// use `nil`, this is default behaviour. - @available(*, deprecated, renamed: "paymentConfirmation.secondaryAction") - public var paymentConfirmationSecondaryAction: SecondaryAction? { - paymentConfirmation.secondaryAction - } - - /// Creates configuration instance. - @available(*, deprecated) - public init( - invoiceId: String, - gatewayConfigurationId: String, - title: String? = nil, - successMessage: String? = nil, - primaryActionTitle: String? = nil, - secondaryAction: SecondaryAction? = nil, - inlineSingleSelectValuesLimit: Int = 5, - skipSuccessScreen: Bool = false, - waitsPaymentConfirmation: Bool = true, - paymentConfirmationTimeout: TimeInterval = 180, - paymentConfirmationSecondaryAction: SecondaryAction? = nil - ) { - self.invoiceId = invoiceId - self.gatewayConfigurationId = gatewayConfigurationId - self.title = title - self.shouldHorizontallyCenterCodeInput = true - self.successMessage = successMessage - self.primaryActionTitle = primaryActionTitle - self.secondaryAction = secondaryAction - self.inlineSingleSelectValuesLimit = inlineSingleSelectValuesLimit - self.skipSuccessScreen = skipSuccessScreen - self.paymentConfirmation = .init( - waitsConfirmation: waitsPaymentConfirmation, - timeout: paymentConfirmationTimeout, - secondaryAction: paymentConfirmationSecondaryAction - ) - } + public let paymentConfirmation: PaymentConfirmation /// Creates configuration instance. public init( @@ -120,19 +116,19 @@ public struct PONativeAlternativePaymentConfiguration { title: String? = nil, shouldHorizontallyCenterCodeInput: Bool = true, successMessage: String? = nil, - primaryActionTitle: String? = nil, - secondaryAction: SecondaryAction? = nil, + primaryButtonTitle: String? = nil, + cancelButton: CancelButton? = nil, inlineSingleSelectValuesLimit: Int = 5, skipSuccessScreen: Bool = false, - paymentConfirmation: PONativeAlternativePaymentConfirmationConfiguration = .init() + paymentConfirmation: PaymentConfirmation = .init() ) { self.invoiceId = invoiceId self.gatewayConfigurationId = gatewayConfigurationId self.title = title self.shouldHorizontallyCenterCodeInput = shouldHorizontallyCenterCodeInput self.successMessage = successMessage - self.primaryActionTitle = primaryActionTitle - self.secondaryAction = secondaryAction + self.primaryButtonTitle = primaryButtonTitle + self.cancelButton = cancelButton self.inlineSingleSelectValuesLimit = inlineSingleSelectValuesLimit self.skipSuccessScreen = skipSuccessScreen self.paymentConfirmation = paymentConfirmation diff --git a/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/Configuration/PONativeAlternativePaymentConfirmation.swift b/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/Configuration/PONativeAlternativePaymentConfirmation.swift deleted file mode 100644 index 22df337db..000000000 --- a/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/Configuration/PONativeAlternativePaymentConfirmation.swift +++ /dev/null @@ -1,47 +0,0 @@ -// -// PONativeAlternativePaymentConfirmation.swift -// ProcessOutUI -// -// Created by Andrii Vysotskyi on 19.03.2024. -// - -import Foundation - -/// Configuration specific to native APM payment confirmation. -public struct PONativeAlternativePaymentConfirmationConfiguration { // swiftlint:disable:this type_name - - /// Boolean value that specifies whether module should wait for payment confirmation from PSP or will - /// complete right after all user's input is submitted. Default value is `true`. - public let waitsConfirmation: Bool - - /// Amount of time (in seconds) that module is allowed to wait before receiving final payment confirmation. - /// Default timeout is 3 minutes while maximum value is 15 minutes. - public let timeout: TimeInterval - - /// A delay before showing progress indicator during payment confirmation. - public let showProgressIndicatorAfter: TimeInterval? - - /// Boolean value indicating whether gateway information (such as name/logo) should stay hidden - /// during payment confirmation even if more specific payment provider details are not available. - /// Default value is `false`. - public let hideGatewayDetails: Bool - - /// Action that could be optionally presented to user during payment confirmation stage. To remove action - /// use `nil`, this is default behaviour. - public let secondaryAction: PONativeAlternativePaymentConfiguration.SecondaryAction? - - /// Creates configuration instance. - public init( - waitsConfirmation: Bool = true, - timeout: TimeInterval = 180, - showProgressIndicatorAfter: TimeInterval? = nil, - hideGatewayDetails: Bool = false, - secondaryAction: PONativeAlternativePaymentConfiguration.SecondaryAction? = nil - ) { - self.waitsConfirmation = waitsConfirmation - self.timeout = timeout - self.showProgressIndicatorAfter = showProgressIndicatorAfter - self.hideGatewayDetails = hideGatewayDetails - self.secondaryAction = secondaryAction - } -} diff --git a/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/Delegate/PONativeAlternativePaymentDelegate.swift b/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/Delegate/PONativeAlternativePaymentDelegate.swift index 1ccdf3af4..8e804e4a4 100644 --- a/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/Delegate/PONativeAlternativePaymentDelegate.swift +++ b/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/Delegate/PONativeAlternativePaymentDelegate.swift @@ -1,13 +1,38 @@ // -// PONativeAlternativePaymentDelegate.swift -// ProcessOutUI +// PONativeAlternativePaymentMethodDelegate.swift +// ProcessOut // -// Created by Andrii Vysotskyi on 23.11.2023. +// Created by Andrii Vysotskyi on 08.02.2023. // import ProcessOut /// Native alternative payment module delegate definition. -/// -/// See original [protocol](https://swiftpackageindex.com/processout/processout-ios/documentation/processout/ponativealternativepaymentmethoddelegate) for details. -public typealias PONativeAlternativePaymentDelegate = PONativeAlternativePaymentMethodDelegate +public protocol PONativeAlternativePaymentDelegate: AnyObject, Sendable { + + /// Invoked when module emits event. + @MainActor + func nativeAlternativePayment(didEmitEvent event: PONativeAlternativePaymentEvent) + + /// Method provides an ability to supply default values for given parameters. Completion expects dictionary + /// where key is a parameter key, and value is desired default. It is not mandatory to provide defaults for + /// all parameters. + /// - NOTE: completion must be called on `main` thread. + func nativeAlternativePayment( + defaultsFor parameters: [PONativeAlternativePaymentMethodParameter] + ) async -> [String: String] +} + +extension PONativeAlternativePaymentDelegate { + + @MainActor + public func nativeAlternativePayment(didEmitEvent event: PONativeAlternativePaymentEvent) { + // Ignored + } + + public func nativeAlternativePayment( + defaultsFor parameters: [PONativeAlternativePaymentMethodParameter] + ) async -> [String: String] { + [:] + } +} diff --git a/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/Delegate/PONativeAlternativePaymentEvent.swift b/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/Delegate/PONativeAlternativePaymentEvent.swift index 3da8a73ba..5daba51d2 100644 --- a/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/Delegate/PONativeAlternativePaymentEvent.swift +++ b/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/Delegate/PONativeAlternativePaymentEvent.swift @@ -2,12 +2,72 @@ // PONativeAlternativePaymentEvent.swift // ProcessOutUI // -// Created by Andrii Vysotskyi on 23.11.2023. +// Created by Andrii Vysotskyi on 07.02.2023. // import ProcessOut /// Describes events that could happen during native alternative payment module lifecycle. -/// -/// See original [enum](https://swiftpackageindex.com/processout/processout-ios/documentation/processout/ponativealternativepaymentmethodevent) for details. -public typealias PONativeAlternativePaymentEvent = PONativeAlternativePaymentMethodEvent +public enum PONativeAlternativePaymentEvent: Sendable { + + public struct WillSubmitParameters: Sendable { + + /// Available parameters. + public let parameters: [PONativeAlternativePaymentMethodParameter] + + /// Parameter values. + /// - NOTE: For parameters other than `singleSelect` values are user facing including formatting. + /// - WARNING: Values could include sensitive information so make sure to protect them accordingly. + public let values: [String: String] + } + + public struct ParametersChanged: Sendable { + + /// Parameter definition that the user changed. + public let parameter: PONativeAlternativePaymentMethodParameter + + /// Parameter value. + /// - NOTE: For parameters other than `singleSelect` this is user facing value including formatting + /// - WARNING: Value could include sensitive information so make sure to protect it accordingly. + public let value: String + } + + /// Initial event that is sent prior any other event. + case willStart + + /// Indicates that implementation successfully loaded initial portion of data and currently waiting for user + /// to fulfil needed info. + case didStart + + /// This event is emitted when a user clicks the "Cancel payment" button, prompting the system to display a + /// confirmation dialog. This event signifies the initiation of the cancellation confirmation process. + /// + /// This event can be used for tracking user interactions related to payment cancellations. It helps in + /// understanding user behaviour, particularly the frequency and context in which users consider canceling a payment. + case didRequestCancelConfirmation + + /// Event is sent when the user changes any editable value. + case parametersChanged(ParametersChanged) + + /// Event is sent just before sending user input, this is usually a result of a user action, e.g. button press. + case willSubmitParameters(WillSubmitParameters) + + /// Sent in case parameters were submitted successfully. You could inspect the associated value to understand + /// whether additional input is required. + case didSubmitParameters(additionalParametersExpected: Bool) + + /// Sent in case parameters submission failed and if error is retriable, otherwise expect `didFail` event. + case didFailToSubmitParameters(failure: POFailure) + + /// Event is sent after all information is collected, and implementation is waiting for a PSP to confirm capture. + /// You could check associated value `additionalActionExpected` to understand whether user needs + /// to execute additional action(s) outside application, for example confirming operation in his/her banking app + /// to make capture happen. + case willWaitForCaptureConfirmation(additionalActionExpected: Bool) + + /// Event is sent after payment was confirmed to be captured. This is a final event. + case didCompletePayment + + /// Event is sent in case unretryable error occurs. This is a final event. + case didFail(failure: POFailure) +} diff --git a/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/Interactor/NativeAlternativePaymentDefaultInteractor.swift b/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/Interactor/NativeAlternativePaymentDefaultInteractor.swift index 3f5e88913..745bd65dd 100644 --- a/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/Interactor/NativeAlternativePaymentDefaultInteractor.swift +++ b/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/Interactor/NativeAlternativePaymentDefaultInteractor.swift @@ -121,7 +121,6 @@ final class NativeAlternativePaymentDefaultInteractor: // MARK: - Starting State - @MainActor private func continueStartUnchecked() async { let details: PONativeAlternativePaymentMethodTransactionDetails do { @@ -143,7 +142,7 @@ final class NativeAlternativePaymentDefaultInteractor: amount: details.invoice.amount, currencyCode: details.invoice.currencyCode, parameters: await createParameters(specifications: details.parameters), - isCancellable: disableDuration(of: configuration.secondaryAction).isZero + isCancellable: (configuration.cancelButton?.disabledFor ?? 0).isZero ) setStateUnchecked(.started(startedState)) send(event: .didStart) @@ -164,7 +163,6 @@ final class NativeAlternativePaymentDefaultInteractor: // MARK: - Submission State - @MainActor private func continueSubmissionUnchecked( startedState: NativeAlternativePaymentInteractorState.Started, values: [String: String] ) async { @@ -180,19 +178,19 @@ final class NativeAlternativePaymentDefaultInteractor: restoreStartedStateAfterSubmissionFailureIfPossible(error, replaceErrorMessages: true) return } - switch response.nativeApm.state { + switch response.state { case .pendingCapture: send(event: .didSubmitParameters(additionalParametersExpected: false)) await setAwaitingCaptureStateUnchecked( - gateway: startedState.gateway, parameterValues: response.nativeApm.parameterValues + gateway: startedState.gateway, parameterValues: response.parameterValues ) case .captured: send(event: .didSubmitParameters(additionalParametersExpected: false)) await setCapturedStateUnchecked( - gateway: startedState.gateway, parameterValues: response.nativeApm.parameterValues + gateway: startedState.gateway, parameterValues: response.parameterValues ) case .customerInput: - await restoreStartedStateAfterSubmission(nativeApm: response.nativeApm) + await restoreStartedStateAfterSubmission(nativeApm: response) case .failed: fallthrough // swiftlint:disable:this fallthrough @unknown default: @@ -203,7 +201,6 @@ final class NativeAlternativePaymentDefaultInteractor: // MARK: - Awaiting Capture State - @MainActor private func setAwaitingCaptureStateUnchecked( gateway: PONativeAlternativePaymentMethodTransactionDetails.Gateway, parameterValues: PONativeAlternativePaymentMethodParameterValues? @@ -223,7 +220,7 @@ final class NativeAlternativePaymentDefaultInteractor: logoImage: logoImage, actionMessage: actionMessage, actionImage: actionImage, - isCancellable: disableDuration(of: configuration.paymentConfirmation.secondaryAction).isZero, + isCancellable: (configuration.paymentConfirmation.cancelButton?.disabledFor ?? 0).isZero, isDelayed: false ) setStateUnchecked(.awaitingCapture(awaitingCaptureState)) @@ -250,7 +247,8 @@ final class NativeAlternativePaymentDefaultInteractor: guard let timeInterval = configuration.paymentConfirmation.showProgressIndicatorAfter else { return } - Timer.scheduledTimer(withTimeInterval: timeInterval, repeats: false) { [weak self] _ in + Task { [weak self] in + try? await Task.sleep(seconds: timeInterval) guard let self, case .awaitingCapture(var awaitingCaptureState) = self.state else { return } @@ -261,7 +259,6 @@ final class NativeAlternativePaymentDefaultInteractor: // MARK: - Captured State - @MainActor private func setCapturedStateUnchecked( gateway: PONativeAlternativePaymentMethodTransactionDetails.Gateway, parameterValues: PONativeAlternativePaymentMethodParameterValues? @@ -330,10 +327,7 @@ final class NativeAlternativePaymentDefaultInteractor: logger.debug("One or more parameters are not valid: \(invalidFields), waiting for parameters to update") } - @MainActor - private func restoreStartedStateAfterSubmission( - nativeApm: PONativeAlternativePaymentMethodResponse.NativeApm - ) async { + private func restoreStartedStateAfterSubmission(nativeApm: PONativeAlternativePaymentMethodResponse) async { guard case var .submitting(startedState) = state else { return } @@ -370,9 +364,8 @@ final class NativeAlternativePaymentDefaultInteractor: // MARK: - Cancellation Availability - @MainActor private func enableCancellationAfterDelay() { - let disabledFor = disableDuration(of: configuration.secondaryAction) + let disabledFor = configuration.cancelButton?.disabledFor ?? 0 guard disabledFor > 0 else { logger.debug("Cancel action is not set or initially enabled.") return @@ -392,9 +385,8 @@ final class NativeAlternativePaymentDefaultInteractor: } } - @MainActor private func enableCaptureCancellationAfterDelay() { - let disabledFor = disableDuration(of: configuration.paymentConfirmation.secondaryAction) + let disabledFor = configuration.paymentConfirmation.cancelButton?.disabledFor ?? 0 guard disabledFor > 0 else { logger.debug("Confirmation cancel action is not set or initially enabled.") return @@ -411,10 +403,10 @@ final class NativeAlternativePaymentDefaultInteractor: // MARK: - Events - private func send(event: PONativeAlternativePaymentMethodEvent) { - assert(Thread.isMainThread, "Method should be called on main thread.") + @MainActor + private func send(event: PONativeAlternativePaymentEvent) { logger.debug("Did send event: '\(event)'") - delegate?.nativeAlternativePaymentMethodDidEmitEvent(event) + delegate?.nativeAlternativePayment(didEmitEvent: event) } private func didUpdate(parameter: NativeAlternativePaymentInteractorState.Parameter, to value: String) { @@ -441,7 +433,6 @@ final class NativeAlternativePaymentDefaultInteractor: self.state = state } - @MainActor private func createParameters( specifications: [PONativeAlternativePaymentMethodParameter] ) async -> [NativeAlternativePaymentInteractorState.Parameter] { @@ -449,7 +440,7 @@ final class NativeAlternativePaymentDefaultInteractor: let formatter: Foundation.Formatter? switch specification.type { case .phone: - formatter = POPhoneNumberFormatter() + formatter = PhoneNumberFormatter() default: formatter = nil } @@ -463,7 +454,7 @@ final class NativeAlternativePaymentDefaultInteractor: // Server doesn't support localized error messages, so local generic error // description is used instead in case particular field is invalid. // todo(andrii-vysotskyi): remove when backend is updated - let resource: POStringResource + let resource: StringResource switch parameterType { case .numeric: resource = .NativeAlternativePayment.Error.invalidNumber @@ -490,39 +481,25 @@ final class NativeAlternativePaymentDefaultInteractor: return gateway.logoUrl } - private func disableDuration(of action: PONativeAlternativePaymentConfiguration.SecondaryAction?) -> TimeInterval { - guard case .cancel(_, let disabledFor, _) = action else { - return 0 - } - return disabledFor - } - // MARK: - Default Values /// Updates parameters with default values. - @MainActor private func setDefaultValues( parameters: inout [NativeAlternativePaymentInteractorState.Parameter] ) async { guard !parameters.isEmpty else { return } - let defaultValues = await withCheckedContinuation { continuation in - if let delegate { - delegate.nativeAlternativePaymentMethodDefaultValues( - for: parameters.map(\.specification), completion: continuation.resume - ) - } else { - continuation.resume(returning: [:]) - } - } + let defaultValues = await delegate?.nativeAlternativePayment( + defaultsFor: parameters.map(\.specification) + ) ?? [:] for (offset, parameter) in parameters.enumerated() { let defaultValue: String? if let value = defaultValues[parameter.specification.key] { switch parameter.specification.type { case .singleSelect: let availableValues = parameter.specification.availableValues?.map(\.value) ?? [] - precondition(availableValues.contains(value), "Unknown `singleSelect` parameter value.") + assert(availableValues.contains(value), "Unknown `singleSelect` parameter value.") defaultValue = value default: defaultValue = parameter.formatter?.string(for: value) ?? value @@ -554,7 +531,7 @@ final class NativeAlternativePaymentDefaultInteractor: parameters.forEach { parameter in var normalizedValue = parameter.value if case .phone = parameter.specification.type, let value = normalizedValue { - normalizedValue = POPhoneNumberFormatter().normalized(number: value) + normalizedValue = PhoneNumberFormatter().normalized(number: value) } if let normalizedValue, normalizedValue != parameter.value { logger.debug("Will use updated value '\(normalizedValue)' for key '\(parameter.specification.key)'") diff --git a/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/Interactor/NativeAlternativePaymentInteractor.swift b/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/Interactor/NativeAlternativePaymentInteractor.swift index 833e28571..357ba0e3f 100644 --- a/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/Interactor/NativeAlternativePaymentInteractor.swift +++ b/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/Interactor/NativeAlternativePaymentInteractor.swift @@ -5,6 +5,7 @@ // Created by Andrii Vysotskyi on 29.02.2024. // +@MainActor protocol NativeAlternativePaymentInteractor: Interactor { /// Configuration. diff --git a/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/Style/PONativeAlternativePaymentBackgroundStyle.swift b/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/Style/PONativeAlternativePaymentBackgroundStyle.swift index 721f48c90..6a3d48794 100644 --- a/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/Style/PONativeAlternativePaymentBackgroundStyle.swift +++ b/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/Style/PONativeAlternativePaymentBackgroundStyle.swift @@ -9,6 +9,7 @@ import SwiftUI @_spi(PO) import ProcessOutCoreUI /// Native alternative payment method screen background style. +@MainActor public struct PONativeAlternativePaymentBackgroundStyle { /// Regular background color. diff --git a/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/Style/PONativeAlternativePaymentStyle.swift b/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/Style/PONativeAlternativePaymentStyle.swift index 4f95a21a9..d1e55cb0e 100644 --- a/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/Style/PONativeAlternativePaymentStyle.swift +++ b/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/Style/PONativeAlternativePaymentStyle.swift @@ -10,6 +10,7 @@ import SwiftUI /// Defines style for native alternative payment module. @available(iOS 14, *) +@MainActor public struct PONativeAlternativePaymentStyle { /// Title style. diff --git a/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/Style/View+NativeAlternativePaymentMethodStyle.swift b/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/Style/View+NativeAlternativePaymentMethodStyle.swift index 4d0233e1f..7ae99467d 100644 --- a/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/Style/View+NativeAlternativePaymentMethodStyle.swift +++ b/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/Style/View+NativeAlternativePaymentMethodStyle.swift @@ -26,7 +26,8 @@ extension EnvironmentValues { // MARK: - Private Nested Types - private struct Key: EnvironmentKey { + @MainActor + private struct Key: @preconcurrency EnvironmentKey { static let defaultValue = PONativeAlternativePaymentStyle.default } } diff --git a/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/Symbols/StringResource+NativeAlternativePayment.swift b/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/Symbols/StringResource+NativeAlternativePayment.swift index 1bf41c1f5..08ec8a49d 100644 --- a/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/Symbols/StringResource+NativeAlternativePayment.swift +++ b/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/Symbols/StringResource+NativeAlternativePayment.swift @@ -5,65 +5,63 @@ // Created by Andrii Vysotskyi on 23.11.2023. // -@_spi(PO) import ProcessOut - // swiftlint:disable nesting -extension POStringResource { +extension StringResource { enum NativeAlternativePayment { /// Screen title. - static let title = POStringResource("native-alternative-payment.title", comment: "") + static let title = StringResource("native-alternative-payment.title", comment: "") enum Placeholder { /// Email placeholder. - static let email = POStringResource("native-alternative-payment.email.placeholder", comment: "") + static let email = StringResource("native-alternative-payment.email.placeholder", comment: "") /// Phone placeholder. - static let phone = POStringResource("native-alternative-payment.phone.placeholder", comment: "") + static let phone = StringResource("native-alternative-payment.phone.placeholder", comment: "") } enum Button { /// Pay. - static let submit = POStringResource("native-alternative-payment.submit-button.default-title", comment: "") + static let submit = StringResource("native-alternative-payment.submit-button.default-title", comment: "") /// Pay %@ - static let submitAmount = POStringResource("native-alternative-payment.submit-button.title", comment: "") + static let submitAmount = StringResource("native-alternative-payment.submit-button.title", comment: "") /// Cancel button title. - static let cancel = POStringResource("native-alternative-payment.cancel-button.title", comment: "") + static let cancel = StringResource("native-alternative-payment.cancel-button.title", comment: "") } enum Success { /// Success message. - static let message = POStringResource("native-alternative-payment.success.message", comment: "") + static let message = StringResource("native-alternative-payment.success.message", comment: "") } enum Error { /// Email is not valid. - static let invalidEmail = POStringResource("native-alternative-payment.error.invalid-email", comment: "") + static let invalidEmail = StringResource("native-alternative-payment.error.invalid-email", comment: "") /// Plural format key: "%#@length@" - static let invalidLength = POStringResource( + static let invalidLength = StringResource( "native-alternative-payment.error.invalid-length-%d", comment: "" ) /// Number is not valid. - static let invalidNumber = POStringResource("native-alternative-payment.error.invalid-number", comment: "") + static let invalidNumber = StringResource("native-alternative-payment.error.invalid-number", comment: "") /// Phone number is not valid. - static let invalidPhone = POStringResource("native-alternative-payment.error.invalid-phone", comment: "") + static let invalidPhone = StringResource("native-alternative-payment.error.invalid-phone", comment: "") /// Value is not valid. - static let invalidValue = POStringResource("native-alternative-payment.error.invalid-value", comment: "") + static let invalidValue = StringResource("native-alternative-payment.error.invalid-value", comment: "") /// Parameter is required. - static let requiredParameter = POStringResource( + static let requiredParameter = StringResource( "native-alternative-payment.error.required-parameter", comment: "" ) } @@ -71,13 +69,13 @@ extension POStringResource { enum CancelConfirmation { /// Success message. - static let title = POStringResource("cancel-confirmation.title", comment: "") + static let title = StringResource("cancel-confirmation.title", comment: "") /// Confirm button title.. - static let confirm = POStringResource("cancel-confirmation.confirm", comment: "") + static let confirm = StringResource("cancel-confirmation.confirm", comment: "") /// Cancel button title. - static let cancel = POStringResource("cancel-confirmation.cancel", comment: "") + static let cancel = StringResource("cancel-confirmation.cancel", comment: "") } } } diff --git a/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/View/NativeAlternativePaymentContentView.swift b/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/View/NativeAlternativePaymentContentView.swift index d3eaba5e5..d47de5485 100644 --- a/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/View/NativeAlternativePaymentContentView.swift +++ b/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/View/NativeAlternativePaymentContentView.swift @@ -69,7 +69,7 @@ struct NativeAlternativePaymentContentView: View { return (top: sections, center: []) } let index = sections.firstIndex { section in - section.items.contains(where: shouldCenter) + section.items.contains { shouldCenter(item: $0) } } guard let index else { return (top: sections, center: []) diff --git a/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/View/PONativeAlternativePaymentView+Init.swift b/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/View/PONativeAlternativePaymentView+Init.swift index 5b17bc11e..6b422d781 100644 --- a/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/View/PONativeAlternativePaymentView+Init.swift +++ b/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/View/PONativeAlternativePaymentView+Init.swift @@ -24,7 +24,7 @@ extension PONativeAlternativePaymentView { completion: @escaping (Result) -> Void ) { let viewModel = { - var logger = ProcessOut.shared.logger + var logger: POLogger = ProcessOut.shared.logger logger[attributeKey: .invoiceId] = configuration.invoiceId logger[attributeKey: .gatewayConfigurationId] = configuration.gatewayConfigurationId let interactor = NativeAlternativePaymentDefaultInteractor( diff --git a/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/View/PONativeAlternativePaymentView.swift b/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/View/PONativeAlternativePaymentView.swift index 119811f3a..0888c61ef 100644 --- a/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/View/PONativeAlternativePaymentView.swift +++ b/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/View/PONativeAlternativePaymentView.swift @@ -37,6 +37,7 @@ public struct PONativeAlternativePaymentView: View { .animation(.default, value: viewModel.state.isCaptured) } .onAppear(perform: viewModel.start) + .onDisappear(perform: viewModel.stop) .poConfirmationDialog(item: $viewModel.state.confirmationDialog) } diff --git a/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/ViewModel/DefaultNativeAlternativePaymentViewModel.swift b/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/ViewModel/DefaultNativeAlternativePaymentViewModel.swift index b7c891a77..f8d3f5db2 100644 --- a/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/ViewModel/DefaultNativeAlternativePaymentViewModel.swift +++ b/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/ViewModel/DefaultNativeAlternativePaymentViewModel.swift @@ -18,10 +18,6 @@ final class DefaultNativeAlternativePaymentViewModel: ViewModel { observeChanges(interactor: interactor) } - deinit { - interactor.cancel() - } - // MARK: - NativeAlternativePaymentViewModel @AnimatablePublished @@ -31,6 +27,10 @@ final class DefaultNativeAlternativePaymentViewModel: ViewModel { $state.performWithoutAnimation(interactor.start) } + func stop() { + interactor.cancel() + } + // MARK: - Private Nested Types private typealias InteractorState = NativeAlternativePaymentInteractorState @@ -135,7 +135,7 @@ final class DefaultNativeAlternativePaymentViewModel: ViewModel { ) -> [POActionsContainerActionViewModel] { let actions = [ submitAction(state: state, isLoading: isSubmitting), - cancelAction(configuration: configuration.secondaryAction, isEnabled: !isSubmitting && state.isCancellable) + cancelAction(configuration: configuration.cancelButton, isEnabled: !isSubmitting && state.isCancellable) ] return actions.compactMap { $0 } } @@ -215,7 +215,7 @@ final class DefaultNativeAlternativePaymentViewModel: ViewModel { private func createActions(state: InteractorState.AwaitingCapture) -> [POActionsContainerActionViewModel] { let actions = [ cancelAction( - configuration: configuration.paymentConfirmation.secondaryAction, isEnabled: state.isCancellable + configuration: configuration.paymentConfirmation.cancelButton, isEnabled: state.isCancellable ) ] return actions.compactMap { $0 } @@ -365,7 +365,7 @@ final class DefaultNativeAlternativePaymentViewModel: ViewModel { private func submitAction(state: InteractorState.Started, isLoading: Bool) -> POActionsContainerActionViewModel? { let title: String - if let customTitle = configuration.primaryActionTitle { + if let customTitle = configuration.primaryButtonTitle { title = customTitle } else { priceFormatter.currencyCode = state.currencyCode @@ -393,19 +393,19 @@ final class DefaultNativeAlternativePaymentViewModel: ViewModel { } private func cancelAction( - configuration: PONativeAlternativePaymentConfiguration.SecondaryAction?, isEnabled: Bool + configuration: PONativeAlternativePaymentConfiguration.CancelButton?, isEnabled: Bool ) -> POActionsContainerActionViewModel? { - guard case let .cancel(title, _, confirmation) = configuration else { + guard let configuration else { return nil } let action = POActionsContainerActionViewModel( id: "native-alternative-payment.secondary-button", - title: title ?? String(resource: .NativeAlternativePayment.Button.cancel), + title: configuration.title ?? String(resource: .NativeAlternativePayment.Button.cancel), isEnabled: isEnabled, isLoading: false, isPrimary: false, action: { [weak self] in - self?.cancelPayment(confirmationConfiguration: confirmation) + self?.cancelPayment(confirmationConfiguration: configuration.confirmation) } ) return action diff --git a/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/ViewModel/NativeAlternativePaymentViewModelState.swift b/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/ViewModel/NativeAlternativePaymentViewModelState.swift index c76e60cda..7fe02eb5a 100644 --- a/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/ViewModel/NativeAlternativePaymentViewModelState.swift +++ b/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/ViewModel/NativeAlternativePaymentViewModelState.swift @@ -35,5 +35,7 @@ extension NativeAlternativePaymentViewModelState: AnimationIdentityProvider { extension NativeAlternativePaymentViewModelState { /// Idle state. - static let idle = Self(sections: [], actions: [], isCaptured: false, focusedItemId: nil, confirmationDialog: nil) + static var idle: Self { + Self(sections: [], actions: [], isCaptured: false, focusedItemId: nil, confirmationDialog: nil) + } } diff --git a/Sources/ProcessOutUI/Sources/Modules/PassKitPaymentAuthorization/POPassKitPaymentAuthorizationController.swift b/Sources/ProcessOutUI/Sources/Modules/PassKitPaymentAuthorization/POPassKitPaymentAuthorizationController.swift index f3ac0ad17..18729d2dc 100644 --- a/Sources/ProcessOutUI/Sources/Modules/PassKitPaymentAuthorization/POPassKitPaymentAuthorizationController.swift +++ b/Sources/ProcessOutUI/Sources/Modules/PassKitPaymentAuthorization/POPassKitPaymentAuthorizationController.swift @@ -10,20 +10,21 @@ import PassKit /// An object that presents a sheet that prompts the user to authorize a payment request @available(*, deprecated, message: "Tokenize payments using cards service accessible via ProcessOut.shared.cards") +@MainActor public final class POPassKitPaymentAuthorizationController: NSObject { /// Determine whether this device can process payment requests. - public static func canMakePayments() -> Bool { + public nonisolated static func canMakePayments() -> Bool { PKPaymentAuthorizationController.canMakePayments() } /// Determine whether this device can process payment requests using specific payment network brands. - public static func canMakePayments(usingNetworks supportedNetworks: [PKPaymentNetwork]) -> Bool { + public nonisolated static func canMakePayments(usingNetworks supportedNetworks: [PKPaymentNetwork]) -> Bool { PKPaymentAuthorizationController.canMakePayments(usingNetworks: supportedNetworks) } /// Determine whether this device can process payments using the specified networks and capabilities bitmask. - public static func canMakePayments( + public nonisolated static func canMakePayments( usingNetworks supportedNetworks: [PKPaymentNetwork], capabilities: PKMerchantCapability ) -> Bool { PKPaymentAuthorizationController.canMakePayments(usingNetworks: supportedNetworks, capabilities: capabilities) @@ -31,10 +32,10 @@ public final class POPassKitPaymentAuthorizationController: NSObject { /// Initialize the controller with a payment request. public init?(paymentRequest: PKPaymentRequest) { - if PKPaymentAuthorizationViewController(paymentRequest: paymentRequest) == nil { + guard Self.canMakePayments() else { return nil } - _didPresentApplePay = .init(wrappedValue: false) + didPresentApplePay = false self.paymentRequest = paymentRequest controller = PKPaymentAuthorizationController(paymentRequest: paymentRequest) errorMapper = PODefaultPassKitPaymentErrorMapper(logger: ProcessOut.shared.logger) @@ -50,7 +51,7 @@ public final class POPassKitPaymentAuthorizationController: NSObject { completion?(false) return } - $didPresentApplePay.withLock { $0 = true } + didPresentApplePay = true // Bound lifecycle of self to underlying PKPaymentAuthorizationController objc_setAssociatedObject(controller, &AssociatedObjectKeys.controller, self, .OBJC_ASSOCIATION_RETAIN) controller.present(completion: completion) @@ -59,7 +60,7 @@ public final class POPassKitPaymentAuthorizationController: NSObject { /// Presents the payment sheet modally over your app. public func present() async -> Bool { await withUnsafeContinuation { continuation in - present(completion: continuation.resume) + present { continuation.resume(returning: $0) } } } @@ -84,7 +85,7 @@ public final class POPassKitPaymentAuthorizationController: NSObject { // MARK: - Private Nested Types private enum AssociatedObjectKeys { - static var controller: UInt8 = 0 + nonisolated(unsafe) static var controller: UInt8 = 0 } // MARK: - Private Properties @@ -94,8 +95,6 @@ public final class POPassKitPaymentAuthorizationController: NSObject { private let errorMapper: POPassKitPaymentErrorMapper private let cardsService: POCardsService - - @POUnfairlyLocked private var didPresentApplePay: Bool } @@ -175,7 +174,9 @@ extension POPassKitPaymentAuthorizationController: PKPaymentAuthorizationControl return update ?? PKPaymentRequestPaymentMethodUpdate() } - public func presentationWindow(for _: PKPaymentAuthorizationController) -> UIWindow? { - delegate?.presentationWindow(for: self) + public nonisolated func presentationWindow(for _: PKPaymentAuthorizationController) -> UIWindow? { + MainActor.assumeIsolated { + delegate?.presentationWindow(for: self) + } } } diff --git a/Sources/ProcessOutUI/Sources/Modules/PassKitPaymentAuthorization/POPassKitPaymentAuthorizationControllerDelegate.swift b/Sources/ProcessOutUI/Sources/Modules/PassKitPaymentAuthorization/POPassKitPaymentAuthorizationControllerDelegate.swift index c4d8e0a10..26d659a42 100644 --- a/Sources/ProcessOutUI/Sources/Modules/PassKitPaymentAuthorization/POPassKitPaymentAuthorizationControllerDelegate.swift +++ b/Sources/ProcessOutUI/Sources/Modules/PassKitPaymentAuthorization/POPassKitPaymentAuthorizationControllerDelegate.swift @@ -89,6 +89,7 @@ public protocol POPassKitPaymentAuthorizationControllerDelegate: AnyObject { @available(*, deprecated) extension POPassKitPaymentAuthorizationControllerDelegate { + @MainActor public func paymentAuthorizationController( _ controller: POPassKitPaymentAuthorizationController, didFailToTokenizePayment payment: PKPayment, @@ -97,6 +98,7 @@ extension POPassKitPaymentAuthorizationControllerDelegate { nil } + @MainActor public func paymentAuthorizationControllerWillAuthorizePayment( _ controller: POPassKitPaymentAuthorizationController ) { @@ -104,6 +106,7 @@ extension POPassKitPaymentAuthorizationControllerDelegate { } @available(iOS 14.0, *) + @MainActor public func paymentAuthorizationControllerDidRequestMerchantSessionUpdate( controller: POPassKitPaymentAuthorizationController ) async -> PKPaymentRequestMerchantSessionUpdate? { @@ -111,6 +114,7 @@ extension POPassKitPaymentAuthorizationControllerDelegate { } @available(iOS 15.0, *) + @MainActor public func paymentAuthorizationController( _ controller: POPassKitPaymentAuthorizationController, didChangeCouponCode couponCode: String @@ -118,6 +122,7 @@ extension POPassKitPaymentAuthorizationControllerDelegate { nil } + @MainActor public func paymentAuthorizationController( _ controller: POPassKitPaymentAuthorizationController, didSelectShippingMethod shippingMethod: PKShippingMethod @@ -125,6 +130,7 @@ extension POPassKitPaymentAuthorizationControllerDelegate { nil } + @MainActor public func paymentAuthorizationController( _ controller: POPassKitPaymentAuthorizationController, didSelectShippingContact contact: PKContact @@ -132,6 +138,7 @@ extension POPassKitPaymentAuthorizationControllerDelegate { nil } + @MainActor public func paymentAuthorizationController( _ controller: POPassKitPaymentAuthorizationController, didSelectPaymentMethod paymentMethod: PKPaymentMethod diff --git a/Sources/ProcessOutUI/Sources/Modules/WebAuthentication/DefaultSafariViewModel.swift b/Sources/ProcessOutUI/Sources/Modules/WebAuthentication/DefaultSafariViewModel.swift deleted file mode 100644 index c81fbe579..000000000 --- a/Sources/ProcessOutUI/Sources/Modules/WebAuthentication/DefaultSafariViewModel.swift +++ /dev/null @@ -1,126 +0,0 @@ -// -// DefaultSafariViewModel.swift -// ProcessOutUI -// -// Created by Andrii Vysotskyi on 10.05.2023. -// - -import Foundation -import SafariServices -@_spi(PO) import ProcessOut - -final class DefaultSafariViewModel: NSObject, SFSafariViewControllerDelegate { - - init( - callback: POWebAuthenticationSessionCallback, - timeout: TimeInterval? = nil, - eventEmitter: POEventEmitter, - logger: POLogger, - completion: @escaping (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 - 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) - } - } - - 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 0a4be1baa..000000000 --- a/Sources/ProcessOutUI/Sources/Modules/WebAuthentication/POWebAuthenticationSession.swift +++ /dev/null @@ -1,119 +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. -public final class POWebAuthenticationSession { - - /// A completion handler for the web authentication session. - typealias Completion = (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. - @MainActor - 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. - @MainActor - 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 { - 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 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 fb0368c13..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: @unchecked 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: (_ 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 bccff14ca..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 { - static var viewModel: UInt8 = 0 - } -} diff --git a/Templates/AutoCompletion.stencil b/Templates/AutoCompletion.stencil index 91b307c88..0dc1d0e02 100644 --- a/Templates/AutoCompletion.stencil +++ b/Templates/AutoCompletion.stencil @@ -18,7 +18,7 @@ extension {{ type.name }} { {% for parameter in method.parameters %} {{ parameter.asSource }}, {% endfor %} - completion: @escaping ({% if method.throws %}Result<{{ method.returnTypeName.asSource }}, POFailure>{% else %}{{ method.returnTypeName.asSource }}{% endif %}) -> Void + completion: @escaping @Sendable ({% if method.throws %}Result<{{ method.returnTypeName.asSource }}, POFailure>{% else %}{{ method.returnTypeName.asSource }}{% endif %}) -> Void ) -> POCancellable { invoke(completion: completion) { {% if method.throws %}try {% endif %}await {{ method.callName }}({% for parameter in method.parameters %}{% if parameter.argumentLabel %}{{ parameter.argumentLabel }}: {% endif %}{{ parameter.name }}{% if not forloop.last %}, {% endif %}{% endfor %}) @@ -29,9 +29,9 @@ extension {{ type.name }} { {% if forloop.last %} /// Invokes given completion with a result of async operation. -private func invoke( - completion: @escaping (Result) -> Void, - after operation: @escaping () async throws -> T +private func invoke( + completion: @escaping @Sendable (Result) -> Void, + after operation: @escaping @Sendable () async throws -> T ) -> POCancellable { Task { @MainActor in do { @@ -47,7 +47,10 @@ private func invoke( } /// Invokes given completion with a result of async operation. -private func invoke(completion: @escaping (T) -> Void, after operation: @escaping () async -> T) -> Task { +private func invoke( + completion: @escaping @Sendable (T) -> Void, + after operation: @escaping @Sendable () async -> T +) -> Task { Task { @MainActor in completion(await operation()) } diff --git a/Tests/ProcessOutTests/Sources/Core/Utils.swift b/Tests/ProcessOutTests/Sources/Core/Utils.swift index 689aa270e..60ccef50c 100644 --- a/Tests/ProcessOutTests/Sources/Core/Utils.swift +++ b/Tests/ProcessOutTests/Sources/Core/Utils.swift @@ -12,16 +12,19 @@ import XCTest /// - expression: An expression that can throw an error. /// - message: An optional description of a failure. @discardableResult -func assertThrowsError( +func assertThrowsError( _ expression: @autoclosure () async throws -> T, _ message: @autoclosure () -> String = "", + errorType: E.Type = Error.self, file: StaticString = #filePath, line: UInt = #line -) async -> Error? { +) async -> E? { do { _ = try await expression() - } catch { + } catch let error as E { return error + } catch { + XCTFail("Unexpected error type") } XCTFail(message(), file: file, line: line) return nil diff --git a/Tests/ProcessOutTests/Sources/Integration/CardsServiceTests.swift b/Tests/ProcessOutTests/Sources/Integration/CardsServiceTests.swift index e135b27f2..76d6d4db0 100644 --- a/Tests/ProcessOutTests/Sources/Integration/CardsServiceTests.swift +++ b/Tests/ProcessOutTests/Sources/Integration/CardsServiceTests.swift @@ -11,9 +11,9 @@ import XCTest final class CardsServiceTests: XCTestCase { - override func setUp() { - super.setUp() - ProcessOut.configure(configuration: .init(projectId: Constants.projectId), force: true) + override func setUp() async throws { + try await super.setUp() + await ProcessOut.configure(configuration: .init(projectId: Constants.projectId), force: true) sut = ProcessOut.shared.cards } @@ -27,7 +27,7 @@ final class CardsServiceTests: XCTestCase { XCTAssertEqual(information.bankName, "UNITED CITIZENS BANK OF SOUTHERN KENTUCKY") XCTAssertEqual(information.brand, "visa business") XCTAssertEqual(information.category, "commercial") - XCTAssertEqual(information.$scheme.typed, .visa) + XCTAssertEqual(information.scheme, .visa) XCTAssertEqual(information.type, "debit") } @@ -99,7 +99,7 @@ final class CardsServiceTests: XCTestCase { ) // Then - XCTAssertEqual(updatedCard.$preferredScheme.typed, "test") + XCTAssertEqual(updatedCard.preferredScheme?.rawValue, "test") } func test_tokenize_whenPreferredSchemeIsSet() async throws { diff --git a/Tests/ProcessOutTests/Sources/Integration/CustomerTokensServiceTests.swift b/Tests/ProcessOutTests/Sources/Integration/CustomerTokensServiceTests.swift index 6d575a652..5fffb13c1 100644 --- a/Tests/ProcessOutTests/Sources/Integration/CustomerTokensServiceTests.swift +++ b/Tests/ProcessOutTests/Sources/Integration/CustomerTokensServiceTests.swift @@ -10,12 +10,12 @@ import XCTest final class CustomerTokensServiceTests: XCTestCase { - override func setUp() { - super.setUp() + override func setUp() async throws { + try await super.setUp() let configuration = ProcessOutConfiguration( projectId: Constants.projectId, privateKey: Constants.projectPrivateKey ) - ProcessOut.configure(configuration: configuration, force: true) + await ProcessOut.configure(configuration: configuration, force: true) sut = ProcessOut.shared.customerTokens cardsService = ProcessOut.shared.cards } @@ -60,19 +60,18 @@ final class CustomerTokensServiceTests: XCTestCase { customerId: Constants.customerId, tokenId: try await createToken(verify: true).id, source: card.id, - verify: true, - enableThreeDS2: true + verify: true ) let threeDSService = Mock3DSService() - threeDSService.authenticationRequestFromClosure = { _, completion in - completion(.failure(.init(code: .cancelled))) + threeDSService.authenticationRequestParametersFromClosure = { _ in + throw POFailure(code: .cancelled) } // When _ = try? await sut.assignCustomerToken(request: request, threeDSService: threeDSService) // Then - XCTAssertEqual(threeDSService.authenticationRequestCallsCount, 1) + XCTAssertEqual(threeDSService.authenticationRequestParametersCallsCount, 1) } // MARK: - Private Properties diff --git a/Tests/ProcessOutTests/Sources/Integration/GatewayConfigurationsRepositoryTests.swift b/Tests/ProcessOutTests/Sources/Integration/GatewayConfigurationsRepositoryTests.swift index 5311741e1..8f994c1d1 100644 --- a/Tests/ProcessOutTests/Sources/Integration/GatewayConfigurationsRepositoryTests.swift +++ b/Tests/ProcessOutTests/Sources/Integration/GatewayConfigurationsRepositoryTests.swift @@ -11,9 +11,9 @@ import XCTest final class GatewayConfigurationsRepositoryTests: XCTestCase { - override func setUp() { - super.setUp() - ProcessOut.configure(configuration: .init(projectId: Constants.projectId), force: true) + override func setUp() async throws { + try await super.setUp() + await ProcessOut.configure(configuration: .init(projectId: Constants.projectId), force: true) sut = ProcessOut.shared.gatewayConfigurations } diff --git a/Tests/ProcessOutTests/Sources/Mocks/3DS/Mock3DSService.swift b/Tests/ProcessOutTests/Sources/Mocks/3DS/Mock3DSService.swift index 26e42f207..1df988250 100644 --- a/Tests/ProcessOutTests/Sources/Mocks/3DS/Mock3DSService.swift +++ b/Tests/ProcessOutTests/Sources/Mocks/3DS/Mock3DSService.swift @@ -5,39 +5,58 @@ // Created by Andrii Vysotskyi on 10.04.2023. // -// swiftlint:disable line_length +// swiftlint:disable identifier_name line_length import Foundation -@testable import ProcessOut +@testable @_spi(PO) import ProcessOut -final class Mock3DSService: PO3DSService { +final class Mock3DSService: PO3DSService, Sendable { - var authenticationRequestCallsCount = 0 - var authenticationRequestFromClosure: ((PO3DS2Configuration, @escaping (Result) -> Void) -> Void)! - var handleChallengeCallsCount = 0 - var handleChallengeFromClosure: ((PO3DS2Challenge, @escaping (Result) -> Void) -> Void)! - var handleRedirectCallsCount = 0 - var handleRedirectFromClosure: ((PO3DSRedirect, @escaping (Result) -> Void) -> Void)! + var authenticationRequestParametersCallsCount: Int { + lock.withLock { _authenticationRequestParametersCallsCount } + } - // MARK: - PO3DSService + var authenticationRequestParametersFromClosure: ((PO3DS2Configuration) throws -> PO3DS2AuthenticationRequestParameters)! { + get { lock.withLock { _authenticationRequestParametersFromClosure } } + set { lock.withLock { _authenticationRequestParametersFromClosure = newValue } } + } - func authenticationRequest( - configuration: PO3DS2Configuration, - completion: @escaping (Result) -> Void - ) { - authenticationRequestCallsCount += 1 - authenticationRequestFromClosure!(configuration, completion) + var performChallengeCallsCount: Int { + lock.withLock { _performChallengeCallsCount } } - func handle(challenge: PO3DS2Challenge, completion: @escaping (Result) -> Void) { - handleChallengeCallsCount += 1 - handleChallengeFromClosure!(challenge, completion) + var performChallengeFromClosure: ((PO3DS2ChallengeParameters) throws -> PO3DS2ChallengeResult)! { + get { lock.withLock { _performChallengeFromClosure } } + set { lock.withLock { _performChallengeFromClosure = newValue } } } - func handle(redirect: PO3DSRedirect, completion: @escaping (Result) -> Void) { - handleRedirectCallsCount += 1 - handleRedirectFromClosure!(redirect, completion) + // MARK: - PO3DSService + + func authenticationRequestParameters( + configuration: PO3DS2Configuration + ) async throws -> PO3DS2AuthenticationRequestParameters { + try lock.withLock { + _authenticationRequestParametersCallsCount += 1 + return try _authenticationRequestParametersFromClosure(configuration) + } } + + func performChallenge(with parameters: PO3DS2ChallengeParameters) async throws -> PO3DS2ChallengeResult { + try lock.withLock { + _performChallengeCallsCount += 1 + return try _performChallengeFromClosure(parameters) + } + } + + // MARK: - Private Properties + + private let lock = POUnfairlyLocked() + + private nonisolated(unsafe) var _authenticationRequestParametersCallsCount = 0 + private nonisolated(unsafe) var _authenticationRequestParametersFromClosure: ((PO3DS2Configuration) throws -> PO3DS2AuthenticationRequestParameters)! + + private nonisolated(unsafe) var _performChallengeCallsCount = 0 + private nonisolated(unsafe) var _performChallengeFromClosure: ((PO3DS2ChallengeParameters) throws -> PO3DS2ChallengeResult)! } -// swiftlint:enable line_length +// swiftlint:enable identifier_name line_length diff --git a/Tests/ProcessOutTests/Sources/Mocks/DeviceMetadataProvider/StubDeviceMetadataProvider.swift b/Tests/ProcessOutTests/Sources/Mocks/DeviceMetadataProvider/StubDeviceMetadataProvider.swift index 55e782cff..374fb6b12 100644 --- a/Tests/ProcessOutTests/Sources/Mocks/DeviceMetadataProvider/StubDeviceMetadataProvider.swift +++ b/Tests/ProcessOutTests/Sources/Mocks/DeviceMetadataProvider/StubDeviceMetadataProvider.swift @@ -11,10 +11,10 @@ struct StubDeviceMetadataProvider: DeviceMetadataProvider { var deviceMetadata: DeviceMetadata { DeviceMetadata( - id: .init(value: ""), - installationId: .init(value: nil), - systemVersion: .init(value: "4"), - model: .init(value: "model"), + id: "", + installationId: nil, + systemVersion: "4", + model: "model", appLanguage: "en", appScreenWidth: 1, appScreenHeight: 2, diff --git a/Tests/ProcessOutTests/Sources/Mocks/HttpConnectorRequestMapper/MockHttpConnectorRequestMapper.swift b/Tests/ProcessOutTests/Sources/Mocks/HttpConnectorRequestMapper/MockHttpConnectorRequestMapper.swift index 5dcdb7694..bb21b6505 100644 --- a/Tests/ProcessOutTests/Sources/Mocks/HttpConnectorRequestMapper/MockHttpConnectorRequestMapper.swift +++ b/Tests/ProcessOutTests/Sources/Mocks/HttpConnectorRequestMapper/MockHttpConnectorRequestMapper.swift @@ -6,15 +6,32 @@ // import Foundation -@testable import ProcessOut +@testable @_spi(PO) import ProcessOut -final class MockHttpConnectorRequestMapper: HttpConnectorRequestMapper { +final class MockHttpConnectorRequestMapper: HttpConnectorRequestMapper, Sendable { - var urlRequestFromCallsCount = 0 - var urlRequestFromClosure: (() throws -> URLRequest)! + var urlRequestFromCallsCount: Int { + lock.withLock { _urlRequestFromCallsCount } + } + + var urlRequestFromClosure: (() throws -> URLRequest)! { + get { lock.withLock { _urlRequestFromClosure } } + set { lock.withLock { _urlRequestFromClosure = newValue } } + } + + // MARK: - HttpConnectorRequestMapper func urlRequest(from request: HttpConnectorRequest) throws -> URLRequest { - urlRequestFromCallsCount += 1 - return try urlRequestFromClosure() + try lock.withLock { + _urlRequestFromCallsCount += 1 + return try _urlRequestFromClosure() + } } + + // MARK: - Private Properties + + private let lock = POUnfairlyLocked() + + private nonisolated(unsafe) var _urlRequestFromCallsCount = 0 + private nonisolated(unsafe) var _urlRequestFromClosure: (() throws -> URLRequest)! } diff --git a/Tests/ProcessOutTests/Sources/Mocks/Logger/POLogger+Extensions.swift b/Tests/ProcessOutTests/Sources/Mocks/Logger/POLogger+Extensions.swift index c318c2477..4b0b53ce9 100644 --- a/Tests/ProcessOutTests/Sources/Mocks/Logger/POLogger+Extensions.swift +++ b/Tests/ProcessOutTests/Sources/Mocks/Logger/POLogger+Extensions.swift @@ -10,5 +10,5 @@ extension POLogger { /// Stub logger. - static var stub = POLogger(category: "") + static let stub = POLogger(category: "") } diff --git a/Tests/ProcessOutTests/Sources/Mocks/PhoneNumberMetadataProvider/MockPhoneNumberMetadataProvider.swift b/Tests/ProcessOutTests/Sources/Mocks/PhoneNumberMetadataProvider/MockPhoneNumberMetadataProvider.swift deleted file mode 100644 index f4319bdb2..000000000 --- a/Tests/ProcessOutTests/Sources/Mocks/PhoneNumberMetadataProvider/MockPhoneNumberMetadataProvider.swift +++ /dev/null @@ -1,19 +0,0 @@ -// -// MockPhoneNumberMetadataProvider.swift -// ProcessOut -// -// Created by Andrii Vysotskyi on 12.05.2023. -// - -@testable @_spi(PO) import ProcessOut - -final class MockPhoneNumberMetadataProvider: POPhoneNumberMetadataProvider { - - var metadataCallsCount = 0 - var metadata: POPhoneNumberMetadata? - - func metadata(for countryCode: String) -> POPhoneNumberMetadata? { - metadataCallsCount += 1 - return metadata - } -} diff --git a/Tests/ProcessOutTests/Sources/Mocks/UrlProtocol/MockUrlProtocol.swift b/Tests/ProcessOutTests/Sources/Mocks/UrlProtocol/MockUrlProtocol.swift index 10a200f2c..368790069 100644 --- a/Tests/ProcessOutTests/Sources/Mocks/UrlProtocol/MockUrlProtocol.swift +++ b/Tests/ProcessOutTests/Sources/Mocks/UrlProtocol/MockUrlProtocol.swift @@ -6,8 +6,9 @@ // import Foundation +@_spi(PO) import ProcessOut -final class MockUrlProtocol: URLProtocol { +final class MockUrlProtocol: URLProtocol, @unchecked Sendable { /// Method doesn't validate whether handler for given method/path is already registered. static func register( @@ -16,15 +17,11 @@ final class MockUrlProtocol: URLProtocol { handler: @escaping (URLRequest) async throws -> (URLResponse, Data) ) { let route = MockUrlProtocolRoute(method: method, path: path, handler: handler) - lock.withLock { - routes.append(route) - } + routes.withLock { $0.append(route) } } static func removeRegistrations() { - lock.withLock { - routes = [] - } + routes.withLock { $0 = [] } } /// Implementation raises an assertion failure is given request can't be handled. @@ -32,10 +29,7 @@ final class MockUrlProtocol: URLProtocol { guard let urlAbsoluteString = request.url?.absoluteString else { fatalError("Invalid request") } - var availableRoutes: [MockUrlProtocolRoute] = [] - lock.withLock { - availableRoutes = routes - } + let availableRoutes = routes.wrappedValue for route in availableRoutes { if let method = route.method, method != request.httpMethod { continue @@ -55,8 +49,7 @@ final class MockUrlProtocol: URLProtocol { // MARK: - Private Properties - private static var routes: [MockUrlProtocolRoute] = [] - private static var lock = NSLock() + private static let routes = POUnfairlyLocked<[MockUrlProtocolRoute]>(wrappedValue: []) // MARK: - URLProtocol @@ -69,18 +62,19 @@ final class MockUrlProtocol: URLProtocol { } override func startLoading() { - currentTask = Task { [weak self] in + let task = Task { [weak self] in await self?.startLoadingAsync() } + currentTask.withLock { $0 = task } } override func stopLoading() { - currentTask?.cancel() + currentTask.withLock { $0?.cancel() } } // MARK: - Private Properties - private var currentTask: Task? + private let currentTask = POUnfairlyLocked?>(wrappedValue: nil) // MARK: - Private Methods diff --git a/Tests/ProcessOutTests/Sources/Mocks/WebAuthenticationSession/MockWebAuthenticationSession.swift b/Tests/ProcessOutTests/Sources/Mocks/WebAuthenticationSession/MockWebAuthenticationSession.swift new file mode 100644 index 000000000..2c20acb1a --- /dev/null +++ b/Tests/ProcessOutTests/Sources/Mocks/WebAuthenticationSession/MockWebAuthenticationSession.swift @@ -0,0 +1,40 @@ +// +// MockWebAuthenticationSession.swift +// ProcessOutTests +// +// Created by Andrii Vysotskyi on 07.08.2024. +// + +import Foundation +@testable @_spi(PO) import ProcessOut + +final class MockWebAuthenticationSession: WebAuthenticationSession { + + var authenticateCallsCount: Int { + lock.withLock { _authenticateCallsCount } + } + + var authenticateFromClosure: ((URL, String?, [String: String]?) async throws -> URL)! { + get { lock.withLock { _authenticateFromClosure } } + set { lock.withLock { _authenticateFromClosure = newValue } } + } + + // MARK: - + + func authenticate( + using url: URL, callbackScheme: String?, additionalHeaderFields: [String: String]? + ) async throws -> URL { + let authenticate = lock.withLock { + _authenticateCallsCount += 1 + return _authenticateFromClosure + } + return try await authenticate!(url, callbackScheme, additionalHeaderFields) + } + + // MARK: - Private Properties + + private let lock = POUnfairlyLocked() + + private nonisolated(unsafe) var _authenticateCallsCount = 0 + private nonisolated(unsafe) var _authenticateFromClosure: ((URL, String?, [String: String]?) async throws -> URL)! +} diff --git a/Tests/ProcessOutTests/Sources/Unit/Connectors/Http/UrlSessionHttpConnectorTests.swift b/Tests/ProcessOutTests/Sources/Unit/Connectors/Http/UrlSessionHttpConnectorTests.swift index a8835e573..ec0ac5dbb 100644 --- a/Tests/ProcessOutTests/Sources/Unit/Connectors/Http/UrlSessionHttpConnectorTests.swift +++ b/Tests/ProcessOutTests/Sources/Unit/Connectors/Http/UrlSessionHttpConnectorTests.swift @@ -40,11 +40,11 @@ final class UrlSessionHttpConnectorTests: XCTestCase { // When let error = await assertThrowsError( - try await sut.execute(request: defaultRequest) + try await sut.execute(request: defaultRequest), errorType: HttpConnectorFailure.self ) // Then - if let failure = error as? HttpConnectorFailure, case .encoding(let encodingError) = failure { + if case .encoding(let encodingError) = error { XCTAssertEqual(encodingError as NSError, codingError) return } @@ -215,8 +215,8 @@ final class UrlSessionHttpConnectorTests: XCTestCase { requestMapper.urlRequestFromClosure = defaultUrlRequest // When - let task = Task { - _ = try await sut.execute(request: defaultRequest) + let task = Task { [sut, defaultRequest] in + _ = try await sut!.execute(request: defaultRequest) } DispatchQueue.main.async { task.cancel() @@ -243,7 +243,7 @@ final class UrlSessionHttpConnectorTests: XCTestCase { // MARK: - Private Methods - private var defaultRequest: HttpConnectorRequest { + private var defaultRequest: HttpConnectorRequest { HttpConnectorRequest.get(path: "") } diff --git a/Tests/ProcessOutTests/Sources/Unit/Core/CodingUtils/FallbackDecodableTests.swift b/Tests/ProcessOutTests/Sources/Unit/Core/CodingUtils/FallbackDecodableTests.swift deleted file mode 100644 index 65682cc01..000000000 --- a/Tests/ProcessOutTests/Sources/Unit/Core/CodingUtils/FallbackDecodableTests.swift +++ /dev/null @@ -1,54 +0,0 @@ -// -// FallbackDecodableTests.swift -// ProcessOutTests -// -// Created by Andrii Vysotskyi on 24.11.2023. -// - -import XCTest -@testable import ProcessOut - -final class FallbackDecodableTests: XCTestCase { - - func test_fallbackDecodable_whenValueIsNotPresent_decodesEmptyString() throws { - // Given - let decoder = JSONDecoder() - let data = Data("{}".utf8) - - // When - let container = try decoder.decode(Container.self, from: data) - - // Then - XCTAssertTrue(container.value.isEmpty) - } - - func test_fallbackDecodable_whenValueIsNull_decodesEmptyString() throws { - // Given - let decoder = JSONDecoder() - let data = Data(#"{ "value": null }"#.utf8) - - // When - let container = try decoder.decode(Container.self, from: data) - - // Then - XCTAssertTrue(container.value.isEmpty) - } - - func test_fallbackDecodable_whenValueIsAvailable_decodesIt() throws { - // Given - let decoder = JSONDecoder() - let data = Data(#"{ "value": "1" }"#.utf8) - - // When - let container = try decoder.decode(Container.self, from: data) - - // Then - XCTAssertEqual(container.value, "1") - } -} - -private struct Container: Decodable { - - @POFallbackDecodable - var value: String -} diff --git a/Tests/ProcessOutTests/Sources/Unit/Core/CodingUtils/ImmutableExcludedCodableTests.swift b/Tests/ProcessOutTests/Sources/Unit/Core/CodingUtils/ImmutableExcludedCodableTests.swift deleted file mode 100644 index 4a39bf7c2..000000000 --- a/Tests/ProcessOutTests/Sources/Unit/Core/CodingUtils/ImmutableExcludedCodableTests.swift +++ /dev/null @@ -1,30 +0,0 @@ -// -// ImmutableExcludedCodableTests.swift -// ProcessOutTests -// -// Created by Andrii Vysotskyi on 29.03.2023. -// - -import XCTest -@testable import ProcessOut - -final class ImmutableExcludedCodableTests: XCTestCase { - - func test_excludedCodable_whenWrappedInContainer_isNotEncoded() throws { - // Given - let encoder = JSONEncoder() - let value = Container(value: POImmutableExcludedCodable(value: 1)) - - // When - let encodeData = try encoder.encode(value) - - // Then - XCTAssertEqual(Data("{}".utf8), encodeData) - } -} - -private struct Container: Encodable { - - @POImmutableExcludedCodable - var value: Int -} diff --git a/Tests/ProcessOutTests/Sources/Unit/Core/CodingUtils/ImmutableStringCodableOptionalDecimalTests.swift b/Tests/ProcessOutTests/Sources/Unit/Core/CodingUtils/StringCodableOptionalDecimalTests.swift similarity index 74% rename from Tests/ProcessOutTests/Sources/Unit/Core/CodingUtils/ImmutableStringCodableOptionalDecimalTests.swift rename to Tests/ProcessOutTests/Sources/Unit/Core/CodingUtils/StringCodableOptionalDecimalTests.swift index 7d1048fd9..2d3b4515f 100644 --- a/Tests/ProcessOutTests/Sources/Unit/Core/CodingUtils/ImmutableStringCodableOptionalDecimalTests.swift +++ b/Tests/ProcessOutTests/Sources/Unit/Core/CodingUtils/StringCodableOptionalDecimalTests.swift @@ -1,5 +1,5 @@ // -// ImmutableStringCodableOptionalDecimalTests.swift +// StringCodableOptionalDecimalTests.swift // ProcessOutTests // // Created by Andrii Vysotskyi on 18.10.2022. @@ -22,7 +22,7 @@ final class ImmutableStringCodableOptionalDecimalTests: XCTestCase { let data = Data(#""1""#.utf8) // When - let decimal = try decoder.decode(POImmutableStringCodableOptionalDecimal.self, from: data) + let decimal = try decoder.decode(POStringCodableOptionalDecimal.self, from: data) // Then XCTAssertEqual(decimal.wrappedValue?.description, "1") @@ -33,7 +33,7 @@ final class ImmutableStringCodableOptionalDecimalTests: XCTestCase { let data = Data(#""1234.25""#.utf8) // When - let decimal = try decoder.decode(POImmutableStringCodableOptionalDecimal.self, from: data) + let decimal = try decoder.decode(POStringCodableOptionalDecimal.self, from: data) // Then XCTAssertEqual(decimal.wrappedValue?.description, "1234.25") @@ -55,7 +55,7 @@ final class ImmutableStringCodableOptionalDecimalTests: XCTestCase { let data = Data("1".utf8) // Then - XCTAssertThrowsError(try decoder.decode(POImmutableStringCodableOptionalDecimal.self, from: data)) + XCTAssertThrowsError(try decoder.decode(POStringCodableOptionalDecimal.self, from: data)) } func test_init_whenInputHasComma_fails() throws { @@ -63,12 +63,12 @@ final class ImmutableStringCodableOptionalDecimalTests: XCTestCase { let data = Data(#""1,2""#.utf8) // Then - XCTAssertThrowsError(try decoder.decode(POImmutableStringCodableOptionalDecimal.self, from: data)) + XCTAssertThrowsError(try decoder.decode(POStringCodableOptionalDecimal.self, from: data)) } func test_encode_returnsStringData() throws { // Given - let decimal = POImmutableStringCodableOptionalDecimal(value: Decimal(1234)) + let decimal = POStringCodableOptionalDecimal(value: Decimal(1234)) // When let data = try encoder.encode(decimal) @@ -80,7 +80,7 @@ final class ImmutableStringCodableOptionalDecimalTests: XCTestCase { func test_encode_whenInContainer_encodesString() throws { // Given - let value = Container(number: POImmutableStringCodableOptionalDecimal(value: Decimal(1234))) + let value = Container(number: POStringCodableOptionalDecimal(value: Decimal(1234))) // When let data = try encoder.encode(value) @@ -97,6 +97,6 @@ final class ImmutableStringCodableOptionalDecimalTests: XCTestCase { private struct Container: Codable { - @POImmutableStringCodableOptionalDecimal + @POStringCodableOptionalDecimal var number: Decimal? } diff --git a/Tests/ProcessOutTests/Sources/Unit/Core/Utils/AsyncUtilsTests.swift b/Tests/ProcessOutTests/Sources/Unit/Core/Utils/AsyncUtilsTests.swift index b3321e36f..ba9421359 100644 --- a/Tests/ProcessOutTests/Sources/Unit/Core/Utils/AsyncUtilsTests.swift +++ b/Tests/ProcessOutTests/Sources/Unit/Core/Utils/AsyncUtilsTests.swift @@ -36,14 +36,11 @@ final class AsyncUtilsTests: XCTestCase { // When let error = await assertThrowsError( - try await withTimeout(timeout, error: Failure.timeout, perform: operation) + try await withTimeout(timeout, error: Failure.timeout, perform: operation), errorType: Failure.self ) // Then - if let failure = error as? Failure, failure == .timeout { - return - } - XCTFail("Expected timeout failure.") + XCTAssertEqual(error, .timeout, "Expected timeout failure.") } func test_withTimeout_whenNonCancellableOperationTimesOut_ignoresTimeout() async throws { @@ -105,12 +102,12 @@ final class AsyncUtilsTests: XCTestCase { func test_retry_whenTimeoutIsZero_executesOperationOnce() async throws { // Given - @POUnfairlyLocked var isOperationExecuted = false + let isOperationExecuted = POUnfairlyLocked(wrappedValue: false) // When try await retry( operation: { - $isOperationExecuted.withLock { $0 = true } + isOperationExecuted.withLock { $0 = true } }, while: { _ in false @@ -120,7 +117,7 @@ final class AsyncUtilsTests: XCTestCase { ) // Then - XCTAssertTrue(isOperationExecuted) + XCTAssertTrue(isOperationExecuted.wrappedValue) } func test_retry_whenTimesOut_throwsTimeoutError() async { @@ -133,19 +130,17 @@ final class AsyncUtilsTests: XCTestCase { while: { _ in false }, timeout: 1, timeoutError: Failure.timeout - ) + ), + errorType: Failure.self ) // Then - if let failure = error as? Failure, failure == .timeout { - return - } - XCTFail("Expected timeout failure.") + XCTAssertEqual(error, .timeout, "Expected timeout failure.") } func test_retry_checksRetryCondition_whenRetryStrategyIsSet() async throws { // Given - @POUnfairlyLocked var isConditionChecked = false + let isConditionChecked = POUnfairlyLocked(wrappedValue: false) // When _ = try await retry( @@ -153,7 +148,7 @@ final class AsyncUtilsTests: XCTestCase { "" }, while: { _ in - $isConditionChecked.withLock { $0 = true } + isConditionChecked.withLock { $0 = true } return false }, timeout: 10, @@ -162,17 +157,17 @@ final class AsyncUtilsTests: XCTestCase { ) // Then - XCTAssertTrue(isConditionChecked) + XCTAssertTrue(isConditionChecked.wrappedValue) } func test_retry_retriesOperation_whenRetryStrategyIsSet() async throws { // Given - @POUnfairlyLocked var operationStartsCount = 0 + let operationStartsCount = POUnfairlyLocked(wrappedValue: 0) // When _ = try await retry( operation: { - $operationStartsCount.withLock { $0 += 1 } + operationStartsCount.withLock { $0 += 1 } }, while: { _ in true @@ -183,7 +178,7 @@ final class AsyncUtilsTests: XCTestCase { ) // Then - XCTAssertEqual(operationStartsCount, 2) + XCTAssertEqual(operationStartsCount.wrappedValue, 2) } // swiftlint:disable:next line_length @@ -217,13 +212,13 @@ final class AsyncUtilsTests: XCTestCase { func test_retry_whenRetryCountIsExceeded_completesWithRecentResult() async throws { // Given - @POUnfairlyLocked var recentOperationValue = "" + let recentOperationValue = POUnfairlyLocked(wrappedValue: "") // When let value = try await retry( operation: { - $recentOperationValue.withLock { $0 = UUID().uuidString } - return recentOperationValue + recentOperationValue.withLock { $0 = UUID().uuidString } + return recentOperationValue.wrappedValue }, while: { _ in true @@ -234,18 +229,18 @@ final class AsyncUtilsTests: XCTestCase { ) // Then - XCTAssertEqual(recentOperationValue, value) + XCTAssertEqual(recentOperationValue.wrappedValue, value) } func test_retry_whenRetryConditionResolvesFalse_completesWithRecentResult() async throws { // Given - @POUnfairlyLocked var recentOperationValue = "" + let recentOperationValue = POUnfairlyLocked(wrappedValue: "") // When let value = try await retry( operation: { - $recentOperationValue.withLock { $0 = UUID().uuidString } - return recentOperationValue + recentOperationValue.withLock { $0 = UUID().uuidString } + return recentOperationValue.wrappedValue }, while: { _ in false @@ -256,7 +251,7 @@ final class AsyncUtilsTests: XCTestCase { ) // Then - XCTAssertEqual(recentOperationValue, value) + XCTAssertEqual(recentOperationValue.wrappedValue, value) } func test_retry_whenCancelledImmediately_completesWithCancellationError() async throws { @@ -286,7 +281,7 @@ final class AsyncUtilsTests: XCTestCase { // MARK: - Private Nested Types - private enum Failure: Error { + private enum Failure: Error, Equatable { case timeout, generic, cancel } } diff --git a/Tests/ProcessOutTests/Sources/Unit/Core/Utils/UIImage+DynamicTests.swift b/Tests/ProcessOutTests/Sources/Unit/Core/Utils/UIImage+DynamicTests.swift deleted file mode 100644 index ace38d5c6..000000000 --- a/Tests/ProcessOutTests/Sources/Unit/Core/Utils/UIImage+DynamicTests.swift +++ /dev/null @@ -1,8 +0,0 @@ -// -// UIImage+Dynamic+Tests.swift -// ProcessOutTests -// -// Created by Andrii Vysotskyi on 04.04.2024. -// - -import Foundation diff --git a/Tests/ProcessOutTests/Sources/Unit/Service/3DS/DefaultThreeDSServiceTests.swift b/Tests/ProcessOutTests/Sources/Unit/Service/3DS/DefaultThreeDSServiceTests.swift index 766720fc4..7c3e18297 100644 --- a/Tests/ProcessOutTests/Sources/Unit/Service/3DS/DefaultThreeDSServiceTests.swift +++ b/Tests/ProcessOutTests/Sources/Unit/Service/3DS/DefaultThreeDSServiceTests.swift @@ -15,8 +15,9 @@ final class DefaultThreeDSServiceTests: XCTestCase { super.setUp() let encoder = JSONEncoder() encoder.outputFormatting = .sortedKeys + webSession = MockWebAuthenticationSession() sut = DefaultThreeDSService( - decoder: JSONDecoder(), encoder: encoder, jsonWritingOptions: [.sortedKeys] + decoder: JSONDecoder(), encoder: encoder, jsonWritingOptions: [.sortedKeys], webSession: webSession ) delegate = Mock3DSService() } @@ -31,17 +32,12 @@ final class DefaultThreeDSServiceTests: XCTestCase { let customerAction = ThreeDSCustomerAction(type: .fingerprintMobile, value: value) // When - let handlingError = await assertThrowsError( - try await sut.handle(action: customerAction, delegate: delegate) + let failure = await assertThrowsError( + try await sut.handle(action: customerAction, delegate: delegate), errorType: POFailure.self ) // Then - switch handlingError { - case let failure as POFailure: - XCTAssertEqual(failure.code, .internal(.mobile)) - default: - XCTFail("Unexpected result") - } + XCTAssertEqual(failure?.code, .internal(.mobile)) } } @@ -56,17 +52,17 @@ final class DefaultThreeDSServiceTests: XCTestCase { directoryServerPublicKey: "2", directoryServerRootCertificates: ["3"], directoryServerTransactionId: "4", - scheme: .unknown("5"), + scheme: .init(rawValue: "5"), messageVersion: "6" ) var delegateCallsCount = 0 for customerAction in customerActions { - delegate.authenticationRequestFromClosure = { configuration, completion in + delegate.authenticationRequestParametersFromClosure = { configuration in // Then XCTAssertEqual(configuration, expectedConfiguration) delegateCallsCount += 1 - completion(.failure(.init(code: .generic(.mobile)))) + throw POFailure(code: .generic(.mobile)) } // When @@ -77,65 +73,53 @@ final class DefaultThreeDSServiceTests: XCTestCase { func test_handle_whenDelegateAuthenticationRequestFails_propagatesFailure() async { // Given - let error = POFailure(code: .unknown(rawValue: "test-error")) - delegate.authenticationRequestFromClosure = { _, completion in - completion(.failure(error)) + let expectedError = POFailure(code: .unknown(rawValue: "test-error")) + delegate.authenticationRequestParametersFromClosure = { _ in + throw expectedError } let customerAction = defaultFingerprintMobileCustomerAction() // When - let handlingError = await assertThrowsError( - try await sut.handle(action: customerAction, delegate: delegate) + let failure = await assertThrowsError( + try await sut.handle(action: customerAction, delegate: delegate), errorType: POFailure.self ) // Then - switch handlingError { - case let failure as POFailure: - XCTAssertEqual(failure.code, error.code) - default: - XCTFail("Unexpected result") - } + XCTAssertEqual(failure?.code, expectedError.code) } func test_handle_whenAuthenticationRequestPublicKeyIsEmpty_fails() async { // Given var isDelegateCalled = false - delegate.authenticationRequestFromClosure = { _, completion in - let invalidAuthenticationRequest = PO3DS2AuthenticationRequest( + delegate.authenticationRequestParametersFromClosure = { _ in + isDelegateCalled = true + return .init( deviceData: "", sdkAppId: "", sdkEphemeralPublicKey: "", sdkReferenceNumber: "", sdkTransactionId: "" ) - isDelegateCalled = true - completion(.success(invalidAuthenticationRequest)) } let customerAction = defaultFingerprintMobileCustomerAction() // When - let error = await assertThrowsError( - try await sut.handle(action: customerAction, delegate: delegate) + let failure = await assertThrowsError( + try await sut.handle(action: customerAction, delegate: delegate), errorType: POFailure.self ) // Then - switch error { - case let failure as POFailure: - XCTAssertEqual(failure.code, .internal(.mobile)) - default: - XCTFail("Unexpected result") - } + XCTAssertEqual(failure?.code, .internal(.mobile)) XCTAssertTrue(isDelegateCalled) } func test_handle_whenAuthenticationRequestIsValid_succeeds() async throws { // Given let customerAction = defaultFingerprintMobileCustomerAction() - delegate.authenticationRequestFromClosure = { _, completion in - let authenticationRequest = PO3DS2AuthenticationRequest( + delegate.authenticationRequestParametersFromClosure = { _ in + PO3DS2AuthenticationRequestParameters( deviceData: "1", sdkAppId: "2", sdkEphemeralPublicKey: #"{"kty": "EC"}"#, sdkReferenceNumber: "3", sdkTransactionId: "4" ) - completion(.success(authenticationRequest)) } // When @@ -157,33 +141,28 @@ final class DefaultThreeDSServiceTests: XCTestCase { let customerAction = ThreeDSCustomerAction(type: .challengeMobile, value: "") // When - let error = await assertThrowsError( - try await sut.handle(action: customerAction, delegate: delegate) + let failure = await assertThrowsError( + try await sut.handle(action: customerAction, delegate: delegate), errorType: POFailure.self ) // Then - switch error { - case let failure as POFailure: - XCTAssertEqual(failure.code, .internal(.mobile)) - default: - XCTFail("Unexpected result") - } + XCTAssertEqual(failure?.code, .internal(.mobile)) } func test_handle_whenChallengeMobileValueIsValid_callsDelegateDoChallenge() async throws { // Given - let expectedChallenge = PO3DS2Challenge( + let expectedChallenge = PO3DS2ChallengeParameters( acsTransactionId: "1", acsReferenceNumber: "2", acsSignedContent: "3", threeDSServerTransactionId: "4" ) var isDelegateCalled = false - delegate.handleChallengeFromClosure = { challenge, completion in + delegate.performChallengeFromClosure = { challenge in // Then XCTAssertEqual(challenge, expectedChallenge) isDelegateCalled = true - completion(.success(true)) + return .init(transactionStatus: true) } // When @@ -193,60 +172,53 @@ final class DefaultThreeDSServiceTests: XCTestCase { func test_handle_whenDelegateDoChallengeFails_propagatesFailure() async { // Given - let error = POFailure(code: .unknown(rawValue: "test-error")) - delegate.handleChallengeFromClosure = { _, completion in - completion(.failure(error)) + let expectedError = POFailure(code: .unknown(rawValue: "test-error")) + delegate.performChallengeFromClosure = { _ in + throw expectedError } // When - let handlingError = await assertThrowsError( - try await sut.handle(action: defaultChallengeMobileCustomerAction, delegate: delegate) + let failure = await assertThrowsError( + try await sut.handle(action: defaultChallengeMobileCustomerAction, delegate: delegate), + errorType: POFailure.self ) // Then - switch handlingError { - case let failure as POFailure: - XCTAssertEqual(failure.code, error.code) - default: - XCTFail("Unexpected result") - } + XCTAssertEqual(failure?.code, expectedError.code) } func test_handle_whenDelegateDoChallengeCompletesWithTrue_succeeds() async throws { // Given - delegate.handleChallengeFromClosure = { _, completion in - completion(.success(true)) + delegate.performChallengeFromClosure = { _ in + .init(transactionStatus: true) } // When let token = try await sut.handle(action: defaultChallengeMobileCustomerAction, delegate: delegate) // Then - XCTAssertEqual(token, "gway_req_eyJib2R5IjoieyBcInRyYW5zU3RhdHVzXCI6IFwiWVwiIH0ifQ==") + XCTAssertEqual(token, "gway_req_eyJib2R5Ijoie1widHJhbnNTdGF0dXNcIjpcIllcIn0ifQ==") } func test_handle_whenDelegateDoChallengeCompletesWithFalse_succeeds() async throws { // Given - delegate.handleChallengeFromClosure = { _, completion in - completion(.success(false)) + delegate.performChallengeFromClosure = { _ in + .init(transactionStatus: false) } // When let token = try await sut.handle(action: defaultChallengeMobileCustomerAction, delegate: delegate) // Then - XCTAssertEqual(token, "gway_req_eyJib2R5IjoieyBcInRyYW5zU3RhdHVzXCI6IFwiTlwiIH0ifQ==") + XCTAssertEqual(token, "gway_req_eyJib2R5Ijoie1widHJhbnNTdGF0dXNcIjpcIk5cIn0ifQ==") } // MARK: - Redirect - func test_handle_whenActionTypeIsUrlOrFingerprint_callsDelegateRedirect() async throws { + func test_handle_whenActionTypeIsUrlOrFingerprint_callsWebSession() async throws { // Given - var delegateCallsCount = 0 - delegate.handleRedirectFromClosure = { _, completion in - // Then - delegateCallsCount += 1 - completion(.success("")) + webSession.authenticateFromClosure = { _, _, _ in + URL(string: "example.com")! } let actionTypes: [ThreeDSCustomerAction.ActionType] = [.url, .fingerprint] @@ -256,7 +228,9 @@ final class DefaultThreeDSServiceTests: XCTestCase { // When _ = try await sut.handle(action: customerAction, delegate: delegate) } - XCTAssertEqual(delegateCallsCount, actionTypes.count) + + // Then + XCTAssertEqual(webSession.authenticateCallsCount, actionTypes.count) } func test_handle_whenRedirectOrFingerprintValueIsNotValidUrl_fails() async { @@ -264,46 +238,35 @@ final class DefaultThreeDSServiceTests: XCTestCase { let actionTypes: [ThreeDSCustomerAction.ActionType] = [.redirect, .url, .fingerprint] for actionType in actionTypes { - let action = ThreeDSCustomerAction(type: actionType, value: "http://:-1") + let action = ThreeDSCustomerAction(type: actionType, value: "") // When - let error = await assertThrowsError( - try await sut.handle(action: action, delegate: delegate) + let failure = await assertThrowsError( + try await sut.handle(action: action, delegate: delegate), errorType: POFailure.self ) // Then - switch error { - case let failure as POFailure: - XCTAssertEqual(failure.code, .internal(.mobile)) - default: - XCTFail("Unexpected result") - } + XCTAssertEqual(failure?.code, .internal(.mobile)) } } - func test_handle_whenRedirectValueIsValidUrl_callsDelegateRedirect() async throws { + func test_handle_whenRedirectValueIsValidUrl_callsWebSession() async throws { // Given - let expectedRedirect = PO3DSRedirect( - url: URL(string: "example.com")!, timeout: nil - ) - var isDelegateCalled = false - delegate.handleRedirectFromClosure = { redirect, completion in - // Then - XCTAssertEqual(redirect, expectedRedirect) - isDelegateCalled = true - completion(.success("")) + webSession.authenticateFromClosure = { url, _, _ in + XCTAssertEqual(URL(string: "example.com"), url) + return URL(string: "test://return")! } let customerAction = ThreeDSCustomerAction(type: .redirect, value: "example.com") // When _ = try await sut.handle(action: customerAction, delegate: delegate) - XCTAssertTrue(isDelegateCalled) + XCTAssertEqual(webSession.authenticateCallsCount, 1) } func test_handle_whenRedirectCompletesWithNewToken_propagatesToken() async throws { // Given - delegate.handleRedirectFromClosure = { _, completion in - completion(.success("test")) + webSession.authenticateFromClosure = { _, _, _ in + URL(string: "test://return?token=test")! } let customerAction = ThreeDSCustomerAction(type: .redirect, value: "example.com") @@ -316,51 +279,41 @@ final class DefaultThreeDSServiceTests: XCTestCase { func test_handle_whenRedirectFails_propagatesError() async { // Given - delegate.handleRedirectFromClosure = { _, completion in - let failure = POFailure(code: .unknown(rawValue: "test-error")) - completion(.failure(failure)) + webSession.authenticateFromClosure = { _, _, _ in + throw POFailure(code: .unknown(rawValue: "test-error")) } + let customerAction = ThreeDSCustomerAction(type: .redirect, value: "example.com") // When - let error = await assertThrowsError( - try await sut.handle(action: customerAction, delegate: delegate) + let failure = await assertThrowsError( + try await sut.handle(action: customerAction, delegate: delegate), errorType: POFailure.self ) // Then - switch error { - case let failure as POFailure: - XCTAssertEqual(failure.code, .unknown(rawValue: "test-error")) - default: - XCTFail("Unexpected result") - } + XCTAssertEqual(failure?.code, .unknown(rawValue: "test-error")) } // MARK: - Fingerprint - func test_handle_whenFingerprintValueIsValidUrl_callsDelegateRedirect() async throws { + func test_handle_whenFingerprintValueIsValidUrl_callsWebSession() async throws { // Given - let expectedRedirect = PO3DSRedirect( - url: URL(string: "example.com")!, timeout: 10 - ) - var isDelegateCalled = false - delegate.handleRedirectFromClosure = { redirect, completion in - // Then - XCTAssertEqual(redirect, expectedRedirect) - isDelegateCalled = true - completion(.success("")) + let expectedRedirectUrl = URL(string: "example.com")! + webSession.authenticateFromClosure = { url, _, _ in + XCTAssertEqual(url, expectedRedirectUrl) + return URL(string: "test://return")! } - let customerAction = ThreeDSCustomerAction(type: .fingerprint, value: "example.com") + let customerAction = ThreeDSCustomerAction(type: .fingerprint, value: expectedRedirectUrl.absoluteString) // When _ = try await sut.handle(action: customerAction, delegate: delegate) - XCTAssertTrue(isDelegateCalled) + XCTAssertEqual(webSession.authenticateCallsCount, 1) } func test_handle_whenFingerprintCompletesWithNewToken_propagatesToken() async throws { // Given - delegate.handleRedirectFromClosure = { _, completion in - completion(.success("test")) + webSession.authenticateFromClosure = { _, _, _ in + URL(string: "test://return?token=test")! } let customerAction = ThreeDSCustomerAction(type: .fingerprint, value: "example.com") @@ -373,31 +326,24 @@ final class DefaultThreeDSServiceTests: XCTestCase { func test_handle_whenFingerprintFails_propagatesError() async { // Given - delegate.handleRedirectFromClosure = { _, completion in - let failure = POFailure(code: .unknown(rawValue: "test-error")) - completion(.failure(failure)) + webSession.authenticateFromClosure = { _, _, _ in + throw POFailure(code: .unknown(rawValue: "test-error")) } let customerAction = ThreeDSCustomerAction(type: .fingerprint, value: "example.com") // When - let error = await assertThrowsError( - try await sut.handle(action: customerAction, delegate: delegate) + let failure = await assertThrowsError( + try await sut.handle(action: customerAction, delegate: delegate), errorType: POFailure.self ) // Then - switch error { - case let failure as POFailure: - XCTAssertEqual(failure.code, .unknown(rawValue: "test-error")) - default: - XCTFail("Unexpected result") - } + XCTAssertEqual(failure?.code, .unknown(rawValue: "test-error")) } func test_handle_whenFingerprintFailsWithTimeoutError_succeeds() async throws { // Given - delegate.handleRedirectFromClosure = { _, completion in - let failure = POFailure(code: .timeout(.mobile)) - completion(.failure(failure)) + webSession.authenticateFromClosure = { _, _, _ in + throw POFailure(code: .timeout(.mobile)) } let customerAction = ThreeDSCustomerAction(type: .fingerprint, value: "example.com") @@ -406,17 +352,36 @@ final class DefaultThreeDSServiceTests: XCTestCase { // Then let expectedValue = """ - gway_req_eyJib2R5IjoieyBcInRocmVlRFMyRmluZ2VycHJpbnRUaW1\ - lb3V0XCI6IHRydWUgfSIsInVybCI6ImV4YW1wbGUuY29tIn0= + gway_req_eyJib2R5IjoieyBcInRocmVlRFMyRmluZ2VycHJpbnRUaW1lb3V0XCI6IHRydWUgfSIsInVybCI6ImV4YW1wbGUuY29tIn0= + """ + XCTAssertEqual(value, expectedValue) + } + + func test_handle_whenFingerprintTakesTooLong_succeedsWithTimeout() async throws { + // Given + webSession.authenticateFromClosure = { _, _, _ in + try await Task.sleep(seconds: 15) + return URL(string: "test://return")! + } + let customerAction = ThreeDSCustomerAction(type: .fingerprint, value: "example.com") + + // When + let value = try await sut.handle(action: customerAction, delegate: delegate) + + // Then + let expectedValue = """ + gway_req_eyJib2R5IjoieyBcInRocmVlRFMyRmluZ2VycHJpbnRUaW1lb3V0XCI6IHRydWUgfSIsInVybCI6ImV4YW1wbGUuY29tIn0= """ XCTAssertEqual(value, expectedValue) } // MARK: - Private Properties - private var delegate: Mock3DSService! private var sut: DefaultThreeDSService! + private var delegate: Mock3DSService! + private var webSession: MockWebAuthenticationSession! + // MARK: - Private Methods private func defaultFingerprintMobileCustomerAction(padded: Bool = false) -> ThreeDSCustomerAction { diff --git a/Tests/ProcessOutTests/Sources/Unit/Service/AlternativePaymentMethods/DefaultAlternativePaymentMethodsServiceTests.swift b/Tests/ProcessOutTests/Sources/Unit/Service/AlternativePaymentMethods/DefaultAlternativePaymentMethodsServiceTests.swift index 3b85cf1a9..a44d19104 100644 --- a/Tests/ProcessOutTests/Sources/Unit/Service/AlternativePaymentMethods/DefaultAlternativePaymentMethodsServiceTests.swift +++ b/Tests/ProcessOutTests/Sources/Unit/Service/AlternativePaymentMethods/DefaultAlternativePaymentMethodsServiceTests.swift @@ -13,21 +13,24 @@ final class DefaultAlternativePaymentMethodsServiceTests: XCTestCase { override func setUp() { super.setUp() - let configuration = AlternativePaymentMethodsServiceConfiguration( - projectId: "proj_test", baseUrl: URL(string: "https://example.com")! + sut = DefaultAlternativePaymentsService( + configuration: { + .init(projectId: "proj_test", baseUrl: URL(string: "https://example.com")!) + }, + webSession: MockWebAuthenticationSession(), + logger: .stub ) - sut = DefaultAlternativePaymentMethodsService(configuration: { configuration }, logger: .stub) } func test_alternativePaymentMethodUrl_authorizationWithAdditionalData_succeeds() throws { - let request = POAlternativePaymentMethodRequest( + let request = POAlternativePaymentAuthorizationRequest( invoiceId: "iv_test", gatewayConfigurationId: "gway_conf_test", additionalData: ["field1": "test", "field2": "test2"] ) // When - let url = sut.alternativePaymentMethodUrl(request: request) + let url = try sut.url(for: request) // Then let expectedUrls = [ @@ -41,14 +44,14 @@ final class DefaultAlternativePaymentMethodsServiceTests: XCTestCase { } func test_alternativePaymentMethodUrl_tokenization_succeeds() throws { - let request = POAlternativePaymentMethodRequest( + let request = POAlternativePaymentTokenizationRequest( customerId: "cust_test", tokenId: "tok_test", gatewayConfigurationId: "gway_conf_test" ) // When - let url = sut.alternativePaymentMethodUrl(request: request) + let url = try sut.url(for: request) // Then let expectedUrl = "https://example.com/proj_test/cust_test/tok_test/redirect/gway_conf_test" @@ -56,44 +59,19 @@ final class DefaultAlternativePaymentMethodsServiceTests: XCTestCase { } func test_alternativePaymentMethodUrl_authorizationWithToken_succeeds() throws { - let request = POAlternativePaymentMethodRequest( + let request = POAlternativePaymentAuthorizationRequest( invoiceId: "iv_test", gatewayConfigurationId: "gway_conf_test", tokenId: "tok_test" ) // When - let url = sut.alternativePaymentMethodUrl(request: request) + let url = try sut.url(for: request) // Then let expectedUrl = "https://example.com/proj_test/iv_test/redirect/gway_conf_test/tokenized/tok_test" XCTAssertEqual(url.absoluteString, expectedUrl) } - func test_alternativePaymentMethodResponse_withOnlyGatewayToken_succeeds() throws { - let result: POAlternativePaymentMethodResponse? = try sut.alternativePaymentMethodResponse( - url: URL(string: "https://processout.return?token=gway_req_test")! - ) - - XCTAssertEqual(result?.gatewayToken, "gway_req_test") - } - - func test_alternativePaymentMethodResponse_withCustomerToken_succeeds() throws { - let result: POAlternativePaymentMethodResponse? = try sut.alternativePaymentMethodResponse( - url: URL(string: "https://processout.return?token=gway_req_test&token_id=tok_test&customer_id=cust_test")! - ) - - XCTAssertEqual(result?.gatewayToken, "gway_req_test") - XCTAssertEqual(result?.tokenId, "tok_test") - XCTAssertEqual(result?.customerId, "cust_test") - } - - func test_alternativePaymentMethodResponse_whenTokenIsNotSet_succeeds() throws { - // When - let url = URL(string: "test://return")! - let result = try sut.alternativePaymentMethodResponse(url: url) - - // Then - XCTAssertTrue(result.gatewayToken.isEmpty) - } + // MARK: - Private Properties - private var sut: DefaultAlternativePaymentMethodsService! + private var sut: DefaultAlternativePaymentsService! } diff --git a/Tests/ProcessOutUITests/Sources/Integration/CardUpdate/DefaultCardUpdateInteractorTests.swift b/Tests/ProcessOutUITests/Sources/Integration/CardUpdate/DefaultCardUpdateInteractorTests.swift index 43cb1d3e3..76efaff82 100644 --- a/Tests/ProcessOutUITests/Sources/Integration/CardUpdate/DefaultCardUpdateInteractorTests.swift +++ b/Tests/ProcessOutUITests/Sources/Integration/CardUpdate/DefaultCardUpdateInteractorTests.swift @@ -11,14 +11,15 @@ import XCTest final class DefaultCardUpdateInteractorTests: XCTestCase { - override func setUp() { - super.setUp() - ProcessOut.configure(configuration: .init(projectId: Constants.projectId), force: true) + override func setUp() async throws { + try await super.setUp() + await ProcessOut.configure(configuration: .init(projectId: Constants.projectId), force: true) cardsService = ProcessOut.shared.cards } // MARK: - Start + @MainActor func test_start_whenCardInfoIsNotSet_setsStartingState() { // Given let delegate = CardUpdateDelegateMock() @@ -41,6 +42,7 @@ final class DefaultCardUpdateInteractorTests: XCTestCase { // MARK: - Scheme Resolve + @MainActor func test_start_whenCardSchemeIsSetInConfiguration_setsStartedStateWithIt() { // Given let configuration = POCardUpdateConfiguration(cardId: "", cardInformation: .init(scheme: "visa")) @@ -57,6 +59,7 @@ final class DefaultCardUpdateInteractorTests: XCTestCase { XCTAssertEqual(startedState.scheme, .visa) } + @MainActor func test_start_whenPreferredCardSchemeIsAvailable_setsStartedStateWithIt() { // Given let configuration = POCardUpdateConfiguration( @@ -76,6 +79,7 @@ final class DefaultCardUpdateInteractorTests: XCTestCase { XCTAssertEqual(startedState.preferredScheme, .carteBancaire) } + @MainActor func test_start_whenCardSchemeIsNotSetAndIinIsSet_attemptsToResolve() { // Given let configuration = POCardUpdateConfiguration(cardId: "", cardInformation: .init(iin: "424242")) @@ -94,6 +98,7 @@ final class DefaultCardUpdateInteractorTests: XCTestCase { wait(for: [expectation]) } + @MainActor func test_start_whenCardSchemeIsNotSetAndMaskedNumberIsSet_attemptsToResolve() { // Given let configuration = POCardUpdateConfiguration( @@ -116,6 +121,7 @@ final class DefaultCardUpdateInteractorTests: XCTestCase { // MARK: - Cancel + @MainActor func test_cancel_whenStarted() { // Given let configuration = POCardUpdateConfiguration(cardId: "", cardInformation: .init(scheme: "visa")) @@ -131,6 +137,7 @@ final class DefaultCardUpdateInteractorTests: XCTestCase { // MARK: - Update CVC + @MainActor func test_updateCvc_whenStarting_isIgnored() { // Given let configuration = POCardUpdateConfiguration(cardId: "") @@ -145,6 +152,7 @@ final class DefaultCardUpdateInteractorTests: XCTestCase { XCTAssertEqual(sut.state, oldState) } + @MainActor func test_updateCvc_whenStarted_updatesState() { // Given let configuration = POCardUpdateConfiguration(cardId: "", cardInformation: .init(scheme: "visa")) @@ -163,6 +171,7 @@ final class DefaultCardUpdateInteractorTests: XCTestCase { // MARK: - Submit + @MainActor func test_submit_whenCvcIsNotSet_causesError() { // Given let configuration = POCardUpdateConfiguration(cardId: "", cardInformation: .init(scheme: "visa")) @@ -182,6 +191,7 @@ final class DefaultCardUpdateInteractorTests: XCTestCase { wait(for: [expectation]) } + @MainActor func test_submit_whenValidCvcIsSet_completes() { // Given let configuration = POCardUpdateConfiguration( @@ -210,6 +220,7 @@ final class DefaultCardUpdateInteractorTests: XCTestCase { // MARK: - Private Methods + @MainActor private func createSut( configuration: POCardUpdateConfiguration, delegate: POCardUpdateDelegate? = nil ) -> any CardUpdateInteractor { diff --git a/Tests/ProcessOutUITests/Sources/Mocks/CardUpdate/CardUpdateDelegateMock.swift b/Tests/ProcessOutUITests/Sources/Mocks/CardUpdate/CardUpdateDelegateMock.swift index ad1ff415a..3e7c96978 100644 --- a/Tests/ProcessOutUITests/Sources/Mocks/CardUpdate/CardUpdateDelegateMock.swift +++ b/Tests/ProcessOutUITests/Sources/Mocks/CardUpdate/CardUpdateDelegateMock.swift @@ -5,26 +5,45 @@ // Created by Andrii Vysotskyi on 14.11.2023. // -import ProcessOut +@_spi(PO) import ProcessOut @testable import ProcessOutUI -final class CardUpdateDelegateMock: POCardUpdateDelegate { +final class CardUpdateDelegateMock: POCardUpdateDelegate, Sendable { - var cardUpdateDidEmitEventFromClosure: ((POCardUpdateEvent) -> Void)? - var cardInformationFromClosure: ((String) -> POCardUpdateInformation?)? - var shouldContinueUpdateFromClosure: ((POFailure) -> Bool)? + var cardUpdateDidEmitEventFromClosure: ((POCardUpdateEvent) -> Void)? { + get { lock.withLock { _cardUpdateDidEmitEventFromClosure } } + set { lock.withLock { _cardUpdateDidEmitEventFromClosure = newValue } } + } + + var cardInformationFromClosure: ((String) -> POCardUpdateInformation?)? { + get { lock.withLock { _cardInformationFromClosure } } + set { lock.withLock { _cardInformationFromClosure = newValue } } + } + + var shouldContinueUpdateFromClosure: ((POFailure) -> Bool)? { + get { lock.withLock { _shouldContinueUpdateFromClosure } } + set { lock.withLock { _shouldContinueUpdateFromClosure = newValue } } + } - // MARK: - PO3DSService + // MARK: - POCardUpdateDelegate - func cardUpdateDidEmitEvent(_ event: POCardUpdateEvent) { + func cardUpdate(didEmitEvent event: POCardUpdateEvent) { cardUpdateDidEmitEventFromClosure?(event) } - func cardInformation(cardId: String) async -> POCardUpdateInformation? { + func cardUpdate(informationFor cardId: String) async -> POCardUpdateInformation? { cardInformationFromClosure?(cardId) } - func shouldContinueUpdate(after failure: POFailure) -> Bool { + func cardUpdate(shouldContinueAfter failure: POFailure) -> Bool { shouldContinueUpdateFromClosure?(failure) ?? false } + + // MARK: - Private Properties + + private let lock = POUnfairlyLocked() + + private nonisolated(unsafe) var _cardUpdateDidEmitEventFromClosure: ((POCardUpdateEvent) -> Void)? + private nonisolated(unsafe) var _cardInformationFromClosure: ((String) -> POCardUpdateInformation?)? + private nonisolated(unsafe) var _shouldContinueUpdateFromClosure: ((POFailure) -> Bool)? } diff --git a/Tests/ProcessOutUITests/Sources/Mocks/Logger/Logger+Extensions.swift b/Tests/ProcessOutUITests/Sources/Mocks/Logger/Logger+Extensions.swift index 03502f7ae..5dd72ee62 100644 --- a/Tests/ProcessOutUITests/Sources/Mocks/Logger/Logger+Extensions.swift +++ b/Tests/ProcessOutUITests/Sources/Mocks/Logger/Logger+Extensions.swift @@ -10,5 +10,5 @@ extension POLogger { /// Stub logger. - static var stub = POLogger(category: "") + static let stub = POLogger(category: "") } diff --git a/Tests/ProcessOutUITests/Sources/Mocks/PhoneNumberMetadataProvider/MockPhoneNumberMetadataProvider.swift b/Tests/ProcessOutUITests/Sources/Mocks/PhoneNumberMetadataProvider/MockPhoneNumberMetadataProvider.swift new file mode 100644 index 000000000..9e39ab294 --- /dev/null +++ b/Tests/ProcessOutUITests/Sources/Mocks/PhoneNumberMetadataProvider/MockPhoneNumberMetadataProvider.swift @@ -0,0 +1,35 @@ +// +// MockPhoneNumberMetadataProvider.swift +// ProcessOut +// +// Created by Andrii Vysotskyi on 12.05.2023. +// + +@_spi(PO) import ProcessOut +@testable import ProcessOutUI + +final class MockPhoneNumberMetadataProvider: PhoneNumberMetadataProvider, Sendable { + + var metadataCallsCount: Int { + lock.withLock { _metadataCallsCount } + } + + var metadata: PhoneNumberMetadata? { + get { lock.withLock { _metadata } } + set { lock.withLock { _metadata = newValue } } + } + + func metadata(for countryCode: String) -> PhoneNumberMetadata? { + lock.withLock { + _metadataCallsCount += 1 + return _metadata + } + } + + // MARK: - Private Properties + + private let lock = POUnfairlyLocked() + + private nonisolated(unsafe) var _metadataCallsCount = 0 + private nonisolated(unsafe) var _metadata: PhoneNumberMetadata? +} diff --git a/Tests/ProcessOutTests/Sources/Unit/Core/Formatters/CardExpiration/CardExpirationFormatterTests.swift b/Tests/ProcessOutUITests/Sources/Unit/Core/Formatters/CardExpiration/CardExpirationFormatterTests.swift similarity index 94% rename from Tests/ProcessOutTests/Sources/Unit/Core/Formatters/CardExpiration/CardExpirationFormatterTests.swift rename to Tests/ProcessOutUITests/Sources/Unit/Core/Formatters/CardExpiration/CardExpirationFormatterTests.swift index 73368c78a..eded9fb7e 100644 --- a/Tests/ProcessOutTests/Sources/Unit/Core/Formatters/CardExpiration/CardExpirationFormatterTests.swift +++ b/Tests/ProcessOutUITests/Sources/Unit/Core/Formatters/CardExpiration/CardExpirationFormatterTests.swift @@ -1,18 +1,18 @@ // // CardExpirationFormatterTests.swift -// ProcessOut +// ProcessOutUITests // // Created by Andrii Vysotskyi on 21.07.2023. // import XCTest -@testable @_spi(PO) import ProcessOut +@testable import ProcessOutUI final class CardExpirationFormatterTests: XCTestCase { override func setUp() { super.setUp() - sut = POCardExpirationFormatter() + sut = CardExpirationFormatter() } func test_string() { @@ -105,5 +105,5 @@ final class CardExpirationFormatterTests: XCTestCase { // MARK: - Private Properties - private var sut: POCardExpirationFormatter! + private var sut: CardExpirationFormatter! } diff --git a/Tests/ProcessOutTests/Sources/Unit/Core/Formatters/CardNumber/CardNumberFormatterTests.swift b/Tests/ProcessOutUITests/Sources/Unit/Core/Formatters/CardNumber/CardNumberFormatterTests.swift similarity index 90% rename from Tests/ProcessOutTests/Sources/Unit/Core/Formatters/CardNumber/CardNumberFormatterTests.swift rename to Tests/ProcessOutUITests/Sources/Unit/Core/Formatters/CardNumber/CardNumberFormatterTests.swift index a815f62de..4d1c09f32 100644 --- a/Tests/ProcessOutTests/Sources/Unit/Core/Formatters/CardNumber/CardNumberFormatterTests.swift +++ b/Tests/ProcessOutUITests/Sources/Unit/Core/Formatters/CardNumber/CardNumberFormatterTests.swift @@ -1,18 +1,18 @@ // // CardNumberFormatterTests.swift -// ProcessOut +// ProcessOutUITests // // Created by Andrii Vysotskyi on 19.07.2023. // import XCTest -@testable @_spi(PO) import ProcessOut +@testable import ProcessOutUI final class CardNumberFormatterTests: XCTestCase { override func setUp() { super.setUp() - sut = POCardNumberFormatter() + sut = CardNumberFormatter() } func test_normalized_retainsDigits() { @@ -57,5 +57,5 @@ final class CardNumberFormatterTests: XCTestCase { // MARK: - Private Properties - private var sut: POCardNumberFormatter! + private var sut: CardNumberFormatter! } diff --git a/Tests/ProcessOutTests/Sources/Unit/Core/Formatters/PhoneNumber/DefaultPhoneNumberMetadataProviderTests.swift b/Tests/ProcessOutUITests/Sources/Unit/Core/Formatters/PhoneNumber/DefaultPhoneNumberMetadataProviderTests.swift similarity index 88% rename from Tests/ProcessOutTests/Sources/Unit/Core/Formatters/PhoneNumber/DefaultPhoneNumberMetadataProviderTests.swift rename to Tests/ProcessOutUITests/Sources/Unit/Core/Formatters/PhoneNumber/DefaultPhoneNumberMetadataProviderTests.swift index ddd3d22a6..c14e4ea45 100644 --- a/Tests/ProcessOutTests/Sources/Unit/Core/Formatters/PhoneNumber/DefaultPhoneNumberMetadataProviderTests.swift +++ b/Tests/ProcessOutUITests/Sources/Unit/Core/Formatters/PhoneNumber/DefaultPhoneNumberMetadataProviderTests.swift @@ -6,7 +6,7 @@ // import XCTest -@testable @_spi(PO) import ProcessOut +@testable import ProcessOutUI final class DefaultPhoneNumberMetadataProviderTests: XCTestCase { @@ -33,5 +33,5 @@ final class DefaultPhoneNumberMetadataProviderTests: XCTestCase { // MARK: - Private Properties - private var sut: PODefaultPhoneNumberMetadataProvider! + private var sut: DefaultPhoneNumberMetadataProvider! } diff --git a/Tests/ProcessOutTests/Sources/Unit/Core/Formatters/PhoneNumber/PhoneNumberFormatterTests.swift b/Tests/ProcessOutUITests/Sources/Unit/Core/Formatters/PhoneNumber/PhoneNumberFormatterTests.swift similarity index 81% rename from Tests/ProcessOutTests/Sources/Unit/Core/Formatters/PhoneNumber/PhoneNumberFormatterTests.swift rename to Tests/ProcessOutUITests/Sources/Unit/Core/Formatters/PhoneNumber/PhoneNumberFormatterTests.swift index ef13cab0f..ee742d8c8 100644 --- a/Tests/ProcessOutTests/Sources/Unit/Core/Formatters/PhoneNumber/PhoneNumberFormatterTests.swift +++ b/Tests/ProcessOutUITests/Sources/Unit/Core/Formatters/PhoneNumber/PhoneNumberFormatterTests.swift @@ -1,12 +1,12 @@ // // PhoneNumberFormatterTests.swift -// ProcessOut +// ProcessOutUITests // // Created by Andrii Vysotskyi on 12.05.2023. // import XCTest -@testable @_spi(PO) import ProcessOut +@testable import ProcessOutUI final class PhoneNumberFormatterTests: XCTestCase { @@ -14,7 +14,7 @@ final class PhoneNumberFormatterTests: XCTestCase { super.setUp() metadataProvider = MockPhoneNumberMetadataProvider() metadataProvider.metadata = nil - sut = POPhoneNumberFormatter(metadataProvider: metadataProvider) + sut = PhoneNumberFormatter(metadataProvider: metadataProvider) } func test_normalized_retainsDigitsAndPlus() { @@ -70,8 +70,8 @@ final class PhoneNumberFormatterTests: XCTestCase { func test_string_whenNumberIsComplete_returnsFormattedNumber() { // Given - let format = POPhoneNumberFormat(pattern: "(\\d)(\\d)", leading: [".*"], format: "$1-$2") - metadataProvider.metadata = POPhoneNumberMetadata(countryCode: "1", formats: [format]) + let format = PhoneNumberFormat(pattern: "(\\d)(\\d)", leading: [".*"], format: "$1-$2") + metadataProvider.metadata = PhoneNumberMetadata(countryCode: "1", formats: [format]) // When let formattedNumber = sut.string(from: "123#") @@ -82,8 +82,8 @@ final class PhoneNumberFormatterTests: XCTestCase { func test_string_whenNationalNumberLeadingDigitsAreUnknown_formatsCountryCode() { // Given - let format = POPhoneNumberFormat(pattern: "", leading: [""], format: "") - metadataProvider.metadata = POPhoneNumberMetadata(countryCode: "1", formats: [format]) + let format = PhoneNumberFormat(pattern: "", leading: [""], format: "") + metadataProvider.metadata = PhoneNumberMetadata(countryCode: "1", formats: [format]) // When let formattedNumber = sut.string(from: "123") @@ -94,8 +94,8 @@ final class PhoneNumberFormatterTests: XCTestCase { func test_string_whenNationalNumberLengthExceedsMaximumLength_formatsCountryCode() { // Given - let format = POPhoneNumberFormat(pattern: "", leading: [], format: "") - metadataProvider.metadata = POPhoneNumberMetadata(countryCode: "1", formats: [format]) + let format = PhoneNumberFormat(pattern: "", leading: [], format: "") + metadataProvider.metadata = PhoneNumberMetadata(countryCode: "1", formats: [format]) // When let formattedNumber = sut.string(from: "1123456789123456") @@ -106,8 +106,8 @@ final class PhoneNumberFormatterTests: XCTestCase { func test_string_whenNumberContainsEasternArabicNumerals_returnsFormattedNumberWithLatinNumerals() { // Given - let format = POPhoneNumberFormat(pattern: "(\\d)(\\d+)", leading: [".*"], format: "$1-$2") - metadataProvider.metadata = POPhoneNumberMetadata(countryCode: "1", formats: [format]) + let format = PhoneNumberFormat(pattern: "(\\d)(\\d+)", leading: [".*"], format: "$1-$2") + metadataProvider.metadata = PhoneNumberMetadata(countryCode: "1", formats: [format]) // When let formattedNumber = sut.string(from: "١٢٣") @@ -118,8 +118,8 @@ final class PhoneNumberFormatterTests: XCTestCase { func test_string_whenNumberIsPartial_returnsFormattedNumberWithoutTrailingSeparators() { // Given - let format = POPhoneNumberFormat(pattern: "(\\d)(\\d)(\\d)", leading: [".*"], format: "$1-$2-$3") - metadataProvider.metadata = POPhoneNumberMetadata(countryCode: "1", formats: [format]) + let format = PhoneNumberFormat(pattern: "(\\d)(\\d)(\\d)", leading: [".*"], format: "$1-$2-$3") + metadataProvider.metadata = PhoneNumberMetadata(countryCode: "1", formats: [format]) // When let formattedNumber = sut.string(from: "123") @@ -185,5 +185,5 @@ final class PhoneNumberFormatterTests: XCTestCase { // MARK: - Private Properties private var metadataProvider: MockPhoneNumberMetadataProvider! - private var sut: POPhoneNumberFormatter! + private var sut: PhoneNumberFormatter! } diff --git a/Tests/ProcessOutTests/Sources/Unit/Core/Utils/FormattingUtilsTests.swift b/Tests/ProcessOutUITests/Sources/Unit/Core/Utils/FormattingUtilsTests.swift similarity index 81% rename from Tests/ProcessOutTests/Sources/Unit/Core/Utils/FormattingUtilsTests.swift rename to Tests/ProcessOutUITests/Sources/Unit/Core/Utils/FormattingUtilsTests.swift index 21d96518c..552c6b133 100644 --- a/Tests/ProcessOutTests/Sources/Unit/Core/Utils/FormattingUtilsTests.swift +++ b/Tests/ProcessOutUITests/Sources/Unit/Core/Utils/FormattingUtilsTests.swift @@ -6,13 +6,13 @@ // import XCTest -@testable @_spi(PO) import ProcessOut +@testable import ProcessOutUI final class FormattingUtilsTests: XCTestCase { func test_adjustedCursorOffset_whenNotGreedy_doesNotIncludeSignificants() { // When - let offset = POFormattingUtils.adjustedCursorOffset( + let offset = FormattingUtils.adjustedCursorOffset( in: "1 2", source: "12", sourceCursorOffset: 1, significantCharacters: .decimalDigits, greedy: false ) @@ -22,7 +22,7 @@ final class FormattingUtilsTests: XCTestCase { func test_adjustedCursorOffset_whenGreedy_includesNonSignificants() { // When - let offset = POFormattingUtils.adjustedCursorOffset( + let offset = FormattingUtils.adjustedCursorOffset( in: "1 2", source: "12", sourceCursorOffset: 1, significantCharacters: .decimalDigits ) @@ -32,7 +32,7 @@ final class FormattingUtilsTests: XCTestCase { func test_adjustedCursorOffset_whenCursorPrefixChanges_returnValidOffset() { // When - let offset = POFormattingUtils.adjustedCursorOffset( + let offset = FormattingUtils.adjustedCursorOffset( in: "+12", source: "12", sourceCursorOffset: 1, significantCharacters: .decimalDigits ) @@ -42,7 +42,7 @@ final class FormattingUtilsTests: XCTestCase { func test_adjustedCursorOffset_whenCursorSuffixChanges_returnEndOfTarget() { // When - let offset = POFormattingUtils.adjustedCursorOffset( + let offset = FormattingUtils.adjustedCursorOffset( in: "2", source: "1", sourceCursorOffset: 0, significantCharacters: .decimalDigits ) @@ -52,7 +52,7 @@ final class FormattingUtilsTests: XCTestCase { func test_adjustedCursorOffset_whenNewCharacterIsAddedToCursorSuffix_returnEndOfTarget() { // When - let offset = POFormattingUtils.adjustedCursorOffset( + let offset = FormattingUtils.adjustedCursorOffset( in: "123", source: "12", sourceCursorOffset: 1, significantCharacters: .decimalDigits ) @@ -62,7 +62,7 @@ final class FormattingUtilsTests: XCTestCase { func test_adjustedCursorOffset_whenSourceIsSameAsTarget_returnsSameOffset() { // When - let offset = POFormattingUtils.adjustedCursorOffset( + let offset = FormattingUtils.adjustedCursorOffset( in: "1", source: "1", sourceCursorOffset: 0, significantCharacters: .decimalDigits ) @@ -72,7 +72,7 @@ final class FormattingUtilsTests: XCTestCase { func test_adjustedCursorOffset_whenSourceIsEmpty_returnEndOfTarget() { // When - let offset = POFormattingUtils.adjustedCursorOffset( + let offset = FormattingUtils.adjustedCursorOffset( in: "1", source: "", sourceCursorOffset: 0, significantCharacters: .decimalDigits ) @@ -82,7 +82,7 @@ final class FormattingUtilsTests: XCTestCase { func test_adjustedCursorOffset_whenTargetIsEmpty_returnStartOfTarget() { // When - let offset = POFormattingUtils.adjustedCursorOffset( + let offset = FormattingUtils.adjustedCursorOffset( in: "", source: "0", sourceCursorOffset: 1, significantCharacters: .decimalDigits ) diff --git a/project.yml b/project.yml index 0768f9270..8afdc5935 100644 --- a/project.yml +++ b/project.yml @@ -4,8 +4,9 @@ settings: CODE_SIGN_IDENTITY: "" CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED: true SUPPORTS_MACCATALYST: false - LOCALIZED_STRING_MACRO_NAMES: "$(inherited) POStringResource" + LOCALIZED_STRING_MACRO_NAMES: "$(inherited) StringResource" LOCALIZATION_PREFERS_STRING_CATALOGS: true + SWIFT_STRICT_CONCURRENCY: complete ASSETCATALOG_COMPILER_GENERATE_ASSET_SYMBOLS: false options: transitivelyLinkDependencies: true @@ -17,7 +18,7 @@ targets: ProcessOutCoreUI: type: framework platform: iOS - deploymentTarget: "13.0" + deploymentTarget: "14.0" settings: PRODUCT_BUNDLE_IDENTIFIER: com.processout.core-ui TARGET_ROOT: $(PROJECT_DIR)/Sources/ProcessOutCoreUI @@ -32,7 +33,7 @@ targets: ProcessOutUI: type: framework platform: iOS - deploymentTarget: "13.0" + deploymentTarget: "14.0" settings: PRODUCT_BUNDLE_IDENTIFIER: com.processout.ui TARGET_ROOT: $(PROJECT_DIR)/Sources/ProcessOutUI @@ -48,12 +49,12 @@ targets: ProcessOut: type: framework platform: iOS - deploymentTarget: "13.0" + deploymentTarget: "14.0" settings: PRODUCT_BUNDLE_IDENTIFIER: com.processout.processout-ios MARKETING_VERSION: ${CURRENT_VERSION} CURRENT_PROJECT_VERSION: 1 - OTHER_SWIFT_FLAGS: "-Xfrontend -module-interface-preserve-types-as-written" + OTHER_SWIFT_FLAGS: "$(inherited) -Xfrontend -module-interface-preserve-types-as-written" TARGET_ROOT: $(PROJECT_DIR)/Sources/ProcessOut preBuildScripts: - path: Scripts/Lint.sh @@ -64,8 +65,6 @@ targets: basedOnDependencyAnalysis: false sources: - path: Sources/ProcessOut - dependencies: - - framework: Vendor/cmark_gfm.xcframework ProcessOutTests: type: bundle.unit-test platform: iOS @@ -87,7 +86,7 @@ targets: ProcessOutCheckout3DS: type: framework platform: iOS - deploymentTarget: "13.0" + deploymentTarget: "14.0" settings: PRODUCT_BUNDLE_IDENTIFIER: com.processout.checkout-3ds-ios EXCLUDED_ARCHS: x86_64 # Checkout3DS doesn't support x86_64 so are we