Files
mobile-ios/Rosetta/Features/Auth/ImportSeedPhraseView.swift
senseiGai 99a35302fa feat: Implement chat list and search functionality
- Added ChatListViewModel to manage chat list state and server search.
- Created ChatRowView for displaying individual chat rows.
- Developed SearchView and SearchViewModel for user search functionality.
- Introduced MainTabView for tab-based navigation between chats and settings.
- Implemented OnboardingPager for onboarding experience.
- Created SettingsView and SettingsViewModel for user settings management.
- Added SplashView for initial app launch experience.
2026-02-25 21:27:41 +05:00

221 lines
7.3 KiB
Swift

import SwiftUI
struct ImportSeedPhraseView: View {
@Binding var seedPhrase: [String]
let onContinue: () -> Void
let onBack: () -> Void
@State private var importedWords: [String] = Array(repeating: "", count: 12)
@State private var errorMessage: String?
@FocusState private var focusedWordIndex: Int?
private var allFilled: Bool {
importedWords.allSatisfy { !$0.trimmingCharacters(in: .whitespaces).isEmpty }
}
var body: some View {
VStack(spacing: 0) {
AuthNavigationBar(onBack: onBack)
ScrollView(showsIndicators: false) {
VStack(spacing: 24) {
headerSection
pasteButton
wordGrid
errorSection
}
.padding(.horizontal, 24)
.padding(.top, 16)
.padding(.bottom, 100)
}
.scrollDismissesKeyboard(.interactively)
.onTapGesture { focusedWordIndex = nil }
continueButton
.padding(.horizontal, 24)
.padding(.bottom, 16)
}
}
}
// MARK: - Header
private extension ImportSeedPhraseView {
var headerSection: some View {
VStack(spacing: 12) {
Text("Import Account")
.font(.system(size: 28, weight: .bold))
.foregroundStyle(.white)
Text("Enter your 12-word recovery phrase\nto restore your account.")
.font(.system(size: 15))
.foregroundStyle(RosettaColors.secondaryText)
.multilineTextAlignment(.center)
.lineSpacing(3)
}
.accessibilityElement(children: .combine)
}
}
// MARK: - Paste Button
private extension ImportSeedPhraseView {
var pasteButton: some View {
Button(action: pasteFromClipboard) {
HStack(spacing: 6) {
Image(systemName: "doc.on.clipboard")
.font(.system(size: 14))
Text("Paste all 12 words")
.font(.system(size: 15, weight: .medium))
}
.foregroundStyle(RosettaColors.primaryBlue)
}
.accessibilityHint("Pastes seed phrase from clipboard")
}
func pasteFromClipboard() {
guard let text = UIPasteboard.general.string, !text.isEmpty else {
showError("Clipboard is empty")
return
}
let parsed = text
.components(separatedBy: CharacterSet.whitespacesAndNewlines.union(.init(charactersIn: ",;")))
.map { $0.trimmingCharacters(in: .whitespaces).lowercased() }
.filter { !$0.isEmpty }
guard !parsed.isEmpty else {
showError("No valid words found in clipboard")
return
}
guard parsed.count == 12 else {
showError("Clipboard contains \(parsed.count) words, need 12")
return
}
importedWords = parsed
withAnimation { errorMessage = nil }
}
}
// MARK: - Word Grid
private extension ImportSeedPhraseView {
var wordGrid: some View {
HStack(alignment: .top, spacing: 12) {
VStack(spacing: 10) {
ForEach(0..<6, id: \.self) { index in
inputRow(index: index)
.staggeredAppearance(index: index, baseDelay: 0.15, stagger: 0.04)
}
}
VStack(spacing: 10) {
ForEach(6..<12, id: \.self) { index in
inputRow(index: index)
.staggeredAppearance(index: index, baseDelay: 0.15, stagger: 0.04)
}
}
}
}
func inputRow(index: Int) -> some View {
let color = RosettaColors.seedWordColors[index % RosettaColors.seedWordColors.count]
let isFocused = focusedWordIndex == index
let hasContent = !importedWords[index].trimmingCharacters(in: .whitespaces).isEmpty
return HStack(spacing: 8) {
Text("\(index + 1).")
.font(.system(size: 15))
.foregroundStyle(RosettaColors.numberGray)
.frame(width: 28, alignment: .trailing)
TextField("word", text: $importedWords[index])
.font(.system(size: 17, weight: .semibold, design: .monospaced))
.foregroundStyle(hasContent ? color : .white)
.autocorrectionDisabled()
.textInputAutocapitalization(.never)
.focused($focusedWordIndex, equals: index)
.submitLabel(index < 11 ? .next : .done)
.onSubmit {
focusedWordIndex = index < 11 ? index + 1 : nil
}
.onChange(of: importedWords[index]) { _, newValue in
importedWords[index] = newValue.lowercased()
if errorMessage != nil { withAnimation { errorMessage = nil } }
}
Spacer()
}
.padding(.horizontal, 16)
.padding(.vertical, 14)
.background {
RoundedRectangle(cornerRadius: 12)
.fill(RosettaColors.cardFill)
.overlay {
RoundedRectangle(cornerRadius: 12)
.stroke(
isFocused ? RosettaColors.primaryBlue : Color.clear,
lineWidth: 2
)
}
.animation(.easeInOut(duration: 0.2), value: isFocused)
}
.accessibilityLabel("Word \(index + 1)")
.accessibilityValue(importedWords[index].isEmpty ? "Empty" : importedWords[index])
}
}
// MARK: - Error & Continue
private extension ImportSeedPhraseView {
@ViewBuilder
var errorSection: some View {
if let message = errorMessage {
Text(message)
.font(.system(size: 14))
.foregroundStyle(RosettaColors.error)
.multilineTextAlignment(.center)
.transition(.opacity.combined(with: .scale(scale: 0.95)))
}
}
func showError(_ message: String) {
withAnimation(.spring(response: 0.3)) { errorMessage = message }
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
withAnimation { errorMessage = nil }
}
}
var continueButton: some View {
Button {
guard allFilled else {
showError("Please fill in all words")
return
}
let normalized = importedWords.map { $0.lowercased().trimmingCharacters(in: .whitespaces) }
guard CryptoManager.shared.validateMnemonic(normalized) else {
showError("Invalid recovery phrase. Check each word for typos.")
return
}
seedPhrase = normalized
onContinue()
} label: {
Text("Continue")
.font(.system(size: 17, weight: .semibold))
.foregroundStyle(.white)
.frame(maxWidth: .infinity)
.frame(height: 50)
}
.buttonStyle(RosettaPrimaryButtonStyle(isEnabled: allFilled))
.accessibilityHint(allFilled ? "Proceeds to password setup" : "Fill in all 12 words first")
}
}
#Preview {
ImportSeedPhraseView(seedPhrase: .constant([]), onContinue: {}, onBack: {})
.preferredColorScheme(.dark)
.background(RosettaColors.authBackground)
}