fix: bridge UI minor fixes (#104)

* fix: duplicate metamask wallet issue and dollars prices not updated issue

* fix: refactor submit button state management + update Transactions page

* fix: add multi platform docker build for bridge ui

* fix: update bridge ui dockerfile + change github action runner os

* fix: remove multi platform docker build

* fix: update docs link + fix tooltip arrow issue

* fix: chrome autofill background issue with input
This commit is contained in:
Victorien Gauch
2024-10-01 14:03:39 +02:00
committed by GitHub
parent 41cec7c602
commit 7fd470489e
15 changed files with 141 additions and 69 deletions

View File

@@ -14,7 +14,7 @@ on:
jobs:
publish:
runs-on: besu-arm64
runs-on: ubuntu-22.04
if: github.event_name != 'pull_request' || (github.event_name == 'pull_request' && github.event.pull_request.draft == false)
steps:
- name: Checkout
@@ -35,10 +35,8 @@ jobs:
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- # Setting up Docker Buildx with docker-container driver is required
# at the moment to be able to use a subdirectory with Git context
name: Set up Docker Buildx
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Docker Image Build and Publish

View File

@@ -22,7 +22,7 @@ COPY ./bridge-ui ./bridge-ui
COPY $ENV_FILE ./bridge-ui/.env.production
RUN --mount=type=cache,id=pnpm,target=/pnpm/store apk add --virtual build-dependencies --no-cache python3 make g++ \
&& pnpm install --frozen-lockfile \
&& pnpm install --frozen-lockfile --prefer-offline \
&& pnpm run -F bridge-ui build \
&& apk del build-dependencies

View File

@@ -13,6 +13,13 @@ body {
}
@layer components {
input:-webkit-autofill,
input:-webkit-autofill:hover,
input:-webkit-autofill:focus,
input:-webkit-autofill:active{
-webkit-box-shadow: 0 0 0 30px #2D2D2D inset !important;
}
.container {
@apply mx-auto px-4;
max-width: 1280px;
@@ -34,9 +41,21 @@ body {
border-radius: 0;
}
.tooltip:after {
.tooltip-top::after {
border-color: #505050 transparent transparent transparent;
}
.tooltip-right::after {
border-color: transparent #505050 transparent transparent;
}
.tooltip-bottom::after {
border-color: transparent transparent #505050 transparent;
}
.tooltip-left::after {
border-color: transparent transparent transparent #505050;
}
.tooltip:before {
border: 1px solid #505050;

View File

@@ -196,7 +196,7 @@ const Bridge = () => {
{!isConnected && <ConnectButton fullWidth />}
</div>
</form>
{token && networkLayer !== NetworkLayer.UNKNOWN && token[networkLayer] && (
{isConnected && token && networkLayer !== NetworkLayer.UNKNOWN && token[networkLayer] && (
<div className="mt-4 px-2">
<ERC20Stepper />
</div>

View File

@@ -26,18 +26,25 @@ export function ReceivedAmount({ receivedAmount }: ReceivedAmountProps) {
{isConnected && (
<>
<span className="text-2xl font-semibold text-white">
{formatBalance(receivedAmount) || 0} {token?.symbol}
{(parseFloat(receivedAmount || "0") > 0 && formatBalance(receivedAmount)) || 0} {token?.symbol}
</span>
{networkType === NetworkType.MAINNET && (
<span className="label-text flex items-center">
<PiApproximateEqualsBold />
{tokenPrices?.[tokenAddress]?.usd
? (Number(receivedAmount) * tokenPrices?.[tokenAddress]?.usd).toLocaleString("en-US", {
{receivedAmount &&
parseFloat(receivedAmount) > 0 &&
tokenPrices?.[tokenAddress.toLowerCase()]?.usd &&
tokenPrices?.[tokenAddress.toLowerCase()]?.usd > 0 ? (
<>
<PiApproximateEqualsBold />
{(Number(receivedAmount) * tokenPrices?.[tokenAddress.toLowerCase()]?.usd).toLocaleString("en-US", {
style: "currency",
currency: "USD",
maximumFractionDigits: 4,
})
: "$0.00"}
})}
</>
) : (
""
)}
</span>
)}
</>

View File

@@ -122,14 +122,20 @@ export function Amount() {
/>
{networkType === NetworkType.MAINNET && (
<span className="label-text flex items-center justify-end">
<PiApproximateEqualsBold />
{amount && tokenPrices?.[tokenAddress]?.usd
? `${(Number(amount) * tokenPrices?.[tokenAddress]?.usd).toLocaleString("en-US", {
{amount &&
tokenPrices?.[tokenAddress.toLowerCase()]?.usd &&
tokenPrices?.[tokenAddress.toLowerCase()]?.usd > 0 ? (
<>
<PiApproximateEqualsBold />
{(Number(amount) * tokenPrices?.[tokenAddress.toLowerCase()]?.usd).toLocaleString("en-US", {
style: "currency",
currency: "USD",
maximumFractionDigits: 4,
})}`
: "$0.00"}
})}
</>
) : (
""
)}
</span>
)}
</>

View File

@@ -9,7 +9,6 @@ import { useSwitchNetwork, useAllowance, useApprove } from "@/hooks";
import { Transaction } from "@/models";
import { useChainStore } from "@/stores/chainStore";
import { cn } from "@/utils/cn";
import { useTokenBalance } from "@/hooks/useTokenBalance";
export type BridgeForm = {
amount: string;
@@ -22,20 +21,15 @@ export default function Approve() {
// Form
const { getValues, setValue, watch } = useFormContext();
const watchAmount = watch("amount", false);
const watchBalance = watch("balance", false);
const [watchAmount, watchBalance] = watch(["amount", "balance"]);
// Context
const { token, fromChain, tokenBridgeAddress, tokenAddress } = useChainStore((state) => ({
tokenAddress: state.token?.[state.networkLayer],
const { token, fromChain, tokenBridgeAddress } = useChainStore((state) => ({
token: state.token,
fromChain: state.fromChain,
tokenBridgeAddress: state.tokenBridgeAddress,
}));
// Hooks
const { balance } = useTokenBalance(tokenAddress, token?.decimals);
const { switchChain } = useSwitchNetwork(fromChain?.id);
const { allowance, refetchAllowance } = useAllowance();
const { hash: newTxHash, setHash, writeApprove, isLoading: isApprovalLoading } = useApprove();
@@ -43,7 +37,8 @@ export default function Approve() {
// Wagmi
const { address } = useAccount();
const hasInsufficientBalance = watchAmount && balance < watchAmount;
const hasInsufficientBalance =
watchAmount && token && parseUnits(watchAmount, token.decimals) > parseUnits(watchBalance, token.decimals);
const {
isLoading: isWaitingLoading,
@@ -101,12 +96,22 @@ export default function Approve() {
}
}, [watchAmount, allowance, token, setValue]);
// Click on approve
const approveHandler = async () => {
await switchChain();
if (token) {
const amount = getValues("amount");
const amountToApprove = parseUnits(amount, token.decimals);
const amountBigInt = parseUnits(amount, token.decimals);
let amountToApprove = amountBigInt;
if (allowance && allowance > 0n) {
if (allowance >= amountBigInt) {
amountToApprove = allowance - amountBigInt;
} else {
amountToApprove = amountBigInt - allowance;
}
}
writeApprove(amountToApprove, tokenBridgeAddress);
}
};

View File

@@ -1,12 +1,13 @@
import { useFormContext } from "react-hook-form";
import { parseUnits } from "viem";
import { useAccount, useBalance } from "wagmi";
import { MdInfo } from "react-icons/md";
import { NetworkLayer } from "@/config";
import { useBridge } from "@/hooks";
import { useChainStore } from "@/stores/chainStore";
import { useFormContext } from "react-hook-form";
import ApproveERC20 from "./ApproveERC20";
import { Button } from "../../ui";
import { useAccount, useBalance } from "wagmi";
import { Button, Tooltip } from "../../ui";
import { cn } from "@/utils/cn";
import { parseUnits } from "viem";
type SubmitProps = {
isLoading: boolean;
@@ -18,7 +19,7 @@ export function Submit({ isLoading = false, isWaitingLoading = false }: SubmitPr
const { watch, formState } = useFormContext();
const { errors } = formState;
const [watchAmount, watchAllowance, watchClaim] = watch(["amount", "allowance", "claim"]);
const [watchAmount, watchAllowance, watchClaim, watchBalance] = watch(["amount", "allowance", "claim", "balance"]);
// Context
const { token, networkLayer, toChainId } = useChainStore((state) => ({
@@ -38,18 +39,22 @@ export function Submit({ isLoading = false, isWaitingLoading = false }: SubmitPr
},
});
const destinationBalanceTooLow =
watchClaim === "manual" && destinationChainBalance && destinationChainBalance.value === 0n;
const originChainBalanceTooLow =
token !== null &&
(errors?.amount?.message !== undefined ||
parseUnits(watchBalance, token.decimals) < parseUnits(watchAmount, token.decimals));
const isERC20Token = token && networkLayer !== NetworkLayer.UNKNOWN && token[networkLayer];
const isButtonDisabled = !bridgeEnabled(watchAmount, watchAllowance || BigInt(0), errors);
const isButtonDisabled = !bridgeEnabled(watchAmount, watchAllowance || BigInt(0), errors) || originChainBalanceTooLow;
const isETHTransfer = token && token.symbol === "ETH";
const showApproveERC20 =
!isETHTransfer &&
(!watchAllowance || (token?.decimals && watchAllowance < parseUnits(watchAmount, token.decimals)));
// TODO: refactor this
const destinationBalanceTooLow =
watchClaim === "manual" && destinationChainBalance && destinationChainBalance.value === 0n;
const buttonText = errors?.amount?.message
const buttonText = originChainBalanceTooLow
? "Insufficient balance"
: destinationBalanceTooLow
? "Bridge anyway"
@@ -65,18 +70,38 @@ export function Submit({ isLoading = false, isWaitingLoading = false }: SubmitPr
loading={isLoading || isWaitingLoading}
>
{buttonText}
{destinationBalanceTooLow && (
<Tooltip
text="You have selected Manual Claim and do not have ETH on the recipient chain to pay for gas. Click this to Bridge Anyway"
className="z-[99] normal-case"
position="bottom"
>
<MdInfo />
</Tooltip>
)}
</Button>
) : showApproveERC20 && isERC20Token ? (
<ApproveERC20 />
) : (
<Button
id="submit-erc-btn"
className="w-full text-lg font-normal"
className={cn("w-full text-lg font-normal", {
"btn-secondary": destinationBalanceTooLow,
})}
disabled={isButtonDisabled}
loading={isLoading || isWaitingLoading}
type="submit"
>
{buttonText}
{destinationBalanceTooLow && (
<Tooltip
text="You have selected Manual Claim and do not have ETH on the recipient chain to pay for gas. Click this to Bridge Anyway"
className="z-[100] normal-case"
position="top"
>
<MdInfo />
</Tooltip>
)}
</Button>
);
}

View File

@@ -1,12 +1,18 @@
import Link from "next/link";
import ReloadHistoryButton from "./ReloadHistoryButton";
import { useFetchHistory } from "@/hooks";
export function NoTransactions() {
const { clearHistory } = useFetchHistory();
return (
<div className="flex min-h-80 flex-col items-center justify-center gap-8 rounded-lg border-2 border-card bg-cardBg p-4">
<span className="text-[#C0C0C0]">No bridge transactions found</span>
<Link href="/" className="btn btn-primary max-w-xs rounded-full uppercase">
Bridge assets
</Link>
<div className="rounded-lg border-2 border-card bg-cardBg p-4">
<ReloadHistoryButton clearHistory={clearHistory} />
<div className="flex min-h-80 flex-col items-center justify-center gap-8 ">
<span className="text-[#C0C0C0]">No bridge transactions found</span>
<Link href="/" className="btn btn-primary max-w-xs rounded-full uppercase">
Bridge assets
</Link>
</div>
</div>
);
}

View File

@@ -0,0 +1,17 @@
import { Button } from "../ui";
export default function ReloadHistoryButton({ clearHistory }: { clearHistory: () => void }) {
return (
<div className="flex justify-end">
<Button
id="reload-history-btn"
variant="link"
size="sm"
className="font-light normal-case text-gray-200 no-underline opacity-60 hover:text-primary hover:opacity-100"
onClick={clearHistory}
>
Reload history
</Button>
</div>
);
}

View File

@@ -7,7 +7,7 @@ import { TransactionHistory } from "@/models/history";
import { formatDate, fromUnixTime } from "date-fns";
import { NoTransactions } from "./NoTransaction";
import { useFetchHistory } from "@/hooks";
import { Button } from "../ui";
import ReloadHistoryButton from "./ReloadHistoryButton";
const groupByDay = (transactions: TransactionHistory[]): Record<string, TransactionHistory[]> => {
return transactions.reduce(
@@ -56,24 +56,6 @@ function SkeletonLoader() {
);
}
function ReloadHistoryButton({ clearHistory }: { clearHistory: () => void }) {
return (
<div className="flex justify-end">
<Button
id="reload-history-btn"
variant="link"
size="sm"
className="font-light normal-case text-gray-200 no-underline opacity-60 hover:text-primary hover:opacity-100"
onClick={() => {
clearHistory();
}}
>
Reload history
</Button>
</div>
);
}
function TransactionGroup({ date, transactions }: { date: string; transactions: TransactionHistory[] }) {
return (
<div className="flex flex-col gap-2">

View File

@@ -20,13 +20,15 @@ export default function Stepper({ steps, activeStep }: StepperProps) {
className={cn(
"-mx-px flex size-14 shrink-0 items-center justify-center rounded-full border-2 border-card bg-cardBg p-1.5",
{
"bg-primary": index < activeStep,
"border-primary": index <= activeStep,
},
)}
>
<span
className={cn("text-base font-bold text-[#E5E5E5]", {
"text-primary": index <= activeStep,
"text-cardBg": index < activeStep,
"text-primary": index === activeStep,
})}
>
{index + 1}

View File

@@ -29,7 +29,7 @@ export const wagmiConfig = defaultWagmiConfig({
projectId: config.walletConnectId,
showQrModal: false,
}),
injected({ shimDisconnect: true, target: "metaMask" }),
injected({ shimDisconnect: true }),
coinbaseWallet({
appName: "Linea Bridge",
}),

View File

@@ -170,6 +170,11 @@ const useBridge = (): UseBridge => {
return false;
}
const amountInteger = parseFloat(amount);
if (isNaN(amountInteger) || amountInteger === 0) {
return false;
}
// Check form errors
if (!isEmptyObject(errors)) {
return false;

View File

@@ -24,7 +24,7 @@ export const MENU_ITEMS = [
},
{
title: "Docs",
href: "https://docs.linea.build/",
href: "https://docs.linea.build/developers/guides/bridge/how-to-bridge-eth",
external: true,
Icon: DocsIcon,
},