diff --git a/app/main.wasp b/app/main.wasp index 7ce6035..3f59068 100644 --- a/app/main.wasp +++ b/app/main.wasp @@ -87,8 +87,9 @@ entity User {=psl createdAt DateTime @default(now()) lastActiveTimestamp DateTime @default(now()) isAdmin Boolean @default(false) - hasAcceptedTos Boolean @default(true) - hasSubscribedToMarketingEmails Boolean @default(true) + hasAcceptedTos Boolean @default(false) + hasSubscribedToMarketingEmails Boolean @default(false) + isSignUpComplete Boolean @default(false) stripeId String? checkoutSessionId String? hasPaid Boolean @default(false) @@ -200,10 +201,10 @@ page LoginPage { component: import Login from "@src/client/auth/LoginPage" } -route SignupRoute { path: "/signup", to: SignupPage } -page SignupPage { - component: import { Signup } from "@src/client/auth/SignupPage" -} +// route SignupRoute { path: "/signup", to: SignupPage } +// page SignupPage { +// component: import { Signup } from "@src/client/auth/SignupPage" +// } route TocPageRoute { path: "/toc", to: TocPage } page TocPage { diff --git a/app/migrations/20240408050407_add_column_to_store_signup_complete_status/migration.sql b/app/migrations/20240408050407_add_column_to_store_signup_complete_status/migration.sql new file mode 100644 index 0000000..8762276 --- /dev/null +++ b/app/migrations/20240408050407_add_column_to_store_signup_complete_status/migration.sql @@ -0,0 +1,4 @@ +-- AlterTable +ALTER TABLE "User" ADD COLUMN "isSignUpComplete" BOOLEAN NOT NULL DEFAULT false, +ALTER COLUMN "hasAcceptedTos" SET DEFAULT false, +ALTER COLUMN "hasSubscribedToMarketingEmails" SET DEFAULT false; diff --git a/app/src/client/App.tsx b/app/src/client/App.tsx index e596311..7f4496e 100644 --- a/app/src/client/App.tsx +++ b/app/src/client/App.tsx @@ -4,7 +4,8 @@ import './Main.css'; import AppNavBar from './components/AppNavBar'; import ServerNotRechableComponent from './components/ServerNotRechableComponent'; import LoadingComponent from './components/LoadingComponent'; -import { useMemo, useEffect, ReactNode } from 'react'; +import TosAndMarketingEmailsModal from './components/TosAndMarketingEmailsModal'; +import { useMemo, useEffect, ReactNode, useState } from 'react'; import { useLocation } from 'react-router-dom'; const addServerErrorClass = () => { @@ -25,6 +26,8 @@ const removeServerErrorClass = () => { */ export default function App({ children }: { children: ReactNode }) { const location = useLocation(); + const [showTosAndMarketingEmailsModal, setShowTosAndMarketingEmailsModal] = + useState(false); const { data: user, isError, isLoading } = useAuth(); const shouldDisplayAppNavBar = useMemo(() => { @@ -43,12 +46,45 @@ export default function App({ children }: { children: ReactNode }) { return location.pathname.startsWith('/chat'); }, [location]); + const isCheckoutPage = useMemo(() => { + return location.pathname.startsWith('/checkout'); + }, [location]); + + const isAccountPage = useMemo(() => { + return location.pathname.startsWith('/account'); + }, [location]); + useEffect(() => { if (user) { - const lastSeenAt = new Date(user.lastActiveTimestamp); - const today = new Date(); - if (today.getTime() - lastSeenAt.getTime() > 5 * 60 * 1000) { - updateCurrentUser({ lastActiveTimestamp: today }); + if (!user.isSignUpComplete) { + if (user.hasAcceptedTos && user.hasSubscribedToMarketingEmails) { + updateCurrentUser({ + isSignUpComplete: true, + }); + setShowTosAndMarketingEmailsModal(false); + } else { + const hasAcceptedTos = + localStorage.getItem('hasAcceptedTos') === 'true'; + const hasSubscribedToMarketingEmails = + localStorage.getItem('hasSubscribedToMarketingEmails') === 'true'; + if (!hasAcceptedTos || !hasSubscribedToMarketingEmails) { + setShowTosAndMarketingEmailsModal(true); + } else { + updateCurrentUser({ + isSignUpComplete: true, + hasAcceptedTos: true, + hasSubscribedToMarketingEmails: true, + }); + setShowTosAndMarketingEmailsModal(false); + } + } + } else { + setShowTosAndMarketingEmailsModal(false); + const lastSeenAt = new Date(user.lastActiveTimestamp); + const today = new Date(); + if (today.getTime() - lastSeenAt.getTime() > 5 * 60 * 1000) { + updateCurrentUser({ lastActiveTimestamp: today }); + } } } }, [user]); @@ -68,17 +104,33 @@ export default function App({ children }: { children: ReactNode }) {
{isError && (addServerErrorClass(), ())} {isAdminDashboard || isChatPage ? ( - <>{children} + <> + {showTosAndMarketingEmailsModal ? ( + <> + + + ) : ( + children + )} + ) : ( <> {shouldDisplayAppNavBar && }
{isError ? ( - <>{children} + children ) : isLoading ? ( ) : ( - (removeServerErrorClass(), children) + (removeServerErrorClass(), + showTosAndMarketingEmailsModal && + (isCheckoutPage || isAccountPage) ? ( + <> + + + ) : ( + children + )) )}
diff --git a/app/src/client/Main.css b/app/src/client/Main.css index 973dbf7..bf3f964 100644 --- a/app/src/client/Main.css +++ b/app/src/client/Main.css @@ -52,28 +52,95 @@ a { list-style: disc; } -.custom-auth-wrapper input[type='checkbox'] { - display: none; -} - -.custom-auth-wrapper input[type='checkbox'] + label { +.toc-marketing-checkbox-wrapper .checkbox-container { + padding-left: 22px; + display: block; position: relative; - padding-left: 35px; cursor: pointer; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; } - -.custom-auth-wrapper input[type='checkbox'] + label:before { - content: ''; +/* Hide the browser's default checkbox */ +.toc-marketing-checkbox-wrapper input { position: absolute; + opacity: 0; + cursor: pointer; + height: 0; + width: 0; +} +/* Create a custom checkbox */ +.toc-marketing-checkbox-wrapper .checkmark { + position: absolute; + top: 2px; left: 0; - top: 0; - width: 15px; - height: 15px; - background: #eae4d9; + height: 13px; + width: 13px; + background-color: #eae4d9; } -.custom-auth-wrapper input[type='checkbox']:checked + label:before { - background: #6faabc; +.toc-marketing-checkbox-wrapper.light .checkmark { + background-color: #fff; +} + +/* On mouse-over, add a grey background color */ +.toc-marketing-checkbox-wrapper .checkbox-container:hover input ~ .checkmark { + background-color: #eae4d9; +} +.toc-marketing-checkbox-wrapper.light .checkbox-container:hover input ~ .checkmark { + background-color: #fff; +} + + +/* When the checkbox is checked, add a blue background */ +.toc-marketing-checkbox-wrapper .checkbox-container input:checked ~ .checkmark { + background-color: #6faabc; +} +.toc-marketing-checkbox-wrapper.light .checkbox-container input:checked ~ .checkmark { + background-color: #fff; +} + +/* Create the checkmark/indicator (hidden when not checked) */ +.toc-marketing-checkbox-wrapper .checkmark:after { + content: ""; + position: absolute; + display: none; +} + +/* Show the checkmark when checked */ +.toc-marketing-checkbox-wrapper .checkbox-container input:checked ~ .checkmark:after { + display: block; +} + +/* Style the checkmark/indicator */ +.toc-marketing-checkbox-wrapper .checkbox-container .checkmark:after { + left: 4px; + top: 0px; + width: 6px; + height: 11px; + border: solid #eae4d9; + border-width: 0 3px 3px 0; + -webkit-transform: rotate(45deg); + -ms-transform: rotate(45deg); + transform: rotate(45deg); +} +.toc-marketing-checkbox-wrapper.light .checkbox-container .checkmark:after { + border: solid #6faabc; + border-width: 0 3px 3px 0; + -webkit-transform: rotate(45deg); + -ms-transform: rotate(45deg); + transform: rotate(45deg); +} + +.toc-marketing-container{ + -webkit-transition: max-height 0.5s; + -moz-transition: max-height 0.5s; + -ms-transition: max-height 0.5s; + -o-transition: max-height 0.5s; + transition: max-height 0.5s; + overflow: hidden; + max-height: 280px; } .custom-auth-wrapper div>a { @@ -138,14 +205,14 @@ a { -webkit-flex-wrap: nowrap; flex-wrap: nowrap; height: 100%; - justify-content: space-between; + justify-content: center; position: relative; width: 100%; } .gsi-material-button .gsi-material-button-contents { - -webkit-flex-grow: 1; - flex-grow: 1; + -webkit-flex-grow: 0; + flex-grow: 0; font-family: 'Roboto', arial, sans-serif; font-weight: 500; overflow: hidden; diff --git a/app/src/client/app/layout/ChatLayout.tsx b/app/src/client/app/layout/ChatLayout.tsx index e882751..7b4b48d 100644 --- a/app/src/client/app/layout/ChatLayout.tsx +++ b/app/src/client/app/layout/ChatLayout.tsx @@ -31,7 +31,7 @@ const ChatLayout: FC = ({ if (!user) { history.push('/login'); } else { - if (!user.hasPaid) { + if (!user.hasPaid && user.isSignUpComplete) { history.push('/pricing'); } } diff --git a/app/src/client/auth/Auth.tsx b/app/src/client/auth/Auth.tsx index 765e0d8..16c97c6 100644 --- a/app/src/client/auth/Auth.tsx +++ b/app/src/client/auth/Auth.tsx @@ -48,6 +48,11 @@ export const AuthContext = createContext({ setSuccessMessage: (successMessage: string | null) => {}, }); +const titles: Record = { + login: 'Sign in to your account', + signup: 'Create a new account', +}; + function Auth({ state, appearance, @@ -62,21 +67,22 @@ function Auth({ const [errorMessage, setErrorMessage] = useState(null); const [successMessage, setSuccessMessage] = useState(null); const [isLoading, setIsLoading] = useState(false); + const [loginFlow, setLoginFlow] = useState(titles.signup); // TODO(matija): this is called on every render, is it a problem? // If we do it in useEffect(), then there is a glitch between the default color and the // user provided one. const customTheme = createTheme(appearance ?? {}); - const titles: Record = { - login: 'Log in to your account', - signup: 'Create a new account', - }; - const title = titles[state]; + // const title = titles[state]; const socialButtonsDirection = socialLayout === 'vertical' ? 'vertical' : 'horizontal'; + const changeHeaderText = (loginFlow: string) => { + setLoginFlow(loginFlow === 'signIn' ? titles.signup : titles.login); + }; + return (
@@ -89,7 +95,7 @@ function Auth({ /> )} {/* {title} */} -

{title}

+

{loginFlow}

{/* {errorMessage && ( @@ -109,6 +115,7 @@ function Auth({ socialButtonsDirection={socialButtonsDirection} additionalSignupFields={additionalSignupFields} errorMessage={errorMessage} + changeHeaderText={changeHeaderText} /> )} diff --git a/app/src/client/auth/LoginSignupForm.tsx b/app/src/client/auth/LoginSignupForm.tsx index ebacf0d..d7c4558 100644 --- a/app/src/client/auth/LoginSignupForm.tsx +++ b/app/src/client/auth/LoginSignupForm.tsx @@ -4,61 +4,14 @@ import { useForm } from 'react-hook-form'; import { styled } from './configs/stitches.config'; import { AuthContext } from './Auth'; import { useHistory } from 'react-router-dom'; -import { Link } from 'react-router-dom'; import config from './configs/config'; +import TosAndMarketingEmails from '../components/TosAndMarketingEmails'; -export const Message = styled('div', { - padding: '0.5rem 0.75rem', - borderRadius: '0.375rem', - marginTop: '1rem', - background: '$gray400', -}); - -export const MessageError = styled(Message, { - background: '#bb6e90', - color: '#eae4d9', -}); - -const OrContinueWith = styled('div', { - position: 'relative', - marginTop: '1.5rem', -}); - -const OrContinueWithLineContainer = styled('div', { - position: 'absolute', - inset: '0px', - display: 'flex', - alignItems: 'center', -}); - -const OrContinueWithLine = styled('div', { - width: '100%', - borderTopWidth: '1px', - borderColor: '$gray500', -}); - -const OrContinueWithTextContainer = styled('div', { - position: 'relative', - display: 'flex', - justifyContent: 'center', - fontSize: '$sm', -}); - -const OrContinueWithText = styled('span', { - backgroundColor: 'white', - paddingLeft: '0.5rem', - paddingRight: '0.5rem', -}); const SocialAuth = styled('div', { marginTop: '1.5rem', marginBottom: '1.5rem', }); -const SocialAuthLabel = styled('div', { - fontWeight: '500', - fontSize: '$sm', -}); - const SocialAuthButtons = styled('div', { marginTop: '0.5rem', display: 'flex', @@ -90,6 +43,12 @@ const SocialAuthButtons = styled('div', { const googleSignInUrl = `${config.apiUrl}/auth/google/login`; +export const checkBoxErrMsg = { + title: + 'To proceed, please ensure you have accepted the Terms & Conditions, Privacy Policy, and opted to receive marketing emails.', + description: '', +}; + export type LoginSignupFormFields = { [key: string]: string; }; @@ -99,11 +58,13 @@ export const LoginSignupForm = ({ socialButtonsDirection = 'horizontal', additionalSignupFields, errorMessage, + changeHeaderText, }: { state: 'login' | 'signup'; socialButtonsDirection?: 'horizontal' | 'vertical'; additionalSignupFields?: any; errorMessage?: any; + changeHeaderText: any; }) => { const { isLoading, setErrorMessage, setSuccessMessage, setIsLoading } = useContext(AuthContext); @@ -112,6 +73,7 @@ export const LoginSignupForm = ({ const history = useHistory(); const [tocChecked, setTocChecked] = useState(false); const [marketingEmailsChecked, setMarketingEmailsChecked] = useState(false); + const [loginFlow, setLoginFlow] = useState('signUp'); // const onErrorHandler = (error) => { // setErrorMessage({ // title: error.message, @@ -124,23 +86,6 @@ export const LoginSignupForm = ({ formState: { errors }, handleSubmit: hookFormHandleSubmit, } = hookForm; - // const { handleSubmit } = useUsernameAndPassword({ - // isLogin, - // onError: onErrorHandler, - // onSuccess() { - // history.push('/chat'); - // }, - // }); - // async function onSubmit(data) { - // setIsLoading(true); - // setErrorMessage(null); - // setSuccessMessage(null); - // try { - // await handleSubmit(data); - // } finally { - // setIsLoading(false); - // } - // } useEffect(() => { if (tocChecked && marketingEmailsChecked) { @@ -158,70 +103,56 @@ export const LoginSignupForm = ({ setMarketingEmailsChecked(event.target.checked); }; + const updateLocalStorage = () => { + localStorage.removeItem('hasAcceptedTos'); + localStorage.removeItem('hasSubscribedToMarketingEmails'); + localStorage.setItem('hasAcceptedTos', JSON.stringify(tocChecked)); + localStorage.setItem( + 'hasSubscribedToMarketingEmails', + JSON.stringify(marketingEmailsChecked) + ); + }; + const handleClick = ( event: React.MouseEvent, googleSignInUrl: string ) => { event.preventDefault(); - if (tocChecked && marketingEmailsChecked) { + if (loginFlow === 'signIn') { + updateLocalStorage(); window.location.href = googleSignInUrl; } else { - const err = { - title: - 'To proceed, please ensure you have accepted the Terms & Conditions, Privacy Policy, and opted to receive marketing emails.', - description: '', - }; - setErrorMessage(err); + if (tocChecked && marketingEmailsChecked) { + updateLocalStorage(); + window.location.href = googleSignInUrl; + } else { + setErrorMessage(checkBoxErrMsg); + } } }; + const toggleLoginFlow = () => { + const newLoginFlow = loginFlow === 'signIn' ? 'signUp' : 'signIn'; + setLoginFlow(newLoginFlow); + setTocChecked(false); + setMarketingEmailsChecked(false); + setErrorMessage(null); + changeHeaderText(loginFlow); + }; + + const googleBtnText = + loginFlow === 'signIn' ? 'Sign in with Google' : 'Sign up with Google'; + return ( <> -
- - -
-
- - -
- {errorMessage && ( -
- - {errorMessage.title} - {errorMessage.description && ': '} - {errorMessage.description} - -
)} @@ -261,131 +192,26 @@ export const LoginSignupForm = ({
- Sign in with Google + {googleBtnText} - Sign in with Google + {googleBtnText}
- {/* - - */} - {/* - - - - - Or continue with - - */} - {/*
- - Username - - {errors.username && {errors.username.message}} - - - Password - - {errors.password && {errors.password.message}} - - - - - {cta} - - - */} +
+ + {loginFlow === 'signIn' + ? "Don't have an account? " + : 'Already have an account? '} + + {loginFlow === 'signIn' ? 'Sign Up' : 'Sign In'} + + +
); }; - -// function AdditionalFormFields({ -// hookForm, -// formState: { isLoading }, -// additionalSignupFields, -// }: { -// hookForm: UseFormReturn; -// formState: FormState; -// additionalSignupFields: AdditionalSignupFields; -// }) { -// const { -// register, -// formState: { errors }, -// } = hookForm; - -// function renderField>( -// field: AdditionalSignupField, -// // Ideally we would use ComponentType here, but it doesn't work with react-hook-form -// Component: any, -// props?: React.ComponentProps -// ) { -// return ( -// -// {field.label} -// -// {errors[field.name] && ( -// {errors[field.name].message} -// )} -// -// ); -// } - -// if (areAdditionalFieldsRenderFn(additionalSignupFields)) { -// return additionalSignupFields(hookForm, { isLoading }); -// } - -// return ( -// additionalSignupFields && -// additionalSignupFields.map((field) => { -// if (isFieldRenderFn(field)) { -// return field(hookForm, { isLoading }); -// } -// switch (field.type) { -// case 'input': -// return renderField(field, FormInput, { -// type: 'text', -// }); -// case 'textarea': -// return renderField(field, FormTextarea); -// default: -// throw new Error( -// `Unsupported additional signup field type: ${field.type}` -// ); -// } -// }) -// ); -// } - -// function isFieldRenderFn( -// additionalSignupField: AdditionalSignupField | AdditionalSignupFieldRenderFn -// ): additionalSignupField is AdditionalSignupFieldRenderFn { -// return typeof additionalSignupField === 'function'; -// } - -// function areAdditionalFieldsRenderFn( -// additionalSignupFields: AdditionalSignupFields -// ): additionalSignupFields is AdditionalSignupFieldRenderFn { -// return typeof additionalSignupFields === 'function'; -// } diff --git a/app/src/client/components/NotificationBox.tsx b/app/src/client/components/NotificationBox.tsx index 7087c8c..5e7c7ef 100644 --- a/app/src/client/components/NotificationBox.tsx +++ b/app/src/client/components/NotificationBox.tsx @@ -14,7 +14,7 @@ const NotificationBox: React.FC = ({ const isSuccess = type === 'success'; return ( -
+

{isSuccess ? 'Success' : 'Error'} diff --git a/app/src/client/components/TosAndMarketingEmails.tsx b/app/src/client/components/TosAndMarketingEmails.tsx new file mode 100644 index 0000000..7ade8aa --- /dev/null +++ b/app/src/client/components/TosAndMarketingEmails.tsx @@ -0,0 +1,88 @@ +import React from 'react'; +import { Link } from 'react-router-dom'; + +import { styled } from '../auth/configs/stitches.config'; + +export const Message = styled('div', { + padding: '0.5rem 0.75rem', + borderRadius: '0.375rem', + marginTop: '1rem', + background: '$gray400', +}); + +export const MessageError = styled(Message, { + background: '#bb6e90', + color: '#eae4d9', +}); + +interface TosAndMarketingEmailsProps { + tocChecked: boolean; + handleTocChange: any; + marketingEmailsChecked: boolean; + handleMarketingEmailsChange: any; + errorMessage: { title: string; description?: string } | null; +} + +const TosAndMarketingEmails: React.FC = ({ + tocChecked, + handleTocChange, + marketingEmailsChecked, + handleMarketingEmailsChange, + errorMessage, +}) => ( +
+
+ +
+
+ +
+ {errorMessage && ( +
+ + {errorMessage.title} + {errorMessage.description && ': '} + {errorMessage.description} + +
+ )} +
+); + +export default TosAndMarketingEmails; diff --git a/app/src/client/components/TosAndMarketingEmailsModal.tsx b/app/src/client/components/TosAndMarketingEmailsModal.tsx new file mode 100644 index 0000000..96d0da8 --- /dev/null +++ b/app/src/client/components/TosAndMarketingEmailsModal.tsx @@ -0,0 +1,104 @@ +import React, { useState, useEffect, useContext, useMemo } from 'react'; +import { updateCurrentUser } from 'wasp/client/operations'; +import { useHistory } from 'react-router-dom'; + +import { AuthContext } from '../auth/Auth'; +import TosAndMarketingEmails from './TosAndMarketingEmails'; +import { checkBoxErrMsg } from '../auth/LoginSignupForm'; +import AppNavBar from './AppNavBar'; + +export type ErrorMessage = { + title: string; + description?: string; +}; + +const TosAndMarketingEmailsModal = () => { + const history = useHistory(); + const { isLoading, setSuccessMessage, setIsLoading } = + useContext(AuthContext); + const [errorMessage, setErrorMessage] = useState(null); + + const [tocChecked, setTocChecked] = useState(false); + const [marketingEmailsChecked, setMarketingEmailsChecked] = useState(false); + + useEffect(() => { + if (tocChecked && marketingEmailsChecked) { + setErrorMessage(null); + } + }, [tocChecked, marketingEmailsChecked]); + + const handleTocChange = (event: React.ChangeEvent) => { + setTocChecked(event.target.checked); + }; + + const handleMarketingEmailsChange = ( + event: React.ChangeEvent + ) => { + setMarketingEmailsChecked(event.target.checked); + }; + + const onClick = (event: React.MouseEvent) => { + event.preventDefault(); + if (tocChecked && marketingEmailsChecked) { + setErrorMessage(null); + updateCurrentUser({ + isSignUpComplete: true, + hasAcceptedTos: tocChecked, + hasSubscribedToMarketingEmails: marketingEmailsChecked, + }); + history.push('/chat'); + } else { + setErrorMessage(checkBoxErrMsg); + } + }; + + const customStyle = errorMessage + ? { maxHeight: '400px' } + : { maxHeight: '280px' }; + + const isAccountPage = useMemo(() => { + return location.pathname.startsWith('/account'); + }, [location]); + + return ( + <> + {!isAccountPage && } + +
+
+
+

+ Almost there... +

+

+ Before accessing the application, please confirm your agreement to + the Terms & Conditions, Privacy Policy, and consent to receiving + marketing emails by checking the boxes below +

+ + +
+ +
+
+
+
+ + ); +}; + +export default TosAndMarketingEmailsModal; diff --git a/app/src/client/tests/App.test.tsx b/app/src/client/tests/App.test.tsx index 4d6ae75..9d429d7 100644 --- a/app/src/client/tests/App.test.tsx +++ b/app/src/client/tests/App.test.tsx @@ -1,13 +1,51 @@ import { renderInContext } from 'wasp/client/test'; -import { test } from 'vitest'; -import { screen } from '@testing-library/react'; +import { test, expect, describe, vi, afterEach } from 'vitest'; +import { render, fireEvent, screen } from '@testing-library/react'; + +import { useAuth } from 'wasp/client/auth'; import App from '../App'; -test('renders App component', async () => { - renderInContext(Test

} />); +// vi.mock('wasp/client/auth', async (importOriginal) => { +// const mod = await importOriginal(); +// return { +// ...mod, +// useAuth: vi +// .fn() +// .mockResolvedValue([ +// { id: 1, isSignUpComplete: true, lastActiveTimestamp: new Date() }, +// ]), +// }; +// }); +const mocks = vi.hoisted(() => { + return { + useAuth: vi.fn(), + }; +}); + +vi.mock('wasp/client/auth', () => { + return { + useAuth: mocks.useAuth, + }; +}); + +describe('App', () => { + // afterEach(() => { + // vi.resetAllMocks(); + // }); + + test('renders App component', async () => { + const mockUser = { + data: { id: 1, isSignUpComplete: true, lastActiveTimestamp: new Date() }, + isError: false, + isLoading: false, + }; + mocks.useAuth.mockResolvedValue(mockUser); + renderInContext(Test
} />); - await screen.findByText('Loading...'); + await screen.findByText('Test'); - screen.debug(); + screen.debug(); + mocks.useAuth.mockRestore(); + }); }); diff --git a/app/src/client/tests/TosAndMarketingEmails.test.tsx b/app/src/client/tests/TosAndMarketingEmails.test.tsx new file mode 100644 index 0000000..3bfb52f --- /dev/null +++ b/app/src/client/tests/TosAndMarketingEmails.test.tsx @@ -0,0 +1,72 @@ +import { test, expect, vi, describe } from 'vitest'; +import { renderInContext } from 'wasp/client/test'; +import { fireEvent, screen } from '@testing-library/react'; + +import TosAndMarketingEmails from '../components/TosAndMarketingEmails'; + +const mockHandleTocChange = vi.fn(); +const mockHandleMarketingEmailsChange = vi.fn(); + +describe('TosAndMarketingEmails', () => { + test('renders TosAndMarketingEmails component', async () => { + renderInContext( + + ); + const linkItems = await screen.findAllByText('Terms & Conditions'); + expect(linkItems).toHaveLength(1); + }); + + test('calls handleTocChange when toc checkbox is clicked', async () => { + renderInContext( + + ); + fireEvent.click( + screen.getByLabelText( + 'I agree to the Terms & Conditions and Privacy Policy' + ) + ); + expect(mockHandleTocChange).toHaveBeenCalled(); + }); + test('calls handleMarketingEmailsChange when marketingEmails checkbox is clicked', async () => { + renderInContext( + + ); + fireEvent.click( + screen.getByLabelText('I agree to receiving marketing emails') + ); + expect(mockHandleMarketingEmailsChange).toHaveBeenCalled(); + }); + + test('renders error message when errorMessage prop is provided', async () => { + const errorMessage = { title: 'Error', description: 'This is an error' }; + renderInContext( + + ); + const errorItems = await screen.findAllByText('Error: This is an error'); + expect(errorItems).toHaveLength(1); + }); +}); diff --git a/app/src/client/tests/TosAndMarketingEmailsModal.test.tsx b/app/src/client/tests/TosAndMarketingEmailsModal.test.tsx new file mode 100644 index 0000000..7efe004 --- /dev/null +++ b/app/src/client/tests/TosAndMarketingEmailsModal.test.tsx @@ -0,0 +1,63 @@ +import { test, expect, vi, describe } from 'vitest'; +import { renderInContext } from 'wasp/client/test'; +import { fireEvent, screen } from '@testing-library/react'; +import { createMemoryHistory } from 'history'; +import { Router } from 'react-router-dom'; + +import TosAndMarketingEmailsModal from '../components/TosAndMarketingEmailsModal'; + +describe('TosAndMarketingEmailsModal', () => { + test('renders TosAndMarketingEmailsModal component', async () => { + renderInContext(); + const linkItems = await screen.findAllByText('Almost there...'); + expect(linkItems).toHaveLength(1); + }); + test('calls handleTocChange when toc checkbox is clicked', async () => { + renderInContext(); + fireEvent.click( + screen.getByLabelText( + 'I agree to the Terms & Conditions and Privacy Policy' + ) + ); + expect( + screen.getByLabelText( + 'I agree to the Terms & Conditions and Privacy Policy' + ) + ).toBeChecked(); + }); + test('calls handleMarketingEmailsChange when marketingEmails checkbox is clicked', async () => { + renderInContext(); + fireEvent.click( + screen.getByLabelText('I agree to receiving marketing emails') + ); + expect( + screen.getByLabelText('I agree to receiving marketing emails') + ).toBeChecked(); + }); + test('navigates to /chat when both checkboxes are checked and save button is clicked', async () => { + const history = createMemoryHistory(); + renderInContext( + + + + ); + fireEvent.click( + screen.getByLabelText( + 'I agree to the Terms & Conditions and Privacy Policy' + ) + ); + fireEvent.click( + screen.getByLabelText('I agree to receiving marketing emails') + ); + fireEvent.click(screen.getByText('Save')); + expect(history.location.pathname).toBe('/chat'); + }); + test('renders error message when save button is clicked and checkboxes are not checked', async () => { + renderInContext(); + fireEvent.click(screen.getByText('Save')); + const errorItems = await screen.findAllByText( + 'Before accessing the application, please confirm your agreement to the Terms & Conditions, Privacy Policy, and consent to receiving marketing emails by checking the boxes below' + ); + expect(errorItems).toHaveLength(1); + }); +}); diff --git a/app/src/server/scripts/usersSeed.ts b/app/src/server/scripts/usersSeed.ts index 9fc3a50..4d0367f 100644 --- a/app/src/server/scripts/usersSeed.ts +++ b/app/src/server/scripts/usersSeed.ts @@ -37,6 +37,7 @@ export function createRandomUser() { subscriptionTier: faker.helpers.arrayElement([TierIds.HOBBY, TierIds.PRO]), hasAcceptedTos: true, hasSubscribedToMarketingEmails: true, + isSignUpComplete: true, }; return user; }