diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index efbd4e0875..81a9fedccc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,6 +13,8 @@ env: REACT_APP_GOOGLE_ANALYTICS_ID: ${{ secrets.REACT_APP_GOOGLE_ANALYTICS_ID }} REACT_APP_BLOCKNATIVE_API_KEY: ${{ secrets.REACT_APP_BLOCKNATIVE_API_KEY }} REACT_APP_BFF_BASE_URL: ${{ secrets.BFF_BASE_URL }} + REACT_APP_CMS_BASE_URL: ${{ secrets.CMS_BASE_URL }} + NEXT_PUBLIC_CMS_BASE_URL: ${{ secrets.CMS_BASE_URL }} jobs: setup: diff --git a/.github/workflows/ipfs.yml b/.github/workflows/ipfs.yml index dd156fef74..62098190c3 100644 --- a/.github/workflows/ipfs.yml +++ b/.github/workflows/ipfs.yml @@ -14,6 +14,8 @@ env: IPFS_DEPLOY_PINATA__API_KEY: ${{ secrets.REACT_APP_PINATA_API_KEY }} IPFS_DEPLOY_PINATA__SECRET_API_KEY: ${{ secrets.REACT_APP_PINATA_SECRET_API_KEY }} REACT_APP_BFF_BASE_URL: ${{ secrets.BFF_BASE_URL }} + REACT_APP_CMS_BASE_URL: ${{ secrets.CMS_BASE_URL }} + NEXT_PUBLIC_CMS_BASE_URL: ${{ secrets.CMS_BASE_URL }} NODE_VERSION: lts/hydrogen jobs: diff --git a/.github/workflows/vercel.yml b/.github/workflows/vercel.yml index 8e2dedc368..8bc79003ae 100644 --- a/.github/workflows/vercel.yml +++ b/.github/workflows/vercel.yml @@ -55,6 +55,8 @@ jobs: REACT_APP_SUBGRAPH_URL_ARBITRUM_ONE=${{ secrets.REACT_APP_SUBGRAPH_URL_ARBITRUM_ONE }} REACT_APP_SUBGRAPH_URL_GNOSIS_CHAIN=${{ secrets.REACT_APP_SUBGRAPH_URL_GNOSIS_CHAIN }} REACT_APP_BFF_BASE_URL=${{ secrets.BFF_BASE_URL }} + REACT_APP_CMS_BASE_URL=${{ secrets.CMS_BASE_URL }} + NEXT_PUBLIC_CMS_BASE_URL=${{ secrets.CMS_BASE_URL }} vercel build -t ${{ secrets.VERCEL_TOKEN }} --prod - name: Get the version diff --git a/README.md b/README.md index d927afba70..9ed4392d7e 100644 --- a/README.md +++ b/README.md @@ -216,6 +216,29 @@ The API endpoint is configured using the environment variable REACT_APP_BFF_BASE_URL=https://bff.cow.fi ``` + +## CMS API Endpoints (Content Management System) + +The CMS API is a helper API that provides some additional content to the frontend. + +It is not considered a required API for CoW Swap core functionality, the +app will still allow the user to place orders and will have some fallback logic +in case this API is not available. + +The reference implementation of the API is +[CMS API](https://github.com/cowprotocol/cms). + +The API endpoint is configured using the environment variable +`REACT_APP_CMS_BASE_URL`: + +```ini +REACT_APP_CMS_BASE_URL=https://cms.cow.fi/api +``` + + + + + ## Price feeds CoW Swap tries to find the best price available on-chain using some price feeds. diff --git a/apps/cow-fi/components/AddRpcButton/index.tsx b/apps/cow-fi/components/AddRpcButton/index.tsx index c3880b639e..b277bfe854 100644 --- a/apps/cow-fi/components/AddRpcButton/index.tsx +++ b/apps/cow-fi/components/AddRpcButton/index.tsx @@ -2,6 +2,8 @@ import { Confetti } from '@cowprotocol/ui' import styled from 'styled-components/macro' import { darken, transparentize } from 'polished' import { useConnectAndAddToWallet } from '../../lib/hooks/useConnectAndAddToWallet' +import { clickOnMevBlocker } from 'modules/analytics' +import { useAccount } from 'wagmi' import { Link, LinkType } from '@/components/Link' @@ -25,14 +27,38 @@ const Message = styled.p<{ state: AddToWalletStateValues }>` ` export function AddRpcButton() { - const { addWalletState, connectAndAddToWallet } = useConnectAndAddToWallet() + const { addWalletState, connectAndAddToWallet, disconnectWallet } = useConnectAndAddToWallet() const { errorMessage, state } = addWalletState + const { isConnected } = useAccount() + + const handleClick = async () => { + clickOnMevBlocker('click-add-rpc-to-wallet') + try { + if (connectAndAddToWallet) { + // Start the connection process + const connectionPromise = connectAndAddToWallet() + + // Wait for the connection process to complete + await connectionPromise + } else { + throw new Error('connectAndAddToWallet is not defined') + } + } catch (error) { + clickOnMevBlocker('click-add-rpc-to-wallet-error') + } + } // Get the label and enable state of button const isAdding = state === 'adding' const isConnecting = state === 'connecting' const disabledButton = isConnecting || isAdding || !connectAndAddToWallet - const buttonLabel = isConnecting ? 'Connecting Wallet...' : isAdding ? 'Adding to Wallet...' : 'Get protected' + const buttonLabel = isConnecting + ? 'Connecting Wallet...' + : isAdding + ? 'Adding to Wallet...' + : isConnected + ? 'Add MEV Blocker RPC' + : 'Get protected' return ( <> @@ -48,13 +74,25 @@ export function AddRpcButton() { fontSize={21} color={'#FEE7CF'} bgColor="#EC4612" - onClick={connectAndAddToWallet || (() => {})} + onClick={handleClick} disabled={disabledButton} asButton > {buttonLabel} {errorMessage && {errorMessage}} + {disconnectWallet && ( + + Disconnect + + )} )} diff --git a/apps/cow-fi/const/cms.ts b/apps/cow-fi/const/cms.ts new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/apps/cow-fi/const/cms.ts @@ -0,0 +1 @@ + diff --git a/apps/cow-fi/lib/hooks/useConnect.ts b/apps/cow-fi/lib/hooks/useConnect.ts index 56281e276d..4057ea26e4 100644 --- a/apps/cow-fi/lib/hooks/useConnect.ts +++ b/apps/cow-fi/lib/hooks/useConnect.ts @@ -1,46 +1,51 @@ import { useAccount, useConnect as useConnectWagmi } from 'wagmi' -import { useCallback, useMemo } from 'react' +import { useCallback, useEffect, useState } from 'react' import { useConnectModal } from '@rainbow-me/rainbowkit' import { ConnectResult, PublicClient } from '@wagmi/core' +import { clickOnMevBlocker } from '../../modules/analytics' export function useConnect() { const { isConnected } = useAccount() const { openConnectModal } = useConnectModal() const { connectAsync, connectors } = useConnectWagmi() + const [connectionPromise, setConnectionPromise] = useState | undefined> | null>( + null + ) - const [injectedConnector, hasInjectedProviderPromise] = useMemo(() => { - const connector = connectors.find((c) => c.id === 'injected') + const injectedConnector = connectors.find((c) => c.id === 'injected') - if (!connector || typeof connector.getProvider !== 'function') { - return [undefined, Promise.resolve(false)] as const + useEffect(() => { + if (isConnected && connectionPromise) { + clickOnMevBlocker('wallet-connected') + setConnectionPromise(null) } + }, [isConnected, connectionPromise]) - return [connector, connector.getProvider().then((p) => !!p)] as const - }, [connectors]) + const connect = useCallback((): Promise | undefined> => { + console.debug('[useConnect] Initiating connection') - const connect = useCallback(async (): Promise | undefined> => { - const hasInjectedProvider = await hasInjectedProviderPromise - - // Shows connect modal if there's no injected wallet - if (!hasInjectedProvider || !injectedConnector) { - console.debug('[useConnect] No injected connector or provider. Using connect modal') + const promise = new Promise | undefined>((resolve, reject) => { if (openConnectModal) { + console.debug('[useConnect] Showing connect modal') openConnectModal() } - return undefined - } - if (!connectAsync) { - console.debug('[useConnect] connectAsync is undefined') - return undefined - } - - // Connects with injected wallet (if available) - console.debug('[useConnect] Connect using injected wallet') - return connectAsync({ - connector: injectedConnector, + const checkConnection = setInterval(async () => { + if (isConnected) { + clearInterval(checkConnection) + try { + const result = await connectAsync({ connector: injectedConnector }) + resolve(result) + } catch (error) { + reject(error) + } + } + }, 500) }) - }, [connectAsync, injectedConnector, hasInjectedProviderPromise, openConnectModal]) + + setConnectionPromise(promise) + return promise + }, [connectAsync, injectedConnector, openConnectModal, isConnected]) return { isConnected, diff --git a/apps/cow-fi/lib/hooks/useConnectAndAddToWallet.ts b/apps/cow-fi/lib/hooks/useConnectAndAddToWallet.ts index fd8d98af5c..d914f5d0b3 100644 --- a/apps/cow-fi/lib/hooks/useConnectAndAddToWallet.ts +++ b/apps/cow-fi/lib/hooks/useConnectAndAddToWallet.ts @@ -1,38 +1,39 @@ -import { useCallback, useEffect, useState } from 'react' +import { useCallback, useState } from 'react' import { useConnect } from './useConnect' -import { useWalletClient } from 'wagmi' +import { useDisconnect, useWalletClient } from 'wagmi' import { handleRpcError } from '@/util/handleRpcError' import { useAddRpcWithTimeout } from './useAddRpcWithTimeout' import { AddToWalletState, AddToWalletStateValues } from '@/components/AddRpcButton' +import { clickOnMevBlocker } from '../../modules/analytics' const DEFAULT_STATE: AddToWalletState = { state: 'unknown', autoConnect: false } const ADDING_STATE: AddToWalletState = { state: 'adding', autoConnect: false } const ADDED_STATE: AddToWalletState = { state: 'added', autoConnect: false } -export interface UseConnectAndAddToWalletPros { +export interface UseConnectAndAddToWalletProps { addWalletState: AddToWalletState - connectAndAddToWallet: (() => void) | null + connectAndAddToWallet: (() => Promise) | null + disconnectWallet: (() => void) | null } -export function useConnectAndAddToWallet(): UseConnectAndAddToWalletPros { +export function useConnectAndAddToWallet(): UseConnectAndAddToWalletProps { const { isConnected, connect } = useConnect() + const { disconnect } = useDisconnect() const { data: walletClient } = useWalletClient() const [addWalletState, setState] = useState(DEFAULT_STATE) const [addingPromise, setAddRpcPromise] = useState | null>(null) - const { state, autoConnect } = addWalletState - // Handle RPC errors const handleError = useCallback( (error: unknown) => { console.error(`[connectAndAddToWallet] handleError`, error) - const { errorMessage: message, isError, isUserRejection } = handleRpcError(error) - - if (isError || isUserRejection) { - // Display the error + if (isUserRejection) { + clickOnMevBlocker('click-add-rpc-to-wallet-user-rejected') + setState({ state: 'unknown', errorMessage: 'User rejected the request', autoConnect: false }) + } else if (isError) { + clickOnMevBlocker('click-add-rpc-to-wallet-error') setState({ state: 'error', errorMessage: message || undefined, autoConnect: false }) } else { - // Not an error: i.e The user is connecting setState(DEFAULT_STATE) } setAddRpcPromise(null) @@ -40,62 +41,78 @@ export function useConnectAndAddToWallet(): UseConnectAndAddToWalletPros { [setState] ) - // Add RPC endpoint to wallet (with analytics + handle timeout state) const addToWallet = useAddRpcWithTimeout({ - isAdding: state === 'adding', + isAdding: addWalletState.state === 'adding', addingPromise, onAdding(newAddRpcPromise) { console.debug('[connectAndAddToWallet] Adding RPC...') + clickOnMevBlocker('click-add-rpc-to-wallet-adding') setAddRpcPromise(newAddRpcPromise) setState(ADDING_STATE) }, onAdded() { console.debug('[connectAndAddToWallet] 🎉 RPC has been added!') + clickOnMevBlocker('click-add-rpc-to-wallet-added-success') setState(ADDED_STATE) setAddRpcPromise(null) }, onTimeout(errorMessage: string, newState: AddToWalletStateValues) { console.debug(`[connectAndAddToWallet] New State: ${newState}. Message`, errorMessage) + clickOnMevBlocker('click-add-rpc-to-wallet-timeout') setState({ state: newState, errorMessage: errorMessage || undefined, - autoConnect, + autoConnect: addWalletState.autoConnect, }) }, walletClient: walletClient ?? undefined, handleError, }) - // Connect and auto-add the RPC endpoint - const allowToConnectAndAddToWallet = !isConnected || walletClient // allow to connectAndAddToWallet if not connected, or if the client is ready - const connectAndAddToWallet = useCallback(() => { - if (!allowToConnectAndAddToWallet) { - return + const connectAndAddToWallet = useCallback((): Promise => { + if (!walletClient && isConnected) { + return Promise.reject(new Error('Connection not allowed')) } - if (!isConnected) { - console.debug('[useConnectAndAddToWallet] Connecting...') - connect() - .then(() => { - console.debug('[useConnectAndAddToWallet] 🔌 Connected!') - addToWallet() - }) - .catch(handleError) - } else { - console.debug('[useConnectAndAddToWallet] Already connected. Adding RPC endpoint...') - addToWallet() - } - }, [allowToConnectAndAddToWallet, isConnected, handleError, connect, addToWallet]) + return new Promise((resolve, reject) => { + if (!isConnected) { + console.debug('[useConnectAndAddToWallet] Connecting...') + clickOnMevBlocker('click-add-rpc-to-wallet-connecting') + connect() + .then((result) => { + if (result) { + console.debug('[useConnectAndAddToWallet] 🔌 Connected!') + clickOnMevBlocker('click-add-rpc-to-wallet-connected') + addToWallet() + resolve() + } else { + console.debug('[useConnectAndAddToWallet] Connection process incomplete') + setState(DEFAULT_STATE) + resolve() + } + }) + .catch((error: unknown) => { + handleError(error) + reject(error) + }) + } else { + console.debug('[useConnectAndAddToWallet] Already connected. Adding RPC endpoint...') + clickOnMevBlocker('click-add-rpc-to-wallet-connected') + addToWallet() + resolve() + } + }) + }, [isConnected, connect, addToWallet, handleError, walletClient]) - // Auto-connect (once we have ) - useEffect(() => { - if (isConnected && walletClient && autoConnect) { - addToWallet() - } - }, [isConnected, walletClient, autoConnect, addToWallet]) + const disconnectWallet = useCallback(() => { + clickOnMevBlocker('click-disconnect-wallet') + disconnect() + setState(DEFAULT_STATE) + }, [disconnect]) return { - connectAndAddToWallet: allowToConnectAndAddToWallet ? connectAndAddToWallet : null, + connectAndAddToWallet: walletClient || !isConnected ? connectAndAddToWallet : null, + disconnectWallet: isConnected ? disconnectWallet : null, addWalletState, } } diff --git a/apps/cow-fi/pages/learn/[article].tsx b/apps/cow-fi/pages/learn/[article].tsx index 474855b5ff..0576739d20 100644 --- a/apps/cow-fi/pages/learn/[article].tsx +++ b/apps/cow-fi/pages/learn/[article].tsx @@ -45,6 +45,7 @@ import { Link, LinkType } from '@/components/Link' import { CONFIG, DATA_CACHE_TIME_SECONDS } from '@/const/meta' import { clickOnKnowledgeBase } from 'modules/analytics' +import { CmsImage } from '@cowprotocol/ui' interface ArticlePageProps { siteConfigData: typeof CONFIG @@ -281,7 +282,7 @@ export default function ArticlePage({ > {imageUrl && ( - {article.attributes?.title + )} {article.attributes?.title} diff --git a/apps/cow-fi/pages/learn/index.tsx b/apps/cow-fi/pages/learn/index.tsx index 041b4dc815..2e256ea862 100644 --- a/apps/cow-fi/pages/learn/index.tsx +++ b/apps/cow-fi/pages/learn/index.tsx @@ -1,46 +1,48 @@ +import { Color, Font, Media } from '@cowprotocol/ui' import { GetStaticProps } from 'next' -import { Font, Color, Media } from '@cowprotocol/ui' import styled from 'styled-components/macro' import { CONFIG, DATA_CACHE_TIME_SECONDS } from '@/const/meta' import Layout from '@/components/Layout' -import { getCategories, getArticles, ArticleListResponse } from 'services/cms' +import { ArticleListResponse, getArticles, getCategories } from 'services/cms' -import { SearchBar } from '@/components/SearchBar' import { ArrowButton } from '@/components/ArrowButton' +import { SearchBar } from '@/components/SearchBar' import IMG_ICON_BULB_COW from '@cowprotocol/assets/images/icon-bulb-cow.svg' import { - ContainerCard, - ContainerCardSection, - ContainerCardInner, - ContainerCardSectionTop, - CategoryLinks, - ArticleList, ArticleCard, + ArticleDescription, ArticleImage, + ArticleList, ArticleTitle, - ArticleDescription, - TopicList, - TopicCard, - TopicImage, - LinkSection, - LinkColumn, - LinkItem, - CTASectionWrapper, + CTAButton, CTAImage, + CTASectionWrapper, CTASubtitle, CTATitle, - CTAButton, + CategoryLinks, + ContainerCard, + ContainerCardInner, + ContainerCardSection, + ContainerCardSectionTop, ContainerCardSectionTopTitle, + LinkColumn, + LinkItem, + LinkSection, + TopicCard, + TopicImage, + TopicList, TopicTitle, } from '@/styles/styled' -import SVG from 'react-inlinesvg' import { clickOnKnowledgeBase } from 'modules/analytics' +import SVG from 'react-inlinesvg' + +import { CmsImage } from '@cowprotocol/ui' const PODCASTS = [ { @@ -67,7 +69,7 @@ const SPACES = [ link: 'https://x.com/cryptotesters/status/1501505365833248774', }, { - title: 'CoW Protocoll & Yearn Finance Partnership Deep Dive', + title: 'CoW Protocol & Yearn Finance Partnership Deep Dive', link: 'https://x.com/CoWSwap/status/1605593667682476032', }, { @@ -196,7 +198,7 @@ export default function Page({ siteConfigData, categories, articles, featuredArt {featuredArticles.map(({ title, description, cover, link }, index) => ( clickOnKnowledgeBase(`click-article-${title}`)}> - {cover && {title}} + {cover && } {title} {description} @@ -220,7 +222,7 @@ export default function Page({ siteConfigData, categories, articles, featuredArt > {imageUrl ? ( - {name} { diff --git a/apps/cow-fi/pages/learn/topic/[topicSlug].tsx b/apps/cow-fi/pages/learn/topic/[topicSlug].tsx index cf36a94998..0060e270fd 100644 --- a/apps/cow-fi/pages/learn/topic/[topicSlug].tsx +++ b/apps/cow-fi/pages/learn/topic/[topicSlug].tsx @@ -19,6 +19,7 @@ import { CategoryLinks, } from '@/styles/styled' import { clickOnKnowledgeBase } from 'modules/analytics' +import { CmsImage } from '@cowprotocol/ui' const Wrapper = styled.div` display: flex; @@ -61,7 +62,7 @@ const CategoryImageWrapper = styled.div` justify-content: center; ` -const CategoryImage = styled.img` +const CategoryImage = styled(CmsImage)` width: 100%; height: 100%; object-fit: cover; diff --git a/apps/cow-fi/pages/learn/topics.tsx b/apps/cow-fi/pages/learn/topics.tsx index 131eaf5eec..3fb5d5ede8 100644 --- a/apps/cow-fi/pages/learn/topics.tsx +++ b/apps/cow-fi/pages/learn/topics.tsx @@ -23,6 +23,7 @@ import { import { CONFIG, DATA_CACHE_TIME_SECONDS } from '@/const/meta' import { clickOnKnowledgeBase } from 'modules/analytics' +import { CmsImage } from '@cowprotocol/ui' interface PageProps { siteConfigData: typeof CONFIG @@ -99,10 +100,10 @@ export default function Page({ siteConfigData, categories, articles }: PageProps > {imageUrl ? ( - {name} { + onError={(e: React.SyntheticEvent) => { e.currentTarget.onerror = null e.currentTarget.style.display = 'none' }} diff --git a/apps/cow-fi/pages/legal/widget-terms.tsx b/apps/cow-fi/pages/legal/widget-terms.tsx index d4666789e8..a03235cb24 100644 --- a/apps/cow-fi/pages/legal/widget-terms.tsx +++ b/apps/cow-fi/pages/legal/widget-terms.tsx @@ -169,7 +169,7 @@ export default function Page({ siteConfigData }: PageProps) {

Obligations of the Partner

- You are prohibited from misusing the the Widget, the Interface, the Protocol or its infrastructure by + You are prohibited from misusing the Widget, the Interface, the Protocol or its infrastructure by knowingly introducing any material that is:

    @@ -182,14 +182,14 @@ export default function Page({ siteConfigData }: PageProps) {

    You are prohibited from attempting to gain unauthorized access to the:

    • the Widget, the Interface, the Protocol or its infrastructure;
    • -
    • Server(s) hosting the the Widget, the Interface, the Protocol or its infrastructure;
    • +
    • Server(s) hosting the Widget, the Interface, the Protocol or its infrastructure;
    • - Any computer or database connected to the the Widget, the Interface, the Protocol or its + Any computer or database connected to the Widget, the Interface, the Protocol or its infrastructure.

    - You are prohibited from attacking the the Widget, the Interface, the Protocol or its infrastructure + You are prohibited from attacking the Widget, the Interface, the Protocol or its infrastructure through:

      diff --git a/apps/cow-fi/pages/mev-blocker.tsx b/apps/cow-fi/pages/mev-blocker.tsx index 80af1e4911..8d19269ab2 100644 --- a/apps/cow-fi/pages/mev-blocker.tsx +++ b/apps/cow-fi/pages/mev-blocker.tsx @@ -122,7 +122,7 @@ export default function Page() { bgColor={'#EC4612'} color={'#FEE7CF'} href="#rpc" - onClick={() => clickOnMevBlocker('click-get-protected')} + onClick={() => clickOnMevBlocker('click-get-protected-heroSection')} > Get protected diff --git a/apps/cow-fi/services/cms/index.ts b/apps/cow-fi/services/cms/index.ts index 990b2ef14f..d1a1bb6c24 100644 --- a/apps/cow-fi/services/cms/index.ts +++ b/apps/cow-fi/services/cms/index.ts @@ -3,6 +3,7 @@ import { PaginationParam } from 'types' import qs from 'qs' import { toQueryParams } from 'util/queryParams' +import { getCmsClient } from '@cowprotocol/core' const PAGE_SIZE = 50 @@ -60,9 +61,7 @@ export function isSharedVideoEmbedComponent(component: ArticleBlock): component /** * Open API Fetch client. See docs for usage https://openapi-ts.pages.dev/openapi-fetch/ */ -export const client = CmsClient({ - url: 'https://cms.cow.fi/api', -}) +export const client = getCmsClient() /** * Returns the article slugs for the given page. diff --git a/apps/cowswap-frontend/src/cmsClient.ts b/apps/cowswap-frontend/src/cmsClient.ts deleted file mode 100644 index 079e460e22..0000000000 --- a/apps/cowswap-frontend/src/cmsClient.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { CmsClient } from '@cowprotocol/cms' - -export const cmsClient = CmsClient({ - url: 'https://cms.cow.fi/api', -}) diff --git a/apps/cowswap-frontend/src/modules/analytics/events.ts b/apps/cowswap-frontend/src/modules/analytics/events.ts index 881d3b518d..71d842e614 100644 --- a/apps/cowswap-frontend/src/modules/analytics/events.ts +++ b/apps/cowswap-frontend/src/modules/analytics/events.ts @@ -123,7 +123,7 @@ export function currencySelectAnalytics(field: string, label: string | undefined export function setMaxSellTokensAnalytics() { cowAnalytics.sendEvent({ category: Category.TRADE, - action: 'Set Maximun Sell Tokens', + action: 'Set Maximum Sell Tokens', }) } diff --git a/apps/cowswap-frontend/src/modules/appData/appData-module.md b/apps/cowswap-frontend/src/modules/appData/appData-module.md index a5f16ee0dd..f367ffcc0e 100644 --- a/apps/cowswap-frontend/src/modules/appData/appData-module.md +++ b/apps/cowswap-frontend/src/modules/appData/appData-module.md @@ -7,7 +7,7 @@ See also: ![appData module](./appData-module.drawio.svg) -## Desciption +## Description This module returns the `appData` metadata, which should be used for attaching additional information to the orders. diff --git a/apps/cowswap-frontend/src/modules/notifications/containers/ConnectTelegram.tsx b/apps/cowswap-frontend/src/modules/notifications/containers/ConnectTelegram.tsx index e61db2c6f5..9ec92600a1 100644 --- a/apps/cowswap-frontend/src/modules/notifications/containers/ConnectTelegram.tsx +++ b/apps/cowswap-frontend/src/modules/notifications/containers/ConnectTelegram.tsx @@ -1,8 +1,8 @@ import { useEffect, useRef, useState } from 'react' +import { getCmsClient } from '@cowprotocol/core' import { useWalletInfo } from '@cowprotocol/wallet' -import { cmsClient } from 'cmsClient' import { CheckCircle } from 'react-feather' interface TelegramData { @@ -14,6 +14,8 @@ interface TelegramData { username: string } +const cmsClient = getCmsClient() + export function ConnectTelegram() { const { account } = useWalletInfo() const [isTgSubscribed, setTgSubscribed] = useState(false) diff --git a/apps/cowswap-frontend/src/modules/notifications/hooks/useAccountNotifications.ts b/apps/cowswap-frontend/src/modules/notifications/hooks/useAccountNotifications.ts index 65056e70a2..aac14181df 100644 --- a/apps/cowswap-frontend/src/modules/notifications/hooks/useAccountNotifications.ts +++ b/apps/cowswap-frontend/src/modules/notifications/hooks/useAccountNotifications.ts @@ -1,6 +1,6 @@ +import { getCmsClient } from '@cowprotocol/core' import { useWalletInfo } from '@cowprotocol/wallet' -import { cmsClient } from 'cmsClient' import ms from 'ms.macro' import useSWR, { SWRConfiguration } from 'swr' @@ -13,6 +13,8 @@ const swrOptions: SWRConfiguration = { revalidateOnFocus: false, } +const cmsClient = getCmsClient() + export function useAccountNotifications() { const { account } = useWalletInfo() diff --git a/apps/cowswap-frontend/src/modules/trade/hooks/setupTradeState/useSetupTradeState.ts b/apps/cowswap-frontend/src/modules/trade/hooks/setupTradeState/useSetupTradeState.ts index 25fb4c41de..e31ed81985 100644 --- a/apps/cowswap-frontend/src/modules/trade/hooks/setupTradeState/useSetupTradeState.ts +++ b/apps/cowswap-frontend/src/modules/trade/hooks/setupTradeState/useSetupTradeState.ts @@ -1,7 +1,7 @@ import { useCallback, useEffect, useRef, useState } from 'react' import { usePrevious } from '@cowprotocol/common-hooks' -import { getRawCurrentChainIdFromUrl } from '@cowprotocol/common-utils' +import { debounce, getRawCurrentChainIdFromUrl } from '@cowprotocol/common-utils' import { SupportedChainId } from '@cowprotocol/cow-sdk' import { useSwitchNetwork, useWalletInfo } from '@cowprotocol/wallet' import { useWalletProvider } from '@cowprotocol/wallet-provider' @@ -54,6 +54,10 @@ export function useSetupTradeState(): void { [switchNetwork] ) + const debouncedSwitchNetworkInWallet = debounce(([targetChainId]: [SupportedChainId]) => { + switchNetworkInWallet(targetChainId) + }, 800) + const onProviderNetworkChanges = useCallback(() => { const rememberedUrlState = rememberedUrlStateRef.current @@ -188,7 +192,9 @@ export function useSetupTradeState(): void { const targetChainId = rememberedUrlStateRef.current?.chainId || currentChainId - switchNetworkInWallet(targetChainId) + // Debouncing switching multiple time in a quick span of time to avoid running into infinity loop of updating provider and url state. + // issue GH : https://github.com/cowprotocol/cowswap/issues/4734 + debouncedSwitchNetworkInWallet(targetChainId) console.debug('[TRADE STATE]', 'Set chainId to provider', { provider, urlChainId }) // Triggering only when chainId in URL is changes, provider is changed or rememberedUrlState is changed diff --git a/apps/cowswap-frontend/src/modules/twap/containers/TwapFormWarnings/warnings/FallbackHandlerWarning.tsx b/apps/cowswap-frontend/src/modules/twap/containers/TwapFormWarnings/warnings/FallbackHandlerWarning.tsx index 0fcd691ba8..f437a567c8 100644 --- a/apps/cowswap-frontend/src/modules/twap/containers/TwapFormWarnings/warnings/FallbackHandlerWarning.tsx +++ b/apps/cowswap-frontend/src/modules/twap/containers/TwapFormWarnings/warnings/FallbackHandlerWarning.tsx @@ -51,6 +51,7 @@ const InlineBannerWithCheckbox = styled(InlineBanner)`` interface FallbackHandlerWarningProps { isFallbackHandlerSetupAccepted: boolean + toggleFallbackHandlerSetupFlag(isChecked: boolean): void } @@ -65,7 +66,7 @@ export function FallbackHandlerWarning({ checked={isFallbackHandlerSetupAccepted} onChange={(event) => toggleFallbackHandlerSetupFlag(event.currentTarget.checked)} /> - Modify Safe's fallback handler when placing order + Make the modification when placing order ) @@ -81,10 +82,9 @@ export function FallbackHandlerWarning({ return ( - Unsupported Safe detected + Your Safe needs a modification

      - Connected Safe lacks required fallback handler. Switch to a compatible Safe or modify fallback handler for - TWAP orders when placing your order. + TWAP orders require a one-time update to your Safe to enable automated execution of scheduled transactions.

      Learn more {fallbackHandlerCheckbox} diff --git a/apps/cowswap-frontend/src/modules/twap/hooks/useEmulatedTwapOrders.ts b/apps/cowswap-frontend/src/modules/twap/hooks/useEmulatedTwapOrders.ts index b22764b3a6..47f9b7528e 100644 --- a/apps/cowswap-frontend/src/modules/twap/hooks/useEmulatedTwapOrders.ts +++ b/apps/cowswap-frontend/src/modules/twap/hooks/useEmulatedTwapOrders.ts @@ -18,7 +18,7 @@ const EMULATED_ORDERS_REFRESH_MS = ms`5s` * Returns a list of emulated twap orders * * `tokenByAddress` is a map of all known tokens, it comes from `useTwapOrdersTokens()` hook - * `useTwapOrdersTokens()` fetches unkown tokens from blockchain and stores them in the store + * `useTwapOrdersTokens()` fetches unknown tokens from blockchain and stores them in the store * So, there might be a race condition when we have an order but haven't fetched its token yet * Because of it, we wrap mapTwapOrderToStoreOrder() in try/catch and just don't add an order to the list */ diff --git a/apps/cowswap-frontend/src/modules/utm/utm-module.md b/apps/cowswap-frontend/src/modules/utm/utm-module.md index b7f1793aa1..b423b5a84e 100644 --- a/apps/cowswap-frontend/src/modules/utm/utm-module.md +++ b/apps/cowswap-frontend/src/modules/utm/utm-module.md @@ -7,7 +7,7 @@ See also: ![UTM module](./utm-module.drawio.svg) -## Desciption +## Description This module adds support for handling UTM codes. diff --git a/libs/core/src/cms/index.ts b/libs/core/src/cms/index.ts new file mode 100644 index 0000000000..13fd4f9984 --- /dev/null +++ b/libs/core/src/cms/index.ts @@ -0,0 +1,15 @@ +import { CmsClient } from '@cowprotocol/cms' + +export const CMS_BASE_URL = + process.env.REACT_APP_CMS_BASE_URL || process.env.NEXT_PUBLIC_CMS_BASE_URL || 'https://cms.cow.fi/api' + +let cmsClient: ReturnType | undefined + +export function getCmsClient() { + if (!cmsClient) { + cmsClient = CmsClient({ + url: CMS_BASE_URL, + }) + } + return cmsClient +} diff --git a/libs/core/src/index.ts b/libs/core/src/index.ts index 7ec7c07d69..50faec5122 100644 --- a/libs/core/src/index.ts +++ b/libs/core/src/index.ts @@ -1,3 +1,4 @@ export * from './jotaiStore' export * from './gasPirce' export * from './gnosisSafe' +export * from './cms' diff --git a/libs/permit-utils/README.md b/libs/permit-utils/README.md index 52176a084d..6447f670e7 100644 --- a/libs/permit-utils/README.md +++ b/libs/permit-utils/README.md @@ -25,7 +25,7 @@ const permitInfo = await getTokenPermitInfo({ ```typescript import { getPermitUtilsInstance } from "@cowprotocol/permit-utils" -// Using the a static account defined in the library +// Using a static account defined in the library const staticEip2612PermitUtils = getPermitUtilsInstance(chainId, provider) // Using a provided account address diff --git a/libs/ui/src/index.ts b/libs/ui/src/index.ts index ed2e000ce8..351ed75c74 100644 --- a/libs/ui/src/index.ts +++ b/libs/ui/src/index.ts @@ -30,6 +30,7 @@ export * from './pure/Popover' export * from './pure/BackButton' export * from './pure/ClosableBanner' export * from './pure/PercentDisplay' +export * from './pure/CmsImage' export * from './containers/CowSwapSafeAppLink' export * from './containers/InlineBanner' diff --git a/libs/ui/src/pure/CmsImage/index.tsx b/libs/ui/src/pure/CmsImage/index.tsx new file mode 100644 index 0000000000..b8af918ff9 --- /dev/null +++ b/libs/ui/src/pure/CmsImage/index.tsx @@ -0,0 +1,13 @@ +import { CMS_BASE_URL } from '@cowprotocol/core' + +const CMS_BASE_URL_ROOT = CMS_BASE_URL.replace('/api', '') // TODO: fix this, base url should not have /api + +function toCmsAbsoluteUrl(url: string) { + return url.startsWith('http') ? url : `${CMS_BASE_URL_ROOT}${url}` +} + +export function CmsImage({ src, className, alt, ...props }: JSX.IntrinsicElements['img']) { + if (!src) return null + + return {alt} +}