payfrit-beacon-ios/PayfritBeacon/Views/LoginView.swift
Schwifty 8ecd533429 fix: replace generic icon with beacon-specific icon and match Android color scheme
- App icon now uses white bg + PAYFRIT text + bluetooth beacon icon (matches Android)
- AccentColor set to Payfrit Green (#22B24B)
- Added BrandColors.swift with full Android color palette parity
- All views updated: payfritGreen replaces .blue, proper status colors throughout
- Signal strength, beacon type badges, QR scanner corners all use brand colors
- DevBanner uses warningOrange matching Android's #FF9800

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 19:29:35 +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(.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(.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
}
}
}