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