245 lines
11 KiB
Swift
245 lines
11 KiB
Swift
//
|
||
// 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))
|
||
}
|
||
}
|