Skip to content

feat: set up deep linking and improve message coverage [POLY-43]#45

Merged
jaydonkc merged 1 commit intodevfrom
feature/POLY-43-deep-linking
Mar 2, 2026
Merged

feat: set up deep linking and improve message coverage [POLY-43]#45
jaydonkc merged 1 commit intodevfrom
feature/POLY-43-deep-linking

Conversation

@cole-hackman
Copy link
Copy Markdown
Collaborator

@cole-hackman cole-hackman commented Feb 21, 2026

Linked Issues

Closes #
Linear: POLY-43

Summary

This PR introduces deep linking / universal links for the PolyBuys app and adds robust backend test coverage to the messages module.

Deep Linking & Web Improvements:

  • Configured the polybuys:// scheme and associated domains for deep linking in app.json.
  • Added placeholder Android Asset Links (assetlinks.json) and Apple App Site Association (apple-app-site-association) files. Team ID, App ID, and SHA256 will need substitution before production deployment.
  • Enhanced the web listings page (app/listings/[id].tsx) with Facebook/iMessage compatible Open Graph tags for rich link previews.
  • Implemented Apple Smart App Banners and a custom "Open in App" banner for mobile web users.
  • Fixed a routing issue that incorrectly navigated to /auth instead of /auth/login.

Backend Test Coverage:

  • Discovered missing branch coverage in backend/convex/messages.ts.
  • Added new tests for sendMessage, getConversationHistory, getOrCreateConversation, markMessagesAsRead, and messagesByConversation to verify unauthenticated access rejection.
  • Added tests asserting errors for non-existent listings and conversations.
  • Added tests verifying the OpenAI moderation flag appropriately throws an error.

How to Test

Steps to verify locally:

  • npm run lint
  • npm run typecheck
  • npm run test --workspace=backend
  • Manual flow:
    1. npm run dev:backend (in terminal A)
    2. npm run dev (in terminal B)
    3. Verify the change: Open the web equivalent on a mobile device to see the app banner, and use npx uri-scheme open "polybuys://listings/<id>" --ios to verify app deep linking.

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

Summary by CodeRabbit

  • New Features

    • Added deep linking support (iOS Universal Links and Android deep links) for direct access to listings
    • Introduced share listing functionality with native sharing options
    • Added sign-in prompts for non-authenticated users accessing listings
  • Improvements

    • Enhanced web experience with SEO metadata and native app launch banner
    • Improved hidden/unavailable listing handling with user-friendly messaging
    • Strengthened messaging system capabilities

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Feb 21, 2026

Warning

Rate limit exceeded

@jaydonkc has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 14 minutes and 55 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📥 Commits

Reviewing files that changed from the base of the PR and between b4cbf82 and 5ecc2e7.

📒 Files selected for processing (7)
  • frontend/app.json
  • frontend/app/index.tsx
  • frontend/app/listings/[id].tsx
  • frontend/app/listings/[id]/edit.tsx
  • frontend/app/messages/[id].tsx
  • frontend/public/.well-known/apple-app-site-association
  • frontend/public/.well-known/assetlinks.json
📝 Walkthrough

Walkthrough

This pull request introduces deep linking support for iOS and Android with corresponding configuration files, extends the messaging system to track message types and conversation participants, implements authentication-aware UI flows on listing detail pages with hidden listing management, adds SEO enhancements for web, and creates new UI components for listing unavailability and hidden listing banners.

Changes

Cohort / File(s) Summary
Deep linking & platform configuration
frontend/app.json, frontend/public/.well-known/apple-app-site-association, frontend/public/.well-known/assetlinks.json
Configures universal links for iOS and Android, adds Expo Router plugin with polybuys.com origin, renames app scheme from "polybuy" to "polybuys", and adds web Metro bundler configuration.
Messaging schema & backend mutations
backend/convex/schema.ts, backend/convex/messages.ts, backend/convex/__tests__/testUtils.ts, backend/convex/__tests__/messages.test.ts
Adds optional type field to messages and participantIds/lastMessageId fields to conversations; introduces backfillMessagingFields mutation to normalize missing fields; updates sendMessage and internalSendMessage to accept and store message type; adds test fixtures with new fields.
Listing detail page enhancements
frontend/app/listings/[id].tsx
Implements authentication-aware data fetching, shows sign-in prompt when unauthenticated, adds conditional messaging UI, introduces web-specific SEO metadata and deep-link banner, implements listing sharing, and handles hidden/unavailable listings with conditional rendering.
Listing edit page guards
frontend/app/listings/[id]/edit.tsx
Adds listingId validation guard, prevents editing hidden listings, uses ListingUnavailable fallback for invalid listings, and refines loading/not-found state handling.
New UI components
frontend/components/ListingUnavailable.tsx, frontend/components/HiddenBanner.tsx
Introduces ListingUnavailable component with "Back to Browse" navigation and HiddenBanner component displaying hidden listing reason with support contact link.
Listing index filtering
frontend/app/index.tsx
Filters out hidden listings from the main listing display with a defensive guard comment.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Possibly related PRs

  • PR #26: Extends the same messaging feature by modifying backend schema and message handling logic with type and participantIds fields, directly continuing messaging infrastructure work.
  • PR #39: Modifies messaging codepaths and conversation creation utilities with participantIds and conversation field changes aligned with this PR's messaging enhancements.
  • PR #40: Updates backend messaging mutations (internalSendMessage, sendMessage) and related actions, overlapping with this PR's message type handling additions.

Poem

🐰 Deep links now guide the way so clear,
Hidden listings disappear,
Messages wear their type with pride,
Auth gates keep us verified,
Hopping forward, linked and bright! 🔗

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 28.57% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the two main changes: deep linking setup and improved message test coverage, matching the changeset's objectives.
Description check ✅ Passed The description includes all required template sections: Linked Issues, Summary, How to Test, and Checklist. All major changes are explained with sufficient detail.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/POLY-43-deep-linking

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

🧹 Nitpick comments (3)
frontend/app.json (1)

25-25: Android pathPrefix is overly permissive — consider adding trailing slash

pathPrefix: "/listings" will match any path beginning with that string, including hypothetical /listingsold, /listings-archive, etc. Using "/listings/" (with trailing slash) or switching to pathPattern with a regex ensures only real listing detail paths are intercepted.

🔧 Proposed fix
-             "pathPrefix": "/listings"
+             "pathPrefix": "/listings/"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/app.json` at line 25, The current pathPrefix value "pathPrefix":
"/listings" is too permissive; update the app routing configuration to use a
trailing slash or a precise pattern so only real listing detail paths match —
either change the pathPrefix value to "/listings/" or replace pathPrefix with a
pathPattern using a regex like ^/listings/ to strictly match listing routes;
locate the pathPrefix entry in frontend/app.json and make one of these changes
and ensure any consumers expecting the old prefix are updated accordingly.
backend/convex/__tests__/messages.test.ts (2)

81-93: Repeated insert-then-delete pattern — extract to a testUtils helper

The pattern of inserting a conversations record and immediately deleting it to obtain a valid-format but non-existent ID appears four times (lines 81–93, 254–266, 633–645, 820–832). A helper in testUtils.ts would remove the duplication:

// In testUtils.ts
export async function createDeletedConversationId(
  t: any,
  listingId: Id<'listings'>,
  buyerId: Id<'users'>,
  sellerId: Id<'users'>
): Promise<Id<'conversations'>> {
  return await t.run(async (ctx: any) => {
    const id = await ctx.db.insert('conversations', {
      listingId, buyerId, sellerId,
      createdAt: Date.now(), updatedAt: Date.now(),
      buyerLastReadAt: 0, sellerLastReadAt: 0,
    });
    await ctx.db.delete(id);
    return id;
  });
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/convex/__tests__/messages.test.ts` around lines 81 - 93, Extract the
repeated "insert then delete" pattern into a helper function named
createDeletedConversationId in testUtils.ts: implement
createDeletedConversationId(t, listingId, buyerId, sellerId) to call t.run, use
ctx.db.insert to create the conversation with the same fields (createdAt,
updatedAt, buyerLastReadAt, sellerLastReadAt) then ctx.db.delete the inserted id
and return the id; replace the four inline blocks that use t.run/ctx.db.insert
followed by ctx.db.delete (the ones producing a conversationId) with calls to
this new createDeletedConversationId helper to remove duplication.

103-126: process.env.OPENAI_API_KEY cleanup should be in afterEach or a try/finally

delete process.env.OPENAI_API_KEY at line 125 only runs if the assertion above resolves normally. If the test framework itself throws (or the mock setup fails), the env var leaks into subsequent tests, potentially affecting their moderation-related behavior.

🔧 Proposed fix using try/finally
     global.fetch = jest.fn().mockResolvedValue({ ... }) as any;
-    process.env.OPENAI_API_KEY = 'mock_key';
 
+    const originalKey = process.env.OPENAI_API_KEY;
+    process.env.OPENAI_API_KEY = 'mock_key';
+    try {
       await expect(async () => {
         await asBuyer.action(api.messages.sendMessage, {
           conversationId,
           body: 'bad message',
         });
       }).rejects.toThrow('inappropriate content');
-    delete process.env.OPENAI_API_KEY;
+    } finally {
+      if (originalKey === undefined) {
+        delete process.env.OPENAI_API_KEY;
+      } else {
+        process.env.OPENAI_API_KEY = originalKey;
+      }
+    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/convex/__tests__/messages.test.ts` around lines 103 - 126, The test
"throws error when message is flagged by moderation" sets
process.env.OPENAI_API_KEY but deletes it only after the assertion, which can
leak the env var if the test fails; fix by ensuring cleanup always runs — either
move deletion of process.env.OPENAI_API_KEY into a file-level afterEach/afterAll
or wrap the test body in a try/finally so process.env.OPENAI_API_KEY is deleted
in the finally block; update the test that calls api.messages.sendMessage and
any setup that assigns process.env.OPENAI_API_KEY to guarantee the env var is
cleared regardless of test outcome.
🤖 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.json`:
- Around line 40-43: Enable typed routes by generating the Expo ambient types on
CI: add a CI setup step (after installing dependencies and before running type
checks) that runs "npx expo customize tsconfig.json" so expo-env.d.ts is created
and TypeScript can resolve route strings; this will allow you to remove the "as
any" workarounds currently used in frontend/app/index.tsx and
frontend/app/listings/new.tsx when experiments.typedRoutes is true.

In `@frontend/app/index.tsx`:
- Line 171: The router.push call using a loose cast currently uses "as any" (see
the object with text 'Sign In' and onPress: () => router.push('/auth/login' as
any)); replace that cast with the correct Href type from expo-router (import
Href from 'expo-router' or use the named type) so it becomes
router.push('/auth/login' as Href), or remove the cast entirely once typedRoutes
are available; update the import to include the Href type and ensure the onPress
arrow uses that typed cast instead of "as any".

In `@frontend/app/listings/`[id].tsx:
- Line 51: The meta tag currently uses the placeholder string
"app-id=YOUR_APP_ID" which prevents Safari from showing the Smart App Banner;
update the meta element in listings/[id].tsx (the <meta name="apple-itunes-app"
...> tag) to use a real numeric App Store ID pulled from configuration (e.g.,
process.env.NEXT_PUBLIC_APPSTORE_ID or a runtime config) and only render the
meta tag when that env var/value is present and numeric so the banner will
appear after the app is published.
- Around line 1-4: Consolidate duplicate imports from 'react-native' by merging
the separate Platform import into the existing import that currently brings in
View, Text, StyleSheet, ScrollView, TouchableOpacity—update the single import
statement (the one that references View, Text, StyleSheet, ScrollView,
TouchableOpacity) to also import Platform and remove the separate Platform
import to avoid duplicate module imports.

In `@frontend/public/.well-known/apple-app-site-association`:
- Line 6: The apple-app-site-association's "appID" value is wrong and will break
Universal Links; update the "appID" to use the exact bundle identifier from
app.json (i.e., use "com.polybuy.app" to match the "bundleIdentifier" field) and
replace the "TEAMID" placeholder with your real 10-character Apple Team ID so
the appID becomes "TEAMIDcom.polybuy.app" (with the actual TEAMID), ensuring the
appID and bundleIdentifier match exactly.

In `@frontend/public/.well-known/assetlinks.json`:
- Line 7: The assetlinks.json currently contains a placeholder
"sha256_cert_fingerprints" value which will break Android App Links
verification; replace "YOUR_SHA256_CERT_FINGERPRINT" in the
"sha256_cert_fingerprints" array with the actual SHA-256 certificate fingerprint
of your release signing certificate (obtain via keytool -list -v -keystore
your-release.keystore -alias your-key-alias or from Play Console → Setup → App
integrity) and ensure the value is the exact fingerprint string used to sign the
production APK/AAB.

---

Nitpick comments:
In `@backend/convex/__tests__/messages.test.ts`:
- Around line 81-93: Extract the repeated "insert then delete" pattern into a
helper function named createDeletedConversationId in testUtils.ts: implement
createDeletedConversationId(t, listingId, buyerId, sellerId) to call t.run, use
ctx.db.insert to create the conversation with the same fields (createdAt,
updatedAt, buyerLastReadAt, sellerLastReadAt) then ctx.db.delete the inserted id
and return the id; replace the four inline blocks that use t.run/ctx.db.insert
followed by ctx.db.delete (the ones producing a conversationId) with calls to
this new createDeletedConversationId helper to remove duplication.
- Around line 103-126: The test "throws error when message is flagged by
moderation" sets process.env.OPENAI_API_KEY but deletes it only after the
assertion, which can leak the env var if the test fails; fix by ensuring cleanup
always runs — either move deletion of process.env.OPENAI_API_KEY into a
file-level afterEach/afterAll or wrap the test body in a try/finally so
process.env.OPENAI_API_KEY is deleted in the finally block; update the test that
calls api.messages.sendMessage and any setup that assigns
process.env.OPENAI_API_KEY to guarantee the env var is cleared regardless of
test outcome.

In `@frontend/app.json`:
- Line 25: The current pathPrefix value "pathPrefix": "/listings" is too
permissive; update the app routing configuration to use a trailing slash or a
precise pattern so only real listing detail paths match — either change the
pathPrefix value to "/listings/" or replace pathPrefix with a pathPattern using
a regex like ^/listings/ to strictly match listing routes; locate the pathPrefix
entry in frontend/app.json and make one of these changes and ensure any
consumers expecting the old prefix are updated accordingly.

Comment thread frontend/app.json
Comment on lines +40 to +43
"scheme": "polybuys",
"experiments": {
"typedRoutes": true
},
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

🧩 Analysis chain

🏁 Script executed:

# Check the frontend/app.json file at the specified lines
cat -n frontend/app.json | sed -n '35,50p'

Repository: codebox-calpoly/PolyBuys

Length of output: 373


🏁 Script executed:

# Search for as any workarounds in the codebase
rg "as any" --type ts --type tsx --type js --type jsx -C 2

Repository: codebox-calpoly/PolyBuys

Length of output: 95


🏁 Script executed:

# Check if expo-env.d.ts exists
find . -name "expo-env.d.ts" -o -name ".gitignore" | head -20

Repository: codebox-calpoly/PolyBuys

Length of output: 123


🏁 Script executed:

# Check if expo-env.d.ts is mentioned in .gitignore
cat .gitignore 2>/dev/null | grep -i expo

Repository: codebox-calpoly/PolyBuys

Length of output: 95


🏁 Script executed:

# Look for typedRoutes configuration in the project
rg "typedRoutes" --type json --type ts --type tsx --type js --type jsx

Repository: codebox-calpoly/PolyBuys

Length of output: 95


🏁 Script executed:

# Search for as any workarounds in TypeScript/JavaScript files
rg "as any" --type ts --type js -C 2

Repository: codebox-calpoly/PolyBuys

Length of output: 17175


🏁 Script executed:

# Check expo-env.d.ts existence in the entire repo
find . -type f -name "*expo-env*" -o -type f -name "*.d.ts" | grep -E "(expo|env)" | head -20

Repository: codebox-calpoly/PolyBuys

Length of output: 50


🏁 Script executed:

# Check frontend directory structure for TypeScript definitions
ls -la frontend/ | head -20

Repository: codebox-calpoly/PolyBuys

Length of output: 826


🏁 Script executed:

# Look at the package.json to understand the project setup
cat frontend/package.json | grep -A 5 -B 5 "expo"

Repository: codebox-calpoly/PolyBuys

Length of output: 1145


🏁 Script executed:

# Search for tsconfig.json in frontend
find frontend -name "tsconfig.json" -exec cat {} \;

Repository: codebox-calpoly/PolyBuys

Length of output: 631


🏁 Script executed:

# Check the specific route usages with as any
cat -n frontend/app/index.tsx | sed -n '1,100p' | grep -A 2 -B 2 "as any"

Repository: codebox-calpoly/PolyBuys

Length of output: 50


🏁 Script executed:

# Check the other route usage
cat -n frontend/app/listings/new.tsx | grep -A 2 -B 2 "as any"

Repository: codebox-calpoly/PolyBuys

Length of output: 235


🏁 Script executed:

# Check if expo-env.d.ts was generated or should be in repo
git status --ignored | grep -i expo

Repository: codebox-calpoly/PolyBuys

Length of output: 50


🏁 Script executed:

# Verify the actual issue: check if TypeScript would error without as any workaround
cat frontend/tsconfig.json

Repository: codebox-calpoly/PolyBuys

Length of output: 631


🌐 Web query:

Expo typedRoutes expo-env.d.ts CI generation setup

💡 Result:

To make experiments.typedRoutes work in CI (so expo-env.d.ts and the route types exist during tsc/lint), do this:

  1. Enable typed routes
  • In app.json / app.config.*:
    {
      "expo": {
        "experiments": { "typedRoutes": true }
      }
    }
    Expo Router typed routes are generated by Expo CLI when enabled. [1]
  1. Ensure your repo already has a tsconfig.json
  • Generate/normalize it once locally (and commit the tsconfig.json changes):
    npx expo customize tsconfig.json
    This command is also what Expo documents for type generation. [2]
  1. Run type generation in CI before typechecking
  • Add a CI step (after install, before tsc/eslint):
    npx expo customize tsconfig.json
    Expo explicitly recommends this when you need typed-route types generated on CI without starting the dev server. [1]
  1. Do NOT commit expo-env.d.ts
  • When typed routes are enabled, Expo CLI generates a git-ignored expo-env.d.ts, updates .gitignore, and updates tsconfig.json include to include expo-env.d.ts and .expo. It should stay uncommitted. [1]

Example GitHub Actions job steps (core part):

- run: npm ci
- run: npx expo customize tsconfig.json
- run: npm run tsc

Sources: Expo Router Typed Routes docs [1]; Expo TypeScript guide (type generation via expo customize) [2].


Remove as any workarounds by generating typed routes on CI

With experiments.typedRoutes: true enabled, Expo needs to generate expo-env.d.ts before TypeScript can resolve route strings. Without this file on CI, valid routes appear as type errors, forcing the as any escape hatches at frontend/app/index.tsx and frontend/app/listings/new.tsx. Add npx expo customize tsconfig.json as a CI setup step (after dependencies install, before type checking) to generate the required types without starting the dev server.

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

In `@frontend/app.json` around lines 40 - 43, Enable typed routes by generating
the Expo ambient types on CI: add a CI setup step (after installing dependencies
and before running type checks) that runs "npx expo customize tsconfig.json" so
expo-env.d.ts is created and TypeScript can resolve route strings; this will
allow you to remove the "as any" workarounds currently used in
frontend/app/index.tsx and frontend/app/listings/new.tsx when
experiments.typedRoutes is true.

Comment thread frontend/app/index.tsx Outdated
Alert.alert('Sign In Required', 'Please sign in to create a listing', [
{ text: 'Cancel', style: 'cancel' },
{ text: 'Sign In', onPress: () => router.push('/auth/login') },
{ text: 'Sign In', onPress: () => router.push('/auth/login' as any) },
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 | 🔴 Critical

Same CI-failing as any cast — apply the same as Href fix

Identical issue to frontend/app/listings/new.tsx line 45. Replace as any with as Href from expo-router, or drop the cast once typedRoutes types are properly generated and included.

🔧 Proposed fix
- import { useLocalSearchParams, useRouter } from 'expo-router';
+ import { useLocalSearchParams, useRouter, type Href } from 'expo-router';
  ...
-       { text: 'Sign In', onPress: () => router.push('/auth/login' as any) },
+       { text: 'Sign In', onPress: () => router.push('/auth/login' as Href) },
🧰 Tools
🪛 GitHub Actions: CI

[error] 171-171: ESLint: Unexpected any. Specify a different type. (no-explicit-any)

🪛 GitHub Check: Lint & Type Check

[warning] 171-171:
Unexpected any. Specify a different type

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

In `@frontend/app/index.tsx` at line 171, The router.push call using a loose cast
currently uses "as any" (see the object with text 'Sign In' and onPress: () =>
router.push('/auth/login' as any)); replace that cast with the correct Href type
from expo-router (import Href from 'expo-router' or use the named type) so it
becomes router.push('/auth/login' as Href), or remove the cast entirely once
typedRoutes are available; update the import to include the Href type and ensure
the onPress arrow uses that typed cast instead of "as any".

Comment thread frontend/app/listings/[id].tsx Outdated
Comment thread frontend/app/listings/[id].tsx Outdated
content={`$${listing.price} - ${listing.description.substring(0, 100)}${listing.description.length > 100 ? '...' : ''}`}
/>
<meta property="og:url" content={`https://polybuys.com/listings/${listing._id}`} />
<meta name="apple-itunes-app" content="app-id=YOUR_APP_ID" />
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

apple-itunes-app placeholder won't show the Smart App Banner

app-id=YOUR_APP_ID is a placeholder. Safari only renders the Smart App Banner when a valid numeric App Store ID is present. This should be tracked and substituted once the app is published.

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

In `@frontend/app/listings/`[id].tsx at line 51, The meta tag currently uses the
placeholder string "app-id=YOUR_APP_ID" which prevents Safari from showing the
Smart App Banner; update the meta element in listings/[id].tsx (the <meta
name="apple-itunes-app" ...> tag) to use a real numeric App Store ID pulled from
configuration (e.g., process.env.NEXT_PUBLIC_APPSTORE_ID or a runtime config)
and only render the meta tag when that env var/value is present and numeric so
the banner will appear after the app is published.

Comment thread frontend/public/.well-known/apple-app-site-association Outdated
"target": {
"namespace": "android_app",
"package_name": "com.polybuy.app",
"sha256_cert_fingerprints": ["YOUR_SHA256_CERT_FINGERPRINT"]
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

Placeholder SHA256 fingerprint must be replaced before production

Android will fetch this file during App Links verification and compare the sha256_cert_fingerprints against the installed APK's signing certificate. A literal placeholder string will cause verification to fail and Android App Links will not activate.

Obtain the fingerprint from your release keystore:

keytool -list -v -keystore your-release.keystore -alias your-key-alias

or from the Play Console under Setup → App integrity → App signing.

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

In `@frontend/public/.well-known/assetlinks.json` at line 7, The assetlinks.json
currently contains a placeholder "sha256_cert_fingerprints" value which will
break Android App Links verification; replace "YOUR_SHA256_CERT_FINGERPRINT" in
the "sha256_cert_fingerprints" array with the actual SHA-256 certificate
fingerprint of your release signing certificate (obtain via keytool -list -v
-keystore your-release.keystore -alias your-key-alias or from Play Console →
Setup → App integrity) and ensure the value is the exact fingerprint string used
to sign the production APK/AAB.

@jaydonkc
Copy link
Copy Markdown
Collaborator

Linting error

@jaydonkc jaydonkc force-pushed the feature/POLY-43-deep-linking branch from c9e07ed to 429d223 Compare March 2, 2026 11:39
@jaydonkc
Copy link
Copy Markdown
Collaborator

jaydonkc commented Mar 2, 2026

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

Feedback:

  • Deep-link setup is useful (app.json + well-known files), but ensure listing detail updates do not regress hidden-content handling.
  • Keep placeholders (e.g. app-id) out of final merge.
  • Scope should remain deep linking + message coverage only.

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

♻️ Duplicate comments (2)
frontend/public/.well-known/assetlinks.json (1)

7-7: ⚠️ Potential issue | 🟠 Major

Replace the SHA-256 placeholder with the release signing fingerprint.

Android App Links auto-verification will fail while this placeholder remains.

🔧 Proposed fix
-      "sha256_cert_fingerprints": ["YOUR_SHA256_CERT_FINGERPRINT"]
+      "sha256_cert_fingerprints": ["<RELEASE_CERT_SHA256_FINGERPRINT>"]
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/public/.well-known/assetlinks.json` at line 7, Replace the
placeholder value in the assetlinks JSON entry "sha256_cert_fingerprints" (the
array currently containing "YOUR_SHA256_CERT_FINGERPRINT") with your release app
signing SHA-256 fingerprint string from the release keystore/Play App Signing so
Android App Links can auto-verify; keep it as a quoted string inside the array
and ensure it exactly matches the release signing fingerprint format.
frontend/public/.well-known/apple-app-site-association (1)

6-6: ⚠️ Potential issue | 🟠 Major

Replace the TEAMID placeholder before merge.

Universal Links won’t verify with a literal placeholder in appID; this must be the real 10-character Apple Team ID.

🔧 Proposed fix
-        "appID": "TEAMID.com.polybuy.app",
+        "appID": "<APPLE_TEAM_ID>.com.polybuy.app",
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/public/.well-known/apple-app-site-association` at line 6, The appID
value currently uses the literal placeholder "TEAMID.com.polybuy.app" in the
apple-app-site-association file; replace "TEAMID" with your actual 10-character
Apple Team ID so the appID becomes "<Your10CharTeamID>.com.polybuy.app" (ensure
the appID string under the "appID" key is updated accordingly).
🧹 Nitpick comments (4)
backend/convex/messages.ts (2)

56-67: Validate type before persisting.

Current flow accepts any non-null string and stores it. Add allow-list validation (or normalization) before calling internalSendMessage to keep message contract stable.

Suggested runtime guard
+const ALLOWED_MESSAGE_TYPES = new Set(['text']);
 ...
-    const type = args.type ?? 'text';
+    const type = args.type ?? 'text';
+    if (!ALLOWED_MESSAGE_TYPES.has(type)) {
+      throw new ConvexError('Invalid message type');
+    }

Also applies to: 110-110

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

In `@backend/convex/messages.ts` around lines 56 - 67, The handler currently
accepts any args.type string and passes it through; add an allow-list
check/normalization for message types before calling internalSendMessage (e.g.,
validate args.type against permitted types like 'text', 'image', etc., or
default to 'text'), and throw a ConvexError for invalid types; update the
handler (referencing handler, args.type, and internalSendMessage) to perform
this runtime guard so only allowed/normalized types are persisted.

224-228: Use full composite index equality instead of post-filter.

Since by_listing_buyer_seller now includes sellerId, include it in .withIndex(...) and drop the .filter(...) to avoid extra scan work.

Suggested query update
     const existing = await ctx.db
       .query('conversations')
       .withIndex('by_listing_buyer_seller', (q) =>
-        q.eq('listingId', args.listingId).eq('buyerId', buyerId)
+        q.eq('listingId', args.listingId).eq('buyerId', buyerId).eq('sellerId', sellerId)
       )
-      .filter((q) => q.eq(q.field('sellerId'), sellerId))
       .first();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/convex/messages.ts` around lines 224 - 228, The query currently uses
.withIndex('by_listing_buyer_seller', (q) => q.eq('listingId',
args.listingId).eq('buyerId', buyerId)) and then a .filter(...) on sellerId
which forces a post-filter scan; instead include sellerId in the composite index
equality so the index is fully used: change the .withIndex call to also
.eq('sellerId', sellerId) and remove the .filter(...) so .first() operates on
the indexed equality across listingId, buyerId, and sellerId.
backend/convex/schema.ts (1)

121-121: Constrain messages.type at the schema layer.

v.optional(v.string()) permits arbitrary values. If the app only supports specific message types, encode that as a union to prevent invalid data drift.

Suggested schema tightening
-    type: v.optional(v.string()),
+    type: v.optional(v.union(v.literal('text'))),
🤖 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 schema currently allows arbitrary
message.type values via v.optional(v.string()); tighten this by replacing that
with an explicit union/enum of the supported message type literals (e.g.
v.optional(v.union([v.literal('text'), v.literal('image'),
v.literal('system')])) or equivalent in your validator) so only known types are
accepted; update the messages.type schema entry and any related type usages to
include all supported message-type strings.
frontend/app/listings/[id].tsx (1)

154-156: Use typed object-form navigation instead of as never cast for query parameters.

At line 155, the as never cast suppresses route typing. The codebase already uses the recommended pattern elsewhere in this file. Use object-form navigation with params:

🔧 Suggested refactor
-              router.push(`/auth/login?returnTo=${encodeURIComponent(redirectTo)}` as never);
+              router.push({
+                pathname: '/auth/login',
+                params: { returnTo: redirectTo },
+              });

This approach is consistent with expo-router v6 best practices and the existing pattern in navigateToFeedWithTag (earlier in this file).

🤖 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 154 - 156, Replace the current
router.push call that uses a string and an `as never` cast with the object-form
navigation to preserve typings: build the redirectTo value as before (const
redirectTo = `/listings/${listing._id}`) and call router.push({ pathname:
'/auth/login', params: { returnTo: redirectTo } }) (referencing router.push,
redirectTo and listing._id) so query parameters are typed instead of suppressed
by `as never`.
🤖 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/messages.ts`:
- Around line 282-306: The backfillMutation backfillMessagingFields currently
does unbounded full-table reads via ctx.db.query('conversations').collect() and
ctx.db.query('messages').collect(); change the handler to paginate/batch the
reads and patches instead of collecting everything into memory — use a loop that
queries with a reasonable page size (e.g., limit) and a cursor/offset (or
startAfter using the last item's _id) to fetch chunks, apply ctx.db.patch for
each item in the chunk (keeping the same logic for setting participantIds and
type), and continue until no more rows; ensure you update the
conversationPatches and messagePatches counters and avoid long-running single
queries in backfillMessagingFields by processing and committing each batch
before fetching the next.

In `@frontend/app/index.tsx`:
- Around line 115-116: The defensive filter on allListings into listings can
cause an empty visible page when all items on the current page are hidden;
update the rendering logic in frontend/app/index.tsx to detect when
listings.length === 0 but there are more pages and auto-advance/fetch the next
page instead of showing the empty state. Specifically, after computing listings
from allListings (filtering by isHidden), check the pagination signals (e.g.,
hasNextPage, pageIndex or totalPages) and call the existing next-page loader
(e.g., fetchNextPage / loadMore / incrementPage) until either listings is
non-empty or no more pages remain, then render the empty state only when no
visible items and no further pages.

In `@frontend/app/listings/`[id].tsx:
- Around line 163-165: The Message Seller button has an empty onPress; add a
handler (e.g., handleMessageSeller) and wire it to TouchableOpacity's onPress so
tapping starts or navigates to a conversation. Implement handleMessageSeller to:
1) check auth (currentUser or isAuthenticated) and redirect to login if
unauthenticated; 2) call the conversation starter (e.g., startConversation or
createConversation) with the listing id and seller id (use listing.id and
listing.sellerId or listing.ownerId); 3) navigate to the chat screen (e.g.,
navigation.navigate('Chat' or 'Conversation', {conversationId})) or open the
chat modal on success; replace the empty onPress={() => {}} with
onPress={handleMessageSeller} and handle errors with a user-friendly alert.

---

Duplicate comments:
In `@frontend/public/.well-known/apple-app-site-association`:
- Line 6: The appID value currently uses the literal placeholder
"TEAMID.com.polybuy.app" in the apple-app-site-association file; replace
"TEAMID" with your actual 10-character Apple Team ID so the appID becomes
"<Your10CharTeamID>.com.polybuy.app" (ensure the appID string under the "appID"
key is updated accordingly).

In `@frontend/public/.well-known/assetlinks.json`:
- Line 7: Replace the placeholder value in the assetlinks JSON entry
"sha256_cert_fingerprints" (the array currently containing
"YOUR_SHA256_CERT_FINGERPRINT") with your release app signing SHA-256
fingerprint string from the release keystore/Play App Signing so Android App
Links can auto-verify; keep it as a quoted string inside the array and ensure it
exactly matches the release signing fingerprint format.

---

Nitpick comments:
In `@backend/convex/messages.ts`:
- Around line 56-67: The handler currently accepts any args.type string and
passes it through; add an allow-list check/normalization for message types
before calling internalSendMessage (e.g., validate args.type against permitted
types like 'text', 'image', etc., or default to 'text'), and throw a ConvexError
for invalid types; update the handler (referencing handler, args.type, and
internalSendMessage) to perform this runtime guard so only allowed/normalized
types are persisted.
- Around line 224-228: The query currently uses
.withIndex('by_listing_buyer_seller', (q) => q.eq('listingId',
args.listingId).eq('buyerId', buyerId)) and then a .filter(...) on sellerId
which forces a post-filter scan; instead include sellerId in the composite index
equality so the index is fully used: change the .withIndex call to also
.eq('sellerId', sellerId) and remove the .filter(...) so .first() operates on
the indexed equality across listingId, buyerId, and sellerId.

In `@backend/convex/schema.ts`:
- Line 121: The schema currently allows arbitrary message.type values via
v.optional(v.string()); tighten this by replacing that with an explicit
union/enum of the supported message type literals (e.g.
v.optional(v.union([v.literal('text'), v.literal('image'),
v.literal('system')])) or equivalent in your validator) so only known types are
accepted; update the messages.type schema entry and any related type usages to
include all supported message-type strings.

In `@frontend/app/listings/`[id].tsx:
- Around line 154-156: Replace the current router.push call that uses a string
and an `as never` cast with the object-form navigation to preserve typings:
build the redirectTo value as before (const redirectTo =
`/listings/${listing._id}`) and call router.push({ pathname: '/auth/login',
params: { returnTo: redirectTo } }) (referencing router.push, redirectTo and
listing._id) so query parameters are typed instead of suppressed by `as never`.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c9e07ed and b4cbf82.

📒 Files selected for processing (12)
  • backend/convex/__tests__/messages.test.ts
  • backend/convex/__tests__/testUtils.ts
  • backend/convex/messages.ts
  • backend/convex/schema.ts
  • frontend/app.json
  • frontend/app/index.tsx
  • frontend/app/listings/[id].tsx
  • frontend/app/listings/[id]/edit.tsx
  • frontend/components/HiddenBanner.tsx
  • frontend/components/ListingUnavailable.tsx
  • frontend/public/.well-known/apple-app-site-association
  • frontend/public/.well-known/assetlinks.json

Comment on lines +282 to +306
export const backfillMessagingFields = internalMutation({
args: {},
handler: async (ctx) => {
const conversations = await ctx.db.query('conversations').collect();
let conversationPatches = 0;

for (const convo of conversations) {
if (!convo.participantIds || convo.participantIds.length !== 2) {
await ctx.db.patch(convo._id, { participantIds: [convo.buyerId, convo.sellerId] });
conversationPatches += 1;
}
}

const messages = await ctx.db.query('messages').collect();
let messagePatches = 0;

for (const message of messages) {
if (!message.type) {
await ctx.db.patch(message._id, { type: 'text' });
messagePatches += 1;
}
}

return { conversationPatches, messagePatches };
},
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

Backfill mutation does unbounded full-table reads in a single run.

collect() across entire conversations and messages can fail or timeout as data grows. This should be batched/paginated to stay operationally safe.

🤖 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 - 306, The backfillMutation
backfillMessagingFields currently does unbounded full-table reads via
ctx.db.query('conversations').collect() and ctx.db.query('messages').collect();
change the handler to paginate/batch the reads and patches instead of collecting
everything into memory — use a loop that queries with a reasonable page size
(e.g., limit) and a cursor/offset (or startAfter using the last item's _id) to
fetch chunks, apply ctx.db.patch for each item in the chunk (keeping the same
logic for setting participantIds and type), and continue until no more rows;
ensure you update the conversationPatches and messagePatches counters and avoid
long-running single queries in backfillMessagingFields by processing and
committing each batch before fetching the next.

Comment thread frontend/app/index.tsx
Comment thread frontend/app/listings/[id].tsx Outdated
@jaydonkc jaydonkc force-pushed the feature/POLY-43-deep-linking branch from 0f395bd to f066dc8 Compare March 2, 2026 22:35
@jaydonkc jaydonkc force-pushed the feature/POLY-43-deep-linking branch from f066dc8 to 5ecc2e7 Compare March 2, 2026 22:36
@jaydonkc jaydonkc merged commit 792754d into dev Mar 2, 2026
5 checks passed
@coderabbitai coderabbitai Bot mentioned this pull request Apr 16, 2026
5 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants