diff --git a/apps/cowswap-frontend/src/common/containers/PermitModal/index.tsx b/apps/cowswap-frontend/src/common/containers/PermitModal/index.tsx new file mode 100644 index 0000000000..d2bb9c8d1a --- /dev/null +++ b/apps/cowswap-frontend/src/common/containers/PermitModal/index.tsx @@ -0,0 +1,15 @@ +import { useMemo } from 'react' + +import { Identicon, useWalletInfo } from '@cowprotocol/wallet' + +import { PermitModal as Pure, PermitModalProps } from '../../pure/PermitModal' + +export type PermitModalContainerProps = Omit + +export function PermitModal(props: PermitModalContainerProps) { + const { account } = useWalletInfo() + + const icon = useMemo(() => (account ? : undefined), [account]) + + return +} diff --git a/apps/cowswap-frontend/src/common/pure/Modal/index.cosmos.tsx b/apps/cowswap-frontend/src/common/pure/Modal/index.cosmos.tsx index 3630b46c01..f07e58e4a5 100644 --- a/apps/cowswap-frontend/src/common/pure/Modal/index.cosmos.tsx +++ b/apps/cowswap-frontend/src/common/pure/Modal/index.cosmos.tsx @@ -1,99 +1,46 @@ -import ICON_ARROW from 'assets/icon/arrow.svg' -import SVG from 'react-inlinesvg' import styled from 'styled-components/macro' -import { MOCK_TOKEN, IMAGE_ACCOUNT } from 'common/constants/cosmos'; -import { UI } from 'common/constants/theme' -import { IconSpinner } from 'common/pure/IconSpinner' -import { Stepper } from 'common/pure/Stepper' - -import { Modal, CowModal, NewModal, NewModalContentTop, NewModalContentBottom } from './index' +import { CowModal, Modal } from './index' const Wrapper = styled.div` width: 100vw; height: 100vh; ` -const ArrowRight = styled(SVG)` - --size: 12px; - width: var(--size); - height: var(--size); - margin: auto; - - > path { - fill: var(${UI.COLOR_TEXT2}); - } -` - const ModalFixtures = { 'default modal': ( - console.log("Dismissed")}> + console.log('Dismissed')}> Default modal content here ), 'modal with minHeight and maxHeight': ( - console.log("Dismissed")} minHeight={50} maxHeight={80}> + console.log('Dismissed')} minHeight={50} maxHeight={80}> Modal with minHeight and maxHeight ), 'cow modal': ( - console.log("Cow Modal Dismissed")} maxWidth={400}> + console.log('Cow Modal Dismissed')} maxWidth={400}> Cow Modal Content ), 'cow modal with background color': ( - console.log("Cow Modal Dismissed")} maxWidth={400} backgroundColor="pink"> + console.log('Cow Modal Dismissed')} + maxWidth={400} + backgroundColor="pink" + > Cow Modal with Pink Background ), - 'new modal + content top/bottom': ( - - - - -

Approve spending AAVE
on CoW Swap

-
- - -

Sign (gas-free!) in your wallet...

- -
-
-
- ), - 'new modal + content top/bottom 2': ( - - - - - -

Confirm Swap

-

10 AAVE 564.7202 DAI

-
-
- - -

Sign (gas-free!) in your wallet...

- -
-
-
- ), - 'new modal + heading title': ( - - - - New Modal - - - - ), } export default ModalFixtures diff --git a/apps/cowswap-frontend/src/common/pure/Modal/index.tsx b/apps/cowswap-frontend/src/common/pure/Modal/index.tsx index 9b3c952155..6bc9d7681c 100644 --- a/apps/cowswap-frontend/src/common/pure/Modal/index.tsx +++ b/apps/cowswap-frontend/src/common/pure/Modal/index.tsx @@ -4,8 +4,6 @@ import { isMobile } from '@cowprotocol/common-utils' import { useSpringValue, useTransition } from '@react-spring/web' import { useGesture } from '@use-gesture/react' -import CLOSE_ICON from 'assets/icon/x.svg' -import SVG from 'react-inlinesvg' import styled from 'styled-components/macro' import { UI } from 'common/constants/theme' @@ -24,6 +22,9 @@ interface ModalProps { children?: React.ReactNode } +/** + * @deprecated use common/pure/NewModal instead + */ export function Modal({ isOpen, onDismiss, @@ -145,160 +146,3 @@ export const CowModal = styled(Modal)<{ } } ` - -// New Modal to be used going forward ================================= -const ModalInner = styled.div` - display: flex; - flex-direction: column; - width: 100%; - height: auto; - margin: auto; - background: var(${UI.COLOR_CONTAINER_BG_01}); - border-radius: var(${UI.BORDER_RADIUS_NORMAL}); - box-shadow: var(${UI.BOX_SHADOW_NORMAL}); - padding: 0; - - ${({ theme }) => theme.mediaWidth.upToSmall` - margin: 8vh 0 0; - border-radius: 0; - border-top-left-radius: var(${UI.BORDER_RADIUS_NORMAL}); - border-top-right-radius: var(${UI.BORDER_RADIUS_NORMAL}); - box-shadow: none; - `} -` - -const NewCowModal = styled.div<{ maxWidth?: number | string; minHeight?: number | string }>` - display: flex; - width: 100%; - height: 100%; - margin: auto; - background: var(${UI.MODAL_BACKDROP}); - overflow-y: auto; - - ${ModalInner} { - max-width: ${({ maxWidth }) => (maxWidth ? `${maxWidth}px` : '100%')}; - min-height: ${({ minHeight }) => (minHeight ? `${minHeight}px` : '100%')}; - - ${({ theme }) => theme.mediaWidth.upToSmall` - max-width: 100%; - min-height: initial; - height: auto; - `} - } -` - -const Heading = styled.h2` - display: flex; - justify-content: space-between; - width: 100%; - height: auto; - padding: 18px; - margin: 0; - font-size: var(${UI.FONT_SIZE_MEDIUM}); - - ${({ theme }) => theme.mediaWidth.upToSmall` - position: sticky; - top: 0; - `} -` - -const IconX = styled.div` - position: fixed; - top: 18px; - right: 18px; - cursor: pointer; - opacity: 0.6; - transition: opacity 0.2s ease-in-out; - margin: 0 0 0 auto; - - > svg { - width: var(${UI.ICON_SIZE_NORMAL}); - height: var(${UI.ICON_SIZE_NORMAL}); - fill: var(${UI.ICON_COLOR_NORMAL}); - } - - &:hover { - opacity: 1; - } -` - -const NewModalContent = styled.div<{ paddingTop?: number }>` - display: flex; - align-items: center; - justify-content: center; - flex-flow: column wrap; - flex: 1; - width: 100%; - height: 100%; - padding: 0 var(${UI.PADDING_NORMAL}) var(${UI.PADDING_NORMAL}); - - h1, - h2, - h3 { - width: 100%; - font-size: var(${UI.FONT_SIZE_LARGER}); - font-weight: var(${UI.FONT_WEIGHT_BOLD}); - text-align: center; - line-height: 1.4; - margin: 0 auto; - } - - p { - font-size: var(${UI.FONT_SIZE_NORMAL}); - font-weight: var(${UI.FONT_WEIGHT_NORMAL}); - color: var(${UI.COLOR_TEXT2}); - margin: 0 auto; - padding: 0; - } -` - -export const NewModalContentTop = styled.div<{ paddingTop?: number }>` - display: flex; - flex-flow: column wrap; - align-items: center; - justify-content: center; - width: 100%; - margin: 0 0 auto; - padding: ${({ paddingTop = 0 }) => `${paddingTop}px`} 0 0; - gap: 24px; - - > span { - gap: 6px; - display: flex; - flex-flow: column wrap; - } - - p { - font-size: var(${UI.FONT_SIZE_MEDIUM}); - } -` - -export const NewModalContentBottom = styled(NewModalContentTop)` - margin: auto 0 0; - - p { - font-size: var(${UI.FONT_SIZE_NORMAL}); - } -` -interface NewModalProps { - maxWidth?: number - minHeight?: number - title?: string - onDismiss?: () => void - children?: React.ReactNode -} - -export function NewModal({ maxWidth = 450, minHeight = 450, title, children, onDismiss }: NewModalProps) { - return ( - - - {title && {title}} - {children} - - - onDismiss && onDismiss()}> - - - - ) -} diff --git a/apps/cowswap-frontend/src/common/pure/NewModal/index.cosmos.tsx b/apps/cowswap-frontend/src/common/pure/NewModal/index.cosmos.tsx new file mode 100644 index 0000000000..72d1966d76 --- /dev/null +++ b/apps/cowswap-frontend/src/common/pure/NewModal/index.cosmos.tsx @@ -0,0 +1,85 @@ +import ICON_ARROW from 'assets/icon/arrow.svg' +import SVG from 'react-inlinesvg' +import styled from 'styled-components/macro' + +import { IMAGE_ACCOUNT, MOCK_TOKEN } from 'common/constants/cosmos' +import { UI } from 'common/constants/theme' +import { IconSpinner } from 'common/pure/IconSpinner' +import { Stepper } from 'common/pure/Stepper' + +import { NewModal, NewModalContentBottom, NewModalContentTop } from './index' + +const Wrapper = styled.div` + width: 100vw; + height: 100vh; +` + +const ArrowRight = styled(SVG)` + --size: 12px; + width: var(--size); + height: var(--size); + margin: auto; + + > path { + fill: var(${UI.COLOR_TEXT2}); + } +` + +const ModalFixtures = { + 'new modal + content top/bottom': ( + + + + +

+ Approve spending AAVE
on CoW Swap +

+
+ + +

Sign (gas-free!) in your wallet...

+ +
+
+
+ ), + 'new modal + content top/bottom 2': ( + + + + + +

Confirm Swap

+

+ 10 AAVE 564.7202 DAI +

+
+
+ + +

Sign (gas-free!) in your wallet...

+ +
+
+
+ ), + 'new modal + heading title': ( + + - New Modal - + + ), +} + +export default ModalFixtures diff --git a/apps/cowswap-frontend/src/common/pure/NewModal/index.tsx b/apps/cowswap-frontend/src/common/pure/NewModal/index.tsx new file mode 100644 index 0000000000..cab27ecbed --- /dev/null +++ b/apps/cowswap-frontend/src/common/pure/NewModal/index.tsx @@ -0,0 +1,163 @@ +import React from 'react' + +import CLOSE_ICON from 'assets/icon/x.svg' +import SVG from 'react-inlinesvg' +import styled from 'styled-components/macro' + +import { UI } from 'common/constants/theme' + +const ModalInner = styled.div` + display: flex; + flex-direction: column; + width: 100%; + height: auto; + margin: auto; + background: var(${UI.COLOR_CONTAINER_BG_01}); + border-radius: var(${UI.BORDER_RADIUS_NORMAL}); + box-shadow: var(${UI.BOX_SHADOW_NORMAL}); + padding: 0; + + ${({ theme }) => theme.mediaWidth.upToSmall` + margin: 8vh 0 0; + border-radius: 0; + border-top-left-radius: var(${UI.BORDER_RADIUS_NORMAL}); + border-top-right-radius: var(${UI.BORDER_RADIUS_NORMAL}); + box-shadow: none; + `} +` + +const Wrapper = styled.div<{ maxWidth?: number | string; minHeight?: number | string }>` + display: flex; + width: 100%; + height: 100%; + margin: auto; + background: var(${UI.MODAL_BACKDROP}); + overflow-y: auto; + + ${ModalInner} { + max-width: ${({ maxWidth }) => (maxWidth ? `${maxWidth}px` : '100%')}; + min-height: ${({ minHeight }) => (minHeight ? `${minHeight}px` : '100%')}; + + ${({ theme }) => theme.mediaWidth.upToSmall` + max-width: 100%; + min-height: initial; + height: auto; + `} + } +` + +const Heading = styled.h2` + display: flex; + justify-content: space-between; + width: 100%; + height: auto; + padding: 18px; + margin: 0; + font-size: var(${UI.FONT_SIZE_MEDIUM}); + + ${({ theme }) => theme.mediaWidth.upToSmall` + position: sticky; + top: 0; + `} +` + +const IconX = styled.div` + position: fixed; + top: 18px; + right: 18px; + cursor: pointer; + opacity: 0.6; + transition: opacity 0.2s ease-in-out; + margin: 0 0 0 auto; + + > svg { + width: var(${UI.ICON_SIZE_NORMAL}); + height: var(${UI.ICON_SIZE_NORMAL}); + fill: var(${UI.ICON_COLOR_NORMAL}); + } + + &:hover { + opacity: 1; + } +` + +const NewModalContent = styled.div<{ paddingTop?: number }>` + display: flex; + align-items: center; + justify-content: center; + flex-flow: column wrap; + flex: 1; + width: 100%; + height: 100%; + padding: 0 var(${UI.PADDING_NORMAL}) var(${UI.PADDING_NORMAL}); + + h1, + h2, + h3 { + width: 100%; + font-size: var(${UI.FONT_SIZE_LARGER}); + font-weight: var(${UI.FONT_WEIGHT_BOLD}); + text-align: center; + line-height: 1.4; + margin: 0 auto; + } + + p { + font-size: var(${UI.FONT_SIZE_NORMAL}); + font-weight: var(${UI.FONT_WEIGHT_NORMAL}); + color: var(${UI.COLOR_TEXT2}); + margin: 0 auto; + padding: 0; + } +` + +export const NewModalContentTop = styled.div<{ paddingTop?: number }>` + display: flex; + flex-flow: column wrap; + align-items: center; + justify-content: center; + width: 100%; + margin: 0 0 auto; + padding: ${({ paddingTop = 0 }) => `${paddingTop}px`} 0 0; + gap: 24px; + + > span { + gap: 6px; + display: flex; + flex-flow: column wrap; + } + + p { + font-size: var(${UI.FONT_SIZE_MEDIUM}); + } +` + +export const NewModalContentBottom = styled(NewModalContentTop)` + margin: auto 0 0; + + p { + font-size: var(${UI.FONT_SIZE_NORMAL}); + } +` +export interface NewModalProps { + maxWidth?: number + minHeight?: number + title?: string + onDismiss?: () => void + children?: React.ReactNode +} + +export function NewModal({ maxWidth = 450, minHeight = 450, title, children, onDismiss }: NewModalProps) { + return ( + + + {title && {title}} + {children} + + + onDismiss && onDismiss()}> + + + + ) +} diff --git a/apps/cowswap-frontend/src/common/pure/PermitModal/index.cosmos.tsx b/apps/cowswap-frontend/src/common/pure/PermitModal/index.cosmos.tsx new file mode 100644 index 0000000000..0c2f775874 --- /dev/null +++ b/apps/cowswap-frontend/src/common/pure/PermitModal/index.cosmos.tsx @@ -0,0 +1,49 @@ +import { USDC_MAINNET, WBTC } from '@cowprotocol/common-const' +import { Identicon } from '@cowprotocol/wallet' +import { CurrencyAmount } from '@uniswap/sdk-core' + +import styled from 'styled-components/macro' + +import { IconSpinner } from '../IconSpinner' + +import { PermitModal } from './index' + +const Wrapper = styled.div` + width: 100vw; + height: 100vh; +` + +const INPUT_AMOUNT = CurrencyAmount.fromRawAmount(USDC_MAINNET, 500_000 * 10 ** USDC_MAINNET.decimals) +const OUTPUT_AMOUNT = CurrencyAmount.fromRawAmount(WBTC, 1.2 * 10 ** WBTC.decimals) + +const WALLET_ICON = ( + + + +) + +const PermitModalFixtures = { + 'Pending permit signature': ( + + + + ), + 'Pending order signature': ( + + + + ), + // These two cases should happen, but including for completeness as the parameters allow it + 'Missing amounts on approve': ( + + + + ), + 'Missing amounts on submit': ( + + + + ), +} + +export default PermitModalFixtures diff --git a/apps/cowswap-frontend/src/common/pure/PermitModal/index.tsx b/apps/cowswap-frontend/src/common/pure/PermitModal/index.tsx new file mode 100644 index 0000000000..572e8091b6 --- /dev/null +++ b/apps/cowswap-frontend/src/common/pure/PermitModal/index.tsx @@ -0,0 +1,102 @@ +import { useMemo } from 'react' + +import { TokenAmount, TokenSymbol } from '@cowprotocol/ui' +import { Currency, CurrencyAmount } from '@uniswap/sdk-core' + +import ICON_ARROW from 'assets/icon/arrow.svg' +import SVG from 'react-inlinesvg' +import styled from 'styled-components/macro' +import { Nullish } from 'types' + +import { UI } from '../../constants/theme' +import { IconSpinner } from '../IconSpinner' +import { NewModal, NewModalContentBottom, NewModalContentTop, NewModalProps } from '../NewModal' +import { Stepper, StepProps } from '../Stepper' + +export type PermitModalProps = NewModalProps & { + inputAmount: Nullish> + outputAmount: Nullish> + step: 'approve' | 'submit' + icon?: React.ReactNode +} + +/** + * You probably want to use containers/PermitModal instead + * This is the pure component for cosmos + */ +export function PermitModal(props: PermitModalProps) { + const { inputAmount, outputAmount, step, icon: inputIcon } = props + + const steps: StepProps[] = useMemo( + () => [ + { + stepState: step === 'approve' ? 'loading' : 'finished', + stepNumber: 1, + label: 'Approve' + (step === 'approve' ? '' : 'd'), + }, + { stepState: step === 'submit' ? 'loading' : 'active', stepNumber: 2, label: 'Submit' }, + ], + [step] + ) + const icon = useMemo( + () => + step === 'approve' ? ( + + ) : ( + {inputIcon} + ), + [inputAmount?.currency, inputIcon, step] + ) + + const title = useMemo( + () => + step === 'approve' ? ( + <> + Approve spending
+ on CoW Swap + + ) : ( + 'Confirm Swap' + ), + [inputAmount?.currency, step] + ) + + const body = useMemo( + () => + step === 'approve' ? null : ( +

+ {' '} + +

+ ), + [inputAmount, outputAmount, step] + ) + + return ( + + + {icon} + +

{title}

+ {body} +
+
+ + +

Sign (gas-free!) in your wallet...

+ +
+
+ ) +} + +const ArrowRight = styled(SVG)` + --size: 12px; + width: var(--size); + height: var(--size); + margin: auto; + + > path { + fill: var(${UI.COLOR_TEXT2}); + } +` diff --git a/apps/cowswap-frontend/src/common/pure/Stepper/index.tsx b/apps/cowswap-frontend/src/common/pure/Stepper/index.tsx index f3a9ee9957..b1673895b8 100644 --- a/apps/cowswap-frontend/src/common/pure/Stepper/index.tsx +++ b/apps/cowswap-frontend/src/common/pure/Stepper/index.tsx @@ -7,7 +7,7 @@ import { IconSpinner } from 'common/pure/IconSpinner' type StepState = 'active' | 'finished' | 'disabled' | 'error' | 'loading' | 'open' -interface StepProps { +export interface StepProps { stepState: StepState stepNumber: number label: string @@ -24,7 +24,11 @@ interface StepStyles { const stateStyles: Record = { active: { dotBackground: UI.COLOR_LINK, dotColor: UI.COLOR_CONTAINER_BG_01, labelColor: UI.COLOR_TEXT1 }, finished: { dotBackground: UI.COLOR_LINK_OPACITY_10, dotColor: UI.COLOR_LINK, labelColor: UI.COLOR_TEXT1 }, - disabled: { dotBackground: UI.COLOR_TEXT1_OPACITY_25, dotColor: UI.COLOR_TEXT1_OPACITY_25, labelColor: UI.COLOR_TEXT1_OPACITY_25 }, + disabled: { + dotBackground: UI.COLOR_TEXT1_OPACITY_25, + dotColor: UI.COLOR_TEXT1_OPACITY_25, + labelColor: UI.COLOR_TEXT1_OPACITY_25, + }, error: { dotBackground: UI.COLOR_DANGER_BG, dotColor: UI.COLOR_DANGER, labelColor: UI.COLOR_DANGER }, loading: { dotBackground: UI.COLOR_LINK, dotColor: UI.COLOR_CONTAINER_BG_01, labelColor: UI.COLOR_LINK }, open: { dotBackground: UI.COLOR_TEXT1_OPACITY_10, dotColor: UI.COLOR_TEXT2, labelColor: UI.COLOR_TEXT2 }, @@ -81,7 +85,12 @@ const Step = styled.div` width: 100%; height: 1px; border: 0; - background: ${({ stepState }) => stepState === 'error' ? `var(${stateStyles['error'].dotBackground})` : stepState === 'finished' ? `var(${stateStyles['finished'].dotBackground})` : `var(${UI.COLOR_TEXT1_OPACITY_25})`}; + background: ${({ stepState }) => + stepState === 'error' + ? `var(${stateStyles['error'].dotBackground})` + : stepState === 'finished' + ? `var(${stateStyles['finished'].dotBackground})` + : `var(${UI.COLOR_TEXT1_OPACITY_25})`}; border-radius: var(${UI.BORDER_RADIUS_NORMAL}); } @@ -96,7 +105,7 @@ const Step = styled.div` const Wrapper = styled.div<{ maxWidth?: string; dotSize?: number }>` --dotSize: ${({ dotSize }) => `${dotSize}px`}; - width: ${({ maxWidth }) => maxWidth ? maxWidth : '100%'}; + width: ${({ maxWidth }) => (maxWidth ? maxWidth : '100%')}; display: flex; align-items: center; justify-content: space-between; @@ -121,14 +130,9 @@ export function Stepper({ steps, maxWidth, dotSize = 21 }: StepperProps) { {step.stepNumber} - ) : - {step.stepState === 'finished' ? ( - - ) : ( - step.stepNumber - )} - - } + ) : ( + {step.stepState === 'finished' ? : step.stepNumber} + )} {step.label}
diff --git a/apps/cowswap-frontend/src/legacy/components/TransactionConfirmationModal/index.tsx b/apps/cowswap-frontend/src/legacy/components/TransactionConfirmationModal/index.tsx index c74ca9b696..18a87ea32e 100644 --- a/apps/cowswap-frontend/src/legacy/components/TransactionConfirmationModal/index.tsx +++ b/apps/cowswap-frontend/src/legacy/components/TransactionConfirmationModal/index.tsx @@ -9,11 +9,14 @@ import { useMultipleActivityDescriptors } from 'legacy/hooks/useRecentActivity' import { ConfirmOperationType } from 'legacy/state/types' import { useSetIsConfirmationModalOpen } from 'modules/swap/state/surplusModal' +import { SwapConfirmState } from 'modules/swap/state/swapConfirmAtom' import { handleFollowPendingTxPopupAtom } from 'modules/wallet/state/followPendingTxPopupAtom' +import { PermitModal } from 'common/containers/PermitModal' import { useGetSurplusData } from 'common/hooks/useGetSurplusFiatValue' import { CowModal } from 'common/pure/Modal' import { TransactionSubmittedContent } from 'common/pure/TransactionSubmittedContent' +import { TradeAmounts } from 'common/types' import { LegacyConfirmationPendingContent } from './LegacyConfirmationPendingContent' @@ -26,6 +29,8 @@ export interface ConfirmationModalProps { pendingText?: ReactNode currencyToAdd?: Currency | undefined operationType: ConfirmOperationType + tradeAmounts?: TradeAmounts | undefined + swapConfirmState?: SwapConfirmState | undefined } export function TransactionConfirmationModal({ @@ -37,6 +42,8 @@ export function TransactionConfirmationModal({ content, currencyToAdd, operationType, + tradeAmounts, + swapConfirmState, }: ConfirmationModalProps) { const { chainId } = useWalletInfo() const setShowFollowPendingTxPopup = useSetAtom(handleFollowPendingTxPopupAtom) @@ -70,7 +77,14 @@ export function TransactionConfirmationModal({ return ( - {attemptingTxn ? ( + {showPermitModal(swapConfirmState) ? ( + + ) : attemptingTxn ? ( ), - [onDismiss, modalBottom, modalHeader, swapErrorMessage] + [swapErrorMessage, onDismiss, modalHeader, modalBottom] + ) + + const tradeAmounts: TradeAmounts | undefined = useMemo( + () => + trade ? { inputAmount: trade.inputAmountWithoutFee, outputAmount: trade.outputAmountWithoutFee } : undefined, + [trade] ) return ( @@ -115,6 +122,8 @@ export function ConfirmSwapModal({ pendingText={} currencyToAdd={trade?.outputAmount.currency} operationType={ConfirmOperationType.ORDER_SIGN} + tradeAmounts={tradeAmounts} + swapConfirmState={swapConfirmState} /> ) } diff --git a/apps/cowswap-frontend/src/legacy/state/swap/TradeGp.ts b/apps/cowswap-frontend/src/legacy/state/swap/TradeGp.ts index 42089c4eb7..d98caa57cd 100644 --- a/apps/cowswap-frontend/src/legacy/state/swap/TradeGp.ts +++ b/apps/cowswap-frontend/src/legacy/state/swap/TradeGp.ts @@ -1,6 +1,6 @@ import { ONE_FRACTION } from '@cowprotocol/common-const' import { CanonicalMarketParams, getCanonicalMarket } from '@cowprotocol/common-utils' -import { CurrencyAmount, Currency, TradeType, Price, Percent } from '@uniswap/sdk-core' +import { Currency, CurrencyAmount, Percent, Price, TradeType } from '@uniswap/sdk-core' interface PriceInformation { token: string @@ -94,7 +94,7 @@ export default class TradeGp { * The output amount for the trade assuming no slippage. */ readonly outputAmount: CurrencyAmount - readonly outputAmountWithoutFee?: CurrencyAmount + readonly outputAmountWithoutFee: CurrencyAmount /** * Trade fee */ diff --git a/apps/cowswap-frontend/src/mocks/tradeStateMock.ts b/apps/cowswap-frontend/src/mocks/tradeStateMock.ts index 84523caefd..20d5b09ac3 100644 --- a/apps/cowswap-frontend/src/mocks/tradeStateMock.ts +++ b/apps/cowswap-frontend/src/mocks/tradeStateMock.ts @@ -50,7 +50,6 @@ export const outputCurrencyInfoMock: CurrencyInfo = { } export const tradeContextMock: TradeFlowContext = { - hasEnoughAllowance: undefined, permitInfo: undefined, postOrderParams: { class: OrderClass.LIMIT, diff --git a/apps/cowswap-frontend/src/modules/limitOrders/hooks/useTradeFlowContext.ts b/apps/cowswap-frontend/src/modules/limitOrders/hooks/useTradeFlowContext.ts index 7d0f090eb3..f049ef4600 100644 --- a/apps/cowswap-frontend/src/modules/limitOrders/hooks/useTradeFlowContext.ts +++ b/apps/cowswap-frontend/src/modules/limitOrders/hooks/useTradeFlowContext.ts @@ -37,7 +37,7 @@ export function useTradeFlowContext(): TradeFlowContext | null { const permitInfo = useIsTokenPermittable(state.inputCurrency, TradeType.LIMIT_ORDER) const checkAllowanceAddress = GP_VAULT_RELAYER[chainId] - const { enoughAllowance: hasEnoughAllowance } = useEnoughBalanceAndAllowance({ + const { enoughAllowance } = useEnoughBalanceAndAllowance({ account, amount: state.slippageAdjustedSellAmount || undefined, checkAllowanceAddress, @@ -75,8 +75,7 @@ export function useTradeFlowContext(): TradeFlowContext | null { dispatch, provider, rateImpact, - permitInfo, - hasEnoughAllowance, + permitInfo: !enoughAllowance ? permitInfo : undefined, postOrderParams: { class: OrderClass.LIMIT, kind: state.orderKind, diff --git a/apps/cowswap-frontend/src/modules/limitOrders/pure/LimitOrdersDetails/index.cosmos.tsx b/apps/cowswap-frontend/src/modules/limitOrders/pure/LimitOrdersDetails/index.cosmos.tsx index 2a71f011a8..68a605eb1f 100644 --- a/apps/cowswap-frontend/src/modules/limitOrders/pure/LimitOrdersDetails/index.cosmos.tsx +++ b/apps/cowswap-frontend/src/modules/limitOrders/pure/LimitOrdersDetails/index.cosmos.tsx @@ -16,7 +16,6 @@ const inputCurrency = COW[SupportedChainId.MAINNET] const outputCurrency = GNO[SupportedChainId.MAINNET] const tradeContext: TradeFlowContext = { - hasEnoughAllowance: undefined, permitInfo: undefined, postOrderParams: { class: OrderClass.LIMIT, diff --git a/apps/cowswap-frontend/src/modules/limitOrders/services/tradeFlow/index.ts b/apps/cowswap-frontend/src/modules/limitOrders/services/tradeFlow/index.ts index b61ef7ffc8..d79a2008ba 100644 --- a/apps/cowswap-frontend/src/modules/limitOrders/services/tradeFlow/index.ts +++ b/apps/cowswap-frontend/src/modules/limitOrders/services/tradeFlow/index.ts @@ -29,7 +29,6 @@ export async function tradeFlow( permitInfo, provider, chainId, - hasEnoughAllowance, allowsOffchainSigning, settlementContract, dispatch, @@ -58,7 +57,6 @@ export async function tradeFlow( logTradeFlow('LIMIT ORDER FLOW', 'STEP 2: handle permit') postOrderParams.appData = await handlePermit({ permitInfo, - hasEnoughAllowance, inputToken: sellToken, provider, account, diff --git a/apps/cowswap-frontend/src/modules/limitOrders/services/types.ts b/apps/cowswap-frontend/src/modules/limitOrders/services/types.ts index 2c88ed15d9..2c55f9e369 100644 --- a/apps/cowswap-frontend/src/modules/limitOrders/services/types.ts +++ b/apps/cowswap-frontend/src/modules/limitOrders/services/types.ts @@ -6,7 +6,7 @@ import SafeAppsSDK from '@safe-global/safe-apps-sdk' import { AppDispatch } from 'legacy/state' import { PostOrderParams } from 'legacy/utils/trade' -import { PermitInfo } from 'modules/permit' +import { IsTokenPermittableResult } from 'modules/permit' export interface TradeFlowContext { // signer changes creates redundant re-renders @@ -19,8 +19,7 @@ export interface TradeFlowContext { provider: Web3Provider allowsOffchainSigning: boolean isGnosisSafeWallet: boolean - permitInfo: PermitInfo | undefined - hasEnoughAllowance: boolean | undefined + permitInfo: IsTokenPermittableResult } export interface SafeBundleFlowContext extends TradeFlowContext { diff --git a/apps/cowswap-frontend/src/modules/permit/index.ts b/apps/cowswap-frontend/src/modules/permit/index.ts index ef487d8884..6906fa3831 100644 --- a/apps/cowswap-frontend/src/modules/permit/index.ts +++ b/apps/cowswap-frontend/src/modules/permit/index.ts @@ -1,5 +1,5 @@ export { useAccountAgnosticPermitHookData } from './hooks/useAccountAgnosticPermitHookData' export { generatePermitHook } from './utils/generatePermitHook' export { useIsTokenPermittable } from './hooks/useIsTokenPermittable' -export { handlePermit } from './utils/handlePermit' +export * from './utils/handlePermit' export * from './types' diff --git a/apps/cowswap-frontend/src/modules/permit/types.ts b/apps/cowswap-frontend/src/modules/permit/types.ts index f0eaa79a7f..9017f2bfb2 100644 --- a/apps/cowswap-frontend/src/modules/permit/types.ts +++ b/apps/cowswap-frontend/src/modules/permit/types.ts @@ -36,7 +36,6 @@ export type PermitHookParams = { export type HandlePermitParams = Omit & { permitInfo: IsTokenPermittableResult - hasEnoughAllowance: undefined | boolean appData: AppDataInfo } diff --git a/apps/cowswap-frontend/src/modules/permit/utils/handlePermit.ts b/apps/cowswap-frontend/src/modules/permit/utils/handlePermit.ts index 5f2d37ba91..e623fd8ec3 100644 --- a/apps/cowswap-frontend/src/modules/permit/utils/handlePermit.ts +++ b/apps/cowswap-frontend/src/modules/permit/utils/handlePermit.ts @@ -1,5 +1,8 @@ import { AppDataInfo, buildAppDataHooks, updateHooksOnAppData } from 'modules/appData' -import { generatePermitHook, HandlePermitParams } from 'modules/permit' + +import { generatePermitHook } from './generatePermitHook' + +import { HandlePermitParams } from '../types' /** * Handle token permit @@ -12,12 +15,11 @@ import { generatePermitHook, HandlePermitParams } from 'modules/permit' * Returns the updated appData */ export async function handlePermit(params: HandlePermitParams): Promise { - const { permitInfo, hasEnoughAllowance, inputToken, provider, account, chainId, appData } = params + const { permitInfo, inputToken, provider, account, chainId, appData } = params - if (permitInfo && !hasEnoughAllowance) { - // If token is permittable and there's not enough allowance, get the permit hook + if (permitInfo) { + // permitInfo will only be set if there's enough allowance - // TODO: maybe we need a modal to inform the user what they need to sign? const permitData = await generatePermitHook({ inputToken, provider, diff --git a/apps/cowswap-frontend/src/modules/swap/hooks/useSwapConfirmManager.ts b/apps/cowswap-frontend/src/modules/swap/hooks/useSwapConfirmManager.ts index d771262739..0ba4f9b23c 100644 --- a/apps/cowswap-frontend/src/modules/swap/hooks/useSwapConfirmManager.ts +++ b/apps/cowswap-frontend/src/modules/swap/hooks/useSwapConfirmManager.ts @@ -1,75 +1,113 @@ -import { useAtom } from 'jotai' +import { useSetAtom } from 'jotai' import { useMemo } from 'react' import TradeGp from 'legacy/state/swap/TradeGp' import { useExpertModeManager } from 'legacy/state/user/hooks' -import { swapConfirmAtom } from 'modules/swap/state/swapConfirmAtom' +import { swapConfirmAtom, SwapConfirmState } from 'modules/swap/state/swapConfirmAtom' export interface SwapConfirmManager { setSwapError(swapErrorMessage: string): void - openSwapConfirmModal(tradeToConfirm: TradeGp): void + openSwapConfirmModal(tradeToConfirm: TradeGp, needsPermitSignature?: boolean): void acceptRateUpdates(tradeToConfirm: TradeGp): void closeSwapConfirm(): void sendTransaction(tradeToConfirm: TradeGp): void transactionSent(txHash: string): void + requestPermitSignature(): void + permitSigned(): void } export function useSwapConfirmManager(): SwapConfirmManager { - const [swapConfirmState, setSwapConfirmState] = useAtom(swapConfirmAtom) + const setSwapConfirmState = useSetAtom(swapConfirmAtom) const [isExpertMode] = useExpertModeManager() return useMemo( () => ({ setSwapError(swapErrorMessage: string) { - const state = { ...swapConfirmState, swapErrorMessage } - console.debug('[Swap confirm state] setSwapError: ', state) - setSwapConfirmState(state) + setSwapConfirmState((prev) => { + const state = { ...prev, swapErrorMessage, attemptingTxn: false, permitSignatureState: undefined } + console.debug('[Swap confirm state] setSwapError: ', state) + return state + }) }, openSwapConfirmModal(tradeToConfirm: TradeGp) { - const state = { + const state: SwapConfirmState = { tradeToConfirm, attemptingTxn: false, swapErrorMessage: undefined, showConfirm: true, txHash: undefined, + permitSignatureState: undefined, } console.debug('[Swap confirm state] openSwapConfirmModal: ', state) setSwapConfirmState(state) }, acceptRateUpdates(tradeToConfirm: TradeGp) { - const state = { ...swapConfirmState, tradeToConfirm } - console.debug('[Swap confirm state] acceptRateUpdates: ', state) - setSwapConfirmState(state) + setSwapConfirmState((prev) => { + const state = { ...prev, tradeToConfirm } + console.debug('[Swap confirm state] acceptRateUpdates: ', state) + return state + }) + }, + requestPermitSignature() { + setSwapConfirmState((prev) => { + const state: SwapConfirmState = { ...prev, permitSignatureState: 'requested' } + console.debug('[Swap confirm state] requestPermitSignature: ', state) + return state + }) + }, + permitSigned() { + setSwapConfirmState((prev) => { + // Move to `signed` state only if previous state was `requested` - which means the order is using the permit + // Set to `undefined` otherwise + const permitSignatureState = prev.permitSignatureState === 'requested' ? 'signed' : undefined + + const state: SwapConfirmState = { + ...prev, + permitSignatureState, + } + + console.debug('[Swap confirm state] permitSigned: ', state) + + return state + }) }, closeSwapConfirm() { - const state = { ...swapConfirmState, showConfirm: false } - console.debug('[Swap confirm state] closeSwapConfirm: ', state) - setSwapConfirmState(state) + setSwapConfirmState((prev) => { + const state = { ...prev, showConfirm: false, permitSignatureState: undefined } + console.debug('[Swap confirm state] closeSwapConfirm: ', state) + return state + }) }, sendTransaction(tradeToConfirm: TradeGp) { - const state = { - tradeToConfirm, - attemptingTxn: true, - swapErrorMessage: undefined, - showConfirm: true, - txHash: undefined, - } - console.debug('[Swap confirm state] sendTransaction: ', state) - setSwapConfirmState(state) + setSwapConfirmState((prev) => { + const state = { + ...prev, + tradeToConfirm, + attemptingTxn: true, + swapErrorMessage: undefined, + showConfirm: true, + txHash: undefined, + } + console.debug('[Swap confirm state] sendTransaction: ', state) + return state + }) }, transactionSent(txHash: string) { - const state = { - ...swapConfirmState, - attemptingTxn: false, - swapErrorMessage: undefined, - showConfirm: !isExpertMode, - txHash, - } - console.debug('[Swap confirm state] transactionSent: ', state) - setSwapConfirmState(state) + setSwapConfirmState((prev) => { + const state = { + ...prev, + attemptingTxn: false, + swapErrorMessage: undefined, + showConfirm: !isExpertMode, + txHash, + permitSignatureState: undefined, + } + console.debug('[Swap confirm state] transactionSent: ', state) + return state + }) }, }), - [swapConfirmState, setSwapConfirmState, isExpertMode] + [setSwapConfirmState, isExpertMode] ) } diff --git a/apps/cowswap-frontend/src/modules/swap/hooks/useSwapFlowContext.ts b/apps/cowswap-frontend/src/modules/swap/hooks/useSwapFlowContext.ts index 85003357ad..f2c1329a80 100644 --- a/apps/cowswap-frontend/src/modules/swap/hooks/useSwapFlowContext.ts +++ b/apps/cowswap-frontend/src/modules/swap/hooks/useSwapFlowContext.ts @@ -16,7 +16,7 @@ export function useSwapFlowContext(): SwapFlowContext | null { const permitInfo = useIsTokenPermittable(sellCurrency, TradeType.SWAP) const checkAllowanceAddress = GP_VAULT_RELAYER[baseProps.chainId || SupportedChainId.MAINNET] - const { enoughAllowance: hasEnoughAllowance } = useEnoughBalanceAndAllowance({ + const { enoughAllowance } = useEnoughBalanceAndAllowance({ account: baseProps.account, amount: baseProps.inputAmountWithSlippage, checkAllowanceAddress, @@ -35,7 +35,6 @@ export function useSwapFlowContext(): SwapFlowContext | null { return { ...baseContext, contract, - permitInfo, - hasEnoughAllowance, + permitInfo: !enoughAllowance ? permitInfo : undefined, } } diff --git a/apps/cowswap-frontend/src/modules/swap/pure/SwapButtons/index.tsx b/apps/cowswap-frontend/src/modules/swap/pure/SwapButtons/index.tsx index c1829a6e1b..8605cc9de6 100644 --- a/apps/cowswap-frontend/src/modules/swap/pure/SwapButtons/index.tsx +++ b/apps/cowswap-frontend/src/modules/swap/pure/SwapButtons/index.tsx @@ -3,7 +3,7 @@ import React, { ReactNode } from 'react' import { GpEther } from '@cowprotocol/common-const' import { genericPropsChecker } from '@cowprotocol/common-utils' import { SupportedChainId } from '@cowprotocol/cow-sdk' -import { ButtonSize, TokenSymbol, ButtonError, ButtonPrimary, AutoRow } from '@cowprotocol/ui' +import { AutoRow, ButtonError, ButtonPrimary, ButtonSize, TokenSymbol } from '@cowprotocol/ui' import { Currency, CurrencyAmount, Token } from '@uniswap/sdk-core' import { Trans } from '@lingui/macro' diff --git a/apps/cowswap-frontend/src/modules/swap/services/swapFlow/index.ts b/apps/cowswap-frontend/src/modules/swap/services/swapFlow/index.ts index 98de4a2c54..326f3889ed 100644 --- a/apps/cowswap-frontend/src/modules/swap/services/swapFlow/index.ts +++ b/apps/cowswap-frontend/src/modules/swap/services/swapFlow/index.ts @@ -27,9 +27,10 @@ export async function swapFlow( try { logTradeFlow('SWAP FLOW', 'STEP 2: handle permit') + if (input.permitInfo) input.swapConfirmManager.requestPermitSignature() + input.orderParams.appData = await handlePermit({ appData: input.orderParams.appData, - hasEnoughAllowance: input.hasEnoughAllowance, inputToken: input.context.trade.inputAmount.currency as Token, provider: input.orderParams.signer.provider as Web3Provider, @@ -37,6 +38,7 @@ export async function swapFlow( chainId: input.orderParams.chainId, permitInfo: input.permitInfo, }) + input.swapConfirmManager.permitSigned() logTradeFlow('SWAP FLOW', 'STEP 3: send transaction') tradeFlowAnalytics.trade(input.swapFlowAnalyticsContext) diff --git a/apps/cowswap-frontend/src/modules/swap/services/types.ts b/apps/cowswap-frontend/src/modules/swap/services/types.ts index 0b09b45fe1..6417e077aa 100644 --- a/apps/cowswap-frontend/src/modules/swap/services/types.ts +++ b/apps/cowswap-frontend/src/modules/swap/services/types.ts @@ -1,5 +1,4 @@ -import { GPv2Settlement, CoWSwapEthFlow } from '@cowprotocol/abis' -import { Erc20, Weth } from '@cowprotocol/abis' +import { CoWSwapEthFlow, Erc20, GPv2Settlement, Weth } from '@cowprotocol/abis' import { Web3Provider } from '@ethersproject/providers' import SafeAppsSDK from '@safe-global/safe-apps-sdk' import { Currency, CurrencyAmount } from '@uniswap/sdk-core' @@ -46,7 +45,6 @@ export interface BaseFlowContext { export type SwapFlowContext = BaseFlowContext & { contract: GPv2Settlement permitInfo: IsTokenPermittableResult - hasEnoughAllowance: boolean | undefined } export type EthFlowContext = BaseFlowContext & { diff --git a/apps/cowswap-frontend/src/modules/swap/state/swapConfirmAtom.ts b/apps/cowswap-frontend/src/modules/swap/state/swapConfirmAtom.ts index c69a80f056..0bd8d19589 100644 --- a/apps/cowswap-frontend/src/modules/swap/state/swapConfirmAtom.ts +++ b/apps/cowswap-frontend/src/modules/swap/state/swapConfirmAtom.ts @@ -8,6 +8,7 @@ export interface SwapConfirmState { attemptingTxn: boolean swapErrorMessage: string | undefined txHash: string | undefined + permitSignatureState: undefined | 'requested' | 'signed' } export const swapConfirmAtom = atom({ @@ -16,4 +17,5 @@ export const swapConfirmAtom = atom({ attemptingTxn: false, swapErrorMessage: undefined, txHash: undefined, + permitSignatureState: undefined, }) diff --git a/libs/ui/src/pure/TokenAmount/index.tsx b/libs/ui/src/pure/TokenAmount/index.tsx index 6046121b82..c6e9917427 100644 --- a/libs/ui/src/pure/TokenAmount/index.tsx +++ b/libs/ui/src/pure/TokenAmount/index.tsx @@ -1,6 +1,5 @@ import { LONG_PRECISION } from '@cowprotocol/common-const' -import { formatTokenAmount, FractionUtils } from '@cowprotocol/common-utils' -import { FeatureFlag } from '@cowprotocol/common-utils' +import { FeatureFlag, formatTokenAmount, FractionUtils } from '@cowprotocol/common-utils' import { darken, transparentize } from 'polished' import styled from 'styled-components' @@ -22,7 +21,7 @@ export const SymbolElement = styled.span<{ opacitySymbol?: boolean }>` export interface TokenAmountProps { amount: Nullish defaultValue?: string - tokenSymbol?: Nullish + tokenSymbol?: TokenSymbolProps['token'] className?: string hideTokenSymbol?: boolean round?: boolean diff --git a/package.json b/package.json index 87f4ad382b..7ac20597d8 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "start": "nx run cowswap-frontend:serve", "preview": "cross-env NODE_OPTIONS=--max-old-space-size=32768 nx run cowswap-frontend:preview", "cosmos:export": "cross-env NODE_OPTIONS=--max-old-space-size=32768 nx run cowswap-frontend:cosmos:export", + "cosmos": "nx run cowswap-frontend:cosmos:run", "test": "nx run-many -t test --output-style=stream", "e2e": "nx run-many -t e2e", "lint": "nx run-many -t lint",