mirror of
https://github.com/arsvendg/Stirling-PDF.git
synced 2026-07-16 11:23:10 +02:00
## Problem When a user is invited to a team, the SaaS web app shows **no invitation banner** — even though the pending invite is returned by `/api/v1/team/invitations/pending` on refresh. ## Root causes 1. **Never rendered in SaaS.** `TeamInvitationBanner` only existed in `desktop/`, wired solely into `DesktopBannerInitializer`. The SaaS banner stack rendered only `<UpgradeBanner />`. 2. **Single banner slot.** `BannerContext` holds one node; `setBanner` replaces it. `TrialStatusBanner` called `setBanner(null)` when there was no active trial (and re-fired once `trialStatus` resolved async), wiping any other banner. 3. **Shadowing was too fragile.** A first attempt shadowed the proprietary `UpgradeBannerInitializer` from the saas layer, but `vite-tsconfig-paths` resolves the `@app` specifier once at dev-server start — a newly-added shadow of an already-resolved module isn't picked up on a browser refresh, only a full restart. So the proprietary initializer kept running and no invite banner appeared (while the SaaS team context still fetched + populated the invite, which is why the pending call was visible). ## Fix - Add `saas/components/shared/TeamInvitationBanner.tsx` — ported from desktop, minus the desktop `connectionMode` gate and explicit billing refresh (SaaS `acceptInvitation` already refreshes credits + session). - Render it **inline in `saas/routes/Landing.tsx`** next to `GuestUserBanner` — a new import specifier in an existing file (HMR-friendly), unambiguously inside `SaaSTeamProvider`, mirroring the proven `GuestUserBanner` pattern. No dependency on the single banner slot. - **Remove `TrialStatusBanner`** (trials are being retired) so it can't clobber banners. Also drops the stale mention from the stripe-lazy-load test comment. ## Verification - `tsc --noEmit -p tsconfig.saas.vite.json`: clean in touched files; total unchanged from baseline (37 pre-existing, unrelated). - Manual: pull + verify the Accept/Decline banner appears for an account with a pending invite.
97 lines
3.1 KiB
TypeScript
97 lines
3.1 KiB
TypeScript
import React, { useMemo } from "react";
|
|
import { Navigate, useLocation } from "react-router-dom";
|
|
import { useAuth } from "@app/auth/UseSession";
|
|
import { useAutoAnonymousAuth } from "@app/hooks/useAutoAnonymousAuth";
|
|
import { isToolRoute } from "@app/utils/pathUtils";
|
|
import HomePage from "@app/pages/HomePage";
|
|
import Login from "@app/routes/Login";
|
|
import GuestUserBanner from "@app/components/auth/GuestUserBanner";
|
|
import { TeamInvitationBanner } from "@app/components/shared/TeamInvitationBanner";
|
|
|
|
export default function Landing() {
|
|
const { session, loading } = useAuth();
|
|
const { isAutoAuthenticating, autoAuthError, shouldTriggerAutoAuth } =
|
|
useAutoAnonymousAuth();
|
|
const location = useLocation();
|
|
|
|
// Check if current path is a tool (prevents premature navigation on first render)
|
|
const isCurrentPathTool = useMemo(
|
|
() => isToolRoute(location.pathname),
|
|
[location.pathname],
|
|
);
|
|
|
|
// Match the same guarded bypass used in RequireAuth
|
|
const isLocalhost =
|
|
typeof window !== "undefined" &&
|
|
/^(localhost|127\.0\.0\.1)$/i.test(window.location.hostname);
|
|
const devBypassEnabled = Boolean(
|
|
import.meta.env.DEV &&
|
|
isLocalhost &&
|
|
import.meta.env.VITE_DEV_BYPASS_AUTH === "true",
|
|
);
|
|
|
|
console.log("[Landing] State:", {
|
|
pathname: location.pathname,
|
|
loading,
|
|
hasSession: !!session,
|
|
isAutoAuthenticating,
|
|
shouldTriggerAutoAuth,
|
|
isCurrentPathTool,
|
|
autoAuthError,
|
|
});
|
|
|
|
// Show loading while checking auth, while auto-authenticating, OR while preparing to auto-authenticate
|
|
// CRITICAL: Also wait if shouldTriggerAutoAuth is true OR if we're on a tool route (prevents navigation before hook evaluates)
|
|
if (
|
|
loading ||
|
|
isAutoAuthenticating ||
|
|
(!session && (shouldTriggerAutoAuth || isCurrentPathTool) && !autoAuthError)
|
|
) {
|
|
return (
|
|
<div
|
|
style={{
|
|
minHeight: "100vh",
|
|
display: "flex",
|
|
alignItems: "center",
|
|
justifyContent: "center",
|
|
}}
|
|
>
|
|
<div className="text-center">
|
|
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600 mx-auto mb-3"></div>
|
|
<div className="text-gray-600">
|
|
{isAutoAuthenticating ? "Setting up your session..." : "Loading..."}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// If we have a session or dev bypass is enabled, show the main app
|
|
if (session || devBypassEnabled) {
|
|
return (
|
|
<>
|
|
<GuestUserBanner />
|
|
<TeamInvitationBanner />
|
|
<HomePage />
|
|
</>
|
|
);
|
|
}
|
|
|
|
// If auto-authentication failed, navigate to login with error state
|
|
if (autoAuthError && shouldTriggerAutoAuth) {
|
|
return (
|
|
<Navigate to="/login" replace state={{ autoAuthError, from: location }} />
|
|
);
|
|
}
|
|
|
|
// If we're at home route ("/"), show login directly (marketing/landing page)
|
|
// Otherwise navigate to login (fixes URL mismatch for tool routes)
|
|
const isHome = location.pathname === "/" || location.pathname === "";
|
|
if (isHome) {
|
|
return <Login />;
|
|
}
|
|
|
|
// For non-home routes without auth, navigate to login (preserves from location)
|
|
return <Navigate to="/login" replace state={{ from: location }} />;
|
|
}
|