mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-28 12:30:58 +00:00
173 lines
5.0 KiB
TypeScript
173 lines
5.0 KiB
TypeScript
import { useState } from "react";
|
|
import { Mail, Loader2 } from "lucide-react";
|
|
import { useMutation } from "@tanstack/react-query";
|
|
import toast from "react-hot-toast";
|
|
|
|
import { api } from "@/services/api";
|
|
import { useAuth } from "@/auth/useAuth";
|
|
import {
|
|
Card,
|
|
CardContent,
|
|
CardDescription,
|
|
CardHeader,
|
|
CardTitle,
|
|
} from "@/components/ui/card";
|
|
import {
|
|
Button,
|
|
Dialog,
|
|
DialogContent,
|
|
DialogDescription,
|
|
DialogFooter,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
Input,
|
|
Label,
|
|
} from "@edr/ui-common";
|
|
|
|
/**
|
|
* Lets the signed-in backoffice user change their own email. Goes through
|
|
* /me/contact/otp + /me/contact rather than the generic (unverified)
|
|
* /auth/update-profile route, so the new address is proven before it's
|
|
* written — see account.controller.ts on the API side.
|
|
*/
|
|
export function ChangeEmailCard() {
|
|
const { user } = useAuth();
|
|
const sendOtpMutation = useMutation(
|
|
api.account.sendContactOtp.mutationOptions(),
|
|
);
|
|
const updateContactMutation = useMutation(
|
|
api.account.updateContact.mutationOptions(),
|
|
);
|
|
|
|
const [open, setOpen] = useState(false);
|
|
const [step, setStep] = useState<"enterEmail" | "enterOtp">("enterEmail");
|
|
const [newEmail, setNewEmail] = useState("");
|
|
const [otp, setOtp] = useState("");
|
|
const [formError, setFormError] = useState("");
|
|
|
|
const closeDialog = () => {
|
|
setOpen(false);
|
|
setStep("enterEmail");
|
|
setNewEmail("");
|
|
setOtp("");
|
|
setFormError("");
|
|
};
|
|
|
|
const sendOtp = () => {
|
|
setFormError("");
|
|
if (!newEmail.trim()) {
|
|
setFormError("Enter the new email address.");
|
|
return;
|
|
}
|
|
|
|
sendOtpMutation.mutate(
|
|
{ channel: "email", value: newEmail.trim() },
|
|
{
|
|
onSuccess: (result) => {
|
|
toast.success(`Verification code sent to ${result.sentTo}`);
|
|
setStep("enterOtp");
|
|
},
|
|
},
|
|
);
|
|
};
|
|
|
|
const confirmOtp = () => {
|
|
setFormError("");
|
|
if (!otp.trim()) {
|
|
setFormError("Enter the verification code.");
|
|
return;
|
|
}
|
|
|
|
updateContactMutation.mutate(
|
|
{ channel: "email", value: newEmail.trim(), otp: otp.trim() },
|
|
{
|
|
onSuccess: () => {
|
|
toast.success("Email updated.");
|
|
closeDialog();
|
|
// Refetches the session so the new email shows everywhere — simplest
|
|
// way to refresh the cached user without a dedicated context method.
|
|
window.location.reload();
|
|
},
|
|
},
|
|
);
|
|
};
|
|
|
|
return (
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center gap-2">
|
|
<Mail className="size-4" />
|
|
Email
|
|
</CardTitle>
|
|
<CardDescription>
|
|
{user?.email ? `Current email: ${user.email}` : "Change your account email."}
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<Button variant="outline" size="sm" onClick={() => setOpen(true)}>
|
|
Change email
|
|
</Button>
|
|
</CardContent>
|
|
|
|
<Dialog open={open} onOpenChange={(next) => !next && closeDialog()}>
|
|
<DialogContent className="sm:max-w-md">
|
|
<DialogHeader>
|
|
<DialogTitle>Change email</DialogTitle>
|
|
<DialogDescription>
|
|
{step === "enterEmail"
|
|
? "We'll send a verification code to the new address."
|
|
: `Enter the code sent to ${newEmail}.`}
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
|
|
{step === "enterEmail" ? (
|
|
<div className="space-y-2">
|
|
<Label htmlFor="newEmail">New email</Label>
|
|
<Input
|
|
id="newEmail"
|
|
type="email"
|
|
value={newEmail}
|
|
onChange={(e) => setNewEmail(e.target.value)}
|
|
/>
|
|
{formError && <p className="text-sm text-destructive">{formError}</p>}
|
|
</div>
|
|
) : (
|
|
<div className="space-y-2">
|
|
<Label htmlFor="otp">Verification code</Label>
|
|
<Input
|
|
id="otp"
|
|
value={otp}
|
|
onChange={(e) => setOtp(e.target.value)}
|
|
/>
|
|
{formError && <p className="text-sm text-destructive">{formError}</p>}
|
|
</div>
|
|
)}
|
|
|
|
<DialogFooter>
|
|
<Button variant="outline" onClick={closeDialog}>
|
|
Cancel
|
|
</Button>
|
|
{step === "enterEmail" ? (
|
|
<Button disabled={sendOtpMutation.isPending} onClick={sendOtp}>
|
|
{sendOtpMutation.isPending ? (
|
|
<Loader2 className="size-4 animate-spin" />
|
|
) : (
|
|
"Send code"
|
|
)}
|
|
</Button>
|
|
) : (
|
|
<Button disabled={updateContactMutation.isPending} onClick={confirmOtp}>
|
|
{updateContactMutation.isPending ? (
|
|
<Loader2 className="size-4 animate-spin" />
|
|
) : (
|
|
"Confirm"
|
|
)}
|
|
</Button>
|
|
)}
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
</Card>
|
|
);
|
|
}
|