diff --git a/.changeset/olive-peaches-brush.md b/.changeset/olive-peaches-brush.md new file mode 100644 index 000000000..b0a9aaf8e --- /dev/null +++ b/.changeset/olive-peaches-brush.md @@ -0,0 +1,5 @@ +--- +'@reservoir0x/relay-kit-ui': patch +--- + +Add approve + swap ux flow and update cta copy in widget and modal diff --git a/packages/ui/panda.config.ts b/packages/ui/panda.config.ts index 9213f715e..bd9bf8143 100644 --- a/packages/ui/panda.config.ts +++ b/packages/ui/panda.config.ts @@ -46,6 +46,7 @@ export const Colors = { // Green green2: { value: '{colors.green.2}' }, green3: { value: '{colors.green.3}' }, + green9: { value: '{colors.green.9}' }, green10: { value: '{colors.green.10}' }, green11: { value: '{colors.green.11}' }, green12: { value: '{colors.green.12}' }, diff --git a/packages/ui/src/components/common/TransactionModal/SwapModal.tsx b/packages/ui/src/components/common/TransactionModal/SwapModal.tsx index 15cbde351..de8613f66 100644 --- a/packages/ui/src/components/common/TransactionModal/SwapModal.tsx +++ b/packages/ui/src/components/common/TransactionModal/SwapModal.tsx @@ -1,6 +1,6 @@ import type { AdaptedWallet, Execute, RelayChain } from '@reservoir0x/relay-sdk' import { type Address } from 'viem' -import { type FC, useEffect } from 'react' +import { type FC, useEffect, useState } from 'react' import { type ChildrenProps, TransactionModalRenderer, @@ -19,6 +19,7 @@ import { formatBN } from '../../../utils/numbers.js' import type { TradeType } from '../../../components/widgets/SwapWidgetRenderer.js' import { extractQuoteId } from '../../../utils/quote.js' import type { LinkedWallet } from '../../../types/index.js' +import { ApprovalPlusSwapStep } from './steps/ApprovalPlusSwapStep.js' type SwapModalProps = { open: boolean @@ -181,6 +182,7 @@ const InnerSwapModal: FC = ({ setSwapError, progressStep, setProgressStep, + steps, setSteps, currentStep, setCurrentStep, @@ -204,6 +206,11 @@ const InnerSwapModal: FC = ({ waitingForSteps, isLoadingTransaction }) => { + const firstStep = quote?.steps?.[0] + const isApprovalPlusSwap = + firstStep?.id === 'approve' && + firstStep?.items?.[0]?.status === 'incomplete' + useEffect(() => { if (!open) { if (currentStep) { @@ -266,7 +273,7 @@ const InnerSwapModal: FC = ({ }} > - {isReviewQuoteStep ? 'Review Quote' : 'Trade Details'} + {isReviewQuoteStep ? 'Review Quote' : 'Transaction Details'} {progressStep === TransactionProgressStep.ReviewQuote ? ( @@ -289,7 +296,21 @@ const InnerSwapModal: FC = ({ /> ) : null} - {progressStep === TransactionProgressStep.WalletConfirmation ? ( + {(progressStep === TransactionProgressStep.WalletConfirmation || + progressStep === TransactionProgressStep.Validating) && + isApprovalPlusSwap ? ( + + ) : null} + + {progressStep === TransactionProgressStep.WalletConfirmation && + !isApprovalPlusSwap ? ( = ({ quote={quote} /> ) : null} - {progressStep === TransactionProgressStep.Validating ? ( + {progressStep === TransactionProgressStep.Validating && + !isApprovalPlusSwap ? ( ['data'] + fromAmountFormatted: string + toAmountFormatted: string + steps: Execute['steps'] | null +} + +export const ApprovalPlusSwapStep: FC = ({ + fromToken, + toToken, + quote, + fromAmountFormatted, + toAmountFormatted, + steps +}) => { + const details = quote?.details + const relayClient = useRelayClient() + + return ( + <> + + + + + + {fromAmountFormatted} {fromToken?.symbol} + + + {formatDollar(Number(details?.currencyIn?.amountUsd))} + + + + + + + + + + + {toAmountFormatted} {toToken?.symbol} + + + {formatDollar(Number(details?.currencyOut?.amountUsd))} + + + + + + {steps?.map((step, index) => { + const isCurrentStep = + step.items?.some((item) => item.status === 'incomplete') && + !steps + ?.slice(0, steps?.indexOf(step)) + ?.some((s) => + s.items?.some((item) => item.status === 'incomplete') + ) + + const hasTxHash = + step?.items?.[0]?.txHashes?.length && + step?.items?.[0]?.txHashes?.length > 0 + + const isApproveStep = step.id === 'approve' + + const stepTitle = isApproveStep + ? 'Approve in wallet' + : hasTxHash + ? `Swapping ${fromToken?.symbol} for ${toToken?.symbol}` + : 'Confirm swap in wallet' + + return ( + + + + {step.id === 'approve' ? ( + + ) : ( + + + + )} + + {stepTitle} + {isApproveStep && !hasTxHash && ( + + Why do I have to approve a token? + + )} + {hasTxHash && + step?.items?.[0]?.txHashes?.map(({ txHash, chainId }) => { + const txUrl = getTxBlockExplorerUrl( + chainId, + relayClient?.chains, + txHash + ) + return ( + + View Tx: {truncateAddress(txHash, '...', 6, 4)} + + ) + })} + + + + + {isCurrentStep && hasTxHash ? ( + + ) : step?.items?.every( + (item) => item.status === 'complete' + ) ? ( + + + + ) : null} + + + + {index !== (steps?.length || 0) - 1 && ( + + + + )} + + ) + })} + + + ) +} diff --git a/packages/ui/src/components/common/TransactionModal/steps/ReviewQuoteStep.tsx b/packages/ui/src/components/common/TransactionModal/steps/ReviewQuoteStep.tsx index 0352a41b7..b63d5dda5 100644 --- a/packages/ui/src/components/common/TransactionModal/steps/ReviewQuoteStep.tsx +++ b/packages/ui/src/components/common/TransactionModal/steps/ReviewQuoteStep.tsx @@ -114,6 +114,41 @@ export const ReviewQuoteStep: FC = ({ return () => clearInterval(interval) }, [quoteUpdatedAt]) + const firstStep = quote?.steps?.[0] + const firstStepItem = firstStep?.items?.[0] + + let ctaCopy: string = 'Confirm' + if (firstStep?.id === 'approve' && firstStepItem?.status === 'incomplete') { + ctaCopy = 'Approve & Swap' + } else { + switch (details?.operation) { + case 'wrap': { + ctaCopy = 'Wrap' + break + } + case 'unwrap': { + ctaCopy = 'Unwrap' + break + } + case 'send': { + ctaCopy = 'Send' + break + } + case 'swap': { + ctaCopy = 'Swap' + break + } + case 'bridge': { + ctaCopy = 'Bridge' + break + } + default: { + ctaCopy = 'Confirm' + break + } + } + } + let breakdown: { title: string; value: ReactNode }[] = [] const slippage = Number( @@ -509,7 +544,7 @@ export const ReviewQuoteStep: FC = ({ disabled={isFetchingQuote || isRefetchingQuote || waitingForSteps} onClick={() => swap?.()} > - Confirm + {ctaCopy} ) diff --git a/packages/ui/src/components/primitives/ChainTokenIcon.tsx b/packages/ui/src/components/primitives/ChainTokenIcon.tsx index b8f6ed480..c82394b39 100644 --- a/packages/ui/src/components/primitives/ChainTokenIcon.tsx +++ b/packages/ui/src/components/primitives/ChainTokenIcon.tsx @@ -54,7 +54,7 @@ export const ChainTokenIcon: FC = ({ height={14} css={{ position: 'absolute', - right: -1, + right: 0, bottom: 0, borderRadius: 4, overflow: 'hidden', diff --git a/packages/ui/src/components/widgets/SwapWidgetRenderer.tsx b/packages/ui/src/components/widgets/SwapWidgetRenderer.tsx index 5929c70d9..00f04adf3 100644 --- a/packages/ui/src/components/widgets/SwapWidgetRenderer.tsx +++ b/packages/ui/src/components/widgets/SwapWidgetRenderer.tsx @@ -626,31 +626,7 @@ const SwapWidgetRenderer: FC = ({ const maxCapacityWei = maxCapacity?.value const maxCapacityFormatted = maxCapacity?.formatted - let ctaCopy: string = context || 'Swap' - - switch (operation) { - case 'wrap': { - ctaCopy = 'Wrap' - break - } - case 'unwrap': { - ctaCopy = 'Unwrap' - break - } - case 'send': { - ctaCopy = 'Send' - break - } - case 'swap': - default: { - if (context === 'Swap') { - ctaCopy = 'Trade' - } else { - ctaCopy = context === 'Deposit' ? 'Deposit' : 'Withdraw' - } - break - } - } + let ctaCopy: string = 'Review' if (!fromToken || !toToken) { ctaCopy = 'Select a token' @@ -677,29 +653,7 @@ const SwapWidgetRenderer: FC = ({ } else if (!toChainWalletVMSupported && !isValidToAddress) { ctaCopy = `Enter ${toChain.displayName} Address` } else if (transactionModalOpen) { - switch (operation) { - case 'wrap': { - ctaCopy = 'Wrapping' - break - } - case 'unwrap': { - ctaCopy = 'Unwrapping' - break - } - case 'send': { - ctaCopy = 'Sending' - break - } - case 'swap': - default: { - if (context === 'Swap') { - ctaCopy = 'Swap' - } else { - ctaCopy = context === 'Deposit' ? 'Depositing' : 'Withdrawing' - } - break - } - } + ctaCopy = 'Review' } usePreviousValueChange(