What's New versionado y acumulativo (build 54)

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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WoScpmHdVj1aUf4rAp6hbe
This commit is contained in:
alexandrev-tibco
2026-07-10 08:06:49 +02:00
parent ed737bc290
commit 08094483ed
10 changed files with 347 additions and 65 deletions
+10 -10
View File
@@ -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;
+13 -2
View File
@@ -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
@@ -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.11.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 %@";
@@ -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.11.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 %@";
@@ -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.11.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 %@";
@@ -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.11.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 %@";
@@ -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.11.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 %@";
@@ -489,3 +489,25 @@
"guided_share_month" = "今月をシェア";
"guided_done_cta" = "完了";
"journal_no_note" = "まだメモがありません。";
// What's New catalog (1.4.11.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" = "バージョン%@の新機能";
@@ -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.11.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 %@";
@@ -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..<max(aParts.count, bParts.count) {
let x = i < aParts.count ? aParts[i] : 0
let y = i < bParts.count ? bParts[i] : 0
if x != y { return x > 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()