Implement profile tab with Android-aligned flows and OSS upload.
Add personal info page, account switch, real-name auth, withdrawal settings, session cache extensions, AlibabaCloudOSS SPM, and unit tests. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@ -0,0 +1,219 @@
|
||||
//
|
||||
// WithdrawalSettingsViewModel.swift
|
||||
// suixinkan
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// 提现设置 ViewModel,管理银行卡绑定两步表单与提交状态。
|
||||
final class WithdrawalSettingsViewModel {
|
||||
private(set) var currentStep = 1
|
||||
private(set) var holderName = ""
|
||||
private(set) var bankNo = ""
|
||||
private(set) var bankCardFrontURL = ""
|
||||
private(set) var bankCardBackURL = ""
|
||||
private(set) var bankName = ""
|
||||
private(set) var provinceCode = ""
|
||||
private(set) var provinceName = ""
|
||||
private(set) var cityCode = ""
|
||||
private(set) var cityName = ""
|
||||
private(set) var branchName = ""
|
||||
private(set) var verificationCode = ""
|
||||
private(set) var isAgree = false
|
||||
private(set) var banks: [String] = []
|
||||
private(set) var areas: [AreasResponse] = []
|
||||
private(set) var loading = false
|
||||
private(set) var sendingCode = false
|
||||
private(set) var submitting = false
|
||||
|
||||
private(set) var pendingFrontImageData: Data?
|
||||
private(set) var pendingBackImageData: Data?
|
||||
private var pendingFrontFileName: String?
|
||||
private var pendingBackFileName: String?
|
||||
|
||||
var onStateChange: (() -> Void)?
|
||||
|
||||
var canGoNextStep: Bool {
|
||||
currentStep == 1
|
||||
&& !holderName.trimmed.isEmpty
|
||||
&& (12 ... 25).contains(bankNo.filter(\.isNumber).count)
|
||||
&& !bankCardFrontURL.isEmpty
|
||||
&& !bankCardBackURL.isEmpty
|
||||
&& isAgree
|
||||
}
|
||||
|
||||
var canSubmit: Bool {
|
||||
currentStep == 2
|
||||
&& !holderName.trimmed.isEmpty
|
||||
&& (12 ... 25).contains(bankNo.filter(\.isNumber).count)
|
||||
&& !bankName.trimmed.isEmpty
|
||||
&& !provinceCode.isEmpty
|
||||
&& !cityCode.isEmpty
|
||||
&& !branchName.trimmed.isEmpty
|
||||
&& !verificationCode.trimmed.isEmpty
|
||||
&& isAgree
|
||||
&& !submitting
|
||||
}
|
||||
|
||||
func updateHolderName(_ value: String) {
|
||||
holderName = value
|
||||
notifyStateChange()
|
||||
}
|
||||
|
||||
func updateBankNo(_ value: String) {
|
||||
bankNo = value
|
||||
notifyStateChange()
|
||||
}
|
||||
|
||||
func updateBankName(_ value: String) {
|
||||
bankName = value
|
||||
notifyStateChange()
|
||||
}
|
||||
|
||||
func updateBranchName(_ value: String) {
|
||||
branchName = value
|
||||
notifyStateChange()
|
||||
}
|
||||
|
||||
func updateVerificationCode(_ value: String) {
|
||||
verificationCode = value
|
||||
notifyStateChange()
|
||||
}
|
||||
|
||||
func updateAgree(_ value: Bool) {
|
||||
isAgree = value
|
||||
notifyStateChange()
|
||||
}
|
||||
|
||||
func selectProvince(_ area: AreasResponse) {
|
||||
provinceCode = area.code
|
||||
provinceName = area.name
|
||||
cityCode = ""
|
||||
cityName = ""
|
||||
notifyStateChange()
|
||||
}
|
||||
|
||||
func selectCity(_ area: AreasResponse) {
|
||||
cityCode = area.code
|
||||
cityName = area.name
|
||||
notifyStateChange()
|
||||
}
|
||||
|
||||
func goNextStep() {
|
||||
guard canGoNextStep else { return }
|
||||
currentStep = 2
|
||||
notifyStateChange()
|
||||
}
|
||||
|
||||
func prepareBankCardImage(data: Data, side: BankCardImageSide) throws {
|
||||
let processed = try BankCardImageProcessor.process(data: data, side: side)
|
||||
switch side {
|
||||
case .front:
|
||||
pendingFrontImageData = processed.data
|
||||
pendingFrontFileName = processed.fileName
|
||||
bankCardFrontURL = "pending"
|
||||
case .back:
|
||||
pendingBackImageData = processed.data
|
||||
pendingBackFileName = processed.fileName
|
||||
bankCardBackURL = "pending"
|
||||
}
|
||||
notifyStateChange()
|
||||
}
|
||||
|
||||
func load(api: ProfileAPI) async throws {
|
||||
guard !loading else { return }
|
||||
loading = true
|
||||
notifyStateChange()
|
||||
defer {
|
||||
loading = false
|
||||
notifyStateChange()
|
||||
}
|
||||
async let bankList = api.bankList()
|
||||
async let areaList = api.areas()
|
||||
let result = try await (bankList, areaList)
|
||||
banks = result.0.banks
|
||||
areas = result.1
|
||||
notifyStateChange()
|
||||
}
|
||||
|
||||
func sendCode(api: ProfileAPI) async throws {
|
||||
guard !sendingCode else { return }
|
||||
sendingCode = true
|
||||
notifyStateChange()
|
||||
defer {
|
||||
sendingCode = false
|
||||
notifyStateChange()
|
||||
}
|
||||
try await api.bankCardSmsVerifyCode()
|
||||
}
|
||||
|
||||
func submit(api: ProfileAPI, uploader: any OSSUploadServing, scenicId: Int) async throws {
|
||||
guard canSubmit else {
|
||||
throw WithdrawalValidationError.message("请完善银行卡信息")
|
||||
}
|
||||
submitting = true
|
||||
notifyStateChange()
|
||||
defer {
|
||||
submitting = false
|
||||
notifyStateChange()
|
||||
}
|
||||
try await uploadPendingImages(uploader: uploader, scenicId: scenicId)
|
||||
let request = UpdateBankInfoRequest(
|
||||
realName: holderName.trimmed,
|
||||
cardNumber: bankNo.filter(\.isNumber),
|
||||
bankName: bankName.trimmed,
|
||||
branchName: branchName.trimmed,
|
||||
frontUrl: bankCardFrontURL,
|
||||
backUrl: bankCardBackURL,
|
||||
provinceCode: provinceCode,
|
||||
cityCode: cityCode,
|
||||
smsVerifyCode: verificationCode.trimmed
|
||||
)
|
||||
try await api.updateBankCard(request)
|
||||
}
|
||||
|
||||
var provinceOptions: [AreasResponse] { areas }
|
||||
|
||||
var cityOptions: [AreasResponse] {
|
||||
areas.first { $0.code == provinceCode || $0.name == provinceName }?.children ?? []
|
||||
}
|
||||
|
||||
private func uploadPendingImages(uploader: any OSSUploadServing, scenicId: Int) async throws {
|
||||
if let pendingFrontImageData {
|
||||
bankCardFrontURL = try await uploader.uploadBankCardImage(
|
||||
data: pendingFrontImageData,
|
||||
fileName: pendingFrontFileName ?? "bank_card_front.jpg",
|
||||
scenicId: scenicId
|
||||
) { _ in }
|
||||
self.pendingFrontImageData = nil
|
||||
}
|
||||
if let pendingBackImageData {
|
||||
bankCardBackURL = try await uploader.uploadBankCardImage(
|
||||
data: pendingBackImageData,
|
||||
fileName: pendingBackFileName ?? "bank_card_back.jpg",
|
||||
scenicId: scenicId
|
||||
) { _ in }
|
||||
self.pendingBackImageData = nil
|
||||
}
|
||||
}
|
||||
|
||||
private func notifyStateChange() {
|
||||
onStateChange?()
|
||||
}
|
||||
}
|
||||
|
||||
enum WithdrawalValidationError: LocalizedError, Equatable {
|
||||
case message(String)
|
||||
|
||||
var errorDescription: String? {
|
||||
switch self {
|
||||
case .message(let message): message
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension String {
|
||||
var trimmed: String {
|
||||
trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user