From f62670aaeca811ba959832da15426ec9dc3f8e1e Mon Sep 17 00:00:00 2001 From: Billy Brawner Date: Tue, 17 May 2022 19:04:27 -0600 Subject: [PATCH] Combine all top-level datastores together --- Twigs.xcodeproj/project.pbxproj | 32 +- Twigs/Budget/BudgetDetailsView.swift | 8 +- Twigs/Budget/BudgetListsView.swift | 10 +- Twigs/Budget/BudgetsDataStore.swift | 99 ---- Twigs/Category/CategoryDataStore.swift | 8 +- Twigs/Category/CategoryDetailsView.swift | 11 +- Twigs/Category/CategoryForm.swift | 10 +- Twigs/Category/CategoryFormSheet.swift | 6 +- Twigs/Category/CategoryListDataStore.swift | 90 ---- Twigs/Category/CategoryListView.swift | 17 +- Twigs/DataStore.swift | 460 ++++++++++++++++++ Twigs/LoginView.swift | 2 +- Twigs/Profile/ProfileView.swift | 7 +- .../RecurringTransactionDataStore.swift | 79 --- .../RecurringTransactionDetailsView.swift | 4 +- .../RecurringTransactionsListView.swift | 13 +- .../RecurringTransactionsRepository.swift | 2 +- Twigs/RegistrationView.swift | 4 +- Twigs/SidebarBudgetView.swift | 18 +- Twigs/TabbedBudgetView.swift | 23 +- Twigs/Transaction/TransactionDataStore.swift | 110 ----- Twigs/Transaction/TransactionDetails.swift | 16 +- .../Transaction/TransactionDetailsView.swift | 22 +- Twigs/Transaction/TransactionForm.swift | 22 +- Twigs/Transaction/TransactionFormSheet.swift | 6 +- Twigs/Transaction/TransactionListView.swift | 76 +-- Twigs/Transaction/TransactionRepository.swift | 2 +- Twigs/TwigsApp.swift | 14 +- Twigs/User/AuthenticationDataStore.swift | 104 ---- Twigs/User/UserDataStore.swift | 30 -- Twigs/User/UserRepository.swift | 8 +- Twigs/Views/MainView.swift | 27 +- 32 files changed, 629 insertions(+), 711 deletions(-) delete mode 100644 Twigs/Budget/BudgetsDataStore.swift delete mode 100644 Twigs/Category/CategoryListDataStore.swift create mode 100644 Twigs/DataStore.swift delete mode 100644 Twigs/Recurring Transactions/RecurringTransactionDataStore.swift delete mode 100644 Twigs/Transaction/TransactionDataStore.swift delete mode 100644 Twigs/User/AuthenticationDataStore.swift delete mode 100644 Twigs/User/UserDataStore.swift diff --git a/Twigs.xcodeproj/project.pbxproj b/Twigs.xcodeproj/project.pbxproj index 3e69b03..36d4782 100644 --- a/Twigs.xcodeproj/project.pbxproj +++ b/Twigs.xcodeproj/project.pbxproj @@ -16,7 +16,6 @@ 284102302342D97300EAFA29 /* BudgetListsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2841022F2342D97300EAFA29 /* BudgetListsView.swift */; }; 284102322342E12F00EAFA29 /* CategoryListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 284102312342E12F00EAFA29 /* CategoryListView.swift */; }; 2857EAED233DA30B0026BC83 /* LoadingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2857EAEC233DA30B0026BC83 /* LoadingView.swift */; }; - 289510242352AAFC00BC862B /* UserDataStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 289510232352AAFC00BC862B /* UserDataStore.swift */; }; 28AC94F2233C373900BFB70A /* LoginView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28AC94F1233C373900BFB70A /* LoginView.swift */; }; 28AC94F4233C373A00BFB70A /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 28AC94F3233C373A00BFB70A /* Assets.xcassets */; }; 28AC94F7233C373A00BFB70A /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 28AC94F6233C373A00BFB70A /* Preview Assets.xcassets */; }; @@ -28,20 +27,15 @@ 28AC952C233C434800BFB70A /* UserRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28AC952B233C434800BFB70A /* UserRepository.swift */; }; 28B9E50E2346BCB2007C3909 /* RegistrationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28B9E50D2346BCB2007C3909 /* RegistrationView.swift */; }; 28CE8B9523525F990072BC4C /* Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28CE8B9423525F990072BC4C /* Extensions.swift */; }; - 28FE6AF42342E3CB00D5543E /* BudgetsDataStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28FE6AF32342E3CB00D5543E /* BudgetsDataStore.swift */; }; 28FE6AF62342E4CC00D5543E /* BudgetRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28FE6AF52342E4CC00D5543E /* BudgetRepository.swift */; }; - 28FE6AFA23441E3700D5543E /* CategoryListDataStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28FE6AF923441E3700D5543E /* CategoryListDataStore.swift */; }; 28FE6AFC23441E4500D5543E /* CategoryRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28FE6AFB23441E4500D5543E /* CategoryRepository.swift */; }; 28FE6B002344308600D5543E /* Transaction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28FE6AFF2344308600D5543E /* Transaction.swift */; }; - 28FE6B022344331B00D5543E /* TransactionDataStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28FE6B012344331B00D5543E /* TransactionDataStore.swift */; }; 28FE6B04234449DC00D5543E /* TransactionListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28FE6B03234449DC00D5543E /* TransactionListView.swift */; }; 28FE6B0623444A9800D5543E /* TransactionDetailsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28FE6B0523444A9800D5543E /* TransactionDetailsView.swift */; }; - 543ECE42233E82A40018A9D9 /* AuthenticationDataStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 543ECE41233E82A40018A9D9 /* AuthenticationDataStore.swift */; }; 8005FD5D277EAB0200E48B23 /* MainView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8005FD5C277EAB0200E48B23 /* MainView.swift */; }; 800DFC2C277FF47A00EDCE9B /* AsyncData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 800DFC2B277FF47A00EDCE9B /* AsyncData.swift */; }; 801D08CC275ECEFA00931465 /* RecurringTransactionsListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 801D08CB275ECEFA00931465 /* RecurringTransactionsListView.swift */; }; 801D08CE275F189E00931465 /* RecurringTransactionsRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 801D08CD275F189E00931465 /* RecurringTransactionsRepository.swift */; }; - 801D08D0275F1AE300931465 /* RecurringTransactionDataStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 801D08CF275F1AE300931465 /* RecurringTransactionDataStore.swift */; }; 801D08D2275FB7DE00931465 /* RecurringTransactionDetailsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 801D08D1275FB7DE00931465 /* RecurringTransactionDetailsView.swift */; }; 802161D0277647920075761A /* AsyncObservableObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = 802161CF277647920075761A /* AsyncObservableObject.swift */; }; 8021EFAD280A0FCA00043F18 /* TwigsCore in Frameworks */ = {isa = PBXBuildFile; productRef = 8021EFAC280A0FCA00043F18 /* TwigsCore */; }; @@ -54,10 +48,12 @@ 8076A84F2809FE8E006B9DC9 /* ArgumentParser in Frameworks */ = {isa = PBXBuildFile; productRef = 8076A84E2809FE8E006B9DC9 /* ArgumentParser */; }; 8076A8522809FE99006B9DC9 /* Collections in Frameworks */ = {isa = PBXBuildFile; productRef = 8076A8512809FE99006B9DC9 /* Collections */; }; 80820145275FFD380040996E /* SidebarBudgetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80820144275FFD380040996E /* SidebarBudgetView.swift */; }; + 808CA1A728354005002EDD59 /* XCTest.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 808CA1A628354005002EDD59 /* XCTest.framework */; }; 809B942327221EC800B1DAE2 /* CategoryFormSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 809B942227221EC800B1DAE2 /* CategoryFormSheet.swift */; }; 80A419ED2787C0A00090C515 /* TwigsCli.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80A419EC2787C0A00090C515 /* TwigsCli.swift */; }; 80A419F52787C1520090C515 /* ArgumentParser in Frameworks */ = {isa = PBXBuildFile; productRef = 80A419F42787C1520090C515 /* ArgumentParser */; }; 80D1FC14277C1EF9007F17FB /* InlineLoadingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80D1FC13277C1EF9007F17FB /* InlineLoadingView.swift */; }; + 80D2CE1A2833448500EDD6C2 /* DataStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80D2CE192833448500EDD6C2 /* DataStore.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -100,7 +96,6 @@ 2841022F2342D97300EAFA29 /* BudgetListsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BudgetListsView.swift; sourceTree = ""; }; 284102312342E12F00EAFA29 /* CategoryListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CategoryListView.swift; sourceTree = ""; }; 2857EAEC233DA30B0026BC83 /* LoadingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingView.swift; sourceTree = ""; }; - 289510232352AAFC00BC862B /* UserDataStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDataStore.swift; sourceTree = ""; }; 28AC94EA233C373900BFB70A /* Twigs.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Twigs.app; sourceTree = BUILT_PRODUCTS_DIR; }; 28AC94F1233C373900BFB70A /* LoginView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginView.swift; sourceTree = ""; }; 28AC94F3233C373A00BFB70A /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; @@ -118,20 +113,15 @@ 28AC952B233C434800BFB70A /* UserRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserRepository.swift; sourceTree = ""; }; 28B9E50D2346BCB2007C3909 /* RegistrationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RegistrationView.swift; sourceTree = ""; }; 28CE8B9423525F990072BC4C /* Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Extensions.swift; sourceTree = ""; }; - 28FE6AF32342E3CB00D5543E /* BudgetsDataStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BudgetsDataStore.swift; sourceTree = ""; }; 28FE6AF52342E4CC00D5543E /* BudgetRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BudgetRepository.swift; sourceTree = ""; }; - 28FE6AF923441E3700D5543E /* CategoryListDataStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CategoryListDataStore.swift; sourceTree = ""; }; 28FE6AFB23441E4500D5543E /* CategoryRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CategoryRepository.swift; sourceTree = ""; }; 28FE6AFF2344308600D5543E /* Transaction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Transaction.swift; sourceTree = ""; }; - 28FE6B012344331B00D5543E /* TransactionDataStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransactionDataStore.swift; sourceTree = ""; }; 28FE6B03234449DC00D5543E /* TransactionListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransactionListView.swift; sourceTree = ""; }; 28FE6B0523444A9800D5543E /* TransactionDetailsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransactionDetailsView.swift; sourceTree = ""; }; - 543ECE41233E82A40018A9D9 /* AuthenticationDataStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationDataStore.swift; sourceTree = ""; }; 8005FD5C277EAB0200E48B23 /* MainView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainView.swift; sourceTree = ""; }; 800DFC2B277FF47A00EDCE9B /* AsyncData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AsyncData.swift; sourceTree = ""; }; 801D08CB275ECEFA00931465 /* RecurringTransactionsListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecurringTransactionsListView.swift; sourceTree = ""; }; 801D08CD275F189E00931465 /* RecurringTransactionsRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecurringTransactionsRepository.swift; sourceTree = ""; }; - 801D08CF275F1AE300931465 /* RecurringTransactionDataStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecurringTransactionDataStore.swift; sourceTree = ""; }; 801D08D1275FB7DE00931465 /* RecurringTransactionDetailsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecurringTransactionDetailsView.swift; sourceTree = ""; }; 802161CF277647920075761A /* AsyncObservableObject.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AsyncObservableObject.swift; sourceTree = ""; }; 8021EFAB280A0FA100043F18 /* TwigsCore */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = TwigsCore; path = ../TwigsCore; sourceTree = ""; }; @@ -142,11 +132,13 @@ 8044BA3E27853054009A78D4 /* CategoryForm.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CategoryForm.swift; sourceTree = ""; }; 806C784F272B700B00FA1375 /* TwigsApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TwigsApp.swift; sourceTree = ""; }; 80820144275FFD380040996E /* SidebarBudgetView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarBudgetView.swift; sourceTree = ""; }; + 808CA1A628354005002EDD59 /* XCTest.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = XCTest.framework; path = Platforms/iPhoneOS.platform/Developer/Library/Frameworks/XCTest.framework; sourceTree = DEVELOPER_DIR; }; 809B942227221EC800B1DAE2 /* CategoryFormSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CategoryFormSheet.swift; sourceTree = ""; }; 809B94242722597800B1DAE2 /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/Localizable.strings; sourceTree = ""; }; 80A419EA2787C0A00090C515 /* twigs-cli */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = "twigs-cli"; sourceTree = BUILT_PRODUCTS_DIR; }; 80A419EC2787C0A00090C515 /* TwigsCli.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TwigsCli.swift; sourceTree = ""; }; 80D1FC13277C1EF9007F17FB /* InlineLoadingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InlineLoadingView.swift; sourceTree = ""; }; + 80D2CE192833448500EDD6C2 /* DataStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataStore.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -164,6 +156,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 808CA1A728354005002EDD59 /* XCTest.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -197,7 +190,6 @@ isa = PBXGroup; children = ( 2841022F2342D97300EAFA29 /* BudgetListsView.swift */, - 28FE6AF32342E3CB00D5543E /* BudgetsDataStore.swift */, 28FE6AF52342E4CC00D5543E /* BudgetRepository.swift */, 282126BA235CDD3C00072D52 /* BudgetDetailsView.swift */, ); @@ -207,7 +199,6 @@ 2841022A2342D8CB00EAFA29 /* Category */ = { isa = PBXGroup; children = ( - 28FE6AF923441E3700D5543E /* CategoryListDataStore.swift */, 8043EB83271F26ED00498E73 /* CategoryDetailsView.swift */, 284102312342E12F00EAFA29 /* CategoryListView.swift */, 28FE6AFB23441E4500D5543E /* CategoryRepository.swift */, @@ -278,6 +269,7 @@ 2857EAEB233DA2F90026BC83 /* Views */, 802161CF277647920075761A /* AsyncObservableObject.swift */, 800DFC2B277FF47A00EDCE9B /* AsyncData.swift */, + 80D2CE192833448500EDD6C2 /* DataStore.swift */, ); path = Twigs; sourceTree = ""; @@ -313,7 +305,6 @@ children = ( 28AC9528233C433400BFB70A /* TransactionRepository.swift */, 28FE6AFF2344308600D5543E /* Transaction.swift */, - 28FE6B012344331B00D5543E /* TransactionDataStore.swift */, 28FE6B03234449DC00D5543E /* TransactionListView.swift */, 28FE6B0523444A9800D5543E /* TransactionDetailsView.swift */, 2821265F23555FD300072D52 /* TransactionFormSheet.swift */, @@ -335,8 +326,6 @@ isa = PBXGroup; children = ( 28AC952B233C434800BFB70A /* UserRepository.swift */, - 543ECE41233E82A40018A9D9 /* AuthenticationDataStore.swift */, - 289510232352AAFC00BC862B /* UserDataStore.swift */, ); path = User; sourceTree = ""; @@ -344,6 +333,7 @@ 8005FD59277E623900E48B23 /* Frameworks */ = { isa = PBXGroup; children = ( + 808CA1A628354005002EDD59 /* XCTest.framework */, ); name = Frameworks; sourceTree = ""; @@ -361,7 +351,6 @@ children = ( 801D08CB275ECEFA00931465 /* RecurringTransactionsListView.swift */, 801D08CD275F189E00931465 /* RecurringTransactionsRepository.swift */, - 801D08CF275F1AE300931465 /* RecurringTransactionDataStore.swift */, 801D08D1275FB7DE00931465 /* RecurringTransactionDetailsView.swift */, ); path = "Recurring Transactions"; @@ -552,31 +541,26 @@ 801D08D2275FB7DE00931465 /* RecurringTransactionDetailsView.swift in Sources */, 28FE6B0623444A9800D5543E /* TransactionDetailsView.swift in Sources */, 80D1FC14277C1EF9007F17FB /* InlineLoadingView.swift in Sources */, - 28FE6AF42342E3CB00D5543E /* BudgetsDataStore.swift in Sources */, 28AC952C233C434800BFB70A /* UserRepository.swift in Sources */, 28FE6B04234449DC00D5543E /* TransactionListView.swift in Sources */, 28AC94F2233C373900BFB70A /* LoginView.swift in Sources */, 802161D0277647920075761A /* AsyncObservableObject.swift in Sources */, 282126BB235CDD3C00072D52 /* BudgetDetailsView.swift in Sources */, - 801D08D0275F1AE300931465 /* RecurringTransactionDataStore.swift in Sources */, + 80D2CE1A2833448500EDD6C2 /* DataStore.swift in Sources */, 2857EAED233DA30B0026BC83 /* LoadingView.swift in Sources */, 282126A3235ABC1800072D52 /* TwigsInMemoryCacheService.swift in Sources */, 800DFC2C277FF47A00EDCE9B /* AsyncData.swift in Sources */, 8044BA3B2784B659009A78D4 /* TransactionDetails.swift in Sources */, - 28FE6AFA23441E3700D5543E /* CategoryListDataStore.swift in Sources */, 28B9E50E2346BCB2007C3909 /* RegistrationView.swift in Sources */, 284102322342E12F00EAFA29 /* CategoryListView.swift in Sources */, 8043EB84271F26ED00498E73 /* CategoryDetailsView.swift in Sources */, - 289510242352AAFC00BC862B /* UserDataStore.swift in Sources */, 28FE6B002344308600D5543E /* Transaction.swift in Sources */, 8005FD5D277EAB0200E48B23 /* MainView.swift in Sources */, 282126A1235929B800072D52 /* ProfileView.swift in Sources */, 28AC9529233C433400BFB70A /* TransactionRepository.swift in Sources */, 809B942327221EC800B1DAE2 /* CategoryFormSheet.swift in Sources */, 28FE6AF62342E4CC00D5543E /* BudgetRepository.swift in Sources */, - 28FE6B022344331B00D5543E /* TransactionDataStore.swift in Sources */, 8044BA3F27853054009A78D4 /* CategoryForm.swift in Sources */, - 543ECE42233E82A40018A9D9 /* AuthenticationDataStore.swift in Sources */, 80820145275FFD380040996E /* SidebarBudgetView.swift in Sources */, 8044BA3927828E9D009A78D4 /* CategoryDataStore.swift in Sources */, 284102302342D97300EAFA29 /* BudgetListsView.swift in Sources */, diff --git a/Twigs/Budget/BudgetDetailsView.swift b/Twigs/Budget/BudgetDetailsView.swift index 3333517..ee35350 100644 --- a/Twigs/Budget/BudgetDetailsView.swift +++ b/Twigs/Budget/BudgetDetailsView.swift @@ -10,14 +10,14 @@ import SwiftUI import TwigsCore struct BudgetDetailsView: View { - @EnvironmentObject var budgetDataStore: BudgetsDataStore + @EnvironmentObject var dataStore: DataStore let budget: Budget @ViewBuilder var body: some View { InlineLoadingView( - data: self.$budgetDataStore.overview, - action: { await self.budgetDataStore.loadOverview(self.budget) }, + data: self.$dataStore.overview, + action: { await self.dataStore.loadOverview(self.budget) }, errorTextLocalizedStringKey: "budgets_load_failure" ) { overview in List { @@ -96,7 +96,7 @@ struct ExpensesOverview: View { struct BudgetDetailsView_Previews: PreviewProvider { static var previews: some View { BudgetDetailsView(budget: MockBudgetRepository.budget) - .environmentObject(BudgetsDataStore(budgetRepository: MockBudgetRepository(), categoryRepository: MockCategoryRepository(), transactionRepository: MockTransactionRepository())) + .environmentObject(TwigsInMemoryCacheService()) } } #endif diff --git a/Twigs/Budget/BudgetListsView.swift b/Twigs/Budget/BudgetListsView.swift index fbaf3a3..9c9ce24 100644 --- a/Twigs/Budget/BudgetListsView.swift +++ b/Twigs/Budget/BudgetListsView.swift @@ -12,12 +12,12 @@ import Combine import TwigsCore struct BudgetListsView: View { - @EnvironmentObject var budgetDataStore: BudgetsDataStore + @EnvironmentObject var dataStore: DataStore var body: some View { InlineLoadingView( - data: $budgetDataStore.budgets, - action: { await self.budgetDataStore.getBudgets(count: nil, page: nil) }, + data: $dataStore.budgets, + action: { await self.dataStore.getBudgets(count: nil, page: nil) }, errorTextLocalizedStringKey: "budgets_load_failure" ) { (budgets: [Budget]) in Section("budgets") { @@ -30,13 +30,13 @@ struct BudgetListsView: View { } struct BudgetListItemView: View { - @EnvironmentObject var budgetDataStore: BudgetsDataStore + @EnvironmentObject var dataStore: DataStore let budget: Budget var body: some View { Button( action: { - self.budgetDataStore.selectBudget(budget) + self.dataStore.selectBudget(budget) }, label: { VStack(alignment: .leading) { diff --git a/Twigs/Budget/BudgetsDataStore.swift b/Twigs/Budget/BudgetsDataStore.swift deleted file mode 100644 index 7547c24..0000000 --- a/Twigs/Budget/BudgetsDataStore.swift +++ /dev/null @@ -1,99 +0,0 @@ -// -// BudgetsDataStore.swift -// Budget -// -// Created by Billy Brawner on 9/30/19. -// Copyright © 2019 William Brawner. All rights reserved. -// - -import Foundation -import Combine -import TwigsCore - -private let LAST_BUDGET = "LAST_BUDGET" - -@MainActor -class BudgetsDataStore: ObservableObject { - private let budgetRepository: BudgetRepository - private let categoryRepository: CategoryRepository - private let transactionRepository: TransactionRepository - @Published var budgets: AsyncData<[Budget]> = .empty - @Published var budget: AsyncData = .empty { - didSet { - self.overview = .empty - if case let .success(budget) = self.budget { - UserDefaults.standard.set(budget.id, forKey: LAST_BUDGET) - self.showBudgetSelection = false - Task { - await loadOverview(budget) - } - } - } - } - @Published var overview: AsyncData = .empty - @Published var showBudgetSelection: Bool = true - - init(budgetRepository: BudgetRepository, categoryRepository: CategoryRepository, transactionRepository: TransactionRepository) { - self.budgetRepository = budgetRepository - self.categoryRepository = categoryRepository - self.transactionRepository = transactionRepository - } - - func getBudgets(count: Int? = nil, page: Int? = nil) async { - // TODO: Find some way to extract this to a generic function - self.budgets = .loading - do { - let budgets = try await self.budgetRepository.getBudgets(count: count, page: page).sorted(by: { $0.name < $1.name }) - self.budgets = .success(budgets) - if self.budget != .empty { - return - } - if let id = UserDefaults.standard.string(forKey: LAST_BUDGET), let lastBudget = budgets.first(where: { $0.id == id }) { - self.budget = .success(lastBudget) - } else { - if let budget = budgets.first { - self.budget = .success(budget) - } - } - } catch { - self.budgets = .error(error) - } - } - - func loadOverview(_ budget: Budget) async { - self.overview = .loading - do { - let budgetBalance = try await self.transactionRepository.sumTransactions(budgetId: budget.id, categoryId: nil, from: nil, to: nil) - let categories = try await self.categoryRepository.getCategories(budgetId: budget.id, expense: nil, archived: false, count: nil, page: nil) - var budgetOverview = BudgetOverview(budget: budget, balance: budgetBalance.balance) - try await withThrowingTaskGroup(of: (TwigsCore.Category, BalanceResponse).self) { group in - for category in categories { - group.addTask { - return (category, try await self.transactionRepository.sumTransactions(budgetId: nil, categoryId: category.id, from: nil, to: nil)) - } - } - - for try await (category, response) in group { - if category.expense { - budgetOverview.expectedExpenses += category.amount - } else { - budgetOverview.expectedIncome += category.amount - } - - if category.expense { - budgetOverview.actualExpenses += abs(response.balance) - } else { - budgetOverview.actualIncome += response.balance - } - } - } - self.overview = .success(budgetOverview) - } catch { - self.overview = .error(error) - } - } - - func selectBudget(_ budget: Budget) { - self.budget = .success(budget) - } -} diff --git a/Twigs/Category/CategoryDataStore.swift b/Twigs/Category/CategoryDataStore.swift index 29f6bee..b8e5260 100644 --- a/Twigs/Category/CategoryDataStore.swift +++ b/Twigs/Category/CategoryDataStore.swift @@ -12,16 +12,16 @@ import TwigsCore @MainActor class CategoryDataStore: ObservableObject { @Published var sum: AsyncData = .empty - let transactionRepository: TransactionRepository + let apiService: TwigsApiService - init(transactionRepository: TransactionRepository) { - self.transactionRepository = transactionRepository + init(_ apiService: TwigsApiService) { + self.apiService = apiService } func sum(categoryId: String, from: Date? = nil, to: Date? = nil) async { self.sum = .loading do { - let sum = try await self.transactionRepository.sumTransactions(budgetId: nil, categoryId: categoryId, from: from, to: to).balance + let sum = try await self.apiService.sumTransactions(budgetId: nil, categoryId: categoryId, from: from, to: to).balance self.sum = .success(sum) } catch { self.sum = .error(error) diff --git a/Twigs/Category/CategoryDetailsView.swift b/Twigs/Category/CategoryDetailsView.swift index d588cfb..9623ac4 100644 --- a/Twigs/Category/CategoryDetailsView.swift +++ b/Twigs/Category/CategoryDetailsView.swift @@ -10,9 +10,8 @@ import SwiftUI import TwigsCore struct CategoryDetailsView: View { - @EnvironmentObject var categoryListDataStore: CategoryListDataStore + @EnvironmentObject var dataStore: DataStore @EnvironmentObject var categoryDataStore: CategoryDataStore - @EnvironmentObject var transactionDataStore: TransactionDataStore @EnvironmentObject var apiService: TwigsApiService let budget: Budget @State var sum: Int? = 0 @@ -35,8 +34,8 @@ struct CategoryDetailsView: View { } var body: some View { - if let category = categoryListDataStore.selectedCategory { - TransactionListView(apiService: apiService, budget: budget, category: category) { + if let category = dataStore.selectedCategory { + TransactionListView() { VStack { Text(verbatim: category.description ?? "") .padding() @@ -59,7 +58,7 @@ struct CategoryDetailsView: View { }, content: { CategoryFormSheet(categoryForm: CategoryForm( category: category, - categoryList: categoryListDataStore, + dataStore: dataStore, budgetId: category.budgetId )) }) @@ -86,7 +85,7 @@ struct LabeledCounter: View { struct CategoryDetailsView_Previews: PreviewProvider { static var previews: some View { CategoryDetailsView(MockBudgetRepository.budget) - .environmentObject(TransactionDataStore(MockTransactionRepository())) + .environmentObject(TwigsInMemoryCacheService()) } } #endif diff --git a/Twigs/Category/CategoryForm.swift b/Twigs/Category/CategoryForm.swift index 3d39475..60f0d04 100644 --- a/Twigs/Category/CategoryForm.swift +++ b/Twigs/Category/CategoryForm.swift @@ -11,7 +11,7 @@ import TwigsCore class CategoryForm: ObservableObject { let category: TwigsCore.Category? - let categoryList: CategoryListDataStore + let dataStore: DataStore let budgetId: String let categoryId: String @Published var title: String @@ -23,11 +23,11 @@ class CategoryForm: ObservableObject { init( category: TwigsCore.Category?, - categoryList: CategoryListDataStore, + dataStore: DataStore, budgetId: String ) { self.category = category - self.categoryList = categoryList + self.dataStore = dataStore self.budgetId = budgetId self.categoryId = category?.id ?? "" self.showDelete = !self.categoryId.isEmpty @@ -42,7 +42,7 @@ class CategoryForm: ObservableObject { func save() async { let amount = Int((Double(self.amount) ?? 0.0) * 100) - await categoryList.save(TwigsCore.Category( + await dataStore.save(TwigsCore.Category( budgetId: budgetId, id: categoryId, title: title, @@ -57,6 +57,6 @@ class CategoryForm: ObservableObject { guard let category = self.category else { return } - await categoryList.delete(category) + await dataStore.delete(category) } } diff --git a/Twigs/Category/CategoryFormSheet.swift b/Twigs/Category/CategoryFormSheet.swift index 7fa8629..da721be 100644 --- a/Twigs/Category/CategoryFormSheet.swift +++ b/Twigs/Category/CategoryFormSheet.swift @@ -10,13 +10,13 @@ import SwiftUI import TwigsCore struct CategoryFormSheet: View { - @EnvironmentObject var categoryList: CategoryListDataStore + @EnvironmentObject var dataStore: DataStore @ObservedObject var categoryForm: CategoryForm @State private var showingAlert = false @ViewBuilder var stateContent: some View { - switch categoryList.category { + switch dataStore.category { case .success(_): EmptyView() case .saving(_): @@ -62,7 +62,7 @@ struct CategoryFormSheet: View { stateContent .navigationBarItems( leading: Button("cancel") { - categoryList.cancelEdit() + dataStore.cancelEditCategory() }, trailing: Button("save") { Task { diff --git a/Twigs/Category/CategoryListDataStore.swift b/Twigs/Category/CategoryListDataStore.swift deleted file mode 100644 index 4957699..0000000 --- a/Twigs/Category/CategoryListDataStore.swift +++ /dev/null @@ -1,90 +0,0 @@ -// -// CategoryListDataStore.swift -// Budget -// -// Created by Billy Brawner on 10/1/19. -// Copyright © 2019 William Brawner. All rights reserved. -// - -import Foundation -import Combine -import TwigsCore - -@MainActor -class CategoryListDataStore: ObservableObject { - @Published var categories: AsyncData<[TwigsCore.Category]> = .empty - @Published var category: AsyncData = .empty { - didSet { - if case let .success(category) = self.category { - self.selectedCategory = category - } else if case .empty = self.category { - self.selectedCategory = nil - } - } - } - @Published var selectedCategory: TwigsCore.Category? = nil - - func getCategories(budgetId: String? = nil, expense: Bool? = nil, archived: Bool? = false, count: Int? = nil, page: Int? = nil) async { - self.categories = .loading - do { - let categories = try await categoryRepository.getCategories(budgetId: budgetId, expense: expense, archived: archived, count: count, page: page) - self.categories = .success(categories) - } catch { - self.categories = .error(error) - } - } - - func save(_ category: TwigsCore.Category) async { - self.category = .loading - do { - var savedCategory: TwigsCore.Category - if category.id != "" { - savedCategory = try await self.categoryRepository.updateCategory(category) - } else { - savedCategory = try await self.categoryRepository.createCategory(category) - } - self.category = .success(savedCategory) - if case let .success(categories) = self.categories { - var updatedCategories = categories.filter(withoutId: category.id) - updatedCategories.append(savedCategory) - self.categories = .success(updatedCategories.sorted(by: { $0.title < $1.title })) - } - } catch { - self.category = .error(error, category) - } - } - - func delete(_ category: TwigsCore.Category) async { - self.category = .loading - do { - try await self.categoryRepository.deleteCategory(category.id) - self.category = .empty - if case let .success(categories) = self.categories { - self.categories = .success(categories.filter(withoutId: category.id)) - } - } catch { - self.category = .error(error, category) - } - } - - func edit(_ category: TwigsCore.Category) async { - self.category = .editing(category) - } - - func cancelEdit() { - if let category = self.selectedCategory { - self.category = .success(category) - } else { - self.category = .empty - } - } - - func clearSelectedCategory() { - self.category = .empty - } - - private let categoryRepository: CategoryRepository - init(_ categoryRepository: CategoryRepository) { - self.categoryRepository = categoryRepository - } -} diff --git a/Twigs/Category/CategoryListView.swift b/Twigs/Category/CategoryListView.swift index 573dfaf..a3bcea6 100644 --- a/Twigs/Category/CategoryListView.swift +++ b/Twigs/Category/CategoryListView.swift @@ -11,26 +11,25 @@ import Combine import TwigsCore struct CategoryListView: View { - @EnvironmentObject var categoryDataStore: CategoryListDataStore - @EnvironmentObject var apiService: TwigsApiService + @EnvironmentObject var dataStore: DataStore @State var requestId: String = "" @ViewBuilder var body: some View { InlineLoadingView( - data: $categoryDataStore.categories, - action: { await self.categoryDataStore.getCategories(budgetId: budget.id, expense: nil, archived: nil, count: nil, page: nil) }, + data: $dataStore.categories, + action: { await self.dataStore.getCategories(budgetId: budget.id, expense: nil, archived: nil, count: nil, page: nil) }, errorTextLocalizedStringKey: "Failed to load categories" ) { categories in List { Section { ForEach(categories.filter { !$0.archived }) { category in - CategoryListItemView(CategoryDataStore(transactionRepository: apiService), budget: budget, category: category) + CategoryListItemView(CategoryDataStore(dataStore.apiService), budget: budget, category: category) } } Section("Archived") { ForEach(categories.filter { $0.archived }) { category in - CategoryListItemView(CategoryDataStore(transactionRepository: apiService), budget: budget, category: category) + CategoryListItemView(CategoryDataStore(dataStore.apiService), budget: budget, category: category) } } } @@ -46,7 +45,7 @@ struct CategoryListView: View { struct CategoryListItemView: View { let category: TwigsCore.Category let budget: Budget - @EnvironmentObject var categoryListDataStore: CategoryListDataStore + @EnvironmentObject var dataStore: DataStore @ObservedObject var categoryDataStore: CategoryDataStore init(_ categoryDataStore: CategoryDataStore, budget: Budget, category: TwigsCore.Category) { @@ -68,11 +67,11 @@ struct CategoryListItemView: View { var body: some View { NavigationLink( tag: category, - selection: $categoryListDataStore.selectedCategory, + selection: $dataStore.selectedCategory, destination: { CategoryDetailsView(self.budget) .environmentObject(categoryDataStore) - .navigationBarTitle(categoryListDataStore.selectedCategory?.title ?? "") + .navigationBarTitle(dataStore.selectedCategory?.title ?? "") }, label: { VStack(alignment: .leading) { diff --git a/Twigs/DataStore.swift b/Twigs/DataStore.swift new file mode 100644 index 0000000..8d44e0b --- /dev/null +++ b/Twigs/DataStore.swift @@ -0,0 +1,460 @@ +// +// DataStore.swift +// Twigs +// +// Created by William Brawner on 5/16/22. +// Copyright © 2022 William Brawner. All rights reserved. +// + +import Collections +import Combine +import Foundation +import TwigsCore + +private let LAST_BUDGET = "LAST_BUDGET" + +@MainActor +class DataStore : ObservableObject { + let apiService: TwigsApiService + @Published var budgets: AsyncData<[Budget]> = .empty + @Published var budget: AsyncData = .empty { + didSet { + self.overview = .empty + if case let .success(budget) = self.budget { + UserDefaults.standard.set(budget.id, forKey: LAST_BUDGET) + self.showBudgetSelection = false + Task { + await loadOverview(budget) + } + } + } + } + @Published var overview: AsyncData = .empty + @Published var showBudgetSelection: Bool = true + + init( + _ apiService: TwigsApiService + ) { + self.apiService = apiService + self.baseUrl = UserDefaults.standard.string(forKey: KEY_BASE_URL) + self.token = UserDefaults.standard.string(forKey: KEY_TOKEN) + self.userId = UserDefaults.standard.string(forKey: KEY_USER_ID) + } + + func getBudgets(count: Int? = nil, page: Int? = nil) async { + // TODO: Find some way to extract this to a generic function + self.budgets = .loading + do { + let budgets = try await self.apiService.getBudgets(count: count, page: page).sorted(by: { $0.name < $1.name }) + self.budgets = .success(budgets) + if self.budget != .empty { + return + } + if let id = UserDefaults.standard.string(forKey: LAST_BUDGET), let lastBudget = budgets.first(where: { $0.id == id }) { + self.budget = .success(lastBudget) + } else { + if let budget = budgets.first { + self.budget = .success(budget) + } + } + } catch { + self.budgets = .error(error) + } + } + + func loadOverview(_ budget: Budget) async { + self.overview = .loading + do { + let budgetBalance = try await self.apiService.sumTransactions(budgetId: budget.id, categoryId: nil, from: nil, to: nil) + let categories = try await self.apiService.getCategories(budgetId: budget.id, expense: nil, archived: false, count: nil, page: nil) + var budgetOverview = BudgetOverview(budget: budget, balance: budgetBalance.balance) + try await withThrowingTaskGroup(of: (TwigsCore.Category, BalanceResponse).self) { group in + for category in categories { + group.addTask { + return (category, try await self.apiService.sumTransactions(budgetId: nil, categoryId: category.id, from: nil, to: nil)) + } + } + + for try await (category, response) in group { + if category.expense { + budgetOverview.expectedExpenses += category.amount + } else { + budgetOverview.expectedIncome += category.amount + } + + if category.expense { + budgetOverview.actualExpenses += abs(response.balance) + } else { + budgetOverview.actualIncome += response.balance + } + } + } + self.overview = .success(budgetOverview) + } catch { + self.overview = .error(error) + } + } + + func selectBudget(_ budget: Budget) { + self.budget = .success(budget) + } + + @Published var categories: AsyncData<[TwigsCore.Category]> = .empty + @Published var category: AsyncData = .empty { + didSet { + if case let .success(category) = self.category { + self.selectedCategory = category + } else if case .empty = self.category { + self.selectedCategory = nil + } + } + } + @Published var selectedCategory: TwigsCore.Category? = nil + + func getCategories(budgetId: String? = nil, expense: Bool? = nil, archived: Bool? = false, count: Int? = nil, page: Int? = nil) async { + self.categories = .loading + do { + let categories = try await apiService.getCategories(budgetId: budgetId, expense: expense, archived: archived, count: count, page: page) + self.categories = .success(categories) + } catch { + self.categories = .error(error) + } + } + + func save(_ category: TwigsCore.Category) async { + self.category = .loading + do { + var savedCategory: TwigsCore.Category + if category.id != "" { + savedCategory = try await self.apiService.updateCategory(category) + } else { + savedCategory = try await self.apiService.createCategory(category) + } + self.category = .success(savedCategory) + if case let .success(categories) = self.categories { + var updatedCategories = categories.filter(withoutId: category.id) + updatedCategories.append(savedCategory) + self.categories = .success(updatedCategories.sorted(by: { $0.title < $1.title })) + } + } catch { + self.category = .error(error, category) + } + } + + func delete(_ category: TwigsCore.Category) async { + self.category = .loading + do { + try await self.apiService.deleteCategory(category.id) + self.category = .empty + if case let .success(categories) = self.categories { + self.categories = .success(categories.filter(withoutId: category.id)) + } + } catch { + self.category = .error(error, category) + } + } + + func edit(_ category: TwigsCore.Category) async { + self.category = .editing(category) + } + + func cancelEditCategory() { + if let category = self.selectedCategory { + self.category = .success(category) + } else { + self.category = .empty + } + } + + func clearSelectedCategory() { + self.category = .empty + } + + @Published var recurringTransactions: AsyncData<[RecurringTransaction]> = .empty + @Published var recurringTransaction: AsyncData = .empty { + didSet { + if case let .success(transaction) = self.recurringTransaction { + self.selectedRecurringTransaction = transaction + } else if case .empty = recurringTransaction { + self.selectedRecurringTransaction = nil + } + } + } + @Published var selectedRecurringTransaction: RecurringTransaction? = nil + + func getRecurringTransactions() async { + guard case let .success(budget) = self.budget else { + return + } + self.recurringTransactions = .loading + do { + let transactions = try await self.apiService.getRecurringTransactions(budget.id) + self.recurringTransactions = .success(transactions.sorted(by: { $0.title < $1.title })) + } catch { + self.recurringTransactions = .error(error) + } + } + + func saveRecurringTransaction(_ transaction: RecurringTransaction) async { + self.recurringTransaction = .loading + do { + var savedTransaction: RecurringTransaction + if (transaction.id != "") { + savedTransaction = try await self.apiService.updateRecurringTransaction(transaction) + } else { + savedTransaction = try await self.apiService.createRecurringTransaction(transaction) + } + self.recurringTransaction = .success(savedTransaction) + if case var .success(transactions) = self.recurringTransactions { + transactions = transactions.filter(withoutId: savedTransaction.id) + transactions.append(savedTransaction) + self.recurringTransactions = .success(transactions.sorted(by: { $0.title < $1.title })) + } + } catch { + self.recurringTransactions = .error(error) + } + } + + func deleteRecurringTransaction(_ transaction: RecurringTransaction) async { + self.recurringTransaction = .loading + do { + try await self.apiService.deleteRecurringTransaction(transaction.id) + self.recurringTransaction = .empty + if case let .success(transactions) = self.recurringTransactions { + self.recurringTransactions = .success(transactions.filter(withoutId: transaction.id)) + } + } catch { + self.recurringTransaction = .error(error, transaction) + } + } + + func clearSelectedRecurringTransaction() { + self.recurringTransaction = .empty + } + + @Published var transactions: AsyncData> = .empty + @Published var transaction: AsyncData = .empty { + didSet { + if case let .success(transaction) = self.transaction { + self.selectedTransaction = transaction + } else if case .empty = self.transaction { + self.selectedTransaction = nil + } + } + } + @Published var selectedTransaction: Transaction? = nil + private var budgetId: String = "" + private var categoryId: String? = nil + private var from: Date? = nil + private var count: Int? = nil + private var page: Int? = nil + + func getTransactions(from: Date? = nil, count: Int? = nil, page: Int? = nil) async { + self.from = from + self.count = count + self.page = page + await self.getTransactions() + } + + func getTransactions() async { + guard case let .success(budget) = self.budget else { + self.transactions = .error(NetworkError.unknown) + return + } + self.budgetId = budget.id + if case let .success(category) = self.category { + self.categoryId = category.id + } else { + self.categoryId = nil + } + self.transactions = .loading + do { + var categoryIds: [String] = [] + if let categoryId = categoryId { + categoryIds.append(categoryId) + } + let transactions = try await self.apiService.getTransactions( + budgetIds: [budgetId], + categoryIds: categoryIds, + from: from ?? Date.firstOfMonth, + to: nil, + count: count, + page: page + ) + let groupedTransactions = OrderedDictionary(grouping: transactions, by: { $0.date.toLocaleString() }) + self.transactions = .success(groupedTransactions) + } catch { + self.transactions = .error(error) + } + } + + func saveTransaction(_ transaction: Transaction) async { + self.transaction = .saving(transaction) + do { + var savedTransaction: Transaction + if (transaction.id != "") { + savedTransaction = try await self.apiService.updateTransaction(transaction) + } else { + savedTransaction = try await self.apiService.createTransaction(transaction) + } + self.transaction = .success(savedTransaction) + await getTransactions() + } catch { + self.transaction = .error(error, transaction) + } + } + + func deleteTransaction(_ transaction: Transaction) async { + self.transaction = .loading + do { + try await self.apiService.deleteTransaction(transaction.id) + self.transaction = .empty + } catch { + self.transaction = .error(error, transaction) + } + } + + func newTransaction() { + var budgetId = "" + if case let .success(budget) = self.budget { + budgetId = budget.id + } + var categoryId: String? = nil + if case let .success(category) = self.category { + categoryId = category.id + } + var createdBy = "" + if case let .success(user) = self.currentUser { + createdBy = user.id + } + self.transaction = .editing(TwigsCore.Transaction(categoryId: categoryId, createdBy: createdBy, budgetId: budgetId)) + } + + func editTransaction(_ transaction: Transaction) { + self.transaction = .editing(transaction) + } + + func cancelEditTransaction() { + if let transaction = self.selectedTransaction { + self.transaction = .success(transaction) + } else { + self.transaction = .empty + } + } + + func clearSelectedTransaction() { + self.transaction = .empty + } + + @Published var currentUser: AsyncData = .empty { + didSet { + if case .success(_) = self.currentUser { + self.showLogin = false + } else { + self.showLogin = true + } + } + } + + private let KEY_BASE_URL = "BASE_URL" + private let KEY_TOKEN = "TOKEN" + private let KEY_USER_ID = "USER_ID" + + @Published var baseUrl: String? { + didSet { + self.apiService.baseUrl = baseUrl + UserDefaults.standard.set(baseUrl, forKey: KEY_BASE_URL) + } + } + @Published var token: String? { + didSet { + self.apiService.token = token + UserDefaults.standard.set(token, forKey: KEY_TOKEN) + } + } + @Published var userId: String? { + didSet { + UserDefaults.standard.set(userId, forKey: KEY_USER_ID) + } + } + @Published var showLogin: Bool = true + + func login(server: String, username: String, password: String) async { + self.currentUser = .loading + self.apiService.baseUrl = server + // The API Service applies some validation and correcting of the server before returning it so we use that + // value instead of the original one + self.baseUrl = self.apiService.baseUrl ?? "" + do { + let response = try await self.apiService.login(username: username, password: password) + self.token = response.token + self.userId = response.userId + await self.loadProfile() + } catch { + switch error { + case NetworkError.jsonParsingFailed(let jsonError): + print(jsonError.localizedDescription) + default: + print(error.localizedDescription) + } + self.currentUser = .error(error) + } + } + + func register(server: String, username: String, email: String, password: String, confirmPassword: String) async { + // TODO: Validate other fields as well + if !password.elementsEqual(confirmPassword) { + // TODO: Show error message to user + return + } + + self.apiService.baseUrl = server + // The API Service applies some validation and correcting of the server before returning it so we use that + // value instead of the original one + self.baseUrl = self.apiService.baseUrl ?? "" + do { + _ = try await apiService.register(username: username, email: email, password: password) + } catch { + switch error { + case NetworkError.jsonParsingFailed(let jsonError): + print(jsonError.localizedDescription) + default: + print(error.localizedDescription) + } + return + } + await self.login(server: server, username: username, password: password) + } + + func loadProfile() async { + guard let userId = self.userId, !userId.isEmpty else { + self.currentUser = .error(UserStatus.unauthenticated) + return + } + do { + let user = try await self.apiService.getUser(userId) + self.currentUser = .success(user) + } catch { + self.currentUser = .error(error) + } + } + + @Published var user: AsyncData = .empty + + func getUser(_ id: String) async { + do { + let user = try await self.apiService.getUser(id) + self.user = .success(user) + } catch { + self.currentUser = .error(error) + } + } +} + +enum UserStatus: Error, Equatable { + case unauthenticated + case authenticating + case failedAuthentication + case authenticated + case passwordMismatch +} diff --git a/Twigs/LoginView.swift b/Twigs/LoginView.swift index ecacc1f..9ca6cba 100644 --- a/Twigs/LoginView.swift +++ b/Twigs/LoginView.swift @@ -13,7 +13,7 @@ struct LoginView: View { @State var server: String = "" @State var username: String = "" @State var password: String = "" - @EnvironmentObject var dataStore: AuthenticationDataStore + @EnvironmentObject var dataStore: DataStore var loading: Bool { switch dataStore.user { case .loading: diff --git a/Twigs/Profile/ProfileView.swift b/Twigs/Profile/ProfileView.swift index a7d6253..47de6f4 100644 --- a/Twigs/Profile/ProfileView.swift +++ b/Twigs/Profile/ProfileView.swift @@ -10,8 +10,9 @@ import SwiftUI import TwigsCore struct ProfileView: View { - @EnvironmentObject var authDataStore: AuthenticationDataStore + @EnvironmentObject var dataStore: DataStore + @ViewBuilder var body: some View { VStack(spacing: 10) { Image(systemName: "person.circle.fill") @@ -21,7 +22,9 @@ struct ProfileView: View { .clipShape(Circle()) .overlay(Circle().stroke(Color.white, lineWidth: 4)) .shadow(radius: 5) - Text(authDataStore.currentUser!.username) + if case let .success(user) = self.dataStore.currentUser { + Text(user.username) + } NavigationLink(destination: EmptyView()) { Text("change_password") } diff --git a/Twigs/Recurring Transactions/RecurringTransactionDataStore.swift b/Twigs/Recurring Transactions/RecurringTransactionDataStore.swift deleted file mode 100644 index d9472e0..0000000 --- a/Twigs/Recurring Transactions/RecurringTransactionDataStore.swift +++ /dev/null @@ -1,79 +0,0 @@ -// -// RecurringTransactionDataStore.swift -// Twigs -// -// Created by William Brawner on 12/6/21. -// Copyright © 2021 William Brawner. All rights reserved. -// - -import Foundation -import Combine -import Collections -import TwigsCore - -@MainActor -class RecurringTransactionDataStore: AsyncObservableObject { - private let repository: RecurringTransactionsRepository - @Published var transactions: AsyncData<[RecurringTransaction]> = .empty - @Published var transaction: AsyncData = .empty { - didSet { - if case let .success(transaction) = self.transaction { - self.selectedTransaction = transaction - } else if case .empty = transaction { - self.selectedTransaction = nil - } - } - } - @Published var selectedTransaction: RecurringTransaction? = nil - - init(_ repository: RecurringTransactionsRepository) { - self.repository = repository - } - - func getRecurringTransactions(_ budgetId: String) async { - self.transactions = .loading - do { - let transactions = try await self.repository.getRecurringTransactions(budgetId) - self.transactions = .success(transactions.sorted(by: { $0.title < $1.title })) - } catch { - self.transactions = .error(error) - } - } - - func saveRecurringTransaction(_ transaction: RecurringTransaction) async { - self.transaction = .loading - do { - var savedTransaction: RecurringTransaction - if (transaction.id != "") { - savedTransaction = try await self.repository.updateRecurringTransaction(transaction) - } else { - savedTransaction = try await self.repository.createRecurringTransaction(transaction) - } - self.transaction = .success(savedTransaction) - if case var .success(transactions) = self.transactions { - transactions = transactions.filter(withoutId: savedTransaction.id) - transactions.append(savedTransaction) - self.transactions = .success(transactions.sorted(by: { $0.title < $1.title })) - } - } catch { - self.transactions = .error(error) - } - } - - func deleteRecurringTransaction(_ transaction: RecurringTransaction) async { - self.transactions = .loading - do { - try await self.repository.deleteRecurringTransaction(transaction.id) - self.transaction = .empty - if case let .success(transactions) = self.transactions { - self.transactions = .success(transactions.filter(withoutId: transaction.id)) - } - } catch { - self.transaction = .error(error, transaction) - } - } - - func clearSelectedRecurringTransaction() { - self.transaction = .empty - } -} diff --git a/Twigs/Recurring Transactions/RecurringTransactionDetailsView.swift b/Twigs/Recurring Transactions/RecurringTransactionDetailsView.swift index 8db05a7..68387bb 100644 --- a/Twigs/Recurring Transactions/RecurringTransactionDetailsView.swift +++ b/Twigs/Recurring Transactions/RecurringTransactionDetailsView.swift @@ -10,10 +10,10 @@ import SwiftUI import TwigsCore struct RecurringTransactionDetailsView: View { - @EnvironmentObject var dataStore: RecurringTransactionDataStore + @EnvironmentObject var dataStore: DataStore var body: some View { - if let transaction = dataStore.selectedTransaction { + if let transaction = dataStore.selectedRecurringTransaction { ScrollView { VStack(alignment: .leading) { Text(transaction.title) diff --git a/Twigs/Recurring Transactions/RecurringTransactionsListView.swift b/Twigs/Recurring Transactions/RecurringTransactionsListView.swift index a64de90..c735cc9 100644 --- a/Twigs/Recurring Transactions/RecurringTransactionsListView.swift +++ b/Twigs/Recurring Transactions/RecurringTransactionsListView.swift @@ -10,13 +10,12 @@ import SwiftUI import TwigsCore struct RecurringTransactionsListView: View { - @ObservedObject var dataStore: RecurringTransactionDataStore - let budget: Budget + @EnvironmentObject var dataStore: DataStore var body: some View { InlineLoadingView( - data: $dataStore.transactions, - action: { await self.dataStore.getRecurringTransactions(self.budget.id) }, + data: $dataStore.recurringTransactions, + action: { await self.dataStore.getRecurringTransactions() }, errorTextLocalizedStringKey: "Failed to load recurring transactions" ) { (transactions: [RecurringTransaction]) in List { @@ -31,13 +30,13 @@ struct RecurringTransactionsListView: View { #if DEBUG struct RecurringTransactionView_Previews: PreviewProvider { static var previews: some View { - RecurringTransactionsListView(dataStore: RecurringTransactionDataStore(MockRecurringTransactionRepository()), budget: MockBudgetRepository.budget) + RecurringTransactionsListView() } } #endif struct RecurringTransactionsListItemView: View { - @EnvironmentObject var dataStore: RecurringTransactionDataStore + @EnvironmentObject var dataStore: DataStore let transaction: RecurringTransaction init (_ transaction: RecurringTransaction) { @@ -47,7 +46,7 @@ struct RecurringTransactionsListItemView: View { var body: some View { NavigationLink( tag: transaction, - selection: $dataStore.selectedTransaction, + selection: $dataStore.selectedRecurringTransaction, destination: { RecurringTransactionDetailsView() .navigationBarTitle("details", displayMode: .inline) diff --git a/Twigs/Recurring Transactions/RecurringTransactionsRepository.swift b/Twigs/Recurring Transactions/RecurringTransactionsRepository.swift index 55cb912..1693831 100644 --- a/Twigs/Recurring Transactions/RecurringTransactionsRepository.swift +++ b/Twigs/Recurring Transactions/RecurringTransactionsRepository.swift @@ -22,7 +22,7 @@ class MockRecurringTransactionRepository: RecurringTransactionsRepository { amount: 10000, categoryId: MockCategoryRepository.category.id, expense: true, - createdBy: MockUserRepository.user.id, + createdBy: MockUserRepository.currentUser.id, budgetId: MockBudgetRepository.budget.id ) diff --git a/Twigs/RegistrationView.swift b/Twigs/RegistrationView.swift index 34b9bc9..dba8fb0 100644 --- a/Twigs/RegistrationView.swift +++ b/Twigs/RegistrationView.swift @@ -14,7 +14,7 @@ struct RegistrationView: View { @State var email: String = "" @State var password: String = "" @State var confirmedPassword: String = "" - @EnvironmentObject var dataStore: AuthenticationDataStore + @EnvironmentObject var dataStore: DataStore var body: some View { VStack { @@ -37,7 +37,7 @@ struct RegistrationView: View { .textContentType(UITextContentType.newPassword) Button("action_register", action: { Task { - try await self.dataStore.register( + await self.dataStore.register( server: self.server, username: self.username, email: self.email, diff --git a/Twigs/SidebarBudgetView.swift b/Twigs/SidebarBudgetView.swift index 791e69a..45b0b97 100644 --- a/Twigs/SidebarBudgetView.swift +++ b/Twigs/SidebarBudgetView.swift @@ -10,16 +10,14 @@ import SwiftUI import TwigsCore struct SidebarBudgetView: View { - @EnvironmentObject var authenticationDataStore: AuthenticationDataStore - @EnvironmentObject var budgetDataStore: BudgetsDataStore - @EnvironmentObject var apiService: TwigsApiService + @EnvironmentObject var dataStore: DataStore @State var isSelectingBudget = true @State var hasSelectedBudget = false @State var tabSelection: Int? = 0 @ViewBuilder var mainView: some View { - if case let .success(budget) = self.budgetDataStore.budget { + if case let .success(budget) = self.dataStore.budget { NavigationView { List { NavigationLink( @@ -33,7 +31,7 @@ struct SidebarBudgetView: View { NavigationLink( tag: 1, selection: $tabSelection, - destination: { TransactionListView(apiService: apiService, budget: budget).navigationBarTitle("transactions") }, + destination: { TransactionListView().navigationBarTitle("transactions") }, label: { Label("transactions", systemImage: "dollarsign.circle") }) .keyboardShortcut("2") NavigationLink( @@ -45,7 +43,7 @@ struct SidebarBudgetView: View { NavigationLink( tag: 3, selection: $tabSelection, - destination: { RecurringTransactionsListView(dataStore: RecurringTransactionDataStore(apiService), budget: budget).navigationBarTitle("recurring_transactions") }, + destination: { RecurringTransactionsListView().navigationBarTitle("recurring_transactions") }, label: { Label("recurring_transactions", systemImage: "arrow.triangle.2.circlepath") }) .keyboardShortcut("4") BudgetListsView() @@ -64,18 +62,18 @@ struct SidebarBudgetView: View { @ViewBuilder var body: some View { mainView - .sheet(isPresented: $authenticationDataStore.showLogin, + .sheet(isPresented: $dataStore.showLogin, onDismiss: { Task { - await self.budgetDataStore.getBudgets() + await self.dataStore.getBudgets() } }, content: { LoginView() - .environmentObject(authenticationDataStore) + .environmentObject(dataStore) .onDisappear { Task { - await self.budgetDataStore.getBudgets() + await self.dataStore.getBudgets() } } }) diff --git a/Twigs/TabbedBudgetView.swift b/Twigs/TabbedBudgetView.swift index 03c430d..b5fe9c6 100644 --- a/Twigs/TabbedBudgetView.swift +++ b/Twigs/TabbedBudgetView.swift @@ -10,21 +10,20 @@ import SwiftUI import TwigsCore struct TabbedBudgetView: View { - @EnvironmentObject var authenticationDataStore: AuthenticationDataStore - @EnvironmentObject var budgetDataStore: BudgetsDataStore + @EnvironmentObject var dataStore: DataStore @EnvironmentObject var apiService: TwigsApiService @AppStorage("budget_tab") var tabSelection: Int = 0 @ViewBuilder var mainView: some View { - if case let .success(budget) = budgetDataStore.budget { + if case let .success(budget) = dataStore.budget { TabView(selection: $tabSelection) { NavigationView { BudgetDetailsView(budget: budget) .navigationBarTitle("overview") .navigationBarItems(leading: HStack { Button("budgets", action: { - self.budgetDataStore.showBudgetSelection = true + self.dataStore.showBudgetSelection = true }).padding() }) } @@ -35,7 +34,7 @@ struct TabbedBudgetView: View { .tag(0) .keyboardShortcut("1") NavigationView { - TransactionListView(apiService: apiService, budget: budget) + TransactionListView() .navigationBarTitle("transactions") } .tabItem { @@ -55,7 +54,7 @@ struct TabbedBudgetView: View { .tag(2) .keyboardShortcut("3") NavigationView { - RecurringTransactionsListView(dataStore: RecurringTransactionDataStore(apiService), budget: budget) + RecurringTransactionsListView() .navigationBarTitle("recurring_transactions") } .tabItem { @@ -71,24 +70,24 @@ struct TabbedBudgetView: View { } var body: some View { - mainView.sheet(isPresented: $authenticationDataStore.showLogin, + mainView.sheet(isPresented: $dataStore.showLogin, onDismiss: { Task { - await self.budgetDataStore.getBudgets() + await self.dataStore.getBudgets() } }, content: { LoginView() - .environmentObject(authenticationDataStore) + .environmentObject(dataStore) .onDisappear { Task { - await self.budgetDataStore.getBudgets() + await self.dataStore.getBudgets() } } - }).sheet(isPresented: $budgetDataStore.showBudgetSelection, + }).sheet(isPresented: $dataStore.showBudgetSelection, content: { List { - BudgetListsView().environmentObject(budgetDataStore) + BudgetListsView().environmentObject(dataStore) } }) .interactiveDismissDisabled(true) diff --git a/Twigs/Transaction/TransactionDataStore.swift b/Twigs/Transaction/TransactionDataStore.swift deleted file mode 100644 index 13b1155..0000000 --- a/Twigs/Transaction/TransactionDataStore.swift +++ /dev/null @@ -1,110 +0,0 @@ -// -// TransactionDataStore.swift -// Budget -// -// Created by Billy Brawner on 10/1/19. -// Copyright © 2019 William Brawner. All rights reserved. -// - -import Foundation -import Combine -import Collections -import TwigsCore - -@MainActor -class TransactionDataStore: ObservableObject { - @Published var transactions: AsyncData> = .empty - @Published var transaction: AsyncData = .empty { - didSet { - if case let .success(transaction) = self.transaction { - self.selectedTransaction = transaction - } else if case .empty = self.transaction { - self.selectedTransaction = nil - } - } - } - @Published var selectedTransaction: Transaction? = nil - private var budgetId: String = "" - private var categoryId: String? = nil - private var from: Date? = nil - private var count: Int? = nil - private var page: Int? = nil - - func getTransactions(_ budgetId: String, categoryId: String? = nil, from: Date? = nil, count: Int? = nil, page: Int? = nil) async { - self.budgetId = budgetId - self.categoryId = categoryId - self.from = from - self.count = count - self.page = page - await self.getTransactions() - } - - func getTransactions() async { - self.transactions = .loading - do { - var categoryIds: [String] = [] - if let categoryId = categoryId { - categoryIds.append(categoryId) - } - let transactions = try await self.transactionRepository.getTransactions( - budgetIds: [budgetId], - categoryIds: categoryIds, - from: from ?? Date.firstOfMonth, - to: nil, - count: count, - page: page - ) - let groupedTransactions = OrderedDictionary(grouping: transactions, by: { $0.date.toLocaleString() }) - self.transactions = .success(groupedTransactions) - } catch { - self.transactions = .error(error) - } - } - - func saveTransaction(_ transaction: Transaction) async { - self.transaction = .saving(transaction) - do { - var savedTransaction: Transaction - if (transaction.id != "") { - savedTransaction = try await self.transactionRepository.updateTransaction(transaction) - } else { - savedTransaction = try await self.transactionRepository.createTransaction(transaction) - } - self.transaction = .success(savedTransaction) - await getTransactions() - } catch { - self.transaction = .error(error, transaction) - } - } - - func deleteTransaction(_ transaction: Transaction) async { - self.transaction = .loading - do { - try await self.transactionRepository.deleteTransaction(transaction.id) - self.transaction = .empty - } catch { - self.transaction = .error(error, transaction) - } - } - - func editTransaction(_ transaction: Transaction) { - self.transaction = .editing(transaction) - } - - func cancelEdit() { - if let transaction = self.selectedTransaction { - self.transaction = .success(transaction) - } else { - self.transaction = .empty - } - } - - func clearSelectedTransaction() { - self.transaction = .empty - } - - private let transactionRepository: TransactionRepository - init(_ transactionRepository: TransactionRepository) { - self.transactionRepository = transactionRepository - } -} diff --git a/Twigs/Transaction/TransactionDetails.swift b/Twigs/Transaction/TransactionDetails.swift index 72b9d52..af27ac2 100644 --- a/Twigs/Transaction/TransactionDetails.swift +++ b/Twigs/Transaction/TransactionDetails.swift @@ -14,14 +14,10 @@ class TransactionDetails: ObservableObject { @Published var category: AsyncData = .empty @Published var budget: AsyncData = .empty @Published var user: AsyncData = .empty - let budgetRepository: BudgetRepository - let categoryRepository: CategoryRepository - let userRepository: UserRepository + let apiService: TwigsApiService - init(budgetRepository: BudgetRepository, categoryRepository: CategoryRepository, userRepository: UserRepository) { - self.budgetRepository = budgetRepository - self.categoryRepository = categoryRepository - self.userRepository = userRepository + init(_ apiService: TwigsApiService) { + self.apiService = apiService } func loadDetails(_ transaction: TwigsCore.Transaction) async { @@ -41,7 +37,7 @@ class TransactionDetails: ObservableObject { private func loadBudget(_ id: String) async { self.budget = .loading do { - let budget = try await budgetRepository.getBudget(id) + let budget = try await apiService.getBudget(id) self.budget = .success(budget) } catch { self.budget = .error(error) @@ -51,7 +47,7 @@ class TransactionDetails: ObservableObject { private func loadCategory(_ id: String) async { self.category = .loading do { - let category = try await categoryRepository.getCategory(id) + let category = try await apiService.getCategory(id) self.category = .success(category) } catch { self.category = .error(error) @@ -61,7 +57,7 @@ class TransactionDetails: ObservableObject { private func loadUser(_ id: String) async { self.user = .loading do { - let user = try await userRepository.getUser(id) + let user = try await apiService.getUser(id) self.user = .success(user) } catch { self.user = .error(error) diff --git a/Twigs/Transaction/TransactionDetailsView.swift b/Twigs/Transaction/TransactionDetailsView.swift index a506a95..dab9f77 100644 --- a/Twigs/Transaction/TransactionDetailsView.swift +++ b/Twigs/Transaction/TransactionDetailsView.swift @@ -8,12 +8,11 @@ import SwiftUI import TwigsCore -import XCTest +import ArgumentParser struct TransactionDetailsView: View { @EnvironmentObject var apiService: TwigsApiService - @EnvironmentObject var authDataStore: AuthenticationDataStore - @EnvironmentObject var dataStore: TransactionDataStore + @EnvironmentObject var dataStore: DataStore @ObservedObject var transactionDetails: TransactionDetails var editing: Bool { if case .editing(_) = dataStore.transaction { @@ -28,6 +27,15 @@ struct TransactionDetailsView: View { init(_ transactionDetails: TransactionDetails) { self.transactionDetails = transactionDetails } + private var currentUserId: String? { + get { + if case let .success(currentUser) = self.dataStore.currentUser { + return currentUser.id + } else { + return nil + } + } + } var body: some View { if let transaction = self.dataStore.selectedTransaction { @@ -63,10 +71,8 @@ struct TransactionDetailsView: View { }) .sheet(isPresented: .constant(self.editing), onDismiss: nil) { TransactionFormSheet(transactionForm: TransactionForm( - budgetRepository: apiService, - categoryRepository: apiService, - transactionList: dataStore, - createdBy: authDataStore.currentUser!.id, + dataStore: dataStore, + createdBy: currentUserId!, budgetId: transaction.budgetId, categoryId: transaction.categoryId, transaction: transaction @@ -173,7 +179,7 @@ struct UserLineItem: View { #if DEBUG struct TransactionDetailsView_Previews: PreviewProvider { static var previews: some View { - TransactionDetailsView(TransactionDetails(budgetRepository: MockBudgetRepository(), categoryRepository: MockCategoryRepository(), userRepository: MockUserRepository())) + TransactionDetailsView(TransactionDetails(TwigsInMemoryCacheService())) } } #endif diff --git a/Twigs/Transaction/TransactionForm.swift b/Twigs/Transaction/TransactionForm.swift index 2ce0d1c..fd81b0f 100644 --- a/Twigs/Transaction/TransactionForm.swift +++ b/Twigs/Transaction/TransactionForm.swift @@ -10,9 +10,8 @@ import Foundation import TwigsCore class TransactionForm: ObservableObject { - let budgetRepository: BudgetRepository - let categoryRepository: CategoryRepository - let transactionList: TransactionDataStore + let apiService: TwigsApiService + let dataStore: DataStore let transaction: TwigsCore.Transaction? let createdBy: String let transactionId: String @@ -34,20 +33,17 @@ class TransactionForm: ObservableObject { let showDelete: Bool init( - budgetRepository: BudgetRepository, - categoryRepository: CategoryRepository, - transactionList: TransactionDataStore, + dataStore: DataStore, createdBy: String, budgetId: String, categoryId: String? = nil, transaction: TwigsCore.Transaction? = nil ) { - self.budgetRepository = budgetRepository - self.categoryRepository = categoryRepository + self.apiService = dataStore.apiService self.budgetId = budgetId self.categoryId = categoryId ?? "" self.createdBy = createdBy - self.transactionList = transactionList + self.dataStore = dataStore let baseTransaction = transaction ?? TwigsCore.Transaction(categoryId: categoryId, createdBy: createdBy, budgetId: budgetId) self.transaction = transaction self.transactionId = baseTransaction.id @@ -64,13 +60,13 @@ class TransactionForm: ObservableObject { self.categories = .loading var budgets: [Budget] do { - budgets = try await budgetRepository.getBudgets(count: nil, page: nil) + budgets = try await apiService.getBudgets(count: nil, page: nil) self.budgets = .success(budgets) } catch { self.budgets = .error(error) } do { - let categories = try await categoryRepository.getCategories(budgetId: nil, expense: nil, archived: false, count: nil, page: nil) + let categories = try await apiService.getCategories(budgetId: nil, expense: nil, archived: false, count: nil, page: nil) self.cachedCategories = categories updateCategories() } catch { @@ -80,7 +76,7 @@ class TransactionForm: ObservableObject { func save() async { let amount = Double(self.amount) ?? 0.0 - await transactionList.saveTransaction(Transaction( + await dataStore.saveTransaction(Transaction( id: transactionId, title: title, description: description, @@ -97,7 +93,7 @@ class TransactionForm: ObservableObject { guard let transaction = self.transaction else { return } - await transactionList.deleteTransaction(transaction) + await dataStore.deleteTransaction(transaction) } private func updateCategories() { diff --git a/Twigs/Transaction/TransactionFormSheet.swift b/Twigs/Transaction/TransactionFormSheet.swift index 7a59600..f25b36a 100644 --- a/Twigs/Transaction/TransactionFormSheet.swift +++ b/Twigs/Transaction/TransactionFormSheet.swift @@ -10,14 +10,14 @@ import SwiftUI import TwigsCore struct TransactionFormSheet: View { - @EnvironmentObject var transactionDataStore: TransactionDataStore + @EnvironmentObject var dataStore: DataStore @ObservedObject var transactionForm: TransactionForm @State private var showingAlert = false @ViewBuilder var body: some View { NavigationView { - switch self.transactionDataStore.transaction { + switch self.dataStore.transaction { case .loading: EmbeddedLoadingView() default: @@ -63,7 +63,7 @@ struct TransactionFormSheet: View { } .navigationTitle(transactionForm.transactionId.isEmpty ? "add_transaction" : "edit_transaction") .navigationBarItems( - leading: Button("cancel", action: { transactionForm.transactionList.cancelEdit() }), + leading: Button("cancel", action: { dataStore.cancelEditTransaction() }), trailing: Button("save", action: { Task { await transactionForm.save() diff --git a/Twigs/Transaction/TransactionListView.swift b/Twigs/Transaction/TransactionListView.swift index cfa0c68..252566a 100644 --- a/Twigs/Transaction/TransactionListView.swift +++ b/Twigs/Transaction/TransactionListView.swift @@ -12,16 +12,14 @@ import Collections import TwigsCore struct TransactionListView: View where Content: View { - @EnvironmentObject var authDataStore: AuthenticationDataStore - @StateObject var transactionDataStore: TransactionDataStore - let apiService: TwigsApiService + @EnvironmentObject var dataStore: DataStore @State var search: String = "" @ViewBuilder let header: (() -> Content)? var addingTransaction: Bool { - if case .editing(_) = self.transactionDataStore.transaction { + if case .editing(_) = self.dataStore.transaction { return true } - if case .saving(_) = self.transactionDataStore.transaction { + if case .saving(_) = self.dataStore.transaction { return true } return false @@ -55,11 +53,41 @@ struct TransactionListView: View where Content: View { } } + private var currentUserId: String? { + get { + if case let .success(currentUser) = dataStore.currentUser { + return currentUser.id + } else { + return nil + } + } + } + + private var budgetId: String? { + get { + if case let .success(budget) = dataStore.budget { + return budget.id + } else { + return nil + } + } + } + + private var categoryId: String? { + get { + if case let .success(category) = dataStore.category { + return category.id + } else { + return nil + } + } + } + @ViewBuilder var body: some View { InlineLoadingView( - data: $transactionDataStore.transactions, - action: { await transactionDataStore.getTransactions(self.budget.id, categoryId: self.category?.id) }, + data: $dataStore.transactions, + action: { await dataStore.getTransactions() }, errorTextLocalizedStringKey: "Failed to load transactions" ) { transactions in List { @@ -67,25 +95,23 @@ struct TransactionListView: View where Content: View { } .searchable(text: $search) .refreshable { - await transactionDataStore.getTransactions(self.budget.id, categoryId: self.category?.id, from: nil, count: nil, page: nil) + await dataStore.getTransactions() } .sheet( isPresented: .constant(addingTransaction), content: { TransactionFormSheet(transactionForm: TransactionForm( - budgetRepository: apiService, - categoryRepository: apiService, - transactionList: transactionDataStore, - createdBy: authDataStore.currentUser!.id, - budgetId: self.budget.id, - categoryId: self.category?.id, + dataStore: dataStore, + createdBy: currentUserId ?? "", + budgetId: budgetId ?? "", + categoryId: categoryId, transaction: nil )) - }) + }) .navigationBarItems( trailing: HStack { Button(action: { - transactionDataStore.editTransaction(TwigsCore.Transaction(createdBy: authDataStore.currentUser!.id, budgetId: budget.id)) + dataStore.newTransaction() }) { Image(systemName: "plus") .padding() @@ -95,20 +121,13 @@ struct TransactionListView: View where Content: View { } } - let budget: Budget - let category: TwigsCore.Category? - init(apiService: TwigsApiService, budget: Budget, category: TwigsCore.Category? = nil, header: (() -> Content)? = nil) { - self.apiService = apiService - self._transactionDataStore = StateObject(wrappedValue: TransactionDataStore(apiService)) - self.budget = budget - self.category = category + init(header: (() -> Content)? = nil) { self.header = header } } struct TransactionListItemView: View { - @EnvironmentObject var dataStore: TransactionDataStore - @EnvironmentObject var apiService: TwigsApiService + @EnvironmentObject var dataStore: DataStore var transaction: TwigsCore.Transaction var body: some View { @@ -116,11 +135,8 @@ struct TransactionListItemView: View { tag: self.transaction, selection: self.$dataStore.selectedTransaction, destination: { - TransactionDetailsView(TransactionDetails( - budgetRepository: apiService, - categoryRepository: apiService, - userRepository: apiService - )).navigationBarTitle("details", displayMode: .inline) + TransactionDetailsView(TransactionDetails(dataStore.apiService)) + .navigationBarTitle("details", displayMode: .inline) }, label: { HStack { diff --git a/Twigs/Transaction/TransactionRepository.swift b/Twigs/Transaction/TransactionRepository.swift index 8369313..01e0b20 100644 --- a/Twigs/Transaction/TransactionRepository.swift +++ b/Twigs/Transaction/TransactionRepository.swift @@ -20,7 +20,7 @@ class MockTransactionRepository: TransactionRepository { amount: 10000, categoryId: MockCategoryRepository.category.id, expense: true, - createdBy: MockUserRepository.user.id, + createdBy: MockUserRepository.currentUser.id, budgetId: MockBudgetRepository.budget.id ) diff --git a/Twigs/TwigsApp.swift b/Twigs/TwigsApp.swift index 8776cc0..2e8662a 100644 --- a/Twigs/TwigsApp.swift +++ b/Twigs/TwigsApp.swift @@ -12,20 +12,10 @@ import TwigsCore @main struct TwigsApp: App { @StateObject var apiService: TwigsInMemoryCacheService = TwigsInMemoryCacheService() - @AppStorage("BASE_URL") var baseUrl: String = "" - @AppStorage("TOKEN") var token: String = "" - @AppStorage("USER_ID") var userId: String = "" - + var body: some Scene { WindowGroup { - MainView(self.apiService, baseUrl: self.$baseUrl, token: self.$token, userId: self.$userId) - .environmentObject(apiService as TwigsApiService) - .onAppear { - if self.baseUrl != "", self.token != "" { - self.apiService.baseUrl = self.baseUrl - self.apiService.token = self.token - } - } + MainView(apiService as TwigsApiService) } } } diff --git a/Twigs/User/AuthenticationDataStore.swift b/Twigs/User/AuthenticationDataStore.swift deleted file mode 100644 index b6f8a42..0000000 --- a/Twigs/User/AuthenticationDataStore.swift +++ /dev/null @@ -1,104 +0,0 @@ -import Foundation -import Combine -import SwiftUI -import TwigsCore - -@MainActor -class AuthenticationDataStore: ObservableObject { - @Published var user: AsyncData = .empty { - didSet { - if case let .success(user) = self.user { - currentUser = user - } - } - } - @Published var currentUser: User? = nil { - didSet { - self.showLogin = self.currentUser == nil - } - } - @Binding private var baseUrl: String - @Binding private var token: String - @Binding private var userId: String - @Published var showLogin: Bool = true - let apiService: TwigsApiService - - init(_ apiService: TwigsApiService, baseUrl: Binding, token: Binding, userId: Binding) { - self.apiService = apiService - self._baseUrl = baseUrl - self._token = token - self._userId = userId - } - - func login(server: String, username: String, password: String) async { - self.user = .loading - self.apiService.baseUrl = server - // The API Service applies some validation and correcting of the server before returning it so we use that - // value instead of the original one - self.baseUrl = self.apiService.baseUrl ?? "" - do { - let response = try await self.apiService.login(username: username, password: password) - self.token = response.token - self.userId = response.userId - await self.loadProfile() - } catch { - switch error { - case NetworkError.jsonParsingFailed(let jsonError): - print(jsonError.localizedDescription) - default: - print(error.localizedDescription) - } - self.user = .error(error) - } - } - - func register(server: String, username: String, email: String, password: String, confirmPassword: String) async { - // TODO: Validate other fields as well - if !password.elementsEqual(confirmPassword) { - // TODO: Show error message to user - return - } - - self.apiService.baseUrl = server - // The API Service applies some validation and correcting of the server before returning it so we use that - // value instead of the original one - self.baseUrl = self.apiService.baseUrl ?? "" - do { - _ = try await apiService.register(username: username, email: email, password: password) - } catch { - switch error { - case NetworkError.jsonParsingFailed(let jsonError): - print(jsonError.localizedDescription) - default: - print(error.localizedDescription) - } - return - } - await self.login(server: server, username: username, password: password) - } - - func loadProfile() async { - if userId == "" { - self.user = .error(UserStatus.unauthenticated) - return - } - do { - let user = try await self.apiService.getUser(userId) - self.user = .success(user) - } catch { - self.user = .error(error) - } - } -} - -private let BASE_URL = "BASE_URL" -private let TOKEN = "TOKEN" -private let USER_ID = "USER_ID" - -enum UserStatus: Error, Equatable { - case unauthenticated - case authenticating - case failedAuthentication - case authenticated - case passwordMismatch -} diff --git a/Twigs/User/UserDataStore.swift b/Twigs/User/UserDataStore.swift deleted file mode 100644 index 9152f4e..0000000 --- a/Twigs/User/UserDataStore.swift +++ /dev/null @@ -1,30 +0,0 @@ -// -// UserDataStore.swift -// Twigs -// -// Created by Billy Brawner on 10/12/19. -// Copyright © 2019 William Brawner. All rights reserved. -// - -import Foundation -import Combine -import TwigsCore - -class UserDataStore: AsyncObservableObject { - @Published var user: AsyncData = .empty - - func getUser(_ id: String) async { - do { - let user = try await self.userRepository.getUser(id) - self.user = .success(user) - } catch { - self.user = .error(error) - } - } - - private let userRepository: UserRepository - - init(_ userRepository: UserRepository) { - self.userRepository = userRepository - } -} diff --git a/Twigs/User/UserRepository.swift b/Twigs/User/UserRepository.swift index 04998be..890c479 100644 --- a/Twigs/User/UserRepository.swift +++ b/Twigs/User/UserRepository.swift @@ -13,7 +13,7 @@ import TwigsCore #if DEBUG class MockUserRepository: UserRepository { static let loginResponse = LoginResponse(token: "token", expiration: "2020-01-01T12:00:00Z", userId: "0") - static let user = User(id: "0", username: "root", email: "root@localhost", avatar: nil) + static let currentUser = User(id: "0", username: "root", email: "root@localhost", avatar: nil) static var token: String? = nil func setToken(_ token: String) { @@ -21,11 +21,11 @@ class MockUserRepository: UserRepository { } func getUser(_ id: String) async throws -> User { - return MockUserRepository.user + return MockUserRepository.currentUser } func searchUsers(_ withUsername: String) async throws -> [User] { - return [MockUserRepository.user] + return [MockUserRepository.currentUser] } func setServer(_ server: String) { @@ -36,7 +36,7 @@ class MockUserRepository: UserRepository { } func register(username: String, email: String, password: String) async throws -> User { - return MockUserRepository.user + return MockUserRepository.currentUser } } diff --git a/Twigs/Views/MainView.swift b/Twigs/Views/MainView.swift index d04a5e2..a24e26a 100644 --- a/Twigs/Views/MainView.swift +++ b/Twigs/Views/MainView.swift @@ -10,22 +10,12 @@ import SwiftUI import TwigsCore struct MainView: View { - @StateObject var authenticationDataStore: AuthenticationDataStore - @StateObject var budgetDataStore: BudgetsDataStore - @StateObject var transactionList: TransactionDataStore - @StateObject var categoryList: CategoryListDataStore - @StateObject var userDataStore: UserDataStore - @StateObject var recurringTransactionList: RecurringTransactionDataStore + @StateObject var dataStore: DataStore let apiService: TwigsApiService - init(_ apiService: TwigsApiService, baseUrl: Binding, token: Binding, userId: Binding) { + init(_ apiService: TwigsApiService) { self.apiService = apiService - self._authenticationDataStore = StateObject(wrappedValue: AuthenticationDataStore(apiService, baseUrl: baseUrl, token: token, userId: userId)) - self._budgetDataStore = StateObject(wrappedValue: BudgetsDataStore(budgetRepository: apiService, categoryRepository: apiService, transactionRepository: apiService)) - self._categoryList = StateObject(wrappedValue: CategoryListDataStore(apiService)) - self._userDataStore = StateObject(wrappedValue: UserDataStore(apiService)) - self._transactionList = StateObject(wrappedValue: TransactionDataStore(apiService)) - self._recurringTransactionList = StateObject(wrappedValue: RecurringTransactionDataStore(apiService)) + self._dataStore = StateObject(wrappedValue: DataStore(apiService)) } @ViewBuilder @@ -39,15 +29,10 @@ struct MainView: View { var body: some View { mainView - .environmentObject(transactionList) - .environmentObject(categoryList) - .environmentObject(budgetDataStore) - .environmentObject(userDataStore) - .environmentObject(recurringTransactionList) - .environmentObject(authenticationDataStore) + .environmentObject(dataStore) .onAppear { Task { - await self.authenticationDataStore.loadProfile() + await self.dataStore.loadProfile() } } } @@ -55,6 +40,6 @@ struct MainView: View { struct MainView_Previews: PreviewProvider { static var previews: some View { - MainView(TwigsInMemoryCacheService(), baseUrl: .constant(""), token: .constant(""), userId: .constant("")) + MainView(TwigsInMemoryCacheService()) } }