- Fix RegisterView to call AuthenticationManager.login() after email verification so user is properly transitioned to home screen instead of returning to login - Fix ResidencesListView to load data when authentication state becomes true, ensuring residences load after registration/login - Add accessibility identifier to verification code field for UI testing - Add NSAppTransportSecurity exceptions for localhost/127.0.0.1 for local dev - Add comprehensive XCUITest suite for registration flow including: - Form validation tests (empty fields, invalid email, mismatched passwords) - Full registration and verification flow test - Logout from verification screen test - Helper scripts for test user cleanup 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
169 lines
7.2 KiB
Swift
169 lines
7.2 KiB
Swift
import SwiftUI
|
|
|
|
struct VerifyEmailView: View {
|
|
@StateObject private var viewModel = VerifyEmailViewModel()
|
|
@FocusState private var isFocused: Bool
|
|
var onVerifySuccess: () -> Void
|
|
var onLogout: () -> Void
|
|
|
|
var body: some View {
|
|
NavigationView {
|
|
ZStack {
|
|
Color.appBackgroundPrimary
|
|
.ignoresSafeArea()
|
|
|
|
ScrollView {
|
|
VStack(spacing: 24) {
|
|
Spacer().frame(height: 20)
|
|
|
|
// Header
|
|
VStack(spacing: 12) {
|
|
Image(systemName: "envelope.badge.shield.half.filled")
|
|
.font(.system(size: 60))
|
|
.foregroundStyle(Color.appPrimary.gradient)
|
|
.padding(.bottom, 8)
|
|
|
|
Text("Verify Your Email")
|
|
.font(.title)
|
|
.fontWeight(.bold)
|
|
.foregroundColor(Color.appTextPrimary)
|
|
|
|
Text("You must verify your email address to continue")
|
|
.font(.subheadline)
|
|
.foregroundColor(Color.appTextSecondary)
|
|
.multilineTextAlignment(.center)
|
|
.padding(.horizontal)
|
|
}
|
|
|
|
// Info Card
|
|
GroupBox {
|
|
HStack(spacing: 12) {
|
|
Image(systemName: "exclamationmark.shield.fill")
|
|
.foregroundColor(Color.appAccent)
|
|
.font(.title2)
|
|
|
|
Text("Email verification is required. Check your inbox for a 6-digit code.")
|
|
.font(.subheadline)
|
|
.foregroundColor(Color.appTextPrimary)
|
|
.fontWeight(.semibold)
|
|
}
|
|
.padding(.vertical, 4)
|
|
}
|
|
.padding(.horizontal)
|
|
|
|
// Code Input
|
|
VStack(alignment: .leading, spacing: 12) {
|
|
Text("Verification Code")
|
|
.font(.headline)
|
|
.foregroundColor(Color.appTextPrimary)
|
|
.padding(.horizontal)
|
|
|
|
TextField("000000", text: $viewModel.code)
|
|
.font(.system(size: 32, weight: .semibold, design: .rounded))
|
|
.multilineTextAlignment(.center)
|
|
.keyboardType(.numberPad)
|
|
.textFieldStyle(.roundedBorder)
|
|
.frame(height: 60)
|
|
.padding(.horizontal)
|
|
.focused($isFocused)
|
|
.accessibilityIdentifier(AccessibilityIdentifiers.Authentication.verificationCodeField)
|
|
.onChange(of: viewModel.code) { _, newValue in
|
|
// Limit to 6 digits
|
|
if newValue.count > 6 {
|
|
viewModel.code = String(newValue.prefix(6))
|
|
}
|
|
// Only allow numbers
|
|
viewModel.code = newValue.filter { $0.isNumber }
|
|
}
|
|
|
|
Text("Code must be 6 digits")
|
|
.font(.caption)
|
|
.foregroundColor(Color.appTextSecondary)
|
|
.padding(.horizontal)
|
|
}
|
|
|
|
// Error Message
|
|
if let errorMessage = viewModel.errorMessage {
|
|
ErrorMessageView(message: errorMessage, onDismiss: viewModel.clearError)
|
|
.padding(.horizontal)
|
|
}
|
|
|
|
// Verify Button
|
|
Button(action: {
|
|
viewModel.verifyEmail()
|
|
}) {
|
|
HStack {
|
|
if viewModel.isLoading {
|
|
ProgressView()
|
|
.progressViewStyle(CircularProgressViewStyle(tint: .white))
|
|
} else {
|
|
Image(systemName: "checkmark.shield.fill")
|
|
Text("Verify Email")
|
|
.fontWeight(.semibold)
|
|
}
|
|
}
|
|
.frame(maxWidth: .infinity)
|
|
.frame(height: 50)
|
|
.background(
|
|
viewModel.code.count == 6 && !viewModel.isLoading
|
|
? Color.appPrimary
|
|
: Color.gray.opacity(0.3)
|
|
)
|
|
.foregroundColor(Color.appTextOnPrimary)
|
|
.cornerRadius(12)
|
|
}
|
|
.disabled(viewModel.code.count != 6 || viewModel.isLoading)
|
|
.padding(.horizontal)
|
|
|
|
Spacer().frame(height: 20)
|
|
|
|
// Help Text
|
|
Text("Didn't receive the code? Check your spam folder or contact support.")
|
|
.font(.caption)
|
|
.foregroundColor(Color.appTextSecondary)
|
|
.multilineTextAlignment(.center)
|
|
.padding(.horizontal, 32)
|
|
}
|
|
}
|
|
}
|
|
.listStyle(.plain)
|
|
.scrollContentBackground(.hidden)
|
|
.background(Color.appBackgroundPrimary)
|
|
.navigationBarTitleDisplayMode(.inline)
|
|
.navigationBarBackButtonHidden(true)
|
|
.interactiveDismissDisabled(true)
|
|
.toolbar {
|
|
ToolbarItem(placement: .navigationBarTrailing) {
|
|
Button(action: onLogout) {
|
|
HStack(spacing: 4) {
|
|
Image(systemName: "rectangle.portrait.and.arrow.right")
|
|
.font(.system(size: 16))
|
|
Text("Logout")
|
|
.font(.subheadline)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
.onAppear {
|
|
isFocused = true
|
|
}
|
|
.onChange(of: viewModel.isVerified) { _, isVerified in
|
|
if isVerified {
|
|
onVerifySuccess()
|
|
}
|
|
}
|
|
.handleErrors(
|
|
error: viewModel.errorMessage,
|
|
onRetry: { viewModel.verifyEmail() }
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
#Preview {
|
|
VerifyEmailView(
|
|
onVerifySuccess: { print("Verified!") },
|
|
onLogout: { print("Logout") }
|
|
)
|
|
}
|