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

Add ErrorPage variants #217

Merged
merged 13 commits into from
Sep 5, 2024
33 changes: 11 additions & 22 deletions packages/widget-v2/src/components/AssetChainInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,9 @@ import { ChevronIcon } from "@/icons/ChevronIcon";
import { useTheme } from "styled-components";
import { CogIcon } from "@/icons/CogIcon";
import { Button, GhostButton } from "@/components/Button";
import { useAtom } from "jotai";
import { skipAssetsAtom, skipChainsAtom } from "@/state/skipClient";
import { useUsdValue } from "@/utils/useUsdValue";
import { formatUSD } from "@/utils/intl";
import { BigNumber } from "bignumber.js";
import { formatNumberWithCommas, formatNumberWithoutCommas } from "@/utils/number";
import { useGetAssetDetails } from "@/hooks/useGetAssetDetails";

export type AssetChainInputProps = {
value?: string;
Expand All @@ -28,18 +25,10 @@ export const AssetChainInput = ({
handleChangeChain,
}: AssetChainInputProps) => {
const theme = useTheme();
const [{ data: assets }] = useAtom(skipAssetsAtom);
const [{ data: chains }] = useAtom(skipChainsAtom)

const selectedAsset = assets?.find(
(asset) => asset.denom === selectedAssetDenom
);

const selectedChain = chains?.find(
(chain) => chain.chainID === selectedAsset?.chainID
);

const usdValue = useUsdValue({ ...selectedAsset, value });
const assetDetails = useGetAssetDetails({
assetDenom: selectedAssetDenom,
amount: value
});

const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
if (!onChangeValue) return;
Expand Down Expand Up @@ -131,10 +120,10 @@ export const AssetChainInput = ({
onKeyDown={handleKeyDown}
/>
<Button onClick={handleChangeAsset} gap={5}>
{selectedAsset ? (
{assetDetails?.assetImage && assetDetails.symbol ? (
<StyledAssetLabel align="center" justify="center" gap={7}>
<img src={selectedAsset?.logoURI} width={23} />
<Text>{selectedAsset?.recommendedSymbol}</Text>
<img src={assetDetails.assetImage} width={23} />
<Text>{assetDetails.symbol}</Text>
</StyledAssetLabel>
) : (
<StyledSelectTokenLabel>
Expand All @@ -149,15 +138,15 @@ export const AssetChainInput = ({
</Button>
</Row>
<Row justify="space-between">
<SmallText>{formatUSD(usdValue?.data ?? 0)}</SmallText>
{selectedAsset ? (
<SmallText>{assetDetails.formattedUsdAmount ?? 0}</SmallText>
{assetDetails?.chainName ? (
<GhostButton
onClick={handleChangeChain}
align="center"
secondary
gap={4}
>
<SmallText>on {selectedChain?.prettyName}</SmallText>
<SmallText>on {assetDetails?.chainName}</SmallText>
<CogIcon color={theme.primary.text.normal} />
</GhostButton>
) : (
Expand Down
4 changes: 2 additions & 2 deletions packages/widget-v2/src/components/Modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { PartialTheme } from "@/widget/theme";

import { ErrorBoundary } from "react-error-boundary";
import { useAtom } from "jotai";
import { errorAtom } from "@/state/errorPage";
import { errorAtom, ErrorType } from "@/state/errorPage";
import { numberOfModalsOpenAtom } from "@/state/modal";

export type ModalProps = {
Expand Down Expand Up @@ -59,7 +59,7 @@ export const createModal = <T extends ModalProps>(

return (
<Modal {...props}>
<ErrorBoundary fallback={null} onError={(error) => setError(error)}>
<ErrorBoundary fallback={null} onError={(error) => setError({ errorType: ErrorType.Unexpected, error })}>
<Component {...props} />
</ErrorBoundary>
</Modal>
Expand Down
97 changes: 97 additions & 0 deletions packages/widget-v2/src/hooks/useGetAssetDetails.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import {
ClientAsset,
skipAssetsAtom,
skipChainsAtom,
} from "@/state/skipClient";
import { getFormattedAssetAmount } from "@/utils/crypto";
import { formatUSD } from "@/utils/intl";
import { useUsdValue } from "@/utils/useUsdValue";
import { Chain } from "@skip-go/client";
import { useAtom } from "jotai";
import { useMemo } from "react";

export type AssetDetails = {
asset?: ClientAsset;
chain?: Chain;
symbol?: string;
assetImage?: string;
chainName?: string;
chainImage?: string;
formattedAmount?: string;
formattedUsdAmount?: string;
};

/**
* @param {string} [params.assetDenom] - The denomination of the asset to retrieve details for.
* @param {string} [params.amount] - The amount of the asset, used for formatting.
* @param {string} [params.amountUsd] - The total value of the asset in usd, used for formatting.
* @param {string} [params.chainId] - The id of the chain associated with the asset.
*
* @returns {AssetDetails} An object containing the following properties:
* - `asset` The asset object corresponding to the provided denomination.
* - `chain` The chain object associated with the provided chain id.
* - `symbol` The symbol of the asset, derived from the asset object.
* - `assetImage` The asset image url derived from the asset object.
* - `chainName` The name of the chain, derived from the chain object.
* - `chainImage` The chain image url, derived from the chain object.
* - `formattedAmount` The formatted amount of the asset, if the amount is provided.
* - `formattedUsdAmount` The formatted usd amount of the asset, if the amountUsd is provided.
*/
export const useGetAssetDetails = ({
assetDenom,
amount,
amountUsd,
chainId,
}: {
assetDenom?: string;
amount?: string;
amountUsd?: string;
chainId?: string;
}): AssetDetails => {
const [{ data: assets }] = useAtom(skipAssetsAtom);
const [{ data: chains }] = useAtom(skipChainsAtom);

const asset = assets?.find((asset) => {
if (chainId) {
return asset.denom === assetDenom && asset.chainID === chainId;
}
return asset.denom === assetDenom;
});
const { data: usdValue } = useUsdValue({...asset, value: amount});
const assetImage = asset?.logoURI;
const symbol = asset?.recommendedSymbol ?? asset?.symbol;

const chain = chains?.find((chain) => {
if (chainId) {
return chain.chainID === chainId;
}
return chain.chainID === asset?.chainID;
});
const chainName = chain?.prettyName ?? chain?.chainName;
const chainImage = chain?.logoURI;

const formattedAmount = amount
? getFormattedAssetAmount(amount, asset?.decimals)
: undefined;

const formattedUsdAmount = useMemo(() => {
if (amountUsd) {
return formatUSD(amountUsd);
}
if (usdValue) {
return formatUSD(usdValue);
}
return;
}, [amountUsd, usdValue]);

return {
asset,
chain,
assetImage,
chainName,
chainImage,
symbol,
formattedAmount,
formattedUsdAmount,
};
};
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,12 @@ import {
RenderWalletListHeader,
Wallet,
} from "@/components/RenderWalletList";
import { WALLET_LIST } from "@/modals/WalletSelectorModal/WalletSelectorFlow";
import { Button } from "@/components/Button";
import { SmallText, Text } from "@/components/Typography";
import { destinationAssetAtom, destinationWalletAtom } from "@/state/swapPage";
import { useAtom, useAtomValue } from "jotai";
import { skipChainsAtom } from "@/state/skipClient";
import { WALLET_LIST } from "../WalletSelectorModal/WalletSelectorModal";

export const ManualAddressModal = createModal((modalProps: ModalProps) => {
const { theme } = modalProps;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { RouteResponse } from "@skip-go/client";
import { SmallText } from "@/components/Typography";
import { useAtom } from "jotai";
import { skipAssetsAtom, ClientAsset, skipChainsAtom } from "@/state/skipClient";
import { ClientAsset } from "@/state/skipClient";
import { Column, Row } from "@/components/Layout";
import styled, { useTheme } from "styled-components";
import { getFormattedAssetAmount } from "@/utils/crypto";
Expand All @@ -10,6 +9,7 @@ import { useMemo } from "react";
import { StyledAnimatedBorder } from "@/pages/SwapExecutionPage/SwapExecutionPageRouteDetailedRow";
import { TransactionHistoryModalItemDetails } from "./TransactionHistoryModalItemDetails";
import { HistoryArrowIcon } from "@/icons/HistoryArrowIcon";
import { useGetAssetDetails } from "@/hooks/useGetAssetDetails";

export type TxStatus = {
chainId: string;
Expand Down Expand Up @@ -56,22 +56,27 @@ export const TransactionHistoryModalItem = ({
timestamp,
status,
} = txHistoryItem;
const [{ data: assets }] = useAtom(skipAssetsAtom);
const [{ data: chains }] = useAtom(skipChainsAtom)
const sourceChain = chains?.find(c => c.chainID === sourceAssetChainID)
const sourceChainImage = sourceChain?.logoURI

const sourceAssetDetails = useGetAssetDetails({
assetDenom: sourceAssetDenom,
chainId: sourceAssetChainID,
});

const destinationAssetDetails = useGetAssetDetails({
assetDenom: destAssetDenom,
chainId: destAssetChainID
});

const source = {
amount: amountIn,
asset: assets?.find((asset) => asset.denom === sourceAssetDenom),
chainImage: sourceChainImage ?? "",
asset: sourceAssetDetails.asset,
chainImage: sourceAssetDetails?.chainImage ?? "",
};

const destinationChain = chains?.find(c => c.chainID === destAssetChainID)
const destinationChainImage = destinationChain?.logoURI
const destination = {
amount: amountOut,
asset: assets?.find((asset) => asset.denom === destAssetDenom),
chainImage: destinationChainImage ?? "",
asset: destinationAssetDetails.asset,
chainImage: destinationAssetDetails.chainImage ?? "",
};

const renderStatus = useMemo(() => {
Expand Down Expand Up @@ -116,7 +121,7 @@ export const TransactionHistoryModalItem = ({
<HistoryArrowIcon color={theme.primary.text.lowContrast} />
<RenderAssetAmount {...destination} />
<SmallText normalTextColor>
on {destinationChain?.prettyName ?? destinationChain?.chainName}
on {destinationAssetDetails.chainName}
</SmallText>
</Row>
<Row align="center" gap={10}>
Expand All @@ -127,10 +132,8 @@ export const TransactionHistoryModalItem = ({
{showDetails && (
<TransactionHistoryModalItemDetails
status={status}
sourceChainName={sourceChain?.prettyName ?? sourceChain?.chainName ?? "--"}
destinationChainName={
destinationChain?.prettyName ?? destinationChain?.chainName ?? "--"
}
sourceChainName={sourceAssetDetails.chainName ?? "--"}
destinationChainName={destinationAssetDetails.chainName ?? "--"}
absoluteTimeString={absoluteTimeString}
relativeTimeString={relativeTime}
transactionID={txStatus[0].txHash}
Expand Down
56 changes: 36 additions & 20 deletions packages/widget-v2/src/pages/ErrorPage/ErrorPage.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,40 @@
import { errorAtom } from "@/state/errorPage";
import { useResetAtom } from "jotai/utils";
import { errorAtom, ErrorType } from "@/state/errorPage";
import { useAtom } from "jotai";
import { ErrorPageTradeWarning } from "./ErrorPageTradeWarning";
import { ErrorPageAuthFailed } from "./ErrorPageAuthFailed";
import { ErrorPageTransactionFailed } from "./ErrorPageTransactionFailed";
import { ErrorPageUnexpected } from "./ErrorPageUnexpected";
import { useMemo } from "react";
import { Column } from "@/components/Layout";
import { ErrorPageTimeout } from "./ErrorPageTimeout";
import { ErrorPageTradeAdditionalSigningRequired } from "./ErrorPageTradeAdditionalSingingRequired";
import { ErrorPageTransactionReverted } from "./ErrorPageTransactionReverted";

export const ErrorPage = ({
resetErrorBoundary,
}: {
error?: Error;
componentStack?: string;
resetErrorBoundary?: () => void;
}) => {
const resetError = useResetAtom(errorAtom);
export const ErrorPage = () => {
const [error] = useAtom(errorAtom);

const handleReset = () => {
resetError();
resetErrorBoundary?.();
};
const renderErrorVariant = useMemo(() => {
switch (error?.errorType) {
case ErrorType.AuthFailed:
return <ErrorPageAuthFailed {...error} />;
case ErrorType.Timeout:
return <ErrorPageTimeout {...error} />;
case ErrorType.AdditionalSigningRequired:
return <ErrorPageTradeAdditionalSigningRequired {...error} />;
case ErrorType.TradeWarning:
return <ErrorPageTradeWarning {...error} />;
case ErrorType.TransactionFailed:
return <ErrorPageTransactionFailed {...error} />;
case ErrorType.TransactionReverted:
return <ErrorPageTransactionReverted {...error} />;
case ErrorType.Unexpected:
return <ErrorPageUnexpected error={error.error} />;
default:
return;
}
}, [error]);

return (
<div>
error page
<button onClick={handleReset}>reset</button>
</div>
);
if (error?.errorType === undefined) return;

return <Column gap={5}>{renderErrorVariant}</Column>
};
32 changes: 32 additions & 0 deletions packages/widget-v2/src/pages/ErrorPage/ErrorPageAuthFailed.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { ErrorState } from "@/components/ErrorState";
import { MainButton } from "@/components/MainButton";
import { ICONS } from "@/icons";
import { useTheme } from "styled-components";

export type ErrorPageAuthFailedProps = {
onClickBack: () => void;
};

export const ErrorPageAuthFailed = ({
onClickBack,
}: ErrorPageAuthFailedProps) => {
const theme = useTheme();

return (
<>
<ErrorState
title="Transaction failed"
description="User rejected authentication request"
icon={ICONS.triangleWarning}
backgroundColor={theme.error.background}
textColor={theme.error.text}
/>
<MainButton
label="Back"
icon={ICONS.leftArrow}
onClick={onClickBack}
backgroundColor={theme.error.text}
/>
</>
);
};
Loading
Loading