1.0.5: Eating out slots, rule violations panel, Crashlytics, search bar fix
- 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>
This commit is contained in:
@@ -0,0 +1,36 @@
|
||||
import Foundation
|
||||
|
||||
/// Encapsulates the premium status sync state machine.
|
||||
/// Extracted from ContentView to be independently testable.
|
||||
@MainActor
|
||||
final class PremiumSyncService {
|
||||
|
||||
/// Injectable for testing — defaults to real StoreKit check.
|
||||
var hasActiveSubscription: () async -> Bool = {
|
||||
await StoreManager.hasActiveSubscription()
|
||||
}
|
||||
|
||||
/// Determine whether the user should be premium after a sync.
|
||||
///
|
||||
/// Rules:
|
||||
/// - Active subscription found → always return `true` (upgrade).
|
||||
/// - No subscription, currently free → return `false`.
|
||||
/// - No subscription, currently premium, **cold launch** → return `false`
|
||||
/// (subscription likely expired between sessions).
|
||||
/// - No subscription, currently premium, **foreground activation** → return `true`
|
||||
/// (StoreKit `currentEntitlements` can return empty for a few seconds immediately
|
||||
/// after a purchase while the receipt propagates; never downgrade mid-session).
|
||||
func sync(currentIsPremium: Bool, isColdLaunch: Bool) async -> Bool {
|
||||
let active = await hasActiveSubscription()
|
||||
switch (active, currentIsPremium, isColdLaunch) {
|
||||
case (true, _, _):
|
||||
return true
|
||||
case (false, false, _):
|
||||
return false
|
||||
case (false, true, true):
|
||||
return false // Subscription expired → downgrade on cold launch
|
||||
case (false, true, false):
|
||||
return true // Keep premium on foreground (timing protection)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user