Skip to content

Commit

Permalink
Merge pull request #115 from thisyahlen-deriv/thisyahlen/signup
Browse files Browse the repository at this point in the history
chore: add signup flow
  • Loading branch information
habib-deriv authored Apr 23, 2024
2 parents 4b3b3ec + acf394e commit eeb23a3
Show file tree
Hide file tree
Showing 19 changed files with 297 additions and 11 deletions.
8 changes: 4 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
"prepare": "husky install"
},
"dependencies": {
"@deriv-com/api-hooks": "^0.1.5",
"@deriv-com/api-hooks": "^0.1.7",
"@deriv-com/analytics": "~1.4.13",
"@deriv-com/ui": "1.12.19",
"@deriv-com/utils": "^0.0.11",
Expand Down
91 changes: 91 additions & 0 deletions src/flows/Signup/CitizenshipModal/CitizenshipModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import React, { useEffect, useState } from 'react';
import { useFormikContext } from 'formik';

import { LabelPairedChevronDownMdRegularIcon } from '@deriv/quill-icons';
import { useResidenceList, useWebsiteStatus } from '@deriv-com/api-hooks';
import { Button, Checkbox, Dropdown, Text } from '@deriv-com/ui';

import { isCVMEnabled } from '@/helpers';

import { TSignupFormValues } from '../SignupWrapper/SignupWrapper';

type TCitizenshipModal = {
onClickNext: VoidFunction;
};

export const CitizenshipModal = ({ onClickNext }: TCitizenshipModal) => {
const { data: residenceList } = useResidenceList();
const { data: websiteStatus } = useWebsiteStatus();
const clientCountry = websiteStatus?.clients_country;
const [isCheckBoxChecked, setIsCheckBoxChecked] = useState(false);
const { values, setFieldValue } = useFormikContext<TSignupFormValues>();
const isCheckboxVisible = isCVMEnabled(values.country);

useEffect(() => {
if (residenceList?.length && clientCountry && values.country === '') {
setFieldValue('country', clientCountry);
}
}, [clientCountry, setFieldValue, residenceList, values.country]);

// Add <Loading /> here later when it's created

return (
<div className='h-full rounded-default max-w-[328px] lg:max-w-[440px] bg-system-light-primary-background'>
<div className='flex flex-col p-16 space-y-16 lg:space-y-24 lg:p-24'>
<Text weight='bold'>Select your country and citizenship:</Text>
<Dropdown
dropdownIcon={<LabelPairedChevronDownMdRegularIcon />}
errorMessage='Country of residence is where you currently live.'
label='Country of residence'
list={residenceList ?? []}
name='country'
onSelect={selectedItem => {
setFieldValue('country', selectedItem);
}}
value={values.country}
variant='comboBox'
/>
<Dropdown
dropdownIcon={<LabelPairedChevronDownMdRegularIcon />}
errorMessage='Select your citizenship/nationality as it appears on your passport or other government-issued ID.'
label='Citizenship'
list={residenceList ?? []}
name='citizenship'
onSelect={selectedItem => {
setFieldValue('citizenship', selectedItem);
}}
value={values.citizenship}
variant='comboBox'
/>
{isCheckboxVisible && (
<Checkbox
checked={isCheckBoxChecked}
label={
<Text size='sm'>
I hereby confirm that my request for opening an account with Deriv to trade OTC products
issued and offered exclusively outside Brazil was initiated by me. I fully understand
that Deriv is not regulated by CVM and by approaching Deriv I intend to set up a
relation with a foreign company.
</Text>
}
labelClassName='flex-1'
name='cvmCheckbox'
onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
setIsCheckBoxChecked(event.target.checked)
}
wrapperClassName='w-auto'
/>
)}
<Button
className='w-full lg:self-end lg:w-fit'
disabled={Boolean(
!values.country || !values.citizenship || (isCheckboxVisible && !isCheckBoxChecked)
)}
onClick={onClickNext}
>
Next
</Button>
</div>
</div>
);
};
1 change: 1 addition & 0 deletions src/flows/Signup/CitizenshipModal/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { CitizenshipModal } from './CitizenshipModal';
43 changes: 43 additions & 0 deletions src/flows/Signup/PasswordSettingModal/PasswordSettingModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { ChangeEvent } from 'react';
import { useFormikContext } from 'formik';

import { Button, PasswordInput, Text } from '@deriv-com/ui';

import { validPassword } from '@/utils';

import { TSignupFormValues } from '../SignupWrapper/SignupWrapper';

export const PasswordSettingModal = () => {
const { values, setFieldValue } = useFormikContext<TSignupFormValues>();

const onPasswordChange = (e: ChangeEvent<HTMLInputElement>) => {
setFieldValue('password', e.target.value);
};

return (
<div className='h-full rounded-default max-w-[328px] lg:max-w-[440px] bg-system-light-primary-background'>
<div className='flex flex-col p-16 space-y-16 lg:space-y-24 lg:p-24'>
<Text align='center' weight='bold'>
Keep your account secure with a password
</Text>
<PasswordInput
isFullWidth
label='Create a password'
onChange={onPasswordChange}
value={values.password}
/>
<Text align='center' size='xs'>
Strong passwords contain at least 8 characters. combine uppercase and lowercase letters, numbers,
and symbols.
</Text>
<Button
className='w-full lg:self-end lg:w-fit'
disabled={!validPassword(values.password)}
type='submit'
>
Start trading
</Button>
</div>
</div>
);
};
1 change: 1 addition & 0 deletions src/flows/Signup/PasswordSettingModal/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { PasswordSettingModal } from './PasswordSettingModal';
20 changes: 20 additions & 0 deletions src/flows/Signup/SignupScreens/SignupScreens.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import React, { Dispatch } from 'react';

import { CitizenshipModal } from '../CitizenshipModal';
import { PasswordSettingModal } from '../PasswordSettingModal';

type TSignupScreens = {
setStep: Dispatch<React.SetStateAction<number>>;
step: number;
};

export const SignupScreens = ({ step, setStep }: TSignupScreens) => {
switch (step) {
case 1:
return <CitizenshipModal onClickNext={() => setStep(prev => prev + 1)} />;
case 2:
return <PasswordSettingModal />;
default:
return null;
}
};
1 change: 1 addition & 0 deletions src/flows/Signup/SignupScreens/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { SignupScreens } from './SignupScreens';
41 changes: 41 additions & 0 deletions src/flows/Signup/SignupWrapper/SignupWrapper.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { useState } from 'react';
import { Form, Formik } from 'formik';

import { Modal } from '@deriv-com/ui';

import { useNewVirtualAccount, useQueryParams } from '@/hooks';
import { signup } from '@/utils/validations';

import { SignupScreens } from '../SignupScreens';

export type TSignupFormValues = {
citizenship: string;
country: string;
password: string;
};

export const SignupWrapper = () => {
const [step, setStep] = useState(1);
const { isModalOpen } = useQueryParams();
const { mutate } = useNewVirtualAccount();

const initialValues = {
country: '',
citizenship: '',
password: '',
};

const handleSubmit = (values: TSignupFormValues) => {
mutate(values);
};

return (
<Modal ariaHideApp={false} isOpen={isModalOpen('Signup')} shouldCloseOnOverlayClick={false}>
<Formik initialValues={initialValues} onSubmit={handleSubmit} validationSchema={signup}>
<Form>
<SignupScreens setStep={setStep} step={step} />
</Form>
</Formik>
</Modal>
);
};
1 change: 1 addition & 0 deletions src/flows/Signup/SignupWrapper/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { SignupWrapper } from './SignupWrapper';
1 change: 1 addition & 0 deletions src/flows/Signup/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { SignupWrapper as Signup } from './SignupWrapper';
1 change: 1 addition & 0 deletions src/flows/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from './RealAccountCreation';
export * from './Signup';
1 change: 1 addition & 0 deletions src/helpers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ export * from './urls';
export * from './currencies';
export * from './formikHelpers';
export * from './isEUCountry';
export * from './signupModalHelpers';
1 change: 1 addition & 0 deletions src/helpers/signupModalHelpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const isCVMEnabled = (countryCode: string) => countryCode === 'br';
1 change: 1 addition & 0 deletions src/hooks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,4 @@ export { useCFDAssets } from './useCFDAssets';
export { useTotalAssets } from './useTotalAssets';
export { useCtraderServiceToken } from './useCtraderServiceToken';
export { useExchangeRates } from './useExchangeRates';
export { useNewVirtualAccount } from './useNewVirtualAccount';
66 changes: 66 additions & 0 deletions src/hooks/useNewVirtualAccount.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { useCallback, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';

import { useAuthData, useNewAccountVirtual } from '@deriv-com/api-hooks';

import { useUIContext } from '@/providers';

import { useQueryParams } from './useQueryParams';

interface Values {
password: string;
country: string;
}

/**
* @name useNewVirtualAccount
* @description A custom hook that creates a new real virtual account.
* @returns {Object} Submit handler function, the new virtual account data and the status of the request.
*/
export const useNewVirtualAccount = () => {
const navigate = useNavigate();
const { openModal } = useQueryParams();
const { setUIState } = useUIContext();
const { data: newTradingAccountData, mutate: createAccount, status, ...rest } = useNewAccountVirtual();

const { appendAccountCookie } = useAuthData();

const verificationCode = localStorage.getItem('verification_code');
useEffect(() => {
if (status === 'success') {
// fail-safe for typescript as the data type is also undefined
if (!newTradingAccountData) return;

appendAccountCookie(
newTradingAccountData?.new_account_virtual?.client_id ?? '',
newTradingAccountData?.new_account_virtual?.oauth_token ?? ''
);

navigate('/');
openModal('RealAccountCreation');
}
// trigger validation error on status change when validation modal is created
}, [appendAccountCookie, navigate, newTradingAccountData, openModal, setUIState, status]);

/**
* @name handleSubmit
* @description A function that handles the form submission and calls the mutation.
*/
const mutate = useCallback(
(values: Values) => {
createAccount({
client_password: values.password,
residence: values.country,
verification_code: verificationCode ?? '',
});
},
[createAccount, verificationCode]
);

return {
mutate,
data: newTradingAccountData,
status,
...rest,
};
};
6 changes: 4 additions & 2 deletions src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ const AnalyticsConfigurator = () => {
Analytics.setAttributes(attributes);
}
}
}, [activeTradingAccount, websiteStatusData]);
}, [activeTradingAccount, getAppId, isDesktop, isMobile, isTablet, websiteStatusData]);

return null;
};
Expand All @@ -57,14 +57,16 @@ const container = document.getElementById('root');
const root = container ? ReactDOM.createRoot(container) : null;
startInitPerformanceTimers();

const signupRoute = window.location.pathname === '/signup';

root?.render(
<React.StrictMode>
<QueryClientProvider client={queryClient}>
<AppDataProvider>
<UIProvider>
<CFDProvider>
<RealAccountCreationProvider>
<Header />
{!signupRoute && <Header />}
<App />
<AnalyticsConfigurator />
</RealAccountCreationProvider>
Expand Down
4 changes: 3 additions & 1 deletion src/pages/redirect/redirect.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@ export const Redirect = () => {
const { search } = useLocation();
const urlParams = new URLSearchParams(search);
const actionParam = urlParams.get('action');
const verificationCode = urlParams.get('code');
localStorage.setItem('verification_code', verificationCode ?? '');

if (actionParam === 'signup') {
return <Navigate to={routes.signup + search} replace />;
return <Navigate to={routes.signup + search} />;
}

return <Navigate to={routes.home} replace />;
Expand Down
18 changes: 15 additions & 3 deletions src/pages/signup/signup.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,31 @@
import { Link } from 'react-router-dom';
import { useEffect } from 'react';

import { useDevice } from '@deriv-com/ui';

import { IconComponent } from '@/components';
import { useQueryParams } from '@/hooks';
import { setPerformanceValue } from '@/utils';

import { Signup as SignupModal } from '../../flows/Signup';

export const Signup = () => {
const { openModal, isModalOpen } = useQueryParams();

const { isMobile } = useDevice();

useEffect(() => {
openModal('Signup');
}, [isModalOpen, openModal]);

// leave it here for now
setPerformanceValue('signup_time', isMobile);

return (
<>
<h1>Signup</h1>
<Link to='/'>Go to Homepage</Link>
<div className='flex justify-center items-center'>
<IconComponent icon='Deriv' height={90} width={90} />
</div>
<SignupModal />
</>
);
};

0 comments on commit eeb23a3

Please sign in to comment.