-
Notifications
You must be signed in to change notification settings - Fork 4
feat: Sponsor/Sponsee Connection System with Intent & Ownership (closes #300) #302
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
feat: Sponsor/Sponsee Connection System with Intent & Ownership (closes #300) #302
Conversation
…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>
|
@Mnehmos is attempting to deploy a commit to the Volvox Team on Vercel. A member of the Team first needs to authorize it. |
|
Note Other AI code review bot(s) detectedCodeRabbit 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. 📝 WalkthroughSummary by CodeRabbit
✏️ Tip: You can customize this high-level summary in your review settings. WalkthroughAdds 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
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
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
Suggested labels
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧪 Generate unit tests (beta)
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.mdmarkdownlint-cli2 v0.18.1 (markdownlint v0.38.0) 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. Comment |
| /** | ||
| * Format the user's role in this match (seeker or provider). | ||
| */ | ||
| function getUserRole(match: ConnectionMatch, visibleMatches: string): 'seeker' | 'provider' { |
There was a problem hiding this comment.
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'); |
There was a problem hiding this comment.
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').
| showToast.error('Failed to generate invite code'); | |
| showToast.error('Failed to regenerate invite code'); |
There was a problem hiding this 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: MoveInputComponentbelow 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(*)')andselect('*, sponsee:sponsee_id(*)')pull full profile rows, which includeexternal_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.
| const [activeInviteCode, setActiveInviteCode] = useState<InviteCode | null>(null); | ||
| const [loadingInviteCode, setLoadingInviteCode] = useState(false); | ||
| const [pendingMatches, setPendingMatches] = useState<ConnectionMatch[]>([]); |
There was a problem hiding this comment.
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.
| 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.
| 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; | ||
|
|
There was a problem hiding this comment.
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.
supabase/migrations/20260117000000_sponsor_sponsee_connection_system.sql
Show resolved
Hide resolved
supabase/migrations/20260117000000_sponsor_sponsee_connection_system.sql
Outdated
Show resolved
Hide resolved
supabase/migrations/20260117000000_sponsor_sponsee_connection_system.sql
Outdated
Show resolved
Hide resolved
PR #302 Review Implementation PlanOverviewCodeRabbit 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 FixesFile:
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 ExposureFile: Problem: Fetches all profile fields including 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 1.3 RLS Violation for Provider InsertsFile: Problem: When Fix: Route all inserts through the existing Phase 2: Performance & UX Fixes (Medium)2.1 Debounce External Handles ChangesFile: Problem: DB write on every keystroke. Fix:
2.2 Timer & Clipboard FixesFile:
2.3 Logger Category MetadataFile: Fix: Add 2.4 togglePlatform State SeparationFile: 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 TestFile: Fix: Use Phase 3: Code Quality & Style Fixes (Low)3.1 Boolean Naming Convention
3.2 Import Alias Fixes
3.3 Extract Shared InputComponentCreate: 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:
3.4 Section DividersFile: Add canonical section headers: // =============================================================================
// Types & Interfaces
// =============================================================================
// =============================================================================
// Constants
// =============================================================================
// =============================================================================
// Helpers
// =============================================================================3.5 Filter Empty Handle ValuesFile: const filteredHandles = Object.entries(otherHandles).filter(([_, v]) => Boolean(v));
const hasOtherHandles = filteredHandles.length > 0;3.6 Test Size VerificationFile: Render icon and verify size prop is applied. 3.7 Consolidate CHANGELOGFile: Merge duplicate Implementation Order
Files Modified Summary
Total: 16 files modified, 1 new file VerificationAfter implementation: pnpm format && pnpm lint && pnpm typecheck && pnpm build:web && pnpm testThen update CHANGELOG.md with all changes under |
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>
There was a problem hiding this 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
visibleMatchesis 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
regenerateInviteCodebut 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
isLoadingInviteCodefollows theisprefix convention, but the setter is stillsetLoadingInviteCodeinstead ofsetIsLoadingInviteCode. 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
setLoadingInviteCodetosetIsLoadingInviteCodethroughout the file.
| /** | ||
| * 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; |
There was a problem hiding this comment.
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.
| /** | |
| * 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.
| 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; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| -- 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()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| -- 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; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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>
There was a problem hiding this 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: RenamevisibleMatchestouserIdfor 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>
There was a problem hiding this 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 aSecuritysection and move security fixes out ofFixed.The changelog guidelines require a
Securitycategory, and the RLS/privacy fixes (Line 110-114) are security items. Please add a### Securitysection 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>
There was a problem hiding this 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'.
| - 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 |
There was a problem hiding this comment.
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.tsxorganization 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.
| - 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) |
There was a problem hiding this comment.
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)
### RemovedAnd 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).
| if (revealState !== 'mutual') { | ||
| setOtherHandles({}); | ||
| return; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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'.
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
not_looking,seeking_sponsor,open_to_sponsoring, oropen_to_bothConnectionIntentSelectorcomponent on profile screenOpt-in Matching
FindSupportSectioncomponent for discovering compatible matchesconnection_matchestable with RLS policiesPersistent Invite Codes
PersistentInviteCardshowing active invite code with expiration timerrevoked_atcolumn for manual revocationSymmetric Contact Reveal
SymmetricRevealSectionfor mutual consent contact sharingExternalHandlesSectionin Settings for managing private contact info (Discord, Telegram, WhatsApp, Signal, Phone)external_handlesJSONB field on profilessponsor_reveal_consentandsponsee_reveal_consenton relationshipsDatabase Changes
New migration:
20260117000000_sponsor_sponsee_connection_system.sqlconnection_intent_typeenummatch_status_typeenumconnection_matchestable with bilateral acceptancefind_potential_matches()RPC functionaccept_match()andreject_match()RPC functionsprofileswithconnection_intent,external_handlessponsor_sponsee_relationshipswith reveal consent fieldsinvite_codeswithrevoked_at,intentRefactoring (DRY)
lib/time-utils.tswithgetTimeRemaining(),formatTimeRemaining()lib/platform-icons.tsxwithgetPlatformIcon(),getPlatformLabel()Test Plan
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.
not_looking,seeking_sponsor,open_to_sponsoring,open_to_both).ConnectionIntentSelectorfor setting user intent.FindSupportSectionfor discovering matches.PersistentInviteCardfor managing invite codes.SymmetricRevealSectionfor contact sharing.20260117000000_sponsor_sponsee_connection_system.sql.connection_intent_typeandmatch_status_typeenums.connection_matchestable and related RPC functions.profiles,sponsor_sponsee_relationships, andinvite_codestables.lib/time-utils.tsandlib/platform-icons.tsxfor utility functions.This description was created by
for ca23d0d. You can customize this summary. It will automatically update as commits are pushed.
#302 (comment)