diff --git a/llm-fe/src/App.tsx b/llm-fe/src/App.tsx
index a2e1b38..e6ca686 100644
--- a/llm-fe/src/App.tsx
+++ b/llm-fe/src/App.tsx
@@ -2,6 +2,9 @@ import React, { Component, useContext } from 'react';
import './App.css';
import { Routes, Route, Outlet, Navigate } from 'react-router-dom';
import SignIn from './llm-fe/pages/SignIn/SignIn';
+import SignUp from './llm-fe/pages/SignUp/SignUp';
+import BillingSuccess from './llm-fe/pages/BillingSuccess/BillingSuccess';
+import BillingCancel from './llm-fe/pages/BillingCancel/BillingCancel';
import PasswordReset from './llm-fe/pages/PasswordReset/PasswordReset';
import NotFound from './llm-fe/pages/NotFound/NotFound';
import { AuthContext } from './llm-fe/contexts/AuthContext';
@@ -46,6 +49,9 @@ class App extends Component {
} />
+
+
+
diff --git a/llm-fe/src/llm-fe/pages/BillingCancel/BillingCancel.tsx b/llm-fe/src/llm-fe/pages/BillingCancel/BillingCancel.tsx
new file mode 100644
index 0000000..2d51f2d
--- /dev/null
+++ b/llm-fe/src/llm-fe/pages/BillingCancel/BillingCancel.tsx
@@ -0,0 +1,104 @@
+import React from 'react';
+import { Link, useNavigate } from 'react-router-dom';
+import styled from 'styled-components';
+import ParticleBackground from '../../components/ParticleBackground/ParticleBackground';
+
+const PageContainer = styled.div`
+ position: relative;
+ width: 100vw;
+ height: 100vh;
+ overflow: hidden;
+ display: flex;
+ flex-direction: column;
+ color: #fff;
+ font-family: 'Inter', sans-serif;
+`;
+
+const ContentWrapper = styled.div`
+ flex: 1;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ z-index: 5;
+ padding: 2rem;
+`;
+
+const GlassCard = styled.div`
+ background: rgba(0, 0, 0, 0.4);
+ backdrop-filter: blur(10px);
+ border: 1px solid rgba(255, 255, 255, 0.1);
+ border-radius: 1rem;
+ padding: 3rem;
+ width: 100%;
+ max-width: 450px;
+ box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.37);
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ text-align: center;
+`;
+
+const CardTitle = styled.h2`
+ font-size: 2rem;
+ margin-bottom: 1rem;
+ color: #fff;
+`;
+
+const BodyText = styled.p`
+ color: rgba(255, 255, 255, 0.75);
+ margin-bottom: 1.5rem;
+ line-height: 1.5;
+`;
+
+const PrimaryButton = styled.button`
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+ border: none;
+ border-radius: 0.5rem;
+ color: #fff;
+ padding: 1rem 1.5rem;
+ font-size: 1rem;
+ font-weight: 600;
+ cursor: pointer;
+ width: 100%;
+ margin-bottom: 1rem;
+
+ &:hover {
+ transform: translateY(-2px);
+ }
+`;
+
+const NavLink = styled(Link)`
+ color: rgba(255, 255, 255, 0.6);
+ font-size: 0.9rem;
+ text-decoration: none;
+
+ &:hover {
+ color: #fff;
+ text-decoration: underline;
+ }
+`;
+
+const BillingCancel = (): JSX.Element => {
+ const navigate = useNavigate();
+
+ return (
+
+
+
+
+ Checkout canceled
+
+ No charge was made. Your account may already exist — sign in and open billing again, or
+ return to sign up to retry Stripe Checkout.
+
+ navigate('/signup/')}>
+ Retry sign up
+
+ Back to sign in
+
+
+
+ );
+};
+
+export default BillingCancel;
diff --git a/llm-fe/src/llm-fe/pages/BillingSuccess/BillingSuccess.tsx b/llm-fe/src/llm-fe/pages/BillingSuccess/BillingSuccess.tsx
new file mode 100644
index 0000000..b07ea20
--- /dev/null
+++ b/llm-fe/src/llm-fe/pages/BillingSuccess/BillingSuccess.tsx
@@ -0,0 +1,124 @@
+import React, { useContext, useEffect, useState } from 'react';
+import { Link, useNavigate, useSearchParams } from 'react-router-dom';
+import styled from 'styled-components';
+import ParticleBackground from '../../components/ParticleBackground/ParticleBackground';
+import { AuthContext } from '../../contexts/AuthContext';
+
+const PageContainer = styled.div`
+ position: relative;
+ width: 100vw;
+ height: 100vh;
+ overflow: hidden;
+ display: flex;
+ flex-direction: column;
+ color: #fff;
+ font-family: 'Inter', sans-serif;
+`;
+
+const ContentWrapper = styled.div`
+ flex: 1;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ z-index: 5;
+ padding: 2rem;
+`;
+
+const GlassCard = styled.div`
+ background: rgba(0, 0, 0, 0.4);
+ backdrop-filter: blur(10px);
+ border: 1px solid rgba(255, 255, 255, 0.1);
+ border-radius: 1rem;
+ padding: 3rem;
+ width: 100%;
+ max-width: 450px;
+ box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.37);
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ text-align: center;
+`;
+
+const CardTitle = styled.h2`
+ font-size: 2rem;
+ margin-bottom: 1rem;
+ color: #fff;
+`;
+
+const BodyText = styled.p`
+ color: rgba(255, 255, 255, 0.75);
+ margin-bottom: 1.5rem;
+ line-height: 1.5;
+`;
+
+const PrimaryButton = styled.button`
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+ border: none;
+ border-radius: 0.5rem;
+ color: #fff;
+ padding: 1rem 1.5rem;
+ font-size: 1rem;
+ font-weight: 600;
+ cursor: pointer;
+ width: 100%;
+ margin-bottom: 1rem;
+
+ &:hover {
+ transform: translateY(-2px);
+ }
+`;
+
+const NavLink = styled(Link)`
+ color: rgba(255, 255, 255, 0.6);
+ font-size: 0.9rem;
+ text-decoration: none;
+
+ &:hover {
+ color: #fff;
+ text-decoration: underline;
+ }
+`;
+
+const BillingSuccess = (): JSX.Element => {
+ const { authenticated, loading } = useContext(AuthContext);
+ const navigate = useNavigate();
+ const [searchParams] = useSearchParams();
+ const [sessionId] = useState(() => searchParams.get('session_id') || '');
+
+ useEffect(() => {
+ if (!loading && authenticated) {
+ const timer = window.setTimeout(() => navigate('/'), 2500);
+ return () => window.clearTimeout(timer);
+ }
+ }, [authenticated, loading, navigate]);
+
+ return (
+
+
+
+
+ Payment received
+
+ Thanks — your subscription checkout completed
+ {sessionId ? ` (session ${sessionId.slice(0, 12)}…)` : ''}. You can continue into the app.
+
+ {authenticated ? (
+ navigate('/')}>
+ Continue to app
+
+ ) : (
+ <>
+ Sign in with the account you just created to get started.
+ navigate('/signin/')}>
+ Sign in
+
+ >
+ )}
+ Back to sign in
+
+
+
+ );
+};
+
+export default BillingSuccess;
diff --git a/llm-fe/src/llm-fe/pages/SignIn/SignIn.test.tsx b/llm-fe/src/llm-fe/pages/SignIn/SignIn.test.tsx
index dd4c2fd..9416664 100644
--- a/llm-fe/src/llm-fe/pages/SignIn/SignIn.test.tsx
+++ b/llm-fe/src/llm-fe/pages/SignIn/SignIn.test.tsx
@@ -51,9 +51,10 @@ describe('SignIn', () => {
localStorage.clear();
mockPost.mockReset();
mockGet.mockReset();
+ mockGet.mockResolvedValue({ data: { enable_account_registration: false } });
});
- it('renders sign-in form fields', () => {
+ it('renders sign-in form fields', async () => {
renderSignIn();
expect(screen.getByRole('heading', { name: 'Sign In' })).toBeInTheDocument();
@@ -61,6 +62,16 @@ describe('SignIn', () => {
expect(screen.getByPlaceholderText('Password')).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Sign In' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Forgot Password?' })).toBeInTheDocument();
+ await waitFor(() => {
+ expect(screen.queryByRole('button', { name: /Don't have an account/i })).not.toBeInTheDocument();
+ });
+ });
+
+ it('shows sign-up link when registration enabled', async () => {
+ mockGet.mockResolvedValue({ data: { enable_account_registration: true } });
+ renderSignIn();
+
+ expect(await screen.findByRole('button', { name: /Don't have an account\? Sign up/i })).toBeInTheDocument();
});
it('does not submit when required fields empty', async () => {
diff --git a/llm-fe/src/llm-fe/pages/SignIn/SignIn.tsx b/llm-fe/src/llm-fe/pages/SignIn/SignIn.tsx
index f3d22bc..533e917 100644
--- a/llm-fe/src/llm-fe/pages/SignIn/SignIn.tsx
+++ b/llm-fe/src/llm-fe/pages/SignIn/SignIn.tsx
@@ -1,5 +1,5 @@
import { Form, Formik, Field } from 'formik';
-import React, { useContext, useState } from 'react';
+import React, { useContext, useEffect, useState } from 'react';
import { applyAccessToken, axiosInstance } from '../../../axiosApi';
import { setTokens } from '../../auth/tokenStorage';
import { AuthContext } from '../../contexts/AuthContext';
@@ -126,6 +126,21 @@ const ForgotPasswordLink = styled.button`
}
`;
+const SignUpLink = styled.button`
+ background: none;
+ border: none;
+ color: rgba(255, 255, 255, 0.6);
+ margin-top: 0.75rem;
+ cursor: pointer;
+ font-size: 0.9rem;
+ transition: color 0.2s ease;
+
+ &:hover {
+ color: #fff;
+ text-decoration: underline;
+ }
+`;
+
export type SignInValues = {
email: string;
password: string;
@@ -141,6 +156,26 @@ const SignIn = (): JSX.Element => {
const { setAccount } = useContext(AccountContext);
const navigate = useNavigate();
const [errorMessage, setErrorMessage] = useState('');
+ const [registrationEnabled, setRegistrationEnabled] = useState(false);
+
+ useEffect(() => {
+ let cancelled = false;
+ (async () => {
+ try {
+ const response = await axiosInstance.get('/public/settings/');
+ if (!cancelled) {
+ setRegistrationEnabled(Boolean(response.data?.enable_account_registration));
+ }
+ } catch {
+ if (!cancelled) {
+ setRegistrationEnabled(false);
+ }
+ }
+ })();
+ return () => {
+ cancelled = true;
+ };
+ }, []);
const handleSignIn = async ({ email, password }: SignInValues): Promise => {
try {
@@ -223,6 +258,11 @@ const SignIn = (): JSX.Element => {
navigate('/password_reset')}>
Forgot Password?
+ {registrationEnabled && (
+ navigate('/signup/')}>
+ Don't have an account? Sign up
+
+ )}
diff --git a/llm-fe/src/llm-fe/pages/SignUp/SignUp.test.tsx b/llm-fe/src/llm-fe/pages/SignUp/SignUp.test.tsx
new file mode 100644
index 0000000..6e780f4
--- /dev/null
+++ b/llm-fe/src/llm-fe/pages/SignUp/SignUp.test.tsx
@@ -0,0 +1,146 @@
+import React from 'react';
+import { render, screen, waitFor } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import { MemoryRouter } from 'react-router-dom';
+import SignUp from './SignUp';
+import { AuthContext } from '../../contexts/AuthContext';
+import { AccountContext } from '../../contexts/AccountContext';
+
+jest.mock('../../components/ParticleBackground/ParticleBackground', () => () => null);
+
+const mockPost = jest.fn();
+const mockGet = jest.fn();
+const assignMock = jest.fn();
+
+jest.mock('../../../axiosApi', () => ({
+ axiosInstance: {
+ post: (...args: unknown[]) => mockPost(...args),
+ get: (...args: unknown[]) => mockGet(...args),
+ defaults: { headers: { common: {} as Record } },
+ },
+ applyAccessToken: jest.fn(),
+}));
+
+const renderSignUp = () => {
+ const setAuthentication = jest.fn();
+ const setNeedsNewPassword = jest.fn();
+ const setAccount = jest.fn();
+
+ render(
+
+
+
+
+
+
+
+ );
+
+ return { setAuthentication, setNeedsNewPassword, setAccount };
+};
+
+describe('SignUp', () => {
+ const originalLocation = window.location;
+
+ beforeEach(() => {
+ localStorage.clear();
+ mockPost.mockReset();
+ mockGet.mockReset();
+ assignMock.mockReset();
+ Object.defineProperty(window, 'location', {
+ configurable: true,
+ value: { ...originalLocation, assign: assignMock, origin: 'http://localhost' },
+ });
+ });
+
+ afterEach(() => {
+ Object.defineProperty(window, 'location', {
+ configurable: true,
+ value: originalLocation,
+ });
+ });
+
+ it('shows disabled message when registration flag is off', async () => {
+ mockGet.mockResolvedValue({ data: { enable_account_registration: false } });
+ renderSignUp();
+
+ expect(await screen.findByText(/not available right now/i)).toBeInTheDocument();
+ expect(screen.queryByPlaceholderText('Email Address')).not.toBeInTheDocument();
+ });
+
+ it('does not submit when required fields empty', async () => {
+ mockGet.mockResolvedValue({ data: { enable_account_registration: true } });
+ const user = userEvent.setup();
+ renderSignUp();
+
+ expect(await screen.findByPlaceholderText('Email Address')).toBeInTheDocument();
+ await user.click(screen.getByRole('button', { name: /Sign Up & Pay/i }));
+
+ await waitFor(() => {
+ expect(mockPost).not.toHaveBeenCalled();
+ });
+ });
+
+ it('registers then redirects to Stripe Checkout URL', async () => {
+ mockGet
+ .mockResolvedValueOnce({ data: { enable_account_registration: true } })
+ .mockResolvedValueOnce({
+ data: {
+ email: 'new@example.com',
+ first_name: 'New',
+ last_name: 'User',
+ is_company_manager: true,
+ has_signed_tos: false,
+ has_usable_password: true,
+ company: { id: 1, name: 'New Co', state: 'NA', zipcode: '00000', address: 'N/A' },
+ },
+ });
+ mockPost
+ .mockResolvedValueOnce({
+ data: { access: 'access-token', refresh: 'refresh-token', email: 'new@example.com' },
+ })
+ .mockResolvedValueOnce({
+ data: {
+ checkout_url: 'https://checkout.stripe.com/c/pay/cs_test_abc',
+ session_id: 'cs_test_abc',
+ },
+ });
+
+ const user = userEvent.setup();
+ const { setAuthentication } = renderSignUp();
+
+ await screen.findByPlaceholderText('Email Address');
+ await user.type(screen.getByPlaceholderText('Email Address'), 'new@example.com');
+ await user.type(screen.getByPlaceholderText('Password'), 'securepass1');
+ await user.type(screen.getByPlaceholderText('Confirm password'), 'securepass1');
+ await user.click(screen.getByRole('button', { name: /Sign Up & Pay/i }));
+
+ await waitFor(() => {
+ expect(mockPost).toHaveBeenCalledWith('/user/create/', {
+ email: 'new@example.com',
+ password: 'securepass1',
+ first_name: '',
+ last_name: '',
+ company_name: '',
+ });
+ });
+
+ await waitFor(() => {
+ expect(mockPost).toHaveBeenCalledWith('/finance/checkout/', {
+ success_url: 'http://localhost/billing/success?session_id={CHECKOUT_SESSION_ID}',
+ cancel_url: 'http://localhost/billing/cancel',
+ });
+ expect(assignMock).toHaveBeenCalledWith('https://checkout.stripe.com/c/pay/cs_test_abc');
+ expect(setAuthentication).toHaveBeenCalledWith(true);
+ expect(localStorage.getItem('access_token')).toBe('access-token');
+ });
+ });
+});
diff --git a/llm-fe/src/llm-fe/pages/SignUp/SignUp.tsx b/llm-fe/src/llm-fe/pages/SignUp/SignUp.tsx
new file mode 100644
index 0000000..8ed5665
--- /dev/null
+++ b/llm-fe/src/llm-fe/pages/SignUp/SignUp.tsx
@@ -0,0 +1,352 @@
+import { Form, Formik, Field } from 'formik';
+import React, { useContext, useEffect, useState } from 'react';
+import { applyAccessToken, axiosInstance } from '../../../axiosApi';
+import { setTokens } from '../../auth/tokenStorage';
+import { AuthContext } from '../../contexts/AuthContext';
+import { Link } from 'react-router-dom';
+import { AccountContext } from '../../contexts/AccountContext';
+import { AxiosResponse } from 'axios';
+import { Account, AccountType } from '../../data';
+import ParticleBackground from '../../components/ParticleBackground/ParticleBackground';
+import styled from 'styled-components';
+import * as Yup from 'yup';
+
+const PageContainer = styled.div`
+ position: relative;
+ width: 100vw;
+ height: 100vh;
+ overflow: hidden;
+ display: flex;
+ flex-direction: column;
+ color: #fff;
+ font-family: 'Inter', sans-serif;
+`;
+
+const ContentWrapper = styled.div`
+ flex: 1;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ z-index: 5;
+ padding: 2rem;
+`;
+
+const GlassCard = styled.div`
+ background: rgba(0, 0, 0, 0.4);
+ backdrop-filter: blur(10px);
+ border: 1px solid rgba(255, 255, 255, 0.1);
+ border-radius: 1rem;
+ padding: 3rem;
+ width: 100%;
+ max-width: 450px;
+ box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.37);
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+`;
+
+const CardTitle = styled.h2`
+ font-size: 2rem;
+ margin-bottom: 0.5rem;
+ color: #fff;
+ text-align: center;
+`;
+
+const CardSubtitle = styled.p`
+ color: rgba(255, 255, 255, 0.65);
+ text-align: center;
+ margin-bottom: 2rem;
+ font-size: 0.95rem;
+`;
+
+const StyledInput = styled.input`
+ width: 100%;
+ background: rgba(255, 255, 255, 0.05);
+ border: 1px solid rgba(255, 255, 255, 0.1);
+ border-radius: 0.5rem;
+ padding: 1rem;
+ color: #fff;
+ font-size: 1rem;
+ outline: none;
+ transition: all 0.3s ease;
+ margin-bottom: 1.5rem;
+
+ &:focus {
+ background: rgba(255, 255, 255, 0.1);
+ border-color: rgba(100, 149, 237, 0.5);
+ box-shadow: 0 0 10px rgba(100, 149, 237, 0.2);
+ }
+
+ &::placeholder {
+ color: rgba(255, 255, 255, 0.5);
+ }
+`;
+
+const StyledButton = styled.button`
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+ border: none;
+ border-radius: 0.5rem;
+ color: #fff;
+ padding: 1rem;
+ font-size: 1.1rem;
+ font-weight: 600;
+ cursor: pointer;
+ transition: transform 0.2s ease;
+ width: 100%;
+ margin-top: 1rem;
+
+ &:hover {
+ transform: translateY(-2px);
+ box-shadow: 0 4px 12px rgba(118, 75, 162, 0.4);
+ }
+
+ &:disabled {
+ opacity: 0.5;
+ cursor: not-allowed;
+ transform: none;
+ }
+`;
+
+const ErrorMessage = styled.div`
+ color: #ff6b6b;
+ margin-bottom: 1rem;
+ text-align: center;
+ background: rgba(255, 107, 107, 0.1);
+ padding: 0.5rem;
+ border-radius: 0.5rem;
+ width: 100%;
+`;
+
+const NavLink = styled(Link)`
+ background: none;
+ border: none;
+ color: rgba(255, 255, 255, 0.6);
+ margin-top: 1.5rem;
+ cursor: pointer;
+ font-size: 0.9rem;
+ transition: color 0.2s ease;
+ text-decoration: none;
+
+ &:hover {
+ color: #fff;
+ text-decoration: underline;
+ }
+`;
+
+const FieldError = styled.div`
+ color: #ff6b6b;
+ font-size: 0.8rem;
+ margin-top: -1.2rem;
+ margin-bottom: 1rem;
+`;
+
+export type SignUpValues = {
+ email: string;
+ password: string;
+ confirmPassword: string;
+ first_name: string;
+ last_name: string;
+ company_name: string;
+};
+
+const validationSchema = Yup.object().shape({
+ email: Yup.string().email('Invalid email').required('Required'),
+ password: Yup.string().min(8, 'Password must be at least 8 characters').required('Required'),
+ confirmPassword: Yup.string()
+ .oneOf([Yup.ref('password')], 'Passwords must match')
+ .required('Required'),
+ first_name: Yup.string(),
+ last_name: Yup.string(),
+ company_name: Yup.string(),
+});
+
+function checkoutReturnUrls(): { success_url: string; cancel_url: string } {
+ const origin = window.location.origin;
+ return {
+ success_url: `${origin}/billing/success?session_id={CHECKOUT_SESSION_ID}`,
+ cancel_url: `${origin}/billing/cancel`,
+ };
+}
+
+const SignUp = (): JSX.Element => {
+ const { setAuthentication, setNeedsNewPassword } = useContext(AuthContext);
+ const { setAccount } = useContext(AccountContext);
+ const [errorMessage, setErrorMessage] = useState('');
+ const [registrationEnabled, setRegistrationEnabled] = useState(null);
+
+ useEffect(() => {
+ let cancelled = false;
+ (async () => {
+ try {
+ const response = await axiosInstance.get('/public/settings/');
+ if (!cancelled) {
+ setRegistrationEnabled(Boolean(response.data?.enable_account_registration));
+ }
+ } catch {
+ if (!cancelled) {
+ setRegistrationEnabled(false);
+ }
+ }
+ })();
+ return () => {
+ cancelled = true;
+ };
+ }, []);
+
+ const loadAccount = async (): Promise => {
+ const get_user_response: AxiosResponse = await axiosInstance.get('/user/get/');
+ const account = new Account({
+ email: get_user_response.data.email,
+ first_name: get_user_response.data.first_name,
+ last_name: get_user_response.data.last_name,
+ is_company_manager: get_user_response.data.is_company_manager,
+ has_signed_tos: get_user_response.data.has_signed_tos,
+ company: {
+ id: get_user_response.data.company?.id,
+ name: get_user_response.data.company?.name,
+ state: get_user_response.data.company?.state,
+ zipcode: get_user_response.data.company?.zipcode,
+ address: get_user_response.data.company?.address,
+ },
+ });
+ setAccount(account);
+ setAuthentication(true);
+ setNeedsNewPassword(get_user_response.data.has_usable_password);
+ return account;
+ };
+
+ const handleSignUp = async (values: SignUpValues): Promise => {
+ setErrorMessage('');
+ try {
+ const registerResponse = await axiosInstance.post('/user/create/', {
+ email: values.email,
+ password: values.password,
+ first_name: values.first_name,
+ last_name: values.last_name,
+ company_name: values.company_name,
+ });
+
+ await setTokens(registerResponse.data.access, registerResponse.data.refresh);
+ applyAccessToken(registerResponse.data.access);
+ await loadAccount();
+
+ const { success_url, cancel_url } = checkoutReturnUrls();
+ const checkoutResponse = await axiosInstance.post('/finance/checkout/', {
+ success_url,
+ cancel_url,
+ });
+
+ const checkoutUrl = checkoutResponse.data?.checkout_url;
+ if (!checkoutUrl) {
+ setErrorMessage('Account created, but checkout could not be started. Try signing in.');
+ return;
+ }
+
+ window.location.assign(checkoutUrl);
+ } catch (error: unknown) {
+ console.log(error);
+ const axiosError = error as {
+ response?: { status?: number; data?: { detail?: string; email?: string[] } };
+ };
+ if (axiosError.response?.status === 403) {
+ setErrorMessage('Account registration is currently disabled.');
+ } else if (axiosError.response?.data?.email?.[0]) {
+ setErrorMessage(axiosError.response.data.email[0]);
+ } else if (axiosError.response?.data?.detail) {
+ setErrorMessage(String(axiosError.response.data.detail));
+ } else {
+ setErrorMessage('Could not create account. Try again.');
+ }
+ }
+ };
+
+ if (registrationEnabled === null) {
+ return (
+
+
+
+
+ Sign Up
+ Loading…
+
+
+
+ );
+ }
+
+ if (!registrationEnabled) {
+ return (
+
+
+
+
+ Sign Up
+ Self-serve registration is not available right now.
+ Already have an account? Sign in
+
+
+
+ );
+ }
+
+ return (
+
+
+
+
+ Sign Up
+ Create an account, then complete payment on Stripe.
+ {errorMessage && {errorMessage}}
+
+ {(formik) => (
+
+ )}
+
+ Already have an account? Sign in
+
+
+
+ );
+};
+
+export default SignUp;