diff --git a/app/(api)/user+api.ts b/app/(api)/user+api.ts index 8bfbcfd..f82e27d 100644 --- a/app/(api)/user+api.ts +++ b/app/(api)/user+api.ts @@ -1,5 +1,26 @@ import { neon } from "@neondatabase/serverless"; +export async function GET(req: Request) { + const sql = neon(process.env.DATABASE_URL!); + const clerkId = new URL(req.url).searchParams.get("clerkId"); + + if (!clerkId) { + return Response.json({ error: "Missing clerkId" }, { status: 400 }); + } + + try { + const response = await sql` + SELECT id, name, email, role FROM users WHERE clerk_id = ${clerkId} + `; + + return Response.json({ data: response[0] ?? null }); + } catch (error) { + console.log("[GET_USER]: ", error); + + return Response.json({ error }, { status: 500 }); + } +} + export async function POST(req: Request) { const sql = neon(process.env.DATABASE_URL!); const { name, email, clerkId } = await req.json(); @@ -36,3 +57,31 @@ export async function POST(req: Request) { return Response.json({ error }, { status: 500 }); } } + +export async function PATCH(req: Request) { + const sql = neon(process.env.DATABASE_URL!); + const { clerkId, role } = await req.json(); + + if (!clerkId || !["rider", "driver"].includes(role)) { + return Response.json( + { error: "Missing clerkId or invalid role." }, + { status: 400 }, + ); + } + + try { + const response = await sql` + UPDATE users SET role = ${role} WHERE clerk_id = ${clerkId} RETURNING id, role + `; + + if (response.length === 0) { + return Response.json({ error: "User not found." }, { status: 404 }); + } + + return Response.json({ data: response[0] }); + } catch (error) { + console.log("[PATCH_USER]: ", error); + + return Response.json({ error }, { status: 500 }); + } +} diff --git a/app/(auth)/sign-up.tsx b/app/(auth)/sign-up.tsx index 9e85d26..9500a15 100644 --- a/app/(auth)/sign-up.tsx +++ b/app/(auth)/sign-up.tsx @@ -236,7 +236,7 @@ const SignUp = () => { router.push("/(root)/(tabs)/home")} + onPress={() => router.push("/")} className="mt-5" /> diff --git a/app/(root)/(tabs)/home.tsx b/app/(root)/(tabs)/home.tsx index 5d485e1..f538675 100644 --- a/app/(root)/(tabs)/home.tsx +++ b/app/(root)/(tabs)/home.tsx @@ -48,24 +48,37 @@ const Home = () => { useEffect(() => { const requestLocation = async () => { - let { status } = await Location.requestForegroundPermissionsAsync(); + try { + let { status } = await Location.requestForegroundPermissionsAsync(); - if (status !== "granted") return setHasPermissions(false); + if (status !== "granted") return setHasPermissions(false); - setHasPermissions(true); + setHasPermissions(true); - let location = await Location.getCurrentPositionAsync(); + let location = await Location.getCurrentPositionAsync(); - const address = await Location.reverseGeocodeAsync({ - longitude: location.coords?.longitude, - latitude: location.coords?.latitude, - }); + let addressText = "Unknown location"; + try { + const address = await Location.reverseGeocodeAsync({ + longitude: location.coords?.longitude, + latitude: location.coords?.latitude, + }); + if (address[0]) { + addressText = `${address[0].name}, ${address[0].region}`; + } + } catch (geocodeErr) { + console.log("[REVERSE_GEOCODE]: ", geocodeErr); + } - setUserLocation({ - latitude: location.coords.latitude, - longitude: location.coords.longitude, - address: `${address[0].name}, ${address[0].region}`, - }); + setUserLocation({ + latitude: location.coords.latitude, + longitude: location.coords.longitude, + address: addressText, + }); + } catch (err) { + console.log("[LOCATION]: ", err); + setHasPermissions(false); + } }; requestLocation(); @@ -143,7 +156,16 @@ const Home = () => { - {hasPermissions && } + {hasPermissions ? ( + + ) : ( + + + Location access is off.{"\n"}Enable it in your device + settings to see nearby drivers. + + + )} diff --git a/app/(root)/_layout.tsx b/app/(root)/_layout.tsx index 01142a2..6d7fee3 100644 --- a/app/(root)/_layout.tsx +++ b/app/(root)/_layout.tsx @@ -7,6 +7,11 @@ const RootLayout = () => { + + ); }; diff --git a/app/(root)/driver-home.tsx b/app/(root)/driver-home.tsx new file mode 100644 index 0000000..625bd05 --- /dev/null +++ b/app/(root)/driver-home.tsx @@ -0,0 +1,41 @@ +import { useClerk, useUser } from "@clerk/clerk-expo"; +import { Image, Text, View } from "react-native"; +import { SafeAreaView } from "react-native-safe-area-context"; + +import { CustomButton } from "@/components/custom-button"; +import { images } from "@/constants"; + +// Placeholder driver home. The driver experience (going online, accepting +// rides) is not built yet — drivers are registered here and managed in the +// database for now. +const DriverHome = () => { + const { user } = useUser(); + const { signOut } = useClerk(); + + return ( + + Registered + + + You're registered as a driver, {user?.firstName || "there"}! + + + + Driver mode is coming soon. We'll contact you at{" "} + {user?.emailAddresses[0]?.emailAddress} once your account is activated. + + + signOut()} + className="mt-10" + /> + + ); +}; + +export default DriverHome; diff --git a/app/(root)/role.tsx b/app/(root)/role.tsx new file mode 100644 index 0000000..20131ee --- /dev/null +++ b/app/(root)/role.tsx @@ -0,0 +1,77 @@ +import { useUser } from "@clerk/clerk-expo"; +import { router } from "expo-router"; +import { useState } from "react"; +import { Alert, Text, TouchableOpacity, View } from "react-native"; +import { SafeAreaView } from "react-native-safe-area-context"; + +import { fetchAPI } from "@/lib/fetch"; + +const RoleSelection = () => { + const { user } = useUser(); + const [saving, setSaving] = useState(false); + + const chooseRole = async (role: "rider" | "driver") => { + if (!user?.id || saving) return; + + setSaving(true); + + try { + const { error } = await fetchAPI("/(api)/user", { + method: "PATCH", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ clerkId: user.id, role }), + }); + + if (error) throw new Error(error); + + router.replace( + role === "driver" ? "/(root)/driver-home" : "/(root)/(tabs)/home", + ); + } catch (err) { + console.log("[ROLE_SELECT]: ", err); + Alert.alert("Error", "Could not save your choice. Please try again."); + } finally { + setSaving(false); + } + }; + + return ( + + + How will you use Waseel? + + + + You can change this later by contacting support. + + + chooseRole("rider")} + disabled={saving} + className="bg-primary-500 rounded-2xl p-7 mb-5 items-center" + > + 🧍 + + I'm a Rider + + + Book rides and get around Lebanon + + + + chooseRole("driver")} + disabled={saving} + className="bg-general-600 rounded-2xl p-7 items-center" + > + 🚗 + I'm a Driver + + Give rides and earn money + + + + ); +}; + +export default RoleSelection; diff --git a/app/index.tsx b/app/index.tsx index 9bfbb92..0aa4b6a 100644 --- a/app/index.tsx +++ b/app/index.tsx @@ -1,12 +1,38 @@ import { useAuth } from "@clerk/clerk-expo"; import { Redirect } from "expo-router"; +import { useEffect, useState } from "react"; +import { ActivityIndicator, View } from "react-native"; + +import { fetchAPI } from "@/lib/fetch"; const App = () => { - const { isSignedIn } = useAuth(); + const { isSignedIn, userId } = useAuth(); + const [role, setRole] = useState(undefined); - if (isSignedIn) return ; + useEffect(() => { + if (!isSignedIn || !userId) return; - return ; + fetchAPI(`/(api)/user?clerkId=${userId}`) + .then((res) => setRole(res?.data?.role ?? null)) + .catch(() => setRole(null)); + }, [isSignedIn, userId]); + + if (!isSignedIn) return ; + + // Still loading the user's role from the database. + if (role === undefined) { + return ( + + + + ); + } + + if (role === "driver") return ; + if (role === "rider") return ; + + // Signed in but no role chosen yet. + return ; }; export default App; diff --git a/components/oauth.tsx b/components/oauth.tsx index 16713cd..0dc56ef 100644 --- a/components/oauth.tsx +++ b/components/oauth.tsx @@ -20,7 +20,7 @@ export const OAuth = ({ title }: OAuthProps) => { const result = await googleOAuth(startOAuthFlow); if (result?.code === "session_exists" || result?.code === "success") { - router.replace("/(root)/(tabs)/home"); + router.replace("/"); } } catch (err: any) { console.error("OAuth error", err); diff --git a/scripts/seed-db.mjs b/scripts/seed-db.mjs index e1a2b01..6e5e014 100644 --- a/scripts/seed-db.mjs +++ b/scripts/seed-db.mjs @@ -26,9 +26,13 @@ await sql`CREATE TABLE IF NOT EXISTS users ( name VARCHAR(255) NOT NULL, email VARCHAR(255) NOT NULL, clerk_id VARCHAR(255) NOT NULL, + role VARCHAR(20), created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP )`; +// For databases created before roles existed. +await sql`ALTER TABLE users ADD COLUMN IF NOT EXISTS role VARCHAR(20)`; + await sql`CREATE TABLE IF NOT EXISTS drivers ( id SERIAL PRIMARY KEY, first_name VARCHAR(100) NOT NULL,