These were surfaced by code review and must be triaged before release:
- Account deletion implemented. In-app deletion (Profile → "Delete Account") via the
deleteAccountCloud Function: anonymizes authored signals (strips phone numbers), tombstones the user doc as "Deleted user", deletes the notifications subcollection + profile photo, then deletes the Auth user. Requires deployingfunctions+firestore:rules. See §1.8.- "My Notifications" page — DEFERRED to next release (decision: keep code, don't ship the feature). The page +
/my_notificationsroute remain in the codebase intentionally but are not wired up (no drawer/AppBar entry point), so the feature is not user-reachable and nothing to QA this release. Do not treat the orphaned page/route as a bug. Implementation plan retained in §6.6 for next release.- Edit Signal & Delete Signal flows exist in code (author-only AppBar actions on Signal Details) but were previously untested. Now covered in §3.6 and §3.7.
- Light theme only. No dark theme is defined; verify rendering under OS dark mode. See §14.1.
- App auto-creates an anonymous Firebase account on first launch
- Anonymous user can view the map, browse signals, and see signal details
- Anonymous user cannot create signals, change status, or post comments (requires authenticated account)
- Anonymous user can access Notification Settings (anonymous account created automatically if needed)
- Anonymous user sees "Sign In" in the navigation drawer
- User can register with email and password from Sign In page
- After registration, a verification email is sent automatically
- User is redirected to
/verify_emailscreen - Verification screen polls every 3 seconds for email verification status
- "Resend Email" button works with a 60-second cooldown
- Manual "Check Verification" button works
- On successful verification, user is redirected to Profile Completion
- Skip button available (in debug builds) to bypass verification
- Google Sign-In button present on Sign In page
- Google OAuth users skip email verification (auto-verified)
- Google display name pre-fills Profile Completion form
- Works on iOS with platform-specific client ID
- When an anonymous user signs in, the anonymous account is linked to the new credential (same UID preserved)
- If credential already exists on a different account, FCM tokens are merged from anonymous account into existing account
- Data (tokens, notification preferences, signal subscriptions) transfers correctly on account upgrade
- Live location is not transferred (it lives in
userLocations/{uid}and self-heals on the next GPS update); the anonymous account'suserLocationsdoc is cleaned up
- After first sign-in, user sees Profile Completion screen
- Full Name field is required (min 2 characters)
- Phone Number field is optional (min 8 characters if provided)
- Name pre-fills from Google OAuth or existing display name
- "Skip" option saves
profileCompleted: truebut skips data entry - Successful completion navigates back through auth stack to original screen
- Shows avatar (or fallback icon), display name, email, phone
- Shows email verification status ("Yes"/"No") with "Verify" button if unverified
- Shows "Member Since" date
- Shows statistics: total signals created, total comments made
- Edit mode toggle via AppBar button
- Can edit display name and phone number
- Can upload profile photo (camera icon): pick from camera or gallery
- Photo resized to 512x512 at 80% quality, stored in Firebase Storage
- Unauthenticated user sees "Please sign in" message with Sign In button
- Sign Out button visible in drawer for authenticated users
- Sign out removes device FCM token from Firestore
- User returns to home screen as anonymous after sign-out
- Drawer updates to show "Sign In" instead of profile info
- "Delete Account" action present in Profile (Danger Zone, red), authenticated users only
- Confirmation dialog warns the action is permanent and explains signals are kept but unlinked
- Cancel aborts with no changes
- On confirm:
deleteAccountCloud Function runs, then the app signs out and returns to a fresh anonymous state at home - Auth record is deleted (user can no longer sign in with the same credentials)
- User doc is tombstoned: name shows "Deleted user", PII removed (fcmTokens, notificationPreferences, signalSubscriptions, phone, photo)
- Authored signals remain on the map but show "Deleted user" as reporter and have phone numbers stripped (
contactPhone/phoneNumberempty) - Comments by the user resolve to "Deleted user"
-
users/{uid}/notificationssubcollection removed; profile photo deleted from Storage - Stored live location removed:
userLocations/{uid}document is deleted (location PII no longer lives on the user doc) - Device no longer receives push notifications meant for the deleted account
- Works for email/password, Google, and anonymous accounts
- Multi-device: deletion removes the account globally (other signed-in devices lose access)
- Error path: function failure shows an error snackbar and leaves the user signed in
- Google Maps loads on app start
- Map initially centers on user's location (if permission granted)
- Falls back to Sofia, Bulgaria (42.6977, 23.3219) if no location permission
- "My Location" blue dot appears when location permission granted
- Zoom controls work (pinch, buttons)
- Map pan/scroll works smoothly
- Signal markers appear on the map based on geo-query (100km radius from center)
- Marker colors correspond to status: Red (Help Needed), Orange (On The Way), Green (Solved)
- Marker clustering works for nearby signals
- Tapping a marker shows an info window (title + description snippet)
- Tapping the info window navigates to Signal Details screen
- Tapping the map dismisses open info windows
- New signals from other users appear on the map in real time (Firestore stream)
- Status changes to signals update marker color in real time
- Stream updates when map center changes significantly or filter changes
- FAB (add button) opens the new signal form
- GPS crosshair appears at map center to indicate signal location
- Title field is required (text capitalization enabled)
- Description field is required (text capitalization enabled)
- Phone Number field is optional (numeric keyboard)
- Signal Type dropdown with 7 options: Emergency, Lost/Found, Blood Donation, Homeless, Unneutered Animals, Wild Animals, Other
- Photo attachment: camera icon opens bottom sheet with "Take Photo" / "Choose from Gallery"
- Photo preview shows with delete (X) button
- Images compressed to max 1920x1920 at 85% quality
- Only 1 photo can be attached during creation
- Submit button disabled if title or description empty
- Loading state shown during submission
- On success: form clears, map centers on new signal, info window auto-shows
- Creator is automatically subscribed to the signal
- Requires authenticated (non-anonymous) user — shows sign-in prompt if anonymous
- Signal location uses map center coordinates at time of submission
- Partial success state: signal created but photo upload failed (shows warning)
- Displays: title, description, signal type name, creation date/time, reporter name, contact phone
- Photo carousel: swipeable PageView with photo counter (e.g., "1/5")
- Tap photo for full-screen gallery (PhotoView)
- Creator can delete individual photos (X button in corner)
- Creator can add photos (up to 5 total): "Add Photo" / "Add Another Photo" page appears
- Navigate button: opens external navigation app (geo: URI or Google Maps fallback)
- Call button: initiates phone call to signal's contact phone
- Share button: shares signal type, description, and location via native share sheet
- Status dropdown visible on details screen (authenticated users only)
- 3 options with colored icons: Help Needed (red), Somebody On The Way (orange), Solved (green)
- Changing status updates the Firestore document
- Status change logs a special comment (orange background) showing "X changed status to Y"
- Status change records
lastUpdatedByuser reference - User auto-subscribed to signal on status change
- Cannot change to same status (no-op)
- Text input field at bottom of details screen with send button
- Comments display: author name, text content, timestamp
- Regular comments have white border and left padding
- Status-change comments have orange background/border with status icon
- Comments ordered by
createdAtascending - Real-time updates via StreamBuilder
- Posting a comment auto-subscribes user to signal
- Requires authenticated user
- Lists all signals created by current user
- Ordered by creation date (newest first)
- Card-based layout with status color indicator and signal type icon
- Tap card navigates to Signal Details
- Requires authentication — shows sign-in prompt if not logged in
- Empty state message when no signals exist
- Edit (pencil) icon appears in Signal Details AppBar only for the signal's creator
- Edit icon is hidden for non-authors and anonymous users
- Tapping it opens the Edit Signal screen (
/edit_signal/:signalId) - Editable fields: Title (required), Description (required), Signal Type (dropdown), Contact Phone (optional, phone keyboard)
- Title/Description validation prevents saving when empty
- Non-author opening the route directly is bounced back (guarded by reporter check)
- Save shows loading state, success snackbar, and returns to details
- Save failure shows an error snackbar and stays on the edit screen
- Photos are not edited here (managed on the Details screen — see §3.2)
- Edited fields reflect immediately on the Details screen and on the map marker
- Delete (trash) icon appears in Signal Details AppBar only for the signal's creator
- Tapping it shows a confirmation dialog before deleting
- Cancel aborts; confirm deletes the signal document
- After deletion, user is navigated away from the (now-gone) details screen
- Deleted signal's marker disappears on the map for all users (real-time)
- Verify cascade behavior: comments subcollection, Storage photos, and subscriber records are cleaned up (or document expected orphaning)
- Filter icon in AppBar opens filter bottom sheet
- Red dot indicator on filter icon when non-default filters are active
- Last 24 hours
- Last 7 days
- Last 30 days (default)
- All time
- Toggle: Help Needed (status 0)
- Toggle: Somebody On The Way (status 1)
- Toggle: Solved (status 2)
- All enabled by default
- Toggle each of 7 types independently: Emergency, Lost/Found, Blood Donation, Homeless, Unneutered Animals, Wild Animals, Other
- All enabled by default
- "Select All" button enables all statuses and types
- "Clear All" button hides all signals
- Filter changes update markers on map in real time
- Hospital icon in AppBar toggles vet clinic mode on/off
- Active state indicated by darker background on toggle
- Vet clinics shown as blue hospital pin markers
- "Search This Area" button appears when map panned >2km or zoom changes >2 levels
- Button auto-hides after search and has 1-second debounce
- Search radius calculated from zoom level (formula-based, 1-100km)
- Results accumulated across searches (keyed by Place ID, no duplicates)
- Maximum 20 results per search
- Shows: name (large/bold), rating with star, address, phone, opening hours
- "View Reviews" link to Google Maps reviews
- "Call" button to phone clinic
- Google Maps and directions buttons
- Error state with "return to map" button if data fails
- Calls the
searchVetClinicscallable (onCall) function, invoked over HTTPS POST to thecloudfunctions.netURL (clinic details usegetVetClinicDetails) - Requires Firebase Auth token and App Check token (verify the onCall envelope is accepted)
- 15-second timeout on requests
- Rate limit handling (429 → "Too many searches" message)
- Server-side cache: repeating a search of the same area (same ~5km cell + radius) returns results without a new Places API call — served from
vetClinicCache(check function logs for "cache hit") - Details cache: opening the same clinic's details twice calls the Places details API once, then serves from
vetClinicDetails - Cached entries refresh after the 30-day TTL (stale entries trigger a fresh Places call)
- Results are unchanged from the user's perspective whether served fresh or cached
- Modal bottom sheet on first app launch explaining location tracking benefits
- "Enable" button triggers permission request flow
- "Maybe Later" dismisses but shows persistent button on map
- Completing onboarding hides all prompts permanently
Cloud Functions emit FCM messages with
typevalues:new_signal,status_change,new_comment(functions/src/index.ts). These are push-only and are not persisted to an in-app inbox (see §6.6).
- New signal nearby (
new_signal): Received when a signal is created within the user's configured radius/region and matches their type preferences - Status change (
status_change): Received when a subscribed signal's status changes - New comment (
new_comment): Received when a comment is posted on a subscribed signal (body truncated to 50 chars)
- Foreground: Shows local notification (high priority Android, sound+badge iOS)
- Background: Device processes push notification
- Terminated:
getInitialMessage()checks for notification on app launch - Tapping any notification deep-links to signal details (
/signal_details/:signalId) - After dismissing details, map focuses on that signal with info window shown
- Toggle notifications enable/disable
- Toggle location tracking enable/disable (requests permission)
- Notification radius slider (1-50 km, default 10 km)
- Signal type filter checkboxes (7 types)
- Region of interest: map-based selection with adjustable radius
- Settings persist in Firestore
notificationPreferences
- Token saved to Firestore on registration (
arrayUnionfor multi-device) - Token removed on sign-out (
arrayRemove) -
tokenLastSavedtimestamp tracked - Cloud Function deduplicates tokens across user documents (prevents orphaned anonymous accounts from receiving notifications)
- Token refresh handled automatically via
onTokenRefreshlistener - Retry logic with exponential backoff (3 retries, up to 30 seconds)
- Moving the device (location updates every 500m) writes to
userLocations/{uid}and does not rewrite the user doc — confirmonUserTokensWrittenis not invoked by location updates (check function logs/invocation count) - Token dedup (
onUserTokensWritten) still fires on genuine token registration and removes the token from other user docs
Decision (this release): keep the code, do not ship the feature.
MyNotificationsPage+ route/my_notificationsstay in the codebase but remain deliberately unreachable (no drawer item, no AppBar bell). Nothing for QA to test this release — and the orphaned page/route should not be reported as a bug.Not built yet (the gaps to close next release):
- No UI entry point navigates to the page.
- No Cloud Function writes to the
users/{uid}/notificationssubcollection it reads → inbox would be empty.- No Firestore rule grants client access to
users/{uid}/notifications(theusers/{userId}rule has no recursive wildcard) → client reads/writes would be denied.- The page's
typeswitches expectsignal_update/comment/nearby_signal, which differ from the values the functions emit (new_signal/status_change/new_comment, §6.2).Implementation plan for next release (est. ~half a day MVP, English-only):
- Server (
functions/src/index.ts): insendNotificationsToUsers()(has recipientuserIdmap + title/body/data), batch-write{ type, title, body, signalId, read:false, createdAt }into each recipient'susers/{uid}/notifications.- Rules (
firestore.rules): addmatch /users/{userId}/notifications/{id}allowing read/write whenrequest.auth.uid == userId.- Entry point (
home_route_drawer.dart): add a "Notifications" item →Routes.myNotifications; optional unread badge via aread==falsecount stream (no composite index needed).- Type alignment: reconcile the page's icon/color switches with the functions'
typestrings.- (Optional, +½ day) Localize stored content by persisting structured params and rendering l10n client-side (today's push title/body are hardcoded English).
Watch-outs: a notification doc is written per recipient on every signal/comment/status event (write cost + trigger latency — batch it); subcollection grows unbounded (page caps reads at 50; add a cap/cleanup later); reconciling the iOS app badge (
badge:1) with the real unread count is extra scope.When the feature is built next release, verify:
- An entry point exists (drawer item or AppBar bell, ideally with unread badge)
- Cloud Functions persist a notification document per push into
users/{uid}/notifications - Firestore rules permit the owner to read/mark-read/delete their notifications
- Type strings are consistent between writer (functions) and reader (page)
- List shows newest-first, limited to 50, with icon/color per type
- Unread items are bold with a dot; tapping marks read and deep-links to the signal
- Swipe-to-delete removes a single notification
- "Mark all as read" and "Clear all" overflow actions work
- Empty state and unauthenticated ("please sign in") states render correctly
handleSignalCreatednow selects recipients via geohash range queries (geofire-common) over theuserLocationscollection (live-location path) andusersregion-of-interest geohash (region path), instead of scanning every enabled user. Bounded byMAX_LOCATION_RADIUS_KM=50/MAX_REGION_RADIUS_KM=100. The precise per-user radius is still enforced by a Haversine check. SeeCOST_ANALYSIS.md.
- Location path: a user with location tracking on, within their
locationRadiusKmof a new signal, receives thenew_signalpush - Region path: a user whose region of interest covers the new signal receives the push (even with no live location tracking)
- Just-out-of-range: a user just beyond both their location radius and region radius does not receive the push (Haversine boundary, not just the geohash bucket)
- Far-away user (different city/country, beyond the 50/100 km query bounds) is excluded — no notification and no wasted read
- Signal type filter still applies (notified only for subscribed signal types)
- Signal reporter is not notified about their own signal
- Users with notifications enabled but no live location and no region of interest receive no
new_signalpushes (nothing to match on) - Test-mode isolation still holds:
testModeusers only receivesignals_testnotifications, prod users onlysignals - Multiple recipients in one area all receive the push (union of location + region paths, deduped by uid — a user matching both paths is notified once)
- Geohash compatibility: a known-near user stored by the client (
geoflutterfire_plus) is matched by the server'sgeofire-commonbounds (precision mismatch does not cause misses)
- Checks location services are enabled
- Requests "While In Use" permission first
- Can upgrade to "Always" (background) via Notification Settings
- Handles denied/permanently denied states gracefully
- Medium accuracy, 500m distance filter
- Updates the user's location in the
userLocations/{uid}collection with geopoint and geohash (kept off the user doc so location writes don't trigger the token-dedup function) - Disabling location tracking deletes the
userLocations/{uid}doc and setsnotificationPreferences.locationTrackingEnabled = false - Auto-initializes if user enabled location tracking in preferences
- Can be toggled on/off from settings
- Checks signals created in last 24 hours
- Uses GeoFlutterFire for geospatial queries
- Filters by user's signal type preferences
- Excludes user's own signals
- Tracks notified signals per session to prevent duplicates
Actual order in
home_route_drawer.dart: header logo → Profile/Sign In → Sign Out (immediately under Profile, authenticated only) → My Signals → Notification Settings → FAQs → Feedback → Privacy Policy → Our Site → About → Share App. Note: My Notifications is NOT in the drawer (see §6.6).
- Profile (authenticated) / Sign In (anonymous)
- Sign Out (authenticated only) — appears directly beneath the Profile tile at the top, not at the bottom
- My Signals — user's created signals
- Notification Settings — full notification configuration
- FAQs — 5 sections, 17 questions with answers
- Feedback — type dropdown, message, optional email, optional device info
- Privacy Policy — in-app + external website link
- Our Site — opens website (shows a launch status message)
- About — version, build, logo, description, links (Website, Facebook, GitHub), Open Source Licenses
- Share App — shares iOS App Store + Android Play Store links + website
- Shows app version and build number (from
package_info_plus) - Logo and description
- Links: Website, Facebook, GitHub
- Open Source Licenses button
- Copyright year
- Type dropdown: General, Bug, Feature, Other
- Message field (required)
- Optional email
- Optional device info checkbox (platform, OS version, app version, build)
- Submitted via Cloud Function that sends SMTP email
- English (en) fully translated (~311 keys)
- Bulgarian (bg) fully translated (key parity with
enconfirmed; remaining diffs are ICU placeholder names, not missing strings) - All UI strings use
AppLocalizations(no hardcoded strings) - Date/time formatting is locale-aware
- Signal type names, status labels, error messages, FAQ content all localized
- Same user account receives notifications on ALL registered devices
- FCM tokens stored as array — each device adds its token
- Sign-out removes only that device's token (other devices continue receiving)
- Cloud Function deduplicates: if a device registers under a new user, its token is removed from the old user document
- Signal creation on Device A visible on Device B in real time (Firestore streams)
- Signal edits (title/description/type/phone) on Device A propagate to Device B's details view and map marker
- Signal deletion on Device A removes the marker and closes the details view on Device B
- Signal deletion on Device A while its info window/overlay is open on the map on Device B: the marker disappears and the open info window + invisible tap target are dismissed cleanly — no crash (
PlatformException: Invalid markerId) and tapping where the window was does not navigate to the deleted signal - Status changes propagate across all devices viewing the same signal
- Comments appear in real time on all devices viewing the signal
- Profile edits (name, phone, photo) sync via Firestore across devices
- Notification preferences sync across devices
- Signal subscriptions shared across devices
- User A creates signal → User B (within radius, matching preferences) receives push notification
- User B changes status → User A (signal creator, auto-subscribed) receives notification
- User C comments → User A and User B (both subscribed) receive notifications; User C does not
- User D subscribes (by commenting or changing status) → User D receives future notifications for that signal
- Status change by any user updates marker color for all users viewing the map
- Multiple users can change status of the same signal — last write wins
- User starts anonymous on Device A
- Subscribes to signals, sets notification preferences
- Signs in on Device A → data transfers from anonymous account to authenticated account
- Anonymous account's FCM token moved to authenticated account
- If user was already authenticated on Device B, tokens merge (both devices receive notifications)
- Debug providers in debug builds (both platforms)
- Production: Play Integrity (Android), App Attest with Device Check fallback (iOS)
- Enforced on Cloud Functions (vet clinic search, clinic details)
- Data modifications (create signal, comment, status change) require authenticated (non-anonymous) user
- Unverified email users redirected to verification screen
- Profile page shows sign-in prompt for unauthenticated users
- Network errors display appropriate messages
- Photo upload failures show partial success state
- Vet clinic search timeout (15s) shows error
- Rate limiting (429) shows "too many searches"
- Invalid arguments handled gracefully
-
launchUrlfailures show fallback messages for calls and navigation
- All Flutter errors captured via
FlutterError.onError - Async errors captured via
PlatformDispatcher.instance.onError - User identifier set (UID only, no PII)
- Manual logging throughout codebase for key events
12. Special / Hidden Features
- Activated by tapping app title 7 times within 2 seconds
- Toggles between
signalsandsignals_testFirestore collections - Cloud Functions have separate test triggers
- Users marked with
testMode: truein Firestore - Useful for QA without polluting production data
- Package name:
org.helpapaw.helpapaw(release) /org.helpapaw.helpapaw.debug(debug) - Min SDK 23, Target SDK 35, Compile SDK 35
- Notification channel: "help_a_paw_signals" (high importance)
- Google Services configured via
google-services.json
- Bundle ID:
com.helpapaw.helpapaw(release) /com.helpapaw.helpapaw.debug(debug) - APNs token acquisition with retry logic
- GoogleService-Info plist selected by build config (debug vs release scripts)
- Google OAuth requires platform-specific client ID
- App uses a single light theme (
ThemeData(primarySwatch: orange, useMaterial3: true)) — no dark theme defined - Verify all screens render correctly when the OS is in dark mode (native dialogs, keyboards, system pickers, share sheet)
- Text remains legible and contrast acceptable throughout
- Camera denied → photo capture (signal & profile) degrades gracefully with a message
- Photo library denied → gallery picker degrades gracefully
- Notifications denied (after onboarding) → app remains usable; settings reflect disabled state
- Location denied / permanently denied → falls back to Sofia, prompts handled (see §7.1)
- Permission re-request routes user to OS settings where appropriate
- Cold launch with no network: map, cached signals, and UI don't crash
- Firestore offline cache serves previously loaded data
- Writes (signal/comment/status) queue or fail with a clear message when offline
- Recovery when connectivity returns (queued writes flush, streams resume)
- Vet clinic search and feedback show clear errors when offline
- Backgrounding during photo upload (signal creation §3.1) resumes or reports partial success
- Backgrounding mid signal creation preserves or cleanly discards form state
- Incoming call / app switch during navigation does not corrupt state
- Returning from background refreshes auth/notification state correctly
- Interactive elements expose
Semanticslabels (FAB, AppBar actions, drawer, map controls) - Screen reader (VoiceOver / TalkBack) can navigate primary flows: view signal, create signal, comment, change status
- Tap targets are adequately sized; dynamic/large font sizes don't break layouts
- Color is not the only status indicator (marker/status colors paired with labels/icons)