import SwiftUI // MARK: - Chart Preview (decorative, no real data) private struct PremiumChartPreview: View { // Normalized growth data representing a healthy upward portfolio trend private let points: [Double] = [0.52, 0.58, 0.55, 0.65, 0.70, 0.66, 0.78, 0.85, 0.82, 0.91, 0.88, 1.0] var body: some View { ZStack { RoundedRectangle(cornerRadius: 12) .fill(Color.appPrimary.opacity(0.08)) VStack(alignment: .leading, spacing: 0) { HStack(alignment: .firstTextBaseline) { Text("€24,750") .font(.system(size: 18, weight: .bold, design: .rounded)) .foregroundColor(.primary) Spacer() Text("+34.2%") .font(.caption.weight(.semibold)) .foregroundColor(.positiveGreen) .padding(.horizontal, 7) .padding(.vertical, 3) .background(Color.positiveGreen.opacity(0.12)) .clipShape(Capsule()) } .padding(.horizontal, 14) .padding(.top, 12) .padding(.bottom, 6) GeometryReader { geo in chartPaths(in: geo.size) } .padding(.bottom, 8) } } .frame(height: 96) } @ViewBuilder private func chartPaths(in size: CGSize) -> some View { let pts = chartPoints(in: size) // Gradient fill Path { path in guard !pts.isEmpty else { return } path.move(to: CGPoint(x: pts[0].x, y: size.height)) path.addLine(to: pts[0]) for i in 1.. [CGPoint] { guard points.count > 1 else { return [] } let stepX = size.width / CGFloat(points.count - 1) return points.enumerated().map { i, val in CGPoint(x: CGFloat(i) * stepX, y: size.height * (1.0 - val * 0.85)) } } } struct PaywallView: View { @Environment(\.dismiss) private var dismiss @EnvironmentObject var iapService: IAPService @State private var isPurchasing = false @State private var errorMessage: String? @State private var showingError = false var body: some View { ZStack(alignment: .topTrailing) { AppBackground() VStack(spacing: 0) { // Header headerSection .padding(.top, 48) .padding(.horizontal, 24) Spacer() // Key benefits benefitsSection .padding(.horizontal, 24) Spacer() // Bottom actions VStack(spacing: 12) { priceLabel purchaseButton restoreButton legalSection } .padding(.horizontal, 24) .padding(.bottom, 32) } // Close button Button { dismiss() } label: { Image(systemName: "xmark") .font(.system(size: 14, weight: .semibold)) .foregroundColor(.secondary) .padding(10) .background(Color(.secondarySystemBackground)) .clipShape(Circle()) } .padding(.top, 16) .padding(.trailing, 16) } .ignoresSafeArea() .alert("Error", isPresented: $showingError) { Button("OK", role: .cancel) {} } message: { Text(errorMessage ?? "An error occurred") } .onChange(of: iapService.isPremium) { _, isPremium in if isPremium { dismiss() } } } // MARK: - Header Section private var headerSection: some View { VStack(spacing: 12) { PremiumChartPreview() Text("Your full portfolio,\nfully clear") .font(.title.weight(.bold)) .multilineTextAlignment(.center) Text("One payment. Every feature. Forever.") .font(.subheadline) .foregroundColor(.secondary) .multilineTextAlignment(.center) } } // MARK: - Benefits Section private var benefitsSection: some View { VStack(spacing: 10) { ForEach(IAPService.paywallBenefits, id: \.title) { benefit in BenefitRow(icon: benefit.icon, title: benefit.title, subtitle: benefit.subtitle) } } .padding(20) .background(Color(.systemBackground)) .cornerRadius(AppConstants.UI.cornerRadius) .shadow(color: .black.opacity(0.05), radius: 8, y: 2) } // MARK: - Price Label private var priceLabel: some View { HStack(spacing: 4) { Text(iapService.formattedPrice) .font(.system(size: 28, weight: .bold, design: .rounded)) .foregroundColor(.appPrimary) Text("· one-time · Family Sharing") .font(.footnote) .foregroundColor(.secondary) } } // MARK: - Purchase Button private var purchaseButton: some View { Button { purchase() } label: { HStack { if isPurchasing { ProgressView() .tint(.white) } else { Text("Get Full Access") .font(.headline) } } .frame(maxWidth: .infinity) .padding() .background(Color.appPrimary) .foregroundColor(.white) .cornerRadius(AppConstants.UI.cornerRadius) } .disabled(isPurchasing) } // MARK: - Restore Button private var restoreButton: some View { Button { restore() } label: { Text("Restore Purchases") .font(.subheadline) .foregroundColor(.appPrimary) } .disabled(isPurchasing) } // MARK: - Legal Section private var legalSection: some View { VStack(spacing: 4) { Text("Payment charged to your Apple ID account.") .font(.caption2) .foregroundColor(.secondary) .multilineTextAlignment(.center) HStack(spacing: 12) { Link("Terms", destination: URL(string: AppConstants.URLs.termsOfService)!) .font(.caption2) .foregroundColor(.secondary) Link("Privacy", destination: URL(string: AppConstants.URLs.privacyPolicy)!) .font(.caption2) .foregroundColor(.secondary) } } } // MARK: - Actions private func purchase() { isPurchasing = true FirebaseService.shared.logPurchaseAttempt(productId: IAPService.premiumProductID) Task { do { try await iapService.purchase() } catch { errorMessage = error.localizedDescription showingError = true } isPurchasing = false } } private func restore() { isPurchasing = true Task { await iapService.restorePurchases() if !iapService.isPremium { errorMessage = "No purchases found to restore" showingError = true } isPurchasing = false } } } // MARK: - Benefit Row struct BenefitRow: View { let icon: String let title: String let subtitle: String var body: some View { HStack(spacing: 14) { Image(systemName: icon) .font(.system(size: 20)) .foregroundColor(.appPrimary) .frame(width: 28) VStack(alignment: .leading, spacing: 1) { Text(title) .font(.subheadline.weight(.semibold)) Text(subtitle) .font(.caption) .foregroundColor(.secondary) } Spacer() Image(systemName: "checkmark.circle.fill") .foregroundColor(.positiveGreen) .font(.system(size: 18)) } .padding(.vertical, 2) } } // MARK: - Feature Row (kept for backward compatibility) struct FeatureRow: View { let icon: String let title: String let description: String var body: some View { BenefitRow(icon: icon, title: title, subtitle: description) } } // MARK: - Compact Paywall (for inline use) struct CompactPaywallBanner: View { @EnvironmentObject var iapService: IAPService @Binding var showingPaywall: Bool var body: some View { HStack(spacing: 12) { Image(systemName: "crown.fill") .font(.title2) .foregroundStyle( LinearGradient( colors: [.yellow, .orange], startPoint: .topLeading, endPoint: .bottomTrailing ) ) VStack(alignment: .leading, spacing: 2) { Text("Full access, one payment") .font(.subheadline.weight(.semibold)) Text("Unlimited sources, advanced charts & more") .font(.caption) .foregroundColor(.secondary) } Spacer() Button { showingPaywall = true } label: { Text(iapService.formattedPrice) .font(.caption.weight(.semibold)) .padding(.horizontal, 12) .padding(.vertical, 6) .background(Color.appPrimary) .foregroundColor(.white) .cornerRadius(16) } } .padding() .background(Color(.systemBackground)) .cornerRadius(AppConstants.UI.cornerRadius) .shadow(color: .black.opacity(0.05), radius: 8, y: 2) } } // MARK: - Premium Lock Overlay struct PremiumLockOverlay: View { let feature: String @Binding var showingPaywall: Bool var body: some View { VStack(spacing: 16) { Image(systemName: "crown.fill") .font(.system(size: 32)) .foregroundStyle( LinearGradient( colors: [.yellow, .orange], startPoint: .topLeading, endPoint: .bottomTrailing ) ) Text(feature) .font(.headline) .multilineTextAlignment(.center) Button { showingPaywall = true } label: { Text("See full access") .font(.subheadline.weight(.semibold)) .padding(.horizontal, 20) .padding(.vertical, 10) .background(Color.appPrimary) .foregroundColor(.white) .cornerRadius(20) } } .frame(maxWidth: .infinity, maxHeight: .infinity) .background(Color(.systemBackground).opacity(0.95)) } } #Preview { PaywallView() .environmentObject(IAPService()) }