768 lines
29 KiB
Swift
768 lines
29 KiB
Swift
//
|
||
// LoginView.swift
|
||
// VolnahubApp
|
||
//
|
||
// Created by cheykrym on 09/06/2025.
|
||
//
|
||
|
||
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, shouldAutofocus: !isShowingMessengerPrompt)
|
||
.transition(.move(edge: .trailing).combined(with: .opacity))
|
||
case .passwordlessVerify:
|
||
PasswordlessVerifyView(viewModel: viewModel, shouldAutofocus: !isShowingMessengerPrompt)
|
||
.transition(.move(edge: .leading).combined(with: .opacity))
|
||
case .password:
|
||
PasswordLoginView(viewModel: viewModel)
|
||
.transition(.opacity)
|
||
case .registration:
|
||
RegistrationView(viewModel: viewModel)
|
||
.transition(.move(edge: .bottom).combined(with: .opacity))
|
||
}
|
||
}
|
||
}
|
||
|
||
private func applyMessengerModeSelection() {
|
||
isMessengerModeEnabled = pendingMessengerMode
|
||
dismissMessengerPrompt()
|
||
}
|
||
|
||
private func dismissMessengerPrompt() {
|
||
withAnimation {
|
||
isShowingMessengerPrompt = false
|
||
}
|
||
}
|
||
}
|
||
|
||
struct PasswordLoginView: View {
|
||
@ObservedObject var viewModel: LoginViewModel
|
||
@EnvironmentObject private var themeManager: ThemeManager
|
||
@Environment(\.colorScheme) private var colorScheme
|
||
private let themeOptions = ThemeOption.ordered
|
||
@AppStorage("messengerModeEnabled") private var isMessengerModeEnabled: Bool = false
|
||
|
||
@State private var showLegacySupportNotice = false
|
||
@State private var isShowingTerms = false
|
||
@State private var hasResetTermsOnAppear = false
|
||
@FocusState private var focusedField: Field?
|
||
|
||
private enum Field: Hashable {
|
||
case username
|
||
case password
|
||
}
|
||
|
||
private var isUsernameValid: Bool {
|
||
let pattern = "^[A-Za-z0-9_]{3,32}$"
|
||
return viewModel.username.range(of: pattern, options: .regularExpression) != nil
|
||
}
|
||
|
||
private var isPasswordValid: Bool {
|
||
return viewModel.password.count >= 8 && viewModel.password.count <= 128
|
||
}
|
||
|
||
private var isLoginButtonEnabled: Bool {
|
||
// !viewModel.isLoading && isUsernameValid && isPasswordValid && viewModel.hasAcceptedTerms
|
||
!viewModel.isLoading && isUsernameValid && isPasswordValid
|
||
}
|
||
|
||
var body: some View {
|
||
ScrollView(showsIndicators: false) {
|
||
VStack(alignment: .leading, spacing: 24) {
|
||
LoginTopBar(openLanguageSettings: openLanguageSettings)
|
||
|
||
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: ""))
|
||
.font(.largeTitle).bold()
|
||
Text(NSLocalizedString("Если предпочитаете классический вход, используйте логин и пароль.", comment: ""))
|
||
.foregroundColor(.secondary)
|
||
}
|
||
|
||
VStack(alignment: .leading, spacing: 12) {
|
||
TextField(NSLocalizedString("Логин", comment: ""), text: $viewModel.username)
|
||
.padding()
|
||
.background(Color(.secondarySystemBackground))
|
||
.cornerRadius(12)
|
||
.autocapitalization(.none)
|
||
.disableAutocorrection(true)
|
||
.focused($focusedField, equals: .username)
|
||
.onChange(of: viewModel.username) { newValue in
|
||
if newValue.count > 32 {
|
||
viewModel.username = String(newValue.prefix(32))
|
||
}
|
||
}
|
||
|
||
if !isUsernameValid && !viewModel.username.isEmpty {
|
||
Text(NSLocalizedString("Неверный логин", comment: "Неверный логин"))
|
||
.foregroundColor(.red)
|
||
.font(.caption)
|
||
}
|
||
|
||
SecureField(NSLocalizedString("Пароль", comment: ""), text: $viewModel.password)
|
||
.padding()
|
||
.background(Color(.secondarySystemBackground))
|
||
.cornerRadius(12)
|
||
.autocapitalization(.none)
|
||
.focused($focusedField, equals: .password)
|
||
.onChange(of: viewModel.password) { newValue in
|
||
if newValue.count > 32 {
|
||
viewModel.password = String(newValue.prefix(32))
|
||
}
|
||
}
|
||
|
||
if !isPasswordValid && !viewModel.password.isEmpty {
|
||
Text(NSLocalizedString("Неверный пароль", comment: "Неверный пароль"))
|
||
.foregroundColor(.red)
|
||
.font(.caption)
|
||
}
|
||
}
|
||
|
||
VStack(alignment: .leading, spacing: 4) {
|
||
Toggle(NSLocalizedString("Режим мессенжера", comment: ""), isOn: $isMessengerModeEnabled)
|
||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||
Text(isMessengerModeEnabled
|
||
? "Мессенджер-режим сейчас проработан примерно на 60%."
|
||
: "Основной режим находится в ранней разработке (около 10%).")
|
||
.font(.footnote)
|
||
.foregroundColor(.secondary)
|
||
}
|
||
|
||
Button(action: {
|
||
viewModel.login()
|
||
}) {
|
||
if viewModel.isLoading {
|
||
ProgressView()
|
||
.progressViewStyle(CircularProgressViewStyle())
|
||
.frame(maxWidth: .infinity)
|
||
.padding()
|
||
} else {
|
||
Text(NSLocalizedString("Войти", comment: ""))
|
||
.foregroundColor(.white)
|
||
.frame(maxWidth: .infinity)
|
||
.padding()
|
||
}
|
||
}
|
||
.background(isLoginButtonEnabled ? Color.blue : Color.gray)
|
||
.cornerRadius(12)
|
||
.disabled(!isLoginButtonEnabled)
|
||
|
||
Button(action: {
|
||
viewModel.hasAcceptedTerms = false
|
||
withAnimation {
|
||
viewModel.showRegistration()
|
||
}
|
||
}) {
|
||
Text(NSLocalizedString("Нет аккаунта? Регистрация", comment: "Регистрация"))
|
||
.foregroundColor(.blue)
|
||
.frame(maxWidth: .infinity)
|
||
}
|
||
.padding(.top, 4)
|
||
|
||
Spacer(minLength: 0)
|
||
}
|
||
.padding(.vertical, 32)
|
||
}
|
||
.padding(.horizontal, 24)
|
||
.background(Color(.systemBackground).ignoresSafeArea())
|
||
.contentShape(Rectangle())
|
||
.onTapGesture {
|
||
focusedField = nil
|
||
}
|
||
.loginErrorAlert(viewModel: viewModel)
|
||
.onAppear {
|
||
if !hasResetTermsOnAppear {
|
||
viewModel.hasAcceptedTerms = false
|
||
hasResetTermsOnAppear = true
|
||
}
|
||
if shouldShowLegacySupportNotice {
|
||
showLegacySupportNotice = true
|
||
}
|
||
}
|
||
.fullScreenCover(isPresented: $isShowingTerms) {
|
||
TermsFullScreenView(
|
||
isPresented: $isShowingTerms,
|
||
title: NSLocalizedString("Правила сервиса", comment: ""),
|
||
content: viewModel.termsContent,
|
||
isLoading: viewModel.isLoadingTerms,
|
||
errorMessage: viewModel.termsErrorMessage,
|
||
onRetry: {
|
||
viewModel.reloadTerms()
|
||
}
|
||
)
|
||
.onAppear {
|
||
if viewModel.termsContent.isEmpty {
|
||
viewModel.loadTermsIfNeeded()
|
||
}
|
||
}
|
||
}
|
||
.overlay(alignment: .center) {
|
||
if showLegacySupportNotice {
|
||
LegacySupportNoticeView(isPresented: $showLegacySupportNotice)
|
||
.transition(.opacity)
|
||
}
|
||
}
|
||
}
|
||
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 var shouldShowLegacySupportNotice: Bool {
|
||
#if os(iOS)
|
||
let requiredVersion = OperatingSystemVersion(majorVersion: 16, minorVersion: 0, patchVersion: 0)
|
||
return !ProcessInfo.processInfo.isOperatingSystemAtLeast(requiredVersion)
|
||
#else
|
||
return false
|
||
#endif
|
||
}
|
||
|
||
private func openLanguageSettings() {
|
||
guard let url = URL(string: UIApplication.openSettingsURLString) else { return }
|
||
UIApplication.shared.open(url)
|
||
}
|
||
|
||
private struct LegacySupportNoticeView: View {
|
||
@Binding var isPresented: Bool
|
||
|
||
var body: some View {
|
||
ZStack {
|
||
Color.black.opacity(0.5)
|
||
.ignoresSafeArea()
|
||
.onTapGesture {
|
||
isPresented = false
|
||
}
|
||
|
||
VStack(spacing: 16) {
|
||
Image(systemName: "exclamationmark.triangle.fill")
|
||
.font(.system(size: 40, weight: .bold))
|
||
.foregroundColor(.yellow)
|
||
|
||
Text("Экспериментальная поддержка iOS 15")
|
||
.font(.headline)
|
||
.multilineTextAlignment(.center)
|
||
|
||
Text("Поддержка iOS 15 работает в экспериментальном режиме. Для лучшей совместимости требуется iOS 16+.")
|
||
.font(.subheadline)
|
||
.foregroundColor(.secondary)
|
||
.multilineTextAlignment(.center)
|
||
|
||
Button {
|
||
isPresented = false
|
||
} label: {
|
||
Text("Понятно")
|
||
.bold()
|
||
.frame(maxWidth: .infinity)
|
||
.padding()
|
||
.background(Color.blue)
|
||
.foregroundColor(.white)
|
||
.cornerRadius(12)
|
||
}
|
||
}
|
||
.padding(24)
|
||
.background(
|
||
RoundedRectangle(cornerRadius: 20, style: .continuous)
|
||
.fill(Color(.systemBackground))
|
||
)
|
||
.frame(maxWidth: 320)
|
||
.shadow(radius: 10)
|
||
}
|
||
}
|
||
}
|
||
|
||
private var selectedThemeOption: ThemeOption {
|
||
ThemeOption.option(for: themeManager.theme)
|
||
}
|
||
|
||
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 PasswordlessRequestView: View {
|
||
@ObservedObject var viewModel: LoginViewModel
|
||
let shouldAutofocus: Bool
|
||
@FocusState private var isFieldFocused: Bool
|
||
|
||
var body: some View {
|
||
ScrollView(showsIndicators: false) {
|
||
VStack(alignment: .leading, spacing: 24) {
|
||
LoginTopBar(openLanguageSettings: openLanguageSettings)
|
||
|
||
VStack(alignment: .leading, spacing: 8) {
|
||
Text(NSLocalizedString("Вход", comment: ""))
|
||
.font(.largeTitle).bold()
|
||
// Text(NSLocalizedString("Введите логин и мы отправим шестизначный код подтверждения.", comment: ""))
|
||
// .foregroundColor(.secondary)
|
||
}
|
||
|
||
VStack(alignment: .leading, spacing: 8) {
|
||
// Text(NSLocalizedString("Логин", comment: ""))
|
||
// .font(.subheadline)
|
||
// .foregroundColor(.secondary)
|
||
TextField(NSLocalizedString("Введите логин", comment: ""), text: $viewModel.passwordlessLogin)
|
||
.textContentType(.username)
|
||
.keyboardType(.default)
|
||
.autocapitalization(.none)
|
||
.disableAutocorrection(true)
|
||
.padding()
|
||
.background(Color(.secondarySystemBackground))
|
||
.cornerRadius(12)
|
||
.focused($isFieldFocused)
|
||
.onChange(of: viewModel.passwordlessLogin) { newValue in
|
||
if newValue.count > 64 {
|
||
viewModel.passwordlessLogin = String(newValue.prefix(64))
|
||
}
|
||
}
|
||
}
|
||
|
||
Button {
|
||
withAnimation {
|
||
viewModel.requestPasswordlessCode()
|
||
}
|
||
} label: {
|
||
if viewModel.isSendingCode {
|
||
ProgressView()
|
||
.frame(maxWidth: .infinity)
|
||
.padding()
|
||
} else {
|
||
Text(NSLocalizedString("Получить код", comment: ""))
|
||
.bold()
|
||
.frame(maxWidth: .infinity)
|
||
.padding()
|
||
}
|
||
}
|
||
.foregroundColor(.white)
|
||
.background(viewModel.canRequestPasswordlessCode ? Color.blue : Color.gray)
|
||
.cornerRadius(12)
|
||
.disabled(!viewModel.canRequestPasswordlessCode)
|
||
|
||
Divider()
|
||
|
||
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)
|
||
}
|
||
.padding(.horizontal, 24)
|
||
.background(Color(.systemBackground).ignoresSafeArea())
|
||
.contentShape(Rectangle())
|
||
.onTapGesture {
|
||
isFieldFocused = false
|
||
}
|
||
.onAppear(perform: scheduleFocusIfNeeded)
|
||
.onChange(of: shouldAutofocus) { newValue in
|
||
if newValue {
|
||
scheduleFocusIfNeeded()
|
||
} else {
|
||
isFieldFocused = false
|
||
}
|
||
}
|
||
.loginErrorAlert(viewModel: viewModel)
|
||
}
|
||
|
||
private func openLanguageSettings() {
|
||
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 {
|
||
ScrollView(showsIndicators: false) {
|
||
VStack(alignment: .leading, spacing: 24) {
|
||
LoginTopBar(openLanguageSettings: openLanguageSettings)
|
||
|
||
VStack(alignment: .leading, spacing: 8) {
|
||
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)
|
||
|
||
VStack(alignment: .leading, spacing: 8) {
|
||
Text(NSLocalizedString("Не получили код?", comment: ""))
|
||
.font(.subheadline)
|
||
if viewModel.resendSecondsRemaining > 0 {
|
||
Text(String(format: NSLocalizedString("Попробовать снова можно через %d сек", comment: ""), viewModel.resendSecondsRemaining))
|
||
.foregroundColor(.secondary)
|
||
}
|
||
|
||
Button {
|
||
withAnimation {
|
||
viewModel.resendPasswordlessCode()
|
||
}
|
||
} label: {
|
||
if viewModel.isSendingCode {
|
||
ProgressView()
|
||
.padding(.vertical, 8)
|
||
} else {
|
||
Text(NSLocalizedString("Отправить код ещё раз", comment: ""))
|
||
}
|
||
}
|
||
.disabled(viewModel.resendSecondsRemaining > 0 || viewModel.isSendingCode)
|
||
}
|
||
|
||
Divider()
|
||
|
||
Button {
|
||
withAnimation {
|
||
viewModel.backToPasswordlessRequest()
|
||
}
|
||
} label: {
|
||
Text(NSLocalizedString("Изменить способ входа", comment: ""))
|
||
.frame(maxWidth: .infinity)
|
||
}
|
||
|
||
Button {
|
||
withAnimation {
|
||
viewModel.showPasswordLogin()
|
||
}
|
||
} label: {
|
||
Text(NSLocalizedString("Войти по паролю", comment: ""))
|
||
.frame(maxWidth: .infinity)
|
||
}
|
||
}
|
||
.padding(.vertical, 32)
|
||
}
|
||
.padding(.horizontal, 24)
|
||
.background(Color(.systemBackground).ignoresSafeArea())
|
||
.contentShape(Rectangle())
|
||
.onTapGesture {
|
||
isCodeFieldFocused = true
|
||
}
|
||
.onAppear(perform: scheduleFocusIfNeeded)
|
||
.onChange(of: shouldAutofocus) { newValue in
|
||
if newValue {
|
||
scheduleFocusIfNeeded()
|
||
} else {
|
||
isCodeFieldFocused = false
|
||
}
|
||
}
|
||
.loginErrorAlert(viewModel: viewModel)
|
||
}
|
||
|
||
private func openLanguageSettings() {
|
||
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 {
|
||
@Binding var code: String
|
||
var length: Int = 6
|
||
let isFocused: FocusState<Bool>.Binding
|
||
|
||
var body: some View {
|
||
ZStack {
|
||
HStack(spacing: 12) {
|
||
ForEach(0..<length, id: \.self) { index in
|
||
Text(symbol(at: index))
|
||
.font(.title2.monospacedDigit())
|
||
.frame(width: 48, height: 56)
|
||
.background(
|
||
RoundedRectangle(cornerRadius: 12)
|
||
.stroke(borderColor(for: index), lineWidth: 1.5)
|
||
)
|
||
}
|
||
}
|
||
|
||
TextField("", text: textBinding)
|
||
.keyboardType(.numberPad)
|
||
.textContentType(.oneTimeCode)
|
||
.focused(isFocused)
|
||
.frame(width: 0, height: 0)
|
||
.opacity(0.01)
|
||
}
|
||
.padding(.vertical, 8)
|
||
.contentShape(Rectangle())
|
||
.onTapGesture {
|
||
isFocused.wrappedValue = true
|
||
}
|
||
}
|
||
|
||
private var textBinding: Binding<String> {
|
||
Binding(
|
||
get: { code },
|
||
set: { newValue in
|
||
let filtered = newValue.filter { $0.isNumber }
|
||
code = String(filtered.prefix(length))
|
||
}
|
||
)
|
||
}
|
||
|
||
private func symbol(at index: Int) -> String {
|
||
guard index < code.count else { return "" }
|
||
let idx = code.index(code.startIndex, offsetBy: index)
|
||
return String(code[idx])
|
||
}
|
||
|
||
private func borderColor(for index: Int) -> Color {
|
||
if index == code.count && code.count < length {
|
||
return .blue
|
||
}
|
||
return .gray.opacity(0.6)
|
||
}
|
||
}
|
||
|
||
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: false
|
||
)
|
||
|
||
optionButton(
|
||
title: NSLocalizedString("Только чаты", comment: ""),
|
||
subtitle: NSLocalizedString("Минимум отвлечений, чистый мессенджер", comment: ""),
|
||
isMessenger: true
|
||
)
|
||
}
|
||
|
||
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(
|
||
get: { viewModel.showError },
|
||
set: { viewModel.showError = $0 }
|
||
)) {
|
||
Alert(
|
||
title: Text(NSLocalizedString("Ошибка авторизации", comment: "")),
|
||
message: Text(viewModel.errorMessage.isEmpty ? NSLocalizedString("Произошла ошибка.", comment: "") : viewModel.errorMessage),
|
||
dismissButton: .default(Text(NSLocalizedString("OK", comment: "")))
|
||
)
|
||
}
|
||
}
|
||
}
|
||
|
||
struct LoginView_Previews: PreviewProvider {
|
||
static var previews: some View {
|
||
Group {
|
||
preview(step: .passwordlessRequest)
|
||
preview(step: .passwordlessVerify)
|
||
preview(step: .password)
|
||
preview(step: .registration)
|
||
}
|
||
.environmentObject(ThemeManager())
|
||
}
|
||
|
||
private static func preview(step: LoginViewModel.LoginFlowStep) -> some View {
|
||
let viewModel = LoginViewModel()
|
||
viewModel.isLoading = false
|
||
viewModel.loginFlowStep = step
|
||
viewModel.passwordlessLogin = "preview@yobble.app"
|
||
viewModel.verificationCode = "123456"
|
||
return LoginView(viewModel: viewModel)
|
||
}
|
||
}
|