ultisuite-client/app/onboard/claim/page.tsx
R3D347HR4Y 6c7278a3aa
Some checks are pending
E2E / Playwright e2e (push) Waiting to run
feat(onboarding): implement claim and migration pages with OAuth support
- Added OnboardClaimPage and OnboardMigrationPage components for user onboarding.
- Integrated OAuth login flow for Google and Microsoft accounts.
- Implemented error handling and user feedback for claim and migration processes.
- Created MigrationStepList and MigrationOnboardingAlerts components for progress tracking.
- Added MailDomainsSection and MigrationProjectsPanel for admin settings.
- Introduced e2e tests for onboarding migration scenarios.
2026-06-13 12:47:03 +02:00

175 lines
5.9 KiB
TypeScript

"use client"
import { Suspense, useEffect, useMemo, useState } from "react"
import Link from "next/link"
import { useRouter, useSearchParams } from "next/navigation"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { MigrationStepList, appOnlyAuthStepLabel } from "@/components/migration/onboarding-ui"
import { ApiRequestError } from "@/lib/api/client"
import { useClaimMigration, useMigrationInvite } from "@/lib/api/hooks/use-hosted-mail"
import { useAuthReady } from "@/lib/api/use-auth-ready"
import { buildOidcLoginUrl } from "@/lib/auth/login-url"
export default function OnboardClaimPage() {
return (
<Suspense
fallback={
<main className="mx-auto max-w-lg px-4 py-16">
<p className="text-muted-foreground">Chargement</p>
</main>
}
>
<OnboardClaimContent />
</Suspense>
)
}
function claimErrorMessage(error: unknown): string {
if (error instanceof ApiRequestError) {
switch (error.code) {
case "email_mismatch":
return "L'adresse de connexion ne correspond pas à l'invitation. Utilisez le compte invité ou contactez votre administrateur pour ajouter une adresse SSO alternative."
case "invite_already_claimed":
return "Cette invitation a déjà été revendiquée."
case "invite_not_found":
return "Invitation introuvable ou expirée."
case "migration_domain_not_active":
return "Le domaine mail du projet n'est pas encore actif — contactez votre administrateur."
case "migration_domain_mismatch":
return "L'adresse invitée ne correspond pas au domaine mail du projet."
}
return error.message
}
if (error instanceof Error) {
return error.message
}
return "Échec de la revendication — vérifiez que vous êtes connecté avec la bonne adresse."
}
function OnboardClaimContent() {
const params = useSearchParams()
const router = useRouter()
const token = params.get("token")
const { authenticated } = useAuthReady()
const inviteQuery = useMigrationInvite(token)
const claimMutation = useClaimMigration()
const [password, setPassword] = useState("")
const loginHref = useMemo(() => {
const returnTo = token ? `/onboard/claim?token=${encodeURIComponent(token)}` : "/onboard/claim"
return buildOidcLoginUrl({ returnTo })
}, [token])
const invite = inviteQuery.data?.invite
const project = inviteQuery.data?.project
const alreadyClaimed = invite?.status === "claimed"
useEffect(() => {
if (!token || !alreadyClaimed) return
router.replace(`/onboard/migration?token=${encodeURIComponent(token)}`)
}, [token, alreadyClaimed, router])
if (!token) {
return (
<main className="mx-auto max-w-lg px-4 py-16">
<h1 className="text-2xl font-semibold">Lien d&apos;invitation invalide</h1>
<p className="mt-2 text-muted-foreground">Ce lien ne contient pas de jeton de migration.</p>
</main>
)
}
if (!authenticated) {
return (
<main className="mx-auto max-w-lg px-4 py-16">
<h1 className="text-2xl font-semibold">Revendiquer votre compte</h1>
<p className="mt-2 text-muted-foreground">
Connectez-vous avec le même compte que l&apos;adresse invitée (
{inviteQuery.data?.invite.email ?? "…"}) pour provisionner votre boîte Ultimail.
</p>
<div className="mt-6">
<MigrationStepList
steps={[
{ label: "Connexion (Google / Microsoft / Ultimail)", done: false, current: true },
{ label: "Revendiquer la boîte mail", done: false, current: false },
{ label: "Autoriser l'import des données", done: false, current: false },
]}
/>
</div>
<Button asChild className="mt-6">
<Link href={loginHref}>Se connecter pour continuer</Link>
</Button>
</main>
)
}
if (alreadyClaimed) {
return (
<main className="mx-auto max-w-lg px-4 py-16">
<p className="text-muted-foreground">Redirection vers le suivi de migration</p>
</main>
)
}
const steps = [
{ label: "Connexion", done: true, current: false },
{ label: "Revendiquer la boîte mail", done: false, current: true },
{
label: appOnlyAuthStepLabel(project),
done: false,
current: false,
},
]
return (
<main className="mx-auto max-w-lg px-4 py-16">
<h1 className="text-2xl font-semibold">Revendiquer votre compte</h1>
<p className="mt-2 text-muted-foreground">
Projet {project?.name ?? ""} {invite?.email}
</p>
<div className="mt-6">
<MigrationStepList steps={steps} />
</div>
<p className="mt-4 text-sm text-muted-foreground">
Connectez-vous avec le compte correspondant à l&apos;invitation ({invite?.email}). Les
alias SSO reconnus (UPN, adresse alternative) sont acceptés si configurés par
l&apos;administrateur.
</p>
<div className="mt-8 space-y-4">
<div className="space-y-2">
<Label htmlFor="mailbox-password">Mot de passe boîte mail (optionnel)</Label>
<Input
id="mailbox-password"
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="Pour IMAP/SMTP hébergé"
/>
</div>
{claimMutation.isError && (
<p className="text-sm text-destructive">{claimErrorMessage(claimMutation.error)}</p>
)}
<Button
className="w-full"
disabled={claimMutation.isPending}
onClick={() => {
void claimMutation
.mutateAsync({ token, password: password || undefined })
.then(() => {
window.location.href = `/onboard/migration?token=${encodeURIComponent(token)}`
})
}}
>
Revendiquer mon compte
</Button>
</div>
</main>
)
}