ff67ea1e71
El círculo decorativo de AppBackground (70% del ancho del panel, offset -35%) se desborda de su panel por diseño y en layouts side-by-side queda flotando SOBRE el sidebar de Charts. Las shapes de SwiftUI participan en hit-testing por defecto y el panel derecho va después en el HStack → cada tap en la zona del sidebar cubierta por el círculo moría en silencio. El patrón lo delató: fallaban Overview + Analyze (arriba, bajo el círculo) y funcionaban Risk + Forecast (abajo). Dependía de la geometría (orientación/tamaño), por eso no reproducía en el simulador en portrait. - AppBackground: .allowsHitTesting(false) — un fondo decorativo jamás debe interceptar toques (fix global: aplica también a Sources/Journal en iPad) - Panel de detalle de Charts: .clipped() para que la decoración tampoco PINTE sobre el sidebar - Selección premium nunca se bloquea: los charts premium se seleccionan y muestran teaser de desbloqueo en el área del chart (chart_locked_* ×7 idiomas); el paywall se presenta desde el botón (contexto fiable) - UITests: testChartSidebarSelection ahora corre en landscape (geometría que reproducía el bug) + testChartSelectionWithoutPremium nuevo; --no-premium en ScreenshotMode para testear la experiencia free Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WoScpmHdVj1aUf4rAp6hbe
100 lines
4.7 KiB
Swift
100 lines
4.7 KiB
Swift
import SwiftUI
|
|
|
|
/// Support for automated App Store screenshot capture. Activated by launching the app
|
|
/// with the `--screenshots` argument (used by the UI-test capture flow). Only mutates
|
|
/// state when that argument is present, so it never affects normal runs.
|
|
enum ScreenshotMode {
|
|
static var isActive: Bool { CommandLine.arguments.contains("--screenshots") }
|
|
|
|
/// Skip onboarding, biometric/PIN lock and the What's New sheet so the app opens
|
|
/// straight into content. Called very early, before ContentView reads @AppStorage.
|
|
static func applyDefaultsIfNeeded() {
|
|
guard isActive else { return }
|
|
let d = UserDefaults.standard
|
|
d.set(true, forKey: "onboardingCompleted")
|
|
d.set(false, forKey: "faceIdEnabled")
|
|
d.set(false, forKey: "pinEnabled")
|
|
d.set(false, forKey: "lockOnLaunch")
|
|
d.set(false, forKey: "lockOnBackground")
|
|
let version = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? ""
|
|
d.set(version, forKey: "lastSeenWhatsNewVersion")
|
|
// Unlock premium (DEBUG-only override) so captures/UI tests can exercise
|
|
// the premium charts without a StoreKit purchase. `--no-premium` keeps it
|
|
// off to test the free-tier experience (locked charts, teasers).
|
|
d.set(!CommandLine.arguments.contains("--no-premium"), forKey: "debugPremiumOverride")
|
|
}
|
|
|
|
/// Seed demo data (no-op if the store already has sources). Called once Core Data
|
|
/// has finished loading so the view context is ready.
|
|
static func seedIfNeeded() {
|
|
guard isActive else { return }
|
|
SampleDataService.shared.seedSampleData()
|
|
}
|
|
}
|
|
|
|
@main
|
|
struct PortfolioJournalApp: App {
|
|
@UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
|
|
@StateObject private var iapService: IAPService
|
|
@StateObject private var adMobService: AdMobService
|
|
@StateObject private var accountStore: AccountStore
|
|
@StateObject private var tabSelection = TabSelectionStore()
|
|
@Environment(\.scenePhase) private var scenePhase
|
|
|
|
let coreDataStack = CoreDataStack.shared
|
|
|
|
init() {
|
|
// Screenshot/UI-capture mode: skip onboarding, lock and What's New so the app
|
|
// launches straight into content with demo data (see ScreenshotMode).
|
|
ScreenshotMode.applyDefaultsIfNeeded()
|
|
|
|
// Clean up any duplicate objects from previous bugs before initializing stores
|
|
CoreDataStack.shared.cleanupDuplicateObjects()
|
|
|
|
let iap = IAPService()
|
|
_iapService = StateObject(wrappedValue: iap)
|
|
_adMobService = StateObject(wrappedValue: AdMobService())
|
|
_accountStore = StateObject(wrappedValue: AccountStore(iapService: iap))
|
|
}
|
|
|
|
var body: some Scene {
|
|
WindowGroup {
|
|
ContentView()
|
|
.environment(\.managedObjectContext, coreDataStack.viewContext)
|
|
.environmentObject(coreDataStack)
|
|
.environmentObject(iapService)
|
|
.environmentObject(adMobService)
|
|
.environmentObject(accountStore)
|
|
.environmentObject(tabSelection)
|
|
}
|
|
.onChange(of: scenePhase) { _, newPhase in
|
|
if newPhase == .active {
|
|
coreDataStack.refreshWidgetData()
|
|
// Re-read all Core Data objects from the persistent store so that
|
|
// iCloud changes made on other devices while this device was inactive
|
|
// are reflected immediately without waiting for a remote-change notification.
|
|
coreDataStack.refreshFromCloudKit()
|
|
NotificationService.shared.scheduleReEngagementNotification()
|
|
NotificationService.shared.scheduleMonthlyCheckIn()
|
|
NotificationService.shared.scheduleStreakProtectionReminder()
|
|
// Share-extension bridge: apply values captured from other apps and
|
|
// refresh the source mirror the extension reads.
|
|
if coreDataStack.isLoaded {
|
|
SharedQuickUpdateSync.ingestPending()
|
|
SharedQuickUpdateSync.refreshMirror()
|
|
}
|
|
} else if newPhase == .background {
|
|
guard iapService.isPremium else { return }
|
|
guard UserDefaults.standard.bool(forKey: "backupsEnabled") else { return }
|
|
let retention = UserDefaults.standard.integer(forKey: "backupRetentionCount")
|
|
let keepCount = [5, 10, 20].contains(retention) ? retention : 10
|
|
let includeICloud = UserDefaults.standard.bool(forKey: "cloudSyncEnabled")
|
|
_ = BackupService.shared.createBackup(
|
|
retentionCount: keepCount,
|
|
includeICloud: includeICloud
|
|
)
|
|
}
|
|
}
|
|
}
|
|
}
|