Added 'cloud storage' functionality

This commit is contained in:
Christopher Sanden
2026-03-04 02:15:53 +01:00
parent 19b9438ef8
commit 45ab15ff40
29 changed files with 1822 additions and 109 deletions

View File

@@ -1,5 +1,3 @@
# Learn more https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files
# dependencies # dependencies
node_modules/ node_modules/
@@ -34,12 +32,12 @@ yarn-error.*
# local env files # local env files
.env*.local .env*.local
.env.local
.env
# typescript # typescript
*.tsbuildinfo *.tsbuildinfo
app-example
# generated native folders # generated native folders
/ios /ios
/android /android

10
FastNotes/app.config.js Normal file
View File

@@ -0,0 +1,10 @@
import "dotenv/config";
export default ({ config }) => ({
...config,
extra: {
...config.extra,
supabaseUrl: process.env.EXPO_PUBLIC_SUPABASE_URL,
supabaseKey: process.env.EXPO_PUBLIC_SUPABASE_KEY,
},
});

View File

@@ -38,7 +38,9 @@
"backgroundColor": "#000000" "backgroundColor": "#000000"
} }
} }
] ],
"expo-sqlite",
"expo-secure-store"
], ],
"experiments": { "experiments": {
"typedRoutes": true, "typedRoutes": true,

View File

@@ -1,31 +1,54 @@
import { DarkTheme, DefaultTheme, ThemeProvider } from '@react-navigation/native'; import { DefaultTheme, ThemeProvider } from '@react-navigation/native';
import { Stack } from 'expo-router'; import { Stack } from 'expo-router';
import { StatusBar } from 'expo-status-bar'; import { StatusBar } from 'expo-status-bar';
import 'react-native-reanimated'; import 'react-native-reanimated';
import { SafeAreaProvider } from 'react-native-safe-area-context'; import { SafeAreaProvider } from 'react-native-safe-area-context';
import { NotesProvider } from "@/src/notes/NotesContext" import { NotesProvider } from "@/src/notes/NotesContext"
import { useColorScheme } from '@/hooks/use-color-scheme'; import { useColorScheme } from '@/hooks/use-color-scheme';
; import { useAuthContext } from '@/hooks/use-auth-context';
import AuthProvider from '@/providers/auth-provider';
// Separate RootNavigator so we can access the AuthContext
function RootNavigator() {
const { isLoggedIn, isLoading } = useAuthContext()
if (isLoading) {
return null
}
return (
<Stack>
<Stack.Protected guard={isLoggedIn}>
<Stack.Screen name="index" options={{ headerShown: false }} />
<Stack.Screen name="newNote" options={{ headerShown: true, title: 'New Note' }} />
<Stack.Screen name="detail" options={{ headerShown: true, title: 'Note' }} />
</Stack.Protected>
<Stack.Protected guard={!isLoggedIn}>
<Stack.Screen name="login" options={{ headerShown: false }} />
<Stack.Screen name="signup" options={{ headerShown: false }} />
</Stack.Protected>
<Stack.Screen name="+not-found" />
</Stack>
)
}
export default function RootLayout() { export default function RootLayout() {
const colorScheme = useColorScheme(); const colorScheme = useColorScheme()
/*TODO /*TODO
Sort ThemeProvider to work with dark theme on iOS Fix ThemeProvider to work with dark theme
*/ */
return ( return (
<SafeAreaProvider> <SafeAreaProvider>
<NotesProvider> <ThemeProvider value={colorScheme === 'dark' ? DefaultTheme : DefaultTheme}>
<ThemeProvider value={colorScheme === 'dark' ? DefaultTheme : DefaultTheme}> <AuthProvider>
<Stack> <NotesProvider>
<Stack.Screen name="index" options={{ title: 'FastNotes' }} /> <RootNavigator />
<Stack.Screen name="newNote" options={{ title: 'New Note'}}/> </NotesProvider>
<Stack.Screen name="detail" options={{ title: 'Detail'}}/> </AuthProvider>
</Stack> <StatusBar style="auto" />
<StatusBar style="auto" /> </ThemeProvider>
</ThemeProvider>
</NotesProvider>
</SafeAreaProvider> </SafeAreaProvider>
); );
} }

View File

@@ -1,18 +1,158 @@
import { StyleSheet, Text, View } from "react-native"; import { useEffect, useState } from "react";
import { useLocalSearchParams } from "expo-router"; import { Alert, Pressable, StyleSheet, Text, TextInput, View } from "react-native";
import { router, useLocalSearchParams } from "expo-router";
import { useAuthContext } from "@/hooks/use-auth-context";
import { useNotes } from "@/src/notes/NotesContext";
export default function DetailScreen() export default function DetailScreen()
{ {
const { title, content } = useLocalSearchParams< const { id } = useLocalSearchParams<
{ {
title?: string; id?: string;
content?: string;
}>(); }>();
const { claims } = useAuthContext();
const { deleteNote, errorMessage, notes, updateNote } = useNotes();
const note = notes.find((entry) => entry.id === id);
const canEdit = note?.createdBy === claims?.sub;
const [title, setTitle] = useState(note?.title ?? "");
const [content, setContent] = useState(note?.content ?? "");
const [isSaving, setIsSaving] = useState(false);
const [isDeleting, setIsDeleting] = useState(false);
const [localErrorMessage, setLocalErrorMessage] = useState<string | null>(null);
const [statusMessage, setStatusMessage] = useState<string | null>(null);
return( const formatTimestamp = (value: string) => {
const parsed = new Date(value);
if (Number.isNaN(parsed.getTime())) {
return "Unknown";
}
return parsed.toLocaleString();
};
useEffect(() => {
setTitle(note?.title ?? "");
setContent(note?.content ?? "");
}, [note?.content, note?.title]);
const onSave = async () => {
if (!id) {
setLocalErrorMessage("This note could not be found.");
return;
}
if (!title.trim() || !content.trim()) {
setLocalErrorMessage("Title and content are required.");
return;
}
setIsSaving(true);
setLocalErrorMessage(null);
setStatusMessage(null);
const wasSaved = await updateNote(id, title, content);
setIsSaving(false);
if (wasSaved) {
setStatusMessage("Note updated.");
}
};
const confirmDelete = () => {
// Require explicit confirmation before deleting the note.
Alert.alert(
"Delete note",
"Are you sure you want to delete this note?",
[
{
text: "Cancel",
style: "cancel",
},
{
text: "Delete",
style: "destructive",
onPress: () => {
void onDelete();
},
},
]
);
};
const onDelete = async () => {
if (!id) {
setLocalErrorMessage("This note could not be found.");
return;
}
setIsDeleting(true);
setLocalErrorMessage(null);
setStatusMessage(null);
const wasDeleted = await deleteNote(id);
setIsDeleting(false);
if (wasDeleted) {
router.replace("/");
}
};
if (!note) {
return (
<View style={styles.container}>
<Text style={styles.title}>Note not found</Text>
<Text style={styles.content}>The note may have been deleted.</Text>
</View>
);
}
return(
<View style={styles.container}> <View style={styles.container}>
<Text style={styles.title}>{title ?? "(No title)"}</Text> <TextInput
<Text style={styles.content}>{content ?? ""}</Text> editable={canEdit}
onChangeText={setTitle}
style={styles.titleInput}
value={title}
/>
<Text style={styles.signature}>Created by {note.creatorLabel}</Text>
<Text style={styles.signature}>
Last changed {formatTimestamp(note.lastChangedAt)}
</Text>
<TextInput
editable={canEdit}
multiline
onChangeText={setContent}
style={styles.contentInput}
textAlignVertical="top"
value={content}
/>
{!canEdit ? (
<Text style={styles.readOnlyText}>
Only the creator of this note can update or delete it.
</Text>
) : null}
{localErrorMessage ? <Text style={styles.errorText}>{localErrorMessage}</Text> : null}
{!localErrorMessage && errorMessage ? <Text style={styles.errorText}>{errorMessage}</Text> : null}
{statusMessage ? <Text style={styles.successText}>{statusMessage}</Text> : null}
{canEdit ? (
<View style={styles.actions}>
<Pressable disabled={isSaving} onPress={onSave} style={styles.primaryButton}>
<Text style={styles.primaryButtonText}>
{isSaving ? "Saving..." : "Save changes"}
</Text>
</Pressable>
<Pressable disabled={isDeleting} onPress={confirmDelete} style={styles.deleteButton}>
<Text style={styles.deleteButtonText}>
{isDeleting ? "Deleting..." : "Delete note"}
</Text>
</Pressable>
</View>
) : null}
</View> </View>
); );
} }
@@ -22,5 +162,58 @@ const styles = StyleSheet.create(
{ {
container: { flex: 1, padding: 16, gap: 12 }, container: { flex: 1, padding: 16, gap: 12 },
title: { fontSize: 22, fontWeight:"700" }, title: { fontSize: 22, fontWeight:"700" },
content: { fontSize: 16 } content: { fontSize: 16 },
titleInput: {
borderWidth: 1,
borderRadius: 8,
padding: 12,
fontSize: 22,
fontWeight: "700",
},
signature: {
fontSize: 12,
color: "#666",
},
contentInput: {
flex: 1,
minHeight: 200,
borderWidth: 1,
borderRadius: 8,
padding: 12,
fontSize: 16,
},
errorText: {
color: "#c62828",
},
successText: {
color: "#2e7d32",
},
readOnlyText: {
color: "#666",
},
actions: {
gap: 12,
},
primaryButton: {
borderRadius: 12,
paddingVertical: 14,
alignItems: "center",
backgroundColor: "#111",
},
primaryButtonText: {
color: "#fff",
fontSize: 16,
fontWeight: "700",
},
deleteButton: {
borderRadius: 12,
paddingVertical: 14,
alignItems: "center",
backgroundColor: "#b71c1c",
},
deleteButtonText: {
color: "#fff",
fontSize: 16,
fontWeight: "700",
},
}); });

View File

@@ -1,49 +1,181 @@
import { useMemo, useState } from "react";
import { FlatList, Pressable, StyleSheet, Text, View } from "react-native"; import { FlatList, Pressable, StyleSheet, Text, View } from "react-native";
import { router } from "expo-router"; import { router } from "expo-router";
import { useSafeAreaInsets } from "react-native-safe-area-context"; import { useSafeAreaInsets } from "react-native-safe-area-context";
import { useAuthContext } from "@/hooks/use-auth-context";
import { useNotes } from "@/src/notes/NotesContext"; import { useNotes } from "@/src/notes/NotesContext";
import SignOutButton from '@/components/social-auth-buttons/sign-out-button'
type TabKey = "my-notes" | "work-notes"
export default function HomeScreen() export default function HomeScreen()
{ {
const { notes } = useNotes(); const { claims } = useAuthContext();
const { errorMessage, isLoading, notes } = useNotes();
const [activeTab, setActiveTab] = useState<TabKey>("my-notes");
const insets = useSafeAreaInsets(); const insets = useSafeAreaInsets();
const userId = claims?.sub;
const filteredNotes = useMemo(
() =>
notes.filter((note) =>
activeTab === "my-notes" ? note.createdBy === userId : note.createdBy !== userId
),
[activeTab, notes, userId]
);
const emptyText =
activeTab === "my-notes"
? "No personal notes yet. Create your first note."
: "No work notes yet."
const formatTimestamp = (value: string) => {
const parsed = new Date(value)
if (Number.isNaN(parsed.getTime())) {
return "Unknown"
}
return parsed.toLocaleString()
}
return ( return (
<View style={styles.container}> <View style={styles.container}>
<View style={[styles.topBar, { paddingTop: insets.top + 8 }]}>
<Text style={styles.screenTitle}>FastNotes</Text>
<SignOutButton />
</View>
<View style={styles.tabBar}>
<Pressable
onPress={() => setActiveTab("my-notes")}
style={[
styles.tabButton,
activeTab === "my-notes" ? styles.tabButtonActive : null,
]}
>
<Text
style={[
styles.tabButtonText,
activeTab === "my-notes" ? styles.tabButtonTextActive : null,
]}
>
My Notes
</Text>
</Pressable>
<Pressable
onPress={() => setActiveTab("work-notes")}
style={[
styles.tabButton,
activeTab === "work-notes" ? styles.tabButtonActive : null,
]}
>
<Text
style={[
styles.tabButtonText,
activeTab === "work-notes" ? styles.tabButtonTextActive : null,
]}
>
Work Notes
</Text>
</Pressable>
</View>
{errorMessage ? <Text style={styles.errorText}>{errorMessage}</Text> : null}
<FlatList <FlatList
data={notes} data={filteredNotes}
keyExtractor={(n) => n.id} keyExtractor={(n) => n.id}
contentContainerStyle={[styles.list, { paddingBottom: 120 }]} contentContainerStyle={[styles.list, { paddingBottom: 120 }]}
ListEmptyComponent={
<Text style={styles.emptyText}>
{isLoading ? "Loading notes..." : emptyText}
</Text>
}
renderItem={({ item }) => ( renderItem={({ item }) => (
<Pressable <Pressable
style={styles.noteItem} style={styles.noteItem}
onPress={() => onPress={() =>
router.push({ router.push({
pathname: "/detail", pathname: "/detail",
params: { title: item.title, content: item.content }, params: { id: item.id },
}) })
} }
> >
<Text style={styles.noteTitle}>{item.title}</Text> <Text style={styles.noteTitle}>{item.title}</Text>
<Text numberOfLines={2} style={styles.notePreview}>{item.content}</Text>
<Text style={styles.noteMeta}>Created by {item.creatorLabel}</Text>
<Text style={styles.noteMeta}>
Last changed {formatTimestamp(item.lastChangedAt)}
</Text>
</Pressable> </Pressable>
)} )}
/> />
<Pressable {activeTab === "my-notes" ? (
style={[styles.fab, { bottom: insets.bottom + 24, right: insets.right + 40 }]} <Pressable
onPress={() => router.push("/newNote")} style={[styles.fab, { bottom: insets.bottom + 24, right: insets.right + 40 }]}
> onPress={() => router.push("/newNote")}
<Text style={styles.fabText}>+</Text> >
</Pressable> <Text style={styles.fabText}>+</Text>
</Pressable>
) : null}
</View> </View>
); );
} }
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { flex: 1 }, container: { flex: 1 },
list: { padding: 16, gap: 12, paddingTop: 20 }, topBar: {
noteItem: { padding: 16, borderWidth: 1, borderRadius:12 }, flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
paddingHorizontal: 16,
paddingBottom: 8,
},
screenTitle: {
fontSize: 24,
fontWeight: "700",
},
tabBar: {
flexDirection: "row",
gap: 8,
paddingHorizontal: 16,
paddingBottom: 8,
},
tabButton: {
flex: 1,
borderRadius: 999,
paddingVertical: 10,
alignItems: "center",
backgroundColor: "#e6e6e6",
},
tabButtonActive: {
backgroundColor: "#111",
},
tabButtonText: {
fontSize: 14,
fontWeight: "600",
color: "#111",
},
tabButtonTextActive: {
color: "#fff",
},
list: { padding: 16, gap: 12, paddingTop: 8 },
noteItem: { padding: 16, borderWidth: 1, borderRadius:12, gap: 8 },
noteTitle: { fontSize: 16, fontWeight: "600" }, noteTitle: { fontSize: 16, fontWeight: "600" },
notePreview: { fontSize: 14, color: "#444" },
noteMeta: { fontSize: 12, color: "#666" },
emptyText: {
textAlign: "center",
paddingVertical: 32,
color: "#666",
},
errorText: {
color: "#c62828",
paddingHorizontal: 16,
paddingBottom: 8,
},
fab: fab:
{ {
position: "absolute", position: "absolute",

146
FastNotes/app/login.tsx Normal file
View File

@@ -0,0 +1,146 @@
import { useState } from 'react'
import { Link, Stack } from 'expo-router'
import { Pressable, StyleSheet, Text, TextInput, View } from 'react-native'
import { supabase } from '@/libs/supabase'
export default function LoginScreen() {
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const [errorMessage, setErrorMessage] = useState<string | null>(null)
const [isSubmitting, setIsSubmitting] = useState(false)
const onLogin = async () => {
if (!email.trim() || !password) {
setErrorMessage('Log in with email and password')
return
}
setIsSubmitting(true)
setErrorMessage(null)
const { error } = await supabase.auth.signInWithPassword({
email: email.trim(),
password,
})
if (error) {
setErrorMessage(error.message)
}
setIsSubmitting(false)
}
return (
<>
<Stack.Screen options={{ title: 'Login' }} />
<View style={styles.container}>
<Text style={styles.title}>Login</Text>
<Text style={styles.label}>E-mail</Text>
<TextInput
autoCapitalize="none"
autoComplete="email"
keyboardType="email-address"
onChangeText={setEmail}
placeholder="name@email.com"
placeholderTextColor="#666"
style={styles.input}
value={email}
/>
<Text style={styles.label}>Password</Text>
<TextInput
autoCapitalize="none"
autoComplete="password"
onChangeText={setPassword}
placeholder="Password"
placeholderTextColor="#666"
secureTextEntry
style={styles.input}
value={password}
/>
{errorMessage ? (
<Text style={styles.errorText}>{errorMessage}</Text>
) : null}
<Pressable
disabled={isSubmitting}
onPress={onLogin}
style={({ pressed }) => [
styles.loginButton,
pressed && !isSubmitting ? styles.loginButtonPressed : null,
isSubmitting ? styles.loginButtonDisabled : null,
]}
>
<Text style={styles.loginButtonText}>
{isSubmitting ? 'Logging in...' : 'Log in'}
</Text>
</Pressable>
<Link href="/signup" style={styles.link}>
<Text style={styles.linkText}>Create a new account</Text>
</Link>
</View>
</>
)
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
padding: 24,
gap: 12,
backgroundColor: '#f7f7f7',
},
title: {
fontSize: 28,
fontWeight: '700',
color: '#111',
},
label: {
fontSize: 14,
fontWeight: '600',
color: '#111',
},
input: {
borderWidth: 1,
borderColor: '#cfcfcf',
borderRadius: 8,
padding: 12,
fontSize: 16,
color: '#111',
backgroundColor: '#fff',
},
errorText: {
color: '#c62828',
fontSize: 14,
},
loginButton: {
borderRadius: 12,
paddingVertical: 14,
alignItems: 'center',
backgroundColor: '#111',
marginTop: 8,
},
loginButtonPressed: {
opacity: 0.85,
},
loginButtonDisabled: {
opacity: 0.6,
},
loginButtonText: {
color: '#fff',
fontSize: 16,
fontWeight: '700',
},
link: {
alignSelf: 'center',
marginTop: 8,
},
linkText: {
color: '#0b57d0',
fontSize: 16,
},
})

View File

@@ -13,20 +13,34 @@ import { useHeaderHeight } from "@react-navigation/elements";
export default function NewNoteScreen() export default function NewNoteScreen()
{ {
const { addNote } = useNotes(); const { addNote, errorMessage } = useNotes()
const[title, setTitle] = useState(""); const [title, setTitle] = useState("")
const [content, setContent] = useState(""); const [content, setContent] = useState("")
const insets = useSafeAreaInsets(); const [isSaving, setIsSaving] = useState(false)
const headerHeight = useHeaderHeight(); const [localErrorMessage, setLocalErrorMessage] = useState<string | null>(null)
const [contentHeight, setContentHeight] = useState(160); const insets = useSafeAreaInsets()
const scrollRef = useRef<ScrollView>(null); const headerHeight = useHeaderHeight()
const [contentHeight, setContentHeight] = useState(160)
const scrollRef = useRef<ScrollView>(null)
const onSave = () => const onSave = async () =>
{ {
if(!title.trim() && !content.trim()) return; if(!title.trim() || !content.trim()) {
addNote(title, content); setLocalErrorMessage("Title and content are required.")
router.back(); return
}; }
setIsSaving(true)
setLocalErrorMessage(null)
const wasSaved = await addNote(title, content)
setIsSaving(false)
if (wasSaved) {
router.back()
}
}
return ( return (
<KeyboardAvoidingView style={{ flex: 1}} behavior={Platform.OS === "ios" ? "padding" : undefined} <KeyboardAvoidingView style={{ flex: 1}} behavior={Platform.OS === "ios" ? "padding" : undefined}
@@ -47,11 +61,21 @@ export default function NewNoteScreen()
scrollRef.current?.scrollToEnd({ animated: true }); scrollRef.current?.scrollToEnd({ animated: true });
}}/> }}/>
{localErrorMessage ? (
<Text style={styles.errorText}>{localErrorMessage}</Text>
) : null}
{!localErrorMessage && errorMessage ? (
<Text style={styles.errorText}>{errorMessage}</Text>
) : null}
</ScrollView> </ScrollView>
<View> <View>
<Pressable onPress={onSave} <Pressable disabled={isSaving} onPress={onSave}
style={[styles.saveFloating, { bottom: insets.bottom + 16 }]}> style={[styles.saveFloating, { bottom: insets.bottom + 16 }]}>
<Text style={styles.saveFloatingText}>Save</Text> <Text style={styles.saveFloatingText}>
{isSaving ? "Saving..." : "Save"}
</Text>
</Pressable> </Pressable>
</View> </View>
</View> </View>
@@ -101,6 +125,9 @@ const styles = StyleSheet.create(
color: "white", color: "white",
fontSize: 16, fontSize: 16,
fontWeight: "700" fontWeight: "700"
} },
errorText: {
color: "#c62828"
},
} }
); );

179
FastNotes/app/signup.tsx Normal file
View File

@@ -0,0 +1,179 @@
import { supabase } from "@/libs/supabase";
import { Link, Stack } from "expo-router";
import { useState } from "react";
import { Pressable, StyleSheet, Text, TextInput, View } from 'react-native'
export default function SignupScreen(){
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const [errorMessage, setErrorMessage] = useState<string | null>(null)
const [successMessage, setSuccessMessage] = useState<string | null>(null)
const [isSubmitting, setIsSubmitting] = useState(false)
const onSignup = async () => {
if(!email.trim() || !password){
setErrorMessage('Sign up using email and password')
return
}
setIsSubmitting(true)
setErrorMessage(null)
setSuccessMessage(null)
const { data, error } = await supabase.auth.signUp({
email: email.trim(),
password
})
if(error){
setErrorMessage(error.message)
} else if (data.session) {
const user = data.user
if (user) {
const { error: profileError } = await supabase
.from('profiles')
.upsert({
id: user.id,
email: user.email ?? email.trim(),
})
if (profileError) {
setErrorMessage(profileError.message)
}
}
}
if (!error && data.session) {
setSuccessMessage('Account created. You are now signed in.')
} else if (!error) {
setSuccessMessage('Account created. Check your email to confirm the signup.')
}
setIsSubmitting(false)
}
return (
<>
<Stack.Screen options={{title: "Signup"}}/>
<View style={styles.container}>
<Text style={styles.title}>Sign up</Text>
<Text style={styles.label}>E-mail</Text>
<TextInput
autoCapitalize="none"
autoComplete="email"
keyboardType="email-address"
onChangeText={setEmail}
placeholder="name@email.com"
placeholderTextColor="#666"
style={styles.input}
value={email}
/>
<Text style={styles.label} >Password</Text>
<TextInput
autoCapitalize="none"
autoComplete="password"
onChangeText={setPassword}
placeholder="Password"
placeholderTextColor="#666"
secureTextEntry
style={styles.input}
value={password}
/>
{errorMessage ? (
<Text style={styles.errorText}>{errorMessage}</Text>
) : null}
{successMessage ? (
<Text style={styles.successText}>{successMessage}</Text>
) : null}
<Pressable
disabled={isSubmitting}
onPress={onSignup}
style={({ pressed }) => [
styles.actionButton,
pressed && !isSubmitting ? styles.actionButtonPressed : null,
isSubmitting ? styles.actionButtonDisabled : null,
]}
>
<Text style={styles.actionButtonText}>
{isSubmitting ? 'Creating account...' : 'Sign up'}
</Text>
</Pressable>
<Link href="/login" style={styles.link}>
<Text style={styles.linkText}>Already have an account? Log in</Text>
</Link>
</View>
</>
)
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
padding: 24,
gap: 12,
backgroundColor: '#f7f7f7',
},
title: {
fontSize: 28,
fontWeight: '700',
color: '#111',
},
label: {
fontSize: 14,
fontWeight: '600',
color: '#111',
},
input: {
borderWidth: 1,
borderColor: '#cfcfcf',
borderRadius: 8,
padding: 12,
fontSize: 16,
color: '#111',
backgroundColor: '#fff',
},
errorText: {
color: '#c62828',
fontSize: 14,
},
successText: {
color: '#2e7d32',
fontSize: 14,
},
actionButton: {
borderRadius: 12,
paddingVertical: 14,
alignItems: 'center',
backgroundColor: '#111',
marginTop: 8,
},
actionButtonPressed: {
opacity: 0.85,
},
actionButtonDisabled: {
opacity: 0.6,
},
actionButtonText: {
color: '#fff',
fontSize: 16,
fontWeight: '700',
},
link: {
alignSelf: 'center',
marginTop: 8,
},
linkText: {
color: '#0b57d0',
fontSize: 16,
},
})

View File

@@ -0,0 +1,19 @@
import { supabase } from '@/libs/supabase'
import { router } from 'expo-router'
import React from 'react'
import { Button } from 'react-native'
async function onSignOutButtonPress() {
const { error } = await supabase.auth.signOut()
if (error) {
console.error('Error signing out:', error)
return
}
router.replace('/login')
}
export default function SignOutButton() {
return <Button title="Sign out" onPress={onSignOutButtonPress} />
}

View File

@@ -0,0 +1,17 @@
import { createContext, useContext } from 'react'
export type AuthData = {
claims?: Record<string, any> | null
profile?: any | null
isLoading: boolean
isLoggedIn: boolean
}
export const AuthContext = createContext<AuthData>({
claims: undefined,
profile: undefined,
isLoading: true,
isLoggedIn: false,
})
export const useAuthContext = () => useContext(AuthContext)

View File

@@ -1,8 +1,3 @@
/**
* Learn more about light and dark modes:
* https://docs.expo.dev/guides/color-schemes/
*/
import { Colors } from '@/constants/theme'; import { Colors } from '@/constants/theme';
import { useColorScheme } from '@/hooks/use-color-scheme'; import { useColorScheme } from '@/hooks/use-color-scheme';

View File

@@ -0,0 +1,49 @@
import { createClient } from '@supabase/supabase-js';
import { deleteItemAsync, getItemAsync, setItemAsync } from 'expo-secure-store';
import 'react-native-url-polyfill/auto';
import Constants from "expo-constants"
import { Platform } from 'react-native';
const ExpoSecureStoreAdapter = {
getItem: (key: string) => {
console.debug("getItem", { key, getItemAsync })
return getItemAsync(key)
},
setItem: (key: string, value: string) => {
if (value.length > 2048) {
console.warn('Value being stored in SecureStore is larger than 2048 bytes and it may not be stored successfully. In a future SDK version, this call may throw an error.')
}
return setItemAsync(key, value)
},
removeItem: (key: string) => {
return deleteItemAsync(key)
},
};
const extra = (Constants.expoConfig?.extra ?? Constants.manifest?.extra) as {
supabaseUrl?: string;
supabaseKey?: string;
};
const supabaseUrl = extra?.supabaseUrl
const supabaseAnonKey = extra?.supabaseKey
if(!supabaseUrl || !supabaseAnonKey){
throw new Error("Cannot read env variables")
}
const storage = (
Platform.OS === "web"
? window.localStorage
: ExpoSecureStoreAdapter
)
export const supabase = createClient(supabaseUrl, supabaseAnonKey, {
auth: {
storage: storage as any,
autoRefreshToken: true,
persistSession: true,
detectSessionInUrl: false,
},
}
)

View File

@@ -9,9 +9,12 @@
"version": "1.0.0", "version": "1.0.0",
"dependencies": { "dependencies": {
"@expo/vector-icons": "^15.0.3", "@expo/vector-icons": "^15.0.3",
"@react-native-async-storage/async-storage": "2.2.0",
"@react-navigation/bottom-tabs": "^7.4.0", "@react-navigation/bottom-tabs": "^7.4.0",
"@react-navigation/elements": "^2.6.3", "@react-navigation/elements": "^2.6.3",
"@react-navigation/native": "^7.1.28", "@react-navigation/native": "^7.1.28",
"@supabase/supabase-js": "^2.98.0",
"async-storage": "^0.1.0",
"expo": "~54.0.33", "expo": "~54.0.33",
"expo-constants": "~18.0.13", "expo-constants": "~18.0.13",
"expo-font": "~14.0.11", "expo-font": "~14.0.11",
@@ -19,7 +22,9 @@
"expo-image": "~3.0.11", "expo-image": "~3.0.11",
"expo-linking": "~8.0.11", "expo-linking": "~8.0.11",
"expo-router": "~6.0.23", "expo-router": "~6.0.23",
"expo-secure-store": "~15.0.8",
"expo-splash-screen": "~31.0.13", "expo-splash-screen": "~31.0.13",
"expo-sqlite": "~16.0.10",
"expo-status-bar": "~3.0.9", "expo-status-bar": "~3.0.9",
"expo-symbols": "~1.0.8", "expo-symbols": "~1.0.8",
"expo-system-ui": "~6.0.9", "expo-system-ui": "~6.0.9",
@@ -31,8 +36,11 @@
"react-native-reanimated": "~4.1.1", "react-native-reanimated": "~4.1.1",
"react-native-safe-area-context": "~5.6.0", "react-native-safe-area-context": "~5.6.0",
"react-native-screens": "~4.16.0", "react-native-screens": "~4.16.0",
"react-native-url-polyfill": "^3.0.0",
"react-native-web": "~0.21.0", "react-native-web": "~0.21.0",
"react-native-worklets": "0.5.1" "react-native-worklets": "0.5.1",
"supabase": "^2.76.16",
"supabase-js": "^0.0.1-security"
}, },
"devDependencies": { "devDependencies": {
"@types/react": "~19.1.0", "@types/react": "~19.1.0",
@@ -2963,6 +2971,18 @@
} }
} }
}, },
"node_modules/@react-native-async-storage/async-storage": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/@react-native-async-storage/async-storage/-/async-storage-2.2.0.tgz",
"integrity": "sha512-gvRvjR5JAaUZF8tv2Kcq/Gbt3JHwbKFYfmb445rhOj6NUMx3qPLixmDx5pZAyb9at1bYvJ4/eTUipU5aki45xw==",
"license": "MIT",
"dependencies": {
"merge-options": "^3.0.4"
},
"peerDependencies": {
"react-native": "^0.0.0-0 || >=0.65 <1.0"
}
},
"node_modules/@react-native/assets-registry": { "node_modules/@react-native/assets-registry": {
"version": "0.81.5", "version": "0.81.5",
"resolved": "https://registry.npmjs.org/@react-native/assets-registry/-/assets-registry-0.81.5.tgz", "resolved": "https://registry.npmjs.org/@react-native/assets-registry/-/assets-registry-0.81.5.tgz",
@@ -3328,6 +3348,107 @@
"@sinonjs/commons": "^3.0.0" "@sinonjs/commons": "^3.0.0"
} }
}, },
"node_modules/@supabase/auth-js": {
"version": "2.98.0",
"resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.98.0.tgz",
"integrity": "sha512-GBH361T0peHU91AQNzOlIrjUZw9TZbB9YDRiyFgk/3Kvr3/Z1NWUZ2athWTfHhwNNi8IrW00foyFxQD9IO/Trg==",
"license": "MIT",
"dependencies": {
"tslib": "2.8.1"
},
"engines": {
"node": ">=20.0.0"
}
},
"node_modules/@supabase/functions-js": {
"version": "2.98.0",
"resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.98.0.tgz",
"integrity": "sha512-N/xEyiNU5Org+d+PNCpv+TWniAXRzxIURxDYsS/m2I/sfAB/HcM9aM2Dmf5edj5oWb9GxID1OBaZ8NMmPXL+Lg==",
"license": "MIT",
"dependencies": {
"tslib": "2.8.1"
},
"engines": {
"node": ">=20.0.0"
}
},
"node_modules/@supabase/postgrest-js": {
"version": "2.98.0",
"resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-2.98.0.tgz",
"integrity": "sha512-v6e9WeZuJijzUut8HyXu6gMqWFepIbaeaMIm1uKzei4yLg9bC9OtEW9O14LE/9ezqNbSAnSLO5GtOLFdm7Bpkg==",
"license": "MIT",
"dependencies": {
"tslib": "2.8.1"
},
"engines": {
"node": ">=20.0.0"
}
},
"node_modules/@supabase/realtime-js": {
"version": "2.98.0",
"resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.98.0.tgz",
"integrity": "sha512-rOWt28uGyFipWOSd+n0WVMr9kUXiWaa7J4hvyLCIHjRFqWm1z9CaaKAoYyfYMC1Exn3WT8WePCgiVhlAtWC2yw==",
"license": "MIT",
"dependencies": {
"@types/phoenix": "^1.6.6",
"@types/ws": "^8.18.1",
"tslib": "2.8.1",
"ws": "^8.18.2"
},
"engines": {
"node": ">=20.0.0"
}
},
"node_modules/@supabase/realtime-js/node_modules/ws": {
"version": "8.19.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz",
"integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==",
"license": "MIT",
"engines": {
"node": ">=10.0.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": ">=5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
},
"node_modules/@supabase/storage-js": {
"version": "2.98.0",
"resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.98.0.tgz",
"integrity": "sha512-tzr2mG+v7ILSAZSfZMSL9OPyIH4z1ikgQ8EcQTKfMRz4EwmlFt3UnJaGzSOxyvF5b+fc9So7qdSUWTqGgeLokQ==",
"license": "MIT",
"dependencies": {
"iceberg-js": "^0.8.1",
"tslib": "2.8.1"
},
"engines": {
"node": ">=20.0.0"
}
},
"node_modules/@supabase/supabase-js": {
"version": "2.98.0",
"resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.98.0.tgz",
"integrity": "sha512-Ohc97CtInLwZyiSASz7tT9/Abm/vqnIbO9REp+PivVUII8UZsuI3bngRQnYgJdFoOIwvaEII1fX1qy8x0CyNiw==",
"license": "MIT",
"dependencies": {
"@supabase/auth-js": "2.98.0",
"@supabase/functions-js": "2.98.0",
"@supabase/postgrest-js": "2.98.0",
"@supabase/realtime-js": "2.98.0",
"@supabase/storage-js": "2.98.0"
},
"engines": {
"node": ">=20.0.0"
}
},
"node_modules/@tybys/wasm-util": { "node_modules/@tybys/wasm-util": {
"version": "0.10.1", "version": "0.10.1",
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz",
@@ -3449,6 +3570,12 @@
"undici-types": "~7.18.0" "undici-types": "~7.18.0"
} }
}, },
"node_modules/@types/phoenix": {
"version": "1.6.7",
"resolved": "https://registry.npmjs.org/@types/phoenix/-/phoenix-1.6.7.tgz",
"integrity": "sha512-oN9ive//QSBkf19rfDv45M7eZPi0eEXylht2OLEXicu5b4KoQ1OzXIw+xDSGWxSxe1JmepRR/ZH283vsu518/Q==",
"license": "MIT"
},
"node_modules/@types/react": { "node_modules/@types/react": {
"version": "19.1.17", "version": "19.1.17",
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.17.tgz", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.17.tgz",
@@ -3465,6 +3592,15 @@
"integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/ws": {
"version": "8.18.1",
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz",
"integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==",
"license": "MIT",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/yargs": { "node_modules/@types/yargs": {
"version": "17.0.35", "version": "17.0.35",
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz",
@@ -4437,6 +4573,16 @@
"integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==", "integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/async-storage": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/async-storage/-/async-storage-0.1.0.tgz",
"integrity": "sha512-29i3KfI7o9YNmqtR3ecY1KqOuM5/Goos3SHzIwvXNYgQMS4ggB+YQ38DC1FVY+Cc4SxiMLy9J4IRZHZ5q5gDpw==",
"license": "MPL",
"engines": {
"fennec": ">=27.0 <=30.0",
"firefox": ">=27.0"
}
},
"node_modules/available-typed-arrays": { "node_modules/available-typed-arrays": {
"version": "1.0.7", "version": "1.0.7",
"resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz",
@@ -4453,6 +4599,12 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/await-lock": {
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/await-lock/-/await-lock-2.2.2.tgz",
"integrity": "sha512-aDczADvlvTGajTDjcjpJMqRkOF6Qdz3YbPZm/PyW6tKPkx2hlYBzxMhEywM/tU72HrVZjgl5VCdRuMlA7pZ8Gw==",
"license": "MIT"
},
"node_modules/babel-jest": { "node_modules/babel-jest": {
"version": "29.7.0", "version": "29.7.0",
"resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz",
@@ -4738,6 +4890,55 @@
"node": ">=0.6" "node": ">=0.6"
} }
}, },
"node_modules/bin-links": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/bin-links/-/bin-links-6.0.0.tgz",
"integrity": "sha512-X4CiKlcV2GjnCMwnKAfbVWpHa++65th9TuzAEYtZoATiOE2DQKhSp4CJlyLoTqdhBKlXjpXjCTYPNNFS33Fi6w==",
"license": "ISC",
"dependencies": {
"cmd-shim": "^8.0.0",
"npm-normalize-package-bin": "^5.0.0",
"proc-log": "^6.0.0",
"read-cmd-shim": "^6.0.0",
"write-file-atomic": "^7.0.0"
},
"engines": {
"node": "^20.17.0 || >=22.9.0"
}
},
"node_modules/bin-links/node_modules/proc-log": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/proc-log/-/proc-log-6.1.0.tgz",
"integrity": "sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ==",
"license": "ISC",
"engines": {
"node": "^20.17.0 || >=22.9.0"
}
},
"node_modules/bin-links/node_modules/signal-exit": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
"integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
"license": "ISC",
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/bin-links/node_modules/write-file-atomic": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-7.0.1.tgz",
"integrity": "sha512-OTIk8iR8/aCRWBqvxrzxR0hgxWpnYBblY1S5hDWBQfk/VFmJwzmJgQFN3WsoUKHISv2eAwe+PpbUzyL1CKTLXg==",
"license": "ISC",
"dependencies": {
"signal-exit": "^4.0.1"
},
"engines": {
"node": "^20.17.0 || >=22.9.0"
}
},
"node_modules/bplist-creator": { "node_modules/bplist-creator": {
"version": "0.1.0", "version": "0.1.0",
"resolved": "https://registry.npmjs.org/bplist-creator/-/bplist-creator-0.1.0.tgz", "resolved": "https://registry.npmjs.org/bplist-creator/-/bplist-creator-0.1.0.tgz",
@@ -5070,6 +5271,15 @@
"node": ">=0.8" "node": ">=0.8"
} }
}, },
"node_modules/cmd-shim": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/cmd-shim/-/cmd-shim-8.0.0.tgz",
"integrity": "sha512-Jk/BK6NCapZ58BKUxlSI+ouKRbjH1NLZCgJkYoab+vEHUY3f6OzpNBN9u7HFSv9J6TRDGs4PLOHezoKGaFRSCA==",
"license": "ISC",
"engines": {
"node": "^20.17.0 || >=22.9.0"
}
},
"node_modules/color": { "node_modules/color": {
"version": "4.2.3", "version": "4.2.3",
"resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz",
@@ -5277,6 +5487,15 @@
"devOptional": true, "devOptional": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/data-uri-to-buffer": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz",
"integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==",
"license": "MIT",
"engines": {
"node": ">= 12"
}
},
"node_modules/data-view-buffer": { "node_modules/data-view-buffer": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz",
@@ -6655,6 +6874,15 @@
"node": ">=10" "node": ">=10"
} }
}, },
"node_modules/expo-secure-store": {
"version": "15.0.8",
"resolved": "https://registry.npmjs.org/expo-secure-store/-/expo-secure-store-15.0.8.tgz",
"integrity": "sha512-lHnzvRajBu4u+P99+0GEMijQMFCOYpWRO4dWsXSuMt77+THPIGjzNvVKrGSl6mMrLsfVaKL8BpwYZLGlgA+zAw==",
"license": "MIT",
"peerDependencies": {
"expo": "*"
}
},
"node_modules/expo-server": { "node_modules/expo-server": {
"version": "1.0.5", "version": "1.0.5",
"resolved": "https://registry.npmjs.org/expo-server/-/expo-server-1.0.5.tgz", "resolved": "https://registry.npmjs.org/expo-server/-/expo-server-1.0.5.tgz",
@@ -6676,6 +6904,20 @@
"expo": "*" "expo": "*"
} }
}, },
"node_modules/expo-sqlite": {
"version": "16.0.10",
"resolved": "https://registry.npmjs.org/expo-sqlite/-/expo-sqlite-16.0.10.tgz",
"integrity": "sha512-tUOKxE9TpfneRG3eOfbNfhN9236SJ7IiUnP8gCqU7umd9DtgDGB/5PhYVVfl+U7KskgolgNoB9v9OZ9iwXN8Eg==",
"license": "MIT",
"dependencies": {
"await-lock": "^2.2.2"
},
"peerDependencies": {
"expo": "*",
"react": "*",
"react-native": "*"
}
},
"node_modules/expo-status-bar": { "node_modules/expo-status-bar": {
"version": "3.0.9", "version": "3.0.9",
"resolved": "https://registry.npmjs.org/expo-status-bar/-/expo-status-bar-3.0.9.tgz", "resolved": "https://registry.npmjs.org/expo-status-bar/-/expo-status-bar-3.0.9.tgz",
@@ -6796,6 +7038,29 @@
"asap": "~2.0.3" "asap": "~2.0.3"
} }
}, },
"node_modules/fetch-blob": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz",
"integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/jimmywarting"
},
{
"type": "paypal",
"url": "https://paypal.me/jimmywarting"
}
],
"license": "MIT",
"dependencies": {
"node-domexception": "^1.0.0",
"web-streams-polyfill": "^3.0.3"
},
"engines": {
"node": "^12.20 || >= 14.13"
}
},
"node_modules/file-entry-cache": { "node_modules/file-entry-cache": {
"version": "8.0.0", "version": "8.0.0",
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
@@ -6929,6 +7194,18 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/formdata-polyfill": {
"version": "4.0.10",
"resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz",
"integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==",
"license": "MIT",
"dependencies": {
"fetch-blob": "^3.1.2"
},
"engines": {
"node": ">=12.20.0"
}
},
"node_modules/freeport-async": { "node_modules/freeport-async": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/freeport-async/-/freeport-async-2.0.0.tgz", "resolved": "https://registry.npmjs.org/freeport-async/-/freeport-async-2.0.0.tgz",
@@ -7447,6 +7724,15 @@
"integrity": "sha512-WDC/ui2VVRrz3jOVi+XtjqkDjiVjTtFaAGiW37k6b+ohyQ5wYDOGkvCZa8+H0nx3gyvv0+BST9xuOgIyGQ00gw==", "integrity": "sha512-WDC/ui2VVRrz3jOVi+XtjqkDjiVjTtFaAGiW37k6b+ohyQ5wYDOGkvCZa8+H0nx3gyvv0+BST9xuOgIyGQ00gw==",
"license": "BSD-3-Clause" "license": "BSD-3-Clause"
}, },
"node_modules/iceberg-js": {
"version": "0.8.1",
"resolved": "https://registry.npmjs.org/iceberg-js/-/iceberg-js-0.8.1.tgz",
"integrity": "sha512-1dhVQZXhcHje7798IVM+xoo/1ZdVfzOMIc8/rgVSijRK38EDqOJoGula9N/8ZI5RD8QTxNQtK/Gozpr+qUqRRA==",
"license": "MIT",
"engines": {
"node": ">=20.0.0"
}
},
"node_modules/ieee754": { "node_modules/ieee754": {
"version": "1.2.1", "version": "1.2.1",
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
@@ -7881,6 +8167,15 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/is-plain-obj": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz",
"integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==",
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/is-regex": { "node_modules/is-regex": {
"version": "1.2.1", "version": "1.2.1",
"resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz",
@@ -8859,6 +9154,18 @@
"integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==", "integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/merge-options": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/merge-options/-/merge-options-3.0.4.tgz",
"integrity": "sha512-2Sug1+knBjkaMsMgf1ctR1Ujx+Ayku4EdJN4Z+C2+JzoeF7A3OZ9KM2GY0CpQS51NR61LTurMJrRKPhSs3ZRTQ==",
"license": "MIT",
"dependencies": {
"is-plain-obj": "^2.1.0"
},
"engines": {
"node": ">=10"
}
},
"node_modules/merge-stream": { "node_modules/merge-stream": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
@@ -9346,6 +9653,26 @@
"integrity": "sha512-SrQrok4CATudVzBS7coSz26QRSmlK9TzzoFbeKfcPBUFPjcQM9Rqvr/DlJkOrwI/0KcgvMub1n1g5Jt9EgRn4A==", "integrity": "sha512-SrQrok4CATudVzBS7coSz26QRSmlK9TzzoFbeKfcPBUFPjcQM9Rqvr/DlJkOrwI/0KcgvMub1n1g5Jt9EgRn4A==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/node-domexception": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz",
"integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==",
"deprecated": "Use your platform's native DOMException instead",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/jimmywarting"
},
{
"type": "github",
"url": "https://paypal.me/jimmywarting"
}
],
"license": "MIT",
"engines": {
"node": ">=10.5.0"
}
},
"node_modules/node-exports-info": { "node_modules/node-exports-info": {
"version": "1.6.0", "version": "1.6.0",
"resolved": "https://registry.npmjs.org/node-exports-info/-/node-exports-info-1.6.0.tgz", "resolved": "https://registry.npmjs.org/node-exports-info/-/node-exports-info-1.6.0.tgz",
@@ -9415,6 +9742,15 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/npm-normalize-package-bin": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-5.0.0.tgz",
"integrity": "sha512-CJi3OS4JLsNMmr2u07OJlhcrPxCeOeP/4xq67aWNai6TNWWbTrlNDgl8NcFKVlcBKp18GPj+EzbNIgrBfZhsag==",
"license": "ISC",
"engines": {
"node": "^20.17.0 || >=22.9.0"
}
},
"node_modules/npm-package-arg": { "node_modules/npm-package-arg": {
"version": "11.0.3", "version": "11.0.3",
"resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-11.0.3.tgz", "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-11.0.3.tgz",
@@ -10391,6 +10727,18 @@
"react-native": "*" "react-native": "*"
} }
}, },
"node_modules/react-native-url-polyfill": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/react-native-url-polyfill/-/react-native-url-polyfill-3.0.0.tgz",
"integrity": "sha512-aA5CiuUCUb/lbrliVCJ6lZ17/RpNJzvTO/C7gC/YmDQhTUoRD5q5HlJfwLWcxz4VgAhHwXKzhxH+wUN24tAdqg==",
"license": "MIT",
"dependencies": {
"whatwg-url-without-unicode": "8.0.0-3"
},
"peerDependencies": {
"react-native": "*"
}
},
"node_modules/react-native-web": { "node_modules/react-native-web": {
"version": "0.21.2", "version": "0.21.2",
"resolved": "https://registry.npmjs.org/react-native-web/-/react-native-web-0.21.2.tgz", "resolved": "https://registry.npmjs.org/react-native-web/-/react-native-web-0.21.2.tgz",
@@ -10611,6 +10959,15 @@
} }
} }
}, },
"node_modules/read-cmd-shim": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/read-cmd-shim/-/read-cmd-shim-6.0.0.tgz",
"integrity": "sha512-1zM5HuOfagXCBWMN83fuFI/x+T/UhZ7k+KIzhrHXcQoeX5+7gmaDYjELQHmmzIodumBHeByBJT4QYS7ufAgs7A==",
"license": "ISC",
"engines": {
"node": "^20.17.0 || >=22.9.0"
}
},
"node_modules/reflect.getprototypeof": { "node_modules/reflect.getprototypeof": {
"version": "1.0.10", "version": "1.0.10",
"resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz",
@@ -11641,6 +11998,48 @@
"node": ">= 6" "node": ">= 6"
} }
}, },
"node_modules/supabase": {
"version": "2.76.16",
"resolved": "https://registry.npmjs.org/supabase/-/supabase-2.76.16.tgz",
"integrity": "sha512-hZ1Kg88+pOlSAzvpas5RYLW6Op6a1OTUhOm8weP8vJf0emE/GO8rrou57irL3hopOwwSnye19VnxplW5+3g1zA==",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
"bin-links": "^6.0.0",
"https-proxy-agent": "^7.0.2",
"node-fetch": "^3.3.2",
"tar": "7.5.9"
},
"bin": {
"supabase": "bin/supabase"
},
"engines": {
"npm": ">=8"
}
},
"node_modules/supabase-js": {
"version": "0.0.1-security",
"resolved": "https://registry.npmjs.org/supabase-js/-/supabase-js-0.0.1-security.tgz",
"integrity": "sha512-PbsAWLAaduzyPDZymFxbVH/qCOe0rL0PYm3rK0S2GP6hqJICUTkDAisgAYEJVDCWaiGk13onNBOVAHbGQJjffg=="
},
"node_modules/supabase/node_modules/node-fetch": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz",
"integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==",
"license": "MIT",
"dependencies": {
"data-uri-to-buffer": "^4.0.0",
"fetch-blob": "^3.1.4",
"formdata-polyfill": "^4.0.10"
},
"engines": {
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/node-fetch"
}
},
"node_modules/supports-color": { "node_modules/supports-color": {
"version": "7.2.0", "version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
@@ -12579,6 +12978,15 @@
"defaults": "^1.0.3" "defaults": "^1.0.3"
} }
}, },
"node_modules/web-streams-polyfill": {
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz",
"integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==",
"license": "MIT",
"engines": {
"node": ">= 8"
}
},
"node_modules/webidl-conversions": { "node_modules/webidl-conversions": {
"version": "3.0.1", "version": "3.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",

View File

@@ -12,9 +12,12 @@
}, },
"dependencies": { "dependencies": {
"@expo/vector-icons": "^15.0.3", "@expo/vector-icons": "^15.0.3",
"@react-native-async-storage/async-storage": "2.2.0",
"@react-navigation/bottom-tabs": "^7.4.0", "@react-navigation/bottom-tabs": "^7.4.0",
"@react-navigation/elements": "^2.6.3", "@react-navigation/elements": "^2.6.3",
"@react-navigation/native": "^7.1.28", "@react-navigation/native": "^7.1.28",
"@supabase/supabase-js": "^2.98.0",
"async-storage": "^0.1.0",
"expo": "~54.0.33", "expo": "~54.0.33",
"expo-constants": "~18.0.13", "expo-constants": "~18.0.13",
"expo-font": "~14.0.11", "expo-font": "~14.0.11",
@@ -22,7 +25,9 @@
"expo-image": "~3.0.11", "expo-image": "~3.0.11",
"expo-linking": "~8.0.11", "expo-linking": "~8.0.11",
"expo-router": "~6.0.23", "expo-router": "~6.0.23",
"expo-secure-store": "~15.0.8",
"expo-splash-screen": "~31.0.13", "expo-splash-screen": "~31.0.13",
"expo-sqlite": "~16.0.10",
"expo-status-bar": "~3.0.9", "expo-status-bar": "~3.0.9",
"expo-symbols": "~1.0.8", "expo-symbols": "~1.0.8",
"expo-system-ui": "~6.0.9", "expo-system-ui": "~6.0.9",
@@ -34,8 +39,11 @@
"react-native-reanimated": "~4.1.1", "react-native-reanimated": "~4.1.1",
"react-native-safe-area-context": "~5.6.0", "react-native-safe-area-context": "~5.6.0",
"react-native-screens": "~4.16.0", "react-native-screens": "~4.16.0",
"react-native-url-polyfill": "^3.0.0",
"react-native-web": "~0.21.0", "react-native-web": "~0.21.0",
"react-native-worklets": "0.5.1" "react-native-worklets": "0.5.1",
"supabase": "^2.76.16",
"supabase-js": "^0.0.1-security"
}, },
"devDependencies": { "devDependencies": {
"@types/react": "~19.1.0", "@types/react": "~19.1.0",

View File

@@ -0,0 +1,112 @@
import { AuthContext } from '@/hooks/use-auth-context'
import { supabase } from '@/libs/supabase'
import { PropsWithChildren, useEffect, useState } from 'react'
export default function AuthProvider({ children }: PropsWithChildren) {
const [claims, setClaims] = useState<Record<string, any> | undefined | null>()
const [profile, setProfile] = useState<any>()
const [isLoading, setIsLoading] = useState<boolean>(true)
useEffect(() => {
const syncAuthState = async () => {
setIsLoading(true)
const {
data: { user },
error,
} = await supabase.auth.getUser()
if (error) {
console.error('Error fetching user:', error)
}
setClaims(
user
? {
sub: user.id,
email: user.email,
}
: null
)
setIsLoading(false)
}
void syncAuthState()
const {
data: { subscription },
} = supabase.auth.onAuthStateChange(async (_event, session) => {
console.log('Auth state changed:', { event: _event })
setClaims(
session?.user
? {
sub: session.user.id,
email: session.user.email,
}
: null
)
})
return () => {
subscription.unsubscribe()
}
}, [])
useEffect(() => {
const fetchProfile = async () => {
setIsLoading(true)
if (claims) {
const fallbackProfile = {
id: claims.sub,
email: claims.email ?? null,
username: null,
full_name: null,
}
const { error: upsertError } = await supabase.from('profiles').upsert({
id: claims.sub,
email: claims.email ?? null,
})
if (upsertError) {
console.error('Error creating profile:', upsertError)
}
const { data, error } = await supabase
.from('profiles')
.select('*')
.eq('id', claims.sub)
.maybeSingle()
if (error) {
console.error('Error fetching profile:', error)
}
setProfile(
data ?? fallbackProfile
)
} else {
setProfile(null)
}
setIsLoading(false)
}
void fetchProfile()
}, [claims])
return (
<AuthContext.Provider
value={{
claims,
isLoading,
profile,
isLoggedIn: claims != null,
}}
>
{children}
</AuthContext.Provider>
)
}

View File

@@ -1,47 +1,291 @@
import React, { createContext, useContext, useMemo, useState } from "react"; import React, { createContext, useContext, useEffect, useState } from "react"
import { useAuthContext } from "@/hooks/use-auth-context"
import { supabase } from "@/libs/supabase"
export type Note = type NoteRow = {
{ id: number
id: string; created_by: string
title: string; title: string
content: string; content: string
}; created_at: string
updated_at?: string | null
type NotesContextValue =
{
notes: Note[];
addNote: (title: string, content: string) => void;
};
const NotesContext = createContext<NotesContextValue | undefined>(undefined);
export function NotesProvider({ children }: { children: React.ReactNode })
{
const [notes, setNotes] = useState<Note[]>
([
{ id: "1", title: "Second Note", content: "Follow up note - also hard coded" },
{ id: "2", title: "First Note", content: "This is the first note currently hard coded" }
]);
const addNote = (title: string, content: string) =>
{
const newNote: Note =
{
id: Date.now().toString(),
title: title.trim() || "(Untitled)",
content: content.trim()
};
setNotes((prev) => [newNote, ...prev])
};
const value = useMemo(() => ({ notes, addNote }), [notes]);
return <NotesContext.Provider value={value}>{children}</NotesContext.Provider>;
} }
export function useNotes() type ProfileRow = {
{ id: string
const ctx = useContext(NotesContext); full_name?: string | null
if(!ctx) throw new Error("useNotes must be used inside NotesProvider"); username?: string | null
return ctx; email?: string | null
}
export type Note = {
id: string
createdBy: string
createdAt: string
lastChangedAt: string
title: string
content: string
creatorLabel: string
}
type NotesContextValue = {
notes: Note[]
isLoading: boolean
errorMessage: string | null
refreshNotes: () => Promise<void>
addNote: (title: string, content: string) => Promise<boolean>
updateNote: (noteId: string, title: string, content: string) => Promise<boolean>
deleteNote: (noteId: string) => Promise<boolean>
}
const NotesContext = createContext<NotesContextValue | undefined>(undefined)
export function NotesProvider({ children }: { children: React.ReactNode }) {
const { claims, isLoggedIn, profile } = useAuthContext()
const [notes, setNotes] = useState<Note[]>([])
const [isLoading, setIsLoading] = useState(false)
const [errorMessage, setErrorMessage] = useState<string | null>(null)
const userId = claims?.sub as string | undefined
const creatorLabel =
profile?.full_name ||
profile?.username ||
claims?.email ||
userId ||
"Unknown user"
const buildCreatorLabels = async (rows: NoteRow[]) => {
const creatorIds = Array.from(new Set(rows.map((row) => row.created_by)))
if (creatorIds.length === 0) {
return {} as Record<string, string>
}
const { data, error } = await supabase
.from("profiles")
.select("id, full_name, username, email")
.in("id", creatorIds)
if (error || !data) {
return {} as Record<string, string>
}
return (data as ProfileRow[]).reduce<Record<string, string>>((acc, row) => {
acc[row.id] =
row.full_name ||
row.username ||
row.email ||
"Unknown user"
return acc
}, {})
}
const mapNote = (row: NoteRow, labels: Record<string, string>): Note => ({
id: String(row.id),
createdBy: row.created_by,
createdAt: row.created_at,
lastChangedAt: row.updated_at || row.created_at,
title: row.title,
content: row.content,
creatorLabel:
labels[row.created_by] ||
(row.created_by === userId ? creatorLabel : "Unknown user"),
})
const loadNotes = async () => {
if (!isLoggedIn) {
setNotes([])
setErrorMessage(null)
setIsLoading(false)
return
}
setIsLoading(true)
setErrorMessage(null)
const { data, error } = await supabase
.from("Notes")
.select("id, created_by, title, content, created_at, updated_at")
.order("updated_at", { ascending: false, nullsFirst: false })
.order("created_at", { ascending: false })
if (error) {
setErrorMessage(error.message)
setNotes([])
setIsLoading(false)
return
}
const rows = (data ?? []) as NoteRow[]
// Resolve public creator labels separately so note reads do not depend on a view schema.
const labels = await buildCreatorLabels(rows)
setNotes(rows.map((row) => mapNote(row, labels)))
setIsLoading(false)
}
const refreshNotes = async () => {
await loadNotes()
}
useEffect(() => {
if (!isLoggedIn || !userId) {
setNotes([])
setErrorMessage(null)
setIsLoading(false)
return
}
void loadNotes()
}, [creatorLabel, isLoggedIn, userId])
useEffect(() => {
if (!isLoggedIn || !userId) {
return
}
// Poll for remote changes so other users' edits appear without a manual refresh.
const intervalId = setInterval(() => {
void loadNotes()
}, 60000)
return () => {
clearInterval(intervalId)
}
}, [creatorLabel, isLoggedIn, userId])
const addNote = async (title: string, content: string) => {
const trimmedTitle = title.trim()
const trimmedContent = content.trim()
if (!trimmedTitle || !trimmedContent) {
setErrorMessage("Title and content are required.")
return false
}
if (!userId) {
setErrorMessage("You must be logged in to save notes.")
return false
}
setErrorMessage(null)
const { error } = await supabase
.from("Notes")
.insert({
title: trimmedTitle,
content: trimmedContent,
})
if (error) {
setErrorMessage(error.message)
return false
}
await refreshNotes()
return true
}
const updateNote = async (noteId: string, title: string, content: string) => {
const trimmedTitle = title.trim()
const trimmedContent = content.trim()
if (!trimmedTitle || !trimmedContent) {
setErrorMessage("Title and content are required.")
return false
}
if (!userId) {
setErrorMessage("You must be logged in to update notes.")
return false
}
setErrorMessage(null)
const { data, error } = await supabase
.from("Notes")
.update({
title: trimmedTitle,
content: trimmedContent,
updated_at: new Date().toISOString(),
})
.eq("id", Number(noteId))
.eq("created_by", userId)
.select("id, title, content, updated_at")
.maybeSingle()
if (error) {
setErrorMessage(error.message)
return false
}
if (!data) {
setErrorMessage("Update failed. You can only edit notes that you created.")
return false
}
// Update the edited note locally so the new timestamp is visible immediately.
setNotes((prev) =>
prev.map((note) =>
note.id === noteId
? {
...note,
title: data.title ?? trimmedTitle,
content: data.content ?? trimmedContent,
lastChangedAt: data.updated_at ?? new Date().toISOString(),
}
: note
)
)
return true
}
const deleteNote = async (noteId: string) => {
if (!userId) {
setErrorMessage("You must be logged in to delete notes.")
return false
}
setErrorMessage(null)
const { error } = await supabase
.from("Notes")
.delete()
.eq("id", Number(noteId))
.eq("created_by", userId)
if (error) {
setErrorMessage(error.message)
return false
}
setNotes((prev) => prev.filter((note) => note.id !== noteId))
return true
}
return (
<NotesContext.Provider
value={{
notes,
isLoading,
errorMessage,
refreshNotes,
addNote,
updateNote,
deleteNote,
}}
>
{children}
</NotesContext.Provider>
)
}
export function useNotes() {
const ctx = useContext(NotesContext)
if (!ctx) {
throw new Error("useNotes must be used inside NotesProvider")
}
return ctx
} }

56
FastNotes/tempStorage.tsx Normal file
View File

@@ -0,0 +1,56 @@
import { Image } from 'expo-image'
import { StyleSheet } from 'react-native'
import { HelloWave } from '@/components/hello-wave'
import ParallaxScrollView from '@/components/parallax-scroll-view'
import { ThemedText } from '@/components/themed-text'
import { ThemedView } from '@/components/themed-view'
import SignOutButton from '@/components/social-auth-buttons/sign-out-button'
import { useAuthContext } from '@/hooks/use-auth-context'
export default function HomeScreen() {
const { profile } = useAuthContext()
return (
<ParallaxScrollView
headerBackgroundColor={{ light: '#A1CEDC', dark: '#1D3D47' }}
headerImage={
<Image
source={require('@/assets/images/partial-react-logo.png')}
style={styles.reactLogo}
/>
}
>
<ThemedView style={styles.titleContainer}>
<ThemedText type="title">Welcome!</ThemedText>
<HelloWave />
</ThemedView>
<ThemedView style={styles.stepContainer}>
<ThemedText type="subtitle">Username</ThemedText>
<ThemedText>{profile?.username}</ThemedText>
<ThemedText type="subtitle">Full name</ThemedText>
<ThemedText>{profile?.full_name}</ThemedText>
</ThemedView>
<SignOutButton />
</ParallaxScrollView>
)
}
const styles = StyleSheet.create({
titleContainer: {
flexDirection: 'row',
alignItems: 'center',
gap: 8,
},
stepContainer: {
gap: 8,
marginBottom: 8,
},
reactLogo: {
height: 178,
width: 290,
bottom: 0,
left: 0,
position: 'absolute',
},
})

3
FastNotesKotlin/.idea/.gitignore generated vendored Normal file
View File

@@ -0,0 +1,3 @@
# Default ignored files
/shelf/
/workspace.xml

1
FastNotesKotlin/.idea/.name generated Normal file
View File

@@ -0,0 +1 @@
FastNotes - Kotlin

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="AndroidProjectSystem">
<option name="providerId" value="com.android.tools.idea.GradleProjectSystem" />
</component>
</project>

6
FastNotesKotlin/.idea/compiler.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CompilerConfiguration">
<bytecodeTargetLevel target="21" />
</component>
</project>

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="deploymentTargetSelector">
<selectionStates>
<SelectionState runConfigName="app">
<option name="selectionMode" value="DROPDOWN" />
</SelectionState>
</selectionStates>
</component>
</project>

13
FastNotesKotlin/.idea/deviceManager.xml generated Normal file
View File

@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DeviceTable">
<option name="columnSorters">
<list>
<ColumnSorterState>
<option name="column" value="Name" />
<option name="order" value="ASCENDING" />
</ColumnSorterState>
</list>
</option>
</component>
</project>

17
FastNotesKotlin/.idea/gradle.xml generated Normal file
View File

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GradleSettings">
<option name="linkedExternalProjectsSettings">
<GradleProjectSettings>
<option name="testRunner" value="CHOOSE_PER_TEST" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$" />
<option value="$PROJECT_DIR$/app" />
</set>
</option>
</GradleProjectSettings>
</option>
</component>
</project>

8
FastNotesKotlin/.idea/markdown.xml generated Normal file
View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="MarkdownSettings">
<option name="previewPanelProviderInfo">
<ProviderInfo name="Compose (experimental)" className="com.intellij.markdown.compose.preview.ComposePanelProvider" />
</option>
</component>
</project>

8
FastNotesKotlin/.idea/misc.xml generated Normal file
View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" project-jdk-name="jbr-21" project-jdk-type="JavaSDK" />
<component name="ProjectType">
<option name="id" value="Android" />
</component>
</project>

View File

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RunConfigurationProducerService">
<option name="ignoredProducers">
<set>
<option value="com.intellij.execution.junit.AbstractAllInDirectoryConfigurationProducer" />
<option value="com.intellij.execution.junit.AllInPackageConfigurationProducer" />
<option value="com.intellij.execution.junit.PatternConfigurationProducer" />
<option value="com.intellij.execution.junit.TestInClassConfigurationProducer" />
<option value="com.intellij.execution.junit.UniqueIdConfigurationProducer" />
<option value="com.intellij.execution.junit.testDiscovery.JUnitTestDiscoveryConfigurationProducer" />
<option value="org.jetbrains.kotlin.idea.junit.KotlinJUnitRunConfigurationProducer" />
<option value="org.jetbrains.kotlin.idea.junit.KotlinPatternConfigurationProducer" />
</set>
</option>
</component>
</project>

6
FastNotesKotlin/.idea/vcs.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$/.." vcs="Git" />
</component>
</project>