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

[UPM-1474]/evgeniy/passkey remove without verification #16764

Merged
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
cab98d2
chore: [UPM-1474]/evgeniy/passkey remove without verification
yauheni-deriv Sep 6, 2024
362f53b
fix: import and typo
yauheni-deriv Sep 6, 2024
f4bcfce
fix: error modal overlapping, no paskey page show when no passkey aft…
yauheni-deriv Sep 9, 2024
b7bfb79
chore: test cases
yauheni-deriv Sep 9, 2024
668b73f
Merge branch 'master' into passkey-remove-without-verification
yauheni-deriv Sep 9, 2024
35c16f8
Merge branch 'master' into passkey-remove-without-verification
yauheni-deriv Sep 10, 2024
0fa6334
Merge branch 'master' into passkey-remove-without-verification
yauheni-deriv Sep 11, 2024
31bf9fa
Merge branch 'master' into passkey-remove-without-verification
yauheni-deriv Sep 11, 2024
8995970
Merge branch 'master' into passkey-remove-without-verification
yauheni-deriv Sep 12, 2024
ad65a0b
refactor: remove usequery from getpasskeys
yauheni-deriv Sep 12, 2024
6932f69
fix: failing test
yauheni-deriv Sep 13, 2024
c3d0059
fix: failing tests
yauheni-deriv Sep 13, 2024
396dc1d
Merge branch 'master' into passkey-remove-without-verification
yauheni-deriv Sep 13, 2024
a8a18bf
fix: test
yauheni-deriv Sep 13, 2024
9b3e89b
Merge branch 'master' into passkey-remove-without-verification
yauheni-deriv Sep 13, 2024
54e5b60
fix: wrong condition to save empty passkey array
yauheni-deriv Sep 13, 2024
a93d332
Merge branch 'master' into passkey-remove-without-verification
yauheni-deriv Sep 13, 2024
0718d8d
Merge branch 'master' into passkey-remove-without-verification
yauheni-deriv Sep 16, 2024
980d2f0
Merge branch 'master' into passkey-remove-without-verification
yauheni-deriv Sep 17, 2024
2ed39d0
Merge branch 'master' into passkey-remove-without-verification
yauheni-deriv Sep 18, 2024
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
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
Loading