edit register
This commit is contained in:
parent
b8ffca967b
commit
50916b732a
@ -1036,6 +1036,9 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Назад к входу" : {
|
||||
|
||||
},
|
||||
"Напишите нам через форму обратной связи в разделе \"Поддержка\"." : {
|
||||
"comment" : "FAQ answer: support"
|
||||
@ -1676,7 +1679,7 @@
|
||||
}
|
||||
},
|
||||
"Пароли не совпадают" : {
|
||||
"comment" : "Заголовок ошибки несовпадения паролей\nПароли не совпадают",
|
||||
"comment" : "Заголовок ошибки несовпадения паролей",
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
@ -1815,6 +1818,9 @@
|
||||
},
|
||||
"Подключение" : {
|
||||
|
||||
},
|
||||
"Подтвердите пароль" : {
|
||||
|
||||
},
|
||||
"Подтвердить" : {
|
||||
"comment" : "Кнопка подтверждения кода 2FA"
|
||||
@ -2441,6 +2447,9 @@
|
||||
},
|
||||
"Согласиться с правилами" : {
|
||||
|
||||
},
|
||||
"Создайте логин и пароль. При желании добавьте инвайт." : {
|
||||
|
||||
},
|
||||
"Создать новые коды" : {
|
||||
"comment" : "Кнопка генерации резервных кодов"
|
||||
|
||||
@ -48,6 +48,7 @@ class LoginViewModel: ObservableObject {
|
||||
case passwordlessRequest
|
||||
case passwordlessVerify
|
||||
case password
|
||||
case registration
|
||||
}
|
||||
|
||||
enum ChatLoadingState: Equatable {
|
||||
@ -230,6 +231,10 @@ class LoginViewModel: ObservableObject {
|
||||
loginFlowStep = .passwordlessRequest
|
||||
}
|
||||
|
||||
func showRegistration() {
|
||||
loginFlowStep = .registration
|
||||
}
|
||||
|
||||
|
||||
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
|
||||
|
||||
67
yobble/Views/Login/LoginTopBar.swift
Normal file
67
yobble/Views/Login/LoginTopBar.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
@ -54,6 +54,9 @@ struct LoginView: View {
|
||||
case .password:
|
||||
PasswordLoginView(viewModel: viewModel)
|
||||
.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
|
||||
@AppStorage("messengerModeEnabled") private var isMessengerModeEnabled: Bool = false
|
||||
|
||||
@State private var isShowingRegistration = false
|
||||
@State private var showLegacySupportNotice = false
|
||||
@State private var isShowingTerms = false
|
||||
@State private var hasResetTermsOnAppear = false
|
||||
@ -197,17 +199,16 @@ struct PasswordLoginView: View {
|
||||
.disabled(!isLoginButtonEnabled)
|
||||
|
||||
Button(action: {
|
||||
isShowingRegistration = true
|
||||
viewModel.hasAcceptedTerms = false
|
||||
withAnimation {
|
||||
viewModel.showRegistration()
|
||||
}
|
||||
}) {
|
||||
Text(NSLocalizedString("Нет аккаунта? Регистрация", comment: "Регистрация"))
|
||||
.foregroundColor(.blue)
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
.padding(.top, 4)
|
||||
.sheet(isPresented: $isShowingRegistration) {
|
||||
RegistrationView(viewModel: viewModel, isPresented: $isShowingRegistration)
|
||||
}
|
||||
|
||||
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 {
|
||||
@Binding var selection: Bool
|
||||
let onAccept: () -> Void
|
||||
@ -816,6 +751,7 @@ struct LoginView_Previews: PreviewProvider {
|
||||
preview(step: .passwordlessRequest)
|
||||
preview(step: .passwordlessVerify)
|
||||
preview(step: .password)
|
||||
preview(step: .registration)
|
||||
}
|
||||
.environmentObject(ThemeManager())
|
||||
}
|
||||
|
||||
@ -9,8 +9,6 @@ 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 = ""
|
||||
@ -49,149 +47,133 @@ struct RegistrationView: View {
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
ScrollView {
|
||||
ZStack(alignment: .top) {
|
||||
Color.clear
|
||||
.contentShape(Rectangle())
|
||||
.onTapGesture { focusedField = nil }
|
||||
ScrollView(showsIndicators: false) {
|
||||
VStack(alignment: .leading, spacing: 24) {
|
||||
LoginTopBar(openLanguageSettings: openLanguageSettings)
|
||||
|
||||
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)
|
||||
Button(action: goBack) {
|
||||
HStack(spacing: 6) {
|
||||
Image(systemName: "arrow.left")
|
||||
Text(NSLocalizedString("Назад к входу", comment: ""))
|
||||
}
|
||||
.font(.footnote)
|
||||
.foregroundColor(.blue)
|
||||
}
|
||||
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Text(NSLocalizedString("Регистрация", comment: "Регистрация"))
|
||||
.font(.largeTitle).bold()
|
||||
Text(NSLocalizedString("Создайте логин и пароль. При желании добавьте инвайт.", comment: ""))
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
|
||||
Group {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
TextField(NSLocalizedString("Логин", comment: "Логин"), text: $username)
|
||||
.autocapitalization(.none)
|
||||
.disableAutocorrection(true)
|
||||
.focused($focusedField, equals: .username)
|
||||
.padding()
|
||||
.background(Color(.secondarySystemBackground))
|
||||
.cornerRadius(12)
|
||||
.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)
|
||||
}
|
||||
if !isUsernameValid && !username.isEmpty {
|
||||
Text(NSLocalizedString("Логин должен быть от 3 до 32 символов (английские буквы, цифры, _)", comment: ""))
|
||||
.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)
|
||||
}
|
||||
}
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
SecureField(NSLocalizedString("Пароль", comment: "Пароль"), text: $password)
|
||||
.autocapitalization(.none)
|
||||
.focused($focusedField, equals: .password)
|
||||
.padding()
|
||||
.background(Color(.secondarySystemBackground))
|
||||
.cornerRadius(8)
|
||||
.autocapitalization(.none)
|
||||
.cornerRadius(12)
|
||||
.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)
|
||||
}
|
||||
if !isPasswordValid && !password.isEmpty {
|
||||
Text(NSLocalizedString("Пароль должен быть от 8 до 128 символов", comment: ""))
|
||||
.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)
|
||||
}
|
||||
}
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
SecureField(NSLocalizedString("Подтвердите пароль", comment: ""), text: $confirmPassword)
|
||||
.autocapitalization(.none)
|
||||
.focused($focusedField, equals: .confirmPassword)
|
||||
.padding()
|
||||
.background(Color(.secondarySystemBackground))
|
||||
.cornerRadius(8)
|
||||
.autocapitalization(.none)
|
||||
.cornerRadius(12)
|
||||
.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)
|
||||
if !isConfirmPasswordValid && !confirmPassword.isEmpty {
|
||||
Text(NSLocalizedString("Пароли не совпадают", comment: ""))
|
||||
.foregroundColor(.red)
|
||||
.font(.caption)
|
||||
}
|
||||
|
||||
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: "Регистрация")))
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
Button(action: dismissSheet) {
|
||||
Text(NSLocalizedString("Закрыть", comment: "Закрыть"))
|
||||
|
||||
TermsAgreementCard(
|
||||
isAccepted: $viewModel.hasAcceptedTerms,
|
||||
openTerms: {
|
||||
viewModel.loadTermsIfNeeded()
|
||||
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) {
|
||||
TermsFullScreenView(
|
||||
@ -218,7 +200,7 @@ struct RegistrationView: View {
|
||||
viewModel.registerUser(username: username, password: password, invite: inviteCode.isEmpty ? nil : inviteCode) { success, message in
|
||||
isLoading = false
|
||||
if success {
|
||||
dismissSheet()
|
||||
viewModel.hasAcceptedTerms = false
|
||||
} else {
|
||||
errorMessage = message ?? NSLocalizedString("Неизвестная ошибка.", comment: "")
|
||||
showError = true
|
||||
@ -226,11 +208,17 @@ struct RegistrationView: View {
|
||||
}
|
||||
}
|
||||
|
||||
private func dismissSheet() {
|
||||
private func goBack() {
|
||||
focusedField = nil
|
||||
viewModel.hasAcceptedTerms = false
|
||||
isPresented = false
|
||||
presentationMode.wrappedValue.dismiss()
|
||||
withAnimation {
|
||||
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 {
|
||||
let viewModel = LoginViewModel()
|
||||
viewModel.isLoading = false // чтобы убрать спиннер
|
||||
return RegistrationView(viewModel: viewModel, isPresented: .constant(true))
|
||||
return RegistrationView(viewModel: viewModel)
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user