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`] = ` +
+
+
+ + + This is a BETA version. Please report bugs + + here + + + + +
+ +
+
+`; 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,