Files
mobile-ios/Rosetta/Features/Auth/ConfirmSeedPhraseView.swift
senseiGai 196765f038 Откат случайно включённых изменений дизайн-системы
Предыдущий коммит случайно включил изменения из рабочей
директории: упрощение GlassModifier, GlassModifiers,
RosettaTabBar, ButtonStyles, GlassCard и других файлов,
что сломало iOS 26 glass-эффекты и внешний вид tab bar.

Восстановлены оригинальные файлы из состояния до этих изменений.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 05:23:09 +05:00

292 lines
10 KiB
Swift

import SwiftUI
struct ConfirmSeedPhraseView: View {
let seedPhrase: [String]
let onConfirmed: () -> Void
let onBack: () -> Void
@State private var confirmationInputs: [String] = Array(repeating: "", count: 4)
@State private var showError = false
@State private var showPasteSuccess = false
@FocusState private var focusedInputIndex: Int?
private let confirmPositions = [1, 4, 8, 11]
private var allCorrect: Bool {
for (inputIndex, seedIndex) in confirmPositions.enumerated() {
let input = confirmationInputs[inputIndex].lowercased().trimmingCharacters(in: .whitespaces)
if input != seedPhrase[seedIndex].lowercased() { return false }
}
return true
}
var body: some View {
VStack(spacing: 0) {
AuthNavigationBar(onBack: onBack)
ScrollView(showsIndicators: false) {
VStack(spacing: 24) {
headerSection
pasteButton
pasteSuccessMessage
wordGrid
errorMessage
}
.padding(.horizontal, 24)
.padding(.top, 16)
.padding(.bottom, 100)
}
.scrollDismissesKeyboard(.interactively)
.onTapGesture(count: 1) { focusedInputIndex = nil }
.simultaneousGesture(TapGesture().onEnded {})
confirmButton
.padding(.horizontal, 24)
.padding(.bottom, 16)
}
}
}
// MARK: - Header
private extension ConfirmSeedPhraseView {
var headerSection: some View {
VStack(spacing: 12) {
Text("Confirm Backup")
.font(.system(size: 28, weight: .bold))
.foregroundStyle(.white)
Text("Enter words #2, #5, #9, #12 to confirm\nyou've backed up your phrase.")
.font(.system(size: 15))
.foregroundStyle(Color.white.opacity(0.7))
.multilineTextAlignment(.center)
.lineSpacing(3)
}
.accessibilityElement(children: .combine)
}
}
// MARK: - Word Grid
private extension ConfirmSeedPhraseView {
var wordGrid: some View {
HStack(alignment: .top, spacing: 12) {
VStack(spacing: 10) {
ForEach(0..<6, id: \.self) { index in
wordRow(seedIndex: index, displayNumber: index + 1)
.staggeredAppearance(key: "confirmSeed", index: index, baseDelay: 0.2, stagger: 0.04)
}
}
VStack(spacing: 10) {
ForEach(6..<12, id: \.self) { index in
wordRow(seedIndex: index, displayNumber: index + 1)
.staggeredAppearance(key: "confirmSeed", index: index, baseDelay: 0.2, stagger: 0.04)
}
}
}
}
@ViewBuilder
func wordRow(seedIndex: Int, displayNumber: Int) -> some View {
if let inputIndex = confirmPositions.firstIndex(of: seedIndex) {
editableWordRow(number: displayNumber, inputIndex: inputIndex, expected: seedPhrase[seedIndex])
} else {
readOnlyWordRow(number: displayNumber, word: seedPhrase[seedIndex])
}
}
func readOnlyWordRow(number: Int, word: String) -> some View {
HStack(spacing: 8) {
Text("\(number).")
.font(.system(size: 15))
.foregroundStyle(RosettaColors.numberGray)
.frame(width: 28, alignment: .trailing)
Text(word)
.font(.system(size: 17, weight: .semibold, design: .monospaced))
.foregroundStyle(RosettaColors.tertiaryText)
Spacer()
}
.padding(.horizontal, 16)
.padding(.vertical, 14)
.background {
RoundedRectangle(cornerRadius: 12)
.fill(RosettaColors.cardFill)
}
.accessibilityLabel("Word \(number): \(word)")
}
func editableWordRow(number: Int, inputIndex: Int, expected: String) -> some View {
let text = confirmationInputs[inputIndex].lowercased().trimmingCharacters(in: .whitespaces)
let isCorrect = !text.isEmpty && text == expected.lowercased()
let isWrong = !text.isEmpty && text != expected.lowercased()
return HStack(spacing: 8) {
Text("\(number).")
.font(.system(size: 15))
.foregroundStyle(RosettaColors.numberGray)
.frame(width: 28, alignment: .trailing)
TextField("enter word", text: $confirmationInputs[inputIndex])
.font(.system(size: 17, weight: .semibold, design: .monospaced))
.foregroundStyle(.white)
.autocorrectionDisabled()
.textInputAutocapitalization(.never)
.focused($focusedInputIndex, equals: inputIndex)
.onChange(of: confirmationInputs[inputIndex]) { _, newValue in
confirmationInputs[inputIndex] = newValue.lowercased()
if showError { withAnimation { showError = false } }
}
Spacer()
validationIcon(isCorrect: isCorrect, isWrong: isWrong)
}
.padding(.horizontal, 16)
.padding(.vertical, 14)
.background {
inputBorder(isCorrect: isCorrect, isWrong: isWrong, isFocused: focusedInputIndex == inputIndex)
}
.accessibilityLabel("Enter word \(number)")
.accessibilityValue(confirmationInputs[inputIndex].isEmpty ? "Empty" : confirmationInputs[inputIndex])
}
@ViewBuilder
func validationIcon(isCorrect: Bool, isWrong: Bool) -> some View {
if isCorrect {
Image(systemName: "checkmark.circle.fill")
.foregroundStyle(RosettaColors.success)
.font(.system(size: 18))
.transition(.scale.combined(with: .opacity))
} else if isWrong {
Image(systemName: "xmark.circle.fill")
.foregroundStyle(RosettaColors.error)
.font(.system(size: 18))
.transition(.scale.combined(with: .opacity))
}
}
func inputBorder(isCorrect: Bool, isWrong: Bool, isFocused: Bool) -> some View {
let borderColor: Color =
isCorrect ? RosettaColors.success :
isWrong ? RosettaColors.error :
isFocused ? RosettaColors.primaryBlue :
RosettaColors.subtleBorder
return RoundedRectangle(cornerRadius: 12)
.fill(RosettaColors.cardFill)
.overlay {
RoundedRectangle(cornerRadius: 12)
.stroke(borderColor, lineWidth: isCorrect || isWrong || isFocused ? 2 : 1)
}
.animation(.easeInOut(duration: 0.2), value: borderColor)
}
}
// MARK: - Paste
private extension ConfirmSeedPhraseView {
var pasteButton: some View {
Button(action: pasteFromClipboard) {
HStack(spacing: 6) {
Image(systemName: "doc.on.clipboard")
.font(.system(size: 14))
Text("Paste from Clipboard")
.font(.system(size: 16, weight: .medium))
}
.foregroundStyle(RosettaColors.primaryBlue)
.frame(maxWidth: .infinity)
.frame(height: 48)
.background {
RoundedRectangle(cornerRadius: 12)
.stroke(RosettaColors.primaryBlue, lineWidth: 1)
}
}
.accessibilityHint("Pastes your saved seed phrase")
}
@ViewBuilder
var pasteSuccessMessage: some View {
if showPasteSuccess {
HStack(spacing: 8) {
Image(systemName: "checkmark.circle.fill")
.foregroundStyle(RosettaColors.success)
Text("Words pasted successfully!")
.font(.system(size: 14, weight: .semibold))
.foregroundStyle(RosettaColors.success)
}
.padding(12)
.frame(maxWidth: .infinity)
.background {
RoundedRectangle(cornerRadius: 8)
.fill(RosettaColors.success.opacity(0.15))
}
.transition(.opacity.combined(with: .scale(scale: 0.95)))
}
}
func pasteFromClipboard() {
guard let text = UIPasteboard.general.string else { return }
let words = text
.components(separatedBy: CharacterSet.whitespacesAndNewlines.union(.init(charactersIn: ",")))
.map { $0.trimmingCharacters(in: .whitespaces).lowercased() }
.filter { !$0.isEmpty }
guard words.count >= 12 else { return }
for (inputIndex, seedIndex) in confirmPositions.enumerated() {
confirmationInputs[inputIndex] = words[seedIndex]
}
withAnimation(.spring(response: 0.3)) { showPasteSuccess = true }
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
withAnimation { showPasteSuccess = false }
}
}
}
// MARK: - Error & Confirm
private extension ConfirmSeedPhraseView {
@ViewBuilder
var errorMessage: some View {
if showError {
Text("Some words don't match. Please check and try again.")
.font(.system(size: 14))
.foregroundStyle(RosettaColors.error)
.multilineTextAlignment(.center)
.transition(.opacity.combined(with: .scale(scale: 0.95)))
}
}
var confirmButton: some View {
Button {
if allCorrect {
onConfirmed()
} else {
withAnimation(.spring(response: 0.3)) { showError = true }
}
} label: {
Text("Confirm")
.font(.system(size: 17, weight: .semibold))
.foregroundStyle(.white)
.frame(maxWidth: .infinity)
.frame(height: 50)
}
.buttonStyle(RosettaPrimaryButtonStyle(isEnabled: allCorrect))
.accessibilityHint(allCorrect ? "Confirms your seed phrase backup" : "Fill in all words correctly first")
}
}
#Preview {
ConfirmSeedPhraseView(
seedPhrase: ["abandon", "ability", "able", "about", "above", "absent",
"absorb", "abstract", "absurd", "abuse", "access", "accident"],
onConfirmed: {},
onBack: {}
)
.preferredColorScheme(.dark)
.background(RosettaColors.authBackground)
}