# Project export: Shower as a Service (SaaS)

This document was generated by HackStack to give an AI agent context about a hackathon project. Sections are labeled with their provenance; content marked as truncated was cut to keep this document small.

## Project metadata

- Hackathon: UC Berkeley AI Hackathon 2025
- Tagline: CS Majors stink. Here's the solution.
- Devpost: https://devpost.com/software/shower-as-a-service-saas
- GitHub: https://github.com/ashvathsureshkumar/brogoshower_part2
- Demo: https://x.com/createdbymason/status/1935433850664153529
- Video: https://www.youtube.com/embed/FZWOKEvj7ws?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Team: 2 GitHub contributor(s) — Mason Arditi (3 commits), Ashvath Suresh Kumar (1 commits)

## Devpost submission (written by the team)

No Devpost description available.

## README (from the GitHub repository)

# BrogoShower

An iOS application built with Swift and UIKit.

## Overview

BrogoShower is an iOS app that includes user authentication, calendar functionality, and AI integration using Claude.

## Features

- User registration and login
- Calendar interface
- AI-powered services via Claude
- Modern iOS UI design

## Project Structure

- `Controllers/` - View controllers for login, signup, and main app functionality
- `Views/` - Custom UI components including calendar cells
- `Services/` - Integration services including AI functionality
- `Assets.xcassets/` - App icons, images, and color assets
- `Utils/` - Utility classes and helpers

## Requirements

- iOS 14.0+
- Xcode 12.0+
- Swift 5.0+

## Setup

1. Clone the repository
2. Open `brogoshower_part2.xcodeproj` in Xcode
3. Build and run the project

## Architecture

The app follows the MVC (Model-View-Controller) pattern with separate controllers for different screens and a service layer for external integrations.

## AI Integration

This app uses Claude for AI functionality rather than OpenAI services.


## Detected evidence (automated analysis)

Indexed codebase: 17 recognized source files, 106 KB.
- Swift (language) — detected in the code
- Firebase (technology) — claimed on Devpost, not found in the code

## Codebase structure (from repository index)

### Files (39 of 39)

```
.DS_Store
.gitignore
brogoshower_part2.xcodeproj/project.pbxproj
brogoshower_part2.xcodeproj/project.xcworkspace/contents.xcworkspacedata
brogoshower_part2.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
brogoshower_part2.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
brogoshower_part2.xcodeproj/project.xcworkspace/xcuserdata/ashvathsureshkumar.xcuserdatad/UserInterfaceState.xcuserstate
brogoshower_part2.xcodeproj/project.xcworkspace/xcuserdata/ashvathsureshkumar.xcuserdatad/WorkspaceSettings.xcsettings
brogoshower_part2.xcodeproj/project.xcworkspace/xcuserdata/user.xcuserdatad/UserInterfaceState.xcuserstate
brogoshower_part2.xcodeproj/project.xcworkspace/xcuserdata/user.xcuserdatad/WorkspaceSettings.xcsettings
brogoshower_part2.xcodeproj/xcuserdata/ashvathsureshkumar.xcuserdatad/xcschemes/xcschememanagement.plist
brogoshower_part2.xcodeproj/xcuserdata/user.xcuserdatad/xcschemes/xcschememanagement.plist
brogoshower_part2/.DS_Store
brogoshower_part2/AppDelegate.swift
brogoshower_part2/Assets.xcassets/AccentColor.colorset/Contents.json
brogoshower_part2/Assets.xcassets/AppIcon.appiconset/Contents.json
brogoshower_part2/Assets.xcassets/Contents.json
brogoshower_part2/Assets.xcassets/diddy.imageset/Contents.json
brogoshower_part2/Assets.xcassets/saas-shower-logo.imageset/Contents.json
brogoshower_part2/Assets.xcassets/sideeye.imageset/Contents.json
brogoshower_part2/Base.lproj/LaunchScreen.storyboard
brogoshower_part2/brogoshower_part2.entitlements
brogoshower_part2/Controllers/LoginViewController.swift
brogoshower_part2/Controllers/MainViewController.swift
brogoshower_part2/Controllers/SignUpViewController.swift
brogoshower_part2/Info.plist
brogoshower_part2/SceneDelegate.swift
brogoshower_part2/Services/ClaudeService.swift
brogoshower_part2/Services/MessageService.swift
brogoshower_part2/Services/MessagesService.swift
brogoshower_part2/ViewController.swift
brogoshower_part2/Views/CalendarCell.swift
brogoshower_part2/Views/Popups/ShowerDetectedPopup.swift
brogoshower_part2/Views/Popups/SideEyePopup.swift
brogoshower_part2/Views/Popups/StinkyPopup.swift
brogoshower_part2Tests/brogoshower_part2Tests.swift
brogoshower_part2UITests/brogoshower_part2UITests.swift
brogoshower_part2UITests/brogoshower_part2UITestsLaunchTests.swift
README.md
```

### Dependencies

No dependency index available.

### Recent commits (newest first)

- link config
- add config back
- Squashed commit of the following:
- Merge branch 'main' of https://github.com/ashvathsureshkumar/brogoshower_part2
- Update README.md
- Create README.md
- read.me
- Squashed commit of the following:
- added some memes
- fixed calender look
- fixed calender look
- first commit

## Key source files (fetched from GitHub, selected and truncated for size)

### brogoshower_part2/ViewController.swift

```swift
//
//  ViewController.swift
//  brogoshower_part2
//
//  Created by Ashvath Suresh Kumar on 6/19/25.
//

import UIKit

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.
    }


}


```

### brogoshower_part2Tests/brogoshower_part2Tests.swift

```swift
//
//  brogoshower_part2Tests.swift
//  brogoshower_part2Tests
//
//  Created by Ashvath Suresh Kumar on 6/19/25.
//

import Testing
@testable import brogoshower_part2

struct brogoshower_part2Tests {

    @Test func example() async throws {
        // Write your test here and use APIs like `#expect(...)` to check expected conditions.
    }

}

```

### brogoshower_part2UITests/brogoshower_part2UITestsLaunchTests.swift

```swift
//
//  brogoshower_part2UITestsLaunchTests.swift
//  brogoshower_part2UITests
//
//  Created by Ashvath Suresh Kumar on 6/19/25.
//

import XCTest

final class brogoshower_part2UITestsLaunchTests: XCTestCase {

    override class var runsForEachTargetApplicationUIConfiguration: Bool {
        true
    }

    override func setUpWithError() throws {
        continueAfterFailure = false
    }

    @MainActor
    func testLaunch() throws {
        let app = XCUIApplication()
        app.launch()

        // Insert steps here to perform after app launch but before taking a screenshot,
        // such as logging into a test account or navigating somewhere in the app

        let attachment = XCTAttachment(screenshot: app.screenshot())
        attachment.name = "Launch Screen"
        attachment.lifetime = .keepAlways
        add(attachment)
    }
}

```

### brogoshower_part2UITests/brogoshower_part2UITests.swift

```swift
//
//  brogoshower_part2UITests.swift
//  brogoshower_part2UITests
//
//  Created by Ashvath Suresh Kumar on 6/19/25.
//

import XCTest

final class brogoshower_part2UITests: XCTestCase {

    override func setUpWithError() throws {
        // Put setup code here. This method is called before the invocation of each test method in the class.

        // In UI tests it is usually best to stop immediately when a failure occurs.
        continueAfterFailure = false

        // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this.
    }

    override func tearDownWithError() throws {
        // Put teardown code here. This method is called after the invocation of each test method in the class.
    }

    @MainActor
    func testExample() throws {
        // UI tests must launch the application that they test.
        let app = XCUIApplication()
        app.launch()

        // Use XCTAssert and related functions to verify your tests produce the correct results.
    }

    @MainActor
    func testLaunchPerformance() throws {
        // This measures how long it takes to launch your application.
        measure(metrics: [XCTApplicationLaunchMetric()]) {
            XCUIApplication().launch()
        }
    }
}

```

### brogoshower_part2/AppDelegate.swift

```swift
//
//  AppDelegate.swift
//  brogoshower_part2
//
//  Created by Ashvath Suresh Kumar on 6/19/25.
//

import UIKit

@main
class AppDelegate: UIResponder, UIApplicationDelegate {

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        // Override point for customization after application launch.
        return true
    }

    // MARK: UISceneSession Lifecycle

    func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
        // Called when a new scene session is being created.
        // Use this method to select a configuration to create the new scene with.
        return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
    }

    func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set<UISceneSession>) {
        // Called when the user discards a scene session.
        // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions.
        // Use this method to release any resources that were specific to the discarded scenes, as they will not return.
    }
}


```

### brogoshower_part2/SceneDelegate.swift

```swift
//
//  SceneDelegate.swift
//  brogoshower_part2
//
//  Created by Ashvath Suresh Kumar on 6/19/25.
//

import UIKit

class SceneDelegate: UIResponder, UIWindowSceneDelegate {

    var window: UIWindow?


    func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
        // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`.
        // If using a storyboard, the `window` property will automatically be initialized and attached to the scene.
        // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead).
        guard let windowScene = (scene as? UIWindowScene) else { return }

        window = UIWindow(windowScene: windowScene)
        
        // Check if user is already logged in
        let rootViewController: UIViewController
        if AuthService.shared.isLoggedIn() {
            // User is logged in, go directly to main screen
            let mainViewController = MainViewController()
            rootViewController = UINavigationController(rootViewController: mainViewController)
        } else {
            // User not logged in, show login screen
            let loginViewController = LoginViewController()
            rootViewController = UINavigationController(rootViewController: loginViewController)
        }
        
        window?.rootViewController = rootViewController
        window?.makeKeyAndVisible()
    }

    func sceneDidDisconnect(_ scene: UIScene) {
        // Called as the scene is being released by the system.
        // This occurs shortly after the scene enters the background, or when its session is discarded.
        // Release any resources associated with this scene that can be re-created the next time the scene connects.
        // The scene may re-connect later, as its session was not necessarily discarded (see `application:didDiscardSceneSessions` instead).
    }

    func sceneDidBecomeActive(_ scene: UIScene) {
        // Called when the scene has moved from an inactive state to an active state.
        // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive.
    }

    func sceneWillResignActive(_ scene: UIScene) {
        // Called when the scene will move from an active state to an inactive state.
        // This may occur due to temporary interruptions (ex. an incoming phone call).
    }

    func sceneWillEnterForeground(_ scene: UIScene) {
        // Called as the scene transitions from the background to the foreground.
        // Use this method to undo the changes made on entering the background.
    }

    func sceneDidEnterBackground(_ scene: UIScene) {
        // Called as the scene transitions from the foreground to the background.
        // Use this method to save data, release shared resources, and store enough scene-specific state information
        // to restore the scene back to its current state.
    }


}


```

### brogoshower_part2/Views/CalendarCell.swift

```swift
import UIKit

class CalendarCell: UICollectionViewCell {
    static let reuseIdentifier = "CalendarCell"
    
    private let colorView: UIView = {
        let view = UIView()
        view.translatesAutoresizingMaskIntoConstraints = false
        view.layer.cornerRadius = 12.0
        view.layer.masksToBounds = true
        view.layer.borderWidth = 1.0
        view.layer.borderColor = UIColor(red: 0.9, green: 0.9, blue: 0.9, alpha: 1.0).cgColor
        return view
    }()
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        contentView.addSubview(colorView)
        
        NSLayoutConstraint.activate([
            colorView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 1),
            colorView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 1),
            colorView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -1),
            colorView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -1)
        ])
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    func configure(with color: UIColor, isToday: Bool = false) {
        colorView.backgroundColor = color
        
        if isToday {
            colorView.layer.borderColor = UIColor.black.cgColor
            colorView.layer.borderWidth = 1.5
        } else {
            colorView.layer.borderColor = UIColor(red: 0.9, green: 0.9, blue: 0.9, alpha: 1.0).cgColor
            colorView.layer.borderWidth = 1.0
        }
    }
} 

```

### brogoshower_part2/Services/MessageService.swift

```swift
import Foundation
import MessageUI
import UIKit

class MessageService {
    static let shared = MessageService()
    
    private init() {}
    
    func sendChallenge(from viewController: UIViewController, delegate: MFMessageComposeViewControllerDelegate) {
        guard MFMessageComposeViewController.canSendText() else {
            let alert = UIAlertController(title: "Messages Not Available", message: "Your device cannot send text messages.", preferredStyle: .alert)
            alert.addAction(UIAlertAction(title: "OK", style: .default, handler: nil))
            viewController.present(alert, animated: true, completion: nil)
            return
        }
        
        let messageController = MFMessageComposeViewController()
        messageController.messageComposeDelegate = delegate
        messageController.body = """
        bro u fucking smell 🤢
        
        i doubt you even showered today
        
        prove me wrong. i dare u
        
        <app link>

        stinky ahh 🤮
        """
        
        viewController.present(messageController, animated: true, completion: nil)
    }
    
    static func handleMessageResult(_ result: MessageComposeResult, viewController: UIViewController) {
        switch result {
        case .cancelled:
            break
        case .sent:
            let alert = UIAlertController(title: "Challenge Sent! 🏆", message: "Your shower challenge has been sent!", preferredStyle: .alert)
            alert.addAction(UIAlertAction(title: "Awesome!", style: .default, handler: nil))
            viewController.present(alert, animated: true, completion: nil)
        case .failed:
            let alert = UIAlertController(title: "Failed to Send", message: "Could not send your challenge message.", preferredStyle: .alert)
            alert.addAction(UIAlertAction(title: "OK", style: .default, handler: nil))
            viewController.present(alert, animated: true, completion: nil)
        @unknown default:
            break
        }
    }
} 
```

### brogoshower_part2/Services/ClaudeService.swift

```swift
import Foundation
import UIKit

class AuthService {
    static let shared = AuthService()
    
    private init() {}
    
    func signUp(email: String, password: String) -> Bool {
        guard !email.isEmpty, !password.isEmpty, email.contains("@") else {
            return false
        }
        
        if getUserPassword(for: email) != nil {
            return false
        }
        
        UserDefaults.standard.set(password, forKey: "user_\(email)")
        UserDefaults.standard.set(email, forKey: "currentUser")
        return true
    }
    
    func login(email: String, password: String) -> Bool {
        guard let storedPassword = getUserPassword(for: email) else {
            return false
        }
        
        if storedPassword == password {
            UserDefaults.standard.set(email, forKey: "currentUser")
            return true
        }
        
        return false
    }
    
    func logout() {
        UserDefaults.standard.removeObject(forKey: "currentUser")
    }
    
    func getCurrentUser() -> String? {
        return UserDefaults.standard.string(forKey: "currentUser")
    }
    
    func isLoggedIn() -> Bool {
        return getCurrentUser() != nil
    }
    
    private func getUserPassword(for email: String) -> String? {
        return UserDefaults.standard.string(forKey: "user_\(email)")
    }
}

// Claude image + text analysis service
class ClaudeService {
    static let shared = ClaudeService()
    
    private init() {}
    
    func analyzeImage(image: Data, completion: @escaping (Result<String, Error>) -> Void) {
        // Compress image to stay under Claude's 5MB limit
        guard let uiImage = UIImage(data: image) else {
            completion(.failure(NSError(domain: "AnthropicError", code: 0, userInfo: [NSLocalizedDescriptionKey: "Could not create UIImage from data"])))
            return
        }
        
        let compressedImageData = compressImageForClaude(uiImage)
        let base64Image = compressedImageData.base64EncodedString()
        
        guard let url = URL(string: "https://api.anthropic.com/v1/messages") else {
            completion(.failure(NSError(domain: "AnthropicError", code: 0, userInfo: [NSLocalizedDescriptionKey: "Invalid URL"])))
            return
        }
        
        var request = URLRequest(url: url)
        request.httpMethod = "POST"
        request.setValue("application/json", forHTTPHeaderField: "Content-Type")
        request.setValue("2023-06-01", forHTTPHeaderField: "anthropic-version")
        request.setValue(getAPIKey(), forHTTPHeaderField: "x-api-key") 
        
        let requestBody: [String: Any] = [
            "model": "claude-3-5-sonnet-20241022",
            "max_tokens": 100,
            "messages": [
                [
                    "role": "user",
                    "content": [
                        [
                            "type": "image",
                            "source": [
                                "type": "base64",
                                "media_type": "image/jpeg",
                                "data": base64Image
                            ]
                        ],
                        [
                            "type": "text",
                            "text": """
                            Analyze this mobile photo and determine if someone is taking a shower. Look for: running water from showerhead, wet hair, steam/fog, shower curtain/door, bathroom tiles with water, soap/shampoo use, water droplets, person in shower position. Output only 'Showered' or 'Not Showered'.
                            """
                        ]
                    ]
                ]
            ]
        ]
        
        do {
            request.httpBody = try JSONSerialization.data(withJSONObject: requestBody)
        } catch {
            completion(.failure(error))
            return
        }
        
        URLSession.shared.dataTask(with: request) { data, response, error in
            DispatchQueue.main.async {
                if let error = error {
                    print("Network Error: \(error)")
                    completion(.failure(error))
                    return
                }
                
                guard let data = data else {
                    completion(.failure(NSError(domain: "AnthropicError", code: 0, userInfo: [NSLocalizedDescriptionKey: "No data received"])))
                    return
                }
                
                do {
                    if let json = try JSONSerialization.jsonObject(with: data) as? [String: Any],
                       let contentArray = json["content"] as? [[String: Any]],
                       let firstContent = contentArray.first,
                       let text = firstContent["text"] as? String {
                        
                        print("Claude Vision Response: \(text)")
                        
                        // Check if it starts with "Showered" (not "Not Showered")
                        if text.lowercased().hasPrefix("showered") {
                            self.saveShowerDataLocally { error in
                                if let error = error {
                                    completion(.failure(error))
                                } else {
                                    completion(.success("Showered"))
                                }
                            }
                        } else {
                            completion(.success("Not Showered"))
                        }
                    } else if let json = try JSONSerialization.jsonObject(with: data) as? [String: Any],
                              let errorInfo = json["error"] as? [String: Any],
                              let message = errorInfo["message"] as? String {
                        print("Anthropic API Error: \(message)")
                        completion(.failure(NSError(domain: "AnthropicError", code: 0, userInfo: [NSLocalize
[truncated — 3264 more characters]
```

### brogoshower_part2/Controllers/LoginViewController.swift

```swift
import UIKit

class LoginViewController: UIViewController {

    private let scrollView: UIScrollView = {
        let scrollView = UIScrollView()
        scrollView.translatesAutoresizingMaskIntoConstraints = false
        scrollView.showsVerticalScrollIndicator = false
        return scrollView
    }()
    
    private let contentView: UIView = {
        let view = UIView()
        view.translatesAutoresizingMaskIntoConstraints = false
        return view
    }()

    private let logoImageView: UIImageView = {
        let imageView = UIImageView()
        imageView.image = UIImage(systemName: "drop.circle.fill")
        imageView.tintColor = .systemBlue
        imageView.contentMode = .scaleAspectFit
        imageView.translatesAutoresizingMaskIntoConstraints = false
        return imageView
    }()

    private let titleLabel: UILabel = {
        let label = UILabel()
        label.text = "Shower as a Service 🚿"
        label.font = UIFont.systemFont(ofSize: 34, weight: .bold)
        label.textAlignment = .center
        label.textColor = .label
        label.translatesAutoresizingMaskIntoConstraints = false
        return label
    }()
    
    private let subtitleLabel: UILabel = {
        let label = UILabel()
        label.text = "Track your daily hygiene habits"
        label.font = UIFont.systemFont(ofSize: 17, weight: .regular)
        label.textAlignment = .center
        label.textColor = .secondaryLabel
        label.translatesAutoresizingMaskIntoConstraints = false
        return label
    }()

    private let emailContainerView: UIView = {
        let view = UIView()
        view.backgroundColor = .secondarySystemBackground
        view.layer.cornerRadius = 12
        view.layer.borderWidth = 1
        view.layer.borderColor = UIColor.separator.cgColor
        view.translatesAutoresizingMaskIntoConstraints = false
        return view
    }()

    private let emailTextField: UITextField = {
        let textField = UITextField()
        textField.placeholder = "Email address"
        textField.font = UIFont.systemFont(ofSize: 16)
        textField.textColor = .label
        textField.keyboardType = .emailAddress
        textField.autocapitalizationType = .none
        textField.autocorrectionType = .no
        textField.translatesAutoresizingMaskIntoConstraints = false
        return textField
    }()

    private let passwordContainerView: UIView = {
        let view = UIView()
        view.backgroundColor = .secondarySystemBackground
        view.layer.cornerRadius = 12
        view.layer.borderWidth = 1
        view.layer.borderColor = UIColor.separator.cgColor
        view.translatesAutoresizingMaskIntoConstraints = false
        return view
    }()

    private let passwordTextField: UITextField = {
        let textField = UITextField()
        textField.placeholder = "Password"
        textField.font = UIFont.systemFont(ofSize: 16)
        textField.textColor = .label
        textField.isSecureTextEntry = true
        textField.translatesAutoresizingMaskIntoConstraints = false
        return textField
    }()

    private let loginButton: UIButton = {
        let button = UIButton(type: .system)
        button.setTitle("Sign In", for: .normal)
        button.titleLabel?.font = UIFont.systemFont(ofSize: 18, weight: .semibold)
        button.backgroundColor = .systemBlue
        button.setTitleColor(.white, for: .normal)
        button.layer.cornerRadius = 16
        button.layer.shadowColor = UIColor.systemBlue.cgColor
        button.layer.shadowOffset = CGSize(width: 0, height: 4)
        button.layer.shadowRadius = 8
        button.layer.shadowOpacity = 0.3
        button.translatesAutoresizingMaskIntoConstraints = false
        button.addTarget(self, action: #selector(handleLogin), for: .touchUpInside)
        return button
    }()
    
    private let signUpButton: UIButton = {
        let button = UIButton(type: .system)
        button.setTitle("Don't have an account? Sign Up", for: .normal)
        button.titleLabel?.font = UIFont.systemFont(ofSize: 16, weight: .medium)
        button.setTitleColor(.systemBlue, for: .normal)
        button.translatesAutoresizingMaskIntoConstraints = false
        button.addTarget(self, action: #selector(handleSignUp), for: .touchUpInside)
        return button
    }()
    
    private let loadingIndicator: UIActivityIndicatorView = {
        let indicator = UIActivityIndicatorView(style: .medium)
        indicator.hidesWhenStopped = true
        indicator.translatesAutoresizingMaskIntoConstraints = false
        return indicator
    }()

    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .systemBackground
        setupViews()
        setupKeyboardObservers()
        animateInitialAppearance()
    }
    
    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        navigationController?.setNavigationBarHidden(true, animated: animated)
    }

    private func setupViews() {
        view.addSubview(scrollView)
        scrollView.addSubview(contentView)
        
        contentView.addSubview(logoImageView)
        contentView.addSubview(titleLabel)
        contentView.addSubview(subtitleLabel)
        contentView.addSubview(emailContainerView)
        contentView.addSubview(passwordContainerView)
        contentView.addSubview(loginButton)
        contentView.addSubview(signUpButton)
        contentView.addSubview(loadingIndicator)
        
        emailContainerView.addSubview(emailTextField)
        passwordContainerView.addSubview(passwordTextField)

        NSLayoutConstraint.activate([
            scrollView.topAnchor.constraint(equalTo: view.topAnchor),
            scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
            scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
            
            contentView.topAnchor.constraint(equalTo:
[truncated — 11536 more characters]
```

[5 more indexed source files omitted to keep this export small. The full file list is in the Codebase structure section above.]