import SwiftUI import LocalAuthentication import SVGKit struct LoginView: View { var onLoginSuccess: (String, Int) -> Void @State private var otpUuid: String? @State private var phone = "" @State private var otp = "" @State private var isLoading = false @State private var errorMessage: String? @State private var showPhoneInput = false @State private var showOtpInput = false @State private var hasCheckedAuth = false var body: some View { ScrollView { VStack(spacing: 24) { Spacer(minLength: 80) // SVG Logo SVGLogoView(width: 200) .frame(width: 200, height: 117) Text("Payfrit Beacon") .font(.title2.bold()) .foregroundColor(.payfritGreen) if isLoading { ProgressView() .padding() } if let error = errorMessage { Text(error) .foregroundColor(.errorRed) .font(.callout) .multilineTextAlignment(.center) .padding(.horizontal) } if showPhoneInput { VStack(spacing: 12) { TextField("Phone number", text: $phone) .keyboardType(.phonePad) .textContentType(.telephoneNumber) .foregroundColor(.primary) .padding() .background(Color(.systemGray6)) .cornerRadius(8) .padding(.horizontal) Button(action: sendOtp) { Text("Send Code") .frame(maxWidth: .infinity) .padding() .background(Color.payfritGreen) .foregroundColor(.white) .cornerRadius(8) } .disabled(isLoading) .padding(.horizontal) } } if showOtpInput { VStack(spacing: 12) { Text("Please enter the 6-digit code sent to your phone") .font(.callout) .foregroundColor(.secondary) .multilineTextAlignment(.center) .padding(.horizontal) TextField("Verification code", text: $otp) .keyboardType(.numberPad) .textContentType(.oneTimeCode) .foregroundColor(.primary) .padding() .background(Color(.systemGray6)) .cornerRadius(8) .padding(.horizontal) Button(action: verifyOtp) { Text("Verify") .frame(maxWidth: .infinity) .padding() .background(Color.payfritGreen) .foregroundColor(.white) .cornerRadius(8) } .disabled(isLoading) .padding(.horizontal) } } Spacer(minLength: 40) } .frame(maxWidth: .infinity) } .onAppear { guard !hasCheckedAuth else { return } hasCheckedAuth = true checkSavedAuth() } } private func checkSavedAuth() { let defaults = UserDefaults.standard let savedToken = defaults.string(forKey: "token") let savedUserId = defaults.integer(forKey: "userId") if let token = savedToken, !token.isEmpty, savedUserId > 0 { if canUseBiometrics() { showBiometricPrompt(token: token, userId: savedUserId) } else { loginSuccess(token: token, userId: savedUserId) } } else { showPhoneInput = true } } private func canUseBiometrics() -> Bool { let context = LAContext() var error: NSError? return context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error) } private func showBiometricPrompt(token: String, userId: Int) { let context = LAContext() context.evaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, localizedReason: "Sign in to Payfrit Beacon") { success, _ in DispatchQueue.main.async { if success { loginSuccess(token: token, userId: userId) } else { showPhoneInput = true } } } } private func sendOtp() { let trimmed = phone.trimmingCharacters(in: .whitespacesAndNewlines) guard trimmed.count >= 10 else { errorMessage = "Enter a valid phone number" return } isLoading = true errorMessage = nil Task { do { let response = try await Api.shared.sendLoginOtp(phone: trimmed) otpUuid = response.uuid showPhoneInput = false showOtpInput = true isLoading = false } catch { errorMessage = error.localizedDescription isLoading = false } } } private func verifyOtp() { guard let uuid = otpUuid else { return } let trimmed = otp.trimmingCharacters(in: .whitespacesAndNewlines) guard trimmed.count >= 4 else { errorMessage = "Enter the verification code" return } isLoading = true errorMessage = nil Task { do { let response = try await Api.shared.verifyLoginOtp(uuid: uuid, otp: trimmed) let defaults = UserDefaults.standard defaults.set(response.token, forKey: "token") defaults.set(response.userId, forKey: "userId") defaults.set(response.userFirstName, forKey: "firstName") loginSuccess(token: response.token, userId: response.userId) } catch { errorMessage = error.localizedDescription isLoading = false } } } private func loginSuccess(token: String, userId: Int) { Api.shared.setAuthToken(token) onLoginSuccess(token, userId) } } // MARK: - SVG Logo UIKit wrapper struct SVGLogoView: UIViewRepresentable { var width: CGFloat = 200 func makeUIView(context: Context) -> UIView { let container = UIView() container.clipsToBounds = true let svgPath = Bundle.main.path(forResource: "payfrit-favicon-light-outlines", ofType: "svg") ?? "" guard let svgImage = SVGKImage(contentsOfFile: svgPath) else { return container } // Scale SVG proportionally based on width let nativeW = svgImage.size.width let nativeH = svgImage.size.height let scale = width / nativeW svgImage.size = CGSize(width: width, height: nativeH * scale) let imageView = SVGKLayeredImageView(svgkImage: svgImage) ?? UIImageView() imageView.translatesAutoresizingMaskIntoConstraints = false container.addSubview(imageView) NSLayoutConstraint.activate([ imageView.widthAnchor.constraint(equalToConstant: svgImage.size.width), imageView.heightAnchor.constraint(equalToConstant: svgImage.size.height), imageView.centerXAnchor.constraint(equalTo: container.centerXAnchor), imageView.centerYAnchor.constraint(equalTo: container.centerYAnchor), ]) return container } func updateUIView(_ uiView: UIView, context: Context) {} }