sparkletown/sparkle

View on GitHub
src/components/organisms/AuthenticationModal/RegisterForm/RegisterForm.tsx

Summary

Maintainability
F
3 days
Test Coverage
import React, { useState } from "react";
import { useForm } from "react-hook-form";
import { useHistory } from "react-router-dom";
import { differenceInYears, parseISO } from "date-fns";
import firebase from "firebase/app";

import {
  ACCOUNT_PROFILE_VENUE_PARAM_URL,
  DEFAULT_REQUIRES_DOB,
} from "settings";

import { checkIsCodeValid, checkIsEmailWhitelisted } from "api/auth";

import { errorCode, errorMessage, errorStatus } from "types/errors";
import { VenueAccessMode } from "types/VenueAcccess";

import { isTruthy } from "utils/types";
import { generateUrl } from "utils/url";

import { useSpaceParams } from "hooks/spaces/useSpaceParams";
import { useWorldAndSpaceBySlug } from "hooks/spaces/useWorldAndSpaceBySlug";
import { useAnalytics } from "hooks/useAnalytics";
import { useSocialSignIn } from "hooks/useSocialSignIn";

import { updateUserPrivate } from "pages/Account/helpers";

import { LoginFormData } from "components/organisms/AuthenticationModal/LoginForm/LoginForm";
import { TicketCodeField } from "components/organisms/TicketCodeField";

import { ButtonNG } from "components/atoms/ButtonNG";
import { ConfirmationModal } from "components/atoms/ConfirmationModal/ConfirmationModal";
import { NotFound } from "components/atoms/NotFound";

import fIcon from "assets/icons/facebook-social-icon.svg";
import gIcon from "assets/icons/google-social-icon.svg";

const validateDateOfBirth = (stringDate: string) => {
  const yearsDifference = differenceInYears(new Date(), parseISO(stringDate));
  return yearsDifference >= 18 && yearsDifference <= 100;
};

export interface RegisterFormInput {
  email: string;
  password: string;
  code: string;
  date_of_birth: string;
  backend?: string;
}

export interface RegisterData {
  date_of_birth: string;
}

export interface RegisterFormProps {
  displayLoginForm: () => void;
  displayPasswordResetForm: () => void;
  afterUserIsLoggedIn?: (data?: LoginFormData) => void;
  closeAuthenticationModal?: () => void;
}

const RegisterForm: React.FC<RegisterFormProps> = ({
  displayLoginForm,
  afterUserIsLoggedIn,
  closeAuthenticationModal,
}) => {
  const history = useHistory();

  const { worldSlug, spaceSlug } = useSpaceParams();
  const { world, space, spaceId, isLoaded } = useWorldAndSpaceBySlug(
    worldSlug,
    spaceSlug
  );

  const [showLoginModal, setShowLoginModal] = useState(false);
  const analytics = useAnalytics({ venue: space });

  const { signInWithGoogle, signInWithFacebook } = useSocialSignIn();

  const signUp = ({ email, password }: RegisterFormInput) => {
    return firebase.auth().createUserWithEmailAndPassword(email, password);
  };

  const {
    register,
    handleSubmit,
    errors,
    formState,
    setError,
    clearError,
    watch,
    getValues,
  } = useForm<RegisterFormInput & Record<string, string>>({
    mode: "onChange",
    reValidateMode: "onChange",
  });

  const clearBackendErrors = () => {
    clearError("backend");
  };

  if (!isLoaded) {
    return <>Loading...</>;
  }

  if (!space || !spaceId || !world) {
    return <NotFound />;
  }

  const checkVenueAccessLevels = async (data: RegisterFormInput) => {
    if (space.access === VenueAccessMode.Emails) {
      const isEmailWhitelisted = await checkIsEmailWhitelisted({
        venueId: spaceId,
        email: data.email,
      });

      if (!isEmailWhitelisted.data) {
        setError(
          "email",
          "validation",
          "We can't find you! Please use the email from your invitation."
        );
        return;
      }
    }

    if (space.access === VenueAccessMode.Codes) {
      const isCodeValid = await checkIsCodeValid({
        venueId: spaceId,
        code: data.code,
      });

      if (!isCodeValid.data) {
        setError(
          "code",
          "validation",
          "We can't find you! Please use the code from your invitation."
        );
        return;
      }
    }
  };

  const postRegisterCheck = (
    authResult: firebase.auth.UserCredential,
    data: RegisterFormInput
  ) => {
    if (authResult.user && isDobRequired) {
      updateUserPrivate(authResult.user.uid, {
        date_of_birth: data.date_of_birth,
      }).catch((e) => console.error(RegisterForm.name, e));
    }

    analytics.trackSignUpEvent(data.email);
    afterUserIsLoggedIn?.(data);

    closeAuthenticationModal?.();
  };

  const onSubmit = async (data: RegisterFormInput) => {
    try {
      setShowLoginModal(false);

      checkVenueAccessLevels(data).catch((e) =>
        console.error(RegisterForm.name, e)
      );

      const auth = await signUp(data);

      postRegisterCheck(auth, data);

      const profileUrl = generateUrl({
        route: ACCOUNT_PROFILE_VENUE_PARAM_URL,
        required: ["worldSlug", "spaceSlug"],
        params: { worldSlug, spaceSlug },
      });

      history.push(profileUrl);
    } catch (error) {
      const status = errorStatus(error);
      const message = errorMessage(error);
      const code = errorCode(error);

      if (code === "auth/email-already-in-use") {
        setShowLoginModal(true);
      }
      if (status === 404) {
        setError(
          "email",
          "validation",
          `Email ${data.email} does not have a ticket; get your ticket at ${space.ticketUrl}`
        );
      } else if (status >= 500) {
        setError("email", "validation", `Error checking ticket: ${message}`);
      } else {
        setError("backend", "firebase", message);
      }
    }
  };

  const handleGoogleSignIn = async () => {
    const { email, password, code, date_of_birth } = getValues();
    const formValues = { email, password, code, date_of_birth };
    checkVenueAccessLevels(formValues).catch((e) =>
      console.error(RegisterForm.name, e)
    );
    try {
      const auth = await signInWithGoogle();
      postRegisterCheck(auth, formValues);
    } catch (error) {
      setError("backend", "firebase", "Error");
    }
  };
  const handleFacebookSignIn = async () => {
    const { email, password, code, date_of_birth } = getValues();
    const formValues = { email, password, code, date_of_birth };
    checkVenueAccessLevels(formValues).catch((e) =>
      console.error(RegisterForm.name, e)
    );
    try {
      const auth = await signInWithFacebook();

      if (auth.message) {
        setError("backend", "firebase", "Error");

        return;
      }

      postRegisterCheck(auth, formValues);
    } catch {
      setError("backend", "firebase", "Error");
    }
  };

  const hasTermsAndConditions = isTruthy(space.termsAndConditions);
  const termsAndConditions = space.termsAndConditions;

  const signIn = async () => {
    const { email, password } = getValues();
    await firebase.auth().signInWithEmailAndPassword(email, password);
  };

  const isDobRequired =
    isLoaded && (world?.requiresDateOfBirth ?? DEFAULT_REQUIRES_DOB);

  return (
    <div className="form-container">
      {showLoginModal && (
        <ConfirmationModal
          header={"This account already exists."}
          message="Would you like to login with the same credentials?"
          onConfirm={signIn}
        />
      )}
      <div>
        <div className="register-form-title">Сreate your account</div>
      </div>
      {errors.backend && (
        <div className="auth-submit-error">
          <span className="auth-submit-error__message">
            Oops! Something went wrong. Please try again or use another method
            to create an account
          </span>
        </div>
      )}
      <form
        onSubmit={handleSubmit(onSubmit)}
        onChange={clearBackendErrors}
        className="form"
      >
        <div className="input-group">
          <input
            name="email"
            className="input-block input-centered auth-input"
            placeholder="Your email address"
            ref={register({ required: true })}
          />
          {errors.email && errors.email.type === "required" && (
            <span className="input-error">Email is required</span>
          )}
          {errors.email &&
            ["firebase", "validation"].includes(errors.email.type) && (
              <span className="input-error">{errors.email.message}</span>
            )}
        </div>

        <div className="input-group">
          <input
            name="password"
            className="input-block input-centered auth-input"
            type="password"
            placeholder="Password"
            ref={register({
              required: true,
              pattern: /^(?=.*[0-9])(?=.*[a-zA-Z]).{6,}$/,
            })}
          />

          <span
            className={`input-${
              errors.password && errors.password.type === "pattern"
                ? "error"
                : "info"
            }`}
          >
            Password must contain letters and numbers
          </span>

          {errors.password && errors.password.type === "required" && (
            <span className="input-error">Password is required</span>
          )}
        </div>

        {space.access === VenueAccessMode.Codes && (
          <TicketCodeField register={register} error={errors?.code} />
        )}

        {isDobRequired && (
          <div className="input-group">
            <input
              name="date_of_birth"
              className="input-block input-centered"
              type="date"
              ref={register({ required: true, validate: validateDateOfBirth })}
            />
            <small className="input-info">
              You need to be 18 years old to attend this event. Please confirm
              your age.
            </small>
            {errors?.date_of_birth && (
              <span className="input-error">
                {errors?.date_of_birth?.type === "required" && (
                  <>Date of birth is required</>
                )}
                {errors?.date_of_birth?.type === "validate" && (
                  <div>You need to be at least 18 years of age.</div>
                )}
              </span>
            )}
          </div>
        )}

        {hasTermsAndConditions &&
          termsAndConditions.map((term) => {
            const required = errors?.[term.name]?.type === "required";
            return (
              <div className="input-group" key={term.name}>
                <label
                  htmlFor={term.name}
                  className={`checkbox ${
                    watch(term.name) && "checkbox-checked"
                  }`}
                >
                  {term.link && (
                    <a
                      href={term.link}
                      target="_blank"
                      rel="noopener noreferrer"
                    >
                      {term.text}
                    </a>
                  )}
                  {!term.link && term.text}
                </label>
                <input
                  type="checkbox"
                  name={term.name}
                  id={term.name}
                  ref={register({
                    required: true,
                  })}
                />
                {required && <span className="input-error">Required</span>}
              </div>
            );
          })}
        <ButtonNG
          className="auth-input register"
          type="submit"
          variant="primary"
          disabled={!formState.isValid}
        >
          Create account
        </ButtonNG>
      </form>

      {world.hasSocialLoginEnabled && (
        <div className="social-auth-container">
          <span>or</span>
          <ButtonNG
            className="auth-input"
            type="submit"
            onClick={handleGoogleSignIn}
          >
            <div className="social-icon">
              <img src={gIcon} alt="asd" />
            </div>
            Sign up with Google
          </ButtonNG>
          <ButtonNG
            className="auth-input"
            type="submit"
            onClick={handleFacebookSignIn}
          >
            <div className="social-icon">
              <img src={fIcon} alt="asd" />
            </div>
            Sign up with Facebook
          </ButtonNG>
        </div>
      )}

      <div className="secondary-action">
        Already have an account?
        <br />
        <span className="link" onClick={displayLoginForm}>
          Login
        </span>
      </div>
    </div>
  );
};

export default RegisterForm;