From 83038ae58bb0155dc8157e2a32a46bc49e876e85 Mon Sep 17 00:00:00 2001 From: Vitalii Vanziak Date: Tue, 22 Apr 2025 13:06:54 +0300 Subject: [PATCH 01/21] PreferredSchemeConfiguration --- .../POCardTokenizationConfiguration.kt | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/POCardTokenizationConfiguration.kt b/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/POCardTokenizationConfiguration.kt index 2ee665996..d40086039 100644 --- a/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/POCardTokenizationConfiguration.kt +++ b/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/POCardTokenizationConfiguration.kt @@ -21,6 +21,8 @@ import kotlinx.parcelize.Parcelize * @param[cvcRequired] Specifies whether the CVC field should be displayed. Default value is _true_. * @param[cardholderNameRequired] Specifies whether the cardholder name field should be displayed. Default value is _true_. * @param[cardScanner] Card scanner configuration. Use _null_ to hide, this is a default behaviour. + * @param[preferredScheme] Preferred scheme selection configuration. Shows scheme selection if co-scheme is available. + * Use _null_ to hide, this is a default behaviour. * @param[billingAddress] Allows to customize the collection of billing address. * @param[savingAllowed] Displays checkbox that allows to save the card details for future payments. * @param[submitButton] Submit button configuration. @@ -35,6 +37,7 @@ data class POCardTokenizationConfiguration( val cvcRequired: Boolean = true, val cardholderNameRequired: Boolean = true, val cardScanner: CardScannerConfiguration? = null, + val preferredScheme: PreferredSchemeConfiguration? = null, val billingAddress: BillingAddressConfiguration = BillingAddressConfiguration(), val savingAllowed: Boolean = false, val submitButton: Button = Button(), @@ -103,6 +106,18 @@ data class POCardTokenizationConfiguration( val configuration: POCardScannerConfiguration = POCardScannerConfiguration() ) : Parcelable + /** + * Preferred scheme selection configuration. + * + * @param[title] Preferred scheme section title. Set _null_ to use a default value or empty string to remove the title. + * @param[displayInline] Indicates whether selection field should be displayed inline. Default value is _true_. + */ + @Parcelize + data class PreferredSchemeConfiguration( + val title: String? = null, + val displayInline: Boolean = true + ) : Parcelable + /** * Defines billing address configuration. * From c18f1e3811f4fb22b960e9e22b8effc58274d79e Mon Sep 17 00:00:00 2001 From: Vitalii Vanziak Date: Tue, 22 Apr 2025 13:41:18 +0300 Subject: [PATCH 02/21] Added radio button style --- .../sdk/ui/card/tokenization/CardTokenizationScreen.kt | 5 +++++ .../ui/card/tokenization/POCardTokenizationConfiguration.kt | 1 + 2 files changed, 6 insertions(+) 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 326cc78fa..ebab42651 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 @@ -28,6 +28,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.checkbox.POCheckbox 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.POTextField import com.processout.sdk.ui.core.state.POActionState import com.processout.sdk.ui.core.state.POImmutableList @@ -369,6 +370,7 @@ internal object CardTokenizationScreen { val sectionTitle: POText.Style, val field: POField.Style, val checkbox: POCheckbox.Style, + val radioGroup: PORadioGroup.Style, val dropdownMenu: PODropdownField.MenuStyle, val errorMessage: POText.Style, val scanButton: POButton.Style, @@ -392,6 +394,9 @@ internal object CardTokenizationScreen { checkbox = custom?.checkbox?.let { POCheckbox.custom(style = it) } ?: POCheckbox.default, + radioGroup = custom?.radioButton?.let { + PORadioGroup.custom(style = it) + } ?: PORadioGroup.default, dropdownMenu = custom?.dropdownMenu?.let { PODropdownField.custom(style = it) } ?: PODropdownField.defaultMenu, diff --git a/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/POCardTokenizationConfiguration.kt b/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/POCardTokenizationConfiguration.kt index d40086039..41bc0e7d8 100644 --- a/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/POCardTokenizationConfiguration.kt +++ b/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/POCardTokenizationConfiguration.kt @@ -198,6 +198,7 @@ data class POCardTokenizationConfiguration( val sectionTitle: POTextStyle? = null, val field: POFieldStyle? = null, val checkbox: POCheckboxStyle? = null, + val radioButton: PORadioButtonStyle? = null, val dropdownMenu: PODropdownMenuStyle? = null, val errorMessage: POTextStyle? = null, val scanButton: POButtonStyle? = null, From 843755c6f13602f30d463d11d2cc024cd61cdfc8 Mon Sep 17 00:00:00 2001 From: Vitalii Vanziak Date: Tue, 22 Apr 2025 14:45:48 +0300 Subject: [PATCH 03/21] Added localizedMessage to ProcessOutResult.Failure --- .../sdk/api/repository/ApiFailureMapper.kt | 6 +++++- .../processout/sdk/core/ProcessOutResult.kt | 1 + ...NativeAlternativePaymentMethodViewModel.kt | 19 ++++++++++++------- 3 files changed, 18 insertions(+), 8 deletions(-) diff --git a/sdk/src/main/kotlin/com/processout/sdk/api/repository/ApiFailureMapper.kt b/sdk/src/main/kotlin/com/processout/sdk/api/repository/ApiFailureMapper.kt index 0a745a2e4..d3e14f95b 100644 --- a/sdk/src/main/kotlin/com/processout/sdk/api/repository/ApiFailureMapper.kt +++ b/sdk/src/main/kotlin/com/processout/sdk/api/repository/ApiFailureMapper.kt @@ -26,7 +26,11 @@ internal class ApiFailureMapper( append(" | Reason: $it") } } - return ProcessOutResult.Failure(failureCode, message, apiError?.invalidFields) + return ProcessOutResult.Failure( + code = failureCode, + message = message, + invalidFields = apiError?.invalidFields + ) } private fun failureCode(statusCode: Int, errorType: String): POFailure.Code { diff --git a/sdk/src/main/kotlin/com/processout/sdk/core/ProcessOutResult.kt b/sdk/src/main/kotlin/com/processout/sdk/core/ProcessOutResult.kt index 48b53add6..eddd0aaae 100644 --- a/sdk/src/main/kotlin/com/processout/sdk/core/ProcessOutResult.kt +++ b/sdk/src/main/kotlin/com/processout/sdk/core/ProcessOutResult.kt @@ -18,6 +18,7 @@ sealed class ProcessOutResult { data class Failure( val code: POFailure.Code, val message: String? = null, + val localizedMessage: String? = null, val invalidFields: List? = null, val cause: Exception? = null ) : ProcessOutResult() 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 234d2b000..cb5ec8392 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 @@ -94,8 +94,8 @@ internal class NativeAlternativePaymentMethodViewModel private constructor( private fun Options.validate() = copy( paymentConfirmationTimeoutSeconds = - if (paymentConfirmationTimeoutSeconds in 0..MAX_PAYMENT_CONFIRMATION_TIMEOUT_SECONDS) - paymentConfirmationTimeoutSeconds else DEFAULT_PAYMENT_CONFIRMATION_TIMEOUT_SECONDS + if (paymentConfirmationTimeoutSeconds in 0..MAX_PAYMENT_CONFIRMATION_TIMEOUT_SECONDS) + paymentConfirmationTimeoutSeconds else DEFAULT_PAYMENT_CONFIRMATION_TIMEOUT_SECONDS ) } @@ -322,9 +322,9 @@ internal class NativeAlternativePaymentMethodViewModel private constructor( val invalidFields = uiModel.inputParameters.mapNotNull { it.validate() } if (invalidFields.isNotEmpty()) { val failure = ProcessOutResult.Failure( - Validation(POFailure.ValidationCode.general), - "Invalid fields.", - invalidFields + code = Validation(POFailure.ValidationCode.general), + message = "Invalid fields.", + invalidFields = invalidFields ) handlePaymentFailure(uiModel, failure, replaceToLocalMessage = false) return@doWhenUserInput @@ -620,8 +620,13 @@ internal class NativeAlternativePaymentMethodViewModel private constructor( fun onViewFailure(failure: PONativeAlternativePaymentMethodResult.Failure) { with(failure) { dispatch( - DidFail(ProcessOutResult.Failure(code, message, invalidFields) - .also { POLogger.warn("View failed: %s", it, attributes = logAttributes) }) + DidFail( + ProcessOutResult.Failure( + code = code, + message = message, + invalidFields = invalidFields + ).also { POLogger.warn("View failed: %s", it, attributes = logAttributes) } + ) ) } } From a5b6617c0f4e09427961b8aa96f426ceb2d4124b Mon Sep 17 00:00:00 2001 From: Vitalii Vanziak Date: Tue, 22 Apr 2025 15:39:48 +0300 Subject: [PATCH 04/21] Prioritize localizedMessage as error message --- .../sdk/ui/card/tokenization/CardTokenizationInteractor.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/CardTokenizationInteractor.kt b/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/CardTokenizationInteractor.kt index c82592a8f..2a5540873 100644 --- a/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/CardTokenizationInteractor.kt +++ b/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/CardTokenizationInteractor.kt @@ -768,7 +768,7 @@ internal class CardTokenizationInteractor( private fun handle(failure: ProcessOutResult.Failure) { val invalidFieldIds = mutableSetOf() - val errorMessage = when (val code = failure.code) { + var errorMessage = when (val code = failure.code) { is Generic -> when (code.genericCode) { requestInvalidCard, cardInvalid -> { @@ -819,6 +819,7 @@ internal class CardTokenizationInteractor( Cancelled -> null else -> app.getString(R.string.po_card_tokenization_error_generic) } + failure.localizedMessage?.let { errorMessage = it } handle(failure, invalidFieldIds, errorMessage) } From 1df957a2bb001ead7a5e79acc5ba283f57018dd6 Mon Sep 17 00:00:00 2001 From: Vitalii Vanziak Date: Tue, 22 Apr 2025 15:43:04 +0300 Subject: [PATCH 05/21] Update example delegate --- .../ui/screen/card/payment/DefaultCardTokenizationDelegate.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/example/src/main/kotlin/com/processout/example/ui/screen/card/payment/DefaultCardTokenizationDelegate.kt b/example/src/main/kotlin/com/processout/example/ui/screen/card/payment/DefaultCardTokenizationDelegate.kt index 1dc8fd6ff..fccca818f 100644 --- a/example/src/main/kotlin/com/processout/example/ui/screen/card/payment/DefaultCardTokenizationDelegate.kt +++ b/example/src/main/kotlin/com/processout/example/ui/screen/card/payment/DefaultCardTokenizationDelegate.kt @@ -24,7 +24,7 @@ class DefaultCardTokenizationDelegate( if (invoice == null) { return ProcessOutResult.Failure( code = Generic(), - message = "Failed to create an invoice." + localizedMessage = "Failed to create an invoice." ) } return invoices.authorize( From 287ebd08cec3f271224bf3fa6b2da2ca45b96df0 Mon Sep 17 00:00:00 2001 From: Vitalii Vanziak Date: Tue, 22 Apr 2025 16:37:31 +0300 Subject: [PATCH 06/21] Update futurePaymentsSection() --- .../sdk/ui/card/tokenization/CardTokenizationViewModel.kt | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/CardTokenizationViewModel.kt b/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/CardTokenizationViewModel.kt index 05fa87406..ef42b6ba9 100644 --- a/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/CardTokenizationViewModel.kt +++ b/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/CardTokenizationViewModel.kt @@ -141,7 +141,7 @@ internal class CardTokenizationViewModel private constructor( val sections = listOf( cardInformationSection(state, lastFocusableFieldId), billingAddressSection(state, lastFocusableFieldId), - futurePaymentsSection(state.saveCardField) + futurePaymentsSection(state) ) return POImmutableList(sections.filterNotNull()) } @@ -318,7 +318,10 @@ internal class CardTokenizationViewModel private constructor( ) } - private fun futurePaymentsSection(saveCardField: Field): Section? { + private fun futurePaymentsSection( + state: CardTokenizationInteractorState + ): Section? { + val saveCardField = state.saveCardField if (!saveCardField.shouldCollect) { return null } From 880ab30aaf0cc3d1be85121fd3a78f0bddae84cf Mon Sep 17 00:00:00 2001 From: Vitalii Vanziak Date: Tue, 22 Apr 2025 20:16:54 +0300 Subject: [PATCH 07/21] Preferred scheme UI (radio/dropdown) --- .../CardTokenizationInteractorState.kt | 4 +- .../tokenization/CardTokenizationScreen.kt | 44 ++++++++-- .../tokenization/CardTokenizationViewModel.kt | 81 ++++++++++++++----- .../CardTokenizationViewModelState.kt | 2 + 4 files changed, 103 insertions(+), 28 deletions(-) diff --git a/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/CardTokenizationInteractorState.kt b/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/CardTokenizationInteractorState.kt index c75a8b772..8439b3c8a 100644 --- a/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/CardTokenizationInteractorState.kt +++ b/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/CardTokenizationInteractorState.kt @@ -10,7 +10,7 @@ internal data class CardTokenizationInteractorState( val started: Boolean = false, val cardFields: List, val addressFields: List, - val addressSpecification: AddressSpecification? = null, + val preferredSchemeField: Field, val saveCardField: Field, val focusedFieldId: String?, val pendingFocusedFieldId: String?, @@ -20,6 +20,7 @@ internal data class CardTokenizationInteractorState( val submitAllowed: Boolean = true, val submitting: Boolean = false, val errorMessage: String? = null, + val addressSpecification: AddressSpecification? = null, val issuerInformation: POCardIssuerInformation? = null, val preferredScheme: String? = null, val tokenizedCard: POCard? = null @@ -50,6 +51,7 @@ internal data class CardTokenizationInteractorState( } object FieldId { + const val PREFERRED_SCHEME = "preferred-scheme" const val SAVE_CARD = "save-card" } 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 ebab42651..924bb37be 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 @@ -24,6 +24,7 @@ import androidx.lifecycle.Lifecycle import com.processout.sdk.ui.card.tokenization.CardTokenizationEvent.* import com.processout.sdk.ui.card.tokenization.CardTokenizationViewModelState.Item import com.processout.sdk.ui.card.tokenization.CardTokenizationViewModelState.SectionId.FUTURE_PAYMENTS +import com.processout.sdk.ui.card.tokenization.CardTokenizationViewModelState.SectionId.PREFERRED_SCHEME import com.processout.sdk.ui.core.component.* import com.processout.sdk.ui.core.component.field.POField import com.processout.sdk.ui.core.component.field.checkbox.POCheckbox @@ -128,13 +129,15 @@ private fun Sections( } val lifecycleEvent = rememberLifecycleEvent() state.sections.elements.forEachIndexed { index, section -> - val padding = if (section.id == FUTURE_PAYMENTS) { - spacing.small - } else when (index) { - 0 -> 0.dp - else -> spacing.extraLarge + val verticalPadding = when (section.id) { + PREFERRED_SCHEME -> if (section.title == null) spacing.small else spacing.extraLarge + FUTURE_PAYMENTS -> spacing.small + else -> when (index) { + 0 -> 0.dp + else -> spacing.extraLarge + } } - Spacer(Modifier.requiredHeight(padding)) + Spacer(Modifier.requiredHeight(verticalPadding)) Column( verticalArrangement = Arrangement.spacedBy(spacing.small) ) { @@ -189,6 +192,12 @@ private fun Item( style = style.field, modifier = modifier ) + is Item.RadioField -> RadioField( + state = item.state, + onEvent = onEvent, + style = style.radioGroup, + modifier = modifier + ) is Item.DropdownField -> DropdownField( state = item.state, onEvent = onEvent, @@ -271,6 +280,29 @@ private fun TextField( } } +@Composable +private fun RadioField( + state: FieldState, + onEvent: (CardTokenizationEvent) -> Unit, + style: PORadioGroup.Style, + modifier: Modifier = Modifier +) { + PORadioGroup( + value = state.value.text, + onValueChange = { + onEvent( + FieldValueChanged( + id = state.id, + value = TextFieldValue(text = it) + ) + ) + }, + availableValues = state.availableValues ?: POImmutableList(emptyList()), + modifier = modifier, + style = style + ) +} + @Composable private fun DropdownField( state: FieldState, diff --git a/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/CardTokenizationViewModel.kt b/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/CardTokenizationViewModel.kt index ef42b6ba9..39fe147a6 100644 --- a/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/CardTokenizationViewModel.kt +++ b/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/CardTokenizationViewModel.kt @@ -140,6 +140,7 @@ internal class CardTokenizationViewModel private constructor( val lastFocusableFieldId = lastFocusableFieldId(state) val sections = listOf( cardInformationSection(state, lastFocusableFieldId), + preferredSchemeSection(state), billingAddressSection(state, lastFocusableFieldId), futurePaymentsSection(state) ) @@ -318,6 +319,35 @@ internal class CardTokenizationViewModel private constructor( ) } + private fun preferredSchemeSection( + state: CardTokenizationInteractorState + ): Section? { + val preferredSchemeField = preferredSchemeField(state) ?: return null + val title = configuration.preferredScheme?.title.let { title -> + if (title == null) + app.getString(R.string.po_card_tokenization_preferred_scheme) + else title.ifBlank { null } + } + return Section( + id = SectionId.PREFERRED_SCHEME, + title = title, + items = POImmutableList(listOf(preferredSchemeField)) + ) + } + + private fun preferredSchemeField( + state: CardTokenizationInteractorState + ): Item? { + val field = state.preferredSchemeField + if (!field.shouldCollect) { + return null + } + val displayInline = configuration.preferredScheme?.displayInline ?: true + return if (displayInline) + radioField(field) + else dropdownField(field) + } + private fun futurePaymentsSection( state: CardTokenizationInteractorState ): Section? { @@ -378,27 +408,6 @@ internal class CardTokenizationViewModel private constructor( ) } - private fun dropdownField(field: Field): Item = - Item.DropdownField( - FieldState( - id = field.id, - value = field.value, - availableValues = POImmutableList(field.availableValues ?: emptyList()) - ) - ) - - private fun checkboxField( - field: Field, - title: String? = null - ): Item = Item.CheckboxField( - FieldState( - id = field.id, - value = field.value, - title = title, - isError = !field.isValid - ) - ) - private fun textField( field: Field, placeholder: String? = null, @@ -422,4 +431,34 @@ internal class CardTokenizationViewModel private constructor( keyboardActionId = keyboardActionId ) ) + + private fun radioField(field: Field): Item = + Item.RadioField( + FieldState( + id = field.id, + value = field.value, + availableValues = POImmutableList(field.availableValues ?: emptyList()) + ) + ) + + private fun dropdownField(field: Field): Item = + Item.DropdownField( + FieldState( + id = field.id, + value = field.value, + availableValues = POImmutableList(field.availableValues ?: emptyList()) + ) + ) + + private fun checkboxField( + field: Field, + title: String? = null + ): Item = Item.CheckboxField( + FieldState( + id = field.id, + value = field.value, + title = title, + isError = !field.isValid + ) + ) } diff --git a/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/CardTokenizationViewModelState.kt b/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/CardTokenizationViewModelState.kt index 1d3a02b8d..6cb8837d3 100644 --- a/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/CardTokenizationViewModelState.kt +++ b/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/CardTokenizationViewModelState.kt @@ -27,6 +27,7 @@ internal data class CardTokenizationViewModelState( @Immutable sealed interface Item { data class TextField(val state: FieldState) : Item + data class RadioField(val state: FieldState) : Item data class DropdownField(val state: FieldState) : Item data class CheckboxField(val state: FieldState) : Item data class Group(val items: POImmutableList) : Item @@ -34,6 +35,7 @@ internal data class CardTokenizationViewModelState( object SectionId { const val CARD_INFORMATION = "card-information" + const val PREFERRED_SCHEME = "preferred-scheme" const val BILLING_ADDRESS = "billing-address" const val FUTURE_PAYMENTS = "future-payments" } From 531da6e9a9aeda25ff7de42427f954e68da0a3e6 Mon Sep 17 00:00:00 2001 From: Vitalii Vanziak Date: Tue, 22 Apr 2025 20:19:38 +0300 Subject: [PATCH 08/21] preferredSchemeField in interactor --- .../card/tokenization/CardTokenizationInteractor.kt | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/CardTokenizationInteractor.kt b/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/CardTokenizationInteractor.kt index 2a5540873..98b928064 100644 --- a/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/CardTokenizationInteractor.kt +++ b/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/CardTokenizationInteractor.kt @@ -126,6 +126,10 @@ internal class CardTokenizationInteractor( private fun initState() = CardTokenizationInteractorState( cardFields = cardFields(), addressFields = emptyList(), + preferredSchemeField = Field( + id = FieldId.PREFERRED_SCHEME, + shouldCollect = false + ), saveCardField = Field( id = FieldId.SAVE_CARD, value = TextFieldValue(text = "false"), @@ -229,6 +233,7 @@ internal class CardTokenizationInteractor( addressFields = it.addressFields.map { field -> updatedField(id, value, field, isTextChanged) }, + preferredSchemeField = updatedField(id, value, it.preferredSchemeField, isTextChanged), saveCardField = updatedField(id, value, it.saveCardField, isTextChanged) ) } @@ -561,7 +566,9 @@ internal class CardTokenizationInteractor( tokenize(tokenizationRequest()) } - private fun allFields(): List = with(_state.value) { cardFields + addressFields + saveCardField } + private fun allFields(): List = with(_state.value) { + cardFields + addressFields + preferredSchemeField + saveCardField + } private fun areAllFieldsValid(): Boolean = allFields().all { it.isValid } @@ -834,8 +841,9 @@ internal class CardTokenizationInteractor( val addressFields = _state.value.addressFields.map { field -> validatedField(invalidFieldIds, field) } + val preferredSchemeField = validatedField(invalidFieldIds, _state.value.preferredSchemeField) val saveCardField = validatedField(invalidFieldIds, _state.value.saveCardField) - val allFields = cardFields + addressFields + saveCardField + val allFields = cardFields + addressFields + preferredSchemeField + saveCardField val firstInvalidFieldId = allFields.find { !it.isValid }?.id _state.update { state -> state.copy( From 1c67ccc414b747d2b62872762a91f292676284c8 Mon Sep 17 00:00:00 2001 From: Vitalii Vanziak Date: Tue, 22 Apr 2025 20:56:40 +0300 Subject: [PATCH 09/21] Replace preferredScheme property with preferredSchemeField in interactor --- .../CardTokenizationInteractor.kt | 6 ++-- .../CardTokenizationInteractorState.kt | 1 - .../tokenization/CardTokenizationViewModel.kt | 29 +++++++++---------- 3 files changed, 17 insertions(+), 19 deletions(-) diff --git a/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/CardTokenizationInteractor.kt b/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/CardTokenizationInteractor.kt index 98b928064..370739a78 100644 --- a/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/CardTokenizationInteractor.kt +++ b/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/CardTokenizationInteractor.kt @@ -415,7 +415,9 @@ internal class CardTokenizationInteractor( _state.update { it.copy( issuerInformation = issuerInformation, - preferredScheme = preferredScheme + preferredSchemeField = it.preferredSchemeField.copy( + value = TextFieldValue(text = preferredScheme ?: String()) + ) ) } POLogger.info("State updated: [issuerInformation=%s] [preferredScheme=%s]", issuerInformation, preferredScheme) @@ -596,7 +598,7 @@ internal class CardTokenizationInteractor( expYear = parsedExpiration.year, cvc = cvc, name = cardholderName, - preferredScheme = _state.value.preferredScheme, + preferredScheme = _state.value.preferredSchemeField.value.text, contact = contact(), metadata = configuration.metadata ) diff --git a/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/CardTokenizationInteractorState.kt b/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/CardTokenizationInteractorState.kt index 8439b3c8a..1d303a694 100644 --- a/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/CardTokenizationInteractorState.kt +++ b/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/CardTokenizationInteractorState.kt @@ -22,7 +22,6 @@ internal data class CardTokenizationInteractorState( val errorMessage: String? = null, val addressSpecification: AddressSpecification? = null, val issuerInformation: POCardIssuerInformation? = null, - val preferredScheme: String? = null, val tokenizedCard: POCard? = null ) { diff --git a/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/CardTokenizationViewModel.kt b/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/CardTokenizationViewModel.kt index 39fe147a6..c84beb5e5 100644 --- a/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/CardTokenizationViewModel.kt +++ b/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/CardTokenizationViewModel.kt @@ -169,22 +169,19 @@ internal class CardTokenizationViewModel private constructor( state.cardFields.forEach { field -> val keyboardAction = keyboardAction(field.id, lastFocusableFieldId) when (field.id) { - CardFieldId.NUMBER -> { - val scheme = state.preferredScheme ?: state.issuerInformation?.scheme - cardNumberField = field( - field = field, - placeholder = app.getString(R.string.po_card_tokenization_card_details_number_placeholder), - iconResId = scheme?.let { cardSchemeDrawableResId(it) }, - forceTextDirectionLtr = true, - inputFilter = CardNumberInputFilter(), - visualTransformation = CardNumberVisualTransformation(), - keyboardOptions = KeyboardOptions( - keyboardType = KeyboardType.Number, - imeAction = keyboardAction.imeAction - ), - keyboardActionId = keyboardAction.actionId - ) - } + CardFieldId.NUMBER -> cardNumberField = field( + field = field, + placeholder = app.getString(R.string.po_card_tokenization_card_details_number_placeholder), + iconResId = cardSchemeDrawableResId(scheme = state.preferredSchemeField.value.text), + forceTextDirectionLtr = true, + inputFilter = CardNumberInputFilter(), + visualTransformation = CardNumberVisualTransformation(), + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Number, + imeAction = keyboardAction.imeAction + ), + keyboardActionId = keyboardAction.actionId + ) CardFieldId.EXPIRATION -> trackFields.add( field( field = field, From c29ff8090ddf33c26b36988da2e3b143688a6c65 Mon Sep 17 00:00:00 2001 From: Vitalii Vanziak Date: Wed, 23 Apr 2025 18:03:19 +0300 Subject: [PATCH 10/21] POCardScheme --- .../sdk/api/model/response/POCardScheme.kt | 301 ++++++++++++++++++ 1 file changed, 301 insertions(+) create mode 100644 sdk/src/main/kotlin/com/processout/sdk/api/model/response/POCardScheme.kt diff --git a/sdk/src/main/kotlin/com/processout/sdk/api/model/response/POCardScheme.kt b/sdk/src/main/kotlin/com/processout/sdk/api/model/response/POCardScheme.kt new file mode 100644 index 000000000..c8246eeb7 --- /dev/null +++ b/sdk/src/main/kotlin/com/processout/sdk/api/model/response/POCardScheme.kt @@ -0,0 +1,301 @@ +package com.processout.sdk.api.model.response + +/** + * Supported card schemes and co-schemes. + */ +enum class POCardScheme( + val rawValue: String, + val displayName: String +) { + + /** American Express is a key credit card around the world. */ + AMEX( + rawValue = "american express", + displayName = "American Express" + ), + + /** Atos Private Label is a private label credit card that is branded for Atos. */ + ATOS_PRIVATE_LABEL( + rawValue = "atos private label", + displayName = "Atos Private Label" + ), + + /** Bancontact is the most popular online payment method in Belgium. */ + BANCONTACT( + rawValue = "bancontact", + displayName = "Bancontact" + ), + + /** BC Global is a South Korean domestic card brand with international acceptance. */ + BC_GLOBAL( + rawValue = "global bc", + displayName = "BC Global" + ), + + /** Cabal is a local credit and debit card payment method based in Argentina. */ + CABAL( + rawValue = "cabal", + displayName = "Cabal" + ), + + /** Carnet is a leading brand of Mexican acceptance, with more than 50 years of experience. */ + CARNET( + rawValue = "carnet", + displayName = "Carnet" + ), + + /** Carte Bancaire is France's local card scheme and the most widely used payment method in the region. */ + CARTE_BANCAIRE( + rawValue = "carte bancaire", + displayName = "Carte Bancaire" + ), + + /** Cirrus is a worldwide interbank network that provides cash to Mastercard cardholders. */ + CIRRUS( + rawValue = "cirrus", + displayName = "Cirrus" + ), + + /** Cielo is a domestic debit and credit card brand of Brazil. */ + CIELO( + rawValue = "cielo", + displayName = "Cielo" + ), + + /** Comprocard is a domestic debit and credit card brand of Brazil. */ + COMPROCARD( + rawValue = "comprocard", + displayName = "Comprocard" + ), + + /** Dankort is the national debit card of Denmark. */ + DANKORT( + rawValue = "dankort", + displayName = "Dankort" + ), + + /** DinaCard is a national payment card of the Republic of Serbia. */ + DINA_CARD( + rawValue = "dinacard", + displayName = "DinaCard" + ), + + /** Diners charge card. */ + DINERS_CLUB( + rawValue = "diners club", + displayName = "Diners Club" + ), + + /** Diners charge card. */ + DINERS_CLUB_CARTE_BLANCHE( + rawValue = "diners club carte blanche", + displayName = "Diners Club Carte Blanche" + ), + + /** Diners charge card. */ + DINERS_CLUB_INTERNATIONAL( + rawValue = "diners club international", + displayName = "Diners Club International" + ), + + /** Diners charge card. */ + DINERS_CLUB_UNITED_STATES_AND_CANADA( + rawValue = "diners club united states & canada", + displayName = "Diners Club United States & Canada" + ), + + /** Discover is a credit card brand issued primarily in the United States. */ + DISCOVER( + rawValue = "discover", + displayName = "Discover" + ), + + /** Elo is a domestic debit and credit card brand of Brazil. */ + ELO( + rawValue = "elo", + displayName = "Elo" + ), + + /** An Electron debit card. */ + ELECTRON( + rawValue = "electron", + displayName = "Electron" + ), + + /** GE Capital is the financial services division of General Electric. */ + GE_CAPITAL( + rawValue = "ge capital", + displayName = "GE Capital" + ), + + /** A Girocard payment method. */ + GIROCARD( + rawValue = "girocard", + displayName = "Girocard" + ), + + /** Giropay is an Internet payment system in Germany. */ + GIROPAY( + rawValue = "giropay", + displayName = "Giropay" + ), + + /** Hipercard is a domestic debit and credit card brand of Brazil. */ + HIPERCARD( + rawValue = "hipercard", + displayName = "Hipercard" + ), + + /** An iD payment card. */ + ID_CREDIT( + rawValue = "idCredit", + displayName = "iD Credit" + ), + + /** An Interac payment method. */ + INTERAC( + rawValue = "interac", + displayName = "Interac" + ), + + /** JCB is a major card issuer and acquirer from Japan. */ + JCB( + rawValue = "jcb", + displayName = "JCB" + ), + + /** Maestro is a brand of debit cards and prepaid cards owned by Mastercard. */ + MAESTRO( + rawValue = "maestro", + displayName = "Maestro" + ), + + /** Mada is the national payment scheme of Saudi Arabia. */ + MADA( + rawValue = "mada", + displayName = "Mada" + ), + + /** Mastercard is a market leading card scheme worldwide. */ + MASTERCARD( + rawValue = "mastercard", + displayName = "Mastercard" + ), + + /** A Meeza payment card. */ + MEEZA( + rawValue = "meeza", + displayName = "Meeza" + ), + + /** A Mir payment card. */ + MIR( + rawValue = "nspk mir", + displayName = "Mir" + ), + + /** A Nanaco payment card. */ + NANACO( + rawValue = "nanaco", + displayName = "Nanaco" + ), + + /** UK Credit Cards issued by NewDay. */ + NEWDAY( + rawValue = "newday", + displayName = "NewDay" + ), + + /** NYCE is an interbank network connecting the ATMs of various financial institutions in the United States and Canada. */ + NYCE( + rawValue = "nyce", + displayName = "NYCE" + ), + + /** Ourocard is a domestic debit and credit card brand of Brazil. */ + OUROCARD( + rawValue = "ourocard", + displayName = "Ourocard" + ), + + /** A Bancomat payment card. */ + PAGO_BANCOMAT( + rawValue = "pagoBancomat", + displayName = "PagoBancomat" + ), + + /** A PostFinance AG payment card. */ + POST_FINANCE( + rawValue = "postFinance", + displayName = "PostFinance" + ), + + /** Private Label is a type of credit card that is branded for a specific retailer or brand. */ + PRIVATE_LABEL( + rawValue = "private label", + displayName = "Private Label" + ), + + /** RuPay is an Indian multinational financial services and payment service system. */ + RUPAY( + rawValue = "rupay", + displayName = "RuPay" + ), + + /** Sodexo is a company that offers prepaid meal cards and other prepaid services. */ + SODEXO( + rawValue = "sodexo", + displayName = "Sodexo" + ), + + /** A Suica payment card. */ + SUICA( + rawValue = "suica", + displayName = "Suica" + ), + + /** A T-money payment card. */ + T_MONEY( + rawValue = "tmoney", + displayName = "T-money" + ), + + /** TROY (acronym of Türkiye’nin Ödeme Yöntemi) is a Turkish card scheme. */ + TROY( + rawValue = "troy", + displayName = "TROY" + ), + + /** UnionPay is the world’s biggest card network with more than 7 billion cards issued. */ + UNION_PAY( + rawValue = "china union pay", + displayName = "UnionPay" + ), + + /** + * V Pay is a Single Euro Payments Area (SEPA) debit card for use in Europe, issued by Visa Europe. + * It uses the EMV chip and PIN system and may be co-branded with various national debit card schemes such as the German Girocard. + */ + V_PAY( + rawValue = "vpay", + displayName = "V Pay" + ), + + /** Verve is Africa's most successful card brand. */ + VERVE( + rawValue = "verve", + displayName = "Verve" + ), + + /** Visa is the largest global card network in the world by transaction value, ubiquitous worldwide. */ + VISA( + rawValue = "visa", + displayName = "Visa" + ), + + /** A WAON payment card. */ + WAON( + rawValue = "waon", + displayName = "WAON" + ) +} From 61efbc7954d5b31e53079793a59f422c3c83de97 Mon Sep 17 00:00:00 2001 From: Vitalii Vanziak Date: Wed, 23 Apr 2025 18:15:58 +0300 Subject: [PATCH 11/21] EnumExtensions in UI module --- .../com/processout/sdk/ui/shared/extension/EnumExtensions.kt | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 ui/src/main/kotlin/com/processout/sdk/ui/shared/extension/EnumExtensions.kt diff --git a/ui/src/main/kotlin/com/processout/sdk/ui/shared/extension/EnumExtensions.kt b/ui/src/main/kotlin/com/processout/sdk/ui/shared/extension/EnumExtensions.kt new file mode 100644 index 000000000..b0cd23563 --- /dev/null +++ b/ui/src/main/kotlin/com/processout/sdk/ui/shared/extension/EnumExtensions.kt @@ -0,0 +1,5 @@ +package com.processout.sdk.ui.shared.extension + +internal inline infix fun , V> ((E) -> V).findBy(value: V): E? { + return enumValues().firstOrNull { this(it) == value } +} From acc6e48a364ccb6efa347825e30eaa21bddb6d5f Mon Sep 17 00:00:00 2001 From: Vitalii Vanziak Date: Wed, 23 Apr 2025 18:32:12 +0300 Subject: [PATCH 12/21] Use POCardScheme in CardSchemeDrawableResProvider --- .../provider/CardSchemeDrawableResProvider.kt | 45 ++++++++++--------- 1 file changed, 24 insertions(+), 21 deletions(-) diff --git a/ui/src/main/kotlin/com/processout/sdk/ui/shared/provider/CardSchemeDrawableResProvider.kt b/ui/src/main/kotlin/com/processout/sdk/ui/shared/provider/CardSchemeDrawableResProvider.kt index 8b818658a..46edc2998 100644 --- a/ui/src/main/kotlin/com/processout/sdk/ui/shared/provider/CardSchemeDrawableResProvider.kt +++ b/ui/src/main/kotlin/com/processout/sdk/ui/shared/provider/CardSchemeDrawableResProvider.kt @@ -1,30 +1,33 @@ package com.processout.sdk.ui.shared.provider import androidx.annotation.DrawableRes +import com.processout.sdk.api.model.response.POCardScheme +import com.processout.sdk.api.model.response.POCardScheme.* import com.processout.sdk.ui.R +import com.processout.sdk.ui.shared.extension.findBy @DrawableRes internal fun cardSchemeDrawableResId(scheme: String): Int? = - when (scheme.lowercase()) { - "american express" -> R.drawable.po_scheme_amex - "carte bancaire" -> R.drawable.po_scheme_carte_bancaire - "dinacard" -> R.drawable.po_scheme_dinacard - "diners club" -> R.drawable.po_scheme_diners - "diners club carte blanche" -> R.drawable.po_scheme_diners - "diners club international" -> R.drawable.po_scheme_diners - "diners club united states & canada" -> R.drawable.po_scheme_diners - "discover" -> R.drawable.po_scheme_discover - "elo" -> R.drawable.po_scheme_elo - "giropay" -> R.drawable.po_scheme_giropay - "jcb" -> R.drawable.po_scheme_jcb - "mada" -> R.drawable.po_scheme_mada - "maestro" -> R.drawable.po_scheme_maestro - "mastercard" -> R.drawable.po_scheme_mastercard - "rupay" -> R.drawable.po_scheme_rupay - "sodexo" -> R.drawable.po_scheme_sodexo - "china union pay" -> R.drawable.po_scheme_union_pay - "verve" -> R.drawable.po_scheme_verve - "visa" -> R.drawable.po_scheme_visa - "vpay" -> R.drawable.po_scheme_vpay + when (POCardScheme::rawValue.findBy(scheme)) { + AMEX -> R.drawable.po_scheme_amex + CARTE_BANCAIRE -> R.drawable.po_scheme_carte_bancaire + DINA_CARD -> R.drawable.po_scheme_dinacard + DINERS_CLUB, + DINERS_CLUB_CARTE_BLANCHE, + DINERS_CLUB_INTERNATIONAL, + DINERS_CLUB_UNITED_STATES_AND_CANADA -> R.drawable.po_scheme_diners + DISCOVER -> R.drawable.po_scheme_discover + ELO -> R.drawable.po_scheme_elo + GIROPAY -> R.drawable.po_scheme_giropay + JCB -> R.drawable.po_scheme_jcb + MADA -> R.drawable.po_scheme_mada + MAESTRO -> R.drawable.po_scheme_maestro + MASTERCARD -> R.drawable.po_scheme_mastercard + RUPAY -> R.drawable.po_scheme_rupay + SODEXO -> R.drawable.po_scheme_sodexo + UNION_PAY -> R.drawable.po_scheme_union_pay + VERVE -> R.drawable.po_scheme_verve + VISA -> R.drawable.po_scheme_visa + V_PAY -> R.drawable.po_scheme_vpay else -> null } From a1091370892876323165ca89644d0c55d8491120 Mon Sep 17 00:00:00 2001 From: Vitalii Vanziak Date: Wed, 23 Apr 2025 18:44:30 +0300 Subject: [PATCH 13/21] Use POCardScheme in CardSchemeProvider --- .../ui/shared/provider/CardSchemeProvider.kt | 37 ++++++++++--------- 1 file changed, 19 insertions(+), 18 deletions(-) diff --git a/ui/src/main/kotlin/com/processout/sdk/ui/shared/provider/CardSchemeProvider.kt b/ui/src/main/kotlin/com/processout/sdk/ui/shared/provider/CardSchemeProvider.kt index 675a9a749..b55aff3bf 100644 --- a/ui/src/main/kotlin/com/processout/sdk/ui/shared/provider/CardSchemeProvider.kt +++ b/ui/src/main/kotlin/com/processout/sdk/ui/shared/provider/CardSchemeProvider.kt @@ -1,5 +1,6 @@ package com.processout.sdk.ui.shared.provider +import com.processout.sdk.api.model.response.POCardScheme.* import com.processout.sdk.ui.shared.provider.CardSchemeProvider.IssuerNumbers.* import com.processout.sdk.ui.shared.provider.CardSchemeProvider.IssuerNumbers.Set import kotlin.math.pow @@ -25,9 +26,9 @@ internal class CardSchemeProvider { // https://www.bincodes.com/bin-list // Sorted by length descending to handle overlapping numbers (like 622126 and 62). private val issuers: List = listOf( - Issuer(scheme = "discover", numbers = Range(622126..622925), length = 6), + Issuer(scheme = DISCOVER.rawValue, numbers = Range(622126..622925), length = 6), Issuer( - scheme = "elo", + scheme = ELO.rawValue, numbers = Set( setOf( 401178, 401179, 431274, 438935, 451416, 457393, 457631, 457632, 504175, 506699, 506770, 506771, @@ -46,7 +47,7 @@ internal class CardSchemeProvider { length = 6 ), Issuer( - scheme = "elo", + scheme = ELO.rawValue, numbers = Set( setOf( 50670, 50671, 50672, 50673, 50674, 50675, 50676, 65004, 65041, 65042, 65043, 65049, 65050, 65051, @@ -56,21 +57,21 @@ internal class CardSchemeProvider { ), length = 5 ), - Issuer(scheme = "discover", numbers = Exact(6011), length = 4), - Issuer(scheme = "jcb", numbers = Range(3528..3589), length = 4), - Issuer(scheme = "elo", numbers = Exact(509), length = 3), - Issuer(scheme = "discover", numbers = Range(644..649), length = 3), - Issuer(scheme = "diners club carte blanche", numbers = Range(300..305), length = 3), - Issuer(scheme = "diners club international", numbers = Exact(309), length = 3), - Issuer(scheme = "mastercard", numbers = Range(51..55), length = 2), - Issuer(scheme = "discover", numbers = Exact(65), length = 2), - Issuer(scheme = "china union pay", numbers = Exact(62), length = 2), - Issuer(scheme = "american express", numbers = Set(setOf(34, 37)), length = 2), - Issuer(scheme = "maestro", numbers = Set(setOf(50, 56, 57, 58, 59)), length = 2), - Issuer(scheme = "diners club international", numbers = Set(setOf(36, 38, 39)), length = 2), - Issuer(scheme = "diners club united states & canada", numbers = Range(54..55), length = 2), - Issuer(scheme = "visa", numbers = Exact(4), length = 1), - Issuer(scheme = "maestro", numbers = Exact(6), length = 1) + Issuer(scheme = DISCOVER.rawValue, numbers = Exact(6011), length = 4), + Issuer(scheme = JCB.rawValue, numbers = Range(3528..3589), length = 4), + Issuer(scheme = ELO.rawValue, numbers = Exact(509), length = 3), + Issuer(scheme = DISCOVER.rawValue, numbers = Range(644..649), length = 3), + Issuer(scheme = DINERS_CLUB_CARTE_BLANCHE.rawValue, numbers = Range(300..305), length = 3), + Issuer(scheme = DINERS_CLUB_INTERNATIONAL.rawValue, numbers = Exact(309), length = 3), + Issuer(scheme = MASTERCARD.rawValue, numbers = Range(51..55), length = 2), + Issuer(scheme = DISCOVER.rawValue, numbers = Exact(65), length = 2), + Issuer(scheme = UNION_PAY.rawValue, numbers = Exact(62), length = 2), + Issuer(scheme = AMEX.rawValue, numbers = Set(setOf(34, 37)), length = 2), + Issuer(scheme = MAESTRO.rawValue, numbers = Set(setOf(50, 56, 57, 58, 59)), length = 2), + Issuer(scheme = DINERS_CLUB_INTERNATIONAL.rawValue, numbers = Set(setOf(36, 38, 39)), length = 2), + Issuer(scheme = DINERS_CLUB_UNITED_STATES_AND_CANADA.rawValue, numbers = Range(54..55), length = 2), + Issuer(scheme = VISA.rawValue, numbers = Exact(4), length = 1), + Issuer(scheme = MAESTRO.rawValue, numbers = Exact(6), length = 1) ) fun scheme(cardNumber: String): String? { From c82377d29d6806ae60123da5384c693f7e1849b9 Mon Sep 17 00:00:00 2001 From: Vitalii Vanziak Date: Wed, 23 Apr 2025 19:21:30 +0300 Subject: [PATCH 14/21] Update preferredSchemeField --- .../tokenization/CardTokenizationInteractor.kt | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/CardTokenizationInteractor.kt b/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/CardTokenizationInteractor.kt index 370739a78..4c0574a26 100644 --- a/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/CardTokenizationInteractor.kt +++ b/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/CardTokenizationInteractor.kt @@ -29,6 +29,7 @@ import com.processout.sdk.ui.card.tokenization.CardTokenizationSideEffect.CardSc import com.processout.sdk.ui.card.tokenization.POCardTokenizationConfiguration.BillingAddressConfiguration.CollectionMode.* import com.processout.sdk.ui.core.state.POAvailableValue import com.processout.sdk.ui.shared.extension.currentAppLocale +import com.processout.sdk.ui.shared.extension.findBy import com.processout.sdk.ui.shared.extension.orElse import com.processout.sdk.ui.shared.filter.CardExpirationInputFilter import com.processout.sdk.ui.shared.filter.CardNumberInputFilter @@ -412,17 +413,31 @@ internal class CardTokenizationInteractor( issuerInformation: POCardIssuerInformation?, preferredScheme: String? ) { + val availableSchemes = listOfNotNull( + availableScheme(issuerInformation?.scheme), + availableScheme(issuerInformation?.coScheme) + ) _state.update { it.copy( issuerInformation = issuerInformation, preferredSchemeField = it.preferredSchemeField.copy( - value = TextFieldValue(text = preferredScheme ?: String()) + value = TextFieldValue(text = preferredScheme ?: String()), + availableValues = availableSchemes, + shouldCollect = configuration.preferredScheme != null && availableSchemes.size > 1 ) ) } POLogger.info("State updated: [issuerInformation=%s] [preferredScheme=%s]", issuerInformation, preferredScheme) } + private fun availableScheme(scheme: String?): POAvailableValue? = + POCardScheme::rawValue.findBy(scheme)?.let { + POAvailableValue( + value = it.rawValue, + text = it.displayName + ) + } + //endregion //region Address Specification From 27e663ddda84ddeec8e9baa8e90bde78ffad5e7f Mon Sep 17 00:00:00 2001 From: Vitalii Vanziak Date: Thu, 24 Apr 2025 15:58:55 +0300 Subject: [PATCH 15/21] Preferred scheme subsection with animations --- .../tokenization/CardTokenizationScreen.kt | 110 ++++++++++++------ .../tokenization/CardTokenizationViewModel.kt | 4 +- .../CardTokenizationViewModelState.kt | 5 +- .../ui/checkout/screen/CardTokenization.kt | 5 +- 4 files changed, 82 insertions(+), 42 deletions(-) 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 924bb37be..8fa0b596d 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 @@ -1,6 +1,7 @@ package com.processout.sdk.ui.card.tokenization import androidx.annotation.DrawableRes +import androidx.compose.animation.* import androidx.compose.foundation.layout.* import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll @@ -23,6 +24,8 @@ import androidx.compose.ui.unit.dp import androidx.lifecycle.Lifecycle import com.processout.sdk.ui.card.tokenization.CardTokenizationEvent.* import com.processout.sdk.ui.card.tokenization.CardTokenizationViewModelState.Item +import com.processout.sdk.ui.card.tokenization.CardTokenizationViewModelState.Section +import com.processout.sdk.ui.card.tokenization.CardTokenizationViewModelState.SectionId.CARD_INFORMATION import com.processout.sdk.ui.card.tokenization.CardTokenizationViewModelState.SectionId.FUTURE_PAYMENTS import com.processout.sdk.ui.card.tokenization.CardTokenizationViewModelState.SectionId.PREFERRED_SCHEME import com.processout.sdk.ui.core.component.* @@ -128,46 +131,79 @@ private fun Sections( ) } val lifecycleEvent = rememberLifecycleEvent() - state.sections.elements.forEachIndexed { index, section -> - val verticalPadding = when (section.id) { - PREFERRED_SCHEME -> if (section.title == null) spacing.small else spacing.extraLarge - FUTURE_PAYMENTS -> spacing.small - else -> when (index) { - 0 -> 0.dp - else -> spacing.extraLarge - } - } - Spacer(Modifier.requiredHeight(verticalPadding)) - Column( - verticalArrangement = Arrangement.spacedBy(spacing.small) - ) { - section.title?.let { - with(style.sectionTitle) { - POText( - text = it, - color = color, - style = textStyle - ) - } - } - section.items.elements.forEach { item -> - Item( - item = item, - onEvent = onEvent, - lifecycleEvent = lifecycleEvent, - focusedFieldId = state.focusedFieldId, - isPrimaryActionEnabled = state.primaryAction.enabled && !state.primaryAction.loading, - style = style, - modifier = Modifier.fillMaxWidth() + state.sections.elements.forEach { section -> + Section( + section = section, + onEvent = onEvent, + lifecycleEvent = lifecycleEvent, + focusedFieldId = state.focusedFieldId, + isPrimaryActionEnabled = state.primaryAction.enabled && !state.primaryAction.loading, + style = style + ) + } +} + +@Composable +private fun Section( + section: Section, + onEvent: (CardTokenizationEvent) -> Unit, + lifecycleEvent: Lifecycle.Event, + focusedFieldId: String?, + isPrimaryActionEnabled: Boolean, + style: CardTokenizationScreen.Style +) { + val paddingTop = when (section.id) { + CARD_INFORMATION -> 0.dp + PREFERRED_SCHEME -> if (section.title == null) spacing.small else spacing.extraLarge + FUTURE_PAYMENTS -> spacing.small + else -> spacing.extraLarge + } + Column( + modifier = Modifier.padding(top = paddingTop), + verticalArrangement = Arrangement.spacedBy(spacing.small) + ) { + section.title?.let { + with(style.sectionTitle) { + POText( + text = it, + color = color, + style = textStyle ) } } - POExpandableText( - text = section.errorMessage, - style = style.errorMessage, - modifier = Modifier - .fillMaxWidth() - .padding(top = spacing.small) + section.items?.elements?.forEach { item -> + Item( + item = item, + onEvent = onEvent, + lifecycleEvent = lifecycleEvent, + focusedFieldId = focusedFieldId, + isPrimaryActionEnabled = isPrimaryActionEnabled, + style = style, + modifier = Modifier.fillMaxWidth() + ) + } + } + POExpandableText( + text = section.errorMessage, + style = style.errorMessage, + modifier = Modifier + .fillMaxWidth() + .padding(top = spacing.small) + ) + var currentSubsection by remember { mutableStateOf(Section(id = String())) } + currentSubsection = section.subsection ?: currentSubsection + AnimatedVisibility( + visible = section.subsection != null, + enter = expandVertically() + fadeIn(), + exit = shrinkVertically() + fadeOut() + ) { + Section( + section = currentSubsection, + onEvent = onEvent, + lifecycleEvent = lifecycleEvent, + focusedFieldId = focusedFieldId, + isPrimaryActionEnabled = isPrimaryActionEnabled, + style = style ) } } diff --git a/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/CardTokenizationViewModel.kt b/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/CardTokenizationViewModel.kt index c84beb5e5..14fc2f79c 100644 --- a/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/CardTokenizationViewModel.kt +++ b/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/CardTokenizationViewModel.kt @@ -140,7 +140,6 @@ internal class CardTokenizationViewModel private constructor( val lastFocusableFieldId = lastFocusableFieldId(state) val sections = listOf( cardInformationSection(state, lastFocusableFieldId), - preferredSchemeSection(state), billingAddressSection(state, lastFocusableFieldId), futurePaymentsSection(state) ) @@ -233,7 +232,8 @@ internal class CardTokenizationViewModel private constructor( return Section( id = SectionId.CARD_INFORMATION, items = POImmutableList(items), - errorMessage = state.errorMessage + errorMessage = state.errorMessage, + subsection = preferredSchemeSection(state) ) } diff --git a/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/CardTokenizationViewModelState.kt b/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/CardTokenizationViewModelState.kt index 6cb8837d3..c066e22e5 100644 --- a/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/CardTokenizationViewModelState.kt +++ b/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/CardTokenizationViewModelState.kt @@ -20,8 +20,9 @@ internal data class CardTokenizationViewModelState( data class Section( val id: String, val title: String? = null, - val items: POImmutableList, - val errorMessage: String? = null + val items: POImmutableList? = null, + val errorMessage: String? = null, + val subsection: Section? = null ) @Immutable diff --git a/ui/src/main/kotlin/com/processout/sdk/ui/checkout/screen/CardTokenization.kt b/ui/src/main/kotlin/com/processout/sdk/ui/checkout/screen/CardTokenization.kt index e65eb377a..515c3da39 100644 --- a/ui/src/main/kotlin/com/processout/sdk/ui/checkout/screen/CardTokenization.kt +++ b/ui/src/main/kotlin/com/processout/sdk/ui/checkout/screen/CardTokenization.kt @@ -79,7 +79,7 @@ internal fun CardTokenization( ) } } - section.items.elements.forEach { item -> + section.items?.elements?.forEach { item -> Item( id = id, item = item, @@ -124,6 +124,9 @@ private fun Item( style = style.field, modifier = modifier ) + is Item.RadioField -> { + // TODO + } is Item.DropdownField -> DropdownField( id = id, state = item.state, From 65b66dbc5b671b17e946064edb0aabf780ec387d Mon Sep 17 00:00:00 2001 From: Vitalii Vanziak Date: Thu, 24 Apr 2025 17:30:10 +0300 Subject: [PATCH 16/21] PreferredSchemeConfiguration on DC --- .../sdk/ui/checkout/DynamicCheckoutActivity.kt | 10 ++++++++-- .../ui/checkout/PODynamicCheckoutConfiguration.kt | 15 +++++++++++++++ 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/ui/src/main/kotlin/com/processout/sdk/ui/checkout/DynamicCheckoutActivity.kt b/ui/src/main/kotlin/com/processout/sdk/ui/checkout/DynamicCheckoutActivity.kt index df265443d..0d9dddf81 100644 --- a/ui/src/main/kotlin/com/processout/sdk/ui/checkout/DynamicCheckoutActivity.kt +++ b/ui/src/main/kotlin/com/processout/sdk/ui/checkout/DynamicCheckoutActivity.kt @@ -38,8 +38,7 @@ import com.processout.sdk.ui.base.BaseTransparentPortraitActivity import com.processout.sdk.ui.card.scanner.POCardScannerLauncher import com.processout.sdk.ui.card.tokenization.CardTokenizationViewModel import com.processout.sdk.ui.card.tokenization.POCardTokenizationConfiguration -import com.processout.sdk.ui.card.tokenization.POCardTokenizationConfiguration.BillingAddressConfiguration -import com.processout.sdk.ui.card.tokenization.POCardTokenizationConfiguration.CardScannerConfiguration +import com.processout.sdk.ui.card.tokenization.POCardTokenizationConfiguration.* import com.processout.sdk.ui.checkout.DynamicCheckoutActivityContract.Companion.EXTRA_CONFIGURATION import com.processout.sdk.ui.checkout.DynamicCheckoutActivityContract.Companion.EXTRA_RESULT import com.processout.sdk.ui.checkout.DynamicCheckoutCompletion.Failure @@ -47,6 +46,8 @@ import com.processout.sdk.ui.checkout.DynamicCheckoutCompletion.Success import com.processout.sdk.ui.checkout.DynamicCheckoutEvent.* import com.processout.sdk.ui.checkout.DynamicCheckoutSideEffect.* import com.processout.sdk.ui.checkout.PODynamicCheckoutConfiguration.* +import com.processout.sdk.ui.checkout.PODynamicCheckoutConfiguration.Button +import com.processout.sdk.ui.checkout.PODynamicCheckoutConfiguration.CancelButton import com.processout.sdk.ui.checkout.screen.DynamicCheckoutScreen import com.processout.sdk.ui.core.theme.ProcessOutTheme import com.processout.sdk.ui.googlepay.POGooglePayCardTokenizationLauncher @@ -93,6 +94,7 @@ internal class DynamicCheckoutActivity : BaseTransparentPortraitActivity() { } private fun cardTokenizationConfiguration(): POCardTokenizationConfiguration { + val preferredScheme = configuration.card.preferredScheme val billingAddress = configuration.card.billingAddress return POCardTokenizationConfiguration( cardScanner = configuration.card.cardScanner?.let { @@ -104,6 +106,10 @@ internal class DynamicCheckoutActivity : BaseTransparentPortraitActivity() { configuration = it.configuration ) }, + preferredScheme = PreferredSchemeConfiguration( + title = preferredScheme.title, + displayInline = preferredScheme.displayInline + ), billingAddress = BillingAddressConfiguration( defaultAddress = billingAddress.defaultAddress, attachDefaultsToPaymentMethod = billingAddress.attachDefaultsToPaymentMethod diff --git a/ui/src/main/kotlin/com/processout/sdk/ui/checkout/PODynamicCheckoutConfiguration.kt b/ui/src/main/kotlin/com/processout/sdk/ui/checkout/PODynamicCheckoutConfiguration.kt index 388ac2a70..2b066bdb1 100644 --- a/ui/src/main/kotlin/com/processout/sdk/ui/checkout/PODynamicCheckoutConfiguration.kt +++ b/ui/src/main/kotlin/com/processout/sdk/ui/checkout/PODynamicCheckoutConfiguration.kt @@ -10,6 +10,7 @@ import com.google.android.gms.wallet.WalletConstants import com.processout.sdk.api.model.request.POContact import com.processout.sdk.api.model.request.POInvoiceRequest import com.processout.sdk.ui.card.scanner.POCardScannerConfiguration +import com.processout.sdk.ui.card.tokenization.POCardTokenizationConfiguration.PreferredSchemeConfiguration import com.processout.sdk.ui.checkout.PODynamicCheckoutConfiguration.GooglePayConfiguration.Environment import com.processout.sdk.ui.core.annotation.ProcessOutInternalApi import com.processout.sdk.ui.core.shared.image.PODrawableImage @@ -68,12 +69,14 @@ data class PODynamicCheckoutConfiguration( * Specifies card payment configuration. * * @param[cardScanner] Card scanner configuration. Use _null_ to hide. + * @param[preferredScheme] Preferred scheme selection configuration. * @param[billingAddress] Specifies billing address configuration. * @param[metadata] Metadata related to the card. */ @Parcelize data class CardConfiguration( val cardScanner: CardScannerConfiguration? = CardScannerConfiguration(), + val preferredScheme: PreferredSchemeConfiguration = PreferredSchemeConfiguration(), val billingAddress: BillingAddressConfiguration = BillingAddressConfiguration(), val metadata: Map? = null ) : Parcelable { @@ -90,6 +93,18 @@ data class PODynamicCheckoutConfiguration( val configuration: POCardScannerConfiguration = POCardScannerConfiguration() ) : Parcelable + /** + * Preferred scheme selection configuration. + * + * @param[title] Preferred scheme section title. Set _null_ to use a default value or empty string to remove the title. + * @param[displayInline] Indicates whether selection field should be displayed inline. Default value is _true_. + */ + @Parcelize + data class PreferredSchemeConfiguration( + val title: String? = null, + val displayInline: Boolean = true + ) : Parcelable + /** * Specifies billing address configuration. * From 7f3dede99b44856d038ec520ef6732110cfc4634 Mon Sep 17 00:00:00 2001 From: Vitalii Vanziak Date: Thu, 24 Apr 2025 17:43:47 +0300 Subject: [PATCH 17/21] Apply schemeSelectionAllowed on DC --- .../com/processout/sdk/ui/checkout/DynamicCheckoutInteractor.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/ui/src/main/kotlin/com/processout/sdk/ui/checkout/DynamicCheckoutInteractor.kt b/ui/src/main/kotlin/com/processout/sdk/ui/checkout/DynamicCheckoutInteractor.kt index 5a704e94b..158bd42c2 100644 --- a/ui/src/main/kotlin/com/processout/sdk/ui/checkout/DynamicCheckoutInteractor.kt +++ b/ui/src/main/kotlin/com/processout/sdk/ui/checkout/DynamicCheckoutInteractor.kt @@ -487,6 +487,7 @@ internal class DynamicCheckoutInteractor( copy( cvcRequired = configuration.cvcRequired, cardholderNameRequired = configuration.cardholderNameRequired, + preferredScheme = if (configuration.schemeSelectionAllowed) preferredScheme else null, billingAddress = billingAddress.copy( mode = configuration.billingAddress.collectionMode.map(), countryCodes = configuration.billingAddress.restrictToCountryCodes From f7d0dd20776fd601dfa00d84c61be60484706f11 Mon Sep 17 00:00:00 2001 From: Vitalii Vanziak Date: Thu, 24 Apr 2025 17:55:14 +0300 Subject: [PATCH 18/21] Add RadioField on DC card --- .../ui/checkout/screen/CardTokenization.kt | 36 +++++++++++++++++-- 1 file changed, 33 insertions(+), 3 deletions(-) diff --git a/ui/src/main/kotlin/com/processout/sdk/ui/checkout/screen/CardTokenization.kt b/ui/src/main/kotlin/com/processout/sdk/ui/checkout/screen/CardTokenization.kt index 515c3da39..a4f20ca21 100644 --- a/ui/src/main/kotlin/com/processout/sdk/ui/checkout/screen/CardTokenization.kt +++ b/ui/src/main/kotlin/com/processout/sdk/ui/checkout/screen/CardTokenization.kt @@ -22,6 +22,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.checkbox.POCheckbox 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.POTextField import com.processout.sdk.ui.core.state.POImmutableList import com.processout.sdk.ui.core.theme.ProcessOutTheme.dimensions @@ -124,9 +125,13 @@ private fun Item( style = style.field, modifier = modifier ) - is Item.RadioField -> { - // TODO - } + is Item.RadioField -> RadioField( + id = id, + state = item.state, + onEvent = onEvent, + style = style.radioGroup, + modifier = modifier + ) is Item.DropdownField -> DropdownField( id = id, state = item.state, @@ -222,6 +227,31 @@ private fun TextField( } } +@Composable +private fun RadioField( + id: String, + state: FieldState, + onEvent: (DynamicCheckoutEvent) -> Unit, + style: PORadioGroup.Style, + modifier: Modifier = Modifier +) { + PORadioGroup( + value = state.value.text, + onValueChange = { + onEvent( + FieldValueChanged( + paymentMethodId = id, + fieldId = state.id, + value = TextFieldValue(text = it) + ) + ) + }, + availableValues = state.availableValues ?: POImmutableList(emptyList()), + modifier = modifier, + style = style + ) +} + @Composable private fun DropdownField( id: String, From d9645564aa7b45fb14ac0a56b48b50751f2865e1 Mon Sep 17 00:00:00 2001 From: Vitalii Vanziak Date: Thu, 24 Apr 2025 18:20:59 +0300 Subject: [PATCH 19/21] Preferred scheme UI on DC card --- .../ui/checkout/screen/CardTokenization.kt | 117 ++++++++++++------ 1 file changed, 79 insertions(+), 38 deletions(-) diff --git a/ui/src/main/kotlin/com/processout/sdk/ui/checkout/screen/CardTokenization.kt b/ui/src/main/kotlin/com/processout/sdk/ui/checkout/screen/CardTokenization.kt index a4f20ca21..31a1c49de 100644 --- a/ui/src/main/kotlin/com/processout/sdk/ui/checkout/screen/CardTokenization.kt +++ b/ui/src/main/kotlin/com/processout/sdk/ui/checkout/screen/CardTokenization.kt @@ -1,9 +1,9 @@ package com.processout.sdk.ui.checkout.screen import androidx.annotation.DrawableRes +import androidx.compose.animation.* import androidx.compose.foundation.layout.* -import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember +import androidx.compose.runtime.* import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester @@ -15,7 +15,10 @@ import androidx.compose.ui.unit.dp import androidx.lifecycle.Lifecycle import com.processout.sdk.ui.card.tokenization.CardTokenizationViewModelState import com.processout.sdk.ui.card.tokenization.CardTokenizationViewModelState.Item +import com.processout.sdk.ui.card.tokenization.CardTokenizationViewModelState.Section +import com.processout.sdk.ui.card.tokenization.CardTokenizationViewModelState.SectionId.CARD_INFORMATION import com.processout.sdk.ui.card.tokenization.CardTokenizationViewModelState.SectionId.FUTURE_PAYMENTS +import com.processout.sdk.ui.card.tokenization.CardTokenizationViewModelState.SectionId.PREFERRED_SCHEME import com.processout.sdk.ui.checkout.DynamicCheckoutEvent import com.processout.sdk.ui.checkout.DynamicCheckoutEvent.* import com.processout.sdk.ui.core.component.* @@ -60,45 +63,83 @@ internal fun CardTokenization( ) } val lifecycleEvent = rememberLifecycleEvent() - state.sections.elements.forEachIndexed { index, section -> - val padding = if (section.id == FUTURE_PAYMENTS) { - spacing.small - } else when (index) { - 0 -> 0.dp - else -> spacing.extraLarge - } - Spacer(Modifier.requiredHeight(padding)) - Column( - verticalArrangement = Arrangement.spacedBy(spacing.small) - ) { - section.title?.let { - with(style.label) { - POText( - text = it, - color = color, - style = textStyle - ) - } - } - section.items?.elements?.forEach { item -> - Item( - id = id, - item = item, - onEvent = onEvent, - lifecycleEvent = lifecycleEvent, - focusedFieldId = state.focusedFieldId, - isPrimaryActionEnabled = state.primaryAction.enabled && !state.primaryAction.loading, - style = style, - modifier = Modifier.fillMaxWidth() + state.sections.elements.forEach { section -> + Section( + id = id, + section = section, + onEvent = onEvent, + lifecycleEvent = lifecycleEvent, + focusedFieldId = state.focusedFieldId, + isPrimaryActionEnabled = state.primaryAction.enabled && !state.primaryAction.loading, + style = style + ) + } +} + +@Composable +private fun Section( + id: String, + section: Section, + onEvent: (DynamicCheckoutEvent) -> Unit, + lifecycleEvent: Lifecycle.Event, + focusedFieldId: String?, + isPrimaryActionEnabled: Boolean, + style: DynamicCheckoutScreen.Style +) { + val paddingTop = when (section.id) { + CARD_INFORMATION -> 0.dp + PREFERRED_SCHEME -> if (section.title == null) spacing.small else spacing.extraLarge + FUTURE_PAYMENTS -> spacing.small + else -> spacing.extraLarge + } + Column( + modifier = Modifier.padding(top = paddingTop), + verticalArrangement = Arrangement.spacedBy(spacing.small) + ) { + section.title?.let { + with(style.label) { + POText( + text = it, + color = color, + style = textStyle ) } } - POExpandableText( - text = section.errorMessage, - style = style.errorText, - modifier = Modifier - .fillMaxWidth() - .padding(top = spacing.small) + section.items?.elements?.forEach { item -> + Item( + id = id, + item = item, + onEvent = onEvent, + lifecycleEvent = lifecycleEvent, + focusedFieldId = focusedFieldId, + isPrimaryActionEnabled = isPrimaryActionEnabled, + style = style, + modifier = Modifier.fillMaxWidth() + ) + } + } + POExpandableText( + text = section.errorMessage, + style = style.errorText, + modifier = Modifier + .fillMaxWidth() + .padding(top = spacing.small) + ) + var currentSubsection by remember { mutableStateOf(Section(id = String())) } + currentSubsection = section.subsection ?: currentSubsection + AnimatedVisibility( + visible = section.subsection != null, + enter = expandVertically() + fadeIn(), + exit = shrinkVertically() + fadeOut() + ) { + Section( + id = id, + section = currentSubsection, + onEvent = onEvent, + lifecycleEvent = lifecycleEvent, + focusedFieldId = focusedFieldId, + isPrimaryActionEnabled = isPrimaryActionEnabled, + style = style ) } } From dcb66ea73ca72c012dc2fcb30f214317ad1774ee Mon Sep 17 00:00:00 2001 From: Vitalii Vanziak Date: Thu, 24 Apr 2025 19:26:47 +0300 Subject: [PATCH 20/21] Update POCardTokenizationConfiguration --- .../ui/card/tokenization/POCardTokenizationConfiguration.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/POCardTokenizationConfiguration.kt b/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/POCardTokenizationConfiguration.kt index 41bc0e7d8..1f29ff6f9 100644 --- a/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/POCardTokenizationConfiguration.kt +++ b/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/POCardTokenizationConfiguration.kt @@ -21,8 +21,8 @@ import kotlinx.parcelize.Parcelize * @param[cvcRequired] Specifies whether the CVC field should be displayed. Default value is _true_. * @param[cardholderNameRequired] Specifies whether the cardholder name field should be displayed. Default value is _true_. * @param[cardScanner] Card scanner configuration. Use _null_ to hide, this is a default behaviour. - * @param[preferredScheme] Preferred scheme selection configuration. Shows scheme selection if co-scheme is available. - * Use _null_ to hide, this is a default behaviour. + * @param[preferredScheme] Preferred scheme selection configuration. + * Shows scheme selection if co-scheme is available. Use _null_ to hide. * @param[billingAddress] Allows to customize the collection of billing address. * @param[savingAllowed] Displays checkbox that allows to save the card details for future payments. * @param[submitButton] Submit button configuration. @@ -37,7 +37,7 @@ data class POCardTokenizationConfiguration( val cvcRequired: Boolean = true, val cardholderNameRequired: Boolean = true, val cardScanner: CardScannerConfiguration? = null, - val preferredScheme: PreferredSchemeConfiguration? = null, + val preferredScheme: PreferredSchemeConfiguration? = PreferredSchemeConfiguration(), val billingAddress: BillingAddressConfiguration = BillingAddressConfiguration(), val savingAllowed: Boolean = false, val submitButton: Button = Button(), From 35361371028c0f884139ad66126f085aa34cf0b7 Mon Sep 17 00:00:00 2001 From: Vitalii Vanziak Date: Thu, 24 Apr 2025 20:08:39 +0300 Subject: [PATCH 21/21] KDoc --- .../sdk/ui/card/tokenization/POCardTokenizationDelegate.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/POCardTokenizationDelegate.kt b/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/POCardTokenizationDelegate.kt index 8697a8e18..535a553aa 100644 --- a/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/POCardTokenizationDelegate.kt +++ b/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/POCardTokenizationDelegate.kt @@ -20,7 +20,8 @@ interface POCardTokenizationDelegate { * for example to authorize an invoice or assign a customer token. * Return the result from respective ProcessOut API if it was used. * In case of a custom implementation you can pass - * _ProcessOutResult.Success(Unit)_ or appropriate _ProcessOutResult.Failure()_. + * _ProcessOutResult.Success(Unit)_ or appropriate _ProcessOutResult.Failure()_ + * with _localizedMessage_ that will be shown directly to the user. * Failure will be propagated to [shouldContinue] function. * * @param[card] Tokenized card.