payfrit-beacon-ios/PayfritBeacon/BusinessListView.swift
John Pinkyfloyd 962a767863 Rewrite app: simplified architecture, CoreLocation beacon scanning, UI fixes
- Flatten project structure: remove Models/, Services/, ViewModels/, Views/ subdirs
- Replace APIService actor with simpler Api class, IS_DEV flag controls dev vs prod URL
- Rewrite BeaconScanner to use CoreLocation (CLBeaconRegion ranging) instead of
  CoreBluetooth — iOS blocks iBeacon data from CBCentralManager
- Add SVG logo on login page with proper scaling (was showing green square)
- Make login page scrollable, add "enter 6-digit code" OTP instruction
- Fix text input visibility (white on white) with .foregroundColor(.primary)
- Add diagonal orange DEV ribbon banner (lower-left corner), gated on Api.IS_DEV
- Update app icon: logo 10% larger, wifi icon closer
- Add en.lproj/InfoPlist.strings for display name localization
- Fix scan flash: keep isScanning=true until enrichment completes
- Add Podfile with SVGKit, Kingfisher, CocoaLumberjack dependencies

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-04 22:07:39 -08:00

142 lines
4.5 KiB
Swift

import SwiftUI
import Kingfisher
struct BusinessListView: View {
@Binding var hasAutoSelected: Bool
var onBusinessSelected: (Business) -> Void
var onLogout: () -> Void
@State private var businesses: [Business] = []
@State private var isLoading = false
@State private var errorMessage: String?
var body: some View {
NavigationStack {
VStack {
if isLoading {
Spacer()
ProgressView("Loading businesses...")
Spacer()
} else if let error = errorMessage {
Spacer()
Text(error)
.foregroundColor(.errorRed)
.multilineTextAlignment(.center)
.padding()
addBusinessButton
Spacer()
} else if businesses.isEmpty {
Spacer()
Text("No businesses found")
.foregroundColor(.secondary)
addBusinessButton
Spacer()
} else {
List(businesses) { business in
Button {
onBusinessSelected(business)
} label: {
businessRow(business)
}
.buttonStyle(.plain)
}
.listStyle(.plain)
addBusinessButton
}
}
.navigationTitle("Select Business")
.toolbar {
ToolbarItem(placement: .topBarTrailing) {
Button("Log Out") {
logout()
}
}
}
}
.onAppear {
loadBusinesses()
}
}
private func businessRow(_ business: Business) -> some View {
HStack(spacing: 12) {
if let ext = business.headerImageExtension, !ext.isEmpty {
let imageUrl = URL(string: "https://dev.payfrit.com/uploads/businesses/\(business.businessId)/header.\(ext)")
KFImage(imageUrl)
.resizable()
.placeholder {
Image(systemName: "building.2")
.font(.title2)
.foregroundColor(.secondary)
}
.scaledToFill()
.frame(width: 48, height: 48)
.clipShape(Circle())
} else {
Image(systemName: "building.2")
.font(.title2)
.foregroundColor(.secondary)
.frame(width: 48, height: 48)
}
VStack(alignment: .leading, spacing: 2) {
Text(business.name)
.font(.body.weight(.medium))
.foregroundColor(.primary)
Text("Tap to configure beacons")
.font(.caption)
.foregroundColor(.secondary)
}
Spacer()
Image(systemName: "chevron.right")
.foregroundColor(.secondary)
.font(.caption)
}
.padding(.vertical, 4)
}
private var addBusinessButton: some View {
Button {
if let url = URL(string: "https://dev.payfrit.com/portal/index.html") {
UIApplication.shared.open(url)
}
} label: {
Text("Add Business via Portal")
.font(.callout)
.foregroundColor(.payfritGreen)
}
.padding()
}
private func loadBusinesses() {
isLoading = true
errorMessage = nil
Task {
do {
let result = try await Api.shared.listBusinesses()
businesses = result
isLoading = false
if businesses.count == 1 && !hasAutoSelected {
hasAutoSelected = true
onBusinessSelected(businesses[0])
}
} catch {
isLoading = false
errorMessage = error.localizedDescription
}
}
}
private func logout() {
UserDefaults.standard.removeObject(forKey: "token")
UserDefaults.standard.removeObject(forKey: "userId")
UserDefaults.standard.removeObject(forKey: "firstName")
Api.shared.setAuthToken(nil)
onLogout()
}
}