diff --git a/WooCommerce-Wear/src/main/AndroidManifest.xml b/WooCommerce-Wear/src/main/AndroidManifest.xml index 31b0cfd5552..69931c330a8 100644 --- a/WooCommerce-Wear/src/main/AndroidManifest.xml +++ b/WooCommerce-Wear/src/main/AndroidManifest.xml @@ -55,6 +55,14 @@ android:pathPrefix="/orders-data" android:scheme="wear" /> + + + + + + get() = flow { + emit(false) + delay(TIMEOUT_MILLIS) + emit(true) + } + +fun Flow.combineWithTimeout( + transform: (data: T, isTimeout: Boolean) -> R +) = combine(this, timeoutFlow, transform) diff --git a/WooCommerce-Wear/src/main/java/com/woocommerce/android/phone/PhoneConnectionRepository.kt b/WooCommerce-Wear/src/main/java/com/woocommerce/android/phone/PhoneConnectionRepository.kt index 55a2a14c6c9..edaa6c1cfe3 100644 --- a/WooCommerce-Wear/src/main/java/com/woocommerce/android/phone/PhoneConnectionRepository.kt +++ b/WooCommerce-Wear/src/main/java/com/woocommerce/android/phone/PhoneConnectionRepository.kt @@ -9,6 +9,7 @@ import com.woocommerce.android.ui.login.LoginRepository import com.woocommerce.android.ui.orders.OrdersRepository import com.woocommerce.android.ui.stats.datasource.StatsRepository import com.woocommerce.commons.wear.DataPath.ORDERS_DATA +import com.woocommerce.commons.wear.DataPath.ORDER_PRODUCTS_DATA import com.woocommerce.commons.wear.DataPath.SITE_DATA import com.woocommerce.commons.wear.DataPath.STATS_DATA import com.woocommerce.commons.wear.MessagePath @@ -38,6 +39,7 @@ class PhoneConnectionRepository @Inject constructor( SITE_DATA.value -> loginRepository.receiveStoreDataFromPhone(dataMap) STATS_DATA.value -> statsRepository.receiveStatsDataFromPhone(dataMap) ORDERS_DATA.value -> ordersRepository.receiveOrdersDataFromPhone(dataMap) + ORDER_PRODUCTS_DATA.value -> ordersRepository.receiveOrderProductsDataFromPhone(dataMap) else -> Log.d(TAG, "Unknown path data received") } } diff --git a/WooCommerce-Wear/src/main/java/com/woocommerce/android/ui/Navigation.kt b/WooCommerce-Wear/src/main/java/com/woocommerce/android/ui/Navigation.kt index b9c6d1858a4..8209dc1e638 100644 --- a/WooCommerce-Wear/src/main/java/com/woocommerce/android/ui/Navigation.kt +++ b/WooCommerce-Wear/src/main/java/com/woocommerce/android/ui/Navigation.kt @@ -3,15 +3,21 @@ package com.woocommerce.android.ui import androidx.compose.runtime.Composable import androidx.hilt.navigation.compose.hiltViewModel import androidx.navigation.NavHostController +import androidx.navigation.NavType import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController +import androidx.navigation.navArgument +import com.woocommerce.android.ui.NavArgs.ORDER_ID import com.woocommerce.android.ui.NavRoutes.LOGIN import com.woocommerce.android.ui.NavRoutes.MY_STORE +import com.woocommerce.android.ui.NavRoutes.ORDER_DETAILS import com.woocommerce.android.ui.login.LoginScreen import com.woocommerce.android.ui.login.LoginViewModel import com.woocommerce.android.ui.mystore.MyStoreScreen -import com.woocommerce.android.ui.orders.OrdersListViewModel +import com.woocommerce.android.ui.orders.details.OrderDetailsScreen +import com.woocommerce.android.ui.orders.details.OrderDetailsViewModel +import com.woocommerce.android.ui.orders.list.OrdersListViewModel import com.woocommerce.android.ui.stats.StoreStatsViewModel @Composable @@ -39,10 +45,30 @@ fun WooWearNavHost( ordersListViewModel = ordersListViewModel ) } + composable( + route = ORDER_DETAILS.withArgs(ORDER_ID), + arguments = listOf(navArgument(ORDER_ID.key) { type = NavType.LongType }) + ) { + val viewModel: OrderDetailsViewModel = hiltViewModel() + OrderDetailsScreen(viewModel) + } } } enum class NavRoutes(val route: String) { LOGIN("login"), - MY_STORE("myStore") + MY_STORE("myStore"), + ORDER_DETAILS("orderDetails"); + + fun withArgs(args: Any): String { + return "$route/$args" + } + + fun withArgs(navArgs: NavArgs): String { + return "$route/{${navArgs.key}}" + } +} + +enum class NavArgs(val key: String) { + ORDER_ID("orderId") } diff --git a/WooCommerce-Wear/src/main/java/com/woocommerce/android/ui/login/ObserveLoginRequest.kt b/WooCommerce-Wear/src/main/java/com/woocommerce/android/ui/login/ObserveLoginRequest.kt index 00a723d190f..d120a85a91e 100644 --- a/WooCommerce-Wear/src/main/java/com/woocommerce/android/ui/login/ObserveLoginRequest.kt +++ b/WooCommerce-Wear/src/main/java/com/woocommerce/android/ui/login/ObserveLoginRequest.kt @@ -1,33 +1,21 @@ package com.woocommerce.android.ui.login +import com.woocommerce.android.extensions.combineWithTimeout import com.woocommerce.android.ui.login.ObserveLoginRequest.LoginRequestState.Logged import com.woocommerce.android.ui.login.ObserveLoginRequest.LoginRequestState.Timeout import com.woocommerce.android.ui.login.ObserveLoginRequest.LoginRequestState.Waiting -import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.flow import javax.inject.Inject class ObserveLoginRequest @Inject constructor( private val loginRepository: LoginRepository ) { - operator fun invoke() = combine( - loginRepository.isSiteAvailable, - timeoutFlow - ) { isSiteAvailable, isTimeout -> - when { - isSiteAvailable -> Logged - isTimeout.not() -> Waiting - else -> Timeout - } - } - - private val timeoutFlow: Flow - get() = flow { - emit(false) - delay(TIMEOUT_MILLIS) - emit(true) + operator fun invoke() = loginRepository.isSiteAvailable + .combineWithTimeout { isSiteAvailable, isTimeout -> + when { + isSiteAvailable -> Logged + isTimeout.not() -> Waiting + else -> Timeout + } } enum class LoginRequestState { @@ -35,8 +23,4 @@ class ObserveLoginRequest @Inject constructor( Waiting, Timeout } - - companion object { - const val TIMEOUT_MILLIS = 20000L - } } diff --git a/WooCommerce-Wear/src/main/java/com/woocommerce/android/ui/mystore/MyStoreScreen.kt b/WooCommerce-Wear/src/main/java/com/woocommerce/android/ui/mystore/MyStoreScreen.kt index b90da867e1d..c3b7f5239ee 100644 --- a/WooCommerce-Wear/src/main/java/com/woocommerce/android/ui/mystore/MyStoreScreen.kt +++ b/WooCommerce-Wear/src/main/java/com/woocommerce/android/ui/mystore/MyStoreScreen.kt @@ -5,8 +5,8 @@ import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import com.google.android.horologist.compose.pager.PagerScreen -import com.woocommerce.android.ui.orders.OrdersListScreen -import com.woocommerce.android.ui.orders.OrdersListViewModel +import com.woocommerce.android.ui.orders.list.OrdersListScreen +import com.woocommerce.android.ui.orders.list.OrdersListViewModel import com.woocommerce.android.ui.stats.StoreStatsScreen import com.woocommerce.android.ui.stats.StoreStatsViewModel diff --git a/WooCommerce-Wear/src/main/java/com/woocommerce/android/ui/orders/FormatOrderData.kt b/WooCommerce-Wear/src/main/java/com/woocommerce/android/ui/orders/FormatOrderData.kt new file mode 100644 index 00000000000..79ba76be92d --- /dev/null +++ b/WooCommerce-Wear/src/main/java/com/woocommerce/android/ui/orders/FormatOrderData.kt @@ -0,0 +1,73 @@ +package com.woocommerce.android.ui.orders + +import android.content.Context +import android.os.Parcelable +import com.woocommerce.android.R +import com.woocommerce.android.util.DateUtils +import kotlinx.parcelize.Parcelize +import org.wordpress.android.fluxc.model.OrderEntity +import org.wordpress.android.fluxc.model.SiteModel +import org.wordpress.android.fluxc.store.WooCommerceStore +import java.util.Locale +import javax.inject.Inject + +class FormatOrderData @Inject constructor( + private val context: Context, + private val wooCommerceStore: WooCommerceStore, + private val dateUtils: DateUtils, + private val locale: Locale, +) { + operator fun invoke( + selectedSite: SiteModel, + order: OrderEntity + ) = order.toOrderItem(selectedSite) + + operator fun invoke( + selectedSite: SiteModel, + orders: List + ) = orders.map { it.toOrderItem(selectedSite) } + + private fun OrderEntity.toOrderItem( + selectedSite: SiteModel + ): OrderItem { + val formattedOrderTotals = wooCommerceStore.formatCurrencyForDisplay( + amount = total.toDoubleOrNull() ?: 0.0, + site = selectedSite, + currencyCode = null, + applyDecimalFormatting = true + ) + + val formattedCreationDate = dateUtils.getFormattedDateWithSiteTimeZone( + dateCreated + ) ?: dateCreated + + val formattedBillingName = takeUnless { + billingFirstName.isEmpty() && billingLastName.isEmpty() + }?.let { "$billingFirstName $billingLastName" } ?: context.getString(R.string.orders_list_guest_customer) + + val formattedStatus = status.replaceFirstChar { + if (it.isLowerCase()) it.titlecase(locale) else it.toString() + } + + val formattedNumber = "#$number" + + return OrderItem( + id = orderId, + date = formattedCreationDate, + number = formattedNumber, + customerName = formattedBillingName, + total = formattedOrderTotals, + status = formattedStatus + ) + } + + @Parcelize + data class OrderItem( + val id: Long, + val date: String, + val number: String, + val customerName: String, + val total: String, + val status: String + ) : Parcelable +} diff --git a/WooCommerce-Wear/src/main/java/com/woocommerce/android/ui/orders/OrdersListViewModel.kt b/WooCommerce-Wear/src/main/java/com/woocommerce/android/ui/orders/OrdersListViewModel.kt deleted file mode 100644 index 91ca0928919..00000000000 --- a/WooCommerce-Wear/src/main/java/com/woocommerce/android/ui/orders/OrdersListViewModel.kt +++ /dev/null @@ -1,114 +0,0 @@ -package com.woocommerce.android.ui.orders - -import android.os.Parcelable -import androidx.lifecycle.SavedStateHandle -import androidx.lifecycle.asLiveData -import androidx.navigation.NavHostController -import com.woocommerce.android.ui.login.LoginRepository -import com.woocommerce.android.util.DateUtils -import com.woocommerce.commons.viewmodel.ScopedViewModel -import com.woocommerce.commons.viewmodel.getStateFlow -import dagger.assisted.Assisted -import dagger.assisted.AssistedFactory -import dagger.assisted.AssistedInject -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.flow.filterNotNull -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.flow.update -import kotlinx.parcelize.Parcelize -import org.wordpress.android.fluxc.model.OrderEntity -import org.wordpress.android.fluxc.model.SiteModel -import org.wordpress.android.fluxc.store.WooCommerceStore -import java.util.Locale - -@Suppress("UnusedPrivateProperty", "LongParameterList") -@HiltViewModel(assistedFactory = OrdersListViewModel.Factory::class) -class OrdersListViewModel @AssistedInject constructor( - @Assisted private val navController: NavHostController, - private val fetchOrders: FetchOrders, - private val wooCommerceStore: WooCommerceStore, - private val dateUtils: DateUtils, - private val locale: Locale, - loginRepository: LoginRepository, - savedState: SavedStateHandle -) : ScopedViewModel(savedState) { - private val _viewState = savedState.getStateFlow( - scope = this, - initialValue = ViewState() - ) - val viewState = _viewState.asLiveData() - - init { - loginRepository.selectedSiteFlow - .filterNotNull() - .onEach { requestOrdersData(it) } - .launchIn(this) - } - - private suspend fun requestOrdersData(selectedSite: SiteModel) { - _viewState.update { it.copy(isLoading = true) } - fetchOrders(selectedSite) - .onEach { orders -> - _viewState.update { viewState -> - viewState.copy( - orders = orders.map { - it.toOrderItem(selectedSite) - }, - isLoading = false - ) - } - }.launchIn(this) - } - - private fun OrderEntity.toOrderItem( - selectedSite: SiteModel - ): OrderItem { - val formattedOrderTotals = wooCommerceStore.formatCurrencyForDisplay( - amount = total.toDoubleOrNull() ?: 0.0, - site = selectedSite, - currencyCode = null, - applyDecimalFormatting = true - ) - - val formattedCreationDate = dateUtils.getFormattedDateWithSiteTimeZone( - dateCreated - ) ?: dateCreated - - val formattedBillingName = takeUnless { - billingFirstName.isEmpty() && billingLastName.isEmpty() - }?.let { "$billingFirstName $billingLastName" } - - val formattedStatus = status.replaceFirstChar { - if (it.isLowerCase()) it.titlecase(locale) else it.toString() - } - - return OrderItem( - date = formattedCreationDate, - number = number, - customerName = formattedBillingName, - total = formattedOrderTotals, - status = formattedStatus - ) - } - - @Parcelize - data class ViewState( - val isLoading: Boolean = false, - val orders: List = emptyList() - ) : Parcelable - - @Parcelize - data class OrderItem( - val date: String, - val number: String, - val customerName: String?, - val total: String, - val status: String - ) : Parcelable - - @AssistedFactory - interface Factory { - fun create(navController: NavHostController): OrdersListViewModel - } -} diff --git a/WooCommerce-Wear/src/main/java/com/woocommerce/android/ui/orders/OrdersRepository.kt b/WooCommerce-Wear/src/main/java/com/woocommerce/android/ui/orders/OrdersRepository.kt index 775d6d8b38a..91371254f68 100644 --- a/WooCommerce-Wear/src/main/java/com/woocommerce/android/ui/orders/OrdersRepository.kt +++ b/WooCommerce-Wear/src/main/java/com/woocommerce/android/ui/orders/OrdersRepository.kt @@ -10,6 +10,9 @@ import com.woocommerce.android.datastore.DataStoreQualifier import com.woocommerce.android.datastore.DataStoreType import com.woocommerce.android.ui.login.LoginRepository import com.woocommerce.commons.wear.DataParameters.ORDERS_JSON +import com.woocommerce.commons.wear.DataParameters.ORDER_ID +import com.woocommerce.commons.wear.DataParameters.ORDER_PRODUCTS_JSON +import com.woocommerce.commons.wear.orders.WearOrderProduct import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.mapNotNull import org.wordpress.android.fluxc.model.OrderEntity @@ -31,6 +34,14 @@ class OrdersRepository @Inject constructor( shouldStoreData = true ) + suspend fun getOrderFromId( + selectedSite: SiteModel, + orderId: Long + ) = orderStore.getOrderByIdAndSite( + site = selectedSite, + orderId = orderId + ) + fun observeOrdersDataChanges() = ordersDataStore.data .mapNotNull { it[stringPreferencesKey(generateOrdersKey())] } .map { gson.fromJson(it, Array::class.java).toList() } @@ -48,6 +59,24 @@ class OrdersRepository @Inject constructor( return "${ORDERS_KEY_PREFIX}:$siteId" } + fun observeOrderProductsDataChanges(orderId: Long) = ordersDataStore.data + .mapNotNull { it[stringPreferencesKey(generateProductsKey(orderId))] } + .map { gson.fromJson(it, Array::class.java).toList() } + + suspend fun receiveOrderProductsDataFromPhone(data: DataMap) { + val orderId = data.getLong(ORDER_ID.value, 0) + val productsJson = data.getString(ORDER_PRODUCTS_JSON.value, "") + + ordersDataStore.edit { prefs -> + prefs[stringPreferencesKey(generateProductsKey(orderId))] = productsJson + } + } + + private fun generateProductsKey(orderId: Long): String { + val siteId = loginRepository.selectedSite?.siteId ?: 0 + return "${ORDERS_KEY_PREFIX}:$siteId:$orderId" + } + companion object { private const val ORDERS_KEY_PREFIX = "store-orders" } diff --git a/WooCommerce-Wear/src/main/java/com/woocommerce/android/ui/orders/details/FetchOrderProducts.kt b/WooCommerce-Wear/src/main/java/com/woocommerce/android/ui/orders/details/FetchOrderProducts.kt new file mode 100644 index 00000000000..4605f6326ed --- /dev/null +++ b/WooCommerce-Wear/src/main/java/com/woocommerce/android/ui/orders/details/FetchOrderProducts.kt @@ -0,0 +1,40 @@ +package com.woocommerce.android.ui.orders.details + +import com.woocommerce.android.extensions.combineWithTimeout +import com.woocommerce.android.phone.PhoneConnectionRepository +import com.woocommerce.android.ui.orders.OrdersRepository +import com.woocommerce.android.ui.orders.details.FetchOrderProducts.OrderProductsRequest.Error +import com.woocommerce.android.ui.orders.details.FetchOrderProducts.OrderProductsRequest.Finished +import com.woocommerce.android.ui.orders.details.FetchOrderProducts.OrderProductsRequest.Waiting +import com.woocommerce.commons.wear.MessagePath.REQUEST_ORDER_PRODUCTS +import com.woocommerce.commons.wear.orders.WearOrderProduct +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.filterNotNull +import javax.inject.Inject + +class FetchOrderProducts @Inject constructor( + private val phoneRepository: PhoneConnectionRepository, + private val ordersRepository: OrdersRepository +) { + suspend operator fun invoke(orderId: Long): Flow { + phoneRepository.sendMessage( + REQUEST_ORDER_PRODUCTS, + orderId.toString().toByteArray() + ) + + return ordersRepository.observeOrderProductsDataChanges(orderId) + .combineWithTimeout { orderProducts, isTimeout -> + when { + orderProducts.isNotEmpty() -> Finished(orderProducts) + isTimeout.not() -> Waiting + else -> Error + } + }.filterNotNull() + } + + sealed class OrderProductsRequest { + data object Error : OrderProductsRequest() + data object Waiting : OrderProductsRequest() + data class Finished(val products: List) : OrderProductsRequest() + } +} diff --git a/WooCommerce-Wear/src/main/java/com/woocommerce/android/ui/orders/details/OrderDetailsScreen.kt b/WooCommerce-Wear/src/main/java/com/woocommerce/android/ui/orders/details/OrderDetailsScreen.kt new file mode 100644 index 00000000000..3aebe27ff7b --- /dev/null +++ b/WooCommerce-Wear/src/main/java/com/woocommerce/android/ui/orders/details/OrderDetailsScreen.kt @@ -0,0 +1,141 @@ +package com.woocommerce.android.ui.orders.details + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.livedata.observeAsState +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.wear.compose.material.TimeText +import androidx.wear.tooling.preview.devices.WearDevices +import com.woocommerce.android.presentation.component.LoadingScreen +import com.woocommerce.android.presentation.theme.WooColors +import com.woocommerce.android.presentation.theme.WooTheme +import com.woocommerce.android.presentation.theme.WooTypography +import com.woocommerce.android.ui.orders.FormatOrderData.OrderItem + +@Composable +fun OrderDetailsScreen(viewModel: OrderDetailsViewModel) { + val viewState = viewModel.viewState.observeAsState() + OrderDetailsScreen( + isLoading = viewState.value?.isLoading ?: false, + order = viewState.value?.orderItem + ) +} + +@Composable +fun OrderDetailsScreen( + modifier: Modifier = Modifier, + isLoading: Boolean, + order: OrderItem? +) { + WooTheme { + TimeText() + Box( + contentAlignment = Alignment.TopCenter, + modifier = modifier + .fillMaxSize() + .verticalScroll(rememberScrollState()) + .padding(top = 32.dp) + .padding(horizontal = 20.dp) + ) { + when { + isLoading -> LoadingScreen() + order == null -> OrderLoadingFailed() + else -> OrderDetailsContent(order, modifier) + } + } + } +} + +@Composable +fun OrderDetailsContent( + order: OrderItem, + modifier: Modifier +) { + Column(modifier = modifier.fillMaxSize()) { + Column( + modifier = modifier.fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + text = order.date, + color = Color.White + ) + Text( + text = order.number, + color = Color.White + ) + } + Column( + modifier = modifier.fillMaxSize(), + horizontalAlignment = Alignment.Start + ) { + Text( + text = order.customerName, + style = WooTypography.title3, + color = Color.White, + textAlign = TextAlign.Start, + modifier = Modifier.fillMaxWidth() + ) + Text( // Needs proper handling + text = "3 Products", + textAlign = TextAlign.Center, + color = Color.White + ) + Text( + text = order.total, + style = WooTypography.body1, + color = Color.White, + fontWeight = FontWeight.Bold, + textAlign = TextAlign.Start, + modifier = Modifier.fillMaxWidth() + ) + Text( + text = order.status, + style = WooTypography.caption1, + color = WooColors.woo_gray_alpha, + textAlign = TextAlign.Start, + modifier = Modifier.fillMaxWidth() + ) + } + } +} + +@Composable +fun OrderLoadingFailed() { + Text( + text = "Failed to load Order data", + color = Color.White + ) +} + +@Preview(device = WearDevices.LARGE_ROUND, showSystemUi = true) +@Preview(device = WearDevices.SMALL_ROUND, showSystemUi = true) +@Preview(device = WearDevices.SQUARE, showSystemUi = true) +@Preview(device = WearDevices.RECT, showSystemUi = true) +@Composable +fun Preview() { + OrderDetailsScreen( + isLoading = false, + order = OrderItem( + id = 0L, + date = "25 Feb", + number = "#125", + customerName = "John Doe", + total = "$100.00", + status = "Processing" + ) + ) +} diff --git a/WooCommerce-Wear/src/main/java/com/woocommerce/android/ui/orders/details/OrderDetailsViewModel.kt b/WooCommerce-Wear/src/main/java/com/woocommerce/android/ui/orders/details/OrderDetailsViewModel.kt new file mode 100644 index 00000000000..78b6f15aa48 --- /dev/null +++ b/WooCommerce-Wear/src/main/java/com/woocommerce/android/ui/orders/details/OrderDetailsViewModel.kt @@ -0,0 +1,57 @@ +package com.woocommerce.android.ui.orders.details + +import android.os.Parcelable +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.asLiveData +import com.woocommerce.android.ui.NavArgs.ORDER_ID +import com.woocommerce.android.ui.login.LoginRepository +import com.woocommerce.android.ui.orders.FormatOrderData +import com.woocommerce.android.ui.orders.FormatOrderData.OrderItem +import com.woocommerce.android.ui.orders.OrdersRepository +import com.woocommerce.commons.viewmodel.ScopedViewModel +import com.woocommerce.commons.viewmodel.getStateFlow +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.update +import kotlinx.parcelize.Parcelize +import javax.inject.Inject + +@HiltViewModel +class OrderDetailsViewModel @Inject constructor( + ordersRepository: OrdersRepository, + formatOrder: FormatOrderData, + loginRepository: LoginRepository, + savedState: SavedStateHandle +) : ScopedViewModel(savedState) { + private val _viewState = savedState.getStateFlow( + scope = this, + initialValue = ViewState() + ) + val viewState = _viewState.asLiveData() + + init { + _viewState.update { it.copy(isLoading = true) } + loginRepository.selectedSiteFlow + .filterNotNull() + .onEach { site -> + savedState.get(ORDER_ID.key) + ?.let { ordersRepository.getOrderFromId(site, it) } + ?.let { formatOrder(site, it) } + .let { presentOrderData(it) } + }.launchIn(this) + } + + private fun presentOrderData(order: OrderItem?) { + _viewState.update { + it.copy(isLoading = false, orderItem = order) + } + } + + @Parcelize + data class ViewState( + val isLoading: Boolean = false, + val orderItem: OrderItem? = null + ) : Parcelable +} diff --git a/WooCommerce-Wear/src/main/java/com/woocommerce/android/ui/orders/FetchOrders.kt b/WooCommerce-Wear/src/main/java/com/woocommerce/android/ui/orders/list/FetchOrders.kt similarity index 59% rename from WooCommerce-Wear/src/main/java/com/woocommerce/android/ui/orders/FetchOrders.kt rename to WooCommerce-Wear/src/main/java/com/woocommerce/android/ui/orders/list/FetchOrders.kt index eb47c4f3b5a..c58317de48f 100644 --- a/WooCommerce-Wear/src/main/java/com/woocommerce/android/ui/orders/FetchOrders.kt +++ b/WooCommerce-Wear/src/main/java/com/woocommerce/android/ui/orders/list/FetchOrders.kt @@ -1,12 +1,14 @@ -package com.woocommerce.android.ui.orders +package com.woocommerce.android.ui.orders.list +import com.woocommerce.android.extensions.combineWithTimeout import com.woocommerce.android.phone.PhoneConnectionRepository import com.woocommerce.android.system.NetworkStatus -import com.woocommerce.android.ui.login.ObserveLoginRequest.Companion.TIMEOUT_MILLIS +import com.woocommerce.android.ui.orders.OrdersRepository +import com.woocommerce.android.ui.orders.list.FetchOrders.OrdersRequest.Error +import com.woocommerce.android.ui.orders.list.FetchOrders.OrdersRequest.Finished +import com.woocommerce.android.ui.orders.list.FetchOrders.OrdersRequest.Waiting import com.woocommerce.commons.wear.MessagePath.REQUEST_ORDERS -import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.flow import org.wordpress.android.fluxc.model.OrderEntity @@ -21,16 +23,14 @@ class FetchOrders @Inject constructor( ) { suspend operator fun invoke( selectedSite: SiteModel - ): Flow> = combine( - selectOrdersDataSource(selectedSite), - timeoutFlow - ) { orders, isTimeout -> - when { - orders.isNotEmpty() -> orders - isTimeout -> emptyList() - else -> null - } - }.filterNotNull() + ): Flow = selectOrdersDataSource(selectedSite) + .combineWithTimeout { orders, isTimeout -> + when { + orders.isNotEmpty() -> Finished(orders) + isTimeout.not() -> Waiting + else -> Error + } + }.filterNotNull() private suspend fun selectOrdersDataSource( selectedSite: SiteModel @@ -48,10 +48,9 @@ class FetchOrders @Inject constructor( } } - private val timeoutFlow: Flow - get() = flow { - emit(false) - delay(TIMEOUT_MILLIS) - emit(true) - } + sealed class OrdersRequest { + data object Error : OrdersRequest() + data object Waiting : OrdersRequest() + data class Finished(val orders: List) : OrdersRequest() + } } diff --git a/WooCommerce-Wear/src/main/java/com/woocommerce/android/ui/orders/OrdersListScreen.kt b/WooCommerce-Wear/src/main/java/com/woocommerce/android/ui/orders/list/OrdersListScreen.kt similarity index 86% rename from WooCommerce-Wear/src/main/java/com/woocommerce/android/ui/orders/OrdersListScreen.kt rename to WooCommerce-Wear/src/main/java/com/woocommerce/android/ui/orders/list/OrdersListScreen.kt index 751cc0f1d26..be75fb06231 100644 --- a/WooCommerce-Wear/src/main/java/com/woocommerce/android/ui/orders/OrdersListScreen.kt +++ b/WooCommerce-Wear/src/main/java/com/woocommerce/android/ui/orders/list/OrdersListScreen.kt @@ -1,6 +1,7 @@ -package com.woocommerce.android.ui.orders +package com.woocommerce.android.ui.orders.list 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 @@ -32,14 +33,15 @@ import com.woocommerce.android.presentation.component.LoadingScreen import com.woocommerce.android.presentation.theme.WooColors import com.woocommerce.android.presentation.theme.WooTheme import com.woocommerce.android.presentation.theme.WooTypography -import com.woocommerce.android.ui.orders.OrdersListViewModel.OrderItem +import com.woocommerce.android.ui.orders.FormatOrderData.OrderItem @Composable fun OrdersListScreen(viewModel: OrdersListViewModel) { val viewState by viewModel.viewState.observeAsState() OrdersListScreen( isLoading = viewState?.isLoading ?: false, - orders = viewState?.orders.orEmpty() + orders = viewState?.orders.orEmpty(), + onOrderClicked = viewModel::onOrderItemClick ) } @@ -47,6 +49,7 @@ fun OrdersListScreen(viewModel: OrdersListViewModel) { fun OrdersListScreen( isLoading: Boolean, orders: List, + onOrderClicked: (orderId: Long) -> Unit, modifier: Modifier = Modifier ) { WooTheme { @@ -70,7 +73,7 @@ fun OrdersListScreen( if (isLoading) { LoadingScreen() } else { - OrdersLazyColumn(orders, modifier) + OrdersLazyColumn(orders, onOrderClicked, modifier) } } } @@ -80,6 +83,7 @@ fun OrdersListScreen( @Composable private fun OrdersLazyColumn( orders: List, + onOrderClicked: (orderId: Long) -> Unit, modifier: Modifier ) { ScalingLazyColumn( @@ -91,8 +95,9 @@ private fun OrdersLazyColumn( ) { items(orders) { OrderListItem( - modifier = modifier, - order = it + order = it, + onOrderClicked = onOrderClicked, + modifier = modifier ) } } @@ -100,8 +105,9 @@ private fun OrdersLazyColumn( @Composable fun OrderListItem( - modifier: Modifier, - order: OrderItem + order: OrderItem, + onOrderClicked: (orderId: Long) -> Unit, + modifier: Modifier ) { Box( modifier = modifier @@ -109,6 +115,7 @@ fun OrderListItem( .background(Color.DarkGray) .padding(10.dp) .fillMaxWidth() + .clickable { onOrderClicked(order.id) } ) { Column(verticalArrangement = Arrangement.spacedBy(4.dp)) { Row( @@ -120,14 +127,12 @@ fun OrderListItem( color = WooColors.woo_purple_20 ) Text( - text = "#${order.number}", + text = order.number, color = WooColors.woo_gray_alpha ) } Text( - text = order.customerName - ?.takeIf { it.isNotEmpty() } - ?: stringResource(id = R.string.orders_list_guest_customer), + text = order.customerName, style = WooTypography.body1, color = Color.White, textAlign = TextAlign.Start, @@ -160,8 +165,10 @@ fun OrderListItem( fun Preview() { OrdersListScreen( isLoading = false, + onOrderClicked = {}, orders = listOf( OrderItem( + id = 0L, date = "25 Feb", number = "#125", customerName = "John Doe", @@ -169,6 +176,7 @@ fun Preview() { status = "Processing" ), OrderItem( + id = 1L, date = "31 Dec", number = "#124", customerName = "Jane Doe", @@ -176,6 +184,7 @@ fun Preview() { status = "Completed" ), OrderItem( + id = 2L, date = "4 Oct", number = "#123", customerName = "John Smith", diff --git a/WooCommerce-Wear/src/main/java/com/woocommerce/android/ui/orders/list/OrdersListViewModel.kt b/WooCommerce-Wear/src/main/java/com/woocommerce/android/ui/orders/list/OrdersListViewModel.kt new file mode 100644 index 00000000000..9b3b5a8baeb --- /dev/null +++ b/WooCommerce-Wear/src/main/java/com/woocommerce/android/ui/orders/list/OrdersListViewModel.kt @@ -0,0 +1,78 @@ +package com.woocommerce.android.ui.orders.list + +import android.os.Parcelable +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.asLiveData +import androidx.navigation.NavHostController +import com.woocommerce.android.ui.NavRoutes.ORDER_DETAILS +import com.woocommerce.android.ui.login.LoginRepository +import com.woocommerce.android.ui.orders.FormatOrderData +import com.woocommerce.android.ui.orders.FormatOrderData.OrderItem +import com.woocommerce.android.ui.orders.list.FetchOrders.OrdersRequest.Finished +import com.woocommerce.android.ui.orders.list.FetchOrders.OrdersRequest.Waiting +import com.woocommerce.commons.viewmodel.ScopedViewModel +import com.woocommerce.commons.viewmodel.getStateFlow +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.update +import kotlinx.parcelize.Parcelize +import org.wordpress.android.fluxc.model.SiteModel + +@HiltViewModel(assistedFactory = OrdersListViewModel.Factory::class) +class OrdersListViewModel @AssistedInject constructor( + @Assisted private val navController: NavHostController, + private val fetchOrders: FetchOrders, + private val formatOrders: FormatOrderData, + loginRepository: LoginRepository, + savedState: SavedStateHandle +) : ScopedViewModel(savedState) { + private val _viewState = savedState.getStateFlow( + scope = this, + initialValue = ViewState() + ) + val viewState = _viewState.asLiveData() + + init { + loginRepository.selectedSiteFlow + .filterNotNull() + .onEach { requestOrdersData(it) } + .launchIn(this) + } + + fun onOrderItemClick(orderId: Long) { + navController.navigate(ORDER_DETAILS.withArgs(orderId)) + } + + private suspend fun requestOrdersData(selectedSite: SiteModel) { + _viewState.update { it.copy(isLoading = true) } + fetchOrders(selectedSite) + .onEach { request -> + when (request) { + is Finished -> _viewState.update { viewState -> + viewState.copy( + orders = formatOrders(selectedSite, request.orders), + isLoading = false + ) + } + is Waiting -> _viewState.update { it.copy(isLoading = true) } + else -> _viewState.update { it.copy(isLoading = false) } + } + }.launchIn(this) + } + + @Parcelize + data class ViewState( + val isLoading: Boolean = false, + val orders: List = emptyList() + ) : Parcelable + + @AssistedFactory + interface Factory { + fun create(navController: NavHostController): OrdersListViewModel + } +} diff --git a/WooCommerce-Wear/src/main/java/com/woocommerce/android/ui/stats/StoreStatsViewModel.kt b/WooCommerce-Wear/src/main/java/com/woocommerce/android/ui/stats/StoreStatsViewModel.kt index 1a1dfbf5770..164007d263b 100644 --- a/WooCommerce-Wear/src/main/java/com/woocommerce/android/ui/stats/StoreStatsViewModel.kt +++ b/WooCommerce-Wear/src/main/java/com/woocommerce/android/ui/stats/StoreStatsViewModel.kt @@ -3,12 +3,11 @@ package com.woocommerce.android.ui.stats import android.os.Parcelable import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.asLiveData -import com.woocommerce.android.phone.PhoneConnectionRepository -import com.woocommerce.android.system.NetworkStatus import com.woocommerce.android.ui.login.LoginRepository -import com.woocommerce.android.ui.stats.datasource.FetchStatsFromPhone -import com.woocommerce.android.ui.stats.datasource.FetchStatsFromStore -import com.woocommerce.android.ui.stats.datasource.StoreStatsRequest +import com.woocommerce.android.ui.stats.datasource.FetchStats +import com.woocommerce.android.ui.stats.datasource.FetchStats.StoreStatsRequest +import com.woocommerce.android.ui.stats.datasource.FetchStats.StoreStatsRequest.Finished +import com.woocommerce.android.ui.stats.datasource.FetchStats.StoreStatsRequest.Waiting import com.woocommerce.commons.viewmodel.ScopedViewModel import com.woocommerce.commons.viewmodel.getStateFlow import dagger.hilt.android.lifecycle.HiltViewModel @@ -26,10 +25,8 @@ import javax.inject.Inject @HiltViewModel class StoreStatsViewModel @Inject constructor( - private val phoneRepository: PhoneConnectionRepository, - private val fetchStatsFromStore: FetchStatsFromStore, - private val fetchStatsFromPhone: FetchStatsFromPhone, - private val networkStatus: NetworkStatus, + private val fetchStats: FetchStats, + private val locale: Locale, loginRepository: LoginRepository, savedState: SavedStateHandle ) : ScopedViewModel(savedState) { @@ -40,7 +37,7 @@ class StoreStatsViewModel @Inject constructor( val viewState = _viewState.asLiveData() private val friendlyTimeFormat: SimpleDateFormat by lazy { - SimpleDateFormat("h:mm a", Locale.getDefault()) + SimpleDateFormat("h:mm a", locale) } init { @@ -52,24 +49,19 @@ class StoreStatsViewModel @Inject constructor( }.launchIn(this) } - private suspend fun evaluateStatsSource(selectedSite: SiteModel) = when { - networkStatus.isConnected() -> fetchStatsFromStore(selectedSite) - phoneRepository.isPhoneConnectionAvailable() -> fetchStatsFromPhone() - else -> error("No connection available") - } - private fun requestStoreStats(selectedSite: SiteModel) { _viewState.update { it.copy(isLoading = true) } launch { - evaluateStatsSource(selectedSite) + fetchStats(selectedSite) .onEach { handleStatsDataChange(it) } .launchIn(this) } } - private fun handleStatsDataChange(statsData: StoreStatsRequest?) { - when (statsData) { - is StoreStatsRequest.Data -> { + private fun handleStatsDataChange(request: StoreStatsRequest?) { + when (request) { + is Finished -> { + val statsData = request.data _viewState.update { it.copy( isLoading = false, @@ -81,7 +73,7 @@ class StoreStatsViewModel @Inject constructor( ) } } - + is Waiting -> _viewState.update { it.copy(isLoading = true) } else -> _viewState.update { it.copy(isLoading = false) } } } diff --git a/WooCommerce-Wear/src/main/java/com/woocommerce/android/ui/stats/datasource/FetchStats.kt b/WooCommerce-Wear/src/main/java/com/woocommerce/android/ui/stats/datasource/FetchStats.kt new file mode 100644 index 00000000000..65933bd9a09 --- /dev/null +++ b/WooCommerce-Wear/src/main/java/com/woocommerce/android/ui/stats/datasource/FetchStats.kt @@ -0,0 +1,111 @@ +package com.woocommerce.android.ui.stats.datasource + +import com.woocommerce.android.extensions.combineWithTimeout +import com.woocommerce.android.phone.PhoneConnectionRepository +import com.woocommerce.android.system.NetworkStatus +import com.woocommerce.android.ui.stats.datasource.FetchStats.StoreStatsRequest.Error +import com.woocommerce.android.ui.stats.datasource.FetchStats.StoreStatsRequest.Finished +import com.woocommerce.android.ui.stats.datasource.FetchStats.StoreStatsRequest.Waiting +import com.woocommerce.android.ui.stats.datasource.StoreStatsData.RevenueData +import com.woocommerce.commons.wear.MessagePath +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.flowOf +import org.wordpress.android.fluxc.model.SiteModel +import org.wordpress.android.fluxc.store.WooCommerceStore +import javax.inject.Inject + +class FetchStats @Inject constructor( + private val statsRepository: StatsRepository, + private val phoneRepository: PhoneConnectionRepository, + private val wooCommerceStore: WooCommerceStore, + private val networkStatus: NetworkStatus +) { + private val revenueStats = MutableStateFlow(null) + private val visitorStats = MutableStateFlow(null) + + suspend operator fun invoke( + selectedSite: SiteModel + ) = when { + networkStatus.isConnected() -> fetchStatsFromStore(selectedSite) + phoneRepository.isPhoneConnectionAvailable() -> fetchStatsFromPhone(selectedSite) + else -> flowOf(Error) + } + + private suspend fun fetchStatsFromPhone( + selectedSite: SiteModel + ): Flow { + phoneRepository.sendMessage(MessagePath.REQUEST_STATS) + return statsRepository.observeStatsDataChanges(selectedSite) + .combineWithTimeout { statsData, isTimeout -> + when { + statsData?.isComplete == true -> Finished(statsData) + isTimeout.not() -> Waiting + else -> Error + } + }.filterNotNull() + } + + private suspend fun fetchStatsFromStore( + selectedSite: SiteModel + ): Flow { + fetchRevenueStats(selectedSite) + fetchVisitorsStats(selectedSite) + + return combine(revenueStats, visitorStats) { revenue, visitors -> + StoreStatsData(revenue, visitors) + }.combineWithTimeout { data, isTimeout -> + when { + data.isComplete -> Finished(data) + isTimeout.not() -> Waiting + else -> Error + } + }.filterNotNull() + } + + private suspend fun fetchRevenueStats(selectedSite: SiteModel) { + statsRepository.fetchRevenueStats(selectedSite) + .fold( + onSuccess = { revenue -> + val totals = revenue?.parseTotal() + + val formattedRevenue = wooCommerceStore.formatCurrencyForDisplay( + amount = totals?.totalSales ?: 0.0, + site = selectedSite, + currencyCode = null, + applyDecimalFormatting = true + ) + + val revenueData = RevenueData( + totalRevenue = formattedRevenue, + orderCount = totals?.ordersCount ?: 0 + ) + + revenueStats.value = revenueData + }, + onFailure = { + revenueStats.value = null + } + ) + } + + private suspend fun fetchVisitorsStats(selectedSite: SiteModel) { + statsRepository.fetchVisitorStats(selectedSite) + .fold( + onSuccess = { visitors -> + visitorStats.value = visitors ?: 0 + }, + onFailure = { + visitorStats.value = null + } + ) + } + + sealed class StoreStatsRequest { + data object Error : StoreStatsRequest() + data object Waiting : StoreStatsRequest() + data class Finished(val data: StoreStatsData) : StoreStatsRequest() + } +} diff --git a/WooCommerce-Wear/src/main/java/com/woocommerce/android/ui/stats/datasource/FetchStatsFromPhone.kt b/WooCommerce-Wear/src/main/java/com/woocommerce/android/ui/stats/datasource/FetchStatsFromPhone.kt deleted file mode 100644 index 827ab9adfb6..00000000000 --- a/WooCommerce-Wear/src/main/java/com/woocommerce/android/ui/stats/datasource/FetchStatsFromPhone.kt +++ /dev/null @@ -1,34 +0,0 @@ -package com.woocommerce.android.ui.stats.datasource - -import com.woocommerce.android.phone.PhoneConnectionRepository -import com.woocommerce.android.ui.login.ObserveLoginRequest.Companion.TIMEOUT_MILLIS -import com.woocommerce.commons.wear.MessagePath.REQUEST_STATS -import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.filterNotNull -import kotlinx.coroutines.flow.flow -import javax.inject.Inject - -class FetchStatsFromPhone @Inject constructor( - private val phoneRepository: PhoneConnectionRepository, - private val statsRepository: StatsRepository -) { - suspend operator fun invoke(): Flow { - phoneRepository.sendMessage(REQUEST_STATS) - return combine(statsRepository.observeStatsDataChanges(), timeoutFlow) { statsData, isTimeout -> - when { - statsData?.isFinished == true -> statsData - isTimeout -> StoreStatsRequest.Error - else -> null - } - }.filterNotNull() - } - - private val timeoutFlow: Flow - get() = flow { - emit(false) - delay(TIMEOUT_MILLIS) - emit(true) - } -} diff --git a/WooCommerce-Wear/src/main/java/com/woocommerce/android/ui/stats/datasource/FetchStatsFromStore.kt b/WooCommerce-Wear/src/main/java/com/woocommerce/android/ui/stats/datasource/FetchStatsFromStore.kt deleted file mode 100644 index 901a81f6f2e..00000000000 --- a/WooCommerce-Wear/src/main/java/com/woocommerce/android/ui/stats/datasource/FetchStatsFromStore.kt +++ /dev/null @@ -1,73 +0,0 @@ -package com.woocommerce.android.ui.stats.datasource - -import com.woocommerce.android.ui.stats.datasource.StoreStatsRequest.Data.RevenueData -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.filter -import org.wordpress.android.fluxc.model.SiteModel -import org.wordpress.android.fluxc.store.WooCommerceStore -import javax.inject.Inject - -class FetchStatsFromStore @Inject constructor( - private val statsRepository: StatsRepository, - private val wooCommerceStore: WooCommerceStore -) { - private val revenueStats = MutableStateFlow(null) - private val visitorStats = MutableStateFlow(null) - - suspend operator fun invoke( - selectedSite: SiteModel - ): Flow { - fetchRevenueStats(selectedSite) - fetchVisitorsStats(selectedSite) - - return combine( - revenueStats, - visitorStats - ) { revenueStats, visitorStats -> - StoreStatsRequest.Data( - revenueData = revenueStats, - visitorData = visitorStats, - ) - }.filter { it.isFinished } - } - - private suspend fun fetchRevenueStats(selectedSite: SiteModel) { - statsRepository.fetchRevenueStats(selectedSite) - .fold( - onSuccess = { revenue -> - val totals = revenue?.parseTotal() - - val formattedRevenue = wooCommerceStore.formatCurrencyForDisplay( - amount = totals?.totalSales ?: 0.0, - site = selectedSite, - currencyCode = null, - applyDecimalFormatting = true - ) - - val revenueData = RevenueData( - totalRevenue = formattedRevenue, - orderCount = totals?.ordersCount ?: 0 - ) - - revenueStats.value = revenueData - }, - onFailure = { - revenueStats.value = null - } - ) - } - - private suspend fun fetchVisitorsStats(selectedSite: SiteModel) { - statsRepository.fetchVisitorStats(selectedSite) - .fold( - onSuccess = { visitors -> - visitorStats.value = visitors ?: 0 - }, - onFailure = { - visitorStats.value = null - } - ) - } -} diff --git a/WooCommerce-Wear/src/main/java/com/woocommerce/android/ui/stats/datasource/StatsRepository.kt b/WooCommerce-Wear/src/main/java/com/woocommerce/android/ui/stats/datasource/StatsRepository.kt index 66e2e02b8f8..953851311c3 100644 --- a/WooCommerce-Wear/src/main/java/com/woocommerce/android/ui/stats/datasource/StatsRepository.kt +++ b/WooCommerce-Wear/src/main/java/com/woocommerce/android/ui/stats/datasource/StatsRepository.kt @@ -9,7 +9,7 @@ import com.google.gson.Gson import com.woocommerce.android.datastore.DataStoreQualifier import com.woocommerce.android.datastore.DataStoreType import com.woocommerce.android.ui.login.LoginRepository -import com.woocommerce.android.ui.stats.datasource.StoreStatsRequest.Data.RevenueData +import com.woocommerce.android.ui.stats.datasource.StoreStatsData.RevenueData import com.woocommerce.android.ui.stats.range.TodayRangeData import com.woocommerce.android.util.DateUtils import com.woocommerce.commons.extensions.formatToYYYYmmDDhhmmss @@ -98,7 +98,7 @@ class StatsRepository @Inject constructor( } suspend fun receiveStatsDataFromPhone(data: DataMap) { - val statsJson = StoreStatsRequest.Data( + val statsJson = StoreStatsData( revenueData = RevenueData( totalRevenue = data.getString(TOTAL_REVENUE.value, ""), orderCount = data.getInt(ORDERS_COUNT.value, 0) @@ -107,16 +107,18 @@ class StatsRepository @Inject constructor( ).let { gson.toJson(it) } statsDataStore.edit { prefs -> - prefs[stringPreferencesKey(generateStatsKey())] = statsJson + val siteId = loginRepository.selectedSite?.siteId ?: 0 + prefs[stringPreferencesKey(generateStatsKey(siteId))] = statsJson } } - fun observeStatsDataChanges() = statsDataStore.data - .mapNotNull { it[stringPreferencesKey(generateStatsKey())] } - .map { gson.fromJson(it, StoreStatsRequest.Data::class.java) } + fun observeStatsDataChanges( + selectedSite: SiteModel + ) = statsDataStore.data + .mapNotNull { it[stringPreferencesKey(generateStatsKey(selectedSite.siteId))] } + .map { gson.fromJson(it, StoreStatsData::class.java) } - private fun generateStatsKey(): String { - val siteId = loginRepository.selectedSite?.siteId ?: 0 + private fun generateStatsKey(siteId: Long): String { return "$STATS_KEY_PREFIX:$siteId" } diff --git a/WooCommerce-Wear/src/main/java/com/woocommerce/android/ui/stats/datasource/StoreStatsData.kt b/WooCommerce-Wear/src/main/java/com/woocommerce/android/ui/stats/datasource/StoreStatsData.kt new file mode 100644 index 00000000000..c21935f4971 --- /dev/null +++ b/WooCommerce-Wear/src/main/java/com/woocommerce/android/ui/stats/datasource/StoreStatsData.kt @@ -0,0 +1,27 @@ +package com.woocommerce.android.ui.stats.datasource + +import com.woocommerce.commons.extensions.convertedFrom + +data class StoreStatsData( + private val revenueData: RevenueData?, + private val visitorData: Int? +) { + val revenue get() = revenueData?.totalRevenue.orEmpty() + val ordersCount get() = revenueData?.orderCount ?: 0 + val visitorsCount get() = visitorData ?: 0 + val conversionRate: String + get() { + val ordersCount = revenueData?.orderCount ?: 0 + val visitorsCount = visitorData ?: 0 + return ordersCount convertedFrom visitorsCount + } + + val isComplete + get() = revenueData != null && + visitorData != null + + data class RevenueData( + val totalRevenue: String, + val orderCount: Int + ) +} diff --git a/WooCommerce-Wear/src/main/java/com/woocommerce/android/ui/stats/datasource/StoreStatsRequest.kt b/WooCommerce-Wear/src/main/java/com/woocommerce/android/ui/stats/datasource/StoreStatsRequest.kt deleted file mode 100644 index 38f10b29b7c..00000000000 --- a/WooCommerce-Wear/src/main/java/com/woocommerce/android/ui/stats/datasource/StoreStatsRequest.kt +++ /dev/null @@ -1,31 +0,0 @@ -package com.woocommerce.android.ui.stats.datasource - -import com.woocommerce.commons.extensions.convertedFrom - -sealed class StoreStatsRequest { - data class Data( - private val revenueData: RevenueData?, - private val visitorData: Int? - ) : StoreStatsRequest() { - val revenue get() = revenueData?.totalRevenue.orEmpty() - val ordersCount get() = revenueData?.orderCount ?: 0 - val visitorsCount get() = visitorData ?: 0 - val conversionRate: String - get() { - val ordersCount = revenueData?.orderCount ?: 0 - val visitorsCount = visitorData ?: 0 - return ordersCount convertedFrom visitorsCount - } - - val isFinished - get() = revenueData != null && - visitorData != null - - data class RevenueData( - val totalRevenue: String, - val orderCount: Int - ) - } - - data object Error : StoreStatsRequest() -} diff --git a/WooCommerce-Wear/src/main/res/values/strings.xml b/WooCommerce-Wear/src/main/res/values/strings.xml index 066672f9ce4..6532d66ab48 100644 --- a/WooCommerce-Wear/src/main/res/values/strings.xml +++ b/WooCommerce-Wear/src/main/res/values/strings.xml @@ -15,6 +15,11 @@ Orders Guest + + 1 product + %1$d products + Failed to load order details + Example tile Example complication diff --git a/WooCommerce-Wear/src/test/java/com/woocommerce/android/ui/mystore/StoreStatsViewModelTest.kt b/WooCommerce-Wear/src/test/java/com/woocommerce/android/ui/mystore/StoreStatsViewModelTest.kt index 3f14a7cd5b2..f117452ff06 100644 --- a/WooCommerce-Wear/src/test/java/com/woocommerce/android/ui/mystore/StoreStatsViewModelTest.kt +++ b/WooCommerce-Wear/src/test/java/com/woocommerce/android/ui/mystore/StoreStatsViewModelTest.kt @@ -1,14 +1,10 @@ package com.woocommerce.android.ui.mystore import androidx.lifecycle.SavedStateHandle -import androidx.navigation.NavHostController import com.woocommerce.android.BaseUnitTest -import com.woocommerce.android.phone.PhoneConnectionRepository -import com.woocommerce.android.system.NetworkStatus import com.woocommerce.android.ui.login.LoginRepository import com.woocommerce.android.ui.stats.StoreStatsViewModel -import com.woocommerce.android.ui.stats.datasource.FetchStatsFromPhone -import com.woocommerce.android.ui.stats.datasource.FetchStatsFromStore +import com.woocommerce.android.ui.stats.datasource.FetchStats import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableStateFlow import org.assertj.core.api.Assertions.assertThat @@ -16,17 +12,14 @@ import org.junit.Test import org.mockito.kotlin.mock import org.mockito.kotlin.whenever import org.wordpress.android.fluxc.model.SiteModel +import java.util.Locale @ExperimentalCoroutinesApi class StoreStatsViewModelTest : BaseUnitTest() { private lateinit var sut: StoreStatsViewModel private val loginRepository: LoginRepository = mock() - private val phoneRepository: PhoneConnectionRepository = mock() - private val fetchStatsFromStore: FetchStatsFromStore = mock() - private val fetchStatsFromPhone: FetchStatsFromPhone = mock() - private val networkStatus: NetworkStatus = mock() - private val navController: NavHostController = mock() + private val fetchStats: FetchStats = mock() @Test fun `when login changes, site data is updated`() = testBlocking { @@ -60,11 +53,8 @@ class StoreStatsViewModelTest : BaseUnitTest() { private fun createSut() { sut = StoreStatsViewModel( - navController, - phoneRepository, - fetchStatsFromStore, - fetchStatsFromPhone, - networkStatus, + fetchStats, + Locale.getDefault(), loginRepository, SavedStateHandle() ) diff --git a/WooCommerce/src/main/AndroidManifest.xml b/WooCommerce/src/main/AndroidManifest.xml index 02c3fc65a5d..ae1e9a8c59b 100644 --- a/WooCommerce/src/main/AndroidManifest.xml +++ b/WooCommerce/src/main/AndroidManifest.xml @@ -278,6 +278,14 @@ android:pathPrefix="/request-orders" android:scheme="wear" /> + + + + + diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/wear/GetWearableOrderProducts.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/wear/GetWearableOrderProducts.kt new file mode 100644 index 00000000000..a074a5f95f1 --- /dev/null +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/wear/GetWearableOrderProducts.kt @@ -0,0 +1,28 @@ +package com.woocommerce.android.wear + +import com.woocommerce.android.model.getNonRefundedProducts +import com.woocommerce.android.ui.orders.details.OrderDetailRepository +import com.woocommerce.commons.wear.orders.WearOrderProduct +import javax.inject.Inject + +class GetWearableOrderProducts @Inject constructor( + private val orderDetailRepository: OrderDetailRepository +) { + suspend operator fun invoke( + orderId: Long + ): List { + val orderItems = orderDetailRepository + .fetchOrderById(orderId)?.items + ?: return emptyList() + + return orderDetailRepository.fetchOrderRefunds(orderId) + .getNonRefundedProducts(orderItems) + .map { + WearOrderProduct( + amount = it.quantity.toString(), + total = it.total.toString(), + name = it.name + ) + } + } +} diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/wear/WearableConnectionRepository.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/wear/WearableConnectionRepository.kt index d97b8bf9ded..691d2d7957a 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/wear/WearableConnectionRepository.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/wear/WearableConnectionRepository.kt @@ -2,6 +2,7 @@ package com.woocommerce.android.wear import com.google.android.gms.wearable.DataClient import com.google.android.gms.wearable.DataMap +import com.google.android.gms.wearable.MessageEvent import com.google.android.gms.wearable.PutDataMapRequest import com.google.gson.Gson import com.woocommerce.android.extensions.convertedFrom @@ -9,12 +10,16 @@ import com.woocommerce.android.tools.SelectedSite import com.woocommerce.commons.wear.DataParameters.CONVERSION_RATE import com.woocommerce.commons.wear.DataParameters.ORDERS_COUNT import com.woocommerce.commons.wear.DataParameters.ORDERS_JSON +import com.woocommerce.commons.wear.DataParameters.ORDER_ID +import com.woocommerce.commons.wear.DataParameters.ORDER_PRODUCTS_JSON import com.woocommerce.commons.wear.DataParameters.SITE_JSON import com.woocommerce.commons.wear.DataParameters.TIMESTAMP import com.woocommerce.commons.wear.DataParameters.TOKEN import com.woocommerce.commons.wear.DataParameters.TOTAL_REVENUE import com.woocommerce.commons.wear.DataParameters.VISITORS_TOTAL import com.woocommerce.commons.wear.DataPath +import com.woocommerce.commons.wear.DataPath.ORDERS_DATA +import com.woocommerce.commons.wear.DataPath.ORDER_PRODUCTS_DATA import com.woocommerce.commons.wear.DataPath.SITE_DATA import com.woocommerce.commons.wear.DataPath.STATS_DATA import kotlinx.coroutines.CoroutineScope @@ -34,6 +39,7 @@ class WearableConnectionRepository @Inject constructor( private val wooCommerceStore: WooCommerceStore, private val orderStore: WCOrderStore, private val getStats: GetWearableMyStoreStats, + private val getOrderProducts: GetWearableOrderProducts, private val coroutineScope: CoroutineScope ) { private val gson by lazy { Gson() } @@ -83,13 +89,32 @@ class WearableConnectionRepository @Inject constructor( ).run { this as? Success }?.orders ?: emptyList() sendData( - DataPath.ORDERS_DATA, + ORDERS_DATA, DataMap().apply { putString(ORDERS_JSON.value, gson.toJson(orders)) } ) } + fun sendOrderProductsData(message: MessageEvent) = coroutineScope.launch { + val orderId = runCatching { + message.data.toString(Charsets.UTF_8).toLong() + }.getOrNull() + + val orderProductsJson = orderId + ?.let { getOrderProducts(it) } + ?.let { gson.toJson(it) } + .orEmpty() + + sendData( + ORDER_PRODUCTS_DATA, + DataMap().apply { + putLong(ORDER_ID.value, orderId ?: -1L) + putString(ORDER_PRODUCTS_JSON.value, orderProductsJson) + } + ) + } + private fun sendData( dataPath: DataPath, data: DataMap diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/wear/WearableConnectionService.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/wear/WearableConnectionService.kt index 4ede7f8f344..ce4b5c49444 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/wear/WearableConnectionService.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/wear/WearableConnectionService.kt @@ -3,6 +3,7 @@ package com.woocommerce.android.wear import com.google.android.gms.wearable.MessageEvent import com.google.android.gms.wearable.WearableListenerService import com.woocommerce.commons.wear.MessagePath.REQUEST_ORDERS +import com.woocommerce.commons.wear.MessagePath.REQUEST_ORDER_PRODUCTS import com.woocommerce.commons.wear.MessagePath.REQUEST_SITE import com.woocommerce.commons.wear.MessagePath.REQUEST_STATS import dagger.hilt.android.AndroidEntryPoint @@ -20,6 +21,7 @@ class WearableConnectionService : WearableListenerService() { REQUEST_SITE.value -> wearableConnectionRepository.sendSiteData() REQUEST_STATS.value -> wearableConnectionRepository.sendStatsData() REQUEST_ORDERS.value -> wearableConnectionRepository.sendOrdersData() + REQUEST_ORDER_PRODUCTS.value -> wearableConnectionRepository.sendOrderProductsData(message) } } } diff --git a/libs/commons/src/main/java/com/woocommerce/commons/wear/Paths.kt b/libs/commons/src/main/java/com/woocommerce/commons/wear/Paths.kt index 5eb26586076..7f29fa39a1f 100644 --- a/libs/commons/src/main/java/com/woocommerce/commons/wear/Paths.kt +++ b/libs/commons/src/main/java/com/woocommerce/commons/wear/Paths.kt @@ -3,13 +3,15 @@ package com.woocommerce.commons.wear enum class MessagePath(val value: String) { REQUEST_SITE("/request-site"), REQUEST_STATS("/request-stats"), - REQUEST_ORDERS("/request-orders") + REQUEST_ORDERS("/request-orders"), + REQUEST_ORDER_PRODUCTS("/request-order-products") } enum class DataPath(val value: String) { SITE_DATA("/site-data"), STATS_DATA("/stats-data"), - ORDERS_DATA("/orders-data") + ORDERS_DATA("/orders-data"), + ORDER_PRODUCTS_DATA("/order-products-data") } enum class DataParameters(val value: String) { @@ -26,5 +28,9 @@ enum class DataParameters(val value: String) { CONVERSION_RATE("conversion-rate"), // Orders data - ORDERS_JSON("orders-json") + ORDERS_JSON("orders-json"), + + // Order products data + ORDER_ID("order-id"), + ORDER_PRODUCTS_JSON("order-products-json") } diff --git a/libs/commons/src/main/java/com/woocommerce/commons/wear/orders/WearOrderProduct.kt b/libs/commons/src/main/java/com/woocommerce/commons/wear/orders/WearOrderProduct.kt new file mode 100644 index 00000000000..8b31423ff79 --- /dev/null +++ b/libs/commons/src/main/java/com/woocommerce/commons/wear/orders/WearOrderProduct.kt @@ -0,0 +1,7 @@ +package com.woocommerce.commons.wear.orders + +data class WearOrderProduct( + val amount: String, + val total: String, + val name: String +)