SignUp.tsx 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316
  1. import log from "@/next/log";
  2. import { sendOtt } from "@ente/accounts/api/user";
  3. import { PasswordStrengthHint } from "@ente/accounts/components/PasswordStrength";
  4. import { PAGES } from "@ente/accounts/constants/pages";
  5. import { isWeakPassword } from "@ente/accounts/utils";
  6. import { generateKeyAndSRPAttributes } from "@ente/accounts/utils/srp";
  7. import { APPS } from "@ente/shared/apps/constants";
  8. import { VerticallyCentered } from "@ente/shared/components//Container";
  9. import FormPaperFooter from "@ente/shared/components/Form/FormPaper/Footer";
  10. import FormPaperTitle from "@ente/shared/components/Form/FormPaper/Title";
  11. import ShowHidePassword from "@ente/shared/components/Form/ShowHidePassword";
  12. import LinkButton from "@ente/shared/components/LinkButton";
  13. import SubmitButton from "@ente/shared/components/SubmitButton";
  14. import {
  15. generateAndSaveIntermediateKeyAttributes,
  16. saveKeyInSessionStore,
  17. } from "@ente/shared/crypto/helpers";
  18. import { LS_KEYS, setData } from "@ente/shared/storage/localStorage";
  19. import {
  20. setJustSignedUp,
  21. setLocalReferralSource,
  22. } from "@ente/shared/storage/localStorage/helpers";
  23. import { SESSION_KEYS } from "@ente/shared/storage/sessionStorage";
  24. import InfoOutlined from "@mui/icons-material/InfoOutlined";
  25. import {
  26. Box,
  27. Checkbox,
  28. FormControlLabel,
  29. FormGroup,
  30. IconButton,
  31. InputAdornment,
  32. Link,
  33. TextField,
  34. Tooltip,
  35. Typography,
  36. } from "@mui/material";
  37. import { Formik, type FormikHelpers } from "formik";
  38. import { t } from "i18next";
  39. import type { NextRouter } from "next/router";
  40. import React, { useState } from "react";
  41. import { Trans } from "react-i18next";
  42. import * as Yup from "yup";
  43. interface FormValues {
  44. email: string;
  45. passphrase: string;
  46. confirm: string;
  47. referral: string;
  48. }
  49. interface SignUpProps {
  50. router: NextRouter;
  51. login: () => void;
  52. appName: APPS;
  53. }
  54. export default function SignUp({ router, appName, login }: SignUpProps) {
  55. const [acceptTerms, setAcceptTerms] = useState(false);
  56. const [loading, setLoading] = useState(false);
  57. const [showPassword, setShowPassword] = useState(false);
  58. const handleClickShowPassword = () => {
  59. setShowPassword(!showPassword);
  60. };
  61. const handleMouseDownPassword = (
  62. event: React.MouseEvent<HTMLButtonElement>,
  63. ) => {
  64. event.preventDefault();
  65. };
  66. const registerUser = async (
  67. { email, passphrase, confirm, referral }: FormValues,
  68. { setFieldError }: FormikHelpers<FormValues>,
  69. ) => {
  70. try {
  71. if (passphrase !== confirm) {
  72. setFieldError("confirm", t("PASSPHRASE_MATCH_ERROR"));
  73. return;
  74. }
  75. setLoading(true);
  76. try {
  77. setData(LS_KEYS.USER, { email });
  78. setLocalReferralSource(referral);
  79. await sendOtt(appName, email);
  80. } catch (e) {
  81. const message = e instanceof Error ? e.message : "";
  82. setFieldError("confirm", `${t("UNKNOWN_ERROR")} ${message}`);
  83. throw e;
  84. }
  85. try {
  86. const { keyAttributes, masterKey, srpSetupAttributes } =
  87. await generateKeyAndSRPAttributes(passphrase);
  88. setData(LS_KEYS.ORIGINAL_KEY_ATTRIBUTES, keyAttributes);
  89. setData(LS_KEYS.SRP_SETUP_ATTRIBUTES, srpSetupAttributes);
  90. await generateAndSaveIntermediateKeyAttributes(
  91. passphrase,
  92. keyAttributes,
  93. masterKey,
  94. );
  95. await saveKeyInSessionStore(
  96. SESSION_KEYS.ENCRYPTION_KEY,
  97. masterKey,
  98. );
  99. setJustSignedUp(true);
  100. router.push(PAGES.VERIFY);
  101. } catch (e) {
  102. setFieldError("confirm", t("PASSWORD_GENERATION_FAILED"));
  103. throw e;
  104. }
  105. } catch (e) {
  106. log.error("signup failed", e);
  107. }
  108. setLoading(false);
  109. };
  110. return (
  111. <>
  112. <FormPaperTitle> {t("SIGN_UP")}</FormPaperTitle>
  113. <Formik<FormValues>
  114. initialValues={{
  115. email: "",
  116. passphrase: "",
  117. confirm: "",
  118. referral: "",
  119. }}
  120. validationSchema={Yup.object().shape({
  121. email: Yup.string()
  122. .email(t("EMAIL_ERROR"))
  123. .required(t("REQUIRED")),
  124. passphrase: Yup.string().required(t("REQUIRED")),
  125. confirm: Yup.string().required(t("REQUIRED")),
  126. })}
  127. validateOnChange={false}
  128. validateOnBlur={false}
  129. onSubmit={registerUser}
  130. >
  131. {({
  132. values,
  133. errors,
  134. handleChange,
  135. handleSubmit,
  136. }): JSX.Element => (
  137. <form noValidate onSubmit={handleSubmit}>
  138. <VerticallyCentered sx={{ mb: 1 }}>
  139. <TextField
  140. fullWidth
  141. id="email"
  142. name="email"
  143. autoComplete="username"
  144. type="email"
  145. label={t("ENTER_EMAIL")}
  146. value={values.email}
  147. onChange={handleChange("email")}
  148. error={Boolean(errors.email)}
  149. helperText={errors.email}
  150. autoFocus
  151. disabled={loading}
  152. />
  153. <TextField
  154. fullWidth
  155. id="password"
  156. name="password"
  157. autoComplete="new-password"
  158. type={showPassword ? "text" : "password"}
  159. label={t("PASSPHRASE_HINT")}
  160. value={values.passphrase}
  161. onChange={handleChange("passphrase")}
  162. error={Boolean(errors.passphrase)}
  163. helperText={errors.passphrase}
  164. disabled={loading}
  165. InputProps={{
  166. endAdornment: (
  167. <ShowHidePassword
  168. showPassword={showPassword}
  169. handleClickShowPassword={
  170. handleClickShowPassword
  171. }
  172. handleMouseDownPassword={
  173. handleMouseDownPassword
  174. }
  175. />
  176. ),
  177. }}
  178. />
  179. <TextField
  180. fullWidth
  181. id="confirm-password"
  182. name="confirm-password"
  183. autoComplete="new-password"
  184. type="password"
  185. label={t("CONFIRM_PASSPHRASE")}
  186. value={values.confirm}
  187. onChange={handleChange("confirm")}
  188. error={Boolean(errors.confirm)}
  189. helperText={errors.confirm}
  190. disabled={loading}
  191. />
  192. <PasswordStrengthHint
  193. password={values.passphrase}
  194. />
  195. <Box sx={{ width: "100%" }}>
  196. <Typography
  197. textAlign={"left"}
  198. color="text.secondary"
  199. mt={"24px"}
  200. >
  201. {t("REFERRAL_CODE_HINT")}
  202. </Typography>
  203. <TextField
  204. hiddenLabel
  205. InputProps={{
  206. endAdornment: (
  207. <InputAdornment position="end">
  208. <Tooltip
  209. title={t("REFERRAL_INFO")}
  210. >
  211. <IconButton
  212. tabIndex={-1}
  213. color="secondary"
  214. edge={"end"}
  215. >
  216. <InfoOutlined />
  217. </IconButton>
  218. </Tooltip>
  219. </InputAdornment>
  220. ),
  221. }}
  222. fullWidth
  223. name="referral"
  224. type="text"
  225. value={values.referral}
  226. onChange={handleChange("referral")}
  227. error={Boolean(errors.referral)}
  228. disabled={loading}
  229. />
  230. </Box>
  231. <FormGroup sx={{ width: "100%" }}>
  232. <FormControlLabel
  233. sx={{
  234. color: "text.muted",
  235. ml: 0,
  236. mt: 2,
  237. mb: 0,
  238. }}
  239. control={
  240. <Checkbox
  241. size="small"
  242. disabled={loading}
  243. checked={acceptTerms}
  244. onChange={(e) =>
  245. setAcceptTerms(e.target.checked)
  246. }
  247. color="accent"
  248. />
  249. }
  250. label={
  251. <Typography variant="small">
  252. <Trans
  253. i18nKey={"TERMS_AND_CONDITIONS"}
  254. components={{
  255. a: (
  256. <Link
  257. href="https://ente.io/terms"
  258. target="_blank"
  259. />
  260. ),
  261. b: (
  262. <Link
  263. href="https://ente.io/privacy"
  264. target="_blank"
  265. />
  266. ),
  267. }}
  268. />
  269. </Typography>
  270. }
  271. />
  272. </FormGroup>
  273. </VerticallyCentered>
  274. <Box mb={4}>
  275. <SubmitButton
  276. sx={{ my: 0 }}
  277. buttonText={t("CREATE_ACCOUNT")}
  278. loading={loading}
  279. disabled={
  280. !acceptTerms ||
  281. isWeakPassword(values.passphrase)
  282. }
  283. />
  284. {loading && (
  285. <Typography
  286. mt={1}
  287. textAlign={"center"}
  288. color="text.muted"
  289. variant="small"
  290. >
  291. {t("KEY_GENERATION_IN_PROGRESS_MESSAGE")}
  292. </Typography>
  293. )}
  294. </Box>
  295. </form>
  296. )}
  297. </Formik>
  298. <FormPaperFooter>
  299. <LinkButton onClick={login}>{t("ACCOUNT_EXISTS")}</LinkButton>
  300. </FormPaperFooter>
  301. </>
  302. );
  303. }