feat(dashboard-v2): implement account registration flow

This commit is contained in:
Michał Leszczyk 2022-03-23 14:32:35 +01:00
parent de7da6f56b
commit 202450e9d8
No known key found for this signature in database
GPG Key ID: FA123CA8BAA2FBF4
6 changed files with 265 additions and 23 deletions

View File

@ -26,7 +26,7 @@ module.exports = {
app.use(
"/api/",
createProxyMiddleware({
target: "https://account.siasky.net",
target: "https://account.skynetpro.net",
secure: false, // Do not reject self-signed certificates.
changeOrigin: true,
})

View File

@ -0,0 +1,109 @@
import PropTypes from "prop-types";
import { Formik, Form } from "formik";
import * as Yup from "yup";
import { TextField } from "../Form/TextField";
import { Button } from "../Button";
import accountsService from "../../services/accountsService";
// TODO: better password strength validation
const registrationSchema = Yup.object().shape({
email: Yup.string().required("Email is required").email("Please provide a valid email address"),
password: Yup.string().required("Password is required").min(6, "Password has to be at least 6 characters long"),
confirmPassword: Yup.string().oneOf([Yup.ref("password"), null], "Passwords must match"),
});
const USER_EXISTS_ERROR = "identity already belongs to an existing user";
export const SignUpForm = ({ onSuccess, onFailure }) => (
<Formik
initialValues={{
email: "",
password: "",
confirmPassword: "",
}}
validationSchema={registrationSchema}
onSubmit={async ({ email, password }, { setErrors }) => {
try {
await accountsService.post("user", {
json: {
email,
password,
},
});
onSuccess();
} catch (err) {
let isFormErrorSet = false;
if (err.response) {
const data = await err.response.json();
// If it's a user error, let's capture it and handle within the form.
if (USER_EXISTS_ERROR === data.message) {
setErrors({ email: "This email address already in use." });
isFormErrorSet = true;
}
}
if (!isFormErrorSet) {
onFailure();
}
}
}}
>
{({ errors, touched }) => (
<Form className="flex flex-col gap-4">
<div className="mt-4 mb-8">
<h3>Create your free account</h3>
<p className="font-light font-sans text-lg">Includes 100 GB storage at basic speed</p>
</div>
<TextField
type="text"
id="email"
name="email"
label="Email address"
error={errors.email}
touched={touched.email}
/>
<TextField
type="password"
id="password"
name="password"
label="Password"
error={errors.password}
touched={touched.password}
/>
<div className="text-xs text-palette-400">
<ul>
<li>At least 6 characters long</li>
<li>Significantly different from the email</li>
</ul>
</div>
<TextField
type="password"
id="confirmPassword"
name="confirmPassword"
label="Confirm password"
error={errors.confirmPassword}
touched={touched.confirmPassword}
/>
<div className="flex w-full justify-center mt-4">
<Button type="submit" className="px-12" $primary>
Sign up!
</Button>
</div>
</Form>
)}
</Formik>
);
SignUpForm.propTypes = {
onFailure: PropTypes.func.isRequired,
onSuccess: PropTypes.func.isRequired,
};

View File

@ -3,7 +3,7 @@ import styled from "styled-components";
import { SWRConfig } from "swr";
import { UserProvider } from "../contexts/user";
import { guestsOnly } from "../lib/swrConfig";
import { guestsOnly, allUsers } from "../lib/swrConfig";
const Layout = styled.div.attrs({
className: "h-screen w-screen bg-black flex",
@ -21,25 +21,30 @@ const Content = styled.div.attrs({
className: "w-full md:w-5/12 md:max-w-[680px] shrink-0",
})``;
const AuthLayout = ({ children }) => {
return (
<>
<SWRConfig value={guestsOnly}>
<UserProvider>
<Layout>
<SloganContainer className="pl-20 pr-20 lg:pr-30 xl:pr-40">
<div className="">
<h1 className="text-4xl lg:text-5xl xl:text-6xl text-white">
The decentralized <span className="text-primary">revolution</span> starts with decentralized storage
</h1>
</div>
</SloganContainer>
<Content>{children}</Content>
</Layout>
</UserProvider>
</SWRConfig>
</>
);
};
const AuthLayout =
(swrConfig) =>
({ children }) => {
return (
<>
<SWRConfig value={swrConfig}>
<UserProvider>
<Layout>
<SloganContainer className="pl-20 pr-20 lg:pr-30 xl:pr-40">
<div className="">
<h1 className="text-4xl lg:text-5xl xl:text-6xl text-white">
The decentralized <span className="text-primary">revolution</span> starts with decentralized storage
</h1>
</div>
</SloganContainer>
<Content>{children}</Content>
</Layout>
</UserProvider>
</SWRConfig>
</>
);
};
export default AuthLayout;
// Some pages (e.g. email confirmation) need to be accessible to both logged-in and guest users.
export const AllUsersAuthLayout = AuthLayout(allUsers);
export default AuthLayout(guestsOnly);

View File

@ -23,6 +23,10 @@ const redirectAuthenticated = (key) =>
return response.json();
});
export const allUsers = {
fetcher: (key) => fetch(`${baseUrl}/${key}`).then((response) => response.json()),
};
export const authenticatedOnly = {
fetcher: redirectUnauthenticated,
};

View File

@ -0,0 +1,56 @@
import { useEffect, useState } from "react";
import AuthLayout from "../../layouts/AuthLayout";
import HighlightedLink from "../../components/HighlightedLink";
import { SignUpForm } from "../../components/forms/SignUpForm";
import { navigate } from "gatsby";
const State = {
Pure: "PURE",
Success: "SUCCESS",
Failure: "FAILURE",
};
const SignUpPage = () => {
const [state, setState] = useState(State.Pure);
useEffect(() => {
if (state === State.Success) {
const timer = setTimeout(() => navigate("/"), 3000);
return () => clearTimeout(timer);
}
}, [state]);
return (
<div className="bg-white px-8 py-10 md:py-32 lg:px-16 xl:px-28 h-screen">
<div className="mb-16">
<img src="/images/logo-black-text.svg" alt="Skynet" />
</div>
{state !== State.Success && (
<SignUpForm onSuccess={() => setState(State.Success)} onFailure={() => setState(State.Failure)} />
)}
{state === State.Success && (
<div className="text-center">
<p className="text-primary font-semibold">Please check your email inbox and confirm your email address.</p>
<p>You will be redirected to your dashboard shortly.</p>
<HighlightedLink to="/">Click here to go there now.</HighlightedLink>
</div>
)}
{state === State.Failure && (
<p className="text-error text-center">Something went wrong, please try again later.</p>
)}
<p className="text-sm text-center mt-8">
Already have an account? <HighlightedLink to="/auth/login">Sign in</HighlightedLink>
</p>
</div>
);
};
SignUpPage.Layout = AuthLayout;
export default SignUpPage;

View File

@ -0,0 +1,68 @@
import { useEffect, useState } from "react";
import { navigate } from "gatsby";
import { AllUsersAuthLayout } from "../../layouts/AuthLayout";
import HighlightedLink from "../../components/HighlightedLink";
import accountsService from "../../services/accountsService";
const State = {
Pure: "PURE",
Success: "SUCCESS",
Failure: "FAILURE",
};
const EmailConfirmationPage = ({ location }) => {
const query = new URLSearchParams(location.search);
const token = query.get("token");
const [state, setState] = useState(State.Pure);
useEffect(() => {
let timer;
async function confirm(token) {
try {
await accountsService.get("user/confirm", { searchParams: { token } });
timer = setTimeout(() => {
navigate("/");
}, 3000);
setState(State.Success);
} catch {
setState(State.Failure);
}
}
if (token) {
confirm(token);
}
return () => clearTimeout(timer);
}, [token]);
return (
<div className="bg-white px-8 py-10 md:py-32 lg:px-16 xl:px-28 h-screen">
<div className="mb-16">
<img src="/images/logo-black-text.svg" alt="Skynet" />
</div>
<div className="text-center">
{state === State.Pure && <p>Please wait while we verify your account...</p>}
{state === State.Success && (
<>
<p className="text-primary font-semibold">All done!</p>
<p>You will be redirected to your dashboard shortly.</p>
<HighlightedLink to="/">Redirect now.</HighlightedLink>
</>
)}
{state === State.Failure && <p className="text-error">Something went wrong, please try again later.</p>}
</div>
</div>
);
};
EmailConfirmationPage.Layout = AllUsersAuthLayout;
export default EmailConfirmationPage;