import SwiftUI import SwiftData struct PaywallStepView: View { @Environment(\.modelContext) private var context @Query private var allSettings: [AppSettings] @StateObject private var storeManager = StoreManager() var onSkip: () -> Void var onConverted: () -> Void @State private var purchaseStatusMessageKey: String? var body: some View { ZStack { Color.mealMoodBackground.ignoresSafeArea() VStack(spacing: 24) { AppIconPlaceholder(size: 84) .padding(.top, 20) VStack(spacing: 8) { Text("onboarding_paywall_title") .font(.mealMoodH1) .foregroundColor(.mealMoodTextPrimary) .multilineTextAlignment(.center) Text("onboarding_paywall_subtitle") .font(.mealMoodBody) .foregroundColor(.mealMoodTextSecondary) .multilineTextAlignment(.center) } .padding(.horizontal, 24) VStack(alignment: .leading, spacing: 10) { benefitRow(text: String(localized: "premium_unlimited_dishes")) benefitRow(text: String(localized: "premium_advanced_rules")) benefitRow(text: String(localized: "premium_future_weeks")) benefitRow(text: String(localized: "premium_no_ads")) } .padding(16) .background(Color.mealMoodSurface) .clipShape(RoundedRectangle(cornerRadius: 14)) .padding(.horizontal, 24) Text("premium_social_proof") .font(.mealMoodCaption) .foregroundColor(.mealMoodTextSecondary) Spacer(minLength: 0) VStack(spacing: 12) { PrimaryButton( title: trialButtonTitle, action: { Task { await startTrial() } }, isEnabled: !storeManager.isLoading ) Button(action: skipPaywall) { Text("onboarding_paywall_cta_skip") .font(.mealMoodSmall) .foregroundColor(.mealMoodTextSecondary) } .accessibilityIdentifier("onboarding_cta_paywall_skip") // Required by guideline 3.1.2(c) in every purchase flow. HStack(spacing: 16) { Link(destination: URL(string: "https://mealmood.app/privacy.html")!) { Text("premium_privacy_link") .font(.mealMoodCaption) .underline() .foregroundColor(.mealMoodTextSecondary) } Link(destination: URL(string: "https://mealmood.app/terms.html")!) { Text("premium_terms_link") .font(.mealMoodCaption) .underline() .foregroundColor(.mealMoodTextSecondary) } } } .padding(.horizontal, 24) .padding(.bottom, 24) } } .task { await storeManager.loadProducts() } .onAppear { AnalyticsService.logPaywallViewed(source: "onboarding") } .alert("premium_title", isPresented: Binding( get: { purchaseStatusMessageKey != nil }, set: { if !$0 { purchaseStatusMessageKey = nil } } )) { Button("dish_delete_blocked_ok", role: .cancel) {} } message: { Text(LocalizedStringKey(purchaseStatusMessageKey ?? "")) } } private var trialButtonTitle: String { if let yearly = storeManager.yearlyProduct { let format = String(localized: "premium_trial_note") let priceLabel = yearly.displayPrice + " / " + String(localized: "premium_year") return String(localized: "onboarding_paywall_cta_trial") + " — " + String(format: format, priceLabel) } return String(localized: "onboarding_paywall_cta_trial") } @ViewBuilder private func benefitRow(text: String) -> some View { HStack(spacing: 10) { Image(systemName: "checkmark.circle.fill") .foregroundColor(.mealMoodSuccess) Text(text) .font(.mealMoodBody) .foregroundColor(.mealMoodTextPrimary) Spacer() } } private func startTrial() async { let candidate = storeManager.yearlyProduct ?? storeManager.monthlyProduct guard let product = candidate else { purchaseStatusMessageKey = "premium_products_not_found" return } AnalyticsService.logPremiumUpgradeTapped(source: "onboarding") let result = await storeManager.purchase(product) switch result { case .success: if let settings = allSettings.first { settings.isPremium = true try? context.save() } AnalyticsService.logFreeTrialStarted(plan: product.id) onConverted() case .pending: purchaseStatusMessageKey = "premium_purchase_pending" case .cancelled: purchaseStatusMessageKey = "premium_purchase_cancelled" case .failed: purchaseStatusMessageKey = "premium_purchase_failed" } } private func skipPaywall() { AnalyticsService.logPaywallDismissed(source: "onboarding", secondsOnScreen: 0, didConvert: false) onSkip() } }