import SwiftUI struct AccountScreen: View { @EnvironmentObject var appState: AppState @Environment(\.dismiss) var dismiss @State private var tierStatus: TierStatus? @State private var ledger: LedgerResponse? @State private var isLoading = true @State private var error: String? @State private var showingMyTasks = false @State private var showActivationInfo = false @State private var appearAnimation = false @State private var avatarURLLoaded: URL? private var displayName: String { appState.userName ?? "Worker" } private var avatarURL: URL? { // Prefer loaded URL from API, fall back to appState if let loaded = avatarURLLoaded { return loaded } guard let urlString = appState.userPhotoUrl, !urlString.isEmpty else { return nil } return URL(string: urlString) } var body: some View { ScrollView { VStack(spacing: 16) { // Avatar section VStack(spacing: 8) { if let url = avatarURL { AsyncImage(url: url) { phase in switch phase { case .success(let image): image .resizable() .scaledToFill() case .failure: placeholderAvatar default: ProgressView() .frame(width: 70, height: 70) } } .frame(width: 70, height: 70) .clipShape(Circle()) .overlay(Circle().stroke(Color.payfritGreen.opacity(0.3), lineWidth: 2)) .shadow(color: .black.opacity(0.1), radius: 6, y: 3) } else { placeholderAvatar } Text(displayName) .font(.subheadline) .fontWeight(.bold) .foregroundColor(.primary) } .frame(maxWidth: .infinity) .padding(.top, 8) .scaleEffect(appearAnimation ? 1 : 0.9) .opacity(appearAnimation ? 1 : 0) // Navigation cards VStack(spacing: 12) { NavigationLink { ProfileScreen() .environmentObject(appState) } label: { accountCard( icon: "person", title: "Edit Profile", subtitle: "View your account info" ) } .staggeredAppear(index: 0, appeared: appearAnimation) NavigationLink { AboutScreen() } label: { accountCard( icon: "info.circle", title: "About Payfrit", subtitle: "App info and features" ) } .staggeredAppear(index: 1, appeared: appearAnimation) } .padding(.horizontal, 16) // Payout content if isLoading { LoadingView() .padding(.top, 20) } else if let error = error { ErrorView(message: error) { Task { await loadData() } } } else { VStack(spacing: 16) { if let tier = tierStatus { tierCard(tier) .staggeredAppear(index: 2, appeared: appearAnimation) activationCard(tier) .staggeredAppear(index: 3, appeared: appearAnimation) } if let ledger = ledger { earningsCard(ledger) .staggeredAppear(index: 4, appeared: appearAnimation) } logoutButton .staggeredAppear(index: 5, appeared: appearAnimation) } .padding(.horizontal, 16) } } .padding(.vertical, 16) } .background(Color(.systemGroupedBackground).ignoresSafeArea()) .navigationTitle("Account") .overlay(alignment: .bottomTrailing) { Button { showingMyTasks = true } label: { Image(systemName: "checkmark.circle.fill") .font(.title3) .padding(12) .background(Color.payfritGreen) .foregroundColor(.white) .clipShape(Circle()) .shadow(radius: 4) } .padding(.trailing, 16) .padding(.bottom, 16) } .navigationDestination(isPresented: $showingMyTasks) { MyTasksScreen() } .task { await loadAvatar() await loadData() } .refreshable { await loadData() } .onAppear { withAnimation(.spring(response: 0.5, dampingFraction: 0.8)) { appearAnimation = true } } .alert("What is Activation?", isPresented: $showActivationInfo) { Button("Got it", role: .cancel) { } } message: { Text("Activation tracks your early earnings to help establish your account. Once you reach the activation cap, your account is fully activated and you can receive regular payouts. You can also complete activation early if you prefer.") } } // MARK: - Avatar private var placeholderAvatar: some View { Image(systemName: "person.circle.fill") .resizable() .foregroundStyle(.linearGradient( colors: [Color.payfritGreen.opacity(0.6), Color.payfritGreen.opacity(0.3)], startPoint: .top, endPoint: .bottom )) .frame(width: 70, height: 70) .shadow(color: .black.opacity(0.1), radius: 6, y: 3) } // MARK: - Account Card private func accountCard(icon: String, title: String, subtitle: 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(subtitle) .font(.caption2) .foregroundColor(.secondary) } Spacer() Image(systemName: "chevron.right") .font(.caption2) .foregroundColor(.secondary) } .padding(12) .background(Color(.secondarySystemGroupedBackground)) .clipShape(RoundedRectangle(cornerRadius: 10, style: .continuous)) .contentShape(Rectangle()) } // MARK: - Tier Card @ViewBuilder private func tierCard(_ tier: TierStatus) -> some View { VStack(alignment: .leading, spacing: 12) { HStack { Image(systemName: tier.tier >= 1 ? "checkmark.shield.fill" : "shield") .foregroundColor(tier.tier >= 1 ? .green : .secondary) .font(.title2) Text("Payout Status") .font(.headline) Spacer() } if tier.tier >= 1 { HStack(spacing: 8) { Image(systemName: "checkmark.circle.fill") .foregroundColor(.green) Text("Tier 1 unlocked — Payouts enabled") .foregroundColor(.green) .font(.subheadline.weight(.medium)) } } else if !tier.stripe.hasAccount { Text("Tier 1 is locked") .font(.subheadline) .foregroundColor(.secondary) Button { startStripeOnboarding() } label: { Label("Complete payout setup", systemImage: "arrow.right.circle.fill") .frame(maxWidth: .infinity) } .buttonStyle(.borderedProminent) .tint(.payfritGreen) } else if tier.stripe.setupIncomplete { Text("Stripe needs more info to enable payouts.") .font(.subheadline) .foregroundColor(.secondary) Button { continueStripeOnboarding() } label: { Label("Continue payout setup", systemImage: "arrow.right.circle.fill") .frame(maxWidth: .infinity) } .buttonStyle(.borderedProminent) .tint(.payfritGreen) } } .padding(16) .background(Color(.secondarySystemGroupedBackground)) .cornerRadius(12) } // MARK: - Activation Card @ViewBuilder private func activationCard(_ tier: TierStatus) -> some View { VStack(alignment: .leading, spacing: 12) { HStack { Image(systemName: tier.activation.isComplete ? "checkmark.circle.fill" : "star.circle") .foregroundColor(tier.activation.isComplete ? .green : .payfritGreen) .font(.title2) Text("Activation") .font(.headline) Spacer() } if tier.activation.isComplete { HStack(spacing: 8) { Image(systemName: "checkmark.circle.fill") .foregroundColor(.green) Text("Activation complete") .foregroundColor(.green) .font(.subheadline.weight(.medium)) } } else { VStack(alignment: .leading, spacing: 6) { ProgressView(value: tier.activation.progress) .tint(.payfritGreen) Text("\(tier.activation.balanceDollars) of \(tier.activation.capDollars) completed") .font(.subheadline) .foregroundColor(.secondary) } Button { earlyUnlock() } label: { Label("Complete activation now (optional)", systemImage: "bolt.fill") .frame(maxWidth: .infinity) } .buttonStyle(.bordered) Button { showActivationInfo = true } label: { Text("What is activation?") .font(.caption) .foregroundColor(.payfritGreen) } } } .padding(16) .background(Color(.secondarySystemGroupedBackground)) .cornerRadius(12) } // MARK: - Earnings Card @ViewBuilder private func earningsCard(_ ledger: LedgerResponse) -> some View { VStack(alignment: .leading, spacing: 12) { HStack { Image(systemName: "dollarsign.circle") .foregroundColor(.green) .font(.title2) Text("Earnings") .font(.headline) Spacer() VStack(alignment: .trailing) { Text(ledger.totals.totalNetDollars) .font(.title3.bold()) .foregroundColor(.green) Text("total earned") .font(.caption) .foregroundColor(.secondary) } } if !ledger.entries.isEmpty { Divider() ForEach(ledger.entries.prefix(20)) { entry in HStack { VStack(alignment: .leading, spacing: 2) { Text("Task #\(entry.taskId)") .font(.subheadline.weight(.medium)) Text(entry.createdAt) .font(.caption) .foregroundColor(.secondary) } Spacer() VStack(alignment: .trailing, spacing: 2) { Text(entry.netDollars) .font(.subheadline.weight(.medium)) .foregroundColor(.green) Text(entry.statusDisplay) .font(.caption2) .foregroundColor(entry.status == "transferred" ? .green : .secondary) .padding(.horizontal, 6) .padding(.vertical, 2) .background( entry.status == "transferred" ? Color.green.opacity(0.1) : Color(.systemGray5) ) .cornerRadius(4) } } .padding(.vertical, 2) } } else { Text("No earnings yet. Complete tasks to start earning!") .font(.subheadline) .foregroundColor(.secondary) } } .padding(16) .background(Color(.secondarySystemGroupedBackground)) .cornerRadius(12) } // MARK: - Logout private var logoutButton: some View { Button(role: .destructive) { Task { await APIService.shared.logout() await AuthStorage.shared.clearAuth() appState.clearAuth() } } label: { Label("Logout", systemImage: "rectangle.portrait.and.arrow.right") .frame(maxWidth: .infinity) } .buttonStyle(.bordered) .tint(.red) .padding(.top, 8) } // MARK: - Actions private func loadAvatar() async { do { if let urlString = try await APIService.shared.getAvatarUrl(), let url = URL(string: urlString) { avatarURLLoaded = url } } catch { // Avatar load failed, will use placeholder } } private func loadData() async { isLoading = true error = nil do { tierStatus = try await APIService.shared.getTierStatus() } catch { self.error = error.localizedDescription isLoading = false return } do { ledger = try await APIService.shared.getLedger() } catch { ledger = LedgerResponse() } isLoading = false } private func startStripeOnboarding() { Task { do { _ = try await APIService.shared.createStripeAccount() let urlStr = try await APIService.shared.getOnboardingLink() if let url = URL(string: urlStr) { await MainActor.run { #if os(iOS) UIApplication.shared.open(url) #endif } } try? await Task.sleep(nanoseconds: 2_000_000_000) await loadData() } catch { self.error = error.localizedDescription } } } private func continueStripeOnboarding() { Task { do { let urlStr = try await APIService.shared.getOnboardingLink() if let url = URL(string: urlStr) { await MainActor.run { #if os(iOS) UIApplication.shared.open(url) #endif } } try? await Task.sleep(nanoseconds: 2_000_000_000) await loadData() } catch { self.error = error.localizedDescription } } } private func earlyUnlock() { Task { do { let urlStr = try await APIService.shared.getEarlyUnlockUrl() if let url = URL(string: urlStr) { await MainActor.run { #if os(iOS) UIApplication.shared.open(url) #endif } } try? await Task.sleep(nanoseconds: 2_000_000_000) await loadData() } catch { self.error = error.localizedDescription } } } } // 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 { AccountScreen() .environmentObject(AppState()) } }