diff --git a/apps/reference/docs/guides/with-expo.mdx b/apps/reference/docs/guides/with-expo.mdx
index e10b5a6c564..6419ac9d0cc 100644
--- a/apps/reference/docs/guides/with-expo.mdx
+++ b/apps/reference/docs/guides/with-expo.mdx
@@ -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() {
/>
-
- signUpWithEmail()}
- />
+ signUpWithEmail()} />
)
@@ -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 }) {
- setUsername(text)}
- />
+ setUsername(text)} />
- setWebsite(text)}
- />
+ setWebsite(text)} />
updateProfile({ username, website, avatar_url })}
+ title={loading ? 'Loading ...' : 'Update'}
+ onPress={() => updateProfile({ username, website, avatar_url: avatarUrl })}
disabled={loading}
/>
@@ -390,7 +378,7 @@ export default function Account({ session }: { session: Session }) {
supabase.auth.signOut()} />
- );
+ )
}
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(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 (
- {session && session.user ? (
-
- ) : (
-
- )}
+ {session && session.user ? : }
)
}
@@ -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(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 (
{avatarUrl ? (
-
+
) : (
-
+
)}
-
+
)
}
+
+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 (
+
+ {/* Add to the body */}
+
+ {
+ setAvatarUrl(url)
+ updateProfile({ username, website, avatar_url: url })
+ }}
+ />
+
+ {/* ... */}
+
+ )
+}
+// ...
+```
+
+Now you will need to run the prebuild command to get the application working on your chosen platform.
+
+```bash
+expo prebuild
```
## Next steps