diff --git a/package.json b/package.json index e7ef0620..8865fae7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "plug", - "version": "0.6.0", + "version": "0.6.1", "description": "Your plug into the Internet Computer", "private": true, "repository": "https://github.com/Psychedelic/plug", @@ -37,8 +37,8 @@ "@material-ui/icons": "^4.11.2", "@metamask/post-message-stream": "^4.0.0", "@psychedelic/browser-rpc": "2.1.0", - "@psychedelic/dab-js": "1.4.12", - "@psychedelic/plug-controller": "0.22.6", + "@psychedelic/dab-js": "1.5.0-beta.1", + "@psychedelic/plug-controller": "0.24.4", "@psychedelic/plug-inpage-provider": "^2.3.1", "@reduxjs/toolkit": "^1.6.0", "advanced-css-reset": "^1.2.2", @@ -58,7 +58,9 @@ "random-color": "^1.0.1", "react": "^17.0.1", "react-collapsible": "^2.8.4", + "react-cool-virtual": "^0.7.0", "react-dom": "^17.0.1", + "react-dropzone": "^14.2.2", "react-feather": "^2.0.9", "react-i18next": "^11.8.13", "react-json-view": "^1.21.3", diff --git a/source/Background/Keyring/index.js b/source/Background/Keyring/index.js index 2522959a..6d5825d3 100644 --- a/source/Background/Keyring/index.js +++ b/source/Background/Keyring/index.js @@ -107,6 +107,11 @@ export const HANDLER_TYPES = { REMOVE_NETWORK: 'remove-network', SET_CURRENT_NETWORK: 'set-current-network', GET_CURRENT_NETWORK: 'get-current-network', + IMPORT_PEM_ACCOUNT: 'import-pem-account', + REMOVE_PEM_ACCOUNT: 'remove-pem-account', + REMOVE_CUSTOM_TOKEN: 'remove-custom-token', + GET_PRINCIPAL_FROM_PEM: 'get-principal-from-pem', + VALIDATE_PEM: 'validate-pem', }; export const getKeyringErrorMessage = (type) => ({ @@ -141,6 +146,8 @@ export const getKeyringErrorMessage = (type) => ({ [HANDLER_TYPES.SET_CURRENT_NETWORK]: 'setting the current network', [HANDLER_TYPES.GET_CURRENT_NETWORK]: 'getting the current network', [HANDLER_TYPES.REMOVE_CUSTOM_TOKEN]: 'removing custom token', + [HANDLER_TYPES.IMPORT_PEM_ACCOUNT]: 'importing account from pem', + [HANDLER_TYPES.REMOVE_PEM_ACCOUNT]: 'removing pem account', }[type]); export const sendMessage = (args, callback) => { @@ -157,6 +164,15 @@ export const sendMessage = (args, callback) => { }); }; +export const asyncSendMessage = (args, callback) => new Promise((resolve) => { + sendMessage(args, (response) => { + if (callback) { + callback(response); + } + resolve(response); + }); +}); + export const getKeyringHandler = (type, keyring) => ({ [HANDLER_TYPES.LOCK]: async () => keyring.lock(), [HANDLER_TYPES.UNLOCK]: async (params) => { @@ -182,7 +198,9 @@ export const getKeyringHandler = (type, keyring) => ({ return null; } }, + [HANDLER_TYPES.IMPORT_PEM_ACCOUNT]: keyring.importAccountFromPem, [HANDLER_TYPES.CREATE_PRINCIPAL]: async (params) => keyring.createPrincipal(params), + [HANDLER_TYPES.REMOVE_PEM_ACCOUNT]: async (params) => keyring.deleteImportedAccount(params), [HANDLER_TYPES.SET_CURRENT_PRINCIPAL]: async (walletId) => { await keyring.setCurrentPrincipal(walletId); @@ -206,22 +224,12 @@ export const getKeyringHandler = (type, keyring) => ({ const parsed = parseTransactions(response); return parsed; }, - [HANDLER_TYPES.GET_ASSETS]: async ({ refresh }) => { + [HANDLER_TYPES.GET_ASSETS]: async () => { try { if (!keyring?.isUnlocked) return {}; - - const { wallets, currentWalletId } = await keyring.getState(); - let assets = Object.values(wallets?.[currentWalletId]?.assets); - const shouldUpdate = Object.values(assets)?.every((asset) => !Number(asset.amount)) - || Object.values(assets)?.some((asset) => asset.amount === 'Error') - || refresh; - if (shouldUpdate) { - assets = await keyring.getBalances(); - } else { - keyring.getBalances(); - } - assets = parseAssetsAmount(assets); - return (assets || []).map((asset) => recursiveParseBigint(asset)); + const assets = await keyring.getBalances(); + const parsedAssets = parseAssetsAmount(assets); + return (parsedAssets || []).map((asset) => recursiveParseBigint(asset)); } catch (e) { // eslint-disable-next-line console.log('Error while fetching the assets', e); @@ -302,9 +310,10 @@ export const getKeyringHandler = (type, keyring) => ({ [HANDLER_TYPES.ADD_CUSTOM_NFT]: async ({ canisterId, standard }) => { try { - const nfts = await keyring.registerNFT({ + await keyring.registerNFT({ canisterId, standard, }); + const nfts = await keyring.getNFTs({ refresh: true }); return (nfts || []).map((nft) => recursiveParseBigint(nft)); } catch (e) { // eslint-disable-next-line @@ -339,18 +348,15 @@ export const getKeyringHandler = (type, keyring) => ({ return { error: e.message }; } }, - [HANDLER_TYPES.GET_NFTS]: async ({ refresh = false }) => { - const { wallets, currentWalletId } = await keyring.getState(); - let collections = wallets?.[currentWalletId]?.collections || []; - if (!collections.length || refresh) { - collections = await keyring.getNFTs({ subaccount: currentWalletId, refresh }); - } + [HANDLER_TYPES.GET_NFTS]: async ({ refresh } = { refresh: false }) => { + const collections = await keyring.getNFTs({ refresh }); return (collections || [])?.map((collection) => recursiveParseBigint(collection)); }, [HANDLER_TYPES.TRANSFER_NFT]: async ({ to, nft }) => { try { - const response = await keyring.transferNFT({ to, token: nft }); - return recursiveParseBigint(response); + await keyring.transferNFT({ to, token: nft }); + const nfts = await keyring.getNFTs({ refresh: true }); + return recursiveParseBigint(nfts); } catch (e) { // eslint-disable-next-line console.log('Error transfering NFT', e); @@ -480,6 +486,8 @@ export const getKeyringHandler = (type, keyring) => ({ return { error: e.message }; } }, + [HANDLER_TYPES.GET_PRINCIPAL_FROM_PEM]: keyring.getPrincipalFromPem, + [HANDLER_TYPES.VALIDATE_PEM]: keyring.validatePem, }[type]); export const getContacts = () => new Promise((resolve, reject) => { diff --git a/source/assets/icons/gradient-file.svg b/source/assets/icons/gradient-file.svg new file mode 100644 index 00000000..97a14564 --- /dev/null +++ b/source/assets/icons/gradient-file.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/source/assets/icons/link-emoji.png b/source/assets/icons/link-emoji.png new file mode 100644 index 00000000..64b16ffd Binary files /dev/null and b/source/assets/icons/link-emoji.png differ diff --git a/source/assets/icons/question-hat.svg b/source/assets/icons/question-hat.svg new file mode 100644 index 00000000..531dd54c --- /dev/null +++ b/source/assets/icons/question-hat.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/source/assets/icons/red-warning-icon.svg b/source/assets/icons/red-warning-icon.svg new file mode 100644 index 00000000..a1657145 --- /dev/null +++ b/source/assets/icons/red-warning-icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/source/components/AssetItem/index.jsx b/source/components/AssetItem/index.jsx index 22ce92a4..f8c5faa6 100644 --- a/source/components/AssetItem/index.jsx +++ b/source/components/AssetItem/index.jsx @@ -8,7 +8,6 @@ import clsx from 'clsx'; import Skeleton from 'react-loading-skeleton'; import 'react-loading-skeleton/dist/skeleton.css'; -import { TOKENS } from '@shared/constants/currencies'; import RefreshIcon from '@assets/icons/blue-refresh.png'; import DeleteIcon from '@assets/icons/delete.svg'; import TokenIcon from '../TokenIcon'; @@ -32,7 +31,7 @@ const AssetItem = ({ const classes = useStyles(); const { t } = useTranslation(); const { currentNetwork, usingMainnet } = useSelector((state) => state.network); - const [ shouldRemove, setShouldRemove] = useState(false); + const [shouldRemove, setShouldRemove] = useState(false); const [isHovering, setIsHovering] = useState(false); const [openDelete, setOpenDelete] = useState(false); @@ -61,13 +60,19 @@ const AssetItem = ({ const handleRemoveAssetDisplay = () => { setShouldRemove(true); handleModalClose(); - } + }; const ledgerNotSpecified = !usingMainnet && !currentNetwork?.ledgerCanisterId; return (
: ()} - )} + )} { !failed && !loading && (
setOpenDelete(true)} @@ -170,4 +181,5 @@ AssetItem.propTypes = { failed: PropTypes.bool, assetNameTestId: PropTypes.string, removeAsset: PropTypes.func.isRequired, + protectedAsset: PropTypes.bool.isRequired, }; diff --git a/source/components/ConnectAccountsModal/index.jsx b/source/components/ConnectAccountsModal/index.jsx index dd4d7664..62c6fe1c 100644 --- a/source/components/ConnectAccountsModal/index.jsx +++ b/source/components/ConnectAccountsModal/index.jsx @@ -61,7 +61,9 @@ const ConnectAccountsModal = ({ const handleConfirm = () => { Object.keys(walletsToUpdate).forEach((walletId) => { - walletsToUpdate[walletId] && connectAccountToTab(walletId, tab); + if (walletsToUpdate[walletId]) { + connectAccountToTab(walletId, tab); + } }); onConfirm?.(); setWalletsToUpdate({}); diff --git a/source/components/ConnectionStatus/components/ConnectionControls/index.jsx b/source/components/ConnectionStatus/components/ConnectionControls/index.jsx index 786633f3..cbca0a58 100644 --- a/source/components/ConnectionStatus/components/ConnectionControls/index.jsx +++ b/source/components/ConnectionStatus/components/ConnectionControls/index.jsx @@ -12,12 +12,11 @@ import { setAssetsLoading, setTransactions, setTransactionsLoading, - setCollections, - setCollectionsLoading, } from '@redux/wallet'; import { getApps } from '@modules/storageManager'; import { getCurrentNetwork, getNetworks } from '@redux/network'; import { getContacts } from '@redux/contacts'; +import { getNFTs } from '@redux/nfts'; import { HANDLER_TYPES, sendMessage } from '@background/Keyring'; import { TABS, useRouter } from '@components/Router'; import RefreshAsset from '@assets/icons/refresh.svg'; @@ -29,14 +28,13 @@ const ConnectionControls = ({ disableNavigation, hidden }) => { const classes = useStyles(); const icpPrice = useICPPrice(); const dispatch = useDispatch(); - const { tabIndex } = disableNavigation ? {} : useRouter(); + const { tabIndex, route } = disableNavigation ? {} : useRouter(); const { - principalId, walletId, assetsLoading, transactionsLoading, - collectionsLoading, } = useSelector((state) => state.wallet); + const { collectionsLoading } = useSelector((state) => state.nfts); const { useICNS } = useSelector((state) => state.icns); const { currentNetwork } = useSelector((state) => state.network); const [selectorOpen, setSelectorOpen] = useState(false); @@ -95,21 +93,7 @@ const ConnectionControls = ({ disableNavigation, hidden }) => { }; const loadCollections = () => { - dispatch(setCollectionsLoading(true)); - sendMessage( - { - type: HANDLER_TYPES.GET_NFTS, - params: { refresh: true }, - }, - (nftCollections) => { - if (nftCollections?.length) { - dispatch( - setCollections({ collections: nftCollections, principalId }), - ); - } - dispatch(setCollectionsLoading(false)); - }, - ); + dispatch(getNFTs({ refresh: route === 'home' && tabIndex === TABS.NFTS })); }; const refreshWallet = () => { diff --git a/source/components/ICNSDisplay/index.jsx b/source/components/ICNSDisplay/index.jsx index b19bef03..a6058559 100644 --- a/source/components/ICNSDisplay/index.jsx +++ b/source/components/ICNSDisplay/index.jsx @@ -11,14 +11,13 @@ const ICNSDisplay = ({ }) => { const classes = useStyles(); const [loading, setLoading] = useState(true); - return (
setLoading(false)} - src={icns.url} + src="https://icns.id/Rectangle.jpg" /> {icns.name?.length > 12 ? shortICNSName(icns?.name) : icns?.name} diff --git a/source/views/Extension/Views/WalletDetails/components/InfoModal/index.jsx b/source/components/InfoModal/index.jsx similarity index 100% rename from source/views/Extension/Views/WalletDetails/components/InfoModal/index.jsx rename to source/components/InfoModal/index.jsx diff --git a/source/views/Extension/Views/WalletDetails/components/InfoModal/styles.js b/source/components/InfoModal/styles.js similarity index 100% rename from source/views/Extension/Views/WalletDetails/components/InfoModal/styles.js rename to source/components/InfoModal/styles.js diff --git a/source/components/NFTs/components/NFTCollection/index.jsx b/source/components/NFTs/components/NFTCollection/index.jsx index 868cf44e..4bb6f6a1 100644 --- a/source/components/NFTs/components/NFTCollection/index.jsx +++ b/source/components/NFTs/components/NFTCollection/index.jsx @@ -1,10 +1,11 @@ -import React, { useState } from 'react'; +import React, { useState, useMemo } from 'react'; import Collapsible from 'react-collapsible'; import PropTypes from 'prop-types'; import { useDispatch } from 'react-redux'; import clsx from 'clsx'; import { Typography } from '@material-ui/core'; import { ChevronDown } from 'react-feather'; +import useVirtual from 'react-cool-virtual'; import { shortICNSName } from '@shared/services/ICNS'; import { useRouter } from '@components/Router'; @@ -32,6 +33,21 @@ function NFTCollection({ const toggleExpanded = () => setExpanded(!expanded); const nftDefaultTag = NFT_COLLECTION_DEFAULT_TYPES[collection.canisterId]; + const rows = useMemo(() => { + const itemCount = collection?.tokens?.length || 0; + const COLUMNS = 3; + const rowsCount = Math.ceil(itemCount / COLUMNS); + // Generate an array of rows with 3 items each + const nftRows = Array.from( + Array(rowsCount), (_, i) => collection.tokens.slice(i * COLUMNS, i * COLUMNS + COLUMNS), + ); + return nftRows; + }, [collection]); + + const { outerRef, innerRef, items: itemRows } = useVirtual({ + itemCount: rows?.length || 0, // Provide the total number for the list items + itemSize: 150, // The size of each row (img + title) + }); return ( )} > -
- {collection?.tokens?.map((nft) => { - const name = nft.name || `#${nft.index}`; - return ( -
handleNftClick(nft)} - data-testid={`nft-id-${name}`} - > - {icns ? ( - handleNftClick(nft)} - /> - ) : ( - handleNftClick(nft)} - /> - )} - {!icns && ( - - {name.length > 12 ? shortICNSName(name) : name} - - )} -
- ); - })} +
+ {expanded && ( +
+ {itemRows?.map(({ index, size }) => { + const rowNfts = rows[index]; + return ( +
+ {rowNfts?.map((nft) => { + const name = nft?.name || `#${nft?.index}`; + return ( +
handleNftClick(nft)} + data-testid={`nft-id-${name}`} + style={{ height: `${size}px` }} + > + {icns ? ( + handleNftClick(nft)} + /> + ) : ( + handleNftClick(nft)} + /> + )} + {!icns && ( + + {name.length > 12 ? shortICNSName(name) : name} + + )} +
+ ); + })} +
+ ); + })} +
+ )}
); diff --git a/source/components/NFTs/components/NFTCollection/styles.js b/source/components/NFTs/components/NFTCollection/styles.js index 6eae7495..4bdfda6c 100644 --- a/source/components/NFTs/components/NFTCollection/styles.js +++ b/source/components/NFTs/components/NFTCollection/styles.js @@ -11,7 +11,7 @@ export default makeStyles(() => ({ display: 'grid', justifyContent: 'space-evenly', gridTemplateColumns: 'repeat(auto-fill, 112px)', - overflow: 'hidden', + padding: '0 5px', }, nft: { height: 112, diff --git a/source/components/NFTs/index.jsx b/source/components/NFTs/index.jsx index 5e1aabdc..8f5255e5 100644 --- a/source/components/NFTs/index.jsx +++ b/source/components/NFTs/index.jsx @@ -1,9 +1,9 @@ import React, { useEffect } from 'react'; import { useDispatch, useSelector } from 'react-redux'; -import { HANDLER_TYPES, sendMessage } from '@background/Keyring'; +import { getNFTs } from '@redux/nfts'; + import LoadingWrapper from '../LoadingWrapper'; -import { setCollections, setCollectionsLoading } from '../../redux/wallet'; import useStyles from './styles'; import EmptyState from './components/EmptyState'; import NFTCollection from './components/NFTCollection'; @@ -12,23 +12,12 @@ const NFTs = () => { const classes = useStyles(); const dispatch = useDispatch(); - const { - collections, collectionsLoading, principalId, optimisticNFTUpdate, - } = useSelector((state) => state.wallet); + const { principalId } = useSelector((state) => state.wallet); + const { collections, collectionsLoading } = useSelector((state) => state.nfts); useEffect(() => { - // Update cache if (!collectionsLoading) { - dispatch(setCollectionsLoading(true)); - sendMessage({ - type: HANDLER_TYPES.GET_NFTS, - params: {}, - }, (nftCollections) => { - if (nftCollections?.length && !optimisticNFTUpdate) { - dispatch(setCollections({ collections: nftCollections, principalId })); - } - dispatch(setCollectionsLoading(false)); - }); + dispatch(getNFTs()); } }, [principalId]); diff --git a/source/components/Profile/components/AccountItem/index.jsx b/source/components/Profile/components/AccountItem/index.jsx index c2f00b8d..4676c777 100644 --- a/source/components/Profile/components/AccountItem/index.jsx +++ b/source/components/Profile/components/AccountItem/index.jsx @@ -1,4 +1,5 @@ import React from 'react'; +import PropTypes from 'prop-types'; import clsx from 'clsx'; import { IconButton } from '@material-ui/core'; import { useDispatch } from 'react-redux'; @@ -7,16 +8,17 @@ import { useHiddenAccounts, toggleAccountHidden } from '@redux/profile'; import BluePencil from '@assets/icons/blue-pencil.svg'; import InvisibleIcon from '@assets/icons/invisible.svg'; import VisibleIcon from '@assets/icons/visible.svg'; +import RemoveCircleOutline from '@material-ui/icons/RemoveCircleOutline'; import UserIcon from '../../../UserIcon'; import useStyles from './styles'; const AccountItem = ({ account, - isCurrentAccount, isEditing, handleChangeAccount, handleEditAccount, + handleRemoveAccountModal, }) => { const classes = useStyles(); const hiddenAccounts = useHiddenAccounts(); @@ -34,20 +36,24 @@ const AccountItem = ({ return (
handleChangeAccount(e, account.walletId)} >
-
- - {account.icnsData.reverseResolvedName ? account.icnsData.reverseResolvedName : account.name } +
+ + {account?.icnsData?.reverseResolvedName || account?.name}
@@ -62,16 +68,40 @@ const AccountItem = ({ src={BluePencil} /> - { isEditing && ( - { toggleAccountVisibility(e, account.walletId) }} - > - - + {!(account.type === 'MNEMONIC') && ( + handleRemoveAccountModal(e, account)}> + + + )} + {isEditing && ( + { + toggleAccountVisibility(e, account.walletId); + }} + > + + )}
); }; +AccountItem.propTypes = { + account: PropTypes.shape({ + name: PropTypes.string, + walletId: PropTypes.string, + icon: PropTypes.string, + type: PropTypes.string, + icnsData: PropTypes.shape({ + reverseResolvedName: PropTypes.string, + names: PropTypes.arrayOf(PropTypes.string), + }), + }).isRequired, + isEditing: PropTypes.bool.isRequired, + handleChangeAccount: PropTypes.func.isRequired, + handleRemoveAccountModal: PropTypes.func.isRequired, + handleEditAccount: PropTypes.func.isRequired, +}; + export default AccountItem; diff --git a/source/components/Profile/components/AccountItem/styles.js b/source/components/Profile/components/AccountItem/styles.js index 11e12e71..6f92ed0b 100644 --- a/source/components/Profile/components/AccountItem/styles.js +++ b/source/components/Profile/components/AccountItem/styles.js @@ -1,5 +1,5 @@ import { makeStyles } from '@material-ui/core/styles'; -import SMOOTH_TRANSITION from '@shared/styles/transitions'; +import SMOOTH_TRANSITION from '@shared/styles/transitions'; export default makeStyles(() => ({ accountItemContainer: { diff --git a/source/components/Profile/index.jsx b/source/components/Profile/index.jsx index 93423cbe..252f51a7 100644 --- a/source/components/Profile/index.jsx +++ b/source/components/Profile/index.jsx @@ -1,3 +1,4 @@ +/* eslint-disable max-len */ import React, { useState, useEffect } from 'react'; import MenuList from '@material-ui/core/MenuList'; import Button from '@material-ui/core/Button'; @@ -12,11 +13,12 @@ import { useTranslation } from 'react-i18next'; import extensionizer from 'extensionizer'; import Plus from '@assets/icons/plus.svg'; +import LinkEmoji from '@assets/icons/link-emoji.png'; + import { setAccountInfo, setAssets, setAssetsLoading, - setCollections, setTransactions, } from '@redux/wallet'; import { getRandomEmoji } from '@shared/constants/emojis'; @@ -27,6 +29,7 @@ import { setICNSData } from '@redux/icns'; import { useICPPrice } from '@redux/icp'; import { getContacts } from '@redux/contacts'; import { useMenuItems } from '@hooks'; +import { Dialog, Button as CButton } from '@components'; import ConnectAccountsModal from '../ConnectAccountsModal'; import HoverAnimation from '../HoverAnimation'; import MenuItem from '../MenuItem'; @@ -39,6 +42,8 @@ import { AccountItem } from './components'; import UserIcon from '../UserIcon'; import useStyles from './styles'; +const IMPORT_WALLET_ENABLED = process.env.TARGET_BROWSER !== 'firefox'; + const Profile = ({ disableProfile }) => { const classes = useStyles(); const { t } = useTranslation(); @@ -46,7 +51,7 @@ const Profile = ({ disableProfile }) => { const { navigator } = disableProfile ? {} : useRouter(); const [isEditing, setIsEditing] = useState(false); - const { walletId, principalId } = useSelector((state) => state.wallet); + const { walletId } = useSelector((state) => state.wallet); const icpPrice = useICPPrice(); const [open, setOpen] = useState(false); @@ -60,6 +65,8 @@ const Profile = ({ disableProfile }) => { const [accountName, setAccountName] = useState(''); const [error, setError] = useState(null); const [connectedWallets, setConnectedWallets] = useState([]); + const [selectedRemoveAccount, setSelectedRemovedAccount] = useState(null); + const [openRemoveModal, setOpenRemoveModal] = useState(false); const handleToggle = () => { setOpen((prevOpen) => !prevOpen); @@ -75,7 +82,7 @@ const Profile = ({ disableProfile }) => { setAccounts(walletsArray); } }); - }, []); + }, [open]); const handleChangeAccountName = (e) => { const name = e.target.value; @@ -112,7 +119,6 @@ const Profile = ({ disableProfile }) => { }; const executeAccountSwitch = (wallet) => { - dispatch(setCollections({ collections: [], principalId })); sendMessage({ type: HANDLER_TYPES.SET_CURRENT_PRINCIPAL, params: wallet }, (state) => { const walletsArray = Object.values(state?.wallets); @@ -181,8 +187,44 @@ const Profile = ({ disableProfile }) => { setSelectedWallet(null); }; + const handleOpenImportWallet = () => { + navigator.navigate('import-wallet'); + }; + + const handleRemoveAccountModal = (e, account) => { + e.stopPropagation(); + setOpenRemoveModal(true); + setSelectedRemovedAccount(account); + }; + + const handleRemoveAccount = () => { + sendMessage({ + type: HANDLER_TYPES.REMOVE_PEM_ACCOUNT, + params: selectedRemoveAccount.walletId, + }, () => executeAccountSwitch(accounts[0].walletId)); + setOpenRemoveModal(false); + setOpen(false); + }; + + return ( <> + setOpenRemoveModal(false)} + open={openRemoveModal} + component={( +
+ + Are you sure you want to remove {selectedRemoveAccount?.name} from your account list? + + + You can always add the wallet back by importing it again. + + +
+ )} + /> { isCurrentAccount={isCurrentAccount} handleChangeAccount={handleChangeAccount} handleEditAccount={handleEditAccount} + handleRemoveAccountModal={handleRemoveAccountModal} /> ); }) @@ -290,6 +333,17 @@ const Profile = ({ disableProfile }) => { onClick={handleOpenCreateAccount} data-testid="create-account-button" /> + { IMPORT_WALLET_ENABLED && ( + + )} { menuItems.map((item) => ( diff --git a/source/components/Profile/styles.js b/source/components/Profile/styles.js index 8e441ff0..985a6d39 100644 --- a/source/components/Profile/styles.js +++ b/source/components/Profile/styles.js @@ -28,6 +28,12 @@ export default makeStyles((theme) => ({ minWidth: 37, minHeight: 37, }, + removeAccountDialog: { + padding: '0px 20px 10px 20px', + display: 'flex', + flexDirection: 'column', + gap: 5, + }, label: { width: 'auto', }, diff --git a/source/components/TokenIcon/index.js b/source/components/TokenIcon/index.js index 6a20ddf1..34c1fbb3 100644 --- a/source/components/TokenIcon/index.js +++ b/source/components/TokenIcon/index.js @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useEffect, useState } from 'react'; import PropTypes from 'prop-types'; import { makeStyles } from '@material-ui/core'; import randomColor from 'random-color'; @@ -6,6 +6,7 @@ import clsx from 'clsx'; import { NFT_COLLECTION_DEFAULT_TYPES } from '@shared/constants/nft'; import SHADOW_1 from '@shared/styles/shadows'; +import QuestionHat from '@assets/icons/question-hat.svg'; import NFTDisplayer from '../NFTDisplayer'; const useStyles = makeStyles((theme) => ({ @@ -33,6 +34,14 @@ const useStyles = makeStyles((theme) => ({ }, })); +const isUrl = (urlString) => { + try { + return Boolean(new URL(urlString)); + } catch (e) { + return false; + } +}; + const TokenIcon = ({ logo, symbol, @@ -44,14 +53,28 @@ const TokenIcon = ({ }) => { const classes = useStyles(); const backgroundColor = `rgb(${color.values.rgb.join(',')})`; - + const [resolvedLogo, setResolvedLogo] = useState(null); const nftDefaultTag = NFT_COLLECTION_DEFAULT_TYPES[nft.canisterId]; + useEffect(() => { + if (isUrl(logo)) { + fetch(logo).then((res) => { + if (res.status === 404) { + setResolvedLogo(QuestionHat); + } else { + setResolvedLogo(logo); + } + }); + } else { + setResolvedLogo(logo); + } + }, [logo]); + if (logo) { return nft ? ( ) : ( - + ); } diff --git a/source/views/Extension/Views/WalletDetails/components/DetailItem/index.jsx b/source/components/WalletDetailItem/index.jsx similarity index 84% rename from source/views/Extension/Views/WalletDetails/components/DetailItem/index.jsx rename to source/components/WalletDetailItem/index.jsx index 1bfd1572..7d0a4016 100644 --- a/source/views/Extension/Views/WalletDetails/components/DetailItem/index.jsx +++ b/source/components/WalletDetailItem/index.jsx @@ -4,18 +4,19 @@ import { Typography } from '@material-ui/core'; import { Info } from 'react-feather'; import { useTranslation } from 'react-i18next'; -import { CopyButton, FormItem } from '@components'; +import CopyButton from '../CopyButton'; +import FormItem from '../FormItem'; +import InfoModal from '../InfoModal'; import useStyles from './styles'; -import InfoModal from '../InfoModal'; -const DetailItem = ({ - value, name, setInfoOpen, isOpen, copyButtonTestId, infoIconButtonTestId, +const WalletDetailItem = ({ + value, name, setInfoOpen, isOpen, copyButtonTestId, infoIconButtonTestId, className }) => { const classes = useStyles(); const { t } = useTranslation(); return ( - <> +
- +
); }; -DetailItem.propTypes = { +WalletDetailItem.propTypes = { value: PropTypes.string.isRequired, name: PropTypes.string.isRequired, setInfoOpen: PropTypes.func.isRequired, isOpen: PropTypes.bool.isRequired, copyButtonTestId: PropTypes.string, infoIconButtonTestId: PropTypes.string, + className: PropTypes.string, }; -DetailItem.defaultProps = { +WalletDetailItem.defaultProps = { copyButtonTestId: '', infoIconButtonTestId: '', + className: '', }; -export default DetailItem; +export default WalletDetailItem; diff --git a/source/views/Extension/Views/WalletDetails/components/DetailItem/styles.js b/source/components/WalletDetailItem/styles.js similarity index 100% rename from source/views/Extension/Views/WalletDetails/components/DetailItem/styles.js rename to source/components/WalletDetailItem/styles.js diff --git a/source/components/index.js b/source/components/index.js index 3805efa7..a4c768d8 100644 --- a/source/components/index.js +++ b/source/components/index.js @@ -69,5 +69,7 @@ export { default as Badge } from './Badge'; export { default as Title } from './Title'; export { default as NFTDisplayer } from './NFTDisplayer'; export { default as ICNSDisplay } from './ICNSDisplay'; +export { default as WalletDetailItem } from './WalletDetailItem'; +export { default as InfoModal } from './InfoModal'; export * from './Router'; diff --git a/source/locales/en/translation.json b/source/locales/en/translation.json index 5cc6d360..aa91353f 100644 --- a/source/locales/en/translation.json +++ b/source/locales/en/translation.json @@ -21,9 +21,14 @@ "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", + "importWallet": "Import Wallet", "settings": "Settings", "subaccountNotConnected": "This account is not connected", "wantToConnect": "Do you want to connect your accounts?", @@ -395,6 +400,16 @@ "dfxUse": "dfx identity use ", "selectAccount": "Select Account" }, + "importPem": { + "importPEMfile": "Import PEM file", + "editWalletPic": "Edit Wallet Pic", + "walletDetails": "Wallet Details", + "dragAndDrop": "Drag and Drop", + "or": "or", + "browse": "browse", + "fileNotSupported": "File not supported. Try a different file.", + "dropIt": "Drop it!" + }, "nfts": { "allNfts": "All NFTs", "expandNFT": "View", diff --git a/source/manifest.json b/source/manifest.json index 5a0b97ef..7664be13 100644 --- a/source/manifest.json +++ b/source/manifest.json @@ -1,7 +1,7 @@ { "manifest_version": 2, "name": "Plug", - "version": "0.6.0", + "version": "0.6.1", "icons": { "16": "assets/icons/favicon-16.png", "32": "assets/icons/favicon-32.png", diff --git a/source/redux/nfts.js b/source/redux/nfts.js index 49953766..69ae20b7 100644 --- a/source/redux/nfts.js +++ b/source/redux/nfts.js @@ -1,4 +1,44 @@ -import { createSlice } from '@reduxjs/toolkit'; +import { createAsyncThunk, createSlice } from '@reduxjs/toolkit'; +import { asyncSendMessage, HANDLER_TYPES } from '@background/Keyring'; + +export const getNFTs = createAsyncThunk( + 'nfts/getNFTs', + async ({ refresh } = { refresh: false }) => asyncSendMessage({ + type: HANDLER_TYPES.GET_NFTS, + params: { refresh }, + }), +); + +export const transferNFT = createAsyncThunk( + 'nfts/transferNFT', + async (params) => asyncSendMessage({ + type: HANDLER_TYPES.TRANSFER_NFT, + params, + }), +); + +export const registerNFT = createAsyncThunk( + 'nfts/registerNFT', + async (params) => asyncSendMessage({ + type: HANDLER_TYPES.ADD_CUSTOM_NFT, + params, + }), +); + +const sortCollections = (collections = []) => { + const icns = collections.find((col) => col.name === 'ICNS'); + const sorted = collections + .filter((col) => col.name !== 'ICNS') + .sort((a, b) => b?.name - a?.name); + return [icns, ...sorted].filter((col) => !!col); +}; + +const setCollectionsLoading = (state) => { state.collectionsLoading = true; }; +const updateCollections = (state, action) => { + state.collections = sortCollections(action.payload); + state.collectionsLoading = false; +}; +const setError = (state, action) => { state.error = action.payload; }; /* eslint-disable no-param-reassign */ export const nftSlice = createSlice({ @@ -7,6 +47,9 @@ export const nftSlice = createSlice({ selectedNft: null, sendAddress: null, resolvedSendAddress: null, + collections: [], + collectionsLoading: false, + error: null, }, reducers: { setSelectedNft: (state, action) => { @@ -18,6 +61,21 @@ export const nftSlice = createSlice({ state.resolvedSendAddress = resolvedAddress; }, }, + extraReducers: (builder) => { + builder + .addCase(getNFTs.pending, setCollectionsLoading) + .addCase(getNFTs.rejected, setError) + .addCase(getNFTs.fulfilled, updateCollections) + .addCase(transferNFT.pending, setCollectionsLoading) + .addCase(transferNFT.fulfilled, (state, action) => { + updateCollections(state, action); + state.selectedNft = null; + }) + .addCase(transferNFT.rejected, setError) + .addCase(registerNFT.pending, setCollectionsLoading) + .addCase(registerNFT.fulfilled, updateCollections) + .addCase(registerNFT.rejected, setError); + }, }); export const { setSelectedNft, setSendAddress } = nftSlice.actions; diff --git a/source/redux/wallet.js b/source/redux/wallet.js index 8b9818f4..5874391a 100644 --- a/source/redux/wallet.js +++ b/source/redux/wallet.js @@ -8,14 +8,6 @@ import { TOKEN_IMAGES, } from '@shared/constants/currencies'; -const sortCollections = (collections = []) => { - const icns = collections.find((col) => col.name === 'ICNS'); - const sorted = collections - .filter((col) => col.name !== 'ICNS') - .sort((a, b) => b?.name - a?.name); - return [icns, ...sorted].filter((col) => !!col); -}; - /* eslint-disable no-param-reassign */ export const walletSlice = createSlice({ name: 'wallet', @@ -28,8 +20,6 @@ export const walletSlice = createSlice({ assets: Object.values(TOKENS), walletId: '', assetsLoading: true, - collections: [], - collectionsLoading: false, transactionsLoading: false, }, reducers: { @@ -107,16 +97,6 @@ export const walletSlice = createSlice({ setAssetsLoading: (state, action) => { state.assetsLoading = action.payload; }, - setCollections: (state, action) => { - const { collections, principalId } = action.payload; - if (state.principalId === principalId && collections) { - state.collections = sortCollections(collections); - } - state.optimisticNFTUpdate = false; - }, - setCollectionsLoading: (state, action) => { - state.collectionsLoading = action.payload; - }, blockNFTFetch: (state) => { state.optimisticNFTUpdate = true; }, @@ -130,8 +110,6 @@ export const { setTransactionsLoading, setAssets, setAssetsLoading, - setCollections, - setCollectionsLoading, blockNFTFetch, } = walletSlice.actions; diff --git a/source/views/Extension/Popup.jsx b/source/views/Extension/Popup.jsx index 1f3fc187..b35305d9 100644 --- a/source/views/Extension/Popup.jsx +++ b/source/views/Extension/Popup.jsx @@ -24,6 +24,7 @@ import SendNFT from './Views/SendNFT'; import ClockError from './Views/ClockError'; import Network from './Views/Network'; import NetworkCreation from './Views/NetworkCreation'; +import ImportWallet from './Views/ImportWallet'; const Popup = ({ initialRoute }) => ( @@ -36,6 +37,7 @@ const Popup = ({ initialRoute }) => ( {/* */} + diff --git a/source/views/Extension/Views/AddNFT/Steps/Step2.jsx b/source/views/Extension/Views/AddNFT/Steps/Step2.jsx index cd0604f2..b1f89f94 100644 --- a/source/views/Extension/Views/AddNFT/Steps/Step2.jsx +++ b/source/views/Extension/Views/AddNFT/Steps/Step2.jsx @@ -1,19 +1,12 @@ import React, { useState } from 'react'; import { useTranslation } from 'react-i18next'; +import { useDispatch, useSelector } from 'react-redux'; import PropTypes from 'prop-types'; import { Typography } from '@material-ui/core'; import Grid from '@material-ui/core/Grid'; -import { - Button, Container, USDFormat, AssetFormat, - TokenIcon, -} from '@components'; -import { HANDLER_TYPES, sendMessage } from '@background/Keyring'; -import { - setCollections, - setCollectionsLoading, -} from '@redux/wallet'; -import { useDispatch, useSelector } from 'react-redux'; +import { Button, Container, TokenIcon } from '@components'; +import { registerNFT } from '@redux/nfts'; import useStyles from '../styles'; @@ -22,30 +15,12 @@ const Step2 = ({ selectedToken, handleClose }) => { const { t } = useTranslation(); const classes = useStyles(); const dispatch = useDispatch(); - const [loading, setLoading] = useState(false); - const { principalId } = useSelector((state) => state.wallet); + const { collectionsLoading } = useSelector((state) => state.nfts); const registerToken = () => { - setLoading(true); - sendMessage({ - type: HANDLER_TYPES.ADD_CUSTOM_NFT, - params: selectedToken, - }, async () => { - sendMessage({ - type: HANDLER_TYPES.GET_NFTS, - params: { refresh: true }, - }, - (nftCollections) => { - if (nftCollections?.length) { - dispatch(setCollections({ collections: nftCollections, principalId })); - } - dispatch(setCollectionsLoading(false)); - setLoading(false); - handleClose(); - } - ); - }); + dispatch(registerNFT(selectedToken)); + handleClose(); }; return ( @@ -70,8 +45,8 @@ const Step2 = ({ selectedToken, handleClose }) => { variant="rainbow" value={t('common.add')} onClick={registerToken} - loading={loading} - disabled={loading} + loading={collectionsLoading} + disabled={collectionsLoading} fullWidth data-testid="add-button" /> diff --git a/source/views/Extension/Views/Home/index.jsx b/source/views/Extension/Views/Home/index.jsx index c752e9fb..287e5b8c 100644 --- a/source/views/Extension/Views/Home/index.jsx +++ b/source/views/Extension/Views/Home/index.jsx @@ -24,7 +24,6 @@ import { getUseICNS, getWalletsConnectedToUrl, updateWalletId, - getWalletIds, } from '@modules/storageManager'; const Home = () => { diff --git a/source/views/Extension/Views/ImportWallet/Steps/Step1.jsx b/source/views/Extension/Views/ImportWallet/Steps/Step1.jsx new file mode 100644 index 00000000..5a25e603 --- /dev/null +++ b/source/views/Extension/Views/ImportWallet/Steps/Step1.jsx @@ -0,0 +1,106 @@ +import React, { useState, useCallback, useEffect } from "react"; +import { useTranslation } from "react-i18next"; +import { useDropzone } from "react-dropzone"; +import { CloudUpload } from "@material-ui/icons"; +import CloseIcon from "@material-ui/icons/Close"; +import { Typography, Grid } from "@material-ui/core"; + +import { Container, Button, Plug } from "@components"; +import GradientFile from "@assets/icons/gradient-file.svg"; + +import useStyles from "../styles"; + +const Step1 = ({ + handleChangeStep, + setUserPemFile, + userPemFile, + loadingValidate, + isPemValid, + importDisabled, +}) => { + const { t } = useTranslation(); + const classes = useStyles(); + + const handleRemoveFile = () => { + setUserPemFile(null); + }; + + const onDrop = useCallback((acceptedFile) => { + setUserPemFile(acceptedFile[0]); + }, []); + + const { getRootProps, getInputProps, isDragActive } = useDropzone({ + onDrop, + maxFiles: 1, + accept: { + "application/x-pem-file": [".pem"], + }, + }); + + return ( + + + +
+ {userPemFile ? ( + <> + gradientFile +
+ {userPemFile.name || ""} + handleRemoveFile()} + /> +
+ + ) : ( + <> + {isDragActive ? ( +
+ + + {t("importPem.dropIt")} + +
+ ) : ( + <> + + + + {t("importPem.dragAndDrop")} +
+ {t("importPem.or")}{" "} + +
+ + )} + + )} +
+
+ +
+ setWalletName(e.target.value)} + type="text" + // error={} + /> + } + /> + +
{expand && (
- { copyButtonTestId="copy-principalId-button" infoIconButtonTestId="info-principalId-icon-button" /> - { const newPage = await newPagePromise; const url = await newPage.evaluate(() => document.location.href); - expect(url).toBe('https://medium.com/plugwallet/internet-computer-ids-101-669b192a2ace'); + expect(url).toBe('https://medium.com/@plug_wallet/669b192a2ace'); }; const waitForWalletName = async (page, walletName, timeout = 1000) => { diff --git a/yarn.lock b/yarn.lock index 35ed9bad..690b597d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -978,7 +978,7 @@ dependencies: regenerator-runtime "^0.12.0" -"@babel/runtime@^7.10.2", "@babel/runtime@^7.12.0", "@babel/runtime@^7.12.5", "@babel/runtime@^7.14.5", "@babel/runtime@^7.15.4", "@babel/runtime@^7.18.9", "@babel/runtime@^7.2.0", "@babel/runtime@^7.3.1", "@babel/runtime@^7.4.4", "@babel/runtime@^7.5.5", "@babel/runtime@^7.8.3", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2": +"@babel/runtime@^7.10.2", "@babel/runtime@^7.12.0", "@babel/runtime@^7.12.5", "@babel/runtime@^7.14.5", "@babel/runtime@^7.15.4", "@babel/runtime@^7.16.7", "@babel/runtime@^7.18.9", "@babel/runtime@^7.2.0", "@babel/runtime@^7.3.1", "@babel/runtime@^7.4.4", "@babel/runtime@^7.5.5", "@babel/runtime@^7.8.3", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2": version "7.19.0" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.19.0.tgz#22b11c037b094d27a8a2504ea4dcff00f50e2259" integrity sha512-eR8Lo9hnDS7tqkO7NsV+mKvCmv5boaXFSZ70DnfhcgiEne8hv9oCEd36Klw74EtizEqLsy4YnW8UWwpBVolHZA== @@ -1773,10 +1773,10 @@ axios "^0.24.0" cross-fetch "^3.1.4" -"@psychedelic/dab-js@1.4.12": - version "1.4.12" - resolved "https://npm.pkg.github.com/download/@Psychedelic/dab-js/1.4.12/ee252e693f8606327f479cbbdbfe8424b422e8f5#ee252e693f8606327f479cbbdbfe8424b422e8f5" - integrity sha512-oJwMbgafesO5KOBoV70oQ929wel18n0X6u/IBtR7mcBa0SdwM2uyhbgjNqVLpFdBaVBRoAxKdCABc4sVRcqglg== +"@psychedelic/dab-js@1.5.0-beta.1": + version "1.5.0-beta.1" + resolved "https://npm.pkg.github.com/download/@Psychedelic/dab-js/1.5.0-beta.1/306e2bb10491927995a3a22f25cd2ca9fd40990e#306e2bb10491927995a3a22f25cd2ca9fd40990e" + integrity sha512-p7GEh++Fv8U1SIYdqjxLLNy3I8fjoEF+lrXsHHKxcFuKpINxWkfWmUxLnjS6rytY+VuovrDEISFD4Tz0QHD2DQ== dependencies: "@dfinity/agent" "0.9.3" "@dfinity/candid" "0.9.3" @@ -1787,17 +1787,17 @@ cross-fetch "^3.1.4" crypto-js "^4.1.1" -"@psychedelic/plug-controller@0.22.6": - version "0.22.6" - resolved "https://npm.pkg.github.com/download/@Psychedelic/plug-controller/0.22.6/8c25286d67f09de822ac3ba5d2871c673b5262a4#8c25286d67f09de822ac3ba5d2871c673b5262a4" - integrity sha512-5XoiF34l9k7jh1uwLwNIidjG7EhIAEh4yvQ6TrSR/1X//NdFO+ipkooUj3GSEqbGZAF4nhOOJfwO7PX9EE8agQ== +"@psychedelic/plug-controller@0.24.4": + version "0.24.4" + resolved "https://npm.pkg.github.com/download/@Psychedelic/plug-controller/0.24.4/7c71cf00d570b5792fb43513b13cf156f4d05ce6#7c71cf00d570b5792fb43513b13cf156f4d05ce6" + integrity sha512-iYXhvsGgRLExLt9J58QJ2xU2YVFobvB+2HI4WZ5KcbPvzGeDZsMsjUSTnNXiqXVn3wzjtzp0ks+gG2K/DslTdw== dependencies: "@dfinity/agent" "0.9.3" "@dfinity/candid" "0.9.3" "@dfinity/identity" "0.9.3" "@dfinity/principal" "0.9.3" "@psychedelic/cap-js" "0.0.7" - "@psychedelic/dab-js" "1.4.12" + "@psychedelic/dab-js" "1.5.0-beta.1" "@types/secp256k1" "^4.0.3" axios "^0.21.1" babel-jest "^25.5.1" @@ -1862,9 +1862,9 @@ integrity sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ== "@sinclair/typebox@^0.24.1": - version "0.24.42" - resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.24.42.tgz#a74b608d494a1f4cc079738e050142a678813f52" - integrity sha512-d+2AtrHGyWek2u2ITF0lHRIv6Tt7X0dEHW+0rP+5aDCEjC3fiN2RBjrLD0yU0at52BcZbRGxLbAtXiR0hFCjYw== + version "0.24.43" + resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.24.43.tgz#2e2bce0e5e493aaf639beed0cd6c88cfde7dd3d7" + integrity sha512-1orQTvtazZmsPeBroJjysvsOQCYV2yjWlebkSY38pl5vr2tdLjEJ+LoxITlGNZaH2RE19WlAwQMkH/7C14wLfw== "@sinonjs/commons@^1.7.0": version "1.8.3" @@ -1919,9 +1919,9 @@ "@babel/types" "^7.0.0" "@types/babel__traverse@*", "@types/babel__traverse@^7.0.6": - version "7.18.1" - resolved "https://registry.yarnpkg.com/@types/babel__traverse/-/babel__traverse-7.18.1.tgz#ce5e2c8c272b99b7a9fd69fa39f0b4cd85028bd9" - integrity sha512-FSdLaZh2UxaMuLp9lixWaHq/golWTRWOnRsAXzDTDSDOQLuZb1nsdCt6pJSPWSEQt2eFZ2YVk3oYhn+1kLMeMA== + version "7.18.2" + resolved "https://registry.yarnpkg.com/@types/babel__traverse/-/babel__traverse-7.18.2.tgz#235bf339d17185bdec25e024ca19cce257cc7309" + integrity sha512-FcFaxOr2V5KZCviw1TnutEMVUVsGt4D2hP1TAfXZAMKuHYW3xQhe3jTxNPWutgCJ3/X1c5yX8ZoGVEItxKbwBg== dependencies: "@babel/types" "^7.3.0" @@ -1981,9 +1981,9 @@ "@types/node" "*" "@types/har-format@*": - version "1.2.8" - resolved "https://registry.yarnpkg.com/@types/har-format/-/har-format-1.2.8.tgz#e6908b76d4c88be3db642846bb8b455f0bfb1c4e" - integrity sha512-OP6L9VuZNdskgNN3zFQQ54ceYD8OLq5IbqO4VK91ORLfOm7WdT/CiT/pHEBSQEqCInJ2y3O6iCm/zGtPElpgJQ== + version "1.2.9" + resolved "https://registry.yarnpkg.com/@types/har-format/-/har-format-1.2.9.tgz#b9b3a9bfc33a078e7d898a00b09662910577f4a4" + integrity sha512-rffW6MhQ9yoa75bdNi+rjZBAvu2HhehWJXlhuWXnWdENeuKe82wUgAwxYOb7KRKKmxYN+D/iRKd2NDQMLqlUmg== "@types/hoist-non-react-statics@^3.3.0": version "3.3.1" @@ -2046,9 +2046,9 @@ integrity sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA== "@types/node@*": - version "18.7.18" - resolved "https://registry.yarnpkg.com/@types/node/-/node-18.7.18.tgz#633184f55c322e4fb08612307c274ee6d5ed3154" - integrity sha512-m+6nTEOadJZuTPkKR/SYK3A2d7FZrgElol9UP1Kae90VVU4a6mxnPuLiIW1m4Cq4gZ/nWb9GrdVXJCoCazDAbg== + version "18.7.23" + resolved "https://registry.yarnpkg.com/@types/node/-/node-18.7.23.tgz#75c580983846181ebe5f4abc40fe9dfb2d65665f" + integrity sha512-DWNcCHolDq0ZKGizjx2DZjR/PqsYwAcYUJmfMWqtVU2MBMG5Mo+xFZrhGId5r/O5HOuMPyQEcM6KUBp5lBZZBg== "@types/node@11.11.6": version "11.11.6" @@ -2061,9 +2061,9 @@ integrity sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA== "@types/prettier@^2.1.5": - version "2.7.0" - resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-2.7.0.tgz#ea03e9f0376a4446f44797ca19d9c46c36e352dc" - integrity sha512-RI1L7N4JnW5gQw2spvL7Sllfuf1SaHdrZpCHiBlCXjIlufi1SMNnbu2teze3/QE67Fg2tBlH7W+mi4hVNk4p0A== + version "2.7.1" + resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-2.7.1.tgz#dfd20e2dc35f027cdd6c1908e80a5ddc7499670e" + integrity sha512-ri0UmynRRvZiiUJdiz38MmIblKK+oH30MztdBVR95dv/Ubw6neWSb8u1XpRb72L4qsZOhz+L+z9JD40SJmfWow== "@types/prop-types@*": version "15.7.5" @@ -2195,9 +2195,9 @@ "@types/yargs-parser" "*" "@types/yargs@^17.0.8": - version "17.0.12" - resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-17.0.12.tgz#0745ff3e4872b4ace98616d4b7e37ccbd75f9526" - integrity sha512-Nz4MPhecOFArtm81gFQvQqdV7XYCrWKx5uUt6GNHredFHn1i2mtWqXTON7EPXMtNi1qjtjEM/VCHDhcHsAMLXQ== + version "17.0.13" + resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-17.0.13.tgz#34cced675ca1b1d51fcf4d34c3c6f0fa142a5c76" + integrity sha512-9sWaruZk2JGxIQU+IhI1fhPYRcQ0UuTNuKuCW9bR5fp7qi2Llf7WDzNa17Cy7TKnh3cdxDOiyTu6gaLS0eDatg== dependencies: "@types/yargs-parser" "*" @@ -2811,6 +2811,11 @@ atob@^2.1.2: resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9" integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg== +attr-accept@^2.2.2: + version "2.2.2" + resolved "https://registry.yarnpkg.com/attr-accept/-/attr-accept-2.2.2.tgz#646613809660110749e92f2c10833b70968d929b" + integrity sha512-7prDjvt9HmqiZ0cl5CRjtS84sEyhsHP2coDkaZKRKVfCDo9s7iw7ChVmar78Gu9pC4SoR/28wFu/G5JJhTnqEg== + autoprefixer@^10.0.2: version "10.4.12" resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-10.4.12.tgz#183f30bf0b0722af54ee5ef257f7d4320bb33129" @@ -4086,9 +4091,9 @@ caniuse-api@^3.0.0: lodash.uniq "^4.5.0" caniuse-lite@^1.0.0, caniuse-lite@^1.0.30000844, caniuse-lite@^1.0.30001400, caniuse-lite@^1.0.30001407: - version "1.0.30001410" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001410.tgz#b5a86366fbbf439d75dd3db1d21137a73e829f44" - integrity sha512-QoblBnuE+rG0lc3Ur9ltP5q47lbguipa/ncNMyyGuqPk44FxbScWAeEO+k5fSQ8WekdAK4mWqNs1rADDAiN5xQ== + version "1.0.30001412" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001412.tgz#30f67d55a865da43e0aeec003f073ea8764d5d7c" + integrity sha512-+TeEIee1gS5bYOiuf+PS/kp2mrXic37Hl66VY6EAfxasIk5fELTktK2oOezYed12H8w7jt3s512PpulQidPjwA== capture-exit@^2.0.0: version "2.0.0" @@ -4609,16 +4614,16 @@ copy-webpack-plugin@^6.3.1: webpack-sources "^1.4.3" core-js-compat@^3.25.1: - version "3.25.2" - resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.25.2.tgz#7875573586809909c69e03ef310810c1969ee138" - integrity sha512-TxfyECD4smdn3/CjWxczVtJqVLEEC2up7/82t7vC0AzNogr+4nQ8vyF7abxAuTXWvjTClSbvGhU0RgqA4ToQaQ== + version "3.25.3" + resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.25.3.tgz#d6a442a03f4eade4555d4e640e6a06151dd95d38" + integrity sha512-xVtYpJQ5grszDHEUU9O7XbjjcZ0ccX3LgQsyqSvTnjX97ZqEgn9F5srmrwwwMtbKzDllyFPL+O+2OFMl1lU4TQ== dependencies: browserslist "^4.21.4" core-js-pure@^3.25.1: - version "3.25.2" - resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.25.2.tgz#44a4fd873bdd4fecf6ca11512bcefedbe87e744a" - integrity sha512-ItD7YpW1cUB4jaqFLZXe1AXkyqIxz6GqPnsDV4uF4hVcWh/WAGIqSqw5p0/WdsILM0Xht9s3Koyw05R3K6RtiA== + version "3.25.3" + resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.25.3.tgz#66ac5bfa5754b47fdfd14f3841c5ed21c46db608" + integrity sha512-T/7qvgv70MEvRkZ8p6BasLZmOVYKzOaWNBEHAU8FmveCJkl4nko2quqPQOmy6AJIp5MBanhz9no3A94NoRb0XA== core-js@^2.4.0: version "2.6.12" @@ -5391,9 +5396,9 @@ ed25519-hd-key@^1.2.0: tweetnacl "1.0.3" electron-to-chromium@^1.3.47, electron-to-chromium@^1.4.251: - version "1.4.258" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.258.tgz#44c5456f487be082f038282fbcfd7b06ae99720d" - integrity sha512-vutF4q0dTUXoAFI7Vbtdwen/BJVwPgj8GRg/SElOodfH7VTX+svUe62A5BG41QRQGk5HsZPB0M++KH1lAlOt0A== + version "1.4.263" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.263.tgz#4cbb8263165cd6d7d1ffb39ce27e2ce6a7f6cb5b" + integrity sha512-RcvChwbVkZBe7e+B23+wjXJgRHTy9Byu6JEL8HuNKILALs98deLnyGqv73nvwRKj6VGghrwnwKWC3ukVhQDvpQ== elliptic@^6.5.3, elliptic@^6.5.4: version "6.5.4" @@ -6122,6 +6127,13 @@ file-loader@^6.2.0: loader-utils "^2.0.0" schema-utils "^3.0.0" +file-selector@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/file-selector/-/file-selector-0.6.0.tgz#fa0a8d9007b829504db4d07dd4de0310b65287dc" + integrity sha512-QlZ5yJC0VxHxQQsQhXvBaC7VRJ2uaxTf+Tfpu4Z/OcVQJVpZO+DGU0rkoVW5ce2SccxugvpBJoMvUs59iILYdw== + dependencies: + tslib "^2.4.0" + file-uri-to-path@1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz#553a7b8446ff6f684359c445f1e37a05dacc33dd" @@ -7152,9 +7164,9 @@ is-buffer@^1.0.2, is-buffer@^1.1.5: integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w== is-callable@^1.1.4, is-callable@^1.2.6: - version "1.2.6" - resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.6.tgz#fd6170b0b8c7e2cc73de342ef8284a2202023c44" - integrity sha512-krO72EO2NptOGAX2KYyqbP9vYMlNAXdB53rq6f8LXY6RY7JdSR/3BD6wLUlPHSAesmY9vstNrjvqGaCiRK/91Q== + version "1.2.7" + resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.7.tgz#3bc2a85ea742d9e36205dcacdd72ca1fdc51b055" + integrity sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA== is-ci@^2.0.0: version "2.0.0" @@ -10691,6 +10703,13 @@ react-collapsible@^2.8.4: resolved "https://registry.yarnpkg.com/react-collapsible/-/react-collapsible-2.10.0.tgz#57330f9f4f968a41ece49c651b56cf30f9a06d19" integrity sha512-kEVsmlFfXBMTCnU5gwIv19MdmPAhbIPzz5Er37TiJSzRKS0IHrqAKQyQeHEmtoGIQMTcVI46FzE4z3NlVTx77A== +react-cool-virtual@^0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/react-cool-virtual/-/react-cool-virtual-0.7.0.tgz#8fa5d8b622b1c73406f4542eba8cd75de3bc3558" + integrity sha512-oWRsF0Pf9dRWncpFvsCNWm0W/d/UEbUqxNIuhGEcLzdW16leokcl9gSYG9Ha5raaF/qq029W1cj65zAwgJyR7A== + dependencies: + "@babel/runtime" "^7.16.7" + react-dom@^17.0.1: version "17.0.2" resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-17.0.2.tgz#ecffb6845e3ad8dbfcdc498f0d0a939736502c23" @@ -10700,6 +10719,15 @@ react-dom@^17.0.1: object-assign "^4.1.1" scheduler "^0.20.2" +react-dropzone@^14.2.2: + version "14.2.2" + resolved "https://registry.yarnpkg.com/react-dropzone/-/react-dropzone-14.2.2.tgz#a75a0676055fe9e2cb78578df4dedb4c42b54f98" + integrity sha512-5oyGN/B5rNhop2ggUnxztXBQ6q6zii+OMEftPzsxAR2hhpVWz0nAV+3Ktxo2h5bZzdcCKrpd8bfWAVsveIBM+w== + dependencies: + attr-accept "^2.2.2" + file-selector "^0.6.0" + prop-types "^15.8.1" + react-event-listener@^0.6.0: version "0.6.6" resolved "https://registry.yarnpkg.com/react-event-listener/-/react-event-listener-0.6.6.tgz#758f7b991cad9086dd39fd29fad72127e1d8962a" @@ -11352,9 +11380,9 @@ run-queue@^1.0.0, run-queue@^1.0.3: aproba "^1.1.1" rxjs@^7.5.4, rxjs@^7.5.5: - version "7.5.6" - resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.5.6.tgz#0446577557862afd6903517ce7cae79ecb9662bc" - integrity sha512-dnyv2/YsXhnm461G+R/Pe5bWP41Nm6LBXEYWI6eiFP4fiwx6WRI/CD0zbdVAudd9xwLEF2IDcKXLHit0FYjUzw== + version "7.5.7" + resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.5.7.tgz#2ec0d57fdc89ece220d2e702730ae8f1e49def39" + integrity sha512-z9MzKh/UcOqB3i20H6rtrlaE/CgjLOvheWK/9ILrbhROGTweAi1BaFsTT9FbwZi5Trr1qNRs+MXkhmR06awzQA== dependencies: tslib "^2.1.0" @@ -12418,7 +12446,7 @@ tsconfig-paths@^3.14.1: minimist "^1.2.6" strip-bom "^3.0.0" -tslib@^2.0.3, tslib@^2.1.0, tslib@^2.3.0: +tslib@^2.0.3, tslib@^2.1.0, tslib@^2.3.0, tslib@^2.4.0: version "2.4.0" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.4.0.tgz#7cecaa7f073ce680a05847aa77be941098f36dc3" integrity sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==