selector
This commit is contained in:
parent
92e4c30c7f
commit
f9026ebf87
@ -753,6 +753,9 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"Какой режим попробовать?" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Кастомная" : {
|
"Кастомная" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
@ -1509,6 +1512,9 @@
|
|||||||
},
|
},
|
||||||
"Основной режим находится в ранней разработке (около 10%)." : {
|
"Основной режим находится в ранней разработке (около 10%)." : {
|
||||||
|
|
||||||
|
},
|
||||||
|
"Оставить мессенджер" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Отключить" : {
|
"Отключить" : {
|
||||||
"comment" : "Кнопка подтверждения отключения 2FA"
|
"comment" : "Кнопка подтверждения отключения 2FA"
|
||||||
@ -1749,6 +1755,9 @@
|
|||||||
},
|
},
|
||||||
"Перейдите в раздел \"Настройки > Сменить пароль\" и следуйте инструкциям." : {
|
"Перейдите в раздел \"Настройки > Сменить пароль\" и следуйте инструкциям." : {
|
||||||
"comment" : "FAQ answer: reset password"
|
"comment" : "FAQ answer: reset password"
|
||||||
|
},
|
||||||
|
"По умолчанию включён полный интерфейс. Мессенджер оставляет только общение и готов к работе уже сейчас. Можно переключиться в любой момент." : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Повторите пароль" : {
|
"Повторите пароль" : {
|
||||||
"comment" : "Поле подтверждения пароля на приложение"
|
"comment" : "Поле подтверждения пароля на приложение"
|
||||||
@ -1831,6 +1840,9 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"Позже" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Поиск" : {
|
"Поиск" : {
|
||||||
|
|
||||||
@ -1880,6 +1892,9 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"Полный интерфейс" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Получать коды на email при входе" : {
|
"Получать коды на email при входе" : {
|
||||||
"comment" : "Переключатель отправки кодов при входе"
|
"comment" : "Переключатель отправки кодов при входе"
|
||||||
@ -2268,6 +2283,9 @@
|
|||||||
},
|
},
|
||||||
"Режим мессенжера" : {
|
"Режим мессенжера" : {
|
||||||
|
|
||||||
|
},
|
||||||
|
"Рекомендуем новичкам" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Сборка:" : {
|
"Сборка:" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
@ -2734,6 +2752,9 @@
|
|||||||
},
|
},
|
||||||
"Экспериментальная поддержка iOS 15" : {
|
"Экспериментальная поддержка iOS 15" : {
|
||||||
|
|
||||||
|
},
|
||||||
|
"Экспериментальный режим" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Это устройство" : {
|
"Это устройство" : {
|
||||||
"comment" : "Заголовок секции текущего устройства"
|
"comment" : "Заголовок секции текущего устройства"
|
||||||
|
|||||||
@ -9,22 +9,64 @@ import SwiftUI
|
|||||||
|
|
||||||
struct LoginView: View {
|
struct LoginView: View {
|
||||||
@ObservedObject var viewModel: LoginViewModel
|
@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 {
|
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 {
|
ZStack {
|
||||||
switch viewModel.loginFlowStep {
|
switch viewModel.loginFlowStep {
|
||||||
case .passwordlessRequest:
|
case .passwordlessRequest:
|
||||||
PasswordlessRequestView(viewModel: viewModel)
|
PasswordlessRequestView(viewModel: viewModel, shouldAutofocus: !isShowingMessengerPrompt)
|
||||||
.transition(.move(edge: .trailing).combined(with: .opacity))
|
.transition(.move(edge: .trailing).combined(with: .opacity))
|
||||||
case .passwordlessVerify:
|
case .passwordlessVerify:
|
||||||
PasswordlessVerifyView(viewModel: viewModel)
|
PasswordlessVerifyView(viewModel: viewModel, shouldAutofocus: !isShowingMessengerPrompt)
|
||||||
.transition(.move(edge: .leading).combined(with: .opacity))
|
.transition(.move(edge: .leading).combined(with: .opacity))
|
||||||
case .password:
|
case .password:
|
||||||
PasswordLoginView(viewModel: viewModel)
|
PasswordLoginView(viewModel: viewModel)
|
||||||
.transition(.opacity)
|
.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 {
|
private struct PasswordlessRequestView: View {
|
||||||
@ObservedObject var viewModel: LoginViewModel
|
@ObservedObject var viewModel: LoginViewModel
|
||||||
|
let shouldAutofocus: Bool
|
||||||
@FocusState private var isFieldFocused: Bool
|
@FocusState private var isFieldFocused: Bool
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
@ -432,9 +475,12 @@ private struct PasswordlessRequestView: View {
|
|||||||
.onTapGesture {
|
.onTapGesture {
|
||||||
isFieldFocused = false
|
isFieldFocused = false
|
||||||
}
|
}
|
||||||
.onAppear {
|
.onAppear(perform: scheduleFocusIfNeeded)
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.35) {
|
.onChange(of: shouldAutofocus) { newValue in
|
||||||
isFieldFocused = true
|
if newValue {
|
||||||
|
scheduleFocusIfNeeded()
|
||||||
|
} else {
|
||||||
|
isFieldFocused = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.loginErrorAlert(viewModel: viewModel)
|
.loginErrorAlert(viewModel: viewModel)
|
||||||
@ -444,10 +490,20 @@ private struct PasswordlessRequestView: View {
|
|||||||
guard let url = URL(string: UIApplication.openSettingsURLString) else { return }
|
guard let url = URL(string: UIApplication.openSettingsURLString) else { return }
|
||||||
UIApplication.shared.open(url)
|
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 {
|
private struct PasswordlessVerifyView: View {
|
||||||
@ObservedObject var viewModel: LoginViewModel
|
@ObservedObject var viewModel: LoginViewModel
|
||||||
|
let shouldAutofocus: Bool
|
||||||
@FocusState private var isCodeFieldFocused: Bool
|
@FocusState private var isCodeFieldFocused: Bool
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
@ -458,8 +514,8 @@ private struct PasswordlessVerifyView: View {
|
|||||||
VStack(alignment: .leading, spacing: 8) {
|
VStack(alignment: .leading, spacing: 8) {
|
||||||
Text(NSLocalizedString("Введите код", comment: ""))
|
Text(NSLocalizedString("Введите код", comment: ""))
|
||||||
.font(.largeTitle).bold()
|
.font(.largeTitle).bold()
|
||||||
// Text(String(format: NSLocalizedString("Мы отправили код на %@", comment: ""), viewModel.passwordlessLogin))
|
Text(String(format: NSLocalizedString("Мы отправили код на %@", comment: ""), viewModel.passwordlessLogin))
|
||||||
// .foregroundColor(.secondary)
|
.foregroundColor(.secondary)
|
||||||
}
|
}
|
||||||
|
|
||||||
OTPInputView(code: $viewModel.verificationCode, isFocused: $isCodeFieldFocused)
|
OTPInputView(code: $viewModel.verificationCode, isFocused: $isCodeFieldFocused)
|
||||||
@ -536,9 +592,12 @@ private struct PasswordlessVerifyView: View {
|
|||||||
.onTapGesture {
|
.onTapGesture {
|
||||||
isCodeFieldFocused = true
|
isCodeFieldFocused = true
|
||||||
}
|
}
|
||||||
.onAppear {
|
.onAppear(perform: scheduleFocusIfNeeded)
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.35) {
|
.onChange(of: shouldAutofocus) { newValue in
|
||||||
isCodeFieldFocused = true
|
if newValue {
|
||||||
|
scheduleFocusIfNeeded()
|
||||||
|
} else {
|
||||||
|
isCodeFieldFocused = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.loginErrorAlert(viewModel: viewModel)
|
.loginErrorAlert(viewModel: viewModel)
|
||||||
@ -548,6 +607,15 @@ private struct PasswordlessVerifyView: View {
|
|||||||
guard let url = URL(string: UIApplication.openSettingsURLString) else { return }
|
guard let url = URL(string: UIApplication.openSettingsURLString) else { return }
|
||||||
UIApplication.shared.open(url)
|
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 {
|
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 {
|
private extension View {
|
||||||
func loginErrorAlert(viewModel: LoginViewModel) -> some View {
|
func loginErrorAlert(viewModel: LoginViewModel) -> some View {
|
||||||
alert(isPresented: Binding(
|
alert(isPresented: Binding(
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user