Release 1.4.0 (build 31): retención, quick update, streak, what's new
- 1A: Notificación mensual de resumen de portfolio (día 5 de cada mes) - 1B: Badge de racha mensual en Dashboard (streak counter) - 1C: Empty states mejorados en Goals y Journal con CTAs claros - 2A: Quick Update sheet — actualiza todas las fuentes desde una pantalla - 2B: Notificación batch update redirige al Quick Update sheet - 2C: Widget deep link portfoliojournal://quickupdate abre Quick Update - 3A: App Store version check banner en Settings - 3C: What's New sheet en primer launch de versión nueva - Localización completa en 7 idiomas para todas las nuevas strings Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,34 @@
|
||||
import SwiftUI
|
||||
import CoreData
|
||||
|
||||
// MARK: - App Tab
|
||||
|
||||
enum AppTab: Int, Hashable, CaseIterable {
|
||||
case dashboard = 0, sources = 1, charts = 2, journal = 3, settings = 4
|
||||
|
||||
var title: String {
|
||||
switch self {
|
||||
case .dashboard: String(localized: "tab_dashboard")
|
||||
case .sources: String(localized: "tab_sources")
|
||||
case .charts: String(localized: "tab_charts")
|
||||
case .journal: "Journal"
|
||||
case .settings: String(localized: "tab_settings")
|
||||
}
|
||||
}
|
||||
|
||||
var icon: String {
|
||||
switch self {
|
||||
case .dashboard: "house.fill"
|
||||
case .sources: "list.bullet"
|
||||
case .charts: "chart.xyaxis.line"
|
||||
case .journal: "book.closed"
|
||||
case .settings: "gearshape.fill"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Content View
|
||||
|
||||
struct ContentView: View {
|
||||
@EnvironmentObject var iapService: IAPService
|
||||
@EnvironmentObject var adMobService: AdMobService
|
||||
@@ -11,11 +39,19 @@ struct ContentView: View {
|
||||
@AppStorage("pinEnabled") private var pinEnabled = false
|
||||
@AppStorage("lockOnLaunch") private var lockOnLaunch = true
|
||||
@AppStorage("lockOnBackground") private var lockOnBackground = false
|
||||
@AppStorage("lastSeenWhatsNewVersion") private var lastSeenWhatsNewVersion = ""
|
||||
@Environment(\.scenePhase) private var scenePhase
|
||||
@Environment(\.horizontalSizeClass) private var horizontalSizeClass
|
||||
@State private var isUnlocked = false
|
||||
@State private var resolvedOnboardingCompleted: Bool?
|
||||
@State private var iCloudCheckDone = false
|
||||
@State private var loadingMessageKey: LocalizedStringKey = "loading_data"
|
||||
@State private var sidebarSelection: AppTab? = .dashboard
|
||||
@State private var showingWhatsNew = false
|
||||
|
||||
private var currentVersion: String {
|
||||
Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? ""
|
||||
}
|
||||
|
||||
private var lockEnabled: Bool {
|
||||
faceIdEnabled || pinEnabled
|
||||
@@ -39,6 +75,8 @@ struct ContentView: View {
|
||||
OnboardingICloudCheckView(onSkip: { iCloudCheckDone = true })
|
||||
} else if resolvedOnboardingCompleted == false {
|
||||
OnboardingView(onboardingCompleted: $onboardingCompleted)
|
||||
} else if horizontalSizeClass == .regular {
|
||||
iPadMainContent
|
||||
} else {
|
||||
mainContent
|
||||
}
|
||||
@@ -75,10 +113,36 @@ struct ContentView: View {
|
||||
}
|
||||
.task {
|
||||
await waitForDataAndResolveOnboarding()
|
||||
if (resolvedOnboardingCompleted == true) && currentVersion != lastSeenWhatsNewVersion {
|
||||
// Small delay to let the UI settle before showing sheet
|
||||
try? await Task.sleep(nanoseconds: 300_000_000)
|
||||
showingWhatsNew = true
|
||||
}
|
||||
}
|
||||
.onChange(of: onboardingCompleted) { _, completed in
|
||||
resolvedOnboardingCompleted = completed
|
||||
}
|
||||
.sheet(isPresented: $showingWhatsNew, onDismiss: {
|
||||
lastSeenWhatsNewVersion = currentVersion
|
||||
}) {
|
||||
WhatsNewView()
|
||||
}
|
||||
.onReceive(NotificationCenter.default.publisher(for: .openBatchUpdate)) { _ in
|
||||
tabSelection.selectedTab = 1
|
||||
sidebarSelection = .sources
|
||||
NotificationCenter.default.post(name: .openQuickUpdate, object: nil)
|
||||
}
|
||||
.onReceive(NotificationCenter.default.publisher(for: .openDashboard)) { _ in
|
||||
tabSelection.selectedTab = 0
|
||||
sidebarSelection = .dashboard
|
||||
}
|
||||
.onOpenURL { url in
|
||||
if url.host == "quickupdate" {
|
||||
tabSelection.selectedTab = 0
|
||||
sidebarSelection = .dashboard
|
||||
NotificationCenter.default.post(name: .openQuickUpdate, object: nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var isReadyForContent: Bool {
|
||||
@@ -160,6 +224,49 @@ struct ContentView: View {
|
||||
return ((try? context.count(for: accountRequest)) ?? 0) > 0
|
||||
}
|
||||
|
||||
// MARK: - iPad Layout
|
||||
|
||||
private var iPadMainContent: some View {
|
||||
NavigationSplitView(columnVisibility: .constant(.all)) {
|
||||
List(AppTab.allCases, id: \.self, selection: $sidebarSelection) { tab in
|
||||
Label(tab.title, systemImage: tab.icon)
|
||||
}
|
||||
.navigationTitle("Portfolio Journal")
|
||||
.navigationSplitViewColumnWidth(min: 200, ideal: 240, max: 280)
|
||||
} detail: {
|
||||
iPadDetailView(for: sidebarSelection ?? .dashboard)
|
||||
}
|
||||
.onChange(of: sidebarSelection) { _, tab in
|
||||
guard let tab else { return }
|
||||
if tabSelection.selectedTab != tab.rawValue {
|
||||
tabSelection.selectedTab = tab.rawValue
|
||||
}
|
||||
}
|
||||
.onChange(of: tabSelection.selectedTab) { _, value in
|
||||
if let tab = AppTab(rawValue: value), sidebarSelection != tab {
|
||||
sidebarSelection = tab
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private func iPadDetailView(for tab: AppTab) -> some View {
|
||||
switch tab {
|
||||
case .dashboard:
|
||||
bannerInsetView(DashboardView())
|
||||
case .sources:
|
||||
bannerInsetView(SourceListView(iapService: iapService))
|
||||
case .charts:
|
||||
bannerInsetView(ChartsContainerView(iapService: iapService))
|
||||
case .journal:
|
||||
bannerInsetView(JournalView())
|
||||
case .settings:
|
||||
bannerInsetView(SettingsView(iapService: iapService))
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - iPhone Layout
|
||||
|
||||
private var mainContent: some View {
|
||||
ZStack {
|
||||
TabView(selection: $tabSelection.selectedTab) {
|
||||
|
||||
Reference in New Issue
Block a user