diff --git a/manifest/manifest.json b/manifest/manifest.json index 1189dbb10..5d8ae5f32 100644 --- a/manifest/manifest.json +++ b/manifest/manifest.json @@ -40,7 +40,8 @@ "notifications", "tabs", "scripting", - "identity" + "identity", + "alarms" ], "host_permissions": ["file://*/*", "http://*/*", "https://*/*"], "oauth2": { diff --git a/src/background/connections/ConnectionService.ts b/src/background/connections/ConnectionService.ts index 7c4a1b021..4929951ec 100644 --- a/src/background/connections/ConnectionService.ts +++ b/src/background/connections/ConnectionService.ts @@ -1,13 +1,8 @@ import browser, { Runtime } from 'webextension-polyfill'; -import { - CONTENT_SCRIPT, - EXTENSION_SCRIPT, - KEEPALIVE_SCRIPT, -} from '@src/common'; +import { CONTENT_SCRIPT, EXTENSION_SCRIPT } from '@src/common'; import { container, singleton } from 'tsyringe'; import { DAppConnectionController } from './dAppConnection/DAppConnectionController'; import { ConnectionController } from './models'; -import { KeepaliveConnectionController } from './keepaliveConnection/KeepaliveConnectionController'; import { ExtensionConnectionController } from './extensionConnection/ExtensionConnectionController'; import { CallbackManager } from '../runtime/CallbackManager'; @@ -46,10 +41,7 @@ export class ConnectionService { connectionController = container.resolve(ExtensionConnectionController); } else if (connection.name === CONTENT_SCRIPT) { connectionController = container.resolve(DAppConnectionController); - } else if (connection.name === KEEPALIVE_SCRIPT) { - connectionController = container.resolve(KeepaliveConnectionController); } - connectionController?.connect(connection); return connectionController; diff --git a/src/background/connections/keepaliveConnection/KeepaliveConnectionController.ts b/src/background/connections/keepaliveConnection/KeepaliveConnectionController.ts deleted file mode 100644 index 922c1b467..000000000 --- a/src/background/connections/keepaliveConnection/KeepaliveConnectionController.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { Runtime } from 'webextension-polyfill'; -import { ConnectionController } from '../models'; - -export class KeepaliveConnectionController implements ConnectionController { - private connection?: Runtime.Port; - - connect(connection: Runtime.Port) { - this.connection = connection; - this.connection.onMessage.addListener(this.onMessage); - setTimeout(() => { - connection.disconnect(); - }, 60000); - } - - disconnect() { - this.connection?.onMessage.removeListener(this.onMessage); - } - - private onMessage(val: any) { - console.log('KEEPALIVE MESSAGE', val); - } -} diff --git a/src/background/services/lock/LockService.ts b/src/background/services/lock/LockService.ts index bade7c6ad..ec4fcf02a 100644 --- a/src/background/services/lock/LockService.ts +++ b/src/background/services/lock/LockService.ts @@ -8,15 +8,17 @@ import { LOCK_TIMEOUT, SessionAuthData, SESSION_AUTH_DATA_KEY, + AlarmsEvents, } from './models'; +import { OnAllExtensionClosed } from '@src/background/runtime/lifecycleCallbacks'; @singleton() -export class LockService { +export class LockService implements OnAllExtensionClosed { private eventEmitter = new EventEmitter(); private _locked = true; - private lockCheckInterval?: any; + #autoLockInMinutes = 30; public get locked(): boolean { return this._locked; @@ -28,6 +30,15 @@ export class LockService { ) {} async activate() { + chrome.runtime.onConnect.addListener(() => { + chrome.alarms.clear(AlarmsEvents.AUTO_LOCK); + }); + + chrome.alarms.onAlarm.addListener((alarm) => { + if (alarm.name === AlarmsEvents.AUTO_LOCK) { + this.lock(); + } + }); const authData = await this.storageService.loadFromSessionStorage( SESSION_AUTH_DATA_KEY, @@ -43,7 +54,14 @@ export class LockService { } await this.unlock(authData.password); - this.startAutoLockInterval(authData?.loginTime); + } + + onAllExtensionsClosed(): void | Promise { + if (!this._locked) { + chrome.alarms.create(AlarmsEvents.AUTO_LOCK, { + periodInMinutes: this.#autoLockInMinutes, + }); + } } async unlock(password: string) { @@ -78,16 +96,6 @@ export class LockService { await this.storageService.changePassword(oldPassword, newPassword); } - private startAutoLockInterval(loginTime: number) { - const timeToLock = loginTime + LOCK_TIMEOUT; - this.lockCheckInterval = setInterval(() => { - if (Date.now() > timeToLock) { - clearInterval(this.lockCheckInterval); - this.lock(); - } - }, 60000); - } - async verifyPassword(password: string): Promise { const authData = await this.storageService.loadFromSessionStorage( @@ -104,6 +112,7 @@ export class LockService { this.eventEmitter.emit(LockEvents.LOCK_STATE_CHANGED, { isUnlocked: false, }); + chrome.alarms.clear(AlarmsEvents.AUTO_LOCK); } addListener( diff --git a/src/background/services/lock/models.ts b/src/background/services/lock/models.ts index a4a0381ec..8806ed1b0 100644 --- a/src/background/services/lock/models.ts +++ b/src/background/services/lock/models.ts @@ -9,6 +9,10 @@ export enum LockEvents { LOCK_STATE_CHANGED = 'LockServiceEvents:Lock', } +export enum AlarmsEvents { + AUTO_LOCK = 'auto-lock', +} + export interface LockStateChangedEventPayload { isUnlocked: boolean; } diff --git a/src/contentscript.ts b/src/contentscript.ts index 0966526e9..73272a870 100644 --- a/src/contentscript.ts +++ b/src/contentscript.ts @@ -1,5 +1,5 @@ -import { CONTENT_SCRIPT, KEEPALIVE_SCRIPT } from './common'; -import browser, { Runtime } from 'webextension-polyfill'; +import { CONTENT_SCRIPT } from './common'; +import browser from 'webextension-polyfill'; import PortConnection from './background/utils/messaging/PortConnection'; import onPageActivated from './background/providers/utils/onPageActivated'; import AutoPairingPostMessageConnection from './background/utils/messaging/AutoPairingPostMessageConnection'; @@ -30,21 +30,6 @@ function setupStream() { backgroundConnection.dispose(); }); - let backgroundKeepaliveConnection: Runtime.Port | null = null; - - function keepAlive() { - if (backgroundKeepaliveConnection) return; - backgroundKeepaliveConnection = browser.runtime.connect({ - name: KEEPALIVE_SCRIPT, - }); - backgroundKeepaliveConnection?.onDisconnect.addListener(() => { - backgroundKeepaliveConnection = null; - keepAlive(); - }); - } - - keepAlive(); - backgroundConnection.on('disconnect', () => { console.log('reconnecting...'); setupStream(); diff --git a/src/contexts/SwapProvider/SwapProvider.test.tsx b/src/contexts/SwapProvider/SwapProvider.test.tsx index 4bd3e4d21..492e7c356 100644 --- a/src/contexts/SwapProvider/SwapProvider.test.tsx +++ b/src/contexts/SwapProvider/SwapProvider.test.tsx @@ -90,6 +90,16 @@ jest.mock('react-i18next', () => ({ })); jest.mock('ethers'); +jest.mock('@avalabs/core-k2-components', () => ({ + toast: { + success: jest.fn(), + loading: jest.fn(), + dismiss: jest.fn(), + error: jest.fn(), + custom: jest.fn(), + remove: jest.fn(), + }, +})); describe('contexts/SwapProvider', () => { const connectionContext = { diff --git a/src/contexts/SwapProvider/SwapProvider.tsx b/src/contexts/SwapProvider/SwapProvider.tsx index 6ea7b5337..c612b7935 100644 --- a/src/contexts/SwapProvider/SwapProvider.tsx +++ b/src/contexts/SwapProvider/SwapProvider.tsx @@ -1,4 +1,4 @@ -import { createContext, useCallback, useContext, useMemo } from 'react'; +import { createContext, useCallback, useContext, useMemo, useRef } from 'react'; import type { JsonRpcBatchInternal } from '@avalabs/core-wallets-sdk'; import { TransactionParams } from '@avalabs/evm-module'; import { resolve } from '@avalabs/core-utils-sdk'; @@ -45,6 +45,8 @@ import { assert, assertPresent } from '@src/utils/assertions'; import { CommonError } from '@src/utils/errors'; import { useWalletContext } from '../WalletProvider'; import { SecretType } from '@src/background/services/secrets/models'; +import { toast } from '@avalabs/core-k2-components'; +import { SwapPendingToast } from '@src/pages/Swap/components/SwapPendingToast'; export const SwapContext = createContext({} as any); @@ -63,6 +65,7 @@ export function SwapContextProvider({ children }: { children: any }) { forceShowTokensWithoutBalances: true, disallowedAssets: DISALLOWED_SWAP_ASSETS, }); + const pendingToastIdRef = useRef(''); const paraswap = useMemo( () => new ParaSwap(ChainId.AVALANCHE_MAINNET_ID, undefined, new Web3()), @@ -273,11 +276,23 @@ export function SwapContextProvider({ children }: { children: any }) { .div(10 ** destDecimals) .toString(); + const notificationText = isSuccessful + ? t('Swap transaction succeeded! πŸŽ‰') + : t('Swap transaction failed! ❌'); + + toast.remove(pendingToastIdRef.current); + + if (isSuccessful) { + toast.success(notificationText); + } + + if (!isSuccessful) { + toast.error(notificationText); + } + browser.notifications.create({ type: 'basic', - title: isSuccessful - ? t('Swap transaction succeeded! πŸŽ‰') - : t('Swap transaction failed! ❌'), + title: notificationText, iconUrl: '../../../../images/icon-192.png', priority: 2, message: isSuccessful @@ -457,6 +472,17 @@ export function SwapContextProvider({ children }: { children: any }) { swapTxHash = txHash; } + const toastId = toast.custom( + toast.remove(toastId)}> + {t('Swap pending...')} + , + + { + duration: Infinity, + }, + ); + pendingToastIdRef.current = toastId; + notifyOnSwapResult({ provider: avaxProviderC, txHash: swapTxHash, @@ -470,15 +496,16 @@ export function SwapContextProvider({ children }: { children: any }) { }); }, [ - activeAccount, + isFlagEnabled, activeNetwork, + networkFee, + activeAccount, avaxProviderC, + getSwapTxProps, buildTx, - networkFee, - request, + t, notifyOnSwapResult, - getSwapTxProps, - isFlagEnabled, + request, ], ); @@ -569,6 +596,17 @@ export function SwapContextProvider({ children }: { children: any }) { }), ); + const toastId = toast.custom( + toast.remove(toastId)}> + {t('Swap pending...')} + , + + { + duration: Infinity, + }, + ); + pendingToastIdRef.current = toastId; + if (signError || !swapTxHash) { return throwError(signError); } @@ -586,14 +624,15 @@ export function SwapContextProvider({ children }: { children: any }) { }); }, [ - activeAccount, activeNetwork, - avaxProviderC, - buildTx, networkFee, + activeAccount, + avaxProviderC, + getSwapTxProps, request, + t, notifyOnSwapResult, - getSwapTxProps, + buildTx, ], ); diff --git a/src/localization/locales/en/translation.json b/src/localization/locales/en/translation.json index 6aef1b474..f158972ce 100644 --- a/src/localization/locales/en/translation.json +++ b/src/localization/locales/en/translation.json @@ -32,11 +32,9 @@ "Active Wallet:": "Active Wallet:", "Activity": "Activity", "Add one recovery method to continue.": "Add one recovery method to continue.", - "Add Address via": "Add Address via", "Add Custom Token": "Add Custom Token", "Add Delegator": "Add Delegator", "Add Network": "Add Network", - "Add New Address": "Add New Address", "Add New Asset?": "Add New Asset?", "Add New Network?": "Add New Network?", "Add Next": "Add Next", @@ -53,12 +51,14 @@ "Add Wallet with Recovery Phrase": "Add Wallet with Recovery Phrase", "Add a Passkey as a recovery method.": "Add a Passkey as a recovery method.", "Add a Yubikey as a recovery method.": "Add a Yubikey as a recovery method.", + "Add a new account to your active wallet": "Add a new account to your active wallet", "Add a {{device}} name, so that it’s easier to find later.": "Add a {{device}} name, so that it’s easier to find later.", "Add account(s) by entering a valid recovery phrase.": "Add account(s) by entering a valid recovery phrase.", "Add an account by entering a private key": "Add an account by entering a private key", "Add assets": "Add assets", "Add assets by Buying or Receiving": "Add assets by Buying or Receiving", "Add assets by clicking the button below": "Add assets by clicking the button below", + "Add or Connect Wallet": "Add or Connect Wallet", "Add using Keystone": "Add using Keystone", "Add using Ledger": "Add using Ledger", "Address": "Address", @@ -195,6 +195,7 @@ "Connect Network": "Connect Network", "Connect Software Wallet": "Connect Software Wallet", "Connect the Ledger device to your computer.": "Connect the Ledger device to your computer.", + "Connect using Wallet Connect": "Connect using Wallet Connect", "Connect your Ledger": "Connect your Ledger", "Connect your Ledger device and open the {{appType}} App to approve this transaction": "Connect your Ledger device and open the {{appType}} App to approve this transaction", "Connect your Wallet": "Connect your Wallet", @@ -237,6 +238,7 @@ "Create Asset": "Create Asset", "Create Blockchain": "Create Blockchain", "Create Chain": "Create Chain", + "Create New Account ": "Create New Account ", "Create New Password": "Create New Password", "Create Subnet": "Create Subnet", "Currency": "Currency", @@ -417,8 +419,14 @@ "Import": "Import", "Import Details": "Import Details", "Import Duplicate Account?": "Import Duplicate Account?", + "Import Fireblocks Account": "Import Fireblocks Account", "Import Keystore File": "Import Keystore File", + "Import Ledger Wallet": "Import Ledger Wallet", "Import Private Key": "Import Private Key", + "Import Recovery Phrase": "Import Recovery Phrase", + "Import a single-chain account": "Import a single-chain account", + "Import account with Wallet Connect": "Import account with Wallet Connect", + "Import accounts from another wallet": "Import accounts from another wallet", "Imported": "Imported", "Imported Private Key": "Imported Private Key", "In order for this network to be fully functional, you need to provide your Glacier API key. You will be prompted to do so upon approval.": "In order for this network to be fully functional, you need to provide your Glacier API key. You will be prompted to do so upon approval.", @@ -463,7 +471,6 @@ "Keystone": "Keystone", "Keystone Support": "Keystone Support", "Keystone {{number}}": "Keystone {{number}}", - "Keystore File": "Keystore File", "Korean": "Korean", "Language": "Language", "Learn More": "Learn More", @@ -629,7 +636,6 @@ "Please reconnect using Wallet Connect to add this network to authorized networks.": "Please reconnect using Wallet Connect to add this network to authorized networks.", "Please refer to Active Transfers list in your Fireblocks Console for a detailed explanation.": "Please refer to Active Transfers list in your Fireblocks Console for a detailed explanation.", "Please remove address prefix. (P- or X-)": "Please remove address prefix. (P- or X-)", - "Please select a wallet": "Please select a wallet", "Please sign on your mobile wallet.": "Please sign on your mobile wallet.", "Please switch to Avalanche C-Chain to import your Fireblocks account.": "Please switch to Avalanche C-Chain to import your Fireblocks account.", "Please switch to the {{requiredAppType}} app on your Ledger": "Please switch to the {{requiredAppType}} app on your Ledger", @@ -981,11 +987,14 @@ "Update": "Update", "Update Required": "Update Required", "Upload Keystore File": "Upload Keystore File", + "Use Fireblocks application": "Use Fireblocks application", + "Use a keystore from the Avalanche Wallet": "Use a keystore from the Avalanche Wallet", "Use an authenticator app as a recovery method.": "Use an authenticator app as a recovery method.", "Use any authenticator app to scan the QR code. Or enter code manually.": "Use any authenticator app to scan the QR code. Or enter code manually.", "Use any authenticator app and paste in the code found below.": "Use any authenticator app and paste in the code found below.", "Use caution, this application may be malicious.": "Use caution, this application may be malicious.", "Use caution, this transaction may be malicious.": "Use caution, this transaction may be malicious.", + "Use your Ledger hardware wallet": "Use your Ledger hardware wallet", "User declined the transaction": "User declined the transaction", "User rejected the request": "User rejected the request", "Users may not use the Bridge if they are on the Specially Designated Nationals (SDN) List of the Office of Foreign Assets Control (OFAC) or any other sanctions or are otherwise a sanctioned person or from a sanctioned jurisdiction": "Users may not use the Bridge if they are on the Specially Designated Nationals (SDN) List of the Office of Foreign Assets Control (OFAC) or any other sanctions or are otherwise a sanctioned person or from a sanctioned jurisdiction", diff --git a/src/pages/Accounts/Accounts.tsx b/src/pages/Accounts/Accounts.tsx index b9ce23ff1..ae2221214 100644 --- a/src/pages/Accounts/Accounts.tsx +++ b/src/pages/Accounts/Accounts.tsx @@ -254,9 +254,6 @@ export function Accounts() { )} diff --git a/src/pages/Accounts/components/AccountsActionButton.tsx b/src/pages/Accounts/components/AccountsActionButton.tsx index 0a9e93ea7..c0b0009b5 100644 --- a/src/pages/Accounts/components/AccountsActionButton.tsx +++ b/src/pages/Accounts/components/AccountsActionButton.tsx @@ -1,6 +1,5 @@ import { Button, - ButtonGroup, ChevronDownIcon, ClickAwayListener, Grow, @@ -14,9 +13,12 @@ import { Tooltip, ListIcon, Typography, - TypographyProps, LedgerIcon, KeystoreIcon, + Box, + ChevronRightIcon, + Stack, + PlusIcon, } from '@avalabs/core-k2-components'; import { useCallback, useMemo, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; @@ -34,52 +36,34 @@ type AccountsActionButtonProps = { isLoading: boolean; canCreateAccount: boolean; onAddNewAccount: () => void; - createAccountTooltip?: string; }; -const StyledMenuItem = styled(MenuItem)` - color: ${({ theme }) => theme.palette.text.secondary}; - &:hover { - color: ${({ theme }) => theme.palette.text.primary}; - } +const MenuItemColumn = styled(Stack)` + flex-direction: row; + align-items: center; + width: 100%; + justify-content: space-between; + border-bottom: 1px solid + ${({ theme, hasNoBorder }) => + hasNoBorder ? 'transparent' : theme.palette.grey[800]}; + padding-top: ${({ theme }) => theme.spacing(1.5)}; + padding-bottom: ${({ theme }) => theme.spacing(1.5)}; `; -const RoundedButtonGroup = styled(ButtonGroup)` - & > .MuiButtonGroup-grouped { - border-radius: 0; - height: 40px; - - &:not(:last-of-type) { - margin-right: 1px; - - &.Mui-disabled { - margin-right: 1px; - } - } - - &:first-of-type { - border-radius: 24px 0 0 24px; - } - - &:last-of-type { - border-radius: 0 24px 24px 0; - } - } +const StyledMenuItem = styled(MenuItem)` + cursor: pointer; + padding: 0; + width: 100%; `; -const MenuHeader = (props: TypographyProps) => ( - -); +const IconColumn = styled(Stack)` + padding-right: ${({ theme }) => theme.spacing(2)}; + padding-left: ${({ theme }) => theme.spacing(2)}; +`; export const AccountsActionButton = ({ isLoading, onAddNewAccount, - createAccountTooltip, canCreateAccount, }: AccountsActionButtonProps) => { const [isMenuOpen, setIsMenuOpen] = useState(false); @@ -138,135 +122,298 @@ export const AccountsActionButton = ({ }, [t, network]); return ( - - setIsMenuOpen(false)}> + - - setIsMenuOpen(false)}> - - - + {featureFlags[FeatureGates.ADD_WALLET_WITH_SEEDPHRASE] && ( + + + + + + + + + {t('Import Recovery Phrase')} + + + + + {t('Import accounts from another wallet')} + + + + + + + + + )} + {featureFlags[FeatureGates.ADD_WALLET_WITH_LEDGER] && ( + + + + + + + + + {t('Import Ledger Wallet')} + + + + + {t('Use your Ledger hardware wallet')} + + + + + + + + + )} + {featureFlags[FeatureGates.ADD_WALLET_WITH_KEYSTORE_FILE] && ( + + + + + + + + + {t('Import Keystore File')} + + + + + {t('Use a keystore from the Avalanche Wallet')} + + + + + + + + + )} + {canCreateAccount && ( + { + onAddNewAccount(); + setIsMenuOpen(false); + }} + data-testid={'add-primary-account'} + > + + + + + + + + {t('Create New Account ')} + + + + + {t('Add a new account to your active wallet')} + + + + + + + + + )} + + + )} + + + ); }; diff --git a/src/pages/Home/components/Portfolio/Assets.tsx b/src/pages/Home/components/Portfolio/Assets.tsx index cb1656f8d..d41a73028 100644 --- a/src/pages/Home/components/Portfolio/Assets.tsx +++ b/src/pages/Home/components/Portfolio/Assets.tsx @@ -28,12 +28,16 @@ import { isBitcoinNetwork } from '@src/background/services/network/utils/isBitco import { useAnalyticsContext } from '@src/contexts/AnalyticsProvider'; import { useTokenPriceMissing } from '@src/hooks/useTokenPriceIsMissing'; import { PAndL } from '@src/components/common/ProfitAndLoss'; +import { useLiveBalance } from '@src/hooks/useLiveBalance'; +import { TokenType } from '@avalabs/vm-module-types'; enum AssetsTabs { TOKENS, ACTIVITY, } +const POLLED_BALANCES = [TokenType.NATIVE, TokenType.ERC20]; + export function Assets() { const { t } = useTranslation(); const { network } = useNetworkContext(); @@ -70,6 +74,8 @@ export function Assets() { return isPriceMissingFromNetwork(network?.chainId); }, [isPriceMissingFromNetwork, network]); + useLiveBalance(POLLED_BALANCES); + return ( ( + + + + + + + {children} + + + + +); diff --git a/src/pages/Wallet/TokenFlow.tsx b/src/pages/Wallet/TokenFlow.tsx index e6c4ac5ab..f2c385be4 100644 --- a/src/pages/Wallet/TokenFlow.tsx +++ b/src/pages/Wallet/TokenFlow.tsx @@ -38,6 +38,10 @@ import { isXchainNetwork } from '@src/background/services/network/utils/isAvalan import { getUnconfirmedBalanceInCurrency } from '@src/background/services/balances/models'; import { isTokenMalicious } from '@src/utils/isTokenMalicious'; import { MaliciousTokenWarningBox } from '@src/components/common/MaliciousTokenWarning'; +import { useLiveBalance } from '@src/hooks/useLiveBalance'; +import { TokenType } from '@avalabs/vm-module-types'; + +const POLLED_BALANCES = [TokenType.NATIVE, TokenType.ERC20]; export function TokenFlow() { const { t } = useTranslation(); @@ -97,6 +101,8 @@ export function TokenFlow() { } }, [activeAccount, hasAccessToActiveNetwork, tokensWithBalances]); + useLiveBalance(POLLED_BALANCES); + if (!hasAccessToActiveNetwork) { return (