MemFire Cloud是一款提供云数据库,用户可以创建云数据库,并对数据库进行管理,还可以对数据库进行备份操作。它还提供后端即服务,用户可以在1分钟内新建一个应用,使用自动生成的API和SDK,访问云数据库、对象存储、用户认证与授权等功能,可专注于编写前端应用程序代码,加速WEB或APP应用开发。
此示例提供了使用 MemFire Cloud 和 React Native构建简单用户管理应用程序(从头开始)的步骤。这包括:
-
MemFire Cloud云数据库:用于存储用户数据的 MemFireDB数据库。
-
MemFire Cloud用户验证:用户可以使用邮箱密码登录。
-
MemFire Cloud存储:用户可以上传照片。
-
行级安全策略:数据受到保护,因此个人只能访问自己的数据。
-
即时API:创建数据库表时会自动生成 API。
在本指南结束时,您将拥有一个允许用户登录和更新一些基本个人资料详细信息的应用程序:
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-K6Vnosrg-1663723902918)(https://g8nb5c2xl3.feishu.cn/space/api/box/stream/download/asynccode/?code=ZDUyZjU2MWZmZmI0NjdhMjBlZDUzZWQxZWY2NmNhNWVfd3ZVZk03Qmw3bGtkTndqYU9tVEtCVndJdjF4ZmhxNFBfVG9rZW46Ym94Y25PUDNDcDZ5bWJxV0hVWTR4ZHZWNlRiXzE2NjM3MjM4ODE6MTY2MzcyNzQ4MV9WNA)]](https://img-hello-world.oss-cn-beijing.aliyuncs.com/e7c48fad4a48188fe385bcfba1d82d27.png)
创建应用
目的:我们的应用就是通过在这里创建的应用来获得数据库、云存储等一系列资源,并将获得该应用专属的API访问链接和访问密钥,用户可以轻松的与以上资源进行交互。
登录https://cloud.memfiredb.com/auth/login创建应用
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-44KaEAKR-1663723902919)(https://g8nb5c2xl3.feishu.cn/space/api/box/stream/download/asynccode/?code=N2I3ZmRjMDRjOTVjMzk3NTg3MWVjNTcxNTdiZmNhZjdfVFFERDZGMWZaanBwUFc4ckdvY1JyVTRRQXBFMzBHWXNfVG9rZW46Ym94Y24yMzJuYWN2YTF4SjRITUlrYVluQWplXzE2NjM3MjM4ODE6MTY2MzcyNzQ4MV9WNA)]](https://img-hello-world.oss-cn-beijing.aliyuncs.com/735ccae80bec405c40e1479d3116bb5a.png)
创建数据表
点击应用,视图化创建数据表
- 创建profiles表;
在数据表页面,点击“新建数据表”,页面配置如下:
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qQ3sQCXy-1663723902919)(https://g8nb5c2xl3.feishu.cn/space/api/box/stream/download/asynccode/?code=NTA2NmQyOTkwZmY0NDQ3MjNjNmU4OGY4OWRmY2JjZDFfTlJlSVAyRG9RZkV3UGpUTWRPN0lVNG1nT1VrWWRJSnVfVG9rZW46Ym94Y25jTEVUQ0NuODNDWnFUMFpGRHJUZFBlXzE2NjM3MjM4ODE6MTY2MzcyNzQ4MV9WNA)]](https://img-hello-world.oss-cn-beijing.aliyuncs.com/5b24b5d67b3a387c285dcb7c589929da.png)
其中profiles表字段id和auth.users表中的id字段(uuid类型)外键关联。
- 开启Profiles的RLS数据安全访问规则;
选中创建的Profiles表,点击表权限栏,如下图所示,点击"启用RLS"按钮
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kV37ml76-1663723902919)(https://g8nb5c2xl3.feishu.cn/space/api/box/stream/download/asynccode/?code=OWMxMWMzZDE1Yzk4MDc2ODg1M2VmMzE4NDU0OTdmNDZfZWJwWW5tb2ZJZTJHUk5xcVpTaGIwcU1vZjEyWE9sMzFfVG9rZW46Ym94Y25oeWtyQmdZYllidWUwTHVXcU9vYTllXzE2NjM3MjM4ODE6MTY2MzcyNzQ4MV9WNA)]](https://img-hello-world.oss-cn-beijing.aliyuncs.com/c0f18963afd1ba167fc1df859e320054.png)
- 允许每个用户可以查看公共的个人信息资料;
点击"新规则"按钮,在弹出弹框中,选择"为所有用户启用访问权限",输入策略名称,选择"SELECT(查询)"操作,点击“创建策略”按钮,如下图。
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2l1hVXUA-1663723902920)(https://g8nb5c2xl3.feishu.cn/space/api/box/stream/download/asynccode/?code=ZDI3NjMxNzZhNGU4YzU3MDNiNDJiZGYxZjNkNDc0NzBfaW9TeU1ZbTVHV2pBOXhYVnphbDZRaGtOM1RoZVMyYkFfVG9rZW46Ym94Y25aWXBsbnRNSkpPYWdHeUlmMzJlM0xjXzE2NjM3MjM4ODE6MTY2MzcyNzQ4MV9WNA)]](https://img-hello-world.oss-cn-beijing.aliyuncs.com/fc662d9b433d9bf5207b75e07975ffe5.png)
- 仅允许用户增删改查本人的个人资料信息;
点击"新规则"按钮,在弹出弹框中,选择"根据用户ID为用户启用访问权限",输入策略名称,选择"ALL(所有)"操作,点击“创建策略”按钮,如下图。
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HbIoZJgF-1663723902925)(https://g8nb5c2xl3.feishu.cn/space/api/box/stream/download/asynccode/?code=YzIwNDhiNTJlNDA2M2QzZTViMGVlODc4ZDYwOWMxNTBfUmpPd2JsbGR2dk85ZTh3SXlYcjFnZVpGNmxndXE1b0ZfVG9rZW46Ym94Y25mRGkwSUtCYXM3RG5BNWh2UUR5MkpnXzE2NjM3MjM4ODE6MTY2MzcyNzQ4MV9WNA)]](https://img-hello-world.oss-cn-beijing.aliyuncs.com/d70e5e910c2168314d7a904736c0601f.png)
创建avatars存储桶
创建云存储的存储桶,用来存储用户的头像图片,涉及操作包括:
- 创建一个存储桶avatars
在该应用的云存储导航栏,点击“新建Bucket”按钮,创建存储桶avatars。
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-crdjl4dB-1663723902926)(https://g8nb5c2xl3.feishu.cn/space/api/box/stream/download/asynccode/?code=MWYxMDM5Y2YyMThlMDJkZDhjYWZhM2ZkY2JjMDE3NDRfR3FOVHVDQ0ptSVRWV0RoTVpPVW52ZFdLbGx5RUdQTVpfVG9rZW46Ym94Y25SSDV0Mlh6bVp6aVRIZWdBQmtLYU44XzE2NjM3MjM4ODE6MTY2MzcyNzQ4MV9WNA)]](https://img-hello-world.oss-cn-beijing.aliyuncs.com/3e36796dd178cfed49e69a9512439cc6.png)
- 允许每个用户可以查看存储桶avatars
选中存储桶avatars,切换到权限设置栏,点击“新规则”按钮,弹出策略编辑弹框,选择“自定义”,如下图所示:
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-puWZV2DP-1663723902927)(https://g8nb5c2xl3.feishu.cn/space/api/box/stream/download/asynccode/?code=NjNmMmU4OTlkNWIzYjk1NDM5MDUxM2UxZjlkODZkMGNfV29IUU90dEEyQlRBVzNaaXhyNFVtQ0l6ZE4zOUxXcGlfVG9rZW46Ym94Y25jUnVkbEVFVWV4VDdHNFdHTklDdXhkXzE2NjM3MjM4ODE6MTY2MzcyNzQ4MV9WNA)]](https://img-hello-world.oss-cn-beijing.aliyuncs.com/16b64e96ad2c2d6ab792465f4ae8e71a.png)
选择SELECT操作,输入策略名称,点击“生成策略”按钮,如下图所示。
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xd2SHI8A-1663723902927)(https://g8nb5c2xl3.feishu.cn/space/api/box/stream/download/asynccode/?code=MTBiNWQ4MTU4NTRhOTFmNjBiMWZkZjdhYTkzNzllZWRfa20yTjBUZmFsZThFd1hQamNaZXpUYnMwbldTUmIwVFZfVG9rZW46Ym94Y25JRkdXTHg1bDk0WGdxVlA5WGRIMkxiXzE2NjM3MjM4ODE6MTY2MzcyNzQ4MV9WNA)]](https://img-hello-world.oss-cn-beijing.aliyuncs.com/bdf7e2d7dc9dc1f187635c7cdb4706e8.png)
- 允许用户上传存储桶avatars;
选中存储桶avatars,切换到权限设置栏,点击“新规则”按钮,弹出策略编辑弹框,选择“自定义”,如下图所示:
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2X3SUcGT-1663723902928)(https://g8nb5c2xl3.feishu.cn/space/api/box/stream/download/asynccode/?code=Y2MwNTYwNDE5ZjU2M2U2NzgyNzAwNzkzYWNmNmQyMjVfZjZtTkI3M0pDdVV0ZURCaUw4ZEZzMzE2RExacjZ0a0VfVG9rZW46Ym94Y25lTzBYR29LcU9kUXlkbGdYN0hpSU9kXzE2NjM3MjM4ODE6MTY2MzcyNzQ4MV9WNA)]](https://img-hello-world.oss-cn-beijing.aliyuncs.com/d399e99093fe55c785b79c1cef327700.png)
选择INSERT操作,输入策略名称,点击“生成策略”按钮,如下图所示。
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bwStQMiv-1663723902929)(https://g8nb5c2xl3.feishu.cn/space/api/box/stream/download/asynccode/?code=MjljYjViYjM0NDJmMGVjZGY0YjFmYjIwYTQzMjVhMDVfM0N1ZWcyTnl0Znh1S242TWRUa3RyN25nRXZrN0VhTEtfVG9rZW46Ym94Y25vT2tDV0YzMUc4elhydnBxeWMxb29kXzE2NjM3MjM4ODE6MTY2MzcyNzQ4MV9WNA)]](https://img-hello-world.oss-cn-beijing.aliyuncs.com/47f4ce0a2909b3e2c45cb441a32026fc.png)
查看结果
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7gmJKS6u-1663723902929)(https://g8nb5c2xl3.feishu.cn/space/api/box/stream/download/asynccode/?code=MzJiY2YzMzU4MTNkYjg1M2MxNzgyNzdiZjEzNjU2NDNfS1o1NUprY2l0eWtJVXM0b0Myc0NzOElBSHNQd0dtZjBfVG9rZW46Ym94Y25rcDVubkpGM1c1aG9CaDN6RDhUODJmXzE2NjM3MjM4ODE6MTY2MzcyNzQ4MV9WNA)]](https://img-hello-world.oss-cn-beijing.aliyuncs.com/a6ec2c05e36026b4f8b93ddc5c023df3.png)
所有数据表及RLS的sql(策略名称用英文代替)
1-- Create a table for public "profiles" 2create table profiles ( 3 id uuid references auth.users not null, 4 updated_at timestamp with time zone, 5 username text unique, 6 avatar_url text, 7 website text, 8 9 primary key (id), 10 unique(username), 11); 12 13alter table profiles enable row level security; 14 15create policy "Public profiles are viewable by everyone." 16 on profiles for select 17 using ( true ); 18 19create policy "Users can insert their own profile." 20 on profiles for insert 21 with check ( auth.uid() = id ); 22 23create policy "Users can update own profile." 24 on profiles for update 25 using ( auth.uid() = id ); 26-- Set up Storage! 27insert into storage.buckets (id, name) 28values ('avatars', 'avatars'); 29 30create policy "Avatar images are publicly accessible." 31 on storage.objects for select 32 using ( bucket_id = 'avatars' ); 33 34create policy "Anyone can upload an avatar." 35 on storage.objects for insert 36 with check ( bucket_id = 'avatars' );
获取 API密钥
现在您已经创建了一些数据库表,您可以使用自动生成的 API 插入数据。我们只需要从API设置中获取您在上面复制的URL和anon的密钥。
在应用->概括页面,获取服务地址以及token信息。
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-p172GkuF-1663723902929)(https://g8nb5c2xl3.feishu.cn/space/api/box/stream/download/asynccode/?code=OTI0OGNkODdkMmQxZWE4ZTZkZDJmNTA1Y2JhMDFhZjVfU3VzY2tMTVlTVmZOR0REN2x1QnBWWHZ2N1lzY0s3TlJfVG9rZW46Ym94Y25neVZSYlQ5MVRrd0p6VVVsS3JKVGZjXzE2NjM3MjM4ODE6MTY2MzcyNzQ4MV9WNA)]](https://img-hello-world.oss-cn-beijing.aliyuncs.com/5e9fd7e48ba602a21286fa9434788218.png)
Anon(公开)密钥是客户端API密钥。它允许“匿名访问”您的数据库,直到用户登录。登录后,密钥将切换到用户自己的登录令牌。这将为数据启用行级安全性。
注意:service_role(秘密)密钥可以绕过任何安全策略完全访问您的数据。这些密钥必须保密,并且要在服务器环境中使用,绝不能在客户端或浏览器上使用。 在后续示例代码中,需要提供supabaseUrl和supabaseKey。
认证设置
当用户点击邮件内魔法链接进行登录时,是需要跳转到我们应用的登录界面的。这里需要在认证设置中进行相关URL重定向的配置。
因为我们最终的应用会在本地的3000端口启动(亦或者其他端口),所以这里我们暂时将url设置为http://localhost:3000/
除此之外,在此界面也可以自定义使用我们自己的smtp服务器。
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gNdiMOe8-1663723902929)(https://g8nb5c2xl3.feishu.cn/space/api/box/stream/download/asynccode/?code=MDY2NmZkNWEyZjJiZDY2YWU3MTcwM2Y1MTk1ODY3NjZfbXBRekllUWZzMEkzNk9RUVdOc2o5WGoyMmFVd0E4THVfVG9rZW46Ym94Y240eVZQREM4M3lzbEw3YW0wY0Z3TTVnXzE2NjM3MjM4ODE6MTY2MzcyNzQ4MV9WNA)]](https://img-hello-world.oss-cn-beijing.aliyuncs.com/8f51bc6db8635e1cef10a3104d9d3e9c.png)
构建应用程序
让我们从头开始构建 React Native应用程序。
初始化 React Native
我们可以使用create-expo-app来初始化一个名为 memfiredbReactNative:
1npx create-expo-app memfiredbReactNative 2cd memfiredbReactNative
然后让我们安装额外的依赖项:supabase-js
1yarn add @supabase/supabase-js 2yarn add react-native-elements 3yarn add react-native-safe-area-context 4yarn add @react-native-async-storage/async-storage 5yarn add react-native-url-polyfill
我们需要创建一个可以访问我们应用程序数据的客户端,我们使用了Supabase 客户端,使用他生态里提供的功能(登录、注册、增删改查等)去进行交互。创建一个可以访问我们应用程序数据的客户端需要接口的地址(URL)和一个数据权限的令牌(ANON_KEY),我们需要去应用的概览里面去获取这两个参数然后配置到supabase.js里面去。
lib/supabase.js文件
1import AsyncStorage from '@react-native-async-storage/async-storage'; 2import { createClient } from '@supabase/supabase-js' 3 4const URL = "" 5const ANON_KEY = "" 6 7export const supabase = createClient(URL, ANON_KEY, { 8 localStorage: AsyncStorage, 9 autoRefreshToken: true, 10 persistSession: true, 11 detectSessionInUrl: false, 12});
让我们设置一个 React Native 组件来管理登录和注册。用户将能够使用他们的电子邮件和密码登录。
components/Auth.js
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 { user, error } = await supabase.auth.signIn({ 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 { user, 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 style={styles.mt100}> 35 <View style={[styles.verticallySpaced, styles.mt20]}> 36 <Input 37 label="邮箱" 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="密码" 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="登录" disabled={loading} onPress={() => signInWithEmail()} /> 58 </View> 59 <View style={styles.verticallySpaced}> 60 <Button title="注册" 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 mt100:{ 80 marginTop:100 81 } 82})
用户信息页面
用户登录后,我们可以允许他们编辑他们的个人资料详细信息并管理他们的帐户。
让我们为它创建一个名为Account.js.
components/Account.js
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 { ApiError, Session } from "@supabase/supabase-js"; 6 7export default function Account({ session }) { 8 const [loading, setLoading] = useState(false); 9 const [username, setUsername] = useState(""); 10 const [website, setWebsite] = useState(""); 11 const [avatar_url, setAvatarUrl] = useState(""); 12 13 useEffect(() => { 14 if (session) getProfile(); 15 }, [session]); 16 17 async function getProfile() { 18 try { 19 setLoading(true); 20 const user = supabase.auth.user(); 21 if (!user) throw new Error("No user on the session!"); 22 23 let { data, error, status } = await supabase 24 .from("profiles") 25 .select(`username, website, avatar_url`) 26 .eq("id", user.id) 27 .single(); 28 if (error && status !== 406) { 29 throw error; 30 } 31 32 if (data) { 33 setUsername(data.username); 34 setWebsite(data.website); 35 setAvatarUrl(data.avatar_url); 36 } 37 } catch (error) { 38 Alert.alert((error).message); 39 } finally { 40 setLoading(false); 41 } 42 } 43 44 async function updateProfile({ 45 username, 46 website, 47 avatar_url, 48 }) { 49 try { 50 setLoading(true); 51 const user = supabase.auth.user(); 52 if (!user) throw new Error(" 会话中没有用户!"); 53 54 const updates = { 55 id: user.id, 56 username, 57 website, 58 avatar_url, 59 updated_at: new Date(), 60 }; 61 62 let { error } = await supabase 63 .from("profiles") 64 .upsert(updates, { returning: "minimal" }); 65 66 if (error) { 67 throw error; 68 } 69 } catch (error) { 70 Alert.alert((error).message); 71 } finally { 72 setLoading(false); 73 } 74 } 75 76 return ( 77 <View style={styles.mt100}> 78 <View> 79 80 </View> 81 <View style={[styles.verticallySpaced, styles.mt20]}> 82 <Input label="邮箱" value={session?.user?.email} disabled /> 83 </View> 84 <View style={styles.verticallySpaced}> 85 <Input 86 label="用户名" 87 value={username || ""} 88 onChangeText={(text) => setUsername(text)} 89 /> 90 </View> 91 <View style={styles.verticallySpaced}> 92 <Input 93 label="网址" 94 value={website || ""} 95 onChangeText={(text) => setWebsite(text)} 96 /> 97 </View> 98 99 <View style={[styles.verticallySpaced, styles.mt20]}> 100 <Button 101 title={loading ? "正在修改" : "确定"} 102 onPress={() => updateProfile({ username, website, avatar_url })} 103 disabled={loading} 104 /> 105 </View> 106 107 <View style={styles.verticallySpaced}> 108 <Button title="登出" onPress={() => supabase.auth.signOut()} /> 109 </View> 110 </View> 111 ); 112} 113 114const styles = StyleSheet.create({ 115 container: { 116 marginTop: 40, 117 padding: 12, 118 backgroundColor:'black' 119 }, 120 verticallySpaced: { 121 paddingTop: 4, 122 paddingBottom: 4, 123 alignSelf: "stretch", 124 }, 125 mt20: { 126 marginTop: 20, 127 }, 128 mt100:{ 129 marginTop:100 130 } 131});
修改项目入口文件
现在我们已经准备好所有组件,让我们更新App.js:
App.js
1import { StatusBar } from 'expo-status-bar'; 2import { StyleSheet, Text, View } from 'react-native'; 3import 'react-native-url-polyfill/auto' 4import React, { useRef, useState, useEffect } from 'react'; 5import { supabase } from './lib/supabase' 6import Auth from './components/Auth' 7import Account from './components/Account' 8 9export default function App() { 10 const [session, setSession] = useState() 11 12 useEffect(() => { 13 setSession(supabase.auth.session()) 14 15 supabase.auth.onAuthStateChange((_event, session) => { 16 setSession(session) 17 }) 18 }, []) 19 20 return ( 21 <View> 22 {session && session.user ? <Account key={session.user.id} session={session} /> : <Auth />} 23 </View> 24 ); 25} 26 27const styles = StyleSheet.create({ 28 container: { 29 flex: 1, 30 backgroundColor: 'black', 31 alignItems: 'center', 32 justifyContent: 'center', 33 }, 34});
完成后,在终端执行打开浏览器
1expo start --web
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-McfycXGq-1663813317938)(https://g8nb5c2xl3.feishu.cn/space/api/box/stream/download/asynccode/?code=OTY2NzVkYmE0MDdkMzI3NjQ1ZDQxNzMwM2FlNDg2M2RfdTdaUHpPNkViQ3FWdFpMVk40d2dycE1va0RhZjRKUXJfVG9rZW46Ym94Y24zbTh3U3VZMFk2OVljbm5IaHZDMmZlXzE2NjM4MTMyNDk6MTY2MzgxNjg0OV9WNA)]](https://img-hello-world.oss-cn-beijing.aliyuncs.com/1821858144dc963f74a4466e0c1fd534.png)
实现:上传头像及更新用户信息
每个 MemFire Cloud项目都配置了存储,用于管理照片和视频等大文件。
创建上传小组件
让我们为用户创建一个头像,以便他们可以上传个人资料照片。
创建上传头像的小组件
components/Avatar.js
1import { useEffect, useState } from 'react' 2import { supabase } from '../lib/supabase' 3import { View } from 'react-native' 4import { Button } from 'react-native-elements' 5 6export default function Avatar({ url, size, onUpload }) { 7 const [avatarUrl, setAvatarUrl] = useState(null) 8 const [uploading, setUploading] = useState(false) 9 10 useEffect(() => { 11 if (url) downloadImage(url) 12 }, [url]) 13 14 async function downloadImage(path) { 15 try { 16 const { data, error } = await supabase.storage.from('avatars').download(path) 17 if (error) { 18 throw error 19 } 20 var blob = new Blob([data], { 21 type: "text/vtt; charset=utf-8" 22 }); 23 24 const fileReaderInstance = new FileReader(); 25 fileReaderInstance.readAsDataURL(blob); 26 fileReaderInstance.onload = () => { 27 let base64 = fileReaderInstance.result; 28 setAvatarUrl(base64) 29 } 30 } catch (error) { 31 console.log('Error downloading image: ', error.message) 32 } 33 } 34 async function uploadAvatar(event) { 35 try { 36 setUploading(true) 37 38 if (!event.target.files || event.target.files.length === 0) { 39 throw new Error('You must select an image to upload.') 40 } 41 42 const file = event.target.files[0] 43 const fileExt = file.name.split('.').pop() 44 const fileName = `${Math.random()}.${fileExt}` 45 const filePath = `${fileName}` 46 47 let { error: uploadError } = await supabase.storage.from('avatars').upload(filePath, file) 48 if (uploadError) { 49 throw uploadError 50 } 51 52 onUpload(filePath) 53 } catch (error) { 54 alert(error.message) 55 } finally { 56 setUploading(false) 57 } 58 } 59 60 return ( 61 <View> 62 {avatarUrl ? ( 63 <img src={avatarUrl} alt="Avatar" className="avatar image" /> 64 ) : ( 65 <View className="avatar no-image" /> 66 )} 67 <View> 68 <label className="button primary block" htmlFor="single"> 69 <Button title={uploading ? 'Uploading ...' : 'Upload'}></Button> 70 71 </label> 72 <input 73 style={{ 74 visibility: 'hidden', 75 position: 'absolute', 76 }} 77 type="file" 78 id="single" 79 accept="image/*" 80 onChange={uploadAvatar} 81 disabled={uploading} 82 /> 83 </View> 84 </View> 85 ) 86}
然后我们可以在components/Account.js模板的顶部添加小部件:
components/Account.js
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 { ApiError, Session } from "@supabase/supabase-js"; 6import Avatar from './Avatar' 7 8export default function Account({ session }) { 9 const [loading, setLoading] = useState(false); 10 const [username, setUsername] = useState(""); 11 const [website, setWebsite] = useState(""); 12 const [avatar_url, setAvatarUrl] = useState(""); 13 14 useEffect(() => { 15 if (session) getProfile(); 16 }, [session]); 17 18 async function getProfile() { 19 try { 20 setLoading(true); 21 const user = supabase.auth.user(); 22 if (!user) throw new Error("No user on the session!"); 23 24 let { data, error, status } = await supabase 25 .from("profiles") 26 .select(`username, website, avatar_url`) 27 .eq("id", user.id) 28 .single(); 29 if (error && status !== 406) { 30 throw error; 31 } 32 33 if (data) { 34 setUsername(data.username); 35 setWebsite(data.website); 36 setAvatarUrl(data.avatar_url); 37 } 38 } catch (error) { 39 Alert.alert((error).message); 40 } finally { 41 setLoading(false); 42 } 43 } 44 45 async function updateProfile({ 46 username, 47 website, 48 avatar_url, 49 }) { 50 try { 51 setLoading(true); 52 const user = supabase.auth.user(); 53 if (!user) throw new Error(" 会话中没有用户!"); 54 55 const updates = { 56 id: user.id, 57 username, 58 website, 59 avatar_url, 60 updated_at: new Date(), 61 }; 62 63 let { error } = await supabase 64 .from("profiles") 65 .upsert(updates, { returning: "minimal" }); 66 67 if (error) { 68 throw error; 69 } 70 } catch (error) { 71 Alert.alert((error).message); 72 } finally { 73 setLoading(false); 74 } 75 } 76 77 return ( 78 <View style={styles.mt100}> 79 <View> 80 <Avatar url={avatar_url} 81 size={150} 82 onUpload={(url) => { 83 setAvatarUrl(url) 84 updateProfile({ username, website, avatar_url: url }) 85 }}/> 86 </View> 87 <View style={[styles.verticallySpaced, styles.mt20]}> 88 <Input label="邮箱" value={session?.user?.email} disabled /> 89 </View> 90 <View style={styles.verticallySpaced}> 91 <Input 92 label="用户名" 93 value={username || ""} 94 onChangeText={(text) => setUsername(text)} 95 /> 96 </View> 97 <View style={styles.verticallySpaced}> 98 <Input 99 label="网址" 100 value={website || ""} 101 onChangeText={(text) => setWebsite(text)} 102 /> 103 </View> 104 105 <View style={[styles.verticallySpaced, styles.mt20]}> 106 <Button 107 title={loading ? "正在修改" : "确定"} 108 onPress={() => updateProfile({ username, website, avatar_url })} 109 disabled={loading} 110 /> 111 </View> 112 113 <View style={styles.verticallySpaced}> 114 <Button title="登出" onPress={() => supabase.auth.signOut()} /> 115 </View> 116 </View> 117 ); 118} 119 120const styles = StyleSheet.create({ 121 container: { 122 marginTop: 40, 123 padding: 12, 124 backgroundColor:'black' 125 }, 126 verticallySpaced: { 127 paddingTop: 4, 128 paddingBottom: 4, 129 alignSelf: "stretch", 130 }, 131 mt20: { 132 marginTop: 20, 133 }, 134 mt100:{ 135 marginTop:100 136 } 137});
恭喜!在这个阶段,您拥有一个功能齐全的应用程序!
