change-email-form.tsx137 lines · main
1'use client';
2
3import { useState, useTransition, type FormEvent } from 'react';
4import { useRouter } from 'next/navigation';
5
6interface Props {
7 currentEmail: string;
8 apiOrigin: string;
9}
10
11export function ChangeEmailForm({ currentEmail, apiOrigin }: Props) {
12 const router = useRouter();
13 const [open, setOpen] = useState(false);
14 const [newEmail, setNewEmail] = useState('');
15 const [error, setError] = useState<string | null>(null);
16 const [sent, setSent] = useState<string | null>(null);
17 const [pending, startTransition] = useTransition();
18
19 function onSubmit(e: FormEvent<HTMLFormElement>) {
20 e.preventDefault();
21 const trimmed = newEmail.trim().toLowerCase();
22 if (!trimmed || trimmed === currentEmail.toLowerCase()) {
23 setError('Enter a different email address.');
24 return;
25 }
26 setError(null);
27 setSent(null);
28 startTransition(async () => {
29 try {
30 const callbackURL = `${window.location.origin}/dashboard/settings?email_changed=1`;
31 const res = await fetch(`${apiOrigin}/v1/auth/change-email`, {
32 method: 'POST',
33 headers: { 'content-type': 'application/json' },
34 credentials: 'include',
35 body: JSON.stringify({ newEmail: trimmed, callbackURL }),
36 });
37 if (!res.ok) {
38 const body = await res.text().catch(() => '');
39 let message = body;
40 try {
41 const parsed = JSON.parse(body) as { message?: string };
42 if (parsed.message) message = parsed.message;
43 } catch {
44 // not JSON
45 }
46 setError(message || `request failed (${res.status})`);
47 return;
48 }
49 setSent(trimmed);
50 setNewEmail('');
51 router.refresh();
52 } catch (err) {
53 setError(err instanceof Error ? err.message : 'change failed');
54 }
55 });
56 }
57
58 if (sent) {
59 return (
60 <div className="mt-3 rounded-md border border-[var(--color-border-primary)] bg-[var(--color-primary-subtle)] p-4 text-xs text-[var(--color-text)]">
61 <p className="font-medium">Confirmation sent to {currentEmail}.</p>
62 <p className="mt-1 text-[var(--color-text-muted)]">
63 Click the link in that mailbox to switch your sign-in email to <strong>{sent}</strong>.
64 The link expires in 1 hour. Until you confirm, you keep signing in with{' '}
65 <strong>{currentEmail}</strong>.
66 </p>
67 <p className="mt-2 text-[var(--color-text-subtle)]">
68 Outlook / Hotmail aggressively spam-filters transactional mail from new domains. If
69 you don&apos;t see it within 5 minutes, check Junk and mark the address as not junk.
70 </p>
71 </div>
72 );
73 }
74
75 if (!open) {
76 return (
77 <button
78 type="button"
79 onClick={() => setOpen(true)}
80 className="mt-3 text-xs text-[var(--color-text-link)] hover:underline"
81 >
82 Change sign-in email →
83 </button>
84 );
85 }
86
87 return (
88 <form
89 onSubmit={onSubmit}
90 className="mt-3 flex flex-col gap-2 rounded-md border border-[var(--color-border-subtle)] bg-[var(--color-surface-raised)] p-4"
91 >
92 <label className="flex flex-col gap-1.5">
93 <span className="font-mono text-[10px] uppercase tracking-wider text-[var(--color-text-subtle)]">
94 New sign-in email
95 </span>
96 <input
97 type="email"
98 required
99 value={newEmail}
100 onChange={(e) => setNewEmail(e.currentTarget.value)}
101 placeholder="you@example.com"
102 maxLength={320}
103 className="rounded-md border border-[var(--color-border)] bg-[var(--color-surface)] px-3 py-2 text-sm text-[var(--color-text)] outline-none focus:border-[var(--color-primary)]"
104 />
105 <span className="text-xs text-[var(--color-text-subtle)]">
106 For security, the confirmation link goes to your <strong>current</strong> email
107 ({currentEmail}). The change only takes effect after you click that link.
108 </span>
109 </label>
110 <div className="flex flex-wrap items-center gap-3 pt-1">
111 <button
112 type="submit"
113 disabled={pending}
114 className="rounded-md bg-[var(--color-primary)] px-3 py-1.5 text-xs font-medium text-[var(--color-text-inverse)] transition hover:bg-[var(--color-primary-hover)] disabled:opacity-40"
115 >
116 {pending ? 'Sending…' : 'Send confirmation'}
117 </button>
118 <button
119 type="button"
120 onClick={() => {
121 setOpen(false);
122 setNewEmail('');
123 setError(null);
124 }}
125 className="text-xs text-[var(--color-text-muted)] hover:text-[var(--color-text)]"
126 >
127 Cancel
128 </button>
129 {error ? (
130 <span role="alert" className="text-xs text-red-400">
131 {error}
132 </span>
133 ) : null}
134 </div>
135 </form>
136 );
137}