import SwiftUI struct LoginView: View { @EnvironmentObject var store: ConnectionStore @StateObject private var vm = LoginViewModel() @State private var appeared = false private var connection: NASConnection? { store.savedConnection } var body: some View { ZStack { AppTheme.background.ignoresSafeArea() VStack(spacing: 0) { Spacer() // ── Brand + logo ────────────────────────── VStack(spacing: 20) { brandMark .opacity(appeared ? 1 : 0) .offset(y: appeared ? 0 : 10) .animation(.spring(response: 0.44, dampingFraction: 0.82).delay(0.06), value: appeared) logoView .scaleEffect(appeared ? 1 : 0.88) .opacity(appeared ? 1 : 0) .animation(.spring(response: 0.48, dampingFraction: 0.74).delay(0.12), value: appeared) } Spacer().frame(height: 32) // ── Session context ─────────────────────── if let conn = connection { VStack(spacing: 4) { Text(conn.username) .font(.system(size: 17, weight: .semibold)) .foregroundStyle(AppTheme.ink) Text(conn.displayName) .font(.system(size: 14, weight: .regular)) .foregroundStyle(AppTheme.inkTertiary) } .opacity(appeared ? 1 : 0) .offset(y: appeared ? 0 : 6) .animation(.spring(response: 0.4, dampingFraction: 0.8).delay(0.18), value: appeared) } Spacer().frame(height: 40) // ── Auth ────────────────────────────────── VStack(spacing: 14) { Button(action: { vm.authenticateWithFaceID() }) { HStack(spacing: 9) { if vm.isAuthenticating { ProgressView() .tint(AppTheme.inkInverse) .scaleEffect(0.85) } else { Image(systemName: "faceid") .font(.system(size: 18, weight: .medium)) Text("Sign in with Face ID") .font(.system(size: 15, weight: .semibold)) } } } .buttonStyle(PrimaryButtonStyle()) .padding(.horizontal, AppTheme.hPad) .disabled(vm.isAuthenticating) .opacity(appeared ? 1 : 0) .animation(.spring(response: 0.4, dampingFraction: 0.8).delay(0.24), value: appeared) if let error = vm.authError { HStack(spacing: 5) { Image(systemName: "exclamationmark.circle") .font(.system(size: 12, weight: .regular)) Text(error) .font(AppTheme.caption()) } .foregroundStyle(AppTheme.destructive) .transition(.asymmetric( insertion: .opacity.combined(with: .move(edge: .top)), removal: .opacity )) } } .animation(.spring(response: 0.3, dampingFraction: 0.85), value: vm.authError != nil) Spacer() // ── Switch user ─────────────────────────── Button(action: { store.signOut() }) { Text("Switch User") .font(.system(size: 13, weight: .regular)) .foregroundStyle(AppTheme.inkTertiary) } .buttonStyle(ScaleButtonStyle()) .padding(.bottom, 52) .opacity(appeared ? 1 : 0) .animation(.spring(response: 0.4, dampingFraction: 0.8).delay(0.32), value: appeared) } } .onAppear { vm.onAuthenticated = { store.isSessionActive = true } appeared = true } } // MARK: — Brand mark private var brandMark: some View { Text("kisani.") .font(.system(size: 13, weight: .medium, design: .monospaced)) .foregroundStyle(AppTheme.inkTertiary) .kerning(1.2) } // MARK: — Logo private var logoView: some View { ZStack { RoundedRectangle(cornerRadius: 24, style: .continuous) .stroke(AppTheme.inkQuaternary, lineWidth: 0.5) .frame(width: 92, height: 92) Image("ugreen_logo") .resizable() .scaledToFit() .frame(width: 90, height: 90) .clipShape(RoundedRectangle(cornerRadius: 22, style: .continuous)) } .shadow(color: .black.opacity(0.12), radius: 20, x: 0, y: 6) } }