Files
mobile-ios/Rosetta/Features/Auth/SeedPhraseView.swift

202 lines
6.6 KiB
Swift

import SwiftUI
struct SeedPhraseView: View {
@Binding var seedPhrase: [String]
let onContinue: () -> Void
let onBack: () -> Void
@State private var showCopiedToast = false
@State private var isContentVisible: Bool
private static var hasAnimated = false
init(seedPhrase: Binding<[String]>, onContinue: @escaping () -> Void, onBack: @escaping () -> Void) {
_seedPhrase = seedPhrase
self.onContinue = onContinue
self.onBack = onBack
_showCopiedToast = State(initialValue: false)
_isContentVisible = State(initialValue: Self.hasAnimated)
}
var body: some View {
VStack(spacing: 0) {
AuthNavigationBar(onBack: onBack)
ScrollView(showsIndicators: false) {
VStack(spacing: 24) {
headerSection
wordGrid
copyButton
}
.padding(.horizontal, 24)
.padding(.top, 16)
.padding(.bottom, 100)
}
continueButton
.padding(.horizontal, 24)
.padding(.bottom, 16)
}
.onAppear(perform: generateSeedPhraseIfNeeded)
}
}
// MARK: - Header
private extension SeedPhraseView {
var headerSection: some View {
VStack(spacing: 12) {
Text("Your Recovery Phrase")
.font(.system(size: 28, weight: .bold))
.foregroundStyle(.white)
.opacity(isContentVisible ? 1.0 : 0.0)
.animation(.easeOut(duration: 0.3), value: isContentVisible)
Text("Write down these 12 words in order.\nYou'll need them to restore your account.")
.font(.system(size: 15))
.foregroundStyle(RosettaColors.secondaryText)
.multilineTextAlignment(.center)
.lineSpacing(3)
.opacity(isContentVisible ? 1.0 : 0.0)
.animation(.easeOut(duration: 0.4).delay(0.1), value: isContentVisible)
}
.accessibilityElement(children: .combine)
}
}
// MARK: - Word Grid
private extension SeedPhraseView {
var wordGrid: some View {
let leftColumn = Array(seedPhrase.prefix(6))
let rightColumn = Array(seedPhrase.dropFirst(6))
return HStack(alignment: .top, spacing: 12) {
wordColumn(words: leftColumn, startIndex: 1)
wordColumn(words: rightColumn, startIndex: 7)
}
.opacity(isContentVisible ? 1.0 : 0.0)
.animation(.easeOut(duration: 0.5).delay(0.2), value: isContentVisible)
}
func wordColumn(words: [String], startIndex: Int) -> some View {
VStack(spacing: 10) {
ForEach(Array(words.enumerated()), id: \.offset) { offset, word in
let globalIndex = startIndex + offset - 1
wordCard(number: startIndex + offset, word: word, colorIndex: globalIndex)
.staggeredAppearance(key: "seedPhrase", index: globalIndex, baseDelay: 0.3, stagger: 0.04)
}
}
}
func wordCard(number: Int, word: String, colorIndex: Int) -> some View {
let color = RosettaColors.seedWordColors[colorIndex % RosettaColors.seedWordColors.count]
return 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(color)
Spacer()
}
.padding(.horizontal, 16)
.padding(.vertical, 14)
.modifier(SeedCardStyle(color: color))
.accessibilityLabel("Word \(number): \(word)")
}
}
// MARK: - Seed Card Glass Style
private struct SeedCardStyle: ViewModifier {
let color: Color
func body(content: Content) -> some View {
if #available(iOS 26, *) {
content
.glassEffect(.regular, in: RoundedRectangle(cornerRadius: 12))
} else {
content
.background {
RoundedRectangle(cornerRadius: 12)
.fill(color.opacity(0.12))
.overlay {
RoundedRectangle(cornerRadius: 12)
.stroke(color.opacity(0.18), lineWidth: 0.5)
}
}
.clipShape(RoundedRectangle(cornerRadius: 12))
}
}
}
// MARK: - Copy Button
private extension SeedPhraseView {
var copyButton: some View {
Button {
UIPasteboard.general.string = seedPhrase.joined(separator: " ")
withAnimation { showCopiedToast = true }
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
withAnimation { showCopiedToast = false }
}
} label: {
HStack(spacing: 6) {
Image(systemName: showCopiedToast ? "checkmark.circle.fill" : "doc.on.doc")
.font(.system(size: 14))
Text(showCopiedToast ? "Copied!" : "Copy to clipboard")
.font(.system(size: 15, weight: .medium))
}
.foregroundStyle(showCopiedToast ? RosettaColors.success : RosettaColors.primaryBlue)
.contentTransition(.symbolEffect(.replace))
}
.accessibilityLabel(showCopiedToast ? "Copied to clipboard" : "Copy seed phrase to clipboard")
}
}
// MARK: - Continue Button
private extension SeedPhraseView {
var continueButton: some View {
Button(action: onContinue) {
Text("Continue")
.font(.system(size: 17, weight: .semibold))
.foregroundStyle(.white)
.frame(maxWidth: .infinity)
.frame(height: 50)
}
.buttonStyle(RosettaPrimaryButtonStyle())
.accessibilityHint("Proceed to confirm your seed phrase")
}
}
// MARK: - Seed Generation
private extension SeedPhraseView {
func generateSeedPhraseIfNeeded() {
guard seedPhrase.isEmpty else {
isContentVisible = true
return
}
do {
seedPhrase = try CryptoManager.shared.generateMnemonic()
} catch {
seedPhrase = []
}
guard !isContentVisible else { return }
withAnimation { isContentVisible = true }
Self.hasAnimated = true
}
}
#Preview {
SeedPhraseView(seedPhrase: .constant([]), onContinue: {}, onBack: {})
.preferredColorScheme(.dark)
.background(RosettaColors.authBackground)
}