Files
prowler/ui/components/onboarding/onboarding-gate.tsx
T

95 lines
3.0 KiB
TypeScript

"use client";
import { usePathname, useRouter } from "next/navigation";
import { useAuth } from "@/hooks/use-auth";
import { useMountEffect } from "@/hooks/use-mount-effect";
import {
getOrderedFlows,
type OnboardingFlow,
shouldStartOnboarding,
} from "@/lib/onboarding";
import {
isFirstRunHandled,
markFirstRunHandled,
} from "@/lib/onboarding/first-run-marker";
import { WIZARD_OPEN_SOURCE } from "@/lib/provider-funnel/provider-funnel-events";
import { buildAddProviderHref } from "@/lib/providers-navigation";
import { isCloud } from "@/lib/shared/env";
import { localStorageAdapter } from "@/lib/tours/store/local-storage-adapter";
import { useTourCompletion } from "@/lib/tours/use-tour-completion";
import { useOnboardingCheckpointStore } from "@/store/onboarding-checkpoint";
interface OnboardingGateProps {
// `undefined` = fetch failed/ambiguous; fail-open (never force the first run).
hasProviders?: boolean;
// Scopes the first-run marker so one tenant's first run never silences another's.
tenantId?: string | null;
}
// New-tenant gate. Mounted once in the layout: an empty tenant is sent straight to
// the add-provider wizard, once per tenant and browser. Renders nothing.
export function OnboardingGate({
hasProviders,
tenantId = null,
}: OnboardingGateProps) {
const pathname = usePathname();
const { permissions } = useAuth();
// Billing must stay usable before onboarding; leaving it keeps the gate eligible.
const isBillingRoute =
pathname === "/billing" || pathname?.startsWith("/billing/");
// Gate forces only the first flow (`add-provider`); remaining flows come via checkpoint/replay.
const flow = getOrderedFlows()[0] ?? null;
// Returns null on server/first render; the redirect re-reads storage before acting.
const completionRecord = useTourCompletion(flow?.tour ?? null);
const shouldRedirect =
flow !== null &&
!isBillingRoute &&
shouldStartOnboarding({
hasProviders,
canManageProviders: permissions.manage_providers === true,
completionRecord,
});
if (!shouldRedirect) return null;
return <FirstRunRedirect flow={flow} tenantId={tenantId} />;
}
interface FirstRunRedirectProps {
flow: OnboardingFlow;
tenantId: string | null;
}
function FirstRunRedirect({ flow, tenantId }: FirstRunRedirectProps) {
const router = useRouter();
useMountEffect(() => {
// Hydration renders with an empty completion snapshot, so decide from storage here.
const tourId = { id: flow.tour.id, version: flow.tour.version };
if (
isFirstRunHandled(tenantId) ||
localStorageAdapter.get(tourId) !== null
) {
return;
}
markFirstRunHandled(tenantId);
const addProviderHref = buildAddProviderHref(WIZARD_OPEN_SOURCE.FIRST_RUN);
if (!isCloud()) {
router.replace(addProviderHref);
return;
}
// Tours and the post-connect checkpoint are Cloud-only.
useOnboardingCheckpointStore.getState().arm();
router.replace(`${addProviderHref}&onboarding=${flow.id}`);
});
return null;
}