diff --git a/.env.dev b/.env.dev deleted file mode 100644 index e79a19c0..00000000 --- a/.env.dev +++ /dev/null @@ -1,3 +0,0 @@ -SUPABASE_DB_PASSWORD="AYH3i8hOkqZSY4Yx" -SUPABASE_ANNON_KEY="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Imx4dXVyb3pxcnB0aHl2eHZuYW1sIiwicm9sZSI6ImFub24iLCJpYXQiOjE3MzkwNzg0NjYsImV4cCI6MjA1NDY1NDQ2Nn0.m8YcHx4RNlRRi4-GsnQTg5h_X_WfRlSY7iYKVmhZCTU" -SUPABASE_URL="https://lxuurozqrpthyvxvnaml.supabase.co" \ No newline at end of file diff --git a/.env.example b/.env.example deleted file mode 100644 index ce3c1262..00000000 --- a/.env.example +++ /dev/null @@ -1,3 +0,0 @@ -SUPABASE_DB_PASSWORD= -SUPABASE_ANNON_KEY= -SUPABASE_URL= \ No newline at end of file diff --git a/app/(auth)/_layout.tsx b/app/(auth)/_layout.tsx new file mode 100644 index 00000000..b8c5721b --- /dev/null +++ b/app/(auth)/_layout.tsx @@ -0,0 +1,10 @@ +import { SafeAreaView } from "react-native"; +import { Slot } from "expo-router"; + +export default function AuthLayout() { + return ( + + + + ); +} diff --git a/app/(auth)/loginScreen.tsx b/app/(auth)/loginScreen.tsx new file mode 100644 index 00000000..a5c7fdb9 --- /dev/null +++ b/app/(auth)/loginScreen.tsx @@ -0,0 +1,11 @@ +import { View } from "react-native"; + +import { Login } from "@/fetaure/auth/screen/Login"; + +export default function LoginScreen() { + return ( + + + + ); +} diff --git a/app/(auth)/signupScreen.tsx b/app/(auth)/signupScreen.tsx new file mode 100644 index 00000000..185dfef9 --- /dev/null +++ b/app/(auth)/signupScreen.tsx @@ -0,0 +1,11 @@ +import { View } from "react-native"; + +import { Signup } from "@/fetaure/auth/screen/Signup"; + +export default function SignupScreen() { + return ( + + + + ); +} diff --git a/app/(protected)/_layout.tsx b/app/(protected)/_layout.tsx new file mode 100644 index 00000000..53411a7c --- /dev/null +++ b/app/(protected)/_layout.tsx @@ -0,0 +1,54 @@ +// app/(protected)/_layout.tsx +import { useEffect, useState } from "react"; +import { ActivityIndicator, View } from "react-native"; +import { Slot, useRouter } from "expo-router"; +import { type User } from "@supabase/supabase-js"; + +import supabase from "@/lib/supabase"; + +export default function ProtectedLayout() { + const router = useRouter(); + const [user, setUser] = useState(null); + const [loading, setLoading] = useState(true); + + useEffect(() => { + // 現在のセッションを取得 + const getSession = async () => { + const { + data: { session }, + } = await supabase.auth.getSession(); + setUser(session?.user ?? null); + setLoading(false); + }; + getSession(); + + // 認証状態の変化を監視 + const { + data: { subscription }, + } = supabase.auth.onAuthStateChange((_event, session) => { + setUser(session?.user ?? null); + }); + return () => subscription?.unsubscribe(); + }, []); + + useEffect(() => { + if (!loading && !user) { + router.replace("/loginScreen"); + } + }, [user, loading, router]); + + if (loading) { + // ローディング中はインジケーターを表示 + return ( + + + + ); + } + + if (!user) { + return null; + } + + return ; +} diff --git a/app/index.tsx b/app/(protected)/index.tsx similarity index 53% rename from app/index.tsx rename to app/(protected)/index.tsx index 7b167a34..7b932301 100644 --- a/app/index.tsx +++ b/app/(protected)/index.tsx @@ -1,10 +1,12 @@ import { Text, View } from "react-native"; +import { SignoutButton } from "@/fetaure/auth/components/SignoutButton"; + export default function Index() { return ( - - Hello World, Linpe + Hello World, Linpe + ); } diff --git a/app/_layout.tsx b/app/_layout.tsx index 3112b2bf..e0eb027a 100644 --- a/app/_layout.tsx +++ b/app/_layout.tsx @@ -1,7 +1,46 @@ -import { Stack } from "expo-router"; +// app/_layout.jsx +import { SafeAreaView, Text } from "react-native"; +import { Slot, useRouter, useSegments } from "expo-router"; import "../assets/styles/global.css"; +import { useEffect, useState } from "react"; +import { type Session } from "@supabase/supabase-js"; + +import supabase from "@/lib/supabase"; + export default function RootLayout() { - return ; + const [session, setSession] = useState(null); + const segments = useSegments(); + const router = useRouter(); + + useEffect(() => { + supabase.auth.getSession().then(({ data: { session } }) => { + setSession(session); + }); + + supabase.auth.onAuthStateChange((_event, session) => { + setSession(session); + }); + }, []); + + useEffect(() => { + const inAuthGroup = segments[0] === "(auth)"; + const inProtectedGroup = segments[0] === "(protected)"; + + if (session && inAuthGroup) { + // ログイン済みで認証画面にいる場合は、protectedにリダイレクト + router.replace("/(protected)"); + } else if (!session && inProtectedGroup) { + // 未ログインでprotected画面にいる場合は、認証画面にリダイレクト + router.replace("/(auth)/loginScreen"); + } + }, [session, segments, router]); + + return ( + + {session ? "ログイン済み" : "未ログイン"} + + + ); } diff --git a/components/button/PrimaryButton.tsx b/components/button/PrimaryButton.tsx new file mode 100644 index 00000000..43dede7d --- /dev/null +++ b/components/button/PrimaryButton.tsx @@ -0,0 +1,23 @@ +import { TouchableOpacity } from "react-native"; + +export const PrimaryButton = ({ + children, + onPress, + loading = false, +}: { + children: React.ReactNode; + onPress: () => void; + loading?: boolean; +}) => { + return ( + + {children} + + ); +}; diff --git a/components/link/DefaultLink.tsx b/components/link/DefaultLink.tsx new file mode 100644 index 00000000..cdf9ba0c --- /dev/null +++ b/components/link/DefaultLink.tsx @@ -0,0 +1,14 @@ +import { Link } from "expo-router"; + +type DefaultLinkProps = Pick< + React.ComponentProps, + "children" | "href" +>; + +export const DefaultLink = ({ children, href }: DefaultLinkProps) => { + return ( + + {children} + + ); +}; diff --git a/fetaure/auth/components/SignoutButton.tsx b/fetaure/auth/components/SignoutButton.tsx new file mode 100644 index 00000000..1baf538a --- /dev/null +++ b/fetaure/auth/components/SignoutButton.tsx @@ -0,0 +1,12 @@ +import { Text } from "react-native"; + +import { PrimaryButton } from "@/components/button/PrimaryButton"; +import { signout } from "../service/authService"; + +export const SignoutButton = () => { + return ( + signout()}> + Signout + + ); +}; diff --git a/fetaure/auth/screen/Login.tsx b/fetaure/auth/screen/Login.tsx new file mode 100644 index 00000000..1c2b9b3d --- /dev/null +++ b/fetaure/auth/screen/Login.tsx @@ -0,0 +1,51 @@ +import { useState } from "react"; +import { Text, TextInput, View } from "react-native"; + +import { PrimaryButton } from "@/components/button/PrimaryButton"; +import { DefaultLink } from "@/components/link/DefaultLink"; +import { loginWithEmail } from "../service/authService"; + +export const Login = () => { + const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); + const [loading, setLoading] = useState(false); + + return ( + + Login + + Email + + + + Password + + + loginWithEmail(email, password, setLoading)} + loading={loading} + > + Login + + + Signup + + + ); +}; diff --git a/fetaure/auth/screen/Signup.tsx b/fetaure/auth/screen/Signup.tsx new file mode 100644 index 00000000..a4010c77 --- /dev/null +++ b/fetaure/auth/screen/Signup.tsx @@ -0,0 +1,51 @@ +import { useState } from "react"; +import { Text, TextInput, View } from "react-native"; + +import { PrimaryButton } from "@/components/button/PrimaryButton"; +import { DefaultLink } from "@/components/link/DefaultLink"; +import { signupWithEmail } from "../service/authService"; + +export const Signup = () => { + const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); + const [loading, setLoading] = useState(false); + + return ( + + Signup + + Email + + + + Password + + + signupWithEmail(email, password, setLoading)} + loading={loading} + > + Signup + + + Login + + + ); +}; diff --git a/fetaure/auth/service/authService.ts b/fetaure/auth/service/authService.ts new file mode 100644 index 00000000..14a76f8d --- /dev/null +++ b/fetaure/auth/service/authService.ts @@ -0,0 +1,42 @@ +import { Alert } from "react-native"; + +import supabase from "@/lib/supabase"; + +export const loginWithEmail = async ( + email: string, + password: string, + setLoading: (loading: boolean) => void, +) => { + setLoading(true); + const { error } = await supabase.auth.signInWithPassword({ + email, + password, + }); + + if (error) Alert.alert(error.message); + + setLoading(false); +}; + +export const signupWithEmail = async ( + email: string, + password: string, + setLoading: (loading: boolean) => void, +) => { + setLoading(true); + const { + data: { session }, + error, + } = await supabase.auth.signUp({ + email, + password, + }); + if (error) Alert.alert(error.message); + if (!session) Alert.alert("Please check your inbox for email verification!"); + setLoading(false); +}; + +export const signout = async () => { + const { error } = await supabase.auth.signOut(); + if (error) Alert.alert(error.message); +}; diff --git a/lib/supabase.ts b/lib/supabase.ts index 86991d68..48eb6e46 100644 --- a/lib/supabase.ts +++ b/lib/supabase.ts @@ -62,8 +62,9 @@ class LargeSecureStore { } } -const supabaseUrl = process.env.SUPABASE_URL; -const supabaseAnonKey = process.env.SUPABASE_ANON_KEY; +const supabaseUrl = "https://lxuurozqrpthyvxvnaml.supabase.co"; +const supabaseAnonKey = + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Imx4dXVyb3pxcnB0aHl2eHZuYW1sIiwicm9sZSI6ImFub24iLCJpYXQiOjE3MzkwNzg0NjYsImV4cCI6MjA1NDY1NDQ2Nn0.m8YcHx4RNlRRi4-GsnQTg5h_X_WfRlSY7iYKVmhZCTU"; const supabase = createClient(supabaseUrl || "", supabaseAnonKey || "", { auth: { diff --git a/tailwind.config.js b/tailwind.config.js index 090d7c16..d8e8aca9 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -1,7 +1,11 @@ /** @type {import('tailwindcss').Config} */ module.exports = { // NOTE: Update this to include the paths to all of your component files. - content: ["./app/**/*.{js,jsx,ts,tsx}", "./app/*.{js,jsx,ts,tsx}"], + content: [ + "./app/**/*.{js,jsx,ts,tsx}", + "./app/*.{js,jsx,ts,tsx}", + "./fetaure/**/*.{js,jsx,ts,tsx}", + ], presets: [require("nativewind/preset")], theme: { extend: {},