Skip to content

[Dynamic Dashboard] Coupons Card, part 1 #11531

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 17 commits into from
May 21, 2024
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ object AppPrefs {
CARD_READER_DO_NOT_SHOW_CASH_ON_DELIVERY_DISABLED_ONBOARDING_STATE,
ACTIVE_STATS_GRANULARITY,
ACTIVE_TOP_PERFORMERS_GRANULARITY,
DASHBOARD_COUPONS_CARD_TAB,
USE_SIMULATED_READER,
UPDATE_SIMULATED_READER_OPTION,
ENABLE_SIMULATED_INTERAC,
Expand Down Expand Up @@ -865,6 +866,12 @@ object AppPrefs {
setString(DeletablePrefKey.ACTIVE_TOP_PERFORMERS_GRANULARITY, selectionName)
}

fun getActiveCouponsTab() = getString(DeletablePrefKey.DASHBOARD_COUPONS_CARD_TAB)

fun setActiveCouponsTab(selectionName: String) {
setString(DeletablePrefKey.DASHBOARD_COUPONS_CARD_TAB, selectionName)
}

fun getActiveTopPerformersTab() = getString(DeletablePrefKey.ACTIVE_TOP_PERFORMERS_GRANULARITY)

fun setCustomDomainsSource(source: String) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -209,15 +209,19 @@ class AppPrefsWrapper @Inject constructor() {
AppPrefs.setActiveStatsTab(selectionName)
}

fun getActiveStoreStatsTab() =
AppPrefs.getActiveStatsTab()
fun getActiveStoreStatsTab() = AppPrefs.getActiveStatsTab()

fun setActiveTopPerformersTab(selectionName: String) {
AppPrefs.setActiveTopPerformersTab(selectionName)
}

fun getActiveTopPerformersTab() =
AppPrefs.getActiveTopPerformersTab()
fun getActiveTopPerformersTab() = AppPrefs.getActiveTopPerformersTab()

fun getActiveCouponsTab() = AppPrefs.getActiveCouponsTab()

fun setActiveCouponsTab(selectionName: String) {
AppPrefs.setActiveCouponsTab(selectionName)
}

fun setCustomDomainsSource(source: DomainFlowSource) {
AppPrefs.setCustomDomainsSource(source.name)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,4 +91,18 @@ class DataStoreModule {
scope = CoroutineScope(appCoroutineScope.coroutineContext + Dispatchers.IO),
serializer = CustomDateRangeSerializer
)

@Provides
@Singleton
@DataStoreQualifier(DataStoreType.COUPONS)
fun provideCouponsCustomDateRangeDataStore(
appContext: Context,
@AppCoroutineScope appCoroutineScope: CoroutineScope
): DataStore<CustomDateRange> = DataStoreFactory.create(
produceFile = {
appContext.preferencesDataStoreFile("dashboard_coupons_custom_date_range_configuration")
},
scope = CoroutineScope(appCoroutineScope.coroutineContext + Dispatchers.IO),
serializer = CustomDateRangeSerializer
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@ enum class DataStoreType {
ANALYTICS_UI_CACHE,
ANALYTICS_CONFIGURATION,
DASHBOARD_STATS,
TOP_PERFORMER_PRODUCTS
TOP_PERFORMER_PRODUCTS,
COUPONS
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ data class DashboardWidget(
BLAZE(R.string.my_store_widget_blaze_title, "blaze"),
REVIEWS(R.string.my_store_widget_reviews_title, "reviews"),
ORDERS(R.string.my_store_widget_orders_title, "orders"),
COUPONS(R.string.my_store_widget_coupons_title, "coupons"),
}

sealed interface Status : Parcelable {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ import com.woocommerce.android.model.Coupon
import com.woocommerce.android.model.CouponPerformanceReport
import com.woocommerce.android.model.toAppModel
import com.woocommerce.android.tools.SelectedSite
import com.woocommerce.android.ui.analytics.ranges.StatsTimeRange
import com.woocommerce.android.util.DateUtils
import com.woocommerce.android.util.WooLog
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.map
Expand All @@ -26,9 +26,16 @@ class CouponRepository @Inject constructor(
) {
suspend fun fetchCoupons(
page: Int,
pageSize: Int
pageSize: Int,
couponIds: List<Long> = emptyList()
): Result<Boolean> {
return store.fetchCoupons(selectedSite.get(), page, pageSize)
return store.fetchCoupons(
site = selectedSite.get(),
page = page,
pageSize = pageSize,
couponIds = couponIds,
deleteOldData = page == 1 && couponIds.isEmpty()
)
.let { result ->
if (result.isError) {
analyticsTrackerWrapper.track(
Expand Down Expand Up @@ -84,8 +91,10 @@ class CouponRepository @Inject constructor(
}
}

@OptIn(ExperimentalCoroutinesApi::class)
fun observeCoupons(): Flow<List<Coupon>> = store.observeCoupons(selectedSite.get()).map {
fun observeCoupons(couponIds: List<Long> = emptyList()): Flow<List<Coupon>> = store.observeCoupons(
site = selectedSite.get(),
couponIds = couponIds
).map {
it.map { couponDataModel -> couponDataModel.toAppModel() }
}

Expand All @@ -110,6 +119,22 @@ class CouponRepository @Inject constructor(
}
}

suspend fun fetchMostActiveCoupons(
dateRange: StatsTimeRange,
limit: Int
): Result<List<CouponPerformanceReport>> {
val result = store.fetchMostActiveCoupons(
site = selectedSite.get(),
dateRange = dateRange.start..dateRange.end,
limit = limit
)

return when {
result.isError -> Result.failure(WooException(result.error))
else -> Result.success(result.model!!.map { it.toAppModel() })
}
}

suspend fun deleteCoupon(couponId: Long): Result<Unit> {
val result = store.deleteCoupon(
site = selectedSite.get(),
Expand Down Expand Up @@ -152,6 +177,10 @@ class CouponRepository @Inject constructor(
}
}

suspend fun getCoupons(couponIds: List<Long>): List<Coupon> {
return store.getCoupons(selectedSite.get(), couponIds).map { it.toAppModel() }
}

private fun Coupon.createUpdateCouponRequest() =
UpdateCouponRequest(
code = code,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import com.woocommerce.android.ui.compose.component.WCColoredButton
import com.woocommerce.android.ui.compose.component.WCOutlinedButton
import com.woocommerce.android.ui.dashboard.DashboardViewModel.DashboardEvent.OpenRangePicker
import com.woocommerce.android.ui.dashboard.blaze.DashboardBlazeCard
import com.woocommerce.android.ui.dashboard.coupons.DashboardCouponsCard
import com.woocommerce.android.ui.dashboard.onboarding.DashboardOnboardingCard
import com.woocommerce.android.ui.dashboard.orders.DashboardOrdersCard
import com.woocommerce.android.ui.dashboard.reviews.DashboardReviewsCard
Expand Down Expand Up @@ -144,6 +145,11 @@ private fun ConfigurableWidgetCard(
parentViewModel = dashboardViewModel,
modifier = modifier
)

DashboardWidget.Type.COUPONS -> DashboardCouponsCard(
parentViewModel = dashboardViewModel,
modifier = modifier
)
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.woocommerce.android.ui.dashboard.stats
package com.woocommerce.android.ui.dashboard

import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
Expand Down Expand Up @@ -34,10 +34,10 @@ import com.woocommerce.android.R
import com.woocommerce.android.R.dimen
import com.woocommerce.android.ui.analytics.ranges.StatsTimeRangeSelection
import com.woocommerce.android.ui.analytics.ranges.StatsTimeRangeSelection.SelectionType
import com.woocommerce.android.ui.dashboard.DashboardViewModel
import com.woocommerce.android.ui.dashboard.stats.DashboardStatsTestTags

@Composable
fun DashboardStatsHeader(
fun DashboardDateRangeHeader(
rangeSelection: StatsTimeRangeSelection,
dateFormatted: String,
onCustomRangeClick: () -> Unit,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
package com.woocommerce.android.ui.dashboard.coupons

import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material.CircularProgressIndicator
import androidx.compose.material.Divider
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.lifecycle.LiveData
import androidx.lifecycle.Observer
import com.woocommerce.android.model.DashboardWidget.Type.COUPONS
import com.woocommerce.android.ui.analytics.ranges.StatsTimeRange
import com.woocommerce.android.ui.analytics.ranges.StatsTimeRangeSelection.SelectionType
import com.woocommerce.android.ui.compose.rememberNavController
import com.woocommerce.android.ui.compose.viewModelWithFactory
import com.woocommerce.android.ui.dashboard.DashboardDateRangeHeader
import com.woocommerce.android.ui.dashboard.DashboardViewModel
import com.woocommerce.android.ui.dashboard.DashboardViewModel.DashboardWidgetMenu
import com.woocommerce.android.ui.dashboard.WidgetCard
import com.woocommerce.android.ui.dashboard.WidgetError
import com.woocommerce.android.ui.dashboard.coupons.DashboardCouponsViewModel.DateRangeState
import com.woocommerce.android.ui.dashboard.coupons.DashboardCouponsViewModel.State
import com.woocommerce.android.ui.dashboard.coupons.DashboardCouponsViewModel.State.Error
import com.woocommerce.android.ui.dashboard.coupons.DashboardCouponsViewModel.State.Loaded
import com.woocommerce.android.ui.dashboard.coupons.DashboardCouponsViewModel.State.Loading
import com.woocommerce.android.viewmodel.MultiLiveEvent
import java.util.Date

@Composable
fun DashboardCouponsCard(
parentViewModel: DashboardViewModel,
modifier: Modifier = Modifier,
viewModel: DashboardCouponsViewModel = viewModelWithFactory { factory: DashboardCouponsViewModel.Factory ->
factory.create(parentViewModel)
}
) {
val viewState = viewModel.viewState.observeAsState().value ?: return
val dateRangeState = viewModel.dateRangeState.observeAsState().value ?: return

HandleEvents(
event = viewModel.event,
openDatePicker = { fromDate, toDate ->
parentViewModel.onDashboardWidgetEvent(
DashboardViewModel.DashboardEvent.OpenRangePicker(fromDate, toDate) { from, to ->
viewModel.onCustomRangeSelected(StatsTimeRange(Date(from), Date(to)))
}
)
}
)

DashboardCouponsCard(
dateRangeState = dateRangeState,
viewState = viewState,
onTabSelected = viewModel::onTabSelected,
onCustomRangeClick = viewModel::onEditCustomRangeTapped,
modifier = modifier
)
}

@Composable
private fun HandleEvents(
event: LiveData<MultiLiveEvent.Event>,
openDatePicker: (Long, Long) -> Unit,
) {
val navController = rememberNavController()
val lifecycleOwner = LocalLifecycleOwner.current

DisposableEffect(event, navController, lifecycleOwner) {
val observer = Observer { event: MultiLiveEvent.Event ->
when (event) {
is DashboardCouponsViewModel.OpenDatePicker -> {
openDatePicker(event.fromDate.time, event.toDate.time)
}
}
}

event.observe(lifecycleOwner, observer)

onDispose {
event.removeObserver(observer)
}
}
}

@Composable
private fun DashboardCouponsCard(
dateRangeState: DateRangeState,
viewState: State,
onTabSelected: (SelectionType) -> Unit,
onCustomRangeClick: () -> Unit,
modifier: Modifier = Modifier
) {
WidgetCard(
titleResource = COUPONS.titleResource,
menu = DashboardWidgetMenu(emptyList()),
isError = viewState is Error,
modifier = modifier
) {
Column {
DashboardDateRangeHeader(
rangeSelection = dateRangeState.rangeSelection,
dateFormatted = dateRangeState.rangeFormatted,
onCustomRangeClick = onCustomRangeClick,
onTabSelected = onTabSelected,
modifier = Modifier.fillMaxWidth()
)

Divider()

when (viewState) {
is Loading -> {
CircularProgressIndicator()
}

is Loaded -> {
DashboardCouponsList(viewState)
}

is Error -> {
WidgetError(
onContactSupportClicked = { /*TODO*/ },
onRetryClicked = { /*TODO*/ }
)
}
}
}
}
}

@Composable
private fun DashboardCouponsList(
state: Loaded,
modifier: Modifier = Modifier
) {
Column(modifier) {
state.coupons.forEach {
Text(text = it.code)
Divider()
}
}
}
Loading
Loading