mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2026-09-02 21:03:34 +03:00
162 lines
6.3 KiB
TypeScript
162 lines
6.3 KiB
TypeScript
import { Suspense, lazy, type ReactNode } from "react";
|
|
import { Routes, Route, useLocation } from "react-router-dom";
|
|
import { isAuthRoute } from "@app/utils/pathUtils";
|
|
import { AppProviders } from "@app/components/AppProviders";
|
|
import { PreferencesProvider } from "@app/contexts/PreferencesContext";
|
|
import { ThemeProvider } from "@app/components/shared/ThemeProvider";
|
|
import { setBaseUrl } from "@app/constants/app";
|
|
import type { AppConfig } from "@app/contexts/AppConfigContext";
|
|
import { AppLayout } from "@app/components/AppLayout";
|
|
import { LoadingFallback } from "@app/components/shared/LoadingFallback";
|
|
import OnboardingTour from "@app/components/onboarding/OnboardingTour";
|
|
import Landing from "@app/routes/Landing";
|
|
import Login from "@app/routes/Login";
|
|
import { ResumePendingConnect } from "@app/routes/ResumePendingConnect";
|
|
import Signup from "@app/routes/Signup";
|
|
import AuthCallback from "@app/routes/AuthCallback";
|
|
import ResetPassword from "@app/routes/ResetPassword";
|
|
import OAuthConsent from "@app/routes/OAuthConsent";
|
|
import ConnectApprove from "@app/routes/ConnectApprove";
|
|
import ShareLinkPage from "@app/routes/ShareLinkPage";
|
|
import { getAdminRouteExtensions } from "@app/routes/adminRouteExtensions";
|
|
import { AppFrame } from "@app/components/layout/AppFrame";
|
|
import { NoAppChrome } from "@app/components/layout/NoAppChrome";
|
|
import OnboardingBootstrap from "@app/components/OnboardingBootstrap";
|
|
import SignupRequiredBootstrap from "@app/components/SignupRequiredBootstrap";
|
|
import UsageLimitModalHost from "@app/components/UsageLimitModalHost";
|
|
import { RootGate } from "@app/routes/RootGate";
|
|
|
|
const MobileScannerPage = lazy(() => import("@app/pages/MobileScannerPage"));
|
|
const MobileSignPage = lazy(() => import("@app/pages/MobileSignPage"));
|
|
|
|
// Import global styles
|
|
import "@app/styles/tailwind.css";
|
|
import "@app/auth/ui/auth-theme.css";
|
|
import "@app/styles/cookieconsent.css";
|
|
import "@app/styles/index.css";
|
|
|
|
// Import file ID debugging helpers (development only)
|
|
import "@app/utils/fileIdSafety";
|
|
|
|
function handleConfigLoaded(config: AppConfig) {
|
|
if (config.baseUrl) setBaseUrl(config.baseUrl);
|
|
}
|
|
|
|
// Minimal providers for the public, no-auth mobile-scanner page. Just theme +
|
|
// preferences, no AppProviders, so no auth and no backend bootstrap - it
|
|
// renders without a logged-in session.
|
|
function PublicRouteProviders({ children }: { children: ReactNode }) {
|
|
return (
|
|
<PreferencesProvider>
|
|
<ThemeProvider>{children}</ThemeProvider>
|
|
</PreferencesProvider>
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Onboarding / sign-up modals must never cover auth-flow pages (login, signup,
|
|
* OAuth consent): they steal focus from the task the user was sent there to
|
|
* complete.
|
|
*/
|
|
function NonAuthBootstraps() {
|
|
const location = useLocation();
|
|
if (isAuthRoute(location.pathname)) {
|
|
return null;
|
|
}
|
|
return (
|
|
<>
|
|
<OnboardingBootstrap />
|
|
<SignupRequiredBootstrap />
|
|
<UsageLimitModalHost />
|
|
</>
|
|
);
|
|
}
|
|
|
|
export default function App() {
|
|
return (
|
|
<Suspense fallback={<LoadingFallback />}>
|
|
<Routes>
|
|
{/* Mobile scanner - public, no auth. Opened on a phone via the QR code,
|
|
so it must render without a logged-in session. Kept outside
|
|
AppProviders or it falls through to the auth-gated catch-all. */}
|
|
<Route
|
|
path="/mobile-scanner"
|
|
element={
|
|
<PublicRouteProviders>
|
|
<MobileScannerPage />
|
|
</PublicRouteProviders>
|
|
}
|
|
/>
|
|
|
|
{/* Mobile signature drawing - reached from the Sign tool QR code */}
|
|
<Route
|
|
path="/mobile-sign"
|
|
element={
|
|
<PublicRouteProviders>
|
|
<MobileSignPage />
|
|
</PublicRouteProviders>
|
|
}
|
|
/>
|
|
|
|
{/* Both apps, under a shared frame so the rail renders once outside them. */}
|
|
<Route element={<AppFrame />}>
|
|
{/* The portal: its own top-level shell, before the catch-all. */}
|
|
{getAdminRouteExtensions()}
|
|
|
|
{/* Everything else needs the auth/backend providers. RootGate routes "/"
|
|
by role before any of it mounts. */}
|
|
<Route
|
|
path="*"
|
|
element={
|
|
<RootGate>
|
|
<AppProviders
|
|
appConfigProviderProps={{
|
|
onConfigLoaded: handleConfigLoaded,
|
|
}}
|
|
>
|
|
<AppLayout>
|
|
<NonAuthBootstraps />
|
|
<ResumePendingConnect />
|
|
<Routes>
|
|
{/* Not the app: no rail over any of these, ever. */}
|
|
<Route element={<NoAppChrome />}>
|
|
<Route path="/login" element={<Login />} />
|
|
<Route path="/signup" element={<Signup />} />
|
|
<Route
|
|
path="/auth/callback"
|
|
element={<AuthCallback />}
|
|
/>
|
|
<Route path="/auth/reset" element={<ResetPassword />} />
|
|
<Route
|
|
path="/oauth/consent"
|
|
element={<OAuthConsent />}
|
|
/>
|
|
{/* Human half of the self-hosted account-link handshake.
|
|
It lives on this origin because a customer hostname can
|
|
never be in the provider's redirect allow-list. Grouped
|
|
with the pages above: it is an approval step, not the
|
|
app. */}
|
|
<Route path="/link" element={<ConnectApprove />} />
|
|
{/* Shared-file links. Team invites are NOT routed here:
|
|
on SaaS they are accepted in-app via the Supabase team
|
|
invitation banner, not the Spring password-based
|
|
/invite/:token page used by the self-hosted build. */}
|
|
<Route
|
|
path="/share/:token"
|
|
element={<ShareLinkPage />}
|
|
/>
|
|
</Route>
|
|
<Route path="/*" element={<Landing />} />
|
|
</Routes>
|
|
<OnboardingTour />
|
|
</AppLayout>
|
|
</AppProviders>
|
|
</RootGate>
|
|
}
|
|
/>
|
|
</Route>
|
|
</Routes>
|
|
</Suspense>
|
|
);
|
|
}
|