107 lines
4.2 KiB
Swift
107 lines
4.2 KiB
Swift
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
|
|
}
|
|
}
|
|
}
|