Files
mobile-ios/Rosetta/Features/Auth/SetPasswordView.swift
2026-02-23 11:35:29 +05:00

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)
}