mirror of
https://github.com/supabase/supabase.git
synced 2026-10-06 09:55:06 +03:00
docs: Expo User Management auth changes (#43568)
## I have read the [CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md) file. YES/
This commit is contained in:
1 parent
01c178e159
commit
8cfdc8576e
7 files changed
+3502
-11475
No files matched your search
@@ -14,15 +14,18 @@ If you get stuck while working through this guide, refer to the [full example on
|
||||
|
||||
</Admonition>
|
||||
|
||||
<$Partial path="project_setup.mdx" variables={{ "framework": "exporeactnative", "tab": "mobiles" }} />
|
||||
<$Partial
|
||||
path="project_setup.mdx"
|
||||
variables={{ "framework": "exporeactnative", "tab": "mobiles" }}
|
||||
/>
|
||||
|
||||
## Building the app
|
||||
|
||||
Let's start building the React Native app from scratch.
|
||||
Start by building the React Native app from scratch.
|
||||
|
||||
### Initialize a React Native app
|
||||
|
||||
We can use [`expo`](https://docs.expo.dev/get-started/create-a-new-app/) to initialize
|
||||
Use [`expo`](https://docs.expo.dev/get-started/create-a-new-app/) to initialize
|
||||
an app called `expo-user-management`:
|
||||
|
||||
```bash
|
||||
@@ -31,14 +34,14 @@ npx create-expo-app -t expo-template-blank-typescript expo-user-management
|
||||
cd expo-user-management
|
||||
```
|
||||
|
||||
Then let's install the additional dependencies: [supabase-js](https://github.com/supabase/supabase-js)
|
||||
Then install the additional dependencies:
|
||||
|
||||
```bash
|
||||
npx expo install @supabase/supabase-js @rneui/themed expo-sqlite
|
||||
```
|
||||
|
||||
Now let's create a helper file to initialize the Supabase client.
|
||||
We need the API URL and the key that you copied [earlier](#get-api-details).
|
||||
Now create a helper file to initialize the Supabase client using the API URL and the key that you copied [earlier](#get-api-details).
|
||||
|
||||
These variables are safe to expose in your Expo app since Supabase has
|
||||
[Row Level Security](/docs/guides/database/postgres/row-level-security) enabled on your Database.
|
||||
|
||||
@@ -51,26 +54,11 @@ These variables are safe to expose in your Expo app since Supabase has
|
||||
>
|
||||
<TabPanel id="local-storage" label="LocalStorage">
|
||||
|
||||
<$CodeTabs>
|
||||
|
||||
```ts name=lib/supabase.ts
|
||||
import 'expo-sqlite/localStorage/install';
|
||||
import { createClient } from '@supabase/supabase-js'
|
||||
|
||||
const supabaseUrl = YOUR_REACT_NATIVE_SUPABASE_URL
|
||||
const supabasePublishableKey = YOUR_REACT_NATIVE_SUPABASE_PUBLISHABLE_KEY
|
||||
|
||||
export const supabase = createClient(supabaseUrl, supabasePublishableKey, {
|
||||
auth: {
|
||||
storage: localStorage,
|
||||
autoRefreshToken: true,
|
||||
persistSession: true,
|
||||
detectSessionInUrl: false,
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
</$CodeTabs>
|
||||
<$CodeSample
|
||||
path="/user-management/expo-user-management/lib/supabase.ts"
|
||||
lines={[[1, -1]]}
|
||||
meta="name=lib/supabase.ts"
|
||||
/>
|
||||
|
||||
</TabPanel>
|
||||
<TabPanel id="secure-store" label="SecureStore">
|
||||
@@ -78,6 +66,7 @@ These variables are safe to expose in your Expo app since Supabase has
|
||||
If you wish to encrypt the user's session information, you can use `aes-js` and store the encryption key in [Expo SecureStore](https://docs.expo.dev/versions/latest/sdk/securestore). The [`aes-js` library](https://github.com/ricmoo/aes-js) is a reputable JavaScript-only implementation of the AES encryption algorithm in CTR mode. A new 256-bit encryption key is generated using the `react-native-get-random-values` library. This key is stored inside Expo's SecureStore, while the value is encrypted and placed inside AsyncStorage.
|
||||
|
||||
Make sure that:
|
||||
|
||||
- You keep the `expo-secure-storage`, `aes-js` and `react-native-get-random-values` libraries up-to-date.
|
||||
- Choose the correct [`SecureStoreOptions`](https://docs.expo.dev/versions/latest/sdk/securestore/#securestoreoptions) for your app's needs. E.g. [`SecureStore.WHEN_UNLOCKED`](https://docs.expo.dev/versions/latest/sdk/securestore/#securestorewhen_unlocked) regulates when the data can be accessed.
|
||||
- Carefully consider optimizations or other modifications to the above example, as those can lead to introducing subtle security vulnerabilities.
|
||||
@@ -169,110 +158,14 @@ These variables are safe to expose in your Expo app since Supabase has
|
||||
|
||||
### Set up a login component
|
||||
|
||||
Let's set up a React Native component to manage logins and sign ups.
|
||||
Users would be able to sign in with their email and password.
|
||||
Set up a React Native component to manage logins and sign ups.
|
||||
Users should be able to sign in with their email and password.
|
||||
|
||||
<$CodeTabs>
|
||||
|
||||
```tsx name=components/Auth.tsx
|
||||
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,
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
</$CodeTabs>
|
||||
<$CodeSample
|
||||
path="/user-management/expo-user-management/components/Auth.tsx"
|
||||
lines={[[1, -1]]}
|
||||
meta="name=components/Auth.tsx"
|
||||
/>
|
||||
|
||||
<Admonition type="note">
|
||||
|
||||
@@ -284,173 +177,25 @@ While testing, you can disable email confirmation in your [project's email auth
|
||||
|
||||
### Account page
|
||||
|
||||
After a user is signed in we can allow them to edit their profile details and manage their account.
|
||||
After a user signs in, you can let them to edit their profile details and manage their account.
|
||||
|
||||
Let's create a new component for that called `Account.tsx`.
|
||||
Create a new component for that called `Account.tsx`.
|
||||
|
||||
<$CodeTabs>
|
||||
|
||||
```tsx name=components/Account.tsx
|
||||
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,
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
</$CodeTabs>
|
||||
<$CodeSample
|
||||
path="/user-management/expo-user-management/components/Account.tsx"
|
||||
lines={[[1, -1]]}
|
||||
meta="name=components/Account.tsx"
|
||||
/>
|
||||
|
||||
### Launch!
|
||||
|
||||
Now that we have all the components in place, let's update `App.tsx`:
|
||||
Now that you have all the components in place, update `App.tsx`:
|
||||
|
||||
<$CodeTabs>
|
||||
|
||||
```tsx name=App.tsx
|
||||
import { useState, useEffect } from 'react'
|
||||
import { supabase } from './lib/supabase'
|
||||
import Auth from './components/Auth'
|
||||
import Account from './components/Account'
|
||||
import { View } from 'react-native'
|
||||
import { Session } from '@supabase/supabase-js'
|
||||
|
||||
export default function App() {
|
||||
const [session, setSession] = useState<Session | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
supabase.auth.getSession().then(({ data: { session } }) => {
|
||||
setSession(session)
|
||||
})
|
||||
|
||||
supabase.auth.onAuthStateChange((_event, session) => {
|
||||
setSession(session)
|
||||
})
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<View>
|
||||
{session && session.user ? <Account key={session.user.id} session={session} /> : <Auth />}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
</$CodeTabs>
|
||||
<$CodeSample
|
||||
path="/user-management/expo-user-management/App.tsx"
|
||||
lines={[[1, -1]]}
|
||||
meta="name=App.tsx"
|
||||
/>
|
||||
|
||||
Once that's done, run this in a terminal window:
|
||||
|
||||
@@ -467,7 +212,7 @@ photos and videos.
|
||||
|
||||
### Additional dependency installation
|
||||
|
||||
You will need an image picker that works on the environment you will build the project for, we will use `expo-image-picker` in this example.
|
||||
You need an image picker that works on the environment you are building the project for, this example uses `expo-image-picker`.
|
||||
|
||||
```bash
|
||||
npx expo install expo-image-picker
|
||||
@@ -475,179 +220,20 @@ npx expo install expo-image-picker
|
||||
|
||||
### Create an upload widget
|
||||
|
||||
Let's create an avatar for the user so that they can upload a profile photo.
|
||||
We can start by creating a new component:
|
||||
Create an avatar for the user so that they can upload a profile photo.
|
||||
Start by creating a new component:
|
||||
|
||||
<$CodeTabs>
|
||||
|
||||
```tsx name=components/Avatar.tsx
|
||||
import { useState, useEffect } from 'react'
|
||||
import { supabase } from '../lib/supabase'
|
||||
import { StyleSheet, View, Alert, Image, Button } from 'react-native'
|
||||
import * as ImagePicker from 'expo-image-picker'
|
||||
|
||||
interface Props {
|
||||
size: number
|
||||
url: string | null
|
||||
onUpload: (filePath: string) => void
|
||||
}
|
||||
|
||||
export default function Avatar({ url, size = 150, onUpload }: Props) {
|
||||
const [uploading, setUploading] = useState(false)
|
||||
const [avatarUrl, setAvatarUrl] = useState<string | null>(null)
|
||||
const avatarSize = { height: size, width: size }
|
||||
|
||||
useEffect(() => {
|
||||
if (url) downloadImage(url)
|
||||
}, [url])
|
||||
|
||||
async function downloadImage(path: string) {
|
||||
try {
|
||||
const { data, error } = await supabase.storage.from('avatars').download(path)
|
||||
|
||||
if (error) {
|
||||
throw error
|
||||
}
|
||||
|
||||
const fr = new FileReader()
|
||||
fr.readAsDataURL(data)
|
||||
fr.onload = () => {
|
||||
setAvatarUrl(fr.result as string)
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
console.log('Error downloading image: ', error.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function uploadAvatar() {
|
||||
try {
|
||||
setUploading(true)
|
||||
|
||||
const result = await ImagePicker.launchImageLibraryAsync({
|
||||
mediaTypes: ImagePicker.MediaTypeOptions.Images, // Restrict to only images
|
||||
allowsMultipleSelection: false, // Can only select one image
|
||||
allowsEditing: true, // Allows the user to crop / rotate their photo before uploading it
|
||||
quality: 1,
|
||||
exif: false, // We don't want nor need that data.
|
||||
})
|
||||
|
||||
if (result.canceled || !result.assets || result.assets.length === 0) {
|
||||
console.log('User cancelled image picker.')
|
||||
return
|
||||
}
|
||||
|
||||
const image = result.assets[0]
|
||||
console.log('Got image', image)
|
||||
|
||||
if (!image.uri) {
|
||||
throw new Error('No image uri!') // Realistically, this should never happen, but just in case...
|
||||
}
|
||||
|
||||
const arraybuffer = await fetch(image.uri).then((res) => res.arrayBuffer())
|
||||
|
||||
const fileExt = image.uri?.split('.').pop()?.toLowerCase() ?? 'jpeg'
|
||||
const path = `${Date.now()}.${fileExt}`
|
||||
const { data, error: uploadError } = await supabase.storage
|
||||
.from('avatars')
|
||||
.upload(path, arraybuffer, {
|
||||
contentType: image.mimeType ?? 'image/jpeg',
|
||||
})
|
||||
|
||||
if (uploadError) {
|
||||
throw uploadError
|
||||
}
|
||||
|
||||
onUpload(data.path)
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
Alert.alert(error.message)
|
||||
} else {
|
||||
throw error
|
||||
}
|
||||
} finally {
|
||||
setUploading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<View>
|
||||
{avatarUrl ? (
|
||||
<Image
|
||||
source={{ uri: avatarUrl }}
|
||||
accessibilityLabel="Avatar"
|
||||
style={[avatarSize, styles.avatar, styles.image]}
|
||||
/>
|
||||
) : (
|
||||
<View style={[avatarSize, styles.avatar, styles.noImage]} />
|
||||
)}
|
||||
<View>
|
||||
<Button
|
||||
title={uploading ? 'Uploading ...' : 'Upload'}
|
||||
onPress={uploadAvatar}
|
||||
disabled={uploading}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
avatar: {
|
||||
borderRadius: 5,
|
||||
overflow: 'hidden',
|
||||
maxWidth: '100%',
|
||||
},
|
||||
image: {
|
||||
objectFit: 'cover',
|
||||
paddingTop: 0,
|
||||
},
|
||||
noImage: {
|
||||
backgroundColor: '#333',
|
||||
borderWidth: 1,
|
||||
borderStyle: 'solid',
|
||||
borderColor: 'rgb(200, 200, 200)',
|
||||
borderRadius: 5,
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
</$CodeTabs>
|
||||
<$CodeSample
|
||||
path="/user-management/expo-user-management/components/Avatar.tsx"
|
||||
lines={[[1, -1]]}
|
||||
meta="name=components/Avatar.tsx"
|
||||
/>
|
||||
|
||||
### Add the new widget
|
||||
|
||||
And then we can add the widget to the Account page:
|
||||
And then add the widget to the Account page. The `Account.tsx` component [shown earlier](#account-page) already includes the `Avatar` component when using the full example code.
|
||||
|
||||
<$CodeTabs>
|
||||
|
||||
```tsx name=components/Account.tsx
|
||||
// Import the new component
|
||||
import Avatar from './Avatar'
|
||||
|
||||
// ...
|
||||
return (
|
||||
<View>
|
||||
{/* Add to the body */}
|
||||
<View>
|
||||
<Avatar
|
||||
size={200}
|
||||
url={avatarUrl}
|
||||
onUpload={(url: string) => {
|
||||
setAvatarUrl(url)
|
||||
updateProfile({ username, website, avatar_url: url })
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
{/* ... */}
|
||||
</View>
|
||||
)
|
||||
// ...
|
||||
```
|
||||
|
||||
</$CodeTabs>
|
||||
|
||||
Now you will need to run the prebuild command to get the application working on your chosen platform.
|
||||
Now run the prebuild command to get the application working on your chosen platform.
|
||||
|
||||
```bash
|
||||
npx expo prebuild
|
||||
|
||||
@@ -4,24 +4,34 @@ import { supabase } from './lib/supabase'
|
||||
import Auth from './components/Auth'
|
||||
import Account from './components/Account'
|
||||
import { View } from 'react-native'
|
||||
import { Session } from '@supabase/supabase-js'
|
||||
|
||||
export default function App() {
|
||||
const [session, setSession] = useState<Session | null>(null)
|
||||
const [userId, setUserId] = useState<string | null>(null)
|
||||
const [email, setEmail] = useState<string | undefined>(undefined)
|
||||
|
||||
useEffect(() => {
|
||||
supabase.auth.getSession().then(({ data: { session } }) => {
|
||||
setSession(session)
|
||||
supabase.auth.getClaims().then(({ data: { claims } }) => {
|
||||
if (claims) {
|
||||
setUserId(claims.sub)
|
||||
setEmail(claims.email)
|
||||
}
|
||||
})
|
||||
|
||||
supabase.auth.onAuthStateChange((_event, session) => {
|
||||
setSession(session)
|
||||
supabase.auth.onAuthStateChange(async (_event, _session) => {
|
||||
const { data: { claims } } = await supabase.auth.getClaims()
|
||||
if (claims) {
|
||||
setUserId(claims.sub)
|
||||
setEmail(claims.email)
|
||||
} else {
|
||||
setUserId(null)
|
||||
setEmail(undefined)
|
||||
}
|
||||
})
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<View>
|
||||
{session && session.user ? <Account key={session.user.id} session={session} /> : <Auth />}
|
||||
{userId ? <Account key={userId} userId={userId} email={email} /> : <Auth />}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
@@ -2,28 +2,26 @@ 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'
|
||||
import Avatar from './Avatar'
|
||||
|
||||
export default function Account({ session }: { session: Session }) {
|
||||
export default function Account({ userId, email }: { userId: string; email?: string }) {
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [username, setUsername] = useState('')
|
||||
const [website, setWebsite] = useState('')
|
||||
const [avatarUrl, setAvatarUrl] = useState('')
|
||||
|
||||
useEffect(() => {
|
||||
if (session) getProfile()
|
||||
}, [session])
|
||||
if (userId) getProfile()
|
||||
}, [userId])
|
||||
|
||||
async function getProfile() {
|
||||
try {
|
||||
setLoading(true)
|
||||
if (!session?.user) throw new Error('No user on the session!')
|
||||
|
||||
let { data, error, status } = await supabase
|
||||
.from('profiles')
|
||||
.select(`username, website, avatar_url`)
|
||||
.eq('id', session?.user.id)
|
||||
.eq('id', userId)
|
||||
.single()
|
||||
if (error && status !== 406) {
|
||||
throw error
|
||||
@@ -54,10 +52,9 @@ export default function Account({ session }: { session: Session }) {
|
||||
}) {
|
||||
try {
|
||||
setLoading(true)
|
||||
if (!session?.user) throw new Error('No user on the session!')
|
||||
|
||||
const updates = {
|
||||
id: session?.user.id,
|
||||
id: userId,
|
||||
username,
|
||||
website,
|
||||
avatar_url,
|
||||
@@ -91,7 +88,7 @@ export default function Account({ session }: { session: Session }) {
|
||||
/>
|
||||
</View>
|
||||
<View style={[styles.verticallySpaced, styles.mt20]}>
|
||||
<Input label="Email" value={session?.user?.email} disabled />
|
||||
<Input label="Email" value={email} disabled />
|
||||
</View>
|
||||
<View style={styles.verticallySpaced}>
|
||||
<Input label="Username" value={username || ''} onChangeText={(text) => setUsername(text)} />
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { supabase } from '../lib/supabase'
|
||||
import { StyleSheet, View, Alert, Image, Button } from 'react-native'
|
||||
import DocumentPicker, { isCancel, isInProgress, types } from 'react-native-document-picker'
|
||||
import * as ImagePicker from 'expo-image-picker'
|
||||
|
||||
interface Props {
|
||||
size: number
|
||||
url: string | null
|
||||
onUpload: (filePath: string) => void
|
||||
url: string | null
|
||||
onUpload: (filePath: string) => void
|
||||
}
|
||||
|
||||
export default function Avatar({ url, size = 150, onUpload }: Props) {
|
||||
@@ -20,10 +20,8 @@ export default function Avatar({ url, size = 150, onUpload }: Props) {
|
||||
|
||||
async function downloadImage(path: string) {
|
||||
try {
|
||||
const { data, error } = await supabase.storage
|
||||
.from('avatars')
|
||||
.download(path)
|
||||
|
||||
const { data, error } = await supabase.storage.from('avatars').download(path)
|
||||
|
||||
if (error) {
|
||||
throw error
|
||||
}
|
||||
@@ -44,28 +42,28 @@ export default function Avatar({ url, size = 150, onUpload }: Props) {
|
||||
try {
|
||||
setUploading(true)
|
||||
|
||||
const file = await DocumentPicker.pickSingle({
|
||||
presentationStyle: 'fullScreen',
|
||||
copyTo: 'cachesDirectory',
|
||||
type: types.images,
|
||||
mode: 'open'
|
||||
const result = await ImagePicker.launchImageLibraryAsync({
|
||||
mediaTypes: ['images'],
|
||||
allowsEditing: true,
|
||||
quality: 1,
|
||||
})
|
||||
|
||||
const photo = {
|
||||
uri: file.fileCopyUri,
|
||||
type: file.type,
|
||||
name: file.name
|
||||
if (result.canceled || !result.assets || result.assets.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
const formData = new FormData()
|
||||
formData.append("file", photo)
|
||||
const image = result.assets[0]
|
||||
if (!image.uri) {
|
||||
throw new Error('No image uri!')
|
||||
}
|
||||
|
||||
const fileExt = file.name.split('.').pop()
|
||||
const arraybuffer = await fetch(image.uri).then((res) => res.arrayBuffer())
|
||||
const fileExt = image.uri.split('.').pop()?.toLowerCase() ?? 'jpeg'
|
||||
const filePath = `${Math.random()}.${fileExt}`
|
||||
|
||||
let { error } = await supabase.storage
|
||||
.from('avatars')
|
||||
.upload(filePath, formData)
|
||||
const { error } = await supabase.storage.from('avatars').upload(filePath, arraybuffer, {
|
||||
contentType: image.mimeType ?? 'image/jpeg',
|
||||
})
|
||||
|
||||
if (error) {
|
||||
throw error
|
||||
@@ -73,15 +71,8 @@ export default function Avatar({ url, size = 150, onUpload }: Props) {
|
||||
|
||||
onUpload(filePath)
|
||||
} catch (error) {
|
||||
if (isCancel(error)) {
|
||||
console.warn('cancelled')
|
||||
// User cancelled the picker, exit any dialogs or menus and move on
|
||||
} else if (isInProgress(error)) {
|
||||
console.warn('multiple pickers were opened, only the last will be considered')
|
||||
} else if (error instanceof Error) {
|
||||
if (error instanceof Error) {
|
||||
Alert.alert(error.message)
|
||||
} else {
|
||||
throw error
|
||||
}
|
||||
} finally {
|
||||
setUploading(false)
|
||||
@@ -91,7 +82,11 @@ export default function Avatar({ url, size = 150, onUpload }: Props) {
|
||||
return (
|
||||
<View>
|
||||
{avatarUrl ? (
|
||||
<Image source={{ uri: avatarUrl }} accessibilityLabel="Avatar" style={[avatarSize, styles.avatar, styles.image]} />
|
||||
<Image
|
||||
source={{ uri: avatarUrl }}
|
||||
accessibilityLabel="Avatar"
|
||||
style={[avatarSize, styles.avatar, styles.image]}
|
||||
/>
|
||||
) : (
|
||||
<View style={[avatarSize, styles.avatar, styles.noImage]} />
|
||||
)}
|
||||
@@ -106,7 +101,7 @@ const styles = StyleSheet.create({
|
||||
avatar: {
|
||||
borderRadius: 5,
|
||||
overflow: 'hidden',
|
||||
maxWidth: '100%'
|
||||
maxWidth: '100%',
|
||||
},
|
||||
image: {
|
||||
objectFit: 'cover',
|
||||
@@ -114,7 +109,6 @@ const styles = StyleSheet.create({
|
||||
},
|
||||
noImage: {
|
||||
backgroundColor: '#333',
|
||||
border: '1px solid rgb(200, 200, 200)',
|
||||
borderRadius: 5
|
||||
borderRadius: 5,
|
||||
},
|
||||
})
|
||||
})
|
||||
@@ -1,10 +1,10 @@
|
||||
import { createClient } from '@supabase/supabase-js'
|
||||
import AsyncStorage from '@react-native-async-storage/async-storage'
|
||||
|
||||
const supabaseUrl = 'https://project.supabase.co'
|
||||
const supabasePublishableKey = 'your-publishable-key'
|
||||
const supabaseUrl = process.env.EXPO_PUBLIC_SUPABASE_URL!
|
||||
const supabaseKey = process.env.EXPO_PUBLIC_SUPABASE_KEY!
|
||||
|
||||
export const supabase = createClient(supabaseUrl, supabasePublishableKey, {
|
||||
export const supabase = createClient(supabaseUrl, supabaseKey, {
|
||||
auth: {
|
||||
storage: AsyncStorage as any,
|
||||
autoRefreshToken: true,
|
||||
|
||||
+3383
-10945
File diff suppressed because it is too large.
Load diff
@@ -10,25 +10,27 @@
|
||||
"format": "prettier --cache --write \"**/*.{js,json,md,ts,tsx,jsx,css}\""
|
||||
},
|
||||
"dependencies": {
|
||||
"@react-native-async-storage/async-storage": "1.23.1",
|
||||
"@rneui/themed": "^4.0.0-rc.8",
|
||||
"@supabase/supabase-js": "^2.0.4",
|
||||
"expo": "~52.0.0",
|
||||
"expo-splash-screen": "~0.27.4",
|
||||
"expo-status-bar": "~1.12.1",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"react-native": "0.74.1",
|
||||
"react-native-document-picker": "^8.1.1",
|
||||
"react-native-url-polyfill": "^1.3.0",
|
||||
"react-native-web": "~0.19.10"
|
||||
"@react-native-async-storage/async-storage": "2.2.0",
|
||||
"@rneui/base": "^5.0.0",
|
||||
"@rneui/themed": "^5.0.0",
|
||||
"@supabase/supabase-js": "^2.99.0",
|
||||
"expo": "~55.0.5",
|
||||
"expo-image-picker": "^55.0.11",
|
||||
"expo-splash-screen": "~55.0.10",
|
||||
"expo-status-bar": "~55.0.4",
|
||||
"react": "19.2.0",
|
||||
"react-dom": "19.2.0",
|
||||
"react-native": "0.83.2",
|
||||
"react-native-safe-area-context": "~5.6.2",
|
||||
"react-native-url-polyfill": "^3.0.0",
|
||||
"react-native-web": "~0.21.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.24.0",
|
||||
"@types/react": "~18.2.79",
|
||||
"@types/react-native": "^0.69.5",
|
||||
"prettier": "^2.7.1",
|
||||
"typescript": "~5.3.3"
|
||||
"@babel/core": "^7.29.0",
|
||||
"@types/react": "~19.2.14",
|
||||
"@types/react-native": "^0.73.0",
|
||||
"prettier": "^3.8.1",
|
||||
"typescript": "~5.9.3"
|
||||
},
|
||||
"private": true
|
||||
}
|
||||
Reference in new issue
Block a user