learning_ai_clock/ios/ChronoMind/Views/Settings/SettingsView.swift
saravanakumardb1 6a41cc9f48 feat(mobile): add auth login/register flow for iOS and Android
- iOS: Add KeychainHelper.swift for secure token storage
- iOS: Add AuthService.swift (CMAuthService) with login/register/refresh/logout
- iOS: Add LoginView.swift (CMLoginView) with ChronoMind theme
- iOS: Wire auth gate in ChronoMindApp.swift (LoginView vs ContentView)
- iOS: Add Account section to SettingsView with email/plan/sign-out
- iOS: Add Cloud group + 3 files to Xcode project.pbxproj
- Android: Add AuthService.kt with Hilt @Singleton, login/register/refresh/logout
- Android: Add LoginScreen.kt with Compose login/register form
- Android: Wire auth gate in MainActivity via Hilt-injected AuthService
- Android: Add Account section to SettingsScreen via HiltViewModel
- Android: Add x-product-id header to PlatformApiClient
2026-02-28 03:22:23 -08:00

376 lines
17 KiB
Swift

// Settings View
// Preferences, categories, sounds, about
import SwiftUI
struct SettingsView: View {
@EnvironmentObject var store: TimerStore
@EnvironmentObject var notificationManager: CMNotificationManager
@ObservedObject private var cloudSync = CloudKitSyncManager.shared
@ObservedObject private var crashReporter = CrashReporter.shared
@ObservedObject private var authService = CMAuthService.shared
@AppStorage("cm_defaultUrgency") private var defaultUrgency = "standard"
@AppStorage("cm_defaultCascade") private var defaultCascade = "standard"
@AppStorage("cm_hapticEnabled") private var hapticEnabled = true
@AppStorage("cm_soundEnabled") private var soundEnabled = true
@State private var showFeedback = false
var body: some View {
NavigationStack {
ZStack {
CMColors.bg.ignoresSafeArea()
List {
// Account
Section {
if let user = authService.currentUser {
HStack {
Label("Email", systemImage: "envelope.fill")
.foregroundStyle(CMColors.text)
Spacer()
Text(user.email)
.font(CMFonts.body(size: 13))
.foregroundStyle(CMColors.textSecondary)
}
HStack {
Label("Plan", systemImage: "creditcard.fill")
.foregroundStyle(CMColors.text)
Spacer()
Text(user.plan.capitalized)
.font(CMFonts.body(size: 13))
.foregroundStyle(CMColors.textSecondary)
}
Button(role: .destructive) {
authService.logout()
} label: {
Label("Sign Out", systemImage: "rectangle.portrait.and.arrow.right")
}
}
} header: {
Text("Account")
.foregroundStyle(CMColors.textMuted)
}
.listRowBackground(CMColors.surface)
// Notifications
Section {
HStack {
Label("Notifications", systemImage: "bell.fill")
.foregroundStyle(CMColors.text)
Spacer()
if notificationManager.isAuthorized {
Text("Enabled")
.font(CMFonts.body(size: 13))
.foregroundStyle(CMColors.success)
} else {
Button("Enable") {
Task {
await notificationManager.requestPermission()
}
}
.font(CMFonts.body(size: 13, weight: .semibold))
.foregroundStyle(CMColors.accent)
}
}
Toggle(isOn: $hapticEnabled) {
Label("Haptic Feedback", systemImage: "iphone.radiowaves.left.and.right")
.foregroundStyle(CMColors.text)
}
.tint(CMColors.accent)
Toggle(isOn: $soundEnabled) {
Label("Sound", systemImage: "speaker.wave.2.fill")
.foregroundStyle(CMColors.text)
}
.tint(CMColors.accent)
} header: {
Text("Notifications & Feedback")
.foregroundStyle(CMColors.textMuted)
}
.listRowBackground(CMColors.surface)
// Defaults
Section {
Picker(selection: $defaultUrgency) {
ForEach(UrgencyLevel.allCases) { level in
HStack {
Circle()
.fill(CMColors.urgencyColor(level))
.frame(width: 8, height: 8)
Text(getUrgencyConfig(level).label)
}
.tag(level.rawValue)
}
} label: {
Label("Default Urgency", systemImage: "exclamationmark.triangle.fill")
.foregroundStyle(CMColors.text)
}
Picker(selection: $defaultCascade) {
ForEach(CascadePreset.allCases.filter { $0 != .custom }) { preset in
Text(preset.label).tag(preset.rawValue)
}
} label: {
Label("Default Cascade", systemImage: "arrow.down.forward.and.arrow.up.backward")
.foregroundStyle(CMColors.text)
}
} header: {
Text("Defaults")
.foregroundStyle(CMColors.textMuted)
}
.listRowBackground(CMColors.surface)
// Data
Section {
HStack {
Label("Total Timers", systemImage: "number")
.foregroundStyle(CMColors.text)
Spacer()
Text("\(store.timers.count)")
.font(CMFonts.mono(size: 14))
.foregroundStyle(CMColors.textSecondary)
}
Button {
store.timers.removeAll { [.completed, .dismissed].contains($0.state) }
} label: {
Label("Clear History", systemImage: "trash")
.foregroundStyle(CMColors.error)
}
Button {
store.timers.removeAll()
CMNotificationManager.shared.removeAllNotifications()
} label: {
Label("Delete All Timers", systemImage: "trash.fill")
.foregroundStyle(CMColors.error)
}
} header: {
Text("Data")
.foregroundStyle(CMColors.textMuted)
}
.listRowBackground(CMColors.surface)
// iCloud Sync
Section {
Toggle(isOn: $cloudSync.syncEnabled) {
Label("iCloud Sync", systemImage: "icloud.fill")
.foregroundStyle(CMColors.text)
}
.tint(CMColors.accent)
if cloudSync.syncEnabled {
HStack {
Label("Last Synced", systemImage: "arrow.triangle.2.circlepath")
.foregroundStyle(CMColors.text)
Spacer()
if let date = cloudSync.lastSyncDate {
Text(formatRelativeTime(date, now: Date()))
.font(CMFonts.body(size: 13))
.foregroundStyle(CMColors.textMuted)
} else {
Text("Never")
.font(CMFonts.body(size: 13))
.foregroundStyle(CMColors.textMuted)
}
}
}
} header: {
Text("Sync")
.foregroundStyle(CMColors.textMuted)
}
.listRowBackground(CMColors.surface)
// Feedback & Diagnostics
Section {
Button {
showFeedback = true
} label: {
Label("Send Feedback", systemImage: "envelope.fill")
.foregroundStyle(CMColors.accent)
}
HStack {
Label("Crash Reports", systemImage: "exclamationmark.triangle")
.foregroundStyle(CMColors.text)
Spacer()
Text("\(crashReporter.diagnosticCount)")
.font(CMFonts.mono(size: 13))
.foregroundStyle(CMColors.textMuted)
}
} header: {
Text("Feedback & Diagnostics")
.foregroundStyle(CMColors.textMuted)
}
.listRowBackground(CMColors.surface)
// About
Section {
HStack {
Label("Version", systemImage: "info.circle")
.foregroundStyle(CMColors.text)
Spacer()
Text("1.0.0 (Phase 3)")
.font(CMFonts.body(size: 13))
.foregroundStyle(CMColors.textMuted)
}
HStack {
Label("Product ID", systemImage: "barcode")
.foregroundStyle(CMColors.text)
Spacer()
Text("chronomind")
.font(CMFonts.mono(size: 13))
.foregroundStyle(CMColors.textMuted)
}
Link(destination: URL(string: "https://chronomind.app/privacy")!) {
Label("Privacy Policy", systemImage: "hand.raised.fill")
.foregroundStyle(CMColors.text)
}
} header: {
Text("About")
.foregroundStyle(CMColors.textMuted)
}
.listRowBackground(CMColors.surface)
}
.scrollContentBackground(.hidden)
.listStyle(.insetGrouped)
}
.navigationTitle("Settings")
.navigationBarTitleDisplayMode(.inline)
.toolbarBackground(CMColors.surface, for: .navigationBar)
.toolbarColorScheme(.dark, for: .navigationBar)
.sheet(isPresented: $showFeedback) {
FeedbackSheet()
}
}
}
}
// MARK: - Feedback Sheet
struct FeedbackSheet: View {
@Environment(\.dismiss) private var dismiss
@State private var feedbackType: FeedbackType = .bug
@State private var feedbackText = ""
@State private var includeDeviceInfo = true
@State private var submitted = false
enum FeedbackType: String, CaseIterable, Identifiable {
case bug = "Bug Report"
case feature = "Feature Request"
case general = "General Feedback"
var id: String { rawValue }
}
var body: some View {
NavigationStack {
ZStack {
CMColors.bg.ignoresSafeArea()
if submitted {
VStack(spacing: CMSpacing.lg) {
Image(systemName: "checkmark.circle.fill")
.font(.system(size: 60))
.foregroundStyle(CMColors.success)
Text("Thanks for your feedback!")
.font(CMFonts.display(size: 20))
.foregroundStyle(CMColors.text)
Text("We'll review it and get back to you.")
.font(CMFonts.body(size: 14))
.foregroundStyle(CMColors.textSecondary)
}
} else {
List {
Section {
Picker("Type", selection: $feedbackType) {
ForEach(FeedbackType.allCases) { type in
Text(type.rawValue).tag(type)
}
}
.pickerStyle(.segmented)
}
.listRowBackground(CMColors.surface)
Section {
TextEditor(text: $feedbackText)
.frame(minHeight: 150)
.scrollContentBackground(.hidden)
.foregroundStyle(CMColors.text)
} header: {
Text("Describe your feedback")
.foregroundStyle(CMColors.textMuted)
}
.listRowBackground(CMColors.surface)
Section {
Toggle(isOn: $includeDeviceInfo) {
VStack(alignment: .leading, spacing: CMSpacing.xxs) {
Text("Include device info")
.foregroundStyle(CMColors.text)
Text("iOS version, device model, app version")
.font(CMFonts.body(size: 11))
.foregroundStyle(CMColors.textMuted)
}
}
.tint(CMColors.accent)
}
.listRowBackground(CMColors.surface)
Section {
Button {
submitFeedback()
} label: {
HStack {
Spacer()
Text("Submit Feedback")
.font(CMFonts.body(size: 16, weight: .semibold))
.foregroundStyle(.white)
Spacer()
}
.padding(.vertical, CMSpacing.sm)
.background(feedbackText.isEmpty ? CMColors.textMuted : CMColors.accent)
.clipShape(RoundedRectangle(cornerRadius: CMRadius.sm))
}
.disabled(feedbackText.isEmpty)
}
.listRowBackground(Color.clear)
}
.scrollContentBackground(.hidden)
.listStyle(.insetGrouped)
}
}
.navigationTitle("Send Feedback")
.navigationBarTitleDisplayMode(.inline)
.toolbarBackground(CMColors.surface, for: .navigationBar)
.toolbarColorScheme(.dark, for: .navigationBar)
.toolbar {
ToolbarItem(placement: .cancellationAction) {
Button(submitted ? "Done" : "Cancel") { dismiss() }
.foregroundStyle(CMColors.textSecondary)
}
}
}
}
private func submitFeedback() {
// Store feedback locally (send to server in production)
let feedback: [String: Any] = [
"type": feedbackType.rawValue,
"text": feedbackText,
"date": Date().timeIntervalSince1970,
"includeDeviceInfo": includeDeviceInfo,
]
var stored = UserDefaults.standard.array(forKey: "chronomind-feedback") as? [[String: Any]] ?? []
stored.append(feedback)
UserDefaults.standard.set(stored, forKey: "chronomind-feedback")
HapticEngine.tap()
withAnimation { submitted = true }
}
}