Implement basic profile editing
This commit is contained in:
parent
a61cc567fe
commit
d72be0abd9
10 changed files with 367 additions and 29 deletions
|
@ -34,6 +34,7 @@
|
||||||
28FE6B0623444A9800D5543E /* TransactionDetailsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28FE6B0523444A9800D5543E /* TransactionDetailsView.swift */; };
|
28FE6B0623444A9800D5543E /* TransactionDetailsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28FE6B0523444A9800D5543E /* TransactionDetailsView.swift */; };
|
||||||
8005FD5D277EAB0200E48B23 /* MainView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8005FD5C277EAB0200E48B23 /* MainView.swift */; };
|
8005FD5D277EAB0200E48B23 /* MainView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8005FD5C277EAB0200E48B23 /* MainView.swift */; };
|
||||||
800DFC2C277FF47A00EDCE9B /* AsyncData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 800DFC2B277FF47A00EDCE9B /* AsyncData.swift */; };
|
800DFC2C277FF47A00EDCE9B /* AsyncData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 800DFC2B277FF47A00EDCE9B /* AsyncData.swift */; };
|
||||||
|
80166EB5284BE98300B3AE06 /* EditUsernameView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80166EB4284BE98300B3AE06 /* EditUsernameView.swift */; };
|
||||||
801D08CC275ECEFA00931465 /* RecurringTransactionsListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 801D08CB275ECEFA00931465 /* RecurringTransactionsListView.swift */; };
|
801D08CC275ECEFA00931465 /* RecurringTransactionsListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 801D08CB275ECEFA00931465 /* RecurringTransactionsListView.swift */; };
|
||||||
801D08CE275F189E00931465 /* RecurringTransactionsRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 801D08CD275F189E00931465 /* RecurringTransactionsRepository.swift */; };
|
801D08CE275F189E00931465 /* RecurringTransactionsRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 801D08CD275F189E00931465 /* RecurringTransactionsRepository.swift */; };
|
||||||
801D08D2275FB7DE00931465 /* RecurringTransactionDetailsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 801D08D1275FB7DE00931465 /* RecurringTransactionDetailsView.swift */; };
|
801D08D2275FB7DE00931465 /* RecurringTransactionDetailsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 801D08D1275FB7DE00931465 /* RecurringTransactionDetailsView.swift */; };
|
||||||
|
@ -55,6 +56,8 @@
|
||||||
809B942327221EC800B1DAE2 /* CategoryFormSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 809B942227221EC800B1DAE2 /* CategoryFormSheet.swift */; };
|
809B942327221EC800B1DAE2 /* CategoryFormSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 809B942227221EC800B1DAE2 /* CategoryFormSheet.swift */; };
|
||||||
80A419ED2787C0A00090C515 /* TwigsCli.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80A419EC2787C0A00090C515 /* TwigsCli.swift */; };
|
80A419ED2787C0A00090C515 /* TwigsCli.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80A419EC2787C0A00090C515 /* TwigsCli.swift */; };
|
||||||
80A419F52787C1520090C515 /* ArgumentParser in Frameworks */ = {isa = PBXBuildFile; productRef = 80A419F42787C1520090C515 /* ArgumentParser */; };
|
80A419F52787C1520090C515 /* ArgumentParser in Frameworks */ = {isa = PBXBuildFile; productRef = 80A419F42787C1520090C515 /* ArgumentParser */; };
|
||||||
|
80AC75CD284E8E100099E846 /* EditPasswordView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80AC75CC284E8E100099E846 /* EditPasswordView.swift */; };
|
||||||
|
80AC75CF284E8E1B0099E846 /* EditEmailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80AC75CE284E8E1B0099E846 /* EditEmailView.swift */; };
|
||||||
80AF7A982835ED3B009565C6 /* RecurringTransactionFormView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80AF7A972835ED3B009565C6 /* RecurringTransactionFormView.swift */; };
|
80AF7A982835ED3B009565C6 /* RecurringTransactionFormView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80AF7A972835ED3B009565C6 /* RecurringTransactionFormView.swift */; };
|
||||||
80D1FC14277C1EF9007F17FB /* InlineLoadingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80D1FC13277C1EF9007F17FB /* InlineLoadingView.swift */; };
|
80D1FC14277C1EF9007F17FB /* InlineLoadingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80D1FC13277C1EF9007F17FB /* InlineLoadingView.swift */; };
|
||||||
80D2CE1A2833448500EDD6C2 /* DataStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80D2CE192833448500EDD6C2 /* DataStore.swift */; };
|
80D2CE1A2833448500EDD6C2 /* DataStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80D2CE192833448500EDD6C2 /* DataStore.swift */; };
|
||||||
|
@ -127,6 +130,7 @@
|
||||||
28FE6B0523444A9800D5543E /* TransactionDetailsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransactionDetailsView.swift; sourceTree = "<group>"; };
|
28FE6B0523444A9800D5543E /* TransactionDetailsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransactionDetailsView.swift; sourceTree = "<group>"; };
|
||||||
8005FD5C277EAB0200E48B23 /* MainView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainView.swift; sourceTree = "<group>"; };
|
8005FD5C277EAB0200E48B23 /* MainView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainView.swift; sourceTree = "<group>"; };
|
||||||
800DFC2B277FF47A00EDCE9B /* AsyncData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AsyncData.swift; sourceTree = "<group>"; };
|
800DFC2B277FF47A00EDCE9B /* AsyncData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AsyncData.swift; sourceTree = "<group>"; };
|
||||||
|
80166EB4284BE98300B3AE06 /* EditUsernameView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditUsernameView.swift; sourceTree = "<group>"; };
|
||||||
801D08CB275ECEFA00931465 /* RecurringTransactionsListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecurringTransactionsListView.swift; sourceTree = "<group>"; };
|
801D08CB275ECEFA00931465 /* RecurringTransactionsListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecurringTransactionsListView.swift; sourceTree = "<group>"; };
|
||||||
801D08CD275F189E00931465 /* RecurringTransactionsRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecurringTransactionsRepository.swift; sourceTree = "<group>"; };
|
801D08CD275F189E00931465 /* RecurringTransactionsRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecurringTransactionsRepository.swift; sourceTree = "<group>"; };
|
||||||
801D08D1275FB7DE00931465 /* RecurringTransactionDetailsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecurringTransactionDetailsView.swift; sourceTree = "<group>"; };
|
801D08D1275FB7DE00931465 /* RecurringTransactionDetailsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecurringTransactionDetailsView.swift; sourceTree = "<group>"; };
|
||||||
|
@ -147,6 +151,8 @@
|
||||||
809B94242722597800B1DAE2 /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/Localizable.strings; sourceTree = "<group>"; };
|
809B94242722597800B1DAE2 /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||||
80A419EA2787C0A00090C515 /* twigs-cli */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = "twigs-cli"; sourceTree = BUILT_PRODUCTS_DIR; };
|
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 = "<group>"; };
|
80A419EC2787C0A00090C515 /* TwigsCli.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TwigsCli.swift; sourceTree = "<group>"; };
|
||||||
|
80AC75CC284E8E100099E846 /* EditPasswordView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditPasswordView.swift; sourceTree = "<group>"; };
|
||||||
|
80AC75CE284E8E1B0099E846 /* EditEmailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditEmailView.swift; sourceTree = "<group>"; };
|
||||||
80AF7A972835ED3B009565C6 /* RecurringTransactionFormView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecurringTransactionFormView.swift; sourceTree = "<group>"; };
|
80AF7A972835ED3B009565C6 /* RecurringTransactionFormView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecurringTransactionFormView.swift; sourceTree = "<group>"; };
|
||||||
80D1FC13277C1EF9007F17FB /* InlineLoadingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InlineLoadingView.swift; sourceTree = "<group>"; };
|
80D1FC13277C1EF9007F17FB /* InlineLoadingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InlineLoadingView.swift; sourceTree = "<group>"; };
|
||||||
80D2CE192833448500EDD6C2 /* DataStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataStore.swift; sourceTree = "<group>"; };
|
80D2CE192833448500EDD6C2 /* DataStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataStore.swift; sourceTree = "<group>"; };
|
||||||
|
@ -196,6 +202,9 @@
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
282126A0235929B800072D52 /* ProfileView.swift */,
|
282126A0235929B800072D52 /* ProfileView.swift */,
|
||||||
|
80166EB4284BE98300B3AE06 /* EditUsernameView.swift */,
|
||||||
|
80AC75CC284E8E100099E846 /* EditPasswordView.swift */,
|
||||||
|
80AC75CE284E8E1B0099E846 /* EditEmailView.swift */,
|
||||||
);
|
);
|
||||||
path = Profile;
|
path = Profile;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -562,11 +571,13 @@
|
||||||
2841022723419A2B00EAFA29 /* TabbedBudgetView.swift in Sources */,
|
2841022723419A2B00EAFA29 /* TabbedBudgetView.swift in Sources */,
|
||||||
801D08D2275FB7DE00931465 /* RecurringTransactionDetailsView.swift in Sources */,
|
801D08D2275FB7DE00931465 /* RecurringTransactionDetailsView.swift in Sources */,
|
||||||
28FE6B0623444A9800D5543E /* TransactionDetailsView.swift in Sources */,
|
28FE6B0623444A9800D5543E /* TransactionDetailsView.swift in Sources */,
|
||||||
|
80AC75CF284E8E1B0099E846 /* EditEmailView.swift in Sources */,
|
||||||
80D1FC14277C1EF9007F17FB /* InlineLoadingView.swift in Sources */,
|
80D1FC14277C1EF9007F17FB /* InlineLoadingView.swift in Sources */,
|
||||||
28AC952C233C434800BFB70A /* UserRepository.swift in Sources */,
|
28AC952C233C434800BFB70A /* UserRepository.swift in Sources */,
|
||||||
28FE6B04234449DC00D5543E /* TransactionListView.swift in Sources */,
|
28FE6B04234449DC00D5543E /* TransactionListView.swift in Sources */,
|
||||||
28AC94F2233C373900BFB70A /* LoginView.swift in Sources */,
|
28AC94F2233C373900BFB70A /* LoginView.swift in Sources */,
|
||||||
80FC1BC2284146CD00682F21 /* MonthlyFrequencyPicker.swift in Sources */,
|
80FC1BC2284146CD00682F21 /* MonthlyFrequencyPicker.swift in Sources */,
|
||||||
|
80AC75CD284E8E100099E846 /* EditPasswordView.swift in Sources */,
|
||||||
802161D0277647920075761A /* AsyncObservableObject.swift in Sources */,
|
802161D0277647920075761A /* AsyncObservableObject.swift in Sources */,
|
||||||
282126BB235CDD3C00072D52 /* BudgetDetailsView.swift in Sources */,
|
282126BB235CDD3C00072D52 /* BudgetDetailsView.swift in Sources */,
|
||||||
80D2CE1A2833448500EDD6C2 /* DataStore.swift in Sources */,
|
80D2CE1A2833448500EDD6C2 /* DataStore.swift in Sources */,
|
||||||
|
@ -589,6 +600,7 @@
|
||||||
807FEAB52837F71200D05338 /* RecurringTransactionForm.swift in Sources */,
|
807FEAB52837F71200D05338 /* RecurringTransactionForm.swift in Sources */,
|
||||||
80820145275FFD380040996E /* SidebarBudgetView.swift in Sources */,
|
80820145275FFD380040996E /* SidebarBudgetView.swift in Sources */,
|
||||||
8044BA3927828E9D009A78D4 /* CategoryDataStore.swift in Sources */,
|
8044BA3927828E9D009A78D4 /* CategoryDataStore.swift in Sources */,
|
||||||
|
80166EB5284BE98300B3AE06 /* EditUsernameView.swift in Sources */,
|
||||||
284102302342D97300EAFA29 /* BudgetListsView.swift in Sources */,
|
284102302342D97300EAFA29 /* BudgetListsView.swift in Sources */,
|
||||||
282126BD235CDE1400072D52 /* ProgressView.swift in Sources */,
|
282126BD235CDE1400072D52 /* ProgressView.swift in Sources */,
|
||||||
806C7850272B700B00FA1375 /* TwigsApp.swift in Sources */,
|
806C7850272B700B00FA1375 /* TwigsApp.swift in Sources */,
|
||||||
|
|
|
@ -476,10 +476,11 @@ class DataStore : ObservableObject {
|
||||||
|
|
||||||
@Published var currentUser: AsyncData<User> = .empty {
|
@Published var currentUser: AsyncData<User> = .empty {
|
||||||
didSet {
|
didSet {
|
||||||
if case .success(_) = self.currentUser {
|
switch currentUser {
|
||||||
self.showLogin = false
|
case .empty, .loading:
|
||||||
} else {
|
|
||||||
self.showLogin = true
|
self.showLogin = true
|
||||||
|
default:
|
||||||
|
self.showLogin = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -579,6 +580,72 @@ class DataStore : ObservableObject {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func updateUsername(_ username: String) async -> UsernameError? {
|
||||||
|
guard case let .success(current) = self.currentUser else {
|
||||||
|
return .unknown
|
||||||
|
}
|
||||||
|
self.currentUser = .saving(current)
|
||||||
|
do {
|
||||||
|
let updated = try await self.apiService.updateUser(current.copy(username: username))
|
||||||
|
self.currentUser = .success(updated)
|
||||||
|
return nil
|
||||||
|
} catch {
|
||||||
|
self.currentUser = .error(error, current)
|
||||||
|
return .unavailable
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateEmail(_ email: String) async -> EmailError? {
|
||||||
|
guard case let .success(current) = self.currentUser else {
|
||||||
|
return .unknown
|
||||||
|
}
|
||||||
|
if !email.isEmpty && (!email.contains("@") || !email.contains(".")) {
|
||||||
|
return .invalid
|
||||||
|
}
|
||||||
|
self.currentUser = .saving(current)
|
||||||
|
do {
|
||||||
|
let updated = try await self.apiService.updateUser(current.copy(email: email))
|
||||||
|
self.currentUser = .success(updated)
|
||||||
|
return nil
|
||||||
|
} catch {
|
||||||
|
self.currentUser = .error(error, current)
|
||||||
|
return .unavailable
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func updatePassword(_ password: String, confirmPassword: String) async -> PasswordError? {
|
||||||
|
guard case let .success(current) = self.currentUser else {
|
||||||
|
return .unknown
|
||||||
|
}
|
||||||
|
if password != confirmPassword {
|
||||||
|
return .notMatching
|
||||||
|
}
|
||||||
|
self.currentUser = .saving(current)
|
||||||
|
do {
|
||||||
|
let updated = try await self.apiService.updateUser(current.copy(password: password))
|
||||||
|
self.currentUser = .success(updated)
|
||||||
|
return nil
|
||||||
|
} catch {
|
||||||
|
self.currentUser = .error(error, current)
|
||||||
|
return .unknown
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func saveUser(_ user: User) async -> Bool {
|
||||||
|
guard case let .success(current) = self.currentUser else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
self.currentUser = .saving(current)
|
||||||
|
do {
|
||||||
|
let updated = try await self.apiService.updateUser(user)
|
||||||
|
self.currentUser = .success(updated)
|
||||||
|
return true
|
||||||
|
} catch {
|
||||||
|
self.currentUser = .error(error, current)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Published var user: AsyncData<User> = .empty
|
@Published var user: AsyncData<User> = .empty
|
||||||
|
|
||||||
func getUser(_ id: String) async {
|
func getUser(_ id: String) async {
|
||||||
|
@ -591,6 +658,24 @@ class DataStore : ObservableObject {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum UsernameError: String, Error {
|
||||||
|
case empty = "cannot_be_empty"
|
||||||
|
case unavailable = "username_taken"
|
||||||
|
case unknown = "unknown_error"
|
||||||
|
}
|
||||||
|
|
||||||
|
enum EmailError: String, Error {
|
||||||
|
case invalid = "email_invalid"
|
||||||
|
case unavailable = "email_taken"
|
||||||
|
case unknown = "unknown_error"
|
||||||
|
}
|
||||||
|
|
||||||
|
enum PasswordError: String, Error {
|
||||||
|
case empty = "cannot_be_empty"
|
||||||
|
case notMatching = "passwords_dont_match"
|
||||||
|
case unknown = "unknown_error"
|
||||||
|
}
|
||||||
|
|
||||||
enum UserStatus: Error, Equatable {
|
enum UserStatus: Error, Equatable {
|
||||||
case unauthenticated
|
case unauthenticated
|
||||||
case authenticating
|
case authenticating
|
||||||
|
|
53
Twigs/Profile/EditEmailView.swift
Normal file
53
Twigs/Profile/EditEmailView.swift
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
//
|
||||||
|
// EditEmailView.swift
|
||||||
|
// Twigs
|
||||||
|
//
|
||||||
|
// Created by William Brawner on 6/6/22.
|
||||||
|
// Copyright © 2022 William Brawner. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct EditEmailView: View {
|
||||||
|
@EnvironmentObject var dataStore: DataStore
|
||||||
|
@Environment(\.dismiss) var dismiss
|
||||||
|
@State var email: String
|
||||||
|
@State var error: EmailError? = nil
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
var body: some View {
|
||||||
|
if case .saving(_) = dataStore.currentUser {
|
||||||
|
ActivityIndicator(isAnimating: .constant(true), style: .large)
|
||||||
|
} else {
|
||||||
|
VStack(alignment: .leading, spacing: 4.0) {
|
||||||
|
Form {
|
||||||
|
Section(content: {
|
||||||
|
TextField("prompt_email", text: $email)
|
||||||
|
.textContentType(.emailAddress)
|
||||||
|
.textInputAutocapitalization(.never)
|
||||||
|
}, footer: {
|
||||||
|
if let error = self.error {
|
||||||
|
Text(LocalizedStringKey(error.rawValue))
|
||||||
|
.foregroundColor(.red)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
Button("save", action: {
|
||||||
|
Task {
|
||||||
|
self.error = await dataStore.updateEmail(email)
|
||||||
|
if self.error == nil {
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct EditEmailView_Previews: PreviewProvider {
|
||||||
|
static var previews: some View {
|
||||||
|
EditEmailView(email: "me@example.com")
|
||||||
|
.environmentObject(DataStore(TwigsInMemoryCacheService()))
|
||||||
|
}
|
||||||
|
}
|
55
Twigs/Profile/EditPasswordView.swift
Normal file
55
Twigs/Profile/EditPasswordView.swift
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
//
|
||||||
|
// EditPasswordView.swift
|
||||||
|
// Twigs
|
||||||
|
//
|
||||||
|
// Created by William Brawner on 6/6/22.
|
||||||
|
// Copyright © 2022 William Brawner. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct EditPasswordView: View {
|
||||||
|
@Environment(\.dismiss) var dismiss
|
||||||
|
@EnvironmentObject var dataStore: DataStore
|
||||||
|
@State var password: String = ""
|
||||||
|
@State var confirmPassword: String = ""
|
||||||
|
@State var error: PasswordError? = nil
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
var body: some View {
|
||||||
|
if case .saving(_) = dataStore.currentUser {
|
||||||
|
ActivityIndicator(isAnimating: .constant(true), style: .large)
|
||||||
|
} else {
|
||||||
|
VStack(alignment: .leading, spacing: 4.0) {
|
||||||
|
if case .notMatching = self.error {
|
||||||
|
Text("passwords_must_match")
|
||||||
|
.foregroundColor(.red)
|
||||||
|
.multilineTextAlignment(.center)
|
||||||
|
}
|
||||||
|
Form {
|
||||||
|
Section {
|
||||||
|
SecureField("prompt_password", text: $password)
|
||||||
|
.textContentType(.newPassword)
|
||||||
|
SecureField("prompt_confirm_password", text: $confirmPassword)
|
||||||
|
.textContentType(.newPassword)
|
||||||
|
}
|
||||||
|
Button("save", action: {
|
||||||
|
Task {
|
||||||
|
self.error = await dataStore.updatePassword(password, confirmPassword: confirmPassword)
|
||||||
|
if self.error == nil {
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct EditPasswordView_Previews: PreviewProvider {
|
||||||
|
static var previews: some View {
|
||||||
|
EditPasswordView()
|
||||||
|
.environmentObject(DataStore(TwigsInMemoryCacheService()))
|
||||||
|
}
|
||||||
|
}
|
53
Twigs/Profile/EditUsernameView.swift
Normal file
53
Twigs/Profile/EditUsernameView.swift
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
//
|
||||||
|
// EditUsernameView.swift
|
||||||
|
// Twigs
|
||||||
|
//
|
||||||
|
// Created by William Brawner on 6/4/22.
|
||||||
|
// Copyright © 2022 William Brawner. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
import TwigsCore
|
||||||
|
|
||||||
|
struct EditUsernameView: View {
|
||||||
|
@EnvironmentObject var dataStore: DataStore
|
||||||
|
@Environment(\.dismiss) var dismiss
|
||||||
|
@State var username: String
|
||||||
|
@State var error: UsernameError? = nil
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
var body: some View {
|
||||||
|
if case .saving(_) = dataStore.currentUser {
|
||||||
|
ActivityIndicator(isAnimating: .constant(true), style: .large)
|
||||||
|
} else {
|
||||||
|
VStack(alignment: .leading, spacing: 4.0) {
|
||||||
|
if case .unavailable = self.error {
|
||||||
|
Text("username_unavailable")
|
||||||
|
.foregroundColor(.red)
|
||||||
|
.multilineTextAlignment(.center)
|
||||||
|
}
|
||||||
|
Form {
|
||||||
|
Section("prompt_username") {
|
||||||
|
TextField("prompt_username", text: $username)
|
||||||
|
.textContentType(.username)
|
||||||
|
}
|
||||||
|
Button("save", action: {
|
||||||
|
Task {
|
||||||
|
self.error = await dataStore.updateUsername(username)
|
||||||
|
if self.error == nil {
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct EditUsernameView_Previews: PreviewProvider {
|
||||||
|
static var previews: some View {
|
||||||
|
EditUsernameView(username: "username")
|
||||||
|
.environmentObject(DataStore(TwigsInMemoryCacheService()))
|
||||||
|
}
|
||||||
|
}
|
|
@ -11,30 +11,77 @@ import TwigsCore
|
||||||
|
|
||||||
struct ProfileView: View {
|
struct ProfileView: View {
|
||||||
@EnvironmentObject var dataStore: DataStore
|
@EnvironmentObject var dataStore: DataStore
|
||||||
|
var username: String {
|
||||||
|
if case let .success(user) = self.dataStore.currentUser {
|
||||||
|
return user.username
|
||||||
|
} else {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var email: String {
|
||||||
|
if case let .success(user) = self.dataStore.currentUser {
|
||||||
|
return user.email ?? ""
|
||||||
|
} else {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(spacing: 10) {
|
List {
|
||||||
Image(systemName: "person.circle.fill")
|
Section(content: {
|
||||||
.resizable()
|
NavigationLink(
|
||||||
.frame(width: 100, height: 100, alignment: .center)
|
destination: EditUsernameView(username: username)
|
||||||
.scaledToFill()
|
.navigationTitle("change_username")
|
||||||
.clipShape(Circle())
|
) {
|
||||||
.overlay(Circle().stroke(Color.white, lineWidth: 4))
|
Text("change_username")
|
||||||
.shadow(radius: 5)
|
}
|
||||||
if case let .success(user) = self.dataStore.currentUser {
|
// NavigationLink(destination: EmptyView()) {
|
||||||
Text(user.username)
|
// Text("change_profile_picture")
|
||||||
|
// }
|
||||||
|
}, header: {
|
||||||
|
HStack {
|
||||||
|
Spacer()
|
||||||
|
VStack(alignment: .center, spacing: 10.0) {
|
||||||
|
Image(systemName: "person.circle.fill")
|
||||||
|
.resizable()
|
||||||
|
.frame(width: 100, height: 100, alignment: .center)
|
||||||
|
.scaledToFill()
|
||||||
|
.clipShape(Circle())
|
||||||
|
.overlay(Circle().stroke(Color.white, lineWidth: 1))
|
||||||
|
.shadow(radius: 4)
|
||||||
|
Spacer()
|
||||||
|
Text(username)
|
||||||
|
}
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
Section {
|
||||||
|
NavigationLink(
|
||||||
|
destination: EditPasswordView()
|
||||||
|
.navigationTitle("change_password")
|
||||||
|
) {
|
||||||
|
Text("change_password")
|
||||||
|
}
|
||||||
|
NavigationLink(
|
||||||
|
destination: EditEmailView(email: email)
|
||||||
|
.navigationTitle("change_email")
|
||||||
|
) {
|
||||||
|
Text("change_email")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
NavigationLink(destination: EmptyView()) {
|
Section {
|
||||||
Text("change_password")
|
Button("logout", action: {
|
||||||
}
|
// TODO: Show some dialog to confirm
|
||||||
NavigationLink(destination: EmptyView()) {
|
dataStore.logout()
|
||||||
Text("change_email")
|
})
|
||||||
}
|
|
||||||
NavigationLink(destination: EmptyView()) {
|
|
||||||
Text("delete_account")
|
|
||||||
.foregroundColor(.red)
|
|
||||||
}
|
}
|
||||||
|
// Section {
|
||||||
|
// NavigationLink(destination: EmptyView()) {
|
||||||
|
// Text("delete_account")
|
||||||
|
// .foregroundColor(.red)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -43,6 +90,7 @@ struct ProfileView: View {
|
||||||
struct ProfileView_Previews: PreviewProvider {
|
struct ProfileView_Previews: PreviewProvider {
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
ProfileView()
|
ProfileView()
|
||||||
|
.environmentObject(DataStore(TwigsInMemoryCacheService()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
|
@ -66,6 +66,16 @@ struct TabbedBudgetView: View {
|
||||||
}
|
}
|
||||||
.tag(3)
|
.tag(3)
|
||||||
.keyboardShortcut("4")
|
.keyboardShortcut("4")
|
||||||
|
NavigationView {
|
||||||
|
ProfileView()
|
||||||
|
.navigationBarTitle("profile")
|
||||||
|
}
|
||||||
|
.tabItem {
|
||||||
|
Image(systemName: "person.circle.fill")
|
||||||
|
Text("profile")
|
||||||
|
}
|
||||||
|
.tag(4)
|
||||||
|
.keyboardShortcut("5")
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
ActivityIndicator(isAnimating: .constant(true), style: .large)
|
ActivityIndicator(isAnimating: .constant(true), style: .large)
|
||||||
|
@ -104,9 +114,13 @@ struct TabbedBudgetView: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
//
|
|
||||||
//struct TabbedBudgetView_Previews: PreviewProvider {
|
struct TabbedBudgetView_Previews: PreviewProvider {
|
||||||
// static var previews: some View {
|
@StateObject static var apiService = TwigsInMemoryCacheService()
|
||||||
// TabbedBudgetView()
|
|
||||||
// }
|
static var previews: some View {
|
||||||
//}
|
TabbedBudgetView()
|
||||||
|
.environmentObject(DataStore(apiService))
|
||||||
|
.environmentObject(apiService)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -13,7 +13,7 @@ import TwigsCore
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
class MockUserRepository: UserRepository {
|
class MockUserRepository: UserRepository {
|
||||||
static let loginResponse = LoginResponse(token: "token", expiration: "2020-01-01T12:00:00Z", userId: "0")
|
static let loginResponse = LoginResponse(token: "token", expiration: "2020-01-01T12:00:00Z", userId: "0")
|
||||||
static let currentUser = User(id: "0", username: "root", email: "root@localhost", avatar: nil)
|
static let currentUser = User(id: "0", username: "root", email: "root@localhost", password: nil, avatar: nil)
|
||||||
static var token: String? = nil
|
static var token: String? = nil
|
||||||
|
|
||||||
func setToken(_ token: String) {
|
func setToken(_ token: String) {
|
||||||
|
|
|
@ -73,6 +73,8 @@
|
||||||
|
|
||||||
// MARK: Profile
|
// MARK: Profile
|
||||||
"profile" = "Profile";
|
"profile" = "Profile";
|
||||||
|
"change_username" = "Change Username";
|
||||||
|
"change_profile_picture" = "Change Profile Picture";
|
||||||
"change_password" = "Change Password";
|
"change_password" = "Change Password";
|
||||||
"change_email" = "Change Email";
|
"change_email" = "Change Email";
|
||||||
"delete_account" = "Delete Account";
|
"delete_account" = "Delete Account";
|
||||||
|
@ -131,3 +133,10 @@
|
||||||
"end" = "End";
|
"end" = "End";
|
||||||
"never" = "Never";
|
"never" = "Never";
|
||||||
"onDate" = "On Date";
|
"onDate" = "On Date";
|
||||||
|
|
||||||
|
"email_invalid" = "Invalid email address";
|
||||||
|
"cannot_be_empty" = "This cannot be empty";
|
||||||
|
"username_taken" = "Username taken";
|
||||||
|
"email_taken" = "Email taken";
|
||||||
|
"passwords_dont_match" = "Password don't match"
|
||||||
|
"unknown_error" = "An unknown error has occurred";
|
||||||
|
|
|
@ -74,6 +74,8 @@
|
||||||
|
|
||||||
// MARK: Profile
|
// MARK: Profile
|
||||||
"profile" = "Perfil";
|
"profile" = "Perfil";
|
||||||
|
"change_username" = "Cambiar Nombre de Usuario";
|
||||||
|
"change_profile_picture" = "Cambiar Foto de Perfíl";
|
||||||
"change_password" = "Cambiar Contraseña";
|
"change_password" = "Cambiar Contraseña";
|
||||||
"change_email" = "Cambiar Correo";
|
"change_email" = "Cambiar Correo";
|
||||||
"delete_account" = "Eliminar Cuenta";
|
"delete_account" = "Eliminar Cuenta";
|
||||||
|
@ -132,3 +134,10 @@
|
||||||
"end" = "Fin ";
|
"end" = "Fin ";
|
||||||
"never" = "Nunca";
|
"never" = "Nunca";
|
||||||
"onDate" = "Fecha";
|
"onDate" = "Fecha";
|
||||||
|
|
||||||
|
"email_invalid" = "Correo electrónico inválido";
|
||||||
|
"cannot_be_empty" = "No se puede omitir";
|
||||||
|
"username_taken" = "Nombre de usuario no disponible";
|
||||||
|
"email_taken" = "Correo electrónico no disponible";
|
||||||
|
"passwords_dont_match" = "Contraseñas no coinciden"
|
||||||
|
"unknown_error" = "Se ocurrió un error";
|
||||||
|
|
Loading…
Reference in a new issue