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
This commit is contained in:
@@ -22,6 +22,26 @@ struct WhatsNewRelease: Identifiable {
|
||||
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",
|
||||
@@ -149,7 +169,7 @@ struct WhatsNewView: View {
|
||||
.font(.title.weight(.bold))
|
||||
Text(String(localized: "whats_new_subtitle"))
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.secondary)
|
||||
.foregroundStyle(.secondary)
|
||||
.multilineTextAlignment(.center)
|
||||
}
|
||||
.padding(.top, 32)
|
||||
@@ -162,7 +182,7 @@ struct WhatsNewView: View {
|
||||
if showsVersionHeaders {
|
||||
Text(String(format: String(localized: "whats_new_version_header"), release.version))
|
||||
.font(.caption.weight(.bold))
|
||||
.foregroundColor(.appPrimary)
|
||||
.foregroundStyle(Color.appPrimary)
|
||||
.padding(.horizontal, 10)
|
||||
.padding(.vertical, 4)
|
||||
.background(Color.appPrimary.opacity(0.12))
|
||||
@@ -172,14 +192,14 @@ struct WhatsNewView: View {
|
||||
HStack(spacing: 16) {
|
||||
Image(systemName: feature.icon)
|
||||
.font(.title2)
|
||||
.foregroundColor(feature.color)
|
||||
.foregroundStyle(feature.color)
|
||||
.frame(width: 40)
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(feature.title)
|
||||
.font(.subheadline.weight(.semibold))
|
||||
Text(feature.description)
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
@@ -188,7 +208,7 @@ struct WhatsNewView: View {
|
||||
.padding()
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.background(Color(.secondarySystemGroupedBackground))
|
||||
.cornerRadius(16)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 16))
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
@@ -199,11 +219,11 @@ struct WhatsNewView: View {
|
||||
} label: {
|
||||
Text(String(localized: "whats_new_continue"))
|
||||
.font(.headline)
|
||||
.foregroundColor(.white)
|
||||
.foregroundStyle(.white)
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding()
|
||||
.background(Color.appPrimary)
|
||||
.cornerRadius(14)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 14))
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ struct AppLockView: View {
|
||||
@State private var errorMessage: String?
|
||||
@State private var didAttemptBiometrics = false
|
||||
@FocusState private var pinFocused: Bool
|
||||
private let biometry = AppLockService.biometryKind
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
@@ -28,7 +29,7 @@ struct AppLockView: View {
|
||||
|
||||
Text("Unlock Portfolio Journal to view your data.")
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.secondary)
|
||||
.foregroundStyle(.secondary)
|
||||
.multilineTextAlignment(.center)
|
||||
.padding(.horizontal, 30)
|
||||
|
||||
@@ -36,13 +37,14 @@ struct AppLockView: View {
|
||||
Button {
|
||||
authenticateWithBiometrics()
|
||||
} label: {
|
||||
Label("Unlock with Face ID", systemImage: "faceid")
|
||||
Label(String(format: String(localized: "biometric_unlock_with"), biometry.displayName),
|
||||
systemImage: biometry.systemImage)
|
||||
.font(.headline)
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding()
|
||||
.background(Color.appPrimary)
|
||||
.foregroundColor(.white)
|
||||
.cornerRadius(AppConstants.UI.cornerRadius)
|
||||
.foregroundStyle(.white)
|
||||
.clipShape(RoundedRectangle(cornerRadius: AppConstants.UI.cornerRadius))
|
||||
}
|
||||
.padding(.horizontal, 24)
|
||||
}
|
||||
@@ -56,7 +58,7 @@ struct AppLockView: View {
|
||||
.font(.title3.weight(.semibold))
|
||||
.padding()
|
||||
.background(Color.gray.opacity(0.1))
|
||||
.cornerRadius(12)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 12))
|
||||
.focused($pinFocused)
|
||||
.onChange(of: pin) { _, newValue in
|
||||
pin = String(newValue.filter(\.isNumber).prefix(4))
|
||||
@@ -78,7 +80,7 @@ struct AppLockView: View {
|
||||
if let errorMessage {
|
||||
Text(errorMessage)
|
||||
.font(.caption)
|
||||
.foregroundColor(.negativeRed)
|
||||
.foregroundStyle(Color.negativeRed)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
@@ -101,7 +103,7 @@ struct AppLockView: View {
|
||||
} else if pinEnabled {
|
||||
pinFocused = true
|
||||
} else {
|
||||
errorMessage = "Face ID failed. Please try again."
|
||||
errorMessage = String(format: String(localized: "biometric_failed"), biometry.displayName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -149,20 +149,20 @@ struct SettingsView: View {
|
||||
} message: {
|
||||
Text("Restart the app to apply iCloud sync changes.")
|
||||
}
|
||||
.alert("Face ID Unavailable", isPresented: $showingBiometricAlert) {
|
||||
.alert(Text(String(format: String(localized: "biometric_unavailable_title"), biometryName)), isPresented: $showingBiometricAlert) {
|
||||
Button("OK") {}
|
||||
} message: {
|
||||
Text("Face ID isn't available on this device.")
|
||||
Text(String(format: String(localized: "biometric_unavailable_body"), biometryName))
|
||||
}
|
||||
.alert("PIN Required", isPresented: $showingPinRequiredAlert) {
|
||||
Button("OK") {}
|
||||
} message: {
|
||||
Text("Enable a PIN before turning on Face ID.")
|
||||
Text(String(format: String(localized: "biometric_pin_required"), biometryName))
|
||||
}
|
||||
.alert("Disable Face ID First", isPresented: $showingPinDisableAlert) {
|
||||
.alert(Text(String(format: String(localized: "biometric_disable_first_title"), biometryName)), isPresented: $showingPinDisableAlert) {
|
||||
Button("OK") {}
|
||||
} message: {
|
||||
Text("Turn off Face ID before disabling your PIN.")
|
||||
Text(String(format: String(localized: "biometric_disable_first_body"), biometryName))
|
||||
}
|
||||
.sheet(isPresented: $showingPinSetup) {
|
||||
PinSetupView(title: "Set PIN") { pin in
|
||||
@@ -184,7 +184,7 @@ struct SettingsView: View {
|
||||
}
|
||||
}
|
||||
.sheet(isPresented: $showingPinVerifyForFaceId) {
|
||||
PinVerifyView(title: "Enter PIN to Disable Face ID") { success in
|
||||
PinVerifyView(title: String(format: String(localized: "biometric_pin_to_disable"), biometryName)) { success in
|
||||
if success {
|
||||
faceIdEnabled = false
|
||||
}
|
||||
@@ -207,19 +207,19 @@ struct SettingsView: View {
|
||||
Link(destination: GoalShareService.appStoreURL) {
|
||||
HStack {
|
||||
Image(systemName: "arrow.down.circle.fill")
|
||||
.foregroundColor(.appPrimary)
|
||||
.foregroundStyle(Color.appPrimary)
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(String(localized: "update_available_title"))
|
||||
.font(.subheadline.weight(.semibold))
|
||||
if let v = updateService.latestVersion {
|
||||
Text(String(format: String(localized: "update_available_body"), v))
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
Spacer()
|
||||
Image(systemName: "chevron.right")
|
||||
.foregroundColor(.secondary)
|
||||
.foregroundStyle(.secondary)
|
||||
.font(.caption)
|
||||
}
|
||||
}
|
||||
@@ -237,14 +237,14 @@ struct SettingsView: View {
|
||||
.frame(width: 36, height: 36)
|
||||
.padding(6)
|
||||
.background(Color.appPrimary.opacity(0.08))
|
||||
.cornerRadius(10)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 10))
|
||||
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(appDisplayName)
|
||||
.font(.headline)
|
||||
Text("Long-term portfolio tracker")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
@@ -280,14 +280,14 @@ struct SettingsView: View {
|
||||
if viewModel.isFamilyShared {
|
||||
Text("Family Sharing")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
Image(systemName: "checkmark.seal.fill")
|
||||
.foregroundColor(.positiveGreen)
|
||||
.foregroundStyle(Color.positiveGreen)
|
||||
}
|
||||
} else {
|
||||
Button {
|
||||
@@ -300,23 +300,23 @@ struct SettingsView: View {
|
||||
.frame(width: 44, height: 44)
|
||||
|
||||
Image(systemName: "crown.fill")
|
||||
.foregroundColor(.appPrimary)
|
||||
.foregroundStyle(Color.appPrimary)
|
||||
}
|
||||
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text("Upgrade to Premium")
|
||||
.font(.headline)
|
||||
.foregroundColor(.primary)
|
||||
.foregroundStyle(.primary)
|
||||
|
||||
Text("Unlock all features for \(iapService.formattedPrice)")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
Image(systemName: "chevron.right")
|
||||
.foregroundColor(.secondary)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -345,7 +345,7 @@ struct SettingsView: View {
|
||||
Text("Notifications")
|
||||
Spacer()
|
||||
Text(viewModel.notificationsEnabled ? "Enabled" : "Disabled")
|
||||
.foregroundColor(.secondary)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
.contentShape(Rectangle())
|
||||
.onTapGesture {
|
||||
@@ -396,7 +396,7 @@ struct SettingsView: View {
|
||||
systemImage: "internaldrive"
|
||||
)
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.primary)
|
||||
.foregroundStyle(.primary)
|
||||
|
||||
Spacer()
|
||||
|
||||
@@ -411,27 +411,27 @@ struct SettingsView: View {
|
||||
if cloudStack.isSyncing {
|
||||
Label("Syncing with iCloud...", systemImage: "arrow.triangle.2.circlepath.icloud")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
.foregroundStyle(.secondary)
|
||||
} else {
|
||||
HStack(spacing: 12) {
|
||||
if let date = cloudStack.lastImportDate {
|
||||
Label("↓ \(date.formatted(.relative(presentation: .named, unitsStyle: .abbreviated)))", systemImage: "icloud.and.arrow.down")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
.foregroundStyle(.secondary)
|
||||
} else {
|
||||
Label("No import yet", systemImage: "icloud.and.arrow.down")
|
||||
.font(.caption)
|
||||
.foregroundColor(.orange)
|
||||
.foregroundStyle(.orange)
|
||||
}
|
||||
|
||||
if let date = cloudStack.lastExportDate {
|
||||
Label("↑ \(date.formatted(.relative(presentation: .named, unitsStyle: .abbreviated)))", systemImage: "icloud.and.arrow.up")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
.foregroundStyle(.secondary)
|
||||
} else {
|
||||
Label("No export yet", systemImage: "icloud.and.arrow.up")
|
||||
.font(.caption)
|
||||
.foregroundColor(.orange)
|
||||
.foregroundStyle(.orange)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -449,12 +449,12 @@ struct SettingsView: View {
|
||||
systemImage: critical ? "exclamationmark.icloud" : "arrow.triangle.2.circlepath.icloud"
|
||||
)
|
||||
.font(.caption.weight(.semibold))
|
||||
.foregroundColor(critical ? .red : .secondary)
|
||||
.foregroundStyle(critical ? .red : .secondary)
|
||||
|
||||
if critical, let hint = cloudStack.lastSyncErrorHint {
|
||||
Text(hint)
|
||||
.font(.caption)
|
||||
.foregroundColor(.primary)
|
||||
.foregroundStyle(.primary)
|
||||
}
|
||||
|
||||
// Technical detail is opt-in behind a disclosure, so it
|
||||
@@ -463,11 +463,11 @@ struct SettingsView: View {
|
||||
if let hint = cloudStack.lastSyncErrorHint, !critical {
|
||||
Text(hint)
|
||||
.font(.caption2)
|
||||
.foregroundColor(.secondary)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
Text(error)
|
||||
.font(.caption2)
|
||||
.foregroundColor(.secondary)
|
||||
.foregroundStyle(.secondary)
|
||||
.textSelection(.enabled)
|
||||
}
|
||||
.font(.caption2)
|
||||
@@ -484,7 +484,7 @@ struct SettingsView: View {
|
||||
} label: {
|
||||
Label(String(localized: "icloud_copy_diagnostics"), systemImage: "doc.on.doc")
|
||||
.font(.caption)
|
||||
.foregroundColor(.appPrimary)
|
||||
.foregroundStyle(Color.appPrimary)
|
||||
}
|
||||
|
||||
// Force upload — use when data exists locally but hasn't reached iCloud
|
||||
@@ -492,7 +492,7 @@ struct SettingsView: View {
|
||||
if let result = forceUploadResult {
|
||||
Label(result, systemImage: "checkmark.icloud")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
.foregroundStyle(.secondary)
|
||||
} else {
|
||||
Button {
|
||||
isForceUploading = true
|
||||
@@ -514,7 +514,7 @@ struct SettingsView: View {
|
||||
}
|
||||
}
|
||||
.font(.caption)
|
||||
.foregroundColor(.appPrimary)
|
||||
.foregroundStyle(Color.appPrimary)
|
||||
.disabled(isForceUploading)
|
||||
}
|
||||
}
|
||||
@@ -533,7 +533,7 @@ struct SettingsView: View {
|
||||
if !viewModel.isPremium {
|
||||
Text("Premium")
|
||||
.font(.caption.weight(.semibold))
|
||||
.foregroundColor(.appWarning)
|
||||
.foregroundStyle(Color.appWarning)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -553,7 +553,7 @@ struct SettingsView: View {
|
||||
Text("Last auto backup")
|
||||
Spacer()
|
||||
Text(last, style: .date)
|
||||
.foregroundColor(.secondary)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
.font(.caption)
|
||||
}
|
||||
@@ -580,7 +580,7 @@ struct SettingsView: View {
|
||||
if !viewModel.canExport {
|
||||
Image(systemName: "lock.fill")
|
||||
.font(.caption)
|
||||
.foregroundColor(.appWarning)
|
||||
.foregroundStyle(Color.appWarning)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -598,21 +598,21 @@ struct SettingsView: View {
|
||||
Text("Total Sources")
|
||||
Spacer()
|
||||
Text("\(viewModel.totalSources)")
|
||||
.foregroundColor(.secondary)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
|
||||
HStack {
|
||||
Text("Total Snapshots")
|
||||
Spacer()
|
||||
Text("\(viewModel.totalSnapshots)")
|
||||
.foregroundColor(.secondary)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
|
||||
HStack {
|
||||
Text("Storage Used")
|
||||
Spacer()
|
||||
Text(viewModel.storageUsedText)
|
||||
.foregroundColor(.secondary)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
} header: {
|
||||
Text("Data")
|
||||
@@ -648,7 +648,7 @@ struct SettingsView: View {
|
||||
if viewModel.backups.isEmpty {
|
||||
Text("No backups yet.")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
.foregroundStyle(.secondary)
|
||||
} else {
|
||||
ForEach(viewModel.backups) { backup in
|
||||
HStack {
|
||||
@@ -657,7 +657,7 @@ struct SettingsView: View {
|
||||
.font(.subheadline)
|
||||
Text("\(backup.location.rawValue) · \(formatBytes(backup.size))")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
Spacer()
|
||||
Button("Restore") {
|
||||
@@ -685,7 +685,7 @@ struct SettingsView: View {
|
||||
Section {
|
||||
Toggle("Hide Balances", isOn: $balancePrivacy.balancesHidden)
|
||||
|
||||
Toggle("Require Face ID to Reveal", isOn: $balancePrivacy.requireBiometricToReveal)
|
||||
Toggle(String(format: String(localized: "biometric_require_to_reveal"), biometryName), isOn: $balancePrivacy.requireBiometricToReveal)
|
||||
.onChange(of: balancePrivacy.requireBiometricToReveal) { _, enabled in
|
||||
if enabled && !AppLockService.canUseBiometrics() {
|
||||
balancePrivacy.requireBiometricToReveal = false
|
||||
@@ -695,10 +695,13 @@ struct SettingsView: View {
|
||||
} header: {
|
||||
Text("Privacy")
|
||||
} footer: {
|
||||
Text("Hide all monetary amounts behind a blur. Great for using the app in public or taking screenshots. Optionally require Face ID to reveal them.")
|
||||
Text(String(format: String(localized: "privacy_footer_biometric"), biometryName))
|
||||
}
|
||||
}
|
||||
|
||||
/// Face ID / Touch ID / Optic ID — resolved from the hardware, never assumed.
|
||||
private var biometryName: String { AppLockService.biometryName }
|
||||
|
||||
private var securitySection: some View {
|
||||
Section {
|
||||
Toggle("Require PIN", isOn: $pinEnabled)
|
||||
@@ -717,7 +720,7 @@ struct SettingsView: View {
|
||||
}
|
||||
}
|
||||
|
||||
Toggle("Enable Face ID", isOn: $faceIdEnabled)
|
||||
Toggle(String(format: String(localized: "biometric_enable"), biometryName), isOn: $faceIdEnabled)
|
||||
.onChange(of: faceIdEnabled) { _, enabled in
|
||||
if enabled {
|
||||
guard AppLockService.canUseBiometrics() else {
|
||||
@@ -748,7 +751,7 @@ struct SettingsView: View {
|
||||
} header: {
|
||||
Text("App Lock")
|
||||
} footer: {
|
||||
Text("Use Face ID or a 4-digit PIN to protect your data.")
|
||||
Text(String(format: String(localized: "biometric_lock_footer"), biometryName))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -802,7 +805,7 @@ struct SettingsView: View {
|
||||
Label("Allocation Targets", systemImage: "target")
|
||||
Spacer()
|
||||
Image(systemName: "chevron.right")
|
||||
.foregroundColor(.secondary)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
} header: {
|
||||
@@ -825,7 +828,7 @@ struct SettingsView: View {
|
||||
if !viewModel.isPremium {
|
||||
Text("Premium")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -844,7 +847,7 @@ struct SettingsView: View {
|
||||
Text("Version")
|
||||
Spacer()
|
||||
Text(viewModel.appVersion)
|
||||
.foregroundColor(.secondary)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
|
||||
Link(destination: URL(string: AppConstants.URLs.privacyPolicy)!) {
|
||||
@@ -853,7 +856,7 @@ struct SettingsView: View {
|
||||
Spacer()
|
||||
Image(systemName: "arrow.up.right")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -863,7 +866,7 @@ struct SettingsView: View {
|
||||
Spacer()
|
||||
Image(systemName: "arrow.up.right")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -873,7 +876,7 @@ struct SettingsView: View {
|
||||
Spacer()
|
||||
Image(systemName: "arrow.up.right")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -884,7 +887,7 @@ struct SettingsView: View {
|
||||
Text("Rate App")
|
||||
Spacer()
|
||||
Image(systemName: "star.fill")
|
||||
.foregroundColor(.yellow)
|
||||
.foregroundStyle(.yellow)
|
||||
}
|
||||
}
|
||||
} header: {
|
||||
@@ -992,7 +995,7 @@ struct ExportOptionsSheet: View {
|
||||
|
||||
Text(viewModel.exportStatus)
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
.padding(.vertical, 8)
|
||||
} header: {
|
||||
@@ -1005,7 +1008,7 @@ struct ExportOptionsSheet: View {
|
||||
} label: {
|
||||
HStack {
|
||||
Image(systemName: "tablecells")
|
||||
.foregroundColor(.positiveGreen)
|
||||
.foregroundStyle(Color.positiveGreen)
|
||||
.frame(width: 30)
|
||||
|
||||
VStack(alignment: .leading) {
|
||||
@@ -1013,7 +1016,7 @@ struct ExportOptionsSheet: View {
|
||||
.font(.headline)
|
||||
Text("Compatible with Excel, Google Sheets")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1023,7 +1026,7 @@ struct ExportOptionsSheet: View {
|
||||
} label: {
|
||||
HStack {
|
||||
Image(systemName: "doc.text")
|
||||
.foregroundColor(.appPrimary)
|
||||
.foregroundStyle(Color.appPrimary)
|
||||
.frame(width: 30)
|
||||
|
||||
VStack(alignment: .leading) {
|
||||
@@ -1031,7 +1034,7 @@ struct ExportOptionsSheet: View {
|
||||
.font(.headline)
|
||||
Text("Full data structure for backup")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1043,7 +1046,7 @@ struct ExportOptionsSheet: View {
|
||||
.navigationTitle(viewModel.isExporting ? "Exporting" : "Export Data")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
ToolbarItem(placement: .topBarTrailing) {
|
||||
Button("Cancel") {
|
||||
dismiss()
|
||||
}
|
||||
@@ -1089,16 +1092,16 @@ struct PinSetupView: View {
|
||||
if let errorMessage {
|
||||
Text(errorMessage)
|
||||
.font(.caption)
|
||||
.foregroundColor(.negativeRed)
|
||||
.foregroundStyle(Color.negativeRed)
|
||||
}
|
||||
}
|
||||
.navigationTitle(title)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarLeading) {
|
||||
ToolbarItem(placement: .topBarLeading) {
|
||||
Button("Cancel") { dismiss() }
|
||||
}
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
ToolbarItem(placement: .topBarTrailing) {
|
||||
Button("Save") { savePin() }
|
||||
.disabled(pin.count < 4 || confirmPin.count < 4)
|
||||
}
|
||||
@@ -1142,19 +1145,19 @@ struct PinVerifyView: View {
|
||||
if let errorMessage {
|
||||
Text(errorMessage)
|
||||
.font(.caption)
|
||||
.foregroundColor(.negativeRed)
|
||||
.foregroundStyle(Color.negativeRed)
|
||||
}
|
||||
}
|
||||
.navigationTitle(title)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarLeading) {
|
||||
ToolbarItem(placement: .topBarLeading) {
|
||||
Button("Cancel") {
|
||||
onResult(false)
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
ToolbarItem(placement: .topBarTrailing) {
|
||||
Button("Verify") { verifyPin() }
|
||||
.disabled(pin.count < 4)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user