Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions RELEASE-NOTES.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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.
* [*] You can now switch back to the previous Stats screen at any time from the Stats overflow menu, and your choice is remembered.

26.9
-----
Expand Down
Original file line number Diff line number Diff line change
@@ -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))
}
}
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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))
}
Expand Down Expand Up @@ -256,7 +289,6 @@ private fun StatsOverflowMenu(
@Composable
private fun NewStatsScreen(
onBackPressed: () -> Unit,
showSwitchToOldStats: Boolean = false,
onSwitchToOldStats: () -> Unit = {},
showIntroBottomSheet: Boolean = false,
onIntroDismissed: () -> Unit = {},
Expand Down Expand Up @@ -361,11 +393,9 @@ private fun NewStatsScreen(
)
}
}
if (showSwitchToOldStats) {
StatsOverflowMenu(
onSwitchToOldStats = onSwitchToOldStats
)
}
StatsOverflowMenu(
onSwitchToOldStats = onSwitchToOldStats
)
}
)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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()
}
Original file line number Diff line number Diff line change
Expand Up @@ -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,

Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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(
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -155,14 +159,16 @@ 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()
}

@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
Expand Down
Loading
Loading