Skip to content

Commit

Permalink
[UPM-1474]/evgeniy/passkey remove without verification (#16764)
Browse files Browse the repository at this point in the history
* chore: [UPM-1474]/evgeniy/passkey remove without verification

* fix: import and typo

* fix: error modal overlapping, no paskey page show when no passkey after removing

* chore: test cases

* refactor: remove usequery from getpasskeys

* fix: failing test

* fix: failing tests

* fix: test

* fix: wrong condition to save empty passkey array
  • Loading branch information
yauheni-deriv committed Sep 18, 2024
1 parent 576a169 commit 97eb8c6
Show file tree
Hide file tree
Showing 35 changed files with 582 additions and 332 deletions.
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
.network-status {
&__container {
.snackbar {
.quill-snackbar {
//center fixed element with dynamic width
left: 50%;
transform: translateX(-50%);
Expand Down
6 changes: 5 additions & 1 deletion packages/account/src/Constants/routes-config.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
import { routes, moduleLoader, makeLazyLoader } from '@deriv/shared';
import { localize } from '@deriv/translations';
import {
Passkeys,
PersonalDetails,
ProofOfIdentity,
ProofOfAddress,
Expand All @@ -27,6 +26,11 @@ const Passwords = makeLazyLoader(
() => <Loading />
)();

const Passkeys = makeLazyLoader(
() => moduleLoader(() => import('../Sections/Security/Passkeys')),
() => <Loading />
)();

const AccountLimits = makeLazyLoader(
() => moduleLoader(() => import('../Sections/Security/AccountLimits')),
() => <Loading />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { Analytics } from '@deriv-com/analytics';
import { APIProvider } from '@deriv/api';
import { useGetPasskeysList, useRegisterPasskey } from '@deriv/hooks';
import { useGetPasskeysList, useRegisterPasskey, useRenamePasskey, useRemovePasskey } from '@deriv/hooks';
import { useDevice } from '@deriv-com/ui';
import { routes } from '@deriv/shared';
import { mockStore, StoreProvider } from '@deriv/stores';
Expand Down Expand Up @@ -48,6 +48,8 @@ jest.mock('@deriv/hooks', () => ({
...jest.requireActual('@deriv/hooks'),
useGetPasskeysList: jest.fn(() => ({})),
useRegisterPasskey: jest.fn(() => ({})),
useRenamePasskey: jest.fn(() => ({})),
useRemovePasskey: jest.fn(() => ({})),
}));

jest.mock('@deriv/components', () => ({
Expand Down Expand Up @@ -119,7 +121,8 @@ describe('Passkeys', () => {

const mockCreatePasskey = jest.fn();
const mockStartPasskeyRegistration = jest.fn();
const mockClearPasskeyRegistrationError = jest.fn();
const mockRenamePasskey = jest.fn();
const mockRemovePasskey = jest.fn();
const mockReloadPasskeysList = jest.fn();

it("doesn't render existed passkeys for desktop and tablet", () => {
Expand Down Expand Up @@ -207,81 +210,92 @@ describe('Passkeys', () => {
);
});

it('renders success screen when new passkeys created and open "add more passkeys" ', () => {
it('renders passkeys creation modal and triggers new passkey creation', async () => {
(useGetPasskeysList as jest.Mock).mockReturnValue({
is_passkeys_list_loading: false,
});
mock_store.client.is_passkey_supported = true;

(useRegisterPasskey as jest.Mock).mockReturnValue({
is_passkey_registered: true,
createPasskey: mockCreatePasskey,
startPasskeyRegistration: mockStartPasskeyRegistration,
});

renderComponent();

expect(screen.getByText('Success!')).toBeInTheDocument();
userEvent.click(screen.getByRole('button', { name: create_passkey }));
expect(screen.getByText('Just a reminder')).toBeInTheDocument();
expect(screen.getByText('Enable screen lock on your device.')).toBeInTheDocument();
expect(screen.getByText('Enable bluetooth.')).toBeInTheDocument();
expect(screen.getByText('Sign in to your Google or iCloud account.')).toBeInTheDocument();

userEvent.click(screen.getByRole('button', { name: continue_button }));
expect(mockCreatePasskey).toBeCalledTimes(1);
expect(Analytics.trackEvent).toHaveBeenCalledWith(
tracking_event,
getAnalyticsParams('create_passkey_finished')
getAnalyticsParams('create_passkey_reminder_passed')
);
const add_more_passkeys_button = screen.getByRole('button', { name: 'Add more passkeys' });
userEvent.click(add_more_passkeys_button);
expect(Analytics.trackEvent).toHaveBeenCalledWith(tracking_event, getAnalyticsParams('add_more_passkeys'));

const create_passkey_button = screen.getByRole('button', { name: create_passkey });
expect(create_passkey_button).toBeInTheDocument();
expect(screen.queryByText('Success!')).not.toBeInTheDocument();
});

it('renders success screen when new passkeys created and open tradershub ', () => {
(useRegisterPasskey as jest.Mock).mockReturnValue({
is_passkey_registered: true,
it('renders passkeys and triggers rename passkey', async () => {
(useGetPasskeysList as jest.Mock).mockReturnValue({
is_passkeys_list_loading: false,
passkeys_list: mock_passkeys_list,
});
mock_store.client.is_passkey_supported = true;

(useRenamePasskey as jest.Mock).mockReturnValue({
renamePasskey: mockRenamePasskey,
});

renderComponent();

expect(screen.getByText('Success!')).toBeInTheDocument();
expect(Analytics.trackEvent).toHaveBeenCalledWith(
tracking_event,
getAnalyticsParams('create_passkey_finished')
);
expect(screen.queryByText('Edit passkey')).not.toBeInTheDocument();

const continue_trading_button = screen.getByRole('button', { name: 'Continue trading' });
userEvent.click(continue_trading_button);
expect(Analytics.trackEvent).toHaveBeenCalledWith(
tracking_event,
getAnalyticsParams('create_passkey_continue_trading')
);
expect(mockHistoryPush).toHaveBeenCalledWith(routes.traders_hub);
userEvent.click(screen.getAllByTestId('dt_dropdown_display')[0]);
userEvent.click(screen.getByText('Rename'));

expect(Analytics.trackEvent).toHaveBeenCalledWith(tracking_event, getAnalyticsParams('passkey_rename_started'));
expect(screen.getByText('Edit passkey')).toBeInTheDocument();

const input: HTMLInputElement = screen.getByRole('textbox');
userEvent.clear(input);
userEvent.type(input, 'new passkey name');
userEvent.click(screen.getByRole('button', { name: /save changes/i }));

await waitFor(() => {
expect(mockRenamePasskey).toHaveBeenCalledTimes(1);
});
});

it('renders passkeys creation modal and triggers new passkey creation', async () => {
it('renders passkeys and triggers remove passkey', async () => {
(useGetPasskeysList as jest.Mock).mockReturnValue({
is_passkeys_list_loading: false,
passkeys_list: mock_passkeys_list,
});
mock_store.client.is_passkey_supported = true;

(useRegisterPasskey as jest.Mock).mockReturnValue({
createPasskey: mockCreatePasskey,
is_passkey_registration_started: true,
startPasskeyRegistration: mockStartPasskeyRegistration,
(useRemovePasskey as jest.Mock).mockReturnValue({
removePasskey: mockRemovePasskey,
});

renderComponent();

userEvent.click(screen.getByRole('button', { name: create_passkey }));
expect(screen.getByText('Just a reminder')).toBeInTheDocument();
expect(screen.getByText('Enable screen lock on your device.')).toBeInTheDocument();
expect(screen.getByText('Enable bluetooth.')).toBeInTheDocument();
expect(screen.getByText('Sign in to your Google or iCloud account.')).toBeInTheDocument();
expect(screen.queryByText('Edit passkey')).not.toBeInTheDocument();

userEvent.click(screen.getByRole('button', { name: continue_button }));
expect(mockCreatePasskey).toBeCalledTimes(1);
expect(Analytics.trackEvent).toHaveBeenCalledWith(
tracking_event,
getAnalyticsParams('create_passkey_reminder_passed')
);
userEvent.click(screen.getAllByTestId('dt_dropdown_display')[0]);
userEvent.click(screen.getByText('Remove'));

expect(Analytics.trackEvent).toHaveBeenCalledWith(tracking_event, getAnalyticsParams('passkey_remove_started'));
expect(screen.getByText('Are you sure you want to remove this passkey?')).toBeInTheDocument();

userEvent.click(screen.getByRole('button', { name: /remove/i }));
expect(mockRemovePasskey).toHaveBeenCalledTimes(1);
});

it('renders passkeys registration error modal and triggers closing', async () => {
(useRegisterPasskey as jest.Mock).mockReturnValue({
passkey_registration_error: { message: 'error' },
clearPasskeyRegistrationError: mockClearPasskeyRegistrationError,
startPasskeyRegistration: mockStartPasskeyRegistration,
createPasskey: mockCreatePasskey,
});
Expand All @@ -297,8 +311,8 @@ describe('Passkeys', () => {
});

userEvent.click(screen.getByRole('button', { name: ok_button }));

await waitFor(() => {
expect(mockClearPasskeyRegistrationError).toBeCalledTimes(1);
expect(mockHistoryPush).toHaveBeenCalledWith(routes.traders_hub);
});
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ const mock_passkeys_list: React.ComponentProps<typeof PasskeysList>['passkeys_li

jest.mock('@deriv/shared', () => ({
...jest.requireActual('@deriv/shared'),
getOSNameWithUAParser: () => 'test OS',
getOSNameWithUAParser: jest.fn(() => 'test OS'),
}));

describe('PasskeyCard', () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import userEvent from '@testing-library/user-event';
import { render, screen } from '@testing-library/react';
import { PasskeyRemoved } from '../passkey-removed';
import { getOSNameWithUAParser } from '@deriv/shared';

jest.mock('@deriv/shared', () => ({
...jest.requireActual('@deriv/shared'),
getOSNameWithUAParser: jest.fn(() => 'test OS'),
}));

describe('PasskeyRemoved', () => {
it('renders correctly for iOS', () => {
(getOSNameWithUAParser as jest.Mock).mockReturnValue('iOS');

const mockOnPrimaryButtonClick = jest.fn();

render(<PasskeyRemoved onPrimaryButtonClick={mockOnPrimaryButtonClick} />);

expect(screen.getByText('Passkey successfully removed')).toBeInTheDocument();
expect(
screen.getByText(
'Your passkey is successfully removed. To avoid sign-in prompts, also remove the passkey from your iCloud keychain.'
)
).toBeInTheDocument();
expect(screen.getByText('Continue')).toBeInTheDocument();

userEvent.click(screen.getByText('Continue'));
expect(mockOnPrimaryButtonClick).toHaveBeenCalled();
});

it('renders correctly for other OS', () => {
(getOSNameWithUAParser as jest.Mock).mockReturnValue('Windows');

const mockOnPrimaryButtonClick = jest.fn();

render(<PasskeyRemoved onPrimaryButtonClick={mockOnPrimaryButtonClick} />);

expect(screen.getByText('Passkey successfully removed')).toBeInTheDocument();
expect(
screen.getByText(
'Your passkey is successfully removed. To avoid sign-in prompts, also remove the passkey from your Google password manager.'
)
).toBeInTheDocument();
expect(screen.getByText('Continue')).toBeInTheDocument();

userEvent.click(screen.getByText('Continue'));
expect(mockOnPrimaryButtonClick).toHaveBeenCalled();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ describe('PasskeyRename', () => {
const new_passkey_name = 'new passkey name';
const mock_current_managed_passkey: React.ComponentProps<
typeof PasskeysStatusContainer
>['current_managed_passkey'] = { id: 777, name: init_passkey_name };
>['current_managed_passkey'] = { id: 777, name: init_passkey_name, passkey_id: 'test_passkey_id' };
const validation_error = 'Only 3-30 characters allowed.';
const mockOnPrimaryButtonClick = jest.fn();
const mockOnSecondaryButtonClick = jest.fn();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ describe('PasskeysStatusContainer', () => {
const mockOnSecondaryButtonClick = jest.fn();
const mock_current_managed_passkey: React.ComponentProps<
typeof PasskeysStatusContainer
>['current_managed_passkey'] = { id: 777, name: 'test passkey name' };
>['current_managed_passkey'] = { id: 777, name: 'test passkey name', passkey_id: 'test_passkey_id' };

const renderComponent = (passkey_status: TPasskeysStatus) => {
render(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import React from 'react';
import { Text } from '@deriv/components';
import { Localize } from '@deriv/translations';
import { Localize } from '@deriv-com/translations';

const getPasskeysDescriptions = () =>
[
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import React from 'react';
import { Localize } from '@deriv/translations';
import { Localize } from '@deriv-com/translations';
import { DerivLightIcAddPasskeyIcon } from '@deriv/quill-icons';
import { PasskeysStatusLayout, TPasskeysButtonOnClicks } from './passkeys-status-layout';

Expand Down
Original file line number Diff line number Diff line change
@@ -1,24 +1,23 @@
import React from 'react';
import { Dropdown, Icon, Text } from '@deriv/components';
import { getLongDate } from '@deriv/shared';
import { localize, Localize } from '@deriv/translations';
import { useTranslations, Localize } from '@deriv-com/translations';
import { TOnPasskeyMenuClick, TPasskey } from '../passkeys';
import { PASSKEY_STATUS_CODES, passkeysMenuActionEventTrack } from '../passkeys-configs';

type TPasskeyCard = TPasskey & { onPasskeyMenuClick: TOnPasskeyMenuClick };

export const PasskeyCard = ({ name, last_used, stored_on, id, icon, onPasskeyMenuClick }: TPasskeyCard) => {
export const PasskeyCard = ({ name, last_used, stored_on, id, icon, passkey_id, onPasskeyMenuClick }: TPasskeyCard) => {
const { localize } = useTranslations();

const current_passkey_data = { id, name, passkey_id };

const handleManagePasskey = (event: { target: { value: string } }) => {
if (event.target.value === 'rename') {
onPasskeyMenuClick(PASSKEY_STATUS_CODES.RENAMING, {
id,
name,
});
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
passkeysMenuActionEventTrack('passkey_rename_open');
} else if (event.target.value === 'revoke') {
// TODO: add action for revoke passkey
onPasskeyMenuClick(PASSKEY_STATUS_CODES.RENAMING, current_passkey_data);
passkeysMenuActionEventTrack('passkey_rename_started');
} else if (event.target.value === 'remove') {
onPasskeyMenuClick(PASSKEY_STATUS_CODES.REMOVING, current_passkey_data);
passkeysMenuActionEventTrack('passkey_remove_started');
}
};

Expand All @@ -45,22 +44,17 @@ export const PasskeyCard = ({ name, last_used, stored_on, id, icon, onPasskeyMen
{icon && <Icon icon={icon} size={24} className='passkeys-card__passkey-type-icon' />}
</div>
<Dropdown
test_id={`dt_passkey_card_menu_${id}`}
is_align_text_left
list={[
{
text: localize('Rename'),
value: 'rename',
},
{
text: localize(''),
value: '',
disabled: true,
text: localize('Remove'),
value: 'remove',
},
// TODO: remove empty option when 'revoke' is implemented. Empty option is needed for proper working dropdown
// {
// text: localize('Revoke'),
// value: 'revoke',
// },
]}
onChange={handleManagePasskey}
suffix_icon='IcMenuDots'
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import React from 'react';
import { Localize } from '@deriv/translations';
import { Localize } from '@deriv-com/translations';
import { DerivLightIcSuccessPasskeyIcon } from '@deriv/quill-icons';
import { PasskeysStatusLayout, TPasskeysButtonOnClicks } from './passkeys-status-layout';

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import React from 'react';
import { Button, Modal, Text } from '@deriv/components';
import { Localize } from '@deriv/translations';
import { TPasskeyError } from '../passkeys-configs';
import { Localize } from '@deriv-com/translations';
import { isNotSupportedError, TPasskeyError } from '../passkeys-configs';
import { TServerError } from '../../../../Types';

type TPasskeyErrorModal = {
Expand All @@ -11,8 +10,6 @@ type TPasskeyErrorModal = {
};

const getErrorModalContent = (error: TPasskeyError) => {
const isNotSupportedError = (error: TServerError) => error?.name === 'NotSupportedError';

const error_message_header = (
<Text size='xs' weight='bold'>
{isNotSupportedError(error as TServerError) ? (
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import React from 'react';
import { MouseEventHandler } from 'react';
import { Button, Modal, Text } from '@deriv/components';
import { Localize } from '@deriv/translations';
import { Localize } from '@deriv-com/translations';

type TPasskeyReminderModal = {
onButtonClick: React.MouseEventHandler<HTMLButtonElement>;
onButtonClick: MouseEventHandler<HTMLButtonElement>;
is_modal_open: boolean;
toggleModal?: () => void;
};
Expand Down
Loading

0 comments on commit 97eb8c6

Please sign in to comment.