Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
8 changes: 8 additions & 0 deletions .changeset/moody-pants-play.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
"thirdweb": patch
---

Miscellaneous PayEmbed error improvements:
- Adds title and message to PayEmbed errors
- Prevents propagating raw errors to the user when in purchase or transaction mode
- Fixes bubble alignment on pulsing animation
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,8 @@ const pulseAnimation = keyframes`
const PulsingDot = /* @__PURE__ */ StyledDiv(() => {
return {
background: "currentColor",
width: "9px",
height: "9px",
width: "10px",
height: "10px",
borderRadius: "50%",
'&[data-active="true"]': {
animation: `${pulseAnimation} 1s infinite`,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -186,9 +186,14 @@ export function TransactionModeScreen(props: {
) : activeAccount ? (
<Container flex="column" gap="sm">
{insufficientFunds && (
<Text size="sm" color="danger" style={{ textAlign: "center" }}>
Insufficient funds
</Text>
<div>
<Text color="danger" size="xs" center multiline>
Insufficient Funds
</Text>
<Text size="xs" center multiline>
Select another token or pay with a debit card.
</Text>
</div>
)}
<Container
flex="row"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,7 @@ import {
import type { PayUIOptions } from "../../../../../../core/hooks/connection/ConnectButtonProps.js";
import { useBuyWithFiatQuote } from "../../../../../../core/hooks/pay/useBuyWithFiatQuote.js";
import { PREFERRED_FIAT_PROVIDER_STORAGE_KEY } from "../../../../../../core/utils/storage.js";
import {
defaultMessage,
getErrorMessage,
} from "../../../../../utils/errors.js";
import { getErrorMessage } from "../../../../../utils/errors.js";
import {
Drawer,
DrawerOverlay,
Expand Down Expand Up @@ -180,7 +177,7 @@ export function FiatScreenContent(props: {
)}

<Container flex="column" gap="sm">
<Text size="sm">Pay with credit card</Text>
<Text size="sm">Pay with a debit card</Text>
<div>
<PayWithCreditCard
isLoading={fiatQuoteQuery.isLoading}
Expand Down Expand Up @@ -242,9 +239,14 @@ export function FiatScreenContent(props: {
/>
</Text>
) : (
<Text color="danger" size="sm" center multiline>
{errorMsg.message || defaultMessage}
</Text>
<div>
<Text color="danger" size="xs" center multiline>
{errorMsg.title}
</Text>
<Text size="xs" center multiline>
{errorMsg.message}
</Text>
</div>
)}
</div>
)}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { CrossCircledIcon } from "@radix-ui/react-icons";
import { useState } from "react";
import { useMemo, useState } from "react";
import { trackPayEvent } from "../../../../../../../analytics/track/pay.js";
import type { Chain } from "../../../../../../../chains/types.js";
import type { ThirdwebClient } from "../../../../../../../client/client.js";
Expand All @@ -13,7 +12,6 @@ import {
waitForReceipt,
} from "../../../../../../../transaction/actions/wait-for-tx-receipt.js";
import { useCustomTheme } from "../../../../../../core/design-system/CustomThemeProvider.js";
import { iconSize } from "../../../../../../core/design-system/index.js";
import { Spacer } from "../../../../components/Spacer.js";
import { Spinner } from "../../../../components/Spinner.js";
import { StepBar } from "../../../../components/StepBar.js";
Expand All @@ -27,6 +25,7 @@ import { Step } from "../Stepper.js";
import type { PayerInfo } from "../types.js";
import { SwapSummary } from "./SwapSummary.js";
import { addPendingTx } from "./pendingSwapTx.js";
import { ErrorText } from "./ErrorText.js";

/**
* @internal
Expand Down Expand Up @@ -59,13 +58,46 @@ export function SwapConfirmationScreen(props: {
const initialStep = needsApprovalStep ? "approval" : "swap";

const [step, setStep] = useState<"approval" | "swap">(initialStep);
const [error, setError] = useState<string | undefined>();
const [status, setStatus] = useState<
"pending" | "success" | "error" | "idle"
>("idle");

const receiver = props.quote.swapDetails.toAddress;
const sender = props.quote.swapDetails.fromAddress;

const uiErrorMessgae = useMemo(() => {
if (step === "approval" && status === "error" && error) {
if (error.toLowerCase().includes("user rejected")) {
return {
title: "Failed to Approve",
message: "Your wallet rejected the approval request.",
};
}
return {
title: "Failed to Approve",
message:
"Your wallet failed to approve the transaction for an unknown reason. Please try again or contact support.",
};
}

if (step === "swap" && status === "error" && error) {
if (error.toLowerCase().includes("user rejected")) {
return {
title: "Failed to Confirm",
message: "Your wallet rejected the confirmation request.",
};
}
return {
title: "Failed to Confirm",
message:
"Your wallet failed to confirm the transaction for an unknown reason. Please try again or contact support.",
};
}

return undefined;
}, [error, step, status]);

return (
<Container p="lg">
<ModalHeader title={props.title} onBack={props.onBack} />
Expand Down Expand Up @@ -128,15 +160,12 @@ export function SwapConfirmationScreen(props: {
</>
)}

{status === "error" && (
{uiErrorMessgae && (
<>
<Container flex="row" gap="xs" center="both" color="danger">
<CrossCircledIcon width={iconSize.sm} height={iconSize.sm} />
<Text color="danger" size="sm">
{step === "approval" ? "Failed to Approve" : "Failed to Confirm"}
</Text>
</Container>

<ErrorText
title={uiErrorMessgae.title}
message={uiErrorMessgae.message}
/>
<Spacer y="md" />
</>
)}
Expand Down Expand Up @@ -224,6 +253,7 @@ export function SwapConfirmationScreen(props: {
setStatus("idle");
} catch (e) {
console.error(e);
setError((e as Error).message);
setStatus("error");
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { CrossCircledIcon } from "@radix-ui/react-icons";
import { iconSize } from "../../../../../../core/design-system/index.js";
import { Container } from "../../../../components/basic.js";
import { Text } from "../../../../components/text.js";

export function ErrorText(props: {
title: string;
message: string;
}) {
return (
<Container gap="xxs" flex="column">
<Container flex="row" gap="xxs" center="both" color="danger">
<CrossCircledIcon width={iconSize.sm} height={iconSize.sm} />
<Text color="danger" size="sm">
{props.title}
</Text>
</Container>
<Text center size="xs">
{props.message}
</Text>
</Container>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,7 @@ import type { Account } from "../../../../../../../wallets/interfaces/wallet.js"
import type { PayUIOptions } from "../../../../../../core/hooks/connection/ConnectButtonProps.js";
import { useWalletBalance } from "../../../../../../core/hooks/others/useWalletBalance.js";
import { useBuyWithCryptoQuote } from "../../../../../../core/hooks/pay/useBuyWithCryptoQuote.js";
import {
defaultMessage,
getErrorMessage,
} from "../../../../../utils/errors.js";
import { getErrorMessage } from "../../../../../utils/errors.js";
import type { PayEmbedConnectOptions } from "../../../../PayEmbed.js";
import {
Drawer,
Expand Down Expand Up @@ -301,17 +298,25 @@ export function SwapScreenContent(props: {
/>
</Text>
) : (
<Text color="danger" size="xs" center multiline>
{errorMsg.message || defaultMessage}
</Text>
<div>
<Text color="danger" size="xs" center multiline>
{errorMsg.title}
</Text>
<Text size="xs" center multiline>
{errorMsg.message}
</Text>
</div>
)}
</div>
)}

{!errorMsg && isNotEnoughBalance && (
<div>
<Text color="danger" size="xs" center multiline>
Insufficient funds
Insufficient Funds
</Text>
<Text size="xs" center multiline>
Select another token or pay with a debit card.
</Text>
</div>
)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -276,7 +276,7 @@ export function TokenSelectorScreen(props: {
>
<CardStackIcon width={iconSize.md} height={iconSize.md} />
<Text size="sm" color="primaryText">
Pay with credit card
Pay with a debit card
</Text>
</Container>
</Button>
Expand Down Expand Up @@ -362,7 +362,7 @@ function WalletRowWithBalances(props: {
) : (
<Container style={{ padding: spacing.sm }}>
<Text size="sm" color="secondaryText">
Insufficient funds
Insufficient Funds
</Text>
</Container>
)}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { CheckCircledIcon } from "@radix-ui/react-icons";
import { useQuery } from "@tanstack/react-query";
import { useState } from "react";
import { useMemo, useState } from "react";
import type { Chain } from "../../../../../../../chains/types.js";
import { getCachedChain } from "../../../../../../../chains/utils.js";
import type { ThirdwebClient } from "../../../../../../../client/client.js";
Expand Down Expand Up @@ -35,6 +35,7 @@ import { Step } from "../Stepper.js";
import type { PayerInfo } from "../types.js";
import { ConnectorLine } from "./ConfirmationScreen.js";
import { SwapSummary } from "./SwapSummary.js";
import { ErrorText } from "./ErrorText.js";

type TransferConfirmationScreenProps = {
title: string;
Expand Down Expand Up @@ -109,6 +110,42 @@ export function TransferConfirmationScreen(
refetchInterval: 30 * 1000,
});

const uiErrorMessgae = useMemo(() => {
if (step === "approve" && status.id === "error" && status.error) {
if (status.error.toLowerCase().includes("user rejected")) {
return {
title: "Failed to Approve",
message: "Your wallet rejected the approval request.",
};
}
return {
title: "Failed to Approve",
message:
"Your wallet failed to approve the transaction for an unknown reason. Please try again or contact support.",
};
}

if (
(step === "transfer" || step === "execute") &&
status.id === "error" &&
status.error
) {
if (status.error.toLowerCase().includes("user rejected")) {
return {
title: "Failed to Confirm",
message: "Your wallet rejected the confirmation request.",
};
}
return {
title: "Failed to Confirm",
message:
"Your wallet failed to confirm the transaction for an unknown reason. Please try again or contact support.",
};
}

return undefined;
}, [step, status]);

if (transferQuery.isLoading) {
return (
<Container p="lg">
Expand Down Expand Up @@ -190,15 +227,12 @@ export function TransferConfirmationScreen(
</>
)}

{status.id === "error" && (
{uiErrorMessgae && (
<>
<Container flex="row" gap="xs" center="both" color="danger">
<Text color="danger" size="sm" style={{ textAlign: "center" }}>
{step === "transfer"
? `${status.error || "Failed to Transfer"}`
: "Failed to Execute"}
</Text>
</Container>
<ErrorText
title={uiErrorMessgae.title}
message={uiErrorMessgae.message}
/>
<Spacer y="md" />
</>
)}
Expand Down
15 changes: 11 additions & 4 deletions packages/thirdweb/src/react/web/utils/errors.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
type ApiError = {
code: string;
message?: string;
title: string;
message: string;
data?: {
minimumAmountUSDCents?: string;
requestedAmountUSDCents?: string;
Expand All @@ -9,19 +10,25 @@ type ApiError = {
};
};

export const defaultMessage = "Unable to get price quote";
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
export function getErrorMessage(err: any): ApiError {
if (typeof err.error === "object" && err.error.code) {
if (err.error.code === "MINIMUM_PURCHASE_AMOUNT") {
return {
code: "MINIMUM_PURCHASE_AMOUNT",
message: "Purchase amount is too low",
title: "Amount Too Low",
message:
"The requested amount is less than the minimum purchase. Try another provider or amount.",
};
}
}

console.error(err);

return {
code: "UNABLE_TO_GET_PRICE_QUOTE",
message: defaultMessage,
title: "Failed to Find Quote",
message:
"We couldn't get a quote for this token pair. Select another token or pay with a debit card.",
};
}
Loading