701 lines
29 KiB
Swift
701 lines
29 KiB
Swift
import SwiftUI
|
||
|
||
struct MainWindowView: View {
|
||
@EnvironmentObject var engine: PresenceEngine
|
||
@EnvironmentObject var updater: UpdateManager
|
||
@EnvironmentObject var vpn: VPNManager
|
||
@EnvironmentObject var crypto: CryptoManager
|
||
@EnvironmentObject var lang: LanguageManager
|
||
@State private var selectedTab = 0
|
||
@State private var pendingMnemonic: [String] = []
|
||
|
||
var body: some View {
|
||
Group {
|
||
if crypto.hasIdentity && pendingMnemonic.isEmpty {
|
||
mainAppView
|
||
} else {
|
||
OnboardingView(pendingMnemonic: $pendingMnemonic)
|
||
.environmentObject(crypto)
|
||
.environmentObject(engine)
|
||
.environmentObject(lang)
|
||
}
|
||
}
|
||
.frame(minWidth: 600, minHeight: 500)
|
||
.background(Color(red: 0.04, green: 0.04, blue: 0.04))
|
||
.preferredColorScheme(.dark)
|
||
}
|
||
|
||
@ViewBuilder
|
||
private var mainAppView: some View {
|
||
Group {
|
||
switch selectedTab {
|
||
case 0:
|
||
JunonaView()
|
||
.environmentObject(engine)
|
||
case 1:
|
||
WalletTabView()
|
||
.environmentObject(engine)
|
||
.environmentObject(updater)
|
||
.environmentObject(vpn)
|
||
case 7:
|
||
HistoryView()
|
||
.environmentObject(engine)
|
||
case 8:
|
||
TimeChainExplorerView()
|
||
.environmentObject(engine)
|
||
case 9:
|
||
SettingsView()
|
||
.environmentObject(engine)
|
||
.environmentObject(lang)
|
||
default:
|
||
JunonaView()
|
||
.environmentObject(engine)
|
||
}
|
||
}
|
||
.onReceive(NotificationCenter.default.publisher(for: .switchToSettingsTab)) { _ in
|
||
selectedTab = 9
|
||
}
|
||
.onReceive(NotificationCenter.default.publisher(for: .switchToTab)) { notification in
|
||
if let tab = notification.userInfo?["tab"] as? Int {
|
||
selectedTab = tab
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// ╔══════════════════════════════════════════════════════════════════╗
|
||
// ║ ONBOARDING — Вход в Montana Protocol ║
|
||
// ║ Ключи = вход. Без ключей — нет доступа к протоколу. ║
|
||
// ╚══════════════════════════════════════════════════════════════════╝
|
||
|
||
struct OnboardingView: View {
|
||
@Binding var pendingMnemonic: [String]
|
||
@EnvironmentObject var crypto: CryptoManager
|
||
@EnvironmentObject var engine: PresenceEngine
|
||
@EnvironmentObject var lang: LanguageManager
|
||
@State private var isGenerating = false
|
||
@State private var showRecovery = false
|
||
@State private var showKeychainInfo = false
|
||
@State private var recoveryInput = ""
|
||
@State private var recoveryError = false
|
||
@State private var copied = false
|
||
|
||
// PIN creation state (mandatory before key generation)
|
||
@State private var showPinCreate = false
|
||
@State private var pinConfirmMode = false // false = create, true = confirm
|
||
@State private var pinInput = ""
|
||
@State private var pinFirst = ""
|
||
@State private var pinError = ""
|
||
@State private var pinShake = false
|
||
private let pinLength = 8
|
||
private let pinPadSize: CGFloat = 56
|
||
private let pinPadSpacing: CGFloat = 18
|
||
|
||
private let gold = Color(red: 0.85, green: 0.68, blue: 0.25)
|
||
private let goldLight = Color(red: 0.95, green: 0.82, blue: 0.45)
|
||
private let bg = Color(red: 0.04, green: 0.04, blue: 0.04)
|
||
|
||
// ── Standard safe margins (магниты) ──
|
||
// Ни один элемент не должен прижиматься к краю окна / тайтл-бару
|
||
private let safeTop: CGFloat = 24
|
||
private let safeBottom: CGFloat = 20
|
||
private let safeSide: CGFloat = 20
|
||
|
||
private var appVersion: String {
|
||
Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "3.9"
|
||
}
|
||
private var appBuild: String {
|
||
Bundle.main.infoDictionary?["CFBundleVersion"] as? String ?? "100"
|
||
}
|
||
|
||
var body: some View {
|
||
VStack(spacing: 0) {
|
||
// ── Language pills (top right) ──
|
||
HStack {
|
||
Spacer()
|
||
HStack(spacing: 6) {
|
||
Button(action: { lang.setLanguage(.ru) }) {
|
||
Text("\u{1F1F7}\u{1F1FA}")
|
||
.font(.system(size: 14))
|
||
.padding(.horizontal, 10)
|
||
.padding(.vertical, 5)
|
||
.background(
|
||
Capsule()
|
||
.fill(lang.isRu ? gold.opacity(0.2) : Color.white.opacity(0.04))
|
||
)
|
||
.overlay(
|
||
Capsule()
|
||
.stroke(lang.isRu ? gold.opacity(0.5) : Color.white.opacity(0.08), lineWidth: 1)
|
||
)
|
||
}
|
||
.buttonStyle(.plain)
|
||
|
||
Button(action: { lang.setLanguage(.en) }) {
|
||
Text("\u{1F1EC}\u{1F1E7}")
|
||
.font(.system(size: 14))
|
||
.padding(.horizontal, 10)
|
||
.padding(.vertical, 5)
|
||
.background(
|
||
Capsule()
|
||
.fill(lang.isEn ? gold.opacity(0.2) : Color.white.opacity(0.04))
|
||
)
|
||
.overlay(
|
||
Capsule()
|
||
.stroke(lang.isEn ? gold.opacity(0.5) : Color.white.opacity(0.08), lineWidth: 1)
|
||
)
|
||
}
|
||
.buttonStyle(.plain)
|
||
}
|
||
.padding(.trailing, safeSide)
|
||
.padding(.top, safeTop)
|
||
}
|
||
|
||
Spacer()
|
||
|
||
if !pendingMnemonic.isEmpty {
|
||
mnemonicView
|
||
} else if showPinCreate {
|
||
pinCreateView
|
||
} else if showKeychainInfo {
|
||
keychainInfoView
|
||
} else if showRecovery {
|
||
recoveryView
|
||
} else {
|
||
welcomeView
|
||
}
|
||
|
||
Spacer()
|
||
|
||
// ── Footer with version ──
|
||
HStack {
|
||
Spacer()
|
||
Text("Montana \u{0248} v\(appVersion)")
|
||
.font(.system(size: 10, design: .monospaced))
|
||
.foregroundColor(.white.opacity(0.15))
|
||
Spacer()
|
||
Text("build \(appBuild)")
|
||
.font(.system(size: 10, design: .monospaced))
|
||
.foregroundColor(.white.opacity(0.1))
|
||
Spacer()
|
||
}
|
||
.padding(.bottom, safeBottom)
|
||
}
|
||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||
.background(bg)
|
||
}
|
||
|
||
// MARK: - Welcome
|
||
|
||
private var welcomeView: some View {
|
||
VStack(spacing: 32) {
|
||
if let logoPath = Bundle.main.path(forResource: "JunonaLogo", ofType: "jpg"),
|
||
let nsImage = NSImage(contentsOfFile: logoPath) {
|
||
Image(nsImage: nsImage)
|
||
.resizable()
|
||
.aspectRatio(contentMode: .fill)
|
||
.frame(width: 120, height: 120)
|
||
.clipShape(Circle())
|
||
.overlay(
|
||
Circle()
|
||
.stroke(
|
||
LinearGradient(
|
||
colors: [gold, goldLight],
|
||
startPoint: .topLeading,
|
||
endPoint: .bottomTrailing
|
||
),
|
||
lineWidth: 3
|
||
)
|
||
)
|
||
.shadow(color: gold.opacity(0.4), radius: 30)
|
||
}
|
||
|
||
VStack(spacing: 8) {
|
||
Text(lang.isRu ? "Протокол Монтана" : "Montana Protocol")
|
||
.font(.system(size: 28, weight: .bold))
|
||
.foregroundColor(.white)
|
||
|
||
Text(lang.isRu
|
||
? "\u{041f}\u{043e}\u{0441}\u{0442}\u{043a}\u{0432}\u{0430}\u{043d}\u{0442}\u{043e}\u{0432}\u{0430}\u{044f} \u{043a}\u{0440}\u{0438}\u{043f}\u{0442}\u{043e}\u{0433}\u{0440}\u{0430}\u{0444}\u{0438}\u{044f} ML-DSA-65"
|
||
: "Post-quantum cryptography ML-DSA-65")
|
||
.font(.system(size: 13))
|
||
.foregroundColor(.white.opacity(0.4))
|
||
}
|
||
|
||
VStack(spacing: 12) {
|
||
Button {
|
||
showKeychainInfo = true
|
||
} label: {
|
||
HStack(spacing: 8) {
|
||
Image(systemName: "key.fill")
|
||
.font(.system(size: 14))
|
||
Text(lang.isRu
|
||
? "\u{0421}\u{043e}\u{0437}\u{0434}\u{0430}\u{0442}\u{044c} \u{043a}\u{043b}\u{044e}\u{0447}\u{0438}"
|
||
: "Create Keys")
|
||
.font(.system(size: 15, weight: .semibold))
|
||
}
|
||
.frame(width: 280, height: 44)
|
||
.background(gold)
|
||
.foregroundColor(.black)
|
||
.cornerRadius(10)
|
||
}
|
||
.buttonStyle(.plain)
|
||
|
||
Button {
|
||
showRecovery = true
|
||
} label: {
|
||
HStack(spacing: 8) {
|
||
Image(systemName: "arrow.counterclockwise")
|
||
.font(.system(size: 14))
|
||
Text(lang.isRu
|
||
? "\u{0412}\u{043e}\u{0441}\u{0441}\u{0442}\u{0430}\u{043d}\u{043e}\u{0432}\u{0438}\u{0442}\u{044c} \u{0438}\u{0437} seed-\u{0444}\u{0440}\u{0430}\u{0437}\u{044b}"
|
||
: "Recover from Seed Phrase")
|
||
.font(.system(size: 14))
|
||
}
|
||
.frame(width: 280, height: 44)
|
||
.background(gold.opacity(0.1))
|
||
.foregroundColor(gold)
|
||
.cornerRadius(10)
|
||
.overlay(
|
||
RoundedRectangle(cornerRadius: 10)
|
||
.stroke(gold.opacity(0.3), lineWidth: 1)
|
||
)
|
||
}
|
||
.buttonStyle(.plain)
|
||
}
|
||
}
|
||
}
|
||
|
||
// MARK: - Keychain Explanation
|
||
|
||
private var keychainInfoView: some View {
|
||
VStack(spacing: 28) {
|
||
Image(systemName: "lock.shield.fill")
|
||
.font(.system(size: 50))
|
||
.foregroundColor(gold)
|
||
.shadow(color: gold.opacity(0.4), radius: 20)
|
||
|
||
VStack(spacing: 8) {
|
||
Text(lang.isRu
|
||
? "Безопасное хранение ключей"
|
||
: "Secure Key Storage")
|
||
.font(.system(size: 22, weight: .bold))
|
||
.foregroundColor(.white)
|
||
|
||
Text(lang.isRu
|
||
? "macOS Keychain"
|
||
: "macOS Keychain")
|
||
.font(.system(size: 14, weight: .medium, design: .monospaced))
|
||
.foregroundColor(gold)
|
||
}
|
||
|
||
VStack(alignment: .leading, spacing: 12) {
|
||
keychainInfoRow(
|
||
icon: "lock.fill",
|
||
text: lang.isRu
|
||
? "Ваши ключи ML-DSA-65 будут храниться в macOS Keychain \u{2014} зашифрованном хранилище вашего Mac. Ключи никогда не покидают устройство"
|
||
: "Your ML-DSA-65 keys will be stored in macOS Keychain \u{2014} your Mac's encrypted vault. Keys never leave your device"
|
||
)
|
||
keychainInfoRow(
|
||
icon: "shield.checkered",
|
||
text: lang.isRu
|
||
? "Это тот же механизм, который используют Safari, 1Password и другие приложения для хранения паролей"
|
||
: "This is the same mechanism Safari, 1Password and other apps use to store passwords"
|
||
)
|
||
keychainInfoRow(
|
||
icon: "hand.raised.fill",
|
||
text: lang.isRu
|
||
? "macOS запросит разрешение на доступ к хранилищу. Рекомендуем нажать \u{00ab}Разрешить всегда\u{00bb} для бесперебойной работы"
|
||
: "macOS will ask for vault access permission. We recommend clicking \"Always Allow\" for seamless operation"
|
||
)
|
||
}
|
||
.frame(maxWidth: 440)
|
||
|
||
HStack(spacing: 16) {
|
||
Button {
|
||
showKeychainInfo = false
|
||
} label: {
|
||
Text(lang.isRu ? "Назад" : "Back")
|
||
.frame(width: 140, height: 44)
|
||
.background(Color.white.opacity(0.05))
|
||
.foregroundColor(.white.opacity(0.7))
|
||
.cornerRadius(10)
|
||
}
|
||
.buttonStyle(.plain)
|
||
|
||
Button {
|
||
showKeychainInfo = false
|
||
showPinCreate = true
|
||
pinConfirmMode = false
|
||
pinInput = ""
|
||
pinFirst = ""
|
||
pinError = ""
|
||
} label: {
|
||
HStack(spacing: 8) {
|
||
Image(systemName: "lock.fill")
|
||
.font(.system(size: 14))
|
||
Text(lang.isRu ? "Понятно, создать PIN" : "Got it, create PIN")
|
||
.font(.system(size: 14, weight: .semibold))
|
||
}
|
||
.frame(width: 220, height: 44)
|
||
.background(gold)
|
||
.foregroundColor(.black)
|
||
.cornerRadius(10)
|
||
}
|
||
.buttonStyle(.plain)
|
||
}
|
||
}
|
||
}
|
||
|
||
private func keychainInfoRow(icon: String, text: String) -> some View {
|
||
HStack(alignment: .top, spacing: 12) {
|
||
Image(systemName: icon)
|
||
.font(.system(size: 14))
|
||
.foregroundColor(gold)
|
||
.frame(width: 20, alignment: .center)
|
||
.padding(.top, 2)
|
||
Text(text)
|
||
.font(.system(size: 13))
|
||
.foregroundColor(.white.opacity(0.6))
|
||
.fixedSize(horizontal: false, vertical: true)
|
||
}
|
||
}
|
||
|
||
// MARK: - PIN Creation (Onboarding)
|
||
|
||
private var pinCreateView: some View {
|
||
ZStack {
|
||
Color.black.ignoresSafeArea()
|
||
|
||
VStack(spacing: 0) {
|
||
Spacer(minLength: 20)
|
||
|
||
Image(systemName: "lock.open")
|
||
.font(.system(size: 28, weight: .thin))
|
||
.symbolRenderingMode(.monochrome)
|
||
.foregroundColor(gold)
|
||
.padding(.bottom, 12)
|
||
|
||
Text(pinConfirmMode
|
||
? (lang.isRu ? "Повторите PIN-код" : "Confirm PIN")
|
||
: (lang.isRu ? "Создайте PIN-код" : "Create PIN"))
|
||
.font(.system(size: 18, weight: .semibold))
|
||
.foregroundColor(.white)
|
||
.padding(.bottom, 4)
|
||
|
||
Text(pinConfirmMode
|
||
? (lang.isRu ? "Введите PIN ещё раз" : "Enter PIN again")
|
||
: (lang.isRu ? "8 цифр для защиты кошелька" : "8 digits to protect your wallet"))
|
||
.font(.system(size: 12))
|
||
.foregroundColor(.gray)
|
||
.padding(.bottom, 16)
|
||
|
||
if !pinError.isEmpty {
|
||
Text(pinError)
|
||
.font(.system(size: 12, weight: .medium))
|
||
.foregroundColor(.red)
|
||
.padding(.bottom, 6)
|
||
}
|
||
|
||
HStack(spacing: 14) {
|
||
ForEach(0..<pinLength, id: \.self) { i in
|
||
Circle()
|
||
.fill(i < pinInput.count ? gold : Color.white.opacity(0.2))
|
||
.frame(width: 12, height: 12)
|
||
}
|
||
}
|
||
.offset(x: pinShake ? -10 : 0)
|
||
.padding(.bottom, 28)
|
||
|
||
VStack(spacing: pinPadSpacing) {
|
||
ForEach(0..<3) { row in
|
||
HStack(spacing: pinPadSpacing) {
|
||
ForEach(1...3, id: \.self) { col in
|
||
let num = row * 3 + col
|
||
onboardingPinButton(String(num))
|
||
}
|
||
}
|
||
}
|
||
|
||
HStack(spacing: pinPadSpacing) {
|
||
Button {
|
||
showPinCreate = false
|
||
showKeychainInfo = false
|
||
pinInput = ""
|
||
pinFirst = ""
|
||
pinError = ""
|
||
} label: {
|
||
Text(lang.isRu ? "Отмена" : "Cancel")
|
||
.font(.system(size: 14))
|
||
.foregroundColor(.gray)
|
||
.frame(width: pinPadSize, height: pinPadSize)
|
||
}
|
||
.buttonStyle(.plain)
|
||
|
||
onboardingPinButton("0")
|
||
|
||
Button {
|
||
if !pinInput.isEmpty { pinInput.removeLast() }
|
||
} label: {
|
||
Image(systemName: "delete.backward")
|
||
.font(.system(size: 18))
|
||
.symbolRenderingMode(.monochrome)
|
||
.foregroundColor(.white)
|
||
.frame(width: pinPadSize, height: pinPadSize)
|
||
}
|
||
.buttonStyle(.plain)
|
||
}
|
||
}
|
||
|
||
Spacer(minLength: 20)
|
||
}
|
||
}
|
||
}
|
||
|
||
private func onboardingPinButton(_ digit: String) -> some View {
|
||
Button {
|
||
onboardingPinDigitPressed(digit)
|
||
} label: {
|
||
Text(digit)
|
||
.font(.system(size: 24, weight: .regular, design: .rounded))
|
||
.foregroundColor(.white)
|
||
.frame(width: pinPadSize, height: pinPadSize)
|
||
.background(Circle().fill(Color.white.opacity(0.07)))
|
||
}
|
||
.buttonStyle(.plain)
|
||
}
|
||
|
||
private func onboardingPinDigitPressed(_ digit: String) {
|
||
guard pinInput.count < pinLength else { return }
|
||
pinInput += digit
|
||
pinError = ""
|
||
|
||
if pinInput.count == pinLength {
|
||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.15) {
|
||
handleOnboardingPinComplete()
|
||
}
|
||
}
|
||
}
|
||
|
||
private func handleOnboardingPinComplete() {
|
||
if !pinConfirmMode {
|
||
// First entry — save and ask to confirm
|
||
pinFirst = pinInput
|
||
pinInput = ""
|
||
pinConfirmMode = true
|
||
} else {
|
||
// Confirm entry — check match
|
||
if pinInput == pinFirst {
|
||
// PIN confirmed — generate identity + encrypt
|
||
let pin = pinInput
|
||
isGenerating = true
|
||
Task {
|
||
if let words = crypto.generateIdentity() {
|
||
if crypto.createPin(pin) {
|
||
engine.address = crypto.address
|
||
showPinCreate = false
|
||
pendingMnemonic = words
|
||
} else {
|
||
crypto.deleteIdentity()
|
||
onboardingPinShakeAndReset(lang.isRu ? "Ошибка шифрования" : "Encryption error")
|
||
}
|
||
} else {
|
||
onboardingPinShakeAndReset(lang.isRu ? "Ошибка генерации ключей" : "Key generation error")
|
||
}
|
||
isGenerating = false
|
||
}
|
||
} else {
|
||
// Mismatch — restart
|
||
pinFirst = ""
|
||
onboardingPinShakeAndReset(lang.isRu ? "PIN-коды не совпадают" : "PINs don't match")
|
||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.6) {
|
||
pinConfirmMode = false
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
private func onboardingPinShakeAndReset(_ error: String) {
|
||
pinError = error
|
||
withAnimation(.default.speed(4).repeatCount(3, autoreverses: true)) {
|
||
pinShake = true
|
||
}
|
||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.4) {
|
||
pinShake = false
|
||
pinInput = ""
|
||
}
|
||
}
|
||
|
||
// MARK: - Mnemonic Display
|
||
|
||
private var mnemonicView: some View {
|
||
VStack(spacing: 24) {
|
||
HStack(spacing: 8) {
|
||
Image(systemName: "exclamationmark.triangle.fill")
|
||
.foregroundColor(.orange)
|
||
.font(.system(size: 20))
|
||
Text(lang.isRu
|
||
? "\u{0417}\u{0430}\u{043f}\u{0438}\u{0448}\u{0438}\u{0442}\u{0435} 24 \u{0441}\u{043b}\u{043e}\u{0432}\u{0430}!"
|
||
: "Write Down 24 Words!")
|
||
.font(.system(size: 18, weight: .bold))
|
||
.foregroundColor(.orange)
|
||
}
|
||
|
||
Text(lang.isRu
|
||
? "\u{042d}\u{0442}\u{043e} \u{0435}\u{0434}\u{0438}\u{043d}\u{0441}\u{0442}\u{0432}\u{0435}\u{043d}\u{043d}\u{044b}\u{0439} \u{0441}\u{043f}\u{043e}\u{0441}\u{043e}\u{0431} \u{0432}\u{043e}\u{0441}\u{0441}\u{0442}\u{0430}\u{043d}\u{043e}\u{0432}\u{0438}\u{0442}\u{044c} \u{0434}\u{043e}\u{0441}\u{0442}\u{0443}\u{043f} \u{043a} \u{0432}\u{0430}\u{0448}\u{0435}\u{043c}\u{0443} \u{0430}\u{0434}\u{0440}\u{0435}\u{0441}\u{0443} Montana. \u{0425}\u{0440}\u{0430}\u{043d}\u{0438}\u{0442}\u{0435} \u{0432} \u{0431}\u{0435}\u{0437}\u{043e}\u{043f}\u{0430}\u{0441}\u{043d}\u{043e}\u{043c} \u{043c}\u{0435}\u{0441}\u{0442}\u{0435}."
|
||
: "This is the only way to recover access to your Montana address. Keep it in a safe place.")
|
||
.font(.system(size: 13))
|
||
.foregroundColor(.white.opacity(0.5))
|
||
.multilineTextAlignment(.center)
|
||
.frame(maxWidth: 400)
|
||
|
||
LazyVGrid(columns: [
|
||
GridItem(.flexible()), GridItem(.flexible()), GridItem(.flexible())
|
||
], spacing: 6) {
|
||
ForEach(Array(pendingMnemonic.enumerated()), id: \.offset) { idx, word in
|
||
HStack(spacing: 4) {
|
||
Text("\(idx + 1).")
|
||
.font(.system(size: 11, design: .monospaced))
|
||
.foregroundColor(.white.opacity(0.3))
|
||
.frame(width: 24, alignment: .trailing)
|
||
Text(word)
|
||
.font(.system(size: 13, weight: .medium, design: .monospaced))
|
||
.foregroundColor(gold)
|
||
Spacer()
|
||
}
|
||
.padding(.vertical, 4)
|
||
.padding(.horizontal, 8)
|
||
.background(Color.white.opacity(0.03))
|
||
.cornerRadius(4)
|
||
}
|
||
}
|
||
.frame(maxWidth: 420)
|
||
|
||
HStack(spacing: 16) {
|
||
Button {
|
||
NSPasteboard.general.clearContents()
|
||
NSPasteboard.general.setString(pendingMnemonic.joined(separator: " "), forType: .string)
|
||
copied = true
|
||
DispatchQueue.main.asyncAfter(deadline: .now() + 2) { copied = false }
|
||
} label: {
|
||
HStack(spacing: 6) {
|
||
Image(systemName: copied ? "checkmark" : "doc.on.doc")
|
||
Text(copied
|
||
? (lang.isRu ? "\u{0421}\u{043a}\u{043e}\u{043f}\u{0438}\u{0440}\u{043e}\u{0432}\u{0430}\u{043d}\u{043e}" : "Copied")
|
||
: (lang.isRu ? "\u{041a}\u{043e}\u{043f}\u{0438}\u{0440}\u{043e}\u{0432}\u{0430}\u{0442}\u{044c}" : "Copy"))
|
||
}
|
||
.frame(width: 160, height: 40)
|
||
.background(Color.white.opacity(0.05))
|
||
.foregroundColor(.white.opacity(0.7))
|
||
.cornerRadius(8)
|
||
}
|
||
.buttonStyle(.plain)
|
||
|
||
Button {
|
||
pendingMnemonic = []
|
||
} label: {
|
||
HStack(spacing: 6) {
|
||
Image(systemName: "checkmark.circle.fill")
|
||
Text(lang.isRu
|
||
? "\u{042f} \u{0441}\u{043e}\u{0445}\u{0440}\u{0430}\u{043d}\u{0438}\u{043b}"
|
||
: "I Saved It")
|
||
}
|
||
.frame(width: 160, height: 40)
|
||
.background(gold)
|
||
.foregroundColor(.black)
|
||
.cornerRadius(8)
|
||
}
|
||
.buttonStyle(.plain)
|
||
}
|
||
}
|
||
}
|
||
|
||
// MARK: - Recovery
|
||
|
||
private var recoveryView: some View {
|
||
VStack(spacing: 24) {
|
||
VStack(spacing: 8) {
|
||
Image(systemName: "arrow.counterclockwise.circle.fill")
|
||
.font(.system(size: 40))
|
||
.foregroundColor(gold)
|
||
|
||
Text(lang.isRu
|
||
? "\u{0412}\u{043e}\u{0441}\u{0441}\u{0442}\u{0430}\u{043d}\u{043e}\u{0432}\u{043b}\u{0435}\u{043d}\u{0438}\u{0435}"
|
||
: "Recovery")
|
||
.font(.system(size: 20, weight: .bold))
|
||
.foregroundColor(.white)
|
||
}
|
||
|
||
Text(lang.isRu
|
||
? "\u{0412}\u{0432}\u{0435}\u{0434}\u{0438}\u{0442}\u{0435} 24 \u{0441}\u{043b}\u{043e}\u{0432}\u{0430} seed-\u{0444}\u{0440}\u{0430}\u{0437}\u{044b} \u{0447}\u{0435}\u{0440}\u{0435}\u{0437} \u{043f}\u{0440}\u{043e}\u{0431}\u{0435}\u{043b}:"
|
||
: "Enter 24 seed phrase words separated by spaces:")
|
||
.font(.system(size: 13))
|
||
.foregroundColor(.white.opacity(0.5))
|
||
|
||
TextEditor(text: $recoveryInput)
|
||
.font(.system(size: 13, design: .monospaced))
|
||
.scrollContentBackground(.hidden)
|
||
.padding(12)
|
||
.frame(width: 400, height: 100)
|
||
.background(Color.white.opacity(0.05))
|
||
.cornerRadius(8)
|
||
.overlay(
|
||
RoundedRectangle(cornerRadius: 8)
|
||
.stroke(recoveryError ? Color.red.opacity(0.5) : gold.opacity(0.2), lineWidth: 1)
|
||
)
|
||
|
||
if recoveryError {
|
||
Text(lang.isRu
|
||
? "\u{041d}\u{0435}\u{0432}\u{0435}\u{0440}\u{043d}\u{0430}\u{044f} seed-\u{0444}\u{0440}\u{0430}\u{0437}\u{0430}. \u{041f}\u{0440}\u{043e}\u{0432}\u{0435}\u{0440}\u{044c}\u{0442}\u{0435} \u{0441}\u{043b}\u{043e}\u{0432}\u{0430} \u{0438} \u{043f}\u{043e}\u{043f}\u{0440}\u{043e}\u{0431}\u{0443}\u{0439}\u{0442}\u{0435} \u{0441}\u{043d}\u{043e}\u{0432}\u{0430}."
|
||
: "Invalid seed phrase. Check words and try again.")
|
||
.font(.system(size: 12))
|
||
.foregroundColor(.red)
|
||
}
|
||
|
||
HStack(spacing: 16) {
|
||
Button {
|
||
showRecovery = false
|
||
recoveryInput = ""
|
||
recoveryError = false
|
||
} label: {
|
||
Text(lang.isRu ? "\u{041d}\u{0430}\u{0437}\u{0430}\u{0434}" : "Back")
|
||
.frame(width: 140, height: 40)
|
||
.background(Color.white.opacity(0.05))
|
||
.foregroundColor(.white.opacity(0.7))
|
||
.cornerRadius(8)
|
||
}
|
||
.buttonStyle(.plain)
|
||
|
||
Button {
|
||
let words = recoveryInput.lowercased()
|
||
.trimmingCharacters(in: .whitespacesAndNewlines)
|
||
.split(separator: " ").map(String.init)
|
||
if crypto.recoverIdentity(from: words) {
|
||
showRecovery = false
|
||
recoveryInput = ""
|
||
recoveryError = false
|
||
} else {
|
||
recoveryError = true
|
||
}
|
||
} label: {
|
||
HStack(spacing: 6) {
|
||
Image(systemName: "key.fill")
|
||
Text(lang.isRu
|
||
? "\u{0412}\u{043e}\u{0441}\u{0441}\u{0442}\u{0430}\u{043d}\u{043e}\u{0432}\u{0438}\u{0442}\u{044c}"
|
||
: "Recover")
|
||
}
|
||
.frame(width: 180, height: 40)
|
||
.background(gold)
|
||
.foregroundColor(.black)
|
||
.cornerRadius(8)
|
||
}
|
||
.buttonStyle(.plain)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
extension Notification.Name {
|
||
static let switchToSettingsTab = Notification.Name("switchToSettingsTab")
|
||
static let switchToTab = Notification.Name("switchToTab")
|
||
}
|