edit register

This commit is contained in:
cheykrym 2025-12-03 07:17:31 +03:00
parent b8ffca967b
commit 50916b732a
5 changed files with 194 additions and 189 deletions

View File

@ -1036,6 +1036,9 @@
} }
} }
} }
},
"Назад к входу" : {
}, },
"Напишите нам через форму обратной связи в разделе \"Поддержка\"." : { "Напишите нам через форму обратной связи в разделе \"Поддержка\"." : {
"comment" : "FAQ answer: support" "comment" : "FAQ answer: support"
@ -1676,7 +1679,7 @@
} }
}, },
"Пароли не совпадают" : { "Пароли не совпадают" : {
"comment" : "Заголовок ошибки несовпадения паролей\nПароли не совпадают", "comment" : "Заголовок ошибки несовпадения паролей",
"localizations" : { "localizations" : {
"en" : { "en" : {
"stringUnit" : { "stringUnit" : {
@ -1815,6 +1818,9 @@
}, },
"Подключение" : { "Подключение" : {
},
"Подтвердите пароль" : {
}, },
"Подтвердить" : { "Подтвердить" : {
"comment" : "Кнопка подтверждения кода 2FA" "comment" : "Кнопка подтверждения кода 2FA"
@ -2441,6 +2447,9 @@
}, },
"Согласиться с правилами" : { "Согласиться с правилами" : {
},
"Создайте логин и пароль. При желании добавьте инвайт." : {
}, },
"Создать новые коды" : { "Создать новые коды" : {
"comment" : "Кнопка генерации резервных кодов" "comment" : "Кнопка генерации резервных кодов"

View File

@ -48,6 +48,7 @@ class LoginViewModel: ObservableObject {
case passwordlessRequest case passwordlessRequest
case passwordlessVerify case passwordlessVerify
case password case password
case registration
} }
enum ChatLoadingState: Equatable { enum ChatLoadingState: Equatable {
@ -230,6 +231,10 @@ class LoginViewModel: ObservableObject {
loginFlowStep = .passwordlessRequest loginFlowStep = .passwordlessRequest
} }
func showRegistration() {
loginFlowStep = .registration
}
func registerUser(username: String, password: String, invite: String?, completion: @escaping (Bool, String?) -> Void) { func registerUser(username: String, password: String, invite: String?, completion: @escaping (Bool, String?) -> Void) {
authService.register(username: username, password: password, invite: invite) { [weak self] success, message in authService.register(username: username, password: password, invite: invite) { [weak self] success, message in

View File

@ -0,0 +1,67 @@
import SwiftUI
struct LoginTopBar: View {
let openLanguageSettings: () -> Void
@EnvironmentObject private var themeManager: ThemeManager
@Environment(\.colorScheme) private var colorScheme
private let themeOptions = ThemeOption.ordered
var body: some View {
HStack {
Button(action: openLanguageSettings) {
Text("🌍")
.padding(8)
}
Spacer()
Menu {
ForEach(themeOptions) { option in
Button(action: { selectTheme(option) }) {
themeMenuContent(for: option)
.opacity(option.isEnabled ? 1.0 : 0.5)
}
.disabled(!option.isEnabled)
}
} label: {
Image(systemName: themeIconName)
.padding(8)
}
}
}
private var selectedThemeOption: ThemeOption {
ThemeOption.option(for: themeManager.theme)
}
private var themeIconName: String {
switch themeManager.theme {
case .system:
return colorScheme == .dark ? "moon.fill" : "sun.max.fill"
case .light:
return "sun.max.fill"
case .oledDark:
return "moon.fill"
}
}
private func themeMenuContent(for option: ThemeOption) -> some View {
let isSelected = option == selectedThemeOption
return HStack(spacing: 8) {
Image(systemName: isSelected ? "checkmark.circle.fill" : "circle")
.foregroundColor(isSelected ? .accentColor : .secondary)
VStack(alignment: .leading, spacing: 2) {
Text(option.title)
if let note = option.note {
Text(note)
.font(.caption)
.foregroundColor(.secondary)
}
}
}
}
private func selectTheme(_ option: ThemeOption) {
guard let mappedTheme = option.mappedTheme else { return }
themeManager.setTheme(mappedTheme)
}
}

View File

@ -54,6 +54,9 @@ struct LoginView: View {
case .password: case .password:
PasswordLoginView(viewModel: viewModel) PasswordLoginView(viewModel: viewModel)
.transition(.opacity) .transition(.opacity)
case .registration:
RegistrationView(viewModel: viewModel)
.transition(.move(edge: .bottom).combined(with: .opacity))
} }
} }
} }
@ -77,7 +80,6 @@ struct PasswordLoginView: View {
private let themeOptions = ThemeOption.ordered private let themeOptions = ThemeOption.ordered
@AppStorage("messengerModeEnabled") private var isMessengerModeEnabled: Bool = false @AppStorage("messengerModeEnabled") private var isMessengerModeEnabled: Bool = false
@State private var isShowingRegistration = false
@State private var showLegacySupportNotice = false @State private var showLegacySupportNotice = false
@State private var isShowingTerms = false @State private var isShowingTerms = false
@State private var hasResetTermsOnAppear = false @State private var hasResetTermsOnAppear = false
@ -197,17 +199,16 @@ struct PasswordLoginView: View {
.disabled(!isLoginButtonEnabled) .disabled(!isLoginButtonEnabled)
Button(action: { Button(action: {
isShowingRegistration = true
viewModel.hasAcceptedTerms = false viewModel.hasAcceptedTerms = false
withAnimation {
viewModel.showRegistration()
}
}) { }) {
Text(NSLocalizedString("Нет аккаунта? Регистрация", comment: "Регистрация")) Text(NSLocalizedString("Нет аккаунта? Регистрация", comment: "Регистрация"))
.foregroundColor(.blue) .foregroundColor(.blue)
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)
} }
.padding(.top, 4) .padding(.top, 4)
.sheet(isPresented: $isShowingRegistration) {
RegistrationView(viewModel: viewModel, isPresented: $isShowingRegistration)
}
Spacer(minLength: 0) Spacer(minLength: 0)
} }
@ -637,72 +638,6 @@ private struct OTPInputView: View {
} }
} }
private struct LoginTopBar: View {
let openLanguageSettings: () -> Void
@EnvironmentObject private var themeManager: ThemeManager
@Environment(\.colorScheme) private var colorScheme
private let themeOptions = ThemeOption.ordered
var body: some View {
HStack {
Button(action: openLanguageSettings) {
Text("🌍")
.padding(8)
}
Spacer()
Menu {
ForEach(themeOptions) { option in
Button(action: { selectTheme(option) }) {
themeMenuContent(for: option)
.opacity(option.isEnabled ? 1.0 : 0.5)
}
.disabled(!option.isEnabled)
}
} label: {
Image(systemName: themeIconName)
.padding(8)
}
}
}
private var selectedThemeOption: ThemeOption {
ThemeOption.option(for: themeManager.theme)
}
private var themeIconName: String {
switch themeManager.theme {
case .system:
return colorScheme == .dark ? "moon.fill" : "sun.max.fill"
case .light:
return "sun.max.fill"
case .oledDark:
return "moon.fill"
}
}
private func themeMenuContent(for option: ThemeOption) -> some View {
let isSelected = option == selectedThemeOption
return HStack(spacing: 8) {
Image(systemName: isSelected ? "checkmark.circle.fill" : "circle")
.foregroundColor(isSelected ? .accentColor : .secondary)
VStack(alignment: .leading, spacing: 2) {
Text(option.title)
if let note = option.note {
Text(note)
.font(.caption)
.foregroundColor(.secondary)
}
}
}
}
private func selectTheme(_ option: ThemeOption) {
guard let mappedTheme = option.mappedTheme else { return }
themeManager.setTheme(mappedTheme)
}
}
private struct MessengerModePrompt: View { private struct MessengerModePrompt: View {
@Binding var selection: Bool @Binding var selection: Bool
let onAccept: () -> Void let onAccept: () -> Void
@ -816,6 +751,7 @@ struct LoginView_Previews: PreviewProvider {
preview(step: .passwordlessRequest) preview(step: .passwordlessRequest)
preview(step: .passwordlessVerify) preview(step: .passwordlessVerify)
preview(step: .password) preview(step: .password)
preview(step: .registration)
} }
.environmentObject(ThemeManager()) .environmentObject(ThemeManager())
} }

View File

@ -9,8 +9,6 @@ import SwiftUI
struct RegistrationView: View { struct RegistrationView: View {
@ObservedObject var viewModel: LoginViewModel @ObservedObject var viewModel: LoginViewModel
@Binding var isPresented: Bool
@Environment(\.presentationMode) private var presentationMode
@State private var username: String = "" @State private var username: String = ""
@State private var password: String = "" @State private var password: String = ""
@ -49,149 +47,133 @@ struct RegistrationView: View {
} }
var body: some View { var body: some View {
NavigationView { ScrollView(showsIndicators: false) {
ScrollView { VStack(alignment: .leading, spacing: 24) {
ZStack(alignment: .top) { LoginTopBar(openLanguageSettings: openLanguageSettings)
Color.clear
.contentShape(Rectangle())
.onTapGesture { focusedField = nil }
VStack(alignment: .leading, spacing: 16) { Button(action: goBack) {
Group { HStack(spacing: 6) {
HStack { Image(systemName: "arrow.left")
TextField(NSLocalizedString("Логин", comment: "Логин"), text: $username) Text(NSLocalizedString("Назад к входу", comment: ""))
.autocapitalization(.none) }
.disableAutocorrection(true) .font(.footnote)
.focused($focusedField, equals: .username) .foregroundColor(.blue)
Spacer() }
if !username.isEmpty {
Image(systemName: isUsernameValid ? "checkmark.circle" : "xmark.circle") VStack(alignment: .leading, spacing: 8) {
.foregroundColor(isUsernameValid ? .green : .red) Text(NSLocalizedString("Регистрация", comment: "Регистрация"))
} .font(.largeTitle).bold()
} Text(NSLocalizedString("Создайте логин и пароль. При желании добавьте инвайт.", comment: ""))
.padding() .foregroundColor(.secondary)
.background(Color(.secondarySystemBackground)) }
.cornerRadius(8)
Group {
VStack(alignment: .leading, spacing: 4) {
TextField(NSLocalizedString("Логин", comment: "Логин"), text: $username)
.autocapitalization(.none) .autocapitalization(.none)
.disableAutocorrection(true) .disableAutocorrection(true)
.focused($focusedField, equals: .username)
.padding()
.background(Color(.secondarySystemBackground))
.cornerRadius(12)
.onChange(of: username) { newValue in .onChange(of: username) { newValue in
if newValue.count > 32 { if newValue.count > 32 {
username = String(newValue.prefix(32)) username = String(newValue.prefix(32))
} }
} }
if !isUsernameValid && !username.isEmpty { if !isUsernameValid && !username.isEmpty {
Text(NSLocalizedString("Логин должен быть от 3 до 32 символов (английские буквы, цифры, _)", comment: "Логин должен быть от 3 до 32 символов (английские буквы, цифры, _)")) Text(NSLocalizedString("Логин должен быть от 3 до 32 символов (английские буквы, цифры, _)", comment: ""))
.foregroundColor(.red) .foregroundColor(.red)
.font(.caption) .font(.caption)
} }
}
HStack { VStack(alignment: .leading, spacing: 4) {
SecureField(NSLocalizedString("Пароль", comment: "Пароль"), text: $password) SecureField(NSLocalizedString("Пароль", comment: "Пароль"), text: $password)
.autocapitalization(.none) .autocapitalization(.none)
.focused($focusedField, equals: .password) .focused($focusedField, equals: .password)
Spacer()
if !password.isEmpty {
Image(systemName: isPasswordValid ? "checkmark.circle" : "xmark.circle")
.foregroundColor(isPasswordValid ? .green : .red)
}
}
.padding() .padding()
.background(Color(.secondarySystemBackground)) .background(Color(.secondarySystemBackground))
.cornerRadius(8) .cornerRadius(12)
.autocapitalization(.none)
.onChange(of: password) { newValue in .onChange(of: password) { newValue in
if newValue.count > 128 { if newValue.count > 128 {
password = String(newValue.prefix(128)) password = String(newValue.prefix(128))
} }
} }
if !isPasswordValid && !password.isEmpty { if !isPasswordValid && !password.isEmpty {
Text(NSLocalizedString("Пароль должен быть от 8 до 128 символов", comment: "Пароль должен быть от 6 до 32 символов")) Text(NSLocalizedString("Пароль должен быть от 8 до 128 символов", comment: ""))
.foregroundColor(.red) .foregroundColor(.red)
.font(.caption) .font(.caption)
} }
}
HStack { VStack(alignment: .leading, spacing: 4) {
SecureField(NSLocalizedString("Подтверждение пароля", comment: "Подтверждение пароля"), text: $confirmPassword) SecureField(NSLocalizedString("Подтвердите пароль", comment: ""), text: $confirmPassword)
.autocapitalization(.none) .autocapitalization(.none)
.focused($focusedField, equals: .confirmPassword) .focused($focusedField, equals: .confirmPassword)
Spacer()
if !confirmPassword.isEmpty {
Image(systemName: isConfirmPasswordValid ? "checkmark.circle" : "xmark.circle")
.foregroundColor(isConfirmPasswordValid ? .green : .red)
}
}
.padding() .padding()
.background(Color(.secondarySystemBackground)) .background(Color(.secondarySystemBackground))
.cornerRadius(8) .cornerRadius(12)
.autocapitalization(.none)
.onChange(of: confirmPassword) { newValue in .onChange(of: confirmPassword) { newValue in
if newValue.count > 32 { if newValue.count > 32 {
confirmPassword = String(newValue.prefix(32)) confirmPassword = String(newValue.prefix(32))
} }
} }
if !isConfirmPasswordValid && !confirmPassword.isEmpty { if !isConfirmPasswordValid && !confirmPassword.isEmpty {
Text(NSLocalizedString("Пароли не совпадают", comment: "Пароли не совпадают")) Text(NSLocalizedString("Пароли не совпадают", comment: ""))
.foregroundColor(.red) .foregroundColor(.red)
.font(.caption) .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()
TextField(NSLocalizedString("Инвайт-код (необязательно)", comment: ""), text: $inviteCode)
.autocapitalization(.none)
.disableAutocorrection(true)
.focused($focusedField, equals: .invite)
.padding()
.background(Color(.secondarySystemBackground))
.cornerRadius(12)
} }
}
.navigationTitle(Text(NSLocalizedString("Регистрация", comment: "Регистрация"))) TermsAgreementCard(
.toolbar { isAccepted: $viewModel.hasAcceptedTerms,
ToolbarItem(placement: .navigationBarTrailing) { openTerms: {
Button(action: dismissSheet) { viewModel.loadTermsIfNeeded()
Text(NSLocalizedString("Закрыть", comment: "Закрыть")) isShowingTerms = true
} }
}
}
.alert(isPresented: $showError) {
Alert(
title: Text(NSLocalizedString("Ошибка регистрация", comment: "Ошибка")),
message: Text(errorMessage),
dismissButton: .default(Text(NSLocalizedString("OK", comment: "")))
) )
Button(action: registerUser) {
if isLoading {
ProgressView()
.frame(maxWidth: .infinity)
.padding()
} else {
Text(NSLocalizedString("Зарегистрироваться", comment: "Зарегистрироваться"))
.foregroundColor(.white)
.frame(maxWidth: .infinity)
.padding()
}
}
.background(isFormValid ? Color.blue : Color.gray.opacity(0.6))
.cornerRadius(12)
.disabled(!isFormValid)
} }
.padding(.vertical, 32)
}
.padding(.horizontal, 24)
.background(Color(.systemBackground).ignoresSafeArea())
.contentShape(Rectangle())
.onTapGesture { focusedField = nil }
.alert(isPresented: $showError) {
Alert(
title: Text(NSLocalizedString("Ошибка регистрация", comment: "Ошибка")),
message: Text(errorMessage),
dismissButton: .default(Text(NSLocalizedString("OK", comment: "")))
)
} }
.fullScreenCover(isPresented: $isShowingTerms) { .fullScreenCover(isPresented: $isShowingTerms) {
TermsFullScreenView( TermsFullScreenView(
@ -218,7 +200,7 @@ struct RegistrationView: View {
viewModel.registerUser(username: username, password: password, invite: inviteCode.isEmpty ? nil : inviteCode) { success, message in viewModel.registerUser(username: username, password: password, invite: inviteCode.isEmpty ? nil : inviteCode) { success, message in
isLoading = false isLoading = false
if success { if success {
dismissSheet() viewModel.hasAcceptedTerms = false
} else { } else {
errorMessage = message ?? NSLocalizedString("Неизвестная ошибка.", comment: "") errorMessage = message ?? NSLocalizedString("Неизвестная ошибка.", comment: "")
showError = true showError = true
@ -226,11 +208,17 @@ struct RegistrationView: View {
} }
} }
private func dismissSheet() { private func goBack() {
focusedField = nil focusedField = nil
viewModel.hasAcceptedTerms = false viewModel.hasAcceptedTerms = false
isPresented = false withAnimation {
presentationMode.wrappedValue.dismiss() viewModel.showPasswordLogin()
}
}
private func openLanguageSettings() {
guard let url = URL(string: UIApplication.openSettingsURLString) else { return }
UIApplication.shared.open(url)
} }
} }
@ -239,6 +227,6 @@ struct RegistrationView_Previews: PreviewProvider {
static var previews: some View { static var previews: some View {
let viewModel = LoginViewModel() let viewModel = LoginViewModel()
viewModel.isLoading = false // чтобы убрать спиннер viewModel.isLoading = false // чтобы убрать спиннер
return RegistrationView(viewModel: viewModel, isPresented: .constant(true)) return RegistrationView(viewModel: viewModel)
} }
} }