Files
InvestmentTrackerApp/PortfolioJournal/Views/Onboarding/WhatsNewView.swift
T
alexandrev-tibco 6774d76146 Biometría genérica (Face ID / Touch ID / Optic ID) y What's New 1.7.0
- AppLockService.biometryKind lee LAContext.biometryType; textos e icono del
  bloqueo y de la privacidad de saldos ya no asumen Face ID (Duo puede llevar
  Touch ID). Nuevas claves biometric_* en los 7 idiomas.
- Entrada 1.7.0 en el catálogo What's New (Duo, inspector, biometría) con
  strings en los 7 idiomas.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F1u4K16xy7eQVtgsYNZ9Vn
2026-09-16 11:44:50 +02:00

234 lines
10 KiB
Swift

import SwiftUI
struct WhatsNewFeature {
let icon: String
let color: Color
let title: String
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.7.0", features: [
WhatsNewFeature(
icon: "iphone.gen3.and.iphone.gen3",
color: .appPrimary,
title: String(localized: "whats_new_170_duo_title"),
description: String(localized: "whats_new_170_duo_body")
),
WhatsNewFeature(
icon: "rectangle.trailinghalf.inset.filled",
color: .appSecondary,
title: String(localized: "whats_new_170_inspector_title"),
description: String(localized: "whats_new_170_inspector_body")
),
WhatsNewFeature(
icon: "touchid",
color: .positiveGreen,
title: String(localized: "whats_new_170_biometrics_title"),
description: String(localized: "whats_new_170_biometrics_body")
)
]),
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
/// 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) {
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)
.foregroundStyle(.secondary)
.multilineTextAlignment(.center)
}
.padding(.top, 32)
.padding(.horizontal)
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))
.foregroundStyle(Color.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)
.foregroundStyle(feature.color)
.frame(width: 40)
VStack(alignment: .leading, spacing: 2) {
Text(feature.title)
.font(.subheadline.weight(.semibold))
Text(feature.description)
.font(.caption)
.foregroundStyle(.secondary)
}
Spacer()
}
}
}
.padding()
.frame(maxWidth: .infinity, alignment: .leading)
.background(Color(.secondarySystemGroupedBackground))
.clipShape(RoundedRectangle(cornerRadius: 16))
}
}
.padding()
}
Button {
dismiss()
} label: {
Text(String(localized: "whats_new_continue"))
.font(.headline)
.foregroundStyle(.white)
.frame(maxWidth: .infinity)
.padding()
.background(Color.appPrimary)
.clipShape(RoundedRectangle(cornerRadius: 14))
}
.padding()
}
}
.interactiveDismissDisabled()
}
}