Add sign-in dialog and request-access gating

The compliance portal's "Get Access" button was inert and the portal
had no way to authenticate or request trust-center access. Add a modal
sign-in flow (magic link + OIDC) that gates the requestAllAccesses
mutation, mirroring the trust app's flow but as a dialog instead of a
full /connect page.

Introduce the two v2 UI-kit primitives this depends on: a headless
Base UI Dialog and a styled Toaster (mutation toasts had no host yet).
Wire the top-bar button to open the dialog, resume the deferred access
request once authenticated, and add standalone routes for magic-link
verification and the full-name gate.

Signed-off-by: Émile Ré <emile@probo.com>
This commit is contained in:
Émile Ré
2026-07-15 13:13:40 +02:00
parent 828bd70371
commit 2a10b29859
32 changed files with 1644 additions and 10 deletions

View File

@@ -0,0 +1,31 @@
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
//
// 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 as BaseDialog } from "@base-ui/react/dialog";
import type { ComponentProps } from "react";
// Root of the modal dialog (Radix "Dialog"). Controlled the same way as Base
// UI's Dialog: `open` / `onOpenChange`, or left uncontrolled. See
// contrib/claude/ui.md.
export type DialogProps = ComponentProps<typeof BaseDialog.Root>;
export function Dialog(props: DialogProps) {
return <BaseDialog.Root {...props} />;
}

View File

@@ -0,0 +1,32 @@
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
//
// 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 { ComponentProps } from "react";
import { dialog } from "./variants";
// Content region between the header and footer, carrying the dialog's horizontal
// padding.
export function DialogBody(props: ComponentProps<"div">) {
const { className, ...rest } = props;
const { body } = dialog();
return <div className={body({ className })} {...rest} />;
}

View File

@@ -0,0 +1,30 @@
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
//
// 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 as BaseDialog } from "@base-ui/react/dialog";
import type { ComponentProps } from "react";
// Closes the dialog. Pass `render` to reuse an existing control (e.g. a Button)
// as the close action, per Base UI's polymorphism.
export type DialogCloseProps = ComponentProps<typeof BaseDialog.Close>;
export function DialogClose(props: DialogCloseProps) {
return <BaseDialog.Close {...props} />;
}

View File

@@ -0,0 +1,36 @@
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
//
// 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 as BaseDialog } from "@base-ui/react/dialog";
import type { ComponentProps } from "react";
import { dialog } from "./variants";
// Accessible dialog description (wires `aria-describedby` via Base UI).
export type DialogDescriptionProps = Omit<ComponentProps<typeof BaseDialog.Description>, "className"> & {
className?: string;
};
export function DialogDescription(props: DialogDescriptionProps) {
const { className, ...rest } = props;
const { description } = dialog();
return <BaseDialog.Description className={description({ className })} {...rest} />;
}

View File

@@ -0,0 +1,32 @@
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
//
// 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 { ComponentProps } from "react";
import { dialog } from "./variants";
// Footer region for dialog actions, right-aligned with the dialog's horizontal
// padding.
export function DialogFooter(props: ComponentProps<"div">) {
const { className, ...rest } = props;
const { footer } = dialog();
return <div className={footer({ className })} {...rest} />;
}

View File

@@ -0,0 +1,32 @@
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
//
// 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 { ComponentProps } from "react";
import { dialog } from "./variants";
// Header region grouping the title and description with the dialog's horizontal
// padding.
export function DialogHeader(props: ComponentProps<"div">) {
const { className, ...rest } = props;
const { header } = dialog();
return <div className={header({ className })} {...rest} />;
}

View File

@@ -0,0 +1,46 @@
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
//
// 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 as BaseDialog } from "@base-ui/react/dialog";
import type { ComponentProps } from "react";
import { dialog } from "./variants";
export type DialogPopupProps
= & Omit<ComponentProps<typeof BaseDialog.Popup>, "className">
& {
className?: string;
};
// Portal + dimmed backdrop + centered, styled popup frame. Children compose the
// header / body / footer regions.
export function DialogPopup(props: DialogPopupProps) {
const { className, children, ...popupProps } = props;
const { backdrop, popup } = dialog();
return (
<BaseDialog.Portal>
<BaseDialog.Backdrop className={backdrop()} />
<BaseDialog.Popup className={popup({ className })} {...popupProps}>
{children}
</BaseDialog.Popup>
</BaseDialog.Portal>
);
}

View File

@@ -0,0 +1,45 @@
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
//
// 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 { TextSkeleton } from "../typography/TextSkeleton";
import { dialog, dialogSkeleton } from "./variants";
// Loading placeholder matching the dialog frame (header + body lines + footer),
// importing only variants and skeleton primitives — never Base UI.
export function DialogSkeleton() {
const { header, body, footer } = dialog();
return (
<div className={dialogSkeleton()} aria-hidden>
<div className={header()}>
<TextSkeleton size={4} className="w-48" />
<TextSkeleton size={2} className="w-72" />
</div>
<div className={body()}>
<TextSkeleton size={2} className="w-full" />
</div>
<div className={footer()}>
<TextSkeleton size={2} className="w-20" />
<TextSkeleton size={2} className="w-28" />
</div>
</div>
);
}

View File

@@ -0,0 +1,36 @@
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
//
// 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 as BaseDialog } from "@base-ui/react/dialog";
import type { ComponentProps } from "react";
import { dialog } from "./variants";
// Accessible dialog title (wires `aria-labelledby` via Base UI).
export type DialogTitleProps = Omit<ComponentProps<typeof BaseDialog.Title>, "className"> & {
className?: string;
};
export function DialogTitle(props: DialogTitleProps) {
const { className, ...rest } = props;
const { title } = dialog();
return <BaseDialog.Title className={title({ className })} {...rest} />;
}

View File

@@ -0,0 +1,30 @@
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
//
// 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 as BaseDialog } from "@base-ui/react/dialog";
import type { ComponentProps } from "react";
// Opens the dialog. Pass `render` to use an existing control (e.g. a Button) as
// the trigger, per Base UI's polymorphism — do not clone children by hand.
export type DialogTriggerProps = ComponentProps<typeof BaseDialog.Trigger>;
export function DialogTrigger(props: DialogTriggerProps) {
return <BaseDialog.Trigger {...props} />;
}

View File

@@ -0,0 +1,57 @@
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
//
// 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 { tv } from "tailwind-variants/lite";
// Modal dialog (Radix "Dialog" over Base UI). The popup is centered and gets
// vertical padding + a 16px gap; each region (header, body, footer) carries its
// own horizontal padding so a full-bleed body slot stays possible.
export const dialog = tv({
slots: {
backdrop: [
"fixed inset-0 z-50 bg-sand-12/40",
"transition-opacity duration-150",
"data-starting-style:opacity-0 data-ending-style:opacity-0",
],
popup: [
"fixed left-1/2 top-1/2 z-50 -translate-x-1/2 -translate-y-1/2",
"flex w-[calc(100vw-2rem)] max-w-[600px] flex-col gap-4",
"max-h-[calc(100vh-2rem)] overflow-y-auto overflow-x-clip",
"rounded-5 border border-sand-6 bg-sand-1 py-6 shadow-6 outline-none",
"transition-all duration-150",
"data-starting-style:scale-95 data-starting-style:opacity-0",
"data-ending-style:scale-95 data-ending-style:opacity-0",
],
header: "flex flex-col gap-2 px-6",
title: "text-4 font-medium text-sand-12",
description: "text-2 text-sand-11",
body: "px-6",
footer: "flex items-center justify-end gap-3 px-6",
},
});
// Static frame matching the dialog popup, without the interactive positioning,
// so a placeholder can render before Base UI (and the content) loads.
export const dialogSkeleton = tv({
base: [
"flex w-full max-w-[600px] flex-col gap-4",
"rounded-5 border border-sand-6 bg-sand-1 py-6 shadow-6",
],
});