import SwiftUI struct LoginView: View { @Bindable var viewModel: AuthViewModel var appState: AppState @State private var showPairing = false @State private var didAttemptBiometric = false var body: some View { NavigationStack { ScrollView { VStack(spacing: 20) { Image(systemName: "lock.shield.fill") .font(.system(size: 60)) .foregroundStyle(.blue) .padding(.top, 40) Text("Encrypted Chat") .font(.largeTitle.bold()) Text("End-to-end encrypted messaging") .font(.subheadline) .foregroundStyle(.secondary) VStack(spacing: 16) { // Server config DisclosureGroup("Server") { TextField("Host", text: $viewModel.serverHost) .textContentType(.URL) .autocapitalization(.none) TextField("Port", text: $viewModel.serverPort) .keyboardType(.numberPad) } .padding(.horizontal) if viewModel.mode == .register { TextField("Username", text: $viewModel.username) .textContentType(.username) .autocapitalization(.none) .textFieldStyle(.roundedBorder) } TextField("Email", text: $viewModel.email) .textContentType(.emailAddress) .keyboardType(.emailAddress) .autocapitalization(.none) .textFieldStyle(.roundedBorder) SecureField("Password", text: $viewModel.password) .textContentType(viewModel.mode == .login ? .password : .oneTimeCode) .autocorrectionDisabled() .textInputAutocapitalization(.never) .textFieldStyle(.roundedBorder) if viewModel.mode == .register { SecureField("Confirm Password", text: $viewModel.confirmPassword) .textContentType(.oneTimeCode) .autocorrectionDisabled() .textInputAutocapitalization(.never) .textFieldStyle(.roundedBorder) } if let error = viewModel.errorMessage { Text(error) .foregroundStyle(.red) .font(.caption) .multilineTextAlignment(.center) } Button(action: { Task { if viewModel.mode == .login { await viewModel.login(appState: appState) } else { await viewModel.register(appState: appState) } } }) { if viewModel.isLoading { ProgressView() .frame(maxWidth: .infinity) } else { Text(viewModel.mode == .login ? "Login" : "Register") .frame(maxWidth: .infinity) } } .buttonStyle(.borderedProminent) .disabled(viewModel.isLoading) Button(viewModel.mode == .login ? "Don't have an account? Register" : "Already have an account? Login") { viewModel.mode = viewModel.mode == .login ? .register : .login viewModel.errorMessage = nil } .font(.caption) if viewModel.hasSavedCredentials && viewModel.mode == .login { Divider() .padding(.vertical, 4) Button { Task { await viewModel.biometricLogin(appState: appState) } } label: { if viewModel.isBiometricLoading { ProgressView() .frame(maxWidth: .infinity) } else { Label("Sign in with Face ID", systemImage: "faceid") .frame(maxWidth: .infinity) } } .buttonStyle(.bordered) .disabled(viewModel.isLoading || viewModel.isBiometricLoading) } Divider() .padding(.vertical, 4) Button("Pair from existing device") { showPairing = true } .font(.caption) } .padding(.horizontal, 32) } } .task { viewModel.checkSavedCredentials() if viewModel.hasSavedCredentials && !didAttemptBiometric { didAttemptBiometric = true await viewModel.biometricLogin(appState: appState) } } .sheet(isPresented: $viewModel.showConfirmation) { ConfirmationSheet(viewModel: viewModel, appState: appState) } .sheet(isPresented: $showPairing) { PairingView(appState: appState, authViewModel: viewModel) } } } } struct ConfirmationSheet: View { @Bindable var viewModel: AuthViewModel var appState: AppState var body: some View { VStack(spacing: 20) { Text("Confirm Registration") .font(.title2.bold()) if let msg = viewModel.registrationMessage { Text(msg) .font(.subheadline) .foregroundStyle(.secondary) .multilineTextAlignment(.center) } TextField("Confirmation Code", text: $viewModel.confirmationCode) .textFieldStyle(.roundedBorder) .keyboardType(.numberPad) if let error = viewModel.errorMessage { Text(error) .foregroundStyle(.red) .font(.caption) } Button("Confirm") { Task { await viewModel.confirmRegistration(appState: appState) } } .buttonStyle(.borderedProminent) .disabled(viewModel.isLoading) } .padding(32) } }