Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
173 changes: 173 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
# CLAUDE.md — Flutter Base (Rootstrap bootstrap template)

## What this repository is

A **reusable company template**, not a product. Teams clone it to start new Flutter apps. Every change
here is inherited by future projects, so optimize for reuse and clear boundaries, and keep
product-specific assumptions out of the base modules.

If you are working in a **project cloned from this template**, the same rules apply, but the example
features (auth, onboarding, home) and the placeholders are yours to replace.
See [docs/development/bootstrap-customization.md](docs/development/bootstrap-customization.md).

## Architecture in one screen

A Dart pub workspace, managed with Melos 7, with one Flutter app and three local path packages, layered like this:

```
app ──► domain ──► common
│ ╲ ▲
│ ╲──► data ───────┘ data ──► domain (implements its interfaces)
└──────────────────────► common
```

| Package | Role | Holds |
|---|---|---|
| `app/` | Presentation + composition root | Pages, widgets, go_router routes, theme, l10n, flavor entrypoints, DI bootstrap |
| `modules/domain/` | Business logic | Cubits and states, services, repository **interfaces**, models, `EnvConfig` |
| `modules/data/` | Data access | Repository **implementations**, Dio `NetworkConfig`, interceptors, `Preferences` (shared_preferences) |
| `modules/common/` | Shared utilities with no feature knowledge | `ResultType`, `Resource`, `Failure`, platform/permission abstractions, analytics interface, validators |

- **State management:** `flutter_bloc` Cubits, which live in `domain` (not in `app`). Async screens use
`BaseCubit<T>`, which emits `Resource<T>` (`RLoading`/`RSuccess`/`RError`).
- **Result flow:** repository returns `Future<ResultType<T>>` (`TSuccess`/`TError`) → service → cubit → `Resource<T>` → widget via `BlocBuilder`.
- **DI:** GetIt. Each package exposes `XInit.initialize(getIt)`. The order in `app/lib/main/init.dart` is Common → Data → Domain.
- **Navigation:** go_router, with the `Routes` enum and `Routers.appRouter` in `app/lib/presentation/navigation/routers.dart`. Auth gating is done with route `redirect` and a root `BlocListener<AuthCubit>`.
- **Networking:** a single Dio instance from `NetworkConfig.provideDio`, with `AuthTokenInterceptor`. The base URL comes from `EnvConfig.apiUrl`.
- **Persistence:** the `Preferences` interface over `SharedPreferences` (in `data`).
- **Errors:** the sealed `Failure` hierarchy plus the `DioException.toFailure()` mapper (in `common`).
- **Flavors:** `dev` / `qa` / `prod` entrypoints + `flutter_dotenv`. Read [docs/architecture/overview.md § Environments](docs/architecture/overview.md#environments-and-flavors) before touching env handling. It has known inconsistencies.

Deeper docs:
- [docs/architecture/overview.md](docs/architecture/overview.md): layers, data flow, DI, navigation, env
- [docs/architecture/modules.md](docs/architecture/modules.md): **module boundaries, dependency rules, creating a module**
- [docs/development/feature-guide.md](docs/development/feature-guide.md): how to add a feature end to end
- [docs/development/testing.md](docs/development/testing.md): testing strategy and commands
- [docs/development/bootstrap-customization.md](docs/development/bootstrap-customization.md): what to change in a new project
- [docs/architecture/known-issues.md](docs/architecture/known-issues.md): verified defects and tech debt. **Read it before "fixing" something that looks wrong.**

## Repository map

```
app/ Flutter application (package name: app)
lib/main.dart prod entrypoint
lib/main/env/ main_dev.dart, main_qa.dart, env_config.dart (Flavor, FlavorConfig, Environment)
lib/main/init.dart composition root: dotenv load, GetIt registration, runApp
lib/main/app.dart MaterialApp.router, global BlocProviders, deep-link initial location
lib/presentation/
navigation/ Routes enum + GoRouter tree
ui/pages/<area>/<feature>/ screens (auth/login, auth/sign_up, onboarding, main/home, splash)
ui/components/ reusable design-system widgets (PrimaryButton)
ui/custom/ app-wide custom widgets (DebugBanner, Cookies, FailureWidget, ...)
ui/base/ TrackedPage + RouteObserver (analytics scaffolding)
themes/ LocalTheme, AppThemes, light/dark palettes (Material 3 tonal)
resources/ Dimen, Images (part files of resources.dart), locale/*.arb + generated/
env/ dotenv files bundled as assets (.dev, .env.example)
test/ app tests
android/ ios/ web/ linux/ platform projects
modules/domain/lib/ bloc/, services/, repositories/ (interfaces), models/, env/, init.dart
modules/data/lib/ repositories/ (impls), network/, preferences/, data_sources/ (placeholders), init.dart
modules/common/lib/ core/, devices/, analytics/, ui/, validators/, init.dart
pubspec.yaml (root) pub workspace root (`workspace:` members) + the `melos:` scripts; one shared pubspec.lock
.fvmrc pinned Flutter SDK version (FVM). CI reads it too
.github/workflows/ sonar-qube-scann.yml: analyze + tests + coverage/SonarQube on every PR and push to main
coverage/full_coverage.py multi-package LCOV merge + SonarQube upload
sonar-project.properties SonarQube config (placeholders)
addModule.py pulls a module from rootstrap/flutter-modules (see known-issues)
```

## Commands

Prerequisites: FVM (`dart pub global activate fvm`, then `fvm install` installs the Flutter version pinned in
`.fvmrc`, 3.41.3) and Melos 7 (`dart pub global activate melos`). Packages require Dart `>=3.6.0` and Flutter `>=3.41.0`.
Run Flutter through FVM (`fvm flutter …`) so you use the pinned SDK. The commands below say `flutter` for brevity.

| Task | Command (from repo root unless noted) |
|---|---|
| Install deps for the whole workspace | `melos bootstrap` |
| Check toolchain | `melos doctor` |
| Static analysis (CI gate) | `melos run analyze` (runs `dart analyze . --fatal-infos` per package) |
| Format check | `melos run format` (runs `dart format --set-exit-if-changed .` per package, and fails on unformatted code) |
| Analyze + format | `melos run lint:all` |
| Regenerate l10n after editing `.arb` | `cd app && dart run intl_utils:generate` |
| build_runner | `melos run pub:runner`. No generators are configured today (see known-issues #12). |
| Run (dev) | `cd app && flutter run -t lib/main/env/main_dev.dart --dart-define-from-file=env/.dev` |
| Run web | `melos run run:web` (uses the **prod** entrypoint `lib/main.dart` with `env/.dev`) |
| Test (CI gate) | `melos exec --dir-exists=test -- flutter test` (runs every package that has a `test/` directory) |
| Coverage + Sonar | `python3 coverage/full_coverage.py --dry-run` (drop `--dry-run` to execute; `--ci` for non-interactive) |
| Build Android | `cd app && flutter build appbundle -t lib/main.dart --dart-define-from-file=<env file>` |
| Build iOS | `cd app && flutter build ipa --release -t lib/main.dart --dart-define-from-file=<env file>` |

Flavors: iOS has `Dev`/`QA`/`Runner` schemes (`--flavor dev|qa` works on iOS). Android has **no**
`productFlavors`, so select the environment with `-t <entrypoint>` only. Details are in overview.md.

## Rules for changing this repository

1. **Respect dependency direction.** `common` depends on no workspace package. `domain` depends only on
`common`. `data` depends on `domain` + `common`. `app` depends on all three. Never import `app` from a
module, never import `data` from `domain`, and in `app` import `package:data/...` **only** from
`lib/main/init.dart` (DI). Full rules are in [modules.md](docs/architecture/modules.md).
2. **Interfaces in `domain`, implementations in `data`.** Presentation talks to cubits and services, not to
repositories or Dio. (`ui/custom/cookies.dart` violates this. It is a known exception, don't copy it.)
3. **Follow the established result pipeline.** Repositories return `ResultType<T>`. Cubits extend
`BaseCubit<T>` and use `onResult(...)` or a `switch` on `TSuccess`/`TError`. Do **not** chain
`mapSuccess`/`mapError` for side effects: `mapError` never calls its callback (known-issues #1).
4. **Register everything in the owning package's `init.dart`.** Global cubits are singletons in
`DomainInit`. Screen-scoped cubits should be created with `BlocProvider(create: ...)` at the page.
5. **Reuse before adding.** Check `common/core`, `BaseCubit`, `ListBlocState`, `PrimaryButton`,
`FailureWidget`, `Dimen`, `context.colors`/`context.theme`, and `FormValidator` first.
6. **User-facing strings go in `app/lib/presentation/resources/locale/intl_*.arb`** (both `en` and `es`),
then regenerate. Never hand-edit `locale/generated/`.
7. **Spacing and sizes come from `Dimen`**, and colors come from the theme (`Theme.of(context).colorScheme`
or `context.colors`). Don't hardcode them in new code.
8. **Keep the base generic.** No product names, endpoints, or business rules in `common`, `domain/bloc/base_cubit.dart`,
`data/network/`, or the theme scaffolding. Example features stay clearly examples.
9. **Don't introduce a second pattern** (Riverpod, Provider-only state, another HTTP client, another DI
container, freezed/json_serializable) without an explicit architecture decision.
10. **Don't edit generated or platform-generated files** (see below).
11. **Behavior changes need tests.** See [testing.md](docs/development/testing.md). There are no tests
yet, so create the package's `test/` directory as the guide describes rather than skipping. CI picks it up automatically.
12. **Report pre-existing problems; don't silently fix them** in unrelated changes. Record them in
known-issues.md instead.

### Generated / do-not-edit files
- `app/lib/presentation/resources/locale/generated/**`: intl_utils output. Edit the `.arb` files and regenerate.
- `**/generated_plugin_registrant.*`, `app/linux/flutter/generated_*`, `ios/Flutter/Generated.xcconfig`: Flutter tool output.
- `pubspec.lock` (a single workspace lock at the root, git-ignored), `.dart_tool/`, `.fvm/`, `build/`, `coverage/lcov*.info`.
- If you add build_runner generators: `*.g.dart`, `*.freezed.dart`, `*.mocks.dart` (already excluded in Sonar and coverage).

## Before implementing

1. Identify the affected package(s) and layer(s), using the table above.
2. Read the matching section in `docs/`, and the package's `lib/**/README.md` if present.
3. Find the closest existing example. **Canonical references:** auth
(`AuthRepository` → `AuthRepositoryImpl` → `AuthService` → `AuthCubit` → `login_form.dart`) for a
request/response flow, and `AppCubit` + `CommonRepository` for persisted settings.
4. Check dependency direction for every new import.
5. List the existing abstractions you will reuse.
6. Decide the tests you'll add (cubit, repository, widget).

## Definition of done

- [ ] `melos run format` passes (run `dart format .` in the package to fix)
- [ ] `melos run analyze` passes (it is `--fatal-infos`, so infos fail too)
- [ ] `melos exec --dir-exists=test -- flutter test` passes
- [ ] `.arb` edited in all locales and `dart run intl_utils:generate` output committed
- [ ] New code registered in the right `init.dart`; no forbidden imports (the rules in § Rules and modules.md)
- [ ] App still launches on the dev entrypoint; `flutter build` succeeds for platforms touched
- [ ] Docs updated if you changed a boundary, command, extension point, or env handling
- [ ] PR follows `.github/pull_request_template.md` (description, issue link, preview)

CI (`.github/workflows/sonar-qube-scann.yml`) is meant to run analyze, the tests and coverage/SonarQube on
every PR, but it currently fails before any of them run (missing `SSH_PRIVATE_KEY` secret). It also has no
format or build step. Run the whole checklist locally (known-issues #3).

## Delivery system

This repository can be worked on with Rootshift. None of its files are kept in this repository.

## Other agent instruction files

`.cursor/rules/*.mdc` and `.github/instructions/*.instructions.md` predate this file and partly
contradict the code (naming, global cubits, helpers that don't exist). **Where they conflict, this
file and `docs/` win**, because they describe the code as it is.
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,14 @@ Flutter base is a boilerplate project created by Rootstrap for new projects usin
objective is helping any new projects jump start into feature development by providing a handful of
functionalities.

## Documentation

- [CLAUDE.md](CLAUDE.md): the entry point for AI agents and a quick reference for developers (architecture, commands, rules, definition of done)
- [Architecture overview](docs/architecture/overview.md) · [Module guide](docs/architecture/modules.md) · [Known issues](docs/architecture/known-issues.md)
- [Feature development](docs/development/feature-guide.md) · [Testing](docs/development/testing.md) · [Bootstrap customization](docs/development/bootstrap-customization.md)

Where this README and `docs/` disagree, `docs/` reflects the current code (see known-issues #15).

# Features

This template comes with:
Expand Down
91 changes: 91 additions & 0 deletions docs/architecture/known-issues.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
# Known issues and technical debt

This register lists defects and debt that were verified by reading the source (on 2026-09-22). It exists
so that people and agents **don't rediscover these repeatedly, and don't fix them silently inside unrelated
changes**. Fix each one in its own PR, then delete its entry or mark it resolved. Numbers are stable
because other docs reference them.

## Correctness

**#1 `ResultType.mapError` never calls its callback, so auth errors never reach the UI.**
`modules/common/lib/core/result_type.dart`: `mapError` returns `TError(e.error)` without invoking `error(...)`.
`AuthCubit.login` / `signUp` use `..mapSuccess(...)..mapError((f) => isError(f))`, so a `TError` leaves the
cubit in `RLoading` forever. It's latent today because the fake `AuthRepositoryImpl` always succeeds. It will
surface the moment a real backend is connected. Use `BaseCubit.onResult` or a `switch` until it's fixed.

**#2 The two environment mechanisms disagree.**
- `Environment.envConfigFile` loads `env/.<ENV>` (`--dart-define ENV`, default `dev`). `EnvConfig.apiUrl` reads
`API_URL_<DEV|QA|PROD>`. The committed `env/.dev` defines `API_URL` (unsuffixed), so **the base URL is `''`**.
- `FlavorConfig.getEnvFilePath()` and `EnvConfig.envConfigFile` both return `env/.env.example` and are unused.
- The doc comments tell you to create `env/.env` and add it to assets, but the code never loads `.env`.
- `melos run run:web` runs the **prod** entrypoint (`lib/main.dart`) with `env/.dev`, so the flavor is PROD with dev values.
- `app/pubspec.yaml` bundles all of `env/` as assets, so any secret placed there ships in the binary.
- `Environment.clientSecret` / `portalUrl` read unsuffixed keys and are unused.

**#3 There are no tests, and CI fails before it checks anything.**
No package has a `test/` directory. `.github/workflows/sonar-qube-scann.yml` is enabled for PRs and pushes to `main`,
but its first step (`webfactory/ssh-agent`) fails because the `SSH_PRIVATE_KEY` secret isn't configured, so analyze,
tests and SonarQube are skipped. No pub dependency is git-based, so the step isn't needed. The workflow also has no
format-check or build step. `sonar.tests` lists `app/test` and `modules/domain/test`, and neither exists.

**#4 The analytics scaffolding isn't wired.**
No `AnalyticsClient` is registered in GetIt, so any `TrackedPage` throws on first track. `routeObserver` isn't passed
to `GoRouter(observers:)`, so the enter/exit events never fire. `FirebaseAnalytics` throws `UnimplementedError`, and
`SetupAnalytics.initialize()` is never called. `firebase_core` is a dependency, but `Firebase.initializeApp` is commented out.

**#5 Build targets point to files that don't exist.**
`ios/Flutter/Release.xcconfig` has `FLUTTER_TARGET=lib/main/env/main.dart`, but the prod entrypoint is `lib/main.dart`.
`ios/qa.xcconfig` uses `FLUTTER_TARGET=lib/main/env/main_dev.dart` and `PREFIX=dev`. The README build commands use
`-t lib/main/env/main.dart --dart-define-from-file=env_prod.json`, which is a nonexistent path and file.

**#6 `addModule.py` copies from the wrong path.**
It clones `rootstrap/flutter-modules` into `/Users/Shared` (a hardcoded macOS path) but copies from
`/Users/Shared/flutter-base/modules/<name>` and deletes `/Users/Shared/flutter-base`. The clone directory is `flutter-modules`.

**#7 Error mapping is incomplete.**
`FailureMapper` maps `DioExceptionType.connectionError` to `UnexpectedFailure`, so `ConnectionFailure` is never produced
and `ConnectionErrorWidget` is never shown. In `FailureWidget`, the case `UnexpectedErrorWidget _` matches a `Failure`
against a widget type, which is dead code. No repository uses `toFailure()` yet.

**#8 `AuthTokenInterceptor` clears every preference.**
It calls `Preferences.clear()` (which also removes theme, language, and cookie consent) on 401/403/422 and on
**every request made without a token**. It sets `Content-Type` only when a token exists.

**#9 Minor code defects.**
`Images.appLogo` → `assets/icons/logo.png` doesn't exist (and `assets/icons/` isn't declared). `Images.img()` passes
`width` as `height`. `CustomNetworkImage` force-unwraps `svgIconColor!` when `color` is non-null.

## Architecture and boundaries

**#10 Boundary exceptions.**
`app/.../ui/custom/cookies.dart` resolves `CommonRepository` directly (it skips the cubit/service). `domain` depends on
`flutter_dotenv`, which is infrastructure. `common` depends on `dio` and `flutter_bloc`. The rule set is in
[modules.md](modules.md). None of it is tool-enforced.

**#11 Service layer is pass-through.** `AuthService` only forwards to the repository, and there's no use-case layer.
That's acceptable, but it means "where business rules go" is by convention only (services).

## Tooling and template hygiene

**#12 Melos scripts.**
`pub:runner` runs `dart run build_runner` in every package, but only `app` declares `build_runner` and no generators are
configured, so it has nothing to do (and fails in packages without the dependency). `run:web` uses the prod entrypoint (see #2).

**#13 Dependency hygiene.**
- `intl_utils` (a code generator) is a runtime dependency of `app` and `common`, and `flutter_gen` is an unversioned
`app` dev dependency that nothing uses.
- Test dependencies are declared but unused, because there are no tests (#3).

**#14 Repository hygiene.**
There are empty `.github/instructions/*.md.new` files. A stray `ios/Podfile` sits at the repo root: a default Flutter-generated
Podfile that references a nonexistent `RunnerTests` target. The one the app uses is `app/ios/Podfile`. `app/linux/` contains only generated plugin registrant files (a partial platform). The template
`README.md`/`CHANGELOG.md` are still in each module.

**#15 Documentation drift.**
The root `README.md` advertises "Chat with Gemini and Vertex AI" and an "RS-GPT-Review" GitHub Action. Neither exists in
this repo. It describes Bitrise CI, but there's no config for it, and its license badge points at `rootstrap/ios-base`.
`.cursor/rules/*.mdc` and `.github/instructions/*.instructions.md` prescribe conventions the code doesn't follow
(PascalCase `AuthService.dart` file names, `ALL_CAPS` constants, "avoid global cubits", entity classes with a
`parseFlexibleNumber` helper that doesn't exist). `CLAUDE.md` and `docs/` describe the actual code.

**#16 Version sources disagree.** Pubspec `1.0.0+1`, iOS xcconfig `2.0.0`, and Android `build.properties` `1.0.0` (not read by Gradle).
Loading
Loading