exercism/website

View on GitHub
app/javascript/components/insiders/InsidersStatus.tsx

Summary

Maintainability
A
45 mins
Test Coverage
import React, { useCallback, useEffect, useState } from 'react'
import { useMutation } from '@tanstack/react-query'
import currency from 'currency.js'
import { typecheck, redirectTo } from '@/utils'
import { sendRequest } from '@/utils/send-request'
import { GraphicalIcon } from '../common'
import { ExercismStripeElements } from '../donations/ExercismStripeElements'
import { StripeForm } from '../donations/StripeForm'
import { Modal } from '../modals'
import { CustomAmountInput } from '../donations/donation-form/CustomAmountInput'

const STATUS_DATA = {
  eligible: {
    text: "You're currently eligible for Insiders. Thank you for being part of Exercism. We're excited to continue with you on our journey!",
    css: '--already-insider',
  },

  ineligible: {
    text: 'Set up a recurring monthly donation of $10 or more to access Insiders',
    css: '--ineligible',
  },

  eligible_lifetime: {
    text: "We've given you lifetime access to Insiders. Thank you for being part of Exercism. We're excited to continue with you on our journey!",
    css: '--already-insider',
  },

  unset: {
    text: "We're currently calculating your Insiders status. This box will update once we've finished.",
    css: '--unset',
  },
}

type InsidersStatus = 'eligible' | 'eligible_lifetime' | 'unset' | 'ineligible'
export type InsidersStatusData = {
  status: InsidersStatus
  insidersStatusRequest: string
  activateInsiderLink: string
  userSignedIn: boolean
  captchaRequired: boolean
  recaptchaSiteKey: string
  links: {
    insiders: string
    paymentPending: string
  }
}

type Response = {
  handle: string
  insidersStatus: InsidersStatus
}

export default function Status({
  activateInsiderLink,
  captchaRequired,
  insidersStatusRequest,
  links,
  recaptchaSiteKey,
  status,
  userSignedIn,
}: InsidersStatusData): JSX.Element {
  const [insidersStatus, setInsidersStatus] = useState(status)
  const [stripeModalOpen, setStripeModalOpen] = useState(false)

  const handleSuccess = useCallback(() => {
    redirectTo(links.insiders)
  }, [links.insiders])

  const handleModalOpen = useCallback(() => {
    setStripeModalOpen(true)
  }, [])

  const { mutate: mutation } = useMutation<Response>(
    async () => {
      const { fetch } = sendRequest({
        endpoint: insidersStatusRequest,
        method: 'GET',
        body: null,
      })

      return fetch.then((json) => typecheck<Response>(json, 'user'))
    },
    {
      onSuccess: (elem) => setInsidersStatus(elem.insidersStatus),
    }
  )

  const { mutate: activateInsider } = useMutation(
    async () => {
      const { fetch } = sendRequest({
        endpoint: activateInsiderLink,
        method: 'PATCH',
        body: null,
      })

      return fetch
    },
    {
      onSuccess: (res) => redirectTo(res.links.redirectUrl),
    }
  )

  useEffect(() => {
    if (insidersStatus === 'unset') {
      mutation()
    }
  }, [insidersStatus, mutation])

  const eligible =
    insidersStatus === 'eligible' || insidersStatus === 'eligible_lifetime'

  const [amount, setAmount] = useState<currency>(currency(10))
  const [showError, setShowError] = useState(false)
  const invalidAmount = amount.value < 10 || isNaN(amount.value)
  const handleShowError = useCallback(() => {
    if (invalidAmount) {
      setShowError(true)
    } else setShowError(false)
  }, [invalidAmount])

  const handleAmountInputChange = useCallback((amount: currency) => {
    setAmount(amount)
  }, [])

  return (
    <div className="flex flex-col items-start">
      <div className={`c-insiders-prompt ${STATUS_DATA[insidersStatus].css}`}>
        {STATUS_DATA[insidersStatus].text}
      </div>

      {eligible ? (
        <button
          className="flex get-insiders-link grow"
          onClick={() => activateInsider()}
        >
          <span>Get access to Insiders</span>
          <GraphicalIcon icon="arrow-right" />
        </button>
      ) : (
        <>
          <button
            onClick={handleModalOpen}
            className="flex get-insiders-link grow mb-12 w-fill lg:w-auto"
          >
            <span>Donate to Exercism to access Insiders</span>
            <GraphicalIcon icon="arrow-right" />
          </button>

          <p className="text-p-base italic">
            Exercism is an independent, registered not-for-profit organisation
            (UK #11733062) with a tiny team. All donations are used to run and
            improve the platform.
          </p>
        </>
      )}

      <Modal
        onClose={() => setStripeModalOpen(false)}
        open={stripeModalOpen}
        theme="dark"
        cover={true}
        closeButton={true}
        ReactModalClassName="max-w-[660px]"
      >
        <div className="--modal-content-inner">
          <ModalHeader />
          <hr className="mb-20 border-borderColor5" />

          <div className="mb-24">
            <h3 className="mb-8 text-h6 font-semibold">
              1. Choose your monthly donation:
            </h3>
            <CustomAmountInput
              onChange={handleAmountInputChange}
              onBlur={handleShowError}
              placeholder="Specify amount"
              value={amount}
              selected={true}
              min="10"
              className="max-w-[150px]"
            />
            {showError && (
              <div className="c-alert mt-12 text-p-base flex flex-row items-center gap-8">
                <GraphicalIcon
                  icon="question-circle"
                  className="h-[24px] w-[24px] filter-warning"
                />
                Please note: The minimum donation amount for Insiders Access is
                $10.00. Thank you for your kind support!
              </div>
            )}
          </div>
          <h3 className="mb-16 text-h6 font-semibold">
            2. Choose your payment method:
          </h3>
          <ExercismStripeElements
            mode="subscription"
            amount={
              isNaN(amount.intValue) ? currency(0).intValue : amount.intValue
            }
          >
            <StripeForm
              confirmParamsReturnUrl={links.paymentPending}
              captchaRequired={captchaRequired}
              userSignedIn={userSignedIn}
              recaptchaSiteKey={recaptchaSiteKey}
              amount={isNaN(amount.value) ? currency(0) : amount}
              onSuccess={handleSuccess}
              submitButtonDisabled={invalidAmount}
              paymentIntentType="subscription"
            />
          </ExercismStripeElements>
          <ModalFooter />
        </div>
      </Modal>
    </div>
  )
}

function ModalHeader(): JSX.Element {
  return (
    <>
      <div className="flex flex-row items-center gap-32 mb-12">
        <div>
          <h2 className="text-h2 mb-2 !text-white">Thank you!</h2>
          <p className="text-p-large !text-white">
            Thank you so much for supporting Exercism. It means the world to us!
            💜
          </p>
        </div>
        <GraphicalIcon
          icon="confetti-without-background"
          category="graphics"
          className="w-[96px] h-[96px]"
        />
      </div>
      <p className="text-p-base !text-white mb-20">
        Please use the form below to set up your monthly donation. You can amend
        or cancel your donation at any time in your settings page.
      </p>
    </>
  )
}

function ModalFooter(): JSX.Element {
  return (
    <p className="text-p-small mt-20">
      Exercism is an independent not-for-profit organisation. All donations are
      used to run and improve the platform. All payments are securely handled by
      Stripe.
    </p>
  )
}