learning_ai_clock/ios/ChronoMind/Views/Settings/LoginView.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

147 lines
5.8 KiB
Swift

// Login / Register View
// Authentication form for ChronoMind via platform-service.
import SwiftUI
struct CMLoginView: View {
@ObservedObject var authService = CMAuthService.shared
@State private var isRegister = false
@State private var name = ""
@State private var email = ""
@State private var password = ""
private var isLoading: Bool {
if case .loading = authService.state { return true }
return false
}
private var errorMessage: String? {
if case .error(let msg) = authService.state { return msg }
return nil
}
private var isValidEmail: Bool {
let p = "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}"
return email.range(of: p, options: .regularExpression) != nil
}
private var isValidPassword: Bool {
password.count >= 8
&& password.rangeOfCharacter(from: .uppercaseLetters) != nil
&& password.rangeOfCharacter(from: .lowercaseLetters) != nil
&& password.rangeOfCharacter(from: .decimalDigits) != nil
}
private var isValid: Bool {
isValidEmail && isValidPassword && (!isRegister || !name.trimmingCharacters(in: .whitespaces).isEmpty)
}
var body: some View {
ZStack {
CMColors.bg.ignoresSafeArea()
ScrollView {
VStack(spacing: 24) {
Spacer(minLength: 60)
Image(systemName: "clock.badge.checkmark")
.font(.system(size: 48))
.foregroundColor(CMColors.accent)
Text("ChronoMind")
.font(CMFonts.display(size: 28))
.foregroundColor(CMColors.text)
Text(isRegister ? "Create your account" : "Sign in to your account")
.font(CMFonts.body(size: 15))
.foregroundColor(CMColors.textSecondary)
VStack(spacing: 14) {
if isRegister {
TextField("Full Name", text: $name)
.textContentType(.name)
.autocapitalization(.words)
.padding(14)
.background(CMColors.surface)
.cornerRadius(CMRadius.md)
.foregroundColor(CMColors.text)
}
TextField("Email", text: $email)
.textContentType(.emailAddress)
.keyboardType(.emailAddress)
.autocapitalization(.none)
.disableAutocorrection(true)
.padding(14)
.background(CMColors.surface)
.cornerRadius(CMRadius.md)
.foregroundColor(CMColors.text)
SecureField("Password", text: $password)
.textContentType(isRegister ? .newPassword : .password)
.padding(14)
.background(CMColors.surface)
.cornerRadius(CMRadius.md)
.foregroundColor(CMColors.text)
if !password.isEmpty && isRegister && !isValidPassword {
Text("Password needs: 8+ chars, uppercase, lowercase, digit")
.font(.caption)
.foregroundColor(CMColors.warning)
}
}
.padding(.horizontal, 24)
if let errorMessage {
Text(errorMessage)
.font(.caption)
.foregroundColor(CMColors.error)
.padding(.horizontal, 24)
}
Button {
Task {
if isRegister {
await authService.register(name: name, email: email, password: password)
} else {
await authService.login(email: email, password: password)
}
}
} label: {
if isLoading {
ProgressView()
.tint(.white)
.frame(maxWidth: .infinity)
.frame(height: 48)
} else {
Text(isRegister ? "Create Account" : "Sign In")
.fontWeight(.semibold)
.frame(maxWidth: .infinity)
.frame(height: 48)
}
}
.buttonStyle(.borderedProminent)
.tint(CMColors.accent)
.disabled(!isValid || isLoading)
.padding(.horizontal, 24)
HStack {
Text(isRegister ? "Already have an account?" : "Don't have an account?")
.font(.caption)
.foregroundColor(CMColors.textSecondary)
Button(isRegister ? "Sign In" : "Register") {
withAnimation { isRegister.toggle() }
}
.font(.caption)
.foregroundColor(CMColors.accent)
}
Spacer(minLength: 40)
}
}
}
.preferredColorScheme(.dark)
}
}