Initial commit: suixinkan_ios UIKit rewrite project.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@ -0,0 +1,532 @@
|
||||
//
|
||||
// LoginViewController.swift
|
||||
// suixinkan
|
||||
//
|
||||
|
||||
import SnapKit
|
||||
import UIKit
|
||||
|
||||
@MainActor
|
||||
/// 登录页,展示手机号/密码表单、协议勾选和多账号选择入口。
|
||||
final class LoginViewController: UIViewController {
|
||||
|
||||
private let services: AppServices
|
||||
private let viewModel = LoginViewModel()
|
||||
|
||||
private let scrollView = UIScrollView()
|
||||
private let contentView = UIView()
|
||||
private let backgroundImageView = UIImageView(image: UIImage(named: "LoginBackground"))
|
||||
private let titleLabel = UILabel()
|
||||
private let cardView = UIView()
|
||||
private let usernameField = LoginInputField(iconName: "person", placeholder: "请输入手机号", isSecure: false)
|
||||
private let passwordField = LoginInputField(iconName: "lock", placeholder: "请输入密码", isSecure: true)
|
||||
private let privacyButton = UIButton(type: .custom)
|
||||
private let privacyLabel = UILabel()
|
||||
private let userAgreementButton = UIButton(type: .system)
|
||||
private let privacyPolicyButton = UIButton(type: .system)
|
||||
private let loginButton = UIButton(type: .system)
|
||||
|
||||
init(services: AppServices) {
|
||||
self.services = services
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
view.backgroundColor = UIColor(hex: 0x0B1220)
|
||||
configureViews()
|
||||
bindViewModel()
|
||||
viewModel.applyPreferences(services.authSessionCoordinator.loginPreferences())
|
||||
}
|
||||
|
||||
private func configureViews() {
|
||||
backgroundImageView.contentMode = .scaleAspectFill
|
||||
backgroundImageView.clipsToBounds = true
|
||||
|
||||
titleLabel.text = "欢迎使用\n随心瞰商家版"
|
||||
titleLabel.font = .systemFont(ofSize: AppMetrics.FontSize.largeTitle, weight: .semibold)
|
||||
titleLabel.textColor = .white
|
||||
titleLabel.numberOfLines = 0
|
||||
|
||||
cardView.backgroundColor = .white
|
||||
cardView.layer.cornerRadius = AppMetrics.CornerRadius.card
|
||||
cardView.layer.shadowColor = UIColor.black.cgColor
|
||||
cardView.layer.shadowOpacity = 0.12
|
||||
cardView.layer.shadowRadius = 18
|
||||
cardView.layer.shadowOffset = CGSize(width: 0, height: 8)
|
||||
|
||||
usernameField.textField.keyboardType = .phonePad
|
||||
usernameField.textField.textContentType = .telephoneNumber
|
||||
usernameField.textField.autocorrectionType = .no
|
||||
usernameField.textField.autocapitalizationType = .none
|
||||
usernameField.textField.returnKeyType = .next
|
||||
usernameField.textField.accessibilityIdentifier = "login.username"
|
||||
usernameField.textField.delegate = self
|
||||
usernameField.textField.addTarget(self, action: #selector(usernameChanged), for: .editingChanged)
|
||||
|
||||
passwordField.textField.autocorrectionType = .no
|
||||
passwordField.textField.autocapitalizationType = .none
|
||||
passwordField.textField.returnKeyType = .go
|
||||
passwordField.textField.accessibilityIdentifier = "login.password"
|
||||
passwordField.textField.delegate = self
|
||||
passwordField.textField.addTarget(self, action: #selector(passwordChanged), for: .editingChanged)
|
||||
passwordField.onToggleVisibility = { [weak self] in
|
||||
self?.viewModel.showsPassword.toggle()
|
||||
}
|
||||
|
||||
privacyButton.accessibilityIdentifier = "login.privacy"
|
||||
privacyButton.addTarget(self, action: #selector(togglePrivacy), for: .touchUpInside)
|
||||
|
||||
configureAgreementText()
|
||||
|
||||
loginButton.setTitle("登录", for: .normal)
|
||||
loginButton.titleLabel?.font = .systemFont(ofSize: AppMetrics.FontSize.body, weight: .medium)
|
||||
loginButton.layer.cornerRadius = AppMetrics.CornerRadius.button
|
||||
loginButton.accessibilityIdentifier = "login.submit"
|
||||
loginButton.addTarget(self, action: #selector(loginTapped), for: .touchUpInside)
|
||||
|
||||
view.addSubview(backgroundImageView)
|
||||
view.addSubview(scrollView)
|
||||
scrollView.addSubview(contentView)
|
||||
contentView.addSubview(titleLabel)
|
||||
contentView.addSubview(cardView)
|
||||
|
||||
cardView.addSubview(usernameField)
|
||||
cardView.addSubview(passwordField)
|
||||
cardView.addSubview(privacyButton)
|
||||
cardView.addSubview(privacyLabel)
|
||||
cardView.addSubview(loginButton)
|
||||
|
||||
backgroundImageView.snp.makeConstraints { make in
|
||||
make.edges.equalToSuperview()
|
||||
}
|
||||
|
||||
scrollView.snp.makeConstraints { make in
|
||||
make.edges.equalToSuperview()
|
||||
}
|
||||
|
||||
contentView.snp.makeConstraints { make in
|
||||
make.edges.equalTo(scrollView.contentLayoutGuide)
|
||||
make.width.equalTo(scrollView.frameLayoutGuide)
|
||||
}
|
||||
|
||||
titleLabel.snp.makeConstraints { make in
|
||||
make.top.equalTo(contentView.safeAreaLayoutGuide).offset(72)
|
||||
make.leading.trailing.equalToSuperview().inset(24)
|
||||
}
|
||||
titleLabel.accessibilityIdentifier = "login.title"
|
||||
|
||||
cardView.snp.makeConstraints { make in
|
||||
make.top.equalTo(titleLabel.snp.bottom).offset(AppMetrics.Spacing.xxLarge)
|
||||
make.leading.trailing.equalToSuperview().inset(20)
|
||||
make.bottom.equalToSuperview().inset(AppMetrics.Spacing.xxLarge)
|
||||
}
|
||||
|
||||
usernameField.snp.makeConstraints { make in
|
||||
make.top.leading.trailing.equalToSuperview().inset(AppMetrics.Spacing.medium)
|
||||
make.height.equalTo(AppMetrics.ControlSize.inputHeight)
|
||||
}
|
||||
|
||||
passwordField.snp.makeConstraints { make in
|
||||
make.top.equalTo(usernameField.snp.bottom).offset(AppMetrics.Spacing.xSmall)
|
||||
make.leading.trailing.equalTo(usernameField)
|
||||
make.height.equalTo(AppMetrics.ControlSize.inputHeight)
|
||||
}
|
||||
|
||||
privacyButton.snp.makeConstraints { make in
|
||||
make.top.equalTo(passwordField.snp.bottom).offset(AppMetrics.ControlSize.checkboxTapArea)
|
||||
make.leading.equalToSuperview().inset(AppMetrics.Spacing.medium)
|
||||
make.width.height.equalTo(AppMetrics.ControlSize.checkboxTapArea)
|
||||
}
|
||||
|
||||
privacyLabel.snp.makeConstraints { make in
|
||||
make.leading.equalTo(privacyButton.snp.trailing)
|
||||
make.trailing.equalToSuperview().inset(AppMetrics.Spacing.medium)
|
||||
make.top.equalTo(privacyButton).offset(AppMetrics.Spacing.xxSmall)
|
||||
}
|
||||
|
||||
loginButton.snp.makeConstraints { make in
|
||||
make.top.equalTo(privacyLabel.snp.bottom).offset(AppMetrics.Spacing.mediumLarge)
|
||||
make.leading.trailing.bottom.equalToSuperview().inset(AppMetrics.Spacing.medium)
|
||||
make.height.equalTo(AppMetrics.ControlSize.primaryButtonHeight)
|
||||
}
|
||||
|
||||
let tap = UITapGestureRecognizer(target: self, action: #selector(dismissKeyboard))
|
||||
tap.cancelsTouchesInView = false
|
||||
view.addGestureRecognizer(tap)
|
||||
}
|
||||
|
||||
private func configureAgreementText() {
|
||||
privacyLabel.numberOfLines = 0
|
||||
privacyLabel.font = .systemFont(ofSize: AppMetrics.FontSize.subheadline)
|
||||
|
||||
userAgreementButton.setTitle("《用户协议》", for: .normal)
|
||||
userAgreementButton.titleLabel?.font = .systemFont(ofSize: AppMetrics.FontSize.subheadline)
|
||||
userAgreementButton.setTitleColor(UIColor(hex: 0x208BFF), for: .normal)
|
||||
userAgreementButton.accessibilityIdentifier = "login.userAgreement"
|
||||
userAgreementButton.addTarget(self, action: #selector(openUserAgreement), for: .touchUpInside)
|
||||
|
||||
privacyPolicyButton.setTitle("《隐私政策》", for: .normal)
|
||||
privacyPolicyButton.titleLabel?.font = .systemFont(ofSize: AppMetrics.FontSize.subheadline)
|
||||
privacyPolicyButton.setTitleColor(UIColor(hex: 0x208BFF), for: .normal)
|
||||
privacyPolicyButton.accessibilityIdentifier = "login.privacyPolicy"
|
||||
privacyPolicyButton.addTarget(self, action: #selector(openPrivacyPolicy), for: .touchUpInside)
|
||||
|
||||
let prefix = UILabel()
|
||||
prefix.text = "已阅读并同意"
|
||||
prefix.font = .systemFont(ofSize: AppMetrics.FontSize.subheadline)
|
||||
prefix.textColor = UIColor(hex: 0x666666)
|
||||
|
||||
let separator = UILabel()
|
||||
separator.text = "与"
|
||||
separator.font = .systemFont(ofSize: AppMetrics.FontSize.subheadline)
|
||||
separator.textColor = UIColor(hex: 0x666666)
|
||||
|
||||
let row = UIStackView(arrangedSubviews: [prefix, userAgreementButton, separator, privacyPolicyButton])
|
||||
row.axis = .horizontal
|
||||
row.spacing = 0
|
||||
row.alignment = .center
|
||||
privacyLabel.addSubview(row)
|
||||
row.snp.makeConstraints { make in
|
||||
make.edges.equalToSuperview()
|
||||
}
|
||||
}
|
||||
|
||||
private func bindViewModel() {
|
||||
viewModel.onChange = { [weak self] in
|
||||
self?.renderViewModel()
|
||||
}
|
||||
renderViewModel()
|
||||
}
|
||||
|
||||
private func renderViewModel() {
|
||||
if usernameField.textField.text != viewModel.username {
|
||||
usernameField.textField.text = viewModel.username
|
||||
}
|
||||
if passwordField.textField.text != viewModel.password {
|
||||
passwordField.textField.text = viewModel.password
|
||||
}
|
||||
|
||||
passwordField.setSecureEntry(!viewModel.showsPassword)
|
||||
|
||||
let checkboxImage = viewModel.privacyChecked ? "LoginCheckboxChecked" : "LoginCheckboxUnchecked"
|
||||
privacyButton.setImage(UIImage(named: checkboxImage), for: .normal)
|
||||
|
||||
loginButton.isEnabled = viewModel.canSubmit && !viewModel.isLoading
|
||||
loginButton.backgroundColor = viewModel.canSubmit ? AppDesign.primary : .gray
|
||||
loginButton.setTitleColor(.white, for: .normal)
|
||||
loginButton.alpha = viewModel.isLoading ? 0.7 : 1
|
||||
|
||||
if viewModel.showsAgreementSheet {
|
||||
viewModel.showsAgreementSheet = false
|
||||
presentAgreementSheet()
|
||||
}
|
||||
|
||||
if let payload = viewModel.pendingAccountSelection {
|
||||
if let controller = accountSelectionController {
|
||||
controller.updateLoading(viewModel.isSelectingAccount)
|
||||
} else if presentedViewController == nil {
|
||||
presentAccountSelection(payload)
|
||||
}
|
||||
} else if accountSelectionController != nil {
|
||||
dismiss(animated: true)
|
||||
accountSelectionController = nil
|
||||
}
|
||||
}
|
||||
|
||||
private weak var accountSelectionController: AccountSelectionViewController?
|
||||
|
||||
@objc private func usernameChanged() {
|
||||
viewModel.username = usernameField.textField.text ?? ""
|
||||
viewModel.normalizeUsernameCountryCodeIfNeeded()
|
||||
services.toastCenter.dismiss()
|
||||
}
|
||||
|
||||
@objc private func passwordChanged() {
|
||||
viewModel.password = passwordField.textField.text ?? ""
|
||||
services.toastCenter.dismiss()
|
||||
}
|
||||
|
||||
@objc private func togglePrivacy() {
|
||||
viewModel.privacyChecked.toggle()
|
||||
}
|
||||
|
||||
@objc private func openUserAgreement() {
|
||||
showToast("用户协议页面待接入")
|
||||
}
|
||||
|
||||
@objc private func openPrivacyPolicy() {
|
||||
showToast("隐私政策页面待接入")
|
||||
}
|
||||
|
||||
@objc private func loginTapped() {
|
||||
view.endEditing(true)
|
||||
performLogin()
|
||||
}
|
||||
|
||||
@objc private func dismissKeyboard() {
|
||||
view.endEditing(true)
|
||||
}
|
||||
|
||||
private func performLogin() {
|
||||
if let validationError = viewModel.validateForLogin() {
|
||||
if validationError == .privacyUnchecked {
|
||||
presentAgreementSheet()
|
||||
} else {
|
||||
showToast(validationError.message)
|
||||
if validationError.focusField == .username {
|
||||
usernameField.textField.becomeFirstResponder()
|
||||
} else if validationError.focusField == .password {
|
||||
passwordField.textField.becomeFirstResponder()
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
Task {
|
||||
do {
|
||||
try await services.globalLoading.withLoading(message: "登录中...") {
|
||||
let resolution = try await viewModel.login(authAPI: services.authAPI)
|
||||
switch resolution {
|
||||
case let .completed(response):
|
||||
await completeLogin(with: response)
|
||||
case .needsAccountSelection:
|
||||
break
|
||||
}
|
||||
}
|
||||
} catch is CancellationError {
|
||||
return
|
||||
} catch {
|
||||
showToast(error.localizedDescription)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func presentAgreementSheet() {
|
||||
let controller = LoginAgreementConsentViewController(
|
||||
onOpenAgreement: { [weak self] title in
|
||||
self?.showToast("\(title)页面待接入")
|
||||
},
|
||||
onAgreeAndContinue: { [weak self] in
|
||||
self?.viewModel.acceptAgreement()
|
||||
self?.performLogin()
|
||||
}
|
||||
)
|
||||
if let sheet = controller.sheetPresentationController {
|
||||
sheet.detents = [.custom(resolver: { _ in 240 })]
|
||||
sheet.prefersGrabberVisible = true
|
||||
}
|
||||
present(controller, animated: true)
|
||||
}
|
||||
|
||||
private func presentAccountSelection(_ payload: AccountSelectionPayload) {
|
||||
let controller = AccountSelectionViewController(
|
||||
payload: payload,
|
||||
isLoading: viewModel.isSelectingAccount,
|
||||
onCancel: { [weak self] in
|
||||
self?.viewModel.clearPendingAccountSelection()
|
||||
self?.accountSelectionController = nil
|
||||
},
|
||||
onConfirm: { [weak self] account in
|
||||
self?.selectAccount(account)
|
||||
}
|
||||
)
|
||||
accountSelectionController = controller
|
||||
let navigation = UINavigationController(rootViewController: controller)
|
||||
navigation.modalPresentationStyle = .formSheet
|
||||
present(navigation, animated: true)
|
||||
}
|
||||
|
||||
private func selectAccount(_ account: AccountSwitchAccount) {
|
||||
Task {
|
||||
do {
|
||||
try await services.globalLoading.withLoading(message: "账号切换中...") {
|
||||
let response = try await viewModel.selectAccount(account, authAPI: services.authAPI)
|
||||
await completeLogin(with: response)
|
||||
accountSelectionController = nil
|
||||
dismiss(animated: true)
|
||||
}
|
||||
} catch is CancellationError {
|
||||
return
|
||||
} catch {
|
||||
showToast(error.localizedDescription)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func completeLogin(with response: V9AuthResponse) async {
|
||||
do {
|
||||
try await services.authSessionCoordinator.completeLogin(
|
||||
with: response,
|
||||
username: viewModel.normalizedUsername,
|
||||
privacyAgreementAccepted: viewModel.privacyChecked,
|
||||
appSession: services.appSession,
|
||||
accountContext: services.accountContext,
|
||||
permissionContext: services.permissionContext,
|
||||
profileAPI: services.profileAPI,
|
||||
accountContextAPI: services.accountContextAPI
|
||||
)
|
||||
} catch {
|
||||
showToast(error.localizedDescription)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension LoginViewController: UITextFieldDelegate {
|
||||
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
|
||||
if textField === usernameField.textField {
|
||||
passwordField.textField.becomeFirstResponder()
|
||||
} else if viewModel.canSubmit, !viewModel.isLoading {
|
||||
performLogin()
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
/// 登录输入框,包含图标、文本框和密码可见切换。
|
||||
private final class LoginInputField: UIView {
|
||||
let textField = UITextField()
|
||||
var onToggleVisibility: (() -> Void)?
|
||||
|
||||
private let toggleButton = UIButton(type: .custom)
|
||||
private var isSecure = false
|
||||
|
||||
init(iconName: String, placeholder: String, isSecure: Bool) {
|
||||
self.isSecure = isSecure
|
||||
super.init(frame: .zero)
|
||||
backgroundColor = UIColor(hex: 0xF1F5F9)
|
||||
layer.cornerRadius = AppMetrics.CornerRadius.input
|
||||
layer.borderWidth = 1
|
||||
layer.borderColor = UIColor(hex: 0xDDE3EA).cgColor
|
||||
|
||||
let icon = UIImageView(image: UIImage(systemName: iconName))
|
||||
icon.tintColor = AppDesign.textSecondary
|
||||
icon.contentMode = .scaleAspectFit
|
||||
|
||||
textField.placeholder = placeholder
|
||||
textField.font = .systemFont(ofSize: AppMetrics.FontSize.body)
|
||||
textField.textColor = AppDesign.textPrimary
|
||||
textField.tintColor = AppDesign.primary
|
||||
textField.borderStyle = .none
|
||||
textField.isSecureTextEntry = isSecure
|
||||
|
||||
addSubview(icon)
|
||||
addSubview(textField)
|
||||
|
||||
icon.snp.makeConstraints { make in
|
||||
make.leading.equalToSuperview().inset(AppMetrics.Spacing.medium)
|
||||
make.centerY.equalToSuperview()
|
||||
make.width.height.equalTo(AppMetrics.ControlSize.smallIcon)
|
||||
}
|
||||
|
||||
if isSecure {
|
||||
toggleButton.addTarget(self, action: #selector(toggleVisibility), for: .touchUpInside)
|
||||
addSubview(toggleButton)
|
||||
toggleButton.snp.makeConstraints { make in
|
||||
make.trailing.equalToSuperview().inset(AppMetrics.Spacing.small)
|
||||
make.centerY.equalToSuperview()
|
||||
make.width.height.equalTo(AppMetrics.ControlSize.iconTapArea)
|
||||
}
|
||||
textField.snp.makeConstraints { make in
|
||||
make.leading.equalTo(icon.snp.trailing).offset(AppMetrics.Spacing.xSmall)
|
||||
make.trailing.equalTo(toggleButton.snp.leading)
|
||||
make.centerY.equalToSuperview()
|
||||
}
|
||||
} else {
|
||||
textField.snp.makeConstraints { make in
|
||||
make.leading.equalTo(icon.snp.trailing).offset(AppMetrics.Spacing.xSmall)
|
||||
make.trailing.equalToSuperview().inset(AppMetrics.Spacing.medium)
|
||||
make.centerY.equalToSuperview()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
nil
|
||||
}
|
||||
|
||||
func setSecureEntry(_ secure: Bool) {
|
||||
textField.isSecureTextEntry = secure
|
||||
let imageName = secure ? "LoginPwdInvisible" : "LoginPwdVisible"
|
||||
toggleButton.setImage(UIImage(named: imageName), for: .normal)
|
||||
}
|
||||
|
||||
@objc private func toggleVisibility() {
|
||||
onToggleVisibility?()
|
||||
}
|
||||
}
|
||||
|
||||
/// 协议确认弹窗,引导用户勾选协议后继续登录。
|
||||
private final class LoginAgreementConsentViewController: UIViewController {
|
||||
|
||||
private let onOpenAgreement: (String) -> Void
|
||||
private let onAgreeAndContinue: () -> Void
|
||||
|
||||
init(onOpenAgreement: @escaping (String) -> Void, onAgreeAndContinue: @escaping () -> Void) {
|
||||
self.onOpenAgreement = onOpenAgreement
|
||||
self.onAgreeAndContinue = onAgreeAndContinue
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
nil
|
||||
}
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
view.backgroundColor = .systemBackground
|
||||
|
||||
let titleLabel = UILabel()
|
||||
titleLabel.text = "请先阅读并同意相关协议"
|
||||
titleLabel.font = .systemFont(ofSize: AppMetrics.FontSize.title3, weight: .semibold)
|
||||
titleLabel.textColor = AppDesign.textPrimary
|
||||
titleLabel.textAlignment = .center
|
||||
|
||||
let descriptionLabel = UILabel()
|
||||
descriptionLabel.text = "为了保障你的账号安全和服务体验,请阅读并同意用户协议和隐私政策。"
|
||||
descriptionLabel.font = .systemFont(ofSize: AppMetrics.FontSize.subheadline)
|
||||
descriptionLabel.textColor = AppDesign.textSecondary
|
||||
descriptionLabel.numberOfLines = 0
|
||||
descriptionLabel.textAlignment = .center
|
||||
|
||||
let continueButton = UIButton(type: .system)
|
||||
continueButton.setTitle("同意并继续", for: .normal)
|
||||
continueButton.titleLabel?.font = .systemFont(ofSize: AppMetrics.FontSize.body, weight: .medium)
|
||||
continueButton.backgroundColor = AppDesign.primary
|
||||
continueButton.setTitleColor(.white, for: .normal)
|
||||
continueButton.layer.cornerRadius = AppMetrics.CornerRadius.button
|
||||
continueButton.accessibilityIdentifier = "login.agreement.continue"
|
||||
continueButton.addTarget(self, action: #selector(continueTapped), for: .touchUpInside)
|
||||
|
||||
view.addSubview(titleLabel)
|
||||
view.addSubview(descriptionLabel)
|
||||
view.addSubview(continueButton)
|
||||
|
||||
titleLabel.snp.makeConstraints { make in
|
||||
make.top.equalTo(view.safeAreaLayoutGuide).offset(AppMetrics.Spacing.sheet)
|
||||
make.leading.trailing.equalToSuperview().inset(AppMetrics.Spacing.large)
|
||||
}
|
||||
|
||||
descriptionLabel.snp.makeConstraints { make in
|
||||
make.top.equalTo(titleLabel.snp.bottom).offset(AppMetrics.Spacing.small)
|
||||
make.leading.trailing.equalTo(titleLabel)
|
||||
}
|
||||
|
||||
continueButton.snp.makeConstraints { make in
|
||||
make.leading.trailing.equalTo(titleLabel)
|
||||
make.bottom.equalTo(view.safeAreaLayoutGuide).inset(AppMetrics.Spacing.mediumLarge)
|
||||
make.height.equalTo(AppMetrics.ControlSize.sheetButtonHeight)
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func continueTapped() {
|
||||
dismiss(animated: true) { [onAgreeAndContinue] in
|
||||
onAgreeAndContinue()
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user