mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 09:25:06 +03:00
Merge pull request #315 from supabase/react-native-example
React native example
This commit is contained in:
20 files changed
+8489
No files matched your search
@@ -76,6 +76,7 @@ typings/
|
||||
# dotenv environment variables file
|
||||
.env
|
||||
.env.test
|
||||
.env.local
|
||||
|
||||
# parcel-bundler cache (https://parceljs.org/)
|
||||
.cache
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
# Update these with your Supabase details from your project settings > API
|
||||
REACT_NATIVE_SUPABASE_URL=https://your-project.supabase.co
|
||||
REACT_NATIVE_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlhdCI6MTYwMzI2MTI0MCwiZXhwIjoxOTE4ODM3MjQwfQ.PjJY_7ZYozWqoMH4R0XqyBHYo8en5Mfd5iL8hZOh4Ic
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"12bb71342c6255bbf50437ec8f4441c083f47cdb74bd89160c15e4f43e52a1cb": true,
|
||||
"40b842e832070c58deac6aa9e08fa459302ee3f9da492c7e77d93d2fbf4a56fd": true
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
node_modules/**/*
|
||||
.expo/*
|
||||
npm-debug.*
|
||||
*.jks
|
||||
*.p8
|
||||
*.p12
|
||||
*.key
|
||||
*.mobileprovision
|
||||
*.orig.*
|
||||
web-build/
|
||||
|
||||
# macOS
|
||||
.DS_Store
|
||||
@@ -0,0 +1,51 @@
|
||||
import React from 'react'
|
||||
import { StatusBar } from 'expo-status-bar'
|
||||
import { StyleSheet, View, Platform } from 'react-native'
|
||||
import { ThemeProvider, colors, Text } from 'react-native-elements'
|
||||
import { Styles } from './lib/constants'
|
||||
import { UserContextProvider, useUser } from './components/UserContext'
|
||||
import List from './components/TodoList'
|
||||
import Auth from './components/Auth'
|
||||
|
||||
const theme = {
|
||||
colors: {
|
||||
...Platform.select({
|
||||
default: colors.platform.android,
|
||||
ios: colors.platform.ios,
|
||||
}),
|
||||
},
|
||||
}
|
||||
|
||||
const Container = () => {
|
||||
const { user } = useUser()
|
||||
|
||||
return user ? <List /> : <Auth />
|
||||
}
|
||||
|
||||
export default function App() {
|
||||
return (
|
||||
<UserContextProvider>
|
||||
<ThemeProvider theme={theme}>
|
||||
<View style={styles.container}>
|
||||
<View style={styles.verticallySpaced}>
|
||||
<Text h1>Todo List</Text>
|
||||
</View>
|
||||
<Container />
|
||||
<StatusBar style="auto" />
|
||||
</View>
|
||||
</ThemeProvider>
|
||||
</UserContextProvider>
|
||||
)
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
marginTop: 40,
|
||||
padding: Styles.spacing,
|
||||
},
|
||||
verticallySpaced: {
|
||||
paddingTop: 4,
|
||||
paddingBottom: 4,
|
||||
alignSelf: 'stretch',
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,88 @@
|
||||
# React Native Todo List example with Expo
|
||||
|
||||
## Requirements
|
||||
|
||||
- Install the [Expo CLI](https://docs.expo.io/get-started/installation/)
|
||||
|
||||
## Setup & run locally
|
||||
|
||||
### 1. Create new project
|
||||
|
||||
Sign up to Supabase - [https://app.supabase.io](https://app.supabase.io) and create a new project. Wait for your database to start.
|
||||
|
||||
### 2. Run "Todo List" Quickstart
|
||||
|
||||
Once your database has started, run the "Todo List" quickstart.
|
||||
|
||||
### 3. Get the URL and Key
|
||||
|
||||
Go to the Project Settings (the cog icon), open the API tab, and find your API URL and `anon` key, you'll need these in the next step.
|
||||
|
||||
The `anon` key is your client-side API key. It allows "anonymous access" to your database, until the user has logged in. Once they have logged in, the keys will switch to the user's own login token. This enables row level security for your data. Read more about this [below](#postgres-row-level-security).
|
||||
|
||||

|
||||
|
||||
**_NOTE_**: The `service_role` key has full access to your data, bypassing any security policies. These keys have to be kept secret and are meant to be used in server environments and never on a client or browser.
|
||||
|
||||
Set the required environment variables:
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
Set `REACT_NATIVE_SUPABASE_URL` and `REACT_NATIVE_SUPABASE_ANON_KEY`.
|
||||
|
||||
### 4. Install the dependencies & run the project:
|
||||
|
||||
Install the dependencies:
|
||||
|
||||
```bash
|
||||
npm i
|
||||
```
|
||||
|
||||
Run the project:
|
||||
|
||||
```bash
|
||||
npm start
|
||||
```
|
||||
|
||||
## Supabase details
|
||||
|
||||
### Postgres Row level security
|
||||
|
||||
This project uses very high-level Authorization using Postgres' Role Level Security.
|
||||
When you start a Postgres database on Supabase, we populate it with an `auth` schema, and some helper functions.
|
||||
When a user logs in, they are issued a JWT with the role `authenticated` and thier UUID.
|
||||
We can use these details to provide fine-grained control over what each user can and cannot do.
|
||||
|
||||
This is a trimmed-down schema, with the policies:
|
||||
|
||||
```sql
|
||||
create table todos (
|
||||
id bigint generated by default as identity primary key,
|
||||
user_id uuid references auth.users not null,
|
||||
task text check (char_length(task) > 3),
|
||||
is_complete boolean default false,
|
||||
inserted_at timestamp with time zone default timezone('utc'::text, now()) not null
|
||||
);
|
||||
|
||||
alter table todos enable row level security;
|
||||
|
||||
create policy "Individuals can create todos." on todos for
|
||||
insert with check (auth.uid() = user_id);
|
||||
|
||||
create policy "Individuals can view their own todos. " on todos for
|
||||
select using (auth.uid() = user_id);
|
||||
|
||||
create policy "Individuals can update their own todos." on todos for
|
||||
update using (auth.uid() = user_id);
|
||||
|
||||
create policy "Individuals can delete their own todos." on todos for
|
||||
delete using (auth.uid() = user_id);
|
||||
```
|
||||
|
||||
## Authors
|
||||
|
||||
- [Supabase](https://supabase.io)
|
||||
|
||||
Supabase is open source, we'd love for you to follow along and get involved at https://github.com/supabase/supabase
|
||||
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"expo": {
|
||||
"name": "supabase-expo",
|
||||
"slug": "supabase-expo",
|
||||
"version": "1.0.0",
|
||||
"orientation": "portrait",
|
||||
"icon": "./assets/icon.png",
|
||||
"splash": {
|
||||
"image": "./assets/splash.png",
|
||||
"resizeMode": "contain",
|
||||
"backgroundColor": "#ffffff"
|
||||
},
|
||||
"updates": {
|
||||
"fallbackToCacheTimeout": 0
|
||||
},
|
||||
"assetBundlePatterns": [
|
||||
"**/*"
|
||||
],
|
||||
"ios": {
|
||||
"supportsTablet": true
|
||||
},
|
||||
"android": {
|
||||
"adaptiveIcon": {
|
||||
"foregroundImage": "./assets/adaptive-icon.png",
|
||||
"backgroundColor": "#FFFFFF"
|
||||
}
|
||||
},
|
||||
"web": {
|
||||
"favicon": "./assets/favicon.png"
|
||||
}
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 17 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.4 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 22 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 47 KiB |
@@ -0,0 +1,7 @@
|
||||
module.exports = function(api) {
|
||||
api.cache(true);
|
||||
return {
|
||||
presets: ['babel-preset-expo'],
|
||||
plugins: ['inline-dotenv'],
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,77 @@
|
||||
import React, { useState } from 'react'
|
||||
import { Alert, StyleSheet, View } from 'react-native'
|
||||
import { Styles } from '../lib/constants'
|
||||
import { supabase } from '../lib/initSupabase'
|
||||
|
||||
import { Button, Input } from 'react-native-elements'
|
||||
|
||||
export default function Auth() {
|
||||
const [email, setEmail] = useState('')
|
||||
const [password, setPassword] = useState('')
|
||||
const [loading, setLoading] = useState('')
|
||||
|
||||
const handleLogin = async (type: string, email: string, password: string) => {
|
||||
setLoading(type)
|
||||
const { error, user } =
|
||||
type === 'LOGIN'
|
||||
? await supabase.auth.signIn({ email, password })
|
||||
: await supabase.auth.signUp({ email, password })
|
||||
if (!error && !user) Alert.alert('Check your email for the login link!')
|
||||
if (error) Alert.alert(error.message)
|
||||
setLoading('')
|
||||
}
|
||||
|
||||
return (
|
||||
<View>
|
||||
<View style={[styles.verticallySpaced, { marginTop: 20 }]}>
|
||||
<Input
|
||||
label="Email"
|
||||
leftIcon={{ type: 'font-awesome', name: 'envelope' }}
|
||||
onChangeText={(text) => setEmail(text)}
|
||||
value={email}
|
||||
placeholder="email@address.com"
|
||||
autoCapitalize={'none'}
|
||||
/>
|
||||
</View>
|
||||
<View style={styles.verticallySpaced}>
|
||||
<Input
|
||||
label="Password"
|
||||
leftIcon={{ type: 'font-awesome', name: 'lock' }}
|
||||
onChangeText={(text) => setPassword(text)}
|
||||
value={password}
|
||||
secureTextEntry={true}
|
||||
placeholder="Password"
|
||||
autoCapitalize={'none'}
|
||||
/>
|
||||
</View>
|
||||
<View style={[styles.verticallySpaced, { marginTop: 20 }]}>
|
||||
<Button
|
||||
title="Sign in"
|
||||
disabled={!!loading.length}
|
||||
loading={loading === 'LOGIN'}
|
||||
onPress={() => handleLogin('LOGIN', email, password)}
|
||||
/>
|
||||
</View>
|
||||
<View style={styles.verticallySpaced}>
|
||||
<Button
|
||||
title="Sign up"
|
||||
disabled={!!loading.length}
|
||||
loading={loading === 'SIGNUP'}
|
||||
onPress={() => handleLogin('SIGNUP', email, password)}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
marginTop: 40,
|
||||
padding: Styles.spacing,
|
||||
},
|
||||
verticallySpaced: {
|
||||
paddingTop: 4,
|
||||
paddingBottom: 4,
|
||||
alignSelf: 'stretch',
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,126 @@
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { View, StyleSheet, ScrollView, SafeAreaView, FlatList } from 'react-native'
|
||||
import { Styles } from '../lib/constants'
|
||||
import { Text } from 'react-native-elements'
|
||||
import { supabase } from '../lib/initSupabase'
|
||||
import { useUser } from '../components/UserContext'
|
||||
|
||||
import { Button, Input, ListItem, CheckBox } from 'react-native-elements'
|
||||
|
||||
/** URL polyfill. Required for Supabase queries to work in React Native. */
|
||||
import 'react-native-url-polyfill/auto'
|
||||
|
||||
type Todo = {
|
||||
id: number
|
||||
user_id: string
|
||||
task: string
|
||||
is_complete: boolean
|
||||
inserted_at: Date
|
||||
}
|
||||
|
||||
export default function TodoList() {
|
||||
const { user } = useUser()
|
||||
const [todos, setTodos] = useState<Array<Todo>>([])
|
||||
const [newTaskText, setNewTaskText] = useState<string>('')
|
||||
|
||||
useEffect(() => {
|
||||
fetchTodos()
|
||||
}, [])
|
||||
|
||||
const fetchTodos = async () => {
|
||||
const { data: todos, error } = await supabase
|
||||
.from<Todo>('todos')
|
||||
.select('*')
|
||||
.order('id', { ascending: false })
|
||||
if (error) console.log('error', error)
|
||||
else setTodos(todos!)
|
||||
}
|
||||
|
||||
const addTodo = async (taskText: string) => {
|
||||
const task = taskText.trim()
|
||||
console.log('newtask:', task)
|
||||
if (task.length) {
|
||||
const { data: todo, error } = await supabase
|
||||
.from<Todo>('todos')
|
||||
.insert({ task, user_id: user!.id })
|
||||
.single()
|
||||
if (error) console.log(error.message)
|
||||
else {
|
||||
setTodos([todo!, ...todos])
|
||||
setNewTaskText('')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const toggleCompleted = async (id: number, is_complete: boolean) => {
|
||||
const { data, error } = await supabase
|
||||
.from<Todo>('todos')
|
||||
.update({ is_complete: !is_complete })
|
||||
.eq('id', id)
|
||||
.single()
|
||||
if (error) console.log(error)
|
||||
else setTodos(todos.map((todo) => (todo.id === id ? data! : todo)))
|
||||
}
|
||||
|
||||
const deleteTodo = async (id: number) => {
|
||||
const { error } = await supabase.from<Todo>('todos').delete().eq('id', id)
|
||||
if (error) console.log('error', error)
|
||||
else setTodos(todos.filter((x) => x.id !== Number(id)))
|
||||
}
|
||||
|
||||
return (
|
||||
<View>
|
||||
<View style={[styles.verticallySpaced, { marginTop: 20 }]}>
|
||||
<Button title="Sign out" onPress={() => supabase.auth.signOut()} />
|
||||
</View>
|
||||
<View style={[styles.verticallySpaced, { marginTop: 20 }]}>
|
||||
<Input
|
||||
label="New todo"
|
||||
leftIcon={{ type: 'font-awesome', name: 'tasks' }}
|
||||
onChangeText={(text) => setNewTaskText(text)}
|
||||
value={newTaskText}
|
||||
/>
|
||||
<Button title="Add" onPress={() => addTodo(newTaskText)} />
|
||||
</View>
|
||||
<SafeAreaView style={styles.verticallySpaced}>
|
||||
<FlatList
|
||||
scrollEnabled={true}
|
||||
data={todos}
|
||||
keyExtractor={(item) => `${item.id}`}
|
||||
renderItem={({ item: todo }) => (
|
||||
<ListItem bottomDivider>
|
||||
<ListItem.Content>
|
||||
<View
|
||||
style={[
|
||||
{ display: 'flex', flexDirection: 'row', justifyContent: 'space-between' },
|
||||
]}
|
||||
>
|
||||
<CheckBox
|
||||
checked={todo.is_complete}
|
||||
onPress={() => toggleCompleted(todo.id, todo.is_complete)}
|
||||
/>
|
||||
<Text h3 style={{ margin: 'auto' }}>
|
||||
{todo.task}
|
||||
</Text>
|
||||
<Button title="Delete" onPress={() => deleteTodo(todo.id)}></Button>
|
||||
</View>
|
||||
</ListItem.Content>
|
||||
</ListItem>
|
||||
)}
|
||||
/>
|
||||
</SafeAreaView>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
marginTop: 40,
|
||||
padding: Styles.spacing,
|
||||
},
|
||||
verticallySpaced: {
|
||||
paddingTop: 4,
|
||||
paddingBottom: 4,
|
||||
alignSelf: 'stretch',
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,43 @@
|
||||
import React, { useEffect, useState, createContext, useContext } from 'react'
|
||||
import { Session, User } from '@supabase/supabase-js'
|
||||
import { supabase } from '../lib/initSupabase'
|
||||
|
||||
export const UserContext = createContext<{ user: User | null; session: Session | null }>({
|
||||
user: null,
|
||||
session: null,
|
||||
})
|
||||
|
||||
export const UserContextProvider = (props: any) => {
|
||||
const [session, setSession] = useState<Session | null>(null)
|
||||
const [user, setUser] = useState<User | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
const session = supabase.auth.session()
|
||||
setSession(session)
|
||||
setUser(session?.user ?? null)
|
||||
const { data: authListener } = supabase.auth.onAuthStateChange(async (event, session) => {
|
||||
console.log(`Supabase auth event: ${event}`)
|
||||
setSession(session)
|
||||
setUser(session?.user ?? null)
|
||||
})
|
||||
|
||||
return () => {
|
||||
authListener!.unsubscribe()
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [])
|
||||
|
||||
const value = {
|
||||
session,
|
||||
user,
|
||||
}
|
||||
return <UserContext.Provider value={value} {...props} />
|
||||
}
|
||||
|
||||
export const useUser = () => {
|
||||
const context = useContext(UserContext)
|
||||
if (context === undefined) {
|
||||
throw new Error(`useUser must be used within a UserContextProvider.`)
|
||||
}
|
||||
return context
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
if (!process.env.SUPABASE_URL) {
|
||||
console.log('constants.ts', 'Make sure you have a `.env` file to populate your variables.')
|
||||
}
|
||||
|
||||
export const SUPABASE_URL = process.env.REACT_NATIVE_SUPABASE_URL || ''
|
||||
export const SUPABASE_ANON_KEY = process.env.REACT_NATIVE_SUPABASE_ANON_KEY || ''
|
||||
|
||||
export const Styles = {
|
||||
fontNormal: 20,
|
||||
fontMedium: 28,
|
||||
fontLarge: 34,
|
||||
fontExtraLarge: 40,
|
||||
colorPrimary: 'black',
|
||||
spacing: 12,
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import AsyncStorage from '@react-native-async-storage/async-storage'
|
||||
import { createClient } from '@supabase/supabase-js'
|
||||
import { SUPABASE_URL, SUPABASE_ANON_KEY } from './constants'
|
||||
|
||||
export const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY, {
|
||||
localStorage: AsyncStorage as any,
|
||||
})
|
||||
Generated
+7979
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"main": "node_modules/expo/AppEntry.js",
|
||||
"scripts": {
|
||||
"start": "expo start",
|
||||
"android": "expo start --android",
|
||||
"ios": "expo start --ios",
|
||||
"web": "expo start --web",
|
||||
"eject": "expo eject"
|
||||
},
|
||||
"dependencies": {
|
||||
"@react-native-async-storage/async-storage": "^1.13.2",
|
||||
"@supabase/supabase-js": "^1.1.2",
|
||||
"babel-plugin-inline-dotenv": "^1.6.0",
|
||||
"expo": "~39.0.2",
|
||||
"expo-status-bar": "~1.0.2",
|
||||
"react": "16.13.1",
|
||||
"react-dom": "16.13.1",
|
||||
"react-native": "https://github.com/expo/react-native/archive/sdk-39.0.4.tar.gz",
|
||||
"react-native-elements": "^3.0.0-alpha.1",
|
||||
"react-native-url-polyfill": "^1.2.0",
|
||||
"react-native-web": "~0.13.12"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "~7.9.0",
|
||||
"@types/react": "~16.9.35",
|
||||
"@types/react-dom": "~16.9.8",
|
||||
"@types/react-native": "~0.63.2",
|
||||
"typescript": "~3.9.5"
|
||||
},
|
||||
"private": true
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"jsx": "react-native",
|
||||
"lib": ["dom", "esnext"],
|
||||
"moduleResolution": "node",
|
||||
"noEmit": true,
|
||||
"skipLibCheck": true,
|
||||
"resolveJsonModule": true,
|
||||
"strict": true
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user