From 08094483edcd5b2a32833b37c7b731e84a8832d2 Mon Sep 17 00:00:00 2001 From: alexandrev-tibco Date: Fri, 10 Jul 2026 08:06:49 +0200 Subject: [PATCH] What's New versionado y acumulativo (build 54) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit El modal estaba congelado en las features de 1.4.0 (hardcodeadas en la vista) y el título decía 'What's New in 1.4' fijo. Rediseñado como catálogo por versión: - WhatsNewCatalog: una entrada por release (1.4.0/1.4.1/1.4.2/1.5.0) con sus features; AÑADIR ENTRADA CON CADA RELEASE (comentario en el código). - Diff ACUMULATIVO desde lastSeenWhatsNewVersion: saltar 1.3.x → 1.4.2 muestra 1.4.0 + 1.4.1 + 1.4.2 agrupadas con badge de versión (headers solo si hay más de una release pendiente). Comparación numérica de versiones (1.10 > 1.9). - lastSeen vacío + onboarding ya completado (usuarios pre-1.4.0) = mostrar todo el catálogo; instalación nueva = lastSeen sella a la versión actual al completar onboarding (un usuario recién onboardeado no ve 'novedades'). - Sin entrada en el catálogo para el salto → no se muestra sheet. - Título dinámico (whats_new_title_format) y contenido scrollable. - Strings de 4 releases + título ×7 idiomas. Verificado en simulador con -lastSeenWhatsNewVersion 1.4.0: pending [1.5.0, 1.4.2, 1.4.1] y sheet agrupada correcta. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01WoScpmHdVj1aUf4rAp6hbe --- PortfolioJournal.xcodeproj/project.pbxproj | 20 +- PortfolioJournal/App/ContentView.swift | 15 +- .../Resources/de.lproj/Localizable.strings | 22 ++ .../Resources/en.lproj/Localizable.strings | 22 ++ .../Resources/es-ES.lproj/Localizable.strings | 22 ++ .../Resources/fr.lproj/Localizable.strings | 22 ++ .../Resources/it.lproj/Localizable.strings | 22 ++ .../Resources/ja.lproj/Localizable.strings | 22 ++ .../Resources/pt-BR.lproj/Localizable.strings | 22 ++ .../Views/Onboarding/WhatsNewView.swift | 223 +++++++++++++----- 10 files changed, 347 insertions(+), 65 deletions(-) diff --git a/PortfolioJournal.xcodeproj/project.pbxproj b/PortfolioJournal.xcodeproj/project.pbxproj index 0b368c8..abb7cde 100644 --- a/PortfolioJournal.xcodeproj/project.pbxproj +++ b/PortfolioJournal.xcodeproj/project.pbxproj @@ -541,7 +541,7 @@ CODE_SIGN_ENTITLEMENTS = PortfolioJournal/PortfolioJournalDebug.entitlements; ENABLE_USER_SCRIPT_SANDBOXING = NO; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 53; + CURRENT_PROJECT_VERSION = 54; DEVELOPMENT_ASSET_PATHS = PortfolioJournal/Assets.xcassets; DEVELOPMENT_TEAM = 2825Q76T7H; ENABLE_PREVIEWS = YES; @@ -582,7 +582,7 @@ CODE_SIGN_IDENTITY = "Apple Distribution"; CODE_SIGN_STYLE = Manual; PROVISIONING_PROFILE_SPECIFIER = "porfoliojournal"; - CURRENT_PROJECT_VERSION = 53; + CURRENT_PROJECT_VERSION = 54; DEVELOPMENT_ASSET_PATHS = PortfolioJournal/Assets.xcassets; DEVELOPMENT_TEAM = 2825Q76T7H; ENABLE_PREVIEWS = YES; @@ -739,7 +739,7 @@ ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground; CODE_SIGN_ENTITLEMENTS = PortfolioJournalWidgetExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 53; + CURRENT_PROJECT_VERSION = 54; DEVELOPMENT_ASSET_PATHS = PortfolioJournalWidget/Assets.xcassets; DEVELOPMENT_TEAM = 2825Q76T7H; GENERATE_INFOPLIST_FILE = NO; @@ -774,7 +774,7 @@ CODE_SIGN_IDENTITY = "Apple Distribution"; CODE_SIGN_STYLE = Manual; PROVISIONING_PROFILE_SPECIFIER = "Portfolio Journalwidget"; - CURRENT_PROJECT_VERSION = 53; + CURRENT_PROJECT_VERSION = 54; DEVELOPMENT_ASSET_PATHS = PortfolioJournalWidget/Assets.xcassets; DEVELOPMENT_TEAM = 2825Q76T7H; GENERATE_INFOPLIST_FILE = NO; @@ -805,7 +805,7 @@ buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 53; + CURRENT_PROJECT_VERSION = 54; DEVELOPMENT_TEAM = 2825Q76T7H; GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 17.6; @@ -829,7 +829,7 @@ buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 53; + CURRENT_PROJECT_VERSION = 54; DEVELOPMENT_TEAM = 2825Q76T7H; GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 17.6; @@ -852,7 +852,7 @@ isa = XCBuildConfiguration; buildSettings = { CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 53; + CURRENT_PROJECT_VERSION = 54; DEVELOPMENT_TEAM = 2825Q76T7H; GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 17.6; @@ -875,7 +875,7 @@ isa = XCBuildConfiguration; buildSettings = { CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 53; + CURRENT_PROJECT_VERSION = 54; DEVELOPMENT_TEAM = 2825Q76T7H; GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 17.6; @@ -899,7 +899,7 @@ buildSettings = { CODE_SIGN_ENTITLEMENTS = PortfolioJournalQuickUpdateExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 53; + CURRENT_PROJECT_VERSION = 54; DEVELOPMENT_TEAM = 2825Q76T7H; GENERATE_INFOPLIST_FILE = NO; INFOPLIST_FILE = PortfolioJournalQuickUpdate/Info.plist; @@ -927,7 +927,7 @@ CODE_SIGN_ENTITLEMENTS = PortfolioJournalQuickUpdateExtension.entitlements; CODE_SIGN_IDENTITY = "Apple Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 53; + CURRENT_PROJECT_VERSION = 54; DEVELOPMENT_TEAM = 2825Q76T7H; PROVISIONING_PROFILE_SPECIFIER = "PortfolioJournal QuickUpdate AppStore"; GENERATE_INFOPLIST_FILE = NO; diff --git a/PortfolioJournal/App/ContentView.swift b/PortfolioJournal/App/ContentView.swift index 7f4c59b..44c1ed6 100644 --- a/PortfolioJournal/App/ContentView.swift +++ b/PortfolioJournal/App/ContentView.swift @@ -113,7 +113,11 @@ struct ContentView: View { } .task { await waitForDataAndResolveOnboarding() - if (resolvedOnboardingCompleted == true) && currentVersion != lastSeenWhatsNewVersion { + // Cumulative What's New: everything between the version the user + // comes from and the one they just installed. Empty catalog diff + // (e.g. patch release without entry) → no sheet. + if (resolvedOnboardingCompleted == true) && currentVersion != lastSeenWhatsNewVersion, + !WhatsNewCatalog.pendingReleases(since: lastSeenWhatsNewVersion, current: currentVersion).isEmpty { // Small delay to let the UI settle before showing sheet try? await Task.sleep(nanoseconds: 300_000_000) showingWhatsNew = true @@ -121,11 +125,18 @@ struct ContentView: View { } .onChange(of: onboardingCompleted) { _, completed in resolvedOnboardingCompleted = completed + // Fresh install: nothing is "new" for a user who just onboarded. + if completed && lastSeenWhatsNewVersion.isEmpty { + lastSeenWhatsNewVersion = currentVersion + } } .sheet(isPresented: $showingWhatsNew, onDismiss: { lastSeenWhatsNewVersion = currentVersion }) { - WhatsNewView() + WhatsNewView(releases: WhatsNewCatalog.pendingReleases( + since: lastSeenWhatsNewVersion, + current: currentVersion + )) } .onReceive(NotificationCenter.default.publisher(for: .openBatchUpdate)) { _ in tabSelection.selectedTab = 1 diff --git a/PortfolioJournal/Resources/de.lproj/Localizable.strings b/PortfolioJournal/Resources/de.lproj/Localizable.strings index 46271f5..e2ab451 100644 --- a/PortfolioJournal/Resources/de.lproj/Localizable.strings +++ b/PortfolioJournal/Resources/de.lproj/Localizable.strings @@ -489,3 +489,25 @@ "guided_share_month" = "Diesen Monat teilen"; "guided_done_cta" = "Fertig"; "journal_no_note" = "Noch keine Notiz."; + +// What's New catalog (1.4.1–1.5.0) +"whats_new_version_header" = "Version %@"; +"whats_new_150_redesign_title" = "Ein ruhigeres, klareres Design"; +"whats_new_150_redesign_body" = "Neu gestaltetes Home mit klarem Fokus, deinem Check-in im Mittelpunkt und weniger Ablenkung."; +"whats_new_150_guided_title" = "Geführter Monats-Check-in"; +"whats_new_150_guided_body" = "Eine Quelle pro Schritt, Live-Deltas, kurze Reflexion — dein 5-Minuten-Ritual, neu gedacht."; +"whats_new_150_rings_title" = "Ziel-Ringe & Journal-Timeline"; +"whats_new_150_rings_body" = "Fortschrittsringe im Fitness-Stil für Ziele und eine Stimmungs-Timeline für deine Investment-Geschichte."; +"whats_new_142_ocr_title" = "Update per Screenshot"; +"whats_new_142_ocr_body" = "Teile einen Screenshot deiner Bank-App — der Saldo wird auf dem Gerät erkannt, nichts verlässt dein iPhone."; +"whats_new_142_siri_title" = "Siri auf Deutsch"; +"whats_new_142_siri_body" = "\"Erfasse den Saldo von Indexa\" — Werte per Sprache oder Kurzbefehle erfassen, ohne die App zu öffnen."; +"whats_new_142_charts_title" = "Diagramme, neu gebaut"; +"whats_new_142_charts_body" = "Native Seitenleiste auf dem iPad, passende KPIs pro Diagramm und Teilen als schönes Bild."; +"whats_new_141_extension_title" = "Aktualisieren ohne App-Wechsel"; +"whats_new_141_extension_body" = "Teile einen Wert aus deiner Bank-App direkt in Portfolio Journal mit der neuen Erweiterung."; +"whats_new_141_ipad_title" = "Besser auf dem iPad"; +"whats_new_141_ipad_body" = "Neu gestaltete Diagramme für den großen Bildschirm, mit Zoom und Wertbeschriftungen."; +"whats_new_141_icloud_title" = "Mehr Sync"; +"whats_new_141_icloud_body" = "Monatliche Beiträge und Allokationsziele werden jetzt über iCloud synchronisiert."; +"whats_new_title_format" = "Neu in %@"; diff --git a/PortfolioJournal/Resources/en.lproj/Localizable.strings b/PortfolioJournal/Resources/en.lproj/Localizable.strings index 96be0eb..8eff90e 100644 --- a/PortfolioJournal/Resources/en.lproj/Localizable.strings +++ b/PortfolioJournal/Resources/en.lproj/Localizable.strings @@ -490,3 +490,25 @@ "guided_share_month" = "Share this month"; "guided_done_cta" = "Done"; "journal_no_note" = "No note yet."; + +// What's New catalog (1.4.1–1.5.0) +"whats_new_version_header" = "Version %@"; +"whats_new_150_redesign_title" = "A calmer, cleaner look"; +"whats_new_150_redesign_body" = "Redesigned Home with a clear hero, your check-in front and center, and less noise everywhere."; +"whats_new_150_guided_title" = "Guided monthly check-in"; +"whats_new_150_guided_body" = "One source per step, live deltas, a quick reflection — your 5-minute ritual, reimagined."; +"whats_new_150_rings_title" = "Goal rings & journal timeline"; +"whats_new_150_rings_body" = "Fitness-style progress rings for goals and a mood timeline for your investing story."; +"whats_new_142_ocr_title" = "Update from a screenshot"; +"whats_new_142_ocr_body" = "Share a screenshot of your bank app — the balance is recognized on-device, nothing leaves your phone."; +"whats_new_142_siri_title" = "Siri in your language"; +"whats_new_142_siri_body" = "\"Log my Indexa balance\" — record values by voice or from Shortcuts, no app needed."; +"whats_new_142_charts_title" = "Charts, rebuilt"; +"whats_new_142_charts_body" = "Native sidebar on iPad, smart KPIs per chart, and a share button with beautiful branded images."; +"whats_new_141_extension_title" = "Update without switching apps"; +"whats_new_141_extension_body" = "Share a value from your bank app straight into Portfolio Journal with the new share extension."; +"whats_new_141_ipad_title" = "Better on iPad"; +"whats_new_141_ipad_body" = "Redesigned charts for the big screen, with pinch-to-zoom and value labels."; +"whats_new_141_icloud_title" = "More in sync"; +"whats_new_141_icloud_body" = "Monthly contributions and allocation targets now sync across your devices via iCloud."; +"whats_new_title_format" = "What's New in %@"; diff --git a/PortfolioJournal/Resources/es-ES.lproj/Localizable.strings b/PortfolioJournal/Resources/es-ES.lproj/Localizable.strings index ce5625d..0f6085e 100644 --- a/PortfolioJournal/Resources/es-ES.lproj/Localizable.strings +++ b/PortfolioJournal/Resources/es-ES.lproj/Localizable.strings @@ -441,3 +441,25 @@ "guided_share_month" = "Compartir este mes"; "guided_done_cta" = "Hecho"; "journal_no_note" = "Sin nota todavía."; + +// What's New catalog (1.4.1–1.5.0) +"whats_new_version_header" = "Versión %@"; +"whats_new_150_redesign_title" = "Un look más limpio y tranquilo"; +"whats_new_150_redesign_body" = "Home rediseñado con tu valor total protagonista, el check-in en el centro y menos ruido en todas partes."; +"whats_new_150_guided_title" = "Check-in mensual guiado"; +"whats_new_150_guided_body" = "Una fuente por paso, deltas en vivo y una reflexión rápida — tu ritual de 5 minutos, reimaginado."; +"whats_new_150_rings_title" = "Anillos de metas y timeline"; +"whats_new_150_rings_body" = "Anillos de progreso estilo Fitness para tus metas y un timeline con tu historia como inversor."; +"whats_new_142_ocr_title" = "Actualiza desde un pantallazo"; +"whats_new_142_ocr_body" = "Comparte una captura de tu banco — el saldo se reconoce en el dispositivo, nada sale de tu iPhone."; +"whats_new_142_siri_title" = "Siri en tu idioma"; +"whats_new_142_siri_body" = "\"Registra el saldo de Indexa\" — apunta valores por voz o desde Atajos, sin abrir la app."; +"whats_new_142_charts_title" = "Gráficas renovadas"; +"whats_new_142_charts_body" = "Sidebar nativa en iPad, KPIs inteligentes por gráfica y botón de compartir con imágenes con estilo."; +"whats_new_141_extension_title" = "Actualiza sin cambiar de app"; +"whats_new_141_extension_body" = "Comparte un valor desde la app de tu banco directo a Portfolio Journal con la nueva extensión."; +"whats_new_141_ipad_title" = "Mejor en iPad"; +"whats_new_141_ipad_body" = "Gráficas rediseñadas para pantalla grande, con zoom y etiquetas de valor."; +"whats_new_141_icloud_title" = "Más sincronizado"; +"whats_new_141_icloud_body" = "Las aportaciones mensuales y los objetivos de asignación ahora se sincronizan vía iCloud."; +"whats_new_title_format" = "Novedades de la %@"; diff --git a/PortfolioJournal/Resources/fr.lproj/Localizable.strings b/PortfolioJournal/Resources/fr.lproj/Localizable.strings index 4042ec0..c50d2d2 100644 --- a/PortfolioJournal/Resources/fr.lproj/Localizable.strings +++ b/PortfolioJournal/Resources/fr.lproj/Localizable.strings @@ -489,3 +489,25 @@ "guided_share_month" = "Partager ce mois"; "guided_done_cta" = "Terminé"; "journal_no_note" = "Pas encore de note."; + +// What's New catalog (1.4.1–1.5.0) +"whats_new_version_header" = "Version %@"; +"whats_new_150_redesign_title" = "Un design plus calme et épuré"; +"whats_new_150_redesign_body" = "Accueil repensé avec votre valeur totale en vedette, le check-in au centre et moins de bruit partout."; +"whats_new_150_guided_title" = "Check-in mensuel guidé"; +"whats_new_150_guided_body" = "Une source par étape, des deltas en direct, une réflexion rapide — votre rituel de 5 minutes, réinventé."; +"whats_new_150_rings_title" = "Anneaux d'objectifs & timeline"; +"whats_new_150_rings_body" = "Des anneaux de progression style Fitness pour vos objectifs et une timeline de votre parcours d'investisseur."; +"whats_new_142_ocr_title" = "Mise à jour par capture d'écran"; +"whats_new_142_ocr_body" = "Partagez une capture de votre banque — le solde est reconnu sur l'appareil, rien ne quitte votre iPhone."; +"whats_new_142_siri_title" = "Siri dans votre langue"; +"whats_new_142_siri_body" = "« Enregistre le solde de Indexa » — notez des valeurs à la voix ou depuis Raccourcis, sans ouvrir l'app."; +"whats_new_142_charts_title" = "Graphiques repensés"; +"whats_new_142_charts_body" = "Barre latérale native sur iPad, KPIs adaptés à chaque graphique et partage en belles images."; +"whats_new_141_extension_title" = "Mettre à jour sans changer d'app"; +"whats_new_141_extension_body" = "Partagez une valeur depuis votre app bancaire directement vers Portfolio Journal avec la nouvelle extension."; +"whats_new_141_ipad_title" = "Meilleur sur iPad"; +"whats_new_141_ipad_body" = "Graphiques repensés pour le grand écran, avec zoom et étiquettes de valeur."; +"whats_new_141_icloud_title" = "Plus synchronisé"; +"whats_new_141_icloud_body" = "Les contributions mensuelles et les objectifs d'allocation se synchronisent désormais via iCloud."; +"whats_new_title_format" = "Nouveautés de la %@"; diff --git a/PortfolioJournal/Resources/it.lproj/Localizable.strings b/PortfolioJournal/Resources/it.lproj/Localizable.strings index 0ffdd86..1f32007 100644 --- a/PortfolioJournal/Resources/it.lproj/Localizable.strings +++ b/PortfolioJournal/Resources/it.lproj/Localizable.strings @@ -489,3 +489,25 @@ "guided_share_month" = "Condividi questo mese"; "guided_done_cta" = "Fatto"; "journal_no_note" = "Ancora nessuna nota."; + +// What's New catalog (1.4.1–1.5.0) +"whats_new_version_header" = "Versione %@"; +"whats_new_150_redesign_title" = "Un look più calmo e pulito"; +"whats_new_150_redesign_body" = "Home ridisegnata con il tuo valore totale in primo piano, il check-in al centro e meno rumore ovunque."; +"whats_new_150_guided_title" = "Check-in mensile guidato"; +"whats_new_150_guided_body" = "Una fonte per passo, delta in tempo reale, una riflessione veloce — il tuo rituale di 5 minuti, ripensato."; +"whats_new_150_rings_title" = "Anelli obiettivi & timeline"; +"whats_new_150_rings_body" = "Anelli di progresso stile Fitness per gli obiettivi e una timeline della tua storia da investitore."; +"whats_new_142_ocr_title" = "Aggiorna da uno screenshot"; +"whats_new_142_ocr_body" = "Condividi uno screenshot della tua banca — il saldo viene riconosciuto sul dispositivo, nulla lascia il tuo iPhone."; +"whats_new_142_siri_title" = "Siri nella tua lingua"; +"whats_new_142_siri_body" = "\"Registra il saldo di Indexa\" — registra valori con la voce o dai Comandi rapidi, senza aprire l'app."; +"whats_new_142_charts_title" = "Grafici rinnovati"; +"whats_new_142_charts_body" = "Barra laterale nativa su iPad, KPI su misura per ogni grafico e condivisione come belle immagini."; +"whats_new_141_extension_title" = "Aggiorna senza cambiare app"; +"whats_new_141_extension_body" = "Condividi un valore dall'app della tua banca direttamente in Portfolio Journal con la nuova estensione."; +"whats_new_141_ipad_title" = "Meglio su iPad"; +"whats_new_141_ipad_body" = "Grafici ridisegnati per lo schermo grande, con zoom ed etichette dei valori."; +"whats_new_141_icloud_title" = "Più sincronizzato"; +"whats_new_141_icloud_body" = "I contributi mensili e gli obiettivi di allocazione ora si sincronizzano via iCloud."; +"whats_new_title_format" = "Novità della %@"; diff --git a/PortfolioJournal/Resources/ja.lproj/Localizable.strings b/PortfolioJournal/Resources/ja.lproj/Localizable.strings index f0c21d9..7b71a3f 100644 --- a/PortfolioJournal/Resources/ja.lproj/Localizable.strings +++ b/PortfolioJournal/Resources/ja.lproj/Localizable.strings @@ -489,3 +489,25 @@ "guided_share_month" = "今月をシェア"; "guided_done_cta" = "完了"; "journal_no_note" = "まだメモがありません。"; + +// What's New catalog (1.4.1–1.5.0) +"whats_new_version_header" = "バージョン%@"; +"whats_new_150_redesign_title" = "より穏やかで洗練されたデザイン"; +"whats_new_150_redesign_body" = "ホームを刷新。総資産を主役に、チェックインを中心に、あらゆる場所のノイズを削減しました。"; +"whats_new_150_guided_title" = "ガイド付き月次チェックイン"; +"whats_new_150_guided_body" = "1画面1ソース、リアルタイム差分、簡単な振り返り — 月5分の習慣を再設計しました。"; +"whats_new_150_rings_title" = "目標リングとジャーナルタイムライン"; +"whats_new_150_rings_body" = "フィットネス風の進捗リングと、投資の歩みを綴るムードタイムライン。"; +"whats_new_142_ocr_title" = "スクリーンショットから更新"; +"whats_new_142_ocr_body" = "銀行アプリのスクリーンショットを共有するだけ。残高はデバイス上で認識され、外部には送信されません。"; +"whats_new_142_siri_title" = "Siriに対応"; +"whats_new_142_siri_body" = "「Indexaの残高を記録」— アプリを開かずに音声やショートカットで記録できます。"; +"whats_new_142_charts_title" = "チャートを一新"; +"whats_new_142_charts_body" = "iPadのネイティブサイドバー、チャートごとの最適なKPI、美しい画像での共有ボタン。"; +"whats_new_141_extension_title" = "アプリを切り替えずに更新"; +"whats_new_141_extension_body" = "新しい共有拡張機能で、銀行アプリから直接Portfolio Journalへ値を送れます。"; +"whats_new_141_ipad_title" = "iPadでより快適に"; +"whats_new_141_ipad_body" = "大画面向けにチャートを再設計。ピンチズームと値ラベルに対応。"; +"whats_new_141_icloud_title" = "さらに同期"; +"whats_new_141_icloud_body" = "毎月の積立額と配分目標がiCloudでデバイス間同期されるようになりました。"; +"whats_new_title_format" = "バージョン%@の新機能"; diff --git a/PortfolioJournal/Resources/pt-BR.lproj/Localizable.strings b/PortfolioJournal/Resources/pt-BR.lproj/Localizable.strings index ed90269..6b09798 100644 --- a/PortfolioJournal/Resources/pt-BR.lproj/Localizable.strings +++ b/PortfolioJournal/Resources/pt-BR.lproj/Localizable.strings @@ -489,3 +489,25 @@ "guided_share_month" = "Compartilhar este mês"; "guided_done_cta" = "Concluído"; "journal_no_note" = "Ainda sem nota."; + +// What's New catalog (1.4.1–1.5.0) +"whats_new_version_header" = "Versão %@"; +"whats_new_150_redesign_title" = "Um visual mais calmo e limpo"; +"whats_new_150_redesign_body" = "Início redesenhado com seu valor total em destaque, o check-in no centro e menos ruído em tudo."; +"whats_new_150_guided_title" = "Check-in mensal guiado"; +"whats_new_150_guided_body" = "Uma fonte por etapa, deltas ao vivo e uma reflexão rápida — seu ritual de 5 minutos, reinventado."; +"whats_new_150_rings_title" = "Anéis de metas & timeline"; +"whats_new_150_rings_body" = "Anéis de progresso estilo Fitness para metas e uma timeline da sua história como investidor."; +"whats_new_142_ocr_title" = "Atualize com uma captura de tela"; +"whats_new_142_ocr_body" = "Compartilhe uma captura do seu banco — o saldo é reconhecido no aparelho, nada sai do seu iPhone."; +"whats_new_142_siri_title" = "Siri no seu idioma"; +"whats_new_142_siri_body" = "\"Registre o saldo da Indexa\" — anote valores por voz ou pelos Atalhos, sem abrir o app."; +"whats_new_142_charts_title" = "Gráficos renovados"; +"whats_new_142_charts_body" = "Barra lateral nativa no iPad, KPIs sob medida por gráfico e compartilhamento com belas imagens."; +"whats_new_141_extension_title" = "Atualize sem trocar de app"; +"whats_new_141_extension_body" = "Compartilhe um valor do app do seu banco direto no Portfolio Journal com a nova extensão."; +"whats_new_141_ipad_title" = "Melhor no iPad"; +"whats_new_141_ipad_body" = "Gráficos redesenhados para a tela grande, com zoom e rótulos de valor."; +"whats_new_141_icloud_title" = "Mais sincronizado"; +"whats_new_141_icloud_body" = "Aportes mensais e metas de alocação agora sincronizam via iCloud."; +"whats_new_title_format" = "Novidades da %@"; diff --git a/PortfolioJournal/Views/Onboarding/WhatsNewView.swift b/PortfolioJournal/Views/Onboarding/WhatsNewView.swift index b3c1c60..e8c7b0a 100644 --- a/PortfolioJournal/Views/Onboarding/WhatsNewView.swift +++ b/PortfolioJournal/Views/Onboarding/WhatsNewView.swift @@ -7,75 +7,192 @@ struct WhatsNewFeature { let description: String } +struct WhatsNewRelease: Identifiable { + let version: String + let features: [WhatsNewFeature] + var id: String { version } +} + +// MARK: - Versioned catalog +// +// One entry per released version, newest first. The sheet shows the CUMULATIVE +// diff between the version the user is coming from (lastSeenWhatsNewVersion) +// and the one they just installed — jumping 1.3.x → 1.4.2 shows 1.4.0 + 1.4.1 +// + 1.4.2, not just the last step. ADD AN ENTRY HERE WITH EVERY RELEASE. +enum WhatsNewCatalog { + static var releases: [WhatsNewRelease] { + [ + WhatsNewRelease(version: "1.5.0", features: [ + WhatsNewFeature( + icon: "sparkles", + color: .appPrimary, + title: String(localized: "whats_new_150_redesign_title"), + description: String(localized: "whats_new_150_redesign_body") + ), + WhatsNewFeature( + icon: "figure.mind.and.body", + color: .positiveGreen, + title: String(localized: "whats_new_150_guided_title"), + description: String(localized: "whats_new_150_guided_body") + ), + WhatsNewFeature( + icon: "circle.dashed.inset.filled", + color: .orange, + title: String(localized: "whats_new_150_rings_title"), + description: String(localized: "whats_new_150_rings_body") + ) + ]), + WhatsNewRelease(version: "1.4.2", features: [ + WhatsNewFeature( + icon: "text.viewfinder", + color: .appPrimary, + title: String(localized: "whats_new_142_ocr_title"), + description: String(localized: "whats_new_142_ocr_body") + ), + WhatsNewFeature( + icon: "mic.fill", + color: .purple, + title: String(localized: "whats_new_142_siri_title"), + description: String(localized: "whats_new_142_siri_body") + ), + WhatsNewFeature( + icon: "chart.xyaxis.line", + color: .appSecondary, + title: String(localized: "whats_new_142_charts_title"), + description: String(localized: "whats_new_142_charts_body") + ) + ]), + WhatsNewRelease(version: "1.4.1", features: [ + WhatsNewFeature( + icon: "square.and.arrow.up.circle.fill", + color: .appPrimary, + title: String(localized: "whats_new_141_extension_title"), + description: String(localized: "whats_new_141_extension_body") + ), + WhatsNewFeature( + icon: "ipad.landscape", + color: .appSecondary, + title: String(localized: "whats_new_141_ipad_title"), + description: String(localized: "whats_new_141_ipad_body") + ), + WhatsNewFeature( + icon: "icloud.fill", + color: .cyan, + title: String(localized: "whats_new_141_icloud_title"), + description: String(localized: "whats_new_141_icloud_body") + ) + ]), + WhatsNewRelease(version: "1.4.0", features: [ + WhatsNewFeature( + icon: "bolt.circle.fill", + color: .appPrimary, + title: String(localized: "whats_new_quick_update_title"), + description: String(localized: "whats_new_quick_update_body") + ), + WhatsNewFeature( + icon: "flame.fill", + color: .orange, + title: String(localized: "whats_new_streak_title"), + description: String(localized: "whats_new_streak_body") + ), + WhatsNewFeature( + icon: "target", + color: .appSecondary, + title: String(localized: "whats_new_goals_title"), + description: String(localized: "whats_new_goals_body") + ) + ]) + ] + } + + /// Releases the user hasn't seen: newer than `since`, no newer than `current`. + /// Newest first. `since` empty (pre-1.4.0 installs, before the key existed) + /// is treated as "show everything in the catalog". + static func pendingReleases(since: String, current: String) -> [WhatsNewRelease] { + releases.filter { release in + (since.isEmpty || compare(release.version, isNewerThan: since)) + && !compare(release.version, isNewerThan: current) + } + } + + /// Numeric semver-style comparison ("1.10.0" > "1.9.0"). + static func compare(_ a: String, isNewerThan b: String) -> Bool { + let aParts = a.split(separator: ".").map { Int($0) ?? 0 } + let bParts = b.split(separator: ".").map { Int($0) ?? 0 } + for i in 0.. y } + } + return false + } +} + +// MARK: - Sheet + struct WhatsNewView: View { + let releases: [WhatsNewRelease] @Environment(\.dismiss) private var dismiss - private let features: [WhatsNewFeature] = [ - WhatsNewFeature( - icon: "bolt.circle.fill", - color: .appPrimary, - title: String(localized: "whats_new_quick_update_title"), - description: String(localized: "whats_new_quick_update_body") - ), - WhatsNewFeature( - icon: "flame.fill", - color: .orange, - title: String(localized: "whats_new_streak_title"), - description: String(localized: "whats_new_streak_body") - ), - WhatsNewFeature( - icon: "target", - color: .appSecondary, - title: String(localized: "whats_new_goals_title"), - description: String(localized: "whats_new_goals_body") - ) - ] + /// Version headers only add value when the user skipped several releases. + private var showsVersionHeaders: Bool { releases.count > 1 } var body: some View { ZStack { AppBackground() VStack(spacing: 0) { - Spacer() + VStack(spacing: 8) { + Text("🎉") + .font(.system(size: 48)) + Text(String(format: String(localized: "whats_new_title_format"), releases.first?.version ?? "")) + .font(.title.weight(.bold)) + Text(String(localized: "whats_new_subtitle")) + .font(.subheadline) + .foregroundColor(.secondary) + .multilineTextAlignment(.center) + } + .padding(.top, 32) + .padding(.horizontal) - VStack(spacing: 32) { - VStack(spacing: 8) { - Text("🎉") - .font(.system(size: 56)) - Text(String(localized: "whats_new_title")) - .font(.title.weight(.bold)) - Text(String(localized: "whats_new_subtitle")) - .font(.subheadline) - .foregroundColor(.secondary) - .multilineTextAlignment(.center) - } - - VStack(spacing: 20) { - ForEach(features, id: \.title) { feature in - HStack(spacing: 16) { - Image(systemName: feature.icon) - .font(.title2) - .foregroundColor(feature.color) - .frame(width: 40) - VStack(alignment: .leading, spacing: 2) { - Text(feature.title) - .font(.subheadline.weight(.semibold)) - Text(feature.description) - .font(.caption) - .foregroundColor(.secondary) + ScrollView { + VStack(spacing: 16) { + ForEach(releases) { release in + VStack(alignment: .leading, spacing: 14) { + if showsVersionHeaders { + Text(String(format: String(localized: "whats_new_version_header"), release.version)) + .font(.caption.weight(.bold)) + .foregroundColor(.appPrimary) + .padding(.horizontal, 10) + .padding(.vertical, 4) + .background(Color.appPrimary.opacity(0.12)) + .clipShape(Capsule()) + } + ForEach(release.features, id: \.title) { feature in + HStack(spacing: 16) { + Image(systemName: feature.icon) + .font(.title2) + .foregroundColor(feature.color) + .frame(width: 40) + VStack(alignment: .leading, spacing: 2) { + Text(feature.title) + .font(.subheadline.weight(.semibold)) + Text(feature.description) + .font(.caption) + .foregroundColor(.secondary) + } + Spacer() + } } - Spacer() } + .padding() + .frame(maxWidth: .infinity, alignment: .leading) + .background(Color(.secondarySystemGroupedBackground)) + .cornerRadius(16) } } .padding() - .background(Color(.systemBackground)) - .cornerRadius(16) - .shadow(color: .black.opacity(0.05), radius: 8, y: 2) } - .padding() - - Spacer() Button { dismiss()