Added 'cloud storage' functionality
This commit is contained in:
6
.gitignore → FastNotes/.gitignore
vendored
6
.gitignore → FastNotes/.gitignore
vendored
@@ -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
10
FastNotes/app.config.js
Normal 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,
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -38,7 +38,9 @@
|
|||||||
"backgroundColor": "#000000"
|
"backgroundColor": "#000000"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
"expo-sqlite",
|
||||||
|
"expo-secure-store"
|
||||||
],
|
],
|
||||||
"experiments": {
|
"experiments": {
|
||||||
"typedRoutes": true,
|
"typedRoutes": true,
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|||||||
@@ -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
146
FastNotes/app/login.tsx
Normal 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,
|
||||||
|
},
|
||||||
|
})
|
||||||
@@ -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}
|
||||||
@@ -43,15 +57,25 @@ export default function NewNoteScreen()
|
|||||||
<TextInput value={content} onChangeText={setContent} placeholder="Write your note..."
|
<TextInput value={content} onChangeText={setContent} placeholder="Write your note..."
|
||||||
style={[styles.input, { height: Math.max(160, contentHeight) }]} multiline
|
style={[styles.input, { height: Math.max(160, contentHeight) }]} multiline
|
||||||
textAlignVertical="top"
|
textAlignVertical="top"
|
||||||
onContentSizeChange={(e) => {setContentHeight(e.nativeEvent.contentSize.height);
|
onContentSizeChange={(e) => {setContentHeight(e.nativeEvent.contentSize.height);
|
||||||
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
179
FastNotes/app/signup.tsx
Normal 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,
|
||||||
|
},
|
||||||
|
})
|
||||||
19
FastNotes/components/social-auth-buttons/sign-out-button.tsx
Normal file
19
FastNotes/components/social-auth-buttons/sign-out-button.tsx
Normal 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} />
|
||||||
|
}
|
||||||
17
FastNotes/hooks/use-auth-context.tsx
Normal file
17
FastNotes/hooks/use-auth-context.tsx
Normal 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)
|
||||||
@@ -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';
|
||||||
|
|
||||||
|
|||||||
49
FastNotes/libs/supabase.ts
Normal file
49
FastNotes/libs/supabase.ts
Normal 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,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
410
FastNotes/package-lock.json
generated
410
FastNotes/package-lock.json
generated
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
112
FastNotes/providers/auth-provider.tsx
Normal file
112
FastNotes/providers/auth-provider.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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
56
FastNotes/tempStorage.tsx
Normal 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
3
FastNotesKotlin/.idea/.gitignore
generated
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# Default ignored files
|
||||||
|
/shelf/
|
||||||
|
/workspace.xml
|
||||||
1
FastNotesKotlin/.idea/.name
generated
Normal file
1
FastNotesKotlin/.idea/.name
generated
Normal file
@@ -0,0 +1 @@
|
|||||||
|
FastNotes - Kotlin
|
||||||
6
FastNotesKotlin/.idea/AndroidProjectSystem.xml
generated
Normal file
6
FastNotesKotlin/.idea/AndroidProjectSystem.xml
generated
Normal 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
6
FastNotesKotlin/.idea/compiler.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="CompilerConfiguration">
|
||||||
|
<bytecodeTargetLevel target="21" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
10
FastNotesKotlin/.idea/deploymentTargetSelector.xml
generated
Normal file
10
FastNotesKotlin/.idea/deploymentTargetSelector.xml
generated
Normal 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
13
FastNotesKotlin/.idea/deviceManager.xml
generated
Normal 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
17
FastNotesKotlin/.idea/gradle.xml
generated
Normal 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
8
FastNotesKotlin/.idea/markdown.xml
generated
Normal 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
8
FastNotesKotlin/.idea/misc.xml
generated
Normal 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>
|
||||||
17
FastNotesKotlin/.idea/runConfigurations.xml
generated
Normal file
17
FastNotesKotlin/.idea/runConfigurations.xml
generated
Normal 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
6
FastNotesKotlin/.idea/vcs.xml
generated
Normal 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>
|
||||||
Reference in New Issue
Block a user