Skip to content

Commit

Permalink
Improve Winback edge-to-edge experience (#3524)
Browse files Browse the repository at this point in the history
  • Loading branch information
MiSikora authored Feb 3, 2025
1 parent f73ba20 commit be51865
Show file tree
Hide file tree
Showing 2 changed files with 153 additions and 167 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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) {
Expand Down
Loading

0 comments on commit be51865

Please sign in to comment.