From 8e03f9b5e64bde68f49e81bd0ae85e289e2c3602 Mon Sep 17 00:00:00 2001 From: Andrey Date: Mon, 6 Jan 2025 14:55:00 +0100 Subject: [PATCH 01/12] Previews with theme --- .../android/ui/prefs/plugins/PluginsScreen.kt | 45 +++++++++++-------- 1 file changed, 26 insertions(+), 19 deletions(-) diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/prefs/plugins/PluginsScreen.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/prefs/plugins/PluginsScreen.kt index bfd66d7571c..e3e442dfa59 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/prefs/plugins/PluginsScreen.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/prefs/plugins/PluginsScreen.kt @@ -44,6 +44,7 @@ import com.woocommerce.android.extensions.isNotNullOrEmpty import com.woocommerce.android.ui.compose.component.Toolbar import com.woocommerce.android.ui.compose.component.WCColoredButton import com.woocommerce.android.ui.compose.preview.LightDarkThemePreviews +import com.woocommerce.android.ui.compose.theme.WooThemeWithBackground import com.woocommerce.android.ui.prefs.plugins.PluginsViewModel.ViewState import com.woocommerce.android.ui.prefs.plugins.PluginsViewModel.ViewState.Loaded.Plugin import com.woocommerce.android.ui.prefs.plugins.PluginsViewModel.ViewState.Loaded.Plugin.PluginStatus.Inactive @@ -252,33 +253,39 @@ private fun Error(onRetryTapped: () -> Unit) { @LightDarkThemePreviews @Composable private fun PreviewPlugins() { - PluginsScreen( - ViewState.Loaded( - plugins = listOf( - Plugin("Plugin 1", "Automattic", "1.0", UpToDate("Up-to-date")), - Plugin("Plugin 2", null, "2.0", UpdateAvailable("Update available (4.9)")), - Plugin("Plugin 3", "Gutenberg", "3.0", Inactive("Inactive")), - Plugin("Plugin 5", "Blabla", "5.0", Unknown) - ) - ), - onRetryTapped = {} - ) + WooThemeWithBackground { + PluginsScreen( + ViewState.Loaded( + plugins = listOf( + Plugin("Plugin 1", "Automattic", "1.0", UpToDate("Up-to-date")), + Plugin("Plugin 2", null, "2.0", UpdateAvailable("Update available (4.9)")), + Plugin("Plugin 3", "Gutenberg", "3.0", Inactive("Inactive")), + Plugin("Plugin 5", "Blabla", "5.0", Unknown) + ) + ), + onRetryTapped = {} + ) + } } @LightDarkThemePreviews @Composable private fun PreviewError() { - PluginsScreen( - ViewState.Error, - onRetryTapped = {} - ) + WooThemeWithBackground { + PluginsScreen( + ViewState.Error, + onRetryTapped = {} + ) + } } @LightDarkThemePreviews @Composable private fun PreviewLoading() { - PluginsScreen( - ViewState.Loading, - onRetryTapped = {} - ) + WooThemeWithBackground { + PluginsScreen( + ViewState.Loading, + onRetryTapped = {} + ) + } } From 65d7f68d9124c07798f35f48ce8c0437026cf419 Mon Sep 17 00:00:00 2001 From: Andrey Date: Mon, 6 Jan 2025 16:03:34 +0100 Subject: [PATCH 02/12] Changes in the UI to hint that the row is clickable --- .../android/ui/prefs/plugins/PluginsScreen.kt | 44 ++++++++++++++++--- .../ui/prefs/plugins/PluginsViewModel.kt | 23 +++++----- 2 files changed, 51 insertions(+), 16 deletions(-) diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/prefs/plugins/PluginsScreen.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/prefs/plugins/PluginsScreen.kt index e3e442dfa59..333fa92b8b6 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/prefs/plugins/PluginsScreen.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/prefs/plugins/PluginsScreen.kt @@ -16,16 +16,19 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.material.Divider +import androidx.compose.material.Icon import androidx.compose.material.MaterialTheme import androidx.compose.material.Scaffold import androidx.compose.material.Surface import androidx.compose.material.Text import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material.icons.filled.Refresh import androidx.compose.runtime.Composable import androidx.compose.runtime.livedata.observeAsState import androidx.compose.ui.Alignment @@ -83,9 +86,11 @@ private fun PluginsScreen(state: ViewState, onRetryTapped: () -> Unit) { is ViewState.Loading -> { ShimmerPluginsList() } + is ViewState.Error -> { Error(onRetryTapped) } + is ViewState.Loaded -> { Plugins(it.plugins) } @@ -134,12 +139,41 @@ private fun PluginItem(plugin: Plugin) { ) } - if (plugin.status !is Unknown) { - Text( + when (plugin.status) { + is Inactive -> Text( text = plugin.status.title, color = colorResource(id = plugin.status.color), fontWeight = FontWeight.Bold ) + + is UpToDate -> Text( + text = plugin.status.title, + color = colorResource(id = plugin.status.color), + fontWeight = FontWeight.Bold + ) + + is UpdateAvailable -> { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + Icon( + imageVector = Icons.Default.Refresh, + contentDescription = plugin.status.title, + tint = colorResource(id = plugin.status.color), + modifier = Modifier + .size(24.dp) + ) + + Text( + text = plugin.status.title, + color = colorResource(id = plugin.status.color), + fontWeight = FontWeight.Bold + ) + } + } + + Unknown -> {} } } } @@ -257,9 +291,9 @@ private fun PreviewPlugins() { PluginsScreen( ViewState.Loaded( plugins = listOf( - Plugin("Plugin 1", "Automattic", "1.0", UpToDate("Up-to-date")), - Plugin("Plugin 2", null, "2.0", UpdateAvailable("Update available (4.9)")), - Plugin("Plugin 3", "Gutenberg", "3.0", Inactive("Inactive")), + Plugin("Plugin 1", "Automattic", "1.0", UpToDate("Up-to-date", R.color.color_info)), + Plugin("Plugin 2", "Something", "2.0", UpdateAvailable("Update available (4.9)", R.color.color_primary)), + Plugin("Plugin 3", "Gutenberg", "3.0", Inactive("Inactive", R.color.color_on_surface_disabled)), Plugin("Plugin 5", "Blabla", "5.0", Unknown) ) ), diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/prefs/plugins/PluginsViewModel.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/prefs/plugins/PluginsViewModel.kt index 591bee9769b..5be0f00f0c9 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/prefs/plugins/PluginsViewModel.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/prefs/plugins/PluginsViewModel.kt @@ -69,12 +69,16 @@ class PluginsViewModel @Inject constructor( private fun SystemPluginModel.getState(): Plugin.PluginStatus { return when { - !isActive -> Inactive(resourceProvider.getString(R.string.plugin_state_inactive)) + !isActive -> Inactive( + resourceProvider.getString(R.string.plugin_state_inactive), + R.color.color_on_surface_disabled, + ) versionLatest.isNullOrEmpty() -> Unknown isUpdateAvailable() -> UpdateAvailable( - resourceProvider.getString(R.string.plugin_state_update_available, versionLatest!!) + resourceProvider.getString(R.string.plugin_state_update_available, versionLatest!!), + R.color.color_primary, ) - else -> UpToDate(resourceProvider.getString(R.string.plugin_state_up_to_date)) + else -> UpToDate(resourceProvider.getString(R.string.plugin_state_up_to_date), R.color.color_info) } } @@ -102,14 +106,11 @@ class PluginsViewModel @Inject constructor( val version: String, val status: PluginStatus ) { - sealed class PluginStatus(open val title: String, @ColorRes val color: Int) { - data class UpToDate(override val title: String) : PluginStatus(title, R.color.color_info) - data class UpdateAvailable(override val title: String) : PluginStatus(title, R.color.color_alert) - data class Inactive(override val title: String) : PluginStatus( - title, - R.color.color_on_surface_disabled - ) - data object Unknown : PluginStatus("", R.color.color_on_surface_disabled) + sealed class PluginStatus { + data class UpToDate(val title: String, @ColorRes val color: Int) : PluginStatus() + data class UpdateAvailable(val title: String, @ColorRes val color: Int) : PluginStatus() + data class Inactive(val title: String, @ColorRes val color: Int) : PluginStatus() + data object Unknown : PluginStatus() } } } From df1f80f4078373148bac508a636acd2f22cab0bf Mon Sep 17 00:00:00 2001 From: Andrey Date: Mon, 6 Jan 2025 16:11:46 +0100 Subject: [PATCH 03/12] Click on plugin. Removed unknown status --- .../android/ui/prefs/plugins/PluginsScreen.kt | 40 ++++++++++++++----- .../ui/prefs/plugins/PluginsViewModel.kt | 33 ++++++++++----- 2 files changed, 53 insertions(+), 20 deletions(-) diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/prefs/plugins/PluginsScreen.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/prefs/plugins/PluginsScreen.kt index 333fa92b8b6..3303cccf9a6 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/prefs/plugins/PluginsScreen.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/prefs/plugins/PluginsScreen.kt @@ -8,6 +8,7 @@ import androidx.compose.animation.core.infiniteRepeatable import androidx.compose.animation.core.rememberInfiniteTransition import androidx.compose.animation.core.tween 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 @@ -73,14 +74,22 @@ fun PluginsScreen(viewModel: PluginsViewModel) { .padding(paddingValues) ) { viewModel.viewState.observeAsState().value?.let { state -> - PluginsScreen(state, viewModel::onRetryClicked) + PluginsScreen( + state = state, + onRetryTapped = viewModel::onRetryClicked, + onPluginClicked = viewModel::onPluginClicked, + ) } } } } @Composable -private fun PluginsScreen(state: ViewState, onRetryTapped: () -> Unit) { +private fun PluginsScreen( + state: ViewState, + onRetryTapped: () -> Unit, + onPluginClicked: (Plugin) -> Unit, +) { Crossfade(targetState = state, label = "") { when (it) { is ViewState.Loading -> { @@ -92,17 +101,23 @@ private fun PluginsScreen(state: ViewState, onRetryTapped: () -> Unit) { } is ViewState.Loaded -> { - Plugins(it.plugins) + Plugins( + it.plugins, + onPluginClicked, + ) } } } } @Composable -private fun Plugins(plugins: List) { +private fun Plugins( + plugins: List, + onPluginClicked: (Plugin) -> Unit +) { LazyColumn { items(plugins) { plugin -> - PluginItem(plugin) + PluginItem(plugin, onPluginClicked) if (plugins.last() != plugin) { Divider() @@ -112,11 +127,15 @@ private fun Plugins(plugins: List) { } @Composable -private fun PluginItem(plugin: Plugin) { +private fun PluginItem( + plugin: Plugin, + onPluginClicked: (Plugin) -> Unit +) { Row( modifier = Modifier .fillMaxWidth() .padding(dimensionResource(R.dimen.major_100)) + .clickable(onClick = { onPluginClicked(plugin) }) ) { Column( modifier = Modifier @@ -297,7 +316,8 @@ private fun PreviewPlugins() { Plugin("Plugin 5", "Blabla", "5.0", Unknown) ) ), - onRetryTapped = {} + onRetryTapped = {}, + onPluginClicked = {}, ) } } @@ -308,7 +328,8 @@ private fun PreviewError() { WooThemeWithBackground { PluginsScreen( ViewState.Error, - onRetryTapped = {} + onRetryTapped = {}, + onPluginClicked = {}, ) } } @@ -319,7 +340,8 @@ private fun PreviewLoading() { WooThemeWithBackground { PluginsScreen( ViewState.Loading, - onRetryTapped = {} + onRetryTapped = {}, + onPluginClicked = {}, ) } } diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/prefs/plugins/PluginsViewModel.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/prefs/plugins/PluginsViewModel.kt index 5be0f00f0c9..41a11bd262d 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/prefs/plugins/PluginsViewModel.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/prefs/plugins/PluginsViewModel.kt @@ -11,7 +11,6 @@ import com.woocommerce.android.ui.prefs.plugins.PluginsViewModel.ViewState.Error import com.woocommerce.android.ui.prefs.plugins.PluginsViewModel.ViewState.Loaded import com.woocommerce.android.ui.prefs.plugins.PluginsViewModel.ViewState.Loaded.Plugin import com.woocommerce.android.ui.prefs.plugins.PluginsViewModel.ViewState.Loaded.Plugin.PluginStatus.Inactive -import com.woocommerce.android.ui.prefs.plugins.PluginsViewModel.ViewState.Loaded.Plugin.PluginStatus.Unknown import com.woocommerce.android.ui.prefs.plugins.PluginsViewModel.ViewState.Loaded.Plugin.PluginStatus.UpToDate import com.woocommerce.android.ui.prefs.plugins.PluginsViewModel.ViewState.Loaded.Plugin.PluginStatus.UpdateAvailable import com.woocommerce.android.ui.prefs.plugins.PluginsViewModel.ViewState.Loading @@ -51,13 +50,18 @@ class PluginsViewModel @Inject constructor( Loaded( plugins = response.model!! .filter { it.version.isNotNullOrEmpty() && it.name.isNotNullOrEmpty() } - .map { - Plugin( - name = StringEscapeUtils.unescapeHtml4(it.name), - authorName = StringEscapeUtils.unescapeHtml4(it.authorName), - version = it.version!!, - status = it.getState() - ) + .mapNotNull { + val status = it.getStatus() + if (status == null) { + null + } else { + Plugin( + name = StringEscapeUtils.unescapeHtml4(it.name), + authorName = StringEscapeUtils.unescapeHtml4(it.authorName), + version = it.version!!, + status = status + ) + } } ) ) @@ -67,13 +71,13 @@ class PluginsViewModel @Inject constructor( } } - private fun SystemPluginModel.getState(): Plugin.PluginStatus { + private fun SystemPluginModel.getStatus(): Plugin.PluginStatus? { return when { !isActive -> Inactive( resourceProvider.getString(R.string.plugin_state_inactive), R.color.color_on_surface_disabled, ) - versionLatest.isNullOrEmpty() -> Unknown + versionLatest.isNullOrEmpty() -> null isUpdateAvailable() -> UpdateAvailable( resourceProvider.getString(R.string.plugin_state_update_available, versionLatest!!), R.color.color_primary, @@ -90,6 +94,14 @@ class PluginsViewModel @Inject constructor( loadPlugins() } + fun onPluginClicked(plugin: Plugin) { + when (plugin.status) { + is Inactive -> {} + is UpToDate -> TODO() + is UpdateAvailable -> TODO() + } + } + fun onBackPressed() { triggerEvent(Exit) } @@ -110,7 +122,6 @@ class PluginsViewModel @Inject constructor( data class UpToDate(val title: String, @ColorRes val color: Int) : PluginStatus() data class UpdateAvailable(val title: String, @ColorRes val color: Int) : PluginStatus() data class Inactive(val title: String, @ColorRes val color: Int) : PluginStatus() - data object Unknown : PluginStatus() } } } From ba0769615c9b4aae8decceed6e1ab3865e991de7 Mon Sep 17 00:00:00 2001 From: Andrey Date: Mon, 6 Jan 2025 16:12:29 +0100 Subject: [PATCH 04/12] Returned back unknown status --- .../ui/prefs/plugins/PluginsViewModel.kt | 26 +++++++++---------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/prefs/plugins/PluginsViewModel.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/prefs/plugins/PluginsViewModel.kt index 41a11bd262d..639a2979680 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/prefs/plugins/PluginsViewModel.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/prefs/plugins/PluginsViewModel.kt @@ -11,6 +11,7 @@ import com.woocommerce.android.ui.prefs.plugins.PluginsViewModel.ViewState.Error import com.woocommerce.android.ui.prefs.plugins.PluginsViewModel.ViewState.Loaded import com.woocommerce.android.ui.prefs.plugins.PluginsViewModel.ViewState.Loaded.Plugin import com.woocommerce.android.ui.prefs.plugins.PluginsViewModel.ViewState.Loaded.Plugin.PluginStatus.Inactive +import com.woocommerce.android.ui.prefs.plugins.PluginsViewModel.ViewState.Loaded.Plugin.PluginStatus.Unknown import com.woocommerce.android.ui.prefs.plugins.PluginsViewModel.ViewState.Loaded.Plugin.PluginStatus.UpToDate import com.woocommerce.android.ui.prefs.plugins.PluginsViewModel.ViewState.Loaded.Plugin.PluginStatus.UpdateAvailable import com.woocommerce.android.ui.prefs.plugins.PluginsViewModel.ViewState.Loading @@ -50,18 +51,13 @@ class PluginsViewModel @Inject constructor( Loaded( plugins = response.model!! .filter { it.version.isNotNullOrEmpty() && it.name.isNotNullOrEmpty() } - .mapNotNull { - val status = it.getStatus() - if (status == null) { - null - } else { - Plugin( - name = StringEscapeUtils.unescapeHtml4(it.name), - authorName = StringEscapeUtils.unescapeHtml4(it.authorName), - version = it.version!!, - status = status - ) - } + .map { + Plugin( + name = StringEscapeUtils.unescapeHtml4(it.name), + authorName = StringEscapeUtils.unescapeHtml4(it.authorName), + version = it.version!!, + status = it.getState() + ) } ) ) @@ -71,13 +67,13 @@ class PluginsViewModel @Inject constructor( } } - private fun SystemPluginModel.getStatus(): Plugin.PluginStatus? { + private fun SystemPluginModel.getState(): Plugin.PluginStatus? { return when { !isActive -> Inactive( resourceProvider.getString(R.string.plugin_state_inactive), R.color.color_on_surface_disabled, ) - versionLatest.isNullOrEmpty() -> null + versionLatest.isNullOrEmpty() -> Unknown isUpdateAvailable() -> UpdateAvailable( resourceProvider.getString(R.string.plugin_state_update_available, versionLatest!!), R.color.color_primary, @@ -97,6 +93,7 @@ class PluginsViewModel @Inject constructor( fun onPluginClicked(plugin: Plugin) { when (plugin.status) { is Inactive -> {} + Unknown -> {} is UpToDate -> TODO() is UpdateAvailable -> TODO() } @@ -122,6 +119,7 @@ class PluginsViewModel @Inject constructor( data class UpToDate(val title: String, @ColorRes val color: Int) : PluginStatus() data class UpdateAvailable(val title: String, @ColorRes val color: Int) : PluginStatus() data class Inactive(val title: String, @ColorRes val color: Int) : PluginStatus() + data object Unknown : PluginStatus() } } } From ea01f72e277a27c01813e7048088d5424e65b8ed Mon Sep 17 00:00:00 2001 From: Andrey Date: Mon, 6 Jan 2025 16:13:05 +0100 Subject: [PATCH 05/12] Renamed state to status in the function name --- .../woocommerce/android/ui/prefs/plugins/PluginsViewModel.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/prefs/plugins/PluginsViewModel.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/prefs/plugins/PluginsViewModel.kt index 639a2979680..b741bb1f48e 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/prefs/plugins/PluginsViewModel.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/prefs/plugins/PluginsViewModel.kt @@ -56,7 +56,7 @@ class PluginsViewModel @Inject constructor( name = StringEscapeUtils.unescapeHtml4(it.name), authorName = StringEscapeUtils.unescapeHtml4(it.authorName), version = it.version!!, - status = it.getState() + status = it.getStatus() ) } ) @@ -67,7 +67,7 @@ class PluginsViewModel @Inject constructor( } } - private fun SystemPluginModel.getState(): Plugin.PluginStatus? { + private fun SystemPluginModel.getStatus(): Plugin.PluginStatus { return when { !isActive -> Inactive( resourceProvider.getString(R.string.plugin_state_inactive), From a5d822a82d2499dd3ba3ed78dba3fa426be8757d Mon Sep 17 00:00:00 2001 From: Andrey Date: Mon, 6 Jan 2025 16:23:10 +0100 Subject: [PATCH 06/12] Handle click --- .../woocommerce/android/ui/prefs/plugins/PluginsEvent.kt | 7 +++++++ .../android/ui/prefs/plugins/PluginsFragment.kt | 5 +++++ .../android/ui/prefs/plugins/PluginsViewModel.kt | 7 +++++-- 3 files changed, 17 insertions(+), 2 deletions(-) create mode 100644 WooCommerce/src/main/kotlin/com/woocommerce/android/ui/prefs/plugins/PluginsEvent.kt diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/prefs/plugins/PluginsEvent.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/prefs/plugins/PluginsEvent.kt new file mode 100644 index 00000000000..ed659390759 --- /dev/null +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/prefs/plugins/PluginsEvent.kt @@ -0,0 +1,7 @@ +package com.woocommerce.android.ui.prefs.plugins + +import com.woocommerce.android.viewmodel.MultiLiveEvent + +sealed class PluginsEvent : MultiLiveEvent.Event() { + data class NavigateToPluginsWeb(val url: String) : PluginsEvent() +} diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/prefs/plugins/PluginsFragment.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/prefs/plugins/PluginsFragment.kt index b1c12d44c29..7f43878b59e 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/prefs/plugins/PluginsFragment.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/prefs/plugins/PluginsFragment.kt @@ -7,6 +7,7 @@ import android.view.ViewGroup import androidx.compose.ui.platform.ComposeView import androidx.fragment.app.viewModels import androidx.navigation.fragment.findNavController +import com.woocommerce.android.NavGraphSettingsDirections import com.woocommerce.android.analytics.AnalyticsTracker import com.woocommerce.android.ui.base.BaseFragment import com.woocommerce.android.ui.compose.theme.WooThemeWithBackground @@ -46,6 +47,10 @@ class PluginsFragment : BaseFragment() { viewModel.event.observe(viewLifecycleOwner) { event -> when (event) { is MultiLiveEvent.Event.Exit -> findNavController().navigateUp() + is PluginsEvent.NavigateToPluginsWeb -> { + findNavController() + .navigate(NavGraphSettingsDirections.actionGlobalWPComWebViewFragment(event.url)) + } } } } diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/prefs/plugins/PluginsViewModel.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/prefs/plugins/PluginsViewModel.kt index b741bb1f48e..abd4dcbe25a 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/prefs/plugins/PluginsViewModel.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/prefs/plugins/PluginsViewModel.kt @@ -5,6 +5,7 @@ import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.asLiveData import androidx.lifecycle.viewModelScope import com.woocommerce.android.R +import com.woocommerce.android.extensions.adminUrlOrDefault import com.woocommerce.android.extensions.isNotNullOrEmpty import com.woocommerce.android.tools.SelectedSite import com.woocommerce.android.ui.prefs.plugins.PluginsViewModel.ViewState.Error @@ -94,8 +95,10 @@ class PluginsViewModel @Inject constructor( when (plugin.status) { is Inactive -> {} Unknown -> {} - is UpToDate -> TODO() - is UpdateAvailable -> TODO() + is UpToDate -> {} + is UpdateAvailable -> { + triggerEvent(PluginsEvent.NavigateToPluginsWeb("${site.get().adminUrlOrDefault}/plugins.php")) + } } } From cf65ddf36b87cc42bf79cc2c740e52e2697ea7f2 Mon Sep 17 00:00:00 2001 From: Andrey Date: Mon, 6 Jan 2025 16:31:52 +0100 Subject: [PATCH 07/12] Change order of the padding and clickable to have proper reaction on click --- .../com/woocommerce/android/ui/prefs/plugins/PluginsScreen.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/prefs/plugins/PluginsScreen.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/prefs/plugins/PluginsScreen.kt index 3303cccf9a6..da412e744c6 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/prefs/plugins/PluginsScreen.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/prefs/plugins/PluginsScreen.kt @@ -134,8 +134,8 @@ private fun PluginItem( Row( modifier = Modifier .fillMaxWidth() - .padding(dimensionResource(R.dimen.major_100)) .clickable(onClick = { onPluginClicked(plugin) }) + .padding(dimensionResource(R.dimen.major_100)) ) { Column( modifier = Modifier From 1fadd3923883518761b4c7340175d0bf406e26b2 Mon Sep 17 00:00:00 2001 From: Andrey Date: Mon, 6 Jan 2025 16:35:57 +0100 Subject: [PATCH 08/12] Move view state to a separate class --- .../android/ui/prefs/plugins/PluginsScreen.kt | 25 ++++++----- .../ui/prefs/plugins/PluginsViewModel.kt | 41 ++++--------------- .../ui/prefs/plugins/PluginsViewState.kt | 25 +++++++++++ 3 files changed, 46 insertions(+), 45 deletions(-) create mode 100644 WooCommerce/src/main/kotlin/com/woocommerce/android/ui/prefs/plugins/PluginsViewState.kt diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/prefs/plugins/PluginsScreen.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/prefs/plugins/PluginsScreen.kt index da412e744c6..dcfe8265a20 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/prefs/plugins/PluginsScreen.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/prefs/plugins/PluginsScreen.kt @@ -49,12 +49,11 @@ import com.woocommerce.android.ui.compose.component.Toolbar import com.woocommerce.android.ui.compose.component.WCColoredButton import com.woocommerce.android.ui.compose.preview.LightDarkThemePreviews import com.woocommerce.android.ui.compose.theme.WooThemeWithBackground -import com.woocommerce.android.ui.prefs.plugins.PluginsViewModel.ViewState -import com.woocommerce.android.ui.prefs.plugins.PluginsViewModel.ViewState.Loaded.Plugin -import com.woocommerce.android.ui.prefs.plugins.PluginsViewModel.ViewState.Loaded.Plugin.PluginStatus.Inactive -import com.woocommerce.android.ui.prefs.plugins.PluginsViewModel.ViewState.Loaded.Plugin.PluginStatus.Unknown -import com.woocommerce.android.ui.prefs.plugins.PluginsViewModel.ViewState.Loaded.Plugin.PluginStatus.UpToDate -import com.woocommerce.android.ui.prefs.plugins.PluginsViewModel.ViewState.Loaded.Plugin.PluginStatus.UpdateAvailable +import com.woocommerce.android.ui.prefs.plugins.PluginsViewState.Loaded.Plugin +import com.woocommerce.android.ui.prefs.plugins.PluginsViewState.Loaded.Plugin.PluginStatus.Inactive +import com.woocommerce.android.ui.prefs.plugins.PluginsViewState.Loaded.Plugin.PluginStatus.Unknown +import com.woocommerce.android.ui.prefs.plugins.PluginsViewState.Loaded.Plugin.PluginStatus.UpToDate +import com.woocommerce.android.ui.prefs.plugins.PluginsViewState.Loaded.Plugin.PluginStatus.UpdateAvailable @Composable fun PluginsScreen(viewModel: PluginsViewModel) { @@ -86,21 +85,21 @@ fun PluginsScreen(viewModel: PluginsViewModel) { @Composable private fun PluginsScreen( - state: ViewState, + state: PluginsViewState, onRetryTapped: () -> Unit, onPluginClicked: (Plugin) -> Unit, ) { Crossfade(targetState = state, label = "") { when (it) { - is ViewState.Loading -> { + is PluginsViewState.Loading -> { ShimmerPluginsList() } - is ViewState.Error -> { + is PluginsViewState.Error -> { Error(onRetryTapped) } - is ViewState.Loaded -> { + is PluginsViewState.Loaded -> { Plugins( it.plugins, onPluginClicked, @@ -308,7 +307,7 @@ private fun Error(onRetryTapped: () -> Unit) { private fun PreviewPlugins() { WooThemeWithBackground { PluginsScreen( - ViewState.Loaded( + PluginsViewState.Loaded( plugins = listOf( Plugin("Plugin 1", "Automattic", "1.0", UpToDate("Up-to-date", R.color.color_info)), Plugin("Plugin 2", "Something", "2.0", UpdateAvailable("Update available (4.9)", R.color.color_primary)), @@ -327,7 +326,7 @@ private fun PreviewPlugins() { private fun PreviewError() { WooThemeWithBackground { PluginsScreen( - ViewState.Error, + PluginsViewState.Error, onRetryTapped = {}, onPluginClicked = {}, ) @@ -339,7 +338,7 @@ private fun PreviewError() { private fun PreviewLoading() { WooThemeWithBackground { PluginsScreen( - ViewState.Loading, + PluginsViewState.Loading, onRetryTapped = {}, onPluginClicked = {}, ) diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/prefs/plugins/PluginsViewModel.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/prefs/plugins/PluginsViewModel.kt index abd4dcbe25a..e9d25313da4 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/prefs/plugins/PluginsViewModel.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/prefs/plugins/PluginsViewModel.kt @@ -1,6 +1,5 @@ package com.woocommerce.android.ui.prefs.plugins -import androidx.annotation.ColorRes import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.asLiveData import androidx.lifecycle.viewModelScope @@ -8,14 +7,14 @@ import com.woocommerce.android.R import com.woocommerce.android.extensions.adminUrlOrDefault import com.woocommerce.android.extensions.isNotNullOrEmpty import com.woocommerce.android.tools.SelectedSite -import com.woocommerce.android.ui.prefs.plugins.PluginsViewModel.ViewState.Error -import com.woocommerce.android.ui.prefs.plugins.PluginsViewModel.ViewState.Loaded -import com.woocommerce.android.ui.prefs.plugins.PluginsViewModel.ViewState.Loaded.Plugin -import com.woocommerce.android.ui.prefs.plugins.PluginsViewModel.ViewState.Loaded.Plugin.PluginStatus.Inactive -import com.woocommerce.android.ui.prefs.plugins.PluginsViewModel.ViewState.Loaded.Plugin.PluginStatus.Unknown -import com.woocommerce.android.ui.prefs.plugins.PluginsViewModel.ViewState.Loaded.Plugin.PluginStatus.UpToDate -import com.woocommerce.android.ui.prefs.plugins.PluginsViewModel.ViewState.Loaded.Plugin.PluginStatus.UpdateAvailable -import com.woocommerce.android.ui.prefs.plugins.PluginsViewModel.ViewState.Loading +import com.woocommerce.android.ui.prefs.plugins.PluginsViewState.Error +import com.woocommerce.android.ui.prefs.plugins.PluginsViewState.Loaded +import com.woocommerce.android.ui.prefs.plugins.PluginsViewState.Loaded.Plugin +import com.woocommerce.android.ui.prefs.plugins.PluginsViewState.Loaded.Plugin.PluginStatus.Inactive +import com.woocommerce.android.ui.prefs.plugins.PluginsViewState.Loaded.Plugin.PluginStatus.Unknown +import com.woocommerce.android.ui.prefs.plugins.PluginsViewState.Loaded.Plugin.PluginStatus.UpToDate +import com.woocommerce.android.ui.prefs.plugins.PluginsViewState.Loaded.Plugin.PluginStatus.UpdateAvailable +import com.woocommerce.android.ui.prefs.plugins.PluginsViewState.Loading import com.woocommerce.android.util.isGreaterThanPluginVersion import com.woocommerce.android.viewmodel.MultiLiveEvent.Event.Exit import com.woocommerce.android.viewmodel.ResourceProvider @@ -36,7 +35,7 @@ class PluginsViewModel @Inject constructor( private val wooCommerceStore: WooCommerceStore, private val resourceProvider: ResourceProvider ) : ScopedViewModel(savedStateHandle) { - private val _viewState = MutableSharedFlow(1) + private val _viewState = MutableSharedFlow(1) val viewState = _viewState.asLiveData() init { @@ -105,26 +104,4 @@ class PluginsViewModel @Inject constructor( fun onBackPressed() { triggerEvent(Exit) } - - sealed interface ViewState { - data object Loading : ViewState - data object Error : ViewState - data class Loaded( - val plugins: List = emptyList() - ) : ViewState { - data class Plugin( - val name: String, - val authorName: String?, - val version: String, - val status: PluginStatus - ) { - sealed class PluginStatus { - data class UpToDate(val title: String, @ColorRes val color: Int) : PluginStatus() - data class UpdateAvailable(val title: String, @ColorRes val color: Int) : PluginStatus() - data class Inactive(val title: String, @ColorRes val color: Int) : PluginStatus() - data object Unknown : PluginStatus() - } - } - } - } } diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/prefs/plugins/PluginsViewState.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/prefs/plugins/PluginsViewState.kt new file mode 100644 index 00000000000..256068ad217 --- /dev/null +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/prefs/plugins/PluginsViewState.kt @@ -0,0 +1,25 @@ +package com.woocommerce.android.ui.prefs.plugins + +import androidx.annotation.ColorRes + +sealed interface PluginsViewState { + data object Loading : PluginsViewState + data object Error : PluginsViewState + data class Loaded( + val plugins: List = emptyList() + ) : PluginsViewState { + data class Plugin( + val name: String, + val authorName: String?, + val version: String, + val status: PluginStatus + ) { + sealed class PluginStatus { + data class UpToDate(val title: String, @ColorRes val color: Int) : PluginStatus() + data class UpdateAvailable(val title: String, @ColorRes val color: Int) : PluginStatus() + data class Inactive(val title: String, @ColorRes val color: Int) : PluginStatus() + data object Unknown : PluginStatus() + } + } + } +} From 03a312978f00f5308da27f9c0b5341d1bb0ade2e Mon Sep 17 00:00:00 2001 From: Andrey Date: Mon, 6 Jan 2025 16:36:52 +0100 Subject: [PATCH 09/12] Fixed formatting --- .../woocommerce/android/ui/prefs/plugins/PluginsScreen.kt | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/prefs/plugins/PluginsScreen.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/prefs/plugins/PluginsScreen.kt index dcfe8265a20..9a7e15ed6dc 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/prefs/plugins/PluginsScreen.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/prefs/plugins/PluginsScreen.kt @@ -310,7 +310,12 @@ private fun PreviewPlugins() { PluginsViewState.Loaded( plugins = listOf( Plugin("Plugin 1", "Automattic", "1.0", UpToDate("Up-to-date", R.color.color_info)), - Plugin("Plugin 2", "Something", "2.0", UpdateAvailable("Update available (4.9)", R.color.color_primary)), + Plugin( + "Plugin 2", + "Something", + "2.0", + UpdateAvailable("Update available (4.9)", R.color.color_primary) + ), Plugin("Plugin 3", "Gutenberg", "3.0", Inactive("Inactive", R.color.color_on_surface_disabled)), Plugin("Plugin 5", "Blabla", "5.0", Unknown) ) From 5bf5ea576a9952d549c86197a50baa16b96020d8 Mon Sep 17 00:00:00 2001 From: Andrey Date: Mon, 6 Jan 2025 16:43:47 +0100 Subject: [PATCH 10/12] Updated release notes --- RELEASE-NOTES.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/RELEASE-NOTES.txt b/RELEASE-NOTES.txt index f4674cd7fec..76cfa703f81 100644 --- a/RELEASE-NOTES.txt +++ b/RELEASE-NOTES.txt @@ -8,6 +8,7 @@ - [*] Fixed a crash on the order details [https://github.com/woocommerce/woocommerce-android/pull/13191] - [**] Fixed a crash when a shop manager was trying to install or activate plugin in the POS onboarding [https://github.com/woocommerce/woocommerce-android/pull/13203] - [**] Introduced fallback logic for the barcode scanner to use the front-facing camera when a back-facing camera is unavailable [https://github.com/woocommerce/woocommerce-android/pull/13230] +- [*] It possible to quickly open plugins page from the plugins list in the up to update an outdated plugin [https://github.com/woocommerce/woocommerce-android/pull/13246] 21.3 ----- From f146f7e6945d1f452a2c0c036bb8ea6b06d1914f Mon Sep 17 00:00:00 2001 From: Andrey Date: Mon, 6 Jan 2025 17:14:54 +0100 Subject: [PATCH 11/12] Some unit tests --- .../ui/prefs/plugins/PluginsViewModelTest.kt | 148 ++++++++++++++++++ 1 file changed, 148 insertions(+) create mode 100644 WooCommerce/src/test/kotlin/com/woocommerce/android/ui/prefs/plugins/PluginsViewModelTest.kt diff --git a/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/prefs/plugins/PluginsViewModelTest.kt b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/prefs/plugins/PluginsViewModelTest.kt new file mode 100644 index 00000000000..b9720043624 --- /dev/null +++ b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/prefs/plugins/PluginsViewModelTest.kt @@ -0,0 +1,148 @@ +package com.woocommerce.android.ui.prefs.plugins + +import androidx.lifecycle.SavedStateHandle +import com.woocommerce.android.R +import com.woocommerce.android.tools.SelectedSite +import com.woocommerce.android.ui.prefs.plugins.PluginsViewState.Loaded +import com.woocommerce.android.ui.prefs.plugins.PluginsViewState.Loaded.Plugin +import com.woocommerce.android.ui.prefs.plugins.PluginsViewState.Loaded.Plugin.PluginStatus.* +import com.woocommerce.android.util.captureValues +import com.woocommerce.android.viewmodel.BaseUnitTest +import com.woocommerce.android.viewmodel.MultiLiveEvent.Event.Exit +import com.woocommerce.android.viewmodel.ResourceProvider +import kotlinx.coroutines.ExperimentalCoroutinesApi +import org.assertj.core.api.Assertions.assertThat +import org.junit.Test +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever +import org.wordpress.android.fluxc.model.SiteModel +import org.wordpress.android.fluxc.network.BaseRequest.GenericErrorType +import org.wordpress.android.fluxc.network.rest.wpcom.wc.WooError +import org.wordpress.android.fluxc.network.rest.wpcom.wc.WooErrorType +import org.wordpress.android.fluxc.network.rest.wpcom.wc.WooResult +import org.wordpress.android.fluxc.network.rest.wpcom.wc.system.WCSystemPluginResponse.SystemPluginModel +import org.wordpress.android.fluxc.store.WooCommerceStore + +@OptIn(ExperimentalCoroutinesApi::class) +class PluginsViewModelTest : BaseUnitTest() { + private val savedStateHandle: SavedStateHandle = mock() + private val selectedSite: SelectedSite = mock { + on { get() }.thenReturn(mock()) + } + private val wooCommerceStore: WooCommerceStore = mock() + private val resourceProvider: ResourceProvider = mock { + on { getString(R.string.plugin_state_update_available, "1.1.0") } + .thenReturn("Update available to 1.1.0") + on { getString(R.string.plugin_state_up_to_date) }.thenReturn("Up-to-date") + on { getString(R.string.plugin_state_inactive) }.thenReturn("Inactive") + } + + private fun createViewModel() = PluginsViewModel( + savedStateHandle, + selectedSite, + wooCommerceStore, + resourceProvider + ) + + @Test + fun `given plugins are fetched successfully, when vm init, then viewState should be Loaded`() = testBlocking { + // GIVEN + val pluginsResponse = listOf( + SystemPluginModel( + name = "Plugin A", + authorName = "Author A", + plugin = "plugin.php", + version = "1.0.0", + versionLatest = "1.1.0", + url = "https://example.com/plugins.php", + isActive = true + ), + SystemPluginModel( + name = "Plugin B", + authorName = "Author B", + plugin = "plugin.php", + version = "2.0.0", + versionLatest = "2.0.0", + url = "https://example.com/plugins.php", + isActive = false + ) + ) + whenever(wooCommerceStore.fetchSystemPlugins(selectedSite.get())).thenReturn(WooResult(pluginsResponse)) + + // WHEN + val viewModel = createViewModel() + + // THEN + val values = viewModel.viewState.captureValues() + assertThat(values.last()).isEqualTo( + Loaded( + plugins = listOf( + Plugin( + "Plugin A", + "Author A", + "1.0.0", + UpdateAvailable("Update available to 1.1.0", R.color.color_primary) + ), + Plugin("Plugin B", "Author B", "2.0.0", Inactive("Inactive", R.color.color_on_surface_disabled)) + ) + ) + ) + } + + @Test + fun `given plugins fetch fails, when vm init, then viewState should be Error`() = testBlocking { + // GIVEN + whenever(wooCommerceStore.fetchSystemPlugins(selectedSite.get())).thenReturn( + WooResult( + WooError( + type = WooErrorType.GENERIC_ERROR, + original = GenericErrorType.SERVER_ERROR + ) + ) + ) + + // WHEN + val viewModel = createViewModel() + + // THEN + val values = viewModel.viewState.captureValues() + assertThat(values.last()).isEqualTo(PluginsViewState.Error) + } + + @Test + fun `given plugin status is UpdateAvailable, when onPluginClicked is called, then navigate event is triggered`() = + testBlocking { + // GIVEN + val plugin = Plugin( + name = "Plugin A", + authorName = "Author A", + version = "1.0.0", + status = UpdateAvailable("Update available to 1.1.0", R.color.color_primary) + ) + val siteModel = mock { + on { adminUrl }.thenReturn("https://example.com/wp-admin") + } + whenever(selectedSite.get()).thenReturn(siteModel) + val viewModel = createViewModel() + + // WHEN + viewModel.onPluginClicked(plugin) + + // THEN + assertThat(viewModel.event.value).isEqualTo( + PluginsEvent.NavigateToPluginsWeb("https://example.com/wp-admin/plugins.php") + ) + } + + @Test + fun `when onBackPressed is called, then Exit event is triggered`() { + // GIVEN + val viewModel = createViewModel() + + // WHEN + viewModel.onBackPressed() + + // THEN + assertThat(viewModel.event.value).isEqualTo(Exit) + } +} From 069f300342d5cc4d811f8788caedb865d26f70c1 Mon Sep 17 00:00:00 2001 From: Andrey Date: Tue, 7 Jan 2025 10:55:39 +0100 Subject: [PATCH 12/12] More tests --- .../ui/prefs/plugins/PluginsViewModel.kt | 6 +- .../ui/prefs/plugins/PluginsViewModelTest.kt | 173 +++++++++++++++++- 2 files changed, 171 insertions(+), 8 deletions(-) diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/prefs/plugins/PluginsViewModel.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/prefs/plugins/PluginsViewModel.kt index e9d25313da4..c3de9c8bec3 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/prefs/plugins/PluginsViewModel.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/prefs/plugins/PluginsViewModel.kt @@ -20,6 +20,7 @@ import com.woocommerce.android.viewmodel.MultiLiveEvent.Event.Exit import com.woocommerce.android.viewmodel.ResourceProvider import com.woocommerce.android.viewmodel.ScopedViewModel import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.launch @@ -33,7 +34,8 @@ class PluginsViewModel @Inject constructor( savedStateHandle: SavedStateHandle, private val site: SelectedSite, private val wooCommerceStore: WooCommerceStore, - private val resourceProvider: ResourceProvider + private val resourceProvider: ResourceProvider, + private val dispatcher: CoroutineDispatcher = Dispatchers.IO, ) : ScopedViewModel(savedStateHandle) { private val _viewState = MutableSharedFlow(1) val viewState = _viewState.asLiveData() @@ -43,7 +45,7 @@ class PluginsViewModel @Inject constructor( } private fun loadPlugins() { - viewModelScope.launch(Dispatchers.IO) { + viewModelScope.launch(dispatcher) { _viewState.emit(Loading) val response = wooCommerceStore.fetchSystemPlugins(site.get()) if (!response.isError && response.model != null) { diff --git a/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/prefs/plugins/PluginsViewModelTest.kt b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/prefs/plugins/PluginsViewModelTest.kt index b9720043624..a6c59f41388 100644 --- a/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/prefs/plugins/PluginsViewModelTest.kt +++ b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/prefs/plugins/PluginsViewModelTest.kt @@ -5,12 +5,16 @@ import com.woocommerce.android.R import com.woocommerce.android.tools.SelectedSite import com.woocommerce.android.ui.prefs.plugins.PluginsViewState.Loaded import com.woocommerce.android.ui.prefs.plugins.PluginsViewState.Loaded.Plugin -import com.woocommerce.android.ui.prefs.plugins.PluginsViewState.Loaded.Plugin.PluginStatus.* +import com.woocommerce.android.ui.prefs.plugins.PluginsViewState.Loaded.Plugin.PluginStatus.Inactive +import com.woocommerce.android.ui.prefs.plugins.PluginsViewState.Loaded.Plugin.PluginStatus.Unknown +import com.woocommerce.android.ui.prefs.plugins.PluginsViewState.Loaded.Plugin.PluginStatus.UpToDate +import com.woocommerce.android.ui.prefs.plugins.PluginsViewState.Loaded.Plugin.PluginStatus.UpdateAvailable import com.woocommerce.android.util.captureValues import com.woocommerce.android.viewmodel.BaseUnitTest import com.woocommerce.android.viewmodel.MultiLiveEvent.Event.Exit import com.woocommerce.android.viewmodel.ResourceProvider import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.UnconfinedTestDispatcher import org.assertj.core.api.Assertions.assertThat import org.junit.Test import org.mockito.kotlin.mock @@ -33,15 +37,18 @@ class PluginsViewModelTest : BaseUnitTest() { private val resourceProvider: ResourceProvider = mock { on { getString(R.string.plugin_state_update_available, "1.1.0") } .thenReturn("Update available to 1.1.0") - on { getString(R.string.plugin_state_up_to_date) }.thenReturn("Up-to-date") - on { getString(R.string.plugin_state_inactive) }.thenReturn("Inactive") + on { getString(R.string.plugin_state_update_available, "2.1.0") } + .thenReturn("Update available to 2.1.0") + on { getString(R.string.plugin_state_inactive) } + .thenReturn("Inactive") } private fun createViewModel() = PluginsViewModel( savedStateHandle, selectedSite, wooCommerceStore, - resourceProvider + resourceProvider, + UnconfinedTestDispatcher() ) @Test @@ -67,7 +74,8 @@ class PluginsViewModelTest : BaseUnitTest() { isActive = false ) ) - whenever(wooCommerceStore.fetchSystemPlugins(selectedSite.get())).thenReturn(WooResult(pluginsResponse)) + whenever(wooCommerceStore.fetchSystemPlugins(selectedSite.get())) + .thenReturn(WooResult(pluginsResponse)) // WHEN val viewModel = createViewModel() @@ -83,7 +91,12 @@ class PluginsViewModelTest : BaseUnitTest() { "1.0.0", UpdateAvailable("Update available to 1.1.0", R.color.color_primary) ), - Plugin("Plugin B", "Author B", "2.0.0", Inactive("Inactive", R.color.color_on_surface_disabled)) + Plugin( + "Plugin B", + "Author B", + "2.0.0", + Inactive("Inactive", R.color.color_on_surface_disabled) + ) ) ) ) @@ -145,4 +158,152 @@ class PluginsViewModelTest : BaseUnitTest() { // THEN assertThat(viewModel.event.value).isEqualTo(Exit) } + + @Test + fun `given plugin status is Inactive, when onPluginClicked is called, then no event is triggered`() = testBlocking { + // GIVEN + val plugin = Plugin( + name = "Plugin A", + authorName = "Author A", + version = "1.0.0", + status = Inactive("Inactive", R.color.color_on_surface_disabled) + ) + val viewModel = createViewModel() + + // WHEN + viewModel.onPluginClicked(plugin) + + // THEN + assertThat(viewModel.event.value).isNull() + } + + @Test + fun `given plugin status is Unknown, when onPluginClicked is called, then no event is triggered`() = testBlocking { + // GIVEN + val plugin = Plugin( + name = "Plugin A", + authorName = "Author A", + version = "1.2.3", + status = Unknown + ) + val viewModel = createViewModel() + + // WHEN + viewModel.onPluginClicked(plugin) + + // THEN + assertThat(viewModel.event.value).isNull() + } + + @Test + fun `given plugin status is UpToDate, when onPluginClicked is called, then no event is triggered`() = testBlocking { + // GIVEN + val plugin = Plugin( + name = "Plugin A", + authorName = "Author A", + version = "1.2.3", + status = UpToDate("Up-to-date", R.color.color_info) + ) + val viewModel = createViewModel() + + // WHEN + viewModel.onPluginClicked(plugin) + + // THEN + assertThat(viewModel.event.value).isNull() + } + + @Test + fun `given store returns success, when onRetryClicked is called, then viewState should be Loaded`() = testBlocking { + // GIVEN + val initialResponse = listOf( + SystemPluginModel( + name = "Plugin A", + authorName = "Author A", + plugin = "plugin.php", + version = "1.0.0", + versionLatest = "1.1.0", + url = "https://example.com/plugins.php", + isActive = true + ) + ) + whenever(wooCommerceStore.fetchSystemPlugins(selectedSite.get())) + .thenReturn(WooResult(initialResponse)) + + val viewModel = createViewModel() + + var values = viewModel.viewState.captureValues() + + val newResponse = listOf( + SystemPluginModel( + name = "Plugin B", + authorName = "Author B", + plugin = "plugin.php", + version = "2.0.0", + versionLatest = "2.1.0", + url = "https://example.com/plugins.php", + isActive = true + ) + ) + whenever(wooCommerceStore.fetchSystemPlugins(selectedSite.get())) + .thenReturn(WooResult(newResponse)) + + // WHEN + viewModel.onRetryClicked() + + // THEN + values = viewModel.viewState.captureValues() + assertThat(values.last()).isEqualTo( + Loaded( + plugins = listOf( + Plugin( + name = "Plugin B", + authorName = "Author B", + version = "2.0.0", + status = UpdateAvailable("Update available to 2.1.0", R.color.color_primary) + ) + ) + ) + ) + } + + @Test + fun `given store returns error, when onRetryClicked is called, then viewState should be Error`() = testBlocking { + // GIVEN + // Return success for the initial load + val initialResponse = listOf( + SystemPluginModel( + name = "Plugin A", + authorName = "Author A", + plugin = "plugin.php", + version = "1.0.0", + versionLatest = "1.1.0", + url = "https://example.com/plugins.php", + isActive = true + ) + ) + whenever(wooCommerceStore.fetchSystemPlugins(selectedSite.get())) + .thenReturn(WooResult(initialResponse)) + + val viewModel = createViewModel() + + var values = viewModel.viewState.captureValues() + + whenever(wooCommerceStore.fetchSystemPlugins(selectedSite.get())) + .thenReturn( + WooResult( + WooError( + type = WooErrorType.GENERIC_ERROR, + original = GenericErrorType.SERVER_ERROR + ) + ) + ) + + // WHEN + viewModel.onRetryClicked() + + // THEN + values = viewModel.viewState.captureValues() + assertThat(values.last()).isEqualTo(PluginsViewState.Error) + } }