From c6aa3f3936d92e22057d6aab748828745eacbdf5 Mon Sep 17 00:00:00 2001 From: PaulNguyen Date: Sun, 20 Nov 2022 21:01:12 +0700 Subject: [PATCH 1/2] restore user data --- GoMoney.xcodeproj/project.pbxproj | 4 + GoMoney/Models/Expense.swift | 12 +++ .../Scences/Auth/GMAuthViewController.swift | 72 ++++++++++++++ .../Auth/SignIn/SignInPasswordVC.swift | 15 +-- .../Auth/SignIn/SignInViewController.swift | 18 ++-- .../Auth/SignUp/SignUpPasswordVC.swift | 11 +-- .../Profile/ProfileViewController.swift | 13 ++- GoMoney/Service/AuthService.swift | 29 ++++++ GoMoney/Service/DataService.swift | 14 +++ GoMoney/Service/RemoteService.swift | 95 +++++++++++++++++-- GoMoney/Service/TagService.swift | 36 +++++-- GoMoney/Service/UserManager.swift | 6 +- 12 files changed, 278 insertions(+), 47 deletions(-) create mode 100644 GoMoney/Scences/Auth/GMAuthViewController.swift diff --git a/GoMoney.xcodeproj/project.pbxproj b/GoMoney.xcodeproj/project.pbxproj index 1e88f24..59a6823 100644 --- a/GoMoney.xcodeproj/project.pbxproj +++ b/GoMoney.xcodeproj/project.pbxproj @@ -39,6 +39,7 @@ 083EA336290659660079605F /* TransactionTag.swift in Sources */ = {isa = PBXBuildFile; fileRef = 083EA335290659660079605F /* TransactionTag.swift */; }; 083EA339290676EB0079605F /* StatViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 083EA338290676EB0079605F /* StatViewModel.swift */; }; 083EA33B2906933A0079605F /* StatLineChartCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 083EA33A2906933A0079605F /* StatLineChartCell.swift */; }; + 083EDBAE292A3ED50058F5D7 /* GMAuthViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 083EDBAD292A3ED40058F5D7 /* GMAuthViewController.swift */; }; 085F7526291215B80094A026 /* SettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 085F7525291215B80094A026 /* SettingsViewController.swift */; }; 085F752A2912490E0094A026 /* SettingsTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 085F75292912490E0094A026 /* SettingsTableViewCell.swift */; }; 085F752C29124CEC0094A026 /* BlockerToggle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 085F752B29124CEC0094A026 /* BlockerToggle.swift */; }; @@ -203,6 +204,7 @@ 083EA335290659660079605F /* TransactionTag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransactionTag.swift; sourceTree = ""; }; 083EA338290676EB0079605F /* StatViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatViewModel.swift; sourceTree = ""; }; 083EA33A2906933A0079605F /* StatLineChartCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatLineChartCell.swift; sourceTree = ""; }; + 083EDBAD292A3ED40058F5D7 /* GMAuthViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GMAuthViewController.swift; sourceTree = ""; }; 085F7525291215B80094A026 /* SettingsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewController.swift; sourceTree = ""; }; 085F75292912490E0094A026 /* SettingsTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsTableViewCell.swift; sourceTree = ""; }; 085F752B29124CEC0094A026 /* BlockerToggle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlockerToggle.swift; sourceTree = ""; }; @@ -703,6 +705,7 @@ 08DB56E528F8FC7D00170C60 /* View */, 08C8731728F865B200DC859D /* SignUp */, 08C872EC28F6C5DE00DC859D /* SignIn */, + 083EDBAD292A3ED40058F5D7 /* GMAuthViewController.swift */, ); path = Auth; sourceTree = ""; @@ -1128,6 +1131,7 @@ 083EA339290676EB0079605F /* StatViewModel.swift in Sources */, 082BF96828FD1D510094FE94 /* Podfile in Sources */, 088D691C29015865003D0660 /* AddExpenseField.swift in Sources */, + 083EDBAE292A3ED50058F5D7 /* GMAuthViewController.swift in Sources */, 08F0688D2921DBEE005C58EC /* ExchangeCell.swift in Sources */, 083B14DF2914EF300058E36E /* SyncManager.swift in Sources */, 08C6894D2913B01C002071CC /* TrackingService.swift in Sources */, diff --git a/GoMoney/Models/Expense.swift b/GoMoney/Models/Expense.swift index ec970b2..684711e 100644 --- a/GoMoney/Models/Expense.swift +++ b/GoMoney/Models/Expense.swift @@ -87,3 +87,15 @@ struct DateAmount { let date: Date var totalAmount: Double } + +/// Transaction structure on firestore. +struct RemoteTransaction: Codable { + var _id: String + var type: String + var tag: String + var amount: Double + var note: String + var occuredOn: Date + var createdAt: Date? + var updatedAt: Date? +} diff --git a/GoMoney/Scences/Auth/GMAuthViewController.swift b/GoMoney/Scences/Auth/GMAuthViewController.swift new file mode 100644 index 0000000..25828a7 --- /dev/null +++ b/GoMoney/Scences/Auth/GMAuthViewController.swift @@ -0,0 +1,72 @@ +import UIKit + +class GMAuthViewController: GMViewController { + func initData(completion: @escaping (String?) -> Void) { + GMLoadingView.shared.startLoadingAnimation(with: "Initing data ...") + TagService.shared.createDefaultDb { err in + GMLoadingView.shared.endLoadingAnimation() + if let err = err { + completion(err.localizedDescription) + } else { + completion(nil) + } + } + } + + func initDataAndGoHome() { + initData { [weak self] err in + if let err = err { + self?.errorAlert(message: err) + } else { + self?.navigateToMainVC() + } + } + } + + func navigateToMainVC() { + let homeVC = GMTabBarViewController() + if let delegate = view.window?.windowScene?.delegate as? SceneDelegate { + if let window = delegate.window { + window.rootViewController = homeVC + + let options: UIView.AnimationOptions = .transitionCrossDissolve + let duration: TimeInterval = 0.5 + UIView.transition( + with: window, + duration: duration, + options: options, + animations: {}, + completion: { _ in }) + } + } + } + + func restoreData(completion: @escaping (Error?) -> Void) { + GMLoadingView.shared.startLoadingAnimation(with: "Restoring data ...") + AuthService.shared.restoreUserData(completion: { err in + GMLoadingView.shared.endLoadingAnimation() + completion(err) + }) + } + + func restoreDataAndGoHome() { + restoreData { [weak self] err in + if let err = err { + self?.errorAlert(message: err.localizedDescription) + } else { + self?.navigateToMainVC() + } + } + } + + func checkIfNewUser(completion: @escaping (Bool) -> Void) { + RemoteService.shared.checkIfUserExist { result in + switch result { + case .success(let exist): + completion(!exist) + case .failure(let err): + self.errorAlert(message: err.localizedDescription) + } + } + } +} diff --git a/GoMoney/Scences/Auth/SignIn/SignInPasswordVC.swift b/GoMoney/Scences/Auth/SignIn/SignInPasswordVC.swift index 2a7b1f8..6079ee6 100644 --- a/GoMoney/Scences/Auth/SignIn/SignInPasswordVC.swift +++ b/GoMoney/Scences/Auth/SignIn/SignInPasswordVC.swift @@ -1,6 +1,6 @@ import UIKit -class SignInPasswordVC: GMViewController { +class SignInPasswordVC: GMAuthViewController { // MARK: - Content private enum Content { @@ -116,25 +116,18 @@ class SignInPasswordVC: GMViewController { errorLabel.text = error } else { errorLabel.text = "" - GMLoadingView.shared.startLoadingAnimation() + GMLoadingView.shared.startLoadingAnimation(with: "Logging in ...") viewModel.signInWithEmailAndPassword(email: email, password: password) { [weak self] error in - GMLoadingView.shared.endLoadingAnimation() if error != nil { + GMLoadingView.shared.endLoadingAnimation() self?.errorLabel.text = error?.localizedDescription } else { - self?.onSuccessLogin() + self?.restoreDataAndGoHome() } } } } } - - private func navigateToMainVC() { - let homeVC = GMTabBarViewController() - if let delegate = view.window?.windowScene?.delegate as? SceneDelegate { - delegate.window?.rootViewController = homeVC - } - } } extension SignInPasswordVC: UITextFieldDelegate { diff --git a/GoMoney/Scences/Auth/SignIn/SignInViewController.swift b/GoMoney/Scences/Auth/SignIn/SignInViewController.swift index 2146530..fcd66ae 100644 --- a/GoMoney/Scences/Auth/SignIn/SignInViewController.swift +++ b/GoMoney/Scences/Auth/SignIn/SignInViewController.swift @@ -7,7 +7,7 @@ import UIKit -class SignInViewController: GMViewController { +class SignInViewController: GMAuthViewController { private lazy var img: UIImageView = .build { view in view.image = UIImage(named: "onboard_1") } @@ -108,18 +108,18 @@ class SignInViewController: GMViewController { with: self, completion: { [weak self] err in if let err = err { + GMLoadingView.shared.endLoadingAnimation() self?.errorAlert(message: err) } else { - self?.navigateToMainVC() + self?.checkIfNewUser { isNew in + if isNew { + self?.initDataAndGoHome() + } else { + self?.restoreDataAndGoHome() + } + } } } ) } - - private func navigateToMainVC() { - let homeVC = GMTabBarViewController() - if let delegate = view.window?.windowScene?.delegate as? SceneDelegate { - delegate.window?.rootViewController = homeVC - } - } } diff --git a/GoMoney/Scences/Auth/SignUp/SignUpPasswordVC.swift b/GoMoney/Scences/Auth/SignUp/SignUpPasswordVC.swift index 385eff1..66a4da8 100644 --- a/GoMoney/Scences/Auth/SignUp/SignUpPasswordVC.swift +++ b/GoMoney/Scences/Auth/SignUp/SignUpPasswordVC.swift @@ -1,6 +1,6 @@ import UIKit -class SignUpPasswordVC: GMViewController { +class SignUpPasswordVC: GMAuthViewController { private enum Constant { static let padding: CGFloat = 16 } @@ -121,7 +121,7 @@ class SignUpPasswordVC: GMViewController { self?.errorLabel.text = error?.localizedDescription } else { // TODO: navigateToDetailVC - self?.navigateToMainVC() + self?.initDataAndGoHome() } } } @@ -133,13 +133,6 @@ class SignUpPasswordVC: GMViewController { errorLabel.text = "" } - private func navigateToMainVC() { - let homeVC = GMTabBarViewController() - if let delegate = view.window?.windowScene?.delegate as? SceneDelegate { - delegate.window?.rootViewController = homeVC - } - } - private func navigateToDetailVC() { let vc = SignUpDetailViewController() navigationController?.pushViewController(vc, animated: true) diff --git a/GoMoney/Scences/Profile/ProfileViewController.swift b/GoMoney/Scences/Profile/ProfileViewController.swift index c9654fc..adcae68 100644 --- a/GoMoney/Scences/Profile/ProfileViewController.swift +++ b/GoMoney/Scences/Profile/ProfileViewController.swift @@ -169,7 +169,18 @@ class ProfileViewController: GMMainViewController { let navVC = UINavigationController(rootViewController: signInVC) if let delegate = view.window?.windowScene?.delegate as? SceneDelegate { - delegate.window?.rootViewController = navVC + if let window = delegate.window { + window.rootViewController = navVC + + let options: UIView.AnimationOptions = .transitionCrossDissolve + let duration: TimeInterval = 0.5 + UIView.transition( + with: window, + duration: duration, + options: options, + animations: {}, + completion: { _ in }) + } } } diff --git a/GoMoney/Service/AuthService.swift b/GoMoney/Service/AuthService.swift index 6e2b2b4..68e5bf2 100644 --- a/GoMoney/Service/AuthService.swift +++ b/GoMoney/Service/AuthService.swift @@ -120,4 +120,33 @@ class AuthService { // remove realm DataService.shared.dropAllTable() } + + func restoreUserData(completion: @escaping (Error?) -> Void) { + RemoteService.shared.getAllTags { result in + switch result { + case .failure(let err): + completion(err) + case .success(let tags): + TagService.shared.setTags(tags: tags) { err in + if let err = err { + completion(err) + } + else { + print("[restore] \(tags.count) tags") + RemoteService.shared.getAllTransactions { result in + switch result { + case .success(let transactions): + print("[restore] \(transactions.count) transactions") + DataService.shared.addTransactions(transactions) { err in + completion(err) + } + case .failure(let err): + completion(err) + } + } + } + } + } + } + } } diff --git a/GoMoney/Service/DataService.swift b/GoMoney/Service/DataService.swift index 2a80ec3..37beb0d 100644 --- a/GoMoney/Service/DataService.swift +++ b/GoMoney/Service/DataService.swift @@ -13,6 +13,7 @@ enum DataError: Error { case transactionNotFound(_ transaction: Expense) case noTransactions case userNotFound + case tagNotFound var localizedDescription: String { switch self { @@ -22,6 +23,8 @@ enum DataError: Error { return "There is no transactions!" case .userNotFound: return "User not found!" + case .tagNotFound: + return "Tag not found!" } } } @@ -111,6 +114,17 @@ class DataService { } } + func addTransactions(_ transactions: [Expense], completion: (Error?) -> Void) { + do { + try realm.write { + realm.add(transactions) + } + completion(nil) + } catch { + completion(error) + } + } + func deleteExpense(expense: Expense, completion: ((Error?) -> Void)? = nil) { do { try realm.write { diff --git a/GoMoney/Service/RemoteService.swift b/GoMoney/Service/RemoteService.swift index cf9e7bf..5bbb6db 100644 --- a/GoMoney/Service/RemoteService.swift +++ b/GoMoney/Service/RemoteService.swift @@ -3,7 +3,6 @@ import FirebaseCore import FirebaseFirestore import FirebaseFirestoreSwift import RealmSwift -import UIKit typealias RemoteCompletion = (Error?) -> Void @@ -33,13 +32,11 @@ class RemoteService { completion(.failure(err)) } else { var list = [Expense]() - snapshot?.documents.forEach { - let id = $0.documentID - let data = try? $0.data(as: Expense.self) - if let data = data { - data._id = try! ObjectId(string: String(id)) - list.append(data) + if let data = try? $0.data(as: RemoteTransaction.self) { + if let transaction = self.remoteTransactionToRealmTransaction(remote: data) { + list.append(transaction) + } } } completion(.success(list)) @@ -90,6 +87,43 @@ class RemoteService { } } + private func remoteTransactionToRealmTransaction(remote: RemoteTransaction) -> Expense? { + guard + let type = ExpenseType(rawValue: remote.type) + else { + return nil + } + + if let tag = TagService.shared.getTagById(remote.tag) { + return Expense(type: type, tag: tag, amount: remote.amount, note: remote.note, occuredOn: remote.occuredOn, createdAt: remote.createdAt, updatedAt: remote.updatedAt) + } + return nil + } +} + +// remote tag table +extension RemoteService { + /// Get all remote tag + func getAllTags(completion: @escaping ((Result<[TransactionTag], Error>) -> Void)) { + guard let userId = userId else { + completion(.failure(DataError.userNotFound)) + return + } + + let doc = self.db + .collection("tags") + .document(userId) + + doc.getDocument { snapshot, err in + if let err = err { + completion(.failure(err)) + } else { + let list = try? snapshot?.data(as: [String: [TransactionTag]].self) + completion(.success(list?["tags"] ?? TransactionTag.defaults)) + } + } + } + /// set tags on firebase: func setTags(tags: [TransactionTag], completion: @escaping RemoteCompletion) { guard let userId = userId else { @@ -120,3 +154,50 @@ class RemoteService { } } } + +// remote user table +extension RemoteService { + func getUserData(completion: @escaping ((Result) -> Void)) { + guard let userId = userId else { + completion(.failure(DataError.userNotFound)) + return + } + + self.db.collection("info") + .document(userId) + .getDocument(as: GMUser.self) { result in + completion(result) + switch result { + case .success(let user): + print("City: \(user)") + case .failure(let error): + print("Error decoding city: \(error)") + } + } + } + + func checkIfUserExist(completion: @escaping (Result) -> Void) { + guard let userId = userId else { + completion(.failure(DataError.userNotFound)) + return + } + let ref = self.db.collection("tags") + .document(userId) + + ref.getDocument { snapShot, err in + if let err = err { + completion(.failure(err)) + } else { + if let snapShot = snapShot { + completion(.success(snapShot.exists)) + } else { + print("Unknown error") + } + } + } + } + + func setUserData() {} + + func restoreUserData() {} +} diff --git a/GoMoney/Service/TagService.swift b/GoMoney/Service/TagService.swift index 5668957..67cf3a7 100644 --- a/GoMoney/Service/TagService.swift +++ b/GoMoney/Service/TagService.swift @@ -13,7 +13,7 @@ class TagService { private let realm: Realm = try! Realm() - func getAllTags(completion: (()->Void)? = nil) { + func getAllTags(completion: (() -> Void)? = nil) { let tags = realm.objects(TransactionTag.self) all = Array(tags) @@ -34,25 +34,43 @@ class TagService { completion?() } - func getTagById(_ id: String, completion: @escaping (TransactionTag?)->Void) { - let tag = realm.objects(TransactionTag.self) - .first(where: { $0._id.stringValue == id }) - completion(tag) + func setTags(tags: [TransactionTag], completion: @escaping (Error?) -> Void) { + do { + try realm.write { + realm.add(tags) + } + getAllTags() + completion(nil) + } catch { + completion(error) + } + } + + func getTagById(_ id: String) -> TransactionTag? { + guard let objectId = try? ObjectId(string: id) else { + return nil + } + return realm.object( + ofType: TransactionTag.self, + forPrimaryKey: objectId + ) } /// Create default database on first launch - func createDefaultDb(completion: @escaping (Error?)->Void) { + func createDefaultDb(completion: @escaping (Error?) -> Void) { do { try realm.write { realm.add(TransactionTag.defaults) } + requireSync() + getAllTags() completion(nil) } catch { completion(error) } } - func remove(tag: TransactionTag, completion: @escaping (Error?)->Void) { + func remove(tag: TransactionTag, completion: @escaping (Error?) -> Void) { do { try realm.write { realm.delete(tag) @@ -65,13 +83,13 @@ class TagService { } } - func tagExist(_ name: String?)-> Bool { + func tagExist(_ name: String?) -> Bool { all.first( where: { $0.name == name } ) != nil } - func add(tag: TransactionTag, completion: @escaping (String?)->Void) { + func add(tag: TransactionTag, completion: @escaping (String?) -> Void) { if tagExist(tag.name) { completion("\(tag.name) existed!") return diff --git a/GoMoney/Service/UserManager.swift b/GoMoney/Service/UserManager.swift index 32dcc3b..1dccd98 100644 --- a/GoMoney/Service/UserManager.swift +++ b/GoMoney/Service/UserManager.swift @@ -1,6 +1,6 @@ import FirebaseAuth -struct GMUser { +struct GMUser: Codable { let uid: String var email: String? var name: String? @@ -38,4 +38,8 @@ class UserManager { } return GMUser(uid: id, email: email, name: name, photoUrl: photo) } + + func getUserId() -> String? { + return UserDefaults.standard.string(forKey: "userId") + } } From dc5eb1534cc43f3ae758db8cf5afec5cbdaca0ca Mon Sep 17 00:00:00 2001 From: PaulNguyen Date: Sun, 20 Nov 2022 21:35:04 +0700 Subject: [PATCH 2/2] fix mistake old userId --- GoMoney/Service/RemoteService.swift | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/GoMoney/Service/RemoteService.swift b/GoMoney/Service/RemoteService.swift index 5bbb6db..061ff80 100644 --- a/GoMoney/Service/RemoteService.swift +++ b/GoMoney/Service/RemoteService.swift @@ -15,15 +15,17 @@ class RemoteService { private let db = Firestore.firestore() - let userId = Auth.auth().currentUser?.uid + let userId = { Auth.auth().currentUser?.uid } /// Get all remote transaction on first login. func getAllTransactions(completion: @escaping ((Result<[Expense], Error>) -> Void)) { - guard let userId = userId else { + guard let userId = userId() else { completion(.failure(DataError.userNotFound)) return } + print("userId=\(userId)") + self.db.collection("transactions") .document(userId) .collection("transactions") @@ -47,7 +49,7 @@ class RemoteService { /// set transaction to firebase. /// query transaction in main-table, use id in temp-table func setTransaction(by id: String, completion: @escaping RemoteCompletion) { - guard let userId = userId else { + guard let userId = userId() else { completion(DataError.userNotFound) return } @@ -73,7 +75,7 @@ class RemoteService { /// remove transaction to firebase. func deleteTransation(by id: String, completion: @escaping RemoteCompletion) { - guard let userId = userId else { + guard let userId = userId() else { completion(DataError.userNotFound) return } @@ -105,11 +107,12 @@ class RemoteService { extension RemoteService { /// Get all remote tag func getAllTags(completion: @escaping ((Result<[TransactionTag], Error>) -> Void)) { - guard let userId = userId else { + guard let userId = userId() else { completion(.failure(DataError.userNotFound)) return } + print("userId=\(userId)") let doc = self.db .collection("tags") .document(userId) @@ -126,7 +129,7 @@ extension RemoteService { /// set tags on firebase: func setTags(tags: [TransactionTag], completion: @escaping RemoteCompletion) { - guard let userId = userId else { + guard let userId = userId() else { completion(DataError.userNotFound) return } @@ -158,7 +161,7 @@ extension RemoteService { // remote user table extension RemoteService { func getUserData(completion: @escaping ((Result) -> Void)) { - guard let userId = userId else { + guard let userId = userId() else { completion(.failure(DataError.userNotFound)) return } @@ -177,7 +180,7 @@ extension RemoteService { } func checkIfUserExist(completion: @escaping (Result) -> Void) { - guard let userId = userId else { + guard let userId = userId() else { completion(.failure(DataError.userNotFound)) return }