diff --git a/.husky/pre-commit b/.husky/pre-commit index 2364af3c..2312dc58 100644 Binary files a/.husky/pre-commit and b/.husky/pre-commit differ diff --git a/src/background/service/flowns.ts b/src/background/service/flowns.ts index 85601150..e6d85ea1 100644 --- a/src/background/service/flowns.ts +++ b/src/background/service/flowns.ts @@ -1,11 +1,11 @@ -import { createPersistStore } from 'background/utils'; -import * as fcl from '@onflow/fcl'; -import * as sdk from '@onflow/sdk'; import * as secp from '@noble/secp256k1'; -import HDWallet from 'ethereum-hdwallet'; -import { keyringService, openapiService } from 'background/service'; -import wallet from 'background/controller/wallet'; +import * as fcl from '@onflow/fcl'; + import { signMessageHash } from '@/ui/utils/modules/passkey.js'; +import wallet from 'background/controller/wallet'; +import { keyringService, openapiService } from 'background/service'; +import { createPersistStore } from 'background/utils'; + import { storage } from '../webapi'; export interface FlownsStore { diff --git a/src/background/service/storage-evaluator.ts b/src/background/service/storage-evaluator.ts index 4b80d11e..3bc64c00 100644 --- a/src/background/service/storage-evaluator.ts +++ b/src/background/service/storage-evaluator.ts @@ -5,7 +5,7 @@ import { openapiService } from '../service'; import type { StorageInfo } from './networkModel'; export class StorageEvaluator { - private static MINIMUM_STORAGE_BUFFER = 10000; // minimum required storage buffer + private static MINIMUM_STORAGE_BUFFER = 100000; // minimum required storage buffer private static MINIMUM_FLOW_BALANCE = 0.001; // minimum required FLOW balance async evaluateStorage(address: string): Promise<{ diff --git a/src/background/service/userWallet.ts b/src/background/service/userWallet.ts index bc522764..cd86cf02 100644 --- a/src/background/service/userWallet.ts +++ b/src/background/service/userWallet.ts @@ -1,21 +1,24 @@ +import { getAuth, signInAnonymously } from '@firebase/auth'; +import * as secp from '@noble/secp256k1'; +import * as fcl from '@onflow/fcl'; +import { getApp } from 'firebase/app'; + +import { withPrefix } from '@/ui/utils/address'; +import { findAddressWithSeed, findAddressWithPK } from '@/ui/utils/modules/findAddressWithPK'; +import { signWithKey, seed2PubKey } from '@/ui/utils/modules/passkey.js'; +import wallet from 'background/controller/wallet'; +import { keyringService, openapiService } from 'background/service'; import { createPersistStore } from 'background/utils'; -import { +import { getHashAlgo, getSignAlgo, getStoragedAccount } from 'ui/utils'; + +import { storage } from '../webapi'; + +import type { WalletResponse, BlockchainResponse, ChildAccount, DeviceInfoRequest, } from './networkModel'; -import * as fcl from '@onflow/fcl'; -import * as secp from '@noble/secp256k1'; -import { keyringService, openapiService, proxyService } from 'background/service'; -import wallet from 'background/controller/wallet'; -import { getApp } from 'firebase/app'; -import { signWithKey, seed2PubKey } from '@/ui/utils/modules/passkey.js'; -import { findAddressWithSeed, findAddressWithPK } from '@/ui/utils/modules/findAddressWithPK'; -import { withPrefix } from '@/ui/utils/address'; -import { getAuth, signInAnonymously } from '@firebase/auth'; -import { storage } from '../webapi'; -import { getHashAlgo, getSignAlgo, getStoragedAccount } from 'ui/utils'; interface UserWalletStore { wallets: Record; @@ -145,7 +148,7 @@ class UserWallet { if (!this.store) { await this.init(); } - if (this.store.network != network) { + if (this.store.network !== network) { this.store.activeChild = null; this.store.currentWallet = this.store.wallets[network][0].blockchain[0]; } diff --git a/src/ui/FRWAssets/svg/lowStorage.svg b/src/ui/FRWAssets/svg/lowStorage.svg new file mode 100644 index 00000000..70345be2 --- /dev/null +++ b/src/ui/FRWAssets/svg/lowStorage.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/ui/FRWComponent/WarningStorageLowSnackbar.tsx b/src/ui/FRWComponent/WarningStorageLowSnackbar.tsx new file mode 100644 index 00000000..6aea1c6f --- /dev/null +++ b/src/ui/FRWComponent/WarningStorageLowSnackbar.tsx @@ -0,0 +1,16 @@ +import React from 'react'; + +import warningIcon from '../FRWAssets/svg/lowStorage.svg'; + +import WarningSnackbar from './WarningSnackbar'; + +export const WarningStorageLowSnackbar = () => { + return ( + {}} + alertIcon={warningIcon} + message={chrome.i18n.getMessage('Insufficient_storage')} + /> + ); +}; diff --git a/src/ui/utils/WalletContext.tsx b/src/ui/utils/WalletContext.tsx index 37bb8fb7..28b8782a 100644 --- a/src/ui/utils/WalletContext.tsx +++ b/src/ui/utils/WalletContext.tsx @@ -1,8 +1,9 @@ -import React, { ReactNode } from 'react'; -import { createContext, useContext } from 'react'; -import { Object } from 'ts-toolbelt'; -import { WalletController as WalletControllerClass } from 'background/controller/wallet'; -import { IExtractFromPromise } from './type'; +import React, { type ReactNode, createContext, useContext } from 'react'; +import type { Object } from 'ts-toolbelt'; + +import type { WalletController as WalletControllerClass } from 'background/controller/wallet'; + +import type { IExtractFromPromise } from './type'; export type WalletControllerType = Object.Merge< { diff --git a/src/ui/utils/modules/findAddressWithPubKey.tsx b/src/ui/utils/modules/findAddressWithPubKey.tsx index b2e15c38..eac0a380 100644 --- a/src/ui/utils/modules/findAddressWithPubKey.tsx +++ b/src/ui/utils/modules/findAddressWithPubKey.tsx @@ -1,4 +1,5 @@ import * as fcl from '@onflow/fcl'; + import { fclMainnetConfig } from 'background/fclConfig'; export const findAddressWithKey = async (pubKeyHex, address) => { @@ -35,7 +36,7 @@ const findAddres = async (address, pubKeyHex) => { .filter((key) => key.publicKey === pubKeyHex && !key.revoked) .filter((key) => key.weight >= 1000); - if (keys.length == 0) { + if (keys.length === 0) { return null; } diff --git a/src/ui/utils/useStorageCheck.ts b/src/ui/utils/useStorageCheck.ts index 6842dd4f..be09707c 100644 --- a/src/ui/utils/useStorageCheck.ts +++ b/src/ui/utils/useStorageCheck.ts @@ -5,6 +5,7 @@ import type { StorageInfo } from '@/background/service/networkModel'; import { useWallet } from './WalletContext'; interface StorageCheckResult { + sufficient: boolean | null; storageInfo: StorageInfo | null; checkStorageStatus: () => Promise<{ sufficient: boolean; storageInfo: StorageInfo }>; checkTransactionStorage: (amount?: number) => Promise<{ canProceed: boolean; reason?: string }>; @@ -13,6 +14,8 @@ interface StorageCheckResult { export const useStorageCheck = (): StorageCheckResult => { const wallet = useWallet(); + const [sufficient, setSufficient] = useState(null); + const [storageInfo, setStorageInfo] = useState(null); // Check general storage status const checkStorageStatus = useCallback(async (): Promise<{ @@ -20,9 +23,9 @@ export const useStorageCheck = (): StorageCheckResult => { storageInfo: StorageInfo; }> => { try { - const { isStorageSufficient: sufficient, storageInfo } = await wallet.checkStorageStatus(); + const { isStorageSufficient, storageInfo } = await wallet.checkStorageStatus(); - return { sufficient, storageInfo }; + return { sufficient: isStorageSufficient, storageInfo }; } catch (error) { console.error('Error checking storage status:', error); return { sufficient: false, storageInfo: { available: 0, used: 0, capacity: 0 } }; // Default to true to not block transactions on error @@ -46,9 +49,10 @@ export const useStorageCheck = (): StorageCheckResult => { useEffect(() => { let mounted = true; if (wallet) { - checkStorageStatus().then(({ storageInfo }) => { + checkStorageStatus().then(({ sufficient: isSufficient, storageInfo }) => { if (mounted) { setStorageInfo(storageInfo); + setSufficient(isSufficient); } }); return () => { @@ -57,8 +61,11 @@ export const useStorageCheck = (): StorageCheckResult => { } }, [checkStorageStatus, wallet]); + console.log('sufficient', sufficient); + console.log('storageInfo', storageInfo); return { storageInfo, + sufficient, checkStorageStatus, checkTransactionStorage, }; diff --git a/src/ui/views/Approval/components/Connect.tsx b/src/ui/views/Approval/components/Connect.tsx index 83c6d9a0..6eae4ff5 100644 --- a/src/ui/views/Approval/components/Connect.tsx +++ b/src/ui/views/Approval/components/Connect.tsx @@ -1,23 +1,22 @@ -import React, { useEffect, useState } from 'react'; -import { useLocation, useHistory } from 'react-router-dom'; -import { useTranslation } from 'react-i18next'; -import { useApproval, useWallet } from 'ui/utils'; -// import { CHAINS_ENUM } from 'consts'; -import { ThemeProvider } from '@mui/system'; import { Stack, Box, Typography, Divider, CardMedia } from '@mui/material'; +import { ThemeProvider } from '@mui/system'; +import { WalletUtils } from '@onflow/fcl'; +import React, { useCallback, useEffect, useState } from 'react'; + +import { storage } from '@/background/webapi'; import { authnServiceDefinition, serviceDefinition } from 'background/controller/serviceDefinition'; import { permissionService } from 'background/service'; -import CheckCircleIcon from '../../../../components/iconfont/IconCheckmark'; -import theme from 'ui/style/LLTheme'; -import { LLPrimaryButton, LLSecondaryButton, LLConnectLoading } from 'ui/FRWComponent'; -import { WalletUtils } from '@onflow/fcl'; +import flowgrey from 'ui/FRWAssets/svg/flow-grey.svg'; import Link from 'ui/FRWAssets/svg/link.svg'; -import testnetsvg from 'ui/FRWAssets/svg/testnet.svg'; -import mainnetsvg from 'ui/FRWAssets/svg/mainnet.svg'; import linkGlobe from 'ui/FRWAssets/svg/linkGlobe.svg'; -import flowgrey from 'ui/FRWAssets/svg/flow-grey.svg'; +import mainnetsvg from 'ui/FRWAssets/svg/mainnet.svg'; +import testnetsvg from 'ui/FRWAssets/svg/testnet.svg'; +import { LLPrimaryButton, LLSecondaryButton, LLConnectLoading } from 'ui/FRWComponent'; +import theme from 'ui/style/LLTheme'; +import { useApproval, useWallet } from 'ui/utils'; +// import { CHAINS_ENUM } from 'consts'; -import { storage } from '@/background/webapi'; +import CheckCircleIcon from '../../../../components/iconfont/IconCheckmark'; interface ConnectProps { params: any; @@ -25,14 +24,8 @@ interface ConnectProps { // defaultChain: CHAINS_ENUM; } -const Connect = ({ params: { icon, origin, tabId } }: ConnectProps) => { - const { state } = useLocation<{ - showChainsModal?: boolean; - }>(); - const { showChainsModal = false } = state ?? {}; - const history = useHistory(); +const Connect = ({ params: { /*icon, origin,*/ tabId } }: ConnectProps) => { const [, resolveApproval, rejectApproval] = useApproval(); - const { t } = useTranslation(); const wallet = useWallet(); const [isLoading, setIsLoading] = useState(false); @@ -51,7 +44,7 @@ const Connect = ({ params: { icon, origin, tabId } }: ConnectProps) => { // TODO: replace default logo const [logo, setLogo] = useState(''); - const handleCancel = () => { + const handleCancel = useCallback(() => { if (opener) { if (windowId) { chrome.windows.update(windowId, { focused: true }); @@ -67,7 +60,7 @@ const Connect = ({ params: { icon, origin, tabId } }: ConnectProps) => { } setApproval(false); rejectApproval('User rejected the request.'); - }; + }, [opener, rejectApproval, windowId]); const handleSwitchNetwork = async () => { wallet.switchNetwork(msgNetwork); @@ -159,22 +152,24 @@ const Connect = ({ params: { icon, origin, tabId } }: ConnectProps) => { // } if (msg.type === 'FCL:VIEW:READY:RESPONSE') { console.log('FCL:VIEW:READY:RESPONSE ', msg); - msg.host && setHost(msg.host); + if (msg.host) { + setHost(msg.host); + } if (!msg.host) { setHost(msg.config.client.hostname); } setMsgNetwork(msg.config.client.network); setAppIdentifier(msg.body?.appIdentifier); setNonce(msg.body?.nonce); - msg.config.app.title && setTitle(msg.config.app.title); - msg.config.app.icon && setLogo(msg.config.app.icon); + if (msg.config.app.title) setTitle(msg.config.app.title); + if (msg.config.app.icon) setLogo(msg.config.app.icon); } sendResponse({ status: 'ok' }); return true; }; - const checkNetwork = async () => { + const checkNetwork = useCallback(async () => { const address = await wallet.getCurrentAddress(); console.log('address currentAddress ', address); setCurrentAddress(address!); @@ -188,18 +183,18 @@ const Connect = ({ params: { icon, origin, tabId } }: ConnectProps) => { } else { setShowSwitch(false); } - }; + }, [wallet, msgNetwork, showSwitch]); useEffect(() => { checkNetwork(); - }, [msgNetwork, currentNetwork]); + }, [msgNetwork, currentNetwork, checkNetwork]); useEffect(() => { /** * We can't use "chrome.runtime.sendMessage" for sending messages from React. * For sending messages from React we need to specify which tab to send it to. */ - chrome.tabs && + if (chrome.tabs) { chrome.tabs .query({ active: true, @@ -214,7 +209,7 @@ const Connect = ({ params: { icon, origin, tabId } }: ConnectProps) => { * in the specified tab for the current extension. */ - const targetTab = tabs.filter((item) => item.id == tabId); + const targetTab = tabs.filter((item) => item.id === tabId); let host = ''; if (targetTab[0].url) { host = new URL(targetTab[0].url).host; @@ -227,7 +222,7 @@ const Connect = ({ params: { icon, origin, tabId } }: ConnectProps) => { setHost(host); chrome.tabs.sendMessage(targetTab[0].id || 0, { type: 'FCL:VIEW:READY' }); }); - + } /** * Fired when a message is sent from either an extension process or a content script. */ @@ -236,13 +231,21 @@ const Connect = ({ params: { icon, origin, tabId } }: ConnectProps) => { return () => { chrome.runtime?.onMessage.removeListener(extMessageHandler); }; - }, []); + }, [tabId]); - window.onbeforeunload = () => { - if (!approval) { - handleCancel(); - } - }; + useEffect(() => { + const handleWindowRemoved = (wId: number) => { + if (wId === windowId && !approval) { + handleCancel(); + } + }; + + chrome.windows.onRemoved.addListener(handleWindowRemoved); + + return () => { + chrome.windows.onRemoved.removeListener(handleWindowRemoved); + }; + }, [approval, handleCancel, windowId]); const networkColor = (network: string) => { switch (network) { diff --git a/src/ui/views/Dashboard/Components/MenuDrawer.tsx b/src/ui/views/Dashboard/Components/MenuDrawer.tsx index 26441b3a..82b4e556 100644 --- a/src/ui/views/Dashboard/Components/MenuDrawer.tsx +++ b/src/ui/views/Dashboard/Components/MenuDrawer.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from 'react'; +import FiberManualRecordIcon from '@mui/icons-material/FiberManualRecord'; import { Box, List, @@ -13,22 +13,20 @@ import { CardMedia, Skeleton, } from '@mui/material'; -import CloseIcon from '@mui/icons-material/Close'; -import { useWallet } from 'ui/utils'; -import { formatString, isValidEthereumAddress } from 'ui/utils/address'; +import { makeStyles } from '@mui/styles'; +import React, { useState, useEffect, useCallback } from 'react'; import { useHistory } from 'react-router-dom'; -import popLock from 'ui/FRWAssets/svg/popLock.svg'; -import popAdd from 'ui/FRWAssets/svg/popAdd.svg'; + +import type { UserInfoResponse } from 'background/service/networkModel'; import importIcon from 'ui/FRWAssets/svg/importIcon.svg'; -import FiberManualRecordIcon from '@mui/icons-material/FiberManualRecord'; -import { makeStyles } from '@mui/styles'; -import { UserInfoResponse } from 'background/service/networkModel'; -import sideMore from '../../../FRWAssets/svg/sideMore.svg'; +import popLock from 'ui/FRWAssets/svg/popLock.svg'; +import { useWallet } from 'ui/utils'; +import { isValidEthereumAddress } from 'ui/utils/address'; import rightarrow from '../../../FRWAssets/svg/rightarrow.svg'; -import NetworkList from './NetworkList'; +import sideMore from '../../../FRWAssets/svg/sideMore.svg'; -import evmlogo from 'ui/FRWAssets/image/evmlogo.png'; +import NetworkList from './NetworkList'; const useStyles = makeStyles(() => ({ menuDrawer: { @@ -108,7 +106,7 @@ const MenuDrawer = (props: MenuDrawerProps) => { history.push('/dashboard/enable'); }; - const checkEvmMode = async () => { + const checkEvmMode = useCallback(async () => { const activeChild = await usewallet.getActiveWallet(); if (activeChild === 'evm') { setIsEvm(true); @@ -116,26 +114,26 @@ const MenuDrawer = (props: MenuDrawerProps) => { setIsEvm(false); } setEvmMode(true); - }; + }, [usewallet]); - const getEvmAddress = async () => { + const getEvmAddress = useCallback(async () => { console.log(props.evmAddress); if (isValidEthereumAddress(props.evmAddress)) { const result = await usewallet.getBalance(props.evmAddress); const readBalance = parseFloat(result) / 1e18; setEvmBalance(readBalance); } - }; + }, [props.evmAddress, usewallet]); const hasChildAccounts = props.childAccounts && Object.keys(props.childAccounts).length > 0; useEffect(() => { checkEvmMode(); - }, []); + }, [checkEvmMode]); useEffect(() => { getEvmAddress(); - }, [props.evmAddress]); + }, [getEvmAddress, props.evmAddress]); return ( { const history = useHistory(); diff --git a/src/ui/views/NFT/EditNFTAddress.tsx b/src/ui/views/NFT/EditNFTAddress.tsx index 3ffbce53..f69c2c9d 100644 --- a/src/ui/views/NFT/EditNFTAddress.tsx +++ b/src/ui/views/NFT/EditNFTAddress.tsx @@ -1,11 +1,13 @@ -import React, { useState, useEffect } from 'react'; +import CloseIcon from '@mui/icons-material/Close'; import { Typography, Box, Drawer, Grid, Stack, InputBase, CircularProgress } from '@mui/material'; import { styled } from '@mui/material/styles'; -import CloseIcon from '@mui/icons-material/Close'; -import { LLPrimaryButton, LLSecondaryButton, LLFormHelperText } from '../../FRWComponent'; -import { useWallet } from 'ui/utils'; -import { useForm, FieldValues } from 'react-hook-form'; +import React, { useState, useEffect, useCallback } from 'react'; +import { useForm, type FieldValues } from 'react-hook-form'; + import { storage } from '@/background/webapi'; +import { useWallet } from 'ui/utils'; + +import { LLPrimaryButton, LLSecondaryButton, LLFormHelperText } from '../../FRWComponent'; const StyledInput = styled(InputBase)(({ theme }) => ({ zIndex: 1, @@ -86,16 +88,16 @@ const EditNFTAddress = (props: EditNFTAddressProps) => { } ); } - }, [props.address]); + }, [props?.address, props?.isEdit, reset]); - const fetchNetworks = async () => { + const fetchNetworks = useCallback(async () => { const currentNetwork = await wallet.getNetwork(); setNetwork(currentNetwork); - }; + }, [wallet]); useEffect(() => { fetchNetworks(); - }, []); + }, [fetchNetworks]); const renderContent = () => ( { + console.log('MoveNftConfirmation'); const usewallet = useWallet(); const history = useHistory(); const [sending, setSending] = useState(false); @@ -33,6 +36,9 @@ const MoveNftConfirmation = (props: SendNFTConfirmationProps) => { const [childWallet, setChildWallet] = useState(null); const [selectedAccount, setSelectedChildAccount] = useState(null); const [childWallets, setChildWallets] = useState({}); + const { sufficient: isSufficient } = useStorageCheck(); + + const isLowStorage = isSufficient !== null && !isSufficient; // isSufficient is null when the storage check is not yet completed const getPending = useCallback(async () => { const pending = await usewallet.getPendingTx(); @@ -357,6 +363,7 @@ const MoveNftConfirmation = (props: SendNFTConfirmationProps) => { + {occupied && ( { )} + {isLowStorage && } + + ) : ( + <> + {failed ? ( + + {chrome.i18n.getMessage('Transaction__failed')} + + ) : ( + + {chrome.i18n.getMessage('Move')} + + )} + + )} + + ); }; diff --git a/src/ui/views/NFT/SendNFT/SendNFTConfirmation.tsx b/src/ui/views/NFT/SendNFT/SendNFTConfirmation.tsx index 1b1fd801..b27b6945 100644 --- a/src/ui/views/NFT/SendNFT/SendNFTConfirmation.tsx +++ b/src/ui/views/NFT/SendNFT/SendNFTConfirmation.tsx @@ -7,7 +7,9 @@ import { useHistory } from 'react-router-dom'; import Web3 from 'web3'; import StorageExceededAlert from '@/ui/FRWComponent/StorageExceededAlert'; +import { WarningStorageLowSnackbar } from '@/ui/FRWComponent/WarningStorageLowSnackbar'; import { MatchMediaType } from '@/ui/utils/url'; +import { useStorageCheck } from '@/ui/utils/useStorageCheck'; import erc721 from 'background/utils/erc721.abi.json'; import { EVM_ENDPOINT } from 'consts'; import IconNext from 'ui/FRWAssets/svg/next.svg'; @@ -25,6 +27,7 @@ interface SendNFTConfirmationProps { } const SendNFTConfirmation = (props: SendNFTConfirmationProps) => { + console.log('SendNFTConfirmation'); const wallet = useWallet(); const history = useHistory(); const [sending, setSending] = useState(false); @@ -36,6 +39,10 @@ const SendNFTConfirmation = (props: SendNFTConfirmationProps) => { const [isChild, setIsChild] = useState(false); const [erc721Contract, setErcContract] = useState(null); const [count, setCount] = useState(0); + const { sufficient: isSufficient } = useStorageCheck(); + + const isLowStorage = isSufficient !== null && !isSufficient; // isSufficient is null when the storage check is not yet completed + const colorArray = [ '#32E35529', '#32E35540', @@ -383,7 +390,6 @@ const SendNFTConfirmation = (props: SendNFTConfirmationProps) => { )} - { /> {props.data.contract && props.data.contract.name} @@ -423,7 +436,6 @@ const SendNFTConfirmation = (props: SendNFTConfirmationProps) => { - {/* { )} + {isLowStorage && }