Compare commits
4 Commits
codex/push
...
1.2.1
| Author | SHA1 | Date | |
|---|---|---|---|
| 1cdce616e4 | |||
| 212607fc82 | |||
| 0cf0a41a60 | |||
| f863004c8d |
@ -436,7 +436,7 @@
|
|||||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||||
CODE_SIGN_ENTITLEMENTS = suixinkan/suixinkan.entitlements;
|
CODE_SIGN_ENTITLEMENTS = suixinkan/suixinkan.entitlements;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 1010501;
|
CURRENT_PROJECT_VERSION = 1020101;
|
||||||
DEVELOPMENT_TEAM = 56GVN5RNVN;
|
DEVELOPMENT_TEAM = 56GVN5RNVN;
|
||||||
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||||
"FRAMEWORK_SEARCH_PATHS[sdk=iphoneos*]" = (
|
"FRAMEWORK_SEARCH_PATHS[sdk=iphoneos*]" = (
|
||||||
@ -460,7 +460,7 @@
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 1.1.5;
|
MARKETING_VERSION = 1.2.1;
|
||||||
OTHER_LDFLAGS = (
|
OTHER_LDFLAGS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"-ObjC",
|
"-ObjC",
|
||||||
@ -502,7 +502,7 @@
|
|||||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||||
CODE_SIGN_ENTITLEMENTS = suixinkan/suixinkan.entitlements;
|
CODE_SIGN_ENTITLEMENTS = suixinkan/suixinkan.entitlements;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 1010501;
|
CURRENT_PROJECT_VERSION = 1020101;
|
||||||
DEVELOPMENT_TEAM = 56GVN5RNVN;
|
DEVELOPMENT_TEAM = 56GVN5RNVN;
|
||||||
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||||
"FRAMEWORK_SEARCH_PATHS[sdk=iphoneos*]" = (
|
"FRAMEWORK_SEARCH_PATHS[sdk=iphoneos*]" = (
|
||||||
@ -526,7 +526,7 @@
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 1.1.5;
|
MARKETING_VERSION = 1.2.1;
|
||||||
OTHER_LDFLAGS = (
|
OTHER_LDFLAGS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"-ObjC",
|
"-ObjC",
|
||||||
@ -741,7 +741,7 @@
|
|||||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||||
CODE_SIGN_ENTITLEMENTS = suixinkan/suixinkan.entitlements;
|
CODE_SIGN_ENTITLEMENTS = suixinkan/suixinkan.entitlements;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 1010501;
|
CURRENT_PROJECT_VERSION = 1020101;
|
||||||
DEVELOPMENT_TEAM = 56GVN5RNVN;
|
DEVELOPMENT_TEAM = 56GVN5RNVN;
|
||||||
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
@ -761,7 +761,7 @@
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 1.1.5;
|
MARKETING_VERSION = 1.2.1;
|
||||||
OTHER_LDFLAGS = (
|
OTHER_LDFLAGS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"-ObjC",
|
"-ObjC",
|
||||||
|
|||||||
@ -70,10 +70,10 @@ enum AuthSessionHelper {
|
|||||||
AppStore.shared.session.userId = String(user.businessUserId)
|
AppStore.shared.session.userId = String(user.businessUserId)
|
||||||
AppStore.shared.session.accountType = .scenicUser
|
AppStore.shared.session.accountType = .scenicUser
|
||||||
AppStore.shared.session.accountDisplayName = firstNonEmpty(
|
AppStore.shared.session.accountDisplayName = firstNonEmpty(
|
||||||
user.scenicName, user.nickname, user.realName, user.username
|
user.scenicName, user.nickname, user.realName
|
||||||
)
|
)
|
||||||
AppStore.shared.session.userName = firstNonEmpty(user.nickname, user.username, user.realName)
|
AppStore.shared.session.userName = firstNonEmpty(user.nickname, user.realName)
|
||||||
AppStore.shared.session.realName = firstNonEmpty(user.realName, user.nickname, user.username)
|
AppStore.shared.session.realName = user.realName
|
||||||
AppStore.shared.session.phone = user.phone
|
AppStore.shared.session.phone = user.phone
|
||||||
AppStore.shared.session.currentScenicId = user.scenicId
|
AppStore.shared.session.currentScenicId = user.scenicId
|
||||||
AppStore.shared.session.currentScenicName = user.scenicName
|
AppStore.shared.session.currentScenicName = user.scenicName
|
||||||
@ -90,10 +90,10 @@ enum AuthSessionHelper {
|
|||||||
AppStore.shared.session.userId = String(user.businessUserId)
|
AppStore.shared.session.userId = String(user.businessUserId)
|
||||||
AppStore.shared.session.accountType = .storeUser
|
AppStore.shared.session.accountType = .storeUser
|
||||||
AppStore.shared.session.accountDisplayName = firstNonEmpty(
|
AppStore.shared.session.accountDisplayName = firstNonEmpty(
|
||||||
user.storeName, user.scenicName, user.realName, user.userName, user.username
|
user.storeName, user.scenicName, user.realName
|
||||||
)
|
)
|
||||||
AppStore.shared.session.userName = firstNonEmpty(user.userName, user.username, user.realName)
|
AppStore.shared.session.userName = user.realName
|
||||||
AppStore.shared.session.realName = firstNonEmpty(user.realName, user.userName, user.username)
|
AppStore.shared.session.realName = user.realName
|
||||||
AppStore.shared.session.phone = user.phone
|
AppStore.shared.session.phone = user.phone
|
||||||
AppStore.shared.session.avatar = user.avatar
|
AppStore.shared.session.avatar = user.avatar
|
||||||
AppStore.shared.session.currentScenicId = user.scenicId
|
AppStore.shared.session.currentScenicId = user.scenicId
|
||||||
|
|||||||
@ -196,7 +196,6 @@ struct V9ScenicUser: Decodable, Equatable {
|
|||||||
let userId: Int
|
let userId: Int
|
||||||
let scenicUserId: Int
|
let scenicUserId: Int
|
||||||
let ssUserId: Int
|
let ssUserId: Int
|
||||||
let username: String
|
|
||||||
let realName: String
|
let realName: String
|
||||||
let nickname: String
|
let nickname: String
|
||||||
let phone: String
|
let phone: String
|
||||||
@ -212,7 +211,7 @@ struct V9ScenicUser: Decodable, Equatable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var displayName: String {
|
var displayName: String {
|
||||||
scenicName.nonEmpty ?? nickname.nonEmpty ?? realName.nonEmpty ?? username
|
scenicName.nonEmpty ?? nickname.nonEmpty ?? realName
|
||||||
}
|
}
|
||||||
|
|
||||||
func toAccountSwitchAccount() -> AccountSwitchAccount {
|
func toAccountSwitchAccount() -> AccountSwitchAccount {
|
||||||
@ -238,8 +237,6 @@ struct V9ScenicUser: Decodable, Equatable {
|
|||||||
case userId = "user_id"
|
case userId = "user_id"
|
||||||
case scenicUserId = "scenic_user_id"
|
case scenicUserId = "scenic_user_id"
|
||||||
case ssUserId = "ss_user_id"
|
case ssUserId = "ss_user_id"
|
||||||
case username = "user_name"
|
|
||||||
case legacyUsername = "username"
|
|
||||||
case realName = "real_name"
|
case realName = "real_name"
|
||||||
case nickname
|
case nickname
|
||||||
case phone
|
case phone
|
||||||
@ -258,9 +255,6 @@ struct V9ScenicUser: Decodable, Equatable {
|
|||||||
userId = try container.decodeLossyInt(forKey: .userId) ?? 0
|
userId = try container.decodeLossyInt(forKey: .userId) ?? 0
|
||||||
scenicUserId = try container.decodeLossyInt(forKey: .scenicUserId) ?? 0
|
scenicUserId = try container.decodeLossyInt(forKey: .scenicUserId) ?? 0
|
||||||
ssUserId = try container.decodeLossyInt(forKey: .ssUserId) ?? 0
|
ssUserId = try container.decodeLossyInt(forKey: .ssUserId) ?? 0
|
||||||
let preferredUsername = try container.decodeLossyString(forKey: .username)
|
|
||||||
let legacyUsername = try container.decodeLossyString(forKey: .legacyUsername)
|
|
||||||
username = preferredUsername.nonEmpty ?? legacyUsername
|
|
||||||
realName = try container.decodeLossyString(forKey: .realName)
|
realName = try container.decodeLossyString(forKey: .realName)
|
||||||
nickname = try container.decodeLossyString(forKey: .nickname)
|
nickname = try container.decodeLossyString(forKey: .nickname)
|
||||||
phone = try container.decodeLossyString(forKey: .phone)
|
phone = try container.decodeLossyString(forKey: .phone)
|
||||||
@ -281,8 +275,6 @@ struct V9StoreUser: Decodable, Equatable {
|
|||||||
let id: Int
|
let id: Int
|
||||||
let userId: Int
|
let userId: Int
|
||||||
let storeUserId: Int
|
let storeUserId: Int
|
||||||
let username: String
|
|
||||||
let userName: String
|
|
||||||
let realName: String
|
let realName: String
|
||||||
let phone: String
|
let phone: String
|
||||||
let avatar: String
|
let avatar: String
|
||||||
@ -300,14 +292,14 @@ struct V9StoreUser: Decodable, Equatable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var displayName: String {
|
var displayName: String {
|
||||||
storeName.nonEmpty ?? scenicName.nonEmpty ?? realName.nonEmpty ?? userName.nonEmpty ?? username
|
storeName.nonEmpty ?? scenicName.nonEmpty ?? realName
|
||||||
}
|
}
|
||||||
|
|
||||||
func toAccountSwitchAccount() -> AccountSwitchAccount {
|
func toAccountSwitchAccount() -> AccountSwitchAccount {
|
||||||
AccountSwitchAccount(
|
AccountSwitchAccount(
|
||||||
accountType: accountType.nonEmpty ?? Self.accountTypeValue,
|
accountType: accountType.nonEmpty ?? Self.accountTypeValue,
|
||||||
businessUserId: businessUserId,
|
businessUserId: businessUserId,
|
||||||
title: storeName.nonEmpty ?? scenicName.nonEmpty ?? realName.nonEmpty ?? userName.nonEmpty ?? "门店账号",
|
title: storeName.nonEmpty ?? scenicName.nonEmpty ?? realName.nonEmpty ?? "门店账号",
|
||||||
subtitle: joinAccountSubtitle(scenicName, roleName),
|
subtitle: joinAccountSubtitle(scenicName, roleName),
|
||||||
phone: phone,
|
phone: phone,
|
||||||
realName: realName,
|
realName: realName,
|
||||||
@ -325,8 +317,6 @@ struct V9StoreUser: Decodable, Equatable {
|
|||||||
case id
|
case id
|
||||||
case userId = "user_id"
|
case userId = "user_id"
|
||||||
case storeUserId = "store_user_id"
|
case storeUserId = "store_user_id"
|
||||||
case username
|
|
||||||
case userName = "user_name"
|
|
||||||
case realName = "real_name"
|
case realName = "real_name"
|
||||||
case phone
|
case phone
|
||||||
case avatar
|
case avatar
|
||||||
@ -346,8 +336,6 @@ struct V9StoreUser: Decodable, Equatable {
|
|||||||
id = try container.decodeLossyInt(forKey: .id) ?? 0
|
id = try container.decodeLossyInt(forKey: .id) ?? 0
|
||||||
userId = try container.decodeLossyInt(forKey: .userId) ?? 0
|
userId = try container.decodeLossyInt(forKey: .userId) ?? 0
|
||||||
storeUserId = try container.decodeLossyInt(forKey: .storeUserId) ?? 0
|
storeUserId = try container.decodeLossyInt(forKey: .storeUserId) ?? 0
|
||||||
username = try container.decodeLossyString(forKey: .username)
|
|
||||||
userName = try container.decodeLossyString(forKey: .userName)
|
|
||||||
realName = try container.decodeLossyString(forKey: .realName)
|
realName = try container.decodeLossyString(forKey: .realName)
|
||||||
phone = try container.decodeLossyString(forKey: .phone)
|
phone = try container.decodeLossyString(forKey: .phone)
|
||||||
avatar = try container.decodeLossyString(forKey: .avatar)
|
avatar = try container.decodeLossyString(forKey: .avatar)
|
||||||
|
|||||||
@ -69,9 +69,9 @@ enum PayPageBrandingPolicy {
|
|||||||
|
|
||||||
/// 收款页当前账号信息的展示文本格式化规则。
|
/// 收款页当前账号信息的展示文本格式化规则。
|
||||||
enum PaymentAccountDisplayFormatter {
|
enum PaymentAccountDisplayFormatter {
|
||||||
/// 解析当前登录摄影师名称,昵称为空时使用实名,仍为空时显示占位符。
|
/// 解析当前登录摄影师的真实姓名,空值显示占位符。
|
||||||
static func photographerName(userName: String, realName: String) -> String {
|
static func photographerName(realName: String) -> String {
|
||||||
userName.trimmedNonEmpty ?? realName.trimmedNonEmpty ?? "-"
|
realName.trimmedNonEmpty ?? "-"
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 去除空值与重复项后,合并当前账号的全部店铺名称。
|
/// 去除空值与重复项后,合并当前账号的全部店铺名称。
|
||||||
|
|||||||
@ -28,6 +28,7 @@ final class PaymentCollectionDetailsViewModel {
|
|||||||
|
|
||||||
private var staticPayURL = ""
|
private var staticPayURL = ""
|
||||||
private var dynamicPayURL = ""
|
private var dynamicPayURL = ""
|
||||||
|
private var hasCompletedPayCodeRequest = false
|
||||||
private let appStore: AppStore
|
private let appStore: AppStore
|
||||||
private let payPageConfigCache: PayPageConfigCaching
|
private let payPageConfigCache: PayPageConfigCaching
|
||||||
|
|
||||||
@ -50,6 +51,11 @@ final class PaymentCollectionDetailsViewModel {
|
|||||||
qrImage != nil && !staticPayURL.isEmpty
|
qrImage != nil && !staticPayURL.isEmpty
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 是否应在二维码区域展示加载状态,首次请求开始前也视为加载中,避免短暂闪现错误占位。
|
||||||
|
var isPayCodeLoading: Bool {
|
||||||
|
loading || !hasCompletedPayCodeRequest
|
||||||
|
}
|
||||||
|
|
||||||
var displayScenicName: String {
|
var displayScenicName: String {
|
||||||
scenicName.isEmpty ? "暂无景区" : scenicName
|
scenicName.isEmpty ? "暂无景区" : scenicName
|
||||||
}
|
}
|
||||||
@ -60,7 +66,6 @@ final class PaymentCollectionDetailsViewModel {
|
|||||||
|
|
||||||
var displayPhotographerName: String {
|
var displayPhotographerName: String {
|
||||||
PaymentAccountDisplayFormatter.photographerName(
|
PaymentAccountDisplayFormatter.photographerName(
|
||||||
userName: appStore.session.userName,
|
|
||||||
realName: appStore.session.realName
|
realName: appStore.session.realName
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -85,11 +90,11 @@ final class PaymentCollectionDetailsViewModel {
|
|||||||
|
|
||||||
func loadPayCode(api: PaymentAPI) async {
|
func loadPayCode(api: PaymentAPI) async {
|
||||||
guard !loading else { return }
|
guard !loading else { return }
|
||||||
refreshLocalState()
|
|
||||||
loading = true
|
loading = true
|
||||||
notifyStateChange()
|
refreshLocalState()
|
||||||
defer {
|
defer {
|
||||||
loading = false
|
loading = false
|
||||||
|
hasCompletedPayCodeRequest = true
|
||||||
notifyStateChange()
|
notifyStateChange()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -17,11 +17,11 @@ final class ProfileViewModel {
|
|||||||
var onStateChange: (() -> Void)?
|
var onStateChange: (() -> Void)?
|
||||||
|
|
||||||
var displayNickname: String {
|
var displayNickname: String {
|
||||||
nonEmpty(userInfo?.nickname) ?? "未设置昵称"
|
nonEmpty(userInfo?.nickname) ?? AppStore.shared.session.userName.nonEmpty ?? "未设置昵称"
|
||||||
}
|
}
|
||||||
|
|
||||||
var displayRealName: String {
|
var displayRealName: String {
|
||||||
nonEmpty(userInfo?.realName) ?? "--"
|
nonEmpty(userInfo?.realName) ?? AppStore.shared.session.realName.nonEmpty ?? "--"
|
||||||
}
|
}
|
||||||
|
|
||||||
var displayPhone: String {
|
var displayPhone: String {
|
||||||
@ -37,6 +37,17 @@ final class ProfileViewModel {
|
|||||||
return uid.isEmpty ? "--" : uid
|
return uid.isEmpty ? "--" : uid
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 本地是否已有可供冷启动首屏展示的基本资料。
|
||||||
|
var hasCachedBasicInfo: Bool {
|
||||||
|
[
|
||||||
|
AppStore.shared.session.userName,
|
||||||
|
AppStore.shared.session.realName,
|
||||||
|
AppStore.shared.session.phone,
|
||||||
|
AppStore.shared.session.avatar,
|
||||||
|
AppStore.shared.session.userId,
|
||||||
|
].contains { nonEmpty($0) != nil }
|
||||||
|
}
|
||||||
|
|
||||||
var accountDisplayName: String {
|
var accountDisplayName: String {
|
||||||
let name = AppStore.shared.session.accountDisplayName.trimmingCharacters(in: .whitespacesAndNewlines)
|
let name = AppStore.shared.session.accountDisplayName.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
return name.isEmpty ? "--" : name
|
return name.isEmpty ? "--" : name
|
||||||
@ -112,6 +123,7 @@ final class ProfileViewModel {
|
|||||||
if !info.roleName.isEmpty {
|
if !info.roleName.isEmpty {
|
||||||
AppStore.shared.session.roleName = info.roleName
|
AppStore.shared.session.roleName = info.roleName
|
||||||
}
|
}
|
||||||
|
notifyStateChange()
|
||||||
|
|
||||||
if showPhotographerFields {
|
if showPhotographerFields {
|
||||||
async let realName = api.realNameInfo()
|
async let realName = api.realNameInfo()
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import UIKit
|
|||||||
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
|
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
|
||||||
|
|
||||||
var window: UIWindow?
|
var window: UIWindow?
|
||||||
|
private var sessionExpiredDialog: SessionExpiredDialogViewController?
|
||||||
|
|
||||||
func scene(
|
func scene(
|
||||||
_ scene: UIScene,
|
_ scene: UIScene,
|
||||||
@ -86,18 +87,36 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@objc private func handleSessionDidExpire() {
|
@objc private func handleSessionDidExpire() {
|
||||||
|
guard AppStore.shared.session.isLoggedIn else { return }
|
||||||
|
guard sessionExpiredDialog == nil else { return }
|
||||||
|
|
||||||
|
GlobalLoadingManager.shared.hideAll()
|
||||||
|
let dialog = SessionExpiredDialogViewController { [weak self] in
|
||||||
|
self?.transitionToLogin()
|
||||||
|
}
|
||||||
|
sessionExpiredDialog = dialog
|
||||||
|
|
||||||
|
guard let presenter = topViewController(from: window?.rootViewController) else {
|
||||||
|
sessionExpiredDialog = nil
|
||||||
|
return
|
||||||
|
}
|
||||||
|
presenter.present(dialog, animated: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func transitionToLogin() {
|
||||||
|
sessionExpiredDialog = nil
|
||||||
PushNotificationManager.shared.handleLogout()
|
PushNotificationManager.shared.handleLogout()
|
||||||
AppStore.shared.logout()
|
AppStore.shared.logout()
|
||||||
AppRouter.setRoot(.login, on: window)
|
AppRouter.setRoot(.login, on: window)
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc private func handleUserDidLogout() {
|
@objc private func handleUserDidLogout() {
|
||||||
PushNotificationManager.shared.handleLogout()
|
transitionToLogin()
|
||||||
AppStore.shared.logout()
|
|
||||||
AppRouter.setRoot(.login, on: window)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc private func handleUserDidLogin() {
|
@objc private func handleUserDidLogin() {
|
||||||
|
sessionExpiredDialog?.dismiss(animated: false)
|
||||||
|
sessionExpiredDialog = nil
|
||||||
AppRouter.setRoot(.mainTab, on: window)
|
AppRouter.setRoot(.mainTab, on: window)
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
PushNotificationManager.shared.handleLoginCompleted()
|
PushNotificationManager.shared.handleLoginCompleted()
|
||||||
@ -108,4 +127,18 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
|
|||||||
@objc private func handleAccountDidSwitch() {
|
@objc private func handleAccountDidSwitch() {
|
||||||
PushNotificationManager.shared.handleAccountSwitched()
|
PushNotificationManager.shared.handleAccountSwitched()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func topViewController(from viewController: UIViewController?) -> UIViewController? {
|
||||||
|
guard let viewController else { return nil }
|
||||||
|
if let presented = viewController.presentedViewController {
|
||||||
|
return topViewController(from: presented)
|
||||||
|
}
|
||||||
|
if let navigationController = viewController as? UINavigationController {
|
||||||
|
return topViewController(from: navigationController.visibleViewController)
|
||||||
|
}
|
||||||
|
if let tabBarController = viewController as? UITabBarController {
|
||||||
|
return topViewController(from: tabBarController.selectedViewController)
|
||||||
|
}
|
||||||
|
return viewController
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
103
suixinkan/UI/Common/SessionExpiredDialogViewController.swift
Normal file
103
suixinkan/UI/Common/SessionExpiredDialogViewController.swift
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
//
|
||||||
|
// SessionExpiredDialogViewController.swift
|
||||||
|
// suixinkan
|
||||||
|
//
|
||||||
|
|
||||||
|
import SnapKit
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
/// 登录凭证失效时展示的强制确认弹窗,用户确认后由调用方执行退出登录。
|
||||||
|
@MainActor
|
||||||
|
final class SessionExpiredDialogViewController: UIViewController {
|
||||||
|
|
||||||
|
private let onConfirm: () -> Void
|
||||||
|
private let confirmButton = AppButton(title: "确认并退出登录")
|
||||||
|
private var didConfirm = false
|
||||||
|
|
||||||
|
/// 创建不可通过点击遮罩关闭的登录失效弹窗。
|
||||||
|
init(onConfirm: @escaping () -> Void) {
|
||||||
|
self.onConfirm = onConfirm
|
||||||
|
super.init(nibName: nil, bundle: nil)
|
||||||
|
modalPresentationStyle = .overFullScreen
|
||||||
|
modalTransitionStyle = .crossDissolve
|
||||||
|
isModalInPresentation = true
|
||||||
|
}
|
||||||
|
|
||||||
|
@available(*, unavailable)
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
override func viewDidLoad() {
|
||||||
|
super.viewDidLoad()
|
||||||
|
setupUI()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func setupUI() {
|
||||||
|
view.backgroundColor = UIColor.black.withAlphaComponent(0.32)
|
||||||
|
view.accessibilityViewIsModal = true
|
||||||
|
view.accessibilityIdentifier = "sessionExpired.dialog"
|
||||||
|
|
||||||
|
let cardView = UIView()
|
||||||
|
cardView.backgroundColor = AppColor.cardBackground
|
||||||
|
cardView.layer.cornerRadius = AppRadius.lg
|
||||||
|
cardView.clipsToBounds = true
|
||||||
|
|
||||||
|
let warningImageView = UIImageView(
|
||||||
|
image: UIImage(systemName: "exclamationmark.circle.fill")
|
||||||
|
)
|
||||||
|
warningImageView.tintColor = UIColor(hex: 0xF05A5A)
|
||||||
|
warningImageView.contentMode = .scaleAspectFit
|
||||||
|
warningImageView.isAccessibilityElement = true
|
||||||
|
warningImageView.accessibilityLabel = "登录状态异常"
|
||||||
|
|
||||||
|
let titleLabel = UILabel()
|
||||||
|
titleLabel.text = "登录提醒"
|
||||||
|
titleLabel.textColor = AppColor.textPrimary
|
||||||
|
titleLabel.font = .systemFont(ofSize: 20, weight: .semibold)
|
||||||
|
titleLabel.textAlignment = .center
|
||||||
|
|
||||||
|
let messageLabel = UILabel()
|
||||||
|
messageLabel.text = "当前用户已在其他设备登录,\n请重新登录"
|
||||||
|
messageLabel.textColor = UIColor(hex: 0x4B5563)
|
||||||
|
messageLabel.font = .systemFont(ofSize: 14)
|
||||||
|
messageLabel.textAlignment = .center
|
||||||
|
messageLabel.numberOfLines = 0
|
||||||
|
|
||||||
|
confirmButton.accessibilityIdentifier = "sessionExpired.confirmButton"
|
||||||
|
confirmButton.addTarget(self, action: #selector(confirmTapped), for: .touchUpInside)
|
||||||
|
|
||||||
|
let contentStack = UIStackView(arrangedSubviews: [
|
||||||
|
warningImageView,
|
||||||
|
titleLabel,
|
||||||
|
messageLabel,
|
||||||
|
confirmButton,
|
||||||
|
])
|
||||||
|
contentStack.axis = .vertical
|
||||||
|
contentStack.alignment = .fill
|
||||||
|
contentStack.spacing = 16
|
||||||
|
contentStack.setCustomSpacing(4, after: titleLabel)
|
||||||
|
contentStack.setCustomSpacing(20, after: messageLabel)
|
||||||
|
|
||||||
|
view.addSubview(cardView)
|
||||||
|
cardView.addSubview(contentStack)
|
||||||
|
|
||||||
|
cardView.snp.makeConstraints { make in
|
||||||
|
make.center.equalToSuperview()
|
||||||
|
make.leading.trailing.equalToSuperview().inset(32)
|
||||||
|
}
|
||||||
|
contentStack.snp.makeConstraints { make in
|
||||||
|
make.edges.equalToSuperview().inset(24)
|
||||||
|
}
|
||||||
|
warningImageView.snp.makeConstraints { make in
|
||||||
|
make.height.equalTo(64)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func confirmTapped() {
|
||||||
|
guard !didConfirm else { return }
|
||||||
|
didConfirm = true
|
||||||
|
confirmButton.isEnabled = false
|
||||||
|
onConfirm()
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -28,6 +28,7 @@ final class PaymentCollectionDetailsViewController: BaseViewController {
|
|||||||
private let qrContainerView = UIView()
|
private let qrContainerView = UIView()
|
||||||
private let qrImageView = UIImageView()
|
private let qrImageView = UIImageView()
|
||||||
private let qrPlaceholderView = UIView()
|
private let qrPlaceholderView = UIView()
|
||||||
|
private let qrLoadingIndicator = UIActivityIndicatorView(style: .medium)
|
||||||
private let qrErrorLabel = UILabel()
|
private let qrErrorLabel = UILabel()
|
||||||
private let refreshButton = UIButton(type: .system)
|
private let refreshButton = UIButton(type: .system)
|
||||||
private let setAmountButton = UIButton(type: .system)
|
private let setAmountButton = UIButton(type: .system)
|
||||||
@ -127,6 +128,10 @@ final class PaymentCollectionDetailsViewController: BaseViewController {
|
|||||||
qrPlaceholderView.layer.cornerRadius = usesBranding ? 10 : 24
|
qrPlaceholderView.layer.cornerRadius = usesBranding ? 10 : 24
|
||||||
qrPlaceholderView.isHidden = true
|
qrPlaceholderView.isHidden = true
|
||||||
|
|
||||||
|
qrLoadingIndicator.color = AppColor.primary
|
||||||
|
qrLoadingIndicator.hidesWhenStopped = true
|
||||||
|
qrLoadingIndicator.accessibilityLabel = "收款码加载中"
|
||||||
|
|
||||||
qrErrorLabel.text = "未选景区或网络问题"
|
qrErrorLabel.text = "未选景区或网络问题"
|
||||||
qrErrorLabel.font = .systemFont(ofSize: 14)
|
qrErrorLabel.font = .systemFont(ofSize: 14)
|
||||||
qrErrorLabel.textColor = AppColor.danger
|
qrErrorLabel.textColor = AppColor.danger
|
||||||
@ -150,6 +155,9 @@ final class PaymentCollectionDetailsViewController: BaseViewController {
|
|||||||
trailing: 24
|
trailing: 24
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
actionRow.alpha = 0
|
||||||
|
actionRow.isUserInteractionEnabled = false
|
||||||
|
actionRow.accessibilityElementsHidden = true
|
||||||
|
|
||||||
configureLinkButton(setAmountButton, title: "设置金额")
|
configureLinkButton(setAmountButton, title: "设置金额")
|
||||||
configureLinkButton(saveQRButton, title: "保存二维码")
|
configureLinkButton(saveQRButton, title: "保存二维码")
|
||||||
@ -206,6 +214,7 @@ final class PaymentCollectionDetailsViewController: BaseViewController {
|
|||||||
qrContentStack.spacing = usesBranding ? brandSectionSpacing : AppSpacing.lg
|
qrContentStack.spacing = usesBranding ? brandSectionSpacing : AppSpacing.lg
|
||||||
qrCardView.addSubview(qrContentStack)
|
qrCardView.addSubview(qrContentStack)
|
||||||
|
|
||||||
|
qrPlaceholderView.addSubview(qrLoadingIndicator)
|
||||||
qrPlaceholderView.addSubview(qrErrorLabel)
|
qrPlaceholderView.addSubview(qrErrorLabel)
|
||||||
qrPlaceholderView.addSubview(refreshButton)
|
qrPlaceholderView.addSubview(refreshButton)
|
||||||
|
|
||||||
@ -275,6 +284,9 @@ final class PaymentCollectionDetailsViewController: BaseViewController {
|
|||||||
qrPlaceholderView.snp.makeConstraints { make in
|
qrPlaceholderView.snp.makeConstraints { make in
|
||||||
make.width.height.equalTo(usesBranding ? brandQRContainerSize : 182)
|
make.width.height.equalTo(usesBranding ? brandQRContainerSize : 182)
|
||||||
}
|
}
|
||||||
|
qrLoadingIndicator.snp.makeConstraints { make in
|
||||||
|
make.center.equalToSuperview()
|
||||||
|
}
|
||||||
qrErrorLabel.snp.makeConstraints { make in
|
qrErrorLabel.snp.makeConstraints { make in
|
||||||
make.top.equalToSuperview().offset(56)
|
make.top.equalToSuperview().offset(56)
|
||||||
make.leading.trailing.equalToSuperview().inset(AppSpacing.sm)
|
make.leading.trailing.equalToSuperview().inset(AppSpacing.sm)
|
||||||
@ -370,9 +382,7 @@ final class PaymentCollectionDetailsViewController: BaseViewController {
|
|||||||
|
|
||||||
private func loadData() async {
|
private func loadData() async {
|
||||||
async let pageConfig: Void = viewModel.loadPayPageConfig(api: paymentAPI)
|
async let pageConfig: Void = viewModel.loadPayPageConfig(api: paymentAPI)
|
||||||
showLoading()
|
|
||||||
await viewModel.loadPayCode(api: paymentAPI)
|
await viewModel.loadPayCode(api: paymentAPI)
|
||||||
hideLoading()
|
|
||||||
await pageConfig
|
await pageConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -387,17 +397,33 @@ final class PaymentCollectionDetailsViewController: BaseViewController {
|
|||||||
voiceSwitch.isOn = viewModel.isVoiceBroadcastOpen
|
voiceSwitch.isOn = viewModel.isVoiceBroadcastOpen
|
||||||
applyBrandConfigIfNeeded()
|
applyBrandConfigIfNeeded()
|
||||||
|
|
||||||
if let image = viewModel.qrImage {
|
if viewModel.isPayCodeLoading {
|
||||||
|
qrImageView.isHidden = true
|
||||||
|
qrContainerView.isHidden = true
|
||||||
|
qrPlaceholderView.backgroundColor = viewModel.usesNalatiBranding
|
||||||
|
? .white
|
||||||
|
: AppColor.inputBackground
|
||||||
|
qrPlaceholderView.isHidden = false
|
||||||
|
qrErrorLabel.isHidden = true
|
||||||
|
refreshButton.isHidden = true
|
||||||
|
qrLoadingIndicator.startAnimating()
|
||||||
|
setActionRowAvailable(false)
|
||||||
|
} else if let image = viewModel.qrImage {
|
||||||
qrImageView.image = image
|
qrImageView.image = image
|
||||||
qrImageView.isHidden = false
|
qrImageView.isHidden = false
|
||||||
qrContainerView.isHidden = !viewModel.usesNalatiBranding
|
qrContainerView.isHidden = !viewModel.usesNalatiBranding
|
||||||
qrPlaceholderView.isHidden = true
|
qrPlaceholderView.isHidden = true
|
||||||
actionRow.isHidden = false
|
qrLoadingIndicator.stopAnimating()
|
||||||
|
setActionRowAvailable(true)
|
||||||
} else {
|
} else {
|
||||||
qrImageView.isHidden = true
|
qrImageView.isHidden = true
|
||||||
qrContainerView.isHidden = true
|
qrContainerView.isHidden = true
|
||||||
|
qrPlaceholderView.backgroundColor = AppColor.inputBackground
|
||||||
qrPlaceholderView.isHidden = false
|
qrPlaceholderView.isHidden = false
|
||||||
actionRow.isHidden = true
|
qrErrorLabel.isHidden = false
|
||||||
|
refreshButton.isHidden = false
|
||||||
|
qrLoadingIndicator.stopAnimating()
|
||||||
|
setActionRowAvailable(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
if viewModel.showAmountDialog {
|
if viewModel.showAmountDialog {
|
||||||
@ -408,6 +434,12 @@ final class PaymentCollectionDetailsViewController: BaseViewController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func setActionRowAvailable(_ isAvailable: Bool) {
|
||||||
|
actionRow.alpha = isAvailable ? 1 : 0
|
||||||
|
actionRow.isUserInteractionEnabled = isAvailable
|
||||||
|
actionRow.accessibilityElementsHidden = !isAvailable
|
||||||
|
}
|
||||||
|
|
||||||
private func presentAmountDialogIfNeeded() {
|
private func presentAmountDialogIfNeeded() {
|
||||||
guard amountDialog == nil else {
|
guard amountDialog == nil else {
|
||||||
amountDialog?.apply(amount: viewModel.amount, remark: viewModel.remark)
|
amountDialog?.apply(amount: viewModel.amount, remark: viewModel.remark)
|
||||||
@ -625,9 +657,7 @@ final class PaymentCollectionDetailsViewController: BaseViewController {
|
|||||||
|
|
||||||
@objc private func refreshTapped() {
|
@objc private func refreshTapped() {
|
||||||
Task {
|
Task {
|
||||||
showLoading()
|
|
||||||
await viewModel.refresh(api: paymentAPI)
|
await viewModel.refresh(api: paymentAPI)
|
||||||
hideLoading()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -38,6 +38,11 @@ final class ProfileViewController: BaseViewController {
|
|||||||
title = "我的"
|
title = "我的"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override func viewDidLoad() {
|
||||||
|
super.viewDidLoad()
|
||||||
|
applyViewModel()
|
||||||
|
}
|
||||||
|
|
||||||
override func setupUI() {
|
override func setupUI() {
|
||||||
view.backgroundColor = UIColor(hex: 0xF7FAFF)
|
view.backgroundColor = UIColor(hex: 0xF7FAFF)
|
||||||
|
|
||||||
@ -104,12 +109,10 @@ final class ProfileViewController: BaseViewController {
|
|||||||
|
|
||||||
override func viewWillAppear(_ animated: Bool) {
|
override func viewWillAppear(_ animated: Bool) {
|
||||||
super.viewWillAppear(animated)
|
super.viewWillAppear(animated)
|
||||||
let showLoading = !hasLoadedProfileOnce
|
let showLoading = !hasLoadedProfileOnce && !viewModel.hasCachedBasicInfo
|
||||||
Task {
|
Task {
|
||||||
await reloadProfile(showGlobalLoading: showLoading)
|
await reloadProfile(showGlobalLoading: showLoading)
|
||||||
if showLoading {
|
hasLoadedProfileOnce = true
|
||||||
hasLoadedProfileOnce = true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -62,6 +62,7 @@ final class AuthModelsTests: XCTestCase {
|
|||||||
XCTAssertEqual(response.token, "person-temp-token")
|
XCTAssertEqual(response.token, "person-temp-token")
|
||||||
XCTAssertEqual(response.accounts.map(\.businessUserId), [101, 201])
|
XCTAssertEqual(response.accounts.map(\.businessUserId), [101, 201])
|
||||||
XCTAssertEqual(response.accounts.map(\.subtitle), ["示例景区 · 摄影师", "示例景区 · 店长"])
|
XCTAssertEqual(response.accounts.map(\.subtitle), ["示例景区 · 摄影师", "示例景区 · 店长"])
|
||||||
|
XCTAssertEqual(response.accounts.map(\.realName), ["张三", "李四"])
|
||||||
}
|
}
|
||||||
|
|
||||||
func testV9AuthResponseDecodesMissingAccountListsAsEmpty() throws {
|
func testV9AuthResponseDecodesMissingAccountListsAsEmpty() throws {
|
||||||
@ -137,4 +138,39 @@ final class AuthModelsTests: XCTestCase {
|
|||||||
XCTAssertEqual(user.toAccountSwitchAccount().realName, "张三")
|
XCTAssertEqual(user.toAccountSwitchAccount().realName, "张三")
|
||||||
XCTAssertEqual(user.toAccountSwitchAccount().toSetUserRequest(), SetUserRequest(storeUserId: 201))
|
XCTAssertEqual(user.toAccountSwitchAccount().toSetUserRequest(), SetUserRequest(storeUserId: 201))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func testV9AccountModelsIgnoreBackendIdentifiersAndUseRealNameFallback() throws {
|
||||||
|
let json = """
|
||||||
|
{
|
||||||
|
"token": "person-temp-token",
|
||||||
|
"scenic_users": [
|
||||||
|
{
|
||||||
|
"account_type": "scenic_user",
|
||||||
|
"ss_user_id": 101,
|
||||||
|
"username": "backend_scenic_account",
|
||||||
|
"user_name": "backend_scenic_unique_id",
|
||||||
|
"real_name": "景区真实姓名"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"store_users": [
|
||||||
|
{
|
||||||
|
"account_type": "store_user",
|
||||||
|
"store_user_id": 201,
|
||||||
|
"username": "backend_store_account",
|
||||||
|
"user_name": "backend_store_unique_id",
|
||||||
|
"real_name": "门店真实姓名"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
""".data(using: .utf8)!
|
||||||
|
|
||||||
|
let response = try JSONDecoder().decode(V9AuthResponse.self, from: json)
|
||||||
|
|
||||||
|
XCTAssertEqual(response.scenicUsers.first?.realName, "景区真实姓名")
|
||||||
|
XCTAssertEqual(response.scenicUsers.first?.displayName, "景区真实姓名")
|
||||||
|
XCTAssertEqual(response.scenicUsers.first?.toAccountSwitchAccount().title, "景区真实姓名")
|
||||||
|
XCTAssertEqual(response.storeUsers.first?.realName, "门店真实姓名")
|
||||||
|
XCTAssertEqual(response.storeUsers.first?.displayName, "门店真实姓名")
|
||||||
|
XCTAssertEqual(response.storeUsers.first?.toAccountSwitchAccount().title, "门店真实姓名")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -69,6 +69,26 @@ final class PaymentCollectionDetailsViewModelTests: XCTestCase {
|
|||||||
XCTAssertFalse(viewModel.showAmountDialog)
|
XCTAssertFalse(viewModel.showAmountDialog)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func testPayCodeLoadingStateCoversInitialRenderAndRequest() async {
|
||||||
|
let payCodeJSON = """
|
||||||
|
{"code":100000,"msg":"success","data":{"static_pay_url":"https://pay.example.com/static","dynamic_pay_url":"https://pay.example.com/dynamic"}}
|
||||||
|
""".data(using: .utf8)!
|
||||||
|
let session = MockURLSession(responses: [payCodeJSON])
|
||||||
|
let api = PaymentAPI(client: APIClient(environment: .testing, session: session))
|
||||||
|
let viewModel = PaymentCollectionDetailsViewModel(appStore: appStore)
|
||||||
|
var loadingStates = [viewModel.isPayCodeLoading]
|
||||||
|
viewModel.onStateChange = {
|
||||||
|
loadingStates.append(viewModel.isPayCodeLoading)
|
||||||
|
}
|
||||||
|
|
||||||
|
await viewModel.loadPayCode(api: api)
|
||||||
|
|
||||||
|
XCTAssertEqual(loadingStates.first, true)
|
||||||
|
XCTAssertTrue(loadingStates.dropLast().allSatisfy { $0 })
|
||||||
|
XCTAssertEqual(loadingStates.last, false)
|
||||||
|
XCTAssertTrue(viewModel.hasPayCode)
|
||||||
|
}
|
||||||
|
|
||||||
func testToggleReceiveVoicePersistsInAppStore() {
|
func testToggleReceiveVoicePersistsInAppStore() {
|
||||||
let viewModel = PaymentCollectionDetailsViewModel(appStore: appStore)
|
let viewModel = PaymentCollectionDetailsViewModel(appStore: appStore)
|
||||||
viewModel.refreshLocalState()
|
viewModel.refreshLocalState()
|
||||||
@ -79,8 +99,9 @@ final class PaymentCollectionDetailsViewModelTests: XCTestCase {
|
|||||||
XCTAssertTrue(viewModel.isVoiceBroadcastOpen)
|
XCTAssertTrue(viewModel.isVoiceBroadcastOpen)
|
||||||
}
|
}
|
||||||
|
|
||||||
func testNalatiAccountInfoUsesLoggedInPhotographerAndAllPermissionStores() {
|
func testNalatiAccountInfoUsesPhotographerRealNameAndAllPermissionStores() {
|
||||||
appStore.session.userName = "摄影师小王"
|
appStore.session.userName = "摄影师昵称"
|
||||||
|
appStore.session.realName = "摄影师真实姓名"
|
||||||
appStore.permissions.saveRolePermissionList([
|
appStore.permissions.saveRolePermissionList([
|
||||||
RolePermissionResponse(
|
RolePermissionResponse(
|
||||||
role: RoleInfo(id: 41, name: "摄影师", roleCode: "photographer"),
|
role: RoleInfo(id: 41, name: "摄影师", roleCode: "photographer"),
|
||||||
@ -93,10 +114,19 @@ final class PaymentCollectionDetailsViewModelTests: XCTestCase {
|
|||||||
|
|
||||||
let viewModel = PaymentCollectionDetailsViewModel(appStore: appStore)
|
let viewModel = PaymentCollectionDetailsViewModel(appStore: appStore)
|
||||||
|
|
||||||
XCTAssertEqual(viewModel.displayPhotographerName, "摄影师小王")
|
XCTAssertEqual(viewModel.displayPhotographerName, "摄影师真实姓名")
|
||||||
XCTAssertEqual(viewModel.displayStoreNames, "空中草原店、河谷草原店")
|
XCTAssertEqual(viewModel.displayStoreNames, "空中草原店、河谷草原店")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func testPhotographerNameDoesNotFallBackToNicknameWhenRealNameMissing() {
|
||||||
|
appStore.session.userName = "摄影师昵称"
|
||||||
|
appStore.session.realName = " "
|
||||||
|
|
||||||
|
let viewModel = PaymentCollectionDetailsViewModel(appStore: appStore)
|
||||||
|
|
||||||
|
XCTAssertEqual(viewModel.displayPhotographerName, "-")
|
||||||
|
}
|
||||||
|
|
||||||
func testLoadPayCodeClearsQRWhenScenicMissing() async {
|
func testLoadPayCodeClearsQRWhenScenicMissing() async {
|
||||||
appStore.session.currentScenicId = 0
|
appStore.session.currentScenicId = 0
|
||||||
let session = MockURLSession(responses: [])
|
let session = MockURLSession(responses: [])
|
||||||
@ -105,6 +135,7 @@ final class PaymentCollectionDetailsViewModelTests: XCTestCase {
|
|||||||
|
|
||||||
await viewModel.loadPayCode(api: api)
|
await viewModel.loadPayCode(api: api)
|
||||||
|
|
||||||
|
XCTAssertFalse(viewModel.isPayCodeLoading)
|
||||||
XCTAssertFalse(viewModel.hasPayCode)
|
XCTAssertFalse(viewModel.hasPayCode)
|
||||||
XCTAssertEqual(session.requests.count, 0)
|
XCTAssertEqual(session.requests.count, 0)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -55,11 +55,12 @@ final class PaymentModelsTests: XCTestCase {
|
|||||||
XCTAssertFalse(PayPageBrandingPolicy.isNalati(scenicId: 0))
|
XCTAssertFalse(PayPageBrandingPolicy.isNalati(scenicId: 0))
|
||||||
}
|
}
|
||||||
|
|
||||||
func testPaymentAccountDisplayFormatterUsesCurrentPhotographerAndAllUniqueStores() {
|
func testPaymentAccountDisplayFormatterUsesRealNameAndAllUniqueStores() {
|
||||||
XCTAssertEqual(
|
XCTAssertEqual(
|
||||||
PaymentAccountDisplayFormatter.photographerName(userName: " 当前摄影师 ", realName: "实名"),
|
PaymentAccountDisplayFormatter.photographerName(realName: " 摄影师真实姓名 "),
|
||||||
"当前摄影师"
|
"摄影师真实姓名"
|
||||||
)
|
)
|
||||||
|
XCTAssertEqual(PaymentAccountDisplayFormatter.photographerName(realName: " "), "-")
|
||||||
XCTAssertEqual(
|
XCTAssertEqual(
|
||||||
PaymentAccountDisplayFormatter.storeNames(["一号店", " 二号店 ", "一号店", ""]),
|
PaymentAccountDisplayFormatter.storeNames(["一号店", " 二号店 ", "一号店", ""]),
|
||||||
"一号店、二号店"
|
"一号店、二号店"
|
||||||
|
|||||||
@ -26,6 +26,22 @@ final class ProfileViewModelTests: XCTestCase {
|
|||||||
XCTAssertEqual(viewModel.displayNickname, "未设置昵称")
|
XCTAssertEqual(viewModel.displayNickname, "未设置昵称")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func testColdStartBasicInfoUsesPersistedSessionCache() {
|
||||||
|
AppStore.shared.session.userName = "缓存昵称"
|
||||||
|
AppStore.shared.session.realName = "缓存真实姓名"
|
||||||
|
AppStore.shared.session.phone = "186****7230"
|
||||||
|
AppStore.shared.session.avatar = "https://cdn.example.com/cached-avatar.jpg"
|
||||||
|
|
||||||
|
let viewModel = ProfileViewModel()
|
||||||
|
|
||||||
|
XCTAssertTrue(viewModel.hasCachedBasicInfo)
|
||||||
|
XCTAssertEqual(viewModel.displayNickname, "缓存昵称")
|
||||||
|
XCTAssertEqual(viewModel.displayRealName, "缓存真实姓名")
|
||||||
|
XCTAssertEqual(viewModel.displayPhone, "186****7230")
|
||||||
|
XCTAssertEqual(viewModel.displayAvatarURL, "https://cdn.example.com/cached-avatar.jpg")
|
||||||
|
XCTAssertEqual(viewModel.displayUID, "profile-user")
|
||||||
|
}
|
||||||
|
|
||||||
func testShowPhotographerFieldsWhenRoleCodeIsPhotographer() {
|
func testShowPhotographerFieldsWhenRoleCodeIsPhotographer() {
|
||||||
AppStore.shared.session.roleCode = AppRoleCode.photographer.rawValue
|
AppStore.shared.session.roleCode = AppRoleCode.photographer.rawValue
|
||||||
let viewModel = ProfileViewModel()
|
let viewModel = ProfileViewModel()
|
||||||
|
|||||||
62
suixinkanTests/SessionExpiredDialogViewControllerTests.swift
Normal file
62
suixinkanTests/SessionExpiredDialogViewControllerTests.swift
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
//
|
||||||
|
// SessionExpiredDialogViewControllerTests.swift
|
||||||
|
// suixinkanTests
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import XCTest
|
||||||
|
@testable import suixinkan
|
||||||
|
|
||||||
|
/// 登录失效弹窗测试,覆盖强制展示配置、Android 对齐文案与确认防重。
|
||||||
|
@MainActor
|
||||||
|
final class SessionExpiredDialogViewControllerTests: XCTestCase {
|
||||||
|
|
||||||
|
func testDialogUsesForcedPresentationAndAndroidCopy() throws {
|
||||||
|
let dialog = SessionExpiredDialogViewController(onConfirm: {})
|
||||||
|
dialog.loadViewIfNeeded()
|
||||||
|
|
||||||
|
XCTAssertEqual(dialog.modalPresentationStyle, .overFullScreen)
|
||||||
|
XCTAssertEqual(dialog.modalTransitionStyle, .crossDissolve)
|
||||||
|
XCTAssertTrue(dialog.isModalInPresentation)
|
||||||
|
XCTAssertTrue(dialog.view.accessibilityViewIsModal)
|
||||||
|
|
||||||
|
let labels = dialog.view.allSubviews(of: UILabel.self)
|
||||||
|
XCTAssertTrue(labels.contains { $0.text == "登录提醒" })
|
||||||
|
XCTAssertTrue(labels.contains { $0.text == "当前用户已在其他设备登录,\n请重新登录" })
|
||||||
|
|
||||||
|
let button = try XCTUnwrap(
|
||||||
|
dialog.view.allSubviews(of: UIButton.self).first {
|
||||||
|
$0.accessibilityIdentifier == "sessionExpired.confirmButton"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
XCTAssertEqual(button.title(for: .normal), "确认并退出登录")
|
||||||
|
}
|
||||||
|
|
||||||
|
func testConfirmCallbackOnlyRunsOnce() throws {
|
||||||
|
var confirmationCount = 0
|
||||||
|
let dialog = SessionExpiredDialogViewController {
|
||||||
|
confirmationCount += 1
|
||||||
|
}
|
||||||
|
dialog.loadViewIfNeeded()
|
||||||
|
let button = try XCTUnwrap(
|
||||||
|
dialog.view.allSubviews(of: UIButton.self).first {
|
||||||
|
$0.accessibilityIdentifier == "sessionExpired.confirmButton"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
button.sendActions(for: .touchUpInside)
|
||||||
|
button.sendActions(for: .touchUpInside)
|
||||||
|
|
||||||
|
XCTAssertEqual(confirmationCount, 1)
|
||||||
|
XCTAssertFalse(button.isEnabled)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension UIView {
|
||||||
|
func allSubviews<View: UIView>(of type: View.Type) -> [View] {
|
||||||
|
subviews.flatMap { subview -> [View] in
|
||||||
|
let current = (subview as? View).map { [$0] } ?? []
|
||||||
|
return current + subview.allSubviews(of: type)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user