diff --git a/packages/account/src/Sections/Security/TwoFactorAuthentication/__tests__/digit-form.spec.tsx b/packages/account/src/Sections/Security/TwoFactorAuthentication/__tests__/digit-form.spec.tsx deleted file mode 100644 index f8909eb18663..000000000000 --- a/packages/account/src/Sections/Security/TwoFactorAuthentication/__tests__/digit-form.spec.tsx +++ /dev/null @@ -1,202 +0,0 @@ -import React from 'react'; -import { render, screen, waitFor } from '@testing-library/react'; -import userEvent from '@testing-library/user-event'; -import { APIProvider, useSendUserOTP } from '@deriv/api'; -import { StoreProvider, mockStore } from '@deriv/stores'; -import DigitForm from '../digit-form'; - -jest.mock('@deriv/api', () => ({ - ...jest.requireActual('@deriv/api'), - useSendUserOTP: jest.fn(() => ({ - is_TwoFA_enabled: false, - data: { account_security: { totp: { is_enabled: 0 } } }, - isLoading: false, - isSuccess: false, - sendUserOTP: jest.fn(() => ({ totp_action: 'disable', otp: '328746' })), - })), -})); - -const mockUseSendUserOTP = useSendUserOTP as jest.MockedFunction; - -describe('', () => { - const store = mockStore({ - client: { - has_enabled_two_fa: false, - is_switching: false, - setTwoFAStatus: jest.fn(), - setTwoFAChangedStatus: jest.fn(), - }, - common: { - is_language_changing: false, - }, - }); - - const renderComponent = ({ store_config = store }) => { - render( - - - - - - ); - }; - - it('should render the DigitForm component', () => { - renderComponent({ store_config: store }); - - const digit_form_label = screen.getByText(/Authentication code/i); - expect(digit_form_label).toBeInTheDocument(); - }); - - it('should disable button when form is empty or validation fails', async () => { - renderComponent({ store_config: store }); - const submitButton = screen.getByRole('button', { name: /Enable/i }); - expect(submitButton).toBeDisabled(); - - const digitInput = screen.getByRole('textbox'); - userEvent.type(digitInput, '669yi9'); - await waitFor(() => { - expect(submitButton).toBeDisabled(); - }); - }); - - it('should change button text when user enables or disables 2FA', () => { - const new_store_config = { - ...store, - client: { - ...store.client, - has_enabled_two_fa: true, - }, - }; - - renderComponent({ store_config: store }); - const enableButton = screen.getByRole('button', { name: /Enable/i }); - expect(enableButton).toBeInTheDocument(); - - renderComponent({ store_config: new_store_config }); - const disableButton = screen.getByRole('button', { name: /Disable/i }); - expect(disableButton).toBeInTheDocument(); - }); - - it('should display error if submits empty form', async () => { - renderComponent({ store_config: store }); - - const nameInput = screen.getByRole('textbox'); - await userEvent.click(nameInput); - - userEvent.tab(); - - await waitFor(() => { - const error = screen.getByText(/Digit code is required./i); - expect(error).toBeInTheDocument(); - }); - }); - - it('should display error if user types alphanumeric characters', async () => { - renderComponent({ store_config: store }); - const digitInput = screen.getByRole('textbox'); - - userEvent.type(digitInput, '669yi9'); - userEvent.tab(); - - await waitFor(() => { - const error = screen.getByText(/Digit code must only contain numbers./i); - expect(error).toBeInTheDocument(); - }); - }); - - it('should display error if user types less than 6 digits', async () => { - renderComponent({ store_config: store }); - const digitInput = screen.getByRole('textbox'); - - userEvent.type(digitInput, '6699'); - userEvent.tab(); - - await waitFor(() => { - const error = screen.getByText(/Length of digit code must be 6 characters./i); - expect(error).toBeInTheDocument(); - }); - }); - - it('should display error if user types invalid OTP', async () => { - // @ts-expect-error need to come up with a way to mock the return type of useSendUserOTP - mockUseSendUserOTP.mockReturnValue({ - error: { message: 'OTP verification failed', code: 'InvalidOTP' }, - sendUserOTP: jest.fn(), - }); - - renderComponent({ store_config: store }); - - const digitInput = screen.getByRole('textbox'); - userEvent.type(digitInput, '786789'); - - const submitButton = screen.getByRole('button'); - userEvent.click(submitButton); - userEvent.tab(); - - await waitFor(() => { - const error = screen.getByText(/That's not the right code. Please try again./i); - expect(error).toBeInTheDocument(); - }); - }); - - it('should display error if error code inside response is not equal to InvalidOTP ', async () => { - // @ts-expect-error need to come up with a way to mock the return type of useSendUserOTP - mockUseSendUserOTP.mockReturnValue({ - error: { message: 'OTP verification failed', code: '' }, - sendUserOTP: jest.fn(), - }); - - renderComponent({ store_config: store }); - - const digitInput = screen.getByRole('textbox'); - const invalidOTP = '786789'; - - userEvent.type(digitInput, invalidOTP); - - const submitButton = screen.getByRole('button', { name: /Enable/i }); - userEvent.click(submitButton); - userEvent.tab(); - - await waitFor(() => { - const error = screen.getByText(/OTP verification failed/i); - expect(error).toBeInTheDocument(); - }); - }); - - it('should call setTwoFAStatus function on call success', async () => { - // @ts-expect-error need to come up with a way to mock the return type of sendUserOTP - mockUseSendUserOTP.mockReturnValue({ - isSuccess: true, - sendUserOTP: jest.fn(), - }); - - renderComponent({ store_config: store }); - - expect(store.client.setTwoFAStatus).toBeCalled(); - }); - - it('should call sendUserOTP with the correct arguments on button submit', async () => { - const mockSendUserOTP = jest.fn(); - // @ts-expect-error need to come up with a way to mock the return type of sendUserOTP - mockUseSendUserOTP.mockReturnValue({ - sendUserOTP: mockSendUserOTP, - is_TwoFA_enabled: true, - }); - - renderComponent({ store_config: store }); - - const digitInput = screen.getByRole('textbox'); - const invalidOTP = '786789'; - - userEvent.type(digitInput, invalidOTP); - - const submitButton = screen.getByRole('button', { name: /Enable/i }); - - userEvent.click(submitButton); - - await waitFor(() => { - expect(mockSendUserOTP).toHaveBeenCalled(); - }); - }); -}); diff --git a/packages/account/src/Sections/Security/TwoFactorAuthentication/__tests__/two-factor-authentication-article.spec.tsx b/packages/account/src/Sections/Security/TwoFactorAuthentication/__tests__/two-factor-authentication-article.spec.tsx deleted file mode 100644 index 893283ebd79e..000000000000 --- a/packages/account/src/Sections/Security/TwoFactorAuthentication/__tests__/two-factor-authentication-article.spec.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import React from 'react'; -import { render, screen } from '@testing-library/react'; -import TwoFactorAuthenticationArticle from '../two-factor-authentication-article'; - -describe('', () => { - it('should render TwoFactorAuthenticationArticle component', () => { - render(); - - const two_factor_authentication_article = screen.getByText(/Two-factor authentication \(2FA\)/i); - expect(two_factor_authentication_article).toBeInTheDocument(); - }); - - it('should render article description properly', () => { - render(); - - const description = screen.getByText( - /Protect your account with 2FA. Each time you log in to your account, you will need to enter your password and an authentication code generated by a 2FA app on your smartphone./i - ); - expect(description).toBeInTheDocument(); - }); -}); diff --git a/packages/account/src/Sections/Security/TwoFactorAuthentication/__tests__/two-factor-authentication.spec.tsx b/packages/account/src/Sections/Security/TwoFactorAuthentication/__tests__/two-factor-authentication.spec.tsx deleted file mode 100644 index 70e272c9a9db..000000000000 --- a/packages/account/src/Sections/Security/TwoFactorAuthentication/__tests__/two-factor-authentication.spec.tsx +++ /dev/null @@ -1,151 +0,0 @@ -import React from 'react'; -import { cleanup, render, screen, waitFor } from '@testing-library/react'; -import { APIProvider, useGetSecretKey, useGetTwoFa } from '@deriv/api'; -import { StoreProvider, mockStore } from '@deriv/stores'; -import TwoFactorAuthentication from '../two-factor-authentication'; - -jest.mock('@deriv/api', () => { - return { - ...jest.requireActual('@deriv/api'), - useGetTwoFa: jest.fn(() => ({ - is_TwoFA_enabled: false, - isLoading: false, - isSuccess: true, - getTwoFA: jest.fn(), - })), - useGetSecretKey: jest.fn(() => ({ - data: { - account_security: { - totp: { - secret_key: 'hello123', - }, - }, - }, - isLoading: false, - isSuccess: true, - getSecretKey: jest.fn(), - })), - }; -}); - -const mockUseGetTwoFa = useGetTwoFa as jest.MockedFunction; -const mockUseGetSecretKey = useGetSecretKey as jest.MockedFunction; - -jest.mock('@deriv/components', () => { - const original_module = jest.requireActual('@deriv/components'); - - return { - ...original_module, - Loading: jest.fn(() => 'mockedLoading'), - }; -}); - -jest.mock('qrcode.react', () => jest.fn(() =>
QRCode
)); - -describe('', () => { - afterEach(() => { - cleanup(); - jest.clearAllMocks(); - }); - - const store = mockStore({ - client: { - has_enabled_two_fa: false, - email_address: 'test@dev.com', - is_switching: false, - setTwoFAChangedStatus: jest.fn(), - }, - }); - - const renderComponent = ({ store_config = store }) => { - render( - - - - - - ); - }; - - it('should render Loader component if is_switching is true', async () => { - const new_store_config = { - ...store, - client: { - ...store.client, - is_switching: true, - }, - }; - - renderComponent({ store_config: new_store_config }); - - await waitFor(() => { - expect(screen.getByText('mockedLoading')).toBeInTheDocument(); - }); - }); - - it('should render LoadErrorMessage component if getTwoFA call returns error', async () => { - // @ts-expect-error need to come up with a way to mock the return type of useGetTwoFa - mockUseGetTwoFa.mockReturnValueOnce({ - error: { message: 'Invalid Request', code: 'InvalidOTP' }, - getTwoFA: jest.fn(), - }); - - renderComponent({ store_config: store }); - - await waitFor(() => { - const error_message = screen.getByText(/Invalid Request/i); - expect(error_message).toBeInTheDocument(); - }); - }); - - it('should render LoadErrorMessage component if getSecretKey call returns error', async () => { - // @ts-expect-error need to come up with a way to mock the return type of useGetSecretKey - mockUseGetSecretKey.mockReturnValueOnce({ - error: { message: 'Invalid request error', code: 'InvalidOTP' }, - getSecretKey: jest.fn(), - }); - - renderComponent({ store_config: store }); - - await waitFor(() => { - const error_message = screen.getByText(/Invalid request error/i); - expect(error_message).toBeInTheDocument(); - }); - }); - - it('should render TwoFactorDisabled component has_enabled_two_fa is false', async () => { - renderComponent({ store_config: store }); - - const setup_title = screen.getByText(/How to set up 2FA for your Deriv account/i); - expect(setup_title).toBeInTheDocument(); - }); - - it('should render TwoFactorEnabled component has_enabled_two_fa is true', async () => { - const new_store_config = { - ...store, - client: { - ...store.client, - has_enabled_two_fa: true, - }, - }; - - renderComponent({ store_config: new_store_config }); - - const enabled_title = screen.getByText(/You have enabled 2FA for your Deriv account./i); - expect(enabled_title).toBeInTheDocument(); - }); - - it('should render QR code if getTwoFA call is successful and is_TwoFA_enabled returns false', async () => { - // @ts-expect-error need to come up with a way to mock the return type of useGetTwoFa - mockUseGetTwoFa.mockReturnValueOnce({ - isSuccess: true, - is_TwoFA_enabled: true, - getTwoFA: jest.fn(), - }); - - renderComponent({ store_config: store }); - - const qr_code = screen.getByText('QRCode'); - expect(qr_code).toBeInTheDocument(); - }); -}); diff --git a/packages/account/src/Sections/Security/TwoFactorAuthentication/__tests__/two-factor-disabled.spec.tsx b/packages/account/src/Sections/Security/TwoFactorAuthentication/__tests__/two-factor-disabled.spec.tsx deleted file mode 100644 index 16065303e11b..000000000000 --- a/packages/account/src/Sections/Security/TwoFactorAuthentication/__tests__/two-factor-disabled.spec.tsx +++ /dev/null @@ -1,132 +0,0 @@ -import React from 'react'; -import { render, screen } from '@testing-library/react'; -import { APIProvider } from '@deriv/api'; -import { StoreProvider, mockStore } from '@deriv/stores'; -import TwoFactorDisabled from '../two-factor-disabled'; - -jest.mock('@deriv/components', () => { - const original_module = jest.requireActual('@deriv/components'); - - return { - ...original_module, - Loading: jest.fn(() => 'mockedLoading'), - }; -}); - -jest.mock('qrcode.react', () => jest.fn(() =>
QRCode
)); - -describe('', () => { - const mock_props: React.ComponentProps = { - secret_key: 'hello123', - qr_secret_key: '3535', - is_loading_secret: false, - }; - - const store = mockStore({ - client: { - has_enabled_two_fa: false, - }, - ui: { - is_mobile: true, - }, - }); - - const renderComponent = ({ store_config = store, mock = mock_props }) => { - render( - - - - - - ); - }; - - it('should render TwoFactorDisabled component if has_enabled_two_fa is false', () => { - renderComponent({ store_config: store }); - - const setup_title = screen.getByText(/How to set up 2FA for your Deriv account/i); - expect(setup_title).toBeInTheDocument(); - }); - - it('should render timeline_1 component title ', () => { - renderComponent({ store_config: store }); - - const timeline_title_1 = screen.getByText(/Scan the QR code below with your 2FA app. We recommend./i); - const authy_link = screen.getByRole('link', { name: 'Authy' }); - const google_authenticator_link = screen.getByRole('link', { name: 'Google Authenticator' }); - - expect(timeline_title_1).toBeInTheDocument(); - expect(authy_link).toHaveAttribute('href', 'https://authy.com/'); - expect(google_authenticator_link).toHaveAttribute( - 'href', - 'https://github.com/google/google-authenticator/wiki#implementations' - ); - }); - - it('should render QR code', () => { - renderComponent({ store_config: store }); - - const qr_code = screen.getByText('QRCode'); - expect(qr_code).toBeInTheDocument(); - }); - - it('should render clipboard component to setup 2FA', () => { - renderComponent({ store_config: store }); - - const helper_text = screen.getByText( - /If you are unable to scan the QR code, you can manually enter this code instead:/i - ); - const secret_text = screen.getByText('hello123'); - const clipboard_component = screen.getByTestId('2fa_clipboard'); - - expect(helper_text).toBeInTheDocument(); - expect(secret_text).toBeInTheDocument(); - expect(clipboard_component).toBeInTheDocument(); - }); - - it('should render step-2 title for setting up 2FA', () => { - renderComponent({ store_config: store }); - - const step_2_title = screen.getByText(/Enter the authentication code generated by your 2FA app:/i); - expect(step_2_title).toBeInTheDocument(); - }); - - it('should render digitform component if 2FA is disabled', () => { - renderComponent({ store_config: store }); - - const digitform = screen.getByTestId('digitform_2fa_disabled'); - expect(digitform).toBeInTheDocument(); - }); - - it('should render 2FA article component for mobile', () => { - const new_store = { - ...store, - ui: { - ...store.ui, - is_mobile: true, - }, - }; - - renderComponent({ store_config: new_store }); - - const article_component = screen.getByText('Two-factor authentication (2FA)'); - expect(article_component).toBeInTheDocument(); - }); - - it('should render 2FA article component for desktop', () => { - renderComponent({ store_config: store }); - - const article_component = screen.getByText('Two-factor authentication (2FA)'); - expect(article_component).toBeInTheDocument(); - }); - - it('should render Loader component if is_loading_secret is true', () => { - const new_mock_props = { - ...mock_props, - is_loading_secret: true, - }; - - renderComponent({ store_config: store, mock: new_mock_props }); - expect(screen.getByText('mockedLoading')).toBeInTheDocument(); - }); -}); diff --git a/packages/account/src/Sections/Security/TwoFactorAuthentication/__tests__/two-factor-enabled.spec.tsx b/packages/account/src/Sections/Security/TwoFactorAuthentication/__tests__/two-factor-enabled.spec.tsx deleted file mode 100644 index 02f07b8fa109..000000000000 --- a/packages/account/src/Sections/Security/TwoFactorAuthentication/__tests__/two-factor-enabled.spec.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import React from 'react'; -import { render, screen } from '@testing-library/react'; -import { APIProvider } from '@deriv/api'; -import { StoreProvider, mockStore } from '@deriv/stores'; -import TwoFactorEnabled from '../two-factor-enabled'; - -describe('', () => { - const store = mockStore({ - client: { - has_enabled_two_fa: false, - }, - }); - - it('should render TwoFactorEnabled component if 2FA is enabled', () => { - render( - - - - - - ); - - const title_1 = screen.getByText(/2FA enabled/i); - const title_2 = screen.getByText(/You have enabled 2FA for your Deriv account./i); - const title_3 = screen.getByText( - /To disable 2FA, please enter the six-digit authentication code generated by your 2FA app below:/i - ); - expect(title_1).toBeInTheDocument(); - expect(title_2).toBeInTheDocument(); - expect(title_3).toBeInTheDocument(); - }); - - it('should render DigitForm component if 2FA is enabled', () => { - render( - - - - - - ); - - const digitform = screen.getByTestId('digitform_2fa_enabled'); - expect(digitform).toBeInTheDocument(); - }); -}); diff --git a/packages/account/src/Sections/Security/TwoFactorAuthentication/digit-form.tsx b/packages/account/src/Sections/Security/TwoFactorAuthentication/digit-form.jsx similarity index 55% rename from packages/account/src/Sections/Security/TwoFactorAuthentication/digit-form.tsx rename to packages/account/src/Sections/Security/TwoFactorAuthentication/digit-form.jsx index 7d43d8cb82b0..0aa3357c688d 100644 --- a/packages/account/src/Sections/Security/TwoFactorAuthentication/digit-form.tsx +++ b/packages/account/src/Sections/Security/TwoFactorAuthentication/digit-form.jsx @@ -1,23 +1,16 @@ import React from 'react'; import classNames from 'classnames'; -import { Formik, Form, Field, FieldProps, FormikProps } from 'formik'; +import { Formik, Form, Field } from 'formik'; import { Input, Button } from '@deriv/components'; -import { useSendUserOTP } from '@deriv/api'; +import { getPropertyValue, WS } from '@deriv/shared'; import { localize } from '@deriv/translations'; -import { useStore, observer } from '@deriv/stores'; -type TDigitFormValues = { - digit_code: string; -}; - -const DigitForm = observer(() => { - const { client, common } = useStore(); - const { is_language_changing } = common; - const { has_enabled_two_fa, setTwoFAChangedStatus, setTwoFAStatus } = client; - - const { is_TwoFA_enabled, error, isSuccess, sendUserOTP } = useSendUserOTP(); - const button_text = has_enabled_two_fa ? localize('Disable') : localize('Enable'); - const formik_ref = React.useRef>(null); +const DigitForm = ({ is_enabled, setTwoFAStatus, setTwoFAChangedStatus, is_language_changing }) => { + const [is_success, setSuccess] = React.useState(false); + const [is_ready_for_verification, setReadyForVerification] = React.useState(false); + const button_text = is_enabled ? localize('Disable') : localize('Enable'); + const formik_ref = React.useRef(); + let enable_response; const initial_form = { digit_code: '', @@ -25,50 +18,47 @@ const DigitForm = observer(() => { React.useEffect(() => { if (is_language_changing) { - formik_ref.current?.setFieldTouched('digit_code'); + formik_ref.current.setFieldTouched('digit_code'); } }, [is_language_changing]); - React.useEffect(() => { - if (error) { - if (typeof error === 'object' && 'code' in error && 'message' in error) { - const { code, message } = error; - if (code === 'InvalidOTP') - formik_ref.current?.setFieldError( - 'digit_code', - localize("That's not the right code. Please try again.") - ); - else { - formik_ref.current?.setFieldError('digit_code', message as string); - } - } - } - }, [error]); - - React.useEffect(() => { - if (isSuccess) { - setTwoFAStatus(is_TwoFA_enabled); - setTwoFAChangedStatus(true); - formik_ref.current?.resetForm(); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [isSuccess, is_TwoFA_enabled]); - - const validateFields = async (values: TDigitFormValues) => { + const validateFields = async values => { const digit_code = values.digit_code; if (!digit_code) { return { digit_code: localize('Digit code is required.') }; - } else if (digit_code.length !== 6) { + } else if (!(digit_code.length === 6)) { return { digit_code: localize('Length of digit code must be 6 characters.') }; - } else if (!/^\d{6}$/g.test(digit_code)) { + } else if (!/^[0-9]{6}$/g.test(digit_code)) { return { digit_code: localize('Digit code must only contain numbers.') }; + } else if (is_ready_for_verification) { + if (formik_ref.current.isValid) { + const totp_action = is_enabled ? 'disable' : 'enable'; + enable_response = await WS.authorized.accountSecurity({ + account_security: 1, + totp_action, + otp: values.digit_code, + }); + if (enable_response.error) { + const { code, message } = enable_response.error; + if (code === 'InvalidOTP') + return { digit_code: localize("That's not the right code. Please try again.") }; + return { digit_code: message }; + } + } else { + return { digit_code: localize("That's not the right code. Please try again.") }; + } } return {}; }; - const handleSubmit = async (values: TDigitFormValues) => { - const action = has_enabled_two_fa ? 'disable' : 'enable'; - sendUserOTP({ totp_action: action, otp: values.digit_code }); + const handleSubmit = async (values, { resetForm }) => { + if (!enable_response.error) { + const is_enabled_response = !!getPropertyValue(enable_response, ['account_security', 'totp', 'is_enabled']); + setSuccess(true); + resetForm(); + setTwoFAStatus(is_enabled_response); + setTwoFAChangedStatus(true); + } }; return ( @@ -77,7 +67,7 @@ const DigitForm = observer(() => {
- {({ field }: FieldProps) => ( + {({ field }) => ( { value={values.digit_code} onChange={e => { handleChange(e); + setReadyForVerification(false); }} onBlur={handleBlur} required - error={touched.digit_code && errors.digit_code ? errors.digit_code : undefined} - maxLength={6} + error={touched.digit_code && errors.digit_code} + maxLength='6' autoComplete='off' /> )}