From 2e28aa5f827d7f2a35f798290a6d965d645a9bdc Mon Sep 17 00:00:00 2001 From: Andrii Vysotskyi Date: Fri, 20 Sep 2024 12:45:40 +0200 Subject: [PATCH 01/11] Add capture payment confirm button configuration --- ...NativeAlternativePaymentConfirmation.swift | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/Configuration/PONativeAlternativePaymentConfirmation.swift b/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/Configuration/PONativeAlternativePaymentConfirmation.swift index 22df337db..462bb13f3 100644 --- a/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/Configuration/PONativeAlternativePaymentConfirmation.swift +++ b/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/Configuration/PONativeAlternativePaymentConfirmation.swift @@ -10,6 +10,18 @@ import Foundation /// Configuration specific to native APM payment confirmation. public struct PONativeAlternativePaymentConfirmationConfiguration { // swiftlint:disable:this type_name + /// Confirmation button configuration. + public struct ConfirmButton { + + /// Button title. + public let title: String? + + /// Creates button instance. + public init(title: String? = nil) { + self.title = title + } + } + /// 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 @@ -26,6 +38,13 @@ public struct PONativeAlternativePaymentConfirmationConfiguration { // swiftlint /// Default value is `false`. public let hideGatewayDetails: Bool + /// Payment confirmation button configuration. + /// + /// Displays a confirmation button when the user needs to perform an external customer action (e.g., + /// completing a step with a third-party service) before proceeding with payment capture. The user + /// must press this button to continue. + public let confirmButton: ConfirmButton? + /// 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? @@ -36,12 +55,14 @@ public struct PONativeAlternativePaymentConfirmationConfiguration { // swiftlint timeout: TimeInterval = 180, showProgressIndicatorAfter: TimeInterval? = nil, hideGatewayDetails: Bool = false, + confirmButton: ConfirmButton? = nil, secondaryAction: PONativeAlternativePaymentConfiguration.SecondaryAction? = nil ) { self.waitsConfirmation = waitsConfirmation self.timeout = timeout self.showProgressIndicatorAfter = showProgressIndicatorAfter self.hideGatewayDetails = hideGatewayDetails + self.confirmButton = confirmButton self.secondaryAction = secondaryAction } } From 612f65d3c85da240b1de86029998ce7f20280b3f Mon Sep 17 00:00:00 2001 From: Andrii Vysotskyi Date: Fri, 20 Sep 2024 13:10:36 +0200 Subject: [PATCH 02/11] Update interactor interface --- .../NativeAlternativePaymentInteractor.swift | 5 ++ ...iveAlternativePaymentInteractorState.swift | 84 +++++++++++-------- 2 files changed, 54 insertions(+), 35 deletions(-) diff --git a/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/Interactor/NativeAlternativePaymentInteractor.swift b/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/Interactor/NativeAlternativePaymentInteractor.swift index 833e28571..e6d22754d 100644 --- a/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/Interactor/NativeAlternativePaymentInteractor.swift +++ b/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/Interactor/NativeAlternativePaymentInteractor.swift @@ -19,6 +19,11 @@ protocol NativeAlternativePaymentInteractor: Interactor Date: Fri, 20 Sep 2024 14:00:34 +0200 Subject: [PATCH 03/11] Handle capture confirmation --- ...eAlternativePaymentDefaultInteractor.swift | 84 ++++++++++++------- 1 file changed, 52 insertions(+), 32 deletions(-) diff --git a/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/Interactor/NativeAlternativePaymentDefaultInteractor.swift b/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/Interactor/NativeAlternativePaymentDefaultInteractor.swift index 3f5e88913..a74239c49 100644 --- a/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/Interactor/NativeAlternativePaymentDefaultInteractor.swift +++ b/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/Interactor/NativeAlternativePaymentDefaultInteractor.swift @@ -86,13 +86,18 @@ final class NativeAlternativePaymentDefaultInteractor: } } + func confirmCapture() { + confirmCapture(force: false) + } + override func cancel() { + // todo(andrii-vysotskyi): fix cancellation handling logger.debug("Will attempt to cancel payment.") switch state { case .started(let state) where state.isCancellable: setFailureStateUnchecked(error: POFailure(code: .cancelled)) case .awaitingCapture(let state) where state.isCancellable: - captureCancellable?.cancel() + state.cancellable?.cancel() default: logger.debug("Ignored cancellation attempt from unsupported state: \(state)") } @@ -117,8 +122,6 @@ final class NativeAlternativePaymentDefaultInteractor: private let logger: POLogger private let completion: (Result) -> Void - private var captureCancellable: AnyCancellable? - // MARK: - Starting State @MainActor @@ -140,8 +143,7 @@ final class NativeAlternativePaymentDefaultInteractor: } let startedState = State.Started( gateway: details.gateway, - amount: details.invoice.amount, - currencyCode: details.invoice.currencyCode, + invoice: details.invoice, parameters: await createParameters(specifications: details.parameters), isCancellable: disableDuration(of: configuration.secondaryAction).isZero ) @@ -213,36 +215,55 @@ final class NativeAlternativePaymentDefaultInteractor: setSubmittedUnchecked() return } - let actionMessage = parameterValues?.customerActionMessage ?? gateway.customerActionMessage - send(event: .willWaitForCaptureConfirmation(additionalActionExpected: actionMessage != nil)) + let customerActionMessage = parameterValues?.customerActionMessage ?? gateway.customerActionMessage + let additionalActionExpected = customerActionMessage != nil + send(event: .willWaitForCaptureConfirmation(additionalActionExpected: additionalActionExpected)) let (logoImage, actionImage) = await imagesRepository.images( at: logoUrl(gateway: gateway, parameterValues: parameterValues), gateway.customerActionImageUrl ) + let shouldConfirmCapture = additionalActionExpected && configuration.paymentConfirmation.confirmButton != nil let awaitingCaptureState = State.AwaitingCapture( - paymentProviderName: parameterValues?.providerName, - logoImage: logoImage, - actionMessage: actionMessage, - actionImage: actionImage, + paymentProvider: .init(name: parameterValues?.providerName, image: logoImage), + customerAction: customerActionMessage.map { message in + .init(message: message, image: actionImage) + }, isCancellable: disableDuration(of: configuration.paymentConfirmation.secondaryAction).isZero, - isDelayed: false + isDelayed: false, + shouldConfirmCapture: shouldConfirmCapture ) setStateUnchecked(.awaitingCapture(awaitingCaptureState)) - logger.info("Waiting for invoice capture confirmation") + if !shouldConfirmCapture { + confirmCapture(force: true) + } + enableCaptureCancellationAfterDelay() + } + + private func confirmCapture(force: Bool) { + guard case .awaitingCapture(var currentState) = state else { + logger.debug("Ignoring attempt to confirm capture from unsupported state: \(state).") + return + } + guard (currentState.shouldConfirmCapture || force) && currentState.cancellable == nil else { + logger.debug("Payment is already being captured, ignored.") + return + } let request = PONativeAlternativePaymentCaptureRequest( invoiceId: configuration.invoiceId, gatewayConfigurationId: configuration.gatewayConfigurationId, timeout: configuration.paymentConfirmation.timeout ) - let task = Task { + let task = Task { @MainActor in do { try await invoicesService.captureNativeAlternativePayment(request: request) - await setCapturedStateUnchecked(gateway: gateway, parameterValues: parameterValues) + await setCapturedStateUnchecked(paymentProvider: currentState.paymentProvider) } catch { setFailureStateUnchecked(error: error) } } - captureCancellable = AnyCancellable(task.cancel) - enableCaptureCancellationAfterDelay() + currentState.cancellable = AnyCancellable(task.cancel) + currentState.shouldConfirmCapture = false + self.state = .awaitingCapture(currentState) + logger.info("Waiting for invoice capture confirmation") schedulePaymentConfirmationDelay() } @@ -265,6 +286,17 @@ final class NativeAlternativePaymentDefaultInteractor: private func setCapturedStateUnchecked( gateway: PONativeAlternativePaymentMethodTransactionDetails.Gateway, parameterValues: PONativeAlternativePaymentMethodParameterValues? + ) async { + let logoImage = await imagesRepository.image( + at: logoUrl(gateway: gateway, parameterValues: parameterValues) + ) + let paymentProvider = State.PaymentProvider(name: parameterValues?.providerName, image: logoImage) + await setCapturedStateUnchecked(paymentProvider: paymentProvider) + } + + @MainActor + private func setCapturedStateUnchecked( + paymentProvider: NativeAlternativePaymentInteractorState.PaymentProvider ) async { logger.info("Did receive invoice capture confirmation") guard configuration.paymentConfirmation.waitsConfirmation else { @@ -272,17 +304,7 @@ final class NativeAlternativePaymentDefaultInteractor: setSubmittedUnchecked() return } - let capturedState: State.Captured - if case .awaitingCapture(let awaitingCaptureState) = state { - capturedState = State.Captured( - paymentProviderName: awaitingCaptureState.paymentProviderName, logoImage: awaitingCaptureState.logoImage - ) - } else { - let logoImage = await imagesRepository.image( - at: logoUrl(gateway: gateway, parameterValues: parameterValues) - ) - capturedState = State.Captured(paymentProviderName: parameterValues?.providerName, logoImage: logoImage) - } + let capturedState = State.Captured(paymentProvider: paymentProvider) setStateUnchecked(.captured(capturedState)) send(event: .didCompletePayment) if !configuration.skipSuccessScreen { @@ -370,14 +392,13 @@ final class NativeAlternativePaymentDefaultInteractor: // MARK: - Cancellation Availability - @MainActor private func enableCancellationAfterDelay() { let disabledFor = disableDuration(of: configuration.secondaryAction) guard disabledFor > 0 else { logger.debug("Cancel action is not set or initially enabled.") return } - Task { + Task { @MainActor in try? await Task.sleep(seconds: disabledFor) switch state { case .started(var state): @@ -392,14 +413,13 @@ final class NativeAlternativePaymentDefaultInteractor: } } - @MainActor private func enableCaptureCancellationAfterDelay() { let disabledFor = disableDuration(of: configuration.paymentConfirmation.secondaryAction) guard disabledFor > 0 else { logger.debug("Confirmation cancel action is not set or initially enabled.") return } - Task { + Task { @MainActor in try? await Task.sleep(seconds: disabledFor) guard case .awaitingCapture(var awaitingState) = state else { return From d0a06f2293cafd136e66caed8b71ac1d3bcb1183 Mon Sep 17 00:00:00 2001 From: Andrii Vysotskyi Date: Fri, 20 Sep 2024 14:04:13 +0200 Subject: [PATCH 04/11] Update UI --- .../Resources/Localizable.xcstrings | 3 ++ ...ingResource+NativeAlternativePayment.swift | 5 ++ ...ultNativeAlternativePaymentViewModel.swift | 46 ++++++++++++++----- 3 files changed, 43 insertions(+), 11 deletions(-) diff --git a/Sources/ProcessOutUI/Resources/Localizable.xcstrings b/Sources/ProcessOutUI/Resources/Localizable.xcstrings index 476012b47..13142cfcc 100644 --- a/Sources/ProcessOutUI/Resources/Localizable.xcstrings +++ b/Sources/ProcessOutUI/Resources/Localizable.xcstrings @@ -1812,6 +1812,9 @@ } } } + }, + "native-alternative-payment.confirm-capture-button.title" : { + }, "native-alternative-payment.email.placeholder" : { "localizations" : { diff --git a/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/Symbols/StringResource+NativeAlternativePayment.swift b/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/Symbols/StringResource+NativeAlternativePayment.swift index 1bf41c1f5..de76dbab3 100644 --- a/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/Symbols/StringResource+NativeAlternativePayment.swift +++ b/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/Symbols/StringResource+NativeAlternativePayment.swift @@ -33,6 +33,11 @@ extension POStringResource { /// Pay %@ static let submitAmount = POStringResource("native-alternative-payment.submit-button.title", comment: "") + /// Capture confirmation. + static let confirmCapture = POStringResource( + "native-alternative-payment.confirm-capture-button.title", comment: "" + ) + /// Cancel button title. static let cancel = POStringResource("native-alternative-payment.cancel-button.title", comment: "") } diff --git a/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/ViewModel/DefaultNativeAlternativePaymentViewModel.swift b/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/ViewModel/DefaultNativeAlternativePaymentViewModel.swift index b7c891a77..8b9cb5bd1 100644 --- a/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/ViewModel/DefaultNativeAlternativePaymentViewModel.swift +++ b/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/ViewModel/DefaultNativeAlternativePaymentViewModel.swift @@ -191,19 +191,20 @@ final class DefaultNativeAlternativePaymentViewModel: ViewModel { private func createSections(state: InteractorState.AwaitingCapture) -> [NativeAlternativePaymentViewModelSection] { let item: NativeAlternativePaymentViewModelItem - if let expectedActionMessage = state.actionMessage { + if let customerAction = state.customerAction { let submittedItem = NativeAlternativePaymentViewModelItem.Submitted( id: "awaiting-capture", - title: state.logoImage == nil ? state.paymentProviderName : nil, - logoImage: state.logoImage, - message: expectedActionMessage, - isMessageCompact: expectedActionMessage.count <= Constants.maximumCompactMessageLength, - image: state.actionImage, + title: state.paymentProvider.image == nil ? state.paymentProvider.name : nil, + logoImage: state.paymentProvider.image, + message: customerAction.message, + isMessageCompact: customerAction.message.count <= Constants.maximumCompactMessageLength, + image: customerAction.image, isCaptured: false, isProgressViewHidden: !state.isDelayed ) item = .submitted(submittedItem) } else { + // todo(andrii-vysotskyi): set additional text saying that payment is being processed. item = .progress } let section = NativeAlternativePaymentViewModelSection( @@ -214,13 +215,36 @@ final class DefaultNativeAlternativePaymentViewModel: ViewModel { private func createActions(state: InteractorState.AwaitingCapture) -> [POActionsContainerActionViewModel] { let actions = [ + createConfirmPaymentCaptureAction(state: state), cancelAction( - configuration: configuration.paymentConfirmation.secondaryAction, isEnabled: state.isCancellable + configuration: configuration.paymentConfirmation.secondaryAction, + isEnabled: state.isCancellable ) ] return actions.compactMap { $0 } } + private func createConfirmPaymentCaptureAction( + state: InteractorState.AwaitingCapture + ) -> POActionsContainerActionViewModel? { + guard state.shouldConfirmCapture else { + return nil + } + let buttonTitle = interactor.configuration.paymentConfirmation.confirmButton?.title + ?? String(resource: .NativeAlternativePayment.Button.confirmCapture) + let action = POActionsContainerActionViewModel( + id: "native-alternative-payment.primary-button", + title: buttonTitle, + isEnabled: true, + isLoading: false, + isPrimary: true, + action: { [weak self] in + self?.interactor.confirmCapture() + } + ) + return action + } + // MARK: - Captured State private func update(with state: InteractorState.Captured) { @@ -237,8 +261,8 @@ final class DefaultNativeAlternativePaymentViewModel: ViewModel { private func createSections(state: InteractorState.Captured) -> [NativeAlternativePaymentViewModelSection] { let item = NativeAlternativePaymentViewModelItem.Submitted( id: "captured", - title: state.logoImage == nil ? state.paymentProviderName : nil, - logoImage: state.logoImage, + title: state.paymentProvider.image == nil ? state.paymentProvider.name : nil, + logoImage: state.paymentProvider.image, message: configuration.successMessage ?? String(resource: .NativeAlternativePayment.Success.message), isMessageCompact: true, image: UIImage(poResource: .success).withRenderingMode(.alwaysTemplate), @@ -368,9 +392,9 @@ final class DefaultNativeAlternativePaymentViewModel: ViewModel { if let customTitle = configuration.primaryActionTitle { title = customTitle } else { - priceFormatter.currencyCode = state.currencyCode + priceFormatter.currencyCode = state.invoice.currencyCode // swiftlint:disable:next legacy_objc_type - if let formattedAmount = priceFormatter.string(from: state.amount as NSDecimalNumber) { + if let formattedAmount = priceFormatter.string(from: state.invoice.amount as NSDecimalNumber) { title = String(resource: .NativeAlternativePayment.Button.submitAmount, replacements: formattedAmount) } else { title = String(resource: .NativeAlternativePayment.Button.submit) From 03f00ad53984aa2f8c6595b461dc6a69816715b6 Mon Sep 17 00:00:00 2001 From: Andrii Vysotskyi Date: Fri, 20 Sep 2024 14:58:03 +0200 Subject: [PATCH 05/11] Update localizations --- .../Resources/Localizable.xcstrings | 33 ++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/Sources/ProcessOutUI/Resources/Localizable.xcstrings b/Sources/ProcessOutUI/Resources/Localizable.xcstrings index 13142cfcc..ec1309af7 100644 --- a/Sources/ProcessOutUI/Resources/Localizable.xcstrings +++ b/Sources/ProcessOutUI/Resources/Localizable.xcstrings @@ -1814,7 +1814,38 @@ } }, "native-alternative-payment.confirm-capture-button.title" : { - + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "لقد دفعت" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "I’ve paid" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "J'ai payé" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Płatność wykonana" + } + }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Já paguei" + } + } + } }, "native-alternative-payment.email.placeholder" : { "localizations" : { From fbf9235b62a8276c1535a53da2590636d61e647c Mon Sep 17 00:00:00 2001 From: Andrii Vysotskyi Date: Fri, 20 Sep 2024 16:26:20 +0200 Subject: [PATCH 06/11] Support cancelling unconfirmed state --- .../NativeAlternativePaymentDefaultInteractor.swift | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/Interactor/NativeAlternativePaymentDefaultInteractor.swift b/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/Interactor/NativeAlternativePaymentDefaultInteractor.swift index a74239c49..3aae0fada 100644 --- a/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/Interactor/NativeAlternativePaymentDefaultInteractor.swift +++ b/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/Interactor/NativeAlternativePaymentDefaultInteractor.swift @@ -91,13 +91,17 @@ final class NativeAlternativePaymentDefaultInteractor: } override func cancel() { - // todo(andrii-vysotskyi): fix cancellation handling + // todo(andrii-vysotskyi): allow cancellation in all states except sink logger.debug("Will attempt to cancel payment.") switch state { case .started(let state) where state.isCancellable: setFailureStateUnchecked(error: POFailure(code: .cancelled)) case .awaitingCapture(let state) where state.isCancellable: - state.cancellable?.cancel() + if let cancellable = state.cancellable { + cancellable.cancel() + } else { + setFailureStateUnchecked(error: POFailure(code: .cancelled)) + } default: logger.debug("Ignored cancellation attempt from unsupported state: \(state)") } From 089b983d626beec03b9f2f80a72f17b43915d13a Mon Sep 17 00:00:00 2001 From: Andrii Vysotskyi Date: Fri, 20 Sep 2024 17:15:08 +0200 Subject: [PATCH 07/11] Fix button animation --- .../Button/Regular/POButtonStyle.swift | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/Sources/ProcessOutCoreUI/Sources/DesignSystem/Button/Regular/POButtonStyle.swift b/Sources/ProcessOutCoreUI/Sources/DesignSystem/Button/Regular/POButtonStyle.swift index f5fb0483a..f62ba6f1e 100644 --- a/Sources/ProcessOutCoreUI/Sources/DesignSystem/Button/Regular/POButtonStyle.swift +++ b/Sources/ProcessOutCoreUI/Sources/DesignSystem/Button/Regular/POButtonStyle.swift @@ -43,20 +43,21 @@ public struct POButtonStyle: ButtonStyle { isEnabled: isEnabled, isLoading: isLoading, isPressed: configuration.isPressed ) ZStack { - ProgressView() - .progressViewStyle(progressStyle) - .opacity(isLoading ? 1 : 0) - configuration.label - .textStyle(currentStyle.title) - .lineLimit(1) - .opacity(isLoading ? 0 : 1) + if isLoading { + ProgressView() + .progressViewStyle(progressStyle) + } else { + configuration.label + .textStyle(currentStyle.title) + .lineLimit(1) + } } .padding(Constants.padding) .frame(maxWidth: .infinity, minHeight: Constants.minHeight) .background(currentStyle.backgroundColor) .border(style: currentStyle.border) .shadow(style: currentStyle.shadow) - .contentShape(.rect) + .contentShape(.standardHittableRect) .animation(.default, value: isLoading) .animation(.default, value: isEnabled) .allowsHitTesting(isEnabled && !isLoading) From a599cf7b31bc614cfa3666e1b59600d98960a96b Mon Sep 17 00:00:00 2001 From: Andrii Vysotskyi Date: Fri, 20 Sep 2024 17:25:33 +0200 Subject: [PATCH 08/11] Show payment capture confirmation button in example --- .../ViewModel/AlternativePaymentsViewModel.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Example/Example/Sources/UI/Modules/AlternativePayments/ViewModel/AlternativePaymentsViewModel.swift b/Example/Example/Sources/UI/Modules/AlternativePayments/ViewModel/AlternativePaymentsViewModel.swift index a5345d6ea..bfffbb9fc 100644 --- a/Example/Example/Sources/UI/Modules/AlternativePayments/ViewModel/AlternativePaymentsViewModel.swift +++ b/Example/Example/Sources/UI/Modules/AlternativePayments/ViewModel/AlternativePaymentsViewModel.swift @@ -168,6 +168,7 @@ final class AlternativePaymentsViewModel: ObservableObject { secondaryAction: .cancel(), paymentConfirmation: .init( showProgressIndicatorAfter: 5, + confirmButton: .init(), secondaryAction: .cancel(disabledFor: 10) ) ) From 7f764af3dd6013a72db96d7b51046ca5d89e6964 Mon Sep 17 00:00:00 2001 From: Andrii Vysotskyi Date: Mon, 23 Sep 2024 10:35:50 +0200 Subject: [PATCH 09/11] Use recent 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 96ede897f..d232ef4bb 100644 --- a/.github/actions/select-xcode/action.yml +++ b/.github/actions/select-xcode/action.yml @@ -4,5 +4,5 @@ runs: using: "composite" steps: - name: Select Xcode Version - run: sudo xcode-select -s '/Applications/Xcode_15.4.app/Contents/Developer' + run: sudo xcode-select -s '/Applications/Xcode_16.app/Contents/Developer' shell: bash From 5860e94b36b29f1cb006fa1f4126bd575a469ab6 Mon Sep 17 00:00:00 2001 From: Andrii Vysotskyi Date: Mon, 23 Sep 2024 11:09:30 +0200 Subject: [PATCH 10/11] Disable Checkout3DS testing --- Scripts/Test.sh | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Scripts/Test.sh b/Scripts/Test.sh index dc5d50b12..a6dc7a386 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): re-enable Checkout3DS wrapper testing +# ProcessOutCheckout3DS is not compatible with Xcode 16 + # Run Tests -for PRODUCT in "ProcessOut" "ProcessOutUI" "ProcessOutCheckout3DS"; do +for PRODUCT in "ProcessOut" "ProcessOutUI"; do xcodebuild clean test \ -destination "$DESTINATION" \ -project $PROJECT \ From 84801e032a801ff808b43207fca31c75798d2f99 Mon Sep 17 00:00:00 2001 From: Andrii Vysotskyi Date: Mon, 23 Sep 2024 12:16:19 +0200 Subject: [PATCH 11/11] Add payment confirmation event --- .../Models/PONativeAlternativePaymentMethodEvent.swift | 5 +++++ .../NativeAlternativePaymentDefaultInteractor.swift | 3 +++ 2 files changed, 8 insertions(+) diff --git a/Sources/ProcessOut/Sources/UI/Modules/NativeAlternativePaymentMethod/Models/PONativeAlternativePaymentMethodEvent.swift b/Sources/ProcessOut/Sources/UI/Modules/NativeAlternativePaymentMethod/Models/PONativeAlternativePaymentMethodEvent.swift index e2ec3a1cf..bb9d53927 100644 --- a/Sources/ProcessOut/Sources/UI/Modules/NativeAlternativePaymentMethod/Models/PONativeAlternativePaymentMethodEvent.swift +++ b/Sources/ProcessOut/Sources/UI/Modules/NativeAlternativePaymentMethod/Models/PONativeAlternativePaymentMethodEvent.swift @@ -75,6 +75,11 @@ public enum PONativeAlternativePaymentMethodEvent { /// to make capture happen. case willWaitForCaptureConfirmation(additionalActionExpected: Bool) + /// This event is triggered during the capture stage when the user confirms that they have completed + /// any required external action (if applicable). Once the event is triggered, the implementation + /// proceeds with the actual capture process. + case didConfirmPayment + /// Event is sent after payment was confirmed to be captured. This is a final event. case didCompletePayment diff --git a/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/Interactor/NativeAlternativePaymentDefaultInteractor.swift b/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/Interactor/NativeAlternativePaymentDefaultInteractor.swift index 3aae0fada..98da642c2 100644 --- a/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/Interactor/NativeAlternativePaymentDefaultInteractor.swift +++ b/Sources/ProcessOutUI/Sources/Modules/NativeAlternativePayment/Interactor/NativeAlternativePaymentDefaultInteractor.swift @@ -251,6 +251,9 @@ final class NativeAlternativePaymentDefaultInteractor: logger.debug("Payment is already being captured, ignored.") return } + if !force { + delegate?.nativeAlternativePaymentMethodDidEmitEvent(.didConfirmPayment) + } let request = PONativeAlternativePaymentCaptureRequest( invoiceId: configuration.invoiceId, gatewayConfigurationId: configuration.gatewayConfigurationId,