Update expo quickstart guide

This commit is contained in:
Andrew Smith committed 2022-08-27 22:11:02 +00:00
1 parent 1df5be4f4e
commit f685c9c258
1 file changed
+189 -116
+189 -116
View File
@@ -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