添加网络层并接入 v9 登录流程及单元测试
This commit is contained in:
@ -12,13 +12,38 @@ final class LoginViewModel {
|
||||
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 {
|
||||
!account.isEmpty && !password.isEmpty
|
||||
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()
|
||||
@ -39,15 +64,101 @@ final class LoginViewModel {
|
||||
notifyStateChange()
|
||||
}
|
||||
|
||||
func login() {
|
||||
/// 当输入包含 +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 {
|
||||
onShowMessage?("请先阅读并同意用户协议与隐私政策")
|
||||
return .privacyUnchecked
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func login() {
|
||||
if let error = validateForLogin() {
|
||||
onShowMessage?(error.message)
|
||||
return
|
||||
}
|
||||
guard isLoginEnabled else { 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)?
|
||||
@ -55,6 +166,39 @@ final class LoginViewModel {
|
||||
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)
|
||||
}
|
||||
|
||||
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?()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user