diff --git a/.gitignore b/.gitignore index 091a2a97..1b57587b 100644 --- a/.gitignore +++ b/.gitignore @@ -70,6 +70,9 @@ typings/ .env .env.test +# PEM files +*.pem + # parcel-bundler cache (https://parceljs.org/) .cache diff --git a/package.json b/package.json index 8fd640f3..e7ef0620 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "plug", - "version": "0.5.4.1", + "version": "0.6.0", "description": "Your plug into the Internet Computer", "private": true, "repository": "https://github.com/Psychedelic/plug", @@ -37,9 +37,9 @@ "@material-ui/icons": "^4.11.2", "@metamask/post-message-stream": "^4.0.0", "@psychedelic/browser-rpc": "2.1.0", - "@psychedelic/dab-js": "1.4.5", - "@psychedelic/plug-controller": "0.19.8", - "@psychedelic/plug-inpage-provider": "^2.3.0", + "@psychedelic/dab-js": "1.4.12", + "@psychedelic/plug-controller": "0.22.6", + "@psychedelic/plug-inpage-provider": "^2.3.1", "@reduxjs/toolkit": "^1.6.0", "advanced-css-reset": "^1.2.2", "axios": "^0.21.1", diff --git a/source/Background/Keyring/index.js b/source/Background/Keyring/index.js index 1efc3001..2522959a 100644 --- a/source/Background/Keyring/index.js +++ b/source/Background/Keyring/index.js @@ -87,7 +87,9 @@ export const HANDLER_TYPES = { EDIT_PRINCIPAL: 'edit-principal', GET_PUBLIC_KEY: 'get-public-key', GET_TOKEN_INFO: 'get-token-info', + GET_NFT_INFO: 'get-nft-info', ADD_CUSTOM_TOKEN: 'add-custom-token', + ADD_CUSTOM_NFT: 'add-custom-nft', CREATE_PRINCIPAL: 'create-principal', SET_CURRENT_PRINCIPAL: 'set-current-principal', GET_PEM_FILE: 'get-pem-file', @@ -121,7 +123,9 @@ export const getKeyringErrorMessage = (type) => ({ [HANDLER_TYPES.EDIT_PRINCIPAL]: 'editing your principal.', [HANDLER_TYPES.GET_PUBLIC_KEY]: 'getting your public key.', [HANDLER_TYPES.GET_TOKEN_INFO]: 'fetching token info.', + [HANDLER_TYPES.GET_NFT_INFO]: 'fetching nft info.', [HANDLER_TYPES.ADD_CUSTOM_TOKEN]: 'adding custom token.', + [HANDLER_TYPES.ADD_CUSTOM_NFT]: 'adding custom nft.', [HANDLER_TYPES.CREATE_PRINCIPAL]: 'creating your principal.', [HANDLER_TYPES.SET_CURRENT_PRINCIPAL]: 'setting your principal.', [HANDLER_TYPES.GET_PEM_FILE]: 'getting your PEM file.', @@ -136,6 +140,7 @@ export const getKeyringErrorMessage = (type) => ({ [HANDLER_TYPES.REMOVE_NETWORK]: 'removing the network', [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', }[type]); export const sendMessage = (args, callback) => { @@ -179,8 +184,8 @@ export const getKeyringHandler = (type, keyring) => ({ }, [HANDLER_TYPES.CREATE_PRINCIPAL]: async (params) => keyring.createPrincipal(params), [HANDLER_TYPES.SET_CURRENT_PRINCIPAL]: - async (walletNumber) => { - await keyring.setCurrentPrincipal(walletNumber); + async (walletId) => { + await keyring.setCurrentPrincipal(walletId); const state = await keyring.getState(); extension.tabs.query({ active: true }, (tabs) => { extension.tabs.sendMessage(tabs[0].id, { action: 'updateConnection' }); @@ -223,12 +228,13 @@ export const getKeyringHandler = (type, keyring) => ({ return { error: e.message }; } }, - [HANDLER_TYPES.GET_BALANCE]: async (subaccount) => { + [HANDLER_TYPES.GET_BALANCE]: async (walletId) => { try { - const assets = await keyring.getBalances({ subaccount }); + const assets = await keyring.getBalances({ subaccount: walletId }); const parsedAssets = parseAssetsAmount(assets); const icpPrice = await getICPPrice(); - return formatAssets(parsedAssets, icpPrice); + const formattedAssets = formatAssets(parsedAssets, icpPrice); + return formattedAssets.map((asset) => recursiveParseBigint(asset)); } catch (error) { // eslint-disable-next-line console.log('Error when fetching token balances', error); @@ -259,8 +265,8 @@ export const getKeyringHandler = (type, keyring) => ({ } }, [HANDLER_TYPES.EDIT_PRINCIPAL]: - async ({ walletNumber, name, emoji }) => ( - keyring.editPrincipal(walletNumber, { name, emoji }) + async ({ walletId, name, emoji }) => ( + keyring.editPrincipal(walletId, { name, emoji }) ), [HANDLER_TYPES.GET_PUBLIC_KEY]: async () => keyring.getPublicKey(), @@ -279,6 +285,33 @@ export const getKeyringHandler = (type, keyring) => ({ return { error: e.message }; } }, + [HANDLER_TYPES.GET_NFT_INFO]: + async ({ canisterId, standard }) => { + try { + const nftInfo = await keyring.getNFTInfo({ + canisterId, + standard, + }); + return nftInfo; + } catch (e) { + // eslint-disable-next-line + console.log('Error while fetching NFT info', e); + return { error: e.message }; + } + }, + [HANDLER_TYPES.ADD_CUSTOM_NFT]: + async ({ canisterId, standard }) => { + try { + const nfts = await keyring.registerNFT({ + canisterId, standard, + }); + return (nfts || []).map((nft) => recursiveParseBigint(nft)); + } catch (e) { + // eslint-disable-next-line + console.log('Error registering nft', e); + return { error: e.message }; + } + }, [HANDLER_TYPES.ADD_CUSTOM_TOKEN]: async ({ canisterId, standard, logo }) => { try { @@ -294,7 +327,7 @@ export const getKeyringHandler = (type, keyring) => ({ } }, [HANDLER_TYPES.GET_PEM_FILE]: - async (walletNumber) => keyring.getPemFile(walletNumber), + async (walletId) => keyring.getPemFile(walletId), [HANDLER_TYPES.BURN_XTC]: async ({ to, amount }) => { try { @@ -324,19 +357,22 @@ export const getKeyringHandler = (type, keyring) => ({ return { error: e.message }; } }, - [HANDLER_TYPES.GET_ICNS_DATA]: async ({ refresh }) => { - const { wallets, currentWalletId } = await keyring.getState(); - let icnsData = wallets?.[currentWalletId]?.icnsData || { names: [] }; + [HANDLER_TYPES.GET_ICNS_DATA]: async ({ refresh, walletId = keyring.currentWalletId }) => { + const { wallets } = await keyring.getState(); + let icnsData = wallets?.[walletId]?.icnsData || { names: [] }; if (!icnsData?.names?.length || refresh) { - icnsData = await keyring.getICNSData(); + icnsData = await keyring.getICNSData({ subaccount: walletId }); } else { keyring.getICNSData(); } return icnsData; }, - [HANDLER_TYPES.SET_REVERSE_RESOLVED_NAME]: async (name) => { + [HANDLER_TYPES.SET_REVERSE_RESOLVED_NAME]: async ({ + name, + walletId = keyring.currentWalletId, + }) => { try { - const res = await keyring.setReverseResolvedName({ name }); + const res = await keyring.setReverseResolvedName({ name, subaccount: walletId }); return res; } catch (e) { // eslint-disable-next-line @@ -434,6 +470,16 @@ export const getKeyringHandler = (type, keyring) => ({ return { error: e.message }; } }, + [HANDLER_TYPES.REMOVE_CUSTOM_TOKEN]: async (canisterId) => { + try { + const newTokens = await keyring.removeToken(canisterId); + return Object.values(newTokens); + } catch (e) { + // eslint-disable-next-line + console.log('Error removing the network', e); + return { error: e.message }; + } + }, }[type]); export const getContacts = () => new Promise((resolve, reject) => { diff --git a/source/Background/errors.js b/source/Background/errors.js index 3f39ee0e..3977a721 100644 --- a/source/Background/errors.js +++ b/source/Background/errors.js @@ -27,5 +27,6 @@ export default { code: 401, message: 'The transaction that was just attempted failed because it was not a valid batch transaction. Please contact the project’s developers.', }, SIZE_ERROR: { code: 400, message: "There isn't enough space to open the popup" }, + GET_BALANCE_ERROR: { code: 400, message: 'There was an error trying to fetch your balances.' }, ...SILENT_ERRORS, }; diff --git a/source/Modules/Controller/connection.js b/source/Modules/Controller/connection.js index 1d2817f2..d108d393 100644 --- a/source/Modules/Controller/connection.js +++ b/source/Modules/Controller/connection.js @@ -99,8 +99,15 @@ export class ConnectionModule extends ControllerModuleBase { #disconnect() { return { methodName: 'disconnect', - handler: async (opts, url) => { - removeApp(this.keyring?.currentWalletId?.toString(), url, (removed) => { + handler: async (opts, url, principal) => { + const state = await this.keyring.getState(); + + const walletIdFromPrincipal = Object.values(state.wallets).find((wallet) => ( + wallet.principal === principal + ))?.walletId; + const walletIdToRemove = walletIdFromPrincipal ?? this.keyring.currentWalletId; + + removeApp(walletIdToRemove, url, (removed) => { if (!removed) { opts.callback(ERRORS.CONNECTION_ERROR, null); } @@ -124,6 +131,7 @@ export class ConnectionModule extends ControllerModuleBase { const { id: callId } = message.data.data; const { id: portId } = sender; const { url: domainUrl, icons } = metadata; + const newMetadata = { ...metadata, host }; if (isValidWhitelist) { canistersInfo = await fetchCanistersInfo(whitelist); @@ -137,8 +145,6 @@ export class ConnectionModule extends ControllerModuleBase { // If we receive a whitelist, we open the allow agent modal if (isValidWhitelist) { - const newMetadata = { ...metadata, requestConnect: true }; - const fixedHeight = this.keyring?.isUnlocked ? Math.min(422 + 65 * whitelist.length, 550) : SIZES.loginHeight; @@ -170,6 +176,7 @@ export class ConnectionModule extends ControllerModuleBase { argsJson: JSON.stringify({ timeout, transactionId }), type: 'connect', domainUrl, + metadataJson: JSON.stringify(newMetadata), }, callback); } }, diff --git a/source/Modules/Controller/information.js b/source/Modules/Controller/information.js index 7a276348..d9dd8a2a 100644 --- a/source/Modules/Controller/information.js +++ b/source/Modules/Controller/information.js @@ -28,13 +28,13 @@ export class InformationModule extends ControllerModuleBase { ]; } - async #internalRequestBalance(accountId, callback) { + async #internalRequestBalance(subaccount, callback, portConfig) { const getBalance = getKeyringHandler(HANDLER_TYPES.GET_BALANCE, this.keyring); - const icpBalance = await getBalance(accountId); - if (icpBalance.error) { - callback(ERRORS.SERVER_ERROR(icpBalance.error), null); + const balances = await getBalance(subaccount); + if (balances?.error) { + callback(ERRORS.GET_BALANCE_ERROR, null, portConfig); } else { - callback(null, icpBalance); + callback(null, balances, portConfig); } } @@ -44,14 +44,10 @@ export class InformationModule extends ControllerModuleBase { methodName: 'requestBalance', handler: async (opts, metadata, subaccount, transactionId) => { const { callback, message, sender } = opts; - getApps(this.keyring?.currentWalletId.toString(), (apps = {}) => { const app = apps?.[metadata.url] || {}; - if (app?.status === CONNECTION_STATUS.accepted) { - if (subaccount && Number.isNaN(parseInt(subaccount, 10))) { - callback(ERRORS.CLIENT_ERROR('Invalid account id'), null); - } else if (!this.keyring?.isUnlocked) { + if (!this.keyring?.isUnlocked) { this.displayPopUp({ callId: message.data.data.id, portId: sender.id, @@ -78,22 +74,9 @@ export class InformationModule extends ControllerModuleBase { const { subaccount } = args; getApps(this.keyring?.currentWalletId.toString(), async (apps = {}) => { const app = apps?.[url] || {}; - callback(null, true); - + callback(null, true); // Close modal if (app?.status === CONNECTION_STATUS.accepted) { - const getBalance = getKeyringHandler( - HANDLER_TYPES.GET_BALANCE, - this.keyring, - ); - const icpBalance = await getBalance(subaccount); - - if (icpBalance.error) { - callback(ERRORS.SERVER_ERROR(icpBalance.error), null, [ - { portId, callId }, - ]); - } else { - callback(null, icpBalance, [{ portId, callId }]); - } + this.#internalRequestBalance(subaccount, callback, [{ portId, callId }]); } else { callback(ERRORS.CONNECTION_ERROR, null, [{ portId, callId }]); } diff --git a/source/Modules/storageManager.js b/source/Modules/storageManager.js index dd8f8665..f35fb69b 100644 --- a/source/Modules/storageManager.js +++ b/source/Modules/storageManager.js @@ -27,7 +27,7 @@ export const getApps = (currentWalletId, cb) => { const defaultValue = {}; secureGetWrapper(currentWalletId, defaultValue, (state) => ( - cb(state?.[parseInt(currentWalletId, 10)]?.apps || defaultValue) + cb(state?.[currentWalletId]?.apps || defaultValue) )); }; @@ -35,7 +35,7 @@ export const getApp = (currentWalletId, appUrl, cb) => { const defaultValue = {}; secureGetWrapper(currentWalletId, defaultValue, (state) => { - cb(state?.[parseInt(currentWalletId, 10)]?.apps?.[appUrl] || defaultValue); + cb(state?.[currentWalletId]?.apps?.[appUrl] || defaultValue); }); }; @@ -121,13 +121,22 @@ export const getProtectedIds = (cb) => { export const setUseICNS = (useICNS, walletNumber, cb = () => {}) => { const defaultValue = true; - secureSetWrapper({ icns: { [walletNumber]: useICNS } }, defaultValue, cb); + + secureGetWrapper('icns', defaultValue, (state) => { + cb(state?.icns?.[walletNumber] ?? defaultValue); + secureSetWrapper({ + icns: { + ...state?.icns, + [walletNumber]: useICNS, + }, + }, defaultValue, cb); + }); }; export const getUseICNS = (walletNumber, cb) => { const defaultValue = true; secureGetWrapper('icns', defaultValue, (state) => { - cb(state?.icns?.[parseInt(walletNumber, 10)] ?? defaultValue); + cb(state?.icns?.[walletNumber] ?? defaultValue); }); }; @@ -145,12 +154,17 @@ export const getBatchTransactions = (cb) => { export const getWalletsConnectedToUrl = (url, walletIds, cb) => { const wallets = []; - walletIds.forEach((id) => { + if (!walletIds.length) { + cb([]); + return; + } + + walletIds.forEach((id, index) => { getApp(id.toString(), url, (app = {}) => { if (app?.status === CONNECTION_STATUS.accepted) { wallets.push(id); } - if (id === walletIds.length - 1) { + if (index === walletIds.length - 1) { cb(wallets); } }); @@ -193,3 +207,12 @@ export const removePendingTransaction = (transactionId, cb) => { export const resetPendingTransactions = () => { secureSetWrapper({ activeTransactions: { } }, {}, () => {}); }; + +export const updateWalletId = (previousWalletId, newWalletId) => { + getApps(previousWalletId, (apps) => { + setApps(newWalletId, apps); + }); + getUseICNS(previousWalletId, (result) => { + setUseICNS(newWalletId, result); + }); +}; diff --git a/source/assets/icons/coins.svg b/source/assets/icons/coins.svg new file mode 100644 index 00000000..856d2bf4 --- /dev/null +++ b/source/assets/icons/coins.svg @@ -0,0 +1,3 @@ + + + diff --git a/source/assets/icons/favicon-16.png b/source/assets/icons/favicon-16.png index fcf94fc5..b0756213 100644 Binary files a/source/assets/icons/favicon-16.png and b/source/assets/icons/favicon-16.png differ diff --git a/source/assets/icons/favicon-32.png b/source/assets/icons/favicon-32.png index 08b079d4..7137f28c 100644 Binary files a/source/assets/icons/favicon-32.png and b/source/assets/icons/favicon-32.png differ diff --git a/source/assets/icons/imageIcon.svg b/source/assets/icons/imageIcon.svg new file mode 100644 index 00000000..6d1716f7 --- /dev/null +++ b/source/assets/icons/imageIcon.svg @@ -0,0 +1,3 @@ + + + diff --git a/source/assets/icons/switch-account.svg b/source/assets/icons/switch-account.svg new file mode 100644 index 00000000..c08325d8 --- /dev/null +++ b/source/assets/icons/switch-account.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/source/components/AssetItem/index.jsx b/source/components/AssetItem/index.jsx index 20f9e257..22ce92a4 100644 --- a/source/components/AssetItem/index.jsx +++ b/source/components/AssetItem/index.jsx @@ -8,17 +8,48 @@ 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'; +import ActionDialog from '../ActionDialog'; import useStyles from './styles'; const AssetItem = ({ - updateToken, logo, name, amount, value, symbol, loading, failed, assetNameTestId, + updateToken, + logo, + name, + amount, + value, + symbol, + loading, + failed, + assetNameTestId, + protectedAsset, + removeAsset, }) => { const classes = useStyles(); const { t } = useTranslation(); const { currentNetwork, usingMainnet } = useSelector((state) => state.network); + const [ shouldRemove, setShouldRemove] = useState(false); + const [isHovering, setIsHovering] = useState(false); + const [openDelete, setOpenDelete] = useState(false); + + const handleMouseOver = () => { + if (!protectedAsset) return; + setIsHovering(true); + }; + + const handleMouseOut = () => { + if (!protectedAsset) return; + setIsHovering(false); + }; + + const handleModalClose = () => { + setIsHovering(false); + setOpenDelete(false); + }; const handleFetchAssets = async () => { // Avoid calling multiple times @@ -26,9 +57,39 @@ const AssetItem = ({ await updateToken(); }; + + const handleRemoveAssetDisplay = () => { + setShouldRemove(true); + handleModalClose(); + } + const ledgerNotSpecified = !usingMainnet && !currentNetwork?.ledgerCanisterId; + return ( -
+
+ + {t('removeToken.mainText')} + {symbol} + {t('removeToken.mainTextContinue')} +
+
+ {t('removeToken.disclaimer')} + + )} + confirmText={t('removeToken.action')} + buttonVariant="danger" + onClick={handleRemoveAssetDisplay} + onClose={handleModalClose} + />
{failed && !loading @@ -50,7 +111,7 @@ const AssetItem = ({ : ()} - )} + )} + { !failed && !loading && ( +
+ setOpenDelete(true)} + alt="delete-token" + src={DeleteIcon} + /> +
+ )}
- ); }; @@ -98,4 +169,5 @@ AssetItem.propTypes = { loading: PropTypes.bool.isRequired, failed: PropTypes.bool, assetNameTestId: PropTypes.string, + removeAsset: PropTypes.func.isRequired, }; diff --git a/source/components/AssetItem/styles.js b/source/components/AssetItem/styles.js index ed3d6b0d..79b00a9a 100644 --- a/source/components/AssetItem/styles.js +++ b/source/components/AssetItem/styles.js @@ -8,6 +8,13 @@ export default makeStyles((theme) => ({ justifyContent: 'flex-start', padding: `${theme.spacing(1)}px ${theme.spacing(2)}px`, }, + '@keyframes swipeLeft': { + from: { transform: 'translateX(0px)' }, + to: { transform: 'translateX(-100%)' }, + }, + removeAnimation: { + animation: '$swipeLeft 0.6s forwards', + }, image: { height: 41, width: 41, @@ -63,4 +70,24 @@ export default makeStyles((theme) => ({ opacity: 0.9, }, }, + deleteToken: { + opacity: 0, + width: 0, + transition: '0.6s', + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + justifyContent: 'center', + }, + deleteTokenMoveRight: { + marginLeft: 'auto !important', + }, + deleteTokenActive: { + opacity: 1, + margin: '0 4px 0 21px', + + '& > img': { + cursor: 'pointer', + } + } })); diff --git a/source/components/ConnectAccountsModal/components/ConnectAccountItem/index.jsx b/source/components/ConnectAccountsModal/components/ConnectAccountItem/index.jsx index 0095716e..b7da819c 100644 --- a/source/components/ConnectAccountsModal/components/ConnectAccountItem/index.jsx +++ b/source/components/ConnectAccountsModal/components/ConnectAccountItem/index.jsx @@ -13,9 +13,14 @@ const ConnectAccountItem = ({ connected, wallet, checked, onCheck, name, }) => { const classes = useStyles(); + + const handleCheckboxChange = (event) => { + !connected && onCheck(event, wallet.walletId); + }; + return (
{name || ''} diff --git a/source/components/ConnectAccountsModal/index.jsx b/source/components/ConnectAccountsModal/index.jsx index 19ac88e6..dd4d7664 100644 --- a/source/components/ConnectAccountsModal/index.jsx +++ b/source/components/ConnectAccountsModal/index.jsx @@ -36,8 +36,8 @@ const ConnectAccountsModal = ({ getReverseResolvedNames(); }, [wallets]); - const connectAccountToTab = (wallet) => { - getApps(wallet.walletNumber.toString(), (apps) => { + const connectAccountToTab = (walletId) => { + getApps(walletId.toString(), (apps) => { // If any other account is connected, create an entry for the current wallet const date = new Date().toISOString(); const url = getTabURL(tab); @@ -55,19 +55,21 @@ const ConnectAccountsModal = ({ ], }, }; - setApps(wallet.walletNumber.toString(), newApps); + setApps(walletId.toString(), newApps); }); }; const handleConfirm = () => { - Object.keys(walletsToUpdate).forEach((walletId) => connectAccountToTab(wallets[walletId], tab)); + Object.keys(walletsToUpdate).forEach((walletId) => { + walletsToUpdate[walletId] && connectAccountToTab(walletId, tab); + }); onConfirm?.(); setWalletsToUpdate({}); setSelectAllWallets(false); onClose(); }; - const onCheckWallet = (walletId) => (event) => { + const onCheckWallet = (event, walletId) => { setWalletsToUpdate({ ...walletsToUpdate, [walletId]: event.target.checked, @@ -77,7 +79,7 @@ const ConnectAccountsModal = ({ const handleSelectAll = (event) => { const newWalletsToUpdate = {}; wallets.forEach((wallet) => { - newWalletsToUpdate[wallet.walletNumber] = event.target.checked; + newWalletsToUpdate[wallet.walletId] = event.target.checked; }); setSelectAllWallets(event.target.checked); setWalletsToUpdate(newWalletsToUpdate); @@ -116,12 +118,13 @@ ConnectAccountsModal.propTypes = { tab: PropTypes.arrayOf(PropTypes.objectOf(PropTypes.string)).isRequired, open: PropTypes.bool.isRequired, onClose: PropTypes.func.isRequired, - onConfirm: PropTypes.func.isRequired, + onConfirm: PropTypes.func, connectedWallets: PropTypes.arrayOf(PropTypes.number), }; ConnectAccountsModal.defaultProps = { connectedWallets: [], + onConfirm: () => {}, }; export default ConnectAccountsModal; diff --git a/source/components/ConnectAccountsModal/layout.jsx b/source/components/ConnectAccountsModal/layout.jsx index 6090f288..3542d7cb 100644 --- a/source/components/ConnectAccountsModal/layout.jsx +++ b/source/components/ConnectAccountsModal/layout.jsx @@ -46,14 +46,14 @@ const ConnectAccountsModalLayout = ({ onScroll={onScroll} > {wallets.map((wallet) => { - const alreadyConnected = connectedWallets.includes(wallet.walletNumber); + const alreadyConnected = connectedWallets.includes(wallet.walletId); return (