Version casi lista

This commit is contained in:
alexandrev-tibco
2026-02-17 13:34:02 +01:00
commit e593453abd
99 changed files with 10867 additions and 0 deletions
+249
View File
@@ -0,0 +1,249 @@
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, 40)
}
}
}
.navigationTitle("settings_premium")
.navigationBarTitleDisplayMode(.inline)
.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)
)
}
}