Files
org.parres.whitenoise/AGENTS.md
T
JeffGandGitHub d57a883e53 Implement iOS NSE notifications and Android push token plumbing (#673)
* 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
2026-05-20 14:47:09 +02:00

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_constructors enabled
  • prefer_final_locals enabled
  • Line width: 100 characters
  • Trailing commas: preserve

Widget Naming

There are three categories of widgets with different naming rules:

  1. 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)
  2. Complex reusable widgets — Used across multiple screens but contain translations, hooks with Rust API calls, or other complex logic.

    • No wn_/Wn prefix (e.g., onboarding_carousel.dart / OnboardingCarousel)
  3. Screen-scoped widgets — Extracted from a single screen for simplicity, only used in that one screen.

    • Prefixed with the screen name (e.g., ChatListTile for a widget only used in the chat list screen)

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_screenutil for all size values to ensure responsive layouts across devices
  • Use .w for width values, e.g. 20.w
  • Use .h for height values, e.g. 16.h
  • Use .sp for font size and letter spacing, e.g. 14.sp
  • Use .r for 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.dart suffix
  • Minimum coverage requirement: 99%
  • Use helpers from test/test_helpers.dart:
    • setUpTestView(tester) - Configure test view dimensions
    • mountTestApp(tester, overrides) - Mount full app with provider overrides
    • mountHook(tester, useHook) - Test individual hooks
    • mountWidget(child, tester) - Mount single widget
    • mountStackedWidget(child, tester) - Mount widget in Stack
  • Mock Rust API using RustLib.initMock(api: mockApi)
  • Always extend MockWnApi from test/mocks/mock_wn_api.dart instead of implementing RustLibApi directly - this ensures consistent mock behavior and reuses common mock implementations
  • Prefer find.byKey() over find.byIcon() - add keys to icons in widgets and use find.byKey(const Key('icon_name')) in tests
  • Use valid 64-char hex strings for pubkeys in tests (see test_helpers.dart for examples), not dummy values like 'abc' or 'test-pubkey'
  • Avoid // coverage:ignore - Do not use // coverage:ignore-line, // coverage:ignore-start, or // coverage:ignore-end to bypass coverage requirements. Write tests for the code instead. The only acceptable exception is truly unreachable code (e.g., a default case in a switch that is exhaustive but required by the compiler).

Development philosophy

Follow these principles when writing code:

  1. Simplicity over complexity - Keep the app thin
  2. Test all code - No untested code
  3. No dead code - Delete commented/unused code
  4. Whitenoise is source of truth - Don't duplicate logic from the Rust crate
  5. No caching in Flutter - Whitenoise persists data in local DB
  6. Shared state in providers - Use Riverpod for app-wide state
  7. Ephemeral state in hooks - Use flutter_hooks for widget-local state
  8. Pass data to hooks, not refs - Hooks receive data, not widget references
  9. Screens watch providers - Screens observe providers and pass data to hooks
  10. 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.rs so whitenoise-rs can own encrypted registration, gossip, and notification collection.
  • WN_PUSH_SERVER_PUBKEY and WN_PUSH_RELAY_HINT can be passed as --dart-define values 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 as Runner; 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/data must be non-destructive and covered by tests.
  • After changing Rust API structs or functions, run just generate and keep lib/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 ApiError enum using thiserror
  • 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.