From 7e8167794c46c71ae61a704bc41c3d967adc5b5d Mon Sep 17 00:00:00 2001 From: Brady Date: Wed, 5 Aug 2026 22:25:36 -0500 Subject: [PATCH] Title --- backend/package.json | 1 + backend/prisma/reseed-screenshots.sh | 98 ++++ backend/src/config/legal.ts | 2 +- docs/ACCESSIBILITY_AUDIT_2026-08-05.md | 115 ++++ docs/ACCESSIBILITY_DEVICE_PASS_2026-08-05.md | 97 ++++ ...ACCESSIBILITY_SECOND_OPINION_2026-08-05.md | 150 ++++++ docs/CLUB_OUTREACH_2026.md | 143 +++++ docs/LAUNCH_PLAN_FALL2026.md | 489 +++++++++--------- frontend/App.tsx | 46 +- frontend/app.json | 5 +- frontend/package.json | 1 + frontend/parse-check.cjs | 16 + frontend/scripts/audit-accessibility.mjs | 166 ++++++ frontend/src/components/ErrorBoundary.tsx | 2 +- frontend/src/components/OnboardingChrome.tsx | 34 +- frontend/src/components/ToastHost.tsx | 8 +- frontend/src/components/clubs/index.tsx | 120 +++-- frontend/src/components/messages.tsx | 66 +-- frontend/src/components/pulse.tsx | 70 ++- .../src/components/ui.accessibility.test.tsx | 80 +++ frontend/src/components/ui.tsx | 400 +++++++++++--- frontend/src/constants/legal.ts | 2 +- frontend/src/dev/previewMode.test.ts | 50 ++ frontend/src/dev/previewMode.ts | 22 +- frontend/src/screens/ActivityPodsScreen.tsx | 107 +++- frontend/src/screens/AuthScreen.tsx | 5 +- frontend/src/screens/BlockedUsersScreen.tsx | 39 +- frontend/src/screens/EditProfileScreen.tsx | 16 +- frontend/src/screens/ExploreScreen.tsx | 177 +++++-- frontend/src/screens/HomeScreen.tsx | 194 +++++-- frontend/src/screens/InboxScreen.tsx | 284 +++++++--- frontend/src/screens/PodChatScreen.tsx | 6 +- frontend/src/screens/PodDetailScreen.tsx | 58 ++- frontend/src/screens/PodsScreen.tsx | 141 +++-- frontend/src/screens/PrivacyDataScreen.tsx | 62 ++- .../src/screens/TermsAcceptanceScreen.tsx | 37 +- frontend/src/screens/ThreadScreen.tsx | 73 ++- frontend/src/screens/UserProfileScreen.tsx | 169 ++++-- frontend/src/screens/UserSearchScreen.tsx | 135 ++++- frontend/src/screens/VerifyEmailScreen.tsx | 2 +- .../screens/clubs/ClubApplicationsScreen.tsx | 4 + frontend/src/screens/clubs/ClubChatScreen.tsx | 86 +-- .../src/screens/clubs/ClubManageScreen.tsx | 6 + .../src/screens/clubs/ClubsHomeScreen.tsx | 243 ++++++--- .../src/screens/clubs/CreateClubScreen.tsx | 147 ++++-- frontend/src/theme.accessibility.test.ts | 60 +++ frontend/src/theme.ts | 18 +- landing/package.json | 1 + landing/scripts/audit-accessibility.mjs | 75 +++ landing/src/App.jsx | 66 ++- .../src/components/AccessibilityStatement.jsx | 79 +++ landing/src/components/CampusLife.jsx | 2 +- landing/src/components/ClubsPage.jsx | 90 ++-- .../src/components/CommunityGuidelines.jsx | 10 +- landing/src/components/DeleteAccount.jsx | 11 +- landing/src/components/FeatureBento.jsx | 6 +- landing/src/components/Footer.jsx | 25 +- landing/src/components/Hero.jsx | 3 +- landing/src/components/HowItWorks.jsx | 4 +- landing/src/components/Nav.jsx | 8 +- landing/src/components/PodInvitePage.jsx | 9 +- landing/src/components/PrivacyPolicy.jsx | 135 ++++- landing/src/components/Support.jsx | 2 +- landing/src/components/TermsOfUse.jsx | 440 ++++++++++++---- landing/src/components/Testimonials.jsx | 8 +- landing/src/components/UserProfilePage.jsx | 5 +- landing/src/components/WaitlistForm.jsx | 12 +- landing/src/index.css | 54 +- package.json | 3 +- 69 files changed, 4106 insertions(+), 1194 deletions(-) create mode 100755 backend/prisma/reseed-screenshots.sh create mode 100644 docs/ACCESSIBILITY_AUDIT_2026-08-05.md create mode 100644 docs/ACCESSIBILITY_DEVICE_PASS_2026-08-05.md create mode 100644 docs/ACCESSIBILITY_SECOND_OPINION_2026-08-05.md create mode 100644 docs/CLUB_OUTREACH_2026.md create mode 100644 frontend/parse-check.cjs create mode 100644 frontend/scripts/audit-accessibility.mjs create mode 100644 frontend/src/components/ui.accessibility.test.tsx create mode 100644 frontend/src/dev/previewMode.test.ts create mode 100644 frontend/src/theme.accessibility.test.ts create mode 100644 landing/scripts/audit-accessibility.mjs create mode 100644 landing/src/components/AccessibilityStatement.jsx diff --git a/backend/package.json b/backend/package.json index a53a6cd..761d5f2 100644 --- a/backend/package.json +++ b/backend/package.json @@ -17,6 +17,7 @@ "seed:launch": "ts-node prisma/seed-launch.ts", "seed:screenshots": "ts-node prisma/seed-screenshots.ts", "cleanup:screenshots": "ts-node prisma/cleanup-screenshots.ts", + "reseed:screenshots": "./prisma/reseed-screenshots.sh", "test": "vitest run", "test:watch": "vitest", "postinstall": "prisma generate" diff --git a/backend/prisma/reseed-screenshots.sh b/backend/prisma/reseed-screenshots.sh new file mode 100755 index 0000000..2ebdd0d --- /dev/null +++ b/backend/prisma/reseed-screenshots.sh @@ -0,0 +1,98 @@ +#!/usr/bin/env bash +# +# Reseed screenshot demo data: clean up the previous seed, then seed fresh. +# +# Wraps cleanup-screenshots.ts + seed-screenshots.ts so the two always run in +# the right order against the same database. Loads backend/.env, shows you the +# target host, and makes you type "yes" before it touches anything. +# +# ./prisma/reseed-screenshots.sh # clean up (if needed), then seed +# ./prisma/reseed-screenshots.sh --cleanup-only # just remove the current seed +# ./prisma/reseed-screenshots.sh --seed-only # skip cleanup, seed on top +# +# SEED_ATTACH_EMAIL defaults to the manifest's previous value, else to +# $OVAL_SEED_EMAIL, else nothing is attached. Override inline: +# SEED_ATTACH_EMAIL=you@osu.edu ./prisma/reseed-screenshots.sh + +set -euo pipefail + +BACKEND_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +cd "$BACKEND_DIR" + +MANIFEST="prisma/.screenshot-seed-manifest.json" + +DO_CLEANUP=1 +DO_SEED=1 +case "${1:-}" in + --cleanup-only) DO_SEED=0 ;; + --seed-only) DO_CLEANUP=0 ;; + "") ;; + *) echo "Unknown option: $1" >&2; exit 2 ;; +esac + +if [[ ! -f .env ]]; then + echo "No backend/.env found. Set DATABASE_URL and DIRECT_URL first." >&2 + exit 1 +fi + +# .env values are quoted, so this is safe for URLs containing ? and &. +set -a +# shellcheck disable=SC1091 +. ./.env +set +a + +if [[ -z "${DATABASE_URL:-}" ]]; then + echo "DATABASE_URL is not set after loading .env." >&2 + exit 1 +fi + +DB_HOST="$(node -e 'try{console.log(new URL(process.env.DATABASE_URL).host)}catch(e){console.log("(unparseable)")}')" + +# Default the attach email from the existing manifest, then $OVAL_SEED_EMAIL. +if [[ -z "${SEED_ATTACH_EMAIL:-}" && -f "$MANIFEST" ]]; then + SEED_ATTACH_EMAIL="$(node -e "try{console.log(require('./$MANIFEST').attachEmail||'')}catch(e){console.log('')}")" +fi +SEED_ATTACH_EMAIL="${SEED_ATTACH_EMAIL:-${OVAL_SEED_EMAIL:-}}" + +echo +echo " Target database : $DB_HOST" +echo " Cleanup first : $([[ $DO_CLEANUP == 1 ]] && echo yes || echo 'no (--seed-only)')" +echo " Seed after : $([[ $DO_SEED == 1 ]] && echo yes || echo 'no (--cleanup-only)')" +echo " Attach account : ${SEED_ATTACH_EMAIL:-(none)}" +if [[ -f "$MANIFEST" ]]; then + node -e " + const m = require('./$MANIFEST'); + console.log(' Current manifest: ' + m.createdAt + ' — ' + + [['users',m.users],['pods',m.pods],['clubs',m.clubs],['dmThreads',m.dmThreads],['friendships',m.friendships]] + .map(([k,v]) => (v||[]).length + ' ' + k).join(', ')); + " +else + echo " Current manifest: none (nothing to clean up)" +fi +echo +read -r -p "This writes to the database above. Type 'yes' to continue: " CONFIRM +if [[ "$CONFIRM" != "yes" ]]; then + echo "Aborted." + exit 1 +fi + +if [[ $DO_CLEANUP == 1 ]]; then + if [[ -f "$MANIFEST" ]]; then + echo + echo "==> Cleaning up previous seed" + CLEANUP_CONFIRM=yes npx ts-node prisma/cleanup-screenshots.ts + else + echo + echo "==> No manifest present; skipping cleanup" + fi +fi + +if [[ $DO_SEED == 1 ]]; then + echo + echo "==> Seeding screenshot data" + SEED_CONFIRM=yes SEED_ATTACH_EMAIL="${SEED_ATTACH_EMAIL:-}" npx ts-node prisma/seed-screenshots.ts +fi + +echo +echo "Done. To remove this seed later:" +echo " ./prisma/reseed-screenshots.sh --cleanup-only" diff --git a/backend/src/config/legal.ts b/backend/src/config/legal.ts index 959713c..b8f3e5e 100644 --- a/backend/src/config/legal.ts +++ b/backend/src/config/legal.ts @@ -1 +1 @@ -export const CURRENT_TERMS_VERSION = '2026-06-20'; +export const CURRENT_TERMS_VERSION = '2026-08-05'; diff --git a/docs/ACCESSIBILITY_AUDIT_2026-08-05.md b/docs/ACCESSIBILITY_AUDIT_2026-08-05.md new file mode 100644 index 0000000..6eca7ff --- /dev/null +++ b/docs/ACCESSIBILITY_AUDIT_2026-08-05.md @@ -0,0 +1,115 @@ +# Oval Accessibility Audit — August 5, 2026 + +## Status + +The code-level remediation and automated verification are complete for the Expo/React Native app and the public website. Oval now has regression checks for the most common accessibility failures and a public accessibility statement. + +This is not a legal certification or a guarantee that no user will encounter a barrier. Accessibility conformance also requires hands-on testing with assistive technologies on release hardware. The real-device checks at the end of this document are required before submitting the release. + +## Target + +- WCAG 2.2 Level AA, applied to the website and mobile software through relevant WCAG2ICT principles +- Apple accessibility behavior for VoiceOver, Dynamic Type, Reduce Motion, orientation, and control semantics +- Android accessibility behavior for TalkBack, font scaling, control semantics, and touch targets + +## Scope Audited + +- Shared mobile UI primitives: buttons, slabs/cards, fields, sheets, chips, segmented controls, list rows, banners, progress, loading, avatars, images, and date/time fields +- Mobile navigation and primary flows: authentication, verification, Home, Explore, Pods, pod detail/chat, Inbox/threads, Clubs, profiles, privacy/data controls, and onboarding +- Mobile contrast tokens, motion configuration, orientation support, touch targets, headings, live regions, images, maps, switches, and nested controls +- Public website routes: landing, clubs, privacy, terms, community guidelines, support, accessibility, account deletion, pod invites, and public profiles +- Website keyboard focus, skip navigation, SPA route focus, form labels, custom controls, motion, contrast, landmarks, headings, images, and 320 CSS-pixel reflow + +## Material Fixes + +- Added accessible names, roles, hints, selected/checked/disabled/busy states, headings, alerts, and live regions across mobile controls. +- Kept disabled/loading controls in the accessibility tree instead of replacing them with non-interactive views. +- Removed nested interactive controls that could hide child actions from VoiceOver or TalkBack. +- Made message and announcement actions available by ordinary assistive-technology activation instead of long-press only. +- Added modal semantics and initial sheet-heading focus. +- Added progress values and loading announcements. +- Marked decorative art, avatars, skeletons, and repeated website mockups as decorative where the same information is already conveyed in text. +- Raised key mobile targets to at least 44 points or provided equivalent hit slop. +- Reworked mobile contrast tokens and added AA contrast regression tests. +- Enabled system Reduce Motion handling for Reanimated and navigation transitions. +- Enabled both portrait and landscape orientation and removed fixed text-container heights that could clip larger text. +- Added a website skip link, visible focus treatment, route focus management, route titles, labeled fields, native category select, form status/error announcements, reduced-motion CSS, and AA contrast safeguards. +- Added a public `/accessibility` statement and accessibility contact path. +- Added permanent `audit:a11y` commands to the release gate for both mobile and website code. + +## Verification Evidence + +- Mobile static accessibility audit: passed +- Website static accessibility audit: passed +- Backend tests: 35 files, 444 tests passed +- Frontend tests after final remediation: 13 suites, 90 tests passed +- Mobile contrast tests: passed +- Shared UI accessibility regression tests: passed +- TypeScript/backend/website builds: passed +- iOS production export: passed +- Expo Doctor: 21/21 checks passed +- Production dependency audits: 0 vulnerabilities in backend, frontend, and landing packages +- Production migration upgrade path: passed +- Browser semantics across all public routes: no unnamed controls, unlabelled fields, missing image alternatives, duplicate IDs, heading jumps, or landmark failures +- Browser contrast sampling across all dark public routes: no AA failures found +- Browser SPA navigation: focus moved to `main-content` +- Browser reflow at 320 CSS pixels: no horizontal overflow or visible text clipping found +- iPhone 16e simulator at `accessibility-extra-extra-extra-large`: seeded native previews were run for Home, Explore, Pods, Clubs, and Inbox. The first expanded pass exposed real blockers in every primary tab: collapsed Home metrics, single-character Explore headings/cards, an unreadable fixed Pods hero, three-column club cards that truncated names, and squeezed Inbox tabs/banner content. Those layouts now switch to stacked, wrapped, or wider accessibility-size presentations; key content remains readable at 200% scaling and dock chrome is bounded to 120% while retaining complete assistive-technology labels. +- Follow-up largest-text screenshots showed readable primary titles, visible filters/cards/metadata/buttons, section headings, and all five dock labels across the five primary tabs. Content extending below the dock is placed in each screen's scroll container with explicit dock clearance; the required release-device pass below must still confirm every off-screen action is reachable by touch and VoiceOver. +- The existing seeded native preview harness now accepts `EXPO_PUBLIC_UI_PREVIEW_TAB` so all five primary tabs can be reopened directly for future device-size and Dynamic Type regression checks. +- The largest-text simulator pass also exercised Privacy & Data, create pod, create club, terms/age acceptance, direct messaging, edit profile, People search, a public profile, and blocked-user management. It exposed and prompted fixes for collapsed privacy rows, oversized form copy, two-column image controls, unreadable policy copy, narrow message bubbles, overlapping profile actions, truncated People tabs/cards, and compressed moderation rows. +- Follow-up screenshots passed for Privacy & Data, create pod, create club, terms/age acceptance, direct messaging, edit profile, and People search. Shared components now stack settings rows, segmented controls, form controls, headers with right-side actions, and person/action cards when the system font scale reaches an accessibility size. +- Native preview URLs now propagate their mode to child-screen fixture hooks, not just the root navigator. This fixes seeded club-detail/chat/event previews when launched directly from an Expo development URL. +- A final screenshot rerun of the remediated public-profile, blocked-user, and seeded club-detail/chat screens could not be completed after the local simulator tooling hit its usage limit. Their code compiles and the shared checks pass, but they remain explicitly included in the exact-build device gate below; this document does not claim a visual pass for those final states. +- iPhone simulator with Reduce Motion enabled: Oval relaunched successfully with the system preference active; Reanimated and native-stack transitions are configured to follow that preference. + +### Re-verification, August 5, 2026 (Linux sandbox) + +Re-ran the platform-independent portion of the gate against the working tree to confirm no regression since the original run: + +- `frontend/scripts/audit-accessibility.mjs`: passed +- `landing/scripts/audit-accessibility.mjs`: passed +- `frontend` TypeScript (`tsc --noEmit`): passed, no errors +- `frontend` Jest: 13 suites, 90 tests passed (includes `theme.accessibility.test.ts` and `ui.accessibility.test.tsx`) +- `scripts/release-check.mjs`: repository configuration passed + +Not re-run, and why — neither is a failure signal: + +- `scripts/verify-migrations.mjs` and backend tests require a local Postgres, which the sandbox lacks. The accessibility change set touches only `frontend/`, `landing/`, and `docs/`; no backend or Prisma files are modified. +- `landing` Vite build and `export:ios` require macOS-native toolchain binaries (rollup native module, Xcode). Both must be re-run on the Mac as part of `npm run release:check` immediately before the store build. + +## Required Real-iPhone Pass Before Submission + +Use the exact release build, not Expo Go. Fix any blocker before uploading. + +A printable, checkbox version of this pass — with the four screens that never got a simulator rerun (public profile, blocked users, club detail, club chat) pulled to the front as Priority 0 — is in [`ACCESSIBILITY_DEVICE_PASS_2026-08-05.md`](./ACCESSIBILITY_DEVICE_PASS_2026-08-05.md). Use that sheet to record results; the steps below are the reference. + +1. Turn on VoiceOver in Settings → Accessibility → VoiceOver. +2. Starting from a logged-out state, swipe through sign in/sign up, email verification, terms, and onboarding. Confirm the focus order follows the visual order and every control has a useful name and state. +3. Test Home, Explore, Pods, Inbox, and Clubs. Open one card in each, use every header action, and confirm cards with multiple actions expose each action separately. +4. Create or join a pod, open pod details, use chat/reply/reaction actions, and leave or remove a member using a test account. +5. Open a club, RSVP to a meeting, use member/announcement/chat actions, and confirm selected tabs and RSVP choices announce their state. +6. Test the safety-critical paths: report content, block a user, privacy settings, download data, and delete account. Do not complete deletion on a real production account. +7. In Settings → Accessibility → Display & Text Size → Larger Text, enable Larger Accessibility Sizes and test at the largest setting. Confirm text wraps, buttons grow, and no action is covered or unreachable. +8. Rotate the device to landscape on authentication, Home, Explore, pod detail, chat, Clubs, and privacy screens. Confirm all primary actions remain reachable by scrolling. +9. Turn on Reduce Motion and repeat navigation, sheet opening, and animated-feed screens. Confirm no essential information depends on motion. +10. Use Voice Control for a quick pass on visible buttons and fields; duplicate visible labels must still target the intended control. + +### Blockers + +Do not submit if any core flow has an unnamed control, trapped or skipped focus, a control that cannot be activated, content unavailable at the largest text size, an unreachable action in landscape, a status/error that is never announced, or information conveyed only by color. + +## Android Follow-up + +Before the first Google Play production release, repeat the same core-flow pass on a physical Android device with TalkBack, the largest font/display size, Remove animations enabled, and both orientations. iOS verification does not substitute for Android assistive-technology testing. + +## Legal/Operational Follow-up + +- Keep `contactus@theovalapp.com` monitored for accessibility reports and document response/fix dates. +- Re-run `npm run audit:a11y` and the release gate for every release. +- Include accessibility checks when adding a new shared control or major flow. +- For formal legal assurance, retain an accessibility professional and counsel familiar with ADA/state-law obligations; no automated or internal code review can make the company lawsuit-proof. + +## Release Note + +Build 22 has already been consumed and cannot be reused. `eas.json` sets `appVersionSource: "remote"` with `autoIncrement: true` on the production profile, so EAS assigns the next build number (23) automatically — do not add a local `buildNumber` to `app.json`, it would conflict with the remote source. Run the full `npm run release:check` on the Mac, then build and submit only after the device pass above is clean. diff --git a/docs/ACCESSIBILITY_DEVICE_PASS_2026-08-05.md b/docs/ACCESSIBILITY_DEVICE_PASS_2026-08-05.md new file mode 100644 index 0000000..967388e --- /dev/null +++ b/docs/ACCESSIBILITY_DEVICE_PASS_2026-08-05.md @@ -0,0 +1,97 @@ +# Accessibility device pass — required before submission + +Companion checklist to `ACCESSIBILITY_AUDIT_2026-08-05.md`. Everything below must be done on a **physical iPhone running the exact release build** (TestFlight or an internal-distribution build — not Expo Go, not the simulator). + +Tester: ______________ Device / iOS: ______________ Build: ______________ Date: ______________ + +--- + +## Before you start + +- [ ] Install the release build on the device and sign out completely. +- [ ] Have a throwaway test account ready for block/report/leave/remove actions. +- [ ] Settings → Accessibility → VoiceOver → **Verbosity → Speak Hints: On** (so missing hints are audible). +- [ ] Reset text size to default before Section A. Section B raises it; Section D lowers it again. + +**Stop and file a blocker if you hit any of these:** unnamed control ("button", "image", or a raw filename), focus that traps or skips visible content, a control VoiceOver can focus but not activate, content that becomes unreachable at the largest text size, an action off-screen with no scroll in landscape, a status or error that is never announced, or information carried only by color. + +--- + +## Priority 0 — never got a simulator pass + +These four screens were remediated in code but the final visual rerun was cut short. Check them **first**, at the largest accessibility text size, before spending time elsewhere. + +- [ ] **Public profile** (another user, via People search) — name/year/bio wrap, action buttons don't overlap, every action separately focusable. +- [ ] **Blocked users** (Privacy & Data → Blocked) — rows stack rather than compress, Unblock reachable and announces its result. +- [ ] **Club detail** — header, tabs, meeting cards, and RSVP controls readable; selected tab announces "selected". +- [ ] **Club chat** — message bubbles wrap, composer and send reachable above the keyboard, new messages announced. + +--- + +## A. VoiceOver pass (default text size) + +Swipe right/left through each screen; don't touch-explore. Focus order should follow reading order. + +- [ ] **Logged out:** sign in, sign up, email verification, terms/age acceptance, onboarding. +- [ ] **Home** — open one card; use every header action. +- [ ] **Explore** — filters announce selected state; open one result. +- [ ] **Pods** — open pod detail; cards with multiple actions expose each action separately. +- [ ] **Inbox** — open one thread; send a message; confirm it's announced. +- [ ] **Clubs** — open a club; RSVP to a meeting; confirm the RSVP choice announces its new state. +- [ ] **Pod flow:** create or join a pod, open details, use chat / reply / reaction, then leave or remove the test account. +- [ ] **Safety paths:** report content, block a user, privacy settings, request data download, open (do **not** complete) delete account. +- [ ] Loading and empty states announce themselves rather than going silent. + +## B. Largest text (Settings → Accessibility → Display & Text Size → Larger Text → Larger Accessibility Sizes → max) + +- [ ] The four Priority 0 screens above. +- [ ] All five primary tabs — titles readable, all five dock labels visible. +- [ ] **Tab bar / dock specifically.** Labels now scale to 200% and wrap to two lines at accessibility sizes (changed 2026-08-05, never rendered). Confirm the dock grows to fit, no label truncates, badges don't overlap adjacent tabs, and nothing is clipped at the bottom edge. +- [ ] Pod detail, pod chat, direct messaging. +- [ ] Create pod, create club, edit profile. +- [ ] Privacy & Data, terms/age acceptance, People search. +- [ ] Every screen: text wraps instead of truncating, buttons grow, nothing sits under the dock unreachably (scroll to the bottom of each). + +## C. Landscape (rotate with the device unlocked from rotation lock) + +- [ ] Authentication +- [ ] Home +- [ ] Explore +- [ ] Pod detail +- [ ] Pod chat +- [ ] Clubs +- [ ] Privacy & Data + +All primary actions must stay reachable, by scrolling if necessary. + +## D. Reduce Motion (Settings → Accessibility → Motion → Reduce Motion: On) + +- [ ] Navigate between tabs and push/pop screens. +- [ ] Open and dismiss sheets and modals. +- [ ] Load the animated feed screens. +- [ ] Confirm no essential information is conveyed only by an animation that is now suppressed. + +## E. Voice Control (Settings → Accessibility → Voice Control) + +- [ ] Say "Show names" on Home, Explore, and one chat screen. +- [ ] Activate three or four visible buttons and one text field by name. +- [ ] Where a label appears twice on screen, confirm the numbered picker targets the intended control. + +--- + +## Result + +- [ ] **Pass — no blockers.** Proceed to build and upload (EAS auto-increments; Build 22 is burned, next upload is 23+). +- [ ] **Blockers found** — list below, fix, rebuild, and rerun at minimum the affected section plus Priority 0. + +| # | Screen | What happened | Section | Fixed in | +|---|--------|---------------|---------|----------| +| | | | | | +| | | | | | +| | | | | | + +--- + +## Android + +The same core-flow pass is required on a physical Android device with TalkBack, largest font/display size, Remove animations, and both orientations before the first Google Play production release. iOS results do not carry over. diff --git a/docs/ACCESSIBILITY_SECOND_OPINION_2026-08-05.md b/docs/ACCESSIBILITY_SECOND_OPINION_2026-08-05.md new file mode 100644 index 0000000..7f73b22 --- /dev/null +++ b/docs/ACCESSIBILITY_SECOND_OPINION_2026-08-05.md @@ -0,0 +1,150 @@ +# Accessibility second opinion — August 5, 2026 + +Independent code review of the accessibility overhaul, done without reference to the previous session's own conclusions. The goal was to find things `ACCESSIBILITY_AUDIT_2026-08-05.md` claims or implies that the code does not support. + +**Verdict: the remediation is real and substantial, but "everything is done" is not accurate.** Six findings below, two of which are genuine WCAG conformance failures that exist in the code today and are not on any checklist. + +## Resolution status (same day) + +**F1 and F2 are fixed.** F3–F6 remain open and are deliberately deferred to after this release, except for one guardrail added below. + +| Finding | Status | +|---------|--------| +| F1 — unread counts not announced | ✅ Fixed, 6 call sites (one more than originally found) | +| F2 — Dynamic Type capped below 200% | ✅ Fixed, 11 sites raised to 2; 2 documented exceptions remain | +| F3 — scanner blind spots | ⚠️ Partially closed — added a `maxFontSizeMultiplier` rule; the rest stand | +| F4 — untested rgba surfaces, no 1.4.11 | ❌ Open | +| F5 — thin shared-UI tests | ⚠️ Improved — 3 assertions → 6 | +| F6 — thin live regions | ❌ Open, flagged for the device pass | + +Verification after the fixes: `tsc --noEmit` clean, `audit:a11y` passes, **13 suites / 93 tests pass** (was 90). The new scanner rule was tested against an injected violation to confirm it actually fires rather than silently passing — the same failure mode F3 criticises. + +### Two corrections to my own findings below + +- I wrote that `CountBubble` used `fontSize: 12`. It uses `bubbleStyles.label`, which is **`fontSize: 10`** — so the old ceiling was **14px**, not 16.8px. The finding was worse than stated. +- `HomeScreen`'s notification button already carried `` `Open inbox, ${total} unread` ``, so it was never an F1 site. Its badge still needed the F2 cap raised and is now also hidden from assistive tech for consistency. + +### Two additional F1 sites found while fixing + +Not in the original table, same defect class: + +- `InboxScreen.tsx:1282` — pod row labelled `` `Open ${title} chat` ``, dropping the unread count. +- `ExploreScreen.tsx:241` — interest pill labelled `` `Interested in ${title}` ``, dropping the visible demand count. + +--- + +## F1 — Unread counts are visible but not announced (WCAG 1.3.1, 4.1.2) — **High** + +`CountBubble` (`ui.tsx:770`) renders a bare number inside an unlabeled `View`. It is used in five places. In three of them the count never reaches a screen-reader user: + +| Site | Container | Accessible name | Count announced? | +|------|-----------|-----------------|------------------| +| `App.tsx:325` (tab bar) | `Pressable` | `` `${label}, ${badge} unread` `` | ✅ yes | +| `PodsScreen.tsx:195` | `Slab` | `` `Open ${title}` `` | ❌ **no** | +| `components/clubs/index.tsx:118` | `Pressable` | `` `Open ${channel.name} channel` `` | ❌ **no** | +| `components/clubs/index.tsx:639` | `Pressable` | label prop + hint | ❌ **no** | +| `ClubChatScreen.tsx:439` | switcher row | inherits container label | ❌ **no** | + +`Slab` (`ui.tsx:217`) renders an `AnimatedPressable`, which React Native defaults to `accessible={true}`. That collapses the subtree into one element and the explicit `accessibilityLabel` wins — so the child `{count}` is never read. A sighted user sees "3 unread"; a VoiceOver user hears "Open Coffee at Sip, button" and has no way to know. + +The tab bar proves the correct pattern was known. It just wasn't applied consistently. + +**Fix:** append the count to the container's `accessibilityLabel` at each of the four sites, exactly as `App.tsx:325` does. + +--- + +## F2 — Dynamic Type is capped, not supported (WCAG 1.4.4) — **High** + +The audit doc says "Maximum Dynamic Type fixes cover primary tabs, pod/club creation, privacy…". What the code actually does is cap text scaling in **303 places**: + +| Cap | Count | Effect | +|-----|-------|--------| +| `maxFontSizeMultiplier={2}` | 292 | 200% ceiling — meets WCAG 1.4.4, below iOS AX3–AX5 | +| `{1.5}` | 5 | **below 200% — fails 1.4.4** | +| `{1.4}` | 4 | **below 200% — fails 1.4.4** | +| `{1.2}` (tab labels, `App.tsx:329`) | 1 | **below 200%** | +| `{1}` (emoji avatars) | 2 | no scaling; defensible, these are images not prose | + +The sub-200% cases on real content: + +- `ExploreScreen.tsx:259` — demand count on `typography.captionSmall`, which is **`fontSize: 10`** (`theme.ts:327`). Capped at 1.4 → **14px ceiling.** A low-vision user can never make this larger. +- `ui.tsx:782` — `CountBubble`, `fontSize: 12` capped at 1.4 → 16.8px ceiling. +- `HomeScreen.tsx:779` — notification badge, same pattern. +- `InboxScreen.tsx:1330` — unread badge, same pattern. + +The 200% caps are a defensible engineering trade-off and satisfy the letter of WCAG 1.4.4. But capping is not the same as supporting, and the audit doc's phrasing implies the latter. The doc does disclose the 120% dock cap; it does not disclose the other 302. + +**Fix:** raise the sub-200% caps to 2 and verify the layouts hold, or accept them as documented, justified exceptions. Do not leave them undisclosed. + +--- + +## F3 — The static scanners are a lint, not an audit — **Medium** + +`frontend/scripts/audit-accessibility.mjs` (132 lines) passing is weaker evidence than the audit doc implies. Confirmed holes: + +- **Presence, not value.** `hasProp` only checks the attribute exists. `accessibilityLabel={undefined}`, `accessibilityLabel=""`, or a label bound to an empty variable all pass. +- **Custom controls aren't required to have names.** Only `Pressable`, `TouchableOpacity`, `TouchableWithoutFeedback` are checked for role + label. `IconButton`, `Button`, `Chip`, `ListRow` are checked only for illegal *nesting*. An `` with no label would pass. *(Checked manually — all 16 current uses do have labels. The guardrail is missing, not the labels.)* +- **`TouchableHighlight` is not covered at all.** +- **`` with no label passes** — the check accepts either `accessible` or `accessibilityLabel`, so an image marked accessible with no name is allowed. +- **`maxFontSizeMultiplier` is invisible to it.** The single most consequential Dynamic Type lever in this codebase (F2) is not checked at all. +- **Not checked at all:** touch-target size, heading hierarchy, focus order, live regions, modal semantics, color-only information. + +`landing/scripts/audit-accessibility.mjs` (75 lines): + +- **An `id` counts as a label.** Line 29 accepts `id=` as proof of an accessible name, but an `id` only helps if a matching `