diff --git a/yobble/Resources/Localizable.xcstrings b/yobble/Resources/Localizable.xcstrings index 6311b08..77c5a8c 100644 --- a/yobble/Resources/Localizable.xcstrings +++ b/yobble/Resources/Localizable.xcstrings @@ -198,6 +198,9 @@ } } } + }, + "Yobble Passport" : { + }, "Автоудаление аккаунта" : { "localizations" : { @@ -285,9 +288,6 @@ }, "Введите логин" : { "comment" : "Логин" - }, - "Введите логин и мы отправим шестизначный код подтверждения." : { - }, "Введите пароль" : { "comment" : "Пароль\nПоле ввода пароля на приложение" @@ -392,7 +392,7 @@ "Всего сессий" : { "comment" : "Сводка по количеству сессий" }, - "Вход" : { + "Вход в аккаунт" : { }, "Вход и защита аккаунта (заглушка)" : { @@ -778,9 +778,6 @@ }, "Код дружбы" : { "comment" : "Friend code badge" - }, - "Код может прийти по почте, push или в другое подключенное приложение." : { - }, "Код отправлен. Аккаунт: @%@" : { @@ -905,6 +902,7 @@ }, "Логин" : { "comment" : "Логин", + "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { @@ -1777,9 +1775,6 @@ }, "Перейдите в раздел \"Настройки > Сменить пароль\" и следуйте инструкциям." : { "comment" : "FAQ answer: reset password" - }, - "Перейти к входу по коду" : { - }, "По умолчанию это полноценная соцсеть с лентой, историями и подписками. Если нужно только общение без лишнего контента, переключитесь на режим “Только чаты”. Переключить режим можно в любой момент." : { @@ -1923,9 +1918,6 @@ }, "Получать коды на email при входе" : { "comment" : "Переключатель отправки кодов при входе" - }, - "Получить код" : { - }, "Получить ответ от команды" : { "comment" : "feedback: contact toggle", diff --git a/yobble/ViewModels/LoginViewModel.swift b/yobble/ViewModels/LoginViewModel.swift index cd0aa14..32767c8 100644 --- a/yobble/ViewModels/LoginViewModel.swift +++ b/yobble/ViewModels/LoginViewModel.swift @@ -25,7 +25,13 @@ class LoginViewModel: ObservableObject { @Published var termsErrorMessage: String? @Published var onboardingDestination: OnboardingDestination? @Published var loginFlowStep: LoginFlowStep = .passwordlessRequest - @Published var passwordlessLogin: String = "" + @Published var passwordlessLogin: String = "" { + didSet { + if passwordlessLogin.count > 32 { + passwordlessLogin = String(passwordlessLogin.prefix(32)) + } + } + } @Published var verificationCode: String = "" { didSet { let filtered = verificationCode @@ -137,8 +143,13 @@ class LoginViewModel: ObservableObject { func login() { isLoading = true showError = false + let trimmedLogin = passwordlessLogin.trimmingCharacters(in: .whitespacesAndNewlines) + if trimmedLogin != passwordlessLogin { + passwordlessLogin = trimmedLogin + } + username = trimmedLogin - authService.login(username: username, password: password) { [weak self] success, error in + authService.login(username: trimmedLogin, password: password) { [weak self] success, error in DispatchQueue.main.async { self?.isLoading = false if success { @@ -178,6 +189,9 @@ class LoginViewModel: ObservableObject { self.loginFlowStep = .passwordlessVerify self.startResendTimer() } else { + if self.handlePasswordlessRedirect(message: message, login: trimmedLogin) { + return + } self.errorMessage = message ?? NSLocalizedString("Не удалось отправить код.", comment: "") self.showError = true } @@ -208,7 +222,7 @@ class LoginViewModel: ObservableObject { } else { self.errorMessage = message ?? NSLocalizedString("Проверьте введённый код и попробуйте снова.", comment: "") self.showError = true - self.verificationCode = "" +// self.verificationCode = "" } } } @@ -385,6 +399,26 @@ extension LoginViewModel { } private extension LoginViewModel { + func handlePasswordlessRedirect(message: String?, login: String) -> Bool { + guard let message else { return false } + + switch message { + case "otp_not_found": + username = login + passwordlessLogin = login + loginFlowStep = .password + return true + case "account_not_found": + username = login + passwordlessLogin = login + hasAcceptedTerms = false + loginFlowStep = .registration + return true + default: + return false + } + } + enum Constants { static let verificationCodeLength = 6 static let defaultResendDelay = 60 diff --git a/yobble/Views/Login/LoginView.swift b/yobble/Views/Login/LoginView.swift index 86d3524..6465cd9 100644 --- a/yobble/Views/Login/LoginView.swift +++ b/yobble/Views/Login/LoginView.swift @@ -109,7 +109,7 @@ struct PasswordLoginView: View { } private var isUsernameValid: Bool { - LoginViewModel.isLoginValid(viewModel.username) + LoginViewModel.isLoginValid(viewModel.passwordlessLogin) } private var isPasswordValid: Bool { @@ -151,21 +151,16 @@ struct PasswordLoginView: View { HStack(spacing: 8) { Text("@") .foregroundColor(.secondary) - TextField(NSLocalizedString("Введите логин", comment: ""), text: $viewModel.username) + TextField(NSLocalizedString("Введите логин", comment: ""), text: $viewModel.passwordlessLogin) .autocapitalization(.none) .disableAutocorrection(true) .focused($focusedField, equals: .username) - .onChange(of: viewModel.username) { newValue in - if newValue.count > 32 { - viewModel.username = String(newValue.prefix(32)) - } - } } .padding() .background(Color(.secondarySystemBackground)) .cornerRadius(12) - if !isUsernameValid && !viewModel.username.isEmpty { + if !isUsernameValid && !viewModel.passwordlessLogin.isEmpty { Text(NSLocalizedString("Неверный логин", comment: "Неверный логин")) .foregroundColor(.red) .font(.caption) @@ -400,22 +395,17 @@ private struct PasswordlessRequestView: View { var body: some View { ScrollView(showsIndicators: false) { + VStack(alignment: .leading, spacing: 24) { LoginTopBar(openLanguageSettings: openLanguageSettings, onShowModePrompt: hideKeyboardAndShowModePrompt) VStack(alignment: .leading, spacing: 8) { - Text(NSLocalizedString("Вход", comment: "")) + + Text(NSLocalizedString("Yobble Passport", comment: "")) .font(.largeTitle).bold() -// Text(NSLocalizedString("Введите логин и мы отправим шестизначный код подтверждения.", comment: "")) -// .foregroundColor(.secondary) -// Text(NSLocalizedString("Введите логин и мы отправим шестизначный код подтверждения.", comment: "")) -// .foregroundColor(.secondary) } VStack(alignment: .leading, spacing: 8) { -// Text(NSLocalizedString("Логин", comment: "")) -// .font(.subheadline) -// .foregroundColor(.secondary) HStack(spacing: 8) { Text("@") .foregroundColor(.secondary) @@ -434,11 +424,6 @@ private struct PasswordlessRequestView: View { .padding() .background(Color(.secondarySystemBackground)) .cornerRadius(12) - if !isLoginValid && !viewModel.passwordlessLogin.isEmpty { - Text(NSLocalizedString("Неверный логин", comment: "")) - .foregroundColor(.red) - .font(.caption) - } } Button { @@ -451,10 +436,6 @@ private struct PasswordlessRequestView: View { .frame(maxWidth: .infinity) .padding() } else { -// Text(NSLocalizedString("Получить код", comment: "")) -// .bold() -// .frame(maxWidth: .infinity) -// .padding() Text(NSLocalizedString("Войти", comment: "")) .bold() .frame(maxWidth: .infinity) @@ -477,32 +458,7 @@ private struct PasswordlessRequestView: View { Text(NSLocalizedString("Нет аккаунта? Регистрация", comment: "")) .frame(maxWidth: .infinity) } - -// Button(action: { -// viewModel.hasAcceptedTerms = false -// withAnimation { -// viewModel.showRegistration() -// } -// }) { -// Text(NSLocalizedString("Нет аккаунта? Регистрация", comment: "Регистрация")) -// .foregroundColor(.blue) -// .frame(maxWidth: .infinity) -// } - - Button { - withAnimation { - viewModel.showPasswordLogin() - } - } label: { - Text(NSLocalizedString("Войти по паролю", comment: "")) - .font(.body) - .frame(maxWidth: .infinity) - } - .padding(.vertical, 4) - -// Text(NSLocalizedString("Код может прийти по почте, push или в другое подключенное приложение.", comment: "")) -// .font(.footnote) -// .foregroundColor(.secondary) + } .padding(.vertical, 32) } @@ -553,36 +509,56 @@ private struct PasswordlessVerifyView: View { ScrollView(showsIndicators: false) { VStack(alignment: .leading, spacing: 24) { LoginTopBar(openLanguageSettings: openLanguageSettings, onShowModePrompt: hideKeyboardAndShowModePrompt) - + + Button { +// focusedField = nil + withAnimation { + viewModel.showPasswordlessRequest() + } + } label: { + HStack(spacing: 6) { + Image(systemName: "arrow.left") + Text(NSLocalizedString("Назад", comment: "")) + } + .font(.footnote) + .foregroundColor(.blue) + } + VStack(alignment: .leading, spacing: 8) { - Text(NSLocalizedString("Введите код", comment: "")) + Text(NSLocalizedString("Вход в аккаунт", comment: "")) .font(.largeTitle).bold() - Text(String(format: NSLocalizedString("Код отправлен. Аккаунт: @%@", comment: ""), viewModel.passwordlessLogin)) + Text(String(format: NSLocalizedString("@%@", comment: ""), viewModel.passwordlessLogin)) .foregroundColor(.secondary) + +// Text(NSLocalizedString("Введите код", comment: "")) +// .font(.largeTitle).bold() +// +// Text(String(format: NSLocalizedString("Код отправлен. Аккаунт: @%@", comment: ""), viewModel.passwordlessLogin)) +// .foregroundColor(.secondary) } OTPInputView(code: $viewModel.verificationCode, isFocused: $isCodeFieldFocused) - Button { - withAnimation { - viewModel.verifyPasswordlessCode() - } - } label: { - if viewModel.isVerifyingCode { - ProgressView() - .frame(maxWidth: .infinity) - .padding() - } else { - Text(NSLocalizedString("Подтвердить вход", comment: "")) - .bold() - .frame(maxWidth: .infinity) - .padding() - } - } - .foregroundColor(.white) - .background(viewModel.canVerifyPasswordlessCode ? Color.blue : Color.gray) - .cornerRadius(12) - .disabled(!viewModel.canVerifyPasswordlessCode) +// Button { +// withAnimation { +// viewModel.verifyPasswordlessCode() +// } +// } label: { +// if viewModel.isVerifyingCode { +// ProgressView() +// .frame(maxWidth: .infinity) +// .padding() +// } else { +// Text(NSLocalizedString("Подтвердить вход", comment: "")) +// .bold() +// .frame(maxWidth: .infinity) +// .padding() +// } +// } +// .foregroundColor(.white) +// .background(viewModel.canVerifyPasswordlessCode ? Color.blue : Color.gray) +// .cornerRadius(12) +// .disabled(!viewModel.canVerifyPasswordlessCode) VStack(alignment: .leading, spacing: 8) { Text(NSLocalizedString("Не получили код?", comment: "")) @@ -609,14 +585,14 @@ private struct PasswordlessVerifyView: View { Divider() - Button { - withAnimation { - viewModel.backToPasswordlessRequest() - } - } label: { - Text(NSLocalizedString("Изменить способ входа", comment: "")) - .frame(maxWidth: .infinity) - } +// Button { +// withAnimation { +// viewModel.backToPasswordlessRequest() +// } +// } label: { +// Text(NSLocalizedString("Изменить способ входа", comment: "")) +// .frame(maxWidth: .infinity) +// } Button { withAnimation { @@ -866,7 +842,7 @@ private struct ForgotPasswordInfoView: View { .foregroundColor(.secondary) Button(action: onUseCode) { - Text(NSLocalizedString("Перейти к входу по коду", comment: "")) + Text(NSLocalizedString("Войти", comment: "")) .foregroundColor(.white) .frame(maxWidth: .infinity) .padding() diff --git a/yobble/Views/Login/RegistrationView.swift b/yobble/Views/Login/RegistrationView.swift index 2c3d704..d0fc2a3 100644 --- a/yobble/Views/Login/RegistrationView.swift +++ b/yobble/Views/Login/RegistrationView.swift @@ -11,7 +11,6 @@ struct RegistrationView: View { @ObservedObject var viewModel: LoginViewModel let onShowModePrompt: (() -> Void)? - @State private var username: String = "" @State private var password: String = "" @State private var confirmPassword: String = "" @State private var inviteCode: String = "" @@ -32,7 +31,7 @@ struct RegistrationView: View { private var isUsernameValid: Bool { let pattern = "^[A-Za-z0-9_]{3,32}$" - return username.range(of: pattern, options: .regularExpression) != nil + return viewModel.passwordlessLogin.range(of: pattern, options: .regularExpression) != nil } private var isPasswordValid: Bool { @@ -78,21 +77,16 @@ struct RegistrationView: View { HStack(spacing: 8) { Text("@") .foregroundColor(.secondary) - TextField(NSLocalizedString("Введите логин", comment: "Логин"), text: $username) + TextField(NSLocalizedString("Введите логин", comment: "Логин"), text: $viewModel.passwordlessLogin) .autocapitalization(.none) .disableAutocorrection(true) .focused($focusedField, equals: .username) - .onChange(of: username) { newValue in - if newValue.count > 32 { - username = String(newValue.prefix(32)) - } - } } .padding() .background(Color(.secondarySystemBackground)) .cornerRadius(12) - if !isUsernameValid && !username.isEmpty { + if !isUsernameValid && !viewModel.passwordlessLogin.isEmpty { Text(NSLocalizedString("Логин должен быть от 3 до 32 символов (английские буквы, цифры, _)", comment: "")) .foregroundColor(.red) .font(.caption) @@ -207,7 +201,9 @@ struct RegistrationView: View { private func registerUser() { isLoading = true errorMessage = "" - viewModel.registerUser(username: username, password: password, invite: inviteCode.isEmpty ? nil : inviteCode) { success, message in + let trimmedLogin = viewModel.passwordlessLogin.trimmingCharacters(in: .whitespacesAndNewlines) + viewModel.passwordlessLogin = trimmedLogin + viewModel.registerUser(username: trimmedLogin, password: password, invite: inviteCode.isEmpty ? nil : inviteCode) { success, message in isLoading = false if success { viewModel.hasAcceptedTerms = false