Merge pull request #10 from rock-n-code/integration/login
Integration: Login module
This commit is contained in:
commit
e7ed33549c
@ -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()
|
||||
|
@ -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: [
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
26
Libraries/Sources/DataModels/Models/Account.swift
Normal file
26
Libraries/Sources/DataModels/Models/Account.swift
Normal 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
|
||||
}
|
||||
|
||||
}
|
73
Libraries/Sources/DataModels/Models/User.swift
Normal file
73
Libraries/Sources/DataModels/Models/User.swift
Normal 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
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@ -9,6 +9,6 @@
|
||||
import APIService
|
||||
import DependencyInjection
|
||||
|
||||
struct APIServiceKey: DependencyKey {
|
||||
static var currentValue = APIService()
|
||||
public struct APIServiceKey: DependencyKey {
|
||||
public static var currentValue = APIService()
|
||||
}
|
||||
|
@ -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.")
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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(
|
||||
|
@ -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.";
|
||||
|
@ -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 }
|
||||
|
||||
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user