diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/main/MainActivity.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/main/MainActivity.kt index 8bdda3441726..10248ececffa 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/main/MainActivity.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/main/MainActivity.kt @@ -940,7 +940,8 @@ class MainActivity : binding.bottomNav.currentPosition = ORDERS binding.bottomNav.active(ORDERS.position) val action = OrderListFragmentDirections.actionOrderListFragmentToOrderCreationFragment( - OrderCreateEditViewModel.Mode.Creation + OrderCreateEditViewModel.Mode.Creation, + null ) navController.navigateSafely(action) } diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/OrderNavigator.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/OrderNavigator.kt index d76dcb9a1ddb..38960a890300 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/OrderNavigator.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/OrderNavigator.kt @@ -182,7 +182,8 @@ class OrderNavigator @Inject constructor() { is EditOrder -> { OrderDetailFragmentDirections .actionOrderDetailFragmentToOrderCreationFragment( - OrderCreateEditViewModel.Mode.Edit(target.orderId) + OrderCreateEditViewModel.Mode.Edit(target.orderId), + null ).let { fragment.findNavController().navigateSafely(it) } } is ViewCustomFields -> { diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/creation/OrderCreateEditViewModel.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/creation/OrderCreateEditViewModel.kt index 168c511109ef..c90ac665a1d5 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/creation/OrderCreateEditViewModel.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/creation/OrderCreateEditViewModel.kt @@ -43,6 +43,7 @@ import com.woocommerce.android.analytics.AnalyticsTracker.Companion.OrderNoteTyp import com.woocommerce.android.analytics.AnalyticsTracker.Companion.VALUE_FLOW_CREATION import com.woocommerce.android.analytics.AnalyticsTracker.Companion.VALUE_FLOW_EDITING import com.woocommerce.android.analytics.AnalyticsTrackerWrapper +import com.woocommerce.android.extensions.isNotNullOrEmpty import com.woocommerce.android.extensions.runWithContext import com.woocommerce.android.model.Address import com.woocommerce.android.model.Order @@ -172,6 +173,11 @@ class OrderCreateEditViewModel @Inject constructor( ) } monitorOrderChanges() + // Presence of barcode indicates that this screen was called from the + // Order listing screen after scanning the barcode. + if (args.sku.isNotNullOrEmpty()) { + fetchProductBySKU(args.sku!!) + } } is Mode.Edit -> { viewModelScope.launch { @@ -189,7 +195,6 @@ class OrderCreateEditViewModel @Inject constructor( } } } - fun onCustomerNoteEdited(newNote: String) { _orderDraft.value.let { order -> tracker.track( diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/list/OrderListFragment.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/list/OrderListFragment.kt index d54610cf7b11..dda1023ade2e 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/list/OrderListFragment.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/list/OrderListFragment.kt @@ -49,6 +49,7 @@ import com.woocommerce.android.ui.jitm.JitmFragment import com.woocommerce.android.ui.main.MainActivity import com.woocommerce.android.ui.main.MainNavigationRouter import com.woocommerce.android.ui.orders.OrderStatusUpdateSource +import com.woocommerce.android.ui.orders.creation.IsAddProductViaBarcodeScanningEnabled import com.woocommerce.android.ui.orders.creation.OrderCreateEditViewModel import com.woocommerce.android.ui.orders.list.OrderListViewModel.OrderListEvent.ShowErrorSnack import com.woocommerce.android.ui.orders.list.OrderListViewModel.OrderListEvent.ShowOrderFilters @@ -87,6 +88,8 @@ class OrderListFragment : internal lateinit var currencyFormatter: CurrencyFormatter @Inject lateinit var feedbackPrefs: FeedbackPrefs + @Inject + lateinit var isAddProductViaBarcodeScanningEnabled: IsAddProductViaBarcodeScanningEnabled private val viewModel: OrderListViewModel by viewModels() private var snackBar: Snackbar? = null @@ -144,6 +147,9 @@ class OrderListFragment : orderListMenu = menu searchMenuItem = menu.findItem(R.id.menu_search) + // TODO Remove the barcode setting visibility logic after the feature is in production. + val barcodeOption = menu.findItem(R.id.menu_barcode) + barcodeOption.isVisible = isAddProductViaBarcodeScanningEnabled() searchView = searchMenuItem?.actionView as SearchView? searchView?.queryHint = getSearchQueryHint() } @@ -256,6 +262,10 @@ class OrderListFragment : enableSearchListeners() true } + R.id.menu_barcode -> { + viewModel.startScan() + true + } else -> false } } @@ -349,6 +359,9 @@ class OrderListFragment : is OrderListViewModel.OrderListEvent.ShowIPPDismissConfirmationDialog -> { showIPPFeedbackDismissConfirmationDialog() } + is OrderListViewModel.OrderListEvent.OnBarcodeScanned -> { + openOrderCreationFragment(event.code) + } else -> event.isHandled = false } } @@ -451,12 +464,13 @@ class OrderListFragment : findNavController().navigateSafely(R.id.action_orderListFragment_to_orderFilterListFragment) } - private fun openOrderCreationFragment() { + private fun openOrderCreationFragment(code: String? = null) { OrderDurationRecorder.startRecording() AnalyticsTracker.track(AnalyticsEvent.ORDERS_ADD_NEW) findNavController().navigateSafely( OrderListFragmentDirections.actionOrderListFragmentToOrderCreationFragment( - OrderCreateEditViewModel.Mode.Creation + OrderCreateEditViewModel.Mode.Creation, + code ) ) } diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/list/OrderListViewModel.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/list/OrderListViewModel.kt index cd7d19417b8e..0a641b6639c5 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/list/OrderListViewModel.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/list/OrderListViewModel.kt @@ -36,6 +36,8 @@ import com.woocommerce.android.notifications.NotificationChannelType import com.woocommerce.android.tools.NetworkStatus import com.woocommerce.android.tools.SelectedSite import com.woocommerce.android.ui.orders.OrderStatusUpdateSource +import com.woocommerce.android.ui.orders.creation.CodeScanner +import com.woocommerce.android.ui.orders.creation.CodeScannerStatus import com.woocommerce.android.ui.orders.details.OrderDetailRepository import com.woocommerce.android.ui.orders.filters.domain.GetSelectedOrderFiltersCount import com.woocommerce.android.ui.orders.filters.domain.GetWCOrderListDescriptorWithFilters @@ -105,6 +107,7 @@ class OrderListViewModel @Inject constructor( private val analyticsTracker: AnalyticsTrackerWrapper, private val appPrefs: AppPrefs, private val feedbackPrefs: FeedbackPrefs, + private val codeScanner: CodeScanner, ) : ScopedViewModel(savedState), LifecycleOwner { private val lifecycleRegistry: LifecycleRegistry by lazy { LifecycleRegistry(this) @@ -279,6 +282,21 @@ class OrderListViewModel @Inject constructor( } } + fun startScan() { + launch { + codeScanner.startScan().collect { status -> + when (status) { + is CodeScannerStatus.Failure -> { + // TODO handle failure case + } + is CodeScannerStatus.Success -> { + triggerEvent(OrderListEvent.OnBarcodeScanned(status.code)) + } + } + } + } + } + /** * Track user clicked to open an order and the status of that order, along with some * data about the order custom fields @@ -718,6 +736,8 @@ class OrderListViewModel @Inject constructor( object ShowIPPDismissConfirmationDialog : OrderListEvent() data class OpenIPPFeedbackSurveyLink(val url: String) : OrderListEvent() + + data class OnBarcodeScanned(val code: String) : OrderListEvent() } @Parcelize diff --git a/WooCommerce/src/main/res/menu/menu_order_list_fragment.xml b/WooCommerce/src/main/res/menu/menu_order_list_fragment.xml index 9520bf2858f0..45c20e3ce89e 100644 --- a/WooCommerce/src/main/res/menu/menu_order_list_fragment.xml +++ b/WooCommerce/src/main/res/menu/menu_order_list_fragment.xml @@ -1,6 +1,11 @@ + + + + \u2014 Share Search + Scan Barcode Clear Done Back diff --git a/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/orders/OrderListViewModelTest.kt b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/orders/OrderListViewModelTest.kt index e3e8ba78ca1f..b68c3baf69a6 100644 --- a/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/orders/OrderListViewModelTest.kt +++ b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/orders/OrderListViewModelTest.kt @@ -18,6 +18,8 @@ import com.woocommerce.android.model.RequestResult import com.woocommerce.android.notifications.NotificationChannelType import com.woocommerce.android.tools.NetworkStatus import com.woocommerce.android.tools.SelectedSite +import com.woocommerce.android.ui.orders.creation.CodeScanner +import com.woocommerce.android.ui.orders.creation.CodeScannerStatus import com.woocommerce.android.ui.orders.details.OrderDetailRepository import com.woocommerce.android.ui.orders.filters.domain.GetSelectedOrderFiltersCount import com.woocommerce.android.ui.orders.filters.domain.GetWCOrderListDescriptorWithFilters @@ -98,6 +100,7 @@ class OrderListViewModelTest : BaseUnitTest() { private val analyticsTracker: AnalyticsTrackerWrapper = mock() private val appPrefs = mock() private val feedbackPrefs = mock() + private val codeScanner = mock() @Before fun setup() = testBlocking { @@ -148,6 +151,7 @@ class OrderListViewModelTest : BaseUnitTest() { analyticsTracker = analyticsTracker, appPrefs = appPrefs, feedbackPrefs = feedbackPrefs, + codeScanner = codeScanner, ) @Test @@ -954,6 +958,38 @@ class OrderListViewModelTest : BaseUnitTest() { assertEquals(IPPSurveyFeedbackBannerState.Hidden, viewModel.viewState.ippFeedbackBannerState) } + // region barcode scanner + + @Test + fun `when code scanner succeeds, then trigger proper event`() { + whenever(codeScanner.startScan()).thenAnswer { + flow { + emit(CodeScannerStatus.Success("12345")) + } + } + + viewModel = createViewModel() + viewModel.startScan() + + assertThat(viewModel.event.value).isInstanceOf(OrderListViewModel.OrderListEvent.OnBarcodeScanned::class.java) + } + + @Test + fun `when code scanner succeeds, then trigger event with proper sku`() { + whenever(codeScanner.startScan()).thenAnswer { + flow { + emit(CodeScannerStatus.Success("12345")) + } + } + + viewModel = createViewModel() + viewModel.startScan() + + assertThat(viewModel.event.value).isEqualTo(OrderListViewModel.OrderListEvent.OnBarcodeScanned("12345")) + } + + //endregion + private companion object { const val ANY_SEARCH_QUERY = "search query" diff --git a/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/orders/creation/CreationFocusedOrderCreateEditViewModelTest.kt b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/orders/creation/CreationFocusedOrderCreateEditViewModelTest.kt index 8abd54e884a5..d66e087bd934 100644 --- a/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/orders/creation/CreationFocusedOrderCreateEditViewModelTest.kt +++ b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/orders/creation/CreationFocusedOrderCreateEditViewModelTest.kt @@ -4,6 +4,7 @@ import com.woocommerce.android.R import com.woocommerce.android.analytics.AnalyticsEvent import com.woocommerce.android.analytics.AnalyticsTracker import com.woocommerce.android.analytics.AnalyticsTracker.Companion.VALUE_FLOW_CREATION +import com.woocommerce.android.initSavedStateHandle import com.woocommerce.android.model.Address import com.woocommerce.android.model.Order import com.woocommerce.android.ui.orders.OrderNavigationTarget.ViewOrderStatusSelector @@ -21,6 +22,7 @@ import com.woocommerce.android.ui.orders.creation.navigation.OrderCreateEditNavi import com.woocommerce.android.ui.orders.creation.navigation.OrderCreateEditNavigationTarget.SelectItems import com.woocommerce.android.ui.orders.creation.navigation.OrderCreateEditNavigationTarget.ShowCreatedOrder import com.woocommerce.android.ui.orders.creation.navigation.OrderCreateEditNavigationTarget.ShowProductDetails +import com.woocommerce.android.ui.products.models.SiteParameters import com.woocommerce.android.ui.products.selector.ProductSelectorViewModel import com.woocommerce.android.viewmodel.MultiLiveEvent.Event import com.woocommerce.android.viewmodel.MultiLiveEvent.Event.Exit @@ -34,14 +36,18 @@ import org.junit.Test import org.mockito.kotlin.any import org.mockito.kotlin.doReturn import org.mockito.kotlin.mock +import org.mockito.kotlin.times import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever import org.wordpress.android.fluxc.network.rest.wpcom.wc.order.CoreOrderStatus +import org.wordpress.android.fluxc.store.WCProductStore import java.math.BigDecimal import java.util.function.Consumer @ExperimentalCoroutinesApi class CreationFocusedOrderCreateEditViewModelTest : UnifiedOrderEditViewModelTest() { override val mode: Mode = Creation + override val sku: String = "123" override val tracksFlow: String = VALUE_FLOW_CREATION companion object { @@ -1109,4 +1115,56 @@ class CreationFocusedOrderCreateEditViewModelTest : UnifiedOrderEditViewModelTes mapOf(AnalyticsTracker.KEY_FLOW to VALUE_FLOW_CREATION) ) } + + @Test + fun `given sku, when view model init, then fetch product information`() { + testBlocking { + val navArgs = OrderCreateEditFormFragmentArgs( + OrderCreateEditViewModel.Mode.Creation, "123" + ).initSavedStateHandle() + whenever(parameterRepository.getParameters("parameters_key", navArgs)).thenReturn( + SiteParameters( + currencyCode = "", + currencySymbol = null, + currencyFormattingParameters = null, + weightUnit = null, + dimensionUnit = null, + gmtOffset = 0F + ) + ) + + createSut(navArgs) + + verify(productListRepository, times(2)).searchProductList( + "123", + WCProductStore.SkuSearchOptions.ExactSearch + ) + } + } + + @Test + fun `given empty sku, when view model init, then do not fetch product information`() { + testBlocking { + val navArgs = OrderCreateEditFormFragmentArgs( + OrderCreateEditViewModel.Mode.Creation, "" + ).initSavedStateHandle() + whenever(parameterRepository.getParameters("parameters_key", navArgs)).thenReturn( + SiteParameters( + currencyCode = "", + currencySymbol = null, + currencyFormattingParameters = null, + weightUnit = null, + dimensionUnit = null, + gmtOffset = 0F + ) + ) + + createSut(navArgs) + + verify(productListRepository, times(1)).searchProductList( + "123", + WCProductStore.SkuSearchOptions.ExactSearch + ) + } + } } diff --git a/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/orders/creation/EditFocusedOrderCreateEditViewModelTest.kt b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/orders/creation/EditFocusedOrderCreateEditViewModelTest.kt index d42ce7479d1d..5244b649b5f4 100644 --- a/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/orders/creation/EditFocusedOrderCreateEditViewModelTest.kt +++ b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/orders/creation/EditFocusedOrderCreateEditViewModelTest.kt @@ -31,6 +31,7 @@ import org.mockito.kotlin.verify @RunWith(MockitoJUnitRunner.Silent::class) class EditFocusedOrderCreateEditViewModelTest : UnifiedOrderEditViewModelTest() { override val mode: Mode = Edit(defaultOrderValue.id) + override val sku: String = "123" override val tracksFlow: String = VALUE_FLOW_EDITING override fun initMocksForAnalyticsWithOrder(order: Order) { diff --git a/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/orders/creation/UnifiedOrderEditViewModelTest.kt b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/orders/creation/UnifiedOrderEditViewModelTest.kt index a4e9476c116b..fa48e69c16a5 100644 --- a/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/orders/creation/UnifiedOrderEditViewModelTest.kt +++ b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/orders/creation/UnifiedOrderEditViewModelTest.kt @@ -57,7 +57,7 @@ abstract class UnifiedOrderEditViewModelTest : BaseUnitTest() { private lateinit var determineMultipleLinesContext: DetermineMultipleLinesContext protected lateinit var tracker: AnalyticsTrackerWrapper private lateinit var codeScanner: CodeScanner - private lateinit var productListRepository: ProductListRepository + lateinit var productListRepository: ProductListRepository protected val defaultOrderValue = Order.EMPTY.copy(id = 123) @@ -68,12 +68,13 @@ abstract class UnifiedOrderEditViewModelTest : BaseUnitTest() { } protected abstract val mode: OrderCreateEditViewModel.Mode + protected abstract val sku: String private fun initMocks() { val defaultOrderItem = createOrderItem() val emptyOrder = Order.EMPTY viewState = OrderCreateEditViewModel.ViewState() - savedState = spy(OrderCreateEditFormFragmentArgs(mode).toSavedStateHandle()) { + savedState = spy(OrderCreateEditFormFragmentArgs(mode, sku).toSavedStateHandle()) { on { getLiveData(viewState.javaClass.name, viewState) } doReturn MutableLiveData(viewState) on { getLiveData(eq(Order.EMPTY.javaClass.name), any()) } doReturn MutableLiveData(emptyOrder) } @@ -406,7 +407,7 @@ abstract class UnifiedOrderEditViewModelTest : BaseUnitTest() { ) ) ) - whenever(createOrderItemUseCase.invoke(10L)).thenReturn( + whenever(createOrderItemUseCase.invoke(0L, 10L)).thenReturn( createOrderItem(10L) ) var newOrder: Order? = null @@ -422,11 +423,11 @@ abstract class UnifiedOrderEditViewModelTest : BaseUnitTest() { //endregion - protected fun createSut() { + protected fun createSut(savedStateHandle: SavedStateHandle = savedState) { autoSyncPriceModifier = AutoSyncPriceModifier(createUpdateOrderUseCase) autoSyncOrder = AutoSyncOrder(createUpdateOrderUseCase) sut = OrderCreateEditViewModel( - savedState = savedState, + savedState = savedStateHandle, dispatchers = coroutinesTestRule.testDispatchers, orderDetailRepository = orderDetailRepository, orderCreateEditRepository = orderCreateEditRepository,