From 34594b2ab6cad87c197eec4a184c5a4e560c7f55 Mon Sep 17 00:00:00 2001 From: Vitalii Vanziak Date: Wed, 22 May 2024 14:41:14 +0300 Subject: [PATCH 01/38] AGP 8.4.1 --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 15e90b9c4..988f52a3c 100644 --- a/build.gradle +++ b/build.gradle @@ -2,7 +2,7 @@ buildscript { ext { - androidGradlePluginVersion = '8.4.0' + androidGradlePluginVersion = '8.4.1' kotlinVersion = '1.9.24' kspVersion = '1.9.24-1.0.20' dokkaVersion = '1.9.20' From 84b16ef1af84c6d09d787cad3802a125034fdf3a Mon Sep 17 00:00:00 2001 From: Vitalii Vanziak Date: Wed, 22 May 2024 19:18:20 +0300 Subject: [PATCH 02/38] fetchTransactionDetails() and dispatch() --- .../NativeAlternativePaymentInteractor.kt | 46 +++++++++++++++---- 1 file changed, 37 insertions(+), 9 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 71d9c8cd9..7ce11aa00 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 @@ -3,10 +3,14 @@ package com.processout.sdk.ui.napm import android.app.Application import androidx.compose.ui.text.input.TextFieldValue import com.processout.sdk.api.dispatcher.napm.PODefaultNativeAlternativePaymentMethodEventDispatcher +import com.processout.sdk.api.model.event.PONativeAlternativePaymentMethodEvent +import com.processout.sdk.api.model.event.PONativeAlternativePaymentMethodEvent.DidFail import com.processout.sdk.api.service.POInvoicesService import com.processout.sdk.core.POFailure import com.processout.sdk.core.ProcessOutResult import com.processout.sdk.core.logger.POLogger +import com.processout.sdk.core.onFailure +import com.processout.sdk.core.onSuccess import com.processout.sdk.core.retry.PORetryStrategy import com.processout.sdk.ui.base.BaseInteractor import com.processout.sdk.ui.napm.NativeAlternativePaymentCompletion.Awaiting @@ -43,16 +47,23 @@ internal class NativeAlternativePaymentInteractor( val state = _state.asStateFlow() init { - // TODO + POLogger.info("Starting native alternative payment.", attributes = logAttributes) + dispatch(PONativeAlternativePaymentMethodEvent.WillStart) + dispatchFailure() +// collectDefaultValues() TODO + fetchTransactionDetails() + } + + private fun fetchTransactionDetails() { interactorScope.launch { - delay(2000) - _state.update { - UserInput( - UserInputStateValue( - primaryActionId = ActionId.SUBMIT, - secondaryActionId = ActionId.CANCEL - ) - ) + invoicesService.fetchNativeAlternativePaymentMethodTransactionDetails( + invoiceId = invoiceId, + gatewayConfigurationId = gatewayConfigurationId + ).onSuccess { details -> + // TODO + }.onFailure { failure -> + POLogger.info("Failed to fetch transaction details: %s", failure) + _completion.update { Failure(failure) } } } } @@ -107,4 +118,21 @@ internal class NativeAlternativePaymentInteractor( ) } } + + private fun dispatch(event: PONativeAlternativePaymentMethodEvent) { + interactorScope.launch { + eventDispatcher.send(event) + POLogger.debug("Event has been sent: %s", event) + } + } + + private fun dispatchFailure() { + interactorScope.launch { + _completion.collect { + if (it is Failure) { + dispatch(DidFail(it.failure)) + } + } + } + } } From ffe70033d91686e65b56e49b993b2c4012b72a86 Mon Sep 17 00:00:00 2001 From: Vitalii Vanziak Date: Thu, 23 May 2024 11:37:11 +0300 Subject: [PATCH 03/38] POMarkdownUtils --- .../processout/sdk/core/util/MarkdownUtils.kt | 17 ---------------- .../sdk/core/util/POMarkdownUtils.kt | 20 +++++++++++++++++++ ...NativeAlternativePaymentMethodViewModel.kt | 4 ++-- 3 files changed, 22 insertions(+), 19 deletions(-) delete mode 100644 sdk/src/main/kotlin/com/processout/sdk/core/util/MarkdownUtils.kt create mode 100644 sdk/src/main/kotlin/com/processout/sdk/core/util/POMarkdownUtils.kt diff --git a/sdk/src/main/kotlin/com/processout/sdk/core/util/MarkdownUtils.kt b/sdk/src/main/kotlin/com/processout/sdk/core/util/MarkdownUtils.kt deleted file mode 100644 index f38d0af69..000000000 --- a/sdk/src/main/kotlin/com/processout/sdk/core/util/MarkdownUtils.kt +++ /dev/null @@ -1,17 +0,0 @@ -package com.processout.sdk.core.util - -private const val ESCAPE_CHAR = '\\' -private const val MARKDOWN_SPECIAL_CHARS = "\\`*_{}[]()#+-.!" - -internal fun escapedMarkdown(text: String?): String? = - text?.let { - StringBuilder().let { - text.forEach { char -> - if (MARKDOWN_SPECIAL_CHARS.contains(char)) { - it.append(ESCAPE_CHAR) - } - it.append(char) - } - it.toString() - } - } diff --git a/sdk/src/main/kotlin/com/processout/sdk/core/util/POMarkdownUtils.kt b/sdk/src/main/kotlin/com/processout/sdk/core/util/POMarkdownUtils.kt new file mode 100644 index 000000000..865c8e8b6 --- /dev/null +++ b/sdk/src/main/kotlin/com/processout/sdk/core/util/POMarkdownUtils.kt @@ -0,0 +1,20 @@ +package com.processout.sdk.core.util + +import com.processout.sdk.core.annotation.ProcessOutInternalApi + +/** @suppress */ +@ProcessOutInternalApi +object POMarkdownUtils { + + private const val ESCAPE_CHAR = '\\' + private const val MARKDOWN_SPECIAL_CHARS = "\\`*_{}[]()#+-.!" + + fun escapedMarkdown(text: String) = buildString { + text.forEach { char -> + if (MARKDOWN_SPECIAL_CHARS.contains(char)) { + append(ESCAPE_CHAR) + } + append(char) + } + } +} diff --git a/sdk/src/main/kotlin/com/processout/sdk/ui/nativeapm/NativeAlternativePaymentMethodViewModel.kt b/sdk/src/main/kotlin/com/processout/sdk/ui/nativeapm/NativeAlternativePaymentMethodViewModel.kt index 87abc70e9..7eb79c19d 100644 --- a/sdk/src/main/kotlin/com/processout/sdk/ui/nativeapm/NativeAlternativePaymentMethodViewModel.kt +++ b/sdk/src/main/kotlin/com/processout/sdk/ui/nativeapm/NativeAlternativePaymentMethodViewModel.kt @@ -35,7 +35,7 @@ import com.processout.sdk.core.ProcessOutResult import com.processout.sdk.core.logger.POLogger import com.processout.sdk.core.retry.PORetryStrategy import com.processout.sdk.core.retry.PORetryStrategy.Exponential -import com.processout.sdk.core.util.escapedMarkdown +import com.processout.sdk.core.util.POMarkdownUtils.escapedMarkdown import com.processout.sdk.ui.nativeapm.NativeAlternativePaymentMethodUiState.* import com.processout.sdk.ui.nativeapm.PONativeAlternativePaymentMethodConfiguration.* import com.processout.sdk.ui.nativeapm.PONativeAlternativePaymentMethodConfiguration.Options.Companion.DEFAULT_PAYMENT_CONFIRMATION_TIMEOUT_SECONDS @@ -603,7 +603,7 @@ internal class NativeAlternativePaymentMethodViewModel( inputParameters = parameters?.toInputParameters() ?: emptyList(), successMessage = options.successMessage ?: app.getString(R.string.po_native_apm_success_message), - customerActionMessageMarkdown = escapedMarkdown(gateway.customerActionMessage), + customerActionMessageMarkdown = gateway.customerActionMessage?.let { escapedMarkdown(it) }, customerActionImageUrl = gateway.customerActionImageUrl, primaryActionText = options.primaryActionText ?: invoice.formatPrimaryActionText(), secondaryAction = options.secondaryAction?.toUiModel(), From b4a55212028f24f682fae3234e2dabcf4b2a1d53 Mon Sep 17 00:00:00 2001 From: Vitalii Vanziak Date: Thu, 23 May 2024 13:09:02 +0300 Subject: [PATCH 04/38] States and mapping --- .../NativeAlternativePaymentInteractor.kt | 36 +++++- ...NativeAlternativePaymentInteractorState.kt | 9 +- .../napm/NativeAlternativePaymentViewModel.kt | 110 ++++++++++++------ .../NativeAlternativePaymentViewModelState.kt | 7 +- 4 files changed, 124 insertions(+), 38 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 7ce11aa00..a7a370483 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 @@ -5,6 +5,10 @@ import androidx.compose.ui.text.input.TextFieldValue import com.processout.sdk.api.dispatcher.napm.PODefaultNativeAlternativePaymentMethodEventDispatcher import com.processout.sdk.api.model.event.PONativeAlternativePaymentMethodEvent import com.processout.sdk.api.model.event.PONativeAlternativePaymentMethodEvent.DidFail +import com.processout.sdk.api.model.response.PONativeAlternativePaymentMethodParameter +import com.processout.sdk.api.model.response.PONativeAlternativePaymentMethodParameterValues +import com.processout.sdk.api.model.response.PONativeAlternativePaymentMethodState +import com.processout.sdk.api.model.response.PONativeAlternativePaymentMethodTransactionDetails import com.processout.sdk.api.service.POInvoicesService import com.processout.sdk.core.POFailure import com.processout.sdk.core.ProcessOutResult @@ -18,6 +22,7 @@ import com.processout.sdk.ui.napm.NativeAlternativePaymentCompletion.Failure import com.processout.sdk.ui.napm.NativeAlternativePaymentEvent.* import com.processout.sdk.ui.napm.NativeAlternativePaymentInteractorState.* import com.processout.sdk.ui.napm.PONativeAlternativePaymentConfiguration.Options +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow @@ -60,7 +65,16 @@ internal class NativeAlternativePaymentInteractor( invoiceId = invoiceId, gatewayConfigurationId = gatewayConfigurationId ).onSuccess { details -> - // TODO + with(details) { + handleState( + stateValue = toStateValue(), + paymentState = state, + parameters = parameters, + parameterValues = parameterValues, + isInitial = true, + coroutineScope = this@launch + ) + } }.onFailure { failure -> POLogger.info("Failed to fetch transaction details: %s", failure) _completion.update { Failure(failure) } @@ -68,6 +82,25 @@ internal class NativeAlternativePaymentInteractor( } } + private fun PONativeAlternativePaymentMethodTransactionDetails.toStateValue() = + UserInputStateValue( + invoice = invoice, + gateway = gateway, + primaryActionId = ActionId.SUBMIT, + secondaryActionId = ActionId.CANCEL + ) + + private suspend fun handleState( + stateValue: UserInputStateValue, + paymentState: PONativeAlternativePaymentMethodState?, + parameters: List?, + parameterValues: PONativeAlternativePaymentMethodParameterValues?, + isInitial: Boolean, + coroutineScope: CoroutineScope + ) { + // TODO + } + fun onEvent(event: NativeAlternativePaymentEvent) { when (event) { is FieldValueChanged -> updateFieldValue(event.id, event.value) @@ -94,6 +127,7 @@ internal class NativeAlternativePaymentInteractor( _state.update { Capturing( CaptureStateValue( + paymentProviderName = null, logoUrl = null, secondaryActionId = ActionId.CANCEL ) diff --git a/ui/src/main/kotlin/com/processout/sdk/ui/napm/NativeAlternativePaymentInteractorState.kt b/ui/src/main/kotlin/com/processout/sdk/ui/napm/NativeAlternativePaymentInteractorState.kt index 8ec3ada46..47996d01a 100644 --- a/ui/src/main/kotlin/com/processout/sdk/ui/napm/NativeAlternativePaymentInteractorState.kt +++ b/ui/src/main/kotlin/com/processout/sdk/ui/napm/NativeAlternativePaymentInteractorState.kt @@ -1,5 +1,7 @@ package com.processout.sdk.ui.napm +import com.processout.sdk.api.model.response.PONativeAlternativePaymentMethodTransactionDetails.Gateway +import com.processout.sdk.api.model.response.PONativeAlternativePaymentMethodTransactionDetails.Invoice import com.processout.sdk.ui.napm.NativeAlternativePaymentInteractorState.* import kotlinx.coroutines.flow.MutableStateFlow @@ -32,11 +34,16 @@ internal sealed interface NativeAlternativePaymentInteractorState { //endregion data class UserInputStateValue( + val invoice: Invoice, + val gateway: Gateway, val primaryActionId: String, - val secondaryActionId: String + val secondaryActionId: String, + val submitAllowed: Boolean = true, + val submitting: Boolean = false ) data class CaptureStateValue( + val paymentProviderName: String?, val logoUrl: String?, val secondaryActionId: String ) 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 a726635d0..9046e631a 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 @@ -4,17 +4,23 @@ import android.app.Application import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewModelScope +import com.processout.sdk.R import com.processout.sdk.api.ProcessOut import com.processout.sdk.api.dispatcher.napm.PODefaultNativeAlternativePaymentMethodEventDispatcher +import com.processout.sdk.api.model.response.PONativeAlternativePaymentMethodTransactionDetails.Invoice import com.processout.sdk.core.retry.PORetryStrategy.Exponential +import com.processout.sdk.core.util.POMarkdownUtils.escapedMarkdown import com.processout.sdk.ui.core.state.POActionState import com.processout.sdk.ui.napm.NativeAlternativePaymentInteractor.Companion.LOG_ATTRIBUTE_GATEWAY_CONFIGURATION_ID import com.processout.sdk.ui.napm.NativeAlternativePaymentInteractor.Companion.LOG_ATTRIBUTE_INVOICE_ID -import com.processout.sdk.ui.napm.NativeAlternativePaymentViewModelState.* +import com.processout.sdk.ui.napm.NativeAlternativePaymentInteractorState.* import com.processout.sdk.ui.napm.PONativeAlternativePaymentConfiguration.Options import com.processout.sdk.ui.napm.PONativeAlternativePaymentConfiguration.PaymentConfirmationConfiguration.Companion.DEFAULT_TIMEOUT_SECONDS import com.processout.sdk.ui.napm.PONativeAlternativePaymentConfiguration.PaymentConfirmationConfiguration.Companion.MAX_TIMEOUT_SECONDS +import com.processout.sdk.ui.napm.PONativeAlternativePaymentConfiguration.SecondaryAction import com.processout.sdk.ui.shared.extension.map +import java.text.NumberFormat +import java.util.Currency internal class NativeAlternativePaymentViewModel( private val app: Application, @@ -76,40 +82,74 @@ internal class NativeAlternativePaymentViewModel( private fun map( state: NativeAlternativePaymentInteractorState - ): NativeAlternativePaymentViewModelState = with(options) { - when (state) { - NativeAlternativePaymentInteractorState.Loading -> Loading - is NativeAlternativePaymentInteractorState.UserInput -> - with(state.value) { - UserInput( - title = "Title", - primaryAction = POActionState( - id = primaryActionId, - text = "Submit", - primary = true - ), - secondaryAction = POActionState( - id = secondaryActionId, - text = "Cancel", - primary = false - ) - ) - } - is NativeAlternativePaymentInteractorState.Capturing -> - Capture( - secondaryAction = POActionState( - id = state.value.secondaryActionId, - text = "Cancel", - primary = false - ), - isCaptured = false - ) - is NativeAlternativePaymentInteractorState.Captured -> - Capture( - secondaryAction = null, - isCaptured = true - ) - else -> this@NativeAlternativePaymentViewModel.state.value + ): NativeAlternativePaymentViewModelState = when (state) { + Loading -> NativeAlternativePaymentViewModelState.Loading + is UserInput -> state.userInput() + is Capturing -> state.capture() + is Captured -> state.capture() + else -> this@NativeAlternativePaymentViewModel.state.value + } + + private fun UserInput.userInput() = with(value) { + NativeAlternativePaymentViewModelState.UserInput( + title = options.title ?: app.getString(R.string.po_native_apm_title_format, gateway.displayName), + primaryAction = POActionState( + id = primaryActionId, + text = options.primaryActionText ?: invoice.formatPrimaryActionText(), + primary = true, + enabled = submitAllowed, + loading = submitting + ), + secondaryAction = options.secondaryAction?.state( + id = secondaryActionId, + enabled = !submitting + ), + actionMessageMarkdown = gateway.customerActionMessage?.let { escapedMarkdown(it) }, + actionImageUrl = gateway.customerActionImageUrl, + successMessage = options.successMessage ?: app.getString(R.string.po_native_apm_success_message) + ) + } + + private fun Capturing.capture() = with(value) { + NativeAlternativePaymentViewModelState.Capture( + paymentProviderName = paymentProviderName, + logoUrl = logoUrl, + secondaryAction = options.paymentConfirmation.secondaryAction?.state( + id = secondaryActionId, + enabled = true + ), + isCaptured = false + ) + } + + private fun Captured.capture() = with(value) { + NativeAlternativePaymentViewModelState.Capture( + paymentProviderName = paymentProviderName, + logoUrl = logoUrl, + secondaryAction = null, + isCaptured = true + ) + } + + private fun Invoice.formatPrimaryActionText() = + try { + val price = NumberFormat.getCurrencyInstance().apply { + currency = Currency.getInstance(currencyCode) + }.format(amount.toDouble()) + app.getString(R.string.po_native_apm_submit_button_text_format, price) + } catch (_: Exception) { + app.getString(R.string.po_native_apm_submit_button_text) } + + private fun SecondaryAction.state( + id: String, + enabled: Boolean + ): POActionState = when (this) { + is SecondaryAction.Cancel -> POActionState( + id = id, + text = text ?: app.getString(R.string.po_native_apm_cancel_button_text), + primary = false, + enabled = enabled + ) } } diff --git a/ui/src/main/kotlin/com/processout/sdk/ui/napm/NativeAlternativePaymentViewModelState.kt b/ui/src/main/kotlin/com/processout/sdk/ui/napm/NativeAlternativePaymentViewModelState.kt index b5a365d75..e18aae4d3 100644 --- a/ui/src/main/kotlin/com/processout/sdk/ui/napm/NativeAlternativePaymentViewModelState.kt +++ b/ui/src/main/kotlin/com/processout/sdk/ui/napm/NativeAlternativePaymentViewModelState.kt @@ -11,11 +11,16 @@ internal sealed interface NativeAlternativePaymentViewModelState { data class UserInput( val title: String, val primaryAction: POActionState, - val secondaryAction: POActionState? + val secondaryAction: POActionState?, + val actionMessageMarkdown: String?, + val actionImageUrl: String?, + val successMessage: String ) : NativeAlternativePaymentViewModelState @Immutable data class Capture( + val paymentProviderName: String?, + val logoUrl: String?, val secondaryAction: POActionState?, val isCaptured: Boolean ) : NativeAlternativePaymentViewModelState From f147e379311b156b8cc113a70f489503d1b01fbc Mon Sep 17 00:00:00 2001 From: Vitalii Vanziak Date: Thu, 23 May 2024 14:13:15 +0300 Subject: [PATCH 05/38] updateFieldFocus() --- .../sdk/ui/napm/NativeAlternativePaymentInteractor.kt | 10 +++++++++- .../napm/NativeAlternativePaymentInteractorState.kt | 11 +++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) 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 a7a370483..c1d64d25a 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 @@ -86,6 +86,8 @@ internal class NativeAlternativePaymentInteractor( UserInputStateValue( invoice = invoice, gateway = gateway, + fields = emptyList(), + focusedFieldId = null, primaryActionId = ActionId.SUBMIT, secondaryActionId = ActionId.CANCEL ) @@ -118,7 +120,13 @@ internal class NativeAlternativePaymentInteractor( } private fun updateFieldFocus(id: String, isFocused: Boolean) { - // TODO + if (isFocused) { + _state.whenUserInput { stateValue -> + _state.update { + UserInput(stateValue.copy(focusedFieldId = id)) + } + } + } } private fun submit() { diff --git a/ui/src/main/kotlin/com/processout/sdk/ui/napm/NativeAlternativePaymentInteractorState.kt b/ui/src/main/kotlin/com/processout/sdk/ui/napm/NativeAlternativePaymentInteractorState.kt index 47996d01a..d1d98cec5 100644 --- a/ui/src/main/kotlin/com/processout/sdk/ui/napm/NativeAlternativePaymentInteractorState.kt +++ b/ui/src/main/kotlin/com/processout/sdk/ui/napm/NativeAlternativePaymentInteractorState.kt @@ -1,7 +1,9 @@ package com.processout.sdk.ui.napm +import androidx.compose.ui.text.input.TextFieldValue import com.processout.sdk.api.model.response.PONativeAlternativePaymentMethodTransactionDetails.Gateway import com.processout.sdk.api.model.response.PONativeAlternativePaymentMethodTransactionDetails.Invoice +import com.processout.sdk.ui.core.state.POAvailableValue import com.processout.sdk.ui.napm.NativeAlternativePaymentInteractorState.* import kotlinx.coroutines.flow.MutableStateFlow @@ -36,6 +38,8 @@ internal sealed interface NativeAlternativePaymentInteractorState { data class UserInputStateValue( val invoice: Invoice, val gateway: Gateway, + val fields: List, + val focusedFieldId: String?, val primaryActionId: String, val secondaryActionId: String, val submitAllowed: Boolean = true, @@ -48,6 +52,13 @@ internal sealed interface NativeAlternativePaymentInteractorState { val secondaryActionId: String ) + data class Field( + val id: String, + val value: TextFieldValue = TextFieldValue(), + val availableValues: List? = null, + val isValid: Boolean = true + ) + object ActionId { const val SUBMIT = "submit" const val CANCEL = "cancel" From feb04d16b355528e9bf0c4f2be440aa688a51428 Mon Sep 17 00:00:00 2001 From: Vitalii Vanziak Date: Thu, 23 May 2024 15:29:59 +0300 Subject: [PATCH 06/38] Map params to fields in interactor --- .../NativeAlternativePaymentInteractor.kt | 37 +++++++++++++++++-- ...NativeAlternativePaymentInteractorState.kt | 11 ++++-- 2 files changed, 41 insertions(+), 7 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 c1d64d25a..8dc2fbbad 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 @@ -1,6 +1,7 @@ package com.processout.sdk.ui.napm import android.app.Application +import androidx.compose.ui.text.TextRange import androidx.compose.ui.text.input.TextFieldValue import com.processout.sdk.api.dispatcher.napm.PODefaultNativeAlternativePaymentMethodEventDispatcher import com.processout.sdk.api.model.event.PONativeAlternativePaymentMethodEvent @@ -17,6 +18,7 @@ import com.processout.sdk.core.onFailure import com.processout.sdk.core.onSuccess import com.processout.sdk.core.retry.PORetryStrategy import com.processout.sdk.ui.base.BaseInteractor +import com.processout.sdk.ui.core.state.POAvailableValue import com.processout.sdk.ui.napm.NativeAlternativePaymentCompletion.Awaiting import com.processout.sdk.ui.napm.NativeAlternativePaymentCompletion.Failure import com.processout.sdk.ui.napm.NativeAlternativePaymentEvent.* @@ -82,15 +84,42 @@ internal class NativeAlternativePaymentInteractor( } } - private fun PONativeAlternativePaymentMethodTransactionDetails.toStateValue() = - UserInputStateValue( + private fun PONativeAlternativePaymentMethodTransactionDetails.toStateValue(): UserInputStateValue { + val fields = parameters?.toFields() ?: emptyList() + return UserInputStateValue( invoice = invoice, gateway = gateway, - fields = emptyList(), - focusedFieldId = null, + fields = fields, + focusedFieldId = fields.firstOrNull()?.id, primaryActionId = ActionId.SUBMIT, secondaryActionId = ActionId.CANCEL ) + } + + private fun List.toFields() = + map { parameter -> + with(parameter) { + val defaultValue = availableValues?.find { it.default == true }?.value ?: String() + Field( + id = key, + value = TextFieldValue( + text = defaultValue, + selection = TextRange(defaultValue.length) + ), + availableValues = availableValues?.map { + POAvailableValue( + value = it.value, + text = it.displayName + ) + }, + type = type(), + length = length, + displayName = displayName, + required = required, + isValid = true + ) + } + } private suspend fun handleState( stateValue: UserInputStateValue, diff --git a/ui/src/main/kotlin/com/processout/sdk/ui/napm/NativeAlternativePaymentInteractorState.kt b/ui/src/main/kotlin/com/processout/sdk/ui/napm/NativeAlternativePaymentInteractorState.kt index d1d98cec5..4e61a7cca 100644 --- a/ui/src/main/kotlin/com/processout/sdk/ui/napm/NativeAlternativePaymentInteractorState.kt +++ b/ui/src/main/kotlin/com/processout/sdk/ui/napm/NativeAlternativePaymentInteractorState.kt @@ -1,6 +1,7 @@ package com.processout.sdk.ui.napm import androidx.compose.ui.text.input.TextFieldValue +import com.processout.sdk.api.model.response.PONativeAlternativePaymentMethodParameter.ParameterType import com.processout.sdk.api.model.response.PONativeAlternativePaymentMethodTransactionDetails.Gateway import com.processout.sdk.api.model.response.PONativeAlternativePaymentMethodTransactionDetails.Invoice import com.processout.sdk.ui.core.state.POAvailableValue @@ -54,9 +55,13 @@ internal sealed interface NativeAlternativePaymentInteractorState { data class Field( val id: String, - val value: TextFieldValue = TextFieldValue(), - val availableValues: List? = null, - val isValid: Boolean = true + val value: TextFieldValue, + val availableValues: List?, + val type: ParameterType, + val length: Int?, + val displayName: String, + val required: Boolean, + val isValid: Boolean ) object ActionId { From 3ad9adb12c1497b433ea401d1ab14caf8ef3749c Mon Sep 17 00:00:00 2001 From: Vitalii Vanziak Date: Thu, 23 May 2024 15:39:50 +0300 Subject: [PATCH 07/38] State fix --- .../sdk/ui/napm/NativeAlternativePaymentInteractor.kt | 4 +++- .../sdk/ui/napm/NativeAlternativePaymentInteractorState.kt | 4 ++-- 2 files changed, 5 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 8dc2fbbad..f3811c234 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 @@ -92,7 +92,9 @@ internal class NativeAlternativePaymentInteractor( fields = fields, focusedFieldId = fields.firstOrNull()?.id, primaryActionId = ActionId.SUBMIT, - secondaryActionId = ActionId.CANCEL + secondaryActionId = ActionId.CANCEL, + submitAllowed = true, + submitting = false ) } diff --git a/ui/src/main/kotlin/com/processout/sdk/ui/napm/NativeAlternativePaymentInteractorState.kt b/ui/src/main/kotlin/com/processout/sdk/ui/napm/NativeAlternativePaymentInteractorState.kt index 4e61a7cca..efc445702 100644 --- a/ui/src/main/kotlin/com/processout/sdk/ui/napm/NativeAlternativePaymentInteractorState.kt +++ b/ui/src/main/kotlin/com/processout/sdk/ui/napm/NativeAlternativePaymentInteractorState.kt @@ -43,8 +43,8 @@ internal sealed interface NativeAlternativePaymentInteractorState { val focusedFieldId: String?, val primaryActionId: String, val secondaryActionId: String, - val submitAllowed: Boolean = true, - val submitting: Boolean = false + val submitAllowed: Boolean, + val submitting: Boolean ) data class CaptureStateValue( From b4f8db193db736cf4847894694868f457a939145 Mon Sep 17 00:00:00 2001 From: Vitalii Vanziak Date: Thu, 23 May 2024 20:00:53 +0300 Subject: [PATCH 08/38] Customer input state and related logic --- ...nativePaymentMethodDefaultValuesRequest.kt | 3 +- .../NativeAlternativePaymentInteractor.kt | 182 ++++++++++++++++-- 2 files changed, 171 insertions(+), 14 deletions(-) diff --git a/sdk/src/main/kotlin/com/processout/sdk/api/model/request/PONativeAlternativePaymentMethodDefaultValuesRequest.kt b/sdk/src/main/kotlin/com/processout/sdk/api/model/request/PONativeAlternativePaymentMethodDefaultValuesRequest.kt index e3d237597..8143aab4d 100644 --- a/sdk/src/main/kotlin/com/processout/sdk/api/model/request/PONativeAlternativePaymentMethodDefaultValuesRequest.kt +++ b/sdk/src/main/kotlin/com/processout/sdk/api/model/request/PONativeAlternativePaymentMethodDefaultValuesRequest.kt @@ -1,6 +1,7 @@ package com.processout.sdk.api.model.request import com.processout.sdk.api.model.response.PONativeAlternativePaymentMethodParameter +import com.processout.sdk.core.annotation.ProcessOutInternalApi import java.util.UUID /** @@ -11,7 +12,7 @@ import java.util.UUID * @param[parameters] Collection of parameters that can be inspected to decide if default values should be provided. * @param[uuid] Unique identifier of request. */ -data class PONativeAlternativePaymentMethodDefaultValuesRequest internal constructor( +data class PONativeAlternativePaymentMethodDefaultValuesRequest @ProcessOutInternalApi constructor( val gatewayConfigurationId: String, val invoiceId: String, val parameters: List, 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 f3811c234..e15c613e0 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 @@ -5,10 +5,13 @@ import androidx.compose.ui.text.TextRange import androidx.compose.ui.text.input.TextFieldValue import com.processout.sdk.api.dispatcher.napm.PODefaultNativeAlternativePaymentMethodEventDispatcher import com.processout.sdk.api.model.event.PONativeAlternativePaymentMethodEvent -import com.processout.sdk.api.model.event.PONativeAlternativePaymentMethodEvent.DidFail +import com.processout.sdk.api.model.event.PONativeAlternativePaymentMethodEvent.* +import com.processout.sdk.api.model.request.PONativeAlternativePaymentMethodDefaultValuesRequest import com.processout.sdk.api.model.response.PONativeAlternativePaymentMethodParameter +import com.processout.sdk.api.model.response.PONativeAlternativePaymentMethodParameter.ParameterType.UNKNOWN import com.processout.sdk.api.model.response.PONativeAlternativePaymentMethodParameterValues import com.processout.sdk.api.model.response.PONativeAlternativePaymentMethodState +import com.processout.sdk.api.model.response.PONativeAlternativePaymentMethodState.* import com.processout.sdk.api.model.response.PONativeAlternativePaymentMethodTransactionDetails import com.processout.sdk.api.service.POInvoicesService import com.processout.sdk.core.POFailure @@ -53,11 +56,13 @@ internal class NativeAlternativePaymentInteractor( private val _state = MutableStateFlow(Loading) val state = _state.asStateFlow() + private var latestDefaultValuesRequest: PONativeAlternativePaymentMethodDefaultValuesRequest? = null + init { POLogger.info("Starting native alternative payment.", attributes = logAttributes) - dispatch(PONativeAlternativePaymentMethodEvent.WillStart) + dispatch(WillStart) dispatchFailure() -// collectDefaultValues() TODO + collectDefaultValues() fetchTransactionDetails() } @@ -73,7 +78,6 @@ internal class NativeAlternativePaymentInteractor( paymentState = state, parameters = parameters, parameterValues = parameterValues, - isInitial = true, coroutineScope = this@launch ) } @@ -84,18 +88,95 @@ internal class NativeAlternativePaymentInteractor( } } - private fun PONativeAlternativePaymentMethodTransactionDetails.toStateValue(): UserInputStateValue { - val fields = parameters?.toFields() ?: emptyList() - return UserInputStateValue( + private fun PONativeAlternativePaymentMethodTransactionDetails.toStateValue() = + UserInputStateValue( invoice = invoice, gateway = gateway, - fields = fields, - focusedFieldId = fields.firstOrNull()?.id, + fields = emptyList(), + focusedFieldId = null, primaryActionId = ActionId.SUBMIT, secondaryActionId = ActionId.CANCEL, submitAllowed = true, submitting = false ) + + private suspend fun handleState( + stateValue: UserInputStateValue, + paymentState: PONativeAlternativePaymentMethodState?, + parameters: List?, + parameterValues: PONativeAlternativePaymentMethodParameterValues?, + coroutineScope: CoroutineScope + ) { + when (paymentState) { + CUSTOMER_INPUT, null -> handleCustomerInput(stateValue, parameters) + PENDING_CAPTURE -> handlePendingCapture(stateValue, parameterValues, coroutineScope) + CAPTURED -> handleCaptured(stateValue) + FAILED -> _completion.update { + Failure( + ProcessOutResult.Failure( + code = POFailure.Code.Generic(), + message = "Payment has failed." + ).also { POLogger.info("%s", it, attributes = logAttributes) } + ) + } + } + } + + private fun handleCustomerInput( + stateValue: UserInputStateValue, + parameters: List? + ) { + if (parameters.isNullOrEmpty()) { + _completion.update { + Failure( + ProcessOutResult.Failure( + code = POFailure.Code.Internal(), + message = "Input parameters is missing in response." + ).also { POLogger.warn("%s", it, attributes = logAttributes) } + ) + } + return + } + if (failWithUnknownInputParameter(parameters)) { + return + } + val fields = parameters.toFields() + val updatedStateValue = stateValue.copy( + fields = fields, + focusedFieldId = fields.firstOrNull()?.id + ) + val isLoading = _state.value is Loading + if (eventDispatcher.subscribedForDefaultValuesRequest()) { + _state.update { + if (isLoading) { + Loaded(updatedStateValue) + } else { + Submitted(updatedStateValue) + } + } + requestDefaultValues(parameters) + } else if (isLoading) { + startUserInput(updatedStateValue) + } else { + continueUserInput(updatedStateValue) + } + } + + private fun failWithUnknownInputParameter( + parameters: List + ): Boolean { + parameters.find { it.type() == UNKNOWN }?.let { parameter -> + _completion.update { + Failure( + ProcessOutResult.Failure( + code = POFailure.Code.Internal(), + message = "Unknown input parameter type: ${parameter.rawType}" + ).also { POLogger.error("%s", it, attributes = logAttributes) } + ) + } + return true + } + return false } private fun List.toFields() = @@ -123,17 +204,92 @@ internal class NativeAlternativePaymentInteractor( } } - private suspend fun handleState( + private fun startUserInput(stateValue: UserInputStateValue) { + _state.update { UserInput(stateValue) } + + // TODO +// uiModel.secondaryAction?.let { +// scheduleSecondaryActionEnabling(it) { enableSecondaryAction() } +// } + + dispatch(DidStart) + POLogger.info("Started. Waiting for payment parameters.") + } + + private fun continueUserInput(stateValue: UserInputStateValue) { + _state.update { + UserInput( + stateValue.copy( + submitAllowed = true, + submitting = false + ) + ) + } + dispatch(DidSubmitParameters(additionalParametersExpected = true)) + POLogger.info("Submitted. Waiting for additional payment parameters.") + } + + private fun requestDefaultValues(parameters: List) { + interactorScope.launch { + val request = PONativeAlternativePaymentMethodDefaultValuesRequest( + gatewayConfigurationId = gatewayConfigurationId, + invoiceId = invoiceId, + parameters = parameters + ) + latestDefaultValuesRequest = request + eventDispatcher.send(request) + POLogger.debug("Requested to provide default values for payment parameters: %s", request) + } + } + + private fun collectDefaultValues() { + interactorScope.launch { + eventDispatcher.defaultValuesResponse.collect { response -> + if (response.uuid == latestDefaultValuesRequest?.uuid) { + latestDefaultValuesRequest = null + POLogger.debug("Collected default values for payment parameters: %s", response) + _state.whenLoaded { stateValue -> + startUserInput(stateValue.withDefaultValues(response.defaultValues)) + } + _state.whenSubmitted { stateValue -> + continueUserInput(stateValue.withDefaultValues(response.defaultValues)) + } + } + } + } + } + + private fun UserInputStateValue.withDefaultValues( + defaultValues: Map + ): UserInputStateValue { + val updatedFields = fields.map { field -> + defaultValues.entries.find { it.key == field.id }?.let { + val defaultValue = field.length?.let { length -> + it.value.take(length) + } ?: it.value + field.copy( + value = TextFieldValue( + text = defaultValue, + selection = TextRange(defaultValue.length) + ) + ) + } ?: field + } + return copy(fields = updatedFields) + } + + private fun handlePendingCapture( stateValue: UserInputStateValue, - paymentState: PONativeAlternativePaymentMethodState?, - parameters: List?, parameterValues: PONativeAlternativePaymentMethodParameterValues?, - isInitial: Boolean, coroutineScope: CoroutineScope ) { // TODO } + private fun handleCaptured(stateValue: UserInputStateValue) { + // TODO + } + fun onEvent(event: NativeAlternativePaymentEvent) { when (event) { is FieldValueChanged -> updateFieldValue(event.id, event.value) From aeb8844fae29847a9780b856ad2c264ea6a25772 Mon Sep 17 00:00:00 2001 From: Vitalii Vanziak Date: Thu, 23 May 2024 20:03:43 +0300 Subject: [PATCH 09/38] Import POFailure.Code.* --- .../sdk/ui/napm/NativeAlternativePaymentInteractor.kt | 10 +++++----- 1 file changed, 5 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 e15c613e0..a9824702e 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 @@ -14,7 +14,7 @@ import com.processout.sdk.api.model.response.PONativeAlternativePaymentMethodSta import com.processout.sdk.api.model.response.PONativeAlternativePaymentMethodState.* import com.processout.sdk.api.model.response.PONativeAlternativePaymentMethodTransactionDetails import com.processout.sdk.api.service.POInvoicesService -import com.processout.sdk.core.POFailure +import com.processout.sdk.core.POFailure.Code.* import com.processout.sdk.core.ProcessOutResult import com.processout.sdk.core.logger.POLogger import com.processout.sdk.core.onFailure @@ -114,7 +114,7 @@ internal class NativeAlternativePaymentInteractor( FAILED -> _completion.update { Failure( ProcessOutResult.Failure( - code = POFailure.Code.Generic(), + code = Generic(), message = "Payment has failed." ).also { POLogger.info("%s", it, attributes = logAttributes) } ) @@ -130,7 +130,7 @@ internal class NativeAlternativePaymentInteractor( _completion.update { Failure( ProcessOutResult.Failure( - code = POFailure.Code.Internal(), + code = Internal(), message = "Input parameters is missing in response." ).also { POLogger.warn("%s", it, attributes = logAttributes) } ) @@ -169,7 +169,7 @@ internal class NativeAlternativePaymentInteractor( _completion.update { Failure( ProcessOutResult.Failure( - code = POFailure.Code.Internal(), + code = Internal(), message = "Unknown input parameter type: ${parameter.rawType}" ).also { POLogger.error("%s", it, attributes = logAttributes) } ) @@ -341,7 +341,7 @@ internal class NativeAlternativePaymentInteractor( _completion.update { Failure( ProcessOutResult.Failure( - code = POFailure.Code.Cancelled, + code = Cancelled, message = "Cancelled by the user with secondary cancel action." ).also { POLogger.info("Cancelled: %s", it, attributes = logAttributes) } ) From f55688e7bb76da29b56ba24e16066c61dedb0dfd Mon Sep 17 00:00:00 2001 From: Vitalii Vanziak Date: Thu, 23 May 2024 20:11:52 +0300 Subject: [PATCH 10/38] 'logAttributes' for each log --- .../napm/NativeAlternativePaymentInteractor.kt | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 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 a9824702e..f0cf173fd 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 @@ -82,7 +82,7 @@ internal class NativeAlternativePaymentInteractor( ) } }.onFailure { failure -> - POLogger.info("Failed to fetch transaction details: %s", failure) + POLogger.info("Failed to fetch transaction details: %s", failure, attributes = logAttributes) _completion.update { Failure(failure) } } } @@ -213,7 +213,7 @@ internal class NativeAlternativePaymentInteractor( // } dispatch(DidStart) - POLogger.info("Started. Waiting for payment parameters.") + POLogger.info("Started. Waiting for payment parameters.", attributes = logAttributes) } private fun continueUserInput(stateValue: UserInputStateValue) { @@ -226,7 +226,7 @@ internal class NativeAlternativePaymentInteractor( ) } dispatch(DidSubmitParameters(additionalParametersExpected = true)) - POLogger.info("Submitted. Waiting for additional payment parameters.") + POLogger.info("Submitted. Waiting for additional payment parameters.", attributes = logAttributes) } private fun requestDefaultValues(parameters: List) { @@ -238,7 +238,10 @@ internal class NativeAlternativePaymentInteractor( ) latestDefaultValuesRequest = request eventDispatcher.send(request) - POLogger.debug("Requested to provide default values for payment parameters: %s", request) + POLogger.debug( + "Requested to provide default values for payment parameters: %s", request, + attributes = logAttributes + ) } } @@ -247,7 +250,10 @@ internal class NativeAlternativePaymentInteractor( eventDispatcher.defaultValuesResponse.collect { response -> if (response.uuid == latestDefaultValuesRequest?.uuid) { latestDefaultValuesRequest = null - POLogger.debug("Collected default values for payment parameters: %s", response) + POLogger.debug( + "Collected default values for payment parameters: %s", response, + attributes = logAttributes + ) _state.whenLoaded { stateValue -> startUserInput(stateValue.withDefaultValues(response.defaultValues)) } @@ -351,7 +357,7 @@ internal class NativeAlternativePaymentInteractor( private fun dispatch(event: PONativeAlternativePaymentMethodEvent) { interactorScope.launch { eventDispatcher.send(event) - POLogger.debug("Event has been sent: %s", event) + POLogger.debug("Event has been sent: %s", event, attributes = logAttributes) } } From 8dcb58dbab59f5868d14a17fc3d3058ef0f3f6c3 Mon Sep 17 00:00:00 2001 From: Vitalii Vanziak Date: Thu, 23 May 2024 22:00:26 +0300 Subject: [PATCH 11/38] VM field types --- .../napm/NativeAlternativePaymentViewModel.kt | 7 +++++++ .../NativeAlternativePaymentViewModelState.kt | 17 +++++++++++++++++ 2 files changed, 24 insertions(+) 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 9046e631a..152cd53b1 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 @@ -11,6 +11,7 @@ import com.processout.sdk.api.model.response.PONativeAlternativePaymentMethodTra import com.processout.sdk.core.retry.PORetryStrategy.Exponential import com.processout.sdk.core.util.POMarkdownUtils.escapedMarkdown import com.processout.sdk.ui.core.state.POActionState +import com.processout.sdk.ui.core.state.POImmutableList import com.processout.sdk.ui.napm.NativeAlternativePaymentInteractor.Companion.LOG_ATTRIBUTE_GATEWAY_CONFIGURATION_ID import com.processout.sdk.ui.napm.NativeAlternativePaymentInteractor.Companion.LOG_ATTRIBUTE_INVOICE_ID import com.processout.sdk.ui.napm.NativeAlternativePaymentInteractorState.* @@ -93,6 +94,8 @@ internal class NativeAlternativePaymentViewModel( private fun UserInput.userInput() = with(value) { NativeAlternativePaymentViewModelState.UserInput( title = options.title ?: app.getString(R.string.po_native_apm_title_format, gateway.displayName), + fields = fields.map(), + focusedFieldId = focusedFieldId, primaryAction = POActionState( id = primaryActionId, text = options.primaryActionText ?: invoice.formatPrimaryActionText(), @@ -131,6 +134,10 @@ internal class NativeAlternativePaymentViewModel( ) } + private fun List.map(): POImmutableList { + return POImmutableList(emptyList()) + } + private fun Invoice.formatPrimaryActionText() = try { val price = NumberFormat.getCurrencyInstance().apply { diff --git a/ui/src/main/kotlin/com/processout/sdk/ui/napm/NativeAlternativePaymentViewModelState.kt b/ui/src/main/kotlin/com/processout/sdk/ui/napm/NativeAlternativePaymentViewModelState.kt index e18aae4d3..28b0ed87b 100644 --- a/ui/src/main/kotlin/com/processout/sdk/ui/napm/NativeAlternativePaymentViewModelState.kt +++ b/ui/src/main/kotlin/com/processout/sdk/ui/napm/NativeAlternativePaymentViewModelState.kt @@ -2,14 +2,21 @@ package com.processout.sdk.ui.napm import androidx.compose.runtime.Immutable import com.processout.sdk.ui.core.state.POActionState +import com.processout.sdk.ui.core.state.POFieldState +import com.processout.sdk.ui.core.state.POImmutableList @Immutable internal sealed interface NativeAlternativePaymentViewModelState { + + //region States + data object Loading : NativeAlternativePaymentViewModelState @Immutable data class UserInput( val title: String, + val fields: POImmutableList, + val focusedFieldId: String?, val primaryAction: POActionState, val secondaryAction: POActionState?, val actionMessageMarkdown: String?, @@ -24,4 +31,14 @@ internal sealed interface NativeAlternativePaymentViewModelState { val secondaryAction: POActionState?, val isCaptured: Boolean ) : NativeAlternativePaymentViewModelState + + //endregion + + @Immutable + sealed interface Field { + data class TextField(val state: POFieldState) : Field + data class CodeField(val state: POFieldState) : Field + data class RadioField(val state: POFieldState) : Field + data class DropdownField(val state: POFieldState) : Field + } } From fdd310f2c070c023635fe4f2753b1e163f797387 Mon Sep 17 00:00:00 2001 From: Vitalii Vanziak Date: Fri, 24 May 2024 14:32:04 +0300 Subject: [PATCH 12/38] Fix log attributes --- .../NativeAlternativePaymentInteractor.kt | 28 ++++++++----------- 1 file changed, 11 insertions(+), 17 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 f0cf173fd..5fb88fba7 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 @@ -59,7 +59,7 @@ internal class NativeAlternativePaymentInteractor( private var latestDefaultValuesRequest: PONativeAlternativePaymentMethodDefaultValuesRequest? = null init { - POLogger.info("Starting native alternative payment.", attributes = logAttributes) + POLogger.info("Starting native alternative payment.") dispatch(WillStart) dispatchFailure() collectDefaultValues() @@ -82,7 +82,7 @@ internal class NativeAlternativePaymentInteractor( ) } }.onFailure { failure -> - POLogger.info("Failed to fetch transaction details: %s", failure, attributes = logAttributes) + POLogger.info("Failed to fetch transaction details: %s", failure) _completion.update { Failure(failure) } } } @@ -116,7 +116,7 @@ internal class NativeAlternativePaymentInteractor( ProcessOutResult.Failure( code = Generic(), message = "Payment has failed." - ).also { POLogger.info("%s", it, attributes = logAttributes) } + ).also { POLogger.info("%s", it) } ) } } @@ -132,7 +132,7 @@ internal class NativeAlternativePaymentInteractor( ProcessOutResult.Failure( code = Internal(), message = "Input parameters is missing in response." - ).also { POLogger.warn("%s", it, attributes = logAttributes) } + ).also { POLogger.error("%s", it, attributes = logAttributes) } ) } return @@ -213,7 +213,7 @@ internal class NativeAlternativePaymentInteractor( // } dispatch(DidStart) - POLogger.info("Started. Waiting for payment parameters.", attributes = logAttributes) + POLogger.info("Started. Waiting for payment parameters.") } private fun continueUserInput(stateValue: UserInputStateValue) { @@ -226,7 +226,7 @@ internal class NativeAlternativePaymentInteractor( ) } dispatch(DidSubmitParameters(additionalParametersExpected = true)) - POLogger.info("Submitted. Waiting for additional payment parameters.", attributes = logAttributes) + POLogger.info("Submitted. Waiting for additional payment parameters.") } private fun requestDefaultValues(parameters: List) { @@ -238,10 +238,7 @@ internal class NativeAlternativePaymentInteractor( ) latestDefaultValuesRequest = request eventDispatcher.send(request) - POLogger.debug( - "Requested to provide default values for payment parameters: %s", request, - attributes = logAttributes - ) + POLogger.debug("Requested to provide default values for payment parameters: %s", request) } } @@ -250,10 +247,7 @@ internal class NativeAlternativePaymentInteractor( eventDispatcher.defaultValuesResponse.collect { response -> if (response.uuid == latestDefaultValuesRequest?.uuid) { latestDefaultValuesRequest = null - POLogger.debug( - "Collected default values for payment parameters: %s", response, - attributes = logAttributes - ) + POLogger.debug("Collected default values for payment parameters: %s", response) _state.whenLoaded { stateValue -> startUserInput(stateValue.withDefaultValues(response.defaultValues)) } @@ -304,7 +298,7 @@ internal class NativeAlternativePaymentInteractor( ActionId.SUBMIT -> submit() ActionId.CANCEL -> cancel() } - is Dismiss -> POLogger.info("Dismissed: %s", event.failure, attributes = logAttributes) + is Dismiss -> POLogger.info("Dismissed: %s", event.failure) } } @@ -349,7 +343,7 @@ internal class NativeAlternativePaymentInteractor( ProcessOutResult.Failure( code = Cancelled, message = "Cancelled by the user with secondary cancel action." - ).also { POLogger.info("Cancelled: %s", it, attributes = logAttributes) } + ).also { POLogger.info("Cancelled: %s", it) } ) } } @@ -357,7 +351,7 @@ internal class NativeAlternativePaymentInteractor( private fun dispatch(event: PONativeAlternativePaymentMethodEvent) { interactorScope.launch { eventDispatcher.send(event) - POLogger.debug("Event has been sent: %s", event, attributes = logAttributes) + POLogger.debug("Event has been sent: %s", event) } } From 3ff2542f2fc9df09ad2d31fff9a95cbddce0b7b2 Mon Sep 17 00:00:00 2001 From: Vitalii Vanziak Date: Fri, 24 May 2024 14:51:14 +0300 Subject: [PATCH 13/38] updateFieldValues() --- .../napm/NativeAlternativePaymentInteractor.kt | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 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 5fb88fba7..93492de80 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 @@ -249,28 +249,28 @@ internal class NativeAlternativePaymentInteractor( latestDefaultValuesRequest = null POLogger.debug("Collected default values for payment parameters: %s", response) _state.whenLoaded { stateValue -> - startUserInput(stateValue.withDefaultValues(response.defaultValues)) + startUserInput(stateValue.updateFieldValues(response.defaultValues)) } _state.whenSubmitted { stateValue -> - continueUserInput(stateValue.withDefaultValues(response.defaultValues)) + continueUserInput(stateValue.updateFieldValues(response.defaultValues)) } } } } } - private fun UserInputStateValue.withDefaultValues( - defaultValues: Map + private fun UserInputStateValue.updateFieldValues( + values: Map ): UserInputStateValue { val updatedFields = fields.map { field -> - defaultValues.entries.find { it.key == field.id }?.let { - val defaultValue = field.length?.let { length -> + values.entries.find { it.key == field.id }?.let { + val value = field.length?.let { length -> it.value.take(length) } ?: it.value field.copy( value = TextFieldValue( - text = defaultValue, - selection = TextRange(defaultValue.length) + text = value, + selection = TextRange(value.length) ) ) } ?: field From baf4c2eb6674fe2df8c0e27f3200a9c9cc119c4e Mon Sep 17 00:00:00 2001 From: Vitalii Vanziak Date: Fri, 24 May 2024 15:13:48 +0300 Subject: [PATCH 14/38] Fix VM names --- .../napm/NativeAlternativePaymentViewModel.kt | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 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 152cd53b1..0dbfd3487 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 @@ -85,13 +85,13 @@ internal class NativeAlternativePaymentViewModel( state: NativeAlternativePaymentInteractorState ): NativeAlternativePaymentViewModelState = when (state) { Loading -> NativeAlternativePaymentViewModelState.Loading - is UserInput -> state.userInput() - is Capturing -> state.capture() - is Captured -> state.capture() + is UserInput -> state.toUserInput() + is Capturing -> state.toCapture() + is Captured -> state.toCapture() else -> this@NativeAlternativePaymentViewModel.state.value } - private fun UserInput.userInput() = with(value) { + private fun UserInput.toUserInput() = with(value) { NativeAlternativePaymentViewModelState.UserInput( title = options.title ?: app.getString(R.string.po_native_apm_title_format, gateway.displayName), fields = fields.map(), @@ -103,7 +103,7 @@ internal class NativeAlternativePaymentViewModel( enabled = submitAllowed, loading = submitting ), - secondaryAction = options.secondaryAction?.state( + secondaryAction = options.secondaryAction?.toActionState( id = secondaryActionId, enabled = !submitting ), @@ -113,11 +113,11 @@ internal class NativeAlternativePaymentViewModel( ) } - private fun Capturing.capture() = with(value) { + private fun Capturing.toCapture() = with(value) { NativeAlternativePaymentViewModelState.Capture( paymentProviderName = paymentProviderName, logoUrl = logoUrl, - secondaryAction = options.paymentConfirmation.secondaryAction?.state( + secondaryAction = options.paymentConfirmation.secondaryAction?.toActionState( id = secondaryActionId, enabled = true ), @@ -125,7 +125,7 @@ internal class NativeAlternativePaymentViewModel( ) } - private fun Captured.capture() = with(value) { + private fun Captured.toCapture() = with(value) { NativeAlternativePaymentViewModelState.Capture( paymentProviderName = paymentProviderName, logoUrl = logoUrl, @@ -148,7 +148,7 @@ internal class NativeAlternativePaymentViewModel( app.getString(R.string.po_native_apm_submit_button_text) } - private fun SecondaryAction.state( + private fun SecondaryAction.toActionState( id: String, enabled: Boolean ): POActionState = when (this) { From 03df32d4efa9d9813a2095c40e7f33009f5b13af Mon Sep 17 00:00:00 2001 From: Vitalii Vanziak Date: Fri, 24 May 2024 16:39:25 +0300 Subject: [PATCH 15/38] Map fields --- .../sdk/ui/core/state/POFieldState.kt | 1 + .../napm/NativeAlternativePaymentViewModel.kt | 72 ++++++++++++++++++- 2 files changed, 72 insertions(+), 1 deletion(-) diff --git a/ui-core/src/main/kotlin/com/processout/sdk/ui/core/state/POFieldState.kt b/ui-core/src/main/kotlin/com/processout/sdk/ui/core/state/POFieldState.kt index bf90038ab..1fe8eac2d 100644 --- a/ui-core/src/main/kotlin/com/processout/sdk/ui/core/state/POFieldState.kt +++ b/ui-core/src/main/kotlin/com/processout/sdk/ui/core/state/POFieldState.kt @@ -15,6 +15,7 @@ data class POFieldState( val id: String, val value: TextFieldValue = TextFieldValue(), val availableValues: POImmutableList? = null, + val length: Int? = null, val title: String? = null, val description: String? = null, val placeholder: String? = null, 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 0dbfd3487..1a687a9b1 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 @@ -7,14 +7,18 @@ import androidx.lifecycle.viewModelScope import com.processout.sdk.R import com.processout.sdk.api.ProcessOut import com.processout.sdk.api.dispatcher.napm.PODefaultNativeAlternativePaymentMethodEventDispatcher +import com.processout.sdk.api.model.response.PONativeAlternativePaymentMethodParameter.ParameterType.NUMERIC +import com.processout.sdk.api.model.response.PONativeAlternativePaymentMethodParameter.ParameterType.SINGLE_SELECT import com.processout.sdk.api.model.response.PONativeAlternativePaymentMethodTransactionDetails.Invoice import com.processout.sdk.core.retry.PORetryStrategy.Exponential import com.processout.sdk.core.util.POMarkdownUtils.escapedMarkdown import com.processout.sdk.ui.core.state.POActionState +import com.processout.sdk.ui.core.state.POFieldState import com.processout.sdk.ui.core.state.POImmutableList import com.processout.sdk.ui.napm.NativeAlternativePaymentInteractor.Companion.LOG_ATTRIBUTE_GATEWAY_CONFIGURATION_ID import com.processout.sdk.ui.napm.NativeAlternativePaymentInteractor.Companion.LOG_ATTRIBUTE_INVOICE_ID import com.processout.sdk.ui.napm.NativeAlternativePaymentInteractorState.* +import com.processout.sdk.ui.napm.NativeAlternativePaymentViewModelState.Field.* import com.processout.sdk.ui.napm.PONativeAlternativePaymentConfiguration.Options import com.processout.sdk.ui.napm.PONativeAlternativePaymentConfiguration.PaymentConfirmationConfiguration.Companion.DEFAULT_TIMEOUT_SECONDS import com.processout.sdk.ui.napm.PONativeAlternativePaymentConfiguration.PaymentConfirmationConfiguration.Companion.MAX_TIMEOUT_SECONDS @@ -71,6 +75,11 @@ internal class NativeAlternativePaymentViewModel( ) } + private companion object { + const val CODE_FIELD_LENGTH_MIN = 1 + const val CODE_FIELD_LENGTH_MAX = 6 + } + val completion = interactor.completion val state = interactor.state.map(viewModelScope, ::map) @@ -135,9 +144,70 @@ internal class NativeAlternativePaymentViewModel( } private fun List.map(): POImmutableList { - return POImmutableList(emptyList()) + val fields = map { field -> + when (field.type) { + NUMERIC -> if (field.length in CODE_FIELD_LENGTH_MIN..CODE_FIELD_LENGTH_MAX) { + field.toCodeField() + } else { + field.toTextField() + } + SINGLE_SELECT -> { + val availableValuesCount = field.availableValues?.size ?: 0 + if (availableValuesCount <= options.inlineSingleSelectValuesLimit) { + field.toRadioField() + } else { + field.toDropdownField() + } + } + else -> field.toTextField() + } + } + return POImmutableList(fields) } + private fun Field.toTextField(): NativeAlternativePaymentViewModelState.Field = + TextField( + POFieldState( + id = id, + value = value, + title = displayName, + isError = !isValid + ) + ) + + private fun Field.toCodeField(): NativeAlternativePaymentViewModelState.Field = + CodeField( + POFieldState( + id = id, + value = value, + length = length, + title = displayName, + isError = !isValid + ) + ) + + private fun Field.toRadioField(): NativeAlternativePaymentViewModelState.Field = + RadioField( + POFieldState( + id = id, + value = value, + availableValues = availableValues?.let { POImmutableList(it) }, + title = displayName, + isError = !isValid + ) + ) + + private fun Field.toDropdownField(): NativeAlternativePaymentViewModelState.Field = + DropdownField( + POFieldState( + id = id, + value = value, + availableValues = availableValues?.let { POImmutableList(it) }, + title = displayName, + isError = !isValid + ) + ) + private fun Invoice.formatPrimaryActionText() = try { val price = NumberFormat.getCurrencyInstance().apply { From a152b259697b50f1d864b412b61e8ee67bae6684 Mon Sep 17 00:00:00 2001 From: Vitalii Vanziak Date: Fri, 24 May 2024 19:26:13 +0300 Subject: [PATCH 16/38] TextField composable --- .../ui/napm/NativeAlternativePaymentScreen.kt | 145 ++++++++++++++++-- 1 file changed, 135 insertions(+), 10 deletions(-) diff --git a/ui/src/main/kotlin/com/processout/sdk/ui/napm/NativeAlternativePaymentScreen.kt b/ui/src/main/kotlin/com/processout/sdk/ui/napm/NativeAlternativePaymentScreen.kt index 31e54814f..4ea9de91e 100644 --- a/ui/src/main/kotlin/com/processout/sdk/ui/napm/NativeAlternativePaymentScreen.kt +++ b/ui/src/main/kotlin/com/processout/sdk/ui/napm/NativeAlternativePaymentScreen.kt @@ -8,10 +8,7 @@ import androidx.compose.animation.core.LinearEasing import androidx.compose.animation.core.MutableTransitionState import androidx.compose.animation.core.tween import androidx.compose.foundation.Image -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.* import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material3.Scaffold @@ -21,28 +18,34 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.graphics.Color import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.platform.rememberNestedScrollInteropConnection import androidx.compose.ui.res.colorResource import androidx.compose.ui.res.painterResource +import androidx.lifecycle.Lifecycle import com.processout.sdk.ui.R -import com.processout.sdk.ui.core.component.POActionsContainer -import com.processout.sdk.ui.core.component.POCircularProgressIndicator -import com.processout.sdk.ui.core.component.POHeader -import com.processout.sdk.ui.core.component.POText +import com.processout.sdk.ui.core.component.* import com.processout.sdk.ui.core.component.field.POField +import com.processout.sdk.ui.core.component.field.POFieldLabels import com.processout.sdk.ui.core.component.field.code.POCodeField import com.processout.sdk.ui.core.component.field.dropdown.PODropdownField import com.processout.sdk.ui.core.component.field.radio.PORadioGroup +import com.processout.sdk.ui.core.component.field.text.POLabeledTextField import com.processout.sdk.ui.core.state.POActionState +import com.processout.sdk.ui.core.state.POFieldState import com.processout.sdk.ui.core.state.POImmutableList import com.processout.sdk.ui.core.style.POAxis import com.processout.sdk.ui.core.theme.ProcessOutTheme -import com.processout.sdk.ui.napm.NativeAlternativePaymentEvent.Action +import com.processout.sdk.ui.napm.NativeAlternativePaymentEvent.* import com.processout.sdk.ui.napm.NativeAlternativePaymentScreen.AnimationDurationMillis import com.processout.sdk.ui.napm.NativeAlternativePaymentScreen.animatedBackgroundColor import com.processout.sdk.ui.napm.NativeAlternativePaymentViewModelState.* +import com.processout.sdk.ui.napm.NativeAlternativePaymentViewModelState.Field.* +import com.processout.sdk.ui.shared.composable.rememberLifecycleEvent @Composable internal fun NativeAlternativePaymentScreen( @@ -113,11 +116,133 @@ private fun UserInput( ) { AnimatedVisibility { Column { - // TODO + val lifecycleEvent = rememberLifecycleEvent() + val labelsStyle = remember { + POFieldLabels.Style( + title = style.label, + description = style.errorMessage + ) + } + val isPrimaryActionEnabled = with(state.primaryAction) { enabled && !loading } + state.fields.elements.forEach { field -> + when (field) { + is TextField -> TextField( + state = field.state, + onEvent = onEvent, + lifecycleEvent = lifecycleEvent, + focusedFieldId = state.focusedFieldId, + isPrimaryActionEnabled = isPrimaryActionEnabled, + fieldStyle = style.field, + labelsStyle = labelsStyle, + modifier = Modifier.fillMaxWidth() + ) + is CodeField -> CodeField( + state = field.state, + onEvent = onEvent, + lifecycleEvent = lifecycleEvent, + focusedFieldId = state.focusedFieldId, + isPrimaryActionEnabled = isPrimaryActionEnabled, + style = style.codeField + ) + is RadioField -> RadioField( + state = field.state, + onEvent = onEvent + ) + is DropdownField -> DropdownField( + state = field.state, + onEvent = onEvent + ) + } + } } } } +@Composable +private fun TextField( + state: POFieldState, + onEvent: (NativeAlternativePaymentEvent) -> Unit, + lifecycleEvent: Lifecycle.Event, + focusedFieldId: String?, + isPrimaryActionEnabled: Boolean, + fieldStyle: POField.Style, + labelsStyle: POFieldLabels.Style, + modifier: Modifier = Modifier +) { + val focusRequester = remember { FocusRequester() } + POLabeledTextField( + value = state.value, + onValueChange = { + onEvent( + FieldValueChanged( + id = state.id, + value = state.inputFilter?.filter(it) ?: it + ) + ) + }, + title = state.title ?: String(), + description = state.description, + modifier = modifier + .focusRequester(focusRequester) + .onFocusChanged { + onEvent( + FieldFocusChanged( + id = state.id, + isFocused = it.isFocused + ) + ) + }, + fieldStyle = fieldStyle, + labelsStyle = labelsStyle, + enabled = state.enabled, + isError = state.isError, + forceTextDirectionLtr = state.forceTextDirectionLtr, + placeholderText = state.placeholder, + visualTransformation = state.visualTransformation, + keyboardOptions = state.keyboardOptions, + keyboardActions = POField.keyboardActions( + imeAction = state.keyboardOptions.imeAction, + actionId = state.keyboardActionId, + enabled = isPrimaryActionEnabled, + onClick = { onEvent(Action(id = it)) } + ) + ) + if (state.id == focusedFieldId && lifecycleEvent == Lifecycle.Event.ON_RESUME) { + PORequestFocus(focusRequester, lifecycleEvent) + } +} + +@Composable +private fun CodeField( + state: POFieldState, + onEvent: (NativeAlternativePaymentEvent) -> Unit, + lifecycleEvent: Lifecycle.Event, + focusedFieldId: String?, + isPrimaryActionEnabled: Boolean, + style: POField.Style, + modifier: Modifier = Modifier +) { + // TODO +} + +@Composable +private fun RadioField( + state: POFieldState, + onEvent: (NativeAlternativePaymentEvent) -> Unit, + modifier: Modifier = Modifier +) { + // TODO +} + +@Composable +private fun DropdownField( + state: POFieldState, + onEvent: (NativeAlternativePaymentEvent) -> Unit, + modifier: Modifier = Modifier +) { + // TODO +} + @Composable private fun Capture( state: Capture, From a8c3fcc46da7ae2970ea2fc313d34230a1dd2281 Mon Sep 17 00:00:00 2001 From: Vitalii Vanziak Date: Fri, 24 May 2024 20:15:27 +0300 Subject: [PATCH 17/38] updateFieldValue() --- .../NativeAlternativePaymentInteractor.kt | 36 ++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) 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 93492de80..9a6b5724a 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 @@ -303,7 +303,41 @@ internal class NativeAlternativePaymentInteractor( } private fun updateFieldValue(id: String, value: TextFieldValue) { - // TODO + _state.whenUserInput { stateValue -> + val previousValue = stateValue.fields.find { it.id == id }?.value ?: TextFieldValue() + val isTextChanged = value.text != previousValue.text + _state.update { + UserInput( + stateValue.copy( + fields = stateValue.fields.map { field -> + // TODO: remove field error message when edited? + updatedField(id, value, field, isTextChanged) + } + ) + ) + } + if (isTextChanged) { + POLogger.debug("Field is edited by the user: %s", id) + dispatch(ParametersChanged) + // TODO: allow submit if all fields valid? + } + } + } + + private fun updatedField( + id: String, + value: TextFieldValue, + field: Field, + isTextChanged: Boolean + ): Field { + if (field.id != id) { + return field + } + return if (isTextChanged) { + field.copy(value = value, isValid = true) + } else { + field.copy(value = value) + } } private fun updateFieldFocus(id: String, isFocused: Boolean) { From e73f91ef659cf9bbb6a1900e19354eb32e108a17 Mon Sep 17 00:00:00 2001 From: Vitalii Vanziak Date: Fri, 24 May 2024 22:18:47 +0300 Subject: [PATCH 18/38] CodeField composable --- .../core/component/field/code/POCodeField.kt | 4 +- .../ui/napm/NativeAlternativePaymentScreen.kt | 43 +++++++++++++++++-- 2 files changed, 42 insertions(+), 5 deletions(-) diff --git a/ui-core/src/main/kotlin/com/processout/sdk/ui/core/component/field/code/POCodeField.kt b/ui-core/src/main/kotlin/com/processout/sdk/ui/core/component/field/code/POCodeField.kt index f51384a6d..3bf34fd7c 100644 --- a/ui-core/src/main/kotlin/com/processout/sdk/ui/core/component/field/code/POCodeField.kt +++ b/ui-core/src/main/kotlin/com/processout/sdk/ui/core/component/field/code/POCodeField.kt @@ -245,8 +245,8 @@ object POCodeField { ) } - internal val LengthMin = 1 - internal val LengthMax = 6 + val LengthMin = 1 + val LengthMax = 6 internal fun validLength(length: Int): Int { if (length in LengthMin..LengthMax) { diff --git a/ui/src/main/kotlin/com/processout/sdk/ui/napm/NativeAlternativePaymentScreen.kt b/ui/src/main/kotlin/com/processout/sdk/ui/napm/NativeAlternativePaymentScreen.kt index 4ea9de91e..a27f0f350 100644 --- a/ui/src/main/kotlin/com/processout/sdk/ui/napm/NativeAlternativePaymentScreen.kt +++ b/ui/src/main/kotlin/com/processout/sdk/ui/napm/NativeAlternativePaymentScreen.kt @@ -32,6 +32,7 @@ import com.processout.sdk.ui.core.component.* import com.processout.sdk.ui.core.component.field.POField import com.processout.sdk.ui.core.component.field.POFieldLabels import com.processout.sdk.ui.core.component.field.code.POCodeField +import com.processout.sdk.ui.core.component.field.code.POLabeledCodeField import com.processout.sdk.ui.core.component.field.dropdown.PODropdownField import com.processout.sdk.ui.core.component.field.radio.PORadioGroup import com.processout.sdk.ui.core.component.field.text.POLabeledTextField @@ -142,7 +143,8 @@ private fun UserInput( lifecycleEvent = lifecycleEvent, focusedFieldId = state.focusedFieldId, isPrimaryActionEnabled = isPrimaryActionEnabled, - style = style.codeField + fieldStyle = style.codeField, + labelsStyle = labelsStyle ) is RadioField -> RadioField( state = field.state, @@ -219,10 +221,45 @@ private fun CodeField( lifecycleEvent: Lifecycle.Event, focusedFieldId: String?, isPrimaryActionEnabled: Boolean, - style: POField.Style, + fieldStyle: POField.Style, + labelsStyle: POFieldLabels.Style, modifier: Modifier = Modifier ) { - // TODO + POLabeledCodeField( + value = state.value, + onValueChange = { + onEvent( + FieldValueChanged( + id = state.id, + value = it + ) + ) + }, + title = state.title ?: String(), + description = state.description, + modifier = modifier + .onFocusChanged { + onEvent( + FieldFocusChanged( + id = state.id, + isFocused = it.isFocused + ) + ) + }, + fieldStyle = fieldStyle, + labelsStyle = labelsStyle, + length = state.length ?: POCodeField.LengthMax, + isError = state.isError, + isFocused = state.id == focusedFieldId, + lifecycleEvent = lifecycleEvent, + keyboardOptions = state.keyboardOptions, + keyboardActions = POField.keyboardActions( + imeAction = state.keyboardOptions.imeAction, + actionId = state.keyboardActionId, + enabled = isPrimaryActionEnabled, + onClick = { onEvent(Action(id = it)) } + ) + ) } @Composable From 29bd0bee9f38a5055652e7a6f6671e8399c39b09 Mon Sep 17 00:00:00 2001 From: Vitalii Vanziak Date: Sun, 26 May 2024 17:41:53 +0300 Subject: [PATCH 19/38] libphonenumber --- build.gradle | 1 + ui/build.gradle | 1 + 2 files changed, 2 insertions(+) diff --git a/build.gradle b/build.gradle index 988f52a3c..8fd7fd87e 100644 --- a/build.gradle +++ b/build.gradle @@ -59,6 +59,7 @@ ext { okioVersion = '3.9.0' coilVersion = '2.6.0' commonMarkVersion = '0.22.0' + libphonenumberVersion = '8.13.37' checkout3dsSdkVersion = '3.2.2' adyen3dsSdkVersion = '2.2.15' diff --git a/ui/build.gradle b/ui/build.gradle index 00d27144c..cb5950ce1 100644 --- a/ui/build.gradle +++ b/ui/build.gradle @@ -94,6 +94,7 @@ dependencies { api "com.google.android.gms:play-services-wallet:$gmsWalletVersion" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-play-services:$kotlinxCoroutinesPlayServicesVersion" + implementation "com.googlecode.libphonenumber:libphonenumber:$libphonenumberVersion" ksp "com.squareup.moshi:moshi-kotlin-codegen:$moshiVersion" } From 5f0d0ff76e7157989151ff6a0ba39ab6795459be Mon Sep 17 00:00:00 2001 From: Vitalii Vanziak Date: Sun, 26 May 2024 19:00:47 +0300 Subject: [PATCH 20/38] PhoneNumberVisualTransformation --- .../PhoneNumberVisualTransformation.kt | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 ui/src/main/kotlin/com/processout/sdk/ui/shared/transformation/PhoneNumberVisualTransformation.kt diff --git a/ui/src/main/kotlin/com/processout/sdk/ui/shared/transformation/PhoneNumberVisualTransformation.kt b/ui/src/main/kotlin/com/processout/sdk/ui/shared/transformation/PhoneNumberVisualTransformation.kt new file mode 100644 index 000000000..b2c7f08a9 --- /dev/null +++ b/ui/src/main/kotlin/com/processout/sdk/ui/shared/transformation/PhoneNumberVisualTransformation.kt @@ -0,0 +1,57 @@ +package com.processout.sdk.ui.shared.transformation + +import android.telephony.PhoneNumberUtils +import android.text.Selection +import com.google.i18n.phonenumbers.PhoneNumberUtil +import java.util.Locale + +internal class PhoneNumberVisualTransformation( + private val countryCode: String = Locale.getDefault().country +) : BaseVisualTransformation() { + + private val formatter = PhoneNumberUtil.getInstance().getAsYouTypeFormatter(countryCode) + + override fun transform(text: String): String { + formatter.clear() + var formatted: String? = null + val cursorIndex = Selection.getSelectionEnd(text) - 1 + var lastNonSeparator = 0.toChar() + var hasCursor = false + text.forEachIndexed { index, char -> + if (PhoneNumberUtils.isNonSeparator(char)) { + if (lastNonSeparator.code != 0) { + formatted = formatted(lastNonSeparator, hasCursor) + hasCursor = false + } + lastNonSeparator = char + } + if (index == cursorIndex) { + hasCursor = true + } + } + if (lastNonSeparator.code != 0) { + formatted = formatted(lastNonSeparator, hasCursor) + } + return formatted ?: text + } + + private fun formatted(lastNonSeparator: Char, hasCursor: Boolean) = + if (hasCursor) { + formatter.inputDigitAndRememberPosition(lastNonSeparator) + } else { + formatter.inputDigit(lastNonSeparator) + } + + override fun isSeparator(char: Char) = !PhoneNumberUtils.isNonSeparator(char) + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + other as PhoneNumberVisualTransformation + return countryCode == other.countryCode + } + + override fun hashCode(): Int { + return countryCode.hashCode() + } +} From 2da0d064ef9f698e7ed4c4f4d9be7365e4de6747 Mon Sep 17 00:00:00 2001 From: Vitalii Vanziak Date: Sun, 26 May 2024 22:31:27 +0300 Subject: [PATCH 21/38] PhoneNumberInputFilter --- .../shared/filter/PhoneNumberInputFilter.kt | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 ui/src/main/kotlin/com/processout/sdk/ui/shared/filter/PhoneNumberInputFilter.kt diff --git a/ui/src/main/kotlin/com/processout/sdk/ui/shared/filter/PhoneNumberInputFilter.kt b/ui/src/main/kotlin/com/processout/sdk/ui/shared/filter/PhoneNumberInputFilter.kt new file mode 100644 index 000000000..007936e5b --- /dev/null +++ b/ui/src/main/kotlin/com/processout/sdk/ui/shared/filter/PhoneNumberInputFilter.kt @@ -0,0 +1,40 @@ +package com.processout.sdk.ui.shared.filter + +import androidx.compose.ui.text.TextRange +import androidx.compose.ui.text.input.TextFieldValue +import com.processout.sdk.ui.core.filter.POInputFilter + +internal class PhoneNumberInputFilter : POInputFilter { + + override fun filter(value: TextFieldValue): TextFieldValue { + var filtered = value.text.filter { it.isDigit() } + filtered = "+$filtered" + val lengthDiff = value.text.length - filtered.length + val selection = with(value) { + if (selection.length == 0) { + if (selection.start == text.length) { + TextRange(filtered.length) + } else if (selection.start == 0) { + TextRange(selection.start + 1) + } else if (lengthDiff > 0) { + TextRange(selection.start - lengthDiff) + } else { + selection + } + } else { + selection + } + } + return value.copy(text = filtered, selection = selection) + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + return true + } + + override fun hashCode(): Int { + return javaClass.hashCode() + } +} From 7c59193f00d72a6d92d3026ee9688cd34ece34b8 Mon Sep 17 00:00:00 2001 From: Vitalii Vanziak Date: Mon, 27 May 2024 12:36:34 +0300 Subject: [PATCH 22/38] Apply phone number filter and transformation --- .../sdk/ui/napm/NativeAlternativePaymentViewModel.kt | 10 +++++++--- 1 file changed, 7 insertions(+), 3 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 1a687a9b1..aa2e3cf79 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 @@ -1,14 +1,14 @@ package com.processout.sdk.ui.napm import android.app.Application +import androidx.compose.ui.text.input.VisualTransformation import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewModelScope import com.processout.sdk.R import com.processout.sdk.api.ProcessOut import com.processout.sdk.api.dispatcher.napm.PODefaultNativeAlternativePaymentMethodEventDispatcher -import com.processout.sdk.api.model.response.PONativeAlternativePaymentMethodParameter.ParameterType.NUMERIC -import com.processout.sdk.api.model.response.PONativeAlternativePaymentMethodParameter.ParameterType.SINGLE_SELECT +import com.processout.sdk.api.model.response.PONativeAlternativePaymentMethodParameter.ParameterType.* import com.processout.sdk.api.model.response.PONativeAlternativePaymentMethodTransactionDetails.Invoice import com.processout.sdk.core.retry.PORetryStrategy.Exponential import com.processout.sdk.core.util.POMarkdownUtils.escapedMarkdown @@ -24,6 +24,8 @@ import com.processout.sdk.ui.napm.PONativeAlternativePaymentConfiguration.Paymen import com.processout.sdk.ui.napm.PONativeAlternativePaymentConfiguration.PaymentConfirmationConfiguration.Companion.MAX_TIMEOUT_SECONDS import com.processout.sdk.ui.napm.PONativeAlternativePaymentConfiguration.SecondaryAction import com.processout.sdk.ui.shared.extension.map +import com.processout.sdk.ui.shared.filter.PhoneNumberInputFilter +import com.processout.sdk.ui.shared.transformation.PhoneNumberVisualTransformation import java.text.NumberFormat import java.util.Currency @@ -171,7 +173,9 @@ internal class NativeAlternativePaymentViewModel( id = id, value = value, title = displayName, - isError = !isValid + isError = !isValid, + inputFilter = if (type == PHONE) PhoneNumberInputFilter() else null, + visualTransformation = if (type == PHONE) PhoneNumberVisualTransformation() else VisualTransformation.None ) ) From 350ea263f2b0ec854abd84250d28eefdc54efb68 Mon Sep 17 00:00:00 2001 From: Vitalii Vanziak Date: Mon, 27 May 2024 13:23:40 +0300 Subject: [PATCH 23/38] Force POCodeField layout direction to LTR --- .../core/component/field/code/POCodeField.kt | 212 +++++++++--------- 1 file changed, 106 insertions(+), 106 deletions(-) diff --git a/ui-core/src/main/kotlin/com/processout/sdk/ui/core/component/field/code/POCodeField.kt b/ui-core/src/main/kotlin/com/processout/sdk/ui/core/component/field/code/POCodeField.kt index 3bf34fd7c..4d93d070e 100644 --- a/ui-core/src/main/kotlin/com/processout/sdk/ui/core/component/field/code/POCodeField.kt +++ b/ui-core/src/main/kotlin/com/processout/sdk/ui/core/component/field/code/POCodeField.kt @@ -17,13 +17,11 @@ import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.input.key.* -import androidx.compose.ui.platform.LocalClipboardManager -import androidx.compose.ui.platform.LocalFocusManager -import androidx.compose.ui.platform.LocalTextToolbar -import androidx.compose.ui.platform.LocalView +import androidx.compose.ui.platform.* import androidx.compose.ui.text.TextRange import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.LayoutDirection import androidx.lifecycle.Lifecycle import com.processout.sdk.ui.core.annotation.ProcessOutInternalApi import com.processout.sdk.ui.core.component.PORequestFocus @@ -50,124 +48,126 @@ fun POCodeField( keyboardOptions: KeyboardOptions = KeyboardOptions.Default, keyboardActions: KeyboardActions = KeyboardActions.Default ) { - Row( - modifier = Modifier - .fillMaxWidth() - .focusGroup(), - horizontalArrangement = Arrangement.spacedBy( - space = ProcessOutTheme.spacing.small, - alignment = horizontalAlignment - ), - verticalAlignment = Alignment.CenterVertically - ) { - val validLength by remember { mutableIntStateOf(validLength(length)) } - var values by remember { mutableStateOf(values(value.text, validLength)) } - var focusedIndex by remember { mutableIntStateOf(values.focusedIndex()) } - val clipboardManager = LocalClipboardManager.current - CompositionLocalProvider( - LocalTextToolbar provides ProcessOutTextToolbar( - view = LocalView.current, - onPasteRequested = { - if (clipboardManager.hasText()) { - val pastedValues = values( - text = clipboardManager.getText()?.text ?: String(), - length = validLength - ) - if (!pastedValues.all { it.text.isEmpty() }) { - values = pastedValues - focusedIndex = values.focusedIndex() - onValueChange(values.textFieldValue()) - } - } - }, - hideUnspecifiedActions = true - ) + CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Ltr) { + Row( + modifier = Modifier + .fillMaxWidth() + .focusGroup(), + horizontalArrangement = Arrangement.spacedBy( + space = ProcessOutTheme.spacing.small, + alignment = horizontalAlignment + ), + verticalAlignment = Alignment.CenterVertically ) { - val focusManager = LocalFocusManager.current - for (textFieldIndex in values.indices) { - val focusRequester = remember { FocusRequester() } - POTextField( - value = values.getOrNull(textFieldIndex) ?: TextFieldValue(), - onValueChange = { updatedValue -> - if (updatedValue.selection.length == 0) { - val filteredText = updatedValue.text.filter { it.isDigit() } - values = values.mapIndexed { valueIndex, textFieldValue -> - if (valueIndex == textFieldIndex) { - val updatedText = filteredText.firstOrNull()?.toString() ?: String() - val isTextChanged = textFieldValue.text != updatedText - TextFieldValue( - text = updatedText, - selection = if (isTextChanged) { - TextRange(updatedText.length) - } else { - updatedValue.selection - } - ) - } else { - textFieldValue.copy(selection = TextRange.Zero) - } + val validLength by remember { mutableIntStateOf(validLength(length)) } + var values by remember { mutableStateOf(values(value.text, validLength)) } + var focusedIndex by remember { mutableIntStateOf(values.focusedIndex()) } + val clipboardManager = LocalClipboardManager.current + CompositionLocalProvider( + LocalTextToolbar provides ProcessOutTextToolbar( + view = LocalView.current, + onPasteRequested = { + if (clipboardManager.hasText()) { + val pastedValues = values( + text = clipboardManager.getText()?.text ?: String(), + length = validLength + ) + if (!pastedValues.all { it.text.isEmpty() }) { + values = pastedValues + focusedIndex = values.focusedIndex() + onValueChange(values.textFieldValue()) } - if (textFieldIndex != values.lastIndex && - filteredText.length == 2 && - updatedValue.selection.start == 2 - ) { - val nextText = filteredText.last().toString() - values = values.mapIndexed { index, textFieldValue -> - if (index == textFieldIndex + 1) { + } + }, + hideUnspecifiedActions = true + ) + ) { + val focusManager = LocalFocusManager.current + for (textFieldIndex in values.indices) { + val focusRequester = remember { FocusRequester() } + POTextField( + value = values.getOrNull(textFieldIndex) ?: TextFieldValue(), + onValueChange = { updatedValue -> + if (updatedValue.selection.length == 0) { + val filteredText = updatedValue.text.filter { it.isDigit() } + values = values.mapIndexed { valueIndex, textFieldValue -> + if (valueIndex == textFieldIndex) { + val updatedText = filteredText.firstOrNull()?.toString() ?: String() + val isTextChanged = textFieldValue.text != updatedText TextFieldValue( - text = nextText, - selection = TextRange(nextText.length) + text = updatedText, + selection = if (isTextChanged) { + TextRange(updatedText.length) + } else { + updatedValue.selection + } ) } else { textFieldValue.copy(selection = TextRange.Zero) } } + if (textFieldIndex != values.lastIndex && + filteredText.length == 2 && + updatedValue.selection.start == 2 + ) { + val nextText = filteredText.last().toString() + values = values.mapIndexed { index, textFieldValue -> + if (index == textFieldIndex + 1) { + TextFieldValue( + text = nextText, + selection = TextRange(nextText.length) + ) + } else { + textFieldValue.copy(selection = TextRange.Zero) + } + } + } + onValueChange(values.textFieldValue()) } - onValueChange(values.textFieldValue()) - } - }, - modifier = modifier - .requiredWidth(ProcessOutTheme.dimensions.formComponentHeight) - .onPreviewKeyEvent { - when { - it.key == Key.Backspace && it.type == KeyEventType.KeyDown -> { - if (textFieldIndex != 0 && values[textFieldIndex].selection.start == 0) { - values = values.mapIndexed { index, textFieldValue -> - if (index == textFieldIndex - 1) { - TextFieldValue() - } else { - textFieldValue.copy(selection = TextRange.Zero) + }, + modifier = modifier + .requiredWidth(ProcessOutTheme.dimensions.formComponentHeight) + .onPreviewKeyEvent { + when { + it.key == Key.Backspace && it.type == KeyEventType.KeyDown -> { + if (textFieldIndex != 0 && values[textFieldIndex].selection.start == 0) { + values = values.mapIndexed { index, textFieldValue -> + if (index == textFieldIndex - 1) { + TextFieldValue() + } else { + textFieldValue.copy(selection = TextRange.Zero) + } } + focusManager.moveFocus(FocusDirection.Previous) + onValueChange(values.textFieldValue()) } - focusManager.moveFocus(FocusDirection.Previous) - onValueChange(values.textFieldValue()) + false } - false - } - else -> { - if (it.type == KeyEventType.KeyDown && textFieldIndex != values.lastIndex) { - focusManager.moveFocus(FocusDirection.Next) + else -> { + if (it.type == KeyEventType.KeyDown && textFieldIndex != values.lastIndex) { + focusManager.moveFocus(FocusDirection.Next) + } + false } - false } } + .focusRequester(focusRequester) + .onFocusChanged { + if (it.isFocused) { + focusedIndex = textFieldIndex + } + }, + style = style(style), + isError = isError, + keyboardOptions = keyboardOptions, + keyboardActions = keyboardActions + ) + if (isFocused && textFieldIndex == focusedIndex) { + if (lifecycleEvent == Lifecycle.Event.ON_RESUME) { + PORequestFocus(focusRequester, lifecycleEvent) + } else { + PORequestFocus(focusRequester) } - .focusRequester(focusRequester) - .onFocusChanged { - if (it.isFocused) { - focusedIndex = textFieldIndex - } - }, - style = style(style), - isError = isError, - keyboardOptions = keyboardOptions, - keyboardActions = keyboardActions - ) - if (isFocused && textFieldIndex == focusedIndex) { - if (lifecycleEvent == Lifecycle.Event.ON_RESUME) { - PORequestFocus(focusRequester, lifecycleEvent) - } else { - PORequestFocus(focusRequester) } } } From a27a515bf16e8c2ddc6eed07e4a09d6e2bb3ff33 Mon Sep 17 00:00:00 2001 From: Vitalii Vanziak Date: Mon, 27 May 2024 15:22:51 +0300 Subject: [PATCH 24/38] Fields keyboard configuration --- .../napm/NativeAlternativePaymentViewModel.kt | 82 +++++++++++++++++-- 1 file changed, 75 insertions(+), 7 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 aa2e3cf79..e9d6f43f7 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 @@ -1,6 +1,10 @@ package com.processout.sdk.ui.napm import android.app.Application +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardCapitalization +import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.VisualTransformation import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider @@ -8,6 +12,7 @@ import androidx.lifecycle.viewModelScope import com.processout.sdk.R import com.processout.sdk.api.ProcessOut import com.processout.sdk.api.dispatcher.napm.PODefaultNativeAlternativePaymentMethodEventDispatcher +import com.processout.sdk.api.model.response.PONativeAlternativePaymentMethodParameter.ParameterType import com.processout.sdk.api.model.response.PONativeAlternativePaymentMethodParameter.ParameterType.* import com.processout.sdk.api.model.response.PONativeAlternativePaymentMethodTransactionDetails.Invoice import com.processout.sdk.core.retry.PORetryStrategy.Exponential @@ -82,6 +87,11 @@ internal class NativeAlternativePaymentViewModel( const val CODE_FIELD_LENGTH_MAX = 6 } + private data class KeyboardAction( + val imeAction: ImeAction, + val actionId: String? + ) + val completion = interactor.completion val state = interactor.state.map(viewModelScope, ::map) @@ -146,12 +156,14 @@ internal class NativeAlternativePaymentViewModel( } private fun List.map(): POImmutableList { + val lastFocusableFieldId = lastFocusableFieldId() val fields = map { field -> + val keyboardAction = keyboardAction(field.id, lastFocusableFieldId) when (field.type) { NUMERIC -> if (field.length in CODE_FIELD_LENGTH_MIN..CODE_FIELD_LENGTH_MAX) { - field.toCodeField() + field.toCodeField(keyboardAction) } else { - field.toTextField() + field.toTextField(keyboardAction) } SINGLE_SELECT -> { val availableValuesCount = field.availableValues?.size ?: 0 @@ -161,13 +173,15 @@ internal class NativeAlternativePaymentViewModel( field.toDropdownField() } } - else -> field.toTextField() + else -> field.toTextField(keyboardAction) } } return POImmutableList(fields) } - private fun Field.toTextField(): NativeAlternativePaymentViewModelState.Field = + private fun Field.toTextField( + keyboardAction: KeyboardAction + ): NativeAlternativePaymentViewModelState.Field = TextField( POFieldState( id = id, @@ -175,18 +189,24 @@ internal class NativeAlternativePaymentViewModel( title = displayName, isError = !isValid, inputFilter = if (type == PHONE) PhoneNumberInputFilter() else null, - visualTransformation = if (type == PHONE) PhoneNumberVisualTransformation() else VisualTransformation.None + visualTransformation = if (type == PHONE) PhoneNumberVisualTransformation() else VisualTransformation.None, + keyboardOptions = type.keyboardOptions(keyboardAction.imeAction), + keyboardActionId = keyboardAction.actionId ) ) - private fun Field.toCodeField(): NativeAlternativePaymentViewModelState.Field = + private fun Field.toCodeField( + keyboardAction: KeyboardAction + ): NativeAlternativePaymentViewModelState.Field = CodeField( POFieldState( id = id, value = value, length = length, title = displayName, - isError = !isValid + isError = !isValid, + keyboardOptions = type.keyboardOptions(keyboardAction.imeAction), + keyboardActionId = keyboardAction.actionId ) ) @@ -212,6 +232,54 @@ internal class NativeAlternativePaymentViewModel( ) ) + private fun List.lastFocusableFieldId(): String? { + reversed().forEach { field -> + if (field.type != SINGLE_SELECT) { + return field.id + } + } + return null + } + + private fun keyboardAction(fieldId: String, lastFocusableFieldId: String?) = + if (fieldId == lastFocusableFieldId) { + KeyboardAction( + imeAction = ImeAction.Done, + actionId = ActionId.SUBMIT + ) + } else { + KeyboardAction( + imeAction = ImeAction.Next, + actionId = null + ) + } + + private fun ParameterType.keyboardOptions( + imeAction: ImeAction + ): KeyboardOptions = when (this) { + NUMERIC -> KeyboardOptions( + keyboardType = KeyboardType.NumberPassword, + imeAction = imeAction + ) + TEXT -> KeyboardOptions( + capitalization = KeyboardCapitalization.Sentences, + keyboardType = KeyboardType.Text, + imeAction = imeAction + ) + EMAIL -> KeyboardOptions( + keyboardType = KeyboardType.Email, + imeAction = imeAction + ) + PHONE -> KeyboardOptions( + keyboardType = KeyboardType.Phone, + imeAction = imeAction + ) + SINGLE_SELECT -> KeyboardOptions.Default + UNKNOWN -> KeyboardOptions( + imeAction = imeAction + ) + } + private fun Invoice.formatPrimaryActionText() = try { val price = NumberFormat.getCurrencyInstance().apply { From 5244b7e13384e9c31e405e7d5b778e35f8a5f1d6 Mon Sep 17 00:00:00 2001 From: Vitalii Vanziak Date: Mon, 27 May 2024 15:53:19 +0300 Subject: [PATCH 25/38] Support RTL for [numeric, email, phone] field types --- .../processout/sdk/ui/napm/NativeAlternativePaymentViewModel.kt | 1 + 1 file changed, 1 insertion(+) 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 e9d6f43f7..1f6ad0860 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 @@ -188,6 +188,7 @@ internal class NativeAlternativePaymentViewModel( value = value, title = displayName, isError = !isValid, + forceTextDirectionLtr = setOf(NUMERIC, EMAIL, PHONE).contains(type), inputFilter = if (type == PHONE) PhoneNumberInputFilter() else null, visualTransformation = if (type == PHONE) PhoneNumberVisualTransformation() else VisualTransformation.None, keyboardOptions = type.keyboardOptions(keyboardAction.imeAction), From 798084c6915485440df5c8aa8af7a4f7b00f189a Mon Sep 17 00:00:00 2001 From: Vitalii Vanziak Date: Mon, 27 May 2024 16:16:48 +0300 Subject: [PATCH 26/38] Placeholders --- .../nativeapm/NativeAlternativePaymentMethodViewModel.kt | 4 ++-- sdk/src/main/res/values-pl/strings.xml | 4 ++-- sdk/src/main/res/values-pt/strings.xml | 4 ++-- sdk/src/main/res/values/strings.xml | 4 ++-- .../sdk/ui/napm/NativeAlternativePaymentViewModel.kt | 7 +++++++ 5 files changed, 15 insertions(+), 8 deletions(-) diff --git a/sdk/src/main/kotlin/com/processout/sdk/ui/nativeapm/NativeAlternativePaymentMethodViewModel.kt b/sdk/src/main/kotlin/com/processout/sdk/ui/nativeapm/NativeAlternativePaymentMethodViewModel.kt index 7eb79c19d..898ab8855 100644 --- a/sdk/src/main/kotlin/com/processout/sdk/ui/nativeapm/NativeAlternativePaymentMethodViewModel.kt +++ b/sdk/src/main/kotlin/com/processout/sdk/ui/nativeapm/NativeAlternativePaymentMethodViewModel.kt @@ -658,8 +658,8 @@ internal class NativeAlternativePaymentMethodViewModel( private fun getInputHint(type: ParameterType) = when (type) { - PHONE -> app.getString(R.string.po_native_apm_input_hint_phone) - EMAIL -> app.getString(R.string.po_native_apm_input_hint_email) + EMAIL -> app.getString(R.string.po_native_apm_email_placeholder) + PHONE -> app.getString(R.string.po_native_apm_phone_placeholder) else -> null } diff --git a/sdk/src/main/res/values-pl/strings.xml b/sdk/src/main/res/values-pl/strings.xml index f5ca02705..646d37f21 100644 --- a/sdk/src/main/res/values-pl/strings.xml +++ b/sdk/src/main/res/values-pl/strings.xml @@ -12,8 +12,8 @@ Anuluj płatność Nie teraz Sukces!\nPłatność przyjęta. - Twój numer telefonu - imię@przykład.pl + imię@przykład.pl + Twój numer telefonu Parametr jest wmagany. Niepoprawny numer. Niepoprawna wartość. diff --git a/sdk/src/main/res/values-pt/strings.xml b/sdk/src/main/res/values-pt/strings.xml index dbb523892..43e0fd0f4 100644 --- a/sdk/src/main/res/values-pt/strings.xml +++ b/sdk/src/main/res/values-pt/strings.xml @@ -12,8 +12,8 @@ Cancelar pagamento Agora não Successo!\nPagamento aprovado. - Insira o seu número de telemóvel - nome@exemplo.pt + nome@exemplo.pt + Insira o seu número de telemóvel Campo obrigatório. Número inválido. Texto inválido. diff --git a/sdk/src/main/res/values/strings.xml b/sdk/src/main/res/values/strings.xml index 449df48b1..8f46ba21f 100644 --- a/sdk/src/main/res/values/strings.xml +++ b/sdk/src/main/res/values/strings.xml @@ -12,8 +12,8 @@ Cancel payment Not now Success!\nPayment approved. - Enter phone number - name@example.com + name@example.com + Enter phone number Parameter is required. Number is not valid. Value is not valid. 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 1f6ad0860..affbdd6b5 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 @@ -187,6 +187,7 @@ internal class NativeAlternativePaymentViewModel( id = id, value = value, title = displayName, + placeholder = type.placeholder(), isError = !isValid, forceTextDirectionLtr = setOf(NUMERIC, EMAIL, PHONE).contains(type), inputFilter = if (type == PHONE) PhoneNumberInputFilter() else null, @@ -281,6 +282,12 @@ internal class NativeAlternativePaymentViewModel( ) } + private fun ParameterType.placeholder(): String? = when (this) { + EMAIL -> app.getString(R.string.po_native_apm_email_placeholder) + PHONE -> app.getString(R.string.po_native_apm_phone_placeholder) + else -> null + } + private fun Invoice.formatPrimaryActionText() = try { val price = NumberFormat.getCurrencyInstance().apply { From e1b01a9fea84cf28b36458ef45f3089cafeb68e7 Mon Sep 17 00:00:00 2001 From: Vitalii Vanziak Date: Mon, 27 May 2024 18:16:12 +0300 Subject: [PATCH 27/38] Submit payment --- .../NativeAlternativePaymentInteractor.kt | 56 +++++++++++++++---- 1 file changed, 44 insertions(+), 12 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 9a6b5724a..c115445d0 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 @@ -7,6 +7,7 @@ import com.processout.sdk.api.dispatcher.napm.PODefaultNativeAlternativePaymentM import com.processout.sdk.api.model.event.PONativeAlternativePaymentMethodEvent import com.processout.sdk.api.model.event.PONativeAlternativePaymentMethodEvent.* import com.processout.sdk.api.model.request.PONativeAlternativePaymentMethodDefaultValuesRequest +import com.processout.sdk.api.model.request.PONativeAlternativePaymentMethodRequest import com.processout.sdk.api.model.response.PONativeAlternativePaymentMethodParameter import com.processout.sdk.api.model.response.PONativeAlternativePaymentMethodParameter.ParameterType.UNKNOWN import com.processout.sdk.api.model.response.PONativeAlternativePaymentMethodParameterValues @@ -28,7 +29,6 @@ import com.processout.sdk.ui.napm.NativeAlternativePaymentEvent.* import com.processout.sdk.ui.napm.NativeAlternativePaymentInteractorState.* import com.processout.sdk.ui.napm.PONativeAlternativePaymentConfiguration.Options import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update @@ -351,22 +351,54 @@ internal class NativeAlternativePaymentInteractor( } private fun submit() { - // TODO - interactorScope.launch { + _state.whenUserInput { stateValue -> + POLogger.info("Will submit payment parameters.") + dispatch(WillSubmitParameters) + + // TODO validate + _state.update { - Capturing( - CaptureStateValue( - paymentProviderName = null, - logoUrl = null, - secondaryActionId = ActionId.CANCEL + UserInput( + stateValue.copy( + submitAllowed = true, + submitting = true ) ) } - delay(2000) - _state.whenCapturing { stateValue -> - _state.update { - Captured(stateValue) + initiatePayment() + } + } + + private fun initiatePayment() { + _state.whenUserInput { stateValue -> + interactorScope.launch { + val parameters = mutableMapOf() + stateValue.fields.forEach { + parameters[it.id] = it.value.text } + val request = PONativeAlternativePaymentMethodRequest( + invoiceId = invoiceId, + gatewayConfigurationId = gatewayConfigurationId, + parameters = parameters + ) + invoicesService.initiatePayment(request) + .onSuccess { payment -> + with(payment) { + handleState( + stateValue = stateValue, + paymentState = state, + parameters = parameterDefinitions, + parameterValues = parameterValues, + coroutineScope = this@launch + ) + } + } + .onFailure { failure -> + // TODO +// handlePaymentFailure( +// uiModel, result, replaceToLocalMessage = true +// ) + } } } } From 45781b47189b1255d09a9c9499e90452080c4eaa Mon Sep 17 00:00:00 2001 From: Vitalii Vanziak Date: Mon, 27 May 2024 20:29:58 +0300 Subject: [PATCH 28/38] Handle payment failure --- .../NativeAlternativePaymentInteractor.kt | 71 +++++++++++++++++-- ...NativeAlternativePaymentInteractorState.kt | 1 + .../napm/NativeAlternativePaymentViewModel.kt | 4 ++ 3 files changed, 69 insertions(+), 7 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 c115445d0..34dd2aa67 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 @@ -3,13 +3,15 @@ package com.processout.sdk.ui.napm import android.app.Application import androidx.compose.ui.text.TextRange import androidx.compose.ui.text.input.TextFieldValue +import com.processout.sdk.R import com.processout.sdk.api.dispatcher.napm.PODefaultNativeAlternativePaymentMethodEventDispatcher import com.processout.sdk.api.model.event.PONativeAlternativePaymentMethodEvent import com.processout.sdk.api.model.event.PONativeAlternativePaymentMethodEvent.* import com.processout.sdk.api.model.request.PONativeAlternativePaymentMethodDefaultValuesRequest import com.processout.sdk.api.model.request.PONativeAlternativePaymentMethodRequest import com.processout.sdk.api.model.response.PONativeAlternativePaymentMethodParameter -import com.processout.sdk.api.model.response.PONativeAlternativePaymentMethodParameter.ParameterType.UNKNOWN +import com.processout.sdk.api.model.response.PONativeAlternativePaymentMethodParameter.ParameterType +import com.processout.sdk.api.model.response.PONativeAlternativePaymentMethodParameter.ParameterType.* import com.processout.sdk.api.model.response.PONativeAlternativePaymentMethodParameterValues import com.processout.sdk.api.model.response.PONativeAlternativePaymentMethodState import com.processout.sdk.api.model.response.PONativeAlternativePaymentMethodState.* @@ -198,6 +200,7 @@ internal class NativeAlternativePaymentInteractor( type = type(), length = length, displayName = displayName, + description = null, required = required, isValid = true ) @@ -310,7 +313,6 @@ internal class NativeAlternativePaymentInteractor( UserInput( stateValue.copy( fields = stateValue.fields.map { field -> - // TODO: remove field error message when edited? updatedField(id, value, field, isTextChanged) } ) @@ -334,7 +336,7 @@ internal class NativeAlternativePaymentInteractor( return field } return if (isTextChanged) { - field.copy(value = value, isValid = true) + field.copy(value = value, description = null, isValid = true) } else { field.copy(value = value) } @@ -394,15 +396,70 @@ internal class NativeAlternativePaymentInteractor( } } .onFailure { failure -> - // TODO -// handlePaymentFailure( -// uiModel, result, replaceToLocalMessage = true -// ) + handlePaymentFailure( + failure = failure, + replaceWithLocalMessage = true + ) } } } } + private fun handlePaymentFailure( + failure: ProcessOutResult.Failure, + replaceWithLocalMessage: Boolean // TODO: Delete this when backend localization is ready. + ) { + _state.whenUserInput { stateValue -> + val invalidFields = failure.invalidFields + if (invalidFields.isNullOrEmpty()) { + POLogger.info("Unrecoverable payment failure: %s", failure) + _completion.update { Failure(failure) } + return@whenUserInput + } + val updatedFields = stateValue.fields.map { field -> + invalidFields.find { it.name == field.id }?.let { invalidField -> + field.copy( + description = fieldErrorMessage( + originalMessage = invalidField.message, + replaceWithLocalMessage = replaceWithLocalMessage, + type = field.type + ), + isValid = false + ) + } ?: field + } + val firstInvalidFieldId = updatedFields.find { !it.isValid }?.id + _state.update { + UserInput( + stateValue.copy( + fields = updatedFields, + focusedFieldId = firstInvalidFieldId ?: stateValue.focusedFieldId, + submitAllowed = updatedFields.all { it.isValid }, + submitting = false + ) + ) + } + POLogger.info("Recovered after the failure: %s", failure) + dispatch(DidFailToSubmitParameters(failure)) + } + } + + // TODO: Delete this when backend localization is ready. + private fun fieldErrorMessage( + originalMessage: String?, + replaceWithLocalMessage: Boolean, + type: ParameterType + ): String? = + if (replaceWithLocalMessage) + when (type) { + NUMERIC -> app.getString(R.string.po_native_apm_error_invalid_number) + TEXT -> app.getString(R.string.po_native_apm_error_invalid_text) + EMAIL -> app.getString(R.string.po_native_apm_error_invalid_email) + PHONE -> app.getString(R.string.po_native_apm_error_invalid_phone) + else -> null + } + else originalMessage + private fun cancel() { _completion.update { Failure( diff --git a/ui/src/main/kotlin/com/processout/sdk/ui/napm/NativeAlternativePaymentInteractorState.kt b/ui/src/main/kotlin/com/processout/sdk/ui/napm/NativeAlternativePaymentInteractorState.kt index efc445702..6274bc0e8 100644 --- a/ui/src/main/kotlin/com/processout/sdk/ui/napm/NativeAlternativePaymentInteractorState.kt +++ b/ui/src/main/kotlin/com/processout/sdk/ui/napm/NativeAlternativePaymentInteractorState.kt @@ -60,6 +60,7 @@ internal sealed interface NativeAlternativePaymentInteractorState { val type: ParameterType, val length: Int?, val displayName: String, + val description: String?, val required: Boolean, val isValid: Boolean ) 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 affbdd6b5..702143302 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 @@ -187,6 +187,7 @@ internal class NativeAlternativePaymentViewModel( id = id, value = value, title = displayName, + description = description, placeholder = type.placeholder(), isError = !isValid, forceTextDirectionLtr = setOf(NUMERIC, EMAIL, PHONE).contains(type), @@ -206,6 +207,7 @@ internal class NativeAlternativePaymentViewModel( value = value, length = length, title = displayName, + description = description, isError = !isValid, keyboardOptions = type.keyboardOptions(keyboardAction.imeAction), keyboardActionId = keyboardAction.actionId @@ -219,6 +221,7 @@ internal class NativeAlternativePaymentViewModel( value = value, availableValues = availableValues?.let { POImmutableList(it) }, title = displayName, + description = description, isError = !isValid ) ) @@ -230,6 +233,7 @@ internal class NativeAlternativePaymentViewModel( value = value, availableValues = availableValues?.let { POImmutableList(it) }, title = displayName, + description = description, isError = !isValid ) ) From da905d169d186f0f3f2fafd991cb56a7b2f776c9 Mon Sep 17 00:00:00 2001 From: Vitalii Vanziak Date: Mon, 27 May 2024 20:46:36 +0300 Subject: [PATCH 29/38] Resolve 'focusedFieldId' on each parameters step --- .../sdk/ui/napm/NativeAlternativePaymentInteractor.kt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) 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 34dd2aa67..21c917c2a 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 @@ -143,9 +143,12 @@ internal class NativeAlternativePaymentInteractor( return } val fields = parameters.toFields() + val focusedFieldId = fields.getOrNull(0)?.let { field -> + if (field.type != SINGLE_SELECT) field.id else null + } val updatedStateValue = stateValue.copy( fields = fields, - focusedFieldId = fields.firstOrNull()?.id + focusedFieldId = focusedFieldId ) val isLoading = _state.value is Loading if (eventDispatcher.subscribedForDefaultValuesRequest()) { From c441a561147879cd34df9643fc7aaba3fe061316 Mon Sep 17 00:00:00 2001 From: Vitalii Vanziak Date: Mon, 27 May 2024 21:59:37 +0300 Subject: [PATCH 30/38] Allow submit when all fields valid after edit --- .../NativeAlternativePaymentInteractor.kt | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 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 21c917c2a..23918a92a 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 @@ -312,19 +312,18 @@ internal class NativeAlternativePaymentInteractor( _state.whenUserInput { stateValue -> val previousValue = stateValue.fields.find { it.id == id }?.value ?: TextFieldValue() val isTextChanged = value.text != previousValue.text - _state.update { - UserInput( - stateValue.copy( - fields = stateValue.fields.map { field -> - updatedField(id, value, field, isTextChanged) - } - ) - ) - } + val updatedStateValue = stateValue.copy( + fields = stateValue.fields.map { field -> + updatedField(id, value, field, isTextChanged) + } + ) + _state.update { UserInput(updatedStateValue) } if (isTextChanged) { POLogger.debug("Field is edited by the user: %s", id) dispatch(ParametersChanged) - // TODO: allow submit if all fields valid? + if (updatedStateValue.areAllFieldsValid()) { + _state.update { UserInput(updatedStateValue.copy(submitAllowed = true)) } + } } } } @@ -374,6 +373,8 @@ internal class NativeAlternativePaymentInteractor( } } + private fun UserInputStateValue.areAllFieldsValid() = fields.all { it.isValid } + private fun initiatePayment() { _state.whenUserInput { stateValue -> interactorScope.launch { From 8ef59d122596d3abe930839eb4611b888695444c Mon Sep 17 00:00:00 2001 From: Vitalii Vanziak Date: Mon, 27 May 2024 23:42:45 +0300 Subject: [PATCH 31/38] Validation --- .../NativeAlternativePaymentInteractor.kt | 55 ++++++++++++++++++- 1 file changed, 52 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 23918a92a..3f2396d30 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 @@ -1,8 +1,11 @@ package com.processout.sdk.ui.napm import android.app.Application +import android.util.Patterns +import androidx.annotation.StringRes import androidx.compose.ui.text.TextRange import androidx.compose.ui.text.input.TextFieldValue +import androidx.core.text.isDigitsOnly import com.processout.sdk.R import com.processout.sdk.api.dispatcher.napm.PODefaultNativeAlternativePaymentMethodEventDispatcher import com.processout.sdk.api.model.event.PONativeAlternativePaymentMethodEvent @@ -18,6 +21,8 @@ import com.processout.sdk.api.model.response.PONativeAlternativePaymentMethodSta import com.processout.sdk.api.model.response.PONativeAlternativePaymentMethodTransactionDetails import com.processout.sdk.api.service.POInvoicesService import com.processout.sdk.core.POFailure.Code.* +import com.processout.sdk.core.POFailure.InvalidField +import com.processout.sdk.core.POFailure.ValidationCode import com.processout.sdk.core.ProcessOutResult import com.processout.sdk.core.logger.POLogger import com.processout.sdk.core.onFailure @@ -358,9 +363,19 @@ internal class NativeAlternativePaymentInteractor( _state.whenUserInput { stateValue -> POLogger.info("Will submit payment parameters.") dispatch(WillSubmitParameters) - - // TODO validate - + val invalidFields = stateValue.fields.mapNotNull { it.validate() } + if (invalidFields.isNotEmpty()) { + val failure = ProcessOutResult.Failure( + code = Validation(ValidationCode.general), + message = "Invalid fields.", + invalidFields = invalidFields + ) + handlePaymentFailure( + failure = failure, + replaceWithLocalMessage = false + ) + return@whenUserInput + } _state.update { UserInput( stateValue.copy( @@ -373,6 +388,40 @@ internal class NativeAlternativePaymentInteractor( } } + private fun Field.validate(): InvalidField? { + val value = value.text + if (required && value.isBlank()) { + return invalidField(R.string.po_native_apm_error_required_parameter) + } + length?.let { + if (value.length != it) { + return InvalidField( + name = id, + message = app.resources.getQuantityString( + R.plurals.po_native_apm_error_invalid_length, it, it + ) + ) + } + } + when (type) { + NUMERIC -> if (!value.isDigitsOnly()) + return invalidField(R.string.po_native_apm_error_invalid_number) + EMAIL -> if (!Patterns.EMAIL_ADDRESS.matcher(value).matches()) + return invalidField(R.string.po_native_apm_error_invalid_email) + PHONE -> if (!Patterns.PHONE.matcher(value).matches()) + return invalidField(R.string.po_native_apm_error_invalid_phone) + else -> {} + } + return null + } + + private fun Field.invalidField( + @StringRes messageResId: Int + ) = InvalidField( + name = id, + message = app.getString(messageResId) + ) + private fun UserInputStateValue.areAllFieldsValid() = fields.all { it.isValid } private fun initiatePayment() { From 9c69d86abb0cccfaf56135e557f246a1c26a5693 Mon Sep 17 00:00:00 2001 From: Vitalii Vanziak Date: Mon, 27 May 2024 23:55:24 +0300 Subject: [PATCH 32/38] Fields spacing --- .../processout/sdk/ui/napm/NativeAlternativePaymentScreen.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/ui/src/main/kotlin/com/processout/sdk/ui/napm/NativeAlternativePaymentScreen.kt b/ui/src/main/kotlin/com/processout/sdk/ui/napm/NativeAlternativePaymentScreen.kt index a27f0f350..4918d561f 100644 --- a/ui/src/main/kotlin/com/processout/sdk/ui/napm/NativeAlternativePaymentScreen.kt +++ b/ui/src/main/kotlin/com/processout/sdk/ui/napm/NativeAlternativePaymentScreen.kt @@ -116,7 +116,9 @@ private fun UserInput( style: NativeAlternativePaymentScreen.Style ) { AnimatedVisibility { - Column { + Column( + verticalArrangement = Arrangement.spacedBy(ProcessOutTheme.spacing.small) + ) { val lifecycleEvent = rememberLifecycleEvent() val labelsStyle = remember { POFieldLabels.Style( From ce4f6d094de1e82b12d0d0e2434c81064fa24f7d Mon Sep 17 00:00:00 2001 From: Vitalii Vanziak Date: Tue, 28 May 2024 00:37:32 +0300 Subject: [PATCH 33/38] codeFieldHorizontalAlignment() --- .../sdk/ui/napm/NativeAlternativePaymentScreen.kt | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/ui/src/main/kotlin/com/processout/sdk/ui/napm/NativeAlternativePaymentScreen.kt b/ui/src/main/kotlin/com/processout/sdk/ui/napm/NativeAlternativePaymentScreen.kt index 4918d561f..17447631d 100644 --- a/ui/src/main/kotlin/com/processout/sdk/ui/napm/NativeAlternativePaymentScreen.kt +++ b/ui/src/main/kotlin/com/processout/sdk/ui/napm/NativeAlternativePaymentScreen.kt @@ -44,6 +44,7 @@ import com.processout.sdk.ui.core.theme.ProcessOutTheme import com.processout.sdk.ui.napm.NativeAlternativePaymentEvent.* import com.processout.sdk.ui.napm.NativeAlternativePaymentScreen.AnimationDurationMillis import com.processout.sdk.ui.napm.NativeAlternativePaymentScreen.animatedBackgroundColor +import com.processout.sdk.ui.napm.NativeAlternativePaymentScreen.codeFieldHorizontalAlignment import com.processout.sdk.ui.napm.NativeAlternativePaymentViewModelState.* import com.processout.sdk.ui.napm.NativeAlternativePaymentViewModelState.Field.* import com.processout.sdk.ui.shared.composable.rememberLifecycleEvent @@ -146,7 +147,8 @@ private fun UserInput( focusedFieldId = state.focusedFieldId, isPrimaryActionEnabled = isPrimaryActionEnabled, fieldStyle = style.codeField, - labelsStyle = labelsStyle + labelsStyle = labelsStyle, + horizontalAlignment = codeFieldHorizontalAlignment(state.fields.elements) ) is RadioField -> RadioField( state = field.state, @@ -225,6 +227,7 @@ private fun CodeField( isPrimaryActionEnabled: Boolean, fieldStyle: POField.Style, labelsStyle: POFieldLabels.Style, + horizontalAlignment: Alignment.Horizontal, modifier: Modifier = Modifier ) { POLabeledCodeField( @@ -251,6 +254,7 @@ private fun CodeField( fieldStyle = fieldStyle, labelsStyle = labelsStyle, length = state.length ?: POCodeField.LengthMax, + horizontalAlignment = horizontalAlignment, isError = state.isError, isFocused = state.id == focusedFieldId, lifecycleEvent = lifecycleEvent, @@ -459,4 +463,8 @@ internal object NativeAlternativePaymentScreen { }, animationSpec = tween(durationMillis = AnimationDurationMillis, easing = LinearEasing) ).value + + fun codeFieldHorizontalAlignment(fields: List): Alignment.Horizontal = + if (fields.size == 1 && fields[0] is CodeField) + Alignment.CenterHorizontally else Alignment.Start } From 49f698fd9e9a43d4892f106cdae6712e04770a29 Mon Sep 17 00:00:00 2001 From: Vitalii Vanziak Date: Tue, 28 May 2024 09:37:17 +0300 Subject: [PATCH 34/38] Fix title padding in POHeader --- .../kotlin/com/processout/sdk/ui/core/component/POHeader.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui-core/src/main/kotlin/com/processout/sdk/ui/core/component/POHeader.kt b/ui-core/src/main/kotlin/com/processout/sdk/ui/core/component/POHeader.kt index 2c8666c28..90ea1724f 100644 --- a/ui-core/src/main/kotlin/com/processout/sdk/ui/core/component/POHeader.kt +++ b/ui-core/src/main/kotlin/com/processout/sdk/ui/core/component/POHeader.kt @@ -65,7 +65,7 @@ private fun titlePadding( if (withDragHandle) PaddingValues( start = spacing.extraLarge, end = spacing.extraLarge, - top = spacing.small, + top = spacing.medium, bottom = spacing.large ) else PaddingValues( horizontal = spacing.extraLarge, From 15258d797aeda5dc55363d9ee76c7f560050a9ba Mon Sep 17 00:00:00 2001 From: Vitalii Vanziak Date: Tue, 28 May 2024 12:52:31 +0300 Subject: [PATCH 35/38] RadioField --- .../ui/napm/NativeAlternativePaymentScreen.kt | 26 +++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/ui/src/main/kotlin/com/processout/sdk/ui/napm/NativeAlternativePaymentScreen.kt b/ui/src/main/kotlin/com/processout/sdk/ui/napm/NativeAlternativePaymentScreen.kt index 17447631d..0cc5c6603 100644 --- a/ui/src/main/kotlin/com/processout/sdk/ui/napm/NativeAlternativePaymentScreen.kt +++ b/ui/src/main/kotlin/com/processout/sdk/ui/napm/NativeAlternativePaymentScreen.kt @@ -34,6 +34,7 @@ import com.processout.sdk.ui.core.component.field.POFieldLabels import com.processout.sdk.ui.core.component.field.code.POCodeField import com.processout.sdk.ui.core.component.field.code.POLabeledCodeField import com.processout.sdk.ui.core.component.field.dropdown.PODropdownField +import com.processout.sdk.ui.core.component.field.radio.POLabeledRadioField import com.processout.sdk.ui.core.component.field.radio.PORadioGroup import com.processout.sdk.ui.core.component.field.text.POLabeledTextField import com.processout.sdk.ui.core.state.POActionState @@ -152,7 +153,9 @@ private fun UserInput( ) is RadioField -> RadioField( state = field.state, - onEvent = onEvent + onEvent = onEvent, + radioGroupStyle = style.radioGroup, + labelsStyle = labelsStyle ) is DropdownField -> DropdownField( state = field.state, @@ -272,9 +275,28 @@ private fun CodeField( private fun RadioField( state: POFieldState, onEvent: (NativeAlternativePaymentEvent) -> Unit, + radioGroupStyle: PORadioGroup.Style, + labelsStyle: POFieldLabels.Style, modifier: Modifier = Modifier ) { - // TODO + POLabeledRadioField( + value = state.value, + onValueChange = { + onEvent( + FieldValueChanged( + id = state.id, + value = it + ) + ) + }, + availableValues = state.availableValues ?: POImmutableList(emptyList()), + title = state.title ?: String(), + description = state.description, + modifier = modifier, + radioGroupStyle = radioGroupStyle, + labelsStyle = labelsStyle, + isError = state.isError + ) } @Composable From 396b2e4e4b7cc3a69b8a40dabc8b7e093b318170 Mon Sep 17 00:00:00 2001 From: Vitalii Vanziak Date: Tue, 28 May 2024 13:13:01 +0300 Subject: [PATCH 36/38] DropdownField --- .../ui/napm/NativeAlternativePaymentScreen.kt | 37 ++++++++++++++++++- 1 file changed, 35 insertions(+), 2 deletions(-) diff --git a/ui/src/main/kotlin/com/processout/sdk/ui/napm/NativeAlternativePaymentScreen.kt b/ui/src/main/kotlin/com/processout/sdk/ui/napm/NativeAlternativePaymentScreen.kt index 0cc5c6603..1765f9b0c 100644 --- a/ui/src/main/kotlin/com/processout/sdk/ui/napm/NativeAlternativePaymentScreen.kt +++ b/ui/src/main/kotlin/com/processout/sdk/ui/napm/NativeAlternativePaymentScreen.kt @@ -34,6 +34,7 @@ import com.processout.sdk.ui.core.component.field.POFieldLabels import com.processout.sdk.ui.core.component.field.code.POCodeField import com.processout.sdk.ui.core.component.field.code.POLabeledCodeField import com.processout.sdk.ui.core.component.field.dropdown.PODropdownField +import com.processout.sdk.ui.core.component.field.dropdown.POLabeledDropdownField import com.processout.sdk.ui.core.component.field.radio.POLabeledRadioField import com.processout.sdk.ui.core.component.field.radio.PORadioGroup import com.processout.sdk.ui.core.component.field.text.POLabeledTextField @@ -159,7 +160,10 @@ private fun UserInput( ) is DropdownField -> DropdownField( state = field.state, - onEvent = onEvent + onEvent = onEvent, + fieldStyle = style.field, + menuStyle = style.dropdownMenu, + modifier = Modifier.fillMaxWidth() ) } } @@ -303,9 +307,38 @@ private fun RadioField( private fun DropdownField( state: POFieldState, onEvent: (NativeAlternativePaymentEvent) -> Unit, + fieldStyle: POField.Style, + menuStyle: PODropdownField.MenuStyle, modifier: Modifier = Modifier ) { - // TODO + POLabeledDropdownField( + value = state.value, + onValueChange = { + onEvent( + FieldValueChanged( + id = state.id, + value = it + ) + ) + }, + availableValues = state.availableValues ?: POImmutableList(emptyList()), + title = state.title ?: String(), + description = state.description, + modifier = modifier + .onFocusChanged { + onEvent( + FieldFocusChanged( + id = state.id, + isFocused = it.isFocused + ) + ) + }, + fieldStyle = fieldStyle, + menuStyle = menuStyle, + enabled = state.enabled, + isError = state.isError, + placeholderText = state.placeholder + ) } @Composable From 5b5f3971204d204e15cee6342bf441a4db8f36bd Mon Sep 17 00:00:00 2001 From: Vitalii Vanziak Date: Tue, 28 May 2024 13:34:49 +0300 Subject: [PATCH 37/38] Pass 'enabled' state to POCodeField --- .../processout/sdk/ui/core/component/field/code/POCodeField.kt | 2 ++ .../sdk/ui/core/component/field/code/POLabeledCodeField.kt | 2 ++ .../processout/sdk/ui/napm/NativeAlternativePaymentScreen.kt | 1 + 3 files changed, 5 insertions(+) diff --git a/ui-core/src/main/kotlin/com/processout/sdk/ui/core/component/field/code/POCodeField.kt b/ui-core/src/main/kotlin/com/processout/sdk/ui/core/component/field/code/POCodeField.kt index 4d93d070e..e07aedd01 100644 --- a/ui-core/src/main/kotlin/com/processout/sdk/ui/core/component/field/code/POCodeField.kt +++ b/ui-core/src/main/kotlin/com/processout/sdk/ui/core/component/field/code/POCodeField.kt @@ -42,6 +42,7 @@ fun POCodeField( style: POField.Style = POCodeField.default, length: Int = POCodeField.LengthMax, horizontalAlignment: Alignment.Horizontal = Alignment.CenterHorizontally, + enabled: Boolean = true, isError: Boolean = false, isFocused: Boolean = false, lifecycleEvent: Lifecycle.Event? = null, @@ -158,6 +159,7 @@ fun POCodeField( } }, style = style(style), + enabled = enabled, isError = isError, keyboardOptions = keyboardOptions, keyboardActions = keyboardActions diff --git a/ui-core/src/main/kotlin/com/processout/sdk/ui/core/component/field/code/POLabeledCodeField.kt b/ui-core/src/main/kotlin/com/processout/sdk/ui/core/component/field/code/POLabeledCodeField.kt index 973fc01a5..7273070a1 100644 --- a/ui-core/src/main/kotlin/com/processout/sdk/ui/core/component/field/code/POLabeledCodeField.kt +++ b/ui-core/src/main/kotlin/com/processout/sdk/ui/core/component/field/code/POLabeledCodeField.kt @@ -25,6 +25,7 @@ fun POLabeledCodeField( labelsStyle: POFieldLabels.Style = POFieldLabels.default, length: Int = POCodeField.LengthMax, horizontalAlignment: Alignment.Horizontal = Alignment.CenterHorizontally, + enabled: Boolean = true, isError: Boolean = false, isFocused: Boolean = false, lifecycleEvent: Lifecycle.Event? = null, @@ -44,6 +45,7 @@ fun POLabeledCodeField( style = fieldStyle, length = length, horizontalAlignment = horizontalAlignment, + enabled = enabled, isError = isError, isFocused = isFocused, lifecycleEvent = lifecycleEvent, diff --git a/ui/src/main/kotlin/com/processout/sdk/ui/napm/NativeAlternativePaymentScreen.kt b/ui/src/main/kotlin/com/processout/sdk/ui/napm/NativeAlternativePaymentScreen.kt index 1765f9b0c..d6d72a9cf 100644 --- a/ui/src/main/kotlin/com/processout/sdk/ui/napm/NativeAlternativePaymentScreen.kt +++ b/ui/src/main/kotlin/com/processout/sdk/ui/napm/NativeAlternativePaymentScreen.kt @@ -262,6 +262,7 @@ private fun CodeField( labelsStyle = labelsStyle, length = state.length ?: POCodeField.LengthMax, horizontalAlignment = horizontalAlignment, + enabled = state.enabled, isError = state.isError, isFocused = state.id == focusedFieldId, lifecycleEvent = lifecycleEvent, From d153e65af3b4dd77c21035a70866416d22c2b12e Mon Sep 17 00:00:00 2001 From: Vitalii Vanziak Date: Tue, 28 May 2024 13:53:27 +0300 Subject: [PATCH 38/38] Do not expose 'enabled' state for PODropdownField as it's no-op --- .../sdk/ui/core/component/field/dropdown/PODropdownField.kt | 3 +-- .../ui/core/component/field/dropdown/POLabeledDropdownField.kt | 2 -- .../sdk/ui/card/tokenization/CardTokenizationScreen.kt | 1 - .../processout/sdk/ui/napm/NativeAlternativePaymentScreen.kt | 1 - 4 files changed, 1 insertion(+), 6 deletions(-) diff --git a/ui-core/src/main/kotlin/com/processout/sdk/ui/core/component/field/dropdown/PODropdownField.kt b/ui-core/src/main/kotlin/com/processout/sdk/ui/core/component/field/dropdown/PODropdownField.kt index 24d19cfe0..2dbf66d55 100644 --- a/ui-core/src/main/kotlin/com/processout/sdk/ui/core/component/field/dropdown/PODropdownField.kt +++ b/ui-core/src/main/kotlin/com/processout/sdk/ui/core/component/field/dropdown/PODropdownField.kt @@ -44,7 +44,6 @@ fun PODropdownField( modifier: Modifier = Modifier, fieldStyle: POField.Style = POField.default, menuStyle: PODropdownField.MenuStyle = PODropdownField.defaultMenu, - enabled: Boolean = true, isError: Boolean = false, placeholderText: String? = null ) { @@ -63,7 +62,7 @@ fun PODropdownField( onValueChange = {}, modifier = modifier.menuAnchor(), style = fieldStyle, - enabled = enabled, + enabled = true, readOnly = true, isDropdown = true, isError = isError, diff --git a/ui-core/src/main/kotlin/com/processout/sdk/ui/core/component/field/dropdown/POLabeledDropdownField.kt b/ui-core/src/main/kotlin/com/processout/sdk/ui/core/component/field/dropdown/POLabeledDropdownField.kt index 6d053b190..485a60ffc 100644 --- a/ui-core/src/main/kotlin/com/processout/sdk/ui/core/component/field/dropdown/POLabeledDropdownField.kt +++ b/ui-core/src/main/kotlin/com/processout/sdk/ui/core/component/field/dropdown/POLabeledDropdownField.kt @@ -23,7 +23,6 @@ fun POLabeledDropdownField( fieldStyle: POField.Style = POField.default, menuStyle: PODropdownField.MenuStyle = PODropdownField.defaultMenu, labelsStyle: POFieldLabels.Style = POFieldLabels.default, - enabled: Boolean = true, isError: Boolean = false, placeholderText: String? = null ) { @@ -39,7 +38,6 @@ fun POLabeledDropdownField( modifier = modifier, fieldStyle = fieldStyle, menuStyle = menuStyle, - enabled = enabled, isError = isError, placeholderText = placeholderText ) diff --git a/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/CardTokenizationScreen.kt b/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/CardTokenizationScreen.kt index 629bf0ae0..d4d2ac978 100644 --- a/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/CardTokenizationScreen.kt +++ b/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/CardTokenizationScreen.kt @@ -249,7 +249,6 @@ private fun DropdownField( }, fieldStyle = fieldStyle, menuStyle = menuStyle, - enabled = state.enabled, isError = state.isError, placeholderText = state.placeholder ) diff --git a/ui/src/main/kotlin/com/processout/sdk/ui/napm/NativeAlternativePaymentScreen.kt b/ui/src/main/kotlin/com/processout/sdk/ui/napm/NativeAlternativePaymentScreen.kt index d6d72a9cf..0e782e6e6 100644 --- a/ui/src/main/kotlin/com/processout/sdk/ui/napm/NativeAlternativePaymentScreen.kt +++ b/ui/src/main/kotlin/com/processout/sdk/ui/napm/NativeAlternativePaymentScreen.kt @@ -336,7 +336,6 @@ private fun DropdownField( }, fieldStyle = fieldStyle, menuStyle = menuStyle, - enabled = state.enabled, isError = state.isError, placeholderText = state.placeholder )