From 42706b4dc6a32a0b7acc6cf131ce91c8cf8db8fa Mon Sep 17 00:00:00 2001 From: null Date: Mon, 22 Jun 2026 18:14:55 -0500 Subject: [PATCH] feat: settings screen + iOS settings views --- .../app/closer/ui/settings/SettingsScreen.kt | 94 ++-- iphone/Closer/Settings/SettingsViews.swift | 442 +++++++++++++----- 2 files changed, 381 insertions(+), 155 deletions(-) diff --git a/app/src/main/java/app/closer/ui/settings/SettingsScreen.kt b/app/src/main/java/app/closer/ui/settings/SettingsScreen.kt index f4354043..33bc428e 100644 --- a/app/src/main/java/app/closer/ui/settings/SettingsScreen.kt +++ b/app/src/main/java/app/closer/ui/settings/SettingsScreen.kt @@ -36,8 +36,8 @@ import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults import androidx.compose.material3.CircularProgressIndicator -import androidx.compose.material3.Divider import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme @@ -117,30 +117,40 @@ fun SettingsSubpage( fun SettingsSection( title: String? = null, modifier: Modifier = Modifier, + accent: Color = Color(0xFFF7C8E4), content: @Composable () -> Unit ) { - Column( + Card( modifier = modifier - .fillMaxWidth() - .background(SettingsCard, RoundedCornerShape(16.dp)) - .padding(vertical = 8.dp), - verticalArrangement = Arrangement.spacedBy(4.dp) + .fillMaxWidth(), + shape = RoundedCornerShape(22.dp), + colors = CardDefaults.cardColors(containerColor = accent.copy(alpha = 0.16f)), + elevation = CardDefaults.cardElevation(defaultElevation = 5.dp) ) { - title?.let { - Text( - text = it, - style = MaterialTheme.typography.labelMedium, - color = SettingsMuted, - modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp) - ) + Column( + modifier = Modifier + .fillMaxWidth() + .padding(1.dp) + .background(SettingsCard.copy(alpha = 0.92f), RoundedCornerShape(21.dp)) + .padding(vertical = 8.dp), + verticalArrangement = Arrangement.spacedBy(2.dp) + ) { + title?.let { + Text( + text = it, + style = MaterialTheme.typography.labelMedium, + color = SettingsMuted, + modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp) + ) + } + content() } - content() } } @Composable fun SettingsSectionDivider() { - Divider(modifier = Modifier.padding(horizontal = 16.dp), thickness = 0.5.dp) + HorizontalDivider(modifier = Modifier.padding(horizontal = 16.dp), thickness = 0.5.dp) } // ==================== @@ -287,18 +297,28 @@ fun SettingsScreen( .navigationBarsPadding() .verticalScroll(rememberScrollState()) .padding(padding) - .padding(horizontal = 16.dp, vertical = 8.dp), - verticalArrangement = Arrangement.spacedBy(12.dp) + .padding(horizontal = 16.dp, vertical = 10.dp), + verticalArrangement = Arrangement.spacedBy(14.dp) ) { + Text( + text = "Manage your space, reminders, privacy, and account.", + style = MaterialTheme.typography.bodyMedium, + color = SettingsMuted, + modifier = Modifier.padding(horizontal = 4.dp), + maxLines = 2, + overflow = TextOverflow.Ellipsis + ) + // Profile card — editable identity (currently local-only) Card( onClick = { onNavigate(AppRoute.ACCOUNT) }, modifier = Modifier.fillMaxWidth(), - shape = RoundedCornerShape(16.dp), - colors = CardDefaults.cardColors(containerColor = SettingsCard) + shape = RoundedCornerShape(22.dp), + colors = CardDefaults.cardColors(containerColor = Color(0xFFFFF8FC).copy(alpha = 0.96f)), + elevation = CardDefaults.cardElevation(defaultElevation = 5.dp) ) { Row( - modifier = Modifier.padding(16.dp), + modifier = Modifier.padding(18.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(12.dp) ) { @@ -345,13 +365,14 @@ fun SettingsScreen( ) }, modifier = Modifier.fillMaxWidth(), - shape = RoundedCornerShape(16.dp), - colors = CardDefaults.cardColors(containerColor = SettingsCard) + shape = RoundedCornerShape(22.dp), + colors = CardDefaults.cardColors(containerColor = Color(0xFFF8F0FF).copy(alpha = 0.96f)), + elevation = CardDefaults.cardElevation(defaultElevation = 5.dp) ) { Row( modifier = Modifier .fillMaxWidth() - .padding(16.dp), + .padding(18.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(12.dp) ) { @@ -399,9 +420,7 @@ fun SettingsScreen( } } - Spacer(Modifier.height(4.dp)) - - SettingsSection(title = "For the two of you") { + SettingsSection(title = "For the two of you", accent = Color(0xFFF7C8E4)) { SettingsRow( icon = Icons.Filled.Done, label = "Answer History", @@ -417,7 +436,7 @@ fun SettingsScreen( ) } - SettingsSection(title = "Your rhythm") { + SettingsSection(title = "Your rhythm", accent = Color(0xFFD9B8FF)) { SettingsRow( icon = Icons.Filled.Notifications, label = "Notifications", @@ -433,7 +452,7 @@ fun SettingsScreen( ) } - SettingsSection(title = "Premium") { + SettingsSection(title = "Premium", accent = Color(0xFFE7A2D1)) { SettingsRow( icon = Icons.Filled.Favorite, label = "Subscription", @@ -443,7 +462,7 @@ fun SettingsScreen( ) } - SettingsSection(title = "Privacy and safety") { + SettingsSection(title = "Privacy and safety", accent = Color(0xFFD9B8FF)) { SettingsRow( icon = Icons.Filled.Lock, label = "Security", @@ -459,7 +478,7 @@ fun SettingsScreen( ) } - SettingsSection(title = "Account") { + SettingsSection(title = "Account", accent = Color(0xFFFFD9E8)) { SettingsRow( icon = Icons.Filled.Warning, label = "Delete account", @@ -540,7 +559,20 @@ private fun SettingsRow( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(12.dp) ) { - Icon(icon, contentDescription = null, tint = tint) + androidx.compose.foundation.layout.Box( + modifier = Modifier + .size(40.dp) + .clip(RoundedCornerShape(14.dp)) + .background(tint.copy(alpha = if (tint == SettingsDanger) 0.12f else 0.14f)), + contentAlignment = Alignment.Center + ) { + Icon( + icon, + contentDescription = null, + tint = tint, + modifier = Modifier.size(20.dp) + ) + } Column( modifier = Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(2.dp) diff --git a/iphone/Closer/Settings/SettingsViews.swift b/iphone/Closer/Settings/SettingsViews.swift index bb5e6b58..decf7b46 100644 --- a/iphone/Closer/Settings/SettingsViews.swift +++ b/iphone/Closer/Settings/SettingsViews.swift @@ -13,8 +13,14 @@ struct SettingsView: View { var body: some View { NavigationStack { - List { - Section { + ScrollView { + VStack(alignment: .leading, spacing: CloserSpacing.lg) { + Text("Manage your space, reminders, privacy, and account.") + .font(CloserFont.callout) + .foregroundColor(.closerTextSecondary) + .fixedSize(horizontal: false, vertical: true) + .padding(.horizontal, CloserSpacing.xl) + if isLoggedInAnonymously { NavigationLink { SignUpView() @@ -26,6 +32,8 @@ struct SettingsView: View { imageUrl: appState.currentUser?.photoUrl ) } + .buttonStyle(.plain) + .closerPadding() } else { NavigationLink { EditProfileView() @@ -37,139 +45,184 @@ struct SettingsView: View { imageUrl: appState.currentUser?.photoUrl ) } + .buttonStyle(.plain) + .closerPadding() } - } - Section("For the two of you") { - if let partner = appState.currentPartner { - SettingsLinkLabel( - icon: "heart.fill", - title: "Connected with \(partner.displayName.isEmpty ? "Partner" : partner.displayName)", - subtitle: "Your shared Closer space is active", - tint: .closerPrimary, - imageUrl: partner.photoUrl - ) - } else { - Button(action: { isPairingActive = true }) { - SettingsLinkLabel( - icon: "heart", - title: "Invite your partner", - subtitle: "Start answering together", - tint: .closerPrimary + SettingsSectionCard(title: "For the two of you", accent: .closerSecondary) { + if let partner = appState.currentPartner { + SettingsLinkRow( + icon: "heart.fill", + title: "Connected with \(partner.displayName.isEmpty ? "Partner" : partner.displayName)", + subtitle: "Your shared Closer space is active", + tint: .closerPrimary, + imageUrl: partner.photoUrl, + showsChevron: false + ) + } else { + Button(action: { isPairingActive = true }) { + SettingsLinkRow( + icon: "heart", + title: "Invite your partner", + subtitle: "Start answering together", + tint: .closerPrimary + ) + } + .buttonStyle(.plain) + } + + SettingsDivider() + + NavigationLink { + AnswerHistoryView() + } label: { + SettingsLinkRow( + icon: "checkmark.circle", + title: "Answer History", + subtitle: "Revisit the moments you have shared" ) } .buttonStyle(.plain) } + .closerPadding() - NavigationLink { - AnswerHistoryView() - } label: { - SettingsLinkLabel( - icon: "checkmark.circle", - title: "Answer History", - subtitle: "Revisit the moments you have shared" - ) - } - } - - Section("Your rhythm") { - NavigationLink { - NavigationSettingsView() - } label: { - SettingsLinkLabel( - icon: "bell.badge", - title: "Notifications", - subtitle: "Set gentle reminders that fit your day" - ) - } - } - - Section("Premium") { - Button { - showPaywall = true - } label: { - PremiumSettingsCTA() - } - .disabled(isLoggedInAnonymously) - .buttonStyle(.plain) - } - - Section("Privacy and safety") { - NavigationLink { - DataExportView() - } label: { - SettingsLinkLabel( - icon: "square.and.arrow.up", - title: "Export Data", - subtitle: "Download a copy of your Closer data" - ) - } - - NavigationLink { - Text("Privacy Policy") - } label: { - SettingsLinkLabel( - icon: "hand.raised", - title: "Privacy Policy", - subtitle: "How your data is handled" - ) - } - - NavigationLink { - Text("Terms of Service") - } label: { - SettingsLinkLabel( - icon: "doc.text", - title: "Terms of Service", - subtitle: "The agreement for using Closer" - ) - } - } - - Section("Support") { - NavigationLink { - HelpCenterView() - } label: { - SettingsLinkLabel( - icon: "questionmark.circle", - title: "Help Center", - subtitle: "Answers for common questions" - ) - } - - NavigationLink { - Text("Contact Us") - } label: { - SettingsLinkLabel( - icon: "envelope", - title: "Contact Us", - subtitle: "Get help from the Closer team" - ) - } - - HStack { - Label("Version", systemImage: "info.circle") - Spacer() - Text(appVersion) - .font(CloserFont.footnote) - .foregroundColor(.closerTextSecondary) - } - } - - Section("Account") { - Button(role: .destructive, action: { showLogoutConfirm = true }) { - Label("Sign Out", systemImage: "rectangle.portrait.and.arrow.right") - } - - Button(role: .destructive, action: { showDeleteConfirm = true }) { - Label("Delete Account", systemImage: "trash") + SettingsSectionCard(title: "Your rhythm", accent: .closerPrimary) { + NavigationLink { + NavigationSettingsView() + } label: { + SettingsLinkRow( + icon: "bell.badge", + title: "Notifications", + subtitle: "Set gentle reminders that fit your day" + ) + } + .buttonStyle(.plain) } + .closerPadding() + + SettingsSectionCard(title: "Premium", accent: .closerSecondary) { + Button { + showPaywall = true + } label: { + SettingsLinkRow( + icon: "heart.fill", + title: "Upgrade to Premium", + subtitle: "One subscription for both partners.", + tint: .closerPrimary + ) + } + .disabled(isLoggedInAnonymously) + .opacity(isLoggedInAnonymously ? 0.55 : 1) + .buttonStyle(.plain) + } + .closerPadding() + + SettingsSectionCard(title: "Privacy and safety", accent: .closerPrimary) { + NavigationLink { + DataExportView() + } label: { + SettingsLinkRow( + icon: "square.and.arrow.up", + title: "Export Data", + subtitle: "Download a copy of your Closer data" + ) + } + .buttonStyle(.plain) + + SettingsDivider() + + NavigationLink { + Text("Privacy Policy") + } label: { + SettingsLinkRow( + icon: "hand.raised", + title: "Privacy Policy", + subtitle: "How your data is handled" + ) + } + .buttonStyle(.plain) + + SettingsDivider() + + NavigationLink { + Text("Terms of Service") + } label: { + SettingsLinkRow( + icon: "doc.text", + title: "Terms of Service", + subtitle: "The agreement for using Closer" + ) + } + .buttonStyle(.plain) + } + .closerPadding() + + SettingsSectionCard(title: "Support", accent: .closerSecondary) { + NavigationLink { + HelpCenterView() + } label: { + SettingsLinkRow( + icon: "questionmark.circle", + title: "Help Center", + subtitle: "Answers for common questions" + ) + } + .buttonStyle(.plain) + + SettingsDivider() + + NavigationLink { + Text("Contact Us") + } label: { + SettingsLinkRow( + icon: "envelope", + title: "Contact Us", + subtitle: "Get help from the Closer team" + ) + } + .buttonStyle(.plain) + + SettingsDivider() + + SettingsVersionRow(version: appVersion) + } + .closerPadding() + + SettingsSectionCard(title: "Account", accent: .closerDanger.opacity(0.5)) { + Button(role: .destructive, action: { showLogoutConfirm = true }) { + SettingsLinkRow( + icon: "rectangle.portrait.and.arrow.right", + title: "Sign Out", + subtitle: "Leave this device while keeping your data saved", + tint: .closerDanger, + isDestructive: true, + showsChevron: false + ) + } + .buttonStyle(.plain) + + SettingsDivider() + + Button(role: .destructive, action: { showDeleteConfirm = true }) { + SettingsLinkRow( + icon: "trash", + title: "Delete Account", + subtitle: "Permanently remove your Closer account", + tint: .closerDanger, + isDestructive: true, + showsChevron: false + ) + } + .buttonStyle(.plain) + } + .closerPadding() } + .padding(.top, CloserSpacing.md) + .padding(.bottom, CloserSpacing.xxl) } - .listStyle(.insetGrouped) .background(Color.closerBackground) .navigationTitle("Settings") - .navigationBarTitleDisplayMode(.inline) + .navigationBarTitleDisplayMode(.large) .fullScreenCover(isPresented: $showPaywall) { PaywallView() } @@ -803,6 +856,46 @@ struct PaywallView: View { // MARK: - Settings Row Helpers +struct SettingsSectionCard: View { + let title: String + let accent: Color + let content: Content + + init(title: String, accent: Color, @ViewBuilder content: () -> Content) { + self.title = title + self.accent = accent + self.content = content() + } + + var body: some View { + VStack(alignment: .leading, spacing: CloserSpacing.sm) { + Text(title) + .font(CloserFont.caption) + .foregroundColor(.closerTextSecondary) + .textCase(.uppercase) + + VStack(spacing: 0) { + content + } + .background(Color.white.opacity(0.58)) + .clipShape(RoundedRectangle(cornerRadius: CloserRadius.large, style: .continuous)) + } + .padding(CloserSpacing.md) + .background( + LinearGradient( + colors: [ + Color.closerSurface, + accent.opacity(0.18) + ], + startPoint: .topLeading, + endPoint: .bottomTrailing + ) + ) + .clipShape(RoundedRectangle(cornerRadius: CloserRadius.xlarge, style: .continuous)) + .closerShadow(level: .small) + } +} + struct SettingsProfileHeader: View { let initials: String let name: String @@ -829,7 +922,108 @@ struct SettingsProfileHeader: View { .multilineTextAlignment(.center) } .frame(maxWidth: .infinity) - .padding(.vertical) + .padding(CloserSpacing.lg) + .background( + LinearGradient( + colors: [ + Color.closerSurface, + Color.closerSecondary.opacity(0.14) + ], + startPoint: .topLeading, + endPoint: .bottomTrailing + ) + ) + .clipShape(RoundedRectangle(cornerRadius: CloserRadius.xlarge, style: .continuous)) + .closerShadow(level: .small) + } +} + +struct SettingsLinkRow: View { + let icon: String + let title: String + let subtitle: String + var tint: Color = .closerTextSecondary + var imageUrl: String? = nil + var isDestructive: Bool = false + var showsChevron: Bool = true + + var body: some View { + HStack(spacing: CloserSpacing.md) { + if let imageUrl, !imageUrl.isEmpty { + SettingsAvatarView( + imageUrl: imageUrl, + fallbackIcon: icon, + fallbackText: nil, + size: 40, + tint: tint + ) + } else { + SettingsIconTile(icon: icon, tint: tint, isDestructive: isDestructive) + } + + VStack(alignment: .leading, spacing: 3) { + Text(title) + .font(CloserFont.body) + .foregroundColor(isDestructive ? .closerDanger : .closerText) + .lineLimit(1) + Text(subtitle) + .font(CloserFont.caption) + .foregroundColor(.closerTextSecondary) + .lineLimit(2) + } + + Spacer(minLength: CloserSpacing.sm) + + if showsChevron { + Image(systemName: "chevron.right") + .font(.footnote.weight(.semibold)) + .foregroundColor(.closerTextSecondary.opacity(0.72)) + } + } + .padding(.horizontal, CloserSpacing.md) + .padding(.vertical, CloserSpacing.sm) + .contentShape(Rectangle()) + } +} + +struct SettingsIconTile: View { + let icon: String + let tint: Color + var isDestructive: Bool = false + + var body: some View { + ZStack { + RoundedRectangle(cornerRadius: 14, style: .continuous) + .fill(tint.opacity(isDestructive ? 0.12 : 0.14)) + Image(systemName: icon) + .font(.headline) + .foregroundColor(tint) + } + .frame(width: 40, height: 40) + .accessibilityHidden(true) + } +} + +struct SettingsDivider: View { + var body: some View { + Rectangle() + .fill(Color.closerDivider.opacity(0.8)) + .frame(height: 1) + .padding(.leading, 64) + .padding(.trailing, CloserSpacing.md) + } +} + +struct SettingsVersionRow: View { + let version: String + + var body: some View { + SettingsLinkRow( + icon: "info.circle", + title: "Version", + subtitle: version, + showsChevron: false + ) } }