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
+)