Merge pull request #315 from supabase/react-native-example

React native example
This commit is contained in:
ykdojo authored and GitHub committed 2020-12-28 16:10:22 -08:00
commit b7d0237de7
20 files changed
+8489

No files matched your search

+1
View File
@@ -76,6 +76,7 @@ typings/
# dotenv environment variables file
.env
.env.test
.env.local
# parcel-bundler cache (https://parceljs.org/)
.cache
+3
View File
@@ -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
}
+13
View File
@@ -0,0 +1,13 @@
node_modules/**/*
.expo/*
npm-debug.*
*.jks
*.p8
*.p12
*.key
*.mobileprovision
*.orig.*
web-build/
# macOS
.DS_Store
+51
View File
@@ -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',
},
})
+88
View File
@@ -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).
![image](https://user-images.githubusercontent.com/10214025/88916245-528c2680-d298-11ea-8a71-708f93e1ce4f.png)
**_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
+32
View File
@@ -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

+7
View File
@@ -0,0 +1,7 @@
module.exports = function(api) {
api.cache(true);
return {
presets: ['babel-preset-expo'],
plugins: ['inline-dotenv'],
}
};
+77
View File
@@ -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',
},
})
+126
View File
@@ -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
}
+15
View File
@@ -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,
}
+7
View File
@@ -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,
})
File diff suppressed because it is too large. Load diff
+31
View File
@@ -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
}
+12
View File
@@ -0,0 +1,12 @@
{
"compilerOptions": {
"allowSyntheticDefaultImports": true,
"jsx": "react-native",
"lib": ["dom", "esnext"],
"moduleResolution": "node",
"noEmit": true,
"skipLibCheck": true,
"resolveJsonModule": true,
"strict": true
}
}