iOS new: OnboardingView.swift

This commit is contained in:
efir369999 2026-05-05 17:16:19 +03:00
parent e211cc0cc7
commit 23be15b53d

View File

@ -0,0 +1,256 @@
import SwiftUI
struct OnboardingView: View {
@EnvironmentObject private var identity: IdentityManager
@State private var name: String = ""
@State private var mode: Mode = .welcome
@State private var edSeed: String = ""
@State private var xSeed: String = ""
@State private var error: String?
@State private var working = false
@FocusState private var nameFocused: Bool
enum Mode { case welcome, create, restore }
var body: some View {
ZStack {
ClaudeTheme.Palette.canvas.ignoresSafeArea()
VStack(spacing: 0) {
switch mode {
case .welcome: welcome
case .create: createForm
case .restore: restoreForm
}
}
.padding(.horizontal, ClaudeTheme.Spacing.xl)
}
.preferredColorScheme(.dark)
}
private var welcome: some View {
VStack(spacing: ClaudeTheme.Spacing.xl) {
Spacer()
Text("Ӂ")
.font(.system(size: 96, weight: .semibold, design: .serif))
.foregroundStyle(ClaudeTheme.Palette.goldGradient)
VStack(spacing: 8) {
Text("MONTANA")
.font(.system(size: 32, weight: .bold, design: .serif))
.tracking(8)
.foregroundStyle(ClaudeTheme.Palette.textPrimary)
Text("Никто не читает твои сообщения.")
.font(.system(size: 14, design: .serif))
.foregroundStyle(ClaudeTheme.Palette.textSecondary)
.multilineTextAlignment(.center)
}
Spacer()
VStack(spacing: 12) {
Button { mode = .create } label: {
Text("Создать идентичность")
.font(ClaudeTheme.Typography.headline)
.foregroundStyle(ClaudeTheme.Palette.bubbleOutText)
.frame(maxWidth: .infinity)
.padding(.vertical, 16)
.background(
RoundedRectangle(cornerRadius: ClaudeTheme.Radius.md, style: .continuous)
.fill(ClaudeTheme.Palette.goldGradient)
)
}
Button { mode = .restore } label: {
Text("Восстановить из ключа")
.font(ClaudeTheme.Typography.headline)
.foregroundStyle(ClaudeTheme.Palette.gold)
.frame(maxWidth: .infinity)
.padding(.vertical, 16)
.background(
RoundedRectangle(cornerRadius: ClaudeTheme.Radius.md, style: .continuous)
.fill(ClaudeTheme.Palette.surface)
)
.overlay(
RoundedRectangle(cornerRadius: ClaudeTheme.Radius.md, style: .continuous)
.strokeBorder(ClaudeTheme.Palette.goldHairline, lineWidth: 1)
)
}
}
.padding(.bottom, 48)
}
}
private var createForm: some View {
VStack(alignment: .leading, spacing: ClaudeTheme.Spacing.lg) {
HStack {
Button { mode = .welcome } label: {
Image(systemName: "chevron.left")
.foregroundStyle(ClaudeTheme.Palette.gold)
.font(.system(size: 18, weight: .semibold))
}
Spacer()
}
.padding(.top, ClaudeTheme.Spacing.lg)
Text("Как тебя зовут?")
.font(.system(size: 28, weight: .semibold, design: .serif))
.foregroundStyle(ClaudeTheme.Palette.textPrimary)
.padding(.top, ClaudeTheme.Spacing.lg)
Text("Имя видят только те, с кем ты общаешься. Можешь поменять позже.")
.font(ClaudeTheme.Typography.callout)
.foregroundStyle(ClaudeTheme.Palette.textTertiary)
TextField("Имя", text: $name)
.font(ClaudeTheme.Typography.body)
.foregroundStyle(ClaudeTheme.Palette.textPrimary)
.focused($nameFocused)
.padding(14)
.background(
RoundedRectangle(cornerRadius: ClaudeTheme.Radius.md, style: .continuous)
.fill(ClaudeTheme.Palette.surface)
)
.overlay(
RoundedRectangle(cornerRadius: ClaudeTheme.Radius.md, style: .continuous)
.strokeBorder(nameFocused ? ClaudeTheme.Palette.gold : ClaudeTheme.Palette.goldHairline, lineWidth: 1)
)
.padding(.top, 4)
if let error {
Text(error)
.font(ClaudeTheme.Typography.caption)
.foregroundStyle(ClaudeTheme.Palette.danger)
}
Spacer()
Button { Task { await createIdentity() } } label: {
if working {
ProgressView().tint(ClaudeTheme.Palette.bubbleOutText)
.frame(maxWidth: .infinity)
.padding(.vertical, 16)
.background(
RoundedRectangle(cornerRadius: ClaudeTheme.Radius.md, style: .continuous)
.fill(ClaudeTheme.Palette.goldGradient)
)
} else {
Text("Создать")
.font(ClaudeTheme.Typography.headline)
.foregroundStyle(ClaudeTheme.Palette.bubbleOutText)
.frame(maxWidth: .infinity)
.padding(.vertical, 16)
.background(
RoundedRectangle(cornerRadius: ClaudeTheme.Radius.md, style: .continuous)
.fill(name.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
? AnyShapeStyle(ClaudeTheme.Palette.surface)
: AnyShapeStyle(ClaudeTheme.Palette.goldGradient))
)
}
}
.disabled(name.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty || working)
.padding(.bottom, 48)
}
.onAppear { nameFocused = true }
}
private var restoreForm: some View {
ScrollView {
VStack(alignment: .leading, spacing: ClaudeTheme.Spacing.lg) {
HStack {
Button { mode = .welcome } label: {
Image(systemName: "chevron.left")
.foregroundStyle(ClaudeTheme.Palette.gold)
.font(.system(size: 18, weight: .semibold))
}
Spacer()
}
.padding(.top, ClaudeTheme.Spacing.lg)
Text("Восстановить идентичность")
.font(.system(size: 26, weight: .semibold, design: .serif))
.foregroundStyle(ClaudeTheme.Palette.textPrimary)
Text("Вставь оба ключа из «Настройки → Account ID» с другого устройства.")
.font(ClaudeTheme.Typography.callout)
.foregroundStyle(ClaudeTheme.Palette.textTertiary)
TextField("Имя", text: $name)
.padding(14)
.background(RoundedRectangle(cornerRadius: ClaudeTheme.Radius.md).fill(ClaudeTheme.Palette.surface))
.overlay(RoundedRectangle(cornerRadius: ClaudeTheme.Radius.md).strokeBorder(ClaudeTheme.Palette.goldHairline, lineWidth: 1))
TextField("ed_seed (base64)", text: $edSeed)
.font(ClaudeTheme.Typography.mono)
.padding(14)
.background(RoundedRectangle(cornerRadius: ClaudeTheme.Radius.md).fill(ClaudeTheme.Palette.surface))
.overlay(RoundedRectangle(cornerRadius: ClaudeTheme.Radius.md).strokeBorder(ClaudeTheme.Palette.goldHairline, lineWidth: 1))
.autocapitalization(.none)
.disableAutocorrection(true)
TextField("x_seed (base64)", text: $xSeed)
.font(ClaudeTheme.Typography.mono)
.padding(14)
.background(RoundedRectangle(cornerRadius: ClaudeTheme.Radius.md).fill(ClaudeTheme.Palette.surface))
.overlay(RoundedRectangle(cornerRadius: ClaudeTheme.Radius.md).strokeBorder(ClaudeTheme.Palette.goldHairline, lineWidth: 1))
.autocapitalization(.none)
.disableAutocorrection(true)
if let error {
Text(error)
.font(ClaudeTheme.Typography.caption)
.foregroundStyle(ClaudeTheme.Palette.danger)
}
Button { Task { await restoreIdentity() } } label: {
Text(working ? "Восстановление..." : "Восстановить")
.font(ClaudeTheme.Typography.headline)
.foregroundStyle(ClaudeTheme.Palette.bubbleOutText)
.frame(maxWidth: .infinity)
.padding(.vertical, 16)
.background(
RoundedRectangle(cornerRadius: ClaudeTheme.Radius.md, style: .continuous)
.fill(canRestore
? AnyShapeStyle(ClaudeTheme.Palette.goldGradient)
: AnyShapeStyle(ClaudeTheme.Palette.surface))
)
}
.disabled(!canRestore || working)
.padding(.top, 12)
.padding(.bottom, 48)
}
}
}
private var canRestore: Bool {
!name.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty &&
!edSeed.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty &&
!xSeed.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
}
@MainActor
private func createIdentity() async {
let trimmed = name.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmed.isEmpty else { return }
working = true; error = nil
defer { working = false }
let id = identity.create(name: trimmed)
do {
try await ApiClient.shared.register(name: trimmed, identity: id)
} catch {
self.error = "Не удалось зарегистрироваться: \(error.localizedDescription)"
identity.wipe()
}
}
@MainActor
private func restoreIdentity() async {
let trimmed = name.trimmingCharacters(in: .whitespacesAndNewlines)
let ed = edSeed.trimmingCharacters(in: .whitespacesAndNewlines)
let x = xSeed.trimmingCharacters(in: .whitespacesAndNewlines)
working = true; error = nil
defer { working = false }
do {
let id = try identity.restore(edSeedB64: ed, xSeedB64: x, name: trimmed)
try await ApiClient.shared.register(name: trimmed, identity: id)
} catch {
self.error = error.localizedDescription
identity.wipe()
}
}
}