246 lines
7.8 KiB
Swift
246 lines
7.8 KiB
Swift
import SwiftUI
|
|
|
|
struct SetPasswordView: View {
|
|
let seedPhrase: [String]
|
|
let isImportMode: Bool
|
|
let onAccountCreated: () -> Void
|
|
let onBack: () -> Void
|
|
|
|
@State private var password = ""
|
|
@State private var confirmPassword = ""
|
|
@State private var showPassword = false
|
|
@State private var showConfirmPassword = false
|
|
@State private var isCreating = false
|
|
@FocusState private var focusedField: Field?
|
|
|
|
fileprivate enum Field {
|
|
case password, confirm
|
|
}
|
|
|
|
private var passwordsMatch: Bool {
|
|
!password.isEmpty && password == confirmPassword
|
|
}
|
|
|
|
private var canCreate: Bool {
|
|
passwordsMatch && !isCreating
|
|
}
|
|
|
|
var body: some View {
|
|
VStack(spacing: 0) {
|
|
AuthNavigationBar(title: "Set Password", onBack: onBack)
|
|
|
|
ScrollView(showsIndicators: false) {
|
|
VStack(spacing: 20) {
|
|
lockIcon
|
|
headerSection
|
|
|
|
VStack(spacing: 8) {
|
|
passwordField
|
|
PasswordStrengthIndicator(password: password)
|
|
}
|
|
|
|
VStack(spacing: 8) {
|
|
confirmField
|
|
PasswordMatchIndicator(password: password, confirmPassword: confirmPassword)
|
|
}
|
|
|
|
WeakPasswordWarning(password: password)
|
|
infoCard
|
|
}
|
|
.padding(.horizontal, 24)
|
|
.padding(.top, 8)
|
|
.padding(.bottom, 100)
|
|
}
|
|
.scrollDismissesKeyboard(.interactively)
|
|
.onTapGesture { focusedField = nil }
|
|
|
|
createButton
|
|
.padding(.horizontal, 24)
|
|
.padding(.bottom, 16)
|
|
}
|
|
.geometryGroup()
|
|
}
|
|
}
|
|
|
|
// MARK: - Lock Icon
|
|
|
|
private extension SetPasswordView {
|
|
var lockIcon: some View {
|
|
GlassCard(cornerRadius: 20, fillOpacity: 0.1) {
|
|
Image(systemName: "lock.shield.fill")
|
|
.font(.system(size: 32))
|
|
.foregroundStyle(RosettaColors.primaryBlue)
|
|
.frame(width: 72, height: 72)
|
|
}
|
|
.accessibilityHidden(true)
|
|
}
|
|
}
|
|
|
|
// MARK: - Header
|
|
|
|
private extension SetPasswordView {
|
|
var headerSection: some View {
|
|
VStack(spacing: 8) {
|
|
Text(isImportMode ? "Recover Account" : "Protect Your Account")
|
|
.font(.system(size: 24, weight: .bold))
|
|
.foregroundStyle(.white)
|
|
|
|
Text(isImportMode
|
|
? "Set a password to protect your recovered account.\nYou'll need it to unlock Rosetta."
|
|
: "This password encrypts your keys locally.\nYou'll need it to unlock Rosetta.")
|
|
.font(.system(size: 14))
|
|
.foregroundStyle(RosettaColors.secondaryText)
|
|
.multilineTextAlignment(.center)
|
|
.lineSpacing(2)
|
|
}
|
|
.accessibilityElement(children: .combine)
|
|
}
|
|
}
|
|
|
|
// MARK: - Password Fields
|
|
|
|
private extension SetPasswordView {
|
|
var passwordField: some View {
|
|
secureInputField(
|
|
placeholder: "Password",
|
|
text: $password,
|
|
isSecure: !showPassword,
|
|
toggleAction: { showPassword.toggle() },
|
|
isRevealed: showPassword,
|
|
field: .password
|
|
)
|
|
.accessibilityLabel("Password input")
|
|
}
|
|
|
|
var confirmField: some View {
|
|
secureInputField(
|
|
placeholder: "Confirm Password",
|
|
text: $confirmPassword,
|
|
isSecure: !showConfirmPassword,
|
|
toggleAction: { showConfirmPassword.toggle() },
|
|
isRevealed: showConfirmPassword,
|
|
field: .confirm
|
|
)
|
|
.accessibilityLabel("Confirm password input")
|
|
}
|
|
|
|
func secureInputField(
|
|
placeholder: String,
|
|
text: Binding<String>,
|
|
isSecure: Bool,
|
|
toggleAction: @escaping () -> Void,
|
|
isRevealed: Bool,
|
|
field: Field
|
|
) -> some View {
|
|
HStack(spacing: 12) {
|
|
Group {
|
|
if isSecure {
|
|
SecureField(placeholder, text: text)
|
|
} else {
|
|
TextField(placeholder, text: text)
|
|
}
|
|
}
|
|
.font(.system(size: 16))
|
|
.foregroundStyle(.white)
|
|
.focused($focusedField, equals: field)
|
|
.textInputAutocapitalization(.never)
|
|
.autocorrectionDisabled()
|
|
|
|
Button(action: toggleAction) {
|
|
Image(systemName: isRevealed ? "eye.slash.fill" : "eye.fill")
|
|
.font(.system(size: 16))
|
|
.foregroundStyle(RosettaColors.secondaryText)
|
|
}
|
|
.accessibilityLabel(isRevealed ? "Hide password" : "Show password")
|
|
}
|
|
.padding(.horizontal, 16)
|
|
.padding(.vertical, 14)
|
|
.background {
|
|
let isFocused = focusedField == field
|
|
RoundedRectangle(cornerRadius: 12)
|
|
.fill(RosettaColors.cardFill)
|
|
.overlay {
|
|
RoundedRectangle(cornerRadius: 12)
|
|
.stroke(
|
|
isFocused ? RosettaColors.primaryBlue : RosettaColors.subtleBorder,
|
|
lineWidth: isFocused ? 2 : 1
|
|
)
|
|
}
|
|
.animation(.easeInOut(duration: 0.2), value: isFocused)
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Info Card
|
|
|
|
private extension SetPasswordView {
|
|
var infoCard: some View {
|
|
GlassCard(cornerRadius: 12, fillOpacity: 0.08) {
|
|
HStack(alignment: .top, spacing: 10) {
|
|
Image(systemName: "info.circle.fill")
|
|
.foregroundStyle(RosettaColors.primaryBlue)
|
|
.font(.system(size: 16))
|
|
|
|
Text("Your password is never stored or sent anywhere. It's only used to encrypt your keys locally.")
|
|
.font(.system(size: 13))
|
|
.foregroundStyle(RosettaColors.secondaryText)
|
|
.lineSpacing(2)
|
|
}
|
|
.padding(16)
|
|
}
|
|
.accessibilityElement(children: .combine)
|
|
.accessibilityLabel("Your password is only used for local encryption and is never sent anywhere.")
|
|
}
|
|
}
|
|
|
|
// MARK: - Create Button
|
|
|
|
private extension SetPasswordView {
|
|
var createButton: some View {
|
|
Button(action: createAccount) {
|
|
Group {
|
|
if isCreating {
|
|
ProgressView()
|
|
.tint(.white)
|
|
} else {
|
|
Text(isImportMode ? "Recover Account" : "Create Account")
|
|
.font(.system(size: 16, weight: .semibold))
|
|
.foregroundStyle(.white)
|
|
}
|
|
}
|
|
.frame(maxWidth: .infinity)
|
|
.frame(height: 56)
|
|
}
|
|
.buttonStyle(RosettaPrimaryButtonStyle(isEnabled: canCreate))
|
|
.accessibilityHint(canCreate ? "Creates your encrypted account" : "Enter matching passwords first")
|
|
}
|
|
|
|
func createAccount() {
|
|
guard canCreate else { return }
|
|
isCreating = true
|
|
|
|
// TODO: Implement real account creation:
|
|
// 1. CryptoManager.generateKeyPairFromSeed(seedPhrase)
|
|
// 2. CryptoManager.encryptWithPassword(privateKey, password)
|
|
// 3. CryptoManager.encryptWithPassword(seedPhrase.joined(separator: " "), password)
|
|
// 4. Save EncryptedAccount to persistence
|
|
// 5. Authenticate with server via Protocol handshake
|
|
|
|
DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) {
|
|
isCreating = false
|
|
onAccountCreated()
|
|
}
|
|
}
|
|
}
|
|
|
|
#Preview {
|
|
SetPasswordView(
|
|
seedPhrase: SeedPhraseGenerator.generate(),
|
|
isImportMode: false,
|
|
onAccountCreated: {},
|
|
onBack: {}
|
|
)
|
|
.preferredColorScheme(.dark)
|
|
.background(RosettaColors.authBackground)
|
|
}
|