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 0607546c94d..d7010ae798b 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 @@ -3,11 +3,6 @@ package com.woocommerce.android.ui.orders.wooshippinglabels import android.content.res.Configuration import android.os.Parcelable import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.core.tween -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut -import androidx.compose.animation.scaleIn -import androidx.compose.animation.scaleOut import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource @@ -24,9 +19,7 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.sizeIn import androidx.compose.foundation.layout.width -import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll import androidx.compose.material.BottomSheetScaffoldState import androidx.compose.material.Divider @@ -36,13 +29,8 @@ import androidx.compose.material.ModalBottomSheetState import androidx.compose.material.ModalBottomSheetValue import androidx.compose.material.Surface import androidx.compose.material.Text -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.outlined.CheckCircleOutline -import androidx.compose.material.icons.outlined.Close -import androidx.compose.material.icons.outlined.Info import androidx.compose.material.rememberModalBottomSheetState import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -60,16 +48,16 @@ import com.woocommerce.android.R import com.woocommerce.android.extensions.appendWithIfNotEmpty import com.woocommerce.android.ui.compose.animations.SkeletonView import com.woocommerce.android.ui.compose.theme.WooThemeWithBackground -import com.woocommerce.android.ui.orders.wooshippinglabels.address.AddressNotification import com.woocommerce.android.ui.orders.wooshippinglabels.address.AddressSectionLandscape import com.woocommerce.android.ui.orders.wooshippinglabels.address.AddressSectionPortrait import com.woocommerce.android.ui.orders.wooshippinglabels.address.AddressStatus import com.woocommerce.android.ui.orders.wooshippinglabels.address.getShipFrom import com.woocommerce.android.ui.orders.wooshippinglabels.address.getShipTo +import com.woocommerce.android.ui.orders.wooshippinglabels.components.NoticeBanner +import com.woocommerce.android.ui.orders.wooshippinglabels.components.NoticeBannerUiState import com.woocommerce.android.ui.orders.wooshippinglabels.models.DestinationShippingAddress import com.woocommerce.android.ui.orders.wooshippinglabels.models.OriginShippingAddress import com.woocommerce.android.util.StringUtils -import kotlinx.coroutines.delay import kotlinx.parcelize.Parcelize @Composable @@ -80,17 +68,14 @@ fun ShipmentDetails( shippingLines: List, shippingAddresses: WooShippingAddresses, shippingRateSummary: ShippingRateSummaryUI?, - addressNotification: AddressNotification?, - itnNotification: ItnMissingNotification? = null, modifier: Modifier = Modifier, + noticeBannerUiState: NoticeBannerUiState? = null, isShipmentDetailsExpanded: Boolean = false, onShipmentDetailsExpandedChange: (Boolean) -> Boolean, onEditDestinationAddress: (DestinationShippingAddress) -> Unit, - onEditOriginAddress: (OriginShippingAddress) -> Unit, destinationStatus: AddressStatus, markOrderComplete: Boolean = false, onMarkOrderCompleteChange: (Boolean) -> Unit = {}, - onDismissAddressNotification: () -> Unit = {}, handlerModifier: Modifier = Modifier, isReadOnly: Boolean = false ) { @@ -128,23 +113,7 @@ fun ShipmentDetails( .padding(top = dimensionResource(R.dimen.minor_100) * LocalConfiguration.current.fontScale) ) - ShippingAddressNotification( - addressNotification = addressNotification, - onDismiss = onDismissAddressNotification, - onAction = { - addressNotification?.let { - when { - it.isSuccess.not() && it.isDestinationNotification -> { - onEditDestinationAddress(shippingAddresses.shipTo) - } - it.isSuccess.not() && it.isDestinationNotification.not() -> { - onEditOriginAddress(shippingAddresses.shipFrom) - } - } - } - } - ) - ItnMissingNotification(itnNotification) + NoticeBanner(noticeBannerUiState) Spacer( modifier = Modifier.size( @@ -559,178 +528,6 @@ private fun ShipmentCostRow( } } -@Composable -private fun ItnMissingNotification( - itnNotification: ItnMissingNotification?, - modifier: Modifier = Modifier -) { - AnimatedVisibility( - visible = itnNotification != null, - enter = fadeIn( - animationSpec = tween( - durationMillis = 180 - ) - ) + scaleIn( - animationSpec = tween( - durationMillis = 180 - ) - ), - exit = fadeOut( - animationSpec = tween( - durationMillis = 90 - ) - ) + scaleOut( - animationSpec = tween( - durationMillis = 90 - ) - - ) - ) { - if (itnNotification == null) return@AnimatedVisibility - - val rowModifier = when (LocalConfiguration.current.orientation) { - Configuration.ORIENTATION_LANDSCAPE -> { - modifier.widthIn(max = 600.dp).fillMaxWidth() - } - else -> { - modifier.fillMaxWidth() - } - } - - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = rowModifier - .padding(dimensionResource(R.dimen.major_100)) - .background( - color = colorResource(R.color.woo_red_5), - shape = RoundedCornerShape(dimensionResource(R.dimen.corner_radius_large)) - ) - .padding(vertical = 8.dp, horizontal = 16.dp), - ) { - Icon( - imageVector = Icons.Outlined.Info, - tint = MaterialTheme.colors.error, - contentDescription = null - ) - Spacer(Modifier.size(dimensionResource(R.dimen.minor_50))) - Text( - text = itnNotification.errorMessage, - style = MaterialTheme.typography.body2, - color = MaterialTheme.colors.error, - modifier = Modifier.weight(1f) - ) - Icon( - imageVector = Icons.Outlined.Close, - tint = MaterialTheme.colors.error, - contentDescription = null, - modifier = Modifier.clickable { - itnNotification.onErrorDismissed() - } - ) - } - } -} - -@Composable -private fun ShippingAddressNotification( - addressNotification: AddressNotification?, - modifier: Modifier = Modifier, - onAction: () -> Unit = {}, - onDismiss: () -> Unit = {} -) { - AnimatedVisibility( - visible = addressNotification != null && addressNotification.isExpired().not(), - enter = fadeIn( - animationSpec = tween( - durationMillis = 180 - ) - ) + scaleIn( - animationSpec = tween( - durationMillis = 180 - ) - ), - exit = fadeOut( - animationSpec = tween( - durationMillis = 90 - ) - ) + scaleOut( - animationSpec = tween( - durationMillis = 90 - ) - - ) - ) { - if (addressNotification != null && addressNotification.isExpired().not()) { - if (addressNotification.expireAfter != null) { - LaunchedEffect(addressNotification) { - delay(addressNotification.expireAfter) - onDismiss() - } - } - - val color = if (addressNotification.isSuccess) { - colorResource(id = R.color.woo_shipping_label_success) - } else { - colorResource(id = R.color.woo_shipping_label_error) - } - - val backgroundColor = if (addressNotification.isSuccess) { - colorResource(id = R.color.woo_shipping_label_success_surface) - } else { - colorResource(id = R.color.woo_shipping_label_error_surface) - } - - val icon = if (addressNotification.isSuccess) { - Icons.Outlined.CheckCircleOutline - } else { - Icons.Outlined.Info - } - - val configuration = LocalConfiguration.current - val rowModifier = when (configuration.orientation) { - Configuration.ORIENTATION_LANDSCAPE -> { - modifier.widthIn(max = 600.dp).fillMaxWidth() - } - else -> { - modifier.fillMaxWidth() - } - } - - Row( - rowModifier - .padding(dimensionResource(R.dimen.major_100)) - .background( - color = backgroundColor, - shape = RoundedCornerShape(dimensionResource(R.dimen.corner_radius_large)) - ) - .clickable { onAction() } - .padding(vertical = 8.dp, horizontal = 16.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Icon( - imageVector = icon, - contentDescription = null, - tint = color, - modifier = Modifier.padding(end = 8.dp) - ) - Text( - text = stringResource(addressNotification.message), - color = color, - modifier = Modifier.weight(1f) - ) - if (addressNotification.isSuccess.not()) { - Icon( - imageVector = Icons.Outlined.Close, - contentDescription = null, - tint = color, - modifier = Modifier.clickable { onDismiss() } - ) - } - } - } - } -} - @Preview @Composable private fun ShipmentCostSectionPreview() { @@ -771,11 +568,6 @@ data class ShippingRateSummaryUI( val optionFee: String? = null ) : Parcelable -data class ItnMissingNotification( - val errorMessage: String, - val onErrorDismissed: () -> Unit -) - @Composable fun VerticalDivider( modifier: Modifier = Modifier, 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 304049b6807..0c96710e678 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 @@ -123,9 +123,7 @@ fun WooShippingLabelCreationScreen(viewModel: WooShippingLabelCreationViewModel) onEditDestinationAddress = viewModel::onEditDestinationAddress, destinationStatus = viewState.destinationStatus, actionSnackbar = viewModel.actionSnackbar, - onDismissAddressNotification = viewModel::onDismissAddressNotification, onSplitShipment = viewModel::onSplitShipment, - onDismissItnNotice = viewModel::onDismissItnNotice ) } @@ -165,11 +163,9 @@ fun WooShippingLabelCreationScreen( onEditCustomsClick: () -> Unit, onNavigateBack: () -> Unit, onEditDestinationAddress: (DestinationShippingAddress) -> Unit, - onDismissItnNotice: () -> Unit, destinationStatus: AddressStatus, modifier: Modifier = Modifier, actionSnackbar: ActionSnackbar? = null, - onDismissAddressNotification: () -> Unit = {}, onSplitShipment: () -> Unit = {} ) { val shipmentDetailsValue = if (uiState.isShipmentDetailsExpanded) { @@ -232,8 +228,6 @@ fun WooShippingLabelCreationScreen( onShipmentDetailsExpandedChange = onShipmentDetailsExpandedChange, onEditCustomsClick = onEditCustomsClick, onEditDestinationAddress = onEditDestinationAddress, - onDismissItnNotice = onDismissItnNotice, - onDismissAddressNotification = onDismissAddressNotification, destinationStatus = destinationStatus, actionSnackbar = actionSnackbar, onSplitShipment = onSplitShipment @@ -312,18 +306,15 @@ private fun LabelCreationScreenWithBottomSheet( onShipmentDetailsExpandedChange: (Boolean) -> Boolean, onEditCustomsClick: () -> Unit, onEditDestinationAddress: (DestinationShippingAddress) -> Unit, - onDismissItnNotice: () -> Unit, destinationStatus: AddressStatus, modifier: Modifier = Modifier, - onDismissAddressNotification: () -> Unit = {}, actionSnackbar: ActionSnackbar? = null, onSplitShipment: () -> Unit = {} ) { val snackbarHostState = remember { SnackbarHostState() } - val isItnMissing = customsState is ItnMissing val isPurchaseButtonDisplayed = shippingRatesState is WooShippingLabelCreationViewModel.ShippingRatesState.DataState - val requiresLargePeekHeight = isPurchaseButtonDisplayed || uiState.addressNotification != null || isItnMissing + val requiresLargePeekHeight = isPurchaseButtonDisplayed || uiState.noticeBannerUiState != null val bottomSheetPeekHeight = when { requiresLargePeekHeight -> 128.dp @@ -371,15 +362,7 @@ private fun LabelCreationScreenWithBottomSheet( onShipmentDetailsExpandedChange = onShipmentDetailsExpandedChange, onEditDestinationAddress = onEditDestinationAddress, destinationStatus = destinationStatus, - addressNotification = uiState.addressNotification, - onDismissAddressNotification = onDismissAddressNotification, - onEditOriginAddress = onEditOriginAddress, - itnNotification = takeIf { isItnMissing }?.let { - ItnMissingNotification( - errorMessage = stringResource(R.string.woo_shipping_labels_customs_itn_required_error), - onErrorDismissed = onDismissItnNotice - ) - } + noticeBannerUiState = uiState.noticeBannerUiState ) } }, @@ -894,7 +877,6 @@ private fun WooShippingLabelCreationScreenPreview() { onSelectAddressExpandedChange = { true }, onEditCustomsClick = {}, onEditDestinationAddress = {}, - onDismissItnNotice = {}, destinationStatus = AddressStatus.VERIFIED ) } 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 1ebcf36c0ce..10f29284157 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 @@ -19,13 +19,14 @@ import com.woocommerce.android.ui.orders.wooshippinglabels.WooShippingLabelCreat import com.woocommerce.android.ui.orders.wooshippinglabels.WooShippingLabelCreationViewModel.CustomsState.Unavailable 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.AddressNotification import com.woocommerce.android.ui.orders.wooshippinglabels.address.AddressStatus import com.woocommerce.android.ui.orders.wooshippinglabels.address.AddressValidationHelper -import com.woocommerce.android.ui.orders.wooshippinglabels.address.GetAddressNotification +import com.woocommerce.android.ui.orders.wooshippinglabels.address.ObserveShippingLabelNotice import com.woocommerce.android.ui.orders.wooshippinglabels.address.destination.VerifyDestinationAddress import com.woocommerce.android.ui.orders.wooshippinglabels.address.origin.FetchOriginAddresses import com.woocommerce.android.ui.orders.wooshippinglabels.address.origin.ObserveOriginAddresses +import com.woocommerce.android.ui.orders.wooshippinglabels.components.NoticeBannerUiState +import com.woocommerce.android.ui.orders.wooshippinglabels.components.NoticeType import com.woocommerce.android.ui.orders.wooshippinglabels.customs.CustomsData import com.woocommerce.android.ui.orders.wooshippinglabels.customs.ShouldRequireCustomsForm import com.woocommerce.android.ui.orders.wooshippinglabels.models.DestinationShippingAddress @@ -45,7 +46,6 @@ import com.woocommerce.android.viewmodel.ScopedViewModel import com.woocommerce.android.viewmodel.navArgs import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.CancellationException -import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableSharedFlow @@ -60,7 +60,6 @@ import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onStart -import kotlinx.coroutines.flow.runningFold import kotlinx.coroutines.flow.update import kotlinx.coroutines.joinAll import kotlinx.coroutines.launch @@ -83,7 +82,7 @@ class WooShippingLabelCreationViewModel @Inject constructor( private val shouldRequireCustoms: ShouldRequireCustomsForm, private val addressValidationHelper: AddressValidationHelper, private val verifyDestinationAddress: VerifyDestinationAddress, - private val getAddressNotification: GetAddressNotification + private val observeShippingLabelNotice: ObserveShippingLabelNotice ) : ScopedViewModel(savedState) { private val navArgs: WooShippingLabelCreationFragmentArgs by savedState.navArgs() @@ -143,7 +142,7 @@ class WooShippingLabelCreationViewModel @Inject constructor( launch { observeShippingRates() } launch { observeShippingRatesState() } launch { observeCustomsDataChanges() } - launch { observeNotifications() } + launch { observeNotices() } } private suspend fun getOrderInformation() { @@ -155,21 +154,33 @@ class WooShippingLabelCreationViewModel @Inject constructor( } } - @OptIn(FlowPreview::class, ExperimentalCoroutinesApi::class) - private suspend fun observeNotifications() { - shippingAddresses.filterNotNull() - .onStart { delay(NOTIFICATIONS_DELAY) } - .runningFold(initial = null as AddressNotification?) { previousNotification, addresses -> - getAddressNotification(addresses, previousNotification) - } - .collectLatest { notification -> - uiState.update { it.copy(addressNotification = notification) } + private suspend fun observeNotices() = observeShippingLabelNotice(shippingAddresses, customsState, viewModelScope) + .onStart { delay(NOTIFICATIONS_DELAY) } + .collectLatest { noticeBanner -> + uiState.update { + it.copy( + noticeBannerUiState = noticeBanner?.copy( + onTapped = { + when (noticeBanner.type) { + NoticeType.UNVERIFIED_ORIGIN_ADDRESS -> { + shippingAddresses.value?.shipFrom?.let { shipFrom -> onEditOriginAddress(shipFrom) } + } + + NoticeType.MISSING_DESTINATION_ADDRESS, NoticeType.UNVERIFIED_DESTINATION_ADDRESS -> { + shippingAddresses.value?.shipTo?.let { shipTo -> onEditDestinationAddress(shipTo) } + } + + NoticeType.MISSING_ITN -> { + onEditCustomsClick() + } + + else -> {} + } + } + ) + ) } - } - - fun onDismissAddressNotification() { - uiState.update { it.copy(addressNotification = null) } - } + } private suspend fun getStoreOptions() { observeStoreOptions().collectLatest { options -> @@ -481,10 +492,6 @@ class WooShippingLabelCreationViewModel @Inject constructor( return true } - fun onDismissItnNotice() { - customsState.value = Unavailable - } - private fun getTotalPrice(items: List): String { val totalPrice = items.sumOf { it.price } val formattedTotalPrice = items.firstOrNull()?.currency?.let { @@ -579,7 +586,9 @@ class WooShippingLabelCreationViewModel @Inject constructor( selectedRate.update { rate } } - fun onSplitShipment() { triggerEvent(StartSplitShipment) } + fun onSplitShipment() { + triggerEvent(StartSplitShipment) + } private fun sortShippingRates( option: ShippingSortOption, @@ -750,7 +759,7 @@ class WooShippingLabelCreationViewModel @Inject constructor( val markOrderComplete: Boolean, val isShipmentDetailsExpanded: Boolean, val isAddressSelectionExpanded: Boolean, - val addressNotification: AddressNotification? = null + val noticeBannerUiState: NoticeBannerUiState? = null ) data class ShippingRatesInfo( diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/wooshippinglabels/address/GetAddressNotification.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/wooshippinglabels/address/GetAddressNotification.kt deleted file mode 100644 index 8027a8c497c..00000000000 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/wooshippinglabels/address/GetAddressNotification.kt +++ /dev/null @@ -1,78 +0,0 @@ -package com.woocommerce.android.ui.orders.wooshippinglabels.address - -import androidx.annotation.StringRes -import com.woocommerce.android.R -import com.woocommerce.android.ui.orders.wooshippinglabels.WooShippingAddresses -import javax.inject.Inject - -class GetAddressNotification @Inject constructor(private val addressValidationHelper: AddressValidationHelper) { - - operator fun invoke( - addresses: WooShippingAddresses, - previousNotification: AddressNotification? = null - ): AddressNotification? { - return when { - addresses.shipFrom.isVerified.not() -> { - AddressNotification( - isSuccess = false, - message = R.string.woo_shipping_address_notification_origin_unverified, - isDestinationNotification = false - ) - } - - addressValidationHelper.isMissingDestinationAddress(addresses.shipTo.address) -> { - AddressNotification( - isSuccess = false, - message = R.string.woo_shipping_address_notification_destination_missing, - isDestinationNotification = true - ) - } - - addresses.shipTo.isVerified.not() -> { - AddressNotification( - isSuccess = false, - message = R.string.woo_shipping_address_notification_destination_unverified, - isDestinationNotification = true - ) - } - - addresses.shipTo.isVerified && - previousNotification?.let { it.isSuccess.not() && it.isDestinationNotification } == true -> { - AddressNotification( - isSuccess = true, - message = R.string.woo_shipping_address_notification_destination_verified, - expireAfter = SUCCESS_EXPIRE_TIME, - isDestinationNotification = true - ) - } - - addresses.shipFrom.isVerified && - previousNotification?.let { it.isSuccess.not() && it.isDestinationNotification.not() } == true -> { - AddressNotification( - isSuccess = true, - message = R.string.woo_shipping_address_notification_origin_verified, - expireAfter = SUCCESS_EXPIRE_TIME, - isDestinationNotification = false - ) - } - - else -> null - } - } - - companion object { - private const val SUCCESS_EXPIRE_TIME = 2_000L - } -} - -data class AddressNotification( - val isSuccess: Boolean, - @StringRes val message: Int, - val expireAfter: Long? = null, - private val timestamp: Long = System.currentTimeMillis(), - val isDestinationNotification: Boolean = false, -) { - fun isExpired(): Boolean = expireAfter?.let { - timestamp + it < System.currentTimeMillis() - } == true -} diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/wooshippinglabels/address/ObserveShippingLabelNotice.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/wooshippinglabels/address/ObserveShippingLabelNotice.kt new file mode 100644 index 00000000000..065180ef8c7 --- /dev/null +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/wooshippinglabels/address/ObserveShippingLabelNotice.kt @@ -0,0 +1,143 @@ +package com.woocommerce.android.ui.orders.wooshippinglabels.address + +import androidx.annotation.VisibleForTesting +import com.woocommerce.android.R +import com.woocommerce.android.ui.orders.wooshippinglabels.WooShippingAddresses +import com.woocommerce.android.ui.orders.wooshippinglabels.WooShippingLabelCreationViewModel.CustomsState +import com.woocommerce.android.ui.orders.wooshippinglabels.components.NoticeBannerUiState +import com.woocommerce.android.ui.orders.wooshippinglabels.components.NoticeType +import com.woocommerce.android.ui.orders.wooshippinglabels.components.NoticeType.MISSING_DESTINATION_ADDRESS +import com.woocommerce.android.ui.orders.wooshippinglabels.components.NoticeType.MISSING_ITN +import com.woocommerce.android.ui.orders.wooshippinglabels.components.NoticeType.UNVERIFIED_DESTINATION_ADDRESS +import com.woocommerce.android.ui.orders.wooshippinglabels.components.NoticeType.UNVERIFIED_ORIGIN_ADDRESS +import com.woocommerce.android.ui.orders.wooshippinglabels.components.NoticeType.VERIFIED_DESTINATION_ADDRESS +import com.woocommerce.android.ui.orders.wooshippinglabels.components.NoticeType.VERIFIED_ORIGIN_ADDRESS +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.launch +import javax.inject.Inject + +class ObserveShippingLabelNotice @Inject constructor(private val addressValidationHelper: AddressValidationHelper) { + private val isDismissedFlow = MutableStateFlow(NoticeType.entries.associateWith { false }) + private var previousNotice: NoticeType? = null + + operator fun invoke( + shippingAddresses: Flow, + customsState: Flow, + coroutineScope: CoroutineScope, + ) = combine( + shippingAddresses.filterNotNull(), + customsState, + isDismissedFlow + ) { addresses, customs, isDismissed -> + val noticeType = getNoticeType(addresses, customs, isDismissed) ?: return@combine null + getNoticeBannerUiState(noticeType).also { state -> + previousNotice = state.type + if (state.autoDismiss) { + // Dismiss the notice after AUTO_DISMISS_TIME passes + coroutineScope.launch { + delay(AUTO_DISMISS_TIME) + onDismissed(noticeType).invoke() + } + } + } + } + + @Suppress("CyclomaticComplexMethod") + private fun getNoticeType( + addresses: WooShippingAddresses, + customs: CustomsState, + isDismissed: Map + ) = when { + !addresses.shipFrom.isVerified && isDismissed[UNVERIFIED_ORIGIN_ADDRESS] == false -> { + UNVERIFIED_ORIGIN_ADDRESS + } + + addressValidationHelper.isMissingDestinationAddress(addresses.shipTo.address) && + isDismissed[MISSING_DESTINATION_ADDRESS] == false -> { + MISSING_DESTINATION_ADDRESS + } + + !addresses.shipTo.isVerified && isDismissed[MISSING_DESTINATION_ADDRESS] == false && + isDismissed[UNVERIFIED_DESTINATION_ADDRESS] == false -> { + UNVERIFIED_DESTINATION_ADDRESS + } + + addresses.shipFrom.isVerified && previousNotice == UNVERIFIED_ORIGIN_ADDRESS && + isDismissed[VERIFIED_ORIGIN_ADDRESS] == false -> { + VERIFIED_ORIGIN_ADDRESS + } + + addresses.shipTo.isVerified && isDismissed[VERIFIED_DESTINATION_ADDRESS] == false && + (previousNotice == MISSING_DESTINATION_ADDRESS || previousNotice == UNVERIFIED_DESTINATION_ADDRESS) -> { + VERIFIED_DESTINATION_ADDRESS + } + + customs is CustomsState.ItnMissing && isDismissed[MISSING_ITN] == false -> { + MISSING_ITN + } + + else -> null + } + + private fun getNoticeBannerUiState(noticeType: NoticeType) = when (noticeType) { + UNVERIFIED_ORIGIN_ADDRESS -> NoticeBannerUiState( + message = R.string.woo_shipping_address_notification_origin_unverified, + type = UNVERIFIED_ORIGIN_ADDRESS, + autoDismiss = false, + error = true, + onDismissed = onDismissed(UNVERIFIED_ORIGIN_ADDRESS) + ) + + MISSING_DESTINATION_ADDRESS -> NoticeBannerUiState( + message = R.string.woo_shipping_address_notification_destination_missing, + type = MISSING_DESTINATION_ADDRESS, + autoDismiss = false, + error = true, + onDismissed = onDismissed(MISSING_DESTINATION_ADDRESS) + ) + + UNVERIFIED_DESTINATION_ADDRESS -> NoticeBannerUiState( + message = R.string.woo_shipping_address_notification_destination_unverified, + type = UNVERIFIED_DESTINATION_ADDRESS, + autoDismiss = false, + error = true, + onDismissed = onDismissed(UNVERIFIED_DESTINATION_ADDRESS) + ) + + VERIFIED_ORIGIN_ADDRESS -> NoticeBannerUiState( + message = R.string.woo_shipping_address_notification_origin_verified, + type = VERIFIED_ORIGIN_ADDRESS, + autoDismiss = true, + error = false, + onDismissed = onDismissed(VERIFIED_ORIGIN_ADDRESS) + ) + + VERIFIED_DESTINATION_ADDRESS -> NoticeBannerUiState( + message = R.string.woo_shipping_address_notification_destination_verified, + type = VERIFIED_DESTINATION_ADDRESS, + autoDismiss = true, + error = false, + onDismissed = onDismissed(VERIFIED_DESTINATION_ADDRESS) + ) + + MISSING_ITN -> NoticeBannerUiState( + message = R.string.woo_shipping_labels_customs_itn_required_error, + type = MISSING_ITN, + autoDismiss = false, + error = true, + onDismissed = onDismissed(MISSING_ITN) + ) + } + + private fun onDismissed(noticeType: NoticeType) = { + isDismissedFlow.value = isDismissedFlow.value.toMutableMap().also { it[noticeType] = true } + } +} + +@VisibleForTesting +const val AUTO_DISMISS_TIME = 2_000L diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/wooshippinglabels/components/NoticeBanner.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/wooshippinglabels/components/NoticeBanner.kt new file mode 100644 index 00000000000..0ec1e08043b --- /dev/null +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/wooshippinglabels/components/NoticeBanner.kt @@ -0,0 +1,108 @@ +package com.woocommerce.android.ui.orders.wooshippinglabels.components + +import android.content.res.Configuration +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.tween +import androidx.compose.animation.fadeIn +import androidx.compose.animation.scaleIn +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +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.layout.widthIn +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.CheckCircleOutline +import androidx.compose.material.icons.outlined.Close +import androidx.compose.material.icons.outlined.Info +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.res.dimensionResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import com.woocommerce.android.R + +@Composable +fun NoticeBanner(noticeBannerUiState: NoticeBannerUiState?, modifier: Modifier = Modifier) { + AnimatedVisibility( + visible = noticeBannerUiState != null, + enter = fadeIn( + animationSpec = tween(durationMillis = 180) + ) + scaleIn( + animationSpec = tween(durationMillis = 180) + ) + ) { + if (noticeBannerUiState == null) return@AnimatedVisibility + + val icon = if (noticeBannerUiState.error) { + Icons.Outlined.Info + } else { + Icons.Outlined.CheckCircleOutline + } + + val color = if (noticeBannerUiState.error) { + R.color.woo_shipping_label_error + } else { + R.color.woo_shipping_label_success + } + + val backgroundColor = if (noticeBannerUiState.error) { + R.color.woo_shipping_label_error_surface + } else { + R.color.woo_shipping_label_success_surface + } + + val rowModifier = + if (LocalConfiguration.current.orientation == Configuration.ORIENTATION_LANDSCAPE) { + modifier + .widthIn(max = 600.dp) + .fillMaxWidth() + } else { + modifier.fillMaxWidth() + } + + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = rowModifier + .padding(dimensionResource(R.dimen.major_100)) + .background( + color = colorResource(backgroundColor), + shape = RoundedCornerShape(dimensionResource(R.dimen.corner_radius_large)) + ) + .clickable(enabled = noticeBannerUiState.onTapped != null) { + noticeBannerUiState.onTapped?.invoke() + } + .padding(vertical = 8.dp, horizontal = 16.dp), + ) { + Icon( + imageVector = icon, + tint = colorResource(color), + contentDescription = null + ) + Spacer(Modifier.size(dimensionResource(R.dimen.minor_100))) + Text( + text = stringResource(noticeBannerUiState.message), + style = MaterialTheme.typography.bodyMedium, + color = colorResource(color), + modifier = Modifier.weight(1f) + ) + if (!noticeBannerUiState.autoDismiss) { + Icon( + imageVector = Icons.Outlined.Close, + tint = colorResource(color), + contentDescription = null, + modifier = Modifier.clickable { noticeBannerUiState.onDismissed?.invoke() } + ) + } + } + } +} diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/wooshippinglabels/components/NoticeBannerUiState.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/wooshippinglabels/components/NoticeBannerUiState.kt new file mode 100644 index 00000000000..f811a47d002 --- /dev/null +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/wooshippinglabels/components/NoticeBannerUiState.kt @@ -0,0 +1,21 @@ +package com.woocommerce.android.ui.orders.wooshippinglabels.components + +import androidx.annotation.StringRes + +data class NoticeBannerUiState( + @StringRes val message: Int, + val type: NoticeType, + val autoDismiss: Boolean = false, + val error: Boolean, + val onTapped: (() -> Unit)? = null, + val onDismissed: (() -> Unit)? = null +) + +enum class NoticeType { + UNVERIFIED_ORIGIN_ADDRESS, + MISSING_DESTINATION_ADDRESS, + UNVERIFIED_DESTINATION_ADDRESS, + VERIFIED_ORIGIN_ADDRESS, + VERIFIED_DESTINATION_ADDRESS, + MISSING_ITN +} diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/wooshippinglabels/purchased/WooShippingLabelPurchasedScreen.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/wooshippinglabels/purchased/WooShippingLabelPurchasedScreen.kt index d8492fab9c1..28bb50e1202 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/wooshippinglabels/purchased/WooShippingLabelPurchasedScreen.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/wooshippinglabels/purchased/WooShippingLabelPurchasedScreen.kt @@ -115,9 +115,7 @@ internal fun WooShippingLabelPurchasedWithBottomSheetScreen( } true }, - addressNotification = null, onEditDestinationAddress = {}, - onEditOriginAddress = {}, destinationStatus = AddressStatus.VERIFIED ) } diff --git a/WooCommerce/src/main/res/values/colors_base.xml b/WooCommerce/src/main/res/values/colors_base.xml index f99000a2fdb..afb1879f288 100644 --- a/WooCommerce/src/main/res/values/colors_base.xml +++ b/WooCommerce/src/main/res/values/colors_base.xml @@ -332,8 +332,8 @@ Shipping labels --> - @color/woo_green_50 + @color/woo_green_60 @color/woo_green_0 - @color/woo_red_50 - @color/woo_red_5 + @color/woo_red_60 + @color/woo_red_0 diff --git a/WooCommerce/src/main/res/values/wc_colors_base.xml b/WooCommerce/src/main/res/values/wc_colors_base.xml index d9a88b32c3c..f86b65b6e3d 100644 --- a/WooCommerce/src/main/res/values/wc_colors_base.xml +++ b/WooCommerce/src/main/res/values/wc_colors_base.xml @@ -19,8 +19,9 @@ #880E4F #5C0935 + #F7EBEC #FACFD2 - #FFA6AB + #FFABAF #FF8085 #F86368 #D63638 @@ -50,11 +51,12 @@ #2FC39E #009172 - #EBF7F1 - #A4F5C8 - #59E38F + #E6F2E8 + #B8E6BF + #68DE86 #1ED15A #008A20 + #007017 #FFFFFF #0DFFFFFF 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 a9ff57afce9..0702a020f4f 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 @@ -15,11 +15,12 @@ import com.woocommerce.android.ui.orders.wooshippinglabels.WooShippingLabelCreat import com.woocommerce.android.ui.orders.wooshippinglabels.WooShippingLabelCreationViewModel.PurchaseState import com.woocommerce.android.ui.orders.wooshippinglabels.WooShippingLabelCreationViewModel.WooShippingViewState import com.woocommerce.android.ui.orders.wooshippinglabels.WooShippingLabelCreationViewModel.WooShippingViewState.DataState -import com.woocommerce.android.ui.orders.wooshippinglabels.address.AddressNotification import com.woocommerce.android.ui.orders.wooshippinglabels.address.AddressValidationHelper -import com.woocommerce.android.ui.orders.wooshippinglabels.address.GetAddressNotification +import com.woocommerce.android.ui.orders.wooshippinglabels.address.ObserveShippingLabelNotice import com.woocommerce.android.ui.orders.wooshippinglabels.address.destination.VerifyDestinationAddress import com.woocommerce.android.ui.orders.wooshippinglabels.address.origin.ObserveOriginAddresses +import com.woocommerce.android.ui.orders.wooshippinglabels.components.NoticeBannerUiState +import com.woocommerce.android.ui.orders.wooshippinglabels.components.NoticeType import com.woocommerce.android.ui.orders.wooshippinglabels.customs.ShouldRequireCustomsForm import com.woocommerce.android.ui.orders.wooshippinglabels.models.OriginShippingAddress import com.woocommerce.android.ui.orders.wooshippinglabels.models.PurchasedLabelData @@ -47,7 +48,6 @@ import kotlinx.coroutines.test.advanceUntilIdle import org.assertj.core.api.Assertions.assertThat import org.junit.Test import org.mockito.kotlin.any -import org.mockito.kotlin.anyOrNull import org.mockito.kotlin.doAnswer import org.mockito.kotlin.doReturn import org.mockito.kotlin.mock @@ -235,7 +235,7 @@ class WooShippingLabelCreationViewModelTest : BaseUnitTest() { private val purchaseShippingLabel: PurchaseShippingLabel = mock() private val observeStoreOptions: ObserveStoreOptions = mock() private val verifyDestinationAddress: VerifyDestinationAddress = mock() - private val getAddressNotification: GetAddressNotification = mock() + private val observeShippingLabelNotice: ObserveShippingLabelNotice = mock() private lateinit var sut: WooShippingLabelCreationViewModel @@ -253,7 +253,7 @@ class WooShippingLabelCreationViewModelTest : BaseUnitTest() { shouldRequireCustoms = shouldRequireCustomsForm, addressValidationHelper = addressValidationHelper, verifyDestinationAddress = verifyDestinationAddress, - getAddressNotification = getAddressNotification, + observeShippingLabelNotice = observeShippingLabelNotice, savedState = savedState ) } @@ -725,35 +725,6 @@ class WooShippingLabelCreationViewModelTest : BaseUnitTest() { assertThat(dataState.customsState).isEqualTo(CustomsState.ItnMissing) } - @Test - fun `ItnMissing is dismissed when onDismissItnNotice is called`() = testBlocking { - var dataState: DataState? = null - val order = OrderTestUtils.generateTestOrder(orderId = orderId).copy( - shippingLines = defaultShippingLines - ) - whenever(orderDetailRepository.getOrderById(any())) doReturn order - whenever(observeOriginAddresses()) doReturn flowOf(defaultOriginAddresses) - whenever(observeStoreOptions()) doReturn flowOf(defaultStoreOptions) - whenever(shouldRequireCustomsForm.invoke(any())) doReturn true - whenever(getShippableItems(any())) doReturn defaultShippableItems.map { it.copy(price = BigDecimal(10000)) } - - createViewModel() - - advanceUntilIdle() - - sut.viewState.asLiveData().observeForever { - dataState = it as? DataState - } - - assertThat(dataState?.customsState).isEqualTo(CustomsState.ItnMissing) - - sut.onDismissItnNotice() - - advanceUntilIdle() - - assertThat(dataState?.customsState).isEqualTo(CustomsState.Unavailable) - } - @Test fun `when onPurchaseShippingLabel succeed then return the label data`() = testBlocking { val order = OrderTestUtils.generateTestOrder(orderId = orderId).copy( @@ -983,62 +954,46 @@ class WooShippingLabelCreationViewModelTest : BaseUnitTest() { } @Test - fun `when there are address notifications then display the notification`() = testBlocking { + fun `when there are notices then display the notices`() = testBlocking { val order = OrderTestUtils.generateTestOrder(orderId = orderId) - val notification = AddressNotification( - isSuccess = false, + val notice = NoticeBannerUiState( message = R.string.woo_shipping_address_notification_destination_missing, - isDestinationNotification = false + type = NoticeType.MISSING_DESTINATION_ADDRESS, + error = true, ) whenever(orderDetailRepository.getOrderById(any())) doReturn order whenever(getShippableItems(any())) doReturn defaultShippableItems whenever(observeOriginAddresses()) doReturn flowOf(defaultOriginAddresses) whenever(observeStoreOptions()) doReturn flowOf(defaultStoreOptions) - whenever(getAddressNotification(any(), anyOrNull())) doReturn notification + whenever(observeShippingLabelNotice(any(), any(), any())) doReturn flowOf(notice) createViewModel() advanceUntilIdle() - val currentViewState = sut.viewState.value - assertThat(currentViewState).isInstanceOf(DataState::class.java) - val dataState = currentViewState as DataState - assertThat(dataState.uiState.addressNotification).isEqualTo(notification) + val dataState = sut.viewState.value as DataState + assertThat(dataState.uiState.noticeBannerUiState?.message).isEqualTo(notice.message) } @Test - fun `when an address notifications is displayed then dismissAddressNotification should dismiss the notification`() = - testBlocking { - val order = OrderTestUtils.generateTestOrder(orderId = orderId) - val notification = AddressNotification( - isSuccess = false, - message = R.string.woo_shipping_address_notification_destination_missing, - isDestinationNotification = false - ) - - whenever(orderDetailRepository.getOrderById(any())) doReturn order - whenever(getShippableItems(any())) doReturn defaultShippableItems - whenever(observeOriginAddresses()) doReturn flowOf(defaultOriginAddresses) - whenever(observeStoreOptions()) doReturn flowOf(defaultStoreOptions) - whenever(getAddressNotification(any(), anyOrNull())) doReturn notification - - createViewModel() + fun `when there are no notices then do not display the notices`() = testBlocking { + val order = OrderTestUtils.generateTestOrder(orderId = orderId) + val notice = null - advanceUntilIdle() + whenever(orderDetailRepository.getOrderById(any())) doReturn order + whenever(getShippableItems(any())) doReturn defaultShippableItems + whenever(observeOriginAddresses()) doReturn flowOf(defaultOriginAddresses) + whenever(observeStoreOptions()) doReturn flowOf(defaultStoreOptions) + whenever(observeShippingLabelNotice(any(), any(), any())) doReturn flowOf(notice) - var currentViewState = sut.viewState.value - assertThat(currentViewState).isInstanceOf(DataState::class.java) - var dataState = currentViewState as DataState - assertThat(dataState.uiState.addressNotification).isEqualTo(notification) + createViewModel() - sut.onDismissAddressNotification() + advanceUntilIdle() - currentViewState = sut.viewState.value - assertThat(currentViewState).isInstanceOf(DataState::class.java) - dataState = currentViewState as DataState - assertThat(dataState.uiState.addressNotification).isNull() - } + val dataState = sut.viewState.value as DataState + assertThat(dataState.uiState.noticeBannerUiState).isEqualTo(notice) + } @Test fun `when the destination address is missing then verify endpoint should not be called`() = testBlocking { diff --git a/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/orders/wooshippinglabels/address/GetAddressNotificationTests.kt b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/orders/wooshippinglabels/address/GetAddressNotificationTests.kt deleted file mode 100644 index aa0fe7f5fa2..00000000000 --- a/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/orders/wooshippinglabels/address/GetAddressNotificationTests.kt +++ /dev/null @@ -1,162 +0,0 @@ -package com.woocommerce.android.ui.orders.wooshippinglabels.address - -import com.woocommerce.android.R -import com.woocommerce.android.model.Address -import com.woocommerce.android.ui.orders.wooshippinglabels.WooShippingAddresses -import com.woocommerce.android.ui.orders.wooshippinglabels.models.DestinationShippingAddress -import com.woocommerce.android.ui.orders.wooshippinglabels.models.OriginShippingAddress -import com.woocommerce.android.viewmodel.BaseUnitTest -import kotlinx.coroutines.ExperimentalCoroutinesApi -import org.assertj.core.api.Assertions.assertThat -import org.mockito.Mockito.mock -import org.mockito.kotlin.doReturn -import org.mockito.kotlin.whenever -import kotlin.test.Test - -@OptIn(ExperimentalCoroutinesApi::class) -class GetAddressNotificationTests : BaseUnitTest() { - private val addressValidationHelper: AddressValidationHelper = mock() - - private val sut = GetAddressNotification(addressValidationHelper) - - private val defaultAddresses = WooShippingAddresses( - shipFrom = OriginShippingAddress.EMPTY.copy( - id = "1", - firstName = "John", - lastName = "Doe", - address1 = "123 Main St", - isVerified = true - ), - shipTo = DestinationShippingAddress( - address = Address.EMPTY.copy( - firstName = "John", - lastName = "Doe", - address1 = "123 Main St" - ), - isVerified = true - ), - originAddresses = emptyList() - ) - - @Test - fun `when addresses as no issues, then don't display any notification`() { - val result = sut.invoke(defaultAddresses) - assertThat(result).isNull() - } - - @Test - fun `when addresses as no issues and previous was a destination warning, then display destination success`() { - val previous = AddressNotification( - isSuccess = false, - message = R.string.woo_shipping_address_notification_destination_missing, - isDestinationNotification = true - ) - val result = sut.invoke(defaultAddresses, previous) - assertThat(result).isNotNull - assertThat(result!!.isSuccess).isTrue - assertThat(result.isDestinationNotification).isTrue - } - - @Test - fun `when addresses as no issues and previous was a origin warning, then display origin success`() { - val previous = AddressNotification( - isSuccess = false, - message = R.string.woo_shipping_address_notification_destination_missing, - isDestinationNotification = false - ) - - val result = sut.invoke(defaultAddresses, previous) - - assertThat(result).isNotNull - assertThat(result!!.isSuccess).isTrue - assertThat(result.isDestinationNotification).isFalse - } - - @Test - fun `when shipTo is not verified, then display destination not verified`() { - val addresses = defaultAddresses.copy(shipTo = defaultAddresses.shipTo.copy(isVerified = false)) - - val result = sut.invoke(addresses, null) - - assertThat(result).isNotNull - assertThat(result!!.isSuccess).isFalse - assertThat(result.isDestinationNotification).isTrue - assertThat(result.message).isEqualTo( - R.string.woo_shipping_address_notification_destination_unverified - ) - } - - @Test - fun `when shipFrom is not verified, then display origin not verified`() { - val addresses = defaultAddresses.copy(shipFrom = defaultAddresses.shipFrom.copy(isVerified = false)) - - val result = sut.invoke(addresses, null) - - assertThat(result).isNotNull - assertThat(result!!.isSuccess).isFalse - assertThat(result.isDestinationNotification).isFalse - assertThat(result.message).isEqualTo( - R.string.woo_shipping_address_notification_origin_unverified - ) - } - - @Test - fun `when shipTo is missing, then display destination missing`() { - val addresses = defaultAddresses.copy(shipTo = DestinationShippingAddress.EMPTY) - - whenever(addressValidationHelper.isMissingDestinationAddress(addresses.shipTo.address)) doReturn true - - val result = sut.invoke(addresses, null) - - assertThat(result).isNotNull - assertThat(result!!.isSuccess).isFalse - assertThat(result.isDestinationNotification).isTrue - assertThat(result.message).isEqualTo( - R.string.woo_shipping_address_notification_destination_missing - ) - } - - @Test - fun `testing both addresses with issues flow`() { - var addresses = defaultAddresses.copy( - shipTo = defaultAddresses.shipTo.copy(isVerified = false), - shipFrom = defaultAddresses.shipFrom.copy(isVerified = false) - ) - - // When address have issues, then display origin warnings first - - var result = sut.invoke(addresses, null) - - assertThat(result).isNotNull - assertThat(result!!.isSuccess).isFalse - assertThat(result.isDestinationNotification).isFalse - assertThat(result.message).isEqualTo( - R.string.woo_shipping_address_notification_origin_unverified - ) - - // Fix origin issue - addresses = defaultAddresses.copy( - shipTo = defaultAddresses.shipTo.copy(isVerified = false) - ) - - result = sut.invoke(addresses, result) - - assertThat(result).isNotNull - assertThat(result!!.isSuccess).isFalse - assertThat(result.isDestinationNotification).isTrue - assertThat(result.message).isEqualTo( - R.string.woo_shipping_address_notification_destination_unverified - ) - - // Fix destination issue - addresses = defaultAddresses - result = sut.invoke(addresses, result) - - assertThat(result).isNotNull - assertThat(result!!.isSuccess).isTrue - assertThat(result.isDestinationNotification).isTrue - assertThat(result.message).isEqualTo( - R.string.woo_shipping_address_notification_destination_verified - ) - } -} diff --git a/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/orders/wooshippinglabels/address/ObserveShippingLabelNoticeTests.kt b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/orders/wooshippinglabels/address/ObserveShippingLabelNoticeTests.kt new file mode 100644 index 00000000000..4114adfdecd --- /dev/null +++ b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/orders/wooshippinglabels/address/ObserveShippingLabelNoticeTests.kt @@ -0,0 +1,165 @@ +package com.woocommerce.android.ui.orders.wooshippinglabels.address + +import com.woocommerce.android.model.Address +import com.woocommerce.android.ui.orders.wooshippinglabels.WooShippingAddresses +import com.woocommerce.android.ui.orders.wooshippinglabels.WooShippingLabelCreationViewModel.CustomsState +import com.woocommerce.android.ui.orders.wooshippinglabels.components.NoticeType +import com.woocommerce.android.ui.orders.wooshippinglabels.models.DestinationShippingAddress +import com.woocommerce.android.ui.orders.wooshippinglabels.models.OriginShippingAddress +import com.woocommerce.android.viewmodel.BaseUnitTest +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.last +import kotlinx.coroutines.flow.take +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runTest +import org.assertj.core.api.Assertions.assertThat +import org.junit.Test +import org.mockito.Mockito.mock +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.whenever + +@OptIn(ExperimentalCoroutinesApi::class) +class ObserveShippingLabelNoticeTests : BaseUnitTest() { + private val addressValidationHelper: AddressValidationHelper = mock() + private val coroutineScope: CoroutineScope = TestScope(coroutinesTestRule.testDispatcher) + + private val sut = ObserveShippingLabelNotice(addressValidationHelper) + + private val defaultAddresses = WooShippingAddresses( + shipFrom = OriginShippingAddress.EMPTY.copy( + id = "1", + firstName = "John", + lastName = "Doe", + address1 = "123 Main St", + isVerified = true + ), + shipTo = DestinationShippingAddress( + address = Address.EMPTY.copy( + firstName = "John", + lastName = "Doe", + address1 = "123 Main St" + ), + isVerified = true + ), + originAddresses = emptyList() + ) + private val defaultAddressesFlow = flowOf(defaultAddresses) + private val customsFlow = flowOf(CustomsState.NotRequired) + + @Test + fun `when no issues, then don't display any notification`() = runTest { + val result = sut.invoke(defaultAddressesFlow, customsFlow, coroutineScope).first() + assertThat(result).isNull() + } + + @Test + fun `when missing origin address was displayed but it is now verified, then display verified notice`() = runTest { + val missingOriginAddress = defaultAddresses.copy(shipFrom = OriginShippingAddress.EMPTY) + val result = sut.invoke(flowOf(missingOriginAddress, defaultAddresses), customsFlow, coroutineScope) + .take(2) + .last() + assertThat(result?.type).isEqualTo(NoticeType.VERIFIED_ORIGIN_ADDRESS) + } + + @Test + fun `when missing destination address was displayed but it is now verified, then display verified notice`() = + runTest { + val missingDestinationAddress = defaultAddresses.copy(shipTo = DestinationShippingAddress.EMPTY) + val result = sut.invoke(flowOf(missingDestinationAddress, defaultAddresses), customsFlow, coroutineScope) + .take(2) + .last() + assertThat(result?.type).isEqualTo(NoticeType.VERIFIED_DESTINATION_ADDRESS) + } + + @Test + fun `when shipFrom is not verified, then display origin not verified`() = runTest { + val addresses = defaultAddresses.copy(shipFrom = defaultAddresses.shipFrom.copy(isVerified = false)) + + val result = sut.invoke(flowOf(addresses), customsFlow, coroutineScope).first() + + assertThat(result?.type).isEqualTo(NoticeType.UNVERIFIED_ORIGIN_ADDRESS) + } + + @Test + fun `when shipTo is not verified, then display destination not verified`() = runTest { + val addresses = defaultAddresses.copy(shipTo = defaultAddresses.shipTo.copy(isVerified = false)) + + val result = sut.invoke(flowOf(addresses), customsFlow, coroutineScope).first() + + assertThat(result?.type).isEqualTo(NoticeType.UNVERIFIED_DESTINATION_ADDRESS) + } + + @Test + fun `when shipTo is missing, then display destination missing`() = runTest { + val missingDestinationAddress = defaultAddresses.copy(shipTo = DestinationShippingAddress.EMPTY) + + whenever( + addressValidationHelper.isMissingDestinationAddress(missingDestinationAddress.shipTo.address) + ) doReturn true + + val result = sut.invoke(flowOf(missingDestinationAddress), customsFlow, coroutineScope).first() + + assertThat(result?.type).isEqualTo(NoticeType.MISSING_DESTINATION_ADDRESS) + } + + @Test + fun `testing both addresses and customs with issues flow`() = runTest { + var addresses = defaultAddresses.copy( + shipTo = defaultAddresses.shipTo.copy(isVerified = false), + shipFrom = defaultAddresses.shipFrom.copy(isVerified = false) + ) + val addressesFlow = MutableStateFlow(addresses) + val missingCustoms = MutableStateFlow(CustomsState.ItnMissing) + + // When address have issues, then display origin warnings first + var result = sut.invoke(addressesFlow, missingCustoms, coroutineScope) + + assertThat(result.first()?.type).isEqualTo(NoticeType.UNVERIFIED_ORIGIN_ADDRESS) + + // Fix origin issue + addresses = addresses.copy(shipFrom = defaultAddresses.shipFrom.copy(isVerified = true)) + addressesFlow.value = addresses + + assertThat(result.first()?.type).isEqualTo(NoticeType.UNVERIFIED_DESTINATION_ADDRESS) + + // Fix destination issue + addresses = defaultAddresses + addressesFlow.value = addresses + + assertThat(result.first()?.type).isEqualTo(NoticeType.VERIFIED_DESTINATION_ADDRESS) + + // Wait 2 seconds for auto dismiss + delay(AUTO_DISMISS_TIME) + + assertThat(result.first()?.type).isEqualTo(NoticeType.MISSING_ITN) + + // Fix ITN issue + missingCustoms.value = CustomsState.NotRequired + + assertThat(result.first()?.type).isNull() + } + + @Test + fun `when missing itn, then display itn notice`() = runTest { + val missingCustoms = flowOf(CustomsState.ItnMissing) + + val result = sut.invoke(defaultAddressesFlow, missingCustoms, coroutineScope).first() + assertThat(result?.type).isEqualTo(NoticeType.MISSING_ITN) + } + + @Test + fun `when notice dismissed, then display no notice`() = runTest { + val missingCustoms = flowOf(CustomsState.ItnMissing) + + val result = sut.invoke(defaultAddressesFlow, missingCustoms, coroutineScope) + + result.first()?.onDismissed?.invoke() + + assertThat(result.first()).isNull() + } +}