Skip to content

Add Clean Air Forum selfie filter, sticker, and wall display#3753

Closed
2phonebabykeem wants to merge 5 commits into
stagingfrom
feature/clean-air-forum-selfie-filter
Closed

Add Clean Air Forum selfie filter, sticker, and wall display#3753
2phonebabykeem wants to merge 5 commits into
stagingfrom
feature/clean-air-forum-selfie-filter

Conversation

@2phonebabykeem

@2phonebabykeem 2phonebabykeem commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Mobile: 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. Includes a live camera screen with the filter chrome overlaid for framing, gallery fallback, and consent-gated submission to a conference wall display.
  • Mobile: share sheet, buttons, switch, and text field now 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.
  • Mobile: Card and Forum filter shares now include the AirQo app link in the share text for conversion tracking.
  • Website: adds a /selfies wall display page for the conference venue screen (auto-polling grid, double-tap + staff PIN to moderate) backed by a temporary in-memory API — stood up so the feature is demoable end-to-end before the real AirQo backend has equivalent endpoints (noted inline as TEMPORARY, with the request/response shapes designed to carry over unchanged).

Test plan

  • flutter analyze clean on all touched mobile 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
  • Verify /selfies wall page against a live submission end-to-end (mobile → Cloudinary → website API → wall page)
  • Confirm real event details (edition/date range in CleanAirForumBrand) before the forum

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added Clean Air Forum selfie sharing across mobile and web, including branded filter cards, sticker exports, and a live camera capture flow.
    • Introduced a Clean Air Forum selfies wall with auto-refresh and moderation/removal using a staff PIN.
    • Added new sharing/submission endpoints with validation and per-IP rate limiting.
  • Bug Fixes
    • Improved air-quality card text sanitization, AQI color/label handling, and icon selection for clearer dashboard display.
  • Chores
    • Documented required Clean Air Forum configuration and secrets for local and production setup.

2phonebabykeem and others added 2 commits July 7, 2026 08:43
…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>
Backs the mobile app's new selfie filter feature with a `/selfies` wall page
for the conference venue display (auto-polling grid, double-tap + staff PIN
to moderate) and a temporary in-memory submissions API, so the feature is
demoable end-to-end before the real AirQo backend has equivalent endpoints.

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

coderabbitai Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 5265a1a8-1d7d-4147-b92a-3413fb69b79f

📥 Commits

Reviewing files that changed from the base of the PR and between ba0d86a and 1752bea.

📒 Files selected for processing (9)
  • docs/clean-air-forum-followups.md
  • 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/website/.env.sample
  • src/website/src/app/api/clean-air-forum/selfies/[id]/route.ts
  • src/website/src/app/api/clean-air-forum/selfies/route.ts
  • src/website/src/configs/cleanAirForumConfig.ts
  • src/website/src/services/cleanAirForumAuth.ts
  • src/website/src/views/selfies/SelfiesWallPage.tsx
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/website/src/app/api/clean-air-forum/selfies/[id]/route.ts
  • 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

📝 Walkthrough

Walkthrough

Adds Clean Air Forum selfie sharing on mobile and a website wall for viewing, polling, and PIN-protected hiding of submissions, plus shared config, auth, and submission plumbing across both apps.

Changes

Mobile Clean Air Forum sharing

Layer / File(s) Summary
Android camera and file provider setup
src/mobile/android/app/build.gradle, .../AndroidManifest.xml, .../res/xml/provider_paths.xml, pubspec.yaml
Pins NDK/minSdk versions, adds camera permissions/features, registers a FileProvider with path config, and adds camera/pasteboard dependencies.
Shared AQI card utilities
dashboard/utils/air_quality_card_utils.dart
Adds text sanitization, AQI color resolution, category label shortening, pill background blending, and AQI icon asset selection helpers.
Clean Air Forum branding widgets
dashboard/utils/clean_air_forum_branding.dart
Adds brand palette constants, an AirQo icon mark widget, and a scalable branded header widget.
Share card refactor
dashboard/widgets/air_quality_share_card.dart
Switches to shared utility helpers for text/color/labels, fixes card text to white, and removes duplicated local helpers.
Filter card and sticker frame
clean_air_forum_filter_card.dart, clean_air_forum_sticker_frame.dart
Adds selfie-based filter card with placeholder/bottom panel and a transparent sticker frame widget for social sharing.
Live camera capture screen
clean_air_forum_camera_screen.dart
Adds a camera screen with permission handling, lifecycle-aware controller management, capture, camera flip, filter overlay, and error states.
Share and submission services
air_quality_share_service.dart, clean_air_forum_submission_service.dart
Adds filter/sticker share entry points with app link messaging and a Cloudinary-upload-backed wall submission service.
Tabbed share sheet
air_quality_share_sheet.dart
Reworks the sheet into card/filter/sticker tabs with selfie capture/picking, inline messaging, consent submission, and clipboard copy.
Exposure text field focus node
exposure_place_name_text_field.dart
Adds optional external FocusNode support with safe internal-node disposal.

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

Website selfies wall

Layer / File(s) Summary
Config and in-memory submission store
configs/cleanAirForumConfig.ts, services/cleanAirForumSelfiesStore.ts
Adds event ID and wall timing config plus a globalThis-backed in-memory store with list, create, and hide operations.
Shared secret and environment docs
services/cleanAirForumAuth.ts, src/website/.env.sample, docs/clean-air-forum-followups.md
Adds production-aware shared-secret checking and documents the wall/submission environment variables and follow-up items.
Selfies API routes
app/api/clean-air-forum/selfies/route.ts, .../[id]/route.ts
Adds GET/POST for listing and creating submissions, and PATCH for PIN-checked soft-hiding.
Selfies wall client page
app/selfies/page.tsx, views/selfies/SelfiesWallPage.tsx
Adds a noindex page and a polling wall UI with double-tap and long-press removal flow.

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

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant ShareSheet as AirQualityShareSheet
  participant CameraScreen as CleanAirForumCameraScreen
  participant ShareService as AirQualityShareService
  participant SubmissionService as CleanAirForumSubmissionService
  participant WallAPI as Selfies API

  User->>ShareSheet: Open filter tab, tap "Take selfie"
  ShareSheet->>CameraScreen: Push CleanAirForumCameraScreen
  CameraScreen-->>ShareSheet: Return captured File
  User->>ShareSheet: Tap "Share"
  ShareSheet->>ShareSheet: _captureBoundary (PNG bytes)
  ShareSheet->>ShareService: shareCleanAirForumFilter(bytes, measurement)
  ShareService-->>User: Native share sheet
  ShareSheet->>SubmissionService: submitSelfie(bytes, measurement) [if consent]
  SubmissionService->>SubmissionService: uploadToCloudinary(bytes)
  SubmissionService->>WallAPI: POST /api/clean-air-forum/selfies
  WallAPI-->>SubmissionService: 201 submission
  SubmissionService-->>ShareSheet: success/failure
Loading
sequenceDiagram
  participant Visitor
  participant WallPage as SelfiesWallPage
  participant WallAPI as Selfies API
  participant Store as cleanAirForumSelfiesStore

  Visitor->>WallPage: Load /selfies
  loop Poll interval
    WallPage->>WallAPI: GET /api/clean-air-forum/selfies
    WallAPI->>Store: listSubmissions(eventId)
    Store-->>WallAPI: active submissions
    WallAPI-->>WallPage: JSON submissions
  end
  Visitor->>WallPage: Double-tap or long-press photo
  WallPage->>WallPage: Show PIN modal
  Visitor->>WallPage: Confirm removal with PIN
  WallPage->>WallAPI: PATCH /api/clean-air-forum/selfies/{id} {pin}
  WallAPI->>Store: hideSubmission(id)
  Store-->>WallAPI: updated submission or null
  WallAPI-->>WallPage: 200/401/404
  WallPage-->>Visitor: Remove tile or show PIN error
Loading

Possibly related PRs

Suggested labels: ready for review, airqo-website

Suggested reviewers: Mozart299, Baalmart

Poem

A camera clicks, a filter glows,
The forum's colors AirQo shows.
Sticker copied, wall now stacked,
PIN in hand, a tile retracked. 📸
From cache to cloud, the selfies fly —
Clean air shared beneath the sky.

🚥 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 accurately summarizes the main additions: Clean Air Forum selfie filter, sticker, and wall display.
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 docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/clean-air-forum-selfie-filter

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
2phonebabykeem requested a review from Mozart299 July 7, 2026 05:48
@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

New azure website changes available for preview here

@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: 5

🧹 Nitpick comments (3)
src/mobile/lib/src/app/exposure/widgets/exposure_place_name_text_field.dart (1)

39-57: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Missing didUpdateWidget to handle a changed focusNode.

Disposal logic correctly skips external nodes, but there's no didUpdateWidget override to re-subscribe _handleFocusChange if widget.focusNode changes identity across rebuilds (e.g., swapped to a different external node, or toggled between external/internal). The listener attached in initState stays on the old node, so focus changes on the new node silently stop updating _focused/border styling.

No current caller passes focusNode (per edit_place_sheet.dart, label_picker_sheet.dart, learn_quiz_free_text.dart), so this is latent today, but worth closing before other call sites adopt the new parameter.

♻️ Suggested fix
   `@override`
   void initState() {
     super.initState();
     _focusNode.addListener(_handleFocusChange);
   }
 
+  `@override`
+  void didUpdateWidget(covariant ExposurePlaceNameTextField oldWidget) {
+    super.didUpdateWidget(oldWidget);
+    if (oldWidget.focusNode != widget.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 `ExposurePlaceNameTextField`
only subscribes once in `initState`, so `_handleFocusChange` will keep watching
the old node if `widget.focusNode` changes or switches between external and
internal nodes. Add a `didUpdateWidget` override to detect `focusNode` identity
changes, remove the listener from the previous node, attach it to the new one,
and keep `_focused` in sync. Make sure the change is localized around
`_focusNode`, `initState`, `_handleFocusChange`, and `dispose` so the
subscription lifecycle stays correct.
src/website/src/services/cleanAirForumSelfiesStore.ts (1)

60-68: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

Bare crypto.randomUUID() relies on the runtime polyfill.

globalThis.crypto isn't native in Node.js 18 (only Node 19+); Next.js polyfills it for the server runtime, so this should work in practice, but it's worth confirming the deployment's Node/Next combination guarantees the polyfill rather than depending on it implicitly.

🛡️ Optional defensive import
+import { randomUUID } from 'node:crypto';
+
 export function createSubmission(
   input: CreateSubmissionInput,
 ): CleanAirForumSelfieSubmission {
   const submission: CleanAirForumSelfieSubmission = {
     ...input,
-    id: crypto.randomUUID(),
+    id: randomUUID(),
🤖 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/website/src/services/cleanAirForumSelfiesStore.ts` around lines 60 - 68,
The createSubmission helper currently uses bare crypto.randomUUID(), which
depends on an implicit runtime polyfill. Update the
CleanAirForumSelfieSubmission ID generation in createSubmission to use an
explicit, guaranteed source of randomness or a defensive import/polynomial-safe
approach, and ensure the code path is clearly tied to the runtime support
assumptions so it does not rely on globalThis.crypto being present implicitly.
src/mobile/lib/src/app/dashboard/widgets/clean_air_forum_filter_card.dart (1)

62-64: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Decode the selfie at display size instead of full resolution.

Image.file(selfieFile!, fit: BoxFit.cover) decodes the full camera/gallery resolution just to render inside a bounded, RepaintBoundary-captured card. Consider cacheWidth/cacheHeight scaled to the rendered card size to cut memory pressure, especially since this widget gets rebuilt for both live preview and share export.

🖼️ Proposed fix
                 selfieFile != null
-                    ? Image.file(selfieFile!, fit: BoxFit.cover)
+                    ? Image.file(
+                        selfieFile!,
+                        fit: BoxFit.cover,
+                        cacheWidth: kCafReferenceWidth.round(),
+                      )
                     : _SelfiePlaceholder(scale: scale),
🤖 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 clean_air_forum_filter_card widget
is being decoded at full source resolution via Image.file, which is unnecessary
for the bounded card and share capture use case. Update the image rendering in
the selfie branch to pass cacheWidth and/or cacheHeight based on the rendered
card size (using the existing scale/layout context in this widget) so the
decoded bitmap matches display size. Keep the change localized to the selfie
image path in the dashboard widget and preserve the current fit/placeholder
behavior.
🤖 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/widgets/clean_air_forum_camera_screen.dart`:
- Around line 79-96: Clear the camera controller when the app becomes inactive
so rebuilds do not try to render CameraPreview from a disposed instance. Update
the lifecycle handling in CleanAirForumCameraScreen so the
AppLifecycleState.inactive path nulls _controller (or uses an explicit active
flag) before dispose runs, and ensure _openCamera still assigns and initializes
a fresh CameraController before resuming preview.

In
`@src/mobile/lib/src/app/shared/services/clean_air_forum_submission_service.dart`:
- Around line 83-120: The Cloudinary upload in _uploadToCloudinary uses an
exposed unsigned preset, which can be reused from the mobile binary; update the
flow so the app does not depend on a freely reusable preset. Either switch
CleanAirForumSubmissionService to call a backend endpoint that generates signed
Cloudinary uploads, or keep the preset only if it is locked down server-side to
the strictest allowed folder, file type, and source restrictions.

In `@src/website/.env.sample`:
- Around line 8-18: The new website config variables are present in the sample
env file but are not passed through deployment, so update the relevant workflow
env blocks to include CLEAN_AIR_FORUM_WALL_PIN and
NEXT_PUBLIC_CLEAN_AIR_FORUM_EVENT_ID. Locate the website GitHub workflow files
under .github/workflows and add these variables alongside the existing website
env entries so the deploy jobs and runtime config stay in sync with the
.env.sample surface.

In `@src/website/src/app/api/clean-air-forum/selfies/`[id]/route.ts:
- Around line 19-27: The PIN validation in the route handler currently skips
auth when CLEAN_AIR_FORUM_WALL_PIN is unset, which can leave the PATCH path
effectively open. Update the logic in the selfie route handler to fail closed in
production by rejecting requests when the env var is missing (or at minimum
emitting a clear warning), while keeping the existing PIN comparison behavior in
the request-processing code that uses request.json() and NextResponse.json().

In `@src/website/src/app/api/clean-air-forum/selfies/route.ts`:
- Around line 35-79: The POST handler in selfied route currently accepts any
non-empty imageUrl, which leaves the public write endpoint open to abuse. Update
POST to validate imageUrl as a well-formed https URL and, if possible, restrict
it to the expected upload host before calling createSubmission; also add basic
per-IP rate limiting/throttling in the same route or shared middleware to reduce
spam. Use the existing POST, request.json, and createSubmission flow as the main
place to enforce these checks.

---

Nitpick comments:
In `@src/mobile/lib/src/app/dashboard/widgets/clean_air_forum_filter_card.dart`:
- Around line 62-64: The selfie image in the clean_air_forum_filter_card widget
is being decoded at full source resolution via Image.file, which is unnecessary
for the bounded card and share capture use case. Update the image rendering in
the selfie branch to pass cacheWidth and/or cacheHeight based on the rendered
card size (using the existing scale/layout context in this widget) so the
decoded bitmap matches display size. Keep the change localized to the selfie
image path in the dashboard widget and preserve the current fit/placeholder
behavior.

In `@src/mobile/lib/src/app/exposure/widgets/exposure_place_name_text_field.dart`:
- Around line 39-57: The focus listener setup in `ExposurePlaceNameTextField`
only subscribes once in `initState`, so `_handleFocusChange` will keep watching
the old node if `widget.focusNode` changes or switches between external and
internal nodes. Add a `didUpdateWidget` override to detect `focusNode` identity
changes, remove the listener from the previous node, attach it to the new one,
and keep `_focused` in sync. Make sure the change is localized around
`_focusNode`, `initState`, `_handleFocusChange`, and `dispose` so the
subscription lifecycle stays correct.

In `@src/website/src/services/cleanAirForumSelfiesStore.ts`:
- Around line 60-68: The createSubmission helper currently uses bare
crypto.randomUUID(), which depends on an implicit runtime polyfill. Update the
CleanAirForumSelfieSubmission ID generation in createSubmission to use an
explicit, guaranteed source of randomness or a defensive import/polynomial-safe
approach, and ensure the code path is clearly tied to the runtime support
assumptions so it does not rely on globalThis.crypto being present implicitly.
🪄 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: 89b8b113-e776-466b-8cbe-ef15fcebd06e

📥 Commits

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

⛔ 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 (21)
  • 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
  • src/website/.env.sample
  • src/website/src/app/api/clean-air-forum/selfies/[id]/route.ts
  • src/website/src/app/api/clean-air-forum/selfies/route.ts
  • src/website/src/app/selfies/page.tsx
  • src/website/src/configs/cleanAirForumConfig.ts
  • src/website/src/services/cleanAirForumSelfiesStore.ts
  • src/website/src/views/selfies/SelfiesWallPage.tsx

Comment thread src/website/.env.sample
Comment on lines +8 to +18
# Shared PIN required to remove a photo from the /selfies conference wall.
# Leave unset locally to skip the PIN check during development.
CLEAN_AIR_FORUM_WALL_PIN=

# Client-side variables (these need NEXT_PUBLIC_ prefix)
# Comma-separated site URLs. The first entry is used as the canonical base.
NEXT_PUBLIC_SITE_URL=
NEXT_PUBLIC_GA_MEASUREMENT_ID=
NEXT_PUBLIC_MAPBOX_ACCESS_TOKEN= No newline at end of file
NEXT_PUBLIC_MAPBOX_ACCESS_TOKEN=
# Which Clean Air Forum edition's submissions the /selfies wall shows.
NEXT_PUBLIC_CLEAN_AIR_FORUM_EVENT_ID=clean-air-forum No newline at end of file

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.

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check whether CI workflows reference the newly added env vars.
rg -n 'CLEAN_AIR_FORUM_WALL_PIN|NEXT_PUBLIC_CLEAN_AIR_FORUM_EVENT_ID' .github/workflows

Repository: airqo-platform/AirQo-frontend

Length of output: 167


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# List workflow files and inspect them for any references or env wiring.
git ls-files '.github/workflows/*' || true
printf '\n---\n'
rg -n 'CLEAN_AIR_FORUM_WALL_PIN|NEXT_PUBLIC_CLEAN_AIR_FORUM_EVENT_ID|CLEAN_AIR_FORUM|NEXT_PUBLIC_' .github/workflows || true

Repository: airqo-platform/AirQo-frontend

Length of output: 8126


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find every usage of the new env vars in the repository.
rg -n 'CLEAN_AIR_FORUM_WALL_PIN|NEXT_PUBLIC_CLEAN_AIR_FORUM_EVENT_ID' .

Repository: airqo-platform/AirQo-frontend

Length of output: 669


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the website workflow env wiring around existing NEXT_PUBLIC_* variables.
for f in \
  .github/workflows/deploy-frontend-to-azurestaging.yml \
  .github/workflows/deploy-frontend-to-azureprod.yml \
  .github/workflows/deploy-frontend-pr-previews.yml \
  .github/workflows/deploy-frontend-azurepreview.yml \
  .github/workflows/deploy-frontends-to-staging.yml \
  .github/workflows/deploy-frontends-to-production.yml
do
  echo "===== $f ====="
  rg -n -C 3 'NEXT_PUBLIC_|WEBSITE_.*(SECRET|API_URL|OPENCAGE_API_KEY|API_TOKEN|SLACK_WEBHOOK_URL|SLACK_CHANNEL|GOOGLE_SITE_VERIFICATION|HCAPTCHA_SITE_KEY)' "$f" || true
  echo
done

Repository: airqo-platform/AirQo-frontend

Length of output: 14495


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '1,120p' src/website/src/configs/cleanAirForumConfig.ts
printf '\n---\n'
sed -n '1,120p' src/website/src/app/api/clean-air-forum/selfies/[id]/route.ts

Repository: airqo-platform/AirQo-frontend

Length of output: 2126


Add the new env vars to the website deployment workflows

CLEAN_AIR_FORUM_WALL_PIN and NEXT_PUBLIC_CLEAN_AIR_FORUM_EVENT_ID are present in src/website/.env.sample, but they still aren’t wired through the website CI/deploy workflows. Add them to the relevant .github/workflows/*.yml env blocks so the deployed site matches the new config surface.

🧰 Tools
🪛 dotenv-linter (4.0.0)

[warning] 10-10: [UnorderedKey] The CLEAN_AIR_FORUM_WALL_PIN key should go before the GOOGLE_SITE_VERIFICATION key

(UnorderedKey)


[warning] 15-15: [UnorderedKey] The NEXT_PUBLIC_GA_MEASUREMENT_ID key should go before the NEXT_PUBLIC_SITE_URL key

(UnorderedKey)


[warning] 16-16: [UnorderedKey] The NEXT_PUBLIC_MAPBOX_ACCESS_TOKEN key should go before the NEXT_PUBLIC_SITE_URL key

(UnorderedKey)


[warning] 18-18: [EndingBlankLine] No blank line at the end of the file

(EndingBlankLine)


[warning] 18-18: [UnorderedKey] The NEXT_PUBLIC_CLEAN_AIR_FORUM_EVENT_ID key should go before the NEXT_PUBLIC_GA_MEASUREMENT_ID key

(UnorderedKey)

🤖 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/website/.env.sample` around lines 8 - 18, The new website config
variables are present in the sample env file but are not passed through
deployment, so update the relevant workflow env blocks to include
CLEAN_AIR_FORUM_WALL_PIN and NEXT_PUBLIC_CLEAN_AIR_FORUM_EVENT_ID. Locate the
website GitHub workflow files under .github/workflows and add these variables
alongside the existing website env entries so the deploy jobs and runtime config
stay in sync with the .env.sample surface.

Source: Coding guidelines

Comment thread src/website/src/app/api/clean-air-forum/selfies/[id]/route.ts
Comment thread src/website/src/app/api/clean-air-forum/selfies/route.ts
@Mozart299

Copy link
Copy Markdown
Contributor

@2phonebabykeem handle coderabbit comments

Copilot AI 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.

Pull request overview

This PR adds an end-to-end “Clean Air Forum” selfie sharing flow across the Flutter mobile app and the Next.js website, including a conference wall display fed by a temporary in-memory API.

Changes:

  • Mobile: adds a Clean Air Forum branded selfie filter (live camera preview + composited share image) and a transparent sticker that can be copied to the clipboard.
  • Mobile: updates share text to include an AirQo app link and refactors card rendering helpers for consistent color/label/text handling.
  • Website: adds a /selfies wall display page with polling + PIN-gated moderation, backed by temporary in-memory API routes and config/env wiring.

Reviewed changes

Copilot reviewed 21 out of 27 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
src/website/src/views/selfies/SelfiesWallPage.tsx Client wall UI: polling grid, double-tap moderation modal, and removal flow.
src/website/src/services/cleanAirForumSelfiesStore.ts Temporary in-memory submissions store (list/create/hide) to demo wall end-to-end.
src/website/src/configs/cleanAirForumConfig.ts Shared config for “current event” ID and wall polling interval.
src/website/src/app/selfies/page.tsx Next.js route wrapper for /selfies with noindex metadata.
src/website/src/app/api/clean-air-forum/selfies/route.ts Temporary API for listing/creating wall submissions.
src/website/src/app/api/clean-air-forum/selfies/[id]/route.ts Temporary API for PIN-gated hiding (soft-removing) a submission.
src/website/.env.sample Adds sample env vars for wall PIN and event ID.
src/mobile/pubspec.yaml Adds camera + pasteboard dependencies required for live capture and clipboard sticker copy.
src/mobile/pubspec.lock Updates dependency lockfile reflecting new packages and resolved SDK minimums.
src/mobile/lib/src/app/shared/services/clean_air_forum_submission_service.dart Uploads composited image to Cloudinary and submits metadata to the website’s temporary API.
src/mobile/lib/src/app/shared/services/air_quality_share_service.dart Adds tracked app link to share text; new share helpers for filter/sticker images.
src/mobile/lib/src/app/exposure/widgets/exposure_place_name_text_field.dart Allows injecting a FocusNode from callers (for keyboard/focus management).
src/mobile/lib/src/app/dashboard/widgets/clean_air_forum_sticker_frame.dart New transparent, square sticker widget that renders AQI/location info.
src/mobile/lib/src/app/dashboard/widgets/clean_air_forum_filter_card.dart New selfie + branded overlay card widget intended for capture/share.
src/mobile/lib/src/app/dashboard/widgets/clean_air_forum_camera_screen.dart New live camera preview screen with overlaid guide chrome for framing.
src/mobile/lib/src/app/dashboard/widgets/air_quality_share_sheet.dart Expands share sheet into tabs (Forum filter/Card/Sticker), adds camera/gallery selection, consent-gated wall submission, and sticker clipboard copy.
src/mobile/lib/src/app/dashboard/widgets/air_quality_share_card.dart Refactors card rendering to reuse shared sanitization and AQI helpers; tweaks styling for consistency.
src/mobile/lib/src/app/dashboard/utils/clean_air_forum_branding.dart Adds CAF brand constants and shared header/icon widgets for templates.
src/mobile/lib/src/app/dashboard/utils/air_quality_card_utils.dart New shared helpers for sanitization, AQI color/label/icon, and pill backgrounds.
src/mobile/assets/images/shared/airqo_icon_mark.svg Adds AirQo mark asset used in CAF templates.
src/mobile/assets/icons/gallery.svg Adds gallery icon used in selfie source sheet.
src/mobile/assets/icons/copy.svg Adds copy icon used for sticker clipboard CTA.
src/mobile/assets/icons/check-circle.svg Adds check icon used for “Copied!” state.
src/mobile/assets/icons/camera.svg Adds camera icon used across CAF capture/share UI.
src/mobile/android/app/src/main/res/xml/provider_paths.xml Adds FileProvider paths needed by pasteboard clipboard image writes.
src/mobile/android/app/src/main/AndroidManifest.xml Adds CAMERA permission/features and a FileProvider for pasteboard.
src/mobile/android/app/build.gradle Updates Android build settings (pins NDK version and raises minSdkVersion).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +42 to +48
FocusNode get _focusNode => widget.focusNode ?? (_internalFocusNode ??= FocusNode());

@override
void initState() {
super.initState();
_focusNode = FocusNode();
_focusNode.addListener(() {
setState(() => _focused = _focusNode.hasFocus);
});
_focusNode.addListener(_handleFocusChange);
}
Comment on lines +121 to +131
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
final controller = _controller;
if (controller == null || !controller.value.isInitialized) return;

if (state == AppLifecycleState.inactive) {
controller.dispose();
} else if (state == AppLifecycleState.resumed) {
_openCamera(_cameraIndex);
}
}
Comment on lines +84 to +88
final cloudName = dotenv.env['NEXT_PUBLIC_CLOUDINARY_NAME'] ?? '';
final uploadPreset = dotenv.env['NEXT_PUBLIC_CLOUDINARY_PRESET'] ?? '';
final cloudinaryUrl =
'https://api.cloudinary.com/v1_1/$cloudName/image/upload';

Comment on lines +99 to +101
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ pin }),
},
Comment on lines +20 to +27
const body = await request
.json()
.catch(() => ({}) as Record<string, unknown>);
const expectedPin = process.env.CLEAN_AIR_FORUM_WALL_PIN;

if (expectedPin && body?.pin !== expectedPin) {
return NextResponse.json({ error: 'Incorrect PIN' }, { status: 401 });
}
// You can update the following values to match your application needs.
// For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration.
minSdkVersion flutter.minSdkVersion
minSdkVersion 24
namespace "com.airqo.app"
compileSdk 36
ndkVersion flutter.ndkVersion
ndkVersion "27.0.12077973"
2phonebabykeem and others added 3 commits July 7, 2026 11:53
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>
The wall page used generic black/white styling with no AirQo branding, all
active submissions in one long scrolling grid, and a POST endpoint that
accepted any imageUrl with no auth — all flagged by the user and CodeRabbit
as needing fixes before this goes live at the event.

- Wall page now uses the site's real logo and brand blue (#145DFF) instead
  of generic dark-UI colors, and shows 8 selfies at a time in an
  auto-advancing slideshow (framer-motion crossfade + page dots) rather
  than everything at once
- Long-press added alongside double-tap for moderation, since a wall
  display has no cursor to hover/right-click with
- POST now requires a shared secret header from the mobile app
  (x-clean-air-forum-secret) and validates imageUrl actually points at the
  Cloudinary folder the app uploads to, closing the "anyone who finds this
  endpoint can push arbitrary images onto the public wall" gap
- Both the submission secret and moderation PIN checks now fail closed
  (with a logged warning) if unset in production, instead of silently
  leaving the endpoint open — they still skip the check in dev/preview
- Added a best-effort per-IP rate limit (5/minute) on submissions to blunt
  spam before staff can react

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Documents what this feature still needs from other teams before it's
production-grade: real backend endpoints to replace the temporary
in-memory store, Cloudinary upload-preset hardening, the new env
vars/secrets that need wiring into the deploy workflows (left as docs
rather than editing the workflow YAML directly), and a note to confirm
the hardcoded event dates before launch.

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

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

New azure website changes available for preview here

@Baalmart
Baalmart requested a review from OchiengPaul442 July 7, 2026 09:13

@Baalmart Baalmart left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Hi @2phonebabykeem and @Mozart299 I am seeing some new changes to the official WEBSITE project, are these accidental or intentional? cc: @OchiengPaul442

@OchiengPaul442 OchiengPaul442 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.

@2phonebabykeem Please keep the mobile app and website changes in separate PRs to maintain clear separation of concerns and avoid mixing unrelated changes.

@2phonebabykeem

Copy link
Copy Markdown
Contributor Author

@OchiengPaul442 @Baalmart Understood, will fix this

@2phonebabykeem

Copy link
Copy Markdown
Contributor Author

Splitting this into two separate PRs to keep mobile and website changes cleanly separated, per team convention:

Both include everything from this PR plus the CodeRabbit/Copilot review fixes. Closing this one in favor of those two.

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>
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.

5 participants