* Add remote push notification plumbing * Checkpoint notification reliability cleanup * Prepare notification reliability PR * Fix notification PR review findings * Address notification review follow-ups * Clean up failed iOS data migration copies * Address app resume and migration cleanup review * Restore coverage for notification review fixes * Fix App Group migration guard * Address notification PR review feedback * Cover notification review edge cases * Address notification test review feedback * Address NSE migration and startup retry review * Use foreground catch-up for notification refresh * Update whitenoise-rs foreground catch-up dependency * Cover notification catch-up error paths * Fix notification review edge cases
13 KiB
AGENTS.md
Project Overview
This is a secure messaging app that uses the whitenoise Rust crate, which implements secure messaging using the Marmot Protocol with MLS (Messaging Layer Security) and Nostr.
Architecture
┌─────────────────────────────────────────────────────────────┐
│ Flutter UI Layer │
│ (screens/, widgets/, hooks/, providers/) │
├─────────────────────────────────────────────────────────────┤
│ Flutter-Rust Bridge Layer │
│ (lib/src/rust/ - auto-generated bindings) │
├─────────────────────────────────────────────────────────────┤
│ Rust API Layer │
│ (rust/src/api/ - thin wrapper around whitenoise) │
├─────────────────────────────────────────────────────────────┤
│ Whitenoise Rust Crate │
│ (external dependency - core messaging logic) │
└─────────────────────────────────────────────────────────────┘
Tech Stack
- Flutter/Dart - UI and application logic
- Rust - Core messaging/crypto functionality via FFI
- flutter_rust_bridge - Dart-Rust FFI bindings
- Riverpod - State management (shared state)
- flutter_hooks - Ephemeral widget state
- go_router - Navigation/routing
Directory Structure
whitenoise/
├── lib/ # Flutter/Dart source code
│ ├── main.dart # App entry point
│ ├── routes.dart # Route definitions (go_router)
│ ├── theme.dart # Theme colors and styles
│ ├── providers/ # Riverpod providers (shared state)
│ ├── hooks/ # Flutter hooks (ephemeral state)
│ ├── screens/ # Full-page UI components
│ ├── widgets/ # Reusable components (see Widget Naming)
│ ├── services/ # Stateless operations (API calls)
│ ├── extensions/ # Dart extensions
│ ├── utils/ # Utility functions
│ ├── constants/ # Shared constants (fixed, related sets or reused elsewhere only)
│ └── src/rust/ # Auto-generated Rust bridge code (DO NOT EDIT)
├── rust/ # Rust source code
│ └── src/api/ # API modules exposed to Flutter
├── test/ # Flutter tests (mirrors lib/ structure)
├── trees/ # Git worktrees for parallel development
├── assets/ # Images, SVGs, fonts
└── scripts/ # Build/CI scripts
Use constants/ only for fixed, related sets (e.g. NIP kinds) or constants repeated in multiple places; otherwise keep constants next to the code that uses them.
Setup Commands
# Install all dependencies
just deps
# Install Flutter dependencies only
just deps-flutter
# Install Rust dependencies only
just deps-rust
Development Commands
# Format all code (Rust + Dart)
just format
# Lint all code
just lint
# Run all tests (verbose output)
just test-flutter
just test-rust
# Run tests with coverage (99% minimum)
just coverage
# Generate coverage HTML report
just coverage-report
# Pre-commit checks (REQUIRED before every commit)
just precommit
# Pre-commit with verbose output (for debugging failures)
just precommit-verbose
# Regenerate flutter_rust_bridge code
just generate
# Rebuild Android native libraries (after Rust code/dependency changes)
just build-android-quiet
iOS Device Installs
iOS installs are data-sensitive. A normal iOS app uninstall deletes the app sandbox, including the local White Noise database. Treat every iOS reinstall command as potentially destructive until you have checked its behavior.
For iOS staging or production device testing, use this flow:
# 1. Find the connected physical iOS device.
flutter devices
xcrun devicectl list devices
# 2. Build a signed app for the flavor you intend to test.
flutter build ios --flavor staging --release
# 3. Install the built app bundle with devicectl.
xcrun devicectl device install app --device <coredevice-id> build/ios/iphoneos/Runner.app
# 4. Verify the installed bundle.
xcrun devicectl device info apps --device <coredevice-id> | rg 'dev\.ipf\.whitenoise\.staging|org\.parres\.whitenoise'
Never use flutter install for iOS staging or production-style testing. It may
print Uninstalling old version... before installing. If that happens, iOS
removes the app container and the local database is gone. Stop immediately if
any command says it is uninstalling an iOS app unless the user explicitly asked
to wipe the app.
Use flutter run --flavor staging -d <flutter-device-id> only for an active
debug session where a debug build launched from Flutter is expected. Do not use
debug installs for APNS/NSE/release-signing validation.
Before notification testing on iOS, verify the signed artifact:
codesign -d --entitlements :- build/ios/iphoneos/Runner.app 2>/dev/null
codesign -d --entitlements :- build/ios/iphoneos/Runner.app/PlugIns/WhiteNoiseNotificationServiceExtension.appex 2>/dev/null
The staging app should be dev.ipf.whitenoise.staging, APNS should usually be
aps-environment = production for ad-hoc/release staging tests, and the app plus
NSE must share group.dev.ipf.whitenoise.staging.
Quiet Commands for Agents
IMPORTANT: When verifying that code works, agents should ALWAYS use the quiet variants. These produce minimal output that is easy to parse while still showing errors on failure.
# Quiet test commands - USE THESE for verification
just test-flutter-quiet # Output: "+1093: All tests passed!" or error details
just test-rust-quiet # Output: "....... test result: ok" or error details
just build-android-quiet # Output: "✅ Android build complete" or error details
# Quiet pre-commit - USE THIS before committing
just precommit # Shows step names + ✓/✗, errors only on failure
Why quiet variants?
- Minimal output reduces context window usage
- Clear pass/fail indicators are easy to parse
- Full error details are still shown when something fails
- No noisy progress indicators or dependency resolution messages
Example quiet precommit output:
flutter deps... ✓
rust deps... ✓
l10n generation... ✓
l10n validation... ✓
auto-fix... ✓
formatting... ✓
linting... ✓
flutter tests... ✓
rust tests... ✓
✅ PRECOMMIT PASSED
Code Style
Dart/Flutter
- Single quotes for strings
prefer_const_constructorsenabledprefer_final_localsenabled- Line width: 100 characters
- Trailing commas: preserve
Widget Naming
There are three categories of widgets with different naming rules:
-
Design system widgets — Simple, presentational widgets that match the Figma design system. They have Widgetbook stories, no translations, and no Rust API calls.
- File prefixed with
wn_(e.g.,wn_filled_button.dart) - Class prefixed with
Wn(e.g.,WnFilledButton)
- File prefixed with
-
Complex reusable widgets — Used across multiple screens but contain translations, hooks with Rust API calls, or other complex logic.
- No
wn_/Wnprefix (e.g.,onboarding_carousel.dart/OnboardingCarousel)
- No
-
Screen-scoped widgets — Extracted from a single screen for simplicity, only used in that one screen.
- Prefixed with the screen name (e.g.,
ChatListTilefor a widget only used in the chat list screen)
- Prefixed with the screen name (e.g.,
Hook Naming
- Hook files prefixed with
use_(e.g.,use_chat_list.dart) - Hook functions start with
use(e.g.,useChatList())
Provider Naming
- Files end with
_provider.dart - Provider variables end with
Provider(e.g.,authProvider)
Comments
- DO NOT add comments except for code that is really complex or hard to understand.
Responsive Sizing with flutter_screenutil
- Use
flutter_screenutilfor all size values to ensure responsive layouts across devices - Use
.wfor width values, e.g.20.w - Use
.hfor height values, e.g.16.h - Use
.spfor font size and letter spacing, e.g.14.sp - Use
.rfor radius values, e.g.8.r - Apply to: padding, margins, gaps, icon sizes, font sizes, border radius, container dimensions
Avoid StatefulWidget
- In line with rules number 6 & 7 below in the Development philosophy, we should avoid the use of StatefulWidgets. Prefer to use providers (shared app-wide state) or hooks (widget-local state) instead.
Testing
IMPORTANT: Test coverage is of utmost importance. Never submit a PR that reduces test coverage.
- Test files mirror source structure with
_test.dartsuffix - Minimum coverage requirement: 99%
- Use helpers from
test/test_helpers.dart:setUpTestView(tester)- Configure test view dimensionsmountTestApp(tester, overrides)- Mount full app with provider overridesmountHook(tester, useHook)- Test individual hooksmountWidget(child, tester)- Mount single widgetmountStackedWidget(child, tester)- Mount widget in Stack
- Mock Rust API using
RustLib.initMock(api: mockApi) - Always extend
MockWnApifromtest/mocks/mock_wn_api.dartinstead of implementingRustLibApidirectly - this ensures consistent mock behavior and reuses common mock implementations - Prefer
find.byKey()overfind.byIcon()- add keys to icons in widgets and usefind.byKey(const Key('icon_name'))in tests - Use valid 64-char hex strings for pubkeys in tests (see
test_helpers.dartfor examples), not dummy values like'abc'or'test-pubkey' - Avoid
// coverage:ignore- Do not use// coverage:ignore-line,// coverage:ignore-start, or// coverage:ignore-endto bypass coverage requirements. Write tests for the code instead. The only acceptable exception is truly unreachable code (e.g., adefaultcase in a switch that is exhaustive but required by the compiler).
Development philosophy
Follow these principles when writing code:
- Simplicity over complexity - Keep the app thin
- Test all code - No untested code
- No dead code - Delete commented/unused code
- Whitenoise is source of truth - Don't duplicate logic from the Rust crate
- No caching in Flutter - Whitenoise persists data in local DB
- Shared state in providers - Use Riverpod for app-wide state
- Ephemeral state in hooks - Use flutter_hooks for widget-local state
- Pass data to hooks, not refs - Hooks receive data, not widget references
- Screens watch providers - Screens observe providers and pass data to hooks
- Self-explanatory code - Avoid comments; write clear, readable code
State Management Pattern
Screen (watches providers)
│
├── Providers (shared/persistent state)
│ └── Auth, account pubkey, etc.
│
└── Hooks (ephemeral/local state)
└── Chat list, messages, form inputs, etc.
Push Notifications
- Remote push is MIP-05/provider-token based. Flutter collects the APNS or FCM token, then passes it through
rust/src/api/notifications.rssowhitenoise-rscan own encrypted registration, gossip, and notification collection. WN_PUSH_SERVER_PUBKEYandWN_PUSH_RELAY_HINTcan be passed as--dart-definevalues to override the default public push-server pubkey and relay hint for special test builds.- Do not use Firebase Messaging in Dart. Android uses native Firebase Messaging only to obtain the FCM token and wake the existing background task from blank pushes.
- iOS remote notification content is handled by
ios/WhiteNoiseNotificationServiceExtension. The extension must share the same App Group and keychain access group asRunner; APNS pushes should still target the main app bundle ID, not the extension bundle ID. - iOS app data lives in the App Group container so the notification service extension can read the same encrypted database. Any migration touching
whitenoise/datamust be non-destructive and covered by tests. - After changing Rust API structs or functions, run
just generateand keeplib/src/rust/as generated output only.
Rust API Guidelines
- Modules in
rust/src/api/are exposed to Flutter - Functions use
#[frb]attribute for bridge generation - Structs use
#[frb(non_opaque)]for Flutter compatibility - Errors wrapped in
ApiErrorenum usingthiserror - Files in
lib/src/rust/are auto-generated - DO NOT EDIT manually
Fixing Bugs
When I report a bug, don't start by trying to fix it. Instead, start by writing a test that reproduces the bug. Then, have subagents try to fix the bug and prove it with a passing test.