Files
honeyDueKMP/iosApp/iosApp/Onboarding/OnboardingCreateAccountView.swift
Trey t fff1032c29 Add onboarding UI tests and improve app data management
- Add Suite0_OnboardingTests with fresh install and login test flows
- Add accessibility identifiers to onboarding views for UI testing
- Remove deprecated DataCache in favor of unified DataManager
- Update API layer to support public upgrade-triggers endpoint
- Improve onboarding first task view with better date handling
- Update various views with accessibility identifiers for testing
- Fix subscription feature comparison view layout
- Update document detail view improvements

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-04 15:55:34 -06:00

352 lines
14 KiB
Swift

import SwiftUI
import AuthenticationServices
import ComposeApp
/// Screen 4: Create Account / Sign In with Apple - Content only (no navigation bar)
struct OnboardingCreateAccountContent: View {
var onAccountCreated: (Bool) -> Void // Bool indicates if user is already verified
@StateObject private var viewModel = RegisterViewModel()
@StateObject private var appleSignInViewModel = AppleSignInViewModel()
@State private var showingLoginSheet = false
@State private var isExpanded = false
@FocusState private var focusedField: Field?
enum Field {
case username, email, password, confirmPassword
}
private var isFormValid: Bool {
!viewModel.username.isEmpty &&
!viewModel.email.isEmpty &&
!viewModel.password.isEmpty &&
viewModel.password == viewModel.confirmPassword
}
var body: some View {
ScrollView {
VStack(spacing: AppSpacing.xl) {
// Header
VStack(spacing: AppSpacing.sm) {
ZStack {
Circle()
.fill(Color.appPrimary.opacity(0.1))
.frame(width: 80, height: 80)
Image(systemName: "person.badge.plus")
.font(.system(size: 36))
.foregroundStyle(Color.appPrimary.gradient)
}
Text("Save your home to your account")
.font(.title2)
.fontWeight(.bold)
.foregroundColor(Color.appTextPrimary)
.multilineTextAlignment(.center)
.accessibilityIdentifier(AccessibilityIdentifiers.Onboarding.createAccountTitle)
Text("Your data will be synced across devices")
.font(.subheadline)
.foregroundColor(Color.appTextSecondary)
}
.padding(.top, AppSpacing.lg)
// Sign in with Apple (Primary)
VStack(spacing: AppSpacing.md) {
SignInWithAppleButton(
onRequest: { request in
request.requestedScopes = [.fullName, .email]
},
onCompletion: { _ in }
)
.frame(height: 56)
.cornerRadius(AppRadius.md)
.signInWithAppleButtonStyle(.black)
.disabled(appleSignInViewModel.isLoading)
.opacity(appleSignInViewModel.isLoading ? 0.6 : 1.0)
.overlay {
Color.clear
.contentShape(Rectangle())
.onTapGesture {
appleSignInViewModel.signInWithApple()
}
}
if appleSignInViewModel.isLoading {
HStack {
ProgressView()
Text("Signing in with Apple...")
.font(.subheadline)
.foregroundColor(Color.appTextSecondary)
}
}
if let error = appleSignInViewModel.errorMessage {
errorMessage(error)
}
}
// Divider
HStack {
Rectangle()
.fill(Color.appTextSecondary.opacity(0.3))
.frame(height: 1)
Text("or")
.font(.subheadline)
.foregroundColor(Color.appTextSecondary)
.padding(.horizontal, AppSpacing.sm)
Rectangle()
.fill(Color.appTextSecondary.opacity(0.3))
.frame(height: 1)
}
// Create Account Form
VStack(spacing: AppSpacing.md) {
if !isExpanded {
// Collapsed state
Button(action: {
withAnimation(.easeInOut(duration: 0.3)) {
isExpanded = true
}
}) {
HStack(spacing: AppSpacing.sm) {
Image(systemName: "envelope.fill")
.font(.title3)
Text("Create Account with Email")
.font(.headline)
.fontWeight(.medium)
}
.frame(maxWidth: .infinity)
.frame(height: 56)
.foregroundColor(Color.appPrimary)
.background(Color.appPrimary.opacity(0.1))
.cornerRadius(AppRadius.md)
}
.accessibilityIdentifier(AccessibilityIdentifiers.Onboarding.emailSignUpExpandButton)
} else {
// Expanded form
VStack(spacing: AppSpacing.md) {
// Username
formField(
icon: "person.fill",
placeholder: "Username",
text: $viewModel.username,
field: .username,
keyboardType: .default,
contentType: .username
)
// Email
formField(
icon: "envelope.fill",
placeholder: "Email",
text: $viewModel.email,
field: .email,
keyboardType: .emailAddress,
contentType: .emailAddress
)
// Password
secureFormField(
icon: "lock.fill",
placeholder: "Password",
text: $viewModel.password,
field: .password
)
// Confirm Password
secureFormField(
icon: "lock.fill",
placeholder: "Confirm Password",
text: $viewModel.confirmPassword,
field: .confirmPassword
)
if let error = viewModel.errorMessage {
errorMessage(error)
}
// Register button
Button(action: {
viewModel.register()
}) {
HStack {
if viewModel.isLoading {
ProgressView()
.progressViewStyle(CircularProgressViewStyle(tint: .white))
}
Text(viewModel.isLoading ? "Creating Account..." : "Create Account")
.font(.headline)
.fontWeight(.semibold)
}
.frame(maxWidth: .infinity)
.frame(height: 56)
.foregroundColor(Color.appTextOnPrimary)
.background(
isFormValid && !viewModel.isLoading
? AnyShapeStyle(LinearGradient(colors: [Color.appPrimary, Color.appPrimary.opacity(0.8)], startPoint: .topLeading, endPoint: .bottomTrailing))
: AnyShapeStyle(Color.appTextSecondary)
)
.cornerRadius(AppRadius.md)
.shadow(color: isFormValid ? Color.appPrimary.opacity(0.3) : .clear, radius: 10, y: 5)
}
.accessibilityIdentifier(AccessibilityIdentifiers.Onboarding.createAccountButton)
.disabled(!isFormValid || viewModel.isLoading)
}
.transition(.opacity.combined(with: .move(edge: .top)))
}
}
// Already have an account
HStack(spacing: AppSpacing.xs) {
Text("Already have an account?")
.font(.body)
.foregroundColor(Color.appTextSecondary)
Button("Log in") {
showingLoginSheet = true
}
.font(.body)
.fontWeight(.semibold)
.foregroundColor(Color.appPrimary)
}
.padding(.top, AppSpacing.md)
}
.padding(.horizontal, AppSpacing.xl)
.padding(.bottom, AppSpacing.xxxl)
}
.background(Color.appBackgroundPrimary)
.sheet(isPresented: $showingLoginSheet) {
LoginView(onLoginSuccess: {
showingLoginSheet = false
onAccountCreated(true)
})
}
.onChange(of: viewModel.isRegistered) { _, isRegistered in
if isRegistered {
// Registration successful - user is authenticated but not verified
onAccountCreated(false)
}
}
.onAppear {
// Set up Apple Sign In callback
appleSignInViewModel.onSignInSuccess = { isVerified in
AuthenticationManager.shared.login(verified: isVerified)
// Residence creation is handled by the coordinator
onAccountCreated(isVerified)
}
}
}
// MARK: - Form Fields
private func formField(
icon: String,
placeholder: String,
text: Binding<String>,
field: Field,
keyboardType: UIKeyboardType,
contentType: UITextContentType
) -> some View {
HStack(spacing: AppSpacing.sm) {
Image(systemName: icon)
.foregroundColor(Color.appTextSecondary)
.frame(width: 20)
TextField(placeholder, text: text)
.textInputAutocapitalization(.never)
.autocorrectionDisabled()
.keyboardType(keyboardType)
.textContentType(contentType)
.focused($focusedField, equals: field)
}
.padding(AppSpacing.md)
.background(Color.appBackgroundSecondary)
.cornerRadius(AppRadius.md)
.overlay(
RoundedRectangle(cornerRadius: AppRadius.md)
.stroke(focusedField == field ? Color.appPrimary : Color.appTextSecondary.opacity(0.3), lineWidth: 1.5)
)
}
private func secureFormField(
icon: String,
placeholder: String,
text: Binding<String>,
field: Field
) -> some View {
HStack(spacing: AppSpacing.sm) {
Image(systemName: icon)
.foregroundColor(Color.appTextSecondary)
.frame(width: 20)
SecureField(placeholder, text: text)
.textContentType(.password)
.focused($focusedField, equals: field)
}
.padding(AppSpacing.md)
.background(Color.appBackgroundSecondary)
.cornerRadius(AppRadius.md)
.overlay(
RoundedRectangle(cornerRadius: AppRadius.md)
.stroke(focusedField == field ? Color.appPrimary : Color.appTextSecondary.opacity(0.3), lineWidth: 1.5)
)
}
private func errorMessage(_ message: String) -> some View {
HStack(spacing: AppSpacing.sm) {
Image(systemName: "exclamationmark.circle.fill")
.foregroundColor(Color.appError)
Text(message)
.font(.callout)
.foregroundColor(Color.appError)
Spacer()
}
.padding(AppSpacing.md)
.background(Color.appError.opacity(0.1))
.cornerRadius(AppRadius.md)
}
}
// MARK: - Legacy wrapper with navigation bar (for backwards compatibility)
struct OnboardingCreateAccountView: View {
var onAccountCreated: (Bool) -> Void
var onBack: () -> Void
var body: some View {
VStack(spacing: 0) {
// Navigation bar
HStack {
Button(action: onBack) {
Image(systemName: "chevron.left")
.font(.title2)
.foregroundColor(Color.appPrimary)
}
Spacer()
OnboardingProgressIndicator(currentStep: 3, totalSteps: 5)
Spacer()
// Invisible spacer for alignment
Image(systemName: "chevron.left")
.font(.title2)
.opacity(0)
}
.padding(.horizontal, AppSpacing.lg)
.padding(.vertical, AppSpacing.md)
OnboardingCreateAccountContent(onAccountCreated: onAccountCreated)
}
.background(Color.appBackgroundPrimary)
}
}
// MARK: - Preview
#Preview {
OnboardingCreateAccountContent(onAccountCreated: { _ in })
}