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}
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 && }
+ {cover && }{title}{description}
@@ -220,7 +222,7 @@ export default function Page({ siteConfigData, categories, articles, featuredArt
>
{imageUrl ? (
- {
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 ? (
- {
+ 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:
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
+}