Skip to content

[codex] Polish frontend launch readiness and login/profile flows#113

Merged
jaydonkc merged 8 commits intodevfrom
ui-fixes-login-flow-improvements
Apr 21, 2026
Merged

[codex] Polish frontend launch readiness and login/profile flows#113
jaydonkc merged 8 commits intodevfrom
ui-fixes-login-flow-improvements

Conversation

@jaydonkc
Copy link
Copy Markdown
Collaborator

@jaydonkc jaydonkc commented Apr 21, 2026

What changed

  • polish frontend launch readiness across home, settings, listings, profile, and conversation flows
  • standardize keyboard-aware UI behavior with reusable screen, dock, underlay, modal, and button primitives
  • improve login and profile editing flows, including keeping profile email read-only and tightening redirect/error handling
  • add shared Cal Poly majors and graduation-year constants plus related validation helpers and tests
  • add local production static serving support and Vercel-facing config updates for the web bundle
  • include supporting backend profile adjustments and regression coverage needed by the UI changes

Why

This branch consolidates a set of UI fixes and polish changes that were spread across login, profile, listings, and messaging surfaces. The goal is to make the launch path more consistent, reduce duplicated keyboard/layout behavior, and harden the user-facing flows before merge.

Impact

  • smoother login and onboarding-adjacent flows
  • cleaner profile editing and account settings behavior
  • more consistent keyboard handling across mobile screens
  • improved maintainability through reusable UI primitives and shared constants/helpers
  • better local/prod parity for the exported web bundle

Validation

  • npm run lint
  • npm run typecheck

Summary by CodeRabbit

  • New Features
    • Major picker UI, new keyboard-aware layout components, glass-style icon button, modal sheet, and keyboard underlay for improved mobile UX
    • Graduation-year utilities and keyboard-height hook
  • Improvements
    • Centralized user-facing error messages across flows
    • Profile email is now read-only in-editor; profile updates ignore email
    • Refined major label formatting, bookmark animations, and web in-app prompts
  • Tests
    • Added unit tests for graduation years, majors, and error-message mappings
  • Chores
    • Added a static web serve script, prod web start script, and global security headers for deployments

@vercel
Copy link
Copy Markdown

vercel Bot commented Apr 21, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
poly-buys Ready Ready Preview, Comment Apr 21, 2026 3:00pm

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 21, 2026

📝 Walkthrough

Walkthrough

Enforces profile email immutability on the backend, adds tests ensuring email cannot be changed, and introduces multiple frontend UI/keyboard utilities, pickers, centralized user-flow error messages, and minor deployment/env updates.

Changes

Cohort / File(s) Summary
Backend Profile Email Enforcement
backend/convex/profiles.ts, backend/convex/__tests__/profiles.test.ts
updateProfile now ignores incoming email in updates; tests added to verify email stays unchanged and that updates with no valid fields are rejected.
Frontend Constants & Validation
frontend/constants/calPolyMajors.ts, frontend/constants/graduationYears.ts, frontend/constants/__tests__/calPolyMajors.test.ts, frontend/constants/__tests__/graduationYears.test.ts
Added Cal Poly majors list and search/format utilities; added graduation-year options/validation and tests.
Frontend Keyboard & Layout Infrastructure
frontend/hooks/useKeyboardHeight.ts, frontend/components/ui/KeyboardUnderlay.tsx, frontend/components/ui/KeyboardDockScreen.tsx, frontend/components/ui/ModalSheet.tsx, frontend/components/ui/ScreenScrollView.tsx, frontend/components/ui/index.ts, frontend/components/ui/KeyboardAwareScreen.tsx
Introduced keyboard-height hook and underlay; added KeyboardDockScreen, ModalSheet, ScreenScrollView, and integrated keyboard-aware behavior into existing screen wrapper exports.
Frontend UI Components
frontend/components/MajorPicker.tsx, frontend/components/ui/GlassIconButton.tsx, frontend/components/ListingCard.tsx, frontend/components/OpenInAppPrompt.tsx, frontend/components/ReportModal.tsx, frontend/components/PriceRangePicker.tsx
Added MajorPicker and GlassIconButton; refactored listing card to use bookmark icons/GlassIconButton; OpenInAppPrompt supports card/page variants; standardized error handling in ReportModal; added KeyboardUnderlay usage in PriceRangePicker.
Frontend Profile & Auth Flows
frontend/app/profile/edit.tsx, frontend/app/auth/login.tsx, frontend/app/profile/[userId].tsx
Replaced free-text major/year inputs with pickers; made profile email read-only and removed sending email in profile mutations; added major validation/display formatting and block/unblock controls.
Frontend Screens & Error Handling
frontend/app/(tabs)/home.tsx, frontend/app/(tabs)/settings.tsx, frontend/app/account-settings.tsx, frontend/app/conversations/[id].tsx, frontend/app/conversations/new.tsx, frontend/app/listings/[id].tsx, frontend/app/listings/[id]/edit.tsx, frontend/app/listings/new.tsx
Integrated keyboard utilities and underlay where appropriate; replaced ad-hoc error messages with centralized getUserFlowErrorMessage; adjusted UI flows (web handoff prompt, compose dock, bookmark animation, share payload, major label formatting).
Frontend Lib & Env
frontend/lib/user-flow-errors.ts, frontend/lib/env.ts, frontend/app/_layout.tsx
Added getUserFlowErrorMessage with many flow contexts; added getRequiredExpoPublicEnv helper and used it in layout for Convex client URL retrieval.
Frontend Dev / Build / Tests
frontend/.env.example, frontend/package.json, package.json, scripts/serve-static.mjs, vercel.json, test import updates (frontend/app/auth/__tests__/loginRedirect.test.ts, frontend/app/listings/__tests__/newListingValidation.test.ts, frontend/lib/__tests__/user-flow-errors.test.ts)
Added example env var EXPO_PUBLIC_EAS_PROJECT_ID; added @react-native-picker/picker dependency; added static server script and root start:prod:web script; added global security headers in vercel.json; updated test imports and added user-flow-errors tests.

Sequence Diagram(s)

(omitted)

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

Suggested reviewers

  • SamanSP1386

Poem

🐰 I hopped through code with joy and ease,

Locked emails safe, no accidental breeze,
Pickers gleam like clover in the sun,
Keyboards settle — underlays done,
Glass buttons sparkle — another sprint well-run! ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title directly and concisely describes the main change: frontend polish focusing on launch readiness and login/profile flows.
Description check ✅ Passed The PR description covers the template's key sections with sufficient detail: clear summary of changes, reasons, impact, and validation steps performed.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ 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 ui-fixes-login-flow-improvements

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 and usage tips.

@jaydonkc jaydonkc marked this pull request as ready for review April 21, 2026 10:58
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 8

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
backend/convex/profiles.ts (1)

165-303: ⚠️ Potential issue | 🟡 Minor

Silently-ignored email can break old clients that send only email.

updateProfile still accepts email in its validator but never writes it. If a released client calls updateProfile({ email }) on its own (e.g., an email-edit screen in an older build), update stays empty and the call now throws 'No valid fields to update' — the opposite of the stated "backwards compatibility" intent on Lines 203–204.

Two options depending on the desired UX for legacy clients:

Option A — treat email-only updates as a successful no-op
-    if (Object.keys(update).length === 0) {
-      throw new ConvexError('No valid fields to update');
-    }
-
-    await ctx.db.patch(profile._id, update);
+    if (Object.keys(update).length === 0) {
+      // Older clients may send only `email`, which is now immutable. Treat as no-op
+      // instead of surfacing a confusing error.
+      if (args.email !== undefined) return;
+      throw new ConvexError('No valid fields to update');
+    }
+
+    await ctx.db.patch(profile._id, update);

Option B — drop email from the args validator entirely and bump the client contract, so stale clients fail loudly at the validator instead of silently drifting.

frontend/components/ui/KeyboardAwareScreen.tsx (1)

35-49: ⚠️ Potential issue | 🟡 Minor

Don’t let caller padding override the safe-area bottom padding.

contentContainerStyle comes last, so any caller with paddingBottom replaces bottomPadding. Add the caller’s bottom padding to the computed inset and apply the merged value last.

🛠️ Proposed fix
   const headerHeight = useHeaderHeight();
   const keyboardHeight = useKeyboardHeight();
   const bottomPadding = disableSafeAreaBottom ? 0 : insets.bottom + 8;
+  const flattenedContentContainerStyle = StyleSheet.flatten(contentContainerStyle);
+  const requestedPaddingBottom =
+    typeof flattenedContentContainerStyle?.paddingBottom === 'number'
+      ? flattenedContentContainerStyle.paddingBottom
+      : 0;
 
   return (
@@
       <ScreenScrollView
         contentContainerStyle={[
           styles.content,
-          { paddingBottom: bottomPadding },
           contentContainerStyle,
+          { paddingBottom: bottomPadding + requestedPaddingBottom },
         ]}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/components/ui/KeyboardAwareScreen.tsx` around lines 35 - 49, The
caller can override the safe-area bottom padding because contentContainerStyle
is spread last; update KeyboardAwareScreen to compute the caller's bottom
padding from the incoming contentContainerStyle (handle object or array) and add
it to the computed bottomPadding (bottomPadding + callerPaddingBottom) and then
apply that merged paddingBottom as the final value in the ScreenScrollView's
contentContainerStyle; reference the symbols bottomPadding,
contentContainerStyle, and ScreenScrollView and ensure the resulting
contentContainerStyle keeps styles.content first, the computed merged
paddingBottom last so callers cannot replace it.
frontend/app/(tabs)/settings.tsx (1)

429-447: ⚠️ Potential issue | 🟡 Minor

Keep pill touch targets at least 44px high.

The new 42 min height makes these primary profile actions slightly undersized for mobile tapping. Bump them back to at least 44.

♿ Proposed fix
   primaryPill: {
     flex: 1,
-    minHeight: 42,
+    minHeight: 44,
@@
   secondaryPill: {
     flex: 1,
-    minHeight: 42,
+    minHeight: 44,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/app/`(tabs)/settings.tsx around lines 429 - 447, The pill touch
targets are undersized: update the minHeight in the style objects for
primaryPill (currently minHeight: 42) and secondaryPill to at least 44 so mobile
tap targets meet accessibility guidelines; locate the minHeight properties in
the primaryPill and secondaryPill style definitions and change their values from
42 to 44 (or greater) and ensure any related components relying on these styles
still lay out correctly.
🧹 Nitpick comments (4)
backend/convex/__tests__/profiles.test.ts (1)

79-104: Good coverage for email immutability.

Consider also asserting behavior when only email is supplied to updateProfile — with the new logic, that call now throws 'No valid fields to update', which is a subtle behavior change older clients may hit. A regression test would lock in whichever behavior you intend.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/convex/__tests__/profiles.test.ts` around lines 79 - 104, Add a
regression test in the same file (profiles.test.ts) that creates a profile via
asUser.mutation(api.profiles.createProfile, ...) and then calls
asUser.mutation(api.profiles.updateProfile, { email: 'updated@calpoly.edu' })
expecting it to throw the 'No valid fields to update' error; reference the
existing convexTest/withIdentity setup and the profileId lookup (ctx.db.get) to
assert the profile's email remains unchanged if you instead decide to assert
ignore behavior—this locks in the intended behavior for updateProfile when only
email is supplied.
frontend/app/conversations/[id].tsx (1)

281-289: Unblock action is fire-and-forget with no confirmation — inconsistent with block and with account-settings.

Tapping the header "Unblock" immediately triggers unblockUser(...) with no confirmation dialog, while Block uses an Alert.alert confirm flow and account-settings.tsx/profile/[userId].tsx both confirm unblock via Alert.alert('Unblock', ...). Consider mirroring the existing confirmation pattern for consistency and to prevent accidental unblocks via the header chevron area.

♻️ Proposed change
-    if (isBlockingOther === true) {
-      unblockUser({ blockedId: otherUserId }).catch((err) => {
-        Alert.alert('Could Not Unblock', getUserFlowErrorMessage(err, 'unblock-user'));
-      });
-      return;
-    }
+    if (isBlockingOther === true) {
+      Alert.alert('Unblock user', 'Allow this user to message you again?', [
+        { text: 'Cancel', style: 'cancel' },
+        {
+          text: 'Unblock',
+          onPress: () => {
+            unblockUser({ blockedId: otherUserId }).catch((err) => {
+              Alert.alert('Could Not Unblock', getUserFlowErrorMessage(err, 'unblock-user'));
+            });
+          },
+        },
+      ]);
+      return;
+    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/app/conversations/`[id].tsx around lines 281 - 289, handleBlockPress
currently calls unblockUser({ blockedId: otherUserId }) immediately when
isBlockingOther is true; change it to show the same Alert.alert confirmation
flow used for blocking and in account-settings/profile pages before calling
unblockUser, and only call unblockUser inside the confirmation callback
(preserving the existing .catch(err => Alert.alert('Could Not Unblock',
getUserFlowErrorMessage(err, 'unblock-user')))). Update handleBlockPress to
mirror the confirm dialog title/body/buttons and the error-handling behavior
used by the Block flow so unblocking from the header requires explicit user
confirmation.
frontend/components/OpenInAppPrompt.tsx (1)

69-82: Nit: inconsistent async-handler pattern.

The primary button uses onPress={() => void handleOpenInApp()} (Line 50) while the download link's onPress is directly assigned an async function on Line 70. With @typescript-eslint/no-misused-promises or similar, the latter pattern can trigger warnings; consider extracting to a named handler and invoking via () => void handleDownload() for consistency.

♻️ Proposed change
+  const handleDownloadApp = async () => {
+    try {
+      await ExpoLinking.openURL(APP_STORE_URL);
+    } catch (error) {
+      Alert.alert('Download Failed', getUserFlowErrorMessage(error, 'download-app'));
+    }
+  };
...
-      <Pressable
-        onPress={async () => {
-          try {
-            await ExpoLinking.openURL(APP_STORE_URL);
-          } catch (error) {
-            Alert.alert('Download Failed', getUserFlowErrorMessage(error, 'download-app'));
-          }
-        }}
+      <Pressable
+        onPress={() => void handleDownloadApp()}
         accessibilityRole="link"
         accessibilityLabel="Download the PolyBuys app"
       >
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/components/OpenInAppPrompt.tsx` around lines 69 - 82, Extract the
inline async onPress into a named async handler (e.g., handleDownload) and call
it from the Pressable using () => void handleDownload() to match the primary
button pattern; inside handleDownload await ExpoLinking.openURL(APP_STORE_URL)
inside a try/catch and on error call Alert.alert('Download Failed',
getUserFlowErrorMessage(error, 'download-app')) so the existing error message
logic and accessibility props remain unchanged.
frontend/components/MajorPicker.tsx (1)

66-77: Minor a11y: missing button role and selected state on major options.

The option TouchableOpacity has no accessibilityRole, accessibilityLabel, or accessibilityState, so screen readers won't announce it as a selectable button or reflect the current selection. Consider:

♻️ Proposed change
             <TouchableOpacity
               style={[styles.option, isSelected && styles.optionSelected]}
               onPress={() => handleSelect(item)}
+              accessibilityRole="button"
+              accessibilityLabel={formatMajorLabel(item)}
+              accessibilityState={{ selected: isSelected }}
             >
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/components/MajorPicker.tsx` around lines 66 - 77, The
TouchableOpacity option (in the MajorPicker component around
handleSelect/formatMajorLabel usage) lacks accessibility props; update the
TouchableOpacity to include accessibilityRole="button", accessibilityLabel
derived from formatMajorLabel(item) (optionally include ordinal/index if
available), accessibilityState={{ selected: isSelected }}, and accessible={true}
so screen readers announce it as a selectable button and reflect the current
selection; keep the existing visual styles (styles.option/styles.optionSelected)
and existing onPress={ () => handleSelect(item) } intact.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@frontend/app/auth/login.tsx`:
- Around line 378-380: The existing branch that handles
isExistingProfileError(err) should not setStep('checking') because
currentProfile may be stale and the checking effect will bounce the UI back to
'profile'; instead, treat this as a confirmed-post-auth state and redirect to
the post-auth/success flow directly. Update the error branch in login.tsx (where
isExistingProfileError(err) is handled) to invoke the same transition used for
successful auth (e.g., setStep('success') or call the post-auth handler) and
avoid reading or relying on currentProfile; keep the checking effect and flow
for other cases unchanged.

In `@frontend/app/listings/`[id].tsx:
- Around line 197-200: The effect that runs on listingId changes currently only
resets prevDisplayedSavedRef and bookmarkScale, but you must also clear the
optimistic bookmark state so displayedSaved doesn't show stale data; update the
useEffect (the one referencing prevDisplayedSavedRef, bookmarkScale and
listingId) to also reset the optimistic state (call the savedOptimistic setter,
e.g. setSavedOptimistic(null) or setSavedOptimistic(undefined)/false per your
state type) so displayedSaved (which prefers savedOptimistic) is recomputed from
fresh props when listingId changes.

In `@frontend/app/profile/edit.tsx`:
- Around line 249-270: The save flow is incorrectly validating a read-only
legacy field: remove the email validation that uses profileEmail/normalizedEmail
and the call to getEmailValidationError in the submit handler so saving
name/major/bio is not blocked for legacy/review accounts, and also remove the
now-unused getEmailValidationError import; verify there are no other references
to profileEmail validation in the createProfile/updateProfile submission code
paths.

In `@frontend/components/ListingCard.tsx`:
- Around line 311-341: The save and manage buttons (rendered when onToggleSave
and onManagePress are truthy using GlassIconButton with styles.saveButton and
styles.manageButton) currently overlap because they share the same absolute
position; update the component to render them in a small stacked/offset layout
(e.g., wrap both in a single container that positions them vertically with a
small gap or change styles.manageButton to apply a right/top offset when both
props exist) so both controls remain reachable and maintain accessibilityLabel
behavior; ensure the change also covers the other occurrence noted around lines
415-430 where the same pattern is used.

In `@frontend/constants/calPolyMajors.ts`:
- Around line 80-96: normalizeMajorSearchValue currently strips all
parentheticals which removes campus labels (e.g., "(Solano Campus)") so
majorMatchesQuery fails for queries like "solano"; update
normalizeMajorSearchValue to preserve parenthetical text that indicates campus
names by only removing parentheticals that match degree/type tokens (e.g., "BS",
"MS", "Minor") while keeping parentheticals containing the word "campus" or
known campus names; ensure majorMatchesQuery still normalizes both major and
query via normalizeMajorSearchValue and performs includes() as before.

In `@frontend/lib/env.ts`:
- Around line 1-20: readEnv currently reads process.env via bracket notation
(process.env[name]) which Metro can't inline; change to use dot-notation by
creating a local mapping object with dot-accessed properties (e.g. const PUBLIC
= { EXPO_PUBLIC_CONVEX_URL: process.env.EXPO_PUBLIC_CONVEX_URL }) and then have
readEnv/getRequiredExpoPublicEnv read from that mapping instead of
process.env[name]; update readEnv and getRequiredExpoPublicEnv to reference
PUBLIC.EXPO_PUBLIC_CONVEX_URL (or PUBLIC[name]) so Expo can inline the
EXPO_PUBLIC_* var while preserving type safety.

In `@frontend/lib/user-flow-errors.ts`:
- Around line 21-23: getRawErrorMessage currently only handles Error instances
so string rejections and error-like objects lose their messages; update
getRawErrorMessage to also handle string values (return trimmed lowercased
string), objects with a message property (stringified/trimmed/lowercased), and
fall back to empty string for other types; ensure you reference the function
name getRawErrorMessage and perform instanceof Error, typeof error === 'string',
and check for (error && typeof (error as any).message === 'string') to locate
and implement the checks.

In `@scripts/serve-static.mjs`:
- Around line 35-40: In resolveAssetPath, guard against malformed
percent-escapes by wrapping decodeURIComponent(urlPath) in a try/catch and fail
early on decode errors, and replace the insecure prefix check
candidatePath.startsWith(distDir) with a boundary-safe test using
path.relative(distDir, candidatePath) (reject when the relative path starts with
'..' or is an absolute escape); preserve the existing normalization logic
(safeRelativePath / candidatePath) but perform path.resolve/distDir computation
after successful decode and return a safe failure (null or throw) when the
boundary check fails. Ensure you reference resolveAssetPath, decodedPath,
candidatePath and distDir when applying these changes.

---

Outside diff comments:
In `@frontend/app/`(tabs)/settings.tsx:
- Around line 429-447: The pill touch targets are undersized: update the
minHeight in the style objects for primaryPill (currently minHeight: 42) and
secondaryPill to at least 44 so mobile tap targets meet accessibility
guidelines; locate the minHeight properties in the primaryPill and secondaryPill
style definitions and change their values from 42 to 44 (or greater) and ensure
any related components relying on these styles still lay out correctly.

In `@frontend/components/ui/KeyboardAwareScreen.tsx`:
- Around line 35-49: The caller can override the safe-area bottom padding
because contentContainerStyle is spread last; update KeyboardAwareScreen to
compute the caller's bottom padding from the incoming contentContainerStyle
(handle object or array) and add it to the computed bottomPadding (bottomPadding
+ callerPaddingBottom) and then apply that merged paddingBottom as the final
value in the ScreenScrollView's contentContainerStyle; reference the symbols
bottomPadding, contentContainerStyle, and ScreenScrollView and ensure the
resulting contentContainerStyle keeps styles.content first, the computed merged
paddingBottom last so callers cannot replace it.

---

Nitpick comments:
In `@backend/convex/__tests__/profiles.test.ts`:
- Around line 79-104: Add a regression test in the same file (profiles.test.ts)
that creates a profile via asUser.mutation(api.profiles.createProfile, ...) and
then calls asUser.mutation(api.profiles.updateProfile, { email:
'updated@calpoly.edu' }) expecting it to throw the 'No valid fields to update'
error; reference the existing convexTest/withIdentity setup and the profileId
lookup (ctx.db.get) to assert the profile's email remains unchanged if you
instead decide to assert ignore behavior—this locks in the intended behavior for
updateProfile when only email is supplied.

In `@frontend/app/conversations/`[id].tsx:
- Around line 281-289: handleBlockPress currently calls unblockUser({ blockedId:
otherUserId }) immediately when isBlockingOther is true; change it to show the
same Alert.alert confirmation flow used for blocking and in
account-settings/profile pages before calling unblockUser, and only call
unblockUser inside the confirmation callback (preserving the existing .catch(err
=> Alert.alert('Could Not Unblock', getUserFlowErrorMessage(err,
'unblock-user')))). Update handleBlockPress to mirror the confirm dialog
title/body/buttons and the error-handling behavior used by the Block flow so
unblocking from the header requires explicit user confirmation.

In `@frontend/components/MajorPicker.tsx`:
- Around line 66-77: The TouchableOpacity option (in the MajorPicker component
around handleSelect/formatMajorLabel usage) lacks accessibility props; update
the TouchableOpacity to include accessibilityRole="button", accessibilityLabel
derived from formatMajorLabel(item) (optionally include ordinal/index if
available), accessibilityState={{ selected: isSelected }}, and accessible={true}
so screen readers announce it as a selectable button and reflect the current
selection; keep the existing visual styles (styles.option/styles.optionSelected)
and existing onPress={ () => handleSelect(item) } intact.

In `@frontend/components/OpenInAppPrompt.tsx`:
- Around line 69-82: Extract the inline async onPress into a named async handler
(e.g., handleDownload) and call it from the Pressable using () => void
handleDownload() to match the primary button pattern; inside handleDownload
await ExpoLinking.openURL(APP_STORE_URL) inside a try/catch and on error call
Alert.alert('Download Failed', getUserFlowErrorMessage(error, 'download-app'))
so the existing error message logic and accessibility props remain unchanged.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 6f8d0a5d-5954-4049-b538-821e010d5234

📥 Commits

Reviewing files that changed from the base of the PR and between a4b8459 and 0f534d5.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (41)
  • backend/convex/__tests__/profiles.test.ts
  • backend/convex/profiles.ts
  • frontend/.env.example
  • frontend/app/(tabs)/home.tsx
  • frontend/app/(tabs)/settings.tsx
  • frontend/app/_layout.tsx
  • frontend/app/account-settings.tsx
  • frontend/app/auth/__tests__/loginRedirect.test.ts
  • frontend/app/auth/login.tsx
  • frontend/app/conversations/[id].tsx
  • frontend/app/conversations/new.tsx
  • frontend/app/listings/[id].tsx
  • frontend/app/listings/[id]/edit.tsx
  • frontend/app/listings/__tests__/newListingValidation.test.ts
  • frontend/app/listings/new.tsx
  • frontend/app/profile/[userId].tsx
  • frontend/app/profile/edit.tsx
  • frontend/components/ListingCard.tsx
  • frontend/components/MajorPicker.tsx
  • frontend/components/OpenInAppPrompt.tsx
  • frontend/components/PriceRangePicker.tsx
  • frontend/components/ReportModal.tsx
  • frontend/components/ui/GlassIconButton.tsx
  • frontend/components/ui/KeyboardAwareScreen.tsx
  • frontend/components/ui/KeyboardDockScreen.tsx
  • frontend/components/ui/KeyboardUnderlay.tsx
  • frontend/components/ui/ModalSheet.tsx
  • frontend/components/ui/ScreenScrollView.tsx
  • frontend/components/ui/index.ts
  • frontend/constants/__tests__/graduationYears.test.ts
  • frontend/constants/calPolyMajors.ts
  • frontend/constants/graduationYears.ts
  • frontend/hooks/useKeyboardHeight.ts
  • frontend/lib/auth/loginRedirect.ts
  • frontend/lib/env.ts
  • frontend/lib/listings/newListingValidation.ts
  • frontend/lib/user-flow-errors.ts
  • frontend/package.json
  • package.json
  • scripts/serve-static.mjs
  • vercel.json

Comment thread frontend/app/auth/login.tsx
Comment thread frontend/app/listings/[id].tsx
Comment thread frontend/app/profile/edit.tsx Outdated
Comment thread frontend/components/ListingCard.tsx Outdated
Comment thread frontend/constants/calPolyMajors.ts
Comment thread frontend/lib/env.ts Outdated
Comment thread frontend/lib/user-flow-errors.ts
Comment thread scripts/serve-static.mjs Outdated
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@frontend/app/conversations/`[id].tsx:
- Around line 284-296: The unblock flow currently calls unblockAction which
fires unblockUser({ blockedId: otherUserId }) without awaiting and never shows
success feedback; update the isBlockingOther branch (functions/symbols:
isBlockingOther, unblockAction, unblockUser, getUserFlowErrorMessage,
Alert.alert) to await the unblockUser mutation inside unblockAction, handle
errors as done today, and on successful resolution call Alert.alert with a
success message (e.g., "User unblocked") so the UX mirrors the block flow and
surfaces success to the user.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 82eaf33b-a7e8-4d9b-b20f-66b660a8760a

📥 Commits

Reviewing files that changed from the base of the PR and between 0f534d5 and b67c21f.

📒 Files selected for processing (16)
  • backend/convex/__tests__/profiles.test.ts
  • frontend/app/(tabs)/settings.tsx
  • frontend/app/auth/login.tsx
  • frontend/app/conversations/[id].tsx
  • frontend/app/listings/[id].tsx
  • frontend/app/profile/edit.tsx
  • frontend/components/ListingCard.tsx
  • frontend/components/MajorPicker.tsx
  • frontend/components/OpenInAppPrompt.tsx
  • frontend/components/ui/KeyboardAwareScreen.tsx
  • frontend/constants/__tests__/calPolyMajors.test.ts
  • frontend/constants/calPolyMajors.ts
  • frontend/lib/__tests__/user-flow-errors.test.ts
  • frontend/lib/env.ts
  • frontend/lib/user-flow-errors.ts
  • scripts/serve-static.mjs
✅ Files skipped from review due to trivial changes (4)
  • backend/convex/tests/profiles.test.ts
  • frontend/lib/tests/user-flow-errors.test.ts
  • scripts/serve-static.mjs
  • frontend/constants/tests/calPolyMajors.test.ts
🚧 Files skipped from review as they are similar to previous changes (6)
  • frontend/lib/env.ts
  • frontend/app/(tabs)/settings.tsx
  • frontend/components/MajorPicker.tsx
  • frontend/app/profile/edit.tsx
  • frontend/app/auth/login.tsx
  • frontend/lib/user-flow-errors.ts

Comment on lines 284 to 296
if (isBlockingOther === true) {
unblockUser({ blockedId: otherUserId }).catch((err) => {
Alert.alert('Could not unblock', err instanceof Error ? err.message : 'Please try again.');
});
const unblockAction = () => {
unblockUser({ blockedId: otherUserId }).catch((err) => {
Alert.alert('Could Not Unblock', getUserFlowErrorMessage(err, 'unblock-user'));
});
};

Alert.alert('Unblock user', 'Allow this user to message you again?', [
{ text: 'Cancel', style: 'cancel' },
{ text: 'Unblock', onPress: () => void unblockAction() },
]);
return;
}
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.

⚠️ Potential issue | 🟡 Minor

Unblock flow lacks success feedback (UX inconsistency with block flow).

The block path surfaces a success Alert ("User blocked"), but the unblock path is silent on success — users get no confirmation that the unblock actually went through. Since unblockUser is fired and not awaited, a slow/failed mutation is easy to miss (error path is covered, success path is not). Recommend mirroring the block flow for consistency.

♻️ Suggested change
     if (isBlockingOther === true) {
-      const unblockAction = () => {
-        unblockUser({ blockedId: otherUserId }).catch((err) => {
+      const unblockAction = async () => {
+        try {
+          await unblockUser({ blockedId: otherUserId });
+          Alert.alert('User unblocked', 'You can now exchange messages with this user again.');
+        } catch (err) {
           Alert.alert('Could Not Unblock', getUserFlowErrorMessage(err, 'unblock-user'));
-        });
+        }
       };

       Alert.alert('Unblock user', 'Allow this user to message you again?', [
         { text: 'Cancel', style: 'cancel' },
         { text: 'Unblock', onPress: () => void unblockAction() },
       ]);
       return;
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (isBlockingOther === true) {
unblockUser({ blockedId: otherUserId }).catch((err) => {
Alert.alert('Could not unblock', err instanceof Error ? err.message : 'Please try again.');
});
const unblockAction = () => {
unblockUser({ blockedId: otherUserId }).catch((err) => {
Alert.alert('Could Not Unblock', getUserFlowErrorMessage(err, 'unblock-user'));
});
};
Alert.alert('Unblock user', 'Allow this user to message you again?', [
{ text: 'Cancel', style: 'cancel' },
{ text: 'Unblock', onPress: () => void unblockAction() },
]);
return;
}
if (isBlockingOther === true) {
const unblockAction = async () => {
try {
await unblockUser({ blockedId: otherUserId });
Alert.alert('User unblocked', 'You can now exchange messages with this user again.');
} catch (err) {
Alert.alert('Could Not Unblock', getUserFlowErrorMessage(err, 'unblock-user'));
}
};
Alert.alert('Unblock user', 'Allow this user to message you again?', [
{ text: 'Cancel', style: 'cancel' },
{ text: 'Unblock', onPress: () => void unblockAction() },
]);
return;
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/app/conversations/`[id].tsx around lines 284 - 296, The unblock flow
currently calls unblockAction which fires unblockUser({ blockedId: otherUserId
}) without awaiting and never shows success feedback; update the isBlockingOther
branch (functions/symbols: isBlockingOther, unblockAction, unblockUser,
getUserFlowErrorMessage, Alert.alert) to await the unblockUser mutation inside
unblockAction, handle errors as done today, and on successful resolution call
Alert.alert with a success message (e.g., "User unblocked") so the UX mirrors
the block flow and surfaces success to the user.

@jaydonkc jaydonkc merged commit 6573a99 into dev Apr 21, 2026
5 checks passed
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.

1 participant