Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
sagemathinc
GitHub Repository: sagemathinc/cocalc
Path: blob/master/src/packages/frontend/account/settings/email-address-setting.tsx
6055 views
1
/*
2
* This file is part of CoCalc: Copyright © 2026 Sagemath, Inc.
3
* License: MS-RSL – see LICENSE.md for details
4
*/
5
6
import { Button, Input, Modal, Space } from "antd";
7
import { useState } from "react";
8
import { defineMessage, FormattedMessage, useIntl } from "react-intl";
9
10
import { alert_message } from "@cocalc/frontend/alerts";
11
import { ErrorDisplay, LabeledRow, Saving } from "@cocalc/frontend/components";
12
import { labels } from "@cocalc/frontend/i18n";
13
import { log } from "@cocalc/frontend/user-tracking";
14
import { webapp_client } from "@cocalc/frontend/webapp-client";
15
import { COLORS } from "@cocalc/util/theme";
16
import { MIN_PASSWORD_LENGTH } from "@cocalc/util/auth";
17
18
const sendWelcomeEmailError = defineMessage({
19
id: "account.settings.email_address.send_welcome_email_error",
20
defaultMessage: "Problem sending welcome email: {error}",
21
description:
22
"Error shown when the welcome or verification email could not be sent.",
23
});
24
25
const passwordTooShortError = defineMessage({
26
id: "account.settings.email_address.password_too_short_error",
27
defaultMessage: "Password must be at least {length} characters long.",
28
description: "Shown when the entered password is too short.",
29
});
30
31
interface Props {
32
email_address?: string;
33
disabled?: boolean;
34
is_anonymous?: boolean;
35
verify_emails?: boolean;
36
}
37
38
export const EmailAddressSetting = ({
39
email_address: email_address0,
40
disabled,
41
is_anonymous,
42
verify_emails,
43
}: Props) => {
44
const intl = useIntl();
45
const [state, setState] = useState<"view" | "edit" | "saving">("view");
46
const [password, setPassword] = useState<string>("");
47
const [email_address, set_email_address] = useState<string>(
48
email_address0 ?? "",
49
);
50
const [error, setError] = useState<string>("");
51
const savedEmailAddress = email_address0 ?? "";
52
53
function start_editing() {
54
setState("edit");
55
set_email_address(savedEmailAddress);
56
setError("");
57
setPassword("");
58
}
59
60
function cancel_editing() {
61
setState("view");
62
set_email_address(savedEmailAddress);
63
setPassword("");
64
}
65
66
async function save_editing(): Promise<void> {
67
if (password.length < MIN_PASSWORD_LENGTH) {
68
setState("edit");
69
setError(
70
intl.formatMessage(passwordTooShortError, {
71
length: MIN_PASSWORD_LENGTH,
72
}),
73
);
74
return;
75
}
76
setState("saving");
77
try {
78
await webapp_client.account_client.change_email(email_address, password);
79
} catch (error) {
80
setState("edit");
81
setError(`Error -- ${error}`);
82
return;
83
}
84
if (is_anonymous) {
85
log("email_sign_up", { source: "anonymous_account" });
86
}
87
setState("view");
88
setError("");
89
setPassword("");
90
// if email verification is enabled, send out a token
91
// in any case, send a welcome email to an anonymous user, possibly
92
// including an email verification link
93
if (!(verify_emails || is_anonymous)) {
94
return;
95
}
96
try {
97
// Do a round of UI refresh, because maybe the "verify email" dialog will pop up
98
await new Promise((resolve) => {
99
setTimeout(resolve, 0);
100
});
101
// anonymous users will get the "welcome" email
102
await webapp_client.account_client.send_verification_email(!is_anonymous);
103
} catch (error) {
104
const err_msg = intl.formatMessage(sendWelcomeEmailError, {
105
error: String(error),
106
});
107
console.log(err_msg);
108
alert_message({ type: "error", message: err_msg });
109
}
110
}
111
112
function is_submittable(): boolean {
113
return !!(password !== "" && email_address !== savedEmailAddress);
114
}
115
116
function render_error() {
117
if (error) {
118
return (
119
<ErrorDisplay
120
error={error}
121
onClose={() => setError("")}
122
style={{ marginTop: "15px" }}
123
/>
124
);
125
}
126
}
127
128
function render_edit() {
129
const password_label = intl.formatMessage(
130
{
131
id: "account.settings.email_address.password_label",
132
defaultMessage:
133
"{have_email, select, true {Current password} other {Choose a password}}",
134
},
135
{
136
have_email: savedEmailAddress !== "",
137
},
138
);
139
return (
140
<Modal
141
closable={state !== "saving"}
142
footer={null}
143
onCancel={state !== "saving" ? cancel_editing : undefined}
144
open={state !== "view"}
145
title={button_label()}
146
width={520}
147
>
148
<div style={{ marginBottom: "15px" }}>
149
<FormattedMessage
150
id="account.settings.email_address.new_email_address_label"
151
defaultMessage="New email address"
152
/>
153
<Input
154
autoFocus
155
placeholder="[email protected]"
156
value={email_address}
157
onChange={(e) => {
158
set_email_address(e.target.value);
159
}}
160
maxLength={254}
161
/>
162
</div>
163
{password_label}
164
<Input.Password
165
value={password}
166
placeholder={password_label}
167
onChange={(e) => {
168
const pw = e.target.value;
169
if (pw != null) {
170
setPassword(pw);
171
}
172
}}
173
onPressEnter={() => {
174
if (is_submittable()) {
175
return save_editing();
176
}
177
}}
178
/>
179
<Space style={{ marginTop: "15px" }}>
180
<Button onClick={cancel_editing} disabled={state === "saving"}>
181
{intl.formatMessage(labels.cancel)}
182
</Button>
183
<Button
184
disabled={!is_submittable() || state === "saving"}
185
loading={state === "saving"}
186
onClick={save_editing}
187
type="primary"
188
>
189
{button_label()}
190
</Button>
191
{render_saving()}
192
</Space>
193
{render_error()}
194
</Modal>
195
);
196
}
197
198
function render_saving() {
199
if (state === "saving") {
200
return <Saving />;
201
}
202
}
203
204
function button_label(): string {
205
return intl.formatMessage(
206
{
207
id: "account.settings.email_address.button_label",
208
defaultMessage: `{type, select,
209
anonymous {Sign up using an email address and password}
210
have_email {Change email address}
211
other {Set email address and password}}`,
212
},
213
{
214
type: is_anonymous
215
? "anonymous"
216
: savedEmailAddress
217
? "have_email"
218
: "",
219
},
220
);
221
}
222
223
const label = is_anonymous ? (
224
<h5 style={{ color: COLORS.GRAY_M }}>
225
Sign up using an email address and password
226
</h5>
227
) : (
228
intl.formatMessage(labels.email_address)
229
);
230
231
return (
232
<LabeledRow
233
label={label}
234
style={disabled ? { color: COLORS.GRAY_M } : undefined}
235
>
236
<div>
237
{savedEmailAddress}
238
{state === "view" ? (
239
<Button
240
disabled={disabled}
241
className="pull-right"
242
onClick={start_editing}
243
>
244
{button_label()}...
245
</Button>
246
) : undefined}
247
</div>
248
{state !== "view" ? render_edit() : undefined}
249
</LabeledRow>
250
);
251
};
252
253