快速入门: Expo

介绍

这个例子提供了建立一个基本用户管理应用程序的步骤。它包括。

  • MemFire Cloud Database:一个用于存储用户数据的Postgres数据库。
  • MemFire Cloud Auth:用户可以用魔法链接登录(没有密码,只有电子邮件)。
  • MemFire Cloud 存储:用户可以上传照片。
  • 行级安全:数据受到保护,个人只能访问自己的数据。
  • 即时APIs。当你创建你的数据库表时,API将自动生成。

在本指南结束时,你将拥有一个允许用户登录和更新一些基本档案细节的应用程序。

Supabase用户管理实例

GitHub#

如果你在阅读指南时遇到困难,请参考此版本

项目设置

在我们开始构建之前,我们要设置我们的数据库和API。这就像在Supabase中启动一个新项目一样简单 然后在数据库中创建一个 "模式"。

创建一个项目

  1. 进入MemFire Cloud
  2. 点击 "新项目"。
  3. 输入你的项目细节。
  4. 等待新数据库的启动。

设置数据库模式

现在我们要设置数据库模式。我们可以使用SQL编辑器中的 "用户管理"的模板快速启动。 或者你可以直接复制/粘贴下面的SQL,然后自己运行它。

  1. 进入仪表版中的SQL编辑器页面。
  2. 点击 用户管理的模板。
  3. 点击 运行

获取API密钥#

现在你已经创建了一些数据库表,你已经准备好使用自动生成的API插入数据。 我们只需要从API设置中获得URL和anon密钥。

  1. 进入仪表板中的设置页面。
  2. 单击侧边栏中的API
  3. 在这个页面上找到你的APIURLanonservice_role键。

构建应用程序

让我们开始从头开始构建React Native应用。

初始化一个React Native应用#

我们可以使用expo来初始化 一个名为 "expo-user-management "的应用程序。

npx create-expo-app -t expo-template-blank-typescript expo-user-management

cd expo-user-management

然后让我们安装额外的依赖项。supabase-js

npm install @supabase/supabase-js
npm install react-native-elements @react-native-async-storage/async-storage react-native-url-polyfill

现在让我们创建一个辅助文件来初始化Supabase客户端。 我们需要API URL和你[早些时候]复制的anon密钥(#get-the-api-keys)。 这些变量将被暴露在浏览器上,这完全没有问题,因为我们有 行级安全在我们的数据库上启用。

lib/supabase.ts
1import AsyncStorage from '@react-native-async-storage/async-storage'
2import { createClient } from '@supabase/supabase-js'
3
4const supabaseUrl = YOUR_REACT_NATIVE_SUPABASE_URL
5const supabaseAnonKey = YOUR_REACT_NATIVE_SUPABASE_ANON_KEY
6
7export const supabase = createClient(supabaseUrl, supabaseAnonKey, {
8  auth: {
9    storage: AsyncStorage as any,
10    autoRefreshToken: true,
11    persistSession: true,
12    detectSessionInUrl: false,
13  },
14})

设置一个登录组件

让我们建立一个React Native组件来管理登录和注册。 用户将能够用他们的电子邮件和密码登录。

components/Auth.tsx
1import React, { useState } from 'react'
2import { Alert, StyleSheet, View } from 'react-native'
3import { supabase } from '../lib/supabase'
4import { Button, Input } from 'react-native-elements'
5
6export default function Auth() {
7  const [email, setEmail] = useState('')
8  const [password, setPassword] = useState('')
9  const [loading, setLoading] = useState(false)
10
11  async function signInWithEmail() {
12    setLoading(true)
13    const { error } = await supabase.auth.signInWithPassword({
14      email: email,
15      password: password,
16    })
17
18    if (error) Alert.alert(error.message)
19    setLoading(false)
20  }
21
22  async function signUpWithEmail() {
23    setLoading(true)
24    const { error } = await supabase.auth.signUp({
25      email: email,
26      password: password,
27    })
28
29    if (error) Alert.alert(error.message)
30    setLoading(false)
31  }
32
33  return (
34    <View>
35      <View style={[styles.verticallySpaced, styles.mt20]}>
36        <Input
37          label="Email"
38          leftIcon={{ type: 'font-awesome', name: 'envelope' }}
39          onChangeText={(text) => setEmail(text)}
40          value={email}
41          placeholder="email@address.com"
42          autoCapitalize={'none'}
43        />
44      </View>
45      <View style={styles.verticallySpaced}>
46        <Input
47          label="Password"
48          leftIcon={{ type: 'font-awesome', name: 'lock' }}
49          onChangeText={(text) => setPassword(text)}
50          value={password}
51          secureTextEntry={true}
52          placeholder="Password"
53          autoCapitalize={'none'}
54        />
55      </View>
56      <View style={[styles.verticallySpaced, styles.mt20]}>
57        <Button title="Sign in" disabled={loading} onPress={() => signInWithEmail()} />
58      </View>
59      <View style={styles.verticallySpaced}>
60        <Button title="Sign up" disabled={loading} onPress={() => signUpWithEmail()} />
61      </View>
62    </View>
63  )
64}
65
66const styles = StyleSheet.create({
67  container: {
68    marginTop: 40,
69    padding: 12,
70  },
71  verticallySpaced: {
72    paddingTop: 4,
73    paddingBottom: 4,
74    alignSelf: 'stretch',
75  },
76  mt20: {
77    marginTop: 20,
78  },
79})

账户页面

在用户登录后,我们可以允许他们编辑他们的个人资料细节和管理他们的账户。

让我们为此创建一个新的组件,叫做Account.tsx

components/Account.tsx
1import { useState, useEffect } from 'react'
2import { supabase } from '../lib/supabase'
3import { StyleSheet, View, Alert } from 'react-native'
4import { Button, Input } from 'react-native-elements'
5import { Session } from '@supabase/supabase-js'
6
7export default function Account({ session }: { session: Session }) {
8  const [loading, setLoading] = useState(true)
9  const [username, setUsername] = useState('')
10  const [website, setWebsite] = useState('')
11  const [avatarUrl, setAvatarUrl] = useState('')
12
13  useEffect(() => {
14    if (session) getProfile()
15  }, [session])
16
17  async function getProfile() {
18    try {
19      setLoading(true)
20      if (!session?.user) throw new Error('No user on the session!')
21
22      let { data, error, status } = await supabase
23        .from('profiles')
24        .select(`username, website, avatar_url`)
25        .eq('id', session?.user.id)
26        .single()
27      if (error && status !== 406) {
28        throw error
29      }
30
31      if (data) {
32        setUsername(data.username)
33        setWebsite(data.website)
34        setAvatarUrl(data.avatar_url)
35      }
36    } catch (error) {
37      if (error instanceof Error) {
38        Alert.alert(error.message)
39      }
40    } finally {
41      setLoading(false)
42    }
43  }
44
45  async function updateProfile({
46    username,
47    website,
48    avatar_url,
49  }: {
50    username: string
51    website: string
52    avatar_url: string
53  }) {
54    try {
55      setLoading(true)
56      if (!session?.user) throw new Error('No user on the session!')
57
58      const updates = {
59        id: session?.user.id,
60        username,
61        website,
62        avatar_url,
63        updated_at: new Date(),
64      }
65
66      let { error } = await supabase.from('profiles').upsert(updates)
67
68      if (error) {
69        throw error
70      }
71    } catch (error) {
72      if (error instanceof Error) {
73        Alert.alert(error.message)
74      }
75    } finally {
76      setLoading(false)
77    }
78  }
79
80  return (
81    <View>
82      <View style={[styles.verticallySpaced, styles.mt20]}>
83        <Input label="Email" value={session?.user?.email} disabled />
84      </View>
85      <View style={styles.verticallySpaced}>
86        <Input label="Username" value={username || ''} onChangeText={(text) => setUsername(text)} />
87      </View>
88      <View style={styles.verticallySpaced}>
89        <Input label="Website" value={website || ''} onChangeText={(text) => setWebsite(text)} />
90      </View>
91
92      <View style={[styles.verticallySpaced, styles.mt20]}>
93        <Button
94          title={loading ? 'Loading ...' : 'Update'}
95          onPress={() => updateProfile({ username, website, avatar_url: avatarUrl })}
96          disabled={loading}
97        />
98      </View>
99
100      <View style={styles.verticallySpaced}>
101        <Button title="Sign Out" onPress={() => supabase.auth.signOut()} />
102      </View>
103    </View>
104  )
105}
106
107const styles = StyleSheet.create({
108  container: {
109    marginTop: 40,
110    padding: 12,
111  },
112  verticallySpaced: {
113    paddingTop: 4,
114    paddingBottom: 4,
115    alignSelf: 'stretch',
116  },
117  mt20: {
118    marginTop: 20,
119  },
120})

启动

现在我们有了所有的组件,让我们更新App.tsx

App.tsx
1import 'react-native-url-polyfill/auto'
2import { useState, useEffect } from 'react'
3import { supabase } from './lib/supabase'
4import Auth from './components/Auth'
5import Account from './components/Account'
6import { View } from 'react-native'
7import { Session } from '@supabase/supabase-js'
8
9export default function App() {
10  const [session, setSession] = useState<Session | null>(null)
11
12  useEffect(() => {
13    supabase.auth.getSession().then(({ data: { session } }) => {
14      setSession(session)
15    })
16
17    supabase.auth.onAuthStateChange((_event, session) => {
18      setSession(session)
19    })
20  }, [])
21
22  return (
23    <View>
24      {session && session.user ? <Account key={session.user.id} session={session} /> : <Auth />}
25    </View>
26  )
27}

一旦完成,在终端窗口运行这个程序。

npm start

然后为你想测试的环境按下相应的键,你应该看到完成的应用程序。

简介照片

每个Supabase项目都配置了存储,用于管理大型文件,如 照片和视频。

额外的依赖安装

你将需要一个在你将建立项目的环境下工作的文件选取器,在这个例子中我们将使用 react-native-document-picker。

expo install react-native-document-picker

创建一个上传小组件

让我们为用户创建一个头像,以便他们可以上传个人资料照片。 我们可以从创建一个新的组件开始。

components/Avatar.tsx
1import { useState, useEffect } from 'react'
2import { supabase } from '../lib/supabase'
3import { StyleSheet, View, Alert, Image, Button } from 'react-native'
4import DocumentPicker, { isCancel, isInProgress, types } from 'react-native-document-picker'
5
6interface Props {
7  size: number
8  url: string | null
9  onUpload: (filePath: string) => void
10}
11
12export default function Avatar({ url, size = 150, onUpload }: Props) {
13  const [uploading, setUploading] = useState(false)
14  const [avatarUrl, setAvatarUrl] = useState<string | null>(null)
15  const avatarSize = { height: size, width: size }
16
17  useEffect(() => {
18    if (url) downloadImage(url)
19  }, [url])
20
21  async function downloadImage(path: string) {
22    try {
23      const { data, error } = await supabase.storage.from('avatars').download(path)
24
25      if (error) {
26        throw error
27      }
28
29      const fr = new FileReader()
30      fr.readAsDataURL(data)
31      fr.onload = () => {
32        setAvatarUrl(fr.result as string)
33      }
34    } catch (error) {
35      if (error instanceof Error) {
36        console.log('Error downloading image: ', error.message)
37      }
38    }
39  }
40
41  async function uploadAvatar() {
42    try {
43      setUploading(true)
44
45      const file = await DocumentPicker.pickSingle({
46        presentationStyle: 'fullScreen',
47        copyTo: 'cachesDirectory',
48        type: types.images,
49        mode: 'open',
50      })
51
52      const photo = {
53        uri: file.fileCopyUri,
54        type: file.type,
55        name: file.name,
56      }
57
58      const formData = new FormData()
59      formData.append('file', photo)
60
61      const fileExt = file.name.split('.').pop()
62      const filePath = `${Math.random()}.${fileExt}`
63
64      let { error } = await supabase.storage.from('avatars').upload(filePath, formData)
65
66      if (error) {
67        throw error
68      }
69
70      onUpload(filePath)
71    } catch (error) {
72      if (isCancel(error)) {
73        console.warn('cancelled')
74        // User cancelled the picker, exit any dialogs or menus and move on
75      } else if (isInProgress(error)) {
76        console.warn('multiple pickers were opened, only the last will be considered')
77      } else if (error instanceof Error) {
78        Alert.alert(error.message)
79      } else {
80        throw error
81      }
82    } finally {
83      setUploading(false)
84    }
85  }
86
87  return (
88    <View>
89      {avatarUrl ? (
90        <Image
91          source={{ uri: avatarUrl }}
92          accessibilityLabel="Avatar"
93          style={[avatarSize, styles.avatar, styles.image]}
94        />
95      ) : (
96        <View style={[avatarSize, styles.avatar, styles.noImage]} />
97      )}
98      <View>
99        <Button
100          title={uploading ? 'Uploading ...' : 'Upload'}
101          onPress={uploadAvatar}
102          disabled={uploading}
103        />
104      </View>
105    </View>
106  )
107}
108
109const styles = StyleSheet.create({
110  avatar: {
111    borderRadius: 5,
112    overflow: 'hidden',
113    maxWidth: '100%',
114  },
115  image: {
116    objectFit: 'cover',
117    paddingTop: 0,
118  },
119  noImage: {
120    backgroundColor: '#333',
121    border: '1px solid rgb(200, 200, 200)',
122    borderRadius: 5,
123  },
124})

添加新的小组件

然后我们就可以把小部件添加到账户页面。

components/Account.tsx
1// Import the new component
2import Avatar from './Avatar'
3
4// ...
5  return (
6    <View>
7      {/* Add to the body */}
8      <View>
9        <Avatar
10          size={200}
11          url={avatarUrl}
12          onUpload={(url: string) => {
13            setAvatarUrl(url)
14            updateProfile({ username, website, avatar_url: url })
15          }}
16        />
17      </View>
18      {/* ... */}
19    </View>
20  )
21}
22// ...

现在你需要运行预编译命令,使应用程序在你选择的平台上工作。

expo prebuild

下一步

在这个阶段,你已经有了一个功能完备的应用程序!