Skip to content

Commit 6468c60

Browse files
kherembourgclaude
andcommitted
feat: polish Settings UI + fullscreen detail hero (both platforms)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2 parents 9aab625 + 93aa774 commit 6468c60

10 files changed

Lines changed: 256 additions & 168 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: 32 additions & 26 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
) {
@@ -113,7 +115,7 @@ fun SettingsScreen(
113115
Row(verticalAlignment = Alignment.CenterVertically) {
114116
Icon(Icons.Default.Person, contentDescription = null)
115117
Spacer(modifier = Modifier.width(8.dp))
116-
Column {
118+
Column(modifier = Modifier.weight(1f)) {
117119
Text(
118120
text = "Logged in as",
119121
style = MaterialTheme.typography.bodySmall,
@@ -124,34 +126,38 @@ fun SettingsScreen(
124126
style = MaterialTheme.typography.bodyLarge
125127
)
126128
}
127-
}
128-
Spacer(modifier = Modifier.height(12.dp))
129-
OutlinedButton(
130-
onClick = { viewModel.logout() },
131-
modifier = Modifier.fillMaxWidth()
132-
) {
133-
Text("Logout")
129+
TextButton(onClick = { viewModel.logout() }) {
130+
Text(
131+
"Logout",
132+
style = MaterialTheme.typography.labelSmall,
133+
color = MaterialTheme.colorScheme.error
134+
)
135+
}
134136
}
135137
} else {
136138
// Login form
137-
OutlinedTextField(
138-
value = loginInput,
139-
onValueChange = { loginInput = it },
140-
label = { Text("User ID") },
141-
placeholder = { Text("Enter any user ID") },
142-
singleLine = true,
139+
Row(
140+
verticalAlignment = Alignment.CenterVertically,
143141
modifier = Modifier.fillMaxWidth()
144-
)
145-
Spacer(modifier = Modifier.height(8.dp))
146-
Button(
147-
onClick = {
148-
viewModel.login(loginInput)
149-
loginInput = ""
150-
},
151-
modifier = Modifier.fillMaxWidth(),
152-
enabled = loginInput.isNotBlank()
153142
) {
154-
Text("Login")
143+
OutlinedTextField(
144+
value = loginInput,
145+
onValueChange = { loginInput = it },
146+
label = { Text("User ID") },
147+
placeholder = { Text("Enter any user ID") },
148+
singleLine = true,
149+
modifier = Modifier.weight(1f)
150+
)
151+
Spacer(modifier = Modifier.width(8.dp))
152+
Button(
153+
onClick = {
154+
viewModel.login(loginInput)
155+
loginInput = ""
156+
},
157+
enabled = loginInput.isNotBlank()
158+
) {
159+
Text("Login")
160+
}
155161
}
156162
}
157163

@@ -351,9 +357,9 @@ fun SettingsScreen(
351357
HorizontalDivider()
352358
Spacer(modifier = Modifier.height(24.dp))
353359

354-
// Display Mode section (Android only)
360+
// Display Mode section
355361
Text(
356-
text = "Paywall Display Mode",
362+
text = "Screen Display Mode",
357363
style = MaterialTheme.typography.titleMedium,
358364
color = MaterialTheme.colorScheme.primary
359365
)
20.7 KB
Loading

docs/screenshots/ios-settings.png

70.3 KB
Loading

0 commit comments

Comments
 (0)