From 23be15b53d8477a942d42c33b2af89112e56dbbf Mon Sep 17 00:00:00 2001 From: efir369999 Date: Tue, 5 May 2026 17:16:19 +0300 Subject: [PATCH] iOS new: OnboardingView.swift --- .../Montana Messenger/OnboardingView.swift | 256 ++++++++++++++++++ 1 file changed, 256 insertions(+) create mode 100644 Montana-iOS/Montana Messenger/Montana Messenger/OnboardingView.swift diff --git a/Montana-iOS/Montana Messenger/Montana Messenger/OnboardingView.swift b/Montana-iOS/Montana Messenger/Montana Messenger/OnboardingView.swift new file mode 100644 index 0000000..994dd35 --- /dev/null +++ b/Montana-iOS/Montana Messenger/Montana Messenger/OnboardingView.swift @@ -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() + } + } +}