diff --git a/apps/compliance-portal/src/_locales/en-US.json b/apps/compliance-portal/src/_locales/en-US.json
index 9eac2b74e..45d0c49e8 100644
--- a/apps/compliance-portal/src/_locales/en-US.json
+++ b/apps/compliance-portal/src/_locales/en-US.json
@@ -28,29 +28,9 @@
"zoomIn": "Zoom in"
},
"auth": {
- "backToPortal": "Back to portal",
- "signIn": {
- "title": "Sign in to continue",
- "description": "Access protected resources or submit a request",
- "or": "OR",
- "withProvider": "Sign in with {{provider}}",
- "emailLabel": "Email Address",
- "emailPlaceholder": "you@company.com",
- "emailRequired": "Email is required",
- "emailInvalid": "Enter a valid email address",
- "sendMagicLink": "Send Magic Link",
- "resend": "Resend link",
- "resendIn": "Resend link in {{seconds}}s",
- "magicLinkSent": "Magic link sent!",
- "magicLinkSentNote": "Check your email and use the link to sign in."
- },
"requestAccess": {
"success": "Access requested"
},
- "verify": {
- "title": "Confirming your email",
- "description": "Please wait while we sign you in\u2026"
- },
"fullName": {
"title": "Add your name",
"label": "Full Name",
@@ -59,17 +39,7 @@
"tooShort": "Full name is too short",
"submit": "Continue"
},
- "magicLinkExpired": {
- "title": "Link expired",
- "description": "This magic link has expired. Please request a new one."
- },
- "magicLinkAlreadyUsed": {
- "title": "Link already used",
- "description": "This magic link has already been used. Please request a new one."
- },
"errors": {
- "magicLinkFailed": "Couldn't send the magic link. Please try again.",
- "verifyFailed": "Couldn't verify the link. Please try again.",
"fullNameFailed": "Couldn't save your name. Please try again.",
"requestFailed": "Couldn't complete your access request. Please try again."
}
diff --git a/apps/compliance-portal/src/_locales/fr-FR.json b/apps/compliance-portal/src/_locales/fr-FR.json
index 65f8a4f26..a97146490 100644
--- a/apps/compliance-portal/src/_locales/fr-FR.json
+++ b/apps/compliance-portal/src/_locales/fr-FR.json
@@ -28,29 +28,9 @@
"zoomIn": "Zoom avant"
},
"auth": {
- "backToPortal": "Retour au portail",
- "signIn": {
- "title": "Connectez-vous pour continuer",
- "description": "Accédez aux ressources protégées ou soumettez une demande",
- "or": "OU",
- "withProvider": "Se connecter avec {{provider}}",
- "emailLabel": "Adresse e-mail",
- "emailPlaceholder": "vous@entreprise.com",
- "emailRequired": "L'e-mail est requis",
- "emailInvalid": "Saisissez une adresse e-mail valide",
- "sendMagicLink": "Envoyer le lien magique",
- "resend": "Renvoyer le lien",
- "resendIn": "Renvoyer le lien dans {{seconds}}s",
- "magicLinkSent": "Lien magique envoyé !",
- "magicLinkSentNote": "Consultez votre e-mail et utilisez le lien pour vous connecter."
- },
"requestAccess": {
"success": "Accès demandé"
},
- "verify": {
- "title": "Confirmation de votre e-mail",
- "description": "Veuillez patienter pendant que nous vous connectons\u2026"
- },
"fullName": {
"title": "Ajoutez votre nom",
"label": "Nom complet",
@@ -59,17 +39,7 @@
"tooShort": "Le nom complet est trop court",
"submit": "Continuer"
},
- "magicLinkExpired": {
- "title": "Lien expiré",
- "description": "Ce lien magique a expiré. Veuillez en demander un nouveau."
- },
- "magicLinkAlreadyUsed": {
- "title": "Lien déjà utilisé",
- "description": "Ce lien magique a déjà été utilisé. Veuillez en demander un nouveau."
- },
"errors": {
- "magicLinkFailed": "Impossible d'envoyer le lien magique. Veuillez réessayer.",
- "verifyFailed": "Impossible de vérifier le lien. Veuillez réessayer.",
"fullNameFailed": "Impossible d'enregistrer votre nom. Veuillez réessayer.",
"requestFailed": "Impossible de finaliser votre demande d'accès. Veuillez réessayer."
}
diff --git a/apps/compliance-portal/src/components/TopBar/TopBar.tsx b/apps/compliance-portal/src/components/TopBar/TopBar.tsx
index 0e3ceea01..7bd9f1fbb 100644
--- a/apps/compliance-portal/src/components/TopBar/TopBar.tsx
+++ b/apps/compliance-portal/src/components/TopBar/TopBar.tsx
@@ -27,6 +27,8 @@ import { useTranslation } from "react-i18next";
import { graphql, useFragment } from "react-relay";
import { Link as RouterLink, useLocation } from "react-router";
+import { buildRequestAllContinueUrl, redirectToInitiate } from "#/lib/auth/continueUrl";
+
import type { TopBar_query$key } from "./__generated__/TopBar_query.graphql";
import { TOP_BAR_NAV_ITEMS } from "./navItems";
import { TopBarMobileNav } from "./TopBarMobileNav";
@@ -110,15 +112,7 @@ export function TopBar({ queryKey }: TopBarProps) {
highContrast
iconStart={}
onClick={() => {
- const initiateURL = new URL(
- "/initiate",
- window.location.origin,
- );
- initiateURL.searchParams.set(
- "continue",
- location.pathname + location.search + location.hash,
- );
- window.location.href = initiateURL.toString();
+ redirectToInitiate(buildRequestAllContinueUrl());
}}
>
{t("topBar.getAccess")}
diff --git a/apps/compliance-portal/src/components/TopBar/TopBarMobileNav.tsx b/apps/compliance-portal/src/components/TopBar/TopBarMobileNav.tsx
index da8f4fec2..ab58721df 100644
--- a/apps/compliance-portal/src/components/TopBar/TopBarMobileNav.tsx
+++ b/apps/compliance-portal/src/components/TopBar/TopBarMobileNav.tsx
@@ -43,8 +43,7 @@ import { useTranslation } from "react-i18next";
import { graphql, useFragment } from "react-relay";
import { useLocation } from "react-router";
-import { buildRequestAllContinueUrl } from "#/lib/auth/continueUrl";
-import { useSignInDialog } from "#/lib/auth/signInDialogContext";
+import { buildRequestAllContinueUrl, redirectToInitiate } from "#/lib/auth/continueUrl";
import { useSignOut } from "#/lib/auth/useSignOut";
import { useSubscribeDialog } from "#/lib/mailingList/subscribeDialogContext";
@@ -72,7 +71,6 @@ function isActive(pathname: string, to: string): boolean {
export function TopBarMobileNav({ identityKey }: TopBarMobileNavProps) {
const { t } = useTranslation();
const { pathname } = useLocation();
- const { openSignIn } = useSignInDialog();
const { openSubscribe, isSubscribed, unsubscribe, isUnsubscribing } = useSubscribeDialog();
const [signOut, isSigningOut] = useSignOut();
const [open, setOpen] = useState(false);
@@ -150,7 +148,7 @@ export function TopBarMobileNav({ identityKey }: TopBarMobileNavProps) {
iconStart={}
onClick={() => {
close();
- openSignIn({ continueTo: buildRequestAllContinueUrl() });
+ redirectToInitiate(buildRequestAllContinueUrl());
}}
>
{t("topBar.getAccess")}
diff --git a/apps/compliance-portal/src/components/auth/OIDCButton.tsx b/apps/compliance-portal/src/components/auth/OIDCButton.tsx
deleted file mode 100644
index ccd6e5814..000000000
--- a/apps/compliance-portal/src/components/auth/OIDCButton.tsx
+++ /dev/null
@@ -1,75 +0,0 @@
-// Copyright (c) 2026 Probo Inc .
-//
-// Permission is hereby granted, free of charge, to any person obtaining a copy
-// of this software and associated documentation files (the "Software"), to deal
-// in the Software without restriction, including without limitation the rights
-// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-// copies of the Software, and to permit persons to whom the Software is
-// furnished to do so, subject to the following conditions:
-//
-// The above copyright notice and this permission notice shall be included in
-// all copies or substantial portions of the Software.
-//
-// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-// SOFTWARE.
-
-import { Google } from "@probo/ui/src/Atoms/ThirdParties/Google";
-import { Microsoft } from "@probo/ui/src/Atoms/ThirdParties/Microsoft";
-import { Button } from "@probo/ui/src/v2/Button/Button";
-import type { ComponentProps } from "react";
-import { useTranslation } from "react-i18next";
-import { graphql, useFragment } from "react-relay";
-
-import type { OIDCButton_provider$key } from "./__generated__/OIDCButton_provider.graphql";
-
-const providerFragment = graphql`
- fragment OIDCButton_provider on OIDCProviderInfo {
- name
- loginURL
- }
-`;
-
-const providerIcons: Record) => React.ReactNode> = {
- google: Google,
- microsoft: Microsoft,
-};
-
-interface OIDCButtonProps {
- providerKey: OIDCButton_provider$key;
- // Absolute URL to return to after the provider completes authentication.
- continueTo: string;
-}
-
-// Redirects the whole window to the provider's hosted login, carrying the
-// `continue` target so the portal resumes the pending flow on return.
-export function OIDCButton({ providerKey, continueTo }: OIDCButtonProps) {
- const { t } = useTranslation();
- const provider = useFragment(providerFragment, providerKey);
- const Icon = providerIcons[provider.name];
- const label = t("auth.signIn.withProvider", {
- provider: provider.name.charAt(0).toUpperCase() + provider.name.slice(1),
- });
-
- return (
- : undefined}
- onClick={() => {
- const loginURL = new URL(provider.loginURL, window.location.origin);
- loginURL.searchParams.set("continue", continueTo);
- window.location.href = loginURL.toString();
- }}
- >
- {label}
-
- );
-}
diff --git a/apps/compliance-portal/src/components/auth/OIDCProviders.tsx b/apps/compliance-portal/src/components/auth/OIDCProviders.tsx
deleted file mode 100644
index f916d4c14..000000000
--- a/apps/compliance-portal/src/components/auth/OIDCProviders.tsx
+++ /dev/null
@@ -1,94 +0,0 @@
-// Copyright (c) 2026 Probo Inc .
-//
-// Permission is hereby granted, free of charge, to any person obtaining a copy
-// of this software and associated documentation files (the "Software"), to deal
-// in the Software without restriction, including without limitation the rights
-// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-// copies of the Software, and to permit persons to whom the Software is
-// furnished to do so, subject to the following conditions:
-//
-// The above copyright notice and this permission notice shall be included in
-// all copies or substantial portions of the Software.
-//
-// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-// SOFTWARE.
-
-import { ButtonSkeleton } from "@probo/ui/src/v2/Button/ButtonSkeleton";
-import { ErrorBoundary } from "@probo/ui/src/v2/ErrorBoundary/ErrorBoundary";
-import { Text } from "@probo/ui/src/v2/typography/Text";
-import { Suspense } from "react";
-import { useTranslation } from "react-i18next";
-import { graphql, useLazyLoadQuery } from "react-relay";
-
-import type { OIDCProvidersQuery } from "./__generated__/OIDCProvidersQuery.graphql";
-import { OIDCButton } from "./OIDCButton";
-
-const oidcProvidersQuery = graphql`
- query OIDCProvidersQuery {
- oidcProviders {
- ...OIDCButton_provider
- }
- }
-`;
-
-interface OIDCProvidersProps {
- continueTo: string;
-}
-
-function Divider() {
- const { t } = useTranslation();
- return (
-
-
- >
- );
-}
-
-// SSO buttons for the sign-in dialog. Providers load lazily on open; if the
-// query fails or the trust center has none, the section renders nothing so the
-// email flow stays usable.
-export function OIDCProviders({ continueTo }: OIDCProvidersProps) {
- return (
- console.error("Failed to load SSO providers", error)}
- >
-
-
-
-
- )}
- >
-
-
-
- );
-}
diff --git a/apps/compliance-portal/src/components/auth/SignInDialog.tsx b/apps/compliance-portal/src/components/auth/SignInDialog.tsx
deleted file mode 100644
index 66c481fa5..000000000
--- a/apps/compliance-portal/src/components/auth/SignInDialog.tsx
+++ /dev/null
@@ -1,53 +0,0 @@
-// Copyright (c) 2026 Probo Inc .
-//
-// Permission is hereby granted, free of charge, to any person obtaining a copy
-// of this software and associated documentation files (the "Software"), to deal
-// in the Software without restriction, including without limitation the rights
-// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-// copies of the Software, and to permit persons to whom the Software is
-// furnished to do so, subject to the following conditions:
-//
-// The above copyright notice and this permission notice shall be included in
-// all copies or substantial portions of the Software.
-//
-// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-// SOFTWARE.
-
-import { Dialog } from "@probo/ui/src/v2/Dialog/Dialog";
-import { DialogDescription } from "@probo/ui/src/v2/Dialog/DialogDescription";
-import { DialogHeader } from "@probo/ui/src/v2/Dialog/DialogHeader";
-import { DialogPopup } from "@probo/ui/src/v2/Dialog/DialogPopup";
-import { DialogTitle } from "@probo/ui/src/v2/Dialog/DialogTitle";
-import { useTranslation } from "react-i18next";
-
-import { SignInForm } from "./SignInForm";
-
-interface SignInDialogProps {
- open: boolean;
- onOpenChange: (open: boolean) => void;
- // Absolute URL to return to after authentication.
- continueTo: string;
-}
-
-// The reusable "Login Dialog" from the design: a modal sign-in gate composed of
-// the kit Dialog plus the magic-link / SSO sign-in form.
-export function SignInDialog({ open, onOpenChange, continueTo }: SignInDialogProps) {
- const { t } = useTranslation();
-
- return (
-
- );
-}
diff --git a/apps/compliance-portal/src/components/auth/SignInForm.tsx b/apps/compliance-portal/src/components/auth/SignInForm.tsx
deleted file mode 100644
index e9347cf07..000000000
--- a/apps/compliance-portal/src/components/auth/SignInForm.tsx
+++ /dev/null
@@ -1,168 +0,0 @@
-// Copyright (c) 2026 Probo Inc .
-//
-// Permission is hereby granted, free of charge, to any person obtaining a copy
-// of this software and associated documentation files (the "Software"), to deal
-// in the Software without restriction, including without limitation the rights
-// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-// copies of the Software, and to permit persons to whom the Software is
-// furnished to do so, subject to the following conditions:
-//
-// The above copyright notice and this permission notice shall be included in
-// all copies or substantial portions of the Software.
-//
-// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-// SOFTWARE.
-
-import { Field } from "@base-ui/react/field";
-import { Form } from "@base-ui/react/form";
-import { Toast } from "@base-ui/react/toast";
-import type { GraphQLError } from "@probo/helpers";
-import { Button } from "@probo/ui/src/v2/Button/Button";
-import { DialogBody } from "@probo/ui/src/v2/Dialog/DialogBody";
-import { DialogFooter } from "@probo/ui/src/v2/Dialog/DialogFooter";
-import { TextField } from "@probo/ui/src/v2/form/TextField";
-import { Text } from "@probo/ui/src/v2/typography/Text";
-import { useEffect, useRef, useState } from "react";
-import { useTranslation } from "react-i18next";
-import { graphql } from "relay-runtime";
-
-import { getSafeContinueUrl } from "#/lib/auth/continueUrl";
-import { useMutation } from "#/lib/relay/useMutation";
-
-import type { SignInFormMutation } from "./__generated__/SignInFormMutation.graphql";
-import { OIDCProviders } from "./OIDCProviders";
-
-const RESEND_COOLDOWN_SECONDS = 60;
-
-const sendMagicLinkMutation = graphql`
- mutation SignInFormMutation($input: SendMagicLinkInput!) {
- sendMagicLink(input: $input) {
- success
- }
- }
-`;
-
-interface SignInFormProps {
- // Absolute URL to return to after authentication (carries the request-all
- // marker so an access request resumes once signed in).
- continueTo: string;
- onCancel: () => void;
-}
-
-// Sign-in form used inside the dialog: SSO providers, then a magic-link email
-// flow. On success it flips to a "check your email" state with a resend timer.
-export function SignInForm({ continueTo, onCancel }: SignInFormProps) {
- const { t } = useTranslation();
- const toast = Toast.useToastManager();
- const [magicLinkSent, setMagicLinkSent] = useState(false);
- const [secondsLeft, setSecondsLeft] = useState(RESEND_COOLDOWN_SECONDS);
- const intervalRef = useRef>(undefined);
-
- const [sendMagicLink, isSending] = useMutation(
- sendMagicLinkMutation,
- { errorToast: false },
- );
-
- useEffect(() => {
- if (!magicLinkSent) {
- return;
- }
- intervalRef.current = setInterval(() => {
- setSecondsLeft(seconds => Math.max(seconds - 1, 0));
- }, 1000);
- return () => clearInterval(intervalRef.current);
- }, [magicLinkSent]);
-
- const handleSend = (email: string) => {
- void sendMagicLink({
- variables: { input: { email, continue: continueTo } },
- onCompleted: (_response, errors) => {
- const code = (errors?.[0] as GraphQLError | undefined)?.extensions?.code;
-
- // Already signed in elsewhere: jump straight to the return URL so any
- // pending access request resumes.
- if (code === "ALREADY_AUTHENTICATED") {
- window.location.href = getSafeContinueUrl(continueTo);
- return;
- }
-
- if (errors && errors.length > 0) {
- toast.add({ title: t("auth.errors.magicLinkFailed"), type: "error" });
- return;
- }
-
- setSecondsLeft(RESEND_COOLDOWN_SECONDS);
- setMagicLinkSent(true);
- toast.add({ title: t("auth.signIn.magicLinkSent"), type: "success" });
- },
- onError: () => {
- toast.add({ title: t("auth.errors.magicLinkFailed"), type: "error" });
- },
- }).catch(() => {});
- };
-
- const resendDisabled = magicLinkSent && secondsLeft > 0;
- const submitLabel = magicLinkSent
- ? secondsLeft > 0
- ? t("auth.signIn.resendIn", { seconds: secondsLeft })
- : t("auth.signIn.resend")
- : t("auth.signIn.sendMagicLink");
-
- return (
-
- );
-}
diff --git a/apps/compliance-portal/src/lib/auth/SignInDialogProvider.tsx b/apps/compliance-portal/src/lib/auth/SignInDialogProvider.tsx
deleted file mode 100644
index 622473fc1..000000000
--- a/apps/compliance-portal/src/lib/auth/SignInDialogProvider.tsx
+++ /dev/null
@@ -1,53 +0,0 @@
-// Copyright (c) 2026 Probo Inc .
-//
-// Permission is hereby granted, free of charge, to any person obtaining a copy
-// of this software and associated documentation files (the "Software"), to deal
-// in the Software without restriction, including without limitation the rights
-// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-// copies of the Software, and to permit persons to whom the Software is
-// furnished to do so, subject to the following conditions:
-//
-// The above copyright notice and this permission notice shall be included in
-// all copies or substantial portions of the Software.
-//
-// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-// SOFTWARE.
-
-import { type ReactNode, useCallback, useMemo, useState } from "react";
-
-import { SignInDialog } from "#/components/auth/SignInDialog";
-import {
- type OpenSignInOptions,
- SignInDialogContextProvider,
-} from "#/lib/auth/signInDialogContext";
-
-interface SignInDialogProviderProps {
- children: ReactNode;
-}
-
-// Owns the single sign-in dialog instance and exposes `openSignIn` so any
-// descendant (top bar, resource rows, …) can prompt authentication and pass the
-// URL to return to afterwards.
-export function SignInDialogProvider({ children }: SignInDialogProviderProps) {
- const [open, setOpen] = useState(false);
- const [continueTo, setContinueTo] = useState(() => window.location.href);
-
- const openSignIn = useCallback((options?: OpenSignInOptions) => {
- setContinueTo(options?.continueTo ?? window.location.href);
- setOpen(true);
- }, []);
-
- const value = useMemo(() => ({ openSignIn }), [openSignIn]);
-
- return (
-
- {children}
-
-
- );
-}
diff --git a/apps/compliance-portal/src/lib/auth/continueUrl.ts b/apps/compliance-portal/src/lib/auth/continueUrl.ts
index c6aed80ae..1932b15db 100644
--- a/apps/compliance-portal/src/lib/auth/continueUrl.ts
+++ b/apps/compliance-portal/src/lib/auth/continueUrl.ts
@@ -108,3 +108,12 @@ export function gateRedirectPath(error: unknown, continueUrl: string): string |
}
return null;
}
+
+// Sends the browser to the OAuth entry point, carrying a validated continue URL
+// so the user returns to the portal (and any deferred access request resumes)
+// after sign-in.
+export function redirectToInitiate(continueTo: string): void {
+ const initiateURL = new URL("/initiate", window.location.origin);
+ initiateURL.searchParams.set("continue", getSafeContinueUrl(continueTo));
+ window.location.href = initiateURL.toString();
+}
diff --git a/apps/compliance-portal/src/lib/auth/signInDialogContext.ts b/apps/compliance-portal/src/lib/auth/signInDialogContext.ts
deleted file mode 100644
index bb9860aaf..000000000
--- a/apps/compliance-portal/src/lib/auth/signInDialogContext.ts
+++ /dev/null
@@ -1,43 +0,0 @@
-// Copyright (c) 2026 Probo Inc .
-//
-// Permission is hereby granted, free of charge, to any person obtaining a copy
-// of this software and associated documentation files (the "Software"), to deal
-// in the Software without restriction, including without limitation the rights
-// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-// copies of the Software, and to permit persons to whom the Software is
-// furnished to do so, subject to the following conditions:
-//
-// The above copyright notice and this permission notice shall be included in
-// all copies or substantial portions of the Software.
-//
-// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-// SOFTWARE.
-
-import { createContext, useContext } from "react";
-
-export type OpenSignInOptions = {
- // Absolute URL the user should return to once authenticated. Defaults to the
- // current page. Include the request-all marker to resume an access request.
- continueTo?: string;
-};
-
-export type SignInDialogContextValue = {
- openSignIn: (options?: OpenSignInOptions) => void;
-};
-
-const SignInDialogContext = createContext(null);
-
-export const SignInDialogContextProvider = SignInDialogContext.Provider;
-
-export function useSignInDialog(): SignInDialogContextValue {
- const context = useContext(SignInDialogContext);
- if (context === null) {
- throw new Error("useSignInDialog must be used within a SignInDialogProvider");
- }
- return context;
-}
diff --git a/apps/compliance-portal/src/lib/auth/useResumeAccessRequest.ts b/apps/compliance-portal/src/lib/auth/useResumeAccessRequest.ts
index d0176729d..2a1105bdb 100644
--- a/apps/compliance-portal/src/lib/auth/useResumeAccessRequest.ts
+++ b/apps/compliance-portal/src/lib/auth/useResumeAccessRequest.ts
@@ -96,7 +96,7 @@ const requestFileMutation = graphql`
}
`;
-// After a user signs in through the dialog, they land back on the page that
+// After a user signs in through OAuth /initiate, they land back on the page that
// carried a deferred access marker. This hook fires the matching mutation once
// (when authenticated) — request-all from the top bar, or a single
// document / report / file requested from a locked row — routes to the
diff --git a/apps/compliance-portal/src/lib/mailingList/SubscribeDialogProvider.tsx b/apps/compliance-portal/src/lib/mailingList/SubscribeDialogProvider.tsx
index d397c283d..19e0b4754 100644
--- a/apps/compliance-portal/src/lib/mailingList/SubscribeDialogProvider.tsx
+++ b/apps/compliance-portal/src/lib/mailingList/SubscribeDialogProvider.tsx
@@ -30,8 +30,7 @@ import { graphql, useFragment } from "react-relay";
import { useSearchParams } from "react-router";
import { SubscribeDialog } from "#/components/SubscribeDialog/SubscribeDialog";
-import { buildSubscribeContinueUrl, SUBSCRIBE_PARAM } from "#/lib/auth/continueUrl";
-import { useSignInDialog } from "#/lib/auth/signInDialogContext";
+import { buildSubscribeContinueUrl, redirectToInitiate, SUBSCRIBE_PARAM } from "#/lib/auth/continueUrl";
import {
SubscribeDialogContextProvider,
} from "#/lib/mailingList/subscribeDialogContext";
@@ -69,7 +68,6 @@ export function SubscribeDialogProvider({
children,
}: SubscribeDialogProviderProps) {
const data = useFragment(subscribeDialogProviderFragment, queryKey);
- const { openSignIn } = useSignInDialog();
const [searchParams, setSearchParams] = useSearchParams();
const [dialogOpen, setDialogOpen] = useState(false);
const [unsubscribeFromMailingList, isUnsubscribing] = useUnsubscribeFromMailingList();
@@ -80,11 +78,11 @@ export function SubscribeDialogProvider({
const openSubscribe = useCallback(() => {
if (viewer == null) {
- openSignIn({ continueTo: buildSubscribeContinueUrl() });
+ redirectToInitiate(buildSubscribeContinueUrl());
return;
}
setDialogOpen(true);
- }, [openSignIn, viewer]);
+ }, [viewer]);
const unsubscribe = useCallback(async () => {
try {
diff --git a/apps/compliance-portal/src/pages/MainLayout.tsx b/apps/compliance-portal/src/pages/MainLayout.tsx
index a2cc1aa16..e4fb531ed 100644
--- a/apps/compliance-portal/src/pages/MainLayout.tsx
+++ b/apps/compliance-portal/src/pages/MainLayout.tsx
@@ -25,7 +25,6 @@ import { Outlet, useMatch } from "react-router";
import { PoweredBy } from "#/components/PoweredBy/PoweredBy";
import { TopBar } from "#/components/TopBar/TopBar";
-import { SignInDialogProvider } from "#/lib/auth/SignInDialogProvider";
import { useResumeAccessRequest } from "#/lib/auth/useResumeAccessRequest";
import { SubscribeDialogProvider } from "#/lib/mailingList/SubscribeDialogProvider";
@@ -57,28 +56,26 @@ export function MainLayout({ queryRef }: MainLayoutProps) {
// stage; every other page uses normal document flow so the footer sits after
// content (and at the bottom of short pages via flex-1 main).
return (
-
-
+
+
+
-
-
-
-
- {isDocumentViewer ? null : }
+
-
-
+ {isDocumentViewer ? null : }
+
+
);
}
diff --git a/apps/compliance-portal/src/pages/auth/AuthLayout.tsx b/apps/compliance-portal/src/pages/auth/AuthLayout.tsx
index 7d55fedac..6171c7a1e 100644
--- a/apps/compliance-portal/src/pages/auth/AuthLayout.tsx
+++ b/apps/compliance-portal/src/pages/auth/AuthLayout.tsx
@@ -21,8 +21,7 @@
import { Card } from "@probo/ui/src/v2/Card/Card";
import { Outlet } from "react-router";
-// Minimal centered chrome for the standalone auth steps that a magic-link email
-// or a full-name gate lands on (these cannot live inside the portal shell).
+// Minimal centered chrome for standalone auth steps (e.g. the full-name gate).
export default function AuthLayout() {
return (
diff --git a/apps/compliance-portal/src/pages/auth/MagicLinkAlreadyUsedPage.tsx b/apps/compliance-portal/src/pages/auth/MagicLinkAlreadyUsedPage.tsx
deleted file mode 100644
index 1bb46be38..000000000
--- a/apps/compliance-portal/src/pages/auth/MagicLinkAlreadyUsedPage.tsx
+++ /dev/null
@@ -1,39 +0,0 @@
-// Copyright (c) 2026 Probo Inc .
-//
-// Permission is hereby granted, free of charge, to any person obtaining a copy
-// of this software and associated documentation files (the "Software"), to deal
-// in the Software without restriction, including without limitation the rights
-// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-// copies of the Software, and to permit persons to whom the Software is
-// furnished to do so, subject to the following conditions:
-//
-// The above copyright notice and this permission notice shall be included in
-// all copies or substantial portions of the Software.
-//
-// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-// SOFTWARE.
-
-import { Link } from "@probo/ui/src/v2/Button/Link";
-import { Heading } from "@probo/ui/src/v2/typography/Heading";
-import { Text } from "@probo/ui/src/v2/typography/Text";
-import { useTranslation } from "react-i18next";
-
-// Shown when a magic link was already consumed; the user restarts sign-in.
-export default function MagicLinkAlreadyUsedPage() {
- const { t } = useTranslation();
-
- return (
-
- );
-}
diff --git a/apps/compliance-portal/src/pages/auth/MagicLinkExpiredPage.tsx b/apps/compliance-portal/src/pages/auth/MagicLinkExpiredPage.tsx
deleted file mode 100644
index 34c8d7b24..000000000
--- a/apps/compliance-portal/src/pages/auth/MagicLinkExpiredPage.tsx
+++ /dev/null
@@ -1,39 +0,0 @@
-// Copyright (c) 2026 Probo Inc .
-//
-// Permission is hereby granted, free of charge, to any person obtaining a copy
-// of this software and associated documentation files (the "Software"), to deal
-// in the Software without restriction, including without limitation the rights
-// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-// copies of the Software, and to permit persons to whom the Software is
-// furnished to do so, subject to the following conditions:
-//
-// The above copyright notice and this permission notice shall be included in
-// all copies or substantial portions of the Software.
-//
-// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-// SOFTWARE.
-
-import { Link } from "@probo/ui/src/v2/Button/Link";
-import { Heading } from "@probo/ui/src/v2/typography/Heading";
-import { Text } from "@probo/ui/src/v2/typography/Text";
-import { useTranslation } from "react-i18next";
-
-// Shown when a magic link has expired; the user restarts sign-in from home.
-export default function MagicLinkExpiredPage() {
- const { t } = useTranslation();
-
- return (
-
- );
-}
diff --git a/apps/compliance-portal/src/pages/auth/VerifyMagicLinkPage.tsx b/apps/compliance-portal/src/pages/auth/VerifyMagicLinkPage.tsx
deleted file mode 100644
index 803d6d94a..000000000
--- a/apps/compliance-portal/src/pages/auth/VerifyMagicLinkPage.tsx
+++ /dev/null
@@ -1,103 +0,0 @@
-// Copyright (c) 2026 Probo Inc .
-//
-// Permission is hereby granted, free of charge, to any person obtaining a copy
-// of this software and associated documentation files (the "Software"), to deal
-// in the Software without restriction, including without limitation the rights
-// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-// copies of the Software, and to permit persons to whom the Software is
-// furnished to do so, subject to the following conditions:
-//
-// The above copyright notice and this permission notice shall be included in
-// all copies or substantial portions of the Software.
-//
-// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-// SOFTWARE.
-
-import { Toast } from "@base-ui/react/toast";
-import type { GraphQLError } from "@probo/helpers";
-import { Heading } from "@probo/ui/src/v2/typography/Heading";
-import { Text } from "@probo/ui/src/v2/typography/Text";
-import { useEffect, useRef } from "react";
-import { useTranslation } from "react-i18next";
-import { useNavigate, useSearchParams } from "react-router";
-import { graphql } from "relay-runtime";
-
-import { getSafeContinueUrl } from "#/lib/auth/continueUrl";
-import { useMutation } from "#/lib/relay/useMutation";
-
-import type { VerifyMagicLinkPageMutation } from "./__generated__/VerifyMagicLinkPageMutation.graphql";
-
-const verifyMagicLinkMutation = graphql`
- mutation VerifyMagicLinkPageMutation($input: VerifyMagicLinkInput!) {
- verifyMagicLink(input: $input) {
- continue
- }
- }
-`;
-
-// Landing page for the magic-link email. It verifies the token on mount and
-// forwards to the (validated) continue URL, where any pending access request
-// resumes.
-export default function VerifyMagicLinkPage() {
- const { t } = useTranslation();
- const toast = Toast.useToastManager();
- const navigate = useNavigate();
- const [searchParams] = useSearchParams();
- const submittedRef = useRef(false);
-
- const [verifyMagicLink] = useMutation(
- verifyMagicLinkMutation,
- { errorToast: false },
- );
-
- useEffect(() => {
- const token = searchParams.get("token");
- if (!token || submittedRef.current) {
- return;
- }
- submittedRef.current = true;
-
- void verifyMagicLink({
- variables: { input: { token: token.trim() } },
- onCompleted: (response, errors) => {
- const code = (errors?.[0] as GraphQLError | undefined)?.extensions?.code;
-
- if (code === "ALREADY_AUTHENTICATED") {
- // Already signed in: honor a `continue` on the URL if present so a
- // deferred access request still resumes, instead of always going home.
- window.location.href = getSafeContinueUrl(searchParams.get("continue"));
- return;
- }
- if (code === "TOKEN_EXPIRED") {
- void navigate("/magic-link-expired");
- return;
- }
- if (code === "TOKEN_ALREADY_USED") {
- void navigate("/magic-link-already-used");
- return;
- }
- if (errors && errors.length > 0) {
- toast.add({ title: t("auth.errors.verifyFailed"), type: "error" });
- return;
- }
-
- window.location.href = getSafeContinueUrl(response.verifyMagicLink?.continue);
- },
- onError: () => {
- toast.add({ title: t("auth.errors.verifyFailed"), type: "error" });
- },
- }).catch(() => {});
- }, [navigate, searchParams, t, toast, verifyMagicLink]);
-
- return (
-