Version casi lista
This commit is contained in:
@@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user