Trying to add supabase as the backend. Updated packages
This commit is contained in:
		
							
								
								
									
										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",
 | 
			
		||||
    "orientation": "portrait",
 | 
			
		||||
    "icon": "./assets/images/icon.png",
 | 
			
		||||
    "scheme": "myapp",
 | 
			
		||||
    "scheme": "com.techtracker",
 | 
			
		||||
    "userInterfaceStyle": "automatic",
 | 
			
		||||
    "newArchEnabled": true,
 | 
			
		||||
    "ios": {
 | 
			
		||||
@@ -34,7 +34,8 @@
 | 
			
		||||
          "resizeMode": "contain",
 | 
			
		||||
          "backgroundColor": "#ffffff"
 | 
			
		||||
        }
 | 
			
		||||
      ]
 | 
			
		||||
      ],
 | 
			
		||||
      "expo-secure-store"
 | 
			
		||||
    ],
 | 
			
		||||
    "experiments": {
 | 
			
		||||
      "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": {
 | 
			
		||||
    "@expo/vector-icons": "^14.0.2",
 | 
			
		||||
    "@react-native-async-storage/async-storage": "^1.23.1",
 | 
			
		||||
    "@react-navigation/bottom-tabs": "^7.2.0",
 | 
			
		||||
    "@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-blur": "~14.0.3",
 | 
			
		||||
    "expo-constants": "~17.0.5",
 | 
			
		||||
@@ -40,11 +44,13 @@
 | 
			
		||||
    "react-dom": "18.3.1",
 | 
			
		||||
    "react-native": "0.76.6",
 | 
			
		||||
    "react-native-gesture-handler": "~2.20.2",
 | 
			
		||||
    "react-native-get-random-values": "^1.11.0",
 | 
			
		||||
    "react-native-reanimated": "~3.16.1",
 | 
			
		||||
    "react-native-safe-area-context": "4.12.0",
 | 
			
		||||
    "react-native-screens": "~4.4.0",
 | 
			
		||||
    "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": {
 | 
			
		||||
    "@babel/core": "^7.25.2",
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user