Files
FamilyMealPlanner/MealMood/Views/Onboarding/PaywallStepView.swift
T
alexandrev-tibco 0b57812a6e premium: enlaces a privacidad y terminos (EULA) en los flujos de compra
Guideline 3.1.2(c): Apple rechazo la 2.0.5 (77) por no tener enlaces
funcionales a la politica de privacidad y los terminos de uso dentro
del paywall. Anadidos en PremiumView y en el paywall del onboarding
(mealmood.app/privacy.html y terms.html), y ambos enlaces al final de
la descripcion del App Store en los 6 idiomas.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013H6bXqGX1ygwib1Dm3n3UG
2026-09-07 08:04:39 +02:00

144 lines
5.7 KiB
Swift

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()
}
}