diff --git a/PortfolioJournal/Services/IAPService.swift b/PortfolioJournal/Services/IAPService.swift index 0341adc..32b17d3 100644 --- a/PortfolioJournal/Services/IAPService.swift +++ b/PortfolioJournal/Services/IAPService.swift @@ -260,4 +260,12 @@ extension IAPService { ("xmark.circle", "No Ads", "Ad-free experience forever"), ("person.2", "Family Sharing", "Share with up to 5 family members") ] + + /// Condensed benefits shown on the paywall (4 max, outcome-focused) + static let paywallBenefits: [(icon: String, title: String, subtitle: String)] = [ + ("clock.arrow.circlepath", "Your complete history", "Every snapshot, contribution, and gain since day one"), + ("chart.bar.xaxis", "Charts that reveal patterns", "Allocation, drawdown, performance — all in one place"), + ("wand.and.stars", "12-month forecasts", "See where your portfolio is likely heading"), + ("xmark.circle", "No ads, ever", "Clean, focused experience with no distractions") + ] } diff --git a/PortfolioJournal/Views/Onboarding/OnboardingView.swift b/PortfolioJournal/Views/Onboarding/OnboardingView.swift index d2a50a5..fddf784 100644 --- a/PortfolioJournal/Views/Onboarding/OnboardingView.swift +++ b/PortfolioJournal/Views/Onboarding/OnboardingView.swift @@ -14,28 +14,28 @@ struct OnboardingView: View { private let pages: [OnboardingPage] = [ OnboardingPage( - icon: "chart.pie.fill", - title: "Long-Term Tracking", - description: "A calm, offline-first portfolio tracker for investors who update monthly.", + icon: "chart.line.uptrend.xyaxis", + title: "Know exactly where you stand", + description: "See your total wealth, real returns, and allocation — always up to date.", color: .appPrimary ), OnboardingPage( - icon: "calendar.circle.fill", - title: "Monthly Check-ins", - description: "Build a deliberate habit. Update sources, log contributions, and add a short note.", + icon: "checkmark.circle.fill", + title: "5 minutes a month is enough", + description: "Log your values once a month. Portfolio Journal handles the math and shows your progress.", color: .positiveGreen ), OnboardingPage( - icon: "bell.badge.fill", - title: "Gentle Reminders", - description: "Get a monthly nudge to review your portfolio without realtime noise.", - color: .appWarning + icon: "leaf.fill", + title: "Ignore the noise. Track the trend.", + description: "Daily swings don't tell the real story. Your growth over months and years does.", + color: .appSecondary ), OnboardingPage( - icon: "leaf.fill", - title: "Calm Mode", - description: "Hide short-term swings and focus on contributions and long-term growth.", - color: .appSecondary + icon: "flag.checkered", + title: "Reach your financial goals", + description: "Set targets, track milestones, and see exactly how far you've come.", + color: .appWarning ) ] diff --git a/PortfolioJournal/Views/Premium/PaywallView.swift b/PortfolioJournal/Views/Premium/PaywallView.swift index 83bf699..0a3bbbc 100644 --- a/PortfolioJournal/Views/Premium/PaywallView.swift +++ b/PortfolioJournal/Views/Premium/PaywallView.swift @@ -9,54 +9,57 @@ struct PaywallView: View { @State private var showingError = false var body: some View { - NavigationStack { - ZStack { - AppBackground() + ZStack(alignment: .topTrailing) { + AppBackground() - ScrollView { - VStack(spacing: 24) { - // Header - headerSection + VStack(spacing: 0) { + // Header + headerSection + .padding(.top, 48) + .padding(.horizontal, 24) - // Features List - featuresSection + Spacer() - // Price Card - priceCard + // Key benefits + benefitsSection + .padding(.horizontal, 24) - // Purchase Button - purchaseButton + Spacer() - // Restore Button - restoreButton - - // Legal - legalSection - } - .padding() + // Bottom actions + VStack(spacing: 12) { + priceLabel + purchaseButton + restoreButton + legalSection } + .padding(.horizontal, 24) + .padding(.bottom, 32) } - .navigationTitle("Upgrade to Premium") - .navigationBarTitleDisplayMode(.inline) - .toolbar { - ToolbarItem(placement: .navigationBarLeading) { - Button { - dismiss() - } label: { - Image(systemName: "xmark") - .foregroundColor(.secondary) - } - } + + // Close button + Button { + dismiss() + } label: { + Image(systemName: "xmark") + .font(.system(size: 14, weight: .semibold)) + .foregroundColor(.secondary) + .padding(10) + .background(Color(.secondarySystemBackground)) + .clipShape(Circle()) } - .alert("Error", isPresented: $showingError) { - Button("OK", role: .cancel) {} - } message: { - Text(errorMessage ?? "An error occurred") - } - .onChange(of: iapService.isPremium) { _, isPremium in - if isPremium { - dismiss() - } + .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() } } } @@ -64,21 +67,20 @@ struct PaywallView: View { // MARK: - Header Section private var headerSection: some View { - VStack(spacing: 16) { - // Crown icon + VStack(spacing: 12) { ZStack { Circle() .fill( LinearGradient( - colors: [Color.yellow.opacity(0.3), Color.orange.opacity(0.3)], + colors: [Color.yellow.opacity(0.25), Color.orange.opacity(0.25)], startPoint: .topLeading, endPoint: .bottomTrailing ) ) - .frame(width: 80, height: 80) + .frame(width: 72, height: 72) Image(systemName: "crown.fill") - .font(.system(size: 36)) + .font(.system(size: 30)) .foregroundStyle( LinearGradient( colors: [.yellow, .orange], @@ -88,65 +90,43 @@ struct PaywallView: View { ) } - Text("Unlock Full Potential") + Text("Your full portfolio,\nfully clear") .font(.title.weight(.bold)) + .multilineTextAlignment(.center) - Text("Get unlimited access to all features with a one-time purchase") + Text("One payment. Every feature. Forever.") .font(.subheadline) .foregroundColor(.secondary) .multilineTextAlignment(.center) } } - // MARK: - Features Section + // MARK: - Benefits Section - private var featuresSection: some View { - VStack(spacing: 12) { - ForEach(IAPService.premiumFeatures, id: \.title) { feature in - FeatureRow( - icon: feature.icon, - title: feature.title, - description: feature.description - ) + 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() + .padding(20) .background(Color(.systemBackground)) .cornerRadius(AppConstants.UI.cornerRadius) .shadow(color: .black.opacity(0.05), radius: 8, y: 2) } - // MARK: - Price Card + // MARK: - Price Label - private var priceCard: some View { - VStack(spacing: 8) { + private var priceLabel: some View { + HStack(spacing: 4) { Text(iapService.formattedPrice) - .font(.system(size: 48, weight: .bold, design: .rounded)) + .font(.system(size: 28, weight: .bold, design: .rounded)) .foregroundColor(.appPrimary) - Text("One-time purchase") - .font(.subheadline) + Text("· one-time · Family Sharing") + .font(.footnote) .foregroundColor(.secondary) - - HStack(spacing: 4) { - Image(systemName: "person.2.fill") - .font(.caption) - Text("Includes Family Sharing") - .font(.caption) - } - .foregroundColor(.appSecondary) - .padding(.top, 4) } - .frame(maxWidth: .infinity) - .padding(.vertical, 24) - .background( - RoundedRectangle(cornerRadius: AppConstants.UI.cornerRadius) - .fill(Color.appPrimary.opacity(0.1)) - .overlay( - RoundedRectangle(cornerRadius: AppConstants.UI.cornerRadius) - .stroke(Color.appPrimary, lineWidth: 2) - ) - ) } // MARK: - Purchase Button @@ -160,7 +140,7 @@ struct PaywallView: View { ProgressView() .tint(.white) } else { - Text("Upgrade Now") + Text("Get Full Access") .font(.headline) } } @@ -189,21 +169,22 @@ struct PaywallView: View { // MARK: - Legal Section private var legalSection: some View { - VStack(spacing: 8) { - Text("Payment will be charged to your Apple ID account. By purchasing, you agree to our Terms of Service and Privacy Policy.") + VStack(spacing: 4) { + Text("Payment charged to your Apple ID account.") .font(.caption2) .foregroundColor(.secondary) .multilineTextAlignment(.center) - HStack(spacing: 16) { + HStack(spacing: 12) { Link("Terms", destination: URL(string: AppConstants.URLs.termsOfService)!) - .font(.caption) + .font(.caption2) + .foregroundColor(.secondary) Link("Privacy", destination: URL(string: AppConstants.URLs.privacyPolicy)!) - .font(.caption) + .font(.caption2) + .foregroundColor(.secondary) } } - .padding(.top, 8) } // MARK: - Actions @@ -238,30 +219,25 @@ struct PaywallView: View { } } -// MARK: - Feature Row +// MARK: - Benefit Row -struct FeatureRow: View { +struct BenefitRow: View { let icon: String let title: String - let description: String + let subtitle: String var body: some View { - HStack(spacing: 16) { - ZStack { - Circle() - .fill(Color.appPrimary.opacity(0.1)) - .frame(width: 44, height: 44) + HStack(spacing: 14) { + Image(systemName: icon) + .font(.system(size: 20)) + .foregroundColor(.appPrimary) + .frame(width: 28) - Image(systemName: icon) - .font(.system(size: 18)) - .foregroundColor(.appPrimary) - } - - VStack(alignment: .leading, spacing: 2) { + VStack(alignment: .leading, spacing: 1) { Text(title) .font(.subheadline.weight(.semibold)) - Text(description) + Text(subtitle) .font(.caption) .foregroundColor(.secondary) } @@ -270,7 +246,21 @@ struct FeatureRow: View { 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) } } @@ -293,10 +283,10 @@ struct CompactPaywallBanner: View { ) VStack(alignment: .leading, spacing: 2) { - Text("Unlock Premium") + Text("Full access, one payment") .font(.subheadline.weight(.semibold)) - Text("Get unlimited access to all features") + Text("Unlimited sources, advanced charts & more") .font(.caption) .foregroundColor(.secondary) } @@ -330,22 +320,24 @@ struct PremiumLockOverlay: View { var body: some View { VStack(spacing: 16) { - Image(systemName: "lock.fill") + Image(systemName: "crown.fill") .font(.system(size: 32)) - .foregroundColor(.appWarning) - - Text("Premium Feature") - .font(.headline) + .foregroundStyle( + LinearGradient( + colors: [.yellow, .orange], + startPoint: .topLeading, + endPoint: .bottomTrailing + ) + ) Text(feature) - .font(.subheadline) - .foregroundColor(.secondary) + .font(.headline) .multilineTextAlignment(.center) Button { showingPaywall = true } label: { - Label("Unlock", systemImage: "crown.fill") + Text("See full access") .font(.subheadline.weight(.semibold)) .padding(.horizontal, 20) .padding(.vertical, 10)