Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions frontend/app.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
},
Expand Down Expand Up @@ -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"
}
}
}
21 changes: 17 additions & 4 deletions frontend/app/onboarding/auth.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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()
});
Expand All @@ -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 {
Expand Down
149 changes: 96 additions & 53 deletions frontend/app/onboarding/invite.tsx
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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 (
<View style={styles.container}>
<Animated.View entering={FadeInUp.delay(200)} style={styles.content}>
<Text style={styles.title}>Invite Your Friends</Text>
<Text style={styles.subtitle}>
We couldn&apos;t find your account information. You can skip this step and start
using Keepsafe.
</Text>
<TouchableOpacity style={styles.continueButton} onPress={handleSkip}>
<Text style={styles.continueButtonText}>Skip for now</Text>
<ArrowRight color="#8B5CF6" size={20} />
</TouchableOpacity>
</Animated.View>
</View>
);
}

if (isLoading) {
return (
<View style={styles.container}>
<Animated.View entering={FadeInUp.delay(200)} style={styles.content}>
<View style={styles.loadingContainer}>
<Text style={styles.loadingText}>Preparing your invite link...</Text>
</View>
</Animated.View>
<Animated.View entering={FadeInDown.delay(800)} style={styles.footer}>
<TouchableOpacity style={styles.skipButton} onPress={handleSkip}>
<Text style={styles.skipText}>Skip for now</Text>
</TouchableOpacity>
</Animated.View>
</View>
);
}

if (isError || !inviteCode) {
return (
<View style={styles.container}>
<Animated.View entering={FadeInUp.delay(200)} style={styles.content}>
<Text style={styles.title}>Invite Unavailable</Text>
<Text style={styles.subtitle}>
We couldn&apos;t load your invite link right now. You can skip this step and start
using Keepsafe.
</Text>
<TouchableOpacity style={styles.continueButton} onPress={handleSkip}>
<Text style={styles.continueButtonText}>Skip for now</Text>
<ArrowRight color="#8B5CF6" size={20} />
</TouchableOpacity>
</Animated.View>
</View>
);
}

return (
<View style={styles.container}>
<Animated.View entering={FadeInUp.delay(200)} style={styles.content}>
Expand All @@ -67,38 +116,32 @@ export default function InviteScreen() {
Share moments with the people who matter most. Send them your invite link to get started.
</Text>

{isGenerating ? (
<Animated.View entering={FadeInDown.delay(400)} style={styles.loadingContainer}>
<Text style={styles.loadingText}>Generating your invite link...</Text>
</Animated.View>
) : (
<Animated.View entering={FadeInDown.delay(600)} style={styles.linkContainer}>
<View style={styles.linkBox}>
<Text style={styles.linkText} numberOfLines={1}>
{inviteLink}
</Text>
<TouchableOpacity style={styles.copyButton} onPress={handleCopyLink}>
<Copy color="#8B5CF6" size={20} />
</TouchableOpacity>
</View>

<Text style={styles.linkInfo}>
This link can be used 10 times
<Animated.View entering={FadeInDown.delay(600)} style={styles.linkContainer}>
<View style={styles.linkBox}>
<Text style={styles.linkText} numberOfLines={1}>
{inviteLink}
</Text>
<TouchableOpacity style={styles.copyButton} onPress={handleCopyLink}>
<Copy color="#8B5CF6" size={20} />
</TouchableOpacity>
</View>

<Text style={styles.linkInfo}>
This link can be used {InviteService.MAX_INVITE_USES} times
</Text>

<View style={styles.actionButtons}>
<TouchableOpacity style={styles.shareButton} onPress={handleShareLink}>
<ShareIcon color="white" size={20} />
<Text style={styles.shareButtonText}>Share Link</Text>
</TouchableOpacity>

<View style={styles.actionButtons}>
<TouchableOpacity style={styles.shareButton} onPress={handleShareLink}>
<ShareIcon color="white" size={20} />
<Text style={styles.shareButtonText}>Share Link</Text>
</TouchableOpacity>

<TouchableOpacity style={styles.continueButton} onPress={handleContinue}>
<Text style={styles.continueButtonText}>Continue to App</Text>
<ArrowRight color="#8B5CF6" size={20} />
</TouchableOpacity>
</View>
</Animated.View>
)}
<TouchableOpacity style={styles.continueButton} onPress={handleContinue}>
<Text style={styles.continueButtonText}>Continue to Login</Text>
<ArrowRight color="#8B5CF6" size={20} />
</TouchableOpacity>
</View>
</Animated.View>
</Animated.View>

<Animated.View entering={FadeInDown.delay(800)} style={styles.footer}>
Expand Down
18 changes: 1 addition & 17 deletions frontend/app/settings/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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;
}

Expand Down
Loading