Skip to content

Add Clean Air Forum selfie filter, live camera guide, and IG sticker (mobile)#3755

Merged
Baalmart merged 3 commits into
stagingfrom
feature/clean-air-forum-selfie-filter-mobile
Jul 7, 2026
Merged

Add Clean Air Forum selfie filter, live camera guide, and IG sticker (mobile)#3755
Baalmart merged 3 commits into
stagingfrom
feature/clean-air-forum-selfie-filter-mobile

Conversation

@2phonebabykeem

@2phonebabykeem 2phonebabykeem commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Summary

Mobile-only half of #3753, split out per team feedback to keep mobile and website changes in separate PRs. See #3753 for the corresponding website PR (wall display + submissions API).

  • Adds a Clean Air Forum branded selfie filter and transparent Instagram sticker, generated from the user's live AQI reading, matching the approved Figma spec pixel-for-pixel.
  • Live camera screen overlays the filter chrome for framing, with gallery fallback and consent-gated submission to the conference wall display.
  • Share sheet, buttons, switch, and text field reuse the Exposure/Learn design tokens instead of one-off styling, for visual consistency with the rest of the app. Sticker can be copied straight to the clipboard for pasting into an Instagram Story.
  • Card and Forum filter shares now include the AirQo app link in the share text for conversion tracking.
  • Fixes a camera-controller lifecycle bug (stale disposed controller could be repainted on a rebuild before resume finished) and sends a shared secret header on wall submissions to match the website's new auth check.

Test plan

  • flutter analyze clean on all touched files
  • Manually verified on a physical Android device: live camera guide, gallery picker, filter/card/sticker previews, share flows, sticker clipboard copy, consent toggle, keyboard handling
  • Verify on iOS device
  • Confirm real event details (edition/date range in CleanAirForumBrand) before the forum — see docs/clean-air-forum-followups.md in the website PR

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added Clean Air Forum selfie sharing with a live camera screen and new branded filter and sticker share layouts.
    • Added shared AQI rendering helpers (text sanitization, AQI labeling/colors, and category-based icon/appearance).
    • Enhanced the share flow with a multi-tab experience and improved inline feedback/error handling.
  • Bug Fixes
    • Improved Android camera/file sharing support via manifest + FileProvider path configuration.
    • Added coverage to ensure common mojibake in AQI card text is corrected appropriately.
  • Refactor
    • Refactored share-card and share-sheet logic to use shared utilities and consistent theming.

2phonebabykeem and others added 2 commits July 7, 2026 13:22
…nd IG sticker

Lets Clean Air Forum attendees generate a branded selfie filter or transparent
Instagram sticker from their live AQI reading and optionally submit it to the
conference wall display, so the feature drives visible engagement at the booth.

- Filter/sticker/share-card templates match the approved Figma spec exactly
  (proportional scaling off a 1080px reference so text/branding stay
  pixel-accurate at any capture resolution)
- Live camera screen overlays the filter chrome so users can frame their shot
  before capturing, falling back to the system camera or gallery
- Share sheet, selfie-source picker, and consent toggle reuse the Exposure/
  Learn design tokens (buttons, switch, drag-handle chrome) for visual
  consistency with the rest of the app instead of one-off styling
- Copies the sticker straight to the clipboard (via `pasteboard`) so it can be
  pasted into an Instagram Story without a separate save/share step
- Card and filter shares now include the app link in the share text for
  conversion tracking

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Fixes a CodeRabbit finding: didChangeAppLifecycleState disposed the camera
controller on AppLifecycleState.inactive but never cleared the field, so a
rebuild before _openCamera() finished on resume could still try to paint
CameraPreview against an already-disposed controller. Now nulls the field
(and rebuilds) before disposing.

Also sends the new x-clean-air-forum-secret header (from
CLEAN_AIR_FORUM_API_SECRET) on wall submissions, matching the website's new
shared-secret check.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Adds Clean Air Forum selfie sharing to the mobile app with new camera capture, branded filter and sticker renderers, shared AQI/text utilities, upload/submission services, a multi-tab share sheet, and Android support for camera and file sharing.

Changes

Clean Air Forum Sharing Feature

Layer / File(s) Summary
Android support and dependencies
src/mobile/android/app/build.gradle, src/mobile/android/app/src/main/AndroidManifest.xml, src/mobile/android/app/src/main/res/xml/provider_paths.xml, src/mobile/pubspec.yaml
Pins Android NDK/minSdk versions, adds camera permission and FileProvider setup, defines provider paths, and adds camera and pasteboard dependencies.
Shared AQI and branding utilities
src/mobile/lib/src/app/dashboard/utils/air_quality_card_utils.dart, src/mobile/lib/src/app/dashboard/utils/clean_air_forum_branding.dart, src/mobile/test/app/dashboard/utils/air_quality_card_utils_test.dart
Adds mojibake sanitization, AQI color/label/icon helpers, Clean Air Forum branding constants/widgets, and tests for the text sanitizer.
Air quality share card refactor
src/mobile/lib/src/app/dashboard/widgets/air_quality_share_card.dart
Moves AQI/text/location formatting to shared helpers and simplifies theme-dependent card styling.
Forum filter, sticker, and camera screens
src/mobile/lib/src/app/dashboard/widgets/clean_air_forum_filter_card.dart, src/mobile/lib/src/app/dashboard/widgets/clean_air_forum_sticker_frame.dart, src/mobile/lib/src/app/dashboard/widgets/clean_air_forum_camera_screen.dart
Adds the branded filter card, transparent sticker frame, and live camera capture screen with overlay and lifecycle handling.
Sharing and submission services
src/mobile/lib/src/app/shared/services/air_quality_share_service.dart, src/mobile/lib/src/app/shared/services/clean_air_forum_submission_service.dart
Adds Clean Air Forum submission/upload handling and new share entry points plus updated share message text.
Multi-tab share sheet integration
src/mobile/lib/src/app/dashboard/widgets/air_quality_share_sheet.dart
Reworks the share sheet into card/filter/sticker tabs with selfie capture, PNG capture, clipboard copy, inline messaging, consent handling, and layout updates.
Exposure text field focus node parameter
src/mobile/lib/src/app/exposure/widgets/exposure_place_name_text_field.dart
Adds an optional external FocusNode and keeps ownership rules for internally created focus nodes.

Estimated code review effort: 4 (Complex) | ~75 minutes

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant ShareSheet as AirQualityShareSheet
  participant CameraScreen as CleanAirForumCameraScreen
  participant ShareService as AirQualityShareService
  participant SubmissionService as CleanAirForumSubmissionService
  participant Cloudinary
  participant ForumWall as Clean Air Forum wall

  User->>ShareSheet: select filter tab and tap Take selfie
  ShareSheet->>CameraScreen: push camera screen
  CameraScreen->>CameraScreen: request permission and open camera
  User->>CameraScreen: tap shutter
  CameraScreen->>ShareSheet: return captured File
  ShareSheet->>ShareService: shareCleanAirForumFilter(...)
  alt consent enabled
    ShareSheet->>SubmissionService: submitSelfie(imageBytes, measurement)
    SubmissionService->>Cloudinary: upload image bytes
    Cloudinary-->>SubmissionService: secure_url
    SubmissionService->>ForumWall: POST selfie payload
  end
  ShareSheet-->>User: inline success or error message
Loading

Possibly related PRs

Suggested labels: mobile-app

Suggested reviewers: Baalmart

Poem

A camera wakes, the tabs align,
AQI hues softly start to shine.
Cards and stickers, crisp and clear,
A forum share flow gathers near.
Clean air, neat code, one tidy spree.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main mobile feature additions: the Clean Air Forum selfie filter, live camera guide, and Instagram sticker.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/clean-air-forum-selfie-filter-mobile

Warning

Review ran into problems

🔥 Problems

Errors were encountered while retrieving linked issues.

Errors (1)
  • JIRA integration encountered authorization issues. Please disconnect and reconnect the integration in the CodeRabbit UI.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@2phonebabykeem

Copy link
Copy Markdown
Contributor Author

@copilot

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

🧹 Nitpick comments (5)
src/mobile/lib/src/app/dashboard/utils/clean_air_forum_branding.dart (1)

48-66: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Nitpick: size reads as square dimensions but only sets width.

Height is derived from aspectRatio, so callers passing size get a non-square render. Consider renaming to width for clarity, or document inline that height is auto-derived.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/mobile/lib/src/app/dashboard/utils/clean_air_forum_branding.dart` around
lines 48 - 66, The AirQoIconMark widget’s size parameter is misleading because
it only controls width while height is derived from aspectRatio in build().
Update the API in AirQoIconMark to use a clearer name like width, or add inline
documentation in the constructor/build method explaining that height is
automatically calculated from the intrinsic aspect ratio so callers don’t assume
a square render.
src/mobile/lib/src/app/dashboard/widgets/air_quality_share_card.dart (1)

120-146: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Pill background still uses the old translucent style; text already switched to the new design.

The aqiPillBackground() helper was added specifically to replace this exact translucent-over-background pattern with the "solid pale bg + solid saturated text" style, and this file already adopted the text half (color: categoryColor at line 138) but not the background half — line 128 still does categoryColor.withValues(alpha: 0.18) directly instead of aqiPillBackground(categoryColor). Looks like a partial migration; worth aligning both halves for visual consistency across share/filter/sticker cards.

♻️ Proposed fix
                   decoration: BoxDecoration(
-                    color: categoryColor.withValues(alpha: 0.18),
+                    color: aqiPillBackground(categoryColor),
                     borderRadius: BorderRadius.circular(999),
                   ),
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/mobile/lib/src/app/dashboard/widgets/air_quality_share_card.dart` around
lines 120 - 146, The AQI pill in the share card is only partially migrated to
the new design: the text already uses the saturated category color, but the
background still uses the old translucent alpha-based style. Update the pill in
air_quality_share_card.dart so the Container decoration uses
aqiPillBackground(categoryColor) instead of categoryColor.withValues(alpha:
0.18), keeping the styling consistent with the other cards that already use the
shared helper.
src/mobile/lib/src/app/exposure/widgets/exposure_place_name_text_field.dart (1)

39-57: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low value

Consider handling focusNode reassignment via didUpdateWidget.

The _focusNode getter resolves widget.focusNode lazily and the listener is only attached once in initState. If a parent ever swaps the FocusNode instance passed in after the first build, the listener stays on the stale node and the new node never gets one, silently breaking the focus-glow styling. None of the current call sites pass focusNode (controller and theme flags only, no focusNode), (controller/hint/isDark, without providing any focusNode), so this isn't triggered today, but it's a gap worth closing before other consumers (e.g. the new share sheet) start passing one in.

🛡️ Optional defensive fix
   `@override`
   void initState() {
     super.initState();
     _focusNode.addListener(_handleFocusChange);
   }
 
+  `@override`
+  void didUpdateWidget(covariant ExposurePlaceNameTextField oldWidget) {
+    super.didUpdateWidget(oldWidget);
+    if (widget.focusNode != oldWidget.focusNode) {
+      final oldNode = oldWidget.focusNode ?? _internalFocusNode;
+      oldNode?.removeListener(_handleFocusChange);
+      if (oldWidget.focusNode == null) {
+        _internalFocusNode?.dispose();
+        _internalFocusNode = null;
+      }
+      _focusNode.addListener(_handleFocusChange);
+    }
+  }
+
   void _handleFocusChange() {
     setState(() => _focused = _focusNode.hasFocus);
   }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/mobile/lib/src/app/exposure/widgets/exposure_place_name_text_field.dart`
around lines 39 - 57, The focus listener setup in
exposure_place_name_text_field.dart only happens in initState, so if the
widget’s focusNode changes later the old FocusNode keeps the listener and the
new one never gets it. Update the ExposurePlaceNameTextField state to handle
FocusNode replacement in didUpdateWidget by comparing the previous and current
widget.focusNode, removing _handleFocusChange from the old node, attaching it to
the new node, and keeping the existing dispose cleanup aligned with the
_focusNode getter and _handleFocusChange listener.
src/mobile/lib/src/app/shared/services/clean_air_forum_submission_service.dart (1)

91-96: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Validate Cloudinary config before building the request.

If NEXT_PUBLIC_CLOUDINARY_NAME/_PRESET are missing/misconfigured, cloudinaryUrl becomes malformed (e.g. .../v1_1//image/upload) and the failure surfaces only as an opaque HTTP error rather than a clear config error.

♻️ Proposed fix
   Future<String> _uploadToCloudinary(Uint8List imageBytes) async {
     final cloudName = dotenv.env['NEXT_PUBLIC_CLOUDINARY_NAME'] ?? '';
     final uploadPreset = dotenv.env['NEXT_PUBLIC_CLOUDINARY_PRESET'] ?? '';
+    if (cloudName.isEmpty || uploadPreset.isEmpty) {
+      throw Exception('Cloudinary is not configured for Clean Air Forum uploads.');
+    }
     final cloudinaryUrl =
         'https://api.cloudinary.com/v1_1/$cloudName/image/upload';
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/mobile/lib/src/app/shared/services/clean_air_forum_submission_service.dart`
around lines 91 - 96, Validate the Cloudinary configuration in
_uploadToCloudinary before constructing cloudinaryUrl. If
NEXT_PUBLIC_CLOUDINARY_NAME or NEXT_PUBLIC_CLOUDINARY_PRESET are missing or
empty, fail fast with a clear configuration error instead of continuing to build
the request; keep the check near the dotenv lookups in
clean_air_forum_submission_service.dart so the malformed URL is never created.
src/mobile/lib/src/app/dashboard/widgets/clean_air_forum_filter_card.dart (1)

62-64: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Consider an errorBuilder for the selfie image.

selfieFile can originate from the gallery picker, so a corrupt/unsupported file would fall through to Flutter's default broken-image placeholder inside the branded card. An errorBuilder falling back to _SelfiePlaceholder would keep the card looking intentional in that edge case.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/mobile/lib/src/app/dashboard/widgets/clean_air_forum_filter_card.dart`
around lines 62 - 64, The selfie image in the card currently only handles the
null case, so a corrupt or unsupported gallery file can show Flutter’s default
broken-image UI inside the branded component. Update the Image.file usage in the
Clean Air Forum filter card to include an errorBuilder that falls back to
_SelfiePlaceholder, keeping the widget intentional even when the selected selfie
fails to load.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/mobile/lib/src/app/dashboard/utils/air_quality_card_utils.dart`:
- Around line 11-22: The mojibake cleanup in sanitizeCardText is too broad and
relies on latin1.encode, which misses common Windows-1252 sequences like ’ and
“ and can also alter legitimate text. Update sanitizeCardText to use a
Windows-1252-aware decoding path and tighten the detection logic so only known
broken encodings are rewritten. Add regression tests covering one malformed
sample that should be fixed and one normal string that must remain unchanged.

In `@src/mobile/lib/src/app/dashboard/widgets/air_quality_share_sheet.dart`:
- Around line 238-260: The _openLiveCamera method can call setState after
awaiting Navigator.of(context).push without confirming the widget is still
mounted. Add the same mounted guard used in _pickSelfie before updating
_selfieFile in AirQualityShareSheet, so the state change only happens if the
widget is still in the tree; keep the existing finally block’s mounted check for
resetting _isPickingSelfie.
- Around line 157-176: The _pickSelfie method updates _selfieFile after an await
without verifying the widget is still mounted, which can throw if the sheet is
dismissed during image picking. In _pickSelfie, add a mounted check immediately
before the setState call that assigns File(picked.path), matching the existing
mounted guard used in the finally block. Keep the early return for null picks,
but ensure no state is set after disposal.
- Around line 309-333: The success message in _copySticker is shown immediately
after Pasteboard.writeImage, but that method does not guarantee the image was
actually accepted on every platform. Update _copySticker to use
platform-specific clipboard handling or a fallback validation path before
setting _stickerCopied and showing “Copied!”, and keep the failure message path
for cases where the write is ignored.

In `@src/mobile/lib/src/app/dashboard/widgets/clean_air_forum_camera_screen.dart`:
- Around line 51-58: The camera permission request in _setUp is unguarded, so a
platform channel failure can leave _initializeFuture unresolved and the UI stuck
on the loading state. Wrap Permission.camera.request() in the same kind of
try/catch used for availableCameras(), and on error set _errorMessage and stop
the setup flow so build() can show the failure instead of a permanent
CircularProgressIndicator.
- Around line 79-96: The _openCamera method in CleanAirForumCameraScreen leaves
a failed CameraController instance undisposed when _controller!.initialize()
throws, so dispose the newly created controller in the failure path before
returning and keep the previous controller cleanup as-is. Also update the
setState inside the catch block to follow the same mounted guard pattern used
elsewhere in this widget, referencing _openCamera and the controller
initialization flow.

In
`@src/mobile/lib/src/app/shared/services/clean_air_forum_submission_service.dart`:
- Around line 56-68: The shared secret in CleanAirForumSubmissionService is only
a weak, client-embedded deterrent because CLEAN_AIR_FORUM_API_SECRET is loaded
from the app bundle and can be recovered. Keep the temporary mock behavior if
intended, but update the class/service docs and implementation around the
request header in clean_air_forum_submission_service.dart to clearly treat this
as interim only and ensure the planned real backend swap uses a stronger
request-specific mechanism, such as server-issued short-lived tokens, instead of
relying on the bundled secret.

---

Nitpick comments:
In `@src/mobile/lib/src/app/dashboard/utils/clean_air_forum_branding.dart`:
- Around line 48-66: The AirQoIconMark widget’s size parameter is misleading
because it only controls width while height is derived from aspectRatio in
build(). Update the API in AirQoIconMark to use a clearer name like width, or
add inline documentation in the constructor/build method explaining that height
is automatically calculated from the intrinsic aspect ratio so callers don’t
assume a square render.

In `@src/mobile/lib/src/app/dashboard/widgets/air_quality_share_card.dart`:
- Around line 120-146: The AQI pill in the share card is only partially migrated
to the new design: the text already uses the saturated category color, but the
background still uses the old translucent alpha-based style. Update the pill in
air_quality_share_card.dart so the Container decoration uses
aqiPillBackground(categoryColor) instead of categoryColor.withValues(alpha:
0.18), keeping the styling consistent with the other cards that already use the
shared helper.

In `@src/mobile/lib/src/app/dashboard/widgets/clean_air_forum_filter_card.dart`:
- Around line 62-64: The selfie image in the card currently only handles the
null case, so a corrupt or unsupported gallery file can show Flutter’s default
broken-image UI inside the branded component. Update the Image.file usage in the
Clean Air Forum filter card to include an errorBuilder that falls back to
_SelfiePlaceholder, keeping the widget intentional even when the selected selfie
fails to load.

In `@src/mobile/lib/src/app/exposure/widgets/exposure_place_name_text_field.dart`:
- Around line 39-57: The focus listener setup in
exposure_place_name_text_field.dart only happens in initState, so if the
widget’s focusNode changes later the old FocusNode keeps the listener and the
new one never gets it. Update the ExposurePlaceNameTextField state to handle
FocusNode replacement in didUpdateWidget by comparing the previous and current
widget.focusNode, removing _handleFocusChange from the old node, attaching it to
the new node, and keeping the existing dispose cleanup aligned with the
_focusNode getter and _handleFocusChange listener.

In
`@src/mobile/lib/src/app/shared/services/clean_air_forum_submission_service.dart`:
- Around line 91-96: Validate the Cloudinary configuration in
_uploadToCloudinary before constructing cloudinaryUrl. If
NEXT_PUBLIC_CLOUDINARY_NAME or NEXT_PUBLIC_CLOUDINARY_PRESET are missing or
empty, fail fast with a clear configuration error instead of continuing to build
the request; keep the check near the dotenv lookups in
clean_air_forum_submission_service.dart so the malformed URL is never created.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 1d58da43-bc13-480c-8030-3dd6e4e61829

📥 Commits

Reviewing files that changed from the base of the PR and between 2aac7fa and cc96356.

⛔ Files ignored due to path filters (6)
  • src/mobile/assets/icons/camera.svg is excluded by !**/*.svg
  • src/mobile/assets/icons/check-circle.svg is excluded by !**/*.svg
  • src/mobile/assets/icons/copy.svg is excluded by !**/*.svg
  • src/mobile/assets/icons/gallery.svg is excluded by !**/*.svg
  • src/mobile/assets/images/shared/airqo_icon_mark.svg is excluded by !**/*.svg
  • src/mobile/pubspec.lock is excluded by !**/*.lock
📒 Files selected for processing (14)
  • src/mobile/android/app/build.gradle
  • src/mobile/android/app/src/main/AndroidManifest.xml
  • src/mobile/android/app/src/main/res/xml/provider_paths.xml
  • src/mobile/lib/src/app/dashboard/utils/air_quality_card_utils.dart
  • src/mobile/lib/src/app/dashboard/utils/clean_air_forum_branding.dart
  • src/mobile/lib/src/app/dashboard/widgets/air_quality_share_card.dart
  • src/mobile/lib/src/app/dashboard/widgets/air_quality_share_sheet.dart
  • src/mobile/lib/src/app/dashboard/widgets/clean_air_forum_camera_screen.dart
  • src/mobile/lib/src/app/dashboard/widgets/clean_air_forum_filter_card.dart
  • src/mobile/lib/src/app/dashboard/widgets/clean_air_forum_sticker_frame.dart
  • src/mobile/lib/src/app/exposure/widgets/exposure_place_name_text_field.dart
  • src/mobile/lib/src/app/shared/services/air_quality_share_service.dart
  • src/mobile/lib/src/app/shared/services/clean_air_forum_submission_service.dart
  • src/mobile/pubspec.yaml

Comment thread src/mobile/lib/src/app/dashboard/utils/air_quality_card_utils.dart Outdated
…, mojibake, cleanup)

- sanitizeCardText: latin1.encode() can't represent Windows-1252-specific
  code points (smart quotes, em-dashes), so it threw and silently left
  that mojibake (e.g. "it’s") unfixed. Rebuild the byte sequence with a
  proper Windows-1252 reverse map instead, and tighten detection to drop
  the bare 'â' trigger (false-positives on legitimate text like "château")
  in favor of the more specific 'â€' compound signature. Added regression
  tests.
- _pickSelfie / _openLiveCamera: guard the post-await setState calls with
  `mounted`, matching the pattern already used elsewhere in the file —
  picking/capturing a selfie after the sheet is dismissed could otherwise
  throw.
- _copySticker: read the clipboard back after Pasteboard.writeImage to
  confirm something was actually copied before showing "Copied!" — on iOS,
  a failed image decode leaves UIPasteboard.general.image nil but still
  resolves the write as successful.
- CleanAirForumCameraScreen._setUp: guard Permission.camera.request() with
  try/catch like the availableCameras() call beside it — build()'s
  FutureBuilder never checks snapshot.hasError, so an unguarded permission
  failure left the UI stuck on a permanent loading spinner.
- CleanAirForumCameraScreen._openCamera: dispose and clear the controller
  when initialize() fails instead of leaving _controller pointing at a
  non-initialized, undisposed instance.
- CleanAirForumSubmissionService: document that the bundled shared secret
  is a weak, interim deterrent only (extractable from the app binary), and
  that the real backend swap should use a proper per-request mechanism
  instead of carrying it forward.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
src/mobile/lib/src/app/dashboard/utils/air_quality_card_utils.dart (2)

88-104: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider consolidating the three category switches into one lookup table.

getMeasurementAqiColor, aqiCategoryLabel, and getMeasurementAqiIconAsset each re-implement the same category-to-value switch. A single map keyed by category (color + label + icon path) would avoid the case lists drifting apart as categories evolve.

Also applies to: 108-116, 131-147

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/mobile/lib/src/app/dashboard/utils/air_quality_card_utils.dart` around
lines 88 - 104, The AQI category logic is duplicated across
getMeasurementAqiColor, aqiCategoryLabel, and getMeasurementAqiIconAsset, which
risks the category mappings drifting apart. Refactor the category handling in
air_quality_card_utils.dart to use one shared lookup structure keyed by
normalized aqiCategory values, and have those three functions read color, label,
and icon from that single source. Make sure the existing category symbols in the
switch cases (such as good, moderate, unhealthy for sensitive groups/u4sg,
unhealthy, very unhealthy, and hazardous) are preserved in the shared mapping.

80-86: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

Optional: validate hex length before parsing aqiColor.

If the API ever returns something other than a plain 6-digit hex (e.g., 8-digit with embedded alpha, or malformed), int.parse succeeds but the resulting Color value can silently overflow ARGB range instead of falling back to the category switch.

🛡️ Optional defensive check
   if (measurement.aqiColor != null) {
     try {
       final colorString = measurement.aqiColor!.replaceAll('#', '');
-      return Color(int.parse('0xFF$colorString'));
+      if (RegExp(r'^[0-9A-Fa-f]{6}$').hasMatch(colorString)) {
+        return Color(int.parse('0xFF$colorString'));
+      }
     } catch (_) {}
   }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/mobile/lib/src/app/dashboard/utils/air_quality_card_utils.dart` around
lines 80 - 86, getMeasurementAqiColor currently parses measurement.aqiColor
without validating that it is a plain 6-digit hex, so malformed or 8-digit
values can produce an invalid Color instead of falling back. Update the aqiColor
parsing block in getMeasurementAqiColor to verify the cleaned string length and
format before calling int.parse, and only return the parsed Color when it
matches the expected 6-digit RGB hex; otherwise continue to the existing
category switch fallback.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@src/mobile/lib/src/app/dashboard/utils/air_quality_card_utils.dart`:
- Around line 88-104: The AQI category logic is duplicated across
getMeasurementAqiColor, aqiCategoryLabel, and getMeasurementAqiIconAsset, which
risks the category mappings drifting apart. Refactor the category handling in
air_quality_card_utils.dart to use one shared lookup structure keyed by
normalized aqiCategory values, and have those three functions read color, label,
and icon from that single source. Make sure the existing category symbols in the
switch cases (such as good, moderate, unhealthy for sensitive groups/u4sg,
unhealthy, very unhealthy, and hazardous) are preserved in the shared mapping.
- Around line 80-86: getMeasurementAqiColor currently parses
measurement.aqiColor without validating that it is a plain 6-digit hex, so
malformed or 8-digit values can produce an invalid Color instead of falling
back. Update the aqiColor parsing block in getMeasurementAqiColor to verify the
cleaned string length and format before calling int.parse, and only return the
parsed Color when it matches the expected 6-digit RGB hex; otherwise continue to
the existing category switch fallback.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 1435a7b7-1826-43e3-922a-7522245d2f56

📥 Commits

Reviewing files that changed from the base of the PR and between cc96356 and b1ceba1.

📒 Files selected for processing (5)
  • src/mobile/lib/src/app/dashboard/utils/air_quality_card_utils.dart
  • src/mobile/lib/src/app/dashboard/widgets/air_quality_share_sheet.dart
  • src/mobile/lib/src/app/dashboard/widgets/clean_air_forum_camera_screen.dart
  • src/mobile/lib/src/app/shared/services/clean_air_forum_submission_service.dart
  • src/mobile/test/app/dashboard/utils/air_quality_card_utils_test.dart
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/mobile/lib/src/app/shared/services/clean_air_forum_submission_service.dart
  • src/mobile/lib/src/app/dashboard/widgets/clean_air_forum_camera_screen.dart
  • src/mobile/lib/src/app/dashboard/widgets/air_quality_share_sheet.dart

2phonebabykeem added a commit that referenced this pull request Jul 7, 2026
Edition and date range confirmed correct with the events team, and
updated the PR reference now that #3753 is closed in favor of #3755
(mobile) and #3756 (website).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@Mozart299
Mozart299 requested a review from Baalmart July 7, 2026 12:36
@Baalmart
Baalmart merged commit afc0552 into staging Jul 7, 2026
22 of 24 checks passed
@Baalmart
Baalmart deleted the feature/clean-air-forum-selfie-filter-mobile branch July 7, 2026 13:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants