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:
authored and GitHub committed 2026-04-23 12:09:10 +01:00
1 parent e51df9be2a
commit b99a9ea416
8 files changed
+180 -119

No files matched your search

@@ -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,
},
})