Предыдущий коммит случайно включил изменения из рабочей директории: упрощение GlassModifier, GlassModifiers, RosettaTabBar, ButtonStyles, GlassCard и других файлов, что сломало iOS 26 glass-эффекты и внешний вид tab bar. Восстановлены оригинальные файлы из состояния до этих изменений. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
292 lines
10 KiB
Swift
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)
|
|
}
|