Rework onboarding flow and move premium prompt post-setup
This commit is contained in:
@@ -643,7 +643,7 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CODE_SIGN_ENTITLEMENTS = MealMood/Resources/MealMood.entitlements;
|
||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
CURRENT_PROJECT_VERSION = 2;
|
||||
DEVELOPMENT_TEAM = 2825Q76T7H;
|
||||
GENERATE_INFOPLIST_FILE = NO;
|
||||
INFOPLIST_FILE = MealMood/Resources/Info.plist;
|
||||
@@ -652,7 +652,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.0.0;
|
||||
MARKETING_VERSION = 1.0.1;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.alexandrevazquez.mealmood;
|
||||
PRODUCT_NAME = MealMood;
|
||||
SDKROOT = iphoneos;
|
||||
@@ -747,7 +747,7 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CODE_SIGN_ENTITLEMENTS = MealMood/Resources/MealMood.entitlements;
|
||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
CURRENT_PROJECT_VERSION = 2;
|
||||
DEVELOPMENT_TEAM = 2825Q76T7H;
|
||||
GENERATE_INFOPLIST_FILE = NO;
|
||||
INFOPLIST_FILE = MealMood/Resources/Info.plist;
|
||||
@@ -756,7 +756,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.0.0;
|
||||
MARKETING_VERSION = 1.0.1;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.alexandrevazquez.mealmood;
|
||||
PRODUCT_NAME = MealMood;
|
||||
SDKROOT = iphoneos;
|
||||
|
||||
@@ -1,27 +1,28 @@
|
||||
/* Onboarding */
|
||||
"onboarding_welcome_title" = "MealMood";
|
||||
"onboarding_welcome_subtitle" = "Plan your week, enjoy your meals";
|
||||
"onboarding_welcome_benefit1" = "No stress";
|
||||
"onboarding_welcome_benefit2" = "No calorie counting";
|
||||
"onboarding_welcome_benefit3" = "Sync with your calendar";
|
||||
"onboarding_welcome_title" = "Organize your meals in 1 minute";
|
||||
"onboarding_welcome_subtitle" = "Answer 4 quick steps and start planning today.";
|
||||
"onboarding_welcome_step_1" = "Choose if you want to plan lunches, dinners, or both";
|
||||
"onboarding_welcome_step_2" = "Decide whether to include weekends";
|
||||
"onboarding_welcome_step_3" = "Optionally connect your calendar";
|
||||
"onboarding_welcome_step_4" = "Add your usual dishes to start fast";
|
||||
"onboarding_start" = "Get Started";
|
||||
"onboarding_continue" = "Continue";
|
||||
"onboarding_skip" = "Skip";
|
||||
"onboarding_finish" = "Finish";
|
||||
|
||||
/* Meal Windows */
|
||||
"meal_windows_title" = "What do you want to plan?";
|
||||
"meal_windows_title" = "Step 1: Lunches, dinners, or both?";
|
||||
"meal_windows_dinner_only" = "Dinners only";
|
||||
"meal_windows_lunch_only" = "Lunches only";
|
||||
"meal_windows_both" = "Lunches and dinners";
|
||||
|
||||
/* Weekends */
|
||||
"weekends_title" = "Include weekends?";
|
||||
"weekends_title" = "Step 2: Also plan weekends?";
|
||||
"weekends_toggle" = "Also plan Saturdays and Sundays";
|
||||
"weekends_note" = "You can change this later in settings";
|
||||
|
||||
/* Calendar */
|
||||
"calendar_title" = "Sync with your calendar?";
|
||||
"calendar_title" = "Step 3: Connect your calendar?";
|
||||
"calendar_sync" = "Sync";
|
||||
"calendar_select" = "Calendar";
|
||||
"calendar_lunch_time" = "Lunch time";
|
||||
@@ -32,7 +33,7 @@
|
||||
"calendar_permission_settings" = "Go to Settings";
|
||||
|
||||
/* First Dishes */
|
||||
"first_dishes_title" = "Add your first dishes";
|
||||
"first_dishes_title" = "Step 4: Add your go-to dishes";
|
||||
"first_dishes_subtitle" = "Add at least 2 dishes to get started";
|
||||
"first_dishes_name_placeholder" = "Dish name";
|
||||
"first_dishes_add_tag" = "Add tag";
|
||||
@@ -232,6 +233,13 @@
|
||||
"review_feedback_title" = "Tell us what to improve";
|
||||
"review_feedback_message" = "Send us your feedback and we'll use it to improve the app.";
|
||||
"review_feedback_contact" = "Send feedback";
|
||||
"onboarding_auto_assign_title" = "Want us to auto-assign your week?";
|
||||
"onboarding_auto_assign_message" = "We can fill this week automatically using your dishes and rules.";
|
||||
"onboarding_auto_assign_yes" = "Yes, auto-assign";
|
||||
"onboarding_auto_assign_no" = "No, I'll do it";
|
||||
"onboarding_premium_prompt_title" = "Want to unlock Premium?";
|
||||
"onboarding_premium_prompt_message" = "Get no ads, unlimited dishes, advanced rules, and future-week planning.";
|
||||
"onboarding_premium_prompt_cta" = "See Premium";
|
||||
|
||||
/* Notifications */
|
||||
"notification_planning_title" = "Plan your next week";
|
||||
|
||||
@@ -1,27 +1,28 @@
|
||||
/* Onboarding */
|
||||
"onboarding_welcome_title" = "MealMood";
|
||||
"onboarding_welcome_subtitle" = "Planifica tu semana, disfruta tus comidas";
|
||||
"onboarding_welcome_benefit1" = "Sin estrés";
|
||||
"onboarding_welcome_benefit2" = "Sin contar calorías";
|
||||
"onboarding_welcome_benefit3" = "Sincroniza con tu calendario";
|
||||
"onboarding_welcome_title" = "Organiza tus comidas en 1 minuto";
|
||||
"onboarding_welcome_subtitle" = "Responde 4 pasos rápidos y empieza a planificar hoy.";
|
||||
"onboarding_welcome_step_1" = "Elige si quieres planificar comidas, cenas o ambas";
|
||||
"onboarding_welcome_step_2" = "Decide si incluir fines de semana";
|
||||
"onboarding_welcome_step_3" = "Conecta tu calendario de forma opcional";
|
||||
"onboarding_welcome_step_4" = "Añade tus platos habituales para empezar rápido";
|
||||
"onboarding_start" = "Comenzar";
|
||||
"onboarding_continue" = "Continuar";
|
||||
"onboarding_skip" = "Omitir";
|
||||
"onboarding_finish" = "Finalizar";
|
||||
|
||||
/* Meal Windows */
|
||||
"meal_windows_title" = "¿Qué quieres planificar?";
|
||||
"meal_windows_title" = "Paso 1: ¿Comidas, cenas o ambas?";
|
||||
"meal_windows_dinner_only" = "Solo cenas";
|
||||
"meal_windows_lunch_only" = "Solo comidas";
|
||||
"meal_windows_both" = "Comidas y cenas";
|
||||
|
||||
/* Weekends */
|
||||
"weekends_title" = "¿Incluir fines de semana?";
|
||||
"weekends_title" = "Paso 2: ¿Planificamos también fines de semana?";
|
||||
"weekends_toggle" = "Planificar también sábados y domingos";
|
||||
"weekends_note" = "Puedes cambiar esto después en ajustes";
|
||||
|
||||
/* Calendar */
|
||||
"calendar_title" = "¿Sincronizar con tu calendario?";
|
||||
"calendar_title" = "Paso 3: ¿Lo integramos con tu calendario?";
|
||||
"calendar_sync" = "Sincronizar";
|
||||
"calendar_select" = "Calendario";
|
||||
"calendar_lunch_time" = "Hora de comidas";
|
||||
@@ -32,7 +33,7 @@
|
||||
"calendar_permission_settings" = "Ir a Ajustes";
|
||||
|
||||
/* First Dishes */
|
||||
"first_dishes_title" = "Añade tus primeros platos";
|
||||
"first_dishes_title" = "Paso 4: Añade tus platos habituales";
|
||||
"first_dishes_subtitle" = "Añade al menos 2 platos para empezar";
|
||||
"first_dishes_name_placeholder" = "Nombre del plato";
|
||||
"first_dishes_add_tag" = "Añadir etiqueta";
|
||||
@@ -232,6 +233,13 @@
|
||||
"review_feedback_title" = "Cuéntanos qué mejorar";
|
||||
"review_feedback_message" = "Envíanos tu feedback y lo usaremos para mejorar la app.";
|
||||
"review_feedback_contact" = "Enviar feedback";
|
||||
"onboarding_auto_assign_title" = "¿Quieres que hagamos una asignación automática?";
|
||||
"onboarding_auto_assign_message" = "Podemos completar esta semana automáticamente usando tus platos y reglas.";
|
||||
"onboarding_auto_assign_yes" = "Sí, asignar automáticamente";
|
||||
"onboarding_auto_assign_no" = "No, lo haré yo";
|
||||
"onboarding_premium_prompt_title" = "¿Quieres desbloquear Premium?";
|
||||
"onboarding_premium_prompt_message" = "Consigue sin anuncios, platos ilimitados, reglas avanzadas y planificación de semanas futuras.";
|
||||
"onboarding_premium_prompt_cta" = "Ver Premium";
|
||||
|
||||
/* Notifications */
|
||||
"notification_planning_title" = "Planifica tu próxima semana";
|
||||
|
||||
@@ -3,6 +3,9 @@ import SwiftData
|
||||
|
||||
@MainActor
|
||||
final class OnboardingViewModel: ObservableObject {
|
||||
static let pendingAutoAssignPromptKey = "onboarding_pending_auto_assign_prompt"
|
||||
static let pendingPremiumPromptKey = "onboarding_pending_premium_prompt"
|
||||
|
||||
@Published var currentStep: Int = 0
|
||||
@Published var selectedMealWindows: MealWindows = .dinnerOnly
|
||||
@Published var includeWeekends: Bool = true
|
||||
@@ -24,16 +27,15 @@ final class OnboardingViewModel: ObservableObject {
|
||||
@Published var addedDishes: [(name: String, tagIds: [UUID])] = []
|
||||
let maxOnboardingDishes = 10
|
||||
|
||||
let totalSteps = 6
|
||||
let totalSteps = 5
|
||||
|
||||
var canContinue: Bool {
|
||||
switch currentStep {
|
||||
case 0: return true // Welcome
|
||||
case 1: return true // Trial
|
||||
case 2: return true // Meal windows (always has selection)
|
||||
case 3: return true // Weekends (toggle)
|
||||
case 4: return true // Calendar (optional)
|
||||
case 5: return addedDishes.count >= 2 // Need at least 2 dishes
|
||||
case 1: return true // Meal windows (always has selection)
|
||||
case 2: return true // Weekends (toggle)
|
||||
case 3: return true // Calendar (optional)
|
||||
case 4: return addedDishes.count >= 2 // Need at least 2 dishes
|
||||
default: return false
|
||||
}
|
||||
}
|
||||
@@ -149,6 +151,8 @@ final class OnboardingViewModel: ObservableObject {
|
||||
|
||||
do {
|
||||
try context.save()
|
||||
UserDefaults.standard.set(true, forKey: Self.pendingAutoAssignPromptKey)
|
||||
UserDefaults.standard.set(true, forKey: Self.pendingPremiumPromptKey)
|
||||
return true
|
||||
} catch {
|
||||
print("Onboarding save failed: \(error)")
|
||||
|
||||
@@ -21,6 +21,9 @@ struct HomeView: View {
|
||||
@State private var showCopyPreviousConfirm: Bool = false
|
||||
@State private var showReviewSentimentPrompt: Bool = false
|
||||
@State private var showReviewSupportPrompt: Bool = false
|
||||
@State private var showPostOnboardingAutoAssignPrompt: Bool = false
|
||||
@State private var showPostOnboardingPremiumPrompt: Bool = false
|
||||
@State private var hasEvaluatedPostOnboardingPrompts: Bool = false
|
||||
|
||||
private var settings: AppSettings? { allSettings.first }
|
||||
|
||||
@@ -251,6 +254,30 @@ struct HomeView: View {
|
||||
secondaryButton: .cancel(Text("reset_cancel"))
|
||||
)
|
||||
}
|
||||
.alert("onboarding_auto_assign_title", isPresented: $showPostOnboardingAutoAssignPrompt) {
|
||||
Button("onboarding_auto_assign_yes") {
|
||||
viewModel.autoComplete(plan: plan, dishes: dishes, tags: tags, settings: settings)
|
||||
UserDefaults.standard.set(false, forKey: OnboardingViewModel.pendingAutoAssignPromptKey)
|
||||
schedulePostOnboardingPremiumPromptIfNeeded(settings: settings)
|
||||
}
|
||||
Button("onboarding_auto_assign_no", role: .cancel) {
|
||||
UserDefaults.standard.set(false, forKey: OnboardingViewModel.pendingAutoAssignPromptKey)
|
||||
schedulePostOnboardingPremiumPromptIfNeeded(settings: settings)
|
||||
}
|
||||
} message: {
|
||||
Text("onboarding_auto_assign_message")
|
||||
}
|
||||
.alert("onboarding_premium_prompt_title", isPresented: $showPostOnboardingPremiumPrompt) {
|
||||
Button("onboarding_premium_prompt_cta") {
|
||||
UserDefaults.standard.set(false, forKey: OnboardingViewModel.pendingPremiumPromptKey)
|
||||
showPremiumFromExport = true
|
||||
}
|
||||
Button("reset_cancel", role: .cancel) {
|
||||
UserDefaults.standard.set(false, forKey: OnboardingViewModel.pendingPremiumPromptKey)
|
||||
}
|
||||
} message: {
|
||||
Text("onboarding_premium_prompt_message")
|
||||
}
|
||||
.alert("review_funnel_title", isPresented: $showReviewSentimentPrompt) {
|
||||
Button("review_funnel_positive") {
|
||||
ReviewPromptService.shared.requestReview()
|
||||
@@ -374,6 +401,7 @@ struct HomeView: View {
|
||||
language: settings.languageEnum.resolved()
|
||||
)
|
||||
wasWeekComplete = isWeekComplete(plan: plan)
|
||||
evaluatePostOnboardingPromptsIfNeeded(plan: plan, settings: settings)
|
||||
}
|
||||
.onChange(of: plan.updatedAt) { _, _ in
|
||||
let nowComplete = isWeekComplete(plan: plan)
|
||||
@@ -643,6 +671,34 @@ struct HomeView: View {
|
||||
ReviewPromptService.shared.markFunnelShown(completedWeeks: completedWeeks)
|
||||
showReviewSentimentPrompt = true
|
||||
}
|
||||
|
||||
private func evaluatePostOnboardingPromptsIfNeeded(plan: WeekPlan, settings: AppSettings) {
|
||||
guard !hasEvaluatedPostOnboardingPrompts else { return }
|
||||
hasEvaluatedPostOnboardingPrompts = true
|
||||
|
||||
let shouldAskAutoAssign = UserDefaults.standard.bool(forKey: OnboardingViewModel.pendingAutoAssignPromptKey)
|
||||
if shouldAskAutoAssign && plan.slots.contains(where: { $0.dishId == nil }) {
|
||||
showPostOnboardingAutoAssignPrompt = true
|
||||
return
|
||||
}
|
||||
|
||||
UserDefaults.standard.set(false, forKey: OnboardingViewModel.pendingAutoAssignPromptKey)
|
||||
schedulePostOnboardingPremiumPromptIfNeeded(settings: settings)
|
||||
}
|
||||
|
||||
private func schedulePostOnboardingPremiumPromptIfNeeded(settings: AppSettings) {
|
||||
guard !settings.isPremium else {
|
||||
UserDefaults.standard.set(false, forKey: OnboardingViewModel.pendingPremiumPromptKey)
|
||||
return
|
||||
}
|
||||
|
||||
let shouldShowPremiumPrompt = UserDefaults.standard.bool(forKey: OnboardingViewModel.pendingPremiumPromptKey)
|
||||
guard shouldShowPremiumPrompt else { return }
|
||||
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) {
|
||||
showPostOnboardingPremiumPrompt = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct MonthlyHistoryView: View {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import SwiftUI
|
||||
import SwiftData
|
||||
import StoreKit
|
||||
|
||||
struct OnboardingView: View {
|
||||
@Environment(\.modelContext) private var context
|
||||
@@ -30,22 +29,17 @@ struct OnboardingView: View {
|
||||
WelcomeStepView(onNext: { viewModel.nextStep() })
|
||||
.tag(0)
|
||||
|
||||
TrialStepView(
|
||||
onContinue: { viewModel.nextStep() }
|
||||
)
|
||||
.tag(1)
|
||||
|
||||
MealWindowsStepView(
|
||||
selection: $viewModel.selectedMealWindows,
|
||||
onNext: { viewModel.nextStep() }
|
||||
)
|
||||
.tag(2)
|
||||
.tag(1)
|
||||
|
||||
WeekendsStepView(
|
||||
includeWeekends: $viewModel.includeWeekends,
|
||||
onNext: { viewModel.nextStep() }
|
||||
)
|
||||
.tag(3)
|
||||
.tag(2)
|
||||
|
||||
CalendarStepView(
|
||||
iCloudSyncEnabled: $viewModel.syncICloud,
|
||||
@@ -59,7 +53,7 @@ struct OnboardingView: View {
|
||||
onNext: { viewModel.nextStep() },
|
||||
onSkip: { viewModel.nextStep() }
|
||||
)
|
||||
.tag(4)
|
||||
.tag(3)
|
||||
|
||||
FirstDishesStepView(
|
||||
viewModel: viewModel,
|
||||
@@ -70,7 +64,7 @@ struct OnboardingView: View {
|
||||
}
|
||||
}
|
||||
)
|
||||
.tag(5)
|
||||
.tag(4)
|
||||
}
|
||||
.tabViewStyle(.page(indexDisplayMode: .never))
|
||||
.animation(.easeInOut(duration: 0.3), value: viewModel.currentStep)
|
||||
@@ -113,133 +107,3 @@ struct OnboardingView: View {
|
||||
return !plans.isEmpty
|
||||
}
|
||||
}
|
||||
|
||||
private struct TrialStepView: View {
|
||||
@Environment(\.modelContext) private var context
|
||||
@StateObject private var storeManager = StoreManager()
|
||||
@State private var purchaseStatusMessageKey: String?
|
||||
|
||||
let onContinue: () -> Void
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 24) {
|
||||
Spacer()
|
||||
|
||||
VStack(spacing: 12) {
|
||||
Text("onboarding_trial_title")
|
||||
.font(.mealMoodH2)
|
||||
.foregroundColor(.mealMoodTextPrimary)
|
||||
.multilineTextAlignment(.center)
|
||||
|
||||
Text("onboarding_trial_subtitle")
|
||||
.font(.mealMoodBody)
|
||||
.foregroundColor(.mealMoodTextSecondary)
|
||||
.multilineTextAlignment(.center)
|
||||
}
|
||||
.padding(.horizontal, 24)
|
||||
|
||||
VStack(alignment: .leading, spacing: 10) {
|
||||
Label("premium_no_ads", systemImage: "checkmark.circle.fill")
|
||||
Label("premium_unlimited_dishes", systemImage: "checkmark.circle.fill")
|
||||
Label("premium_advanced_rules", systemImage: "checkmark.circle.fill")
|
||||
}
|
||||
.font(.mealMoodBody)
|
||||
.foregroundColor(.mealMoodTextPrimary)
|
||||
.padding(16)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.background(Color.mealMoodSurface)
|
||||
.cornerRadius(14)
|
||||
.padding(.horizontal, 24)
|
||||
|
||||
if let monthly = storeManager.monthlyProduct {
|
||||
VStack(spacing: 10) {
|
||||
Text(
|
||||
String(
|
||||
format: String(localized: "onboarding_trial_price_format"),
|
||||
monthly.displayPrice
|
||||
)
|
||||
)
|
||||
.font(.mealMoodCaption)
|
||||
.foregroundColor(.mealMoodTextSecondary)
|
||||
|
||||
PrimaryButton(
|
||||
title: storeManager.isLoading
|
||||
? String(localized: "premium_processing")
|
||||
: String(localized: "onboarding_trial_cta"),
|
||||
action: { Task { await startTrial(with: monthly) } },
|
||||
isEnabled: !storeManager.isLoading,
|
||||
localizeTitle: false
|
||||
)
|
||||
}
|
||||
.padding(.horizontal, 24)
|
||||
} else {
|
||||
let fallbackText = fallbackContent(for: storeManager.productLoadState)
|
||||
VStack(spacing: 8) {
|
||||
Text(fallbackText)
|
||||
.font(.mealMoodCaption)
|
||||
.foregroundColor(.mealMoodTextSecondary)
|
||||
.multilineTextAlignment(.center)
|
||||
SecondaryButton(
|
||||
title: String(localized: "premium_retry_products"),
|
||||
action: { Task { await storeManager.loadProducts() } },
|
||||
localizeTitle: false
|
||||
)
|
||||
}
|
||||
.padding(.horizontal, 24)
|
||||
}
|
||||
|
||||
SecondaryButton(
|
||||
title: String(localized: "onboarding_trial_skip"),
|
||||
action: onContinue,
|
||||
localizeTitle: false
|
||||
)
|
||||
.padding(.horizontal, 24)
|
||||
.padding(.bottom, 40)
|
||||
}
|
||||
.alert("premium_title", isPresented: Binding(
|
||||
get: { purchaseStatusMessageKey != nil },
|
||||
set: { isPresented in
|
||||
if !isPresented { purchaseStatusMessageKey = nil }
|
||||
}
|
||||
)) {
|
||||
Button("dish_delete_blocked_ok", role: .cancel) {}
|
||||
} message: {
|
||||
Text(LocalizedStringKey(purchaseStatusMessageKey ?? ""))
|
||||
}
|
||||
}
|
||||
|
||||
private func startTrial(with product: Product) async {
|
||||
let result = await storeManager.purchase(product)
|
||||
switch result {
|
||||
case .success:
|
||||
let descriptor = FetchDescriptor<AppSettings>()
|
||||
let settings = (try? context.fetch(descriptor))?.first ?? {
|
||||
let created = AppSettings()
|
||||
context.insert(created)
|
||||
return created
|
||||
}()
|
||||
settings.isPremium = true
|
||||
try? context.save()
|
||||
onContinue()
|
||||
case .pending:
|
||||
purchaseStatusMessageKey = "premium_purchase_pending"
|
||||
case .cancelled:
|
||||
purchaseStatusMessageKey = "premium_purchase_cancelled"
|
||||
case .failed:
|
||||
purchaseStatusMessageKey = "premium_purchase_failed"
|
||||
}
|
||||
}
|
||||
|
||||
private func fallbackContent(for state: StoreManager.ProductLoadState) -> String {
|
||||
switch state {
|
||||
case .timedOut:
|
||||
return String(localized: "premium_loading_timeout_hint")
|
||||
case .notFound:
|
||||
return String(localized: "premium_products_not_found_hint")
|
||||
case .failed:
|
||||
return String(localized: "premium_loading_failed_hint")
|
||||
case .idle, .loading, .loaded:
|
||||
return String(localized: "premium_loading_products_hint")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,28 +13,20 @@ struct WelcomeStepView: View {
|
||||
Text("onboarding_welcome_title")
|
||||
.font(.mealMoodH1)
|
||||
.foregroundColor(.mealMoodTextPrimary)
|
||||
.multilineTextAlignment(.center)
|
||||
|
||||
Text("onboarding_welcome_subtitle")
|
||||
.font(.mealMoodBody)
|
||||
.foregroundColor(.mealMoodTextSecondary)
|
||||
.multilineTextAlignment(.center)
|
||||
}
|
||||
|
||||
Image(systemName: "figure.2.and.child.holdinghands")
|
||||
.font(.system(size: 80))
|
||||
.foregroundStyle(
|
||||
LinearGradient(
|
||||
colors: [.mealMoodCoral, .mealMoodMint],
|
||||
startPoint: .topLeading,
|
||||
endPoint: .bottomTrailing
|
||||
)
|
||||
)
|
||||
.padding(.vertical, 20)
|
||||
.padding(.horizontal, 24)
|
||||
|
||||
VStack(alignment: .leading, spacing: 16) {
|
||||
BenefitRow(icon: "face.smiling", text: String(localized: "onboarding_welcome_benefit1"))
|
||||
BenefitRow(icon: "scalemass", text: String(localized: "onboarding_welcome_benefit2"))
|
||||
BenefitRow(icon: "calendar", text: String(localized: "onboarding_welcome_benefit3"))
|
||||
BenefitRow(icon: "1.circle.fill", text: String(localized: "onboarding_welcome_step_1"))
|
||||
BenefitRow(icon: "2.circle.fill", text: String(localized: "onboarding_welcome_step_2"))
|
||||
BenefitRow(icon: "3.circle.fill", text: String(localized: "onboarding_welcome_step_3"))
|
||||
BenefitRow(icon: "4.circle.fill", text: String(localized: "onboarding_welcome_step_4"))
|
||||
}
|
||||
.padding(.horizontal, 40)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user