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
4 changes: 2 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ Fully implemented end-to-end.
- **Geolocator**: App uses `desiredAccuracy: LocationAccuracy.high` (geolocator ^10). In geolocator 13+ prefer `locationSettings: LocationSettings(accuracy: LocationAccuracy.high)`.
- **Flutter Compass**: Code uses `FlutterCompass.events?.listen(...)` and `mounted` check for null safety. Package is lightly maintained; alternative: `sensors_plus` (magnetometer) with custom heading calculation.
- **Android**: Root `android/build.gradle` uses **jcenter()** (deprecated/removed). **compileSdkVersion 33**, **targetSdkVersion 30** — Play Store may require higher target. Kotlin plugin version needs updating (Flutter now prompts: update `org.jetbrains.kotlin.android` in `android/settings.gradle`). Debug builds fail with Java heap space — use `--release` or increase Gradle JVM heap.
- **iOS**: `ios/Podfile` exists (iOS 16.0 minimum). `firebase_options.dart` has iOS config (FlutterFire CLI has been run). Built and shipped to TestFlight from the `build-ios` CI job (see CI below) — there is no local Mac. `Info.plist` has `NSLocation*`, `NSCameraUsageDescription`, and `NSPhotoLibraryUsageDescription` strings (the last two added for report photo attachments).
- **iOS**: **iPhone-only** (`TARGETED_DEVICE_FAMILY = 1`, chosen for the first App Store release so no iPad screenshots/review are needed). App Store listing copy is in `fastlane/metadata/ios/` (deliver layout, guarded by `test/app_store_metadata_test.dart`: field limits, keyword format, no other-platform names), the 6.9" screenshot set in `screenshots/marketing/appstore/` (`screenshots/build_appstore.sh`), the support page at `web/support.html`, and the field-by-field checklist (App Privacy, age rating, review notes) in `docs/app-store-submission.md`. `ios/Podfile` exists (iOS 16.0 minimum). `firebase_options.dart` has iOS config (FlutterFire CLI has been run). Built and shipped to TestFlight from the `build-ios` CI job (see CI below) — there is no local Mac. `Info.plist` has `NSLocation*`, `NSCameraUsageDescription`, and `NSPhotoLibraryUsageDescription` strings (the last two added for report photo attachments).
- **firebase_dynamic_links**: Removed (Firebase deprecated Dynamic Links Aug 2025; was unused in lib).
- **Intro / Postman James assets**: (Fixed) `flare_flutter` / `james.flr` removed. James is `PostmanJames` in `lib/postman_james.dart` using `assets/postman_james.png`. No custom font needed — `google_fonts` provides Plus Jakarta Sans and Playfair Display.
- **Claim screen**: (Fixed) Full `initial/searching/results/empty/quiz/quizFailed/claimed` state machine. `startScoring` Cloud Function implemented with per-user claim tracking, streak updates, and leaderboard aggregation.
Expand All @@ -88,7 +88,7 @@ Fully implemented end-to-end.

## Tests

- `test/widget_test.dart` uses `firebase_auth_mocks` + `fake_cloud_firestore` and `setupFirebaseCoreMocks()` — tests run without real Firebase. **709 Dart tests passing.**
- `test/widget_test.dart` uses `firebase_auth_mocks` + `fake_cloud_firestore` and `setupFirebaseCoreMocks()` — tests run without real Firebase. **720 Dart tests passing.**
- `functions/src/test/test.index.ts` uses `firebase-functions-test`. **606 TypeScript tests passing** (pure unit tests + auth/validation integration tests that gracefully skip when no emulator is running). Includes tests for `updateFcmTokens`, `diffFriends`, `shouldNotifyFirstClaim`, `shouldNotifyOvertake`, `buildOsmChange`, `parsePhotos`, `nextQuotaState`, `pointsForMonarch`, `maxDailyFromClaims`, `repointClaimsForPostbox` (mock Firestore), `submitReport`/`reviewReport` auth & validation, and the `plan_route` CLI helpers.
- `test/cross_language_sync_test.dart` is the drift guard for facts duplicated across languages/files. Beyond the constants listed under "Added features", it now also parses source to assert: every `startScoring` call site sends an `attemptId` (Dart sheet, Wear, **and the Kotlin car**); every claim surface consults `MaintenanceGuard` and every entry point initialises Remote Config; and every collection the Cloud Functions touch has a `match` block in `firestore.rules`. Each is verified to FAIL when the thing it guards is removed. `countySlug` is checked against all 218 features of the heatmap geojson (TS side, `test.index.ts`).

Expand Down
5 changes: 3 additions & 2 deletions assets/legal/privacy_policy.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# The Postbox Game – Privacy Policy

_Last updated: September 2026_
_Last updated: October 2026_

This Privacy Policy explains how The Postbox Game ("the App", "we", "us") collects, uses, and protects your personal data when you use our mobile application. We are committed to complying with the UK General Data Protection Regulation (UK GDPR) and the Data Protection Act 2018.

Expand All @@ -10,7 +10,7 @@ The Postbox Game is an independent mobile game. For data protection enquiries, c

## 2. Data We Collect

- **Account information:** When you register or sign in with Google, we receive your email address and display name. When you register with email/password, we store your email address and chosen display name.
- **Account information:** When you register or sign in with Google, we receive your email address and display name. When you sign in with Apple, we receive your email address (or an Apple private-relay address, if you choose to hide yours) and, the first time only, your name. When you register with email/password, we store your email address and chosen display name.
- **Location data:** With your permission, we collect your precise GPS location to identify nearby postboxes and validate claims. The location you claimed from is stored with each claim for anti-cheat verification and is automatically removed after 90 days.
- **Gameplay data:** Postbox claims you make (postbox ID, timestamp, points awarded), your running scores, streaks, and leaderboard entries (display name and points only).
- **Device token:** A random per-install identifier sent with claims to detect multi-account abuse. It is not derived from your hardware and identifies only the app installation; it is removed from claims after 90 days.
Expand Down Expand Up @@ -42,6 +42,7 @@ We process your personal data on the following legal bases under UK GDPR:
We do not sell your personal data. We share data only with the following service providers, who process it on our behalf:

- **Google Firebase** (Authentication, Firestore database, Cloud Functions, Crashlytics, Performance Monitoring, Analytics) – processed within Google's infrastructure. See Google's Privacy Policy at policies.google.com/privacy.
- **Apple** (Sign in with Apple) – only if you choose to sign in with Apple; Apple confirms your identity and shares the details above. Deleting your account also revokes the App's access to your Apple ID where your device supports it. See Apple's Privacy Policy at apple.com/legal/privacy.
- **OpenStreetMap / Nominatim:** If you search for a destination in Route Mode, only the text you type is sent to the OpenStreetMap Nominatim search service – never your GPS position.

Your display name and score are visible to other users on leaderboards and to friends you add in the App. Corrections we submit back to OpenStreetMap from accepted postbox reports contain only postbox data, never anything about you.
Expand Down
120 changes: 120 additions & 0 deletions docs/app-store-submission.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
# App Store submission checklist (iOS)

Everything App Store Connect asks for before **Add for Review**, in the order it
appears. Copy lives in `fastlane/metadata/ios/` (fastlane `deliver` layout) so it is
versioned and validated by `test/app_store_metadata_test.dart`; this page covers
the parts that are questionnaires or settings rather than text.

## 1. Build

- [ ] Merge, then run **Actions → CI → Run workflow** (`build-ios`). This build is
the first to be iPhone-only (`TARGETED_DEVICE_FAMILY = 1`), so earlier
TestFlight builds must not be submitted.
- [ ] Sign in with Apple prerequisites (from the Apple sign-in PR) are done:
the capability is enabled on the App ID and the "Postbox Game App Store"
profile was regenerated afterwards. The Apple provider is enabled in Firebase Auth.
- [ ] APNs key is uploaded to Firebase Cloud Messaging.
- [ ] On the version page, under **Build**, select the new build. Export compliance
is answered automatically (`ITSAppUsesNonExemptEncryption = NO`).

## 2. Version page (English (U.K.))

| Field | Source | Limit |
|---|---|---|
| Promotional Text | `fastlane/metadata/ios/en-GB/promotional_text.txt` | 170 |
| Description | `…/description.txt` | 4000 |
| Keywords | `…/keywords.txt` | 100 |
| Support URL | `…/support_url.txt` → `web/support.html` | |
| Marketing URL | optional, leave blank | |
| Version | `1.5.3` (from `pubspec.yaml`) | |
| Copyright | `2026 <your legal name or company>` | |
| What's New | not shown for a first release | |

**Previews and Screenshots → iPhone 6.9" Display.** Upload the 8 files from
`screenshots/marketing/appstore/iphone_6.9/light/` in filename order. The dark set
is an alternative. Smaller iPhone sizes are generated from these. No iPad set is
needed now that the app is iPhone-only.

**App Previews (video)** are optional; skip them for the first release. If added
later: 15–30 s, portrait **886×1920** for the 6.9" slot, H.264 `.mov`/`.mp4`, max
30 fps, and only footage captured from the app itself.

### Screenshots: where they come from

`screenshots/build_appstore.sh [light|dark]` builds the set. For each shot, a
genuine iPhone capture in `screenshots/raw/ios/<theme>/<name>.png` wins, if one
exists. Otherwise the app screen is lifted from the existing Play final, with the
Android status bar and gesture bar painted over. The current set comes from the
Play finals, so its UI is pixel-for-pixel the same app, upscaled ~1.5×. Replacing
them with real iPhone captures from TestFlight sharpens them. Redact leaderboard
names first; `raw/` is gitignored.

## 3. App Information

| Field | Value |
|---|---|
| Name | `…/name.txt`: **The Postbox Game** (must be unique on the store) |
| Subtitle | `…/subtitle.txt` |
| Primary category | Games, subcategories **Adventure** and **Trivia** |
| Secondary category | Travel |
| Content Rights | **Yes**, it contains third-party content, and you have the rights: postbox data and map tiles are OpenStreetMap (ODbL), attributed in-app on every map |
| Age Rating | see §5 |

## 4. App Privacy

**Privacy Policy URL:** `…/privacy_url.txt`. **Tracking:** No. There are no ads, no
IDFA and no data brokers, so no App Tracking Transparency prompt is needed.

Data types to declare. All are collected; none are used for tracking.

| Data type | Linked to user | Purposes |
|---|---|---|
| Contact Info → Email Address | Yes | App Functionality |
| Contact Info → Name | Yes | App Functionality (Apple/Google sign-in name, display name) |
| Location → Precise Location | Yes | App Functionality (finding postboxes, verifying claims) |
| User Content → Photos or Videos | Yes | App Functionality (optional report photos) |
| User Content → Other User Content | Yes | App Functionality (report notes) |
| Identifiers → User ID | Yes | App Functionality, Analytics |
| Identifiers → Device ID | Yes | App Functionality (per-install anti-abuse token) |
| Usage Data → Product Interaction | Yes | Analytics (Firebase Analytics with user ID; opt-out in Settings) |
| Diagnostics → Crash Data | No | App Functionality |
| Diagnostics → Performance Data | No | App Functionality |

## 5. Age Rating questionnaire

Answer **None / No** to everything (violence, sexual content, profanity, horror,
drugs, gambling, simulated gambling, contests, medical, unrestricted web access),
except:

- **User-generated content / messaging:** No. Players can't message each other.
The only player-visible text is profanity-filtered display names, and report
photos and notes are visible only to the reporter and moderators.
- **Location:** the questionnaire asks about *sharing* location with other users.
The answer is No, because the fuzzy compass never reveals exact positions, and
other players never see yours.
- **Made for Kids:** No (the privacy policy says the app is not directed at under-13s).

Expected result: **4+**.

## 6. App Review Information

- [ ] **Sign-in required: Yes.** Create a dedicated demo account (email/password)
in the app, give it a few claims, friends and leaderboard history, and enter
its email and password here. Never commit the password.
- [ ] **Contact:** your first name, last name, phone and email.
- [ ] **Notes:** paste `fastlane/metadata/ios/review_information/notes.txt`.
First record a screen recording of a full claim at a postbox on the iPhone,
upload it (unlisted YouTube, or a shared Drive link) and replace
`[ADD VIDEO LINK]`. Reviewers are not in the UK, and without the video the
core loop can't be seen.

## 7. Pricing and Availability

- Price: **Free**.
- Availability: **United Kingdom** only for launch. Gameplay only works there, and
players elsewhere would leave "it doesn't work" reviews. App Review is unaffected.

## 8. Submit

- [ ] **Add for Review → Submit.** First reviews usually take 1–3 days.
- [ ] Choose manual or automatic release. Manual lets you check the live listing first.
32 changes: 32 additions & 0 deletions fastlane/metadata/ios/en-GB/description.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
Turn every walk into a treasure hunt. Britain's red postboxes carry the royal cypher of the monarch who reigned when they were installed, and some are well over a century old. The Postbox Game turns spotting them into a daily game.

FIND, CLAIM, SCORE
• Walk up to any postbox in the UK, scan, and name its royal cypher to claim it.
• Each postbox can be claimed once a day. Common Elizabeth II boxes earn 2 points; rare Victorian, Edward VII and Edward VIII boxes are worth far more.
• Keep a daily streak going and watch your collection grow.

HINTS, NOT DIRECTIONS
A fuzzy compass shows roughly which way unclaimed postboxes lie, never their exact spot. You still have to go and look, which is half the fun.

WALK SOMEWHERE WITH ROUTE MODE
Pick a destination and the game tells you how many postboxes, and how many points, you could pick up on the way. Add a few minutes of detour and it finds a route with more. When you pass one, you can claim it without leaving the route.

COMPETE WITH FRIENDS
• Daily, weekly, monthly and all-time leaderboards.
• Add friends and filter the leaderboard to just them.
• See every box you've claimed on your personal history map.

MEET POSTMAN JAMES
Your cheerful guide shows you the ropes and keeps you company with dry, very British commentary as you play.

PLAYS WELL OUTDOORS
• No signal? Claims are saved on your phone and submitted when you're back online.
• Light and dark themes.

HELP KEEP THE MAP RIGHT
Postbox data comes from OpenStreetMap. Report a missing postbox or a wrong cypher, with photos if you like, and accepted fixes update everyone's scores.

FAIR AND PRIVATE
Claims use your location only to check you're really at the postbox. Leaderboards show display names and points, nothing more. Analytics and crash reporting can be turned off in Settings, and you can delete your account from the app at any time.

The Postbox Game currently covers postboxes in the United Kingdom.
1 change: 1 addition & 0 deletions fastlane/metadata/ios/en-GB/keywords.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pillar box,letterbox,cypher,walking,explore,uk,history,heritage,collect,streak,outdoor,quiz,monarch
1 change: 1 addition & 0 deletions fastlane/metadata/ios/en-GB/name.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
The Postbox Game
1 change: 1 addition & 0 deletions fastlane/metadata/ios/en-GB/privacy_url.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
https://the-postbox-game.web.app/privacy-policy
1 change: 1 addition & 0 deletions fastlane/metadata/ios/en-GB/promotional_text.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Every red postbox wears a royal cypher. Spot them on your walks, claim one a day for points, and chase the rare Victorian and Edward VII boxes up the leaderboards.
1 change: 1 addition & 0 deletions fastlane/metadata/ios/en-GB/subtitle.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Hunt Britain's royal postboxes
1 change: 1 addition & 0 deletions fastlane/metadata/ios/en-GB/support_url.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
https://the-postbox-game.web.app/support
20 changes: 20 additions & 0 deletions fastlane/metadata/ios/review_information/notes.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
The Postbox Game is a location-based game played at real postboxes in the United Kingdom. A player stands within about 30 m of a postbox, scans, and names the royal cypher shown on the box to claim it for points.

SIGNING IN
Use the demo account in the Sign-In Information fields (email and password on the login screen). Sign in with Apple and Google are also offered.

TESTING OUTSIDE THE UK
Claiming requires being physically at a UK postbox, so it cannot be completed from outside the UK. Everything else works anywhere:
- Nearby tab: tap "Find nearby postboxes". Outside the UK it finds none and offers "Report a missing postbox".
- Leaderboard, Friends and History tabs show the demo account's existing claims, friends and rankings.
- Nearby tab > "Walk to a destination" (Route Mode) opens the destination picker. Route previews start from your current location and need a destination within 30 km, so they only show postboxes when used in the UK.
- A screen recording of a complete claim at a London postbox is here: [ADD VIDEO LINK]

LOCATION
Location is used only while the app is in use, to find nearby postboxes and confirm the player is at a postbox when claiming. The app shows only rough directions to postboxes, never exact locations.

ACCOUNT DELETION
Settings (top-right menu) > Delete account. Sign in with Apple accounts also have their Apple tokens revoked.

CONTENT
Display names are profanity-filtered. Photos attached to postbox reports are visible only to the reporter and our moderators.
12 changes: 6 additions & 6 deletions ios/Runner.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -302,7 +302,7 @@
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
TARGETED_DEVICE_FAMILY = 1;
VALIDATE_PRODUCT = YES;
};
name = Profile;
Expand Down Expand Up @@ -383,7 +383,7 @@
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
TARGETED_DEVICE_FAMILY = 1;
};
name = Debug;
};
Expand Down Expand Up @@ -431,7 +431,7 @@
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule";
TARGETED_DEVICE_FAMILY = "1,2";
TARGETED_DEVICE_FAMILY = 1;
VALIDATE_PRODUCT = YES;
};
name = Release;
Expand Down Expand Up @@ -549,7 +549,7 @@
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
TARGETED_DEVICE_FAMILY = 1;
};
name = "Debug-phone";
};
Expand Down Expand Up @@ -597,7 +597,7 @@
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule";
TARGETED_DEVICE_FAMILY = "1,2";
TARGETED_DEVICE_FAMILY = 1;
VALIDATE_PRODUCT = YES;
};
name = "Release-phone";
Expand Down Expand Up @@ -645,7 +645,7 @@
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
TARGETED_DEVICE_FAMILY = 1;
VALIDATE_PRODUCT = YES;
};
name = "Profile-phone";
Expand Down
17 changes: 17 additions & 0 deletions screenshots/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,23 @@ The upload-ready copies also live in the fastlane layout:
`fastlane/metadata/android/en-GB/images/{phoneScreenshots,wearScreenshots}/`
(structure only — nothing is auto-uploaded).

## App Store (iOS) set

`marketing/appstore/iphone_6.9/{light,dark}/` holds the App Store Connect iPhone set:
8 shots at **1320x2868** (the 6.9" size; App Store Connect scales smaller iPhones from
it), RGB with no alpha, in the same narrative order and with the same captions as the
Play set. Build with `./build_appstore.sh [light|dark]` (needs `fonts/`, see below);
`frame.sh ios` does the compositing.

For each shot the script prefers a genuine iPhone capture at
`raw/ios/<theme>/<name>.png` (e.g. a TestFlight screenshot, PII redacted). Without
one, it lifts the app screen out of the Play final and paints over the Android status
bar and gesture handle, because App Review rejects listings that show another platform
(guideline 2.3.10). The app is iPhone-only, so there is no iPad set.
`test/app_store_metadata_test.dart` checks the dimensions and colour type.
Listing copy and the submission checklist live in `fastlane/metadata/ios/` and
`docs/app-store-submission.md`.

## The marquee set (order = narrative)

1. Stand close. Tap. Claim. (claim CTA + Postman James + streak)
Expand Down
Loading
Loading