diff --git a/packages/extension-polkagate/tsconfig.build.json b/packages/extension-polkagate/tsconfig.build.json index 70949b8f9..1a4bbc9cf 100644 --- a/packages/extension-polkagate/tsconfig.build.json +++ b/packages/extension-polkagate/tsconfig.build.json @@ -18,8 +18,6 @@ { "path": "../extension-inject/tsconfig.build.json" }, { "path": "../extension-mocks/tsconfig.build.json" - }, - { "path": "../extension-ui/tsconfig.build.json" } ] } diff --git a/packages/extension-ui/src/Popup/contexts/AccountAssetProvider.tsx b/packages/extension-ui/src/Popup/contexts/AccountAssetProvider.tsx new file mode 100644 index 000000000..5b5b52288 --- /dev/null +++ b/packages/extension-ui/src/Popup/contexts/AccountAssetProvider.tsx @@ -0,0 +1,51 @@ +// Copyright 2019-2024 @polkadot/extension-polkagate authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { SavedAssets } from '@polkadot/extension-polkagate/hooks/useAssetsBalances'; + +import React, { useContext, useEffect, useState } from 'react'; + +import { AccountContext, AccountsAssetsContext, AlertContext, GenesisHashOptionsContext, UserAddedChainContext, WorkerContext } from '@polkadot/extension-polkagate/src/components/contexts'; +import { setStorage } from '@polkadot/extension-polkagate/src/components/Loading'; +import useAssetsBalances, { ASSETS_NAME_IN_STORAGE } from '@polkadot/extension-polkagate/src/hooks/useAssetsBalances'; +import useNFT from '@polkadot/extension-polkagate/src/hooks/useNFT'; + +export default function AccountAssetProvider ({ children }: { children: React.ReactNode }) { + const { accounts } = useContext(AccountContext); + const genesisHashOptions = useContext(GenesisHashOptionsContext); + const { setAlerts } = useContext(AlertContext); + const userAddedChainCtx = useContext(UserAddedChainContext); + const worker = useContext(WorkerContext); + + const [accountsAssets, setAccountsAssets] = useState(); + + const assetsOnChains = useAssetsBalances(accounts, setAlerts, genesisHashOptions, userAddedChainCtx, worker); + + useNFT(accounts); + + useEffect(() => { + assetsOnChains && setAccountsAssets({ ...assetsOnChains }); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [assetsOnChains?.timeStamp]); + + useEffect(() => { + /** remove forgotten accounts from assetChains if any */ + if (accounts && assetsOnChains?.balances) { + Object.keys(assetsOnChains.balances).forEach((_address) => { + const found = accounts.find(({ address }) => address === _address); + + if (!found) { + delete assetsOnChains.balances[_address]; + setStorage(ASSETS_NAME_IN_STORAGE, assetsOnChains, true).catch(console.error); + } + }); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [accounts?.length, assetsOnChains?.timeStamp]); + + return ( + + {children} + + ); +} diff --git a/packages/extension-ui/src/Popup/contexts/AccountIconThemeProvider.tsx b/packages/extension-ui/src/Popup/contexts/AccountIconThemeProvider.tsx new file mode 100644 index 000000000..6b46a7c76 --- /dev/null +++ b/packages/extension-ui/src/Popup/contexts/AccountIconThemeProvider.tsx @@ -0,0 +1,32 @@ +// Copyright 2019-2024 @polkadot/extension-polkagate authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { IconTheme } from '@polkadot/react-identicon/types'; + +import React, { useEffect, useState } from 'react'; + +import { AccountIconThemeContext } from '@polkadot/extension-polkagate/src/components/contexts'; +import { getStorage, watchStorage } from '@polkadot/extension-polkagate/src/components/Loading'; +import { DEFAULT_ACCOUNT_ICON_THEME } from '@polkadot/extension-polkagate/src/util/constants'; + +export default function AccountIconThemeProvider ({ children }: { children: React.ReactNode }) { + const [accountIconTheme, setAccountIconTheme] = useState(DEFAULT_ACCOUNT_ICON_THEME); + + useEffect(() => { + getStorage('iconTheme') + .then((maybeTheme) => setAccountIconTheme((maybeTheme as IconTheme | undefined) || DEFAULT_ACCOUNT_ICON_THEME)) + .catch(console.error); + + const unsubscribe = watchStorage('iconTheme', setAccountIconTheme); + + return () => { + unsubscribe(); + }; + }, []); + + return ( + + {children} + + ); +} diff --git a/packages/extension-ui/src/Popup/contexts/AccountProvider.tsx b/packages/extension-ui/src/Popup/contexts/AccountProvider.tsx new file mode 100644 index 000000000..d7f5bb8cc --- /dev/null +++ b/packages/extension-ui/src/Popup/contexts/AccountProvider.tsx @@ -0,0 +1,75 @@ +// Copyright 2019-2024 @polkadot/extension-polkagate authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { AccountJson, AccountsContext } from '@polkadot/extension-base/background/types'; + +import React, { useEffect, useState } from 'react'; + +import { canDerive } from '@polkadot/extension-base/utils'; +import { AccountContext } from '@polkadot/extension-polkagate/src/components/contexts'; +import { getStorage, type LoginInfo, updateStorage } from '@polkadot/extension-polkagate/src/components/Loading'; +import { subscribeAccounts } from '@polkadot/extension-polkagate/src/messaging'; +import { buildHierarchy } from '@polkadot/extension-polkagate/src/util/buildHierarchy'; + +function initAccountContext (accounts: AccountJson[]): AccountsContext { + const hierarchy = buildHierarchy(accounts); + const master = hierarchy.find(({ isExternal, type }) => !isExternal && canDerive(type)); + + return { + accounts, + hierarchy, + master + }; +} + +export default function AccountProvider ({ children }: { children: React.ReactNode }) { + const [accounts, setAccounts] = useState(null); + const [accountCtx, setAccountCtx] = useState({ accounts: [], hierarchy: [] }); + const [loginInfo, setLoginInfo] = useState(); + + useEffect(() => { + subscribeAccounts(setAccounts).catch(console.log); + }, []); + + useEffect(() => { + const fetchLoginInfo = async () => { + chrome.storage.onChanged.addListener(function (changes, areaName) { + if (areaName === 'local' && 'loginInfo' in changes) { + const newValue = changes['loginInfo'].newValue as LoginInfo; + + setLoginInfo(newValue); + } + }); + const info = await getStorage('loginInfo') as LoginInfo; + + setLoginInfo(info); + }; + + fetchLoginInfo().catch(console.error); + }, []); + + useEffect(() => { + if (!loginInfo) { + return; + } + + if (loginInfo.status !== 'forgot') { + setAccountCtx(initAccountContext(accounts || [])); + } else if (loginInfo.status === 'forgot') { + setAccountCtx(initAccountContext([])); + const addresses = accounts?.map((account) => account.address); + + updateStorage('loginInfo', { addressesToForget: addresses }).catch(console.error); + } + }, [accounts, loginInfo]); + + if (!accounts) { + return null; + } + + return ( + + {children} + + ); +} diff --git a/packages/extension-ui/src/Popup/contexts/ActionProvider.tsx b/packages/extension-ui/src/Popup/contexts/ActionProvider.tsx new file mode 100644 index 000000000..ed5717940 --- /dev/null +++ b/packages/extension-ui/src/Popup/contexts/ActionProvider.tsx @@ -0,0 +1,20 @@ +// Copyright 2019-2024 @polkadot/extension-polkagate authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import React, { useCallback } from 'react'; + +import { ActionContext } from '@polkadot/extension-polkagate/src/components/contexts'; + +export default function ActionProvider ({ children }: { children: React.ReactNode }) { + const onAction = useCallback((to?: string): void => { + if (to) { + window.location.hash = to; + } + }, []); + + return ( + + {children} + + ); +} diff --git a/packages/extension-ui/src/Popup/contexts/AlertProvider.tsx b/packages/extension-ui/src/Popup/contexts/AlertProvider.tsx new file mode 100644 index 000000000..88b66e204 --- /dev/null +++ b/packages/extension-ui/src/Popup/contexts/AlertProvider.tsx @@ -0,0 +1,18 @@ +// Copyright 2019-2024 @polkadot/extension-polkagate authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { AlertType } from '@polkadot/extension-polkagate/util/types'; + +import React, { useState } from 'react'; + +import { AlertContext } from '@polkadot/extension-polkagate/src/components/contexts'; + +export default function AlertProvider ({ children }: { children: React.ReactNode }) { + const [alerts, setAlerts] = useState([]); + + return ( + + {children} + + ); +} diff --git a/packages/extension-ui/src/Popup/contexts/ApiProvider.tsx b/packages/extension-ui/src/Popup/contexts/ApiProvider.tsx new file mode 100644 index 000000000..49c4c86e8 --- /dev/null +++ b/packages/extension-ui/src/Popup/contexts/ApiProvider.tsx @@ -0,0 +1,22 @@ +// Copyright 2019-2024 @polkadot/extension-polkagate authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { APIs } from '@polkadot/extension-polkagate/util/types'; + +import React, { useState } from 'react'; + +import { APIContext } from '@polkadot/extension-polkagate/src/components/contexts'; + +export default function ApiProvider ({ children }: { children: React.ReactNode }) { + const [apis, setApis] = useState({}); + + const updateApis = React.useCallback((change: APIs) => { + setApis(change); + }, []); + + return ( + + {children} + + ); +} diff --git a/packages/extension-ui/src/Popup/contexts/CurrencyProvider.tsx b/packages/extension-ui/src/Popup/contexts/CurrencyProvider.tsx new file mode 100644 index 000000000..867541674 --- /dev/null +++ b/packages/extension-ui/src/Popup/contexts/CurrencyProvider.tsx @@ -0,0 +1,61 @@ +// Copyright 2019-2024 @polkadot/extension-polkagate authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { CurrencyItemType } from '@polkadot/extension-polkagate/src/fullscreen/homeFullScreen/partials/Currency'; +import type { Prices, PricesInCurrencies } from '@polkadot/extension-polkagate/src/util/types'; + +import React, { useEffect, useState } from 'react'; + +import { CurrencyContext } from '@polkadot/extension-polkagate/src/components/contexts'; +import { getStorage, setStorage } from '@polkadot/extension-polkagate/src/components/Loading'; +import usePriceIds from '@polkadot/extension-polkagate/src/hooks/usePriceIds'; +import { isPriceUpToDate } from '@polkadot/extension-polkagate/src/hooks/usePrices'; +import { getPrices } from '@polkadot/extension-polkagate/src/util/api'; + +interface CurrencyProviderProps { + children: React.ReactNode; +} + +export default function CurrencyProvider ({ children }: CurrencyProviderProps) { + const priceIds = usePriceIds(); + + const [currency, setCurrency] = useState(); + const isFetchingPricesRef = React.useRef(false); + + useEffect(() => { + if (priceIds && currency?.code && !isFetchingPricesRef.current) { + isFetchingPricesRef.current = true; + + getStorage('pricesInCurrencies') + .then((res) => { + const savedPricesInCurrencies = (res || {}) as PricesInCurrencies; + const maybeSavedPriceInCurrentCurrencyCode = savedPricesInCurrencies[currency.code]; + + if (maybeSavedPriceInCurrentCurrencyCode && isPriceUpToDate(maybeSavedPriceInCurrentCurrencyCode.date)) { + /** price in the selected currency is already updated hence no need to fetch again */ + // TODO: FixMe: what if users change selected chainS during price validity period? + return; + } + + getPrices(priceIds, currency.code.toLowerCase()) + .then((newPrices) => { + delete (newPrices as Prices).currencyCode; + savedPricesInCurrencies[currency.code] = newPrices; + setStorage('pricesInCurrencies', savedPricesInCurrencies) + .catch(console.error); + }) + .catch(console.error); + }) + .catch(console.error) + .finally(() => { + isFetchingPricesRef.current = false; + }); + } + }, [currency?.code, priceIds]); + + return ( + + {children} + + ); +} diff --git a/packages/extension-ui/src/Popup/contexts/FetchingProvider.tsx b/packages/extension-ui/src/Popup/contexts/FetchingProvider.tsx new file mode 100644 index 000000000..460c30dc2 --- /dev/null +++ b/packages/extension-ui/src/Popup/contexts/FetchingProvider.tsx @@ -0,0 +1,22 @@ +// Copyright 2019-2024 @polkadot/extension-polkagate authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { Fetching } from '@polkadot/extension-polkagate/util/types'; + +import React, { useCallback, useState } from 'react'; + +import { FetchingContext } from '@polkadot/extension-polkagate/src/components/contexts'; + +export default function FetchingProvider ({ children }: { children: React.ReactNode }) { + const [fetching, setFetching] = useState({}); + + const set = useCallback((change: Fetching) => { + setFetching(change); + }, []); + + return ( + + {children} + + ); +} diff --git a/packages/extension-ui/src/Popup/contexts/GenesisHashOptionsProvider.tsx b/packages/extension-ui/src/Popup/contexts/GenesisHashOptionsProvider.tsx new file mode 100644 index 000000000..06136679e --- /dev/null +++ b/packages/extension-ui/src/Popup/contexts/GenesisHashOptionsProvider.tsx @@ -0,0 +1,17 @@ +// Copyright 2019-2024 @polkadot/extension-polkagate authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import React from 'react'; + +import { GenesisHashOptionsContext } from '@polkadot/extension-polkagate/src/components/contexts'; +import useGenesisHashOptions from '@polkadot/extension-polkagate/src/hooks/useGenesisHashOptions'; + +export default function GenesisHashOptionsProvider ({ children }: { children: React.ReactNode }) { + const genesisHashOptionsCtx = useGenesisHashOptions(); + + return ( + + {children} + + ); +} diff --git a/packages/extension-ui/src/Popup/contexts/MediaProvider.tsx b/packages/extension-ui/src/Popup/contexts/MediaProvider.tsx new file mode 100644 index 000000000..668c1d058 --- /dev/null +++ b/packages/extension-ui/src/Popup/contexts/MediaProvider.tsx @@ -0,0 +1,49 @@ +// Copyright 2019-2024 @polkadot/extension-polkagate authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import React, { useContext, useEffect, useState } from 'react'; + +import { MediaContext, SettingsContext } from '@polkadot/extension-polkagate/src/components/contexts'; + +interface MediaProviderProps { + children: React.ReactNode; +} + +// Request permission for video, based on access we can hide/show import +async function requestMediaAccess (cameraOn: boolean): Promise { + if (!cameraOn) { + return false; + } + + try { + await navigator.mediaDevices.getUserMedia({ video: true }); + + return true; + } catch (error) { + console.error('Permission for video declined', (error as Error).message); + } + + return false; +} + +export default function MediaProvider ({ children }: MediaProviderProps) { + const settings = useContext(SettingsContext); + const [cameraOn, setCameraOn] = useState(settings.camera === 'on'); + const [mediaAllowed, setMediaAllowed] = useState(false); + + useEffect(() => { + setCameraOn(settings.camera === 'on'); + }, [settings.camera]); + + useEffect(() => { + requestMediaAccess(cameraOn) + .then(setMediaAllowed) + .catch(console.error); + }, [cameraOn]); + + return ( + + {children} + + ); +} diff --git a/packages/extension-ui/src/Popup/contexts/ReferendaProvider.tsx b/packages/extension-ui/src/Popup/contexts/ReferendaProvider.tsx new file mode 100644 index 000000000..49d01046b --- /dev/null +++ b/packages/extension-ui/src/Popup/contexts/ReferendaProvider.tsx @@ -0,0 +1,18 @@ +// Copyright 2019-2024 @polkadot/extension-polkagate authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { LatestRefs } from '@polkadot/extension-polkagate/util/types'; + +import React, { useState } from 'react'; + +import { ReferendaContext } from '@polkadot/extension-polkagate/src/components/contexts'; + +export default function ReferendaProvider ({ children }: { children: React.ReactNode }) { + const [refs, setRefs] = useState({}); + + return ( + + {children} + + ); +} diff --git a/packages/extension-ui/src/Popup/contexts/RequestsProvider.tsx b/packages/extension-ui/src/Popup/contexts/RequestsProvider.tsx new file mode 100644 index 000000000..fe98e25f2 --- /dev/null +++ b/packages/extension-ui/src/Popup/contexts/RequestsProvider.tsx @@ -0,0 +1,78 @@ +// Copyright 2019-2024 @polkadot/extension-polkagate authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { AuthorizeRequest, MetadataRequest, SigningRequest } from '@polkadot/extension-base/background/types'; + +import React, { useContext, useEffect, useLayoutEffect, useState } from 'react'; +import { useLocation } from 'react-router'; + +import { ActionContext, AuthorizeReqContext, MetadataReqContext, SigningReqContext } from '@polkadot/extension-polkagate/src/components/contexts'; +import { subscribeAuthorizeRequests, subscribeMetadataRequests, subscribeSigningRequests } from '@polkadot/extension-polkagate/src/messaging'; + +interface AuthorizationProviderProps { + children: React.ReactNode; +} + +export default function RequestsProvider ({ children }: AuthorizationProviderProps) { + const onAction = useContext(ActionContext); + const { pathname } = useLocation(); + + const [authRequests, setAuthRequests] = useState(null); + const [metaRequests, setMetaRequests] = useState(null); + const [signRequests, setSignRequests] = useState(null); + + useLayoutEffect(() => { + // Use a flag to prevent race conditions + let isMounted = true; + + const handleRouting = () => { + if (!isMounted) { + return; + } + + if (!authRequests || !metaRequests || !signRequests) { + return; + } + + if (authRequests.length) { + onAction('/authorize'); + } else if (metaRequests.length) { + onAction('/metadata'); + } else if (signRequests.length) { + onAction('/signing'); + } else if (['/authorize', '/metadata', '/signing'].includes(pathname)) { + onAction('/'); + } else { + onAction(); + } + }; + + handleRouting(); + + return () => { + isMounted = false; + }; + }, [onAction, authRequests, authRequests?.length, metaRequests, metaRequests?.length, signRequests, signRequests?.length, pathname]); + + useEffect(() => { + Promise.all([ + subscribeAuthorizeRequests(setAuthRequests), + subscribeMetadataRequests(setMetaRequests), + subscribeSigningRequests(setSignRequests) + ]).catch(console.error); + }, []); + + if (authRequests === null || metaRequests === null || signRequests === null) { + return null; + } + + return ( + + + + {children} + + + + ); +} diff --git a/packages/extension-ui/src/Popup/contexts/SettingsProvider.tsx b/packages/extension-ui/src/Popup/contexts/SettingsProvider.tsx new file mode 100644 index 000000000..0aad98f74 --- /dev/null +++ b/packages/extension-ui/src/Popup/contexts/SettingsProvider.tsx @@ -0,0 +1,31 @@ +// Copyright 2019-2024 @polkadot/extension-polkagate authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { SettingsStruct } from '@polkadot/ui-settings/types'; + +import React, { useEffect, useState } from 'react'; + +import { SettingsContext } from '@polkadot/extension-polkagate/src/components/contexts'; +import uiSettings from '@polkadot/ui-settings'; + +interface SettingsProviderProps { + children: React.ReactNode; +} + +export default function SettingsProvider ({ children }: SettingsProviderProps) { + const [settingsCtx, setSettingsCtx] = useState(uiSettings.get()); + + useEffect(() => { + const settingsChange = (settings: SettingsStruct): void => { + setSettingsCtx(settings); + }; + + uiSettings.on('change', settingsChange); + }, []); + + return ( + + {children} + + ); +} diff --git a/packages/extension-ui/src/Popup/contexts/UserAddedChainsProvider.tsx b/packages/extension-ui/src/Popup/contexts/UserAddedChainsProvider.tsx new file mode 100644 index 000000000..f29e941ea --- /dev/null +++ b/packages/extension-ui/src/Popup/contexts/UserAddedChainsProvider.tsx @@ -0,0 +1,26 @@ +// Copyright 2019-2024 @polkadot/extension-polkagate authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { UserAddedChains } from '@polkadot/extension-polkagate/util/types'; + +import React, { useEffect, useState } from 'react'; + +import { UserAddedChainContext } from '@polkadot/extension-polkagate/src/components/contexts'; +import { getStorage } from '@polkadot/extension-polkagate/src/components/Loading'; + +export default function UserAddedChainsProvider ({ children }: { children: React.ReactNode }) { + const [userAddedChainCtx, setUserAddedChainCtx] = useState({}); + + useEffect((): void => { + getStorage('userAddedEndpoint').then((info) => { + info && setUserAddedChainCtx(info as UserAddedChains); + }).catch(console.error); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + return ( + + {children} + + ); +} diff --git a/packages/extension-ui/src/Popup/contexts/WorkerProvider.tsx b/packages/extension-ui/src/Popup/contexts/WorkerProvider.tsx new file mode 100644 index 000000000..5b0b9a0e6 --- /dev/null +++ b/packages/extension-ui/src/Popup/contexts/WorkerProvider.tsx @@ -0,0 +1,33 @@ +// Copyright 2019-2024 @polkadot/extension-polkagate authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import React, { useEffect, useState } from 'react'; + +import { WorkerContext } from '@polkadot/extension-polkagate/src/components/contexts'; + +interface WorkerProviderProps { + children: React.ReactNode; +} + +export default function WorkerProvider ({ children }: WorkerProviderProps) { + const [worker, setWorker] = useState(undefined); + + useEffect(() => { + const newWorker = new Worker(new URL('@polkadot/extension-polkagate/src/util/workers/sharedWorker.js', import.meta.url)); + + setWorker(newWorker); + + return () => { + // Cleanup on unmount + if (newWorker) { + newWorker.terminate(); + } + }; + }, []); + + return ( + + {children} + + ); +} diff --git a/packages/extension-ui/src/Popup/contexts/index.ts b/packages/extension-ui/src/Popup/contexts/index.ts new file mode 100644 index 000000000..165d62df5 --- /dev/null +++ b/packages/extension-ui/src/Popup/contexts/index.ts @@ -0,0 +1,18 @@ +// Copyright 2019-2024 @polkadot/extension-polkagate authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +export { default as AccountAssetProvider } from './AccountAssetProvider'; +export { default as AccountIconThemeProvider } from './AccountIconThemeProvider'; +export { default as AccountProvider } from './AccountProvider'; +export { default as ActionProvider } from './ActionProvider'; +export { default as AlertProvider } from './AlertProvider'; +export { default as ApiProvider } from './ApiProvider'; +export { default as CurrencyProvider } from './CurrencyProvider'; +export { default as FetchingProvider } from './FetchingProvider'; +export { default as GenesisHashOptionsProvider } from './GenesisHashOptionsProvider'; +export { default as MediaProvider } from './MediaProvider'; +export { default as ReferendaProvider } from './ReferendaProvider'; +export { default as RequestsProvider } from './RequestsProvider'; +export { default as SettingsProvider } from './SettingsProvider'; +export { default as UserAddedChainsProvider } from './UserAddedChainsProvider'; +export { default as WorkerProvider } from './WorkerProvider'; diff --git a/packages/extension-ui/src/Popup/index.tsx b/packages/extension-ui/src/Popup/index.tsx index f676132bf..84c3abecd 100644 --- a/packages/extension-ui/src/Popup/index.tsx +++ b/packages/extension-ui/src/Popup/index.tsx @@ -1,312 +1,51 @@ // Copyright 2019-2024 @polkadot/extension-polkagate authors & contributors // SPDX-License-Identifier: Apache-2.0 -import type { AccountJson, AccountsContext, AuthorizeRequest, MetadataRequest, SigningRequest } from '@polkadot/extension-base/background/types'; -import type { CurrencyItemType } from '@polkadot/extension-polkagate/src/fullscreen/homeFullScreen/partials/Currency'; -import type { AlertType, APIs, Fetching, LatestRefs, Prices, PricesInCurrencies, UserAddedChains } from '@polkadot/extension-polkagate/src/util/types'; -import type { IconTheme } from '@polkadot/react-identicon/types'; -import type { SettingsStruct } from '@polkadot/ui-settings/types'; - import { AnimatePresence } from 'framer-motion'; -import React, { useCallback, useEffect, useRef, useState } from 'react'; -import { useLocation } from 'react-router'; +import React from 'react'; -import { canDerive } from '@polkadot/extension-base/utils'; import { Loading } from '@polkadot/extension-polkagate/src/components'; -import { AccountContext, AccountIconThemeContext, AccountsAssetsContext, ActionContext, AlertContext, APIContext, AuthorizeReqContext, CurrencyContext, FetchingContext, GenesisHashOptionsContext, MediaContext, MetadataReqContext, ReferendaContext, SettingsContext, SigningReqContext, UserAddedChainContext, WorkerContext } from '@polkadot/extension-polkagate/src/components/contexts'; -import { getStorage, type LoginInfo, setStorage, updateStorage, watchStorage } from '@polkadot/extension-polkagate/src/components/Loading'; import { ExtensionLockProvider } from '@polkadot/extension-polkagate/src/context/ExtensionLockContext'; -import { useGenesisHashOptions, useNFT, usePriceIds } from '@polkadot/extension-polkagate/src/hooks'; -import useAssetsBalances, { ASSETS_NAME_IN_STORAGE, type SavedAssets } from '@polkadot/extension-polkagate/src/hooks/useAssetsBalances'; -import { isPriceUpToDate } from '@polkadot/extension-polkagate/src/hooks/usePrices'; -import { subscribeAccounts, subscribeAuthorizeRequests, subscribeMetadataRequests, subscribeSigningRequests } from '@polkadot/extension-polkagate/src/messaging'; -import { getPrices } from '@polkadot/extension-polkagate/src/util/api'; -import { buildHierarchy } from '@polkadot/extension-polkagate/src/util/buildHierarchy'; -import { DEFAULT_ACCOUNT_ICON_THEME } from '@polkadot/extension-polkagate/src/util/constants'; -import uiSettings from '@polkadot/ui-settings'; import Routes from './routes/RouteDefinitions'; - -const startSettings = uiSettings.get(); - -// Request permission for video, based on access we can hide/show import -async function requestMediaAccess (cameraOn: boolean): Promise { - if (!cameraOn) { - return false; - } - - try { - await navigator.mediaDevices.getUserMedia({ video: true }); - - return true; - } catch (error) { - console.error('Permission for video declined', (error as Error).message); - } - - return false; -} - -function initAccountContext (accounts: AccountJson[]): AccountsContext { - const hierarchy = buildHierarchy(accounts); - const master = hierarchy.find(({ isExternal, type }) => !isExternal && canDerive(type)); - - return { - accounts, - hierarchy, - master - }; -} +import { AccountAssetProvider, AccountIconThemeProvider, AccountProvider, ActionProvider, AlertProvider, ApiProvider, CurrencyProvider, FetchingProvider, GenesisHashOptionsProvider, MediaProvider, ReferendaProvider, RequestsProvider, SettingsProvider, UserAddedChainsProvider, WorkerProvider } from './contexts'; export default function Popup (): React.ReactElement { - const [accounts, setAccounts] = useState(null); - const priceIds = usePriceIds(); - const isFetchingPricesRef = useRef(false); - const genesisHashOptionsCtx = useGenesisHashOptions(); - const workerRef = useRef(undefined); - - const { pathname } = useLocation(); // also to trigger component to fix forgot pass issue - - const [accountCtx, setAccountCtx] = useState({ accounts: [], hierarchy: [] }); - const [userAddedChainCtx, setUserAddedChainCtx] = useState({}); - const [authRequests, setAuthRequests] = useState(null); - const [cameraOn, setCameraOn] = useState(startSettings.camera === 'on'); - const [mediaAllowed, setMediaAllowed] = useState(false); - const [metaRequests, setMetaRequests] = useState(null); - const [signRequests, setSignRequests] = useState(null); - const [settingsCtx, setSettingsCtx] = useState(startSettings); - const [apis, setApis] = useState({}); - const [fetching, setFetching] = useState({}); - const [refs, setRefs] = useState({}); - const [accountsAssets, setAccountsAssets] = useState(); - const [currency, setCurrency] = useState(); - const [accountIconTheme, setAccountIconTheme] = useState(DEFAULT_ACCOUNT_ICON_THEME); - const [loginInfo, setLoginInfo] = useState(); - const [alerts, setAlerts] = useState([]); - - const assetsOnChains = useAssetsBalances(accounts, setAlerts, genesisHashOptionsCtx, userAddedChainCtx, workerRef.current); - - useNFT(accounts); - - const set = useCallback((change: Fetching) => { - setFetching(change); - }, []); - - const setIt = useCallback((change: APIs) => { - setApis(change); - }, []); - - const _onAction = useCallback((to?: string): void => { - if (to) { - window.location.hash = to; - } - }, []); - - useEffect(() => { - // Use a flag to prevent race conditions - let isMounted = true; - - const handleRouting = () => { - if (!isMounted) { - return; - } - - if (!authRequests || !metaRequests || !signRequests) { - return; - } - - if (authRequests.length) { - _onAction('/authorize'); - } else if (metaRequests.length) { - _onAction('/metadata'); - } else if (signRequests.length) { - _onAction('/signing'); - } else if (['/authorize', '/metadata', '/signing'].includes(pathname)) { - _onAction('/'); - } - }; - - handleRouting(); - - return () => { - isMounted = false; - }; - }, [_onAction, authRequests, authRequests?.length, metaRequests, metaRequests?.length, pathname, signRequests, signRequests?.length]); - - useEffect(() => { - workerRef.current = new Worker(new URL('../../../extension-polkagate/src/util/workers/sharedWorker.js', import.meta.url)); - - return () => { - // Cleanup on unmount - if (workerRef.current) { - workerRef.current.terminate(); - } - }; - }, []); - - useEffect(() => { - getStorage('iconTheme') - .then((maybeTheme) => setAccountIconTheme((maybeTheme as IconTheme | undefined) || DEFAULT_ACCOUNT_ICON_THEME)) - .catch(console.error); - - const unsubscribe = watchStorage('iconTheme', setAccountIconTheme); - - return () => { - unsubscribe(); - }; - }, []); - - useEffect(() => { - assetsOnChains && setAccountsAssets({ ...assetsOnChains }); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [assetsOnChains?.timeStamp]); - - useEffect(() => { - /** remove forgotten accounts from assetChains if any */ - if (accounts && assetsOnChains?.balances) { - Object.keys(assetsOnChains.balances).forEach((_address) => { - const found = accounts.find(({ address }) => address === _address); - - if (!found) { - delete assetsOnChains.balances[_address]; - setStorage(ASSETS_NAME_IN_STORAGE, assetsOnChains, true).catch(console.error); - } - }); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [accounts?.length, assetsOnChains?.timeStamp]); - - useEffect(() => { - if (priceIds && currency?.code && !isFetchingPricesRef.current) { - isFetchingPricesRef.current = true; - - getStorage('pricesInCurrencies') - .then((res) => { - const savedPricesInCurrencies = (res || {}) as PricesInCurrencies; - const maybeSavedPriceInCurrentCurrencyCode = savedPricesInCurrencies[currency.code]; - - if (maybeSavedPriceInCurrentCurrencyCode && isPriceUpToDate(maybeSavedPriceInCurrentCurrencyCode.date)) { - /** price in the selected currency is already updated hence no need to fetch again */ - // TODO: FixMe: what if users change selected chainS during price validity period? - return; - } - - getPrices(priceIds, currency.code.toLowerCase()) - .then((newPrices) => { - delete (newPrices as Prices).currencyCode; - savedPricesInCurrencies[currency.code] = newPrices; - setStorage('pricesInCurrencies', savedPricesInCurrencies) - .catch(console.error); - }) - .catch(console.error); - }) - .catch(console.error) - .finally(() => { - isFetchingPricesRef.current = false; - }); - } - }, [currency?.code, priceIds]); - - useEffect((): void => { - Promise.all([ - subscribeAccounts(setAccounts), - subscribeAuthorizeRequests(setAuthRequests), - subscribeMetadataRequests(setMetaRequests), - subscribeSigningRequests(setSignRequests) - ]).catch(console.error); - - uiSettings.on('change', (settings): void => { - setSettingsCtx(settings); - setCameraOn(settings.camera === 'on'); - }); - - getStorage('userAddedEndpoint').then((info) => { - info && setUserAddedChainCtx(info as UserAddedChains); - }).catch(console.error); - - _onAction(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - useEffect(() => { - const fetchLoginInfo = async () => { - chrome.storage.onChanged.addListener(function (changes, areaName) { - if (areaName === 'local' && 'loginInfo' in changes) { - const newValue = changes['loginInfo'].newValue as LoginInfo; - - setLoginInfo(newValue); - } - }); - const info = await getStorage('loginInfo') as LoginInfo; - - setLoginInfo(info); - }; - - fetchLoginInfo().catch(console.error); - }, []); - - useEffect((): void => { - if (!loginInfo) { - return; - } - - if (loginInfo.status !== 'forgot') { - setAccountCtx(initAccountContext(accounts || [])); - } else if (loginInfo.status === 'forgot') { - setAccountCtx(initAccountContext([])); - const addresses = accounts?.map((account) => account.address); - - updateStorage('loginInfo', { addressesToForget: addresses }).catch(console.error); - } - }, [accounts, loginInfo]); - - useEffect((): void => { - requestMediaAccess(cameraOn) - .then(setMediaAllowed) - .catch(console.error); - }, [cameraOn]); - return ( - { - accounts && authRequests && metaRequests && signRequests && - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - } + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/extension-ui/src/Popup/routes/RouteDefinitions.tsx b/packages/extension-ui/src/Popup/routes/RouteDefinitions.tsx index 6a8e7ddb4..a84efa935 100644 --- a/packages/extension-ui/src/Popup/routes/RouteDefinitions.tsx +++ b/packages/extension-ui/src/Popup/routes/RouteDefinitions.tsx @@ -430,7 +430,7 @@ const ALL_ROUTES: RouteConfig[] = [ ...ROOT_ROUTES ]; -const Routes = () => { +export default function Routes () { const routeComponents = useMemo(() => ALL_ROUTES.map(({ Component, exact, path, props, trigger }) => ( { {routeComponents} ); -}; - -export default React.memo(Routes); +} diff --git a/packages/extension-ui/tsconfig.build.json b/packages/extension-ui/tsconfig.build.json index 1a4bbc9cf..7a694c712 100644 --- a/packages/extension-ui/tsconfig.build.json +++ b/packages/extension-ui/tsconfig.build.json @@ -18,6 +18,8 @@ { "path": "../extension-inject/tsconfig.build.json" }, { "path": "../extension-mocks/tsconfig.build.json" + }, + { "path": "../extension-polkagate/tsconfig.build.json" } ] }