diff --git a/features/home/one-inch-info/one-inch-info.tsx b/features/home/one-inch-info/one-inch-info.tsx index 46394e854..5fa307578 100644 --- a/features/home/one-inch-info/one-inch-info.tsx +++ b/features/home/one-inch-info/one-inch-info.tsx @@ -24,7 +24,7 @@ const ONE_INCH_RATE_LIMIT = 1.004; export const OneInchInfo: FC = () => { const linkProps = use1inchLinkProps(); - const apiOneInchRatePath = 'api/oneinch-rate?token=eth'; + const apiOneInchRatePath = 'api/oneinch-rate/?token=eth'; const { data, initialLoading } = useLidoSWR<{ rate: number }>( dynamics.ipfsMode ? `${dynamics.widgetApiBasePathForIpfs}/${apiOneInchRatePath}` diff --git a/features/ipfs/csp-violation-box/csp-violation-box.tsx b/features/ipfs/csp-violation-box/csp-violation-box.tsx new file mode 100644 index 000000000..e98c6a11d --- /dev/null +++ b/features/ipfs/csp-violation-box/csp-violation-box.tsx @@ -0,0 +1,31 @@ +import { Wrap, InfoLink, Text } from './styles'; + +const GATEWAY_CHECKER_URL = 'https://ipfs.github.io/public-gateway-checker/'; +const DESKTOP_APP_URL = 'https://github.com/ipfs/ipfs-desktop#ipfs-desktop'; + +export const CSPViolationBox = () => { + return ( + + + Insufficient Gateway CSP + + + RPC requests are blocked by Content Security Policy set by IPFS Gateway + service you are using. + + + Possible actions: + + + Use another gateway service: +
+ Public Gateway Checker +
+ + Or install your IPFS Gateway: +
+ IPFS Desktop +
+
+ ); +}; diff --git a/features/ipfs/csp-violation-box/index.tsx b/features/ipfs/csp-violation-box/index.tsx new file mode 100644 index 000000000..3070ef119 --- /dev/null +++ b/features/ipfs/csp-violation-box/index.tsx @@ -0,0 +1 @@ +export * from './csp-violation-box'; diff --git a/features/ipfs/csp-violation-box/styles.tsx b/features/ipfs/csp-violation-box/styles.tsx new file mode 100644 index 000000000..a74907368 --- /dev/null +++ b/features/ipfs/csp-violation-box/styles.tsx @@ -0,0 +1,35 @@ +import { ComponentProps, FC } from 'react'; +import styled from 'styled-components'; +import { Text as TextOriginal, themeDefault } from '@lidofinance/lido-ui'; +import { LinkArrow } from 'shared/components/link-arrow/link-arrow'; + +type TextProps = Omit, 'color'> & { + color?: keyof typeof themeDefault.colors; +}; +export const Text: FC = styled(TextOriginal)` + color: ${({ color }) => `var(--lido-color-${color})`}; +`; + +export const Wrap = styled.div` + position: relative; + display: flex; + flex-direction: column; + gap: 10px; + + border-radius: ${({ theme }) => theme.borderRadiusesMap.sm}px; + padding: ${({ theme }) => theme.spaceMap.md}px + ${({ theme }) => theme.spaceMap.md}px; + + color: var(--lido-color-accentContrast); + background-color: var(--lido-color-error); +`; + +export const InfoLink = styled(LinkArrow)` + font-weight: 700; + + &, + &:visited, + &:hover { + color: var(--lido-color-accentContrast); + } +`; diff --git a/features/ipfs/csp-violation-box/use-csp-violation.ts b/features/ipfs/csp-violation-box/use-csp-violation.ts new file mode 100644 index 000000000..bed757267 --- /dev/null +++ b/features/ipfs/csp-violation-box/use-csp-violation.ts @@ -0,0 +1,20 @@ +import { useEffect, useState } from 'react'; + +export const useCSPViolation = () => { + const [isCSPViolated, setCSPViolated] = useState(false); + + useEffect(() => { + const handler = () => { + setCSPViolated(true); + }; + document.addEventListener('securitypolicyviolation', handler); + + return () => { + document.removeEventListener('securitypolicyviolation', handler); + }; + }, []); + + return { + isCSPViolated, + }; +}; diff --git a/features/ipfs/ipfs-info-box/ipfs-info-box.tsx b/features/ipfs/ipfs-info-box/ipfs-info-box.tsx index 695c7adf9..3abc57e66 100644 --- a/features/ipfs/ipfs-info-box/ipfs-info-box.tsx +++ b/features/ipfs/ipfs-info-box/ipfs-info-box.tsx @@ -1,102 +1,15 @@ -import { useCallback } from 'react'; -import { useLidoSWR, useLocalStorage, useSDK } from '@lido-sdk/react'; +import { useIPFSInfoBoxStatuses } from 'providers/ipfs-info-box-statuses'; -import { useRpcUrl } from 'config/rpc'; -import { SETTINGS_PATH } from 'config/urls'; -import { usePrefixedPush } from 'shared/hooks/use-prefixed-history'; -import { useRouterPath } from 'shared/hooks/use-router-path'; -import { LinkArrow } from 'shared/components/link-arrow/link-arrow'; - -import { Check, Close } from '@lidofinance/lido-ui'; -import { Wrap, RpcStatusBox, Button, Text } from './styles'; - -import { checkRpcUrl } from 'utils/check-rpc-url'; -import { STORAGE_IPFS_INFO_DISMISS } from 'config/storage'; - -const IPFS_INFO_URL = 'https://docs.ipfs.tech/concepts/what-is-ipfs/'; +import { CSPViolationBox } from '../csp-violation-box'; +import { RPCAvailabilityCheckResultBox } from '../rpc-availability-check-result-box'; export const IPFSInfoBox = () => { - const { chainId } = useSDK(); - const push = usePrefixedPush(); - const [isDismissed, setDismissStorage] = useLocalStorage( - STORAGE_IPFS_INFO_DISMISS, - false, - ); - - const rpcUrl = useRpcUrl(); - const { data: rpcCheckResult, initialLoading: isLoading } = useLidoSWR( - `rpc-url-check-${rpcUrl}-${chainId}`, - async () => { - return await checkRpcUrl(rpcUrl, chainId); - }, - ); - - const handleClickDismiss = useCallback(() => { - setDismissStorage(true); - }, [setDismissStorage]); - - const handleClickSettings = useCallback(() => { - void push(SETTINGS_PATH); - setDismissStorage(true); - }, [push, setDismissStorage]); + const { isCSPViolated, isShownTheRPCNotAvailableBox } = + useIPFSInfoBoxStatuses(); - const pathname = useRouterPath(); - const isSettingsPage = pathname === SETTINGS_PATH; + if (isCSPViolated) return ; - if ((isDismissed && rpcCheckResult === true) || isLoading || isSettingsPage) { - return null; - } + if (isShownTheRPCNotAvailableBox) return ; - return ( - - - You are currently using the IPFS widget's version. - - IPFS - {rpcCheckResult === true && ( - <> - - - The pre-installed RPC node URL is now functioning correctly. - - - - - However, you can visit the settings if you wish to customize your - own RPC node URL. - - - - )} - {rpcCheckResult !== true && ( - <> - - - The pre-installed RPC node URL is not now functioning. - - - - - You should visit the settings page and specify your own RPC node - URL. - - - - )} - - ); + return null; }; diff --git a/features/ipfs/rpc-availability-check-result-box/index.tsx b/features/ipfs/rpc-availability-check-result-box/index.tsx new file mode 100644 index 000000000..b2a30bb28 --- /dev/null +++ b/features/ipfs/rpc-availability-check-result-box/index.tsx @@ -0,0 +1 @@ +export * from './rpc-availability-check-result-box'; diff --git a/features/ipfs/rpc-availability-check-result-box/rpc-availability-check-result-box.tsx b/features/ipfs/rpc-availability-check-result-box/rpc-availability-check-result-box.tsx new file mode 100644 index 000000000..fa2488ac1 --- /dev/null +++ b/features/ipfs/rpc-availability-check-result-box/rpc-availability-check-result-box.tsx @@ -0,0 +1,75 @@ +import { useCallback } from 'react'; +import { Check, Close } from '@lidofinance/lido-ui'; + +import { SETTINGS_PATH } from 'config/urls'; +import { useIPFSInfoBoxStatuses } from 'providers/ipfs-info-box-statuses'; +import { usePrefixedPush } from 'shared/hooks/use-prefixed-history'; +import { LinkArrow } from 'shared/components/link-arrow/link-arrow'; + +import { Wrap, RpcStatusBox, Button, Text } from './styles'; + +const IPFS_INFO_URL = 'https://docs.ipfs.tech/concepts/what-is-ipfs/'; + +export const RPCAvailabilityCheckResultBox = () => { + const { isRPCAvailable, handleClickDismiss } = useIPFSInfoBoxStatuses(); + + const push = usePrefixedPush(); + + const handleClickSettings = useCallback(() => { + void push(SETTINGS_PATH); + handleClickDismiss(); + }, [push, handleClickDismiss]); + + return ( + + + You are currently using the IPFS widget's version. + + IPFS + {isRPCAvailable && ( + <> + + + The pre-installed RPC node URL is now functioning correctly. + + + + + However, you can visit the settings if you wish to customize your + own RPC node URL. + + + + )} + {!isRPCAvailable && ( + <> + + + The pre-installed RPC node URL is not now functioning. + + + + + You should visit the settings page and specify your own RPC node + URL. + + + + )} + + ); +}; diff --git a/features/ipfs/ipfs-info-box/styles.tsx b/features/ipfs/rpc-availability-check-result-box/styles.tsx similarity index 100% rename from features/ipfs/ipfs-info-box/styles.tsx rename to features/ipfs/rpc-availability-check-result-box/styles.tsx diff --git a/features/rewards/components/rewardsListContent/RewardsListContent.tsx b/features/rewards/components/rewardsListContent/RewardsListContent.tsx index 28782a615..a940cc100 100644 --- a/features/rewards/components/rewardsListContent/RewardsListContent.tsx +++ b/features/rewards/components/rewardsListContent/RewardsListContent.tsx @@ -5,7 +5,11 @@ import { ErrorBlockNoSteth } from 'features/rewards/components/errorBlocks/Error import { RewardsListsEmpty } from './RewardsListsEmpty'; import { RewardsListErrorMessage } from './RewardsListErrorMessage'; -import { LoaderWrapper, TableWrapperStyle } from './RewardsListContentStyles'; +import { + LoaderWrapper, + TableWrapperStyle, + ErrorWrapper, +} from './RewardsListContentStyles'; import { RewardsTable } from 'features/rewards/components/rewardsTable'; export const RewardsListContent: FC = () => { @@ -31,7 +35,13 @@ export const RewardsListContent: FC = () => { ); } - if (error) return ; + if (error) { + return ( + + + + ); + } if (data && data.events.length === 0) return ; return ( diff --git a/features/rewards/components/rewardsListContent/RewardsListContentStyles.ts b/features/rewards/components/rewardsListContent/RewardsListContentStyles.ts index 4ca31d086..292f7f3ca 100644 --- a/features/rewards/components/rewardsListContent/RewardsListContentStyles.ts +++ b/features/rewards/components/rewardsListContent/RewardsListContentStyles.ts @@ -8,3 +8,7 @@ export const LoaderWrapper = styled.div` export const TableWrapperStyle = styled.div` margin-top: 20px; `; + +export const ErrorWrapper = styled.div` + word-wrap: break-word; +`; diff --git a/features/rewards/components/rewardsListHeader/styles.ts b/features/rewards/components/rewardsListHeader/styles.ts index c58a4a178..98190d54b 100644 --- a/features/rewards/components/rewardsListHeader/styles.ts +++ b/features/rewards/components/rewardsListHeader/styles.ts @@ -10,7 +10,7 @@ export const RewardsListHeaderStyle = styled.div` color: ${({ theme }) => theme.colors.secondary}; - ${({ theme }) => theme.mediaQueries.md} { + ${({ theme }) => theme.mediaQueries.lg} { flex-direction: column; height: auto; align-items: initial; @@ -28,6 +28,7 @@ export const LeftOptionsWrapper = styled.div` flex-wrap: wrap; margin-right: auto; gap: 16px; + ${({ theme }) => theme.mediaQueries.lg} { order: 3; margin-right: 0; @@ -36,6 +37,10 @@ export const LeftOptionsWrapper = styled.div` flex: 1 0; } } + + ${({ theme }) => theme.mediaQueries.md} { + flex-direction: column; + } `; export const RightOptionsWrapper = styled.div` diff --git a/features/rewards/fetchers/requesters/json/backend.ts b/features/rewards/fetchers/requesters/json/backend.ts index b27738277..202a32336 100644 --- a/features/rewards/fetchers/requesters/json/backend.ts +++ b/features/rewards/fetchers/requesters/json/backend.ts @@ -1,3 +1,5 @@ +import { dynamics } from 'config'; + export type BackendQuery = { address: string; currency?: string; @@ -12,7 +14,11 @@ export const backendRequest = async (query: BackendQuery) => { Object.entries(query).forEach(([k, v]) => params.append(k, v.toString())); - const requested = await fetch(`/api/rewards?${params.toString()}`); + const apiRewardsPath = `/api/rewards/?${params.toString()}`; + const apiRewardsUrl = dynamics.ipfsMode + ? `${dynamics.widgetApiBasePathForIpfs}${apiRewardsPath}` + : apiRewardsPath; + const requested = await fetch(apiRewardsUrl); if (!requested.ok) { const responded = await requested.json(); diff --git a/features/rewards/hooks/useRewardsDataLoad.ts b/features/rewards/hooks/useRewardsDataLoad.ts index 6f41da2c4..d07e62008 100644 --- a/features/rewards/hooks/useRewardsDataLoad.ts +++ b/features/rewards/hooks/useRewardsDataLoad.ts @@ -1,5 +1,6 @@ -import { Backend } from 'features/rewards/types'; import { useEffect, useRef } from 'react'; +import { Backend } from 'features/rewards/types'; +import { dynamics } from 'config'; import { useLidoSWR } from 'shared/hooks'; import { swrAbortableMiddleware } from 'utils'; @@ -43,8 +44,14 @@ export const useRewardsDataLoad: UseRewardsDataLoad = (props) => { Object.entries(requestOptions).forEach(([k, v]) => params.append(k, v.toString()), ); + + const apiRewardsPath = `/api/rewards/?${params.toString()}`; + const apiRewardsUrl = dynamics.ipfsMode + ? `${dynamics.widgetApiBasePathForIpfs}${apiRewardsPath}` + : apiRewardsPath; + const { data, ...rest } = useLidoSWR( - address ? `/api/rewards?${params.toString()}` : null, + address ? apiRewardsUrl : null, { shouldRetryOnError: false, revalidateOnFocus: false, diff --git a/features/withdrawals/hooks/useWithdrawalRates.ts b/features/withdrawals/hooks/useWithdrawalRates.ts index 7b3b635ee..9282ae24d 100644 --- a/features/withdrawals/hooks/useWithdrawalRates.ts +++ b/features/withdrawals/hooks/useWithdrawalRates.ts @@ -60,7 +60,7 @@ const getOneInchRate: GetRateType = async (amount, token) => { }; } - const apiOneInchRatePath = `api/oneinch-rate?token=${token}`; + const apiOneInchRatePath = `api/oneinch-rate/?token=${token}`; const respData = await standardFetcher<{ rate: string }>( dynamics.ipfsMode ? `${dynamics.widgetApiBasePathForIpfs}/${apiOneInchRatePath}` diff --git a/features/withdrawals/request/form/options/dex-options.tsx b/features/withdrawals/request/form/options/dex-options.tsx index 70cfd35aa..cc9cfecd3 100644 --- a/features/withdrawals/request/form/options/dex-options.tsx +++ b/features/withdrawals/request/form/options/dex-options.tsx @@ -99,7 +99,7 @@ const DexOption: React.FC = ({ Go to {title} - {loading && } + {loading && !toReceive && } {toReceive ? ( ) : data?.map(({ name, toReceive, rate }) => { const dex = dexInfo[name]; - if (!dex) return null; + if (!dex || (amount.gt('0') && !rate)) return null; return ( = ({ children }) => ( - {children} + + {children} + diff --git a/providers/ipfs-info-box-statuses.tsx b/providers/ipfs-info-box-statuses.tsx new file mode 100644 index 000000000..59fd51260 --- /dev/null +++ b/providers/ipfs-info-box-statuses.tsx @@ -0,0 +1,93 @@ +import { + FC, + PropsWithChildren, + createContext, + useCallback, + useContext, + useMemo, +} from 'react'; +import { useLidoSWR, useLocalStorage, useSDK } from '@lido-sdk/react'; +import invariant from 'tiny-invariant'; + +import { useCSPViolation } from 'features/ipfs/csp-violation-box/use-csp-violation'; +import { useRpcUrl } from 'config/rpc'; +import { STORAGE_IPFS_INFO_DISMISS } from 'config/storage'; +import { SETTINGS_PATH } from 'config/urls'; + +import { useRouterPath } from 'shared/hooks/use-router-path'; + +import { STRATEGY_LAZY } from 'utils/swrStrategies'; +import { checkRpcUrl } from 'utils/check-rpc-url'; + +type IPFSInfoBoxStatusesContextValue = { + isCSPViolated: boolean; + isShownTheRPCNotAvailableBox: boolean; + isRPCAvailable: boolean; + handleClickDismiss: () => void; +}; + +const IPFSInfoBoxStatusContext = + createContext(null); + +export const useIPFSInfoBoxStatuses = () => { + const value = useContext(IPFSInfoBoxStatusContext); + invariant(value, 'useIPFSInfoBoxStatuses was called outside the provider'); + return value; +}; + +export const IPFSInfoBoxStatusesProvider: FC = ({ + children, +}) => { + const { chainId } = useSDK(); + + // CSP violation box + const { isCSPViolated } = useCSPViolation(); + + // RPC availability check result box + const [isDismissed, setDismissStorage] = useLocalStorage( + STORAGE_IPFS_INFO_DISMISS, + false, + ); + + const handleClickDismiss = useCallback(() => { + setDismissStorage(true); + }, [setDismissStorage]); + + const rpcUrl = useRpcUrl(); + const { data: isRPCAvailableRaw, initialLoading: isLoading } = useLidoSWR( + `rpc-url-check-${rpcUrl}-${chainId}`, + async () => await checkRpcUrl(rpcUrl, chainId), + STRATEGY_LAZY, + ); + const isRPCAvailable = isRPCAvailableRaw === true; + + const pathname = useRouterPath(); + const isSettingsPage = pathname === SETTINGS_PATH; + + const isShownTheRPCNotAvailableBox = + (!isDismissed || !isRPCAvailable) && + !isLoading && + !isSettingsPage && + !isCSPViolated; + + const contextValue = useMemo( + () => ({ + isCSPViolated, + isShownTheRPCNotAvailableBox, + isRPCAvailable, + handleClickDismiss, + }), + [ + handleClickDismiss, + isCSPViolated, + isRPCAvailable, + isShownTheRPCNotAvailableBox, + ], + ); + + return ( + + {children} + + ); +}; diff --git a/shared/components/header/components/header-wallet.tsx b/shared/components/header/components/header-wallet.tsx index 2745b210a..86ade2496 100644 --- a/shared/components/header/components/header-wallet.tsx +++ b/shared/components/header/components/header-wallet.tsx @@ -10,7 +10,11 @@ import { Button, Connect } from 'shared/wallet'; import NoSSRWrapper from 'shared/components/no-ssr-wrapper'; import { HeaderSettingsButton } from './header-settings-button'; -import { HeaderWalletChainStyle, DotStyle, IPFSInfoBoxWrap } from '../styles'; +import { + HeaderWalletChainStyle, + DotStyle, + IPFSInfoBoxWrapper, +} from '../styles'; const HeaderWallet: FC = () => { const { active } = useWeb3(); @@ -38,9 +42,9 @@ const HeaderWallet: FC = () => { {dynamics.ipfsMode && } {dynamics.ipfsMode && ( - + - + )} ); diff --git a/shared/components/header/styles.tsx b/shared/components/header/styles.tsx index 35c2acbe9..637154fa5 100644 --- a/shared/components/header/styles.tsx +++ b/shared/components/header/styles.tsx @@ -39,7 +39,7 @@ export const DotStyle = styled.p` margin-right: 6px; `; -export const IPFSInfoBoxWrap = styled.div` +export const IPFSInfoBoxWrapper = styled.div` position: absolute; right: 0; top: calc(100% + 15px); diff --git a/shared/hooks/useLidoStats.ts b/shared/hooks/useLidoStats.ts index 1fd7b3095..2f23daedc 100644 --- a/shared/hooks/useLidoStats.ts +++ b/shared/hooks/useLidoStats.ts @@ -21,7 +21,7 @@ export const useLidoStats = (): { initialLoading: boolean; } => { const { chainId } = useSDK(); - const apiShortLidoStatsPath = `api/short-lido-stats?chainId=${chainId}`; + const apiShortLidoStatsPath = `api/short-lido-stats/?chainId=${chainId}`; const lidoStats = useLidoSWR( dynamics.ipfsMode ? `${dynamics.widgetApiBasePathForIpfs}/${apiShortLidoStatsPath}`