From ae1cda8d7d484bec1f88e64e2ed230061a24c121 Mon Sep 17 00:00:00 2001 From: Alejo Date: Tue, 10 Dec 2024 22:32:21 -0300 Subject: [PATCH 1/7] add mock use case --- .../wooshippinglabels/GetShippingRates.kt | 74 +++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/wooshippinglabels/GetShippingRates.kt diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/wooshippinglabels/GetShippingRates.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/wooshippinglabels/GetShippingRates.kt new file mode 100644 index 00000000000..c53389cb40b --- /dev/null +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/wooshippinglabels/GetShippingRates.kt @@ -0,0 +1,74 @@ +package com.woocommerce.android.ui.orders.wooshippinglabels + +import com.woocommerce.android.R +import com.woocommerce.android.ui.orders.wooshippinglabels.packages.datasource.PackageDAO +import kotlinx.coroutines.delay +import javax.inject.Inject +import kotlin.random.Random + +class GetShippingRates @Inject constructor() { + private val cheapestComparator = Comparator { r1, r2 -> + r1.rate.substring(1).toBigDecimal().compareTo(r2.rate.substring(1).toBigDecimal()) + } + + private val fastestComparator = Comparator { r1, r2 -> + r1.deliveryDays.compareTo(r2.deliveryDays) + } + + suspend operator fun invoke( + selectedPackage: PackageDAO, + sortOrder: ShippingSortOption + ): Result>> { + delay(1_000) + val comparator = when (sortOrder) { + ShippingSortOption.CHEAPEST -> { + cheapestComparator + } + + ShippingSortOption.FASTEST -> { + fastestComparator + } + } + val carriers = if (selectedPackage.isLetter) { + listOf( + Carrier( + id = "dhl", + name = "DHL Express", + logoRes = R.drawable.dhl_logo + ), + Carrier( + id = "usps", + name = "USPS", + logoRes = R.drawable.usps_logo + ) + ) + } else { + listOf( + Carrier( + id = "dhl", + name = "DHL Express", + logoRes = R.drawable.dhl_logo + ), + Carrier( + id = "usps", + name = "USPS", + logoRes = R.drawable.usps_logo + ), + Carrier( + id = "ups", + name = "UPS", + logoRes = R.drawable.ups_logo + ) + ) + } + + return Result.success( + carriers.associateWith { + generateRates( + it.name, + Random(0).nextInt(from = 3, until = 10) + ).sortedWith(comparator) + } + ) + } +} From ee6b4a0096af8ceb16f9fa9cc53078b15ceca30c Mon Sep 17 00:00:00 2001 From: Alejo Date: Tue, 10 Dec 2024 22:35:05 -0300 Subject: [PATCH 2/7] refactor shipping rate section --- .../wooshippinglabels/ShippingRatesSection.kt | 72 +++++++++++++------ 1 file changed, 50 insertions(+), 22 deletions(-) diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/wooshippinglabels/ShippingRatesSection.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/wooshippinglabels/ShippingRatesSection.kt index 6a0d692d79a..3ef45ee6f70 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/wooshippinglabels/ShippingRatesSection.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/wooshippinglabels/ShippingRatesSection.kt @@ -18,6 +18,7 @@ import androidx.compose.foundation.layout.sizeIn import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.CircularProgressIndicator import androidx.compose.material.Colors import androidx.compose.material.DropdownMenu import androidx.compose.material.DropdownMenuItem @@ -66,23 +67,24 @@ val Colors.selectedRateBackgroundColor: Color get() = if (isLight) Color(0xFFF2E @Composable internal fun ShippingRatesCard( - selected: ShippingRate?, - onSelectedChange: (ShippingRate) -> Unit = {}, - shippingRates: Map>, + selectedRate: ShippingRateUI?, + onSelectedChange: (ShippingRateUI) -> Unit, + shippingRates: Map>, signatureRequired: SignatureRequired?, onSelectedSignatureChange: (SignatureRequired?) -> Unit, signatureRequiredOptions: List, + selectedSortOption: ShippingSortOption, + onSelectedRateSortOrderChanged: (ShippingSortOption) -> Unit, modifier: Modifier = Modifier ) { - var selectedSortOption by remember { mutableStateOf(ShippingSortOption.CHEAPEST) } Column(modifier = modifier) { ShippingRatesHeader( selectedSortOption = selectedSortOption, - onSortOptionSelected = { selectedSortOption = it }, + onSortOptionSelected = onSelectedRateSortOrderChanged, modifier = Modifier.padding(start = dimensionResource(R.dimen.major_100)) ) ShippingRates( - selected = selected, + selectedRate = selectedRate, onSelectedChange = onSelectedChange, shippingRates = shippingRates, signatureRequired = signatureRequired, @@ -100,15 +102,41 @@ private fun ShippingRatesCardPreview() { val selected = rates.values.first().first() WooThemeWithBackground { ShippingRatesCard( - selected = selected, + selectedRate = selected, shippingRates = generateShippingRates(), signatureRequired = null, + onSelectedChange = {}, onSelectedSignatureChange = {}, signatureRequiredOptions = listOf( SignatureRequired("Signature Required", "$10.00"), SignatureRequired("Adult Signature Required", "$15.00") - ) + ), + selectedSortOption = ShippingSortOption.CHEAPEST, + onSelectedRateSortOrderChanged = {} + ) + } +} + +@Composable +internal fun ShippingRatesLoading( + selectedSortOption: ShippingSortOption, + onSelectedRateSortOrderChanged: (ShippingSortOption) -> Unit, + modifier: Modifier = Modifier, +) { + Column(modifier = modifier) { + ShippingRatesHeader( + selectedSortOption = selectedSortOption, + onSortOptionSelected = onSelectedRateSortOrderChanged, + modifier = Modifier.padding(start = dimensionResource(R.dimen.major_100)) ) + Box( + modifier = Modifier + .fillMaxWidth() + .sizeIn(minHeight = 300.dp), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator() + } } } @@ -200,9 +228,9 @@ private fun SortingDropdownMenu( @OptIn(ExperimentalFoundationApi::class) @Composable fun ShippingRates( - selected: ShippingRate?, - onSelectedChange: (ShippingRate) -> Unit = {}, - shippingRates: Map>, + selectedRate: ShippingRateUI?, + onSelectedChange: (ShippingRateUI) -> Unit, + shippingRates: Map>, signatureRequired: SignatureRequired?, onSelectedSignatureChange: (SignatureRequired?) -> Unit, signatureRequiredOptions: List, @@ -259,7 +287,7 @@ fun ShippingRates( ShippingRateItem( carrier = carrier, shippingRate = rate, - isSelected = selected == rate, + isSelected = selectedRate == rate, signatureRequired = signatureRequired, onSelectedSignatureChange = onSelectedSignatureChange, signatureRequiredOptions = signatureRequiredOptions, @@ -288,7 +316,7 @@ private fun CarrierLogo( @Composable private fun ShippingRateItem( carrier: Carrier, - shippingRate: ShippingRate, + shippingRate: ShippingRateUI, isSelected: Boolean, signatureRequired: SignatureRequired?, onSelectedSignatureChange: (SignatureRequired?) -> Unit, @@ -363,7 +391,7 @@ private fun ShippingRateItem( private fun getShippingRateFormattedDescription( context: Context, - shippingRate: ShippingRate + shippingRate: ShippingRateUI ): AnnotatedString { return buildAnnotatedString { withStyle(style = SpanStyle(fontWeight = FontWeight.Bold)) { @@ -383,7 +411,7 @@ private fun getShippingRateFormattedDescription( @Composable private fun ShippingRateItemExpandedDescription( - shippingRate: ShippingRate, + shippingRate: ShippingRateUI, signatureRequired: SignatureRequired?, onSelectedSignatureChange: (SignatureRequired?) -> Unit, signatureRequiredOptions: List, @@ -455,7 +483,7 @@ private fun SelectSignatureRequired( } } -fun ShippingRate.getEstimatedDays(context: Context): String { +fun ShippingRateUI.getEstimatedDays(context: Context): String { return StringUtils.getQuantityString( context = context, quantity = deliveryDays, @@ -464,7 +492,7 @@ fun ShippingRate.getEstimatedDays(context: Context): String { ) } -fun ShippingRate.getIncludedOptions(context: Context): List { +fun ShippingRateUI.getIncludedOptions(context: Context): List { val options = mutableListOf() if (tracking) { val tracking = context.getString( @@ -499,7 +527,7 @@ data class Carrier( val logoRes: Int? = null, ) -data class ShippingRate( +data class ShippingRateUI( val name: String, val rate: String, val currency: String, @@ -514,7 +542,7 @@ data class SignatureRequired( val amount: String, ) -fun generateShippingRates(): Map> { +fun generateShippingRates(): Map> { val carriers = listOf( Carrier( id = "dhl", @@ -551,11 +579,11 @@ fun generateShippingRates(): Map> { } } -fun generateRates(carrier: String, number: Int): List { +fun generateRates(carrier: String, number: Int): List { return List(number) { - ShippingRate( + ShippingRateUI( name = "$carrier - Ground Advantage Express", - rate = "$${(it + 1) * 2}.00", + rate = "$${(it + 100) / (it + 1)}.00", currency = "USD", deliveryDays = it, insurance = "$100.00", From 437413078721ffcf7167ee82128a35e3a9bde0a9 Mon Sep 17 00:00:00 2001 From: Alejo Date: Tue, 10 Dec 2024 22:35:42 -0300 Subject: [PATCH 3/7] UI for shipping rate states --- .../WooShippingLabelCreationScreen.kt | 120 ++++++++++++++---- 1 file changed, 93 insertions(+), 27 deletions(-) diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/wooshippinglabels/WooShippingLabelCreationScreen.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/wooshippinglabels/WooShippingLabelCreationScreen.kt index 37610eabe8b..379a59a09d1 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/wooshippinglabels/WooShippingLabelCreationScreen.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/wooshippinglabels/WooShippingLabelCreationScreen.kt @@ -4,19 +4,20 @@ import android.content.res.Configuration import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.sizeIn import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll import androidx.compose.material.BottomSheetScaffold import androidx.compose.material.BottomSheetScaffoldState import androidx.compose.material.CircularProgressIndicator -import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.Icon import androidx.compose.material.IconButton import androidx.compose.material.MaterialTheme @@ -65,27 +66,34 @@ fun WooShippingLabelCreationScreen(viewModel: WooShippingLabelCreationViewModel) shippableItems = viewState.shippableItems, shippingLines = viewState.shippingLines, shippingAddresses = viewState.shippingAddresses, + shippingRatesState = viewState.shippingRates, onShippingFromAddressChange = viewModel::onShippingFromAddressChange, - onShippingToAddressChange = viewModel::onShippingToAddressChange + onShippingToAddressChange = viewModel::onShippingToAddressChange, + onSelectedRateSortOrderChanged = viewModel::onSelectedRateSortOrderChanged, + onRefreshShippingRates = viewModel::onRefreshShippingRates ) } WooShippingLabelCreationViewModel.WooShippingViewState.Error -> { - TODO() + Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { + Text("Error") + } } } } -@OptIn(ExperimentalMaterialApi::class) @Composable fun WooShippingLabelCreationScreen( shippableItems: ShippableItemsUI, shippingLines: List, + shippingRatesState: WooShippingLabelCreationViewModel.ShippingRatesState, shippingAddresses: WooShippingAddresses, onShippingFromAddressChange: (OriginShippingAddress) -> Unit, onShippingToAddressChange: (Address) -> Unit, onSelectPackageClick: () -> Unit, onPurchaseShippingLabel: () -> Unit, + onSelectedRateSortOrderChanged: (ShippingSortOption) -> Unit, + onRefreshShippingRates: () -> Unit, modifier: Modifier = Modifier ) { val scaffoldState = rememberBottomSheetScaffoldState() @@ -97,16 +105,30 @@ fun WooShippingLabelCreationScreen( scaffoldState = scaffoldState, shippingLines = shippingLines, shippingAddresses = shippingAddresses, + shippingRatesState = shippingRatesState, onShippingFromAddressChange = onShippingFromAddressChange, - onShippingToAddressChange = onShippingToAddressChange + onShippingToAddressChange = onShippingToAddressChange, + onSelectedRateSortOrderChanged = onSelectedRateSortOrderChanged, + onRefreshShippingRates = onRefreshShippingRates ) val isDarkTheme = isSystemInDarkTheme() val isCollapsed = scaffoldState.bottomSheetState.isCollapsed val elevation = when { - isDarkTheme && isCollapsed -> { 7.dp } - !isDarkTheme && isCollapsed -> { 0.dp } - isDarkTheme && !isCollapsed -> { 16.dp } - else -> { 8.dp } + isDarkTheme && isCollapsed -> { + 7.dp + } + + !isDarkTheme && isCollapsed -> { + 0.dp + } + + isDarkTheme && !isCollapsed -> { + 16.dp + } + + else -> { + 8.dp + } } Box( modifier = Modifier @@ -129,15 +151,17 @@ fun WooShippingLabelCreationScreen( } } -@OptIn(ExperimentalMaterialApi::class) @Composable private fun LabelCreationScreenWithBottomSheet( shippableItems: ShippableItemsUI, shippingLines: List, + shippingRatesState: WooShippingLabelCreationViewModel.ShippingRatesState, onSelectPackageClick: () -> Unit, shippingAddresses: WooShippingAddresses, onShippingFromAddressChange: (OriginShippingAddress) -> Unit, onShippingToAddressChange: (Address) -> Unit, + onSelectedRateSortOrderChanged: (ShippingSortOption) -> Unit, + onRefreshShippingRates: () -> Unit, scaffoldState: BottomSheetScaffoldState, modifier: Modifier = Modifier ) { @@ -198,28 +222,67 @@ private fun LabelCreationScreenWithBottomSheet( modifier = Modifier.padding(16.dp), onSelectPackageClick = onSelectPackageClick ) - val selected = remember { mutableStateOf(null) } - val signatureRequired = remember { mutableStateOf(null) } - ShippingRatesCard( - selected = selected.value, - onSelectedChange = { - selected.value = it - signatureRequired.value = null - }, - shippingRates = generateShippingRates(), - signatureRequired = signatureRequired.value, - onSelectedSignatureChange = { signatureRequired.value = it }, - signatureRequiredOptions = listOf( - SignatureRequired("Signature Required", "$10.00"), - SignatureRequired("Adult Signature Required", "$15.00") - ), - modifier = Modifier.fillMaxWidth() + WooShippingShippingRatesSection( + shippingRatesState = shippingRatesState, + onSelectedRateSortOrderChanged = onSelectedRateSortOrderChanged, + onRefreshShippingRates = onRefreshShippingRates ) } } } } +@Composable +private fun WooShippingShippingRatesSection( + shippingRatesState: WooShippingLabelCreationViewModel.ShippingRatesState, + onSelectedRateSortOrderChanged: (ShippingSortOption) -> Unit, + onRefreshShippingRates: () -> Unit, +) { + when (shippingRatesState) { + is WooShippingLabelCreationViewModel.ShippingRatesState.DataState -> { + val signatureRequired = remember { mutableStateOf(null) } + ShippingRatesCard( + selectedRate = null, + onSelectedChange = {}, + shippingRates = shippingRatesState.shippingRates, + signatureRequired = signatureRequired.value, + onSelectedSignatureChange = { signatureRequired.value = it }, + signatureRequiredOptions = listOf( + SignatureRequired("Signature Required", "$10.00"), + SignatureRequired("Adult Signature Required", "$15.00") + ), + selectedSortOption = shippingRatesState.selectedRatesSortOrder, + onSelectedRateSortOrderChanged = onSelectedRateSortOrderChanged, + modifier = Modifier.fillMaxWidth() + ) + } + + WooShippingLabelCreationViewModel.ShippingRatesState.Error -> { + Column( + modifier = Modifier + .fillMaxWidth() + .sizeIn(minHeight = 300.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Text(text = "Error") + WCColoredButton(onClick = { onRefreshShippingRates() }) { + Text(text = "Retry") + } + } + } + + is WooShippingLabelCreationViewModel.ShippingRatesState.Loading -> { + ShippingRatesLoading( + selectedSortOption = shippingRatesState.selectedRatesSortOrder, + onSelectedRateSortOrderChanged = onSelectedRateSortOrderChanged + ) + } + + WooShippingLabelCreationViewModel.ShippingRatesState.NoAvailable -> {} + } +} + @Preview(name = "dark", uiMode = Configuration.UI_MODE_NIGHT_YES, device = Devices.PIXEL) @Preview(name = "light", uiMode = Configuration.UI_MODE_NIGHT_NO, device = Devices.PIXEL) @Composable @@ -240,8 +303,11 @@ private fun WooShippingLabelCreationScreenPreview() { shipTo = getShipTo(), originAddresses = listOf(getShipFrom()) ), + shippingRatesState = WooShippingLabelCreationViewModel.ShippingRatesState.NoAvailable, onShippingFromAddressChange = {}, - onShippingToAddressChange = {} + onShippingToAddressChange = {}, + onRefreshShippingRates = {}, + onSelectedRateSortOrderChanged = {} ) } } From 8a3c535608b16d36a688697942a6d614111fbc23 Mon Sep 17 00:00:00 2001 From: Alejo Date: Tue, 10 Dec 2024 22:36:33 -0300 Subject: [PATCH 4/7] connect UI with shipping rates from use case --- .../WooShippingLabelCreationViewModel.kt | 141 ++++++++++++++---- 1 file changed, 108 insertions(+), 33 deletions(-) diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/wooshippinglabels/WooShippingLabelCreationViewModel.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/wooshippinglabels/WooShippingLabelCreationViewModel.kt index d7f8eccce6c..79b85a93908 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/wooshippinglabels/WooShippingLabelCreationViewModel.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/wooshippinglabels/WooShippingLabelCreationViewModel.kt @@ -9,14 +9,20 @@ import com.woocommerce.android.ui.orders.details.OrderDetailRepository import com.woocommerce.android.ui.orders.wooshippinglabels.models.OriginShippingAddress import com.woocommerce.android.ui.orders.wooshippinglabels.models.ShippableItemModel import com.woocommerce.android.ui.orders.wooshippinglabels.models.StoreOptionsModel +import com.woocommerce.android.ui.orders.wooshippinglabels.packages.datasource.PackageDAO import com.woocommerce.android.util.CurrencyFormatter import com.woocommerce.android.viewmodel.MultiLiveEvent.Event import com.woocommerce.android.viewmodel.ScopedViewModel import com.woocommerce.android.viewmodel.navArgs import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.launch import javax.inject.Inject @@ -26,55 +32,99 @@ class WooShippingLabelCreationViewModel @Inject constructor( private val orderDetailRepository: OrderDetailRepository, private val getShippableItems: GetShippableItems, private val currencyFormatter: CurrencyFormatter, - private val observeOriginAddresses: ObserveOriginAddresses + private val observeOriginAddresses: ObserveOriginAddresses, + private val getShippingRates: GetShippingRates ) : ScopedViewModel(savedState) { private val navArgs: WooShippingLabelCreationFragmentArgs by savedState.navArgs() - private val storeOptions = StoreOptionsModel( + private val mockStoreOptions = StoreOptionsModel( currencySymbol = "$", dimensionUnit = "cm", weightUnit = "kg", originCountry = "US" ) + private val mockSelectedPackage = PackageDAO( + id = "small_flat_box", + name = "Small Flat Rate Box", + dimensions = "21.91 x 13.65 x 4.13", + isLetter = false, + weight = "0.5", + dimensionUnit = "cm", + weightUnit = "kg" + ) + private val shippableItems = MutableStateFlow>(emptyList()) + private val selectedPackage = MutableStateFlow(mockSelectedPackage) + private val storeOptions = MutableStateFlow(mockStoreOptions) + private val selectedRatesSortOrder = MutableStateFlow(ShippingSortOption.FASTEST) + private val refreshShippingRates = MutableSharedFlow() + + @OptIn(ExperimentalCoroutinesApi::class) + private val shippingRates = + combine( + selectedPackage, + selectedRatesSortOrder, + refreshShippingRates.onStart { emit(Unit) } + ) { selectedPackage, sortOrder, _ -> + Pair(selectedPackage, sortOrder) + }.flatMapLatest { + val (selectedPackage, sortOrder) = it + refreshShippingRates(selectedPackage, sortOrder) + } + val viewState: MutableStateFlow = MutableStateFlow(WooShippingViewState.Loading) init { launch { observeShippingLabelInformation() } } + private fun refreshShippingRates(selectedPackage: PackageDAO, sortOrder: ShippingSortOption) = flow { + emit(ShippingRatesState.Loading(sortOrder)) + val shippingRatesResult = getShippingRates(selectedPackage, sortOrder) + if (shippingRatesResult.isSuccess) { + emit(ShippingRatesState.DataState(sortOrder, shippingRatesResult.getOrThrow())) + } else { + emit(ShippingRatesState.Error) + } + } + private suspend fun observeShippingLabelInformation() { - flowOf(orderDetailRepository.getOrderById(navArgs.orderId)) - .combine(observeOriginAddresses()) { order, originAddresses -> - val selectedOriginAddress = getSelectedOriginAddress(originAddresses) - if (order == null || selectedOriginAddress == null) { - return@combine WooShippingViewState.Error - } - val items = getShippableItems(order) - shippableItems.value = items - - val shippableItemsUI = items.map { item -> item.toUIModel(currencyFormatter, storeOptions) } - val formattedTotalPrice = getTotalPrice(items) - val formattedTotalWeight = getTotalWeight(items) - - val shippingLineSummary = getShippingLinesSummary(order) - - return@combine WooShippingViewState.DataState( - shippableItems = ShippableItemsUI( - shippableItems = shippableItemsUI, - formattedTotalWeight = formattedTotalWeight, - formattedTotalPrice = formattedTotalPrice - ), - shippingLines = shippingLineSummary, - shippingAddresses = WooShippingAddresses( - shipFrom = selectedOriginAddress, - originAddresses = originAddresses, - shipTo = order.shippingAddress - ) - ) - }.collect { - viewState.value = it + combine( + storeOptions, + flowOf(orderDetailRepository.getOrderById(navArgs.orderId)), + observeOriginAddresses(), + shippingRates + ) { storeOptions, order, originAddresses, shippingRates -> + val selectedOriginAddress = getSelectedOriginAddress(originAddresses) + if (order == null || selectedOriginAddress == null) { + return@combine WooShippingViewState.Error } + val items = getShippableItems(order) + shippableItems.value = items + + val shippableItemsUI = items.map { item -> item.toUIModel(currencyFormatter, storeOptions) } + val formattedTotalPrice = getTotalPrice(items) + val formattedTotalWeight = getTotalWeight(items, storeOptions) + + val shippingLineSummary = getShippingLinesSummary(order) + + return@combine WooShippingViewState.DataState( + shippableItems = ShippableItemsUI( + shippableItems = shippableItemsUI, + formattedTotalWeight = formattedTotalWeight, + formattedTotalPrice = formattedTotalPrice + ), + shippingLines = shippingLineSummary, + shippingAddresses = WooShippingAddresses( + shipFrom = selectedOriginAddress, + originAddresses = originAddresses, + shipTo = order.shippingAddress + ), + shippingRates = shippingRates + ) + }.collect { + viewState.value = it + } } private fun getSelectedOriginAddress(originAddresses: List): OriginShippingAddress? { @@ -103,6 +153,12 @@ class WooShippingLabelCreationViewModel @Inject constructor( } } + fun onRefreshShippingRates() { + launch { + refreshShippingRates.emit(Unit) + } + } + private fun getTotalPrice(items: List): String { val totalPrice = items.sumOf { it.price } val formattedTotalPrice = items.firstOrNull()?.currency?.let { @@ -111,7 +167,7 @@ class WooShippingLabelCreationViewModel @Inject constructor( return formattedTotalPrice } - private fun getTotalWeight(items: List): String { + private fun getTotalWeight(items: List, storeOptions: StoreOptionsModel): String { val totalWeight = items.sumByFloat { it.weight * it.quantity } return "${totalWeight.formatToString()} ${storeOptions.weightUnit}" } @@ -133,6 +189,10 @@ class WooShippingLabelCreationViewModel @Inject constructor( triggerEvent(LabelPurchased) } + fun onSelectedRateSortOrderChanged(option: ShippingSortOption) { + selectedRatesSortOrder.value = option + } + data object StartPackageSelection : Event() data object LabelPurchased : Event() @@ -143,8 +203,23 @@ class WooShippingLabelCreationViewModel @Inject constructor( val shippableItems: ShippableItemsUI, val shippingLines: List, val shippingAddresses: WooShippingAddresses, + val shippingRates: ShippingRatesState, ) : WooShippingViewState() } + + sealed class ShippingRatesState { + data object NoAvailable : ShippingRatesState() + data object Error : ShippingRatesState() + + data class Loading( + val selectedRatesSortOrder: ShippingSortOption + ) : ShippingRatesState() + + data class DataState( + val selectedRatesSortOrder: ShippingSortOption, + val shippingRates: Map> + ) : ShippingRatesState() + } } data class WooShippingAddresses( From 470b928a85ff5e4efe010a102831900fba9e6f96 Mon Sep 17 00:00:00 2001 From: Alejo Date: Wed, 11 Dec 2024 08:39:38 -0300 Subject: [PATCH 5/7] refactor card elevation --- .../WooShippingLabelCreationScreen.kt | 19 ++++--------------- 1 file changed, 4 insertions(+), 15 deletions(-) diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/wooshippinglabels/WooShippingLabelCreationScreen.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/wooshippinglabels/WooShippingLabelCreationScreen.kt index 379a59a09d1..108190d6a12 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/wooshippinglabels/WooShippingLabelCreationScreen.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/wooshippinglabels/WooShippingLabelCreationScreen.kt @@ -114,21 +114,10 @@ fun WooShippingLabelCreationScreen( val isDarkTheme = isSystemInDarkTheme() val isCollapsed = scaffoldState.bottomSheetState.isCollapsed val elevation = when { - isDarkTheme && isCollapsed -> { - 7.dp - } - - !isDarkTheme && isCollapsed -> { - 0.dp - } - - isDarkTheme && !isCollapsed -> { - 16.dp - } - - else -> { - 8.dp - } + isDarkTheme && isCollapsed -> 7.dp + !isDarkTheme && isCollapsed -> 0.dp + isDarkTheme && !isCollapsed -> 16.dp + else -> 8.dp } Box( modifier = Modifier From b256baabe6941f203f0a073cfeb6f262e2f298cb Mon Sep 17 00:00:00 2001 From: Alejo Date: Wed, 11 Dec 2024 09:47:32 -0300 Subject: [PATCH 6/7] fi detekt warning --- .../android/ui/orders/wooshippinglabels/GetShippingRates.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/wooshippinglabels/GetShippingRates.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/wooshippinglabels/GetShippingRates.kt index c53389cb40b..6195b736449 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/wooshippinglabels/GetShippingRates.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/wooshippinglabels/GetShippingRates.kt @@ -5,7 +5,7 @@ import com.woocommerce.android.ui.orders.wooshippinglabels.packages.datasource.P import kotlinx.coroutines.delay import javax.inject.Inject import kotlin.random.Random - +@Suppress("MagicNumber") class GetShippingRates @Inject constructor() { private val cheapestComparator = Comparator { r1, r2 -> r1.rate.substring(1).toBigDecimal().compareTo(r2.rate.substring(1).toBigDecimal()) From 063540b77d705976f703f59726e538865681c073 Mon Sep 17 00:00:00 2001 From: Alejo Date: Wed, 11 Dec 2024 09:47:47 -0300 Subject: [PATCH 7/7] add unit tests --- .../WooShippingLabelCreationViewModelTest.kt | 98 +++++++++++++++++++ 1 file changed, 98 insertions(+) diff --git a/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/orders/wooshippinglabels/WooShippingLabelCreationViewModelTest.kt b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/orders/wooshippinglabels/WooShippingLabelCreationViewModelTest.kt index 0e65804cd4d..497fc4b2b8b 100644 --- a/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/orders/wooshippinglabels/WooShippingLabelCreationViewModelTest.kt +++ b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/orders/wooshippinglabels/WooShippingLabelCreationViewModelTest.kt @@ -16,9 +16,12 @@ import org.mockito.kotlin.any import org.mockito.kotlin.doAnswer import org.mockito.kotlin.doReturn import org.mockito.kotlin.mock +import org.mockito.kotlin.times +import org.mockito.kotlin.verify import org.mockito.kotlin.whenever import java.math.BigDecimal import kotlin.test.assertEquals +import kotlin.test.assertIs @OptIn(ExperimentalCoroutinesApi::class) class WooShippingLabelCreationViewModelTest : BaseUnitTest() { @@ -65,6 +68,8 @@ class WooShippingLabelCreationViewModelTest : BaseUnitTest() { isVerified = true ) ) + private val defaultShippingRates = emptyMap>() + private val orderDetailRepository: OrderDetailRepository = mock() private val getShippableItems: GetShippableItems = mock() private val currencyFormatter: CurrencyFormatter = mock { @@ -77,6 +82,7 @@ class WooShippingLabelCreationViewModelTest : BaseUnitTest() { WooShippingLabelCreationFragmentArgs(orderId = orderId).toSavedStateHandle() private val observeOriginAddresses: ObserveOriginAddresses = mock() + private val getShippingRates: GetShippingRates = mock() private lateinit var sut: WooShippingLabelCreationViewModel @@ -86,6 +92,7 @@ class WooShippingLabelCreationViewModelTest : BaseUnitTest() { getShippableItems = getShippableItems, currencyFormatter = currencyFormatter, observeOriginAddresses = observeOriginAddresses, + getShippingRates = getShippingRates, savedState = savedState ) } @@ -98,6 +105,7 @@ class WooShippingLabelCreationViewModelTest : BaseUnitTest() { whenever(orderDetailRepository.getOrderById(any())) doReturn order whenever(getShippableItems(any())) doReturn defaultShippableItems whenever(observeOriginAddresses()) doReturn flowOf(defaultOriginAddresses) + whenever(getShippingRates(any(), any())) doReturn Result.success(defaultShippingRates) createViewModel() @@ -115,6 +123,7 @@ class WooShippingLabelCreationViewModelTest : BaseUnitTest() { whenever(orderDetailRepository.getOrderById(any())) doReturn order whenever(getShippableItems(any())) doReturn defaultShippableItems whenever(observeOriginAddresses()) doReturn flowOf(defaultOriginAddresses) + whenever(getShippingRates(any(), any())) doReturn Result.success(defaultShippingRates) createViewModel() @@ -130,6 +139,7 @@ class WooShippingLabelCreationViewModelTest : BaseUnitTest() { val order: Order? = null whenever(orderDetailRepository.getOrderById(any())) doReturn order whenever(observeOriginAddresses()) doReturn flowOf(defaultOriginAddresses) + whenever(getShippingRates(any(), any())) doReturn Result.success(defaultShippingRates) createViewModel() @@ -144,6 +154,7 @@ class WooShippingLabelCreationViewModelTest : BaseUnitTest() { ) whenever(orderDetailRepository.getOrderById(any())) doReturn order whenever(observeOriginAddresses()) doReturn flowOf(emptyList()) + whenever(getShippingRates(any(), any())) doReturn Result.success(defaultShippingRates) createViewModel() @@ -159,6 +170,7 @@ class WooShippingLabelCreationViewModelTest : BaseUnitTest() { whenever(orderDetailRepository.getOrderById(any())) doReturn order whenever(getShippableItems(any())) doReturn defaultShippableItems whenever(observeOriginAddresses()) doReturn flowOf(defaultOriginAddresses) + whenever(getShippingRates(any(), any())) doReturn Result.success(defaultShippingRates) createViewModel() @@ -169,4 +181,90 @@ class WooShippingLabelCreationViewModelTest : BaseUnitTest() { val ids = dataState.shippingAddresses.originAddresses.map { it.id } assert(ids.containsAll(defaultOriginAddresses.map { it.id })) } + + @Test + fun `when shipping rates succeed then display the shipping rates`() = testBlocking { + val order = OrderTestUtils.generateTestOrder(orderId = orderId).copy( + shippingLines = defaultShippingLines + ) + whenever(orderDetailRepository.getOrderById(any())) doReturn order + whenever(getShippableItems(any())) doReturn defaultShippableItems + whenever(observeOriginAddresses()) doReturn flowOf(defaultOriginAddresses) + whenever(getShippingRates(any(), any())) doReturn Result.success(defaultShippingRates) + + createViewModel() + + val currentViewState = sut.viewState.value + assert(currentViewState is WooShippingViewState.DataState) + val dataState = currentViewState as WooShippingViewState.DataState + assertIs(dataState.shippingRates) + } + + @Test + fun `when shipping rates fail then display an error`() = testBlocking { + val order = OrderTestUtils.generateTestOrder(orderId = orderId).copy( + shippingLines = defaultShippingLines + ) + whenever(orderDetailRepository.getOrderById(any())) doReturn order + whenever(getShippableItems(any())) doReturn defaultShippableItems + whenever(observeOriginAddresses()) doReturn flowOf(defaultOriginAddresses) + whenever(getShippingRates(any(), any())) doReturn Result.failure(Exception("Random error")) + + createViewModel() + + val currentViewState = sut.viewState.value + assert(currentViewState is WooShippingViewState.DataState) + val dataState = currentViewState as WooShippingViewState.DataState + assertIs(dataState.shippingRates) + } + + @Test + fun `when refresh rates is triggered then refresh shipping rates`() = testBlocking { + val order = OrderTestUtils.generateTestOrder(orderId = orderId).copy( + shippingLines = defaultShippingLines + ) + whenever(orderDetailRepository.getOrderById(any())) doReturn order + whenever(getShippableItems(any())) doReturn defaultShippableItems + whenever(observeOriginAddresses()) doReturn flowOf(defaultOriginAddresses) + whenever(getShippingRates(any(), any())) doReturn Result.success(defaultShippingRates) + + createViewModel() + sut.onRefreshShippingRates() + + verify(getShippingRates, times(2)).invoke(any(), any()) + } + + @Test + fun `when rates sort order is changed then refresh shipping rates`() = testBlocking { + val order = OrderTestUtils.generateTestOrder(orderId = orderId).copy( + shippingLines = defaultShippingLines + ) + whenever(orderDetailRepository.getOrderById(any())) doReturn order + whenever(getShippableItems(any())) doReturn defaultShippableItems + whenever(observeOriginAddresses()) doReturn flowOf(defaultOriginAddresses) + whenever(getShippingRates(any(), any())) doReturn Result.success(defaultShippingRates) + + createViewModel() + + sut.onSelectedRateSortOrderChanged(ShippingSortOption.CHEAPEST) + + verify(getShippingRates, times(2)).invoke(any(), any()) + } + + @Test + fun `when rates sort order is NOT changed then DON'T refresh shipping rates`() = testBlocking { + val order = OrderTestUtils.generateTestOrder(orderId = orderId).copy( + shippingLines = defaultShippingLines + ) + whenever(orderDetailRepository.getOrderById(any())) doReturn order + whenever(getShippableItems(any())) doReturn defaultShippableItems + whenever(observeOriginAddresses()) doReturn flowOf(defaultOriginAddresses) + whenever(getShippingRates(any(), any())) doReturn Result.success(defaultShippingRates) + + createViewModel() + + sut.onSelectedRateSortOrderChanged(ShippingSortOption.FASTEST) + + verify(getShippingRates, times(1)).invoke(any(), any()) + } }