こんにちはnaoです。
最近はネイティブアプリを作りたいなと思っていて、Flutterなどあると思いますが
普段Reactを触っているので今回はReactNativeを使っていこうと思います。
DBに関してはFirebaseなど考えましたが勉強がてらSupabaseで構築していこうと思います。
前提条件
- Macで開発を進めます。
- Expo、Supabase、Simuratorのインストール方法は割愛します。
環境構築
SupabaseにログインをしてDBを構築します。
まだアカウントを作ってない人はGithub認証でアカウントを作成しておきましょう。
DBの設定内容は以下となります。
次に、データベース スキーマを設定します。SQL エディターの「User Management Starter」を使いセットアップしましょう。
API キーを取得する
データベース テーブルを作成したので、自動生成された API を使用してデータを挿入する準備が整いました。anonAPI 設定からプロジェクトの URL とキーを取得するだけです。
ダッシュボードの「API 設定」ページに移動します。
このページでプロジェクトURL、anon、およびキーを見つけます。service_role
アプリの構築
今回はexpoを使ってアプリを構築していきます。
command
npx create-expo-app -t expo-template-blank-typescript expo-user-management
cd expo-user-management
npm install @supabase/supabase-js
npm install react-native-elements @react-native-async-storage/async-storage react-native-url-polyfill
npx expo install expo-secure-store
npm startでSimuratorが立ち上がれば環境構築は完了です。
supabaseクライアントヘルパーファイルの作成
次はsupabaseをどこからでも呼び出せるようヘルパーファイルを作成します。
lib/supabase.ts
import "react-native-url-polyfill/auto";
import * as SecureStore from "expo-secure-store";
import { createClient } from "@supabase/supabase-js";
const ExpoSecureStoreAdapter = {
getItem: (key: string) => {
return SecureStore.getItemAsync(key);
},
setItem: (key: string, value: string) => {
SecureStore.setItemAsync(key, value);
},
removeItem: (key: string) => {
SecureStore.deleteItemAsync(key);
},
};
const supabaseUrl = process.env.REACT_NATIVE_SUPABASE_URL!;
const supabaseAnonKey = process.env.REACT_NATIVE_SUPABASE_ANON_KEY!;
export const supabase = createClient(supabaseUrl, supabaseAnonKey, {
auth: {
storage: ExpoSecureStoreAdapter as any,
autoRefreshToken: true,
persistSession: true,
detectSessionInUrl: false,
},
});
html
const ExpoSecureStoreAdapter = {
getItem: (key: string) => {
return SecureStore.getItemAsync(key);
},
setItem: (key: string, value: string) => {
SecureStore.setItemAsync(key, value);
},
removeItem: (key: string) => {
SecureStore.deleteItemAsync(key);
},
};
このアダプタは、Supabaseの認証情報やセッション情報を保存するためのインターフェースを提供します。Supabaseはこれを内部で使用し、認証情報をexpo-secure-storeに保存します。
html
export const supabase = createClient(supabaseUrl, supabaseAnonKey, {
auth: {
storage: ExpoSecureStoreAdapter as any,
autoRefreshToken: true,
persistSession: true,
detectSessionInUrl: false,
},
});
createClientメソッドを使用して、Supabaseのクライアントを作成しています。その際、環境変数から取得したURLと匿名キー、さらにオプションとして先ほど作成したアダプタを含む設定を渡しています。
ログインコンポーネント作成
メールアドレスとパスワードを用いてログインするためのコンポーネントを作成します。
components/Auth.tsx
import React, { useState } from "react";
import { Alert, StyleSheet, View } from "react-native";
import { supabase } from "../lib/supabase";
import { Button, Input } from "react-native-elements";
const Auth = () => {
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [loading, setLoading] = useState(false);
const signInWithEmail = async () => {
setLoading(true);
const { error } = await supabase.auth.signInWithPassword({
email: email,
password: password,
});
if (error) Alert.alert(error.message);
setLoading(false);
};
const signUpWithEmail = async () => {
setLoading(true);
const { error } = await supabase.auth.signUp({
email: email,
password: password,
});
if (error) Alert.alert(error.message);
setLoading(false);
};
return (
<View style={styles.container}>
<View style={[styles.verticallySpaced, styles.mt20]}>
<Input
label="Email"
leftIcon={{ type: "font-awesome", name: "envelop" }}
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, styles.mt20]}>
<Button
title="Sign in"
disabled={loading}
onPress={() => signInWithEmail()}
/>
</View>
<View style={[styles.verticallySpaced, styles.mt20]}>
<Button
title="Sign up"
disabled={loading}
onPress={() => signUpWithEmail()}
/>
</View>
</View>
);
};
const styles = StyleSheet.create({
container: {
marginTop: 40,
padding: 12,
},
verticallySpaced: {
paddingTop: 4,
paddingBottom: 4,
alignSelf: "stretch",
},
mt20: {
marginTop: 20,
},
});
export default Auth;
アカウントページの作成
次はマイページを作成します。
components/Account.tsx
import { useState, useEffect } from "react";
import { supabase } from "../lib/supabase";
import { StyleSheet, View, Alert } from "react-native";
import { Button, Input } from "react-native-elements";
import { Session } from "@supabase/supabase-js";
const Account = ({ session }: { session: Session }) => {
const [loading, setLoading] = useState(true);
const [username, setUsername] = useState("");
const [website, setWebsite] = useState("");
const [avatarUrl, setAvatarUrl] = useState("");
useEffect(() => {
if (session) getProfile();
}, [session]);
const getProfile = async () => {
try {
setLoading(true);
if (!session?.user) throw new Error("セッションが切れています。");
let { data, error, status } = await supabase
.from("profiles")
.select("username,website,avatar_url")
.eq("id", session?.user.id)
.single();
if (error && status !== 406) {
throw error;
}
if (data) {
setUsername(data.username);
setWebsite(data.website);
setAvatarUrl(data.avatar_url);
}
} catch (error) {
if (error instanceof Error) {
Alert.alert(error.message);
}
} finally {
setLoading(false);
}
};
const updateProfile = async ({
username,
website,
avatar_url,
}: {
username: string;
website: string;
avatar_url: string;
}) => {
try {
setLoading(true);
if (!session?.user) throw new Error("No user on the session!");
const updates = {
id: session?.user.id,
username,
website,
avatar_url,
updated_at: new Date(),
};
let { error } = await supabase.from("profiles").upsert(updates);
if (error) {
throw error;
}
} catch (error) {
if (error instanceof Error) {
Alert.alert(error.message);
}
} finally {
setLoading(false);
}
};
return (
<View style={styles.container}>
<View style={[styles.verticallySpaced, styles.mt20]}>
<Input label="Email" value={session?.user?.email} disabled />
</View>
<View style={styles.verticallySpaced}>
<Input
label="Username"
value={username || ""}
onChangeText={(text) => setUsername(text)}
/>
</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}
/>
</View>
<View style={styles.verticallySpaced}>
<Button title="Sign Out" onPress={() => supabase.auth.signOut()} />
</View>
</View>
);
};
const styles = StyleSheet.create({
container: {
marginTop: 40,
padding: 12,
},
verticallySpaced: {
paddingTop: 4,
paddingBottom: 4,
alignSelf: "stretch",
},
mt20: {
marginTop: 20,
},
});
export default Account;
App.tsxで呼び出す
それでは今作成したコンポーネントを呼び出しましょう。
App.tsx
import "react-native-url-polyfill/auto";
import { useState, useEffect } from "react";
import { supabase } from "./lib/supabase";
import Auth from "./components/Auth";
import Account from "./components/Account";
import { View } from "react-native";
import { Session } from "@supabase/supabase-js";
export default function App() {
const [session, setSession] = useState<Session | null>(null);
useEffect(() => {
supabase.auth.getSession().then(({ data: { session } }) => {
setSession(session);
});
supabase.auth.onAuthStateChange((_event, session) => {
setSession(session);
});
}, []);
return (
<View>
{session && session.user ? (
<Account key={session.user.id} session={session} />
) : (
<Auth />
)}
</View>
);
}
npm startでサインアップとサインインができプロフィールの更新ができればOKです。