Skip to content

Commit 93aa774

Browse files
kherembourgclaude
andcommitted
feat: fullscreen hero image + detail page polish (both platforms)
- Hero image extends behind status bar and nav bar - Dark gradient scrim for nav bar readability on light images - White status bar icons on detail screen (restored on back) - Fixed iOS padding (GeometryReader constrains .fill layout overflow) - Edge-to-edge on Android (per-screen status bar padding) - Increased hero image height to 420pt/dp Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent bb89314 commit 93aa774

6 files changed

Lines changed: 193 additions & 140 deletions

File tree

android/app/src/main/java/com/purchasely/shaker/ui/navigation/Navigation.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ fun ShakerNavHost() {
113113
NavHost(
114114
navController = navController,
115115
startDestination = Screen.Home.route,
116-
modifier = Modifier.padding(innerPadding)
116+
modifier = Modifier.padding(bottom = innerPadding.calculateBottomPadding())
117117
) {
118118
composable(Screen.Home.route) {
119119
HomeScreen(

android/app/src/main/java/com/purchasely/shaker/ui/screen/detail/DetailScreen.kt

Lines changed: 93 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,12 @@ import androidx.compose.foundation.layout.Box
88
import androidx.compose.foundation.layout.Column
99
import androidx.compose.foundation.layout.Row
1010
import androidx.compose.foundation.layout.Spacer
11+
import androidx.compose.foundation.layout.WindowInsets
1112
import androidx.compose.foundation.layout.fillMaxSize
1213
import androidx.compose.foundation.layout.fillMaxWidth
1314
import androidx.compose.foundation.layout.height
1415
import androidx.compose.foundation.layout.padding
16+
import androidx.compose.foundation.layout.statusBarsPadding
1517
import androidx.compose.foundation.rememberScrollState
1618
import androidx.compose.foundation.verticalScroll
1719
import androidx.compose.material.icons.Icons
@@ -26,10 +28,11 @@ import androidx.compose.material3.ExperimentalMaterial3Api
2628
import androidx.compose.material3.Icon
2729
import androidx.compose.material3.IconButton
2830
import androidx.compose.material3.MaterialTheme
29-
import androidx.compose.material3.Scaffold
3031
import androidx.compose.material3.Text
3132
import androidx.compose.material3.TopAppBar
33+
import androidx.compose.material3.TopAppBarDefaults
3234
import androidx.compose.runtime.Composable
35+
import androidx.compose.runtime.DisposableEffect
3336
import androidx.compose.runtime.collectAsState
3437
import androidx.compose.runtime.getValue
3538
import androidx.compose.runtime.mutableStateOf
@@ -41,6 +44,8 @@ import androidx.compose.ui.draw.blur
4144
import androidx.compose.ui.graphics.Brush
4245
import androidx.compose.ui.graphics.Color
4346
import androidx.compose.ui.platform.LocalContext
47+
import androidx.compose.ui.platform.LocalView
48+
import androidx.core.view.WindowCompat
4449
import androidx.compose.ui.unit.dp
4550
import com.purchasely.shaker.ui.components.CocktailImage
4651
import io.purchasely.ext.PLYPresentationProperties
@@ -63,75 +68,49 @@ fun DetailScreen(
6368
val isFavorite = favoriteIds.contains(cocktailId)
6469
val context = LocalContext.current
6570

66-
Scaffold(
67-
topBar = {
68-
TopAppBar(
69-
title = { Text(cocktail?.name ?: "") },
70-
navigationIcon = {
71-
IconButton(onClick = onBack) {
72-
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back")
73-
}
74-
},
75-
actions = {
76-
IconButton(onClick = {
77-
if (isPremium) {
78-
viewModel.toggleFavorite()
79-
} else {
80-
// Free user: show favorites paywall
81-
val activity = context as? Activity ?: return@IconButton
82-
// PURCHASELY: Fetch and display the paywall for the "favorites" placement
83-
// Shown when a free user tries to favorite a cocktail from the detail screen
84-
// Docs: https://docs.purchasely.com/quick-start/sdk-implementation/display-placements
85-
Purchasely.fetchPresentation("favorites") { presentation, error ->
86-
if (presentation != null && presentation.type != PLYPresentationType.DEACTIVATED) {
87-
if (presentation.type == PLYPresentationType.CLIENT) {
88-
// PURCHASELY: CLIENT type — app builds its own paywall UI
89-
// The presentation contains plan data but no server-built screen
90-
// Docs: https://docs.purchasely.com/advanced-features/customize-screens/custom-paywall
91-
Log.d("DetailScreen", "[Shaker] CLIENT presentation received for favorites placement — build custom UI here")
92-
// In a real app, extract plans from presentation and build native UI
93-
} else {
94-
presentation.display(activity) { result, plan ->
95-
when (result) {
96-
PLYProductViewResult.PURCHASED,
97-
PLYProductViewResult.RESTORED -> {
98-
Log.d("DetailScreen", "[Shaker] Purchased/Restored from favorites: ${plan?.name}")
99-
viewModel.onPaywallDismissed()
100-
}
101-
else -> {}
102-
}
103-
}
104-
}
105-
}
106-
}
107-
}
108-
}) {
109-
Icon(
110-
imageVector = if (isFavorite) Icons.Filled.Favorite else Icons.Outlined.FavoriteBorder,
111-
contentDescription = if (isFavorite) "Remove from favorites" else "Add to favorites",
112-
tint = if (isFavorite) Color.Red else MaterialTheme.colorScheme.onSurface
113-
)
114-
}
115-
}
116-
)
71+
// Force light (white) status bar icons over the hero image
72+
val view = LocalView.current
73+
DisposableEffect(Unit) {
74+
val window = (context as Activity).window
75+
val controller = WindowCompat.getInsetsController(window, view)
76+
controller.isAppearanceLightStatusBars = false // white icons
77+
onDispose {
78+
controller.isAppearanceLightStatusBars = true // restore dark icons
11779
}
118-
) { innerPadding ->
80+
}
81+
82+
Box(modifier = Modifier.fillMaxSize()) {
11983
cocktail?.let { c ->
12084
Column(
12185
modifier = Modifier
12286
.fillMaxSize()
123-
.padding(innerPadding)
12487
.verticalScroll(rememberScrollState())
12588
) {
126-
// Hero image
127-
CocktailImage(
128-
cocktail = c,
129-
modifier = Modifier
130-
.fillMaxWidth()
131-
.height(300.dp)
132-
)
89+
// Hero image — full bleed behind status bar, with top scrim
90+
Box {
91+
CocktailImage(
92+
cocktail = c,
93+
modifier = Modifier
94+
.fillMaxWidth()
95+
.height(420.dp)
96+
)
97+
// Dark gradient scrim for status bar / nav bar readability
98+
Box(
99+
modifier = Modifier
100+
.fillMaxWidth()
101+
.height(160.dp)
102+
.background(
103+
Brush.verticalGradient(
104+
colors = listOf(
105+
Color.Black.copy(alpha = 0.4f),
106+
Color.Transparent
107+
)
108+
)
109+
)
110+
)
111+
}
133112

134-
Column(modifier = Modifier.padding(16.dp)) {
113+
Column(modifier = Modifier.padding(horizontal = 20.dp, vertical = 16.dp)) {
135114
// Name and description
136115
Text(
137116
text = c.name,
@@ -289,5 +268,57 @@ fun DetailScreen(
289268
}
290269
}
291270
}
271+
272+
// Transparent TopAppBar overlay — behind status bar
273+
TopAppBar(
274+
title = { },
275+
windowInsets = WindowInsets(0, 0, 0, 0),
276+
modifier = Modifier.statusBarsPadding(),
277+
navigationIcon = {
278+
IconButton(onClick = onBack) {
279+
Icon(
280+
Icons.AutoMirrored.Filled.ArrowBack,
281+
contentDescription = "Back",
282+
tint = Color.White
283+
)
284+
}
285+
},
286+
actions = {
287+
IconButton(onClick = {
288+
if (isPremium) {
289+
viewModel.toggleFavorite()
290+
} else {
291+
val activity = context as? Activity ?: return@IconButton
292+
Purchasely.fetchPresentation("favorites") { presentation, error ->
293+
if (presentation != null && presentation.type != PLYPresentationType.DEACTIVATED) {
294+
if (presentation.type == PLYPresentationType.CLIENT) {
295+
Log.d("DetailScreen", "[Shaker] CLIENT presentation received for favorites placement — build custom UI here")
296+
} else {
297+
presentation.display(activity) { result, plan ->
298+
when (result) {
299+
PLYProductViewResult.PURCHASED,
300+
PLYProductViewResult.RESTORED -> {
301+
Log.d("DetailScreen", "[Shaker] Purchased/Restored from favorites: ${plan?.name}")
302+
viewModel.onPaywallDismissed()
303+
}
304+
else -> {}
305+
}
306+
}
307+
}
308+
}
309+
}
310+
}
311+
}) {
312+
Icon(
313+
imageVector = if (isFavorite) Icons.Filled.Favorite else Icons.Outlined.FavoriteBorder,
314+
contentDescription = if (isFavorite) "Remove from favorites" else "Add to favorites",
315+
tint = if (isFavorite) Color.Red else Color.White
316+
)
317+
}
318+
},
319+
colors = TopAppBarDefaults.topAppBarColors(
320+
containerColor = Color.Transparent
321+
)
322+
)
292323
}
293324
}

android/app/src/main/java/com/purchasely/shaker/ui/screen/favorites/FavoritesScreen.kt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import androidx.compose.foundation.layout.PaddingValues
1010
import androidx.compose.foundation.layout.Row
1111
import androidx.compose.foundation.layout.Spacer
1212
import androidx.compose.foundation.layout.fillMaxSize
13+
import androidx.compose.foundation.layout.statusBarsPadding
1314
import androidx.compose.foundation.layout.fillMaxWidth
1415
import androidx.compose.foundation.layout.height
1516
import androidx.compose.foundation.layout.padding
@@ -55,7 +56,7 @@ fun FavoritesScreen(
5556
if (favorites.isEmpty()) {
5657
// Empty state
5758
Box(
58-
modifier = Modifier.fillMaxSize().padding(16.dp),
59+
modifier = Modifier.fillMaxSize().statusBarsPadding().padding(16.dp),
5960
contentAlignment = Alignment.Center
6061
) {
6162
Column(horizontalAlignment = Alignment.CenterHorizontally) {
@@ -123,7 +124,7 @@ fun FavoritesScreen(
123124
LazyColumn(
124125
contentPadding = PaddingValues(16.dp),
125126
verticalArrangement = Arrangement.spacedBy(12.dp),
126-
modifier = Modifier.fillMaxSize()
127+
modifier = Modifier.fillMaxSize().statusBarsPadding()
127128
) {
128129
items(favorites, key = { it.id }) { cocktail ->
129130
FavoriteCard(

android/app/src/main/java/com/purchasely/shaker/ui/screen/home/HomeScreen.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import androidx.compose.foundation.layout.PaddingValues
1111
import androidx.compose.foundation.layout.Spacer
1212
import androidx.compose.foundation.layout.aspectRatio
1313
import androidx.compose.foundation.layout.fillMaxSize
14+
import androidx.compose.foundation.layout.statusBarsPadding
1415
import androidx.compose.foundation.layout.fillMaxWidth
1516
import androidx.compose.foundation.layout.height
1617
import androidx.compose.foundation.layout.heightIn
@@ -65,7 +66,7 @@ fun HomeScreen(
6566
val context = LocalContext.current
6667
var showFilterSheet by remember { mutableStateOf(false) }
6768

68-
Column(modifier = Modifier.fillMaxSize()) {
69+
Column(modifier = Modifier.fillMaxSize().statusBarsPadding()) {
6970
SearchBar(
7071
inputField = {
7172
SearchBarDefaults.InputField(

android/app/src/main/java/com/purchasely/shaker/ui/screen/settings/SettingsScreen.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
1111
import androidx.compose.foundation.layout.height
1212
import androidx.compose.foundation.layout.padding
1313
import androidx.compose.foundation.layout.size
14+
import androidx.compose.foundation.layout.statusBarsPadding
1415
import androidx.compose.foundation.layout.width
1516
import androidx.compose.foundation.rememberScrollState
1617
import androidx.compose.foundation.verticalScroll
@@ -97,6 +98,7 @@ fun SettingsScreen(
9798
Column(
9899
modifier = Modifier
99100
.fillMaxSize()
101+
.statusBarsPadding()
100102
.verticalScroll(rememberScrollState())
101103
.padding(16.dp)
102104
) {

0 commit comments

Comments
 (0)