diff --git a/.gitignore b/FastNotes/.gitignore
similarity index 77%
rename from .gitignore
rename to FastNotes/.gitignore
index 6a9b42c..30f0284 100644
--- a/.gitignore
+++ b/FastNotes/.gitignore
@@ -1,5 +1,3 @@
-# Learn more https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files
-
# dependencies
node_modules/
@@ -34,12 +32,12 @@ yarn-error.*
# local env files
.env*.local
+.env.local
+.env
# typescript
*.tsbuildinfo
-app-example
-
# generated native folders
/ios
/android
diff --git a/FastNotes/app.config.js b/FastNotes/app.config.js
new file mode 100644
index 0000000..60f4433
--- /dev/null
+++ b/FastNotes/app.config.js
@@ -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,
+ },
+});
\ No newline at end of file
diff --git a/FastNotes/app.json b/FastNotes/app.json
index 875c3a4..6d727a6 100644
--- a/FastNotes/app.json
+++ b/FastNotes/app.json
@@ -38,7 +38,9 @@
"backgroundColor": "#000000"
}
}
- ]
+ ],
+ "expo-sqlite",
+ "expo-secure-store"
],
"experiments": {
"typedRoutes": true,
diff --git a/FastNotes/app/_layout.tsx b/FastNotes/app/_layout.tsx
index c9bb8e1..3289d0c 100644
--- a/FastNotes/app/_layout.tsx
+++ b/FastNotes/app/_layout.tsx
@@ -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 { StatusBar } from 'expo-status-bar';
import 'react-native-reanimated';
import { SafeAreaProvider } from 'react-native-safe-area-context';
import { NotesProvider } from "@/src/notes/NotesContext"
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 (
+
+
+
+
+
+
+
+
+
+
+
+
+ )
+}
export default function RootLayout() {
- const colorScheme = useColorScheme();
-
+ const colorScheme = useColorScheme()
/*TODO
- Sort ThemeProvider to work with dark theme on iOS
+ Fix ThemeProvider to work with dark theme
*/
return (
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
);
}
diff --git a/FastNotes/app/detail.tsx b/FastNotes/app/detail.tsx
index ee726f1..1ce0262 100644
--- a/FastNotes/app/detail.tsx
+++ b/FastNotes/app/detail.tsx
@@ -1,18 +1,158 @@
-import { StyleSheet, Text, View } from "react-native";
-import { useLocalSearchParams } from "expo-router";
+import { useEffect, useState } from "react";
+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()
{
- const { title, content } = useLocalSearchParams<
+ const { id } = useLocalSearchParams<
{
- title?: string;
- content?: string;
+ id?: 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(null);
+ const [statusMessage, setStatusMessage] = useState(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 (
+
+ Note not found
+ The note may have been deleted.
+
+ );
+ }
+
+ return(
- {title ?? "(No title)"}
- {content ?? ""}
+
+ Created by {note.creatorLabel}
+
+ Last changed {formatTimestamp(note.lastChangedAt)}
+
+
+ {!canEdit ? (
+
+ Only the creator of this note can update or delete it.
+
+ ) : null}
+ {localErrorMessage ? {localErrorMessage} : null}
+ {!localErrorMessage && errorMessage ? {errorMessage} : null}
+ {statusMessage ? {statusMessage} : null}
+ {canEdit ? (
+
+
+
+ {isSaving ? "Saving..." : "Save changes"}
+
+
+
+
+ {isDeleting ? "Deleting..." : "Delete note"}
+
+
+
+ ) : null}
);
}
@@ -22,5 +162,58 @@ const styles = StyleSheet.create(
{
container: { flex: 1, padding: 16, gap: 12 },
title: { fontSize: 22, fontWeight:"700" },
- content: { fontSize: 16 }
- });
\ No newline at end of file
+ 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",
+ },
+ });
diff --git a/FastNotes/app/index.tsx b/FastNotes/app/index.tsx
index 7475e3d..1621f28 100644
--- a/FastNotes/app/index.tsx
+++ b/FastNotes/app/index.tsx
@@ -1,49 +1,181 @@
+import { useMemo, useState } from "react";
import { FlatList, Pressable, StyleSheet, Text, View } from "react-native";
import { router } from "expo-router";
import { useSafeAreaInsets } from "react-native-safe-area-context";
+import { useAuthContext } from "@/hooks/use-auth-context";
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()
{
- const { notes } = useNotes();
+ const { claims } = useAuthContext();
+ const { errorMessage, isLoading, notes } = useNotes();
+ const [activeTab, setActiveTab] = useState("my-notes");
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 (
+
+ FastNotes
+
+
+
+
+ setActiveTab("my-notes")}
+ style={[
+ styles.tabButton,
+ activeTab === "my-notes" ? styles.tabButtonActive : null,
+ ]}
+ >
+
+ My Notes
+
+
+ setActiveTab("work-notes")}
+ style={[
+ styles.tabButton,
+ activeTab === "work-notes" ? styles.tabButtonActive : null,
+ ]}
+ >
+
+ Work Notes
+
+
+
+
+ {errorMessage ? {errorMessage} : null}
+
n.id}
contentContainerStyle={[styles.list, { paddingBottom: 120 }]}
+ ListEmptyComponent={
+
+ {isLoading ? "Loading notes..." : emptyText}
+
+ }
renderItem={({ item }) => (
router.push({
pathname: "/detail",
- params: { title: item.title, content: item.content },
+ params: { id: item.id },
})
}
>
{item.title}
+ {item.content}
+ Created by {item.creatorLabel}
+
+ Last changed {formatTimestamp(item.lastChangedAt)}
+
)}
/>
- router.push("/newNote")}
- >
- +
-
+ {activeTab === "my-notes" ? (
+ router.push("/newNote")}
+ >
+ +
+
+ ) : null}
);
}
const styles = StyleSheet.create({
container: { flex: 1 },
- list: { padding: 16, gap: 12, paddingTop: 20 },
- noteItem: { padding: 16, borderWidth: 1, borderRadius:12 },
+ topBar: {
+ 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" },
+ 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:
{
position: "absolute",
diff --git a/FastNotes/app/login.tsx b/FastNotes/app/login.tsx
new file mode 100644
index 0000000..0adecc4
--- /dev/null
+++ b/FastNotes/app/login.tsx
@@ -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(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 (
+ <>
+
+
+ Login
+
+ E-mail
+
+
+ Password
+
+
+ {errorMessage ? (
+ {errorMessage}
+ ) : null}
+
+ [
+ styles.loginButton,
+ pressed && !isSubmitting ? styles.loginButtonPressed : null,
+ isSubmitting ? styles.loginButtonDisabled : null,
+ ]}
+ >
+
+ {isSubmitting ? 'Logging in...' : 'Log in'}
+
+
+
+
+ Create a new account
+
+
+ >
+ )
+}
+
+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,
+ },
+})
diff --git a/FastNotes/app/newNote.tsx b/FastNotes/app/newNote.tsx
index ac195bc..41fc817 100644
--- a/FastNotes/app/newNote.tsx
+++ b/FastNotes/app/newNote.tsx
@@ -13,20 +13,34 @@ import { useHeaderHeight } from "@react-navigation/elements";
export default function NewNoteScreen()
{
- const { addNote } = useNotes();
- const[title, setTitle] = useState("");
- const [content, setContent] = useState("");
- const insets = useSafeAreaInsets();
- const headerHeight = useHeaderHeight();
- const [contentHeight, setContentHeight] = useState(160);
- const scrollRef = useRef(null);
+ const { addNote, errorMessage } = useNotes()
+ const [title, setTitle] = useState("")
+ const [content, setContent] = useState("")
+ const [isSaving, setIsSaving] = useState(false)
+ const [localErrorMessage, setLocalErrorMessage] = useState(null)
+ const insets = useSafeAreaInsets()
+ const headerHeight = useHeaderHeight()
+ const [contentHeight, setContentHeight] = useState(160)
+ const scrollRef = useRef(null)
- const onSave = () =>
+ const onSave = async () =>
{
- if(!title.trim() && !content.trim()) return;
- addNote(title, content);
- router.back();
- };
+ if(!title.trim() || !content.trim()) {
+ setLocalErrorMessage("Title and content are required.")
+ return
+ }
+
+ setIsSaving(true)
+ setLocalErrorMessage(null)
+
+ const wasSaved = await addNote(title, content)
+
+ setIsSaving(false)
+
+ if (wasSaved) {
+ router.back()
+ }
+ }
return (
{setContentHeight(e.nativeEvent.contentSize.height);
+ onContentSizeChange={(e) => {setContentHeight(e.nativeEvent.contentSize.height);
scrollRef.current?.scrollToEnd({ animated: true });
}}/>
+ {localErrorMessage ? (
+ {localErrorMessage}
+ ) : null}
+
+ {!localErrorMessage && errorMessage ? (
+ {errorMessage}
+ ) : null}
+
-
- Save
+
+ {isSaving ? "Saving..." : "Save"}
+
@@ -101,6 +125,9 @@ const styles = StyleSheet.create(
color: "white",
fontSize: 16,
fontWeight: "700"
- }
+ },
+ errorText: {
+ color: "#c62828"
+ },
}
-);
\ No newline at end of file
+);
diff --git a/FastNotes/app/signup.tsx b/FastNotes/app/signup.tsx
new file mode 100644
index 0000000..cb0a9c3
--- /dev/null
+++ b/FastNotes/app/signup.tsx
@@ -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(null)
+ const [successMessage, setSuccessMessage] = useState(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 (
+ <>
+
+
+ Sign up
+
+ E-mail
+
+
+ Password
+
+
+ {errorMessage ? (
+ {errorMessage}
+ ) : null}
+
+ {successMessage ? (
+ {successMessage}
+ ) : null}
+
+ [
+ styles.actionButton,
+ pressed && !isSubmitting ? styles.actionButtonPressed : null,
+ isSubmitting ? styles.actionButtonDisabled : null,
+ ]}
+ >
+
+ {isSubmitting ? 'Creating account...' : 'Sign up'}
+
+
+
+
+ Already have an account? Log in
+
+
+
+ >
+ )
+}
+
+
+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,
+ },
+})
diff --git a/FastNotes/components/social-auth-buttons/sign-out-button.tsx b/FastNotes/components/social-auth-buttons/sign-out-button.tsx
new file mode 100644
index 0000000..0aa20d1
--- /dev/null
+++ b/FastNotes/components/social-auth-buttons/sign-out-button.tsx
@@ -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
+}
diff --git a/FastNotes/hooks/use-auth-context.tsx b/FastNotes/hooks/use-auth-context.tsx
new file mode 100644
index 0000000..8786a99
--- /dev/null
+++ b/FastNotes/hooks/use-auth-context.tsx
@@ -0,0 +1,17 @@
+import { createContext, useContext } from 'react'
+
+export type AuthData = {
+ claims?: Record | null
+ profile?: any | null
+ isLoading: boolean
+ isLoggedIn: boolean
+}
+
+export const AuthContext = createContext({
+ claims: undefined,
+ profile: undefined,
+ isLoading: true,
+ isLoggedIn: false,
+})
+
+export const useAuthContext = () => useContext(AuthContext)
\ No newline at end of file
diff --git a/FastNotes/hooks/use-theme-color.ts b/FastNotes/hooks/use-theme-color.ts
index 0cbc3a6..d98c088 100644
--- a/FastNotes/hooks/use-theme-color.ts
+++ b/FastNotes/hooks/use-theme-color.ts
@@ -1,8 +1,3 @@
-/**
- * Learn more about light and dark modes:
- * https://docs.expo.dev/guides/color-schemes/
- */
-
import { Colors } from '@/constants/theme';
import { useColorScheme } from '@/hooks/use-color-scheme';
diff --git a/FastNotes/libs/supabase.ts b/FastNotes/libs/supabase.ts
new file mode 100644
index 0000000..16d9370
--- /dev/null
+++ b/FastNotes/libs/supabase.ts
@@ -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,
+ },
+ }
+)
\ No newline at end of file
diff --git a/FastNotes/package-lock.json b/FastNotes/package-lock.json
index 94219f7..b6ea872 100644
--- a/FastNotes/package-lock.json
+++ b/FastNotes/package-lock.json
@@ -9,9 +9,12 @@
"version": "1.0.0",
"dependencies": {
"@expo/vector-icons": "^15.0.3",
+ "@react-native-async-storage/async-storage": "2.2.0",
"@react-navigation/bottom-tabs": "^7.4.0",
"@react-navigation/elements": "^2.6.3",
"@react-navigation/native": "^7.1.28",
+ "@supabase/supabase-js": "^2.98.0",
+ "async-storage": "^0.1.0",
"expo": "~54.0.33",
"expo-constants": "~18.0.13",
"expo-font": "~14.0.11",
@@ -19,7 +22,9 @@
"expo-image": "~3.0.11",
"expo-linking": "~8.0.11",
"expo-router": "~6.0.23",
+ "expo-secure-store": "~15.0.8",
"expo-splash-screen": "~31.0.13",
+ "expo-sqlite": "~16.0.10",
"expo-status-bar": "~3.0.9",
"expo-symbols": "~1.0.8",
"expo-system-ui": "~6.0.9",
@@ -31,8 +36,11 @@
"react-native-reanimated": "~4.1.1",
"react-native-safe-area-context": "~5.6.0",
"react-native-screens": "~4.16.0",
+ "react-native-url-polyfill": "^3.0.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": {
"@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": {
"version": "0.81.5",
"resolved": "https://registry.npmjs.org/@react-native/assets-registry/-/assets-registry-0.81.5.tgz",
@@ -3328,6 +3348,107 @@
"@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": {
"version": "0.10.1",
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz",
@@ -3449,6 +3570,12 @@
"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": {
"version": "19.1.17",
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.17.tgz",
@@ -3465,6 +3592,15 @@
"integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==",
"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": {
"version": "17.0.35",
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz",
@@ -4437,6 +4573,16 @@
"integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==",
"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": {
"version": "1.0.7",
"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"
}
},
+ "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": {
"version": "29.7.0",
"resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz",
@@ -4738,6 +4890,55 @@
"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": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/bplist-creator/-/bplist-creator-0.1.0.tgz",
@@ -5070,6 +5271,15 @@
"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": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz",
@@ -5277,6 +5487,15 @@
"devOptional": true,
"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": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz",
@@ -6655,6 +6874,15 @@
"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": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/expo-server/-/expo-server-1.0.5.tgz",
@@ -6676,6 +6904,20 @@
"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": {
"version": "3.0.9",
"resolved": "https://registry.npmjs.org/expo-status-bar/-/expo-status-bar-3.0.9.tgz",
@@ -6796,6 +7038,29 @@
"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": {
"version": "8.0.0",
"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"
}
},
+ "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": {
"version": "2.0.0",
"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==",
"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": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
@@ -7881,6 +8167,15 @@
"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": {
"version": "1.2.1",
"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==",
"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": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
@@ -9346,6 +9653,26 @@
"integrity": "sha512-SrQrok4CATudVzBS7coSz26QRSmlK9TzzoFbeKfcPBUFPjcQM9Rqvr/DlJkOrwI/0KcgvMub1n1g5Jt9EgRn4A==",
"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": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/node-exports-info/-/node-exports-info-1.6.0.tgz",
@@ -9415,6 +9742,15 @@
"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": {
"version": "11.0.3",
"resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-11.0.3.tgz",
@@ -10391,6 +10727,18 @@
"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": {
"version": "0.21.2",
"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": {
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz",
@@ -11641,6 +11998,48 @@
"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": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
@@ -12579,6 +12978,15 @@
"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": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
diff --git a/FastNotes/package.json b/FastNotes/package.json
index 18b4380..ca309ac 100644
--- a/FastNotes/package.json
+++ b/FastNotes/package.json
@@ -12,9 +12,12 @@
},
"dependencies": {
"@expo/vector-icons": "^15.0.3",
+ "@react-native-async-storage/async-storage": "2.2.0",
"@react-navigation/bottom-tabs": "^7.4.0",
"@react-navigation/elements": "^2.6.3",
"@react-navigation/native": "^7.1.28",
+ "@supabase/supabase-js": "^2.98.0",
+ "async-storage": "^0.1.0",
"expo": "~54.0.33",
"expo-constants": "~18.0.13",
"expo-font": "~14.0.11",
@@ -22,7 +25,9 @@
"expo-image": "~3.0.11",
"expo-linking": "~8.0.11",
"expo-router": "~6.0.23",
+ "expo-secure-store": "~15.0.8",
"expo-splash-screen": "~31.0.13",
+ "expo-sqlite": "~16.0.10",
"expo-status-bar": "~3.0.9",
"expo-symbols": "~1.0.8",
"expo-system-ui": "~6.0.9",
@@ -34,8 +39,11 @@
"react-native-reanimated": "~4.1.1",
"react-native-safe-area-context": "~5.6.0",
"react-native-screens": "~4.16.0",
+ "react-native-url-polyfill": "^3.0.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": {
"@types/react": "~19.1.0",
diff --git a/FastNotes/providers/auth-provider.tsx b/FastNotes/providers/auth-provider.tsx
new file mode 100644
index 0000000..e18608d
--- /dev/null
+++ b/FastNotes/providers/auth-provider.tsx
@@ -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 | undefined | null>()
+ const [profile, setProfile] = useState()
+ const [isLoading, setIsLoading] = useState(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 (
+
+ {children}
+
+ )
+}
diff --git a/FastNotes/src/notes/NotesContext.tsx b/FastNotes/src/notes/NotesContext.tsx
index 024609f..3344d6e 100644
--- a/FastNotes/src/notes/NotesContext.tsx
+++ b/FastNotes/src/notes/NotesContext.tsx
@@ -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 =
-{
- id: string;
- title: string;
- content: string;
-};
-
-type NotesContextValue =
-{
- notes: Note[];
- addNote: (title: string, content: string) => void;
-};
-
-const NotesContext = createContext(undefined);
-
-export function NotesProvider({ children }: { children: React.ReactNode })
-{
- const [notes, setNotes] = useState
- ([
- { 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 {children} ;
+type NoteRow = {
+ id: number
+ created_by: string
+ title: string
+ content: string
+ created_at: string
+ updated_at?: string | null
}
-export function useNotes()
-{
- const ctx = useContext(NotesContext);
- if(!ctx) throw new Error("useNotes must be used inside NotesProvider");
- return ctx;
-}
\ No newline at end of file
+type ProfileRow = {
+ id: string
+ full_name?: string | null
+ username?: string | null
+ 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
+ addNote: (title: string, content: string) => Promise
+ updateNote: (noteId: string, title: string, content: string) => Promise
+ deleteNote: (noteId: string) => Promise
+}
+
+const NotesContext = createContext(undefined)
+
+export function NotesProvider({ children }: { children: React.ReactNode }) {
+ const { claims, isLoggedIn, profile } = useAuthContext()
+ const [notes, setNotes] = useState([])
+ const [isLoading, setIsLoading] = useState(false)
+ const [errorMessage, setErrorMessage] = useState(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
+ }
+
+ const { data, error } = await supabase
+ .from("profiles")
+ .select("id, full_name, username, email")
+ .in("id", creatorIds)
+
+ if (error || !data) {
+ return {} as Record
+ }
+
+ return (data as ProfileRow[]).reduce>((acc, row) => {
+ acc[row.id] =
+ row.full_name ||
+ row.username ||
+ row.email ||
+ "Unknown user"
+
+ return acc
+ }, {})
+ }
+
+ const mapNote = (row: NoteRow, labels: Record): 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 (
+
+ {children}
+
+ )
+}
+
+export function useNotes() {
+ const ctx = useContext(NotesContext)
+ if (!ctx) {
+ throw new Error("useNotes must be used inside NotesProvider")
+ }
+
+ return ctx
+}
diff --git a/FastNotes/tempStorage.tsx b/FastNotes/tempStorage.tsx
new file mode 100644
index 0000000..fb1c20b
--- /dev/null
+++ b/FastNotes/tempStorage.tsx
@@ -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 (
+
+ }
+ >
+
+ Welcome!
+
+
+
+ Username
+ {profile?.username}
+ Full name
+ {profile?.full_name}
+
+
+
+ )
+}
+
+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',
+ },
+})
\ No newline at end of file
diff --git a/FastNotesKotlin/.idea/.gitignore b/FastNotesKotlin/.idea/.gitignore
new file mode 100644
index 0000000..26d3352
--- /dev/null
+++ b/FastNotesKotlin/.idea/.gitignore
@@ -0,0 +1,3 @@
+# Default ignored files
+/shelf/
+/workspace.xml
diff --git a/FastNotesKotlin/.idea/.name b/FastNotesKotlin/.idea/.name
new file mode 100644
index 0000000..f084df3
--- /dev/null
+++ b/FastNotesKotlin/.idea/.name
@@ -0,0 +1 @@
+FastNotes - Kotlin
\ No newline at end of file
diff --git a/FastNotesKotlin/.idea/AndroidProjectSystem.xml b/FastNotesKotlin/.idea/AndroidProjectSystem.xml
new file mode 100644
index 0000000..4a53bee
--- /dev/null
+++ b/FastNotesKotlin/.idea/AndroidProjectSystem.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/FastNotesKotlin/.idea/compiler.xml b/FastNotesKotlin/.idea/compiler.xml
new file mode 100644
index 0000000..b86273d
--- /dev/null
+++ b/FastNotesKotlin/.idea/compiler.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/FastNotesKotlin/.idea/deploymentTargetSelector.xml b/FastNotesKotlin/.idea/deploymentTargetSelector.xml
new file mode 100644
index 0000000..b268ef3
--- /dev/null
+++ b/FastNotesKotlin/.idea/deploymentTargetSelector.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/FastNotesKotlin/.idea/deviceManager.xml b/FastNotesKotlin/.idea/deviceManager.xml
new file mode 100644
index 0000000..91f9558
--- /dev/null
+++ b/FastNotesKotlin/.idea/deviceManager.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/FastNotesKotlin/.idea/gradle.xml b/FastNotesKotlin/.idea/gradle.xml
new file mode 100644
index 0000000..cdbc250
--- /dev/null
+++ b/FastNotesKotlin/.idea/gradle.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/FastNotesKotlin/.idea/markdown.xml b/FastNotesKotlin/.idea/markdown.xml
new file mode 100644
index 0000000..c61ea33
--- /dev/null
+++ b/FastNotesKotlin/.idea/markdown.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/FastNotesKotlin/.idea/misc.xml b/FastNotesKotlin/.idea/misc.xml
new file mode 100644
index 0000000..e984dde
--- /dev/null
+++ b/FastNotesKotlin/.idea/misc.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/FastNotesKotlin/.idea/runConfigurations.xml b/FastNotesKotlin/.idea/runConfigurations.xml
new file mode 100644
index 0000000..16660f1
--- /dev/null
+++ b/FastNotesKotlin/.idea/runConfigurations.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/FastNotesKotlin/.idea/vcs.xml b/FastNotesKotlin/.idea/vcs.xml
new file mode 100644
index 0000000..6c0b863
--- /dev/null
+++ b/FastNotesKotlin/.idea/vcs.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file