iOS new: OnboardingView.swift
This commit is contained in:
parent
e211cc0cc7
commit
23be15b53d
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user