Add push notifications support and update AppDelegate
This commit is contained in:
parent
49638e2519
commit
757342f057
4 changed files with 210 additions and 227 deletions
Binary file not shown.
|
|
@ -1,10 +1,4 @@
|
|||
//
|
||||
// Theme.swift
|
||||
// Coinly
|
||||
//
|
||||
// Created by Vadym Samoilenko on 03/03/2025.
|
||||
//
|
||||
|
||||
// Common/Theme/Theme.swift
|
||||
|
||||
import SwiftUI
|
||||
|
||||
|
|
@ -19,14 +13,19 @@ enum Theme {
|
|||
|
||||
// Акцентные цвета
|
||||
static let primary = Color.accentColor
|
||||
static let positive = Color("AccentGreen", bundle: nil) // Fallback to system green
|
||||
static let negative = Color("AccentRed", bundle: nil) // Fallback to system red
|
||||
static let positive = Color("AccentGreen", bundle: nil)
|
||||
static let negative = Color("AccentRed", bundle: nil)
|
||||
|
||||
// Цвета для типов счетов
|
||||
static let debitCard = Color.blue
|
||||
static let credit = Color.purple
|
||||
static let cash = Color.green
|
||||
static let savings = Color.mint
|
||||
static let debitCard = Color.blue
|
||||
static let savings = Color.purple
|
||||
static let investment = Color.orange
|
||||
static let credit = Color.red
|
||||
static let loan = Color.pink
|
||||
static let deposit = Color.yellow
|
||||
static let debtToMe = Color.mint
|
||||
static let myDebt = Color.brown
|
||||
|
||||
// Цвета текста
|
||||
static let primaryText = Color(.label)
|
||||
|
|
@ -38,4 +37,4 @@ enum Theme {
|
|||
startPoint: .topLeading,
|
||||
endPoint: .bottomTrailing
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,49 +3,63 @@ import CoreData
|
|||
|
||||
struct AccountListView: View {
|
||||
@Environment(\.managedObjectContext) private var viewContext
|
||||
@EnvironmentObject private var settingsViewModel: SettingsViewModel
|
||||
@StateObject private var viewModel = AccountViewModel()
|
||||
@State private var showingAddAccount = false
|
||||
@State private var showingSettings = false
|
||||
@State private var showArchived = false
|
||||
|
||||
@State private var animateChanges = false
|
||||
|
||||
var body: some View {
|
||||
ScrollView {
|
||||
VStack(spacing: 24) {
|
||||
// Total Balance Card
|
||||
if !viewModel.activeAccounts.isEmpty {
|
||||
if viewModel.activeAccounts.isEmpty {
|
||||
emptyStateView
|
||||
.transition(.opacity.combined(with: .scale))
|
||||
} else {
|
||||
totalBalanceCard
|
||||
}
|
||||
|
||||
// Account Groups
|
||||
VStack(spacing: 24) {
|
||||
// Basic Accounts
|
||||
ForEach(AccountType.basicAccounts, id: \.self) { type in
|
||||
accountGroup(for: type)
|
||||
}
|
||||
|
||||
// Investment Accounts
|
||||
if !viewModel.accounts(for: .investmentAccount).isEmpty {
|
||||
accountGroup(for: .investmentAccount)
|
||||
}
|
||||
|
||||
// Credit & Debt Accounts
|
||||
if hasDebtAccounts {
|
||||
VStack(alignment: .leading, spacing: 16) {
|
||||
Text("CREDIT & DEBT")
|
||||
.font(.caption)
|
||||
.fontWeight(.semibold)
|
||||
.foregroundColor(Theme.secondaryText)
|
||||
.padding(.horizontal, 16)
|
||||
|
||||
ForEach(AccountType.debtAccounts, id: \.self) { type in
|
||||
if !viewModel.accounts(for: type).isEmpty {
|
||||
accountGroup(for: type)
|
||||
.transition(.opacity.combined(with: .scale))
|
||||
|
||||
VStack(spacing: 24) {
|
||||
ForEach(AccountType.basicAccounts, id: \.self) { type in
|
||||
accountGroup(for: type)
|
||||
}
|
||||
|
||||
if !viewModel.accounts(for: AccountType.investmentAccounts[0]).isEmpty {
|
||||
VStack(alignment: .leading, spacing: 16) {
|
||||
Text("INVESTMENTS")
|
||||
.font(.caption)
|
||||
.fontWeight(.semibold)
|
||||
.foregroundColor(Theme.secondaryText)
|
||||
.padding(.horizontal, 16)
|
||||
|
||||
ForEach(AccountType.investmentAccounts, id: \.self) { type in
|
||||
if !viewModel.accounts(for: type).isEmpty {
|
||||
accountGroup(for: type)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if hasDebtAccounts {
|
||||
VStack(alignment: .leading, spacing: 16) {
|
||||
Text("CREDIT & DEBT")
|
||||
.font(.caption)
|
||||
.fontWeight(.semibold)
|
||||
.foregroundColor(Theme.secondaryText)
|
||||
.padding(.horizontal, 16)
|
||||
|
||||
ForEach(AccountType.debtAccounts, id: \.self) { type in
|
||||
if !viewModel.accounts(for: type).isEmpty {
|
||||
accountGroup(for: type)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.animation(.spring(), value: animateChanges)
|
||||
.padding(.top)
|
||||
}
|
||||
.background(Theme.background)
|
||||
|
|
@ -53,13 +67,19 @@ struct AccountListView: View {
|
|||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
HStack(spacing: 16) {
|
||||
Button(action: { showingAddAccount = true }) {
|
||||
Button(action: {
|
||||
hapticFeedback()
|
||||
showingAddAccount = true
|
||||
}) {
|
||||
Image(systemName: "plus.circle.fill")
|
||||
.font(.title2)
|
||||
.foregroundColor(Theme.primary)
|
||||
}
|
||||
|
||||
Button(action: { showingSettings = true }) {
|
||||
|
||||
Button(action: {
|
||||
hapticFeedback()
|
||||
showingSettings = true
|
||||
}) {
|
||||
Image(systemName: "gear")
|
||||
.font(.title2)
|
||||
.foregroundColor(Theme.primary)
|
||||
|
|
@ -70,107 +90,76 @@ struct AccountListView: View {
|
|||
.sheet(isPresented: $showingAddAccount) {
|
||||
AccountFormView()
|
||||
.environment(\.managedObjectContext, viewContext)
|
||||
.environmentObject(settingsViewModel)
|
||||
}
|
||||
.sheet(isPresented: $showingSettings) {
|
||||
SettingsView()
|
||||
.environmentObject(settingsViewModel)
|
||||
}
|
||||
.refreshable {
|
||||
hapticFeedback()
|
||||
await viewModel.loadAccounts()
|
||||
withAnimation {
|
||||
animateChanges.toggle()
|
||||
}
|
||||
}
|
||||
.task {
|
||||
await viewModel.loadAccounts()
|
||||
animateChanges.toggle()
|
||||
}
|
||||
}
|
||||
|
||||
private var totalBalanceCard: some View {
|
||||
VStack(spacing: 8) {
|
||||
Text("Total Balance")
|
||||
.font(.subheadline)
|
||||
.foregroundColor(Theme.secondaryText)
|
||||
|
||||
ForEach(Array(viewModel.accountsByCurrency.keys), id: \.self) { currency in
|
||||
VStack(spacing: 4) {
|
||||
Text(viewModel.totalBalanceFormatted(currency: currency))
|
||||
.font(.system(size: 34, weight: .medium, design: .rounded))
|
||||
.foregroundColor(Theme.primaryText)
|
||||
|
||||
Text(currency.code)
|
||||
.font(.caption)
|
||||
.foregroundColor(Theme.secondaryText)
|
||||
}
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.vertical, 24)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 24)
|
||||
.fill(Theme.cardBackground)
|
||||
.shadow(color: Theme.cardShadow, radius: 15, x: 0, y: 5)
|
||||
)
|
||||
.padding(.horizontal, 16)
|
||||
}
|
||||
|
||||
|
||||
private func accountGroup(for type: AccountType) -> some View {
|
||||
let accounts = viewModel.accounts(for: type)
|
||||
if accounts.isEmpty { return EmptyView().erasedToAnyView() }
|
||||
|
||||
return VStack(alignment: .leading, spacing: 16) {
|
||||
// Group Header
|
||||
HStack {
|
||||
Image(systemName: type.icon)
|
||||
.foregroundColor(accountTypeColor(for: type))
|
||||
Text(type.localizedName.uppercased())
|
||||
.font(.caption)
|
||||
.fontWeight(.semibold)
|
||||
.foregroundColor(Theme.secondaryText)
|
||||
}
|
||||
.padding(.horizontal, 16)
|
||||
|
||||
// Account Cards
|
||||
VStack(spacing: 12) {
|
||||
ForEach(accounts) { account in
|
||||
NavigationLink {
|
||||
AccountDetailView(account: account)
|
||||
.environment(\.managedObjectContext, viewContext)
|
||||
} label: {
|
||||
AccountRowView(account: account)
|
||||
if accounts.isEmpty { return AnyView(EmptyView()) }
|
||||
|
||||
return AnyView(
|
||||
VStack(alignment: .leading, spacing: 16) {
|
||||
HStack {
|
||||
Image(systemName: type.icon)
|
||||
.foregroundColor(Color(type.color))
|
||||
Text(type.localizedName.uppercased())
|
||||
.font(.caption)
|
||||
.fontWeight(.semibold)
|
||||
.foregroundColor(Theme.secondaryText)
|
||||
}
|
||||
.padding(.horizontal, 16)
|
||||
|
||||
VStack(spacing: 12) {
|
||||
ForEach(accounts) { account in
|
||||
NavigationLink {
|
||||
AccountDetailView(account: account)
|
||||
.environment(\.managedObjectContext, viewContext)
|
||||
.environmentObject(settingsViewModel)
|
||||
} label: {
|
||||
AccountRowView(account: account)
|
||||
.environmentObject(settingsViewModel)
|
||||
}
|
||||
.buttonStyle(PlainButtonStyle())
|
||||
}
|
||||
.buttonStyle(PlainButtonStyle())
|
||||
}
|
||||
}
|
||||
}
|
||||
.erasedToAnyView()
|
||||
)
|
||||
}
|
||||
|
||||
private func accountTypeColor(for type: AccountType) -> Color {
|
||||
switch type {
|
||||
case .debitCard: return Theme.debitCard
|
||||
case .credit: return Theme.credit
|
||||
case .cash: return Theme.cash
|
||||
case .savings: return Theme.savings
|
||||
default: return Theme.primary
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private var hasDebtAccounts: Bool {
|
||||
AccountType.debtAccounts.contains { !viewModel.accounts(for: $0).isEmpty }
|
||||
}
|
||||
}
|
||||
|
||||
extension View {
|
||||
func erasedToAnyView() -> AnyView {
|
||||
AnyView(self)
|
||||
private var emptyStateView: some View {
|
||||
Text("No accounts available")
|
||||
.font(.title)
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
|
||||
private var totalBalanceCard: some View {
|
||||
Text("Total Balance: $0.00")
|
||||
.font(.title)
|
||||
.foregroundColor(.primary)
|
||||
}
|
||||
|
||||
private func hapticFeedback() {
|
||||
let generator = UIImpactFeedbackGenerator(style: .medium)
|
||||
generator.impactOccurred()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Preview
|
||||
#if DEBUG
|
||||
struct AccountListView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
NavigationView {
|
||||
AccountListView()
|
||||
.environment(\.managedObjectContext, PersistenceController.preview.container.viewContext)
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
|
|
|||
|
|
@ -51,7 +51,6 @@ struct SettingsView: View {
|
|||
_groupTransactionsByDay = State(initialValue: settings.display.groupTransactionsByDay)
|
||||
_showRunningBalance = State(initialValue: settings.display.showRunningBalance)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
List {
|
||||
|
|
@ -125,7 +124,6 @@ struct SettingsView: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var appearanceSection: some View {
|
||||
Section {
|
||||
ForEach(AppSettings.AppTheme.allCases, id: \.self) { theme in
|
||||
|
|
@ -158,8 +156,8 @@ struct SettingsView: View {
|
|||
.tag(currency)
|
||||
}
|
||||
}
|
||||
.onChange(of: selectedCurrency) { newCurrency in
|
||||
viewModel.updateDefaultCurrency(newCurrency)
|
||||
.onChange(of: selectedCurrency) { oldValue, newValue in
|
||||
viewModel.updateDefaultCurrency(newValue)
|
||||
}
|
||||
} header: {
|
||||
Text("CURRENCY")
|
||||
|
|
@ -169,51 +167,50 @@ struct SettingsView: View {
|
|||
private var privacySection: some View {
|
||||
Section {
|
||||
Toggle("Hide Balances", isOn: $hideBalances)
|
||||
.onChange(of: hideBalances) { newValue in
|
||||
.onChange(of: hideBalances) { oldValue, newValue in
|
||||
viewModel.updatePrivacySettings(hideBalances: newValue)
|
||||
}
|
||||
|
||||
Toggle("Use Face ID", isOn: $useBiometrics)
|
||||
.onChange(of: useBiometrics) { newValue in
|
||||
.onChange(of: useBiometrics) { oldValue, newValue in
|
||||
viewModel.updatePrivacySettings(useBiometrics: newValue)
|
||||
}
|
||||
|
||||
Toggle("Require Passcode on Launch", isOn: $requirePasscodeOnLaunch)
|
||||
.onChange(of: requirePasscodeOnLaunch) { newValue in
|
||||
.onChange(of: requirePasscodeOnLaunch) { oldValue, newValue in
|
||||
viewModel.updatePrivacySettings(requirePasscode: newValue)
|
||||
}
|
||||
} header: {
|
||||
Text("PRIVACY & SECURITY")
|
||||
}
|
||||
}
|
||||
|
||||
private var notificationsSection: some View {
|
||||
Section {
|
||||
Toggle("Enable Notifications", isOn: $notificationsEnabled)
|
||||
.onChange(of: notificationsEnabled) { newValue in
|
||||
.onChange(of: notificationsEnabled) { oldValue, newValue in
|
||||
viewModel.updateNotificationSettings(isEnabled: newValue)
|
||||
}
|
||||
|
||||
if notificationsEnabled {
|
||||
Toggle("Daily Balance Reminder", isOn: $dailyReminder)
|
||||
.onChange(of: dailyReminder) { newValue in
|
||||
.onChange(of: dailyReminder) { oldValue, newValue in
|
||||
viewModel.updateNotificationSettings(dailyReminder: newValue)
|
||||
}
|
||||
|
||||
if dailyReminder {
|
||||
DatePicker("Reminder Time", selection: $dailyReminderTime, displayedComponents: .hourAndMinute)
|
||||
.onChange(of: dailyReminderTime) { newValue in
|
||||
.onChange(of: dailyReminderTime) { oldValue, newValue in
|
||||
viewModel.updateNotificationSettings(reminderTime: newValue)
|
||||
}
|
||||
}
|
||||
|
||||
Toggle("Weekly Report", isOn: $weeklyReport)
|
||||
.onChange(of: weeklyReport) { newValue in
|
||||
.onChange(of: weeklyReport) { oldValue, newValue in
|
||||
viewModel.updateNotificationSettings(weeklyReport: newValue)
|
||||
}
|
||||
|
||||
Toggle("Transaction Alerts", isOn: $transactionAlerts)
|
||||
.onChange(of: transactionAlerts) { newValue in
|
||||
.onChange(of: transactionAlerts) { oldValue, newValue in
|
||||
viewModel.updateNotificationSettings(transactionAlerts: newValue)
|
||||
}
|
||||
}
|
||||
|
|
@ -225,24 +222,23 @@ struct SettingsView: View {
|
|||
private var displaySection: some View {
|
||||
Section {
|
||||
Toggle("Show Cents", isOn: $showCents)
|
||||
.onChange(of: showCents) { newValue in
|
||||
.onChange(of: showCents) { oldValue, newValue in
|
||||
viewModel.updateDisplaySettings(showCents: newValue)
|
||||
}
|
||||
|
||||
Toggle("Group Transactions by Day", isOn: $groupTransactionsByDay)
|
||||
.onChange(of: groupTransactionsByDay) { newValue in
|
||||
.onChange(of: groupTransactionsByDay) { oldValue, newValue in
|
||||
viewModel.updateDisplaySettings(groupByDay: newValue)
|
||||
}
|
||||
|
||||
Toggle("Show Running Balance", isOn: $showRunningBalance)
|
||||
.onChange(of: showRunningBalance) { newValue in
|
||||
.onChange(of: showRunningBalance) { oldValue, newValue in
|
||||
viewModel.updateDisplaySettings(showRunningBalance: newValue)
|
||||
}
|
||||
} header: {
|
||||
Text("DISPLAY")
|
||||
}
|
||||
}
|
||||
|
||||
private var appInfoSection: some View {
|
||||
Section {
|
||||
HStack {
|
||||
|
|
@ -296,96 +292,95 @@ struct SettingsView: View {
|
|||
.contentShape(Rectangle())
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var userProfileSection: some View {
|
||||
if let email = viewModel.settings.userEmail {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text(email)
|
||||
private var userProfileSection: some View {
|
||||
if let email = viewModel.settings.userEmail {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text(email)
|
||||
.foregroundColor(.primary)
|
||||
Text("Signed in")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
|
||||
Button(role: .destructive) {
|
||||
// Handle sign out
|
||||
} label: {
|
||||
Text("Sign Out")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var authenticationSection: some View {
|
||||
VStack(spacing: 12) {
|
||||
NavigationLink {
|
||||
SignInView()
|
||||
} label: {
|
||||
HStack {
|
||||
Image(systemName: "person.fill")
|
||||
Text("Sign In")
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding()
|
||||
.background(Color.accentColor)
|
||||
.foregroundColor(.white)
|
||||
.cornerRadius(10)
|
||||
}
|
||||
|
||||
NavigationLink {
|
||||
SignUpView()
|
||||
} label: {
|
||||
HStack {
|
||||
Image(systemName: "person.badge.plus")
|
||||
Text("Create Account")
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding()
|
||||
.background(Color.secondary.opacity(0.1))
|
||||
.foregroundColor(.primary)
|
||||
Text("Signed in")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
|
||||
Button(role: .destructive) {
|
||||
// Handle sign out
|
||||
} label: {
|
||||
Text("Sign Out")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var authenticationSection: some View {
|
||||
VStack(spacing: 12) {
|
||||
NavigationLink {
|
||||
SignInView()
|
||||
} label: {
|
||||
HStack {
|
||||
Image(systemName: "person.fill")
|
||||
Text("Sign In")
|
||||
.cornerRadius(10)
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding()
|
||||
.background(Color.accentColor)
|
||||
.foregroundColor(.white)
|
||||
.cornerRadius(10)
|
||||
}
|
||||
.padding(.vertical, 8)
|
||||
}
|
||||
|
||||
// MARK: - Actions
|
||||
private func checkForUpdates() {
|
||||
Task {
|
||||
await viewModel.checkForUpdates()
|
||||
}
|
||||
}
|
||||
|
||||
private func resetSettings() {
|
||||
viewModel.resetSettings()
|
||||
|
||||
NavigationLink {
|
||||
SignUpView()
|
||||
} label: {
|
||||
HStack {
|
||||
Image(systemName: "person.badge.plus")
|
||||
Text("Create Account")
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding()
|
||||
.background(Color.secondary.opacity(0.1))
|
||||
.foregroundColor(.primary)
|
||||
.cornerRadius(10)
|
||||
}
|
||||
}
|
||||
.padding(.vertical, 8)
|
||||
}
|
||||
|
||||
// MARK: - Actions
|
||||
private func checkForUpdates() {
|
||||
Task {
|
||||
await viewModel.checkForUpdates()
|
||||
// Update local state
|
||||
selectedTheme = viewModel.settings.theme
|
||||
selectedLanguage = viewModel.settings.language
|
||||
selectedCurrency = viewModel.settings.defaultCurrency
|
||||
|
||||
// Privacy
|
||||
hideBalances = viewModel.settings.privacy.hideBalances
|
||||
useBiometrics = viewModel.settings.privacy.useBiometrics
|
||||
requirePasscodeOnLaunch = viewModel.settings.privacy.requirePasscodeOnLaunch
|
||||
|
||||
// Notifications
|
||||
notificationsEnabled = viewModel.settings.notifications.isEnabled
|
||||
dailyReminder = viewModel.settings.notifications.dailyReminder
|
||||
dailyReminderTime = viewModel.settings.notifications.dailyReminderTime
|
||||
weeklyReport = viewModel.settings.notifications.weeklyReport
|
||||
transactionAlerts = viewModel.settings.notifications.transactionAlerts
|
||||
|
||||
// Display
|
||||
showCents = viewModel.settings.display.showCents
|
||||
groupTransactionsByDay = viewModel.settings.display.groupTransactionsByDay
|
||||
showRunningBalance = viewModel.settings.display.showRunningBalance
|
||||
}
|
||||
}
|
||||
|
||||
private func resetSettings() {
|
||||
viewModel.resetSettings()
|
||||
|
||||
// Update local state
|
||||
selectedTheme = viewModel.settings.theme
|
||||
selectedLanguage = viewModel.settings.language
|
||||
selectedCurrency = viewModel.settings.defaultCurrency
|
||||
|
||||
// Privacy
|
||||
hideBalances = viewModel.settings.privacy.hideBalances
|
||||
useBiometrics = viewModel.settings.privacy.useBiometrics
|
||||
requirePasscodeOnLaunch = viewModel.settings.privacy.requirePasscodeOnLaunch
|
||||
|
||||
// Notifications
|
||||
notificationsEnabled = viewModel.settings.notifications.isEnabled
|
||||
dailyReminder = viewModel.settings.notifications.dailyReminder
|
||||
dailyReminderTime = viewModel.settings.notifications.dailyReminderTime
|
||||
weeklyReport = viewModel.settings.notifications.weeklyReport
|
||||
transactionAlerts = viewModel.settings.notifications.transactionAlerts
|
||||
|
||||
// Display
|
||||
showCents = viewModel.settings.display.showCents
|
||||
groupTransactionsByDay = viewModel.settings.display.groupTransactionsByDay
|
||||
showRunningBalance = viewModel.settings.display.showRunningBalance
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Preview
|
||||
struct SettingsView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
SettingsView()
|
||||
// MARK: - Preview
|
||||
struct SettingsView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
SettingsView()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue