mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 09:25:06 +03:00
docs: react native expo user management update (#42300)
## I have read the [CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md) file. YES ## What kind of change does this PR introduce? Docs - [React Native Expo user management guide](https://supabase.com/docs/guides/getting-started/tutorials/with-expo-react-native) ## What is the new behavior? Guide has been updated to be in line with #42269 - Removing `@rneui/themed` package and using native components. - Update guide screenshot. - Main stylesheet for the example. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit ## Documentation - Updated Expo React Native tutorial with modernized component approach using React Native primitives and centralized styling patterns. - Simplified codebase examples with consistent formatting and improved maintainability. - Core functionality preserved with updated implementation patterns. <sub>✏️ Tip: You can customize this high-level summary in your review settings.</sub> <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Chris Chinchilla <chris.ward@supabase.io> Co-authored-by: Chris Chinchilla <chris@chrischinchilla.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
8 files changed
+180
-119
No files matched your search
@@ -6,7 +6,7 @@ tocVideo: 'AE7dKIKMJy4'
|
||||
|
||||
<$Partial path="quickstart_intro.mdx" />
|
||||
|
||||

|
||||

|
||||
|
||||
<Admonition type="note">
|
||||
|
||||
@@ -37,7 +37,7 @@ cd expo-user-management
|
||||
Then install the additional dependencies:
|
||||
|
||||
```bash
|
||||
npx expo install @supabase/supabase-js @rneui/themed expo-sqlite
|
||||
npx expo install @supabase/supabase-js @react-native-async-storage/async-storage
|
||||
```
|
||||
|
||||
Now create a helper file to initialize the Supabase client using the API URL and the key that you copied [earlier](#get-api-details).
|
||||
@@ -75,7 +75,7 @@ These variables are safe to expose in your Expo app since Supabase has
|
||||
|
||||
```bash
|
||||
npm install @supabase/supabase-js
|
||||
npm install @rneui/themed @react-native-async-storage/async-storage
|
||||
npm install @react-native-async-storage/async-storage
|
||||
npm install aes-js react-native-get-random-values
|
||||
npm install --save-dev @types/aes-js
|
||||
npx expo install expo-secure-store
|
||||
@@ -156,6 +156,16 @@ These variables are safe to expose in your Expo app since Supabase has
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
|
||||
### App styling
|
||||
|
||||
You can use the following `StyleSheet` component in `styles/styles.ts` to add style to the app:
|
||||
|
||||
<$CodeSample
|
||||
path="/user-management/expo-user-management/styles/styles.ts"
|
||||
lines={[[1, -1]]}
|
||||
meta="name=styles/styles.ts"
|
||||
/>
|
||||
|
||||
### Set up a login component
|
||||
|
||||
Set up a React Native component to manage logins and sign ups.
|
||||
@@ -177,7 +187,7 @@ While testing, you can disable email confirmation in your [project's email auth
|
||||
|
||||
### Account page
|
||||
|
||||
After a user signs in, you can let them to edit their profile details and manage their account.
|
||||
After a user signs in, let them edit their profile details and manage their account.
|
||||
|
||||
Create a new component for that called `Account.tsx`.
|
||||
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 69 KiB |
@@ -1,14 +0,0 @@
|
||||
{
|
||||
"arrowParens": "always",
|
||||
"bracketSpacing": true,
|
||||
"bracketSameLine": false,
|
||||
"endOfLine": "lf",
|
||||
"jsxSingleQuote": false,
|
||||
"htmlWhitespaceSensitivity": "ignore",
|
||||
"printWidth": 100,
|
||||
"semi": false,
|
||||
"singleQuote": true,
|
||||
"tabWidth": 2,
|
||||
"trailingComma": "all",
|
||||
"useTabs": false
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
import 'react-native-url-polyfill/auto'
|
||||
import { useState, useEffect } from 'react'
|
||||
import { supabase } from './lib/supabase'
|
||||
import Auth from './components/Auth'
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { supabase } from '../lib/supabase'
|
||||
import { StyleSheet, View, Alert } from 'react-native'
|
||||
import { Button, Input } from '@rneui/themed'
|
||||
import { View, Alert, TextInput, Text, TouchableOpacity } from 'react-native'
|
||||
import Avatar from './Avatar'
|
||||
import { appStyles } from '../styles/styles'
|
||||
|
||||
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('')
|
||||
const styles = appStyles
|
||||
|
||||
useEffect(() => {
|
||||
if (userId) getProfile()
|
||||
@@ -66,17 +67,15 @@ export default function Account({ userId, email }: { userId: string; email?: str
|
||||
if (error) {
|
||||
throw error
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
Alert.alert(error.message)
|
||||
}
|
||||
} catch (error: any) {
|
||||
Alert.alert(error.message)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<View>
|
||||
<View style={styles.container}>
|
||||
<View>
|
||||
<Avatar
|
||||
size={200}
|
||||
@@ -88,41 +87,46 @@ export default function Account({ userId, email }: { userId: string; email?: str
|
||||
/>
|
||||
</View>
|
||||
<View style={[styles.verticallySpaced, styles.mt20]}>
|
||||
<Input label="Email" value={email} disabled />
|
||||
<Text style={styles.label}>Email</Text>
|
||||
<TextInput
|
||||
value={email ?? ''}
|
||||
editable={false}
|
||||
selectTextOnFocus={false}
|
||||
style={[styles.input, styles.inputDisabled]}
|
||||
/>
|
||||
</View>
|
||||
<View style={styles.verticallySpaced}>
|
||||
<Input label="Username" value={username || ''} onChangeText={(text) => setUsername(text)} />
|
||||
<Text style={styles.label}>Username</Text>
|
||||
<TextInput
|
||||
value={username || ''}
|
||||
onChangeText={(text) => setUsername(text)}
|
||||
style={styles.input}
|
||||
/>
|
||||
</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}
|
||||
<Text style={styles.label}>Website</Text>
|
||||
<TextInput
|
||||
value={website || ''}
|
||||
onChangeText={(text) => setWebsite(text)}
|
||||
style={styles.input}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View style={[styles.verticallySpaced, styles.mt20]}>
|
||||
<TouchableOpacity
|
||||
style={[styles.button, loading && styles.buttonDisabled]}
|
||||
onPress={() => updateProfile({ username, website, avatar_url: avatarUrl })}
|
||||
disabled={loading}
|
||||
>
|
||||
<Text style={styles.buttonText}>{loading ? 'Loading ...' : 'Update'}</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
<View style={styles.verticallySpaced}>
|
||||
<Button title="Sign Out" onPress={() => supabase.auth.signOut()} />
|
||||
<TouchableOpacity style={styles.button} onPress={() => supabase.auth.signOut()}>
|
||||
<Text style={styles.buttonText}>Sign Out</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
marginTop: 40,
|
||||
padding: 12,
|
||||
},
|
||||
verticallySpaced: {
|
||||
paddingTop: 4,
|
||||
paddingBottom: 4,
|
||||
alignSelf: 'stretch',
|
||||
},
|
||||
mt20: {
|
||||
marginTop: 20,
|
||||
},
|
||||
})
|
||||
@@ -1,16 +1,16 @@
|
||||
import React, { useState } from 'react'
|
||||
import { Alert, StyleSheet, View } from 'react-native'
|
||||
import { Alert, Text, TextInput, TouchableOpacity, View } from 'react-native'
|
||||
import { supabase } from '../lib/supabase'
|
||||
import { Button, Input } from '@rneui/themed'
|
||||
import { appStyles } from '../styles/styles'
|
||||
|
||||
export default function Auth() {
|
||||
const [email, setEmail] = useState('')
|
||||
const [password, setPassword] = useState('')
|
||||
const [loading, setLoading] = useState(false)
|
||||
const styles = appStyles
|
||||
|
||||
async function signInWithEmail() {
|
||||
setLoading(true)
|
||||
console.log({ email, password })
|
||||
const { error } = await supabase.auth.signInWithPassword({
|
||||
email: email,
|
||||
password: password,
|
||||
@@ -32,49 +32,46 @@ export default function Auth() {
|
||||
}
|
||||
|
||||
return (
|
||||
<View>
|
||||
<View style={styles.container}>
|
||||
<View style={[styles.verticallySpaced, styles.mt20]}>
|
||||
<Input
|
||||
label="Email"
|
||||
leftIcon={{ type: 'font-awesome', name: 'envelope' }}
|
||||
<Text style={styles.label}>Email</Text>
|
||||
<TextInput
|
||||
onChangeText={(text) => setEmail(text)}
|
||||
value={email}
|
||||
placeholder="email@address.com"
|
||||
autoCapitalize={'none'}
|
||||
autoCapitalize="none"
|
||||
style={styles.input}
|
||||
/>
|
||||
</View>
|
||||
<View style={styles.verticallySpaced}>
|
||||
<Input
|
||||
label="Password"
|
||||
leftIcon={{ type: 'font-awesome', name: 'lock' }}
|
||||
<Text style={styles.label}>Password</Text>
|
||||
<TextInput
|
||||
onChangeText={(text) => setPassword(text)}
|
||||
value={password}
|
||||
secureTextEntry={true}
|
||||
placeholder="Password"
|
||||
autoCapitalize={'none'}
|
||||
autoCapitalize="none"
|
||||
style={styles.input}
|
||||
/>
|
||||
</View>
|
||||
<View style={[styles.verticallySpaced, styles.mt20]}>
|
||||
<Button title="Sign in" disabled={loading} onPress={() => signInWithEmail()} />
|
||||
<TouchableOpacity
|
||||
style={[styles.button, loading && styles.buttonDisabled]}
|
||||
onPress={() => signInWithEmail()}
|
||||
disabled={loading}
|
||||
>
|
||||
<Text style={styles.buttonText}>Sign in</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
<View style={styles.verticallySpaced}>
|
||||
<Button title="Sign up" disabled={loading} onPress={() => signUpWithEmail()} />
|
||||
<TouchableOpacity
|
||||
style={[styles.button, loading && styles.buttonDisabled]}
|
||||
onPress={() => signUpWithEmail()}
|
||||
disabled={loading}
|
||||
>
|
||||
<Text style={styles.buttonText}>Sign up</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
marginTop: 40,
|
||||
padding: 12,
|
||||
},
|
||||
verticallySpaced: {
|
||||
paddingTop: 4,
|
||||
paddingBottom: 4,
|
||||
alignSelf: 'stretch',
|
||||
},
|
||||
mt20: {
|
||||
marginTop: 20,
|
||||
},
|
||||
})
|
||||
@@ -1,7 +1,8 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { supabase } from '../lib/supabase'
|
||||
import { StyleSheet, View, Alert, Image, Button } from 'react-native'
|
||||
import { View, Alert, Image, Text, TouchableOpacity } from 'react-native'
|
||||
import * as ImagePicker from 'expo-image-picker'
|
||||
import { appStyles } from '../styles/styles'
|
||||
|
||||
interface Props {
|
||||
size: number
|
||||
@@ -13,6 +14,7 @@ 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 }
|
||||
const styles = appStyles
|
||||
|
||||
useEffect(() => {
|
||||
if (url) downloadImage(url)
|
||||
@@ -31,10 +33,8 @@ export default function Avatar({ url, size = 150, onUpload }: Props) {
|
||||
fr.onload = () => {
|
||||
setAvatarUrl(fr.result as string)
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
console.log('Error downloading image: ', error.message)
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.log('Error downloading image: ', error.message)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,36 +43,45 @@ export default function Avatar({ url, size = 150, onUpload }: Props) {
|
||||
setUploading(true)
|
||||
|
||||
const result = await ImagePicker.launchImageLibraryAsync({
|
||||
mediaTypes: ['images'],
|
||||
allowsEditing: true,
|
||||
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!')
|
||||
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 filePath = `${Math.random()}.${fileExt}`
|
||||
|
||||
const { error } = await supabase.storage.from('avatars').upload(filePath, arraybuffer, {
|
||||
contentType: image.mimeType ?? 'image/jpeg',
|
||||
})
|
||||
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 (error) {
|
||||
throw error
|
||||
if (uploadError) {
|
||||
throw uploadError
|
||||
}
|
||||
|
||||
onUpload(filePath)
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
onUpload(data.path)
|
||||
} catch (error: any) {
|
||||
if (error) {
|
||||
Alert.alert(error.message)
|
||||
} else {
|
||||
throw error
|
||||
}
|
||||
} finally {
|
||||
setUploading(false)
|
||||
@@ -80,7 +89,7 @@ export default function Avatar({ url, size = 150, onUpload }: Props) {
|
||||
}
|
||||
|
||||
return (
|
||||
<View>
|
||||
<View style={styles.avatarContainer}>
|
||||
{avatarUrl ? (
|
||||
<Image
|
||||
source={{ uri: avatarUrl }}
|
||||
@@ -91,28 +100,14 @@ export default function Avatar({ url, size = 150, onUpload }: Props) {
|
||||
<View style={[avatarSize, styles.avatar, styles.noImage]} />
|
||||
)}
|
||||
<View>
|
||||
<Button
|
||||
title={uploading ? 'Uploading ...' : 'Upload'}
|
||||
<TouchableOpacity
|
||||
style={[styles.button, uploading && styles.buttonDisabled]}
|
||||
onPress={uploadAvatar}
|
||||
disabled={uploading}
|
||||
/>
|
||||
>
|
||||
<Text style={styles.buttonText}>{uploading ? 'Uploading ...' : 'Upload'}</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
avatar: {
|
||||
borderRadius: 5,
|
||||
overflow: 'hidden',
|
||||
maxWidth: '100%',
|
||||
},
|
||||
image: {
|
||||
objectFit: 'cover',
|
||||
paddingTop: 0,
|
||||
},
|
||||
noImage: {
|
||||
backgroundColor: '#333',
|
||||
borderRadius: 5,
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,70 @@
|
||||
import { StyleSheet } from 'react-native'
|
||||
|
||||
export const appStyles = StyleSheet.create({
|
||||
container: {
|
||||
marginTop: 40,
|
||||
padding: 12,
|
||||
},
|
||||
verticallySpaced: {
|
||||
paddingTop: 4,
|
||||
paddingBottom: 4,
|
||||
alignSelf: 'stretch',
|
||||
},
|
||||
mt20: {
|
||||
marginTop: 20,
|
||||
},
|
||||
label: {
|
||||
fontSize: 16,
|
||||
fontWeight: '600',
|
||||
color: '#86939e',
|
||||
marginBottom: 6,
|
||||
},
|
||||
input: {
|
||||
borderWidth: 1,
|
||||
borderColor: '#86939e',
|
||||
borderRadius: 4,
|
||||
padding: 12,
|
||||
fontSize: 16,
|
||||
},
|
||||
inputDisabled: {
|
||||
backgroundColor: '#f2f2f2',
|
||||
borderColor: '#d1d1d1',
|
||||
color: '#9e9e9e',
|
||||
},
|
||||
button: {
|
||||
backgroundColor: '#2089dc',
|
||||
borderRadius: 4,
|
||||
padding: 12,
|
||||
alignItems: 'center',
|
||||
},
|
||||
buttonDisabled: {
|
||||
opacity: 0.5,
|
||||
},
|
||||
buttonText: {
|
||||
color: '#fff',
|
||||
fontSize: 16,
|
||||
fontWeight: '600',
|
||||
},
|
||||
avatarContainer: {
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
marginTop: 20,
|
||||
},
|
||||
avatar: {
|
||||
borderRadius: 5,
|
||||
overflow: 'hidden',
|
||||
maxWidth: '100%',
|
||||
marginBottom: 20,
|
||||
},
|
||||
image: {
|
||||
objectFit: 'cover',
|
||||
paddingTop: 0,
|
||||
},
|
||||
noImage: {
|
||||
backgroundColor: '#333',
|
||||
borderWidth: 1,
|
||||
borderStyle: 'solid',
|
||||
borderColor: 'rgb(200, 200, 200)',
|
||||
borderRadius: 5,
|
||||
},
|
||||
})
|
||||
Reference in new issue
Block a user