From be51865b33fede47832118698e8c7a0b156e926a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Sikora?= Date: Mon, 3 Feb 2025 00:15:12 -0800 Subject: [PATCH] Improve Winback edge-to-edge experience (#3524) --- .../profile/winback/WinbackFragment.kt | 308 +++++++++--------- .../shiftyjelly/pocketcasts/compose/Theme.kt | 12 +- 2 files changed, 153 insertions(+), 167 deletions(-) diff --git a/modules/features/profile/src/main/java/au/com/shiftyjelly/pocketcasts/profile/winback/WinbackFragment.kt b/modules/features/profile/src/main/java/au/com/shiftyjelly/pocketcasts/profile/winback/WinbackFragment.kt index d2b2fc3e6a1..895d50d4982 100644 --- a/modules/features/profile/src/main/java/au/com/shiftyjelly/pocketcasts/profile/winback/WinbackFragment.kt +++ b/modules/features/profile/src/main/java/au/com/shiftyjelly/pocketcasts/profile/winback/WinbackFragment.kt @@ -9,29 +9,31 @@ import android.view.LayoutInflater import android.view.ViewGroup import androidx.appcompat.app.AppCompatActivity import androidx.compose.animation.AnimatedContentTransitionScope -import androidx.compose.animation.animateColorAsState import androidx.compose.animation.core.tween import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsetsSides +import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.only import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.safeDrawing +import androidx.compose.foundation.layout.windowInsetsPadding +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.AppBarDefaults import androidx.compose.material.MaterialTheme import androidx.compose.material.Snackbar import androidx.compose.material.SnackbarHost import androidx.compose.material.SnackbarHostState -import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue -import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.unit.IntOffset @@ -41,7 +43,6 @@ import androidx.core.os.bundleOf import androidx.fragment.app.viewModels import androidx.fragment.compose.content import androidx.navigation.NavBackStackEntry -import androidx.navigation.NavHostController import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController @@ -70,181 +71,176 @@ class WinbackFragment : BaseDialogFragment() { private var currentRoute: String? = null + override val includeNavigationBarPadding = false + override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?, ) = content { + LaunchedEffect(Unit) { + setBackgroundTint(Color.Transparent.toArgb()) + } + val scope = rememberCoroutineScope() val state by viewModel.uiState.collectAsState() - AppThemeWithBackground( - themeType = theme.activeTheme, + Box( + modifier = Modifier + .fillMaxHeight(0.93f) + .clip(RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp)), ) { - val navController = rememberNavController() - val snackbarHostState = remember { SnackbarHostState() } + AppThemeWithBackground( + themeType = theme.activeTheme, + ) { + val navController = rememberNavController() + val snackbarHostState = remember { SnackbarHostState() } - Box { - NavHost( - navController = navController, - startDestination = if (params.hasGoogleSubscription) { - WinbackNavRoutes.WinbackOffer - } else { - WinbackNavRoutes.CancelConfirmation - }, - enterTransition = { slideInToStart() }, - exitTransition = { slideOutToStart() }, - popEnterTransition = { slideInToEnd() }, - popExitTransition = { slideOutToEnd() }, - modifier = Modifier.fillMaxSize(), + Box( + modifier = Modifier + .windowInsetsPadding(WindowInsets.safeDrawing.only(WindowInsetsSides.Horizontal + WindowInsetsSides.Bottom)) + .padding(top = 8.dp), ) { - composable(WinbackNavRoutes.WinbackOffer) { - WinbackOfferPage( - onClaimOffer = { - viewModel.trackClaimOfferTapped() - navController.navigate(WinbackNavRoutes.OfferClaimed) { - popUpTo(WinbackNavRoutes.WinbackOffer) { - inclusive = true + NavHost( + navController = navController, + startDestination = if (params.hasGoogleSubscription) { + WinbackNavRoutes.WinbackOffer + } else { + WinbackNavRoutes.CancelConfirmation + }, + enterTransition = { slideInToStart() }, + exitTransition = { slideOutToStart() }, + popEnterTransition = { slideInToEnd() }, + popExitTransition = { slideOutToEnd() }, + modifier = Modifier.fillMaxSize(), + ) { + composable(WinbackNavRoutes.WinbackOffer) { + WinbackOfferPage( + onClaimOffer = { + viewModel.trackClaimOfferTapped() + navController.navigate(WinbackNavRoutes.OfferClaimed) { + popUpTo(WinbackNavRoutes.WinbackOffer) { + inclusive = true + } } - } - }, - onSeeAvailablePlans = { - viewModel.trackAvailablePlansTapped() - navController.navigate(WinbackNavRoutes.AvailablePlans) - }, - onSeeHelpAndFeedback = { - viewModel.trackHelpAndFeedbackTapped() - navController.navigate(WinbackNavRoutes.HelpAndFeedback) - }, - onContinueToCancellation = { - viewModel.trackContinueCancellationTapped() - navController.navigate(WinbackNavRoutes.CancelConfirmation) - }, - ) - } - composable(WinbackNavRoutes.OfferClaimed) { - OfferClaimedPage( - theme = theme.activeTheme, - onConfirm = { - viewModel.trackOfferClaimedConfirmationTapped() - dismiss() - }, - ) - } - composable(WinbackNavRoutes.AvailablePlans) { - AvailablePlansPage( - plansState = state.subscriptionPlansState, - onSelectPlan = { plan -> viewModel.changePlan(requireActivity() as AppCompatActivity, plan) }, - onGoToSubscriptions = { - if (!goToPlayStoreSubscriptions()) { - scope.launch { - snackbarHostState.showSnackbar(getString(LR.string.error_generic_message)) + }, + onSeeAvailablePlans = { + viewModel.trackAvailablePlansTapped() + navController.navigate(WinbackNavRoutes.AvailablePlans) + }, + onSeeHelpAndFeedback = { + viewModel.trackHelpAndFeedbackTapped() + navController.navigate(WinbackNavRoutes.HelpAndFeedback) + }, + onContinueToCancellation = { + viewModel.trackContinueCancellationTapped() + navController.navigate(WinbackNavRoutes.CancelConfirmation) + }, + ) + } + composable(WinbackNavRoutes.OfferClaimed) { + OfferClaimedPage( + theme = theme.activeTheme, + onConfirm = { + viewModel.trackOfferClaimedConfirmationTapped() + dismiss() + }, + ) + } + composable(WinbackNavRoutes.AvailablePlans) { + AvailablePlansPage( + plansState = state.subscriptionPlansState, + onSelectPlan = { plan -> viewModel.changePlan(requireActivity() as AppCompatActivity, plan) }, + onGoToSubscriptions = { + if (!goToPlayStoreSubscriptions()) { + scope.launch { + snackbarHostState.showSnackbar(getString(LR.string.error_generic_message)) + } } - } - }, - onReload = { viewModel.loadInitialPlans() }, - onGoBack = { - viewModel.trackPlansBackButtonTapped() - navController.popBackStack() - }, - ) - } - composable(WinbackNavRoutes.HelpAndFeedback) { - HelpPage( - activity = requireActivity(), - appBarInsets = AppBarDefaults.topAppBarWindowInsets.only(WindowInsetsSides.Horizontal), - onShowLogs = { navController.navigate(WinbackNavRoutes.SupportLogs) }, - onShowStatusPage = { navController.navigate(WinbackNavRoutes.StatusCheck) }, - onGoBack = { navController.popBackStack() }, - ) - } - composable(WinbackNavRoutes.SupportLogs) { - LogsPage( - bottomInset = 0.dp, - appBarInsets = AppBarDefaults.topAppBarWindowInsets.only(WindowInsetsSides.Horizontal), - onBackPressed = { navController.popBackStack() }, - ) - } - composable(WinbackNavRoutes.StatusCheck) { - StatusPage( - bottomInset = 0.dp, - appBarInsets = AppBarDefaults.topAppBarWindowInsets.only(WindowInsetsSides.Horizontal), - onBackPressed = { navController.popBackStack() }, - ) - } - composable(WinbackNavRoutes.CancelConfirmation) { - CancelConfirmationPage( - expirationDate = state.currentSubscriptionExpirationDate, - onKeepSubscription = { - viewModel.trackKeepSubscriptionTapped() - dismiss() - }, - onCancelSubscription = { - viewModel.trackCancelSubscriptionTapped() - if (handleSubscriptionCancellation(state.purchasedProductIds)) { + }, + onReload = { viewModel.loadInitialPlans() }, + onGoBack = { + viewModel.trackPlansBackButtonTapped() + navController.popBackStack() + }, + ) + } + composable(WinbackNavRoutes.HelpAndFeedback) { + HelpPage( + activity = requireActivity(), + appBarInsets = AppBarDefaults.topAppBarWindowInsets.only(WindowInsetsSides.Horizontal), + onShowLogs = { navController.navigate(WinbackNavRoutes.SupportLogs) }, + onShowStatusPage = { navController.navigate(WinbackNavRoutes.StatusCheck) }, + onGoBack = { navController.popBackStack() }, + ) + } + composable(WinbackNavRoutes.SupportLogs) { + LogsPage( + bottomInset = 0.dp, + appBarInsets = AppBarDefaults.topAppBarWindowInsets.only(WindowInsetsSides.Horizontal), + onBackPressed = { navController.popBackStack() }, + ) + } + composable(WinbackNavRoutes.StatusCheck) { + StatusPage( + bottomInset = 0.dp, + appBarInsets = AppBarDefaults.topAppBarWindowInsets.only(WindowInsetsSides.Horizontal), + onBackPressed = { navController.popBackStack() }, + ) + } + composable(WinbackNavRoutes.CancelConfirmation) { + CancelConfirmationPage( + expirationDate = state.currentSubscriptionExpirationDate, + onKeepSubscription = { + viewModel.trackKeepSubscriptionTapped() dismiss() - } else { - scope.launch { - snackbarHostState.showSnackbar(getString(LR.string.error_generic_message)) + }, + onCancelSubscription = { + viewModel.trackCancelSubscriptionTapped() + if (handleSubscriptionCancellation(state.purchasedProductIds)) { + dismiss() + } else { + scope.launch { + snackbarHostState.showSnackbar(getString(LR.string.error_generic_message)) + } } - } - }, - ) + }, + ) + } } - } - - DialogTintEffect(navController) - val hasPlanChangeFailed = (state.subscriptionPlansState as? SubscriptionPlansState.Loaded)?.hasPlanChangeFailed == true - if (hasPlanChangeFailed) { - LaunchedEffect(Unit) { - snackbarHostState.showSnackbar(getString(LR.string.error_generic_message)) + val hasPlanChangeFailed = (state.subscriptionPlansState as? SubscriptionPlansState.Loaded)?.hasPlanChangeFailed == true + if (hasPlanChangeFailed) { + LaunchedEffect(Unit) { + snackbarHostState.showSnackbar(getString(LR.string.error_generic_message)) + } } - } - LaunchedEffect(navController) { - navController.currentBackStackEntryFlow.collect { entry -> - val route = entry.destination.route.also { currentRoute = it } - if (route != null) { - viewModel.trackScreenShown(route) + LaunchedEffect(navController) { + navController.currentBackStackEntryFlow.collect { entry -> + val route = entry.destination.route.also { currentRoute = it } + if (route != null) { + viewModel.trackScreenShown(route) + } } } - } - - SnackbarHost( - hostState = snackbarHostState, - snackbar = { data -> - val isLightTheme = MaterialTheme.theme.isLight - Snackbar( - backgroundColor = if (isLightTheme) Color.Black else Color.White, - content = { TextH50(data.message, color = if (isLightTheme) Color.White else Color.Black) }, - ) - }, - modifier = Modifier - .align(Alignment.BottomCenter) - .padding(16.dp), - ) - } - } - } - @Composable - private fun DialogTintEffect( - navController: NavHostController, - ) { - var isNavBarWhite by remember { mutableStateOf(false) } - LaunchedEffect(navController) { - navController.currentBackStackEntryFlow.collect { entry -> - isNavBarWhite = entry.destination.route == WinbackNavRoutes.HelpAndFeedback + SnackbarHost( + hostState = snackbarHostState, + snackbar = { data -> + val isLightTheme = MaterialTheme.theme.isLight + Snackbar( + backgroundColor = if (isLightTheme) Color.Black else Color.White, + content = { TextH50(data.message, color = if (isLightTheme) Color.White else Color.Black) }, + ) + }, + modifier = Modifier + .align(Alignment.BottomCenter) + .padding(16.dp), + ) + } } } - val navigationBarTint by animateColorAsState( - animationSpec = colorAnimationSpec, - targetValue = if (isNavBarWhite) Color.White else MaterialTheme.theme.colors.primaryUi01, - ) - LaunchedEffect(Unit) { - snapshotFlow { navigationBarTint }.collect { tint -> setNavigationBarTint(tint.toArgb()) } - } } override fun onDismiss(dialog: DialogInterface) { diff --git a/modules/services/compose/src/main/java/au/com/shiftyjelly/pocketcasts/compose/Theme.kt b/modules/services/compose/src/main/java/au/com/shiftyjelly/pocketcasts/compose/Theme.kt index 67a2f02f1a0..b24380ea807 100644 --- a/modules/services/compose/src/main/java/au/com/shiftyjelly/pocketcasts/compose/Theme.kt +++ b/modules/services/compose/src/main/java/au/com/shiftyjelly/pocketcasts/compose/Theme.kt @@ -2,7 +2,6 @@ package au.com.shiftyjelly.pocketcasts.compose import android.annotation.SuppressLint import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Box import androidx.compose.material.Colors import androidx.compose.material.MaterialTheme import androidx.compose.material.Surface @@ -10,8 +9,6 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.ReadOnlyComposable import androidx.compose.runtime.staticCompositionLocalOf -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.sp import au.com.shiftyjelly.pocketcasts.ui.theme.Theme @@ -25,19 +22,12 @@ val LocalColors = staticCompositionLocalOf { PocketCastsTheme(type = Theme.Theme @Composable fun AppThemeWithBackground( themeType: Theme.ThemeType, - backgroundColor: @Composable () -> Color = { Color.Unspecified }, content: @Composable () -> Unit, ) { AppTheme(themeType) { // Use surface so Material uses appropraite tinting for icons etc. Surface(color = MaterialTheme.colors.background) { - // If we specify a custom color set is a background after Material - // sets colors through a surface - Box( - modifier = Modifier.background(backgroundColor()), - ) { - content() - } + content() } } }