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>
220 lines
6.4 KiB
Swift
220 lines
6.4 KiB
Swift
//
|
||
// 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)
|
||
}
|
||
}
|