selector
This commit is contained in:
parent
92e4c30c7f
commit
f9026ebf87
@ -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" : "Заголовок секции текущего устройства"
|
||||
|
||||
@ -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(
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user