Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/olive-peaches-brush.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@reservoir0x/relay-kit-ui': patch
---

Add approve + swap ux flow and update cta copy in widget and modal
1 change: 1 addition & 0 deletions packages/ui/panda.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}' },
Expand Down
30 changes: 26 additions & 4 deletions packages/ui/src/components/common/TransactionModal/SwapModal.tsx
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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
Expand Down Expand Up @@ -181,6 +182,7 @@ const InnerSwapModal: FC<InnerSwapModalProps> = ({
setSwapError,
progressStep,
setProgressStep,
steps,
setSteps,
currentStep,
setCurrentStep,
Expand All @@ -204,6 +206,11 @@ const InnerSwapModal: FC<InnerSwapModalProps> = ({
waitingForSteps,
isLoadingTransaction
}) => {
const firstStep = quote?.steps?.[0]
const isApprovalPlusSwap =
firstStep?.id === 'approve' &&
firstStep?.items?.[0]?.status === 'incomplete'

useEffect(() => {
if (!open) {
if (currentStep) {
Expand Down Expand Up @@ -266,7 +273,7 @@ const InnerSwapModal: FC<InnerSwapModalProps> = ({
}}
>
<Text style="h6" css={{ mb: 8 }}>
{isReviewQuoteStep ? 'Review Quote' : 'Trade Details'}
{isReviewQuoteStep ? 'Review Quote' : 'Transaction Details'}
</Text>

{progressStep === TransactionProgressStep.ReviewQuote ? (
Expand All @@ -289,7 +296,21 @@ const InnerSwapModal: FC<InnerSwapModalProps> = ({
/>
) : null}

{progressStep === TransactionProgressStep.WalletConfirmation ? (
{(progressStep === TransactionProgressStep.WalletConfirmation ||
progressStep === TransactionProgressStep.Validating) &&
isApprovalPlusSwap ? (
<ApprovalPlusSwapStep
fromToken={fromToken}
toToken={toToken}
fromAmountFormatted={fromAmountFormatted}
toAmountFormatted={toAmountFormatted}
steps={steps}
quote={quote}
/>
) : null}

{progressStep === TransactionProgressStep.WalletConfirmation &&
!isApprovalPlusSwap ? (
<SwapConfirmationStep
fromToken={fromToken}
toToken={toToken}
Expand All @@ -298,7 +319,8 @@ const InnerSwapModal: FC<InnerSwapModalProps> = ({
quote={quote}
/>
) : null}
{progressStep === TransactionProgressStep.Validating ? (
{progressStep === TransactionProgressStep.Validating &&
!isApprovalPlusSwap ? (
<ValidatingStep
currentStep={currentStep}
currentStepItem={currentStepItem}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,247 @@
import { type FC } from 'react'
import {
Flex,
Text,
ChainTokenIcon,
Box,
Anchor
} from '../../../primitives/index.js'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { LoadingSpinner } from '../../LoadingSpinner.js'
import { type Token } from '../../../../types/index.js'
import { faArrowRight } from '@fortawesome/free-solid-svg-icons/faArrowRight'
import type { useQuote } from '@reservoir0x/relay-kit-hooks'
import { formatDollar } from '../../../../utils/numbers.js'
import { faCheck } from '@fortawesome/free-solid-svg-icons/faCheck'
import type { Execute } from '@reservoir0x/relay-sdk'
import { faRepeat } from '@fortawesome/free-solid-svg-icons'
import { truncateAddress } from '../../../../utils/truncate.js'
import { getTxBlockExplorerUrl } from '../../../../utils/getTxBlockExplorerUrl.js'
import useRelayClient from '../../../../hooks/useRelayClient.js'

type ApprovalPlusSwapStepProps = {
fromToken?: Token
toToken?: Token
quote?: ReturnType<typeof useQuote>['data']
fromAmountFormatted: string
toAmountFormatted: string
steps: Execute['steps'] | null
}

export const ApprovalPlusSwapStep: FC<ApprovalPlusSwapStepProps> = ({
fromToken,
toToken,
quote,
fromAmountFormatted,
toAmountFormatted,
steps
}) => {
const details = quote?.details
const relayClient = useRelayClient()

return (
<>
<Flex
align="center"
justify="between"
direction="column"
css={{ flexShrink: 0, bp500: { flexDirection: 'row' } }}
>
<Flex
direction="column"
css={{
backgroundColor: 'subtle-background-color',
p: '12px 16px',
borderRadius: 12,
gap: 1,
width: '100%'
}}
>
<Flex
direction="column"
align="start"
css={{ gap: '1', cursor: 'pointer' }}
>
<ChainTokenIcon
chainId={fromToken?.chainId}
tokenlogoURI={fromToken?.logoURI}
css={{ height: 32, width: 32 }}
/>
<Text style="h6" ellipsify>
{fromAmountFormatted} {fromToken?.symbol}
</Text>
<Text style="subtitle3" color="subtle">
{formatDollar(Number(details?.currencyIn?.amountUsd))}
</Text>
</Flex>
</Flex>
<Text
style="body1"
css={{
color: 'gray9',
p: '0 16px',
bp400Down: { transform: 'rotate(90deg)' }
}}
>
<FontAwesomeIcon icon={faArrowRight} width={16} />
</Text>
<Flex
direction="column"
css={{
backgroundColor: 'subtle-background-color',
p: '12px 16px',
borderRadius: 12,
gap: 1,
width: '100%'
}}
>
<Flex
direction="column"
align="start"
css={{ gap: '1', cursor: 'pointer' }}
>
<ChainTokenIcon
chainId={toToken?.chainId}
tokenlogoURI={toToken?.logoURI}
css={{ height: 32, width: 32 }}
/>
<Text style="h6" ellipsify>
{toAmountFormatted} {toToken?.symbol}
</Text>
<Text style="subtitle3" color="subtle">
{formatDollar(Number(details?.currencyOut?.amountUsd))}
</Text>
</Flex>
</Flex>
</Flex>
<Flex
direction="column"
css={{
'--borderColor': 'colors.gray3',
border: '1px solid var(--borderColor)',
borderRadius: 12,
p: '3',
height: 260,
gap: '8px'
}}
>
{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 (
<Box key={step.id}>
<Flex
align="center"
justify="between"
css={{ width: '100%', gap: '3' }}
>
<Flex align="center" css={{ gap: '2', height: 40 }}>
{step.id === 'approve' ? (
<ChainTokenIcon
chainId={fromToken?.chainId}
tokenlogoURI={fromToken?.logoURI}
css={{
borderRadius: 9999999,
flexShrink: 0,
filter: isCurrentStep ? 'none' : 'grayscale(100%)'
}}
/>
) : (
<Flex
css={{
height: 32,
width: 32,
borderRadius: 9999999,
flexShrink: 0,
backgroundColor: isCurrentStep ? 'primary5' : 'gray5',
color: isCurrentStep ? 'primary8' : 'gray9',
alignItems: 'center',
justifyContent: 'center'
}}
>
<FontAwesomeIcon icon={faRepeat} width={16} />
</Flex>
)}
<Flex direction="column" css={{ gap: '2px' }}>
<Text style="subtitle2">{stepTitle}</Text>
{isApproveStep && !hasTxHash && (
<Anchor
css={{ fontSize: 12 }}
href="https://support.relay.link/en/articles/10371133-why-do-i-have-to-approve-a-token"
target="_blank"
>
Why do I have to approve a token?
</Anchor>
)}
{hasTxHash &&
step?.items?.[0]?.txHashes?.map(({ txHash, chainId }) => {
const txUrl = getTxBlockExplorerUrl(
chainId,
relayClient?.chains,
txHash
)
return (
<Anchor
key={txHash}
href={txUrl}
target="_blank"
css={{ fontSize: 12 }}
>
View Tx: {truncateAddress(txHash, '...', 6, 4)}
</Anchor>
)
})}
</Flex>
</Flex>

<Flex>
{isCurrentStep && hasTxHash ? (
<LoadingSpinner
css={{ height: 16, width: 16, fill: 'gray9' }}
/>
) : step?.items?.every(
(item) => item.status === 'complete'
) ? (
<Box css={{ color: 'green9' }}>
<FontAwesomeIcon icon={faCheck} width={16} />
</Box>
) : null}
</Flex>
</Flex>

{index !== (steps?.length || 0) - 1 && (
<Box css={{ height: '14px', pl: '16px', marginTop: '12px' }}>
<Box
css={{
width: '1px',
height: '100%',
backgroundColor: 'gray11'
}}
/>
</Box>
)}
</Box>
)
})}
</Flex>
</>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,41 @@ export const ReviewQuoteStep: FC<ReviewQuoteProps> = ({
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(
Expand Down Expand Up @@ -509,7 +544,7 @@ export const ReviewQuoteStep: FC<ReviewQuoteProps> = ({
disabled={isFetchingQuote || isRefetchingQuote || waitingForSteps}
onClick={() => swap?.()}
>
Confirm
{ctaCopy}
</Button>
</>
)
Expand Down
2 changes: 1 addition & 1 deletion packages/ui/src/components/primitives/ChainTokenIcon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ export const ChainTokenIcon: FC<ChainTokenProps> = ({
height={14}
css={{
position: 'absolute',
right: -1,
right: 0,
bottom: 0,
borderRadius: 4,
overflow: 'hidden',
Expand Down
Loading
Loading