diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json
index b2a467c12..0e336110f 100644
--- a/app/_locales/en/messages.json
+++ b/app/_locales/en/messages.json
@@ -437,6 +437,10 @@
"beta": {
"message": "Beta"
},
+ "betaHeaderText": {
+ "message": "This is a BETA version. Please report bugs $1",
+ "description": "$1 represents the word 'here' in a hyperlink"
+ },
"betaMetamaskDescription": {
"message": "Trusted by millions, MetaMask is a secure wallet making the world of web3 accessible to all."
},
diff --git a/app/scripts/controllers/app-state.js b/app/scripts/controllers/app-state.js
index 0cf5fc24b..f66868861 100644
--- a/app/scripts/controllers/app-state.js
+++ b/app/scripts/controllers/app-state.js
@@ -4,6 +4,7 @@ import { METAMASK_CONTROLLER_EVENTS } from '../metamask-controller';
import { MINUTE } from '../../../shared/constants/time';
import { AUTO_LOCK_TIMEOUT_ALARM } from '../../../shared/constants/alarms';
import { isManifestV3 } from '../../../shared/modules/mv3.utils';
+import { isBeta } from '../../../ui/helpers/utils/build-types';
export default class AppStateController extends EventEmitter {
/**
@@ -36,6 +37,7 @@ export default class AppStateController extends EventEmitter {
enableEIP1559V2NoticeDismissed: false,
showTestnetMessageInDropdown: true,
showPortfolioTooltip: true,
+ showBetaHeader: isBeta(),
trezorModel: null,
...initState,
qrHardware: {},
@@ -291,6 +293,15 @@ export default class AppStateController extends EventEmitter {
this.store.updateState({ showPortfolioTooltip });
}
+ /**
+ * Sets whether the beta notification heading on the home page
+ *
+ * @param showBetaHeader
+ */
+ setShowBetaHeader(showBetaHeader) {
+ this.store.updateState({ showBetaHeader });
+ }
+
/**
* Sets a property indicating the model of the user's Trezor hardware wallet
*
diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js
index 05c698635..a9d5f6061 100644
--- a/app/scripts/metamask-controller.js
+++ b/app/scripts/metamask-controller.js
@@ -1697,6 +1697,8 @@ export default class MetamaskController extends EventEmitter {
),
setShowPortfolioTooltip:
appStateController.setShowPortfolioTooltip.bind(appStateController),
+ setShowBetaHeader:
+ appStateController.setShowBetaHeader.bind(appStateController),
setCollectiblesDetectionNoticeDismissed:
appStateController.setCollectiblesDetectionNoticeDismissed.bind(
appStateController,
diff --git a/test/data/mock-state.json b/test/data/mock-state.json
index a376803df..8b699fde1 100644
--- a/test/data/mock-state.json
+++ b/test/data/mock-state.json
@@ -19,6 +19,7 @@
},
"metamask": {
"gasEstimateType": "fee-market",
+ "showBetaHeader": false,
"gasFeeEstimates": {
"low": {
"minWaitTimeEstimate": 180000,
diff --git a/ui/components/app/app-components.scss b/ui/components/app/app-components.scss
index ed008d5f0..0b457c66c 100644
--- a/ui/components/app/app-components.scss
+++ b/ui/components/app/app-components.scss
@@ -9,6 +9,7 @@
@import 'alerts/alerts';
@import 'app-header/index';
@import 'asset-list-item/asset-list-item';
+@import 'beta-header/index';
@import 'cancel-speedup-popover/index';
@import 'confirm-page-container/index';
@import 'confirm-page-container/enableEIP1559V2-notice';
diff --git a/ui/components/app/app-header/app-header.component.js b/ui/components/app/app-header/app-header.component.js
index 7f1bbc253..f5e7d57c5 100644
--- a/ui/components/app/app-header/app-header.component.js
+++ b/ui/components/app/app-header/app-header.component.js
@@ -7,6 +7,10 @@ import { DEFAULT_ROUTE } from '../../../helpers/constants/routes';
import { EVENT, EVENT_NAMES } from '../../../../shared/constants/metametrics';
import NetworkDisplay from '../network-display';
+///: BEGIN:ONLY_INCLUDE_IN(beta)
+import BetaHeader from '../beta-header';
+///: END:ONLY_INCLUDE_IN(beta)
+
export default class AppHeader extends PureComponent {
static propTypes = {
history: PropTypes.object,
@@ -23,6 +27,9 @@ export default class AppHeader extends PureComponent {
///: BEGIN:ONLY_INCLUDE_IN(flask)
unreadNotificationsCount: PropTypes.number,
///: END:ONLY_INCLUDE_IN
+ ///: BEGIN:ONLY_INCLUDE_IN(beta)
+ showBetaHeader: PropTypes.bool,
+ ///: END:ONLY_INCLUDE_IN
onClick: PropTypes.func,
};
@@ -112,33 +119,44 @@ export default class AppHeader extends PureComponent {
disableNetworkIndicator,
disabled,
onClick,
+ ///: BEGIN:ONLY_INCLUDE_IN(beta)
+ showBetaHeader,
+ ///: END:ONLY_INCLUDE_IN(beta)
} = this.props;
return (
-
-
-
{
- if (onClick) {
- await onClick();
- }
- history.push(DEFAULT_ROUTE);
- }}
- />
-
- {!hideNetworkIndicator && (
-
- this.handleNetworkIndicatorClick(event)}
- disabled={disabled || disableNetworkIndicator}
- />
-
- )}
- {this.renderAccountMenu()}
+ <>
+ {
+ ///: BEGIN:ONLY_INCLUDE_IN(beta)
+ showBetaHeader ?
: null
+ ///: END:ONLY_INCLUDE_IN(beta)
+ }
+
+
+
+
{
+ if (onClick) {
+ await onClick();
+ }
+ history.push(DEFAULT_ROUTE);
+ }}
+ />
+
+ {!hideNetworkIndicator && (
+
+ this.handleNetworkIndicatorClick(event)}
+ disabled={disabled || disableNetworkIndicator}
+ />
+
+ )}
+ {this.renderAccountMenu()}
+
-
+ >
);
}
}
diff --git a/ui/components/app/app-header/app-header.container.js b/ui/components/app/app-header/app-header.container.js
index e30049bc3..04b9c47c4 100644
--- a/ui/components/app/app-header/app-header.container.js
+++ b/ui/components/app/app-header/app-header.container.js
@@ -1,9 +1,14 @@
import { connect } from 'react-redux';
import { withRouter } from 'react-router-dom';
import { compose } from 'redux';
-///: BEGIN:ONLY_INCLUDE_IN(flask)
-import { getUnreadNotificationsCount } from '../../../selectors';
-///: END:ONLY_INCLUDE_IN
+import {
+ ///: BEGIN:ONLY_INCLUDE_IN(flask)
+ getUnreadNotificationsCount,
+ ///: END:ONLY_INCLUDE_IN
+ ///: BEGIN:ONLY_INCLUDE_IN(beta)
+ getShowBetaHeader,
+ ///: END:ONLY_INCLUDE_IN
+} from '../../../selectors';
import * as actions from '../../../store/actions';
import AppHeader from './app-header.component';
@@ -17,6 +22,10 @@ const mapStateToProps = (state) => {
const unreadNotificationsCount = getUnreadNotificationsCount(state);
///: END:ONLY_INCLUDE_IN
+ ///: BEGIN:ONLY_INCLUDE_IN(beta)
+ const showBetaHeader = getShowBetaHeader(state);
+ ///: END:ONLY_INCLUDE_IN
+
return {
networkDropdownOpen,
selectedAddress,
@@ -25,6 +34,9 @@ const mapStateToProps = (state) => {
///: BEGIN:ONLY_INCLUDE_IN(flask)
unreadNotificationsCount,
///: END:ONLY_INCLUDE_IN
+ ///: BEGIN:ONLY_INCLUDE_IN(beta)
+ showBetaHeader,
+ ///: END:ONLY_INCLUDE_IN
};
};
diff --git a/ui/components/app/beta-header/__snapshots__/beta-header.test.js.snap b/ui/components/app/beta-header/__snapshots__/beta-header.test.js.snap
new file mode 100644
index 000000000..24c327ba4
--- /dev/null
+++ b/ui/components/app/beta-header/__snapshots__/beta-header.test.js.snap
@@ -0,0 +1,36 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Beta Header should match snapshot 1`] = `
+
+
+
+`;
diff --git a/ui/components/app/beta-header/beta-header.stories.js b/ui/components/app/beta-header/beta-header.stories.js
new file mode 100644
index 000000000..4a4480090
--- /dev/null
+++ b/ui/components/app/beta-header/beta-header.stories.js
@@ -0,0 +1,24 @@
+import React from 'react';
+import { Provider } from 'react-redux';
+import testData from '../../../../.storybook/test-data';
+import configureStore from '../../../store/store';
+import BetaHeader from '.';
+
+const store = configureStore({
+ ...testData,
+ metamask: { ...testData.metamask, isUnlocked: true, showBetaHeader: true },
+});
+
+export default {
+ title: 'Components/App/BetaHeader',
+ decorators: [(story) => {story()}],
+ id: __filename,
+};
+
+export const DefaultStory = () => (
+ <>
+
+ >
+);
+
+DefaultStory.storyName = 'Default';
diff --git a/ui/components/app/beta-header/beta-header.test.js b/ui/components/app/beta-header/beta-header.test.js
new file mode 100644
index 000000000..2036b23e0
--- /dev/null
+++ b/ui/components/app/beta-header/beta-header.test.js
@@ -0,0 +1,45 @@
+import React from 'react';
+import { fireEvent } from '@testing-library/react';
+import configureMockStore from 'redux-mock-store';
+import thunk from 'redux-thunk';
+import mockState from '../../../../test/data/mock-state.json';
+import { renderWithProvider } from '../../../../test/lib/render-helpers';
+import BetaHeader from '.';
+
+const mockHideBetaHeader = jest.fn();
+
+jest.mock('../../../store/actions', () => {
+ return {
+ hideBetaHeader: () => {
+ mockHideBetaHeader();
+ },
+ };
+});
+
+describe('Beta Header', () => {
+ let store;
+
+ beforeEach(() => {
+ store = configureMockStore([thunk])(mockState);
+ });
+
+ afterEach(() => {
+ mockHideBetaHeader.mockClear();
+ });
+
+ it('should match snapshot', () => {
+ const { container } = renderWithProvider(, store);
+ expect(container).toMatchSnapshot();
+ });
+
+ describe('Beta Header', () => {
+ it('gets hidden when close button is clicked', () => {
+ const { queryByTestId } = renderWithProvider(, store);
+
+ const closeButton = queryByTestId('beta-header-close');
+ fireEvent.click(closeButton);
+
+ expect(mockHideBetaHeader).toHaveBeenCalledTimes(1);
+ });
+ });
+});
diff --git a/ui/components/app/beta-header/index.js b/ui/components/app/beta-header/index.js
new file mode 100644
index 000000000..dd1884339
--- /dev/null
+++ b/ui/components/app/beta-header/index.js
@@ -0,0 +1,59 @@
+import React from 'react';
+import { useI18nContext } from '../../../hooks/useI18nContext';
+
+import Box from '../../ui/box/box';
+import Typography from '../../ui/typography/typography';
+import {
+ TYPOGRAPHY,
+ COLORS,
+ BLOCK_SIZES,
+ DISPLAY,
+} from '../../../helpers/constants/design-system';
+import { BETA_BUGS_URL } from '../../../helpers/constants/beta';
+
+import { hideBetaHeader } from '../../../store/actions';
+
+const BetaHeader = () => {
+ const t = useI18nContext();
+
+ return (
+
+
+ {t('betaHeaderText', [
+
+ {t('here')}
+ ,
+ ])}
+
+
+
+ );
+};
+
+export default BetaHeader;
diff --git a/ui/components/app/beta-header/index.scss b/ui/components/app/beta-header/index.scss
new file mode 100644
index 000000000..6d143af97
--- /dev/null
+++ b/ui/components/app/beta-header/index.scss
@@ -0,0 +1,16 @@
+.beta-header {
+ &__message {
+ text-align: center;
+ flex-grow: 1;
+ }
+
+ &__button {
+ background: transparent;
+ padding: 0 6px;
+ margin: 0;
+
+ i {
+ color: var(--color-warning-inverse);
+ }
+ }
+}
diff --git a/ui/helpers/constants/beta.js b/ui/helpers/constants/beta.js
new file mode 100644
index 000000000..8250c78ab
--- /dev/null
+++ b/ui/helpers/constants/beta.js
@@ -0,0 +1 @@
+export const BETA_BUGS_URL = 'https://metamask.zendesk.com/hc/en-us';
diff --git a/ui/selectors/selectors.js b/ui/selectors/selectors.js
index 9c6cce639..dbcff10b2 100644
--- a/ui/selectors/selectors.js
+++ b/ui/selectors/selectors.js
@@ -978,6 +978,10 @@ export function getShowPortfolioTooltip(state) {
return state.metamask.showPortfolioTooltip;
}
+export function getShowBetaHeader(state) {
+ return state.metamask.showBetaHeader;
+}
+
/**
* To get the useTokenDetection flag which determines whether a static or dynamic token list is used
*
diff --git a/ui/store/actions.js b/ui/store/actions.js
index fb9639282..ee3d8fb1e 100644
--- a/ui/store/actions.js
+++ b/ui/store/actions.js
@@ -3779,6 +3779,10 @@ export function hidePortfolioTooltip() {
return submitRequestToBackground('setShowPortfolioTooltip', [false]);
}
+export function hideBetaHeader() {
+ return submitRequestToBackground('setShowBetaHeader', [false]);
+}
+
export function setCollectiblesDetectionNoticeDismissed() {
return submitRequestToBackground('setCollectiblesDetectionNoticeDismissed', [
true,