Implement activate account page

Signed-off-by: Émile Ré <emile@getprobo.com>
This commit is contained in:
Émile Ré
2026-02-17 14:30:32 +04:00
parent 47aba96b69
commit 5ab5d2cc4c
13 changed files with 278 additions and 138 deletions

View File

@@ -1,5 +1,5 @@
/**
* @generated SignedSource<<e920b2bbd380e12014a100bfb963c8f7>>
* @generated SignedSource<<b79143becc90a4776f321d0e91db49bd>>
* @lightSyntaxTransform
* @nogrep
*/
@@ -10,7 +10,7 @@
import { ConcreteRequest } from 'relay-runtime';
export type ActivateAccountInput = {
password: string;
password?: string | null | undefined;
token: string;
};
export type ActivateAccountPageMutation$variables = {

View File

@@ -1,14 +1,13 @@
import { formatError } from "@probo/helpers";
import { formatError, type GraphQLError } from "@probo/helpers";
import { usePageTitle } from "@probo/hooks";
import { useTranslate } from "@probo/i18n";
import { Button, Field, useToast } from "@probo/ui";
import { useToast } from "@probo/ui";
import { useCallback, useEffect, useRef } from "react";
import { useMutation } from "react-relay";
import { Link, useNavigate, useSearchParams } from "react-router";
import { graphql } from "relay-runtime";
import { z } from "zod";
import type { ActivateAccountPageMutation } from "#/__generated__/iam/ActivateAccountPageMutation.graphql";
import { useFormWithSchema } from "#/hooks/useFormWithSchema";
const activateAccountMutation = graphql`
mutation ActivateAccountPageMutation(
@@ -22,64 +21,45 @@ const activateAccountMutation = graphql`
}
`;
const schema = z.object({
password: z.string().min(8),
fullName: z.string().min(2),
});
type FormData = z.infer<typeof schema>;
export default function ActivateAccountPage() {
const { __ } = useTranslate();
const { toast } = useToast();
const navigate = useNavigate();
const [searchParams] = useSearchParams();
const fullNameFromParams = searchParams.get("fullName") || "";
const submittedRef = useRef<boolean>(false);
usePageTitle(__("Sign up"));
const { register, handleSubmit, formState } = useFormWithSchema(schema, {
defaultValues: {
password: "",
fullName: fullNameFromParams,
},
});
usePageTitle(__("Activate Account"));
const [activateAccount] = useMutation<ActivateAccountPageMutation>(activateAccountMutation);
const onSubmit = (data: FormData) => {
const token = searchParams.get("token");
if (!token) {
toast({
title: __("Signup failed"),
description: __("Invalid or missing invitation token"),
variant: "error",
});
return;
}
const handleActivateAccount = useCallback((token: string) => {
if (submittedRef.current) return;
activateAccount({
variables: {
input: {
token,
password: data.password,
fullName: data.fullName,
},
input: { token },
},
onCompleted: (_, e) => {
if (e) {
onCompleted: (_, errors: GraphQLError[] | null) => {
if (errors) {
for (const err of errors) {
if (err.extensions?.code === "ALREADY_AUTHENTICATED") {
window.location.href = "/";
return;
}
}
toast({
title: __("Signup failed"),
description: formatError(__("Signup failed"), e),
title: __("Activation failed"),
description: formatError(__("Activation failed"), errors),
variant: "error",
});
return;
}
toast({
title: __("Success"),
description: __(
"Account created successfully. Please accept your invitation to join the organization.",
"Account activated successfully.",
),
variant: "success",
});
@@ -87,60 +67,37 @@ export default function ActivateAccountPage() {
},
onError: (e) => {
toast({
title: __("Signup failed"),
title: __("Activation failed"),
description: e.message,
variant: "error",
});
},
});
};
}, [__, toast, activateAccount, navigate]);
useEffect(() => {
const token = searchParams.get("token");
if (!submittedRef.current && token) {
void handleActivateAccount(token.trim());
submittedRef.current = true;
}
}, [handleActivateAccount, searchParams]);
return (
<div className="space-y-6 w-full max-w-md mx-auto pt-8">
<div className="space-y-2 text-center">
<h1 className="text-3xl font-bold">{__("Create your account")}</h1>
<h1 className="text-3xl font-bold">{__("Account Activation")}</h1>
<p className="text-txt-tertiary">
{__("Set your password to join the organization")}
{__("Activating your account…")}
</p>
</div>
<form onSubmit={e => void handleSubmit(onSubmit)(e)} className="space-y-4">
<Field
label={__("Full Name")}
type="text"
placeholder={__("John Doe")}
{...register("fullName")}
required
error={formState.errors.fullName?.message}
/>
<Field
label={__("Password")}
type="password"
placeholder="••••••••"
{...register("password")}
required
error={formState.errors.password?.message}
/>
<Button type="submit" className="w-xs h-10 mx-auto mt-6" disabled={formState.isLoading}>
{formState.isLoading
? __("Creating account...")
: __("Create account")}
</Button>
</form>
<div className="text-center">
<p className="text-sm text-txt-tertiary">
{__("Already have an account?")}
{" "}
<Link
to="/auth/login"
className="underline text-txt-primary hover:text-txt-secondary"
>
{__("Log in here")}
</Link>
</p>
<div className="text-center mt-6 text-sm text-txt-secondary">
<Link
to="/auth/login"
className="underline hover:text-txt-primary"
>
{__("Go back")}
</Link>
</div>
</div>
);