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