mirror of
https://github.com/kremalicious/metamask-extension.git
synced 2024-11-23 02:10:12 +01:00
1e56fdbf66
* added code fences in whats new popup * Improved code * Added missing prop * Update LavaMoat policies * updated functions by using an options object for the rendering functions in order to bypass possible typsecript issues --------- Co-authored-by: MetaMask Bot <metamaskbot@users.noreply.github.com>
423 lines
12 KiB
JavaScript
423 lines
12 KiB
JavaScript
import React, { useContext, useMemo, useRef, useState, useEffect } from 'react';
|
|
import { useHistory } from 'react-router-dom';
|
|
import { useSelector } from 'react-redux';
|
|
import PropTypes from 'prop-types';
|
|
import classnames from 'classnames';
|
|
import { debounce } from 'lodash';
|
|
import { getCurrentLocale } from '../../../ducks/locale/locale';
|
|
import { I18nContext } from '../../../contexts/i18n';
|
|
import { useEqualityCheck } from '../../../hooks/useEqualityCheck';
|
|
import Popover from '../../ui/popover';
|
|
import {
|
|
Button,
|
|
///: BEGIN:ONLY_INCLUDE_IN(build-mmi)
|
|
IconName,
|
|
///: END:ONLY_INCLUDE_IN
|
|
Text,
|
|
} from '../../component-library';
|
|
import { updateViewedNotifications } from '../../../store/actions';
|
|
import { getTranslatedUINotifications } from '../../../../shared/notifications';
|
|
import { getSortedAnnouncementsToShow } from '../../../selectors';
|
|
import {
|
|
BUILD_QUOTE_ROUTE,
|
|
PREPARE_SWAP_ROUTE,
|
|
ADVANCED_ROUTE,
|
|
EXPERIMENTAL_ROUTE,
|
|
SECURITY_ROUTE,
|
|
} from '../../../helpers/constants/routes';
|
|
import {
|
|
///: BEGIN:ONLY_INCLUDE_IN(build-mmi)
|
|
Size,
|
|
///: END:ONLY_INCLUDE_IN
|
|
TextVariant,
|
|
} from '../../../helpers/constants/design-system';
|
|
import ZENDESK_URLS from '../../../helpers/constants/zendesk-url';
|
|
import { MetaMetricsContext } from '../../../contexts/metametrics';
|
|
import {
|
|
MetaMetricsEventCategory,
|
|
MetaMetricsEventName,
|
|
} from '../../../../shared/constants/metametrics';
|
|
|
|
function getActionFunctionById(id, history) {
|
|
const actionFunctions = {
|
|
2: () => {
|
|
global.platform.openTab({
|
|
url: 'https://survey.alchemer.com/s3/6173069/MetaMask-Extension-NPS-January-2021',
|
|
});
|
|
},
|
|
3: () => {
|
|
global.platform.openTab({
|
|
url: 'https://community.metamask.io/t/about-the-security-category/72',
|
|
});
|
|
},
|
|
4: () => {
|
|
updateViewedNotifications({ 4: true });
|
|
history.push(BUILD_QUOTE_ROUTE);
|
|
},
|
|
5: () => {
|
|
updateViewedNotifications({ 5: true });
|
|
global.platform.openTab({
|
|
url: ZENDESK_URLS.SECRET_RECOVERY_PHRASE,
|
|
});
|
|
},
|
|
8: () => {
|
|
updateViewedNotifications({ 8: true });
|
|
history.push(ADVANCED_ROUTE);
|
|
},
|
|
10: () => {
|
|
updateViewedNotifications({ 10: true });
|
|
history.push(`${SECURITY_ROUTE}#token-description`);
|
|
},
|
|
12: () => {
|
|
updateViewedNotifications({ 12: true });
|
|
history.push(EXPERIMENTAL_ROUTE);
|
|
},
|
|
14: () => {
|
|
updateViewedNotifications({ 14: true });
|
|
history.push(`${ADVANCED_ROUTE}#backup-userdata`);
|
|
},
|
|
16: () => {
|
|
updateViewedNotifications({ 16: true });
|
|
},
|
|
17: () => {
|
|
updateViewedNotifications({ 17: true });
|
|
},
|
|
18: () => {
|
|
updateViewedNotifications({ 18: true });
|
|
history.push(`${EXPERIMENTAL_ROUTE}#transaction-security-check`);
|
|
},
|
|
19: () => {
|
|
updateViewedNotifications({ 19: true });
|
|
history.push(`${EXPERIMENTAL_ROUTE}#autodetect-nfts`);
|
|
},
|
|
20: () => {
|
|
updateViewedNotifications({ 20: true });
|
|
global.platform.openTab({
|
|
url: ZENDESK_URLS.LEDGER_FIREFOX_U2F_GUIDE,
|
|
});
|
|
},
|
|
21: () => {
|
|
updateViewedNotifications({ 21: true });
|
|
history.push(PREPARE_SWAP_ROUTE);
|
|
},
|
|
};
|
|
|
|
return actionFunctions[id];
|
|
}
|
|
|
|
const renderDescription = (description) => {
|
|
if (!Array.isArray(description)) {
|
|
return <Text variant={TextVariant.bodyMd}>{description}</Text>;
|
|
}
|
|
|
|
return (
|
|
<>
|
|
{description.map((piece, index) => {
|
|
const isLast = index === description.length - 1;
|
|
return (
|
|
<Text
|
|
key={`item-${index}`}
|
|
variant={TextVariant.bodyMd}
|
|
marginBottom={isLast ? 0 : 4}
|
|
>
|
|
{piece}
|
|
</Text>
|
|
);
|
|
})}
|
|
</>
|
|
);
|
|
};
|
|
|
|
const renderFirstNotification = ({
|
|
notification,
|
|
idRefMap,
|
|
history,
|
|
isLast,
|
|
trackEvent,
|
|
///: BEGIN:ONLY_INCLUDE_IN(build-mmi)
|
|
mmiPortfolioUrl,
|
|
seenNotifications,
|
|
onClose,
|
|
///: END:ONLY_INCLUDE_IN
|
|
}) => {
|
|
const {
|
|
id,
|
|
date,
|
|
title,
|
|
description,
|
|
image,
|
|
actionText,
|
|
///: BEGIN:ONLY_INCLUDE_IN(build-mmi)
|
|
customButton,
|
|
hideDate,
|
|
///: END:ONLY_INCLUDE_IN
|
|
} = notification;
|
|
const actionFunction = getActionFunctionById(id, history);
|
|
let showNotificationDate = true;
|
|
|
|
///: BEGIN:ONLY_INCLUDE_IN(build-mmi)
|
|
showNotificationDate = !hideDate;
|
|
///: END:ONLY_INCLUDE_IN
|
|
|
|
const imageComponent = image && (
|
|
<img
|
|
className="whats-new-popup__notification-image"
|
|
src={image.src}
|
|
height={image.height}
|
|
width={image.width}
|
|
/>
|
|
);
|
|
const placeImageBelowDescription = image?.placeImageBelowDescription;
|
|
return (
|
|
<div
|
|
className={classnames(
|
|
'whats-new-popup__notification whats-new-popup__first-notification',
|
|
{
|
|
'whats-new-popup__last-notification': isLast,
|
|
},
|
|
)}
|
|
key={`whats-new-popop-notification-${id}`}
|
|
>
|
|
<Text variant={TextVariant.bodyLgMedium} marginBottom={2}>
|
|
{title}
|
|
</Text>
|
|
{!placeImageBelowDescription && imageComponent}
|
|
<div className="whats-new-popup__description-and-date">
|
|
<div className="whats-new-popup__notification-description">
|
|
{renderDescription(description)}
|
|
</div>
|
|
{showNotificationDate && (
|
|
<div className="whats-new-popup__notification-date">{date}</div>
|
|
)}
|
|
</div>
|
|
{placeImageBelowDescription && imageComponent}
|
|
{actionText && (
|
|
<Button
|
|
type="primary"
|
|
className="whats-new-popup__button"
|
|
onClick={() => {
|
|
actionFunction();
|
|
trackEvent({
|
|
category: MetaMetricsEventCategory.Home,
|
|
event: MetaMetricsEventName.WhatsNewClicked,
|
|
});
|
|
}}
|
|
>
|
|
{actionText}
|
|
</Button>
|
|
)}
|
|
{
|
|
///: BEGIN:ONLY_INCLUDE_IN(build-mmi)
|
|
customButton && customButton.name === 'mmi-portfolio' && (
|
|
<Button
|
|
className="whats-new-popup__button"
|
|
data-testid="view-mmi-portfolio"
|
|
size={Size.SM}
|
|
startIconName={IconName.MmmiPortfolioDashboard}
|
|
onClick={() => {
|
|
updateViewedNotifications(seenNotifications);
|
|
onClose();
|
|
window.open(mmiPortfolioUrl, '_blank');
|
|
}}
|
|
>
|
|
{customButton.text}
|
|
</Button>
|
|
)
|
|
///: END:ONLY_INCLUDE_IN
|
|
}
|
|
<div
|
|
className="whats-new-popup__intersection-observable"
|
|
ref={idRefMap[id]}
|
|
/>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
const renderSubsequentNotification = ({
|
|
notification,
|
|
idRefMap,
|
|
history,
|
|
isLast,
|
|
}) => {
|
|
const { id, date, title, description, actionText } = notification;
|
|
|
|
const actionFunction = getActionFunctionById(id, history);
|
|
return (
|
|
<div
|
|
className={classnames('whats-new-popup__notification', {
|
|
'whats-new-popup__last-notification': isLast,
|
|
})}
|
|
key={`whats-new-popop-notification-${id}`}
|
|
>
|
|
<div className="whats-new-popup__notification-title">{title}</div>
|
|
<div className="whats-new-popup__description-and-date">
|
|
<div className="whats-new-popup__notification-description">
|
|
{renderDescription(description)}
|
|
</div>
|
|
<div className="whats-new-popup__notification-date">{date}</div>
|
|
</div>
|
|
{actionText && (
|
|
<div className="whats-new-popup__link" onClick={actionFunction}>
|
|
{`${actionText} >`}
|
|
</div>
|
|
)}
|
|
<div
|
|
className="whats-new-popup__intersection-observable"
|
|
ref={idRefMap[id]}
|
|
/>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default function WhatsNewPopup({
|
|
onClose,
|
|
///: BEGIN:ONLY_INCLUDE_IN(build-mmi)
|
|
mmiPortfolioUrl,
|
|
///: END:ONLY_INCLUDE_IN
|
|
}) {
|
|
const t = useContext(I18nContext);
|
|
const history = useHistory();
|
|
|
|
const notifications = useSelector(getSortedAnnouncementsToShow);
|
|
const locale = useSelector(getCurrentLocale);
|
|
|
|
const [seenNotifications, setSeenNotifications] = useState({});
|
|
const [shouldShowScrollButton, setShouldShowScrollButton] = useState(true);
|
|
|
|
const popoverRef = useRef();
|
|
|
|
const memoizedNotifications = useEqualityCheck(notifications);
|
|
const idRefMap = useMemo(
|
|
() =>
|
|
memoizedNotifications.reduce(
|
|
(_idRefMap, notification) => ({
|
|
..._idRefMap,
|
|
[notification.id]: React.createRef(),
|
|
}),
|
|
{},
|
|
),
|
|
[memoizedNotifications],
|
|
);
|
|
|
|
const trackEvent = useContext(MetaMetricsContext);
|
|
|
|
const handleDebouncedScroll = debounce((target) => {
|
|
setShouldShowScrollButton(
|
|
target.scrollHeight - target.scrollTop !== target.clientHeight,
|
|
);
|
|
}, 100);
|
|
|
|
const handleScroll = (e) => {
|
|
handleDebouncedScroll(e.target);
|
|
};
|
|
|
|
const handleScrollDownClick = (e) => {
|
|
e.stopPropagation();
|
|
idRefMap[notifications[notifications.length - 1].id].current.scrollIntoView(
|
|
{
|
|
behavior: 'smooth',
|
|
},
|
|
);
|
|
};
|
|
useEffect(() => {
|
|
const observer = new window.IntersectionObserver(
|
|
(entries, _observer) => {
|
|
entries.forEach((entry) => {
|
|
if (entry.isIntersecting) {
|
|
const [id, ref] = Object.entries(idRefMap).find(([_, _ref]) =>
|
|
_ref.current.isSameNode(entry.target),
|
|
);
|
|
|
|
setSeenNotifications((_seenNotifications) => ({
|
|
..._seenNotifications,
|
|
[id]: true,
|
|
}));
|
|
|
|
_observer.unobserve(ref.current);
|
|
}
|
|
});
|
|
},
|
|
{
|
|
root: popoverRef.current,
|
|
threshold: 1.0,
|
|
},
|
|
);
|
|
|
|
Object.values(idRefMap).forEach((ref) => {
|
|
observer.observe(ref.current);
|
|
});
|
|
|
|
return () => {
|
|
observer.disconnect();
|
|
};
|
|
}, [idRefMap, setSeenNotifications]);
|
|
|
|
// Display the swaps notification with full image
|
|
// Displays the NFTs & OpenSea notifications 18,19 with full image
|
|
const notificationRenderers = {
|
|
0: renderFirstNotification,
|
|
1: renderFirstNotification,
|
|
18: renderFirstNotification,
|
|
19: renderFirstNotification,
|
|
21: renderFirstNotification,
|
|
};
|
|
|
|
return (
|
|
<Popover
|
|
title={t('whatsNew')}
|
|
headerProps={{ padding: [4, 4, 4] }}
|
|
className="whats-new-popup__popover"
|
|
onClose={() => {
|
|
updateViewedNotifications(seenNotifications);
|
|
trackEvent({
|
|
category: MetaMetricsEventCategory.Home,
|
|
event: MetaMetricsEventName.WhatsNewViewed,
|
|
properties: {
|
|
number_viewed: Object.keys(seenNotifications).pop(),
|
|
completed_all: true,
|
|
},
|
|
});
|
|
onClose();
|
|
}}
|
|
popoverRef={popoverRef}
|
|
showScrollDown={shouldShowScrollButton && notifications.length > 1}
|
|
onScrollDownButtonClick={handleScrollDownClick}
|
|
onScroll={handleScroll}
|
|
>
|
|
<div className="whats-new-popup__notifications">
|
|
{notifications.map(({ id }, index) => {
|
|
const notification = getTranslatedUINotifications(t, locale)[id];
|
|
const isLast = index === notifications.length - 1;
|
|
// Choose the appropriate rendering function based on the id
|
|
let renderNotification =
|
|
notificationRenderers[id] || renderSubsequentNotification;
|
|
|
|
///: BEGIN:ONLY_INCLUDE_IN(build-mmi)
|
|
renderNotification = renderFirstNotification;
|
|
///: END:ONLY_INCLUDE_IN
|
|
|
|
return renderNotification({
|
|
notification,
|
|
idRefMap,
|
|
history,
|
|
isLast,
|
|
trackEvent,
|
|
///: BEGIN:ONLY_INCLUDE_IN(build-mmi)
|
|
mmiPortfolioUrl,
|
|
seenNotifications,
|
|
onClose,
|
|
///: END:ONLY_INCLUDE_IN
|
|
});
|
|
})}
|
|
</div>
|
|
</Popover>
|
|
);
|
|
}
|
|
|
|
WhatsNewPopup.propTypes = {
|
|
onClose: PropTypes.func.isRequired,
|
|
///: BEGIN:ONLY_INCLUDE_IN(build-mmi)
|
|
mmiPortfolioUrl: PropTypes.string.isRequired,
|
|
///: END:ONLY_INCLUDE_IN
|
|
};
|