110807d239
- Eating out: tap any empty slot → "Mark as eating out"; shows teal indicator, skipped by auto-assign, counts as complete, tap again to unmark - Rule violations panel: access via wand long-press context menu when conflicts exist; shows all isRuleOverridden slots with Fix (clear) or Ignore (acknowledge) actions - Firebase Crashlytics integrated: CrashlyticsService + dSYM upload build phase, isPremium property tracked per session - PremiumSyncService: extracted premium state machine, StoreKit Transaction.updates listener, isPremium no longer synced via iCloud to avoid stale state - StoreManager: analytics on purchase/restore, bundle ID fallback for product ID lookup - Search bar contrast bug fixed: TextField now has explicit foreground color for dark mode - WelcomeStepView: redesigned onboarding welcome screen with week preview - Version bump: 1.0.5 build 22 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
166 lines
6.4 KiB
Swift
166 lines
6.4 KiB
Swift
import SwiftUI
|
|
|
|
struct WelcomeStepView: View {
|
|
var onNext: () -> Void
|
|
|
|
var body: some View {
|
|
VStack(spacing: 0) {
|
|
Spacer()
|
|
|
|
AppIconPlaceholder(size: 96)
|
|
.padding(.bottom, 12)
|
|
|
|
Text("onboarding_welcome_subtitle")
|
|
.font(.mealMoodBody)
|
|
.foregroundColor(.mealMoodTextSecondary)
|
|
.multilineTextAlignment(.center)
|
|
.padding(.horizontal, 32)
|
|
.padding(.bottom, 24)
|
|
|
|
weekPreview
|
|
.padding(.bottom, 24)
|
|
|
|
VStack(alignment: .leading, spacing: 14) {
|
|
BenefitRow(icon: "wand.and.stars", text: String(localized: "onboarding_welcome_step_1"))
|
|
BenefitRow(icon: "square.and.arrow.up", text: String(localized: "onboarding_welcome_step_3"))
|
|
BenefitRow(icon: "icloud", text: String(localized: "onboarding_welcome_step_4"))
|
|
}
|
|
.padding(.horizontal, 40)
|
|
|
|
Spacer()
|
|
|
|
PrimaryButton(title: String(localized: "onboarding_start"), icon: "✨", action: onNext)
|
|
.padding(.horizontal, 24)
|
|
.padding(.bottom, 40)
|
|
}
|
|
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
|
|
}
|
|
|
|
// MARK: - Week preview
|
|
|
|
private var weekPreview: some View {
|
|
VStack(alignment: .leading, spacing: 8) {
|
|
Text(String(localized: "onboarding_welcome_preview_label"))
|
|
.font(.mealMoodCaption)
|
|
.foregroundColor(.mealMoodTextSecondary)
|
|
.padding(.horizontal, 24)
|
|
|
|
ScrollView(.horizontal, showsIndicators: false) {
|
|
HStack(spacing: 6) {
|
|
ForEach(sampleDays, id: \.name) { day in
|
|
VStack(spacing: 4) {
|
|
Text(day.name)
|
|
.font(.system(size: 10, weight: .bold))
|
|
.foregroundColor(.mealMoodTextPrimary)
|
|
.frame(maxWidth: .infinity)
|
|
.padding(.vertical, 5)
|
|
.background(day.color)
|
|
.clipShape(RoundedRectangle(cornerRadius: 8))
|
|
|
|
Text(day.dish)
|
|
.font(.system(size: 10, weight: .medium))
|
|
.foregroundColor(.mealMoodTextPrimary)
|
|
.multilineTextAlignment(.center)
|
|
.lineLimit(2)
|
|
.minimumScaleFactor(0.8)
|
|
.frame(maxWidth: .infinity, minHeight: 38)
|
|
.padding(.horizontal, 4)
|
|
.padding(.vertical, 5)
|
|
.background(Color.white.opacity(0.92))
|
|
.clipShape(RoundedRectangle(cornerRadius: 8))
|
|
.overlay(
|
|
RoundedRectangle(cornerRadius: 8)
|
|
.stroke(Color(hex: "#EEE4DE"), lineWidth: 1)
|
|
)
|
|
}
|
|
.frame(width: 66)
|
|
}
|
|
}
|
|
.padding(.horizontal, 20)
|
|
.padding(.vertical, 8)
|
|
}
|
|
.background(
|
|
RoundedRectangle(cornerRadius: 14)
|
|
.fill(Color.mealMoodBackground)
|
|
.overlay(
|
|
RoundedRectangle(cornerRadius: 14)
|
|
.stroke(Color.mealMoodCoral.opacity(0.25), lineWidth: 1)
|
|
)
|
|
)
|
|
.padding(.horizontal, 16)
|
|
}
|
|
}
|
|
|
|
private var sampleDays: [(name: String, dish: String, color: Color)] {
|
|
let lang = Locale.current.language.languageCode?.identifier ?? "en"
|
|
switch lang {
|
|
case "es":
|
|
return [
|
|
("Lun", "Pasta al horno", Color(hex: "#FFD5D9")),
|
|
("Mar", "Pollo asado", Color(hex: "#FFE1C8")),
|
|
("Mié", "Lentejas", Color(hex: "#FFF0C8")),
|
|
("Jue", "Salmón", Color(hex: "#DFF2CC")),
|
|
("Vie", "Tortilla", Color(hex: "#D3F0E7"))
|
|
]
|
|
case "fr":
|
|
return [
|
|
("Lun", "Pâtes gratinées", Color(hex: "#FFD5D9")),
|
|
("Mar", "Poulet rôti", Color(hex: "#FFE1C8")),
|
|
("Mer", "Lentilles", Color(hex: "#FFF0C8")),
|
|
("Jeu", "Saumon", Color(hex: "#DFF2CC")),
|
|
("Ven", "Omelette", Color(hex: "#D3F0E7"))
|
|
]
|
|
case "de":
|
|
return [
|
|
("Mo", "Nudeln", Color(hex: "#FFD5D9")),
|
|
("Di", "Hähnchen", Color(hex: "#FFE1C8")),
|
|
("Mi", "Linsensuppe", Color(hex: "#FFF0C8")),
|
|
("Do", "Lachs", Color(hex: "#DFF2CC")),
|
|
("Fr", "Omelett", Color(hex: "#D3F0E7"))
|
|
]
|
|
case "it":
|
|
return [
|
|
("Lun", "Pasta al forno", Color(hex: "#FFD5D9")),
|
|
("Mar", "Pollo arrosto", Color(hex: "#FFE1C8")),
|
|
("Mer", "Lenticchie", Color(hex: "#FFF0C8")),
|
|
("Gio", "Salmone", Color(hex: "#DFF2CC")),
|
|
("Ven", "Frittata", Color(hex: "#D3F0E7"))
|
|
]
|
|
case "pt":
|
|
return [
|
|
("Seg", "Macarrão", Color(hex: "#FFD5D9")),
|
|
("Ter", "Frango assado", Color(hex: "#FFE1C8")),
|
|
("Qua", "Lentilhas", Color(hex: "#FFF0C8")),
|
|
("Qui", "Salmão", Color(hex: "#DFF2CC")),
|
|
("Sex", "Omelete", Color(hex: "#D3F0E7"))
|
|
]
|
|
default:
|
|
return [
|
|
("Mon", "Baked pasta", Color(hex: "#FFD5D9")),
|
|
("Tue", "Roast chicken", Color(hex: "#FFE1C8")),
|
|
("Wed", "Lentil soup", Color(hex: "#FFF0C8")),
|
|
("Thu", "Salmon", Color(hex: "#DFF2CC")),
|
|
("Fri", "Omelette", Color(hex: "#D3F0E7"))
|
|
]
|
|
}
|
|
}
|
|
}
|
|
|
|
private struct BenefitRow: View {
|
|
let icon: String
|
|
let text: String
|
|
|
|
var body: some View {
|
|
HStack(spacing: 12) {
|
|
Image(systemName: icon)
|
|
.font(.system(size: 18))
|
|
.foregroundColor(.mealMoodCoral)
|
|
.frame(width: 28)
|
|
|
|
Text(text)
|
|
.font(.mealMoodBody)
|
|
.foregroundColor(.mealMoodTextPrimary)
|
|
}
|
|
}
|
|
}
|