258 lines
9.5 KiB
Swift
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
|
|
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()
|
|
}
|
|
}
|