import SwiftUI import LocalAuthentication /// OTP Login screen + biometric auth (matches Android LoginActivity) struct LoginView: View { @EnvironmentObject var appState: AppState @State private var phone = "" @State private var otpCode = "" @State private var otpUUID = "" @State private var showOTPField = false @State private var isLoading = false @State private var errorMessage: String? var body: some View { NavigationStack { VStack(spacing: 32) { Spacer() // Logo Image(systemName: "antenna.radiowaves.left.and.right") .font(.system(size: 64)) .foregroundStyle(Color.payfritGreen) Text("Payfrit Beacon") .font(.title.bold()) Text("Provision beacons for your business") .font(.subheadline) .foregroundStyle(.secondary) // Phone input VStack(spacing: 16) { TextField("Phone number", text: $phone) .keyboardType(.phonePad) .textContentType(.telephoneNumber) .padding() .background(.quaternary, in: RoundedRectangle(cornerRadius: 12)) .disabled(showOTPField) if showOTPField { TextField("Enter OTP code", text: $otpCode) .keyboardType(.numberPad) .textContentType(.oneTimeCode) .padding() .background(.quaternary, in: RoundedRectangle(cornerRadius: 12)) } if let error = errorMessage { Text(error) .font(.caption) .foregroundStyle(Color.errorRed) } Button(action: handleAction) { if isLoading { ProgressView() .tint(.white) } else { Text(showOTPField ? "Verify OTP" : "Send OTP") } } .buttonStyle(.borderedProminent) .controlSize(.large) .disabled(isLoading || (showOTPField ? otpCode.isEmpty : phone.isEmpty)) } .padding(.horizontal, 32) Spacer() Spacer() } .navigationBarTitleDisplayMode(.inline) } .task { // Try biometric login on appear await tryBiometricLogin() } } private func handleAction() { Task { if showOTPField { await verifyOTP() } else { await sendOTP() } } } private func sendOTP() async { isLoading = true errorMessage = nil do { let uuid = try await APIClient.shared.sendOTP(phone: phone) otpUUID = uuid showOTPField = true } catch { errorMessage = error.localizedDescription } isLoading = false } private func verifyOTP() async { isLoading = true errorMessage = nil do { let result = try await APIClient.shared.verifyOTP(uuid: otpUUID, code: otpCode) appState.didLogin(token: result.token, userId: result.userId) } catch { errorMessage = error.localizedDescription } isLoading = false } private func tryBiometricLogin() async { guard let session = SecureStorage.loadSession() else { return } let context = LAContext() var authError: NSError? guard context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &authError) else { return } do { let success = try await context.evaluatePolicy( .deviceOwnerAuthenticationWithBiometrics, localizedReason: "Log in to Payfrit Beacon" ) if success { appState.didLogin(token: session.token, userId: session.userId) } } catch { // Biometric failed — show manual login } } }