Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: add signup flow #115

Merged
merged 6 commits into from
Apr 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 />
</>
);
};
Loading