Skip to content

Commit 0d7e5ce

Browse files
feat: shield entry modal (#35347)
<!-- Please submit this PR as a draft initially. Do not mark it as "Ready for review" until the template has been completely filled out, and PR status checks have passed at least once. --> ## **Description** Adds Metamask Shield Entry point modal UI for users to subscribe to metamask shield. https://www.figma.com/design/HTAO1SrmixV4ppv7qIvLoa/MetaMask-Shield---Phase-1?node-id=10393-173520&t=h6Rb20hua95hevDR-4 <!-- Write a short description of the changes included in this pull request, also include relevant motivation and context. Have in mind the following questions: 1. What is the reason for the change? 2. What is the improvement/solution? --> [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/35347?quickstart=1) ## **Changelog** <!-- If this PR is not End-User-Facing and should not show up in the CHANGELOG, you can choose to either: 1. Write `CHANGELOG entry: null` 2. Label with `no-changelog` If this PR is End-User-Facing, please write a short User-Facing description in the past tense like: `CHANGELOG entry: Added a new tab for users to see their NFTs` `CHANGELOG entry: Fixed a bug that was causing some NFTs to flicker` (This helps the Release Engineer do their job more quickly and accurately) --> CHANGELOG entry: Added metamask shield entry point modal ## **Related issues** Fixes: ## **Manual testing steps** 1. N/A - there is no logic yet to show the modal ## **Screenshots/Recordings** <!-- If applicable, add screenshots and/or recordings to visualize the before and after of your change. --> ### **Before** <!-- [screenshots/recordings] --> ### **After** <img width="371" height="446" alt="Screenshot 2025-08-26 at 12 59 08 PM" src="https://github.com/user-attachments/assets/07af9a6d-0933-46f7-98ce-71878d1ac270" /> <!-- [screenshots/recordings] --> ## **Pre-merge author checklist** - [x] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [x] I’ve included tests if applicable - [x] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [x] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [x] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots.
1 parent e085b42 commit 0d7e5ce

File tree

11 files changed

+263
-0
lines changed

11 files changed

+263
-0
lines changed

app/_locales/en/messages.json

Lines changed: 22 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

app/_locales/en_GB/messages.json

Lines changed: 22 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
28.9 KB
Loading

ui/components/app/app-components.scss

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,3 +77,4 @@
7777
@import 'name/index';
7878
@import 'name/name-details/index';
7979
@import 'confirm/info/row/index';
80+
@import 'shield-entry-modal/index';
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
.shield-entry-modal {
2+
&__header {
3+
background: url('/images/shield-entry-modal-banner.png');
4+
background-size: cover;
5+
background-position: center;
6+
background-repeat: no-repeat;
7+
}
8+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { default } from './shield-entry-modal';
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import React from 'react';
2+
import ShieldEntryModal from './shield-entry-modal';
3+
4+
export default {
5+
title: 'Components/App/ShieldEntryModal',
6+
component: ShieldEntryModal,
7+
};
8+
9+
export const DefaultStory = () => {
10+
return <ShieldEntryModal onClose={() => {}} onGetStarted={() => {}} />;
11+
};
12+
13+
DefaultStory.storyName = 'Default';
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import React from 'react';
2+
import { fireEvent } from '@testing-library/react';
3+
import { renderWithProvider } from '../../../../test/jest/rendering';
4+
import ShieldEntryModal from './shield-entry-modal';
5+
6+
describe('Shield Entry Modal', () => {
7+
const onCloseStub = jest.fn();
8+
const onGetStartedStub = jest.fn();
9+
10+
it('should render', () => {
11+
const { getByTestId } = renderWithProvider(
12+
<ShieldEntryModal
13+
onClose={onCloseStub}
14+
onGetStarted={onGetStartedStub}
15+
/>,
16+
);
17+
18+
const shieldEntryModal = getByTestId('shield-entry-modal');
19+
expect(shieldEntryModal).toBeInTheDocument();
20+
});
21+
22+
it('should call onClose when the skip button is clicked', () => {
23+
const { getByTestId } = renderWithProvider(
24+
<ShieldEntryModal
25+
onClose={onCloseStub}
26+
onGetStarted={onGetStartedStub}
27+
/>,
28+
);
29+
30+
const skipButton = getByTestId('shield-entry-modal-skip-button');
31+
fireEvent.click(skipButton);
32+
expect(onCloseStub).toHaveBeenCalled();
33+
});
34+
35+
it('should call onGetStarted when the get started button is clicked', () => {
36+
const { getByTestId } = renderWithProvider(
37+
<ShieldEntryModal
38+
onClose={onCloseStub}
39+
onGetStarted={onGetStartedStub}
40+
/>,
41+
);
42+
43+
const getStartedButton = getByTestId(
44+
'shield-entry-modal-get-started-button',
45+
);
46+
fireEvent.click(getStartedButton);
47+
expect(onGetStartedStub).toHaveBeenCalled();
48+
});
49+
});
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
import React from 'react';
2+
import { useI18nContext } from '../../../hooks/useI18nContext';
3+
import {
4+
AlignItems,
5+
Display,
6+
FlexDirection,
7+
TextVariant,
8+
} from '../../../helpers/constants/design-system';
9+
import {
10+
Modal,
11+
ModalContent,
12+
ModalHeader,
13+
ModalOverlay,
14+
Text,
15+
Button,
16+
ModalFooter,
17+
ModalBody,
18+
ButtonSize,
19+
IconName,
20+
Box,
21+
AvatarIcon,
22+
AvatarIconSize,
23+
ButtonVariant,
24+
ButtonLink,
25+
ButtonLinkSize,
26+
} from '../../component-library';
27+
import { ThemeType } from '../../../../shared/constants/preferences';
28+
29+
// TODO: Fix in https://github.com/MetaMask/metamask-extension/issues/31860
30+
// eslint-disable-next-line @typescript-eslint/naming-convention
31+
export default function ShieldEntryModal({
32+
onClose,
33+
onGetStarted,
34+
}: {
35+
onClose: () => void;
36+
onGetStarted: () => void;
37+
}) {
38+
const t = useI18nContext();
39+
40+
return (
41+
<Modal
42+
data-testid="shield-entry-modal"
43+
isOpen
44+
onClose={onClose}
45+
className="shield-entry-modal"
46+
>
47+
<ModalOverlay />
48+
<ModalContent
49+
alignItems={AlignItems.flexStart}
50+
modalDialogProps={{ paddingTop: 0, paddingBottom: 6 }}
51+
>
52+
{/* TODO: update with full image banner */}
53+
<ModalHeader
54+
className="shield-entry-modal__header h-[160px]"
55+
data-theme={ThemeType.dark}
56+
closeButtonProps={{
57+
className: 'absolute top-2 right-2',
58+
}}
59+
onClose={onClose}
60+
/>
61+
<ModalBody paddingTop={4}>
62+
<Text variant={TextVariant.headingMd} marginBottom={1}>
63+
{t('shieldEntryModalSubtitle')}
64+
</Text>
65+
<Text variant={TextVariant.bodySm} marginBottom={4}>
66+
{/* TODO: update link to learn more page */}
67+
{t('shieldEntryModalDescription', [
68+
'$8',
69+
<ButtonLink
70+
key="learn-more-link"
71+
size={ButtonLinkSize.Inherit}
72+
target="_blank"
73+
rel="noopener noreferrer"
74+
href="#"
75+
>
76+
{t('learnMoreUpperCase')}
77+
</ButtonLink>,
78+
])}
79+
</Text>
80+
<Box
81+
display={Display.Flex}
82+
flexDirection={FlexDirection.Column}
83+
gap={4}
84+
>
85+
<Box display={Display.Flex} alignItems={AlignItems.center} gap={2}>
86+
<AvatarIcon size={AvatarIconSize.Sm} iconName={IconName.Plant} />
87+
<Text variant={TextVariant.bodySm}>
88+
{t('shieldEntryModalAssetCoverage')}
89+
</Text>
90+
</Box>
91+
<Box display={Display.Flex} alignItems={AlignItems.center} gap={2}>
92+
<AvatarIcon
93+
size={AvatarIconSize.Sm}
94+
iconName={IconName.ShieldLock}
95+
/>
96+
<Text variant={TextVariant.bodySm}>
97+
{t('shieldEntryModalProtection')}
98+
</Text>
99+
</Box>
100+
<Box display={Display.Flex} alignItems={AlignItems.center} gap={2}>
101+
<AvatarIcon size={AvatarIconSize.Sm} iconName={IconName.Flash} />
102+
<Text variant={TextVariant.bodySm}>
103+
{t('shieldEntryModalSupport')}
104+
</Text>
105+
</Box>
106+
</Box>
107+
</ModalBody>
108+
<ModalFooter>
109+
<Box display={Display.Flex} gap={4}>
110+
<Button
111+
data-testid="shield-entry-modal-skip-button"
112+
variant={ButtonVariant.Secondary}
113+
size={ButtonSize.Lg}
114+
block
115+
onClick={onClose}
116+
>
117+
{t('shieldEntryModalSkip')}
118+
</Button>
119+
<Button
120+
data-testid="shield-entry-modal-get-started-button"
121+
size={ButtonSize.Lg}
122+
block
123+
onClick={onGetStarted}
124+
>
125+
{t('shieldEntryModalGetStarted')}
126+
</Button>
127+
</Box>
128+
</ModalFooter>
129+
</ModalContent>
130+
</Modal>
131+
);
132+
}

ui/pages/home/home.component.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ import { setEditedNetwork } from '../../store/actions';
7070
import { navigateToConfirmation } from '../confirmations/hooks/useConfirmationNavigation';
7171
import PasswordOutdatedModal from '../../components/app/password-outdated-modal';
7272
import ConnectionsRemovedModal from '../../components/app/connections-removed-modal';
73+
import ShieldEntryModal from '../../components/app/shield-entry-modal';
7374
///: BEGIN:ONLY_INCLUDE_IF(build-beta)
7475
import BetaHomeFooter from './beta/beta-home-footer.component';
7576
///: END:ONLY_INCLUDE_IF
@@ -169,6 +170,7 @@ export default class Home extends PureComponent {
169170
isSeedlessPasswordOutdated: PropTypes.bool,
170171
isPrimarySeedPhraseBackedUp: PropTypes.bool,
171172
showConnectionsRemovedModal: PropTypes.bool,
173+
showShieldEntryModal: PropTypes.bool,
172174
};
173175

174176
state = {
@@ -825,6 +827,7 @@ export default class Home extends PureComponent {
825827
isSeedlessPasswordOutdated,
826828
isPrimarySeedPhraseBackedUp,
827829
showConnectionsRemovedModal,
830+
showShieldEntryModal,
828831
} = this.props;
829832

830833
if (forgottenPassword) {
@@ -888,6 +891,16 @@ export default class Home extends PureComponent {
888891
<TermsOfUsePopup onAccept={this.onAcceptTermsOfUse} />
889892
) : null}
890893
{showConnectionsRemovedModal && <ConnectionsRemovedModal />}
894+
{showShieldEntryModal && (
895+
<ShieldEntryModal
896+
onClose={() => {
897+
// TODO: implement
898+
}}
899+
onGetStarted={() => {
900+
// TODO: implement
901+
}}
902+
/>
903+
)}
891904
{isPopup && !connectedStatusPopoverHasBeenShown
892905
? this.renderPopover()
893906
: null}

0 commit comments

Comments
 (0)