diff --git a/RELEASE-NOTES.txt b/RELEASE-NOTES.txt
index a5e9d347a47f..02253ea329d2 100644
--- a/RELEASE-NOTES.txt
+++ b/RELEASE-NOTES.txt
@@ -6,6 +6,7 @@
* [**] Fixed media uploads and the Media Library failing on Jetpack-connected self-hosted sites that have Application Passwords disabled.
* [*] Stats now refresh when you return to the screen, so your latest data appears without a manual pull-to-refresh. [https://github.com/wordpress-mobile/WordPress-Android/pull/23112]
* [*] Links from emails that open outside the app now show a clear message when no browser is available, instead of closing the app.
+* [*] Switching between the old and new Stats screen is available from the Stats overflow menu while we continue iterating on the new Stats design.
26.9
-----
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/compose/components/FeedbackDialog.kt b/WordPress/src/main/java/org/wordpress/android/ui/compose/components/FeedbackDialog.kt
new file mode 100644
index 000000000000..19b9c9994450
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/compose/components/FeedbackDialog.kt
@@ -0,0 +1,39 @@
+package org.wordpress.android.ui.compose.components
+
+import androidx.compose.material3.AlertDialog
+import androidx.compose.material3.Button
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.res.stringResource
+import org.wordpress.android.R
+
+/**
+ * Asks the user whether they'd like to share feedback after turning a feature off.
+ *
+ * @param message Feature-specific prompt, e.g. "Are you willing to share feedback on the new stats?"
+ * @param onDismiss Called when the user declines or dismisses the dialog
+ * @param onSendFeedback Called when the user opts to send feedback
+ */
+@Composable
+fun FeedbackDialog(
+ message: String,
+ onDismiss: () -> Unit,
+ onSendFeedback: () -> Unit
+) {
+ AlertDialog(
+ onDismissRequest = onDismiss,
+ title = { Text(text = stringResource(R.string.experimental_features_feedback_dialog_title)) },
+ text = { Text(text = message) },
+ confirmButton = {
+ Button(onClick = onSendFeedback) {
+ Text(text = stringResource(R.string.send_feedback))
+ }
+ },
+ dismissButton = {
+ TextButton(onClick = onDismiss) {
+ Text(text = stringResource(R.string.experimental_features_feedback_dialog_decline))
+ }
+ }
+ )
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/ListItemActionHandler.kt b/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/ListItemActionHandler.kt
index f6ea59acb767..b2dd5b883503 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/ListItemActionHandler.kt
+++ b/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/ListItemActionHandler.kt
@@ -8,16 +8,14 @@ import org.wordpress.android.ui.blaze.blazecampaigns.campaignlisting.CampaignLis
import org.wordpress.android.ui.jetpackoverlay.JetpackFeatureRemovalPhaseHelper
import org.wordpress.android.ui.mysite.SiteNavigationAction
import org.wordpress.android.ui.mysite.items.listitem.ListItemAction
-import org.wordpress.android.ui.prefs.AppPrefsWrapper
-import org.wordpress.android.util.config.NewStatsFeatureConfig
+import org.wordpress.android.ui.newstats.NewStatsRouting
import javax.inject.Inject
class ListItemActionHandler @Inject constructor(
private val accountStore: AccountStore,
private val jetpackFeatureRemovalPhaseHelper: JetpackFeatureRemovalPhaseHelper,
private val blazeFeatureUtils: BlazeFeatureUtils,
- private val newStatsFeatureConfig: NewStatsFeatureConfig,
- private val appPrefsWrapper: AppPrefsWrapper
+ private val newStatsRouting: NewStatsRouting
) {
fun handleAction(
action: ListItemAction,
@@ -61,8 +59,7 @@ class ListItemActionHandler @Inject constructor(
// If it's a WordPress.com or Jetpack site, show the Stats screen.
site.isWPCom || site.isJetpackInstalled && site.isJetpackConnected -> {
- // Show New Stats when the remote flag is on (rollout) or the user has opted in locally.
- if (newStatsFeatureConfig.isEnabled() || appPrefsWrapper.getNewStatsUserOptedIn()) {
+ if (newStatsRouting.isNewStatsEnabled()) {
SiteNavigationAction.OpenNewStats
} else {
SiteNavigationAction.OpenStats(site)
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/newstats/NewStatsActivity.kt b/WordPress/src/main/java/org/wordpress/android/ui/newstats/NewStatsActivity.kt
index e5ca53aed61c..79b94051c384 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/newstats/NewStatsActivity.kt
+++ b/WordPress/src/main/java/org/wordpress/android/ui/newstats/NewStatsActivity.kt
@@ -49,6 +49,7 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@@ -67,6 +68,7 @@ import org.wordpress.android.WordPress
import org.wordpress.android.fluxc.store.SiteStore
import org.wordpress.android.ui.ActivityLauncher
import org.wordpress.android.ui.ActivityNavigator
+import org.wordpress.android.ui.compose.components.FeedbackDialog
import org.wordpress.android.ui.compose.theme.AppThemeM3
import org.wordpress.android.ui.main.BaseAppCompatActivity
import org.wordpress.android.ui.newstats.components.AddCardBottomSheet
@@ -125,7 +127,6 @@ import org.wordpress.android.ui.stats.refresh.utils.StatsLaunchedFrom
import org.wordpress.android.analytics.AnalyticsTracker.Stat
import org.wordpress.android.util.AppLog
import org.wordpress.android.util.analytics.AnalyticsTrackerWrapper
-import org.wordpress.android.util.config.NewStatsFeatureConfig
import javax.inject.Inject
@AndroidEntryPoint
@@ -143,7 +144,7 @@ class NewStatsActivity : BaseAppCompatActivity() {
lateinit var analyticsTracker: AnalyticsTrackerWrapper
@Inject
- lateinit var newStatsFeatureConfig: NewStatsFeatureConfig
+ lateinit var newStatsRouting: NewStatsRouting
@Inject
lateinit var activityNavigator: ActivityNavigator
@@ -155,14 +156,31 @@ class NewStatsActivity : BaseAppCompatActivity() {
selectSiteFromIntentIfNeeded()
val shouldShowIntro =
!appPrefsWrapper.getNewStatsIntroShown()
- val canSwitchToOldStats = !newStatsFeatureConfig.isEnabled()
setContent {
AppThemeM3 {
+ var showFeedbackDialog by rememberSaveable { mutableStateOf(false) }
+
+ if (showFeedbackDialog) {
+ FeedbackDialog(
+ message = stringResource(R.string.stats_new_stats_feedback_dialog_message),
+ onDismiss = {
+ showFeedbackDialog = false
+ leaveNewStats(sendFeedback = false)
+ },
+ onSendFeedback = {
+ showFeedbackDialog = false
+ leaveNewStats(sendFeedback = true)
+ }
+ )
+ }
+
NewStatsScreen(
onBackPressed =
onBackPressedDispatcher::onBackPressed,
- showSwitchToOldStats = canSwitchToOldStats,
- onSwitchToOldStats = ::switchToOldStats,
+ onSwitchToOldStats = {
+ switchToOldStats()
+ showFeedbackDialog = true
+ },
showIntroBottomSheet = shouldShowIntro,
onIntroDismissed = {
appPrefsWrapper
@@ -190,21 +208,36 @@ class NewStatsActivity : BaseAppCompatActivity() {
activityNavigator.openPostDetailStats(this, item.id, item.postType, item.title, item.url)
}
+ /**
+ * Records the opt-out. Navigation is deferred until the feedback dialog is answered, since
+ * finishing this activity straight away would tear the dialog down with it.
+ */
private fun switchToOldStats() {
analyticsTracker.track(Stat.STATS_NEW_STATS_DISABLED)
- appPrefsWrapper.setNewStatsUserOptedIn(false)
- appPrefsWrapper.setNewStatsIntroShown(false)
+ newStatsRouting.optOut()
+ }
+
+ private fun leaveNewStats(sendFeedback: Boolean) {
+ // The opt-out is already persisted, so always finish - bailing out here would strand the
+ // user on a screen the rest of the app no longer routes them to. Old Stats needs a site,
+ // but without one we can still leave and fall back to whatever is underneath.
selectedSiteRepository.getSelectedSite()?.let { site ->
StatsActivity.start(
this,
site,
launchedFrom = StatsLaunchedFrom.STATS_TOGGLE
)
- finish()
}
+ // Started after old Stats so the form sits on top of it and backs out to it.
+ if (sendFeedback) {
+ ActivityLauncher.viewFeedbackForm(this, FEEDBACK_PREFIX_STATS)
+ }
+ finish()
}
companion object {
+ private const val FEEDBACK_PREFIX_STATS = "Stats"
+
fun start(context: Context) {
context.startActivity(Intent(context, NewStatsActivity::class.java))
}
@@ -256,7 +289,6 @@ private fun StatsOverflowMenu(
@Composable
private fun NewStatsScreen(
onBackPressed: () -> Unit,
- showSwitchToOldStats: Boolean = false,
onSwitchToOldStats: () -> Unit = {},
showIntroBottomSheet: Boolean = false,
onIntroDismissed: () -> Unit = {},
@@ -361,11 +393,9 @@ private fun NewStatsScreen(
)
}
}
- if (showSwitchToOldStats) {
- StatsOverflowMenu(
- onSwitchToOldStats = onSwitchToOldStats
- )
- }
+ StatsOverflowMenu(
+ onSwitchToOldStats = onSwitchToOldStats
+ )
}
)
}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/newstats/NewStatsRouting.kt b/WordPress/src/main/java/org/wordpress/android/ui/newstats/NewStatsRouting.kt
new file mode 100644
index 000000000000..3a8af1e2d261
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/newstats/NewStatsRouting.kt
@@ -0,0 +1,43 @@
+package org.wordpress.android.ui.newstats
+
+import org.wordpress.android.ui.prefs.AppPrefsWrapper
+import org.wordpress.android.util.config.NewStatsFeatureConfig
+import javax.inject.Inject
+import javax.inject.Singleton
+
+/**
+ * Decides whether New Stats is shown, shared by the My Site menu and the Stats widgets so the two
+ * can't drift apart.
+ *
+ * Note this does not yet govern every route into Stats: shortcuts, deep links, notifications and
+ * the activity log go straight to the old [org.wordpress.android.ui.stats.refresh.StatsActivity]
+ * via ActivityLauncher.viewBlogStats, regardless of this decision.
+ */
+@Singleton
+class NewStatsRouting @Inject constructor(
+ private val newStatsFeatureConfig: NewStatsFeatureConfig,
+ private val appPrefsWrapper: AppPrefsWrapper
+) {
+ /**
+ * New Stats is the default when the remote flag is on (rollout) or the user opted in locally,
+ * but an explicit opt-out always wins so users on the rollout can switch back.
+ */
+ fun isNewStatsEnabled(): Boolean =
+ !appPrefsWrapper.getNewStatsUserOptedOut() &&
+ (newStatsFeatureConfig.isEnabled() || appPrefsWrapper.getNewStatsUserOptedIn())
+
+ fun optIn() {
+ appPrefsWrapper.setNewStatsUserOptedOut(false)
+ appPrefsWrapper.setNewStatsUserOptedIn(true)
+ // Reintroduce New Stats on the way in. Resetting this on opt-out instead would re-arm the
+ // intro sheet while NewStatsActivity is still on screen, so it reappears on recreation.
+ appPrefsWrapper.setNewStatsIntroShown(false)
+ }
+
+ fun optOut() {
+ appPrefsWrapper.setNewStatsUserOptedOut(true)
+ appPrefsWrapper.setNewStatsUserOptedIn(false)
+ }
+
+ fun hasOptedOut(): Boolean = appPrefsWrapper.getNewStatsUserOptedOut()
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/prefs/AppPrefs.java b/WordPress/src/main/java/org/wordpress/android/ui/prefs/AppPrefs.java
index a52538f5ce52..b0dab86f4d62 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/prefs/AppPrefs.java
+++ b/WordPress/src/main/java/org/wordpress/android/ui/prefs/AppPrefs.java
@@ -246,6 +246,11 @@ public enum UndeletablePrefKey implements PrefKey {
BOOKMARKS_SAVED_LOCALLY_DIALOG_SHOWN,
+ // Set when the user explicitly turns New Stats off. Undeletable so the choice survives
+ // sign-out - it is the only thing that can override the android_new_stats rollout flag,
+ // so erasing it would silently push the user back into New Stats.
+ NEW_STATS_USER_OPTED_OUT,
+
// When we need to show the snackbar indicating how notifications can be navigated through
SWIPE_TO_NAVIGATE_NOTIFICATIONS,
@@ -2156,6 +2161,14 @@ public static void setNewStatsUserOptedIn(boolean optedIn) {
setBoolean(DeletablePrefKey.NEW_STATS_USER_OPTED_IN, optedIn);
}
+ public static boolean getNewStatsUserOptedOut() {
+ return getBoolean(UndeletablePrefKey.NEW_STATS_USER_OPTED_OUT, false);
+ }
+
+ public static void setNewStatsUserOptedOut(boolean optedOut) {
+ setBoolean(UndeletablePrefKey.NEW_STATS_USER_OPTED_OUT, optedOut);
+ }
+
/**
* One-time migration for users who enabled New Stats via the old experimental-features toggle,
* before it was replaced by the remote flag + local opt-in preference. Without this, those users
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/prefs/AppPrefsWrapper.kt b/WordPress/src/main/java/org/wordpress/android/ui/prefs/AppPrefsWrapper.kt
index 1292b8dceb6c..8fc18b7bf05e 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/prefs/AppPrefsWrapper.kt
+++ b/WordPress/src/main/java/org/wordpress/android/ui/prefs/AppPrefsWrapper.kt
@@ -582,6 +582,11 @@ class AppPrefsWrapper @Inject constructor(val buildConfigWrapper: BuildConfigWra
fun setNewStatsUserOptedIn(optedIn: Boolean) =
AppPrefs.setNewStatsUserOptedIn(optedIn)
+ fun getNewStatsUserOptedOut(): Boolean = AppPrefs.getNewStatsUserOptedOut()
+
+ fun setNewStatsUserOptedOut(optedOut: Boolean) =
+ AppPrefs.setNewStatsUserOptedOut(optedOut)
+
fun getStatsNewStatsSuggestionShown(): Boolean =
AppPrefs.getStatsNewStatsSuggestionShown()
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/prefs/experimentalfeatures/ExperimentalFeaturesActivity.kt b/WordPress/src/main/java/org/wordpress/android/ui/prefs/experimentalfeatures/ExperimentalFeaturesActivity.kt
index 8e1f17cf30d7..4c8467d70e72 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/prefs/experimentalfeatures/ExperimentalFeaturesActivity.kt
+++ b/WordPress/src/main/java/org/wordpress/android/ui/prefs/experimentalfeatures/ExperimentalFeaturesActivity.kt
@@ -4,10 +4,13 @@ import android.os.Bundle
import androidx.activity.viewModels
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.ui.res.stringResource
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import dagger.hilt.android.AndroidEntryPoint
+import org.wordpress.android.R
import org.wordpress.android.ui.ActivityLauncher
+import org.wordpress.android.ui.compose.components.FeedbackDialog
import org.wordpress.android.ui.compose.theme.AppThemeM3
import org.wordpress.android.ui.main.BaseAppCompatActivity
import org.wordpress.android.util.extensions.setContent
@@ -24,7 +27,7 @@ class ExperimentalFeaturesActivity : BaseAppCompatActivity() {
val features by viewModel.switchStates.collectAsStateWithLifecycle()
val showNetworkDebuggingError by
viewModel.showNetworkDebuggingError.collectAsStateWithLifecycle()
- val showDialog = remember { mutableStateOf(false) }
+ val showDialog = rememberSaveable { mutableStateOf(false) }
if (showNetworkDebuggingError) {
NetworkDebuggingErrorDialog(
@@ -34,6 +37,7 @@ class ExperimentalFeaturesActivity : BaseAppCompatActivity() {
if (showDialog.value) {
FeedbackDialog(
+ message = stringResource(R.string.experimental_features_feedback_dialog_message),
onDismiss = { showDialog.value = false },
onSendFeedback = {
showDialog.value = false
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/prefs/experimentalfeatures/ExperimentalFeaturesScreen.kt b/WordPress/src/main/java/org/wordpress/android/ui/prefs/experimentalfeatures/ExperimentalFeaturesScreen.kt
index 73b242897a7b..b19122acf912 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/prefs/experimentalfeatures/ExperimentalFeaturesScreen.kt
+++ b/WordPress/src/main/java/org/wordpress/android/ui/prefs/experimentalfeatures/ExperimentalFeaturesScreen.kt
@@ -22,7 +22,6 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
-import androidx.compose.material3.TextButton
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
@@ -127,26 +126,6 @@ fun FeatureToggle(
)
}
-@Composable
-fun FeedbackDialog(onDismiss: () -> Unit, onSendFeedback: () -> Unit) {
- AlertDialog(
- onDismissRequest = onDismiss,
- title = { Text(text = stringResource(R.string.experimental_features_feedback_dialog_title)) },
- text = { Text(text = stringResource(R.string.experimental_features_feedback_dialog_message)) },
- confirmButton = {
- Button(onClick = onSendFeedback) {
- Text(text = stringResource(R.string.send_feedback))
- }
- },
- dismissButton = {
- TextButton(onClick = onDismiss) {
- Text(text = stringResource(R.string.experimental_features_feedback_dialog_decline))
- }
- }
- )
-}
-
-
@Composable
fun NetworkDebuggingErrorDialog(
onDismiss: () -> Unit
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/StatsFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/StatsFragment.kt
index a5b8a3f81d0b..363ac2021de7 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/StatsFragment.kt
+++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/StatsFragment.kt
@@ -35,6 +35,7 @@ import org.wordpress.android.ui.jetpackoverlay.JetpackOverlayConnectedFeature
import org.wordpress.android.ui.main.WPMainNavigationView.PageType.MY_SITE
import org.wordpress.android.ui.mysite.jetpackbadge.JetpackPoweredBottomSheetFragment
import org.wordpress.android.ui.newstats.NewStatsActivity
+import org.wordpress.android.ui.newstats.NewStatsRouting
import org.wordpress.android.ui.pages.SnackbarMessageHolder
import org.wordpress.android.ui.prefs.AppPrefsWrapper
import org.wordpress.android.ui.stats.refresh.StatsViewModel.StatsModuleUiModel
@@ -87,6 +88,9 @@ class StatsFragment : Fragment(R.layout.stats_fragment), ScrollableViewInitializ
@Inject
lateinit var appPrefsWrapper: AppPrefsWrapper
+ @Inject
+ lateinit var newStatsRouting: NewStatsRouting
+
@Inject
lateinit var jetpackFeatureRemovalOverlayUtil: JetpackFeatureRemovalOverlayUtil
@@ -155,7 +159,7 @@ class StatsFragment : Fragment(R.layout.stats_fragment), ScrollableViewInitializ
private fun switchToNewStats() {
if (!isAdded) return
analyticsTracker.track(Stat.STATS_NEW_STATS_ENABLED)
- appPrefsWrapper.setNewStatsUserOptedIn(true)
+ newStatsRouting.optIn()
NewStatsActivity.start(requireContext())
requireActivity().finish()
}
@@ -163,6 +167,8 @@ class StatsFragment : Fragment(R.layout.stats_fragment), ScrollableViewInitializ
@Suppress("ReturnCount")
private fun maybeShowNewStatsSuggestion() {
if (appPrefsWrapper.getStatsNewStatsSuggestionShown()) return
+ // Don't nag users who deliberately switched back to old Stats.
+ if (newStatsRouting.hasOptedOut()) return
// Avoid stacking on top of the Jetpack-powered bottom sheet or the feature-removal overlay,
// both of which may show on a fresh Stats activity launch.
if (jetpackBrandingUtils.shouldShowJetpackPoweredBottomSheet()) return
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/widget/utils/WidgetUtils.kt b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/widget/utils/WidgetUtils.kt
index 8233a2fb6e7b..1905e72d7ed3 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/widget/utils/WidgetUtils.kt
+++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/widget/utils/WidgetUtils.kt
@@ -21,7 +21,7 @@ import org.wordpress.android.fluxc.model.stats.time.VisitsAndViewsModel.PeriodDa
import org.wordpress.android.fluxc.network.utils.StatsGranularity
import org.wordpress.android.modules.UI_THREAD
import org.wordpress.android.ui.newstats.NewStatsActivity
-import org.wordpress.android.ui.prefs.AppPrefsWrapper
+import org.wordpress.android.ui.newstats.NewStatsRouting
import org.wordpress.android.ui.stats.StatsTimeframe
import org.wordpress.android.ui.stats.refresh.StatsActivity
import org.wordpress.android.ui.stats.refresh.lists.widget.IS_WIDE_VIEW_KEY
@@ -32,7 +32,6 @@ import org.wordpress.android.ui.stats.refresh.lists.widget.configuration.StatsCo
import org.wordpress.android.ui.stats.refresh.lists.widget.configuration.StatsColorSelectionViewModel.Color.LIGHT
import org.wordpress.android.ui.stats.refresh.lists.widget.configuration.StatsWidgetConfigureFragment.WidgetType
import org.wordpress.android.ui.stats.refresh.utils.StatsLaunchedFrom
-import org.wordpress.android.util.config.NewStatsFeatureConfig
import org.wordpress.android.util.image.ImageManager
import org.wordpress.android.util.image.ImageType.ICON
import org.wordpress.android.viewmodel.ResourceProvider
@@ -51,8 +50,7 @@ class WidgetUtils
@Inject constructor(
@Named(UI_THREAD) private val mainDispatcher: CoroutineDispatcher,
val imageManager: ImageManager,
- private val newStatsFeatureConfig: NewStatsFeatureConfig,
- private val appPrefsWrapper: AppPrefsWrapper
+ private val newStatsRouting: NewStatsRouting
) {
private val coroutineScope = CoroutineScope(mainDispatcher)
fun isWidgetWiderThanLimit(
@@ -181,7 +179,7 @@ class WidgetUtils
statsTimeframe: StatsTimeframe,
granularity: StatsGranularity? = null
): PendingIntent {
- val intent = if (isNewStatsEnabled()) {
+ val intent = if (newStatsRouting.isNewStatsEnabled()) {
Intent(context, NewStatsActivity::class.java).apply {
putExtra(WordPress.LOCAL_SITE_ID, localSiteId)
// Mirror the row-tap (template) path: CLEAR_TOP forces an already-running
@@ -210,7 +208,7 @@ class WidgetUtils
private fun getPendingTemplate(context: Context): PendingIntent {
// The per-row fill-in intents already carry the LOCAL_SITE_ID extra, which is merged into
// whichever component this template targets, so New Stats receives the tapped row's site.
- val targetActivity = if (isNewStatsEnabled()) {
+ val targetActivity = if (newStatsRouting.isNewStatsEnabled()) {
NewStatsActivity::class.java
} else {
StatsActivity::class.java
@@ -235,11 +233,6 @@ class WidgetUtils
return Random(Date().time).nextInt()
}
- // Mirrors the routing used by the My Site menu (ListItemActionHandler): open New Stats when the
- // remote flag is on (rollout) or the user has opted in locally.
- private fun isNewStatsEnabled(): Boolean =
- newStatsFeatureConfig.isEnabled() || appPrefsWrapper.getNewStatsUserOptedIn()
-
fun getLastWeekPeriodData(visitsAndViewsModel: VisitsAndViewsModel): PeriodData? {
val currentDateForSite = WeeklyRoundupUtils.parseStandardDate(visitsAndViewsModel.period) ?: return null
val lastWeekStartDate = currentDateForSite.minusWeeks(1).with(TemporalAdjusters.previousOrSame(MONDAY))
diff --git a/WordPress/src/main/res/values/strings.xml b/WordPress/src/main/res/values/strings.xml
index 72a3d2d66515..7a9dd76b2be3 100644
--- a/WordPress/src/main/res/values/strings.xml
+++ b/WordPress/src/main/res/values/strings.xml
@@ -1018,6 +1018,7 @@
We\'ve built a brand new Stats experience. Want to give it a try? You can always go back to the old one from the Stats menu.
Try it out
Maybe later
+ Are you willing to share feedback on the new Stats experience?
New Stats
A better way to understand your site\'s performance.
diff --git a/WordPress/src/test/java/org/wordpress/android/ui/mysite/cards/ListItemActionHandlerTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/mysite/cards/ListItemActionHandlerTest.kt
index f65bb4adeb8e..c0e924236278 100644
--- a/WordPress/src/test/java/org/wordpress/android/ui/mysite/cards/ListItemActionHandlerTest.kt
+++ b/WordPress/src/test/java/org/wordpress/android/ui/mysite/cards/ListItemActionHandlerTest.kt
@@ -16,8 +16,7 @@ import org.wordpress.android.ui.blaze.blazecampaigns.campaignlisting.CampaignLis
import org.wordpress.android.ui.jetpackoverlay.JetpackFeatureRemovalPhaseHelper
import org.wordpress.android.ui.mysite.SiteNavigationAction
import org.wordpress.android.ui.mysite.items.listitem.ListItemAction
-import org.wordpress.android.ui.prefs.AppPrefsWrapper
-import org.wordpress.android.util.config.NewStatsFeatureConfig
+import org.wordpress.android.ui.newstats.NewStatsRouting
import kotlin.test.assertEquals
@ExperimentalCoroutinesApi
@@ -33,10 +32,7 @@ class ListItemActionHandlerTest: BaseUnitTest() {
lateinit var blazeFeatureUtils: BlazeFeatureUtils
@Mock
- lateinit var newStatsFeatureConfig: NewStatsFeatureConfig
-
- @Mock
- lateinit var appPrefsWrapper: AppPrefsWrapper
+ lateinit var newStatsRouting: NewStatsRouting
private val site = SiteModel()
@@ -48,8 +44,7 @@ class ListItemActionHandlerTest: BaseUnitTest() {
accountStore,
jetpackFeatureRemovalPhaseHelper,
blazeFeatureUtils,
- newStatsFeatureConfig,
- appPrefsWrapper
+ newStatsRouting
)
}
@@ -167,32 +162,9 @@ class ListItemActionHandlerTest: BaseUnitTest() {
}
@Test
- fun `stats item click emits OpenNewStats when NEW_STATS feature is enabled and site is WPCom`() {
- site.setIsWPCom(true)
- whenever(newStatsFeatureConfig.isEnabled()).thenReturn(true)
-
- val navigationAction = invokeItemClickAction(action = ListItemAction.STATS)
-
- assertEquals(SiteNavigationAction.OpenNewStats, navigationAction)
- }
-
- @Test
- fun `stats item click emits OpenNewStats when NEW_STATS feature is enabled and site is Jetpack`() {
- site.setIsJetpackConnected(true)
- site.setIsWPCom(true)
- whenever(accountStore.hasAccessToken()).thenReturn(true)
- whenever(newStatsFeatureConfig.isEnabled()).thenReturn(true)
-
- val navigationAction = invokeItemClickAction(action = ListItemAction.STATS)
-
- assertEquals(SiteNavigationAction.OpenNewStats, navigationAction)
- }
-
- @Test
- fun `stats item click emits OpenNewStats when user opted in even if NEW_STATS feature is disabled`() {
+ fun `stats item click emits OpenNewStats when New Stats is enabled`() {
site.setIsWPCom(true)
- whenever(newStatsFeatureConfig.isEnabled()).thenReturn(false)
- whenever(appPrefsWrapper.getNewStatsUserOptedIn()).thenReturn(true)
+ whenever(newStatsRouting.isNewStatsEnabled()).thenReturn(true)
val navigationAction = invokeItemClickAction(action = ListItemAction.STATS)
@@ -200,10 +172,9 @@ class ListItemActionHandlerTest: BaseUnitTest() {
}
@Test
- fun `stats item click emits OpenStats when NEW_STATS feature is disabled and user has not opted in`() {
+ fun `stats item click emits OpenStats when New Stats is disabled`() {
site.setIsWPCom(true)
- whenever(newStatsFeatureConfig.isEnabled()).thenReturn(false)
- whenever(appPrefsWrapper.getNewStatsUserOptedIn()).thenReturn(false)
+ whenever(newStatsRouting.isNewStatsEnabled()).thenReturn(false)
val navigationAction = invokeItemClickAction(action = ListItemAction.STATS)
diff --git a/WordPress/src/test/java/org/wordpress/android/ui/newstats/NewStatsRoutingTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/newstats/NewStatsRoutingTest.kt
new file mode 100644
index 000000000000..60dca47867ff
--- /dev/null
+++ b/WordPress/src/test/java/org/wordpress/android/ui/newstats/NewStatsRoutingTest.kt
@@ -0,0 +1,78 @@
+package org.wordpress.android.ui.newstats
+
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.junit.MockitoJUnitRunner
+import org.mockito.kotlin.any
+import org.mockito.kotlin.never
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
+import org.wordpress.android.ui.prefs.AppPrefsWrapper
+import org.wordpress.android.util.config.NewStatsFeatureConfig
+import kotlin.test.assertFalse
+import kotlin.test.assertTrue
+
+@RunWith(MockitoJUnitRunner::class)
+class NewStatsRoutingTest {
+ @Mock
+ lateinit var newStatsFeatureConfig: NewStatsFeatureConfig
+
+ @Mock
+ lateinit var appPrefsWrapper: AppPrefsWrapper
+
+ private lateinit var newStatsRouting: NewStatsRouting
+
+ @Before
+ fun setUp() {
+ newStatsRouting = NewStatsRouting(newStatsFeatureConfig, appPrefsWrapper)
+ }
+
+ @Test
+ fun `new stats is enabled when the remote flag is on`() {
+ whenever(newStatsFeatureConfig.isEnabled()).thenReturn(true)
+
+ assertTrue(newStatsRouting.isNewStatsEnabled())
+ }
+
+ @Test
+ fun `new stats is enabled when the user opted in and the remote flag is off`() {
+ whenever(newStatsFeatureConfig.isEnabled()).thenReturn(false)
+ whenever(appPrefsWrapper.getNewStatsUserOptedIn()).thenReturn(true)
+
+ assertTrue(newStatsRouting.isNewStatsEnabled())
+ }
+
+ @Test
+ fun `new stats is disabled when the remote flag is off and the user has not opted in`() {
+ whenever(newStatsFeatureConfig.isEnabled()).thenReturn(false)
+ whenever(appPrefsWrapper.getNewStatsUserOptedIn()).thenReturn(false)
+
+ assertFalse(newStatsRouting.isNewStatsEnabled())
+ }
+
+ @Test
+ fun `opting out beats the remote flag`() {
+ whenever(appPrefsWrapper.getNewStatsUserOptedOut()).thenReturn(true)
+
+ assertFalse(newStatsRouting.isNewStatsEnabled())
+ }
+
+ /** Without this, a user who opted out could never get back into New Stats. */
+ @Test
+ fun `opting in clears a previous opt-out and re-arms the intro`() {
+ newStatsRouting.optIn()
+
+ verify(appPrefsWrapper).setNewStatsUserOptedOut(false)
+ verify(appPrefsWrapper).setNewStatsIntroShown(false)
+ }
+
+ /** Re-arming it here would make the intro sheet reappear if New Stats is recreated. */
+ @Test
+ fun `opting out leaves the intro flag alone`() {
+ newStatsRouting.optOut()
+
+ verify(appPrefsWrapper, never()).setNewStatsIntroShown(any())
+ }
+}