mirror of
https://github.com/kremalicious/metamask-extension.git
synced 2024-12-23 09:52:26 +01:00
Add form-field component and new account view (#11450)
* add generic form-field component * swap in new form-field component for advanced-gas-controls-row * add new create password view for redesigned onboarding flow * make text additions translatable
This commit is contained in:
parent
68ad9c619f
commit
b1e2005a73
@ -471,6 +471,9 @@
|
||||
"createAccount": {
|
||||
"message": "Create Account"
|
||||
},
|
||||
"createNewWallet": {
|
||||
"message": "Create a new wallet"
|
||||
},
|
||||
"createPassword": {
|
||||
"message": "Create Password"
|
||||
},
|
||||
@ -1464,6 +1467,12 @@
|
||||
"passwordNotLongEnough": {
|
||||
"message": "Password not long enough"
|
||||
},
|
||||
"passwordSetupDetails": {
|
||||
"message": "This password will unlock your MetaMask wallet only on this device. MetaMask can not recover this password."
|
||||
},
|
||||
"passwordTermsWarning": {
|
||||
"message": "I understand that MetaMask cannot recover this password for me. $1"
|
||||
},
|
||||
"passwordsDontMatch": {
|
||||
"message": "Passwords Don't Match"
|
||||
},
|
||||
@ -1797,6 +1806,9 @@
|
||||
"settings": {
|
||||
"message": "Settings"
|
||||
},
|
||||
"show": {
|
||||
"message": "Show"
|
||||
},
|
||||
"showAdvancedGasInline": {
|
||||
"message": "Advanced gas controls"
|
||||
},
|
||||
|
@ -1,97 +0,0 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import Typography from '../../ui/typography/typography';
|
||||
import {
|
||||
COLORS,
|
||||
TEXT_ALIGN,
|
||||
DISPLAY,
|
||||
TYPOGRAPHY,
|
||||
FONT_WEIGHT,
|
||||
} from '../../../helpers/constants/design-system';
|
||||
|
||||
import NumericInput from '../../ui/numeric-input/numeric-input.component';
|
||||
import InfoTooltip from '../../ui/info-tooltip/info-tooltip';
|
||||
|
||||
export default function AdvancedGasControlsRow({
|
||||
titleText,
|
||||
titleUnit,
|
||||
tooltipText,
|
||||
titleDetailText,
|
||||
error,
|
||||
onChange,
|
||||
value,
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
className={classNames('advanced-gas-controls__row', {
|
||||
'advanced-gas-controls__row--error': error,
|
||||
})}
|
||||
>
|
||||
<label>
|
||||
<div className="advanced-gas-controls__row-heading">
|
||||
<div className="advanced-gas-controls__row-heading-title">
|
||||
<Typography
|
||||
tag={TYPOGRAPHY.H6}
|
||||
fontWeight={FONT_WEIGHT.BOLD}
|
||||
variant={TYPOGRAPHY.H6}
|
||||
boxProps={{ display: DISPLAY.INLINE_BLOCK }}
|
||||
>
|
||||
{titleText}
|
||||
</Typography>
|
||||
<Typography
|
||||
tag={TYPOGRAPHY.H6}
|
||||
variant={TYPOGRAPHY.H6}
|
||||
color={COLORS.UI4}
|
||||
boxProps={{ display: DISPLAY.INLINE_BLOCK }}
|
||||
>
|
||||
{titleUnit}
|
||||
</Typography>
|
||||
<InfoTooltip position="top" contentText={tooltipText} />
|
||||
</div>
|
||||
{titleDetailText && (
|
||||
<Typography
|
||||
className="advanced-gas-controls__row-heading-detail"
|
||||
align={TEXT_ALIGN.END}
|
||||
color={COLORS.UI4}
|
||||
variant={TYPOGRAPHY.H8}
|
||||
>
|
||||
{titleDetailText}
|
||||
</Typography>
|
||||
)}
|
||||
</div>
|
||||
<NumericInput error={error} onChange={onChange} value={value} />
|
||||
{error && (
|
||||
<Typography
|
||||
color={COLORS.ERROR1}
|
||||
variant={TYPOGRAPHY.H7}
|
||||
className="advanced-gas-controls__row-error"
|
||||
>
|
||||
{error}
|
||||
</Typography>
|
||||
)}
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
AdvancedGasControlsRow.propTypes = {
|
||||
titleText: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
|
||||
titleUnit: PropTypes.string,
|
||||
tooltipText: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
|
||||
titleDetailText: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
|
||||
error: PropTypes.string,
|
||||
onChange: PropTypes.func,
|
||||
value: PropTypes.number,
|
||||
};
|
||||
|
||||
AdvancedGasControlsRow.defaultProps = {
|
||||
titleText: '',
|
||||
titleUnit: '',
|
||||
tooltipText: '',
|
||||
titleDetailText: '',
|
||||
error: '',
|
||||
onChange: undefined,
|
||||
value: 0,
|
||||
};
|
@ -7,7 +7,7 @@ import {
|
||||
TYPOGRAPHY,
|
||||
COLORS,
|
||||
} from '../../../helpers/constants/design-system';
|
||||
import AdvancedGasControlsRow from './advanced-gas-controls-row.component';
|
||||
import FormField from '../../ui/form-field';
|
||||
|
||||
export default function AdvancedGasControls() {
|
||||
const t = useContext(I18nContext);
|
||||
@ -21,22 +21,23 @@ export default function AdvancedGasControls() {
|
||||
|
||||
return (
|
||||
<div className="advanced-gas-controls">
|
||||
<AdvancedGasControlsRow
|
||||
<FormField
|
||||
titleText={t('gasLimit')}
|
||||
onChange={setGasLimit}
|
||||
tooltipText=""
|
||||
titleDetailText=""
|
||||
value={gasLimit}
|
||||
numeric
|
||||
/>
|
||||
{process.env.SHOW_EIP_1559_UI ? (
|
||||
<>
|
||||
<AdvancedGasControlsRow
|
||||
<FormField
|
||||
titleText={t('maxPriorityFee')}
|
||||
titleUnit="(GWEI)"
|
||||
tooltipText=""
|
||||
onChange={setMaxPriorityFee}
|
||||
value={maxPriorityFee}
|
||||
titleDetailText={
|
||||
numeric
|
||||
titleDetail={
|
||||
<>
|
||||
<Typography
|
||||
tag="span"
|
||||
@ -54,13 +55,14 @@ export default function AdvancedGasControls() {
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<AdvancedGasControlsRow
|
||||
<FormField
|
||||
titleText={t('maxFee')}
|
||||
titleUnit="(GWEI)"
|
||||
tooltipText=""
|
||||
onChange={setMaxFee}
|
||||
value={maxFee}
|
||||
titleDetailText={
|
||||
numeric
|
||||
titleDetail={
|
||||
<>
|
||||
<Typography
|
||||
tag="span"
|
||||
@ -81,13 +83,13 @@ export default function AdvancedGasControls() {
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<AdvancedGasControlsRow
|
||||
<FormField
|
||||
titleText={t('gasPrice')}
|
||||
titleUnit="(GWEI)"
|
||||
onChange={setGasPrice}
|
||||
tooltipText={t('editGasPriceTooltip')}
|
||||
titleDetailText=""
|
||||
value={gasPrice}
|
||||
numeric
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
134
ui/components/ui/form-field/form-field.js
Normal file
134
ui/components/ui/form-field/form-field.js
Normal file
@ -0,0 +1,134 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import Typography from '../typography/typography';
|
||||
import Box from '../box/box';
|
||||
import {
|
||||
COLORS,
|
||||
TEXT_ALIGN,
|
||||
DISPLAY,
|
||||
TYPOGRAPHY,
|
||||
FONT_WEIGHT,
|
||||
} from '../../../helpers/constants/design-system';
|
||||
|
||||
import NumericInput from '../numeric-input/numeric-input.component';
|
||||
import InfoTooltip from '../info-tooltip/info-tooltip';
|
||||
|
||||
export default function FormField({
|
||||
titleText,
|
||||
titleUnit,
|
||||
tooltipText,
|
||||
titleDetail,
|
||||
error,
|
||||
onChange,
|
||||
value,
|
||||
numeric,
|
||||
detailText,
|
||||
autoFocus,
|
||||
password,
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
className={classNames('form-field', {
|
||||
'form-field__row--error': error,
|
||||
})}
|
||||
>
|
||||
<label>
|
||||
<div className="form-field__heading">
|
||||
<div className="form-field__heading-title">
|
||||
{titleText && (
|
||||
<Typography
|
||||
tag={TYPOGRAPHY.H6}
|
||||
fontWeight={FONT_WEIGHT.BOLD}
|
||||
variant={TYPOGRAPHY.H6}
|
||||
boxProps={{ display: DISPLAY.INLINE_BLOCK }}
|
||||
>
|
||||
{titleText}
|
||||
</Typography>
|
||||
)}
|
||||
{titleUnit && (
|
||||
<Typography
|
||||
tag={TYPOGRAPHY.H6}
|
||||
variant={TYPOGRAPHY.H6}
|
||||
color={COLORS.UI4}
|
||||
boxProps={{ display: DISPLAY.INLINE_BLOCK }}
|
||||
>
|
||||
{titleUnit}
|
||||
</Typography>
|
||||
)}
|
||||
{tooltipText && (
|
||||
<InfoTooltip position="top" contentText={tooltipText} />
|
||||
)}
|
||||
</div>
|
||||
{titleDetail && (
|
||||
<Box
|
||||
className="form-field__heading-detail"
|
||||
textAlign={TEXT_ALIGN.END}
|
||||
marginBottom={3}
|
||||
marginRight={2}
|
||||
>
|
||||
{titleDetail}
|
||||
</Box>
|
||||
)}
|
||||
</div>
|
||||
{numeric ? (
|
||||
<NumericInput
|
||||
error={error}
|
||||
onChange={onChange}
|
||||
value={value}
|
||||
detailText={detailText}
|
||||
autoFocus={autoFocus}
|
||||
/>
|
||||
) : (
|
||||
<input
|
||||
className={classNames('form-field__input', {
|
||||
'form-field__input--error': error,
|
||||
})}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
value={value}
|
||||
type={password ? 'password' : 'text'}
|
||||
autoFocus={autoFocus}
|
||||
/>
|
||||
)}
|
||||
{error && (
|
||||
<Typography
|
||||
color={COLORS.ERROR1}
|
||||
variant={TYPOGRAPHY.H7}
|
||||
className="form-field__error"
|
||||
>
|
||||
{error}
|
||||
</Typography>
|
||||
)}
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
FormField.propTypes = {
|
||||
titleText: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
|
||||
titleUnit: PropTypes.string,
|
||||
tooltipText: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
|
||||
titleDetail: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
|
||||
error: PropTypes.string,
|
||||
onChange: PropTypes.func,
|
||||
value: PropTypes.number,
|
||||
detailText: PropTypes.string,
|
||||
autoFocus: PropTypes.bool,
|
||||
numeric: PropTypes.bool,
|
||||
password: PropTypes.bool,
|
||||
};
|
||||
|
||||
FormField.defaultProps = {
|
||||
titleText: '',
|
||||
titleUnit: '',
|
||||
tooltipText: '',
|
||||
titleDetail: '',
|
||||
error: '',
|
||||
onChange: undefined,
|
||||
value: 0,
|
||||
detailText: '',
|
||||
autoFocus: false,
|
||||
numeric: false,
|
||||
password: false,
|
||||
};
|
55
ui/components/ui/form-field/form-field.stories.js
Normal file
55
ui/components/ui/form-field/form-field.stories.js
Normal file
@ -0,0 +1,55 @@
|
||||
/* eslint-disable react/prop-types */
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { select } from '@storybook/addon-knobs';
|
||||
import FormField from '.';
|
||||
|
||||
export default {
|
||||
title: 'FormField',
|
||||
};
|
||||
|
||||
export const Plain = ({ ...props }) => {
|
||||
const options = { text: false, numeric: true };
|
||||
const [value, setValue] = useState('');
|
||||
return (
|
||||
<div style={{ width: '600px' }}>
|
||||
<FormField
|
||||
onChange={setValue}
|
||||
titleText="Title"
|
||||
value={value}
|
||||
numeric={select('text or numeric', options, options.text)}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const FormFieldWithTitleDetail = () => {
|
||||
const [clicked, setClicked] = useState(false);
|
||||
const detailOptions = {
|
||||
text: <div style={{ fontSize: '12px' }}>Detail</div>,
|
||||
button: (
|
||||
<button
|
||||
style={{ backgroundColor: clicked ? 'orange' : 'rgb(239, 239, 239)' }}
|
||||
onClick={() => setClicked(!clicked)}
|
||||
>
|
||||
Click Me
|
||||
</button>
|
||||
),
|
||||
checkmark: <i className="fas fa-check" />,
|
||||
};
|
||||
return (
|
||||
<Plain
|
||||
titleText="Title"
|
||||
titleDetail={
|
||||
detailOptions[
|
||||
select('detailType', ['text', 'button', 'checkmark'], 'text')
|
||||
]
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const FormFieldWithError = () => {
|
||||
return <Plain titleText="Title" error="Incorrect Format" />;
|
||||
};
|
1
ui/components/ui/form-field/index.js
Normal file
1
ui/components/ui/form-field/index.js
Normal file
@ -0,0 +1 @@
|
||||
export { default } from './form-field';
|
48
ui/components/ui/form-field/index.scss
Normal file
48
ui/components/ui/form-field/index.scss
Normal file
@ -0,0 +1,48 @@
|
||||
.form-field {
|
||||
margin-bottom: 20px;
|
||||
|
||||
&__heading {
|
||||
display: flex;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.info-tooltip {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
&__heading-detail {
|
||||
flex-grow: 1;
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
&__error,
|
||||
&__error h6 {
|
||||
color: $error-1 !important;
|
||||
padding-top: 6px;
|
||||
}
|
||||
|
||||
h6 {
|
||||
padding-bottom: 6px;
|
||||
margin-inline-end: 6px;
|
||||
}
|
||||
|
||||
i {
|
||||
color: #dadada;
|
||||
font-size: $font-size-h7;
|
||||
}
|
||||
|
||||
&__input {
|
||||
width: 100%;
|
||||
border: solid 1px $ui-3;
|
||||
padding: 10px;
|
||||
border-radius: 6px;
|
||||
|
||||
&:focus {
|
||||
border: solid 2px $primary-1;
|
||||
}
|
||||
|
||||
&--error {
|
||||
border-color: $error-1;
|
||||
}
|
||||
}
|
||||
}
|
@ -35,6 +35,7 @@
|
||||
@import 'loading-screen/index';
|
||||
@import 'menu/menu';
|
||||
@import 'numeric-input/numeric-input';
|
||||
@import 'form-field/index';
|
||||
@import 'page-container/index';
|
||||
@import 'popover/index';
|
||||
@import 'pulse-loader/index';
|
||||
|
34
ui/pages/onboarding-flow/new-account/index.scss
Normal file
34
ui/pages/onboarding-flow/new-account/index.scss
Normal file
@ -0,0 +1,34 @@
|
||||
.new-account {
|
||||
&__wrapper {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
&__link-text {
|
||||
color: $primary-1;
|
||||
}
|
||||
|
||||
&__form {
|
||||
padding: 0 24px;
|
||||
|
||||
&--password-button {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
&--submit-button {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
&--checkmark {
|
||||
i {
|
||||
color: $success-1;
|
||||
}
|
||||
}
|
||||
|
||||
.form-field__input {
|
||||
height: 50px;
|
||||
}
|
||||
}
|
||||
}
|
184
ui/pages/onboarding-flow/new-account/new-account.js
Normal file
184
ui/pages/onboarding-flow/new-account/new-account.js
Normal file
@ -0,0 +1,184 @@
|
||||
import React, { useState, useMemo } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { useNewMetricEvent } from '../../../hooks/useMetricEvent';
|
||||
import { useI18nContext } from '../../../hooks/useI18nContext';
|
||||
import Button from '../../../components/ui/button';
|
||||
import Typography from '../../../components/ui/typography';
|
||||
import {
|
||||
TEXT_ALIGN,
|
||||
TYPOGRAPHY,
|
||||
JUSTIFY_CONTENT,
|
||||
FONT_WEIGHT,
|
||||
ALIGN_ITEMS,
|
||||
} from '../../../helpers/constants/design-system';
|
||||
import { INITIALIZE_SEED_PHRASE_INTRO_ROUTE } from '../../../helpers/constants/routes';
|
||||
import FormField from '../../../components/ui/form-field';
|
||||
import Box from '../../../components/ui/box';
|
||||
import CheckBox from '../../../components/ui/check-box';
|
||||
|
||||
export default function NewAccount({ onSubmit }) {
|
||||
const t = useI18nContext();
|
||||
const [confirmPassword, setConfirmPassword] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [passwordError, setPasswordError] = useState('');
|
||||
const [confirmPasswordError, setConfirmPasswordError] = useState('');
|
||||
const [termsChecked, setTermsChecked] = useState(false);
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const history = useHistory();
|
||||
|
||||
const submitPasswordEvent = useNewMetricEvent({
|
||||
event: 'Submit Password',
|
||||
category: 'Onboarding',
|
||||
});
|
||||
|
||||
const isValid = useMemo(() => {
|
||||
if (!password || !confirmPassword || password !== confirmPassword) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (password.length < 8) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return !passwordError && !confirmPasswordError;
|
||||
}, [password, confirmPassword, passwordError, confirmPasswordError]);
|
||||
|
||||
const handlePasswordChange = (passwordInput) => {
|
||||
let error = '';
|
||||
let confirmError = '';
|
||||
if (passwordInput && passwordInput.length < 8) {
|
||||
error = t('passwordNotLongEnough');
|
||||
}
|
||||
|
||||
if (confirmPassword && passwordInput !== confirmPassword) {
|
||||
confirmError = t('passwordsDontMatch');
|
||||
}
|
||||
|
||||
setPassword(passwordInput);
|
||||
setPasswordError(error);
|
||||
setConfirmPasswordError(confirmError);
|
||||
};
|
||||
|
||||
const handleConfirmPasswordChange = (confirmPasswordInput) => {
|
||||
let error = '';
|
||||
if (password !== confirmPasswordInput) {
|
||||
error = t('passwordsDontMatch');
|
||||
}
|
||||
|
||||
setConfirmPassword(confirmPasswordInput);
|
||||
setConfirmPasswordError(error);
|
||||
};
|
||||
|
||||
const handleCreate = async (event) => {
|
||||
event.preventDefault();
|
||||
|
||||
if (!isValid) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
if (onSubmit) {
|
||||
await onSubmit(password);
|
||||
}
|
||||
submitPasswordEvent();
|
||||
history.push(INITIALIZE_SEED_PHRASE_INTRO_ROUTE);
|
||||
} catch (error) {
|
||||
setPasswordError(error.message);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="new-account__wrapper">
|
||||
<Typography variant={TYPOGRAPHY.H2} fontWeight={FONT_WEIGHT.BOLD}>
|
||||
{t('createPassword')}
|
||||
</Typography>
|
||||
<Typography
|
||||
variant={TYPOGRAPHY.H4}
|
||||
align={TEXT_ALIGN.CENTER}
|
||||
boxProps={{ margin: 5 }}
|
||||
>
|
||||
{t('passwordSetupDetails')}
|
||||
</Typography>
|
||||
<Box
|
||||
justifyContent={JUSTIFY_CONTENT.CENTER}
|
||||
marginTop={3}
|
||||
padding={[0, 12]}
|
||||
>
|
||||
<form className="new-account__form" onSubmit={handleCreate}>
|
||||
<FormField
|
||||
autoFocus
|
||||
error={passwordError}
|
||||
onChange={handlePasswordChange}
|
||||
password={!showPassword}
|
||||
titleText={t('newPassword')}
|
||||
value={password}
|
||||
titleDetail={
|
||||
<button
|
||||
className="new-account__form--password-button"
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
setShowPassword(!showPassword);
|
||||
}}
|
||||
>
|
||||
{showPassword ? t('hide') : t('show')}
|
||||
</button>
|
||||
}
|
||||
/>
|
||||
<FormField
|
||||
onChange={handleConfirmPasswordChange}
|
||||
password={!showPassword}
|
||||
error={confirmPasswordError}
|
||||
titleText={t('confirmPassword')}
|
||||
value={confirmPassword}
|
||||
titleDetail={
|
||||
isValid && (
|
||||
<div className="new-account__form--checkmark">
|
||||
<i className="fas fa-check" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
/>
|
||||
<Box
|
||||
alignItems={ALIGN_ITEMS.CENTER}
|
||||
justifyContent={JUSTIFY_CONTENT.SPACE_BETWEEN}
|
||||
marginBottom={4}
|
||||
>
|
||||
<CheckBox
|
||||
onClick={() => setTermsChecked(!termsChecked)}
|
||||
checked={termsChecked}
|
||||
/>
|
||||
<Typography variant={TYPOGRAPHY.H5} boxProps={{ marginLeft: 3 }}>
|
||||
{t('passwordTermsWarning', [
|
||||
<a
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
key="new-account__link-text"
|
||||
href="https://metamask.io/terms.html"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<span className="new-account__link-text">
|
||||
{t('learnMore')}
|
||||
</span>
|
||||
</a>,
|
||||
])}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Button
|
||||
type="primary"
|
||||
className="new-account__form--submit-button"
|
||||
disabled={!isValid || !termsChecked}
|
||||
onClick={handleCreate}
|
||||
rounded
|
||||
>
|
||||
{t('createNewWallet')}
|
||||
</Button>
|
||||
</form>
|
||||
</Box>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
NewAccount.propTypes = {
|
||||
onSubmit: PropTypes.func,
|
||||
};
|
Loading…
Reference in New Issue
Block a user