Files
LabWiseiOS/LabWise/LoginView.swift

211 lines
7.9 KiB
Swift
Raw Normal View History

2026-03-20 02:05:53 -05:00
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))
2026-03-20 02:05:53 -05:00
}
// 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)
2026-03-20 02:05:53 -05:00
)
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)
2026-03-20 02:05:53 -05:00
)
}
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))
2026-03-20 02:05:53 -05:00
.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))
2026-03-20 02:05:53 -05:00
.frame(height: 1)
Text("or")
.font(.footnote)
.foregroundStyle(Color(.brandMutedForeground))
2026-03-20 02:05:53 -05:00
Rectangle()
.fill(Color(.brandPrimary).opacity(0.15))
2026-03-20 02:05:53 -05:00
.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)
2026-03-20 02:05:53 -05:00
.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)
2026-03-20 02:05:53 -05:00
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)
2026-03-20 02:05:53 -05:00
)
}
.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()
}
}
2026-03-20 02:05:53 -05:00
#Preview {
LoginView()
.environment(AppState.shared)
}