Creates a reusable KeyboardDismissToolbar view modifier that adds a "Done" button to dismiss keyboards that don't have a return key. Applied to all numeric keyboards (numberPad, decimalPad, phonePad) and multi-line text inputs (TextEditor, TextField with axis: .vertical). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
197 lines
7.7 KiB
Swift
197 lines
7.7 KiB
Swift
import SwiftUI
|
|
import ComposeApp
|
|
|
|
/// Screen 5: Email verification during onboarding - Content only (no navigation bar)
|
|
struct OnboardingVerifyEmailContent: View {
|
|
var onVerified: () -> Void
|
|
|
|
@StateObject private var viewModel = VerifyEmailViewModel()
|
|
@FocusState private var isCodeFieldFocused: Bool
|
|
@State private var hasCalledOnVerified = false
|
|
|
|
var body: some View {
|
|
VStack(spacing: 0) {
|
|
Spacer()
|
|
|
|
// Content
|
|
VStack(spacing: AppSpacing.xl) {
|
|
// Icon
|
|
ZStack {
|
|
Circle()
|
|
.fill(Color.appPrimary.opacity(0.1))
|
|
.frame(width: 100, height: 100)
|
|
|
|
Image(systemName: "envelope.badge.fill")
|
|
.font(.system(size: 44))
|
|
.foregroundStyle(Color.appPrimary.gradient)
|
|
}
|
|
|
|
// Title
|
|
VStack(spacing: AppSpacing.sm) {
|
|
Text("Verify your email")
|
|
.font(.title2)
|
|
.fontWeight(.bold)
|
|
.foregroundColor(Color.appTextPrimary)
|
|
.accessibilityIdentifier(AccessibilityIdentifiers.Onboarding.verifyEmailTitle)
|
|
|
|
Text("We sent a 6-digit code to your email address. Enter it below to verify your account.")
|
|
.font(.subheadline)
|
|
.foregroundColor(Color.appTextSecondary)
|
|
.multilineTextAlignment(.center)
|
|
}
|
|
|
|
// Code input
|
|
VStack(alignment: .leading, spacing: AppSpacing.xs) {
|
|
HStack(spacing: AppSpacing.sm) {
|
|
Image(systemName: "key.fill")
|
|
.foregroundColor(Color.appTextSecondary)
|
|
.frame(width: 20)
|
|
|
|
TextField("Enter 6-digit code", text: $viewModel.code)
|
|
.keyboardType(.numberPad)
|
|
.textContentType(.oneTimeCode)
|
|
.focused($isCodeFieldFocused)
|
|
.accessibilityIdentifier(AccessibilityIdentifiers.Onboarding.verificationCodeField)
|
|
.keyboardDismissToolbar()
|
|
.onChange(of: viewModel.code) { _, newValue in
|
|
// Limit to 6 digits
|
|
if newValue.count > 6 {
|
|
viewModel.code = String(newValue.prefix(6))
|
|
}
|
|
// Auto-verify when 6 digits entered
|
|
if newValue.count == 6 {
|
|
viewModel.verifyEmail()
|
|
}
|
|
}
|
|
}
|
|
.padding(AppSpacing.md)
|
|
.background(Color.appBackgroundSecondary)
|
|
.cornerRadius(AppRadius.md)
|
|
.overlay(
|
|
RoundedRectangle(cornerRadius: AppRadius.md)
|
|
.stroke(isCodeFieldFocused ? Color.appPrimary : Color.appTextSecondary.opacity(0.3), lineWidth: 1.5)
|
|
)
|
|
}
|
|
.padding(.horizontal, AppSpacing.xl)
|
|
|
|
// Error message
|
|
if let error = viewModel.errorMessage {
|
|
HStack(spacing: AppSpacing.sm) {
|
|
Image(systemName: "exclamationmark.circle.fill")
|
|
.foregroundColor(Color.appError)
|
|
Text(error)
|
|
.font(.callout)
|
|
.foregroundColor(Color.appError)
|
|
Spacer()
|
|
}
|
|
.padding(AppSpacing.md)
|
|
.background(Color.appError.opacity(0.1))
|
|
.cornerRadius(AppRadius.md)
|
|
.padding(.horizontal, AppSpacing.xl)
|
|
}
|
|
|
|
// Loading indicator
|
|
if viewModel.isLoading {
|
|
HStack {
|
|
ProgressView()
|
|
Text("Verifying...")
|
|
.font(.subheadline)
|
|
.foregroundColor(Color.appTextSecondary)
|
|
}
|
|
}
|
|
|
|
// Resend code hint
|
|
Text("Didn't receive a code? Check your spam folder or re-register")
|
|
.font(.caption)
|
|
.foregroundColor(Color.appTextSecondary)
|
|
.multilineTextAlignment(.center)
|
|
}
|
|
|
|
Spacer()
|
|
|
|
// Verify button
|
|
Button(action: {
|
|
viewModel.verifyEmail()
|
|
}) {
|
|
Text("Verify")
|
|
.font(.headline)
|
|
.fontWeight(.semibold)
|
|
.frame(maxWidth: .infinity)
|
|
.frame(height: 56)
|
|
.foregroundColor(Color.appTextOnPrimary)
|
|
.background(
|
|
viewModel.code.count == 6 && !viewModel.isLoading
|
|
? AnyShapeStyle(LinearGradient(colors: [Color.appPrimary, Color.appPrimary.opacity(0.8)], startPoint: .topLeading, endPoint: .bottomTrailing))
|
|
: AnyShapeStyle(Color.appTextSecondary)
|
|
)
|
|
.cornerRadius(AppRadius.md)
|
|
.shadow(color: viewModel.code.count == 6 ? Color.appPrimary.opacity(0.3) : .clear, radius: 10, y: 5)
|
|
}
|
|
.accessibilityIdentifier(AccessibilityIdentifiers.Onboarding.verifyButton)
|
|
.disabled(viewModel.code.count != 6 || viewModel.isLoading)
|
|
.padding(.horizontal, AppSpacing.xl)
|
|
.padding(.bottom, AppSpacing.xxxl)
|
|
}
|
|
.background(Color.appBackgroundPrimary)
|
|
.onAppear {
|
|
print("🏠 ONBOARDING: OnboardingVerifyEmailContent appeared")
|
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
|
isCodeFieldFocused = true
|
|
}
|
|
}
|
|
.onReceive(viewModel.$isVerified) { isVerified in
|
|
print("🏠 ONBOARDING: onReceive isVerified = \(isVerified), hasCalledOnVerified = \(hasCalledOnVerified)")
|
|
if isVerified && !hasCalledOnVerified {
|
|
hasCalledOnVerified = true
|
|
print("🏠 ONBOARDING: Calling onVerified callback FIRST (before markVerified)")
|
|
// CRITICAL: Call onVerified FIRST so coordinator can create residence
|
|
// BEFORE markVerified changes auth state and disposes this view
|
|
onVerified()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Legacy wrapper with navigation bar (for backwards compatibility)
|
|
|
|
struct OnboardingVerifyEmailView: View {
|
|
var onVerified: () -> Void
|
|
var onLogout: () -> Void
|
|
|
|
var body: some View {
|
|
VStack(spacing: 0) {
|
|
// Navigation bar
|
|
HStack {
|
|
// Logout option
|
|
Button(action: onLogout) {
|
|
Text("Back")
|
|
.font(.subheadline)
|
|
.foregroundColor(Color.appPrimary)
|
|
}
|
|
|
|
Spacer()
|
|
|
|
OnboardingProgressIndicator(currentStep: 4, totalSteps: 5)
|
|
|
|
Spacer()
|
|
|
|
// Invisible spacer for alignment
|
|
Text("Back")
|
|
.font(.subheadline)
|
|
.opacity(0)
|
|
}
|
|
.padding(.horizontal, AppSpacing.lg)
|
|
.padding(.vertical, AppSpacing.md)
|
|
|
|
OnboardingVerifyEmailContent(onVerified: onVerified)
|
|
}
|
|
.background(Color.appBackgroundPrimary)
|
|
}
|
|
}
|
|
|
|
// MARK: - Preview
|
|
|
|
#Preview {
|
|
OnboardingVerifyEmailContent(onVerified: {})
|
|
}
|