Merge pull request #10 from rock-n-code/integration/login

Integration: Login module
This commit is contained in:
Javier Cicchelli 2022-12-11 22:51:26 +01:00 committed by GitHub
commit e7ed33549c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 235 additions and 34 deletions

View File

@ -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<Bool> {
.init { account == nil } set: { _ in }
}
}
// MARK: - Previews
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()

View File

@ -10,6 +10,8 @@ let package = Package(
name: "Libraries",
targets: [
"APIService",
"DataModels",
"Dependencies",
"KeychainStorage"
]
),
@ -23,6 +25,7 @@ let package = Package(
],
targets: [
.target(name: "APIService"),
.target(name: "DataModels"),
.target(
name: "Dependencies",
dependencies: [

View File

@ -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

View File

@ -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

View File

@ -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"
}
}

View File

@ -0,0 +1,26 @@
//
// Account.swift
// DataModels
//
// Created by Javier Cicchelli on 11/12/2022.
// Copyright © 2022 Röck+Cöde. All rights reserved.
//
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
}
}

View File

@ -0,0 +1,73 @@
//
// 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 {
// 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
}
}
}

View File

@ -9,6 +9,6 @@
import APIService
import DependencyInjection
struct APIServiceKey: DependencyKey {
static var currentValue = APIService()
public struct APIServiceKey: DependencyKey {
public static var currentValue = APIService()
}

View File

@ -62,13 +62,11 @@ public struct KeychainStorage<Model: Codable>: 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<Model: Codable>: 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.")
}
}

View File

@ -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(

View File

@ -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 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.";

View File

@ -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)
@ -76,9 +72,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)
@ -88,6 +92,9 @@ struct LoginForm: View {
.onAppear {
setClearButtonIfNeeded()
}
.onChange(of: focusedField) { _ in
onTextFieldFocused()
}
}
}
@ -101,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 }

View File

@ -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(
@ -84,6 +97,7 @@ fileprivate extension LoginView {
} icon: {
if isAuthenticating {
ProgressView()
.controlSize(.regular)
} else {
EmptyView()
}
@ -95,6 +109,9 @@ fileprivate extension LoginView {
.buttonBorderShape(.roundedRectangle(radius: 8))
.controlSize(.large)
.disabled(isLoginDisabled)
.task(id: isAuthenticating) {
await authenticate()
}
}
}
}
@ -103,9 +120,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 = "login.error.authentication_failed.text"
isAuthenticating = false
} catch {
errorMessage = "login.error.authentication_unknown.text"
isAuthenticating = false
}
}
}
// MARK: - Previews