Skip to content

Comments

Fixed Issues with Invites and Friends#5

Merged
fortune710 merged 11 commits intomainfrom
dev
Dec 12, 2025
Merged

Fixed Issues with Invites and Friends#5
fortune710 merged 11 commits intomainfrom
dev

Conversation

@fortune710
Copy link
Owner

@fortune710 fortune710 commented Dec 12, 2025

Summary by CodeRabbit

  • New Features

    • Real invite flow: fetches and displays real invite links with share/copy and usage limits; new hook to fetch a user’s invite; async invite-code generation.
    • Friend acceptance now triggers real invite-accept workflow and disables UI while processing.
  • Bug Fixes

    • Safer signup/onboarding flow with fallback when userId is missing; prevents self-invite acceptance.
    • Improved delete-account sign-in message.
  • Other Changes

    • Added contact/audio permissions, runtime/updates config, build channels, dependency for updates, cache expiration, project rename, and UI badge refinements.

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

@vercel
Copy link

vercel bot commented Dec 12, 2025

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

Project Deployment Preview Comments Updated (UTC)
keepsafe Ready Ready Preview Comment Dec 12, 2025 5:23am

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Dec 12, 2025

Walkthrough

Adds an InviteService, async invite code generation, and useUserInvite; signUp now returns userId and upserts profiles; onboarding invite UI replaced with data-driven flows and loading/error branches; several hooks, components, config, and permission updates across the frontend.

Changes

Cohort / File(s) Summary
Auth / Profile
frontend/hooks/use-auth.ts, frontend/providers/auth-provider.tsx, frontend/hooks/use-profile.ts
signUp now returns { data?: { userId } }; signUp upserts profiles via TABLES.PROFILES; updateProfile type updated to Partial<ProfileUpdate>; added ProfileUpdate type alias.
Invite Service & Utilities
frontend/services/invite-service.ts, frontend/lib/utils.ts, frontend/services/friend-service.ts
New InviteService with create/get/update and MAX_INVITE_USES; generateInviteCode made async using expo-crypto and rejection sampling; FriendService now awaits external generator and removed its private generator.
Onboarding / Invite UI
frontend/app/onboarding/auth.tsx, frontend/app/onboarding/invite.tsx
Auth navigation now passes userId; invite screen uses useUserInvite(userId) to fetch invite, adds loading/error/missing-user flows, replaces mock invite generation, updates UI actions and copy/share behavior.
Hooks & Acceptance Flow
frontend/hooks/use-user-invite.ts, frontend/hooks/use-invite-acceptance.ts, frontend/hooks/use-friends.ts, frontend/hooks/use-auth.ts
Added useUserInvite hook (react-query); acceptInvite validates inputs, forbids self-connection, creates friendship with PENDING status; added DEV logging around friendship updates; signUp return type changed.
Components / UI tweaks
frontend/components/friends-section.tsx, frontend/components/friends/suggested-friend-item.tsx, frontend/components/friends/suggested-friends-list.tsx
Connected badge uses connectedFriends.length; pending count moved to separate badge; SuggestedFriendItem now requires index, uses auth, invite acceptance, toast feedback, and disables Add while processing; list now passes index.
Storage, Settings & Providers
frontend/services/device-storage.ts, frontend/app/settings/index.tsx, frontend/providers/auth-provider.tsx
suggested_friends stored with 7-day expiry; settings delete-account message updated; AuthContext signUp signature updated to include optional data.userId.
Config / Build
frontend/app.json, frontend/eas.json, frontend/package.json
Added CAMERA, READ_CONTACTS, MODIFY_AUDIO_SETTINGS permissions; added runtimeVersion and updates block in app.json; eas.json build profiles include channel; package renamed to keepsafe and expo-updates dependency added.
Other / Minor
frontend/components/*, frontend/hooks/*, frontend/app/*
Import cleanups, small formatting and comment changes across multiple files; no additional exported API breaks beyond listed signature/type changes.

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant AuthScreen as Auth Screen
    participant AuthHook as useAuth / signUp
    participant Supabase
    participant InviteScreen as Invite Screen
    participant InviteHook as useUserInvite
    participant InviteService
    participant FriendService

    User->>AuthScreen: Submit signup (email, password)
    AuthScreen->>AuthHook: signUp(...)
    AuthHook->>Supabase: Create auth user
    Supabase-->>AuthHook: Return user record (id)
    AuthHook->>Supabase: Upsert profile (TABLES.PROFILES)
    Supabase-->>AuthHook: Profile upsert result
    AuthHook-->>AuthScreen: Return { error:null, data:{ userId } }

    AuthScreen->>InviteScreen: Navigate with user_id
    InviteScreen->>InviteHook: useUserInvite(userId)
    InviteHook->>InviteService: getInvite(userId)
    InviteService->>Supabase: Query invites by inviter_id
    Supabase-->>InviteService: Invite record
    InviteService-->>InviteHook: Invite data
    InviteHook-->>InviteScreen: { invite, isLoading:false }

    InviteScreen->>User: Render invite link / Share / Continue
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

  • Areas needing careful review:
    • frontend/services/invite-service.ts — upsert/query error handling and edge cases
    • frontend/lib/utils.ts — async crypto-based generator and rejection sampling correctness
    • frontend/app/onboarding/invite.tsx — loading/error/missing-user branches and deep-link construction
    • frontend/hooks/use-invite-acceptance.ts — input validation and change to PENDING status semantics
    • Type/signature changes: use-auth.ts, providers/auth-provider.tsx, use-profile.ts (ensure callers updated)
    • Component prop changes: SuggestedFriendItem now requires index and removes onAccept callback

Poem

🐰 I hopped through bytes and signed a name,
A code was spun from crypto's flame,
Invites arose, links sent with cheer,
Badges blinked — new friends draw near,
The rabbit hops on — deployment's near!

Pre-merge checks and finishing touches

❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
Title check ❓ Inconclusive The title 'Fixed Issues with Invites and Friends' is vague and does not clearly describe the specific changes in the pull request. While it references features mentioned in the PR (invites and friends), it uses non-descriptive language ('Fixed Issues') that obscures the actual scope of work. Replace with a more specific title that describes the main changes, such as 'Implement real invite flow and update friend suggestion acceptance logic' or 'Connect invite system with real data and profile management'.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch dev

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 40b5852 and 3c9a235.

📒 Files selected for processing (3)
  • frontend/app/settings/index.tsx (1 hunks)
  • frontend/components/friends/suggested-friend-item.tsx (2 hunks)
  • frontend/components/friends/suggested-friends-list.tsx (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • frontend/app/settings/index.tsx
🧰 Additional context used
🧬 Code graph analysis (1)
frontend/components/friends/suggested-friends-list.tsx (1)
frontend/components/friends/suggested-friend-item.tsx (1)
  • SuggestedFriendItem (21-68)

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

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

Caution

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

⚠️ Outside diff range comments (4)
frontend/providers/auth-provider.tsx (1)

15-23: Align updateProfile type with the new profiles.Update shape (avoid misleading callers).
Right now the context advertises Partial<Profile> which implies fields that shouldn’t be writable; but useProfile.updateProfile is (correctly) narrower.

Suggested fix:

 import { Database } from '@/types/database';

 type Profile = Database['public']['Tables']['profiles']['Row'];
+type ProfileUpdate = Database['public']['Tables']['profiles']['Update'];

 interface AuthContextType {
   ...
-  updateProfile: (updates: Partial<Profile>) => Promise<{ error: Error | null }>;
+  updateProfile: (updates: Partial<ProfileUpdate>) => Promise<{ error: Error | null }>;
   refreshProfile: () => Promise<void>;
 }
frontend/app/onboarding/auth.tsx (1)

40-55: Remove debug logging from sign-in error path.
console.log(error,"errorrr") is likely leftover and can leak details into device logs.

       const { error } = await signIn(email, password);
       if (error) {
-        console.log(error,"errorrr")
         // Handle custom error types with specific messages
         if (error instanceof EmailNotVerifiedError) {
frontend/hooks/use-auth.ts (2)

86-93: Don’t log full session objects (token leakage).
console.log('Auth state changed:', event, session); can leak access/refresh tokens into device logs.

     const { data: { subscription } } = supabase.auth.onAuthStateChange(
       async (event, session) => {
-        console.log('Auth state changed:', event, session);
+        console.log('Auth state changed:', event);
         setSession(session);

98-148: Guard data.user.email and handle auth session state in signup flow.

The non-null assertion on data.user.email! is unsafe. Supabase can return a user object without an email (especially in email-confirmation flows), and without a verified session, RLS policies will fail on the profile upsert and any subsequent invite queries.

Recommended changes:

-      if (!data.user) return { error: new Error('User not found') };
+      if (!data.user) return { error: new Error('User not found') };
+      if (!data.user.email) return { error: new Error('Email is required') };

       const userId = data.user.id;

       try {
         const profileData = {
           id: userId,
-          email: data.user.email!,
+          email: data.user.email,
           full_name: userData?.full_name || null,
           username: userData?.username || null,
           avatar_url: userData?.avatar_url || null,
           bio: userData?.bio || null,
         };

Alternatively, if email may legitimately be absent during email-confirmation flows, allow null and ensure downstream invite-fetch logic handles the case where profile creation failed or was incomplete due to missing session authorization.

🧹 Nitpick comments (3)
frontend/services/device-storage.ts (1)

165-166: 7‑day TTL looks correct; consider making expirationMinutes === 0 explicit in setItem.

This change correctly caches suggested_friends for 60 * 24 * 7 minutes. Minor footgun: setItem uses a truthy check (expirationMinutes ? ...) so a future caller passing 0 won’t expire immediately (it will store with no expiry). Consider expirationMinutes != null (or typeof expirationMinutes === 'number') in setItem to avoid surprising behavior.

frontend/hooks/use-profile.ts (1)

8-16: Good tightening of update typing (ProfileUpdate)—but keep table refs consistent.
updateProfile correctly targets TABLES.PROFILES, but the “create profile” path still inserts into hard-coded 'profiles'.

-            const { data: newProfile, error: createError } = await supabase
-              .from('profiles')
+            const { data: newProfile, error: createError } = await supabase
+              .from(TABLES.PROFILES)
               .insert(profileData as never)
               .select()
               .single();

Also applies to: 85-107

frontend/services/invite-service.ts (1)

42-50: Restrict updateInvite to mutable columns.
Partial<Invite> allows callers to attempt updates to immutable fields (e.g., id, inviter_id, created_at) and can create accidental corruption if RLS permits it. Prefer a narrow InviteUpdate type (e.g., only message, max_uses, current_uses, is_active).

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 05a058a and 679e600.

⛔ Files ignored due to path filters (1)
  • frontend/bun.lock is excluded by !**/*.lock
📒 Files selected for processing (16)
  • frontend/app.json (2 hunks)
  • frontend/app/onboarding/auth.tsx (2 hunks)
  • frontend/app/onboarding/invite.tsx (3 hunks)
  • frontend/app/settings/index.tsx (2 hunks)
  • frontend/components/friends-section.tsx (3 hunks)
  • frontend/components/friends/suggested-friend-item.tsx (2 hunks)
  • frontend/eas.json (1 hunks)
  • frontend/hooks/use-auth.ts (3 hunks)
  • frontend/hooks/use-friends.ts (1 hunks)
  • frontend/hooks/use-invite-acceptance.ts (2 hunks)
  • frontend/hooks/use-profile.ts (2 hunks)
  • frontend/hooks/use-user-invite.ts (1 hunks)
  • frontend/package.json (2 hunks)
  • frontend/providers/auth-provider.tsx (1 hunks)
  • frontend/services/device-storage.ts (1 hunks)
  • frontend/services/invite-service.ts (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (6)
frontend/hooks/use-user-invite.ts (2)
frontend/types/database.ts (1)
  • Database (11-244)
frontend/services/invite-service.ts (1)
  • InviteService (7-51)
frontend/hooks/use-friends.ts (2)
frontend/lib/supabase.ts (1)
  • supabase (14-30)
frontend/constants/supabase.ts (1)
  • TABLES (4-13)
frontend/hooks/use-invite-acceptance.ts (1)
frontend/constants/supabase.ts (1)
  • FRIENDSHIP_STATUS (29-33)
frontend/components/friends/suggested-friend-item.tsx (4)
frontend/providers/auth-provider.tsx (1)
  • useAuthContext (69-75)
frontend/services/friend-service.ts (1)
  • sendFriendRequest (90-99)
frontend/hooks/use-invite-acceptance.ts (1)
  • useInviteAcceptance (34-183)
frontend/hooks/use-toast.ts (1)
  • useToast (3-11)
frontend/app/onboarding/invite.tsx (3)
frontend/hooks/use-user-invite.ts (1)
  • useUserInvite (14-32)
frontend/lib/utils.ts (1)
  • generateDeepLinkUrl (54-65)
frontend/services/invite-service.ts (1)
  • InviteService (7-51)
frontend/hooks/use-profile.ts (1)
frontend/constants/supabase.ts (1)
  • TABLES (4-13)
🔇 Additional comments (7)
frontend/package.json (2)

2-2: Name change is fine.
No concerns with renaming the package.


65-65: No action needed. expo-updates@~29.0.14 is compatible with expo@54. The Expo SDK 54 compatibility matrix specifies expo-updates ~29.0.12 as the bundled version, with ~29.0.15 available on the latest docs. The pinned version ~29.0.14 is within the correct 29.0.x range and poses no compatibility risk.

frontend/app/settings/index.tsx (1)

107-109: Improved UX copy for missing auth token.
The new message is clearer and less “internal error”-ish.

frontend/hooks/use-invite-acceptance.ts (1)

41-48: Good guardrails on invite acceptance inputs.
The missing-id and self-invite checks prevent bad friendships from being created.

frontend/components/friends-section.tsx (1)

65-73: Nice UX improvement: separate Connected vs Pending counts.
Only minor thing: if Badge expects string, wrap with String(...); otherwise LGTM.

Also applies to: 89-97, 116-119

frontend/app/onboarding/auth.tsx (1)

69-95: Signup-to-invite navigation looks consistent with the new user_id param.
The data?.userId guard + fallback prevents hard failure if signup returns without a user id.

frontend/app.json (1)

140-146: Expo updates config addition is fine—verify it matches the intended EAS/updates setup.
Just ensure the runtimeVersion policy and updates URL align with how you’re publishing updates for this project.

Comment on lines 22 to 37
export default function SuggestedFriendItem({ friend, onAccept, index = 0 }: FriendItemProps) {
const handleAccept = () => {}
const { profile } = useAuthContext();
const { acceptInvite: sendFriendRequest } = useInviteAcceptance();
const { toast: showToast } = useToast();

const handleAccept = async () => {
if (!profile?.id) {
return showToast('Please login to send a friend request', 'error');
}
const result = await sendFriendRequest(friend.id, profile.id);
if (result.success) {
showToast('Friend request sent', 'success');
} else {
showToast(result.message || 'Failed to send friend request', 'error');
}
}
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

Guard against thrown errors + prevent duplicate sends; consider wiring onAccept.
Wrap sendFriendRequest in try/catch and (if available from useInviteAcceptance) use an isProcessing flag to disable the button; also call onAccept?.(result.friendId) on success if the hook returns it.

 export default function SuggestedFriendItem({ friend, onAccept, index = 0 }: FriendItemProps) {
   const { profile } = useAuthContext();
-  const { acceptInvite: sendFriendRequest } = useInviteAcceptance();
+  const { acceptInvite: sendFriendRequest, isProcessing } = useInviteAcceptance();
   const { toast: showToast } = useToast();

   const handleAccept = async () => {
     if (!profile?.id) {
       return showToast('Please login to send a friend request', 'error');
     }
-    const result = await sendFriendRequest(friend.id, profile.id);
-    if (result.success) {
-      showToast('Friend request sent', 'success');
-    } else {
-      showToast(result.message || 'Failed to send friend request', 'error');
-    }
-  }
+    try {
+      const result = await sendFriendRequest(friend.id, profile.id);
+      if (result.success) {
+        showToast('Friend request sent', 'success');
+        if (result.friendId) onAccept?.(result.friendId);
+      } else {
+        showToast(result.message || 'Failed to send friend request', 'error');
+      }
+    } catch {
+      showToast('Failed to send friend request', 'error');
+    }
+  };

   return (
     ...
         <TouchableOpacity
           style={styles.addButton}
           onPress={handleAccept}
+          disabled={isProcessing}
           hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
         >
📝 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
export default function SuggestedFriendItem({ friend, onAccept, index = 0 }: FriendItemProps) {
const handleAccept = () => {}
const { profile } = useAuthContext();
const { acceptInvite: sendFriendRequest } = useInviteAcceptance();
const { toast: showToast } = useToast();
const handleAccept = async () => {
if (!profile?.id) {
return showToast('Please login to send a friend request', 'error');
}
const result = await sendFriendRequest(friend.id, profile.id);
if (result.success) {
showToast('Friend request sent', 'success');
} else {
showToast(result.message || 'Failed to send friend request', 'error');
}
}
export default function SuggestedFriendItem({ friend, onAccept, index = 0 }: FriendItemProps) {
const { profile } = useAuthContext();
const { acceptInvite: sendFriendRequest, isProcessing } = useInviteAcceptance();
const { toast: showToast } = useToast();
const handleAccept = async () => {
if (!profile?.id) {
return showToast('Please login to send a friend request', 'error');
}
try {
const result = await sendFriendRequest(friend.id, profile.id);
if (result.success) {
showToast('Friend request sent', 'success');
if (result.friendId) onAccept?.(result.friendId);
} else {
showToast(result.message || 'Failed to send friend request', 'error');
}
} catch {
showToast('Failed to send friend request', 'error');
}
};
return (
...
<TouchableOpacity
style={styles.addButton}
onPress={handleAccept}
disabled={isProcessing}
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
>

Comment on lines 7 to 19
"development": {
"developmentClient": true,
"distribution": "internal"
"distribution": "internal",
"channel": "development"
},
"preview": {
"distribution": "internal"
"distribution": "internal",
"channel": "preview"
},
"production": {
"autoIncrement": true
"autoIncrement": true,
"channel": "production"
}
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 | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

git ls-files | head -20

Repository: fortune710/keepsafe

Length of output: 590


🏁 Script executed:

# Look for app.json and eas.json in the repository
fd -t f "app\.json|eas\.json|package\.json" --max-one-result-per-directory | head -20

Repository: fortune710/keepsafe

Length of output: 310


🏁 Script executed:

# Search for CI/release workflow files
fd -t f "\.github|workflows|\.yml|\.yaml" | grep -E "(github|workflow|CI|release)" | head -20

Repository: fortune710/keepsafe

Length of output: 45


🏁 Script executed:

# Look for eas update commands in the codebase
rg "eas update" --type js --type json --type yaml -A 2 -B 2

Repository: fortune710/keepsafe

Length of output: 45


🏁 Script executed:

# Check if frontend directory exists
ls -la frontend/ 2>&1 || echo "frontend directory not found"

Repository: fortune710/keepsafe

Length of output: 1663


🏁 Script executed:

# Search more broadly for the files mentioned in the review
find . -name "app.json" -o -name "eas.json" -o -name "package.json" 2>/dev/null | head -30

Repository: fortune710/keepsafe

Length of output: 126


🏁 Script executed:

# Look for any frontend-related files
find . -type d -name "frontend" 2>/dev/null

Repository: fortune710/keepsafe

Length of output: 73


🏁 Script executed:

cat -n eas.json

Repository: fortune710/keepsafe

Length of output: 103


🏁 Script executed:

cat -n app.json | head -80

Repository: fortune710/keepsafe

Length of output: 103


🏁 Script executed:

# Check package.json for build and release scripts
cat package.json | jq '.scripts' 2>/dev/null || grep -A 20 '"scripts"' package.json

Repository: fortune710/keepsafe

Length of output: 107


🏁 Script executed:

# Look for GitHub Actions or CI workflows
find .github -type f 2>/dev/null || echo "No .github directory found"

Repository: fortune710/keepsafe

Length of output: 89


🏁 Script executed:

# Search for any update or publish scripts
rg "eas.*update|publish.*update" --type json --type js --type yaml

Repository: fortune710/keepsafe

Length of output: 45


🏁 Script executed:

cat -n ./frontend/eas.json

Repository: fortune710/keepsafe

Length of output: 665


🏁 Script executed:

cat -n ./frontend/app.json | head -100

Repository: fortune710/keepsafe

Length of output: 3175


🏁 Script executed:

cat ./frontend/package.json | jq '.scripts' 2>/dev/null || sed -n '/"scripts":/,/^  }/p' ./frontend/package.json

Repository: fortune710/keepsafe

Length of output: 436


🏁 Script executed:

# Search for eas update or release commands in frontend
rg "eas.*update|release|publish" ./frontend --type json --type js

Repository: fortune710/keepsafe

Length of output: 310


🏁 Script executed:

cat ./frontend/app.json | jq '.expo | {runtimeVersion, updates}' 2>/dev/null || rg "runtimeVersion|updates" ./frontend/app.json

Repository: fortune710/keepsafe

Length of output: 208


🏁 Script executed:

# Search the entire frontend for runtimeVersion or updates config
rg "runtimeVersion|\"updates\"|updates:" ./frontend --type json

Repository: fortune710/keepsafe

Length of output: 143


🏁 Script executed:

# Check if there are any CI or release scripts elsewhere
find ./frontend -type f \( -name "*.yml" -o -name "*.yaml" -o -name "Makefile" -o -name "*.sh" \) 2>/dev/null

Repository: fortune710/keepsafe

Length of output: 45


EAS channels are configured but lack update publishing workflow.

Your eas.json defines build channels (development/preview/production) and app.json has runtimeVersion and updates.url configured, but there is no CI/automation to publish updates to these channels. Add eas update commands to your release workflow or build scripts to distribute updates to the appropriate channel matching your app version.

🤖 Prompt for AI Agents
In frontend/eas.json around lines 7 to 19, you’ve defined
development/preview/production channels but haven’t automated publishing updates
to those channels; add CI or npm script steps that run EAS Update after a
successful build (e.g., run an EAS CLI update command targeted to the matching
channel/branch for each pipeline: development -> development, preview ->
preview, production -> production), ensure the command uses the same
runtimeVersion/updates.url configured in app.json, and wire these commands into
your release workflows (GitHub Actions/GitLab CI/whatever) so updates are
published automatically after a release build.

Comment on lines 7 to 12
interface UseUserInviteResult {
invite: Invite | undefined;
isLoading: boolean;
isError: boolean;
error: unknown;
}
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

🧩 Analysis chain

🏁 Script executed:

cat -n frontend/hooks/use-user-invite.ts

Repository: fortune710/keepsafe

Length of output: 1069


🏁 Script executed:

cat package.json | grep -i "react-query\|@tanstack"

Repository: fortune710/keepsafe

Length of output: 107


🏁 Script executed:

fd package.json

Repository: fortune710/keepsafe

Length of output: 84


🏁 Script executed:

head -50 package.json

Repository: fortune710/keepsafe

Length of output: 134


🏁 Script executed:

cat frontend/package.json | grep -A 2 -B 2 "@tanstack\|react-query"

Repository: fortune710/keepsafe

Length of output: 240


🌐 Web query:

React Query v5 isPending vs isLoading deprecation

💡 Result:

Short answer: In v5 React Query (TanStack Query) the old "loading" terminology was renamed to "pending" — the derived flag isLoading was renamed to isPending, isInitialLoading was renamed to isLoading (and isInitialLoading is deprecated), and a new isLoading now equals isPending && isFetching (i.e., “initially pending AND currently fetching”). Update code to use isPending where you previously checked isLoading for pending state; use the new isLoading only when you want the combined pending+fetching behavior. [1][2]


Update to React Query v5 flags, expose refetch, and add retry strategy for async-created invites.

The hook uses deprecated isLoading (React Query v5 renamed this to isPending); use isPending for pending state instead. Also, refetch is not exposed, limiting consumers' ability to retry if an invite row isn't created yet—this is important when invites are created asynchronously. Add retry tuning to handle the race condition automatically.

 interface UseUserInviteResult {
   invite: Invite | undefined;
-  isLoading: boolean;
+  isPending: boolean;
   isError: boolean;
-  error: unknown;
+  error: unknown;
+  refetch: () => Promise<void>;
 }

 export function useUserInvite(userId?: string): UseUserInviteResult {
-  const { data, isLoading, isError, error } = useQuery({
+  const { data, isPending, isError, error, refetch } = useQuery({
     queryKey: ['user-invite', userId],
     enabled: !!userId,
+    retry: 5,
+    retryDelay: attempt => Math.min(500 * 2 ** attempt, 4000),
     queryFn: async () => {
       if (!userId) {
         throw new Error('Missing user id');
       }
       return InviteService.getInvite(userId);
     },
   });

   return {
     invite: data,
-    isLoading,
+    isPending,
     isError,
     error,
+    refetch,
   };
 }

Also applies to: 15-24

🤖 Prompt for AI Agents
In frontend/hooks/use-user-invite.ts around lines 7-12 and 15-24, update the
hook to use React Query v5 flags and add retry/refetch support: replace the
deprecated isLoading with isPending in the UseUserInviteResult interface and in
any returned object, expose the query's refetch method on the result so callers
can manually retry, and configure the useQuery call with a retry strategy (e.g.,
set retry to a small number and an appropriate retryDelay or
exponential/backoff) to handle the async-created invite race; ensure types for
refetch and retry options align with React Query v5 APIs.

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 (2)
frontend/hooks/use-invite-acceptance.ts (1)

149-169: declineInvite is a no-op stub.

The declineInvite function returns success without performing any database operations or cleanup. If invite declination is not needed, consider removing this method; otherwise, implement the required logic.

Do you want me to help implement the decline logic, or should this method be removed?

frontend/hooks/use-profile.ts (1)

54-58: Use TABLES.PROFILES constant for consistency.

Line 55 uses the string literal 'profiles' while line 92 uses TABLES.PROFILES. Both should use the constant for maintainability.

Apply this diff:

-            const { data: newProfile, error: createError } = await supabase
-              .from('profiles')
-              .insert(profileData as never)
+            const { data: newProfile, error: createError } = await supabase
+              .from(TABLES.PROFILES)
+              .insert(profileData as never)
♻️ Duplicate comments (4)
frontend/hooks/use-invite-acceptance.ts (1)

56-62: Past comment addressed: comment now matches code behavior.

The comment correctly states that the friendship is created with pending status. This resolves the previous inconsistency.

frontend/services/invite-service.ts (2)

14-24: Past comment addressed: onConflict now uses 'inviter_id'.

The upsert correctly uses onConflict: 'inviter_id' to enforce one invite per inviter, resolving the previous mismatch. Ensure the database schema has a unique constraint on inviter_id.

Run the following script to verify the unique constraint exists:

#!/bin/bash
# Check for unique constraint on inviter_id in the invites table schema
rg -A 5 -B 5 "inviter_id.*unique|unique.*inviter_id|constraint.*inviter_id" --type sql

26-40: Past comment addressed: maybeSingle() with explicit null check.

The query now uses maybeSingle() and explicitly checks for null, correctly handling the case when no invite exists. This resolves the previous issue.

frontend/app/onboarding/invite.tsx (1)

49-50: Past comment addressed: "Continue to Login" now routes correctly.

The handler now navigates to mode=signin instead of mode=signup, fixing the redirect loop.

🧹 Nitpick comments (3)
frontend/services/invite-service.ts (1)

10-12: Consider removing redundant wrapper.

InviteService.generateInviteCode() simply delegates to the imported generateInviteCode(). Unless you plan to add service-specific logic here, callers could import the utility directly.

frontend/app/onboarding/invite.tsx (2)

16-18: useUserInvite exposes refetch but it's not destructured.

According to the relevant code snippets, useUserInvite returns a refetch function. Consider destructuring it to enable retry functionality in the error state.

Apply this diff:

-  const { invite, isLoading, isError } = useUserInvite(
+  const { invite, isLoading, isError, refetch } = useUserInvite(
     typeof userId === 'string' ? userId : undefined
   );

89-105: Add retry button in error state.

The error UI provides no way to retry fetching the invite. Since useUserInvite returns refetch, add a "Try Again" button alongside "Skip for now" to allow users to retry without leaving the screen.

Example implementation:

const handleRetry = async () => {
  if (refetch) {
    await refetch();
  }
};

// In the error UI JSX:
<View style={styles.actionButtons}>
  <TouchableOpacity style={styles.shareButton} onPress={handleRetry}>
    <Text style={styles.shareButtonText}>Try Again</Text>
  </TouchableOpacity>
  <TouchableOpacity style={styles.continueButton} onPress={handleSkip}>
    <Text style={styles.continueButtonText}>Skip for now</Text>
    <ArrowRight color="#8B5CF6" size={20} />
  </TouchableOpacity>
</View>
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 679e600 and 40b5852.

📒 Files selected for processing (9)
  • frontend/app.json (2 hunks)
  • frontend/app/onboarding/invite.tsx (3 hunks)
  • frontend/hooks/use-friends.ts (1 hunks)
  • frontend/hooks/use-invite-acceptance.ts (2 hunks)
  • frontend/hooks/use-profile.ts (3 hunks)
  • frontend/hooks/use-user-invite.ts (1 hunks)
  • frontend/lib/utils.ts (2 hunks)
  • frontend/services/friend-service.ts (2 hunks)
  • frontend/services/invite-service.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • frontend/app.json
  • frontend/hooks/use-user-invite.ts
🧰 Additional context used
🧬 Code graph analysis (5)
frontend/hooks/use-invite-acceptance.ts (2)
frontend/lib/supabase.ts (1)
  • supabase (14-30)
frontend/constants/supabase.ts (2)
  • TABLES (4-13)
  • FRIENDSHIP_STATUS (29-33)
frontend/app/onboarding/invite.tsx (3)
frontend/hooks/use-user-invite.ts (1)
  • useUserInvite (15-34)
frontend/lib/utils.ts (1)
  • generateDeepLinkUrl (68-79)
frontend/services/invite-service.ts (1)
  • InviteService (8-51)
frontend/lib/utils.ts (1)
frontend/services/invite-service.ts (1)
  • generateInviteCode (10-12)
frontend/services/friend-service.ts (2)
frontend/lib/utils.ts (1)
  • generateInviteCode (47-66)
frontend/services/invite-service.ts (1)
  • generateInviteCode (10-12)
frontend/services/invite-service.ts (4)
frontend/types/database.ts (1)
  • Database (11-244)
frontend/lib/utils.ts (1)
  • generateInviteCode (47-66)
frontend/lib/supabase.ts (1)
  • supabase (14-30)
frontend/constants/supabase.ts (1)
  • TABLES (4-13)
🔇 Additional comments (8)
frontend/hooks/use-invite-acceptance.ts (1)

37-43: LGTM! Input validation is solid.

The validation correctly prevents empty IDs and self-connections with clear error messages.

frontend/services/friend-service.ts (2)

9-9: LGTM! Correctly imports the async generateInviteCode.

The import statement properly references the external utility function.


16-28: LGTM! generateInviteLink now uses async invite code generation.

The method correctly awaits the async generateInviteCode() and constructs the invite link with the result.

frontend/lib/utils.ts (1)

44-66: LGTM! Cryptographically secure invite code generation with proper rejection sampling.

The implementation correctly uses expo-crypto for secure randomness and applies rejection sampling to avoid modulo bias. The logic is sound: maxMultiple ensures only unbiased bytes are used, and the while loop guarantees exactly 8 characters.

frontend/hooks/use-profile.ts (2)

51-51: LGTM! Correctly awaits async generateInviteCode().

The profile creation now properly awaits the async invite code generation.


8-8: LGTM! ProfileUpdate type appropriately narrows the update interface.

Using the database-generated ProfileUpdate type for the updateProfile parameter ensures type safety and aligns with Supabase's generated types.

Also applies to: 15-15, 85-85

frontend/services/invite-service.ts (1)

42-50: LGTM! updateInvite provides flexible partial updates.

The method allows updating specific invite fields by invite_code with proper error handling.

frontend/app/onboarding/invite.tsx (1)

13-22: LGTM! Invite link construction handles missing invite code gracefully.

The code correctly extracts userId from route params, uses useUserInvite to fetch invite data, and constructs the invite link with a fallback when inviteCode is absent.

Comment on lines 137 to 155
const updateFriendshipMutation = useMutation({
mutationFn: async ({ id, status }: { id: string; status: typeof FRIENDSHIP_STATUS.ACCEPTED | typeof FRIENDSHIP_STATUS.DECLINED }) => {

if (__DEV__) console.log('Updating friendship:', { id, status });
const { data, error } = await supabase
.from(TABLES.FRIENDSHIPS)
.update({ status } as never)
.eq('id', id)
.select()
.single();

if (error) {
if (__DEV__) console.error('Error updating friendship:', error);
throw new Error(error.message);
}

if (__DEV__) console.log('Updated friendship:', data);
return data;
},
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

Avoid possible runtime crash: __DEV__ may be undefined in this frontend build.

If __DEV__ isn’t globally defined (common outside React Native), if (__DEV__) ... can throw at runtime. Safer guard:

-      if (__DEV__) console.log('Updating friendship:', { id, status });
+      const isDev = typeof __DEV__ !== 'undefined'
+        ? __DEV__
+        : process.env.NODE_ENV !== 'production';
+      if (isDev) console.log('Updating friendship:', { id, status });

       const { data, error } = await supabase
         .from(TABLES.FRIENDSHIPS)
         .update({ status } as never)
         .eq('id', id)
         .select()
         .single();

       if (error) {
-        if (__DEV__) console.error('Error updating friendship:', error);
+        if (isDev) console.error('Error updating friendship:', error);
         throw new Error(error.message);
       }

-      if (__DEV__) console.log('Updated friendship:', data);
+      if (isDev) console.log('Updated friendship:', data);
       return data;
📝 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 updateFriendshipMutation = useMutation({
mutationFn: async ({ id, status }: { id: string; status: typeof FRIENDSHIP_STATUS.ACCEPTED | typeof FRIENDSHIP_STATUS.DECLINED }) => {
if (__DEV__) console.log('Updating friendship:', { id, status });
const { data, error } = await supabase
.from(TABLES.FRIENDSHIPS)
.update({ status } as never)
.eq('id', id)
.select()
.single();
if (error) {
if (__DEV__) console.error('Error updating friendship:', error);
throw new Error(error.message);
}
if (__DEV__) console.log('Updated friendship:', data);
return data;
},
const updateFriendshipMutation = useMutation({
mutationFn: async ({ id, status }: { id: string; status: typeof FRIENDSHIP_STATUS.ACCEPTED | typeof FRIENDSHIP_STATUS.DECLINED }) => {
const isDev = typeof __DEV__ !== 'undefined'
? __DEV__
: process.env.NODE_ENV !== 'production';
if (isDev) console.log('Updating friendship:', { id, status });
const { data, error } = await supabase
.from(TABLES.FRIENDSHIPS)
.update({ status } as never)
.eq('id', id)
.select()
.single();
if (error) {
if (isDev) console.error('Error updating friendship:', error);
throw new Error(error.message);
}
if (isDev) console.log('Updated friendship:', data);
return data;
},
🤖 Prompt for AI Agents
In frontend/hooks/use-friends.ts around lines 137 to 155, there are several
runtime checks using __DEV__ which may be undefined in the browser build and
cause a crash; replace each raw `if (__DEV__)` with a safe guard such as `if
(typeof __DEV__ !== 'undefined' && __DEV__)` or use an equivalent environment
check like `process.env.NODE_ENV !== 'production'` to conditionally log,
ensuring no direct reference to an undefined global; keep the existing log
messages and error handling otherwise.

@fortune710 fortune710 merged commit f11c0b3 into main Dec 12, 2025
2 of 3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant