payfrit-works-ios/PayfritWorks/Views/AboutScreen.swift
Schwifty d5d1c35b55 fix: gate debug print() statements behind IS_DEV flag
Wrapped all debug print() calls in APIService (avatar debugging),
BeaconScanner (scan/resolve logging), TaskDetailScreen (beacon state),
and AboutScreen (error logging) with IS_DEV checks so they are silent
in production builds. Preview-only prints in RatingDialog left as-is.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 22:12:13 +00:00

258 lines
9.5 KiB
Swift

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