From b6252db0b801fc0da3cc90f37b8c01277a9f940f Mon Sep 17 00:00:00 2001 From: Vitalii Vanziak Date: Thu, 17 Oct 2024 12:24:29 +0300 Subject: [PATCH 1/9] PaymentSuccess configuration --- .../PODynamicCheckoutConfiguration.kt | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) 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 47ab6190..1bd89adb 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 @@ -21,6 +21,7 @@ data class PODynamicCheckoutConfiguration( val alternativePayment: AlternativePaymentConfiguration = AlternativePaymentConfiguration(), val submitButtonText: String? = null, val cancelButton: CancelButton? = CancelButton(), + val paymentSuccess: PaymentSuccess? = PaymentSuccess(), val style: Style? = null ) : Parcelable { @@ -113,6 +114,12 @@ data class PODynamicCheckoutConfiguration( val confirmation: POActionConfirmationConfiguration? = null ) : Parcelable + @Parcelize + data class PaymentSuccess( + val message: String? = null, + val durationSeconds: Int = 3 + ) : Parcelable + @Parcelize data class Style( val googlePayButton: POGooglePayButtonStyle? = null, @@ -134,7 +141,8 @@ data class PODynamicCheckoutConfiguration( @ColorRes val progressIndicatorColorResId: Int? = null, @ColorRes - val controlsTintColorResId: Int? = null + val controlsTintColorResId: Int? = null, + val paymentSuccess: PaymentSuccessStyle? = null ) : Parcelable @Parcelize @@ -145,4 +153,13 @@ data class PODynamicCheckoutConfiguration( @DrawableRes val descriptionIconResId: Int? = null ) : Parcelable + + @Parcelize + data class PaymentSuccessStyle( + val message: POTextStyle, + @DrawableRes + val successImageResId: Int? = null, + @ColorRes + val backgroundColorResId: Int? = null + ) : Parcelable } From c702b4be5130f6bf0e2c326250fba9c773274716 Mon Sep 17 00:00:00 2001 From: Vitalii Vanziak Date: Thu, 17 Oct 2024 13:07:20 +0300 Subject: [PATCH 2/9] Default and custom PaymentSuccessStyle --- .../checkout/screen/DynamicCheckoutScreen.kt | 35 +++++++++++++++++-- 1 file changed, 32 insertions(+), 3 deletions(-) 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 23891fc8..187ec087 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 @@ -3,6 +3,7 @@ package com.processout.sdk.ui.checkout.screen import android.view.Gravity +import androidx.annotation.DrawableRes import androidx.compose.animation.* import androidx.compose.animation.core.MutableTransitionState import androidx.compose.animation.core.tween @@ -511,7 +512,8 @@ internal object DynamicCheckoutScreen { val actionsContainer: POActionsContainer.Style, val dialog: PODialog.Style, val backgroundColor: Color, - val progressIndicatorColor: Color + val progressIndicatorColor: Color, + val paymentSuccess: PaymentSuccessStyle ) @Immutable @@ -522,6 +524,13 @@ internal object DynamicCheckoutScreen { val description: POTextWithIcon.Style ) + @Immutable + data class PaymentSuccessStyle( + val message: POText.Style, + @DrawableRes val successImageResId: Int, + val backgroundColor: Color + ) + @Composable fun style( custom: PODynamicCheckoutConfiguration.Style?, @@ -574,7 +583,8 @@ internal object DynamicCheckoutScreen { } ?: colors.surface.default, progressIndicatorColor = custom?.progressIndicatorColorResId?.let { colorResource(id = it) - } ?: colors.button.primaryBackgroundDefault + } ?: colors.button.primaryBackgroundDefault, + paymentSuccess = custom?.paymentSuccess?.custom() ?: defaultPaymentSuccess ) private val defaultRegularPayment: RegularPaymentStyle @@ -607,13 +617,32 @@ internal object DynamicCheckoutScreen { ), description = POTextWithIcon.Style( text = description, - iconResId = descriptionIconResId ?: R.drawable.po_info_icon, + iconResId = descriptionIconResId ?: defaultRegularPayment.description.iconResId, iconColorFilter = if (descriptionIconResId != null) null else ColorFilter.tint(color = description.color) ) ) } + private val defaultPaymentSuccess: PaymentSuccessStyle + @Composable get() = PaymentSuccessStyle( + message = Style( + color = colors.text.success, + textStyle = typography.body1 + ), + successImageResId = com.processout.sdk.ui.R.drawable.po_success_image, + backgroundColor = colors.surface.success + ) + + @Composable + private fun PODynamicCheckoutConfiguration.PaymentSuccessStyle.custom() = + PaymentSuccessStyle( + message = POText.custom(style = message), + successImageResId = successImageResId ?: defaultPaymentSuccess.successImageResId, + backgroundColor = backgroundColorResId?.let { colorResource(id = it) } + ?: defaultPaymentSuccess.backgroundColor + ) + @Composable fun POBrandButtonStyle?.toButtonStyle( brandColor: POColor, From 26216b8994a95f4a42e7df842ef9e45be125c5ba Mon Sep 17 00:00:00 2001 From: Vitalii Vanziak Date: Thu, 17 Oct 2024 16:48:09 +0300 Subject: [PATCH 3/9] po_dynamic_checkout_success_message --- 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 16694af4..d89efe0f 100644 --- a/sdk/src/main/res/values-ar/strings.xml +++ b/sdk/src/main/res/values-ar/strings.xml @@ -9,6 +9,7 @@ سيتم إعادة توجيهك لإتمام هذه الدفعة. لم نتمكن من إتمام دفعتك. يرجى التحقق من التفاصيل أو تجربة طريقة دفع أخرى. طريقة الدفع المطلوبة غير متاحة، يرجى تجربة طريقة دفع أخرى. + نجاح! تمت الموافقة على الدفع الدفع بواسطة %s diff --git a/sdk/src/main/res/values-fr/strings.xml b/sdk/src/main/res/values-fr/strings.xml index 568a42a3..4e298286 100644 --- a/sdk/src/main/res/values-fr/strings.xml +++ b/sdk/src/main/res/values-fr/strings.xml @@ -9,6 +9,7 @@ Vous serez redirigé pour finaliser ce paiement. Nous n\'avons pas pu finaliser votre paiement. Veuillez vérifier vos informations ou essayer un autre mode de paiement. Le mode de paiement demandé n\'est pas disponible, veuillez essayer un autre mode de paiement. + Succès !\nPaiement confirmé. Payer avec %s diff --git a/sdk/src/main/res/values-pl/strings.xml b/sdk/src/main/res/values-pl/strings.xml index ec2fea7f..20085131 100644 --- a/sdk/src/main/res/values-pl/strings.xml +++ b/sdk/src/main/res/values-pl/strings.xml @@ -9,6 +9,7 @@ Zostaniesz przekierowany, aby sfinalizować tę płatność. Nie byliśmy w stanie sfinalizować Twojej płatności. Zweryfikuj swoje dane lub wybierz inną metody płatności. Wybrana metoda płatności nie jest obsługiwana. Proszę wybierz inną metodę. + Sukces!\nPłatność przyjęta. Zapłać przy pomocy %s diff --git a/sdk/src/main/res/values-pt/strings.xml b/sdk/src/main/res/values-pt/strings.xml index 9f13237a..599a648e 100644 --- a/sdk/src/main/res/values-pt/strings.xml +++ b/sdk/src/main/res/values-pt/strings.xml @@ -9,6 +9,7 @@ Será redireccionado para finalizar este pagamento. Não foi possível concluir o seu pagamento. Por favor, verifique os detalhes ou tente outro método de pagamento. O método de pagamento escolhido não está disponível, por favor tente outro método de pagamento. + Successo!\nPagamento aprovado. Pagar com %s diff --git a/sdk/src/main/res/values/strings.xml b/sdk/src/main/res/values/strings.xml index ab719bd6..f3a2b33d 100644 --- a/sdk/src/main/res/values/strings.xml +++ b/sdk/src/main/res/values/strings.xml @@ -9,6 +9,7 @@ You will be redirected to finalise this payment. We were unable to process your payment. Please check your payment details or try another payment method. The requested payment method is not available, please try another payment method. + Success!\nPayment approved. Pay with %s From 75a551ad05b760fc5a57a7ba2bcacdc82bd1ad70 Mon Sep 17 00:00:00 2001 From: Vitalii Vanziak Date: Thu, 17 Oct 2024 17:34:45 +0300 Subject: [PATCH 4/9] Delayed success logic in interactor and VM --- .../ui/checkout/DynamicCheckoutInteractor.kt | 65 ++++++++++++------- .../DynamicCheckoutInteractorState.kt | 3 +- .../ui/checkout/DynamicCheckoutViewModel.kt | 7 +- 3 files changed, 48 insertions(+), 27 deletions(-) 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 d0a9232e..d53146ca 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 @@ -1,6 +1,9 @@ package com.processout.sdk.ui.checkout import android.app.Application +import android.os.Handler +import android.os.Looper +import androidx.core.os.postDelayed import coil.imageLoader import coil.request.CachePolicy import coil.request.ImageRequest @@ -87,6 +90,8 @@ internal class DynamicCheckoutInteractor( private val _submitEvents = Channel() val submitEvents = _submitEvents.receiveAsFlow() + private val handler = Handler(Looper.getMainLooper()) + private var latestInvoiceRequest: PODynamicCheckoutInvoiceRequest? = null init { @@ -155,7 +160,7 @@ internal class DynamicCheckoutInteractor( .onSuccess { invoice -> when (invoice.transaction?.status()) { WAITING -> setStartedState(invoice) - AUTHORIZED, COMPLETED -> _completion.update { Success } + AUTHORIZED, COMPLETED -> handleSuccess() else -> _completion.update { Failure( ProcessOutResult.Failure( @@ -594,6 +599,29 @@ internal class DynamicCheckoutInteractor( } } + fun handleGooglePay(result: ProcessOutResult) { + result.onSuccess { response -> + authorizeInvoice(source = response.card.id) + }.onFailure { failure -> + invalidateInvoice( + reason = PODynamicCheckoutInvoiceInvalidationReason.Failure(failure) + ) + } + } + + fun handleAlternativePayment(result: ProcessOutResult) { + result.onSuccess { response -> + authorizeInvoice( + source = response.gatewayToken, + allowFallbackToSale = true + ) + }.onFailure { failure -> + invalidateInvoice( + reason = PODynamicCheckoutInvoiceInvalidationReason.Failure(failure) + ) + } + } + private fun cancel() { _completion.update { Failure( @@ -735,7 +763,7 @@ internal class DynamicCheckoutInteractor( cardTokenizationEventDispatcher.complete(result) } else { result.onSuccess { - _completion.update { Success } + handleSuccess() }.onFailure { failure -> invalidateInvoice( reason = PODynamicCheckoutInvoiceInvalidationReason.Failure(failure) @@ -750,7 +778,7 @@ internal class DynamicCheckoutInteractor( interactorScope.launch { cardTokenization.completion.collect { completion -> when (completion) { - is CardTokenizationCompletion.Success -> _completion.update { Success } + is CardTokenizationCompletion.Success -> handleSuccess() is CardTokenizationCompletion.Failure -> invalidateInvoice( reason = PODynamicCheckoutInvoiceInvalidationReason.Failure(completion.failure) ) @@ -761,7 +789,7 @@ internal class DynamicCheckoutInteractor( interactorScope.launch { nativeAlternativePayment.completion.collect { completion -> when (completion) { - NativeAlternativePaymentCompletion.Success -> _completion.update { Success } + NativeAlternativePaymentCompletion.Success -> handleSuccess() is NativeAlternativePaymentCompletion.Failure -> invalidateInvoice( reason = PODynamicCheckoutInvoiceInvalidationReason.Failure(completion.failure) ) @@ -771,30 +799,17 @@ internal class DynamicCheckoutInteractor( } } - fun handleGooglePay(result: ProcessOutResult) { - result.onSuccess { response -> - authorizeInvoice(source = response.card.id) - }.onFailure { failure -> - invalidateInvoice( - reason = PODynamicCheckoutInvoiceInvalidationReason.Failure(failure) - ) - } - } - - fun handleAlternativePayment(result: ProcessOutResult) { - result.onSuccess { response -> - authorizeInvoice( - source = response.gatewayToken, - allowFallbackToSale = true - ) - }.onFailure { failure -> - invalidateInvoice( - reason = PODynamicCheckoutInvoiceInvalidationReason.Failure(failure) - ) - } + private fun handleSuccess() { + configuration.paymentSuccess?.let { paymentSuccess -> + _state.update { it.copy(delayedSuccess = true) } + handler.postDelayed(delayInMillis = paymentSuccess.durationSeconds * 1000L) { + _completion.update { Success } + } + } ?: _completion.update { Success } } fun onCleared() { threeDSService.close() + handler.removeCallbacksAndMessages(null) } } 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 f075e269..50c37c62 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 @@ -14,7 +14,8 @@ internal data class DynamicCheckoutInteractorState( val selectedPaymentMethodId: String? = null, val processingPaymentMethodId: String? = null, val pendingSubmitPaymentMethodId: String? = null, - val errorMessage: String? = null + val errorMessage: String? = null, + val delayedSuccess: Boolean = false ) { sealed interface PaymentMethod { diff --git a/ui/src/main/kotlin/com/processout/sdk/ui/checkout/DynamicCheckoutViewModel.kt b/ui/src/main/kotlin/com/processout/sdk/ui/checkout/DynamicCheckoutViewModel.kt index aa0da8a5..7ec506b7 100644 --- a/ui/src/main/kotlin/com/processout/sdk/ui/checkout/DynamicCheckoutViewModel.kt +++ b/ui/src/main/kotlin/com/processout/sdk/ui/checkout/DynamicCheckoutViewModel.kt @@ -108,7 +108,12 @@ internal class DynamicCheckoutViewModel private constructor( nativeAlternativePaymentState: NativeAlternativePaymentViewModelState ): DynamicCheckoutViewModelState { val cancelAction = cancelAction(interactorState, cardTokenizationState, nativeAlternativePaymentState) - return if (interactorState.loading) { + return if (interactorState.delayedSuccess) { + Success( + message = configuration.paymentSuccess?.message + ?: app.getString(R.string.po_dynamic_checkout_success_message) + ) + } else if (interactorState.loading) { Starting(cancelAction = cancelAction) } else { Started( From f37e63ea805cae62da48320d2589dba6ccc0bd9f Mon Sep 17 00:00:00 2001 From: Vitalii Vanziak Date: Thu, 17 Oct 2024 18:15:13 +0300 Subject: [PATCH 5/9] Updated VM state --- .../sdk/ui/checkout/DynamicCheckoutViewModel.kt | 13 ++++++------- .../ui/checkout/DynamicCheckoutViewModelState.kt | 8 ++------ 2 files changed, 8 insertions(+), 13 deletions(-) diff --git a/ui/src/main/kotlin/com/processout/sdk/ui/checkout/DynamicCheckoutViewModel.kt b/ui/src/main/kotlin/com/processout/sdk/ui/checkout/DynamicCheckoutViewModel.kt index 7ec506b7..43153259 100644 --- a/ui/src/main/kotlin/com/processout/sdk/ui/checkout/DynamicCheckoutViewModel.kt +++ b/ui/src/main/kotlin/com/processout/sdk/ui/checkout/DynamicCheckoutViewModel.kt @@ -108,19 +108,18 @@ internal class DynamicCheckoutViewModel private constructor( nativeAlternativePaymentState: NativeAlternativePaymentViewModelState ): DynamicCheckoutViewModelState { val cancelAction = cancelAction(interactorState, cardTokenizationState, nativeAlternativePaymentState) - return if (interactorState.delayedSuccess) { - Success( - message = configuration.paymentSuccess?.message - ?: app.getString(R.string.po_dynamic_checkout_success_message) - ) - } else if (interactorState.loading) { + return if (interactorState.loading) { Starting(cancelAction = cancelAction) } else { Started( expressPayments = expressPayments(interactorState), regularPayments = regularPayments(interactorState, cardTokenizationState, nativeAlternativePaymentState), cancelAction = cancelAction, - errorMessage = interactorState.errorMessage + errorMessage = interactorState.errorMessage, + successMessage = if (interactorState.delayedSuccess) { + configuration.paymentSuccess?.message + ?: app.getString(R.string.po_dynamic_checkout_success_message) + } else null ) } } diff --git a/ui/src/main/kotlin/com/processout/sdk/ui/checkout/DynamicCheckoutViewModelState.kt b/ui/src/main/kotlin/com/processout/sdk/ui/checkout/DynamicCheckoutViewModelState.kt index 853acc04..8fda4502 100644 --- a/ui/src/main/kotlin/com/processout/sdk/ui/checkout/DynamicCheckoutViewModelState.kt +++ b/ui/src/main/kotlin/com/processout/sdk/ui/checkout/DynamicCheckoutViewModelState.kt @@ -23,12 +23,8 @@ internal sealed interface DynamicCheckoutViewModelState { val expressPayments: POImmutableList, val regularPayments: POImmutableList, val cancelAction: POActionState?, - val errorMessage: String? = null - ) : DynamicCheckoutViewModelState - - @Immutable - data class Success( - val message: String + val errorMessage: String? = null, + val successMessage: String? = null ) : DynamicCheckoutViewModelState //endregion From 87f58ff098088520ef57a3c66178cd5cef5ac569 Mon Sep 17 00:00:00 2001 From: Vitalii Vanziak Date: Thu, 17 Oct 2024 18:21:54 +0300 Subject: [PATCH 6/9] Fix UI --- .../sdk/ui/checkout/screen/DynamicCheckoutScreen.kt | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) 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 187ec087..ddea2c4d 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 @@ -107,7 +107,6 @@ internal fun DynamicCheckoutScreen( style = style, isLightTheme = isLightTheme ) - else -> {} } } } @@ -452,11 +451,9 @@ private fun Actions( containerStyle: POActionsContainer.Style, dialogStyle: PODialog.Style ) { - var cancelAction: POActionState? = null - when (state) { - is Starting -> cancelAction = state.cancelAction - is Started -> cancelAction = state.cancelAction - else -> {} + val cancelAction: POActionState? = when (state) { + is Starting -> state.cancelAction + is Started -> state.cancelAction } if (cancelAction != null) { POActionsContainer( From 42fb5aeba788dd18184d85ea31e2ee994a52e471 Mon Sep 17 00:00:00 2001 From: Vitalii Vanziak Date: Thu, 17 Oct 2024 19:55:44 +0300 Subject: [PATCH 7/9] Success screen with crossfade animation --- .../ui/checkout/DynamicCheckoutViewModel.kt | 2 +- .../checkout/screen/DynamicCheckoutScreen.kt | 123 +++++++++++++++--- 2 files changed, 103 insertions(+), 22 deletions(-) diff --git a/ui/src/main/kotlin/com/processout/sdk/ui/checkout/DynamicCheckoutViewModel.kt b/ui/src/main/kotlin/com/processout/sdk/ui/checkout/DynamicCheckoutViewModel.kt index 43153259..5c09f19b 100644 --- a/ui/src/main/kotlin/com/processout/sdk/ui/checkout/DynamicCheckoutViewModel.kt +++ b/ui/src/main/kotlin/com/processout/sdk/ui/checkout/DynamicCheckoutViewModel.kt @@ -114,7 +114,7 @@ internal class DynamicCheckoutViewModel private constructor( Started( expressPayments = expressPayments(interactorState), regularPayments = regularPayments(interactorState, cardTokenizationState, nativeAlternativePaymentState), - cancelAction = cancelAction, + cancelAction = if (interactorState.delayedSuccess) null else cancelAction, errorMessage = interactorState.errorMessage, successMessage = if (interactorState.delayedSuccess) { configuration.paymentSuccess?.message 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 ddea2c4d..07989f8b 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 @@ -1,10 +1,11 @@ -@file:Suppress("MayBeConstant") +@file:Suppress("MayBeConstant", "MemberVisibilityCanBePrivate", "AnimateAsStateLabel", "CrossfadeLabel") package com.processout.sdk.ui.checkout.screen import android.view.Gravity import androidx.annotation.DrawableRes import androidx.compose.animation.* +import androidx.compose.animation.core.LinearEasing import androidx.compose.animation.core.MutableTransitionState import androidx.compose.animation.core.tween import androidx.compose.foundation.* @@ -20,7 +21,10 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.res.colorResource +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import coil.compose.AsyncImage @@ -34,10 +38,15 @@ import com.processout.sdk.ui.checkout.DynamicCheckoutViewModelState.* import com.processout.sdk.ui.checkout.DynamicCheckoutViewModelState.RegularPayment.Content.Card import com.processout.sdk.ui.checkout.DynamicCheckoutViewModelState.RegularPayment.Content.NativeAlternativePayment import com.processout.sdk.ui.checkout.PODynamicCheckoutConfiguration +import com.processout.sdk.ui.checkout.screen.DynamicCheckoutScreen.CrossfadeAnimationDurationMillis import com.processout.sdk.ui.checkout.screen.DynamicCheckoutScreen.LongAnimationDurationMillis import com.processout.sdk.ui.checkout.screen.DynamicCheckoutScreen.PaymentLogoSize +import com.processout.sdk.ui.checkout.screen.DynamicCheckoutScreen.PaymentSuccessStyle import com.processout.sdk.ui.checkout.screen.DynamicCheckoutScreen.RowComponentSpacing import com.processout.sdk.ui.checkout.screen.DynamicCheckoutScreen.ShortAnimationDurationMillis +import com.processout.sdk.ui.checkout.screen.DynamicCheckoutScreen.SuccessImageHeight +import com.processout.sdk.ui.checkout.screen.DynamicCheckoutScreen.SuccessImageWidth +import com.processout.sdk.ui.checkout.screen.DynamicCheckoutScreen.animatedBackgroundColor import com.processout.sdk.ui.checkout.screen.DynamicCheckoutScreen.toButtonStyle import com.processout.sdk.ui.core.R import com.processout.sdk.ui.core.component.* @@ -79,7 +88,11 @@ internal fun DynamicCheckoutScreen( modifier = Modifier .consumeWindowInsets(WindowInsets.safeDrawing) .clip(shape = shapes.topRoundedCornersLarge), - containerColor = style.backgroundColor, + containerColor = animatedBackgroundColor( + state = state, + normalColor = style.backgroundColor, + successColor = style.paymentSuccess.backgroundColor + ), bottomBar = { DynamicFooter { Actions( @@ -101,12 +114,27 @@ internal fun DynamicCheckoutScreen( ) { when (state) { is Starting -> Loading(progressIndicatorColor = style.progressIndicatorColor) - is Started -> Content( - state = state, - onEvent = onEvent, - style = style, - isLightTheme = isLightTheme - ) + is Started -> Crossfade( + targetState = state.successMessage != null, + animationSpec = tween( + durationMillis = CrossfadeAnimationDurationMillis, + easing = LinearEasing + ) + ) { isSuccess -> + if (isSuccess) { + Success( + message = state.successMessage ?: String(), + style = style.paymentSuccess + ) + } else { + Content( + state = state, + onEvent = onEvent, + style = style, + isLightTheme = isLightTheme + ) + } + } } } } @@ -451,24 +479,56 @@ private fun Actions( containerStyle: POActionsContainer.Style, dialogStyle: PODialog.Style ) { + val actions = mutableListOf() val cancelAction: POActionState? = when (state) { is Starting -> state.cancelAction is Started -> state.cancelAction } - if (cancelAction != null) { - POActionsContainer( - actions = POImmutableList(listOf(cancelAction)), - onClick = { - onEvent( - Action( - actionId = it, - paymentMethodId = null - ) + cancelAction?.let { actions.add(it) } + POActionsContainer( + actions = POImmutableList(actions), + onClick = { + onEvent( + Action( + actionId = it, + paymentMethodId = null ) - }, - onConfirmationRequested = { onEvent(ActionConfirmationRequested(id = it)) }, - containerStyle = containerStyle, - dialogStyle = dialogStyle + ) + }, + onConfirmationRequested = { onEvent(ActionConfirmationRequested(id = it)) }, + containerStyle = containerStyle, + dialogStyle = dialogStyle, + animationDurationMillis = CrossfadeAnimationDurationMillis + ) +} + +@Composable +private fun Success( + message: String, + style: PaymentSuccessStyle +) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(top = 100.dp), + verticalArrangement = Arrangement.spacedBy(spacing.extraLarge), + horizontalAlignment = Alignment.CenterHorizontally + ) { + POText( + text = message, + color = style.message.color, + style = style.message.textStyle, + textAlign = TextAlign.Center + ) + Image( + painter = painterResource(id = style.successImageResId), + contentDescription = null, + modifier = Modifier.requiredSize( + width = SuccessImageWidth, + height = SuccessImageHeight + ), + alignment = Alignment.Center, + contentScale = ContentScale.Fit ) } } @@ -704,16 +764,37 @@ internal object DynamicCheckoutScreen { paddingVertical = paddingVerticalDp.dp ) + @Composable + fun animatedBackgroundColor( + state: DynamicCheckoutViewModelState, + normalColor: Color, + successColor: Color + ): Color = animateColorAsState( + targetValue = when (state) { + is Started -> if (state.successMessage != null) successColor else normalColor + else -> normalColor + }, + animationSpec = tween( + durationMillis = CrossfadeAnimationDurationMillis, + easing = LinearEasing + ) + ).value + val ShortAnimationDurationMillis = 300 val LongAnimationDurationMillis = 600 + val CrossfadeAnimationDurationMillis = 400 val RowComponentSpacing = 10.dp val PaymentLogoSize = 24.dp val CaptureLogoHeight = 34.dp + val CaptureImageWidth = 110.dp val CaptureImageHeight = 140.dp + val SuccessImageWidth = 220.dp + val SuccessImageHeight = 280.dp + private val ShortMessageMaxLength = 150 fun isMessageShort(text: String) = text.length <= ShortMessageMaxLength From 8908eb9ac24d090b513218848da4e7e37f2d17b0 Mon Sep 17 00:00:00 2001 From: Vitalii Vanziak Date: Fri, 18 Oct 2024 11:28:46 +0300 Subject: [PATCH 8/9] Fix padding --- .../processout/sdk/ui/checkout/screen/DynamicCheckoutScreen.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 07989f8b..42b7d962 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 @@ -510,7 +510,7 @@ private fun Success( Column( modifier = Modifier .fillMaxWidth() - .padding(top = 100.dp), + .padding(top = spacing.extraLarge * 2), verticalArrangement = Arrangement.spacedBy(spacing.extraLarge), horizontalAlignment = Alignment.CenterHorizontally ) { From 4696b22939831d1aa12973cbed9466b53fc1177f Mon Sep 17 00:00:00 2001 From: Vitalii Vanziak Date: Fri, 18 Oct 2024 12:27:59 +0300 Subject: [PATCH 9/9] Complete with Success if dismissed while on the Success screen --- .../sdk/ui/checkout/DynamicCheckoutInteractor.kt | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) 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 d53146ca..e8e943ae 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 @@ -409,8 +409,12 @@ internal class DynamicCheckoutInteractor( // TODO } is Dismiss -> { - POLogger.warn("Dismissed: %s", event.failure) - _completion.update { Failure(event.failure) } + if (_state.value.delayedSuccess) { + _completion.update { Success } + } else { + POLogger.warn("Dismissed: %s", event.failure) + _completion.update { Failure(event.failure) } + } } } }