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.
}
}
}