From 2648bf5404ec6a26c6d85a1f87162b32bb35e848 Mon Sep 17 00:00:00 2001 From: Fortune Oluwasemilore Alebiosu Date: Thu, 11 Dec 2025 22:28:59 -0500 Subject: [PATCH 01/10] added new publish channel --- frontend/app.json | 18 ++++++++++++++++-- frontend/eas.json | 9 ++++++--- 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/frontend/app.json b/frontend/app.json index bfd0a83..7492dc6 100644 --- a/frontend/app.json +++ b/frontend/app.json @@ -55,7 +55,15 @@ ], "permissions": [ "android.permission.RECORD_AUDIO", - "android.permission.CAMERA" + "android.permission.CAMERA", + "android.permission.READ_CONTACTS", + "android.permission.WRITE_CONTACTS", + "android.permission.MODIFY_AUDIO_SETTINGS", + "android.permission.RECORD_AUDIO", + "android.permission.CAMERA", + "android.permission.READ_CONTACTS", + "android.permission.WRITE_CONTACTS", + "android.permission.MODIFY_AUDIO_SETTINGS" ], "package": "com.fortune710.keepsafe" }, @@ -129,6 +137,12 @@ "icon": "./assets/images/icon.png", "color": "#8B5CF6" }, - "owner": "fortune710" + "owner": "fortune710", + "runtimeVersion": { + "policy": "appVersion" + }, + "updates": { + "url": "https://u.expo.dev/97eb5c9e-4f9a-47a1-9994-1351aca19f05" + } } } diff --git a/frontend/eas.json b/frontend/eas.json index 4993517..9f33874 100644 --- a/frontend/eas.json +++ b/frontend/eas.json @@ -6,13 +6,16 @@ "build": { "development": { "developmentClient": true, - "distribution": "internal" + "distribution": "internal", + "channel": "development" }, "preview": { - "distribution": "internal" + "distribution": "internal", + "channel": "preview" }, "production": { - "autoIncrement": true + "autoIncrement": true, + "channel": "production" } }, "submit": { From ffa32aa0be6cde734f5f16685648d58c586901b6 Mon Sep 17 00:00:00 2001 From: Fortune Oluwasemilore Alebiosu Date: Thu, 11 Dec 2025 22:30:04 -0500 Subject: [PATCH 02/10] creted invite service and max uses constant --- frontend/app/onboarding/invite.tsx | 149 ++++++++++++++++++---------- frontend/services/invite-service.ts | 51 ++++++++++ 2 files changed, 147 insertions(+), 53 deletions(-) create mode 100644 frontend/services/invite-service.ts diff --git a/frontend/app/onboarding/invite.tsx b/frontend/app/onboarding/invite.tsx index 1e0ef91..de57f78 100644 --- a/frontend/app/onboarding/invite.tsx +++ b/frontend/app/onboarding/invite.tsx @@ -1,30 +1,25 @@ -import React, { useState, useEffect } from 'react'; -import { View, Text, TouchableOpacity, StyleSheet, Dimensions, Alert, Share } from 'react-native'; -import { router } from 'expo-router'; +import React from 'react'; +import { View, Text, TouchableOpacity, StyleSheet, Alert, Share } from 'react-native'; +import { router, useLocalSearchParams } from 'expo-router'; import Animated, { FadeInDown, FadeInUp } from 'react-native-reanimated'; import { Copy, Share as ShareIcon, Users, ArrowRight } from 'lucide-react-native'; import * as Clipboard from 'expo-clipboard'; +import { generateDeepLinkUrl } from '@/lib/utils'; +import { InviteService } from '@/services/invite-service'; +import { useUserInvite } from '@/hooks/use-user-invite'; -const { width } = Dimensions.get('window'); export default function InviteScreen() { - const [inviteLink, setInviteLink] = useState(''); - const [isGenerating, setIsGenerating] = useState(true); - - useEffect(() => { - generateInviteLink(); - }, []); - - const generateInviteLink = async () => { - setIsGenerating(true); - - // Simulate generating invite link - setTimeout(() => { - const mockCode = Math.random().toString(36).substring(2, 10); - setInviteLink(`https://keepsafe.app/invite/${mockCode}`); - setIsGenerating(false); - }, 1500); - }; + const { user_id } = useLocalSearchParams(); + const userId = Array.isArray(user_id) ? user_id[0] : user_id; + + const { invite, isLoading, isError } = useUserInvite( + typeof userId === 'string' ? userId : undefined + ); + + const baseUrl = generateDeepLinkUrl(); + const inviteCode = invite?.invite_code; + const inviteLink = inviteCode ? `${baseUrl}/invite/${inviteCode}` : `${baseUrl}/invite`; const handleCopyLink = async () => { try { @@ -48,13 +43,67 @@ export default function InviteScreen() { }; const handleSkip = () => { - router.replace('/capture'); + return router.replace('/onboarding/auth?mode=signin'); }; const handleContinue = () => { - router.replace('/capture'); + return router.replace('/onboarding/auth?mode=signup'); }; + // If we don't have a user id, just let the user skip this step. + if (!userId) { + return ( + + + Invite Your Friends + + We couldn't find your account information. You can skip this step and start + using Keepsafe. + + + Skip for now + + + + + ); + } + + if (isLoading) { + return ( + + + + Preparing your invite link... + + + + + Skip for now + + + + ); + } + + if (isError || !inviteCode) { + return ( + + + Invite Unavailable + + We couldn't load your invite link right now. You can skip this step and start + using Keepsafe. + + + Skip for now + + + + + ); + } + return ( @@ -67,38 +116,32 @@ export default function InviteScreen() { Share moments with the people who matter most. Send them your invite link to get started. - {isGenerating ? ( - - Generating your invite link... - - ) : ( - - - - {inviteLink} - - - - - - - - This link can be used 10 times + + + + {inviteLink} + + + + + + + This link can be used {InviteService.MAX_INVITE_USES} times + + + + + + Share Link + - - - - Share Link - - - - Continue to App - - - - - )} + + Continue to Login + + + + diff --git a/frontend/services/invite-service.ts b/frontend/services/invite-service.ts new file mode 100644 index 0000000..22f8c09 --- /dev/null +++ b/frontend/services/invite-service.ts @@ -0,0 +1,51 @@ +import { TABLES } from "@/constants/supabase"; +import { supabase } from "@/lib/supabase"; +import { Database } from "@/types/database"; + +type Invite = Database['public']['Tables']['invites']['Row']; + +export class InviteService { + static readonly MAX_INVITE_USES = 10; + static generateInviteCode(): string { + const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; + let result = ''; + for (let i = 0; i < 8; i++) { + result += characters.charAt(Math.floor(Math.random() * characters.length)); + } + return result; + } + + static async createInvite(inviterId: string, inviteCode: string): Promise { + const invite = await supabase.from(TABLES.INVITES).upsert({ + inviter_id: inviterId, + invite_code: inviteCode, + max_uses: this.MAX_INVITE_USES, + } as never, { onConflict: 'invite_code' }); + + if (invite.error) { + throw new Error(invite.error.message); + } + } + + static async getInvite(userId: string): Promise { + const { data: invite, error } = await supabase + .from(TABLES.INVITES) + .select('*') + .eq('inviter_id', userId) + .single(); + if (error) { + throw new Error(error.message); + } + return invite; + } + + static async updateInvite(inviteCode: string, updates: Partial): Promise { + const { error } = await supabase + .from(TABLES.INVITES) + .update(updates as never) + .eq('invite_code', inviteCode); + if (error) { + throw new Error(error.message); + } + } +} \ No newline at end of file From 677a219d3ebe9ed12e83ad122d194d4a0742af41 Mon Sep 17 00:00:00 2001 From: Fortune Oluwasemilore Alebiosu Date: Thu, 11 Dec 2025 22:32:52 -0500 Subject: [PATCH 03/10] cached suggested friends and made them to be friend requestable --- .../friends/suggested-friend-item.tsx | 19 ++++++++++++++++++- frontend/services/device-storage.ts | 3 ++- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/frontend/components/friends/suggested-friend-item.tsx b/frontend/components/friends/suggested-friend-item.tsx index d218d26..4b3513b 100644 --- a/frontend/components/friends/suggested-friend-item.tsx +++ b/frontend/components/friends/suggested-friend-item.tsx @@ -7,6 +7,9 @@ import { Colors } from '@/lib/constants'; import { SuggestedFriend } from '@/types/friends'; import { getDefaultAvatarUrl } from '@/lib/utils'; import { scale, verticalScale } from 'react-native-size-matters'; +import { useInviteAcceptance } from '@/hooks/use-invite-acceptance'; +import { useAuthContext } from '@/providers/auth-provider'; +import { useToast } from '@/hooks/use-toast'; @@ -17,7 +20,21 @@ interface FriendItemProps { } 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'); + } + } return ( diff --git a/frontend/services/device-storage.ts b/frontend/services/device-storage.ts index 7f575b4..22d4f34 100644 --- a/frontend/services/device-storage.ts +++ b/frontend/services/device-storage.ts @@ -162,7 +162,8 @@ class DeviceStorage { } async setSuggestedFriends(data: SuggestedFriend[]): Promise { - await this.setItem('suggested_friends', data); + const cacheDurationMinutes = 60 * 24 * 7; // 7 days + await this.setItem('suggested_friends', data, cacheDurationMinutes); } } From 547a2081fff83476a7209a85f50d1915a75800a3 Mon Sep 17 00:00:00 2001 From: Fortune Oluwasemilore Alebiosu Date: Thu, 11 Dec 2025 22:37:32 -0500 Subject: [PATCH 04/10] retruned userId from auth --- frontend/app/onboarding/auth.tsx | 21 ++++++++-- frontend/hooks/use-auth.ts | 59 +++++++++++++++++----------- frontend/providers/auth-provider.tsx | 6 ++- 3 files changed, 57 insertions(+), 29 deletions(-) diff --git a/frontend/app/onboarding/auth.tsx b/frontend/app/onboarding/auth.tsx index 3e10759..9cbf3f2 100644 --- a/frontend/app/onboarding/auth.tsx +++ b/frontend/app/onboarding/auth.tsx @@ -67,7 +67,7 @@ export default function AuthScreen() { try { const fullName = `${firstName.trim()} ${lastName.trim()}`; - const { error } = await signUp(email, password, { + const { error, data } = await signUp(email, password, { full_name: fullName, username: username.trim() }); @@ -76,9 +76,22 @@ export default function AuthScreen() { showToast(error.message || 'Sign up failed. Please try again.', 'error'); return; } - - // Navigate to invite page after successful signup - router.push('/onboarding/invite'); + + // Navigate to invite page after successful signup. + // We now rely on Supabase triggers to create the invite for this user, + // so we just pass the userId to the invite screen. + if (!data?.userId) { + // Fallback: if for some reason we don't have a userId, just continue. + router.replace('/capture'); + return; + } + + return router.push({ + pathname: '/onboarding/invite', + params: { + user_id: data.userId, + }, + }); } catch (error) { showToast('An unexpected error occurred', 'error'); } finally { diff --git a/frontend/hooks/use-auth.ts b/frontend/hooks/use-auth.ts index bc47abe..7fc447a 100644 --- a/frontend/hooks/use-auth.ts +++ b/frontend/hooks/use-auth.ts @@ -3,6 +3,7 @@ import { supabase } from '@/lib/supabase'; import { User, Session, AuthError } from '@supabase/supabase-js'; import { Database } from '@/types/database'; import { Platform } from 'react-native'; +import { TABLES } from '@/constants/supabase'; type Profile = Database['public']['Tables']['profiles']['Row']; @@ -39,7 +40,11 @@ interface UseAuthResult { user: User | null; session: Session | null; loading: boolean; - signUp: (email: string, password: string, userData?: Partial) => Promise<{ error: Error | null }>; + signUp: ( + email: string, + password: string, + userData?: Partial + ) => Promise<{ error: Error | null; data?: { userId: string } }>; signIn: (email: string, password: string) => Promise<{ error: Error | null }>; signOut: () => Promise<{ error: Error | null }>; } @@ -106,35 +111,41 @@ export function useAuth(): UseAuthResult { return { error: new Error(error.message) }; } - console.log(data); // Wait for user to be authenticated and session to be established - if (data.user) { - // Create profile manually after successful signup - try { - const profileData = { - id: data.user.id, - email: data.user.email!, - full_name: userData?.full_name || null, - username: userData?.username || null, - avatar_url: userData?.avatar_url || null, - bio: userData?.bio || null, - }; - - const { error: profileError } = await supabase - .from('profiles') - .upsert(profileData as never, { onConflict: 'id' }); - - if (profileError) { - console.error('Error creating profile:', profileError); - // Don't return error here as signup was successful - } - } catch (profileError) { + if (!data.user) return { error: new Error('User not found') }; + + const userId = data.user.id; + + // Create profile manually after successful signup. + // The invite_code is now generated by a Supabase trigger, so we don't + // create it here or insert into the invites table manually. + try { + const profileData = { + id: userId, + email: data.user.email!, + full_name: userData?.full_name || null, + username: userData?.username || null, + avatar_url: userData?.avatar_url || null, + bio: userData?.bio || null, + }; + + const { error: profileError } = await supabase + .from(TABLES.PROFILES) + .upsert(profileData as never, { onConflict: 'id' }); + + if (profileError) { console.error('Error creating profile:', profileError); // Don't return error here as signup was successful } + } catch (profileError) { + console.error('Error creating profile:', profileError); + // Don't return error here as signup was successful } - return { error: null }; + // Always return the userId so the caller can fetch the invite that + // was created by the Supabase trigger. + return { error: null, data: { userId } }; + } catch (error) { return { error: error as Error }; } finally { diff --git a/frontend/providers/auth-provider.tsx b/frontend/providers/auth-provider.tsx index 7e9d10a..4c88595 100644 --- a/frontend/providers/auth-provider.tsx +++ b/frontend/providers/auth-provider.tsx @@ -12,7 +12,11 @@ interface AuthContextType { session: Session | null; loading: boolean; profileLoading: boolean; - signUp: (email: string, password: string, userData?: Partial) => Promise<{ error: any }>; + signUp: ( + email: string, + password: string, + userData?: Partial + ) => Promise<{ error: any; data?: { userId: string } }>; signIn: (email: string, password: string) => Promise<{ error: any }>; signOut: () => Promise<{ error: any }>; updateProfile: (updates: Partial) => Promise<{ error: Error | null }>; From 28a1690166a7720e18518fde0617d6012018a134 Mon Sep 17 00:00:00 2001 From: Fortune Oluwasemilore Alebiosu Date: Thu, 11 Dec 2025 22:37:49 -0500 Subject: [PATCH 05/10] improved invites --- frontend/hooks/use-invite-acceptance.ts | 12 +++++++-- frontend/hooks/use-user-invite.ts | 34 +++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 2 deletions(-) create mode 100644 frontend/hooks/use-user-invite.ts diff --git a/frontend/hooks/use-invite-acceptance.ts b/frontend/hooks/use-invite-acceptance.ts index 6b89563..7b7706d 100644 --- a/frontend/hooks/use-invite-acceptance.ts +++ b/frontend/hooks/use-invite-acceptance.ts @@ -34,9 +34,18 @@ interface UseInviteAcceptanceResult { export function useInviteAcceptance(inviteId?: string): UseInviteAcceptanceResult { const [error, setError] = useState(null); const [isProcessing, setIsProcessing] = useState(false); + const acceptInviteMutation = useMutation({ mutationFn: async ({ inviteeId, userId }: { inviteeId: string; userId: string }) => { + if (!inviteeId || !userId) { + throw new Error('Invalid invitee or user ID'); + } + + if (inviteeId === userId) { + throw new Error('You cannot connect with yourself'); + } + const { data: existingFriendship } = await supabase .from(TABLES.FRIENDSHIPS) .select('id') @@ -47,7 +56,6 @@ export function useInviteAcceptance(inviteId?: string): UseInviteAcceptanceResul throw new Error('You are already connected with this user'); } - // Create friendship with accepted status const { data: friendship, error: friendshipError } = await supabase @@ -55,7 +63,7 @@ export function useInviteAcceptance(inviteId?: string): UseInviteAcceptanceResul .insert({ user_id: userId, friend_id: inviteeId, - status: FRIENDSHIP_STATUS.ACCEPTED, + status: FRIENDSHIP_STATUS.PENDING, } as never) .select() .single(); diff --git a/frontend/hooks/use-user-invite.ts b/frontend/hooks/use-user-invite.ts new file mode 100644 index 0000000..8b60702 --- /dev/null +++ b/frontend/hooks/use-user-invite.ts @@ -0,0 +1,34 @@ +import { useQuery } from '@tanstack/react-query'; +import { InviteService } from '@/services/invite-service'; +import { Database } from '@/types/database'; + +type Invite = Database['public']['Tables']['invites']['Row']; + +interface UseUserInviteResult { + invite: Invite | undefined; + isLoading: boolean; + isError: boolean; + error: unknown; +} + +export function useUserInvite(userId?: string): UseUserInviteResult { + const { data, isLoading, isError, error } = useQuery({ + queryKey: ['user-invite', userId], + enabled: !!userId, + queryFn: async () => { + if (!userId) { + throw new Error('Missing user id'); + } + return InviteService.getInvite(userId); + }, + }); + + return { + invite: data, + isLoading, + isError, + error, + }; +} + + From b5248d9d1bd8b2930f0f9bf2298c80abfeff2382 Mon Sep 17 00:00:00 2001 From: Fortune Oluwasemilore Alebiosu Date: Thu, 11 Dec 2025 22:39:25 -0500 Subject: [PATCH 06/10] made profile updates work --- frontend/app/settings/index.tsx | 5 ++--- frontend/hooks/use-profile.ts | 7 ++++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/frontend/app/settings/index.tsx b/frontend/app/settings/index.tsx index e246848..25c1617 100644 --- a/frontend/app/settings/index.tsx +++ b/frontend/app/settings/index.tsx @@ -1,9 +1,8 @@ -import React, { useState } from 'react'; +import React from 'react'; import { View, Text, TouchableOpacity, StyleSheet, ScrollView, Image } from 'react-native'; import { router } from 'expo-router'; import { ChevronRight, User, Bell, Shield, HardDrive, Info, LogOut } from 'lucide-react-native'; -import { Gesture, GestureDetector } from 'react-native-gesture-handler'; -import Animated, { SlideInDown, SlideOutUp } from 'react-native-reanimated'; +import { Gesture } from 'react-native-gesture-handler'; import { useAuthContext } from '@/providers/auth-provider'; import { SafeAreaView } from 'react-native-safe-area-context'; diff --git a/frontend/hooks/use-profile.ts b/frontend/hooks/use-profile.ts index 8855303..9babc8a 100644 --- a/frontend/hooks/use-profile.ts +++ b/frontend/hooks/use-profile.ts @@ -5,13 +5,14 @@ import { TABLES } from '@/constants/supabase'; import { generateInviteCode } from '@/lib/utils'; type Profile = Database['public']['Tables']['profiles']['Row']; +type ProfileUpdate = Database['public']['Tables']['profiles']['Update']; interface UseProfileResult { profile: Profile | null; isLoading: boolean; error: string | null; fetchProfile: (userId: string) => Promise; - updateProfile: (updates: Partial) => Promise<{ error: Error | null }>; + updateProfile: (updates: Partial) => Promise<{ error: Error | null }>; refreshProfile: (userId: string) => Promise; } @@ -81,14 +82,14 @@ export function useProfile(): UseProfileResult { } }, []); - const updateProfile = useCallback(async (updates: Partial>) => { + const updateProfile = useCallback(async (updates: Partial) => { if (!profile) { return { error: new Error('No profile loaded') }; } try { const { data, error } = await supabase - .from('profiles') + .from(TABLES.PROFILES) .update(updates as never) .eq('id', profile.id) .select() From 6aed2b340565b9f87570e997676b5b16b7d82a7b Mon Sep 17 00:00:00 2001 From: Fortune Oluwasemilore Alebiosu Date: Thu, 11 Dec 2025 22:42:00 -0500 Subject: [PATCH 07/10] fixed friends acceptance --- frontend/components/friends-section.tsx | 6 ++++-- frontend/hooks/use-friends.ts | 8 ++++++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/frontend/components/friends-section.tsx b/frontend/components/friends-section.tsx index 7b45676..6ab86bf 100644 --- a/frontend/components/friends-section.tsx +++ b/frontend/components/friends-section.tsx @@ -69,7 +69,7 @@ export default function FriendsSection({ Connected - + {connectedFriends.map((friend, index) => ( @@ -91,8 +91,9 @@ export default function FriendsSection({ - Pending ({pendingFriends.length}) + Pending + {pendingFriends.map((friend, index) => ( @@ -114,6 +115,7 @@ export default function FriendsSection({ const styles = StyleSheet.create({ connectedBadge: { marginLeft: 5, backgroundColor: "#10B981" }, + pendingBadge: { marginLeft: 5, backgroundColor: "#F59E0B" }, container: { flex: 1, }, diff --git a/frontend/hooks/use-friends.ts b/frontend/hooks/use-friends.ts index 1106cb0..2c36125 100644 --- a/frontend/hooks/use-friends.ts +++ b/frontend/hooks/use-friends.ts @@ -136,17 +136,21 @@ export function useFriends(userId?: string): UseFriendsResult { const updateFriendshipMutation = useMutation({ mutationFn: async ({ id, status }: { id: string; status: typeof FRIENDSHIP_STATUS.ACCEPTED | typeof FRIENDSHIP_STATUS.DECLINED }) => { + + console.log('Updating friendship:', { id, status }); const { data, error } = await supabase .from(TABLES.FRIENDSHIPS) .update({ status } as never) .eq('id', id) - .select() - .single(); + .select(); if (error) { + console.error('Error updating friendship:', error); throw new Error(error.message); } + console.log('Updated friendship:', data); + return data; }, onSuccess: async () => { From cc7ce0f116d134d18d1f8f379850c24aaf51b8e1 Mon Sep 17 00:00:00 2001 From: Fortune Oluwasemilore Alebiosu Date: Thu, 11 Dec 2025 22:42:10 -0500 Subject: [PATCH 08/10] added new packages --- frontend/bun.lock | 50 +++++++++++++++++++++++++++++++++++++++++-- frontend/package.json | 3 ++- 2 files changed, 50 insertions(+), 3 deletions(-) diff --git a/frontend/bun.lock b/frontend/bun.lock index e1bc552..6e19bd5 100644 --- a/frontend/bun.lock +++ b/frontend/bun.lock @@ -1,6 +1,5 @@ { "lockfileVersion": 1, - "configVersion": 0, "workspaces": { "": { "name": "bolt-expo-starter", @@ -48,6 +47,7 @@ "expo-symbols": "~1.0.7", "expo-system-ui": "~6.0.7", "expo-task-manager": "~14.0.7", + "expo-updates": "~29.0.14", "expo-video": "~3.0.11", "expo-web-browser": "~15.0.7", "lucide-react-native": "^0.475.0", @@ -340,6 +340,10 @@ "@ide/backoff": ["@ide/backoff@1.0.0", "", {}, "sha512-F0YfUDjvT+Mtt/R4xdl2X0EYCHMMiJqNLdxHD++jDT5ydEFIyqbCHh51Qx2E211dgZprPKhV7sHmnXKpLuvc5g=="], + "@isaacs/balanced-match": ["@isaacs/balanced-match@4.0.1", "", {}, "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ=="], + + "@isaacs/brace-expansion": ["@isaacs/brace-expansion@5.0.0", "", { "dependencies": { "@isaacs/balanced-match": "^4.0.1" } }, "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA=="], + "@isaacs/cliui": ["@isaacs/cliui@8.0.2", "", { "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", "strip-ansi": "^7.0.1", "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", "wrap-ansi": "^8.1.0", "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" } }, "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA=="], "@isaacs/fs-minipass": ["@isaacs/fs-minipass@4.0.1", "", { "dependencies": { "minipass": "^7.0.4" } }, "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w=="], @@ -640,7 +644,7 @@ "anymatch": ["anymatch@3.1.3", "", { "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" } }, "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw=="], - "arg": ["arg@5.0.2", "", {}, "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg=="], + "arg": ["arg@4.1.0", "", {}, "sha512-ZWc51jO3qegGkVh8Hwpv636EkbesNV5ZNQPCtRa+0qytRYPEs9IYT9qITY9buezqUH5uqyzlWLcufrzU2rffdg=="], "argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="], @@ -976,6 +980,8 @@ "expo-document-picker": ["expo-document-picker@14.0.7", "", { "peerDependencies": { "expo": "*" } }, "sha512-81Jh8RDD0GYBUoSTmIBq30hXXjmkDV1ZY2BNIp1+3HR5PDSh2WmdhD/Ezz5YFsv46hIXHsQc+Kh1q8vn6OLT9Q=="], + "expo-eas-client": ["expo-eas-client@1.0.8", "", {}, "sha512-5or11NJhSeDoHHI6zyvQDW2cz/yFyE+1Cz8NTs5NK8JzC7J0JrkUgptWtxyfB6Xs/21YRNifd3qgbBN3hfKVgA=="], + "expo-file-system": ["expo-file-system@19.0.12", "", { "peerDependencies": { "expo": "*", "react-native": "*" } }, "sha512-gqpxpnjfhzXLcqMOi49isB5S1Af49P9410fsaFfnLZWN3X6Dwc8EplDwbaolOI/wnGwP81P+/nDn5RNmU6m7mQ=="], "expo-font": ["expo-font@14.0.8", "", { "dependencies": { "fontfaceobserver": "^2.1.0" }, "peerDependencies": { "expo": "*", "react": "*", "react-native": "*" } }, "sha512-bTUHaJWRZ7ywP8dg3f+wfOwv6RwMV3mWT2CDUIhsK70GjNGlCtiWOCoHsA5Od/esPaVxqc37cCBvQGQRFStRlA=="], @@ -1012,12 +1018,16 @@ "expo-status-bar": ["expo-status-bar@3.0.8", "", { "dependencies": { "react-native-is-edge-to-edge": "^1.2.1" }, "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-L248XKPhum7tvREoS1VfE0H6dPCaGtoUWzRsUv7hGKdiB4cus33Rc0sxkWkoQ77wE8stlnUlL5lvmT0oqZ3ZBw=="], + "expo-structured-headers": ["expo-structured-headers@5.0.0", "", {}, "sha512-RmrBtnSphk5REmZGV+lcdgdpxyzio5rJw8CXviHE6qH5pKQQ83fhMEcigvrkBdsn2Efw2EODp4Yxl1/fqMvOZw=="], + "expo-symbols": ["expo-symbols@1.0.7", "", { "dependencies": { "sf-symbols-typescript": "^2.0.0" }, "peerDependencies": { "expo": "*", "react-native": "*" } }, "sha512-ZqFUeTXbwO6BrE00n37wTXYfJmsjFrfB446jeB9k9w7aA8a6eugNUIzNsUIUfbFWoOiY4wrGmpLSLPBwk4PH+g=="], "expo-system-ui": ["expo-system-ui@6.0.7", "", { "dependencies": { "@react-native/normalize-colors": "0.81.4", "debug": "^4.3.2" }, "peerDependencies": { "expo": "*", "react-native": "*", "react-native-web": "*" }, "optionalPeers": ["react-native-web"] }, "sha512-NT+/r/BOg08lFI9SZO2WFi9X1ZmawkVStknioWzQq6Mt4KinoMS6yl3eLbyOLM3LoptN13Ywfo4W5KHA6TV9Ow=="], "expo-task-manager": ["expo-task-manager@14.0.7", "", { "dependencies": { "unimodules-app-loader": "~6.0.7" }, "peerDependencies": { "expo": "*", "react-native": "*" } }, "sha512-wZRksJg4+Me1wDYmv0wnGh5I30ZOkEpjdXECp/cTKbON1ISQgnaz+4B2eJtljvEPYC1ocBdpAGmz9N0CPtc4mg=="], + "expo-updates": ["expo-updates@29.0.14", "", { "dependencies": { "@expo/code-signing-certificates": "0.0.5", "@expo/plist": "^0.4.7", "@expo/spawn-async": "^1.7.2", "arg": "4.1.0", "chalk": "^4.1.2", "debug": "^4.3.4", "expo-eas-client": "~1.0.7", "expo-manifests": "~1.0.9", "expo-structured-headers": "~5.0.0", "expo-updates-interface": "~2.0.0", "getenv": "^2.0.0", "glob": "^13.0.0", "ignore": "^5.3.1", "resolve-from": "^5.0.0" }, "peerDependencies": { "expo": "*", "react": "*", "react-native": "*" }, "bin": { "expo-updates": "bin/cli.js" } }, "sha512-VgXtjczQ4A/r4Jy/XEj+jWimk0vSd+GdDsYfLzl3CG/9fyQ6NXDP20PgiGfeF+A9rfA4IU3VyWdNJFBPyPPIgg=="], + "expo-updates-interface": ["expo-updates-interface@2.0.0", "", { "peerDependencies": { "expo": "*" } }, "sha512-pTzAIufEZdVPKql6iMi5ylVSPqV1qbEopz9G6TSECQmnNde2nwq42PxdFBaUEd8IZJ/fdJLQnOT3m6+XJ5s7jg=="], "expo-video": ["expo-video@3.0.11", "", { "peerDependencies": { "expo": "*", "react": "*", "react-native": "*" } }, "sha512-k/xz8Ml/LekuD2U2LomML2mUISvkHzYDz3fXY8Au1fEaYVNTfTs7Gyfo1lvF6S1X7u3XutoAfew8e8e1ZUR2fg=="], @@ -1038,6 +1048,8 @@ "fbjs-css-vars": ["fbjs-css-vars@1.0.2", "", {}, "sha512-b2XGFAFdWZWg0phtAWLHCk836A1Xann+I+Dgd3Gk64MHKZO44FfoD1KxyvbSh0qZsIoXQGGlVztIY+oitJPpRQ=="], + "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], + "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], "filter-obj": ["filter-obj@1.1.0", "", {}, "sha512-8rXg1ZnX7xzy2NGDVkBVaAy+lSlPNwad13BtgSlLuxfIslyt5Vg64U7tFcCt4WS1R0hvtnQybT/IyCkGZ3DpXQ=="], @@ -1786,6 +1798,8 @@ "throat": ["throat@5.0.0", "", {}, "sha512-fcwX4mndzpLQKBS1DVYhGAcYaYt7vsHNIvQV+WXMvnow5cgjPphq5CaayLaGsjRdSCKZFNGt7/GYAuXaNOiYCA=="], + "tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="], + "tmpl": ["tmpl@1.0.5", "", {}, "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw=="], "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], @@ -1926,6 +1940,8 @@ "@expo/cli/@expo/config": ["@expo/config@12.0.8", "", { "dependencies": { "@babel/code-frame": "~7.10.4", "@expo/config-plugins": "~54.0.0", "@expo/config-types": "^54.0.7", "@expo/json-file": "^10.0.7", "deepmerge": "^4.3.1", "getenv": "^2.0.0", "glob": "^10.4.2", "require-from-string": "^2.0.2", "resolve-from": "^5.0.0", "resolve-workspace-root": "^2.0.0", "semver": "^7.6.0", "slugify": "^1.3.4", "sucrase": "3.35.0" } }, "sha512-yFadXa5Cmja57EVOSyEYV1hF7kCaSbPnd1twx0MfvTr1Yj2abIbrEu2MUZqcvElNQOtgADnLRP0YJiuEdgoO5A=="], + "@expo/cli/arg": ["arg@5.0.2", "", {}, "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg=="], + "@expo/cli/glob": ["glob@10.4.5", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": "dist/esm/bin.mjs" }, "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg=="], "@expo/cli/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], @@ -1950,6 +1966,8 @@ "@expo/devcert/glob": ["glob@10.4.5", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": "dist/esm/bin.mjs" }, "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg=="], + "@expo/fingerprint/arg": ["arg@5.0.2", "", {}, "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg=="], + "@expo/fingerprint/glob": ["glob@10.4.5", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": "dist/esm/bin.mjs" }, "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg=="], "@expo/fingerprint/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], @@ -2100,6 +2118,10 @@ "expo-router/semver": ["semver@7.6.3", "", { "bin": "bin/semver.js" }, "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A=="], + "expo-updates/expo-manifests": ["expo-manifests@1.0.10", "", { "dependencies": { "@expo/config": "~12.0.11", "expo-json-utils": "~0.15.0" }, "peerDependencies": { "expo": "*" } }, "sha512-oxDUnURPcL4ZsOBY6X1DGWGuoZgVAFzp6PISWV7lPP2J0r8u1/ucuChBgpK7u1eLGFp6sDIPwXyEUCkI386XSQ=="], + + "expo-updates/glob": ["glob@13.0.0", "", { "dependencies": { "minimatch": "^10.1.1", "minipass": "^7.1.2", "path-scurry": "^2.0.0" } }, "sha512-tvZgpqk6fz4BaNZ66ZsRaZnbHvP/jG3uKJvAZOwEVUL4RTA5nJeeLYfyN9/VA8NX/V3IBG+hkeuGpKjvELkVhA=="], + "fbjs/promise": ["promise@7.3.1", "", { "dependencies": { "asap": "~2.0.3" } }, "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg=="], "fbjs/ua-parser-js": ["ua-parser-js@1.0.41", "", { "bin": "script/cli.js" }, "sha512-LbBDqdIC5s8iROCUjMbW1f5dJQTEFB1+KO9ogbvlb3nm9n4YHa5p4KTvFPWvh2Hs8gZMBuiB1/8+pdfe/tDPug=="], @@ -2320,6 +2342,8 @@ "test-exclude/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], + "tinyglobby/picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], + "whatwg-url/webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="], "wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], @@ -2448,6 +2472,14 @@ "expo-router/@expo/metro-runtime/pretty-format": ["pretty-format@29.7.0", "", { "dependencies": { "@jest/schemas": "^29.6.3", "ansi-styles": "^5.0.0", "react-is": "^18.0.0" } }, "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ=="], + "expo-updates/expo-manifests/@expo/config": ["@expo/config@12.0.11", "", { "dependencies": { "@babel/code-frame": "~7.10.4", "@expo/config-plugins": "~54.0.3", "@expo/config-types": "^54.0.9", "@expo/json-file": "^10.0.7", "deepmerge": "^4.3.1", "getenv": "^2.0.0", "glob": "^13.0.0", "require-from-string": "^2.0.2", "resolve-from": "^5.0.0", "resolve-workspace-root": "^2.0.0", "semver": "^7.6.0", "slugify": "^1.3.4", "sucrase": "~3.35.1" } }, "sha512-bGKNCbHirwgFlcOJHXpsAStQvM0nU3cmiobK0o07UkTfcUxl9q9lOQQh2eoMGqpm6Vs1IcwBpYye6thC3Nri/w=="], + + "expo-updates/glob/minimatch": ["minimatch@10.1.1", "", { "dependencies": { "@isaacs/brace-expansion": "^5.0.0" } }, "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ=="], + + "expo-updates/glob/minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="], + + "expo-updates/glob/path-scurry": ["path-scurry@2.0.1", "", { "dependencies": { "lru-cache": "^11.0.0", "minipass": "^7.1.2" } }, "sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA=="], + "expo/@expo/config/@babel/code-frame": ["@babel/code-frame@7.10.4", "", { "dependencies": { "@babel/highlight": "^7.10.4" } }, "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg=="], "expo/@expo/config/glob": ["glob@10.4.5", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": "dist/esm/bin.mjs" }, "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg=="], @@ -2614,6 +2646,18 @@ "expo-router/@expo/metro-runtime/pretty-format/@jest/schemas": ["@jest/schemas@29.6.3", "", { "dependencies": { "@sinclair/typebox": "^0.27.8" } }, "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA=="], + "expo-updates/expo-manifests/@expo/config/@babel/code-frame": ["@babel/code-frame@7.10.4", "", { "dependencies": { "@babel/highlight": "^7.10.4" } }, "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg=="], + + "expo-updates/expo-manifests/@expo/config/@expo/config-plugins": ["@expo/config-plugins@54.0.3", "", { "dependencies": { "@expo/config-types": "^54.0.9", "@expo/json-file": "~10.0.7", "@expo/plist": "^0.4.7", "@expo/sdk-runtime-versions": "^1.0.0", "chalk": "^4.1.2", "debug": "^4.3.5", "getenv": "^2.0.0", "glob": "^13.0.0", "resolve-from": "^5.0.0", "semver": "^7.5.4", "slash": "^3.0.0", "slugify": "^1.6.6", "xcode": "^3.0.1", "xml2js": "0.6.0" } }, "sha512-tBIUZIxLQfCu5jmqTO+UOeeDUGIB0BbK6xTMkPRObAXRQeTLPPfokZRCo818d2owd+Bcmq1wBaDz0VY3g+glfw=="], + + "expo-updates/expo-manifests/@expo/config/@expo/config-types": ["@expo/config-types@54.0.9", "", {}, "sha512-Llf4jwcrAnrxgE5WCdAOxtMf8FGwS4Sk0SSgI0NnIaSyCnmOCAm80GPFvsK778Oj19Ub4tSyzdqufPyeQPksWw=="], + + "expo-updates/expo-manifests/@expo/config/semver": ["semver@7.7.2", "", { "bin": "bin/semver.js" }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="], + + "expo-updates/expo-manifests/@expo/config/sucrase": ["sucrase@3.35.1", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.2", "commander": "^4.0.0", "lines-and-columns": "^1.1.6", "mz": "^2.7.0", "pirates": "^4.0.1", "tinyglobby": "^0.2.11", "ts-interface-checker": "^0.1.9" }, "bin": { "sucrase": "bin/sucrase", "sucrase-node": "bin/sucrase-node" } }, "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw=="], + + "expo-updates/glob/path-scurry/lru-cache": ["lru-cache@11.2.4", "", {}, "sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg=="], + "expo/@expo/config/glob/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], "expo/@expo/config/glob/minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="], @@ -2674,6 +2718,8 @@ "expo-router/@expo/metro-runtime/pretty-format/@jest/schemas/@sinclair/typebox": ["@sinclair/typebox@0.27.8", "", {}, "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA=="], + "expo-updates/expo-manifests/@expo/config/sucrase/commander": ["commander@4.1.1", "", {}, "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA=="], + "jest-runner/jest-message-util/pretty-format/@jest/schemas/@sinclair/typebox": ["@sinclair/typebox@0.27.8", "", {}, "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA=="], "jest-runtime/jest-message-util/pretty-format/@jest/schemas/@sinclair/typebox": ["@sinclair/typebox@0.27.8", "", {}, "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA=="], diff --git a/frontend/package.json b/frontend/package.json index eaab9db..f6f4d0e 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,5 +1,5 @@ { - "name": "bolt-expo-starter", + "name": "keepsafe", "main": "expo-router/entry", "version": "1.0.0", "private": true, @@ -62,6 +62,7 @@ "expo-symbols": "~1.0.7", "expo-system-ui": "~6.0.7", "expo-task-manager": "~14.0.7", + "expo-updates": "~29.0.14", "expo-video": "~3.0.11", "expo-web-browser": "~15.0.7", "lucide-react-native": "^0.475.0", From 40b5852385cc7948a3a7e8a1674aa5942bb9a0d3 Mon Sep 17 00:00:00 2001 From: Fortune Oluwasemilore Alebiosu Date: Fri, 12 Dec 2025 00:03:53 -0500 Subject: [PATCH 09/10] fixed code review issues --- frontend/app.json | 6 ----- frontend/app/onboarding/invite.tsx | 2 +- frontend/hooks/use-friends.ts | 12 +++++----- frontend/hooks/use-invite-acceptance.ts | 6 +---- frontend/hooks/use-profile.ts | 2 +- frontend/hooks/use-user-invite.ts | 6 +++-- frontend/lib/utils.ts | 32 ++++++++++++++++++------- frontend/services/friend-service.ts | 15 ++---------- frontend/services/invite-service.ts | 18 +++++++------- 9 files changed, 47 insertions(+), 52 deletions(-) diff --git a/frontend/app.json b/frontend/app.json index 7492dc6..56b31ed 100644 --- a/frontend/app.json +++ b/frontend/app.json @@ -57,12 +57,6 @@ "android.permission.RECORD_AUDIO", "android.permission.CAMERA", "android.permission.READ_CONTACTS", - "android.permission.WRITE_CONTACTS", - "android.permission.MODIFY_AUDIO_SETTINGS", - "android.permission.RECORD_AUDIO", - "android.permission.CAMERA", - "android.permission.READ_CONTACTS", - "android.permission.WRITE_CONTACTS", "android.permission.MODIFY_AUDIO_SETTINGS" ], "package": "com.fortune710.keepsafe" diff --git a/frontend/app/onboarding/invite.tsx b/frontend/app/onboarding/invite.tsx index de57f78..4c82e7b 100644 --- a/frontend/app/onboarding/invite.tsx +++ b/frontend/app/onboarding/invite.tsx @@ -47,7 +47,7 @@ export default function InviteScreen() { }; const handleContinue = () => { - return router.replace('/onboarding/auth?mode=signup'); + return router.replace('/onboarding/auth?mode=signin'); }; // If we don't have a user id, just let the user skip this step. diff --git a/frontend/hooks/use-friends.ts b/frontend/hooks/use-friends.ts index 2c36125..990420d 100644 --- a/frontend/hooks/use-friends.ts +++ b/frontend/hooks/use-friends.ts @@ -137,20 +137,20 @@ export function useFriends(userId?: string): UseFriendsResult { const updateFriendshipMutation = useMutation({ mutationFn: async ({ id, status }: { id: string; status: typeof FRIENDSHIP_STATUS.ACCEPTED | typeof FRIENDSHIP_STATUS.DECLINED }) => { - console.log('Updating friendship:', { id, status }); + if (__DEV__) console.log('Updating friendship:', { id, status }); const { data, error } = await supabase .from(TABLES.FRIENDSHIPS) .update({ status } as never) .eq('id', id) - .select(); - + .select() + .single(); + if (error) { - console.error('Error updating friendship:', error); + if (__DEV__) console.error('Error updating friendship:', error); throw new Error(error.message); } - console.log('Updated friendship:', data); - + if (__DEV__) console.log('Updated friendship:', data); return data; }, onSuccess: async () => { diff --git a/frontend/hooks/use-invite-acceptance.ts b/frontend/hooks/use-invite-acceptance.ts index 7b7706d..b42fbe4 100644 --- a/frontend/hooks/use-invite-acceptance.ts +++ b/frontend/hooks/use-invite-acceptance.ts @@ -2,10 +2,6 @@ import { useState, useCallback } from 'react'; import { useMutation, useQuery } from '@tanstack/react-query'; import { supabase } from '@/lib/supabase'; import { TABLES, FRIENDSHIP_STATUS } from '@/constants/supabase'; -import { Database } from '@/types/database'; - -type Invite = Database['public']['Tables']['invites']['Row']; -type Profile = Database['public']['Tables']['profiles']['Row']; export interface InviteData { id: string; @@ -57,7 +53,7 @@ export function useInviteAcceptance(inviteId?: string): UseInviteAcceptanceResul } - // Create friendship with accepted status + // Create friendship with pending status (requires follow-up acceptance) const { data: friendship, error: friendshipError } = await supabase .from(TABLES.FRIENDSHIPS) .insert({ diff --git a/frontend/hooks/use-profile.ts b/frontend/hooks/use-profile.ts index 9babc8a..1b5ecf9 100644 --- a/frontend/hooks/use-profile.ts +++ b/frontend/hooks/use-profile.ts @@ -48,7 +48,7 @@ export function useProfile(): UseProfileResult { username: null, avatar_url: null, bio: null, - invite_code: generateInviteCode(), + invite_code: await generateInviteCode(), }; const { data: newProfile, error: createError } = await supabase diff --git a/frontend/hooks/use-user-invite.ts b/frontend/hooks/use-user-invite.ts index 8b60702..04f2ab0 100644 --- a/frontend/hooks/use-user-invite.ts +++ b/frontend/hooks/use-user-invite.ts @@ -9,10 +9,11 @@ interface UseUserInviteResult { isLoading: boolean; isError: boolean; error: unknown; + refetch: () => 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, queryFn: async () => { @@ -25,9 +26,10 @@ export function useUserInvite(userId?: string): UseUserInviteResult { return { invite: data, - isLoading, + isLoading: isPending, isError, error, + refetch, }; } diff --git a/frontend/lib/utils.ts b/frontend/lib/utils.ts index 3f5fa06..0dc31e9 100644 --- a/frontend/lib/utils.ts +++ b/frontend/lib/utils.ts @@ -1,6 +1,7 @@ import { EntryWithProfile } from "@/types/entries"; import { MediaType } from "@/types/media" import { TZDate } from "@date-fns/tz"; +import { getRandomBytesAsync } from 'expo-crypto'; export const getDefaultAvatarUrl = (fullName: string) => { return `https://api.dicebear.com/9.x/adventurer-neutral/png?seed=${fullName}` @@ -37,18 +38,31 @@ export const isLocalFile = (uri: string) => { } export const isBase64File = (uri: string) => { - return uri.startsWith('data:'); + return uri.startsWith('data:'); } -export function generateInviteCode(): string { - const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; - let result = ''; - - for (let i = 0; i < 8; i++) { - result += characters.charAt(Math.floor(Math.random() * characters.length)); +const INVITE_CHARSET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; +const INVITE_CODE_LENGTH = 8; + +export async function generateInviteCode(): Promise { + const charsetSize = INVITE_CHARSET.length; + const maxMultiple = Math.floor(256 / charsetSize) * charsetSize; + let result = ''; + + while (result.length < INVITE_CODE_LENGTH) { + const bytes = await getRandomBytesAsync(INVITE_CODE_LENGTH); + + for (let i = 0; i < bytes.length && result.length < INVITE_CODE_LENGTH; i++) { + const randomByte = bytes[i]; + // Rejection sampling to avoid modulo bias + if (randomByte >= maxMultiple) continue; + + const index = randomByte % charsetSize; + result += INVITE_CHARSET[index]; } - - return result; + } + + return result; } export const generateDeepLinkUrl = () => { diff --git a/frontend/services/friend-service.ts b/frontend/services/friend-service.ts index 9dd1537..91982dd 100644 --- a/frontend/services/friend-service.ts +++ b/frontend/services/friend-service.ts @@ -6,7 +6,7 @@ import { supabase } from '@/lib/supabase'; import { TABLES } from '@/constants/supabase'; import { Database } from '@/types/database'; import { deviceStorage } from './device-storage'; -import { generateDeepLinkUrl } from '@/lib/utils'; +import { generateDeepLinkUrl, generateInviteCode } from '@/lib/utils'; type Profile = Database['public']['Tables']['profiles']['Row'] @@ -16,7 +16,7 @@ export class FriendService { static async generateInviteLink(): Promise { try { // Generate a unique invite code - const inviteCode = this.generateInviteCode(); + const inviteCode = await generateInviteCode(); // Create invite link const inviteLink: InviteLink = { @@ -163,17 +163,6 @@ export class FriendService { } } - private static generateInviteCode(): string { - const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; - let result = ''; - - for (let i = 0; i < 8; i++) { - result += characters.charAt(Math.floor(Math.random() * characters.length)); - } - - return result; - } - static formatInviteUrl(code: string): string { return `${this.BASE_INVITE_URL}/${code}`; } diff --git a/frontend/services/invite-service.ts b/frontend/services/invite-service.ts index 22f8c09..6c8684e 100644 --- a/frontend/services/invite-service.ts +++ b/frontend/services/invite-service.ts @@ -1,18 +1,14 @@ import { TABLES } from "@/constants/supabase"; import { supabase } from "@/lib/supabase"; import { Database } from "@/types/database"; +import { generateInviteCode } from "@/lib/utils"; type Invite = Database['public']['Tables']['invites']['Row']; export class InviteService { static readonly MAX_INVITE_USES = 10; - static generateInviteCode(): string { - const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; - let result = ''; - for (let i = 0; i < 8; i++) { - result += characters.charAt(Math.floor(Math.random() * characters.length)); - } - return result; + static async generateInviteCode(): Promise { + return generateInviteCode(); } static async createInvite(inviterId: string, inviteCode: string): Promise { @@ -20,7 +16,7 @@ export class InviteService { inviter_id: inviterId, invite_code: inviteCode, max_uses: this.MAX_INVITE_USES, - } as never, { onConflict: 'invite_code' }); + } as never, { onConflict: 'inviter_id' }); if (invite.error) { throw new Error(invite.error.message); @@ -32,10 +28,14 @@ export class InviteService { .from(TABLES.INVITES) .select('*') .eq('inviter_id', userId) - .single(); + .maybeSingle(); if (error) { throw new Error(error.message); } + + if (!invite) { + throw new Error('Invite not found'); + } return invite; } From 3c9a235ebf251bac50f0a5512d5e652dd82835ab Mon Sep 17 00:00:00 2001 From: Fortune Oluwasemilore Alebiosu Date: Fri, 12 Dec 2025 00:17:02 -0500 Subject: [PATCH 10/10] made improvements to service --- frontend/app/settings/index.tsx | 15 --------------- .../components/friends/suggested-friend-item.tsx | 12 ++++++------ .../components/friends/suggested-friends-list.tsx | 8 ++++++-- 3 files changed, 12 insertions(+), 23 deletions(-) diff --git a/frontend/app/settings/index.tsx b/frontend/app/settings/index.tsx index e3525b5..16296f4 100644 --- a/frontend/app/settings/index.tsx +++ b/frontend/app/settings/index.tsx @@ -2,7 +2,6 @@ import React, { useState } from 'react'; import { View, Text, TouchableOpacity, StyleSheet, ScrollView, Image, Alert } from 'react-native'; import { router } from 'expo-router'; import { ChevronRight, User, Bell, Shield, HardDrive, Info, LogOut, Trash2 } from 'lucide-react-native'; -import { Gesture } from 'react-native-gesture-handler'; import { useAuthContext } from '@/providers/auth-provider'; import { SafeAreaView } from 'react-native-safe-area-context'; import { supabase } from '@/lib/supabase'; @@ -63,20 +62,6 @@ const settingsItems: SettingsItem[] = [ export default function SettingsScreen() { const { profile, session } = useAuthContext(); const [isDeleting, setIsDeleting] = useState(false); - - // Swipe down from top to close settings - const swipeDownGesture = Gesture.Pan() - .onUpdate((event) => { - // Only allow downward swipes from the top area - if (event.translationY > 0 && event.absoluteY < 100) { - // Handle swipe down animation here if needed - } - }) - .onEnd((event) => { - if (event.translationY > 100 && event.velocityY > 500 && event.absoluteY < 200) { - router.back(); - } - }); const handleLogout = async () => { try { diff --git a/frontend/components/friends/suggested-friend-item.tsx b/frontend/components/friends/suggested-friend-item.tsx index 4b3513b..82018d4 100644 --- a/frontend/components/friends/suggested-friend-item.tsx +++ b/frontend/components/friends/suggested-friend-item.tsx @@ -15,13 +15,12 @@ import { useToast } from '@/hooks/use-toast'; interface FriendItemProps { friend: SuggestedFriend; - onAccept?: (friendshipId: string) => void; - index?: number; + index: number; } -export default function SuggestedFriendItem({ friend, onAccept, index = 0 }: FriendItemProps) { +export default function SuggestedFriendItem({ friend, index }: FriendItemProps) { const { profile } = useAuthContext(); - const { acceptInvite: sendFriendRequest } = useInviteAcceptance(); + const { acceptInvite: sendFriendRequest, isProcessing } = useInviteAcceptance(); const { toast: showToast } = useToast(); const handleAccept = async () => { @@ -30,9 +29,9 @@ export default function SuggestedFriendItem({ friend, onAccept, index = 0 }: Fri } const result = await sendFriendRequest(friend.id, profile.id); if (result.success) { - showToast('Friend request sent', 'success'); + return showToast('Friend request sent', 'success'); } else { - showToast(result.message || 'Failed to send friend request', 'error'); + return showToast(result.message || 'Failed to send friend request', 'error'); } } @@ -58,6 +57,7 @@ export default function SuggestedFriendItem({ friend, onAccept, index = 0 }: Fri style={styles.addButton} onPress={handleAccept} hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }} + disabled={isProcessing} > Add diff --git a/frontend/components/friends/suggested-friends-list.tsx b/frontend/components/friends/suggested-friends-list.tsx index 9f5498b..46bb119 100644 --- a/frontend/components/friends/suggested-friends-list.tsx +++ b/frontend/components/friends/suggested-friends-list.tsx @@ -23,8 +23,12 @@ export default function SuggestedFriendsList({ friends }: SuggestedFriendsListPr { - friends.map((friend) => ( - + friends.map((friend, index) => ( + )) }