- 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>
327 lines
14 KiB
Swift
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: {}
|
|
)
|
|
}
|