1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-11-22 09:57:02 +01:00

UX Multichain: Added product tour component (#18571)

* adding product tour component

* updated control for prevIcon

* updated app-header and product tour

* updated css

* updated message strings

* updated tests

* removed console statement

* added selector for product tour

* updated test

* updated test

* updated state with steps

* Update ui/components/multichain/product-tour-popover/product-tour-popover.js

Co-authored-by: Garrett Bear <gwhisten@gmail.com>

* Update ui/components/multichain/product-tour-popover/product-tour-popover.js

Co-authored-by: Garrett Bear <gwhisten@gmail.com>

* Update ui/components/multichain/product-tour-popover/product-tour-popover.js

Co-authored-by: Garrett Bear <gwhisten@gmail.com>

* Update ui/components/multichain/product-tour-popover/product-tour-popover.js

Co-authored-by: Garrett Bear <gwhisten@gmail.com>

* Update ui/components/multichain/product-tour-popover/product-tour-popover.js

Co-authored-by: Garrett Bear <gwhisten@gmail.com>

* Update ui/components/multichain/product-tour-popover/product-tour-popover.js

Co-authored-by: Garrett Bear <gwhisten@gmail.com>

* Update ui/components/multichain/product-tour-popover/product-tour-popover.js

Co-authored-by: Garrett Bear <gwhisten@gmail.com>

* Update ui/components/multichain/product-tour-popover/product-tour-popover.js

Co-authored-by: Garrett Bear <gwhisten@gmail.com>

* Update ui/components/multichain/product-tour-popover/product-tour-popover.js

Co-authored-by: Garrett Bear <gwhisten@gmail.com>

* Update ui/components/multichain/product-tour-popover/product-tour-popover.scss

Co-authored-by: Garrett Bear <gwhisten@gmail.com>

* fixed lint errors

* updated lint error

* added changes for rtl support

* added changes for rtl support

* fixed lint errors

* Some suggestions (#18676)

* updated messages and indentation

* fixed popup close on my final step

* updated rtl classname condition

---------

Co-authored-by: Garrett Bear <gwhisten@gmail.com>
Co-authored-by: George Marshall <george.marshall@consensys.net>
This commit is contained in:
Nidhi Kumari 2023-04-21 20:58:18 +05:30 committed by GitHub
parent b467fbc07b
commit 0efd00b755
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 585 additions and 127 deletions

View File

@ -1636,6 +1636,12 @@
"general": { "general": {
"message": "General" "message": "General"
}, },
"globalTitle": {
"message": "Global menu"
},
"globalTourDescription": {
"message": "See your portfolio, connected sites, settings, and more"
},
"goBack": { "goBack": {
"message": "Go back" "message": "Go back"
}, },
@ -3052,6 +3058,12 @@
"permissions": { "permissions": {
"message": "Permissions" "message": "Permissions"
}, },
"permissionsTitle": {
"message": "Permissions"
},
"permissionsTourDescription": {
"message": "Find your connected accounts and manage permissions here"
},
"personalAddressDetected": { "personalAddressDetected": {
"message": "Personal address detected. Input the token contract address." "message": "Personal address detected. Input the token contract address."
}, },
@ -4279,6 +4291,12 @@
"switchedTo": { "switchedTo": {
"message": "You have switched to" "message": "You have switched to"
}, },
"switcherTitle": {
"message": "Network switcher"
},
"switcherTourDescription": {
"message": "Click the icon to switch networks or add a new network"
},
"switchingNetworksCancelsPendingConfirmations": { "switchingNetworksCancelsPendingConfirmations": {
"message": "Switching networks will cancel all pending confirmations" "message": "Switching networks will cancel all pending confirmations"
}, },

View File

@ -46,6 +46,7 @@ export default class AppStateController extends EventEmitter {
nftsDetectionNoticeDismissed: false, nftsDetectionNoticeDismissed: false,
showTestnetMessageInDropdown: true, showTestnetMessageInDropdown: true,
showBetaHeader: isBeta(), showBetaHeader: isBeta(),
showProductTour: true,
trezorModel: null, trezorModel: null,
currentPopupId: undefined, currentPopupId: undefined,
...initState, ...initState,
@ -331,6 +332,15 @@ export default class AppStateController extends EventEmitter {
this.store.updateState({ showBetaHeader }); this.store.updateState({ showBetaHeader });
} }
/**
* Sets whether the product tour should be shown
*
* @param showProductTour
*/
setShowProductTour(showProductTour) {
this.store.updateState({ showProductTour });
}
/** /**
* Sets a property indicating the model of the user's Trezor hardware wallet * Sets a property indicating the model of the user's Trezor hardware wallet
* *

View File

@ -2061,6 +2061,8 @@ export default class MetamaskController extends EventEmitter {
), ),
setShowBetaHeader: setShowBetaHeader:
appStateController.setShowBetaHeader.bind(appStateController), appStateController.setShowBetaHeader.bind(appStateController),
setShowProductTour:
appStateController.setShowProductTour.bind(appStateController),
updateNftDropDownState: updateNftDropDownState:
appStateController.updateNftDropDownState.bind(appStateController), appStateController.updateNftDropDownState.bind(appStateController),
setFirstTimeUsedNetwork: setFirstTimeUsedNetwork:

View File

@ -19,44 +19,51 @@ import { Text } from '../text';
import { AVATAR_BASE_SIZES } from './avatar-base.constants'; import { AVATAR_BASE_SIZES } from './avatar-base.constants';
export const AvatarBase = ({ export const AvatarBase = React.forwardRef(
size = AVATAR_BASE_SIZES.MD, (
children, {
backgroundColor = BackgroundColor.backgroundAlternative, size = AVATAR_BASE_SIZES.MD,
borderColor = BorderColor.borderDefault, children,
color = TextColor.textDefault, backgroundColor = BackgroundColor.backgroundAlternative,
className, borderColor = BorderColor.borderDefault,
...props color = TextColor.textDefault,
}) => { className,
let fallbackTextVariant; ...props
},
ref,
) => {
let fallbackTextVariant;
if (size === AVATAR_BASE_SIZES.LG || size === AVATAR_BASE_SIZES.XL) {
fallbackTextVariant = TextVariant.bodyLgMedium;
} else if (size === AVATAR_BASE_SIZES.SM || size === AVATAR_BASE_SIZES.MD) {
fallbackTextVariant = TextVariant.bodySm;
} else {
fallbackTextVariant = TextVariant.bodyXs;
}
return (
<Text
className={classnames(
'mm-avatar-base',
`mm-avatar-base--size-${size}`,
className,
)}
ref={ref}
as="div"
display={DISPLAY.FLEX}
justifyContent={JustifyContent.center}
alignItems={AlignItems.center}
borderRadius={BorderRadius.full}
variant={fallbackTextVariant}
textTransform={TEXT_TRANSFORM.UPPERCASE}
{...{ backgroundColor, borderColor, color, ...props }}
>
{children}
</Text>
);
},
);
if (size === AVATAR_BASE_SIZES.LG || size === AVATAR_BASE_SIZES.XL) {
fallbackTextVariant = TextVariant.bodyLgMedium;
} else if (size === AVATAR_BASE_SIZES.SM || size === AVATAR_BASE_SIZES.MD) {
fallbackTextVariant = TextVariant.bodySm;
} else {
fallbackTextVariant = TextVariant.bodyXs;
}
return (
<Text
className={classnames(
'mm-avatar-base',
`mm-avatar-base--size-${size}`,
className,
)}
as="div"
display={DISPLAY.FLEX}
justifyContent={JustifyContent.center}
alignItems={AlignItems.center}
borderRadius={BorderRadius.full}
variant={fallbackTextVariant}
textTransform={TEXT_TRANSFORM.UPPERCASE}
{...{ backgroundColor, borderColor, color, ...props }}
>
{children}
</Text>
);
};
AvatarBase.propTypes = { AvatarBase.propTypes = {
/** /**
* The size of the AvatarBase. * The size of the AvatarBase.
@ -95,3 +102,5 @@ AvatarBase.propTypes = {
*/ */
...Text.propTypes, ...Text.propTypes,
}; };
AvatarBase.displayName = 'AvatarBase';

View File

@ -14,70 +14,76 @@ import {
} from '../../../helpers/constants/design-system'; } from '../../../helpers/constants/design-system';
import { AVATAR_NETWORK_SIZES } from './avatar-network.constants'; import { AVATAR_NETWORK_SIZES } from './avatar-network.constants';
export const AvatarNetwork = ({ export const AvatarNetwork = React.forwardRef(
size = Size.MD, (
name, {
src, size = Size.MD,
showHalo, name,
color = TextColor.textDefault, src,
backgroundColor = BackgroundColor.backgroundAlternative, showHalo,
borderColor = BorderColor.transparent, color = TextColor.textDefault,
className, backgroundColor = BackgroundColor.backgroundAlternative,
...props borderColor = BorderColor.transparent,
}) => { className,
const [showFallback, setShowFallback] = useState(false); ...props
},
ref,
) => {
const [showFallback, setShowFallback] = useState(false);
useEffect(() => { useEffect(() => {
setShowFallback(!src); setShowFallback(!src);
}, [src]); }, [src]);
const fallbackString = name && name[0] ? name[0] : '?'; const fallbackString = name && name[0] ? name[0] : '?';
const handleOnError = () => { const handleOnError = () => {
setShowFallback(true); setShowFallback(true);
}; };
return ( return (
<AvatarBase <AvatarBase
size={size} ref={ref}
display={DISPLAY.FLEX} size={size}
alignItems={AlignItems.center} display={DISPLAY.FLEX}
justifyContent={JustifyContent.center} alignItems={AlignItems.center}
className={classnames( justifyContent={JustifyContent.center}
'mm-avatar-network', className={classnames(
showHalo && 'mm-avatar-network--with-halo', 'mm-avatar-network',
className, showHalo && 'mm-avatar-network--with-halo',
)} className,
{...{ backgroundColor, borderColor, color, ...props }} )}
> {...{ backgroundColor, borderColor, color, ...props }}
{showFallback ? ( >
fallbackString {showFallback ? (
) : ( fallbackString
<> ) : (
{showHalo && ( <>
{showHalo && (
<img
src={src}
className={
showHalo ? 'mm-avatar-network__network-image--blurred' : ''
}
aria-hidden="true"
/>
)}
<img <img
src={src}
className={ className={
showHalo ? 'mm-avatar-network__network-image--blurred' : '' showHalo
? 'mm-avatar-network__network-image--size-reduced'
: 'mm-avatar-network__network-image'
} }
aria-hidden="true" onError={handleOnError}
src={src}
alt={`${name} logo` || 'network logo'}
/> />
)} </>
<img )}
className={ </AvatarBase>
showHalo );
? 'mm-avatar-network__network-image--size-reduced' },
: 'mm-avatar-network__network-image' );
}
onError={handleOnError}
src={src}
alt={`${name} logo` || 'network logo'}
/>
</>
)}
</AvatarBase>
);
};
AvatarNetwork.propTypes = { AvatarNetwork.propTypes = {
/** /**
@ -123,3 +129,5 @@ AvatarNetwork.propTypes = {
*/ */
...Box.propTypes, ...Box.propTypes,
}; };
AvatarNetwork.displayName = 'AvatarNetwork';

View File

@ -23,7 +23,6 @@ import {
} from '../../../helpers/constants/design-system'; } from '../../../helpers/constants/design-system';
import { import {
AvatarNetwork, AvatarNetwork,
Button,
ButtonIcon, ButtonIcon,
IconName, IconName,
PickerNetwork, PickerNetwork,
@ -31,29 +30,41 @@ import {
import { import {
getCurrentNetwork, getCurrentNetwork,
getOnboardedInThisUISession,
getOriginOfCurrentTab, getOriginOfCurrentTab,
getSelectedIdentity, getSelectedIdentity,
getShowProductTour,
} from '../../../selectors'; } from '../../../selectors';
import { GlobalMenu, AccountPicker } from '..'; import { GlobalMenu, ProductTour, AccountPicker } from '..';
import Box from '../../ui/box/box'; import Box from '../../ui/box/box';
import { toggleAccountMenu, toggleNetworkMenu } from '../../../store/actions'; import {
hideProductTour,
toggleAccountMenu,
toggleNetworkMenu,
} from '../../../store/actions';
import MetafoxLogo from '../../ui/metafox-logo'; import MetafoxLogo from '../../ui/metafox-logo';
import { getEnvironmentType } from '../../../../app/scripts/lib/util'; import { getEnvironmentType } from '../../../../app/scripts/lib/util';
import { ENVIRONMENT_TYPE_POPUP } from '../../../../shared/constants/app'; import { ENVIRONMENT_TYPE_POPUP } from '../../../../shared/constants/app';
import ConnectedStatusIndicator from '../../app/connected-status-indicator'; import ConnectedStatusIndicator from '../../app/connected-status-indicator';
import { useI18nContext } from '../../../hooks/useI18nContext';
import { getCompletedOnboarding } from '../../../ducks/metamask/metamask';
export const AppHeader = ({ onClick }) => { export const AppHeader = ({ onClick }) => {
const trackEvent = useContext(MetaMetricsContext); const trackEvent = useContext(MetaMetricsContext);
const [accountOptionsMenuOpen, setAccountOptionsMenuOpen] = useState(false); const [accountOptionsMenuOpen, setAccountOptionsMenuOpen] = useState(false);
const [multichainProductTourStep, setMultichainProductTourStep] = useState(1);
const menuRef = useRef(false); const menuRef = useRef(false);
const origin = useSelector(getOriginOfCurrentTab); const origin = useSelector(getOriginOfCurrentTab);
const history = useHistory(); const history = useHistory();
const isUnlocked = useSelector((state) => state.metamask.isUnlocked); const isUnlocked = useSelector((state) => state.metamask.isUnlocked);
const t = useI18nContext();
// Used for account picker // Used for account picker
const identity = useSelector(getSelectedIdentity); const identity = useSelector(getSelectedIdentity);
const dispatch = useDispatch(); const dispatch = useDispatch();
const completedOnboarding = useSelector(getCompletedOnboarding);
const onboardedInThisUISession = useSelector(getOnboardedInThisUISession);
const showProductTourPopup = useSelector(getShowProductTour);
// Used for network icon / dropdown // Used for network icon / dropdown
const currentNetwork = useSelector(getCurrentNetwork); const currentNetwork = useSelector(getCurrentNetwork);
@ -64,12 +75,17 @@ export const AppHeader = ({ onClick }) => {
getEnvironmentType() === ENVIRONMENT_TYPE_POPUP && getEnvironmentType() === ENVIRONMENT_TYPE_POPUP &&
origin && origin &&
origin !== browser.runtime.id; origin !== browser.runtime.id;
const showProductTour =
completedOnboarding && !onboardedInThisUISession && showProductTourPopup;
const productTourDirection = document
.querySelector('[dir]')
?.getAttribute('dir');
return ( return (
<> <>
{isUnlocked && !popupStatus ? ( {isUnlocked && !popupStatus ? (
<Box <Box
display={DISPLAY.FLEX} display={[DISPLAY.NONE, DISPLAY.FLEX]}
alignItems={AlignItems.center} alignItems={AlignItems.center}
margin={2} margin={2}
className="multichain-app-header-logo" className="multichain-app-header-logo"
@ -112,25 +128,43 @@ export const AppHeader = ({ onClick }) => {
padding={2} padding={2}
gap={2} gap={2}
> >
{popupStatus ? ( <AvatarNetwork
<Button margin={2}
className="multichain-app-header__contents--avatar-network" className="multichain-app-header__contents--avatar-network"
justifyContent={JustifyContent.flexStart} ref={menuRef}
> as="button"
<AvatarNetwork aria-label="Network Menu" // TODO: needs locale
name={currentNetwork?.nickname} padding={0}
src={currentNetwork?.rpcPrefs?.imageUrl} name={currentNetwork?.nickname}
size={Size.SM} src={currentNetwork?.rpcPrefs?.imageUrl}
onClick={() => dispatch(toggleNetworkMenu())} size={Size.SM}
/> onClick={() => dispatch(toggleNetworkMenu())}
</Button> display={[DISPLAY.FLEX, DISPLAY.NONE]} // show on popover hide on desktop
) : ( />
<PickerNetwork <PickerNetwork
label={currentNetwork?.nickname} margin={2}
src={currentNetwork?.rpcPrefs?.imageUrl} label={currentNetwork?.nickname}
onClick={() => dispatch(toggleNetworkMenu())} src={currentNetwork?.rpcPrefs?.imageUrl}
onClick={() => dispatch(toggleNetworkMenu())}
display={[DISPLAY.NONE, DISPLAY.FLEX]} // show on desktop hide on popover
/>
{showProductTour &&
popupStatus &&
multichainProductTourStep === 1 ? (
<ProductTour
className="multichain-app-header__product-tour"
anchorElement={menuRef.current}
title={t('switcherTitle')}
description={t('switcherTourDescription')}
currentStep="1"
totalSteps="3"
onClick={() =>
setMultichainProductTourStep(multichainProductTourStep + 1)
}
positionObj={productTourDirection === 'rtl' ? '0%' : '88%'}
productTourDirection={productTourDirection}
/> />
)} ) : null}
<AccountPicker <AccountPicker
address={identity.address} address={identity.address}
@ -143,8 +177,34 @@ export const AppHeader = ({ onClick }) => {
justifyContent={JustifyContent.spaceBetween} justifyContent={JustifyContent.spaceBetween}
> >
{showStatus ? ( {showStatus ? (
<ConnectedStatusIndicator <Box ref={menuRef}>
onClick={() => history.push(CONNECTED_ACCOUNTS_ROUTE)} <ConnectedStatusIndicator
onClick={() => history.push(CONNECTED_ACCOUNTS_ROUTE)}
/>
</Box>
) : null}{' '}
{popupStatus && multichainProductTourStep === 2 ? (
<ProductTour
className="multichain-app-header__product-tour"
anchorElement={menuRef.current}
closeMenu={() => setAccountOptionsMenuOpen(false)}
prevIcon
title={t('permissionsTitle')}
description={t('permissionsTourDescription')}
currentStep="2"
totalSteps="3"
prevClick={() =>
setMultichainProductTourStep(
multichainProductTourStep - 1,
)
}
onClick={() =>
setMultichainProductTourStep(
multichainProductTourStep + 1,
)
}
positionObj={productTourDirection === 'rtl' ? '74%' : '12%'}
productTourDirection={productTourDirection}
/> />
) : null} ) : null}
<Box <Box
@ -176,6 +236,28 @@ export const AppHeader = ({ onClick }) => {
closeMenu={() => setAccountOptionsMenuOpen(false)} closeMenu={() => setAccountOptionsMenuOpen(false)}
/> />
) : null} ) : null}
{showProductTour &&
popupStatus &&
multichainProductTourStep === 3 ? (
<ProductTour
className="multichain-app-header__product-tour"
anchorElement={menuRef.current}
closeMenu={() => setAccountOptionsMenuOpen(false)}
prevIcon
title={t('globalTitle')}
description={t('globalTourDescription')}
currentStep="3"
totalSteps="3"
prevClick={() =>
setMultichainProductTourStep(multichainProductTourStep - 1)
}
onClick={() => {
hideProductTour();
}}
positionObj={productTourDirection === 'rtl' ? '89%' : '0%'}
productTourDirection={productTourDirection}
/>
) : null}
</Box> </Box>
) : ( ) : (
<Box <Box

View File

@ -30,15 +30,7 @@
} }
&--avatar-network { &--avatar-network {
background-color: transparent; padding: 0; // TODO: Remove once https://github.com/MetaMask/metamask-extension/pull/17006 is merged
width: min-content;
padding: 8px;
&:hover,
&:active {
box-shadow: none;
background: transparent;
}
} }
} }

View File

@ -97,6 +97,9 @@ describe('App Header', () => {
}, },
}, },
}, },
appState: {
onboardedInThisUISession: false,
},
}; };
const mockStore = configureStore(); const mockStore = configureStore();

View File

@ -11,3 +11,4 @@ export { AddressCopyButton } from './address-copy-button';
export { MultichainConnectedSiteMenu } from './multichain-connected-site-menu'; export { MultichainConnectedSiteMenu } from './multichain-connected-site-menu';
export { NetworkListItem } from './network-list-item'; export { NetworkListItem } from './network-list-item';
export { NetworkListMenu } from './network-list-menu'; export { NetworkListMenu } from './network-list-menu';
export { ProductTour } from './product-tour-popover';

View File

@ -14,3 +14,4 @@
@import 'multichain-token-list-item/multichain-token-list-item'; @import 'multichain-token-list-item/multichain-token-list-item';
@import 'network-list-item/'; @import 'network-list-item/';
@import 'network-list-menu/'; @import 'network-list-menu/';
@import 'product-tour-popover/product-tour-popover';

View File

@ -0,0 +1 @@
export { ProductTour } from './product-tour-popover';

View File

@ -0,0 +1,179 @@
import React from 'react';
import classnames from 'classnames';
import PropTypes from 'prop-types';
import Box from '../../ui/box/box';
import {
AlignItems,
BLOCK_SIZES,
BorderRadius,
BackgroundColor,
DISPLAY,
IconColor,
JustifyContent,
Size,
TextColor,
TextVariant,
TextAlign,
} from '../../../helpers/constants/design-system';
import {
ButtonBase,
ButtonIcon,
IconName,
Text,
} from '../../component-library';
import { useI18nContext } from '../../../hooks/useI18nContext';
import { Menu } from '../../ui/menu';
export const ProductTour = ({
className,
prevIcon,
title,
description,
currentStep,
totalSteps,
positionObj = '5%',
closeMenu,
anchorElement,
onClick,
prevClick,
productTourDirection,
...props
}) => {
const t = useI18nContext();
return (
<Menu
className={classnames(
'multichain-product-tour-menu',
{
'multichain-product-tour-menu--rtl': productTourDirection === 'rtl',
},
className,
)}
anchorElement={anchorElement}
onHide={closeMenu}
data-testid="multichain-product-tour-menu-popover"
{...props}
>
<Box
className="multichain-product-tour-menu__container"
backgroundColor={BackgroundColor.infoDefault}
borderRadius={BorderRadius.LG}
padding={4}
>
<Box
borderWidth={1}
className="multichain-product-tour-menu__arrow"
display={DISPLAY.FLEX}
justifyContent={JustifyContent.center}
alignItems={AlignItems.center}
style={{ right: positionObj }}
/>
<Box
display={DISPLAY.FLEX}
alignItems={AlignItems.center}
className="multichain-product-tour-menu__header"
>
{prevIcon ? (
<ButtonIcon
iconName={IconName.ArrowLeft}
size={Size.SM}
color={IconColor.infoInverse}
onClick={prevClick}
className="multichain-product-tour-menu__previous-icon"
data-testid="multichain-product-tour-menu-popover-prevIcon"
/>
) : null}
<Text
textAlign={TextAlign.Center}
variant={TextVariant.headingSm}
width={BLOCK_SIZES.FULL}
color={TextColor.infoInverse}
>
{title}
</Text>
</Box>
<Text
paddingBottom={2}
paddingTop={2}
color={TextColor.infoInverse}
variant={TextVariant.bodyMd}
>
{description}
</Text>
<Box
display={DISPLAY.FLEX}
alignItems={AlignItems.center}
justifyContent={JustifyContent.spaceBetween}
>
<Text
paddingBottom={2}
paddingTop={2}
color={TextColor.infoInverse}
variant={TextVariant.bodyMd}
>
{currentStep}/{totalSteps}
</Text>
<ButtonBase
backgroundColor={BackgroundColor.primaryInverse}
color={TextColor.primaryDefault}
className="multichain-product-tour-menu__button"
onClick={onClick}
>
{t('recoveryPhraseReminderConfirm')}
</ButtonBase>
</Box>
</Box>
</Menu>
);
};
ProductTour.propTypes = {
/**
* The element that the menu should display next to
*/
anchorElement: PropTypes.instanceOf(window.Element),
/**
* Function that closes this menu
*/
closeMenu: PropTypes.func.isRequired,
/**
* Additional classNames to be added
*/
className: PropTypes.string,
/**
* Boolean to decide whether to show prevIcon or not
*/
prevIcon: PropTypes.bool,
/**
* Title of the popover
*/
title: PropTypes.string,
/**
* Description of the popover
*/
description: PropTypes.string,
/**
* Current step in the product tour
*/
currentStep: PropTypes.string,
/**
* Total steps in the product tour
*/
totalSteps: PropTypes.string,
/**
* PositionObj to decide the position of the popover tip
*/
positionObj: PropTypes.string,
/**
* The onClick handler to be passed
*/
onClick: PropTypes.func,
/**
* The handler to be passed to prevIcon
*/
prevClick: PropTypes.func,
/**
* Direction to determine the css for menu component
*/
productTourDirection: PropTypes.string,
};

View File

@ -0,0 +1,57 @@
.multichain-product-tour-menu {
width: 344px;
box-shadow: none;
left: -7px !important;
top: 10px !important; //important is required here since Menu has absolute position added via inline style in base component.
&--rtl {
left: 6px !important;
right: 6px !important;
}
&__arrow,
&__arrow::before {
position: absolute;
width: 12px;
height: 12px;
background: inherit;
}
&__arrow {
width: 40px;
height: 40px;
visibility: hidden;
top: 0;
}
&__arrow::before {
display: block;
background-color: inherit;
visibility: visible;
content: '';
transform: rotate(45deg);
border-radius: 2px 0 0 0;
top: -7px;
}
&__header {
position: relative;
}
&__previous-icon {
position: absolute;
left: 0;
top: 0;
}
&__button {
&:hover {
color: var(--color-primary-default);
}
&:active {
opacity: 0.5;
background-color: var(--color-primary-inverse);
}
}
}

View File

@ -0,0 +1,54 @@
import React from 'react';
import { ProductTour } from './product-tour-popover';
export default {
title: 'Components/Multichain/ProductTour',
component: ProductTour,
argTypes: {
prevIcon: {
control: 'boolean',
},
title: {
control: 'text',
},
description: {
control: 'text',
},
currentStep: {
control: 'text',
},
totalSteps: {
control: 'text',
},
positionObj: {
control: 'text',
},
onClick: {
action: 'onClick',
},
onHide: {
action: 'onHide',
},
closeMenu: {
action: 'closeMenu',
},
},
args: {
prevIcon: true,
title: 'Permissions',
description: 'Find your connected accounts and manage permissions here.',
currentStep: '1',
totalSteps: '3',
},
};
const Template = (args) => {
return <ProductTour {...args} />;
};
export const DefaultStory = Template.bind({});
export const CustomPopoverTipPosition = Template.bind({});
CustomPopoverTipPosition.args = {
positionObj: '80%',
};

View File

@ -0,0 +1,30 @@
import React from 'react';
import { render } from '@testing-library/react';
import { ProductTour } from './product-tour-popover';
describe('DetectedTokensBanner', () => {
const props = {
title: 'Permissions',
description: 'Find your connected accounts and manage permissions here.',
currentStep: '1',
totalSteps: '3',
};
it('should render correctly', () => {
const { getByTestId } = render(
<ProductTour anchorElement={document.body} {...props} />,
);
const menuContainer = getByTestId('multichain-product-tour-menu-popover');
expect(menuContainer).toBeInTheDocument();
});
it('should render prev Icon', () => {
const { getByTestId } = render(
<ProductTour anchorElement={document.body} {...props} prevIcon />,
);
const prevIcon = getByTestId(
'multichain-product-tour-menu-popover-prevIcon',
);
expect(prevIcon).toBeInTheDocument();
});
});

View File

@ -1041,6 +1041,9 @@ export function getShowBetaHeader(state) {
return state.metamask.showBetaHeader; return state.metamask.showBetaHeader;
} }
export function getShowProductTour(state) {
return state.metamask.showProductTour;
}
/** /**
* To get the useTokenDetection flag which determines whether a static or dynamic token list is used * To get the useTokenDetection flag which determines whether a static or dynamic token list is used
* *
@ -1431,6 +1434,10 @@ export function getCustomTokenAmount(state) {
return state.appState.customTokenAmount; return state.appState.customTokenAmount;
} }
export function getOnboardedInThisUISession(state) {
return state.appState.onboardedInThisUISession;
}
/** /**
* To get the useCurrencyRateCheck flag which to check if the user prefers currency conversion * To get the useCurrencyRateCheck flag which to check if the user prefers currency conversion
* *

View File

@ -4560,6 +4560,10 @@ export function hideBetaHeader() {
return submitRequestToBackground('setShowBetaHeader', [false]); return submitRequestToBackground('setShowBetaHeader', [false]);
} }
export function hideProductTour() {
return submitRequestToBackground('setShowProductTour', [false]);
}
// TODO: codeword NOT_A_THUNK @brad-decker // TODO: codeword NOT_A_THUNK @brad-decker
export function setTransactionSecurityCheckEnabled( export function setTransactionSecurityCheckEnabled(
transactionSecurityCheckEnabled: boolean, transactionSecurityCheckEnabled: boolean,