- 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.
221 lines
7.3 KiB
Swift
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)
|
|
}
|