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
27 changes: 27 additions & 0 deletions apps/frontend/src/app/1_atoms/Stepper/Stepper.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import React from 'react';

import classNames from 'classnames';

export interface StepperProps {
steps: number;
activeStep: number;
className?: string;
}

export const Stepper: React.FC<StepperProps> = ({
steps,
activeStep,
className,
}) => (
<div className={classNames('flex items-center gap-2 w-full', className)}>
{Array.from({ length: steps }, (_, index) => (
<div
className={classNames('h-1 flex-1 rounded-full transition-colors', {
'bg-primary-20': index < activeStep,
'bg-gray-40': index >= activeStep,
})}
key={index}
/>
))}
</div>
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { QueryClient } from '@tanstack/react-query';

export const ACTIVE_CLASSNAME = 'border-t-primary-30';

export const queryClient = new QueryClient();
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
import { QueryClientProvider } from '@tanstack/react-query';

import React, { useCallback, useEffect, useMemo, useState } from 'react';

import { t } from 'i18next';

import {
AddressBadge,
Button,
ButtonStyle,
Dialog,
DialogSize,
Heading,
Tabs,
VerticalTabs,
} from '@sovryn/ui';

import { MobileCloseButton } from '../../1_atoms/MobileCloseButton/MobileCloseButton';
import { useAccount } from '../../../hooks/useAccount';
import { useIsMobile } from '../../../hooks/useIsMobile';
import { translations } from '../../../locales/i18n';
import { ACTIVE_CLASSNAME, queryClient } from './ERC20BridgeDialog.constants';
import { ReceiveFlow } from './components/ReceiveFlow/ReceiveFlow';
import { SendFlow } from './components/SendFlow/SendFlow';
import { ReceiveFlowContextProvider } from './contextproviders/ReceiveFlowContext';
import { SendFlowContextProvider } from './contextproviders/SendFlowContext';

const translation = translations.erc20Bridge.mainScreen;

type ERC20BridgeDialogProps = {
isOpen: boolean;
onClose: () => void;
step?: number;
};

export const ERC20BridgeDialog: React.FC<ERC20BridgeDialogProps> = ({
isOpen,
onClose,
step = 0,
}) => {
const [index, setIndex] = useState(step);
const { account } = useAccount();
const { isMobile } = useIsMobile();

useEffect(() => {
setIndex(step);
}, [step]);

const items = useMemo(() => {
return [
{
label: t(translation.tabs.receiveLabel),
infoText: t(translation.tabs.receiveInfoText),
content: (
<ReceiveFlowContextProvider>
<ReceiveFlow onClose={onClose} />
<MobileCloseButton onClick={onClose} />
</ReceiveFlowContextProvider>
),
activeClassName: ACTIVE_CLASSNAME,
dataAttribute: 'erc20-bridge-receive',
},
{
label: t(translation.tabs.sendLabel),
infoText: t(translation.tabs.sendInfoText),
content: (
<SendFlowContextProvider>
<SendFlow onClose={onClose} />
<MobileCloseButton onClick={onClose} />
</SendFlowContextProvider>
),
activeClassName: ACTIVE_CLASSNAME,
dataAttribute: 'erc20-bridge-send',
},
];
}, [onClose]);

const onChangeIndex = useCallback((index: number | null) => {
index !== null ? setIndex(index) : setIndex(0);
}, []);

const dialogSize = useMemo(
() => (isMobile ? DialogSize.md : DialogSize.xl3),
[isMobile],
);

useEffect(() => {
setIndex(0);
}, [account]);

return (
<Dialog
isOpen={isOpen}
width={dialogSize}
className="p-4 flex items-center sm:p-0"
disableFocusTrap
closeOnEscape={false}
>
<QueryClientProvider client={queryClient}>
<Tabs
index={index}
items={items}
onChange={onChangeIndex}
className="w-full md:hidden"
contentClassName="pt-9 px-6 pb-7 h-full"
/>
<VerticalTabs
items={items}
onChange={onChangeIndex}
selectedIndex={index}
tabsClassName="min-h-[42rem] h-auto self-stretch block pt-0 relative flex-1"
headerClassName="pb-0 pt-5"
footerClassName="absolute bottom-5 left-5"
contentClassName="px-9 pb-10 pt-6 flex-1"
className="hidden md:flex"
header={() => (
<>
<div className="rounded bg-gray-60 px-2 py-1 w-fit mb-9">
<AddressBadge address={account} />
</div>
<Heading className="mb-20">{t(translation.title)}</Heading>
</>
)}
footer={() => (
<Button
text={t(translations.common.buttons.close)}
onClick={onClose}
style={ButtonStyle.ghost}
dataAttribute="erc20-bridge-close"
/>
)}
/>
</QueryClientProvider>
</Dialog>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import React from 'react';

import { t } from 'i18next';
import { Trans } from 'react-i18next';

import { Heading, HeadingType, Link } from '@sovryn/ui';

import { HELPDESK_LINK } from '../../../../constants/links';
import { translations } from '../../../../locales/i18n';

type InstructionsProps = {
isReceive?: boolean;
};

export const Instructions: React.FC<InstructionsProps> = () => {
return (
<>
<Heading type={HeadingType.h2} className="font-medium leading-[1.375rem]">
{t(translations.erc20Bridge.instructions.title)}:
</Heading>

<ul className="list-disc list-inside text-xs leading-5 font-medium text-gray-30 mt-6 mb-12">
<li className="mb-2">
{t(translations.erc20Bridge.instructions['1'])}
</li>
<li className="mb-2">
{t(translations.erc20Bridge.instructions['2'])}
</li>
<li className="mb-2">
{t(translations.erc20Bridge.instructions['3'])}
</li>
<li className="mb-2">
{t(translations.erc20Bridge.instructions['4'])}
</li>
<li className="mb-2">
<Trans
i18nKey={t(translations.erc20Bridge.instructions['5'])}
tOptions={{ hours: 1.5 }}
components={[
<Link
text={t(
translations.erc20Bridge.instructions.createSupportTicketCta,
)}
className="md:ml-4"
href={HELPDESK_LINK}
/>,
]}
/>
</li>
</ul>
</>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import React, { useCallback, useState } from 'react';

import { formatUnits } from 'ethers/lib/utils';
import { t } from 'i18next';

import { ChainId } from '@sovryn/ethers-provider';
import { Accordion, SimpleTable, SimpleTableRow } from '@sovryn/ui';

import { getTokenDisplayName } from '../../../../constants/tokens';
import { translations } from '../../../../locales/i18n';
import { useBridgeLimits } from '../hooks/useBridgeLimits';

const translation = translations.erc20Bridge.limits;

type LimitsProps = {
sourceChain?: ChainId;
targetChain?: ChainId;
asset?: string;
className?: string;
};

export const Limits: React.FC<LimitsProps> = ({
sourceChain,
targetChain,
asset,
className,
}) => {
const [open, setOpen] = useState(false);
const { data: limits } = useBridgeLimits(sourceChain, targetChain, asset);
const onClick = useCallback((toOpen: boolean) => setOpen(toOpen), []);

if (!asset) {
return null;
}
return (
<>
<Accordion
label={t(translation.title)}
disabled={!asset || !sourceChain || !targetChain}
children={
limits ? (
<SimpleTable border>
<SimpleTableRow
label={t(translation.minimumAmount)}
value={`${formatUnits(
limits.minPerToken,
)} ${getTokenDisplayName(asset)}`}
/>
<SimpleTableRow
label={t(translation.maximumAmount)}
value={`${formatUnits(
limits.maxTokensAllowed,
)} ${getTokenDisplayName(asset)}`}
/>
<SimpleTableRow
label={t(translation.dailyLimit)}
value={`${formatUnits(limits.dailyLimit)} ${getTokenDisplayName(
asset,
)}`}
/>
<SimpleTableRow
label={t(translation.dailyLimitSpent)}
value={`${formatUnits(limits.spentToday)} ${getTokenDisplayName(
asset,
)}`}
/>
<SimpleTableRow
label={t(translation.fee)}
value={`${formatUnits(
limits.feePerToken,
)} ${getTokenDisplayName(asset)}`}
/>
</SimpleTable>
) : (
<span className="h-10 w-full block bg-gray-50 rounded animate-pulse" />
)
}
className={className}
open={open}
onClick={onClick}
/>
</>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import React, { FC } from 'react';

import classNames from 'classnames';

import { ChainIds } from '@sovryn/ethers-provider';

import { useBridgeService } from '../../hooks/useBridgeService';
import { getNetworkIcon } from './NetworkRenderer.utils';

type NetworkRendererProps = {
chainId?: ChainIds;
className?: string;
};

export const NetworkRenderer: FC<NetworkRendererProps> = ({
chainId,
className,
}) => {
const bridgeService = useBridgeService();

if (!bridgeService || !chainId) {
return null;
}

const networkConfig = bridgeService.getNetworkConfig(chainId);

if (!networkConfig) {
return null;
}

return (
<div className={classNames('flex items-center gap-2', className)}>
<img
className="w-5 h-5 rounded-full"
src={getNetworkIcon(chainId)}
alt={networkConfig.name}
/>
<span>{networkConfig.name}</span>
</div>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { ChainId, ChainIds } from '@sovryn/ethers-provider';

import bscLogo from '../../../../../assets/chains/bsc.svg';
import ethLogo from '../../../../../assets/chains/eth.svg';
import rskLogo from '../../../../../assets/chains/rsk.svg';
import unknownLogo from '../../../../../assets/chains/unknown.svg';

export const getNetworkIcon = (chainId: ChainId) => {
switch (chainId) {
case ChainIds.RSK_MAINNET:
case ChainIds.RSK_TESTNET:
return rskLogo;
case ChainIds.BSC_MAINNET:
case ChainIds.BSC_TESTNET:
return bscLogo;
case ChainIds.MAINNET:
case ChainIds.ROPSTEN:
return ethLogo;

default:
return unknownLogo;
}
};
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './NetworkRenderer';
Loading