Trying to add supabase as the backend. Updated packages
This commit is contained in:
parent
c5916a195a
commit
06471f688a
7
.vscode/settings.json
vendored
7
.vscode/settings.json
vendored
@ -1,7 +0,0 @@
|
|||||||
{
|
|
||||||
"editor.codeActionsOnSave": {
|
|
||||||
"source.fixAll": "explicit",
|
|
||||||
"source.organizeImports": "explicit",
|
|
||||||
"source.sortMembers": "explicit"
|
|
||||||
}
|
|
||||||
}
|
|
5
app.json
5
app.json
@ -5,7 +5,7 @@
|
|||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"orientation": "portrait",
|
"orientation": "portrait",
|
||||||
"icon": "./assets/images/icon.png",
|
"icon": "./assets/images/icon.png",
|
||||||
"scheme": "myapp",
|
"scheme": "com.techtracker",
|
||||||
"userInterfaceStyle": "automatic",
|
"userInterfaceStyle": "automatic",
|
||||||
"newArchEnabled": true,
|
"newArchEnabled": true,
|
||||||
"ios": {
|
"ios": {
|
||||||
@ -34,7 +34,8 @@
|
|||||||
"resizeMode": "contain",
|
"resizeMode": "contain",
|
||||||
"backgroundColor": "#ffffff"
|
"backgroundColor": "#ffffff"
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
"expo-secure-store"
|
||||||
],
|
],
|
||||||
"experiments": {
|
"experiments": {
|
||||||
"typedRoutes": true
|
"typedRoutes": true
|
||||||
|
120
components/Account.tsx
Normal file
120
components/Account.tsx
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
import { useState, useEffect } from 'react'
|
||||||
|
import { supabase } from '../lib/supabase'
|
||||||
|
import { StyleSheet, View, Alert } from 'react-native'
|
||||||
|
import { Button, Input } from '@rneui/themed'
|
||||||
|
import { Session } from '@supabase/supabase-js'
|
||||||
|
|
||||||
|
export default function Account({ session }: { session: Session }) {
|
||||||
|
const [loading, setLoading] = useState(true)
|
||||||
|
const [username, setUsername] = useState('')
|
||||||
|
const [website, setWebsite] = useState('')
|
||||||
|
const [avatarUrl, setAvatarUrl] = useState('')
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (session) getProfile()
|
||||||
|
}, [session])
|
||||||
|
|
||||||
|
async function getProfile() {
|
||||||
|
try {
|
||||||
|
setLoading(true)
|
||||||
|
if (!session?.user) throw new Error('No user on the session!')
|
||||||
|
|
||||||
|
const { data, error, status } = await supabase
|
||||||
|
.from('profiles')
|
||||||
|
.select(`username, website, avatar_url`)
|
||||||
|
.eq('id', session?.user.id)
|
||||||
|
.single()
|
||||||
|
if (error && status !== 406) {
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data) {
|
||||||
|
setUsername(data.username)
|
||||||
|
setWebsite(data.website)
|
||||||
|
setAvatarUrl(data.avatar_url)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof Error) {
|
||||||
|
Alert.alert(error.message)
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateProfile({
|
||||||
|
username,
|
||||||
|
website,
|
||||||
|
avatar_url,
|
||||||
|
}: {
|
||||||
|
username: string
|
||||||
|
website: string
|
||||||
|
avatar_url: string
|
||||||
|
}) {
|
||||||
|
try {
|
||||||
|
setLoading(true)
|
||||||
|
if (!session?.user) throw new Error('No user on the session!')
|
||||||
|
|
||||||
|
const updates = {
|
||||||
|
id: session?.user.id,
|
||||||
|
username,
|
||||||
|
website,
|
||||||
|
avatar_url,
|
||||||
|
updated_at: new Date(),
|
||||||
|
}
|
||||||
|
|
||||||
|
const { error } = await supabase.from('profiles').upsert(updates)
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof Error) {
|
||||||
|
Alert.alert(error.message)
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={styles.container}>
|
||||||
|
<View style={[styles.verticallySpaced, styles.mt20]}>
|
||||||
|
<Input label="Email" value={session?.user?.email} disabled />
|
||||||
|
</View>
|
||||||
|
<View style={styles.verticallySpaced}>
|
||||||
|
<Input label="Username" value={username || ''} onChangeText={(text) => setUsername(text)} />
|
||||||
|
</View>
|
||||||
|
<View style={styles.verticallySpaced}>
|
||||||
|
<Input label="Website" value={website || ''} onChangeText={(text) => setWebsite(text)} />
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View style={[styles.verticallySpaced, styles.mt20]}>
|
||||||
|
<Button
|
||||||
|
title={loading ? 'Loading ...' : 'Update'}
|
||||||
|
onPress={() => updateProfile({ username, website, avatar_url: avatarUrl })}
|
||||||
|
disabled={loading}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View style={styles.verticallySpaced}>
|
||||||
|
<Button title="Sign Out" onPress={() => supabase.auth.signOut()} />
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
marginTop: 40,
|
||||||
|
padding: 12,
|
||||||
|
},
|
||||||
|
verticallySpaced: {
|
||||||
|
paddingTop: 4,
|
||||||
|
paddingBottom: 4,
|
||||||
|
alignSelf: 'stretch',
|
||||||
|
},
|
||||||
|
mt20: {
|
||||||
|
marginTop: 20,
|
||||||
|
},
|
||||||
|
})
|
95
components/Auth.tsx
Normal file
95
components/Auth.tsx
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
import React, { useState } from 'react'
|
||||||
|
import { Alert, StyleSheet, View, AppState } from 'react-native'
|
||||||
|
import { supabase } from '../lib/supabase'
|
||||||
|
import { Button, Input } from '@rneui/themed'
|
||||||
|
|
||||||
|
// Tells Supabase Auth to continuously refresh the session automatically if
|
||||||
|
// the app is in the foreground. When this is added, you will continue to receive
|
||||||
|
// `onAuthStateChange` events with the `TOKEN_REFRESHED` or `SIGNED_OUT` event
|
||||||
|
// if the user's session is terminated. This should only be registered once.
|
||||||
|
AppState.addEventListener('change', (state) => {
|
||||||
|
if (state === 'active') {
|
||||||
|
supabase.auth.startAutoRefresh()
|
||||||
|
} else {
|
||||||
|
supabase.auth.stopAutoRefresh()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export default function Auth() {
|
||||||
|
const [email, setEmail] = useState('')
|
||||||
|
const [password, setPassword] = useState('')
|
||||||
|
const [loading, setLoading] = useState(false)
|
||||||
|
|
||||||
|
async function signInWithEmail() {
|
||||||
|
setLoading(true)
|
||||||
|
const { error } = await supabase.auth.signInWithPassword({
|
||||||
|
email: email,
|
||||||
|
password: password,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (error) Alert.alert(error.message)
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function signUpWithEmail() {
|
||||||
|
setLoading(true)
|
||||||
|
const {
|
||||||
|
data: { session },
|
||||||
|
error,
|
||||||
|
} = await supabase.auth.signUp({
|
||||||
|
email: email,
|
||||||
|
password: password,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (error) Alert.alert(error.message)
|
||||||
|
if (!session) Alert.alert('Please check your inbox for email verification!')
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={styles.container}>
|
||||||
|
<View style={[styles.verticallySpaced, styles.mt20]}>
|
||||||
|
<Input
|
||||||
|
label="Email"
|
||||||
|
leftIcon={{ type: 'font-awesome', name: 'envelope' }}
|
||||||
|
onChangeText={(text) => setEmail(text)}
|
||||||
|
value={email}
|
||||||
|
placeholder="email@address.com"
|
||||||
|
autoCapitalize={'none'}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
<View style={styles.verticallySpaced}>
|
||||||
|
<Input
|
||||||
|
label="Password"
|
||||||
|
leftIcon={{ type: 'font-awesome', name: 'lock' }}
|
||||||
|
onChangeText={(text) => setPassword(text)}
|
||||||
|
value={password}
|
||||||
|
secureTextEntry={true}
|
||||||
|
placeholder="Password"
|
||||||
|
autoCapitalize={'none'}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
<View style={[styles.verticallySpaced, styles.mt20]}>
|
||||||
|
<Button title="Sign in" disabled={loading} onPress={() => signInWithEmail()} />
|
||||||
|
</View>
|
||||||
|
<View style={styles.verticallySpaced}>
|
||||||
|
<Button title="Sign up" disabled={loading} onPress={() => signUpWithEmail()} />
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
marginTop: 40,
|
||||||
|
padding: 12,
|
||||||
|
},
|
||||||
|
verticallySpaced: {
|
||||||
|
paddingTop: 4,
|
||||||
|
paddingBottom: 4,
|
||||||
|
alignSelf: 'stretch',
|
||||||
|
},
|
||||||
|
mt20: {
|
||||||
|
marginTop: 20,
|
||||||
|
},
|
||||||
|
})
|
63
lib/supabase.ts
Normal file
63
lib/supabase.ts
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
import { createClient } from "@supabase/supabase-js";
|
||||||
|
import AsyncStorage from "@react-native-async-storage/async-storage";
|
||||||
|
import * as SecureStore from 'expo-secure-store';
|
||||||
|
import * as aesjs from 'aes-js';
|
||||||
|
import 'react-native-get-random-values';
|
||||||
|
|
||||||
|
// As Expo's SecureStore does not support values larger than 2048
|
||||||
|
// bytes, an AES-256 key is generated and stored in SecureStore, while
|
||||||
|
// it is used to encrypt/decrypt values stored in AsyncStorage.
|
||||||
|
class LargeSecureStore {
|
||||||
|
private async _encrypt(key: string, value: string) {
|
||||||
|
const encryptionKey = crypto.getRandomValues(new Uint8Array(256 / 8));
|
||||||
|
|
||||||
|
const cipher = new aesjs.ModeOfOperation.ctr(encryptionKey, new aesjs.Counter(1));
|
||||||
|
const encryptedBytes = cipher.encrypt(aesjs.utils.utf8.toBytes(value));
|
||||||
|
|
||||||
|
await SecureStore.setItemAsync(key, aesjs.utils.hex.fromBytes(encryptionKey));
|
||||||
|
|
||||||
|
return aesjs.utils.hex.fromBytes(encryptedBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _decrypt(key: string, value: string) {
|
||||||
|
const encryptionKeyHex = await SecureStore.getItemAsync(key);
|
||||||
|
if (!encryptionKeyHex) {
|
||||||
|
return encryptionKeyHex;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cipher = new aesjs.ModeOfOperation.ctr(aesjs.utils.hex.toBytes(encryptionKeyHex), new aesjs.Counter(1));
|
||||||
|
const decryptedBytes = cipher.decrypt(aesjs.utils.hex.toBytes(value));
|
||||||
|
|
||||||
|
return aesjs.utils.utf8.fromBytes(decryptedBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getItem(key: string) {
|
||||||
|
const encrypted = await AsyncStorage.getItem(key);
|
||||||
|
if (!encrypted) { return encrypted; }
|
||||||
|
|
||||||
|
return await this._decrypt(key, encrypted);
|
||||||
|
}
|
||||||
|
|
||||||
|
async removeItem(key: string) {
|
||||||
|
await AsyncStorage.removeItem(key);
|
||||||
|
await SecureStore.deleteItemAsync(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
async setItem(key: string, value: string) {
|
||||||
|
const encrypted = await this._encrypt(key, value);
|
||||||
|
|
||||||
|
await AsyncStorage.setItem(key, encrypted);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const supabaseUrl = process.env.EXPO_PUBLIC_SUPABASE_URL as string;
|
||||||
|
const supabaseAnonKey = process.env.EXPO_PUBLIC_SUPABASE_ANON_KEY as string;
|
||||||
|
|
||||||
|
export const supabase = createClient(supabaseUrl, supabaseAnonKey, {
|
||||||
|
auth: {
|
||||||
|
storage: new LargeSecureStore(),
|
||||||
|
autoRefreshToken: true,
|
||||||
|
persistSession: true,
|
||||||
|
detectSessionInUrl: false,
|
||||||
|
},
|
||||||
|
});
|
1811
package-lock.json
generated
1811
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -19,8 +19,12 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@expo/vector-icons": "^14.0.2",
|
"@expo/vector-icons": "^14.0.2",
|
||||||
|
"@react-native-async-storage/async-storage": "^1.23.1",
|
||||||
"@react-navigation/bottom-tabs": "^7.2.0",
|
"@react-navigation/bottom-tabs": "^7.2.0",
|
||||||
"@react-navigation/native": "^7.0.14",
|
"@react-navigation/native": "^7.0.14",
|
||||||
|
"@rneui/themed": "^4.0.0-rc.8",
|
||||||
|
"@supabase/supabase-js": "^2.48.1",
|
||||||
|
"aes-js": "^3.1.2",
|
||||||
"expo": "~52.0.28",
|
"expo": "~52.0.28",
|
||||||
"expo-blur": "~14.0.3",
|
"expo-blur": "~14.0.3",
|
||||||
"expo-constants": "~17.0.5",
|
"expo-constants": "~17.0.5",
|
||||||
@ -40,11 +44,13 @@
|
|||||||
"react-dom": "18.3.1",
|
"react-dom": "18.3.1",
|
||||||
"react-native": "0.76.6",
|
"react-native": "0.76.6",
|
||||||
"react-native-gesture-handler": "~2.20.2",
|
"react-native-gesture-handler": "~2.20.2",
|
||||||
|
"react-native-get-random-values": "^1.11.0",
|
||||||
"react-native-reanimated": "~3.16.1",
|
"react-native-reanimated": "~3.16.1",
|
||||||
"react-native-safe-area-context": "4.12.0",
|
"react-native-safe-area-context": "4.12.0",
|
||||||
"react-native-screens": "~4.4.0",
|
"react-native-screens": "~4.4.0",
|
||||||
"react-native-web": "~0.19.13",
|
"react-native-web": "~0.19.13",
|
||||||
"react-native-webview": "13.12.5"
|
"react-native-webview": "13.12.5",
|
||||||
|
"expo-secure-store": "~14.0.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.25.2",
|
"@babel/core": "^7.25.2",
|
||||||
|
Loading…
x
Reference in New Issue
Block a user