diff --git a/web/src/components/BetaGate.tsx b/web/src/components/BetaGate.tsx new file mode 100644 index 0000000..e8b497b --- /dev/null +++ b/web/src/components/BetaGate.tsx @@ -0,0 +1,37 @@ +import { type ReactNode, useEffect, useState } from "react"; +import { Navigate, useLocation } from "react-router-dom"; + +const STORAGE_KEY = "skymoney_beta_access"; + +type Props = { + children: ReactNode; +}; + +export function BetaGate({ children }: Props) { + const location = useLocation(); + const [hasAccess, setHasAccess] = useState(null); + + useEffect(() => { + setHasAccess(localStorage.getItem(STORAGE_KEY) === "true"); + }, []); + + if (location.pathname === "/beta") { + return <>{children}; + } + + if (hasAccess === null) { + return ( +
+ Checking access… +
+ ); + } + + if (!hasAccess) { + return ; + } + + return <>{children}; +} + +export const betaAccessStorageKey = STORAGE_KEY; diff --git a/web/src/main.tsx b/web/src/main.tsx index 6c7ff4a..2ee19e4 100644 --- a/web/src/main.tsx +++ b/web/src/main.tsx @@ -10,6 +10,7 @@ import { import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { ToastProvider } from "./components/Toast"; import { RequireAuth } from "./components/RequireAuth"; +import { BetaGate } from "./components/BetaGate"; import App from "./App"; import "./styles.css"; @@ -52,13 +53,23 @@ const HealthPage = lazy(() => import("./pages/HealthPage")); const OnboardingPage = lazy(() => import("./pages/OnboardingPage")); const LoginPage = lazy(() => import("./pages/LoginPage")); const RegisterPage = lazy(() => import("./pages/RegisterPage")); +const BetaAccessPage = lazy(() => import("./pages/BetaAccessPage")); const router = createBrowserRouter( createRoutesFromElements( - }> - {/* Public */} - } /> - } /> + <> + } /> + + + + + } + > + {/* Public */} + } /> + } /> {/* Protected onboarding */} - {/* Fallback */} - } /> - + {/* Fallback */} + } /> + + ) ); diff --git a/web/src/pages/BetaAccessPage.tsx b/web/src/pages/BetaAccessPage.tsx new file mode 100644 index 0000000..3d31a76 --- /dev/null +++ b/web/src/pages/BetaAccessPage.tsx @@ -0,0 +1,96 @@ +import { useMemo, useState } from "react"; +import { useNavigate } from "react-router-dom"; +import { betaAccessStorageKey } from "../components/BetaGate"; + +const ACCESS_CODE = "jodygavemeaccess123"; + +export default function BetaAccessPage() { + const navigate = useNavigate(); + const [code, setCode] = useState(""); + const [touched, setTouched] = useState(false); + + const isValid = useMemo(() => code.trim() === ACCESS_CODE, [code]); + const isUnlocked = useMemo( + () => localStorage.getItem(betaAccessStorageKey) === "true", + [] + ); + + const handleSubmit = (event: React.FormEvent) => { + event.preventDefault(); + setTouched(true); + if (!isValid) return; + localStorage.setItem(betaAccessStorageKey, "true"); + navigate("/login", { replace: true }); + }; + + return ( +
+
+
+
+ Private beta +
+

+ Welcome to SkyMoney +

+

+ This build is private. If you’ve been given access, enter your code + below. If not, reach out to{" "} + + Jody + {" "} + for access. +

+
+ +
+ + + {touched && code.length > 0 && !isValid && ( +
+ That code doesn’t match. Please try again. +
+ )} + + + + {isUnlocked && ( + + )} +
+
+
+ ); +}