Skip to content

Commit

Permalink
feat: add new chain selection view
Browse files Browse the repository at this point in the history
  • Loading branch information
chybisov committed Sep 5, 2022
1 parent 287fc3b commit 3c4d3fe
Show file tree
Hide file tree
Showing 35 changed files with 539 additions and 360 deletions.
15 changes: 15 additions & 0 deletions packages/widget/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { NotFound } from './components/NotFound';
import { PoweredBy } from './components/PoweredBy';
import { ActiveSwapsPage } from './pages/ActiveSwapsPage';
import { MainPage } from './pages/MainPage';
import { SelectChainPage } from './pages/SelectChainPage';
import { SelectTokenPage } from './pages/SelectTokenPage';
import { SelectWalletPage } from './pages/SelectWalletPage';
import { SettingsPage } from './pages/SettingsPage';
Expand Down Expand Up @@ -56,6 +57,20 @@ const AppRoutes = () => {
path: navigationRoutes.toToken,
element: <SelectTokenPage formType="to" />,
},
...[
navigationRoutes.fromChain,
`${navigationRoutes.fromToken}/${navigationRoutes.fromChain}`,
].map((path) => ({
path,
element: <SelectChainPage formType="from" />,
})),
...[
navigationRoutes.toChain,
`${navigationRoutes.toToken}/${navigationRoutes.toChain}`,
].map((path) => ({
path,
element: <SelectChainPage formType="to" />,
})),
{
path: navigationRoutes.swapRoutes,
element: <SwapRoutesPage />,
Expand Down
4 changes: 2 additions & 2 deletions packages/widget/src/components/ActiveSwaps/ActiveSwapItem.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import {
ArrowForward as ArrowForwardIcon,
Info as InfoIcon,
Warning as WarningIcon
Warning as WarningIcon,
} from '@mui/icons-material';
import { ListItemAvatar, ListItemText, Typography } from '@mui/material';
import { useNavigate } from 'react-router-dom';
Expand All @@ -16,7 +16,7 @@ export const ActiveSwapItem: React.FC<{
dense?: boolean;
}> = ({ routeId, dense }) => {
const navigate = useNavigate();
const { route, status } = useRouteExecution(routeId);
const { route, status } = useRouteExecution(routeId, true);

// TODO: replace with ES2023 findLast
const lastActiveStep = route?.steps
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import {
Button,
ListItem as MuiListItem,
ListItemButton as MuiListItemButton
ListItemButton as MuiListItemButton,
} from '@mui/material';
import { listItemSecondaryActionClasses } from '@mui/material/ListItemSecondaryAction';
import { alpha, styled } from '@mui/material/styles';
Expand All @@ -18,17 +18,16 @@ export const ProgressCard = styled(Card)(({ theme }) => ({

export const ListItemButton = styled(MuiListItemButton)(({ theme }) => ({
borderRadius: theme.shape.borderRadiusSecondary,
paddingLeft: theme.spacing(1),
paddingRight: theme.spacing(1),
paddingLeft: theme.spacing(1.5),
paddingRight: theme.spacing(1.5),
height: 64,
'&:hover': {
backgroundColor: getContrastAlphaColor(theme, '4%'),
},
}));

export const ListItem = styled(MuiListItem, {
shouldForwardProp: (prop) =>
prop !== 'disableRipple',
shouldForwardProp: (prop) => prop !== 'disableRipple',
})(({ theme }) => ({
padding: theme.spacing(0, 1.5),
[`.${listItemSecondaryActionClasses.root}`]: {
Expand Down
77 changes: 46 additions & 31 deletions packages/widget/src/components/Card/Card.tsx
Original file line number Diff line number Diff line change
@@ -1,41 +1,56 @@
import type { Theme } from '@mui/material';
import { Box } from '@mui/material';
import { darken, lighten, styled } from '@mui/material/styles';
import { alpha, darken, lighten, styled } from '@mui/material/styles';
import type { MouseEventHandler } from 'react';

type CardVariant = 'default' | 'selected' | 'error';

const getBackgroundColor = (theme: Theme, variant?: CardVariant) =>
variant === 'selected'
? theme.palette.mode === 'light'
? alpha(theme.palette.primary.main, 0.04)
: alpha(theme.palette.primary.main, 0.42)
: theme.palette.background.paper;

export const Card = styled(Box, {
shouldForwardProp: (prop) =>
!['dense', 'variant', 'indented'].includes(prop as string),
})<{
variant?: 'default' | 'error';
variant?: CardVariant;
dense?: boolean;
indented?: boolean;
onClick?: MouseEventHandler<HTMLDivElement>;
}>(({ theme, variant, dense, indented, onClick }) => ({
backgroundColor: theme.palette.background.paper,
border: `1px solid`,
borderColor:
variant === 'error'
? theme.palette.error.main
: theme.palette.mode === 'light'
? theme.palette.grey[300]
: theme.palette.grey[800],
borderRadius: dense
? theme.shape.borderRadiusSecondary
: theme.shape.borderRadius,
overflow: 'hidden',
position: 'relative',
padding: indented ? theme.spacing(2) : 0,
boxSizing: 'border-box',
'&:hover': {
cursor: onClick ? 'pointer' : 'default',
backgroundColor: onClick
? theme.palette.mode === 'light'
? darken(theme.palette.background.paper, 0.02)
: lighten(theme.palette.background.paper, 0.02)
: theme.palette.background.paper,
},
transition: theme.transitions.create(['background-color'], {
duration: theme.transitions.duration.enteringScreen,
easing: theme.transitions.easing.easeOut,
}),
}));
}>(({ theme, variant, dense, indented, onClick }) => {
const backgroundColor = getBackgroundColor(theme, variant);
return {
backgroundColor,
border: `1px solid`,
borderColor:
variant === 'error'
? theme.palette.error.main
: variant === 'selected'
? theme.palette.primary.main
: theme.palette.mode === 'light'
? theme.palette.grey[300]
: theme.palette.grey[800],
borderRadius: dense
? theme.shape.borderRadiusSecondary
: theme.shape.borderRadius,
overflow: 'hidden',
position: 'relative',
padding: indented ? theme.spacing(2) : 0,
boxSizing: 'border-box',
'&:hover': {
cursor: onClick ? 'pointer' : 'default',
backgroundColor: onClick
? theme.palette.mode === 'light'
? darken(backgroundColor, 0.02)
: lighten(backgroundColor, 0.02)
: theme.palette.background.paper,
},
transition: theme.transitions.create(['background-color'], {
duration: theme.transitions.duration.enteringScreen,
easing: theme.transitions.easing.easeOut,
}),
};
});
18 changes: 18 additions & 0 deletions packages/widget/src/components/ChainSelect/ChainSelect.style.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Box } from '@mui/material';
import { styled } from '@mui/material/styles';
import { Card } from '../../components/Card';

export const ChainCard = styled(Card)(({ theme }) => ({
display: 'grid',
placeItems: 'center',
width: 56,
height: 56,
}));

export const ChainContainer = styled(Box)(({ theme }) => ({
display: 'grid',
gridTemplateColumns: 'repeat(auto-fit, 56px)',
gridAutoRows: '56px',
justifyContent: 'space-between',
gap: theme.spacing(1.5),
}));
72 changes: 72 additions & 0 deletions packages/widget/src/components/ChainSelect/ChainSelect.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/* eslint-disable react/no-array-index-key */
import type { EVMChain } from '@lifi/sdk';
import { Avatar, Box, Skeleton, Typography } from '@mui/material';
import { useWatch } from 'react-hook-form';
import { useNavigate } from 'react-router-dom';
import type { SwapFormTypeProps } from '../../providers';
import { SwapFormKeyHelper } from '../../providers';
import { maxChainToOrder } from '../../stores/chains';
import { navigationRoutes } from '../../utils';
import { ChainCard, ChainContainer } from './ChainSelect.style';
import { useChainSelect } from './useChainSelect';

export const ChainSelect = ({ formType }: SwapFormTypeProps) => {
const navigate = useNavigate();
const { chains, getChains, setCurrentChain, isLoading } =
useChainSelect(formType);
const [chainId] = useWatch({
name: [SwapFormKeyHelper.getChainKey(formType)],
});

const showAllChains = () => {
navigate(navigationRoutes[`${formType}Chain`], {
state: { formType },
});
};

const chainsToHide = (chains?.length ?? 0) - maxChainToOrder;

return (
<ChainContainer>
{isLoading
? Array.from({ length: maxChainToOrder + 1 }).map((_, index) => (
<Skeleton
key={index}
variant="rectangular"
width={56}
height={56}
sx={{ borderRadius: 1 }}
/>
))
: getChains().map((chain: EVMChain) => (
<ChainCard
key={chain.id}
onClick={() => setCurrentChain(chain.id)}
variant={chainId === chain.id ? 'selected' : 'default'}
>
<Avatar
src={chain.logoURI}
alt={chain.key}
sx={{ width: 40, height: 40 }}
>
{chain.name[0]}
</Avatar>
</ChainCard>
))}
{chainsToHide > 0 ? (
<ChainCard onClick={showAllChains}>
<Box
sx={{
width: 40,
height: 40,
display: 'grid',
placeItems: 'center',
}}
>
<Typography fontWeight={500}>+{chainsToHide}</Typography>
</Box>
</ChainCard>
) : null}
</ChainContainer>
);
};
2 changes: 2 additions & 0 deletions packages/widget/src/components/ChainSelect/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './ChainSelect';
export * from './useChainSelect';
36 changes: 36 additions & 0 deletions packages/widget/src/components/ChainSelect/useChainSelect.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import type { EVMChain } from '@lifi/sdk';
import { useFormContext } from 'react-hook-form';
import { useChains } from '../../hooks';
import type { SwapFormDirection } from '../../providers';
import { SwapFormKey, SwapFormKeyHelper } from '../../providers';
import { useChainOrder } from '../../stores/chains';

export const useChainSelect = (formType: SwapFormDirection) => {
const { setValue } = useFormContext();
const { chains, isLoading } = useChains();
const [chainOrder, setChainOrder] = useChainOrder();
const chainKey = SwapFormKeyHelper.getChainKey(formType);

const getChains = () => {
if (!chains) {
return [];
}
const selectedChains = chainOrder
.map((chainId) => chains.find((chain) => chain.id === chainId))
.filter((chain) => chain) as EVMChain[];

return selectedChains;
};

const setCurrentChain = (chainId: number) => {
setValue(chainKey, chainId, { shouldDirty: true });
setValue(SwapFormKeyHelper.getTokenKey(formType), '', {
shouldDirty: false,
});
setValue(SwapFormKeyHelper.getAmountKey(formType), '');
setValue(SwapFormKey.TokenSearchFilter, '');
setChainOrder(chainId);
};

return { chains, getChains, setCurrentChain, isLoading };
};
3 changes: 3 additions & 0 deletions packages/widget/src/components/Header/NavigationHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ export const NavigationHeader: React.FC = () => {
return t(`header.from`);
case navigationRoutes.toToken:
return t(`header.to`);
case navigationRoutes.fromChain:
case navigationRoutes.toChain:
return t(`header.selectChain`);
case navigationRoutes.swapRoutes:
return t(`header.routes`);
case navigationRoutes.activeSwaps:
Expand Down
4 changes: 1 addition & 3 deletions packages/widget/src/components/Step/StepTimer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,7 @@ export const StepTimer: React.FC<{ step: Step; hideInProgress?: boolean }> = ({
}
const shouldBePaused = step.execution.process.some(
(process) =>
process.status === 'ACTION_REQUIRED' ||
process.status === 'CHAIN_SWITCH_REQUIRED' ||
process.status === 'FAILED',
process.status === 'ACTION_REQUIRED' || process.status === 'FAILED',
);
if (isRunning && shouldBePaused) {
pause();
Expand Down
8 changes: 6 additions & 2 deletions packages/widget/src/components/TokenList/TokenList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,9 @@ export const TokenList: FC<TokenListProps> = ({

const handleTokenClick = useCallback(
(tokenAddress: string) => {
setValue(SwapFormKeyHelper.getTokenKey(formType), tokenAddress);
setValue(SwapFormKeyHelper.getTokenKey(formType), tokenAddress, {
shouldDirty: true,
});
setValue(SwapFormKeyHelper.getAmountKey(formType), '');
const oppositeFormType = formType === 'from' ? 'to' : 'from';
const [selectedOppositeToken, selectedOppositeChain, selectedChain] =
Expand All @@ -77,7 +79,9 @@ export const TokenList: FC<TokenListProps> = ({
selectedOppositeToken === tokenAddress &&
selectedOppositeChain === selectedChain
) {
setValue(SwapFormKeyHelper.getTokenKey(oppositeFormType), '');
setValue(SwapFormKeyHelper.getTokenKey(oppositeFormType), '', {
shouldDirty: false,
});
}
onClick?.();
},
Expand Down
24 changes: 19 additions & 5 deletions packages/widget/src/hooks/useChains.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,27 @@ import { useQuery } from '@tanstack/react-query';
import { useCallback } from 'react';
import { LiFi } from '../config/lifi';
import { useWidgetConfig } from '../providers/WidgetProvider';
import { useSetChainOrder } from '../stores/chains';

export const useChains = () => {
const { disabledChains } = useWidgetConfig();
const { data, ...other } = useQuery(['chains'], async () => {
const chains = await LiFi.getChains();
return chains.filter((chain) => !disabledChains?.includes(chain.id));
});
const [, initializeChains] = useSetChainOrder();
const { data, isLoading } = useQuery(
['chains'],
async () => {
const chains = await LiFi.getChains();
const filteredChains = chains.filter(
(chain) => !disabledChains?.includes(chain.id),
);
initializeChains(filteredChains.map((chain) => chain.id));
return filteredChains;
},
{
onError(err) {
console.log(err);
},
},
);

const getChainById = useCallback(
(chainId: number) => {
Expand All @@ -21,5 +35,5 @@ export const useChains = () => {
[data],
);

return { chains: data, getChainById, ...other };
return { chains: data, getChainById, isLoading };
};
Loading

0 comments on commit 3c4d3fe

Please sign in to comment.