Cross-platform (Flutter) mobile app + Firebase (Auth, Firestore, Cloud Functions, Crashlytics, Performance, Analytics, Storage). Core loop: find nearby postboxes (UK, monarch-era rarity), claim once per day for points; rarer = more points. Postbox data is sourced from OpenStreetMap (e.g. Overpass API); test.json is a sample of that data; the data is ingested and stored in the cloud database (Firestore) for the app to use. A fuzzy compass hints at where claimed vs unclaimed postboxes are (e.g. by rough direction), without precise locations, to encourage exploration. Login is required before play—users must sign in (e.g. Google / email) before accessing the main game. Friends and leaderboards: users can add friends and compete on daily, weekly, monthly, and lifetime leaderboards. The in-app character Postman James introduces the game on first launch and acts as a persistent advisor (Theme Park–style) at the bottom of the screen, commenting on the user's actions with light British humour.
- Role: Onboarding (first launch) and in-app advisor throughout the app.
- Placement: Introduces the concept on first launch; appears in a strip/panel at the bottom of the screen on other screens, commenting on what the user is doing.
- Tone: Light, British humour (like the advisor in Theme Park).
- Current state: James is rendered by
lib/postman_james_svg.dart(PostmanJamesSvg) using theassets/postman_james.svgasset with animated overlays (head-bob, mouth open/close, blink, star-eyes). The persistent James strip (lib/james_strip.dart) slides up at the bottom of all main screens viaJamesController/JamesControllerScopeinhome.dart. Messages are centralised inlib/james_messages.dart. Idle non-sequiturs fire every 2–5 minutes.
Auth gate is fully implemented. lib/main.dart → Unauthenticated → _UnauthGate (shows Intro on first run, then LoginScreen); Authenticated → Home. Login screen (lib/login/) supports email/password and Google; registration in lib/register/. Both have specific FirebaseAuthException error messages (not generic "Login Failure"), a loading overlay on submit, and a password visibility toggle. LoginButton is a FilledButton; GoogleLoginButton is an OutlinedButton.icon.
Both screens are implemented and accessible from the NavigationBar in Home.
- Friends (
lib/friends_screen.dart): add by UID (users/{uid}/friendsarray in Firestore), shows "Your UID" copy banner, friend cards withCircleAvatarinitials. Email lookup not yet implemented — UID only. Tapping a friend card opensUserProfilePage(lib/user_profile_page.dart) showing their stats and leaderboard rankings. - Leaderboard (
lib/leaderboard_screen.dart): Daily/Weekly/Monthly/Lifetime tabs readingleaderboards/{period}/entries. Top-3 trophy icons, current user's row highlighted. Friends-only toggle filters to_FriendsPeriodList(batchedwhereInqueries, groups of 30). Lifetime sort:uniquePostboxesClaimeddesc, thentotalPointsdesc. Backend writes{uid, displayName, points}entries (period) and{uid, displayName, uniquePostboxesClaimed, totalPoints}(lifetime); no in-app aggregation.
Display names are stored by the onUserCreated Cloud Function in users/{uid}.displayName and resolved client-side in the friends list via FutureBuilder (with a name cache). This is fully implemented.
The app shows a fuzzy compass that gives the user an indication of where claimed and unclaimed postboxes are nearby (e.g. rough direction or "something in that direction"), without giving precise directions or exact locations. Goal: encourage exploration rather than turn-by-turn navigation. Implementation: lib/fuzzy_compass.dart — to8Sectors(counts) merges 16-wind into 8-wind sectors, vagueLabel(count) returns None/One/A few/Several. _FuzzyCompassPainter draws claimed sectors grey and unclaimed sectors red, with a North marker. claimedCompassCounts and unclaimedCompassCounts are returned by the nearbyPostboxes Cloud Function. Avoid showing exact bearings or distances that would allow pinpointing.
- App entry:
lib/main.dart→ if unauthenticated →_UnauthGate→Intro(first run) orLoginScreen; if authenticated →Home.Home(lib/home.dart) is aNavigationBar+IndexedStackshell: tabs are Nearby (index 0), Claim (index 1), Leaderboard (index 2), Friends (index 3). Settings is in an AppBarPopupMenuButton. Named routes/nearby,/claim,/friends,/leaderboard,/settingsare retained for deep-link use. - Backend:
functions/src/index.tsexportsnearbyPostboxes,startScoring,updateDisplayName,onUserCreated,newDayScoreboard,registerFcmToken,onFriendAdded. Helper modules:_lookupPostboxes.ts(ngeohash + Firestore geohash prefix queries),_getPoints.ts(monarch → points: EIIR=2, GR/GVR/GVIR/SCOTTISH_CROWN=4, VR=7, EVIIR/CIIIR=9, EVIIIR=12),_leaderboardUtils.ts(period key staleness, merge/sort helpers),_nearbyUtils.ts(applyUserClaimsfor per-user claim state),_streakUtils.ts(computeNewStreak),_notifications.ts(FCM send, notification eligibility helpers). Friends list inusers/{uid}/friendsarray; leaderboards updated by Cloud Functions inleaderboards/{daily|weekly|monthly|lifetime}documents.fcmTokens/{uid}stores FCM tokens (separate collection — not exposed via world-readableusers/{uid}rules).newDayScoreboardscheduled at midnight London time; resets daily scores, rebuilds weekly/monthly from claims. - Postbox data source and storage: Postbox data is sourced from OpenStreetMap (OSM)—e.g. Overpass API (
amenity=post_box, UK area). test.json in the repo is a sample of the OSM/Overpass response: nodes withtype,id,lat,lon, andtags(e.g.amenity,ref,royal_cypher,post_box:type,collection_times,postal_code). This data is not queried from OSM at app runtime; it is ingested and stored in the cloud database (Firestore). The app and existing Cloud Functions read from Firestore only. - OSM→Firestore import pipeline: Implemented in
functions/import_postboxes.js. Run from thefunctions/directory:node import_postboxes.js <overpass-export.json> --project the-postbox-game. Stores each postbox as{ geohash (precision 9), geopoint, overpass_id, monarch?, reference? }inpostbox/{osm_<id>}with batch writes of 400. Use--dry-run --limit 5to preview. GEOHASH_PRECISION must remain 9 (maximum) so stored hashes match precision-8 prefix queries used by the 30 m claim scan. - Auth:
UserRepository+AuthenticationBloc; Google Sign-In + Email/Password;firebase_options.darthas Android, iOS, macOS, Web, and Windows configurations (generated by FlutterFire CLI).
- Functions: (Fixed) Geohash logic uses ngeohash (replacing geofirex). Firestore uses
database.collection('postboxes')anddatabase.collection('claims').add(data). index.js initializes admin withif (!admin.apps.length) admin.initializeApp();before requiring other modules. - Flutter SDK: (Fixed)
pubspec.yamlsdk: ">=3.0.0 <4.0.0". Dart 3 compatible. - Bloc: (Fixed) All blocs use constructor-based
on<Event>API (Bloc 8.x). - Geolocator: App uses
desiredAccuracy: LocationAccuracy.high(geolocator ^10). In geolocator 13+ preferlocationSettings: LocationSettings(accuracy: LocationAccuracy.high). - Flutter Compass: Code uses
FlutterCompass.events?.listen(...)andmountedcheck for null safety. Package is lightly maintained; alternative:sensors_plus(magnetometer) with custom heading calculation. - Android: Root
android/build.gradleuses jcenter() (deprecated/removed). compileSdkVersion 33, targetSdkVersion 30 — Play Store may require higher target. Kotlin plugin version needs updating (Flutter now prompts: updateorg.jetbrains.kotlin.androidinandroid/settings.gradle). Debug builds fail with Java heap space — use--releaseor increase Gradle JVM heap. - iOS: No
Podfilein repo (Flutter regenerates onpod install).firebase_options.dartnow has iOS config (FlutterFire CLI has been run). APodfilewill be needed when building for iOS. - firebase_dynamic_links: Removed (Firebase deprecated Dynamic Links Aug 2025; was unused in lib).
- Intro / Postman James assets: (Fixed)
flare_flutter/james.flrremoved. James isPostmanJamesSvginlib/postman_james_svg.dartusingassets/postman_james.svg. No custom font needed —google_fontsprovides Plus Jakarta Sans and Playfair Display. - Claim screen: (Fixed) Full
initial/searching/results/empty/quiz/quizFailed/claimedstate machine.startScoringCloud Function implemented with per-user claim tracking, streak updates, and leaderboard aggregation. - Theme: (Fixed) Centralized in
lib/theme.dart(AppTheme.light/dark,AppSpacing). Postal red#C8102Eis primary; gold#FFB400is accent; royal navy#0A1931is dark. Light and dark themes both configured. flutter_lints: Added to dev_dependencies (flutter_lints: ^6.0.0).flutter analyzereports no issues.
test/widget_test.dartusesfirebase_auth_mocks+fake_cloud_firestoreandsetupFirebaseCoreMocks()— tests run without real Firebase. 67 Dart tests passing.functions/src/test/test.index.tsusesfirebase-functions-test. 198 TypeScript tests passing (pure unit tests + auth/validation integration tests that gracefully skip when no emulator is running). Includes tests forupdateFcmTokens,diffFriends,shouldNotifyFirstClaim,shouldNotifyOvertake.
firebase_options.dart and test file reference project ID the-postbox-game and service account path; ensure no secrets in repo for store release. Use environment/config for CI and store builds.
Web build succeeds (flutter build web). Android debug build fails with Java heap space (pre-existing Gradle/JVM issue); release build blocked by Kotlin plugin version. The following are suggestions only for follow-up work.
- Android build: Update Kotlin plugin in
android/settings.gradleto latest stable. BumpcompileSdkVersion/targetSdkVersionto 34+ for Play Store. Debug builds fail with Java heap space — use--releaseor increase Gradle JVM heap. - iOS build: A
Podfilewill be needed (pod installin theios/directory). Firebase options are already configured. - Rate limiting / App Check enforcement: App Check is configured with
AndroidPlayIntegrityProviderfor release builds — ensure it is enforced in the Firebase Console. - Friend challenges / social features: Friend profile pages (
UserProfilePage) and friends-only leaderboard filtering are implemented. Friend challenges (e.g. direct head-to-head invites) are not yet implemented. - Push notifications (social): Friend-first-claim, overtake, and friend-added FCM notifications are implemented. Gameplay notifications (daily reminder, streak break, rare postbox nearby) are not yet implemented.
Already done (items from prior list that are now complete):
startScoringCloud Function (with per-user claims, streaks, leaderboard updates)- Display names stored in Firestore (
onUserCreated+updateDisplayName) - Postman James SVG strip on all main screens with idle non-sequiturs
- OSM→Firestore import script (
functions/import_postboxes.js) - Firebase/Flutter test mocks (67 Dart tests, 198 TS tests, all passing)
- iOS
firebase_options.dartconfigured via FlutterFire CLI - Staggered animations, confetti, pull-to-refresh all implemented
- FCM push notifications for social events: friend's first claim of the day, overtake, added as friend (
_notifications.ts,registerFcmToken,onFriendAdded) - Lifetime leaderboard tab (unique boxes + total points, sorted by unique boxes)
- Friends-only leaderboard toggle (
_FriendsPeriodListwith batchedwhereInqueries) UserProfilePage— friend/own profile with stats and 4-period leaderboard rankings- Android home-screen widget (
HomeWidgetService, deep-link auto-scan on tap) - OSM tile zoom hard-capped at 17 in
PostboxMap(hides postbox POI icons at ≥18) newDayScoreboardscheduled Cloud Function — midnight London rollover, weekly/monthly rebuild
- Firestore rules: (Done)
firestore.rulesrestricts all writes — postboxes/claims/leaderboards are server-only;users/{uid}client writes restricted tofriendsarray andnotificationPrefsmap only.fcmTokens/{uid}is server-write only (client read by owner only). - Cloud Functions: (Done) All callables enforce
request.auth?.uidand validate inputs (lat/lng ranges, meters bounds, name length/profanity). - Secrets: No API keys or service account JSON paths in the repo; use env/config and secure storage for release builds.
- Rate limiting / abuse: Consider rate limits on callables and abuse detection for claims (e.g. same device, location spoofing).
- PII: Limit PII in claims and leaderboard display (e.g. display names only); comply with privacy policy and data deletion requests.
- Streaks for daily claims; achievements/badges.
- “Postbox of the day” or rare-find highlights; push reminders.
- Friend challenges; seasonal or regional leaderboards.
- Sharing a claim to social; Postman James unlockable lines or reactions.
- Light narrative or collectible angle tied to monarch eras.