mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 17:35:10 +03:00
Update expo quickstart guide
This commit is contained in:
1 parent
1df5be4f4e
commit
f685c9c258
1 file changed
+189
-116
@@ -8,6 +8,12 @@ sidebar_label: Expo
|
||||
import Tabs from '@theme/Tabs'
|
||||
import TabItem from '@theme/TabItem'
|
||||
|
||||
:::note
|
||||
|
||||
This guide uses supabase-js v2.0.0 Release Candidate (RC).
|
||||
|
||||
:::
|
||||
|
||||
## Intro
|
||||
|
||||
This example provides the steps to build a simple user management app (from scratch!) using Supabase and React Native. It includes:
|
||||
@@ -138,23 +144,19 @@ Let's start 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
|
||||
an app called `supabaseReactNative`:
|
||||
an app called `expo-user-management`:
|
||||
|
||||
```bash
|
||||
#select the "Blank (TypeScript)" template when prompted
|
||||
expo init supabaseReactNative
|
||||
npx create-expo-app -t expo-template-blank-typescript expo-user-management
|
||||
|
||||
cd supabaseReactNative
|
||||
cd expo-user-management
|
||||
```
|
||||
|
||||
Then let's install the additional dependencies: [supabase-js](https://github.com/supabase/supabase-js)
|
||||
|
||||
```bash
|
||||
yarn add @supabase/supabase-js
|
||||
yarn add react-native-elements
|
||||
yarn add react-native-safe-area-context
|
||||
yarn add @react-native-async-storage/async-storage
|
||||
yarn add react-native-url-polyfill
|
||||
npm install @supabase/supabase-js@rc
|
||||
npm install react-native-elements @react-native-async-storage/async-storage react-native-url-polyfill
|
||||
```
|
||||
|
||||
Now let's create a helper file to initialize the Supabase client.
|
||||
@@ -170,11 +172,13 @@ const supabaseUrl = YOUR_REACT_NATIVE_SUPABASE_URL
|
||||
const supabaseAnonKey = YOUR_REACT_NATIVE_SUPABASE_ANON_KEY
|
||||
|
||||
export const supabase = createClient(supabaseUrl, supabaseAnonKey, {
|
||||
localStorage: AsyncStorage as any,
|
||||
autoRefreshToken: true,
|
||||
persistSession: true,
|
||||
detectSessionInUrl: false,
|
||||
});
|
||||
auth: {
|
||||
storage: AsyncStorage as any,
|
||||
autoRefreshToken: true,
|
||||
persistSession: true,
|
||||
detectSessionInUrl: false,
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
### Set up a Login component
|
||||
@@ -195,7 +199,7 @@ export default function Auth() {
|
||||
|
||||
async function signInWithEmail() {
|
||||
setLoading(true)
|
||||
const { user, error } = await supabase.auth.signIn({
|
||||
const { error } = await supabase.auth.signInWithPassword({
|
||||
email: email,
|
||||
password: password,
|
||||
})
|
||||
@@ -206,7 +210,7 @@ export default function Auth() {
|
||||
|
||||
async function signUpWithEmail() {
|
||||
setLoading(true)
|
||||
const { user, error } = await supabase.auth.signUp({
|
||||
const { error } = await supabase.auth.signUp({
|
||||
email: email,
|
||||
password: password,
|
||||
})
|
||||
@@ -239,18 +243,10 @@ export default function Auth() {
|
||||
/>
|
||||
</View>
|
||||
<View style={[styles.verticallySpaced, styles.mt20]}>
|
||||
<Button
|
||||
title="Sign in"
|
||||
disabled={loading}
|
||||
onPress={() => signInWithEmail()}
|
||||
/>
|
||||
<Button title="Sign in" disabled={loading} onPress={() => signInWithEmail()} />
|
||||
</View>
|
||||
<View style={styles.verticallySpaced}>
|
||||
<Button
|
||||
title="Sign up"
|
||||
disabled={loading}
|
||||
onPress={() => signUpWithEmail()}
|
||||
/>
|
||||
<Button title="Sign up" disabled={loading} onPress={() => signUpWithEmail()} />
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
@@ -279,46 +275,47 @@ After a user is signed in we can allow them to edit their profile details and ma
|
||||
Let's create a new component for that called `Account.tsx`.
|
||||
|
||||
```tsx title="components/Account.tsx"
|
||||
import { useState, useEffect } from "react";
|
||||
import { supabase } from "../lib/supabase";
|
||||
import { StyleSheet, View, Alert } from "react-native";
|
||||
import { Button, Input } from "react-native-elements";
|
||||
import { ApiError, Session } from "@supabase/supabase-js";
|
||||
import { useState, useEffect } from 'react'
|
||||
import { supabase } from '../lib/supabase'
|
||||
import { StyleSheet, View, Alert } from 'react-native'
|
||||
import { Button, Input } from 'react-native-elements'
|
||||
import { Session } from '@supabase/supabase-js'
|
||||
|
||||
export default function Account({ session }: { session: Session }) {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [username, setUsername] = useState("");
|
||||
const [website, setWebsite] = useState("");
|
||||
const [avatar_url, setAvatarUrl] = useState("");
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [username, setUsername] = useState('')
|
||||
const [website, setWebsite] = useState('')
|
||||
const [avatarUrl, setAvatarUrl] = useState('')
|
||||
|
||||
useEffect(() => {
|
||||
if (session) getProfile();
|
||||
}, [session]);
|
||||
if (session) getProfile()
|
||||
}, [session])
|
||||
|
||||
async function getProfile() {
|
||||
try {
|
||||
setLoading(true);
|
||||
const user = supabase.auth.user();
|
||||
if (!user) throw new Error("No user on the session!");
|
||||
setLoading(true)
|
||||
if (!session?.user) throw new Error('No user on the session!')
|
||||
|
||||
let { data, error, status } = await supabase
|
||||
.from("profiles")
|
||||
.from('profiles')
|
||||
.select(`username, website, avatar_url`)
|
||||
.eq("id", user.id)
|
||||
.single();
|
||||
.eq('id', session?.user.id)
|
||||
.single()
|
||||
if (error && status !== 406) {
|
||||
throw error;
|
||||
throw error
|
||||
}
|
||||
|
||||
if (data) {
|
||||
setUsername(data.username);
|
||||
setWebsite(data.website);
|
||||
setAvatarUrl(data.avatar_url);
|
||||
setUsername(data.username)
|
||||
setWebsite(data.website)
|
||||
setAvatarUrl(data.avatar_url)
|
||||
}
|
||||
} catch (error) {
|
||||
Alert.alert((error as ApiError).message);
|
||||
if (error instanceof Error) {
|
||||
Alert.alert(error.message)
|
||||
}
|
||||
} finally {
|
||||
setLoading(false);
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -327,34 +324,33 @@ export default function Account({ session }: { session: Session }) {
|
||||
website,
|
||||
avatar_url,
|
||||
}: {
|
||||
username: string;
|
||||
website: string;
|
||||
avatar_url: string;
|
||||
username: string
|
||||
website: string
|
||||
avatar_url: string
|
||||
}) {
|
||||
try {
|
||||
setLoading(true);
|
||||
const user = supabase.auth.user();
|
||||
if (!user) throw new Error("No user on the session!");
|
||||
setLoading(true)
|
||||
if (!session?.user) throw new Error('No user on the session!')
|
||||
|
||||
const updates = {
|
||||
id: user.id,
|
||||
id: session?.user.id,
|
||||
username,
|
||||
website,
|
||||
avatar_url,
|
||||
updated_at: new Date(),
|
||||
};
|
||||
}
|
||||
|
||||
let { error } = await supabase
|
||||
.from("profiles")
|
||||
.upsert(updates, { returning: "minimal" });
|
||||
let { error } = await supabase.from('profiles').upsert(updates)
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
throw error
|
||||
}
|
||||
} catch (error) {
|
||||
Alert.alert((error as ApiError).message);
|
||||
if (error instanceof Error) {
|
||||
Alert.alert(error.message)
|
||||
}
|
||||
} finally {
|
||||
setLoading(false);
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -364,24 +360,16 @@ export default function Account({ session }: { session: Session }) {
|
||||
<Input label="Email" value={session?.user?.email} disabled />
|
||||
</View>
|
||||
<View style={styles.verticallySpaced}>
|
||||
<Input
|
||||
label="Username"
|
||||
value={username || ""}
|
||||
onChangeText={(text) => setUsername(text)}
|
||||
/>
|
||||
<Input label="Username" value={username || ''} onChangeText={(text) => setUsername(text)} />
|
||||
</View>
|
||||
<View style={styles.verticallySpaced}>
|
||||
<Input
|
||||
label="Website"
|
||||
value={website || ""}
|
||||
onChangeText={(text) => setWebsite(text)}
|
||||
/>
|
||||
<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 })}
|
||||
title={loading ? 'Loading ...' : 'Update'}
|
||||
onPress={() => updateProfile({ username, website, avatar_url: avatarUrl })}
|
||||
disabled={loading}
|
||||
/>
|
||||
</View>
|
||||
@@ -390,7 +378,7 @@ export default function Account({ session }: { session: Session }) {
|
||||
<Button title="Sign Out" onPress={() => supabase.auth.signOut()} />
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
@@ -401,12 +389,12 @@ const styles = StyleSheet.create({
|
||||
verticallySpaced: {
|
||||
paddingTop: 4,
|
||||
paddingBottom: 4,
|
||||
alignSelf: "stretch",
|
||||
alignSelf: 'stretch',
|
||||
},
|
||||
mt20: {
|
||||
marginTop: 20,
|
||||
},
|
||||
});
|
||||
})
|
||||
```
|
||||
|
||||
### Launch!
|
||||
@@ -426,7 +414,9 @@ export default function App() {
|
||||
const [session, setSession] = useState<Session | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
setSession(supabase.auth.session())
|
||||
supabase.auth.getSession().then(({ data: { session } }) => {
|
||||
setSession(session)
|
||||
})
|
||||
|
||||
supabase.auth.onAuthStateChange((_event, session) => {
|
||||
setSession(session)
|
||||
@@ -435,11 +425,7 @@ export default function App() {
|
||||
|
||||
return (
|
||||
<View>
|
||||
{session && session.user ? (
|
||||
<Account key={session.user.id} session={session} />
|
||||
) : (
|
||||
<Auth />
|
||||
)}
|
||||
{session && session.user ? <Account key={session.user.id} session={session} /> : <Auth />}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
@@ -448,74 +434,115 @@ export default function App() {
|
||||
Once that's done, run this in a terminal window:
|
||||
|
||||
```bash
|
||||
yarn start
|
||||
npm start
|
||||
```
|
||||
|
||||
And then open the browser to [localhost:3000](http://localhost:3000) and you should see the completed app.
|
||||
And then press the appropriate key for the environment you want to test the app in and you should see the completed app.
|
||||
|
||||
## Bonus: Profile photos
|
||||
|
||||
Every Supabase project is configured with [Storage](/docs/guides/storage) for managing large files like
|
||||
photos and videos.
|
||||
|
||||
### Additional dependency installation
|
||||
|
||||
You will need a file picker that works on the environment you will build the project for, we will use react-native-document-picker in this example.
|
||||
|
||||
```bash
|
||||
expo install react-native-document-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:
|
||||
|
||||
```jsx title="components/Avatar.jsx"
|
||||
import { useEffect, useState } from 'react'
|
||||
```tsx title="components/Avatar.tsx"
|
||||
import { useState, useEffect } from 'react'
|
||||
import { supabase } from '../lib/supabase'
|
||||
import { Alert, StyleSheet, View, Image } from 'react-native'
|
||||
import { Button, Input } from 'react-native-elements'
|
||||
import { StyleSheet, View, Alert, Image, Button } from 'react-native'
|
||||
import DocumentPicker, { isCancel, isInProgress, types } from 'react-native-document-picker'
|
||||
|
||||
export default function Avatar({ url, size, onUpload }) {
|
||||
const [avatarUrl, setAvatarUrl] = useState(null)
|
||||
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) {
|
||||
async function downloadImage(path: string) {
|
||||
try {
|
||||
const { data, error } = await supabase.storage
|
||||
.from('avatars')
|
||||
.download(path)
|
||||
|
||||
if (error) {
|
||||
throw error
|
||||
}
|
||||
const url = URL.createObjectURL(data)
|
||||
setAvatarUrl(url)
|
||||
|
||||
const fr = new FileReader()
|
||||
fr.readAsDataURL(data)
|
||||
fr.onload = () => {
|
||||
setAvatarUrl(fr.result as string)
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('Error downloading image: ', error.message)
|
||||
if (error instanceof Error) {
|
||||
console.log('Error downloading image: ', error.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function uploadAvatar(event) {
|
||||
async function uploadAvatar() {
|
||||
try {
|
||||
setUploading(true)
|
||||
|
||||
if (!event.target.files || event.target.files.length === 0) {
|
||||
throw new Error('You must select an image to upload.')
|
||||
const file = await DocumentPicker.pickSingle({
|
||||
presentationStyle: 'fullScreen',
|
||||
copyTo: 'cachesDirectory',
|
||||
type: types.images,
|
||||
mode: 'open'
|
||||
})
|
||||
|
||||
const photo = {
|
||||
uri: file.fileCopyUri,
|
||||
type: file.type,
|
||||
name: file.name
|
||||
}
|
||||
|
||||
const file = event.target.files[0]
|
||||
const formData = new FormData()
|
||||
formData.append("file", photo)
|
||||
|
||||
const fileExt = file.name.split('.').pop()
|
||||
const fileName = `${Math.random()}.${fileExt}`
|
||||
const filePath = `${fileName}`
|
||||
const filePath = `${Math.random()}.${fileExt}`
|
||||
|
||||
let { error: uploadError } = await supabase.storage
|
||||
let { error } = await supabase.storage
|
||||
.from('avatars')
|
||||
.upload(filePath, file)
|
||||
.upload(filePath, formData)
|
||||
|
||||
if (uploadError) {
|
||||
throw uploadError
|
||||
if (error) {
|
||||
throw error
|
||||
}
|
||||
|
||||
onUpload(filePath)
|
||||
} catch (error) {
|
||||
alert(error.message)
|
||||
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) {
|
||||
Alert.alert(error.message)
|
||||
} else {
|
||||
throw error
|
||||
}
|
||||
} finally {
|
||||
setUploading(false)
|
||||
}
|
||||
@@ -524,22 +551,68 @@ export default function Avatar({ url, size, onUpload }) {
|
||||
return (
|
||||
<View>
|
||||
{avatarUrl ? (
|
||||
<Image src={avatarUrl} alt="Avatar" className="avatar image" />
|
||||
<Image source={{ uri: avatarUrl }} accessibilityLabel="Avatar" style={[avatarSize, styles.avatar, styles.image]} />
|
||||
) : (
|
||||
<View className="avatar no-image" />
|
||||
<View style={[avatarSize, styles.avatar, styles.noImage]} />
|
||||
)}
|
||||
<View>
|
||||
<Input
|
||||
type="file"
|
||||
id="single"
|
||||
accept="image/*"
|
||||
onChange={uploadAvatar}
|
||||
disabled={uploading}
|
||||
/>
|
||||
<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',
|
||||
border: '1px solid rgb(200, 200, 200)',
|
||||
borderRadius: 5
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
### Add the new widget
|
||||
|
||||
And then we can add the widget to the Account page:
|
||||
|
||||
```tsx title="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>
|
||||
)
|
||||
}
|
||||
// ...
|
||||
```
|
||||
|
||||
Now you will need to run the prebuild command to get the application working on your chosen platform.
|
||||
|
||||
```bash
|
||||
expo prebuild
|
||||
```
|
||||
|
||||
## Next steps
|
||||
|
||||
Reference in new issue
Block a user