# Project export: Learndeck

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: CruzHacks 2024
- Tagline: Supercharge your study sessions with AI.
- Devpost: https://devpost.com/software/learndeck
- GitHub: https://github.com/mithilp/learndeck
- Result: winner ([MLH - Auth0] Best Use of Auth0)
- Team: 3 GitHub contributor(s) — mithilp (31 commits), bub655 (17 commits), Atharv Deshmukh (6 commits)

## Devpost submission (written by the team)

### Overview

Why did we make Learndeck? Studying sucks for all of us. I'll give it to you, it feels great to knock a test out of the park, but that's because you stayed up till 3am last night hopped up on Red Bulls neglecting your girlfriend. It was pretty evident that there had to be a better way, so we checked and, uh, it was slim pickings to say the least. Instead, we put our heads together and made what's tantamount to an AI tutor. Like one of the fancy ones you'd get after inheriting your dead uncle's estate, but on your phone. How does it feel to use Learndeck? Short answer? Learndeck is a lightweight mobile app that generates a course for you based on literally any topic you give us, composed of relevant YouTube videos, written lesson plans, adaptive quizzes, and an AI chatbot that answers any questions you have. The long answer – I'm going to try and walk you through the flow here. You start your Learndeck journey by logging into the app securely (special thanks to Auth0). Now you're on the homepage where you'll see your saved courses and recently viewed courses. That part's a lot like Quizlet, and in accordance with that, there's a search function allowing you to access specific courses other folks have already generated. But say you can't find exactly what you're looking for. No worries, like we mentioned, just generate it yourself. Hit the course creation button, input the topic and the subtopics you wanted to study, and that's literally it. I swear, studying becomes so much more efficient this way. I'll be using it on my calc test next week. Okay that's sick, how did you guys pull it off? Good question. I'm going to take advantage of your attention here to shine the spotlight on my wonderful colleagues. Mithil and Anav helped work out the initial concept and push a beta. They built the skeleton of Learndeck with Gemini, React, and plenty of elbow grease. That's where Atharv jumped in, helping to bring their concept onto your phone right now. Together, and with strategic implementation of our sponsor software Auth0 and MongoDB Atlas, we're proud of the beautiful mess we ended up with. As for my contribution, I was in charge of UI/UX, branding and messaging, and writing whatever this is. You could say they assembled the organs of Learndeck and I wrapped them in skin, although that would be morbid and I'm regretting this phrasing already. Challenges, accomplishments, what we learned I'm going to hit you with these rapid fire. Challenges, we're used to 24 hour hackathons and this was insane, so pacing ourselves was definitely something weird to contend with. Accomplishments, we all got a lot better at our individual strengths and we managed to stay afloat in a sea of college students. We're all high school juniors right now, trying to beef up our portfolio you know how it is, so definitely a bit of a culture shock, right? In terms of what we learned, this is our first time building something for social good so it was a pretty unique experience that we're looking forward to doing again.

### What's next

for us Follow me on Devpost if you enjoyed this project. Every single hackathon we aim to outdo ourselves, so I guarantee the next one's going to be even better. Also, this isn't related to Learndeck at all, but I'm launching an overnight hackathon but for music this April. I'd love to give you the whole pitch if you'd be interested in potentially contributing to the event fiscally, so please reach out to me on LinkedIn. Have a good one!

## README (from the GitHub repository)

# Flutter Sample App

This sample app demonstrates the integration of the Auth0 Flutter SDK into a Flutter app. The sample is a companion to the [Auth0 Flutter Quickstart](https://auth0.com/docs/quickstart/native/flutter/interactive).

## Requirements

- Flutter 3+
- Xcode 14.x / 15.x (for iOS/macOS)
- Android Studio 4+ (for Android)

## Configure Auth0 Application

### 📱 Mobile/Desktop

Go to the settings page of your [Auth0 application](https://manage.auth0.com/#/applications/) and add the corresponding URL to **Allowed Callback URLs** and **Allowed Logout URLs**, according to the app target you want to run. If you are using a [custom domain](https://auth0.com/docs/customize/custom-domains), replace `YOUR_AUTH0_DOMAIN` with the value of your custom domain instead of the value from the settings page.

**Android**

```text
SCHEME://YOUR_AUTH0_DOMAIN/android/YOUR_PACKAGE_NAME/callback
```

**iOS**

```text
YOUR_BUNDLE_ID://YOUR_AUTH0_DOMAIN/ios/YOUR_BUNDLE_ID/callback
```

**macOS**

```text
YOUR_BUNDLE_ID://YOUR_AUTH0_DOMAIN/macos/YOUR_BUNDLE_ID/callback
```

### 🌐 Web

Go to the settings page of your [Auth0 application](https://manage.auth0.com/#/applications/) and configure the following URLs:

- Allowed Callback URLs: `http://localhost:3000`
- Allowed Logout URLs: `http://localhost:3000`
- Allowed Web Origins: `http://localhost:3000`

## Configure Auth0 Flutter

The sample app uses [flutter_dotenv](https://pub.dev/packages/flutter_dotenv) for its configuration. Rename `.env.example` to `.env` and provide the appropriate **domain** and **Client Id**. Optionally, when using Android, you can configure the scheme as well. This can be set to a custom scheme, or `https` if you want to use [Android App Links](https://auth0.com/docs/applications/enable-android-app-links). You can read more about setting this value in the [Auth0.Android SDK README](https://github.com/auth0/Auth0.Android#a-note-about-app-deep-linking).

```sh
AUTH0_DOMAIN=dev-t1qeokemoah3ctwm.us.auth0.com
AUTH0_CLIENT_ID={yourClientId}
# AUTH0_CUSTOM_SCHEME=SCHEME
```

### Android

In the sample, we are using values referenced from `android/app/src/main/res/values/strings.xml`. Rename `strings.xml.example` to `strings.xml` and fill in the respective values.

- `com_auth0_domain`: The domain of your Auth0 tenant. Generally, you can find this in the Auth0 Dashboard under your application's settings in the **Domain** field. If you are using a custom domain, you should set this to the value of your custom domain instead.
- `com_auth0_scheme`: The scheme to use. Can be a custom scheme, or `https` if you want to use [Android App Links](https://auth0.com/docs/applications/enable-android-app-links). You can read more about setting this value in the [Auth0.Android SDK README](https://github.com/auth0/Auth0.Android#a-note-about-app-deep-linking).

### iOS/macOS

The sample has already configured the required **Url Types** to ensure the callback and logout URLs can reach the app, so there is nothing to configure to run the sample on iOS and macOS.

## Run the sample

To run the project, use the [Flutter CLI's](https://docs.flutter.dev/reference/flutter-cli) `run` command.

### 📱 Mobile/Desktop

```sh
flutter run
```

Ensure you have at least one emulator running. If you have multiple running, the CLI will prompt you to select the one to run the app on.

### 🌐 Web

```sh
flutter run -d chrome --web-port 3000 --web-renderer html
```

## Compile the sample

To compile the project, use the [Flutter CLI's](https://docs.flutter.dev/reference/flutter-cli) `build` command, including the platform you want to target.

```sh
flutter build apk
flutter build ios
flutter build macos
flutter build web
```

## Issue Reporting

For general support or usage questions, use the [Auth0 Community](https://community.auth0.com/c/sdks/5) forums or raise a [support ticket](https://support.auth0.com/). Only [raise an issue](https://github.com/auth0-samples/auth0-flutter-samples/issues) if you have found a bug or want to request a feature.

**Do not report security vulnerabilities on the public GitHub issue tracker.** The [Responsible Disclosure Program](https://auth0.com/responsible-disclosure-policy) details the procedure for disclosing security issues.

## What is Auth0?

Auth0 helps you to:

* Add authentication with [multiple sources](https://auth0.com/docs/authenticate/identity-providers), either social identity providers such as **Google, Facebook, Microsoft Account, LinkedIn, GitHub, Twitter, Box, Salesforce** (amongst others), or enterprise identity systems like **Windows Azure AD, Google Apps, Active Directory, ADFS, or any SAML identity provider**.
* Add authentication through more traditional **[username/password databases](https://auth0.com/docs/authenticate/database-connections/custom-db)**.
* Add support for **[linking different user accounts](https://auth0.com/docs/manage-users/user-accounts/user-account-linking)** with the same user.
* Support for generating signed [JSON Web Tokens](https://auth0.com/docs/secure/tokens/json-web-tokens) to call your APIs and **flow the user identity** securely.
* Analytics of how, when, and where users are logging in.
* Pull data from other sources and add it to the user profile through [JavaScript Actions](https://auth0.com/docs/customize/actions).

**Why Auth0?** Because you should save time, be happy, and focus on what really matters: building your product.

## License

This project is licensed under Apache License 2.0. See the [LICENSE](../LICENSE) file for more information.


## Detected evidence (automated analysis)

Indexed codebase: 42 recognized source files, 143 KB.
- C (language) — detected in the code
- Dart (language) — detected in the code
- Kotlin (language) — detected in the code
- Swift (language) — detected in the code
- Google Gemini (technology) — claimed on Devpost, not found in the code
- MongoDB (technology) — claimed on Devpost, not found in the code

## Codebase structure (from repository index)

### Files (71 of 71)

```
.gitattributes
.gitignore
.metadata
analysis_options.yaml
android/.gitignore
android/app/build.gradle
android/app/src/androidTest/kotlin/com/example/sample/SmokeTests.kt
android/app/src/debug/AndroidManifest.xml
android/app/src/main/AndroidManifest.xml
android/app/src/main/kotlin/com/auth0/sample/MainActivity.kt
android/app/src/main/kotlin/com/example/sample/MainActivity.kt
android/app/src/main/res/drawable-v21/launch_background.xml
android/app/src/main/res/drawable/launch_background.xml
android/app/src/main/res/values-night/styles.xml
android/app/src/main/res/values/styles.xml
android/app/src/profile/AndroidManifest.xml
android/build.gradle
android/gradle.properties
android/gradle/wrapper/gradle-wrapper.properties
android/settings.gradle
ios/.gitignore
ios/Flutter/AppFrameworkInfo.plist
ios/Flutter/Debug.xcconfig
ios/Flutter/Release.xcconfig
ios/Podfile
ios/Podfile.lock
ios/Runner.xcodeproj/project.pbxproj
ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata
ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
ios/Runner.xcworkspace/contents.xcworkspacedata
ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
ios/Runner/AppDelegate.swift
ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json
ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json
ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md
ios/Runner/Base.lproj/LaunchScreen.storyboard
ios/Runner/Base.lproj/Main.storyboard
ios/Runner/Info.plist
ios/Runner/Runner-Bridging-Header.h
ios/UITests/SmokeTests.swift
lib/components/course_chapter.dart
lib/components/navbar.dart
lib/components/quiz.dart
lib/components/thumbnail.dart
lib/components/unit_field.dart
lib/main.dart
lib/screens/chapter.dart
lib/screens/course_gen/chapters.dart
lib/screens/course_gen/title.dart
lib/screens/course_gen/units.dart
lib/screens/course.dart
lib/screens/home.dart
lib/screens/library.dart
lib/screens/login.dart
lib/screens/schedule.dart
lib/screens/username.dart
lib/utils/course_generator.dart
lib/utils/gemini.dart
lib/utils/models/chapter.dart
lib/utils/models/course.dart
lib/utils/models/question.dart
lib/utils/models/unit.dart
lib/utils/models/user.dart
lib/utils/mongodb.dart
lib/utils/youtube_api.dart
pubspec.lock
pubspec.yaml
README.md
```

### Dependencies

No dependency index available.

### Recent commits (newest first)

- NavBar and reduce Units
- try more
- Calendar Implementation
- Merge branch 'main' of https://github.com/mithilp/learndeck
- Recent Courses on Home
- push Replacement to prevent going back
- Merge branch 'main' of https://github.com/mithilp/learndeck
- Setup Library Page
- Get User Library from MongoDB and Update NavBar for Library
- Gemini Fallback Retry
- Full Create a Course Backend
- Merge branch 'main' of https://github.com/mithilp/learndeck
- calendar
- Create Summary and Quizzes
- Viewing Courses Backend
- Merge branch 'main' of https://github.com/mithilp/learndeck
- Home Page Featured Section Loading
- Merge branch 'main' of https://github.com/mithilp/learndeck
- Change to Model Objects for Course Creation and Fix Loading for Chapter Name Generation
- Merge branch 'main' of https://github.com/mithilp/learndeck

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

### analysis_options.yaml

```yaml
# This file configures the analyzer, which statically analyzes Dart code to
# check for errors, warnings, and lints.
#
# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
# invoked from the command line by running `flutter analyze`.

# The following line activates a set of recommended lints for Flutter apps,
# packages, and plugins designed to encourage good coding practices.
include: package:flutter_lints/flutter.yaml

linter:
  # The lint rules applied to this project can be customized in the
  # section below to disable rules from the `package:flutter_lints/flutter.yaml`
  # included above or to enable additional rules. A list of all available lints
  # and their documentation is published at
  # https://dart-lang.github.io/linter/lints/index.html.
  #
  # Instead of disabling a lint rule for the entire project in the
  # section below, it can also be suppressed for a single line of code
  # or a specific dart file by using the `// ignore: name_of_lint` and
  # `// ignore_for_file: name_of_lint` syntax on the line or in the file
  # producing the lint.
  rules:
    avoid_print: false
    prefer_single_quotes: true

# Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options

```

### pubspec.yaml

```yaml
name: sample
description: A new Flutter project.

# The following line prevents the package from being accidentally published to
# pub.dev using `flutter pub publish`. This is preferred for private packages.
publish_to: 'none' # Remove this line if you wish to publish to pub.dev

# The following defines the version and build number for your application.
# A version number is three numbers separated by dots, like 1.2.43
# followed by an optional build number separated by a +.
# Both the version and the builder number may be overridden in flutter
# build by specifying --build-name and --build-number, respectively.
# In Android, build-name is used as versionName while build-number used as versionCode.
# Read more about Android versioning at https://developer.android.com/studio/publish/versioning
# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
# Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
version: 1.0.0+1

environment:
  sdk: ">=2.17.5 <3.0.0"
  flutter: ">=3.0.0"

# Dependencies specify other packages that your package needs in order to work.
# To automatically upgrade your package dependencies to the latest versions
# consider running `flutter pub upgrade --major-versions`. Alternatively,
# dependencies can be manually updated by changing the version numbers below to
# the latest version available on pub.dev. To see which dependencies have newer
# versions available, run `flutter pub outdated`.
dependencies:
  auth0_flutter: 1.5.0
  flutter:
    sdk: flutter
  flutter_dotenv: ^5.0.2
  google_fonts: ^4.0.3
  url_launcher: ^6.0.13
  cupertino_icons: ^1.0.2
  http: ^1.2.0
  mongo_dart: ^0.10.0
  youtube_player_flutter: ^8.1.2

dev_dependencies:
  build_runner: ^2.1.8
  flutter_test:
    sdk: flutter

  # The "flutter_lints" package below contains a set of recommended lints to
  # encourage good coding practices. The lint set provided by the package is
  # activated in the `analysis_options.yaml` file located at the root of your
  # package. See that file for information about deactivating specific lint
  # rules and activating additional ones.
  flutter_lints: ^2.0.0
  mockito: ^5.1.0
  test: ^1.16.5

# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec

# The following section is specific to Flutter packages.
flutter:

  # The following line ensures that the Material Icons font is
  # included with your application, so that you can use the icons in
  # the material Icons class.
  uses-material-design: true

  # To add assets to your application, add an assets section, like this:
  # assets:
  #   - images/a_dot_burr.jpeg
  #   - images/a_dot_ham.jpeg
  assets:
    - .env
    - assets/logo.png

  # An image asset can refer to one or more resolution-specific "variants", see
  # https://flutter.dev/assets-and-images/#resolution-aware

  # For details regarding adding assets from package dependencies, see
  # https://flutter.dev/assets-and-images/#from-packages

  # To add custom fonts to your application, add a fonts section here,
  # in this "flutter" section. Each entry in this list should have a
  # "family" key with the font family name, and a "fonts" key with a
  # list giving the asset and other descriptors for the font. For
  # example:
  # fonts:
  #   - family: Schyler
  #     fonts:
  #       - asset: fonts/Schyler-Regular.ttf
  #       - asset: fonts/Schyler-Italic.ttf
  #         style: italic
  #   - family: Trajan Pro
  #     fonts:
  #       - asset: fonts/TrajanPro.ttf
  #       - asset: fonts/TrajanPro_Bold.ttf
  #         weight: 700
  #
  # For details regarding fonts from package dependencies,
  # see https://flutter.dev/custom-fonts/#from-packages

```

### lib/main.dart

```dart
import 'package:flutter/material.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:sample/screens/login.dart';
import 'package:sample/utils/mongodb.dart';

void main() async {
  await dotenv.load();

  WidgetsFlutterBinding.ensureInitialized();
  await MongoDB.connect();

  runApp(const MaterialApp(
    home: Login(),
  ));
}

```

### ios/Runner/Runner-Bridging-Header.h

```c
#import "GeneratedPluginRegistrant.h"

```

### lib/components/course_chapter.dart

```dart
import 'package:flutter/material.dart';

class CreateCourseChapter extends StatefulWidget {

  const CreateCourseChapter({super.key});

  @override
  State<CreateCourseChapter> createState() => _CreateCourseChapterState();
}

class _CreateCourseChapterState extends State<CreateCourseChapter> {
  @override
  Widget build(BuildContext context) {
    return const Placeholder();
  }
}

```

### ios/Runner/AppDelegate.swift

```swift
import UIKit
import Flutter

@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
  override func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
  ) -> Bool {
    GeneratedPluginRegistrant.register(with: self)
    if ProcessInfo.processInfo.arguments.contains("SmokeTests") {
        self.window?.layer.speed = 0.0
        UIView.setAnimationsEnabled(false)
    }
    return super.application(application, didFinishLaunchingWithOptions: launchOptions)
  }
}

```

### lib/utils/course_generator.dart

```dart
import 'package:mongo_dart/mongo_dart.dart';
import 'package:sample/utils/models/course.dart';
import 'package:sample/utils/models/unit.dart';

class CourseGenerator {
  static Course generate(String title, String author) {
    const unitTitles = [''];

    final units = unitTitles
        .map((title) => Unit(id: ObjectId().oid, title: title))
        .toList();
    return Course(
        id: ObjectId().oid,
        title: title,
        author: author,
        units: units,
        added: true,
        image: 'https://developers.elementor.com/docs/assets/img/elementor-placeholder-image.png',
        progress: 0);
  }
}

```

### lib/utils/youtube_api.dart

```dart
import 'dart:convert';
import 'dart:io';
import 'package:http/http.dart' as http;
import 'package:flutter_dotenv/flutter_dotenv.dart';

class YouTubeAPI {
  static Future<String> getVideo(String query) async {
    final headers = {
      'Accept': 'application/json',
      'Accept-Encoding': 'gzip',
    };

    final url = Uri.parse(
        'https://youtube.googleapis.com/youtube/v3/search?part=snippet&q=${query}&type=video&videoEmbeddable=true&key=${dotenv.env['YOUTUBE_API']}');

    try {
      final res = await http.get(url, headers: headers);
      final status = res.statusCode;
      if (status != 200) throw Exception('http.get error: statusCode= $status');

      return await json.decode(res.body)['items'][0]['id']['videoId'];
    } catch (e) {
      print('Retrying...');
      sleep(Duration(seconds: 3));
      print('3s done');
      final res = await http.get(url, headers: headers);
      final status = res.statusCode;
      if (status != 200) throw Exception('http.get error: statusCode= $status');

      return await json.decode(res.body)['items'][0]['id']['videoId'];
    }
  }
}

```

### lib/screens/library.dart

```dart
import 'package:flutter/material.dart';
import 'package:sample/utils/models/course.dart';
import 'package:sample/utils/models/unit.dart';
import 'package:sample/utils/models/user.dart';
import 'package:sample/utils/mongodb.dart';
import 'package:sample/components/thumbnail.dart';

class LibraryPage extends StatefulWidget {
  final User user;
  const LibraryPage({super.key, required this.user});

  @override
  State<LibraryPage> createState() => _LibraryPageState(user: user);
}

class _LibraryPageState extends State<LibraryPage> {
  List<dynamic> library = [];
  bool loading = true;

  User user;
  _LibraryPageState({required this.user});
  void initState() {
    super.initState();
    load().then((result) {
      setState(() {
        loading = false;
      });
    });
  }

  Future<void> load() async {
    print('sending to d');
    library = await MongoDB.getStarred(widget.user.username);
    print('library ${library}');
  }
  @override
  Widget build(BuildContext context) {
    bool large = true;
    if(library.length > 3) {
      large = false;
    }

    return
      SafeArea(
        child: loading ?
        Center(
          child: CircularProgressIndicator(
            color: Color(0xff009966),
          ),
        ) : Padding(
          padding: const EdgeInsets.fromLTRB(30, 20, 30, 0.0),
          child: SingleChildScrollView(
            child: Column(
              children: [
                ListView(
                  shrinkWrap: true,
                  physics: NeverScrollableScrollPhysics(),
                  children: [
                    for(int i = 0; i < library.length; i++)
                      Column(
                      children: [
                        Thumbnail(
                            course: library[i],
                            large: large
                        ),
                        SizedBox(height: 10)
                      ],
                    )
                  ],
                ),

              ]
            ),
          ),
        ),
      );
  }
}
```

### ios/UITests/SmokeTests.swift

```swift
import XCTest

class SmokeTests: XCTestCase {
    private let email = ProcessInfo.processInfo.environment["USER_EMAIL"]!
    private let password = ProcessInfo.processInfo.environment["USER_PASSWORD"]!
    private let loginButton = "Login"
    private let logoutButton = "Logout"
    private let continueButton = "Continue"
    private let timeout: TimeInterval = 30

    override func setUp() {
        continueAfterFailure = false
        let app = XCUIApplication()
        app.launchArguments = ["SmokeTests"]
        app.launchEnvironment = ProcessInfo.processInfo.environment
        app.launch()
    }

    func testLogin() {
        let app = XCUIApplication()
        app.activate()
        tap(button: loginButton)
        let emailInput = app.webViews.textFields.firstMatch
        XCTAssertTrue(emailInput.waitForExistence(timeout: timeout))
        emailInput.tap()
        emailInput.typeText(email)
        emailInput.typeText("\n")
        let passwordInput = app.webViews.secureTextFields.firstMatch
        passwordInput.tap()
        passwordInput.typeText(password)
        passwordInput.typeText("\n")
        XCTAssertTrue(app.buttons[logoutButton].waitForExistence(timeout: timeout))
    }

    func testLogout() {
        let app = XCUIApplication()
        app.activate()
        tap(button: loginButton)
        let sessionButton = app.webViews.staticTexts[email]
        XCTAssertTrue(sessionButton.waitForExistence(timeout: timeout))
        sessionButton.tap()
        tap(button: logoutButton)
        XCTAssertTrue(app.buttons[loginButton].waitForExistence(timeout: timeout))
    }
}

private extension SmokeTests {
    func tapAlert() {
        let continueButton = XCUIApplication(bundleIdentifier: "com.apple.springboard").buttons[continueButton]
        XCTAssertTrue(continueButton.waitForExistence(timeout: timeout))
        continueButton.tap()
    }

    func tap(button label: String) {
        let button = XCUIApplication().buttons[label]
        XCTAssertTrue(button.waitForExistence(timeout: timeout))
        button.tap()
        tapAlert()
    }
}

```

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