payfrit-works-ios/PayfritWorks/Views/AccountScreen.swift
2026-02-01 23:38:34 -08:00

353 lines
12 KiB
Swift

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
var body: some View {
ScrollView {
VStack(spacing: 16) {
if isLoading {
ProgressView()
.padding(.top, 40)
} else if let error = error {
errorView(error)
} else {
if let tier = tierStatus {
tierCard(tier)
activationCard(tier)
}
if let ledger = ledger {
earningsCard(ledger)
}
logoutButton
}
}
.padding(16)
}
.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 loadData() }
.refreshable { await loadData() }
.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: - 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 {
// Tier 1 unlocked
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 {
// No account yet
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 {
// Account exists but incomplete
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 {
// Progress bar
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: - Error
private func errorView(_ msg: String) -> some View {
VStack(spacing: 16) {
Image(systemName: "exclamationmark.circle")
.font(.system(size: 48))
.foregroundColor(.red)
Text(msg)
.multilineTextAlignment(.center)
Button("Retry") { Task { await loadData() } }
.buttonStyle(.borderedProminent)
}
.padding()
}
// MARK: - Actions
private func loadData() async {
isLoading = true
error = nil
// Load tier status (required)
do {
tierStatus = try await APIService.shared.getTierStatus()
} catch {
self.error = error.localizedDescription
isLoading = false
return
}
// Load ledger (optional don't block screen if it fails)
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
}
}
// Refresh on return
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
}
}
}
}