Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(tangle-dapp): Bridge UI on Tangle Dapp #2307

Merged
merged 14 commits into from
May 15, 2024
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ const ActiveChainDropdown = () => {
chain={chain}
status="success"
placeholder={activeChain === null ? 'Unsupported Chain' : undefined}
textClassname="hidden lg:!block"
textClassName="hidden lg:!block"
/>
</DropdownButton>
<DropdownBody className="mt-2">
Expand Down
74 changes: 74 additions & 0 deletions apps/tangle-dapp/app/bridge/BridgeContainer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
'use client';

import Button from '@webb-tools/webb-ui-components/components/buttons/Button';
import { FC } from 'react';
import { twMerge } from 'tailwind-merge';

import AddressInput, {
AddressType,
} from '../../components/AddressInput/AddressInput';
import AmountInput from '../../components/AmountInput/AmountInput';
import { useBridge } from '../../context/BridgeContext';
import ChainSelectors from './ChainSelectors';
import useActionButton from './useActionButton';

interface BridgeContainerProps {
className?: string;
}

const BridgeContainer: FC<BridgeContainerProps> = ({ className }) => {
const { destinationAddress, setDestinationAddress, amount, setAmount } =
useBridge();
const { buttonAction, buttonText, isLoading } = useActionButton();

return (
<div
className={twMerge(
'max-w-[640px] min-h-[580px] bg-mono-0 dark:bg-mono-190 p-8',
'rounded-xl border border-mono-40 dark:border-mono-160',
'shadow-webb-lg dark:shadow-webb-lg-dark',
'flex flex-col',
className
)}
>
<div className="flex-1 w-full flex flex-col justify-between">
<div className="space-y-8">
<ChainSelectors />

<AmountInput
id="bridge-amount-input"
title="Amount"
amount={amount}
setAmount={setAmount}
baseInputOverrides={{
isFullWidth: true,
}}
placeholder=""
/>

<AddressInput
id="bridge-destination-address-input"
type={AddressType.Both}
title="Receiver Address"
baseInputOverrides={{ isFullWidth: true }}
value={destinationAddress}
setValue={setDestinationAddress}
/>

{/* TODO: Tx Info (Fees & Estimated Time) */}
</div>
<Button
isFullWidth
isDisabled={isLoading}
isLoading={isLoading}
onClick={buttonAction}
loadingText="Connecting..."
>
{buttonText}
</Button>
</div>
</div>
);
};

export default BridgeContainer;
161 changes: 161 additions & 0 deletions apps/tangle-dapp/app/bridge/ChainSelectors.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
'use client';

import { DropdownMenuTrigger as DropdownTrigger } from '@radix-ui/react-dropdown-menu';
import { ChainConfig } from '@webb-tools/dapp-config/chains/chain-config.interface';
import { ArrowRight } from '@webb-tools/icons/ArrowRight';
import { ChainIcon } from '@webb-tools/icons/ChainIcon';
import ChainButton from '@webb-tools/webb-ui-components/components/buttons/ChainButton';
import {
Dropdown,
DropdownBody,
} from '@webb-tools/webb-ui-components/components/Dropdown';
import { MenuItem } from '@webb-tools/webb-ui-components/components/MenuItem';
import { ScrollArea } from '@webb-tools/webb-ui-components/components/ScrollArea';
import { FC, useCallback } from 'react';

import { useBridge } from '../../context/BridgeContext';

interface ChainSelectorProps {
title: string;
selectedChain: ChainConfig | null;
chainOptions: ChainConfig[];
onSelectChain: (chain: ChainConfig) => void;
className?: string;
}

const ChainSelectors: FC = () => {
const {
sourceChain,
setSourceChain,
destinationChain,
setDestinationChain,
supportedSourceChains,
supportedDestinationChains,
} = useBridge();

const onSetSourceChain = useCallback(
(chain: ChainConfig) => {
setSourceChain(chain);
// If the source chain is the same as the destination chain, clear the destination chain.
if (chain.id === destinationChain?.id) {
setDestinationChain(null);
}
},
[destinationChain?.id, setDestinationChain, setSourceChain]
);

const onSetDestinationChain = useCallback(
(chain: ChainConfig) => {
setDestinationChain(chain);
// If the destination chain is the same as the source chain, clear the source chain.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems that instead of switching and nullifying things that we should not present the same chain as an option? Or do you think this is better. I'm ambivalent is this is the logic you think is worth having.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Working on it right now!

if (chain.id === sourceChain?.id) {
setSourceChain(null);
}
},
[setDestinationChain, setSourceChain, sourceChain?.id]
);

const switchChains = useCallback(() => {
const temp = sourceChain;

// If the destination chain is null or is in the supported destination chains,
// set it as the source chain.
if (
temp === null ||
supportedDestinationChains.find((chain) => chain.id === temp.id)
) {
setDestinationChain(temp);
} else {
setDestinationChain(null);
}

// If the source chain is null or is in the supported source chains,
// set it as the destination chain.
if (
destinationChain === null ||
supportedSourceChains.find((chain) => chain.id === destinationChain.id)
) {
setSourceChain(destinationChain);
} else {
setSourceChain(null);
}
}, [
setSourceChain,
setDestinationChain,
destinationChain,
sourceChain,
supportedDestinationChains,
supportedSourceChains,
]);

return (
<div className="flex flex-col md:flex-row justify-between items-center gap-3">
<ChainSelector
title="From"
selectedChain={sourceChain}
chainOptions={supportedSourceChains}
onSelectChain={onSetSourceChain}
className="flex-1 w-full md:w-auto"
/>

<div
className="cursor-pointer p-1 rounded-full hover:bg-mono-20 dark:hover:bg-mono-160"
onClick={switchChains}
>
<ArrowRight size="lg" className="rotate-90 md:rotate-0" />
</div>

<ChainSelector
title="To"
selectedChain={destinationChain}
chainOptions={supportedDestinationChains}
onSelectChain={onSetDestinationChain}
className="flex-1 w-full md:w-auto"
/>
</div>
);
};

const ChainSelector: FC<ChainSelectorProps> = ({
title,
selectedChain,
chainOptions,
onSelectChain,
className,
}) => {
return (
<Dropdown className={className}>
<DropdownTrigger asChild>
<ChainButton
chain={selectedChain ?? undefined}
status="success"
placeholder={selectedChain === null ? title : undefined}
className="w-full bg-mono-20 dark:bg-mono-160 border-0"
textClassName={
selectedChain === null ? 'text-mono-100 dark:text-mono-80' : ''
}
/>
</DropdownTrigger>
<DropdownBody>
<ScrollArea className="max-h-[300px]">
<ul>
{chainOptions.map((chain) => {
return (
<li key={`${chain.chainType}-${chain.id}`}>
<MenuItem
startIcon={<ChainIcon size="lg" name={chain.name} />}
onSelect={() => onSelectChain(chain)}
>
{chain.name}
</MenuItem>
</li>
);
})}
</ul>
</ScrollArea>
</DropdownBody>
</Dropdown>
);
};

export default ChainSelectors;
9 changes: 9 additions & 0 deletions apps/tangle-dapp/app/bridge/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { FC, PropsWithChildren } from 'react';

import BridgeProvider from '../../context/BridgeContext';

const BridgeLayout: FC<PropsWithChildren> = ({ children }) => {
return <BridgeProvider>{children}</BridgeProvider>;
};

export default BridgeLayout;
19 changes: 19 additions & 0 deletions apps/tangle-dapp/app/bridge/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { Metadata } from 'next';
import { FC } from 'react';

import createPageMetadata from '../../utils/createPageMetadata';
import BridgeContainer from './BridgeContainer';

export const metadata: Metadata = createPageMetadata({
title: 'Bridge',
});

const Bridge: FC = () => {
return (
<div>
<BridgeContainer className="mx-auto" />
</div>
);
};

export default Bridge;
32 changes: 32 additions & 0 deletions apps/tangle-dapp/app/bridge/useActionButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
'use client';

import {
useConnectWallet,
useWebContext,
} from '@webb-tools/api-provider-environment';
import { useCallback, useMemo } from 'react';

export default function useActionButton() {
const { activeAccount, activeWallet, loading, isConnecting } =
useWebContext();

const { toggleModal } = useConnectWallet();

const noActiveAccountOrWallet = useMemo(() => {
return !activeAccount || !activeWallet;
}, [activeAccount, activeWallet]);

const openWalletModal = useCallback(() => {
toggleModal(true);
}, [toggleModal]);

const bridgeTx = useCallback(() => {
// TODO: handle bridge Tx for each case from the source and destination chain
}, []);

return {
isLoading: loading || isConnecting,
buttonAction: noActiveAccountOrWallet ? openWalletModal : bridgeTx,
buttonText: noActiveAccountOrWallet ? 'Connect' : 'Transfer',
};
}
4 changes: 3 additions & 1 deletion apps/tangle-dapp/components/AmountInput/AmountInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export type AmountInputProps = {
errorOnEmptyValue?: boolean;
setAmount: (newAmount: BN | null) => void;
setErrorMessage?: (error: string | null) => void;
placeholder?: string;
};

const AmountInput: FC<AmountInputProps> = ({
Expand All @@ -36,6 +37,7 @@ const AmountInput: FC<AmountInputProps> = ({
baseInputOverrides,
errorOnEmptyValue = false,
setErrorMessage,
placeholder,
}) => {
const inputRef = useRef<HTMLInputElement>(null);
const { nativeTokenSymbol } = useNetworkStore();
Expand Down Expand Up @@ -99,7 +101,7 @@ const AmountInput: FC<AmountInputProps> = ({
inputRef={inputRef}
inputClassName="placeholder:text-md"
type="text"
placeholder={`0 ${nativeTokenSymbol}`}
placeholder={placeholder ?? `0 ${nativeTokenSymbol}`}
size="sm"
autoComplete="off"
value={displayAmount}
Expand Down
2 changes: 2 additions & 0 deletions apps/tangle-dapp/components/Breadcrumbs/Breadcrumbs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import { isAddress } from '@polkadot/util-crypto';
import {
ArrowLeftRightLineIcon,
CheckboxBlankCircleLine,
CodeFill,
FundsLine,
Expand Down Expand Up @@ -30,6 +31,7 @@ const BREADCRUMB_ICONS: Record<string, BreadcrumbType['icon']> = {
services: <GridFillIcon className="w-4 h-4 lg:w-6 lg:h-6" />,
restake: <TokenSwapLineIcon className="w-4 h-4 lg:w-6 lg:h-6" />,
nomination: <FundsLine className="w-4 h-4 lg:w-6 lg:h-6" />,
bridge: <ArrowLeftRightLineIcon className="w-4 h-4 lg:w-6 lg:h-6" />,
};

// TODO: Need to statically link the breadcrumb labels to the page path for better type safety and to enable fearless refactoring in the future.
Expand Down
10 changes: 10 additions & 0 deletions apps/tangle-dapp/components/Sidebar/sidebarProps.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { isAppEnvironmentType } from '@webb-tools/dapp-config/types';
import {
AppsLine,
ArrowLeftRightLineIcon,
DocumentationIcon,
FundsLine,
GiftLineIcon,
Expand Down Expand Up @@ -33,6 +34,15 @@ const SIDEBAR_STATIC_ITEMS: SideBarItemProps[] = [
Icon: UserLineIcon,
subItems: [],
},
{
name: 'Bridge',
href: PagePath.BRIDGE,
isInternal: true,
isNext: true,
Icon: ArrowLeftRightLineIcon,
subItems: [],
environments: ['development', 'staging', 'test'],
},
{
name: 'Services',
href: '',
Expand Down
9 changes: 9 additions & 0 deletions apps/tangle-dapp/constants/bridge.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { chainsConfig } from '@webb-tools/dapp-config';
import { ChainConfig } from '@webb-tools/dapp-config/chains/chain-config.interface';
import { PresetTypedChainId } from '@webb-tools/dapp-types';

// This is just a temporary variable to use as supported source and destination chains
export const BRIDGE_SUPPORTED_CHAINS: ChainConfig[] = [
chainsConfig[PresetTypedChainId.TangleMainnetNative],
chainsConfig[PresetTypedChainId.TangleTestnetNative],
];
Loading
Loading