From 620f9faf36e25d9d31eeeb379f58087f344b88cd Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Sun, 11 Dec 2022 20:30:01 +0100 Subject: [PATCH 01/14] Added the Cores and Libraries dependencies to the Login target in the Modules package. --- Modules/Package.swift | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/Modules/Package.swift b/Modules/Package.swift index 6e6f8a9..5e4826e 100644 --- a/Modules/Package.swift +++ b/Modules/Package.swift @@ -5,9 +5,7 @@ import PackageDescription let package = Package( name: "Modules", defaultLocalization: "en", - platforms: [ - .iOS(.v15) - ], + platforms: [.iOS(.v15)], products: [ .library( name: "Modules", @@ -18,9 +16,17 @@ let package = Package( ] ), ], + dependencies: [ + .package(path: "../Cores"), + .package(path: "../Libraries") + ], targets: [ .target( name: "Login", + dependencies: [ + "Cores", + "Libraries" + ], resources: [.process("Resources")] ), .target( From 71021f22abd8244e91c631a5ae94c15a1a1090e3 Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Sun, 11 Dec 2022 20:31:21 +0100 Subject: [PATCH 02/14] Changed the accessor of the APIServiceKey dependency key to public. --- Libraries/Sources/Dependencies/Keys/APIServiceKey.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Libraries/Sources/Dependencies/Keys/APIServiceKey.swift b/Libraries/Sources/Dependencies/Keys/APIServiceKey.swift index e118aaf..88eed55 100644 --- a/Libraries/Sources/Dependencies/Keys/APIServiceKey.swift +++ b/Libraries/Sources/Dependencies/Keys/APIServiceKey.swift @@ -9,6 +9,6 @@ import APIService import DependencyInjection -struct APIServiceKey: DependencyKey { - static var currentValue = APIService() +public struct APIServiceKey: DependencyKey { + public static var currentValue = APIService() } From e4dc9251681d0fdd92fc9a411f3c98cdabd3044f Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Sun, 11 Dec 2022 20:31:52 +0100 Subject: [PATCH 03/14] Changed the accessor of the properties of the Me and Item models to public. --- Libraries/Sources/APIService/Models/Item.swift | 14 +++++++------- Libraries/Sources/APIService/Models/Me.swift | 6 +++--- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/Libraries/Sources/APIService/Models/Item.swift b/Libraries/Sources/APIService/Models/Item.swift index e607a77..fc82509 100644 --- a/Libraries/Sources/APIService/Models/Item.swift +++ b/Libraries/Sources/APIService/Models/Item.swift @@ -9,13 +9,13 @@ import Foundation public struct Item { - let idParent: String? - let id: String - let name: String - let isDirectory: Bool - let lastModifiedAt: Date - let size: Int? - let contentType: String? + public let idParent: String? + public let id: String + public let name: String + public let isDirectory: Bool + public let lastModifiedAt: Date + public let size: Int? + public let contentType: String? } // MARK: - Decodable diff --git a/Libraries/Sources/APIService/Models/Me.swift b/Libraries/Sources/APIService/Models/Me.swift index 35c1b87..3a3e287 100644 --- a/Libraries/Sources/APIService/Models/Me.swift +++ b/Libraries/Sources/APIService/Models/Me.swift @@ -7,9 +7,9 @@ // public struct Me { - let firstName: String - let lastName: String - let rootItem: Item + public let firstName: String + public let lastName: String + public let rootItem: Item } // MARK: - Decodable From 81ca7ce6f39b78671149924667c6db611c34b735 Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Sun, 11 Dec 2022 20:53:15 +0100 Subject: [PATCH 04/14] Created the DataModel target in the Libraries package. --- Libraries/Package.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Libraries/Package.swift b/Libraries/Package.swift index 14d92b0..2ef8d6e 100644 --- a/Libraries/Package.swift +++ b/Libraries/Package.swift @@ -23,6 +23,7 @@ let package = Package( ], targets: [ .target(name: "APIService"), + .target(name: "DataModels"), .target( name: "Dependencies", dependencies: [ From 4f53a40f047aa2d7158e66d555cc88fb840e3e0a Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Sun, 11 Dec 2022 20:53:44 +0100 Subject: [PATCH 05/14] Implemented the Account and User models for the DataModel target. --- .../Sources/DataModels/Models/Account.swift | 12 ++++++++ .../Sources/DataModels/Models/User.swift | 29 +++++++++++++++++++ 2 files changed, 41 insertions(+) create mode 100644 Libraries/Sources/DataModels/Models/Account.swift create mode 100644 Libraries/Sources/DataModels/Models/User.swift diff --git a/Libraries/Sources/DataModels/Models/Account.swift b/Libraries/Sources/DataModels/Models/Account.swift new file mode 100644 index 0000000..f06e6aa --- /dev/null +++ b/Libraries/Sources/DataModels/Models/Account.swift @@ -0,0 +1,12 @@ +// +// Account.swift +// DataModels +// +// Created by Javier Cicchelli on 11/12/2022. +// Copyright © 2022 Röck+Cöde. All rights reserved. +// + +public struct Account: Codable { + public let username: String + public let password: String +} diff --git a/Libraries/Sources/DataModels/Models/User.swift b/Libraries/Sources/DataModels/Models/User.swift new file mode 100644 index 0000000..2244f53 --- /dev/null +++ b/Libraries/Sources/DataModels/Models/User.swift @@ -0,0 +1,29 @@ +// +// User.swift +// DataModels +// +// Created by Javier Cicchelli on 11/12/2022. +// Copyright © 2022 Röck+Cöde. All rights reserved. +// + +import Foundation + +public struct User { + public let profile: Profile + public let rootFolder: RootFolder +} + +// MARK: - Structs + +extension User { + public struct Profile { + public let firstName: String + public let lastName: String + } + + public struct RootFolder { + public let id: String + public let name: String + public let lastModifiedAt: Date + } +} From 09b092f33cecdfe17ff0f92e13c0da1328f26676 Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Sun, 11 Dec 2022 21:00:51 +0100 Subject: [PATCH 06/14] Added the DataModels and Dependencies targets to the list of targets to public from the Libraries package. --- Libraries/Package.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Libraries/Package.swift b/Libraries/Package.swift index 2ef8d6e..f4e096b 100644 --- a/Libraries/Package.swift +++ b/Libraries/Package.swift @@ -10,6 +10,8 @@ let package = Package( name: "Libraries", targets: [ "APIService", + "DataModels", + "Dependencies", "KeychainStorage" ] ), From 39444cd9ea4ecd2ffe6368fb4a5d5be489ebda12 Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Sun, 11 Dec 2022 21:01:47 +0100 Subject: [PATCH 07/14] Defined the "account" keychain storage key in the String+KeychainStorageKeys extension. --- .../Keys/String+KeychainStorageKeys.swift | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 Libraries/Sources/DataModels/Keys/String+KeychainStorageKeys.swift diff --git a/Libraries/Sources/DataModels/Keys/String+KeychainStorageKeys.swift b/Libraries/Sources/DataModels/Keys/String+KeychainStorageKeys.swift new file mode 100644 index 0000000..e744ff8 --- /dev/null +++ b/Libraries/Sources/DataModels/Keys/String+KeychainStorageKeys.swift @@ -0,0 +1,13 @@ +// +// String+KeychainStorageKeys.swift +// DataModels +// +// Created by Javier Cicchelli on 11/12/2022. +// Copyright © 2022 Röck+Cöde. All rights reserved. +// + +public extension String { + enum KeychainStorage { + public static let account = "com.rockncode.app.assignment.be-real.library.data-models.keychain.key.account" + } +} From c00fe919f075efcc0f01f3cf7676e2ddaf5cf852 Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Sun, 11 Dec 2022 21:12:25 +0100 Subject: [PATCH 08/14] Implemented the initialisers for the Account and User data models. --- .../Sources/DataModels/Models/Account.swift | 14 ++++++ .../Sources/DataModels/Models/User.swift | 44 +++++++++++++++++++ 2 files changed, 58 insertions(+) diff --git a/Libraries/Sources/DataModels/Models/Account.swift b/Libraries/Sources/DataModels/Models/Account.swift index f06e6aa..c0b1711 100644 --- a/Libraries/Sources/DataModels/Models/Account.swift +++ b/Libraries/Sources/DataModels/Models/Account.swift @@ -7,6 +7,20 @@ // public struct Account: Codable { + + // MARK: Properties + public let username: String public let password: String + + // MARK: Initialisers + + public init( + username: String, + password: String + ) { + self.username = username + self.password = password + } + } diff --git a/Libraries/Sources/DataModels/Models/User.swift b/Libraries/Sources/DataModels/Models/User.swift index 2244f53..6a5e1cc 100644 --- a/Libraries/Sources/DataModels/Models/User.swift +++ b/Libraries/Sources/DataModels/Models/User.swift @@ -9,21 +9,65 @@ import Foundation public struct User { + + // MARK: Properties + public let profile: Profile public let rootFolder: RootFolder + + // MARK: Initialisers + + public init( + profile: Profile, + rootFolder: RootFolder + ) { + self.profile = profile + self.rootFolder = rootFolder + } + } // MARK: - Structs extension User { public struct Profile { + + // MARK: Properties + public let firstName: String public let lastName: String + + // MARK: Initialisers + + public init( + firstName: String, + lastName: String + ) { + self.firstName = firstName + self.lastName = lastName + } + } public struct RootFolder { + + // MARK: Properties + public let id: String public let name: String public let lastModifiedAt: Date + + // MARK: Initialisers + + public init( + id: String, + name: String, + lastModifiedAt: Date + ) { + self.id = id + self.name = name + self.lastModifiedAt = lastModifiedAt + } + } } From 0a8b18719a708fcddd7b182415e448832265074a Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Sun, 11 Dec 2022 21:32:35 +0100 Subject: [PATCH 09/14] Added the error messages in the Localizable strings of the Login module. --- Modules/Sources/Login/Resources/en.lproj/Localizable.strings | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Modules/Sources/Login/Resources/en.lproj/Localizable.strings b/Modules/Sources/Login/Resources/en.lproj/Localizable.strings index bd901a9..e53614f 100644 --- a/Modules/Sources/Login/Resources/en.lproj/Localizable.strings +++ b/Modules/Sources/Login/Resources/en.lproj/Localizable.strings @@ -10,3 +10,6 @@ "login.text_field.username.placeholder" = "Username"; "login.text_field.password.placeholder" = "Password"; "login.button.log_in.text" = "Log in"; + +"login.error.authentication_failed.text" = "The provided username and/or password are incorrect.\nPlease try again."; +"login.error.unknown.text" = "An unexpected error occurred while trying to authenticate your credentials.\nPlease try again at a later time."; From 342658e3f761c707cf5108b884390d10efd34de8 Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Sun, 11 Dec 2022 21:33:33 +0100 Subject: [PATCH 10/14] Implemented the first attempt at authentication in the LoginView view. --- .../Sources/Login/UI/Views/LoginView.swift | 48 ++++++++++++++++++- 1 file changed, 46 insertions(+), 2 deletions(-) diff --git a/Modules/Sources/Login/UI/Views/LoginView.swift b/Modules/Sources/Login/UI/Views/LoginView.swift index 392073b..89e8e0e 100644 --- a/Modules/Sources/Login/UI/Views/LoginView.swift +++ b/Modules/Sources/Login/UI/Views/LoginView.swift @@ -6,6 +6,11 @@ // Copyright © 2022 Röck+Cöde. All rights reserved. // +import APIService +import DataModels +import DependencyInjection +import Dependencies +import KeychainStorage import SwiftUI public struct LoginView: View { @@ -43,6 +48,14 @@ public struct LoginView: View { fileprivate extension LoginView { struct LoginContainer: View { + // MARK: Dependencies + + @Dependency(\.apiService) private var apiService + + // MARK: Storages + + @KeychainStorage(key: .KeychainStorage.account) private var account: Account? + // MARK: States @State private var isAuthenticating: Bool = false @@ -68,11 +81,11 @@ fileprivate extension LoginView { password: $password, errorMessage: $errorMessage ) { - // TODO: login with the username and password. + isAuthenticating = true } Button { - // TODO: login with the username and password. + isAuthenticating = true } label: { Label { Text( @@ -95,6 +108,9 @@ fileprivate extension LoginView { .buttonBorderShape(.roundedRectangle(radius: 8)) .controlSize(.large) .disabled(isLoginDisabled) + .task(id: isAuthenticating) { + await authenticate() + } } } } @@ -103,9 +119,37 @@ fileprivate extension LoginView { // MARK: - Helpers private extension LoginView.LoginContainer { + + // MARK: Computed + var isLoginDisabled: Bool { username.isEmpty || password.isEmpty } + + // MARK: Functions + + func authenticate() async { + guard isAuthenticating else { return } + + do { + _ = try await apiService.getUser(credentials: .init( + username: username, + password: password + )) + + account = .init( + username: username, + password: password + ) + } catch APIClientError.authenticationFailed { + errorMessage = .init(localized: "login.error.authentication_failed.text") + isAuthenticating = false + } catch { + errorMessage = .init(localized: "login.error.unknown.text") + isAuthenticating = false + } + } + } // MARK: - Previews From c9468a6a685bec56665a1ae79e86ad70f588b5c7 Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Sun, 11 Dec 2022 22:09:39 +0100 Subject: [PATCH 11/14] Fixed the localisation of the error messages in the LoginView view. --- .../Login/Resources/en.lproj/Localizable.strings | 4 ++-- .../Sources/Login/UI/Components/LoginForm.swift | 14 +++++++++++--- Modules/Sources/Login/UI/Views/LoginView.swift | 4 ++-- 3 files changed, 15 insertions(+), 7 deletions(-) diff --git a/Modules/Sources/Login/Resources/en.lproj/Localizable.strings b/Modules/Sources/Login/Resources/en.lproj/Localizable.strings index e53614f..f571afc 100644 --- a/Modules/Sources/Login/Resources/en.lproj/Localizable.strings +++ b/Modules/Sources/Login/Resources/en.lproj/Localizable.strings @@ -11,5 +11,5 @@ "login.text_field.password.placeholder" = "Password"; "login.button.log_in.text" = "Log in"; -"login.error.authentication_failed.text" = "The provided username and/or password are incorrect.\nPlease try again."; -"login.error.unknown.text" = "An unexpected error occurred while trying to authenticate your credentials.\nPlease try again at a later time."; +"login.error.authentication_failed.text" = "The given username and/or password are not correct.\nPlease re-enter your credentials and try again."; +"login.error.authentication_unknown.text" = "An unexpected error occurred while trying to authenticate your credentials.\nPlease try again at a later time."; diff --git a/Modules/Sources/Login/UI/Components/LoginForm.swift b/Modules/Sources/Login/UI/Components/LoginForm.swift index c40fc6c..543dc27 100644 --- a/Modules/Sources/Login/UI/Components/LoginForm.swift +++ b/Modules/Sources/Login/UI/Components/LoginForm.swift @@ -76,9 +76,17 @@ struct LoginForm: View { if let errorMessage { Divider() - Text(errorMessage) - .font(.body) - .foregroundColor(.red) + Text( + NSLocalizedString( + errorMessage, + bundle: .module, + comment: "The error message received from the backend." + ) + ) + .font(.body) + .foregroundColor(.red) + .frame(maxWidth: .infinity) + .multilineTextAlignment(.center) } } .frame(maxWidth: .infinity, alignment: .leading) diff --git a/Modules/Sources/Login/UI/Views/LoginView.swift b/Modules/Sources/Login/UI/Views/LoginView.swift index 89e8e0e..955a6f3 100644 --- a/Modules/Sources/Login/UI/Views/LoginView.swift +++ b/Modules/Sources/Login/UI/Views/LoginView.swift @@ -142,10 +142,10 @@ private extension LoginView.LoginContainer { password: password ) } catch APIClientError.authenticationFailed { - errorMessage = .init(localized: "login.error.authentication_failed.text") + errorMessage = "login.error.authentication_failed.text" isAuthenticating = false } catch { - errorMessage = .init(localized: "login.error.unknown.text") + errorMessage = "login.error.authentication_unknown.text" isAuthenticating = false } } From 07defd0045bf360229a0228a17ac8ff13e6758e1 Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Sun, 11 Dec 2022 22:11:28 +0100 Subject: [PATCH 12/14] Fixed the initialiser of the KeychainStorage property wrapper. --- .../Property Wrappers/KeychainStorage.swift | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/Libraries/Sources/KeychainStorage/Property Wrappers/KeychainStorage.swift b/Libraries/Sources/KeychainStorage/Property Wrappers/KeychainStorage.swift index 11fc43f..34f8332 100644 --- a/Libraries/Sources/KeychainStorage/Property Wrappers/KeychainStorage.swift +++ b/Libraries/Sources/KeychainStorage/Property Wrappers/KeychainStorage.swift @@ -62,13 +62,11 @@ public struct KeychainStorage: DynamicProperty { self.keychain = keychain do { - let data = try keychain.getData(key, ignoringAttributeSynchronizable: true) - - guard let data else { - assertionFailure("The data of the '\(key)' key should have been obtained from the keychain storage.") + guard let data = try keychain.getData(key, ignoringAttributeSynchronizable: true) else { + self._value = .init(initialValue: defaultValue) return } - + do { let model = try decoder.decode(Value.self, from: data) @@ -77,7 +75,7 @@ public struct KeychainStorage: DynamicProperty { assertionFailure("The data for the '\(key)' key should have been decoded properly.") } } catch { - self._value = .init(initialValue: defaultValue) + assertionFailure("The data of the '\(key)' key should have been obtained from the keychain storage.") } } From a2f7a7f0f717f3cfc728a230310b37412d01cb56 Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Sun, 11 Dec 2022 22:13:56 +0100 Subject: [PATCH 13/14] Fixed the error cleanup when any of the text field is focused in the LoginForm component. --- .../Sources/Login/UI/Components/LoginForm.swift | 16 +++++++++++----- Modules/Sources/Login/UI/Views/LoginView.swift | 1 + 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/Modules/Sources/Login/UI/Components/LoginForm.swift b/Modules/Sources/Login/UI/Components/LoginForm.swift index 543dc27..5bb0711 100644 --- a/Modules/Sources/Login/UI/Components/LoginForm.swift +++ b/Modules/Sources/Login/UI/Components/LoginForm.swift @@ -38,11 +38,7 @@ struct LoginForm: View { comment: "The placeholder for the username text field." ), text: $username - ) { isBeginEditing in - guard isBeginEditing, errorMessage != nil else { return } - - errorMessage = nil - } + ) .textContentType(.username) .lineLimit(1) .autocapitalization(.none) @@ -96,6 +92,9 @@ struct LoginForm: View { .onAppear { setClearButtonIfNeeded() } + .onChange(of: focusedField) { _ in + onTextFieldFocused() + } } } @@ -109,6 +108,13 @@ private extension LoginForm { textFieldAppearance.clearButtonMode = .whileEditing } + func onTextFieldFocused() { + guard errorMessage != nil else { return } + + password = "" + errorMessage = nil + } + func onUsernameReturnPressed() { guard !username.isEmpty else { return } diff --git a/Modules/Sources/Login/UI/Views/LoginView.swift b/Modules/Sources/Login/UI/Views/LoginView.swift index 955a6f3..754fe4c 100644 --- a/Modules/Sources/Login/UI/Views/LoginView.swift +++ b/Modules/Sources/Login/UI/Views/LoginView.swift @@ -97,6 +97,7 @@ fileprivate extension LoginView { } icon: { if isAuthenticating { ProgressView() + .controlSize(.regular) } else { EmptyView() } From c02e371adb29f94a47ffc477e420f7b3ab92f936 Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Sun, 11 Dec 2022 22:50:21 +0100 Subject: [PATCH 14/14] Implemented the showing of the LoginView view in the ContentView view. --- BeReal/ContentView.swift | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/BeReal/ContentView.swift b/BeReal/ContentView.swift index 12650bb..2c544e1 100644 --- a/BeReal/ContentView.swift +++ b/BeReal/ContentView.swift @@ -6,22 +6,42 @@ // Copyright © 2022 Röck+Cöde. All rights reserved. // -import SwiftUI import Browse +import DataModels import Login +import KeychainStorage import Profile +import SwiftUI struct ContentView: View { + + // MARK: Storages + + @KeychainStorage(key: .KeychainStorage.account) private var account: Account? + + // MARK: Body + var body: some View { NavigationView { BrowseView() } - .sheet(isPresented: .constant(true)) { - ProfileView {} + .sheet(isPresented: showLogin) { + LoginView() } } + } +// MARK: - Helpers + +private extension ContentView { + var showLogin: Binding { + .init { account == nil } set: { _ in } + } +} + +// MARK: - Previews + struct ContentView_Previews: PreviewProvider { static var previews: some View { ContentView()