import SwiftUI import AuthenticationServices import LabWiseKit struct LoginView: View { @Environment(AppState.self) private var appState @State private var email = "" @State private var password = "" @State private var isLoading = false @State private var isGoogleLoading = false @State private var errorMessage: String? private let authClient = AuthClient() var body: some View { NavigationStack { ZStack { Color(UIColor.systemGroupedBackground) .ignoresSafeArea() VStack(spacing: 32) { Spacer() // Logo VStack(spacing: 16) { Image("Logo") .resizable() .scaledToFit() .frame(height: 52) Text("Chemical Inventory Management") .font(.subheadline) .foregroundStyle(Color(.brandMutedForeground)) } // Card VStack(spacing: 20) { VStack(spacing: 12) { TextField("Email", text: $email) .keyboardType(.emailAddress) .textContentType(.emailAddress) .autocapitalization(.none) .padding(.horizontal, 12) .padding(.vertical, 10) .background(Color(UIColor.secondarySystemGroupedBackground)) .clipShape(RoundedRectangle(cornerRadius: 10)) .overlay( RoundedRectangle(cornerRadius: 10) .strokeBorder(Color(.brandPrimary).opacity(0.2), lineWidth: 1) ) SecureField("Password", text: $password) .textContentType(.password) .padding(.horizontal, 12) .padding(.vertical, 10) .background(Color(UIColor.secondarySystemGroupedBackground)) .clipShape(RoundedRectangle(cornerRadius: 10)) .overlay( RoundedRectangle(cornerRadius: 10) .strokeBorder(Color(.brandPrimary).opacity(0.2), lineWidth: 1) ) } if let errorMessage { Text(errorMessage) .foregroundStyle(.red) .font(.footnote) .multilineTextAlignment(.center) } Button { Task { await signIn() } } label: { Group { if isLoading { ProgressView() .tint(.white) } else { Text("Sign In") .fontWeight(.semibold) } } .frame(maxWidth: .infinity) .frame(height: 44) } .background(Color(.brandPrimary)) .foregroundStyle(.white) .clipShape(RoundedRectangle(cornerRadius: 10)) .disabled(email.isEmpty || password.isEmpty || isLoading || isGoogleLoading) .opacity((email.isEmpty || password.isEmpty || isLoading || isGoogleLoading) ? 0.5 : 1) HStack { Rectangle() .fill(Color(.brandPrimary).opacity(0.15)) .frame(height: 1) Text("or") .font(.footnote) .foregroundStyle(Color(.brandMutedForeground)) Rectangle() .fill(Color(.brandPrimary).opacity(0.15)) .frame(height: 1) } GoogleSignInButton(isLoading: isGoogleLoading) { Task { await signInWithGoogle() } } .disabled(isLoading || isGoogleLoading) } .padding(24) .background(Color(UIColor.secondarySystemGroupedBackground)) .clipShape(RoundedRectangle(cornerRadius: 16)) .shadow(color: Color(.brandPrimary).opacity(0.06), radius: 12, x: 0, y: 4) .padding(.horizontal, 24) Spacer() } } .navigationBarHidden(true) } } // MARK: - Actions private func signIn() async { isLoading = true errorMessage = nil defer { isLoading = false } do { let user = try await authClient.signIn(email: email, password: password) await MainActor.run { appState.signedIn(user: user) } } catch APIError.httpError(let code, _) where code == 401 { await MainActor.run { errorMessage = "Invalid email or password." } } catch APIError.httpError(_, let data) { let msg = String(data: data, encoding: .utf8) ?? "Sign in failed." await MainActor.run { errorMessage = msg } } catch { await MainActor.run { errorMessage = "Sign in failed. Please check your connection." } } } @MainActor private func signInWithGoogle() async { guard let window = UIApplication.shared.connectedScenes .compactMap({ $0 as? UIWindowScene }) .flatMap({ $0.windows }) .first(where: { $0.isKeyWindow }) else { return } isGoogleLoading = true errorMessage = nil defer { isGoogleLoading = false } do { let user = try await authClient.signInWithGoogle(presentingWindow: window) appState.signedIn(user: user) } catch ASWebAuthenticationSessionError.canceledLogin { // User cancelled — no error shown } catch { errorMessage = "Google sign-in failed. Please try again." } } } // MARK: - Google button struct GoogleSignInButton: View { let isLoading: Bool let action: () -> Void var body: some View { Button(action: action) { HStack(spacing: 10) { if isLoading { ProgressView() .frame(maxWidth: .infinity) } else { GoogleGLogo() .frame(width: 18, height: 18) Text("Continue with Google") .fontWeight(.medium) .foregroundStyle(Color(UIColor.label)) } } .frame(maxWidth: .infinity) .frame(height: 44) .background(Color(UIColor.systemBackground)) .clipShape(RoundedRectangle(cornerRadius: 10)) .overlay( RoundedRectangle(cornerRadius: 10) .strokeBorder(Color(.brandPrimary).opacity(0.25), lineWidth: 1) ) } .buttonStyle(.plain) } } /// The four-colour Google "G" logo, matching the web app implementation. struct GoogleGLogo: View { var body: some View { Image("GoogleLogo") .resizable() .renderingMode(.original) .scaledToFit() } } #Preview { LoginView() .environment(AppState.shared) }