Skip to content

diti223/AuthenticationKit

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

5 Commits
 
 
 
 
 
 
 
 
 
 

Repository files navigation

This is my take on refactoring the AuthenticationViewModel. With the Open/Closed Principle in mind, I've leveraged the usage of an abstract AuthenticationUseCase to delegate the complexity outside of the view model. This abstraction allowed for easy composition & decoration, splitting multiple responsibilities into smaller components. Even though the example is quite small, and such separation wouldn't be justified for this simple use case. I used my imagination to think of a bigger context, a bigger app that would probably have multiple API calls but also about testing.

Original demo code AuthenticationViewModel

class AuthenticationViewModel {
    @Published var errorMessage: String?
    @Published var message: String?
    let service: AuthenticationService
    let userDefault: UserDefaults
    let userSession: UserSession
    
    init(
        service: AuthenticationService,
        userDefault: UserDefaults = .standard,
        userSession: UserSession = .shared
    ) {
        self.service = service
        self.userDefault = userDefault
        self.userSession = userSession
    }
    
    public func login(username: String, password: String) async throws {
        guard !username.isEmpty, !password.isEmpty else {
            throw ValidationError.invalidCredentials
        }
        
        do {
            let user = try await service.login(
                username: username,
                password: password
            )
            if user.isNewUser {
                message = "Welcome, new user!"
            }
            
            userSession.currentUser = user
            userSession.isLoggedIn = true
            
            userDefault.set(username, forKey: "username")
            userDefault.set(password, forKey: "password")
        } catch {
            errorMessage = "Login failed: \(error.localizedDescription)"
        }
    }
}

Refactored AuthenticationViewModel

import Foundation
import Combine

public class AuthenticationViewModel: ObservableObject {
    @Published public var user: User?
    @Published public var errorMessage: String?
    @Published public var message: String?
    
    public let authenticationUseCase: AuthenticationUseCase
    
    public init(authenticationUseCase: AuthenticationUseCase) {
        self.authenticationUseCase = authenticationUseCase
    }
    
    public func login(username: String, password: String) async {
        do {
            self.user = try await authenticationUseCase.login(username: username, password: password)
            if user?.isNewUser == true {
                message = "Welcome, new user!"
            }
        } catch {
            errorMessage = "Login failed: \(error.localizedDescription)"
        }
    }
}

During the refactoring, the view model has been left with little responsibilities, besides managing the states. The actual service has become just one abstract use case, implemented with APIAuthenticationUseCase. In this API use case, demonstrates a scenario where the structure and values received differ substantially from that of the domain. In order to map the responses, I've used APIUserResponse and APIFetchTokenUseCaseResponse.

public struct APIAuthenticationUseCase: AuthenticationUseCase {
    public let httpClient: HTTPClient
    public let tokenStore: (Token) -> Void
    
    private let jsonDecoder: JSONDecoder = {
        let decoder = JSONDecoder()
        decoder.dateDecodingStrategy = .iso8601
        return decoder
    }()
    
    public let jsonEncoder = JSONEncoder()
    
    private var tokenUseCase: APIFetchTokenUseCase {
        APIFetchTokenUseCase(
            jsonDecoder: jsonDecoder,
            jsonEncoder: jsonEncoder,
            httpClient: httpClient
        )
    }
    
    public init(httpClient: HTTPClient, tokenStore: @escaping (Token) -> Void) {
        self.httpClient = httpClient
        self.tokenStore = tokenStore
    }
    
    
    
    public func login(username: String, password: String) async throws -> User {
        let token = try await tokenUseCase.fetch(
            username: username,
            password: password
        )
        
        tokenStore(token)
        
        let user = try await fetchUser(token: token)
        
        return user
    }
    
    private func fetchUser(token: String) async throws -> User {
        let responseData = try await httpClient.request(
            HTTPRequest(
                headers: ["Authorization": "Bearer \(token)"],
                method: .get,
                path: "/user"
            )
        ).body
        
        let apiResponse = try jsonDecoder.decode(APIUserResponse.self, from: responseData)
        
        return apiResponse.toUser()
    }
}

The storage logic has been moved to Keychain, to the relief of many developers that saw the code before. The entire dependencies management has been constructed in MainRoot to demonstrate how complexity can be composed, leaving the view model more simple and elegant, but in the end, delivering the same functionality.

public class MainRoot {
    public let httpClient: HTTPClient
    public let secureStorage: SecureStorage
    public var currentUser: User?
    
    init(
        httpClient: HTTPClient = URLSessionClient(
            baseURL: URL(string: "https://apple.com/")!,
            session: .shared
        ),
        secureStorage: SecureStorage = KeychainStorage()
    ) {
        self.httpClient = httpClient
        self.secureStorage = secureStorage
    }
    
    private static let tokenKey: String = "kAccessToken"
    
    private func makeAPIUseCase() -> APIAuthenticationUseCase {
        APIAuthenticationUseCase(
            httpClient: httpClient,
            tokenStore: { [weak self] token in
                let encoder = JSONEncoder()
                self?.secureStorage.store(data: try! encoder.encode(token), key: Self.tokenKey)
            }
        )
    }
    
    public func makeViewModel() -> AuthenticationViewModel {
        let useCase = makeAPIUseCase()
            .addValidation()
            .intercept { [weak self] user in
                self?.currentUser = user
            }
            
        
        let viewModel = AuthenticationViewModel(
            authenticationUseCase: useCase
        )
        
        return viewModel
    }
}

About

Demo of a simple authentication presentation logic

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages