Skip to content

Commit

Permalink
Merge pull request #13295 from woocommerce/pos-reader-flow-animation
Browse files Browse the repository at this point in the history
[POS] Card payment animation improvements
  • Loading branch information
samiuelson authored Jan 17, 2025
2 parents cda6ca5 + bf88936 commit d5af3f2
Show file tree
Hide file tree
Showing 5 changed files with 157 additions and 117 deletions.
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.woocommerce.android.ui.woopos.home.totals

import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.AnimatedVisibilityScope
import androidx.compose.animation.fadeIn
Expand Down Expand Up @@ -52,6 +53,7 @@ import com.woocommerce.android.ui.woopos.common.composeui.component.WooPosErrorS
import com.woocommerce.android.ui.woopos.common.composeui.component.WooPosOutlinedButton
import com.woocommerce.android.ui.woopos.common.composeui.component.WooPosShimmerBox
import com.woocommerce.android.ui.woopos.common.composeui.toAdaptivePadding
import com.woocommerce.android.ui.woopos.home.totals.WooPosTotalsViewState.Totals
import com.woocommerce.android.ui.woopos.home.totals.payment.failed.WooPosPaymentFailedScreen
import com.woocommerce.android.ui.woopos.home.totals.payment.inprogress.WooPosPaymentInProgressScreen
import com.woocommerce.android.ui.woopos.home.totals.payment.success.WooPosPaymentSuccessScreen
Expand All @@ -74,8 +76,8 @@ private fun WooPosTotalsScreen(
onUIEvent: (WooPosTotalsUIEvent) -> Unit,
) {
Box(modifier = modifier) {
StateChangeAnimated(visible = state is WooPosTotalsViewState.Totals) {
if (state is WooPosTotalsViewState.Totals) {
StateChangeAnimated(visible = state is WooPosTotalsViewState.Checkout) {
if (state is WooPosTotalsViewState.Checkout) {
TotalsLoaded(
state = state,
onUIEvent = onUIEvent,
Expand Down Expand Up @@ -140,7 +142,7 @@ private fun StateChangeAnimated(

@Composable
private fun TotalsLoaded(
state: WooPosTotalsViewState.Totals,
state: WooPosTotalsViewState.Checkout,
onUIEvent: (WooPosTotalsUIEvent) -> Unit,
) {
Column(
Expand Down Expand Up @@ -176,26 +178,36 @@ private fun TotalsLoaded(
}
}
}
Column(
modifier = Modifier
.fillMaxWidth()
.padding(
horizontal = 40.dp.toAdaptivePadding(),
vertical = 16.dp.toAdaptivePadding()
)
.weight(.9f),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
) {
TotalsGrid(state = state)

Column(horizontalAlignment = Alignment.CenterHorizontally) {
Spacer(modifier = Modifier.height(16.dp.toAdaptivePadding()))
WooPosOutlinedButton(
text = stringResource(R.string.woopos_payment_take_cash_payment_label),
onClick = { onUIEvent(WooPosTotalsUIEvent.OnCashPaymentClicked) },
)
Spacer(modifier = Modifier.height(16.dp.toAdaptivePadding()))
AnimatedContent(
targetState = state.totals,
label = "totals_grid_animation",
) { state ->
when (state) {
is Totals.Hidden -> Unit
is Totals.Visible -> {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(
horizontal = 40.dp.toAdaptivePadding(),
vertical = 16.dp.toAdaptivePadding()
)
.weight(.9f),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
) {
TotalsGrid(totals = state)
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Spacer(modifier = Modifier.height(16.dp.toAdaptivePadding()))
WooPosOutlinedButton(
text = stringResource(R.string.woopos_payment_take_cash_payment_label),
onClick = { onUIEvent(WooPosTotalsUIEvent.OnCashPaymentClicked) },
)
Spacer(modifier = Modifier.height(16.dp.toAdaptivePadding()))
}
}
}
}
}
}
Expand Down Expand Up @@ -287,7 +299,7 @@ private fun ReaderDisconnected(
}

@Composable
private fun TotalsGrid(state: WooPosTotalsViewState.Totals) {
private fun TotalsGrid(totals: Totals.Visible) {
Column(
modifier = Modifier
.padding(24.dp.toAdaptivePadding())
Expand All @@ -297,14 +309,14 @@ private fun TotalsGrid(state: WooPosTotalsViewState.Totals) {
) {
TotalsGridRow(
textOne = stringResource(R.string.woopos_payment_subtotal_label),
textTwo = state.orderSubtotalText,
textTwo = totals.orderSubtotalText,
)

Spacer(modifier = Modifier.height(8.dp.toAdaptivePadding()))

TotalsGridRow(
textOne = stringResource(R.string.woopos_payment_tax_label),
textTwo = state.orderTaxText,
textTwo = totals.orderTaxText,
)

Spacer(modifier = Modifier.height(16.dp.toAdaptivePadding()))
Expand All @@ -315,7 +327,7 @@ private fun TotalsGrid(state: WooPosTotalsViewState.Totals) {

TotalsGridRow(
textOne = stringResource(R.string.woopos_payment_total_label),
textTwo = state.orderTotalText,
textTwo = totals.orderTotalText,
styleOne = MaterialTheme.typography.h4,
styleTwo = MaterialTheme.typography.h4,
fontWeightOne = FontWeight.Medium,
Expand Down Expand Up @@ -412,10 +424,12 @@ fun WooPosTotalsScreenPreview(modifier: Modifier = Modifier) {
WooPosTheme {
WooPosTotalsScreen(
modifier = modifier,
state = WooPosTotalsViewState.Totals(
orderSubtotalText = "$420.00",
orderTotalText = "$462.00",
orderTaxText = "$42.00",
state = WooPosTotalsViewState.Checkout(
totals = Totals.Visible(
orderSubtotalText = "$420.00",
orderTotalText = "$462.00",
orderTaxText = "$42.00",
),
readerStatus = WooPosTotalsViewState.ReaderStatus.ReadyForPayment(
title = "Ready for payment",
subtitle = "Tap, swipe or insert card"
Expand All @@ -433,10 +447,12 @@ fun WooPosTotalsScreenPreviewReaderNotConnected(modifier: Modifier = Modifier) {
WooPosTheme {
WooPosTotalsScreen(
modifier = modifier,
state = WooPosTotalsViewState.Totals(
orderSubtotalText = "$420.00",
orderTotalText = "$462.00",
orderTaxText = "$42.00",
state = WooPosTotalsViewState.Checkout(
totals = Totals.Visible(
orderSubtotalText = "$420.00",
orderTotalText = "$462.00",
orderTaxText = "$42.00",
),
readerStatus = WooPosTotalsViewState.ReaderStatus.Disconnected(
title = "Reader not connected",
subtitle = "To process this payment, please connect your reader.",
Expand All @@ -455,10 +471,12 @@ fun WooPosTotalsScreenPreviewWithCashPaymentAvailable() {
WooPosTheme {
WooPosTotalsScreen(
modifier = Modifier,
state = WooPosTotalsViewState.Totals(
orderSubtotalText = "$420.00",
orderTotalText = "$462.00",
orderTaxText = "$42.00",
state = WooPosTotalsViewState.Checkout(
totals = Totals.Visible(
orderSubtotalText = "$420.00",
orderTotalText = "$462.00",
orderTaxText = "$42.00",
),
readerStatus = WooPosTotalsViewState.ReaderStatus.Disconnected(
title = "Reader not connected",
subtitle = "To process this payment, please connect your reader.",
Expand All @@ -477,10 +495,12 @@ fun WooPosTotalsScreenPreviewForFreeOrders() {
WooPosTheme {
WooPosTotalsScreen(
modifier = Modifier,
state = WooPosTotalsViewState.Totals(
orderSubtotalText = "$420.00",
orderTotalText = "$462.00",
orderTaxText = "$42.00",
state = WooPosTotalsViewState.Checkout(
totals = Totals.Visible(
orderSubtotalText = "$420.00",
orderTotalText = "$462.00",
orderTaxText = "$42.00",
),
readerStatus = WooPosTotalsViewState.ReaderStatus.Disconnected(
title = "Reader not connected",
subtitle = "To process this payment, please connect your reader.",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import com.woocommerce.android.ui.woopos.home.WooPosParentToChildrenEventReceive
import com.woocommerce.android.ui.woopos.home.items.WooPosItemsViewModel
import com.woocommerce.android.ui.woopos.home.totals.WooPosTotalsViewState.PaymentFailed
import com.woocommerce.android.ui.woopos.home.totals.WooPosTotalsViewState.PaymentInProgress
import com.woocommerce.android.ui.woopos.home.totals.WooPosTotalsViewState.Totals
import com.woocommerce.android.ui.woopos.util.WooPosNetworkStatus
import com.woocommerce.android.ui.woopos.util.analytics.WooPosAnalyticsEvent
import com.woocommerce.android.ui.woopos.util.analytics.WooPosAnalyticsTracker
Expand All @@ -44,6 +45,7 @@ import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
Expand Down Expand Up @@ -119,14 +121,14 @@ class WooPosTotalsViewModel @Inject constructor(
when (status) {
is NotConnected, is Connecting -> {
val state = uiState.value
if (state !is WooPosTotalsViewState.Totals) return@collect
if (state !is WooPosTotalsViewState.Checkout) return@collect
uiState.value = state.copy(readerStatus = buildTotalsReaderNotConnectedError())
cancelPaymentAction()
}

is Connected -> {
val state = uiState.value
if (state !is WooPosTotalsViewState.Totals) return@collect
if (state !is WooPosTotalsViewState.Checkout) return@collect
uiState.value = state.copy(readerStatus = buildPreparingReaderStatusState())
if (data.orderId != EMPTY_ORDER_ID) {
collectPayment()
Expand Down Expand Up @@ -260,8 +262,8 @@ class WooPosTotalsViewModel @Inject constructor(
dataState.value.orderTotal?.compareTo(BigDecimal.ZERO) == 1
) {
val state = uiState.value
check(state is WooPosTotalsViewState.Totals)
check(uiState.value is WooPosTotalsViewState.Totals)
check(state is WooPosTotalsViewState.Checkout)
check(uiState.value is WooPosTotalsViewState.Checkout)
createCardReaderPaymentController(dataState.value.orderId)
cardReaderPaymentController?.start()
listenToPaymentState()
Expand Down Expand Up @@ -307,6 +309,14 @@ class WooPosTotalsViewModel @Inject constructor(

is CardReaderPaymentState.ProcessingPayment,
is CardReaderPaymentState.PaymentCapturing -> {
val state = uiState.value
if (state is WooPosTotalsViewState.Checkout) {
uiState.value = state.copy(totals = Totals.Hidden)
// allow the UI to show "shrinking" exit animation of totals grid before showing
// the "payment in progress" state.
@Suppress("MagicNumber")
delay(384)
}
uiState.value = buildPaymentInProgressState()
childrenToParentEventSender.sendToParent(ChildToParentEvent.PaymentInProgress)
childrenToParentEventSender.sendToParent(
Expand Down Expand Up @@ -338,7 +348,7 @@ class WooPosTotalsViewModel @Inject constructor(

private suspend fun handleCollectingPaymentState(paymentState: CardReaderPaymentState.CollectingPayment) {
val totalsState = uiState.value
if (totalsState is WooPosTotalsViewState.Totals) {
if (totalsState is WooPosTotalsViewState.Checkout) {
uiState.value = totalsState.copy(
readerStatus = WooPosTotalsViewState.ReaderStatus.ReadyForPayment(
title = resourceProvider.getString(R.string.woopos_totals_reader_ready_for_payment_title),
Expand All @@ -357,7 +367,7 @@ class WooPosTotalsViewModel @Inject constructor(

private suspend fun handleReaderLoadingPaymentState() {
val totalsState = uiState.value
if (totalsState is WooPosTotalsViewState.Totals) {
if (totalsState is WooPosTotalsViewState.Checkout) {
uiState.value = totalsState.copy(
readerStatus =
WooPosTotalsViewState.ReaderStatus.Preparing(
Expand Down Expand Up @@ -455,18 +465,20 @@ class WooPosTotalsViewModel @Inject constructor(
}
}

private suspend fun buildWooPosTotalsViewState(order: Order): WooPosTotalsViewState.Totals {
private suspend fun buildWooPosTotalsViewState(order: Order): WooPosTotalsViewState.Checkout {
val subtotalAmount = order.productsTotal
val taxAmount = order.totalTax
val totalAmount = order.total
val readerStatus = when (cardReaderFacade.readerStatus.value) {
is Connected -> buildPreparingReaderStatusState()
else -> buildTotalsReaderNotConnectedError()
}
return WooPosTotalsViewState.Totals(
orderSubtotalText = priceFormat(subtotalAmount),
orderTaxText = priceFormat(taxAmount),
orderTotalText = priceFormat(totalAmount),
return WooPosTotalsViewState.Checkout(
totals = Totals.Visible(
orderSubtotalText = priceFormat(subtotalAmount),
orderTaxText = priceFormat(taxAmount),
orderTotalText = priceFormat(totalAmount),
),
readerStatus = readerStatus,
isFreeOrder = totalAmount.compareTo(BigDecimal.ZERO) == 0
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,24 @@ import kotlinx.parcelize.Parcelize
sealed class WooPosTotalsViewState : Parcelable {
data object Loading : WooPosTotalsViewState()

data class Totals(
val orderSubtotalText: String,
val orderTaxText: String,
val orderTotalText: String,
data class Checkout(
val totals: Totals,
val readerStatus: ReaderStatus,
val isFreeOrder: Boolean,
) : WooPosTotalsViewState()

sealed class Totals : Parcelable {
@Parcelize
data object Hidden : Totals()

@Parcelize
data class Visible(
val orderSubtotalText: String,
val orderTaxText: String,
val orderTotalText: String,
) : Totals()
}

data class PaymentSuccess(val orderTotalText: String) : WooPosTotalsViewState()

sealed class ReaderStatus(
Expand Down
Loading

0 comments on commit d5af3f2

Please sign in to comment.