Files
honeyDueKMP/iosApp/iosApp/Onboarding/OnboardingNameResidenceView.swift
Trey t bcd8b36a9b Fix TokenStorage stale cache bug and add user-friendly error messages
- Fix TokenStorage.getToken() returning stale cached token after login/logout
- Add comprehensive ErrorMessageParser with 80+ error code mappings
- Add Suite9 and Suite10 UI test files for E2E integration testing
- Fix accessibility identifiers in RegisterView and ResidenceFormView
- Fix UITestHelpers logout to target alert button specifically
- Update various UI components with proper accessibility identifiers

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-17 11:48:35 -06:00

327 lines
14 KiB
Swift

import SwiftUI
/// Screen 3: Name your home - Content only (no navigation bar)
struct OnboardingNameResidenceContent: View {
@Binding var residenceName: String
var onContinue: () -> Void
@FocusState private var isTextFieldFocused: Bool
@State private var showSuggestions = false
@State private var isAnimating = false
@Environment(\.colorScheme) var colorScheme
private var isValid: Bool {
!residenceName.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
}
private let nameSuggestions = [
"Casa de [Your Name]",
"The Cozy Corner",
"Home Sweet Home",
"The Nest",
"Château Us"
]
var body: some View {
ZStack {
WarmGradientBackground()
// Decorative blobs
GeometryReader { geo in
OrganicBlobShape(variation: 2)
.fill(
RadialGradient(
colors: [
Color.appAccent.opacity(0.08),
Color.appAccent.opacity(0.02),
Color.clear
],
center: .center,
startRadius: 0,
endRadius: geo.size.width * 0.35
)
)
.frame(width: geo.size.width * 0.6, height: geo.size.height * 0.35)
.offset(x: -geo.size.width * 0.15, y: geo.size.height * 0.05)
.blur(radius: 25)
OrganicBlobShape(variation: 0)
.fill(
RadialGradient(
colors: [
Color.appPrimary.opacity(0.06),
Color.appPrimary.opacity(0.01),
Color.clear
],
center: .center,
startRadius: 0,
endRadius: geo.size.width * 0.3
)
)
.frame(width: geo.size.width * 0.5, height: geo.size.height * 0.3)
.offset(x: geo.size.width * 0.55, y: geo.size.height * 0.6)
.blur(radius: 20)
}
VStack(spacing: 0) {
Spacer()
// Content
VStack(spacing: OrganicSpacing.comfortable) {
// Animated house icon
ZStack {
// Pulsing glow circles
Circle()
.fill(
RadialGradient(
colors: [Color.appAccent.opacity(0.15), Color.clear],
center: .center,
startRadius: 30,
endRadius: 80
)
)
.frame(width: 160, height: 160)
.offset(x: -20, y: -20)
.scaleEffect(isAnimating ? 1.1 : 1.0)
.animation(
Animation.easeInOut(duration: 2.5).repeatForever(autoreverses: true),
value: isAnimating
)
Circle()
.fill(
RadialGradient(
colors: [Color.appPrimary.opacity(0.15), Color.clear],
center: .center,
startRadius: 30,
endRadius: 80
)
)
.frame(width: 160, height: 160)
.offset(x: 20, y: 20)
.scaleEffect(isAnimating ? 0.95 : 1.05)
.animation(
Animation.easeInOut(duration: 3).repeatForever(autoreverses: true).delay(0.5),
value: isAnimating
)
// Main icon
Image("icon")
.resizable()
.scaledToFit()
.frame(width: 100, height: 100)
.naturalShadow(.pronounced)
}
// Title with playful wording
VStack(spacing: 12) {
Text("Let's give your place a name!")
.font(.system(size: 26, weight: .bold, design: .rounded))
.foregroundColor(Color.appTextPrimary)
.multilineTextAlignment(.center)
.accessibilityIdentifier(AccessibilityIdentifiers.Onboarding.nameResidenceTitle)
Text("Don't worry, nothing's written in stone here.\nYou can always change it later in the app.")
.font(.system(size: 15, weight: .medium))
.foregroundColor(Color.appTextSecondary)
.multilineTextAlignment(.center)
.lineSpacing(4)
}
// Text field with organic styling
VStack(alignment: .leading, spacing: 12) {
HStack(spacing: 14) {
ZStack {
Circle()
.fill(
LinearGradient(
colors: [Color.appPrimary.opacity(0.15), Color.appAccent.opacity(0.1)],
startPoint: .topLeading,
endPoint: .bottomTrailing
)
)
.frame(width: 40, height: 40)
Image("house_outline")
.renderingMode(.template)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 18, height: 18)
.foregroundStyle(
LinearGradient(
colors: [Color.appPrimary, Color.appAccent],
startPoint: .topLeading,
endPoint: .bottomTrailing
)
)
}
TextField("The Smith Residence", text: $residenceName)
.font(.system(size: 17, weight: .medium))
.textInputAutocapitalization(.words)
.focused($isTextFieldFocused)
.submitLabel(.continue)
.accessibilityIdentifier(AccessibilityIdentifiers.Onboarding.residenceNameField)
.onSubmit {
if isValid {
onContinue()
}
}
if !residenceName.isEmpty {
Button(action: { residenceName = "" }) {
Image(systemName: "xmark.circle.fill")
.font(.system(size: 20))
.foregroundColor(Color.appTextSecondary.opacity(0.5))
}
}
}
.padding(18)
.background(
ZStack {
Color.appBackgroundSecondary
GrainTexture(opacity: 0.01)
}
)
.clipShape(RoundedRectangle(cornerRadius: 20, style: .continuous))
.overlay(
RoundedRectangle(cornerRadius: 20, style: .continuous)
.stroke(
isTextFieldFocused
? LinearGradient(colors: [Color.appPrimary, Color.appAccent], startPoint: .leading, endPoint: .trailing)
: LinearGradient(colors: [Color.appTextSecondary.opacity(0.2), Color.appTextSecondary.opacity(0.2)], startPoint: .leading, endPoint: .trailing),
lineWidth: 2
)
)
.naturalShadow(isTextFieldFocused ? .medium : .subtle)
.animation(.easeInOut(duration: 0.2), value: isTextFieldFocused)
// Name suggestions
if residenceName.isEmpty {
VStack(alignment: .leading, spacing: 8) {
Text("Need inspiration?")
.font(.system(size: 13, weight: .semibold))
.foregroundColor(Color.appTextSecondary)
.padding(.top, 4)
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 10) {
ForEach(nameSuggestions, id: \.self) { suggestion in
Button(action: {
withAnimation(.spring(response: 0.3)) {
residenceName = suggestion
}
}) {
Text(suggestion)
.font(.system(size: 13, weight: .semibold))
.foregroundColor(Color.appPrimary)
.padding(.horizontal, 14)
.padding(.vertical, 10)
.background(Color.appPrimary.opacity(0.1))
.clipShape(Capsule())
}
}
}
}
}
}
}
.padding(.horizontal, OrganicSpacing.comfortable)
}
Spacer()
// Continue button
Button(action: onContinue) {
HStack(spacing: 10) {
Text("That's Perfect!")
.font(.system(size: 17, weight: .bold))
Image(systemName: "arrow.right")
.font(.system(size: 16, weight: .bold))
}
.frame(maxWidth: .infinity)
.frame(height: 56)
.foregroundColor(Color.appTextOnPrimary)
.background(
isValid
? AnyShapeStyle(LinearGradient(colors: [Color.appPrimary, Color.appSecondary], startPoint: .leading, endPoint: .trailing))
: AnyShapeStyle(Color.appTextSecondary.opacity(0.4))
)
.clipShape(RoundedRectangle(cornerRadius: 16, style: .continuous))
.naturalShadow(isValid ? .medium : .subtle)
}
.accessibilityIdentifier(AccessibilityIdentifiers.Onboarding.nameResidenceContinueButton)
.disabled(!isValid)
.padding(.horizontal, OrganicSpacing.comfortable)
.padding(.bottom, OrganicSpacing.airy)
.animation(.easeInOut(duration: 0.2), value: isValid)
}
}
.onAppear {
isAnimating = true
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
isTextFieldFocused = true
}
}
}
}
// MARK: - Legacy wrapper with navigation bar (for backwards compatibility)
struct OnboardingNameResidenceView: View {
@Binding var residenceName: String
var onContinue: () -> Void
var onBack: () -> Void
var body: some View {
ZStack {
WarmGradientBackground()
VStack(spacing: 0) {
// Navigation bar
HStack {
Button(action: onBack) {
ZStack {
Circle()
.fill(Color.appPrimary.opacity(0.1))
.frame(width: 36, height: 36)
Image(systemName: "chevron.left")
.font(.system(size: 14, weight: .semibold))
.foregroundColor(Color.appPrimary)
}
}
Spacer()
OnboardingProgressIndicator(currentStep: 2, totalSteps: 5)
Spacer()
// Invisible spacer for alignment
Circle()
.fill(Color.clear)
.frame(width: 36, height: 36)
}
.padding(.horizontal, 20)
.padding(.vertical, 12)
OnboardingNameResidenceContent(
residenceName: $residenceName,
onContinue: onContinue
)
}
}
}
}
// MARK: - Preview
#Preview {
OnboardingNameResidenceContent(
residenceName: .constant(""),
onContinue: {}
)
}