Skip to content

feat: gender preference toggle, unified Gifts tab, Maxi bundles, browse similar, swipe timing - #61

Merged
Tar-ive merged 3 commits into
mainfrom
devin/1782655818-challenge-enhancements
Jun 28, 2026
Merged

feat: gender preference toggle, unified Gifts tab, Maxi bundles, browse similar, swipe timing#61
Tar-ive merged 3 commits into
mainfrom
devin/1782655818-challenge-enhancements

Conversation

@Tar-ive

@Tar-ive Tar-ive commented Jun 28, 2026

Copy link
Copy Markdown
Owner

Summary

Addresses the viral loop gap: sharing a swipe challenge with a guy showed no relevant items. This PR adds a recipient-side gender/preference selector before swiping begins, a unified "Gifts" tab (replacing "Group Gifts") that surfaces Maxi's bundle suggestions from completed challenges, and delivery date awareness that flags items unlikely to arrive by the occasion.

Gender preference flow (recipient-side toggle)

New "preference" phase inserted between consent and swipe in /invite/[code]:

type Phase = "welcome" | "consent" | "preference" | "swipe" | "birthday" | "reveal";

Recipient picks GenderPref = "he" | "she" | "they" — this reorders the swipe deck via sortByGenderPref() (category weight maps: tech/sports/fitness weighted high for "he", jewelry/vintage/wellness for "she", even mix for "they"). Stored in:

  • SoftProfile.genderPref (localStorage)
  • SoftConnection.genderPref (API + localStorage fallback)
  • GuestSoftProfile.genderPrefPOST /connections body

Unified Gifts tab

  • Sidebar: "Group Gifts""Gifts" (both desktop + mobile drawer)
  • /feed/pools page now has solo/group sub-tabs
  • Solo tab shows SoloGiftCard for each completed challenge connection, with:
    • Maxi's bundle (first 6 seed pins from the connection)
    • Per-item delivery estimate + "Late" badge if won't ship by occasion date
    • One-click checkout button (scaffolded, not wired to payment)
    • Vibes tags from the swipe session

Backend (infra/src/handler.mjs)

  • POST /connections: now parses + validates guest.genderPref (enum: ["he","she","they"])
  • New GET /bundles?userId=&connectionId=: returns bundle items with delivery estimates relative to the connection's birthday/date

Deploy instructions

See .agents/DEPLOY.md for Terraform plan/apply steps and post-deploy wiring. TL;DR: only the Lambda source hash changes — no new tables, no IAM changes, no new infra.

Link to Devin session: https://calhacks-promptetheus.devinenterprise.com/sessions/3d8a7a08115f45c8938df8f9862f2f84
Requested by: @Tar-ive


Open in Devin Review

Summary by CodeRabbit

  • New Features
    • Added an optional style/gender preference step before swiping (with skip) and personalized swipe ordering.
    • Introduced a Gifts view with tabs for Solo Gifts and Group Gifts.
    • Added bundle viewing for solo completed challenges, including delivery estimates and deadline indicators.
  • Bug Fixes
    • Preference data is now captured and kept consistent across the app and backend.
  • Documentation
    • Added deployment and verification instructions for the updated preference, bundles, and related flows.

…delivery dates

- Add recipient-side gender/preference selector (he/she/they) in invite flow
- Prioritize swipe deck items based on selected gender preference
- Rename 'Group Gifts' nav tab to 'Gifts' with solo/group sub-tabs
- Show Maxi's gift bundle suggestions from completed challenges in Gifts tab
- Add delivery date awareness (items marked 'Late' if won't ship by occasion)
- One-click checkout button scaffolded for bundles
- Store genderPref in soft profiles (localStorage + API backend)
- Add GET /bundles API route for server-side bundle generation
- Include .agents/DEPLOY.md with Terraform plan/apply instructions

Co-Authored-By: Saksham <tarive22@gmail.com>
@Tar-ive Tar-ive self-assigned this Jun 28, 2026
@devin-ai-integration

Copy link
Copy Markdown
Contributor

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment, CI, and merge conflict monitoring

@vercel

vercel Bot commented Jun 28, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
giftmaxxing Ready Ready Preview, Comment, Open in v0 Jun 28, 2026 2:39pm

@coderabbitai

coderabbitai Bot commented Jun 28, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

Adds gender preference capture in the invite flow, stores it on connections, introduces GET /bundles, and updates the feed to show solo gift bundles alongside the existing pool view.

Changes

Gender Preference + Bundle Feature

Layer / File(s) Summary
Deployment and verification docs
.agents/DEPLOY.md
DEPLOY.md adds AWS scope, Terraform plan/apply steps, curl verification commands for POST /connections and GET /bundles, optional future roadmap items, and a no-op confirmation checklist.
GenderPref types and sort utility
web/lib/gender-prefs.ts, web/lib/api.ts, web/lib/soft-profile.ts
gender-prefs defines the GenderPref union, preference metadata, category weights, and deterministic sorting. GuestSoftProfile, SoftConnection, and SoftProfile gain optional genderPref fields.
Invite flow preference phase
web/app/invite/[code]/page.tsx, web/components/app/swipe-deck.tsx
The invite page adds a preference phase, genderPref state, a preference-selection UI, and propagates genderPref into connection, local save, and soft-profile payloads. SwipeDeck accepts genderPref and re-sorts the deck when it changes.
Connection persistence and bundle route
infra/src/handler.mjs
POST /connections validates and stores genderPref. GET /bundles authorizes access, loads the connection, fetches up to 8 seed posts, computes deadline and delivery-day fields, and returns bundle data with bundleTotal.
Solo Gifts feed and navigation
web/app/feed/pools/page.tsx, web/components/app/sidebar.tsx
PoolsPage adds solo/group tabs, loads solo gifts from API and local connections, and renders SoloGiftCard bundle tiles with deadline and delivery indicators. The sidebar label changes from Group Gifts to Gifts in desktop and drawer navigation.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Poem

🎁 A preference blooms before the swipe,
and bundles gather posts by type.
From POST /connections to GET /bundles bright,
the feed now splits to solo light.
Gifts, not groups, now guide the way,
with deadlines, tiers, and tabs at play.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 45.45% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title tracks the main changes: gender preference flow, Gifts tab consolidation, bundle generation, and swipe personalization.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch devin/1782655818-challenge-enhancements

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.

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Devin Review found 4 potential issues.

Open in Devin Review

Comment thread infra/src/handler.mjs
Comment on lines +2356 to +2365
return {
postId: item.postId,
title: item.title,
image: item.image,
price,
category: item.category,
deliveryDays,
canDeliverByDeadline,
};
});

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.

🔴 Gift bundle items returned without title or image because the backend reads non-existent fields from stored posts

Bundle items are built by reading item.title and item.image (infra/src/handler.mjs:2358-2359) from DynamoDB post records, but ingested posts store those values as caption and product.image instead, so every bundle item is returned with undefined for both fields.

Impact: The gift bundle API endpoint returns items with no title and no image, rendering the bundle unusable for any consumer.

Post schema mismatch in the /bundles handler

Posts ingested via infra/ingest/ingest-pins.mjs:102 store the display name as caption: shortCaption(pin.title) and the image inside a nested object at product.image (infra/ingest/ingest-pins.mjs:129). There is no top-level title or image attribute on the DynamoDB item.

Other parts of the handler correctly handle this — e.g. infra/src/handler.mjs:874 uses String(p.product?.name || p.caption || p.title || p.name || "") and infra/src/handler.mjs:1282 uses a similar fallback chain. The /bundles handler should follow the same pattern.

Suggested change
return {
postId: item.postId,
title: item.title,
image: item.image,
price,
category: item.category,
deliveryDays,
canDeliverByDeadline,
};
});
return {
postId: item.postId,
title: item.caption ?? item.product?.name ?? item.title ?? "",
image: item.product?.image ?? item.image ?? "",
price,
category: item.category,
deliveryDays,
canDeliverByDeadline,
};
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

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.

Fixed in 9bb6f77 — field resolution now uses item.caption ?? item.product?.name ?? item.title ?? "" and item.product?.image ?? item.image ?? "", matching the fallback chain used by other handler routes. Also added item.product?.price fallback for price.

Comment thread web/components/app/sidebar.tsx Outdated
{ label: "Events", href: "/feed/events", icon: "calendar" },
{ label: "Shop", href: "/feed/shop", icon: "gift" },
{ label: "Group Gifts", href: "/feed/pools", icon: "users" },
{ label: "Gifts", href: "/feed/pools", icon: "gift" },

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.

🚩 Sidebar now has two items with the same 'gift' icon — Shop and Gifts

The sidebar item for the pools/gifts page was changed from icon: "users" to icon: "gift" at web/components/app/sidebar.tsx:57, making it identical to the Shop item's icon at line 56. Both "Shop" and "Gifts" now render the same Icons.gift icon in the sidebar and mobile drawer. This isn't a crash but may cause user confusion since two nav items are visually indistinguishable by icon alone.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

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.

Fixed in 9bb6f77 — reverted to icon: "users" for the Gifts tab.

Comment thread .agents/DEPLOY.md
Comment on lines +49 to +54
curl -X POST https://tvyu8gqmki.execute-api.us-east-1.amazonaws.com/connections \
-H "content-type: application/json" \
-d '{"senderId":"test_user","guest":{"name":"TestGuy","genderPref":"he","vibes":["tech"],"seeds":["pin_1"]}}'

# Test bundle endpoint
curl "https://tvyu8gqmki.execute-api.us-east-1.amazonaws.com/bundles?userId=test_user&connectionId=<id_from_above>"

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.

🟥 Secrets exposure: AWS account ID and live API endpoint hardcoded in documentation

The deploy instructions file (.agents/DEPLOY.md:6,49,54) hardcodes the AWS account ID 445056752928, region us-east-1, and the full live API Gateway endpoint URL https://tvyu8gqmki.execute-api.us-east-1.amazonaws.com. While the account ID and endpoint are also mentioned in CLAUDE.md (which predates this PR), this PR adds new files containing curl command examples with these values that make reconnaissance trivial for attackers.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

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.

Acknowledged — the API endpoint and account ID are already in CLAUDE.md (committed before this PR) and this is a private repo. The information is not a secret (account IDs aren't sensitive, and the API endpoint is publicly routable by design). Keeping the curl examples as-is for developer convenience.

Comment thread infra/src/handler.mjs
Comment on lines +2319 to +2375
if (method === "GET" && path === "/bundles") {
const userId = qs.userId;
const connectionId = qs.connectionId;
if (!userId || !connectionId) return json(400, { error: "userId and connectionId required" });
// Fetch the connection record
const connOut = await ddb.send(
new GetCommand({ TableName: CONNECTIONS, Key: { userId, connectionId } })
);
const conn = connOut.Item;
if (!conn) return json(404, { error: "connection not found" });
// Build a bundle from the seeds — query the posts table for matching items
const seeds = conn.seeds ?? [];
const genderPref = conn.genderPref; // "he" | "she" | "they" | undefined
const deadline = conn.birthday; // "YYYY-MM-DD" or undefined
let bundleItems = [];
if (seeds.length > 0) {
// Look up seed pins from the posts table
for (const seed of seeds.slice(0, 8)) {
try {
const out = await ddb.send(new GetCommand({ TableName: POSTS, Key: { postId: seed } }));
if (out.Item) bundleItems.push(out.Item);
} catch { /* skip missing */ }
}
}
// Compute delivery estimates for each item
const today = new Date();
today.setHours(0, 0, 0, 0);
let deadlineDays = null;
if (deadline && /^\d{4}-\d{2}-\d{2}$/.test(deadline)) {
const [y, m, d] = deadline.split("-").map(Number);
const target = new Date(y, m - 1, d);
deadlineDays = Math.ceil((target.getTime() - today.getTime()) / 86_400_000);
}
const bundle = bundleItems.map((item) => {
const price = Number(item.price) || 50;
const deliveryDays = price > 200 ? 7 : price > 100 ? 5 : 3;
const canDeliverByDeadline = deadlineDays === null || deliveryDays <= deadlineDays;
return {
postId: item.postId,
title: item.title,
image: item.image,
price,
category: item.category,
deliveryDays,
canDeliverByDeadline,
};
});
return json(200, {
connectionId,
guestName: conn.guestName,
genderPref,
deadline,
deadlineDays,
bundle,
bundleTotal: bundle.reduce((sum, i) => sum + i.price, 0),
});
}

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.

🟨 Missing authorization check: /bundles endpoint allows any authenticated user to read another user's connection data

The GET /bundles endpoint (infra/src/handler.mjs:2319-2375) accepts userId and connectionId as query parameters and returns the connection's guest name, gender preference, birthday, and gift bundle. It only checks that the caller is authenticated (not in isPublicRoute), but does NOT verify that the authenticated user's identity (auth.sub) matches the requested userId. Any authenticated user can read any other user's connection data by guessing or enumerating connection IDs.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

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.

Fixed in 9bb6f77 — added authorizeRequest call and auth.sub === userId check (with admin bypass). Returns 403 if mismatched.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 7

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@infra/src/handler.mjs`:
- Around line 2336-2340: The seed lookup loop in handler.mjs is swallowing real
DynamoDB/runtime errors in the GetCommand path. Update the seed-fetch logic in
the bundle-building code to only skip when the response has no Item, and let
actual exceptions from ddb.send surface or be handled as a failure instead of
being ignored. Use the existing bundle/seeds loop and GetCommand call as the
place to distinguish “missing seed” from infrastructure failure.
- Around line 2352-2360: The bundle mapper in handler.mjs only reads top-level
item.title, item.image, and item.price, so post-shaped items with nested product
data can render blank fields and hit the $50 fallback. Update the
bundleItems.map callback to normalize each item first by preferring nested
product fields when present, then use those normalized values for title, image,
and price before computing deliveryDays and the returned bundle object.

In `@web/app/feed/pools/page.tsx`:
- Around line 326-328: The “One-click checkout bundle” CTA in the feed pools
scaffold is currently styled like a primary action but has no click handler, so
make it non-interactive until the checkout flow is implemented. Update the
button in the relevant render path to be disabled (and styled accordingly) or
wire it to the actual checkout handler once available, using the existing button
element as the anchor for the change.

In `@web/app/invite/`[code]/page.tsx:
- Around line 279-305: The gender preference selector in the invite page is
currently only visually indicating the active option. Update the button group in
the preference-mapping block to expose the selected state to assistive tech by
adding appropriate pressed/checked semantics on each option, using the existing
selected logic derived from genderPref and GENDER_PREF_META so screen readers
can announce the active choice.

In `@web/components/app/swipe-deck.tsx`:
- Around line 109-113: The deck reset path in startOver() is bypassing the
preference-aware ordering used in the effect, so the rebuilt deck loses the
current genderPref sort. Update startOver() to rebuild the deck through the same
logic as the useEffect for swipe-deck.tsx, applying sortByGenderPref to
buildDeck() whenever genderPref is set, and keep the stats/mounted reset
behavior unchanged.

In `@web/lib/api.ts`:
- Line 486: The transport types for `genderPref` in `api.ts` are using a plain
`string`, which breaks alignment with the shared gender preference contract.
Update the affected payload/response type definitions to use the shared
`GenderPref` union from `web/lib/gender-prefs.ts` at both locations mentioned,
so `POST /connections` and local-connection payloads stay type-safe and
exhaustive without casts or guards.

In `@web/lib/soft-profile.ts`:
- Line 19: The soft-profile state currently allows an unchecked string for
genderPref, so malformed persisted values can survive hydration in
loadSoftProfile(). Change the SoftProfile shape to use the shared GenderPref
union instead of string, and update the hydration/parsing logic to validate the
stored value against that union and reject unknown strings before returning the
profile.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 7800e319-70d6-4212-94eb-e2bb79b5f9b5

📥 Commits

Reviewing files that changed from the base of the PR and between 4805d44 and 2f21ffc.

📒 Files selected for processing (9)
  • .agents/DEPLOY.md
  • infra/src/handler.mjs
  • web/app/feed/pools/page.tsx
  • web/app/invite/[code]/page.tsx
  • web/components/app/sidebar.tsx
  • web/components/app/swipe-deck.tsx
  • web/lib/api.ts
  • web/lib/gender-prefs.ts
  • web/lib/soft-profile.ts

Comment thread infra/src/handler.mjs Outdated
Comment thread infra/src/handler.mjs
Comment thread web/app/feed/pools/page.tsx Outdated
Comment thread web/app/invite/[code]/page.tsx
Comment thread web/components/app/swipe-deck.tsx
Comment thread web/lib/api.ts
name: string;
handle?: string;
birthday?: string;
genderPref?: string;

@coderabbitai coderabbitai Bot Jun 28, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Use the shared GenderPref union at this transport boundary.

Typing these fields as string reintroduces schema drift (contract mismatch): invalid values can compile into POST /connections or local-connection payloads, and downstream code now needs casts/guards instead of getting exhaustiveness from the type system. Keep this in lockstep with web/lib/gender-prefs.ts.

🔧 Proposed fix
+import { type GenderPref } from "`@/lib/gender-prefs`";
+
 export type GuestSoftProfile = {
   name: string;
   handle?: string;
   birthday?: string;
-  genderPref?: string;
+  genderPref?: GenderPref;
   vibes?: string[];
   seeds?: string[];
   interests?: string[];
   yesCount?: number;
   totalSwipes?: number;
 };

 export type SoftConnection = {
   userId: string;
   connectionId: string;
   soft?: boolean;
   kind?: string;
   guestName: string;
   guestHandle?: string;
   birthday?: string;
-  genderPref?: string;
+  genderPref?: GenderPref;
   vibes?: string[];
   seeds?: string[];
   interests?: string[];
   yesCount?: number;
   totalSwipes?: number;
   seen?: boolean;
   createdAt?: number;
 };

Also applies to: 503-503

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/lib/api.ts` at line 486, The transport types for `genderPref` in `api.ts`
are using a plain `string`, which breaks alignment with the shared gender
preference contract. Update the affected payload/response type definitions to
use the shared `GenderPref` union from `web/lib/gender-prefs.ts` at both
locations mentioned, so `POST /connections` and local-connection payloads stay
type-safe and exhaustive without casts or guards.

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.

Acknowledged as a type-safety improvement. Keeping string at the transport boundary is intentional — the API can return values the frontend doesn't yet know about without breaking deserialization. Runtime validation happens at the UI layer (genderPref in GENDER_PREF_META).

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Skipped: comment is from another GitHub bot.

Comment thread web/lib/soft-profile.ts
vibes: string[];
seeds: string[];
birthday?: string;
genderPref?: string;

@coderabbitai coderabbitai Bot Jun 28, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Harden genderPref at the localStorage boundary.

loadSoftProfile() trust-casts any object with a name, so genderPref?: string lets malformed values round-trip through persisted state unchecked. Use the shared GenderPref union here and reject unknown strings when hydrating.

🔧 Proposed fix
+import { type GenderPref, GENDER_PREF_META } from "`@/lib/gender-prefs`";
+
 export type SoftProfile = {
   name: string;
   vibes: string[];
   seeds: string[];
   birthday?: string;
-  genderPref?: string;
+  genderPref?: GenderPref;
   inviterName: string;
   completedAt: number;
 };
 
 export function loadSoftProfile(): SoftProfile | null {
   if (typeof window === "undefined") return null;
   try {
     const raw = localStorage.getItem(KEY);
     if (!raw) return null;
     const parsed: unknown = JSON.parse(raw);
     if (
       parsed &&
       typeof parsed === "object" &&
       typeof (parsed as SoftProfile).name === "string"
     ) {
-      return parsed as SoftProfile;
+      const candidate = parsed as Partial<SoftProfile> & { genderPref?: unknown };
+      if (
+        candidate.genderPref != null &&
+        (typeof candidate.genderPref !== "string" || !(candidate.genderPref in GENDER_PREF_META))
+      ) {
+        return null;
+      }
+      return candidate as SoftProfile;
     }
     return null;
   } catch {
     return null;
   }
 }
📝 Committable suggestion

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

Suggested change
genderPref?: string;
import { type GenderPref, GENDER_PREF_META } from "`@/lib/gender-prefs`";
export type SoftProfile = {
name: string;
vibes: string[];
seeds: string[];
birthday?: string;
genderPref?: GenderPref;
inviterName: string;
completedAt: number;
};
export function loadSoftProfile(): SoftProfile | null {
if (typeof window === "undefined") return null;
try {
const raw = localStorage.getItem(KEY);
if (!raw) return null;
const parsed: unknown = JSON.parse(raw);
if (
parsed &&
typeof parsed === "object" &&
typeof (parsed as SoftProfile).name === "string"
) {
const candidate = parsed as Partial<SoftProfile> & { genderPref?: unknown };
if (
candidate.genderPref != null &&
(typeof candidate.genderPref !== "string" || !(candidate.genderPref in GENDER_PREF_META))
) {
return null;
}
return candidate as SoftProfile;
}
return null;
} catch {
return null;
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/lib/soft-profile.ts` at line 19, The soft-profile state currently allows
an unchecked string for genderPref, so malformed persisted values can survive
hydration in loadSoftProfile(). Change the SoftProfile shape to use the shared
GenderPref union instead of string, and update the hydration/parsing logic to
validate the stored value against that union and reject unknown strings before
returning the profile.

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.

Same rationale as api.ts — keeping string at the localStorage boundary is intentional so stale persisted data from older app versions doesn't cause loadSoftProfile() to return null. Invalid stored values are harmlessly ignored at the consumption point.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Skipped: comment is from another GitHub bot.

- Fix duplicate gift icon: Gifts tab reverts to 'users' icon
- Fix /bundles handler field normalization (caption/product.image/product.price)
- Add auth check: verify auth.sub === userId on GET /bundles
- Remove silent error swallowing in seed lookup loop
- Add per-item 'Add to cart' button + 'Add entire bundle' on gift cards
- Fix startOver() to preserve genderPref sort order
- Boost category weights for guy-friendly items (10x for tech/sports)
- Add aria-pressed to gender preference selector buttons

Co-Authored-By: Saksham <tarive22@gmail.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@infra/src/handler.mjs`:
- Around line 2323-2326: The bundles authorization check is re-running request
auth inside the handler, bypassing the global AUTH_ENFORCE behavior and causing
unsigned callers to be rejected even when enforcement is off. Update the bundles
read path in infra/src/handler.mjs to reuse the already-computed top-level auth
result instead of calling authorizeRequest again, and keep the owner/admin check
against that shared auth object so /bundles follows the same toggle as the other
protected routes.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: e9b88231-75e9-421c-b84b-ff9e6abdb2ef

📥 Commits

Reviewing files that changed from the base of the PR and between 2f21ffc and 9bb6f77.

📒 Files selected for processing (6)
  • infra/src/handler.mjs
  • web/app/feed/pools/page.tsx
  • web/app/invite/[code]/page.tsx
  • web/components/app/sidebar.tsx
  • web/components/app/swipe-deck.tsx
  • web/lib/gender-prefs.ts
✅ Files skipped from review due to trivial changes (1)
  • web/components/app/sidebar.tsx
🚧 Files skipped from review as they are similar to previous changes (4)
  • web/components/app/swipe-deck.tsx
  • web/lib/gender-prefs.ts
  • web/app/invite/[code]/page.tsx
  • web/app/feed/pools/page.tsx

Comment thread infra/src/handler.mjs
Comment on lines +2323 to +2326
// Authorization: only the owner (or admin) can read their bundles
const auth = await authorizeRequest(event, method, path);
if (!(auth.via === "admin" || auth.sub === userId)) {
return json(403, { error: "forbidden" });

@coderabbitai coderabbitai Bot Jun 28, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Reuse the top-level auth result here.

Line 2324 re-runs the auth gate (request filter) unconditionally, so /bundles no longer follows the global AUTH_ENFORCE toggle. When enforcement is off, this endpoint still returns 403 for unsigned callers while sibling protected routes stay open, which creates environment drift.

Suggested fix
-      const auth = await authorizeRequest(event, method, path);
-      if (!(auth.via === "admin" || auth.sub === userId)) {
+      if (auth && !(auth.via === "admin" || auth.sub === userId)) {
         return json(403, { error: "forbidden" });
       }
📝 Committable suggestion

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

Suggested change
// Authorization: only the owner (or admin) can read their bundles
const auth = await authorizeRequest(event, method, path);
if (!(auth.via === "admin" || auth.sub === userId)) {
return json(403, { error: "forbidden" });
// Authorization: only the owner (or admin) can read their bundles
if (auth && !(auth.via === "admin" || auth.sub === userId)) {
return json(403, { error: "forbidden" });
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@infra/src/handler.mjs` around lines 2323 - 2326, The bundles authorization
check is re-running request auth inside the handler, bypassing the global
AUTH_ENFORCE behavior and causing unsigned callers to be rejected even when
enforcement is off. Update the bundles read path in infra/src/handler.mjs to
reuse the already-computed top-level auth result instead of calling
authorizeRequest again, and keep the owner/admin check against that shared auth
object so /bundles follows the same toggle as the other protected routes.

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.

Intentional — /bundles (like /connections/claim at L2287) unconditionally enforces auth because it returns user-specific private data. The global AUTH_ENFORCE toggle is a development convenience for non-sensitive read routes; endpoints that expose personal data should never be unauthenticated regardless of the toggle state.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Skipped: comment is from another GitHub bot.

…g signals

- Add 'Browse similar' button next to 'Add all to cart' in Maxi bundles
- Make each bundle item card clickable (opens item in new tab)
- Shop page supports ?category= param to highlight/scroll to matching section
- Track swipe dwell time (ms between card shown and swipe action)
- Store dwellSignals in localStorage swipe records
- Send dwellSignals to backend on connection creation (POST /connections)
- Backend persists dwellSignals in DynamoDB for future recommendation enrichment

Co-Authored-By: Saksham <tarive22@gmail.com>
@devin-ai-integration devin-ai-integration Bot changed the title feat: gender preference toggle, unified Gifts tab, Maxi bundles with delivery dates feat: gender preference toggle, unified Gifts tab, Maxi bundles, browse similar, swipe timing Jun 28, 2026
@Tar-ive
Tar-ive merged commit fd3deb5 into main Jun 28, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant