Fix sign up and accept invite

Signed-off-by: Émile Ré <nemile.re@gmail.com>
This commit is contained in:
Émile Ré
2026-01-02 18:15:10 +01:00
committed by Bryan Frimin
parent c3d4573091
commit 3564bd5495
14 changed files with 516 additions and 117 deletions

View File

@@ -1,45 +1,51 @@
import { Link, useNavigate, useSearchParams } from "react-router";
import { Button, Field, useToast } from "@probo/ui";
import { useTranslate } from "@probo/i18n";
import { z } from "zod";
import { useFormWithSchema } from "/hooks/useFormWithSchema";
import { usePageTitle } from "@probo/hooks";
import { useEffect } from "react";
import { useTranslate } from "@probo/i18n";
import { Button, Field, useToast } from "@probo/ui";
import { Link, useNavigate, useSearchParams } from "react-router";
import z from "zod";
import { useFormWithSchema } from "/hooks/useFormWithSchema";
import { graphql } from "relay-runtime";
import { useMutation } from "react-relay";
const signUpFromIvitationMutation = graphql`
mutation SignUpFromInvitationPageMutation(
$input: SignUpFromInvitationInput!
) {
signUpFromInvitation(input: $input) {
identity {
id
}
}
}
`;
const schema = z.object({
fullName: z.string().min(2),
password: z.string().min(8),
fullName: z.string().min(2),
});
export default function SignupFromInvitationPage() {
type FormData = z.infer<typeof schema>;
export default function SignUpFromInvitationPage() {
const { __ } = useTranslate();
const navigate = useNavigate();
const { toast } = useToast();
const navigate = useNavigate();
const [searchParams] = useSearchParams();
const fullNameFromParams = searchParams.get("fullName") || "";
const { register, handleSubmit, formState, reset } = useFormWithSchema(
schema,
{
defaultValues: {
fullName: "",
password: "",
},
}
);
usePageTitle(__("Sign up"));
useEffect(() => {
const fullNameFromParams = searchParams.get("fullName") || "";
if (fullNameFromParams) {
reset({
fullName: fullNameFromParams,
password: "",
});
}
}, [searchParams, reset]);
const { register, handleSubmit, formState } = useFormWithSchema(schema, {
defaultValues: {
password: "",
fullName: fullNameFromParams,
},
});
const onSubmit = handleSubmit(async (data) => {
const [signUpFromInvitation] = useMutation(signUpFromIvitationMutation);
const onSubmit = (data: FormData) => {
const token = searchParams.get("token");
if (!token) {
toast({
title: __("Signup failed"),
@@ -49,40 +55,33 @@ export default function SignupFromInvitationPage() {
return;
}
const response = await fetch("/connect/signup-from-invitation", {
method: "POST",
headers: {
"Content-Type": "application/json",
signUpFromInvitation({
variables: {
input: {
token,
password: data.password,
fullName: data.fullName,
},
},
onCompleted: () => {
toast({
title: __("Success"),
description: __(
"Account created successfully. Please accept your invitation to join the organization.",
),
variant: "success",
});
navigate("/", { replace: true });
},
onError: (errorData) => {
toast({
title: __("Signup failed"),
description: errorData.message || __("Signup failed"),
variant: "error",
});
},
credentials: "include",
body: JSON.stringify({
token: token,
password: data.password,
fullName: data.fullName,
}),
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
toast({
title: __("Signup failed"),
description: errorData.message || __("Signup failed"),
variant: "error",
});
return;
}
toast({
title: __("Success"),
description: __(
"Account created successfully. Please accept your invitation to join the organization."
),
variant: "success",
});
navigate("/", { replace: true });
});
usePageTitle(__("Create your account"));
};
return (
<div className="space-y-6 w-full max-w-md mx-auto">
@@ -93,7 +92,7 @@ export default function SignupFromInvitationPage() {
</p>
</div>
<form onSubmit={onSubmit} className="space-y-4">
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
<Field
label={__("Full Name")}
type="text"

View File

@@ -1,9 +1,21 @@
import { Link, useNavigate } from "react-router";
import { Button, Field, useToast } from "@probo/ui";
import { useTranslate } from "@probo/i18n";
import { z } from "zod";
import { useFormWithSchema } from "/hooks/useFormWithSchema";
import { usePageTitle } from "@probo/hooks";
import { useTranslate } from "@probo/i18n";
import { Button, Field, useToast } from "@probo/ui";
import { Link, useNavigate } from "react-router";
import z from "zod";
import { useFormWithSchema } from "/hooks/useFormWithSchema";
import { graphql } from "relay-runtime";
import { useMutation } from "react-relay";
const signUpMutation = graphql`
mutation SignUpPageMutation($input: SignUpInput!) {
signUp(input: $input) {
identity {
id
}
}
}
`;
const schema = z.object({
email: z.string().email(),
@@ -11,10 +23,15 @@ const schema = z.object({
fullName: z.string().min(2),
});
export default function RegisterPage() {
type FormData = z.infer<typeof schema>;
export default function SignUpPage() {
const { __ } = useTranslate();
const navigate = useNavigate();
const { toast } = useToast();
const navigate = useNavigate();
usePageTitle(__("Sign up"));
const { register, handleSubmit, formState } = useFormWithSchema(schema, {
defaultValues: {
email: "",
@@ -23,36 +40,32 @@ export default function RegisterPage() {
},
});
const onSubmit = handleSubmit(async (data) => {
const response = await fetch("/connect/register", {
method: "POST",
headers: {
"Content-Type": "application/json",
const [signUp] = useMutation(signUpMutation);
const onSubmit = (data: FormData) => {
signUp({
variables: {
email: data.email,
password: data.password,
fullName: data.fullName,
},
onCompleted: () => {
toast({
title: __("Success"),
description: __("Account created successfully"),
variant: "success",
});
navigate("/", { replace: true });
},
onError: (errorData) => {
toast({
title: __("Registration failed"),
description: errorData.message || __("Registration failed"),
variant: "error",
});
},
credentials: "include",
body: JSON.stringify(data),
});
// Registration failed
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
toast({
title: __("Registration failed"),
description: errorData.message || __("Registration failed"),
variant: "error",
});
return;
}
toast({
title: __("Success"),
description: __("Account created successfully"),
variant: "success",
});
navigate("/", { replace: true });
});
usePageTitle(__("Sign up"));
};
return (
<div className="space-y-6 w-full max-w-md mx-auto">
@@ -63,7 +76,7 @@ export default function RegisterPage() {
</p>
</div>
<form onSubmit={onSubmit} className="space-y-4">
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
<Field
label={__("Full Name")}
type="text"

View File

@@ -11,6 +11,7 @@ import { useMemo, useState } from "react";
import { InvitationCard } from "./_components/InvitationCard";
import { MembershipCard } from "./_components/MembershipCard";
import type { MembershipsPageQuery } from "/__generated__/iam/MembershipsPageQuery.graphql";
import { usePageTitle } from "@probo/hooks";
export const membershipsPageQuery = graphql`
query MembershipsPageQuery {
@@ -48,6 +49,8 @@ export function MembershipsPage(props: {
const { __ } = useTranslate();
const [search, setSearch] = useState("");
usePageTitle(__("Select an organization"));
const { queryRef } = props;
const {
viewer: {
@@ -78,12 +81,7 @@ export function MembershipsPage(props: {
{__("Pending invitations")}
</h2>
{invitations.map(({ node }) => (
<InvitationCard
key={node.id}
fKey={node}
// onAccept={handleAcceptInvitation}
// isAccepting={isAccepting}
/>
<InvitationCard key={node.id} fKey={node} />
))}
</div>
)}

View File

@@ -1,9 +1,10 @@
import { formatDate } from "@probo/helpers";
import { useTranslate } from "@probo/i18n";
import { Card } from "@probo/ui";
import { Button, Card } from "@probo/ui";
import { graphql } from "relay-runtime";
import { useFragment } from "react-relay";
import { useFragment, useMutation } from "react-relay";
import type { InvitationCardFragment$key } from "/__generated__/iam/InvitationCardFragment.graphql";
import { useNavigate } from "react-router";
const fragment = graphql`
fragment InvitationCardFragment on Invitation {
@@ -17,19 +18,49 @@ const fragment = graphql`
}
`;
const acceptMutation = graphql`
mutation InvitationCardMutation($input: AcceptInvitationInput!) {
acceptInvitation(input: $input) {
membershipEdge {
node {
id
}
}
}
}
`;
interface InvitationCardProps {
fKey: InvitationCardFragment$key;
// onAccept: (invitationId: string, organizationId: string) => void;
// isAccepting: boolean;
}
export function InvitationCard(props: InvitationCardProps) {
const { fKey } = props;
const navigate = useNavigate();
const { __ } = useTranslate();
const invitation = useFragment<InvitationCardFragment$key>(fragment, fKey);
const [acceptInvitation, isAccepting] = useMutation(acceptMutation);
const handleAccept = () => {
acceptInvitation({
variables: {
input: {
invitationId: invitation.id,
},
},
onCompleted: () => {
navigate(`/organizations/${invitation.organization.id}`);
},
onError: (err) => {
console.error("Failed to accept invitation:", err);
alert(__("Failed to accept invitation"));
},
});
};
return (
<Card padded className="w-full">
<div className="flex items-start justify-between gap-4">
@@ -44,12 +75,9 @@ export function InvitationCard(props: InvitationCardProps) {
{__("Invited on")} {formatDate(invitation.createdAt)}
</p>
</div>
{/* <Button
onClick={() => onAccept(invitation.id, invitation.organization.id)}
disabled={isAccepting}
>
<Button onClick={handleAccept} disabled={isAccepting}>
{isAccepting ? __("Accepting...") : __("Accept invitation")}
</Button> */}
</Button>
</div>
</Card>
);