Skip to content

Feature/poly 34 shareable links#47

Merged
jaydonkc merged 7 commits intodevfrom
feature/POLY-34-Shareable-Links
Mar 2, 2026
Merged

Feature/poly 34 shareable links#47
jaydonkc merged 7 commits intodevfrom
feature/POLY-34-Shareable-Links

Conversation

@mattphanm
Copy link
Copy Markdown
Collaborator

@mattphanm mattphanm commented Feb 25, 2026

Closes #42 & #34
Linear: POLY-42 & POLY-34

Summary

-Added shareable URL generation and copy to clipboard

How to Test

Steps to verify locally:

  • npm run lint
  • npm run typecheck
  • npm test
  • Manual flow:
    1. npm run dev:backend (in terminal A)
    2. npm run dev (in terminal B)
    3. Verify the change: <describe expected behavior/screens>

Checklist

  • Tests added/updated (if applicable)
  • Lint/tests pass locally (npm run lint)
  • Docs updated (README/ADR/changelog if needed)
  • Follows conventional commit format
  • No merge conflicts with dev

Screenshots / Demos

(if UI or visible behavior - attach images, videos, or GIFs)
Screenshot 2026-02-24 at 4 28 35 PM

Summary by CodeRabbit

Release Notes

  • New Features

    • Added ability to share listings via the share button in listing details
    • Introduced shortened listing URL redirects for easier sharing
    • Added message type support for enhanced messaging capabilities
    • Implemented appeal process for hidden listings through support contact
  • Bug Fixes

    • Hidden listings are now properly filtered from the browsing view
    • Improved access control to prevent non-owners from viewing hidden listings
    • Enhanced loading state handling in listing details

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Feb 25, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR adds hidden listing visibility control, message type tracking with participant metadata, and introduces short URL redirects for listings. The backend schema is extended with participantIds, lastMessageId, and message type fields, while the frontend introduces filtering for hidden listings, a sharing capability, and new components for unavailable and hidden listing states.

Changes

Cohort / File(s) Summary
Schema & Database Structure
backend/convex/schema.ts
Added participantIds and lastMessageId fields to conversations table; updated by_listing_buyer_seller index to include sellerId; added type field to messages table.
Backend Messaging Logic
backend/convex/messages.ts
Extended sendMessage and internalSendMessage to accept and persist message type (defaults to 'text'); added participant IDs to conversation creation in getOrCreateConversation and debugCreateConversationID; introduced backfillMessagingFields internalMutation to patch missing participantIds and type fields.
Backend Tests
backend/convex/__tests__/messages.test.ts, backend/convex/__tests__/testUtils.ts
Updated test fixtures to include type: 'text' in message records and participantIds in conversation records.
Frontend Listing Display
frontend/app/listings/[id].tsx
Added listing share functionality via React Native Share API; introduced isHidden and isOwner logic to gate access and render HiddenBanner for owners or ListingUnavailable for non-owners; added share button in header alongside title.
Frontend Listing Edit
frontend/app/listings/[id]/edit.tsx
Added isHidden validation to prevent submission and render ListingUnavailable when listing is hidden.
Frontend Listing Index & Routing
frontend/app/index.tsx, frontend/app/l/[id].tsx
Filter allListings to exclude hidden items in index view; added new short URL redirect route that maps /l/[id] to /listings/{id}.
Frontend UI Components
frontend/components/HiddenBanner.tsx, frontend/components/ListingUnavailable.tsx
Added HiddenBanner component with support email contact link for appeal; added ListingUnavailable component with "Back to Browse" navigation.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested reviewers

  • SamanSP1386

Poem

🐰 Hop, hop, hooray! A listing's now shy,
With banners and messages soaring high.
Participants counted, messages typed with care,
Share buttons gleaming—let buyers beware!

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Out of Scope Changes check ❓ Inconclusive Changes include schema updates (participantIds, lastMessageId, message type), backend messaging enhancements, frontend hidden listing UI logic, and share functionality. The participantIds and message type fields appear tangential to shareable links but support the broader messaging infrastructure mentioned in issue #42. Verify that backend schema changes (participantIds, lastMessageId, message type) and backfill logic are required for the shareable links feature or clarify if they support separate objectives from issue #42.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Feature/poly 34 shareable links' clearly summarizes the main change: adding shareable links functionality, which aligns with the PR's primary objective of implementing shareable URL generation.
Description check ✅ Passed The PR description follows the template structure with linked issues, summary, testing steps, checklist, and screenshots. However, the summary is very brief and incomplete details are left in the testing section.
Linked Issues check ✅ Passed The changes implement shareable links (short URL redirects via /l/[id] route, share button functionality), visibility control (isHidden logic), and participant tracking for conversations. These align with objectives from issue #42 including authorization, visibility logic, and UI improvements.

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

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/POLY-34-Shareable-Links

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.

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: 6

Caution

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

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

249-250: ⚠️ Potential issue | 🟠 Major

createListing is missing non-negative price validation.

Line 249 accepts any number, while update path already rejects negative values. This allows invalid data on create.

💵 Proposed fix
   handler: async (ctx, args) => {
     const userId = await getAuthUserId(ctx);
     if (userId === null) {
       throw new Error('You must be logged in to create a listing');
     }
+    if (args.price < 0) {
+      throw new Error('Price must be non-negative');
+    }
 
     if (args.tags) {

Also applies to: 257-260

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

In `@backend/convex/listings.ts` around lines 249 - 250, The create path accepts
any number for price; update already rejects negatives, so update the validation
schemas used in createListing (and the analogous schema around lines 257-260) to
enforce non-negative prices by adding a min(0) or equivalent check to the price
field (e.g., change v.number() to v.number().min(0)) so create and update
behavior are consistent; update the validation for the price field in the schema
referenced by createListing and the other schema mentioned so negative prices
are rejected on create and update.
🧹 Nitpick comments (2)
frontend/hooks/useAuth.ts (1)

27-33: Prefer explicit mapping over ...user in normalized output.

This keeps the hook contract tightly scoped to User and avoids carrying unintended fields from backend responses.

♻️ Proposed refactor
-  const normalizedUser: User | null = user
-    ? {
-        ...user,
-        name: user.name ?? null,
-        createdAt: user.createdAt ?? user._creationTime,
-      }
-    : null;
+  const normalizedUser: User | null = user
+    ? {
+        _id: user._id,
+        _creationTime: user._creationTime,
+        email: user.email,
+        name: user.name ?? null,
+        createdAt: user.createdAt ?? user._creationTime,
+      }
+    : null;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/hooks/useAuth.ts` around lines 27 - 33, Replace the spread-based
normalization in useAuth.ts (normalizedUser) with an explicit mapping of only
the User interface fields so unintended backend fields aren’t carried forward;
remove the "...user" spread and return an object that sets each known User
property (e.g., id, email, name, createdAt, roles — whatever is declared on the
User type in your codebase) using user.field ?? default, and keep null/default
fallbacks for missing values (preserve the existing name and createdAt logic but
assign them explicitly).
backend/convex/listings.ts (1)

81-105: Pagination is applied after loading the full dataset.

Line 81 collects all rows, then Lines 101-105 slice in memory. This removes most pagination benefits and will degrade latency/memory as listing count grows.

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

In `@backend/convex/listings.ts` around lines 81 - 105, The code currently calls
query.order('desc').collect() into listings then applies pagination by slicing
the in-memory array (listings.slice...), which wastes memory and CPU; update the
logic to apply pagination to the Convex query before calling collect: use the
pagination inputs (args.paginationOpts.cursor and args.paginationOpts.numItems)
to compute an offset/limit and call the query's server-side pagination methods
(e.g., query.limit(...) and query.skip(...) or the equivalent Convex
cursor-based approach) on the query returned by query.order('desc') so only the
needed rows are collected, and remove the in-memory slice logic that operates on
listings. Ensure you still apply the minPrice/maxPrice/tags filters either on
the query (preferred) or before calling collect so they are included in the
server-side pagination.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@backend/convex/listings.ts`:
- Around line 32-34: The authorization check in verifyOwnership is inverted: it
currently throws when listing.sellerId === userId which blocks owners and allows
non-owners; update verifyOwnership to throw only when listing.sellerId !==
userId so owners can proceed and non-owners are blocked, then ensure
updateListing, updateListingStatus, and deleteListing call verifyOwnership
before performing mutations and rely on the corrected check (referencing
verifyOwnership, listing.sellerId, and userId).

In `@frontend/app/auth/login.tsx`:
- Around line 69-71: The redirect logic allows protocol-relative targets like
"//evil.example"; tighten validation in the block that computes nextRoute: when
evaluating redirectTo (the variable) ensure it's a string that startsWith('/')
but NOT startsWith('//') (e.g., add a check !redirectTo.startsWith('//')), and
only then pass that safe nextRoute to router.replace(nextRoute as Href); keep
the fallback to '/' otherwise. Use the existing symbols redirectTo, nextRoute,
and router.replace to locate and update the code.

In `@frontend/app/l/`[id].tsx:
- Around line 6-10: The redirect uses the raw id in the path which can create
malformed URLs for IDs with special characters; update the component to use an
encoded id by calling encodeURIComponent(id) when constructing the redirect href
for the valid branch (the Redirect that currently uses `/listings/${id}`) so the
dynamic segment is safely URL-encoded; keep the existing validation (typeof id
=== 'string' && id) and only encode the id in the constructed href.

In `@frontend/app/listings/`[id].tsx:
- Around line 39-40: The auth gating currently uses useAuth() but ignores its
loading state, causing the sign-in prompt (triggered in the buy/contact actions
around the listing page handlers) to open while auth is still resolving; update
the code to destructure isLoading from useAuth (e.g., const { user: currentUser,
isLoading } = useAuth()) and change the action checks that currently do if
(!currentUser) { openSignIn(); } to first guard for loading (if (isLoading)
return or disable the action) and only call openSignIn() when !isLoading &&
!currentUser, ensuring signed-in users don't get prompted while auth is
resolving.
- Around line 232-236: The ReportModal is being passed a potentially invalid
route param via String(id); instead pass the canonical resolved ID from the
listing object. Update the ReportModal call to use listing._id (e.g.,
targetId={String(listing._id)} or targetId={listing._id} depending on prop type)
and keep the existing props (isVisible={reportOpen}, onClose={() =>
setReportOpen(false)}, targetType="listing") so the modal always receives the
resolved listing._id rather than the raw route id.

In `@frontend/components/ReportModal.tsx`:
- Around line 45-58: The submit flow in ReportModal.tsx currently simulates
submission (setTimeout) and logs sensitive report data; replace that with a real
Convex mutation call and remove the console.log. In the onSubmit handler (where
resetState(), props.onClose(), and Toast.show are used), call your Convex
mutation (e.g., await mutationClient.mutate('reports.createReport', { targetId:
props.targetId, targetType: props.targetType, reason, notes })) instead of the
Promise, handle errors with try/catch (show an error Toast on failure and do not
reset state or close the modal), and only on success call resetState(),
props.onClose(), and Toast.show for success; ensure no raw notes or sensitive
content are written to console or logs.

---

Outside diff comments:
In `@backend/convex/listings.ts`:
- Around line 249-250: The create path accepts any number for price; update
already rejects negatives, so update the validation schemas used in
createListing (and the analogous schema around lines 257-260) to enforce
non-negative prices by adding a min(0) or equivalent check to the price field
(e.g., change v.number() to v.number().min(0)) so create and update behavior are
consistent; update the validation for the price field in the schema referenced
by createListing and the other schema mentioned so negative prices are rejected
on create and update.

---

Nitpick comments:
In `@backend/convex/listings.ts`:
- Around line 81-105: The code currently calls query.order('desc').collect()
into listings then applies pagination by slicing the in-memory array
(listings.slice...), which wastes memory and CPU; update the logic to apply
pagination to the Convex query before calling collect: use the pagination inputs
(args.paginationOpts.cursor and args.paginationOpts.numItems) to compute an
offset/limit and call the query's server-side pagination methods (e.g.,
query.limit(...) and query.skip(...) or the equivalent Convex cursor-based
approach) on the query returned by query.order('desc') so only the needed rows
are collected, and remove the in-memory slice logic that operates on listings.
Ensure you still apply the minPrice/maxPrice/tags filters either on the query
(preferred) or before calling collect so they are included in the server-side
pagination.

In `@frontend/hooks/useAuth.ts`:
- Around line 27-33: Replace the spread-based normalization in useAuth.ts
(normalizedUser) with an explicit mapping of only the User interface fields so
unintended backend fields aren’t carried forward; remove the "...user" spread
and return an object that sets each known User property (e.g., id, email, name,
createdAt, roles — whatever is declared on the User type in your codebase) using
user.field ?? default, and keep null/default fallbacks for missing values
(preserve the existing name and createdAt logic but assign them explicitly).

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1bb0535 and 13e6d5c.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (11)
  • backend/convex/listings.ts
  • backend/convex/schema.ts
  • frontend/app.json
  • frontend/app/_layout.tsx
  • frontend/app/auth/login.tsx
  • frontend/app/index.tsx
  • frontend/app/l/[id].tsx
  • frontend/app/listings/[id].tsx
  • frontend/components/ReportModal.tsx
  • frontend/hooks/useAuth.ts
  • package.json

Comment thread backend/convex/listings.ts Outdated
Comment thread frontend/app/auth/login.tsx Outdated
Comment thread frontend/app/l/[id].tsx Outdated
Comment thread frontend/app/listings/[id].tsx Outdated
Comment thread frontend/app/listings/[id].tsx Outdated
Comment thread frontend/components/ReportModal.tsx 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: 5

Caution

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

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

112-112: ⚠️ Potential issue | 🟡 Minor

Typo in comment: tagestags.

-// Normalize tages to lowercase and within 1-20 characters exclusive
+// Normalize tags to lowercase and within 1-20 characters exclusive
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/convex/listings.ts` at line 112, Fix the typo in the inline comment
that currently reads "Normalize tages to lowercase and within 1-20 characters
exclusive" by replacing "tages" with "tags" so the comment reads "Normalize tags
to lowercase and within 1-20 characters exclusive"; update the comment near
where tag normalization logic is implemented (look for the comment string
"Normalize tages..." in backend/convex/listings.ts) to ensure clarity.
♻️ Duplicate comments (1)
backend/convex/listings.ts (1)

24-34: verifyOwnership auth migration looks correct.

getAuthUserId(ctx) is used with a null guard, and the ownership condition is properly !==. The previously flagged inverted check is resolved.

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

In `@backend/convex/listings.ts` around lines 24 - 34, No change required: the
auth migration in this block is correct—keep the null guard using
getAuthUserId(ctx) and the ownership check using listing.sellerId !== userId
as-is; if there are older/inverted checks elsewhere, replace them with this
pattern (use getAuthUserId(ctx) with a null guard and compare listing.sellerId
!== userId) to ensure consistency.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@backend/convex/listings.ts`:
- Around line 81-99: The current code calls query.order('desc').collect()
(assigning to listings) then filters in-memory using args.minPrice,
args.maxPrice and args.tags, which pulls the entire table; change the logic so
price bounds are applied at the DB layer before collect: add/ensure a composite
index (e.g., by_status_price) and use query.withIndex('by_status_price') plus
the appropriate range/greaterThanEqual/lessThanEqual calls to push args.minPrice
and args.maxPrice into the query before calling .collect(); keep tag filtering
(args.tags) in-memory after collect if necessary and retain the .order('desc')
behavior. Ensure any new index is declared in the Convex schema to support the
index name used.
- Line 73: The local variable named "query" in backend/convex/listings.ts
shadows the module-level import "query" from './_generated/server'; rename the
local variable (for example to "categoryQuery" or "selectedCategory") in the
handler where it's declared and update all its usages in that scope (the
declaration at the diff and any subsequent references) so the imported "query"
identifier is no longer shadowed and ESLint no-shadow errors are avoided.
- Line 70: getListings currently declares paginationOpts:
v.optional(paginationOptsValidator) but implements manual .collect(),
parseInt(cursor,10) and returns an array slice — breaking Convex's pagination
contract; either (A) convert getListings to use Convex native pagination by
replacing the .collect()/manual slicing with .paginate(paginationOpts) and
return the required PaginationResult{page,isDone,continueCursor} shape (ensure
all filters used are indexable), or (B) remove paginationOptsValidator from the
argument list, mirror searchAndFilterListings by accepting a manual cursor/limit
pattern (opaque string or offset scheme), stop using .paginate() in the
signature, and return the same manual result shape; update getListings, the
cursor parsing logic, and the return type accordingly.

In `@frontend/app/listings/`[id].tsx:
- Around line 207-210: The Seller Information section currently renders PII by
showing listing.sellerEmail directly (see listing.sellerEmail inside the JSX
that uses styles.section/styles.sectionText); remove that direct exposure and
instead render a non-PII fallback UI such as "Contact Seller" button or link
that triggers the existing auth-gated contact flow (e.g., call the contact
modal/navigation handler used elsewhere), and ensure any access to
listing.sellerEmail is only performed server-side or after authentication before
sending to the client.
- Around line 19-34: The copyLinkToClipboard function can throw if
navigator.clipboard.writeText rejects; wrap the await clipboard.writeText(url)
call in a try/catch so any rejection is caught and the function returns false
(preserving the existing boolean contract), and optionally log or ignore the
error; update the function (referenced by name copyLinkToClipboard) so callers
like handleCopyLink always receive false on failure instead of an unhandled
rejection.

---

Outside diff comments:
In `@backend/convex/listings.ts`:
- Line 112: Fix the typo in the inline comment that currently reads "Normalize
tages to lowercase and within 1-20 characters exclusive" by replacing "tages"
with "tags" so the comment reads "Normalize tags to lowercase and within 1-20
characters exclusive"; update the comment near where tag normalization logic is
implemented (look for the comment string "Normalize tages..." in
backend/convex/listings.ts) to ensure clarity.

---

Duplicate comments:
In `@backend/convex/listings.ts`:
- Around line 24-34: No change required: the auth migration in this block is
correct—keep the null guard using getAuthUserId(ctx) and the ownership check
using listing.sellerId !== userId as-is; if there are older/inverted checks
elsewhere, replace them with this pattern (use getAuthUserId(ctx) with a null
guard and compare listing.sellerId !== userId) to ensure consistency.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 13e6d5c and 22df9b0.

📒 Files selected for processing (4)
  • backend/convex/listings.ts
  • frontend/app/auth/login.tsx
  • frontend/app/l/[id].tsx
  • frontend/app/listings/[id].tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • frontend/app/l/[id].tsx

Comment thread backend/convex/listings.ts Outdated
Comment thread backend/convex/listings.ts Outdated
Comment thread backend/convex/listings.ts Outdated
Comment on lines +81 to +99
let listings = await query.order('desc').collect();

if (args.minPrice !== undefined) {
listings = listings.filter((listing) => listing.price >= args.minPrice!);
}

if (args.maxPrice !== undefined) {
listings = listings.filter((listing) => listing.price <= args.maxPrice!);
}

if (args.tags && args.tags.length > 0) {
const normalizedTags = new Set(
args.tags.map((tag) => tag.trim().toLowerCase()).filter((tag) => tag.length > 0)
);

listings = listings.filter((listing) =>
(listing.tags ?? []).some((tag) => normalizedTags.has(tag.toLowerCase()))
);
}
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 | 🟠 Major

Full table collect before in-memory filtering — unbounded memory and latency.

collect() is called before any price or tag filtering, meaning every active listing is pulled into memory on every call. As the listings table grows, this becomes an O(n) read on each query regardless of how selective the filters are.

Consider adding a database-level index for price range (e.g., by_status_price) and using .withIndex to push price bounds down to the database tier. Tag filtering will likely have to remain in-memory given its multi-value nature, but at least the price range can prune the result set early.

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

In `@backend/convex/listings.ts` around lines 81 - 99, The current code calls
query.order('desc').collect() (assigning to listings) then filters in-memory
using args.minPrice, args.maxPrice and args.tags, which pulls the entire table;
change the logic so price bounds are applied at the DB layer before collect:
add/ensure a composite index (e.g., by_status_price) and use
query.withIndex('by_status_price') plus the appropriate
range/greaterThanEqual/lessThanEqual calls to push args.minPrice and
args.maxPrice into the query before calling .collect(); keep tag filtering
(args.tags) in-memory after collect if necessary and retain the .order('desc')
behavior. Ensure any new index is declared in the Convex schema to support the
index name used.

Comment thread frontend/app/listings/[id].tsx Outdated
Comment thread frontend/app/listings/[id].tsx 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: 4

Caution

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

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

258-267: ⚠️ Potential issue | 🟠 Major

Do not trust client-supplied sellerEmail when creating listings.

sellerEmail is accepted from request args and persisted directly. A logged-in user can submit another person’s email.

🔐 Suggested server-side source of truth
 export const createListing = mutation({
   args: {
@@
-    sellerEmail: v.string(),
@@
   handler: async (ctx, args) => {
@@
+    const userDoc = await ctx.db.get(userId as Id<'users'>);
+    if (!userDoc) {
+      throw new Error('Authenticated user not found');
+    }
@@
     const listingId = await ctx.db.insert('listings', {
       ...args,
+      sellerEmail: userDoc.email,
       sellerId: userId,

Also applies to: 294-297

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

In `@backend/convex/listings.ts` around lines 258 - 267, The listing creation
accepts a client-supplied sellerEmail in the args schema — stop trusting that
value and use the server-side authenticated user's email instead: remove/ignore
the args.sellerEmail field in the validator (the args object with title,
description, price, sellerEmail, ...), read the current user's email from the
request/session context (e.g. ctx.auth.email or your session helper) in the
create-listing mutation/handler, ensure it's present and canonicalized, and
persist that value as the seller email; apply the same change to the other
occurrence referenced at lines 294-297 so no code ever uses args.sellerEmail for
persistence or authorization.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@backend/convex/listings.ts`:
- Around line 269-273: createListing currently allows negative prices while
updateListing rejects them; add the same validation in createListing by checking
the incoming price field (e.g., the price on the listing input) and throw an
error if price < 0 before inserting. Locate createListing in listings.ts (same
file as getAuthUserId usage) and mirror the negative-price check used in
updateListing so new listings with negative prices are rejected early.
- Around line 428-449: The submitReport mutation currently only validates
listings but allows 'profile' targets; add validation for args.targetType ===
'profile' in submitReport: fetch the profile via ctx.db.get(args.targetId as
Id<'profiles'>), throw 'Profile not found' if missing, and throw 'Cannot report
your own profile' if the profile belongs to the authenticated user (compare
userId to profile.userId or the appropriate owner field on the profile record).
Ensure this logic sits alongside the existing listing checks in the submitReport
handler and uses getAuthUserId, ctx.db.get, reportTargetTypeValidator and the
same error patterns.

In `@frontend/app/listings/`[id].tsx:
- Around line 12-17: Replace the local-dev fallback for WEB_BASE_URL so shared
links don't default to http://localhost:8081: change the WEB_BASE_URL constant
to use a production-safe default (e.g., 'https://polybuy.com') instead of
localhost, keep the trailing-slash removal (.replace(/\/$/, '')), and let
APP_DOWNLOAD_URL continue to derive from WEB_BASE_URL (APP_DOWNLOAD_URL
constant) so canonical/share links resolve to a public domain when
EXPO_PUBLIC_WEB_BASE_URL is not set; alternatively, make WEB_BASE_URL optional
(empty string) and add guards where WEB_BASE_URL or APP_DOWNLOAD_URL are used in
listings/[id].tsx to avoid emitting localhost links.

In `@frontend/components/ReportModal.tsx`:
- Around line 31-34: Prevent closing the modal while a report submission is in
flight by adding a guard that checks the local submitting state before calling
resetState() and props.onClose(); update the close handlers referenced
(handleClose and the other close triggers around the other noted locations) to
early-return when submitting is true (optionally show a brief "submission in
progress" toast or keep the modal open), ensuring the unique functions/methods
resetState(), props.onClose(), and handleClose are used to locate and update the
logic.

---

Outside diff comments:
In `@backend/convex/listings.ts`:
- Around line 258-267: The listing creation accepts a client-supplied
sellerEmail in the args schema — stop trusting that value and use the
server-side authenticated user's email instead: remove/ignore the
args.sellerEmail field in the validator (the args object with title,
description, price, sellerEmail, ...), read the current user's email from the
request/session context (e.g. ctx.auth.email or your session helper) in the
create-listing mutation/handler, ensure it's present and canonicalized, and
persist that value as the seller email; apply the same change to the other
occurrence referenced at lines 294-297 so no code ever uses args.sellerEmail for
persistence or authorization.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 22df9b0 and 13a085c.

📒 Files selected for processing (4)
  • backend/convex/listings.ts
  • backend/convex/schema.ts
  • frontend/app/listings/[id].tsx
  • frontend/components/ReportModal.tsx

Comment thread backend/convex/listings.ts Outdated
Comment thread backend/convex/listings.ts Outdated
Comment thread frontend/app/listings/[id].tsx Outdated
Comment thread frontend/components/ReportModal.tsx 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: 2

♻️ Duplicate comments (2)
frontend/app/listings/[id].tsx (1)

12-21: ⚠️ Potential issue | 🟠 Major

WEB_BASE_URL can still resolve to localhost in non-web runtime.

Line 15 falls back to http://localhost:8081. On native (no window) with missing env config, shared/canonical links become invalid public URLs.

💡 Suggested hardening (avoid localhost fallback in production paths)
-const fallbackWebBaseUrl =
-  typeof window !== 'undefined' && window.location?.origin
-    ? window.location.origin
-    : 'http://localhost:8081';
-const WEB_BASE_URL = (process.env.EXPO_PUBLIC_WEB_BASE_URL ?? fallbackWebBaseUrl).replace(
-  /\/$/,
-  ''
-);
+const envWebBaseUrl = process.env.EXPO_PUBLIC_WEB_BASE_URL?.replace(/\/$/, '');
+const runtimeWebBaseUrl =
+  typeof window !== 'undefined' && window.location?.origin
+    ? window.location.origin.replace(/\/$/, '')
+    : '';
+const WEB_BASE_URL = envWebBaseUrl ?? runtimeWebBaseUrl;
-  const shareUrl = `${WEB_BASE_URL}/listings/${listing._id}`;
+  const shareUrl = WEB_BASE_URL ? `${WEB_BASE_URL}/listings/${listing._id}` : '';
+    if (!shareUrl) {
+      Toast.show({ type: 'error', text1: 'Share link unavailable' });
+      return;
+    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/app/listings/`[id].tsx around lines 12 - 21, The current
fallbackWebBaseUrl allows WEB_BASE_URL to become "http://localhost:8081" when
window is undefined, causing invalid public links on native builds; update the
logic so that fallbackWebBaseUrl does not default to localhost in non-web
runtimes (e.g., set fallbackWebBaseUrl to undefined or an empty string instead
of 'http://localhost:8081'), keep WEB_BASE_URL derived from
process.env.EXPO_PUBLIC_WEB_BASE_URL or that safe fallback, and ensure
APP_DOWNLOAD_URL uses EXPO_PUBLIC_APP_DOWNLOAD_URL if provided or only
constructs from WEB_BASE_URL when WEB_BASE_URL is truthy (so update references
to fallbackWebBaseUrl, WEB_BASE_URL, and APP_DOWNLOAD_URL accordingly).
backend/convex/listings.ts (1)

444-463: ⚠️ Potential issue | 🔴 Critical

submitReport branch nesting is wrong and breaks valid listing reports.

Lines 451-462 are currently attached to the ownership check, not to targetType. Result: non-owner listing reports hit profile lookup and fail; profile reports skip validation entirely.

🐛 Suggested fix (move profile path to target-type branch)
     if (args.targetType === 'listing') {
       const listing = await ctx.db.get(args.targetId as Id<'listings'>);
       if (!listing) {
         throw new Error('Listing not found');
       }
       if (listing.sellerId === userId) {
         throw new Error('Cannot report your own listing');
-      } else {
-        const profile = await ctx.db
-          .query('profiles')
-          .withIndex('by_userId', (q) => q.eq('userId', args.targetId))
-          .unique();
-        if (!profile) {
-          throw new Error('Profile not found');
-        }
-        if (profile.userId === userId) {
-          throw new Error('Cannot report your own profile');
-        }
       }
+    } else {
+      const profile = await ctx.db
+        .query('profiles')
+        .withIndex('by_userId', (q) => q.eq('userId', args.targetId))
+        .unique();
+      if (!profile) {
+        throw new Error('Profile not found');
+      }
+      if (profile.userId === userId) {
+        throw new Error('Cannot report your own profile');
+      }
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/convex/listings.ts` around lines 444 - 463, The code in submitReport
incorrectly nests the profile lookup and profile ownership check inside the
"listing" branch's else, causing profile reports to skip validation and listing
reports to run profile logic; fix by restructuring the conditional so that if
args.targetType === 'listing' you only fetch the listing and check
listing.sellerId !== userId (throwing 'Listing not found' or 'Cannot report your
own listing' as appropriate), and add a separate else if branch for
args.targetType === 'profile' that performs the profiles query (using the
'by_userId' index and .unique()), verifies the profile exists, and checks
profile.userId !== userId (throwing 'Profile not found' or 'Cannot report your
own profile'). Ensure you update the code around the listing/profile checks in
submitReport to reflect these two distinct branches.
🤖 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/listings/`[id].tsx:
- Around line 23-40: The copyLinkToClipboard function currently only checks
navigator.clipboard and returns false on native (iOS/Android); install and
import a cross-platform clipboard (expo-clipboard), detect platform using
Platform.OS === 'web' and use navigator.clipboard.writeText for web while
calling ExpoClipboard.setStringAsync for native; update copyLinkToClipboard to
branch on Platform.OS and return true/false based on the chosen API, and update
callers handleCopyLink and handleShare fallback to use the new
copyLinkToClipboard so mobile devices no longer trigger the "Clipboard
unavailable" path.

In `@frontend/components/ReportModal.tsx`:
- Around line 73-77: The unauthenticated error message in ReportModal always
says "report a listing" regardless of the component's targetType; update the
setError call inside the ReportModal (where isUnauth is handled) to pick the
message based on the targetType prop (targetType === 'profile' ? 'You must be
logged in to report a profile.' : 'You must be logged in to report a listing.').
Ensure the same targetType-aware wording is applied where setError is used for
unauthenticated cases so the message correctly reflects 'listing' or 'profile'.

---

Duplicate comments:
In `@backend/convex/listings.ts`:
- Around line 444-463: The code in submitReport incorrectly nests the profile
lookup and profile ownership check inside the "listing" branch's else, causing
profile reports to skip validation and listing reports to run profile logic; fix
by restructuring the conditional so that if args.targetType === 'listing' you
only fetch the listing and check listing.sellerId !== userId (throwing 'Listing
not found' or 'Cannot report your own listing' as appropriate), and add a
separate else if branch for args.targetType === 'profile' that performs the
profiles query (using the 'by_userId' index and .unique()), verifies the profile
exists, and checks profile.userId !== userId (throwing 'Profile not found' or
'Cannot report your own profile'). Ensure you update the code around the
listing/profile checks in submitReport to reflect these two distinct branches.

In `@frontend/app/listings/`[id].tsx:
- Around line 12-21: The current fallbackWebBaseUrl allows WEB_BASE_URL to
become "http://localhost:8081" when window is undefined, causing invalid public
links on native builds; update the logic so that fallbackWebBaseUrl does not
default to localhost in non-web runtimes (e.g., set fallbackWebBaseUrl to
undefined or an empty string instead of 'http://localhost:8081'), keep
WEB_BASE_URL derived from process.env.EXPO_PUBLIC_WEB_BASE_URL or that safe
fallback, and ensure APP_DOWNLOAD_URL uses EXPO_PUBLIC_APP_DOWNLOAD_URL if
provided or only constructs from WEB_BASE_URL when WEB_BASE_URL is truthy (so
update references to fallbackWebBaseUrl, WEB_BASE_URL, and APP_DOWNLOAD_URL
accordingly).

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 13a085c and e8246f5.

📒 Files selected for processing (3)
  • backend/convex/listings.ts
  • frontend/app/listings/[id].tsx
  • frontend/components/ReportModal.tsx

Comment thread frontend/app/listings/[id].tsx Outdated
Comment thread frontend/components/ReportModal.tsx Outdated
@jaydonkc jaydonkc force-pushed the feature/POLY-34-Shareable-Links branch from a645e94 to 80e89cc Compare March 2, 2026 11:53
@jaydonkc
Copy link
Copy Markdown
Collaborator

jaydonkc commented Mar 2, 2026

Created clean companion branch jaydon/clean-pr-47.

Feedback:

  • Shareable links should stay frontend-only where possible (short route + share action).
  • Avoid backend/schema rewrites and lockfile churn in this PR scope.
  • Keep canonical link format stable (https://polybuys.com/l/:id).

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.

🧹 Nitpick comments (2)
backend/convex/schema.ts (1)

121-121: Consider using a union type for message type field.

Using v.optional(v.string()) allows any string value. If the message types are well-defined (e.g., 'text', 'image', 'system'), consider using a union type for better type safety and validation:

-    type: v.optional(v.string()),
+    type: v.optional(v.union(v.literal('text'), v.literal('image'), v.literal('system'))),

This prevents invalid type values from being stored and improves developer experience with autocompletion.

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

In `@backend/convex/schema.ts` at line 121, The message schema's type field
currently uses v.optional(v.string()), allowing any string; change it to a
union/enum of the allowed message types (for example replace
v.optional(v.string()) with v.optional(v.union(v.literal('text'),
v.literal('image'), v.literal('system'))) or
v.optional(v.enum(['text','image','system']))) in the schema definition (refer
to the message schema's type field in schema.ts) so only valid types are
accepted and developers get proper autocomplete and validation.
backend/convex/messages.ts (1)

282-307: Backfill mutation may not scale for large datasets.

The current implementation loads all conversations and messages into memory with .collect() and patches them one by one. This could cause:

  • Memory issues with large datasets
  • Transaction timeouts if there are many records to patch
  • Hitting Convex's mutation size limits

For production use, consider implementing pagination or running in batches:

♻️ Example batched approach
 export const backfillMessagingFields = internalMutation({
-  args: {},
-  handler: async (ctx) => {
-    const conversations = await ctx.db.query('conversations').collect();
+  args: {
+    cursor: v.optional(v.string()),
+    batchSize: v.optional(v.number()),
+  },
+  handler: async (ctx, args) => {
+    const limit = args.batchSize ?? 100;
+    const conversationsQuery = ctx.db.query('conversations');
+    const conversations = await (args.cursor
+      ? conversationsQuery.paginate({ cursor: args.cursor, numItems: limit })
+      : conversationsQuery.paginate({ numItems: limit }));
+    
     let conversationPatches = 0;
-
-    for (const convo of conversations) {
+    for (const convo of conversations.page) {
       if (!convo.participantIds || convo.participantIds.length !== 2) {
         await ctx.db.patch(convo._id, { participantIds: [convo.buyerId, convo.sellerId] });
         conversationPatches += 1;
       }
     }
-
-    // ... similar for messages
-
-    return { conversationPatches, messagePatches };
+    return { 
+      conversationPatches,
+      isDone: conversations.isDone,
+      cursor: conversations.continueCursor,
+    };
   },
 });

If this is intended as a one-time migration on a small dataset, the current approach is acceptable.

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

In `@backend/convex/messages.ts` around lines 282 - 307, The
backfillMessagingFields internalMutation currently loads all records via
ctx.db.query(...).collect() and patches each one, which won't scale; change
backfillMessagingFields to iterate records in batches instead of using
collect(): use paginated queries (e.g., loop using query with a limit/offset or
a cursor) to fetch e.g. 100–1000 conversations/messages per page, apply patches
for that page (either sequentially or with a bounded concurrency worker pool),
await completion, then continue to the next page—replace direct collect() calls
and single-item ctx.db.patch loops with this batched fetch-and-patch pattern for
both conversations and messages to avoid memory bloat, long transactions, and
mutation size limits.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@backend/convex/messages.ts`:
- Around line 282-307: The backfillMessagingFields internalMutation currently
loads all records via ctx.db.query(...).collect() and patches each one, which
won't scale; change backfillMessagingFields to iterate records in batches
instead of using collect(): use paginated queries (e.g., loop using query with a
limit/offset or a cursor) to fetch e.g. 100–1000 conversations/messages per
page, apply patches for that page (either sequentially or with a bounded
concurrency worker pool), await completion, then continue to the next
page—replace direct collect() calls and single-item ctx.db.patch loops with this
batched fetch-and-patch pattern for both conversations and messages to avoid
memory bloat, long transactions, and mutation size limits.

In `@backend/convex/schema.ts`:
- Line 121: The message schema's type field currently uses
v.optional(v.string()), allowing any string; change it to a union/enum of the
allowed message types (for example replace v.optional(v.string()) with
v.optional(v.union(v.literal('text'), v.literal('image'), v.literal('system')))
or v.optional(v.enum(['text','image','system']))) in the schema definition
(refer to the message schema's type field in schema.ts) so only valid types are
accepted and developers get proper autocomplete and validation.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e8246f5 and 80e89cc.

📒 Files selected for processing (10)
  • backend/convex/__tests__/messages.test.ts
  • backend/convex/__tests__/testUtils.ts
  • backend/convex/messages.ts
  • backend/convex/schema.ts
  • frontend/app/index.tsx
  • frontend/app/l/[id].tsx
  • frontend/app/listings/[id].tsx
  • frontend/app/listings/[id]/edit.tsx
  • frontend/components/HiddenBanner.tsx
  • frontend/components/ListingUnavailable.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • frontend/app/index.tsx

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.

4 participants