1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-12-23 09:52:26 +01:00

Merge branch 'develop' of github.com:MetaMask/metamask-extension into minimal

This commit is contained in:
Matthias Kretschmann 2023-04-21 22:12:53 +01:00
commit 89ec45d6d5
Signed by: m
GPG Key ID: 606EEEF3C479A91F
43 changed files with 1350 additions and 333 deletions

17
.github/workflows/do-not-merge.yml vendored Normal file
View File

@ -0,0 +1,17 @@
# Fails the pull request if it has the "DO-NOT-MERGE" label
name: Check "DO-NOT-MERGE" label
on:
pull_request:
types: [opened, reopened, labeled, unlabeled, synchronize]
jobs:
do-not-merge:
runs-on: ubuntu-latest
if: ${{ contains(github.event.pull_request.labels.*.name, 'DO-NOT-MERGE') }}
steps:
- name: 'Check for label "DO-NOT-MERGE"'
run: |
echo 'This check fails PRs with the "DO-NOT-MERGE" label to block merging'
exit 1

View File

@ -1636,6 +1636,12 @@
"general": {
"message": "General"
},
"globalTitle": {
"message": "Global menu"
},
"globalTourDescription": {
"message": "See your portfolio, connected sites, settings, and more"
},
"goBack": {
"message": "Go back"
},
@ -2396,6 +2402,9 @@
"noNFTs": {
"message": "No NFTs yet"
},
"noReport": {
"message": "No Report"
},
"noSnaps": {
"message": "You don't have any snaps installed."
},
@ -3052,6 +3061,12 @@
"permissions": {
"message": "Permissions"
},
"permissionsTitle": {
"message": "Permissions"
},
"permissionsTourDescription": {
"message": "Find your connected accounts and manage permissions here"
},
"personalAddressDetected": {
"message": "Personal address detected. Input the token contract address."
},
@ -3233,6 +3248,12 @@
"replace": {
"message": "replace"
},
"reportLastRun": {
"message": "Report last run"
},
"reportLastRunTooltip": {
"message": "The date and time of when the last AML/CFT report was run"
},
"requestFailed": {
"message": "Request failed"
},
@ -3362,9 +3383,18 @@
"revokeSpendingCapTooltipText": {
"message": "This third party will be unable to spend any more of your current or future tokens."
},
"riskRating": {
"message": "Risk rating"
},
"riskRatingTooltip": {
"message": "The risk rating of the address you are interacting with based on your risk settings"
},
"rpcUrl": {
"message": "New RPC URL"
},
"runReport": {
"message": "Run report"
},
"safeTransferFrom": {
"message": "Safe transfer from"
},
@ -3579,6 +3609,9 @@
"showPrivateKeys": {
"message": "Show Private Keys"
},
"showReport": {
"message": "Show report"
},
"showTestnetNetworks": {
"message": "Show test networks"
},
@ -3649,7 +3682,7 @@
"description": "$1 is the dApp origin requesting the snap and $2 is the snap name"
},
"snapInstallWarningCheck": {
"message": "Ensure that the permission below align with your intended actions. Only proceed with authors you trust."
"message": "Ensure that the permission below aligns with your intended actions. Only proceed with authors you trust."
},
"snapInstallWarningCheckPlural": {
"message": "Ensure that the permissions below align with your intended actions. Only proceed with authors you trust."
@ -4279,6 +4312,12 @@
"switchedTo": {
"message": "You have switched to"
},
"switcherTitle": {
"message": "Network switcher"
},
"switcherTourDescription": {
"message": "Click the icon to switch networks or add a new network"
},
"switchingNetworksCancelsPendingConfirmations": {
"message": "Switching networks will cancel all pending confirmations"
},

View File

@ -46,6 +46,7 @@ export default class AppStateController extends EventEmitter {
nftsDetectionNoticeDismissed: false,
showTestnetMessageInDropdown: true,
showBetaHeader: isBeta(),
showProductTour: true,
trezorModel: null,
currentPopupId: undefined,
...initState,
@ -331,6 +332,15 @@ export default class AppStateController extends EventEmitter {
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
*

View File

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

View File

@ -122,100 +122,100 @@ exports[`SignatureRequestSIWE (Sign in with Ethereum) should match snapshot 1`]
class="box box--margin-top-2 box--margin-bottom-2 box--flex-direction-row"
>
<h4
class="box box--margin-top-2 box--margin-bottom-2 box--flex-direction-row typography typography--h4 typography--weight-normal typography--style-normal typography--color-text-default"
class="box mm-text mm-text--body-lg-medium box--margin-top-2 box--margin-bottom-2 box--flex-direction-row box--color-text-default"
>
Message:
</h4>
<h6
class="box box--margin-top-2 box--margin-bottom-2 box--flex-direction-row typography signature-request-siwe-message__sub-text typography--h6 typography--weight-normal typography--style-normal typography--color-text-default"
<p
class="box mm-text signature-request-siwe-message__sub-text mm-text--body-md mm-text--overflow-wrap-break-word box--margin-top-2 box--margin-bottom-2 box--flex-direction-row box--color-text-default"
>
Click to sign in and accept the Terms of Service: https://community.metamask.io/tos
</h6>
</p>
</div>
<div
class="box box--margin-top-2 box--margin-bottom-2 box--flex-direction-row"
>
<h4
class="box box--margin-top-2 box--margin-bottom-2 box--flex-direction-row typography typography--h4 typography--weight-normal typography--style-normal typography--color-text-default"
class="box mm-text mm-text--body-lg-medium box--margin-top-2 box--margin-bottom-2 box--flex-direction-row box--color-text-default"
>
URI:
</h4>
<h6
class="box box--margin-top-2 box--margin-bottom-2 box--flex-direction-row typography signature-request-siwe-message__sub-text typography--h6 typography--weight-normal typography--style-normal typography--color-text-default"
<p
class="box mm-text signature-request-siwe-message__sub-text mm-text--body-md mm-text--overflow-wrap-break-word box--margin-top-2 box--margin-bottom-2 box--flex-direction-row box--color-text-default"
>
http://localhost:8080
</h6>
</p>
</div>
<div
class="box box--margin-top-2 box--margin-bottom-2 box--flex-direction-row"
>
<h4
class="box box--margin-top-2 box--margin-bottom-2 box--flex-direction-row typography typography--h4 typography--weight-normal typography--style-normal typography--color-text-default"
class="box mm-text mm-text--body-lg-medium box--margin-top-2 box--margin-bottom-2 box--flex-direction-row box--color-text-default"
>
Version:
</h4>
<h6
class="box box--margin-top-2 box--margin-bottom-2 box--flex-direction-row typography signature-request-siwe-message__sub-text typography--h6 typography--weight-normal typography--style-normal typography--color-text-default"
<p
class="box mm-text signature-request-siwe-message__sub-text mm-text--body-md mm-text--overflow-wrap-break-word box--margin-top-2 box--margin-bottom-2 box--flex-direction-row box--color-text-default"
>
1
</h6>
</p>
</div>
<div
class="box box--margin-top-2 box--margin-bottom-2 box--flex-direction-row"
>
<h4
class="box box--margin-top-2 box--margin-bottom-2 box--flex-direction-row typography typography--h4 typography--weight-normal typography--style-normal typography--color-text-default"
class="box mm-text mm-text--body-lg-medium box--margin-top-2 box--margin-bottom-2 box--flex-direction-row box--color-text-default"
>
Chain ID:
</h4>
<h6
class="box box--margin-top-2 box--margin-bottom-2 box--flex-direction-row typography signature-request-siwe-message__sub-text typography--h6 typography--weight-normal typography--style-normal typography--color-text-default"
<p
class="box mm-text signature-request-siwe-message__sub-text mm-text--body-md mm-text--overflow-wrap-break-word box--margin-top-2 box--margin-bottom-2 box--flex-direction-row box--color-text-default"
>
1
</h6>
</p>
</div>
<div
class="box box--margin-top-2 box--margin-bottom-2 box--flex-direction-row"
>
<h4
class="box box--margin-top-2 box--margin-bottom-2 box--flex-direction-row typography typography--h4 typography--weight-normal typography--style-normal typography--color-text-default"
class="box mm-text mm-text--body-lg-medium box--margin-top-2 box--margin-bottom-2 box--flex-direction-row box--color-text-default"
>
Nonce:
</h4>
<h6
class="box box--margin-top-2 box--margin-bottom-2 box--flex-direction-row typography signature-request-siwe-message__sub-text typography--h6 typography--weight-normal typography--style-normal typography--color-text-default"
<p
class="box mm-text signature-request-siwe-message__sub-text mm-text--body-md mm-text--overflow-wrap-break-word box--margin-top-2 box--margin-bottom-2 box--flex-direction-row box--color-text-default"
>
STMt6KQMwwdOXE306
</h6>
</p>
</div>
<div
class="box box--margin-top-2 box--margin-bottom-2 box--flex-direction-row"
>
<h4
class="box box--margin-top-2 box--margin-bottom-2 box--flex-direction-row typography typography--h4 typography--weight-normal typography--style-normal typography--color-text-default"
class="box mm-text mm-text--body-lg-medium box--margin-top-2 box--margin-bottom-2 box--flex-direction-row box--color-text-default"
>
Issued At:
</h4>
<h6
class="box box--margin-top-2 box--margin-bottom-2 box--flex-direction-row typography signature-request-siwe-message__sub-text typography--h6 typography--weight-normal typography--style-normal typography--color-text-default"
<p
class="box mm-text signature-request-siwe-message__sub-text mm-text--body-md mm-text--overflow-wrap-break-word box--margin-top-2 box--margin-bottom-2 box--flex-direction-row box--color-text-default"
>
2023-03-18T21:40:40.823Z
</h6>
</p>
</div>
<div
class="box box--margin-top-2 box--margin-bottom-2 box--flex-direction-row"
>
<h4
class="box box--margin-top-2 box--margin-bottom-2 box--flex-direction-row typography typography--h4 typography--weight-normal typography--style-normal typography--color-text-default"
class="box mm-text mm-text--body-lg-medium box--margin-top-2 box--margin-bottom-2 box--flex-direction-row box--color-text-default"
>
Resources: 2
</h4>
<h6
class="box box--margin-top-2 box--margin-bottom-2 box--flex-direction-row typography signature-request-siwe-message__sub-text typography--h6 typography--weight-normal typography--style-normal typography--color-text-default"
<p
class="box mm-text signature-request-siwe-message__sub-text mm-text--body-md mm-text--overflow-wrap-break-word box--margin-top-2 box--margin-bottom-2 box--flex-direction-row box--color-text-default"
>
ipfs://Qme7ss3ARVgxv6rXqVPiikMJ8u2NLgmgszg13pYrDKEoiu
https://example.com/my-web2-claim.json
</h6>
</p>
</div>
</div>
</div>

View File

@ -8,6 +8,5 @@
&__sub-text {
white-space: pre-line;
overflow: hidden;
word-wrap: break-word;
}
}

View File

@ -1,11 +1,12 @@
import React from 'react';
import PropTypes from 'prop-types';
import Box from '../../../ui/box';
import Typography from '../../../ui/typography';
import { Text } from '../../../component-library';
import {
FLEX_DIRECTION,
TypographyVariant,
OVERFLOW_WRAP,
TextVariant,
} from '../../../../helpers/constants/design-system';
const SignatureRequestSIWEMessage = ({ data }) => {
@ -14,21 +15,22 @@ const SignatureRequestSIWEMessage = ({ data }) => {
<Box flexDirection={FLEX_DIRECTION.COLUMN}>
{data.map(({ label, value }, i) => (
<Box key={i.toString()} marginTop={2} marginBottom={2}>
<Typography
variant={TypographyVariant.H4}
<Text
as="h4"
variant={TextVariant.bodyLgMedium}
marginTop={2}
marginBottom={2}
>
{label}
</Typography>
<Typography
</Text>
<Text
className="signature-request-siwe-message__sub-text"
variant={TypographyVariant.H6}
overflowWrap={OVERFLOW_WRAP.BREAK_WORD}
marginTop={2}
marginBottom={2}
>
{value}
</Typography>
</Text>
</Box>
))}
</Box>

View File

@ -64,7 +64,15 @@ export default function SignatureRequestSIWE({
const isSIWEDomainValid = checkSIWEDomain();
const [isShowingDomainWarning, setIsShowingDomainWarning] = useState(false);
const [agreeToDomainWarning, setAgreeToDomainWarning] = useState(false);
const [hasAgreedToDomainWarning, setHasAgreedToDomainWarning] =
useState(false);
const showSecurityProviderBanner =
(txData?.securityProviderResponse?.flagAsDangerous !== undefined &&
txData?.securityProviderResponse?.flagAsDangerous !==
SECURITY_PROVIDER_MESSAGE_SEVERITIES.NOT_MALICIOUS) ||
(txData?.securityProviderResponse &&
Object.keys(txData.securityProviderResponse).length === 0);
const onSign = useCallback(
async (event) => {
@ -96,15 +104,13 @@ export default function SignatureRequestSIWE({
isSIWEDomainValid={isSIWEDomainValid}
subjectMetadata={targetSubjectMetadata}
/>
{(txData?.securityProviderResponse?.flagAsDangerous !== undefined &&
txData?.securityProviderResponse?.flagAsDangerous !==
SECURITY_PROVIDER_MESSAGE_SEVERITIES.NOT_MALICIOUS) ||
(txData?.securityProviderResponse &&
Object.keys(txData.securityProviderResponse).length === 0) ? (
{showSecurityProviderBanner && (
<SecurityProviderBannerMessage
securityProviderResponse={txData.securityProviderResponse}
/>
) : null}
)}
<Message data={formatMessageParams(parsedMessage, t)} />
{!isMatchingAddress && (
<BannerAlert
@ -165,16 +171,16 @@ export default function SignatureRequestSIWE({
onSubmit={onSign}
submitText={t('confirm')}
submitButtonType="danger-primary"
disabled={!agreeToDomainWarning}
disabled={!hasAgreedToDomainWarning}
/>
}
>
<div className="signature-request-siwe__warning-popover__checkbox-wrapper">
<Checkbox
id="signature-request-siwe_domain-checkbox"
checked={agreeToDomainWarning}
checked={hasAgreedToDomainWarning}
className="signature-request-siwe__warning-popover__checkbox-wrapper__checkbox"
onClick={() => setAgreeToDomainWarning((checked) => !checked)}
onClick={() => setHasAgreedToDomainWarning((checked) => !checked)}
/>
<label
className="signature-request-siwe__warning-popover__checkbox-wrapper__label"

View File

@ -13,32 +13,38 @@ import {
AvatarAccountSize,
} from './avatar-account.types';
export const AvatarAccount = ({
size = AvatarAccountSize.Md,
address,
className,
variant = AvatarAccountVariant.Jazzicon,
...props
}) => (
<AvatarBase
size={size}
className={classnames('mm-avatar-account', className)}
{...props}
>
{variant === AvatarAccountVariant.Jazzicon ? (
<Jazzicon
className={classnames('mm-avatar-account__jazzicon')}
address={address}
diameter={AvatarAccountDiameter[size]}
/>
) : (
<BlockieIdenticon
address={address}
diameter={AvatarAccountDiameter[size]}
borderRadius="50%"
/>
)}
</AvatarBase>
export const AvatarAccount = React.forwardRef(
(
{
size = AvatarAccountSize.Md,
address,
className,
variant = AvatarAccountVariant.Jazzicon,
...props
},
ref,
) => (
<AvatarBase
ref={ref}
size={size}
className={classnames('mm-avatar-account', className)}
{...props}
>
{variant === AvatarAccountVariant.Jazzicon ? (
<Jazzicon
className={classnames('mm-avatar-account__jazzicon')}
address={address}
diameter={AvatarAccountDiameter[size]}
/>
) : (
<BlockieIdenticon
address={address}
diameter={AvatarAccountDiameter[size]}
borderRadius="50%"
/>
)}
</AvatarBase>
),
);
AvatarAccount.propTypes = {
@ -66,3 +72,5 @@ AvatarAccount.propTypes = {
*/
...Box.propTypes,
};
AvatarAccount.displayName = 'AvatarAccount';

View File

@ -112,4 +112,15 @@ describe('AvatarAccount', () => {
'mm-avatar-base--size-xl',
);
});
it('should forward a ref to the root html element', () => {
const ref = React.createRef();
render(
<AvatarAccount
address="0x5CfE73b6021E818B776b421B1c4Db2474086a7e1"
ref={ref}
/>,
);
expect(ref.current).not.toBeNull();
expect(ref.current.nodeName).toBe('DIV');
});
});

View File

@ -19,44 +19,51 @@ import { Text } from '../text';
import { AVATAR_BASE_SIZES } from './avatar-base.constants';
export const AvatarBase = ({
size = AVATAR_BASE_SIZES.MD,
children,
backgroundColor = BackgroundColor.backgroundAlternative,
borderColor = BorderColor.borderDefault,
color = TextColor.textDefault,
className,
...props
}) => {
let fallbackTextVariant;
export const AvatarBase = React.forwardRef(
(
{
size = AVATAR_BASE_SIZES.MD,
children,
backgroundColor = BackgroundColor.backgroundAlternative,
borderColor = BorderColor.borderDefault,
color = TextColor.textDefault,
className,
...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 = {
/**
* The size of the AvatarBase.
@ -95,3 +102,5 @@ AvatarBase.propTypes = {
*/
...Text.propTypes,
};
AvatarBase.displayName = 'AvatarBase';

View File

@ -121,4 +121,10 @@ describe('AvatarBase', () => {
`box--border-color-${Color.errorDefault}`,
);
});
it('should forward a ref to the root html element', () => {
const ref = React.createRef();
render(<AvatarBase ref={ref}>A</AvatarBase>);
expect(ref.current).not.toBeNull();
expect(ref.current.nodeName).toBe('DIV');
});
});

View File

@ -15,43 +15,48 @@ import {
import { useI18nContext } from '../../../hooks/useI18nContext';
import { AVATAR_FAVICON_SIZES } from './avatar-favicon.constants';
export const AvatarFavicon = ({
size = Size.MD,
src,
name = 'avatar-favicon',
className,
fallbackIconProps,
borderColor = BorderColor.transparent,
...props
}) => {
const t = useI18nContext();
return (
<AvatarBase
size={size}
display={DISPLAY.FLEX}
alignItems={AlignItems.center}
justifyContent={JustifyContent.center}
className={classnames('mm-avatar-favicon', className)}
{...{ borderColor, ...props }}
>
{src ? (
<img
className="mm-avatar-favicon__image"
src={src}
alt={t('logo', [name])}
/>
) : (
<Icon
name={IconName.Global}
color={IconColor.iconDefault}
size={size}
{...fallbackIconProps}
/>
)}
</AvatarBase>
);
};
export const AvatarFavicon = React.forwardRef(
(
{
size = Size.MD,
src,
name = 'avatar-favicon',
className,
fallbackIconProps,
borderColor = BorderColor.transparent,
...props
},
ref,
) => {
const t = useI18nContext();
return (
<AvatarBase
ref={ref}
size={size}
display={DISPLAY.FLEX}
alignItems={AlignItems.center}
justifyContent={JustifyContent.center}
className={classnames('mm-avatar-favicon', className)}
{...{ borderColor, ...props }}
>
{src ? (
<img
className="mm-avatar-favicon__image"
src={src}
alt={t('logo', [name])}
/>
) : (
<Icon
name={IconName.Global}
color={IconColor.iconDefault}
size={size}
{...fallbackIconProps}
/>
)}
</AvatarBase>
);
},
);
AvatarFavicon.propTypes = {
/**
@ -87,3 +92,5 @@ AvatarFavicon.propTypes = {
*/
...Box.propTypes,
};
AvatarFavicon.displayName = 'AvatarFavicon';

View File

@ -105,4 +105,10 @@ describe('AvatarFavicon', () => {
);
expect(getByTestId('classname')).toHaveClass('mm-avatar-favicon--test');
});
it('should forward a ref to the root html element', () => {
const ref = React.createRef();
render(<AvatarFavicon name="test" ref={ref} />);
expect(ref.current).not.toBeNull();
expect(ref.current.nodeName).toBe('DIV');
});
});

View File

@ -19,33 +19,39 @@ import { AvatarBase } from '../avatar-base';
import { AVATAR_ICON_SIZES } from './avatar-icon.constants';
export const AvatarIcon = ({
size = Size.MD,
color = TextColor.primaryDefault,
backgroundColor = BackgroundColor.primaryMuted,
className,
iconProps,
iconName,
...props
}) => (
<AvatarBase
size={size}
display={DISPLAY.FLEX}
alignItems={AlignItems.center}
justifyContent={JustifyContent.center}
color={color}
backgroundColor={backgroundColor}
borderColor={BorderColor.transparent}
className={classnames('mm-avatar-icon', className)}
{...props}
>
<Icon
color={IconColor.inherit}
name={iconName}
export const AvatarIcon = React.forwardRef(
(
{
size = Size.MD,
color = TextColor.primaryDefault,
backgroundColor = BackgroundColor.primaryMuted,
className,
iconProps,
iconName,
...props
},
ref,
) => (
<AvatarBase
ref={ref}
size={size}
{...iconProps}
/>
</AvatarBase>
display={DISPLAY.FLEX}
alignItems={AlignItems.center}
justifyContent={JustifyContent.center}
color={color}
backgroundColor={backgroundColor}
borderColor={BorderColor.transparent}
className={classnames('mm-avatar-icon', className)}
{...props}
>
<Icon
color={IconColor.inherit}
name={iconName}
size={size}
{...iconProps}
/>
</AvatarBase>
),
);
AvatarIcon.propTypes = {
@ -87,3 +93,5 @@ AvatarIcon.propTypes = {
*/
...Box.propTypes,
};
AvatarIcon.displayName = 'AvatarIcon';

View File

@ -105,4 +105,10 @@ describe('AvatarIcon', () => {
'box--background-color-success-muted',
);
});
it('should forward a ref to the root html element', () => {
const ref = React.createRef();
render(<AvatarIcon iconName={IconName.SwapHorizontal} ref={ref} />);
expect(ref.current).not.toBeNull();
expect(ref.current.nodeName).toBe('DIV');
});
});

View File

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

View File

@ -123,4 +123,10 @@ describe('AvatarNetwork', () => {
`box--border-color-${BorderColor.errorDefault}`,
);
});
it('should forward a ref to the root html element', () => {
const ref = React.createRef();
render(<AvatarNetwork ref={ref} />);
expect(ref.current).not.toBeNull();
expect(ref.current.nodeName).toBe('DIV');
});
});

View File

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

View File

@ -121,4 +121,10 @@ describe('AvatarToken', () => {
`box--border-color-${BorderColor.errorDefault}`,
);
});
it('should forward a ref to the root html element', () => {
const ref = React.createRef();
render(<AvatarToken ref={ref} />);
expect(ref.current).not.toBeNull();
expect(ref.current.nodeName).toBe('DIV');
});
});

View File

@ -0,0 +1,144 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ComplianceDetails should render correctly 1`] = `
<div>
<div
class="box compliance-details box--padding-right-4 box--padding-left-4 box--display-flex box--flex-direction-column"
>
<div
class="box compliance-details__row box--padding-top-4 box--padding-bottom-4 box--display-flex box--flex-direction-column box--justify-content-center box--height-2/3"
>
<p
class="box mm-text mm-text--body-md box--flex-direction-row box--color-text-default"
>
Address
</p>
<p
class="box mm-text mm-text--body-xs box--flex-direction-row box--color-text-default"
>
0xAddress
</p>
</div>
<div
class="box compliance-details__row box--padding-top-4 box--padding-bottom-4 box--display-flex box--flex-direction-column box--justify-content-center box--height-2/3"
>
<div
class="box box--margin-bottom-1 box--display-flex box--flex-direction-row box--align-items-center box--color-text-alternative"
>
<p
class="box mm-text mm-text--body-md box--margin-right-2 box--flex-direction-row box--color-text-default"
>
Risk rating
</p>
<div
class="info-tooltip"
>
<div>
<div
aria-describedby="tippy-tooltip-1"
class="info-tooltip__tooltip-container"
data-original-title="null"
data-tooltipped=""
style="display: inline;"
tabindex="0"
>
<svg
viewBox="0 0 10 10"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M5 0C2.2 0 0 2.2 0 5s2.2 5 5 5 5-2.2 5-5-2.2-5-5-5zm0 2c.4 0 .7.3.7.7s-.3.7-.7.7-.7-.2-.7-.6.3-.8.7-.8zm.7 6H4.3V4.3h1.5V8z"
fill="var(--color-icon-alternative)"
/>
</svg>
</div>
</div>
</div>
</div>
<div
class="box compliance-row__column-risk compliance-row__column-risk--green box--flex-direction-row"
>
<p
class="box mm-text mm-text--body-md box--flex-direction-row box--color-text-default"
>
low
</p>
</div>
</div>
<div
class="box compliance-details__row box--padding-top-4 box--padding-bottom-4 box--display-flex box--flex-direction-column box--justify-content-center box--height-2/3"
>
<div
class="box box--display-flex box--flex-direction-row box--align-items-center box--color-text-alternative"
>
<p
class="box mm-text mm-text--body-md box--margin-right-2 box--flex-direction-row box--color-text-default"
>
Report last run
</p>
<div
class="info-tooltip"
>
<div>
<div
aria-describedby="tippy-tooltip-2"
class="info-tooltip__tooltip-container"
data-original-title="null"
data-tooltipped=""
style="display: inline;"
tabindex="0"
>
<svg
viewBox="0 0 10 10"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M5 0C2.2 0 0 2.2 0 5s2.2 5 5 5 5-2.2 5-5-2.2-5-5-5zm0 2c.4 0 .7.3.7.7s-.3.7-.7.7-.7-.2-.7-.6.3-.8.7-.8zm.7 6H4.3V4.3h1.5V8z"
fill="var(--color-icon-alternative)"
/>
</svg>
</div>
</div>
</div>
</div>
<p
class="box mm-text mm-text--body-md box--flex-direction-row box--color-text-default"
/>
</div>
<div
class="box box--flex-direction-row"
>
<div
class="swaps-footer"
>
<div
class="swaps-footer__buttons swaps-footer__buttons--border"
>
<div
class="page-container__footer swaps-footer__custom-page-container-footer-class"
>
<footer>
<button
class="button btn--rounded btn-secondary page-container__footer-button page-container__footer-button__cancel swaps-footer__custom-page-container-footer-button-class"
data-testid="page-container-footer-cancel"
role="button"
tabindex="0"
>
Show report
</button>
<button
class="button btn--rounded btn-primary page-container__footer-button swaps-footer__custom-page-container-footer-button-class"
data-testid="page-container-footer-next"
role="button"
tabindex="0"
>
Run report
</button>
</footer>
</div>
</div>
</div>
</div>
</div>
</div>
`;

View File

@ -0,0 +1,159 @@
import React, { useContext, useEffect, useState } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import { I18nContext } from '../../../contexts/i18n';
import InfoTooltip from '../../ui/info-tooltip';
import SwapsFooter from '../../../pages/swaps/swaps-footer';
import {
fetchHistoricalReports,
getComplianceHistoricalReportsByAddress,
getComplianceTenantSubdomain,
} from '../../../ducks/institutional/institutional';
import { formatDate } from '../../../helpers/utils/util';
import Box from '../../ui/box';
import { Text } from '../../component-library';
import {
TextColor,
TextVariant,
JustifyContent,
AlignItems,
BLOCK_SIZES,
DISPLAY,
FLEX_DIRECTION,
} from '../../../helpers/constants/design-system';
const ComplianceDetails = ({ address, onClose, onGenerate }) => {
const t = useContext(I18nContext);
const dispatch = useDispatch();
useEffect(() => {
dispatch(fetchHistoricalReports(address));
}, [address, dispatch]);
const [lastReport, setLastReport] = useState(null);
const historicalReports = useSelector(
getComplianceHistoricalReportsByAddress(address),
);
useEffect(() => {
if (historicalReports && historicalReports.length) {
setLastReport(
historicalReports.reduce((prev, cur) =>
prev.createTime > cur.createTime ? prev : cur,
),
);
}
}, [historicalReports]);
const complianceTenantSubdomain = useSelector(getComplianceTenantSubdomain);
return (
<Box
display={DISPLAY.FLEX}
flexDirection={FLEX_DIRECTION.COLUMN}
paddingLeft={4}
paddingRight={4}
className="compliance-details"
>
<Box
display={DISPLAY.FLEX}
flexDirection={FLEX_DIRECTION.COLUMN}
justifyContent={JustifyContent.center}
height={BLOCK_SIZES.TWO_THIRDS}
paddingTop={4}
paddingBottom={4}
className="compliance-details__row"
>
<Text>{t('address')}</Text>
<Text variant={TextVariant.bodyXs}>{address}</Text>
</Box>
<Box
display={DISPLAY.FLEX}
flexDirection={FLEX_DIRECTION.COLUMN}
justifyContent={JustifyContent.center}
height={BLOCK_SIZES.TWO_THIRDS}
paddingTop={4}
paddingBottom={4}
className="compliance-details__row"
>
<Box
display={DISPLAY.FLEX}
alignItems={AlignItems.center}
marginBottom={1}
color={TextColor.textAlternative}
>
<Text marginRight={2}>{t('riskRating')}</Text>
<InfoTooltip
position="bottom"
contentText={<span>{t('riskRatingTooltip')}</span>}
/>
</Box>
<Box
className={classnames('compliance-row__column-risk', {
'compliance-row__column-risk--green': lastReport?.risk === 'low',
'compliance-row__column-risk--yellow':
lastReport?.risk === 'medium',
'compliance-row__column-risk--orange': lastReport?.risk === 'high',
'compliance-row__column-risk--red':
lastReport?.risk === 'unacceptable',
})}
>
<Text>{lastReport ? lastReport.risk : t('noReport')}</Text>
</Box>
</Box>
<Box
display={DISPLAY.FLEX}
flexDirection={FLEX_DIRECTION.COLUMN}
justifyContent={JustifyContent.center}
height={BLOCK_SIZES.TWO_THIRDS}
paddingTop={4}
paddingBottom={4}
className="compliance-details__row"
>
<Box
display={DISPLAY.FLEX}
alignItems={AlignItems.center}
color={TextColor.textAlternative}
>
<Text marginRight={2}>{t('reportLastRun')}</Text>
<InfoTooltip
position="bottom"
contentText={<span>{t('reportLastRunTooltip')}</span>}
/>
</Box>
<Text color={TextColor.textDefault}>
{lastReport
? formatDate(new Date(lastReport.createTime).getTime())
: 'N/A'}
</Text>
</Box>
<Box>
<SwapsFooter
onSubmit={() => {
onGenerate(address);
onClose();
}}
submitText={t('runReport')}
onCancel={() =>
global.platform.openTab({
url: `https://${complianceTenantSubdomain}.compliance.codefi.network/app/kyt/addresses/${lastReport.address}/${lastReport.reportId}`,
})
}
cancelText={t('showReport')}
hideCancel={!lastReport}
approveActive={lastReport}
showTopBorder
/>
</Box>
</Box>
);
};
ComplianceDetails.propTypes = {
address: PropTypes.string,
onClose: PropTypes.func,
onGenerate: PropTypes.func,
};
export default ComplianceDetails;

View File

@ -0,0 +1,48 @@
import React from 'react';
import { Provider } from 'react-redux';
import configureStore from '../../../store/store';
import testData from '../../../../.storybook/test-data';
import ComplianceDetails from '.';
const customData = {
...testData,
metamask: {
institutionalFeatures: {
complianceProjectId: '',
complianceClientId: '',
reportsInProgress: {},
historicalReports: {
'0xAddress': [
{
reportId: 'reportId',
address: '0xAddress',
risk: 'low',
creatTime: new Date(),
},
],
},
},
},
};
const store = configureStore(customData);
export default {
title: 'Components/Institutional/ComplianceDetails',
decorators: [(story) => <Provider store={store}>{story()}</Provider>],
component: ComplianceDetails,
args: {
address: '0xAddress',
onClose: () => undefined,
onGenerate: () => undefined,
},
argTypes: {
onClick: {
action: 'onClick',
},
},
};
export const DefaultStory = (args) => <ComplianceDetails {...args} />;
DefaultStory.storyName = 'ComplianceDetails';

View File

@ -0,0 +1,67 @@
import React from 'react';
import configureStore from 'redux-mock-store';
import { fireEvent, screen } from '@testing-library/react';
import thunk from 'redux-thunk';
import { renderWithProvider } from '../../../../test/lib/render-helpers';
import ComplianceDetails from './compliance-details';
const initState = {
metamask: {
institutionalFeatures: {
complianceProjectId: '',
complianceClientId: '',
reportsInProgress: {},
historicalReports: {
'0xAddress': [
{
reportId: 'reportId',
address: '0xAddress',
risk: 'low',
creatTime: new Date(),
},
],
},
},
},
};
const middlewares = [thunk];
const mockStore = configureStore(middlewares);
describe('ComplianceDetails', () => {
const props = {
address: '0xAddress',
onClose: jest.fn(),
onGenerate: jest.fn(),
};
const store = mockStore(initState);
it('should render correctly', () => {
const { container } = renderWithProvider(
<ComplianceDetails
address={props.address}
onClose={props.onClose}
onGenerate={props.onGenerate}
/>,
store,
);
expect(container).toMatchSnapshot();
});
it('runs onGenerate fuction', () => {
renderWithProvider(
<ComplianceDetails
address={props.address}
onClose={props.onClose}
onGenerate={props.onGenerate}
/>,
store,
);
fireEvent.click(screen.queryByTestId('page-container-footer-next'));
expect(props.onGenerate).toHaveBeenCalledTimes(1);
expect(props.onGenerate).toHaveBeenCalledWith(props.address);
});
});

View File

@ -0,0 +1 @@
export { default } from './compliance-details';

View File

@ -0,0 +1,5 @@
.compliance-details {
&__row {
border-top: 1px solid var(--color-border-muted);
}
}

View File

@ -0,0 +1,11 @@
/**
* Please import your styles in order of atomicity.
* The most atomic styles should be imported first.
* This will help improve specificity and reduce the chance of
* unintended overrides.
**/
@import 'compliance-details/index';
@import 'compliance-settings/index';
@import 'jwt-dropdown/jwt-dropdown';
@import 'jwt-url-form/jwt-url-form';
@import 'note-to-trader/index';

View File

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

View File

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

View File

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

View File

@ -11,3 +11,4 @@ export { AddressCopyButton } from './address-copy-button';
export { MultichainConnectedSiteMenu } from './multichain-connected-site-menu';
export { NetworkListItem } from './network-list-item';
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 'network-list-item/';
@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

@ -11,6 +11,9 @@
@import '../components/component-library/component-library-components.scss';
@import '../components/app/app-components';
@import '../components/ui/ui-components';
///: BEGIN:ONLY_INCLUDE_IN(mmi)
@import '../components/institutional/institutional-components';
///: END:ONLY_INCLUDE_IN
@import '../components/multichain/multichain-components.scss';
@import '../pages/pages';
@import './errors.scss';

View File

@ -20,8 +20,12 @@ export const SUPPORT_REQUEST_LINK = _supportRequestLink;
export const CONTRACT_ADDRESS_LINK = _contractAddressLink;
export const PASSWORD_MIN_LENGTH = 8;
export const OUTDATED_BROWSER_VERSIONS = {
chrome: '<80',
edge: '<80',
firefox: '<78',
opera: '<67',
// Chrome and Edge should match the latest Chrome version released ~2 years ago
chrome: '<90',
edge: '<90',
// Firefox should match the most recent end-of-life extended support release
firefox: '<91',
// Opera should be set to the equivalent of the Chrome version set
// See https://en.wikipedia.org/wiki/History_of_the_Opera_web_browser
opera: '<76',
};

View File

@ -367,7 +367,7 @@ export const PERMISSION_DESCRIPTIONS = deepFreeze({
label: t('permission_ethereumProvider'),
description: t('permission_ethereumProviderDescription'),
leftIcon: ICON_NAMES.ETHEREUM,
weight: 1,
weight: 2,
id: 'ethereum-provider-access',
message: t('ethereumProviderAccess', [targetSubjectMetadata?.origin]),
}),

View File

@ -204,56 +204,56 @@ describe('util', () => {
});
it('should return false when given a modern chrome browser', () => {
const browser = Bowser.getParser(
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.2623.112 Safari/537.36',
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.2623.112 Safari/537.36',
);
const result = util.getIsBrowserDeprecated(browser);
expect(result).toStrictEqual(false);
});
it('should return true when given an outdated chrome browser', () => {
const browser = Bowser.getParser(
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.2623.112 Safari/537.36',
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.2623.112 Safari/537.36',
);
const result = util.getIsBrowserDeprecated(browser);
expect(result).toStrictEqual(true);
});
it('should return false when given a modern firefox browser', () => {
const browser = Bowser.getParser(
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:78.0) Gecko/20100101 Firefox/78.0',
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:78.0) Gecko/20100101 Firefox/91.0',
);
const result = util.getIsBrowserDeprecated(browser);
expect(result).toStrictEqual(false);
});
it('should return true when given an outdated firefox browser', () => {
const browser = Bowser.getParser(
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:75.0) Gecko/20100101 Firefox/75.0',
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:75.0) Gecko/20100101 Firefox/90.0',
);
const result = util.getIsBrowserDeprecated(browser);
expect(result).toStrictEqual(true);
});
it('should return false when given a modern opera browser', () => {
const browser = Bowser.getParser(
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_16_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.3578.98 Safari/537.36 OPR/68.0.3135.47',
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_16_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.3578.98 Safari/537.36 OPR/76.0.3135.47',
);
const result = util.getIsBrowserDeprecated(browser);
expect(result).toStrictEqual(false);
});
it('should return true when given an outdated opera browser', () => {
const browser = Bowser.getParser(
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_16_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36 OPR/58.0.3135.47',
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_16_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.3578.98 Safari/537.36 OPR/58.0.3135.47',
);
const result = util.getIsBrowserDeprecated(browser);
expect(result).toStrictEqual(true);
});
it('should return false when given a modern edge browser', () => {
const browser = Bowser.getParser(
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.3578.98 Safari/537.36 Edg/81.0.416.68',
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.3578.98 Safari/537.36 Edg/90.0.416.68',
);
const result = util.getIsBrowserDeprecated(browser);
expect(result).toStrictEqual(false);
});
it('should return true when given an outdated edge browser', () => {
const browser = Bowser.getParser(
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36 Edge/71.0.416.68',
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.3578.98 Safari/537.36 Edge/89.0.416.68',
);
const result = util.getIsBrowserDeprecated(browser);
expect(result).toStrictEqual(true);

View File

@ -1041,6 +1041,9 @@ export function getShowBetaHeader(state) {
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
*
@ -1420,6 +1423,10 @@ export function getCustomTokenAmount(state) {
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
*

View File

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