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