Closer/iphone/Closer/Pairing/RecoveryPhraseView.swift

107 lines
4.2 KiB
Swift
Raw Normal View History

import SwiftUI
/// Displays a newly-generated recovery phrase for the inviter to copy/share.
///
/// Security UX:
/// - Marked `.privacySensitive` so it is hidden in screenshots / app switcher preview.
/// - Copy-to-clipboard is the only action; the phrase is never sent to analytics.
/// - The view does not persist the phrase; the caller (PairingViewModel) owns it.
struct RecoveryPhraseView: View {
let phrase: String
let onContinue: () -> Void
@State private var copied = false
private var words: [String] {
phrase.split(separator: " ").map(String.init)
}
var body: some View {
ScrollView {
VStack(spacing: CloserSpacing.xl) {
VStack(spacing: CloserSpacing.sm) {
Image(systemName: "lock.shield.fill")
.font(.system(size: 44))
.foregroundColor(.closerPrimary)
Text("Save this phrase")
.font(CloserFont.title1)
.foregroundColor(.closerText)
Text("This is the only way to recover your encrypted answers if you switch devices. Your partner does not need it to join.")
.font(CloserFont.callout)
.foregroundColor(.closerTextSecondary)
.multilineTextAlignment(.center)
}
.padding(.top, CloserSpacing.xxl)
if words.count == 10 {
HStack(spacing: CloserSpacing.lg) {
VStack(spacing: CloserSpacing.md) {
ForEach(0..<5, id: \.self) { i in
wordRow(index: i, word: words[i])
}
}
VStack(spacing: CloserSpacing.md) {
ForEach(5..<10, id: \.self) { i in
wordRow(index: i, word: words[i])
}
}
}
.padding(CloserSpacing.lg)
.background(Color.closerSurface)
.clipShape(RoundedRectangle(cornerRadius: CloserRadius.large, style: .continuous))
.privacySensitive()
} else {
Text(phrase)
.font(CloserFont.body.monospaced())
.foregroundColor(.closerText)
.padding(CloserSpacing.lg)
.background(Color.closerSurface)
.clipShape(RoundedRectangle(cornerRadius: CloserRadius.large, style: .continuous))
.privacySensitive()
}
Button(action: copyPhrase) {
Label(copied ? "Copied" : "Copy Phrase", systemImage: copied ? "checkmark" : "doc.on.doc")
}
.buttonStyle(SecondaryButtonStyle())
Button(action: onContinue) {
Text("I saved it")
}
.buttonStyle(PrimaryButtonStyle())
Text("Do not screenshot this screen. Store the phrase in a password manager or write it down offline.")
.font(CloserFont.footnote)
.foregroundColor(.closerTextSecondary)
.multilineTextAlignment(.center)
}
.closerPadding()
}
.background(Color.closerBackground)
.navigationBarTitleDisplayMode(.inline)
}
private func wordRow(index: Int, word: String) -> some View {
HStack(spacing: CloserSpacing.sm) {
Text("\(index + 1)")
.font(CloserFont.caption2)
.foregroundColor(.closerTextSecondary)
.frame(width: 24, alignment: .leading)
Text(word)
.font(CloserFont.body.monospaced())
.foregroundColor(.closerText)
Spacer()
}
}
private func copyPhrase() {
UIPasteboard.general.string = phrase
copied = true
// Reset the copied badge after a few seconds.
Task {
try? await Task.sleep(nanoseconds: 3_000_000_000)
copied = false
}
}
}