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() // ── App identity ────────────────────────── VStack(spacing: 6) { Text("Kisani") .font(.system(size: 34, weight: .bold, design: .default)) .foregroundStyle(AppTheme.ink) .opacity(appeared ? 1 : 0) .offset(y: appeared ? 0 : 8) .animation(.spring(response: 0.44, dampingFraction: 0.82).delay(0.06), value: appeared) } Spacer().frame(height: 28) // ── Logo ────────────────────────────────── 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: 24) // ── Session context ─────────────────────── if let conn = connection { VStack(spacing: 5) { Text(conn.displayName) .font(.system(size: 17, weight: .semibold)) .foregroundStyle(AppTheme.ink) Text(conn.username) .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: 44) // ── 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: — Logo private var logoView: some View { ZStack { // Subtle ring so logo edge is always legible against any background RoundedRectangle(cornerRadius: 24, style: .continuous) .stroke(AppTheme.inkQuaternary, lineWidth: 0.5) .frame(width: 92, height: 92) Image("nas_logo_clean") .resizable() .scaledToFit() .frame(width: 90, height: 90) .clipShape(RoundedRectangle(cornerRadius: 22, style: .continuous)) } .shadow(color: .black.opacity(0.14), radius: 20, x: 0, y: 6) } }