From a0b297285a01ed77d7bb585de9d007c52119b902 Mon Sep 17 00:00:00 2001 From: Krikorios Date: Sun, 23 Aug 2026 16:38:41 +0300 Subject: [PATCH] Add self-hosted auth, admin API, and owner web dashboard - Replace Clerk with self-hosted JWT auth (register/login/verify, bcrypt passwords, Gmail OTP with console fallback) - Add lib/db.ts pg pool + transaction helpers; seed script migrates legacy Clerk-era schema (drop clerk_id, enforce UUID ids and unique email) - Add owner-gated admin API: stats, users, drivers CRUD, rides - Add dashboard/ Vite React owner dashboard (login, overview, users, fleet, rides) with dev-server proxy to avoid Expo CORS middleware - Add scripts/set-owner.mjs for role management --- .env.example | 19 +- app/(api)/admin/drivers+api.ts | 79 + app/(api)/admin/drivers/[id]+api.ts | 86 + app/(api)/admin/rides+api.ts | 70 + app/(api)/admin/stats+api.ts | 66 + app/(api)/admin/users+api.ts | 68 + app/(api)/admin/users/[id]+api.ts | 60 + app/(api)/auth/google+api.ts | 90 + app/(api)/auth/login+api.ts | 56 + app/(api)/auth/register+api.ts | 87 + app/(api)/auth/verify+api.ts | 64 + app/(api)/driver+api.ts | 4 +- app/(api)/ride/[id]+api.ts | 12 +- app/(api)/ride/create+api.ts | 14 +- app/(api)/user+api.ts | 67 +- app/(auth)/sign-in.tsx | 40 +- app/(auth)/sign-up.tsx | 92 +- app/(root)/(tabs)/home.tsx | 23 +- app/(root)/(tabs)/profile.tsx | 16 +- app/(root)/(tabs)/rides.tsx | 4 +- app/(root)/book-ride.tsx | 41 +- app/(root)/confirm-ride.tsx | 10 +- app/(root)/driver-home.tsx | 9 +- app/(root)/find-ride.tsx | 13 +- app/(root)/role.tsx | 10 +- app/_layout.tsx | 25 +- app/index.tsx | 24 +- components/driver-card.tsx | 2 +- components/google-text-input.tsx | 8 +- components/map.tsx | 4 +- components/oauth.tsx | 54 +- components/payment.tsx | 100 +- components/ride-card.tsx | 18 +- config/index.ts | 3 - dashboard/.gitignore | 24 + dashboard/.oxlintrc.json | 8 + dashboard/README.md | 32 + dashboard/index.html | 13 + dashboard/package-lock.json | 1225 ++++++++++ dashboard/package.json | 25 + dashboard/public/favicon.svg | 1 + dashboard/public/icons.svg | 24 + dashboard/src/App.tsx | 64 + dashboard/src/Login.tsx | 53 + dashboard/src/assets/hero.png | Bin 0 -> 13057 bytes dashboard/src/assets/vite.svg | 1 + dashboard/src/index.css | 250 ++ dashboard/src/lib/api.ts | 50 + dashboard/src/main.tsx | 10 + dashboard/src/pages/Drivers.tsx | 199 ++ dashboard/src/pages/Rides.tsx | 88 + dashboard/src/pages/Stats.tsx | 82 + dashboard/src/pages/Users.tsx | 124 + dashboard/tsconfig.app.json | 26 + dashboard/tsconfig.json | 7 + dashboard/tsconfig.node.json | 23 + dashboard/vite.config.ts | 25 + environment.d.ts | 21 +- lib/admin.ts | 37 + lib/auth.ts | 106 +- lib/db.ts | 67 + lib/fetch.ts | 21 +- lib/jwt.ts | 100 + lib/mailer.ts | 96 + lib/map.ts | 34 +- lib/password.ts | 24 + lib/pricing.ts | 34 + lib/session.tsx | 157 ++ lib/users.ts | 39 + lib/utils.ts | 15 +- package-lock.json | 3423 ++++++++------------------- package.json | 7 +- scripts/seed-db.mjs | 77 +- scripts/set-owner.mjs | 44 + tsconfig.json | 1 + 75 files changed, 5158 insertions(+), 2837 deletions(-) create mode 100644 app/(api)/admin/drivers+api.ts create mode 100644 app/(api)/admin/drivers/[id]+api.ts create mode 100644 app/(api)/admin/rides+api.ts create mode 100644 app/(api)/admin/stats+api.ts create mode 100644 app/(api)/admin/users+api.ts create mode 100644 app/(api)/admin/users/[id]+api.ts create mode 100644 app/(api)/auth/google+api.ts create mode 100644 app/(api)/auth/login+api.ts create mode 100644 app/(api)/auth/register+api.ts create mode 100644 app/(api)/auth/verify+api.ts delete mode 100644 config/index.ts create mode 100644 dashboard/.gitignore create mode 100644 dashboard/.oxlintrc.json create mode 100644 dashboard/README.md create mode 100644 dashboard/index.html create mode 100644 dashboard/package-lock.json create mode 100644 dashboard/package.json create mode 100644 dashboard/public/favicon.svg create mode 100644 dashboard/public/icons.svg create mode 100644 dashboard/src/App.tsx create mode 100644 dashboard/src/Login.tsx create mode 100644 dashboard/src/assets/hero.png create mode 100644 dashboard/src/assets/vite.svg create mode 100644 dashboard/src/index.css create mode 100644 dashboard/src/lib/api.ts create mode 100644 dashboard/src/main.tsx create mode 100644 dashboard/src/pages/Drivers.tsx create mode 100644 dashboard/src/pages/Rides.tsx create mode 100644 dashboard/src/pages/Stats.tsx create mode 100644 dashboard/src/pages/Users.tsx create mode 100644 dashboard/tsconfig.app.json create mode 100644 dashboard/tsconfig.json create mode 100644 dashboard/tsconfig.node.json create mode 100644 dashboard/vite.config.ts create mode 100644 lib/admin.ts create mode 100644 lib/db.ts create mode 100644 lib/jwt.ts create mode 100644 lib/mailer.ts create mode 100644 lib/password.ts create mode 100644 lib/pricing.ts create mode 100644 lib/session.tsx create mode 100644 lib/users.ts create mode 100644 scripts/set-owner.mjs diff --git a/.env.example b/.env.example index a3a9d33..d4e9cdd 100644 --- a/.env.example +++ b/.env.example @@ -1,14 +1,25 @@ # .env -# clerk publishable key -EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_YOUR_KEY_HERE +# jwt secret for self-hosted auth sessions (generate with: openssl rand -hex 32) +AUTH_JWT_SECRET=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX -# postgres db url (neon db) -DATABASE_URL="postgresql://username:password@hostname:port/uber-clone?sslmode=require" +# postgres db url (self-hosted, e.g. postgresql://user:password@localhost:5432/waseel) +DATABASE_URL="postgresql://username:password@hostname:port/waseel" # expo api server url (you can set it to any random url for development) EXPO_PUBLIC_SERVER_URL="https://example.com/" +# google oauth client ids (from Google Cloud console, type "Web/iOS/Android") +EXPO_PUBLIC_GOOGLE_AUTH_WEB_CLIENT_ID=XXXXXXXX.apps.googleusercontent.com +EXPO_PUBLIC_GOOGLE_AUTH_IOS_CLIENT_ID=XXXXXXXX.apps.googleusercontent.com +EXPO_PUBLIC_GOOGLE_AUTH_ANDROID_CLIENT_ID=XXXXXXXX.apps.googleusercontent.com + +# gmail api (oauth refresh token with gmail.send scope; leave blank to log codes to server console) +GMAIL_CLIENT_ID= +GMAIL_CLIENT_SECRET= +GMAIL_REFRESH_TOKEN= +GMAIL_FROM="Waseel " + # geoapify api key EXPO_PUBLIC_GEOAPIFY_API_KEY=XXXXXXXXXXXXXXXXXXXXXXXXXXXXX diff --git a/app/(api)/admin/drivers+api.ts b/app/(api)/admin/drivers+api.ts new file mode 100644 index 0000000..8438940 --- /dev/null +++ b/app/(api)/admin/drivers+api.ts @@ -0,0 +1,79 @@ +import { requireOwner, withCors, preflight } from "@/lib/admin"; +import { sql } from "@/lib/db"; + +export async function OPTIONS() { + return preflight(); +} + +export async function GET(request: Request) { + const auth = await requireOwner(request); + if ("error" in auth) return withCors(auth.error); + + try { + const rows = await sql` + SELECT + d.*, + (SELECT COUNT(*)::int FROM rides r WHERE r.driver_id = d.id) AS total_rides, + COALESCE(( + SELECT SUM(r.fare_price)::int FROM rides r + WHERE r.driver_id = d.id AND r.payment_status = 'paid' + ), 0) AS revenue + FROM drivers d + ORDER BY d.id + `; + + return withCors(Response.json({ data: rows })); + } catch (error) { + console.error("[ADMIN_DRIVERS]: ", error); + return withCors( + Response.json({ error: "Internal Server Error" }, { status: 500 }), + ); + } +} + +type DriverBody = { + first_name?: string; + last_name?: string; + profile_image_url?: string; + car_image_url?: string; + car_seats?: number; + rating?: number; +}; + +export async function POST(request: Request) { + const auth = await requireOwner(request); + if ("error" in auth) return withCors(auth.error); + + try { + const body = (await request.json()) as DriverBody; + + if (!body.first_name?.trim() || !body.last_name?.trim()) { + return withCors( + Response.json( + { error: "first_name and last_name are required." }, + { status: 400 }, + ), + ); + } + + const [driver] = await sql` + INSERT INTO drivers + (first_name, last_name, profile_image_url, car_image_url, car_seats, rating) + VALUES + (${body.first_name.trim()}, + ${body.last_name.trim()}, + ${body.profile_image_url ?? null}, + ${body.car_image_url ?? null}, + ${body.car_seats ?? 4}, + ${body.rating ?? 4.5}) + RETURNING * + `; + + return withCors(Response.json({ data: driver }, { status: 201 })); + } catch (error) { + console.error("[ADMIN_DRIVER_CREATE]: ", error); + return withCors( + Response.json({ error: "Internal Server Error" }, { status: 500 }), + ); + } +} diff --git a/app/(api)/admin/drivers/[id]+api.ts b/app/(api)/admin/drivers/[id]+api.ts new file mode 100644 index 0000000..e7f8fd2 --- /dev/null +++ b/app/(api)/admin/drivers/[id]+api.ts @@ -0,0 +1,86 @@ +import { requireOwner, withCors, preflight } from "@/lib/admin"; +import { sql } from "@/lib/db"; + +export async function OPTIONS() { + return preflight(); +} + +type DriverBody = { + first_name?: string; + last_name?: string; + profile_image_url?: string; + car_image_url?: string; + car_seats?: number; + rating?: number; +}; + +export async function PATCH(request: Request, { id }: { id: string }) { + const auth = await requireOwner(request); + if ("error" in auth) return withCors(auth.error); + + try { + const body = (await request.json()) as DriverBody; + + const rows = await sql` + UPDATE drivers SET + first_name = COALESCE(${body.first_name ?? null}, first_name), + last_name = COALESCE(${body.last_name ?? null}, last_name), + profile_image_url = COALESCE(${body.profile_image_url ?? null}, profile_image_url), + car_image_url = COALESCE(${body.car_image_url ?? null}, car_image_url), + car_seats = COALESCE(${body.car_seats ?? null}, car_seats), + rating = COALESCE(${body.rating ?? null}, rating) + WHERE id = ${id} + RETURNING * + `; + + if (!rows[0]) { + return withCors( + Response.json({ error: "Driver not found." }, { status: 404 }), + ); + } + + return withCors(Response.json({ data: rows[0] })); + } catch (error) { + console.error("[ADMIN_DRIVER_PATCH]: ", error); + return withCors( + Response.json({ error: "Internal Server Error" }, { status: 500 }), + ); + } +} + +export async function DELETE(request: Request, { id }: { id: string }) { + const auth = await requireOwner(request); + if ("error" in auth) return withCors(auth.error); + + try { + const used = await sql<{ n: number }>` + SELECT COUNT(*)::int AS n FROM rides WHERE driver_id = ${id} + `; + + if (used[0].n > 0) { + return withCors( + Response.json( + { error: "Driver has recorded rides and cannot be deleted." }, + { status: 409 }, + ), + ); + } + + const rows = await sql` + DELETE FROM drivers WHERE id = ${id} RETURNING id + `; + + if (!rows[0]) { + return withCors( + Response.json({ error: "Driver not found." }, { status: 404 }), + ); + } + + return withCors(Response.json({ data: rows[0] })); + } catch (error) { + console.error("[ADMIN_DRIVER_DELETE]: ", error); + return withCors( + Response.json({ error: "Internal Server Error" }, { status: 500 }), + ); + } +} diff --git a/app/(api)/admin/rides+api.ts b/app/(api)/admin/rides+api.ts new file mode 100644 index 0000000..29fb019 --- /dev/null +++ b/app/(api)/admin/rides+api.ts @@ -0,0 +1,70 @@ +import { requireOwner, withCors, preflight } from "@/lib/admin"; +import { sql } from "@/lib/db"; + +export async function OPTIONS() { + return preflight(); +} + +export async function GET(request: Request) { + const auth = await requireOwner(request); + if ("error" in auth) return withCors(auth.error); + + try { + const url = new URL(request.url); + const status = url.searchParams.get("status")?.trim().toLowerCase() ?? ""; + + const rows = status + ? await sql` + SELECT + r.ride_id, + r.origin_address, + r.destination_address, + r.ride_time, + r.fare_price, + r.payment_status, + r.created_at, + u.id AS user_id, + u.email AS user_email, + json_build_object( + 'driver_id', d.id, + 'name', d.first_name || ' ' || d.last_name, + 'rating', d.rating + ) AS driver + FROM rides r + INNER JOIN drivers d ON d.id = r.driver_id + INNER JOIN users u ON u.id = r.user_id + WHERE LOWER(r.payment_status) = ${status} + ORDER BY r.created_at DESC + LIMIT 500 + ` + : await sql` + SELECT + r.ride_id, + r.origin_address, + r.destination_address, + r.ride_time, + r.fare_price, + r.payment_status, + r.created_at, + u.id AS user_id, + u.email AS user_email, + json_build_object( + 'driver_id', d.id, + 'name', d.first_name || ' ' || d.last_name, + 'rating', d.rating + ) AS driver + FROM rides r + INNER JOIN drivers d ON d.id = r.driver_id + INNER JOIN users u ON u.id = r.user_id + ORDER BY r.created_at DESC + LIMIT 500 + `; + + return withCors(Response.json({ data: rows })); + } catch (error) { + console.error("[ADMIN_RIDES]: ", error); + return withCors( + Response.json({ error: "Internal Server Error" }, { status: 500 }), + ); + } +} diff --git a/app/(api)/admin/stats+api.ts b/app/(api)/admin/stats+api.ts new file mode 100644 index 0000000..5f94846 --- /dev/null +++ b/app/(api)/admin/stats+api.ts @@ -0,0 +1,66 @@ +import { requireOwner, withCors, preflight } from "@/lib/admin"; +import { sql } from "@/lib/db"; + +export async function OPTIONS() { + return preflight(); +} + +export async function GET(request: Request) { + const auth = await requireOwner(request); + if ("error" in auth) return withCors(auth.error); + + try { + const [totals] = await sql<{ + users: number; + drivers: number; + rides: number; + revenue: number; + }>` + SELECT + (SELECT COUNT(*)::int FROM users) AS users, + (SELECT COUNT(*)::int FROM drivers) AS drivers, + (SELECT COUNT(*)::int FROM rides) AS rides, + (SELECT COALESCE(SUM(fare_price), 0)::int FROM rides WHERE payment_status = 'paid') AS revenue + `; + + const trend = await sql<{ day: string; rides: number; revenue: number }>` + SELECT + TO_CHAR(DAY, 'YYYY-MM-DD') AS day, + COUNT(r.ride_id)::int AS rides, + COALESCE(SUM(r.fare_price) FILTER (WHERE r.payment_status = 'paid'), 0)::int AS revenue + FROM generate_series( + CURRENT_DATE - INTERVAL '13 days', + CURRENT_DATE, + INTERVAL '1 day' + ) AS DAY + LEFT JOIN rides r ON r.created_at >= DAY AND r.created_at < DAY + INTERVAL '1 day' + GROUP BY DAY + ORDER BY DAY + `; + + const topDrivers = await sql<{ + driver_id: number; + name: string; + rides: number; + revenue: number; + }>` + SELECT + d.id AS driver_id, + d.first_name || ' ' || d.last_name AS name, + COUNT(r.ride_id)::int AS rides, + COALESCE(SUM(r.fare_price) FILTER (WHERE r.payment_status = 'paid'), 0)::int AS revenue + FROM drivers d + LEFT JOIN rides r ON r.driver_id = d.id + GROUP BY d.id, d.first_name, d.last_name + ORDER BY revenue DESC, rides DESC + LIMIT 5 + `; + + return withCors(Response.json({ data: { totals, trend, topDrivers } })); + } catch (error) { + console.error("[ADMIN_STATS]: ", error); + return withCors( + Response.json({ error: "Internal Server Error" }, { status: 500 }), + ); + } +} diff --git a/app/(api)/admin/users+api.ts b/app/(api)/admin/users+api.ts new file mode 100644 index 0000000..7d8388e --- /dev/null +++ b/app/(api)/admin/users+api.ts @@ -0,0 +1,68 @@ +import { requireOwner, withCors, preflight } from "@/lib/admin"; +import { sql } from "@/lib/db"; + +export async function OPTIONS() { + return preflight(); +} + +export async function GET(request: Request) { + const auth = await requireOwner(request); + if ("error" in auth) return withCors(auth.error); + + try { + const url = new URL(request.url); + const search = url.searchParams.get("q")?.trim().toLowerCase() ?? ""; + + const rows = search + ? await sql<{ + id: string; + name: string; + email: string; + role: string | null; + email_verified: boolean; + created_at: string; + rides: number; + }>` + SELECT + u.id, + u.name, + u.email, + u.role, + u.email_verified, + u.created_at, + (SELECT COUNT(*)::int FROM rides r WHERE r.user_id = u.id) AS rides + FROM users u + WHERE (LOWER(u.email) LIKE ${`%${search}%`} OR LOWER(u.name) LIKE ${`%${search}%`}) + ORDER BY u.created_at DESC + LIMIT 500 + ` + : await sql<{ + id: string; + name: string; + email: string; + role: string | null; + email_verified: boolean; + created_at: string; + rides: number; + }>` + SELECT + u.id, + u.name, + u.email, + u.role, + u.email_verified, + u.created_at, + (SELECT COUNT(*)::int FROM rides r WHERE r.user_id = u.id) AS rides + FROM users u + ORDER BY u.created_at DESC + LIMIT 500 + `; + + return withCors(Response.json({ data: rows })); + } catch (error) { + console.error("[ADMIN_USERS]: ", error); + return withCors( + Response.json({ error: "Internal Server Error" }, { status: 500 }), + ); + } +} diff --git a/app/(api)/admin/users/[id]+api.ts b/app/(api)/admin/users/[id]+api.ts new file mode 100644 index 0000000..c2dd4a0 --- /dev/null +++ b/app/(api)/admin/users/[id]+api.ts @@ -0,0 +1,60 @@ +import { requireOwner, withCors, preflight } from "@/lib/admin"; +import { sql } from "@/lib/db"; + +type Body = { + role?: string | null; + email_verified?: boolean; +}; + +export async function OPTIONS() { + return preflight(); +} + +export async function PATCH(request: Request, { id }: { id: string }) { + const auth = await requireOwner(request); + if ("error" in auth) return withCors(auth.error); + + try { + const body = (await request.json()) as Body; + + if (body.role !== undefined) { + const allowed = ["rider", "driver", "owner", null]; + if (!allowed.includes(body.role)) { + return withCors( + Response.json( + { error: "Role must be rider, driver, owner or null." }, + { status: 400 }, + ), + ); + } + + if (id === auth.userId && body.role !== "owner") { + return withCors( + Response.json( + { error: "You cannot remove your own owner role." }, + { status: 400 }, + ), + ); + } + } + + const rows = await sql<{ id: string; role: string | null; email_verified: boolean }>` + UPDATE users SET + role = COALESCE(${body.role ?? null}, role), + email_verified = COALESCE(${body.email_verified ?? null}, email_verified) + WHERE id = ${id} + RETURNING id, role, email_verified + `; + + if (!rows[0]) { + return withCors(Response.json({ error: "User not found." }, { status: 404 })); + } + + return withCors(Response.json({ data: rows[0] })); + } catch (error) { + console.error("[ADMIN_USER_PATCH]: ", error); + return withCors( + Response.json({ error: "Internal Server Error" }, { status: 500 }), + ); + } +} diff --git a/app/(api)/auth/google+api.ts b/app/(api)/auth/google+api.ts new file mode 100644 index 0000000..654ba1b --- /dev/null +++ b/app/(api)/auth/google+api.ts @@ -0,0 +1,90 @@ +import { sql } from "@/lib/db"; +import { issueSession, toProfile } from "@/lib/users"; + +const TOKENINFO_URL = "https://oauth2.googleapis.com/tokeninfo?id_token="; + +type GoogleTokenInfo = { + aud?: string; + sub?: string; + email?: string; + email_verified?: string | boolean; + name?: string; + exp?: string; + error_description?: string; +}; + +export async function POST(req: Request) { + const { idToken } = await req.json(); + + if (!idToken || typeof idToken !== "string") { + return Response.json({ error: "Missing idToken." }, { status: 400 }); + } + + const audience = process.env.GOOGLE_OAUTH_CLIENT_ID ?? process.env.EXPO_PUBLIC_GOOGLE_AUTH_WEB_CLIENT_ID; + + if (!audience) { + return Response.json( + { error: "Server is missing GOOGLE_OAUTH_CLIENT_ID." }, + { status: 500 }, + ); + } + + try { + const response = await fetch(`${TOKENINFO_URL}${idToken}`); + + if (!response.ok) { + return Response.json( + { error: "Invalid Google token." }, + { status: 401 }, + ); + } + + const info = (await response.json()) as GoogleTokenInfo; + + if ( + info.aud !== audience || + !info.sub || + !info.email || + (info.email_verified !== true && info.email_verified !== "true") || + (info.exp && Number(info.exp) * 1000 < Date.now()) + ) { + return Response.json( + { error: "Google token failed validation." }, + { status: 401 }, + ); + } + + const name = info.name?.trim() || info.email.split("@")[0]; + + const rows = await sql<{ + id: string; + name: string; + email: string; + role: string | null; + }>` + INSERT INTO users (name, email, google_sub, email_verified) + VALUES (${name}, ${info.email.toLowerCase()}, ${info.sub}, TRUE) + ON CONFLICT (email) DO UPDATE SET + google_sub = EXCLUDED.google_sub, + email_verified = TRUE, + name = CASE WHEN users.name = split_part(users.email, '@', 1) + THEN EXCLUDED.name ELSE users.name END + RETURNING id, name, email, role + `; + + const user = rows[0]; + + if (!user) { + return Response.json({ error: "Could not create user." }, { status: 500 }); + } + + const session = issueSession(user); + + return Response.json({ + data: { token: session.token, user: toProfile(user) }, + }); + } catch (error) { + console.error("[GOOGLE_AUTH]: ", error); + return Response.json({ error: "Internal Server Error" }, { status: 500 }); + } +} diff --git a/app/(api)/auth/login+api.ts b/app/(api)/auth/login+api.ts new file mode 100644 index 0000000..9a1b0a9 --- /dev/null +++ b/app/(api)/auth/login+api.ts @@ -0,0 +1,56 @@ +import { createHash } from "crypto"; + +import { sql } from "@/lib/db"; +import { verifyPassword } from "@/lib/password"; +import { issueSession, toProfile } from "@/lib/users"; + +export async function POST(req: Request) { + const { email, password } = await req.json(); + + if (!email?.trim() || !password) { + return Response.json( + { error: "Email and password are required." }, + { status: 400 }, + ); + } + + try { + const rows = await sql<{ + id: string; + name: string; + email: string; + role: string | null; + password_hash: string | null; + email_verified: boolean; + }>` + SELECT id, name, email, role, password_hash, email_verified + FROM users + WHERE email = ${email.trim().toLowerCase()} + `; + + const user = rows[0]; + + if (!user || !user.password_hash || !verifyPassword(password, user.password_hash)) { + return Response.json( + { error: "Invalid email or password." }, + { status: 401 }, + ); + } + + if (!user.email_verified) { + return Response.json( + { error: "Please verify your email first." }, + { status: 403 }, + ); + } + + const session = issueSession(user); + + return Response.json({ + data: { token: session.token, user: toProfile(user) }, + }); + } catch (error) { + console.error("[LOGIN]: ", error); + return Response.json({ error: "Internal Server Error" }, { status: 500 }); + } +} diff --git a/app/(api)/auth/register+api.ts b/app/(api)/auth/register+api.ts new file mode 100644 index 0000000..4d87977 --- /dev/null +++ b/app/(api)/auth/register+api.ts @@ -0,0 +1,87 @@ +import { createHash, randomInt } from "crypto"; + +import { sql } from "@/lib/db"; +import { hashPassword } from "@/lib/password"; +import { sendEmail } from "@/lib/mailer"; + +const normalizePhone = (raw: string): string => { + const cleaned = raw.replace(/[^\d+]/g, ""); + if (cleaned.startsWith("+")) return cleaned; + return `+961${cleaned.replace(/^0+/, "")}`; +}; + +const hashCode = (email: string, code: string): string => + createHash("sha256").update(`${email}:${code}`).digest("hex"); + +export async function POST(req: Request) { + const { name, email, phone, password } = await req.json(); + + if (!name?.trim() || !email?.trim() || !password) { + return Response.json( + { error: "Name, email and password are required." }, + { status: 400 }, + ); + } + + if (typeof password !== "string" || password.length < 8) { + return Response.json( + { error: "Password must be at least 8 characters." }, + { status: 400 }, + ); + } + + try { + const existing = await sql<{ id: string; email_verified: boolean }>` + SELECT id, email_verified FROM users WHERE email = ${email.trim().toLowerCase()} + `; + + if (existing[0]?.email_verified) { + return Response.json( + { error: "An account with this email already exists. Please sign in." }, + { status: 409 }, + ); + } + + // Unverified rows may be re-registered (e.g. the first mail never arrived). + await sql` + INSERT INTO users (name, email, phone, password_hash, email_verified) + VALUES ( + ${name.trim()}, + ${email.trim().toLowerCase()}, + ${phone ? normalizePhone(phone) : null}, + ${hashPassword(password)}, + FALSE + ) + ON CONFLICT (email) DO UPDATE SET + name = EXCLUDED.name, + phone = COALESCE(EXCLUDED.phone, users.phone), + password_hash = EXCLUDED.password_hash + `; + + const code = String(randomInt(0, 1_000_000)).padStart(6, "0"); + + await sql` + INSERT INTO email_verification_codes (email, code_hash, expires_at) + VALUES ( + ${email.trim().toLowerCase()}, + ${hashCode(email.trim().toLowerCase(), code)}, + CURRENT_TIMESTAMP + INTERVAL '15 minutes' + ) + ON CONFLICT (email) DO UPDATE SET + code_hash = EXCLUDED.code_hash, + expires_at = EXCLUDED.expires_at, + attempts = 0 + `; + + await sendEmail( + email.trim().toLowerCase(), + "Your Waseel verification code", + `Welcome to Waseel!\n\nYour verification code is: ${code}\n\nIt expires in 15 minutes.`, + ); + + return Response.json({ data: { sent: true } }, { status: 201 }); + } catch (error) { + console.error("[REGISTER]: ", error); + return Response.json({ error: "Internal Server Error" }, { status: 500 }); + } +} diff --git a/app/(api)/auth/verify+api.ts b/app/(api)/auth/verify+api.ts new file mode 100644 index 0000000..47b09e0 --- /dev/null +++ b/app/(api)/auth/verify+api.ts @@ -0,0 +1,64 @@ +import { createHash } from "crypto"; + +import { sql } from "@/lib/db"; +import { issueSession, toProfile } from "@/lib/users"; + +const hashCode = (email: string, code: string): string => + createHash("sha256").update(`${email}:${code}`).digest("hex"); + +export async function POST(req: Request) { + const { email, code } = await req.json(); + + if (!email?.trim() || !/^\d{6}$/.test(code ?? "")) { + return Response.json( + { error: "Email and a 6-digit code are required." }, + { status: 400 }, + ); + } + + const normalized = email.trim().toLowerCase(); + + try { + const rows = await sql<{ + id: string; + name: string; + email: string; + role: string | null; + }>` + UPDATE users SET email_verified = TRUE + WHERE email = ${normalized} + AND EXISTS ( + SELECT 1 FROM email_verification_codes + WHERE email = ${normalized} + AND code_hash = ${hashCode(normalized, code)} + AND expires_at > CURRENT_TIMESTAMP + ) + RETURNING id, name, email, role + `; + + const user = rows[0]; + + if (!user) { + await sql` + UPDATE email_verification_codes SET attempts = attempts + 1 + WHERE email = ${normalized} + `; + + return Response.json( + { error: "Invalid or expired verification code." }, + { status: 400 }, + ); + } + + await sql`DELETE FROM email_verification_codes WHERE email = ${normalized}`; + + const session = issueSession(user); + + return Response.json({ + data: { token: session.token, user: toProfile(user) }, + }); + } catch (error) { + console.error("[VERIFY]: ", error); + return Response.json({ error: "Internal Server Error" }, { status: 500 }); + } +} diff --git a/app/(api)/driver+api.ts b/app/(api)/driver+api.ts index c346b6e..5c3bdb6 100644 --- a/app/(api)/driver+api.ts +++ b/app/(api)/driver+api.ts @@ -1,9 +1,7 @@ -import { neon } from "@neondatabase/serverless"; +import { sql } from "@/lib/db"; export async function GET() { try { - const sql = neon(process.env.DATABASE_URL!); - const response = await sql`SELECT * FROM drivers`; return Response.json({ data: response }); diff --git a/app/(api)/ride/[id]+api.ts b/app/(api)/ride/[id]+api.ts index fb5853b..3de91bc 100644 --- a/app/(api)/ride/[id]+api.ts +++ b/app/(api)/ride/[id]+api.ts @@ -1,11 +1,11 @@ -import { neon } from "@neondatabase/serverless"; +import { requireAuth } from "@/lib/jwt"; +import { sql } from "@/lib/db"; export async function GET(request: Request, { id }: { id: string }) { - if (!id) - return Response.json({ error: "Missing required fields" }, { status: 400 }); + const auth = requireAuth(request); + if ("error" in auth) return auth.error; try { - const sql = neon(`${process.env.DATABASE_URL}`); const response = await sql` SELECT rides.ride_id, @@ -19,7 +19,7 @@ export async function GET(request: Request, { id }: { id: string }) { rides.fare_price, rides.payment_status, rides.created_at, - 'driver', json_build_object( + json_build_object( 'driver_id', drivers.id, 'first_name', drivers.first_name, 'last_name', drivers.last_name, @@ -33,7 +33,7 @@ export async function GET(request: Request, { id }: { id: string }) { INNER JOIN drivers ON rides.driver_id = drivers.id WHERE - rides.user_id = ${id} + rides.user_id = ${auth.userId} ORDER BY rides.created_at DESC; `; diff --git a/app/(api)/ride/create+api.ts b/app/(api)/ride/create+api.ts index c7b0987..8b32af5 100644 --- a/app/(api)/ride/create+api.ts +++ b/app/(api)/ride/create+api.ts @@ -1,6 +1,10 @@ -import { neon } from "@neondatabase/serverless"; +import { requireAuth } from "@/lib/jwt"; +import { sql } from "@/lib/db"; export async function POST(request: Request) { + const auth = requireAuth(request); + if ("error" in auth) return auth.error; + try { const body = await request.json(); const { @@ -14,7 +18,6 @@ export async function POST(request: Request) { fare_price, payment_status, driver_id, - user_id, } = body; if ( @@ -27,8 +30,7 @@ export async function POST(request: Request) { !ride_time || !fare_price || !payment_status || - !driver_id || - !user_id + !driver_id ) { return Response.json( { error: "Missing required fields" }, @@ -36,8 +38,6 @@ export async function POST(request: Request) { ); } - const sql = neon(`${process.env.DATABASE_URL}`); - const response = await sql` INSERT INTO rides ( origin_address, @@ -62,7 +62,7 @@ export async function POST(request: Request) { ${fare_price}, ${payment_status}, ${driver_id}, - ${user_id} + ${auth.userId} ) RETURNING *; `; diff --git a/app/(api)/user+api.ts b/app/(api)/user+api.ts index f82e27d..c65ed25 100644 --- a/app/(api)/user+api.ts +++ b/app/(api)/user+api.ts @@ -1,16 +1,13 @@ -import { neon } from "@neondatabase/serverless"; +import { requireAuth } from "@/lib/jwt"; +import { sql } from "@/lib/db"; 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 }); - } + const auth = requireAuth(req); + if ("error" in auth) return auth.error; try { const response = await sql` - SELECT id, name, email, role FROM users WHERE clerk_id = ${clerkId} + SELECT id, name, email, phone, role FROM users WHERE id = ${auth.userId} `; return Response.json({ data: response[0] ?? null }); @@ -21,57 +18,21 @@ export async function GET(req: Request) { } } -export async function POST(req: Request) { - const sql = neon(process.env.DATABASE_URL!); - const { name, email, clerkId } = await req.json(); - - if (!name || !email || !clerkId) { - return Response.json( - { - error: "Missing required fields!", - }, - { - status: 404, - }, - ); - } - - try { - const response = await sql` - INSERT INTO users ( - name, - email, - clerk_id - ) - VALUES ( - ${name}, - ${email}, - ${clerkId} - ) - `; - - return new Response(JSON.stringify({ data: response })); - } catch (error) { - console.log(error); - - 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(); + const auth = requireAuth(req); + if ("error" in auth) return auth.error; - if (!clerkId || !["rider", "driver"].includes(role)) { - return Response.json( - { error: "Missing clerkId or invalid role." }, - { status: 400 }, - ); + const { role } = await req.json(); + + if (!["rider", "driver"].includes(role)) { + return Response.json({ error: "Invalid role." }, { status: 400 }); } try { const response = await sql` - UPDATE users SET role = ${role} WHERE clerk_id = ${clerkId} RETURNING id, role + UPDATE users SET role = ${role} + WHERE id = ${auth.userId} + RETURNING id, role `; if (response.length === 0) { diff --git a/app/(auth)/sign-in.tsx b/app/(auth)/sign-in.tsx index 50ac5f2..1666b0a 100644 --- a/app/(auth)/sign-in.tsx +++ b/app/(auth)/sign-in.tsx @@ -1,4 +1,3 @@ -import { useSignIn } from "@clerk/clerk-expo"; import { Link, useRouter } from "expo-router"; import { useCallback, useState } from "react"; import { Alert, Image, ScrollView, Text, View } from "react-native"; @@ -7,42 +6,45 @@ import { CustomButton } from "@/components/custom-button"; import { InputField } from "@/components/input-field"; import { OAuth } from "@/components/oauth"; import { icons, images } from "@/constants"; +import { fetchAPI } from "@/lib/fetch"; +import { useSession } from "@/lib/session"; const SignIn = () => { const router = useRouter(); - const { signIn, setActive, isLoaded } = useSignIn(); + const { isLoaded, setSession } = useSession(); const [form, setForm] = useState({ email: "", password: "", }); const onSignInPress = useCallback(async () => { - if (!isLoaded) return; - try { - const signInAttempt = await signIn.create({ - identifier: form.email, - password: form.password, + const response = await fetchAPI("/(api)/auth/login", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + email: form.email, + password: form.password, + }), }); - if (signInAttempt.status === "complete") { - await setActive({ session: signInAttempt.createdSessionId }); - router.replace("/"); - } else { - Alert.alert("Error", "Invalid email or password."); - setForm((prevForm) => ({ - ...prevForm, - password: "", - })); - } + await setSession(response.data); + router.replace("/"); } catch (err: any) { - Alert.alert("Error", err?.errors[0]?.longMessage); + const status = String(err?.message ?? ""); + const message = status.includes("403") + ? "Please verify your email first." + : status.includes("401") + ? "Invalid email or password." + : "Could not sign in. Please try again."; + + Alert.alert("Error", message); setForm((prevForm) => ({ ...prevForm, password: "", })); } - }, [isLoaded, signIn, form.email, form.password, setActive, router]); + }, [isLoaded, form.email, form.password, setSession, router]); return ( diff --git a/app/(auth)/sign-up.tsx b/app/(auth)/sign-up.tsx index 9500a15..d275a32 100644 --- a/app/(auth)/sign-up.tsx +++ b/app/(auth)/sign-up.tsx @@ -1,4 +1,3 @@ -import { useSignUp } from "@clerk/clerk-expo"; import { Link, router } from "expo-router"; import { useState } from "react"; import { Alert, Image, ScrollView, Text, View } from "react-native"; @@ -9,13 +8,15 @@ import { InputField } from "@/components/input-field"; import { OAuth } from "@/components/oauth"; import { icons, images } from "@/constants"; import { fetchAPI } from "@/lib/fetch"; +import { useSession } from "@/lib/session"; const SignUp = () => { - const { isLoaded, signUp, setActive } = useSignUp(); + const { setSession } = useSession(); const [form, setForm] = useState({ name: "", email: "", + phone: "", password: "", }); @@ -26,18 +27,34 @@ const SignUp = () => { }); const onSignUpPress = async () => { - if (!isLoaded) return; + if (!form.name.trim() || !form.email.trim() || !form.password) { + Alert.alert( + "Missing information", + "Please fill in your name, email and password.", + ); + return; + } + + if (form.phone.trim() && !/^[0-9\s\-()+.]+$/.test(form.phone)) { + Alert.alert( + "Invalid phone number", + "Enter a valid Lebanese number, e.g. 70 123 456.", + ); + return; + } try { - await signUp.create({ - firstName: form.name, - lastName: "", - emailAddress: form.email, - password: form.password, + await fetchAPI("/(api)/auth/register", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + name: form.name, + email: form.email, + phone: form.phone.trim(), + password: form.password, + }), }); - await signUp.prepareEmailAddressVerification({ strategy: "email_code" }); - setVerification((prevVerification) => ({ ...prevVerification, state: "pending", @@ -52,44 +69,29 @@ const SignUp = () => { ...prevForm, password: "", })); - Alert.alert("Error", err?.errors[0]?.longMessage); + Alert.alert("Error", err?.message ?? "Could not create your account."); } }; const onPressVerify = async () => { - if (!isLoaded) return; - try { - const completeSignUp = await signUp.attemptEmailAddressVerification({ - code: verification.code, + const response = await fetchAPI("/(api)/auth/verify", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ email: form.email, code: verification.code }), }); - if (completeSignUp.status === "complete") { - await fetchAPI("/(api)/user", { - method: "POST", - body: JSON.stringify({ - name: form.name, - email: form.email, - clerkId: completeSignUp.createdUserId, - }), - }); - - await setActive({ session: completeSignUp.createdSessionId }); - setVerification((prevVerification) => ({ - ...prevVerification, - state: "success", - })); - } else { - setVerification((prevVerification) => ({ - ...prevVerification, - error: "Verification failed.", - state: "failed", - })); - } + await setSession(response.data); + setVerification((prevVerification) => ({ + ...prevVerification, + state: "success", + })); } catch (err: any) { setVerification((prevVerification) => ({ ...prevVerification, - error: err?.errors[0]?.longMessage, + error: err?.message?.includes("400") + ? "Invalid or expired verification code." + : err?.message ?? "Verification failed.", state: "failed", })); } @@ -140,6 +142,20 @@ const SignUp = () => { keyboardType="email-address" /> + + setForm((prevForm) => ({ + ...prevForm, + phone: value, + })) + } + keyboardType="phone-pad" + /> + { const { setUserLocation, setDestinationLocation } = useLocationStore(); - const { signOut } = useAuth(); - const { user } = useUser(); + const { signOut, user } = useSession(); const { data: recentRides, loading } = useFetch( `/(api)/ride/${user?.id}`, ); @@ -119,23 +117,10 @@ const Home = () => { numberOfLines={1} > Welcome{" "} - {user?.firstName || user?.emailAddresses[0].emailAddress} 👋 + {user?.name || user?.email} 👋 - - GitHub - - { - const { user } = useUser(); + const { user } = useSession(); return ( @@ -17,9 +17,7 @@ const Profile = () => { Your Avatar { { { { - const { user } = useUser(); + const { user } = useSession(); const { data: recentRides, loading } = useFetch( `/(api)/ride/${user?.id}`, ); diff --git a/app/(root)/book-ride.tsx b/app/(root)/book-ride.tsx index d068d5a..844df6b 100644 --- a/app/(root)/book-ride.tsx +++ b/app/(root)/book-ride.tsx @@ -1,14 +1,17 @@ -import { useUser } from "@clerk/clerk-expo"; +import { router } from "expo-router"; import { Image, Text, View } from "react-native"; +import { CustomButton } from "@/components/custom-button"; import { Payment } from "@/components/payment"; import { RideLayout } from "@/components/ride-layout"; import { icons } from "@/constants"; +import { formatLBP } from "@/lib/pricing"; +import { useSession } from "@/lib/session"; import { formatTime } from "@/lib/utils"; import { useDriverStore, useLocationStore } from "@/store"; const BookRide = () => { - const { user } = useUser(); + const { user } = useSession(); const { userAddress, destinationAddress } = useLocationStore(); const { drivers, selectedDriver } = useDriverStore(); @@ -16,6 +19,24 @@ const BookRide = () => { (driver) => +driver.id === selectedDriver, )[0]; + if (!driverDetails) { + return ( + + + + No driver selected.{"\n"}Please go back and choose a driver first. + + + router.replace("/(root)/confirm-ride")} + className="mt-6" + /> + + + ); + } + return ( <> @@ -54,9 +75,15 @@ const BookRide = () => { Ride Price - - ${driverDetails?.price} - + + + ${driverDetails?.price} + + + + ≈ {formatLBP(parseFloat(driverDetails?.price ?? "0"))} + + @@ -95,8 +122,8 @@ const BookRide = () => { { setSelected={() => setSelectedDriver(item.id)} /> )} + ListEmptyComponent={() => ( + + No drivers available on this route right now.{"\n"}Please try + another destination. + + )} ListFooterComponent={() => ( router.push("/(root)/book-ride")} + disabled={selectedDriver === null} + className={selectedDriver === null ? "opacity-50" : ""} /> )} diff --git a/app/(root)/driver-home.tsx b/app/(root)/driver-home.tsx index 625bd05..3ae3d18 100644 --- a/app/(root)/driver-home.tsx +++ b/app/(root)/driver-home.tsx @@ -1,16 +1,15 @@ -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"; +import { useSession } from "@/lib/session"; // 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(); + const { signOut, user } = useSession(); return ( @@ -21,12 +20,12 @@ const DriverHome = () => { /> - You're registered as a driver, {user?.firstName || "there"}! + You're registered as a driver, {user?.name || "there"}! Driver mode is coming soon. We'll contact you at{" "} - {user?.emailAddresses[0]?.emailAddress} once your account is activated. + {user?.email} once your account is activated. { const { userAddress, destinationAddress, + userLatitude, + userLongitude, + destinationLatitude, + destinationLongitude, setDestinationLocation, setUserLocation, } = useLocationStore(); + const canFind = + !!userLatitude && + !!userLongitude && + !!destinationLatitude && + !!destinationLongitude; + return ( @@ -43,7 +53,8 @@ const FindRide = () => { router.push("/(root)/confirm-ride")} - className="mt-5" + disabled={!canFind} + className={`mt-5 ${!canFind ? "opacity-50" : ""}`} /> ); diff --git a/app/(root)/role.tsx b/app/(root)/role.tsx index 20131ee..49561cd 100644 --- a/app/(root)/role.tsx +++ b/app/(root)/role.tsx @@ -1,17 +1,17 @@ -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"; +import { useSession } from "@/lib/session"; const RoleSelection = () => { - const { user } = useUser(); + const { setUserRole } = useSession(); const [saving, setSaving] = useState(false); const chooseRole = async (role: "rider" | "driver") => { - if (!user?.id || saving) return; + if (saving) return; setSaving(true); @@ -19,11 +19,13 @@ const RoleSelection = () => { const { error } = await fetchAPI("/(api)/user", { method: "PATCH", headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ clerkId: user.id, role }), + body: JSON.stringify({ role }), }); if (error) throw new Error(error); + setUserRole(role); + router.replace( role === "driver" ? "/(root)/driver-home" : "/(root)/(tabs)/home", ); diff --git a/app/_layout.tsx b/app/_layout.tsx index 337a8e9..15a957f 100644 --- a/app/_layout.tsx +++ b/app/_layout.tsx @@ -1,4 +1,3 @@ -import { ClerkProvider, ClerkLoaded } from "@clerk/clerk-expo"; import { useFonts } from "expo-font"; import { Stack } from "expo-router"; import * as SplashScreen from "expo-splash-screen"; @@ -7,7 +6,7 @@ import { useEffect } from "react"; import { LogBox } from "react-native"; import "react-native-reanimated"; -import { tokenCache } from "@/lib/auth"; +import { SessionProvider } from "@/lib/session"; // Prevent the splash screen from auto-hiding before asset loading is complete. SplashScreen.preventAutoHideAsync(); @@ -35,22 +34,16 @@ const RootLayout = () => { return null; } - const publishableKey = process.env.EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY!; - - if (!publishableKey) throw new Error("Missing Clerk Publishable Key."); - return ( - - - - - - - + + + + + + - - - + + ); }; diff --git a/app/index.tsx b/app/index.tsx index 0aa4b6a..e522f00 100644 --- a/app/index.tsx +++ b/app/index.tsx @@ -1,21 +1,35 @@ -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"; +import { useSession } from "@/lib/session"; const App = () => { - const { isSignedIn, userId } = useAuth(); + const { isLoaded, isSignedIn, user } = useSession(); const [role, setRole] = useState(undefined); useEffect(() => { - if (!isSignedIn || !userId) return; + if (!isSignedIn) return; - fetchAPI(`/(api)/user?clerkId=${userId}`) + // Prefer the role cached at sign-in; fall back to a fresh fetch. + if (user?.role !== undefined && user?.role !== null) { + setRole(user.role); + return; + } + + fetchAPI("/(api)/user") .then((res) => setRole(res?.data?.role ?? null)) .catch(() => setRole(null)); - }, [isSignedIn, userId]); + }, [isSignedIn, user]); + + if (!isLoaded) { + return ( + + + + ); + } if (!isSignedIn) return ; diff --git a/components/driver-card.tsx b/components/driver-card.tsx index 11e0983..b3cb12a 100644 --- a/components/driver-card.tsx +++ b/components/driver-card.tsx @@ -30,7 +30,7 @@ export const DriverCard = ({ Star - 4 + {item.rating} diff --git a/components/google-text-input.tsx b/components/google-text-input.tsx index 0307120..7dfaf75 100644 --- a/components/google-text-input.tsx +++ b/components/google-text-input.tsx @@ -19,7 +19,7 @@ interface Suggestion { } // Places API (New) — the legacy Places web service is unavailable to -// newer Google Cloud projects. +// newer Google Cloud projects. Results are restricted to Lebanon. const fetchSuggestions = async (input: string): Promise => { const res = await fetch( "https://places.googleapis.com/v1/places:autocomplete", @@ -29,7 +29,11 @@ const fetchSuggestions = async (input: string): Promise => { "Content-Type": "application/json", "X-Goog-Api-Key": googleApiKey, }, - body: JSON.stringify({ input, languageCode: "en" }), + body: JSON.stringify({ + input, + languageCode: "en", + includedRegionCodes: ["lb"], + }), }, ); const data = await res.json(); diff --git a/components/map.tsx b/components/map.tsx index 1a905a6..edb744f 100644 --- a/components/map.tsx +++ b/components/map.tsx @@ -45,10 +45,10 @@ export const Map = () => { setMarkers(newMarkers); } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [drivers]); + }, [drivers, userLatitude, userLongitude]); useEffect(() => { - if (markers.length > 0 && destinationLatitude && destinationLatitude) { + if (markers.length > 0 && destinationLatitude && destinationLongitude) { calculateDriverTimes({ markers, userLatitude, diff --git a/components/oauth.tsx b/components/oauth.tsx index 0dc56ef..cceb5b7 100644 --- a/components/oauth.tsx +++ b/components/oauth.tsx @@ -1,10 +1,11 @@ -import { useOAuth } from "@clerk/clerk-expo"; +import * as Google from "expo-auth-session/providers/google"; import { router } from "expo-router"; -import { useCallback } from "react"; +import { useCallback, useEffect } from "react"; import { Image, Text, View, Alert } from "react-native"; import { icons } from "@/constants"; -import { googleOAuth } from "@/lib/auth"; +import { googleAuth } from "@/lib/auth"; +import { useSession } from "@/lib/session"; import { CustomButton } from "./custom-button"; @@ -13,23 +14,41 @@ type OAuthProps = { }; export const OAuth = ({ title }: OAuthProps) => { - const { startOAuthFlow } = useOAuth({ strategy: "oauth_google" }); + const { setSession } = useSession(); - const handleGoogleOAuth = useCallback(async () => { - try { - const result = await googleOAuth(startOAuthFlow); + const [request, response, promptAsync] = Google.useIdTokenAuthRequest({ + clientId: process.env.EXPO_PUBLIC_GOOGLE_AUTH_WEB_CLIENT_ID, + iosClientId: process.env.EXPO_PUBLIC_GOOGLE_AUTH_IOS_CLIENT_ID, + androidClientId: process.env.EXPO_PUBLIC_GOOGLE_AUTH_ANDROID_CLIENT_ID, + }); - if (result?.code === "session_exists" || result?.code === "success") { - router.replace("/"); - } - } catch (err: any) { - console.error("OAuth error", err); - Alert.alert( - "Google sign-in failed", - err?.errors?.[0]?.longMessage || err?.message || "Please try again.", - ); + useEffect(() => { + if (response?.type !== "success") return; + + const idToken = response.params?.id_token; + + if (!idToken) { + Alert.alert("Google sign-in failed", "No token returned. Try again."); + return; } - }, [startOAuthFlow]); + + void (async () => { + try { + await setSession(await googleAuth(idToken)); + router.replace("/"); + } catch (err: any) { + console.error("OAuth error", err); + Alert.alert( + "Google sign-in failed", + err?.message || "Please try again.", + ); + } + })(); + }, [response, setSession]); + + const handleGoogleOAuth = useCallback(() => { + void promptAsync(); + }, [promptAsync]); return ( @@ -55,6 +74,7 @@ export const OAuth = ({ title }: OAuthProps) => { bgVariant="outline" textVariant="primary" onPress={handleGoogleOAuth} + disabled={!request} /> ); diff --git a/components/payment.tsx b/components/payment.tsx index 003baf8..4cd2099 100644 --- a/components/payment.tsx +++ b/components/payment.tsx @@ -1,17 +1,19 @@ -import { useAuth } from "@clerk/clerk-expo"; import { router } from "expo-router"; import * as WebBrowser from "expo-web-browser"; import { useState } from "react"; -import { Alert, Image, Text, View } from "react-native"; +import { Alert, Image, Text, TouchableOpacity, View } from "react-native"; import ReactNativeModal from "react-native-modal"; import { images } from "@/constants"; import { fetchAPI } from "@/lib/fetch"; +import { formatLBP } from "@/lib/pricing"; import { useLocationStore } from "@/store"; import type { PaymentProps } from "@/types/type"; import { CustomButton } from "./custom-button"; +type PaymentMethod = "cash" | "card"; + export const Payment = ({ fullName, email, @@ -27,11 +29,11 @@ export const Payment = ({ destinationAddress, destinationLongitude, } = useLocationStore(); - const { userId } = useAuth(); + const [method, setMethod] = useState("cash"); const [success, setSuccess] = useState(false); const [processing, setProcessing] = useState(false); - const recordRide = async () => { + const recordRide = async (paymentStatus: string) => { await fetchAPI("/(api)/ride/create", { method: "POST", headers: { @@ -45,15 +47,31 @@ export const Payment = ({ destination_latitude: destinationLatitude, destination_longitude: destinationLongitude, ride_time: rideTime.toFixed(0), - fare_price: parseInt(amount) * 100, // in cents - payment_status: "paid", + fare_price: Math.round(parseFloat(amount) * 100), // in cents + payment_status: paymentStatus, driver_id: driverId, - user_id: userId, }), }); }; - const payWithAreeba = async () => { + // Cash is settled directly with the driver at drop-off. + const payWithCash = async () => { + setProcessing(true); + try { + await recordRide("cash"); + setSuccess(true); + } catch (err) { + console.log("[PAYMENT]: ", err); + Alert.alert( + "Error", + "Something went wrong while booking your ride. Please try again.", + ); + } finally { + setProcessing(false); + } + }; + + const payWithCard = async () => { setProcessing(true); try { @@ -99,7 +117,7 @@ export const Payment = ({ }); if (verification.success) { - await recordRide(); + await recordRide("paid"); setSuccess(true); } else { Alert.alert( @@ -118,12 +136,66 @@ export const Payment = ({ } }; + const confirm = () => + method === "cash" + ? payWithCash() + : Alert.alert("Pay by card", `Your card will be charged $${amount}.`, [ + { text: "Cancel", style: "cancel" }, + { text: "Continue", onPress: () => void payWithCard() }, + ]); + return ( <> + + Payment Method + + + + setMethod("cash")} + className={`flex-1 items-center py-3 rounded-xl border ${ + method === "cash" + ? "bg-general-600 border-primary-500" + : "bg-white border-general-700" + }`} + > + + 💵 Cash + + + + setMethod("card")} + className={`flex-1 items-center py-3 rounded-xl border ${ + method === "card" + ? "bg-general-600 border-primary-500" + : "bg-white border-general-700" + }`} + > + + 💳 Card + + + + @@ -141,7 +213,9 @@ export const Payment = ({ Thank you for your booking.{"\n"} Your reservation has been placed. {"\n"} - Please proceed with your trip. + {method === "cash" + ? `Please have ${formatLBP(parseFloat(amount))} ready.` + : null} { + + + Fare + + + + ${(ride.fare_price / 100).toFixed(2)} + + + Payment Status - {payment_status} + {payment_status === "cash" + ? "Cash · Pay to driver" + : payment_status === "paid" + ? "Paid by card" + : payment_status} diff --git a/config/index.ts b/config/index.ts deleted file mode 100644 index 8de53ed..0000000 --- a/config/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export const LINKS = { - sourceCode: "https://github.com/sanidhyy/uber-clone", -} as const; diff --git a/dashboard/.gitignore b/dashboard/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/dashboard/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/dashboard/.oxlintrc.json b/dashboard/.oxlintrc.json new file mode 100644 index 0000000..6fa991d --- /dev/null +++ b/dashboard/.oxlintrc.json @@ -0,0 +1,8 @@ +{ + "$schema": "./node_modules/oxlint/configuration_schema.json", + "plugins": ["react", "typescript", "oxc"], + "rules": { + "react/rules-of-hooks": "error", + "react/only-export-components": ["warn", { "allowConstantExport": true }] + } +} diff --git a/dashboard/README.md b/dashboard/README.md new file mode 100644 index 0000000..d6af7e3 --- /dev/null +++ b/dashboard/README.md @@ -0,0 +1,32 @@ +# React + TypeScript + Vite + +This template provides a minimal setup to get React working in Vite with HMR and some Oxlint rules. + +Currently, two official plugins are available: + +- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Oxc](https://oxc.rs) +- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) + +## React Compiler + +The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation). + +## Expanding the Oxlint configuration + +If you are developing a production application, we recommend enabling type-aware lint rules by installing `oxlint-tsgolint` and editing `.oxlintrc.json`: + +```json +{ + "$schema": "./node_modules/oxlint/configuration_schema.json", + "plugins": ["react", "typescript", "oxc"], + "options": { + "typeAware": true + }, + "rules": { + "react/rules-of-hooks": "error", + "react/only-export-components": ["warn", { "allowConstantExport": true }] + } +} +``` + +See the [Oxlint rules documentation](https://oxc.rs/docs/guide/usage/linter/rules) for the full list of rules and categories. diff --git a/dashboard/index.html b/dashboard/index.html new file mode 100644 index 0000000..5f65ad2 --- /dev/null +++ b/dashboard/index.html @@ -0,0 +1,13 @@ + + + + + + + Waseel Owner Dashboard + + +
+ + + diff --git a/dashboard/package-lock.json b/dashboard/package-lock.json new file mode 100644 index 0000000..2ca794e --- /dev/null +++ b/dashboard/package-lock.json @@ -0,0 +1,1225 @@ +{ + "name": "dashboard", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "dashboard", + "version": "0.0.0", + "dependencies": { + "react": "^19.2.8", + "react-dom": "^19.2.8" + }, + "devDependencies": { + "@types/node": "^24.13.3", + "@types/react": "^19.2.17", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^6.0.4", + "oxlint": "^1.75.0", + "typescript": "~6.0.2", + "vite": "^8.2.0" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.146.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.146.0.tgz", + "integrity": "sha512-XC0QsnnhVe7sLIWmYmdPw7x5P0h4W8vUU3Nv1ySgWXtvCz8NizoAEpGXA0sOYoJQV2Rl13LgURAHQ5cI5ILCSA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@oxlint/binding-android-arm-eabi": { + "version": "1.79.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm-eabi/-/binding-android-arm-eabi-1.79.0.tgz", + "integrity": "sha512-TebFaaMklO/RXzTv7PucaCq9l3X6D1gA+C8H6K4njtjFOV+zWE9MKLpulcJZN9bzytbUbQIY0mZuz12nQ5Kv4Q==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-android-arm64": { + "version": "1.79.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm64/-/binding-android-arm64-1.79.0.tgz", + "integrity": "sha512-KqqnOtAVgNsPPF0YSodkFZA1O80jcKoCZCTu3bgsszxA+MrMP9TLzfXitKjEj1FmrPprKDMdRDMmY3weESO9sg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-darwin-arm64": { + "version": "1.79.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-arm64/-/binding-darwin-arm64-1.79.0.tgz", + "integrity": "sha512-BVC2nsMzqQzRDPc5RhixkZ+m1p7iH4bxRRvqkbwDXX0PlQKm1BPy8J8cRjnAFafOq2QzI+BfO3vE8w2GZ3CBag==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-darwin-x64": { + "version": "1.79.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-x64/-/binding-darwin-x64-1.79.0.tgz", + "integrity": "sha512-p6Lm+snmhGuLKL1+CpCV8L6ijkE/qJzK2H2jG9+eKJT0n31RbY4FLsdhexekgP3bLpw4Kgde+9DZuDZQ4yIInA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-freebsd-x64": { + "version": "1.79.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-freebsd-x64/-/binding-freebsd-x64-1.79.0.tgz", + "integrity": "sha512-qDMm0dXZnoHyRqSL4N4xUq82T4sqK5cbKSjvd/dF/YbMUXc2R1wEPf+vmA5S0qUmi0nwXfNbjXBtZaIqzQLIMg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-arm-gnueabihf": { + "version": "1.79.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.79.0.tgz", + "integrity": "sha512-2od7s0nuKPzqyUZAWk9KkCyGg7eI9dwFPZg+20lB15fKFkVZ0c9ZFxqPfiBAyDTlTkh9stPI0t+JlPCqMbItVA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-arm-musleabihf": { + "version": "1.79.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-1.79.0.tgz", + "integrity": "sha512-ZOQUjkzDnvlhSE3+tWC3YXx94MMl+sYMlwH+u1+YGApGHOJP/YAc8ZBRFOXZ6eOBmxtXAWuS/fBcdZr8qqNO1A==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-arm64-gnu": { + "version": "1.79.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.79.0.tgz", + "integrity": "sha512-lu158FR4nGqGeRS3BQvtG85wRgU/Fy4MD5Cxp1hzJXizGiLo6u2742wJSCDKh8cFcZntvX7fcxlq4mMmfryH1g==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-arm64-musl": { + "version": "1.79.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.79.0.tgz", + "integrity": "sha512-mbpKQeE2aflTjddaHK7MP8KP/OFbUM++lt5M635ENM8IyIdK0jm2t9pb+2v9mVVIvhF6TqA4l7F79Pll1mi+uw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-ppc64-gnu": { + "version": "1.79.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.79.0.tgz", + "integrity": "sha512-WpGNua7gaxaHnpSDeog2ji8IDHn/QLPl9LPzwkR/FvVv58vT5BcXjRXnU+wbu3N75cpeha8CdC7ho/U2OIsB4g==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-riscv64-gnu": { + "version": "1.79.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-1.79.0.tgz", + "integrity": "sha512-tK1E93A5LVzISg4ngpKJnfTs7EqtIUceGI7MQ4GyDjJiLi8wPCkEyKlj2xkyKWZ1yzkDJyLHTBJ5/iFWRdnJvg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-riscv64-musl": { + "version": "1.79.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-1.79.0.tgz", + "integrity": "sha512-qhQvUIrngXivA2A9pQ+xPCychztn/5qUv7yS3gDwXv3w7Rag+eTeeXWmRyx+t7XsW5x6LuY/8AsTq36UgFIblg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-s390x-gnu": { + "version": "1.79.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.79.0.tgz", + "integrity": "sha512-sv6AaVgU/eE6u+6WFiQVDcPPwTxP6IJMSB9k701W2r/r6Tx465e8vPvVyRxquNH4Vy6KwRNu90mVbxXJN8+5gg==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-x64-gnu": { + "version": "1.79.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.79.0.tgz", + "integrity": "sha512-iFZL02deziHslb3jEX9KdqlAkYoo4fGyotchKDzdfK1f5mxlIBeiQeHhvK3iFpuEJSB4ma/qeFn9oxPiwnhUPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-x64-musl": { + "version": "1.79.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-musl/-/binding-linux-x64-musl-1.79.0.tgz", + "integrity": "sha512-3DtZR2raqObnh7wXZoFYFd0Fw7skBvcb3f7A+/lkEiDuh8hrE6vv9b/62Qxao1a9/OeHLw/FcXlXzgsW9wTRFg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-openharmony-arm64": { + "version": "1.79.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-openharmony-arm64/-/binding-openharmony-arm64-1.79.0.tgz", + "integrity": "sha512-Oatt4GuA1WJkqzk2ozx4HrWROOi7opV3AKDw/U8qDIqeTqzsjn5K2x3REJMNjU3/KU/Bkq96Zi3CknaiDTaC/Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-win32-arm64-msvc": { + "version": "1.79.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.79.0.tgz", + "integrity": "sha512-NAgZr9Qp8nIA9rpo0JEvwiabTF/2UVqBNnupBG9X4kxXcQoScJUTi+qHhvabb9s/thgj5wQ4XcIaJvb+ZMgoKw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-win32-ia32-msvc": { + "version": "1.79.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-1.79.0.tgz", + "integrity": "sha512-+KyXjIvcpaXmWW/j9NNY5yWjrIVxaX18VyIheQy3jwc2GSYgpCr7MGI/HxIGQ/shAL5IWEKbhsqoMpAO5Stiog==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-win32-x64-msvc": { + "version": "1.79.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.79.0.tgz", + "integrity": "sha512-mEelcCMMBS57sIXh2veGMNy+pQwuGtcMxHxGIZWQ5Ba9pJ5jCCUFOZB9E2JhBaxGsURe+WGe0zJp4RVre52gpQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-android-arm-eabi": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.5.tgz", + "integrity": "sha512-DLe/i+l8ynIBY7XEQ191TeZvCoowIGa18R+dIV30GW7DiOtp74i/xX8hs8GUjW5ARV7VZuie3d6AumSmCwbeRA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.5.tgz", + "integrity": "sha512-zXcwKlQApYAOELHd8PwKDFkagYF9Wy4e0RJ+0qnzl9Pjnpj75TEG8ufv40p2J7kCEfwZAsNiuzRIyNNMWT38ig==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.5.tgz", + "integrity": "sha512-dK4QakI42nzWgJT5sm4y4y/O//D4OxM75/cH28RLV+nzIN9AY+YsbuUVrUTjlLjXR6vpyxFbSsbmNuJ6BP9sww==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.5.tgz", + "integrity": "sha512-fqSALaUu1Wjd1nK2uW2kJDWdLCc8lx1IcY+MTY26Aurfdx19anlzhqXOgCFbBFQnlFDTn4TC1/7Nz4Bl2mLP3A==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.5.tgz", + "integrity": "sha512-/vCnNxlkxs9tKxNDcyWUePpJ/PgTzxIaVhoM5SmG8UV+GR/IcPam4VYxi7GIMo7PSDuNqlJqvprqii9NqqVCMw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.5.tgz", + "integrity": "sha512-abk0NLA519LxRCszmbE0jYKuQ9YPocOXTiOXOo6Yr+YAT95VH+PtqYAjOJvGKt3viEd/x4qzabAlwd5bHOOARg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.5.tgz", + "integrity": "sha512-Y7eALiJ8lr0M2HH103Js+g7V34wf6snlpZLAsHI90uLhr3PVlNsbFVAXJC9d/V6BnPyKtpSwI+NcB/RLxsQxuA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.5.tgz", + "integrity": "sha512-xMvZgnbZg4YVnR/AX2b3oOPDTFYJvUVaJg5FedA/LuvexAtXibZQej4cnTkw3rjsJ/ggUROB64TdtETiim+FYA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.5.tgz", + "integrity": "sha512-GRjeqTUDHTo5GwntsLaAMcBahG3nlpjftXWZLN73HiYQlhwEowvarFgQnRnQZtIp4keXX7quXFbG38uPZBa2EA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.5.tgz", + "integrity": "sha512-vLNTR45F2Uwc8AufkNXPmB4VliaXs+FvcheEogIzOXzO4l+LzieXF5A/TWxLy5HtqpsRCHUfd0lPVrrdgXdLHQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.5.tgz", + "integrity": "sha512-Mgj59/HTuYeK9Gz2MA+mBWKnHsAgkBSec15ZMb1st3oIfFbX7gCjOae7GydHhzcyQi9Z/7M1QuN9bR3oFqF0jQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.5.tgz", + "integrity": "sha512-mY8AP0/ichsbhAxGnLa3d3+MwV0EfgrPND2bplI3Ym8T6R2pJ0N87bvrKVwNXmdy3jnr6eQBecdqx/HMknBmpA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.5.tgz", + "integrity": "sha512-8SLssA2oweAxyRgDp789ACfRb/3P+zNRJpzZxSizxF9m8NUDQ4+3xjo8ttjhVGGw6Qxb70oZiEtIjaKikCO7Yw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.5.tgz", + "integrity": "sha512-vGbruD5zquhoc8D9SViXgN2FBJtNdTyQ4DtG+SWiEGlJiAzoKcZ2xp+xuXCffhubVdt0NJlTZqkeRuERy7g8Cw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.5.tgz", + "integrity": "sha512-e/SXpgISz+IoqVcSSI0rx/d/he8zqLex+/rCWpnHpmVfmPIUjag9H6P7zotf0gJHwPUhQxZ/mF8tr6acebT9yw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true + }, + "node_modules/@types/node": { + "version": "24.13.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.3.tgz", + "integrity": "sha512-Dh8vAsV36ig5wa9OX4pXvMc9D3Veibfw2wix0CUwYODLD8nkj9UsLjASr49nPg+2eKzxhBV+v7L8pXvT4e639Q==", + "dev": true, + "dependencies": { + "undici-types": "~7.18.0" + } + }, + "node_modules/@types/react": { + "version": "19.2.18", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.18.tgz", + "integrity": "sha512-AnzbBERsrLKtk2XSfTbYRLjQPdy116Sty4q+T+Bp3IC4l6jNBvreVPAHmpq9qhXQM7CXZPjLVmGMw9sy+hxQ3w==", + "dev": true, + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.4.tgz", + "integrity": "sha512-Bsc+QHgp+P/F02XDzNCY9jnZNCUuLki36KT7VKrTXXLdHf+vHMNZnW1rVu5DNW/rCK+fya3DATySbLM4yhtKUw==", + "dev": true, + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.1.0.tgz", + "integrity": "sha512-qd2BzUBehkov86WFhg0JkEFEYyCLG9uPCe6qWTY/kRlss9OvJrOF2UbIWT7p+8IzZHkEu0DNGHc4HSv+JdDLsw==", + "dev": true, + "dependencies": { + "@rolldown/pluginutils": "^1.0.1" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", + "babel-plugin-react-compiler": "^1.0.0", + "oxc-transform-react": "^0.145.0", + "vite": "^8.0.0" + }, + "peerDependenciesMeta": { + "@rolldown/plugin-babel": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + }, + "oxc-transform-react": { + "optional": true + } + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/lightningcss": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", + "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", + "dev": true, + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.33.0", + "lightningcss-darwin-arm64": "1.33.0", + "lightningcss-darwin-x64": "1.33.0", + "lightningcss-freebsd-x64": "1.33.0", + "lightningcss-linux-arm-gnueabihf": "1.33.0", + "lightningcss-linux-arm64-gnu": "1.33.0", + "lightningcss-linux-arm64-musl": "1.33.0", + "lightningcss-linux-x64-gnu": "1.33.0", + "lightningcss-linux-x64-musl": "1.33.0", + "lightningcss-win32-arm64-msvc": "1.33.0", + "lightningcss-win32-x64-msvc": "1.33.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", + "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", + "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", + "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", + "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", + "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", + "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", + "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", + "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", + "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", + "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", + "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/nanoid": { + "version": "3.3.18", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", + "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/oxlint": { + "version": "1.79.0", + "resolved": "https://registry.npmjs.org/oxlint/-/oxlint-1.79.0.tgz", + "integrity": "sha512-hVJ9hq9m2unPS+Of4eJJgCPdIeCC+3DHEUX3tkmrPJr3OK2hz7PhXwgC+ZP71ZcYu8cCDEtQrqLxWNvxBppBVg==", + "dev": true, + "bin": { + "oxlint": "bin/oxlint" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/sponsors/Boshen" + }, + "optionalDependencies": { + "@oxlint/binding-android-arm-eabi": "1.79.0", + "@oxlint/binding-android-arm64": "1.79.0", + "@oxlint/binding-darwin-arm64": "1.79.0", + "@oxlint/binding-darwin-x64": "1.79.0", + "@oxlint/binding-freebsd-x64": "1.79.0", + "@oxlint/binding-linux-arm-gnueabihf": "1.79.0", + "@oxlint/binding-linux-arm-musleabihf": "1.79.0", + "@oxlint/binding-linux-arm64-gnu": "1.79.0", + "@oxlint/binding-linux-arm64-musl": "1.79.0", + "@oxlint/binding-linux-ppc64-gnu": "1.79.0", + "@oxlint/binding-linux-riscv64-gnu": "1.79.0", + "@oxlint/binding-linux-riscv64-musl": "1.79.0", + "@oxlint/binding-linux-s390x-gnu": "1.79.0", + "@oxlint/binding-linux-x64-gnu": "1.79.0", + "@oxlint/binding-linux-x64-musl": "1.79.0", + "@oxlint/binding-openharmony-arm64": "1.79.0", + "@oxlint/binding-win32-arm64-msvc": "1.79.0", + "@oxlint/binding-win32-ia32-msvc": "1.79.0", + "@oxlint/binding-win32-x64-msvc": "1.79.0" + }, + "peerDependencies": { + "oxlint-tsgolint": ">=7.0.2001", + "vite-plus": "*" + }, + "peerDependenciesMeta": { + "oxlint-tsgolint": { + "optional": true + }, + "vite-plus": { + "optional": true + } + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true + }, + "node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.26", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz", + "integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.17", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/react": { + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.8.tgz", + "integrity": "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.8.tgz", + "integrity": "sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ==", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.8" + } + }, + "node_modules/rolldown": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.5.tgz", + "integrity": "sha512-VD2IE5PUG4Oj8zz2VGykiYd5wbnjdIiSsNQb8Qu5B+noEp+A78mu2iVvpp27g8es14Tk9rofNs5Tku9iQCS4fA==", + "dev": true, + "dependencies": { + "@oxc-project/types": "=0.146.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm-eabi": "1.2.5", + "@rolldown/binding-android-arm64": "1.2.5", + "@rolldown/binding-darwin-arm64": "1.2.5", + "@rolldown/binding-darwin-x64": "1.2.5", + "@rolldown/binding-freebsd-x64": "1.2.5", + "@rolldown/binding-linux-arm-gnueabihf": "1.2.5", + "@rolldown/binding-linux-arm64-gnu": "1.2.5", + "@rolldown/binding-linux-arm64-musl": "1.2.5", + "@rolldown/binding-linux-ppc64-gnu": "1.2.5", + "@rolldown/binding-linux-s390x-gnu": "1.2.5", + "@rolldown/binding-linux-x64-gnu": "1.2.5", + "@rolldown/binding-linux-x64-musl": "1.2.5", + "@rolldown/binding-openharmony-arm64": "1.2.5", + "@rolldown/binding-win32-arm64-msvc": "1.2.5", + "@rolldown/binding-win32-x64-msvc": "1.2.5" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==" + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/typescript": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "dev": true + }, + "node_modules/vite": { + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.2.2.tgz", + "integrity": "sha512-cFKLV/PRgAUlIRm5WjMjJ86jrftzpqcgH+Us+DS8mI3CDNiH30Whrz8uHL3+MOLPAgqbMBAqWdAHAphOAM+z/Q==", + "dev": true, + "dependencies": { + "lightningcss": "^1.33.0", + "picomatch": "^4.0.5", + "postcss": "^8.5.26", + "rolldown": "~1.2.4", + "tinyglobby": "^0.2.17" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.4.0 || ^0.5.0", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + } + } +} diff --git a/dashboard/package.json b/dashboard/package.json new file mode 100644 index 0000000..15faffb --- /dev/null +++ b/dashboard/package.json @@ -0,0 +1,25 @@ +{ + "name": "dashboard", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "lint": "oxlint", + "preview": "vite preview" + }, + "dependencies": { + "react": "^19.2.8", + "react-dom": "^19.2.8" + }, + "devDependencies": { + "@types/node": "^24.13.3", + "@types/react": "^19.2.17", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^6.0.4", + "oxlint": "^1.75.0", + "typescript": "~6.0.2", + "vite": "^8.2.0" + } +} diff --git a/dashboard/public/favicon.svg b/dashboard/public/favicon.svg new file mode 100644 index 0000000..6893eb1 --- /dev/null +++ b/dashboard/public/favicon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/dashboard/public/icons.svg b/dashboard/public/icons.svg new file mode 100644 index 0000000..e952219 --- /dev/null +++ b/dashboard/public/icons.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/dashboard/src/App.tsx b/dashboard/src/App.tsx new file mode 100644 index 0000000..a61c98d --- /dev/null +++ b/dashboard/src/App.tsx @@ -0,0 +1,64 @@ +import { useState } from "react"; +import "./index.css"; +import Login from "./Login"; +import Stats from "./pages/Stats"; +import Users from "./pages/Users"; +import Drivers from "./pages/Drivers"; +import Rides from "./pages/Rides"; +import { clearToken, getToken } from "./lib/api"; + +type Page = "stats" | "users" | "drivers" | "rides"; + +const NAV: { key: Page; label: string }[] = [ + { key: "stats", label: "Overview" }, + { key: "users", label: "Users" }, + { key: "drivers", label: "Drivers & Fleet" }, + { key: "rides", label: "Rides & Payments" }, +]; + +export default function App() { + const [authed, setAuthed] = useState(Boolean(getToken())); + const [page, setPage] = useState("stats"); + + if (!authed) { + return setAuthed(true)} />; + } + + return ( + <> + +
+ {page === "stats" && } + {page === "users" && } + {page === "drivers" && } + {page === "rides" && } +
+ + ); +} diff --git a/dashboard/src/Login.tsx b/dashboard/src/Login.tsx new file mode 100644 index 0000000..83c1023 --- /dev/null +++ b/dashboard/src/Login.tsx @@ -0,0 +1,53 @@ +import { useState, type FormEvent } from "react"; +import { api, setToken } from "./lib/api"; + +export default function Login({ onDone }: { onDone: () => void }) { + const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); + const [error, setError] = useState(null); + const [busy, setBusy] = useState(false); + + const submit = async (e: FormEvent) => { + e.preventDefault(); + setBusy(true); + setError(null); + try { + const res = await api<{ data: { token: string; user: { role: string | null } } }>( + "/auth/login", + { method: "POST", body: JSON.stringify({ email, password }) }, + ); + if (res.data.user.role !== "owner") { + setError("This account does not have owner access."); + return; + } + setToken(res.data.token); + onDone(); + } catch (err) { + setError((err as Error).message); + } finally { + setBusy(false); + } + }; + + return ( +
+

Waseel Owner

+ setEmail(e.target.value)} + required + /> + setPassword(e.target.value)} + required + /> + {error &&
{error}
} + +
+ ); +} diff --git a/dashboard/src/assets/hero.png b/dashboard/src/assets/hero.png new file mode 100644 index 0000000000000000000000000000000000000000..02251f4b956c55af2d76fd0788124d7eee2b45eb GIT binary patch literal 13057 zcmV+cGycqpP)V|)f$;Qooc7=_G zlYe)HToTQIc!$)^+J1M1y0*T%w!p~7%ux`!eRhO?c80XDxKQ*R^lUUMnA>6NT^?feoZ8xxvP32D&s-9ow zqjcM}eesrC)NeDmsf)*P7wJ|K!&xP%Zy4iI8lF)Tv2!reW)tCzg_1=PmOwd1SQfxa z8;58t!=z~Ba7CYlNWVG>he8aRPY|+-JmozNhn!#9i#77Aa_Edt$ijyCWL#=~I>~2X zZNrQ8I0=D+NWD4pq=7~(i zhfThMNw|G>g^y9pGzxX7ZSApl@tIxFcs{p#MX{Ax&XZT+cR#U+OWc@S)pkIuI}dzu zH?^Q=<(y&Vq-oxSLfc0Zmq81bjZWf}RnssBaD6}2g-XJHLcN_|*IOu>m|x$nbm(?E zyNy!Zp=RroS;?Vg*kmoJYBi!n5{_^@rA!)=t#a^;N$8GL!*DsQb}`yvEuX!G@||An znOfUZAevPrkV_qjl|<~3QRZzG&h@C9Y5z zqpNH4xqbF_InIPh)kX}Vn^5kyed|mOuq+2>M;v~KO37a#yrEn3XDqtOl=rc6_KZ!; zreo)DFVB4|>1Zd(bvMI%8uM;3!)YMYu&cG?(PE!B~y@3yKBMt|R zAf=I16tFwPsl)!jDqvYkLHaAQ+f@W1m6F5aZvwhm4JL z{_l)@b;)mDSzle2gyFP5-r1x-5X{G}ot%VyWP@vEW80!Q=f%RTfpg>B*TA^pyWYUQ z<=xPtz}WcZ!;rFl4m1D&FFHv?K~#9!?A%+fn=lXt;9!Fc#kQ;zk~gZFsH z8e5iu@c_pzX&qb8&Dum*oXwB+fm6l6gFfC|o*wgEiy6tw~&co z9Vd_4)P%wP-KwQW7|lN-znGK#?N+j24U=$982myIBM+vsiKsc*@4-rwJxuAaHKna6 zT3wi!C~a4ZKH03qU}_1bKyx0&$CaK7_%Z+Kl$)fF5^op zZApQF2TvDav!s|krTjw-8US6ep z%!VmX4luub+fseQz_D9ATJQ?iQQwD}TZz{-yo#l12a%+7bT@E(X-hyaVS-5vuXc#^ zx^w;L21;NphGVoj*{s3f4dme0y2LC=G1-7THd`#z?;tuC{^9k(dM{Rf2GOxg7Jzho z7nSZHl7?M9kdalX`)YgoKEfiae5+;$(OGeN1eqxrv!ZCVKyH>xiyNqfe8xzY8*7)H zQls8KMp)F4D>ED;idMOU^^WhVF@q>ZSmeB0y~qC~|DB648hr%Sh|*T(4q|w2l?m2+ zvBVw3@7+Mz?^Yc#+se6KM;a<=(W-I>k)$-qL2V*t}VaW`;?P4)WqI%maIDq8!oUcSYAD`}wWjkSyAVsnF65#2zQ zZ>(K*TlS(E#4y$4Zq+e^_&}d)q20hCe3!LfLYP%nQpLJ~gM6a1hJlz3)aS<9C9me| zAcmJ#>tOwBy{HoP0Sm1&_(E+S@6 zgBIFUoei8zJmdpiq8q5=OY7t@`)JWxn_&GvKVr=Zdb_pEL_j|=?f;WK^U9Q0efd#K z9q7SfJTl4pmA$jsZ5oK8@O9#!I3Cv-kL)<8SalSsp#dcpvJ}Nz#G6FC0%9|7Fi#8; zGDJXtj!&GljT3*HE@0EE>G8Se&d)*nkqe}-?`3vPl&UqK?xG z!3XJ4M-x`EuQjhBbu?ik-)rmIt=DF_N?TVMP)8Gjn)TZ2V%H|zENbeix}kOxd@0}Q z>)HuH6Ean!uS#~4g2Ne2WsMGel|h%j9*W_quQheG^JqmKhc*RYzp0wKlGjBq2VzY_ zgOv8WC1+%W=W)k)Yp_`8kfE=uiiwOZTXi8Uj9YGr$f@yJcJ;#&-Nq~sJ7anE(@;QN z=~br%7%7`isKStX|7!1?L(apl^QvPKlrHV4S+6tNVQ*R1iGdC~WMNE1$a+=rpQmcB z>wxiLIBvOnm;u*;9Y!kJdy(T4lk|8>JAm(&wEsFIF1$_*{>2ZNd$V6DS=SfrGxAv0 zzKe377JI`&o9Ljr+VnS*EwehA{f&{cKZF(6*MG5!p5MvrFA3ll{fmRG*L@6^cb;o^ z3Wm8c?Sc6$`>~VEWw(c$Y?nRO;2Q$=ulpqPtM^=1IZx;@xK0PgO7rKQ^WHVLwtgUT z%|JF{^f(VH)wLKQ%dYiu2RmchBdxL0-M?wxxul_z*{h6ZZ`>-k(vizs((vW8Lt6Z6 zY;Dt?@JWyN`O`f;&d1Mb?e%9oyRK1ql?EE5XB2(W)|D1~Rx35$H6@6)$F?)7V|zEO zI}fu0-0}8W5=6sg$fPnZ~7=tTudl?Ecb@pxbo)vni%gP-?hL|%*?62C;x6?@E`VRnJv z?fTb;k4x;TS7Cu-z%J}uy}e-pwpLQ17Q@4DC+FCdAmNKklG$`I_pyw7E{fYmw~{Fj zi?6KcVy=Wrel)EB_DWO|0CKmI|13!gBV?X`Ozp7x>?6jr`>Qz=^4ea35!$*f}) zS$i+x_k+@P2q1RFUH^ZTTk7=n?cjfR>hTq3l3SY~#w+I8SSutXGyhw;Ws~=zMQ%Vc z>$On~47Ut?P*_!TOQ&PFmLAyJieB2X4_Fd_!WxI-AY`q1Lc-oK?+qcOTzlQ?@~x@OT}*9jTVNfl@3rGvZpWI=eKg>T zZb@6YWz)J=IhP7CF|c?G62vMEG%#U}?#86$0jR4sG~i(jRd#jmn`7b(O#?N;3a;1t zhXLssmUwGhp79luw#(*V8WL0|8+E z6=YZ_O@er~$LrD_PYGc(kJgB=;yw#+Z3X6LDUZ(NcwN=B-hjdiHm!JFar%m{(5bEW z@@_VEtG$5;`EJZ|OkJ@l&G9n((w@uNFwmU%bG|s#TbcJJos!{e+bjCjrCq_}LcN!UFgKtgg7siV*7# z!}1whTRRi*-avJPu->C}Z8EiuK$#886+H_#_!btv+rsiBbv2jAJvJ+O0{#}y(%L3H zfjU-kq_-L@2XrL*ae{{qYJkD{@dw%*bkh2P&YS-0!Xt!PRz7KHV0+~j(t9W8lAVWR zt@B*DgURgEz4>WuN>o?_iKcw$?k{||Pg7{Q2o4|VmJ)mg?{VQJA<}zEr^YAAS zgGm5RT4T3p)U;yz-tfBO^kw8?IoG!IVmc+Z3m#}AOQ?5MRa>)OcU!$N^_+yK6ayn? zK>~WK0!#ysuj^oNLakm)Zvu+J)OSubX^kv!c*xgdIvs;kln!rgG4*uZ;w0mQQO4XD zO9P{GNdv!=cQ(CAL{S(%KtuV^zC&Q{%g)PoXnp^gn^>c*`E>$hLYg2HjnbVGtWLa{7zHdG1jT@B{|Dm16 z7K2(jsfG+m*Zxof)iXxu+!H5Mo-0$pkyV3VV4B@Qms46M zuBxGRV@HxU7Wwx-6CB zaU*HO<_qn$5GH>&@?nRy1{z zkik!sLfWQ)r#75)vVwCBU*r_)Q6mp?!j85{#Xqse)ApRdE$V0%I0*~e(_{)5H)`Mk z#rExC>yjhZxuL@|+#v4#<Axw$+VpV zuT;!2Vww$je$DpAW`$FX_Ab|Ip%$;&T$-lW8jS~B$>G}rd>eQG+$h9lQx4Mx0w={m zx9?T6VU`>sR}XClkAhHEShOUe8awiq zmizhL+}5UKs3}6~It7vBTig9dfQ2Q8coo+Miiaw7n~>4ybv2Ptt0^^=VqX(t*Yya9 zr`FxxFX8(v*H=+uJ#JJWIB2A(==HDYx~^zZ2nu?2`}|Wsa*f3h3ixc+U|FDtAG$Y! z*lc_7se5Oso-Cgqe0){{!8H4g$3<8!R<6JOurD;((({c$1(pwb>(#TT!sge@4>r2@ zVL7>U`0`nsWAYErezk4(Z!gMI2?UTo{J3Ajo(u4)KYIRd>BRcG4BoS3G0EXyEp@tw z%P7__?A^a>Q&AKL@ayDO9D*Qkc!NHnO9l}kpp_6hXbMppYL(X1L?njdFT|-h2<_$; zAtDZ!1Rf%|yb!qbWKd}%0b`LzBeyNy43|QO(&h2mxQLUL)|0%agVOW)6TV!&Ip^Ls z`PG2cygM8)IecQx=Fc+nqYRo4hS^^-nM_&-y8?EJXUczP=DIw(GkTJdpEdh<_STs{ z|A)4n1GKdE=Wu!!nYoZHcUQ4S&R;oDOKX2lrkdF(mK>hz<$Pp>igjOcvoRIjlN=W8 zu8Gx5(roqn8$>gEE5vy{GiGeW8Tq{vnf3hS-V=$tZkQuftUVuU8o6k&dn=Yg3)6MOIH>nlK^-2+C6BZITr~1@So?NvG#TwL)|~=1YXGMTLpS<)ziK_CSOabe z=cB#5)yz|@0i9dSo?*CX)}UP=s6)B+F@~Em(u@Q(I9J9i_V{LmMu8BfXYMh~*oPP+ z!3~xTv|(>|=n6ZOtT~C@V!z!w%18*8T2t6}U2S##rC)mekBql&VsBX;$~ByGE$oA9 z`0Wzq8p?R{4)$l*on;!cLa}Dh^Xe?owiQZt9nH1fxxh$pN9K%CtOw?u3>85L7rr!d zXs)l{TZ{xXP&U8exz?9cv~dNNibOmt*K4I$?RxqIBZ0(?Mg-9FS{*9Bc49Qc1`=sIF-rye`aNT1G@4NwXcnyc@+bw_mTsR>5< zF<2;X0QesG_pw|TonqVBhRtfqI>ty(SIu&VOXd0CrLlfp+;WH7HYjhqnu^oAY!9cB z=B6#R?Rfz9BP`dJ=@v_?70s3HxQPk+{6Y+lM85f2NF^00*^OcM0~?JOZfR9ZPYF+# zYSs}(_BUYV8{n@2a1hD^SV41bwmi2uztR;PeBgF1F-`9>`zoNss-@3LaF2sjl~>OaaVmp7PNp+UT`6@}gR%uzqHDVeEZ14{Yt?n%JeQm+t(1_u zSc}oj^{b;+rlS|ME%+LjzSI&xu0Bblxo$MJ-J$kJ?Qu_XUXh}*@*-x@ny|}wVM%Lg z3tNB`yvr*}N?ClGL;H2cglcvErIccU3(eP7>@~4nOIcI~-`P8tSQnx=jI&{9)!1}l z;gQ%_h>ZlPSV@o@Azq1R$C6ja5!^ZGh;YRhhxs58qJWo9@Bceac&yy(pET1hnn`~7@}2L0&dfPKYs$ih7m2}R!25!(hxqA(!UIw; zK4+~Jowy3=RNC6nE=ncU{LH5?*9@W24lacJlvCZXB$CYtE@>c+~H zkV=(5I&gb{xn2!~f&fs2NQgAL6`p|kyt6kpWk}iVlqIp(H;ig`{_U9yxs1jzu^ETM z7~)Rg8C-NueqTYP&U8l{DY=Y47cR zOR@U%$KQV{mkRF|4)z9Y^t3K`@p>duY&QLUFeh6VoV`a`$U@)(z!-N*5Cj<11$EZW&hJLX83TO{lJYP74rlDZQPkm@t<=U^I)x@|UnHHkdQlh?!ltZwl92rE;;^ zZuIappj4dhld1}kttYYV-j|KF1Kus zWBnzttD^00%LFK(wrwNragFub6xiV8QE2rm<`&fcR4SLFcdtLxVuN!Aal-g6dE4%k zARZ}|xeo;K{0yf7@9aua%2j5o)CPcIOc6uLHFJOcgtB5owlcNAwyAHc0QB0Dts?c@ zUemG~j_E&W7R%+x-IO4FJl8e&*2Blmp1S#RA|)geVrxvP)NHdYuxi~g&Etn?QdNK8ZDKZ?QFLU?zh30G|t9G>a_X4zk}Ygw<^$7K!GIn(Io$>(d4ODJQ2XSd%jpK zm7>ptl$a3GyB}5-%p4>Q*p#VL^B{yQMuFCM^#l#+N!Ne z5_PrJWB=@Iy+t)H`g1lX`{bm($KE5I?0c(JEYm#t{F}j!xtsbob0{xu@0TB_*>G7w0ICn zr#VoBktqHZ~XxhiKD*lcG|b;H*|Ny3P^8ceV`sfBRfrhwZ!T+MFZ!F1Bt{q$8d9i6o?~ zODj^POr}&ivSa^R^YFIq7o0giLBKCycH_aU`F6)O6JX%nPTwh~Q`eq6*0iE#Srj2^ z*_hN3%*b83zfafy60@Cp3{J({RlSaEn&E?mrxRNC9GQ7#+f=s! z0KBf-9Ny_v2VbE%aB|Di)5kNJ^t&C`4D(>t7zYUWUFtbxt+Oq=!@O7BU)}>d*R72o zFF)3jQD_lLe4is&xzyJYC1-c{8TX$RU>&>P$%)ufpez0XSAukmh!xcekg`s$c<>-q zI#zn^JU0zzF}V60)o$_gY}PQH>b2M9&8fRZa#OauglPb zeQ@pMm&=!vNgos4CluQjLMV!pfkmxK+35bi^k&=k>9h02?l+u+m0agG;(h2|Jslc-llvtEwn~*w3bx7qnvZACG<8}AGeaDVvcHbKd2>3G^ zSFPULUn-?Pmo^-_`mLZr??uNH`2=I&yajlrF{DtUxMy#Nu}z=3y7qbUA;5`)hibMR zhXL@@uKyV0-2&A@t@!xyrBnMJl&^o@Gx$&5_q6?D=ji5grd-~=?dlg;ur(_V0wjh! zA=JV^C1m+DDkOsgr<%O9ZQFg!0}pD(#PSz4Dr_EyS5$`)VIAv);4n-SFP~YtC7sH= z7&*MfpH;gd*FHbkmD#)hVxb6xjc9~`t?_{=JS+@ip_cTicXxG<=7m9& zPX+Z8IC*GSAXuGCrZDHgR$r%jyk-fctis2Kx4HvZ|B~8uC@o)m^>Hy-O!&TKA?$&n zkP2Xc54w~!=z2?^NafyL*L0V9cbYrugHBBUj`xVyZmGFR&kvk#>1J*Z~i zNTz}?IAdJ$gkqd2!Gw(%LzE!O5s4C7q4%T~e_P{+z=DNDKrG**p=U`d5yg^vp`;Zn zsU=8gd0a9s4s0FPJePWR9eH5=+O^Kks&kC-iblNqTh2&Pw*^(4384f+D8N|fewZu_ zg2ejQ)ov;ztz;NQl7yj;A`(!H!XQu_$sqY9h_IrH*}_%1{L&_YLDvO?%R5Z-t+ClW z_qERbL?HKUZ!nt+!E9S`uoh^5A|DaIHe*_gf1`E_Vq+}{&T@t$EGhMnRjJ4z2w_W8 zp+qjs7as22^&S3wY1?+}^j-I=RcCE>#|39)g(lU7v_8;?=qK(9D8-*pPdiy)P3lIblG`+?%ea| zYoD3dopYt!tKgFicfNmNi(EWE=E4hC6(r|PYtanqJlmt57YOVrr2^tfrG(eG9C##X zu&1t@%L$RIvpj!wUA z8i>Pqot#_+Cnp6L2XPcZy1ar|9MnY+7eNvK1E)@Tr#2KsXq1*>)uUCozT7L##ok?o zhA6ofP4E|b*9tAfG?uf$#}>TIR&1A!yslP8}i7w-EzW(x#9VEvx18k%Tn=-$VV zkOtUr0b2!w3t>h?#8AZl^Az*(6KCGlD;4j~yx};`#2gN1_gv=%7KVzecIRakN{f*4 zeaI>yH;-o4OGhvGTU)(quWI)-q?V*(sVesSMv|wMUQ3hLEt=lBB$KZ9TyHr>)f7o%) zPYeU<3P)*P10*7vE)nA5#{c=6-E-_>r_u4e3i!I2+UksELwDqwMeBZ9FSP$;^Ajro z_@M#_Ss$?ejoB@!wN|kbGKs(0zLo%0QpQXW#t;oC$B0MZYZ&Ej?8~fNhcCVvPo3vo zFn0WWZaPliF^8_}yzb`*f@yg0uWv6HgNI)xa=pO%Ck(C<=-60l#uD3(wXP~c7!NoX z0&^6=N`zcc90F#qt@=Rn@r!3(*1v(Tl{B!m?Mc7yIA+nEHpY{YWr$=)F7rhR1P}(v zt{YhY#;jsW6G>#xhP*B`OCk|Pf+NN;ju1rxa*HAgoGq*rvqw&xe~;t1JA31$s?GBb z*g7&@cbKo4n<`>)!UlIAgR6q&))B0KYU8r66GbFj?8Guw4E%&}Qi_lT003LtoIZei zwD~=XZmeo+yZ2Pq3KYCF-R&11^p= z@H%s+=G`}wrbJ{()Mh71#2SP3Zy3m>l1n?0N-N1Q;z6?oSxr-G(H5m4EO>~&;}VKi zfY}3w+9z>vp#d)hVuu`)vG_aaH%3b=WKMnSu&c31;<3O;bz2iD=w+o4#oBb36 z5ZCF*Gu?zjZIR0S>_%pHY2$k8D^n7Sz_K8tCDeXM+dO<#LSg%h6`~dnVG1N@T7v&e z%wEd1!k{^zfz_1BTW{!$!B%g)J^2b87!9Y>>100X1SgT7s0z$o>^lAA=Gp_cC1(h=*5Tmf8z&LGJJ>$|K^~s`z9*OWz5MFUr?>Bi?_PGBB)#psD5?>n+q{o_ zz7~ez&;t#h8l$jwGPCC&xq2YetXYQT+0F3j(`xmNGf8dj#an|p#I*pvI*kwW4iuB> z+q3_7xB8y;pLzHG-S%+UHQA zvqp;$kmGJY>lLsN4C~&TcvAS1SErTcwcw0r@wngk zShAUA1M9b#g}^pL-zH7Q#z^&j#r9F8BTVfkR&qF<=e35goTu7c|GN)0mokj4m0%~0 zXJ8j4Hc_l;HJ&uU*Iw`8d_EscJ``s0tk9mkKo^&#TYXm-EoAzTQObxa@^u~g2t#T) zJz|rE!I_?i4dCJC=B8(_pZ{YR>|V?0iCcnU;E@$239^x?SYCfNaMHN;CtHIS_zHN9 zTkQc1v@O35okiFtq5_u+5FkY55ap@pi)O?}x0D1c*qB0KpYR}>Ul+B0Vmr}Z@+%mJ|As}sis_=ROPbov@*2thpE&?!V#Qgu$snYvCZ zrkhmkMU+fSf-s8(L37fPr&M*jRs{{THb!aXQu|P9l_-vJhHvLzMGH zE?1U0H_+PmNABp9`|KzkGfrrZ%XvdGo6*<{d5m9~L7 z_^`M;X6xDo=m6LY6RfvJEvsTK1!u8d2HPx|$S}p;sRy!I zWL55Yxu~_B`OP@~(q6&W3#)~I&+MGL%GWR$#udC151^wsswhqlii;rP9jJpiI7o&Z zAb})=HY7?4HA|re3ns`%$)FuvKCFWjhb~?IE)F6dF2K5}poj-NK6Gf;hw$t3=1txY zoxQxZWrQU6K!%|~!m?~Bnw-6Rr!F3BZ{u5!LqnZTDON}Coj9^@&le)V!NYrVwS~B% zEL+>Sr@}qGwGvu|HrOo|gSt__ezN^&%~{*)a=rf7y1HujUcr`zZB<4#l@T#eN)si} z)lZA<{=tKx8E%c9>A(##6}_p+~EZpKsl5a4pj`E*;_-6`ysiv zffA!7=MT1vCz}-m4~tjVey1b2KSR4OEtLd-(_DdUqYZ74LaDkhH?KFh?%WAOP2WbX zp@zT+Dx|5_f%JQiAGvVw!oh+g3e50u!aPfMxdC=E)XB{F5IcEZhePIM- zph6Y`$Oy?JBL<8Ex(SqEhLeQ@XcrdA>a?rx+_~HLA;l14)WmmpH}_w?Pg#HBZs0eS zwypwAW?M-x+3AU-(GGWSJ=ngxUEcEZ5OsX(Qlt!MQ zn^(`S{GHkAv(8@D`EAfSYig%Cxv?z!{=w^F#y)5_d7FuKZH7qlR-#5B0bt806%D0I zT7VdVP_?q*%Rq8UR;JkD4i^RXowt+E%#V2U>TfDqzZSDZ+dR!a#T3I>-z_$q9@k|m zy5~A*m~&JWP@E7a=pc}4kVHTc4h&R;Li7d@f`|hKMLkbb^uhOakNr3&FLjlm~i5NBM< zFaYI{;cpiHCNRdE0dg*>qIm(_t?#$h=(SCw?h3rJV2*ER8{O4^3#=dO)KwklZkoqU zS8i5c%YL*y*4;FY#D=XmkQnYj%LH)?02~gSJH`Qp1XY64g>%c_K$xseI&|e)7vRoL zAqRba$G@%fSGA7X7hQk%_3NVOYVS+$leU_!&6*5uN)8#5ZBz_6ASCA;azYS-Rt@ki zg2NWz(=;t}SC(~Ibl63$5C8FPmhXqb^)5#jaJ~I{Ex3xZ!+2h8$}}h_g@Be>HZ;72 z6#y#>AY3^skuVKF#0WxFBQ()5d5_nWb?c6c>EeMM|Mh+*&wEpPyxHCq{R-Gdr-`hN zF=1sxl&mBoK+#qRLl9#CEN|Fg8>nbmsTg3a1;#M9enQ$RgWk}kp#-5wh=EF&1tl%mJln2V^8o%Qv(*=zEuO7y z=m*8?xpUn-*@h5Cl_3BK3joiGkyaScK+>|MWdMRWm@RT!Q1piAlv5hL@B6>3&GI8) zP!xBc6}ZNIpJLL%2a8Y!+(<=f%WX>_uWVxlga9!D*oYt$l0cxRDMvqfU;Kq_mLK5k z)dvqYcgLa_Lz?3HyeF)@$%$&6lI?r4I>6W#M*<)vq{?&Oqrx``d`mhpVPr> z#q078F6gw_X<=?KR>8%^t%@wbITvNMu!hKiTSkCTJkw>1!e*Y{%31#_yMf=LW7{RJ zYoC^w$6%3cBtVG5)x#{Hg6IVTh9XEcM{gQwXk!R^y95^f-hZ`d{aVa+xW1EO4wDV4 zB?JgD7*?qkvc|$nIykTvNl2x0j3Q!MXoLL^)~}d7jcYf(H8D~c+?$pKL(px>Z3`eb z04RzS6_AgFT6Pn#iZAg$Sl_j8#;6ShF%&(Fag#E2asU@@LaN;=b=Wf7sgPKhfzhBM zC@eFL8^MrnA*9&Khe*Ab@CC9*uyJGXyi(;y2>lQLJZt;ShtJi?3Yf_t`F+$hY!+Q2Ndsx=U+bjTiAy7djLji>7k%k`$9&--f<*BNA3Hy&ZrHH|4 zG5H&9cB?O#zI1_OOf0Ce%mDfQxdtp3vU%(iY6yji3iISS61XLv#z|!zI_sZqza@B+ zyu9st5-h+`H7QUKx9}3w@oU@EO}&cEzG?fu!!bLO->%zkcg;i9^j`S~=WKMnDi1f= P00000NkvXXu0mjft=yBf literal 0 HcmV?d00001 diff --git a/dashboard/src/assets/vite.svg b/dashboard/src/assets/vite.svg new file mode 100644 index 0000000..5101b67 --- /dev/null +++ b/dashboard/src/assets/vite.svg @@ -0,0 +1 @@ +Vite diff --git a/dashboard/src/index.css b/dashboard/src/index.css new file mode 100644 index 0000000..6a2a2df --- /dev/null +++ b/dashboard/src/index.css @@ -0,0 +1,250 @@ +:root { + --bg: #0f1117; + --panel: #171a23; + --border: #262b38; + --text: #e8eaf0; + --muted: #8a91a5; + --accent: #4f7cff; + --danger: #e5484d; + --success: #30a46c; + font-family: Inter, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; +} + +* { + box-sizing: border-box; +} + +body { + margin: 0; + background: var(--bg); + color: var(--text); +} + +#root { + display: flex; + min-height: 100vh; +} + +.sidebar { + width: 220px; + background: var(--panel); + border-right: 1px solid var(--border); + padding: 20px 12px; + display: flex; + flex-direction: column; + gap: 4px; +} + +.sidebar h1 { + font-size: 16px; + margin: 0 8px 18px; +} + +.sidebar a { + color: var(--muted); + text-decoration: none; + padding: 9px 12px; + border-radius: 8px; + font-size: 14px; +} + +.sidebar a.active, +.sidebar a:hover { + background: var(--bg); + color: var(--text); +} + +.main { + flex: 1; + padding: 28px 32px; + max-width: 1200px; +} + +.main h2 { + margin-top: 0; +} + +.cards { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); + gap: 14px; + margin-bottom: 28px; +} + +.card { + background: var(--panel); + border: 1px solid var(--border); + border-radius: 12px; + padding: 18px; +} + +.card .label { + color: var(--muted); + font-size: 13px; +} + +.card .value { + font-size: 26px; + font-weight: 600; + margin-top: 6px; +} + +table { + width: 100%; + border-collapse: collapse; + background: var(--panel); + border: 1px solid var(--border); + border-radius: 12px; + overflow: hidden; + font-size: 14px; +} + +th, +td { + text-align: left; + padding: 10px 14px; + border-bottom: 1px solid var(--border); +} + +th { + color: var(--muted); + font-weight: 500; + font-size: 13px; +} + +tr:last-child td { + border-bottom: none; +} + +.badge { + display: inline-block; + padding: 2px 9px; + border-radius: 999px; + font-size: 12px; + border: 1px solid var(--border); +} + +.badge.owner { color: #b088ff; } +.badge.driver { color: #4f7cff; } +.badge.rider { color: #8a91a5; } +.badge.paid { color: var(--success); border-color: var(--success); } +.badge.unpaid { color: var(--danger); border-color: var(--danger); } + +button, +select, +input { + font: inherit; + color: inherit; +} + +button { + background: var(--accent); + border: none; + border-radius: 8px; + padding: 8px 14px; + cursor: pointer; +} + +button.secondary { + background: transparent; + border: 1px solid var(--border); +} + +button.danger { + background: transparent; + border: 1px solid var(--danger); + color: var(--danger); +} + +input, +select { + background: var(--bg); + border: 1px solid var(--border); + border-radius: 8px; + padding: 8px 10px; +} + +.toolbar { + display: flex; + gap: 10px; + margin-bottom: 14px; + align-items: center; +} + +.error { + color: var(--danger); + margin: 10px 0; +} + +.login-wrap { + margin: auto; + width: 340px; + background: var(--panel); + border: 1px solid var(--border); + border-radius: 14px; + padding: 28px; + display: flex; + flex-direction: column; + gap: 12px; +} + +.login-wrap h2 { + margin: 0; +} + +.chart { + display: flex; + align-items: flex-end; + gap: 6px; + height: 160px; + background: var(--panel); + border: 1px solid var(--border); + border-radius: 12px; + padding: 16px; +} + +.chart .bar { + flex: 1; + background: var(--accent); + border-radius: 4px 4px 0 0; + min-height: 2px; + position: relative; +} + +.chart .bar span { + position: absolute; + bottom: -22px; + left: 50%; + transform: translateX(-50%); + font-size: 10px; + color: var(--muted); +} + +.modal-backdrop { + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.6); + display: flex; + align-items: center; + justify-content: center; +} + +.modal { + background: var(--panel); + border: 1px solid var(--border); + border-radius: 14px; + padding: 24px; + width: 420px; + display: flex; + flex-direction: column; + gap: 10px; +} + +.modal input { + width: 100%; +} + +.row-actions { + display: flex; + gap: 6px; +} diff --git a/dashboard/src/lib/api.ts b/dashboard/src/lib/api.ts new file mode 100644 index 0000000..76adbca --- /dev/null +++ b/dashboard/src/lib/api.ts @@ -0,0 +1,50 @@ +const API_URL = + (import.meta.env.VITE_API_URL as string | undefined) ?? ""; + +let authToken: string | null = localStorage.getItem("owner_token"); + +export const setToken = (token: string) => { + authToken = token; + localStorage.setItem("owner_token", token); +}; + +export const clearToken = () => { + authToken = null; + localStorage.removeItem("owner_token"); +}; + +export const getToken = () => authToken; + +export class ApiError extends Error { + status: number; + constructor(message: string, status: number) { + super(message); + this.status = status; + } +} + +export const api = async ( + path: string, + options: RequestInit = {}, +): Promise => { + const headers = new Headers(options.headers); + if (!headers.has("Content-Type") && options.body) { + headers.set("Content-Type", "application/json"); + } + if (authToken) headers.set("Authorization", `Bearer ${authToken}`); + + const res = await fetch(`${API_URL}${path}`, { ...options, headers }); + + if (res.status === 401) { + clearToken(); + throw new ApiError("Session expired. Please sign in again.", 401); + } + + const body = await res.json().catch(() => ({})); + + if (!res.ok) { + throw new ApiError(body.error ?? `Request failed (${res.status})`, res.status); + } + + return body as T; +}; diff --git a/dashboard/src/main.tsx b/dashboard/src/main.tsx new file mode 100644 index 0000000..c2a145c --- /dev/null +++ b/dashboard/src/main.tsx @@ -0,0 +1,10 @@ +import { StrictMode } from "react"; +import { createRoot } from "react-dom/client"; +import App from "./App"; +import "./index.css"; + +createRoot(document.getElementById("root")!).render( + + + , +); diff --git a/dashboard/src/pages/Drivers.tsx b/dashboard/src/pages/Drivers.tsx new file mode 100644 index 0000000..3468ee6 --- /dev/null +++ b/dashboard/src/pages/Drivers.tsx @@ -0,0 +1,199 @@ +import { useCallback, useEffect, useState, type FormEvent } from "react"; +import { api } from "../lib/api"; + +type Driver = { + id: number; + first_name: string; + last_name: string; + profile_image_url: string | null; + car_image_url: string | null; + car_seats: number; + rating: string; + total_rides: number; + revenue: number; +}; + +const EMPTY = { + first_name: "", + last_name: "", + profile_image_url: "", + car_image_url: "", + car_seats: 4, + rating: 4.5, +}; + +export default function Drivers() { + const [drivers, setDrivers] = useState([]); + const [error, setError] = useState(null); + const [editing, setEditing] = useState(null); + + const load = useCallback(async () => { + try { + const res = await api<{ data: Driver[] }>("/admin/drivers"); + setDrivers(res.data); + setError(null); + } catch (e) { + setError((e as Error).message); + } + }, []); + + useEffect(() => { + load(); + }, [load]); + + const remove = async (id: number) => { + if (!confirm("Delete this driver?")) return; + try { + await api(`/admin/drivers/${id}`, { method: "DELETE" }); + await load(); + } catch (e) { + setError((e as Error).message); + } + }; + + return ( + <> +

Drivers & fleet

+
+ +
+ {error &&
{error}
} + + + + + + + + + + + + + + {drivers.map((d) => ( + + + + + + + + + + ))} + +
IDNameSeatsRatingRidesRevenue
{d.id} + {d.first_name} {d.last_name} + {d.car_seats}{d.rating}{d.total_rides}{d.revenue.toLocaleString()} +
+ + +
+
+ + {editing && ( + setEditing(null)} + onSaved={() => { + setEditing(null); + load(); + }} + /> + )} + + ); +} + +function DriverForm({ + initial, + onClose, + onSaved, +}: { + initial: Driver | null; + onClose: () => void; + onSaved: () => void; +}) { + const [form, setForm] = useState( + initial + ? { + first_name: initial.first_name, + last_name: initial.last_name, + profile_image_url: initial.profile_image_url ?? "", + car_image_url: initial.car_image_url ?? "", + car_seats: initial.car_seats, + rating: Number(initial.rating), + } + : { ...EMPTY }, + ); + const [error, setError] = useState(null); + const [busy, setBusy] = useState(false); + + const set = (key: keyof typeof form) => (e: { target: { value: string } }) => + setForm((f) => ({ + ...f, + [key]: + key === "car_seats" || key === "rating" + ? Number(e.target.value) + : e.target.value, + })); + + const submit = async (e: FormEvent) => { + e.preventDefault(); + setBusy(true); + try { + await api(initial ? `/admin/drivers/${initial.id}` : "/admin/drivers", { + method: initial ? "PATCH" : "POST", + body: JSON.stringify(form), + }); + onSaved(); + } catch (err) { + setError((err as Error).message); + } finally { + setBusy(false); + } + }; + + return ( +
+
e.stopPropagation()} onSubmit={submit}> +

{initial ? `Edit driver #${initial.id}` : "New driver"}

+ + + + + + + {error &&
{error}
} +
+ + +
+
+
+ ); +} diff --git a/dashboard/src/pages/Rides.tsx b/dashboard/src/pages/Rides.tsx new file mode 100644 index 0000000..ce35c9c --- /dev/null +++ b/dashboard/src/pages/Rides.tsx @@ -0,0 +1,88 @@ +import { useCallback, useEffect, useState } from "react"; +import { api } from "../lib/api"; + +type Ride = { + ride_id: number; + origin_address: string; + destination_address: string; + ride_time: number; + fare_price: number; + payment_status: string; + created_at: string; + user_email: string; + driver: { driver_id: number; name: string; rating: number }; +}; + +export default function Rides() { + const [rides, setRides] = useState([]); + const [status, setStatus] = useState(""); + const [error, setError] = useState(null); + + const load = useCallback(async (status: string) => { + try { + const res = await api<{ data: Ride[] }>( + `/admin/rides${status ? `?status=${encodeURIComponent(status)}` : ""}`, + ); + setRides(res.data); + setError(null); + } catch (e) { + setError((e as Error).message); + } + }, []); + + useEffect(() => { + load(status); + }, [load, status]); + + return ( + <> +

Rides & payments

+
+ +
+ {error &&
{error}
} + + + + + + + + + + + + + + + {rides.map((r) => ( + + + + + + + + + + + ))} + +
IDRouteUserDriverTime (min)FarePaymentDate
{r.ride_id} + {r.origin_address} → {r.destination_address} + {r.user_email}{r.driver.name}{r.ride_time}{r.fare_price.toLocaleString()} + + {r.payment_status} + + {new Date(r.created_at).toLocaleString()}
+ + ); +} diff --git a/dashboard/src/pages/Stats.tsx b/dashboard/src/pages/Stats.tsx new file mode 100644 index 0000000..8d8b33d --- /dev/null +++ b/dashboard/src/pages/Stats.tsx @@ -0,0 +1,82 @@ +import { useEffect, useState } from "react"; +import { api } from "../lib/api"; + +type Stats = { + totals: { users: number; drivers: number; rides: number; revenue: number }; + trend: { day: string; rides: number; revenue: number }[]; + topDrivers: { driver_id: number; name: string; rides: number; revenue: number }[]; +}; + +export default function Stats() { + const [stats, setStats] = useState(null); + const [error, setError] = useState(null); + + useEffect(() => { + api<{ data: Stats }>("/admin/stats") + .then((r) => setStats(r.data)) + .catch((e) => setError(e.message)); + }, []); + + if (error) return
{error}
; + if (!stats) return
Loading…
; + + const maxRides = Math.max(1, ...stats.trend.map((d) => d.rides)); + + return ( + <> +

Overview

+
+
+
Users
+
{stats.totals.users}
+
+
+
Drivers
+
{stats.totals.drivers}
+
+
+
Rides
+
{stats.totals.rides}
+
+
+
Revenue (paid)
+
{stats.totals.revenue.toLocaleString()}
+
+
+ +

Rides — last 14 days

+
+ {stats.trend.map((d) => ( +
+ {d.day.slice(5)} +
+ ))} +
+ +

Top drivers

+ + + + + + + + + + {stats.topDrivers.map((d) => ( + + + + + + ))} + +
DriverRidesRevenue
{d.name}{d.rides}{d.revenue.toLocaleString()}
+ + ); +} diff --git a/dashboard/src/pages/Users.tsx b/dashboard/src/pages/Users.tsx new file mode 100644 index 0000000..fbaa512 --- /dev/null +++ b/dashboard/src/pages/Users.tsx @@ -0,0 +1,124 @@ +import { useCallback, useEffect, useState } from "react"; +import { api } from "../lib/api"; + +type User = { + id: string; + name: string; + email: string; + role: string | null; + email_verified: boolean; + created_at: string; + rides: number; +}; + +const ROLES = ["rider", "driver", "owner"]; + +export default function Users() { + const [users, setUsers] = useState([]); + const [query, setQuery] = useState(""); + const [error, setError] = useState(null); + + const load = useCallback(async (q: string) => { + try { + const res = await api<{ data: User[] }>( + `/admin/users${q ? `?q=${encodeURIComponent(q)}` : ""}`, + ); + setUsers(res.data); + setError(null); + } catch (e) { + setError((e as Error).message); + } + }, []); + + useEffect(() => { + load(""); + }, [load]); + + const setRole = async (id: string, role: string) => { + try { + await api(`/admin/users/${id}`, { + method: "PATCH", + body: JSON.stringify({ role }), + }); + await load(query); + } catch (e) { + setError((e as Error).message); + } + }; + + const toggleVerified = async (u: User) => { + try { + await api(`/admin/users/${u.id}`, { + method: "PATCH", + body: JSON.stringify({ email_verified: !u.email_verified }), + }); + await load(query); + } catch (e) { + setError((e as Error).message); + } + }; + + return ( + <> +

Users

+
+ setQuery(e.target.value)} + onKeyDown={(e) => e.key === "Enter" && load(query)} + /> + +
+ {error &&
{error}
} + + + + + + + + + + + + + + {users.map((u) => ( + + + + + + + + + + ))} + +
NameEmailRoleVerifiedRidesJoined
{u.name}{u.email} + + + + {u.email_verified ? "yes" : "no"} + + {u.rides}{new Date(u.created_at).toLocaleDateString()} + +
+ + ); +} diff --git a/dashboard/tsconfig.app.json b/dashboard/tsconfig.app.json new file mode 100644 index 0000000..6830b6f --- /dev/null +++ b/dashboard/tsconfig.app.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + "target": "es2023", + "lib": ["ES2023", "DOM"], + "module": "esnext", + "types": ["vite/client"], + "allowArbitraryExtensions": true, + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src"] +} diff --git a/dashboard/tsconfig.json b/dashboard/tsconfig.json new file mode 100644 index 0000000..1ffef60 --- /dev/null +++ b/dashboard/tsconfig.json @@ -0,0 +1,7 @@ +{ + "files": [], + "references": [ + { "path": "./tsconfig.app.json" }, + { "path": "./tsconfig.node.json" } + ] +} diff --git a/dashboard/tsconfig.node.json b/dashboard/tsconfig.node.json new file mode 100644 index 0000000..8455dcb --- /dev/null +++ b/dashboard/tsconfig.node.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + "target": "es2023", + "lib": ["ES2023"], + "types": ["node"], + "skipLibCheck": true, + + /* Bundler mode */ + "module": "nodenext", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + + /* Linting */ + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["vite.config.ts"] +} diff --git a/dashboard/vite.config.ts b/dashboard/vite.config.ts new file mode 100644 index 0000000..2afcd19 --- /dev/null +++ b/dashboard/vite.config.ts @@ -0,0 +1,25 @@ +import { defineConfig, type ProxyOptions } from 'vite' +import react from '@vitejs/plugin-react' + +const API_TARGET = process.env.API_PROXY_TARGET ?? 'http://localhost:8081' + +// Proxy API calls through the dev server so the dashboard is same-origin. +// Expo's dev-server CORS middleware rejects foreign Origin headers, so +// the proxy strips them before forwarding. +const proxy: Record = {} +for (const path of ['/auth', '/user', '/ride', '/driver', '/admin']) { + proxy[path] = { + target: API_TARGET, + changeOrigin: true, + configure: (p) => { + p.on('proxyReq', (req) => { + req.removeHeader('origin') + }) + }, + } +} + +export default defineConfig({ + plugins: [react()], + server: { proxy }, +}) diff --git a/environment.d.ts b/environment.d.ts index 4d118e0..dfc7813 100644 --- a/environment.d.ts +++ b/environment.d.ts @@ -4,12 +4,27 @@ export {}; declare global { namespace NodeJS { interface ProcessEnv { - // clerk publishable key - EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY: string; + // self-hosted auth + AUTH_JWT_SECRET: string; + GOOGLE_OAUTH_CLIENT_ID: string; - // postgres db url (neon db) + // postgres db url (self-hosted) DATABASE_URL: string; + // expo api server url + EXPO_PUBLIC_SERVER_URL: string; + + // google oauth client ids + EXPO_PUBLIC_GOOGLE_AUTH_WEB_CLIENT_ID: string; + EXPO_PUBLIC_GOOGLE_AUTH_IOS_CLIENT_ID: string; + EXPO_PUBLIC_GOOGLE_AUTH_ANDROID_CLIENT_ID: string; + + // gmail api + GMAIL_CLIENT_ID: string; + GMAIL_CLIENT_SECRET: string; + GMAIL_REFRESH_TOKEN: string; + GMAIL_FROM: string; + // geoapify api key EXPO_PUBLIC_GEOAPIFY_API_KEY: string; diff --git a/lib/admin.ts b/lib/admin.ts new file mode 100644 index 0000000..96bcee7 --- /dev/null +++ b/lib/admin.ts @@ -0,0 +1,37 @@ +import { sql } from "@/lib/db"; +import { requireAuth } from "@/lib/jwt"; + +export const corsHeaders: Record = { + "Access-Control-Allow-Origin": process.env.ADMIN_CORS_ORIGIN ?? "*", + "Access-Control-Allow-Methods": "GET, POST, PATCH, DELETE, OPTIONS", + "Access-Control-Allow-Headers": "Content-Type, Authorization", +}; + +export const withCors = (response: Response): Response => { + for (const [key, value] of Object.entries(corsHeaders)) { + response.headers.set(key, value); + } + return response; +}; + +export const preflight = (): Response => withCors(new Response(null, { status: 204 })); + +// Returns the authenticated owner or a ready-to-return error Response. +export const requireOwner = async ( + req: Request, +): Promise<{ userId: string; email: string } | { error: Response }> => { + const auth = requireAuth(req); + if ("error" in auth) return auth; + + const rows = await sql<{ role: string | null }>` + SELECT role FROM users WHERE id = ${auth.userId} + `; + + if (rows[0]?.role !== "owner") { + return { + error: Response.json({ error: "Forbidden." }, { status: 403 }), + }; + } + + return auth; +}; diff --git a/lib/auth.ts b/lib/auth.ts index a238213..d47f59e 100644 --- a/lib/auth.ts +++ b/lib/auth.ts @@ -1,97 +1,21 @@ -import type { - StartOAuthFlowParams, - StartOAuthFlowReturnType, -} from "@clerk/clerk-expo"; -import * as Linking from "expo-linking"; -import * as SecureStore from "expo-secure-store"; - import { fetchAPI } from "./fetch"; +import type { SessionUser } from "./session"; -export interface TokenCache { - getToken: (key: string) => Promise; - saveToken: (key: string, token: string) => Promise; - clearToken?: (key: string) => void; -} - -export const tokenCache = { - async getToken(key: string) { - try { - const item = await SecureStore.getItemAsync(key); - - return item; - } catch (error) { - console.error("GET_TOKEN_CACHE: ", error); - - await SecureStore.deleteItemAsync(key); - - return null; - } - }, - async saveToken(key: string, value: string) { - try { - return SecureStore.setItemAsync(key, value); - } catch (err) { - console.error("SAVE_TOKEN_CACHE: ", err); - - return; - } - }, +export type AuthResult = { + token: string; + user: SessionUser; }; -type StartOAuthFlowType = ( - startOAuthFlowParams?: StartOAuthFlowParams, -) => Promise; +// Exchanges a Google idToken (obtained via expo-auth-session on the client) +// for a Waseel session issued by our own server. +export const googleAuth = async ( + idToken: string, +): Promise => { + const response = await fetchAPI("/(api)/auth/google", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ idToken }), + }); -export const googleOAuth = async (startOAuthFlow: StartOAuthFlowType) => { - try { - // No explicit scheme: in Expo Go this resolves to exp://... so the - // browser can redirect back into the app; in a standalone build it - // automatically uses the app.json scheme ("waseel"). - const { createdSessionId, signUp, setActive } = await startOAuthFlow({ - redirectUrl: Linking.createURL("/(root)/(tabs)/home"), - }); - - if (createdSessionId) { - if (setActive) { - await setActive!({ session: createdSessionId }); - - if (signUp && signUp.createdUserId) { - await fetchAPI("/(api)/user", { - method: "POST", - body: JSON.stringify({ - name: `${signUp.firstName} ${signUp.lastName}`, - email: signUp.emailAddress, - clerkId: signUp.createdUserId, - }), - }); - } - - return { - success: true, - code: "success", - message: "You are logged in.", - }; - } - - return { - success: false, - code: "failed", - message: "An error occured.", - }; - } else { - // Use signIn or signUp for next steps such as MFA - } - } catch (err) { - console.log("[OAUTH]: ", err); - - return { - success: false, - code: ( - err as { - code?: string; - } - )?.code, - message: "Internal Server Error.", - }; - } + return response.data as AuthResult; }; diff --git a/lib/db.ts b/lib/db.ts new file mode 100644 index 0000000..bf6fa07 --- /dev/null +++ b/lib/db.ts @@ -0,0 +1,67 @@ +import { Pool, type QueryResultRow } from "pg"; + +const pool = new Pool({ + connectionString: process.env.DATABASE_URL, + max: 10, + idleTimeoutMillis: 30_000, + connectionTimeoutMillis: 10_000, +}); + +type SqlValue = string | number | boolean | null | Date; + +export async function sql( + strings: TemplateStringsArray, + ...values: SqlValue[] +): Promise { + const text = strings.reduce( + (acc, chunk, i) => + acc + chunk + (i < values.length ? `$${i + 1}` : ""), + "", + ); + + const result = await pool.query(text, values); + + return result.rows; +} + +export async function transaction( + callback: ( + tx: ( + strings: TemplateStringsArray, + ...values: SqlValue[] + ) => Promise, + ) => Promise, +): Promise { + const client = await pool.connect(); + + try { + await client.query("BEGIN"); + + const tx = async ( + strings: TemplateStringsArray, + ...values: SqlValue[] + ) => { + const text = strings.reduce( + (acc, chunk, i) => + acc + chunk + (i < values.length ? `$${i + 1}` : ""), + "", + ); + + const result = await client.query(text, values); + + return result.rows; + }; + + const out = await callback(tx); + + await client.query("COMMIT"); + + return out; + } catch (error) { + await client.query("ROLLBACK"); + + throw error; + } finally { + client.release(); + } +} diff --git a/lib/fetch.ts b/lib/fetch.ts index 4a8150d..e7cd942 100644 --- a/lib/fetch.ts +++ b/lib/fetch.ts @@ -1,10 +1,27 @@ import { useState, useEffect, useCallback } from "react"; +// Set by lib/session.tsx once a token is available; read synchronously here so +// callers never have to await SecureStore before every request. +let authToken: string | null = null; + +export const setAuthToken = (token: string) => { + authToken = token; +}; + +export const clearAuthToken = () => { + authToken = null; +}; + export const fetchAPI = async (url: string, options?: RequestInit) => { try { - const response = await fetch(url, options); + const headers = new Headers(options?.headers); + if (authToken && !headers.has("Authorization")) { + headers.set("Authorization", `Bearer ${authToken}`); + } + + const response = await fetch(url, { ...options, headers }); if (!response.ok) { - new Error(`HTTP error! status: ${response.status}`); + throw new Error(`HTTP error! status: ${response.status}`); } return await response.json(); } catch (error) { diff --git a/lib/jwt.ts b/lib/jwt.ts new file mode 100644 index 0000000..8eaf456 --- /dev/null +++ b/lib/jwt.ts @@ -0,0 +1,100 @@ +import { createHmac, timingSafeEqual } from "crypto"; + +const base64Url = (input: Buffer | string): string => + Buffer.from(input) + .toString("base64") + .replace(/=/g, "") + .replace(/\+/g, "-") + .replace(/\//g, "_"); + +const fromBase64Url = (input: string): Buffer => + Buffer.from(input.replace(/-/g, "+").replace(/_/g, "/"), "base64"); + +export type JwtPayload = { + sub: string; + email: string; + iat: number; + exp: number; +}; + +const secret = (): string => { + const value = process.env.AUTH_JWT_SECRET; + if (!value) throw new Error("Missing AUTH_JWT_SECRET."); + return value; +}; + +export const signJwt = ( + payload: { sub: string; email: string }, + expiresInSeconds = 30 * 24 * 60 * 60, +): string => { + const iat = Math.floor(Date.now() / 1000); + const body: JwtPayload = { ...payload, iat, exp: iat + expiresInSeconds }; + + const header = base64Url(JSON.stringify({ alg: "HS256", typ: "JWT" })); + const claims = base64Url(JSON.stringify(body)); + const signature = base64Url( + createHmac("sha256", secret()).update(`${header}.${claims}`).digest(), + ); + + return `${header}.${claims}.${signature}`; +}; + +export const verifyJwt = (token: string): JwtPayload | null => { + const parts = token.split("."); + if (parts.length !== 3) return null; + + const [header, claims, signature] = parts; + + const expected = createHmac("sha256", secret()) + .update(`${header}.${claims}`) + .digest(); + + const received = fromBase64Url(signature); + + if ( + expected.length !== received.length || + !timingSafeEqual(expected, received) + ) { + return null; + } + + try { + const payload = JSON.parse(fromBase64Url(claims).toString()) as JwtPayload; + + if (payload.exp * 1000 < Date.now()) return null; + + return payload; + } catch { + return null; + } +}; + +// Returns the authenticated principal or a ready-to-return error Response. +export const requireAuth = ( + req: Request, +): { userId: string; email: string } | { error: Response } => { + const header = req.headers.get("authorization") ?? ""; + const token = header.startsWith("Bearer ") ? header.slice(7) : ""; + + const payload = token ? verifyJwt(token) : null; + + if (!payload) { + return { + error: Response.json({ error: "Unauthorized." }, { status: 401 }), + }; + } + + return { userId: payload.sub, email: payload.email }; +}; + +export const decodeJwtExp = (token: string): number | null => { + try { + const claims = JSON.parse( + fromBase64Url(token.split(".")[1]).toString(), + ) as JwtPayload; + + return claims.exp ?? null; + } catch { + return null; + } +}; diff --git a/lib/mailer.ts b/lib/mailer.ts new file mode 100644 index 0000000..af60d9a --- /dev/null +++ b/lib/mailer.ts @@ -0,0 +1,96 @@ +// Sends transactional email through the Gmail API using an OAuth2 refresh +// token (no third-party email service needed on a self-hosted box). +// +// Setup (one-time): +// 1. Google Cloud console -> enable Gmail API, create an OAuth client. +// 2. Generate a refresh token with scope +// https://www.googleapis.com/auth/gmail.send +// 3. Set GMAIL_CLIENT_ID, GMAIL_CLIENT_SECRET, GMAIL_REFRESH_TOKEN, GMAIL_FROM. + +const TOKEN_URL = "https://oauth2.googleapis.com/token"; +const SEND_URL = "https://gmail.googleapis.com/gmail/v1/users/me/messages/send"; + +let cachedAccessToken: { token: string; expiresAt: number } | null = null; + +const getAccessToken = async (): Promise => { + const clientId = process.env.GMAIL_CLIENT_ID; + const clientSecret = process.env.GMAIL_CLIENT_SECRET; + const refreshToken = process.env.GMAIL_REFRESH_TOKEN; + + if (!clientId || !clientSecret || !refreshToken) return null; + + if (cachedAccessToken && cachedAccessToken.expiresAt > Date.now() + 60_000) { + return cachedAccessToken.token; + } + + const response = await fetch(TOKEN_URL, { + method: "POST", + headers: { "Content-Type": "application/x-www-form-urlencoded" }, + body: new URLSearchParams({ + client_id: clientId, + client_secret: clientSecret, + refresh_token: refreshToken, + grant_type: "refresh_token", + }), + }); + + if (!response.ok) { + throw new Error(`Gmail token exchange failed: ${response.status}`); + } + + const data = (await response.json()) as { + access_token: string; + expires_in: number; + }; + + cachedAccessToken = { + token: data.access_token, + expiresAt: Date.now() + data.expires_in * 1000, + }; + + return cachedAccessToken.token; +}; + +export const sendEmail = async ( + to: string, + subject: string, + text: string, +): Promise => { + const accessToken = await getAccessToken(); + if (!accessToken) { + // Not configured: fall back to the server log so development still works. + console.log(`[MAIL to=${to}] ${subject}\n${text}`); + return; + } + + const from = process.env.GMAIL_FROM; + if (!from) throw new Error("Missing GMAIL_FROM."); + + const mime = [ + `From: ${from}`, + `To: ${to}`, + `Subject: ${subject}`, + "Content-Type: text/plain; charset=UTF-8", + "", + text, + ].join("\r\n"); + + const response = await fetch(SEND_URL, { + method: "POST", + headers: { + Authorization: `Bearer ${accessToken}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({ + raw: Buffer.from(mime) + .toString("base64") + .replace(/\+/g, "-") + .replace(/\//g, "_") + .replace(/=+$/, ""), + }), + }); + + if (!response.ok) { + throw new Error(`Gmail send failed: ${response.status}`); + } +}; diff --git a/lib/map.ts b/lib/map.ts index 67339e8..278e5db 100644 --- a/lib/map.ts +++ b/lib/map.ts @@ -1,3 +1,4 @@ +import { calculateFare } from "@/lib/pricing"; import type { Driver, MarkerData } from "@/types/type"; const directionsAPI = process.env.EXPO_PUBLIC_GOOGLE_API_KEY; @@ -37,11 +38,12 @@ export const calculateRegion = ({ destinationLongitude?: number | null; }) => { if (!userLatitude || !userLongitude) { + // Default to Beirut, Lebanon. return { - latitude: 37.78825, - longitude: -122.4324, - latitudeDelta: 0.01, - longitudeDelta: 0.01, + latitude: 33.8938, + longitude: 35.5018, + latitudeDelta: 0.09, + longitudeDelta: 0.09, }; } @@ -100,20 +102,32 @@ export const calculateDriverTimes = async ({ `https://maps.googleapis.com/maps/api/directions/json?origin=${marker.latitude},${marker.longitude}&destination=${userLatitude},${userLongitude}&key=${directionsAPI}`, ); const dataToUser = await responseToUser.json(); - const timeToUser = dataToUser.routes[0].legs[0].duration.value; // Time in seconds const responseToDestination = await fetch( `https://maps.googleapis.com/maps/api/directions/json?origin=${userLatitude},${userLongitude}&destination=${destinationLatitude},${destinationLongitude}&key=${directionsAPI}`, ); const dataToDestination = await responseToDestination.json(); - const timeToDestination = - dataToDestination.routes[0].legs[0].duration.value; // Time in seconds + // Google returns no routes when a leg is unreachable (ZERO_RESULTS). + const legToUser = dataToUser.routes?.[0]?.legs?.[0]; + const legToDestination = dataToDestination.routes?.[0]?.legs?.[0]; + if (!legToUser || !legToDestination) { + return { ...marker, time: 0, price: "0.00" }; + } - const totalTime = (timeToUser + timeToDestination) / 60; // Total time in minutes - const price = (totalTime * 0.5).toFixed(2); // Calculate price based on time + const timeToUser = legToUser.duration.value; // Pickup ETA in seconds + const timeToDestination = legToDestination.duration.value; // Trip duration in seconds - return { ...marker, time: totalTime, price }; + // The rider pays for the trip leg only (distance + duration) — + // never for the driver's approach. + const price = calculateFare({ + distanceMeters: legToDestination.distance.value, + durationSeconds: timeToDestination, + }); + + const totalTripTime = (timeToUser + timeToDestination) / 60; // Minutes until drop-off + + return { ...marker, time: totalTripTime, price }; }); return await Promise.all(timesPromises); diff --git a/lib/password.ts b/lib/password.ts new file mode 100644 index 0000000..3156418 --- /dev/null +++ b/lib/password.ts @@ -0,0 +1,24 @@ +import { randomBytes, scryptSync, timingSafeEqual } from "crypto"; + +const KEY_LENGTH = 64; + +export const hashPassword = (password: string): string => { + const salt = randomBytes(16).toString("hex"); + const hash = scryptSync(password, salt, KEY_LENGTH).toString("hex"); + return `scrypt:${salt}:${hash}`; +}; + +export const verifyPassword = ( + password: string, + stored: string, +): boolean => { + const [scheme, salt, hash] = stored.split(":"); + if (scheme !== "scrypt" || !salt || !hash) return false; + + const candidate = scryptSync(password, salt, KEY_LENGTH); + const expected = Buffer.from(hash, "hex"); + + return ( + candidate.length === expected.length && timingSafeEqual(candidate, expected) + ); +}; diff --git a/lib/pricing.ts b/lib/pricing.ts new file mode 100644 index 0000000..bcaeaf8 --- /dev/null +++ b/lib/pricing.ts @@ -0,0 +1,34 @@ +// Fare model tuned to the Lebanese market: +// - Distance-based with a base fare (like CTaxi/local taxis), not time-only. +// - Typical Beirut rides land in the $3–8 range riders expect. +// - Prices are quoted in USD (the de facto ride-hailing currency) with an +// L.B.P. equivalent shown for cash settlement. + +export const FARE = { + base: 1.5, // USD, flag drop + perKm: 0.55, // USD per kilometer of the trip + perMin: 0.15, // USD per minute of the trip + minimum: 3.0, // USD minimum fare +} as const; + +// Parallel market rate used for the L.B.P. cash equivalent shown in-app. +export const LBP_RATE = 89500; + +export const calculateFare = ({ + distanceMeters, + durationSeconds, +}: { + distanceMeters: number; + durationSeconds: number; +}): string => { + const km = distanceMeters / 1000; + const minutes = durationSeconds / 60; + + const fare = FARE.base + km * FARE.perKm + minutes * FARE.perMin; + + return Math.max(fare, FARE.minimum).toFixed(2); +}; + +// Rounds to the nearest 1,000 L.B.P. — the smallest practical cash note. +export const formatLBP = (usd: number): string => + `${(Math.round((usd * LBP_RATE) / 1000) * 1000).toLocaleString("en-US")} L.B.P.`; diff --git a/lib/session.tsx b/lib/session.tsx new file mode 100644 index 0000000..1afa3fb --- /dev/null +++ b/lib/session.tsx @@ -0,0 +1,157 @@ +import * as SecureStore from "expo-secure-store"; +import { + createContext, + useCallback, + useContext, + useEffect, + useMemo, + useState, + type ReactNode, +} from "react"; + +import { setAuthToken, clearAuthToken } from "./fetch"; + +const TOKEN_KEY = "waseel_auth_token"; +const USER_KEY = "waseel_auth_user"; +const DEFAULT_TOKEN_TTL_SECONDS = 30 * 24 * 60 * 60; + +export type SessionUser = { + id: string; + name: string; + email: string; + role?: string | null; + avatarUrl?: string | null; +}; + +type AuthResult = { token: string; user: SessionUser }; + +type SessionContextValue = { + isLoaded: boolean; + isSignedIn: boolean; + userId: string | null; + user: SessionUser | null; + setSession: (result: AuthResult) => Promise; + setUserRole: (role: string) => void; + signOut: () => Promise; +}; + +const SessionContext = createContext(null); + +// Mirrors lib/jwt.ts payload decoding (no signature check needed client-side). +export const decodeJwtExp = (token: string): number | null => { + try { + const claims = JSON.parse( + atob(token.split(".")[1].replace(/-/g, "+").replace(/_/g, "/")), + ); + return typeof claims.exp === "number" ? claims.exp : null; + } catch { + return null; + } +}; + +export const SessionProvider = ({ children }: { children: ReactNode }) => { + const [isLoaded, setIsLoaded] = useState(false); + const [user, setUser] = useState(null); + + useEffect(() => { + let cancelled = false; + + const restore = async () => { + try { + const [token, storedUser] = await Promise.all([ + SecureStore.getItemAsync(TOKEN_KEY), + SecureStore.getItemAsync(USER_KEY), + ]); + + if (!token || !storedUser) return; + + const exp = decodeJwtExp(token) ?? 0; + if (exp * 1000 < Date.now()) { + await SecureStore.deleteItemAsync(TOKEN_KEY); + await SecureStore.deleteItemAsync(USER_KEY); + return; + } + + if (cancelled) return; + + setAuthToken(token); + setUser(JSON.parse(storedUser) as SessionUser); + } catch (error) { + console.error("[SESSION_RESTORE]: ", error); + } finally { + if (!cancelled) setIsLoaded(true); + } + }; + + void restore(); + + return () => { + cancelled = true; + }; + }, []); + + const setSession = useCallback(async (result: AuthResult) => { + setAuthToken(result.token); + setUser(result.user); + + await SecureStore.setItemAsync(TOKEN_KEY, result.token); + await SecureStore.setItemAsync(USER_KEY, JSON.stringify(result.user)); + }, []); + + const setUserRole = useCallback( + (role: string) => { + setUser((currentUser) => { + if (!currentUser) return currentUser; + + const updated = { ...currentUser, role }; + + void SecureStore.setItemAsync(USER_KEY, JSON.stringify(updated)); + + return updated; + }); + }, + [], + ); + + const signOut = useCallback(async () => { + clearAuthToken(); + setUser(null); + + await SecureStore.deleteItemAsync(TOKEN_KEY); + await SecureStore.deleteItemAsync(USER_KEY); + }, []); + + const value = useMemo( + () => ({ + isLoaded, + isSignedIn: user !== null, + userId: user?.id ?? null, + user, + setSession, + setUserRole, + signOut, + }), + [isLoaded, user, setSession, setUserRole, signOut], + ); + + return ( + + {children} + + ); +}; + +export const useSession = (): SessionContextValue => { + const context = useContext(SessionContext); + + if (!context) { + throw new Error("useSession must be used within a SessionProvider."); + } + + return context; +}; + +// Alias kept for parity with the previous auth API. +export const useAuth = useSession; + +export const TOKEN_TTL_SECONDS = DEFAULT_TOKEN_TTL_SECONDS; diff --git a/lib/users.ts b/lib/users.ts new file mode 100644 index 0000000..a2610e8 --- /dev/null +++ b/lib/users.ts @@ -0,0 +1,39 @@ +import { sql } from "@/lib/db"; +import { signJwt } from "@/lib/jwt"; + +export type UserProfile = { + id: string; + name: string; + email: string; + role: string | null; +}; + +type UserRow = { + id: string; + name: string; + email: string; + role: string | null; +}; + +export const toProfile = (row: UserRow): UserProfile => ({ + id: row.id, + name: row.name, + email: row.email, + role: row.role, +}); + +export const issueSession = ( + row: UserRow, +): { token: string; user: UserProfile } => ({ + token: signJwt({ sub: row.id, email: row.email }), + user: toProfile(row), +}); + +export const findUserByEmail = async ( + email: string, +): Promise => { + const rows = await sql` + SELECT id, name, email, role FROM users WHERE email = ${email} + `; + return rows[0] ?? null; +}; diff --git a/lib/utils.ts b/lib/utils.ts index a92ed7a..37758b7 100644 --- a/lib/utils.ts +++ b/lib/utils.ts @@ -1,20 +1,17 @@ import type { Ride } from "@/types/type"; export const sortRides = (rides: Ride[]): Ride[] => { - const result = rides.sort((a, b) => { - const dateA = new Date(`${a.created_at}T${a.ride_time}`); - const dateB = new Date(`${b.created_at}T${b.ride_time}`); - return dateB.getTime() - dateA.getTime(); - }); - - return result.reverse(); + return [...rides].sort( + (a, b) => + new Date(b.created_at).getTime() - new Date(a.created_at).getTime(), + ); }; export function formatTime(minutes: number): string { - const formattedMinutes = +minutes?.toFixed(0) || 0; + const formattedMinutes = Math.round(minutes) || 0; if (formattedMinutes < 60) { - return `${minutes} min`; + return `${formattedMinutes} min`; } else { const hours = Math.floor(formattedMinutes / 60); const remainingMinutes = formattedMinutes % 60; diff --git a/package-lock.json b/package-lock.json index 86acd95..6c75572 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,17 +19,16 @@ ], "license": "MIT", "dependencies": { - "@clerk/clerk-expo": "^2.2.5", "@expo/metro-runtime": "~3.2.3", "@expo/vector-icons": "^14.0.2", "@gorhom/bottom-sheet": "^4.6.4", - "@neondatabase/serverless": "^0.9.4", "@react-navigation/native": "^6.0.2", "eslint-config-prettier": "^9.1.0", "eslint-plugin-prettier": "^5.2.1", "expo": "~51.0.28", "expo-auth-session": "~5.5.2", "expo-constants": "~16.0.2", + "expo-crypto": "^57.0.1", "expo-font": "~12.0.9", "expo-linking": "^6.3.1", "expo-location": "^17.0.1", @@ -40,6 +39,7 @@ "expo-system-ui": "~3.0.7", "expo-web-browser": "~13.0.3", "nativewind": "^2.0.11", + "pg": "^8.23.0", "prettier": "^3.3.3", "react": "18.2.0", "react-dom": "18.2.0", @@ -58,6 +58,7 @@ "devDependencies": { "@babel/core": "^7.20.0", "@types/jest": "^29.5.12", + "@types/pg": "^8.23.1", "@types/react": "~18.2.45", "@types/react-test-renderer": "^18.0.7", "eslint": "^8.57.0", @@ -69,13 +70,8 @@ "typescript": "~5.3.3" } }, - "node_modules/@adraffy/ens-normalize": { - "version": "1.11.1", - "license": "MIT" - }, "node_modules/@alloc/quick-lru": { "version": "5.2.0", - "dev": true, "license": "MIT", "engines": { "node": ">=10" @@ -456,6 +452,101 @@ "node": ">=6.0.0" } }, + "node_modules/@babel/plugin-bugfix-firefox-class-in-computed-class-key": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.29.7.tgz", + "integrity": "sha512-j8SrR0zLZrRsC09DlszEx8FpMiwukKffYXMK0d5LmOglO7vGG6sz/BR/20yHqWH+Lnn31JTt2PE3hIWNgM2J6w==", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-safari-class-field-initializer-scope": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-class-field-initializer-scope/-/plugin-bugfix-safari-class-field-initializer-scope-7.29.7.tgz", + "integrity": "sha512-r8j8escF+U2FUHo0KOhPUdMzUO+jp9fInva6+ACVAF3Y97Ev+5iNZwiqTghmzNeWwDkOPlYuTcfb1vDaoZKmAQ==", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.29.7.tgz", + "integrity": "sha512-GE1TFSiuFeGsCxmYXZl8HwoPrVlwe4rHPFE8weieGKZqnDORK+Ar3vgWMgW+AOxQ6/2TgLSKx9p6W7O4rC6qgQ==", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-safari-rest-destructuring-rhs-array": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-rest-destructuring-rhs-array/-/plugin-bugfix-safari-rest-destructuring-rhs-array-7.29.7.tgz", + "integrity": "sha512-oBNVCvnO5tND+xSopWvV8WNGfpTfgP4Zr/YXXSj8zfmcPktp5Ku/aZlsIowgSD4fjmgHn6sGmB9APVsU5zOdhA==", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.29.7.tgz", + "integrity": "sha512-QQt9qKHZ2sg/kivaLr7lnQr8HVrQDdBNSfCsTjiDxRuX/K5ORyKq+Bu8Xr0cDE3Dfkv0cw28Ve0EKyKMvulkOw==", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7", + "@babel/plugin-transform-optional-chaining": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.13.0" + } + }, + "node_modules/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.29.7.tgz", + "integrity": "sha512-pn6QacGLgvCcwc+syUhKE/qSjV2D1IHDB84RNxWYSt1mW3K/SCtjinZ2p0cETJxAWBjPy3K/1lHwG5BjjPxNlw==", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, "node_modules/@babel/plugin-proposal-async-generator-functions": { "version": "7.20.7", "license": "MIT", @@ -602,6 +693,18 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-proposal-private-property-in-object": { + "version": "7.21.0-placeholder-for-preset-env.2", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz", + "integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==", + "peer": true, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/plugin-syntax-async-generators": { "version": "7.8.4", "license": "MIT", @@ -697,9 +800,23 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-syntax-import-assertions": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.29.7.tgz", + "integrity": "sha512-/An1OCBN93thpBAGyfsK2pcf0jvju1SAtKkL2Ny++B5Sy6sqgzXDQH1cZxWbF96Wuk+bn41MDA9bLd4VVAw6rw==", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/plugin-syntax-import-attributes": { "version": "7.29.7", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" @@ -847,6 +964,22 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-syntax-unicode-sets-regex": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz", + "integrity": "sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==", + "peer": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, "node_modules/@babel/plugin-transform-arrow-functions": { "version": "7.29.7", "license": "MIT", @@ -860,6 +993,23 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-transform-async-generator-functions": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.29.7.tgz", + "integrity": "sha512-d98gXZkgswvkyohMBABkhm3GeXhYj8psWfwQ2C7gtfrKGTykQa/iOIi+JJhwMjPlZ6Vm2XN+DCf3Es1EoG4ZLA==", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-remap-async-to-generator": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/plugin-transform-async-to-generator": { "version": "7.29.7", "license": "MIT", @@ -875,6 +1025,21 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-transform-block-scoped-functions": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.29.7.tgz", + "integrity": "sha512-cUSmjh72N+rN4PrkFlN1dJwNCwjVp5d38/CQrEsFggkD10UiFlBFgdH3tv5dNsLuHY+3S8db2xCHjhZcv5WgvA==", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/plugin-transform-block-scoping": { "version": "7.29.7", "license": "MIT", @@ -888,6 +1053,38 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-transform-class-properties": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.29.7.tgz", + "integrity": "sha512-GtcpjFvanPfzNQi3eTitsCqtRRmmqzpy/A+yhTR1HaZo1Ly3EA8ZXxlPyHdR8/IuRMYc3E4wdGBewB2QKQjAaA==", + "peer": true, + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-class-static-block": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.29.7.tgz", + "integrity": "sha512-kibJgmEdX2iMwsHY2tSZNDgj8PwIlCQz7FK9KuGKO8zsuoUwSEhoNnNVp/emKWrbY4HeO6kkXfdMqRKKKXBm2A==", + "peer": true, + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.12.0" + } + }, "node_modules/@babel/plugin-transform-classes": { "version": "7.29.7", "license": "MIT", @@ -934,6 +1131,99 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-transform-dotall-regex": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.29.7.tgz", + "integrity": "sha512-3qc18hsD2RdZiyJNDNc7HQpv6xbncwh8FYtxNFFzclSyh/trPD9KkVR9BDECUjDLvb7yJVF15GfYUuC+LMkkiQ==", + "peer": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-duplicate-keys": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.29.7.tgz", + "integrity": "sha512-6IvRRriEMqnBwD6chtxdLpMYCHWEzN+oL5cyQtjykya19UgzbmKhxmhZgKC/LHxS2nYr9Q/qYPZ5Lr6jOL9+yQ==", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-duplicate-named-capturing-groups-regex": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.29.7.tgz", + "integrity": "sha512-2wiIyo2BjtgU7HufSeDnL9L2O7zr8jmhFKuSr65VpRkUiRKRNpb0mdlk56+XPPKoIrfHqzbMuglDvZun0RISsA==", + "peer": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-dynamic-import": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.29.7.tgz", + "integrity": "sha512-giOlEm/EFjfjr+te9NsdjkUo2v4f8rS/SXPumRVHAtbNcyNlvtREkU1dZzaIDclNpnaVhlCqRdFKhJBjBikzLg==", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-explicit-resource-management": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-explicit-resource-management/-/plugin-transform-explicit-resource-management-7.29.7.tgz", + "integrity": "sha512-Rstj7coNz8sE+7Ju7ihpHLI564lsK5pUpNNlvptCIC/16E/S5hbl6n3kESPKdNRmqEWlpn5xpS5Q2dvXBsySLw==", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/plugin-transform-destructuring": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-exponentiation-operator": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.29.7.tgz", + "integrity": "sha512-zFpMOTLZBdW5LfObqcSbL6kefg4R4eLdmvS0wbN9M6D5Mym/sKm9toOoWyVOa+xDjvCnuWcHls2YonXwHvH3CQ==", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/plugin-transform-export-namespace-from": { "version": "7.29.7", "license": "MIT", @@ -961,6 +1251,22 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-transform-for-of": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.29.7.tgz", + "integrity": "sha512-zeSIHh0+E1Um1WJRXCFlHQYu2ieJNdivLLjlBEp+dIBu3S51n+SZZmIXjxnItw6pz56Cn+KvK68BIBVsxq2JiQ==", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/plugin-transform-function-name": { "version": "7.29.7", "license": "MIT", @@ -976,6 +1282,21 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-transform-json-strings": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.29.7.tgz", + "integrity": "sha512-RRnE2+eon1rJAq8MnoF1b5kTpY1vU88twHcvcKMrsqP/jxIRqDVs9iJB5fqPuqyeFAW0wJo4MlUIPpQCq/aRsg==", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/plugin-transform-literals": { "version": "7.29.7", "license": "MIT", @@ -989,6 +1310,52 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-transform-logical-assignment-operators": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.29.7.tgz", + "integrity": "sha512-A0H91hh6W8MFRkp5TqJmMr39jzGD1A1E1Ysiv2O06Sfbhkapm+XyIzxWCEh5kqwOZ1/8QZ0dY3SeQ7XBqfJd5Q==", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-member-expression-literals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.29.7.tgz", + "integrity": "sha512-hl1kwFZCCiDyfH25Xmco9jTrkPgnS9pmOzSG7W5I4SaGbLeqKv417hcU2RKmaxoPEgsoJh7ZPOrnPGq99bHoUg==", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-amd": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.29.7.tgz", + "integrity": "sha512-fxtQoH3m5ywUSIfaH0FGCzWu4McsYon5bD3K4XnskC7f+OyQMj7rsOMi4NvvmJ83WwBAg4UCe+ov4VZlqEvyew==", + "peer": true, + "dependencies": { + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/plugin-transform-modules-commonjs": { "version": "7.29.7", "license": "MIT", @@ -1003,6 +1370,40 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-transform-modules-systemjs": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.29.8.tgz", + "integrity": "sha512-6iSnEK0zlkLKU4heofK/AdmRD4e2SHVpJMtrwnTCzhnaM98ria4rTrOXBBi45BTTYnJtO8txnPsX4fChYXkmeA==", + "peer": true, + "dependencies": { + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.8" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-umd": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.29.7.tgz", + "integrity": "sha512-B4UkaTK3QpgCwJnrxKfMPKdo92CN7OKXAlpAAnM3UPu0Q0lCCk57ylA9AJbRy2v8dDKOPAAWcoR6CMyeoHwRCA==", + "peer": true, + "dependencies": { + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/plugin-transform-named-capturing-groups-regex": { "version": "7.29.7", "license": "MIT", @@ -1017,6 +1418,21 @@ "@babel/core": "^7.0.0" } }, + "node_modules/@babel/plugin-transform-new-target": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.29.7.tgz", + "integrity": "sha512-fEo41GmsOUhOBlw8ioo6zvjX5Xc2Lqkzlyfqbpsk3eB6TReV18uhxZ0esfEokVbY2+PVJAQHNKxER6lGrzNd3A==", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/plugin-transform-nullish-coalescing-operator": { "version": "7.29.7", "license": "MIT", @@ -1030,6 +1446,21 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-transform-numeric-separator": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.29.7.tgz", + "integrity": "sha512-zR7fv/z14OjgHl4AgRtkDBvBMhIzCxqV/qN/2BCRC7LjFwvuzjYe7gDWxC4Wl/SNsLM6SE1IWvRPYMgSJaUvNw==", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/plugin-transform-object-rest-spread": { "version": "7.29.7", "license": "MIT", @@ -1047,6 +1478,37 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-transform-object-super": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.29.7.tgz", + "integrity": "sha512-Ea/diGcw0twB5IlZPO5sgET6fJsLJqPABqTuFWIR+iMPGPZJkATEIWx0wa+aEQ5UY1CBQyP/gkAiLEqn1vBiQA==", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-replace-supers": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-optional-catch-binding": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.29.7.tgz", + "integrity": "sha512-sLsyndxK2VwX6yNUOakMb7Sh553ZTe/vVM1XJ+9Z5aW1ytsc8xOIwmyk05NNjN60vkc5/KqoTH6hB4V41LJhng==", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/plugin-transform-optional-chaining": { "version": "7.29.7", "license": "MIT", @@ -1103,6 +1565,21 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-transform-property-literals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.29.7.tgz", + "integrity": "sha512-bOMRLQuI0A5ZqHq3OWJ89/rXpJ/NJrbVhXiP4zwPGMs6kpcVsuTUNjwoE30K0Qm3mf48a/TnRYYD6vPNqcg6jA==", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/plugin-transform-react-display-name": { "version": "7.29.7", "license": "MIT", @@ -1186,6 +1663,52 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-transform-regenerator": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.29.8.tgz", + "integrity": "sha512-0UpIXPtdDtMXfnV2OJAVMLpj3H/92vmkA6lpSRakmycJvj3VUy6Xs1dM8tXRugupykr5WB+LpiVl0J8LMVg2mg==", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-regexp-modifiers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regexp-modifiers/-/plugin-transform-regexp-modifiers-7.29.7.tgz", + "integrity": "sha512-mB5Fs0VWrJ42ZCmc8114v60qetdaUVNkj9PmSZRmanCZM3S9hm0CFRLjRmYIsuXav14l2jvZ+4T8iiCGnhj3nQ==", + "peer": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-reserved-words": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.29.7.tgz", + "integrity": "sha512-5+YhdpVgmfSmwZyLMftfaiffLRMHjzIRHFHHLdibcSyJm2pasMrKHrO3Ptrt2DRshjvpgjEJJ1zVW14WPq/6QA==", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/plugin-transform-runtime": { "version": "7.29.7", "license": "MIT", @@ -1257,6 +1780,21 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-transform-typeof-symbol": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.29.7.tgz", + "integrity": "sha512-223mNGoTkBiTEWFoK+Q6Go3tueMRclO8vxxxxquNCYuNI4jWOofFKJRRDu6SDrB8Sgo1UEGW9T4GAQ8ZyRso1A==", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/plugin-transform-typescript": { "version": "7.29.7", "license": "MIT", @@ -1274,6 +1812,37 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-transform-unicode-escapes": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.29.7.tgz", + "integrity": "sha512-jCfXxSjf94lf4E0hKE0AByxF6F3/pVFqRdUUNkDJhsY0m1ZKjnN6ZYyMeHNpzflxb/0q5b7t3p+BE+SLF1WOtA==", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-property-regex": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.29.7.tgz", + "integrity": "sha512-OgZ+zoAJgZLUCunsTRQ5LAjOywDv5zzZ2/hQ5aMw1pGXyY2rtE8/chXYUmu3AlVHKpm10KEdG9aMwbI/K76ZGw==", + "peer": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/plugin-transform-unicode-regex": { "version": "7.29.7", "license": "MIT", @@ -1288,6 +1857,120 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-transform-unicode-sets-regex": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.29.7.tgz", + "integrity": "sha512-BLOhLht9DOJwIxlmp91wHvkXv1lguuHS3/FwUO8HL1H0u8s4hR1gASVFyilu9iGtcTRYqjTZmlsFFeQletntEg==", + "peer": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/preset-env": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.29.7.tgz", + "integrity": "sha512-GYzX36n1nsciIb0uyH0GHwxwtNwPQIcpxSeiVLDtG/B7jB5xXgchnmL1f/jCX5o+pwnaDBtO60ONSJhEBJfxYA==", + "peer": true, + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.29.7", + "@babel/plugin-bugfix-safari-class-field-initializer-scope": "^7.29.7", + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.29.7", + "@babel/plugin-bugfix-safari-rest-destructuring-rhs-array": "^7.29.7", + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.29.7", + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.29.7", + "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", + "@babel/plugin-syntax-import-assertions": "^7.29.7", + "@babel/plugin-syntax-import-attributes": "^7.29.7", + "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", + "@babel/plugin-transform-arrow-functions": "^7.29.7", + "@babel/plugin-transform-async-generator-functions": "^7.29.7", + "@babel/plugin-transform-async-to-generator": "^7.29.7", + "@babel/plugin-transform-block-scoped-functions": "^7.29.7", + "@babel/plugin-transform-block-scoping": "^7.29.7", + "@babel/plugin-transform-class-properties": "^7.29.7", + "@babel/plugin-transform-class-static-block": "^7.29.7", + "@babel/plugin-transform-classes": "^7.29.7", + "@babel/plugin-transform-computed-properties": "^7.29.7", + "@babel/plugin-transform-destructuring": "^7.29.7", + "@babel/plugin-transform-dotall-regex": "^7.29.7", + "@babel/plugin-transform-duplicate-keys": "^7.29.7", + "@babel/plugin-transform-duplicate-named-capturing-groups-regex": "^7.29.7", + "@babel/plugin-transform-dynamic-import": "^7.29.7", + "@babel/plugin-transform-explicit-resource-management": "^7.29.7", + "@babel/plugin-transform-exponentiation-operator": "^7.29.7", + "@babel/plugin-transform-export-namespace-from": "^7.29.7", + "@babel/plugin-transform-for-of": "^7.29.7", + "@babel/plugin-transform-function-name": "^7.29.7", + "@babel/plugin-transform-json-strings": "^7.29.7", + "@babel/plugin-transform-literals": "^7.29.7", + "@babel/plugin-transform-logical-assignment-operators": "^7.29.7", + "@babel/plugin-transform-member-expression-literals": "^7.29.7", + "@babel/plugin-transform-modules-amd": "^7.29.7", + "@babel/plugin-transform-modules-commonjs": "^7.29.7", + "@babel/plugin-transform-modules-systemjs": "^7.29.7", + "@babel/plugin-transform-modules-umd": "^7.29.7", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.29.7", + "@babel/plugin-transform-new-target": "^7.29.7", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.29.7", + "@babel/plugin-transform-numeric-separator": "^7.29.7", + "@babel/plugin-transform-object-rest-spread": "^7.29.7", + "@babel/plugin-transform-object-super": "^7.29.7", + "@babel/plugin-transform-optional-catch-binding": "^7.29.7", + "@babel/plugin-transform-optional-chaining": "^7.29.7", + "@babel/plugin-transform-parameters": "^7.29.7", + "@babel/plugin-transform-private-methods": "^7.29.7", + "@babel/plugin-transform-private-property-in-object": "^7.29.7", + "@babel/plugin-transform-property-literals": "^7.29.7", + "@babel/plugin-transform-regenerator": "^7.29.7", + "@babel/plugin-transform-regexp-modifiers": "^7.29.7", + "@babel/plugin-transform-reserved-words": "^7.29.7", + "@babel/plugin-transform-shorthand-properties": "^7.29.7", + "@babel/plugin-transform-spread": "^7.29.7", + "@babel/plugin-transform-sticky-regex": "^7.29.7", + "@babel/plugin-transform-template-literals": "^7.29.7", + "@babel/plugin-transform-typeof-symbol": "^7.29.7", + "@babel/plugin-transform-unicode-escapes": "^7.29.7", + "@babel/plugin-transform-unicode-property-regex": "^7.29.7", + "@babel/plugin-transform-unicode-regex": "^7.29.7", + "@babel/plugin-transform-unicode-sets-regex": "^7.29.7", + "@babel/preset-modules": "0.1.6-no-external-plugins", + "babel-plugin-polyfill-corejs2": "^0.4.15", + "babel-plugin-polyfill-corejs3": "^0.14.0", + "babel-plugin-polyfill-regenerator": "^0.6.6", + "core-js-compat": "^3.48.0", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-env/node_modules/babel-plugin-polyfill-corejs3": { + "version": "0.14.2", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.14.2.tgz", + "integrity": "sha512-coWpDLJ410R781Npmn/SIBZEsAetR4xVi0SxLMXPaMO4lSf1MwnkGYMtkFxew0Dn8B3/CpbpYxN0JCgg8mn67g==", + "peer": true, + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.6.8", + "core-js-compat": "^3.48.0" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, "node_modules/@babel/preset-flow": { "version": "7.29.7", "license": "MIT", @@ -1303,6 +1986,20 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/preset-modules": { + "version": "0.1.6-no-external-plugins", + "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.6-no-external-plugins.tgz", + "integrity": "sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/types": "^7.4.4", + "esutils": "^2.0.2" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0 || ^8.0.0-0 <8.0.0" + } + }, "node_modules/@babel/preset-react": { "version": "7.29.7", "license": "MIT", @@ -1419,211 +2116,11 @@ "node": ">=6.9.0" } }, - "node_modules/@base-org/account": { - "version": "2.0.1", - "license": "Apache-2.0", - "dependencies": { - "@noble/hashes": "1.4.0", - "clsx": "1.2.1", - "eventemitter3": "5.0.1", - "idb-keyval": "6.2.1", - "ox": "0.6.9", - "preact": "10.24.2", - "viem": "^2.31.7", - "zustand": "5.0.3" - } - }, - "node_modules/@base-org/account/node_modules/zustand": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.3.tgz", - "integrity": "sha512-14fwWQtU3pH4dE0dOpdMiWjddcH+QzKIgk1cl8epwSE7yag43k/AD/m4L6+K7DytAOr9gGBe3/EXj9g7cdostg==", - "engines": { - "node": ">=12.20.0" - }, - "peerDependencies": { - "@types/react": ">=18.0.0", - "immer": ">=9.0.6", - "react": ">=18.0.0", - "use-sync-external-store": ">=1.2.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "immer": { - "optional": true - }, - "react": { - "optional": true - }, - "use-sync-external-store": { - "optional": true - } - } - }, "node_modules/@bcoe/v8-coverage": { "version": "0.2.3", "dev": true, "license": "MIT" }, - "node_modules/@clerk/clerk-expo": { - "version": "2.20.0", - "license": "MIT", - "dependencies": { - "@clerk/clerk-js": "^5.127.2", - "@clerk/clerk-react": "^5.61.9", - "@clerk/shared": "^3.47.8", - "@clerk/types": "^4.101.26", - "base-64": "^1.0.0", - "react-native-url-polyfill": "2.0.0", - "tslib": "2.8.1" - }, - "engines": { - "node": ">=18.17.0" - }, - "peerDependencies": { - "@clerk/expo-passkeys": ">=0.0.6", - "expo-apple-authentication": ">=7.0.0", - "expo-auth-session": ">=5", - "expo-crypto": ">=12", - "expo-local-authentication": ">=13.5.0", - "expo-secure-store": ">=12.4.0", - "expo-web-browser": ">=12.5.0", - "react": "^18.0.0 || ^19.0.0", - "react-dom": "^18.0.0 || ^19.0.0", - "react-native": ">=0.73" - }, - "peerDependenciesMeta": { - "@clerk/expo-passkeys": { - "optional": true - }, - "expo-apple-authentication": { - "optional": true - }, - "expo-crypto": { - "optional": true - }, - "expo-local-authentication": { - "optional": true - }, - "expo-secure-store": { - "optional": true - } - } - }, - "node_modules/@clerk/clerk-js": { - "version": "5.127.2", - "license": "MIT", - "dependencies": { - "@base-org/account": "2.0.1", - "@clerk/localizations": "^3.37.8", - "@clerk/shared": "^3.47.8", - "@coinbase/wallet-sdk": "4.3.0", - "@emotion/cache": "11.11.0", - "@emotion/react": "11.11.1", - "@floating-ui/react": "0.27.12", - "@floating-ui/react-dom": "^2.1.3", - "@formkit/auto-animate": "^0.8.2", - "@solana/wallet-adapter-base": "0.9.27", - "@solana/wallet-adapter-react": "0.15.39", - "@solana/wallet-standard": "1.1.4", - "@stripe/stripe-js": "5.6.0", - "@swc/helpers": "^0.5.17", - "@tanstack/query-core": "5.87.4", - "@wallet-standard/core": "1.1.1", - "@zxcvbn-ts/core": "3.0.4", - "@zxcvbn-ts/language-common": "3.0.4", - "alien-signals": "2.0.6", - "browser-tabs-lock": "1.3.0", - "copy-to-clipboard": "3.3.3", - "core-js": "3.41.0", - "crypto-js": "^4.2.0", - "dequal": "2.0.3", - "input-otp": "1.4.2", - "qrcode.react": "4.2.0", - "regenerator-runtime": "0.14.1" - }, - "engines": { - "node": ">=18.17.0" - }, - "peerDependencies": { - "react": "^18.0.0 || ~19.0.3 || ~19.1.4 || ~19.2.3 || ~19.3.0-0", - "react-dom": "^18.0.0 || ~19.0.3 || ~19.1.4 || ~19.2.3 || ~19.3.0-0" - } - }, - "node_modules/@clerk/clerk-react": { - "version": "5.61.9", - "license": "MIT", - "dependencies": { - "@clerk/shared": "^3.47.8", - "tslib": "2.8.1" - }, - "engines": { - "node": ">=18.17.0" - }, - "peerDependencies": { - "react": "^18.0.0 || ~19.0.3 || ~19.1.4 || ~19.2.3 || ~19.3.0-0", - "react-dom": "^18.0.0 || ~19.0.3 || ~19.1.4 || ~19.2.3 || ~19.3.0-0" - } - }, - "node_modules/@clerk/localizations": { - "version": "3.37.8", - "license": "MIT", - "dependencies": { - "@clerk/types": "^4.101.26" - }, - "engines": { - "node": ">=18.17.0" - } - }, - "node_modules/@clerk/shared": { - "version": "3.47.8", - "hasInstallScript": true, - "license": "MIT", - "dependencies": { - "csstype": "3.1.3", - "dequal": "2.0.3", - "glob-to-regexp": "0.4.1", - "js-cookie": "3.0.7", - "std-env": "^3.9.0", - "swr": "2.3.4" - }, - "engines": { - "node": ">=18.17.0" - }, - "peerDependencies": { - "react": "^18.0.0 || ~19.0.3 || ~19.1.4 || ~19.2.3 || ~19.3.0-0", - "react-dom": "^18.0.0 || ~19.0.3 || ~19.1.4 || ~19.2.3 || ~19.3.0-0" - }, - "peerDependenciesMeta": { - "react": { - "optional": true - }, - "react-dom": { - "optional": true - } - } - }, - "node_modules/@clerk/types": { - "version": "4.101.26", - "license": "MIT", - "dependencies": { - "@clerk/shared": "^3.47.8" - }, - "engines": { - "node": ">=18.17.0" - } - }, - "node_modules/@coinbase/wallet-sdk": { - "version": "4.3.0", - "license": "Apache-2.0", - "dependencies": { - "@noble/hashes": "^1.4.0", - "clsx": "^1.2.1", - "eventemitter3": "^5.0.1", - "preact": "^10.24.2" - } - }, "node_modules/@egjs/hammerjs": { "version": "2.0.17", "license": "MIT", @@ -1662,116 +2159,8 @@ "tslib": "^2.4.0" } }, - "node_modules/@emotion/babel-plugin": { - "version": "11.13.5", - "license": "MIT", - "dependencies": { - "@babel/helper-module-imports": "^7.16.7", - "@babel/runtime": "^7.18.3", - "@emotion/hash": "^0.9.2", - "@emotion/memoize": "^0.9.0", - "@emotion/serialize": "^1.3.3", - "babel-plugin-macros": "^3.1.0", - "convert-source-map": "^1.5.0", - "escape-string-regexp": "^4.0.0", - "find-root": "^1.1.0", - "source-map": "^0.5.7", - "stylis": "4.2.0" - } - }, - "node_modules/@emotion/babel-plugin/node_modules/@emotion/memoize": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz", - "integrity": "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==" - }, - "node_modules/@emotion/babel-plugin/node_modules/convert-source-map": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", - "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==" - }, - "node_modules/@emotion/cache": { - "version": "11.11.0", - "license": "MIT", - "dependencies": { - "@emotion/memoize": "^0.8.1", - "@emotion/sheet": "^1.2.2", - "@emotion/utils": "^1.2.1", - "@emotion/weak-memoize": "^0.3.1", - "stylis": "4.2.0" - } - }, - "node_modules/@emotion/hash": { - "version": "0.9.2", - "license": "MIT" - }, - "node_modules/@emotion/memoize": { - "version": "0.8.1", - "license": "MIT" - }, - "node_modules/@emotion/react": { - "version": "11.11.1", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.18.3", - "@emotion/babel-plugin": "^11.11.0", - "@emotion/cache": "^11.11.0", - "@emotion/serialize": "^1.1.2", - "@emotion/use-insertion-effect-with-fallbacks": "^1.0.1", - "@emotion/utils": "^1.2.1", - "@emotion/weak-memoize": "^0.3.1", - "hoist-non-react-statics": "^3.3.1" - }, - "peerDependencies": { - "react": ">=16.8.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@emotion/serialize": { - "version": "1.3.3", - "license": "MIT", - "dependencies": { - "@emotion/hash": "^0.9.2", - "@emotion/memoize": "^0.9.0", - "@emotion/unitless": "^0.10.0", - "@emotion/utils": "^1.4.2", - "csstype": "^3.0.2" - } - }, - "node_modules/@emotion/serialize/node_modules/@emotion/memoize": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz", - "integrity": "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==" - }, - "node_modules/@emotion/sheet": { - "version": "1.4.0", - "license": "MIT" - }, - "node_modules/@emotion/unitless": { - "version": "0.10.0", - "license": "MIT" - }, - "node_modules/@emotion/use-insertion-effect-with-fallbacks": { - "version": "1.2.0", - "license": "MIT", - "peerDependencies": { - "react": ">=16.8.0" - } - }, - "node_modules/@emotion/utils": { - "version": "1.4.2", - "license": "MIT" - }, - "node_modules/@emotion/weak-memoize": { - "version": "0.3.1", - "license": "MIT" - }, "node_modules/@eslint-community/eslint-utils": { "version": "4.10.1", - "dev": true, "license": "MIT", "dependencies": { "eslint-visitor-keys": "^3.4.3" @@ -1788,7 +2177,6 @@ }, "node_modules/@eslint-community/regexpp": { "version": "4.12.2", - "dev": true, "license": "MIT", "engines": { "node": "^12.0.0 || ^14.0.0 || >=16.0.0" @@ -1796,7 +2184,6 @@ }, "node_modules/@eslint/eslintrc": { "version": "2.1.4", - "dev": true, "license": "MIT", "dependencies": { "ajv": "^6.12.4", @@ -1816,9 +2203,28 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/@eslint/eslintrc/node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@eslint/eslintrc/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" + }, "node_modules/@eslint/js": { "version": "8.57.1", - "dev": true, "license": "MIT", "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" @@ -2667,53 +3073,6 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/@floating-ui/core": { - "version": "1.8.0", - "license": "MIT", - "dependencies": { - "@floating-ui/utils": "^0.2.12" - } - }, - "node_modules/@floating-ui/dom": { - "version": "1.8.0", - "license": "MIT", - "dependencies": { - "@floating-ui/core": "^1.8.0", - "@floating-ui/utils": "^0.2.12" - } - }, - "node_modules/@floating-ui/react": { - "version": "0.27.12", - "license": "MIT", - "dependencies": { - "@floating-ui/react-dom": "^2.1.3", - "@floating-ui/utils": "^0.2.9", - "tabbable": "^6.0.0" - }, - "peerDependencies": { - "react": ">=17.0.0", - "react-dom": ">=17.0.0" - } - }, - "node_modules/@floating-ui/react-dom": { - "version": "2.1.9", - "license": "MIT", - "dependencies": { - "@floating-ui/dom": "^1.8.0" - }, - "peerDependencies": { - "react": ">=16.8.0", - "react-dom": ">=16.8.0" - } - }, - "node_modules/@floating-ui/utils": { - "version": "0.2.12", - "license": "MIT" - }, - "node_modules/@formkit/auto-animate": { - "version": "0.8.4", - "license": "MIT" - }, "node_modules/@gorhom/bottom-sheet": { "version": "4.6.4", "license": "MIT", @@ -2769,7 +3128,6 @@ }, "node_modules/@humanwhocodes/config-array": { "version": "0.13.0", - "dev": true, "license": "Apache-2.0", "dependencies": { "@humanwhocodes/object-schema": "^2.0.3", @@ -2782,7 +3140,6 @@ }, "node_modules/@humanwhocodes/module-importer": { "version": "1.0.1", - "dev": true, "license": "Apache-2.0", "engines": { "node": ">=12.22" @@ -2794,7 +3151,6 @@ }, "node_modules/@humanwhocodes/object-schema": { "version": "2.0.3", - "dev": true, "license": "BSD-3-Clause" }, "node_modules/@isaacs/cliui": { @@ -3465,57 +3821,6 @@ "@emnapi/runtime": "^1.7.1 || ^2.0.0-alpha.4" } }, - "node_modules/@neondatabase/serverless": { - "version": "0.9.5", - "license": "MIT", - "dependencies": { - "@types/pg": "8.11.6" - } - }, - "node_modules/@noble/ciphers": { - "version": "1.3.0", - "license": "MIT", - "engines": { - "node": "^14.21.3 || >=16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@noble/curves": { - "version": "2.3.0", - "license": "MIT", - "dependencies": { - "@noble/hashes": "2.3.0" - }, - "engines": { - "node": ">= 20.19.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@noble/curves/node_modules/@noble/hashes": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.3.0.tgz", - "integrity": "sha512-oN+QwyX7VSHotibwubG3kpzbwKrfnyR6OOO+3Nk/53ADL7FmgHHz4TgrbaYKvvOw09u6QTx0oiH1cNCIOuN0CQ==", - "engines": { - "node": ">= 20.19.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@noble/hashes": { - "version": "1.4.0", - "license": "MIT", - "engines": { - "node": ">= 16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "license": "MIT", @@ -3612,17 +3917,6 @@ "react": "^16.8 || ^17.0 || ^18.0" } }, - "node_modules/@react-native-async-storage/async-storage": { - "version": "1.24.0", - "license": "MIT", - "optional": true, - "dependencies": { - "merge-options": "^3.0.4" - }, - "peerDependencies": { - "react-native": "^0.0.0-0 || >=0.60 <1.0" - } - }, "node_modules/@react-native-community/cli": { "version": "13.6.9", "license": "MIT", @@ -5478,69 +5772,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@scure/base": { - "version": "1.2.6", - "license": "MIT", - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@scure/bip32": { - "version": "1.7.0", - "license": "MIT", - "dependencies": { - "@noble/curves": "~1.9.0", - "@noble/hashes": "~1.8.0", - "@scure/base": "~1.2.5" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@scure/bip32/node_modules/@noble/curves": { - "version": "1.9.7", - "license": "MIT", - "dependencies": { - "@noble/hashes": "1.8.0" - }, - "engines": { - "node": "^14.21.3 || >=16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@scure/bip32/node_modules/@noble/hashes": { - "version": "1.8.0", - "license": "MIT", - "engines": { - "node": "^14.21.3 || >=16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@scure/bip39": { - "version": "1.6.0", - "license": "MIT", - "dependencies": { - "@noble/hashes": "~1.8.0", - "@scure/base": "~1.2.5" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@scure/bip39/node_modules/@noble/hashes": { - "version": "1.8.0", - "license": "MIT", - "engines": { - "node": "^14.21.3 || >=16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, "node_modules/@segment/loosely-validate-event": { "version": "2.0.0", "dependencies": { @@ -5581,1215 +5812,6 @@ "@sinonjs/commons": "^3.0.0" } }, - "node_modules/@solana-mobile/mobile-wallet-adapter-protocol": { - "version": "2.3.0", - "license": "Apache-2.0", - "dependencies": { - "@noble/curves": "^2.2.0", - "@noble/hashes": "^2.2.0", - "@solana/kit": "^7.0.0", - "@solana/wallet-standard-features": "^1.3.0", - "@solana/wallet-standard-util": "^1.1.2", - "@wallet-standard/core": "^1.1.1" - }, - "peerDependencies": { - "react-native": ">0.74" - } - }, - "node_modules/@solana-mobile/mobile-wallet-adapter-protocol-web3js": { - "version": "2.3.0", - "license": "Apache-2.0", - "dependencies": { - "@solana-mobile/mobile-wallet-adapter-protocol": "^2.3.0" - }, - "peerDependencies": { - "@solana/web3.js": "^1.98.4" - } - }, - "node_modules/@solana-mobile/mobile-wallet-adapter-protocol/node_modules/@noble/hashes": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.3.0.tgz", - "integrity": "sha512-oN+QwyX7VSHotibwubG3kpzbwKrfnyR6OOO+3Nk/53ADL7FmgHHz4TgrbaYKvvOw09u6QTx0oiH1cNCIOuN0CQ==", - "engines": { - "node": ">= 20.19.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@solana-mobile/wallet-adapter-mobile": { - "version": "2.3.0", - "license": "Apache-2.0", - "dependencies": { - "@solana-mobile/mobile-wallet-adapter-protocol": "^2.3.0", - "@solana-mobile/mobile-wallet-adapter-protocol-web3js": "^2.3.0", - "@solana-mobile/wallet-standard-mobile": "^0.6.0", - "@solana/wallet-adapter-base": "^0.9.27", - "@solana/wallet-standard-features": "^1.3.0", - "@wallet-standard/core": "^1.1.1", - "tslib": "^2.8.1" - }, - "optionalDependencies": { - "@react-native-async-storage/async-storage": "^1.17.7" - }, - "peerDependencies": { - "@solana/web3.js": "^1.98.4", - "react-native": ">0.74" - } - }, - "node_modules/@solana-mobile/wallet-standard-mobile": { - "version": "0.6.0", - "license": "Apache-2.0", - "dependencies": { - "@solana-mobile/mobile-wallet-adapter-protocol": "^2.3.0", - "@solana/wallet-standard-chains": "^1.1.1", - "@solana/wallet-standard-features": "^1.3.0", - "@wallet-standard/base": "^1.0.1", - "@wallet-standard/features": "^1.0.3", - "@wallet-standard/wallet": "^1.1.0", - "qrcode": "^1.5.4", - "tslib": "^2.8.1" - }, - "optionalDependencies": { - "@react-native-async-storage/async-storage": "^1.17.7" - } - }, - "node_modules/@solana/accounts": { - "version": "7.1.1", - "license": "MIT", - "dependencies": { - "@solana/addresses": "7.1.1", - "@solana/codecs-core": "7.1.1", - "@solana/codecs-strings": "7.1.1", - "@solana/errors": "7.1.1", - "@solana/rpc-spec": "7.1.1", - "@solana/rpc-types": "7.1.1" - }, - "engines": { - "node": ">=20.18.0" - }, - "peerDependencies": { - "typescript": ">=5.4.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@solana/addresses": { - "version": "7.1.1", - "license": "MIT", - "dependencies": { - "@solana/assertions": "7.1.1", - "@solana/codecs-core": "7.1.1", - "@solana/codecs-strings": "7.1.1", - "@solana/errors": "7.1.1", - "@solana/nominal-types": "7.1.1" - }, - "engines": { - "node": ">=20.18.0" - }, - "peerDependencies": { - "typescript": ">=5.4.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@solana/assertions": { - "version": "7.1.1", - "license": "MIT", - "dependencies": { - "@solana/errors": "7.1.1" - }, - "engines": { - "node": ">=20.18.0" - }, - "peerDependencies": { - "typescript": ">=5.4.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@solana/codecs": { - "version": "7.1.1", - "license": "MIT", - "dependencies": { - "@solana/codecs-core": "7.1.1", - "@solana/codecs-data-structures": "7.1.1", - "@solana/codecs-numbers": "7.1.1", - "@solana/codecs-strings": "7.1.1", - "@solana/fixed-points": "7.1.1", - "@solana/options": "7.1.1" - }, - "engines": { - "node": ">=20.18.0" - }, - "peerDependencies": { - "typescript": ">=5.4.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@solana/codecs-core": { - "version": "7.1.1", - "license": "MIT", - "dependencies": { - "@solana/errors": "7.1.1" - }, - "engines": { - "node": ">=20.18.0" - }, - "peerDependencies": { - "typescript": ">=5.4.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@solana/codecs-data-structures": { - "version": "7.1.1", - "license": "MIT", - "dependencies": { - "@solana/codecs-core": "7.1.1", - "@solana/codecs-numbers": "7.1.1", - "@solana/errors": "7.1.1" - }, - "engines": { - "node": ">=20.18.0" - }, - "peerDependencies": { - "typescript": ">=5.4.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@solana/codecs-numbers": { - "version": "7.1.1", - "license": "MIT", - "dependencies": { - "@solana/codecs-core": "7.1.1", - "@solana/errors": "7.1.1" - }, - "engines": { - "node": ">=20.18.0" - }, - "peerDependencies": { - "typescript": ">=5.4.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@solana/codecs-strings": { - "version": "7.1.1", - "license": "MIT", - "dependencies": { - "@solana/codecs-core": "7.1.1", - "@solana/codecs-numbers": "7.1.1", - "@solana/errors": "7.1.1" - }, - "engines": { - "node": ">=20.18.0" - }, - "peerDependencies": { - "fastestsmallesttextencoderdecoder": "^1.0.22", - "typescript": ">=5.4.0" - }, - "peerDependenciesMeta": { - "fastestsmallesttextencoderdecoder": { - "optional": true - }, - "typescript": { - "optional": true - } - } - }, - "node_modules/@solana/errors": { - "version": "7.1.1", - "license": "MIT", - "dependencies": { - "chalk": "5.6.2", - "commander": "15.0.0" - }, - "bin": { - "errors": "bin/cli.mjs" - }, - "engines": { - "node": ">=20.18.0" - }, - "peerDependencies": { - "typescript": ">=5.4.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@solana/fast-stable-stringify": { - "version": "7.1.1", - "license": "MIT", - "engines": { - "node": ">=20.18.0" - }, - "peerDependencies": { - "typescript": ">=5.4.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@solana/fixed-points": { - "version": "7.1.1", - "license": "MIT", - "dependencies": { - "@solana/codecs-core": "7.1.1", - "@solana/errors": "7.1.1" - }, - "engines": { - "node": ">=20.18.0" - }, - "peerDependencies": { - "typescript": ">=5.4.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@solana/functional": { - "version": "7.1.1", - "license": "MIT", - "engines": { - "node": ">=20.18.0" - }, - "peerDependencies": { - "typescript": ">=5.4.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@solana/instruction-plans": { - "version": "7.1.1", - "license": "MIT", - "dependencies": { - "@solana/errors": "7.1.1", - "@solana/instructions": "7.1.1", - "@solana/keys": "7.1.1", - "@solana/promises": "7.1.1", - "@solana/transaction-messages": "7.1.1", - "@solana/transactions": "7.1.1" - }, - "engines": { - "node": ">=20.18.0" - }, - "peerDependencies": { - "typescript": ">=5.4.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@solana/instructions": { - "version": "7.1.1", - "license": "MIT", - "dependencies": { - "@solana/codecs-core": "7.1.1", - "@solana/errors": "7.1.1" - }, - "engines": { - "node": ">=20.18.0" - }, - "peerDependencies": { - "typescript": ">=5.4.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@solana/keys": { - "version": "7.1.1", - "license": "MIT", - "dependencies": { - "@solana/assertions": "7.1.1", - "@solana/codecs-core": "7.1.1", - "@solana/codecs-strings": "7.1.1", - "@solana/errors": "7.1.1", - "@solana/nominal-types": "7.1.1", - "@solana/promises": "7.1.1" - }, - "engines": { - "node": ">=20.18.0" - }, - "peerDependencies": { - "typescript": ">=5.4.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@solana/kit": { - "version": "7.1.1", - "license": "MIT", - "dependencies": { - "@solana/accounts": "7.1.1", - "@solana/addresses": "7.1.1", - "@solana/codecs": "7.1.1", - "@solana/errors": "7.1.1", - "@solana/functional": "7.1.1", - "@solana/instruction-plans": "7.1.1", - "@solana/instructions": "7.1.1", - "@solana/keys": "7.1.1", - "@solana/offchain-messages": "7.1.1", - "@solana/plugin-core": "7.1.1", - "@solana/plugin-interfaces": "7.1.1", - "@solana/program-client-core": "7.1.1", - "@solana/programs": "7.1.1", - "@solana/promises": "7.1.1", - "@solana/rpc": "7.1.1", - "@solana/rpc-api": "7.1.1", - "@solana/rpc-parsed-types": "7.1.1", - "@solana/rpc-spec-types": "7.1.1", - "@solana/rpc-subscriptions": "7.1.1", - "@solana/rpc-types": "7.1.1", - "@solana/signers": "7.1.1", - "@solana/subscribable": "7.1.1", - "@solana/sysvars": "7.1.1", - "@solana/transaction-confirmation": "7.1.1", - "@solana/transaction-introspection": "7.1.1", - "@solana/transaction-messages": "7.1.1", - "@solana/transactions": "7.1.1" - }, - "engines": { - "node": ">=20.18.0" - }, - "peerDependencies": { - "typescript": ">=5.4.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@solana/nominal-types": { - "version": "7.1.1", - "license": "MIT", - "engines": { - "node": ">=20.18.0" - }, - "peerDependencies": { - "typescript": ">=5.4.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@solana/offchain-messages": { - "version": "7.1.1", - "license": "MIT", - "dependencies": { - "@solana/addresses": "7.1.1", - "@solana/codecs-core": "7.1.1", - "@solana/codecs-data-structures": "7.1.1", - "@solana/codecs-numbers": "7.1.1", - "@solana/codecs-strings": "7.1.1", - "@solana/errors": "7.1.1", - "@solana/keys": "7.1.1", - "@solana/nominal-types": "7.1.1" - }, - "engines": { - "node": ">=20.18.0" - }, - "peerDependencies": { - "typescript": ">=5.4.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@solana/options": { - "version": "7.1.1", - "license": "MIT", - "dependencies": { - "@solana/codecs-core": "7.1.1", - "@solana/codecs-data-structures": "7.1.1", - "@solana/codecs-numbers": "7.1.1", - "@solana/codecs-strings": "7.1.1", - "@solana/errors": "7.1.1" - }, - "engines": { - "node": ">=20.18.0" - }, - "peerDependencies": { - "typescript": ">=5.4.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@solana/plugin-core": { - "version": "7.1.1", - "license": "MIT", - "engines": { - "node": ">=20.18.0" - }, - "peerDependencies": { - "typescript": ">=5.4.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@solana/plugin-interfaces": { - "version": "7.1.1", - "license": "MIT", - "dependencies": { - "@solana/accounts": "7.1.1", - "@solana/addresses": "7.1.1", - "@solana/instruction-plans": "7.1.1", - "@solana/keys": "7.1.1", - "@solana/rpc-spec": "7.1.1", - "@solana/rpc-subscriptions-spec": "7.1.1", - "@solana/rpc-types": "7.1.1", - "@solana/signers": "7.1.1" - }, - "engines": { - "node": ">=20.18.0" - }, - "peerDependencies": { - "typescript": ">=5.4.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@solana/program-client-core": { - "version": "7.1.1", - "license": "MIT", - "dependencies": { - "@solana/accounts": "7.1.1", - "@solana/addresses": "7.1.1", - "@solana/codecs-core": "7.1.1", - "@solana/errors": "7.1.1", - "@solana/instruction-plans": "7.1.1", - "@solana/instructions": "7.1.1", - "@solana/plugin-interfaces": "7.1.1", - "@solana/rpc-api": "7.1.1", - "@solana/signers": "7.1.1" - }, - "engines": { - "node": ">=20.18.0" - }, - "peerDependencies": { - "typescript": ">=5.4.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@solana/programs": { - "version": "7.1.1", - "license": "MIT", - "dependencies": { - "@solana/addresses": "7.1.1", - "@solana/errors": "7.1.1" - }, - "engines": { - "node": ">=20.18.0" - }, - "peerDependencies": { - "typescript": ">=5.4.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@solana/promises": { - "version": "7.1.1", - "license": "MIT", - "engines": { - "node": ">=20.18.0" - }, - "peerDependencies": { - "typescript": ">=5.4.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@solana/rpc": { - "version": "7.1.1", - "license": "MIT", - "dependencies": { - "@solana/errors": "7.1.1", - "@solana/fast-stable-stringify": "7.1.1", - "@solana/functional": "7.1.1", - "@solana/rpc-api": "7.1.1", - "@solana/rpc-spec": "7.1.1", - "@solana/rpc-spec-types": "7.1.1", - "@solana/rpc-transformers": "7.1.1", - "@solana/rpc-transport-http": "7.1.1", - "@solana/rpc-types": "7.1.1" - }, - "engines": { - "node": ">=20.18.0" - }, - "peerDependencies": { - "typescript": ">=5.4.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@solana/rpc-api": { - "version": "7.1.1", - "license": "MIT", - "dependencies": { - "@solana/addresses": "7.1.1", - "@solana/codecs-core": "7.1.1", - "@solana/codecs-strings": "7.1.1", - "@solana/errors": "7.1.1", - "@solana/keys": "7.1.1", - "@solana/rpc-parsed-types": "7.1.1", - "@solana/rpc-spec": "7.1.1", - "@solana/rpc-transformers": "7.1.1", - "@solana/rpc-types": "7.1.1", - "@solana/transaction-messages": "7.1.1", - "@solana/transactions": "7.1.1" - }, - "engines": { - "node": ">=20.18.0" - }, - "peerDependencies": { - "typescript": ">=5.4.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@solana/rpc-parsed-types": { - "version": "7.1.1", - "license": "MIT", - "engines": { - "node": ">=20.18.0" - }, - "peerDependencies": { - "typescript": ">=5.4.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@solana/rpc-spec": { - "version": "7.1.1", - "license": "MIT", - "dependencies": { - "@solana/errors": "7.1.1", - "@solana/rpc-spec-types": "7.1.1", - "@solana/subscribable": "7.1.1" - }, - "engines": { - "node": ">=20.18.0" - }, - "peerDependencies": { - "typescript": ">=5.4.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@solana/rpc-spec-types": { - "version": "7.1.1", - "license": "MIT", - "dependencies": { - "@solana/errors": "7.1.1" - }, - "engines": { - "node": ">=20.18.0" - }, - "peerDependencies": { - "typescript": ">=5.4.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@solana/rpc-subscriptions": { - "version": "7.1.1", - "license": "MIT", - "dependencies": { - "@solana/errors": "7.1.1", - "@solana/fast-stable-stringify": "7.1.1", - "@solana/functional": "7.1.1", - "@solana/promises": "7.1.1", - "@solana/rpc-spec-types": "7.1.1", - "@solana/rpc-subscriptions-api": "7.1.1", - "@solana/rpc-subscriptions-channel-websocket": "7.1.1", - "@solana/rpc-subscriptions-spec": "7.1.1", - "@solana/rpc-transformers": "7.1.1", - "@solana/rpc-types": "7.1.1", - "@solana/subscribable": "7.1.1" - }, - "engines": { - "node": ">=20.18.0" - }, - "peerDependencies": { - "typescript": ">=5.4.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@solana/rpc-subscriptions-api": { - "version": "7.1.1", - "license": "MIT", - "dependencies": { - "@solana/addresses": "7.1.1", - "@solana/keys": "7.1.1", - "@solana/rpc-subscriptions-spec": "7.1.1", - "@solana/rpc-transformers": "7.1.1", - "@solana/rpc-types": "7.1.1", - "@solana/transaction-messages": "7.1.1", - "@solana/transactions": "7.1.1" - }, - "engines": { - "node": ">=20.18.0" - }, - "peerDependencies": { - "typescript": ">=5.4.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@solana/rpc-subscriptions-channel-websocket": { - "version": "7.1.1", - "license": "MIT", - "dependencies": { - "@solana/errors": "7.1.1", - "@solana/functional": "7.1.1", - "@solana/rpc-subscriptions-spec": "7.1.1", - "@solana/subscribable": "7.1.1", - "ws": "^8.21.0" - }, - "engines": { - "node": ">=20.18.0" - }, - "peerDependencies": { - "typescript": ">=5.4.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@solana/rpc-subscriptions-spec": { - "version": "7.1.1", - "license": "MIT", - "dependencies": { - "@solana/errors": "7.1.1", - "@solana/promises": "7.1.1", - "@solana/rpc-spec-types": "7.1.1", - "@solana/subscribable": "7.1.1" - }, - "engines": { - "node": ">=20.18.0" - }, - "peerDependencies": { - "typescript": ">=5.4.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@solana/rpc-transformers": { - "version": "7.1.1", - "license": "MIT", - "dependencies": { - "@solana/errors": "7.1.1", - "@solana/functional": "7.1.1", - "@solana/nominal-types": "7.1.1", - "@solana/rpc-spec-types": "7.1.1", - "@solana/rpc-types": "7.1.1" - }, - "engines": { - "node": ">=20.18.0" - }, - "peerDependencies": { - "typescript": ">=5.4.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@solana/rpc-transport-http": { - "version": "7.1.1", - "license": "MIT", - "dependencies": { - "@solana/errors": "7.1.1", - "@solana/rpc-spec": "7.1.1", - "@solana/rpc-spec-types": "7.1.1", - "undici-types": "^8.10.0" - }, - "engines": { - "node": ">=20.18.0" - }, - "peerDependencies": { - "typescript": ">=5.4.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@solana/rpc-types": { - "version": "7.1.1", - "license": "MIT", - "dependencies": { - "@solana/addresses": "7.1.1", - "@solana/codecs-core": "7.1.1", - "@solana/codecs-numbers": "7.1.1", - "@solana/codecs-strings": "7.1.1", - "@solana/errors": "7.1.1", - "@solana/fixed-points": "7.1.1", - "@solana/nominal-types": "7.1.1" - }, - "engines": { - "node": ">=20.18.0" - }, - "peerDependencies": { - "typescript": ">=5.4.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@solana/signers": { - "version": "7.1.1", - "license": "MIT", - "dependencies": { - "@solana/addresses": "7.1.1", - "@solana/codecs-core": "7.1.1", - "@solana/errors": "7.1.1", - "@solana/instructions": "7.1.1", - "@solana/keys": "7.1.1", - "@solana/nominal-types": "7.1.1", - "@solana/offchain-messages": "7.1.1", - "@solana/transaction-messages": "7.1.1", - "@solana/transactions": "7.1.1" - }, - "engines": { - "node": ">=20.18.0" - }, - "peerDependencies": { - "typescript": ">=5.4.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@solana/subscribable": { - "version": "7.1.1", - "license": "MIT", - "dependencies": { - "@solana/errors": "7.1.1", - "@solana/promises": "7.1.1" - }, - "engines": { - "node": ">=20.18.0" - }, - "peerDependencies": { - "typescript": ">=5.4.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@solana/sysvars": { - "version": "7.1.1", - "license": "MIT", - "dependencies": { - "@solana/accounts": "7.1.1", - "@solana/codecs-core": "7.1.1", - "@solana/codecs-data-structures": "7.1.1", - "@solana/codecs-numbers": "7.1.1", - "@solana/errors": "7.1.1", - "@solana/rpc-types": "7.1.1" - }, - "engines": { - "node": ">=20.18.0" - }, - "peerDependencies": { - "typescript": ">=5.4.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@solana/transaction-confirmation": { - "version": "7.1.1", - "license": "MIT", - "dependencies": { - "@solana/addresses": "7.1.1", - "@solana/codecs-strings": "7.1.1", - "@solana/errors": "7.1.1", - "@solana/keys": "7.1.1", - "@solana/promises": "7.1.1", - "@solana/rpc": "7.1.1", - "@solana/rpc-subscriptions": "7.1.1", - "@solana/rpc-types": "7.1.1", - "@solana/transaction-messages": "7.1.1", - "@solana/transactions": "7.1.1" - }, - "engines": { - "node": ">=20.18.0" - }, - "peerDependencies": { - "typescript": ">=5.4.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@solana/transaction-introspection": { - "version": "7.1.1", - "license": "MIT", - "dependencies": { - "@solana/addresses": "7.1.1", - "@solana/codecs-core": "7.1.1", - "@solana/codecs-strings": "7.1.1", - "@solana/errors": "7.1.1", - "@solana/instructions": "7.1.1", - "@solana/rpc-types": "7.1.1", - "@solana/transaction-messages": "7.1.1", - "@solana/transactions": "7.1.1" - }, - "engines": { - "node": ">=20.18.0" - }, - "peerDependencies": { - "typescript": ">=5.4.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@solana/transaction-messages": { - "version": "7.1.1", - "license": "MIT", - "dependencies": { - "@solana/addresses": "7.1.1", - "@solana/codecs-core": "7.1.1", - "@solana/codecs-data-structures": "7.1.1", - "@solana/codecs-numbers": "7.1.1", - "@solana/errors": "7.1.1", - "@solana/functional": "7.1.1", - "@solana/instructions": "7.1.1", - "@solana/nominal-types": "7.1.1", - "@solana/rpc-types": "7.1.1" - }, - "engines": { - "node": ">=20.18.0" - }, - "peerDependencies": { - "typescript": ">=5.4.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@solana/transactions": { - "version": "7.1.1", - "license": "MIT", - "dependencies": { - "@solana/addresses": "7.1.1", - "@solana/codecs-core": "7.1.1", - "@solana/codecs-data-structures": "7.1.1", - "@solana/codecs-numbers": "7.1.1", - "@solana/codecs-strings": "7.1.1", - "@solana/errors": "7.1.1", - "@solana/functional": "7.1.1", - "@solana/instructions": "7.1.1", - "@solana/keys": "7.1.1", - "@solana/nominal-types": "7.1.1", - "@solana/rpc-types": "7.1.1", - "@solana/transaction-messages": "7.1.1" - }, - "engines": { - "node": ">=20.18.0" - }, - "peerDependencies": { - "typescript": ">=5.4.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@solana/wallet-adapter-base": { - "version": "0.9.27", - "license": "Apache-2.0", - "dependencies": { - "@solana/wallet-standard-features": "^1.3.0", - "@wallet-standard/base": "^1.1.0", - "@wallet-standard/features": "^1.1.0", - "eventemitter3": "^5.0.1" - }, - "engines": { - "node": ">=20" - }, - "peerDependencies": { - "@solana/web3.js": "^1.98.0" - } - }, - "node_modules/@solana/wallet-adapter-react": { - "version": "0.15.39", - "license": "Apache-2.0", - "dependencies": { - "@solana-mobile/wallet-adapter-mobile": "^2.2.0", - "@solana/wallet-adapter-base": "^0.9.27", - "@solana/wallet-standard-wallet-adapter-react": "^1.1.4" - }, - "engines": { - "node": ">=20" - }, - "peerDependencies": { - "@solana/web3.js": "^1.98.0", - "react": "*" - } - }, - "node_modules/@solana/wallet-standard": { - "version": "1.1.4", - "license": "Apache-2.0", - "dependencies": { - "@solana/wallet-standard-core": "^1.1.2", - "@solana/wallet-standard-wallet-adapter": "^1.1.4" - }, - "engines": { - "node": ">=16" - } - }, - "node_modules/@solana/wallet-standard-chains": { - "version": "1.1.2", - "license": "Apache-2.0", - "dependencies": { - "@wallet-standard/base": "^1.1.0" - }, - "engines": { - "node": ">=22" - } - }, - "node_modules/@solana/wallet-standard-core": { - "version": "1.1.3", - "license": "Apache-2.0", - "dependencies": { - "@solana/wallet-standard-chains": "^1.1.2", - "@solana/wallet-standard-features": "^1.4.0", - "@solana/wallet-standard-util": "^1.1.3" - }, - "engines": { - "node": ">=22" - } - }, - "node_modules/@solana/wallet-standard-features": { - "version": "1.4.0", - "license": "Apache-2.0", - "dependencies": { - "@wallet-standard/base": "^1.1.0", - "@wallet-standard/features": "^1.1.0" - }, - "engines": { - "node": ">=22" - } - }, - "node_modules/@solana/wallet-standard-util": { - "version": "1.1.3", - "license": "Apache-2.0", - "dependencies": { - "@noble/curves": "^1.8.2", - "@solana/wallet-standard-chains": "^1.1.2", - "@solana/wallet-standard-features": "^1.4.0" - }, - "engines": { - "node": ">=22" - } - }, - "node_modules/@solana/wallet-standard-util/node_modules/@noble/curves": { - "version": "1.9.7", - "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.7.tgz", - "integrity": "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw==", - "dependencies": { - "@noble/hashes": "1.8.0" - }, - "engines": { - "node": "^14.21.3 || >=16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@solana/wallet-standard-util/node_modules/@noble/hashes": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", - "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", - "engines": { - "node": "^14.21.3 || >=16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@solana/wallet-standard-wallet-adapter": { - "version": "1.1.6", - "license": "Apache-2.0", - "dependencies": { - "@solana/wallet-standard-wallet-adapter-base": "^1.1.5", - "@solana/wallet-standard-wallet-adapter-react": "^1.1.6" - }, - "engines": { - "node": ">=22" - } - }, - "node_modules/@solana/wallet-standard-wallet-adapter-base": { - "version": "1.1.5", - "license": "Apache-2.0", - "dependencies": { - "@solana/wallet-adapter-base": "^0.9.24", - "@solana/wallet-standard-chains": "^1.1.2", - "@solana/wallet-standard-features": "^1.4.0", - "@solana/wallet-standard-util": "^1.1.3", - "@wallet-standard/app": "^1.1.0", - "@wallet-standard/base": "^1.1.0", - "@wallet-standard/features": "^1.1.0", - "@wallet-standard/wallet": "^1.1.0" - }, - "engines": { - "node": ">=22" - }, - "peerDependencies": { - "@solana/web3.js": "^1.98.0", - "bs58": "^6.0.0" - } - }, - "node_modules/@solana/wallet-standard-wallet-adapter-react": { - "version": "1.1.6", - "license": "Apache-2.0", - "dependencies": { - "@solana/wallet-standard-wallet-adapter-base": "^1.1.5", - "@wallet-standard/app": "^1.1.0", - "@wallet-standard/base": "^1.1.0" - }, - "engines": { - "node": ">=22" - }, - "peerDependencies": { - "@solana/wallet-adapter-base": "*", - "react": "*" - } - }, - "node_modules/@stripe/stripe-js": { - "version": "5.6.0", - "license": "MIT", - "engines": { - "node": ">=12.16" - } - }, - "node_modules/@swc/helpers": { - "version": "0.5.23", - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.8.0" - } - }, - "node_modules/@tanstack/query-core": { - "version": "5.87.4", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" - } - }, "node_modules/@tootallnate/once": { "version": "2.0.1", "dev": true, @@ -6930,25 +5952,30 @@ }, "node_modules/@types/parse-json": { "version": "4.0.2", - "license": "MIT" + "dev": true, + "license": "MIT", + "optional": true, + "peer": true }, "node_modules/@types/pg": { - "version": "8.11.6", - "license": "MIT", + "version": "8.23.1", + "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.23.1.tgz", + "integrity": "sha512-fKVHpikPdg4GKks3JuLEhvwSyvwzF23hnabPy6DD8ljVbC7+6J5dQzdv4arV6jqq57djnMgs1HKBxX4P8aBI3A==", + "dev": true, "dependencies": { "@types/node": "*", "pg-protocol": "*", - "pg-types": "^4.0.1" + "pg-types": "^2.2.0" } }, "node_modules/@types/prop-types": { "version": "15.7.15", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/@types/react": { "version": "18.2.79", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@types/prop-types": "*", @@ -7195,7 +6222,6 @@ }, "node_modules/@ungap/structured-clone": { "version": "1.3.3", - "dev": true, "license": "ISC" }, "node_modules/@unrs/resolver-binding-linux-x64-gnu": { @@ -7244,79 +6270,6 @@ "graphql": "^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0" } }, - "node_modules/@wallet-standard/app": { - "version": "1.1.1", - "license": "Apache-2.0", - "dependencies": { - "@wallet-standard/base": "^1.1.1" - }, - "engines": { - "node": ">=22" - } - }, - "node_modules/@wallet-standard/base": { - "version": "1.1.1", - "license": "Apache-2.0", - "engines": { - "node": ">=22" - } - }, - "node_modules/@wallet-standard/core": { - "version": "1.1.1", - "license": "Apache-2.0", - "dependencies": { - "@wallet-standard/app": "^1.1.0", - "@wallet-standard/base": "^1.1.0", - "@wallet-standard/errors": "^0.1.1", - "@wallet-standard/features": "^1.1.0", - "@wallet-standard/wallet": "^1.1.0" - }, - "engines": { - "node": ">=16" - } - }, - "node_modules/@wallet-standard/errors": { - "version": "0.1.2", - "license": "Apache-2.0", - "dependencies": { - "chalk": "^5.4.1", - "commander": "^13.1.0" - }, - "bin": { - "errors": "bin/cli.mjs" - }, - "engines": { - "node": ">=22" - } - }, - "node_modules/@wallet-standard/errors/node_modules/commander": { - "version": "13.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-13.1.0.tgz", - "integrity": "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==", - "engines": { - "node": ">=18" - } - }, - "node_modules/@wallet-standard/features": { - "version": "1.1.1", - "license": "Apache-2.0", - "dependencies": { - "@wallet-standard/base": "^1.1.1" - }, - "engines": { - "node": ">=22" - } - }, - "node_modules/@wallet-standard/wallet": { - "version": "1.1.1", - "license": "Apache-2.0", - "dependencies": { - "@wallet-standard/base": "^1.1.1" - }, - "engines": { - "node": ">=22" - } - }, "node_modules/@web3-storage/multipart-parser": { "version": "1.0.0", "license": "(Apache-2.0 AND MIT)" @@ -7328,17 +6281,6 @@ "node": ">=10.0.0" } }, - "node_modules/@zxcvbn-ts/core": { - "version": "3.0.4", - "license": "MIT", - "dependencies": { - "fastest-levenshtein": "1.0.16" - } - }, - "node_modules/@zxcvbn-ts/language-common": { - "version": "3.0.4", - "license": "MIT" - }, "node_modules/@zxing/text-encoding": { "version": "0.9.0", "license": "(Unlicense OR Apache-2.0)", @@ -7349,25 +6291,6 @@ "dev": true, "license": "BSD-3-Clause" }, - "node_modules/abitype": { - "version": "1.3.0", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/wevm" - }, - "peerDependencies": { - "typescript": ">=5.0.4", - "zod": "^3.22.0 || ^4.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - }, - "zod": { - "optional": true - } - } - }, "node_modules/abort-controller": { "version": "3.0.0", "license": "MIT", @@ -7410,7 +6333,6 @@ }, "node_modules/acorn-jsx": { "version": "5.3.2", - "dev": true, "license": "MIT", "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" @@ -7449,14 +6371,14 @@ } }, "node_modules/ajv": { - "version": "6.15.0", - "dev": true, - "license": "MIT", + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" }, "funding": { "type": "github", @@ -7478,24 +6400,6 @@ } } }, - "node_modules/ajv-formats/node_modules/ajv": { - "version": "8.20.0", - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/ajv-formats/node_modules/json-schema-traverse": { - "version": "1.0.0", - "license": "MIT" - }, "node_modules/ajv-keywords": { "version": "5.1.0", "license": "MIT", @@ -7506,10 +6410,6 @@ "ajv": "^8.8.2" } }, - "node_modules/alien-signals": { - "version": "2.0.6", - "license": "MIT" - }, "node_modules/anser": { "version": "1.4.10", "license": "MIT" @@ -7934,7 +6834,10 @@ }, "node_modules/babel-plugin-macros": { "version": "3.1.0", + "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "@babel/runtime": "^7.12.5", "cosmiconfig": "^7.0.0", @@ -8178,10 +7081,6 @@ "version": "1.0.2", "license": "MIT" }, - "node_modules/base-64": { - "version": "1.0.0", - "license": "MIT" - }, "node_modules/base64-js": { "version": "1.5.1", "funding": [ @@ -8229,7 +7128,6 @@ }, "node_modules/binary-extensions": { "version": "2.3.0", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -8294,14 +7192,6 @@ "node": ">=8" } }, - "node_modules/browser-tabs-lock": { - "version": "1.3.0", - "hasInstallScript": true, - "license": "MIT", - "dependencies": { - "lodash": ">=4.17.21" - } - }, "node_modules/browserslist": { "version": "4.28.8", "funding": [ @@ -8411,6 +7301,20 @@ "version": "1.1.2", "license": "MIT" }, + "node_modules/bufferutil": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bufferutil/-/bufferutil-4.1.0.tgz", + "integrity": "sha512-ZMANVnAixE6AWWnPzlW2KpUrxhm9woycYvPOo67jWHyFowASTEd9s+QN1EIMsSDtwhIxN4sWE1jotpuDUIgyIw==", + "hasInstallScript": true, + "optional": true, + "peer": true, + "dependencies": { + "node-gyp-build": "^4.3.0" + }, + "engines": { + "node": ">=6.14.2" + } + }, "node_modules/builtins": { "version": "1.0.3", "license": "MIT" @@ -8569,7 +7473,6 @@ }, "node_modules/camelcase-css": { "version": "2.0.1", - "dev": true, "license": "MIT", "engines": { "node": ">= 6" @@ -8600,16 +7503,6 @@ ], "license": "CC-BY-4.0" }, - "node_modules/chalk": { - "version": "5.6.2", - "license": "MIT", - "engines": { - "node": "^12.17.0 || ^14.13 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, "node_modules/char-regex": { "version": "1.0.2", "dev": true, @@ -8627,7 +7520,6 @@ }, "node_modules/chokidar": { "version": "3.6.0", - "dev": true, "license": "MIT", "dependencies": { "anymatch": "~3.1.2", @@ -8652,7 +7544,6 @@ "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, "hasInstallScript": true, "optional": true, "os": [ @@ -8664,7 +7555,6 @@ }, "node_modules/chokidar/node_modules/glob-parent": { "version": "5.1.2", - "dev": true, "license": "ISC", "dependencies": { "is-glob": "^4.0.1" @@ -8772,13 +7662,6 @@ "node": ">=6" } }, - "node_modules/clsx": { - "version": "1.2.1", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/co": { "version": "4.6.0", "dev": true, @@ -8844,13 +7727,6 @@ "version": "1.2.9", "license": "MIT" }, - "node_modules/commander": { - "version": "15.0.0", - "license": "MIT", - "engines": { - "node": ">=22.12.0" - } - }, "node_modules/commondir": { "version": "1.0.1", "license": "MIT" @@ -8952,22 +7828,6 @@ "node": ">=6.6.0" } }, - "node_modules/copy-to-clipboard": { - "version": "3.3.3", - "license": "MIT", - "dependencies": { - "toggle-selection": "^1.0.6" - } - }, - "node_modules/core-js": { - "version": "3.41.0", - "hasInstallScript": true, - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/core-js" - } - }, "node_modules/core-js-compat": { "version": "3.50.0", "license": "MIT", @@ -8988,7 +7848,10 @@ }, "node_modules/cosmiconfig": { "version": "7.1.0", + "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "@types/parse-json": "^4.0.0", "import-fresh": "^3.2.1", @@ -9075,10 +7938,6 @@ "node": "*" } }, - "node_modules/crypto-js": { - "version": "4.2.0", - "license": "MIT" - }, "node_modules/crypto-random-string": { "version": "2.0.0", "license": "MIT", @@ -9146,6 +8005,7 @@ }, "node_modules/csstype": { "version": "3.1.3", + "devOptional": true, "license": "MIT" }, "node_modules/dag-map": { @@ -9277,7 +8137,6 @@ }, "node_modules/deep-is": { "version": "0.1.4", - "dev": true, "license": "MIT" }, "node_modules/deepmerge": { @@ -9390,13 +8249,6 @@ "node": ">= 0.8" } }, - "node_modules/dequal": { - "version": "2.0.3", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/destroy": { "version": "1.2.0", "license": "MIT", @@ -9425,7 +8277,6 @@ }, "node_modules/didyoumean": { "version": "1.2.2", - "dev": true, "license": "Apache-2.0" }, "node_modules/diff-sequences": { @@ -9436,10 +8287,6 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/dijkstrajs": { - "version": "1.0.3", - "license": "MIT" - }, "node_modules/dir-glob": { "version": "3.0.1", "license": "MIT", @@ -9452,12 +8299,10 @@ }, "node_modules/dlv": { "version": "1.1.3", - "dev": true, "license": "MIT" }, "node_modules/doctrine": { "version": "3.0.0", - "dev": true, "license": "Apache-2.0", "dependencies": { "esutils": "^2.0.2" @@ -9836,7 +8681,6 @@ }, "node_modules/eslint": { "version": "8.57.1", - "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", @@ -10188,7 +9032,6 @@ }, "node_modules/eslint-scope": { "version": "7.2.2", - "dev": true, "license": "BSD-2-Clause", "dependencies": { "esrecurse": "^4.3.0", @@ -10203,7 +9046,6 @@ }, "node_modules/eslint-visitor-keys": { "version": "3.4.3", - "dev": true, "license": "Apache-2.0", "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" @@ -10212,9 +9054,23 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/eslint/node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, "node_modules/eslint/node_modules/ansi-styles": { "version": "4.3.0", - "dev": true, "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -10228,7 +9084,6 @@ }, "node_modules/eslint/node_modules/chalk": { "version": "4.1.2", - "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^4.1.0", @@ -10241,9 +9096,13 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/eslint/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" + }, "node_modules/espree": { "version": "9.6.1", - "dev": true, "license": "BSD-2-Clause", "dependencies": { "acorn": "^8.9.0", @@ -10270,7 +9129,6 @@ }, "node_modules/esquery": { "version": "1.7.0", - "dev": true, "license": "BSD-3-Clause", "dependencies": { "estraverse": "^5.1.0" @@ -10281,7 +9139,6 @@ }, "node_modules/esrecurse": { "version": "4.3.0", - "dev": true, "license": "BSD-2-Clause", "dependencies": { "estraverse": "^5.2.0" @@ -10292,7 +9149,6 @@ }, "node_modules/estraverse": { "version": "5.3.0", - "dev": true, "license": "BSD-2-Clause", "engines": { "node": ">=4.0" @@ -10300,7 +9156,6 @@ }, "node_modules/esutils": { "version": "2.0.3", - "dev": true, "license": "BSD-2-Clause", "engines": { "node": ">=0.10.0" @@ -10320,10 +9175,6 @@ "node": ">=6" } }, - "node_modules/eventemitter3": { - "version": "5.0.1", - "license": "MIT" - }, "node_modules/execa": { "version": "1.0.0", "license": "MIT", @@ -10474,6 +9325,17 @@ "invariant": "^2.2.4" } }, + "node_modules/expo-auth-session/node_modules/expo-crypto": { + "version": "13.0.2", + "resolved": "https://registry.npmjs.org/expo-crypto/-/expo-crypto-13.0.2.tgz", + "integrity": "sha512-7f/IMPYJZkBM21LNEMXGrNo/0uXSVfZTwufUdpNKedJR0fm5fH4DCSN79ZddlV26nF90PuXjK2inIbI6lb0qRA==", + "dependencies": { + "base64-js": "^1.3.0" + }, + "peerDependencies": { + "expo": "*" + } + }, "node_modules/expo-constants": { "version": "16.0.2", "license": "MIT", @@ -10486,12 +9348,9 @@ } }, "node_modules/expo-crypto": { - "version": "13.0.2", - "resolved": "https://registry.npmjs.org/expo-crypto/-/expo-crypto-13.0.2.tgz", - "integrity": "sha512-7f/IMPYJZkBM21LNEMXGrNo/0uXSVfZTwufUdpNKedJR0fm5fH4DCSN79ZddlV26nF90PuXjK2inIbI6lb0qRA==", - "dependencies": { - "base64-js": "^1.3.0" - }, + "version": "57.0.1", + "resolved": "https://registry.npmjs.org/expo-crypto/-/expo-crypto-57.0.1.tgz", + "integrity": "sha512-xwegXQw3ATgeL1ZuqbSNrGzOeG+zNeh6Z6DSJk825Qpa3TEQQ1kG3ioE1p3g/SNF373BAVz2iBKUTSytlIbBRA==", "peerDependencies": { "expo": "*" } @@ -10740,12 +9599,10 @@ }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", - "dev": true, "license": "MIT" }, "node_modules/fast-levenshtein": { "version": "2.0.6", - "dev": true, "license": "MIT" }, "node_modules/fast-loops": { @@ -10782,13 +9639,6 @@ "fxparser": "src/cli/cli.js" } }, - "node_modules/fastest-levenshtein": { - "version": "1.0.16", - "license": "MIT", - "engines": { - "node": ">= 4.9.1" - } - }, "node_modules/fastq": { "version": "1.20.1", "license": "ISC", @@ -10849,7 +9699,6 @@ }, "node_modules/file-entry-cache": { "version": "6.0.1", - "dev": true, "license": "MIT", "dependencies": { "flat-cache": "^3.0.4" @@ -10993,10 +9842,6 @@ "semver": "bin/semver" } }, - "node_modules/find-root": { - "version": "1.1.0", - "license": "MIT" - }, "node_modules/find-up": { "version": "5.0.0", "license": "MIT", @@ -11020,7 +9865,6 @@ }, "node_modules/flat-cache": { "version": "3.2.0", - "dev": true, "license": "MIT", "dependencies": { "flatted": "^3.2.9", @@ -11033,7 +9877,6 @@ }, "node_modules/flatted": { "version": "3.4.4", - "dev": true, "license": "ISC" }, "node_modules/flow-enums-runtime": { @@ -11312,7 +10155,6 @@ }, "node_modules/glob-parent": { "version": "6.0.2", - "dev": true, "license": "ISC", "dependencies": { "is-glob": "^4.0.3" @@ -11321,13 +10163,8 @@ "node": ">=10.13.0" } }, - "node_modules/glob-to-regexp": { - "version": "0.4.1", - "license": "BSD-2-Clause" - }, "node_modules/globals": { "version": "13.24.0", - "dev": true, "license": "MIT", "dependencies": { "type-fest": "^0.20.2" @@ -11387,7 +10224,6 @@ }, "node_modules/graphemer": { "version": "1.4.0", - "dev": true, "license": "MIT" }, "node_modules/graphql": { @@ -11625,10 +10461,6 @@ "node": ">=0.10.0" } }, - "node_modules/idb-keyval": { - "version": "6.2.1", - "license": "Apache-2.0" - }, "node_modules/ieee754": { "version": "1.2.1", "funding": [ @@ -11744,14 +10576,6 @@ "fast-loops": "^1.1.3" } }, - "node_modules/input-otp": { - "version": "1.4.2", - "license": "MIT", - "peerDependencies": { - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc" - } - }, "node_modules/internal-ip": { "version": "4.3.0", "license": "MIT", @@ -11861,7 +10685,6 @@ }, "node_modules/is-binary-path": { "version": "2.1.0", - "dev": true, "license": "MIT", "dependencies": { "binary-extensions": "^2.0.0" @@ -12143,14 +10966,6 @@ "node": ">=8" } }, - "node_modules/is-plain-obj": { - "version": "2.1.0", - "license": "MIT", - "optional": true, - "engines": { - "node": ">=8" - } - }, "node_modules/is-plain-object": { "version": "2.0.4", "license": "MIT", @@ -12336,19 +11151,6 @@ "node": ">=0.10.0" } }, - "node_modules/isows": { - "version": "1.0.7", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/wevm" - } - ], - "license": "MIT", - "peerDependencies": { - "ws": "*" - } - }, "node_modules/istanbul-lib-coverage": { "version": "3.2.2", "dev": true, @@ -13714,7 +12516,6 @@ }, "node_modules/jiti": { "version": "1.21.7", - "dev": true, "license": "MIT", "bin": { "jiti": "bin/jiti.js" @@ -13735,13 +12536,6 @@ "version": "1.1.0", "license": "MIT" }, - "node_modules/js-cookie": { - "version": "3.0.7", - "license": "MIT", - "engines": { - "node": ">=20" - } - }, "node_modules/js-tokens": { "version": "4.0.0", "license": "MIT" @@ -13904,7 +12698,6 @@ }, "node_modules/json-buffer": { "version": "3.0.1", - "dev": true, "license": "MIT" }, "node_modules/json-parse-better-errors": { @@ -13913,6 +12706,7 @@ }, "node_modules/json-parse-even-better-errors": { "version": "2.3.1", + "dev": true, "license": "MIT" }, "node_modules/json-schema-deref-sync": { @@ -13942,13 +12736,12 @@ } }, "node_modules/json-schema-traverse": { - "version": "0.4.1", - "dev": true, - "license": "MIT" + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" }, "node_modules/json-stable-stringify-without-jsonify": { "version": "1.0.1", - "dev": true, "license": "MIT" }, "node_modules/json5": { @@ -13984,7 +12777,6 @@ }, "node_modules/keyv": { "version": "4.5.4", - "dev": true, "license": "MIT", "dependencies": { "json-buffer": "3.0.1" @@ -14013,7 +12805,6 @@ }, "node_modules/levn": { "version": "0.4.1", - "dev": true, "license": "MIT", "dependencies": { "prelude-ls": "^1.2.1", @@ -14218,7 +13009,6 @@ }, "node_modules/lilconfig": { "version": "2.1.0", - "dev": true, "license": "MIT", "engines": { "node": ">=10" @@ -14255,7 +13045,6 @@ }, "node_modules/lodash.merge": { "version": "4.6.2", - "dev": true, "license": "MIT" }, "node_modules/lodash.throttle": { @@ -14548,17 +13337,6 @@ "version": "0.2.0", "license": "BSD-2-Clause" }, - "node_modules/merge-options": { - "version": "3.0.4", - "license": "MIT", - "optional": true, - "dependencies": { - "is-plain-obj": "^2.1.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/merge-stream": { "version": "2.0.0", "license": "MIT" @@ -14965,6 +13743,20 @@ "version": "2.0.0", "license": "MIT" }, + "node_modules/metro/node_modules/utf-8-validate": { + "version": "5.0.10", + "resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-5.0.10.tgz", + "integrity": "sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ==", + "hasInstallScript": true, + "optional": true, + "peer": true, + "dependencies": { + "node-gyp-build": "^4.3.0" + }, + "engines": { + "node": ">=6.14.2" + } + }, "node_modules/metro/node_modules/ws": { "version": "7.5.13", "license": "MIT", @@ -15262,7 +14054,6 @@ }, "node_modules/natural-compare": { "version": "1.4.0", - "dev": true, "license": "MIT" }, "node_modules/negotiator": { @@ -15363,6 +14154,18 @@ "node": ">= 6.13.0" } }, + "node_modules/node-gyp-build": { + "version": "4.8.4", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", + "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==", + "optional": true, + "peer": true, + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, "node_modules/node-int64": { "version": "0.4.0", "license": "MIT" @@ -15454,7 +14257,6 @@ }, "node_modules/object-hash": { "version": "3.0.0", - "dev": true, "license": "MIT", "engines": { "node": ">= 6" @@ -15556,10 +14358,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/obuf": { - "version": "1.1.2", - "license": "MIT" - }, "node_modules/on-finished": { "version": "2.3.0", "license": "MIT", @@ -15611,7 +14409,6 @@ }, "node_modules/optionator": { "version": "0.9.4", - "dev": true, "license": "MIT", "dependencies": { "deep-is": "^0.1.3", @@ -15752,58 +14549,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/ox": { - "version": "0.6.9", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/wevm" - } - ], - "license": "MIT", - "dependencies": { - "@adraffy/ens-normalize": "^1.10.1", - "@noble/curves": "^1.6.0", - "@noble/hashes": "^1.5.0", - "@scure/bip32": "^1.5.0", - "@scure/bip39": "^1.4.0", - "abitype": "^1.0.6", - "eventemitter3": "5.0.1" - }, - "peerDependencies": { - "typescript": ">=5.4.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/ox/node_modules/@noble/curves": { - "version": "1.9.7", - "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.7.tgz", - "integrity": "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw==", - "dependencies": { - "@noble/hashes": "1.8.0" - }, - "engines": { - "node": "^14.21.3 || >=16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/ox/node_modules/@noble/hashes": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", - "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", - "engines": { - "node": "^14.21.3 || >=16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, "node_modules/p-finally": { "version": "1.0.0", "license": "MIT", @@ -15873,6 +14618,7 @@ }, "node_modules/parse-json": { "version": "5.2.0", + "dev": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^7.0.0", @@ -15965,38 +14711,85 @@ "node": ">=8" } }, + "node_modules/pg": { + "version": "8.23.0", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.23.0.tgz", + "integrity": "sha512-Ip2EQCngowJLGOfCwkFhPXU7/ljlhn6Rxlmy4XYfL2Y+vyRM59+8uR2xqRWKdYmbXmxCFOAmKxBuSUCdF34qLg==", + "dependencies": { + "pg-connection-string": "^2.14.0", + "pg-pool": "^3.14.0", + "pg-protocol": "^1.16.0", + "pg-types": "2.2.0", + "pgpass": "1.0.5" + }, + "engines": { + "node": ">= 16.0.0" + }, + "optionalDependencies": { + "pg-cloudflare": "^1.4.0" + }, + "peerDependencies": { + "pg-native": ">=3.0.1" + }, + "peerDependenciesMeta": { + "pg-native": { + "optional": true + } + } + }, + "node_modules/pg-cloudflare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.4.0.tgz", + "integrity": "sha512-Vo7z/6rrQYxpNRylp4Tlob2elzbh+N/MOQbxFVWCxS7oEx6jF53GTJFxK2WWpKuBRkmiin4Mt+xofFDjx09R0A==", + "optional": true + }, + "node_modules/pg-connection-string": { + "version": "2.14.0", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.14.0.tgz", + "integrity": "sha512-XwWDGcLRGCXAR8F/AM5bG7Q+A3Wm2s6QeEjlOKZLlH3UYcguiqCWKyWXVag5TLTIjR7oOJUY8kcADaZgWPyLeg==" + }, "node_modules/pg-int8": { "version": "1.0.1", - "license": "ISC", + "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", + "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", "engines": { "node": ">=4.0.0" } }, - "node_modules/pg-numeric": { - "version": "1.0.2", - "license": "ISC", - "engines": { - "node": ">=4" + "node_modules/pg-pool": { + "version": "3.14.0", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.14.0.tgz", + "integrity": "sha512-gKtPkFdQPU3DksooVLi9LsjZxrsBUZIpa+7aVx+LV5pNh0KzP4Zleud2po+ConrxbuXGBJ6Hfer6hdgpIBpBaw==", + "peerDependencies": { + "pg": ">=8.0" } }, "node_modules/pg-protocol": { "version": "1.16.0", - "license": "MIT" + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.16.0.tgz", + "integrity": "sha512-sILXutLVjCLjcDuOmvhX5e2Z4cS5qG/6Bu3VkpFwdf/633ElGLpEh9bgmuI5I4sqKqkifQiGyiCcx1HdtrK7tg==" }, "node_modules/pg-types": { - "version": "4.1.0", - "license": "MIT", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", + "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", "dependencies": { "pg-int8": "1.0.1", - "pg-numeric": "1.0.2", - "postgres-array": "~3.0.1", - "postgres-bytea": "~3.0.0", - "postgres-date": "~2.1.0", - "postgres-interval": "^3.0.0", - "postgres-range": "^1.1.1" + "postgres-array": "~2.0.0", + "postgres-bytea": "~1.0.0", + "postgres-date": "~1.0.4", + "postgres-interval": "^1.1.0" }, "engines": { - "node": ">=10" + "node": ">=4" + } + }, + "node_modules/pgpass": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", + "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", + "dependencies": { + "split2": "^4.1.0" } }, "node_modules/picocolors": { @@ -16201,7 +14994,6 @@ }, "node_modules/postcss-import": { "version": "15.1.0", - "dev": true, "license": "MIT", "dependencies": { "postcss-value-parser": "^4.0.0", @@ -16217,7 +15009,6 @@ }, "node_modules/postcss-js": { "version": "4.1.0", - "dev": true, "funding": [ { "type": "opencollective", @@ -16241,7 +15032,6 @@ }, "node_modules/postcss-load-config": { "version": "4.0.2", - "dev": true, "funding": [ { "type": "opencollective", @@ -16275,7 +15065,6 @@ }, "node_modules/postcss-load-config/node_modules/lilconfig": { "version": "3.1.3", - "dev": true, "license": "MIT", "engines": { "node": ">=14" @@ -16286,7 +15075,6 @@ }, "node_modules/postcss-load-config/node_modules/yaml": { "version": "2.9.0", - "dev": true, "license": "ISC", "bin": { "yaml": "bin.mjs" @@ -16331,51 +15119,42 @@ "license": "MIT" }, "node_modules/postgres-array": { - "version": "3.0.4", - "license": "MIT", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", "engines": { - "node": ">=12" + "node": ">=4" } }, "node_modules/postgres-bytea": { - "version": "3.0.0", - "license": "MIT", - "dependencies": { - "obuf": "~1.1.2" - }, + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.1.tgz", + "integrity": "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==", "engines": { - "node": ">= 6" + "node": ">=0.10.0" } }, "node_modules/postgres-date": { - "version": "2.1.0", - "license": "MIT", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", + "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", "engines": { - "node": ">=12" + "node": ">=0.10.0" } }, "node_modules/postgres-interval": { - "version": "3.0.0", - "license": "MIT", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", + "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", + "dependencies": { + "xtend": "^4.0.0" + }, "engines": { - "node": ">=12" - } - }, - "node_modules/postgres-range": { - "version": "1.1.4", - "license": "MIT" - }, - "node_modules/preact": { - "version": "10.24.2", - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/preact" + "node": ">=0.10.0" } }, "node_modules/prelude-ls": { "version": "1.2.1", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.8.0" @@ -16517,154 +15296,12 @@ ], "license": "MIT" }, - "node_modules/qrcode": { - "version": "1.5.4", - "license": "MIT", - "dependencies": { - "dijkstrajs": "^1.0.1", - "pngjs": "^5.0.0", - "yargs": "^15.3.1" - }, - "bin": { - "qrcode": "bin/qrcode" - }, - "engines": { - "node": ">=10.13.0" - } - }, "node_modules/qrcode-terminal": { "version": "0.11.0", "bin": { "qrcode-terminal": "bin/qrcode-terminal.js" } }, - "node_modules/qrcode.react": { - "version": "4.2.0", - "license": "ISC", - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" - } - }, - "node_modules/qrcode/node_modules/ansi-styles": { - "version": "4.3.0", - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/qrcode/node_modules/cliui": { - "version": "6.0.0", - "license": "ISC", - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", - "wrap-ansi": "^6.2.0" - } - }, - "node_modules/qrcode/node_modules/find-up": { - "version": "4.1.0", - "license": "MIT", - "dependencies": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/qrcode/node_modules/locate-path": { - "version": "5.0.0", - "license": "MIT", - "dependencies": { - "p-locate": "^4.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/qrcode/node_modules/p-limit": { - "version": "2.3.0", - "license": "MIT", - "dependencies": { - "p-try": "^2.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/qrcode/node_modules/p-locate": { - "version": "4.1.0", - "license": "MIT", - "dependencies": { - "p-limit": "^2.2.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/qrcode/node_modules/pngjs": { - "version": "5.0.0", - "license": "MIT", - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/qrcode/node_modules/wrap-ansi": { - "version": "6.2.0", - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/qrcode/node_modules/y18n": { - "version": "4.0.3", - "license": "ISC" - }, - "node_modules/qrcode/node_modules/yargs": { - "version": "15.4.1", - "license": "MIT", - "dependencies": { - "cliui": "^6.0.0", - "decamelize": "^1.2.0", - "find-up": "^4.1.0", - "get-caller-file": "^2.0.1", - "require-directory": "^2.1.1", - "require-main-filename": "^2.0.0", - "set-blocking": "^2.0.0", - "string-width": "^4.2.0", - "which-module": "^2.0.0", - "y18n": "^4.0.0", - "yargs-parser": "^18.1.2" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/qrcode/node_modules/yargs-parser": { - "version": "18.1.3", - "license": "ISC", - "dependencies": { - "camelcase": "^5.0.0", - "decamelize": "^1.2.0" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/query-string": { "version": "7.1.3", "license": "MIT", @@ -16763,6 +15400,20 @@ "ws": "^7" } }, + "node_modules/react-devtools-core/node_modules/utf-8-validate": { + "version": "5.0.10", + "resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-5.0.10.tgz", + "integrity": "sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ==", + "hasInstallScript": true, + "optional": true, + "peer": true, + "dependencies": { + "node-gyp-build": "^4.3.0" + }, + "engines": { + "node": ">=6.14.2" + } + }, "node_modules/react-devtools-core/node_modules/ws": { "version": "7.5.13", "license": "MIT", @@ -16997,16 +15648,6 @@ "prop-types": "^15.5.10" } }, - "node_modules/react-native-url-polyfill": { - "version": "2.0.0", - "license": "MIT", - "dependencies": { - "whatwg-url-without-unicode": "8.0.0-3" - }, - "peerDependencies": { - "react-native": "*" - } - }, "node_modules/react-native-web": { "version": "0.19.13", "license": "MIT", @@ -17182,7 +15823,6 @@ }, "node_modules/read-cache": { "version": "1.0.2", - "dev": true, "license": "MIT" }, "node_modules/readable-stream": { @@ -17208,7 +15848,6 @@ }, "node_modules/readdirp": { "version": "3.6.0", - "dev": true, "license": "MIT", "dependencies": { "picomatch": "^2.2.1" @@ -17219,7 +15858,6 @@ }, "node_modules/readdirp/node_modules/picomatch": { "version": "2.3.2", - "dev": true, "license": "MIT", "engines": { "node": ">=8.6" @@ -17286,10 +15924,6 @@ "node": ">=4" } }, - "node_modules/regenerator-runtime": { - "version": "0.14.1", - "license": "MIT" - }, "node_modules/regexp.prototype.flags": { "version": "1.5.4", "license": "MIT", @@ -17602,24 +16236,6 @@ "url": "https://opencollective.com/webpack" } }, - "node_modules/schema-utils/node_modules/ajv": { - "version": "8.20.0", - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/schema-utils/node_modules/json-schema-traverse": { - "version": "1.0.0", - "license": "MIT" - }, "node_modules/selfsigned": { "version": "2.4.1", "license": "MIT", @@ -18092,6 +16708,14 @@ "node": ">=6" } }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "engines": { + "node": ">= 10.x" + } + }, "node_modules/sprintf-js": { "version": "1.0.3", "license": "BSD-3-Clause" @@ -18191,10 +16815,6 @@ "node": ">= 0.6" } }, - "node_modules/std-env": { - "version": "3.10.0", - "license": "MIT" - }, "node_modules/stop-iteration-iterator": { "version": "1.1.0", "license": "MIT", @@ -18417,7 +17037,6 @@ }, "node_modules/strip-json-comments": { "version": "3.1.1", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -18444,10 +17063,6 @@ "version": "0.1.3", "license": "MIT" }, - "node_modules/stylis": { - "version": "4.2.0", - "license": "MIT" - }, "node_modules/sucrase": { "version": "3.34.0", "license": "MIT", @@ -18528,17 +17143,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/swr": { - "version": "2.3.4", - "license": "MIT", - "dependencies": { - "dequal": "^2.0.3", - "use-sync-external-store": "^1.4.0" - }, - "peerDependencies": { - "react": "^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" - } - }, "node_modules/symbol-tree": { "version": "3.2.4", "dev": true, @@ -18557,13 +17161,8 @@ "url": "https://opencollective.com/synckit" } }, - "node_modules/tabbable": { - "version": "6.5.0", - "license": "MIT" - }, "node_modules/tailwindcss": { "version": "3.3.2", - "dev": true, "license": "MIT", "dependencies": { "@alloc/quick-lru": "^5.2.0", @@ -18602,7 +17201,6 @@ "version": "6.2.0", "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", - "dev": true, "funding": [ { "type": "opencollective", @@ -18860,10 +17458,6 @@ "node": ">=8.0" } }, - "node_modules/toggle-selection": { - "version": "1.0.6", - "license": "MIT" - }, "node_modules/toidentifier": { "version": "1.0.1", "license": "MIT", @@ -18981,7 +17575,6 @@ }, "node_modules/type-check": { "version": "0.4.0", - "dev": true, "license": "MIT", "dependencies": { "prelude-ls": "^1.2.1" @@ -18999,7 +17592,6 @@ }, "node_modules/type-fest": { "version": "0.20.2", - "dev": true, "license": "(MIT OR CC0-1.0)", "engines": { "node": ">=10" @@ -19098,7 +17690,7 @@ "version": "5.3.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz", "integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==", - "dev": true, + "devOptional": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -19154,10 +17746,6 @@ "node": ">=18.17" } }, - "node_modules/undici-types": { - "version": "8.10.0", - "license": "MIT" - }, "node_modules/unicode-canonical-property-names-ecmascript": { "version": "2.0.1", "license": "MIT", @@ -19537,8 +18125,8 @@ }, "node_modules/uri-js": { "version": "4.4.1", - "dev": true, - "license": "BSD-2-Clause", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", "dependencies": { "punycode": "^2.1.0" } @@ -19570,6 +18158,20 @@ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, + "node_modules/utf-8-validate": { + "version": "6.0.6", + "resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-6.0.6.tgz", + "integrity": "sha512-q3l3P9UtEEiAHcsgsqTgf9PPjctrDWoIXW3NpOHFdRDbLvu4DLIcxHangJ4RLrWkBcKjmcs/6NkerI8T/rE4LA==", + "hasInstallScript": true, + "optional": true, + "peer": true, + "dependencies": { + "node-gyp-build": "^4.3.0" + }, + "engines": { + "node": ">=6.14.2" + } + }, "node_modules/util": { "version": "0.12.5", "license": "MIT", @@ -19629,127 +18231,6 @@ "node": ">= 0.8" } }, - "node_modules/viem": { - "version": "2.55.19", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/wevm" - } - ], - "license": "MIT", - "dependencies": { - "@noble/curves": "1.9.1", - "@noble/hashes": "1.8.0", - "@scure/bip32": "1.7.0", - "@scure/bip39": "1.6.0", - "abitype": "1.2.3", - "isows": "1.0.7", - "ox": "0.14.34", - "ws": "8.21.0" - }, - "peerDependencies": { - "typescript": ">=5.0.4" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/viem/node_modules/@noble/curves": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.1.tgz", - "integrity": "sha512-k11yZxZg+t+gWvBbIswW0yoJlu8cHOC7dhunwOzoWH/mXGBiYyR4YY6hAEK/3EUs4UpB8la1RfdRpeGsFHkWsA==", - "dependencies": { - "@noble/hashes": "1.8.0" - }, - "engines": { - "node": "^14.21.3 || >=16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/viem/node_modules/@noble/hashes": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", - "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", - "engines": { - "node": "^14.21.3 || >=16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/viem/node_modules/abitype": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/abitype/-/abitype-1.2.3.tgz", - "integrity": "sha512-Ofer5QUnuUdTFsBRwARMoWKOH1ND5ehwYhJ3OJ/BQO+StkwQjHw0XyVh4vDttzHB7QOFhPHa/o413PJ82gU/Tg==", - "funding": { - "url": "https://github.com/sponsors/wevm" - }, - "peerDependencies": { - "typescript": ">=5.0.4", - "zod": "^3.22.0 || ^4.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - }, - "zod": { - "optional": true - } - } - }, - "node_modules/viem/node_modules/ox": { - "version": "0.14.34", - "resolved": "https://registry.npmjs.org/ox/-/ox-0.14.34.tgz", - "integrity": "sha512-12seOIk7dv8eAoGQhcWaeKZxNz304IVcDvb9U5Y7JZAEVe21Nm1YMxLjhWah+su5BD4Omx4Zz0z5x3ij9M4GYQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/wevm" - } - ], - "dependencies": { - "@adraffy/ens-normalize": "^1.11.0", - "@noble/ciphers": "^1.3.0", - "@noble/curves": "1.9.1", - "@noble/hashes": "^1.8.0", - "@scure/bip32": "^1.7.0", - "@scure/bip39": "^1.6.0", - "abitype": "^1.2.3", - "eventemitter3": "5.0.1" - }, - "peerDependencies": { - "typescript": ">=5.4.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/viem/node_modules/ws": { - "version": "8.21.0", - "license": "MIT", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, "node_modules/vlq": { "version": "1.0.1", "license": "MIT" @@ -19963,7 +18444,6 @@ }, "node_modules/word-wrap": { "version": "1.2.5", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -20134,7 +18614,10 @@ }, "node_modules/yaml": { "version": "1.10.3", + "dev": true, "license": "ISC", + "optional": true, + "peer": true, "engines": { "node": ">= 6" } diff --git a/package.json b/package.json index 4f9d8f0..95cd740 100644 --- a/package.json +++ b/package.json @@ -28,10 +28,8 @@ "uber-clone", "uber", "expo-router", - "clerk", "postgresql", "alert", - "neon-postgres", "zustand", "mysql", "google-maps", @@ -72,17 +70,16 @@ } ], "dependencies": { - "@clerk/clerk-expo": "^2.2.5", "@expo/metro-runtime": "~3.2.3", "@expo/vector-icons": "^14.0.2", "@gorhom/bottom-sheet": "^4.6.4", - "@neondatabase/serverless": "^0.9.4", "@react-navigation/native": "^6.0.2", "eslint-config-prettier": "^9.1.0", "eslint-plugin-prettier": "^5.2.1", "expo": "~51.0.28", "expo-auth-session": "~5.5.2", "expo-constants": "~16.0.2", + "expo-crypto": "^57.0.1", "expo-font": "~12.0.9", "expo-linking": "^6.3.1", "expo-location": "^17.0.1", @@ -93,6 +90,7 @@ "expo-system-ui": "~3.0.7", "expo-web-browser": "~13.0.3", "nativewind": "^2.0.11", + "pg": "^8.23.0", "prettier": "^3.3.3", "react": "18.2.0", "react-dom": "18.2.0", @@ -111,6 +109,7 @@ "devDependencies": { "@babel/core": "^7.20.0", "@types/jest": "^29.5.12", + "@types/pg": "^8.23.1", "@types/react": "~18.2.45", "@types/react-test-renderer": "^18.0.7", "eslint": "^8.57.0", diff --git a/scripts/seed-db.mjs b/scripts/seed-db.mjs index 6e5e014..a457838 100644 --- a/scripts/seed-db.mjs +++ b/scripts/seed-db.mjs @@ -1,7 +1,7 @@ -// Creates and seeds the Waseel database tables on Neon. +// Creates and seeds the Waseel database tables (local/self-hosted PostgreSQL). // Usage: node scripts/seed-db.mjs (reads DATABASE_URL from .env) -import { neon } from "@neondatabase/serverless"; +import pg from "pg"; import { readFileSync } from "fs"; const env = readFileSync(new URL("../.env", import.meta.url), "utf8"); @@ -19,19 +19,78 @@ if (!databaseUrl) { process.exit(1); } -const sql = neon(databaseUrl); +const pool = new pg.Pool({ connectionString: databaseUrl }); + +const sql = async (strings, ...values) => { + const text = strings.reduce( + (acc, chunk, i) => acc + chunk + (i < values.length ? `$${i + 1}` : ""), + "", + ); + const result = await pool.query(text, values); + return result.rows; +}; + +// Migrate databases created during the Clerk era (clerk_id column, no UNIQUE email). +const clerkCol = await sql` + SELECT 1 FROM information_schema.columns + WHERE table_name = 'users' AND column_name = 'clerk_id' +`; +if (clerkCol.length > 0) { + await sql`ALTER TABLE users DROP COLUMN clerk_id`; + console.log("Dropped legacy clerk_id column from users."); +} + +// Rebuild legacy tables where users.id / rides.user_id are not UUID. +const idType = await sql` + SELECT data_type FROM information_schema.columns + WHERE table_name = 'users' AND column_name = 'id' +`; +if (idType.length > 0 && idType[0]?.data_type !== "uuid") { + console.log("Detected legacy non-UUID users schema, rebuilding users/rides..."); + await sql`DROP TABLE IF EXISTS rides`; + await sql`DROP TABLE IF EXISTS users CASCADE`; + console.log("Legacy users/rides tables dropped (test data only)."); +} await sql`CREATE TABLE IF NOT EXISTS users ( - id SERIAL PRIMARY KEY, + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), name VARCHAR(255) NOT NULL, - email VARCHAR(255) NOT NULL, - clerk_id VARCHAR(255) NOT NULL, + email VARCHAR(255) NOT NULL UNIQUE, + phone VARCHAR(20), + password_hash TEXT, + google_sub TEXT UNIQUE, + email_verified BOOLEAN NOT NULL DEFAULT FALSE, role VARCHAR(20), created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP )`; -// For databases created before roles existed. +// For databases created before self-hosted auth existed. await sql`ALTER TABLE users ADD COLUMN IF NOT EXISTS role VARCHAR(20)`; +await sql`ALTER TABLE users ADD COLUMN IF NOT EXISTS phone VARCHAR(20)`; +await sql`ALTER TABLE users ADD COLUMN IF NOT EXISTS password_hash TEXT`; +await sql`ALTER TABLE users ADD COLUMN IF NOT EXISTS google_sub TEXT`; +await sql`ALTER TABLE users ADD COLUMN IF NOT EXISTS email_verified BOOLEAN NOT NULL DEFAULT FALSE`; + +// email must be UNIQUE for ON CONFLICT (email) upserts in register+api.ts. +const emailUnique = await sql` + SELECT 1 FROM pg_constraint + WHERE conrelid = 'users'::regclass AND contype = 'u' + AND conkey @> ARRAY[ + (SELECT attnum::smallint FROM pg_attribute + WHERE attrelid = 'users'::regclass AND attname = 'email') + ] +`; +if (emailUnique.length === 0) { + await sql`ALTER TABLE users ADD CONSTRAINT users_email_unique UNIQUE (email)`; + console.log("Added unique constraint on users.email."); +} + +await sql`CREATE TABLE IF NOT EXISTS email_verification_codes ( + email VARCHAR(255) PRIMARY KEY, + code_hash TEXT NOT NULL, + attempts INTEGER NOT NULL DEFAULT 0, + expires_at TIMESTAMP NOT NULL +)`; await sql`CREATE TABLE IF NOT EXISTS drivers ( id SERIAL PRIMARY KEY, @@ -55,7 +114,7 @@ await sql`CREATE TABLE IF NOT EXISTS rides ( fare_price INTEGER NOT NULL, payment_status VARCHAR(50) NOT NULL, driver_id INTEGER NOT NULL REFERENCES drivers(id), - user_id VARCHAR(255) NOT NULL, + user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP )`; @@ -74,3 +133,5 @@ if (count[0].n === 0) { } console.log("Database ready."); + +await pool.end(); diff --git a/scripts/set-owner.mjs b/scripts/set-owner.mjs new file mode 100644 index 0000000..cb4b080 --- /dev/null +++ b/scripts/set-owner.mjs @@ -0,0 +1,44 @@ +// Promotes (or demotes) a user's role. Usage: +// node scripts/set-owner.mjs owner@example.com owner +// node scripts/set-owner.mjs owner@example.com rider + +import pg from "pg"; +import { readFileSync } from "fs"; + +const [email, role = "owner"] = process.argv.slice(2); + +if (!email) { + console.error("Usage: node scripts/set-owner.mjs [role]"); + console.error("Roles: owner | driver | rider"); + process.exit(1); +} + +const env = readFileSync(new URL("../.env", import.meta.url), "utf8"); +const databaseUrl = env + .split("\n") + .find((l) => l.startsWith("DATABASE_URL=")) + ?.split("=") + .slice(1) + .join("=") + .trim() + .replace(/^"|"$/g, ""); + +const pool = new pg.Pool({ connectionString: databaseUrl }); + +try { + const { rows } = await pool.query( + `UPDATE users SET email_verified = TRUE, role = $2 WHERE email = $1 RETURNING id, email, role`, + [email.toLowerCase(), role], + ); + + if (!rows[0]) { + console.error(`No user found with email ${email}`); + process.exit(1); + } + + console.log( + `Updated ${rows[0].email}: role=${rows[0].role}, email_verified=true`, + ); +} finally { + await pool.end(); +} diff --git a/tsconfig.json b/tsconfig.json index 909e901..6e916b8 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -8,6 +8,7 @@ ] } }, + "exclude": ["dashboard"], "include": [ "**/*.ts", "**/*.tsx",