From 999bbc97e5ac5e199404ac84e329569a73877fba Mon Sep 17 00:00:00 2001 From: Vitalii Vanziak Date: Tue, 12 May 2026 14:16:21 +0300 Subject: [PATCH 1/6] KDoc --- .../sdk/ui/napm/PONativeAlternativePaymentConfiguration.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ui/src/main/kotlin/com/processout/sdk/ui/napm/PONativeAlternativePaymentConfiguration.kt b/ui/src/main/kotlin/com/processout/sdk/ui/napm/PONativeAlternativePaymentConfiguration.kt index 91931d1c..aae7648c 100644 --- a/ui/src/main/kotlin/com/processout/sdk/ui/napm/PONativeAlternativePaymentConfiguration.kt +++ b/ui/src/main/kotlin/com/processout/sdk/ui/napm/PONativeAlternativePaymentConfiguration.kt @@ -63,6 +63,7 @@ data class PONativeAlternativePaymentConfiguration( * @param[invoiceId] Invoice identifier. * @param[gatewayConfigurationId] Gateway configuration identifier. * @param[customerTokenId] Optional customer token identifier that will be used for authorization. + * @param[configuration] Authorization configuration. */ @Parcelize data class Authorization( @@ -78,6 +79,7 @@ data class PONativeAlternativePaymentConfiguration( * @param[customerId] Customer identifier. * @param[customerTokenId] Customer token identifier. * @param[gatewayConfigurationId] Gateway configuration identifier. + * @param[configuration] Tokenization configuration. */ @Parcelize data class Tokenization( From a6a729dcde442f43d8bceea6c63d68848eab20c1 Mon Sep 17 00:00:00 2001 From: Vitalii Vanziak Date: Wed, 13 May 2026 17:36:56 +0300 Subject: [PATCH 2/6] Added 'continueButton' configuration and auto redirect logic in interactor --- .../NativeAlternativePaymentInteractor.kt | 151 +++++++++--------- ...PONativeAlternativePaymentConfiguration.kt | 4 +- 2 files changed, 80 insertions(+), 75 deletions(-) diff --git a/ui/src/main/kotlin/com/processout/sdk/ui/napm/NativeAlternativePaymentInteractor.kt b/ui/src/main/kotlin/com/processout/sdk/ui/napm/NativeAlternativePaymentInteractor.kt index 9795201e..65ac77e4 100644 --- a/ui/src/main/kotlin/com/processout/sdk/ui/napm/NativeAlternativePaymentInteractor.kt +++ b/ui/src/main/kotlin/com/processout/sdk/ui/napm/NativeAlternativePaymentInteractor.kt @@ -248,46 +248,6 @@ internal class NativeAlternativePaymentInteractor( } } - //region Next Step - - private fun handleNextStep( - stateValue: NextStepStateValue, - elements: List, - redirect: PONativeAlternativePaymentRedirect? - ) { - val parameters = elements.flatMap { - if (it is Element.Form) it.form.parameterDefinitions else emptyList() - } - if (parameters.isEmpty()) { - POLogger.warn( - message = "Parameters is empty in response.", - attributes = configuration.logAttributes - ) - } - if (failWithUnknownParameter(parameters)) { - return - } - if (failWithUnknownRedirect(redirect)) { - return - } - val fields = parameters.toFields() - val updatedStateValue = stateValue.copy( - uuid = UUID.randomUUID().toString(), - redirect = redirect, - elements = elements, - fields = fields, - focusedFieldId = fields.firstFocusableFieldId() - ) - _state.update { - if (_state.value is Loading) { - Loaded(updatedStateValue) - } else { - Submitted(updatedStateValue) - } - } - requestDefaultValues(parameters) - } - private suspend fun List.map(): List = mapNotNull { element -> when (element) { @@ -345,16 +305,53 @@ internal class NativeAlternativePaymentInteractor( } } - private fun failWithUnknownParameter( - parameters: List + //region Next Step + + private fun handleNextStep( + stateValue: NextStepStateValue, + elements: List, + redirect: PONativeAlternativePaymentRedirect? + ) { + if (failWithUnsupportedHeadlessMode(redirect)) { + return + } + if (failWithUnknownRedirect(redirect)) { + return + } + val parameters = elements.flatMap { + if (it is Element.Form) it.form.parameterDefinitions else emptyList() + } + if (failWithUnknownParameter(parameters)) { + return + } + val fields = parameters.toFields() + val updatedStateValue = stateValue.copy( + uuid = UUID.randomUUID().toString(), + redirect = redirect, + elements = elements, + fields = fields, + focusedFieldId = fields.firstFocusableFieldId() + ) + _state.update { + if (_state.value is Loading) { + Loaded(updatedStateValue) + } else { + Submitted(updatedStateValue) + } + } + requestDefaultValues(parameters) + } + + private fun failWithUnsupportedHeadlessMode( + redirect: PONativeAlternativePaymentRedirect? ): Boolean { - parameters.find { it == Parameter.Unknown }?.let { + if (configuration.redirect?.enableHeadlessMode == true && redirect == null) { val failure = ProcessOutResult.Failure( - code = Internal(), - message = "Unknown parameter type." + code = Generic(genericCode = mobileOperationNotSupported), + message = "Headless mode is not supported: redirect parameters are missing in the response." ) POLogger.error( - message = "Unexpected response: %s", failure, + message = "Unsupported operation: %s", failure, attributes = configuration.logAttributes ) _completion.update { Failure(failure) } @@ -381,6 +378,24 @@ internal class NativeAlternativePaymentInteractor( return false } + private fun failWithUnknownParameter( + parameters: List + ): Boolean { + parameters.find { it == Parameter.Unknown }?.let { + val failure = ProcessOutResult.Failure( + code = Internal(), + message = "Unknown parameter type." + ) + POLogger.error( + message = "Unexpected response: %s", failure, + attributes = configuration.logAttributes + ) + _completion.update { Failure(failure) } + return true + } + return false + } + private fun List.toFields() = map { parameter -> val defaultValue = when (parameter) { @@ -414,18 +429,15 @@ internal class NativeAlternativePaymentInteractor( enableNextStepSecondaryAction() POLogger.info("Started: waiting for payment parameters.") dispatch(DidStart) - handleHeadlessRedirect() + handleAutoRedirect() } private fun continueNextStep(stateValue: NextStepStateValue) { - _state.update { - NextStep( - stateValue.copy( - submitAllowed = true, - submitting = false - ) - ) - } + val updatedStateValue = stateValue.copy( + submitAllowed = true, + submitting = false + ) + _state.update { NextStep(updatedStateValue) } POLogger.info("Submitted: waiting for additional payment parameters.") dispatch( event = DidSubmitParameters( @@ -433,33 +445,24 @@ internal class NativeAlternativePaymentInteractor( additionalParametersExpected = true ) ) - handleHeadlessRedirect() + handleAutoRedirect() } - private fun handleHeadlessRedirect() { - if (configuration.redirect?.enableHeadlessMode != true) { - return - } + private fun handleAutoRedirect() { _state.whenNextStep { stateValue -> - if (stateValue.redirect == null) { - val failure = ProcessOutResult.Failure( - code = Generic(genericCode = mobileOperationNotSupported), - message = "Headless mode is not supported: redirect parameters are missing in the response." - ) - POLogger.error( - message = "Unsupported operation: %s", failure, - attributes = configuration.logAttributes + if (stateValue.redirect != null && shouldAutoRedirect()) { + redirect( + stateValue = stateValue, + redirect = stateValue.redirect ) - _completion.update { Failure(failure) } - return@whenNextStep } - redirect( - stateValue = stateValue, - redirect = stateValue.redirect - ) } } + private fun shouldAutoRedirect(): Boolean = + configuration.redirect?.enableHeadlessMode == true || + configuration.redirect?.continueButton == null + //endregion //region Default Values diff --git a/ui/src/main/kotlin/com/processout/sdk/ui/napm/PONativeAlternativePaymentConfiguration.kt b/ui/src/main/kotlin/com/processout/sdk/ui/napm/PONativeAlternativePaymentConfiguration.kt index aae7648c..a2ca3e2f 100644 --- a/ui/src/main/kotlin/com/processout/sdk/ui/napm/PONativeAlternativePaymentConfiguration.kt +++ b/ui/src/main/kotlin/com/processout/sdk/ui/napm/PONativeAlternativePaymentConfiguration.kt @@ -396,11 +396,13 @@ data class PONativeAlternativePaymentConfiguration( * The redirect (web or deep link) will be handled directly when it's the first step in the flow, without starting the bottom sheet. * It will also capture the payment in the background when it's required by the flow. * __Note:__ use only with flows that do not require user input or instructions in the native UI. + * @param[continueButton] Continue button configuration. Pass _null_ to hide and redirect automatically. */ @Parcelize data class RedirectConfiguration( val returnUrl: String, - val enableHeadlessMode: Boolean = false + val enableHeadlessMode: Boolean = false, + val continueButton: Button? = Button() ) : Parcelable /** From ae406133a8ae1ec678eabef8ae14ee15b88b1284 Mon Sep 17 00:00:00 2001 From: Vitalii Vanziak Date: Wed, 13 May 2026 17:46:20 +0300 Subject: [PATCH 3/6] Code improvement: safe cast instead of custom check --- .../sdk/ui/napm/NativeAlternativePaymentInteractor.kt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/ui/src/main/kotlin/com/processout/sdk/ui/napm/NativeAlternativePaymentInteractor.kt b/ui/src/main/kotlin/com/processout/sdk/ui/napm/NativeAlternativePaymentInteractor.kt index 65ac77e4..3eb89fc8 100644 --- a/ui/src/main/kotlin/com/processout/sdk/ui/napm/NativeAlternativePaymentInteractor.kt +++ b/ui/src/main/kotlin/com/processout/sdk/ui/napm/NativeAlternativePaymentInteractor.kt @@ -1317,7 +1317,7 @@ internal class NativeAlternativePaymentInteractor( private fun saveBarcode() { _state.whenNextStep { stateValue -> val instructions = stateValue.elements.mapNotNull { - if (it is Element.Instruction) it else null + it as? Element.Instruction } instructions.forEach { if (it.instruction is Instruction.Barcode) { @@ -1328,7 +1328,7 @@ internal class NativeAlternativePaymentInteractor( } _state.whenPending { stateValue -> val instructions = stateValue.elements?.mapNotNull { - if (it is Element.Instruction) it else null + it as? Element.Instruction } instructions?.forEach { if (it.instruction is Instruction.Barcode) { @@ -1360,7 +1360,7 @@ internal class NativeAlternativePaymentInteractor( if (result.isGranted) { _state.whenNextStep { stateValue -> val instructions = stateValue.elements.mapNotNull { - if (it is Element.Instruction) it else null + it as? Element.Instruction } instructions.forEach { if (it.instruction is Instruction.Barcode) { @@ -1375,7 +1375,7 @@ internal class NativeAlternativePaymentInteractor( } _state.whenPending { stateValue -> val instructions = stateValue.elements?.mapNotNull { - if (it is Element.Instruction) it else null + it as? Element.Instruction } instructions?.forEach { if (it.instruction is Instruction.Barcode) { From 87332e68dc64a0d43b9ba22f6949081aa72b7dbe Mon Sep 17 00:00:00 2001 From: Vitalii Vanziak Date: Wed, 13 May 2026 17:52:57 +0300 Subject: [PATCH 4/6] _state.update --- .../ui/napm/NativeAlternativePaymentInteractor.kt | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/ui/src/main/kotlin/com/processout/sdk/ui/napm/NativeAlternativePaymentInteractor.kt b/ui/src/main/kotlin/com/processout/sdk/ui/napm/NativeAlternativePaymentInteractor.kt index 3eb89fc8..39b83a2e 100644 --- a/ui/src/main/kotlin/com/processout/sdk/ui/napm/NativeAlternativePaymentInteractor.kt +++ b/ui/src/main/kotlin/com/processout/sdk/ui/napm/NativeAlternativePaymentInteractor.kt @@ -433,11 +433,14 @@ internal class NativeAlternativePaymentInteractor( } private fun continueNextStep(stateValue: NextStepStateValue) { - val updatedStateValue = stateValue.copy( - submitAllowed = true, - submitting = false - ) - _state.update { NextStep(updatedStateValue) } + _state.update { + NextStep( + stateValue.copy( + submitAllowed = true, + submitting = false + ) + ) + } POLogger.info("Submitted: waiting for additional payment parameters.") dispatch( event = DidSubmitParameters( From 99ae32a81394bfddd764212750a14251f0ac4046 Mon Sep 17 00:00:00 2001 From: Vitalii Vanziak Date: Thu, 14 May 2026 12:30:22 +0300 Subject: [PATCH 5/6] redirectButton --- .../sdk/ui/napm/NativeAlternativePaymentInteractor.kt | 2 +- .../sdk/ui/napm/PONativeAlternativePaymentConfiguration.kt | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/ui/src/main/kotlin/com/processout/sdk/ui/napm/NativeAlternativePaymentInteractor.kt b/ui/src/main/kotlin/com/processout/sdk/ui/napm/NativeAlternativePaymentInteractor.kt index 39b83a2e..ebbfa878 100644 --- a/ui/src/main/kotlin/com/processout/sdk/ui/napm/NativeAlternativePaymentInteractor.kt +++ b/ui/src/main/kotlin/com/processout/sdk/ui/napm/NativeAlternativePaymentInteractor.kt @@ -464,7 +464,7 @@ internal class NativeAlternativePaymentInteractor( private fun shouldAutoRedirect(): Boolean = configuration.redirect?.enableHeadlessMode == true || - configuration.redirect?.continueButton == null + configuration.redirect?.redirectButton == null //endregion diff --git a/ui/src/main/kotlin/com/processout/sdk/ui/napm/PONativeAlternativePaymentConfiguration.kt b/ui/src/main/kotlin/com/processout/sdk/ui/napm/PONativeAlternativePaymentConfiguration.kt index a2ca3e2f..4a0cf086 100644 --- a/ui/src/main/kotlin/com/processout/sdk/ui/napm/PONativeAlternativePaymentConfiguration.kt +++ b/ui/src/main/kotlin/com/processout/sdk/ui/napm/PONativeAlternativePaymentConfiguration.kt @@ -396,13 +396,14 @@ data class PONativeAlternativePaymentConfiguration( * The redirect (web or deep link) will be handled directly when it's the first step in the flow, without starting the bottom sheet. * It will also capture the payment in the background when it's required by the flow. * __Note:__ use only with flows that do not require user input or instructions in the native UI. - * @param[continueButton] Continue button configuration. Pass _null_ to hide and redirect automatically. + * @param[redirectButton] Redirect button configuration. + * Pass _null_ to hide and redirect automatically, this is a default behaviour. */ @Parcelize data class RedirectConfiguration( val returnUrl: String, val enableHeadlessMode: Boolean = false, - val continueButton: Button? = Button() + val redirectButton: Button? = null ) : Parcelable /** From 375152a1f40890b2b1b1cd6c9431ae4d8387179e Mon Sep 17 00:00:00 2001 From: Vitalii Vanziak Date: Thu, 14 May 2026 13:54:30 +0300 Subject: [PATCH 6/6] Map submit/redirect primary action state in ViewModel --- .../napm/NativeAlternativePaymentViewModel.kt | 33 +++++++++++++------ 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/ui/src/main/kotlin/com/processout/sdk/ui/napm/NativeAlternativePaymentViewModel.kt b/ui/src/main/kotlin/com/processout/sdk/ui/napm/NativeAlternativePaymentViewModel.kt index 176c1034..7526b6e5 100644 --- a/ui/src/main/kotlin/com/processout/sdk/ui/napm/NativeAlternativePaymentViewModel.kt +++ b/ui/src/main/kotlin/com/processout/sdk/ui/napm/NativeAlternativePaymentViewModel.kt @@ -133,16 +133,7 @@ internal class NativeAlternativePaymentViewModel private constructor( ), elements = elements.map(fields) ), - primaryAction = POActionState( - id = primaryActionId, - text = redirect?.hint - ?: configuration.submitButton.text - ?: app.getString(R.string.po_native_apm_continue_button_text), - primary = true, - enabled = submitAllowed, - loading = submitting, - icon = configuration.submitButton.icon - ), + primaryAction = toSubmitAction(), secondaryAction = configuration.cancelButton?.toActionState( id = secondaryAction.id, enabled = secondaryAction.enabled && !submitting @@ -567,6 +558,28 @@ internal class NativeAlternativePaymentViewModel private constructor( private fun Invoice.priceSuccessMessage(): String? = price()?.let { app.getString(R.string.po_native_apm_success_message_format, it) } + private fun NextStepStateValue.toSubmitAction(): POActionState? { + val submitAction = POActionState( + id = primaryActionId, + text = configuration.submitButton.text + ?: app.getString(R.string.po_native_apm_continue_button_text), + primary = true, + enabled = submitAllowed, + loading = submitting, + icon = configuration.submitButton.icon + ) + return if (redirect != null) { + configuration.redirect?.redirectButton?.let { + submitAction.copy( + text = it.text ?: redirect.hint, + icon = it.icon + ) + } + } else { + submitAction + } + } + private fun CancelButton.toActionState( id: String, enabled: Boolean