186 lines
4.7 KiB
Swift
186 lines
4.7 KiB
Swift
//
|
|
// LoginView.swift
|
|
// Login
|
|
//
|
|
// Created by Javier Cicchelli on 30/11/2022.
|
|
// Copyright © 2022 Röck+Cöde. All rights reserved.
|
|
//
|
|
|
|
import APIService
|
|
import DataModels
|
|
import SwiftUI
|
|
import UseCases
|
|
|
|
public struct LoginView: View {
|
|
|
|
// MARK: States
|
|
|
|
@State private var containerTopPadding: CGFloat = 0
|
|
|
|
// MARK: Properties
|
|
|
|
private let authenticated: AuthenticatedClosure
|
|
|
|
// MARK: Initialisers
|
|
|
|
public init(authenticated: @escaping AuthenticatedClosure) {
|
|
self.authenticated = authenticated
|
|
}
|
|
|
|
// MARK: Body
|
|
|
|
public var body: some View {
|
|
ScrollView(
|
|
.vertical,
|
|
showsIndicators: false
|
|
) {
|
|
Container(authenticated)
|
|
.padding(.horizontal, 24)
|
|
.padding(.top, containerTopPadding)
|
|
}
|
|
.background(Color.primary.colorInvert())
|
|
.ignoresSafeArea()
|
|
.overlay(ViewHeightGeometry())
|
|
.onPreferenceChange(ViewHeightPreferenceKey.self) { height in
|
|
containerTopPadding = height * 0.1
|
|
}
|
|
.interactiveDismissDisabled()
|
|
}
|
|
|
|
}
|
|
|
|
// MARK: - Views
|
|
|
|
fileprivate extension LoginView {
|
|
struct Container: View {
|
|
|
|
// MARK: States
|
|
|
|
@State private var isAuthenticating: Bool = false
|
|
@State private var username: String = ""
|
|
@State private var password: String = ""
|
|
@State private var errorMessage: String?
|
|
|
|
// MARK: Properties
|
|
|
|
private var getUser: GetUserUseCase = .init()
|
|
|
|
private let authenticated: AuthenticatedClosure
|
|
|
|
init(_ authenticated: @escaping AuthenticatedClosure) {
|
|
self.authenticated = authenticated
|
|
}
|
|
|
|
// MARK: Body
|
|
|
|
var body: some View {
|
|
VStack(spacing: 32) {
|
|
Text(
|
|
"login.title.text",
|
|
bundle: .module
|
|
)
|
|
.font(.largeTitle)
|
|
.fontWeight(.bold)
|
|
.foregroundColor(.primary)
|
|
|
|
LoginForm(
|
|
username: $username,
|
|
password: $password,
|
|
errorMessage: $errorMessage
|
|
) {
|
|
isAuthenticating = true
|
|
}
|
|
|
|
Button {
|
|
isAuthenticating = true
|
|
} label: {
|
|
Label {
|
|
Text(
|
|
"login.button.log_in.text",
|
|
bundle: .module
|
|
)
|
|
.fontWeight(.semibold)
|
|
} icon: {
|
|
if isAuthenticating {
|
|
ProgressView()
|
|
.tint(.white)
|
|
.controlSize(.regular)
|
|
} else {
|
|
EmptyView()
|
|
}
|
|
}
|
|
.labelStyle(.logIn)
|
|
}
|
|
.tint(.red)
|
|
.buttonStyle(.borderedProminent)
|
|
.buttonBorderShape(.roundedRectangle(radius: 8))
|
|
.controlSize(.large)
|
|
.disabled(isLoginDisabled)
|
|
.task(id: isAuthenticating) {
|
|
await authenticate()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Helpers
|
|
|
|
private extension LoginView.Container {
|
|
|
|
// MARK: Computed
|
|
|
|
var isLoginDisabled: Bool {
|
|
username.isEmpty
|
|
|| password.isEmpty
|
|
|| errorMessage != nil
|
|
}
|
|
|
|
// MARK: Functions
|
|
|
|
func authenticate() async {
|
|
guard isAuthenticating else { return }
|
|
|
|
defer { isAuthenticating = false }
|
|
|
|
do {
|
|
let user = try await getUser(
|
|
username: username,
|
|
password: password
|
|
)
|
|
|
|
// Added some throttle (1 second) not to hide the loading indicator right away.
|
|
try await Task.sleep(nanoseconds: .Constants.secondInNanoseconds)
|
|
|
|
authenticated(user)
|
|
} catch APIClientError.authenticationFailed {
|
|
errorMessage = "login.error.authentication_failed.text"
|
|
} catch {
|
|
errorMessage = "login.error.authentication_unknown.text"
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
// MARK: - Type aliases
|
|
|
|
public typealias AuthenticatedClosure = (User) -> Void
|
|
|
|
// MARK: - UInt64+Constants
|
|
|
|
private extension UInt64 {
|
|
enum Constants {
|
|
static let secondInNanoseconds = UInt64(1 * Double(NSEC_PER_SEC))
|
|
}
|
|
}
|
|
|
|
// MARK: - Previews
|
|
|
|
struct LoginView_Previews: PreviewProvider {
|
|
static var previews: some View {
|
|
LoginView { _ in
|
|
// authenticated closure.
|
|
}
|
|
}
|
|
}
|