fix: 增加登录失效确认弹窗
This commit is contained in:
@ -8,6 +8,7 @@ import UIKit
|
||||
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
|
||||
|
||||
var window: UIWindow?
|
||||
private var sessionExpiredDialog: SessionExpiredDialogViewController?
|
||||
|
||||
func scene(
|
||||
_ scene: UIScene,
|
||||
@ -86,18 +87,36 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
|
||||
}
|
||||
|
||||
@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()
|
||||
AppStore.shared.logout()
|
||||
AppRouter.setRoot(.login, on: window)
|
||||
}
|
||||
|
||||
@objc private func handleUserDidLogout() {
|
||||
PushNotificationManager.shared.handleLogout()
|
||||
AppStore.shared.logout()
|
||||
AppRouter.setRoot(.login, on: window)
|
||||
transitionToLogin()
|
||||
}
|
||||
|
||||
@objc private func handleUserDidLogin() {
|
||||
sessionExpiredDialog?.dismiss(animated: false)
|
||||
sessionExpiredDialog = nil
|
||||
AppRouter.setRoot(.mainTab, on: window)
|
||||
DispatchQueue.main.async {
|
||||
PushNotificationManager.shared.handleLoginCompleted()
|
||||
@ -108,4 +127,18 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
|
||||
@objc private func handleAccountDidSwitch() {
|
||||
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()
|
||||
}
|
||||
}
|
||||
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