This commit is contained in:
cheykrym 2025-12-03 07:05:41 +03:00
parent 92e4c30c7f
commit f9026ebf87
2 changed files with 192 additions and 11 deletions

View File

@ -753,6 +753,9 @@
}
}
}
},
"Какой режим попробовать?" : {
},
"Кастомная" : {
"localizations" : {
@ -1509,6 +1512,9 @@
},
"Основной режим находится в ранней разработке (около 10%)." : {
},
"Оставить мессенджер" : {
},
"Отключить" : {
"comment" : "Кнопка подтверждения отключения 2FA"
@ -1749,6 +1755,9 @@
},
"Перейдите в раздел \"Настройки > Сменить пароль\" и следуйте инструкциям." : {
"comment" : "FAQ answer: reset password"
},
"По умолчанию включён полный интерфейс. Мессенджер оставляет только общение и готов к работе уже сейчас. Можно переключиться в любой момент." : {
},
"Повторите пароль" : {
"comment" : "Поле подтверждения пароля на приложение"
@ -1831,6 +1840,9 @@
}
}
}
},
"Позже" : {
},
"Поиск" : {
@ -1880,6 +1892,9 @@
}
}
}
},
"Полный интерфейс" : {
},
"Получать коды на email при входе" : {
"comment" : "Переключатель отправки кодов при входе"
@ -2268,6 +2283,9 @@
},
"Режим мессенжера" : {
},
"Рекомендуем новичкам" : {
},
"Сборка:" : {
"localizations" : {
@ -2734,6 +2752,9 @@
},
"Экспериментальная поддержка iOS 15" : {
},
"Экспериментальный режим" : {
},
"Это устройство" : {
"comment" : "Заголовок секции текущего устройства"

View File

@ -9,22 +9,64 @@ import SwiftUI
struct LoginView: View {
@ObservedObject var viewModel: LoginViewModel
@AppStorage("messengerModeEnabled") private var isMessengerModeEnabled: Bool = false
@State private var isShowingMessengerPrompt: Bool = true
@State private var pendingMessengerMode: Bool = UserDefaults.standard.bool(forKey: "messengerModeEnabled")
var body: some View {
ZStack {
content
.animation(.easeInOut(duration: 0.25), value: viewModel.loginFlowStep)
.allowsHitTesting(!isShowingMessengerPrompt)
.blur(radius: isShowingMessengerPrompt ? 3 : 0)
if isShowingMessengerPrompt {
Color.black.opacity(0.35)
.ignoresSafeArea()
.transition(.opacity)
MessengerModePrompt(
selection: $pendingMessengerMode,
onAccept: applyMessengerModeSelection,
onSkip: dismissMessengerPrompt
)
.padding(.horizontal, 24)
.transition(.scale.combined(with: .opacity))
}
}
.onAppear {
pendingMessengerMode = isMessengerModeEnabled
withAnimation {
isShowingMessengerPrompt = true
}
}
}
private var content: some View {
ZStack {
switch viewModel.loginFlowStep {
case .passwordlessRequest:
PasswordlessRequestView(viewModel: viewModel)
PasswordlessRequestView(viewModel: viewModel, shouldAutofocus: !isShowingMessengerPrompt)
.transition(.move(edge: .trailing).combined(with: .opacity))
case .passwordlessVerify:
PasswordlessVerifyView(viewModel: viewModel)
PasswordlessVerifyView(viewModel: viewModel, shouldAutofocus: !isShowingMessengerPrompt)
.transition(.move(edge: .leading).combined(with: .opacity))
case .password:
PasswordLoginView(viewModel: viewModel)
.transition(.opacity)
}
}
.animation(.easeInOut(duration: 0.25), value: viewModel.loginFlowStep)
}
private func applyMessengerModeSelection() {
isMessengerModeEnabled = pendingMessengerMode
dismissMessengerPrompt()
}
private func dismissMessengerPrompt() {
withAnimation {
isShowingMessengerPrompt = false
}
}
}
@ -352,6 +394,7 @@ struct PasswordLoginView: View {
private struct PasswordlessRequestView: View {
@ObservedObject var viewModel: LoginViewModel
let shouldAutofocus: Bool
@FocusState private var isFieldFocused: Bool
var body: some View {
@ -432,9 +475,12 @@ private struct PasswordlessRequestView: View {
.onTapGesture {
isFieldFocused = false
}
.onAppear {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.35) {
isFieldFocused = true
.onAppear(perform: scheduleFocusIfNeeded)
.onChange(of: shouldAutofocus) { newValue in
if newValue {
scheduleFocusIfNeeded()
} else {
isFieldFocused = false
}
}
.loginErrorAlert(viewModel: viewModel)
@ -444,10 +490,20 @@ private struct PasswordlessRequestView: View {
guard let url = URL(string: UIApplication.openSettingsURLString) else { return }
UIApplication.shared.open(url)
}
private func scheduleFocusIfNeeded() {
guard shouldAutofocus else { return }
DispatchQueue.main.asyncAfter(deadline: .now() + 0.35) {
if shouldAutofocus {
isFieldFocused = true
}
}
}
}
private struct PasswordlessVerifyView: View {
@ObservedObject var viewModel: LoginViewModel
let shouldAutofocus: Bool
@FocusState private var isCodeFieldFocused: Bool
var body: some View {
@ -458,8 +514,8 @@ private struct PasswordlessVerifyView: View {
VStack(alignment: .leading, spacing: 8) {
Text(NSLocalizedString("Введите код", comment: ""))
.font(.largeTitle).bold()
// Text(String(format: NSLocalizedString("Мы отправили код на %@", comment: ""), viewModel.passwordlessLogin))
// .foregroundColor(.secondary)
Text(String(format: NSLocalizedString("Мы отправили код на %@", comment: ""), viewModel.passwordlessLogin))
.foregroundColor(.secondary)
}
OTPInputView(code: $viewModel.verificationCode, isFocused: $isCodeFieldFocused)
@ -536,9 +592,12 @@ private struct PasswordlessVerifyView: View {
.onTapGesture {
isCodeFieldFocused = true
}
.onAppear {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.35) {
isCodeFieldFocused = true
.onAppear(perform: scheduleFocusIfNeeded)
.onChange(of: shouldAutofocus) { newValue in
if newValue {
scheduleFocusIfNeeded()
} else {
isCodeFieldFocused = false
}
}
.loginErrorAlert(viewModel: viewModel)
@ -548,6 +607,15 @@ private struct PasswordlessVerifyView: View {
guard let url = URL(string: UIApplication.openSettingsURLString) else { return }
UIApplication.shared.open(url)
}
private func scheduleFocusIfNeeded() {
guard shouldAutofocus else { return }
DispatchQueue.main.asyncAfter(deadline: .now() + 0.35) {
if shouldAutofocus {
isCodeFieldFocused = true
}
}
}
}
private struct OTPInputView: View {
@ -673,6 +741,98 @@ private struct LoginTopBar: View {
}
}
private struct MessengerModePrompt: View {
@Binding var selection: Bool
let onAccept: () -> Void
let onSkip: () -> Void
var body: some View {
VStack(spacing: 20) {
Text(NSLocalizedString("Какой режим попробовать?", comment: ""))
.font(.title3.bold())
.multilineTextAlignment(.center)
Text(NSLocalizedString("По умолчанию включён полный интерфейс. Мессенджер оставляет только общение и готов к работе уже сейчас. Можно переключиться в любой момент.", comment: ""))
.font(.callout)
.foregroundColor(.secondary)
.multilineTextAlignment(.center)
VStack(spacing: 12) {
optionButton(
title: NSLocalizedString("Оставить мессенджер", comment: ""),
subtitle: NSLocalizedString("Рекомендуем новичкам", comment: ""),
isMessenger: true
)
optionButton(
title: NSLocalizedString("Полный интерфейс", comment: ""),
subtitle: NSLocalizedString("Экспериментальный режим", comment: ""),
isMessenger: false
)
}
HStack(spacing: 12) {
Button(action: onSkip) {
Text(NSLocalizedString("Позже", comment: ""))
.font(.callout)
.frame(maxWidth: .infinity)
.padding()
.background(
RoundedRectangle(cornerRadius: 14, style: .continuous)
.stroke(Color.secondary.opacity(0.3))
)
}
Button(action: onAccept) {
Text(NSLocalizedString("Применить", comment: ""))
.font(.callout.bold())
.foregroundColor(.white)
.frame(maxWidth: .infinity)
.padding()
.background(
RoundedRectangle(cornerRadius: 14, style: .continuous)
.fill(Color.accentColor)
)
}
}
}
.padding(24)
.background(
RoundedRectangle(cornerRadius: 24, style: .continuous)
.fill(Color(.systemBackground))
)
.shadow(color: Color.black.opacity(0.2), radius: 30, x: 0, y: 12)
}
private func optionButton(title: String, subtitle: String, isMessenger: Bool) -> some View {
Button {
selection = isMessenger
} label: {
VStack(alignment: .leading, spacing: 4) {
HStack {
Text(title)
.font(.headline)
Spacer()
if selection == isMessenger {
Image(systemName: "checkmark.circle.fill")
.foregroundColor(.accentColor)
}
}
Text(subtitle)
.font(.footnote)
.foregroundColor(.secondary)
}
.padding()
.frame(maxWidth: .infinity)
.background(
RoundedRectangle(cornerRadius: 16, style: .continuous)
.fill(selection == isMessenger ? Color.accentColor.opacity(0.15) : Color(.secondarySystemBackground))
)
}
.buttonStyle(.plain)
}
}
private extension View {
func loginErrorAlert(viewModel: LoginViewModel) -> some View {
alert(isPresented: Binding(