Files
Stirling-PDF/frontend/editor/src/saas/routes/Landing.tsx
T
ConnorYohandGitHub 5fa5e12c64 fix(saas): show team invitation banner in SaaS web build (#6612)
## 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.
2026-06-11 18:01:58 +01:00

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 }} />;
}