Skip to content

Conversation

@Mnehmos
Copy link

@Mnehmos Mnehmos commented Jan 18, 2026

Summary

#302 (comment)

Implements the complete Sponsor/Sponsee Connection System with Intent & Ownership pattern as specified in #300. This feature enables users to find and connect with sponsors/sponsees through an opt-in matching system with privacy-first contact sharing.

Key Features

Connection Intent System

  • Users can set their connection intent: not_looking, seeking_sponsor, open_to_sponsoring, or open_to_both
  • Intent drives the matching algorithm and UI visibility
  • ConnectionIntentSelector component on profile screen

Opt-in Matching

  • FindSupportSection component for discovering compatible matches
  • System finds users with complementary intents (seeker ↔ provider)
  • Bilateral acceptance pattern - both parties must accept to connect
  • 7-day match expiration with visual countdown
  • connection_matches table with RLS policies

Persistent Invite Codes

  • PersistentInviteCard showing active invite code with expiration timer
  • Copy, share, regenerate, and revoke actions
  • Codes track intent context when created
  • revoked_at column for manual revocation

Symmetric Contact Reveal

  • SymmetricRevealSection for mutual consent contact sharing
  • Neither party sees contact info until both opt in
  • ExternalHandlesSection in Settings for managing private contact info (Discord, Telegram, WhatsApp, Signal, Phone)
  • external_handles JSONB field on profiles
  • sponsor_reveal_consent and sponsee_reveal_consent on relationships

Database Changes

New migration: 20260117000000_sponsor_sponsee_connection_system.sql

  • connection_intent_type enum
  • match_status_type enum
  • connection_matches table with bilateral acceptance
  • find_potential_matches() RPC function
  • accept_match() and reject_match() RPC functions
  • Extended profiles with connection_intent, external_handles
  • Extended sponsor_sponsee_relationships with reveal consent fields
  • Extended invite_codes with revoked_at, intent
  • Comprehensive RLS policies

Refactoring (DRY)

  • Extracted lib/time-utils.ts with getTimeRemaining(), formatTimeRemaining()
  • Extracted lib/platform-icons.tsx with getPlatformIcon(), getPlatformLabel()
  • 25 new tests for utility functions

Test Plan

  • All 88 test suites pass (2172 tests)
  • TypeScript type checking passes
  • ESLint passes
  • Web build succeeds
  • Live testing of matching flow between two accounts
  • Verify consent toggle works for both sponsor and sponsee
  • Test invite code flow (generate, share, claim, revoke)
  • Test match expiration behavior

Screenshots

Connection Intent Selector, Find Support Section, and Symmetric Reveal Section are integrated into the profile screen.


🤖 Generated with Claude Code

Closes #300


Important

Introduces a Sponsor/Sponsee Connection System with Intent & Ownership, featuring intent-based matching, invite code management, and mutual contact sharing, supported by new UI components and database changes.

  • Behavior:
    • Implements Sponsor/Sponsee Connection System with Intent & Ownership.
    • Users set connection intent (not_looking, seeking_sponsor, open_to_sponsoring, open_to_both).
    • Opt-in matching system with bilateral acceptance and 7-day expiration.
    • Persistent invite codes with actions (copy, share, regenerate, revoke).
    • Symmetric contact reveal with mutual consent.
  • Components:
    • ConnectionIntentSelector for setting user intent.
    • FindSupportSection for discovering matches.
    • PersistentInviteCard for managing invite codes.
    • SymmetricRevealSection for contact sharing.
  • Database:
    • Migration 20260117000000_sponsor_sponsee_connection_system.sql.
    • Adds connection_intent_type and match_status_type enums.
    • Adds connection_matches table and related RPC functions.
    • Extends profiles, sponsor_sponsee_relationships, and invite_codes tables.
    • Comprehensive RLS policies for new features.
  • Refactoring:
    • Extracts lib/time-utils.ts and lib/platform-icons.tsx for utility functions.
    • Adds 25 tests for utility functions.

This description was created by Ellipsis for ca23d0d. You can customize this summary. It will automatically update as commits are pushed.

#302 (comment)

Mnehmos and others added 3 commits January 18, 2026 00:31
…t & ownership

Implements Issue VolvoxCommunity#300 - Sponsor/Sponsee Connection System:

Intent & Ownership:
- Add ConnectionIntentSelector for declaring connection intent
  (not_looking, seeking_sponsor, open_to_sponsoring, open_to_both)
- Add PersistentInviteCard with expiration timer and actions
  (Copy, Share, Regenerate, Revoke)
- Invite codes are now visible, persistent objects in the UI

First-Class Relationships:
- Integrate SymmetricRevealSection into RelationshipCard
- Show connection cards for active sponsor/sponsee relationships

Trust Mechanism:
- Add ExternalHandlesSection to Settings for private handle storage
  (Discord, Telegram, WhatsApp, Signal, Phone)
- Per-connection reveal consent toggles
- Symmetric reveal: contacts only shown when both parties consent

Database Schema:
- Add migration for connection_intent, external_handles, reveal consent
- Add RLS policies for secure data access

Also includes:
- Web compatibility fix for bottom sheet TextInput components
- Update test mocks for new icon usage

Closes VolvoxCommunity#300

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
…ions

Add FindSupportSection component for discovering matches based on
complementary connection intents. Implement bilateral acceptance pattern
where both parties must accept before a relationship is established.

Database changes:
- Add connection_matches table with seeker/provider roles
- Add find_potential_matches RPC for discovering compatible users
- Add accept_match and reject_match RPCs for match resolution
- Handle existing inactive relationships in accept flow

UI changes:
- Add FindSupportSection to profile when user has an active intent
- Fix consent mapping in RelationshipCard for symmetric reveal

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Extract getTimeRemaining and formatTimeRemaining to lib/time-utils.ts
- Extract getPlatformIcon and getPlatformLabel to lib/platform-icons.tsx
- Update FindSupportSection, PersistentInviteCard, SymmetricRevealSection
  to use shared utilities
- Add comprehensive tests for new utility files (25 passing tests)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@vercel
Copy link

vercel bot commented Jan 18, 2026

@Mnehmos is attempting to deploy a commit to the Volvox Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 18, 2026

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • Intent-based sponsor/sponsee matching (Find Support), bilateral match actions, persistent invite cards, consented contact reveal, connection intent selector, editable external contact handles.
  • Refactor

    • Centralized platform icons, time utilities, and a platform‑aware sheet input for consistent UI/behavior.
  • Bug Fixes

    • Debounced saves, improved clipboard/share/toast feedback, timer/expiry reliability, UI stability tweaks.
  • Tests

    • Expanded tests and mocks, deterministic crypto mock for stable invite code tests.

✏️ Tip: You can customize this high-level summary in your review settings.

Walkthrough

Adds an intent-driven sponsor/sponsee system: persistent invite codes with lifecycle actions, intent-based matching backed by new RPCs and a connection_matches table, consent-driven symmetric reveal of external handles, UI components for intents/invites/matching, shared time/platform utilities, and supporting tests/mocks and migrations.

Changes

Cohort / File(s) Summary
Database Schema & RPCs
supabase/migrations/20260117000000_sponsor_sponsee_connection_system.sql
New enums (connection_intent_type, match_status_type), connection_matches table, RPCs find_potential_matches, accept_match, reject_match, get_handles_with_consent, secure invite lookup; new columns profiles.connection_intent, profiles.external_handles, sponsor_sponsee_relationships.*_reveal_consent, invite_codes.revoked_at/intent, plus indexes and RLS/policies.
Types / Public Interfaces
types/database.ts
Adds ConnectionIntent, MatchStatus, ExternalHandles, ConnectionMatch, PotentialMatch; extends Profile, SponsorSponseeRelationship, and InviteCode with new fields.
Profile Screen & Invite Flow
app/(app)/(tabs)/profile/index.tsx, components/profile/PersistentInviteCard.tsx, components/profile/ConnectionIntentSelector.tsx
Persistent invite lifecycle (fetch/regenerate/revoke), intent selection, pendingMatches fetch; UI components for intent selection and persistent invite card (status, timer, copy/share/regenerate/revoke).
Match Discovery & Matching UI
components/profile/FindSupportSection.tsx, app/(app)/(tabs)/profile/index.tsx
FindSupportSection calls find_potential_matches, inserts connection_matches, uses accept_match/reject_match RPCs; renders new/waiting match lists, match actions, processing states, and time-remaining info.
Consent & Handle Reveal
components/profile/SymmetricRevealSection.tsx, components/profile/RelationshipCard.tsx
SymmetricRevealSection with reveal-state logic, consent toggles, RPC to fetch handles with consent; RelationshipCard accepts relationship, myHandles, onConsentChange and conditionally renders reveal UI.
Settings — External Handles
components/settings/ExternalHandlesSection.tsx, components/settings/SettingsContent.tsx
ExternalHandlesSection for per-platform handle editing; SettingsContent debounces persistence to profiles.external_handles, manages draft state and saving indicator.
Shared Utilities & Inputs
lib/time-utils.ts, lib/platform-icons.tsx, lib/sheet-input.tsx
New time helpers (getTimeRemaining, formatTimeRemaining), platform icon/label helpers (PlatformKey, getPlatformIcon, getPlatformLabel, IconTheme), and SheetInputComponent to select TextInput vs BottomSheetTextInput by platform.
Sheet Input Replacements
components/TaskCreationSheet.tsx, components/sheets/*
Replace multiple BottomSheetTextInput usages with SheetInputComponent for cross-platform compatibility.
Tests & Mocks
__tests__/lib/time-utils.test.ts, __tests__/lib/platform-icons.test.tsx, __mocks__/lucide-react-native.js, many __tests__/**, jest.setup.js
Adds tests for time-utils and platform-icons; expands lucide-react-native mocks across tests; updates invite-related test mocks and Toast assertions; deterministic expo-crypto mock in jest setup.
Package & Changelog
package.json, CHANGELOG.md
Adds expo-crypto dependency and documents the feature and type/export changes.

Sequence Diagram(s)

sequenceDiagram
    participant User as User
    participant Profile as ProfileScreen
    participant Find as FindSupportSection
    participant RPC as SupabaseRPC
    participant DB as Database

    User->>Profile: Select intent / open profile
    Profile->>Profile: set connectionIntent, fetch active invite & pendingMatches
    User->>Find: Click "Find Matches"
    Find->>RPC: call find_potential_matches(user_id)
    RPC->>DB: query complementary intents and eligible profiles
    DB-->>RPC: return candidates
    RPC-->>Find: potential matches
    Find->>Profile: display matches
    User->>Find: Accept match
    Find->>RPC: call accept_match(match_id)
    RPC->>DB: lock match, create/reactivate relationship, update status
    DB-->>RPC: updated match/relationship
    RPC-->>Find: confirmation
    Find->>Profile: refresh pendingMatches
Loading
sequenceDiagram
    participant User as User
    participant Card as RelationshipCard
    participant Reveal as SymmetricRevealSection
    participant RPC as SupabaseRPC
    participant DB as Database

    User->>Card: Open connection card
    Card->>Reveal: render with consent flags
    User->>Reveal: Toggle "Share my contact"
    Reveal->>RPC: update reveal consent
    RPC->>DB: update sponsor_reveal_consent / sponsee_reveal_consent
    DB-->>RPC: confirm update
    RPC-->>Reveal: return updated relationship
    alt Both parties consented
        Reveal->>RPC: call get_handles_with_consent(relationship_id)
        RPC->>DB: fetch allowed external_handles JSON
        DB-->>RPC: return handles
        RPC-->>Reveal: handles
        Reveal->>User: display mutual handles with platform icons
    else One-sided consent
        Reveal->>User: show "Waiting for them..." status
    end
Loading

Suggested labels

feature, frontend, supabase, testing

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat: Sponsor/Sponsee Connection System with Intent & Ownership (closes #300)' accurately describes the main feature implemented and is closely aligned with the changeset objectives.
Description check ✅ Passed The PR description is comprehensive and directly related to the changeset, covering key features, database changes, refactoring, and testing status aligned with the implementation.
Linked Issues check ✅ Passed The PR implements all primary coding requirements from issue #300: intent system [ConnectionIntentSelector], opt-in matching [FindSupportSection, connection_matches RPC functions], persistent invite codes [PersistentInviteCard with revoked_at/intent], symmetric contact reveal [SymmetricRevealSection, external_handles, consent toggles], database schema, and utility extractions [time-utils, platform-icons].
Out of Scope Changes check ✅ Passed All changes are directly aligned with issue #300 objectives: new intent/matching/invite/reveal components, database extensions, utility libraries, and test coverage. Mock enhancements in test files and SheetInputComponent refactoring support the main feature implementation and are in-scope.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

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

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 markdownlint-cli2 (0.18.1)
CHANGELOG.md

markdownlint-cli2 v0.18.1 (markdownlint v0.38.0)
Finding: CHANGELOG.md
Linting: 1 file(s)
Summary: 0 error(s)
Error: EACCES: permission denied, open '/markdownlint-cli2-results.json'
at async open (node:internal/fs/promises:640:25)
at async Object.writeFile (node:internal/fs/promises:1214:14)
at async Promise.all (index 0)
at async outputSummary (file:///usr/local/lib/node_modules/markdownlint-cli2/markdownlint-cli2.mjs:877:5)
at async main (file:///usr/local/lib/node_modules/markdownlint-cli2/markdownlint-cli2.mjs:1053:25)
at async file:///usr/local/lib/node_modules/markdownlint-cli2/markdownlint-cli2-bin.mjs:12:22 {
errno: -13,
code: 'EACCES',
syscall: 'open',
path: '/markdownlint-cli2-results.json'
}


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.

/**
* Format the user's role in this match (seeker or provider).
*/
function getUserRole(match: ConnectionMatch, visibleMatches: string): 'seeker' | 'provider' {
Copy link
Contributor

Choose a reason for hiding this comment

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

Minor naming suggestion: the helper function getUserRole uses a parameter called visibleMatches which is actually the user ID. Renaming it to userId would improve clarity.

logger.error('Failed to regenerate invite code', error as Error, {
category: LogCategory.DATABASE,
});
showToast.error('Failed to generate invite code');
Copy link
Contributor

Choose a reason for hiding this comment

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

There's an inconsistency in the error messaging in the regenerateInviteCode function: the catch block logs 'Failed to regenerate invite code' while the toast message says 'Failed to generate invite code'. Consider using consistent wording (e.g., update the toast to 'Failed to regenerate invite code').

Suggested change
showToast.error('Failed to generate invite code');
showToast.error('Failed to regenerate invite code');

Copy link
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: 21

Caution

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

⚠️ Outside diff range comments (2)
components/TaskCreationSheet.tsx (1)

21-35: Move InputComponent below Types and into a Constants section.

Current placement violates the required file ordering.

As per coding guidelines, please keep file sections ordered.

♻️ Suggested move
-// Use regular TextInput on web to avoid BottomSheetTextInput compatibility issues
-const InputComponent = Platform.OS === 'web' ? TextInput : BottomSheetTextInput;
-
-// =============================================================================
-// Types & Interfaces
-// =============================================================================
+// =============================================================================
+// Types & Interfaces
+// =============================================================================
@@
+// =============================================================================
+// Constants
+// =============================================================================
+// Use regular TextInput on web to avoid BottomSheetTextInput compatibility issues
+const InputComponent = Platform.OS === 'web' ? TextInput : BottomSheetTextInput;
+
 // =============================================================================
 // Component
 // =============================================================================
app/(app)/(tabs)/profile/index.tsx (1)

228-248: External handles are fetched even without mutual consent

select('*, sponsor:sponsor_id(*)') and select('*, sponsee:sponsee_id(*)') pull full profile rows, which include external_handles. That means contact info is delivered to clients before mutual consent, undermining the symmetric-reveal privacy guarantee. Prefer selecting only non-sensitive fields here and fetch handles via a consent-checked RPC/view when both consents are true.

🔒️ Example mitigation (exclude external_handles)
- .select('*, sponsor:sponsor_id(*)')
+ .select('*, sponsor:sponsor_id(id, display_name, sobriety_date, avatar_url)')
🤖 Fix all issues with AI agents
In `@__tests__/lib/platform-icons.test.tsx`:
- Around line 78-81: The test for getPlatformIcon should not only assert
truthiness; render the returned icon and verify the size is applied: call
getPlatformIcon('discord', mockTheme, 24), render the resulting React element
(e.g., with React Testing Library render or shallow render), then assert that
the rendered element or the underlying icon component (the element returned from
getPlatformIcon) has the size prop or style reflecting 24 (e.g., prop size ===
24 or width/height === 24). Update the test in
__tests__/lib/platform-icons.test.tsx to locate the element returned by
getPlatformIcon and assert the size is passed through.

In `@__tests__/lib/time-utils.test.ts`:
- Around line 53-59: The test "handles edge case of exactly now" is flaky
because it constructs now with new Date().toISOString() then calls
getTimeRemaining(now), allowing a race between timestamp creation and
getTimeRemaining's internal time check; update the test to use a deterministic
time source by mocking Date.now() (or replacing the input with a fixed ISO
timestamp) so both the produced timestamp and getTimeRemaining use the same base
time—e.g., in this test mock Date.now() to return a fixed epoch, generate the
ISO string from that same epoch for the input, call getTimeRemaining(input), and
restore the mock afterwards to ensure deterministic isExpired behavior.

In `@app/`(app)/(tabs)/profile/index.tsx:
- Around line 66-68: Rename the boolean state loadingInviteCode to
isLoadingInviteCode across this file: update the useState declaration (replace
loadingInviteCode with isLoadingInviteCode and keep setLoadingInviteCode or
rename to setIsLoadingInviteCode for consistency), and update every usage of
loadingInviteCode and setLoadingInviteCode in functions, JSX, and effect hooks
to the new identifiers (isLoadingInviteCode / setIsLoadingInviteCode) to follow
the is/has/should boolean naming convention.

In `@CHANGELOG.md`:
- Around line 10-14: Under the Unreleased section there are two duplicate "###
Fixed" headings; consolidate them by keeping a single "### Fixed" heading and
merging the two bullet lists (e.g., the entries "Fix Steps tab still showing in
native tab bar when 12-step content toggle is disabled" and "Fix bottom sheet
text inputs not working on web by using platform-specific InputComponent
pattern") into that one list so the Unreleased category has exactly one Fixed
section.

In `@components/profile/FindSupportSection.tsx`:
- Around line 120-132: The client-side inserts to the connection_matches table
violate RLS when a provider (intent === 'open_to_sponsoring') tries to create
rows because matchData sets seeker_id to the other user; update the logic so
only seekers perform direct inserts: check the isSeeking boolean before calling
supabase.from('connection_matches').insert(matchData) and skip/return an error
for non-seekers, or instead route all inserts through a SERVER-side RPC (e.g.,
call the create_connection_matches SECURITY DEFINER function) which can insert
for either role; adjust the code around intent, isSeeking, matchData and the
supabase.insert call to implement one of these two options so client behavior
aligns with the RLS policy.
- Around line 107-139: The logger calls in this block (logger.error('Failed to
find matches', ...), logger.warn('Failed to create match', ...), and
logger.error('Error finding matches', ...)) are missing structured LogCategory
metadata; update each logger invocation to include a category field (e.g., {
category: LogCategory.Profile }) as part of the log metadata argument so logs
conform to structured logging guidelines, leaving existing error/message
payloads intact and retaining existing fields like errorMessage where used.
- Around line 18-31: The boolean prop on the FindSupportSectionProps interface
is named disabled but should follow the is/has/should convention; rename the
prop to isDisabled in the FindSupportSectionProps interface and update all
usages and prop destructuring/consumption in the FindSupportSection component
(and any callers) to use isDisabled instead of disabled, preserving the optional
typing (?); run a project-wide search for "disabled" references tied to
FindSupportSectionProps or the FindSupportSection component and update those to
isDisabled to keep types and runtime behavior consistent.

In `@components/profile/PersistentInviteCard.tsx`:
- Around line 69-80: The timer can show stale values because the useState
initializer runs only once and the interval waits 60s; inside the useEffect that
depends on inviteCode.expires_at (which currently creates interval with
setInterval), call setTimeRemaining(getTimeRemaining(inviteCode.expires_at, 3))
immediately before creating the interval so state updates right away, then
create the interval as before and return the cleanup that clears it; update
references to timeRemaining and setTimeRemaining accordingly to ensure the
display reflects the new expires_at immediately.
- Around line 82-92: handleCopy currently performs clipboard writes on web
(navigator.clipboard.writeText) and native (setStringAsync from
'expo-clipboard') without any error handling; wrap both branches of the
clipboard operation in a try/catch inside the handleCopy function, catch and log
the error (or surface to your error logger) and call showToast.error with a
user-friendly message (e.g., "Failed to copy code") while keeping the success
path calling showToast.success; reference the existing symbols handleCopy,
inviteCode.code, Platform.OS, navigator.clipboard.writeText, and setStringAsync
to locate and update the code.

In `@components/profile/RelationshipCard.tsx`:
- Around line 6-8: In RelationshipCard.tsx replace the relative import of
SymmetricRevealSection with the project alias import so internal modules use the
`@/` path; change the import statement that currently references
'./SymmetricRevealSection' to use '@/components/profile/SymmetricRevealSection'
(update the import at the top of the RelationshipCard component).

In `@components/profile/SymmetricRevealSection.tsx`:
- Around line 87-90: The iteration over otherProfile.external_handles
(otherHandles) can render entries with empty values because hasOtherHandles only
checks key count; filter otherHandles to remove entries whose values are
falsy/empty (e.g., empty string or null) before mapping (create filteredHandles
from Object.entries(otherHandles).filter(([k,v]) => Boolean(v)) and use
filteredHandles where Object.entries(otherHandles) is currently used), and
update hasOtherHandles to reflect filteredHandles.length > 0 so rendering skips
when no valid handle values exist.

In `@components/settings/ExternalHandlesSection.tsx`:
- Around line 119-131: The togglePlatform function mixes UI state updates with
data updates by calling handleChange(key, '') inside the setExpandedPlatforms
updater; instead compute the new Set outside the updater, call
setExpandedPlatforms with that new Set, and then if the key was removed call
handleChange(key, '') after setExpandedPlatforms returns. In other words, in
togglePlatform use setExpandedPlatforms(prev => { const next = new Set(prev);
...; return next; }) or compute next first, then call setExpandedPlatforms(next)
and separately call handleChange(key, '') when collapsing to avoid invoking
onChange during the state update callback.

In `@components/settings/SettingsContent.tsx`:
- Around line 772-799: handleExternalHandlesChange currently writes to the DB on
every keystroke which blocks UI because inputs are driven from
profile?.external_handles and isSavingHandles disables editing; change to
maintain a local draft state (e.g., externalHandlesDraft) that
ExternalHandlesSection updates immediately, stop disabling inputs during typing,
and debounce the persistence: call a debounced function that uses supabase
update (the logic in handleExternalHandlesChange) after a short delay or on
blur/submit, ensure refreshProfile() runs only after successful debounced save,
and keep isSavingHandles solely for the save lifecycle (not per keystroke) so
typing remains responsive.
- Line 67: In SettingsContent, replace the relative import of
ExternalHandlesSection with the project alias import (use the '@/...' alias) so
the SettingsContent component imports ExternalHandlesSection via the alias
rather than a relative path; update the import statement that references
ExternalHandlesSection to use the "@/..." alias form consistent with other
internal imports.

In `@components/sheets/EditSavingsSheet.tsx`:
- Around line 34-35: The const InputComponent declaration is placed before the
Types section and should be moved under the Constants section to follow the
Imports → Types → Constants → Helpers → Component → Styles ordering; locate the
InputComponent symbol (currently: const InputComponent = Platform.OS === 'web' ?
TextInput : BottomSheetTextInput;) and cut/paste it into the Constants block
(after type definitions) so the file sections remain in the required order.

In `@components/sheets/EnterInviteCodeSheet.tsx`:
- Around line 5-21: The file defines InputComponent at the top which breaks the
required section ordering; move the InputComponent declaration (the ternary
using Platform, TextInput and BottomSheetTextInput) into the Constants section
immediately after the Types section so file sections remain ordered; ensure you
keep the same identifier InputComponent and imports (Platform, TextInput,
BottomSheetTextInput) intact and update any references that use InputComponent
accordingly.

In `@components/sheets/TaskCompletionSheet.tsx`:
- Around line 12-29: Move the platform-specific InputComponent declaration so it
resides inside the file's Constants section (below any imports and above Types
declarations), e.g., relocate the line "const InputComponent = Platform.OS ===
'web' ? TextInput : BottomSheetTextInput;" from its current position to the
Constants block; ensure any references to InputComponent (such as in
TaskCompletionSheet component) still import Platform, TextInput, and
BottomSheetTextInput at the top and that the file section order is preserved:
Imports → Constants (with InputComponent) → Types → rest of implementation.

In `@lib/platform-icons.tsx`:
- Around line 1-12: This file lacks the required section divider comments and
the prescribed import→types→constants→helpers→main logic→exports ordering; add
the canonical section headers (e.g. //
=============================================================================
and labelled subsections) and reorder/annotate blocks so imports (React,
lucide-react-native) are in the imports section, the PlatformKey type is under
types, any icon mapping constants go under constants, any utility functions
under helpers, and the component or mapping export(s) (the main logic
referencing PlatformKey and icons like MessageCircle, Send, Phone, Shield)
appear under main logic → exports to match project guidelines.

In `@supabase/migrations/20260117000000_sponsor_sponsee_connection_system.sql`:
- Around line 117-129: The current policy "Users can view sponsor profile via
invite code" on public.profiles is too broad—remove this policy and instead
implement a SECURITY DEFINER RPC (e.g., create function
get_sponsor_profile_by_invite_code(invite TEXT) RETURNS public.profiles) that
validates the invite against public.invite_codes (check sponsor_id, expires_at >
now(), used_by IS NULL, revoked_at IS NULL) and returns only that single sponsor
row; mark the function SECURITY DEFINER, perform any necessary edge-case checks,
grant EXECUTE ON FUNCTION to role authenticated, and ensure no general SELECT
policy remains on public.profiles that permits directory-style enumeration.
- Around line 107-112: The UPDATE policy "Users can update invite codes when
using them" on table public.invite_codes currently omits revoked_at, allowing
revoked codes to be updated; modify that policy to include a check that
revoked_at IS NULL (or revoked_at <= now() as appropriate) in the USING
expression and also include revoked_at IS NULL in the WITH CHECK expression
alongside used_by = auth.uid() so revoked invite codes cannot be claimed or
updated; target the policy named "Users can update invite codes when using them"
and the public.invite_codes table/columns revoked_at and used_by when making the
change.
- Around line 217-219: The UNIQUE(seeker_id, provider_id) constraint on
connection_matches (constraint name unique_active_match) prevents future
rematches after a rejection/expiry; remove that constraint and replace it with a
partial unique index that only enforces uniqueness for active/pending matches
(e.g., create a unique index idx_connection_matches_unique_pending on
(seeker_id, provider_id) WHERE status = 'pending'), or alternatively change the
uniqueness to include the status column so only identical-status rows are
unique; ensure you DROP CONSTRAINT unique_active_match (IF EXISTS) then CREATE
the partial unique index (IF NOT EXISTS) to allow rematching once status
changes.

Comment on lines 66 to 68
const [activeInviteCode, setActiveInviteCode] = useState<InviteCode | null>(null);
const [loadingInviteCode, setLoadingInviteCode] = useState(false);
const [pendingMatches, setPendingMatches] = useState<ConnectionMatch[]>([]);
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Rename loadingInviteCode to isLoadingInviteCode

Boolean state names should use the is/has/should prefix.

♻️ Proposed rename
-const [loadingInviteCode, setLoadingInviteCode] = useState(false);
+const [isLoadingInviteCode, setIsLoadingInviteCode] = useState(false);

As per coding guidelines, boolean variables should use is/has/should prefix.

📝 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
const [activeInviteCode, setActiveInviteCode] = useState<InviteCode | null>(null);
const [loadingInviteCode, setLoadingInviteCode] = useState(false);
const [pendingMatches, setPendingMatches] = useState<ConnectionMatch[]>([]);
const [activeInviteCode, setActiveInviteCode] = useState<InviteCode | null>(null);
const [isLoadingInviteCode, setIsLoadingInviteCode] = useState(false);
const [pendingMatches, setPendingMatches] = useState<ConnectionMatch[]>([]);
🤖 Prompt for AI Agents
In `@app/`(app)/(tabs)/profile/index.tsx around lines 66 - 68, Rename the boolean
state loadingInviteCode to isLoadingInviteCode across this file: update the
useState declaration (replace loadingInviteCode with isLoadingInviteCode and
keep setLoadingInviteCode or rename to setIsLoadingInviteCode for consistency),
and update every usage of loadingInviteCode and setLoadingInviteCode in
functions, JSX, and effect hooks to the new identifiers (isLoadingInviteCode /
setIsLoadingInviteCode) to follow the is/has/should boolean naming convention.

Comment on lines 12 to 29
import {
View,
Text,
StyleSheet,
TouchableOpacity,
ActivityIndicator,
TextInput,
Platform,
} from 'react-native';
import { BottomSheetScrollView, BottomSheetTextInput } from '@gorhom/bottom-sheet';
import { X, CheckCircle } from 'lucide-react-native';
import { ThemeColors } from '@/contexts/ThemeContext';
import GlassBottomSheet, { GlassBottomSheetRef } from '@/components/GlassBottomSheet';
import type { Task } from '@/types/database';

// Use regular TextInput on web to avoid BottomSheetTextInput compatibility issues
const InputComponent = Platform.OS === 'web' ? TextInput : BottomSheetTextInput;

Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Relocate InputComponent under the Constants section.

It currently sits before Types, violating the required file structure.

As per coding guidelines, please keep file sections ordered.

♻️ Suggested move
-// Use regular TextInput on web to avoid BottomSheetTextInput compatibility issues
-const InputComponent = Platform.OS === 'web' ? TextInput : BottomSheetTextInput;
-
-// =============================================================================
-// Types & Interfaces
-// =============================================================================
+// =============================================================================
+// Types & Interfaces
+// =============================================================================
@@
+// =============================================================================
+// Constants
+// =============================================================================
+// Use regular TextInput on web to avoid BottomSheetTextInput compatibility issues
+const InputComponent = Platform.OS === 'web' ? TextInput : BottomSheetTextInput;
🤖 Prompt for AI Agents
In `@components/sheets/TaskCompletionSheet.tsx` around lines 12 - 29, Move the
platform-specific InputComponent declaration so it resides inside the file's
Constants section (below any imports and above Types declarations), e.g.,
relocate the line "const InputComponent = Platform.OS === 'web' ? TextInput :
BottomSheetTextInput;" from its current position to the Constants block; ensure
any references to InputComponent (such as in TaskCompletionSheet component)
still import Platform, TextInput, and BottomSheetTextInput at the top and that
the file section order is preserved: Imports → Constants (with InputComponent) →
Types → rest of implementation.

@Mnehmos
Copy link
Author

Mnehmos commented Jan 18, 2026

PR #302 Review Implementation Plan

Overview

CodeRabbit requested changes with 21 actionable comments + 2 outside-diff-range issues (23 total). This plan addresses all issues grouped by priority and file proximity for efficient implementation.


Phase 1: Security & Privacy Fixes (Critical)

1.1 SQL Migration Security Fixes

File: supabase/migrations/20260117000000_sponsor_sponsee_connection_system.sql

Issue Lines Fix
UPDATE policy missing revoked_at check 107-112 Add AND revoked_at IS NULL to USING clause
RLS policy too broad 117-129 Replace with SECURITY DEFINER RPC function
UNIQUE constraint prevents rematches 217-219 Convert to partial unique index on status = 'pending'

Implementation:

-- Fix 1.1a: Add revoked_at check
CREATE POLICY "Users can update invite codes when using them"
  ON public.invite_codes FOR UPDATE TO authenticated
  USING (expires_at > now() AND used_by IS NULL AND revoked_at IS NULL)
  WITH CHECK (used_by = auth.uid() AND revoked_at IS NULL);

-- Fix 1.1b: Replace broad policy with RPC
DROP POLICY IF EXISTS "Users can view sponsor profile via invite code" ON public.profiles;
CREATE OR REPLACE FUNCTION get_sponsor_profile_by_invite_code(invite TEXT)
RETURNS public.profiles AS $$
  SELECT p.* FROM public.profiles p
  JOIN public.invite_codes ic ON ic.sponsor_id = p.id
  WHERE ic.code = invite
    AND ic.expires_at > now()
    AND ic.used_by IS NULL
    AND ic.revoked_at IS NULL
  LIMIT 1;
$$ LANGUAGE sql SECURITY DEFINER;

-- Fix 1.1c: Partial unique index
ALTER TABLE public.connection_matches DROP CONSTRAINT IF EXISTS unique_active_match;
CREATE UNIQUE INDEX IF NOT EXISTS idx_connection_matches_unique_pending
  ON public.connection_matches(seeker_id, provider_id)
  WHERE status = 'pending';

1.2 Privacy: External Handles Exposure

File: app/(app)/(tabs)/profile/index.tsx (lines 228-248)

Problem: Fetches all profile fields including external_handles before consent check.

Fix: Select only non-sensitive fields in relationship queries:

.select('*, sponsor:sponsor_id(id, display_name, sobriety_date, avatar_url)')
.select('*, sponsee:sponsee_id(id, display_name, sobriety_date, avatar_url)')

Then fetch handles via separate consent-checked query when rendering SymmetricRevealSection.

1.3 RLS Violation for Provider Inserts

File: components/profile/FindSupportSection.tsx (lines 120-132)

Problem: When intent === 'open_to_sponsoring', client inserts with seeker_id as another user, violating RLS.

Fix: Route all inserts through the existing create_connection_matches RPC or only allow seekers to create matches directly.


Phase 2: Performance & UX Fixes (Medium)

2.1 Debounce External Handles Changes

File: components/settings/SettingsContent.tsx (lines 772-799)

Problem: DB write on every keystroke.

Fix:

  • Add local draft state for immediate UI updates
  • Debounce persistence (500ms delay or on blur)
  • Keep isSavingHandles for save lifecycle only

2.2 Timer & Clipboard Fixes

File: components/profile/PersistentInviteCard.tsx

Issue Lines Fix
Timer stale on mount 69-80 Call setTimeRemaining() immediately in useEffect before interval
handleCopy no error handling 82-92 Wrap clipboard operations in try/catch, show error toast

2.3 Logger Category Metadata

File: components/profile/FindSupportSection.tsx (lines 107-139)

Fix: Add { category: LogCategory.Profile } to all logger calls.

2.4 togglePlatform State Separation

File: components/settings/ExternalHandlesSection.tsx (lines 119-131)

Fix: Separate UI state update from data mutation:

const togglePlatform = (key: PlatformKey) => {
  const wasExpanded = expandedPlatforms.has(key);
  setExpandedPlatforms((prev) => {
    const next = new Set(prev);
    wasExpanded ? next.delete(key) : next.add(key);
    return next;
  });
  if (wasExpanded) handleChange(key, '');
};

2.5 Fix Flaky Test

File: __tests__/lib/time-utils.test.ts (lines 53-59)

Fix: Use jest.useFakeTimers() and jest.setSystemTime() for deterministic testing.


Phase 3: Code Quality & Style Fixes (Low)

3.1 Boolean Naming Convention

File Current New
components/profile/FindSupportSection.tsx:30 disabled isDisabled
app/(app)/(tabs)/profile/index.tsx:67 loadingInviteCode isLoadingInviteCode

3.2 Import Alias Fixes

File Line Change
components/profile/RelationshipCard.tsx 8 './SymmetricRevealSection''@/components/profile/SymmetricRevealSection'
components/settings/SettingsContent.tsx 67 './ExternalHandlesSection''@/components/settings/ExternalHandlesSection'

3.3 Extract Shared InputComponent

Create: lib/sheet-input.tsx

import { Platform, TextInput } from 'react-native';
import { BottomSheetTextInput } from '@gorhom/bottom-sheet';

export const SheetInputComponent = Platform.OS === 'web' ? TextInput : BottomSheetTextInput;

Update files to import from shared location:

  • components/TaskCreationSheet.tsx
  • components/sheets/EditSavingsSheet.tsx
  • components/sheets/EnterInviteCodeSheet.tsx
  • components/sheets/TaskCompletionSheet.tsx

3.4 Section Dividers

File: lib/platform-icons.tsx

Add canonical section headers:

// =============================================================================
// Types & Interfaces
// =============================================================================

// =============================================================================
// Constants
// =============================================================================

// =============================================================================
// Helpers
// =============================================================================

3.5 Filter Empty Handle Values

File: components/profile/SymmetricRevealSection.tsx (lines 87-90)

const filteredHandles = Object.entries(otherHandles).filter(([_, v]) => Boolean(v));
const hasOtherHandles = filteredHandles.length > 0;

3.6 Test Size Verification

File: __tests__/lib/platform-icons.test.tsx (lines 78-81)

Render icon and verify size prop is applied.

3.7 Consolidate CHANGELOG

File: CHANGELOG.md

Merge duplicate ### Fixed sections under [Unreleased] into single section.


Implementation Order

  1. Phase 1 (Security) - Must be done first, affects data integrity
  2. Phase 2 (Performance) - User-facing improvements
  3. Phase 3 (Style) - Code quality, can be parallelized

Files Modified Summary

File Changes
supabase/migrations/20260117000000_sponsor_sponsee_connection_system.sql 3 security fixes
app/(app)/(tabs)/profile/index.tsx Privacy fix + naming
components/profile/FindSupportSection.tsx RLS fix + logger + naming
components/profile/PersistentInviteCard.tsx Timer + clipboard
components/profile/RelationshipCard.tsx Import alias
components/profile/SymmetricRevealSection.tsx Filter empty handles
components/settings/ExternalHandlesSection.tsx State separation
components/settings/SettingsContent.tsx Debounce + import
lib/sheet-input.tsx NEW - shared InputComponent
lib/platform-icons.tsx Section dividers
components/TaskCreationSheet.tsx Use shared InputComponent
components/sheets/EditSavingsSheet.tsx Use shared InputComponent
components/sheets/EnterInviteCodeSheet.tsx Use shared InputComponent
components/sheets/TaskCompletionSheet.tsx Use shared InputComponent
__tests__/lib/platform-icons.test.tsx Size verification
__tests__/lib/time-utils.test.ts Deterministic time
CHANGELOG.md Consolidate sections

Total: 16 files modified, 1 new file


Verification

After implementation:

pnpm format && pnpm lint && pnpm typecheck && pnpm build:web && pnpm test

Then update CHANGELOG.md with all changes under [Unreleased].

Mnehmos and others added 15 commits January 18, 2026 08:40
Prevents users from claiming invite codes that have been revoked by
the sponsor. Both USING and WITH CHECK clauses now verify revoked_at
IS NULL.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The previous RLS policy allowed any authenticated user to view profiles
of sponsors who had active invite codes. This was overly permissive.

Now replaced with get_sponsor_profile_by_invite_code() function that
requires the actual invite code to look up sponsor info, and returns
only non-sensitive fields (id, display_name, sobriety_date, avatar_url).

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The unconditional UNIQUE(seeker_id, provider_id) constraint prevented
users from ever rematching after a rejection or expiry. Now replaced
with a partial unique index that only applies when status = 'pending'.

This allows the app to create new match proposals between users who
previously had a rejected/expired match, while still preventing
duplicate pending matches.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Previously, external_handles were included in relationship queries
even before consent was verified, exposing contact info to the client
prematurely.

Changes:
- Profile queries now exclude external_handles
- Added get_handles_with_consent() RPC that verifies mutual consent
- SymmetricRevealSection fetches handles via RPC only when mutual
- Filter empty handle values before rendering

This ensures external_handles are never sent to the client unless
both parties in the relationship have opted in to reveal.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Providers (users with open_to_sponsoring intent) were attempting to
create matches with seeker_id set to other users, which violated the
RLS policy requiring seeker_id = auth.uid().

Now only seekers can directly create matches. Providers are shown to
seekers who then initiate the match request.

Also added LogCategory.DATABASE metadata to all logger calls in
FindSupportSection for better log categorization.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Previously, handleExternalHandlesChange triggered a database write on
every keystroke, causing API spam and poor UX. Now:

- Draft state provides immediate UI updates while typing
- Database writes are debounced (500ms delay)
- Inputs remain enabled during save (no blocking)
- Small "Saving..." indicator shows when persisting

This improves typing responsiveness and reduces Supabase load.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
PersistentInviteCard improvements:
- Timer now updates immediately when expires_at changes (not just on
  first render), fixing stale countdown display
- handleCopy now wrapped in try/catch with error toast for clipboard
  failures

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The togglePlatform function was mixing UI state updates with data
mutations by calling handleChange inside the setExpandedPlatforms
updater. Now properly separated:

1. Determine if collapsing first
2. Update UI state (expandedPlatforms)
3. Clear data separately when collapsing

This improves testability and follows React best practices for
state management.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The test was flaky because there was a race condition between creating
the timestamp with new Date().toISOString() and getTimeRemaining()
checking the current time - milliseconds could pass between these calls.

Now uses jest.useFakeTimers() and jest.setSystemTime() to make the
test deterministic by controlling the system clock.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Follow naming convention for boolean variables:
- FindSupportSection: disabled → isDisabled
- profile/index.tsx: loadingInviteCode → isLoadingInviteCode

This improves code readability and consistency with project guidelines.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- RelationshipCard: './SymmetricRevealSection' → '@/components/profile/SymmetricRevealSection'
- SettingsContent: './ExternalHandlesSection' → '@/components/settings/ExternalHandlesSection'

Consistent use of project alias for internal imports improves
refactoring safety and readability.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Create lib/sheet-input.tsx with platform-specific input component
- Update TaskCreationSheet to use shared SheetInputComponent
- Update EditSavingsSheet to use shared SheetInputComponent
- Update EnterInviteCodeSheet to use shared SheetInputComponent
- Update TaskCompletionSheet to use shared SheetInputComponent
- Remove duplicate InputComponent definitions from 4 sheet files

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add canonical section headers for better code organization:
- Imports
- Types & Interfaces
- Constants
- Helpers

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Verify custom size parameter is applied to icon props
- Add test for default size of 16 when not specified
- Use root.findByProps to inspect rendered icon properties

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
…ew fixes

- Merge duplicate Fixed sections under [Unreleased]
- Add entries for all PR VolvoxCommunity#302 CodeRabbit review fixes
- Add Changed entries for new shared utilities

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Copy link
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: 10

🤖 Fix all issues with AI agents
In `@app/`(app)/(tabs)/profile/index.tsx:
- Around line 658-678: The inline async onChange handler passed to
ConnectionIntentSelector should be extracted and memoized with useCallback to
avoid recreating the function each render; create a handler function (e.g.,
handleConnectionIntentChange) that accepts (intent: ConnectionIntent | null),
returns early if !profile, runs the supabase
.from('profiles').update(...).eq('id', profile.id), logs errors with logger and
showToast, and calls refreshProfile and showToast.success on success, then pass
that memoized handleConnectionIntentChange to ConnectionIntentSelector's
onChange prop and include dependencies (profile, supabase, refreshProfile,
showToast, logger, theme if used) in the useCallback dependency array.
- Around line 191-194: Replace the insecure Math.random() code generation in the
code variable with a call to expo-crypto: import Crypto from 'expo-crypto' and
use either Crypto.randomUUID() (then format/upper-case and truncate as needed)
or Crypto.getRandomBytesAsync() (encode to base36/base64 and slice) to produce
the 8-char uppercase invite code; update the code assignment in the same block
(where code is created and expiresAt set) to use the chosen Crypto method and
ensure you still set expiresAt the same way.

In `@components/profile/FindSupportSection.tsx`:
- Around line 291-294: The current rendering in FindSupportSection uses timeLeft
to show days/hours and yields "0h left" when days === 0 and hours === 0; change
the conditional rendering inside the Text where timeLeft is used to: if
timeLeft.days > 0 show "Xd Yh left", else if timeLeft.hours > 0 show "Xh left",
else if timeLeft.minutes > 0 show "Xm left", otherwise show "0h left" or
"Expired" as appropriate; update the JSX in FindSupportSection (the Text that
references timeLeft.days/timeLeft.hours) to implement this three-way check so
minutes are displayed when hours are zero.

In `@components/profile/SymmetricRevealSection.tsx`:
- Around line 100-131: The useEffect's async fetchHandles can update state after
unmount or when revealState changes; add a cancellation guard: create a local
let isMounted = true (or use AbortController if supabase supports it) inside the
effect, and in the cleanup set isMounted = false; before calling
setIsLoadingHandles, setOtherHandles, or any logger/state updates in
fetchHandles check isMounted and skip updates if false; ensure the cleanup runs
on revealState/relationshipId changes so fetchHandles won't write to an
unmounted component.

In `@lib/platform-icons.tsx`:
- Around line 12-26: Export the IconTheme interface so consumers can reference
it when calling getPlatformIcon; locate the IconTheme declaration in this file
and change it to an exported type/interface (export interface IconTheme { ... })
so callers can import IconTheme alongside PlatformKey and getPlatformIcon for
proper type-safe theme objects.
- Around line 32-41: The platformLabels map is currently typed as Record<string,
string> which is too permissive; change its type to Record<PlatformKey, string>
so the keys are constrained to the existing PlatformKey union/enum, update the
declaration of platformLabels accordingly (ensure PlatformKey is imported or
referenced in this file), and fix any downstream usages that assume arbitrary
string keys to satisfy the stricter type.

In `@lib/sheet-input.tsx`:
- Around line 1-12: Export typed aliases so consumers can annotate refs and
props for the platform-specific component: add exports like a SheetInputProps =
React.ComponentProps<typeof SheetInputComponent> and SheetInputRef =
React.ComponentRef<typeof SheetInputComponent> (or a single
SheetInputComponentType = typeof SheetInputComponent) alongside the existing
SheetInputComponent so callers can accurately type refs/props when using that
component.

In `@supabase/migrations/20260117000000_sponsor_sponsee_connection_system.sql`:
- Around line 300-305: The INSERT policy "Users can create match requests" on
public.connection_matches only checks seeker_id = auth.uid() and must also
enforce that the authenticated user has seeking intent; update the WITH CHECK
clause to require seeker_id = auth.uid() AND that the current user's intent
equals 'seeking' (for example by adding a subquery against your users/profiles
table: (seeker_id = auth.uid() AND (SELECT intent FROM public.users WHERE id =
auth.uid()) = 'seeking')), ensuring the policy references the same user-intent
column used elsewhere in the schema.
- Around line 127-149: The SECURITY DEFINER function
get_sponsor_profile_by_invite_code lacks a fixed search_path; to prevent schema
injection set the session search_path at the start of the function body (before
the RETURN QUERY) to a safe value such as "public, pg_temp" by calling
set_config('search_path', 'public, pg_temp', true) (or otherwise SET LOCAL
search_path) so all object lookups use the intended schemas while keeping
SECURITY DEFINER semantics.
- Around line 311-365: The function find_potential_matches is SECURITY DEFINER
and currently trusts the user_id parameter; to prevent any caller from querying
other users' matches, override/ignore the passed user_id and bind it to the
authenticated caller by assigning user_id := auth.uid() (or remove the user_id
parameter and use auth.uid() directly) at the start of the function body so all
subsequent logic (the SELECT into user_intent, the WHERE p.id != user_id checks,
and the relationship/match EXISTS checks) uses the authenticated user's UUID.
♻️ Duplicate comments (3)
components/profile/FindSupportSection.tsx (1)

40-42: Rename parameter for clarity.

The parameter visibleMatches is misleading since it actually receives a user ID. This was noted in a previous review.

♻️ Suggested fix
-function getUserRole(match: ConnectionMatch, visibleMatches: string): 'seeker' | 'provider' {
-  return match.seeker_id === visibleMatches ? 'seeker' : 'provider';
+function getUserRole(match: ConnectionMatch, userId: string): 'seeker' | 'provider' {
+  return match.seeker_id === userId ? 'seeker' : 'provider';
 }
app/(app)/(tabs)/profile/index.tsx (2)

213-217: Fix inconsistent error message in toast.

The function is regenerateInviteCode but the error toast says "Failed to generate invite code". This was flagged in a previous review.

🐛 Proposed fix
-      showToast.error('Failed to generate invite code');
+      showToast.error('Failed to regenerate invite code');

66-68: Setter naming inconsistent with boolean naming convention.

The state variable isLoadingInviteCode follows the is prefix convention, but the setter is still setLoadingInviteCode instead of setIsLoadingInviteCode. As per coding guidelines, boolean variables should use is/has/should prefix consistently.

♻️ Suggested fix
-const [isLoadingInviteCode, setLoadingInviteCode] = useState(false);
+const [isLoadingInviteCode, setIsLoadingInviteCode] = useState(false);

Then update all usages of setLoadingInviteCode to setIsLoadingInviteCode throughout the file.

Comment on lines +1 to +12
/**
* Platform-specific input component for bottom sheets.
*
* On web, uses regular TextInput to avoid BottomSheetTextInput compatibility issues.
* On native, uses BottomSheetTextInput for proper keyboard handling in sheets.
*/

import { Platform, TextInput } from 'react-native';
import { BottomSheetTextInput } from '@gorhom/bottom-sheet';

// Use regular TextInput on web to avoid BottomSheetTextInput compatibility issues
export const SheetInputComponent = Platform.OS === 'web' ? TextInput : BottomSheetTextInput;
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

LGTM! Clean platform-specific abstraction.

The conditional component selection correctly addresses web/native compatibility for bottom sheet inputs. This DRY extraction benefits all sheet components.

Consider exporting the component type for consumers who need to type refs or props:

♻️ Optional type export
 import { Platform, TextInput } from 'react-native';
 import { BottomSheetTextInput } from '@gorhom/bottom-sheet';

+/** Type alias for the platform-specific input component */
+export type SheetInputComponentType = typeof TextInput | typeof BottomSheetTextInput;
+
 // Use regular TextInput on web to avoid BottomSheetTextInput compatibility issues
 export const SheetInputComponent = Platform.OS === 'web' ? TextInput : BottomSheetTextInput;
📝 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
/**
* Platform-specific input component for bottom sheets.
*
* On web, uses regular TextInput to avoid BottomSheetTextInput compatibility issues.
* On native, uses BottomSheetTextInput for proper keyboard handling in sheets.
*/
import { Platform, TextInput } from 'react-native';
import { BottomSheetTextInput } from '@gorhom/bottom-sheet';
// Use regular TextInput on web to avoid BottomSheetTextInput compatibility issues
export const SheetInputComponent = Platform.OS === 'web' ? TextInput : BottomSheetTextInput;
/**
* Platform-specific input component for bottom sheets.
*
* On web, uses regular TextInput to avoid BottomSheetTextInput compatibility issues.
* On native, uses BottomSheetTextInput for proper keyboard handling in sheets.
*/
import { Platform, TextInput } from 'react-native';
import { BottomSheetTextInput } from '@gorhom/bottom-sheet';
/** Type alias for the platform-specific input component */
export type SheetInputComponentType = typeof TextInput | typeof BottomSheetTextInput;
// Use regular TextInput on web to avoid BottomSheetTextInput compatibility issues
export const SheetInputComponent = Platform.OS === 'web' ? TextInput : BottomSheetTextInput;
🤖 Prompt for AI Agents
In `@lib/sheet-input.tsx` around lines 1 - 12, Export typed aliases so consumers
can annotate refs and props for the platform-specific component: add exports
like a SheetInputProps = React.ComponentProps<typeof SheetInputComponent> and
SheetInputRef = React.ComponentRef<typeof SheetInputComponent> (or a single
SheetInputComponentType = typeof SheetInputComponent) alongside the existing
SheetInputComponent so callers can accurately type refs/props when using that
component.

Comment on lines +127 to +149
CREATE OR REPLACE FUNCTION public.get_sponsor_profile_by_invite_code(invite_code TEXT)
RETURNS TABLE (
id uuid,
display_name text,
sobriety_date date,
avatar_url text
) AS $$
BEGIN
RETURN QUERY
SELECT
p.id,
p.display_name,
p.sobriety_date,
p.avatar_url
FROM public.profiles p
JOIN public.invite_codes ic ON ic.sponsor_id = p.id
WHERE ic.code = invite_code
AND ic.expires_at > now()
AND ic.used_by IS NULL
AND ic.revoked_at IS NULL
LIMIT 1;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

SECURITY DEFINER functions should set a safe search_path

Without an explicit search_path, SECURITY DEFINER functions can be tricked into resolving objects in unintended schemas. Set a fixed path (e.g., public, pg_temp) for each SECURITY DEFINER function.

🔒 Suggested pattern (apply to all SECURITY DEFINER functions)
-$$ LANGUAGE plpgsql SECURITY DEFINER;
+$$ LANGUAGE plpgsql SECURITY DEFINER
+   SET search_path = public, pg_temp;
🤖 Prompt for AI Agents
In `@supabase/migrations/20260117000000_sponsor_sponsee_connection_system.sql`
around lines 127 - 149, The SECURITY DEFINER function
get_sponsor_profile_by_invite_code lacks a fixed search_path; to prevent schema
injection set the session search_path at the start of the function body (before
the RETURN QUERY) to a safe value such as "public, pg_temp" by calling
set_config('search_path', 'public, pg_temp', true) (or otherwise SET LOCAL
search_path) so all object lookups use the intended schemas while keeping
SECURITY DEFINER semantics.

Comment on lines +300 to +305
-- Only authenticated users with seeking intent can create match requests
-- (In practice, this would be done via a server function, but policy allows it)
DROP POLICY IF EXISTS "Users can create match requests" ON public.connection_matches;
CREATE POLICY "Users can create match requests"
ON public.connection_matches FOR INSERT TO authenticated
WITH CHECK (seeker_id = auth.uid());
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Match INSERT policy doesn’t enforce intent constraints

The policy only checks seeker_id = auth.uid(), allowing users with any intent to create match requests for any provider. That contradicts intent-driven matching.

🐛 Proposed fix
 CREATE POLICY "Users can create match requests"
   ON public.connection_matches FOR INSERT TO authenticated
-  WITH CHECK (seeker_id = auth.uid());
+  WITH CHECK (
+    seeker_id = auth.uid()
+    AND seeker_id <> provider_id
+    AND EXISTS (
+      SELECT 1 FROM public.profiles p
+      WHERE p.id = auth.uid()
+        AND p.connection_intent IN ('seeking_sponsor', 'open_to_both')
+    )
+    AND EXISTS (
+      SELECT 1 FROM public.profiles p
+      WHERE p.id = provider_id
+        AND p.connection_intent IN ('open_to_sponsoring', 'open_to_both')
+    )
+  );
🤖 Prompt for AI Agents
In `@supabase/migrations/20260117000000_sponsor_sponsee_connection_system.sql`
around lines 300 - 305, The INSERT policy "Users can create match requests" on
public.connection_matches only checks seeker_id = auth.uid() and must also
enforce that the authenticated user has seeking intent; update the WITH CHECK
clause to require seeker_id = auth.uid() AND that the current user's intent
equals 'seeking' (for example by adding a subquery against your users/profiles
table: (seeker_id = auth.uid() AND (SELECT intent FROM public.users WHERE id =
auth.uid()) = 'seeking')), ensuring the policy references the same user-intent
column used elsewhere in the schema.

Comment on lines +311 to +365
-- Function to find users with complementary intents
CREATE OR REPLACE FUNCTION public.find_potential_matches(user_id uuid, max_results integer DEFAULT 5)
RETURNS TABLE (
matched_user_id uuid,
matched_intent connection_intent_type,
display_name text
) AS $$
DECLARE
user_intent connection_intent_type;
BEGIN
-- Get the requesting user's intent
SELECT connection_intent INTO user_intent
FROM public.profiles
WHERE id = user_id;

-- Return empty if user has no intent set or is not looking
IF user_intent IS NULL OR user_intent = 'not_looking' THEN
RETURN;
END IF;

RETURN QUERY
SELECT
p.id as matched_user_id,
p.connection_intent as matched_intent,
p.display_name
FROM public.profiles p
WHERE
p.id != user_id
-- Match complementary intents
AND (
-- User is seeking sponsor -> match with those open to sponsoring
(user_intent IN ('seeking_sponsor', 'open_to_both')
AND p.connection_intent IN ('open_to_sponsoring', 'open_to_both'))
OR
-- User is open to sponsoring -> match with those seeking sponsor
(user_intent IN ('open_to_sponsoring', 'open_to_both')
AND p.connection_intent IN ('seeking_sponsor', 'open_to_both'))
)
-- Exclude users with ACTIVE relationships only (allow re-matching after disconnect)
AND NOT EXISTS (
SELECT 1 FROM public.sponsor_sponsee_relationships r
WHERE r.status = 'active'
AND ((r.sponsor_id = user_id AND r.sponsee_id = p.id)
OR (r.sponsor_id = p.id AND r.sponsee_id = user_id))
)
-- Exclude users with pending matches with this user
AND NOT EXISTS (
SELECT 1 FROM public.connection_matches m
WHERE m.status = 'pending'
AND ((m.seeker_id = user_id AND m.provider_id = p.id)
OR (m.seeker_id = p.id AND m.provider_id = user_id))
)
LIMIT max_results;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

find_potential_matches can be invoked for any user_id

Because the function is SECURITY DEFINER and accepts a user_id param, any authenticated user can query other users’ potential matches. Bind the function to auth.uid().

🐛 Proposed fix
 CREATE OR REPLACE FUNCTION public.find_potential_matches(user_id uuid, max_results integer DEFAULT 5)
 RETURNS TABLE (
   matched_user_id uuid,
   matched_intent connection_intent_type,
   display_name text
 ) AS $$
 DECLARE
   user_intent connection_intent_type;
+  current_user_id uuid;
 BEGIN
+  current_user_id := auth.uid();
+  IF user_id IS DISTINCT FROM current_user_id THEN
+    RAISE EXCEPTION 'Cannot query matches for another user';
+  END IF;
   -- Get the requesting user's intent
-  SELECT connection_intent INTO user_intent
-  FROM public.profiles
-  WHERE id = user_id;
+  SELECT connection_intent INTO user_intent
+  FROM public.profiles
+  WHERE id = current_user_id;
@@
-    p.id != user_id
+    p.id != current_user_id
🤖 Prompt for AI Agents
In `@supabase/migrations/20260117000000_sponsor_sponsee_connection_system.sql`
around lines 311 - 365, The function find_potential_matches is SECURITY DEFINER
and currently trusts the user_id parameter; to prevent any caller from querying
other users' matches, override/ignore the passed user_id and bind it to the
authenticated caller by assigning user_id := auth.uid() (or remove the user_id
parameter and use auth.uid() directly) at the start of the function body so all
subsequent logic (the SELECT into user_intent, the WHERE p.id != user_id checks,
and the relationship/match EXISTS checks) uses the authenticated user's UUID.

Security:
- Replace Math.random() with expo-crypto for cryptographically secure invite codes

Performance:
- Memoize handleConnectionIntentChange with useCallback

Bug fixes:
- Fix timer display not showing minutes when hours = 0
- Fix potential memory leak in SymmetricRevealSection with isMounted guard

Type safety:
- Export IconTheme interface from lib/platform-icons.tsx
- Strengthen platformLabels typing with Record<PlatformKey, string>

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@components/profile/SymmetricRevealSection.tsx`:
- Around line 1-145: The effect currently uses a local isMounted boolean (in
useEffect) which can close over stale state; replace that pattern with a
ref-based mount guard plus a per-effect local isActive flag: create a const
isMountedRef = useRef(true) at component scope, update it in a useEffect cleanup
to false, and inside the existing useEffect for fetchHandles keep a local let
isActive = true (or check isMountedRef.current) before updating state in
fetchHandles; ensure you reference isMountedRef.current when guarding
setOtherHandles and setIsLoadingHandles and remove the local isMounted variable
so fetchHandles, supabase.rpc('get_handles_with_consent'), setOtherHandles and
setIsLoadingHandles use the ref-based guard.
♻️ Duplicate comments (1)
components/profile/FindSupportSection.tsx (1)

37-42: Rename visibleMatches to userId for clarity. The parameter is the current user's id, not a list, so the current name is misleading.

♻️ Suggested rename
-function getUserRole(match: ConnectionMatch, visibleMatches: string): 'seeker' | 'provider' {
-  return match.seeker_id === visibleMatches ? 'seeker' : 'provider';
+function getUserRole(match: ConnectionMatch, userId: string): 'seeker' | 'provider' {
+  return match.seeker_id === userId ? 'seeker' : 'provider';
}

Replace local isMounted boolean with ref-based pattern:
- Add isMountedRef at component scope for persistent mount state
- Add cleanup useEffect to set ref to false on unmount
- Keep per-effect isActive flag for effect cancellation
- Guard state updates with both isActive and isMountedRef.current

This prevents stale closure issues that could occur with the
local isMounted pattern when effect dependencies change.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

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

⚠️ Outside diff range comments (1)
CHANGELOG.md (1)

95-125: Add a Security section and move security fixes out of Fixed.

The changelog guidelines require a Security category, and the RLS/privacy fixes (Line 110-114) are security items. Please add a ### Security section and relocate those entries so they’re surfaced properly. As per coding guidelines, ...

🛠️ Suggested changelog adjustment
 ### Fixed
@@
-- Fix UPDATE policy missing `revoked_at` check in invite_codes RLS migration
-- Fix overly broad RLS policy for profile viewing via invite code, replaced with SECURITY DEFINER RPC
-- Fix UNIQUE constraint preventing sponsor/sponsee rematches after disconnect, converted to partial index
-- Fix external handles exposure in profile queries before consent check
-- Fix RLS violation when providers create connection matches directly
 - Fix external handles DB write on every keystroke, added debouncing with draft state
 - Fix timer showing stale values on mount, now updates immediately when expires_at changes
@@
 - Fix potential memory leak in SymmetricRevealSection using ref-based mount guard pattern
+
+### Security
+
+- Fix UPDATE policy missing `revoked_at` check in invite_codes RLS migration
+- Fix overly broad RLS policy for profile viewing via invite code, replaced with SECURITY DEFINER RPC
+- Fix external handles exposure in profile queries before consent check
+- Fix RLS violation when providers create connection matches directly
🤖 Fix all issues with AI agents
In `@components/profile/SymmetricRevealSection.tsx`:
- Around line 22-40: The prop names in SymmetricRevealSectionProps aren't
following the is/has/should boolean convention—rename myConsent -> hasMyConsent,
theirConsent -> hasTheirConsent, and disabled -> isDisabled in the
SymmetricRevealSectionProps interface and update all usages inside the
SymmetricRevealSection component (props destructuring and JSX references) and
any parent call sites that pass these props; ensure TypeScript types and the
onConsentChange handler signature remain unchanged and run a project-wide
replace for the old symbols (myConsent, theirConsent, disabled) to the new ones
to keep code consistent.

Align SymmetricRevealSection props with naming conventions:
- myConsent → hasMyConsent
- theirConsent → hasTheirConsent
- disabled → isDisabled

Updated props interface, JSDoc example, helper function params,
component destructuring, JSX usages, and RelationshipCard call site.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Copy link
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

🤖 Fix all issues with AI agents
In `@CHANGELOG.md`:
- Around line 81-83: Update the three "Changed" entries in CHANGELOG.md (the
lines referencing lib/platform-icons.tsx, platform-icons test suite, and
expo-crypto invite code generation) so they are either moved to the "Added"
section if they truly introduce new content, or reworded to reflect a
change/improvement; for example, replace "Add canonical section dividers to
`lib/platform-icons.tsx`" with "Improve `lib/platform-icons.tsx` organization
with canonical section dividers", replace "Add size verification tests to
platform-icons test suite" with "Enhance platform-icons test suite with size
verification tests", and replace "Replace `Math.random()` with cryptographically
secure `expo-crypto` for invite code generation" with "Switch invite code
generation to cryptographically secure `expo-crypto` (replacing
`Math.random()`)" so the entries belong correctly in the "Changed" section.
- Line 120: Move the changelog entry "Fix boolean prop naming inconsistency
(myConsent → hasMyConsent, theirConsent → hasTheirConsent, disabled →
isDisabled, loadingInviteCode → isLoadingInviteCode)" out of the "Fixed" section
and add it to the "Changed" section alongside other refactors; remove the
original line from "Fixed" so it no longer appears there and ensure the entry
formatting matches other "Changed" items (same tense/style) and is placed near
the existing refactor entries (the ones around the prior refactor block).

In `@components/profile/SymmetricRevealSection.tsx`:
- Around line 114-117: When revealState switches away from 'mutual' in the
SymmetricRevealSection component the cleanup currently calls setOtherHandles({})
but doesn't reset the isLoadingHandles flag, which can remain true if a prior
fetch was cancelled; update the same branch where setOtherHandles({}) is called
to also call setIsLoadingHandles(false) (or the relevant state setter for
isLoadingHandles) so loading state is cleared whenever revealState !== 'mutual'.

Comment on lines +81 to +83
- Add canonical section dividers to `lib/platform-icons.tsx` for better code organization
- Add size verification tests to platform-icons test suite
- Replace `Math.random()` with cryptographically secure `expo-crypto` for invite code generation
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Reword entries using "Add" in the Changed section.

Lines 81-83 use the verb "Add" within the "Changed" section, which creates ambiguity. Entries that add new content typically belong in the "Added" section. Either:

  • Move these entries to the "Added" section if they represent new content, or
  • Reword them to clarify the change (e.g., "Improve lib/platform-icons.tsx organization with section dividers" or "Enhance platform-icons test suite with size verification tests").

As per coding guidelines, CHANGELOG entries should be clear and readable for humans.

♻️ Suggested rewording
-- Add canonical section dividers to `lib/platform-icons.tsx` for better code organization
-- Add size verification tests to platform-icons test suite
+- Improve `lib/platform-icons.tsx` organization with canonical section dividers
+- Enhance platform-icons test suite with size verification tests
📝 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
- Add canonical section dividers to `lib/platform-icons.tsx` for better code organization
- Add size verification tests to platform-icons test suite
- Replace `Math.random()` with cryptographically secure `expo-crypto` for invite code generation
- Improve `lib/platform-icons.tsx` organization with canonical section dividers
- Enhance platform-icons test suite with size verification tests
- Replace `Math.random()` with cryptographically secure `expo-crypto` for invite code generation
🤖 Prompt for AI Agents
In `@CHANGELOG.md` around lines 81 - 83, Update the three "Changed" entries in
CHANGELOG.md (the lines referencing lib/platform-icons.tsx, platform-icons test
suite, and expo-crypto invite code generation) so they are either moved to the
"Added" section if they truly introduce new content, or reworded to reflect a
change/improvement; for example, replace "Add canonical section dividers to
`lib/platform-icons.tsx`" with "Improve `lib/platform-icons.tsx` organization
with canonical section dividers", replace "Add size verification tests to
platform-icons test suite" with "Enhance platform-icons test suite with size
verification tests", and replace "Replace `Math.random()` with cryptographically
secure `expo-crypto` for invite code generation" with "Switch invite code
generation to cryptographically secure `expo-crypto` (replacing
`Math.random()`)" so the entries belong correctly in the "Changed" section.

- Fix clipboard copy errors not handled in PersistentInviteCard
- Fix togglePlatform UI/data state mixing in ExternalHandlesSection
- Fix flaky time-utils test by using deterministic fake timers
- Fix boolean prop naming inconsistency (myConsent → hasMyConsent, theirConsent → hasTheirConsent, disabled → isDisabled, loadingInviteCode → isLoadingInviteCode)
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Consider moving to "Changed" section.

Line 120 describes a code quality/style improvement (renaming props for consistency) rather than a functional bug fix. The PR objectives categorize this as "Phase 3 — Code quality & style" work. Consider moving this entry to the "Changed" section to better reflect its nature as a refactoring/consistency improvement, similar to how other refactors (lines 78-86) are categorized.

The "Fixed" section is typically reserved for actual bugs that affect functionality, while code quality and naming consistency improvements belong in "Changed".

♻️ Suggested change

Move this entry from the "Fixed" section to the "Changed" section:

 - Memoize `handleConnectionIntentChange` with `useCallback` for performance optimization
 - Export `IconTheme` interface from `lib/platform-icons.tsx` for type safety
 - Strengthen `platformLabels` typing with `Record<PlatformKey, string>` constraint
+- Improve boolean prop naming consistency with `is`/`has` prefixes (myConsent → hasMyConsent, theirConsent → hasTheirConsent, disabled → isDisabled, loadingInviteCode → isLoadingInviteCode)

 ### Removed

And remove from "Fixed" section.

🤖 Prompt for AI Agents
In `@CHANGELOG.md` at line 120, Move the changelog entry "Fix boolean prop naming
inconsistency (myConsent → hasMyConsent, theirConsent → hasTheirConsent,
disabled → isDisabled, loadingInviteCode → isLoadingInviteCode)" out of the
"Fixed" section and add it to the "Changed" section alongside other refactors;
remove the original line from "Fixed" so it no longer appears there and ensure
the entry formatting matches other "Changed" items (same tense/style) and is
placed near the existing refactor entries (the ones around the prior refactor
block).

Comment on lines +114 to +117
if (revealState !== 'mutual') {
setOtherHandles({});
return;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Reset loading state when consent is not mutual.

When revealState switches away from mutual, isLoadingHandles can stay true if a prior fetch was in-flight and got cancelled. Clear it alongside otherHandles to avoid stale loading state.

🩹 Proposed fix
     if (revealState !== 'mutual') {
       setOtherHandles({});
+      setIsLoadingHandles(false);
       return;
     }
🤖 Prompt for AI Agents
In `@components/profile/SymmetricRevealSection.tsx` around lines 114 - 117, When
revealState switches away from 'mutual' in the SymmetricRevealSection component
the cleanup currently calls setOtherHandles({}) but doesn't reset the
isLoadingHandles flag, which can remain true if a prior fetch was cancelled;
update the same branch where setOtherHandles({}) is called to also call
setIsLoadingHandles(false) (or the relevant state setter for isLoadingHandles)
so loading state is cleared whenever revealState !== 'mutual'.

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.

feat: Sponsor/Sponsee Connection System with Intent-Based Matching

1 participant