From aa41e4228575cb9eb96952a98ccc750cdefd89b3 Mon Sep 17 00:00:00 2001 From: Andrii Vysotskyi Date: Fri, 26 Jul 2024 16:13:28 +0200 Subject: [PATCH 01/10] fix(POM-393): resolve primary concurrency warnings (#314) --- Package.swift | 15 +- .../ProcessOutHttpConnectorBuilder.swift | 4 +- .../Api/Models/PODeepLinkReceivedEvent.swift | 3 +- .../Api/Models/ProcessOutConfiguration.swift | 4 +- .../ProcessOut/Sources/Api/ProcessOut.swift | 235 +++++++++++------- .../Api/Utils/Test3DS/POTest3DSService.swift | 51 ++-- .../Connectors/Http/HttpConnector.swift | 2 +- .../DefaultHttpConnectorRequestMapper.swift | 4 +- .../HttpConnectorRequestMapper.swift | 2 +- .../UrlSession/UrlSessionHttpConnector.swift | 2 +- .../Http/Models/HttpConnectorFailure.swift | 6 +- .../Http/Models/HttpConnectorRequest.swift | 14 +- .../Core/Cancellable/POCancellable.swift | 2 +- .../POFallbackValueProvider.swift | 2 +- .../POImmutableExcludedCodable.swift | 2 + .../POImmutableStringCodableDecimal.swift | 2 +- ...mmutableStringCodableOptionalDecimal.swift | 2 +- .../CodingUtils/POStringCodableColor.swift | 2 +- .../Core/CodingUtils/VoidCodable.swift | 2 +- .../Core/DeviceMetadata/DeviceMetadata.swift | 2 +- .../DeviceMetadataProvider.swift | 2 +- .../EventEmitter/POEventEmitterEvent.swift | 3 +- .../{Utils => Extensions}/Task+Sleep.swift | 0 .../UIImage+Dynamic.swift | 1 + ...PODefaultPhoneNumberMetadataProvider.swift | 15 +- .../POPhoneNumberFormat.swift | 3 +- .../POPhoneNumberMetadata.swift | 3 +- .../POPhoneNumberMetadataProvider.swift | 3 +- .../PhoneNumber/POPhoneNumberFormatter.swift | 3 +- .../UrlRequest/UrlRequestFormatter.swift | 2 +- .../UrlResponse/UrlResponseFormatter.swift | 2 +- .../Sources/Core/Keychain/Keychain.swift | 2 +- .../Keychain/KeychainItemAccessibility.swift | 6 +- .../Core/Keychain/KeychainItemClass.swift | 6 +- .../SystemLoggerDestination.swift | 2 +- .../Core/Logger/LoggerDestination.swift | 2 +- .../Sources/Core/Logger/Models/LogEvent.swift | 4 +- .../Logger/Models/POLogAttributeKey.swift | 2 +- .../Sources/Core/Logger/POLogger.swift | 8 +- .../Core/Markdown/MarkdownParser.swift | 4 +- .../Markdown/Nodes/MarkdownBlockQuote.swift | 2 +- .../Markdown/Nodes/MarkdownCodeBlock.swift | 28 +-- .../Markdown/Nodes/MarkdownCodeSpan.swift | 21 +- .../Markdown/Nodes/MarkdownDocument.swift | 8 +- .../Markdown/Nodes/MarkdownEmphasis.swift | 4 +- .../Core/Markdown/Nodes/MarkdownHeading.swift | 11 +- .../Markdown/Nodes/MarkdownLinebreak.swift | 2 +- .../Core/Markdown/Nodes/MarkdownLink.swift | 18 +- .../Core/Markdown/Nodes/MarkdownList.swift | 38 +-- .../Markdown/Nodes/MarkdownListItem.swift | 4 +- .../Core/Markdown/Nodes/MarkdownNode.swift | 22 +- .../Markdown/Nodes/MarkdownParagraph.swift | 4 +- .../Markdown/Nodes/MarkdownSoftbreak.swift | 2 +- .../Core/Markdown/Nodes/MarkdownStrong.swift | 4 +- .../Core/Markdown/Nodes/MarkdownText.swift | 21 +- .../Nodes/MarkdownThematicBreak.swift | 2 +- .../Core/Markdown/Nodes/MarkdownUnknown.swift | 2 +- .../MarkdownDebugDescriptionPrinter.swift | 10 +- .../ImmutableNullHashable.swift | 2 + .../Core/RegexProvider/RegexProvider.swift | 4 +- .../Core/RetryStrategy/RetryStrategy.swift | 4 +- .../Sources/Core/Utils/AsyncUtils.swift | 10 +- .../Sources/Core/Utils/Batcher.swift | 10 +- .../Sources/Core/Utils/POStringResource.swift | 2 +- .../Core/Utils/POTypedRepresentation.swift | 2 + .../UnfairlyLocked/POUnfairlyLocked.swift | 3 +- .../Utils/UnfairlyLocked/UnfairLock.swift | 4 +- .../Generated/Sourcery+Generated.swift | 51 ++-- .../Sources/Legacy/CardPaymentWebView.swift | 1 + .../Legacy/ProcessOutRequestManager.swift | 1 + .../Sources/Legacy/ProcessOutWebView.swift | 1 + .../Cards/HttpCardsRepository.swift | 2 +- .../ApplePayCardTokenizationRequest.swift | 10 +- .../Requests/POCardTokenizationRequest.swift | 2 +- .../Cards/Requests/POCardUpdateRequest.swift | 2 +- .../Cards/Requests/POContact.swift | 2 +- .../Responses/CardTokenizationResponse.swift | 2 +- .../Repositories/Cards/Responses/POCard.swift | 2 +- .../Responses/POCardIssuerInformation.swift | 2 +- .../POAssignCustomerTokenRequest.swift | 2 +- .../POCreateCustomerTokenRequest.swift | 2 +- .../AssignCustomerTokenResponse.swift | 2 +- .../Responses/POCustomerToken.swift | 4 +- .../POAllGatewayConfigurationsRequest.swift | 4 +- .../POFindGatewayConfigurationRequest.swift | 6 +- .../POAllGatewayConfigurationsResponse.swift | 2 +- .../Responses/POGatewayConfiguration.swift | 6 +- .../POImageRemoteResource.swift | 4 +- .../Images/POImagesRepository.swift | 3 +- ...tiveAlternativePaymentCaptureRequest.swift | 2 +- ...ativeAlternativePaymentMethodRequest.swift | 2 +- ...ymentMethodTransactionDetailsRequest.swift | 4 +- .../POInvoiceAuthorizationRequest.swift | 2 +- .../Requests/POInvoiceCreationRequest.swift | 2 +- .../Invoices/Requests/POInvoiceRequest.swift | 2 +- ...iveAlternativePaymentMethodParameter.swift | 6 +- ...ernativePaymentMethodParameterValues.swift | 2 +- ...tiveAlternativePaymentMethodResponse.swift | 4 +- ...ONativeAlternativePaymentMethodState.swift | 2 +- ...ativePaymentMethodTransactionDetails.swift | 6 +- .../POBillingAddressCollectionMode.swift | 2 +- .../PODynamicCheckoutPaymentMethod.swift | 30 +-- .../POStringDecodableMerchantCapability.swift | 2 +- .../Invoices/Responses/POInvoice.swift | 2 +- .../Responses/ThreeDSCustomerAction.swift | 4 +- .../HttpConnectorFailureMapper.swift | 2 +- .../Repositories/Shared/PORepository.swift | 2 +- .../POPaginationOptions.swift | 6 +- .../Shared/Responses/POFailure.swift | 20 +- .../Repositories/Telemetry/Telemetry.swift | 10 +- .../Models/PO3DS2AuthenticationRequest.swift | 2 +- .../Services/3DS/Models/PO3DS2Challenge.swift | 2 +- .../3DS/Models/PO3DS2Configuration.swift | 2 +- .../PO3DS2ConfigurationCardScheme.swift | 2 +- .../Services/3DS/Models/PO3DSRedirect.swift | 2 +- .../Sources/Services/3DS/PO3DSService.swift | 20 +- .../Sources/Services/3DS/ThreeDSService.swift | 2 +- ...aultAlternativePaymentMethodsService.swift | 4 +- .../POAlternativePaymentMethodsService.swift | 2 +- .../POAlternativePaymentMethodRequest.swift | 2 +- .../POAlternativePaymentMethodResponse.swift | 4 +- .../Services/Cards/DefaultCardsService.swift | 2 +- ...pplePayCardTokenizationRequestMapper.swift | 6 +- ...pplePayCardTokenizationRequestMapper.swift | 1 + .../PassKitContact/PassKitContactMapper.swift | 2 +- .../POApplePayCardTokenizationRequest.swift | 1 + ...tiveAlternativePaymentCaptureRequest.swift | 2 +- .../Sources/Services/Shared/POService.swift | 2 +- .../Telemetry/DefaultTelemetryService.swift | 7 +- .../TelemetryServiceConfiguration.swift | 2 +- .../PO3DSRedirectViewControllerBuilder.swift | 1 + ...vePaymentMethodViewControllerBuilder.swift | 1 + ...vePaymentMethodViewControllerBuilder.swift | 8 +- ...veAlternativePaymentMethodInteractor.swift | 143 ++++++----- ...tiveAlternativePaymentMethodDelegate.swift | 7 +- ...veAlternativePaymentMethodInteractor.swift | 4 +- ...ernativePaymentMethodInteractorState.swift | 8 +- ...ernativePaymentMethodBackgroundStyle.swift | 1 + ...ONativeAlternativePaymentMethodStyle.swift | 2 + .../NativeAlternativePaymentMethodCell.swift | 2 + ...iveAlternativePaymentMethodViewModel.swift | 10 +- ...iveAlternativePaymentMethodViewModel.swift | 1 + .../Safari/DefaultSafariViewModel.swift | 7 +- .../SafariViewController+Extensions.swift | 1 + .../View/BaseViewController.swift | 8 +- .../Architecture/ViewModel/ViewModel.swift | 1 + .../CollectionViewDelegateCenterLayout.swift | 1 + .../Styles/Input/POInputStateStyle.swift | 1 + .../Styles/Input/POInputStyle.swift | 1 + .../DesignSystem/Styles/POBorderStyle.swift | 1 + .../DesignSystem/Styles/POShadowStyle.swift | 1 + .../DesignSystem/Typography/POTextStyle.swift | 1 + .../Typography/POTypography.swift | 3 + .../POActionsContainerStyle.swift | 1 + .../ActivityIndicatorViewFactory.swift | 1 + .../POActivityIndicatorStyle.swift | 1 + .../POActivityIndicatorView.swift | 1 + .../Views/Button/POButtonStateStyle.swift | 1 + .../Views/Button/POButtonStyle.swift | 1 + .../CodeTextField/CodeTextFieldDelegate.swift | 1 + .../PORadioButtonKnobStateStyle.swift | 1 + .../RadioButton/PORadioButtonStateStyle.swift | 1 + .../RadioButton/PORadioButtonStyle.swift | 1 + .../TextField/TextFieldContainerView.swift | 6 +- .../Extensions/UIImageView+Extensions.swift | 1 + .../CollectionReusableViewSizeProvider.swift | 1 + .../Shared/Utils/KeyboardNotification.swift | 1 + .../Sources/UI/Shared/Utils/Reusable.swift | 1 + .../UI/Shared/Utils/TextFieldUtils.swift | 1 + .../FocusState/FocusCoordinator.swift | 4 +- .../Backports/FocusState/View+Focused.swift | 19 +- .../Backports/OnSubmit/View+OnSubmit.swift | 44 ++-- .../Sources/Backports/POBackport.swift | 7 +- .../SubmitLabel/View+SubmitLabel.swift | 2 +- .../Sources/Backports/Task/View+Task.swift | 8 +- .../HorizontalSizeReader.swift | 2 +- .../Core/MarkdownParser/MarkdownParser.swift | 4 +- .../Nodes/MarkdownBlockQuote.swift | 2 +- .../Nodes/MarkdownCodeBlock.swift | 28 +-- .../Nodes/MarkdownCodeSpan.swift | 21 +- .../Nodes/MarkdownDocument.swift | 8 +- .../Nodes/MarkdownEmphasis.swift | 4 +- .../Nodes/MarkdownHeading.swift | 11 +- .../Nodes/MarkdownLinebreak.swift | 2 +- .../MarkdownParser/Nodes/MarkdownLink.swift | 18 +- .../MarkdownParser/Nodes/MarkdownList.swift | 38 +-- .../Nodes/MarkdownListItem.swift | 4 +- .../MarkdownParser/Nodes/MarkdownNode.swift | 22 +- .../Nodes/MarkdownParagraph.swift | 4 +- .../Nodes/MarkdownSoftbreak.swift | 2 +- .../MarkdownParser/Nodes/MarkdownStrong.swift | 4 +- .../MarkdownParser/Nodes/MarkdownText.swift | 21 +- .../Nodes/MarkdownThematicBreak.swift | 2 +- .../Nodes/MarkdownUnknown.swift | 2 +- .../MarkdownDebugDescriptionPrinter.swift | 26 +- .../Core/Modifiers/Blink/View+Blink.swift | 3 +- .../KeyboardType/View+KeyboardType.swift | 3 +- .../Core/Modifiers/Modify/View+Modify.swift | 3 +- .../OnSizeChange/View+OnSizeChange.swift | 2 +- .../View+TextContentType.swift | 3 +- .../POActionsContainerStyle.swift | 1 + .../View+ActionsContainerStyle.swift | 3 +- .../AsyncImage/POAsyncImage.swift | 1 - .../DesignSystem/Border/POBorderStyle.swift | 2 +- .../PassKit/POPassKitPaymentButton.swift | 1 + .../PassKit/POPassKitPaymentButtonStyle.swift | 2 +- .../Button/Regular/POButtonStateStyle.swift | 2 +- .../CodeField/CodeFieldViewCoordinator.swift | 1 + .../DesignSystem/CodeField/POCodeField.swift | 3 +- .../CodeField/Style/AnyCodeFieldStyle.swift | 25 -- .../CodeField/Style/CodeFieldStyle.swift | 1 + .../Style/CodeFieldStyleConfiguration.swift | 3 + .../CodeField/Style/View+CodeFieldStyle.swift | 11 +- .../View+ConfirmationDialog.swift | 2 +- .../InputStyle/POInputStateStyle.swift | 2 +- .../InputStyle/POInputStyle.swift | 2 +- .../InputStyle/View+InputStyle.swift | 3 +- .../Message/AnyMessageViewStyle.swift | 25 -- .../Message/MessageView+Style.swift | 9 +- .../DesignSystem/Message/POMessageView.swift | 2 +- .../Message/POMessageViewStyle.swift | 1 + .../POMessageViewStyleConfiguration.swift | 3 + .../Message/POToastMessageStyle.swift | 2 +- .../DesignSystem/Picker/POPicker.swift | 2 +- .../DesignSystem/Picker/POPickerStyle.swift | 28 +-- .../Picker/View+PickerStyle.swift | 11 +- .../ProgressView/View+ProgressViewStyle.swift | 2 +- .../PORadioButtonKnobStateStyle.swift | 2 +- .../RadioButton/PORadioButtonStateStyle.swift | 2 +- .../View+RadioButtonSelected.swift | 3 +- .../DesignSystem/Shadow/POShadowStyle.swift | 2 +- .../DesignSystem/Spacing/POSpacing.swift | 3 +- .../DesignSystem/Text/POTextStyle.swift | 2 +- .../DesignSystem/TextField/POTextField.swift | 2 +- .../Typography/POTypography.swift | 2 +- .../ResourceSymbols/ColorResource.swift | 2 +- .../ResourceSymbols/FontResource.swift | 2 +- .../Api/Test3DS/POTest3DSService.swift | 38 +-- .../Core/Interactor/BaseInteractor.swift | 1 - .../Sources/Core/Interactor/Interactor.swift | 1 + .../AddressSpecification.swift | 12 +- .../AddressSpecificationProvider.swift | 11 +- .../CardScheme/CardSchemeProvider.swift | 6 +- .../CardSchemeImageProvider.swift | 2 +- .../PresentingViewControllerProvider.swift | 1 + .../POConfirmationDialogConfiguration.swift | 2 +- .../Sources/Core/ViewModel/AnyViewModel.swift | 30 ++- .../Sources/Core/ViewModel/ViewModel.swift | 4 + .../3DSRedirect/PO3DSRedirectController.swift | 7 +- ...WebAuthenticationSession+3DSRedirect.swift | 4 +- .../SFSafariViewController+3DSRedirect.swift | 6 +- ...enticationSession+AlternativePayment.swift | 8 +- ...ariViewController+AlternativePayment.swift | 33 ++- .../POBillingAddressConfiguration.swift | 2 +- .../POCardTokenizationConfiguration.swift | 2 +- .../Delegate/POCardTokenizationDelegate.swift | 8 +- .../Delegate/POCardTokenizationEvent.swift | 2 +- .../CardTokenizationInteractor.swift | 4 +- .../CardTokenizationInteractorState.swift | 3 + .../DefaultCardTokenizationInteractor.swift | 26 +- .../Style/POCardTokenizationStyle.swift | 23 +- .../Style/View+CardTokenizationStyle.swift | 3 +- .../View/POCardTokenizationView.swift | 1 + .../CardTokenizationViewModelState.swift | 3 + .../DefaultCardTokenizationViewModel.swift | 4 + .../POCardUpdateConfiguration.swift | 2 +- .../Delegate/POCardUpdateDelegate.swift | 20 +- .../Delegate/POCardUpdateEvent.swift | 2 +- .../Delegate/POCardUpdateInformation.swift | 2 +- .../Interactor/CardUpdateInteractor.swift | 4 +- .../CardUpdateInteractorState.swift | 3 + .../DefaultCardUpdateInteractor.swift | 9 +- .../CardUpdate/Style/POCardUpdateStyle.swift | 21 +- .../Style/View+CardUpdateStyle.swift | 3 +- .../View/POCardUpdateView+Init.swift | 2 +- .../ViewModel/CardUpdateViewModel.swift | 1 + .../ViewModel/CardUpdateViewModelItem.swift | 3 + .../CardUpdateViewModelSection.swift | 3 + ...ckoutAlternativePaymentConfiguration.swift | 6 +- .../PODynamicCheckoutCardConfiguration.swift | 4 +- .../PODynamicCheckoutConfiguration.swift | 6 +- .../Delegate/PODynamicCheckoutDelegate.swift | 13 +- .../Delegate/PODynamicCheckoutEvent.swift | 2 +- ...namicCheckoutInteractorChildProvider.swift | 1 + .../DynamicCheckoutDefaultInteractor.swift | 10 +- .../DynamicCheckoutInteractor.swift | 1 + ...koutAlternativePaymentDefaultSession.swift | 20 +- ...micCheckoutAlternativePaymentSession.swift | 1 + ...CheckoutPassKitPaymentDefaultSession.swift | 1 - ...DynamicCheckoutPassKitPaymentSession.swift | 1 + .../Style/PODynamicCheckoutStyle.swift | 3 + .../Style/View+DynamicCheckoutStyle.swift | 3 +- .../View/PODynamicCheckoutView+Init.swift | 2 +- .../View/PODynamicCheckoutView.swift | 1 + .../DefaultDynamicCheckoutViewModel.swift | 4 + .../DynamicCheckoutViewModelItem.swift | 3 + .../DynamicCheckoutViewModelState.swift | 7 +- ...eAlternativePaymentDefaultInteractor.swift | 15 +- .../NativeAlternativePaymentInteractor.swift | 1 + ...iveAlternativePaymentBackgroundStyle.swift | 1 + .../PONativeAlternativePaymentStyle.swift | 1 + ...+NativeAlternativePaymentMethodStyle.swift | 3 +- .../PONativeAlternativePaymentView+Init.swift | 2 +- .../View/PONativeAlternativePaymentView.swift | 1 + ...ultNativeAlternativePaymentViewModel.swift | 8 +- ...tiveAlternativePaymentViewModelState.swift | 4 +- ...assKitPaymentAuthorizationController.swift | 23 +- ...ymentAuthorizationControllerDelegate.swift | 7 + .../DefaultSafariViewModel.swift | 11 +- .../POWebAuthenticationSession.swift | 11 +- .../POWebAuthenticationSessionCallback.swift | 4 +- .../SafariViewController+Extensions.swift | 2 +- Templates/AutoCompletion.stencil | 13 +- project.yml | 1 + 314 files changed, 1227 insertions(+), 993 deletions(-) rename Sources/ProcessOut/Sources/Core/{Utils => Extensions}/Task+Sleep.swift (100%) rename Sources/ProcessOut/Sources/Core/{Utils => Extensions}/UIImage+Dynamic.swift (97%) rename Sources/ProcessOut/Sources/Core/Formatters/PhoneNumber/{ => MetadataProvider}/POPhoneNumberFormat.swift (81%) rename Sources/ProcessOut/Sources/Core/Formatters/PhoneNumber/{ => MetadataProvider}/POPhoneNumberMetadata.swift (77%) rename Sources/ProcessOut/Sources/Repositories/{Shared/Responses => Images}/POImageRemoteResource.swift (79%) rename Sources/ProcessOut/Sources/Repositories/{Shared/Responses => Invoices/Responses/AlternativePayment}/PONativeAlternativePaymentMethodParameter.swift (91%) rename Sources/ProcessOut/Sources/Repositories/{Shared/Responses => Invoices/Responses/DynamicCheckout}/POBillingAddressCollectionMode.swift (83%) rename Sources/ProcessOut/Sources/Repositories/Invoices/Responses/{ => DynamicCheckout}/PODynamicCheckoutPaymentMethod.swift (89%) rename Sources/ProcessOut/Sources/Repositories/Invoices/Responses/{ => DynamicCheckout}/POStringDecodableMerchantCapability.swift (95%) rename Sources/ProcessOut/Sources/Repositories/{Shared => Invoices}/Responses/ThreeDSCustomerAction.swift (85%) delete mode 100644 Sources/ProcessOutCoreUI/Sources/DesignSystem/CodeField/Style/AnyCodeFieldStyle.swift delete mode 100644 Sources/ProcessOutCoreUI/Sources/DesignSystem/Message/AnyMessageViewStyle.swift diff --git a/Package.swift b/Package.swift index 048f4f333..e98b56b8b 100644 --- a/Package.swift +++ b/Package.swift @@ -1,7 +1,11 @@ -// swift-tools-version: 5.9 +// swift-tools-version: 6.0 import PackageDescription +let swiftSettings: [SwiftSetting] = [ + .enableUpcomingFeature("StrictConcurrency") +] + let package = Package( name: "ProcessOut", defaultLocalization: "en", @@ -26,7 +30,8 @@ let package = Package( exclude: ["swiftgen.yml"], resources: [ .process("Resources") - ] + ], + swiftSettings: swiftSettings ), .target( name: "ProcessOutCheckout3DS", @@ -45,7 +50,8 @@ let package = Package( ], resources: [ .process("Resources") - ] + ], + swiftSettings: swiftSettings ), .target( name: "ProcessOutCoreUI", @@ -54,7 +60,8 @@ let package = Package( ], resources: [ .process("Resources") - ] + ], + swiftSettings: swiftSettings ), .binaryTarget(name: "cmark", path: "Vendor/cmark.xcframework") ] 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 index 9feb2fb01..6a474d5da 100644 --- a/Sources/ProcessOut/Sources/Api/Models/PODeepLinkReceivedEvent.swift +++ b/Sources/ProcessOut/Sources/Api/Models/PODeepLinkReceivedEvent.swift @@ -7,7 +7,8 @@ import Foundation -@_spi(PO) public struct PODeepLinkReceivedEvent: POEventEmitterEvent { +@_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 158d87d92..a4b4bc665 100644 --- a/Sources/ProcessOut/Sources/Api/Models/ProcessOutConfiguration.swift +++ b/Sources/ProcessOut/Sources/Api/Models/ProcessOutConfiguration.swift @@ -13,9 +13,9 @@ 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:)`` /// method. -public struct ProcessOutConfiguration { +public struct ProcessOutConfiguration: Sendable { - public struct Application { + public struct Application: Sendable { /// Application name. public let name: String? diff --git a/Sources/ProcessOut/Sources/Api/ProcessOut.swift b/Sources/ProcessOut/Sources/Api/ProcessOut.swift index 7a6eea6ac..509f0e1c5 100644 --- a/Sources/ProcessOut/Sources/Api/ProcessOut.swift +++ b/Sources/ProcessOut/Sources/Api/ProcessOut.swift @@ -5,6 +5,8 @@ // Created by Andrii Vysotskyi on 07.10.2022. // +// swiftlint:disable implicitly_unwrapped_optional force_unwrapping + import Foundation import UIKit @@ -12,58 +14,28 @@ import UIKit 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.checkoutBaseUrl) - } - return DefaultAlternativePaymentMethodsService(configuration: serviceConfiguration, logger: serviceLogger) - }() + /// Alternative payment methods service. + public private(set) var alternativePaymentMethods: POAlternativePaymentMethodsService! - /// Returns cards repository. - public private(set) lazy var cards: POCardsService = { - let contactMapper = DefaultPassKitContactMapper( - logger: serviceLogger - ) - let requestMapper = DefaultApplePayCardTokenizationRequestMapper( - contactMapper: contactMapper, - decoder: JSONDecoder(), - logger: serviceLogger - ) - let service = DefaultCardsService( - repository: HttpCardsRepository(connector: httpConnector), - applePayCardTokenizationRequestMapper: requestMapper - ) - 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 - ) - }() + public private(set) var customerTokens: POCustomerTokensService! /// Call this method in your app or scene delegate whenever your implementation receives incoming URL. Only deep /// links are supported. @@ -79,15 +51,15 @@ public final class ProcessOut { /// Logger with application category. @_spi(PO) - public private(set) lazy var logger: POLogger = createLogger(for: Constants.applicationLoggerCategory) + public private(set) var logger: POLogger! /// Event emitter to use for events exchange. @_spi(PO) - public private(set) lazy var eventEmitter: POEventEmitter = LocalEventEmitter(logger: logger) + public private(set) var eventEmitter: POEventEmitter! /// 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 @@ -101,58 +73,99 @@ 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() + } - private lazy var httpConnector: HttpConnector = { - createConnector(includeLoggerRemoteDestination: true) - }() + @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 lazy var remoteLoggerDestination: LoggerDestination = { - let configuration: () -> TelemetryServiceConfiguration = { [unowned self] 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(includeLoggerRemoteDestination: false) + private func initServices(httpConnector: HttpConnector, threeDSService: ThreeDSService, logger: POLogger) { + gatewayConfigurations = HttpGatewayConfigurationsRepository( + connector: httpConnector ) - return DefaultTelemetryService( - configuration: configuration, repository: repository, deviceMetadataProvider: deviceMetadataProvider + invoices = Self.createInvoicesService( + httpConnector: httpConnector, threeDSService: threeDSService, logger: logger + ) + alternativePaymentMethods = createAlternativePaymentsService() + cards = Self.createCardsService( + httpConnector: httpConnector, logger: logger ) - }() + customerTokens = Self.createCustomerTokensService( + httpConnector: httpConnector, threeDSService: threeDSService, logger: logger + ) + eventEmitter = LocalEventEmitter(logger: logger) + } + + // MARK: - - private lazy var threeDSService: ThreeDSService = { + 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), + applePayCardTokenizationRequestMapper: requestMapper + ) + 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 static func createCustomerTokensService( + httpConnector: HttpConnector, threeDSService: ThreeDSService, logger: POLogger + ) -> POCustomerTokensService { + let repository = HttpCustomerTokensRepository(connector: httpConnector) + return DefaultCustomerTokensService(repository: repository, threeDSService: threeDSService, logger: logger) + } + + private func createAlternativePaymentsService() -> POAlternativePaymentMethodsService { + let serviceConfiguration = { @Sendable [unowned self] () -> AlternativePaymentMethodsServiceConfiguration in + let configuration = self.configuration + return .init(projectId: configuration.projectId, baseUrl: configuration.checkoutBaseUrl) + } + return DefaultAlternativePaymentMethodsService(configuration: serviceConfiguration, logger: logger) + } + + 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) } - 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.apiBaseUrl, @@ -163,8 +176,7 @@ public final class ProcessOut { ) } let logger = createLogger( - for: Constants.connectorLoggerCategory, - includeRemoteDestination: includeLoggerRemoteDestination + for: Constants.connectorLoggerCategory, additionalDestinations: remoteLoggerDestination ) let connector = ProcessOutHttpConnectorBuilder() .with(configuration: connectorConfiguration) @@ -174,18 +186,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 @@ -194,13 +235,13 @@ 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. @@ -208,30 +249,36 @@ extension ProcessOut { /// - 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") + MainActor.preconditionIsolated("Shared instance must be configured 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 + private static let _shared = POUnfairlyLocked(wrappedValue: nil) // MARK: - Private Methods + @MainActor private static func prewarm() { FontFamily.registerAllCustomFonts() PODefaultPhoneNumberMetadataProvider.shared.prewarm() } } + +// 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 index 75cd81cf5..c19ebb228 100644 --- a/Sources/ProcessOut/Sources/Api/Utils/Test3DS/POTest3DSService.swift +++ b/Sources/ProcessOut/Sources/Api/Utils/Test3DS/POTest3DSService.swift @@ -19,13 +19,14 @@ public final class POTest3DSService: PO3DSService { } /// View controller to use for presentations. + @MainActor public unowned var viewController: UIViewController! // swiftlint:disable:this implicitly_unwrapped_optional // MARK: - PO3DSService public func authenticationRequest( configuration: PO3DS2Configuration, - completion: @escaping (Result) -> Void + completion: @escaping @Sendable (Result) -> Void ) { let request = PO3DS2AuthenticationRequest( deviceData: "", @@ -37,32 +38,36 @@ public final class POTest3DSService: PO3DSService { 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)) + public func handle(challenge: PO3DS2Challenge, completion: @escaping @Sendable (Result) -> Void) { + MainActor.assumeIsolated { + 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) } - 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) + public func handle(redirect: PO3DSRedirect, completion: @escaping @Sendable (Result) -> Void) { + MainActor.assumeIsolated { + 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) + .build() + self.viewController.present(viewController, animated: true) + } } // MARK: - Private Properties 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 1921031c9..481d435c8 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/POCancellable.swift b/Sources/ProcessOut/Sources/Core/Cancellable/POCancellable.swift index b5debb144..a6bb0ceef 100644 --- a/Sources/ProcessOut/Sources/Core/Cancellable/POCancellable.swift +++ b/Sources/ProcessOut/Sources/Core/Cancellable/POCancellable.swift @@ -9,7 +9,7 @@ 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/POFallbackValueProvider.swift b/Sources/ProcessOut/Sources/Core/CodingUtils/POFallbackDecodable/POFallbackValueProvider.swift index e9cff7683..b83a87c2c 100644 --- a/Sources/ProcessOut/Sources/Core/CodingUtils/POFallbackDecodable/POFallbackValueProvider.swift +++ b/Sources/ProcessOut/Sources/Core/CodingUtils/POFallbackDecodable/POFallbackValueProvider.swift @@ -8,7 +8,7 @@ import Foundation /// Contract for providing a default value of a Type. -public protocol POFallbackValueProvider { +public protocol POFallbackValueProvider: Sendable { associatedtype Value diff --git a/Sources/ProcessOut/Sources/Core/CodingUtils/POImmutableExcludedCodable.swift b/Sources/ProcessOut/Sources/Core/CodingUtils/POImmutableExcludedCodable.swift index a8ce028e2..755370a1f 100644 --- a/Sources/ProcessOut/Sources/Core/CodingUtils/POImmutableExcludedCodable.swift +++ b/Sources/ProcessOut/Sources/Core/CodingUtils/POImmutableExcludedCodable.swift @@ -30,3 +30,5 @@ extension KeyedEncodingContainer { _ value: POImmutableExcludedCodable, forKey key: KeyedEncodingContainer.Key ) throws { /* Ignored */ } } + +extension POImmutableExcludedCodable: Sendable where Value: Sendable { } diff --git a/Sources/ProcessOut/Sources/Core/CodingUtils/POImmutableStringCodableDecimal.swift b/Sources/ProcessOut/Sources/Core/CodingUtils/POImmutableStringCodableDecimal.swift index 70ad98ea8..5b6b1abf5 100644 --- a/Sources/ProcessOut/Sources/Core/CodingUtils/POImmutableStringCodableDecimal.swift +++ b/Sources/ProcessOut/Sources/Core/CodingUtils/POImmutableStringCodableDecimal.swift @@ -12,7 +12,7 @@ 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 POImmutableStringCodableDecimal: Codable, Sendable { public let wrappedValue: Decimal diff --git a/Sources/ProcessOut/Sources/Core/CodingUtils/POImmutableStringCodableOptionalDecimal.swift b/Sources/ProcessOut/Sources/Core/CodingUtils/POImmutableStringCodableOptionalDecimal.swift index d7364eff8..a5b710029 100644 --- a/Sources/ProcessOut/Sources/Core/CodingUtils/POImmutableStringCodableOptionalDecimal.swift +++ b/Sources/ProcessOut/Sources/Core/CodingUtils/POImmutableStringCodableOptionalDecimal.swift @@ -12,7 +12,7 @@ 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 POImmutableStringCodableOptionalDecimal: Codable, Sendable { public let wrappedValue: Decimal? 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/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/DeviceMetadata.swift b/Sources/ProcessOut/Sources/Core/DeviceMetadata/DeviceMetadata.swift index 80da5cd1b..58fe897d0 100644 --- a/Sources/ProcessOut/Sources/Core/DeviceMetadata/DeviceMetadata.swift +++ b/Sources/ProcessOut/Sources/Core/DeviceMetadata/DeviceMetadata.swift @@ -7,7 +7,7 @@ import Foundation -struct DeviceMetadata: Encodable { +struct DeviceMetadata: Encodable, Sendable { /// Current device identifier. @POImmutableExcludedCodable 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/POEventEmitterEvent.swift b/Sources/ProcessOut/Sources/Core/EventEmitter/POEventEmitterEvent.swift index 97ea50358..1f96154df 100644 --- a/Sources/ProcessOut/Sources/Core/EventEmitter/POEventEmitterEvent.swift +++ b/Sources/ProcessOut/Sources/Core/EventEmitter/POEventEmitterEvent.swift @@ -5,7 +5,8 @@ // Created by Andrii Vysotskyi on 10.05.2023. // -@_spi(PO) public protocol POEventEmitterEvent: Sendable { +@_spi(PO) +public protocol POEventEmitterEvent: Sendable { /// Event name. static var name: String { get } 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 97% rename from Sources/ProcessOut/Sources/Core/Utils/UIImage+Dynamic.swift rename to Sources/ProcessOut/Sources/Core/Extensions/UIImage+Dynamic.swift index 1bcc88a67..ca5969e16 100644 --- a/Sources/ProcessOut/Sources/Core/Utils/UIImage+Dynamic.swift +++ b/Sources/ProcessOut/Sources/Core/Extensions/UIImage+Dynamic.swift @@ -10,6 +10,7 @@ import UIKit extension UIImage { static func dynamic(lightImage: UIImage?, darkImage: UIImage?) -> UIImage? { + assert(Thread.isMainThread) // When image with scale greater than 3 is registed asset created explicitly produced image // is malformed and doesn't contain images for light nor dark styles. guard let image = lightImage ?? darkImage else { diff --git a/Sources/ProcessOut/Sources/Core/Formatters/PhoneNumber/MetadataProvider/PODefaultPhoneNumberMetadataProvider.swift b/Sources/ProcessOut/Sources/Core/Formatters/PhoneNumber/MetadataProvider/PODefaultPhoneNumberMetadataProvider.swift index 7be0c4446..20f1c4a73 100644 --- a/Sources/ProcessOut/Sources/Core/Formatters/PhoneNumber/MetadataProvider/PODefaultPhoneNumberMetadataProvider.swift +++ b/Sources/ProcessOut/Sources/Core/Formatters/PhoneNumber/MetadataProvider/PODefaultPhoneNumberMetadataProvider.swift @@ -7,7 +7,8 @@ import Foundation -@_spi(PO) public final class PODefaultPhoneNumberMetadataProvider: POPhoneNumberMetadataProvider { +@_spi(PO) +public final class PODefaultPhoneNumberMetadataProvider: POPhoneNumberMetadataProvider { public static let shared = PODefaultPhoneNumberMetadataProvider() @@ -20,19 +21,17 @@ import Foundation public func metadata(for countryCode: String) -> POPhoneNumberMetadata? { 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: POPhoneNumberMetadata]?>(wrappedValue: nil) // MARK: - Private Methods @@ -42,7 +41,7 @@ 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] @@ -57,7 +56,7 @@ import Foundation 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/ProcessOut/Sources/Core/Formatters/PhoneNumber/POPhoneNumberFormat.swift b/Sources/ProcessOut/Sources/Core/Formatters/PhoneNumber/MetadataProvider/POPhoneNumberFormat.swift similarity index 81% rename from Sources/ProcessOut/Sources/Core/Formatters/PhoneNumber/POPhoneNumberFormat.swift rename to Sources/ProcessOut/Sources/Core/Formatters/PhoneNumber/MetadataProvider/POPhoneNumberFormat.swift index bcd41d63f..2958aeea1 100644 --- a/Sources/ProcessOut/Sources/Core/Formatters/PhoneNumber/POPhoneNumberFormat.swift +++ b/Sources/ProcessOut/Sources/Core/Formatters/PhoneNumber/MetadataProvider/POPhoneNumberFormat.swift @@ -5,7 +5,8 @@ // Created by Andrii Vysotskyi on 16.03.2023. // -@_spi(PO) public struct POPhoneNumberFormat: Decodable { +@_spi(PO) +public struct POPhoneNumberFormat: Decodable, Sendable { /// Formatting patern. public let pattern: String diff --git a/Sources/ProcessOut/Sources/Core/Formatters/PhoneNumber/POPhoneNumberMetadata.swift b/Sources/ProcessOut/Sources/Core/Formatters/PhoneNumber/MetadataProvider/POPhoneNumberMetadata.swift similarity index 77% rename from Sources/ProcessOut/Sources/Core/Formatters/PhoneNumber/POPhoneNumberMetadata.swift rename to Sources/ProcessOut/Sources/Core/Formatters/PhoneNumber/MetadataProvider/POPhoneNumberMetadata.swift index be1e30fae..bb4fdb35f 100644 --- a/Sources/ProcessOut/Sources/Core/Formatters/PhoneNumber/POPhoneNumberMetadata.swift +++ b/Sources/ProcessOut/Sources/Core/Formatters/PhoneNumber/MetadataProvider/POPhoneNumberMetadata.swift @@ -5,7 +5,8 @@ // Created by Andrii Vysotskyi on 16.03.2023. // -@_spi(PO) public struct POPhoneNumberMetadata: Decodable { +@_spi(PO) +public struct POPhoneNumberMetadata: Decodable, Sendable { /// Country code. public let countryCode: String diff --git a/Sources/ProcessOut/Sources/Core/Formatters/PhoneNumber/MetadataProvider/POPhoneNumberMetadataProvider.swift b/Sources/ProcessOut/Sources/Core/Formatters/PhoneNumber/MetadataProvider/POPhoneNumberMetadataProvider.swift index 761f0f2a8..9d8e25963 100644 --- a/Sources/ProcessOut/Sources/Core/Formatters/PhoneNumber/MetadataProvider/POPhoneNumberMetadataProvider.swift +++ b/Sources/ProcessOut/Sources/Core/Formatters/PhoneNumber/MetadataProvider/POPhoneNumberMetadataProvider.swift @@ -5,7 +5,8 @@ // Created by Andrii Vysotskyi on 23.03.2023. // -@_spi(PO) public protocol POPhoneNumberMetadataProvider { +@_spi(PO) +public protocol POPhoneNumberMetadataProvider: Sendable { /// Returns metadata for given country code if any. func metadata(for countryCode: String) -> POPhoneNumberMetadata? diff --git a/Sources/ProcessOut/Sources/Core/Formatters/PhoneNumber/POPhoneNumberFormatter.swift b/Sources/ProcessOut/Sources/Core/Formatters/PhoneNumber/POPhoneNumberFormatter.swift index e0d4022c3..11017c8b7 100644 --- a/Sources/ProcessOut/Sources/Core/Formatters/PhoneNumber/POPhoneNumberFormatter.swift +++ b/Sources/ProcessOut/Sources/Core/Formatters/PhoneNumber/POPhoneNumberFormatter.swift @@ -7,7 +7,8 @@ import Foundation -@_spi(PO) public final class POPhoneNumberFormatter: Formatter { +@_spi(PO) +public final class POPhoneNumberFormatter: Formatter { public init(metadataProvider: POPhoneNumberMetadataProvider = PODefaultPhoneNumberMetadataProvider.shared) { regexProvider = RegexProvider.shared 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..e48772e63 100644 --- a/Sources/ProcessOut/Sources/Core/Logger/Destinations/SystemLoggerDestination.swift +++ b/Sources/ProcessOut/Sources/Core/Logger/Destinations/SystemLoggerDestination.swift @@ -31,7 +31,7 @@ final class SystemLoggerDestination: LoggerDestination { private let subsystem: String private let lock: NSLock - private var logs: [String: OSLog] + private nonisolated(unsafe) var logs: [String: OSLog] // MARK: - Private Methods 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..f01fde5eb 100644 --- a/Sources/ProcessOut/Sources/Core/Logger/POLogger.swift +++ b/Sources/ProcessOut/Sources/Core/Logger/POLogger.swift @@ -7,11 +7,11 @@ 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 @@ -83,7 +83,7 @@ public struct POLogger { // MARK: - Private Properties private let destinations: [LoggerDestination] - private let minimumLevel: () -> LogLevel + private let minimumLevel: @Sendable () -> LogLevel private let lock: NSLock private var attributes: [POLogAttributeKey: String] diff --git a/Sources/ProcessOut/Sources/Core/Markdown/MarkdownParser.swift b/Sources/ProcessOut/Sources/Core/Markdown/MarkdownParser.swift index cfa258319..cf3fe646b 100644 --- a/Sources/ProcessOut/Sources/Core/Markdown/MarkdownParser.swift +++ b/Sources/ProcessOut/Sources/Core/Markdown/MarkdownParser.swift @@ -17,7 +17,9 @@ 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 } /// Escapes given plain text so it can be represented as is, in markdown. diff --git a/Sources/ProcessOut/Sources/Core/Markdown/Nodes/MarkdownBlockQuote.swift b/Sources/ProcessOut/Sources/Core/Markdown/Nodes/MarkdownBlockQuote.swift index 12a8d2e3e..45212855c 100644 --- a/Sources/ProcessOut/Sources/Core/Markdown/Nodes/MarkdownBlockQuote.swift +++ b/Sources/ProcessOut/Sources/Core/Markdown/Nodes/MarkdownBlockQuote.swift @@ -7,7 +7,7 @@ @_implementationOnly import cmark -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/ProcessOut/Sources/Core/Markdown/Nodes/MarkdownCodeBlock.swift b/Sources/ProcessOut/Sources/Core/Markdown/Nodes/MarkdownCodeBlock.swift index 5af49fb24..653d97b27 100644 --- a/Sources/ProcessOut/Sources/Core/Markdown/Nodes/MarkdownCodeBlock.swift +++ b/Sources/ProcessOut/Sources/Core/Markdown/Nodes/MarkdownCodeBlock.swift @@ -7,26 +7,22 @@ @_implementationOnly import cmark -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? = { - guard let info = cmarkNode.pointee.as.code.info else { - return nil - } - return String(cString: info) - }() - - 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/ProcessOut/Sources/Core/Markdown/Nodes/MarkdownCodeSpan.swift b/Sources/ProcessOut/Sources/Core/Markdown/Nodes/MarkdownCodeSpan.swift index 3ba6fccb2..0c1dc39d7 100644 --- a/Sources/ProcessOut/Sources/Core/Markdown/Nodes/MarkdownCodeSpan.swift +++ b/Sources/ProcessOut/Sources/Core/Markdown/Nodes/MarkdownCodeSpan.swift @@ -7,18 +7,23 @@ @_implementationOnly import cmark -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/ProcessOut/Sources/Core/Markdown/Nodes/MarkdownDocument.swift b/Sources/ProcessOut/Sources/Core/Markdown/Nodes/MarkdownDocument.swift index d38f165e8..7e650a6e6 100644 --- a/Sources/ProcessOut/Sources/Core/Markdown/Nodes/MarkdownDocument.swift +++ b/Sources/ProcessOut/Sources/Core/Markdown/Nodes/MarkdownDocument.swift @@ -7,13 +7,7 @@ @_implementationOnly import cmark -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/ProcessOut/Sources/Core/Markdown/Nodes/MarkdownEmphasis.swift b/Sources/ProcessOut/Sources/Core/Markdown/Nodes/MarkdownEmphasis.swift index 07335342d..ccfc84b3c 100644 --- a/Sources/ProcessOut/Sources/Core/Markdown/Nodes/MarkdownEmphasis.swift +++ b/Sources/ProcessOut/Sources/Core/Markdown/Nodes/MarkdownEmphasis.swift @@ -7,9 +7,7 @@ @_implementationOnly import cmark -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/ProcessOut/Sources/Core/Markdown/Nodes/MarkdownHeading.swift b/Sources/ProcessOut/Sources/Core/Markdown/Nodes/MarkdownHeading.swift index c64837edb..07f1c41c1 100644 --- a/Sources/ProcessOut/Sources/Core/Markdown/Nodes/MarkdownHeading.swift +++ b/Sources/ProcessOut/Sources/Core/Markdown/Nodes/MarkdownHeading.swift @@ -7,14 +7,17 @@ @_implementationOnly import cmark -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/ProcessOut/Sources/Core/Markdown/Nodes/MarkdownLinebreak.swift b/Sources/ProcessOut/Sources/Core/Markdown/Nodes/MarkdownLinebreak.swift index fd73994f6..f0388e79a 100644 --- a/Sources/ProcessOut/Sources/Core/Markdown/Nodes/MarkdownLinebreak.swift +++ b/Sources/ProcessOut/Sources/Core/Markdown/Nodes/MarkdownLinebreak.swift @@ -7,7 +7,7 @@ @_implementationOnly import cmark -final class MarkdownLinebreak: MarkdownBaseNode { +final class MarkdownLinebreak: MarkdownBaseNode, @unchecked Sendable { override static var cmarkNodeType: cmark_node_type { CMARK_NODE_LINEBREAK diff --git a/Sources/ProcessOut/Sources/Core/Markdown/Nodes/MarkdownLink.swift b/Sources/ProcessOut/Sources/Core/Markdown/Nodes/MarkdownLink.swift index d1d1cd130..bf3778b22 100644 --- a/Sources/ProcessOut/Sources/Core/Markdown/Nodes/MarkdownLink.swift +++ b/Sources/ProcessOut/Sources/Core/Markdown/Nodes/MarkdownLink.swift @@ -7,17 +7,21 @@ @_implementationOnly import cmark -final class MarkdownLink: MarkdownBaseNode { +final class MarkdownLink: MarkdownBaseNode, @unchecked Sendable { - private(set) lazy var url: String? = { - if let url = cmarkNode.pointee.as.link.url { - return String(cString: url) - } - return nil - }() + let url: String? // MARK: - MarkdownBaseNode + required init(cmarkNode: MarkdownBaseNode.CmarkNode, validatesType: Bool = true) { + if let url = cmarkNode.pointee.as.link.url { + self.url = String(cString: url) + } else { + url = nil + } + super.init(cmarkNode: cmarkNode, validatesType: validatesType) + } + override static var cmarkNodeType: cmark_node_type { CMARK_NODE_LINK } diff --git a/Sources/ProcessOut/Sources/Core/Markdown/Nodes/MarkdownList.swift b/Sources/ProcessOut/Sources/Core/Markdown/Nodes/MarkdownList.swift index e5d3fb46c..44d73e36a 100644 --- a/Sources/ProcessOut/Sources/Core/Markdown/Nodes/MarkdownList.swift +++ b/Sources/ProcessOut/Sources/Core/Markdown/Nodes/MarkdownList.swift @@ -7,7 +7,7 @@ @_implementationOnly import cmark -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 UInt32(listNode.list_type) { case CMARK_BULLET_LIST.rawValue: @@ -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/ProcessOut/Sources/Core/Markdown/Nodes/MarkdownListItem.swift b/Sources/ProcessOut/Sources/Core/Markdown/Nodes/MarkdownListItem.swift index 808cc624b..d45e4fe16 100644 --- a/Sources/ProcessOut/Sources/Core/Markdown/Nodes/MarkdownListItem.swift +++ b/Sources/ProcessOut/Sources/Core/Markdown/Nodes/MarkdownListItem.swift @@ -7,9 +7,7 @@ @_implementationOnly import cmark -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/ProcessOut/Sources/Core/Markdown/Nodes/MarkdownNode.swift b/Sources/ProcessOut/Sources/Core/Markdown/Nodes/MarkdownNode.swift index b2c87df5c..da1890c5e 100644 --- a/Sources/ProcessOut/Sources/Core/Markdown/Nodes/MarkdownNode.swift +++ b/Sources/ProcessOut/Sources/Core/Markdown/Nodes/MarkdownNode.swift @@ -7,7 +7,7 @@ @_implementationOnly import cmark -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/ProcessOut/Sources/Core/Markdown/Nodes/MarkdownParagraph.swift b/Sources/ProcessOut/Sources/Core/Markdown/Nodes/MarkdownParagraph.swift index 7e5b38a22..1dc9f4c65 100644 --- a/Sources/ProcessOut/Sources/Core/Markdown/Nodes/MarkdownParagraph.swift +++ b/Sources/ProcessOut/Sources/Core/Markdown/Nodes/MarkdownParagraph.swift @@ -7,9 +7,7 @@ @_implementationOnly import cmark -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/ProcessOut/Sources/Core/Markdown/Nodes/MarkdownSoftbreak.swift b/Sources/ProcessOut/Sources/Core/Markdown/Nodes/MarkdownSoftbreak.swift index c35e3a438..c049db6e9 100644 --- a/Sources/ProcessOut/Sources/Core/Markdown/Nodes/MarkdownSoftbreak.swift +++ b/Sources/ProcessOut/Sources/Core/Markdown/Nodes/MarkdownSoftbreak.swift @@ -7,7 +7,7 @@ @_implementationOnly import cmark -final class MarkdownSoftbreak: MarkdownBaseNode { +final class MarkdownSoftbreak: MarkdownBaseNode, @unchecked Sendable { override static var cmarkNodeType: cmark_node_type { CMARK_NODE_SOFTBREAK diff --git a/Sources/ProcessOut/Sources/Core/Markdown/Nodes/MarkdownStrong.swift b/Sources/ProcessOut/Sources/Core/Markdown/Nodes/MarkdownStrong.swift index 089af879f..e9f960a85 100644 --- a/Sources/ProcessOut/Sources/Core/Markdown/Nodes/MarkdownStrong.swift +++ b/Sources/ProcessOut/Sources/Core/Markdown/Nodes/MarkdownStrong.swift @@ -7,9 +7,7 @@ @_implementationOnly import cmark -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/ProcessOut/Sources/Core/Markdown/Nodes/MarkdownText.swift b/Sources/ProcessOut/Sources/Core/Markdown/Nodes/MarkdownText.swift index 958504d6c..574981d74 100644 --- a/Sources/ProcessOut/Sources/Core/Markdown/Nodes/MarkdownText.swift +++ b/Sources/ProcessOut/Sources/Core/Markdown/Nodes/MarkdownText.swift @@ -7,18 +7,23 @@ @_implementationOnly import cmark -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/ProcessOut/Sources/Core/Markdown/Nodes/MarkdownThematicBreak.swift b/Sources/ProcessOut/Sources/Core/Markdown/Nodes/MarkdownThematicBreak.swift index 1d02e0715..f802f9b01 100644 --- a/Sources/ProcessOut/Sources/Core/Markdown/Nodes/MarkdownThematicBreak.swift +++ b/Sources/ProcessOut/Sources/Core/Markdown/Nodes/MarkdownThematicBreak.swift @@ -7,7 +7,7 @@ @_implementationOnly import cmark -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/ProcessOut/Sources/Core/Markdown/Nodes/MarkdownUnknown.swift b/Sources/ProcessOut/Sources/Core/Markdown/Nodes/MarkdownUnknown.swift index 13398216f..654488bbc 100644 --- a/Sources/ProcessOut/Sources/Core/Markdown/Nodes/MarkdownUnknown.swift +++ b/Sources/ProcessOut/Sources/Core/Markdown/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/ProcessOut/Sources/Core/Markdown/Visitor/MarkdownDebugDescriptionPrinter.swift b/Sources/ProcessOut/Sources/Core/Markdown/Visitor/MarkdownDebugDescriptionPrinter.swift index e5aea8978..a2b1797ac 100644 --- a/Sources/ProcessOut/Sources/Core/Markdown/Visitor/MarkdownDebugDescriptionPrinter.swift +++ b/Sources/ProcessOut/Sources/Core/Markdown/Visitor/MarkdownDebugDescriptionPrinter.swift @@ -5,7 +5,7 @@ // Created by Andrii Vysotskyi on 14.06.2023. // -import Foundation +#if DEBUG final class MarkdownDebugDescriptionPrinter: MarkdownVisitor { @@ -71,11 +71,7 @@ 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 { @@ -144,3 +140,5 @@ extension MarkdownBaseNode: CustomDebugStringConvertible { return self.accept(visitor: visitor) } } + +#endif diff --git a/Sources/ProcessOut/Sources/Core/PropertyWrappers/ImmutableNullHashable.swift b/Sources/ProcessOut/Sources/Core/PropertyWrappers/ImmutableNullHashable.swift index b89550179..4d5c76ce7 100644 --- a/Sources/ProcessOut/Sources/Core/PropertyWrappers/ImmutableNullHashable.swift +++ b/Sources/ProcessOut/Sources/Core/PropertyWrappers/ImmutableNullHashable.swift @@ -18,3 +18,5 @@ struct ImmutableNullHashable: Hashable { // Ignored } } + +extension ImmutableNullHashable: Sendable where Value: Sendable { } diff --git a/Sources/ProcessOut/Sources/Core/RegexProvider/RegexProvider.swift b/Sources/ProcessOut/Sources/Core/RegexProvider/RegexProvider.swift index a889270fa..e6196dc3a 100644 --- a/Sources/ProcessOut/Sources/Core/RegexProvider/RegexProvider.swift +++ b/Sources/ProcessOut/Sources/Core/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/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..73521219e 100644 --- a/Sources/ProcessOut/Sources/Core/Utils/AsyncUtils.swift +++ b/Sources/ProcessOut/Sources/Core/Utils/AsyncUtils.swift @@ -15,11 +15,11 @@ func withTimeout( error timeoutError: @autoclosure () -> Error, perform operation: @escaping @Sendable () 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() @@ -49,7 +49,7 @@ func withTimeout( func retry( operation: @escaping @Sendable () async throws -> T, - while condition: @escaping (Result) -> Bool, + while condition: @escaping @Sendable (Result) -> Bool, timeout: TimeInterval, timeoutError: @autoclosure () -> Error, retryStrategy: RetryStrategy? = nil @@ -69,7 +69,7 @@ func retry( private func retry( operation: @escaping @Sendable () async throws -> T, after result: Result, - while condition: @escaping (Result) -> Bool, + while condition: @escaping @Sendable (Result) -> Bool, retryStrategy: RetryStrategy?, attempt: Int ) async throws -> T { diff --git a/Sources/ProcessOut/Sources/Core/Utils/Batcher.swift b/Sources/ProcessOut/Sources/Core/Utils/Batcher.swift index 2d435250b..7899ad5fd 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 (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/POStringResource.swift b/Sources/ProcessOut/Sources/Core/Utils/POStringResource.swift index c3fe28e16..7bbdf7370 100644 --- a/Sources/ProcessOut/Sources/Core/Utils/POStringResource.swift +++ b/Sources/ProcessOut/Sources/Core/Utils/POStringResource.swift @@ -7,7 +7,7 @@ import Foundation -@_spi(PO) public struct POStringResource { +@_spi(PO) public struct POStringResource: Sendable { /// The key to use to look up a localized string. let key: String diff --git a/Sources/ProcessOut/Sources/Core/Utils/POTypedRepresentation.swift b/Sources/ProcessOut/Sources/Core/Utils/POTypedRepresentation.swift index d19ade046..47d57d842 100644 --- a/Sources/ProcessOut/Sources/Core/Utils/POTypedRepresentation.swift +++ b/Sources/ProcessOut/Sources/Core/Utils/POTypedRepresentation.swift @@ -102,3 +102,5 @@ extension KeyedDecodingContainer { try type.init(from: try superDecoder(forKey: key)) } } + +extension POTypedRepresentation: Sendable where Wrapped: Sendable { } diff --git a/Sources/ProcessOut/Sources/Core/Utils/UnfairlyLocked/POUnfairlyLocked.swift b/Sources/ProcessOut/Sources/Core/Utils/UnfairlyLocked/POUnfairlyLocked.swift index 2fa43725c..266ed092e 100644 --- a/Sources/ProcessOut/Sources/Core/Utils/UnfairlyLocked/POUnfairlyLocked.swift +++ b/Sources/ProcessOut/Sources/Core/Utils/UnfairlyLocked/POUnfairlyLocked.swift @@ -8,8 +8,9 @@ import os /// A thread-safe wrapper around a value. +@_spi(PO) @propertyWrapper -@_spi(PO) public final class POUnfairlyLocked: @unchecked Sendable { +public final class POUnfairlyLocked: @unchecked Sendable { public init(wrappedValue: Value) { value = wrappedValue diff --git a/Sources/ProcessOut/Sources/Core/Utils/UnfairlyLocked/UnfairLock.swift b/Sources/ProcessOut/Sources/Core/Utils/UnfairlyLocked/UnfairLock.swift index 5a12c1c42..e9e0c59cc 100644 --- a/Sources/ProcessOut/Sources/Core/Utils/UnfairlyLocked/UnfairLock.swift +++ b/Sources/ProcessOut/Sources/Core/Utils/UnfairlyLocked/UnfairLock.swift @@ -8,7 +8,7 @@ import os /// An `os_unfair_lock` wrapper. -final class UnfairLock { +final class UnfairLock: Sendable { init() { unfairLock = .allocate(capacity: 1) @@ -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/Sourcery+Generated.swift b/Sources/ProcessOut/Sources/Generated/Sourcery+Generated.swift index c29e1741b..293024b92 100644 --- a/Sources/ProcessOut/Sources/Generated/Sourcery+Generated.swift +++ b/Sources/ProcessOut/Sources/Generated/Sourcery+Generated.swift @@ -82,7 +82,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) @@ -96,7 +96,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) @@ -107,7 +107,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) @@ -118,7 +118,7 @@ extension POCardsService { @discardableResult public func tokenize( request: POApplePayCardTokenizationRequest, - completion: @escaping (Result) -> Void + completion: @escaping @Sendable (Result) -> Void ) -> POCancellable { invoke(completion: completion) { try await tokenize(request: request) @@ -133,7 +133,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) @@ -145,7 +145,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) @@ -159,7 +159,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) @@ -170,7 +170,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) @@ -180,7 +180,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() @@ -195,7 +195,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) @@ -206,7 +206,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) @@ -218,7 +218,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) @@ -231,7 +231,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) @@ -243,7 +243,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) @@ -257,7 +257,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) @@ -271,7 +271,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) @@ -282,7 +282,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) @@ -294,7 +294,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) @@ -305,7 +305,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) @@ -317,7 +317,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) @@ -326,9 +326,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 { @@ -344,7 +344,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/CardPaymentWebView.swift b/Sources/ProcessOut/Sources/Legacy/CardPaymentWebView.swift index 5ebf309a6..eef951d33 100644 --- a/Sources/ProcessOut/Sources/Legacy/CardPaymentWebView.swift +++ b/Sources/ProcessOut/Sources/Legacy/CardPaymentWebView.swift @@ -8,6 +8,7 @@ import Foundation @available(*, deprecated) +@preconcurrency final class CardPaymentWebView: ProcessOutWebView { override func onRedirect(url: URL) { diff --git a/Sources/ProcessOut/Sources/Legacy/ProcessOutRequestManager.swift b/Sources/ProcessOut/Sources/Legacy/ProcessOutRequestManager.swift index 6026771fd..f340a710d 100644 --- a/Sources/ProcessOut/Sources/Legacy/ProcessOutRequestManager.swift +++ b/Sources/ProcessOut/Sources/Legacy/ProcessOutRequestManager.swift @@ -8,6 +8,7 @@ import Foundation @available(*, deprecated) +@preconcurrency final class ProcessOutRequestManager { let apiUrl: String diff --git a/Sources/ProcessOut/Sources/Legacy/ProcessOutWebView.swift b/Sources/ProcessOut/Sources/Legacy/ProcessOutWebView.swift index d1abef5e6..4bbd2eca8 100644 --- a/Sources/ProcessOut/Sources/Legacy/ProcessOutWebView.swift +++ b/Sources/ProcessOut/Sources/Legacy/ProcessOutWebView.swift @@ -9,6 +9,7 @@ import Foundation import WebKit @available(*, deprecated, message: "Use PO3DSRedirectViewControllerBuilder or POAlternativePaymentMethodViewControllerBuilder instead.") +@preconcurrency public class ProcessOutWebView: WKWebView, WKNavigationDelegate, WKUIDelegate { private let REDIRECT_URL_PATTERN = "https:\\/\\/checkout\\.processout\\.(ninja|com)\\/helpers\\/mobile-processout-webview-landing.*" diff --git a/Sources/ProcessOut/Sources/Repositories/Cards/HttpCardsRepository.swift b/Sources/ProcessOut/Sources/Repositories/Cards/HttpCardsRepository.swift index 5e3ffe0f4..a822a2ffb 100644 --- a/Sources/ProcessOut/Sources/Repositories/Cards/HttpCardsRepository.swift +++ b/Sources/ProcessOut/Sources/Repositories/Cards/HttpCardsRepository.swift @@ -16,7 +16,7 @@ final class HttpCardsRepository: CardsRepository { // MARK: - CardsRepository func issuerInformation(iin: String) async throws -> POCardIssuerInformation { - struct Response: Decodable { + struct Response: Decodable, Sendable { let cardInformation: POCardIssuerInformation } let httpRequest = HttpConnectorRequest.get(path: "/iins/" + iin) 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..3263a0cae 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 diff --git a/Sources/ProcessOut/Sources/Repositories/Cards/Requests/POCardUpdateRequest.swift b/Sources/ProcessOut/Sources/Repositories/Cards/Requests/POCardUpdateRequest.swift index 8d9df5ce4..8c0653694 100644 --- a/Sources/ProcessOut/Sources/Repositories/Cards/Requests/POCardUpdateRequest.swift +++ b/Sources/ProcessOut/Sources/Repositories/Cards/Requests/POCardUpdateRequest.swift @@ -6,7 +6,7 @@ // /// Updated card details. -public struct POCardUpdateRequest: Encodable { +public struct POCardUpdateRequest: Encodable, Sendable { /// Card id. @POImmutableExcludedCodable 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 index 163f9463f..d54e9f857 100644 --- a/Sources/ProcessOut/Sources/Repositories/Cards/Responses/CardTokenizationResponse.swift +++ b/Sources/ProcessOut/Sources/Repositories/Cards/Responses/CardTokenizationResponse.swift @@ -7,6 +7,6 @@ import Foundation -struct CardTokenizationResponse: Decodable { +struct CardTokenizationResponse: Decodable, Sendable { 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..322762bd1 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, @unchecked Sendable { /// Value that uniquely identifies the card. public let id: String diff --git a/Sources/ProcessOut/Sources/Repositories/Cards/Responses/POCardIssuerInformation.swift b/Sources/ProcessOut/Sources/Repositories/Cards/Responses/POCardIssuerInformation.swift index 21aa03dfa..89b06d80d 100644 --- a/Sources/ProcessOut/Sources/Repositories/Cards/Responses/POCardIssuerInformation.swift +++ b/Sources/ProcessOut/Sources/Repositories/Cards/Responses/POCardIssuerInformation.swift @@ -6,7 +6,7 @@ // /// 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 diff --git a/Sources/ProcessOut/Sources/Repositories/CustomerTokens/Requests/POAssignCustomerTokenRequest.swift b/Sources/ProcessOut/Sources/Repositories/CustomerTokens/Requests/POAssignCustomerTokenRequest.swift index a59f2abe6..24b924516 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 diff --git a/Sources/ProcessOut/Sources/Repositories/CustomerTokens/Requests/POCreateCustomerTokenRequest.swift b/Sources/ProcessOut/Sources/Repositories/CustomerTokens/Requests/POCreateCustomerTokenRequest.swift index 6e55ab7a8..db3a39304 100644 --- a/Sources/ProcessOut/Sources/Repositories/CustomerTokens/Requests/POCreateCustomerTokenRequest.swift +++ b/Sources/ProcessOut/Sources/Repositories/CustomerTokens/Requests/POCreateCustomerTokenRequest.swift @@ -8,7 +8,7 @@ import Foundation @_spi(PO) -public struct POCreateCustomerTokenRequest: Encodable { +public struct POCreateCustomerTokenRequest: Encodable, Sendable { /// Customer id to associate created token with. @POImmutableExcludedCodable 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/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..3105905b3 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 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/Requests/AlternativePayment/NativeAlternativePaymentCaptureRequest.swift b/Sources/ProcessOut/Sources/Repositories/Invoices/Requests/AlternativePayment/NativeAlternativePaymentCaptureRequest.swift index 557752683..6f487da18 100644 --- a/Sources/ProcessOut/Sources/Repositories/Invoices/Requests/AlternativePayment/NativeAlternativePaymentCaptureRequest.swift +++ b/Sources/ProcessOut/Sources/Repositories/Invoices/Requests/AlternativePayment/NativeAlternativePaymentCaptureRequest.swift @@ -5,7 +5,7 @@ // Created by Andrii Vysotskyi on 16.12.2022. // -struct NativeAlternativePaymentCaptureRequest: Encodable { +struct NativeAlternativePaymentCaptureRequest: Encodable, Sendable { /// Invoice identifier. @POImmutableExcludedCodable 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 07c2de63d..6f452ac4b 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 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..aa12839cc 100644 --- a/Sources/ProcessOut/Sources/Repositories/Invoices/Responses/AlternativePayment/PONativeAlternativePaymentMethodResponse.swift +++ b/Sources/ProcessOut/Sources/Repositories/Invoices/Responses/AlternativePayment/PONativeAlternativePaymentMethodResponse.swift @@ -7,12 +7,12 @@ import Foundation -public struct PONativeAlternativePaymentMethodResponse: Decodable { +public struct PONativeAlternativePaymentMethodResponse: Decodable, Sendable { @available(*, deprecated, message: "Use PONativeAlternativePaymentMethodParameterValues directly.") public typealias NativeAlternativePaymentMethodParameterValues = PONativeAlternativePaymentMethodParameterValues - public struct NativeApm: Decodable { + public struct NativeApm: Decodable, Sendable { /// Payment's state. public let state: PONativeAlternativePaymentMethodState 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..9c07a70ae 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,7 +27,7 @@ public struct PONativeAlternativePaymentMethodTransactionDetails: Decodable { } /// Invoice details. - public struct Invoice: Decodable { + public struct Invoice: Decodable, Sendable { /// Invoice amount. @POImmutableStringCodableDecimal 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 89% rename from Sources/ProcessOut/Sources/Repositories/Invoices/Responses/PODynamicCheckoutPaymentMethod.swift rename to Sources/ProcessOut/Sources/Repositories/Invoices/Responses/DynamicCheckout/PODynamicCheckoutPaymentMethod.swift index e9f20fb43..2702aaabb 100644 --- a/Sources/ProcessOut/Sources/Repositories/Invoices/Responses/PODynamicCheckoutPaymentMethod.swift +++ b/Sources/ProcessOut/Sources/Repositories/Invoices/Responses/DynamicCheckout/PODynamicCheckoutPaymentMethod.swift @@ -11,11 +11,11 @@ import PassKit /// Dynamic checkout payment method description. @_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) @@ -30,7 +30,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 @@ -48,7 +48,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) @@ -63,7 +63,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 @@ -71,7 +71,7 @@ public enum PODynamicCheckoutPaymentMethod { // MARK: - APM - public struct AlternativePayment: Decodable { // sourcery: AutoCodingKeys + public struct AlternativePayment: Decodable, Sendable { // sourcery: AutoCodingKeys /// Payment method ID. @_spi(PO) @@ -89,7 +89,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 @@ -100,7 +100,7 @@ public enum PODynamicCheckoutPaymentMethod { // MARK: - Card - public struct Card: Decodable { // sourcery: AutoCodingKeys + public struct Card: Decodable, Sendable { // sourcery: AutoCodingKeys /// Payment method ID. @_spi(PO) @@ -113,7 +113,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 allowSchemeSelection: Bool @@ -128,7 +128,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? @@ -139,7 +139,7 @@ public enum PODynamicCheckoutPaymentMethod { // MARK: - Customer Tokens - public struct CustomerToken { + public struct CustomerToken: Sendable { /// Payment method ID. @_spi(PO) @@ -157,7 +157,7 @@ public enum PODynamicCheckoutPaymentMethod { public let configuration: CustomerTokenConfiguration } - public struct CustomerTokenConfiguration: Decodable { + public struct CustomerTokenConfiguration: Decodable, Sendable { /// Customer token ID. public let customerTokenId: String @@ -168,7 +168,7 @@ public enum PODynamicCheckoutPaymentMethod { // MARK: - Unknown - public struct Unknown { + public struct Unknown: Sendable { /// Transient ID assigned to method during decoding. @_spi(PO) @@ -180,7 +180,7 @@ public enum PODynamicCheckoutPaymentMethod { // MARK: - Common - public struct Display: Decodable { + public struct Display: Decodable, @unchecked Sendable { /// Display name. public let name: String @@ -192,7 +192,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 95% rename from Sources/ProcessOut/Sources/Repositories/Invoices/Responses/POStringDecodableMerchantCapability.swift rename to Sources/ProcessOut/Sources/Repositories/Invoices/Responses/DynamicCheckout/POStringDecodableMerchantCapability.swift index c7c23a735..b7d1a9cfd 100644 --- a/Sources/ProcessOut/Sources/Repositories/Invoices/Responses/POStringDecodableMerchantCapability.swift +++ b/Sources/ProcessOut/Sources/Repositories/Invoices/Responses/DynamicCheckout/POStringDecodableMerchantCapability.swift @@ -9,7 +9,7 @@ import PassKit /// Property wrapper allowing to decode `PKMerchantCapability`. @propertyWrapper -public struct POStringDecodableMerchantCapability: Decodable { +public struct POStringDecodableMerchantCapability: Decodable, Sendable { public let wrappedValue: PKMerchantCapability diff --git a/Sources/ProcessOut/Sources/Repositories/Invoices/Responses/POInvoice.swift b/Sources/ProcessOut/Sources/Repositories/Invoices/Responses/POInvoice.swift index a6624c59b..bd7af52d1 100644 --- a/Sources/ProcessOut/Sources/Repositories/Invoices/Responses/POInvoice.swift +++ b/Sources/ProcessOut/Sources/Repositories/Invoices/Responses/POInvoice.swift @@ -8,7 +8,7 @@ import Foundation /// Invoice details. -public struct POInvoice: Decodable { +public struct POInvoice: Decodable, Sendable { /// String value that uniquely identifies this invoice. public let id: 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..a1b8efa46 100644 --- a/Sources/ProcessOut/Sources/Repositories/Shared/PORepository.swift +++ b/Sources/ProcessOut/Sources/Repositories/Shared/PORepository.swift @@ -9,7 +9,7 @@ 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/Models/PO3DS2AuthenticationRequest.swift b/Sources/ProcessOut/Sources/Services/3DS/Models/PO3DS2AuthenticationRequest.swift index 6095d33ca..f42d1fa36 100644 --- a/Sources/ProcessOut/Sources/Services/3DS/Models/PO3DS2AuthenticationRequest.swift +++ b/Sources/ProcessOut/Sources/Services/3DS/Models/PO3DS2AuthenticationRequest.swift @@ -6,7 +6,7 @@ // /// Holds transaction data that the 3DS Server requires to create the AReq. -public struct PO3DS2AuthenticationRequest: Hashable { +public struct PO3DS2AuthenticationRequest: 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/PO3DS2Challenge.swift index b44d18597..2764a221e 100644 --- a/Sources/ProcessOut/Sources/Services/3DS/Models/PO3DS2Challenge.swift +++ b/Sources/ProcessOut/Sources/Services/3DS/Models/PO3DS2Challenge.swift @@ -7,7 +7,7 @@ /// 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 PO3DS2Challenge: Decodable, Hashable, Sendable { /// Unique transaction identifier assigned by the ACS. public let acsTransactionId: String diff --git a/Sources/ProcessOut/Sources/Services/3DS/Models/PO3DS2Configuration.swift b/Sources/ProcessOut/Sources/Services/3DS/Models/PO3DS2Configuration.swift index dfc08200f..0c6d7384b 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 diff --git a/Sources/ProcessOut/Sources/Services/3DS/Models/PO3DS2ConfigurationCardScheme.swift b/Sources/ProcessOut/Sources/Services/3DS/Models/PO3DS2ConfigurationCardScheme.swift index 48dec718e..a2792de03 100644 --- a/Sources/ProcessOut/Sources/Services/3DS/Models/PO3DS2ConfigurationCardScheme.swift +++ b/Sources/ProcessOut/Sources/Services/3DS/Models/PO3DS2ConfigurationCardScheme.swift @@ -8,7 +8,7 @@ // todo(andrii-vysotskyi): remove when updating to 5.0.0 /// Available card schemes. -public enum PO3DS2ConfigurationCardScheme: RawRepresentable, Decodable, Hashable { +public enum PO3DS2ConfigurationCardScheme: RawRepresentable, Decodable, Hashable, Sendable { /// Known card schemes. case visa, mastercard, europay, carteBancaire, jcb, diners, discover, unionpay, americanExpress diff --git a/Sources/ProcessOut/Sources/Services/3DS/Models/PO3DSRedirect.swift b/Sources/ProcessOut/Sources/Services/3DS/Models/PO3DSRedirect.swift index 7b50949ee..8dc9ef9bf 100644 --- a/Sources/ProcessOut/Sources/Services/3DS/Models/PO3DSRedirect.swift +++ b/Sources/ProcessOut/Sources/Services/3DS/Models/PO3DSRedirect.swift @@ -8,7 +8,7 @@ import Foundation /// Holds information about 3DS redirect. -public struct PO3DSRedirect: Hashable { +public struct PO3DSRedirect: Hashable, Sendable { /// Redirect url. public let url: URL diff --git a/Sources/ProcessOut/Sources/Services/3DS/PO3DSService.swift b/Sources/ProcessOut/Sources/Services/3DS/PO3DSService.swift index a18a6745f..527ad59f6 100644 --- a/Sources/ProcessOut/Sources/Services/3DS/PO3DSService.swift +++ b/Sources/ProcessOut/Sources/Services/3DS/PO3DSService.swift @@ -9,23 +9,23 @@ public typealias PO3DSServiceType = PO3DSService /// This interface provides methods to process 3-D Secure transactions. -public protocol PO3DSService: AnyObject { +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, - completion: @escaping (Result) -> Void + completion: @escaping @Sendable (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) + func handle(challenge: PO3DS2Challenge, completion: @escaping @Sendable (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) + func handle(redirect: PO3DSRedirect, completion: @escaping @Sendable (Result) -> Void) } @MainActor @@ -34,7 +34,9 @@ extension PO3DSService { /// 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) + authenticationRequest(configuration: configuration) { result in + continuation.resume(with: result) + } } } @@ -43,7 +45,9 @@ extension PO3DSService { /// with failure indicating what went wrong. func handle(challenge: PO3DS2Challenge) async throws -> Bool { try await withUnsafeThrowingContinuation { continuation in - handle(challenge: challenge, completion: continuation.resume) + handle(challenge: challenge) { result in + continuation.resume(with: result) + } } } @@ -52,7 +56,9 @@ extension PO3DSService { /// ``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) + handle(redirect: redirect) { result in + continuation.resume(with: result) + } } } } 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 index 3db28b5c4..c0f4dd64d 100644 --- a/Sources/ProcessOut/Sources/Services/AlternativePaymentMethods/DefaultAlternativePaymentMethodsService.swift +++ b/Sources/ProcessOut/Sources/Services/AlternativePaymentMethods/DefaultAlternativePaymentMethodsService.swift @@ -9,7 +9,7 @@ import Foundation final class DefaultAlternativePaymentMethodsService: POAlternativePaymentMethodsService { - init(configuration: @escaping () -> AlternativePaymentMethodsServiceConfiguration, logger: POLogger) { + init(configuration: @escaping @Sendable () -> AlternativePaymentMethodsServiceConfiguration, logger: POLogger) { self.configuration = configuration self.logger = logger } @@ -63,7 +63,7 @@ final class DefaultAlternativePaymentMethodsService: POAlternativePaymentMethods // MARK: - Private - private let configuration: () -> AlternativePaymentMethodsServiceConfiguration + private let configuration: @Sendable () -> AlternativePaymentMethodsServiceConfiguration private let logger: POLogger // MARK: - Private Methods diff --git a/Sources/ProcessOut/Sources/Services/AlternativePaymentMethods/POAlternativePaymentMethodsService.swift b/Sources/ProcessOut/Sources/Services/AlternativePaymentMethods/POAlternativePaymentMethodsService.swift index ddf6babfe..fc95f9c03 100644 --- a/Sources/ProcessOut/Sources/Services/AlternativePaymentMethods/POAlternativePaymentMethodsService.swift +++ b/Sources/ProcessOut/Sources/Services/AlternativePaymentMethods/POAlternativePaymentMethodsService.swift @@ -11,7 +11,7 @@ import Foundation public typealias POAlternativePaymentMethodsServiceType = POAlternativePaymentMethodsService /// Service that provides set of methods to work with alternative payments. -public protocol POAlternativePaymentMethodsService { +public protocol POAlternativePaymentMethodsService: POService { /// Creates the redirection URL for APM Payments and APM token creation. /// diff --git a/Sources/ProcessOut/Sources/Services/AlternativePaymentMethods/Requests/POAlternativePaymentMethodRequest.swift b/Sources/ProcessOut/Sources/Services/AlternativePaymentMethods/Requests/POAlternativePaymentMethodRequest.swift index 37603bd15..20087ddfd 100644 --- a/Sources/ProcessOut/Sources/Services/AlternativePaymentMethods/Requests/POAlternativePaymentMethodRequest.swift +++ b/Sources/ProcessOut/Sources/Services/AlternativePaymentMethods/Requests/POAlternativePaymentMethodRequest.swift @@ -14,7 +14,7 @@ import Foundation /// /// - NOTE: Make sure to supply proper `additionalData` specific for particular payment /// method. -public struct POAlternativePaymentMethodRequest { +public struct POAlternativePaymentMethodRequest: Sendable { /// Invoice identifier to to perform APM payment for. public let invoiceId: String diff --git a/Sources/ProcessOut/Sources/Services/AlternativePaymentMethods/Responses/POAlternativePaymentMethodResponse.swift b/Sources/ProcessOut/Sources/Services/AlternativePaymentMethods/Responses/POAlternativePaymentMethodResponse.swift index 3c63690a7..b849ad69e 100644 --- a/Sources/ProcessOut/Sources/Services/AlternativePaymentMethods/Responses/POAlternativePaymentMethodResponse.swift +++ b/Sources/ProcessOut/Sources/Services/AlternativePaymentMethods/Responses/POAlternativePaymentMethodResponse.swift @@ -8,9 +8,9 @@ import Foundation /// Result of alternative payment. -public struct POAlternativePaymentMethodResponse { +public struct POAlternativePaymentMethodResponse: Sendable { - public enum APMReturnType { + public enum APMReturnType: Sendable { case authorization, createToken } diff --git a/Sources/ProcessOut/Sources/Services/Cards/DefaultCardsService.swift b/Sources/ProcessOut/Sources/Services/Cards/DefaultCardsService.swift index 1abbec15e..9e791a664 100644 --- a/Sources/ProcessOut/Sources/Services/Cards/DefaultCardsService.swift +++ b/Sources/ProcessOut/Sources/Services/Cards/DefaultCardsService.swift @@ -32,7 +32,7 @@ final class DefaultCardsService: POCardsService { } func tokenize(request: POApplePayCardTokenizationRequest) 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) } diff --git a/Sources/ProcessOut/Sources/Services/Cards/Mappers/ApplePayCardTokenizationRequest/ApplePayCardTokenizationRequestMapper.swift b/Sources/ProcessOut/Sources/Services/Cards/Mappers/ApplePayCardTokenizationRequest/ApplePayCardTokenizationRequestMapper.swift index 6313fe4cb..38fb2eff1 100644 --- a/Sources/ProcessOut/Sources/Services/Cards/Mappers/ApplePayCardTokenizationRequest/ApplePayCardTokenizationRequestMapper.swift +++ b/Sources/ProcessOut/Sources/Services/Cards/Mappers/ApplePayCardTokenizationRequest/ApplePayCardTokenizationRequestMapper.swift @@ -5,8 +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: POApplePayCardTokenizationRequest) throws -> ApplePayCardTokenizationRequest + func tokenizationRequest( + from request: POApplePayCardTokenizationRequest + ) 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 130ffbbbc..414557bca 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: POApplePayCardTokenizationRequest ) 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/Requests/POApplePayCardTokenizationRequest.swift b/Sources/ProcessOut/Sources/Services/Cards/Requests/POApplePayCardTokenizationRequest.swift index fb3a0a44e..78b9538f4 100644 --- a/Sources/ProcessOut/Sources/Services/Cards/Requests/POApplePayCardTokenizationRequest.swift +++ b/Sources/ProcessOut/Sources/Services/Cards/Requests/POApplePayCardTokenizationRequest.swift @@ -9,6 +9,7 @@ import Foundation import PassKit /// Apple pay card details. +@MainActor public struct POApplePayCardTokenizationRequest { /// Payment information. 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..ffdd68cc2 100644 --- a/Sources/ProcessOut/Sources/Services/Shared/POService.swift +++ b/Sources/ProcessOut/Sources/Services/Shared/POService.swift @@ -6,7 +6,7 @@ // /// Common protocol that all services conform to. -public protocol POService { +public protocol POService: Sendable { /// Service's failure type. typealias Failure = POFailure 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/UI/Modules/3DSRedirect/PO3DSRedirectViewControllerBuilder.swift b/Sources/ProcessOut/Sources/UI/Modules/3DSRedirect/PO3DSRedirectViewControllerBuilder.swift index d356642d8..70c8724a5 100644 --- a/Sources/ProcessOut/Sources/UI/Modules/3DSRedirect/PO3DSRedirectViewControllerBuilder.swift +++ b/Sources/ProcessOut/Sources/UI/Modules/3DSRedirect/PO3DSRedirectViewControllerBuilder.swift @@ -10,6 +10,7 @@ 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 +@MainActor public final class PO3DSRedirectViewControllerBuilder { /// Creates builder instance with given redirect information. diff --git a/Sources/ProcessOut/Sources/UI/Modules/AlternativePaymentMethod/POAlternativePaymentMethodViewControllerBuilder.swift b/Sources/ProcessOut/Sources/UI/Modules/AlternativePaymentMethod/POAlternativePaymentMethodViewControllerBuilder.swift index 40e568dda..6da19432e 100644 --- a/Sources/ProcessOut/Sources/UI/Modules/AlternativePaymentMethod/POAlternativePaymentMethodViewControllerBuilder.swift +++ b/Sources/ProcessOut/Sources/UI/Modules/AlternativePaymentMethod/POAlternativePaymentMethodViewControllerBuilder.swift @@ -11,6 +11,7 @@ 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 +@MainActor public final class POAlternativePaymentMethodViewControllerBuilder { /// Creates builder instance with given request. diff --git a/Sources/ProcessOut/Sources/UI/Modules/NativeAlternativePaymentMethod/Builder/PONativeAlternativePaymentMethodViewControllerBuilder.swift b/Sources/ProcessOut/Sources/UI/Modules/NativeAlternativePaymentMethod/Builder/PONativeAlternativePaymentMethodViewControllerBuilder.swift index f637b7c84..c5782bb65 100644 --- a/Sources/ProcessOut/Sources/UI/Modules/NativeAlternativePaymentMethod/Builder/PONativeAlternativePaymentMethodViewControllerBuilder.swift +++ b/Sources/ProcessOut/Sources/UI/Modules/NativeAlternativePaymentMethod/Builder/PONativeAlternativePaymentMethodViewControllerBuilder.swift @@ -11,6 +11,7 @@ import UIKit /// Alternative Payment. Call ``PONativeAlternativePaymentMethodViewControllerBuilder/build()`` /// to create view controller's instance. @available(*, deprecated, message: "Use ProcessOutUI.PONativeAlternativePaymentViewController instead.") +@MainActor public final class PONativeAlternativePaymentMethodViewControllerBuilder { // swiftlint:disable:this type_name @available(*, deprecated, message: "Use non static method instead.") @@ -73,13 +74,12 @@ public final class PONativeAlternativePaymentMethodViewControllerBuilder { // sw 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 + var logger: POLogger = ProcessOut.shared.logger logger[attributeKey: .invoiceId] = invoiceId logger[attributeKey: .gatewayConfigurationId] = gatewayConfigurationId let interactor = PODefaultNativeAlternativePaymentMethodInteractor( - invoicesService: api.invoices, - imagesRepository: api.images, + invoicesService: ProcessOut.shared.invoices, + imagesRepository: ProcessOut.shared.images, configuration: .init( gatewayConfigurationId: gatewayConfigurationId, invoiceId: invoiceId, diff --git a/Sources/ProcessOut/Sources/UI/Modules/NativeAlternativePaymentMethod/Interactor/PODefaultNativeAlternativePaymentMethodInteractor.swift b/Sources/ProcessOut/Sources/UI/Modules/NativeAlternativePaymentMethod/Interactor/PODefaultNativeAlternativePaymentMethodInteractor.swift index 670dbbdb1..c68f93f27 100644 --- a/Sources/ProcessOut/Sources/UI/Modules/NativeAlternativePaymentMethod/Interactor/PODefaultNativeAlternativePaymentMethodInteractor.swift +++ b/Sources/ProcessOut/Sources/UI/Modules/NativeAlternativePaymentMethod/Interactor/PODefaultNativeAlternativePaymentMethodInteractor.swift @@ -53,13 +53,17 @@ import UIKit 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) + MainActor.assumeIsolated { + switch result { + case let .success(details): + self?.defaultValues(for: details.parameters) { values in + MainActor.assumeIsolated { + self?.setStartedStateUnchecked(details: details, defaultValues: values) + } + } + case .failure(let failure): + self?.setFailureStateUnchecked(failure: failure) } - case .failure(let failure): - self?.setFailureStateUnchecked(failure: failure) } } } @@ -113,11 +117,13 @@ import UIKit ) 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) + MainActor.assumeIsolated { + 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 { @@ -204,7 +210,9 @@ import UIKit switch response.nativeApm.state { case .customerInput: defaultValues(for: response.nativeApm.parameterDefinitions) { [weak self] values in - self?.restoreStartedStateAfterSubmission(nativeApm: response.nativeApm, defaultValues: values) + MainActor.assumeIsolated { + self?.restoreStartedStateAfterSubmission(nativeApm: response.nativeApm, defaultValues: values) + } } case .pendingCapture: send(event: .didSubmitParameters(additionalParametersExpected: false)) @@ -236,35 +244,39 @@ import UIKit 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) - } + MainActor.assumeIsolated { + guard let self else { + return } - ) - 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() + 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 + MainActor.assumeIsolated { + 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() + } } } @@ -303,11 +315,13 @@ import UIKit 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) + MainActor.assumeIsolated { + let capturedState = State.Captured( + paymentProviderName: parameterValues?.providerName, logoImage: logoImage + ) + self?.state = .captured(capturedState) + self?.send(event: .didCompletePayment) + } } } } @@ -402,7 +416,7 @@ import UIKit private func defaultValues( for parameters: [PONativeAlternativePaymentMethodParameter]?, - completion: @escaping ([String: State.ParameterValue]) -> Void + completion: @escaping @Sendable ([String: State.ParameterValue]) -> Void ) { guard let parameters, !parameters.isEmpty else { completion([:]) @@ -410,27 +424,28 @@ import UIKit } 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 + MainActor.assumeIsolated { + 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) } - } else { - defaultValue = self.defaultValue(for: parameter) + defaultValues[parameter.key] = .init(value: defaultValue, recentErrorMessage: nil) } - defaultValues[parameter.key] = .init(value: defaultValue, recentErrorMessage: nil) + completion(defaultValues) } - completion(defaultValues) } } else { var defaultValues: [String: State.ParameterValue] = [:] diff --git a/Sources/ProcessOut/Sources/UI/Modules/NativeAlternativePaymentMethod/Interactor/PONativeAlternativePaymentMethodDelegate.swift b/Sources/ProcessOut/Sources/UI/Modules/NativeAlternativePaymentMethod/Interactor/PONativeAlternativePaymentMethodDelegate.swift index 328476082..20996f46f 100644 --- a/Sources/ProcessOut/Sources/UI/Modules/NativeAlternativePaymentMethod/Interactor/PONativeAlternativePaymentMethodDelegate.swift +++ b/Sources/ProcessOut/Sources/UI/Modules/NativeAlternativePaymentMethod/Interactor/PONativeAlternativePaymentMethodDelegate.swift @@ -6,16 +6,19 @@ // /// Native alternative payment module delegate definition. -public protocol PONativeAlternativePaymentMethodDelegate: AnyObject { +public protocol PONativeAlternativePaymentMethodDelegate: AnyObject, Sendable { /// Invoked when module emits event. + @MainActor 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. + @MainActor func nativeAlternativePaymentMethodDefaultValues( - for parameters: [PONativeAlternativePaymentMethodParameter], completion: @escaping ([String: String]) -> Void + for parameters: [PONativeAlternativePaymentMethodParameter], + completion: @escaping @Sendable ([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 index 67e1a6df7..21e45e569 100644 --- a/Sources/ProcessOut/Sources/UI/Modules/NativeAlternativePaymentMethod/Interactor/PONativeAlternativePaymentMethodInteractor.swift +++ b/Sources/ProcessOut/Sources/UI/Modules/NativeAlternativePaymentMethod/Interactor/PONativeAlternativePaymentMethodInteractor.swift @@ -8,7 +8,9 @@ import Foundation // todo(andrii-vysotskyi): migrate interactor and dependencies to UI module when ready -@_spi(PO) public protocol PONativeAlternativePaymentMethodInteractor: AnyObject { +@_spi(PO) +@MainActor +public protocol PONativeAlternativePaymentMethodInteractor: AnyObject { typealias State = PONativeAlternativePaymentMethodInteractorState diff --git a/Sources/ProcessOut/Sources/UI/Modules/NativeAlternativePaymentMethod/Interactor/PONativeAlternativePaymentMethodInteractorState.swift b/Sources/ProcessOut/Sources/UI/Modules/NativeAlternativePaymentMethod/Interactor/PONativeAlternativePaymentMethodInteractorState.swift index 22b2e3374..979ab5a6b 100644 --- a/Sources/ProcessOut/Sources/UI/Modules/NativeAlternativePaymentMethod/Interactor/PONativeAlternativePaymentMethodInteractorState.swift +++ b/Sources/ProcessOut/Sources/UI/Modules/NativeAlternativePaymentMethod/Interactor/PONativeAlternativePaymentMethodInteractorState.swift @@ -7,9 +7,10 @@ import UIKit -@_spi(PO) public enum PONativeAlternativePaymentMethodInteractorState { +@_spi(PO) +public enum PONativeAlternativePaymentMethodInteractorState { - public struct ParameterValue { + public struct ParameterValue: Sendable { /// Actual parameter value. public let value: String? @@ -91,3 +92,6 @@ import UIKit /// Payment is completed. case captured(Captured) } + +@available(*, unavailable) +extension PONativeAlternativePaymentMethodInteractorState: Sendable { } diff --git a/Sources/ProcessOut/Sources/UI/Modules/NativeAlternativePaymentMethod/Models/Style/PONativeAlternativePaymentMethodBackgroundStyle.swift b/Sources/ProcessOut/Sources/UI/Modules/NativeAlternativePaymentMethod/Models/Style/PONativeAlternativePaymentMethodBackgroundStyle.swift index 47456ea82..68864a89b 100644 --- a/Sources/ProcessOut/Sources/UI/Modules/NativeAlternativePaymentMethod/Models/Style/PONativeAlternativePaymentMethodBackgroundStyle.swift +++ b/Sources/ProcessOut/Sources/UI/Modules/NativeAlternativePaymentMethod/Models/Style/PONativeAlternativePaymentMethodBackgroundStyle.swift @@ -9,6 +9,7 @@ import UIKit /// Native alternative payment method screen background style. @available(*, deprecated, message: "Use ProcessOutUI.PONativeAlternativePaymentBackgroundStyle instead.") +@MainActor public struct PONativeAlternativePaymentMethodBackgroundStyle { /// Regular background color. diff --git a/Sources/ProcessOut/Sources/UI/Modules/NativeAlternativePaymentMethod/Models/Style/PONativeAlternativePaymentMethodStyle.swift b/Sources/ProcessOut/Sources/UI/Modules/NativeAlternativePaymentMethod/Models/Style/PONativeAlternativePaymentMethodStyle.swift index 4f227505d..3da47284c 100644 --- a/Sources/ProcessOut/Sources/UI/Modules/NativeAlternativePaymentMethod/Models/Style/PONativeAlternativePaymentMethodStyle.swift +++ b/Sources/ProcessOut/Sources/UI/Modules/NativeAlternativePaymentMethod/Models/Style/PONativeAlternativePaymentMethodStyle.swift @@ -9,6 +9,7 @@ import UIKit /// Defines style for native alternative payment method module. @available(*, deprecated, message: "Use ProcessOutUI.PONativeAlternativePaymentStyle instead.") +@MainActor public struct PONativeAlternativePaymentMethodStyle { /// Title style. @@ -81,6 +82,7 @@ public struct PONativeAlternativePaymentMethodStyle { // MARK: - Private Nested Types + @MainActor private enum Constants { static let title = POTextStyle(color: UIColor(resource: .Text.primary), typography: .Medium.title) static let sectionTitle = POTextStyle( diff --git a/Sources/ProcessOut/Sources/UI/Modules/NativeAlternativePaymentMethod/View/Cells/NativeAlternativePaymentMethodCell.swift b/Sources/ProcessOut/Sources/UI/Modules/NativeAlternativePaymentMethod/View/Cells/NativeAlternativePaymentMethodCell.swift index a43130564..366e26466 100644 --- a/Sources/ProcessOut/Sources/UI/Modules/NativeAlternativePaymentMethod/View/Cells/NativeAlternativePaymentMethodCell.swift +++ b/Sources/ProcessOut/Sources/UI/Modules/NativeAlternativePaymentMethod/View/Cells/NativeAlternativePaymentMethodCell.swift @@ -8,6 +8,7 @@ import UIKit @available(*, deprecated) +@MainActor protocol NativeAlternativePaymentMethodCell: UICollectionViewCell { /// Tells the cell that it is about to be displayed. @@ -24,6 +25,7 @@ protocol NativeAlternativePaymentMethodCell: UICollectionViewCell { } @available(*, deprecated) +@MainActor protocol NativeAlternativePaymentMethodCellDelegate: AnyObject { /// Should return boolean value indicating whether cells input should return ie resign first responder. diff --git a/Sources/ProcessOut/Sources/UI/Modules/NativeAlternativePaymentMethod/ViewModel/DefaultNativeAlternativePaymentMethodViewModel.swift b/Sources/ProcessOut/Sources/UI/Modules/NativeAlternativePaymentMethod/ViewModel/DefaultNativeAlternativePaymentMethodViewModel.swift index c12085f82..9d5be1b7a 100644 --- a/Sources/ProcessOut/Sources/UI/Modules/NativeAlternativePaymentMethod/ViewModel/DefaultNativeAlternativePaymentMethodViewModel.swift +++ b/Sources/ProcessOut/Sources/UI/Modules/NativeAlternativePaymentMethod/ViewModel/DefaultNativeAlternativePaymentMethodViewModel.swift @@ -209,7 +209,9 @@ final class DefaultNativeAlternativePaymentMethodViewModel: withTimeInterval: Constants.captureSuccessCompletionDelay, repeats: false, block: { [weak self] _ in - self?.completion?(.success(())) + MainActor.assumeIsolated { + self?.completion?(.success(())) + } } ) let submittedItem = State.SubmittedItem( @@ -389,8 +391,10 @@ final class DefaultNativeAlternativePaymentMethodViewModel: } self[keyPath: isDisabled] = true let timer = Timer.scheduledTimer(withTimeInterval: interval, repeats: false) { [weak self] _ in - self?[keyPath: isDisabled] = false - self?.configureWithInteractorState() + MainActor.assumeIsolated { + self?[keyPath: isDisabled] = false + self?.configureWithInteractorState() + } } cancelActionTimers[timerKey] = timer } diff --git a/Sources/ProcessOut/Sources/UI/Modules/NativeAlternativePaymentMethod/ViewModel/NativeAlternativePaymentMethodViewModel.swift b/Sources/ProcessOut/Sources/UI/Modules/NativeAlternativePaymentMethod/ViewModel/NativeAlternativePaymentMethodViewModel.swift index 9491c39e4..e9b502a51 100644 --- a/Sources/ProcessOut/Sources/UI/Modules/NativeAlternativePaymentMethod/ViewModel/NativeAlternativePaymentMethodViewModel.swift +++ b/Sources/ProcessOut/Sources/UI/Modules/NativeAlternativePaymentMethod/ViewModel/NativeAlternativePaymentMethodViewModel.swift @@ -6,6 +6,7 @@ // @available(*, deprecated) +@MainActor protocol NativeAlternativePaymentMethodViewModel: ViewModel { /// Submits parameter values. diff --git a/Sources/ProcessOut/Sources/UI/Modules/Safari/DefaultSafariViewModel.swift b/Sources/ProcessOut/Sources/UI/Modules/Safari/DefaultSafariViewModel.swift index 52071eb4a..c6d1279d2 100644 --- a/Sources/ProcessOut/Sources/UI/Modules/Safari/DefaultSafariViewModel.swift +++ b/Sources/ProcessOut/Sources/UI/Modules/Safari/DefaultSafariViewModel.swift @@ -9,7 +9,8 @@ import Foundation import SafariServices @available(*, deprecated) -final class DefaultSafariViewModel: NSObject, SFSafariViewControllerDelegate { +@MainActor +final class DefaultSafariViewModel: NSObject, @preconcurrency SFSafariViewControllerDelegate { init( configuration: DefaultSafariViewModelConfiguration, @@ -30,7 +31,9 @@ final class DefaultSafariViewModel: NSObject, SFSafariViewControllerDelegate { } if let timeout = configuration.timeout { timeoutTimer = Timer.scheduledTimer(withTimeInterval: timeout, repeats: false) { [weak self] _ in - self?.setCompletedState(with: POFailure(code: .timeout(.mobile))) + MainActor.assumeIsolated { + self?.setCompletedState(with: POFailure(code: .timeout(.mobile))) + } } } deepLinkObserver = eventEmitter.on(PODeepLinkReceivedEvent.self) { [weak self] event in diff --git a/Sources/ProcessOut/Sources/UI/Modules/Safari/SafariViewController+Extensions.swift b/Sources/ProcessOut/Sources/UI/Modules/Safari/SafariViewController+Extensions.swift index d31338c31..b13f287bb 100644 --- a/Sources/ProcessOut/Sources/UI/Modules/Safari/SafariViewController+Extensions.swift +++ b/Sources/ProcessOut/Sources/UI/Modules/Safari/SafariViewController+Extensions.swift @@ -16,6 +16,7 @@ extension SFSafariViewController { // MARK: - Private Nested Types + @MainActor private enum Keys { static var viewModel: UInt8 = 0 } diff --git a/Sources/ProcessOut/Sources/UI/Shared/Architecture/View/BaseViewController.swift b/Sources/ProcessOut/Sources/UI/Shared/Architecture/View/BaseViewController.swift index cade1a550..3fdf1cb8e 100644 --- a/Sources/ProcessOut/Sources/UI/Shared/Architecture/View/BaseViewController.swift +++ b/Sources/ProcessOut/Sources/UI/Shared/Architecture/View/BaseViewController.swift @@ -66,7 +66,9 @@ class BaseViewController: UIViewController where Model: ViewModel { // 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) + RunLoop.current.perform { + MainActor.assumeIsolated(self.viewModelDidChange) + } return } // View is configured without animation if it is not yet part of the hierarchy to avoid visual issues. @@ -113,7 +115,9 @@ class BaseViewController: UIViewController where Model: ViewModel { // 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) + RunLoop.current.perform { + MainActor.assumeIsolated(animator.startAnimation) + } } } diff --git a/Sources/ProcessOut/Sources/UI/Shared/Architecture/ViewModel/ViewModel.swift b/Sources/ProcessOut/Sources/UI/Shared/Architecture/ViewModel/ViewModel.swift index 629c4e586..ade646c02 100644 --- a/Sources/ProcessOut/Sources/UI/Shared/Architecture/ViewModel/ViewModel.swift +++ b/Sources/ProcessOut/Sources/UI/Shared/Architecture/ViewModel/ViewModel.swift @@ -6,6 +6,7 @@ // @available(*, deprecated) +@MainActor protocol ViewModel: AnyObject { associatedtype State diff --git a/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Layouts/Center/CollectionViewDelegateCenterLayout.swift b/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Layouts/Center/CollectionViewDelegateCenterLayout.swift index 678439d74..332392d7f 100644 --- a/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Layouts/Center/CollectionViewDelegateCenterLayout.swift +++ b/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Layouts/Center/CollectionViewDelegateCenterLayout.swift @@ -7,6 +7,7 @@ import UIKit +@MainActor protocol CollectionViewDelegateCenterLayout: AnyObject, UICollectionViewDelegateFlowLayout { /// Should return index of the section that should be centered. diff --git a/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Styles/Input/POInputStateStyle.swift b/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Styles/Input/POInputStateStyle.swift index 1a3728988..7ebbb0526 100644 --- a/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Styles/Input/POInputStateStyle.swift +++ b/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Styles/Input/POInputStateStyle.swift @@ -8,6 +8,7 @@ import UIKit /// Defines input's styling information in a specific state. +@MainActor public struct POInputStateStyle { /// Text style. diff --git a/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Styles/Input/POInputStyle.swift b/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Styles/Input/POInputStyle.swift index 836e06545..5f13e0576 100644 --- a/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Styles/Input/POInputStyle.swift +++ b/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Styles/Input/POInputStyle.swift @@ -11,6 +11,7 @@ import UIKit public typealias POTextFieldStyle = POInputStyle /// Defines input control style in both normal and error states. +@MainActor public struct POInputStyle { /// Style for normal state. diff --git a/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Styles/POBorderStyle.swift b/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Styles/POBorderStyle.swift index 49c5a00c0..8283ea8dd 100644 --- a/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Styles/POBorderStyle.swift +++ b/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Styles/POBorderStyle.swift @@ -8,6 +8,7 @@ import UIKit /// Style that defines border appearance. Border is always a solid line. +@MainActor public struct POBorderStyle { /// Corner radius. diff --git a/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Styles/POShadowStyle.swift b/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Styles/POShadowStyle.swift index 2bc326bcb..3839d35b7 100644 --- a/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Styles/POShadowStyle.swift +++ b/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Styles/POShadowStyle.swift @@ -8,6 +8,7 @@ import UIKit /// Style that defines shadow appearance. +@MainActor public struct POShadowStyle { /// The color of the shadow. diff --git a/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Typography/POTextStyle.swift b/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Typography/POTextStyle.swift index e37cacad5..48f4b801c 100644 --- a/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Typography/POTextStyle.swift +++ b/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Typography/POTextStyle.swift @@ -8,6 +8,7 @@ import UIKit /// Text style. +@MainActor public struct POTextStyle { /// Text foreground color. diff --git a/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Typography/POTypography.swift b/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Typography/POTypography.swift index cb2ee3189..1726a0ef8 100644 --- a/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Typography/POTypography.swift +++ b/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Typography/POTypography.swift @@ -8,6 +8,7 @@ import UIKit /// Holds typesetting information that could be applied to displayed text. +@MainActor public struct POTypography { /// Font associated with given typography. @@ -50,6 +51,7 @@ public struct POTypography { extension POTypography { + @MainActor enum Fixed { /// Use for captions, status labels and tags. @@ -70,6 +72,7 @@ extension POTypography { static let labelHeading = POTypography(font: FontFamily.WorkSans.medium.font(size: 14), lineHeight: 18) } + @MainActor enum Medium { /// Use for page titles. diff --git a/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Views/ActionsContainer/POActionsContainerStyle.swift b/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Views/ActionsContainer/POActionsContainerStyle.swift index 303b0ff7b..cce9e0dfa 100644 --- a/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Views/ActionsContainer/POActionsContainerStyle.swift +++ b/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Views/ActionsContainer/POActionsContainerStyle.swift @@ -8,6 +8,7 @@ import UIKit /// Actions container style. +@MainActor public struct POActionsContainerStyle { /// Style for primary button. diff --git a/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Views/ActivityIndicator/ActivityIndicatorViewFactory.swift b/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Views/ActivityIndicator/ActivityIndicatorViewFactory.swift index a07913913..9b8f2ef8b 100644 --- a/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Views/ActivityIndicator/ActivityIndicatorViewFactory.swift +++ b/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Views/ActivityIndicator/ActivityIndicatorViewFactory.swift @@ -7,6 +7,7 @@ import UIKit +@MainActor final class ActivityIndicatorViewFactory { func create(style: POActivityIndicatorStyle) -> POActivityIndicatorView { diff --git a/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Views/ActivityIndicator/POActivityIndicatorStyle.swift b/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Views/ActivityIndicator/POActivityIndicatorStyle.swift index 7f38e4f27..9e3bde848 100644 --- a/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Views/ActivityIndicator/POActivityIndicatorStyle.swift +++ b/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Views/ActivityIndicator/POActivityIndicatorStyle.swift @@ -8,6 +8,7 @@ import UIKit /// Possible activity indicator styles. +@MainActor public enum POActivityIndicatorStyle { /// Custom activity indicator. diff --git a/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Views/ActivityIndicator/POActivityIndicatorView.swift b/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Views/ActivityIndicator/POActivityIndicatorView.swift index b42b82c8d..82843c4ba 100644 --- a/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Views/ActivityIndicator/POActivityIndicatorView.swift +++ b/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Views/ActivityIndicator/POActivityIndicatorView.swift @@ -12,6 +12,7 @@ public typealias POActivityIndicatorViewType = POActivityIndicatorView /// Protocol that activity indicator should conform to in order to be used with /// ``POActivityIndicatorStyle`` custom style. +@MainActor public protocol POActivityIndicatorView: UIView { /// Changes animation state. diff --git a/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Views/Button/POButtonStateStyle.swift b/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Views/Button/POButtonStateStyle.swift index d636dd192..c0cbed388 100644 --- a/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Views/Button/POButtonStateStyle.swift +++ b/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Views/Button/POButtonStateStyle.swift @@ -8,6 +8,7 @@ import UIKit /// Defines button's styling information in a specific state. +@MainActor public struct POButtonStateStyle { /// Text typography. diff --git a/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Views/Button/POButtonStyle.swift b/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Views/Button/POButtonStyle.swift index 5cd2bd91c..3270c2294 100644 --- a/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Views/Button/POButtonStyle.swift +++ b/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Views/Button/POButtonStyle.swift @@ -8,6 +8,7 @@ import UIKit /// Defines button style in all possible states. +@MainActor public struct POButtonStyle { /// Style for normal state. diff --git a/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Views/CodeTextField/CodeTextFieldDelegate.swift b/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Views/CodeTextField/CodeTextFieldDelegate.swift index 0d81b96d1..f74c1a177 100644 --- a/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Views/CodeTextField/CodeTextFieldDelegate.swift +++ b/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Views/CodeTextField/CodeTextFieldDelegate.swift @@ -7,6 +7,7 @@ import UIKit +@MainActor protocol CodeTextFieldDelegate: AnyObject { /// Asks the delegate whether to begin editing in the specified text field. diff --git a/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Views/RadioButton/PORadioButtonKnobStateStyle.swift b/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Views/RadioButton/PORadioButtonKnobStateStyle.swift index 197fe377e..64d62f70e 100644 --- a/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Views/RadioButton/PORadioButtonKnobStateStyle.swift +++ b/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Views/RadioButton/PORadioButtonKnobStateStyle.swift @@ -8,6 +8,7 @@ import UIKit /// Describes radio button knob style in a particular state. +@MainActor public struct PORadioButtonKnobStateStyle { /// Background color. diff --git a/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Views/RadioButton/PORadioButtonStateStyle.swift b/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Views/RadioButton/PORadioButtonStateStyle.swift index 0fa7ed2e5..17cf3b762 100644 --- a/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Views/RadioButton/PORadioButtonStateStyle.swift +++ b/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Views/RadioButton/PORadioButtonStateStyle.swift @@ -8,6 +8,7 @@ import UIKit /// Describes radio button style in a particular state, for example when selected. +@MainActor public struct PORadioButtonStateStyle { /// Styling of the radio button knob not including 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 index 753bc08b0..e4caec879 100644 --- a/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Views/RadioButton/PORadioButtonStyle.swift +++ b/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Views/RadioButton/PORadioButtonStyle.swift @@ -8,6 +8,7 @@ import UIKit /// Describes radio button style in different states. +@MainActor public struct PORadioButtonStyle { /// Style to use when radio button is in default state ie enabled and not selected. diff --git a/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Views/TextField/TextFieldContainerView.swift b/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Views/TextField/TextFieldContainerView.swift index 4b791d239..c3cf9877b 100644 --- a/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Views/TextField/TextFieldContainerView.swift +++ b/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Views/TextField/TextFieldContainerView.swift @@ -74,8 +74,10 @@ final class TextFieldContainerView: UIView { ] NSLayoutConstraint.activate(constraints) placeholderObservation = textField.observe(\.placeholder, options: .old) { [weak self] textField, value in - if textField.placeholder != value.oldValue { - self?.configureWithCurrentState(animated: false) + MainActor.assumeIsolated { + if textField.placeholder != value.oldValue { + self?.configureWithCurrentState(animated: false) + } } } } diff --git a/Sources/ProcessOut/Sources/UI/Shared/Extensions/UIImageView+Extensions.swift b/Sources/ProcessOut/Sources/UI/Shared/Extensions/UIImageView+Extensions.swift index 75a6fc048..297363ce2 100644 --- a/Sources/ProcessOut/Sources/UI/Shared/Extensions/UIImageView+Extensions.swift +++ b/Sources/ProcessOut/Sources/UI/Shared/Extensions/UIImageView+Extensions.swift @@ -21,6 +21,7 @@ extension UIImageView { // MARK: - Private Nested Types + @MainActor private enum AssociatedKeys { static var widthConstraint: UInt8 = 0 } diff --git a/Sources/ProcessOut/Sources/UI/Shared/Utils/CollectionReusableViewSizeProvider.swift b/Sources/ProcessOut/Sources/UI/Shared/Utils/CollectionReusableViewSizeProvider.swift index ca5297700..9a0ec4d5e 100644 --- a/Sources/ProcessOut/Sources/UI/Shared/Utils/CollectionReusableViewSizeProvider.swift +++ b/Sources/ProcessOut/Sources/UI/Shared/Utils/CollectionReusableViewSizeProvider.swift @@ -7,6 +7,7 @@ import UIKit +@MainActor final class CollectionReusableViewSizeProvider { init() { diff --git a/Sources/ProcessOut/Sources/UI/Shared/Utils/KeyboardNotification.swift b/Sources/ProcessOut/Sources/UI/Shared/Utils/KeyboardNotification.swift index 52169fd92..f958e3fa2 100644 --- a/Sources/ProcessOut/Sources/UI/Shared/Utils/KeyboardNotification.swift +++ b/Sources/ProcessOut/Sources/UI/Shared/Utils/KeyboardNotification.swift @@ -7,6 +7,7 @@ import UIKit +@MainActor struct KeyboardNotification { /// Keyboard’s frame at the end of its animation. diff --git a/Sources/ProcessOut/Sources/UI/Shared/Utils/Reusable.swift b/Sources/ProcessOut/Sources/UI/Shared/Utils/Reusable.swift index 2e3a91710..c0e6807a8 100644 --- a/Sources/ProcessOut/Sources/UI/Shared/Utils/Reusable.swift +++ b/Sources/ProcessOut/Sources/UI/Shared/Utils/Reusable.swift @@ -5,6 +5,7 @@ // Created by Andrii Vysotskyi on 27.04.2023. // +@MainActor protocol Reusable: AnyObject { /// Reuse identifier. diff --git a/Sources/ProcessOut/Sources/UI/Shared/Utils/TextFieldUtils.swift b/Sources/ProcessOut/Sources/UI/Shared/Utils/TextFieldUtils.swift index 434a18428..3231bff95 100644 --- a/Sources/ProcessOut/Sources/UI/Shared/Utils/TextFieldUtils.swift +++ b/Sources/ProcessOut/Sources/UI/Shared/Utils/TextFieldUtils.swift @@ -8,6 +8,7 @@ import Foundation import UIKit +@MainActor enum TextFieldUtils { static func changeText( 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..16d97b214 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 + final class SubmitAction: Sendable { + + typealias Action = () -> Void // swiftlint:disable:this nesting + + nonisolated init() { + actions = [] + } + + func callAsFunction() { + actions.forEach { $0() } + } + + 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..2da36731f 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, + @_inheritActorContext _ action: @escaping @Sendable () 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, @_inheritActorContext _ action: @escaping @Sendable () 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 5790d5044..9e4df9b82 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 b91204e2a..ae127000b 100644 --- a/Sources/ProcessOutCoreUI/Sources/Core/MarkdownParser/Nodes/MarkdownBlockQuote.swift +++ b/Sources/ProcessOutCoreUI/Sources/Core/MarkdownParser/Nodes/MarkdownBlockQuote.swift @@ -7,7 +7,7 @@ @_implementationOnly import cmark -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 ff5e1476c..adfd07111 100644 --- a/Sources/ProcessOutCoreUI/Sources/Core/MarkdownParser/Nodes/MarkdownCodeBlock.swift +++ b/Sources/ProcessOutCoreUI/Sources/Core/MarkdownParser/Nodes/MarkdownCodeBlock.swift @@ -7,26 +7,22 @@ @_implementationOnly import cmark -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? = { - guard let info = cmarkNode.pointee.as.code.info else { - return nil - } - return String(cString: info) - }() - - 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 0fea4bbf7..c36af6b9a 100644 --- a/Sources/ProcessOutCoreUI/Sources/Core/MarkdownParser/Nodes/MarkdownCodeSpan.swift +++ b/Sources/ProcessOutCoreUI/Sources/Core/MarkdownParser/Nodes/MarkdownCodeSpan.swift @@ -7,18 +7,23 @@ @_implementationOnly import cmark -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 bb4afee92..447a72153 100644 --- a/Sources/ProcessOutCoreUI/Sources/Core/MarkdownParser/Nodes/MarkdownDocument.swift +++ b/Sources/ProcessOutCoreUI/Sources/Core/MarkdownParser/Nodes/MarkdownDocument.swift @@ -7,13 +7,7 @@ @_implementationOnly import cmark -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 9430c10db..9bca8252d 100644 --- a/Sources/ProcessOutCoreUI/Sources/Core/MarkdownParser/Nodes/MarkdownEmphasis.swift +++ b/Sources/ProcessOutCoreUI/Sources/Core/MarkdownParser/Nodes/MarkdownEmphasis.swift @@ -7,9 +7,7 @@ @_implementationOnly import cmark -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 a0f84d70a..764b6f9c3 100644 --- a/Sources/ProcessOutCoreUI/Sources/Core/MarkdownParser/Nodes/MarkdownHeading.swift +++ b/Sources/ProcessOutCoreUI/Sources/Core/MarkdownParser/Nodes/MarkdownHeading.swift @@ -7,14 +7,17 @@ @_implementationOnly import cmark -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 dd1e3d02c..b9b37ca28 100644 --- a/Sources/ProcessOutCoreUI/Sources/Core/MarkdownParser/Nodes/MarkdownLinebreak.swift +++ b/Sources/ProcessOutCoreUI/Sources/Core/MarkdownParser/Nodes/MarkdownLinebreak.swift @@ -7,7 +7,7 @@ @_implementationOnly import cmark -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 7de304ff2..4b6c6f3a6 100644 --- a/Sources/ProcessOutCoreUI/Sources/Core/MarkdownParser/Nodes/MarkdownLink.swift +++ b/Sources/ProcessOutCoreUI/Sources/Core/MarkdownParser/Nodes/MarkdownLink.swift @@ -7,17 +7,21 @@ @_implementationOnly import cmark -final class MarkdownLink: MarkdownBaseNode { +final class MarkdownLink: MarkdownBaseNode, @unchecked Sendable { - private(set) lazy var url: String? = { - if let url = cmarkNode.pointee.as.link.url { - return String(cString: url) - } - return nil - }() + let url: String? // MARK: - MarkdownBaseNode + required init(cmarkNode: MarkdownBaseNode.CmarkNode, validatesType: Bool = true) { + if let url = cmarkNode.pointee.as.link.url { + self.url = String(cString: url) + } else { + url = nil + } + 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 fcd219153..8e1cd66aa 100644 --- a/Sources/ProcessOutCoreUI/Sources/Core/MarkdownParser/Nodes/MarkdownList.swift +++ b/Sources/ProcessOutCoreUI/Sources/Core/MarkdownParser/Nodes/MarkdownList.swift @@ -7,7 +7,7 @@ @_implementationOnly import cmark -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 UInt32(listNode.list_type) { case CMARK_BULLET_LIST.rawValue: @@ -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 f4e9911f8..ba8fdfab8 100644 --- a/Sources/ProcessOutCoreUI/Sources/Core/MarkdownParser/Nodes/MarkdownListItem.swift +++ b/Sources/ProcessOutCoreUI/Sources/Core/MarkdownParser/Nodes/MarkdownListItem.swift @@ -7,9 +7,7 @@ @_implementationOnly import cmark -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 5c4c57970..f3716fc39 100644 --- a/Sources/ProcessOutCoreUI/Sources/Core/MarkdownParser/Nodes/MarkdownNode.swift +++ b/Sources/ProcessOutCoreUI/Sources/Core/MarkdownParser/Nodes/MarkdownNode.swift @@ -7,7 +7,7 @@ @_implementationOnly import cmark -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 bbe1e3337..7f52cd21e 100644 --- a/Sources/ProcessOutCoreUI/Sources/Core/MarkdownParser/Nodes/MarkdownParagraph.swift +++ b/Sources/ProcessOutCoreUI/Sources/Core/MarkdownParser/Nodes/MarkdownParagraph.swift @@ -7,9 +7,7 @@ @_implementationOnly import cmark -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 10dcf6e16..523b0cc53 100644 --- a/Sources/ProcessOutCoreUI/Sources/Core/MarkdownParser/Nodes/MarkdownSoftbreak.swift +++ b/Sources/ProcessOutCoreUI/Sources/Core/MarkdownParser/Nodes/MarkdownSoftbreak.swift @@ -7,7 +7,7 @@ @_implementationOnly import cmark -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 badad3cc1..c67f4568e 100644 --- a/Sources/ProcessOutCoreUI/Sources/Core/MarkdownParser/Nodes/MarkdownStrong.swift +++ b/Sources/ProcessOutCoreUI/Sources/Core/MarkdownParser/Nodes/MarkdownStrong.swift @@ -7,9 +7,7 @@ @_implementationOnly import cmark -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 62c841d91..020705c12 100644 --- a/Sources/ProcessOutCoreUI/Sources/Core/MarkdownParser/Nodes/MarkdownText.swift +++ b/Sources/ProcessOutCoreUI/Sources/Core/MarkdownParser/Nodes/MarkdownText.swift @@ -7,18 +7,23 @@ @_implementationOnly import cmark -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 384788c9f..169489ca4 100644 --- a/Sources/ProcessOutCoreUI/Sources/Core/MarkdownParser/Nodes/MarkdownThematicBreak.swift +++ b/Sources/ProcessOutCoreUI/Sources/Core/MarkdownParser/Nodes/MarkdownThematicBreak.swift @@ -7,7 +7,7 @@ @_implementationOnly import cmark -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/DesignSystem/ActionsContainer/POActionsContainerStyle.swift b/Sources/ProcessOutCoreUI/Sources/DesignSystem/ActionsContainer/POActionsContainerStyle.swift index c1d9799d5..e1a550ee2 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..7ed2f7e67 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 { 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/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 22619c26d..1c357336d 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 44219545b..79eac29ee 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 c34e9a7f1..4286150d9 100644 --- a/Sources/ProcessOutCoreUI/Sources/DesignSystem/TextField/POTextField.swift +++ b/Sources/ProcessOutCoreUI/Sources/DesignSystem/TextField/POTextField.swift @@ -117,7 +117,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/ColorResource.swift b/Sources/ProcessOutCoreUI/Sources/ResourceSymbols/ColorResource.swift index a6a6113e3..9707897d7 100644 --- a/Sources/ProcessOutCoreUI/Sources/ResourceSymbols/ColorResource.swift +++ b/Sources/ProcessOutCoreUI/Sources/ResourceSymbols/ColorResource.swift @@ -11,7 +11,7 @@ import SwiftUI /// A color resource. /// - NOTE: This type wraps natively generated `ColorResource` to make resources publicly accessible. -@_spi(PO) public struct POColorResource { +@_spi(PO) public struct POColorResource: Sendable { fileprivate init(_ colorResource: ColorResource) { self.colorResource = colorResource diff --git a/Sources/ProcessOutCoreUI/Sources/ResourceSymbols/FontResource.swift b/Sources/ProcessOutCoreUI/Sources/ResourceSymbols/FontResource.swift index 6f949b8b9..a2f2b7943 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/Sources/Api/Test3DS/POTest3DSService.swift b/Sources/ProcessOutUI/Sources/Api/Test3DS/POTest3DSService.swift index dd19cbd82..87b203d90 100644 --- a/Sources/ProcessOutUI/Sources/Api/Test3DS/POTest3DSService.swift +++ b/Sources/ProcessOutUI/Sources/Api/Test3DS/POTest3DSService.swift @@ -21,7 +21,7 @@ public final class POTest3DSService: PO3DSService { public func authenticationRequest( configuration: PO3DS2Configuration, - completion: @escaping (Result) -> Void + completion: @escaping @Sendable (Result) -> Void ) { let request = PO3DS2AuthenticationRequest( deviceData: "", @@ -33,26 +33,28 @@ public final class POTest3DSService: PO3DSService { completion(.success(request)) } - public func handle(challenge: PO3DS2Challenge, completion: @escaping (Result) -> Void) { - 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)) - } - alertController.addAction(acceptAction) - let rejectAction = UIAlertAction(title: String(resource: .Test3DS.reject), style: .default) { _ in - completion(.success(false)) + public func handle(challenge: PO3DS2Challenge, completion: @escaping @Sendable (Result) -> Void) { + MainActor.assumeIsolated { + 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)) + } + 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) } - alertController.addAction(rejectAction) - presentingViewController.present(alertController, animated: true) } - public func handle(redirect: PO3DSRedirect, completion: @escaping (Result) -> Void) { + public func handle(redirect: PO3DSRedirect, completion: @escaping @Sendable (Result) -> Void) { Task { @MainActor in let session = POWebAuthenticationSession(redirect: redirect, returnUrl: returnUrl, completion: completion) if await session.start() { 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.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 dadd4c3f8..365bba15e 100644 --- a/Sources/ProcessOutUI/Sources/Core/Providers/CardSchemeImage/CardSchemeImageProvider.swift +++ b/Sources/ProcessOutUI/Sources/Core/Providers/CardSchemeImage/CardSchemeImageProvider.swift @@ -8,7 +8,7 @@ import SwiftUI import ProcessOut -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/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 index 35d064012..61538fbe1 100644 --- a/Sources/ProcessOutUI/Sources/Modules/3DSRedirect/PO3DSRedirectController.swift +++ b/Sources/ProcessOutUI/Sources/Modules/3DSRedirect/PO3DSRedirectController.swift @@ -15,7 +15,8 @@ import ProcessOut /// 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 { +@MainActor +public final class PO3DSRedirectController: Sendable { /// - Parameters: /// - redirect: redirect to handle. @@ -79,7 +80,7 @@ public final class PO3DSRedirectController { } /// Completion to invoke when redirect handling ends. - public var completion: ((Result) -> Void)? + public var completion: (@Sendable (Result) -> Void)? /// The preferred color to tint the background of the navigation bar and toolbar. public var preferredBarTintColor: UIColor? @@ -90,7 +91,7 @@ public final class PO3DSRedirectController { // MARK: - Private Nested Types private enum AssociatedKeys { - static var redirectController: UInt8 = 0 + nonisolated(unsafe) static var redirectController: UInt8 = 0 } // MARK: - Private Properties diff --git a/Sources/ProcessOutUI/Sources/Modules/3DSRedirect/POWebAuthenticationSession+3DSRedirect.swift b/Sources/ProcessOutUI/Sources/Modules/3DSRedirect/POWebAuthenticationSession+3DSRedirect.swift index 5127ff5f4..2542194e6 100644 --- a/Sources/ProcessOutUI/Sources/Modules/3DSRedirect/POWebAuthenticationSession+3DSRedirect.swift +++ b/Sources/ProcessOutUI/Sources/Modules/3DSRedirect/POWebAuthenticationSession+3DSRedirect.swift @@ -19,7 +19,7 @@ extension POWebAuthenticationSession { public convenience init( redirect: PO3DSRedirect, returnUrl: URL, - completion: @escaping (Result) -> Void + completion: @escaping @Sendable (Result) -> Void ) { let completionBox: Completion = { result in completion(result.map(Self.token(with:))) @@ -30,7 +30,7 @@ extension POWebAuthenticationSession { // MARK: - Private Methods - private static func token(with url: URL) -> String { + private static nonisolated 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 index b1ecbd305..1d3d81607 100644 --- a/Sources/ProcessOutUI/Sources/Modules/3DSRedirect/SFSafariViewController+3DSRedirect.swift +++ b/Sources/ProcessOutUI/Sources/Modules/3DSRedirect/SFSafariViewController+3DSRedirect.swift @@ -24,7 +24,7 @@ extension SFSafariViewController { redirect: PO3DSRedirect, returnUrl: URL, safariConfiguration: SFSafariViewController.Configuration = .init(), - completion: @escaping (Result) -> Void + completion: @escaping @Sendable (Result) -> Void ) { self.init(url: redirect.url, configuration: safariConfiguration) let api: ProcessOut = ProcessOut.shared // swiftlint:disable:this redundant_type_annotation @@ -34,7 +34,7 @@ extension SFSafariViewController { eventEmitter: api.eventEmitter, logger: api.logger, completion: { result in - completion(result.map(Self.token(with:))) + completion(result.map(Self.token)) } ) setViewModel(viewModel) @@ -43,7 +43,7 @@ extension SFSafariViewController { // MARK: - Private Methods - private static func token(with url: URL) -> String { + private static nonisolated 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 index a094a3dda..8e3127e70 100644 --- a/Sources/ProcessOutUI/Sources/Modules/AlternativePayment/POWebAuthenticationSession+AlternativePayment.swift +++ b/Sources/ProcessOutUI/Sources/Modules/AlternativePayment/POWebAuthenticationSession+AlternativePayment.swift @@ -19,7 +19,7 @@ extension POWebAuthenticationSession { public convenience init( request: POAlternativePaymentMethodRequest, returnUrl: URL, - completion: @escaping (Result) -> Void + completion: @escaping @Sendable (Result) -> Void ) { let url = ProcessOut.shared.alternativePaymentMethods.alternativePaymentMethodUrl(request: request) self.init(alternativePaymentMethodUrl: url, returnUrl: returnUrl, completion: completion) @@ -35,17 +35,17 @@ extension POWebAuthenticationSession { public convenience init( alternativePaymentMethodUrl url: URL, returnUrl: URL, - completion: @escaping (Result) -> Void + completion: @escaping @Sendable (Result) -> Void ) { let completionBox: Completion = { result in - completion(result.flatMap(Self.response(with:))) + completion(result.flatMap(Self.response)) } self.init(url: url, callback: .customScheme(returnUrl.scheme ?? ""), completion: completionBox) } // MARK: - Private Methods - private static func response(with url: URL) -> Result { + private static nonisolated func response(with url: URL) -> Result { let result = Result { try ProcessOut.shared.alternativePaymentMethods.alternativePaymentMethodResponse(url: url) } diff --git a/Sources/ProcessOutUI/Sources/Modules/AlternativePayment/SFSafariViewController+AlternativePayment.swift b/Sources/ProcessOutUI/Sources/Modules/AlternativePayment/SFSafariViewController+AlternativePayment.swift index 02aab1d86..fd53eb53d 100644 --- a/Sources/ProcessOutUI/Sources/Modules/AlternativePayment/SFSafariViewController+AlternativePayment.swift +++ b/Sources/ProcessOutUI/Sources/Modules/AlternativePayment/SFSafariViewController+AlternativePayment.swift @@ -24,11 +24,15 @@ extension SFSafariViewController { request: POAlternativePaymentMethodRequest, returnUrl: URL, safariConfiguration: SFSafariViewController.Configuration = Configuration(), - completion: @escaping (Result) -> Void + completion: @escaping @Sendable (Result) -> Void ) { let url = ProcessOut.shared.alternativePaymentMethods.alternativePaymentMethodUrl(request: request) - self.init(url: url, configuration: safariConfiguration) - commonInit(returnUrl: returnUrl, completion: completion) + self.init( + alternativePaymentMethodUrl: url, + returnUrl: returnUrl, + safariConfiguration: safariConfiguration, + completion: completion + ) } /// Creates view controller that is capable of handling Alternative Payment. @@ -46,33 +50,24 @@ extension SFSafariViewController { alternativePaymentMethodUrl url: URL, returnUrl: URL, safariConfiguration: SFSafariViewController.Configuration = Configuration(), - completion: @escaping (Result) -> Void + completion: @escaping @Sendable (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, + eventEmitter: ProcessOut.shared.eventEmitter, + logger: ProcessOut.shared.logger, completion: { result in - completion(result.flatMap(Self.response(with:))) + completion(result.flatMap(Self.response)) } ) self.setViewModel(viewModel) viewModel.start() } - private static func response(with url: URL) -> Result { + // MARK: - Private Methods + + private nonisolated static func response(with url: URL) -> Result { let result = Result { try ProcessOut.shared.alternativePaymentMethods.alternativePaymentMethodResponse(url: url) } diff --git a/Sources/ProcessOutUI/Sources/Modules/CardTokenization/Configuration/POBillingAddressConfiguration.swift b/Sources/ProcessOutUI/Sources/Modules/CardTokenization/Configuration/POBillingAddressConfiguration.swift index 822e7d611..31e8c4cf0 100644 --- a/Sources/ProcessOutUI/Sources/Modules/CardTokenization/Configuration/POBillingAddressConfiguration.swift +++ b/Sources/ProcessOutUI/Sources/Modules/CardTokenization/Configuration/POBillingAddressConfiguration.swift @@ -8,7 +8,7 @@ import ProcessOut /// Billing address collection configuration. -public struct POBillingAddressConfiguration { +public struct POBillingAddressConfiguration: Sendable { @available(*, deprecated, message: "Use POBillingAddressCollectionMode directly.") public typealias CollectionMode = POBillingAddressCollectionMode diff --git a/Sources/ProcessOutUI/Sources/Modules/CardTokenization/Configuration/POCardTokenizationConfiguration.swift b/Sources/ProcessOutUI/Sources/Modules/CardTokenization/Configuration/POCardTokenizationConfiguration.swift index cffb610e8..f2035fc30 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 1cad02ce8..e4cec010f 100644 --- a/Sources/ProcessOutUI/Sources/Modules/CardTokenization/Delegate/POCardTokenizationDelegate.swift +++ b/Sources/ProcessOutUI/Sources/Modules/CardTokenization/Delegate/POCardTokenizationDelegate.swift @@ -8,9 +8,10 @@ import ProcessOut /// Card tokenization module delegate definition. -public protocol POCardTokenizationDelegate: AnyObject { +public protocol POCardTokenizationDelegate: AnyObject, Sendable { /// Invoked when module emits event. + @MainActor func cardTokenizationDidEmitEvent(_ event: POCardTokenizationEvent) /// Allows delegate to additionally process tokenized card before ending module's lifecycle. For example @@ -22,15 +23,18 @@ public protocol POCardTokenizationDelegate: AnyObject { /// Allows to choose preferred scheme that will be selected by default based on issuer information. Default /// implementation returns primary scheme. + @MainActor func preferredScheme(issuerInformation: POCardIssuerInformation) -> String? /// Asks delegate whether user should be allowed to continue after failure or module should complete. /// Default implementation returns `true`. + @MainActor func shouldContinueTokenization(after failure: POFailure) -> Bool } extension POCardTokenizationDelegate { + @MainActor public func cardTokenizationDidEmitEvent(_ event: POCardTokenizationEvent) { // Ignored } @@ -39,10 +43,12 @@ extension POCardTokenizationDelegate { // Ignored } + @MainActor public func preferredScheme(issuerInformation: POCardIssuerInformation) -> String? { issuerInformation.scheme } + @MainActor public func shouldContinueTokenization(after 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 8d31a11f2..77bfe744c 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. @@ -23,7 +24,4 @@ protocol CardTokenizationInteractor: Interactor /// Starts card tokenization. func tokenize() - - /// Cancells tokenization if possible. - func cancel() } diff --git a/Sources/ProcessOutUI/Sources/Modules/CardTokenization/Interactor/CardTokenizationInteractorState.swift b/Sources/ProcessOutUI/Sources/Modules/CardTokenization/Interactor/CardTokenizationInteractorState.swift index 5e3d5a707..349886426 100644 --- a/Sources/ProcessOutUI/Sources/Modules/CardTokenization/Interactor/CardTokenizationInteractorState.swift +++ b/Sources/ProcessOutUI/Sources/Modules/CardTokenization/Interactor/CardTokenizationInteractorState.swift @@ -133,3 +133,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 e1096bd0c..9e50d84ad 100644 --- a/Sources/ProcessOutUI/Sources/Modules/CardTokenization/Interactor/DefaultCardTokenizationInteractor.swift +++ b/Sources/ProcessOutUI/Sources/Modules/CardTokenization/Interactor/DefaultCardTokenizationInteractor.swift @@ -124,7 +124,7 @@ final class DefaultCardTokenizationInteractor: preferredScheme: startedState.preferredScheme?.rawValue, metadata: configuration.metadata ) - Task { @MainActor in + Task { do { let card = try await cardsService.tokenize(request: request) logger.debug("Did tokenize card: \(String(describing: card))") @@ -258,20 +258,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]) } } } diff --git a/Sources/ProcessOutUI/Sources/Modules/CardTokenization/Style/POCardTokenizationStyle.swift b/Sources/ProcessOutUI/Sources/Modules/CardTokenization/Style/POCardTokenizationStyle.swift index 91ab8f016..e5b127370 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. @@ -64,16 +65,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/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 3aa40d9cf..c1fbc840b 100644 --- a/Sources/ProcessOutUI/Sources/Modules/CardTokenization/ViewModel/CardTokenizationViewModelState.swift +++ b/Sources/ProcessOutUI/Sources/Modules/CardTokenization/ViewModel/CardTokenizationViewModelState.swift @@ -117,3 +117,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 ca2bc6435..d723a915b 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 { interactor.start() } + func stop() { + interactor.cancel() + } + // MARK: - Private Nested Types private typealias InteractorState = CardTokenizationInteractorState diff --git a/Sources/ProcessOutUI/Sources/Modules/CardUpdate/Configuration/POCardUpdateConfiguration.swift b/Sources/ProcessOutUI/Sources/Modules/CardUpdate/Configuration/POCardUpdateConfiguration.swift index 741b5b5b7..576450318 100644 --- a/Sources/ProcessOutUI/Sources/Modules/CardUpdate/Configuration/POCardUpdateConfiguration.swift +++ b/Sources/ProcessOutUI/Sources/Modules/CardUpdate/Configuration/POCardUpdateConfiguration.swift @@ -7,7 +7,7 @@ /// 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 diff --git a/Sources/ProcessOutUI/Sources/Modules/CardUpdate/Delegate/POCardUpdateDelegate.swift b/Sources/ProcessOutUI/Sources/Modules/CardUpdate/Delegate/POCardUpdateDelegate.swift index 4bd63feba..d2968aeec 100644 --- a/Sources/ProcessOutUI/Sources/Modules/CardUpdate/Delegate/POCardUpdateDelegate.swift +++ b/Sources/ProcessOutUI/Sources/Modules/CardUpdate/Delegate/POCardUpdateDelegate.swift @@ -8,29 +8,33 @@ 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? + /// Invoked when module emits event. + @MainActor + func cardUpdateDidEmitEvent(_ event: POCardUpdateEvent) + /// Asks delegate whether user should be allowed to continue after failure or module should complete. /// Default implementation returns `true`. + @MainActor func shouldContinueUpdate(after failure: POFailure) -> Bool } extension POCardUpdateDelegate { - public func cardUpdateDidEmitEvent(_ event: POCardUpdateEvent) { - // Ignored - } - public func cardInformation(cardId: String) async -> POCardUpdateInformation? { nil } + @MainActor + public func cardUpdateDidEmitEvent(_ event: POCardUpdateEvent) { + // Ignored + } + + @MainActor public func shouldContinueUpdate(after 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..c52b52dd3 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? 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..9d204070b 100644 --- a/Sources/ProcessOutUI/Sources/Modules/CardUpdate/Interactor/DefaultCardUpdateInteractor.swift +++ b/Sources/ProcessOutUI/Sources/Modules/CardUpdate/Interactor/DefaultCardUpdateInteractor.swift @@ -77,7 +77,6 @@ final class DefaultCardUpdateInteractor: BaseInteractor) -> 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..5d92b2d49 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? 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 fc77c3fb9..0109a026f 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 @@ -35,15 +37,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. + @MainActor func dynamicCheckout(preferredSchemeFor issuerInformation: POCardIssuerInformation) -> String? // 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. @@ -57,15 +62,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 } @@ -74,14 +82,17 @@ extension PODynamicCheckoutDelegate { nil } + @MainActor public func dynamicCheckout(didEmitCardTokenizationEvent event: POCardTokenizationEvent) { // Ignored } + @MainActor public func dynamicCheckout(preferredSchemeFor issuerInformation: POCardIssuerInformation) -> String? { 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/ChildProvider/DynamicCheckoutInteractorChildProvider.swift b/Sources/ProcessOutUI/Sources/Modules/DynamicCheckout/Interactor/ChildProvider/DynamicCheckoutInteractorChildProvider.swift index 15a470d1b..16922623e 100644 --- a/Sources/ProcessOutUI/Sources/Modules/DynamicCheckout/Interactor/ChildProvider/DynamicCheckoutInteractorChildProvider.swift +++ b/Sources/ProcessOutUI/Sources/Modules/DynamicCheckout/Interactor/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/Interactor/DynamicCheckout/DynamicCheckoutDefaultInteractor.swift b/Sources/ProcessOutUI/Sources/Modules/DynamicCheckout/Interactor/DynamicCheckout/DynamicCheckoutDefaultInteractor.swift index 5a46f8c04..28f58b66a 100644 --- a/Sources/ProcessOutUI/Sources/Modules/DynamicCheckout/Interactor/DynamicCheckout/DynamicCheckoutDefaultInteractor.swift +++ b/Sources/ProcessOutUI/Sources/Modules/DynamicCheckout/Interactor/DynamicCheckout/DynamicCheckoutDefaultInteractor.swift @@ -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) @@ -351,7 +350,7 @@ final class DynamicCheckoutDefaultInteractor: shouldInvalidateInvoice: true ) state = .paymentProcessing(paymentProcessingState) - Task { @MainActor in + Task { do { try await passKitPaymentSession.start(invoiceId: startedState.invoice.id, request: request) setSuccessState() @@ -430,7 +429,7 @@ final class DynamicCheckoutDefaultInteractor: shouldInvalidateInvoice: true ) state = .paymentProcessing(paymentProcessingState) - Task { @MainActor in + Task { do { _ = try await alternativePaymentSession.start(url: method.configuration.redirectUrl) setSuccessState() @@ -662,7 +661,7 @@ final class DynamicCheckoutDefaultInteractor: } state = .success send(event: .didCompletePayment) - Task { @MainActor in + Task { try? await Task.sleep(seconds: configuration.paymentSuccess?.duration ?? 0) completion(.success(())) } @@ -753,7 +752,8 @@ extension DynamicCheckoutDefaultInteractor: PONativeAlternativePaymentDelegate { } func nativeAlternativePaymentMethodDefaultValues( - for parameters: [PONativeAlternativePaymentMethodParameter], completion: @escaping ([String: String]) -> Void + for parameters: [PONativeAlternativePaymentMethodParameter], + completion: @escaping @Sendable ([String: String]) -> Void ) { Task { @MainActor in let values = await delegate?.dynamicCheckout(alternativePaymentDefaultsFor: parameters) ?? [:] diff --git a/Sources/ProcessOutUI/Sources/Modules/DynamicCheckout/Interactor/DynamicCheckout/DynamicCheckoutInteractor.swift b/Sources/ProcessOutUI/Sources/Modules/DynamicCheckout/Interactor/DynamicCheckout/DynamicCheckoutInteractor.swift index c55311625..7f8044cd9 100644 --- a/Sources/ProcessOutUI/Sources/Modules/DynamicCheckout/Interactor/DynamicCheckout/DynamicCheckoutInteractor.swift +++ b/Sources/ProcessOutUI/Sources/Modules/DynamicCheckout/Interactor/DynamicCheckout/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/Sessions/AlternativePayment/DynamicCheckoutAlternativePaymentDefaultSession.swift b/Sources/ProcessOutUI/Sources/Modules/DynamicCheckout/Sessions/AlternativePayment/DynamicCheckoutAlternativePaymentDefaultSession.swift index 198b099b8..ff1f34a45 100644 --- a/Sources/ProcessOutUI/Sources/Modules/DynamicCheckout/Sessions/AlternativePayment/DynamicCheckoutAlternativePaymentDefaultSession.swift +++ b/Sources/ProcessOutUI/Sources/Modules/DynamicCheckout/Sessions/AlternativePayment/DynamicCheckoutAlternativePaymentDefaultSession.swift @@ -8,7 +8,6 @@ import Foundation import ProcessOut -@MainActor final class DynamicCheckoutAlternativePaymentDefaultSession: DynamicCheckoutAlternativePaymentSession { init(configuration: PODynamicCheckoutAlternativePaymentConfiguration) { @@ -19,15 +18,18 @@ final class DynamicCheckoutAlternativePaymentDefaultSession: DynamicCheckoutAlte 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) + return try await withCheckedThrowingContinuation { continuation in + let session = POWebAuthenticationSession(alternativePaymentMethodUrl: url, returnUrl: returnUrl) { result in + continuation.resume(with: result) + } + Task { + guard await !session.start() else { + return + } + let failure = POFailure(message: "Unable to start alternative payment.", code: .generic(.mobile)) + continuation.resume(throwing: failure) + } } - 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 diff --git a/Sources/ProcessOutUI/Sources/Modules/DynamicCheckout/Sessions/AlternativePayment/DynamicCheckoutAlternativePaymentSession.swift b/Sources/ProcessOutUI/Sources/Modules/DynamicCheckout/Sessions/AlternativePayment/DynamicCheckoutAlternativePaymentSession.swift index 2f5e32ec2..82ae0e29b 100644 --- a/Sources/ProcessOutUI/Sources/Modules/DynamicCheckout/Sessions/AlternativePayment/DynamicCheckoutAlternativePaymentSession.swift +++ b/Sources/ProcessOutUI/Sources/Modules/DynamicCheckout/Sessions/AlternativePayment/DynamicCheckoutAlternativePaymentSession.swift @@ -8,6 +8,7 @@ import Foundation import ProcessOut +@MainActor protocol DynamicCheckoutAlternativePaymentSession { /// Starts alternative payment. diff --git a/Sources/ProcessOutUI/Sources/Modules/DynamicCheckout/Sessions/PassKit/DynamicCheckoutPassKitPaymentDefaultSession.swift b/Sources/ProcessOutUI/Sources/Modules/DynamicCheckout/Sessions/PassKit/DynamicCheckoutPassKitPaymentDefaultSession.swift index 3907e7d18..d3dafad82 100644 --- a/Sources/ProcessOutUI/Sources/Modules/DynamicCheckout/Sessions/PassKit/DynamicCheckoutPassKitPaymentDefaultSession.swift +++ b/Sources/ProcessOutUI/Sources/Modules/DynamicCheckout/Sessions/PassKit/DynamicCheckoutPassKitPaymentDefaultSession.swift @@ -9,7 +9,6 @@ import Foundation import PassKit import ProcessOut -@MainActor final class DynamicCheckoutPassKitPaymentDefaultSession: DynamicCheckoutPassKitPaymentSession { init(delegate: PODynamicCheckoutDelegate?, invoicesService: POInvoicesService) { diff --git a/Sources/ProcessOutUI/Sources/Modules/DynamicCheckout/Sessions/PassKit/DynamicCheckoutPassKitPaymentSession.swift b/Sources/ProcessOutUI/Sources/Modules/DynamicCheckout/Sessions/PassKit/DynamicCheckoutPassKitPaymentSession.swift index 25a70418d..7528f2a52 100644 --- a/Sources/ProcessOutUI/Sources/Modules/DynamicCheckout/Sessions/PassKit/DynamicCheckoutPassKitPaymentSession.swift +++ b/Sources/ProcessOutUI/Sources/Modules/DynamicCheckout/Sessions/PassKit/DynamicCheckoutPassKitPaymentSession.swift @@ -7,6 +7,7 @@ import PassKit +@MainActor protocol DynamicCheckoutPassKitPaymentSession { /// Boolean value indicating whether PassKit payments are supported. diff --git a/Sources/ProcessOutUI/Sources/Modules/DynamicCheckout/Style/PODynamicCheckoutStyle.swift b/Sources/ProcessOutUI/Sources/Modules/DynamicCheckout/Style/PODynamicCheckoutStyle.swift index 8bf8f6ca1..8e43014c8 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/View/PODynamicCheckoutView+Init.swift b/Sources/ProcessOutUI/Sources/Modules/DynamicCheckout/View/PODynamicCheckoutView+Init.swift index 8523f90f8..a20dcc315 100644 --- a/Sources/ProcessOutUI/Sources/Modules/DynamicCheckout/View/PODynamicCheckoutView+Init.swift +++ b/Sources/ProcessOutUI/Sources/Modules/DynamicCheckout/View/PODynamicCheckoutView+Init.swift @@ -21,7 +21,7 @@ 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, diff --git a/Sources/ProcessOutUI/Sources/Modules/DynamicCheckout/View/PODynamicCheckoutView.swift b/Sources/ProcessOutUI/Sources/Modules/DynamicCheckout/View/PODynamicCheckoutView.swift index 534ab7236..c9e014c33 100644 --- a/Sources/ProcessOutUI/Sources/Modules/DynamicCheckout/View/PODynamicCheckoutView.swift +++ b/Sources/ProcessOutUI/Sources/Modules/DynamicCheckout/View/PODynamicCheckoutView.swift @@ -41,6 +41,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 cb4b4fd2e..66ce2722a 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 { 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 10480cdf6..f477a9542 100644 --- a/Sources/ProcessOutUI/Sources/Modules/DynamicCheckout/ViewModel/DynamicCheckoutViewModelItem.swift +++ b/Sources/ProcessOutUI/Sources/Modules/DynamicCheckout/ViewModel/DynamicCheckoutViewModelItem.swift @@ -165,3 +165,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/Interactor/NativeAlternativePaymentDefaultInteractor.swift b/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/Interactor/NativeAlternativePaymentDefaultInteractor.swift index 13d663540..2634523e0 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 { @@ -164,7 +163,6 @@ final class NativeAlternativePaymentDefaultInteractor: // MARK: - Submission State - @MainActor private func continueSubmissionUnchecked( startedState: NativeAlternativePaymentInteractorState.Started, values: [String: String] ) async { @@ -203,7 +201,6 @@ final class NativeAlternativePaymentDefaultInteractor: // MARK: - Awaiting Capture State - @MainActor private func setAwaitingCaptureStateUnchecked( gateway: PONativeAlternativePaymentMethodTransactionDetails.Gateway, parameterValues: PONativeAlternativePaymentMethodParameterValues? @@ -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,7 +327,6 @@ 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 { @@ -370,7 +366,6 @@ final class NativeAlternativePaymentDefaultInteractor: // MARK: - Cancellation Availability - @MainActor private func enableCancellationAfterDelay() { let disabledFor = disableDuration(of: configuration.secondaryAction) guard disabledFor > 0 else { @@ -392,7 +387,6 @@ final class NativeAlternativePaymentDefaultInteractor: } } - @MainActor private func enableCaptureCancellationAfterDelay() { let disabledFor = disableDuration(of: configuration.paymentConfirmation.secondaryAction) guard disabledFor > 0 else { @@ -441,7 +435,6 @@ final class NativeAlternativePaymentDefaultInteractor: self.state = state } - @MainActor private func createParameters( specifications: [PONativeAlternativePaymentMethodParameter] ) async -> [NativeAlternativePaymentInteractorState.Parameter] { @@ -497,7 +490,6 @@ final class NativeAlternativePaymentDefaultInteractor: // MARK: - Default Values /// Updates parameters with default values. - @MainActor private func setDefaultValues( parameters: inout [NativeAlternativePaymentInteractorState.Parameter] ) async { @@ -507,7 +499,8 @@ final class NativeAlternativePaymentDefaultInteractor: let defaultValues = await withCheckedContinuation { continuation in if let delegate { delegate.nativeAlternativePaymentMethodDefaultValues( - for: parameters.map(\.specification), completion: continuation.resume + for: parameters.map(\.specification), + completion: { continuation.resume(returning: $0) } ) } else { continuation.resume(returning: [:]) 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/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 f93405dda..1a6b412b0 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 { interactor.start() } + func stop() { + interactor.cancel() + } + // MARK: - Private Nested Types private typealias InteractorState = NativeAlternativePaymentInteractorState 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 e3705adef..3acc4cb55 100644 --- a/Sources/ProcessOutUI/Sources/Modules/PassKitPaymentAuthorization/POPassKitPaymentAuthorizationController.swift +++ b/Sources/ProcessOutUI/Sources/Modules/PassKitPaymentAuthorization/POPassKitPaymentAuthorizationController.swift @@ -9,20 +9,21 @@ import PassKit @_spi(PO) import ProcessOut /// An object that presents a sheet that prompts the user to authorize a payment request +@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) @@ -30,10 +31,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 = DefaultPassKitPaymentErrorMapper(logger: ProcessOut.shared.logger) @@ -49,7 +50,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) @@ -83,7 +84,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 @@ -93,8 +94,6 @@ public final class POPassKitPaymentAuthorizationController: NSObject { private let errorMapper: PassKitPaymentErrorMapper private let cardsService: POCardsService - - @POUnfairlyLocked private var didPresentApplePay: Bool } @@ -173,7 +172,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 5825d6b82..971a13b0c 100644 --- a/Sources/ProcessOutUI/Sources/Modules/PassKitPaymentAuthorization/POPassKitPaymentAuthorizationControllerDelegate.swift +++ b/Sources/ProcessOutUI/Sources/Modules/PassKitPaymentAuthorization/POPassKitPaymentAuthorizationControllerDelegate.swift @@ -87,6 +87,7 @@ public protocol POPassKitPaymentAuthorizationControllerDelegate: AnyObject { extension POPassKitPaymentAuthorizationControllerDelegate { + @MainActor public func paymentAuthorizationController( _ controller: POPassKitPaymentAuthorizationController, didFailToTokenizePayment payment: PKPayment, @@ -95,6 +96,7 @@ extension POPassKitPaymentAuthorizationControllerDelegate { nil } + @MainActor public func paymentAuthorizationControllerWillAuthorizePayment( _ controller: POPassKitPaymentAuthorizationController ) { @@ -102,6 +104,7 @@ extension POPassKitPaymentAuthorizationControllerDelegate { } @available(iOS 14.0, *) + @MainActor public func paymentAuthorizationControllerDidRequestMerchantSessionUpdate( controller: POPassKitPaymentAuthorizationController ) async -> PKPaymentRequestMerchantSessionUpdate? { @@ -109,6 +112,7 @@ extension POPassKitPaymentAuthorizationControllerDelegate { } @available(iOS 15.0, *) + @MainActor public func paymentAuthorizationController( _ controller: POPassKitPaymentAuthorizationController, didChangeCouponCode couponCode: String @@ -116,6 +120,7 @@ extension POPassKitPaymentAuthorizationControllerDelegate { nil } + @MainActor public func paymentAuthorizationController( _ controller: POPassKitPaymentAuthorizationController, didSelectShippingMethod shippingMethod: PKShippingMethod @@ -123,6 +128,7 @@ extension POPassKitPaymentAuthorizationControllerDelegate { nil } + @MainActor public func paymentAuthorizationController( _ controller: POPassKitPaymentAuthorizationController, didSelectShippingContact contact: PKContact @@ -130,6 +136,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 index c81fbe579..005e8ef38 100644 --- a/Sources/ProcessOutUI/Sources/Modules/WebAuthentication/DefaultSafariViewModel.swift +++ b/Sources/ProcessOutUI/Sources/Modules/WebAuthentication/DefaultSafariViewModel.swift @@ -9,14 +9,15 @@ import Foundation import SafariServices @_spi(PO) import ProcessOut -final class DefaultSafariViewModel: NSObject, SFSafariViewControllerDelegate { +@MainActor +final class DefaultSafariViewModel: NSObject, Sendable, @preconcurrency SFSafariViewControllerDelegate { init( callback: POWebAuthenticationSessionCallback, timeout: TimeInterval? = nil, eventEmitter: POEventEmitter, logger: POLogger, - completion: @escaping (Result) -> Void + completion: @escaping @Sendable (Result) -> Void ) { self.callback = callback self.timeout = timeout @@ -32,7 +33,9 @@ final class DefaultSafariViewModel: NSObject, SFSafariViewControllerDelegate { } if let timeout { timeoutTimer = Timer.scheduledTimer(withTimeInterval: timeout, repeats: false) { [weak self] _ in - self?.setCompletedState(with: POFailure(code: .timeout(.mobile))) + MainActor.assumeIsolated { + self?.setCompletedState(with: POFailure(code: .timeout(.mobile))) + } } } deepLinkObserver = eventEmitter.on(PODeepLinkReceivedEvent.self) { [weak self] event in @@ -59,7 +62,7 @@ final class DefaultSafariViewModel: NSObject, SFSafariViewControllerDelegate { } } - func safariViewController(_ controller: SFSafariViewController, initialLoadDidRedirectTo url: URL) { + nonisolated func safariViewController(_ controller: SFSafariViewController, initialLoadDidRedirectTo url: URL) { logger.debug("Safari did redirect to url: \(url)") } diff --git a/Sources/ProcessOutUI/Sources/Modules/WebAuthentication/POWebAuthenticationSession.swift b/Sources/ProcessOutUI/Sources/Modules/WebAuthentication/POWebAuthenticationSession.swift index 0a4be1baa..33bb00a9a 100644 --- a/Sources/ProcessOutUI/Sources/Modules/WebAuthentication/POWebAuthenticationSession.swift +++ b/Sources/ProcessOutUI/Sources/Modules/WebAuthentication/POWebAuthenticationSession.swift @@ -10,17 +10,17 @@ import AuthenticationServices @_spi(PO) import ProcessOut /// A session that an app uses to authenticate a payment. -public final class POWebAuthenticationSession { +@MainActor +public final class POWebAuthenticationSession: Sendable { /// A completion handler for the web authentication session. - typealias Completion = (Result) -> Void + typealias Completion = @Sendable (Result) -> Void /// Only call this method once for a given POWebAuthenticationSession instance after initialization. /// Calling the start() method on a canceled session results in a failure. /// /// After you call start(), the session instance stores a strong reference to itself. To avoid deallocation during /// the authentication process, the session keeps the reference until after it calls the completion handler. - @MainActor public func start() async -> Bool { guard state == nil else { preconditionFailure("Session start must be attempted only once.") @@ -41,7 +41,6 @@ public final class POWebAuthenticationSession { /// /// 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 @@ -71,7 +70,7 @@ public final class POWebAuthenticationSession { // MARK: - Private Nested Types private enum AssociatedKeys { - static var controller: UInt8 = 0 + nonisolated(unsafe) static var controller: UInt8 = 0 } private enum State { @@ -105,7 +104,7 @@ public final class POWebAuthenticationSession { return viewController } - private func complete(with result: Result) { + private nonisolated func complete(with result: Result) { Task { @MainActor in await self.cancel() state = .completed diff --git a/Sources/ProcessOutUI/Sources/Modules/WebAuthentication/POWebAuthenticationSessionCallback.swift b/Sources/ProcessOutUI/Sources/Modules/WebAuthentication/POWebAuthenticationSessionCallback.swift index fb0368c13..1ced4cbe8 100644 --- a/Sources/ProcessOutUI/Sources/Modules/WebAuthentication/POWebAuthenticationSessionCallback.swift +++ b/Sources/ProcessOutUI/Sources/Modules/WebAuthentication/POWebAuthenticationSessionCallback.swift @@ -8,7 +8,7 @@ import Foundation /// An object used to evaluate navigation events in an authentication session. -public struct POWebAuthenticationSessionCallback: @unchecked Sendable { +public struct POWebAuthenticationSessionCallback: Sendable { /// Creates a callback object that matches against URLs with the given custom scheme. /// - Parameter customScheme: The custom scheme that the app expects in the callback URL. @@ -17,5 +17,5 @@ public struct POWebAuthenticationSessionCallback: @unchecked Sendable { } /// Check whether a given main-frame navigation URL matches the callback expected by the client app. - let matchesURL: (_ url: URL) -> Bool + let matchesURL: @Sendable (_ url: URL) -> Bool } diff --git a/Sources/ProcessOutUI/Sources/Modules/WebAuthentication/SafariViewController+Extensions.swift b/Sources/ProcessOutUI/Sources/Modules/WebAuthentication/SafariViewController+Extensions.swift index bccff14ca..f6a2beb39 100644 --- a/Sources/ProcessOutUI/Sources/Modules/WebAuthentication/SafariViewController+Extensions.swift +++ b/Sources/ProcessOutUI/Sources/Modules/WebAuthentication/SafariViewController+Extensions.swift @@ -17,6 +17,6 @@ extension SFSafariViewController { // MARK: - Private Nested Types private enum Keys { - static var viewModel: UInt8 = 0 + nonisolated(unsafe) 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/project.yml b/project.yml index 00c2f632e..3c9140d6b 100644 --- a/project.yml +++ b/project.yml @@ -8,6 +8,7 @@ settings: SUPPORTS_MACCATALYST: false LOCALIZED_STRING_MACRO_NAMES: "$(inherited) POStringResource" LOCALIZATION_PREFERS_STRING_CATALOGS: true + SWIFT_STRICT_CONCURRENCY: complete options: transitivelyLinkDependencies: true packages: From 59f0984b302b03b0e9e7d76e6a0a1640e3496724 Mon Sep 17 00:00:00 2001 From: Andrii Vysotskyi Date: Fri, 26 Jul 2024 17:27:29 +0200 Subject: [PATCH 02/10] feat(ad-hoc): remove legacy UI (#315) --- .../Action/Border/Contents.json | 9 - .../Border/Disabled.colorset/Contents.json | 38 - .../Border/Selected.colorset/Contents.json | 38 - .../Colors.xcassets/Action/Contents.json | 9 - .../Action/Primary/Contents.json | 9 - .../Primary/Default.colorset/Contents.json | 38 - .../Primary/Disabled.colorset/Contents.json | 38 - .../Primary/Pressed.colorset/Contents.json | 38 - .../Action/Secondary/Contents.json | 9 - .../Secondary/Default.colorset/Contents.json | 38 - .../Secondary/Pressed.colorset/Contents.json | 38 - .../Colors.xcassets/Border/Contents.json | 9 - .../Border/Default.colorset/Contents.json | 38 - .../Border/Divider.colorset/Contents.json | 38 - .../Border/Subtle.colorset/Contents.json | 38 - .../Resources/Colors.xcassets/Contents.json | 6 - .../Surface/Background.colorset/Contents.json | 38 - .../Colors.xcassets/Surface/Contents.json | 9 - .../Surface/Error.colorset/Contents.json | 38 - .../Surface/Level1.colorset/Contents.json | 38 - .../Surface/Neutral.colorset/Contents.json | 38 - .../Surface/Success.colorset/Contents.json | 38 - .../Surface/Warning.colorset/Contents.json | 38 - .../Colors.xcassets/Text/Contents.json | 9 - .../Text/Disabled.colorset/Contents.json | 38 - .../Text/Error.colorset/Contents.json | 38 - .../Text/Muted.colorset/Contents.json | 38 - .../Text/OnColor.colorset/Contents.json | 38 - .../Text/Primary.colorset/Contents.json | 38 - .../Text/Secondary.colorset/Contents.json | 38 - .../Text/Success.colorset/Contents.json | 38 - .../Text/Tertiary.colorset/Contents.json | 38 - .../Text/Warning.colorset/Contents.json | 38 - .../Resources/Fonts/WorkSans-Italic.ttf | Bin 335056 -> 0 bytes .../ProcessOut/Resources/Fonts/WorkSans.ttf | Bin 227432 -> 0 bytes .../ChevronDown.imageset/ChevronDownDark.pdf | Bin 1408 -> 0 bytes .../ChevronDown.imageset/ChevronDownLight.pdf | Bin 1408 -> 0 bytes .../ChevronDown.imageset/Contents.json | 22 - .../Resources/Images.xcassets/Contents.json | 6 - .../Success.imageset/Contents.json | 15 - .../Success.imageset/Success.pdf | Bin 7664 -> 0 bytes .../Resources/Localizable.xcstrings | 16 + .../ProcessOut/Sources/Api/ProcessOut.swift | 9 - .../Api/Utils/Test3DS/POTest3DSService.swift | 76 -- .../Test3DS/StringResource+Test3DS.swift | 21 - .../Core/Cancellable/GroupCancellable.swift | 47 - .../URLSessionTask+Cancellable.swift | 10 - .../POPhoneNumberFormat.swift | 19 - .../POPhoneNumberMetadata.swift | 16 - .../POPhoneNumberMetadataProvider.swift | 13 - .../Core/Markdown/MarkdownNodeFactory.swift | 53 - .../Core/Markdown/MarkdownParser.swift | 12 - .../Markdown/Nodes/MarkdownBlockQuote.swift | 19 - .../Markdown/Nodes/MarkdownCodeBlock.swift | 33 - .../Markdown/Nodes/MarkdownCodeSpan.swift | 34 - .../Markdown/Nodes/MarkdownDocument.swift | 19 - .../Markdown/Nodes/MarkdownEmphasis.swift | 19 - .../Core/Markdown/Nodes/MarkdownHeading.swift | 28 - .../Markdown/Nodes/MarkdownLinebreak.swift | 19 - .../Core/Markdown/Nodes/MarkdownLink.swift | 32 - .../Core/Markdown/Nodes/MarkdownList.swift | 64 -- .../Markdown/Nodes/MarkdownListItem.swift | 19 - .../Core/Markdown/Nodes/MarkdownNode.swift | 45 - .../Markdown/Nodes/MarkdownParagraph.swift | 19 - .../Markdown/Nodes/MarkdownSoftbreak.swift | 19 - .../Core/Markdown/Nodes/MarkdownStrong.swift | 19 - .../Core/Markdown/Nodes/MarkdownText.swift | 34 - .../Nodes/MarkdownThematicBreak.swift | 19 - .../Core/Markdown/Nodes/MarkdownUnknown.swift | 20 - .../MarkdownDebugDescriptionPrinter.swift | 144 --- .../Markdown/Visitor/MarkdownVisitor.swift | 59 -- .../Sources/Core/Markers/POAutoAsync.swift | 14 - .../Core/Markers/POAutoCompletion.swift | 12 - .../ImmutableNullHashable.swift | 22 - .../PropertyWrappers/ReferenceWrapper.swift | 77 -- .../Sources/Generated/Files+Generated.swift | 53 - .../Sources/Generated/Fonts+Generated.swift | 144 --- .../Sources/Legacy/APMTokenReturn.swift | 40 - .../Sources/Legacy/ApiResponse.swift | 19 - .../AuthentificationChallengeData.swift | 29 - .../Sources/Legacy/AuthorizationRequest.swift | 33 - .../Sources/Legacy/AuthorizationResult.swift | 22 - .../Sources/Legacy/CardPaymentWebView.swift | 21 - .../Sources/Legacy/CardTokenWebView.swift | 20 - .../Sources/Legacy/CustomerAction.swift | 28 - .../Legacy/CustomerActionHandler.swift | 159 --- .../Sources/Legacy/DirectoryServerData.swift | 23 - .../FingerPrintWebViewSchemeHandler.swift | 47 - .../Sources/Legacy/FingerprintWebView.swift | 61 -- .../Sources/Legacy/GatewayConfiguration.swift | 61 -- .../IncrementAuthorizationRequest.swift | 10 - .../Sources/Legacy/MiscGatewayRequest.swift | 41 - .../Sources/Legacy/ProcessOutException.swift | 15 - .../Sources/Legacy/ProcessOutLegacyApi.swift | 908 ------------------ .../Legacy/ProcessOutRequestManager.swift | 130 --- .../Sources/Legacy/ProcessOutWebView.swift | 70 -- .../Sources/Legacy/RetryPolicy.swift | 56 -- .../Legacy/ThreeDSFingerprintResponse.swift | 48 - .../Sources/Legacy/ThreeDSHandler.swift | 42 - .../Sources/Legacy/ThreeDSTestHandler.swift | 58 -- .../Sources/Legacy/TokenRequest.swift | 37 - .../Sources/Legacy/TokenizationResult.swift | 29 - .../PO3DSRedirectViewControllerBuilder.swift | 88 -- ...reeDSRedirectSafariViewModelDelegate.swift | 44 - ...PaymentMethodSafariViewModelDelegate.swift | 35 - ...vePaymentMethodViewControllerBuilder.swift | 100 -- ...vePaymentMethodViewControllerBuilder.swift | 107 --- ...veAlternativePaymentMethodInteractor.swift | 578 ----------- ...tiveAlternativePaymentMethodDelegate.swift | 24 - ...veAlternativePaymentMethodInteractor.swift | 39 - ...PaymentMethodInteractorConfiguration.swift | 40 - ...ernativePaymentMethodInteractorState.swift | 97 -- ...lternativePaymentMethodConfiguration.swift | 80 -- ...ONativeAlternativePaymentMethodEvent.swift | 83 -- ...AlternativePaymentMethodActionsStyle.swift | 9 - ...ernativePaymentMethodBackgroundStyle.swift | 26 - ...ONativeAlternativePaymentMethodStyle.swift | 104 -- ...ingResource+NativeAlternativePayment.swift | 73 -- .../NativeAlternativePaymentMethodCell.swift | 33 - ...lternativePaymentMethodCodeInputCell.swift | 115 --- ...iveAlternativePaymentMethodInputCell.swift | 146 --- ...veAlternativePaymentMethodLoaderCell.swift | 36 - ...veAlternativePaymentMethodPickerCell.swift | 59 -- ...iveAlternativePaymentMethodRadioCell.swift | 64 -- ...lternativePaymentMethodSubmittedCell.swift | 181 ---- ...ativePaymentMethodSubmittedCellStyle.swift | 19 - ...ternativePaymentMethodViewController.swift | 503 ---------- ...iveAlternativePaymentMethodViewModel.swift | 416 -------- ...iveAlternativePaymentMethodViewModel.swift | 14 - ...ternativePaymentMethodViewModelState.swift | 176 ---- .../Safari/DefaultSafariViewModel.swift | 138 --- .../DefaultSafariViewModelConfiguration.swift | 18 - .../DefaultSafariViewModelDelegate.swift | 19 - .../SafariViewController+Extensions.swift | 23 - .../View/BaseViewController.swift | 126 --- .../ViewModel/BaseViewModel.swift | 28 - .../Architecture/ViewModel/ViewModel.swift | 24 - .../Error/CollectionViewErrorCell.swift | 58 -- .../Error/CollectionViewErrorViewModel.swift | 15 - .../Radio/CollectionViewRadioCell.swift | 57 -- .../Radio/CollectionViewRadioViewModel.swift | 25 - .../CollectionViewSectionHeaderView.swift | 58 -- ...CollectionViewSectionHeaderViewModel.swift | 15 - .../CollectionViewSeparatorView.swift | 15 - .../Title/CollectionViewTitleCell.swift | 58 -- .../Title/CollectionViewTitleViewModel.swift | 12 - .../UIActivityIndicatorView+Extensions.swift | 19 - .../Extensions/UIView+Style.swift | 26 - .../Center/CollectionViewCenterLayout.swift | 212 ---- .../CollectionViewDelegateCenterLayout.swift | 18 - .../Styles/Input/POInputStateStyle.swift | 48 - .../Styles/Input/POInputStyle.swift | 53 - .../DesignSystem/Styles/POBorderStyle.swift | 41 - .../DesignSystem/Styles/POShadowStyle.swift | 58 -- .../Typography/AttributedStringBuilder.swift | 128 --- .../AttributedStringMarkdownVisitor.swift | 179 ---- .../DesignSystem/Typography/POTextStyle.swift | 24 - .../Typography/POTypography.swift | 81 -- .../ActionsContainerView.swift | 137 --- .../ActionsContainerViewModel.swift | 33 - .../POActionsContainerStyle.swift | 43 - .../ActivityIndicatorViewFactory.swift | 26 - .../POActivityIndicatorStyle.swift | 19 - .../POActivityIndicatorView.swift | 26 - .../DesignSystem/Views/Button/Button.swift | 190 ---- .../Views/Button/POButtonStateStyle.swift | 32 - .../Views/Button/POButtonStyle.swift | 92 -- .../Views/CodeTextField/CodeTextField.swift | 502 ---------- .../CodeTextFieldCarretPosition.swift | 10 - .../CodeTextFieldComponentView.swift | 172 ---- .../CodeTextField/CodeTextFieldDelegate.swift | 23 - .../DesignSystem/Views/Picker/Picker.swift | 174 ---- .../Views/Picker/PickerViewModel.swift | 30 - .../PORadioButtonKnobStateStyle.swift | 39 - .../RadioButton/PORadioButtonStateStyle.swift | 25 - .../RadioButton/PORadioButtonStyle.swift | 85 -- .../Views/RadioButton/RadioButton.swift | 128 --- .../RadioButton/RadioButtonKnobView.swift | 83 -- .../RadioButton/RadioButtonViewModel.swift | 18 - .../TextField/TextFieldContainerView.swift | 128 --- .../Extensions/Array+NSAttributedString.swift | 25 - .../NSLayoutConstraint+Extensions.swift | 16 - .../UICollectionView+Extensions.swift | 39 - .../Extensions/UIImageView+Extensions.swift | 35 - .../UITraitCollection+ColorAppearance.swift | 22 - .../Shared/Extensions/UIView+Extensions.swift | 49 - .../CollectionViewDiffableDataSource.swift | 253 ----- .../DiffableDataSourceSnapshot.swift | 53 - .../CollectionReusableViewSizeProvider.swift | 52 - .../Shared/Utils/KeyboardNotification.swift | 32 - .../UI/Shared/Utils/PassthroughView.swift | 19 - .../Sources/UI/Shared/Utils/Reusable.swift | 13 - .../UI/Shared/Utils/TextFieldUtils.swift | 43 - Sources/ProcessOut/swiftgen.yml | 24 - .../PhoneNumberMetadata.json | 0 .../PhoneNumberMetadata.version | 0 .../Sources/Api/ProcessOutUI.swift | 2 + .../Api/Test3DS/POTest3DSService.swift | 2 +- .../Api/Test3DS/StringResource+Test3DS.swift | 10 +- .../Core/Extensions/String+Extensions.swift | 0 .../StringProtocol+Extensions.swift | 2 +- .../CardExpirationFormatter.swift} | 18 +- .../CardNumber/CardNumberFormatter.swift} | 14 +- .../CardSecurityCodeFormatter.swift | 4 +- .../DefaultPhoneNumberMetadataProvider.swift} | 25 +- .../MetadataProvider/PhoneNumberFormat.swift | 18 + .../PhoneNumberMetadata.swift | 15 + .../PhoneNumberMetadataProvider.swift | 12 + .../PhoneNumber/PhoneNumberFormatter.swift} | 31 +- .../RegexProvider/RegexProvider.swift | 0 .../Formatters/Utils/FormattingUtils.swift} | 4 +- .../AddressSpecification+StringResource.swift | 51 +- .../Utils/POStringResource+Extension.swift | 15 - .../Sources/Core/Utils/StringResource.swift} | 11 +- .../DefaultCardTokenizationInteractor.swift | 6 +- .../StringResource+CardTokenization.swift | 38 +- .../DefaultCardUpdateInteractor.swift | 2 +- .../Symbols/StringResource+CardUpdate.swift | 18 +- .../DynamicCheckoutDefaultInteractor.swift | 4 +- .../StringResource+DynamicCheckout.swift | 20 +- .../View/DynamicCheckoutContentView.swift | 1 - .../View/PODynamicCheckoutView.swift | 1 - .../PONativeAlternativePaymentDelegate.swift | 25 +- .../PONativeAlternativePaymentEvent.swift | 68 +- ...eAlternativePaymentDefaultInteractor.swift | 12 +- ...ingResource+NativeAlternativePayment.swift | 36 +- project.yml | 6 - 227 files changed, 293 insertions(+), 12080 deletions(-) delete mode 100644 Sources/ProcessOut/Resources/Colors.xcassets/Action/Border/Contents.json delete mode 100644 Sources/ProcessOut/Resources/Colors.xcassets/Action/Border/Disabled.colorset/Contents.json delete mode 100644 Sources/ProcessOut/Resources/Colors.xcassets/Action/Border/Selected.colorset/Contents.json delete mode 100644 Sources/ProcessOut/Resources/Colors.xcassets/Action/Contents.json delete mode 100644 Sources/ProcessOut/Resources/Colors.xcassets/Action/Primary/Contents.json delete mode 100644 Sources/ProcessOut/Resources/Colors.xcassets/Action/Primary/Default.colorset/Contents.json delete mode 100644 Sources/ProcessOut/Resources/Colors.xcassets/Action/Primary/Disabled.colorset/Contents.json delete mode 100644 Sources/ProcessOut/Resources/Colors.xcassets/Action/Primary/Pressed.colorset/Contents.json delete mode 100644 Sources/ProcessOut/Resources/Colors.xcassets/Action/Secondary/Contents.json delete mode 100644 Sources/ProcessOut/Resources/Colors.xcassets/Action/Secondary/Default.colorset/Contents.json delete mode 100644 Sources/ProcessOut/Resources/Colors.xcassets/Action/Secondary/Pressed.colorset/Contents.json delete mode 100644 Sources/ProcessOut/Resources/Colors.xcassets/Border/Contents.json delete mode 100644 Sources/ProcessOut/Resources/Colors.xcassets/Border/Default.colorset/Contents.json delete mode 100644 Sources/ProcessOut/Resources/Colors.xcassets/Border/Divider.colorset/Contents.json delete mode 100644 Sources/ProcessOut/Resources/Colors.xcassets/Border/Subtle.colorset/Contents.json delete mode 100644 Sources/ProcessOut/Resources/Colors.xcassets/Contents.json delete mode 100644 Sources/ProcessOut/Resources/Colors.xcassets/Surface/Background.colorset/Contents.json delete mode 100644 Sources/ProcessOut/Resources/Colors.xcassets/Surface/Contents.json delete mode 100644 Sources/ProcessOut/Resources/Colors.xcassets/Surface/Error.colorset/Contents.json delete mode 100644 Sources/ProcessOut/Resources/Colors.xcassets/Surface/Level1.colorset/Contents.json delete mode 100644 Sources/ProcessOut/Resources/Colors.xcassets/Surface/Neutral.colorset/Contents.json delete mode 100644 Sources/ProcessOut/Resources/Colors.xcassets/Surface/Success.colorset/Contents.json delete mode 100644 Sources/ProcessOut/Resources/Colors.xcassets/Surface/Warning.colorset/Contents.json delete mode 100644 Sources/ProcessOut/Resources/Colors.xcassets/Text/Contents.json delete mode 100644 Sources/ProcessOut/Resources/Colors.xcassets/Text/Disabled.colorset/Contents.json delete mode 100644 Sources/ProcessOut/Resources/Colors.xcassets/Text/Error.colorset/Contents.json delete mode 100644 Sources/ProcessOut/Resources/Colors.xcassets/Text/Muted.colorset/Contents.json delete mode 100644 Sources/ProcessOut/Resources/Colors.xcassets/Text/OnColor.colorset/Contents.json delete mode 100644 Sources/ProcessOut/Resources/Colors.xcassets/Text/Primary.colorset/Contents.json delete mode 100644 Sources/ProcessOut/Resources/Colors.xcassets/Text/Secondary.colorset/Contents.json delete mode 100644 Sources/ProcessOut/Resources/Colors.xcassets/Text/Success.colorset/Contents.json delete mode 100644 Sources/ProcessOut/Resources/Colors.xcassets/Text/Tertiary.colorset/Contents.json delete mode 100644 Sources/ProcessOut/Resources/Colors.xcassets/Text/Warning.colorset/Contents.json delete mode 100644 Sources/ProcessOut/Resources/Fonts/WorkSans-Italic.ttf delete mode 100644 Sources/ProcessOut/Resources/Fonts/WorkSans.ttf delete mode 100644 Sources/ProcessOut/Resources/Images.xcassets/ChevronDown.imageset/ChevronDownDark.pdf delete mode 100644 Sources/ProcessOut/Resources/Images.xcassets/ChevronDown.imageset/ChevronDownLight.pdf delete mode 100644 Sources/ProcessOut/Resources/Images.xcassets/ChevronDown.imageset/Contents.json delete mode 100644 Sources/ProcessOut/Resources/Images.xcassets/Contents.json delete mode 100644 Sources/ProcessOut/Resources/Images.xcassets/Success.imageset/Contents.json delete mode 100644 Sources/ProcessOut/Resources/Images.xcassets/Success.imageset/Success.pdf delete mode 100644 Sources/ProcessOut/Sources/Api/Utils/Test3DS/POTest3DSService.swift delete mode 100644 Sources/ProcessOut/Sources/Api/Utils/Test3DS/StringResource+Test3DS.swift delete mode 100644 Sources/ProcessOut/Sources/Core/Cancellable/GroupCancellable.swift delete mode 100644 Sources/ProcessOut/Sources/Core/Extensions/URLSessionTask+Cancellable.swift delete mode 100644 Sources/ProcessOut/Sources/Core/Formatters/PhoneNumber/MetadataProvider/POPhoneNumberFormat.swift delete mode 100644 Sources/ProcessOut/Sources/Core/Formatters/PhoneNumber/MetadataProvider/POPhoneNumberMetadata.swift delete mode 100644 Sources/ProcessOut/Sources/Core/Formatters/PhoneNumber/MetadataProvider/POPhoneNumberMetadataProvider.swift delete mode 100644 Sources/ProcessOut/Sources/Core/Markdown/MarkdownNodeFactory.swift delete mode 100644 Sources/ProcessOut/Sources/Core/Markdown/Nodes/MarkdownBlockQuote.swift delete mode 100644 Sources/ProcessOut/Sources/Core/Markdown/Nodes/MarkdownCodeBlock.swift delete mode 100644 Sources/ProcessOut/Sources/Core/Markdown/Nodes/MarkdownCodeSpan.swift delete mode 100644 Sources/ProcessOut/Sources/Core/Markdown/Nodes/MarkdownDocument.swift delete mode 100644 Sources/ProcessOut/Sources/Core/Markdown/Nodes/MarkdownEmphasis.swift delete mode 100644 Sources/ProcessOut/Sources/Core/Markdown/Nodes/MarkdownHeading.swift delete mode 100644 Sources/ProcessOut/Sources/Core/Markdown/Nodes/MarkdownLinebreak.swift delete mode 100644 Sources/ProcessOut/Sources/Core/Markdown/Nodes/MarkdownLink.swift delete mode 100644 Sources/ProcessOut/Sources/Core/Markdown/Nodes/MarkdownList.swift delete mode 100644 Sources/ProcessOut/Sources/Core/Markdown/Nodes/MarkdownListItem.swift delete mode 100644 Sources/ProcessOut/Sources/Core/Markdown/Nodes/MarkdownNode.swift delete mode 100644 Sources/ProcessOut/Sources/Core/Markdown/Nodes/MarkdownParagraph.swift delete mode 100644 Sources/ProcessOut/Sources/Core/Markdown/Nodes/MarkdownSoftbreak.swift delete mode 100644 Sources/ProcessOut/Sources/Core/Markdown/Nodes/MarkdownStrong.swift delete mode 100644 Sources/ProcessOut/Sources/Core/Markdown/Nodes/MarkdownText.swift delete mode 100644 Sources/ProcessOut/Sources/Core/Markdown/Nodes/MarkdownThematicBreak.swift delete mode 100644 Sources/ProcessOut/Sources/Core/Markdown/Nodes/MarkdownUnknown.swift delete mode 100644 Sources/ProcessOut/Sources/Core/Markdown/Visitor/MarkdownDebugDescriptionPrinter.swift delete mode 100644 Sources/ProcessOut/Sources/Core/Markdown/Visitor/MarkdownVisitor.swift delete mode 100644 Sources/ProcessOut/Sources/Core/Markers/POAutoAsync.swift delete mode 100644 Sources/ProcessOut/Sources/Core/Markers/POAutoCompletion.swift delete mode 100644 Sources/ProcessOut/Sources/Core/PropertyWrappers/ImmutableNullHashable.swift delete mode 100644 Sources/ProcessOut/Sources/Core/PropertyWrappers/ReferenceWrapper.swift delete mode 100644 Sources/ProcessOut/Sources/Generated/Files+Generated.swift delete mode 100644 Sources/ProcessOut/Sources/Generated/Fonts+Generated.swift delete mode 100644 Sources/ProcessOut/Sources/Legacy/APMTokenReturn.swift delete mode 100644 Sources/ProcessOut/Sources/Legacy/ApiResponse.swift delete mode 100644 Sources/ProcessOut/Sources/Legacy/AuthentificationChallengeData.swift delete mode 100644 Sources/ProcessOut/Sources/Legacy/AuthorizationRequest.swift delete mode 100644 Sources/ProcessOut/Sources/Legacy/AuthorizationResult.swift delete mode 100644 Sources/ProcessOut/Sources/Legacy/CardPaymentWebView.swift delete mode 100644 Sources/ProcessOut/Sources/Legacy/CardTokenWebView.swift delete mode 100644 Sources/ProcessOut/Sources/Legacy/CustomerAction.swift delete mode 100644 Sources/ProcessOut/Sources/Legacy/CustomerActionHandler.swift delete mode 100644 Sources/ProcessOut/Sources/Legacy/DirectoryServerData.swift delete mode 100644 Sources/ProcessOut/Sources/Legacy/FingerPrintWebViewSchemeHandler.swift delete mode 100644 Sources/ProcessOut/Sources/Legacy/FingerprintWebView.swift delete mode 100644 Sources/ProcessOut/Sources/Legacy/GatewayConfiguration.swift delete mode 100644 Sources/ProcessOut/Sources/Legacy/IncrementAuthorizationRequest.swift delete mode 100644 Sources/ProcessOut/Sources/Legacy/MiscGatewayRequest.swift delete mode 100644 Sources/ProcessOut/Sources/Legacy/ProcessOutException.swift delete mode 100644 Sources/ProcessOut/Sources/Legacy/ProcessOutLegacyApi.swift delete mode 100644 Sources/ProcessOut/Sources/Legacy/ProcessOutRequestManager.swift delete mode 100644 Sources/ProcessOut/Sources/Legacy/ProcessOutWebView.swift delete mode 100644 Sources/ProcessOut/Sources/Legacy/RetryPolicy.swift delete mode 100644 Sources/ProcessOut/Sources/Legacy/ThreeDSFingerprintResponse.swift delete mode 100644 Sources/ProcessOut/Sources/Legacy/ThreeDSHandler.swift delete mode 100644 Sources/ProcessOut/Sources/Legacy/ThreeDSTestHandler.swift delete mode 100644 Sources/ProcessOut/Sources/Legacy/TokenRequest.swift delete mode 100644 Sources/ProcessOut/Sources/Legacy/TokenizationResult.swift delete mode 100644 Sources/ProcessOut/Sources/UI/Modules/3DSRedirect/PO3DSRedirectViewControllerBuilder.swift delete mode 100644 Sources/ProcessOut/Sources/UI/Modules/3DSRedirect/ThreeDSRedirectSafariViewModelDelegate.swift delete mode 100644 Sources/ProcessOut/Sources/UI/Modules/AlternativePaymentMethod/AlternativePaymentMethodSafariViewModelDelegate.swift delete mode 100644 Sources/ProcessOut/Sources/UI/Modules/AlternativePaymentMethod/POAlternativePaymentMethodViewControllerBuilder.swift delete mode 100644 Sources/ProcessOut/Sources/UI/Modules/NativeAlternativePaymentMethod/Builder/PONativeAlternativePaymentMethodViewControllerBuilder.swift delete mode 100644 Sources/ProcessOut/Sources/UI/Modules/NativeAlternativePaymentMethod/Interactor/PODefaultNativeAlternativePaymentMethodInteractor.swift delete mode 100644 Sources/ProcessOut/Sources/UI/Modules/NativeAlternativePaymentMethod/Interactor/PONativeAlternativePaymentMethodDelegate.swift delete mode 100644 Sources/ProcessOut/Sources/UI/Modules/NativeAlternativePaymentMethod/Interactor/PONativeAlternativePaymentMethodInteractor.swift delete mode 100644 Sources/ProcessOut/Sources/UI/Modules/NativeAlternativePaymentMethod/Interactor/PONativeAlternativePaymentMethodInteractorConfiguration.swift delete mode 100644 Sources/ProcessOut/Sources/UI/Modules/NativeAlternativePaymentMethod/Interactor/PONativeAlternativePaymentMethodInteractorState.swift delete mode 100644 Sources/ProcessOut/Sources/UI/Modules/NativeAlternativePaymentMethod/Models/PONativeAlternativePaymentMethodConfiguration.swift delete mode 100644 Sources/ProcessOut/Sources/UI/Modules/NativeAlternativePaymentMethod/Models/PONativeAlternativePaymentMethodEvent.swift delete mode 100644 Sources/ProcessOut/Sources/UI/Modules/NativeAlternativePaymentMethod/Models/Style/PONativeAlternativePaymentMethodActionsStyle.swift delete mode 100644 Sources/ProcessOut/Sources/UI/Modules/NativeAlternativePaymentMethod/Models/Style/PONativeAlternativePaymentMethodBackgroundStyle.swift delete mode 100644 Sources/ProcessOut/Sources/UI/Modules/NativeAlternativePaymentMethod/Models/Style/PONativeAlternativePaymentMethodStyle.swift delete mode 100644 Sources/ProcessOut/Sources/UI/Modules/NativeAlternativePaymentMethod/Symbols/StringResource+NativeAlternativePayment.swift delete mode 100644 Sources/ProcessOut/Sources/UI/Modules/NativeAlternativePaymentMethod/View/Cells/NativeAlternativePaymentMethodCell.swift delete mode 100644 Sources/ProcessOut/Sources/UI/Modules/NativeAlternativePaymentMethod/View/Cells/NativeAlternativePaymentMethodCodeInputCell.swift delete mode 100644 Sources/ProcessOut/Sources/UI/Modules/NativeAlternativePaymentMethod/View/Cells/NativeAlternativePaymentMethodInputCell.swift delete mode 100644 Sources/ProcessOut/Sources/UI/Modules/NativeAlternativePaymentMethod/View/Cells/NativeAlternativePaymentMethodLoaderCell.swift delete mode 100644 Sources/ProcessOut/Sources/UI/Modules/NativeAlternativePaymentMethod/View/Cells/NativeAlternativePaymentMethodPickerCell.swift delete mode 100644 Sources/ProcessOut/Sources/UI/Modules/NativeAlternativePaymentMethod/View/Cells/NativeAlternativePaymentMethodRadioCell.swift delete mode 100644 Sources/ProcessOut/Sources/UI/Modules/NativeAlternativePaymentMethod/View/Cells/Submitted/NativeAlternativePaymentMethodSubmittedCell.swift delete mode 100644 Sources/ProcessOut/Sources/UI/Modules/NativeAlternativePaymentMethod/View/Cells/Submitted/NativeAlternativePaymentMethodSubmittedCellStyle.swift delete mode 100644 Sources/ProcessOut/Sources/UI/Modules/NativeAlternativePaymentMethod/View/NativeAlternativePaymentMethodViewController.swift delete mode 100644 Sources/ProcessOut/Sources/UI/Modules/NativeAlternativePaymentMethod/ViewModel/DefaultNativeAlternativePaymentMethodViewModel.swift delete mode 100644 Sources/ProcessOut/Sources/UI/Modules/NativeAlternativePaymentMethod/ViewModel/NativeAlternativePaymentMethodViewModel.swift delete mode 100644 Sources/ProcessOut/Sources/UI/Modules/NativeAlternativePaymentMethod/ViewModel/NativeAlternativePaymentMethodViewModelState.swift delete mode 100644 Sources/ProcessOut/Sources/UI/Modules/Safari/DefaultSafariViewModel.swift delete mode 100644 Sources/ProcessOut/Sources/UI/Modules/Safari/DefaultSafariViewModelConfiguration.swift delete mode 100644 Sources/ProcessOut/Sources/UI/Modules/Safari/DefaultSafariViewModelDelegate.swift delete mode 100644 Sources/ProcessOut/Sources/UI/Modules/Safari/SafariViewController+Extensions.swift delete mode 100644 Sources/ProcessOut/Sources/UI/Shared/Architecture/View/BaseViewController.swift delete mode 100644 Sources/ProcessOut/Sources/UI/Shared/Architecture/ViewModel/BaseViewModel.swift delete mode 100644 Sources/ProcessOut/Sources/UI/Shared/Architecture/ViewModel/ViewModel.swift delete mode 100644 Sources/ProcessOut/Sources/UI/Shared/DesignSystem/CollectionReusableViews/Error/CollectionViewErrorCell.swift delete mode 100644 Sources/ProcessOut/Sources/UI/Shared/DesignSystem/CollectionReusableViews/Error/CollectionViewErrorViewModel.swift delete mode 100644 Sources/ProcessOut/Sources/UI/Shared/DesignSystem/CollectionReusableViews/Radio/CollectionViewRadioCell.swift delete mode 100644 Sources/ProcessOut/Sources/UI/Shared/DesignSystem/CollectionReusableViews/Radio/CollectionViewRadioViewModel.swift delete mode 100644 Sources/ProcessOut/Sources/UI/Shared/DesignSystem/CollectionReusableViews/SectionHeader/CollectionViewSectionHeaderView.swift delete mode 100644 Sources/ProcessOut/Sources/UI/Shared/DesignSystem/CollectionReusableViews/SectionHeader/CollectionViewSectionHeaderViewModel.swift delete mode 100644 Sources/ProcessOut/Sources/UI/Shared/DesignSystem/CollectionReusableViews/Separator/CollectionViewSeparatorView.swift delete mode 100644 Sources/ProcessOut/Sources/UI/Shared/DesignSystem/CollectionReusableViews/Title/CollectionViewTitleCell.swift delete mode 100644 Sources/ProcessOut/Sources/UI/Shared/DesignSystem/CollectionReusableViews/Title/CollectionViewTitleViewModel.swift delete mode 100644 Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Extensions/UIActivityIndicatorView+Extensions.swift delete mode 100644 Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Extensions/UIView+Style.swift delete mode 100644 Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Layouts/Center/CollectionViewCenterLayout.swift delete mode 100644 Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Layouts/Center/CollectionViewDelegateCenterLayout.swift delete mode 100644 Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Styles/Input/POInputStateStyle.swift delete mode 100644 Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Styles/Input/POInputStyle.swift delete mode 100644 Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Styles/POBorderStyle.swift delete mode 100644 Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Styles/POShadowStyle.swift delete mode 100644 Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Typography/AttributedStringBuilder.swift delete mode 100644 Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Typography/AttributedStringMarkdownVisitor.swift delete mode 100644 Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Typography/POTextStyle.swift delete mode 100644 Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Typography/POTypography.swift delete mode 100644 Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Views/ActionsContainer/ActionsContainerView.swift delete mode 100644 Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Views/ActionsContainer/ActionsContainerViewModel.swift delete mode 100644 Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Views/ActionsContainer/POActionsContainerStyle.swift delete mode 100644 Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Views/ActivityIndicator/ActivityIndicatorViewFactory.swift delete mode 100644 Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Views/ActivityIndicator/POActivityIndicatorStyle.swift delete mode 100644 Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Views/ActivityIndicator/POActivityIndicatorView.swift delete mode 100644 Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Views/Button/Button.swift delete mode 100644 Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Views/Button/POButtonStateStyle.swift delete mode 100644 Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Views/Button/POButtonStyle.swift delete mode 100644 Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Views/CodeTextField/CodeTextField.swift delete mode 100644 Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Views/CodeTextField/CodeTextFieldCarretPosition.swift delete mode 100644 Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Views/CodeTextField/CodeTextFieldComponentView.swift delete mode 100644 Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Views/CodeTextField/CodeTextFieldDelegate.swift delete mode 100644 Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Views/Picker/Picker.swift delete mode 100644 Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Views/Picker/PickerViewModel.swift delete mode 100644 Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Views/RadioButton/PORadioButtonKnobStateStyle.swift delete mode 100644 Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Views/RadioButton/PORadioButtonStateStyle.swift delete mode 100644 Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Views/RadioButton/PORadioButtonStyle.swift delete mode 100644 Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Views/RadioButton/RadioButton.swift delete mode 100644 Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Views/RadioButton/RadioButtonKnobView.swift delete mode 100644 Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Views/RadioButton/RadioButtonViewModel.swift delete mode 100644 Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Views/TextField/TextFieldContainerView.swift delete mode 100644 Sources/ProcessOut/Sources/UI/Shared/Extensions/Array+NSAttributedString.swift delete mode 100644 Sources/ProcessOut/Sources/UI/Shared/Extensions/NSLayoutConstraint+Extensions.swift delete mode 100644 Sources/ProcessOut/Sources/UI/Shared/Extensions/UICollectionView+Extensions.swift delete mode 100644 Sources/ProcessOut/Sources/UI/Shared/Extensions/UIImageView+Extensions.swift delete mode 100644 Sources/ProcessOut/Sources/UI/Shared/Extensions/UITraitCollection+ColorAppearance.swift delete mode 100644 Sources/ProcessOut/Sources/UI/Shared/Extensions/UIView+Extensions.swift delete mode 100644 Sources/ProcessOut/Sources/UI/Shared/Utils/CollectionDiffableDataSource/CollectionViewDiffableDataSource.swift delete mode 100644 Sources/ProcessOut/Sources/UI/Shared/Utils/CollectionDiffableDataSource/DiffableDataSourceSnapshot.swift delete mode 100644 Sources/ProcessOut/Sources/UI/Shared/Utils/CollectionReusableViewSizeProvider.swift delete mode 100644 Sources/ProcessOut/Sources/UI/Shared/Utils/KeyboardNotification.swift delete mode 100644 Sources/ProcessOut/Sources/UI/Shared/Utils/PassthroughView.swift delete mode 100644 Sources/ProcessOut/Sources/UI/Shared/Utils/Reusable.swift delete mode 100644 Sources/ProcessOut/Sources/UI/Shared/Utils/TextFieldUtils.swift delete mode 100644 Sources/ProcessOut/swiftgen.yml rename Sources/{ProcessOut => ProcessOutUI}/Resources/PhoneNumberMetadata/PhoneNumberMetadata.json (100%) rename Sources/{ProcessOut => ProcessOutUI}/Resources/PhoneNumberMetadata/PhoneNumberMetadata.version (100%) rename Sources/{ProcessOut => ProcessOutUI}/Sources/Core/Extensions/String+Extensions.swift (100%) rename Sources/{ProcessOut => ProcessOutUI}/Sources/Core/Extensions/StringProtocol+Extensions.swift (89%) rename Sources/{ProcessOut/Sources/Core/Formatters/CardExpiration/POCardExpirationFormatter.swift => ProcessOutUI/Sources/Core/Formatters/CardExpiration/CardExpirationFormatter.swift} (90%) rename Sources/{ProcessOut/Sources/Core/Formatters/CardNumber/POCardNumberFormatter.swift => ProcessOutUI/Sources/Core/Formatters/CardNumber/CardNumberFormatter.swift} (92%) rename Sources/{ProcessOut/Sources/Core/Formatters/PhoneNumber/MetadataProvider/PODefaultPhoneNumberMetadataProvider.swift => ProcessOutUI/Sources/Core/Formatters/PhoneNumber/MetadataProvider/DefaultPhoneNumberMetadataProvider.swift} (63%) create mode 100644 Sources/ProcessOutUI/Sources/Core/Formatters/PhoneNumber/MetadataProvider/PhoneNumberFormat.swift create mode 100644 Sources/ProcessOutUI/Sources/Core/Formatters/PhoneNumber/MetadataProvider/PhoneNumberMetadata.swift create mode 100644 Sources/ProcessOutUI/Sources/Core/Formatters/PhoneNumber/MetadataProvider/PhoneNumberMetadataProvider.swift rename Sources/{ProcessOut/Sources/Core/Formatters/PhoneNumber/POPhoneNumberFormatter.swift => ProcessOutUI/Sources/Core/Formatters/PhoneNumber/PhoneNumberFormatter.swift} (88%) rename Sources/{ProcessOut/Sources/Core => ProcessOutUI/Sources/Core/Formatters}/RegexProvider/RegexProvider.swift (100%) rename Sources/{ProcessOut/Sources/Core/Formatters/Utils/POFormattingUtils.swift => ProcessOutUI/Sources/Core/Formatters/Utils/FormattingUtils.swift} (96%) delete mode 100644 Sources/ProcessOutUI/Sources/Core/Utils/POStringResource+Extension.swift rename Sources/{ProcessOut/Sources/Core/Utils/POStringResource.swift => ProcessOutUI/Sources/Core/Utils/StringResource.swift} (88%) 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 03e9671c1e5a78a3835fbee5baec661949778149..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 335056 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 diff --git a/Sources/ProcessOut/Resources/Fonts/WorkSans.ttf b/Sources/ProcessOut/Resources/Fonts/WorkSans.ttf deleted file mode 100644 index 0a2b4de9245fbeaa91b698ea12bc6a9a425930d6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 227432 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 diff --git a/Sources/ProcessOut/Resources/Localizable.xcstrings b/Sources/ProcessOut/Resources/Localizable.xcstrings index 7a2ae5b9d..19c0adfac 100644 --- a/Sources/ProcessOut/Resources/Localizable.xcstrings +++ b/Sources/ProcessOut/Resources/Localizable.xcstrings @@ -2,6 +2,7 @@ "sourceLanguage" : "en", "strings" : { "native-alternative-payment.cancel-button.title" : { + "extractionState" : "stale", "localizations" : { "ar" : { "stringUnit" : { @@ -36,6 +37,7 @@ } }, "native-alternative-payment.email.placeholder" : { + "extractionState" : "stale", "localizations" : { "ar" : { "stringUnit" : { @@ -70,6 +72,7 @@ } }, "native-alternative-payment.error.invalid-email" : { + "extractionState" : "stale", "localizations" : { "ar" : { "stringUnit" : { @@ -104,6 +107,7 @@ } }, "native-alternative-payment.error.invalid-length-%d" : { + "extractionState" : "stale", "localizations" : { "ar" : { "variations" : { @@ -198,6 +202,7 @@ } }, "native-alternative-payment.error.invalid-number" : { + "extractionState" : "stale", "localizations" : { "ar" : { "stringUnit" : { @@ -232,6 +237,7 @@ } }, "native-alternative-payment.error.invalid-phone" : { + "extractionState" : "stale", "localizations" : { "ar" : { "stringUnit" : { @@ -266,6 +272,7 @@ } }, "native-alternative-payment.error.invalid-value" : { + "extractionState" : "stale", "localizations" : { "ar" : { "stringUnit" : { @@ -300,6 +307,7 @@ } }, "native-alternative-payment.error.required-parameter" : { + "extractionState" : "stale", "localizations" : { "ar" : { "stringUnit" : { @@ -334,6 +342,7 @@ } }, "native-alternative-payment.phone.placeholder" : { + "extractionState" : "stale", "localizations" : { "ar" : { "stringUnit" : { @@ -368,6 +377,7 @@ } }, "native-alternative-payment.submit-button.default-title" : { + "extractionState" : "stale", "localizations" : { "ar" : { "stringUnit" : { @@ -402,6 +412,7 @@ } }, "native-alternative-payment.submit-button.title" : { + "extractionState" : "stale", "localizations" : { "ar" : { "stringUnit" : { @@ -436,6 +447,7 @@ } }, "native-alternative-payment.success.message" : { + "extractionState" : "stale", "localizations" : { "ar" : { "stringUnit" : { @@ -470,6 +482,7 @@ } }, "native-alternative-payment.title" : { + "extractionState" : "stale", "localizations" : { "ar" : { "stringUnit" : { @@ -504,6 +517,7 @@ } }, "test-3ds.challenge.accept" : { + "extractionState" : "stale", "localizations" : { "ar" : { "stringUnit" : { @@ -538,6 +552,7 @@ } }, "test-3ds.challenge.reject" : { + "extractionState" : "stale", "localizations" : { "ar" : { "stringUnit" : { @@ -572,6 +587,7 @@ } }, "test-3ds.challenge.title" : { + "extractionState" : "stale", "localizations" : { "ar" : { "stringUnit" : { diff --git a/Sources/ProcessOut/Sources/Api/ProcessOut.swift b/Sources/ProcessOut/Sources/Api/ProcessOut.swift index 509f0e1c5..61ea1139d 100644 --- a/Sources/ProcessOut/Sources/Api/ProcessOut.swift +++ b/Sources/ProcessOut/Sources/Api/ProcessOut.swift @@ -260,7 +260,6 @@ extension ProcessOut { shared.logger.debug("ProcessOut can be configured only once, ignored") } } else { - Self.prewarm() _shared.withLock { instance in instance = ProcessOut(configuration: configuration) } @@ -271,14 +270,6 @@ extension ProcessOut { // MARK: - Private Properties private static let _shared = POUnfairlyLocked(wrappedValue: nil) - - // MARK: - Private Methods - - @MainActor - private static func prewarm() { - FontFamily.registerAllCustomFonts() - PODefaultPhoneNumberMetadataProvider.shared.prewarm() - } } // 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 c19ebb228..000000000 --- a/Sources/ProcessOut/Sources/Api/Utils/Test3DS/POTest3DSService.swift +++ /dev/null @@ -1,76 +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. - @MainActor - public unowned var viewController: UIViewController! // swiftlint:disable:this implicitly_unwrapped_optional - - // MARK: - PO3DSService - - public func authenticationRequest( - configuration: PO3DS2Configuration, - completion: @escaping @Sendable (Result) -> Void - ) { - let request = PO3DS2AuthenticationRequest( - deviceData: "", - sdkAppId: "", - sdkEphemeralPublicKey: "{}", - sdkReferenceNumber: "", - sdkTransactionId: "" - ) - completion(.success(request)) - } - - public func handle(challenge: PO3DS2Challenge, completion: @escaping @Sendable (Result) -> Void) { - MainActor.assumeIsolated { - 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 @Sendable (Result) -> Void) { - MainActor.assumeIsolated { - 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/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/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/POPhoneNumberFormat.swift b/Sources/ProcessOut/Sources/Core/Formatters/PhoneNumber/MetadataProvider/POPhoneNumberFormat.swift deleted file mode 100644 index 2958aeea1..000000000 --- a/Sources/ProcessOut/Sources/Core/Formatters/PhoneNumber/MetadataProvider/POPhoneNumberFormat.swift +++ /dev/null @@ -1,19 +0,0 @@ -// -// POPhoneNumberFormat.swift -// ProcessOut -// -// Created by Andrii Vysotskyi on 16.03.2023. -// - -@_spi(PO) -public struct POPhoneNumberFormat: Decodable, Sendable { - - /// 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/MetadataProvider/POPhoneNumberMetadata.swift b/Sources/ProcessOut/Sources/Core/Formatters/PhoneNumber/MetadataProvider/POPhoneNumberMetadata.swift deleted file mode 100644 index bb4fdb35f..000000000 --- a/Sources/ProcessOut/Sources/Core/Formatters/PhoneNumber/MetadataProvider/POPhoneNumberMetadata.swift +++ /dev/null @@ -1,16 +0,0 @@ -// -// POPhoneNumberMetadata.swift -// ProcessOut -// -// Created by Andrii Vysotskyi on 16.03.2023. -// - -@_spi(PO) -public struct POPhoneNumberMetadata: Decodable, Sendable { - - /// Country code. - public let countryCode: String - - /// Available formats. - public let formats: [POPhoneNumberFormat] -} 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 9d8e25963..000000000 --- a/Sources/ProcessOut/Sources/Core/Formatters/PhoneNumber/MetadataProvider/POPhoneNumberMetadataProvider.swift +++ /dev/null @@ -1,13 +0,0 @@ -// -// POPhoneNumberMetadataProvider.swift -// ProcessOut -// -// Created by Andrii Vysotskyi on 23.03.2023. -// - -@_spi(PO) -public protocol POPhoneNumberMetadataProvider: Sendable { - - /// Returns metadata for given country code if any. - func metadata(for countryCode: String) -> POPhoneNumberMetadata? -} diff --git a/Sources/ProcessOut/Sources/Core/Markdown/MarkdownNodeFactory.swift b/Sources/ProcessOut/Sources/Core/Markdown/MarkdownNodeFactory.swift deleted file mode 100644 index 05e84ffd5..000000000 --- a/Sources/ProcessOut/Sources/Core/Markdown/MarkdownNodeFactory.swift +++ /dev/null @@ -1,53 +0,0 @@ -// -// MarkdownNodeFactory.swift -// ProcessOut -// -// Created by Andrii Vysotskyi on 13.06.2023. -// - -@_implementationOnly import cmark - -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 { - assertionFailure("Unknown node type: \(nodeType)") - 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 index cf3fe646b..bace98f06 100644 --- a/Sources/ProcessOut/Sources/Core/Markdown/MarkdownParser.swift +++ b/Sources/ProcessOut/Sources/Core/Markdown/MarkdownParser.swift @@ -10,18 +10,6 @@ import Foundation 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") - } - let markdownDocument = MarkdownDocument(cmarkNode: document) - cmark_node_free(document) - return markdownDocument - } - /// Escapes given plain text so it can be represented as is, in markdown. static func escaped(plainText: String) -> String { var markdown = String() 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 45212855c..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. -// - -@_implementationOnly import cmark - -final class MarkdownBlockQuote: MarkdownBaseNode, @unchecked Sendable { - - 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 653d97b27..000000000 --- a/Sources/ProcessOut/Sources/Core/Markdown/Nodes/MarkdownCodeBlock.swift +++ /dev/null @@ -1,33 +0,0 @@ -// -// MarkdownCodeBlock.swift -// ProcessOut -// -// Created by Andrii Vysotskyi on 15.06.2023. -// - -@_implementationOnly import cmark - -final class MarkdownCodeBlock: MarkdownBaseNode, @unchecked Sendable { - - /// 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 - } - - 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 0c1dc39d7..000000000 --- a/Sources/ProcessOut/Sources/Core/Markdown/Nodes/MarkdownCodeSpan.swift +++ /dev/null @@ -1,34 +0,0 @@ -// -// MarkdownCodeSpan.swift -// ProcessOut -// -// Created by Andrii Vysotskyi on 15.06.2023. -// - -@_implementationOnly import cmark - -final class MarkdownCodeSpan: MarkdownBaseNode, @unchecked Sendable { - - /// 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 - } - - 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 7e650a6e6..000000000 --- a/Sources/ProcessOut/Sources/Core/Markdown/Nodes/MarkdownDocument.swift +++ /dev/null @@ -1,19 +0,0 @@ -// -// MarkdownDocument.swift -// ProcessOut -// -// Created by Andrii Vysotskyi on 11.06.2023. -// - -@_implementationOnly import cmark - -final class MarkdownDocument: MarkdownBaseNode, @unchecked Sendable { - - 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 ccfc84b3c..000000000 --- a/Sources/ProcessOut/Sources/Core/Markdown/Nodes/MarkdownEmphasis.swift +++ /dev/null @@ -1,19 +0,0 @@ -// -// MarkdownEmphasis.swift -// ProcessOut -// -// Created by Andrii Vysotskyi on 12.06.2023. -// - -@_implementationOnly import cmark - -final class MarkdownEmphasis: MarkdownBaseNode, @unchecked Sendable { - - 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 07f1c41c1..000000000 --- a/Sources/ProcessOut/Sources/Core/Markdown/Nodes/MarkdownHeading.swift +++ /dev/null @@ -1,28 +0,0 @@ -// -// MarkdownHeading.swift -// ProcessOut -// -// Created by Andrii Vysotskyi on 15.06.2023. -// - -@_implementationOnly import cmark - -final class MarkdownHeading: MarkdownBaseNode, @unchecked Sendable { - - 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 - } - - 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 f0388e79a..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. -// - -@_implementationOnly import cmark - -final class MarkdownLinebreak: MarkdownBaseNode, @unchecked Sendable { - - 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 bf3778b22..000000000 --- a/Sources/ProcessOut/Sources/Core/Markdown/Nodes/MarkdownLink.swift +++ /dev/null @@ -1,32 +0,0 @@ -// -// MarkdownLink.swift -// ProcessOut -// -// Created by Andrii Vysotskyi on 15.06.2023. -// - -@_implementationOnly import cmark - -final class MarkdownLink: MarkdownBaseNode, @unchecked Sendable { - - let url: String? - - // MARK: - MarkdownBaseNode - - required init(cmarkNode: MarkdownBaseNode.CmarkNode, validatesType: Bool = true) { - if let url = cmarkNode.pointee.as.link.url { - self.url = String(cString: url) - } else { - url = nil - } - super.init(cmarkNode: cmarkNode, validatesType: validatesType) - } - - 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 44d73e36a..000000000 --- a/Sources/ProcessOut/Sources/Core/Markdown/Nodes/MarkdownList.swift +++ /dev/null @@ -1,64 +0,0 @@ -// -// MarkdownList.swift -// ProcessOut -// -// Created by Andrii Vysotskyi on 12.06.2023. -// - -@_implementationOnly import cmark - -final class MarkdownList: MarkdownBaseNode, @unchecked Sendable { - - enum ListType { - - /// Ordered - case ordered(delimiter: Character, startIndex: Int) - - /// Bullet aka unordered list. - case bullet(marker: Character) - } - - /// 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 UInt32(listNode.list_type) { - case CMARK_BULLET_LIST.rawValue: - let marker = Character(Unicode.Scalar(listNode.bullet_char)) - return .bullet(marker: marker) - case CMARK_ORDERED_LIST.rawValue: - 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)") - } - } -} 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 d45e4fe16..000000000 --- a/Sources/ProcessOut/Sources/Core/Markdown/Nodes/MarkdownListItem.swift +++ /dev/null @@ -1,19 +0,0 @@ -// -// MarkdownListItem.swift -// ProcessOut -// -// Created by Andrii Vysotskyi on 12.06.2023. -// - -@_implementationOnly import cmark - -final class MarkdownListItem: MarkdownBaseNode, @unchecked Sendable { - - 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 da1890c5e..000000000 --- a/Sources/ProcessOut/Sources/Core/Markdown/Nodes/MarkdownNode.swift +++ /dev/null @@ -1,45 +0,0 @@ -// -// MarkdownBaseNode.swift -// ProcessOut -// -// Created by Andrii Vysotskyi on 12.06.2023. -// - -@_implementationOnly import cmark - -class MarkdownBaseNode: @unchecked Sendable { - - 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.children = Self.children(of: cmarkNode) - } - - /// Returns node children. - 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 { - let child = MarkdownNodeFactory(cmarkNode: cmarkNode).create() - children.append(child) - cmarkChild = cmarkNode.pointee.next - } - return children - } -} 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 1dc9f4c65..000000000 --- a/Sources/ProcessOut/Sources/Core/Markdown/Nodes/MarkdownParagraph.swift +++ /dev/null @@ -1,19 +0,0 @@ -// -// MarkdownParagraph.swift -// ProcessOut -// -// Created by Andrii Vysotskyi on 12.06.2023. -// - -@_implementationOnly import cmark - -final class MarkdownParagraph: MarkdownBaseNode, @unchecked Sendable { - - 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 c049db6e9..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. -// - -@_implementationOnly import cmark - -final class MarkdownSoftbreak: MarkdownBaseNode, @unchecked Sendable { - - 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 e9f960a85..000000000 --- a/Sources/ProcessOut/Sources/Core/Markdown/Nodes/MarkdownStrong.swift +++ /dev/null @@ -1,19 +0,0 @@ -// -// MarkdownStrong.swift -// ProcessOut -// -// Created by Andrii Vysotskyi on 12.06.2023. -// - -@_implementationOnly import cmark - -final class MarkdownStrong: MarkdownBaseNode, @unchecked Sendable { - - 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 574981d74..000000000 --- a/Sources/ProcessOut/Sources/Core/Markdown/Nodes/MarkdownText.swift +++ /dev/null @@ -1,34 +0,0 @@ -// -// MarkdownText.swift -// ProcessOut -// -// Created by Andrii Vysotskyi on 12.06.2023. -// - -@_implementationOnly import cmark - -final class MarkdownText: MarkdownBaseNode, @unchecked Sendable { - - /// 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 - } - - 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 f802f9b01..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. -// - -@_implementationOnly import cmark - -final class MarkdownThematicBreak: MarkdownBaseNode, @unchecked Sendable { - - 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 654488bbc..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, @unchecked Sendable { - - 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 a2b1797ac..000000000 --- a/Sources/ProcessOut/Sources/Core/Markdown/Visitor/MarkdownDebugDescriptionPrinter.swift +++ /dev/null @@ -1,144 +0,0 @@ -// -// MarkdownDebugDescriptionPrinter.swift -// ProcessOut -// -// Created by Andrii Vysotskyi on 14.06.2023. -// - -#if DEBUG - -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 { - description(node: codeBlock, nodeName: "Code Block", 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) - } -} - -#endif 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 4d5c76ce7..000000000 --- a/Sources/ProcessOut/Sources/Core/PropertyWrappers/ImmutableNullHashable.swift +++ /dev/null @@ -1,22 +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 - } -} - -extension ImmutableNullHashable: Sendable where Value: Sendable { } 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/Generated/Files+Generated.swift b/Sources/ProcessOut/Sources/Generated/Files+Generated.swift deleted file mode 100644 index e54a3f827..000000000 --- a/Sources/ProcessOut/Sources/Generated/Files+Generated.swift +++ /dev/null @@ -1,53 +0,0 @@ -// swiftlint:disable all -// Generated using SwiftGen — https://github.com/SwiftGen/SwiftGen - -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 2c7a6fe1a..000000000 --- a/Sources/ProcessOut/Sources/Generated/Fonts+Generated.swift +++ /dev/null @@ -1,144 +0,0 @@ -// swiftlint:disable all -// Generated using SwiftGen — https://github.com/SwiftGen/SwiftGen - -#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/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 eef951d33..000000000 --- a/Sources/ProcessOut/Sources/Legacy/CardPaymentWebView.swift +++ /dev/null @@ -1,21 +0,0 @@ -// -// CardPaymentWebView.swift -// ProcessOut -// -// Created by Jeremy Lejoux on 30/09/2019. -// - -import Foundation - -@available(*, deprecated) -@preconcurrency -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 aa2d03db9..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.apiBaseUrl.absoluteString - } - - internal static var CheckoutUrl: String { - ProcessOut.shared.configuration.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 f340a710d..000000000 --- a/Sources/ProcessOut/Sources/Legacy/ProcessOutRequestManager.swift +++ /dev/null @@ -1,130 +0,0 @@ -// -// ProcessOutRequest.swift -// ProcessOut -// -// Created by Mauro Vime Castillo on 15/2/21. -// - -import Foundation - -@available(*, deprecated) -@preconcurrency -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 4bbd2eca8..000000000 --- a/Sources/ProcessOut/Sources/Legacy/ProcessOutWebView.swift +++ /dev/null @@ -1,70 +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.") -@preconcurrency -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/UI/Modules/3DSRedirect/PO3DSRedirectViewControllerBuilder.swift b/Sources/ProcessOut/Sources/UI/Modules/3DSRedirect/PO3DSRedirectViewControllerBuilder.swift deleted file mode 100644 index 70c8724a5..000000000 --- a/Sources/ProcessOut/Sources/UI/Modules/3DSRedirect/PO3DSRedirectViewControllerBuilder.swift +++ /dev/null @@ -1,88 +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 -@MainActor -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 6da19432e..000000000 --- a/Sources/ProcessOut/Sources/UI/Modules/AlternativePaymentMethod/POAlternativePaymentMethodViewControllerBuilder.swift +++ /dev/null @@ -1,100 +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 -@MainActor -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 c5782bb65..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.") -@MainActor -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.") - } - var logger: POLogger = ProcessOut.shared.logger - logger[attributeKey: .invoiceId] = invoiceId - logger[attributeKey: .gatewayConfigurationId] = gatewayConfigurationId - let interactor = PODefaultNativeAlternativePaymentMethodInteractor( - invoicesService: ProcessOut.shared.invoices, - imagesRepository: ProcessOut.shared.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 c68f93f27..000000000 --- a/Sources/ProcessOut/Sources/UI/Modules/NativeAlternativePaymentMethod/Interactor/PODefaultNativeAlternativePaymentMethodInteractor.swift +++ /dev/null @@ -1,578 +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 - MainActor.assumeIsolated { - switch result { - case let .success(details): - self?.defaultValues(for: details.parameters) { values in - MainActor.assumeIsolated { - 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 - MainActor.assumeIsolated { - 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 - MainActor.assumeIsolated { - 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 - MainActor.assumeIsolated { - 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 - MainActor.assumeIsolated { - 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 - MainActor.assumeIsolated { - 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 @Sendable ([String: State.ParameterValue]) -> Void - ) { - guard let parameters, !parameters.isEmpty else { - completion([:]) - return - } - if let delegate { - delegate.nativeAlternativePaymentMethodDefaultValues(for: parameters) { [self] values in - MainActor.assumeIsolated { - 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 20996f46f..000000000 --- a/Sources/ProcessOut/Sources/UI/Modules/NativeAlternativePaymentMethod/Interactor/PONativeAlternativePaymentMethodDelegate.swift +++ /dev/null @@ -1,24 +0,0 @@ -// -// PONativeAlternativePaymentMethodDelegate.swift -// ProcessOut -// -// Created by Andrii Vysotskyi on 08.02.2023. -// - -/// Native alternative payment module delegate definition. -public protocol PONativeAlternativePaymentMethodDelegate: AnyObject, Sendable { - - /// Invoked when module emits event. - @MainActor - 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. - @MainActor - func nativeAlternativePaymentMethodDefaultValues( - for parameters: [PONativeAlternativePaymentMethodParameter], - completion: @escaping @Sendable ([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 21e45e569..000000000 --- a/Sources/ProcessOut/Sources/UI/Modules/NativeAlternativePaymentMethod/Interactor/PONativeAlternativePaymentMethodInteractor.swift +++ /dev/null @@ -1,39 +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) -@MainActor -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 979ab5a6b..000000000 --- a/Sources/ProcessOut/Sources/UI/Modules/NativeAlternativePaymentMethod/Interactor/PONativeAlternativePaymentMethodInteractorState.swift +++ /dev/null @@ -1,97 +0,0 @@ -// -// PONativeAlternativePaymentMethodInteractorState.swift -// ProcessOut -// -// Created by Andrii Vysotskyi on 23.11.2023. -// - -import UIKit - -@_spi(PO) -public enum PONativeAlternativePaymentMethodInteractorState { - - public struct ParameterValue: Sendable { - - /// 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) -} - -@available(*, unavailable) -extension PONativeAlternativePaymentMethodInteractorState: Sendable { } 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 68864a89b..000000000 --- a/Sources/ProcessOut/Sources/UI/Modules/NativeAlternativePaymentMethod/Models/Style/PONativeAlternativePaymentMethodBackgroundStyle.swift +++ /dev/null @@ -1,26 +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.") -@MainActor -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(resource: .Surface.level1) - self.success = success ?? UIColor(resource: .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 3da47284c..000000000 --- a/Sources/ProcessOut/Sources/UI/Modules/NativeAlternativePaymentMethod/Models/Style/PONativeAlternativePaymentMethodStyle.swift +++ /dev/null @@ -1,104 +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.") -@MainActor -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 - - @MainActor - private enum Constants { - static let title = POTextStyle(color: UIColor(resource: .Text.primary), typography: .Medium.title) - static let sectionTitle = POTextStyle( - color: UIColor(resource: .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(resource: .Text.error), typography: .Fixed.label) - static let actions = POActionsContainerStyle() - static let activityIndicator = POActivityIndicatorStyle.system( - .large, color: UIColor(resource: .Text.secondary) - ) - static let message = POTextStyle(color: UIColor(resource: .Text.primary), typography: .Fixed.body) - static let successMessage = POTextStyle(color: UIColor(resource: .Text.success), typography: .Fixed.body) - static let background = PONativeAlternativePaymentMethodBackgroundStyle() - static let separatorColor = UIColor(resource: .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 366e26466..000000000 --- a/Sources/ProcessOut/Sources/UI/Modules/NativeAlternativePaymentMethod/View/Cells/NativeAlternativePaymentMethodCell.swift +++ /dev/null @@ -1,33 +0,0 @@ -// -// NativeAlternativePaymentMethodCell.swift -// ProcessOut -// -// Created by Andrii Vysotskyi on 27.04.2023. -// - -import UIKit - -@available(*, deprecated) -@MainActor -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) -@MainActor -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 9d5be1b7a..000000000 --- a/Sources/ProcessOut/Sources/UI/Modules/NativeAlternativePaymentMethod/ViewModel/DefaultNativeAlternativePaymentMethodViewModel.swift +++ /dev/null @@ -1,416 +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 - MainActor.assumeIsolated { - 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(resource: .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 - MainActor.assumeIsolated { - 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 e9b502a51..000000000 --- a/Sources/ProcessOut/Sources/UI/Modules/NativeAlternativePaymentMethod/ViewModel/NativeAlternativePaymentMethodViewModel.swift +++ /dev/null @@ -1,14 +0,0 @@ -// -// NativeAlternativePaymentMethodViewModel.swift -// ProcessOut -// -// Created by Andrii Vysotskyi on 20.04.2023. -// - -@available(*, deprecated) -@MainActor -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 c6d1279d2..000000000 --- a/Sources/ProcessOut/Sources/UI/Modules/Safari/DefaultSafariViewModel.swift +++ /dev/null @@ -1,138 +0,0 @@ -// -// DefaultSafariViewModel.swift -// ProcessOut -// -// Created by Andrii Vysotskyi on 10.05.2023. -// - -import Foundation -import SafariServices - -@available(*, deprecated) -@MainActor -final class DefaultSafariViewModel: NSObject, @preconcurrency 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 - MainActor.assumeIsolated { - self?.setCompletedState(with: POFailure(code: .timeout(.mobile))) - } - } - } - deepLinkObserver = eventEmitter.on(PODeepLinkReceivedEvent.self) { [weak self] event in - self?.setCompletedState(with: event.url) ?? false - } - state = .started - } - - // MARK: - SFSafariViewControllerDelegate - - func safariViewControllerDidFinish(_ controller: SFSafariViewController) { - if state != .completed { - logger.debug("Safari did finish, but state is not completed, handling as cancelation") - let failure = POFailure(code: .cancelled) - setCompletedState(with: failure) - } - } - - func safariViewController(_ controller: SFSafariViewController, didCompleteInitialLoad didLoadSuccessfully: Bool) { - if !didLoadSuccessfully { - logger.debug("Safari failed to load initial url, aborting") - let failure = POFailure(code: .generic(.mobile)) - setCompletedState(with: failure) - } - } - - 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 b13f287bb..000000000 --- a/Sources/ProcessOut/Sources/UI/Modules/Safari/SafariViewController+Extensions.swift +++ /dev/null @@ -1,23 +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 - - @MainActor - private enum Keys { - static var viewModel: UInt8 = 0 - } -} 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 3fdf1cb8e..000000000 --- a/Sources/ProcessOut/Sources/UI/Shared/Architecture/View/BaseViewController.swift +++ /dev/null @@ -1,126 +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 { - MainActor.assumeIsolated(self.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 { - MainActor.assumeIsolated(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 ade646c02..000000000 --- a/Sources/ProcessOut/Sources/UI/Shared/Architecture/ViewModel/ViewModel.swift +++ /dev/null @@ -1,24 +0,0 @@ -// -// ViewModel.swift -// ProcessOut -// -// Created by Andrii Vysotskyi on 19.10.2022. -// - -@available(*, deprecated) -@MainActor -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 332392d7f..000000000 --- a/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Layouts/Center/CollectionViewDelegateCenterLayout.swift +++ /dev/null @@ -1,18 +0,0 @@ -// -// CollectionViewDelegateCenterLayout.swift -// ProcessOut -// -// Created by Andrii Vysotskyi on 01.05.2023. -// - -import UIKit - -@MainActor -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 7ebbb0526..000000000 --- a/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Styles/Input/POInputStateStyle.swift +++ /dev/null @@ -1,48 +0,0 @@ -// -// POInputStateStyle.swift -// ProcessOut -// -// Created by Andrii Vysotskyi on 24.11.2022. -// - -import UIKit - -/// Defines input's styling information in a specific state. -@MainActor -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 5f13e0576..000000000 --- a/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Styles/Input/POInputStyle.swift +++ /dev/null @@ -1,53 +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. -@MainActor -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(resource: .Text.primary), typography: typography ?? .Fixed.label), - placeholder: .init(color: UIColor(resource: .Text.muted), typography: typography ?? .Fixed.label), - backgroundColor: UIColor(resource: .Surface.background), - border: .regular(radius: 8, color: UIColor(resource: .Border.default)), - shadow: .clear, - tintColor: UIColor(resource: .Text.primary) - ), - error: POInputStateStyle( - text: .init(color: UIColor(resource: .Text.primary), typography: typography ?? .Fixed.label), - placeholder: .init(color: UIColor(resource: .Text.muted), typography: typography ?? .Fixed.label), - backgroundColor: UIColor(resource: .Surface.background), - border: .regular(radius: 8, color: UIColor(resource: .Text.error)), - shadow: .clear, - tintColor: UIColor(resource: .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 8283ea8dd..000000000 --- a/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Styles/POBorderStyle.swift +++ /dev/null @@ -1,41 +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. -@MainActor -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 3839d35b7..000000000 --- a/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Styles/POShadowStyle.swift +++ /dev/null @@ -1,58 +0,0 @@ -// -// POShadowStyle.swift -// ProcessOut -// -// Created by Andrii Vysotskyi on 28.11.2022. -// - -import UIKit - -/// Style that defines shadow appearance. -@MainActor -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 48f4b801c..000000000 --- a/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Typography/POTextStyle.swift +++ /dev/null @@ -1,24 +0,0 @@ -// -// POTextStyle.swift -// ProcessOut -// -// Created by Andrii Vysotskyi on 23.11.2022. -// - -import UIKit - -/// Text style. -@MainActor -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 1726a0ef8..000000000 --- a/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Typography/POTypography.swift +++ /dev/null @@ -1,81 +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. -@MainActor -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 { - - @MainActor - 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) - } - - @MainActor - 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 cce9e0dfa..000000000 --- a/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Views/ActionsContainer/POActionsContainerStyle.swift +++ /dev/null @@ -1,43 +0,0 @@ -// -// POActionsContainerStyle.swift -// ProcessOut -// -// Created by Andrii Vysotskyi on 22.05.2023. -// - -import UIKit - -/// Actions container style. -@MainActor -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(resource: .Border.subtle) - self.backgroundColor = backgroundColor ?? UIColor(resource: .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 9b8f2ef8b..000000000 --- a/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Views/ActivityIndicator/ActivityIndicatorViewFactory.swift +++ /dev/null @@ -1,26 +0,0 @@ -// -// ActivityIndicatorViewFactory.swift -// ProcessOut -// -// Created by Andrii Vysotskyi on 20.12.2022. -// - -import UIKit - -@MainActor -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 9e3bde848..000000000 --- a/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Views/ActivityIndicator/POActivityIndicatorStyle.swift +++ /dev/null @@ -1,19 +0,0 @@ -// -// POActivityIndicatorStyle.swift -// ProcessOut -// -// Created by Andrii Vysotskyi on 23.11.2022. -// - -import UIKit - -/// Possible activity indicator styles. -@MainActor -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 82843c4ba..000000000 --- a/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Views/ActivityIndicator/POActivityIndicatorView.swift +++ /dev/null @@ -1,26 +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. -@MainActor -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 c0cbed388..000000000 --- a/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Views/Button/POButtonStateStyle.swift +++ /dev/null @@ -1,32 +0,0 @@ -// -// POButtonStateStyle.swift -// ProcessOut -// -// Created by Andrii Vysotskyi on 28.11.2022. -// - -import UIKit - -/// Defines button's styling information in a specific state. -@MainActor -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 3270c2294..000000000 --- a/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Views/Button/POButtonStyle.swift +++ /dev/null @@ -1,92 +0,0 @@ -// -// POButtonStyle.swift -// ProcessOut -// -// Created by Andrii Vysotskyi on 22.11.2022. -// - -import UIKit - -/// Defines button style in all possible states. -@MainActor -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(resource: .Text.on), typography: .Fixed.button), - border: .clear(radius: 8), - shadow: .clear, - backgroundColor: UIColor(resource: .Action.Primary.default) - ), - highlighted: .init( - title: .init(color: UIColor(resource: .Text.on), typography: .Fixed.button), - border: .clear(radius: 8), - shadow: .clear, - backgroundColor: UIColor(resource: .Action.Primary.pressed) - ), - disabled: .init( - title: .init(color: UIColor(resource: .Text.disabled), typography: .Fixed.button), - border: .clear(radius: 8), - shadow: .clear, - backgroundColor: UIColor(resource: .Action.Primary.disabled) - ), - activityIndicator: activityIndicatorStyle(color: UIColor(resource: .Text.on)) - ) - - /// Default style for secondary button. - public static let secondary = POButtonStyle( - normal: .init( - title: .init(color: UIColor(resource: .Text.secondary), typography: .Fixed.button), - border: .regular(radius: 8, color: UIColor(resource: .Border.default)), - shadow: .clear, - backgroundColor: UIColor(resource: .Action.Secondary.default) - ), - highlighted: .init( - title: .init(color: UIColor(resource: .Text.secondary), typography: .Fixed.button), - border: .regular(radius: 8, color: UIColor(resource: .Border.default)), - shadow: .clear, - backgroundColor: UIColor(resource: .Action.Secondary.pressed) - ), - disabled: .init( - title: .init(color: UIColor(resource: .Text.disabled), typography: .Fixed.button), - border: .regular(radius: 8, color: UIColor(resource: .Action.Border.disabled)), - shadow: .clear, - backgroundColor: .clear - ), - activityIndicator: activityIndicatorStyle(color: UIColor(resource: .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 f74c1a177..000000000 --- a/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Views/CodeTextField/CodeTextFieldDelegate.swift +++ /dev/null @@ -1,23 +0,0 @@ -// -// CodeTextFieldDelegate.swift -// ProcessOut -// -// Created by Andrii Vysotskyi on 29.11.2022. -// - -import UIKit - -@MainActor -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 f7c05e9bf..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(resource: .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 64d62f70e..000000000 --- a/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Views/RadioButton/PORadioButtonKnobStateStyle.swift +++ /dev/null @@ -1,39 +0,0 @@ -// -// PORadioButtonKnobStateStyle.swift -// ProcessOut -// -// Created by Andrii Vysotskyi on 08.06.2023. -// - -import UIKit - -/// Describes radio button knob style in a particular state. -@MainActor -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 17cf3b762..000000000 --- a/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Views/RadioButton/PORadioButtonStateStyle.swift +++ /dev/null @@ -1,25 +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. -@MainActor -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 e4caec879..000000000 --- a/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Views/RadioButton/PORadioButtonStyle.swift +++ /dev/null @@ -1,85 +0,0 @@ -// -// PORadioButtonStyle.swift -// ProcessOut -// -// Created by Andrii Vysotskyi on 06.06.2023. -// - -import UIKit - -/// Describes radio button style in different states. -@MainActor -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(resource: .Border.default)), - innerCircleColor: .clear, - innerCircleRadius: 0 - ), - value: valueStyle - ), - selected: .init( - knob: .init( - backgroundColor: .clear, - border: .regular(radius: 0, color: UIColor(resource: .Action.Primary.default)), - innerCircleColor: UIColor(resource: .Action.Primary.default), - innerCircleRadius: 4 - ), - value: valueStyle - ), - highlighted: .init( - knob: .init( - backgroundColor: .clear, - border: .regular(radius: 0, color: UIColor(resource: .Text.muted)), - innerCircleColor: .clear, - innerCircleRadius: 0 - ), - value: valueStyle - ), - error: .init( - knob: .init( - backgroundColor: .clear, - border: .regular(radius: 0, color: UIColor(resource: .Text.error)), - innerCircleColor: .clear, - innerCircleRadius: 0 - ), - value: valueStyle - ) - ) - - // MARK: - Private Properties - - private static let valueStyle = POTextStyle(color: UIColor(resource: .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 c3cf9877b..000000000 --- a/Sources/ProcessOut/Sources/UI/Shared/DesignSystem/Views/TextField/TextFieldContainerView.swift +++ /dev/null @@ -1,128 +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 - MainActor.assumeIsolated { - 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 297363ce2..000000000 --- a/Sources/ProcessOut/Sources/UI/Shared/Extensions/UIImageView+Extensions.swift +++ /dev/null @@ -1,35 +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 - - @MainActor - 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 9a0ec4d5e..000000000 --- a/Sources/ProcessOut/Sources/UI/Shared/Utils/CollectionReusableViewSizeProvider.swift +++ /dev/null @@ -1,52 +0,0 @@ -// -// CollectionReusableViewSizeProvider.swift -// ProcessOut -// -// Created by Andrii Vysotskyi on 25.04.2023. -// - -import UIKit - -@MainActor -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 f958e3fa2..000000000 --- a/Sources/ProcessOut/Sources/UI/Shared/Utils/KeyboardNotification.swift +++ /dev/null @@ -1,32 +0,0 @@ -// -// KeyboardNotification.swift -// ProcessOut -// -// Created by Andrii Vysotskyi on 07.05.2023. -// - -import UIKit - -@MainActor -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 c0e6807a8..000000000 --- a/Sources/ProcessOut/Sources/UI/Shared/Utils/Reusable.swift +++ /dev/null @@ -1,13 +0,0 @@ -// -// Reusable.swift -// ProcessOut -// -// Created by Andrii Vysotskyi on 27.04.2023. -// - -@MainActor -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 3231bff95..000000000 --- a/Sources/ProcessOut/Sources/UI/Shared/Utils/TextFieldUtils.swift +++ /dev/null @@ -1,43 +0,0 @@ -// -// TextFieldUtils.swift -// ProcessOut -// -// Created by Andrii Vysotskyi on 07.08.2023. -// - -import Foundation -import UIKit - -@MainActor -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/ProcessOut/swiftgen.yml b/Sources/ProcessOut/swiftgen.yml deleted file mode 100644 index acfdfc679..000000000 --- a/Sources/ProcessOut/swiftgen.yml +++ /dev/null @@ -1,24 +0,0 @@ -input_dir: ${TARGET_ROOT}/Resources/ -output_dir: ${TARGET_ROOT}/Sources/Generated/ - -## Files -files: - inputs: - - PhoneNumberMetadata/ - filter: .+[.]json$ - outputs: - - templateName: flat-swift5 - params: - bundle: BundleLocator.bundle - useExtension: false - output: Files+Generated.swift - -## Fonts -fonts: - inputs: - - Fonts - outputs: - - templateName: swift5 - params: - bundle: BundleLocator.bundle - output: Fonts+Generated.swift 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 87b203d90..e4d898a2b 100644 --- a/Sources/ProcessOutUI/Sources/Api/Test3DS/POTest3DSService.swift +++ b/Sources/ProcessOutUI/Sources/Api/Test3DS/POTest3DSService.swift @@ -6,7 +6,7 @@ // 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. 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 63% rename from Sources/ProcessOut/Sources/Core/Formatters/PhoneNumber/MetadataProvider/PODefaultPhoneNumberMetadataProvider.swift rename to Sources/ProcessOutUI/Sources/Core/Formatters/PhoneNumber/MetadataProvider/DefaultPhoneNumberMetadataProvider.swift index 20f1c4a73..cb0e1b3a9 100644 --- a/Sources/ProcessOut/Sources/Core/Formatters/PhoneNumber/MetadataProvider/PODefaultPhoneNumberMetadataProvider.swift +++ b/Sources/ProcessOutUI/Sources/Core/Formatters/PhoneNumber/MetadataProvider/DefaultPhoneNumberMetadataProvider.swift @@ -1,25 +1,25 @@ // -// 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.wrappedValue { return metadata[transformedCountryCode] @@ -31,7 +31,7 @@ public final class PODefaultPhoneNumberMetadataProvider: POPhoneNumberMetadataPr // MARK: - Private Properties private let dispatchQueue: DispatchQueue - private let metadata = POUnfairlyLocked<[String: POPhoneNumberMetadata]?>(wrappedValue: nil) + private let metadata = POUnfairlyLocked<[String: PhoneNumberMetadata]?>(wrappedValue: nil) // MARK: - Private Methods @@ -44,13 +44,16 @@ public final class PODefaultPhoneNumberMetadataProvider: POPhoneNumberMetadataPr 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)") 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 11017c8b7..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,10 +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() @@ -21,7 +20,7 @@ public final class POPhoneNumberFormatter: Formatter { 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 "" @@ -35,7 +34,7 @@ public final class POPhoneNumberFormatter: Formatter { !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 ) { @@ -51,20 +50,20 @@ public final class POPhoneNumberFormatter: Formatter { 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, @@ -73,7 +72,7 @@ public final class POPhoneNumberFormatter: 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 @@ -102,12 +101,12 @@ public final class POPhoneNumberFormatter: Formatter { // 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 { @@ -124,7 +123,7 @@ public final class POPhoneNumberFormatter: Formatter { 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 @@ -136,7 +135,7 @@ public final class POPhoneNumberFormatter: Formatter { 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) @@ -152,7 +151,7 @@ public final class POPhoneNumberFormatter: Formatter { // 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( @@ -188,7 +187,7 @@ public final class POPhoneNumberFormatter: Formatter { // 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 100% rename from Sources/ProcessOut/Sources/Core/RegexProvider/RegexProvider.swift rename to Sources/ProcessOutUI/Sources/Core/Formatters/RegexProvider/RegexProvider.swift diff --git a/Sources/ProcessOut/Sources/Core/Formatters/Utils/POFormattingUtils.swift b/Sources/ProcessOutUI/Sources/Core/Formatters/Utils/FormattingUtils.swift similarity index 96% rename from Sources/ProcessOut/Sources/Core/Formatters/Utils/POFormattingUtils.swift rename to Sources/ProcessOutUI/Sources/Core/Formatters/Utils/FormattingUtils.swift index b30ec6c1f..4e373430a 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 { +public enum FormattingUtils { /// Returns index in formatted string that matches index in `string`. /// 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/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 88% rename from Sources/ProcessOut/Sources/Core/Utils/POStringResource.swift rename to Sources/ProcessOutUI/Sources/Core/Utils/StringResource.swift index 7bbdf7370..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: Sendable { +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/Modules/CardTokenization/Interactor/DefaultCardTokenizationInteractor.swift b/Sources/ProcessOutUI/Sources/Modules/CardTokenization/Interactor/DefaultCardTokenizationInteractor.swift index 9e50d84ad..4e244465d 100644 --- a/Sources/ProcessOutUI/Sources/Modules/CardTokenization/Interactor/DefaultCardTokenizationInteractor.swift +++ b/Sources/ProcessOutUI/Sources/Modules/CardTokenization/Interactor/DefaultCardTokenizationInteractor.swift @@ -160,8 +160,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? @@ -198,7 +198,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]) diff --git a/Sources/ProcessOutUI/Sources/Modules/CardTokenization/Symbols/StringResource+CardTokenization.swift b/Sources/ProcessOutUI/Sources/Modules/CardTokenization/Symbols/StringResource+CardTokenization.swift index ffac8c19a..e62ae148d 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,49 +36,49 @@ 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: "") } } } diff --git a/Sources/ProcessOutUI/Sources/Modules/CardUpdate/Interactor/DefaultCardUpdateInteractor.swift b/Sources/ProcessOutUI/Sources/Modules/CardUpdate/Interactor/DefaultCardUpdateInteractor.swift index 9d204070b..b5d43e28f 100644 --- a/Sources/ProcessOutUI/Sources/Modules/CardUpdate/Interactor/DefaultCardUpdateInteractor.swift +++ b/Sources/ProcessOutUI/Sources/Modules/CardUpdate/Interactor/DefaultCardUpdateInteractor.swift @@ -215,7 +215,7 @@ final class DefaultCardUpdateInteractor: BaseInteractor Void ) { diff --git a/Sources/ProcessOutUI/Sources/Modules/DynamicCheckout/Symbols/StringResource+DynamicCheckout.swift b/Sources/ProcessOutUI/Sources/Modules/DynamicCheckout/Symbols/StringResource+DynamicCheckout.swift index 040ff21c1..bc522f337 100644 --- a/Sources/ProcessOutUI/Sources/Modules/DynamicCheckout/Symbols/StringResource+DynamicCheckout.swift +++ b/Sources/ProcessOutUI/Sources/Modules/DynamicCheckout/Symbols/StringResource+DynamicCheckout.swift @@ -5,49 +5,47 @@ // 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: "") } 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/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.swift b/Sources/ProcessOutUI/Sources/Modules/DynamicCheckout/View/PODynamicCheckoutView.swift index c9e014c33..b2fdef859 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. diff --git a/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/Delegate/PONativeAlternativePaymentDelegate.swift b/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/Delegate/PONativeAlternativePaymentDelegate.swift index 1ccdf3af4..39c11fe10 100644 --- a/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/Delegate/PONativeAlternativePaymentDelegate.swift +++ b/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/Delegate/PONativeAlternativePaymentDelegate.swift @@ -1,13 +1,26 @@ // -// 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 nativeAlternativePaymentDidEmitEvent(_ 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. + @MainActor + func nativeAlternativePaymentDefaultValues( + for parameters: [PONativeAlternativePaymentMethodParameter], + completion: @escaping @Sendable ([String: String]) -> Void + ) +} 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 2634523e0..0bb589187 100644 --- a/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/Interactor/NativeAlternativePaymentDefaultInteractor.swift +++ b/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/Interactor/NativeAlternativePaymentDefaultInteractor.swift @@ -405,10 +405,10 @@ final class NativeAlternativePaymentDefaultInteractor: // MARK: - Events - private func send(event: PONativeAlternativePaymentMethodEvent) { + private func send(event: PONativeAlternativePaymentEvent) { assert(Thread.isMainThread, "Method should be called on main thread.") logger.debug("Did send event: '\(event)'") - delegate?.nativeAlternativePaymentMethodDidEmitEvent(event) + delegate?.nativeAlternativePaymentDidEmitEvent(event) } private func didUpdate(parameter: NativeAlternativePaymentInteractorState.Parameter, to value: String) { @@ -442,7 +442,7 @@ final class NativeAlternativePaymentDefaultInteractor: let formatter: Foundation.Formatter? switch specification.type { case .phone: - formatter = POPhoneNumberFormatter() + formatter = PhoneNumberFormatter() default: formatter = nil } @@ -456,7 +456,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 @@ -498,7 +498,7 @@ final class NativeAlternativePaymentDefaultInteractor: } let defaultValues = await withCheckedContinuation { continuation in if let delegate { - delegate.nativeAlternativePaymentMethodDefaultValues( + delegate.nativeAlternativePaymentDefaultValues( for: parameters.map(\.specification), completion: { continuation.resume(returning: $0) } ) @@ -544,7 +544,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/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/project.yml b/project.yml index 3c9140d6b..8807ee963 100644 --- a/project.yml +++ b/project.yml @@ -61,12 +61,6 @@ targets: - path: Scripts/Lint.sh name: Swiftlint basedOnDependencyAnalysis: false - - path: Scripts/SwiftGen.sh - name: SwiftGen - outputFiles: - - $(TARGET_ROOT)/Sources/Generated/Files+Generated.swift - - $(TARGET_ROOT)/Sources/Generated/Fonts+Generated.swift - basedOnDependencyAnalysis: false - path: Scripts/Sourcery.sh name: Sourcery basedOnDependencyAnalysis: false From 9a673575dd04bfa7e1066aab5b76edbe2ff06640 Mon Sep 17 00:00:00 2001 From: Andrii Vysotskyi Date: Fri, 2 Aug 2024 14:35:30 +0200 Subject: [PATCH 03/10] feat(POM-399): rework 3DS interface (#321) * Fix example app build issues/warnings * Handle 3DS using ASWebAuthenticationSession * Make web based 3DS internal * Rework PO3DSService to use structured concurrency --- .../Data/AlternativePaymentDataBuilder.swift | 1 + .../AlternativePaymentDataViewModelType.swift | 1 + .../AlternativePaymentDataEntryBuilder.swift | 1 + .../AlternativePaymentMethodsBuilder.swift | 1 + .../AuthorizationAmountBuilder.swift | 1 + .../CardPayment/CardPaymentBuilder.swift | 8 +- ...ardPaymentCheckout3DSServiceDelegate.swift | 27 --- .../UI/Modules/Features/FeaturesBuilder.swift | 1 + .../ViewModel/FeaturesViewModel.swift | 2 +- .../Architecture/Router/RouterType.swift | 1 + .../ViewModel/ViewModelType.swift | 1 + .../UI/Shared/View/ViewController.swift | 4 +- Package.swift | 2 +- .../ProcessOut/Sources/Api/ProcessOut.swift | 3 +- .../Sources/Core/Utils/AsyncUtils.swift | 16 +- .../Sources/Core/Utils/Batcher.swift | 4 +- .../WebAuthenticationSession.swift | 120 +++++++++++ .../Services/3DS/DefaultThreeDSService.swift | 99 +++++---- ...3DS2AuthenticationRequestParameters.swift} | 7 +- ....swift => PO3DS2ChallengeParameters.swift} | 7 +- .../3DS/Models/PO3DS2ChallengeResult.swift | 23 ++ .../3DS/Models/PO3DS2Configuration.swift | 3 +- .../PO3DS2ConfigurationCardScheme.swift | 75 ------- .../Services/3DS/Models/PO3DSRedirect.swift | 23 -- .../Sources/Services/3DS/PO3DSService.swift | 53 +---- .../Builder/POCheckout3DSServiceBuilder.swift | 56 ----- .../Checkout3DSTransaction+Async.swift | 27 +++ .../AuthenticationErrorMapper.swift | 4 +- .../DefaultAuthenticationErrorMapper.swift | 2 +- .../Configuration/ConfigurationMapper.swift | 2 +- .../DefaultConfigurationMapper.swift | 4 +- .../Sources/Service/Checkout3DSService.swift | 199 +++++++----------- .../Service/Checkout3DSServiceState.swift | 33 --- .../POCheckout3DSServiceDelegate.swift | 92 ++++---- .../Sources/Backports/Task/View+Task.swift | 4 +- .../AsyncImage/POAsyncImage.swift | 5 +- .../Api/Test3DS/POTest3DSService.swift | 48 ++--- .../3DSRedirect/PO3DSRedirectController.swift | 104 --------- ...WebAuthenticationSession+3DSRedirect.swift | 37 ---- .../SFSafariViewController+3DSRedirect.swift | 50 ----- 40 files changed, 432 insertions(+), 719 deletions(-) delete mode 100644 Example/Example/Sources/UI/Modules/CardPayment/CardPaymentCheckout3DSServiceDelegate.swift create mode 100644 Sources/ProcessOut/Sources/Core/WebAuthenticationSession/WebAuthenticationSession.swift rename Sources/ProcessOut/Sources/Services/3DS/Models/{PO3DS2AuthenticationRequest.swift => PO3DS2AuthenticationRequestParameters.swift} (79%) rename Sources/ProcessOut/Sources/Services/3DS/Models/{PO3DS2Challenge.swift => PO3DS2ChallengeParameters.swift} (80%) create mode 100644 Sources/ProcessOut/Sources/Services/3DS/Models/PO3DS2ChallengeResult.swift delete mode 100644 Sources/ProcessOut/Sources/Services/3DS/Models/PO3DS2ConfigurationCardScheme.swift delete mode 100644 Sources/ProcessOut/Sources/Services/3DS/Models/PO3DSRedirect.swift delete mode 100644 Sources/ProcessOutCheckout3DS/Sources/Builder/POCheckout3DSServiceBuilder.swift create mode 100644 Sources/ProcessOutCheckout3DS/Sources/Extensions/Checkout3DSTransaction+Async.swift delete mode 100644 Sources/ProcessOutCheckout3DS/Sources/Service/Checkout3DSServiceState.swift delete mode 100644 Sources/ProcessOutUI/Sources/Modules/3DSRedirect/PO3DSRedirectController.swift delete mode 100644 Sources/ProcessOutUI/Sources/Modules/3DSRedirect/POWebAuthenticationSession+3DSRedirect.swift delete mode 100644 Sources/ProcessOutUI/Sources/Modules/3DSRedirect/SFSafariViewController+3DSRedirect.swift diff --git a/Example/Example/Sources/UI/Modules/AlternativePayment/Data/AlternativePaymentDataBuilder.swift b/Example/Example/Sources/UI/Modules/AlternativePayment/Data/AlternativePaymentDataBuilder.swift index 61022a4c4..42fd1bce2 100644 --- a/Example/Example/Sources/UI/Modules/AlternativePayment/Data/AlternativePaymentDataBuilder.swift +++ b/Example/Example/Sources/UI/Modules/AlternativePayment/Data/AlternativePaymentDataBuilder.swift @@ -7,6 +7,7 @@ import UIKit +@MainActor final class AlternativePaymentDataBuilder { init(completion: @escaping ([String: String]) -> Void) { diff --git a/Example/Example/Sources/UI/Modules/AlternativePayment/Data/ViewModel/AlternativePaymentDataViewModelType.swift b/Example/Example/Sources/UI/Modules/AlternativePayment/Data/ViewModel/AlternativePaymentDataViewModelType.swift index 6df0b8d3f..66df9bfbc 100644 --- a/Example/Example/Sources/UI/Modules/AlternativePayment/Data/ViewModel/AlternativePaymentDataViewModelType.swift +++ b/Example/Example/Sources/UI/Modules/AlternativePayment/Data/ViewModel/AlternativePaymentDataViewModelType.swift @@ -7,6 +7,7 @@ import Foundation +@MainActor protocol AlternativePaymentDataViewModelType: ViewModelType { /// Submits items and continues payment. diff --git a/Example/Example/Sources/UI/Modules/AlternativePayment/DataEntry/AlternativePaymentDataEntryBuilder.swift b/Example/Example/Sources/UI/Modules/AlternativePayment/DataEntry/AlternativePaymentDataEntryBuilder.swift index 68c30560c..57ec95f7e 100644 --- a/Example/Example/Sources/UI/Modules/AlternativePayment/DataEntry/AlternativePaymentDataEntryBuilder.swift +++ b/Example/Example/Sources/UI/Modules/AlternativePayment/DataEntry/AlternativePaymentDataEntryBuilder.swift @@ -7,6 +7,7 @@ import UIKit +@MainActor final class AlternativePaymentDataEntryBuilder { init(completion: @escaping (_ key: String, _ value: String) -> Void) { diff --git a/Example/Example/Sources/UI/Modules/AlternativePayment/Methods/AlternativePaymentMethodsBuilder.swift b/Example/Example/Sources/UI/Modules/AlternativePayment/Methods/AlternativePaymentMethodsBuilder.swift index 27adb2590..0e015c28d 100644 --- a/Example/Example/Sources/UI/Modules/AlternativePayment/Methods/AlternativePaymentMethodsBuilder.swift +++ b/Example/Example/Sources/UI/Modules/AlternativePayment/Methods/AlternativePaymentMethodsBuilder.swift @@ -8,6 +8,7 @@ import UIKit import ProcessOut +@MainActor final class AlternativePaymentMethodsBuilder { init(filter: POAllGatewayConfigurationsRequest.Filter) { diff --git a/Example/Example/Sources/UI/Modules/AuthorizationAmount/AuthorizationAmountBuilder.swift b/Example/Example/Sources/UI/Modules/AuthorizationAmount/AuthorizationAmountBuilder.swift index 8419d723c..df7a28cf2 100644 --- a/Example/Example/Sources/UI/Modules/AuthorizationAmount/AuthorizationAmountBuilder.swift +++ b/Example/Example/Sources/UI/Modules/AuthorizationAmount/AuthorizationAmountBuilder.swift @@ -7,6 +7,7 @@ import UIKit +@MainActor final class AuthorizationAmountBuilder { init(completion: @escaping (_ amount: Decimal, _ currencyCode: String) -> Void) { diff --git a/Example/Example/Sources/UI/Modules/CardPayment/CardPaymentBuilder.swift b/Example/Example/Sources/UI/Modules/CardPayment/CardPaymentBuilder.swift index c5db320d5..7ed7a2a50 100644 --- a/Example/Example/Sources/UI/Modules/CardPayment/CardPaymentBuilder.swift +++ b/Example/Example/Sources/UI/Modules/CardPayment/CardPaymentBuilder.swift @@ -10,6 +10,7 @@ import ProcessOut import ProcessOutUI import ProcessOutCheckout3DS +@MainActor final class CardPaymentBuilder { init(threeDSService: CardPayment3DSService = .test, completion: @escaping (Result) -> Void) { @@ -21,12 +22,9 @@ final class CardPaymentBuilder { let threeDSService: PO3DSService switch self.threeDSService { case .test: - threeDSService = POTest3DSService(returnUrl: Constants.returnUrl) + threeDSService = POTest3DSService() case .checkout: - threeDSService = POCheckout3DSServiceBuilder() - .with(delegate: CardPaymentCheckout3DSServiceDelegate()) - .with(environment: .sandbox) - .build() + threeDSService = POCheckout3DSService(environment: .sandbox) } let delegate = CardPaymentDelegate( invoicesService: ProcessOut.shared.invoices, threeDSService: threeDSService diff --git a/Example/Example/Sources/UI/Modules/CardPayment/CardPaymentCheckout3DSServiceDelegate.swift b/Example/Example/Sources/UI/Modules/CardPayment/CardPaymentCheckout3DSServiceDelegate.swift deleted file mode 100644 index 1c2aacee5..000000000 --- a/Example/Example/Sources/UI/Modules/CardPayment/CardPaymentCheckout3DSServiceDelegate.swift +++ /dev/null @@ -1,27 +0,0 @@ -// -// CardPaymentCheckout3DSServiceDelegate.swift -// Example -// -// Created by Andrii Vysotskyi on 31.01.2024. -// - -import UIKit -import ProcessOut -import ProcessOutUI -import ProcessOutCheckout3DS - -final class CardPaymentCheckout3DSServiceDelegate: 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)) - } - } -} diff --git a/Example/Example/Sources/UI/Modules/Features/FeaturesBuilder.swift b/Example/Example/Sources/UI/Modules/Features/FeaturesBuilder.swift index 98c8d1369..03aee7aff 100644 --- a/Example/Example/Sources/UI/Modules/Features/FeaturesBuilder.swift +++ b/Example/Example/Sources/UI/Modules/Features/FeaturesBuilder.swift @@ -8,6 +8,7 @@ import UIKit import ProcessOut +@MainActor final class FeaturesBuilder { func build() -> UIViewController { diff --git a/Example/Example/Sources/UI/Modules/Features/ViewModel/FeaturesViewModel.swift b/Example/Example/Sources/UI/Modules/Features/ViewModel/FeaturesViewModel.swift index 69c22dda5..3ed8d4511 100644 --- a/Example/Example/Sources/UI/Modules/Features/ViewModel/FeaturesViewModel.swift +++ b/Example/Example/Sources/UI/Modules/Features/ViewModel/FeaturesViewModel.swift @@ -148,7 +148,7 @@ extension FeaturesViewModel: PODynamicCheckoutDelegate { func dynamicCheckout( willAuthorizeInvoiceWith request: inout POInvoiceAuthorizationRequest ) async -> any PO3DSService { - POTest3DSService(returnUrl: Constants.returnUrl) + POTest3DSService() } func dynamicCheckout(willAuthorizeInvoiceWith request: PKPaymentRequest) async { diff --git a/Example/Example/Sources/UI/Shared/Architecture/Router/RouterType.swift b/Example/Example/Sources/UI/Shared/Architecture/Router/RouterType.swift index 27e13e9d3..a756517fd 100644 --- a/Example/Example/Sources/UI/Shared/Architecture/Router/RouterType.swift +++ b/Example/Example/Sources/UI/Shared/Architecture/Router/RouterType.swift @@ -5,6 +5,7 @@ // Created by Andrii Vysotskyi on 23.10.2022. // +@MainActor protocol RouterType: AnyObject { /// `RouteType` defines which routes can be triggered in a certain implementation. diff --git a/Example/Example/Sources/UI/Shared/Architecture/ViewModel/ViewModelType.swift b/Example/Example/Sources/UI/Shared/Architecture/ViewModel/ViewModelType.swift index 5af09e2f6..946473048 100644 --- a/Example/Example/Sources/UI/Shared/Architecture/ViewModel/ViewModelType.swift +++ b/Example/Example/Sources/UI/Shared/Architecture/ViewModel/ViewModelType.swift @@ -5,6 +5,7 @@ // Created by Andrii Vysotskyi on 21.10.2022. // +@MainActor protocol ViewModelType: AnyObject { associatedtype State diff --git a/Example/Example/Sources/UI/Shared/View/ViewController.swift b/Example/Example/Sources/UI/Shared/View/ViewController.swift index e558dcfe9..1b8ffa276 100644 --- a/Example/Example/Sources/UI/Shared/View/ViewController.swift +++ b/Example/Example/Sources/UI/Shared/View/ViewController.swift @@ -40,7 +40,9 @@ class ViewController: UIViewController { // as a workaround, configuration is postponed to a point when tracking ends. if RunLoop.current.currentMode == .tracking { RunLoop.current.perform { - self.configure(with: self.viewModel.state) + MainActor.assumeIsolated { + self.configure(with: self.viewModel.state) + } } } else { configure(with: viewModel.state) diff --git a/Package.swift b/Package.swift index e98b56b8b..4c5ebd5a3 100644 --- a/Package.swift +++ b/Package.swift @@ -3,6 +3,7 @@ import PackageDescription let swiftSettings: [SwiftSetting] = [ + .enableExperimentalFeature("IsolatedAny"), .enableUpcomingFeature("StrictConcurrency") ] @@ -27,7 +28,6 @@ let package = Package( dependencies: [ .target(name: "cmark") ], - exclude: ["swiftgen.yml"], resources: [ .process("Resources") ], diff --git a/Sources/ProcessOut/Sources/Api/ProcessOut.swift b/Sources/ProcessOut/Sources/Api/ProcessOut.swift index 61ea1139d..a6d80fa7e 100644 --- a/Sources/ProcessOut/Sources/Api/ProcessOut.swift +++ b/Sources/ProcessOut/Sources/Api/ProcessOut.swift @@ -154,12 +154,13 @@ public final class ProcessOut: @unchecked Sendable { } private static func create3DSService() -> DefaultThreeDSService { + let webSession = WebAuthenticationSession() let decoder = JSONDecoder() decoder.keyDecodingStrategy = .useDefaultKeys let encoder = JSONEncoder() encoder.dataEncodingStrategy = .base64 encoder.keyEncodingStrategy = .useDefaultKeys - return DefaultThreeDSService(decoder: decoder, encoder: encoder) + return DefaultThreeDSService(decoder: decoder, encoder: encoder, webSession: webSession) } private func createConnector( diff --git a/Sources/ProcessOut/Sources/Core/Utils/AsyncUtils.swift b/Sources/ProcessOut/Sources/Core/Utils/AsyncUtils.swift index 73521219e..996983b0a 100644 --- a/Sources/ProcessOut/Sources/Core/Utils/AsyncUtils.swift +++ b/Sources/ProcessOut/Sources/Core/Utils/AsyncUtils.swift @@ -12,8 +12,8 @@ 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 { let isTimedOut = POUnfairlyLocked(wrappedValue: false) let task = Task(operation: operation) @@ -48,10 +48,10 @@ func withTimeout( // MARK: - Retry func retry( - operation: @escaping @Sendable () async throws -> T, + 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,7 +67,7 @@ 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 @Sendable (Result) -> Bool, retryStrategy: RetryStrategy?, @@ -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 7899ad5fd..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: Sendable { +final class Batcher: Sendable { - typealias Executor = @Sendable (Array) async -> Bool + typealias Executor = @Sendable @isolated(any) (Array) async -> Bool init(executionInterval: TimeInterval = 10, executor: @escaping Executor) { self.executionInterval = executionInterval diff --git a/Sources/ProcessOut/Sources/Core/WebAuthenticationSession/WebAuthenticationSession.swift b/Sources/ProcessOut/Sources/Core/WebAuthenticationSession/WebAuthenticationSession.swift new file mode 100644 index 000000000..7ccbbf187 --- /dev/null +++ b/Sources/ProcessOut/Sources/Core/WebAuthenticationSession/WebAuthenticationSession.swift @@ -0,0 +1,120 @@ +// +// WebAuthenticationSession.swift +// ProcessOut +// +// Created by Andrii Vysotskyi on 01.08.2024. +// + +import AuthenticationServices + +@MainActor +final class WebAuthenticationSession: NSObject, Sendable, ASWebAuthenticationPresentationContextProviding { + + override nonisolated init() { + // Ignored + } + + func authenticate( + using url: URL, + callbackScheme: String? = nil, + additionalHeaderFields: [String: String]? = nil + ) async throws -> URL { + let sessionProxy = WebAuthenticationSessionProxy() + return try await withTaskCancellationHandler( + operation: { + try await withCheckedThrowingContinuation { continuation in + guard !Task.isCancelled else { + let failure = POFailure(message: "Authentication session was cancelled.", code: .cancelled) + continuation.resume(throwing: failure) + return + } + let session = ASWebAuthenticationSession( + url: url, + callbackURLScheme: callbackScheme, + completionHandler: { url, error in + sessionProxy.invalidate() + if let error { + continuation.resume(throwing: Self.converted(error: error)) + } else if let url { + continuation.resume(returning: url) + } else { + preconditionFailure("Unexpected ASWebAuthenticationSession completion result.") + } + } + ) + session.prefersEphemeralWebBrowserSession = true + session.presentationContextProvider = self + if #available(iOS 17.4, *) { + session.additionalHeaderFields = additionalHeaderFields + } + sessionProxy.setSession(session, continuation: continuation) + session.start() + } + }, + onCancel: { + sessionProxy.cancel() + } + ) + } + + // MARK: - ASWebAuthenticationPresentationContextProviding + + func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor { + let application = UIApplication.shared + let scene = application.connectedScenes.first { $0 is UIWindowScene } as? UIWindowScene + let window = scene?.windows.first(where: \.isKeyWindow) + return window ?? ASPresentationAnchor() + } + + // MARK: - Private Methods + + private static func converted(error: Error) -> POFailure { + guard let error = error as? ASWebAuthenticationSessionError else { + return POFailure(code: .generic(.mobile), underlyingError: error) + } + let poCode: POFailure.Code + switch error.code { + case .canceledLogin: + poCode = .cancelled + case .presentationContextNotProvided, .presentationContextInvalid: + poCode = .internal(.mobile) + @unknown default: + poCode = .generic(.mobile) + } + return POFailure(code: poCode, underlyingError: error) + } +} + +@MainActor +private final class WebAuthenticationSessionProxy: Sendable { + + func setSession(_ session: ASWebAuthenticationSession, continuation: CheckedContinuation) { + 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/Services/3DS/DefaultThreeDSService.swift b/Sources/ProcessOut/Sources/Services/3DS/DefaultThreeDSService.swift index 966605091..a8cf84259 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 Constants.tokenPrefix + 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 f42d1fa36..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, Sendable { +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 2764a221e..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, Sendable { +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..ef327e5ca --- /dev/null +++ b/Sources/ProcessOut/Sources/Services/3DS/Models/PO3DS2ChallengeResult.swift @@ -0,0 +1,23 @@ +// +// 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 + } + + // 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 0c6d7384b..85b2af2ff 100644 --- a/Sources/ProcessOut/Sources/Services/3DS/Models/PO3DS2Configuration.swift +++ b/Sources/ProcessOut/Sources/Services/3DS/Models/PO3DS2Configuration.swift @@ -21,8 +21,7 @@ public struct PO3DS2Configuration: Decodable, Hashable, Sendable { 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 a2792de03..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, Sendable { - - /// 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 8dc9ef9bf..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, Sendable { - - /// 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 527ad59f6..15352d77a 100644 --- a/Sources/ProcessOut/Sources/Services/3DS/PO3DSService.swift +++ b/Sources/ProcessOut/Sources/Services/3DS/PO3DSService.swift @@ -12,53 +12,10 @@ public typealias PO3DSServiceType = 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, - completion: @escaping @Sendable (Result) -> Void - ) + func authenticationRequestParameters( + configuration: PO3DS2Configuration + ) async throws -> PO3DS2AuthenticationRequestParameters - /// 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 @Sendable (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 @Sendable (Result) -> Void) -} - -@MainActor -extension PO3DSService { - - /// 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) { result in - continuation.resume(with: result) - } - } - } - - /// 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) { result in - continuation.resume(with: result) - } - } - } - - /// 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) { result in - continuation.resume(with: result) - } - } - } + /// Implementation must handle given 3DS2 challenge. + func performChallenge(with parameters: PO3DS2ChallengeParameters) async throws -> PO3DS2ChallengeResult } 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 index 309e1b8e8..af408c01f 100644 --- a/Sources/ProcessOutCheckout3DS/Sources/Service/Checkout3DSService.swift +++ b/Sources/ProcessOutCheckout3DS/Sources/Service/Checkout3DSService.swift @@ -1,5 +1,5 @@ // -// Checkout3DSService.swift +// POCheckout3DSService.swift // ProcessOutCheckout3DS // // Created by Andrii Vysotskyi on 28.02.2023. @@ -8,170 +8,125 @@ import ProcessOut import Checkout3DS -final class Checkout3DSService: PO3DSService { +public actor POCheckout3DSService: PO3DSService, Sendable { - init( - errorMapper: AuthenticationErrorMapper, - configurationMapper: ConfigurationMapper, - delegate: POCheckout3DSServiceDelegate, - environment: Checkout3DS.Environment - ) { - self.errorMapper = errorMapper - self.configurationMapper = configurationMapper + public init(delegate: POCheckout3DSServiceDelegate? = nil, environment: Environment = .production) { + errorMapper = DefaultAuthenticationErrorMapper() + configurationMapper = DefaultConfigurationMapper() self.delegate = delegate self.environment = environment - queue = DispatchQueue.global() - state = .idle } deinit { - clean() + service?.cleanUp() } + /// Service's delegate. + public weak var delegate: POCheckout3DSServiceDelegate? + // 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) + public func authenticationRequestParameters( + configuration: PO3DS2Configuration + ) async throws -> PO3DS2AuthenticationRequestParameters { + invalidate() 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)) - } - } - } + 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) } - } catch let error as AuthenticationError { - let failure = errorMapper.convert(error: error) - delegate.didCreateAuthenticationRequest(result: .failure(failure)) - completion(.failure(failure)) + let authenticationRequest = authenticationRequest( + with: try await service.createTransaction().getAuthenticationRequestParameters() + ) + await delegate?.checkout3DSService(self, didCreateFingerprintWith: .success(authenticationRequest)) + return authenticationRequest } catch { - let failure = POFailure(code: .generic(.mobile), underlyingError: error) - delegate.didCreateAuthenticationRequest(result: .failure(failure)) - completion(.failure(failure)) + invalidate() + let failure = failure(with: error) + await delegate?.checkout3DSService(self, didCreateFingerprintWith: .failure(failure)) + throw 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 + public func performChallenge(with parameters: PO3DS2ChallengeParameters) async throws -> PO3DS2ChallengeResult { + defer { + invalidate() } - 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) + 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 } } - 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 + private var service: ThreeDS2Service? // 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() + private func invalidate() { + service?.cleanUp() + service = nil } // MARK: - Utils - private func convertToAuthenticationRequest( - request: AuthenticationRequestParameters - ) -> PO3DS2AuthenticationRequest { - let authenticationRequest = PO3DS2AuthenticationRequest( + 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 ) - return authenticationRequest } - private func convertToChallengeParameters(data: PO3DS2Challenge) -> ChallengeParameters { - let challengeParameters = ChallengeParameters( - threeDSServerTransactionID: data.threeDSServerTransactionId, - acsTransactionID: data.acsTransactionId, - acsRefNumber: data.acsReferenceNumber, - acsSignedContent: data.acsSignedContent + private func challengeParameters(with parameters: PO3DS2ChallengeParameters) -> ChallengeParameters { + ChallengeParameters( + threeDSServerTransactionID: parameters.threeDSServerTransactionId, + acsTransactionID: parameters.acsTransactionId, + acsRefNumber: parameters.acsReferenceNumber, + acsSignedContent: parameters.acsSignedContent ) - return challengeParameters } - private func extractStatus(authenticationResult: AuthenticationResult) -> Bool { - authenticationResult.transactionStatus?.uppercased() == "Y" + 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/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/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/Sources/Backports/Task/View+Task.swift b/Sources/ProcessOutCoreUI/Sources/Backports/Task/View+Task.swift index 2da36731f..48200ceae 100644 --- a/Sources/ProcessOutCoreUI/Sources/Backports/Task/View+Task.swift +++ b/Sources/ProcessOutCoreUI/Sources/Backports/Task/View+Task.swift @@ -16,7 +16,7 @@ extension POBackport where Wrapped: View { public func task( id value: T, priority: TaskPriority = .userInitiated, - @_inheritActorContext _ action: @escaping @Sendable () async -> Void + _ action: @escaping @Sendable @isolated(any) () async -> Void ) -> some View where T: Equatable { if #available(iOS 15, *) { wrapped.task(id: value, priority: priority, action) @@ -29,7 +29,7 @@ extension POBackport where Wrapped: View { @available(iOS 14, *) @ViewBuilder public func task( - priority: TaskPriority = .userInitiated, @_inheritActorContext _ 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/DesignSystem/AsyncImage/POAsyncImage.swift b/Sources/ProcessOutCoreUI/Sources/DesignSystem/AsyncImage/POAsyncImage.swift index 7ed2f7e67..d7dcc44c9 100644 --- a/Sources/ProcessOutCoreUI/Sources/DesignSystem/AsyncImage/POAsyncImage.swift +++ b/Sources/ProcessOutCoreUI/Sources/DesignSystem/AsyncImage/POAsyncImage.swift @@ -16,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 ) { @@ -35,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 @@ -54,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/ProcessOutUI/Sources/Api/Test3DS/POTest3DSService.swift b/Sources/ProcessOutUI/Sources/Api/Test3DS/POTest3DSService.swift index e4d898a2b..b1741150d 100644 --- a/Sources/ProcessOutUI/Sources/Api/Test3DS/POTest3DSService.swift +++ b/Sources/ProcessOutUI/Sources/Api/Test3DS/POTest3DSService.swift @@ -12,60 +12,40 @@ import ProcessOut /// 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 init() { + // Ignored } - // MARK: - PO3DSService - - public func authenticationRequest( - configuration: PO3DS2Configuration, - completion: @escaping @Sendable (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 @Sendable (Result) -> Void) { - MainActor.assumeIsolated { - guard let presentingViewController = PresentingViewControllerProvider.find() else { - completion(.success(false)) - return - } + @MainActor + public func performChallenge(with parameters: PO3DS2ChallengeParameters) async throws -> PO3DS2ChallengeResult { + guard let presentingViewController = PresentingViewControllerProvider.find() else { + throw POFailure(code: .generic(.mobile)) + } + 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 - completion(.success(true)) + continuation.resume(returning: PO3DS2ChallengeResult(transactionStatus: "Y")) } alertController.addAction(acceptAction) let rejectAction = UIAlertAction(title: String(resource: .Test3DS.reject), style: .default) { _ in - completion(.success(false)) + continuation.resume(returning: PO3DS2ChallengeResult(transactionStatus: "N")) } alertController.addAction(rejectAction) presentingViewController.present(alertController, animated: true) } } - - public func handle(redirect: PO3DSRedirect, completion: @escaping @Sendable (Result) -> Void) { - Task { @MainActor in - let session = POWebAuthenticationSession(redirect: redirect, returnUrl: returnUrl, completion: completion) - if await session.start() { - return - } - let failure = POFailure(message: "Unable to process redirect", code: .generic(.mobile)) - completion(.failure(failure)) - } - } - - // MARK: - Private Properties - - private let returnUrl: URL } diff --git a/Sources/ProcessOutUI/Sources/Modules/3DSRedirect/PO3DSRedirectController.swift b/Sources/ProcessOutUI/Sources/Modules/3DSRedirect/PO3DSRedirectController.swift deleted file mode 100644 index 61538fbe1..000000000 --- a/Sources/ProcessOutUI/Sources/Modules/3DSRedirect/PO3DSRedirectController.swift +++ /dev/null @@ -1,104 +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.") -@MainActor -public final class PO3DSRedirectController: Sendable { - - /// - 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: (@Sendable (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 { - nonisolated(unsafe) 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 2542194e6..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 @Sendable (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 nonisolated 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 1d3d81607..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 @Sendable (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)) - } - ) - setViewModel(viewModel) - viewModel.start() - } - - // MARK: - Private Methods - - private static nonisolated func token(with url: URL) -> String { - let components = URLComponents(url: url, resolvingAgainstBaseURL: true) - return components?.queryItems?.first { $0.name == "token" }?.value ?? "" - } -} From 29f339b5afb14b378fd6253fb71b425a9703975d Mon Sep 17 00:00:00 2001 From: Andrii Vysotskyi Date: Wed, 7 Aug 2024 13:28:53 +0200 Subject: [PATCH 04/10] feat(POM-400): rework APM implementation (#324) * Handle redirects inside AlternativePayments service * Split POAlternativePaymentRequest to tokenization and auth requests * Remove event emitter * Remove explicit deep link handling --- .../Sources/Application/SceneDelegate.swift | 6 - .../AlternativePaymentMethodsBuilder.swift | 1 + .../AlternativePaymentMethodsInteractor.swift | 9 ++ ...ernativePaymentMethodsInteractorType.swift | 3 + .../AlternativePaymentMethodsRoute.swift | 3 - .../AlternativePaymentMethodsRouter.swift | 5 - .../AlternativePaymentMethodsViewModel.swift | 4 +- .../ViewModel/FeaturesViewModel.swift | 1 - .../Api/Models/PODeepLinkReceivedEvent.swift | 15 -- .../ProcessOut/Sources/Api/ProcessOut.swift | 32 ++--- .../Core/EventEmitter/LocalEventEmitter.swift | 91 ------------ .../Core/EventEmitter/POEventEmitter.swift | 17 --- .../EventEmitter/POEventEmitterEvent.swift | 20 --- .../POCreateCustomerTokenRequest.swift | 8 +- .../POAlternativePaymentMethodsService.swift | 26 ---- .../POAlternativePaymentMethodRequest.swift | 79 ----------- .../POAlternativePaymentMethodResponse.swift | 28 ---- ...rnativePaymentsServiceConfiguration.swift} | 4 +- .../DefaultAlternativePaymentsService.swift} | 82 ++++++----- .../POAlternativePaymentsService.swift | 24 ++++ ...ternativePaymentAuthorizationRequest.swift | 38 ++++++ ...lternativePaymentTokenizationRequest.swift | 38 ++++++ .../POAlternativePaymentResponse.swift | 19 +++ ...enticationSession+AlternativePayment.swift | 54 -------- ...ariViewController+AlternativePayment.swift | 76 ----------- ...ckoutAlternativePaymentConfiguration.swift | 10 +- .../DynamicCheckoutDefaultInteractor.swift | 10 +- ...koutAlternativePaymentDefaultSession.swift | 38 ------ ...micCheckoutAlternativePaymentSession.swift | 16 --- .../View/PODynamicCheckoutView+Init.swift | 4 +- .../DefaultSafariViewModel.swift | 129 ------------------ .../POWebAuthenticationSession.swift | 118 ---------------- .../POWebAuthenticationSessionCallback.swift | 21 --- .../SafariViewController+Extensions.swift | 22 --- 34 files changed, 208 insertions(+), 843 deletions(-) delete mode 100644 Sources/ProcessOut/Sources/Api/Models/PODeepLinkReceivedEvent.swift delete mode 100644 Sources/ProcessOut/Sources/Core/EventEmitter/LocalEventEmitter.swift delete mode 100644 Sources/ProcessOut/Sources/Core/EventEmitter/POEventEmitter.swift delete mode 100644 Sources/ProcessOut/Sources/Core/EventEmitter/POEventEmitterEvent.swift delete mode 100644 Sources/ProcessOut/Sources/Services/AlternativePaymentMethods/POAlternativePaymentMethodsService.swift delete mode 100644 Sources/ProcessOut/Sources/Services/AlternativePaymentMethods/Requests/POAlternativePaymentMethodRequest.swift delete mode 100644 Sources/ProcessOut/Sources/Services/AlternativePaymentMethods/Responses/POAlternativePaymentMethodResponse.swift rename Sources/ProcessOut/Sources/Services/{AlternativePaymentMethods/AlternativePaymentMethodsServiceConfiguration.swift => AlternativePayments/AlternativePaymentsServiceConfiguration.swift} (59%) rename Sources/ProcessOut/Sources/Services/{AlternativePaymentMethods/DefaultAlternativePaymentMethodsService.swift => AlternativePayments/DefaultAlternativePaymentsService.swift} (51%) create mode 100644 Sources/ProcessOut/Sources/Services/AlternativePayments/POAlternativePaymentsService.swift create mode 100644 Sources/ProcessOut/Sources/Services/AlternativePayments/Requests/POAlternativePaymentAuthorizationRequest.swift create mode 100644 Sources/ProcessOut/Sources/Services/AlternativePayments/Requests/POAlternativePaymentTokenizationRequest.swift create mode 100644 Sources/ProcessOut/Sources/Services/AlternativePayments/Responses/POAlternativePaymentResponse.swift delete mode 100644 Sources/ProcessOutUI/Sources/Modules/AlternativePayment/POWebAuthenticationSession+AlternativePayment.swift delete mode 100644 Sources/ProcessOutUI/Sources/Modules/AlternativePayment/SFSafariViewController+AlternativePayment.swift delete mode 100644 Sources/ProcessOutUI/Sources/Modules/DynamicCheckout/Sessions/AlternativePayment/DynamicCheckoutAlternativePaymentDefaultSession.swift delete mode 100644 Sources/ProcessOutUI/Sources/Modules/DynamicCheckout/Sessions/AlternativePayment/DynamicCheckoutAlternativePaymentSession.swift delete mode 100644 Sources/ProcessOutUI/Sources/Modules/WebAuthentication/DefaultSafariViewModel.swift delete mode 100644 Sources/ProcessOutUI/Sources/Modules/WebAuthentication/POWebAuthenticationSession.swift delete mode 100644 Sources/ProcessOutUI/Sources/Modules/WebAuthentication/POWebAuthenticationSessionCallback.swift delete mode 100644 Sources/ProcessOutUI/Sources/Modules/WebAuthentication/SafariViewController+Extensions.swift diff --git a/Example/Example/Sources/Application/SceneDelegate.swift b/Example/Example/Sources/Application/SceneDelegate.swift index 0619074f0..5c87f8763 100644 --- a/Example/Example/Sources/Application/SceneDelegate.swift +++ b/Example/Example/Sources/Application/SceneDelegate.swift @@ -22,10 +22,4 @@ final class SceneDelegate: UIResponder, UIWindowSceneDelegate { window?.rootViewController = FeaturesBuilder().build() window?.makeKeyAndVisible() } - - func scene(_ scene: UIScene, openURLContexts urlContexts: Set) { - if let url = urlContexts.first?.url { - ProcessOut.shared.processDeepLink(url: url) - } - } } diff --git a/Example/Example/Sources/UI/Modules/AlternativePayment/Methods/AlternativePaymentMethodsBuilder.swift b/Example/Example/Sources/UI/Modules/AlternativePayment/Methods/AlternativePaymentMethodsBuilder.swift index 0e015c28d..92b3e4829 100644 --- a/Example/Example/Sources/UI/Modules/AlternativePayment/Methods/AlternativePaymentMethodsBuilder.swift +++ b/Example/Example/Sources/UI/Modules/AlternativePayment/Methods/AlternativePaymentMethodsBuilder.swift @@ -19,6 +19,7 @@ final class AlternativePaymentMethodsBuilder { let interactor = AlternativePaymentMethodsInteractor( gatewayConfigurationsRepository: ProcessOut.shared.gatewayConfigurations, invoicesService: ProcessOut.shared.invoices, + alternativePaymentsService: ProcessOut.shared.alternativePayments, filter: filter ) let router = AlternativePaymentMethodsRouter() diff --git a/Example/Example/Sources/UI/Modules/AlternativePayment/Methods/Interactor/AlternativePaymentMethodsInteractor.swift b/Example/Example/Sources/UI/Modules/AlternativePayment/Methods/Interactor/AlternativePaymentMethodsInteractor.swift index 17583eb1a..88ba3c440 100644 --- a/Example/Example/Sources/UI/Modules/AlternativePayment/Methods/Interactor/AlternativePaymentMethodsInteractor.swift +++ b/Example/Example/Sources/UI/Modules/AlternativePayment/Methods/Interactor/AlternativePaymentMethodsInteractor.swift @@ -14,10 +14,12 @@ final class AlternativePaymentMethodsInteractor: init( gatewayConfigurationsRepository: POGatewayConfigurationsRepository, invoicesService: POInvoicesService, + alternativePaymentsService: POAlternativePaymentsService, filter: POAllGatewayConfigurationsRequest.Filter? ) { self.gatewayConfigurationsRepository = gatewayConfigurationsRepository self.invoicesService = invoicesService + self.alternativePaymentsService = alternativePaymentsService self.filter = filter super.init(state: .idle) } @@ -111,6 +113,12 @@ final class AlternativePaymentMethodsInteractor: } } + func authorize(request: POAlternativePaymentAuthorizationRequest) { + Task { + try await alternativePaymentsService.authorize(request: request) + } + } + // MARK: - Private Nested Types private enum Constants { @@ -121,6 +129,7 @@ final class AlternativePaymentMethodsInteractor: private let gatewayConfigurationsRepository: POGatewayConfigurationsRepository private let invoicesService: POInvoicesService + private let alternativePaymentsService: POAlternativePaymentsService private let filter: POAllGatewayConfigurationsRequest.Filter? // MARK: - State Management diff --git a/Example/Example/Sources/UI/Modules/AlternativePayment/Methods/Interactor/AlternativePaymentMethodsInteractorType.swift b/Example/Example/Sources/UI/Modules/AlternativePayment/Methods/Interactor/AlternativePaymentMethodsInteractorType.swift index 40e1eea68..7a528041b 100644 --- a/Example/Example/Sources/UI/Modules/AlternativePayment/Methods/Interactor/AlternativePaymentMethodsInteractorType.swift +++ b/Example/Example/Sources/UI/Modules/AlternativePayment/Methods/Interactor/AlternativePaymentMethodsInteractorType.swift @@ -16,6 +16,9 @@ protocol AlternativePaymentMethodsInteractorType: InteractorType Void) + /// Authorizes alternative payment using given request. + func authorize(request: POAlternativePaymentAuthorizationRequest) + /// Loads more data if possible. func loadMore() } diff --git a/Example/Example/Sources/UI/Modules/AlternativePayment/Methods/Router/AlternativePaymentMethodsRoute.swift b/Example/Example/Sources/UI/Modules/AlternativePayment/Methods/Router/AlternativePaymentMethodsRoute.swift index 576b8988c..986668454 100644 --- a/Example/Example/Sources/UI/Modules/AlternativePayment/Methods/Router/AlternativePaymentMethodsRoute.swift +++ b/Example/Example/Sources/UI/Modules/AlternativePayment/Methods/Router/AlternativePaymentMethodsRoute.swift @@ -25,9 +25,6 @@ enum AlternativePaymentMethodsRoute: RouteType { /// Alternative payment executed natively. case nativeAlternativePayment(NativeAlternativePayment) - /// Alternative payment. - case alternativePayment(request: POAlternativePaymentMethodRequest) - /// Asks user for authorisation amount and currency. case authorizationtAmount(completion: (Decimal, String) -> Void) diff --git a/Example/Example/Sources/UI/Modules/AlternativePayment/Methods/Router/AlternativePaymentMethodsRouter.swift b/Example/Example/Sources/UI/Modules/AlternativePayment/Methods/Router/AlternativePaymentMethodsRouter.swift index 97af6bc21..fb93edaf6 100644 --- a/Example/Example/Sources/UI/Modules/AlternativePayment/Methods/Router/AlternativePaymentMethodsRouter.swift +++ b/Example/Example/Sources/UI/Modules/AlternativePayment/Methods/Router/AlternativePaymentMethodsRouter.swift @@ -36,11 +36,6 @@ final class AlternativePaymentMethodsRouter: RouterType { ) viewController.isModalInPresentation = true self.viewController?.present(viewController, animated: true) - case let .alternativePayment(request): - let session = POWebAuthenticationSession(request: request, returnUrl: Constants.returnUrl) { _ in } - Task { - _ = await session.start() - } case let .authorizationtAmount(completion): let viewController = AuthorizationAmountBuilder(completion: completion).build() self.viewController?.present(viewController, animated: true) diff --git a/Example/Example/Sources/UI/Modules/AlternativePayment/Methods/ViewModel/AlternativePaymentMethodsViewModel.swift b/Example/Example/Sources/UI/Modules/AlternativePayment/Methods/ViewModel/AlternativePaymentMethodsViewModel.swift index 4cd287012..6df0d5258 100644 --- a/Example/Example/Sources/UI/Modules/AlternativePayment/Methods/ViewModel/AlternativePaymentMethodsViewModel.swift +++ b/Example/Example/Sources/UI/Modules/AlternativePayment/Methods/ViewModel/AlternativePaymentMethodsViewModel.swift @@ -130,12 +130,12 @@ final class AlternativePaymentMethodsViewModel: route = .nativeAlternativePayment(paymentRoute) } else { route = AlternativePaymentMethodsRoute.additionalData { [weak self] additionalData in - let request = POAlternativePaymentMethodRequest( + let request = POAlternativePaymentAuthorizationRequest( invoiceId: invoice.id, gatewayConfigurationId: gatewayConfiguration.id, additionalData: additionalData ) - self?.router.trigger(route: .alternativePayment(request: request)) + self?.interactor.authorize(request: request) } } self?.router.trigger(route: route) diff --git a/Example/Example/Sources/UI/Modules/Features/ViewModel/FeaturesViewModel.swift b/Example/Example/Sources/UI/Modules/Features/ViewModel/FeaturesViewModel.swift index 3ed8d4511..670a3b85e 100644 --- a/Example/Example/Sources/UI/Modules/Features/ViewModel/FeaturesViewModel.swift +++ b/Example/Example/Sources/UI/Modules/Features/ViewModel/FeaturesViewModel.swift @@ -102,7 +102,6 @@ final class FeaturesViewModel: BaseViewModel, FeaturesVi } let configuration = PODynamicCheckoutConfiguration( invoiceRequest: .init(invoiceId: invoice.id, clientSecret: invoice.clientSecret), - alternativePayment: .init(returnUrl: Constants.returnUrl), cancelButton: .init(confirmation: .init()) ) self.router.trigger(route: .dynamicCheckout(configuration: configuration, delegate: self)) diff --git a/Sources/ProcessOut/Sources/Api/Models/PODeepLinkReceivedEvent.swift b/Sources/ProcessOut/Sources/Api/Models/PODeepLinkReceivedEvent.swift deleted file mode 100644 index 6a474d5da..000000000 --- a/Sources/ProcessOut/Sources/Api/Models/PODeepLinkReceivedEvent.swift +++ /dev/null @@ -1,15 +0,0 @@ -// -// PODeepLinkReceivedEvent.swift -// ProcessOut -// -// Created by Andrii Vysotskyi on 10.05.2023. -// - -import Foundation - -@_spi(PO) -public struct PODeepLinkReceivedEvent: POEventEmitterEvent { - - /// Url representing deep link or universal link. - public let url: URL -} diff --git a/Sources/ProcessOut/Sources/Api/ProcessOut.swift b/Sources/ProcessOut/Sources/Api/ProcessOut.swift index a6d80fa7e..54c782b68 100644 --- a/Sources/ProcessOut/Sources/Api/ProcessOut.swift +++ b/Sources/ProcessOut/Sources/Api/ProcessOut.swift @@ -28,8 +28,8 @@ public final class ProcessOut: @unchecked Sendable { /// Invoices service. public private(set) var invoices: POInvoicesService! - /// Alternative payment methods service. - public private(set) var alternativePaymentMethods: POAlternativePaymentMethodsService! + /// Alternative payments service. + public private(set) var alternativePayments: POAlternativePaymentsService! /// Cards service. public private(set) var cards: POCardsService! @@ -37,26 +37,12 @@ public final class ProcessOut: @unchecked Sendable { /// Returns customer tokens service. public private(set) var customerTokens: POCustomerTokensService! - /// Call this method in your app or scene delegate whenever your implementation receives incoming URL. Only deep - /// links are supported. - /// - /// - Returns: `true` if the URL is expected and will be handled by SDK. `false` otherwise. - @discardableResult - public func processDeepLink(url: URL) -> Bool { - logger.debug("Will process deep link: \(url)") - return eventEmitter.emit(event: PODeepLinkReceivedEvent(url: url)) - } - // MARK: - SPI /// Logger with application category. @_spi(PO) public private(set) var logger: POLogger! - /// Event emitter to use for events exchange. - @_spi(PO) - public private(set) var eventEmitter: POEventEmitter! - /// Images repository. @_spi(PO) public let images: POImagesRepository = UrlSessionImagesRepository(session: .shared) @@ -107,14 +93,13 @@ public final class ProcessOut: @unchecked Sendable { invoices = Self.createInvoicesService( httpConnector: httpConnector, threeDSService: threeDSService, logger: logger ) - alternativePaymentMethods = createAlternativePaymentsService() + alternativePayments = createAlternativePaymentsService() cards = Self.createCardsService( httpConnector: httpConnector, logger: logger ) customerTokens = Self.createCustomerTokensService( httpConnector: httpConnector, threeDSService: threeDSService, logger: logger ) - eventEmitter = LocalEventEmitter(logger: logger) } // MARK: - @@ -145,21 +130,24 @@ public final class ProcessOut: @unchecked Sendable { return DefaultCustomerTokensService(repository: repository, threeDSService: threeDSService, logger: logger) } - private func createAlternativePaymentsService() -> POAlternativePaymentMethodsService { - let serviceConfiguration = { @Sendable [unowned self] () -> AlternativePaymentMethodsServiceConfiguration in + private func createAlternativePaymentsService() -> POAlternativePaymentsService { + let serviceConfiguration = { @Sendable [unowned self] () -> AlternativePaymentsServiceConfiguration in let configuration = self.configuration return .init(projectId: configuration.projectId, baseUrl: configuration.checkoutBaseUrl) } - return DefaultAlternativePaymentMethodsService(configuration: serviceConfiguration, logger: logger) + let webSession = WebAuthenticationSession() + return DefaultAlternativePaymentsService( + configuration: serviceConfiguration, webSession: webSession, logger: logger + ) } private static func create3DSService() -> DefaultThreeDSService { - let webSession = WebAuthenticationSession() let decoder = JSONDecoder() decoder.keyDecodingStrategy = .useDefaultKeys let encoder = JSONEncoder() encoder.dataEncodingStrategy = .base64 encoder.keyEncodingStrategy = .useDefaultKeys + let webSession = WebAuthenticationSession() return DefaultThreeDSService(decoder: decoder, encoder: encoder, webSession: webSession) } diff --git a/Sources/ProcessOut/Sources/Core/EventEmitter/LocalEventEmitter.swift b/Sources/ProcessOut/Sources/Core/EventEmitter/LocalEventEmitter.swift deleted file mode 100644 index 4f51bf8cb..000000000 --- a/Sources/ProcessOut/Sources/Core/EventEmitter/LocalEventEmitter.swift +++ /dev/null @@ -1,91 +0,0 @@ -// -// LocalEventEmitter.swift -// ProcessOut -// -// Created by Andrii Vysotskyi on 10.05.2023. -// - -import Foundation - -final class LocalEventEmitter: POEventEmitter, @unchecked Sendable { - - init(logger: POLogger) { - self.logger = logger - lock = NSLock() - subscriptions = [:] - } - - // MARK: - POEventEmitter - - func emit(event: Event) -> Bool { - lock.lock() - guard let eventSubscriptions = subscriptions[Event.name]?.values, !eventSubscriptions.isEmpty else { - lock.unlock() - logger.debug("No subscribers for '\(Event.name)' event, ignored") - return false - } - lock.unlock() - var isHandled = false - for subscription in eventSubscriptions { - // Event should be delivered to all subscribers. - isHandled = subscription.listener(event) || isHandled - } - if !isHandled { - logger.debug("Subscribers refused to handle '\(Event.name)' event") - } - return isHandled - } - - func on(_ eventType: Event.Type, listener: @escaping (Event) -> Bool) -> AnyObject { - let subscription = Subscription { event in - guard let event = event as? Event else { - return false - } - return listener(event) - } - let subscriptionId = UUID().uuidString - lock.lock() - if subscriptions[Event.name] != nil { - subscriptions[Event.name]?[subscriptionId] = subscription - } else { - subscriptions[Event.name] = [subscriptionId: subscription] - } - lock.unlock() - let cancellable = Cancellable { [weak self] in - guard let self = self else { - return - } - self.lock.lock() - self.subscriptions[Event.name]?[subscriptionId] = nil - self.lock.unlock() - } - return cancellable - } - - // MARK: - Private Nested Types - - private struct Subscription { - - /// Type erased listener. - let listener: (Any) -> Bool - } - - private final class Cancellable { - - let didCancel: () -> Void - - init(didCancel: @escaping () -> Void) { - self.didCancel = didCancel - } - - deinit { - didCancel() - } - } - - // MARK: - Private Properties - - private let logger: POLogger - private let lock: NSLock - private var subscriptions: [String: [AnyHashable: Subscription]] -} diff --git a/Sources/ProcessOut/Sources/Core/EventEmitter/POEventEmitter.swift b/Sources/ProcessOut/Sources/Core/EventEmitter/POEventEmitter.swift deleted file mode 100644 index 4c03b1878..000000000 --- a/Sources/ProcessOut/Sources/Core/EventEmitter/POEventEmitter.swift +++ /dev/null @@ -1,17 +0,0 @@ -// -// POEventEmitter.swift -// ProcessOut -// -// Created by Andrii Vysotskyi on 10.05.2023. -// - -import Foundation - -@_spi(PO) public protocol POEventEmitter: Sendable { - - /// Emits given event. - func emit(event: Event) -> Bool - - /// Adds subscription for given event. - func on(_ eventType: Event.Type, listener: @escaping (Event) -> Bool) -> AnyObject -} diff --git a/Sources/ProcessOut/Sources/Core/EventEmitter/POEventEmitterEvent.swift b/Sources/ProcessOut/Sources/Core/EventEmitter/POEventEmitterEvent.swift deleted file mode 100644 index 1f96154df..000000000 --- a/Sources/ProcessOut/Sources/Core/EventEmitter/POEventEmitterEvent.swift +++ /dev/null @@ -1,20 +0,0 @@ -// -// POEventEmitterEvent.swift -// ProcessOut -// -// Created by Andrii Vysotskyi on 10.05.2023. -// - -@_spi(PO) -public protocol POEventEmitterEvent: Sendable { - - /// Event name. - static var name: String { get } -} - -extension POEventEmitterEvent { - - public static var name: String { - String(describing: self) - } -} diff --git a/Sources/ProcessOut/Sources/Repositories/CustomerTokens/Requests/POCreateCustomerTokenRequest.swift b/Sources/ProcessOut/Sources/Repositories/CustomerTokens/Requests/POCreateCustomerTokenRequest.swift index db3a39304..5e3da407c 100644 --- a/Sources/ProcessOut/Sources/Repositories/CustomerTokens/Requests/POCreateCustomerTokenRequest.swift +++ b/Sources/ProcessOut/Sources/Repositories/CustomerTokens/Requests/POCreateCustomerTokenRequest.swift @@ -20,9 +20,13 @@ public struct POCreateCustomerTokenRequest: Encodable, Sendable { /// Return URL to assign to verification invoice. public let invoiceReturnUrl: URL? - public init(customerId: String, verify: Bool = false, invoiceReturnUrl: URL? = nil) { + /// Return URL. + public let returnUrl: URL? + + public init(customerId: String, verify: Bool = false, returnUrl: URL? = nil) { self._customerId = .init(value: customerId) self.verify = verify - self.invoiceReturnUrl = invoiceReturnUrl + self.invoiceReturnUrl = returnUrl + self.returnUrl = returnUrl } } diff --git a/Sources/ProcessOut/Sources/Services/AlternativePaymentMethods/POAlternativePaymentMethodsService.swift b/Sources/ProcessOut/Sources/Services/AlternativePaymentMethods/POAlternativePaymentMethodsService.swift deleted file mode 100644 index fc95f9c03..000000000 --- a/Sources/ProcessOut/Sources/Services/AlternativePaymentMethods/POAlternativePaymentMethodsService.swift +++ /dev/null @@ -1,26 +0,0 @@ -// -// POAlternativePaymentMethodsService.swift -// ProcessOut -// -// Created by Simeon Kostadinov on 27/10/2022. -// - -import Foundation - -@available(*, deprecated, renamed: "POAlternativePaymentMethodsService") -public typealias POAlternativePaymentMethodsServiceType = POAlternativePaymentMethodsService - -/// Service that provides set of methods to work with alternative payments. -public protocol POAlternativePaymentMethodsService: POService { - - /// Creates the redirection URL for APM Payments and APM token creation. - /// - /// - Parameter request: request containing information needed to build the URL. - func alternativePaymentMethodUrl(request: POAlternativePaymentMethodRequest) -> URL - - /// Convert given APMs response URL into response object. - /// - /// - Parameter url: url response that our checkout service sends back when the customer gets redirected. - /// - Returns: response parsed from given url. - func alternativePaymentMethodResponse(url: URL) throws -> POAlternativePaymentMethodResponse -} diff --git a/Sources/ProcessOut/Sources/Services/AlternativePaymentMethods/Requests/POAlternativePaymentMethodRequest.swift b/Sources/ProcessOut/Sources/Services/AlternativePaymentMethods/Requests/POAlternativePaymentMethodRequest.swift deleted file mode 100644 index 20087ddfd..000000000 --- a/Sources/ProcessOut/Sources/Services/AlternativePaymentMethods/Requests/POAlternativePaymentMethodRequest.swift +++ /dev/null @@ -1,79 +0,0 @@ -// -// POAlternativePaymentMethodRequest.swift -// ProcessOut -// -// Created by Simeon Kostadinov on 27/10/2022. -// - -// todo(andrii-vysotskyi): consider splitting request into separate tokenization and authorization requests - -import Foundation - -/// Request describing parameters that are used to create URL that user can be redirected to initiate -/// alternative payment. -/// -/// - NOTE: Make sure to supply proper `additionalData` specific for particular payment -/// method. -public struct POAlternativePaymentMethodRequest: Sendable { - - /// Invoice identifier to to perform APM payment for. - public let invoiceId: String - - /// Gateway Configuration ID of the APM the payment will be made on. - public let gatewayConfigurationId: String - - /// Customer ID that may be used for creating APM recurring token. - public let customerId: String? - - /// Customer token ID that may be used for creating APM recurring token. - public let tokenId: String? - - /// Additional Data that will be supplied to the APM. - public let additionalData: [String: String]? - - @_disfavoredOverload - @available(*, deprecated, message: "Use other init that creates either tokenization or payment request explicitly.") - public init( - invoiceId: String, - gatewayConfigurationId: String, - additionalData: [String: String]? = nil, - customerId: String? = nil, - tokenId: String? = nil - ) { - self.invoiceId = invoiceId - self.gatewayConfigurationId = gatewayConfigurationId - self.additionalData = additionalData - self.customerId = customerId - self.tokenId = tokenId - } - - /// Creates a request that can be used to tokenize APM. - public init( - customerId: String, - tokenId: String, - gatewayConfigurationId: String, - additionalData: [String: String]? = nil - ) { - self.invoiceId = "" - self.customerId = customerId - self.tokenId = tokenId - self.gatewayConfigurationId = gatewayConfigurationId - self.additionalData = additionalData - } - - /// Creates a request that can be used to authorize APM. - /// - Parameters: - /// - tokenId: when value is set invoice is being authorized with previously tokenized APM. - public init( - invoiceId: String, - gatewayConfigurationId: String, - tokenId: String? = nil, - additionalData: [String: String]? = nil - ) { - self.invoiceId = invoiceId - self.gatewayConfigurationId = gatewayConfigurationId - self.customerId = nil - self.tokenId = tokenId - self.additionalData = additionalData - } -} diff --git a/Sources/ProcessOut/Sources/Services/AlternativePaymentMethods/Responses/POAlternativePaymentMethodResponse.swift b/Sources/ProcessOut/Sources/Services/AlternativePaymentMethods/Responses/POAlternativePaymentMethodResponse.swift deleted file mode 100644 index b849ad69e..000000000 --- a/Sources/ProcessOut/Sources/Services/AlternativePaymentMethods/Responses/POAlternativePaymentMethodResponse.swift +++ /dev/null @@ -1,28 +0,0 @@ -// -// POAlternativePaymentMethodResponse.swift -// ProcessOut -// -// Created by Simeon Kostadinov on 27/10/2022. -// - -import Foundation - -/// Result of alternative payment. -public struct POAlternativePaymentMethodResponse: Sendable { - - public enum APMReturnType: Sendable { - case authorization, createToken - } - - /// Gateway token starting with prefix gway_req_ that can be used to perform a sale call. - public let gatewayToken: String - - /// Customer ID that may be used for creating APM recurring token. - public let customerId: String? - - /// Customer token ID that may be used for creating APM recurring token. - public let tokenId: String? - - /// returnType informs if this was an APM token creation or a payment creation response. - public let returnType: APMReturnType -} diff --git a/Sources/ProcessOut/Sources/Services/AlternativePaymentMethods/AlternativePaymentMethodsServiceConfiguration.swift b/Sources/ProcessOut/Sources/Services/AlternativePayments/AlternativePaymentsServiceConfiguration.swift similarity index 59% rename from Sources/ProcessOut/Sources/Services/AlternativePaymentMethods/AlternativePaymentMethodsServiceConfiguration.swift rename to Sources/ProcessOut/Sources/Services/AlternativePayments/AlternativePaymentsServiceConfiguration.swift index 2b32d59c0..93915eebc 100644 --- a/Sources/ProcessOut/Sources/Services/AlternativePaymentMethods/AlternativePaymentMethodsServiceConfiguration.swift +++ b/Sources/ProcessOut/Sources/Services/AlternativePayments/AlternativePaymentsServiceConfiguration.swift @@ -1,5 +1,5 @@ // -// AlternativePaymentMethodsServiceConfiguration.swift +// AlternativePaymentsServiceConfiguration.swift // ProcessOut // // Created by Andrii Vysotskyi on 13.02.2024. @@ -7,7 +7,7 @@ import Foundation -struct AlternativePaymentMethodsServiceConfiguration: Sendable { +struct AlternativePaymentsServiceConfiguration: Sendable { /// Project ID. let projectId: String diff --git a/Sources/ProcessOut/Sources/Services/AlternativePaymentMethods/DefaultAlternativePaymentMethodsService.swift b/Sources/ProcessOut/Sources/Services/AlternativePayments/DefaultAlternativePaymentsService.swift similarity index 51% rename from Sources/ProcessOut/Sources/Services/AlternativePaymentMethods/DefaultAlternativePaymentMethodsService.swift rename to Sources/ProcessOut/Sources/Services/AlternativePayments/DefaultAlternativePaymentsService.swift index c0f4dd64d..b5548c440 100644 --- a/Sources/ProcessOut/Sources/Services/AlternativePaymentMethods/DefaultAlternativePaymentMethodsService.swift +++ b/Sources/ProcessOut/Sources/Services/AlternativePayments/DefaultAlternativePaymentsService.swift @@ -1,5 +1,5 @@ // -// DefaultAlternativePaymentMethodsService.swift +// DefaultAlternativePaymentsService.swift // ProcessOut // // Created by Simeon Kostadinov on 27/10/2022. @@ -7,41 +7,68 @@ import Foundation -final class DefaultAlternativePaymentMethodsService: POAlternativePaymentMethodsService { +final class DefaultAlternativePaymentsService: POAlternativePaymentsService { - init(configuration: @escaping @Sendable () -> AlternativePaymentMethodsServiceConfiguration, logger: POLogger) { + init( + configuration: @escaping @Sendable () -> AlternativePaymentsServiceConfiguration, + webSession: WebAuthenticationSession, + logger: POLogger + ) { self.configuration = configuration + self.webSession = webSession self.logger = logger } - // MARK: - POAlternativePaymentMethodsService + // MARK: - POAlternativePaymentsService - func alternativePaymentMethodUrl(request: POAlternativePaymentMethodRequest) -> URL { + func tokenize(request: POAlternativePaymentTokenizationRequest) async throws -> POAlternativePaymentResponse { + let pathComponents = [request.customerId, request.tokenId, "redirect", request.gatewayConfigurationId] + let redirectUrl = try url(with: pathComponents, additionalData: request.additionalData) + return try await authenticate(using: redirectUrl) + } + + func authorize(request: POAlternativePaymentAuthorizationRequest) async throws -> POAlternativePaymentResponse { + var pathComponents = [request.invoiceId, "redirect", request.gatewayConfigurationId] + if let tokenId = request.tokenId { + pathComponents += ["tokenized", tokenId] + } + let redirectUrl = try url(with: pathComponents, additionalData: request.additionalData) + return try await authenticate(using: redirectUrl) + } + + func authenticate(using url: URL) async throws -> POAlternativePaymentResponse { + let returnUrl = try await webSession.authenticate(using: url) + return try response(from: returnUrl) + } + + // MARK: - Private + + private let configuration: @Sendable () -> AlternativePaymentsServiceConfiguration + private let logger: POLogger + private let webSession: WebAuthenticationSession + + // MARK: - Request + + /// - NOTE: Method prepends project ID to path components automatically. + private func url(with additionalPathComponents: [String], additionalData: [String: String]?) throws -> URL { let configuration = self.configuration() guard var components = URLComponents(url: configuration.baseUrl, resolvingAgainstBaseURL: true) else { - preconditionFailure("Failed to create components from base url.") - } - var pathComponents: [String] - if let customerId = request.customerId, let tokenId = request.tokenId { - pathComponents = [configuration.projectId, customerId, tokenId, "redirect", request.gatewayConfigurationId] - } else { - precondition(!request.invoiceId.isEmpty, "Invoice ID must be set.") - pathComponents = [configuration.projectId, request.invoiceId, "redirect", request.gatewayConfigurationId] - if let tokenId = request.tokenId { - pathComponents += ["tokenized", tokenId] - } + preconditionFailure("Invalid base URL.") } + let pathComponents = [configuration.projectId] + additionalPathComponents components.path = "/" + pathComponents.joined(separator: "/") - components.queryItems = request.additionalData?.map { data in + components.queryItems = additionalData?.map { data in URLQueryItem(name: "additional_data[" + data.key + "]", value: data.value) } - guard let url = components.url else { - preconditionFailure("Failed to create APM redirection URL.") + if let url = components.url { + return url } - return url + throw POFailure(message: "Unable to create redirect URL.", code: .generic(.mobile)) } - func alternativePaymentMethodResponse(url: URL) throws -> POAlternativePaymentMethodResponse { + // MARK: - Response + + private func response(from url: URL) throws -> POAlternativePaymentResponse { guard let components = URLComponents(url: url, resolvingAgainstBaseURL: true) else { let message = "Invalid or malformed Alternative Payment Method URL response provided." throw POFailure(message: message, code: .generic(.mobile), underlyingError: nil) @@ -54,20 +81,9 @@ final class DefaultAlternativePaymentMethodsService: POAlternativePaymentMethods if gatewayToken.isEmpty { logger.debug("Gateway 'token' is not set in \(url), this may be an error.") } - let tokenId = queryItems.queryItemValue(name: "token_id") - if let customerId = queryItems.queryItemValue(name: "customer_id"), let tokenId { - return .init(gatewayToken: gatewayToken, customerId: customerId, tokenId: tokenId, returnType: .createToken) - } - return .init(gatewayToken: gatewayToken, customerId: nil, tokenId: tokenId, returnType: .authorization) + return .init(gatewayToken: gatewayToken) } - // MARK: - Private - - private let configuration: @Sendable () -> AlternativePaymentMethodsServiceConfiguration - private let logger: POLogger - - // MARK: - Private Methods - private func createFailureCode(rawValue: String) -> POFailure.Code { if let code = POFailure.AuthenticationCode(rawValue: rawValue) { return .authentication(code) diff --git a/Sources/ProcessOut/Sources/Services/AlternativePayments/POAlternativePaymentsService.swift b/Sources/ProcessOut/Sources/Services/AlternativePayments/POAlternativePaymentsService.swift new file mode 100644 index 000000000..65dd0990f --- /dev/null +++ b/Sources/ProcessOut/Sources/Services/AlternativePayments/POAlternativePaymentsService.swift @@ -0,0 +1,24 @@ +// +// POAlternativePaymentsService.swift +// ProcessOut +// +// Created by Simeon Kostadinov on 27/10/2022. +// + +import Foundation + +@available(*, deprecated, renamed: "POAlternativePaymentsService") +public typealias POAlternativePaymentMethodsServiceType = POAlternativePaymentsService + +/// Service that provides set of methods to work with alternative payments. +public protocol POAlternativePaymentsService: POService { + + /// Attempts to tokenize APM using given request. + func tokenize(request: POAlternativePaymentTokenizationRequest) async throws -> POAlternativePaymentResponse + + /// Authorizes invoice using given request. + func authorize(request: POAlternativePaymentAuthorizationRequest) async throws -> POAlternativePaymentResponse + + /// Authenticates alternative payment using given raw URL. + func authenticate(using url: URL) async throws -> POAlternativePaymentResponse +} diff --git a/Sources/ProcessOut/Sources/Services/AlternativePayments/Requests/POAlternativePaymentAuthorizationRequest.swift b/Sources/ProcessOut/Sources/Services/AlternativePayments/Requests/POAlternativePaymentAuthorizationRequest.swift new file mode 100644 index 000000000..91ff4692d --- /dev/null +++ b/Sources/ProcessOut/Sources/Services/AlternativePayments/Requests/POAlternativePaymentAuthorizationRequest.swift @@ -0,0 +1,38 @@ +// +// POAlternativePaymentAuthorizationRequest.swift +// ProcessOut +// +// Created by Andrii Vysotskyi on 05.08.2024. +// + +/// Invoice authorization request. +/// +/// - NOTE: Make sure to supply proper `additionalData` specific for particular payment +/// method. +public struct POAlternativePaymentAuthorizationRequest: Sendable { + + /// Invoice identifier to to perform APM payment for. + public let invoiceId: String + + /// Gateway Configuration ID of the APM the payment will be made on. + public let gatewayConfigurationId: String + + /// When value is set invoice is being authorized with previously tokenized APM. + public let tokenId: String? + + /// Additional Data that will be supplied to the APM. + public let additionalData: [String: String]? + + /// Creates authorization request. + public init( + invoiceId: String, + gatewayConfigurationId: String, + tokenId: String? = nil, + additionalData: [String: String]? = nil + ) { + self.invoiceId = invoiceId + self.gatewayConfigurationId = gatewayConfigurationId + self.tokenId = tokenId + self.additionalData = additionalData + } +} diff --git a/Sources/ProcessOut/Sources/Services/AlternativePayments/Requests/POAlternativePaymentTokenizationRequest.swift b/Sources/ProcessOut/Sources/Services/AlternativePayments/Requests/POAlternativePaymentTokenizationRequest.swift new file mode 100644 index 000000000..eccf408c9 --- /dev/null +++ b/Sources/ProcessOut/Sources/Services/AlternativePayments/Requests/POAlternativePaymentTokenizationRequest.swift @@ -0,0 +1,38 @@ +// +// POAlternativePaymentTokenizationRequest.swift +// ProcessOut +// +// Created by Andrii Vysotskyi on 05.08.2024. +// + +/// APM tokenization request. +/// +/// - NOTE: Make sure to supply proper `additionalData` specific for particular payment +/// method. +public struct POAlternativePaymentTokenizationRequest: Sendable { + + /// Customer ID that may be used for creating APM recurring token. + public let customerId: String + + /// Customer token ID that may be used for creating APM recurring token. + public let tokenId: String + + /// Gateway Configuration ID of the APM the payment will be made on. + public let gatewayConfigurationId: String + + /// Additional data that will be supplied to the APM. + public let additionalData: [String: String]? + + /// Creates tokenization request. + public init( + customerId: String, + tokenId: String, + gatewayConfigurationId: String, + additionalData: [String: String]? = nil + ) { + self.customerId = customerId + self.tokenId = tokenId + self.gatewayConfigurationId = gatewayConfigurationId + self.additionalData = additionalData + } +} diff --git a/Sources/ProcessOut/Sources/Services/AlternativePayments/Responses/POAlternativePaymentResponse.swift b/Sources/ProcessOut/Sources/Services/AlternativePayments/Responses/POAlternativePaymentResponse.swift new file mode 100644 index 000000000..3097180b8 --- /dev/null +++ b/Sources/ProcessOut/Sources/Services/AlternativePayments/Responses/POAlternativePaymentResponse.swift @@ -0,0 +1,19 @@ +// +// POAlternativePaymentResponse.swift +// ProcessOut +// +// Created by Simeon Kostadinov on 27/10/2022. +// + +import Foundation + +/// Generic alternative payment response. +public struct POAlternativePaymentResponse: Sendable { + + /// Represents a gateway token. + /// + /// - Authorization: The token can be used to capture the payment on your server. + /// - Tokenization: The token is a gateway request token, which can only be used to + /// generate the eventual customer token. It should not be used as a payment source. + public let gatewayToken: String +} diff --git a/Sources/ProcessOutUI/Sources/Modules/AlternativePayment/POWebAuthenticationSession+AlternativePayment.swift b/Sources/ProcessOutUI/Sources/Modules/AlternativePayment/POWebAuthenticationSession+AlternativePayment.swift deleted file mode 100644 index 8e3127e70..000000000 --- a/Sources/ProcessOutUI/Sources/Modules/AlternativePayment/POWebAuthenticationSession+AlternativePayment.swift +++ /dev/null @@ -1,54 +0,0 @@ -// -// POWebAuthenticationSession+AlternativePayment.swift -// ProcessOutUI -// -// Created by Andrii Vysotskyi on 18.03.2024. -// - -import Foundation -import ProcessOut - -extension POWebAuthenticationSession { - - /// Creates session that is capable of handling alternative payment. - /// - /// - Parameters: - /// - request: Alternative payment request. - /// - returnUrl: Return URL specified when creating invoice. - /// - completion: Completion to invoke when APM flow completes. - public convenience init( - request: POAlternativePaymentMethodRequest, - returnUrl: URL, - completion: @escaping @Sendable (Result) -> Void - ) { - let url = ProcessOut.shared.alternativePaymentMethods.alternativePaymentMethodUrl(request: request) - self.init(alternativePaymentMethodUrl: url, returnUrl: returnUrl, completion: completion) - } - - /// Creates session that is capable of handling alternative payment. - /// - /// - Parameters: - /// - url: initial URL instead of **request**. Implementation does not validate - /// whether given value is valid to actually start APM flow. - /// - returnUrl: Return URL specified when creating invoice. - /// - completion: Completion to invoke when APM flow completes. - public convenience init( - alternativePaymentMethodUrl url: URL, - returnUrl: URL, - completion: @escaping @Sendable (Result) -> Void - ) { - let completionBox: Completion = { result in - completion(result.flatMap(Self.response)) - } - self.init(url: url, callback: .customScheme(returnUrl.scheme ?? ""), completion: completionBox) - } - - // MARK: - Private Methods - - private static nonisolated func response(with url: URL) -> Result { - let result = Result { - try ProcessOut.shared.alternativePaymentMethods.alternativePaymentMethodResponse(url: url) - } - return result.mapError { $0 as! POFailure } // swiftlint:disable:this force_cast - } -} diff --git a/Sources/ProcessOutUI/Sources/Modules/AlternativePayment/SFSafariViewController+AlternativePayment.swift b/Sources/ProcessOutUI/Sources/Modules/AlternativePayment/SFSafariViewController+AlternativePayment.swift deleted file mode 100644 index fd53eb53d..000000000 --- a/Sources/ProcessOutUI/Sources/Modules/AlternativePayment/SFSafariViewController+AlternativePayment.swift +++ /dev/null @@ -1,76 +0,0 @@ -// -// SFSafariViewController+AlternativePayment.swift -// ProcessOutUI -// -// Created by Andrii Vysotskyi on 17.11.2023. -// - -import Foundation -import SafariServices -@_spi(PO) import ProcessOut - -extension SFSafariViewController { - - /// Creates view controller that is capable of handling Alternative Payment. - /// - /// - Note: Caller should dismiss view controller after completion is called. - /// - Note: Object's delegate shouldn't be modified. - /// - /// - Parameters: - /// - returnUrl: Return URL specified when creating invoice. - /// - safariConfiguration: The configuration for the new view controller. - /// - completion: Completion to invoke when APM flow completes. - public convenience init( - request: POAlternativePaymentMethodRequest, - returnUrl: URL, - safariConfiguration: SFSafariViewController.Configuration = Configuration(), - completion: @escaping @Sendable (Result) -> Void - ) { - let url = ProcessOut.shared.alternativePaymentMethods.alternativePaymentMethodUrl(request: request) - self.init( - alternativePaymentMethodUrl: url, - returnUrl: returnUrl, - safariConfiguration: safariConfiguration, - completion: completion - ) - } - - /// Creates view controller that is capable of handling Alternative Payment. - /// - /// - Note: Caller should dismiss view controller after completion is called. - /// - Note: Object's delegate shouldn't be modified. - /// - /// - Parameters: - /// - url: initial URL instead of **request**. Implementation does not validate - /// whether given value is valid to actually start APM flow. - /// - returnUrl: Return URL specified when creating invoice. - /// - safariConfiguration: The configuration for the new view controller. - /// - completion: Completion to invoke when APM flow completes. - public convenience init( - alternativePaymentMethodUrl url: URL, - returnUrl: URL, - safariConfiguration: SFSafariViewController.Configuration = Configuration(), - completion: @escaping @Sendable (Result) -> Void - ) { - self.init(url: url, configuration: safariConfiguration) - let viewModel = DefaultSafariViewModel( - callback: .customScheme(returnUrl.scheme ?? ""), - eventEmitter: ProcessOut.shared.eventEmitter, - logger: ProcessOut.shared.logger, - completion: { result in - completion(result.flatMap(Self.response)) - } - ) - self.setViewModel(viewModel) - viewModel.start() - } - - // MARK: - Private Methods - - private nonisolated static func response(with url: URL) -> Result { - let result = Result { - try ProcessOut.shared.alternativePaymentMethods.alternativePaymentMethodResponse(url: url) - } - return result.mapError { $0 as! POFailure } // swiftlint:disable:this force_cast - } -} diff --git a/Sources/ProcessOutUI/Sources/Modules/DynamicCheckout/Configuration/PODynamicCheckoutAlternativePaymentConfiguration.swift b/Sources/ProcessOutUI/Sources/Modules/DynamicCheckout/Configuration/PODynamicCheckoutAlternativePaymentConfiguration.swift index 5d92b2d49..8ac943bbd 100644 --- a/Sources/ProcessOutUI/Sources/Modules/DynamicCheckout/Configuration/PODynamicCheckoutAlternativePaymentConfiguration.swift +++ b/Sources/ProcessOutUI/Sources/Modules/DynamicCheckout/Configuration/PODynamicCheckoutAlternativePaymentConfiguration.swift @@ -59,9 +59,6 @@ public struct PODynamicCheckoutAlternativePaymentConfiguration: Sendable { } } - /// Return URL to expect when handling OOB or web based payments. - public let returnUrl: URL? - /// For parameters where user should select single option from multiple values defines /// maximum number of options that framework will display inline (e.g. using radio buttons). /// @@ -72,12 +69,7 @@ public struct PODynamicCheckoutAlternativePaymentConfiguration: Sendable { public let paymentConfirmation: PaymentConfirmation /// Creates configuration. - public init( - returnUrl: URL? = nil, - inlineSingleSelectValuesLimit: Int = 5, - paymentConfirmation: PaymentConfirmation = .init() - ) { - self.returnUrl = returnUrl + public init(inlineSingleSelectValuesLimit: Int = 5, paymentConfirmation: PaymentConfirmation = .init()) { self.inlineSingleSelectValuesLimit = inlineSingleSelectValuesLimit self.paymentConfirmation = paymentConfirmation } diff --git a/Sources/ProcessOutUI/Sources/Modules/DynamicCheckout/Interactor/DynamicCheckout/DynamicCheckoutDefaultInteractor.swift b/Sources/ProcessOutUI/Sources/Modules/DynamicCheckout/Interactor/DynamicCheckout/DynamicCheckoutDefaultInteractor.swift index 70257cbf9..e27accac9 100644 --- a/Sources/ProcessOutUI/Sources/Modules/DynamicCheckout/Interactor/DynamicCheckout/DynamicCheckoutDefaultInteractor.swift +++ b/Sources/ProcessOutUI/Sources/Modules/DynamicCheckout/Interactor/DynamicCheckout/DynamicCheckoutDefaultInteractor.swift @@ -19,18 +19,18 @@ final class DynamicCheckoutDefaultInteractor: configuration: PODynamicCheckoutConfiguration, delegate: PODynamicCheckoutDelegate?, passKitPaymentSession: DynamicCheckoutPassKitPaymentSession, - alternativePaymentSession: DynamicCheckoutAlternativePaymentSession, childProvider: DynamicCheckoutInteractorChildProvider, invoicesService: POInvoicesService, + alternativePaymentsService: POAlternativePaymentsService, logger: POLogger, completion: @escaping (Result) -> Void ) { self.configuration = configuration self.delegate = delegate self.passKitPaymentSession = passKitPaymentSession - self.alternativePaymentSession = alternativePaymentSession self.childProvider = childProvider self.invoicesService = invoicesService + self.alternativePaymentsService = alternativePaymentsService self.logger = logger self.completion = completion super.init(state: .idle) @@ -138,7 +138,7 @@ final class DynamicCheckoutDefaultInteractor: // MARK: - Private Properties private let passKitPaymentSession: DynamicCheckoutPassKitPaymentSession - private let alternativePaymentSession: DynamicCheckoutAlternativePaymentSession + private let alternativePaymentsService: POAlternativePaymentsService private let childProvider: DynamicCheckoutInteractorChildProvider private let invoicesService: POInvoicesService private let completion: (Result) -> Void @@ -431,7 +431,7 @@ final class DynamicCheckoutDefaultInteractor: state = .paymentProcessing(paymentProcessingState) Task { do { - _ = try await alternativePaymentSession.start(url: method.configuration.redirectUrl) + _ = try await alternativePaymentsService.authenticate(using: method.configuration.redirectUrl) setSuccessState() } catch { recoverPaymentProcessing(error: error) @@ -523,7 +523,7 @@ final class DynamicCheckoutDefaultInteractor: Task { @MainActor in do { if let redirectUrl = method.configuration.redirectUrl { - _ = try await alternativePaymentSession.start(url: redirectUrl) + _ = try await alternativePaymentsService.authenticate(using: redirectUrl) } else { try await authorizeInvoice(source: method.configuration.customerTokenId, startedState: startedState) } diff --git a/Sources/ProcessOutUI/Sources/Modules/DynamicCheckout/Sessions/AlternativePayment/DynamicCheckoutAlternativePaymentDefaultSession.swift b/Sources/ProcessOutUI/Sources/Modules/DynamicCheckout/Sessions/AlternativePayment/DynamicCheckoutAlternativePaymentDefaultSession.swift deleted file mode 100644 index ff1f34a45..000000000 --- a/Sources/ProcessOutUI/Sources/Modules/DynamicCheckout/Sessions/AlternativePayment/DynamicCheckoutAlternativePaymentDefaultSession.swift +++ /dev/null @@ -1,38 +0,0 @@ -// -// DynamicCheckoutAlternativePaymentDefaultSession.swift -// ProcessOutUI -// -// Created by Andrii Vysotskyi on 25.03.2024. -// - -import Foundation -import ProcessOut - -final class DynamicCheckoutAlternativePaymentDefaultSession: DynamicCheckoutAlternativePaymentSession { - - init(configuration: PODynamicCheckoutAlternativePaymentConfiguration) { - self.configuration = configuration - } - - func start(url: URL) async throws -> POAlternativePaymentMethodResponse { - guard let returnUrl = configuration.returnUrl else { - throw POFailure(message: "Return URL must be set.", code: .generic(.mobile)) - } - return try await withCheckedThrowingContinuation { continuation in - let session = POWebAuthenticationSession(alternativePaymentMethodUrl: url, returnUrl: returnUrl) { result in - continuation.resume(with: result) - } - Task { - guard await !session.start() else { - return - } - let failure = POFailure(message: "Unable to start alternative payment.", code: .generic(.mobile)) - continuation.resume(throwing: failure) - } - } - } - - // MARK: - Private Properties - - private let configuration: PODynamicCheckoutAlternativePaymentConfiguration -} diff --git a/Sources/ProcessOutUI/Sources/Modules/DynamicCheckout/Sessions/AlternativePayment/DynamicCheckoutAlternativePaymentSession.swift b/Sources/ProcessOutUI/Sources/Modules/DynamicCheckout/Sessions/AlternativePayment/DynamicCheckoutAlternativePaymentSession.swift deleted file mode 100644 index 82ae0e29b..000000000 --- a/Sources/ProcessOutUI/Sources/Modules/DynamicCheckout/Sessions/AlternativePayment/DynamicCheckoutAlternativePaymentSession.swift +++ /dev/null @@ -1,16 +0,0 @@ -// -// DynamicCheckoutAlternativePaymentSession.swift -// ProcessOutUI -// -// Created by Andrii Vysotskyi on 17.03.2024. -// - -import Foundation -import ProcessOut - -@MainActor -protocol DynamicCheckoutAlternativePaymentSession { - - /// Starts alternative payment. - func start(url: URL) async throws -> POAlternativePaymentMethodResponse -} diff --git a/Sources/ProcessOutUI/Sources/Modules/DynamicCheckout/View/PODynamicCheckoutView+Init.swift b/Sources/ProcessOutUI/Sources/Modules/DynamicCheckout/View/PODynamicCheckoutView+Init.swift index a20dcc315..a8942f8b0 100644 --- a/Sources/ProcessOutUI/Sources/Modules/DynamicCheckout/View/PODynamicCheckoutView+Init.swift +++ b/Sources/ProcessOutUI/Sources/Modules/DynamicCheckout/View/PODynamicCheckoutView+Init.swift @@ -28,9 +28,6 @@ extension PODynamicCheckoutView { passKitPaymentSession: DynamicCheckoutPassKitPaymentDefaultSession( delegate: delegate, invoicesService: ProcessOut.shared.invoices ), - alternativePaymentSession: DynamicCheckoutAlternativePaymentDefaultSession( - configuration: configuration.alternativePayment - ), childProvider: DynamicCheckoutInteractorDefaultChildProvider( configuration: configuration, cardsService: ProcessOut.shared.cards, @@ -39,6 +36,7 @@ extension PODynamicCheckoutView { logger: logger ), invoicesService: ProcessOut.shared.invoices, + alternativePaymentsService: ProcessOut.shared.alternativePayments, logger: logger, completion: completion ) diff --git a/Sources/ProcessOutUI/Sources/Modules/WebAuthentication/DefaultSafariViewModel.swift b/Sources/ProcessOutUI/Sources/Modules/WebAuthentication/DefaultSafariViewModel.swift deleted file mode 100644 index 005e8ef38..000000000 --- a/Sources/ProcessOutUI/Sources/Modules/WebAuthentication/DefaultSafariViewModel.swift +++ /dev/null @@ -1,129 +0,0 @@ -// -// DefaultSafariViewModel.swift -// ProcessOutUI -// -// Created by Andrii Vysotskyi on 10.05.2023. -// - -import Foundation -import SafariServices -@_spi(PO) import ProcessOut - -@MainActor -final class DefaultSafariViewModel: NSObject, Sendable, @preconcurrency SFSafariViewControllerDelegate { - - init( - callback: POWebAuthenticationSessionCallback, - timeout: TimeInterval? = nil, - eventEmitter: POEventEmitter, - logger: POLogger, - completion: @escaping @Sendable (Result) -> Void - ) { - self.callback = callback - self.timeout = timeout - self.eventEmitter = eventEmitter - self.logger = logger - self.completion = completion - state = .idle - } - - func start() { - guard case .idle = state else { - return - } - if let timeout { - timeoutTimer = Timer.scheduledTimer(withTimeInterval: timeout, repeats: false) { [weak self] _ in - MainActor.assumeIsolated { - self?.setCompletedState(with: POFailure(code: .timeout(.mobile))) - } - } - } - deepLinkObserver = eventEmitter.on(PODeepLinkReceivedEvent.self) { [weak self] event in - self?.setCompletedState(with: event.url) ?? false - } - state = .started - } - - // MARK: - SFSafariViewControllerDelegate - - func safariViewControllerDidFinish(_ controller: SFSafariViewController) { - if state != .completed { - logger.debug("Safari did finish, but state is not completed, handling as cancelation") - let failure = POFailure(code: .cancelled) - setCompletedState(with: failure) - } - } - - func safariViewController(_ controller: SFSafariViewController, didCompleteInitialLoad didLoadSuccessfully: Bool) { - if !didLoadSuccessfully { - logger.debug("Safari failed to load initial url, aborting") - let failure = POFailure(code: .generic(.mobile)) - setCompletedState(with: failure) - } - } - - nonisolated func safariViewController(_ controller: SFSafariViewController, initialLoadDidRedirectTo url: URL) { - logger.debug("Safari did redirect to url: \(url)") - } - - // MARK: - Private Nested Types - - private enum State { - - /// View model is currently idle and waiting for start. - case idle - - /// View model has been started and is currently operating. - case started - - /// View model did complete with either success or failure. - case completed - } - - // MARK: - Private Properties - - private let callback: POWebAuthenticationSessionCallback - private let timeout: TimeInterval? - private let eventEmitter: POEventEmitter - private let logger: POLogger - private let completion: (Result) -> Void - - private var state: State - private var deepLinkObserver: AnyObject? - private var timeoutTimer: Timer? - - // MARK: - Private Methods - - private func setCompletedState(with url: URL) -> Bool { - if case .completed = state { - logger.info("Can't change state to completed because already in sink state.") - return false - } - // todo(andrii-vysotskyi): consider validating whether url is related to initial request if possible - guard callback.matchesURL(url) else { - logger.debug("Ignoring unrelated url: \(url)") - return false - } - invalidateObservers() - state = .completed - logger.info("Did complete with url: \(url)") - completion(.success(url)) - return true - } - - private func setCompletedState(with failure: POFailure) { - if case .completed = state { - logger.info("Can't change state to completed because already in a sink state.") - return - } - invalidateObservers() - state = .completed - logger.debug("Did complete with error: \(failure)") - completion(.failure(failure)) - } - - private func invalidateObservers() { - timeoutTimer?.invalidate() - deepLinkObserver = nil - } -} diff --git a/Sources/ProcessOutUI/Sources/Modules/WebAuthentication/POWebAuthenticationSession.swift b/Sources/ProcessOutUI/Sources/Modules/WebAuthentication/POWebAuthenticationSession.swift deleted file mode 100644 index 33bb00a9a..000000000 --- a/Sources/ProcessOutUI/Sources/Modules/WebAuthentication/POWebAuthenticationSession.swift +++ /dev/null @@ -1,118 +0,0 @@ -// -// POWebAuthenticationSession.swift -// ProcessOutUI -// -// Created by Andrii Vysotskyi on 29.05.2024. -// - -import SafariServices -import AuthenticationServices -@_spi(PO) import ProcessOut - -/// A session that an app uses to authenticate a payment. -@MainActor -public final class POWebAuthenticationSession: Sendable { - - /// A completion handler for the web authentication session. - typealias Completion = @Sendable (Result) -> Void - - /// Only call this method once for a given POWebAuthenticationSession instance after initialization. - /// Calling the start() method on a canceled session results in a failure. - /// - /// After you call start(), the session instance stores a strong reference to itself. To avoid deallocation during - /// the authentication process, the session keeps the reference until after it calls the completion handler. - public func start() async -> Bool { - guard state == nil else { - preconditionFailure("Session start must be attempted only once.") - } - guard let presentingViewController = PresentingViewControllerProvider.find() else { - return false - } - let viewController = createViewController() - state = .started(viewController: viewController) - await withCheckedContinuation { continuation in - presentingViewController.present(viewController, animated: true, completion: continuation.resume) - } - associate(controller: self, with: viewController) - return true - } - - /// Cancels a web authentication session. - /// - /// If the session has already presented a view with the authentication webpage, calling this method dismisses - /// that view. Calling cancel() on an already canceled/completed session has no effect. - public func cancel() async { - guard case .started(let viewController) = state else { - return - } - // Break retain cycle to allow de-initialization of self. - await withCheckedContinuation { continuation in - viewController.dismiss(animated: true, completion: continuation.resume) - } - state = .cancelling - associate(controller: nil, with: viewController) - } - - // MARK: - - - init( - url: URL, - callback: POWebAuthenticationSessionCallback, - timeout: TimeInterval? = nil, - completion: @escaping Completion - ) { - self.url = url - self.callback = callback - self.timeout = timeout - self.completion = completion - } - - // MARK: - Private Nested Types - - private enum AssociatedKeys { - nonisolated(unsafe) static var controller: UInt8 = 0 - } - - private enum State { - case started(viewController: SFSafariViewController), cancelling, completed - } - - // MARK: - Private Properties - - private let url: URL - private let callback: POWebAuthenticationSessionCallback - private let timeout: TimeInterval? - private let completion: Completion - private var state: State? - - // MARK: - Utils - - private func createViewController() -> SFSafariViewController { - let viewController = SFSafariViewController(url: url) - viewController.dismissButtonStyle = .cancel - let viewModel = DefaultSafariViewModel( - callback: callback, - timeout: timeout, - eventEmitter: ProcessOut.shared.eventEmitter, - logger: ProcessOut.shared.logger, - completion: { [weak self] result in - self?.complete(with: result) - } - ) - viewController.setViewModel(viewModel) - viewModel.start() - return viewController - } - - private nonisolated func complete(with result: Result) { - Task { @MainActor in - await self.cancel() - state = .completed - completion(result) - } - } - - private func associate(controller: POWebAuthenticationSession?, with object: Any) { - objc_setAssociatedObject(object, &AssociatedKeys.controller, controller, .OBJC_ASSOCIATION_RETAIN) - } -} diff --git a/Sources/ProcessOutUI/Sources/Modules/WebAuthentication/POWebAuthenticationSessionCallback.swift b/Sources/ProcessOutUI/Sources/Modules/WebAuthentication/POWebAuthenticationSessionCallback.swift deleted file mode 100644 index 1ced4cbe8..000000000 --- a/Sources/ProcessOutUI/Sources/Modules/WebAuthentication/POWebAuthenticationSessionCallback.swift +++ /dev/null @@ -1,21 +0,0 @@ -// -// POWebAuthenticationSessionCallback.swift -// ProcessOutUI -// -// Created by Andrii Vysotskyi on 29.05.2024. -// - -import Foundation - -/// An object used to evaluate navigation events in an authentication session. -public struct POWebAuthenticationSessionCallback: Sendable { - - /// Creates a callback object that matches against URLs with the given custom scheme. - /// - Parameter customScheme: The custom scheme that the app expects in the callback URL. - public static func customScheme(_ customScheme: String) -> Self { - Self { $0.scheme == customScheme } - } - - /// Check whether a given main-frame navigation URL matches the callback expected by the client app. - let matchesURL: @Sendable (_ url: URL) -> Bool -} diff --git a/Sources/ProcessOutUI/Sources/Modules/WebAuthentication/SafariViewController+Extensions.swift b/Sources/ProcessOutUI/Sources/Modules/WebAuthentication/SafariViewController+Extensions.swift deleted file mode 100644 index f6a2beb39..000000000 --- a/Sources/ProcessOutUI/Sources/Modules/WebAuthentication/SafariViewController+Extensions.swift +++ /dev/null @@ -1,22 +0,0 @@ -// -// SafariViewController+Extensions.swift -// ProcessOutUI -// -// Created by Andrii Vysotskyi on 10.05.2023. -// - -import SafariServices - -extension SFSafariViewController { - - func setViewModel(_ viewModel: DefaultSafariViewModel) { - objc_setAssociatedObject(self, &Keys.viewModel, viewModel, .OBJC_ASSOCIATION_RETAIN) - delegate = viewModel - } - - // MARK: - Private Nested Types - - private enum Keys { - nonisolated(unsafe) static var viewModel: UInt8 = 0 - } -} From 2343087cf61322df2080026c3f57ebdea28c69cc Mon Sep 17 00:00:00 2001 From: Andrii Vysotskyi Date: Wed, 7 Aug 2024 13:45:10 +0200 Subject: [PATCH 05/10] feat(ad-hoc): resolve deprecations (#322) * Remove deprecated declarations * Create ProcessOutConfiguration via regular init rather than static method * Remove non-needed property wrappers, types and properties --- .../Api/Models/ProcessOutConfiguration.swift | 57 ++++------ .../ProcessOut/Sources/Api/ProcessOut.swift | 3 - .../Core/Cancellable/POCancellable.swift | 3 - .../POFallbackDecodable.swift | 30 ----- .../POFallbackValueProvider.swift | 23 ---- .../POImmutableExcludedCodable.swift | 34 ------ ...mal.swift => POStringCodableDecimal.swift} | 12 +- ...t => POStringCodableOptionalDecimal.swift} | 12 +- .../DefaultDeviceMetadataProvider.swift | 8 +- .../Core/DeviceMetadata/DeviceMetadata.swift | 14 +-- .../Core/Utils/POTypedRepresentation.swift | 106 ------------------ .../UnfairlyLocked/POUnfairlyLocked.swift | 1 - .../Generated/Sourcery+Generated.swift | 38 ++++++- .../Cards/HttpCardsRepository.swift | 17 ++- .../Requests/POCardTokenizationRequest.swift | 7 +- .../Cards/Requests/POCardUpdateRequest.swift | 14 +-- .../Responses/CardTokenizationResponse.swift | 12 -- .../Repositories/Cards/Responses/POCard.swift | 18 +-- .../Responses/POCardIssuerInformation.swift | 10 +- .../Cards/Responses/POCardScheme.swift | 7 +- .../POAssignCustomerTokenRequest.swift | 21 ++-- .../POCreateCustomerTokenRequest.swift | 7 +- .../POGatewayConfigurationsRepository.swift | 3 - .../Responses/POGatewayConfiguration.swift | 4 - .../Invoices/HttpInvoicesRepository.swift | 38 ++++--- .../Invoices/InvoicesRepository.swift | 2 +- ...tiveAlternativePaymentCaptureRequest.swift | 10 +- .../POInvoiceAuthorizationRequest.swift | 25 ++--- ...tiveAlternativePaymentMethodResponse.swift | 23 ++-- ...ativePaymentMethodTransactionDetails.swift | 4 +- .../PODynamicCheckoutPaymentMethod.swift | 2 +- .../POStringDecodableMerchantCapability.swift | 2 +- .../Invoices/Responses/POInvoice.swift | 4 +- .../Repositories/Shared/PORepository.swift | 3 - .../Sources/Services/3DS/PO3DSService.swift | 3 - .../POAlternativePaymentsService.swift | 3 - .../Services/Cards/POCardsService.swift | 3 - .../POCustomerTokensService.swift | 3 - .../Invoices/DefaultInvoicesService.swift | 4 +- .../Services/Invoices/POInvoicesService.swift | 3 - .../Sources/Services/Shared/POService.swift | 3 - .../POBillingAddressConfiguration.swift | 3 - .../Delegate/POCardTokenizationDelegate.swift | 4 +- .../DefaultCardTokenizationInteractor.swift | 13 +-- .../DefaultCardTokenizationViewModel.swift | 8 +- .../Delegate/POCardUpdateInformation.swift | 21 ++-- .../DefaultCardUpdateInteractor.swift | 23 ++-- .../Delegate/PODynamicCheckoutDelegate.swift | 4 +- .../DynamicCheckoutDefaultInteractor.swift | 2 +- ...ativeAlternativePaymentConfiguration.swift | 52 --------- ...eAlternativePaymentDefaultInteractor.swift | 12 +- 51 files changed, 218 insertions(+), 520 deletions(-) delete mode 100644 Sources/ProcessOut/Sources/Core/CodingUtils/POFallbackDecodable/POFallbackDecodable.swift delete mode 100644 Sources/ProcessOut/Sources/Core/CodingUtils/POFallbackDecodable/POFallbackValueProvider.swift delete mode 100644 Sources/ProcessOut/Sources/Core/CodingUtils/POImmutableExcludedCodable.swift rename Sources/ProcessOut/Sources/Core/CodingUtils/{POImmutableStringCodableDecimal.swift => POStringCodableDecimal.swift} (80%) rename Sources/ProcessOut/Sources/Core/CodingUtils/{POImmutableStringCodableOptionalDecimal.swift => POStringCodableOptionalDecimal.swift} (79%) delete mode 100644 Sources/ProcessOut/Sources/Core/Utils/POTypedRepresentation.swift delete mode 100644 Sources/ProcessOut/Sources/Repositories/Cards/Responses/CardTokenizationResponse.swift diff --git a/Sources/ProcessOut/Sources/Api/Models/ProcessOutConfiguration.swift b/Sources/ProcessOut/Sources/Api/Models/ProcessOutConfiguration.swift index a4b4bc665..56b771805 100644 --- a/Sources/ProcessOut/Sources/Api/Models/ProcessOutConfiguration.swift +++ b/Sources/ProcessOut/Sources/Api/Models/ProcessOutConfiguration.swift @@ -7,9 +7,6 @@ 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:)`` /// method. @@ -35,13 +32,6 @@ public struct ProcessOutConfiguration: Sendable { /// 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 @@ -60,38 +50,33 @@ public struct ProcessOutConfiguration: Sendable { @_spi(PO) public let privateKey: String? - /// Api base URL. - let apiBaseUrl = URL(string: "https://api.processout.com")! // swiftlint:disable:this force_unwrapping - - /// Checkout base URL. - let checkoutBaseUrl = URL(string: "https://checkout.processout.com")! // swiftlint:disable:this force_unwrapping -} - -extension ProcessOutConfiguration { - - /// Creates production configuration. - /// - /// - Parameters: - /// - appVersion: when application parameter is set, it takes precedence over this parameter. - public static func production( + /// Creates configuration. + public init( projectId: String, application: Application? = nil, - appVersion: String? = nil, isDebug: Bool = false, isTelemetryEnabled: Bool = true - ) -> Self { - .init( - projectId: projectId, - application: application ?? .init(name: nil, version: appVersion), - isDebug: isDebug, - isTelemetryEnabled: isTelemetryEnabled, - privateKey: nil - ) + ) { + self.projectId = projectId + self.application = application + self.isDebug = isDebug + self.isTelemetryEnabled = isTelemetryEnabled + self.privateKey = nil } - /// Creates debug production configuration with optional private key. + /// Creates debug configuration. @_spi(PO) - public static func production(projectId: String, privateKey: String? = nil) -> Self { - .init(projectId: projectId, application: nil, isDebug: true, isTelemetryEnabled: false, privateKey: privateKey) + public init(projectId: String, privateKey: String) { + self.projectId = projectId + self.application = nil + self.isDebug = true + self.isTelemetryEnabled = false + self.privateKey = privateKey } + + /// Api base URL. + let apiBaseUrl = URL(string: "https://api.processout.com")! // swiftlint:disable:this force_unwrapping + + /// Checkout base URL. + let checkoutBaseUrl = URL(string: "https://checkout.processout.com")! // swiftlint:disable:this force_unwrapping } diff --git a/Sources/ProcessOut/Sources/Api/ProcessOut.swift b/Sources/ProcessOut/Sources/Api/ProcessOut.swift index 54c782b68..d40821695 100644 --- a/Sources/ProcessOut/Sources/Api/ProcessOut.swift +++ b/Sources/ProcessOut/Sources/Api/ProcessOut.swift @@ -10,9 +10,6 @@ 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: Instance methods and properties of this class could be access from any thread. public final class ProcessOut: @unchecked Sendable { diff --git a/Sources/ProcessOut/Sources/Core/Cancellable/POCancellable.swift b/Sources/ProcessOut/Sources/Core/Cancellable/POCancellable.swift index a6bb0ceef..6825f0fa2 100644 --- a/Sources/ProcessOut/Sources/Core/Cancellable/POCancellable.swift +++ b/Sources/ProcessOut/Sources/Core/Cancellable/POCancellable.swift @@ -5,9 +5,6 @@ // 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: Sendable { 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 b83a87c2c..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: Sendable { - - 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 755370a1f..000000000 --- a/Sources/ProcessOut/Sources/Core/CodingUtils/POImmutableExcludedCodable.swift +++ /dev/null @@ -1,34 +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 */ } -} - -extension POImmutableExcludedCodable: Sendable where Value: Sendable { } 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 5b6b1abf5..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, Sendable { +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, Sendable { 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 79% rename from Sources/ProcessOut/Sources/Core/CodingUtils/POImmutableStringCodableOptionalDecimal.swift rename to Sources/ProcessOut/Sources/Core/CodingUtils/POStringCodableOptionalDecimal.swift index a5b710029..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, Sendable { +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, Sendable { 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/DeviceMetadata/DefaultDeviceMetadataProvider.swift b/Sources/ProcessOut/Sources/Core/DeviceMetadata/DefaultDeviceMetadataProvider.swift index a7232cf28..f42464c3f 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), diff --git a/Sources/ProcessOut/Sources/Core/DeviceMetadata/DeviceMetadata.swift b/Sources/ProcessOut/Sources/Core/DeviceMetadata/DeviceMetadata.swift index 58fe897d0..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, Sendable { +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/Utils/POTypedRepresentation.swift b/Sources/ProcessOut/Sources/Core/Utils/POTypedRepresentation.swift deleted file mode 100644 index 47d57d842..000000000 --- a/Sources/ProcessOut/Sources/Core/Utils/POTypedRepresentation.swift +++ /dev/null @@ -1,106 +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)) - } -} - -extension POTypedRepresentation: Sendable where Wrapped: Sendable { } diff --git a/Sources/ProcessOut/Sources/Core/Utils/UnfairlyLocked/POUnfairlyLocked.swift b/Sources/ProcessOut/Sources/Core/Utils/UnfairlyLocked/POUnfairlyLocked.swift index 266ed092e..43429859e 100644 --- a/Sources/ProcessOut/Sources/Core/Utils/UnfairlyLocked/POUnfairlyLocked.swift +++ b/Sources/ProcessOut/Sources/Core/Utils/UnfairlyLocked/POUnfairlyLocked.swift @@ -9,7 +9,6 @@ import os /// A thread-safe wrapper around a value. @_spi(PO) -@propertyWrapper public final class POUnfairlyLocked: @unchecked Sendable { public init(wrappedValue: Value) { diff --git a/Sources/ProcessOut/Sources/Generated/Sourcery+Generated.swift b/Sources/ProcessOut/Sources/Generated/Sourcery+Generated.swift index 293024b92..f74d9e2de 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,25 @@ 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 invoiceReturnUrl } } @@ -57,7 +91,6 @@ extension POInvoiceAuthorizationRequest { enum CodingKeys: String, CodingKey { case source case incremental - case enableThreeDS2 = "enable_three_d_s_2" case preferredScheme case thirdPartySdkVersion case invoiceDetailIds @@ -68,6 +101,7 @@ extension POInvoiceAuthorizationRequest { case authorizeOnly case allowFallbackToSale case metadata + case enableThreeDS2 = "enable_three_d_s_2" } } diff --git a/Sources/ProcessOut/Sources/Repositories/Cards/HttpCardsRepository.swift b/Sources/ProcessOut/Sources/Repositories/Cards/HttpCardsRepository.swift index a822a2ffb..694c1b4f7 100644 --- a/Sources/ProcessOut/Sources/Repositories/Cards/HttpCardsRepository.swift +++ b/Sources/ProcessOut/Sources/Repositories/Cards/HttpCardsRepository.swift @@ -16,7 +16,7 @@ final class HttpCardsRepository: CardsRepository { // MARK: - CardsRepository func issuerInformation(iin: String) async throws -> POCardIssuerInformation { - struct Response: Decodable, Sendable { + struct Response: Decodable { let cardInformation: POCardIssuerInformation } let httpRequest = HttpConnectorRequest.get(path: "/iins/" + iin) @@ -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/POCardTokenizationRequest.swift b/Sources/ProcessOut/Sources/Repositories/Cards/Requests/POCardTokenizationRequest.swift index 3263a0cae..5b4b0825b 100644 --- a/Sources/ProcessOut/Sources/Repositories/Cards/Requests/POCardTokenizationRequest.swift +++ b/Sources/ProcessOut/Sources/Repositories/Cards/Requests/POCardTokenizationRequest.swift @@ -29,8 +29,7 @@ public struct POCardTokenizationRequest: Encodable, Sendable { 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, Sendable { 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, Sendable { 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 8c0653694..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, Sendable { +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, Sendable { /// 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/Responses/CardTokenizationResponse.swift b/Sources/ProcessOut/Sources/Repositories/Cards/Responses/CardTokenizationResponse.swift deleted file mode 100644 index d54e9f857..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, Sendable { - let card: POCard -} diff --git a/Sources/ProcessOut/Sources/Repositories/Cards/Responses/POCard.swift b/Sources/ProcessOut/Sources/Repositories/Cards/Responses/POCard.swift index 322762bd1..b522c04c8 100644 --- a/Sources/ProcessOut/Sources/Repositories/Cards/Responses/POCard.swift +++ b/Sources/ProcessOut/Sources/Repositories/Cards/Responses/POCard.swift @@ -18,20 +18,16 @@ public struct POCard: Decodable, Hashable, @unchecked Sendable { 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, @unchecked Sendable { /// 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, @unchecked Sendable { 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 89b06d80d..65c485c0f 100644 --- a/Sources/ProcessOut/Sources/Repositories/Cards/Responses/POCardIssuerInformation.swift +++ b/Sources/ProcessOut/Sources/Repositories/Cards/Responses/POCardIssuerInformation.swift @@ -9,12 +9,10 @@ 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, Sendable { 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..87d9a79f5 100644 --- a/Sources/ProcessOut/Sources/Repositories/Cards/Responses/POCardScheme.swift +++ b/Sources/ProcessOut/Sources/Repositories/Cards/Responses/POCardScheme.swift @@ -176,10 +176,15 @@ extension POCardScheme { public static let mir: POCardScheme = "nspk mir" } -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 24b924516..a53ce3f18 100644 --- a/Sources/ProcessOut/Sources/Repositories/CustomerTokens/Requests/POAssignCustomerTokenRequest.swift +++ b/Sources/ProcessOut/Sources/Repositories/CustomerTokens/Requests/POAssignCustomerTokenRequest.swift @@ -21,8 +21,7 @@ public struct POAssignCustomerTokenRequest: Encodable, Sendable { // sourcery: A 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, Sendable { // sourcery: A /// 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 5e3da407c..112b5e018 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, Sendable { +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,7 +23,7 @@ public struct POCreateCustomerTokenRequest: Encodable, Sendable { public let returnUrl: URL? public init(customerId: String, verify: Bool = false, returnUrl: URL? = nil) { - self._customerId = .init(value: customerId) + self.customerId = customerId self.verify = verify self.invoiceReturnUrl = returnUrl self.returnUrl = returnUrl 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/Responses/POGatewayConfiguration.swift b/Sources/ProcessOut/Sources/Repositories/GatewayConfigurations/Responses/POGatewayConfiguration.swift index 3105905b3..59adac0bf 100644 --- a/Sources/ProcessOut/Sources/Repositories/GatewayConfigurations/Responses/POGatewayConfiguration.swift +++ b/Sources/ProcessOut/Sources/Repositories/GatewayConfigurations/Responses/POGatewayConfiguration.swift @@ -37,10 +37,6 @@ public struct POGatewayConfiguration: Decodable, Sendable { /// 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/Invoices/HttpInvoicesRepository.swift b/Sources/ProcessOut/Sources/Repositories/Invoices/HttpInvoicesRepository.swift index 56c7b224d..c531d18b8 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 { @@ -66,11 +76,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 { @@ -85,16 +101,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 6f487da18..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, Sendable { +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/POInvoiceAuthorizationRequest.swift b/Sources/ProcessOut/Sources/Repositories/Invoices/Requests/POInvoiceAuthorizationRequest.swift index 6f452ac4b..8241acb26 100644 --- a/Sources/ProcessOut/Sources/Repositories/Invoices/Requests/POInvoiceAuthorizationRequest.swift +++ b/Sources/ProcessOut/Sources/Repositories/Invoices/Requests/POInvoiceAuthorizationRequest.swift @@ -18,16 +18,8 @@ public struct POInvoiceAuthorizationRequest: Encodable, Sendable { // sourcery: /// 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? @@ -48,8 +40,8 @@ public struct POInvoiceAuthorizationRequest: Encodable, Sendable { // sourcery: /// 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`. @@ -63,12 +55,17 @@ public struct POInvoiceAuthorizationRequest: Encodable, Sendable { // sourcery: /// 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, incremental: Bool = false, - enableThreeDS2 _: Bool = true, - preferredScheme: String? = nil, + preferredScheme: POCardScheme? = nil, thirdPartySdkVersion: String? = nil, invoiceDetailIds: [String]? = nil, overrideMacBlocking: Bool = false, @@ -82,7 +79,7 @@ public struct POInvoiceAuthorizationRequest: Encodable, Sendable { // sourcery: self.invoiceId = invoiceId self.source = source 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/Responses/AlternativePayment/PONativeAlternativePaymentMethodResponse.swift b/Sources/ProcessOut/Sources/Repositories/Invoices/Responses/AlternativePayment/PONativeAlternativePaymentMethodResponse.swift index aa12839cc..142ffd3d5 100644 --- a/Sources/ProcessOut/Sources/Repositories/Invoices/Responses/AlternativePayment/PONativeAlternativePaymentMethodResponse.swift +++ b/Sources/ProcessOut/Sources/Repositories/Invoices/Responses/AlternativePayment/PONativeAlternativePaymentMethodResponse.swift @@ -9,22 +9,13 @@ import Foundation 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, Sendable { + /// 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/PONativeAlternativePaymentMethodTransactionDetails.swift b/Sources/ProcessOut/Sources/Repositories/Invoices/Responses/AlternativePayment/PONativeAlternativePaymentMethodTransactionDetails.swift index 9c07a70ae..075402b85 100644 --- a/Sources/ProcessOut/Sources/Repositories/Invoices/Responses/AlternativePayment/PONativeAlternativePaymentMethodTransactionDetails.swift +++ b/Sources/ProcessOut/Sources/Repositories/Invoices/Responses/AlternativePayment/PONativeAlternativePaymentMethodTransactionDetails.swift @@ -30,8 +30,8 @@ public struct PONativeAlternativePaymentMethodTransactionDetails: Decodable, Sen 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 diff --git a/Sources/ProcessOut/Sources/Repositories/Invoices/Responses/DynamicCheckout/PODynamicCheckoutPaymentMethod.swift b/Sources/ProcessOut/Sources/Repositories/Invoices/Responses/DynamicCheckout/PODynamicCheckoutPaymentMethod.swift index 7e1a6706b..411266fe9 100644 --- a/Sources/ProcessOut/Sources/Repositories/Invoices/Responses/DynamicCheckout/PODynamicCheckoutPaymentMethod.swift +++ b/Sources/ProcessOut/Sources/Repositories/Invoices/Responses/DynamicCheckout/PODynamicCheckoutPaymentMethod.swift @@ -42,7 +42,7 @@ public enum PODynamicCheckoutPaymentMethod: Sendable { /// Merchant capabilities. @POStringDecodableMerchantCapability - public var merchantCapabilities: PKMerchantCapability + public private(set) var merchantCapabilities: PKMerchantCapability /// The payment methods that are supported. public let supportedNetworks: Set diff --git a/Sources/ProcessOut/Sources/Repositories/Invoices/Responses/DynamicCheckout/POStringDecodableMerchantCapability.swift b/Sources/ProcessOut/Sources/Repositories/Invoices/Responses/DynamicCheckout/POStringDecodableMerchantCapability.swift index b7d1a9cfd..47c58a0bf 100644 --- a/Sources/ProcessOut/Sources/Repositories/Invoices/Responses/DynamicCheckout/POStringDecodableMerchantCapability.swift +++ b/Sources/ProcessOut/Sources/Repositories/Invoices/Responses/DynamicCheckout/POStringDecodableMerchantCapability.swift @@ -11,7 +11,7 @@ import PassKit @propertyWrapper 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 bd7af52d1..becca98d0 100644 --- a/Sources/ProcessOut/Sources/Repositories/Invoices/Responses/POInvoice.swift +++ b/Sources/ProcessOut/Sources/Repositories/Invoices/Responses/POInvoice.swift @@ -13,8 +13,8 @@ public struct POInvoice: Decodable, Sendable { /// String value that uniquely identifies this invoice. public let id: String - @POImmutableStringCodableDecimal - public var amount: Decimal + @POStringCodableDecimal + public private(set) var amount: Decimal /// Invoice currency. public let currency: String diff --git a/Sources/ProcessOut/Sources/Repositories/Shared/PORepository.swift b/Sources/ProcessOut/Sources/Repositories/Shared/PORepository.swift index a1b8efa46..156f2e818 100644 --- a/Sources/ProcessOut/Sources/Repositories/Shared/PORepository.swift +++ b/Sources/ProcessOut/Sources/Repositories/Shared/PORepository.swift @@ -5,9 +5,6 @@ // 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: Sendable { diff --git a/Sources/ProcessOut/Sources/Services/3DS/PO3DSService.swift b/Sources/ProcessOut/Sources/Services/3DS/PO3DSService.swift index 15352d77a..2ca761429 100644 --- a/Sources/ProcessOut/Sources/Services/3DS/PO3DSService.swift +++ b/Sources/ProcessOut/Sources/Services/3DS/PO3DSService.swift @@ -5,9 +5,6 @@ // 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, Sendable { diff --git a/Sources/ProcessOut/Sources/Services/AlternativePayments/POAlternativePaymentsService.swift b/Sources/ProcessOut/Sources/Services/AlternativePayments/POAlternativePaymentsService.swift index 65dd0990f..18c78cd5b 100644 --- a/Sources/ProcessOut/Sources/Services/AlternativePayments/POAlternativePaymentsService.swift +++ b/Sources/ProcessOut/Sources/Services/AlternativePayments/POAlternativePaymentsService.swift @@ -7,9 +7,6 @@ import Foundation -@available(*, deprecated, renamed: "POAlternativePaymentsService") -public typealias POAlternativePaymentMethodsServiceType = POAlternativePaymentsService - /// Service that provides set of methods to work with alternative payments. public protocol POAlternativePaymentsService: POService { diff --git a/Sources/ProcessOut/Sources/Services/Cards/POCardsService.swift b/Sources/ProcessOut/Sources/Services/Cards/POCardsService.swift index 9140848b9..ac4f457f7 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/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 6ded8428c..448f64992 100644 --- a/Sources/ProcessOut/Sources/Services/Invoices/POInvoicesService.swift +++ b/Sources/ProcessOut/Sources/Services/Invoices/POInvoicesService.swift @@ -5,9 +5,6 @@ // Created by Andrii Vysotskyi on 02.11.2022. // -@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/Shared/POService.swift b/Sources/ProcessOut/Sources/Services/Shared/POService.swift index ffdd68cc2..bc34254e5 100644 --- a/Sources/ProcessOut/Sources/Services/Shared/POService.swift +++ b/Sources/ProcessOut/Sources/Services/Shared/POService.swift @@ -11,6 +11,3 @@ public protocol POService: Sendable { /// Service's failure type. typealias Failure = POFailure } - -@available(*, deprecated, renamed: "POService") -public typealias POServiceType = POService diff --git a/Sources/ProcessOutUI/Sources/Modules/CardTokenization/Configuration/POBillingAddressConfiguration.swift b/Sources/ProcessOutUI/Sources/Modules/CardTokenization/Configuration/POBillingAddressConfiguration.swift index 31e8c4cf0..b5f06aa11 100644 --- a/Sources/ProcessOutUI/Sources/Modules/CardTokenization/Configuration/POBillingAddressConfiguration.swift +++ b/Sources/ProcessOutUI/Sources/Modules/CardTokenization/Configuration/POBillingAddressConfiguration.swift @@ -10,9 +10,6 @@ import ProcessOut /// Billing address collection configuration. public struct POBillingAddressConfiguration: Sendable { - @available(*, deprecated, message: "Use POBillingAddressCollectionMode directly.") - public typealias CollectionMode = POBillingAddressCollectionMode - /// Billing address collection mode. public let mode: POBillingAddressCollectionMode diff --git a/Sources/ProcessOutUI/Sources/Modules/CardTokenization/Delegate/POCardTokenizationDelegate.swift b/Sources/ProcessOutUI/Sources/Modules/CardTokenization/Delegate/POCardTokenizationDelegate.swift index e4cec010f..1fc132fe7 100644 --- a/Sources/ProcessOutUI/Sources/Modules/CardTokenization/Delegate/POCardTokenizationDelegate.swift +++ b/Sources/ProcessOutUI/Sources/Modules/CardTokenization/Delegate/POCardTokenizationDelegate.swift @@ -24,7 +24,7 @@ public protocol POCardTokenizationDelegate: AnyObject, Sendable { /// Allows to choose preferred scheme that will be selected by default based on issuer information. Default /// implementation returns primary scheme. @MainActor - func preferredScheme(issuerInformation: POCardIssuerInformation) -> String? + func preferredScheme(issuerInformation: POCardIssuerInformation) -> POCardScheme? /// Asks delegate whether user should be allowed to continue after failure or module should complete. /// Default implementation returns `true`. @@ -44,7 +44,7 @@ extension POCardTokenizationDelegate { } @MainActor - public func preferredScheme(issuerInformation: POCardIssuerInformation) -> String? { + public func preferredScheme(issuerInformation: POCardIssuerInformation) -> POCardScheme? { issuerInformation.scheme } diff --git a/Sources/ProcessOutUI/Sources/Modules/CardTokenization/Interactor/DefaultCardTokenizationInteractor.swift b/Sources/ProcessOutUI/Sources/Modules/CardTokenization/Interactor/DefaultCardTokenizationInteractor.swift index abb43fb59..131ac0106 100644 --- a/Sources/ProcessOutUI/Sources/Modules/CardTokenization/Interactor/DefaultCardTokenizationInteractor.swift +++ b/Sources/ProcessOutUI/Sources/Modules/CardTokenization/Interactor/DefaultCardTokenizationInteractor.swift @@ -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 { @@ -132,7 +132,7 @@ 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 { @@ -295,13 +295,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.preferredScheme(issuerInformation: 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/ViewModel/DefaultCardTokenizationViewModel.swift b/Sources/ProcessOutUI/Sources/Modules/CardTokenization/ViewModel/DefaultCardTokenizationViewModel.swift index 7428ddfab..47da08f3a 100644 --- a/Sources/ProcessOutUI/Sources/Modules/CardTokenization/ViewModel/DefaultCardTokenizationViewModel.swift +++ b/Sources/ProcessOutUI/Sources/Modules/CardTokenization/ViewModel/DefaultCardTokenizationViewModel.swift @@ -158,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) } @@ -175,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/Delegate/POCardUpdateInformation.swift b/Sources/ProcessOutUI/Sources/Modules/CardUpdate/Delegate/POCardUpdateInformation.swift index c52b52dd3..7e423a4fa 100644 --- a/Sources/ProcessOutUI/Sources/Modules/CardUpdate/Delegate/POCardUpdateInformation.swift +++ b/Sources/ProcessOutUI/Sources/Modules/CardUpdate/Delegate/POCardUpdateInformation.swift @@ -22,28 +22,25 @@ public struct POCardUpdateInformation: Sendable { 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/DefaultCardUpdateInteractor.swift b/Sources/ProcessOutUI/Sources/Modules/CardUpdate/Interactor/DefaultCardUpdateInteractor.swift index b5d43e28f..99beb2c9e 100644 --- a/Sources/ProcessOutUI/Sources/Modules/CardUpdate/Interactor/DefaultCardUpdateInteractor.swift +++ b/Sources/ProcessOutUI/Sources/Modules/CardUpdate/Interactor/DefaultCardUpdateInteractor.swift @@ -91,9 +91,7 @@ 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/DynamicCheckout/Delegate/PODynamicCheckoutDelegate.swift b/Sources/ProcessOutUI/Sources/Modules/DynamicCheckout/Delegate/PODynamicCheckoutDelegate.swift index 0109a026f..c525d6b5f 100644 --- a/Sources/ProcessOutUI/Sources/Modules/DynamicCheckout/Delegate/PODynamicCheckoutDelegate.swift +++ b/Sources/ProcessOutUI/Sources/Modules/DynamicCheckout/Delegate/PODynamicCheckoutDelegate.swift @@ -43,7 +43,7 @@ public protocol PODynamicCheckoutDelegate: AnyObject, Sendable { /// Allows to choose preferred scheme that will be selected by default based on issuer information. Default /// implementation returns primary scheme. @MainActor - func dynamicCheckout(preferredSchemeFor issuerInformation: POCardIssuerInformation) -> String? + func dynamicCheckout(preferredSchemeFor issuerInformation: POCardIssuerInformation) -> POCardScheme? // MARK: - Alternative Payment @@ -88,7 +88,7 @@ extension PODynamicCheckoutDelegate { } @MainActor - public func dynamicCheckout(preferredSchemeFor issuerInformation: POCardIssuerInformation) -> String? { + public func dynamicCheckout(preferredSchemeFor issuerInformation: POCardIssuerInformation) -> POCardScheme? { issuerInformation.scheme } diff --git a/Sources/ProcessOutUI/Sources/Modules/DynamicCheckout/Interactor/DynamicCheckout/DynamicCheckoutDefaultInteractor.swift b/Sources/ProcessOutUI/Sources/Modules/DynamicCheckout/Interactor/DynamicCheckout/DynamicCheckoutDefaultInteractor.swift index e27accac9..eb6a163e0 100644 --- a/Sources/ProcessOutUI/Sources/Modules/DynamicCheckout/Interactor/DynamicCheckout/DynamicCheckoutDefaultInteractor.swift +++ b/Sources/ProcessOutUI/Sources/Modules/DynamicCheckout/Interactor/DynamicCheckout/DynamicCheckoutDefaultInteractor.swift @@ -729,7 +729,7 @@ extension DynamicCheckoutDefaultInteractor: POCardTokenizationDelegate { try await authorizeInvoice(source: card.id, startedState: currentState.snapshot) } - func preferredScheme(issuerInformation: POCardIssuerInformation) -> String? { + func preferredScheme(issuerInformation: POCardIssuerInformation) -> POCardScheme? { delegate?.dynamicCheckout(preferredSchemeFor: issuerInformation) } diff --git a/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/Configuration/PONativeAlternativePaymentConfiguration.swift b/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/Configuration/PONativeAlternativePaymentConfiguration.swift index ad9b4bba8..e663adb89 100644 --- a/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/Configuration/PONativeAlternativePaymentConfiguration.swift +++ b/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/Configuration/PONativeAlternativePaymentConfiguration.swift @@ -61,58 +61,6 @@ public struct PONativeAlternativePaymentConfiguration { /// 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 - ) - } - /// Creates configuration instance. public init( invoiceId: String, diff --git a/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/Interactor/NativeAlternativePaymentDefaultInteractor.swift b/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/Interactor/NativeAlternativePaymentDefaultInteractor.swift index 0bb589187..d1af2cd2f 100644 --- a/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/Interactor/NativeAlternativePaymentDefaultInteractor.swift +++ b/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/Interactor/NativeAlternativePaymentDefaultInteractor.swift @@ -178,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: @@ -327,9 +327,7 @@ final class NativeAlternativePaymentDefaultInteractor: logger.debug("One or more parameters are not valid: \(invalidFields), waiting for parameters to update") } - private func restoreStartedStateAfterSubmission( - nativeApm: PONativeAlternativePaymentMethodResponse.NativeApm - ) async { + private func restoreStartedStateAfterSubmission(nativeApm: PONativeAlternativePaymentMethodResponse) async { guard case var .submitting(startedState) = state else { return } From 6b3f3e10ee6578bdf1860a1735f02fbc0bae5074 Mon Sep 17 00:00:00 2001 From: Andrii Vysotskyi Date: Fri, 9 Aug 2024 10:11:48 +0200 Subject: [PATCH 06/10] feat(POM-405): update tests (#325) * Fix tests * Use Xcode 16 (beta 4) on CI * Temporarily disable ProcessOutCheckout3DS tests until Swift 6 compatibility is resolved * Add protocol for WebAuthenticationSession * Fix 3DS challenge result encoding * Expose method to create APM redirect URLs * Bump min iOS version to iOS 14 --- .github/actions/bootstrap-project/action.yml | 3 +- .../Sources/Application/AppDelegate.swift | 2 +- Package.swift | 17 +- ProcessOut.podspec | 5 +- ProcessOutCheckout3DS.podspec | 4 +- ProcessOutCoreUI.podspec | 4 +- ProcessOutUI.podspec | 2 +- Scripts/Test.sh | 5 +- .../ProcessOut/Sources/Api/ProcessOut.swift | 4 +- .../DefaultDeviceMetadataProvider.swift | 2 +- .../SystemLoggerDestination.swift | 8 +- .../Sources/Core/Logger/POLogger.swift | 4 +- .../Core/Markdown/MarkdownParser.swift | 1 - .../UnfairlyLocked/POUnfairlyLocked.swift | 22 +- .../Utils/UnfairlyLocked/UnfairLock.swift | 4 +- .../DefaultWebAuthenticationSession.swift | 121 +++++++++ .../WebAuthenticationSession.swift | 116 +-------- .../Generated/Sourcery+Generated.swift | 1 + .../Services/3DS/DefaultThreeDSService.swift | 2 +- .../DefaultAlternativePaymentsService.swift | 16 +- .../POAlternativePaymentsService.swift | 6 + .../NativeAlternativePaymentContentView.swift | 2 +- ...assKitPaymentAuthorizationController.swift | 2 +- .../ProcessOutTests/Sources/Core/Utils.swift | 9 +- .../Integration/CardsServiceTests.swift | 7 +- .../CustomerTokensServiceTests.swift | 12 +- ...GatewayConfigurationsRepositoryTests.swift | 3 +- .../Sources/Mocks/3DS/Mock3DSService.swift | 65 +++-- .../StubDeviceMetadataProvider.swift | 8 +- .../MockHttpConnectorRequestMapper.swift | 29 ++- .../Mocks/Logger/POLogger+Extensions.swift | 2 +- .../MockPhoneNumberMetadataProvider.swift | 19 -- .../Mocks/UrlProtocol/MockUrlProtocol.swift | 26 +- .../MockWebAuthenticationSession.swift | 40 +++ .../Http/UrlSessionHttpConnectorTests.swift | 10 +- .../CodingUtils/FallbackDecodableTests.swift | 54 ---- .../ImmutableExcludedCodableTests.swift | 30 --- ...> StringCodableOptionalDecimalTests.swift} | 16 +- .../Unit/Core/Utils/AsyncUtilsTests.swift | 51 ++-- .../Core/Utils/UIImage+DynamicTests.swift | 8 - .../3DS/DefaultThreeDSServiceTests.swift | 239 ++++++++---------- ...lternativePaymentMethodsServiceTests.swift | 50 +--- .../DefaultCardUpdateInteractorTests.swift | 14 +- .../CardUpdate/CardUpdateDelegateMock.swift | 29 ++- .../Mocks/Logger/Logger+Extensions.swift | 2 +- .../MockPhoneNumberMetadataProvider.swift | 35 +++ .../CardExpirationFormatterTests.swift | 8 +- .../CardNumber/CardNumberFormatterTests.swift | 8 +- ...aultPhoneNumberMetadataProviderTests.swift | 4 +- .../PhoneNumberFormatterTests.swift | 28 +- .../Core/Utils/FormattingUtilsTests.swift | 18 +- project.yml | 12 +- 52 files changed, 595 insertions(+), 594 deletions(-) create mode 100644 Sources/ProcessOut/Sources/Core/WebAuthenticationSession/DefaultWebAuthenticationSession.swift delete mode 100644 Tests/ProcessOutTests/Sources/Mocks/PhoneNumberMetadataProvider/MockPhoneNumberMetadataProvider.swift create mode 100644 Tests/ProcessOutTests/Sources/Mocks/WebAuthenticationSession/MockWebAuthenticationSession.swift delete mode 100644 Tests/ProcessOutTests/Sources/Unit/Core/CodingUtils/FallbackDecodableTests.swift delete mode 100644 Tests/ProcessOutTests/Sources/Unit/Core/CodingUtils/ImmutableExcludedCodableTests.swift rename Tests/ProcessOutTests/Sources/Unit/Core/CodingUtils/{ImmutableStringCodableOptionalDecimalTests.swift => StringCodableOptionalDecimalTests.swift} (74%) delete mode 100644 Tests/ProcessOutTests/Sources/Unit/Core/Utils/UIImage+DynamicTests.swift create mode 100644 Tests/ProcessOutUITests/Sources/Mocks/PhoneNumberMetadataProvider/MockPhoneNumberMetadataProvider.swift rename Tests/{ProcessOutTests => ProcessOutUITests}/Sources/Unit/Core/Formatters/CardExpiration/CardExpirationFormatterTests.swift (94%) rename Tests/{ProcessOutTests => ProcessOutUITests}/Sources/Unit/Core/Formatters/CardNumber/CardNumberFormatterTests.swift (90%) rename Tests/{ProcessOutTests => ProcessOutUITests}/Sources/Unit/Core/Formatters/PhoneNumber/DefaultPhoneNumberMetadataProviderTests.swift (88%) rename Tests/{ProcessOutTests => ProcessOutUITests}/Sources/Unit/Core/Formatters/PhoneNumber/PhoneNumberFormatterTests.swift (81%) rename Tests/{ProcessOutTests => ProcessOutUITests}/Sources/Unit/Core/Utils/FormattingUtilsTests.swift (81%) diff --git a/.github/actions/bootstrap-project/action.yml b/.github/actions/bootstrap-project/action.yml index 1a2713ac0..58b5cca45 100644 --- a/.github/actions/bootstrap-project/action.yml +++ b/.github/actions/bootstrap-project/action.yml @@ -16,7 +16,8 @@ runs: echo "$CONSTANTS" > Example/Example/Resources/Constants.yml shell: bash - name: Select Xcode Version - run: sudo xcode-select -s '/Applications/Xcode_15.4.app/Contents/Developer' + # todo(andrii-vysotskyi): Migrate to public release once available + run: sudo xcode-select -s '/Applications/Xcode_16_beta_4.app/Contents/Developer' shell: bash - name: Bootstrap Project run: ./Scripts/BootstrapProject.sh diff --git a/Example/Example/Sources/Application/AppDelegate.swift b/Example/Example/Sources/Application/AppDelegate.swift index a3157eecd..e052ec712 100644 --- a/Example/Example/Sources/Application/AppDelegate.swift +++ b/Example/Example/Sources/Application/AppDelegate.swift @@ -36,7 +36,7 @@ final class AppDelegate: UIResponder, UIApplicationDelegate { // Please note that implementation is using factory method (part of private interface) that creates // configuration with private key. It is only done for demonstration/testing purposes to avoid setting // up test server and shouldn't be shipped with production code. - let configuration = ProcessOutConfiguration.production( + let configuration = ProcessOutConfiguration( projectId: Constants.projectId, privateKey: Constants.projectPrivateKey ) ProcessOut.configure(configuration: configuration) diff --git a/Package.swift b/Package.swift index 4c5ebd5a3..e37d94c12 100644 --- a/Package.swift +++ b/Package.swift @@ -2,11 +2,6 @@ import PackageDescription -let swiftSettings: [SwiftSetting] = [ - .enableExperimentalFeature("IsolatedAny"), - .enableUpcomingFeature("StrictConcurrency") -] - let package = Package( name: "ProcessOut", defaultLocalization: "en", @@ -25,13 +20,9 @@ let package = Package( targets: [ .target( name: "ProcessOut", - dependencies: [ - .target(name: "cmark") - ], resources: [ .process("Resources") - ], - swiftSettings: swiftSettings + ] ), .target( name: "ProcessOutCheckout3DS", @@ -50,8 +41,7 @@ let package = Package( ], resources: [ .process("Resources") - ], - swiftSettings: swiftSettings + ] ), .target( name: "ProcessOutCoreUI", @@ -60,8 +50,7 @@ let package = Package( ], resources: [ .process("Resources") - ], - swiftSettings: swiftSettings + ] ), .binaryTarget(name: "cmark", path: "Vendor/cmark.xcframework") ] diff --git a/ProcessOut.podspec b/ProcessOut.podspec index 31ece0de1..ac0eea650 100644 --- a/ProcessOut.podspec +++ b/ProcessOut.podspec @@ -1,15 +1,14 @@ Pod::Spec.new do |s| s.name = 'ProcessOut' s.version = '4.19.0' - s.swift_versions = ['5.9'] + s.swift_versions = ['6.0'] s.license = { :type => 'MIT', :file => 'LICENSE' } s.homepage = 'https://github.com/processout/processout-ios' s.author = 'ProcessOut' s.summary = 'The smart router for payments. Smartly route each transaction to the relevant payment providers.' s.source = { :git => 'https://github.com/processout/processout-ios.git', :tag => s.version.to_s } s.frameworks = 'Foundation', 'UIKit' - s.ios.deployment_target = '13.0' - s.vendored_frameworks = "Vendor/cmark.xcframework" + s.ios.deployment_target = '14.0' s.ios.resources = 'Sources/ProcessOut/Resources/**/*' s.source_files = 'Sources/ProcessOut/**/*.swift' s.pod_target_xcconfig = { 'OTHER_SWIFT_FLAGS' => '-Xfrontend -module-interface-preserve-types-as-written' } diff --git a/ProcessOutCheckout3DS.podspec b/ProcessOutCheckout3DS.podspec index 302ab1da0..658e9f47b 100644 --- a/ProcessOutCheckout3DS.podspec +++ b/ProcessOutCheckout3DS.podspec @@ -1,14 +1,14 @@ Pod::Spec.new do |s| s.name = 'ProcessOutCheckout3DS' s.version = '4.19.0' - s.swift_versions = ['5.9'] + s.swift_versions = ['6.0'] s.license = { :type => 'MIT', :file => 'LICENSE' } s.homepage = 'https://github.com/processout/processout-ios' s.author = 'ProcessOut' s.summary = 'Integration with Checkout.com 3D Secure (3DS) mobile SDK.' s.source = { :git => 'https://github.com/processout/processout-ios.git', :tag => s.version.to_s } s.frameworks = 'Foundation' - s.ios.deployment_target = '13.0' + s.ios.deployment_target = '14.0' s.ios.resources = 'Sources/ProcessOutCheckout3DS/Resources/**/*' s.source_files = 'Sources/ProcessOutCheckout3DS/**/*.swift' s.pod_target_xcconfig = { 'EXCLUDED_ARCHS' => 'x86_64' } diff --git a/ProcessOutCoreUI.podspec b/ProcessOutCoreUI.podspec index 139911ccc..f97e99b90 100644 --- a/ProcessOutCoreUI.podspec +++ b/ProcessOutCoreUI.podspec @@ -1,15 +1,15 @@ Pod::Spec.new do |s| s.name = 'ProcessOutCoreUI' s.version = '4.19.0' - s.swift_versions = ['5.9'] + s.swift_versions = ['6.0'] s.license = { :type => 'MIT', :file => 'LICENSE' } s.homepage = 'https://github.com/processout/processout-ios' s.author = 'ProcessOut' s.summary = 'Reusable UI components and logic. Pod is meant to be used only with other ProcessOut pods.' s.source = { :git => 'https://github.com/processout/processout-ios.git', :tag => s.version.to_s } s.frameworks = 'Foundation', 'SwiftUI' + s.vendored_frameworks = "Vendor/cmark.xcframework" s.ios.deployment_target = '14.0' s.ios.resources = 'Sources/ProcessOutCoreUI/Resources/**/*' s.source_files = 'Sources/ProcessOutCoreUI/**/*.swift' - s.dependency 'ProcessOut' # todo(andrii-vysotskyi): vendor cmark.xcframework instead after UI migration is completed end diff --git a/ProcessOutUI.podspec b/ProcessOutUI.podspec index 5226d9460..1b762cc0e 100644 --- a/ProcessOutUI.podspec +++ b/ProcessOutUI.podspec @@ -1,7 +1,7 @@ Pod::Spec.new do |s| s.name = 'ProcessOutUI' s.version = '4.19.0' - s.swift_versions = ['5.9'] + s.swift_versions = ['6.0'] s.license = { :type => 'MIT', :file => 'LICENSE' } s.homepage = 'https://github.com/processout/processout-ios' s.author = 'ProcessOut' 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/Sources/Api/ProcessOut.swift b/Sources/ProcessOut/Sources/Api/ProcessOut.swift index d40821695..f030d1c16 100644 --- a/Sources/ProcessOut/Sources/Api/ProcessOut.swift +++ b/Sources/ProcessOut/Sources/Api/ProcessOut.swift @@ -132,7 +132,7 @@ public final class ProcessOut: @unchecked Sendable { let configuration = self.configuration return .init(projectId: configuration.projectId, baseUrl: configuration.checkoutBaseUrl) } - let webSession = WebAuthenticationSession() + let webSession = DefaultWebAuthenticationSession() return DefaultAlternativePaymentsService( configuration: serviceConfiguration, webSession: webSession, logger: logger ) @@ -144,7 +144,7 @@ public final class ProcessOut: @unchecked Sendable { let encoder = JSONEncoder() encoder.dataEncodingStrategy = .base64 encoder.keyEncodingStrategy = .useDefaultKeys - let webSession = WebAuthenticationSession() + let webSession = DefaultWebAuthenticationSession() return DefaultThreeDSService(decoder: decoder, encoder: encoder, webSession: webSession) } diff --git a/Sources/ProcessOut/Sources/Core/DeviceMetadata/DefaultDeviceMetadataProvider.swift b/Sources/ProcessOut/Sources/Core/DeviceMetadata/DefaultDeviceMetadataProvider.swift index f42464c3f..bc58aacd1 100644 --- a/Sources/ProcessOut/Sources/Core/DeviceMetadata/DefaultDeviceMetadataProvider.swift +++ b/Sources/ProcessOut/Sources/Core/DeviceMetadata/DefaultDeviceMetadataProvider.swift @@ -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/Logger/Destinations/SystemLoggerDestination.swift b/Sources/ProcessOut/Sources/Core/Logger/Destinations/SystemLoggerDestination.swift index e48772e63..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 nonisolated(unsafe) 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/POLogger.swift b/Sources/ProcessOut/Sources/Core/Logger/POLogger.swift index f01fde5eb..b6b337374 100644 --- a/Sources/ProcessOut/Sources/Core/Logger/POLogger.swift +++ b/Sources/ProcessOut/Sources/Core/Logger/POLogger.swift @@ -16,7 +16,7 @@ public struct POLogger: Sendable { self.category = category self.minimumLevel = minimumLevel self.attributes = [:] - lock = NSLock() + lock = POUnfairlyLocked() } init(destinations: [LoggerDestination] = [], category: String) { @@ -84,7 +84,7 @@ public struct POLogger: Sendable { private let destinations: [LoggerDestination] private let minimumLevel: @Sendable () -> LogLevel - private let lock: NSLock + private let lock: POUnfairlyLocked private var attributes: [POLogAttributeKey: String] // MARK: - Private Methods diff --git a/Sources/ProcessOut/Sources/Core/Markdown/MarkdownParser.swift b/Sources/ProcessOut/Sources/Core/Markdown/MarkdownParser.swift index bace98f06..cfad9257d 100644 --- a/Sources/ProcessOut/Sources/Core/Markdown/MarkdownParser.swift +++ b/Sources/ProcessOut/Sources/Core/Markdown/MarkdownParser.swift @@ -6,7 +6,6 @@ // import Foundation -@_implementationOnly import cmark enum MarkdownParser { diff --git a/Sources/ProcessOut/Sources/Core/Utils/UnfairlyLocked/POUnfairlyLocked.swift b/Sources/ProcessOut/Sources/Core/Utils/UnfairlyLocked/POUnfairlyLocked.swift index 43429859e..df7289c3a 100644 --- a/Sources/ProcessOut/Sources/Core/Utils/UnfairlyLocked/POUnfairlyLocked.swift +++ b/Sources/ProcessOut/Sources/Core/Utils/UnfairlyLocked/POUnfairlyLocked.swift @@ -20,14 +20,8 @@ public final class POUnfairlyLocked: @unchecked Sendable { 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 @@ public final class POUnfairlyLocked: @unchecked Sendable { 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 e9e0c59cc..0c58c9d12 100644 --- a/Sources/ProcessOut/Sources/Core/Utils/UnfairlyLocked/UnfairLock.swift +++ b/Sources/ProcessOut/Sources/Core/Utils/UnfairlyLocked/UnfairLock.swift @@ -15,12 +15,12 @@ final class UnfairLock: Sendable { 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 { diff --git a/Sources/ProcessOut/Sources/Core/WebAuthenticationSession/DefaultWebAuthenticationSession.swift b/Sources/ProcessOut/Sources/Core/WebAuthenticationSession/DefaultWebAuthenticationSession.swift new file mode 100644 index 000000000..8ee950b81 --- /dev/null +++ b/Sources/ProcessOut/Sources/Core/WebAuthenticationSession/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/Core/WebAuthenticationSession/WebAuthenticationSession.swift b/Sources/ProcessOut/Sources/Core/WebAuthenticationSession/WebAuthenticationSession.swift index 7ccbbf187..9ce8f6457 100644 --- a/Sources/ProcessOut/Sources/Core/WebAuthenticationSession/WebAuthenticationSession.swift +++ b/Sources/ProcessOut/Sources/Core/WebAuthenticationSession/WebAuthenticationSession.swift @@ -5,116 +5,22 @@ // Created by Andrii Vysotskyi on 01.08.2024. // -import AuthenticationServices +import Foundation -@MainActor -final class WebAuthenticationSession: NSObject, Sendable, ASWebAuthenticationPresentationContextProviding { - - override nonisolated init() { - // Ignored - } +protocol WebAuthenticationSession: Sendable { + /// Begins a web authentication session. func authenticate( - using url: URL, - callbackScheme: String? = nil, - additionalHeaderFields: [String: String]? = nil - ) async throws -> URL { - let sessionProxy = WebAuthenticationSessionProxy() - return try await withTaskCancellationHandler( - operation: { - try await withCheckedThrowingContinuation { continuation in - guard !Task.isCancelled else { - let failure = POFailure(message: "Authentication session was cancelled.", code: .cancelled) - continuation.resume(throwing: failure) - return - } - let session = ASWebAuthenticationSession( - url: url, - callbackURLScheme: callbackScheme, - completionHandler: { url, error in - sessionProxy.invalidate() - if let error { - continuation.resume(throwing: Self.converted(error: error)) - } else if let url { - continuation.resume(returning: url) - } else { - preconditionFailure("Unexpected ASWebAuthenticationSession completion result.") - } - } - ) - session.prefersEphemeralWebBrowserSession = true - session.presentationContextProvider = self - if #available(iOS 17.4, *) { - session.additionalHeaderFields = additionalHeaderFields - } - sessionProxy.setSession(session, continuation: continuation) - session.start() - } - }, - onCancel: { - sessionProxy.cancel() - } - ) - } - - // MARK: - ASWebAuthenticationPresentationContextProviding - - func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor { - let application = UIApplication.shared - let scene = application.connectedScenes.first { $0 is UIWindowScene } as? UIWindowScene - let window = scene?.windows.first(where: \.isKeyWindow) - return window ?? ASPresentationAnchor() - } - - // MARK: - Private Methods - - private static func converted(error: Error) -> POFailure { - guard let error = error as? ASWebAuthenticationSessionError else { - return POFailure(code: .generic(.mobile), underlyingError: error) - } - let poCode: POFailure.Code - switch error.code { - case .canceledLogin: - poCode = .cancelled - case .presentationContextNotProvided, .presentationContextInvalid: - poCode = .internal(.mobile) - @unknown default: - poCode = .generic(.mobile) - } - return POFailure(code: poCode, underlyingError: error) - } + using url: URL, callbackScheme: String?, additionalHeaderFields: [String: String]? + ) async throws -> URL } -@MainActor -private final class WebAuthenticationSessionProxy: Sendable { +extension WebAuthenticationSession { - 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() + /// 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/Generated/Sourcery+Generated.swift b/Sources/ProcessOut/Sources/Generated/Sourcery+Generated.swift index f74d9e2de..b2da9612d 100644 --- a/Sources/ProcessOut/Sources/Generated/Sourcery+Generated.swift +++ b/Sources/ProcessOut/Sources/Generated/Sourcery+Generated.swift @@ -50,6 +50,7 @@ extension POCreateCustomerTokenRequest { enum CodingKeys: String, CodingKey { case verify case invoiceReturnUrl + case returnUrl } } diff --git a/Sources/ProcessOut/Sources/Services/3DS/DefaultThreeDSService.swift b/Sources/ProcessOut/Sources/Services/3DS/DefaultThreeDSService.swift index a8cf84259..f9181bf30 100644 --- a/Sources/ProcessOut/Sources/Services/3DS/DefaultThreeDSService.swift +++ b/Sources/ProcessOut/Sources/Services/3DS/DefaultThreeDSService.swift @@ -84,7 +84,7 @@ final class DefaultThreeDSService: ThreeDSService { throw POFailure(message: message, code: .internal(.mobile), underlyingError: error) } let response = AuthenticationResponse(url: nil, body: encodedChallengeResult) - return try Constants.tokenPrefix + encode(authenticationResponse: response) + return try encode(authenticationResponse: response) } // MARK: - Web Based 3DS diff --git a/Sources/ProcessOut/Sources/Services/AlternativePayments/DefaultAlternativePaymentsService.swift b/Sources/ProcessOut/Sources/Services/AlternativePayments/DefaultAlternativePaymentsService.swift index b5548c440..e3150c576 100644 --- a/Sources/ProcessOut/Sources/Services/AlternativePayments/DefaultAlternativePaymentsService.swift +++ b/Sources/ProcessOut/Sources/Services/AlternativePayments/DefaultAlternativePaymentsService.swift @@ -22,18 +22,24 @@ final class DefaultAlternativePaymentsService: POAlternativePaymentsService { // MARK: - POAlternativePaymentsService func tokenize(request: POAlternativePaymentTokenizationRequest) async throws -> POAlternativePaymentResponse { - let pathComponents = [request.customerId, request.tokenId, "redirect", request.gatewayConfigurationId] - let redirectUrl = try url(with: pathComponents, additionalData: request.additionalData) - return try await authenticate(using: redirectUrl) + 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] } - let redirectUrl = try url(with: pathComponents, additionalData: request.additionalData) - return try await authenticate(using: redirectUrl) + return try url(with: pathComponents, additionalData: request.additionalData) } func authenticate(using url: URL) async throws -> POAlternativePaymentResponse { diff --git a/Sources/ProcessOut/Sources/Services/AlternativePayments/POAlternativePaymentsService.swift b/Sources/ProcessOut/Sources/Services/AlternativePayments/POAlternativePaymentsService.swift index 18c78cd5b..ec2e2c616 100644 --- a/Sources/ProcessOut/Sources/Services/AlternativePayments/POAlternativePaymentsService.swift +++ b/Sources/ProcessOut/Sources/Services/AlternativePayments/POAlternativePaymentsService.swift @@ -16,6 +16,12 @@ public protocol POAlternativePaymentsService: POService { /// 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/ProcessOutUI/Sources/Modules/NativeAlternativePayment/View/Sections/NativeAlternativePaymentContentView.swift b/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/View/Sections/NativeAlternativePaymentContentView.swift index 51fa62817..5a11f0abb 100644 --- a/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/View/Sections/NativeAlternativePaymentContentView.swift +++ b/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/View/Sections/NativeAlternativePaymentContentView.swift @@ -72,7 +72,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/PassKitPaymentAuthorization/POPassKitPaymentAuthorizationController.swift b/Sources/ProcessOutUI/Sources/Modules/PassKitPaymentAuthorization/POPassKitPaymentAuthorizationController.swift index 3acc4cb55..d89344311 100644 --- a/Sources/ProcessOutUI/Sources/Modules/PassKitPaymentAuthorization/POPassKitPaymentAuthorizationController.swift +++ b/Sources/ProcessOutUI/Sources/Modules/PassKitPaymentAuthorization/POPassKitPaymentAuthorizationController.swift @@ -59,7 +59,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) } } } 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 853504d14..83f7e1918 100644 --- a/Tests/ProcessOutTests/Sources/Integration/CardsServiceTests.swift +++ b/Tests/ProcessOutTests/Sources/Integration/CardsServiceTests.swift @@ -11,9 +11,10 @@ import XCTest final class CardsServiceTests: XCTestCase { + @MainActor override func setUp() { super.setUp() - ProcessOut.configure(configuration: .production(projectId: Constants.projectId), force: true) + ProcessOut.configure(configuration: .init(projectId: Constants.projectId), force: true) sut = ProcessOut.shared.cards } @@ -27,7 +28,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 +100,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 3b31e5eb3..549c884a2 100644 --- a/Tests/ProcessOutTests/Sources/Integration/CustomerTokensServiceTests.swift +++ b/Tests/ProcessOutTests/Sources/Integration/CustomerTokensServiceTests.swift @@ -10,9 +10,10 @@ import XCTest final class CustomerTokensServiceTests: XCTestCase { + @MainActor override func setUp() { super.setUp() - let configuration = ProcessOutConfiguration.production( + let configuration = ProcessOutConfiguration( projectId: Constants.projectId, privateKey: Constants.projectPrivateKey ) ProcessOut.configure(configuration: configuration, force: true) @@ -60,19 +61,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 3bb418ada..d257f268c 100644 --- a/Tests/ProcessOutTests/Sources/Integration/GatewayConfigurationsRepositoryTests.swift +++ b/Tests/ProcessOutTests/Sources/Integration/GatewayConfigurationsRepositoryTests.swift @@ -11,9 +11,10 @@ import XCTest final class GatewayConfigurationsRepositoryTests: XCTestCase { + @MainActor override func setUp() { super.setUp() - ProcessOut.configure(configuration: .production(projectId: Constants.projectId), force: true) + 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..5ccb8daf3 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: "Y") } // 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: "Y") } // 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: "N") } // 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 01f5911ad..d0b225ce4 100644 --- a/Tests/ProcessOutUITests/Sources/Integration/CardUpdate/DefaultCardUpdateInteractorTests.swift +++ b/Tests/ProcessOutUITests/Sources/Integration/CardUpdate/DefaultCardUpdateInteractorTests.swift @@ -11,14 +11,16 @@ import XCTest final class DefaultCardUpdateInteractorTests: XCTestCase { + @MainActor override func setUp() { super.setUp() - ProcessOut.configure(configuration: .production(projectId: Constants.projectId), force: true) + 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 +43,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 +60,7 @@ final class DefaultCardUpdateInteractorTests: XCTestCase { XCTAssertEqual(startedState.scheme, .visa) } + @MainActor func test_start_whenPreferredCardSchemeIsAvailable_setsStartedStateWithIt() { // Given let configuration = POCardUpdateConfiguration( @@ -76,6 +80,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 +99,7 @@ final class DefaultCardUpdateInteractorTests: XCTestCase { wait(for: [expectation]) } + @MainActor func test_start_whenCardSchemeIsNotSetAndMaskedNumberIsSet_attemptsToResolve() { // Given let configuration = POCardUpdateConfiguration( @@ -116,6 +122,7 @@ final class DefaultCardUpdateInteractorTests: XCTestCase { // MARK: - Cancel + @MainActor func test_cancel_whenStarted() { // Given let configuration = POCardUpdateConfiguration(cardId: "", cardInformation: .init(scheme: "visa")) @@ -131,6 +138,7 @@ final class DefaultCardUpdateInteractorTests: XCTestCase { // MARK: - Update CVC + @MainActor func test_updateCvc_whenStarting_isIgnored() { // Given let configuration = POCardUpdateConfiguration(cardId: "") @@ -145,6 +153,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 +172,7 @@ final class DefaultCardUpdateInteractorTests: XCTestCase { // MARK: - Submit + @MainActor func test_submit_whenCvcIsNotSet_causesError() { // Given let configuration = POCardUpdateConfiguration(cardId: "", cardInformation: .init(scheme: "visa")) @@ -182,6 +192,7 @@ final class DefaultCardUpdateInteractorTests: XCTestCase { wait(for: [expectation]) } + @MainActor func test_submit_whenValidCvcIsSet_completes() { // Given let configuration = POCardUpdateConfiguration( @@ -210,6 +221,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..13e18362b 100644 --- a/Tests/ProcessOutUITests/Sources/Mocks/CardUpdate/CardUpdateDelegateMock.swift +++ b/Tests/ProcessOutUITests/Sources/Mocks/CardUpdate/CardUpdateDelegateMock.swift @@ -5,14 +5,25 @@ // 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 @@ -27,4 +38,12 @@ final class CardUpdateDelegateMock: POCardUpdateDelegate { func shouldContinueUpdate(after 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 b81d6838d..bfa073813 100644 --- a/project.yml +++ b/project.yml @@ -20,7 +20,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 @@ -35,7 +35,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 @@ -51,12 +51,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 @@ -69,8 +69,6 @@ targets: - path: Sources/ProcessOut excludes: - swiftgen.yml - dependencies: - - framework: Vendor/cmark.xcframework ProcessOutTests: type: bundle.unit-test platform: iOS @@ -110,7 +108,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 From 70f10499bd2d5e9dafe0efb94fe607b2152956b1 Mon Sep 17 00:00:00 2001 From: Andrii Vysotskyi Date: Mon, 12 Aug 2024 17:14:50 +0200 Subject: [PATCH 07/10] feat(ad-hoc): update localizations (#326) * Fix localized string macro name * Remove unused strings from "Localizable.xcstrings" * Ensure that Accept-Language header is set to proper value by adding "dummy" key translation to all supported languages --- .../Resources/Localizable.xcstrings | 599 +----------------- .../Utils/Strings+PreferredLocalization.swift | 5 +- .../Invoices/Responses/POInvoice.swift | 1 + .../DynamicCheckoutDefaultInteractor.swift | 2 +- project.yml | 2 +- 5 files changed, 13 insertions(+), 596 deletions(-) diff --git a/Sources/ProcessOut/Resources/Localizable.xcstrings b/Sources/ProcessOut/Resources/Localizable.xcstrings index 19c0adfac..c82776e74 100644 --- a/Sources/ProcessOut/Resources/Localizable.xcstrings +++ b/Sources/ProcessOut/Resources/Localizable.xcstrings @@ -1,622 +1,37 @@ { "sourceLanguage" : "en", "strings" : { - "native-alternative-payment.cancel-button.title" : { - "extractionState" : "stale", + "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" : { - "extractionState" : "stale", - "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" : { - "extractionState" : "stale", - "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" : { - "extractionState" : "stale", - "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" : { - "extractionState" : "stale", - "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" : { - "extractionState" : "stale", - "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" : { - "extractionState" : "stale", - "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" : { - "extractionState" : "stale", - "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" : { - "extractionState" : "stale", - "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" : { - "extractionState" : "stale", - "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" : { - "extractionState" : "stale", - "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" : { - "extractionState" : "stale", - "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" : { - "extractionState" : "stale", - "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" : { - "extractionState" : "stale", - "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" : { - "extractionState" : "stale", - "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" : { - "extractionState" : "stale", - "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/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/Repositories/Invoices/Responses/POInvoice.swift b/Sources/ProcessOut/Sources/Repositories/Invoices/Responses/POInvoice.swift index becca98d0..09e5bdaa5 100644 --- a/Sources/ProcessOut/Sources/Repositories/Invoices/Responses/POInvoice.swift +++ b/Sources/ProcessOut/Sources/Repositories/Invoices/Responses/POInvoice.swift @@ -13,6 +13,7 @@ public struct POInvoice: Decodable, Sendable { /// String value that uniquely identifies this invoice. public let id: String + /// Invoice amount. @POStringCodableDecimal public private(set) var amount: Decimal diff --git a/Sources/ProcessOutUI/Sources/Modules/DynamicCheckout/Interactor/DynamicCheckout/DynamicCheckoutDefaultInteractor.swift b/Sources/ProcessOutUI/Sources/Modules/DynamicCheckout/Interactor/DynamicCheckout/DynamicCheckoutDefaultInteractor.swift index eb6a163e0..16848f62a 100644 --- a/Sources/ProcessOutUI/Sources/Modules/DynamicCheckout/Interactor/DynamicCheckout/DynamicCheckoutDefaultInteractor.swift +++ b/Sources/ProcessOutUI/Sources/Modules/DynamicCheckout/Interactor/DynamicCheckout/DynamicCheckoutDefaultInteractor.swift @@ -298,7 +298,7 @@ final class DynamicCheckoutDefaultInteractor: case .recovering: logger.debug("Ignoring attempt to cancel payment during error recovery.") default: - assertionFailure("Attempted to cancel payment from unsupported state.") + logger.debug("Ignoring attempt to cancel payment from unsupported state.") } } diff --git a/project.yml b/project.yml index bfa073813..31204d08d 100644 --- a/project.yml +++ b/project.yml @@ -6,7 +6,7 @@ settings: SWIFT_TREAT_WARNINGS_AS_ERRORS: true 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 From fbee46d7ab9637e447c29330a8f991e0f2ac7091 Mon Sep 17 00:00:00 2001 From: Andrii Vysotskyi Date: Wed, 14 Aug 2024 15:15:52 +0200 Subject: [PATCH 08/10] fix(ad-hoc): dynamic checkout issues (#329) * Fix input submission crash * Replace thread asserts with explicit actor isolation * Remove redundant asserts --- .../ProcessOut/Sources/Core/Extensions/UIImage+Dynamic.swift | 4 ++-- .../Sources/Backports/OnSubmit/View+OnSubmit.swift | 4 ++-- .../DynamicCheckout/DynamicCheckoutDefaultInteractor.swift | 2 +- .../PassKit/DynamicCheckoutPassKitPaymentDefaultSession.swift | 2 +- .../NativeAlternativePaymentDefaultInteractor.swift | 4 ++-- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Sources/ProcessOut/Sources/Core/Extensions/UIImage+Dynamic.swift b/Sources/ProcessOut/Sources/Core/Extensions/UIImage+Dynamic.swift index ca5969e16..2e68d34da 100644 --- a/Sources/ProcessOut/Sources/Core/Extensions/UIImage+Dynamic.swift +++ b/Sources/ProcessOut/Sources/Core/Extensions/UIImage+Dynamic.swift @@ -9,9 +9,9 @@ import UIKit extension UIImage { + @MainActor static func dynamic(lightImage: UIImage?, darkImage: UIImage?) -> UIImage? { - assert(Thread.isMainThread) - // 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/ProcessOutCoreUI/Sources/Backports/OnSubmit/View+OnSubmit.swift b/Sources/ProcessOutCoreUI/Sources/Backports/OnSubmit/View+OnSubmit.swift index 16d97b214..2a9d6a3aa 100644 --- a/Sources/ProcessOutCoreUI/Sources/Backports/OnSubmit/View+OnSubmit.swift +++ b/Sources/ProcessOutCoreUI/Sources/Backports/OnSubmit/View+OnSubmit.swift @@ -10,7 +10,7 @@ import SwiftUI extension POBackport where Wrapped: Any { @MainActor - final class SubmitAction: Sendable { + struct SubmitAction: Sendable { typealias Action = () -> Void // swiftlint:disable:this nesting @@ -22,7 +22,7 @@ extension POBackport where Wrapped: Any { actions.forEach { $0() } } - func append(action: @escaping Action) { + mutating func append(action: @escaping Action) { actions.append(action) } diff --git a/Sources/ProcessOutUI/Sources/Modules/DynamicCheckout/Interactor/DynamicCheckout/DynamicCheckoutDefaultInteractor.swift b/Sources/ProcessOutUI/Sources/Modules/DynamicCheckout/Interactor/DynamicCheckout/DynamicCheckoutDefaultInteractor.swift index f5db2ed92..c8854910c 100644 --- a/Sources/ProcessOutUI/Sources/Modules/DynamicCheckout/Interactor/DynamicCheckout/DynamicCheckoutDefaultInteractor.swift +++ b/Sources/ProcessOutUI/Sources/Modules/DynamicCheckout/Interactor/DynamicCheckout/DynamicCheckoutDefaultInteractor.swift @@ -707,8 +707,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) } diff --git a/Sources/ProcessOutUI/Sources/Modules/DynamicCheckout/Sessions/PassKit/DynamicCheckoutPassKitPaymentDefaultSession.swift b/Sources/ProcessOutUI/Sources/Modules/DynamicCheckout/Sessions/PassKit/DynamicCheckoutPassKitPaymentDefaultSession.swift index d3dafad82..dfe27d0a5 100644 --- a/Sources/ProcessOutUI/Sources/Modules/DynamicCheckout/Sessions/PassKit/DynamicCheckoutPassKitPaymentDefaultSession.swift +++ b/Sources/ProcessOutUI/Sources/Modules/DynamicCheckout/Sessions/PassKit/DynamicCheckoutPassKitPaymentDefaultSession.swift @@ -55,7 +55,7 @@ extension DynamicCheckoutPassKitPaymentDefaultSession: POPassKitPaymentAuthoriza func paymentAuthorizationControllerDidFinish(_ controller: POPassKitPaymentAuthorizationController) { guard let didFinishContinuation else { - preconditionFailure("Continue must be set.") + preconditionFailure("Continuation must be set.") } didFinishContinuation.resume() } diff --git a/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/Interactor/NativeAlternativePaymentDefaultInteractor.swift b/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/Interactor/NativeAlternativePaymentDefaultInteractor.swift index c53011ae3..52f387ac4 100644 --- a/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/Interactor/NativeAlternativePaymentDefaultInteractor.swift +++ b/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/Interactor/NativeAlternativePaymentDefaultInteractor.swift @@ -403,8 +403,8 @@ final class NativeAlternativePaymentDefaultInteractor: // MARK: - Events + @MainActor private func send(event: PONativeAlternativePaymentEvent) { - assert(Thread.isMainThread, "Method should be called on main thread.") logger.debug("Did send event: '\(event)'") delegate?.nativeAlternativePaymentDidEmitEvent(event) } @@ -513,7 +513,7 @@ final class NativeAlternativePaymentDefaultInteractor: 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 From 51a1bc82315250546657a1833650e827356d768a Mon Sep 17 00:00:00 2001 From: Andrii Vysotskyi Date: Wed, 21 Aug 2024 10:40:12 +0200 Subject: [PATCH 09/10] feat(POM-408): add 5.0.0 migration guide (#333) * Add migration guide to ProcessOut, ProcessOutUI and ProcessOutCheckout3DS packages. * Update documentation. * Ensure nAPM, card tokenization and update delegate methods naming is consistent. * Update nAPM configuration structure. --- .github/actions/bootstrap-project/action.yml | 2 +- .../AlternativePaymentMethodsRouter.swift | 4 +- .../CardPayment/CardPaymentDelegate.swift | 4 +- Sources/ProcessOut/ProcessOut.docc/3DS.md | 13 +- .../ProcessOut.docc/Localizations.md | 2 +- .../ProcessOut.docc/MigrationGuides.md | 138 ++++++++++++++++-- .../NativeAlternativePaymentMethod.md | 71 --------- .../ProcessOut/ProcessOut.docc/ProcessOut.md | 82 ++--------- .../Api/Models/ProcessOutConfiguration.swift | 2 +- .../ProcessOut/Sources/Api/ProcessOut.swift | 2 +- .../Repositories/Cards/Responses/POCard.swift | 2 +- .../Cards/Responses/POCardScheme.swift | 90 ++++++------ .../3DS/Models/PO3DS2ChallengeResult.swift | 4 + .../DefaultWebAuthenticationSession.swift | 0 .../WebAuthenticationSession.swift | 0 .../MigrationGuides.md | 24 +++ .../ProcessOutCheckout3DS.md | 7 +- ...rvice.swift => POCheckout3DSService.swift} | 2 + .../ProcessOutCoreUI.docc/ProcessOutCoreUI.md | 4 +- .../ButtonStyle+POBrandButtonStyle.swift | 2 +- Sources/ProcessOutUI/ProcessOutUI.docc/3DS.md | 54 ------- .../ProcessOutUI.docc/CardUpdate.md | 8 +- .../ProcessOutUI.docc/MigrationGuides.md | 94 ++++++++++++ .../NativeAlternativePayment.md | 2 +- .../ProcessOutUI.docc/ProcessOutUI.md | 27 +--- .../Api/Test3DS/POTest3DSService.swift | 6 +- .../Formatters/Utils/FormattingUtils.swift | 4 +- .../Delegate/POCardTokenizationDelegate.swift | 16 +- .../DefaultCardTokenizationInteractor.swift | 22 +-- .../POCardUpdateConfiguration.swift | 2 +- .../Delegate/POCardUpdateDelegate.swift | 12 +- .../DefaultCardUpdateInteractor.swift | 17 +-- ...ckoutAlternativePaymentConfiguration.swift | 2 +- ...eckoutInteractorDefaultChildProvider.swift | 14 +- .../DynamicCheckoutDefaultInteractor.swift | 22 ++- ...ativeAlternativePaymentConfiguration.swift | 92 +++++++++--- ...NativeAlternativePaymentConfirmation.swift | 47 ------ .../PONativeAlternativePaymentDelegate.swift | 22 ++- ...eAlternativePaymentDefaultInteractor.swift | 30 +--- ...ultNativeAlternativePaymentViewModel.swift | 14 +- .../Integration/CardsServiceTests.swift | 7 +- .../CustomerTokensServiceTests.swift | 7 +- ...GatewayConfigurationsRepositoryTests.swift | 7 +- .../3DS/DefaultThreeDSServiceTests.swift | 6 +- .../DefaultCardUpdateInteractorTests.swift | 7 +- .../CardUpdate/CardUpdateDelegateMock.swift | 8 +- 46 files changed, 516 insertions(+), 488 deletions(-) delete mode 100644 Sources/ProcessOut/ProcessOut.docc/NativeAlternativePaymentMethod.md rename Sources/ProcessOut/Sources/{Core/WebAuthenticationSession => Sessions/WebAuthentication}/DefaultWebAuthenticationSession.swift (100%) rename Sources/ProcessOut/Sources/{Core/WebAuthenticationSession => Sessions/WebAuthentication}/WebAuthenticationSession.swift (100%) rename Sources/ProcessOutCheckout3DS/Sources/Service/{Checkout3DSService.swift => POCheckout3DSService.swift} (97%) delete mode 100644 Sources/ProcessOutUI/ProcessOutUI.docc/3DS.md create mode 100644 Sources/ProcessOutUI/ProcessOutUI.docc/MigrationGuides.md delete mode 100644 Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/Configuration/PONativeAlternativePaymentConfirmation.swift diff --git a/.github/actions/bootstrap-project/action.yml b/.github/actions/bootstrap-project/action.yml index 58b5cca45..67af23f13 100644 --- a/.github/actions/bootstrap-project/action.yml +++ b/.github/actions/bootstrap-project/action.yml @@ -17,7 +17,7 @@ runs: shell: bash - name: Select Xcode Version # todo(andrii-vysotskyi): Migrate to public release once available - run: sudo xcode-select -s '/Applications/Xcode_16_beta_4.app/Contents/Developer' + run: sudo xcode-select -s '/Applications/Xcode_16_beta_5.app/Contents/Developer' shell: bash - name: Bootstrap Project run: ./Scripts/BootstrapProject.sh diff --git a/Example/Example/Sources/UI/Modules/AlternativePayment/Methods/Router/AlternativePaymentMethodsRouter.swift b/Example/Example/Sources/UI/Modules/AlternativePayment/Methods/Router/AlternativePaymentMethodsRouter.swift index fb93edaf6..65a512449 100644 --- a/Example/Example/Sources/UI/Modules/AlternativePayment/Methods/Router/AlternativePaymentMethodsRouter.swift +++ b/Example/Example/Sources/UI/Modules/AlternativePayment/Methods/Router/AlternativePaymentMethodsRouter.swift @@ -20,10 +20,10 @@ final class AlternativePaymentMethodsRouter: RouterType { let configuration = PONativeAlternativePaymentConfiguration( invoiceId: route.invoiceId, gatewayConfigurationId: route.gatewayConfigurationId, - secondaryAction: .cancel(), + cancelButton: .init(), paymentConfirmation: .init( showProgressIndicatorAfter: 5, - secondaryAction: .cancel(disabledFor: 10) + cancelButton: .init(disabledFor: 10) ) ) let viewController = PONativeAlternativePaymentViewController( diff --git a/Example/Example/Sources/UI/Modules/CardPayment/CardPaymentDelegate.swift b/Example/Example/Sources/UI/Modules/CardPayment/CardPaymentDelegate.swift index c01ae3ec2..560d01948 100644 --- a/Example/Example/Sources/UI/Modules/CardPayment/CardPaymentDelegate.swift +++ b/Example/Example/Sources/UI/Modules/CardPayment/CardPaymentDelegate.swift @@ -16,7 +16,9 @@ final class CardPaymentDelegate: POCardTokenizationDelegate { self.threeDSService = threeDSService } - func processTokenizedCard(card: POCard) async throws { + // MARK: - POCardTokenizationDelegate + + func cardTokenization(didTokenizeCard card: POCard) async throws { let invoiceCreationRequest = POInvoiceCreationRequest( name: UUID().uuidString, amount: "20", 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 b7bd6e891..3861ebf93 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 @@ -98,24 +79,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 @@ -126,36 +95,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/Sources/Api/Models/ProcessOutConfiguration.swift b/Sources/ProcessOut/Sources/Api/Models/ProcessOutConfiguration.swift index 56b771805..b798a30ce 100644 --- a/Sources/ProcessOut/Sources/Api/Models/ProcessOutConfiguration.swift +++ b/Sources/ProcessOut/Sources/Api/Models/ProcessOutConfiguration.swift @@ -8,7 +8,7 @@ import Foundation /// 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: Sendable { diff --git a/Sources/ProcessOut/Sources/Api/ProcessOut.swift b/Sources/ProcessOut/Sources/Api/ProcessOut.swift index f030d1c16..1977dd902 100644 --- a/Sources/ProcessOut/Sources/Api/ProcessOut.swift +++ b/Sources/ProcessOut/Sources/Api/ProcessOut.swift @@ -232,12 +232,12 @@ extension ProcessOut { /// 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) { - MainActor.preconditionIsolated("Shared instance must be configured from main thread.") if isConfigured { if force { shared._configuration.withLock { $0 = configuration } diff --git a/Sources/ProcessOut/Sources/Repositories/Cards/Responses/POCard.swift b/Sources/ProcessOut/Sources/Repositories/Cards/Responses/POCard.swift index b522c04c8..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, @unchecked Sendable { +public struct POCard: Decodable, Hashable, Sendable { /// Value that uniquely identifies the card. public let id: String diff --git a/Sources/ProcessOut/Sources/Repositories/Cards/Responses/POCardScheme.swift b/Sources/ProcessOut/Sources/Repositories/Cards/Responses/POCardScheme.swift index 87d9a79f5..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,59 +128,56 @@ 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: Codable { diff --git a/Sources/ProcessOut/Sources/Services/3DS/Models/PO3DS2ChallengeResult.swift b/Sources/ProcessOut/Sources/Services/3DS/Models/PO3DS2ChallengeResult.swift index ef327e5ca..e18167582 100644 --- a/Sources/ProcessOut/Sources/Services/3DS/Models/PO3DS2ChallengeResult.swift +++ b/Sources/ProcessOut/Sources/Services/3DS/Models/PO3DS2ChallengeResult.swift @@ -15,6 +15,10 @@ public struct PO3DS2ChallengeResult: Encodable, Sendable { self.transactionStatus = transactionStatus } + public init(transactionStatus: Bool) { + self.transactionStatus = transactionStatus ? "Y" : "N" + } + // MARK: - Private Nested Types private enum CodingKeys: String, CodingKey { diff --git a/Sources/ProcessOut/Sources/Core/WebAuthenticationSession/DefaultWebAuthenticationSession.swift b/Sources/ProcessOut/Sources/Sessions/WebAuthentication/DefaultWebAuthenticationSession.swift similarity index 100% rename from Sources/ProcessOut/Sources/Core/WebAuthenticationSession/DefaultWebAuthenticationSession.swift rename to Sources/ProcessOut/Sources/Sessions/WebAuthentication/DefaultWebAuthenticationSession.swift diff --git a/Sources/ProcessOut/Sources/Core/WebAuthenticationSession/WebAuthenticationSession.swift b/Sources/ProcessOut/Sources/Sessions/WebAuthentication/WebAuthenticationSession.swift similarity index 100% rename from Sources/ProcessOut/Sources/Core/WebAuthenticationSession/WebAuthenticationSession.swift rename to Sources/ProcessOut/Sources/Sessions/WebAuthentication/WebAuthenticationSession.swift 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/Service/Checkout3DSService.swift b/Sources/ProcessOutCheckout3DS/Sources/Service/POCheckout3DSService.swift similarity index 97% rename from Sources/ProcessOutCheckout3DS/Sources/Service/Checkout3DSService.swift rename to Sources/ProcessOutCheckout3DS/Sources/Service/POCheckout3DSService.swift index af408c01f..c97456f7b 100644 --- a/Sources/ProcessOutCheckout3DS/Sources/Service/Checkout3DSService.swift +++ b/Sources/ProcessOutCheckout3DS/Sources/Service/POCheckout3DSService.swift @@ -8,6 +8,8 @@ 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) { 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/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/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/ProcessOutUI/Sources/Api/Test3DS/POTest3DSService.swift b/Sources/ProcessOutUI/Sources/Api/Test3DS/POTest3DSService.swift index b1741150d..cca4e2e85 100644 --- a/Sources/ProcessOutUI/Sources/Api/Test3DS/POTest3DSService.swift +++ b/Sources/ProcessOutUI/Sources/Api/Test3DS/POTest3DSService.swift @@ -12,7 +12,7 @@ import ProcessOut /// Control Server (ACS). Should be used only for testing purposes in sandbox environment. public final class POTest3DSService: PO3DSService { - public init() { + public nonisolated init() { // Ignored } @@ -38,11 +38,11 @@ public final class POTest3DSService: PO3DSService { title: String(resource: .Test3DS.title), message: "", preferredStyle: .alert ) let acceptAction = UIAlertAction(title: String(resource: .Test3DS.accept), style: .default) { _ in - continuation.resume(returning: PO3DS2ChallengeResult(transactionStatus: "Y")) + 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: "N")) + continuation.resume(returning: PO3DS2ChallengeResult(transactionStatus: false)) } alertController.addAction(rejectAction) presentingViewController.present(alertController, animated: true) diff --git a/Sources/ProcessOutUI/Sources/Core/Formatters/Utils/FormattingUtils.swift b/Sources/ProcessOutUI/Sources/Core/Formatters/Utils/FormattingUtils.swift index 4e373430a..696c59c42 100644 --- a/Sources/ProcessOutUI/Sources/Core/Formatters/Utils/FormattingUtils.swift +++ b/Sources/ProcessOutUI/Sources/Core/Formatters/Utils/FormattingUtils.swift @@ -7,7 +7,7 @@ import Foundation -public enum FormattingUtils { +enum FormattingUtils { /// Returns index in formatted string that matches index in `string`. /// @@ -17,7 +17,7 @@ public enum FormattingUtils { /// 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/Modules/CardTokenization/Delegate/POCardTokenizationDelegate.swift b/Sources/ProcessOutUI/Sources/Modules/CardTokenization/Delegate/POCardTokenizationDelegate.swift index 1fc132fe7..f061ec684 100644 --- a/Sources/ProcessOutUI/Sources/Modules/CardTokenization/Delegate/POCardTokenizationDelegate.swift +++ b/Sources/ProcessOutUI/Sources/Modules/CardTokenization/Delegate/POCardTokenizationDelegate.swift @@ -12,44 +12,44 @@ public protocol POCardTokenizationDelegate: AnyObject, Sendable { /// Invoked when module emits event. @MainActor - func cardTokenizationDidEmitEvent(_ event: POCardTokenizationEvent) + 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. /// /// - NOTE: When possible please prefer throwing `POFailure` instead of other error types. - func processTokenizedCard(card: POCard) async throws + func cardTokenization(didTokenizeCard card: POCard) async throws /// Allows to choose preferred scheme that will be selected by default based on issuer information. Default /// implementation returns primary scheme. @MainActor - func preferredScheme(issuerInformation: POCardIssuerInformation) -> POCardScheme? + 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`. @MainActor - func shouldContinueTokenization(after failure: POFailure) -> Bool + func cardTokenization(shouldContinueAfter failure: POFailure) -> Bool } extension POCardTokenizationDelegate { @MainActor - public func cardTokenizationDidEmitEvent(_ event: POCardTokenizationEvent) { + public func cardTokenization(didEmitEvent event: POCardTokenizationEvent) { // Ignored } - public func processTokenizedCard(card: POCard) async throws { + public func cardTokenization(didTokenizeCard card: POCard) async throws { // Ignored } @MainActor - public func preferredScheme(issuerInformation: POCardIssuerInformation) -> POCardScheme? { + public func cardTokenization(preferredSchemeFor issuerInformation: POCardIssuerInformation) -> POCardScheme? { issuerInformation.scheme } @MainActor - public func shouldContinueTokenization(after failure: POFailure) -> Bool { + public func cardTokenization(shouldContinueAfter failure: POFailure) -> Bool { true } } diff --git a/Sources/ProcessOutUI/Sources/Modules/CardTokenization/Interactor/DefaultCardTokenizationInteractor.swift b/Sources/ProcessOutUI/Sources/Modules/CardTokenization/Interactor/DefaultCardTokenizationInteractor.swift index 131ac0106..409c86c05 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) { @@ -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), @@ -139,8 +139,8 @@ final class DefaultCardTokenizationInteractor: do { let card = try await cardsService.tokenize(request: request) logger.debug("Did tokenize card: \(String(describing: card))") - delegate?.cardTokenizationDidEmitEvent(.didTokenize(card: card)) - try await delegate?.processTokenizedCard(card: card) + delegate?.cardTokenization(didEmitEvent: .didTokenize(card: card)) + try await delegate?.cardTokenization(didTokenizeCard: card) setTokenizedState(card: card) } catch let error as POFailure { restoreStartedState(tokenizationFailure: error) @@ -185,7 +185,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)) } @@ -193,7 +193,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 } @@ -295,7 +295,7 @@ final class DefaultCardTokenizationInteractor: if !resolvePreferredScheme { startedState.preferredScheme = nil } else if let issuerInformation, let delegate = delegate { - startedState.preferredScheme = delegate.preferredScheme(issuerInformation: issuerInformation) + startedState.preferredScheme = delegate.cardTokenization(preferredSchemeFor: issuerInformation) } else { startedState.preferredScheme = issuerInformation?.scheme } diff --git a/Sources/ProcessOutUI/Sources/Modules/CardUpdate/Configuration/POCardUpdateConfiguration.swift b/Sources/ProcessOutUI/Sources/Modules/CardUpdate/Configuration/POCardUpdateConfiguration.swift index 576450318..37090eda5 100644 --- a/Sources/ProcessOutUI/Sources/Modules/CardUpdate/Configuration/POCardUpdateConfiguration.swift +++ b/Sources/ProcessOutUI/Sources/Modules/CardUpdate/Configuration/POCardUpdateConfiguration.swift @@ -13,7 +13,7 @@ public struct POCardUpdateConfiguration: Sendable { 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 d2968aeec..182750f88 100644 --- a/Sources/ProcessOutUI/Sources/Modules/CardUpdate/Delegate/POCardUpdateDelegate.swift +++ b/Sources/ProcessOutUI/Sources/Modules/CardUpdate/Delegate/POCardUpdateDelegate.swift @@ -11,31 +11,31 @@ import ProcessOut 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 cardUpdateDidEmitEvent(_ event: POCardUpdateEvent) + func cardUpdate(didEmitEvent event: POCardUpdateEvent) /// Asks delegate whether user should be allowed to continue after failure or module should complete. /// Default implementation returns `true`. @MainActor - func shouldContinueUpdate(after failure: POFailure) -> Bool + func cardUpdate(shouldContinueAfter failure: POFailure) -> Bool } extension POCardUpdateDelegate { - public func cardInformation(cardId: String) async -> POCardUpdateInformation? { + public func cardUpdate(informationFor cardId: String) async -> POCardUpdateInformation? { nil } @MainActor - public func cardUpdateDidEmitEvent(_ event: POCardUpdateEvent) { + public func cardUpdate(didEmitEvent event: POCardUpdateEvent) { // Ignored } @MainActor - public func shouldContinueUpdate(after failure: POFailure) -> Bool { + public func cardUpdate(shouldContinueAfter failure: POFailure) -> Bool { true } } diff --git a/Sources/ProcessOutUI/Sources/Modules/CardUpdate/Interactor/DefaultCardUpdateInteractor.swift b/Sources/ProcessOutUI/Sources/Modules/CardUpdate/Interactor/DefaultCardUpdateInteractor.swift index 99beb2c9e..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? { + func cardTokenization(preferredSchemeFor issuerInformation: POCardIssuerInformation) -> POCardScheme? { delegate?.dynamicCheckout(preferredSchemeFor: issuerInformation) } - func shouldContinueTokenization(after failure: POFailure) -> Bool { + func cardTokenization(shouldContinueAfter failure: POFailure) -> Bool { canRecoverCardTokenization(from: failure) } } @@ -779,7 +779,7 @@ extension DynamicCheckoutDefaultInteractor: POCardTokenizationDelegate { @available(iOS 14.0, *) extension DynamicCheckoutDefaultInteractor: PONativeAlternativePaymentDelegate { - func nativeAlternativePaymentDidEmitEvent(_ event: PONativeAlternativePaymentEvent) { + func nativeAlternativePayment(didEmitEvent event: PONativeAlternativePaymentEvent) { switch event { case .didSubmitParameters: invalidateInvoiceIfPossible() @@ -789,14 +789,10 @@ extension DynamicCheckoutDefaultInteractor: PONativeAlternativePaymentDelegate { delegate?.dynamicCheckout(didEmitAlternativePaymentEvent: event) } - func nativeAlternativePaymentDefaultValues( - for parameters: [PONativeAlternativePaymentMethodParameter], - completion: @escaping @Sendable ([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/NativeAlternativePayment/Configuration/PONativeAlternativePaymentConfiguration.swift b/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/Configuration/PONativeAlternativePaymentConfiguration.swift index e663adb89..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,7 +107,7 @@ public struct PONativeAlternativePaymentConfiguration { public let skipSuccessScreen: Bool /// Payment confirmation configuration. - public let paymentConfirmation: PONativeAlternativePaymentConfirmationConfiguration + public let paymentConfirmation: PaymentConfirmation /// Creates configuration instance. public init( @@ -68,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 39c11fe10..8e804e4a4 100644 --- a/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/Delegate/PONativeAlternativePaymentDelegate.swift +++ b/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/Delegate/PONativeAlternativePaymentDelegate.swift @@ -12,15 +12,27 @@ public protocol PONativeAlternativePaymentDelegate: AnyObject, Sendable { /// Invoked when module emits event. @MainActor - func nativeAlternativePaymentDidEmitEvent(_ event: PONativeAlternativePaymentEvent) + 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 - func nativeAlternativePaymentDefaultValues( - for parameters: [PONativeAlternativePaymentMethodParameter], - completion: @escaping @Sendable ([String: String]) -> Void - ) + public func nativeAlternativePayment(didEmitEvent event: PONativeAlternativePaymentEvent) { + // Ignored + } + + public func nativeAlternativePayment( + defaultsFor parameters: [PONativeAlternativePaymentMethodParameter] + ) async -> [String: String] { + [:] + } } diff --git a/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/Interactor/NativeAlternativePaymentDefaultInteractor.swift b/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/Interactor/NativeAlternativePaymentDefaultInteractor.swift index 52f387ac4..745bd65dd 100644 --- a/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/Interactor/NativeAlternativePaymentDefaultInteractor.swift +++ b/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/Interactor/NativeAlternativePaymentDefaultInteractor.swift @@ -142,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) @@ -220,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)) @@ -365,7 +365,7 @@ final class NativeAlternativePaymentDefaultInteractor: // MARK: - Cancellation Availability 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 @@ -386,7 +386,7 @@ final class NativeAlternativePaymentDefaultInteractor: } 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 @@ -406,7 +406,7 @@ final class NativeAlternativePaymentDefaultInteractor: @MainActor private func send(event: PONativeAlternativePaymentEvent) { logger.debug("Did send event: '\(event)'") - delegate?.nativeAlternativePaymentDidEmitEvent(event) + delegate?.nativeAlternativePayment(didEmitEvent: event) } private func didUpdate(parameter: NativeAlternativePaymentInteractorState.Parameter, to value: String) { @@ -481,13 +481,6 @@ 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. @@ -497,16 +490,9 @@ final class NativeAlternativePaymentDefaultInteractor: guard !parameters.isEmpty else { return } - let defaultValues = await withCheckedContinuation { continuation in - if let delegate { - delegate.nativeAlternativePaymentDefaultValues( - for: parameters.map(\.specification), - completion: { continuation.resume(returning: $0) } - ) - } 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] { diff --git a/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/ViewModel/DefaultNativeAlternativePaymentViewModel.swift b/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/ViewModel/DefaultNativeAlternativePaymentViewModel.swift index d826d10fe..1d74d904d 100644 --- a/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/ViewModel/DefaultNativeAlternativePaymentViewModel.swift +++ b/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/ViewModel/DefaultNativeAlternativePaymentViewModel.swift @@ -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/Tests/ProcessOutTests/Sources/Integration/CardsServiceTests.swift b/Tests/ProcessOutTests/Sources/Integration/CardsServiceTests.swift index 83f7e1918..76d6d4db0 100644 --- a/Tests/ProcessOutTests/Sources/Integration/CardsServiceTests.swift +++ b/Tests/ProcessOutTests/Sources/Integration/CardsServiceTests.swift @@ -11,10 +11,9 @@ import XCTest final class CardsServiceTests: XCTestCase { - @MainActor - 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 } diff --git a/Tests/ProcessOutTests/Sources/Integration/CustomerTokensServiceTests.swift b/Tests/ProcessOutTests/Sources/Integration/CustomerTokensServiceTests.swift index 549c884a2..5fffb13c1 100644 --- a/Tests/ProcessOutTests/Sources/Integration/CustomerTokensServiceTests.swift +++ b/Tests/ProcessOutTests/Sources/Integration/CustomerTokensServiceTests.swift @@ -10,13 +10,12 @@ import XCTest final class CustomerTokensServiceTests: XCTestCase { - @MainActor - 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 } diff --git a/Tests/ProcessOutTests/Sources/Integration/GatewayConfigurationsRepositoryTests.swift b/Tests/ProcessOutTests/Sources/Integration/GatewayConfigurationsRepositoryTests.swift index d257f268c..8f994c1d1 100644 --- a/Tests/ProcessOutTests/Sources/Integration/GatewayConfigurationsRepositoryTests.swift +++ b/Tests/ProcessOutTests/Sources/Integration/GatewayConfigurationsRepositoryTests.swift @@ -11,10 +11,9 @@ import XCTest final class GatewayConfigurationsRepositoryTests: XCTestCase { - @MainActor - 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/Unit/Service/3DS/DefaultThreeDSServiceTests.swift b/Tests/ProcessOutTests/Sources/Unit/Service/3DS/DefaultThreeDSServiceTests.swift index 5ccb8daf3..7c3e18297 100644 --- a/Tests/ProcessOutTests/Sources/Unit/Service/3DS/DefaultThreeDSServiceTests.swift +++ b/Tests/ProcessOutTests/Sources/Unit/Service/3DS/DefaultThreeDSServiceTests.swift @@ -162,7 +162,7 @@ final class DefaultThreeDSServiceTests: XCTestCase { // Then XCTAssertEqual(challenge, expectedChallenge) isDelegateCalled = true - return .init(transactionStatus: "Y") + return .init(transactionStatus: true) } // When @@ -190,7 +190,7 @@ final class DefaultThreeDSServiceTests: XCTestCase { func test_handle_whenDelegateDoChallengeCompletesWithTrue_succeeds() async throws { // Given delegate.performChallengeFromClosure = { _ in - .init(transactionStatus: "Y") + .init(transactionStatus: true) } // When @@ -203,7 +203,7 @@ final class DefaultThreeDSServiceTests: XCTestCase { func test_handle_whenDelegateDoChallengeCompletesWithFalse_succeeds() async throws { // Given delegate.performChallengeFromClosure = { _ in - .init(transactionStatus: "N") + .init(transactionStatus: false) } // When diff --git a/Tests/ProcessOutUITests/Sources/Integration/CardUpdate/DefaultCardUpdateInteractorTests.swift b/Tests/ProcessOutUITests/Sources/Integration/CardUpdate/DefaultCardUpdateInteractorTests.swift index d0b225ce4..76efaff82 100644 --- a/Tests/ProcessOutUITests/Sources/Integration/CardUpdate/DefaultCardUpdateInteractorTests.swift +++ b/Tests/ProcessOutUITests/Sources/Integration/CardUpdate/DefaultCardUpdateInteractorTests.swift @@ -11,10 +11,9 @@ import XCTest final class DefaultCardUpdateInteractorTests: XCTestCase { - @MainActor - 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 } diff --git a/Tests/ProcessOutUITests/Sources/Mocks/CardUpdate/CardUpdateDelegateMock.swift b/Tests/ProcessOutUITests/Sources/Mocks/CardUpdate/CardUpdateDelegateMock.swift index 13e18362b..3e7c96978 100644 --- a/Tests/ProcessOutUITests/Sources/Mocks/CardUpdate/CardUpdateDelegateMock.swift +++ b/Tests/ProcessOutUITests/Sources/Mocks/CardUpdate/CardUpdateDelegateMock.swift @@ -25,17 +25,17 @@ final class CardUpdateDelegateMock: POCardUpdateDelegate, Sendable { 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 } From fa7a0880b81fd727f7cfd25b8cfbb3d213cfd4dc Mon Sep 17 00:00:00 2001 From: Andrii Vysotskyi Date: Tue, 10 Sep 2024 12:14:56 +0200 Subject: [PATCH 10/10] Update Xcode version --- .github/actions/select-xcode/action.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/actions/select-xcode/action.yml b/.github/actions/select-xcode/action.yml index 2d6c57687..544dddb3b 100644 --- a/.github/actions/select-xcode/action.yml +++ b/.github/actions/select-xcode/action.yml @@ -5,5 +5,5 @@ runs: steps: - name: Select Xcode Version # todo(andrii-vysotskyi): Migrate to public release once available - run: sudo xcode-select -s '/Applications/Xcode_16_beta_5.app/Contents/Developer' + run: sudo xcode-select -s '/Applications/Xcode_16_beta_6.app/Contents/Developer' shell: bash