ios_app_v2/yobble/Views/Login/RegistrationView.swift
2025-10-23 19:20:04 +03:00

245 lines
11 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.

//
// RegistrationView.swift
// VolnahubApp
//
// Created by cheykrym on 09/06/2025.
//
import SwiftUI
struct RegistrationView: View {
@ObservedObject var viewModel: LoginViewModel
@Binding var isPresented: Bool
@Environment(\.presentationMode) private var presentationMode
@State private var username: String = ""
@State private var password: String = ""
@State private var confirmPassword: String = ""
@State private var inviteCode: String = ""
@State private var isLoading: Bool = false
@State private var showError: Bool = false
@State private var errorMessage: String = ""
@State private var isShowingTerms: Bool = false
@FocusState private var focusedField: Field?
private enum Field: Hashable {
case username
case password
case confirmPassword
case invite
}
private var isUsernameValid: Bool {
let pattern = "^[A-Za-z0-9_]{3,32}$"
return username.range(of: pattern, options: .regularExpression) != nil
}
private var isPasswordValid: Bool {
password.count >= 8 && password.count <= 128
}
private var isConfirmPasswordValid: Bool {
confirmPassword == password && !confirmPassword.isEmpty
}
private var isFormValid: Bool {
isUsernameValid && isPasswordValid && isConfirmPasswordValid && viewModel.hasAcceptedTerms
}
var body: some View {
NavigationView {
ScrollView {
ZStack(alignment: .top) {
Color.clear
.contentShape(Rectangle())
.onTapGesture { focusedField = nil }
VStack(alignment: .leading, spacing: 16) {
Group {
HStack {
TextField(NSLocalizedString("Логин", comment: "Логин"), text: $username)
.autocapitalization(.none)
.disableAutocorrection(true)
.focused($focusedField, equals: .username)
Spacer()
if !username.isEmpty {
Image(systemName: isUsernameValid ? "checkmark.circle" : "xmark.circle")
.foregroundColor(isUsernameValid ? .green : .red)
}
}
.padding()
.background(Color(.secondarySystemBackground))
.cornerRadius(8)
.autocapitalization(.none)
.disableAutocorrection(true)
.onChange(of: username) { newValue in
if newValue.count > 32 {
username = String(newValue.prefix(32))
}
}
if !isUsernameValid && !username.isEmpty {
Text(NSLocalizedString("Логин должен быть от 3 до 32 символов (английские буквы, цифры, _)", comment: "Логин должен быть от 3 до 32 символов (английские буквы, цифры, _)"))
.foregroundColor(.red)
.font(.caption)
}
HStack {
SecureField(NSLocalizedString("Пароль", comment: "Пароль"), text: $password)
.autocapitalization(.none)
.focused($focusedField, equals: .password)
Spacer()
if !password.isEmpty {
Image(systemName: isPasswordValid ? "checkmark.circle" : "xmark.circle")
.foregroundColor(isPasswordValid ? .green : .red)
}
}
.padding()
.background(Color(.secondarySystemBackground))
.cornerRadius(8)
.autocapitalization(.none)
.onChange(of: password) { newValue in
if newValue.count > 128 {
password = String(newValue.prefix(128))
}
}
if !isPasswordValid && !password.isEmpty {
Text(NSLocalizedString("Пароль должен быть от 8 до 128 символов", comment: "Пароль должен быть от 6 до 32 символов"))
.foregroundColor(.red)
.font(.caption)
}
HStack {
SecureField(NSLocalizedString("Подтверждение пароля", comment: "Подтверждение пароля"), text: $confirmPassword)
.autocapitalization(.none)
.focused($focusedField, equals: .confirmPassword)
Spacer()
if !confirmPassword.isEmpty {
Image(systemName: isConfirmPasswordValid ? "checkmark.circle" : "xmark.circle")
.foregroundColor(isConfirmPasswordValid ? .green : .red)
}
}
.padding()
.background(Color(.secondarySystemBackground))
.cornerRadius(8)
.autocapitalization(.none)
.onChange(of: confirmPassword) { newValue in
if newValue.count > 32 {
confirmPassword = String(newValue.prefix(32))
}
}
if !isConfirmPasswordValid && !confirmPassword.isEmpty {
Text(NSLocalizedString("Пароли не совпадают", comment: "Пароли не совпадают"))
.foregroundColor(.red)
.font(.caption)
}
TextField(NSLocalizedString("Инвайт-код (необязательно)", comment: "Инвайт-код"), text: $inviteCode)
.padding()
.background(Color(.secondarySystemBackground))
.cornerRadius(8)
.autocapitalization(.none)
.disableAutocorrection(true)
.focused($focusedField, equals: .invite)
}
TermsAgreementCard(
isAccepted: $viewModel.hasAcceptedTerms,
openTerms: {
viewModel.loadTermsIfNeeded()
isShowingTerms = true
}
)
Button(action: registerUser) {
if isLoading {
ProgressView()
.padding()
.frame(maxWidth: .infinity)
.background(Color.gray.opacity(0.6))
.cornerRadius(8)
} else {
Text(NSLocalizedString("Зарегистрироваться", comment: "Зарегистрироваться"))
.foregroundColor(.white)
.padding()
.frame(maxWidth: .infinity)
.background(isFormValid ? Color.blue : Color.gray.opacity(0.6))
.cornerRadius(8)
}
}
.disabled(!isFormValid)
.padding(.bottom)
}
.padding()
}
}
.navigationTitle(Text(NSLocalizedString("Регистрация", comment: "Регистрация")))
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button(action: dismissSheet) {
Text(NSLocalizedString("Закрыть", comment: "Закрыть"))
}
}
}
.alert(isPresented: $showError) {
Alert(
title: Text(NSLocalizedString("Ошибка регистрация", comment: "Ошибка")),
message: Text(errorMessage),
dismissButton: .default(Text(NSLocalizedString("OK", comment: "")))
)
}
}
.fullScreenCover(isPresented: $isShowingTerms) {
TermsFullScreenView(
isPresented: $isShowingTerms,
title: NSLocalizedString("Правила сервиса", comment: ""),
content: viewModel.termsContent,
isLoading: viewModel.isLoadingTerms,
errorMessage: viewModel.termsErrorMessage,
onRetry: {
viewModel.reloadTerms()
}
)
.onAppear {
if viewModel.termsContent.isEmpty {
viewModel.loadTermsIfNeeded()
}
}
}
}
private func registerUser() {
isLoading = true
errorMessage = ""
viewModel.registerUser(username: username, password: password, invite: inviteCode.isEmpty ? nil : inviteCode) { success, message in
isLoading = false
if success {
dismissSheet()
} else {
errorMessage = message ?? NSLocalizedString("Неизвестная ошибка.", comment: "")
showError = true
}
}
}
private func dismissSheet() {
focusedField = nil
viewModel.hasAcceptedTerms = false
isPresented = false
presentationMode.wrappedValue.dismiss()
}
}
struct RegistrationView_Previews: PreviewProvider {
static var previews: some View {
let viewModel = LoginViewModel()
viewModel.isLoading = false // чтобы убрать спиннер
return RegistrationView(viewModel: viewModel, isPresented: .constant(true))
}
}