This commit is contained in:
spaced4ndy
2026-07-31 14:54:17 +04:00
parent 8ffcd80aed
commit 2189782819
8 changed files with 153 additions and 130 deletions
@@ -53,7 +53,7 @@ struct BadgesPayView: View {
Text(level.tagline)
.font(.body)
.foregroundColor(theme.colors.secondary)
.foregroundColor(theme.colors.onBackground)
.multilineTextAlignment(.center)
.fixedSize(horizontal: false, vertical: true)
.padding(.top, 4)
@@ -66,15 +66,17 @@ struct BadgesPayView: View {
Spacer(minLength: 20)
payButton()
Text(billingFooter)
.font(.footnote)
.foregroundColor(theme.colors.secondary)
.multilineTextAlignment(.center)
.fixedSize(horizontal: false, vertical: true)
.padding(.top, 4)
.padding(.bottom, g.safeAreaInsets.bottom == 0 ? 20 : 0)
VStack(spacing: 10) {
payButton()
.padding(.vertical, 10)
Text(billingFooter)
.font(.footnote)
.foregroundColor(theme.colors.secondary)
.multilineTextAlignment(.center)
.fixedSize(horizontal: false, vertical: true)
.frame(height: 22)
}
.padding(.bottom, g.safeAreaInsets.bottom == 0 ? 20 : 0)
}
.padding(.horizontal, 25)
.padding(.top, 8)
@@ -14,7 +14,7 @@ import SimpleXChat
struct BadgesSupportSimplexView: View {
@EnvironmentObject var theme: AppTheme
@Environment(\.colorScheme) var colorScheme: ColorScheme
@State private var showWhySimpleX = false
@State private var whyBuiltActive = false
@State private var chooseLevelActive = false
@State private var redeemCodeActive = false
@@ -34,10 +34,7 @@ struct BadgesSupportSimplexView: View {
.multilineTextAlignment(.center)
.fixedSize(horizontal: false, vertical: true)
Button { showWhySimpleX = true } label: {
Label("Why SimpleX is built.", systemImage: "info.circle")
.font(.headline)
}
whyBuiltButton()
Spacer(minLength: 0)
@@ -47,11 +44,14 @@ struct BadgesSupportSimplexView: View {
Spacer(minLength: 0)
chooseLevelButton()
redeemCodeButton()
.padding(.top, 4)
.padding(.bottom, g.safeAreaInsets.bottom == 0 ? 20 : 0)
// Onboarding pattern: nested VStack(spacing: 10) + action button vertical padding 10.
VStack(spacing: 10) {
chooseLevelButton()
.padding(.vertical, 10)
redeemCodeButton()
.frame(height: 22)
}
.padding(.bottom, g.safeAreaInsets.bottom == 0 ? 20 : 0)
}
.padding(.horizontal, 25)
.padding(.top, 28)
@@ -63,8 +63,22 @@ struct BadgesSupportSimplexView: View {
.frame(maxHeight: .infinity)
.modifier(ThemedBackground())
.navigationBarHidden(true)
.sheet(isPresented: $showWhySimpleX) {
WhySimpleX(onboarding: false, titleColor: theme.colors.primary, createProfileNavLinkActive: .constant(false))
}
private func whyBuiltButton() -> some View {
ZStack {
Button { whyBuiltActive = true } label: {
Label("Why SimpleX is built.", systemImage: "info.circle")
.font(.headline)
}
NavigationLink(isActive: $whyBuiltActive) {
WhySimpleX(onboarding: false, titleColor: theme.colors.primary, createProfileNavLinkActive: .constant(false))
.modifier(ThemedBackground())
} label: {
EmptyView()
}
.frame(width: 1, height: 1)
.hidden()
}
}
@@ -113,11 +113,13 @@ struct BadgesYourLevelView: View {
Spacer(minLength: 20)
continueButton()
howItWorksButton()
.padding(.top, 4)
.padding(.bottom, g.safeAreaInsets.bottom == 0 ? 20 : 0)
VStack(spacing: 10) {
continueButton()
.padding(.vertical, 10)
howItWorksButton()
.frame(height: 22)
}
.padding(.bottom, g.safeAreaInsets.bottom == 0 ? 20 : 0)
}
.padding(.horizontal, 25)
.padding(.top, 8)
@@ -53,7 +53,8 @@ struct SupportSimpleXBanner: View {
}
.padding(.leading, cardLeadingPadding)
.padding(.trailing, cardTrailingPadding)
.frame(height: cardHeight)
.padding(.vertical, 12)
.frame(minHeight: cardHeight)
.background(gradientBackground())
.clipShape(RoundedRectangle(cornerRadius: cardCornerRadius))
}
@@ -46,7 +46,7 @@ fun BadgesPayView(level: BadgeLevel) {
var selectedPeriod by remember { mutableStateOf(BadgePeriod.Subscribe) }
ColumnWithScrollBar(
Modifier.padding(horizontal = 25.dp).padding(top = 8.dp, bottom = 20.dp),
Modifier.background(MaterialTheme.colors.background).padding(horizontal = 25.dp).padding(top = 8.dp, bottom = 20.dp),
verticalArrangement = Arrangement.spacedBy(16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
maxIntrinsicSize = true,
@@ -65,7 +65,7 @@ fun BadgesPayView(level: BadgeLevel) {
Text(
stringResource(level.tagline),
style = MaterialTheme.typography.body1,
color = MaterialTheme.colors.secondary,
color = MaterialTheme.colors.onBackground,
textAlign = TextAlign.Center,
modifier = Modifier.fillMaxWidth().padding(top = 4.dp)
)
@@ -80,15 +80,20 @@ fun BadgesPayView(level: BadgeLevel) {
Spacer(Modifier.weight(1f).heightIn(min = 20.dp))
PayButton(level, selectedPeriod)
Text(
stringResource(billingFooter(selectedPeriod)),
style = MaterialTheme.typography.body2,
color = MaterialTheme.colors.secondary,
textAlign = TextAlign.Center,
modifier = Modifier.fillMaxWidth().padding(top = 4.dp)
)
// Plain Text (not TextButtonBelowOnboardingButton) because the billing footer is informational,
// not an action — using TextButtonBelowOnboardingButton would force a Medium-weight bold look.
// 15.5dp top padding matches the visual gap the TextButtonBelowOnboardingButton produces on
// the other two badges views (7.5dp Modifier + 8dp TextButton chip inner padding).
Column(horizontalAlignment = Alignment.CenterHorizontally) {
PayButton(level, selectedPeriod)
Text(
stringResource(billingFooter(selectedPeriod)),
style = MaterialTheme.typography.body2,
color = MaterialTheme.colors.secondary,
textAlign = TextAlign.Center,
modifier = Modifier.fillMaxWidth().padding(top = 15.5.dp)
)
}
}
}
@@ -103,7 +108,7 @@ private fun PeriodCard(period: BadgePeriod, selectedPeriod: BadgePeriod, modifie
.background(MaterialTheme.colors.background.mixWith(MaterialTheme.colors.onBackground, 0.97f), shape)
.border(2.dp, borderColor, shape)
.clickable { onSelect(period) }
.padding(vertical = 20.dp),
.padding(vertical = 20.dp, horizontal = 12.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
@@ -113,7 +118,7 @@ private fun PeriodCard(period: BadgePeriod, selectedPeriod: BadgePeriod, modifie
tint = if (isSelected) MaterialTheme.colors.primary else MaterialTheme.colors.secondary,
modifier = Modifier.size(32.dp)
)
Text(stringResource(period.label), style = MaterialTheme.typography.h3, fontWeight = FontWeight.Bold)
Text(stringResource(period.label), style = MaterialTheme.typography.h3, fontWeight = FontWeight.Bold, textAlign = TextAlign.Center)
}
}
@@ -27,6 +27,7 @@ import chat.simplex.common.views.newchat.gradientPoints
import chat.simplex.common.views.newchat.lightStops
import chat.simplex.common.views.onboarding.HowItWorks
import chat.simplex.common.views.onboarding.OnboardingActionButton
import chat.simplex.common.views.onboarding.TextButtonBelowOnboardingButton
import chat.simplex.res.MR
// Entry point for badges management. Subsequent screens push via ModalManager; the enclosing
@@ -35,7 +36,7 @@ import chat.simplex.res.MR
fun BadgesSupportSimplexView() {
// TODO [badges] gate on user badge status (no badge → this view, active → "Manage your badge")
ColumnWithScrollBar(
Modifier.padding(horizontal = 25.dp).padding(top = 28.dp, bottom = 20.dp),
Modifier.background(MaterialTheme.colors.background).padding(horizontal = 25.dp).padding(top = 28.dp, bottom = 20.dp),
verticalArrangement = Arrangement.spacedBy(16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
maxIntrinsicSize = true,
@@ -72,9 +73,13 @@ fun BadgesSupportSimplexView() {
Spacer(Modifier.weight(1f))
ChooseLevelButton()
RedeemCodeButton(Modifier.padding(top = 4.dp))
Column(horizontalAlignment = Alignment.CenterHorizontally) {
ChooseLevelButton()
TextButtonBelowOnboardingButton(
text = stringResource(MR.strings.badges_redeem_code_button),
onClick = { ModalManager.start.showModal { BadgesRedeemCodeView() } }
)
}
}
}
@@ -90,19 +95,6 @@ private fun ChooseLevelButton() {
)
}
@Composable
private fun RedeemCodeButton(modifier: Modifier = Modifier) {
TextButton(
onClick = { ModalManager.start.showModal { BadgesRedeemCodeView() } },
modifier = modifier
) {
Text(
stringResource(MR.strings.badges_redeem_code_button),
color = MaterialTheme.colors.primary,
fontWeight = FontWeight.Medium
)
}
}
// Hero image reused across badges views and WhatsNewView v7.1. Fallback (no SIMPLEX_ASSETS) is a
// gradient card carrying the small supporter badge glyph.
@@ -20,6 +20,7 @@ import chat.simplex.common.platform.*
import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.helpers.*
import chat.simplex.common.views.onboarding.OnboardingActionButton
import chat.simplex.common.views.onboarding.TextButtonBelowOnboardingButton
import chat.simplex.res.MR
// Draft levels used by the badges UI while the API/state machine is still being designed. TODO [badges]:
@@ -88,7 +89,7 @@ fun BadgesYourLevelView() {
var selectedLevel by remember { mutableStateOf(BadgeLevel.Supporter) }
ColumnWithScrollBar(
Modifier.padding(horizontal = 25.dp).padding(top = 8.dp, bottom = 20.dp),
Modifier.background(MaterialTheme.colors.background).padding(horizontal = 25.dp).padding(top = 8.dp, bottom = 20.dp),
verticalArrangement = Arrangement.spacedBy(16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
maxIntrinsicSize = true,
@@ -120,9 +121,16 @@ fun BadgesYourLevelView() {
Spacer(Modifier.weight(1f).heightIn(min = 20.dp))
ContinueButton(selectedLevel)
HowItWorksButton(Modifier.padding(top = 4.dp))
// Nested Column with no spacing so the TextButtonBelowOnboardingButton sits directly under
// the action button (matches onboarding pattern where its own 7.5dp top padding is the gap).
Column(horizontalAlignment = Alignment.CenterHorizontally) {
ContinueButton(selectedLevel)
TextButtonBelowOnboardingButton(
text = stringResource(MR.strings.badges_how_it_works_button),
icon = painterResource(MR.images.ic_info),
onClick = { ModalManager.start.showModal { BadgesHowItWorksView() } }
)
}
}
}
@@ -137,7 +145,7 @@ private fun LevelCard(level: BadgeLevel, selectedLevel: BadgeLevel, modifier: Mo
.background(MaterialTheme.colors.background.mixWith(MaterialTheme.colors.onBackground, 0.97f), shape)
.border(2.dp, borderColor, shape)
.clickable { onSelect(level) }
.padding(vertical = 20.dp),
.padding(vertical = 20.dp, horizontal = 12.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(10.dp)
) {
@@ -147,9 +155,9 @@ private fun LevelCard(level: BadgeLevel, selectedLevel: BadgeLevel, modifier: Mo
contentScale = ContentScale.Fit,
modifier = Modifier.size(60.dp)
)
Text(stringResource(level.title), style = MaterialTheme.typography.h3, fontWeight = FontWeight.Bold)
Text(stringResource(level.filesDescription), style = MaterialTheme.typography.body2, color = MaterialTheme.colors.secondary)
Text(stringResource(level.monthlyPrice), style = MaterialTheme.typography.body1)
Text(stringResource(level.title), style = MaterialTheme.typography.h3, fontWeight = FontWeight.Bold, textAlign = TextAlign.Center)
Text(stringResource(level.filesDescription), style = MaterialTheme.typography.body2, color = MaterialTheme.colors.secondary, textAlign = TextAlign.Center)
Text(stringResource(level.monthlyPrice), style = MaterialTheme.typography.body1, textAlign = TextAlign.Center)
}
}
@@ -165,15 +173,3 @@ private fun ContinueButton(selectedLevel: BadgeLevel) {
)
}
@Composable
private fun HowItWorksButton(modifier: Modifier = Modifier) {
TextButton(
onClick = { ModalManager.start.showModal { BadgesHowItWorksView() } },
modifier = modifier
) {
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(6.dp)) {
Icon(painterResource(MR.images.ic_info), null, tint = MaterialTheme.colors.primary)
Text(stringResource(MR.strings.badges_how_it_works_button), color = MaterialTheme.colors.primary, fontWeight = FontWeight.Medium)
}
}
}
@@ -32,9 +32,10 @@ import chat.simplex.res.MR
@Composable
fun SupportSimpleXBanner(onTap: () -> Unit, onDismiss: () -> Unit) {
val cardCornerRadius = 16.dp
// grows with system font but never shrinks below the default so small-font users see the same
// banner as today; hero image stays fixed so its above-card overhang shrinks at very large fonts
val cardHeight = (72.dp * fontSizeSqrtMultiplier).coerceAtLeast(72.dp)
// grows linearly with system font but never shrinks below the default so small-font users see the
// same baseline; the card Row uses heightIn(min = cardHeight) and grows further when 2-line text
// wraps at very large fonts. Hero stays fixed so its above-card overhang shrinks at very large fonts.
val cardHeight = (72.dp * fontSizeMultiplier).coerceAtLeast(72.dp)
// matches OneHandUICard's segment icon leading so the text aligns with it in the list
val cardLeadingPadding = 16.dp
val cardTrailingPadding = 8.dp
@@ -49,36 +50,66 @@ fun SupportSimpleXBanner(onTap: () -> Unit, onDismiss: () -> Unit) {
var cardSize by remember { mutableStateOf(IntSize.Zero) }
val brush = remember(isDark, cardSize) { gradientBrush(isDark, cardSize) }
// Root Box is sized to the card; hero and X are children so they can render outside the card's
// rounded-corner clip. Hero anchors bottom-end and its 108dp height extends 36dp above the card top.
Box(Modifier.fillMaxWidth().height(cardHeight)) {
Row(
Modifier
.fillMaxSize()
.clip(RoundedCornerShape(cardCornerRadius))
.background(brush)
.clickable(onClick = onTap)
.onSizeChanged { cardSize = it }
.padding(start = cardLeadingPadding, end = cardTrailingPadding + heroWidth + heroTrailingPadding + textToHeroGap),
verticalAlignment = Alignment.CenterVertically
) {
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
Text(
generalGetString(MR.strings.badges_banner_title),
style = MaterialTheme.typography.body1,
fontWeight = FontWeight.SemiBold,
color = MaterialTheme.colors.primary,
maxLines = 2,
overflow = TextOverflow.Ellipsis
)
Text(
generalGetString(MR.strings.badges_banner_subtitle),
style = MaterialTheme.typography.body2,
color = MaterialTheme.colors.onBackground,
maxLines = 2,
overflow = TextOverflow.Ellipsis
)
// Two-Box structure so text can grow (2-line wrapping at large fonts) while X stays anchored to
// the card's top-right, not the outer wrapper's top-right (which would drift up into hero overhang).
// Outer Box takes max(inner card, hero); inner Box wraps the card Row + X together and aligns to
// Outer bottom so hero overhangs above.
Box(Modifier.fillMaxWidth(), contentAlignment = Alignment.BottomStart) {
Box(Modifier.fillMaxWidth()) {
Row(
Modifier
.fillMaxWidth()
.heightIn(min = cardHeight)
.clip(RoundedCornerShape(cardCornerRadius))
.background(brush)
.clickable(onClick = onTap)
.onSizeChanged { cardSize = it }
.padding(
start = cardLeadingPadding,
end = cardTrailingPadding + heroWidth + heroTrailingPadding + textToHeroGap,
top = 12.dp,
bottom = 12.dp
),
verticalAlignment = Alignment.CenterVertically
) {
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
Text(
generalGetString(MR.strings.badges_banner_title),
style = MaterialTheme.typography.body1,
fontWeight = FontWeight.SemiBold,
color = MaterialTheme.colors.primary,
maxLines = 2,
overflow = TextOverflow.Ellipsis
)
Text(
generalGetString(MR.strings.badges_banner_subtitle),
style = MaterialTheme.typography.body2,
color = MaterialTheme.colors.onBackground,
maxLines = 2,
overflow = TextOverflow.Ellipsis
)
}
}
// Same X pattern as OneHandUICard: circle-clipped clickable region with inner padding for hit area.
Icon(
painterResource(MR.images.ic_close),
contentDescription = generalGetString(MR.strings.icon_descr_close_button),
tint = MaterialTheme.colors.secondary,
modifier = Modifier
.align(Alignment.TopEnd)
.padding(end = 4.dp, top = 4.dp)
.clip(CircleShape)
.clickable {
AlertManager.shared.showAlertMsg(
title = generalGetString(MR.strings.badges_banner_title),
text = generalGetString(MR.strings.badges_banner_dismiss_message),
onConfirm = onDismiss
)
}
.padding(8.dp)
.size(16.dp)
)
}
HeroThumbnail(
@@ -88,26 +119,6 @@ fun SupportSimpleXBanner(onTap: () -> Unit, onDismiss: () -> Unit) {
trailingPadding = heroTrailingPadding,
modifier = Modifier.align(Alignment.BottomEnd)
)
// Same X pattern as OneHandUICard: circle-clipped clickable region with inner padding for hit area.
Icon(
painterResource(MR.images.ic_close),
contentDescription = generalGetString(MR.strings.icon_descr_close_button),
tint = MaterialTheme.colors.secondary,
modifier = Modifier
.align(Alignment.TopEnd)
.padding(end = 4.dp, top = 4.dp)
.clip(CircleShape)
.clickable {
AlertManager.shared.showAlertMsg(
title = generalGetString(MR.strings.badges_banner_title),
text = generalGetString(MR.strings.badges_banner_dismiss_message),
onConfirm = onDismiss
)
}
.padding(8.dp)
.size(16.dp)
)
}
}