From 40a634687299d154ba3190b518ff65005524cc68 Mon Sep 17 00:00:00 2001 From: Vitalii Vanziak Date: Mon, 24 Mar 2025 19:05:53 +0200 Subject: [PATCH 01/32] CardScannerConfiguration in POCardTokenizationConfiguration --- .../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 5209edd9e..94b3a15bf 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 @@ -3,6 +3,7 @@ package com.processout.sdk.ui.card.tokenization import android.os.Parcelable import androidx.annotation.ColorRes import com.processout.sdk.api.model.request.POContact +import com.processout.sdk.ui.card.scanner.POCardScannerConfiguration import com.processout.sdk.ui.card.tokenization.POCardTokenizationConfiguration.BillingAddressConfiguration.CollectionMode import com.processout.sdk.ui.core.shared.image.PODrawableImage import com.processout.sdk.ui.core.style.* @@ -19,6 +20,7 @@ import kotlinx.parcelize.Parcelize * @param[title] Custom title. * @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[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. @@ -32,6 +34,7 @@ data class POCardTokenizationConfiguration( val title: String? = null, val cvcRequired: Boolean = true, val cardholderNameRequired: Boolean = true, + val cardScanner: CardScannerConfiguration? = null, val billingAddress: BillingAddressConfiguration = BillingAddressConfiguration(), val savingAllowed: Boolean = false, val submitButton: Button = Button(), @@ -88,6 +91,18 @@ data class POCardTokenizationConfiguration( style = style ) + /** + * Defines card scanner configuration. + * + * @param[scanButton] Scan button configuration. + * @param[configuration] Card scanner configuration. + */ + @Parcelize + data class CardScannerConfiguration( + val scanButton: Button = Button(), + val configuration: POCardScannerConfiguration = POCardScannerConfiguration() + ) : Parcelable + /** * Defines billing address configuration. * From 8d007246132782df562ec691018aab3ed8dc3c65 Mon Sep 17 00:00:00 2001 From: Vitalii Vanziak Date: Mon, 24 Mar 2025 19:20:00 +0200 Subject: [PATCH 02/32] po_icon_camera --- ui/src/main/res/drawable/po_icon_camera.xml | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 ui/src/main/res/drawable/po_icon_camera.xml diff --git a/ui/src/main/res/drawable/po_icon_camera.xml b/ui/src/main/res/drawable/po_icon_camera.xml new file mode 100644 index 000000000..bbd415a37 --- /dev/null +++ b/ui/src/main/res/drawable/po_icon_camera.xml @@ -0,0 +1,20 @@ + + + + From 2c82a827c2305af85af319cf36a84f07899436b0 Mon Sep 17 00:00:00 2001 From: Vitalii Vanziak Date: Tue, 25 Mar 2025 12:50:44 +0200 Subject: [PATCH 03/32] Localizations --- sdk/src/main/res/values-ar/strings.xml | 1 + sdk/src/main/res/values-fr/strings.xml | 1 + sdk/src/main/res/values-pl/strings.xml | 1 + sdk/src/main/res/values-pt/strings.xml | 1 + sdk/src/main/res/values/strings.xml | 1 + 5 files changed, 5 insertions(+) diff --git a/sdk/src/main/res/values-ar/strings.xml b/sdk/src/main/res/values-ar/strings.xml index cf66dbbe6..e75130d11 100644 --- a/sdk/src/main/res/values-ar/strings.xml +++ b/sdk/src/main/res/values-ar/strings.xml @@ -62,6 +62,7 @@ إضافة بطاقة جديدة إرسال إلغاء + امسح البطاقة النظام المفضل للبطاقة اسم حامل البطاقة كود التحقق CVV diff --git a/sdk/src/main/res/values-fr/strings.xml b/sdk/src/main/res/values-fr/strings.xml index d4aafb842..31d5bb9c1 100644 --- a/sdk/src/main/res/values-fr/strings.xml +++ b/sdk/src/main/res/values-fr/strings.xml @@ -60,6 +60,7 @@ Ajouter une nouvelle carte Envoyer Annuler + Scanner la carte Réseau de Carte Préféré Nom du porteur de carte CVV diff --git a/sdk/src/main/res/values-pl/strings.xml b/sdk/src/main/res/values-pl/strings.xml index 4989872df..5134c292e 100644 --- a/sdk/src/main/res/values-pl/strings.xml +++ b/sdk/src/main/res/values-pl/strings.xml @@ -61,6 +61,7 @@ Dodaj nową kartę Zatwierdź Anuluj + Skanowanie karty Preferowany Schemat Imię i nazwisko na karcie CVC diff --git a/sdk/src/main/res/values-pt/strings.xml b/sdk/src/main/res/values-pt/strings.xml index e50fe615b..1acf9e383 100644 --- a/sdk/src/main/res/values-pt/strings.xml +++ b/sdk/src/main/res/values-pt/strings.xml @@ -60,6 +60,7 @@ Adicionar novo cartão Enviar Cancelar + Digitalizar cartão Rede de pagamento preferencial Nome no cartão CVC diff --git a/sdk/src/main/res/values/strings.xml b/sdk/src/main/res/values/strings.xml index 3b5b540d8..e75db6707 100644 --- a/sdk/src/main/res/values/strings.xml +++ b/sdk/src/main/res/values/strings.xml @@ -59,6 +59,7 @@ Add New Card Submit Cancel + Scan card Preferred Scheme Cardholder Name CVC From 7ef633aa74013a2126473125c7813033c4605cf9 Mon Sep 17 00:00:00 2001 From: Vitalii Vanziak Date: Tue, 25 Mar 2025 13:22:15 +0200 Subject: [PATCH 04/32] Add scan button to Card Tokenization --- .../screen/card/payment/CardPaymentFragment.kt | 6 +++--- .../tokenization/CardTokenizationInteractor.kt | 4 +++- .../CardTokenizationInteractorState.kt | 2 ++ .../card/tokenization/CardTokenizationScreen.kt | 16 ++++++++++++++++ .../tokenization/CardTokenizationViewModel.kt | 13 +++++++++++++ .../CardTokenizationViewModelState.kt | 1 + .../POCardTokenizationConfiguration.kt | 1 + 7 files changed, 39 insertions(+), 4 deletions(-) diff --git a/example/src/main/kotlin/com/processout/example/ui/screen/card/payment/CardPaymentFragment.kt b/example/src/main/kotlin/com/processout/example/ui/screen/card/payment/CardPaymentFragment.kt index efad028d3..79a8bb146 100644 --- a/example/src/main/kotlin/com/processout/example/ui/screen/card/payment/CardPaymentFragment.kt +++ b/example/src/main/kotlin/com/processout/example/ui/screen/card/payment/CardPaymentFragment.kt @@ -27,7 +27,7 @@ import com.processout.sdk.core.ProcessOutActivityResult import com.processout.sdk.core.onFailure import com.processout.sdk.core.onSuccess import com.processout.sdk.ui.card.tokenization.POCardTokenizationConfiguration -import com.processout.sdk.ui.card.tokenization.POCardTokenizationConfiguration.Button +import com.processout.sdk.ui.card.tokenization.POCardTokenizationConfiguration.CardScannerConfiguration import com.processout.sdk.ui.card.tokenization.POCardTokenizationLauncher import com.processout.sdk.ui.shared.view.dialog.POAlertDialog import com.processout.sdk.ui.threeds.PO3DSRedirectCustomTabLauncher @@ -99,8 +99,8 @@ class CardPaymentFragment : BaseFragment( viewModel.onTokenizing() launcher.launch( POCardTokenizationConfiguration( - savingAllowed = true, - submitButton = Button() + cardScanner = CardScannerConfiguration(), + savingAllowed = true ) ) } 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 a5b926082..5823f6392 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 @@ -117,7 +117,8 @@ internal class CardTokenizationInteractor( ), focusedFieldId = CardFieldId.NUMBER, primaryActionId = ActionId.SUBMIT, - secondaryActionId = ActionId.CANCEL + secondaryActionId = ActionId.CANCEL, + scanActionId = ActionId.SCAN ) private fun cardFields(): List = mutableListOf( @@ -176,6 +177,7 @@ internal class CardTokenizationInteractor( is Action -> when (event.id) { ActionId.SUBMIT -> submit() ActionId.CANCEL -> cancel() + ActionId.SCAN -> {} } is Dismiss -> POLogger.info("Dismissed: %s", event.failure) } 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 80509bc15..1affacbdb 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 @@ -15,6 +15,7 @@ internal data class CardTokenizationInteractorState( val focusedFieldId: String?, val primaryActionId: String, val secondaryActionId: String, + val scanActionId: String, val submitAllowed: Boolean = true, val submitting: Boolean = false, val errorMessage: String? = null, @@ -54,5 +55,6 @@ internal data class CardTokenizationInteractorState( object ActionId { const val SUBMIT = "submit" const val CANCEL = "cancel" + const val SCAN = "scan" } } 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 9c9737fec..8bcc4c338 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 @@ -94,6 +94,18 @@ internal fun CardTokenizationScreen( onContentHeightChanged(contentHeight) } ) { + state.scanAction?.let { action -> + POButton( + state = action, + onClick = { onEvent(Action(id = it)) }, + modifier = Modifier + .fillMaxWidth() + .requiredHeightIn(min = dimensions.buttonIconSizeSmall) + .padding(bottom = spacing.small), + style = style.scanButton, + iconSize = dimensions.iconSizeSmall + ) + } Sections( state = state, onEvent = onEvent, @@ -359,6 +371,7 @@ internal object CardTokenizationScreen { val checkbox: POCheckbox.Style, val dropdownMenu: PODropdownField.MenuStyle, val errorMessage: POText.Style, + val scanButton: POButton.Style, val actionsContainer: POActionsContainer.Style, val backgroundColor: Color, val dividerColor: Color, @@ -385,6 +398,9 @@ internal object CardTokenizationScreen { errorMessage = custom?.errorMessage?.let { POText.custom(style = it) } ?: POText.errorLabel, + scanButton = custom?.scanButton?.let { + POButton.custom(style = it) + } ?: POButton.secondary, actionsContainer = custom?.actionsContainer?.let { POActionsContainer.custom(style = it) } ?: POActionsContainer.default, 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 b6341ed9d..e9a9d0a7c 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 @@ -15,6 +15,8 @@ import com.processout.sdk.api.ProcessOut import com.processout.sdk.api.dispatcher.card.tokenization.PODefaultCardTokenizationEventDispatcher import com.processout.sdk.ui.card.tokenization.CardTokenizationInteractorState.* import com.processout.sdk.ui.card.tokenization.CardTokenizationViewModelState.* +import com.processout.sdk.ui.core.shared.image.PODrawableImage +import com.processout.sdk.ui.core.shared.image.POImageRenderingMode import com.processout.sdk.ui.core.state.POActionState import com.processout.sdk.ui.core.state.POActionState.Confirmation import com.processout.sdk.ui.core.state.POImmutableList @@ -117,6 +119,17 @@ internal class CardTokenizationViewModel private constructor( } ) }, + scanAction = cardScanner?.scanButton?.let { + POActionState( + id = state.scanActionId, + text = it.text ?: app.getString(R.string.po_card_tokenization_button_scan), + primary = false, + icon = it.icon ?: PODrawableImage( + resId = com.processout.sdk.ui.R.drawable.po_icon_camera, + renderingMode = POImageRenderingMode.TEMPLATE + ) + ) + }, draggable = bottomSheet.cancellation.dragDown || bottomSheet.expandable ) } 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 7cb8f5f0c..6e79bea24 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 @@ -12,6 +12,7 @@ internal data class CardTokenizationViewModelState( val focusedFieldId: String?, val primaryAction: POActionState, val secondaryAction: POActionState?, + val scanAction: POActionState?, val draggable: Boolean ) { 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 94b3a15bf..2ee665996 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 @@ -185,6 +185,7 @@ data class POCardTokenizationConfiguration( val checkbox: POCheckboxStyle? = null, val dropdownMenu: PODropdownMenuStyle? = null, val errorMessage: POTextStyle? = null, + val scanButton: POButtonStyle? = null, val actionsContainer: POActionsContainerStyle? = null, @ColorRes val backgroundColorResId: Int? = null, From a3502ffe2a38f9d5f626067e9f53b3d26bcd3fd8 Mon Sep 17 00:00:00 2001 From: Vitalii Vanziak Date: Tue, 25 Mar 2025 17:33:32 +0200 Subject: [PATCH 05/32] clear focus --- .../ui/card/tokenization/CardTokenizationScreen.kt | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 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 8bcc4c338..e5f362d37 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 @@ -94,10 +94,14 @@ internal fun CardTokenizationScreen( onContentHeightChanged(contentHeight) } ) { + var clearFocus by remember { mutableStateOf(false) } state.scanAction?.let { action -> POButton( state = action, - onClick = { onEvent(Action(id = it)) }, + onClick = { + clearFocus = true + onEvent(Action(id = it)) + }, modifier = Modifier .fillMaxWidth() .requiredHeightIn(min = dimensions.buttonIconSizeSmall) @@ -111,6 +115,10 @@ internal fun CardTokenizationScreen( onEvent = onEvent, style = style ) + if (clearFocus || state.focusedFieldId == null) { + LocalFocusManager.current.clearFocus(force = true) + clearFocus = false + } } } } @@ -122,9 +130,6 @@ private fun Sections( onEvent: (CardTokenizationEvent) -> Unit, style: CardTokenizationScreen.Style ) { - if (state.focusedFieldId == null) { - LocalFocusManager.current.clearFocus(force = true) - } val lifecycleEvent = rememberLifecycleEvent() state.sections.elements.forEachIndexed { index, section -> val padding = if (section.id == FUTURE_PAYMENTS) { From 9b1690bc1bc5bbc856e95a3e6cb8825e2fcfc75f Mon Sep 17 00:00:00 2001 From: Vitalii Vanziak Date: Tue, 25 Mar 2025 18:22:54 +0200 Subject: [PATCH 06/32] Launch card scanner --- .../CardTokenizationBottomSheet.kt | 38 +++++++++++++++---- .../tokenization/CardTokenizationEvent.kt | 6 +++ .../CardTokenizationInteractor.kt | 13 ++++++- .../tokenization/CardTokenizationViewModel.kt | 2 + 4 files changed, 51 insertions(+), 8 deletions(-) diff --git a/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/CardTokenizationBottomSheet.kt b/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/CardTokenizationBottomSheet.kt index 70314755e..220a75ee3 100644 --- a/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/CardTokenizationBottomSheet.kt +++ b/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/CardTokenizationBottomSheet.kt @@ -15,19 +15,24 @@ import com.processout.sdk.api.dispatcher.PODefaultEventDispatchers import com.processout.sdk.api.model.response.POCard import com.processout.sdk.core.ProcessOutActivityResult import com.processout.sdk.core.ProcessOutResult +import com.processout.sdk.core.onSuccess import com.processout.sdk.core.toActivityResult import com.processout.sdk.ui.base.BaseBottomSheetDialogFragment +import com.processout.sdk.ui.card.scanner.POCardScannerLauncher import com.processout.sdk.ui.card.tokenization.CardTokenizationActivityContract.Companion.EXTRA_CONFIGURATION import com.processout.sdk.ui.card.tokenization.CardTokenizationActivityContract.Companion.EXTRA_RESULT import com.processout.sdk.ui.card.tokenization.CardTokenizationCompletion.Failure import com.processout.sdk.ui.card.tokenization.CardTokenizationCompletion.Success +import com.processout.sdk.ui.card.tokenization.CardTokenizationEvent.CardScannerResult import com.processout.sdk.ui.card.tokenization.CardTokenizationEvent.Dismiss +import com.processout.sdk.ui.card.tokenization.CardTokenizationSideEffect.CardScanner import com.processout.sdk.ui.card.tokenization.POCardTokenizationConfiguration.Button import com.processout.sdk.ui.core.theme.ProcessOutTheme import com.processout.sdk.ui.shared.component.displayCutoutHeight import com.processout.sdk.ui.shared.component.screenModeAsState import com.processout.sdk.ui.shared.configuration.POBottomSheetConfiguration.Height.Fixed import com.processout.sdk.ui.shared.configuration.POBottomSheetConfiguration.Height.WrapContent +import com.processout.sdk.ui.shared.extension.collectImmediately import kotlin.math.roundToInt internal class CardTokenizationBottomSheet : BaseBottomSheetDialogFragment() { @@ -39,21 +44,32 @@ internal class CardTokenizationBottomSheet : BaseBottomSheetDialogFragment + result.onSuccess { card -> + viewModel.onEvent(CardScannerResult(card)) + } + } + ) viewModel.start() } @@ -68,6 +84,8 @@ internal class CardTokenizationBottomSheet : BaseBottomSheetDialogFragment contentHeight } }, - style = CardTokenizationScreen.style(custom = configuration?.style) + style = CardTokenizationScreen.style(custom = configuration.style) ) } } @@ -90,9 +108,15 @@ internal class CardTokenizationBottomSheet : BaseBottomSheetDialogFragment configuration.cardScanner?.configuration?.let { + cardScannerLauncher.launch(configuration = it) + } } } diff --git a/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/CardTokenizationEvent.kt b/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/CardTokenizationEvent.kt index 46b8c2ff3..29695aaca 100644 --- a/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/CardTokenizationEvent.kt +++ b/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/CardTokenizationEvent.kt @@ -3,14 +3,20 @@ package com.processout.sdk.ui.card.tokenization import androidx.compose.ui.text.input.TextFieldValue import com.processout.sdk.api.model.response.POCard import com.processout.sdk.core.ProcessOutResult +import com.processout.sdk.ui.card.scanner.recognition.POScannedCard internal sealed interface CardTokenizationEvent { data class FieldValueChanged(val id: String, val value: TextFieldValue) : CardTokenizationEvent data class FieldFocusChanged(val id: String, val isFocused: Boolean) : CardTokenizationEvent data class Action(val id: String) : CardTokenizationEvent + data class CardScannerResult(val card: POScannedCard) : CardTokenizationEvent data class Dismiss(val failure: ProcessOutResult.Failure) : CardTokenizationEvent } +internal sealed interface CardTokenizationSideEffect { + data object CardScanner : CardTokenizationSideEffect +} + internal sealed interface CardTokenizationCompletion { data object Awaiting : CardTokenizationCompletion data class Success(val card: POCard) : CardTokenizationCompletion 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 5823f6392..98ffcdf79 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 @@ -23,6 +23,7 @@ import com.processout.sdk.ui.base.BaseInteractor import com.processout.sdk.ui.card.tokenization.CardTokenizationCompletion.* import com.processout.sdk.ui.card.tokenization.CardTokenizationEvent.* import com.processout.sdk.ui.card.tokenization.CardTokenizationInteractorState.* +import com.processout.sdk.ui.card.tokenization.CardTokenizationSideEffect.CardScanner 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 @@ -33,8 +34,10 @@ import com.processout.sdk.ui.shared.provider.address.AddressSpecification.Addres import com.processout.sdk.ui.shared.provider.address.AddressSpecificationProvider import kotlinx.coroutines.Job import kotlinx.coroutines.cancelChildren +import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import java.util.Locale @@ -64,6 +67,9 @@ internal class CardTokenizationInteractor( private val _state = MutableStateFlow(initState()) val state = _state.asStateFlow() + private val _sideEffects = Channel() + val sideEffects = _sideEffects.receiveAsFlow() + private var latestPreferredSchemeRequest: POCardTokenizationPreferredSchemeRequest? = null private var latestShouldContinueRequest: POCardTokenizationShouldContinueRequest? = null @@ -177,7 +183,12 @@ internal class CardTokenizationInteractor( is Action -> when (event.id) { ActionId.SUBMIT -> submit() ActionId.CANCEL -> cancel() - ActionId.SCAN -> {} + ActionId.SCAN -> interactorScope.launch { + _sideEffects.send(CardScanner) + } + } + is CardScannerResult -> { + // TODO } is Dismiss -> POLogger.info("Dismissed: %s", event.failure) } 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 e9a9d0a7c..c6e64b559 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 @@ -72,6 +72,8 @@ internal class CardTokenizationViewModel private constructor( val state = interactor.state.map(viewModelScope, ::map) + val sideEffects = interactor.sideEffects + init { addCloseable(interactor.interactorScope) } From 9f21ba56e63d826abab518ea9d2a139236a9c532 Mon Sep 17 00:00:00 2001 From: Vitalii Vanziak Date: Tue, 25 Mar 2025 18:43:04 +0200 Subject: [PATCH 07/32] cardScannerAction --- .../sdk/ui/card/tokenization/CardTokenizationInteractor.kt | 4 ++-- .../ui/card/tokenization/CardTokenizationInteractorState.kt | 4 ++-- .../sdk/ui/card/tokenization/CardTokenizationScreen.kt | 2 +- .../sdk/ui/card/tokenization/CardTokenizationViewModel.kt | 4 ++-- .../ui/card/tokenization/CardTokenizationViewModelState.kt | 2 +- 5 files changed, 8 insertions(+), 8 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 98ffcdf79..94ceef0d7 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 @@ -124,7 +124,7 @@ internal class CardTokenizationInteractor( focusedFieldId = CardFieldId.NUMBER, primaryActionId = ActionId.SUBMIT, secondaryActionId = ActionId.CANCEL, - scanActionId = ActionId.SCAN + cardScannerActionId = ActionId.CARD_SCANNER ) private fun cardFields(): List = mutableListOf( @@ -183,7 +183,7 @@ internal class CardTokenizationInteractor( is Action -> when (event.id) { ActionId.SUBMIT -> submit() ActionId.CANCEL -> cancel() - ActionId.SCAN -> interactorScope.launch { + ActionId.CARD_SCANNER -> interactorScope.launch { _sideEffects.send(CardScanner) } } 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 1affacbdb..395d1453e 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 @@ -15,7 +15,7 @@ internal data class CardTokenizationInteractorState( val focusedFieldId: String?, val primaryActionId: String, val secondaryActionId: String, - val scanActionId: String, + val cardScannerActionId: String, val submitAllowed: Boolean = true, val submitting: Boolean = false, val errorMessage: String? = null, @@ -55,6 +55,6 @@ internal data class CardTokenizationInteractorState( object ActionId { const val SUBMIT = "submit" const val CANCEL = "cancel" - const val SCAN = "scan" + const val CARD_SCANNER = "card-scanner" } } 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 e5f362d37..ebb900df5 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 @@ -95,7 +95,7 @@ internal fun CardTokenizationScreen( } ) { var clearFocus by remember { mutableStateOf(false) } - state.scanAction?.let { action -> + state.cardScannerAction?.let { action -> POButton( state = action, onClick = { 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 c6e64b559..e73a7976a 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 @@ -121,9 +121,9 @@ internal class CardTokenizationViewModel private constructor( } ) }, - scanAction = cardScanner?.scanButton?.let { + cardScannerAction = cardScanner?.scanButton?.let { POActionState( - id = state.scanActionId, + id = state.cardScannerActionId, text = it.text ?: app.getString(R.string.po_card_tokenization_button_scan), primary = false, icon = it.icon ?: PODrawableImage( 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 6e79bea24..1d3a02b8d 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 @@ -12,7 +12,7 @@ internal data class CardTokenizationViewModelState( val focusedFieldId: String?, val primaryAction: POActionState, val secondaryAction: POActionState?, - val scanAction: POActionState?, + val cardScannerAction: POActionState?, val draggable: Boolean ) { From 9a061b8361592e222d581ff8f93075ccaacae860 Mon Sep 17 00:00:00 2001 From: Vitalii Vanziak Date: Tue, 25 Mar 2025 19:20:54 +0200 Subject: [PATCH 08/32] Update POScannedCard --- .../CardTokenizationInteractor.kt | 42 +++++++++++++++++-- 1 file changed, 39 insertions(+), 3 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 94ceef0d7..a48e9307e 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 @@ -20,6 +20,7 @@ import com.processout.sdk.core.logger.POLogger import com.processout.sdk.core.onFailure import com.processout.sdk.core.onSuccess import com.processout.sdk.ui.base.BaseInteractor +import com.processout.sdk.ui.card.scanner.recognition.POScannedCard import com.processout.sdk.ui.card.tokenization.CardTokenizationCompletion.* import com.processout.sdk.ui.card.tokenization.CardTokenizationEvent.* import com.processout.sdk.ui.card.tokenization.CardTokenizationInteractorState.* @@ -28,6 +29,8 @@ import com.processout.sdk.ui.card.tokenization.POCardTokenizationConfiguration.B import com.processout.sdk.ui.core.state.POAvailableValue import com.processout.sdk.ui.shared.extension.currentAppLocale import com.processout.sdk.ui.shared.extension.orElse +import com.processout.sdk.ui.shared.filter.CardExpirationInputFilter +import com.processout.sdk.ui.shared.filter.CardNumberInputFilter import com.processout.sdk.ui.shared.provider.CardSchemeProvider import com.processout.sdk.ui.shared.provider.address.AddressSpecification import com.processout.sdk.ui.shared.provider.address.AddressSpecification.AddressUnit @@ -70,6 +73,9 @@ internal class CardTokenizationInteractor( private val _sideEffects = Channel() val sideEffects = _sideEffects.receiveAsFlow() + private val cardNumberInputFilter = CardNumberInputFilter() + private val cardExpirationInputFilter = CardExpirationInputFilter() + private var latestPreferredSchemeRequest: POCardTokenizationPreferredSchemeRequest? = null private var latestShouldContinueRequest: POCardTokenizationShouldContinueRequest? = null @@ -187,9 +193,7 @@ internal class CardTokenizationInteractor( _sideEffects.send(CardScanner) } } - is CardScannerResult -> { - // TODO - } + is CardScannerResult -> update(event.card) is Dismiss -> POLogger.info("Dismissed: %s", event.failure) } } @@ -247,6 +251,38 @@ internal class CardTokenizationInteractor( } } else field + private fun update(card: POScannedCard) { + updateFieldValue( + id = CardFieldId.NUMBER, + value = cardNumberInputFilter.filter( + TextFieldValue( + text = card.number, + selection = TextRange(index = card.number.length) + ) + ) + ) + card.expiration?.let { + updateFieldValue( + id = CardFieldId.EXPIRATION, + value = cardExpirationInputFilter.filter( + TextFieldValue( + text = it.formatted, + selection = TextRange(index = it.formatted.length) + ) + ) + ) + } + card.cardholderName?.let { + updateFieldValue( + id = CardFieldId.CARDHOLDER, + value = TextFieldValue( + text = it, + selection = TextRange(index = it.length) + ) + ) + } + } + private fun updateFieldFocus(id: String, isFocused: Boolean) { if (isFocused) { _state.update { it.copy(focusedFieldId = id) } From 7b3c1907d01ce3cf262b65fcc6b4ef7172615f52 Mon Sep 17 00:00:00 2001 From: Vitalii Vanziak Date: Wed, 26 Mar 2025 15:12:20 +0200 Subject: [PATCH 09/32] Improve card scanner animation --- .../sdk/ui/card/scanner/CardScannerScreen.kt | 29 +++++++------------ 1 file changed, 10 insertions(+), 19 deletions(-) diff --git a/ui/src/main/kotlin/com/processout/sdk/ui/card/scanner/CardScannerScreen.kt b/ui/src/main/kotlin/com/processout/sdk/ui/card/scanner/CardScannerScreen.kt index 74df6a4d6..cadfe4c35 100644 --- a/ui/src/main/kotlin/com/processout/sdk/ui/card/scanner/CardScannerScreen.kt +++ b/ui/src/main/kotlin/com/processout/sdk/ui/card/scanner/CardScannerScreen.kt @@ -17,8 +17,10 @@ import androidx.camera.view.CameraController.IMAGE_ANALYSIS import androidx.camera.view.CameraController.IMAGE_CAPTURE import androidx.camera.view.LifecycleCameraController import androidx.camera.view.PreviewView -import androidx.compose.animation.* +import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.core.tween +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut import androidx.compose.foundation.border import androidx.compose.foundation.layout.* import androidx.compose.foundation.rememberScrollState @@ -253,23 +255,12 @@ private fun ScannedCard( modifier = Modifier.requiredHeightIn(min = 90.dp), verticalArrangement = Arrangement.spacedBy(space = 10.dp) ) { - AnimatedContent( - targetState = card?.number, - transitionSpec = { - fadeIn( - animationSpec = tween(durationMillis = AnimationDurationMillis) - ) togetherWith fadeOut( - animationSpec = tween(durationMillis = AnimationDurationMillis) - ) - } - ) { number -> - POTextAutoSize( - text = number ?: String(), - modifier = Modifier.fillMaxWidth(), - color = style.number.color, - style = style.number.textStyle - ) - } + POTextAutoSize( + text = card?.number ?: String(), + modifier = Modifier.fillMaxWidth(), + color = style.number.color, + style = style.number.textStyle + ) Row { POText( text = card?.cardholderName ?: String(), @@ -437,5 +428,5 @@ internal object CardScannerScreen { /** Height to width ratio of a card by ISO/IEC 7810 standard. */ val CardHeightToWidthRatio = 0.63f - val AnimationDurationMillis = 250 + val AnimationDurationMillis = 300 } From 56ce22de7d002d7e6a54da51c3e8be378512915d Mon Sep 17 00:00:00 2001 From: Vitalii Vanziak Date: Wed, 26 Mar 2025 19:19:00 +0200 Subject: [PATCH 10/32] Delay permission request and camera preview initialization for smooth behaviour --- .../ui/card/scanner/CardScannerInteractor.kt | 33 +++++++--- .../scanner/CardScannerInteractorState.kt | 5 +- .../sdk/ui/card/scanner/CardScannerScreen.kt | 65 ++++++++++--------- .../ui/card/scanner/CardScannerViewModel.kt | 3 +- .../card/scanner/CardScannerViewModelState.kt | 3 +- 5 files changed, 64 insertions(+), 45 deletions(-) diff --git a/ui/src/main/kotlin/com/processout/sdk/ui/card/scanner/CardScannerInteractor.kt b/ui/src/main/kotlin/com/processout/sdk/ui/card/scanner/CardScannerInteractor.kt index 67a7df3bd..29458d15d 100644 --- a/ui/src/main/kotlin/com/processout/sdk/ui/card/scanner/CardScannerInteractor.kt +++ b/ui/src/main/kotlin/com/processout/sdk/ui/card/scanner/CardScannerInteractor.kt @@ -11,6 +11,7 @@ import com.processout.sdk.ui.card.scanner.CardScannerSideEffect.CameraPermission import com.processout.sdk.ui.card.scanner.recognition.CardRecognitionSession import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.receiveAsFlow @@ -21,6 +22,10 @@ internal class CardScannerInteractor( private val cardRecognitionSession: CardRecognitionSession ) : BaseInteractor() { + private companion object { + const val INIT_DELAY_MS = 500L + } + private val _completion = MutableStateFlow(Awaiting) val completion = _completion.asStateFlow() @@ -33,29 +38,25 @@ internal class CardScannerInteractor( init { collectRecognizedCards() interactorScope.launch { + // Delay permission request and camera preview initialization for smooth behaviour. + delay(INIT_DELAY_MS) _sideEffects.send(CameraPermissionRequest) } } private fun initState() = CardScannerInteractorState( - currentCard = null, - isTorchEnabled = false + isCameraPermissionGranted = false, + isTorchEnabled = false, + currentCard = null ) fun onEvent(event: CardScannerEvent) { when (event) { + is CameraPermissionResult -> handle(event) is ImageAnalysis -> interactorScope.launch { cardRecognitionSession.recognize(event.imageProxy) } is TorchToggle -> _state.update { it.copy(isTorchEnabled = event.isEnabled) } - is CameraPermissionResult -> if (!event.isGranted) { - cancel( - ProcessOutResult.Failure( - code = Generic(), - message = "Camera permission is not granted." - ) - ) - } is Cancel -> cancel( ProcessOutResult.Failure( code = Cancelled, @@ -66,6 +67,18 @@ internal class CardScannerInteractor( } } + private fun handle(event: CameraPermissionResult) { + _state.update { it.copy(isCameraPermissionGranted = event.isGranted) } + if (!event.isGranted) { + cancel( + ProcessOutResult.Failure( + code = Generic(), + message = "Camera permission is not granted." + ) + ) + } + } + private fun collectRecognizedCards() { interactorScope.launch(Dispatchers.Main.immediate) { cardRecognitionSession.currentCard.collect { card -> diff --git a/ui/src/main/kotlin/com/processout/sdk/ui/card/scanner/CardScannerInteractorState.kt b/ui/src/main/kotlin/com/processout/sdk/ui/card/scanner/CardScannerInteractorState.kt index daa858faf..ced759b0d 100644 --- a/ui/src/main/kotlin/com/processout/sdk/ui/card/scanner/CardScannerInteractorState.kt +++ b/ui/src/main/kotlin/com/processout/sdk/ui/card/scanner/CardScannerInteractorState.kt @@ -3,6 +3,7 @@ package com.processout.sdk.ui.card.scanner import com.processout.sdk.ui.card.scanner.recognition.POScannedCard internal data class CardScannerInteractorState( - val currentCard: POScannedCard?, - val isTorchEnabled: Boolean + val isCameraPermissionGranted: Boolean, + val isTorchEnabled: Boolean, + val currentCard: POScannedCard? ) diff --git a/ui/src/main/kotlin/com/processout/sdk/ui/card/scanner/CardScannerScreen.kt b/ui/src/main/kotlin/com/processout/sdk/ui/card/scanner/CardScannerScreen.kt index cadfe4c35..d9e41e5c3 100644 --- a/ui/src/main/kotlin/com/processout/sdk/ui/card/scanner/CardScannerScreen.kt +++ b/ui/src/main/kotlin/com/processout/sdk/ui/card/scanner/CardScannerScreen.kt @@ -21,6 +21,7 @@ import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.core.tween import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut +import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.layout.* import androidx.compose.foundation.rememberScrollState @@ -135,10 +136,9 @@ internal fun CardScannerScreen( style = style.description.textStyle ) CameraPreview( - isTorchEnabled = state.torchAction.checked, - currentCard = state.currentCard, + state = state, onEvent = onEvent, - cameraPreviewStyle = style.cameraPreview, + style = style.cameraPreview, cardStyle = style.card, modifier = Modifier .fillMaxWidth() @@ -163,30 +163,21 @@ internal fun CardScannerScreen( @Composable private fun CameraPreview( - isTorchEnabled: Boolean, - currentCard: POScannedCard?, + state: CardScannerViewModelState, onEvent: (CardScannerEvent) -> Unit, - cameraPreviewStyle: CameraPreviewStyle, + style: CameraPreviewStyle, cardStyle: CardStyle, modifier: Modifier = Modifier, offsetSize: Dp = spacing.extraLarge ) { - val cameraController = rememberLifecycleCameraController( - onAnalyze = { imageProxy -> - onEvent(ImageAnalysis(imageProxy)) - } - ) - LaunchedEffect(isTorchEnabled) { - cameraController.enableTorch(isTorchEnabled) - } Box( modifier = modifier .border( - width = cameraPreviewStyle.border.width, - color = cameraPreviewStyle.border.color, - shape = cameraPreviewStyle.shape + width = style.border.width, + color = style.border.color, + shape = style.shape ) - .clip(cameraPreviewStyle.shape) + .clip(style.shape) .drawWithContent { val offsetSizePx = offsetSize.toPx() val cardSize = androidx.compose.ui.geometry.Size( @@ -198,7 +189,7 @@ private fun CameraPreview( val cornerRadius = CornerRadius(cornerRadiusSizePx, cornerRadiusSizePx) drawContent() drawWithLayer { - drawRect(color = cameraPreviewStyle.overlayColor) + drawRect(color = style.overlayColor) drawRoundRect( size = cardSize, topLeft = topLeftOffset, @@ -216,20 +207,32 @@ private fun CameraPreview( ) } ) { - AndroidView( - modifier = modifier.clip(cameraPreviewStyle.shape), - factory = { - PreviewView(it).apply { - controller = cameraController - clipToOutline = true - scaleType = PreviewView.ScaleType.FILL_START - implementationMode = PreviewView.ImplementationMode.COMPATIBLE + if (state.isCameraPermissionGranted) { + val cameraController = rememberLifecycleCameraController( + onAnalyze = { imageProxy -> + onEvent(ImageAnalysis(imageProxy)) } - }, - onRelease = { cameraController.unbind() } - ) + ) + val isTorchEnabled = state.torchAction.checked + LaunchedEffect(isTorchEnabled) { + cameraController.enableTorch(isTorchEnabled) + } + AndroidView( + modifier = modifier, + factory = { + PreviewView(it).apply { + controller = cameraController + scaleType = PreviewView.ScaleType.FILL_START + implementationMode = PreviewView.ImplementationMode.COMPATIBLE + } + }, + onRelease = { cameraController.unbind() } + ) + } else { + Box(modifier.background(Color.Black)) + } ScannedCard( - card = currentCard, + card = state.currentCard, style = cardStyle ) } diff --git a/ui/src/main/kotlin/com/processout/sdk/ui/card/scanner/CardScannerViewModel.kt b/ui/src/main/kotlin/com/processout/sdk/ui/card/scanner/CardScannerViewModel.kt index 575f8019e..d39d077d1 100644 --- a/ui/src/main/kotlin/com/processout/sdk/ui/card/scanner/CardScannerViewModel.kt +++ b/ui/src/main/kotlin/com/processout/sdk/ui/card/scanner/CardScannerViewModel.kt @@ -61,7 +61,8 @@ internal class CardScannerViewModel( description = description ?: app.getString(R.string.po_card_scanner_description), currentCard = state.currentCard, torchAction = torchAction(state.isTorchEnabled), - cancelAction = cancelButton?.toAction() + cancelAction = cancelButton?.toAction(), + isCameraPermissionGranted = state.isCameraPermissionGranted ) } diff --git a/ui/src/main/kotlin/com/processout/sdk/ui/card/scanner/CardScannerViewModelState.kt b/ui/src/main/kotlin/com/processout/sdk/ui/card/scanner/CardScannerViewModelState.kt index 120d8b9ac..6b91300bc 100644 --- a/ui/src/main/kotlin/com/processout/sdk/ui/card/scanner/CardScannerViewModelState.kt +++ b/ui/src/main/kotlin/com/processout/sdk/ui/card/scanner/CardScannerViewModelState.kt @@ -8,5 +8,6 @@ internal data class CardScannerViewModelState( val description: String, val currentCard: POScannedCard?, val torchAction: POActionState, - val cancelAction: POActionState? + val cancelAction: POActionState?, + val isCameraPermissionGranted: Boolean ) From c7d9805ca88b80c3ecb49f7d367e01f65f11ac7b Mon Sep 17 00:00:00 2001 From: Vitalii Vanziak Date: Wed, 26 Mar 2025 21:14:58 +0200 Subject: [PATCH 11/32] Fix starting of card scanner from card tokenization --- .../ui/card/scanner/CardScannerInteractor.kt | 1 - .../CardTokenizationBottomSheet.kt | 6 +-- .../tokenization/CardTokenizationEvent.kt | 2 +- .../CardTokenizationInteractor.kt | 44 ++++++++++++++++--- .../CardTokenizationInteractorState.kt | 1 + .../tokenization/CardTokenizationScreen.kt | 13 ++---- 6 files changed, 47 insertions(+), 20 deletions(-) diff --git a/ui/src/main/kotlin/com/processout/sdk/ui/card/scanner/CardScannerInteractor.kt b/ui/src/main/kotlin/com/processout/sdk/ui/card/scanner/CardScannerInteractor.kt index 29458d15d..cd6751c1e 100644 --- a/ui/src/main/kotlin/com/processout/sdk/ui/card/scanner/CardScannerInteractor.kt +++ b/ui/src/main/kotlin/com/processout/sdk/ui/card/scanner/CardScannerInteractor.kt @@ -38,7 +38,6 @@ internal class CardScannerInteractor( init { collectRecognizedCards() interactorScope.launch { - // Delay permission request and camera preview initialization for smooth behaviour. delay(INIT_DELAY_MS) _sideEffects.send(CameraPermissionRequest) } diff --git a/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/CardTokenizationBottomSheet.kt b/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/CardTokenizationBottomSheet.kt index 220a75ee3..5b13ed741 100644 --- a/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/CardTokenizationBottomSheet.kt +++ b/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/CardTokenizationBottomSheet.kt @@ -15,7 +15,7 @@ import com.processout.sdk.api.dispatcher.PODefaultEventDispatchers import com.processout.sdk.api.model.response.POCard import com.processout.sdk.core.ProcessOutActivityResult import com.processout.sdk.core.ProcessOutResult -import com.processout.sdk.core.onSuccess +import com.processout.sdk.core.getOrNull import com.processout.sdk.core.toActivityResult import com.processout.sdk.ui.base.BaseBottomSheetDialogFragment import com.processout.sdk.ui.card.scanner.POCardScannerLauncher @@ -65,9 +65,7 @@ internal class CardTokenizationBottomSheet : BaseBottomSheetDialogFragment - result.onSuccess { card -> - viewModel.onEvent(CardScannerResult(card)) - } + viewModel.onEvent(CardScannerResult(result.getOrNull())) } ) viewModel.start() diff --git a/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/CardTokenizationEvent.kt b/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/CardTokenizationEvent.kt index 29695aaca..b0a09d2a0 100644 --- a/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/CardTokenizationEvent.kt +++ b/ui/src/main/kotlin/com/processout/sdk/ui/card/tokenization/CardTokenizationEvent.kt @@ -9,7 +9,7 @@ internal sealed interface CardTokenizationEvent { data class FieldValueChanged(val id: String, val value: TextFieldValue) : CardTokenizationEvent data class FieldFocusChanged(val id: String, val isFocused: Boolean) : CardTokenizationEvent data class Action(val id: String) : CardTokenizationEvent - data class CardScannerResult(val card: POScannedCard) : CardTokenizationEvent + data class CardScannerResult(val card: POScannedCard?) : CardTokenizationEvent data class Dismiss(val failure: ProcessOutResult.Failure) : CardTokenizationEvent } 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 a48e9307e..9d6569982 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 @@ -38,6 +38,7 @@ import com.processout.sdk.ui.shared.provider.address.AddressSpecificationProvide import kotlinx.coroutines.Job import kotlinx.coroutines.cancelChildren import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.receiveAsFlow @@ -57,6 +58,7 @@ internal class CardTokenizationInteractor( private companion object { const val IIN_LENGTH = 6 const val EXPIRATION_DATE_PART_LENGTH = 2 + const val CARD_SCANNER_DELAY_MS = 350L } private data class Expiration( @@ -128,6 +130,7 @@ internal class CardTokenizationInteractor( shouldCollect = configuration.savingAllowed ), focusedFieldId = CardFieldId.NUMBER, + pendingFocusedFieldId = null, primaryActionId = ActionId.SUBMIT, secondaryActionId = ActionId.CANCEL, cardScannerActionId = ActionId.CARD_SCANNER @@ -189,15 +192,26 @@ internal class CardTokenizationInteractor( is Action -> when (event.id) { ActionId.SUBMIT -> submit() ActionId.CANCEL -> cancel() - ActionId.CARD_SCANNER -> interactorScope.launch { - _sideEffects.send(CardScanner) - } + ActionId.CARD_SCANNER -> startCardScanner() } - is CardScannerResult -> update(event.card) + is CardScannerResult -> handle(event) is Dismiss -> POLogger.info("Dismissed: %s", event.failure) } } + private fun startCardScanner() { + interactorScope.launch { + rememberFocusedField() + delay(CARD_SCANNER_DELAY_MS) + _sideEffects.send(CardScanner) + } + } + + private fun handle(event: CardScannerResult) { + event.card?.let { updateCardFields(it) } + restoreFocusedField() + } + //endregion //region Update Field @@ -251,7 +265,7 @@ internal class CardTokenizationInteractor( } } else field - private fun update(card: POScannedCard) { + private fun updateCardFields(card: POScannedCard) { updateFieldValue( id = CardFieldId.NUMBER, value = cardNumberInputFilter.filter( @@ -289,6 +303,26 @@ internal class CardTokenizationInteractor( } } + private fun rememberFocusedField() { + _state.update { + val focusedFieldId = it.focusedFieldId + it.copy( + focusedFieldId = null, + pendingFocusedFieldId = focusedFieldId + ) + } + } + + private fun restoreFocusedField() { + _state.update { + val pendingFocusedFieldId = it.pendingFocusedFieldId + it.copy( + focusedFieldId = pendingFocusedFieldId, + pendingFocusedFieldId = null + ) + } + } + //endregion //region Issuer Information & Preferred Scheme 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 395d1453e..c75a8b772 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 @@ -13,6 +13,7 @@ internal data class CardTokenizationInteractorState( val addressSpecification: AddressSpecification? = null, val saveCardField: Field, val focusedFieldId: String?, + val pendingFocusedFieldId: String?, val primaryActionId: String, val secondaryActionId: String, val cardScannerActionId: String, 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 ebb900df5..9fff68024 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 @@ -94,14 +94,10 @@ internal fun CardTokenizationScreen( onContentHeightChanged(contentHeight) } ) { - var clearFocus by remember { mutableStateOf(false) } state.cardScannerAction?.let { action -> POButton( state = action, - onClick = { - clearFocus = true - onEvent(Action(id = it)) - }, + onClick = { onEvent(Action(id = it)) }, modifier = Modifier .fillMaxWidth() .requiredHeightIn(min = dimensions.buttonIconSizeSmall) @@ -115,10 +111,6 @@ internal fun CardTokenizationScreen( onEvent = onEvent, style = style ) - if (clearFocus || state.focusedFieldId == null) { - LocalFocusManager.current.clearFocus(force = true) - clearFocus = false - } } } } @@ -130,6 +122,9 @@ private fun Sections( onEvent: (CardTokenizationEvent) -> Unit, style: CardTokenizationScreen.Style ) { + if (state.focusedFieldId == null) { + LocalFocusManager.current.clearFocus(force = true) + } val lifecycleEvent = rememberLifecycleEvent() state.sections.elements.forEachIndexed { index, section -> val padding = if (section.id == FUTURE_PAYMENTS) { From 5ffa6ef0d34e1556224c3d140b428907f2a4a03a Mon Sep 17 00:00:00 2001 From: Vitalii Vanziak Date: Thu, 27 Mar 2025 12:14:14 +0200 Subject: [PATCH 12/32] CardScannerInteractor logs --- .../sdk/ui/card/scanner/CardScannerInteractor.kt | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/ui/src/main/kotlin/com/processout/sdk/ui/card/scanner/CardScannerInteractor.kt b/ui/src/main/kotlin/com/processout/sdk/ui/card/scanner/CardScannerInteractor.kt index cd6751c1e..9345b985c 100644 --- a/ui/src/main/kotlin/com/processout/sdk/ui/card/scanner/CardScannerInteractor.kt +++ b/ui/src/main/kotlin/com/processout/sdk/ui/card/scanner/CardScannerInteractor.kt @@ -36,6 +36,7 @@ internal class CardScannerInteractor( val sideEffects = _sideEffects.receiveAsFlow() init { + POLogger.info("Starting card scanner.") collectRecognizedCards() interactorScope.launch { delay(INIT_DELAY_MS) @@ -55,7 +56,10 @@ internal class CardScannerInteractor( is ImageAnalysis -> interactorScope.launch { cardRecognitionSession.recognize(event.imageProxy) } - is TorchToggle -> _state.update { it.copy(isTorchEnabled = event.isEnabled) } + is TorchToggle -> { + POLogger.debug("Torch toggle: ${event.isEnabled}.") + _state.update { it.copy(isTorchEnabled = event.isEnabled) } + } is Cancel -> cancel( ProcessOutResult.Failure( code = Cancelled, @@ -68,7 +72,9 @@ internal class CardScannerInteractor( private fun handle(event: CameraPermissionResult) { _state.update { it.copy(isCameraPermissionGranted = event.isGranted) } - if (!event.isGranted) { + if (event.isGranted) { + POLogger.info("Started: camera permission is granted.") + } else { cancel( ProcessOutResult.Failure( code = Generic(), @@ -81,11 +87,13 @@ internal class CardScannerInteractor( private fun collectRecognizedCards() { interactorScope.launch(Dispatchers.Main.immediate) { cardRecognitionSession.currentCard.collect { card -> + POLogger.debug("Current card: $card.") _state.update { it.copy(currentCard = card) } } } interactorScope.launch(Dispatchers.Main.immediate) { cardRecognitionSession.mostFrequentCard.collect { card -> + POLogger.debug("Most frequent card: $card.") _completion.update { Success(card) } } } From c1075a8f599ac49922ecac5e679481dd740cb932 Mon Sep 17 00:00:00 2001 From: Vitalii Vanziak Date: Thu, 27 Mar 2025 12:19:15 +0200 Subject: [PATCH 13/32] CardTokenizationInteractor raname metods --- .../ui/card/tokenization/CardTokenizationInteractor.kt | 8 ++++---- 1 file changed, 4 insertions(+), 4 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 9d6569982..1f22e5fea 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 @@ -201,7 +201,7 @@ internal class CardTokenizationInteractor( private fun startCardScanner() { interactorScope.launch { - rememberFocusedField() + clearAndRememberFieldFocus() delay(CARD_SCANNER_DELAY_MS) _sideEffects.send(CardScanner) } @@ -209,7 +209,7 @@ internal class CardTokenizationInteractor( private fun handle(event: CardScannerResult) { event.card?.let { updateCardFields(it) } - restoreFocusedField() + restoreFieldFocus() } //endregion @@ -303,7 +303,7 @@ internal class CardTokenizationInteractor( } } - private fun rememberFocusedField() { + private fun clearAndRememberFieldFocus() { _state.update { val focusedFieldId = it.focusedFieldId it.copy( @@ -313,7 +313,7 @@ internal class CardTokenizationInteractor( } } - private fun restoreFocusedField() { + private fun restoreFieldFocus() { _state.update { val pendingFocusedFieldId = it.pendingFocusedFieldId it.copy( From af52aab9e34646dc43478f40407e607a3fad862c Mon Sep 17 00:00:00 2001 From: Vitalii Vanziak Date: Thu, 27 Mar 2025 12:29:03 +0200 Subject: [PATCH 14/32] Logs --- .../sdk/ui/card/tokenization/CardTokenizationInteractor.kt | 5 +++-- 1 file changed, 3 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 1f22e5fea..7192a2f65 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 @@ -201,7 +201,7 @@ internal class CardTokenizationInteractor( private fun startCardScanner() { interactorScope.launch { - clearAndRememberFieldFocus() + rememberAndClearFieldFocus() delay(CARD_SCANNER_DELAY_MS) _sideEffects.send(CardScanner) } @@ -266,6 +266,7 @@ internal class CardTokenizationInteractor( } else field private fun updateCardFields(card: POScannedCard) { + POLogger.debug("Updating card field values with the scanned card: $card.") updateFieldValue( id = CardFieldId.NUMBER, value = cardNumberInputFilter.filter( @@ -303,7 +304,7 @@ internal class CardTokenizationInteractor( } } - private fun clearAndRememberFieldFocus() { + private fun rememberAndClearFieldFocus() { _state.update { val focusedFieldId = it.focusedFieldId it.copy( From a71bac671de067163c837a9a1fbebc3edc05e94f Mon Sep 17 00:00:00 2001 From: Vitalii Vanziak Date: Thu, 27 Mar 2025 12:58:50 +0200 Subject: [PATCH 15/32] Extended PODynamicCheckoutConfiguration --- .../checkout/PODynamicCheckoutConfiguration.kt | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) 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 d6ac5cd1f..388ac2a70 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 @@ -9,6 +9,7 @@ import androidx.annotation.IntRange 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.checkout.PODynamicCheckoutConfiguration.GooglePayConfiguration.Environment import com.processout.sdk.ui.core.annotation.ProcessOutInternalApi import com.processout.sdk.ui.core.shared.image.PODrawableImage @@ -66,15 +67,29 @@ data class PODynamicCheckoutConfiguration( /** * Specifies card payment configuration. * + * @param[cardScanner] Card scanner configuration. Use _null_ to hide. * @param[billingAddress] Specifies billing address configuration. * @param[metadata] Metadata related to the card. */ @Parcelize data class CardConfiguration( + val cardScanner: CardScannerConfiguration? = CardScannerConfiguration(), val billingAddress: BillingAddressConfiguration = BillingAddressConfiguration(), val metadata: Map? = null ) : Parcelable { + /** + * Specifies card scanner configuration. + * + * @param[scanButton] Scan button configuration. + * @param[configuration] Card scanner configuration. + */ + @Parcelize + data class CardScannerConfiguration( + val scanButton: Button = Button(), + val configuration: POCardScannerConfiguration = POCardScannerConfiguration() + ) : Parcelable + /** * Specifies billing address configuration. * @@ -311,6 +326,7 @@ data class PODynamicCheckoutConfiguration( * @param[errorText] Error text style. * @param[messageBox] Message box style. * @param[dialog] Dialog style. + * @param[scanCardButton] Scan card button style. * @param[actionsContainer] Style of action buttons and their container. * @param[backgroundColorResId] Color resource ID for background. * @param[progressIndicatorColorResId] Color resource ID for progress indicator. @@ -333,6 +349,7 @@ data class PODynamicCheckoutConfiguration( val errorText: POTextStyle? = null, val messageBox: POMessageBoxStyle? = null, val dialog: PODialogStyle? = null, + val scanCardButton: POButtonStyle? = null, val actionsContainer: POActionsContainerStyle? = null, @ColorRes val backgroundColorResId: Int? = null, From 601dd46cf82fbbb0b74c50e9cbefc2eba9a84e77 Mon Sep 17 00:00:00 2001 From: Vitalii Vanziak Date: Thu, 27 Mar 2025 13:03:57 +0200 Subject: [PATCH 16/32] Updated DynamicCheckoutInteractorState --- .../sdk/ui/checkout/DynamicCheckoutInteractorState.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ui/src/main/kotlin/com/processout/sdk/ui/checkout/DynamicCheckoutInteractorState.kt b/ui/src/main/kotlin/com/processout/sdk/ui/checkout/DynamicCheckoutInteractorState.kt index 16f552017..d9dd79c42 100644 --- a/ui/src/main/kotlin/com/processout/sdk/ui/checkout/DynamicCheckoutInteractorState.kt +++ b/ui/src/main/kotlin/com/processout/sdk/ui/checkout/DynamicCheckoutInteractorState.kt @@ -71,6 +71,7 @@ internal data class DynamicCheckoutInteractorState( data class Actions( val submitId: String = ActionId.SUBMIT, val cancelId: String = ActionId.CANCEL, + val cardScannerId: String = ActionId.CARD_SCANNER, val savedPaymentMethodsId: String = ActionId.SAVED_PAYMENT_METHODS ) @@ -85,6 +86,7 @@ internal data class DynamicCheckoutInteractorState( object ActionId { const val SUBMIT = "dc-submit" const val CANCEL = "dc-cancel" + const val CARD_SCANNER = "dc-card-scanner" const val SAVED_PAYMENT_METHODS = "dc-saved-payment-methods" } } From 5fd89adb9c36f756d4fd7e6435618aa4e27fc6a1 Mon Sep 17 00:00:00 2001 From: Vitalii Vanziak Date: Thu, 27 Mar 2025 13:06:22 +0200 Subject: [PATCH 17/32] Revert --- .../sdk/ui/checkout/DynamicCheckoutInteractorState.kt | 2 -- 1 file changed, 2 deletions(-) diff --git a/ui/src/main/kotlin/com/processout/sdk/ui/checkout/DynamicCheckoutInteractorState.kt b/ui/src/main/kotlin/com/processout/sdk/ui/checkout/DynamicCheckoutInteractorState.kt index d9dd79c42..16f552017 100644 --- a/ui/src/main/kotlin/com/processout/sdk/ui/checkout/DynamicCheckoutInteractorState.kt +++ b/ui/src/main/kotlin/com/processout/sdk/ui/checkout/DynamicCheckoutInteractorState.kt @@ -71,7 +71,6 @@ internal data class DynamicCheckoutInteractorState( data class Actions( val submitId: String = ActionId.SUBMIT, val cancelId: String = ActionId.CANCEL, - val cardScannerId: String = ActionId.CARD_SCANNER, val savedPaymentMethodsId: String = ActionId.SAVED_PAYMENT_METHODS ) @@ -86,7 +85,6 @@ internal data class DynamicCheckoutInteractorState( object ActionId { const val SUBMIT = "dc-submit" const val CANCEL = "dc-cancel" - const val CARD_SCANNER = "dc-card-scanner" const val SAVED_PAYMENT_METHODS = "dc-saved-payment-methods" } } From f807e6143c83c4ef9db82d4706c82744d5a3a1a4 Mon Sep 17 00:00:00 2001 From: Vitalii Vanziak Date: Thu, 27 Mar 2025 13:21:01 +0200 Subject: [PATCH 18/32] Map card scanner config on DC --- .../sdk/ui/checkout/DynamicCheckoutActivity.kt | 10 ++++++++++ 1 file changed, 10 insertions(+) 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 dc1f20708..3042ea134 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 @@ -42,6 +42,7 @@ import com.processout.sdk.ui.base.BaseTransparentPortraitActivity 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.checkout.DynamicCheckoutActivityContract.Companion.EXTRA_CONFIGURATION import com.processout.sdk.ui.checkout.DynamicCheckoutActivityContract.Companion.EXTRA_RESULT import com.processout.sdk.ui.checkout.DynamicCheckoutCompletion.Failure @@ -101,6 +102,15 @@ internal class DynamicCheckoutActivity : BaseTransparentPortraitActivity() { private fun cardTokenizationConfiguration(): POCardTokenizationConfiguration { val billingAddress = configuration?.card?.billingAddress return POCardTokenizationConfiguration( + cardScanner = configuration?.card?.cardScanner?.let { + CardScannerConfiguration( + scanButton = POCardTokenizationConfiguration.Button( + text = it.scanButton.text, + icon = it.scanButton.icon + ), + configuration = it.configuration + ) + }, billingAddress = BillingAddressConfiguration( defaultAddress = billingAddress?.defaultAddress, attachDefaultsToPaymentMethod = billingAddress?.attachDefaultsToPaymentMethod ?: false From 5c0cd0c757ccb24b9509ccf2648e9a874a7d9768 Mon Sep 17 00:00:00 2001 From: Vitalii Vanziak Date: Thu, 27 Mar 2025 13:35:00 +0200 Subject: [PATCH 19/32] Move card scanner button in Sections on CardTokenizationScreen --- .../tokenization/CardTokenizationScreen.kt | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 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 9fff68024..326cc78fa 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 @@ -94,18 +94,6 @@ internal fun CardTokenizationScreen( onContentHeightChanged(contentHeight) } ) { - state.cardScannerAction?.let { action -> - POButton( - state = action, - onClick = { onEvent(Action(id = it)) }, - modifier = Modifier - .fillMaxWidth() - .requiredHeightIn(min = dimensions.buttonIconSizeSmall) - .padding(bottom = spacing.small), - style = style.scanButton, - iconSize = dimensions.iconSizeSmall - ) - } Sections( state = state, onEvent = onEvent, @@ -125,6 +113,18 @@ private fun Sections( if (state.focusedFieldId == null) { LocalFocusManager.current.clearFocus(force = true) } + state.cardScannerAction?.let { action -> + POButton( + state = action, + onClick = { onEvent(Action(id = it)) }, + modifier = Modifier + .fillMaxWidth() + .requiredHeightIn(min = dimensions.buttonIconSizeSmall) + .padding(bottom = spacing.small), + style = style.scanButton, + iconSize = dimensions.iconSizeSmall + ) + } val lifecycleEvent = rememberLifecycleEvent() state.sections.elements.forEachIndexed { index, section -> val padding = if (section.id == FUTURE_PAYMENTS) { From 1d96cf04e53edf48770dc15124d36632cac9b082 Mon Sep 17 00:00:00 2001 From: Vitalii Vanziak Date: Thu, 27 Mar 2025 13:41:03 +0200 Subject: [PATCH 20/32] Add card scanner button on DC --- .../ui/checkout/screen/CardTokenization.kt | 29 ++++++++++++++----- .../checkout/screen/DynamicCheckoutScreen.kt | 4 +++ 2 files changed, 26 insertions(+), 7 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 4019641a5..e65eb377a 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 @@ -17,12 +17,8 @@ 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.SectionId.FUTURE_PAYMENTS import com.processout.sdk.ui.checkout.DynamicCheckoutEvent -import com.processout.sdk.ui.checkout.DynamicCheckoutEvent.FieldFocusChanged -import com.processout.sdk.ui.checkout.DynamicCheckoutEvent.FieldValueChanged -import com.processout.sdk.ui.core.component.POAnimatedImage -import com.processout.sdk.ui.core.component.POExpandableText -import com.processout.sdk.ui.core.component.PORequestFocus -import com.processout.sdk.ui.core.component.POText +import com.processout.sdk.ui.checkout.DynamicCheckoutEvent.* +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 @@ -43,6 +39,25 @@ internal fun CardTokenization( if (state.focusedFieldId == null) { LocalFocusManager.current.clearFocus(force = true) } + state.cardScannerAction?.let { action -> + POButton( + state = action, + onClick = { + onEvent( + Action( + actionId = it, + paymentMethodId = id + ) + ) + }, + modifier = Modifier + .fillMaxWidth() + .requiredHeightIn(min = dimensions.buttonIconSizeSmall) + .padding(bottom = spacing.small), + style = style.scanCardButton, + iconSize = dimensions.iconSizeSmall + ) + } val lifecycleEvent = rememberLifecycleEvent() state.sections.elements.forEachIndexed { index, section -> val padding = if (section.id == FUTURE_PAYMENTS) { @@ -191,7 +206,7 @@ private fun TextField( enabled = isPrimaryActionEnabled, onClick = { onEvent( - DynamicCheckoutEvent.Action( + Action( actionId = it, paymentMethodId = id ) diff --git a/ui/src/main/kotlin/com/processout/sdk/ui/checkout/screen/DynamicCheckoutScreen.kt b/ui/src/main/kotlin/com/processout/sdk/ui/checkout/screen/DynamicCheckoutScreen.kt index a1ecaed9d..5dc582707 100644 --- a/ui/src/main/kotlin/com/processout/sdk/ui/checkout/screen/DynamicCheckoutScreen.kt +++ b/ui/src/main/kotlin/com/processout/sdk/ui/checkout/screen/DynamicCheckoutScreen.kt @@ -678,6 +678,7 @@ internal object DynamicCheckoutScreen { val errorText: POText.Style, val messageBox: POMessageBox.Style, val dialog: PODialog.Style, + val scanCardButton: POButton.Style, val actionsContainer: POActionsContainer.Style, val backgroundColor: Color, val progressIndicatorColor: Color, @@ -751,6 +752,9 @@ internal object DynamicCheckoutScreen { dialog = custom?.dialog?.let { PODialog.custom(style = it) } ?: PODialog.default, + scanCardButton = custom?.scanCardButton?.let { + POButton.custom(style = it) + } ?: POButton.secondary, actionsContainer = custom?.actionsContainer?.let { POActionsContainer.custom(style = it) } ?: POActionsContainer.default, From 123bf2c99c9a15b1a3710aa1427e5b5d395daab4 Mon Sep 17 00:00:00 2001 From: Vitalii Vanziak Date: Thu, 27 Mar 2025 13:46:59 +0200 Subject: [PATCH 21/32] Dispatchers.Main.immediate --- .../com/processout/sdk/ui/checkout/DynamicCheckoutInteractor.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 a7f51f7f6..21fb4633a 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 @@ -1022,7 +1022,7 @@ internal class DynamicCheckoutInteractor( eventDispatcher.send(request) } } - interactorScope.launch { + interactorScope.launch(Dispatchers.Main.immediate) { nativeAlternativePayment.sideEffects.collect { sideEffect -> when (sideEffect) { is NativeAlternativePaymentSideEffect.PermissionRequest -> From aa3b9236e473807b7c4a5ba4311b84aa2933aaf9 Mon Sep 17 00:00:00 2001 From: Vitalii Vanziak Date: Thu, 27 Mar 2025 14:00:44 +0200 Subject: [PATCH 22/32] Log --- .../sdk/ui/card/tokenization/CardTokenizationInteractor.kt | 1 + 1 file changed, 1 insertion(+) 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 7192a2f65..54202c393 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 @@ -204,6 +204,7 @@ internal class CardTokenizationInteractor( rememberAndClearFieldFocus() delay(CARD_SCANNER_DELAY_MS) _sideEffects.send(CardScanner) + POLogger.info("Starting card scanner.") } } From 4350ed1e5020a19c9d370a9dd6398c7f0fe13bd9 Mon Sep 17 00:00:00 2001 From: Vitalii Vanziak Date: Thu, 27 Mar 2025 14:07:48 +0200 Subject: [PATCH 23/32] dispatchSideEffects --- .../sdk/ui/checkout/DynamicCheckoutEvent.kt | 2 ++ .../ui/checkout/DynamicCheckoutInteractor.kt | 19 +++++++++++++++---- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/ui/src/main/kotlin/com/processout/sdk/ui/checkout/DynamicCheckoutEvent.kt b/ui/src/main/kotlin/com/processout/sdk/ui/checkout/DynamicCheckoutEvent.kt index be56b2f7c..9a6abff05 100644 --- a/ui/src/main/kotlin/com/processout/sdk/ui/checkout/DynamicCheckoutEvent.kt +++ b/ui/src/main/kotlin/com/processout/sdk/ui/checkout/DynamicCheckoutEvent.kt @@ -85,6 +85,8 @@ internal sealed interface DynamicCheckoutSideEffect { val permission: String ) : DynamicCheckoutSideEffect + data object CardScanner : DynamicCheckoutSideEffect + data object CancelWebAuthorization : DynamicCheckoutSideEffect data object BeforeSuccess : DynamicCheckoutSideEffect 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 21fb4633a..695365839 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 @@ -39,10 +39,7 @@ import com.processout.sdk.core.logger.POLogger import com.processout.sdk.core.onFailure import com.processout.sdk.core.onSuccess import com.processout.sdk.ui.base.BaseInteractor -import com.processout.sdk.ui.card.tokenization.CardTokenizationCompletion -import com.processout.sdk.ui.card.tokenization.CardTokenizationEvent -import com.processout.sdk.ui.card.tokenization.CardTokenizationViewModel -import com.processout.sdk.ui.card.tokenization.POCardTokenizationConfiguration +import com.processout.sdk.ui.card.tokenization.* import com.processout.sdk.ui.card.tokenization.POCardTokenizationConfiguration.BillingAddressConfiguration.CollectionMode import com.processout.sdk.ui.checkout.DynamicCheckoutCompletion.* import com.processout.sdk.ui.checkout.DynamicCheckoutEvent.* @@ -128,6 +125,7 @@ internal class DynamicCheckoutInteractor( private suspend fun start() { handleCompletions() dispatchEvents() + dispatchSideEffects() collectInvoice() collectInvoiceAuthorizationRequest() collectTokenizedCard() @@ -1022,6 +1020,19 @@ internal class DynamicCheckoutInteractor( eventDispatcher.send(request) } } + } + + private fun dispatchSideEffects() { + interactorScope.launch(Dispatchers.Main.immediate) { + cardTokenization.sideEffects.collect { sideEffect -> + when (sideEffect) { + CardTokenizationSideEffect.CardScanner -> { + _sideEffects.send(DynamicCheckoutSideEffect.CardScanner) + POLogger.info("Starting card scanner.") + } + } + } + } interactorScope.launch(Dispatchers.Main.immediate) { nativeAlternativePayment.sideEffects.collect { sideEffect -> when (sideEffect) { From a920531dff100a6072f41fc11652827de1d71c20 Mon Sep 17 00:00:00 2001 From: Vitalii Vanziak Date: Thu, 27 Mar 2025 14:13:19 +0200 Subject: [PATCH 24/32] Logs --- .../sdk/ui/card/tokenization/CardTokenizationInteractor.kt | 2 +- .../com/processout/sdk/ui/checkout/DynamicCheckoutInteractor.kt | 2 +- 2 files changed, 2 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 54202c393..bfff7d3c0 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 @@ -203,8 +203,8 @@ internal class CardTokenizationInteractor( interactorScope.launch { rememberAndClearFieldFocus() delay(CARD_SCANNER_DELAY_MS) - _sideEffects.send(CardScanner) POLogger.info("Starting card scanner.") + _sideEffects.send(CardScanner) } } 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 695365839..f0aa8a109 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 @@ -1027,8 +1027,8 @@ internal class DynamicCheckoutInteractor( cardTokenization.sideEffects.collect { sideEffect -> when (sideEffect) { CardTokenizationSideEffect.CardScanner -> { - _sideEffects.send(DynamicCheckoutSideEffect.CardScanner) POLogger.info("Starting card scanner.") + _sideEffects.send(DynamicCheckoutSideEffect.CardScanner) } } } From 8b72e46641952af4ef67f43e9c691858810453e5 Mon Sep 17 00:00:00 2001 From: Vitalii Vanziak Date: Thu, 27 Mar 2025 15:44:10 +0200 Subject: [PATCH 25/32] DC: launch card scanner and handle result --- .../ui/card/scanner/CardScannerInteractor.kt | 2 -- .../tokenization/CardTokenizationInteractor.kt | 1 - .../sdk/ui/checkout/DynamicCheckoutActivity.kt | 17 +++++++++++++---- .../sdk/ui/checkout/DynamicCheckoutEvent.kt | 5 +++++ .../ui/checkout/DynamicCheckoutInteractor.kt | 9 ++++++++- 5 files changed, 26 insertions(+), 8 deletions(-) diff --git a/ui/src/main/kotlin/com/processout/sdk/ui/card/scanner/CardScannerInteractor.kt b/ui/src/main/kotlin/com/processout/sdk/ui/card/scanner/CardScannerInteractor.kt index 9345b985c..02b2dc611 100644 --- a/ui/src/main/kotlin/com/processout/sdk/ui/card/scanner/CardScannerInteractor.kt +++ b/ui/src/main/kotlin/com/processout/sdk/ui/card/scanner/CardScannerInteractor.kt @@ -87,13 +87,11 @@ internal class CardScannerInteractor( private fun collectRecognizedCards() { interactorScope.launch(Dispatchers.Main.immediate) { cardRecognitionSession.currentCard.collect { card -> - POLogger.debug("Current card: $card.") _state.update { it.copy(currentCard = card) } } } interactorScope.launch(Dispatchers.Main.immediate) { cardRecognitionSession.mostFrequentCard.collect { card -> - POLogger.debug("Most frequent card: $card.") _completion.update { Success(card) } } } 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 bfff7d3c0..7192a2f65 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 @@ -203,7 +203,6 @@ internal class CardTokenizationInteractor( interactorScope.launch { rememberAndClearFieldFocus() delay(CARD_SCANNER_DELAY_MS) - POLogger.info("Starting card scanner.") _sideEffects.send(CardScanner) } } 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 3042ea134..554618c12 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 @@ -31,14 +31,12 @@ import com.processout.sdk.api.model.event.POSavedPaymentMethodsEvent.DidDeleteCu import com.processout.sdk.api.model.request.POInvoiceRequest import com.processout.sdk.api.model.response.POAlternativePaymentMethodResponse import com.processout.sdk.api.model.response.POGooglePayCardTokenizationData +import com.processout.sdk.core.* import com.processout.sdk.core.POFailure.Code.Cancelled import com.processout.sdk.core.POFailure.Code.Generic -import com.processout.sdk.core.POUnit -import com.processout.sdk.core.ProcessOutActivityResult -import com.processout.sdk.core.ProcessOutResult -import com.processout.sdk.core.toActivityResult import com.processout.sdk.ui.apm.POAlternativePaymentMethodCustomTabLauncher 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 @@ -190,6 +188,8 @@ internal class DynamicCheckoutActivity : BaseTransparentPortraitActivity() { ) private var pendingPermissionRequest: PermissionRequest? = null + private lateinit var cardScannerLauncher: POCardScannerLauncher + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) enableEdgeToEdge(statusBarStyle = SystemBarStyle.dark(Color.TRANSPARENT)) @@ -213,6 +213,12 @@ internal class DynamicCheckoutActivity : BaseTransparentPortraitActivity() { delegate = savedPaymentMethodsDelegate, callback = { pendingSavedPaymentMethods = false } ) + cardScannerLauncher = POCardScannerLauncher.create( + from = this, + callback = { result -> + viewModel.onEvent(CardScannerResult(result.getOrNull())) + } + ) setContent { val isLightTheme = !isSystemInDarkTheme() ProcessOutTheme(isLightTheme = isLightTheme) { @@ -286,6 +292,9 @@ internal class DynamicCheckoutActivity : BaseTransparentPortraitActivity() { savedPaymentMethodsLauncher.launch(sideEffect.configuration) } is PermissionRequest -> requestPermission(sideEffect) + CardScanner -> configuration?.card?.cardScanner?.configuration?.let { + cardScannerLauncher.launch(configuration = it) + } CancelWebAuthorization -> cancelWebAuthorization() BeforeSuccess -> if (pendingSavedPaymentMethods) { savedPaymentMethodsLauncher.finish() diff --git a/ui/src/main/kotlin/com/processout/sdk/ui/checkout/DynamicCheckoutEvent.kt b/ui/src/main/kotlin/com/processout/sdk/ui/checkout/DynamicCheckoutEvent.kt index 9a6abff05..6b34ce12c 100644 --- a/ui/src/main/kotlin/com/processout/sdk/ui/checkout/DynamicCheckoutEvent.kt +++ b/ui/src/main/kotlin/com/processout/sdk/ui/checkout/DynamicCheckoutEvent.kt @@ -4,6 +4,7 @@ import androidx.compose.ui.text.input.TextFieldValue import com.processout.sdk.api.model.response.POAlternativePaymentMethodResponse import com.processout.sdk.api.model.response.POGooglePayCardTokenizationData import com.processout.sdk.core.ProcessOutResult +import com.processout.sdk.ui.card.scanner.recognition.POScannedCard import com.processout.sdk.ui.savedpaymentmethods.POSavedPaymentMethodsConfiguration import org.json.JSONObject @@ -55,6 +56,10 @@ internal sealed interface DynamicCheckoutEvent { val isGranted: Boolean ) : DynamicCheckoutEvent + data class CardScannerResult( + val card: POScannedCard? + ) : DynamicCheckoutEvent + data class CustomerTokenDeleted( val tokenId: String ) : DynamicCheckoutEvent 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 f0aa8a109..12700670c 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 @@ -419,6 +419,7 @@ internal class DynamicCheckoutInteractor( is GooglePayResult -> handleGooglePay(event.paymentMethodId, event.result) is AlternativePaymentResult -> handleAlternativePayment(event.paymentMethodId, event.result) is PermissionRequestResult -> handlePermission(event) + is CardScannerResult -> handleCardScanner(event) is CustomerTokenDeleted -> deleteLocalCustomerToken(event.tokenId) is Dismiss -> dismiss(event) } @@ -1027,7 +1028,6 @@ internal class DynamicCheckoutInteractor( cardTokenization.sideEffects.collect { sideEffect -> when (sideEffect) { CardTokenizationSideEffect.CardScanner -> { - POLogger.info("Starting card scanner.") _sideEffects.send(DynamicCheckoutSideEffect.CardScanner) } } @@ -1063,6 +1063,13 @@ internal class DynamicCheckoutInteractor( } } + private fun handleCardScanner(result: CardScannerResult) { + result.card?.let { POLogger.debug("Scanned card: $it") } + cardTokenization.onEvent( + CardTokenizationEvent.CardScannerResult(result.card) + ) + } + private fun deleteLocalCustomerToken(tokenId: String) { _state.update { state -> state.copy( From fd4851a3f926a606117c03dc8d1ced3780f97391 Mon Sep 17 00:00:00 2001 From: Vitalii Vanziak Date: Thu, 27 Mar 2025 16:06:56 +0200 Subject: [PATCH 26/32] Fix initConfiguration() on DC --- .../com/processout/sdk/ui/checkout/DynamicCheckoutActivity.kt | 4 +--- 1 file changed, 1 insertion(+), 3 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 554618c12..96e322853 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 @@ -193,9 +193,7 @@ internal class DynamicCheckoutActivity : BaseTransparentPortraitActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) enableEdgeToEdge(statusBarStyle = SystemBarStyle.dark(Color.TRANSPARENT)) - if (savedInstanceState == null) { - initConfiguration() - } + initConfiguration() dispatchBackPressed() googlePayLauncher = POGooglePayCardTokenizationLauncher.create( from = this, From 5716173fe3b5ca3e5ca85d3f0b4629b173fa9f83 Mon Sep 17 00:00:00 2001 From: Vitalii Vanziak Date: Thu, 27 Mar 2025 16:14:56 +0200 Subject: [PATCH 27/32] AGP 8.9.1 --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 116f37a84..42ad70a46 100644 --- a/build.gradle +++ b/build.gradle @@ -2,7 +2,7 @@ buildscript { ext { - androidGradlePluginVersion = '8.9.0' + androidGradlePluginVersion = '8.9.1' kotlinVersion = '2.1.10' kspVersion = '2.1.10-1.0.29' dokkaVersion = '1.9.20' From 4e332c45be44cd7b00b766a11458128c3f3e9831 Mon Sep 17 00:00:00 2001 From: Vitalii Vanziak Date: Thu, 27 Mar 2025 16:22:33 +0200 Subject: [PATCH 28/32] Updated libs --- build.gradle | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/build.gradle b/build.gradle index 42ad70a46..7f216f3d1 100644 --- a/build.gradle +++ b/build.gradle @@ -6,7 +6,7 @@ buildscript { kotlinVersion = '2.1.10' kspVersion = '2.1.10-1.0.29' dokkaVersion = '1.9.20' - androidxNavigationVersion = '2.8.6' + androidxNavigationVersion = '2.8.9' nexusPublishPluginVersion = '2.0.0' } dependencies { @@ -38,14 +38,14 @@ ext { androidxCoreVersion = '1.15.0' androidxAppCompatVersion = '1.7.0' - androidxConstraintLayoutVersion = '2.2.0' - androidxActivityVersion = '1.10.0' - androidxFragmentVersion = '1.8.5' + androidxConstraintLayoutVersion = '2.2.1' + androidxActivityVersion = '1.10.1' + androidxFragmentVersion = '1.8.6' androidxLifecycleVersion = '2.8.7' androidxRecyclerViewVersion = '1.4.0' androidxSwipeRefreshLayoutVersion = '1.1.0' androidxBrowserVersion = '1.8.0' - androidxCameraVersion = '1.4.1' + androidxCameraVersion = '1.4.2' androidxComposeBOMVersion = '2025.03.00' composeGooglePayButtonVersion = '1.0.0' From 3f01f94db5beffe529f906f42e9bdf178f2193bd Mon Sep 17 00:00:00 2001 From: Vitalii Vanziak Date: Thu, 27 Mar 2025 16:29:58 +0200 Subject: [PATCH 29/32] Kotlin 2.1.20 --- .idea/kotlinc.xml | 2 +- build.gradle | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml index c22b6fa9e..131e44d79 100644 --- a/.idea/kotlinc.xml +++ b/.idea/kotlinc.xml @@ -1,6 +1,6 @@ - \ No newline at end of file diff --git a/build.gradle b/build.gradle index 7f216f3d1..1466901d6 100644 --- a/build.gradle +++ b/build.gradle @@ -3,8 +3,8 @@ buildscript { ext { androidGradlePluginVersion = '8.9.1' - kotlinVersion = '2.1.10' - kspVersion = '2.1.10-1.0.29' + kotlinVersion = '2.1.20' + kspVersion = '2.1.20-1.0.32' dokkaVersion = '1.9.20' androidxNavigationVersion = '2.8.9' nexusPublishPluginVersion = '2.0.0' From f5de93bd0788cdc19b71db44954b6228c45c0744 Mon Sep 17 00:00:00 2001 From: Vitalii Vanziak Date: Thu, 27 Mar 2025 16:39:04 +0200 Subject: [PATCH 30/32] toUri --- .../com/processout/sdk/ui/checkout/DynamicCheckoutActivity.kt | 4 ++-- 1 file changed, 2 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 96e322853..a3d776b2c 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 @@ -4,7 +4,6 @@ import android.app.Activity import android.content.Intent import android.content.pm.PackageManager import android.graphics.Color -import android.net.Uri import android.os.Build import android.os.Bundle import androidx.activity.SystemBarStyle @@ -18,6 +17,7 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.remember import androidx.core.app.ActivityCompat import androidx.core.content.ContextCompat +import androidx.core.net.toUri import androidx.lifecycle.Lifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.google.android.gms.wallet.Wallet.WalletOptions @@ -281,7 +281,7 @@ internal class DynamicCheckoutActivity : BaseTransparentPortraitActivity() { is AlternativePayment -> { pendingAlternativePayment = sideEffect alternativePaymentLauncher.launch( - uri = Uri.parse(sideEffect.redirectUrl), + uri = sideEffect.redirectUrl.toUri(), returnUrl = sideEffect.returnUrl ) } From 3518ac0c132b081dd6654ba21565949604734fd3 Mon Sep 17 00:00:00 2001 From: Vitalii Vanziak Date: Thu, 27 Mar 2025 17:06:27 +0200 Subject: [PATCH 31/32] lateinit var configuration: PODynamicCheckoutConfiguration --- .../ui/checkout/DynamicCheckoutActivity.kt | 80 +++++++++---------- 1 file changed, 37 insertions(+), 43 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 a3d776b2c..df265443d 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 @@ -21,7 +21,6 @@ import androidx.core.net.toUri import androidx.lifecycle.Lifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.google.android.gms.wallet.Wallet.WalletOptions -import com.google.android.gms.wallet.WalletConstants import com.processout.sdk.R import com.processout.sdk.api.ProcessOut import com.processout.sdk.api.dispatcher.card.tokenization.PODefaultCardTokenizationEventDispatcher @@ -54,10 +53,8 @@ import com.processout.sdk.ui.googlepay.POGooglePayCardTokenizationLauncher import com.processout.sdk.ui.napm.NativeAlternativePaymentViewModel import com.processout.sdk.ui.napm.PONativeAlternativePaymentConfiguration import com.processout.sdk.ui.napm.PONativeAlternativePaymentConfiguration.PaymentConfirmationConfiguration -import com.processout.sdk.ui.napm.PONativeAlternativePaymentConfiguration.PaymentConfirmationConfiguration.Companion.DEFAULT_TIMEOUT_SECONDS import com.processout.sdk.ui.savedpaymentmethods.POSavedPaymentMethodsDelegate import com.processout.sdk.ui.savedpaymentmethods.POSavedPaymentMethodsLauncher -import com.processout.sdk.ui.shared.configuration.POBarcodeConfiguration import com.processout.sdk.ui.shared.extension.collectImmediately import com.processout.sdk.ui.web.customtab.POCustomTabAuthorizationActivity import com.processout.sdk.ui.web.customtab.POCustomTabAuthorizationActivityContract @@ -66,7 +63,7 @@ import com.processout.sdk.ui.web.webview.POWebViewAuthorizationActivityContract internal class DynamicCheckoutActivity : BaseTransparentPortraitActivity() { - private var configuration: PODynamicCheckoutConfiguration? = null + private lateinit var configuration: PODynamicCheckoutConfiguration private val viewModel: DynamicCheckoutViewModel by viewModels { val cardTokenizationEventDispatcher = PODefaultCardTokenizationEventDispatcher() @@ -87,9 +84,7 @@ internal class DynamicCheckoutActivity : BaseTransparentPortraitActivity() { } DynamicCheckoutViewModel.Factory( app = application, - configuration = configuration ?: PODynamicCheckoutConfiguration( - invoiceRequest = POInvoiceRequest(invoiceId = String()) - ), + configuration = configuration, cardTokenization = cardTokenization, cardTokenizationEventDispatcher = cardTokenizationEventDispatcher, nativeAlternativePayment = nativeAlternativePayment, @@ -98,9 +93,9 @@ internal class DynamicCheckoutActivity : BaseTransparentPortraitActivity() { } private fun cardTokenizationConfiguration(): POCardTokenizationConfiguration { - val billingAddress = configuration?.card?.billingAddress + val billingAddress = configuration.card.billingAddress return POCardTokenizationConfiguration( - cardScanner = configuration?.card?.cardScanner?.let { + cardScanner = configuration.card.cardScanner?.let { CardScannerConfiguration( scanButton = POCardTokenizationConfiguration.Button( text = it.scanButton.text, @@ -110,52 +105,52 @@ internal class DynamicCheckoutActivity : BaseTransparentPortraitActivity() { ) }, billingAddress = BillingAddressConfiguration( - defaultAddress = billingAddress?.defaultAddress, - attachDefaultsToPaymentMethod = billingAddress?.attachDefaultsToPaymentMethod ?: false + defaultAddress = billingAddress.defaultAddress, + attachDefaultsToPaymentMethod = billingAddress.attachDefaultsToPaymentMethod ), - submitButton = configuration?.submitButton?.let { + submitButton = configuration.submitButton.let { POCardTokenizationConfiguration.Button( text = it.text, icon = it.icon ) - } ?: POCardTokenizationConfiguration.Button(), - cancelButton = configuration?.cancelButton?.let { + }, + cancelButton = configuration.cancelButton?.let { POCardTokenizationConfiguration.CancelButton( text = it.text, icon = it.icon, confirmation = it.confirmation ) }, - metadata = configuration?.card?.metadata + metadata = configuration.card.metadata ) } private fun nativeAlternativePaymentConfiguration(): PONativeAlternativePaymentConfiguration { - val paymentConfirmation = configuration?.alternativePayment?.paymentConfirmation + val paymentConfirmation = configuration.alternativePayment.paymentConfirmation return PONativeAlternativePaymentConfiguration( - invoiceId = configuration?.invoiceRequest?.invoiceId ?: String(), + invoiceId = configuration.invoiceRequest.invoiceId, gatewayConfigurationId = String(), - submitButton = configuration?.submitButton?.map() ?: PONativeAlternativePaymentConfiguration.Button(), - cancelButton = configuration?.cancelButton?.map(), + submitButton = configuration.submitButton.map(), + cancelButton = configuration.cancelButton?.map(), paymentConfirmation = PaymentConfirmationConfiguration( waitsConfirmation = true, - timeoutSeconds = paymentConfirmation?.timeoutSeconds ?: DEFAULT_TIMEOUT_SECONDS, - showProgressIndicatorAfterSeconds = paymentConfirmation?.showProgressIndicatorAfterSeconds, + timeoutSeconds = paymentConfirmation.timeoutSeconds, + showProgressIndicatorAfterSeconds = paymentConfirmation.showProgressIndicatorAfterSeconds, hideGatewayDetails = true, - confirmButton = paymentConfirmation?.confirmButton?.map(), - cancelButton = paymentConfirmation?.cancelButton?.map() + confirmButton = paymentConfirmation.confirmButton?.map(), + cancelButton = paymentConfirmation.cancelButton?.map() ), - barcode = configuration?.alternativePayment?.barcode - ?: POBarcodeConfiguration(saveButton = POBarcodeConfiguration.Button()), - inlineSingleSelectValuesLimit = configuration?.alternativePayment?.inlineSingleSelectValuesLimit ?: 5, + barcode = configuration.alternativePayment.barcode, + inlineSingleSelectValuesLimit = configuration.alternativePayment.inlineSingleSelectValuesLimit, skipSuccessScreen = true ) } - private fun Button.map() = PONativeAlternativePaymentConfiguration.Button( - text = text, - icon = icon - ) + private fun Button.map() = + PONativeAlternativePaymentConfiguration.Button( + text = text, + icon = icon + ) private fun CancelButton.map() = PONativeAlternativePaymentConfiguration.CancelButton( @@ -198,7 +193,7 @@ internal class DynamicCheckoutActivity : BaseTransparentPortraitActivity() { googlePayLauncher = POGooglePayCardTokenizationLauncher.create( from = this, walletOptions = WalletOptions.Builder() - .setEnvironment(configuration?.googlePay?.environment?.value ?: WalletConstants.ENVIRONMENT_TEST) + .setEnvironment(configuration.googlePay.environment.value) .build(), callback = ::handleGooglePay ) @@ -231,7 +226,7 @@ internal class DynamicCheckoutActivity : BaseTransparentPortraitActivity() { state = viewModel.state.collectAsStateWithLifecycle().value, onEvent = remember { viewModel::onEvent }, style = DynamicCheckoutScreen.style( - custom = configuration?.style, + custom = configuration.style, isLightTheme = isLightTheme ), isLightTheme = isLightTheme @@ -243,23 +238,22 @@ internal class DynamicCheckoutActivity : BaseTransparentPortraitActivity() { private fun initConfiguration() { @Suppress("DEPRECATION") configuration = intent.getParcelableExtra(EXTRA_CONFIGURATION) - configuration?.run { - if (invoiceRequest.invoiceId.isBlank()) { - viewModel.onEvent( - Dismiss( - ProcessOutResult.Failure( - code = Generic(), - message = "Invalid configuration: 'invoiceId' is required." - ) + ?: PODynamicCheckoutConfiguration(invoiceRequest = POInvoiceRequest(invoiceId = String())) + if (configuration.invoiceRequest.invoiceId.isBlank()) { + viewModel.onEvent( + Dismiss( + ProcessOutResult.Failure( + code = Generic(), + message = "Invalid configuration: 'invoiceId' is required." ) ) - } + ) } } private fun dispatchBackPressed() { onBackPressedDispatcher.addCallback(this) { - if (configuration?.cancelOnBackPressed == true) { + if (configuration.cancelOnBackPressed) { viewModel.onEvent( Dismiss( ProcessOutResult.Failure( @@ -290,7 +284,7 @@ internal class DynamicCheckoutActivity : BaseTransparentPortraitActivity() { savedPaymentMethodsLauncher.launch(sideEffect.configuration) } is PermissionRequest -> requestPermission(sideEffect) - CardScanner -> configuration?.card?.cardScanner?.configuration?.let { + CardScanner -> configuration.card.cardScanner?.configuration?.let { cardScannerLauncher.launch(configuration = it) } CancelWebAuthorization -> cancelWebAuthorization() From f3ee04bcf1c08c441eb6ed9b6f03c7f0e98ad6a0 Mon Sep 17 00:00:00 2001 From: Vitalii Vanziak Date: Thu, 27 Mar 2025 17:55:03 +0200 Subject: [PATCH 32/32] Add OCR metadata to Example app --- example/src/main/AndroidManifest.xml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/example/src/main/AndroidManifest.xml b/example/src/main/AndroidManifest.xml index 46d6667e5..aa8485c75 100644 --- a/example/src/main/AndroidManifest.xml +++ b/example/src/main/AndroidManifest.xml @@ -26,6 +26,10 @@ + +