Files
suixinkan_uikit/suixinkan/Features/Profile/ViewModels/WithdrawalSettingsViewModel.swift
汉秋 005cac3f78 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>
2026-07-06 16:01:05 +08:00

220 lines
6.4 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.

//
// 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)
}
}