payfrit-beacon-ios/PayfritBeacon/Views/LoginView.swift
Schwifty 09db3e8ec7 fix: resolve ambiguous color references by removing ShapeStyle extension
The ShapeStyle where Self == Color extension duplicated Color statics,
causing 'ambiguous use' errors in foregroundStyle/stroke/tint/background
contexts. Removed the extension entirely and use explicit Color.xxx prefix
at all call sites instead.
2026-03-22 19:40:51 +00:00

141 lines
4.3 KiB
Swift

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
}
}
}