diff --git a/frontend/app.json b/frontend/app.json index bfd0a83..56b31ed 100644 --- a/frontend/app.json +++ b/frontend/app.json @@ -55,7 +55,9 @@ ], "permissions": [ "android.permission.RECORD_AUDIO", - "android.permission.CAMERA" + "android.permission.CAMERA", + "android.permission.READ_CONTACTS", + "android.permission.MODIFY_AUDIO_SETTINGS" ], "package": "com.fortune710.keepsafe" }, @@ -129,6 +131,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/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/app/onboarding/invite.tsx b/frontend/app/onboarding/invite.tsx index 1e0ef91..4c82e7b 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=signin'); }; + // 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/app/settings/index.tsx b/frontend/app/settings/index.tsx index a366495..16296f4 100644 --- a/frontend/app/settings/index.tsx +++ b/frontend/app/settings/index.tsx @@ -2,8 +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, GestureDetector } from 'react-native-gesture-handler'; -import Animated, { SlideInDown, SlideOutUp } from 'react-native-reanimated'; import { useAuthContext } from '@/providers/auth-provider'; import { SafeAreaView } from 'react-native-safe-area-context'; import { supabase } from '@/lib/supabase'; @@ -64,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 { @@ -106,7 +90,7 @@ export default function SettingsScreen() { // Guard clause for missing session/token if (!session?.access_token) { - Alert.alert('Error', 'Authentication token missing. Please sign in again.'); + Alert.alert('Error', 'You need to be signed in to delete your account.'); return; } diff --git a/frontend/bun.lock b/frontend/bun.lock index 62699e5..ea785a2 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", @@ -341,6 +341,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=="], @@ -641,7 +645,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=="], @@ -983,6 +987,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=="], @@ -1019,12 +1025,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=="], @@ -1045,6 +1055,7 @@ "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=="], "fetch-blob": ["fetch-blob@3.2.0", "", { "dependencies": { "node-domexception": "^1.0.0", "web-streams-polyfill": "^3.0.3" } }, "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ=="], "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], @@ -1805,6 +1816,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=="], @@ -1947,6 +1960,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=="], @@ -1973,6 +1988,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=="], @@ -2129,6 +2146,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=="], @@ -2353,6 +2374,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=="], @@ -2487,6 +2510,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=="], @@ -2653,6 +2684,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=="], @@ -2713,6 +2756,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/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/components/friends/suggested-friend-item.tsx b/frontend/components/friends/suggested-friend-item.tsx index d218d26..82018d4 100644 --- a/frontend/components/friends/suggested-friend-item.tsx +++ b/frontend/components/friends/suggested-friend-item.tsx @@ -7,17 +7,33 @@ 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'; interface FriendItemProps { friend: SuggestedFriend; - onAccept?: (friendshipId: string) => void; - index?: number; + index: number; } -export default function SuggestedFriendItem({ friend, onAccept, index = 0 }: FriendItemProps) { - const handleAccept = () => {} +export default function SuggestedFriendItem({ friend, index }: 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'); + } + const result = await sendFriendRequest(friend.id, profile.id); + if (result.success) { + return showToast('Friend request sent', 'success'); + } else { + return showToast(result.message || 'Failed to send friend request', 'error'); + } + } return ( @@ -41,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) => ( + )) } 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": { 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/hooks/use-friends.ts b/frontend/hooks/use-friends.ts index 1106cb0..990420d 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 }) => { + + 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; }, onSuccess: async () => { diff --git a/frontend/hooks/use-invite-acceptance.ts b/frontend/hooks/use-invite-acceptance.ts index 6b89563..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; @@ -34,9 +30,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,15 +52,14 @@ export function useInviteAcceptance(inviteId?: string): UseInviteAcceptanceResul throw new Error('You are already connected with this user'); } - - // 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({ user_id: userId, friend_id: inviteeId, - status: FRIENDSHIP_STATUS.ACCEPTED, + status: FRIENDSHIP_STATUS.PENDING, } as never) .select() .single(); diff --git a/frontend/hooks/use-profile.ts b/frontend/hooks/use-profile.ts index 8855303..1b5ecf9 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; } @@ -47,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 @@ -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() diff --git a/frontend/hooks/use-user-invite.ts b/frontend/hooks/use-user-invite.ts new file mode 100644 index 0000000..04f2ab0 --- /dev/null +++ b/frontend/hooks/use-user-invite.ts @@ -0,0 +1,36 @@ +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; + refetch: () => void; +} + +export function useUserInvite(userId?: string): UseUserInviteResult { + const { data, isPending, isError, error, refetch } = 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: 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/package.json b/frontend/package.json index c424cdf..99e7609 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", 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 }>; 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); } } 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 new file mode 100644 index 0000000..6c8684e --- /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"; +import { generateInviteCode } from "@/lib/utils"; + +type Invite = Database['public']['Tables']['invites']['Row']; + +export class InviteService { + static readonly MAX_INVITE_USES = 10; + static async generateInviteCode(): Promise { + return generateInviteCode(); + } + + 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: 'inviter_id' }); + + 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) + .maybeSingle(); + if (error) { + throw new Error(error.message); + } + + if (!invite) { + throw new Error('Invite not found'); + } + 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