Skip to content
Open
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
8 changes: 8 additions & 0 deletions .changeset/checkout-exchange-destination.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
"@lifi/widget-checkout": minor
"@lifi/widget-provider": minor
"@lifi/widget-provider-mesh": minor
"@lifi/widget": patch
---

Let users reconnect previously linked exchange accounts and set their own destination address in the checkout flow.
2 changes: 2 additions & 0 deletions packages/widget-checkout/src/CheckoutLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,11 @@ import { CheckoutToastHost } from './components/CheckoutToastHost.js'
import { Container, ExpandedContainer } from './components/Container.js'
import { Header } from './components/Header.js'
import { OnRampHostedModals } from './components/OnRampHostedModals.js'
import { useSyncCheckoutRecipientToForm } from './hooks/useSyncCheckoutRecipientToForm.js'

export const CheckoutLayout: React.FC = () => {
const { elementId } = useWidgetConfig()
useSyncCheckoutRecipientToForm()

return (
<ExpandedContainer
Expand Down
8 changes: 8 additions & 0 deletions packages/widget-checkout/src/CheckoutRouter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { ProgressPage } from './pages/ProgressPage/ProgressPage.js'
import { SelectCashCurrencyPage } from './pages/SelectCashCurrencyPage/SelectCashCurrencyPage.js'
import { SelectSourcePage } from './pages/SelectSourcePage/SelectSourcePage.js'
import { SelectTokenPage } from './pages/SelectTokenPage/SelectTokenPage.js'
import { SetDestinationAddressPage } from './pages/SetDestinationAddressPage/SetDestinationAddressPage.js'
import { TransferDepositPage } from './pages/TransferDepositPage/TransferDepositPage.js'
import { CheckoutFlowStoreContext } from './stores/useCheckoutFlowStore.js'
import {
Expand All @@ -46,6 +47,12 @@ const enterAmountRoute = createRoute({
component: EnterAmountPage,
})

const setDestinationRoute = createRoute({
getParentRoute: () => rootRoute,
path: checkoutNavigationRoutes.setDestination,
component: SetDestinationAddressPage,
})

const progressRoute = createRoute({
getParentRoute: () => rootRoute,
path: checkoutNavigationRoutes.progress,
Expand Down Expand Up @@ -147,6 +154,7 @@ const transactionExecutionStatusRoute = createRoute({
const routeTree = rootRoute.addChildren([
indexRoute,
enterAmountRoute,
setDestinationRoute,
progressRoute,
transferDepositRoute,
depositErrorRoute,
Expand Down
2 changes: 2 additions & 0 deletions packages/widget-checkout/src/LifiWidgetCheckout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,15 @@ export const LifiWidgetCheckout: ForwardRefExoticComponent<
onError: props.onError,
config: props.config,
resumePending: props.resumePending,
allowUserDestinationAddress: props.allowUserDestinationAddress,
}),
[
props.integrator,
props.onSuccess,
props.onError,
props.config,
props.resumePending,
props.allowUserDestinationAddress,
]
)

Expand Down
19 changes: 16 additions & 3 deletions packages/widget-checkout/src/components/CheckoutFlowCtaButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import { useCheckoutFlowQuote } from '../hooks/useCheckoutFlowQuote.js'
import { useFrozenQuote } from '../hooks/useFrozenQuote.js'
import { useResolvedCheckoutRecipient } from '../hooks/useResolvedCheckoutRecipient.js'
import { useOnRampSessionByCategory } from '../providers/OnRampProvider/OnRampProvider.js'
import {
type CheckoutFundingSource,
Expand All @@ -33,15 +34,19 @@ const ctaLabelKey = {
const statusPath = `/${checkoutNavigationRoutes.transactionExecution}/${checkoutNavigationRoutes.transactionStatus}`

export const CheckoutFlowCtaButton: React.FC = (): JSX.Element => {
const { t } = useTranslation()
const { t, i18n } = useTranslation()
const navigate = useNavigate()
const emitter = useWidgetEvents()
const { toAddress, requiredToAddress } = useToAddressRequirements()
const { recipient, isUserSettable } = useResolvedCheckoutRecipient()
const { route, routes, depositAddress, setReviewableRoute } =
useCheckoutFlowQuote()
const { freeze } = useFrozenQuote()
const fundingSource = useCheckoutFlowStore((s) => s.fundingSource) ?? 'wallet'
const setFrozenRouteId = useCheckoutFlowStore((s) => s.setFrozenRouteId)
const selectedExchangeAccount = useCheckoutFlowStore(
(s) => s.selectedExchangeAccount
)
const fiatCurrency = useFiatCurrencyStore((s) => s.currency)
const onRampSession = useOnRampSessionByCategory(
fundingSource === 'cash' || fundingSource === 'exchange'
Expand Down Expand Up @@ -99,6 +104,10 @@ export const CheckoutFlowCtaButton: React.FC = (): JSX.Element => {
fiatAmount,
fromChainId: route.fromChainId,
fromTokenAddress: route.fromToken.address,
accessTokens: selectedExchangeAccount
? [selectedExchangeAccount]
: undefined,
language: i18n.language,
})
navigate({
to: statusPath,
Expand All @@ -115,6 +124,8 @@ export const CheckoutFlowCtaButton: React.FC = (): JSX.Element => {
setFrozenRouteId,
fiatCurrency,
navigate,
selectedExchangeAccount,
i18n.language,
])

const handlersByFunding: Record<CheckoutFundingSource, () => void> = {
Expand All @@ -126,13 +137,15 @@ export const CheckoutFlowCtaButton: React.FC = (): JSX.Element => {

const label = t(ctaLabelKey[fundingSource])

const needsRecipient = isUserSettable && !recipient

// Only the wallet flow may connect-on-demand; other sources fund without a wallet.
if (fundingSource === 'wallet') {
return (
<BaseTransactionButton
text={label}
onClick={handleWalletDeposit}
disabled={!route || (requiredToAddress && !toAddress)}
disabled={!route || (requiredToAddress && !toAddress) || needsRecipient}
route={route}
sx={{ flex: 1 }}
/>
Expand All @@ -145,7 +158,7 @@ export const CheckoutFlowCtaButton: React.FC = (): JSX.Element => {
color="primary"
fullWidth
onClick={handlersByFunding[fundingSource]}
disabled={!route || !depositAddress}
disabled={!route || !depositAddress || needsRecipient}
sx={{ flex: 1 }}
>
{label}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ import {
TokenRate,
useChain,
useFieldValues,
useRoutes,
useToken,
} from '@lifi/widget/shared'
import AccessTimeFilled from '@mui/icons-material/AccessTimeFilled'
Expand All @@ -25,6 +24,7 @@ import LocalGasStationRounded from '@mui/icons-material/LocalGasStationRounded'
import { Box, Collapse, IconButton, Skeleton, Typography } from '@mui/material'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useCheckoutRoutes } from '../hooks/useCheckoutRoutes.js'
export const CheckoutReceiveCard: React.FC = () => {
const { t, i18n } = useTranslation()
const [expanded, setExpanded] = useState(false)
Expand All @@ -42,7 +42,7 @@ export const CheckoutReceiveCard: React.FC = () => {
isFetched,
dataUpdatedAt,
refetchTime,
} = useRoutes()
} = useCheckoutRoutes()

const parsedAmount = Number.parseFloat(
typeof fromAmount === 'string'
Expand Down
88 changes: 88 additions & 0 deletions packages/widget-checkout/src/components/CheckoutRecipientCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import {
ChainAvatar,
shortenAddress,
useChain,
useToken,
useWidgetConfig,
} from '@lifi/widget/shared'
import CloseIcon from '@mui/icons-material/Close'
import { Box, Card, Chip, IconButton, Typography } from '@mui/material'
import type { JSX } from 'react'
import { useTranslation } from 'react-i18next'
import { useCheckoutNavigate } from '../hooks/useCheckoutNavigate.js'
import { useResolvedCheckoutRecipient } from '../hooks/useResolvedCheckoutRecipient.js'
import { checkoutNavigationRoutes } from '../utils/navigationRoutes.js'

export const CheckoutRecipientCard: React.FC = (): JSX.Element | null => {
const { t } = useTranslation()
const navigate = useCheckoutNavigate()
const { recipient, isUserSettable, clearUserRecipient } =
useResolvedCheckoutRecipient()
const { toChain, toToken } = useWidgetConfig()
const { token } = useToken(toChain, toToken)
const { chain: destinationChain } = useChain(toChain)

if (!isUserSettable) {
return null
}

if (!recipient) {
return (
<Card
variant="outlined"
onClick={() =>
navigate({ to: checkoutNavigationRoutes.setDestination })
}
sx={{ p: 2, cursor: 'pointer' }}
>
<Box
sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
mb: 1,
}}
>
<Typography variant="subtitle2" sx={{ fontWeight: 700 }}>
{t('checkout.whereToSendIt')}
</Typography>
<Chip
size="small"
color="warning"
variant="outlined"
label={t('checkout.required')}
/>
</Box>
<Typography variant="body2" color="text.secondary">
{t('checkout.addWalletToReceive', { token: token?.symbol ?? '' })}
</Typography>
</Card>
)
}

return (
<Card variant="outlined" sx={{ p: 2 }}>
<Typography variant="subtitle2" sx={{ fontWeight: 700, mb: 1.5 }}>
{t('checkout.sendingTo')}
</Typography>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5 }}>
<ChainAvatar
src={destinationChain?.logoURI}
alt={destinationChain?.name}
>
{destinationChain?.name?.[0] ?? '?'}
</ChainAvatar>
<Typography variant="body1" sx={{ flex: 1, fontWeight: 500 }}>
{shortenAddress(recipient.address)}
</Typography>
<IconButton
size="small"
aria-label={t('button.close')}
onClick={clearUserRecipient}
>
<CloseIcon fontSize="small" />
</IconButton>
</Box>
</Card>
)
}
8 changes: 5 additions & 3 deletions packages/widget-checkout/src/hooks/useCheckoutConfigError.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
import { useWidgetConfig } from '@lifi/widget/shared'
import { useMemo } from 'react'
import { useCheckoutToAddress } from './useCheckoutToAddress.js'
import { useResolvedCheckoutRecipient } from './useResolvedCheckoutRecipient.js'

/** Returns the names of required checkout config fields (toAddress/toChain/toToken) that are missing. */
/** Required config fields that are missing. `toAddress` is fatal only when the user can't set it in-widget. */
export function useCheckoutConfigError(): string[] {
const toAddress = useCheckoutToAddress()
const { isUserSettable } = useResolvedCheckoutRecipient()
const { toChain, toToken } = useWidgetConfig()

return useMemo(() => {
const missing: string[] = []
if (!toAddress) {
if (!toAddress && !isUserSettable) {
missing.push('toAddress')
}
if (!toChain) {
Expand All @@ -19,5 +21,5 @@ export function useCheckoutConfigError(): string[] {
missing.push('toToken')
}
return missing
}, [toAddress, toChain, toToken])
}, [toAddress, isUserSettable, toChain, toToken])
}
5 changes: 3 additions & 2 deletions packages/widget-checkout/src/hooks/useCheckoutFlowQuote.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import type { LiFiStep, Route } from '@lifi/sdk'
import { getStepTransaction } from '@lifi/sdk'
import { useRoutes, useSDKClient } from '@lifi/widget/shared'
import { useSDKClient } from '@lifi/widget/shared'
import { useQuery } from '@tanstack/react-query'
import { useMemo } from 'react'
import { extractDepositAddress } from '../utils/extractDepositAddress.js'
import { useCheckoutRoutes } from './useCheckoutRoutes.js'

export interface CheckoutFlowQuote {
route: Route | undefined
Expand All @@ -26,7 +27,7 @@ export function useCheckoutFlowQuote(): CheckoutFlowQuote {
isFetched: routesFetched,
refetch,
setReviewableRoute,
} = useRoutes()
} = useCheckoutRoutes()

const rawRoute = routes?.[0]
const firstStep = rawRoute?.steps?.[0]
Expand Down
Loading