d8345a5b20
El hogar compartido cambia donde viven los datos, asi que se va a la 3.0 y la 2.x queda para correcciones. El trabajo sigue vivo en la rama 3.0.0 (163fd60). Importante para esta rama: esto tambien retira el entitlement com.apple.developer.applesignin, que sin la capability marcada en el App ID habria roto la firma al subir la 2.1.1 a TestFlight. This reverts commit163fd60. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013su1ttRiMeMYxkZJ1Y3246
124 lines
4.7 KiB
Swift
124 lines
4.7 KiB
Swift
import SwiftUI
|
|
import SwiftData
|
|
import StoreKit
|
|
|
|
struct ContentView: View {
|
|
@Environment(\.modelContext) private var context
|
|
@Environment(\.scenePhase) private var scenePhase
|
|
@Query private var allSettings: [AppSettings]
|
|
@State private var isReady = false
|
|
@State private var hasCompletedColdLaunch = false
|
|
|
|
private let premiumSyncService = PremiumSyncService()
|
|
private var settings: AppSettings? { allSettings.first }
|
|
|
|
var body: some View {
|
|
Group {
|
|
if !isReady {
|
|
ZStack {
|
|
Color.mealMoodBackground.ignoresSafeArea()
|
|
AppIconPlaceholder(size: 120)
|
|
}
|
|
} else if let settings = settings, settings.onboardingCompleted {
|
|
HomeView()
|
|
} else {
|
|
OnboardingView {
|
|
// Onboarding completed
|
|
}
|
|
}
|
|
}
|
|
.environment(\.locale, Locale(identifier: settings?.languageEnum.localeIdentifier ?? Locale.current.identifier))
|
|
// MARK: Transaction.updates listener (StoreKit 2 recommended pattern)
|
|
// Fires for new purchases, renewals and revocations while the app is running.
|
|
// Activate WatchConnectivity as early as possible so the first widget
|
|
// refresh doesn't race the session activation.
|
|
.onAppear { WatchSyncService.shared.activate() }
|
|
// This is the most reliable way to catch a purchase even if the app was
|
|
// interrupted during the payment flow.
|
|
.task {
|
|
for await result in Transaction.updates {
|
|
guard case .verified(let transaction) = result else { continue }
|
|
guard let s = settings else {
|
|
await transaction.finish()
|
|
continue
|
|
}
|
|
let isActive = transaction.revocationDate == nil &&
|
|
(transaction.expirationDate.map { $0 > Date() } ?? true)
|
|
|
|
if isActive && !s.isPremium {
|
|
s.isPremium = true
|
|
try? context.save()
|
|
} else if transaction.revocationDate != nil && s.isPremium {
|
|
// Explicitly revoked subscription → downgrade
|
|
s.isPremium = false
|
|
try? context.save()
|
|
}
|
|
await transaction.finish()
|
|
}
|
|
}
|
|
.onAppear {
|
|
initializeAppIfNeeded()
|
|
Task {
|
|
if settings?.iCloudSyncEnabledResolved ?? true {
|
|
await ICloudSyncService.shared.pullRemoteIfNeeded(context: context)
|
|
}
|
|
await syncPremiumStatus(isColdLaunch: true)
|
|
hasCompletedColdLaunch = true
|
|
}
|
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
|
withAnimation(.easeInOut) {
|
|
isReady = true
|
|
}
|
|
}
|
|
}
|
|
.onChange(of: scenePhase) { _, phase in
|
|
Task {
|
|
switch phase {
|
|
case .active:
|
|
// Only run foreground sync after cold launch is complete.
|
|
// The first activation is covered by .onAppear above.
|
|
if hasCompletedColdLaunch {
|
|
await syncPremiumStatus(isColdLaunch: false)
|
|
}
|
|
case .inactive, .background:
|
|
if settings?.iCloudSyncEnabledResolved ?? true {
|
|
await ICloudSyncService.shared.pushLocalSnapshot(context: context)
|
|
}
|
|
@unknown default:
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Private
|
|
|
|
private func initializeAppIfNeeded() {
|
|
// CloudKit mirroring can duplicate singletons/default data created on
|
|
// other devices — collapse them before anything reads the store.
|
|
if CloudSyncRuntime.isCloudKitActive {
|
|
DeduplicationService.run(context: context)
|
|
}
|
|
if allSettings.isEmpty {
|
|
DefaultDataService.createDefaultSettings(context: context)
|
|
}
|
|
let tagDescriptor = FetchDescriptor<Tag>()
|
|
if (try? context.fetch(tagDescriptor))?.isEmpty ?? true {
|
|
DefaultDataService.createDefaultTags(context: context)
|
|
}
|
|
}
|
|
|
|
private func syncPremiumStatus(isColdLaunch: Bool) async {
|
|
guard let settings else { return }
|
|
let newPremium = await premiumSyncService.sync(
|
|
currentIsPremium: settings.isPremium,
|
|
isColdLaunch: isColdLaunch
|
|
)
|
|
if settings.isPremium != newPremium {
|
|
settings.isPremium = newPremium
|
|
try? context.save()
|
|
}
|
|
CrashlyticsService.setUserProperties(isPremium: settings.isPremium)
|
|
}
|
|
}
|