import SwiftUI import StoreKit struct SettingsView: View { @EnvironmentObject var iapService: IAPService @StateObject private var viewModel: SettingsViewModel @AppStorage("calmModeEnabled") private var calmModeEnabled = true @AppStorage("cloudSyncEnabled") private var cloudSyncEnabled = false @AppStorage("faceIdEnabled") private var faceIdEnabled = false @AppStorage("pinEnabled") private var pinEnabled = false @AppStorage("lockOnLaunch") private var lockOnLaunch = true @AppStorage("lockOnBackground") private var lockOnBackground = false @State private var showingPinSetup = false @State private var showingPinChange = false @State private var showingBiometricAlert = false @State private var showingPinRequiredAlert = false @State private var showingPinDisableAlert = false @State private var showingRestartAlert = false @State private var didLoadCloudSync = false init() { _viewModel = StateObject(wrappedValue: SettingsViewModel(iapService: IAPService())) } var body: some View { NavigationStack { ZStack { AppBackground() List { brandSection // Premium Section premiumSection // Notifications Section notificationsSection // Data Section dataSection // Security Section securitySection // Preferences Section preferencesSection // Long-Term Focus longTermSection // Accounts Section accountsSection // About Section aboutSection // Danger Zone dangerZoneSection } .scrollContentBackground(.hidden) } .navigationTitle("Settings") .sheet(isPresented: $viewModel.showingPaywall) { PaywallView() } .sheet(isPresented: $viewModel.showingExportOptions) { ExportOptionsSheet(viewModel: viewModel) } .sheet(isPresented: $viewModel.showingImportSheet) { ImportDataView() } .confirmationDialog( "Reset All Data", isPresented: $viewModel.showingResetConfirmation, titleVisibility: .visible ) { Button("Reset Everything", role: .destructive) { viewModel.resetAllData() } } message: { Text("This will permanently delete all your investment data. This action cannot be undone.") } .alert("Success", isPresented: .constant(viewModel.successMessage != nil)) { Button("OK") { viewModel.successMessage = nil } } message: { Text(viewModel.successMessage ?? "") } .alert("Error", isPresented: .constant(viewModel.errorMessage != nil)) { Button("OK") { viewModel.errorMessage = nil } } message: { Text(viewModel.errorMessage ?? "") } .alert("Restart Required", isPresented: $showingRestartAlert) { Button("OK") {} } message: { Text("Restart the app to apply iCloud sync changes.") } .alert("Face ID Unavailable", isPresented: $showingBiometricAlert) { Button("OK") {} } message: { Text("Face ID isn't available on this device.") } .alert("PIN Required", isPresented: $showingPinRequiredAlert) { Button("OK") {} } message: { Text("Enable a PIN before turning on Face ID.") } .alert("Disable Face ID First", isPresented: $showingPinDisableAlert) { Button("OK") {} } message: { Text("Turn off Face ID before disabling your PIN.") } .sheet(isPresented: $showingPinSetup) { PinSetupView(title: "Set PIN") { pin in if KeychainService.savePin(pin) { pinEnabled = true } else { pinEnabled = false } } .onDisappear { if KeychainService.readPin() == nil { pinEnabled = false } } } .sheet(isPresented: $showingPinChange) { PinSetupView(title: "Change PIN") { pin in _ = KeychainService.savePin(pin) } } .onAppear { didLoadCloudSync = true } } } // MARK: - Brand Section private var brandSection: some View { Section { HStack(spacing: 12) { Image("BrandMark") .resizable() .scaledToFit() .frame(width: 36, height: 36) .padding(6) .background(Color.appPrimary.opacity(0.08)) .cornerRadius(10) VStack(alignment: .leading, spacing: 2) { Text(appDisplayName) .font(.headline) Text("Long-term portfolio tracker") .font(.caption) .foregroundColor(.secondary) } Spacer() } } } // MARK: - Premium Section private var premiumSection: some View { Section { if viewModel.isPremium { HStack { ZStack { Circle() .fill(Color.yellow.opacity(0.2)) .frame(width: 44, height: 44) Image(systemName: "crown.fill") .foregroundStyle( LinearGradient( colors: [.yellow, .orange], startPoint: .topLeading, endPoint: .bottomTrailing ) ) } VStack(alignment: .leading, spacing: 2) { Text("Premium Active") .font(.headline) if viewModel.isFamilyShared { Text("Family Sharing") .font(.caption) .foregroundColor(.secondary) } } Spacer() Image(systemName: "checkmark.seal.fill") .foregroundColor(.positiveGreen) } } else { Button { viewModel.upgradeToPremium() } label: { HStack { ZStack { Circle() .fill(Color.appPrimary.opacity(0.1)) .frame(width: 44, height: 44) Image(systemName: "crown.fill") .foregroundColor(.appPrimary) } VStack(alignment: .leading, spacing: 2) { Text("Upgrade to Premium") .font(.headline) .foregroundColor(.primary) Text("Unlock all features for €4.69") .font(.caption) .foregroundColor(.secondary) } Spacer() Image(systemName: "chevron.right") .foregroundColor(.secondary) } } Button { Task { await viewModel.restorePurchases() } } label: { Text("Restore Purchases") } } } header: { Text("Subscription") } footer: { if !viewModel.isPremium { Text("Free: \(viewModel.sourceLimitText) • \(viewModel.historyLimitText)") } } } // MARK: - Notifications Section private var notificationsSection: some View { Section { HStack { Text("Notifications") Spacer() Text(viewModel.notificationsEnabled ? "Enabled" : "Disabled") .foregroundColor(.secondary) } .contentShape(Rectangle()) .onTapGesture { if !viewModel.notificationsEnabled { Task { await viewModel.requestNotificationPermission() } } else { viewModel.openSystemSettings() } } if viewModel.notificationsEnabled { DatePicker( "Default Reminder Time", selection: $viewModel.defaultNotificationTime, displayedComponents: .hourAndMinute ) .onChange(of: viewModel.defaultNotificationTime) { _, newTime in viewModel.updateNotificationTime(newTime) } } } header: { Text("Notifications") } footer: { Text("Set when you'd like to receive investment update reminders.") } } // MARK: - Data Section private var dataSection: some View { Section { Toggle("Sync with iCloud", isOn: $cloudSyncEnabled) .onChange(of: cloudSyncEnabled) { _, _ in if didLoadCloudSync { showingRestartAlert = true } } Button { if viewModel.canExport { viewModel.showingExportOptions = true } else { viewModel.showingPaywall = true } } label: { HStack { Label("Export Data", systemImage: "square.and.arrow.up") Spacer() if !viewModel.canExport { Image(systemName: "lock.fill") .font(.caption) .foregroundColor(.appWarning) } } } Button { viewModel.showingImportSheet = true } label: { HStack { Label("Import Data", systemImage: "square.and.arrow.down") Spacer() } } HStack { Text("Total Sources") Spacer() Text("\(viewModel.totalSources)") .foregroundColor(.secondary) } HStack { Text("Total Snapshots") Spacer() Text("\(viewModel.totalSnapshots)") .foregroundColor(.secondary) } HStack { Text("Storage Used") Spacer() Text(viewModel.storageUsedText) .foregroundColor(.secondary) } } header: { Text("Data") } } // MARK: - Security Section private var securitySection: some View { Section { Toggle("Require PIN", isOn: $pinEnabled) .onChange(of: pinEnabled) { _, enabled in if enabled { if KeychainService.readPin() == nil { showingPinSetup = true } } else { if faceIdEnabled { pinEnabled = true showingPinDisableAlert = true } else { KeychainService.deletePin() } } } Toggle("Enable Face ID", isOn: $faceIdEnabled) .onChange(of: faceIdEnabled) { _, enabled in if enabled { guard AppLockService.canUseBiometrics() else { faceIdEnabled = false showingBiometricAlert = true return } if !pinEnabled { faceIdEnabled = false showingPinRequiredAlert = true } } } if pinEnabled { Button("Change PIN") { showingPinChange = true } } if faceIdEnabled || pinEnabled { Toggle("Lock on App Launch", isOn: $lockOnLaunch) Toggle("Lock When Backgrounded", isOn: $lockOnBackground) } } header: { Text("App Lock") } footer: { Text("Use Face ID or a 4-digit PIN to protect your data.") } } // MARK: - Preferences Section private var preferencesSection: some View { Section { Picker("Currency", selection: $viewModel.currencyCode) { ForEach(CurrencyPicker.commonCodes, id: \.self) { code in Text(code).tag(code) } } .onChange(of: viewModel.currencyCode) { _, newValue in viewModel.updateCurrency(newValue) } Picker("Input Mode", selection: $viewModel.inputMode) { ForEach(InputMode.allCases) { mode in Text(mode.title).tag(mode) } } .onChange(of: viewModel.inputMode) { _, newValue in viewModel.updateInputMode(newValue) } } header: { Text("Preferences") } footer: { Text("Currency and input mode apply globally unless overridden per account.") } } // MARK: - Long-Term Focus Section private var longTermSection: some View { Section { Toggle("Calm Mode", isOn: $calmModeEnabled) NavigationLink { AllocationTargetsView() } label: { HStack { Label("Allocation Targets", systemImage: "target") Spacer() Image(systemName: "chevron.right") .foregroundColor(.secondary) } } } header: { Text("Long-Term Focus") } footer: { Text("Calm Mode hides short-term noise and advanced charts, keeping the app focused on monthly check-ins.") } } // MARK: - Accounts Section private var accountsSection: some View { Section { NavigationLink { AccountsView() } label: { HStack { Label("Manage Accounts", systemImage: "person.2") Spacer() if !viewModel.isPremium { Text("Premium") .font(.caption) .foregroundColor(.secondary) } } } } header: { Text("Accounts") } footer: { Text(viewModel.isPremium ? "Create multiple accounts and switch between them." : "Free users can use one account.") } } // MARK: - About Section private var aboutSection: some View { Section { HStack { Text("Version") Spacer() Text(viewModel.appVersion) .foregroundColor(.secondary) } Link(destination: URL(string: AppConstants.URLs.privacyPolicy)!) { HStack { Text("Privacy Policy") Spacer() Image(systemName: "arrow.up.right") .font(.caption) .foregroundColor(.secondary) } } Link(destination: URL(string: AppConstants.URLs.termsOfService)!) { HStack { Text("Terms of Service") Spacer() Image(systemName: "arrow.up.right") .font(.caption) .foregroundColor(.secondary) } } Link(destination: URL(string: AppConstants.URLs.support)!) { HStack { Text("Support") Spacer() Image(systemName: "arrow.up.right") .font(.caption) .foregroundColor(.secondary) } } Button { requestAppReview() } label: { HStack { Text("Rate App") Spacer() Image(systemName: "star.fill") .foregroundColor(.yellow) } } } header: { Text("About") } } // MARK: - Danger Zone Section private var dangerZoneSection: some View { Section { Button(role: .destructive) { viewModel.showingResetConfirmation = true } label: { HStack { Image(systemName: "trash") Text("Reset All Data") } } } header: { Text("Danger Zone") } footer: { Text("This will permanently delete all your investment sources, snapshots, and settings.") } } // MARK: - Helpers private func requestAppReview() { guard let scene = UIApplication.shared.connectedScenes.first as? UIWindowScene else { return } if #available(iOS 18.0, *) { AppStore.requestReview(in: scene) } else { SKStoreReviewController.requestReview(in: scene) } } private var appDisplayName: String { if let name = Bundle.main.object(forInfoDictionaryKey: "CFBundleDisplayName") as? String { return name } if let name = Bundle.main.object(forInfoDictionaryKey: "CFBundleName") as? String { return name } return "Portfolio Journal" } } // MARK: - Export Options Sheet struct ExportOptionsSheet: View { @Environment(\.dismiss) private var dismiss @ObservedObject var viewModel: SettingsViewModel var body: some View { NavigationStack { List { Section { Button { viewModel.exportData(format: .csv) dismiss() } label: { HStack { Image(systemName: "tablecells") .foregroundColor(.positiveGreen) .frame(width: 30) VStack(alignment: .leading) { Text("CSV") .font(.headline) Text("Compatible with Excel, Google Sheets") .font(.caption) .foregroundColor(.secondary) } } } Button { viewModel.exportData(format: .json) dismiss() } label: { HStack { Image(systemName: "doc.text") .foregroundColor(.appPrimary) .frame(width: 30) VStack(alignment: .leading) { Text("JSON") .font(.headline) Text("Full data structure for backup") .font(.caption) .foregroundColor(.secondary) } } } } header: { Text("Select Format") } } .navigationTitle("Export Data") .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .navigationBarTrailing) { Button("Cancel") { dismiss() } } } } .presentationDetents([.medium]) } } // MARK: - PIN Setup View struct PinSetupView: View { @Environment(\.dismiss) private var dismiss let title: String let onSave: (String) -> Void @State private var pin = "" @State private var confirmPin = "" @State private var errorMessage: String? var body: some View { NavigationStack { Form { Section { SecureField("New PIN", text: $pin) .keyboardType(.numberPad) .onChange(of: pin) { _, newValue in pin = String(newValue.filter(\.isNumber).prefix(4)) } SecureField("Confirm PIN", text: $confirmPin) .keyboardType(.numberPad) .onChange(of: confirmPin) { _, newValue in confirmPin = String(newValue.filter(\.isNumber).prefix(4)) } } header: { Text("4-Digit PIN") } if let errorMessage { Text(errorMessage) .font(.caption) .foregroundColor(.negativeRed) } } .navigationTitle(title) .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .navigationBarLeading) { Button("Cancel") { dismiss() } } ToolbarItem(placement: .navigationBarTrailing) { Button("Save") { savePin() } .disabled(pin.count < 4 || confirmPin.count < 4) } } } .presentationDetents([.medium]) } private func savePin() { guard pin.count == 4, pin == confirmPin else { errorMessage = "PINs do not match." confirmPin = "" return } onSave(pin) dismiss() } } #Preview { SettingsView() .environmentObject(IAPService()) .environmentObject(AccountStore(iapService: IAPService())) }