diff --git a/package.json b/package.json index dbe67b9c..b1fea4ca 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "plug", - "version": "0.6.1.1", + "version": "0.6.1.2", "description": "Your plug into the Internet Computer", "private": true, "repository": "https://github.com/Psychedelic/plug", diff --git a/source/components/Alert/index.jsx b/source/components/Alert/index.jsx index 22f9a04e..3904c10d 100644 --- a/source/components/Alert/index.jsx +++ b/source/components/Alert/index.jsx @@ -11,6 +11,7 @@ const Alert = ({ startIcon, endIcon, url, + className, }) => { const classes = useStyles(); @@ -32,7 +33,7 @@ const Alert = ({ }; return ( -
+
{startIcon && icon(type)} { subtitle @@ -56,6 +57,7 @@ Alert.defaultProps = { endIcon: false, url: null, subtitle: null, + className: '', }; Alert.propTypes = { @@ -65,4 +67,5 @@ Alert.propTypes = { startIcon: PropTypes.bool, endIcon: PropTypes.bool, url: PropTypes.string, + className: PropTypes.string, }; diff --git a/source/components/Button/index.jsx b/source/components/Button/index.jsx index 2667aaa4..6e8aaa61 100644 --- a/source/components/Button/index.jsx +++ b/source/components/Button/index.jsx @@ -27,13 +27,22 @@ const VARIANTS = { }; const Button = ({ - value, onClick, variant, loading, disabled, fullWidth, wrapperStyle, buttonTestId, ...other + value, + onClick, + variant, + loading, + disabled, + fullWidth, + wrapperStyle, + buttonTestId, + className, + ...other }) => { const classes = useStyles(); return (
{ +const FormInput = forwardRef(({ + id, label, fullWidth, containerClassName, ...props +}, ref) => { const classes = useStyles(); return ( - + {label} @@ -19,8 +21,6 @@ const FormInput = forwardRef(({ id, label, ...props }, ref) => { ); }); -export default FormInput; - FormInput.propTypes = { id: PropTypes.string.isRequired, label: PropTypes.string.isRequired, @@ -28,4 +28,13 @@ FormInput.propTypes = { value: PropTypes.string.isRequired, onChange: PropTypes.func.isRequired, error: PropTypes.bool.isRequired, + fullWidth: PropTypes.bool, + containerClassName: PropTypes.string, +}; + +FormInput.defaultProps = { + fullWidth: true, + containerClassName: '', }; + +export default FormInput; diff --git a/source/components/FormItem/index.jsx b/source/components/FormItem/index.jsx index 1eca9d5c..c99b74fd 100644 --- a/source/components/FormItem/index.jsx +++ b/source/components/FormItem/index.jsx @@ -5,12 +5,12 @@ import clsx from 'clsx'; import useStyles from './styles'; const FormItem = ({ - label, component, subtitle, smallLabel, endIcon, ...other + label, component, subtitle, smallLabel, endIcon, className, ...other }) => { const classes = useStyles(); return ( -
+
; - -export const Default = Template.bind({}); -Default.args = {}; diff --git a/source/components/FullscreenContainer/index.jsx b/source/components/FullscreenContainer/index.jsx deleted file mode 100644 index f50994de..00000000 --- a/source/components/FullscreenContainer/index.jsx +++ /dev/null @@ -1,26 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import Container from '@material-ui/core/Container'; -import MadeBy from '../MadeBy'; -import useStyles from './styles'; - -const FullscreenContainer = ({ children, maxWidth }) => { - const classes = useStyles(); - return ( -
-
- -
- - {children} - -
- ); -}; - -export default FullscreenContainer; - -FullscreenContainer.propTypes = { - children: PropTypes.node.isRequired, - maxWidth: PropTypes.string.isRequired, -}; diff --git a/source/components/FullscreenContainer/styles.js b/source/components/FullscreenContainer/styles.js deleted file mode 100644 index f1f9bf27..00000000 --- a/source/components/FullscreenContainer/styles.js +++ /dev/null @@ -1,23 +0,0 @@ -import { makeStyles } from '@material-ui/core/styles'; - -export default makeStyles((theme) => ({ - root: { - display: 'flex', - alignItems: 'center', - justifyContent: 'center', - textAlign: 'center', - width: '100%', - height: '100%', - minHeight: '100vh', - padding: `${theme.spacing(4)}px 0`, - position: 'relative', - background: - 'linear-gradient(122.45deg, rgba(255, 231, 1, 0.2) 15.68%, rgba(250, 81, 211, 0.2) 39.58%, rgba(16, 217, 237, 0.2) 63.84%, rgba(82, 255, 83, 0.2) 85.21%)', - }, - madeByContainer: { - position: 'absolute', - top: theme.spacing(3), - right: theme.spacing(3), - display: 'flex', - }, -})); diff --git a/source/components/IDInput/index.jsx b/source/components/IDInput/index.jsx index 18d2642b..ee619a5d 100644 --- a/source/components/IDInput/index.jsx +++ b/source/components/IDInput/index.jsx @@ -6,7 +6,7 @@ import BookIcon from '@assets/icons/notebook.svg'; import clsx from 'clsx'; import { useTranslation } from 'react-i18next'; -import { CircularProgress, Grid } from '@material-ui/core'; +import { CircularProgress, Grid, Typography } from '@material-ui/core'; import { getRandomEmoji } from '@shared/constants/emojis'; import { getContacts, addContact as addContactAction } from '@redux/contacts'; @@ -31,23 +31,23 @@ const IDInput = ({ const [isContactsOpened, setIsContactsOpened] = useState(false); const [contactName, setContactName] = useState(''); const [openContacts, setOpenContacts] = useState(false); + const [contactsError, setContactsError] = useState(null); const { principalId, accountId } = useSelector((state) => state.wallet); - const { groupedContacts: contacts } = useSelector((state) => state.contacts); + const { contacts } = useSelector((state) => state.contacts); const { contactsLoading } = useSelector((state) => state.contacts); useEffect(() => { - dispatch(getContacts()); + dispatch(getContacts()); }, []); const isUserAddress = useMemo( () => [principalId, accountId].includes(value), [principalId, accountId, value], ); - const inContacts = useMemo(() => contacts - .flatMap((c) => c.contacts) - .map((c) => c.id) - .includes(value), [contacts, value]); + const inContacts = useMemo(() => !!contacts.find( + (contact) => (contact.id === value), + ), [contacts, value]); const shouldDisplayAddToContacts = value !== null && value !== '' && !loading && isValid && !inContacts && !isUserAddress; @@ -85,14 +85,17 @@ const IDInput = ({ }; const handleChangeContactName = (e) => { - setContactName(e.target.value); + const name = e.target.value; + const existingContact = contacts.find((contact) => contact.name === name); + const error = existingContact ? t('contacts.errorExists').replace('{name}', existingContact.name) : null; + setContactsError(error); + setContactName(name); }; const searchContactFromId = (e) => { const id = e.target.value; onChange(id); - const allContacts = contacts.flatMap((contact) => contact.contacts); - setSelectedContact(allContacts.find((contact) => contact.id === id)); + setSelectedContact(contacts.find((contact) => contact.id === id)); }; return ( @@ -178,20 +181,24 @@ const IDInput = ({ label={t('contacts.name')} smallLabel component={( - + <> + + {contactsError} + )} /> )} confirmText={t('common.add')} buttonVariant="rainbow" onClick={addContact} - submitButtonProps={{ 'data-testid': 'confirm-adding-contact-button' }} + submitButtonProps={{ 'data-testid': 'confirm-adding-contact-button', disabled: !contactName || !!contactsError }} onClose={() => setIsContactsOpened(false)} /> )} diff --git a/source/components/IDInput/styles.js b/source/components/IDInput/styles.js index b8107ab7..74911513 100644 --- a/source/components/IDInput/styles.js +++ b/source/components/IDInput/styles.js @@ -90,7 +90,7 @@ export default makeStyles((theme) => ({ animationName: '$appear', animationDuration: '0.5s', }, - errorMessage: { + danger: { marginTop: 5, color: theme.palette.danger.main, }, diff --git a/source/components/RevealSeedPhrase/index.jsx b/source/components/SeedPhrase/components/RevealSeedPhrase/index.jsx similarity index 74% rename from source/components/RevealSeedPhrase/index.jsx rename to source/components/SeedPhrase/components/RevealSeedPhrase/index.jsx index 621d4d0d..1cfea7f1 100644 --- a/source/components/RevealSeedPhrase/index.jsx +++ b/source/components/SeedPhrase/components/RevealSeedPhrase/index.jsx @@ -14,11 +14,8 @@ const RevealSeedPhrase = ({ onClick, ...other }) => { data-testid="reveal-seedphrase-button" {...other} > -
-
- - {t('seedPhrase.reveal')} -
+ + {t('seedPhrase.reveal')}
); }; diff --git a/source/components/RevealSeedPhrase/reveal-seed-phrase.stories.jsx b/source/components/SeedPhrase/components/RevealSeedPhrase/reveal-seed-phrase.stories.jsx similarity index 100% rename from source/components/RevealSeedPhrase/reveal-seed-phrase.stories.jsx rename to source/components/SeedPhrase/components/RevealSeedPhrase/reveal-seed-phrase.stories.jsx diff --git a/source/components/RevealSeedPhrase/styles.js b/source/components/SeedPhrase/components/RevealSeedPhrase/styles.js similarity index 65% rename from source/components/RevealSeedPhrase/styles.js rename to source/components/SeedPhrase/components/RevealSeedPhrase/styles.js index 1d6dda14..c2fe2d4f 100644 --- a/source/components/RevealSeedPhrase/styles.js +++ b/source/components/SeedPhrase/components/RevealSeedPhrase/styles.js @@ -4,23 +4,12 @@ export default makeStyles((theme) => ({ root: { width: 350, height: 134, - position: 'relative', cursor: 'pointer', transition: 'transform 0.3s', '&:hover': { transform: 'scale(1.03)', }, - }, - blur: { - width: '100%', - height: '100%', - background: - 'linear-gradient(96.51deg, rgba(255, 231, 1, 0.8) 8.72%, rgba(255, 0, 196, 0.8) 38.27%, rgba(0, 232, 255, 0.8) 68.28%, rgba(0, 255, 1, 0.8) 94.7%)', - filter: 'blur(25px)', - borderRadius: 300, - }, - center: { position: 'absolute', top: 0, bottom: 0, @@ -31,6 +20,7 @@ export default makeStyles((theme) => ({ margin: 'auto', display: 'flex', alignItems: 'center', + zIndex: 100000, }, text: { fontWeight: 700, diff --git a/source/components/SeedPhrase/index.jsx b/source/components/SeedPhrase/index.jsx index 7140d98c..745e324d 100644 --- a/source/components/SeedPhrase/index.jsx +++ b/source/components/SeedPhrase/index.jsx @@ -2,14 +2,20 @@ import React, { useState } from 'react'; import { useTranslation } from 'react-i18next'; import PropTypes from 'prop-types'; import Tooltip from '@material-ui/core/Tooltip'; +import clsx from 'clsx'; + import ListItem from '../ListItem'; +import RevealSeedPhrase from './components/RevealSeedPhrase'; import useStyles from './styles'; -const SeedPhrase = ({ words, seedPhraseBoxTestId }) => { +const SeedPhrase = ({ + words, seedPhraseBoxTestId, onReveal, className, +}) => { const classes = useStyles(); const { t } = useTranslation(); const [copied, setCopied] = useState(false); + const [revealed, setRevealed] = useState(false); const copyText = t('copy.copyText'); const copiedText = t('copy.copiedText'); @@ -31,6 +37,10 @@ const SeedPhrase = ({ words, seedPhraseBoxTestId }) => { }, 3000); }; + const handleReveal = () => { + setRevealed(true); + onReveal(); + }; return ( { open={showTooltip || copied} placement="top" > -
handleClick()} - data-testid={seedPhraseBoxTestId} - onMouseOver={() => setShowTooltip(true)} - onMouseLeave={() => setShowTooltip(false)} - > - { - words.map((word, i) => ( +
+
handleClick()} + data-testid={seedPhraseBoxTestId} + onMouseOver={() => setShowTooltip(true)} + onMouseLeave={() => setShowTooltip(false)} + > + {words.map((word, i) => (
- )) - } - { - showTooltip - &&
- } + ))} + {showTooltip &&
} +
+ {!revealed && ( + <> + +
+
+
+
+
+
+ + )}
); @@ -66,9 +84,12 @@ export default SeedPhrase; SeedPhrase.defaultProps = { seedPhraseBoxTestId: 'seed-phrase-box', + className: '', }; SeedPhrase.propTypes = { + onReveal: PropTypes.func.isRequired, words: PropTypes.arrayOf(PropTypes.string).isRequired, seedPhraseBoxTestId: PropTypes.string, + className: PropTypes.string, }; diff --git a/source/components/SeedPhrase/styles.js b/source/components/SeedPhrase/styles.js index 1b31d8b0..ed5d82d6 100644 --- a/source/components/SeedPhrase/styles.js +++ b/source/components/SeedPhrase/styles.js @@ -2,16 +2,21 @@ import { makeStyles } from '@material-ui/core/styles'; import SHADOW_1 from '@shared/styles/shadows'; import RAINBOW_GRADIENT from '@shared/styles/gradients'; +const RAINBOW_GRADIENT_BG = 'linear-gradient(96.51deg, rgba(255, 231, 1, 0.8) 8.72%, rgba(255, 0, 196, 0.8) 38.27%, rgba(0, 232, 255, 0.8) 68.28%, rgba(0, 255, 1, 0.8) 94.7%)'; + export default makeStyles((theme) => ({ root: { + position: 'relative', + }, + seedContainer: { background: theme.palette.common.white, boxShadow: SHADOW_1, borderRadius: 10, padding: 2, - position: 'relative', cursor: 'pointer', + maxWidth: 550, width: '100%', - height: '100%', + height: 185, display: 'flex', flexDirection: 'row', flexWrap: 'wrap', @@ -44,4 +49,31 @@ export default makeStyles((theme) => ({ tooltip: { margin: '8px 0', }, + blurContainer: { + position: 'absolute', + height: '100%', + width: '100%', + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + top: 0, + left: 0, + }, + blur: { + height: '100%', + width: '100%', + margin: 12, + inset: 0, + zIndex: 2, + backdropFilter: 'blur(7px)', + borderRadius: 10, + }, + rainbowBg: { + width: '100%', + height: '80%', + maxWidth: 350, + background: RAINBOW_GRADIENT_BG, + filter: 'blur(25px)', + borderRadius: 300, + }, })); diff --git a/source/components/index.js b/source/components/index.js index a4c768d8..99991437 100644 --- a/source/components/index.js +++ b/source/components/index.js @@ -8,12 +8,10 @@ export { default as Actions } from './Actions'; export { default as Tokens } from './Tokens'; export { default as Activity } from './Activity'; export { default as Apps } from './Apps'; -export { default as RevealSeedPhrase } from './RevealSeedPhrase'; export { default as SeedPhrase } from './SeedPhrase'; export { default as QRCode } from './QRCode'; export { default as SwapInfo } from './SwapInfo'; export { default as IDInput } from './IDInput'; -export { default as MadeBy } from './MadeBy'; export { default as ActionDialog } from './ActionDialog'; export { default as ContactItem } from './ContactItem'; export { default as ContactList } from './ContactList'; @@ -60,8 +58,6 @@ export { default as AssetFormat } from './AssetFormat'; export { default as USDFormat } from './USDFormat'; export { default as IncomingAction } from './IncomingAction'; export { default as DataDisplay } from './DataDisplay'; -export { default as FullscreenContainer } from './FullscreenContainer'; -export { default as ActionCard } from './ActionCard'; export { default as Alert } from './Alert'; export { default as CanisterInfoContainer } from './CanisterInfo/components/Container'; export { default as CanisterInfoItem } from './CanisterInfo/components/Item'; diff --git a/source/locales/en/translation.json b/source/locales/en/translation.json index aa91353f..c17f2806 100644 --- a/source/locales/en/translation.json +++ b/source/locales/en/translation.json @@ -21,10 +21,6 @@ "copyTextAddress": "Copy address to clipboard", "copiedText": "Copied!" }, - "importPEM": { - "added-account": "Account already added", - "invalid-key": "Invalid PEM file" - }, "profile": { "myAccounts": "My Accounts", "createAccount": "Create Account", @@ -291,7 +287,8 @@ "seedMessage": "Keep this sh*t safe, forreal.", "seedCheckbox": "I wrote down the Secret Recovery Phrase and put it somewhere safe.", "passwordShortError": "The minimum is 8 characters, smh!", - "passwordMatchError": "The passwords gotta match, smh!" + "passwordMatchError": "The passwords gotta match, smh!", + "pasteMnemonic": "You can paste the totality of your secret phrase into any input" }, "contacts": { "title": "Contacts", @@ -408,7 +405,9 @@ "or": "or", "browse": "browse", "fileNotSupported": "File not supported. Try a different file.", - "dropIt": "Drop it!" + "dropIt": "Drop it!", + "added-account": "Account already added", + "invalid-key": "Invalid PEM file" }, "nfts": { "allNfts": "All NFTs", diff --git a/source/shared/utils/array.js b/source/shared/utils/array.js index 2b573b07..f45e2ea2 100644 --- a/source/shared/utils/array.js +++ b/source/shared/utils/array.js @@ -1,2 +1,3 @@ // eslint-disable-next-line export const areAllElementsIn = (elements, array) => elements.every((element) => array.includes(element)); +export const createArray = (length, fillValue) => new Array(length).fill(fillValue); diff --git a/source/views/Extension/Views/ImportWallet/hooks/useSteps.jsx b/source/views/Extension/Views/ImportWallet/hooks/useSteps.jsx index 8f7384d8..103699eb 100644 --- a/source/views/Extension/Views/ImportWallet/hooks/useSteps.jsx +++ b/source/views/Extension/Views/ImportWallet/hooks/useSteps.jsx @@ -1,12 +1,12 @@ -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect } from "react"; import { HANDLER_TYPES, sendMessage } from "@background/Keyring"; -import { toast } from 'react-toastify'; -import { LinkButton } from '@components'; -import { useTranslation } from 'react-i18next'; -import { useRouter } from '@components/Router'; -import BackIcon from '@assets/icons/back.svg'; -import Step1 from '../Steps/Step1'; -import Step2 from '../Steps/Step2'; +import { toast } from "react-toastify"; +import { LinkButton } from "@components"; +import { useTranslation } from "react-i18next"; +import { useRouter } from "@components/Router"; +import BackIcon from "@assets/icons/back.svg"; +import Step1 from "../Steps/Step1"; +import Step2 from "../Steps/Step2"; const useSteps = () => { const [step, setStep] = useState(0); @@ -18,10 +18,18 @@ const useSteps = () => { const [importDisabled, setImportDisabled] = useState(true); const handleChangeStep = (index) => setStep(index); - const handleClose = () => navigator.navigate('home'); + const handleClose = () => navigator.navigate("home"); - const leftButton = (onClick) => ; - const rightButton = ; + const leftButton = (onClick) => ( + + ); + const rightButton = ( + + ); useEffect(() => { if (userPemFile === null) { @@ -31,22 +39,32 @@ const useSteps = () => { setValidateLoading(true); let fileReader = new FileReader(); - fileReader.readAsText(userPemFile); + try { + fileReader.readAsText(userPemFile); + } catch (e) { + toast(t("importPem.fileNotSupported"), { type: "error" }); + setUserPemFile(null); + + return; + } fileReader.onloadend = async () => { const content = fileReader.result; - sendMessage({ - type: HANDLER_TYPES.VALIDATE_PEM, - params: { pem: content }, - }, (response) => { - const { isValid, errorType } = response; - if (!isValid) { - toast(t(`importPEM.${errorType}`), { type: 'error' }); - setUserPemFile(null); + sendMessage( + { + type: HANDLER_TYPES.VALIDATE_PEM, + params: { pem: content }, + }, + (response) => { + const { isValid, errorType } = response; + if (!isValid) { + toast(t(`importPem.${errorType}`), { type: "error" }); + setUserPemFile(null); + } + setValidateLoading(false); + setImportDisabled(!isValid); } - setValidateLoading(false); - setImportDisabled(!isValid); - }); + ); }; }, [userPemFile]); @@ -66,10 +84,7 @@ const useSteps = () => { center: `${t("importPem.importPEMfile")}`, }, { - component: , + component: , right: rightButton, center: `${t("importPem.walletDetails")}`, }, diff --git a/source/views/Extension/Views/NFTDetails/index.jsx b/source/views/Extension/Views/NFTDetails/index.jsx index 535f21d6..57830500 100644 --- a/source/views/Extension/Views/NFTDetails/index.jsx +++ b/source/views/Extension/Views/NFTDetails/index.jsx @@ -53,9 +53,9 @@ const NFTDetails = () => { const name = `${nft?.name ?? `#${nft?.index}`}`; const isICNS = collection?.name === 'ICNS'; - const openNFT = (url) => () => { + const openNFT = (url) => { const parsedUrl = isICNS - ? `https://icns.id/domains/${populatedNFT?.name.replace('.icp', '')}/detail` + ? `https://icns.id/domains/${nft?.name.replace('.icp', '')}/detail` : url; extension.tabs.create({ @@ -84,7 +84,7 @@ const NFTDetails = () => { openNFT(nft?.url)} data-testid="expand-nft" /> )} @@ -129,7 +129,7 @@ const NFTDetails = () => { {populatedNFT?.desc} )} - {populatedNFT?.metadata?.properties?.filter((prop) => typeof prop?.value !== 'object')?.length >= 1 && ( + {!isICNS && populatedNFT?.metadata?.properties?.filter((prop) => typeof prop?.value !== 'object')?.length >= 1 && (
{ populatedNFT?.metadata?.properties?.map((prop) => (( diff --git a/source/components/ActionCard/action-card.stories.jsx b/source/views/Options/Views/Welcome/components/ActionCard/action-card.stories.jsx similarity index 100% rename from source/components/ActionCard/action-card.stories.jsx rename to source/views/Options/Views/Welcome/components/ActionCard/action-card.stories.jsx diff --git a/source/components/ActionCard/index.jsx b/source/views/Options/Views/Welcome/components/ActionCard/index.jsx similarity index 96% rename from source/components/ActionCard/index.jsx rename to source/views/Options/Views/Welcome/components/ActionCard/index.jsx index bd94a24c..d55dd091 100644 --- a/source/components/ActionCard/index.jsx +++ b/source/views/Options/Views/Welcome/components/ActionCard/index.jsx @@ -1,7 +1,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import { Typography } from '@material-ui/core'; -import Button from '../Button'; +import { Button } from '@components'; import useStyles from './styles'; const ActionCard = ({ diff --git a/source/components/ActionCard/styles.js b/source/views/Options/Views/Welcome/components/ActionCard/styles.js similarity index 89% rename from source/components/ActionCard/styles.js rename to source/views/Options/Views/Welcome/components/ActionCard/styles.js index 5bb7b7e9..62c644cf 100644 --- a/source/components/ActionCard/styles.js +++ b/source/views/Options/Views/Welcome/components/ActionCard/styles.js @@ -11,6 +11,8 @@ export default makeStyles((theme) => ({ width: '100%', background: 'rgba(255, 255, 255, 0.4)', borderRadius: 15, + maxWidth: 420, + margin: theme.spacing(1), }, image: { marginBottom: theme.spacing(1), diff --git a/source/views/Options/Views/Welcome/components/Header/index.jsx b/source/views/Options/Views/Welcome/components/Header/index.jsx deleted file mode 100644 index b03e098c..00000000 --- a/source/views/Options/Views/Welcome/components/Header/index.jsx +++ /dev/null @@ -1,25 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { Plug } from '@components'; -import { Typography } from '@material-ui/core'; -import useStyles from './styles'; - -const Header = ({ title, subtitle, message }) => { - const classes = useStyles(); - - return ( -
- - {title} - {subtitle} -
- ); -}; - -export default Header; - -Header.propTypes = { - title: PropTypes.string.isRequired, - subtitle: PropTypes.string.isRequired, - message: PropTypes.string.isRequired, -}; diff --git a/source/views/Options/Views/Welcome/components/Header/styles.js b/source/views/Options/Views/Welcome/components/Header/styles.js deleted file mode 100644 index 76bb6b2a..00000000 --- a/source/views/Options/Views/Welcome/components/Header/styles.js +++ /dev/null @@ -1,15 +0,0 @@ -import { makeStyles } from '@material-ui/core/styles'; - -export default makeStyles((theme) => ({ - root: { - height: 188, - display: 'flex', - alignItems: 'center', - justifyContent: 'space-between', - flexDirection: 'column', - marginBottom: theme.spacing(1), - }, - subtitle: { - width: '102%', - }, -})); diff --git a/source/components/MadeBy/index.jsx b/source/views/Options/Views/Welcome/components/MadeByFleek/index.jsx similarity index 87% rename from source/components/MadeBy/index.jsx rename to source/views/Options/Views/Welcome/components/MadeByFleek/index.jsx index 4e33b19a..15276a0e 100644 --- a/source/components/MadeBy/index.jsx +++ b/source/views/Options/Views/Welcome/components/MadeByFleek/index.jsx @@ -3,7 +3,7 @@ import { fleekUrl } from '@shared/constants/urls'; import ThunderImg from '@assets/icons/options/thunder.svg'; import useStyles from './styles'; -const MadeBy = () => { +const MadeByFleek = () => { const classes = useStyles(); return ( @@ -13,4 +13,4 @@ const MadeBy = () => { ); }; -export default MadeBy; +export default MadeByFleek; diff --git a/source/components/MadeBy/styles.js b/source/views/Options/Views/Welcome/components/MadeByFleek/styles.js similarity index 85% rename from source/components/MadeBy/styles.js rename to source/views/Options/Views/Welcome/components/MadeByFleek/styles.js index 20996100..1e0ba734 100644 --- a/source/components/MadeBy/styles.js +++ b/source/views/Options/Views/Welcome/components/MadeByFleek/styles.js @@ -8,6 +8,9 @@ export default makeStyles((theme) => ({ fontSize: 12, fontWeight: 700, alignItems: 'center', + position: 'absolute', + top: 30, + right: 30, }, image: { marginLeft: 3, diff --git a/source/views/Options/Views/Welcome/components/MnemonicInput/index.jsx b/source/views/Options/Views/Welcome/components/MnemonicInput/index.jsx new file mode 100644 index 00000000..cec7bdbb --- /dev/null +++ b/source/views/Options/Views/Welcome/components/MnemonicInput/index.jsx @@ -0,0 +1,118 @@ +import React, { useCallback, useState } from 'react'; +import PropTypes from 'prop-types'; +import { useTranslation } from 'react-i18next'; +import { Typography } from '@material-ui/core'; +import Info from '@assets/icons/info.svg'; + +import { TextInput } from '@components'; +import { createArray } from '@shared/utils/array'; + +import useStyles from './styles'; +import ShowHideToggle from '../ShowHideToggle'; +import { clearClipboard, parseMnemonic } from './utils'; + +const MNEMONIC_LENGTH = 12; + +const MnemonicInput = ({ onChange, draftMnemonic }) => { + const { t } = useTranslation(); + const classes = useStyles(); + const [pasteFailed, setPasteFailed] = useState(false); + const [showMnemonic, setShowMnemonic] = useState(createArray(MNEMONIC_LENGTH, false)); + + const toggleShowMnemonic = useCallback((index) => { + setShowMnemonic((currentShowMnemonic) => { + const newShowMnemonic = currentShowMnemonic.slice(); + if (newShowMnemonic[index]) { + newShowMnemonic[index] = false; + } else { + newShowMnemonic.fill(false); + newShowMnemonic[index] = true; + } + return newShowMnemonic; + }); + }, []); + + const onMnemonicWordChange = useCallback( + (index, newWord) => { + setPasteFailed(false); + const newMnemonic = draftMnemonic.slice(); + newMnemonic[index] = newWord.trim(); + onChange(newMnemonic); + }, + [draftMnemonic, onChange], + ); + + const onMnemonicPaste = useCallback( + (rawMnemonic) => { + const parsedMnemonic = parseMnemonic(rawMnemonic); + let newDraftMnemonic = parsedMnemonic.split(' '); + if (newDraftMnemonic.length > MNEMONIC_LENGTH) { + setPasteFailed(true); + return; + } + setPasteFailed(false); + + // If paste content is shorter than 12 words, fill the rest with empty strings + if (newDraftMnemonic.length < MNEMONIC_LENGTH) { + newDraftMnemonic = newDraftMnemonic.concat( + createArray(MNEMONIC_LENGTH - newDraftMnemonic.length, ''), + ); + } + setShowMnemonic(createArray(MNEMONIC_LENGTH, false)); + onChange(newDraftMnemonic); + clearClipboard(); + }, + [onChange, pasteFailed, setPasteFailed], + ); + + return ( +
+
+ info + {t('welcome.pasteMnemonic')} +
+
+ {[...Array(MNEMONIC_LENGTH).keys()].map((index) => { + const id = `mnemonic-word-${index}`; + return ( +
+ {`${index + 1}.`} + { + e.preventDefault(); + onMnemonicWordChange(index, e.target.value); + }} + value={draftMnemonic[index]} + autoComplete="off" + onPaste={(event) => { + const newMnemonic = event.clipboardData.getData('text'); + if (newMnemonic.trim().match(/\s/u)) { + event.preventDefault(); + onMnemonicPaste(newMnemonic); + } + }} + /> + toggleShowMnemonic(index)} + label={t('MnemonicToggleShow')} + /> +
+ ); + })} +
+
+ ); +}; + +MnemonicInput.propTypes = { + onChange: PropTypes.func.isRequired, + draftMnemonic: PropTypes.arrayOf(PropTypes.string).isRequired, +}; + +export default MnemonicInput; diff --git a/source/views/Options/Views/Welcome/components/MnemonicInput/styles.js b/source/views/Options/Views/Welcome/components/MnemonicInput/styles.js new file mode 100644 index 00000000..e22d2ada --- /dev/null +++ b/source/views/Options/Views/Welcome/components/MnemonicInput/styles.js @@ -0,0 +1,71 @@ +import { makeStyles } from '@material-ui/core/styles'; + +export default makeStyles((theme) => ({ + mnemonicContainer: { + width: '100%', + maxWidth: 702, + }, + container: { + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + }, + mnemonicWordsContainer: { + display: 'flex', + flexWrap: 'wrap', + justifyContent: 'center', + alignItems: 'center', + width: '100%', + marginBottom: theme.spacing(2), + }, + mnemonicWordInputContainer: { + width: '100%', + maxWidth: 200, + margin: 15, + display: 'flex', + alignItems: 'center', + justifyContent: 'space-between', + }, + mnemonicWordAction: { + display: 'flex', + flexDirection: 'row', + justifyContent: 'flex-end', + alignItems: 'center', + gap: '8px', + }, + mnemonicWordInput: { + height: 30, + maxWidth: 150, + }, + divider: { + width: '100%', + height: 1, + backgroundColor: 'rgba(0, 0, 0, 0.16)', + marginBottom: theme.spacing(2), + }, + footerContainer: { + padding: '0 45px', + }, + passwordInputContainer: { + marginBottom: theme.spacing(2), + display: 'flex', + justifyContent: 'space-between', + width: '100%', + }, + passwordInput: { + maxWidth: 290, + }, + pasteMessage: { + display: 'flex', + alignItems: 'center', + color: '#767676', + opacity: 0.9, + marginBottom: 10, + }, + infoIcon: { + marginRight: 5, + height: 16, + width: 16, + opacity: 0.5, + }, +})); diff --git a/source/views/Options/Views/Welcome/components/MnemonicInput/utils.js b/source/views/Options/Views/Welcome/components/MnemonicInput/utils.js new file mode 100644 index 00000000..596f8133 --- /dev/null +++ b/source/views/Options/Views/Welcome/components/MnemonicInput/utils.js @@ -0,0 +1,5 @@ +export function clearClipboard() { + window.navigator.clipboard.writeText(''); +} + +export const parseMnemonic = (mnemonic) => (mnemonic || '').trim().toLowerCase().match(/\w+/gu)?.join(' ') || ''; diff --git a/source/views/Options/Views/Welcome/components/ShowHideToggle/index.jsx b/source/views/Options/Views/Welcome/components/ShowHideToggle/index.jsx new file mode 100644 index 00000000..cd8fb1d3 --- /dev/null +++ b/source/views/Options/Views/Welcome/components/ShowHideToggle/index.jsx @@ -0,0 +1,42 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +import { Eye, EyeOff } from 'react-feather'; + +import useStyles from './styles'; + +const ShowHideToggle = ({ + show, label, name, onChange, disabled, size, +}) => { + const classes = useStyles(); + return ( +
+ + +
+ ); +}; + +ShowHideToggle.propTypes = { + show: PropTypes.bool.isRequired, + label: PropTypes.string.isRequired, + name: PropTypes.string.isRequired, + onChange: PropTypes.func.isRequired, + disabled: PropTypes.bool.isRequired, + size: PropTypes.number, +}; + +ShowHideToggle.defaultProps = { + size: 17, +}; + +export default ShowHideToggle; diff --git a/source/views/Options/Views/Welcome/components/ShowHideToggle/styles.js b/source/views/Options/Views/Welcome/components/ShowHideToggle/styles.js new file mode 100644 index 00000000..c09cc3d5 --- /dev/null +++ b/source/views/Options/Views/Welcome/components/ShowHideToggle/styles.js @@ -0,0 +1,14 @@ +import { makeStyles } from "@material-ui/core/styles"; + +export default makeStyles(() => ({ + showHideToggleContainer: { + height: 17, + width: 17, + "& > label": { + cursor: "pointer", + }, + }, + showHideToggleInput: { + display: "none", + }, +})); diff --git a/source/views/Options/Views/Welcome/hooks/useSteps.jsx b/source/views/Options/Views/Welcome/hooks/useSteps.jsx index 9e6d0c95..4fe101e2 100644 --- a/source/views/Options/Views/Welcome/hooks/useSteps.jsx +++ b/source/views/Options/Views/Welcome/hooks/useSteps.jsx @@ -1,9 +1,9 @@ -import React, { useState } from 'react'; +import React, { useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { ActionsStep, CreatePasswordStep, - ImportWalletStep, + ImportMnemonicStep, MemeStep, SeedPhraseStep, } from '../steps'; @@ -24,8 +24,8 @@ const useSteps = () => { setCurrentBranch(branch); handleNextStep(); }; - const handleSetMnemonic = (value) => setMnemonic(value); + useEffect(() => setMnemonic(null), [currentBranch]); const branches = { import: [ @@ -41,20 +41,11 @@ const useSteps = () => { title: t('welcome.importWallet'), subtitle: t('welcome.importSubtitle'), message: t('welcome.importMessage'), - component: , }, - { - title: t('welcome.passwordTitle'), - subtitle: t('welcome.passwordSubtitle'), - message: t('welcome.passwordMessage'), - component: , - }, { title: t('welcome.memeTitle'), subtitle: t('welcome.memeSubtitle'), diff --git a/source/views/Options/Views/Welcome/index.jsx b/source/views/Options/Views/Welcome/index.jsx index eef4f0f2..f3a74701 100644 --- a/source/views/Options/Views/Welcome/index.jsx +++ b/source/views/Options/Views/Welcome/index.jsx @@ -1,11 +1,13 @@ import React from 'react'; -import Grid from '@material-ui/core/Grid'; -import { FullscreenContainer, LinkButton } from '@components'; -import BackIcon from '@assets/icons/back.svg'; import { useTranslation } from 'react-i18next'; -import Header from './components/Header'; -import useSteps from './hooks/useSteps'; +import { Typography } from '@material-ui/core'; + +import BackIcon from '@assets/icons/back.svg'; +import { LinkButton, Plug } from '@components'; + import useStyles from './styles'; +import MadeByFleek from './components/MadeByFleek'; +import useSteps from './hooks/useSteps'; const Welcome = () => { const { t } = useTranslation(); @@ -17,26 +19,24 @@ const Welcome = () => { } = useSteps(); const step = steps[currentStep]; - + const isMiddleStep = currentStep > 0 && currentStep < steps.length - 1; return ( - - - { - (currentStep > 0 && currentStep < steps.length - 1) - && ( +
+ +
+ {isMiddleStep && (
- ) - } - -
- - { - step.component - } - - + )} + + {step.title} + {step.subtitle} +
+
+ {step.component} +
+
); }; diff --git a/source/views/Options/Views/Welcome/steps/ActionsStep.jsx b/source/views/Options/Views/Welcome/steps/ActionsStep.jsx deleted file mode 100644 index 855f0aec..00000000 --- a/source/views/Options/Views/Welcome/steps/ActionsStep.jsx +++ /dev/null @@ -1,43 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { ActionCard } from '@components'; -import Grid from '@material-ui/core/Grid'; -import ImportImg from '@assets/icons/options/importwallet.svg'; -import CreateImg from '@assets/icons/options/createwallet.svg'; -import { useTranslation } from 'react-i18next'; - -const ActionsStep = ({ handleChangeBranch }) => { - const { t } = useTranslation(); - - return ( - <> - - handleChangeBranch('import')} - buttonProps={{ 'data-testid': 'import-wallet-button' }} - /> - - - - handleChangeBranch('create')} - buttonProps={{ 'data-testid': 'create-wallet-button' }} - /> - - - ); -}; - -export default ActionsStep; - -ActionsStep.propTypes = { - handleChangeBranch: PropTypes.func.isRequired, -}; diff --git a/source/views/Options/Views/Welcome/steps/ActionsStep/index.jsx b/source/views/Options/Views/Welcome/steps/ActionsStep/index.jsx new file mode 100644 index 00000000..1ab0ef7d --- /dev/null +++ b/source/views/Options/Views/Welcome/steps/ActionsStep/index.jsx @@ -0,0 +1,40 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { useTranslation } from 'react-i18next'; + +import ImportImg from '@assets/icons/options/importwallet.svg'; +import CreateImg from '@assets/icons/options/createwallet.svg'; + +import ActionCard from '../../components/ActionCard'; +import useStyles from './styles'; + +const ActionsStep = ({ handleChangeBranch }) => { + const { t } = useTranslation(); + const classes = useStyles(); + return ( +
+ handleChangeBranch('import')} + buttonProps={{ 'data-testid': 'import-wallet-button' }} + /> + handleChangeBranch('create')} + buttonProps={{ 'data-testid': 'create-wallet-button' }} + /> +
+ ); +}; + +export default ActionsStep; + +ActionsStep.propTypes = { + handleChangeBranch: PropTypes.func.isRequired, +}; diff --git a/source/views/Options/Views/Welcome/steps/ActionsStep/styles.js b/source/views/Options/Views/Welcome/steps/ActionsStep/styles.js new file mode 100644 index 00000000..95ec9f0f --- /dev/null +++ b/source/views/Options/Views/Welcome/steps/ActionsStep/styles.js @@ -0,0 +1,11 @@ +import { makeStyles } from '@material-ui/core/styles'; + +export default makeStyles(() => ({ + actionsStepContainer: { + display: 'flex', + justifyContent: 'center', + maxWidth: 900, + width: '100%', + flexWrap: 'wrap', + }, +})); diff --git a/source/views/Options/Views/Welcome/steps/CreatePasswordStep.jsx b/source/views/Options/Views/Welcome/steps/CreatePasswordStep/index.jsx similarity index 61% rename from source/views/Options/Views/Welcome/steps/CreatePasswordStep.jsx rename to source/views/Options/Views/Welcome/steps/CreatePasswordStep/index.jsx index 47dcf955..52b398a5 100644 --- a/source/views/Options/Views/Welcome/steps/CreatePasswordStep.jsx +++ b/source/views/Options/Views/Welcome/steps/CreatePasswordStep/index.jsx @@ -1,6 +1,5 @@ import React, { useState } from 'react'; import PropTypes from 'prop-types'; -import Grid from '@material-ui/core/Grid'; import { useTranslation } from 'react-i18next'; import { Alert, Button, FormItem, TextInput, @@ -9,7 +8,7 @@ import { HANDLER_TYPES, sendMessage } from '@background/Keyring'; import facepalmEmoji from '@assets/icons/facepalm.svg'; import { getRandomEmoji } from '@shared/constants/emojis'; import { clearStorage } from '@modules/storageManager'; -import useStyles from '../styles'; +import useStyles from './styles'; const CreatePasswordStep = ({ handleNextStep, handleSetMnemonic, mnemonic }) => { const { t } = useTranslation(); @@ -61,55 +60,51 @@ const CreatePasswordStep = ({ handleNextStep, handleSetMnemonic, mnemonic }) => const handleKeyPress = (e) => e.key === 'Enter' && handleCreateAccount(); return ( - <> - - +
+ )} - /> - - - + /> + )} - /> - - -
); }; diff --git a/source/views/Options/Views/Welcome/steps/CreatePasswordStep/styles.js b/source/views/Options/Views/Welcome/steps/CreatePasswordStep/styles.js new file mode 100644 index 00000000..61fb6c41 --- /dev/null +++ b/source/views/Options/Views/Welcome/steps/CreatePasswordStep/styles.js @@ -0,0 +1,27 @@ +import { makeStyles } from '@material-ui/core/styles'; + +export default makeStyles((theme) => ({ + createPasswordContainer: { + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + justifyContent: 'center', + textAlign: 'center', + width: '100%', + height: '100%', + maxWidth: 600, + }, + marginBottom: { + marginBottom: theme.spacing(2), + }, + passwordError: { + display: 'flex', + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center', + + '& > img': { + marginRight: '1rem', + }, + }, +})); diff --git a/source/views/Options/Views/Welcome/steps/ImportMnemonicStep/index.jsx b/source/views/Options/Views/Welcome/steps/ImportMnemonicStep/index.jsx new file mode 100644 index 00000000..e8a2a4b8 --- /dev/null +++ b/source/views/Options/Views/Welcome/steps/ImportMnemonicStep/index.jsx @@ -0,0 +1,161 @@ +import React, { useCallback, useEffect, useState } from 'react'; +import PropTypes from 'prop-types'; +import { useTranslation } from 'react-i18next'; +import * as bip39 from 'bip39'; + +import { HANDLER_TYPES, sendMessage } from '@background/Keyring'; +import { clearStorage } from '@modules/storageManager'; +import { getRandomEmoji } from '@shared/constants/emojis'; +import { createArray } from '@shared/utils/array'; +import { Alert, Button, FormInput } from '@components'; +import facepalmEmoji from '@assets/icons/facepalm.svg'; + +import useStyles from './styles'; +import MnemonicInput from '../../components/MnemonicInput'; + +const MNEMONIC_LENGTH = 12; + +export function clearClipboard() { + window.navigator.clipboard.writeText(''); +} + +const isValidMnemonic = (mnemonic) => bip39.validateMnemonic(mnemonic); + +const ImportWalletStep = ({ handleNextStep, handleSetMnemonic }) => { + const { t } = useTranslation(); + const classes = useStyles(); + const [draftMnemonic, setDraftMnemonic] = useState( + createArray(MNEMONIC_LENGTH, ''), + ); + const [confirmPassword, setConfirmPassword] = useState(''); + const [confirmPasswordError, setConfirmPasswordError] = useState(''); + const [password, setPassword] = useState(''); + const [passwordError, setPasswordError] = useState(''); + + const [invalidMnemonic, setInvalidMnemonic] = useState(true); + + const onMnemonicChange = (newDraftMnemonic) => { + let newMnemonicError = ''; + const joinedDraftMnemonic = newDraftMnemonic.join(' ').trim().toLowerCase(); + if (newDraftMnemonic.some((word) => word !== '')) { + if (newDraftMnemonic.some((word) => word === '')) { + newMnemonicError = t('seedPhraseReq'); + } else if (!isValidMnemonic(joinedDraftMnemonic)) { + setInvalidMnemonic(true); + newMnemonicError = t('invalidSeedPhrase'); + } + } + setInvalidMnemonic(newMnemonicError?.length); + setDraftMnemonic(newDraftMnemonic); + handleSetMnemonic(newMnemonicError ? '' : newDraftMnemonic); + }; + + const handleCreateAccount = async () => { + // clean the storage before initiating keyring + clearStorage(); + const params = { + password, + mnemonic: draftMnemonic.join(' '), + icon: getRandomEmoji(), + }; + sendMessage({ type: HANDLER_TYPES.IMPORT, params }, (response) => { + handleSetMnemonic(response?.mnemonic); + }); + handleNextStep(); + }; + + const onPasswordChange = useCallback( + (event) => { + const newPassword = event.target.value; + let newConfirmPasswordError = ''; + let newPasswordError = ''; + + if (newPassword && newPassword.length < 8) { + newPasswordError = t('welcome.passwordShortError'); + } + + if (confirmPassword && newPassword !== confirmPassword) { + newConfirmPasswordError = t('welcome.passwordMatchError'); + } + + setPassword(newPassword); + setPasswordError(newPasswordError); + setConfirmPasswordError(newConfirmPasswordError); + }, + [confirmPassword, t], + ); + + const onConfirmPasswordChange = useCallback( + (event) => { + const newConfirmPassword = event.target.value; + let newConfirmPasswordError = ''; + + if (password !== newConfirmPassword) { + newConfirmPasswordError = t('welcome.passwordMatchError'); + } + + setConfirmPassword(newConfirmPassword); + setConfirmPasswordError(newConfirmPasswordError); + }, + [password, t], + ); + const isDisabled = invalidMnemonic + || !password + || !confirmPassword + || passwordError + || confirmPasswordError; + + useEffect(() => setDraftMnemonic(createArray(MNEMONIC_LENGTH, '')), []); + return ( +
+ +
+
+
+ + +
+
+
+ ); +}; + +export default ImportWalletStep; + +ImportWalletStep.propTypes = { + handleNextStep: PropTypes.func.isRequired, + handleSetMnemonic: PropTypes.func.isRequired, +}; diff --git a/source/views/Options/Views/Welcome/steps/ImportMnemonicStep/styles.js b/source/views/Options/Views/Welcome/steps/ImportMnemonicStep/styles.js new file mode 100644 index 00000000..0ab79f57 --- /dev/null +++ b/source/views/Options/Views/Welcome/steps/ImportMnemonicStep/styles.js @@ -0,0 +1,69 @@ +import { makeStyles } from '@material-ui/core/styles'; + +export default makeStyles((theme) => ({ + mnemonicContainer: { + width: '100%', + maxWidth: 702, + }, + mnemonicWordsContainer: { + display: 'flex', + flexWrap: 'wrap', + justifyContent: 'center', + alignItems: 'center', + width: '100%', + marginBottom: theme.spacing(2), + }, + mnemonicWordInputContainer: { + width: '100%', + maxWidth: 200, + margin: 15, + display: 'flex', + alignItems: 'center', + justifyContent: 'space-between', + }, + mnemonicWordAction: { + display: 'flex', + flexDirection: 'row', + justifyContent: 'flex-end', + alignItems: 'center', + gap: '8px', + }, + mnemonicWordInput: { + height: 30, + maxWidth: 150, + }, + divider: { + width: '100%', + height: 1, + backgroundColor: 'rgba(0, 0, 0, 0.16)', + marginBottom: theme.spacing(2), + }, + footerContainer: { + padding: '0 45px', + }, + passwordInputContainer: { + marginBottom: theme.spacing(2), + display: 'flex', + justifyContent: 'space-between', + width: '100%', + }, + passwordInput: { + maxWidth: 290, + }, + submitButton: { + marginBottom: 20, + }, + pwdAlert: { + margin: '20px 0', + }, + passwordError: { + display: 'flex', + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center', + + '& > img': { + marginRight: '1rem', + }, + }, +})); diff --git a/source/views/Options/Views/Welcome/steps/ImportWalletStep.jsx b/source/views/Options/Views/Welcome/steps/ImportWalletStep.jsx deleted file mode 100644 index ab1262f9..00000000 --- a/source/views/Options/Views/Welcome/steps/ImportWalletStep.jsx +++ /dev/null @@ -1,75 +0,0 @@ -import React, { useState } from 'react'; -import PropTypes from 'prop-types'; -import Grid from '@material-ui/core/Grid'; -import { useTranslation } from 'react-i18next'; -import { Button, FormItem, TextInput } from '@components'; -import * as bip39 from 'bip39'; - -const ImportWalletStep = ({ handleNextStep, handleSetMnemonic }) => { - const { t } = useTranslation(); - - const [text, setText] = useState(''); - const [invalidMnemonic, setInvalidMnemonic] = useState(false); - - const handleChangeText = (e) => { - setText(e.target.value); - setInvalidMnemonic(false); - }; - - const handleImportMnemonic = () => { - const trimmedText = text.trim(); - - const isValid = bip39.validateMnemonic(trimmedText); - if (isValid) { - handleSetMnemonic(trimmedText); - handleNextStep(); - } else { - setInvalidMnemonic(true); - } - }; - - const handleValidateMnemonic = () => ( - text === '' - || text.trim().split(/\s+/g).length !== 12 - || invalidMnemonic - ); - - return ( - <> - - - )} - /> - - -
+ ); + } + + return null; +}; + +export default SeedPhraseStep; + +SeedPhraseStep.defaultProps = { + mnemonic: null, +}; + +SeedPhraseStep.propTypes = { + handleNextStep: PropTypes.func.isRequired, + mnemonic: PropTypes.arrayOf(PropTypes.string), +}; diff --git a/source/views/Options/Views/Welcome/steps/SeedPhraseStep/styles.js b/source/views/Options/Views/Welcome/steps/SeedPhraseStep/styles.js new file mode 100644 index 00000000..4abeb0b4 --- /dev/null +++ b/source/views/Options/Views/Welcome/steps/SeedPhraseStep/styles.js @@ -0,0 +1,20 @@ +import { makeStyles } from '@material-ui/core/styles'; + +export default makeStyles((theme) => ({ + seedphraseStepContainer: { + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + width: '100%', + maxWidth: 545, + }, + seedPhraseContainer: { + height: 185, + position: 'relative', + marginBottom: theme.spacing(2), + }, + checkbox: { + marginBottom: theme.spacing(2), + paddingLeft: 5, + }, +})); diff --git a/source/views/Options/Views/Welcome/steps/index.jsx b/source/views/Options/Views/Welcome/steps/index.jsx index e071e1f1..8ed89ac0 100644 --- a/source/views/Options/Views/Welcome/steps/index.jsx +++ b/source/views/Options/Views/Welcome/steps/index.jsx @@ -1,5 +1,5 @@ export { default as ActionsStep } from './ActionsStep'; export { default as CreatePasswordStep } from './CreatePasswordStep'; -export { default as ImportWalletStep } from './ImportWalletStep'; +export { default as ImportMnemonicStep } from './ImportMnemonicStep'; export { default as MemeStep } from './MemeStep'; export { default as SeedPhraseStep } from './SeedPhraseStep'; diff --git a/source/views/Options/Views/Welcome/styles.js b/source/views/Options/Views/Welcome/styles.js index 69121760..a2785a8e 100644 --- a/source/views/Options/Views/Welcome/styles.js +++ b/source/views/Options/Views/Welcome/styles.js @@ -1,40 +1,43 @@ import { makeStyles } from '@material-ui/core/styles'; export default makeStyles((theme) => ({ - goBack: { - position: 'absolute', - left: 0, - top: 0, - }, - blur: { - height: 'calc(100% - 24px)', - width: 'calc(100% - 24px)', - position: 'absolute', - margin: 12, - inset: 0, - zIndex: 2, - backdropFilter: 'blur(7px)', - borderRadius: 10, - - '@supports ( -moz-appearance:none )': { - background: 'white', - }, + welcomeContainer: { + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + justifyContent: 'center', + textAlign: 'center', + width: '100%', + height: '100%', + minHeight: '100vh', + padding: `${theme.spacing(4)}px 0`, + position: 'relative', + background: + 'linear-gradient(122.45deg, rgba(255, 231, 1, 0.2) 15.68%, rgba(250, 81, 211, 0.2) 39.58%, rgba(16, 217, 237, 0.2) 63.84%, rgba(82, 255, 83, 0.2) 85.21%)', }, - marginBottom: { + headerContainer: { + height: 190, + maxWidth: 540, + width: '100%', + display: 'flex', + alignItems: 'center', + justifyContent: 'space-between', + flexDirection: 'column', marginBottom: theme.spacing(2), }, - passwordError: { + stepContainer: { + width: '100%', + maxWidth: 950, + padding: theme.spacing(1), display: 'flex', - flexDirection: 'row', - alignItems: 'center', justifyContent: 'center', - - '& > img': { - marginRight: '1rem', - }, + }, + goBack: { + alignSelf: 'flex-start', }, memeContainer: { width: '100%', + maxWidth: 400, background: 'white', borderRadius: 5, display: 'flex', diff --git a/tests/e2e/edit-account.test.js b/tests/e2e/edit-account.test.js index 99622f8c..e34fb555 100644 --- a/tests/e2e/edit-account.test.js +++ b/tests/e2e/edit-account.test.js @@ -89,7 +89,7 @@ const learnMoreButtonClick = async (browser, page) => { const newPage = await newPagePromise; const url = await newPage.evaluate(() => document.location.href); - expect(url).toBe('https://medium.com/@plug_wallet/669b192a2ace'); + expect(url).toContain('https://medium.com/plugwallet/internet-computer-ids-101-669b192a2ace'); }; const waitForWalletName = async (page, walletName, timeout = 1000) => { diff --git a/tests/e2e/import-create-account.test.js b/tests/e2e/import-create-account.test.js index d6e34811..82a23049 100644 --- a/tests/e2e/import-create-account.test.js +++ b/tests/e2e/import-create-account.test.js @@ -1,3 +1,5 @@ +const { fillSeedPhraseInput } = require('../utils/seed'); + const MINIMUM_PLUG_VERSION_ALLOWED = 60; const getErrorMessage = async (page, error) => { @@ -36,10 +38,7 @@ describe('Import/Create', () => { }); test('failing on incorrect seedphrase', async () => { - const seedphraseTextarea = await page.getByTestId('seedphrase-input'); - await seedphraseTextarea.click(); - await seedphraseTextarea.type(badSeedphrase); - + await fillSeedPhraseInput(page, badSeedphrase); const submitImportButton = await page.getByTestId('confirm-seedphrase-button'); await submitImportButton.click(); @@ -52,14 +51,12 @@ describe('Import/Create', () => { }); test('failing on missmatched passwords', async () => { - const seedphraseTextarea = await page.getByTestId('seedphrase-input'); - await seedphraseTextarea.click(); - await seedphraseTextarea.type(secrets.seedphrase); + await fillSeedPhraseInput(page, secrets.seedphrase); const submitImportButton = await page.getByTestId('confirm-seedphrase-button'); await submitImportButton.click(); - const newPasswordInput = await page.getByTestId('new-password-input'); + const newPasswordInput = await page.getByTestId('enter-password-input'); const confirmPasswordInput = await page.getByTestId('confirm-password-input'); await newPasswordInput.click(); @@ -67,21 +64,19 @@ describe('Import/Create', () => { await confirmPasswordInput.click(); await confirmPasswordInput.type('MissMatchedPassword'); - const submitPasswordButton = await page.getByTestId('password-confirmation-button'); + const submitPasswordButton = await page.getByTestId('confirm-seedphrase-button'); await submitPasswordButton.click(); await getErrorMessage(page, 'The passwords gotta match, smh!'); }); test('failing on password shorter than 12 characters', async () => { - const seedphraseTextarea = await page.getByTestId('seedphrase-input'); - await seedphraseTextarea.click(); - await seedphraseTextarea.type(secrets.seedphrase); + await fillSeedPhraseInput(page, secrets.seedphrase); const submitImportButton = await page.getByTestId('confirm-seedphrase-button'); await submitImportButton.click(); - const newPasswordInput = await page.getByTestId('new-password-input'); + const newPasswordInput = await page.getByTestId('enter-password-input'); const confirmPasswordInput = await page.getByTestId('confirm-password-input'); await newPasswordInput.click(); @@ -89,21 +84,19 @@ describe('Import/Create', () => { await confirmPasswordInput.click(); await confirmPasswordInput.type('123'); - const submitPasswordButton = await page.getByTestId('password-confirmation-button'); + const submitPasswordButton = await page.getByTestId('confirm-seedphrase-button'); await submitPasswordButton.click(); await getErrorMessage(page, 'The minimum is 8 characters, smh!'); }); test('importing wallet correctly', async () => { - const seedphraseTextarea = await page.getByTestId('seedphrase-input'); - await seedphraseTextarea.click(); - await seedphraseTextarea.type(secrets.seedphrase); + await fillSeedPhraseInput(page, secrets.seedphrase); const submitImportButton = await page.getByTestId('confirm-seedphrase-button'); await submitImportButton.click(); - const newPasswordInput = await page.getByTestId('new-password-input'); + const newPasswordInput = await page.getByTestId('enter-password-input'); const confirmPasswordInput = await page.getByTestId('confirm-password-input'); await newPasswordInput.click(); @@ -111,7 +104,7 @@ describe('Import/Create', () => { await confirmPasswordInput.click(); await confirmPasswordInput.type(secrets.password); - const submitPasswordButton = await page.getByTestId('password-confirmation-button'); + const submitPasswordButton = await page.getByTestId('confirm-seedphrase-button'); await submitPasswordButton.click(); await page.goto(chromeData.popupUrl); @@ -139,7 +132,7 @@ describe('Import/Create', () => { }); test('fails on missmatched passwords', async () => { - const newPasswordInput = await page.getByTestId('new-password-input'); + const newPasswordInput = await page.getByTestId('enter-password-input'); await newPasswordInput.click(); await newPasswordInput.type('TestPassword123'); @@ -154,7 +147,7 @@ describe('Import/Create', () => { }); test('fails on password shorter than 12 characters', async () => { - const newPasswordInput = await page.getByTestId('new-password-input'); + const newPasswordInput = await page.getByTestId('enter-password-input'); await newPasswordInput.click(); await newPasswordInput.type('123'); @@ -169,7 +162,7 @@ describe('Import/Create', () => { }); test('correctly creates', async () => { - const newPasswordInput = await page.getByTestId('new-password-input'); + const newPasswordInput = await page.getByTestId('enter-password-input'); await newPasswordInput.click(); await newPasswordInput.type(secrets.password); diff --git a/tests/setup/jestSetup.js b/tests/setup/jestSetup.js index ece314b5..1c65b90a 100644 --- a/tests/setup/jestSetup.js +++ b/tests/setup/jestSetup.js @@ -1,3 +1,5 @@ +const { fillSeedPhraseInput } = require('../utils/seed'); + require('dotenv').config(); const PAGE_TITLE = 'Plug'; @@ -122,23 +124,18 @@ const importAccount = async (page, seedphrase, password) => { const importButton = await getByTestId(page, 'import-wallet-button'); await importButton.click(); - const seedphraseTextArea = await getByTestId(page, 'seedphrase-input'); - await seedphraseTextArea.click(); - await seedphraseTextArea.type(seedphrase); - - const confirmSeedphraseButton = await getByTestId(page, 'confirm-seedphrase-button'); - await confirmSeedphraseButton.click(); + await fillSeedPhraseInput(page, seedphrase); - const newPasswordInput = await getByTestId(page, 'new-password-input'); - const confirmPasswordInput = await getByTestId(page, 'confirm-password-input'); + const newPasswordInput = await getByTestId(page, 'enter-password-input'); + const confirmPasswordInput = await getByTestId(page, 'confirm-password-input', true); await newPasswordInput.click(); await newPasswordInput.type(password); await confirmPasswordInput.click(); await confirmPasswordInput.type(password); - const submitPasswordButton = await getByTestId(page, 'password-confirmation-button'); - await submitPasswordButton.click(); + const confirmSeedphraseButton = await getByTestId(page, 'confirm-seedphrase-button'); + await confirmSeedphraseButton.click(); }; const unlock = async (page, password) => { diff --git a/tests/utils/seed.js b/tests/utils/seed.js new file mode 100644 index 00000000..450cbbf3 --- /dev/null +++ b/tests/utils/seed.js @@ -0,0 +1,12 @@ +const fillSeedPhraseInput = async (page, seedPhrase) => { + const seedArray = seedPhrase.trim().split(' '); + for (let i = 0; i < seedArray.length; i += 1) { + const seedPhraseInput = await page.getByTestId(`seedphrase-input-${i + 1}`); + await seedPhraseInput.click(); + await seedPhraseInput.type(seedArray[i]); + } +}; + +module.exports = { + fillSeedPhraseInput, +};