diff --git a/RELEASE-NOTES.txt b/RELEASE-NOTES.txt index 9db9b097821..d2bd7cfe5f6 100644 --- a/RELEASE-NOTES.txt +++ b/RELEASE-NOTES.txt @@ -4,6 +4,7 @@ 23.2 ----- - [*] Shipping Labels: Add error message for invalid dimensions errors to custom package creation screen [https://github.com/woocommerce/woocommerce-android/pull/14499] +- [*] Shipping Labels: Update the UI of shipping label purchase in-progress and failure states [https://github.com/woocommerce/woocommerce-android/pull/14498] 23.1 ----- diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/wooshippinglabels/GetShipments.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/wooshippinglabels/GetShipments.kt index c33136ecb17..5acbaf3df77 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/wooshippinglabels/GetShipments.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/wooshippinglabels/GetShipments.kt @@ -69,7 +69,7 @@ class GetShipments @Inject constructor( currentOrderLabels: List ) = shipmentUIModelList.map { shipmentUIModel -> val shipmentLabels = currentOrderLabels.filter { - it.shipmentId == shipmentUIModel.remoteId + it.shipmentId == shipmentUIModel.remoteId && it.status != ShippingLabelStatus.PURCHASE_ERROR }.sortedByDescending { it.createdDate?.time } val purchasedLabel = shipmentLabels.find { diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/wooshippinglabels/ShipmentDetails.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/wooshippinglabels/ShipmentDetails.kt index b79b6ed73a1..6d0ecf0b810 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/wooshippinglabels/ShipmentDetails.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/wooshippinglabels/ShipmentDetails.kt @@ -87,7 +87,7 @@ fun ShipmentDetails( onEditOriginAddress: (OriginShippingAddress) -> Unit, onOriginAddressSelected: (OriginShippingAddress) -> Unit, destinationStatus: AddressStatus, - shipmentPurchased: Boolean, + readOnly: Boolean, onPeekHeightChanged: (Dp) -> Unit ) { val expandProgress = bottomSheetState.progress( @@ -200,7 +200,7 @@ fun ShipmentDetails( modifier = Modifier .weight(1f) .padding(horizontal = 16.dp), - shipmentPurchased = shipmentPurchased, + readOnly = readOnly, onEditDestinationAddress = onEditDestinationAddress, onEditOriginAddress = onEditOriginAddress, onOriginAddressSelected = onOriginAddressSelected, @@ -217,7 +217,7 @@ fun ShipmentDetails( modifier = Modifier .weight(1f) .padding(horizontal = 16.dp), - shipmentPurchased = shipmentPurchased, + readOnly = readOnly, onEditDestinationAddress = onEditDestinationAddress, onEditOriginAddress = onEditOriginAddress, onOriginAddressSelected = onOriginAddressSelected, @@ -259,7 +259,7 @@ private fun ShipmentDetailsPortrait( onOriginAddressSelected: (OriginShippingAddress) -> Unit, destinationStatus: AddressStatus, modifier: Modifier = Modifier, - shipmentPurchased: Boolean + readOnly: Boolean ) { Column( verticalArrangement = Arrangement.spacedBy(16.dp), @@ -272,14 +272,14 @@ private fun ShipmentDetailsPortrait( totalItems = totalItems, totalItemsCost = totalItemsCost, shippingLines = shippingLines, - isReadOnly = shipmentPurchased, + isReadOnly = readOnly, onEditDestinationAddress = onEditDestinationAddress, onEditOriginAddress = onEditOriginAddress, onOriginAddressSelected = onOriginAddressSelected, destinationStatus = destinationStatus ) Divider() - if (!shipmentPurchased) { + if (!readOnly) { PaymentSection(paymentsSectionUI = paymentsSectionUI) Divider() } @@ -300,7 +300,7 @@ private fun ShipmentDetailsLandscape( onOriginAddressSelected: (OriginShippingAddress) -> Unit, destinationStatus: AddressStatus, modifier: Modifier = Modifier, - shipmentPurchased: Boolean = false + readOnly: Boolean = false ) { Column( modifier @@ -309,7 +309,7 @@ private fun ShipmentDetailsLandscape( ) { AddressSectionLandscape( shippingAddresses = shippingAddresses, - isReadOnly = shipmentPurchased, + isReadOnly = readOnly, onEditDestinationAddress = onEditDestinationAddress, onEditOriginAddress = onEditOriginAddress, onOriginAddressSelected = onOriginAddressSelected, @@ -330,7 +330,7 @@ private fun ShipmentDetailsLandscape( ) VerticalDivider(Modifier.padding(top = 16.dp)) Column(modifier = Modifier.weight(1f)) { - if (!shipmentPurchased) { + if (!readOnly) { PaymentSection( paymentsSectionUI = paymentsSectionUI, modifier = Modifier @@ -679,7 +679,7 @@ fun ShipmentDetailsExpandedPreview() { onEditOriginAddress = {}, onOriginAddressSelected = {}, destinationStatus = AddressStatus.Verified, - shipmentPurchased = false, + readOnly = false, onPeekHeightChanged = {} ) } @@ -713,7 +713,7 @@ private fun ShipmentDetailsCollapsedPreview() { onEditOriginAddress = {}, onOriginAddressSelected = {}, destinationStatus = AddressStatus.Verified, - shipmentPurchased = false, + readOnly = false, onPeekHeightChanged = {}, ) } diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/wooshippinglabels/ShippableItemsMapper.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/wooshippinglabels/ShippableItemsMapper.kt index b72c88cfb19..718bcd17797 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/wooshippinglabels/ShippableItemsMapper.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/wooshippinglabels/ShippableItemsMapper.kt @@ -12,6 +12,7 @@ import com.woocommerce.android.ui.orders.wooshippinglabels.WooShippingLabelCreat import com.woocommerce.android.ui.orders.wooshippinglabels.models.ShipmentUIModel import com.woocommerce.android.ui.orders.wooshippinglabels.models.ShippableItemModel import com.woocommerce.android.ui.orders.wooshippinglabels.models.ShippableItemModel.Companion.SINGLE_QUANTITY +import com.woocommerce.android.ui.orders.wooshippinglabels.models.ShippingLabelModel import com.woocommerce.android.ui.orders.wooshippinglabels.models.ShippingLabelStatus import com.woocommerce.android.ui.orders.wooshippinglabels.models.WooShippingLabelPaperSize import com.woocommerce.android.ui.orders.wooshippinglabels.rates.ui.ShippingRateOption @@ -61,20 +62,14 @@ fun List.toUIModel( shippableItems = shippableItemsUI, formattedTotalWeight = formattedTotalWeight, formattedTotalPrice = formattedTotalPrice, - purchased = shipmentUIModel.purchased, packageSelectionState = packageSelectionState, customsState = customsState, hazmatState = hazmatCategory?.let { WooShippingLabelCreationViewModel.HazmatState.Declared(it) } ?: WooShippingLabelCreationViewModel.HazmatState.NoSelection, shippingRatesState = shippingRates, shipmentCostUI = shipmentCostUI, - purchaseState = shipmentUIModel.purchaseState, - status = shipmentUIModel.label?.status ?: ShippingLabelStatus.UNKNOWN, - shipmentPrintLabelUI = ShipmentPrintLabelUI( - availablePrintSizes = getPaperSizes(shipmentUIModel.label?.originAddress?.country?.code), - isRefundAvailable = shipmentUIModel.label?.isRefundAvailable == true, - isCustomsFormAvailable = shipmentUIModel.label?.commercialInvoiceUrl.isNotNullOrEmpty() - ), + isPurchaseAPILoading = shipmentUIModel.isPurchaseAPILoading, + labelPurchaseStatus = shipmentUIModel.label?.toPurchaseStatus() ?: LabelPurchaseStatus.Idle ) } @@ -158,7 +153,7 @@ private fun getShipmentCostUI( currencyFormatter: (BigDecimal) -> String ): ShipmentCostUI? { return when { - shipmentUIModel.purchased -> { + shipmentUIModel.isPurchasedOrInProgress -> { requireNotNull(shipmentUIModel.label) ShipmentCostUI( serviceName = shipmentUIModel.label.serviceName, @@ -196,9 +191,26 @@ private fun getShipmentCostUI( } } -private fun getPaperSizes(countryCode: String?): List = - if (countryCode.isNullOrEmpty() || countryCode.uppercase() in listOf("US", "CA", "MX", "DO")) { - WooShippingLabelPaperSize.entries.minus(WooShippingLabelPaperSize.A4) - } else { - WooShippingLabelPaperSize.entries +private fun ShippingLabelModel.toPurchaseStatus(): LabelPurchaseStatus { + fun getPaperSizes(countryCode: String?): List = + if (countryCode.isNullOrEmpty() || countryCode.uppercase() in listOf("US", "CA", "MX", "DO")) { + WooShippingLabelPaperSize.entries.minus(WooShippingLabelPaperSize.A4) + } else { + WooShippingLabelPaperSize.entries + } + + return when { + status == ShippingLabelStatus.PURCHASE_IN_PROGRESS -> LabelPurchaseStatus.PurchaseInProgress + status == ShippingLabelStatus.PURCHASED && refund == null -> { + LabelPurchaseStatus.Purchased( + availablePrintSizes = getPaperSizes(originAddress?.country?.code), + isRefundAvailable = isRefundAvailable, + isCustomsFormAvailable = commercialInvoiceUrl.isNotNullOrEmpty() + ) + } + status == ShippingLabelStatus.PURCHASE_ERROR -> LabelPurchaseStatus.Failed( + errorMessage = error + ) + else -> LabelPurchaseStatus.Idle } +} diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/wooshippinglabels/ShippingLabelSampleData.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/wooshippinglabels/ShippingLabelSampleData.kt index 5eb10d94942..b4d7d125509 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/wooshippinglabels/ShippingLabelSampleData.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/wooshippinglabels/ShippingLabelSampleData.kt @@ -9,13 +9,11 @@ import com.woocommerce.android.ui.orders.wooshippinglabels.models.DestinationShi import com.woocommerce.android.ui.orders.wooshippinglabels.models.OriginShippingAddress import com.woocommerce.android.ui.orders.wooshippinglabels.models.PaymentMethodModel import com.woocommerce.android.ui.orders.wooshippinglabels.models.PaymentMethodOptions -import com.woocommerce.android.ui.orders.wooshippinglabels.models.PurchaseState import com.woocommerce.android.ui.orders.wooshippinglabels.models.ShipmentUIModel import com.woocommerce.android.ui.orders.wooshippinglabels.models.ShippableItemModel import com.woocommerce.android.ui.orders.wooshippinglabels.models.ShippingLabelModel import com.woocommerce.android.ui.orders.wooshippinglabels.models.ShippingLabelStatus import com.woocommerce.android.ui.orders.wooshippinglabels.models.WooShippingCarrier -import com.woocommerce.android.ui.orders.wooshippinglabels.models.WooShippingLabelPaperSize import com.woocommerce.android.ui.orders.wooshippinglabels.rates.datasource.WooShippingRateModel import com.woocommerce.android.ui.orders.wooshippinglabels.rates.ui.CarrierUI import com.woocommerce.android.ui.orders.wooshippinglabels.rates.ui.ShippingRateOptionUI @@ -201,12 +199,6 @@ object ShippingLabelSampleData { ) } - fun getShipmentPrintLabelUI() = ShipmentPrintLabelUI( - availablePrintSizes = listOf(WooShippingLabelPaperSize.LABEL, WooShippingLabelPaperSize.LETTER), - isRefundAvailable = true, - isCustomsFormAvailable = true - ) - @Suppress("LongMethod") fun getShippingLabelUIModel(purchased: Boolean = false) = ShipmentUIModel( localId = "1", @@ -239,7 +231,6 @@ object ShippingLabelSampleData { currency = "USD" ) ), - purchaseState = PurchaseState.NoStarted, label = if (purchased) { ShippingLabelModel( labelId = 0L, 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 c6fa2ece3e0..02338a6f017 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 @@ -73,16 +73,15 @@ import com.woocommerce.android.ui.orders.wooshippinglabels.WooShippingLabelCreat import com.woocommerce.android.ui.orders.wooshippinglabels.WooShippingLabelCreationViewModel.PackageSelectionState.DataAvailable import com.woocommerce.android.ui.orders.wooshippinglabels.WooShippingLabelCreationViewModel.PackageSelectionState.NotSelected import com.woocommerce.android.ui.orders.wooshippinglabels.address.AddressStatus -import com.woocommerce.android.ui.orders.wooshippinglabels.components.PrintShippingLabelSection import com.woocommerce.android.ui.orders.wooshippinglabels.components.ShipmentTabData import com.woocommerce.android.ui.orders.wooshippinglabels.components.ShipmentsTabRow +import com.woocommerce.android.ui.orders.wooshippinglabels.components.ShippingLabelPurchaseStatusSection import com.woocommerce.android.ui.orders.wooshippinglabels.components.ShippingLabelsSnackbar import com.woocommerce.android.ui.orders.wooshippinglabels.components.ShippingLabelsSnackbarData import com.woocommerce.android.ui.orders.wooshippinglabels.components.ShippingLabelsSnackbarVisuals import com.woocommerce.android.ui.orders.wooshippinglabels.hazmat.HazmatCard import com.woocommerce.android.ui.orders.wooshippinglabels.models.DestinationShippingAddress import com.woocommerce.android.ui.orders.wooshippinglabels.models.OriginShippingAddress -import com.woocommerce.android.ui.orders.wooshippinglabels.models.PurchaseState import com.woocommerce.android.ui.orders.wooshippinglabels.models.WooShippingLabelPaperSize import com.woocommerce.android.ui.orders.wooshippinglabels.packages.components.ErrorMessageWithButton import com.woocommerce.android.ui.orders.wooshippinglabels.packages.ui.PackageData @@ -226,8 +225,7 @@ fun WooShippingLabelCreationScreen( ) val selectedShipment = shipmentUIList[uiState.selectedIndex] - val selectedPurchaseState = selectedShipment.purchaseState - if (selectedPurchaseState is PurchaseState.InProgress) { + if (selectedShipment.isPurchaseAPILoading) { Box( modifier = Modifier .fillMaxSize() @@ -289,7 +287,7 @@ private fun LabelCreationScreenWithBottomSheet( var bottomSheetPeekHeight by remember { mutableStateOf(0.dp) } - val screenTitle = if (shipmentUIList[uiState.selectedIndex].purchased) { + val screenTitle = if (shipmentUIList[uiState.selectedIndex].isPurchased) { R.string.shipping_label_print_screen_title } else { R.string.shipping_label_create_title @@ -317,7 +315,7 @@ private fun LabelCreationScreenWithBottomSheet( onOriginAddressSelected = onOriginAddressSelected, destinationStatus = destinationStatus, noticeBannerUiState = uiState.noticeBannerUiState, - shipmentPurchased = selectedShipment.purchased, + readOnly = selectedShipment.isReadOnly, onPeekHeightChanged = { bottomSheetPeekHeight = it }, ) }, @@ -367,7 +365,7 @@ private fun LabelCreationScreenWithBottomSheet( Row(modifier = Modifier.fillMaxWidth()) { ShipmentsTabRow( shipmentTabs = shipmentUIList.mapIndexed { index, shipment -> - ShipmentTabData(shipmentIndex = index + 1, isPurchased = shipment.purchased) + ShipmentTabData(shipmentIndex = index + 1, isPurchased = shipment.isPurchased) }, selectedTabIndex = if (pagerState.currentPage < pagerState.pageCount) { pagerState.currentPage @@ -465,23 +463,19 @@ private fun CreateShippingCards( Column { val isExpanded = remember { mutableStateOf(false) } - if (shipmentUI.purchased && shipmentUI.shipmentPrintLabelUI != null) { - PrintShippingLabelSection( - status = shipmentUI.status, - isCustomsFormAvailable = shipmentUI.shipmentPrintLabelUI.isCustomsFormAvailable, - isRefundAvailable = shipmentUI.shipmentPrintLabelUI.isRefundAvailable, - availablePaperSizes = shipmentUI.shipmentPrintLabelUI.availablePrintSizes, - selectedLabelPaperSizeOption = uiState.paperSizeOption, - onLabelPaperSizeOptionSelected = onLabelPaperSizeOptionSelected, - onPrintShippingLabelClicked = onPrintShippingLabelClicked, - onTrackShipmentClicked = onTrackShipmentClicked, - onSchedulePickUpClicked = onSchedulePickUpClicked, - onRefundClicked = onRefundClicked, - onPrintCustomsClicked = onPrintCustomsClicked, - onLearnMoreClicked = onLearnMoreClicked, - modifier = Modifier.padding(horizontal = 16.dp) - ) - } + ShippingLabelPurchaseStatusSection( + labelPurchaseStatus = shipmentUI.labelPurchaseStatus, + selectedLabelPaperSizeOption = uiState.paperSizeOption, + onLabelPaperSizeOptionSelected = onLabelPaperSizeOptionSelected, + onPrintShippingLabelClicked = onPrintShippingLabelClicked, + onTrackShipmentClicked = onTrackShipmentClicked, + onSchedulePickUpClicked = onSchedulePickUpClicked, + onRefundClicked = onRefundClicked, + onPrintCustomsClicked = onPrintCustomsClicked, + onLearnMoreClicked = onLearnMoreClicked, + modifier = Modifier.padding(horizontal = 16.dp) + ) + ShippingProductsCard( shippableItems = shipmentUI, modifier = Modifier @@ -491,13 +485,13 @@ private fun CreateShippingCards( onExpand = { isExpanded.value = it } ) HazmatCard( - onClick = if (shipmentUI.purchased) null else onHazmatNoticeClick, + onClick = if (shipmentUI.isReadOnly) null else onHazmatNoticeClick, selectedCategory = shipmentUI.hazmatState.hazmatSelection, modifier = Modifier .fillMaxWidth() .padding(start = 4.dp, end = 8.dp) ) - if (!shipmentUI.purchased) { + if (!shipmentUI.isReadOnly) { CustomsCard( customsState = shipmentUI.customsState, onEditCustomsClick = onEditCustomsClick, @@ -838,13 +832,12 @@ private fun WooShippingLabelCreationScreenPreview() { shippableItems = generateItems(6), formattedTotalWeight = "8.5kg", formattedTotalPrice = "$92.78", - purchased = false, packageSelectionState = NotSelected, customsState = Unavailable, hazmatState = Declared(ShippingLabelHazmatCategory.CLASS_1), shippingRatesState = ShippingLabelSampleData.getShippingRatesSection(), shipmentCostUI = ShippingLabelSampleData.getShippingRateSummaryUI(), - shipmentPrintLabelUI = ShippingLabelSampleData.getShipmentPrintLabelUI(), + labelPurchaseStatus = LabelPurchaseStatus.Idle, ) ), shouldShowSplitShipmentButton = true, 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 6d9702af7d0..00f24ef6c7e 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 @@ -49,8 +49,6 @@ import com.woocommerce.android.ui.orders.wooshippinglabels.domain.DownloadAndPri import com.woocommerce.android.ui.orders.wooshippinglabels.models.DestinationShippingAddress import com.woocommerce.android.ui.orders.wooshippinglabels.models.OriginShippingAddress import com.woocommerce.android.ui.orders.wooshippinglabels.models.PaymentMethodModel -import com.woocommerce.android.ui.orders.wooshippinglabels.models.PurchaseState -import com.woocommerce.android.ui.orders.wooshippinglabels.models.PurchasedLabelData import com.woocommerce.android.ui.orders.wooshippinglabels.models.ShipmentUIModel import com.woocommerce.android.ui.orders.wooshippinglabels.models.ShippableItemModel import com.woocommerce.android.ui.orders.wooshippinglabels.models.ShippingLabelStatus @@ -205,7 +203,7 @@ class WooShippingLabelCreationViewModel @Inject constructor( } private suspend fun trackScreenShownEvent() { - val unfulfilledShipmentsCount = shipments.drop(1).first().count { !it.purchased } + val unfulfilledShipmentsCount = shipments.drop(1).first().count { !it.isPurchasedOrInProgress } analyticsTracker.track( AnalyticsEvent.WCS_CREATE_SHIPPING_LABEL_FORM_SHOWN, mapOf("unfulfilled_shipments_count" to unfulfilledShipmentsCount) @@ -228,7 +226,7 @@ class WooShippingLabelCreationViewModel @Inject constructor( ) { shipments, selectedShipmentIndex -> shipments[selectedShipmentIndex] }.flatMapLatest { shipment -> - if (shipment.purchased) { + if (shipment.isPurchasedOrInProgress) { flowOf(null) } else { observeShippingLabelNotice( @@ -585,7 +583,7 @@ class WooShippingLabelCreationViewModel @Inject constructor( uiState.map { it.selectedIndex }.distinctUntilChanged(), ) { order, destination, originAddresses, shipments, selectedIndex -> val currentShipment = shipments[selectedIndex] - val updatedAddress = if (currentShipment.purchased) { + val updatedAddress = if (currentShipment.isPurchasedOrInProgress) { val selectedAddress = shippingAddresses.value.getOrNull(selectedShipmentIndex) val shipFrom = selectedAddress?.shipFrom?.takeIf { it != OriginShippingAddress.EMPTY } ?: currentShipment.label?.originAddress?.let { originAddress -> @@ -711,7 +709,7 @@ class WooShippingLabelCreationViewModel @Inject constructor( hazmatStatesFlow ) { accountSettings, order, shipments, addresses, shippingRatesList, packageSelections, uiState, customsState, hazmatStates -> - if (accountSettings == null || shipments.any { it.purchaseState is PurchaseState.Error }) { + if (accountSettings == null) { return@combine WooShippingViewState.Error } @@ -754,7 +752,7 @@ class WooShippingLabelCreationViewModel @Inject constructor( onEditPaymentMethodClicked = ::onEditPaymentMethodClicked ), purchaseSectionUI = PurchaseSectionUI( - isVisible = !shipmentUIList[uiState.selectedIndex].purchased && + isVisible = !shipmentUIList[uiState.selectedIndex].isReadOnly && shippingRatesStatesFlow.value[uiState.selectedIndex] is ShippingRatesState.DataState, isOrderAlreadyCompleted = order.status == Order.Status.Completed, markOrderComplete = uiState.markOrderComplete, @@ -937,8 +935,9 @@ class WooShippingLabelCreationViewModel @Inject constructor( launch { refreshShippingRates.emit(Unit) } } - @Suppress("ComplexCondition") + @Suppress("ComplexCondition", "LongMethod") fun onPurchaseShippingLabel() { + val selectedShipmentIndex = selectedShipmentIndex val selectedPackage = selectedPackagesFlow.value[selectedShipmentIndex] val selectedAddress = shippingAddresses.value.getOrNull(selectedShipmentIndex) val shippingRate = selectedRatesFlow.value[selectedShipmentIndex] @@ -951,10 +950,9 @@ class WooShippingLabelCreationViewModel @Inject constructor( val shippableItemsIdList = shipmentItems.value[selectedShipmentIndex].map { it.productId } val hazmatSelection = hazmatStatesFlow.value[selectedShipmentIndex].hazmatSelection - val fallbackPurchaseState = shipments.value[selectedShipmentIndex].purchaseState updateShipment( selectedShipmentIndex, - shipments.value[selectedShipmentIndex].copy(purchaseState = PurchaseState.InProgress) + shipments.value[selectedShipmentIndex].copy(isPurchaseAPILoading = true) ) val customsData = customsFormDataFlow.value[selectedShipmentIndex] @@ -976,12 +974,21 @@ class WooShippingLabelCreationViewModel @Inject constructor( hazmatSelection ).fold( onSuccess = { - handlePurchaseSuccess(it, selectedShipmentIndex) + updateShipment( + selectedShipmentIndex, + shipments.value[selectedShipmentIndex].copy( + isPurchaseAPILoading = false, + label = it.labels.firstOrNull() + ) + ) + observeShippingLabelPurchaseStatus(selectedShipmentIndex) + // TODO check if we need to track this here or in the observeShippingLabelPurchaseStatus method + analyticsTracker.track(AnalyticsEvent.WCS_PURCHASE_STEP, mapOf(KEY_STATE to "purchase_success")) }, onFailure = { exception -> updateShipment( selectedShipmentIndex, - shipments.value[selectedShipmentIndex].copy(purchaseState = fallbackPurchaseState) + shipments.value[selectedShipmentIndex].copy(isPurchaseAPILoading = false) ) if (exception is WooException && exception.error.apiErrorCode == UPSDAP_MISSING_TOS_ERROR_CODE) { triggerEvent(NavigateToUPSDAPTermsOfService(selectedAddress.shipFrom)) @@ -1000,17 +1007,6 @@ class WooShippingLabelCreationViewModel @Inject constructor( } } - private fun handlePurchaseSuccess(result: PurchasedLabelData, shipmentId: Int) { - updateShipment(shipmentId, shipments.value[shipmentId].copy(purchaseState = PurchaseState.Success)) - result.labels - .firstOrNull() - ?.let { purchasedLabel -> - updateShipment(shipmentId, shipments.value[shipmentId].copy(label = purchasedLabel)) - observeShippingLabelPurchaseStatus(shipmentId) - } - analyticsTracker.track(AnalyticsEvent.WCS_PURCHASE_STEP, mapOf(KEY_STATE to "purchase_success")) - } - fun onSelectedRateSortOrderChanged(option: ShippingSortOption) { selectedRatesSortOrdersFlow.value = selectedRatesSortOrdersFlow.value.toMutableList().apply { set(selectedShipmentIndex, option) @@ -1261,9 +1257,12 @@ class WooShippingLabelCreationViewModel @Inject constructor( ) : WooShippingViewState() { val shouldShowSplitShipmentButton: Boolean get() { - val unpurchasedShipments = shipmentUIList.filterNot { it.purchased } - return unpurchasedShipments.size > 1 || - (unpurchasedShipments.firstOrNull()?.totalItemQuantity ?: 0) > 1 + val unpurchasedShipments = shipmentUIList.filterNot { it.isPurchased } + return shipmentUIList.none { it.isPurchaseInProgress } && + ( + unpurchasedShipments.size > 1 || + (unpurchasedShipments.firstOrNull()?.totalItemQuantity ?: 0) > 1 + ) } } } @@ -1417,16 +1416,23 @@ data class ShipmentUI( val shippableItems: List, val formattedTotalWeight: String, val formattedTotalPrice: String, - val purchased: Boolean, val packageSelectionState: PackageSelectionState, val customsState: CustomsState, val hazmatState: HazmatState, val shippingRatesState: ShippingRatesState, val shipmentCostUI: ShipmentCostUI?, - val purchaseState: PurchaseState = PurchaseState.NoStarted, - val status: ShippingLabelStatus = ShippingLabelStatus.UNKNOWN, - val shipmentPrintLabelUI: ShipmentPrintLabelUI?, + val isPurchaseAPILoading: Boolean = false, + val labelPurchaseStatus: LabelPurchaseStatus = LabelPurchaseStatus.Idle ) : Parcelable { + val isPurchased + get() = labelPurchaseStatus is LabelPurchaseStatus.Purchased + + val isPurchaseInProgress + get() = labelPurchaseStatus is LabelPurchaseStatus.PurchaseInProgress + + val isReadOnly + get() = isPurchaseInProgress || isPurchased + val totalItemQuantity get() = shippableItems.sumByFloat { it.quantity }.toInt() } @@ -1445,12 +1451,23 @@ data class ShipmentCostUI( val optionsWithFees: Map, ) : Parcelable -@Parcelize -data class ShipmentPrintLabelUI( - val availablePrintSizes: List, - val isRefundAvailable: Boolean = false, - val isCustomsFormAvailable: Boolean = false, -) : Parcelable +sealed interface LabelPurchaseStatus : Parcelable { + @Parcelize + data object Idle : LabelPurchaseStatus + + @Parcelize + data object PurchaseInProgress : LabelPurchaseStatus + + @Parcelize + data class Purchased( + val availablePrintSizes: List, + val isRefundAvailable: Boolean = false, + val isCustomsFormAvailable: Boolean = false + ) : LabelPurchaseStatus + + @Parcelize + data class Failed(val errorMessage: String?) : LabelPurchaseStatus +} data class PurchaseSectionUI( val isVisible: Boolean, diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/wooshippinglabels/WooShippingProductsCard.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/wooshippinglabels/WooShippingProductsCard.kt index 61f63c7a57c..8c0850c356d 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/wooshippinglabels/WooShippingProductsCard.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/wooshippinglabels/WooShippingProductsCard.kt @@ -96,13 +96,12 @@ private fun ShippingProductsCardPreview(@PreviewParameter(IsExpandedProvider::cl shippableItems = generateItems(6), formattedTotalWeight = "8.5kg", formattedTotalPrice = "$92.78", - purchased = false, packageSelectionState = WooShippingLabelCreationViewModel.PackageSelectionState.NotSelected, customsState = WooShippingLabelCreationViewModel.CustomsState.NotRequired, hazmatState = WooShippingLabelCreationViewModel.HazmatState.NoSelection, shippingRatesState = WooShippingLabelCreationViewModel.ShippingRatesState.NoAvailable, shipmentCostUI = ShippingLabelSampleData.getShippingRateSummaryUI(), - shipmentPrintLabelUI = ShippingLabelSampleData.getShipmentPrintLabelUI() + labelPurchaseStatus = LabelPurchaseStatus.Idle ), isExpanded = isExpanded ) @@ -173,13 +172,12 @@ private fun ShippingProductsCardHeaderPreview() { shippableItems = generateItems(4), formattedTotalWeight = "8.5kg", formattedTotalPrice = "$92.78", - purchased = false, packageSelectionState = WooShippingLabelCreationViewModel.PackageSelectionState.NotSelected, customsState = WooShippingLabelCreationViewModel.CustomsState.NotRequired, hazmatState = WooShippingLabelCreationViewModel.HazmatState.NoSelection, shippingRatesState = WooShippingLabelCreationViewModel.ShippingRatesState.NoAvailable, shipmentCostUI = ShippingLabelSampleData.getShippingRateSummaryUI(), - shipmentPrintLabelUI = ShippingLabelSampleData.getShipmentPrintLabelUI() + labelPurchaseStatus = LabelPurchaseStatus.Idle ) val isExpanded = remember { mutableStateOf(false) } diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/wooshippinglabels/components/PrintShippingLabelSection.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/wooshippinglabels/components/PrintShippingLabelSection.kt deleted file mode 100644 index d4aa585fae6..00000000000 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/wooshippinglabels/components/PrintShippingLabelSection.kt +++ /dev/null @@ -1,347 +0,0 @@ -package com.woocommerce.android.ui.orders.wooshippinglabels.components - -import android.content.res.Configuration -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -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.Spacer -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.Divider -import androidx.compose.material.DropdownMenu -import androidx.compose.material.DropdownMenuItem -import androidx.compose.material.Icon -import androidx.compose.material.MaterialTheme -import androidx.compose.material.Surface -import androidx.compose.material.Text -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.outlined.OpenInNew -import androidx.compose.material.icons.filled.ArrowDropDown -import androidx.compose.material.icons.outlined.Info -import androidx.compose.material3.ButtonDefaults.buttonColors -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.colorResource -import androidx.compose.ui.res.dimensionResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.tooling.preview.Devices -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import com.woocommerce.android.R -import com.woocommerce.android.ui.compose.component.WCColoredButton -import com.woocommerce.android.ui.compose.theme.WooThemeWithBackground -import com.woocommerce.android.ui.orders.wooshippinglabels.RoundedCornerBoxWithBorder -import com.woocommerce.android.ui.orders.wooshippinglabels.models.ShippingLabelStatus -import com.woocommerce.android.ui.orders.wooshippinglabels.models.ShippingLabelStatus.PURCHASED -import com.woocommerce.android.ui.orders.wooshippinglabels.models.ShippingLabelStatus.PURCHASE_IN_PROGRESS -import com.woocommerce.android.ui.orders.wooshippinglabels.models.WooShippingLabelPaperSize - -@Composable -fun PrintShippingLabelSection( - status: ShippingLabelStatus, - isCustomsFormAvailable: Boolean, - isRefundAvailable: Boolean, - availablePaperSizes: List, - selectedLabelPaperSizeOption: WooShippingLabelPaperSize, - onLabelPaperSizeOptionSelected: (WooShippingLabelPaperSize) -> Unit, - onPrintShippingLabelClicked: () -> Unit, - onTrackShipmentClicked: () -> Unit, - onSchedulePickUpClicked: () -> Unit, - onRefundClicked: () -> Unit, - onPrintCustomsClicked: () -> Unit, - onLearnMoreClicked: () -> Unit, - modifier: Modifier = Modifier, -) { - Column( - modifier = modifier.padding(top = 8.dp) - ) { - val (titleResId, messageResId) = when (status) { - PURCHASED -> Pair( - R.string.shipping_label_purchased_success_title, - R.string.shipping_label_purchased_success_message - ) - - PURCHASE_IN_PROGRESS -> Pair( - R.string.shipping_label_purchased_in_progress_title, - R.string.shipping_label_purchased_in_progress_message - ) - - else -> Pair( - R.string.shipping_label_purchased_failure_title, - R.string.shipping_label_purchased_failure_message - ) - } - - Text( - text = stringResource(id = titleResId), - style = MaterialTheme.typography.subtitle1, - fontWeight = FontWeight.Bold - ) - Text( - text = stringResource(id = messageResId), - modifier = Modifier.padding(top = 8.dp), - style = MaterialTheme.typography.subtitle1, - ) - Spacer(modifier = Modifier.padding(top = 16.dp)) - PrintShippingLabelCard( - isPrintButtonEnabled = status == PURCHASED, - isCustomsFormAvailable = isCustomsFormAvailable, - isRefundAvailable = isRefundAvailable, - availablePaperSizes = availablePaperSizes, - selectedLabelPaperSizeOption = selectedLabelPaperSizeOption, - onLabelPaperSizeOptionSelected = onLabelPaperSizeOptionSelected, - onPrintShippingLabelClicked = onPrintShippingLabelClicked, - onTrackShipmentClicked = onTrackShipmentClicked, - onSchedulePickUpClicked = onSchedulePickUpClicked, - onRefundClicked = onRefundClicked, - onPrintCustomsClicked = onPrintCustomsClicked, - onLearnMoreClicked = onLearnMoreClicked, - ) - Text( - text = stringResource(id = R.string.shipping_label_purchased_note), - style = MaterialTheme.typography.caption, - color = colorResource(id = R.color.color_on_surface_medium), - modifier = Modifier.padding(top = 8.dp), - ) - Spacer(modifier = Modifier.padding(top = 16.dp)) - } -} - -@Composable -private fun PrintShippingLabelCard( - isPrintButtonEnabled: Boolean, - isCustomsFormAvailable: Boolean, - isRefundAvailable: Boolean, - availablePaperSizes: List, - selectedLabelPaperSizeOption: WooShippingLabelPaperSize, - onLabelPaperSizeOptionSelected: (WooShippingLabelPaperSize) -> Unit, - onPrintShippingLabelClicked: () -> Unit, - onTrackShipmentClicked: () -> Unit, - onSchedulePickUpClicked: () -> Unit, - onRefundClicked: () -> Unit, - onPrintCustomsClicked: () -> Unit, - onLearnMoreClicked: () -> Unit, - modifier: Modifier = Modifier, -) { - Column( - modifier = modifier - .background( - color = colorResource(id = R.color.woo_shipping_label_success_surface), - shape = RoundedCornerShape(dimensionResource(R.dimen.corner_radius_large)) - ) - .padding(16.dp) - ) { - RoundedCornerBoxWithBorder(backgroundColor = colorResource(id = R.color.woo_shipping_label_success_surface)) { - LabelPaperSizeDropdownMenu( - availablePaperSizes = availablePaperSizes, - selectedLabelPaperSizeOption = selectedLabelPaperSizeOption, - onLabelPaperSizeOptionSelected = onLabelPaperSizeOptionSelected, - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp, vertical = 12.dp) - ) - } - - WCColoredButton( - enabled = isPrintButtonEnabled, - text = stringResource(id = R.string.shipping_label_print_button), - onClick = { onPrintShippingLabelClicked() }, - modifier = Modifier - .fillMaxWidth() - .padding(top = 12.dp, bottom = 4.dp), - colors = buttonColors( - containerColor = colorResource(id = R.color.woo_shipping_label_success), - contentColor = colorResource(id = R.color.woo_white) - ) - ) - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier - .clickable { onLearnMoreClicked() } - .padding(vertical = 8.dp) - ) { - Icon( - imageVector = Icons.Outlined.Info, - contentDescription = null, - modifier = Modifier - .padding(end = 8.dp) - .size(16.dp), - tint = colorResource(id = R.color.woo_shipping_label_success) - ) - Text( - text = stringResource(id = R.string.shipping_label_purchased_learn_how_to_print), - style = MaterialTheme.typography.caption, - color = colorResource(id = R.color.woo_shipping_label_success) - ) - } - Divider(modifier = Modifier.padding(vertical = 8.dp)) - ShippingLabelLink( - text = stringResource(id = R.string.shipping_label_purchased_track_shipment), - onClick = { onTrackShipmentClicked() }, - showIcon = true, - modifier = Modifier.padding(vertical = 8.dp) - ) - ShippingLabelLink( - text = stringResource(id = R.string.shipping_label_purchased_schedule_pick_up), - onClick = { onSchedulePickUpClicked() }, - showIcon = true, - modifier = Modifier.padding(vertical = 8.dp) - ) - if (isCustomsFormAvailable) { - ShippingLabelLink( - text = stringResource(id = R.string.shipping_label_print_customs_form), - onClick = { onPrintCustomsClicked() }, - showIcon = true, - modifier = Modifier.padding(vertical = 8.dp) - ) - } - if (isRefundAvailable) { - ShippingLabelLink( - text = stringResource(id = R.string.shipping_label_purchased_request_refund), - onClick = { onRefundClicked() }, - showIcon = false, - modifier = Modifier.padding(vertical = 8.dp) - ) - } - } -} - -@Composable -private fun LabelPaperSizeDropdownMenu( - availablePaperSizes: List, - selectedLabelPaperSizeOption: WooShippingLabelPaperSize, - onLabelPaperSizeOptionSelected: (WooShippingLabelPaperSize) -> Unit, - modifier: Modifier = Modifier, -) { - var expanded by remember { mutableStateOf(false) } - - Box { - Row( - modifier = Modifier - .clickable { expanded = true } - .then(modifier), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.Center - ) { - Text( - text = stringResource(selectedLabelPaperSizeOption.stringResource), - style = MaterialTheme.typography.subtitle1, - modifier = Modifier.weight(1f) - ) - Icon( - imageVector = Icons.Filled.ArrowDropDown, - contentDescription = stringResource( - R.string.sorted_by, - stringResource(selectedLabelPaperSizeOption.stringResource) - ), - tint = colorResource(id = R.color.woo_shipping_label_success) - - ) - } - DropdownMenu( - expanded = expanded, - onDismissRequest = { expanded = false }, - modifier = Modifier.align(alignment = Alignment.CenterEnd) - ) { - availablePaperSizes.forEach { option -> - DropdownMenuItem(onClick = { - onLabelPaperSizeOptionSelected(option) - expanded = false - }) { - Text( - text = stringResource(option.stringResource), - style = MaterialTheme.typography.subtitle1 - ) - } - } - } - } -} - -@Composable -private fun ShippingLabelLink( - text: String, - onClick: () -> Unit, - modifier: Modifier = Modifier, - showIcon: Boolean = false -) { - Row( - modifier = Modifier - .clickable { onClick() } - .then(modifier), - verticalAlignment = Alignment.CenterVertically - ) { - Text( - text = text, - style = MaterialTheme.typography.subtitle1, - color = colorResource(id = R.color.woo_shipping_label_success), - fontWeight = FontWeight.Bold - ) - if (showIcon) { - Icon( - imageVector = Icons.AutoMirrored.Outlined.OpenInNew, - contentDescription = null, - tint = colorResource(id = R.color.woo_shipping_label_success), - modifier = Modifier.padding(start = 8.dp) - ) - } - } -} - -@Preview( - name = "dark", - uiMode = Configuration.UI_MODE_NIGHT_YES, - showSystemUi = true, - device = Devices.PIXEL_4 -) -@Preview( - name = "light", - uiMode = Configuration.UI_MODE_NIGHT_NO, - showSystemUi = true, - device = Devices.PIXEL_4 -) -@Composable -internal fun PrintShippingLabelSectionPreview() { - WooThemeWithBackground { - Surface { - val selectedLabelPaperSizeOption = remember { mutableStateOf(WooShippingLabelPaperSize.A4) } - PrintShippingLabelSection( - status = PURCHASED, - isCustomsFormAvailable = true, - isRefundAvailable = true, - availablePaperSizes = emptyList(), - selectedLabelPaperSizeOption = selectedLabelPaperSizeOption.value, - onLabelPaperSizeOptionSelected = { selectedLabelPaperSizeOption.value = it }, - onPrintShippingLabelClicked = {}, - onTrackShipmentClicked = {}, - onSchedulePickUpClicked = {}, - onRefundClicked = {}, - onPrintCustomsClicked = {}, - onLearnMoreClicked = {} - ) - } - } -} - -@Preview -@Composable -private fun ShippingLabelLinkPreview() { - WooThemeWithBackground { - ShippingLabelLink( - text = "Shipping Label", - onClick = {}, - showIcon = true - ) - } -} diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/wooshippinglabels/components/ShippingLabelPurchaseStatusSection.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/wooshippinglabels/components/ShippingLabelPurchaseStatusSection.kt new file mode 100644 index 00000000000..2ee12b6cc94 --- /dev/null +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/wooshippinglabels/components/ShippingLabelPurchaseStatusSection.kt @@ -0,0 +1,425 @@ +package com.woocommerce.android.ui.orders.wooshippinglabels.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +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.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Divider +import androidx.compose.material.DropdownMenu +import androidx.compose.material.DropdownMenuItem +import androidx.compose.material.Icon +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.outlined.OpenInNew +import androidx.compose.material.icons.filled.ArrowDropDown +import androidx.compose.material.icons.outlined.Info +import androidx.compose.material3.ButtonDefaults.buttonColors +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.res.dimensionResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import com.woocommerce.android.R +import com.woocommerce.android.ui.compose.component.WCColoredButton +import com.woocommerce.android.ui.compose.preview.LightDarkThemePreviews +import com.woocommerce.android.ui.compose.theme.WooThemeWithBackground +import com.woocommerce.android.ui.orders.wooshippinglabels.LabelPurchaseStatus +import com.woocommerce.android.ui.orders.wooshippinglabels.RoundedCornerBoxWithBorder +import com.woocommerce.android.ui.orders.wooshippinglabels.models.WooShippingLabelPaperSize + +@Composable +fun ShippingLabelPurchaseStatusSection( + labelPurchaseStatus: LabelPurchaseStatus, + selectedLabelPaperSizeOption: WooShippingLabelPaperSize, + onLabelPaperSizeOptionSelected: (WooShippingLabelPaperSize) -> Unit, + onPrintShippingLabelClicked: () -> Unit, + onTrackShipmentClicked: () -> Unit, + onSchedulePickUpClicked: () -> Unit, + onRefundClicked: () -> Unit, + onPrintCustomsClicked: () -> Unit, + onLearnMoreClicked: () -> Unit, + modifier: Modifier = Modifier, +) { + when (labelPurchaseStatus) { + LabelPurchaseStatus.Idle -> { + // Empty + } + + LabelPurchaseStatus.PurchaseInProgress -> { + PurchaseInProgressNotice(modifier.padding(vertical = 16.dp)) + } + + is LabelPurchaseStatus.Purchased -> { + PurchasedLabelSection( + status = labelPurchaseStatus, + availablePaperSizes = labelPurchaseStatus.availablePrintSizes, + selectedLabelPaperSizeOption = selectedLabelPaperSizeOption, + onLabelPaperSizeOptionSelected = onLabelPaperSizeOptionSelected, + onPrintShippingLabelClicked = onPrintShippingLabelClicked, + onTrackShipmentClicked = onTrackShipmentClicked, + onSchedulePickUpClicked = onSchedulePickUpClicked, + onRefundClicked = onRefundClicked, + onPrintCustomsClicked = onPrintCustomsClicked, + onLearnMoreClicked = onLearnMoreClicked, + modifier = modifier.padding(vertical = 16.dp) + ) + } + + is LabelPurchaseStatus.Failed -> { + PurchaseFailureNotice( + errorMessage = labelPurchaseStatus.errorMessage, + modifier = modifier.padding(vertical = 16.dp) + ) + } + } +} + +@Composable +private fun PurchasedLabelSection( + status: LabelPurchaseStatus.Purchased, + availablePaperSizes: List, + selectedLabelPaperSizeOption: WooShippingLabelPaperSize, + onLabelPaperSizeOptionSelected: (WooShippingLabelPaperSize) -> Unit, + onPrintShippingLabelClicked: () -> Unit, + onTrackShipmentClicked: () -> Unit, + onSchedulePickUpClicked: () -> Unit, + onRefundClicked: () -> Unit, + onPrintCustomsClicked: () -> Unit, + onLearnMoreClicked: () -> Unit, + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier + ) { + Text( + text = stringResource(id = R.string.shipping_label_purchased_success_title), + style = MaterialTheme.typography.subtitle1, + fontWeight = FontWeight.Bold + ) + Text( + text = stringResource(id = R.string.shipping_label_purchased_success_message), + modifier = Modifier.padding(top = 8.dp), + style = MaterialTheme.typography.subtitle1, + ) + + Spacer(modifier = Modifier.padding(top = 16.dp)) + + Column( + modifier = Modifier + .background( + color = colorResource(id = R.color.woo_shipping_label_success_surface), + shape = RoundedCornerShape(dimensionResource(R.dimen.corner_radius_large)) + ) + .padding(16.dp) + ) { + RoundedCornerBoxWithBorder( + backgroundColor = colorResource(id = R.color.woo_shipping_label_success_surface) + ) { + LabelPaperSizeDropdownMenu( + availablePaperSizes = availablePaperSizes, + selectedLabelPaperSizeOption = selectedLabelPaperSizeOption, + onLabelPaperSizeOptionSelected = onLabelPaperSizeOptionSelected, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 12.dp) + ) + } + + WCColoredButton( + text = stringResource(id = R.string.shipping_label_print_button), + onClick = { onPrintShippingLabelClicked() }, + modifier = Modifier + .fillMaxWidth() + .padding(top = 12.dp, bottom = 4.dp), + colors = buttonColors( + containerColor = colorResource(id = R.color.woo_shipping_label_success), + contentColor = colorResource(id = R.color.woo_white) + ) + ) + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .clickable { onLearnMoreClicked() } + .padding(vertical = 8.dp) + ) { + Icon( + imageVector = Icons.Outlined.Info, + contentDescription = null, + modifier = Modifier + .padding(end = 8.dp) + .size(16.dp), + tint = colorResource(id = R.color.woo_shipping_label_success) + ) + Text( + text = stringResource(id = R.string.shipping_label_purchased_learn_how_to_print), + style = MaterialTheme.typography.caption, + color = colorResource(id = R.color.woo_shipping_label_success) + ) + } + Divider(modifier = Modifier.padding(vertical = 8.dp)) + ShippingLabelLink( + text = stringResource(id = R.string.shipping_label_purchased_track_shipment), + onClick = { onTrackShipmentClicked() }, + showIcon = true, + modifier = Modifier.padding(vertical = 8.dp) + ) + ShippingLabelLink( + text = stringResource(id = R.string.shipping_label_purchased_schedule_pick_up), + onClick = { onSchedulePickUpClicked() }, + showIcon = true, + modifier = Modifier.padding(vertical = 8.dp) + ) + if (status.isCustomsFormAvailable) { + ShippingLabelLink( + text = stringResource(id = R.string.shipping_label_print_customs_form), + onClick = { onPrintCustomsClicked() }, + showIcon = true, + modifier = Modifier.padding(vertical = 8.dp) + ) + } + if (status.isRefundAvailable) { + ShippingLabelLink( + text = stringResource(id = R.string.shipping_label_purchased_request_refund), + onClick = { onRefundClicked() }, + showIcon = false, + modifier = Modifier.padding(vertical = 8.dp) + ) + } + } + ReprintWarning(Modifier.padding(top = 8.dp)) + } +} + +@Composable +private fun PurchaseInProgressNotice(modifier: Modifier) { + Column( + verticalArrangement = Arrangement.spacedBy(16.dp), + modifier = modifier + ) { + Text( + text = stringResource(id = R.string.shipping_label_purchased_in_progress_title), + style = MaterialTheme.typography.subtitle1, + fontWeight = FontWeight.Bold + ) + Text( + text = stringResource(id = R.string.shipping_label_purchased_in_progress_message), + style = MaterialTheme.typography.subtitle1, + modifier = Modifier + .fillMaxWidth() + .background( + color = colorResource(R.color.woo_yellow_10).copy(alpha = 0.2f), + shape = RoundedCornerShape(dimensionResource(R.dimen.corner_radius_large)) + ) + .padding(16.dp) + ) + ReprintWarning() + } +} + +@Composable +private fun PurchaseFailureNotice(errorMessage: String?, modifier: Modifier) { + Column( + verticalArrangement = Arrangement.spacedBy(16.dp), + modifier = modifier + ) { + Text( + text = stringResource(id = R.string.shipping_label_purchased_failure_title), + style = MaterialTheme.typography.subtitle1, + fontWeight = FontWeight.Bold + ) + Column( + verticalArrangement = Arrangement.spacedBy(8.dp), + modifier = Modifier + .fillMaxWidth() + .background( + color = MaterialTheme.colors.error.copy(alpha = 0.2f), + shape = RoundedCornerShape(dimensionResource(R.dimen.corner_radius_large)) + ) + .padding(16.dp) + ) { + errorMessage?.let { + Text( + text = it, + style = MaterialTheme.typography.subtitle1 + ) + } + + Text( + text = stringResource(id = R.string.shipping_label_purchased_failure_message), + style = MaterialTheme.typography.subtitle1 + ) + } + } +} + +@Composable +private fun LabelPaperSizeDropdownMenu( + availablePaperSizes: List, + selectedLabelPaperSizeOption: WooShippingLabelPaperSize, + onLabelPaperSizeOptionSelected: (WooShippingLabelPaperSize) -> Unit, + modifier: Modifier = Modifier, +) { + var expanded by remember { mutableStateOf(false) } + + Box { + Row( + modifier = Modifier + .clickable { expanded = true } + .then(modifier), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center + ) { + Text( + text = stringResource(selectedLabelPaperSizeOption.stringResource), + style = MaterialTheme.typography.subtitle1, + modifier = Modifier.weight(1f) + ) + Icon( + imageVector = Icons.Filled.ArrowDropDown, + contentDescription = stringResource( + R.string.sorted_by, + stringResource(selectedLabelPaperSizeOption.stringResource) + ), + tint = colorResource(id = R.color.woo_shipping_label_success) + + ) + } + DropdownMenu( + expanded = expanded, + onDismissRequest = { expanded = false }, + modifier = Modifier.align(alignment = Alignment.CenterEnd) + ) { + availablePaperSizes.forEach { option -> + DropdownMenuItem(onClick = { + onLabelPaperSizeOptionSelected(option) + expanded = false + }) { + Text( + text = stringResource(option.stringResource), + style = MaterialTheme.typography.subtitle1 + ) + } + } + } + } +} + +@Composable +private fun ShippingLabelLink( + text: String, + onClick: () -> Unit, + modifier: Modifier = Modifier, + showIcon: Boolean = false +) { + Row( + modifier = Modifier + .clickable { onClick() } + .then(modifier), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = text, + style = MaterialTheme.typography.subtitle1, + color = colorResource(id = R.color.woo_shipping_label_success), + fontWeight = FontWeight.Bold + ) + if (showIcon) { + Icon( + imageVector = Icons.AutoMirrored.Outlined.OpenInNew, + contentDescription = null, + tint = colorResource(id = R.color.woo_shipping_label_success), + modifier = Modifier.padding(start = 8.dp) + ) + } + } +} + +@Composable +private fun ReprintWarning( + modifier: Modifier = Modifier, +) { + Text( + text = stringResource(id = R.string.shipping_label_purchased_note), + style = MaterialTheme.typography.caption, + color = colorResource(id = R.color.color_on_surface_medium), + modifier = modifier, + ) +} + +@LightDarkThemePreviews +@Composable +private fun PurchaseInProgressPreview() { + WooThemeWithBackground { + val selectedLabelPaperSizeOption = remember { mutableStateOf(WooShippingLabelPaperSize.A4) } + ShippingLabelPurchaseStatusSection( + labelPurchaseStatus = LabelPurchaseStatus.PurchaseInProgress, + selectedLabelPaperSizeOption = selectedLabelPaperSizeOption.value, + onLabelPaperSizeOptionSelected = { selectedLabelPaperSizeOption.value = it }, + onPrintShippingLabelClicked = {}, + onTrackShipmentClicked = {}, + onSchedulePickUpClicked = {}, + onRefundClicked = {}, + onPrintCustomsClicked = {}, + onLearnMoreClicked = {}, + modifier = Modifier.padding(24.dp) + ) + } +} + +@LightDarkThemePreviews +@Composable +private fun PurchasedPreview() { + WooThemeWithBackground { + val selectedLabelPaperSizeOption = remember { mutableStateOf(WooShippingLabelPaperSize.A4) } + ShippingLabelPurchaseStatusSection( + labelPurchaseStatus = LabelPurchaseStatus.Purchased( + availablePrintSizes = listOf(WooShippingLabelPaperSize.A4, WooShippingLabelPaperSize.LABEL), + isCustomsFormAvailable = true, + isRefundAvailable = true + ), + selectedLabelPaperSizeOption = selectedLabelPaperSizeOption.value, + onLabelPaperSizeOptionSelected = { selectedLabelPaperSizeOption.value = it }, + onPrintShippingLabelClicked = {}, + onTrackShipmentClicked = {}, + onSchedulePickUpClicked = {}, + onRefundClicked = {}, + onPrintCustomsClicked = {}, + onLearnMoreClicked = {}, + modifier = Modifier.padding(24.dp) + ) + } +} + +@LightDarkThemePreviews +@Composable +private fun FailurePreview() { + WooThemeWithBackground { + ShippingLabelPurchaseStatusSection( + labelPurchaseStatus = LabelPurchaseStatus.Failed("An error occurred while purchasing the label."), + selectedLabelPaperSizeOption = WooShippingLabelPaperSize.A4, + onLabelPaperSizeOptionSelected = {}, + onPrintShippingLabelClicked = {}, + onTrackShipmentClicked = {}, + onSchedulePickUpClicked = {}, + onRefundClicked = {}, + onPrintCustomsClicked = {}, + onLearnMoreClicked = {}, + modifier = Modifier.padding(24.dp) + ) + } +} diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/wooshippinglabels/models/ShipmentUIModel.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/wooshippinglabels/models/ShipmentUIModel.kt index 3deaa19097e..0cf3bc21297 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/wooshippinglabels/models/ShipmentUIModel.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/wooshippinglabels/models/ShipmentUIModel.kt @@ -8,22 +8,10 @@ data class ShipmentUIModel( val localId: String, val remoteId: String? = null, val items: List, - val purchaseState: PurchaseState = PurchaseState.NoStarted, + val isPurchaseAPILoading: Boolean = false, val label: ShippingLabelModel? = null, ) : Parcelable { - /** - * Whether the shipment has been purchased or not. - * A shipment is considered purchased if the label was already purchased or is in the process of being purchased. - */ - val purchased: Boolean + val isPurchasedOrInProgress: Boolean get() = label?.status == ShippingLabelStatus.PURCHASE_IN_PROGRESS || (label?.status == ShippingLabelStatus.PURCHASED && label.refund == null) } - -@Parcelize -sealed class PurchaseState : Parcelable { - data object NoStarted : PurchaseState() - data object InProgress : PurchaseState() - data object Success : PurchaseState() - data object Error : PurchaseState() -} diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/wooshippinglabels/models/ShippingLabelModel.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/wooshippinglabels/models/ShippingLabelModel.kt index 1089187f0e2..9ebc1d399e8 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/wooshippinglabels/models/ShippingLabelModel.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/wooshippinglabels/models/ShippingLabelModel.kt @@ -39,7 +39,8 @@ data class ShippingLabelModel( val refund: Refund?, val products: List = emptyList(), val originAddress: Address? = null, - val destinationAddress: Address? = null + val destinationAddress: Address? = null, + val error: String? = null, ) : Parcelable { @IgnoredOnParcel val trackingLink: String diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/wooshippinglabels/networking/DTOs.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/wooshippinglabels/networking/DTOs.kt index 44448c53394..51ead1a5815 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/wooshippinglabels/networking/DTOs.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/wooshippinglabels/networking/DTOs.kt @@ -120,6 +120,7 @@ data class ShippingLabelDTO( @SerializedName("tracking") val tracking: String? = null, @SerializedName("refundable_amount") val refundableAmount: BigDecimal? = null, @SerializedName("status") val status: ShippingLabelStatus = ShippingLabelStatus.UNKNOWN, + @SerializedName("error") val error: String? = null, @SerializedName("created") val created: Long? = null, @SerializedName("carrier_id") val carrierId: String? = null, @SerializedName("service_name") val serviceName: String? = null, diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/wooshippinglabels/networking/WooShippingNetworkingMapper.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/wooshippinglabels/networking/WooShippingNetworkingMapper.kt index 061b13b2c71..60e04172053 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/wooshippinglabels/networking/WooShippingNetworkingMapper.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/wooshippinglabels/networking/WooShippingNetworkingMapper.kt @@ -75,6 +75,7 @@ class WooShippingNetworkingMapper @Inject constructor( tracking = shippingLabelDTO.tracking.orEmpty(), refundableAmount = shippingLabelDTO.refundableAmount ?: BigDecimal.ZERO, status = shippingLabelDTO.status, + error = shippingLabelDTO.error, created = shippingLabelDTO.created?.let { Date(it) }, carrierId = shippingLabelDTO.carrierId.orEmpty(), serviceName = shippingLabelDTO.serviceName.orEmpty(), diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/wooshippinglabels/split/GetSplitMovements.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/wooshippinglabels/split/GetSplitMovements.kt index db71d9426fa..8e02e894535 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/wooshippinglabels/split/GetSplitMovements.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/wooshippinglabels/split/GetSplitMovements.kt @@ -67,7 +67,9 @@ class GetSplitMovements @Inject constructor() { items: Map, isRemoveMovement: Boolean ): List { - val otherUnfulfilledKeys = items.filterNot { it.key == sourceShipmentKey || it.value.purchased }.keys.toList() + val otherUnfulfilledKeys = items.filterNot { + it.key == sourceShipmentKey || it.value.isPurchasedOrInProgress + }.keys.toList() if (isRemoveMovement) return otherUnfulfilledKeys val otherKeys = items.keys.filter { it != sourceShipmentKey } diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/wooshippinglabels/split/WooShippingSplitShipmentViewModel.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/wooshippinglabels/split/WooShippingSplitShipmentViewModel.kt index 4e79ece4ea5..4b1e06c9143 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/wooshippinglabels/split/WooShippingSplitShipmentViewModel.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/wooshippinglabels/split/WooShippingSplitShipmentViewModel.kt @@ -58,7 +58,7 @@ class WooShippingSplitShipmentViewModel @Inject constructor( currencyFormatter = currencyFormatter, dimensionUnit = storeOptions.dimensionUnit, weightUnit = storeOptions.weightUnit, - purchased = it.value.purchased + purchased = it.value.isPurchasedOrInProgress ) } @@ -174,8 +174,8 @@ class WooShippingSplitShipmentViewModel @Inject constructor( } private fun mergeUnfulfilledShipments() { - val fulfilledShipments = currentShipments.value.filter { it.value.purchased } - val unfulfilledShipments = currentShipments.value.filterNot { it.value.purchased } + val fulfilledShipments = currentShipments.value.filter { it.value.isPurchasedOrInProgress } + val unfulfilledShipments = currentShipments.value.filterNot { it.value.isPurchasedOrInProgress } val firstUnfulfilledShipmentKey = unfulfilledShipments.keys.first() val firstUnfulfilledShipmentValue = unfulfilledShipments.values.first() val mergedShipmentUIModel = firstUnfulfilledShipmentValue.copy( diff --git a/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/orders/wooshippinglabels/GetShipmentsTests.kt b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/orders/wooshippinglabels/GetShipmentsTests.kt index 3506722a77c..32595564811 100644 --- a/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/orders/wooshippinglabels/GetShipmentsTests.kt +++ b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/orders/wooshippinglabels/GetShipmentsTests.kt @@ -228,7 +228,7 @@ class GetShipmentsTests : BaseUnitTest() { val result = sut.invoke(order) val shipmentUIModel = result.first() - assertFalse(shipmentUIModel.purchased) + assertFalse(shipmentUIModel.isPurchasedOrInProgress) } @Test @@ -275,7 +275,7 @@ class GetShipmentsTests : BaseUnitTest() { val result = sut.invoke(order) val shipmentUIModel = result.first() - assertFalse(shipmentUIModel.purchased) + assertFalse(shipmentUIModel.isPurchasedOrInProgress) } @Test @@ -322,7 +322,7 @@ class GetShipmentsTests : BaseUnitTest() { val result = sut.invoke(order) val shipmentUIModel = result.first() - assertTrue(shipmentUIModel.purchased) + assertTrue(shipmentUIModel.isPurchasedOrInProgress) assertNotNull(shipmentUIModel.label) } } 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 08d7221f2ef..e9e3e831085 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 @@ -1499,7 +1499,7 @@ class WooShippingLabelCreationViewModelTest : BaseUnitTest() { }.last() as DataState viewState.shipmentUIList.first().let { - assertThat(it.purchased).isTrue() + assertThat(it.isReadOnly).isTrue() } }