Files
suixinkan_uikit/suixinkan/UI/Login/LoginViewModel.swift

206 lines
6.3 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//
// LoginViewModel.swift
// suixinkan
//
import Foundation
/// ViewModelUI Android `LoginViewModel`
final class LoginViewModel {
private(set) var account = ""
private(set) var password = ""
private(set) var isPrivacyChecked = false
private(set) var enableRegister = false
private(set) var isLoading = false
private(set) var isSelectingAccount = false
private(set) var pendingAccountSelection: AccountSelectionPayload?
var isLoginEnabled: Bool {
isValidPhone && !trimmedPassword.isEmpty
}
var trimmedPassword: String {
password.trimmingCharacters(in: .whitespacesAndNewlines)
}
var normalizedUsername: String {
normalizedPhoneNumber(account)
}
var isValidPhone: Bool {
normalizedUsername.count == 11 && normalizedUsername.first == "1"
}
var onStateChange: (() -> Void)?
///
func applyStoredPreferences() {
if account.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty,
let lastUsername = AppStore.shared.lastLoginUsername {
account = lastUsername
}
isPrivacyChecked = AppStore.shared.privacyAgreementAccepted
notifyStateChange()
}
func updateAccount(_ value: String) {
account = value
notifyStateChange()
}
func updatePassword(_ value: String) {
password = value
notifyStateChange()
}
func togglePrivacyChecked() {
isPrivacyChecked.toggle()
notifyStateChange()
}
func updateEnableRegister(_ enabled: Bool) {
enableRegister = enabled
notifyStateChange()
}
/// +86 11
func normalizeUsernameCountryCodeIfNeeded() {
let digits = account.filter(\.isNumber)
let normalizedPhone = normalizedPhoneNumber(account)
guard normalizedPhone != digits, normalizedPhone.count == 11 else { return }
account = normalizedPhone
notifyStateChange()
}
///
func validateForLogin() -> LoginValidationError? {
guard isValidPhone else {
return .invalidPhone
}
guard !trimmedPassword.isEmpty else {
return .emptyPassword
}
guard isPrivacyChecked else {
return .privacyUnchecked
}
return nil
}
func login() {
if let error = validateForLogin() {
onShowMessage?(error.message)
return
}
onLogin?()
}
/// App /
func loadAppConfig(authAPI: AuthAPI) async {
do {
let config = try await authAPI.getAppConfig()
updateEnableRegister(config.enableRegister)
} catch {
//
}
}
///
func login(authAPI: AuthAPI) async throws -> LoginResolution {
guard !isLoading else {
throw CancellationError()
}
isLoading = true
notifyStateChange()
defer {
isLoading = false
notifyStateChange()
}
let response = try await authAPI.login(
username: normalizedUsername,
password: trimmedPassword
)
return try await resolveLoginResponse(response, authAPI: authAPI)
}
/// set-user token
func selectAccount(_ account: AccountSwitchAccount, authAPI: AuthAPI) async throws -> V9AuthResponse {
guard let payload = pendingAccountSelection, payload.hasTempToken else {
throw LoginFlowError.missingToken
}
guard account.businessUserId > 0 else {
throw LoginFlowError.invalidAccount
}
guard !isSelectingAccount else {
throw CancellationError()
}
isSelectingAccount = true
notifyStateChange()
defer {
isSelectingAccount = false
notifyStateChange()
}
let response = try await authAPI.setUser(account.toSetUserRequest(), tokenOverride: payload.tempToken)
guard !response.token.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else {
throw LoginFlowError.missingToken
}
pendingAccountSelection = nil
notifyStateChange()
return response
}
///
func clearPendingAccountSelection() {
pendingAccountSelection = nil
notifyStateChange()
}
var onShowMessage: ((String) -> Void)?
var onLogin: (() -> Void)?
var onLoginByCode: (() -> Void)?
var onRegister: (() -> Void)?
var onUserAgreement: (() -> Void)?
var onPrivacyPolicy: (() -> Void)?
private func resolveLoginResponse(_ response: V9AuthResponse, authAPI: AuthAPI) async throws -> LoginResolution {
let token = response.token.trimmingCharacters(in: .whitespacesAndNewlines)
guard !token.isEmpty else {
throw LoginFlowError.missingToken
}
let accounts = response.accounts.filter { $0.businessUserId > 0 }
guard !accounts.isEmpty else {
throw LoginFlowError.noAvailableAccount
}
if accounts.count == 1, let account = accounts.first {
let finalResponse = try await authAPI.setUser(account.toSetUserRequest(), tokenOverride: token)
guard !finalResponse.token.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else {
throw LoginFlowError.missingToken
}
return .completed(finalResponse, account)
}
let payload = AccountSelectionPayload(tempToken: token, accounts: accounts)
pendingAccountSelection = payload
notifyStateChange()
return .needsAccountSelection(payload)
}
private func normalizedPhoneNumber(_ value: String) -> String {
var digits = value.filter(\.isNumber)
if digits.hasPrefix("86"), digits.count == 13 {
digits.removeFirst(2)
}
return digits
}
private func notifyStateChange() {
onStateChange?()
}
}