import SwiftUI struct AboutScreen: View { @Environment(\.dismiss) private var dismiss @State private var appearAnimation = false @State private var aboutInfo: AboutInfo? @State private var isLoading = true @State private var error: String? // Fallback content if API fails private let fallbackInfo = AboutInfo( description: "Payfrit Works helps you manage tasks, accept cash payments, and earn money. Get notified of new tasks, complete them efficiently, and track your earnings.", features: [ AboutFeature(icon: "checkmark.circle", title: "Claim Tasks", description: "View available tasks from businesses you work for and claim them with one tap."), AboutFeature(icon: "dollarsign.circle", title: "Accept Cash", description: "Collect cash payments from customers with automatic change calculation."), AboutFeature(icon: "antenna.radiowaves.left.and.right", title: "Beacon Auto-Complete", description: "Tasks complete automatically when you're near the customer's table beacon."), AboutFeature(icon: "creditcard", title: "Earn & Get Paid", description: "Track your earnings in real-time and receive payouts to your bank account.") ], contacts: [ AboutContact(icon: "globe", label: "Website", url: "https://www.payfrit.com") ], copyright: "© 2026 Payfrit. All rights reserved." ) private var displayInfo: AboutInfo { aboutInfo ?? fallbackInfo } var body: some View { ScrollView { VStack(spacing: 20) { // Logo + version VStack(spacing: 8) { Image("PayfritLogoLight") .resizable() .aspectRatio(contentMode: .fit) .frame(width: 80, height: 80) Text("Payfrit Works") .font(.subheadline) .fontWeight(.bold) .foregroundColor(.primary) Text("Version \(appVersion)") .font(.caption) .foregroundColor(.secondary) } .frame(maxWidth: .infinity) .padding(.top, 16) .scaleEffect(appearAnimation ? 1 : 0.9) .opacity(appearAnimation ? 1 : 0) if isLoading { ProgressView() .padding(.vertical, 20) } else { // Description if !displayInfo.description.isEmpty { Text(displayInfo.description) .font(.caption) .foregroundColor(.secondary) .multilineTextAlignment(.leading) .padding(.horizontal, 16) .opacity(appearAnimation ? 1 : 0) .offset(y: appearAnimation ? 0 : 15) } // Feature cards VStack(spacing: 12) { ForEach(Array(displayInfo.features.enumerated()), id: \.offset) { index, feature in featureCard( icon: mapIconName(feature.icon), title: feature.title, description: feature.description ) .staggeredAppear(index: index, appeared: appearAnimation) } } .padding(.horizontal, 16) // Contact links if !displayInfo.contacts.isEmpty { VStack(spacing: 8) { ForEach(Array(displayInfo.contacts.enumerated()), id: \.offset) { index, contact in if let url = URL(string: contact.url) { Link(destination: url) { contactRow(contact: contact) } } } } .padding(.horizontal, 16) .staggeredAppear(index: displayInfo.features.count, appeared: appearAnimation) } // Copyright if !displayInfo.copyright.isEmpty { Text(displayInfo.copyright) .font(.caption2) .foregroundColor(.secondary) .padding(.top, 4) } } Spacer() .frame(height: 20) } } .background(Color(.systemGroupedBackground).ignoresSafeArea()) .navigationTitle("About Payfrit") .navigationBarTitleDisplayMode(.inline) .task { await loadAboutInfo() } .onAppear { withAnimation(.spring(response: 0.5, dampingFraction: 0.8)) { appearAnimation = true } } } private var appVersion: String { Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "1.0.0" } private func loadAboutInfo() async { do { aboutInfo = try await APIService.shared.getAboutInfo() } catch { // Use fallback on error if IS_DEV { print("Failed to load about info: \(error)") } } isLoading = false } /// Map icon names from API to SF Symbols private func mapIconName(_ apiIcon: String) -> String { // If it's already an SF Symbol name, use it if UIImage(systemName: apiIcon) != nil { return apiIcon } // Map common icon names switch apiIcon.lowercased() { case "beacon", "radar", "walkup": return "wave.3.right" case "group", "friends", "people": return "person.3" case "delivery", "bag", "takeaway": return "bag" case "payment", "card", "credit": return "creditcard" case "globe", "web", "website": return "globe" case "email", "mail": return "envelope" case "phone", "call": return "phone" case "chat", "message": return "message" case "task", "tasks", "check": return "checkmark.circle" case "cash", "money", "dollar": return "dollarsign.circle" case "earn", "payout": return "creditcard" default: return "star" } } private func featureCard(icon: String, title: String, description: String) -> some View { HStack(spacing: 10) { Image(systemName: icon) .font(.subheadline) .foregroundColor(.payfritGreen) .frame(width: 28, height: 28) .background(Color.payfritGreen.opacity(0.1)) .clipShape(RoundedRectangle(cornerRadius: 6, style: .continuous)) VStack(alignment: .leading, spacing: 2) { Text(title) .font(.caption) .fontWeight(.medium) .foregroundColor(.primary) Text(description) .font(.caption2) .foregroundColor(.secondary) } Spacer() } .padding(12) .background(Color(.secondarySystemGroupedBackground)) .clipShape(RoundedRectangle(cornerRadius: 10, style: .continuous)) .contentShape(Rectangle()) } private func contactRow(contact: AboutContact) -> some View { HStack(spacing: 10) { Image(systemName: mapIconName(contact.icon)) .font(.subheadline) .foregroundColor(.payfritGreen) .frame(width: 28, height: 28) .background(Color.payfritGreen.opacity(0.1)) .clipShape(RoundedRectangle(cornerRadius: 6, style: .continuous)) VStack(alignment: .leading, spacing: 2) { Text(contact.label) .font(.caption) .fontWeight(.medium) .foregroundColor(.primary) Text(contact.url.replacingOccurrences(of: "https://", with: "").replacingOccurrences(of: "http://", with: "")) .font(.caption2) .foregroundColor(.secondary) } Spacer() Image(systemName: "arrow.up.right") .font(.caption2) .foregroundColor(.secondary) } .padding(12) .background(Color(.secondarySystemGroupedBackground)) .clipShape(RoundedRectangle(cornerRadius: 10, style: .continuous)) .contentShape(Rectangle()) } } // MARK: - Staggered Appear Modifier private struct StaggeredAppearModifier: ViewModifier { let index: Int let appeared: Bool func body(content: Content) -> some View { content .opacity(appeared ? 1 : 0) .offset(y: appeared ? 0 : 20) .animation( .spring(response: 0.4, dampingFraction: 0.8) .delay(Double(index) * 0.06), value: appeared ) } } private extension View { func staggeredAppear(index: Int, appeared: Bool) -> some View { modifier(StaggeredAppearModifier(index: index, appeared: appeared)) } } #Preview { NavigationStack { AboutScreen() } }