mirror of
https://github.com/kremalicious/metamask-extension.git
synced 2024-11-22 01:47:00 +01:00
[MMI] interactive replacement token notification (#18620)
* adds the component and styles * adds tests * story file update * clean up * lint and prettier * lint & prettier fix * adds review changes * adds necessary dependencies * runs lint and prettier
This commit is contained in:
parent
8632acbba3
commit
290353da9b
3
app/_locales/en/messages.json
generated
3
app/_locales/en/messages.json
generated
@ -951,6 +951,9 @@
|
|||||||
"custodyRefreshTokenModalTitle": {
|
"custodyRefreshTokenModalTitle": {
|
||||||
"message": "Your custodian session has expired"
|
"message": "Your custodian session has expired"
|
||||||
},
|
},
|
||||||
|
"custodySessionExpired": {
|
||||||
|
"message": "Custodian session expired."
|
||||||
|
},
|
||||||
"custom": {
|
"custom": {
|
||||||
"message": "Advanced"
|
"message": "Advanced"
|
||||||
},
|
},
|
||||||
|
11
shared/modules/hash.utils.ts
Normal file
11
shared/modules/hash.utils.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
/* eslint-disable no-undef */
|
||||||
|
export async function sha256(str: string): Promise<string> {
|
||||||
|
const buf = await crypto.subtle.digest(
|
||||||
|
'SHA-256',
|
||||||
|
new TextEncoder().encode(str),
|
||||||
|
);
|
||||||
|
|
||||||
|
return Array.prototype.map
|
||||||
|
.call(new Uint8Array(buf), (x: number) => `00${x.toString(16)}`.slice(-2))
|
||||||
|
.join('');
|
||||||
|
}
|
@ -0,0 +1 @@
|
|||||||
|
export { default } from './interactive-replacement-token-notification';
|
@ -0,0 +1,143 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { getCurrentKeyring, getSelectedAddress } from '../../../selectors';
|
||||||
|
import { getInteractiveReplacementToken } from '../../../selectors/institutional/selectors';
|
||||||
|
import { getIsUnlocked } from '../../../ducks/metamask/metamask';
|
||||||
|
import { useI18nContext } from '../../../hooks/useI18nContext';
|
||||||
|
import { mmiActionsFactory } from '../../../store/institutional/institution-background';
|
||||||
|
import { sha256 } from '../../../../shared/modules/hash.utils';
|
||||||
|
import {
|
||||||
|
Size,
|
||||||
|
IconColor,
|
||||||
|
AlignItems,
|
||||||
|
DISPLAY,
|
||||||
|
BLOCK_SIZES,
|
||||||
|
JustifyContent,
|
||||||
|
TextColor,
|
||||||
|
TextVariant,
|
||||||
|
BackgroundColor,
|
||||||
|
} from '../../../helpers/constants/design-system';
|
||||||
|
import {
|
||||||
|
Icon,
|
||||||
|
IconName,
|
||||||
|
IconSize,
|
||||||
|
ButtonLink,
|
||||||
|
Text,
|
||||||
|
} from '../../component-library';
|
||||||
|
|
||||||
|
import Box from '../../ui/box';
|
||||||
|
|
||||||
|
const InteractiveReplacementTokenNotification = ({ isVisible }) => {
|
||||||
|
const t = useI18nContext();
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const mmiActions = mmiActionsFactory();
|
||||||
|
|
||||||
|
const keyring = useSelector(getCurrentKeyring);
|
||||||
|
const address = useSelector(getSelectedAddress);
|
||||||
|
const isUnlocked = useSelector(getIsUnlocked);
|
||||||
|
const interactiveReplacementToken = useSelector(
|
||||||
|
getInteractiveReplacementToken,
|
||||||
|
);
|
||||||
|
|
||||||
|
const [showNotification, setShowNotification] = useState(isVisible);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleShowNotification = async () => {
|
||||||
|
const hasInteractiveReplacementToken =
|
||||||
|
interactiveReplacementToken &&
|
||||||
|
Boolean(Object.keys(interactiveReplacementToken).length);
|
||||||
|
|
||||||
|
if (!/^Custody/u.test(keyring.type)) {
|
||||||
|
setShowNotification(false);
|
||||||
|
return;
|
||||||
|
} else if (!hasInteractiveReplacementToken) {
|
||||||
|
setShowNotification(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const token = await dispatch(mmiActions.getCustodianToken());
|
||||||
|
const custodyAccountDetails = await dispatch(
|
||||||
|
mmiActions.getAllCustodianAccountsWithToken(
|
||||||
|
keyring.type.split(' - ')[1],
|
||||||
|
token,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
const showNotificationValue =
|
||||||
|
isUnlocked &&
|
||||||
|
interactiveReplacementToken.oldRefreshToken &&
|
||||||
|
custodyAccountDetails &&
|
||||||
|
Boolean(Object.keys(custodyAccountDetails).length);
|
||||||
|
|
||||||
|
let tokenAccount;
|
||||||
|
|
||||||
|
if (Array.isArray(custodyAccountDetails)) {
|
||||||
|
tokenAccount = custodyAccountDetails
|
||||||
|
.filter(
|
||||||
|
(item) => item.address.toLowerCase() === address.toLowerCase(),
|
||||||
|
)
|
||||||
|
.map((item) => ({
|
||||||
|
token: item.authDetails?.refreshToken,
|
||||||
|
}))[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
const refreshTokenAccount = await sha256(
|
||||||
|
tokenAccount?.token + interactiveReplacementToken.url,
|
||||||
|
);
|
||||||
|
|
||||||
|
setShowNotification(
|
||||||
|
showNotificationValue &&
|
||||||
|
refreshTokenAccount === interactiveReplacementToken.oldRefreshToken,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
handleShowNotification();
|
||||||
|
}, [
|
||||||
|
dispatch,
|
||||||
|
address,
|
||||||
|
interactiveReplacementToken,
|
||||||
|
isUnlocked,
|
||||||
|
keyring,
|
||||||
|
mmiActions,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return showNotification ? (
|
||||||
|
<Box
|
||||||
|
width={BLOCK_SIZES.FULL}
|
||||||
|
display={DISPLAY.FLEX}
|
||||||
|
justifyContent={JustifyContent.center}
|
||||||
|
alignItems={AlignItems.center}
|
||||||
|
padding={[1, 2]}
|
||||||
|
backgroundColor={BackgroundColor.backgroundAlternative}
|
||||||
|
marginBottom={1}
|
||||||
|
className="interactive-replacement-token-notification"
|
||||||
|
data-testid="interactive-replacement-token-notification"
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
name={IconName.Danger}
|
||||||
|
color={IconColor.errorDefault}
|
||||||
|
size={IconSize.Xl}
|
||||||
|
/>
|
||||||
|
<Text variant={TextVariant.bodyXs} gap={2} color={TextColor.errorDefault}>
|
||||||
|
{t('custodySessionExpired')}
|
||||||
|
</Text>
|
||||||
|
<ButtonLink
|
||||||
|
data-testid="show-modal"
|
||||||
|
size={Size.auto}
|
||||||
|
marginLeft={1}
|
||||||
|
onClick={() => {
|
||||||
|
dispatch(mmiActions.showInteractiveReplacementTokenModal());
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t('learnMore')}
|
||||||
|
</ButtonLink>
|
||||||
|
</Box>
|
||||||
|
) : null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default InteractiveReplacementTokenNotification;
|
||||||
|
|
||||||
|
InteractiveReplacementTokenNotification.propTypes = {
|
||||||
|
isVisible: PropTypes.bool,
|
||||||
|
};
|
@ -0,0 +1,15 @@
|
|||||||
|
.interactive-replacement-token-notification {
|
||||||
|
height: 24px;
|
||||||
|
|
||||||
|
@media screen and (min-width: $break-large) {
|
||||||
|
width: 85vw;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (min-width: 768px) {
|
||||||
|
width: 80vw;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (min-width: 1280px) {
|
||||||
|
width: 62vw;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,59 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Provider } from 'react-redux';
|
||||||
|
import configureStore from '../../../store/store';
|
||||||
|
import testData from '../../../../.storybook/test-data';
|
||||||
|
import InteractiveReplacementTokenNotification from '.';
|
||||||
|
|
||||||
|
const customData = {
|
||||||
|
...testData,
|
||||||
|
metamask: {
|
||||||
|
...testData.metamask,
|
||||||
|
provider: {
|
||||||
|
type: 'test',
|
||||||
|
},
|
||||||
|
selectedAddress: '0xca8f1F0245530118D0cf14a06b01Daf8f76Cf281',
|
||||||
|
identities: {
|
||||||
|
'0xca8f1F0245530118D0cf14a06b01Daf8f76Cf281': {
|
||||||
|
address: '0xca8f1F0245530118D0cf14a06b01Daf8f76Cf281',
|
||||||
|
name: 'Custodian A',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
isUnlocked: true,
|
||||||
|
interactiveReplacementToken: {
|
||||||
|
oldRefreshToken:
|
||||||
|
'81f96a88b6cbc5f50d3864122349fa9a9755833ee82a7e3cf6f268c78aab51ab',
|
||||||
|
url: 'url',
|
||||||
|
},
|
||||||
|
preferences: {
|
||||||
|
useNativeCurrencyAsPrimaryCurrency: true,
|
||||||
|
},
|
||||||
|
keyrings: [
|
||||||
|
{
|
||||||
|
type: 'Custody - Saturn',
|
||||||
|
accounts: ['0xca8f1F0245530118D0cf14a06b01Daf8f76Cf281'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const store = configureStore(customData);
|
||||||
|
|
||||||
|
export default {
|
||||||
|
title: 'Components/Institutional/InteractiveReplacementToken-Notification',
|
||||||
|
decorators: [(story) => <Provider store={store}>{story()}</Provider>],
|
||||||
|
component: InteractiveReplacementTokenNotification,
|
||||||
|
args: {
|
||||||
|
isVisible: true,
|
||||||
|
},
|
||||||
|
argTypes: {
|
||||||
|
onClick: {
|
||||||
|
action: 'onClick',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DefaultStory = (args) => (
|
||||||
|
<InteractiveReplacementTokenNotification {...args} />
|
||||||
|
);
|
||||||
|
|
||||||
|
DefaultStory.storyName = 'InteractiveReplacementTokenNotification';
|
@ -0,0 +1,156 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import configureMockStore from 'redux-mock-store';
|
||||||
|
import thunk from 'redux-thunk';
|
||||||
|
import { screen, fireEvent } from '@testing-library/react';
|
||||||
|
import { act } from 'react-dom/test-utils';
|
||||||
|
import { sha256 } from '../../../../shared/modules/hash.utils';
|
||||||
|
import { KeyringType } from '../../../../shared/constants/keyring';
|
||||||
|
import { renderWithProvider } from '../../../../test/lib/render-helpers';
|
||||||
|
import InteractiveReplacementTokenNotification from './interactive-replacement-token-notification';
|
||||||
|
|
||||||
|
jest.mock('../../../../shared/modules/hash.utils');
|
||||||
|
|
||||||
|
const mockedShowInteractiveReplacementTokenModal = jest
|
||||||
|
.fn()
|
||||||
|
.mockReturnValue({ type: 'TYPE' });
|
||||||
|
|
||||||
|
const mockedGetCustodianToken = jest
|
||||||
|
.fn()
|
||||||
|
.mockReturnValue({ type: 'Custody', payload: 'token' });
|
||||||
|
|
||||||
|
const mockedGetAllCustodianAccountsWithToken = jest.fn().mockReturnValue({
|
||||||
|
type: 'TYPE',
|
||||||
|
payload: [
|
||||||
|
{
|
||||||
|
address: '0xca8f1F0245530118D0cf14a06b01Daf8f76Cf281',
|
||||||
|
authDetails: { refreshToken: 'def' },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
jest.mock('../../../store/institutional/institution-background', () => ({
|
||||||
|
mmiActionsFactory: () => ({
|
||||||
|
getCustodianToken: mockedGetCustodianToken,
|
||||||
|
getAllCustodianAccountsWithToken: mockedGetAllCustodianAccountsWithToken,
|
||||||
|
showInteractiveReplacementTokenModal:
|
||||||
|
mockedShowInteractiveReplacementTokenModal,
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('Interactive Replacement Token Notification', () => {
|
||||||
|
const selectedAddress = '0xca8f1F0245530118D0cf14a06b01Daf8f76Cf281';
|
||||||
|
|
||||||
|
const identities = {
|
||||||
|
'0xca8f1F0245530118D0cf14a06b01Daf8f76Cf281': {
|
||||||
|
address: '0xca8f1F0245530118D0cf14a06b01Daf8f76Cf281',
|
||||||
|
name: 'Custodian A',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockStore = {
|
||||||
|
metamask: {
|
||||||
|
provider: {
|
||||||
|
type: 'test',
|
||||||
|
},
|
||||||
|
selectedAddress,
|
||||||
|
identities,
|
||||||
|
isUnlocked: false,
|
||||||
|
interactiveReplacementToken: { oldRefreshToken: 'abc' },
|
||||||
|
preferences: {
|
||||||
|
useNativeCurrencyAsPrimaryCurrency: true,
|
||||||
|
},
|
||||||
|
keyrings: [
|
||||||
|
{
|
||||||
|
type: KeyringType.imported,
|
||||||
|
accounts: ['0xca8f1F0245530118D0cf14a06b01Daf8f76Cf281', '0x2'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: KeyringType.ledger,
|
||||||
|
accounts: [],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
it('should not render if show notification is false', () => {
|
||||||
|
const store = configureMockStore([thunk])(mockStore);
|
||||||
|
|
||||||
|
renderWithProvider(<InteractiveReplacementTokenNotification />, store);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
screen.queryByTestId('interactive-replacement-token-notification'),
|
||||||
|
).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render if show notification is true and click on learn more', async () => {
|
||||||
|
const customMockStore = {
|
||||||
|
...mockStore,
|
||||||
|
metamask: {
|
||||||
|
...mockStore.metamask,
|
||||||
|
isUnlocked: true,
|
||||||
|
interactiveReplacementToken: { oldRefreshToken: 'def', url: 'url' },
|
||||||
|
keyrings: [
|
||||||
|
{
|
||||||
|
type: 'Custody - Saturn',
|
||||||
|
accounts: ['0xca8f1F0245530118D0cf14a06b01Daf8f76Cf281'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const store = configureMockStore([thunk])(customMockStore);
|
||||||
|
|
||||||
|
sha256.mockReturnValue('def');
|
||||||
|
await act(async () => {
|
||||||
|
renderWithProvider(<InteractiveReplacementTokenNotification />, store);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(
|
||||||
|
screen.getByTestId('interactive-replacement-token-notification'),
|
||||||
|
).toBeInTheDocument();
|
||||||
|
expect(screen.getByTestId('show-modal')).toBeInTheDocument();
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
fireEvent.click(screen.getByTestId('show-modal'));
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockedShowInteractiveReplacementTokenModal).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render and call showNotification when component starts', async () => {
|
||||||
|
const customMockStore = {
|
||||||
|
...mockStore,
|
||||||
|
metamask: {
|
||||||
|
...mockStore.metamask,
|
||||||
|
isUnlocked: true,
|
||||||
|
interactiveReplacementToken: { oldRefreshToken: 'def', url: 'url' },
|
||||||
|
keyrings: [
|
||||||
|
{
|
||||||
|
type: 'Custody - Saturn',
|
||||||
|
accounts: ['0xca8f1F0245530118D0cf14a06b01Daf8f76Cf281', '0x2'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: KeyringType.ledger,
|
||||||
|
accounts: [],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const store = configureMockStore([thunk])(customMockStore);
|
||||||
|
|
||||||
|
sha256.mockReturnValue('def');
|
||||||
|
await act(async () => {
|
||||||
|
renderWithProvider(<InteractiveReplacementTokenNotification />, store);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockedGetCustodianToken).toHaveBeenCalled();
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(
|
||||||
|
screen.getByTestId('interactive-replacement-token-notification'),
|
||||||
|
).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
@ -62,3 +62,7 @@ export function getIsCustodianSupportedChain(state) {
|
|||||||
)
|
)
|
||||||
: true;
|
: true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getInteractiveReplacementToken(state) {
|
||||||
|
return state.metamask.interactiveReplacementToken || {};
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user