202 lines
6.6 KiB
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)
|
|
}
|