import SwiftUI import StoreKit struct PremiumView: View { @Environment(\.modelContext) private var context @StateObject private var storeManager = StoreManager() @State private var purchaseStatusMessageKey: String? let settings: AppSettings var body: some View { ZStack { Color.mealMoodBackground.ignoresSafeArea() ScrollView { VStack(spacing: 24) { VStack(spacing: 16) { AppIconPlaceholder(size: 84) Text("premium_title") .font(.mealMoodH1) .foregroundColor(.mealMoodTextPrimary) Text("premium_subtitle") .font(.mealMoodBody) .foregroundColor(.mealMoodTextSecondary) .multilineTextAlignment(.center) } .padding(.top, 28) .padding(.horizontal, 24) VStack(alignment: .leading, spacing: 12) { BenefitItem(text: String(localized: "premium_no_ads")) BenefitItem(text: String(localized: "premium_unlimited_dishes")) BenefitItem(text: String(localized: "premium_advanced_rules")) BenefitItem(text: String(localized: "premium_future_weeks")) BenefitItem(text: String(localized: "premium_share_week")) BenefitItem(text: String(localized: "premium_family_sharing")) } .padding(20) .background(Color.mealMoodSurface) .cornerRadius(16) .padding(.horizontal, 24) if settings.isPremium { Label("premium_active", systemImage: "checkmark.seal.fill") .font(.mealMoodBodyBold) .foregroundColor(.mealMoodSuccess) .padding(.horizontal, 24) } else if let monthly = storeManager.monthlyProduct { PlanCard( title: String(localized: "premium_monthly"), price: monthly.displayPrice + " / " + String(localized: "premium_month"), caption: String(localized: "premium_price_note"), isLoading: storeManager.isLoading ) { Task { await purchase(monthly) } } .padding(.horizontal, 24) } else { let fallbackText = fallbackContent(for: storeManager.productLoadState) PlanCard( title: String(localized: "premium_monthly"), price: fallbackText.price, caption: fallbackText.hint, buttonTitle: String(localized: "premium_retry_products"), isLoading: storeManager.isLoadingProducts ) { Task { await storeManager.loadProducts() } } .padding(.horizontal, 24) #if DEBUG let hasStoreKitFileInBundle = Bundle.main.url(forResource: "MealMood", withExtension: "storekit") != nil let hasStoreKitLaunchArgument = ProcessInfo.processInfo.arguments .contains { $0.localizedCaseInsensitiveContains("storekit") } VStack(alignment: .leading, spacing: 6) { Text("Debug") .font(.mealMoodCaption) .foregroundColor(.mealMoodTextSecondary) Text("StoreKit file in bundle: \(hasStoreKitFileInBundle ? "yes" : "no")") .font(.mealMoodCaption) .foregroundColor(.mealMoodTextSecondary) Text("StoreKit launch arg: \(hasStoreKitLaunchArgument ? "yes" : "no")") .font(.mealMoodCaption) .foregroundColor(.mealMoodTextSecondary) Text("Bundle: \(Bundle.main.bundleIdentifier ?? "-")") .font(.mealMoodCaption) .foregroundColor(.mealMoodTextSecondary) Text("IDs: \(storeManager.debugProductIds.joined(separator: ", "))") .font(.mealMoodCaption) .foregroundColor(.mealMoodTextSecondary) Text("Loaded: \(storeManager.debugLoadedProductIds.joined(separator: ", "))") .font(.mealMoodCaption) .foregroundColor(.mealMoodTextSecondary) Text("Loaded detail: \(storeManager.debugLoadedProducts.joined(separator: ", "))") .font(.mealMoodCaption) .foregroundColor(.mealMoodTextSecondary) } .padding(.horizontal, 24) #endif } Button { Task { await restorePurchases() } } label: { Text("premium_restore") .font(.mealMoodSmall) .foregroundColor(.mealMoodCoral) } .padding(.top, 4) Text("premium_terms_note") .font(.mealMoodCaption) .foregroundColor(.mealMoodTextSecondary) .multilineTextAlignment(.center) .padding(.horizontal, 24) .padding(.bottom, 24) } .safeAreaPadding(.bottom, 20) } } .navigationTitle("settings_premium") .navigationBarTitleDisplayMode(.inline) .presentationDetents([.large]) .presentationDragIndicator(.visible) .task { await storeManager.loadProducts() } .onChange(of: storeManager.isPremium) { _, isPremium in if settings.isPremium != isPremium { settings.isPremium = isPremium try? context.save() } } .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 purchase(_ product: Product) async { let result = await storeManager.purchase(product) switch result { case .success: settings.isPremium = true try? context.save() case .pending: purchaseStatusMessageKey = "premium_purchase_pending" case .cancelled: purchaseStatusMessageKey = "premium_purchase_cancelled" case .failed: purchaseStatusMessageKey = "premium_purchase_failed" } } private func restorePurchases() async { await storeManager.restorePurchases() settings.isPremium = storeManager.isPremium try? context.save() } private func fallbackContent(for state: StoreManager.ProductLoadState) -> (price: String, hint: String) { switch state { case .timedOut: return ( String(localized: "premium_loading_timeout"), String(localized: "premium_loading_timeout_hint") ) case .notFound: return ( String(localized: "premium_products_not_found"), String(localized: "premium_products_not_found_hint") ) case .failed: return ( String(localized: "premium_loading_failed"), String(localized: "premium_loading_failed_hint") ) case .idle, .loading, .loaded: return ( String(localized: "premium_loading_products"), String(localized: "premium_loading_products_hint") ) } } } private struct BenefitItem: View { let text: String var body: some View { HStack(spacing: 10) { Image(systemName: "checkmark.circle.fill") .foregroundColor(.mealMoodSuccess) .font(.system(size: 18)) Text(text) .font(.mealMoodBody) .foregroundColor(.mealMoodTextPrimary) } } } private struct PlanCard: View { let title: String let price: String let caption: String var buttonTitle: String? = nil var isLoading: Bool = false let action: () -> Void var body: some View { VStack(spacing: 12) { HStack { VStack(alignment: .leading, spacing: 4) { Text(title) .font(.mealMoodH3) .foregroundColor(.mealMoodTextPrimary) Text(price) .font(.mealMoodBody) .foregroundColor(.mealMoodTextSecondary) Text(caption) .font(.mealMoodCaption) .foregroundColor(.mealMoodTextSecondary) } Spacer() } PrimaryButton( title: isLoading ? String(localized: "premium_processing") : (buttonTitle ?? String(localized: "premium_subscribe")), action: action, isEnabled: !isLoading ) } .padding(16) .background(Color.mealMoodSurface) .cornerRadius(16) .overlay( RoundedRectangle(cornerRadius: 16) .stroke(Color(hex: "#F0F0F0"), lineWidth: 1) ) } }