1
0
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:
Alex Donesky 2021-07-06 17:19:11 -05:00 committed by GitHub
parent 68ad9c619f
commit b1e2005a73
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 480 additions and 106 deletions

View File

@ -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"
},

View File

@ -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,
};

View File

@ -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
/>
</>
)}

View 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,
};

View 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" />;
};

View File

@ -0,0 +1 @@
export { default } from './form-field';

View 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;
}
}
}

View File

@ -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';

View 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;
}
}
}

View 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,
};