From f0ecd8ea662bf20e737d846e8ed836baa37334ee Mon Sep 17 00:00:00 2001 From: gregfromstl Date: Sun, 13 Apr 2025 18:24:31 -0700 Subject: [PATCH 1/2] fix: tool-4077 Error message cleanup on UB embed Closes: tool-4077 --- .changeset/moody-pants-play.md | 8 +++ .../ui/ConnectWallet/screens/Buy/Stepper.tsx | 4 +- .../screens/Buy/TransactionModeScreen.tsx | 11 ++-- .../screens/Buy/fiat/FiatScreenContent.tsx | 18 ++++--- .../screens/Buy/swap/ConfirmationScreen.tsx | 52 +++++++++++++++---- .../screens/Buy/swap/ErrorText.tsx | 23 ++++++++ .../screens/Buy/swap/SwapScreenContent.tsx | 21 +++++--- .../screens/Buy/swap/TokenSelectorScreen.tsx | 4 +- .../Buy/swap/TransferConfirmationScreen.tsx | 52 +++++++++++++++---- .../thirdweb/src/react/web/utils/errors.ts | 15 ++++-- 10 files changed, 161 insertions(+), 47 deletions(-) create mode 100644 .changeset/moody-pants-play.md create mode 100644 packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/swap/ErrorText.tsx diff --git a/.changeset/moody-pants-play.md b/.changeset/moody-pants-play.md new file mode 100644 index 00000000000..96fc751b96e --- /dev/null +++ b/.changeset/moody-pants-play.md @@ -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 diff --git a/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/Stepper.tsx b/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/Stepper.tsx index 208a13f714e..8a862ba46ef 100644 --- a/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/Stepper.tsx +++ b/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/Stepper.tsx @@ -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`, diff --git a/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/TransactionModeScreen.tsx b/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/TransactionModeScreen.tsx index c5ef33abce5..74924c3cb58 100644 --- a/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/TransactionModeScreen.tsx +++ b/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/TransactionModeScreen.tsx @@ -186,9 +186,14 @@ export function TransactionModeScreen(props: { ) : activeAccount ? ( {insufficientFunds && ( - - Insufficient funds - +
+ + Insufficient Funds + + + Select another token or pay with a debit card. + +
)} - Pay with credit card + Pay with a debit card
) : ( - - {errorMsg.message || defaultMessage} - +
+ + {errorMsg.title} + + + {errorMsg.message} + +
)}
)} diff --git a/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/swap/ConfirmationScreen.tsx b/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/swap/ConfirmationScreen.tsx index b14c201afee..34ed89eb0d8 100644 --- a/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/swap/ConfirmationScreen.tsx +++ b/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/swap/ConfirmationScreen.tsx @@ -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"; @@ -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"; @@ -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 @@ -59,6 +58,7 @@ export function SwapConfirmationScreen(props: { const initialStep = needsApprovalStep ? "approval" : "swap"; const [step, setStep] = useState<"approval" | "swap">(initialStep); + const [error, setError] = useState(); const [status, setStatus] = useState< "pending" | "success" | "error" | "idle" >("idle"); @@ -66,6 +66,38 @@ export function SwapConfirmationScreen(props: { 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 ( @@ -128,15 +160,12 @@ export function SwapConfirmationScreen(props: { )} - {status === "error" && ( + {uiErrorMessgae && ( <> - - - - {step === "approval" ? "Failed to Approve" : "Failed to Confirm"} - - - + )} @@ -224,6 +253,7 @@ export function SwapConfirmationScreen(props: { setStatus("idle"); } catch (e) { console.error(e); + setError((e as Error).message); setStatus("error"); } } diff --git a/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/swap/ErrorText.tsx b/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/swap/ErrorText.tsx new file mode 100644 index 00000000000..729783282f9 --- /dev/null +++ b/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/swap/ErrorText.tsx @@ -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 ( + + + + + {props.title} + + + + {props.message} + + + ); +} diff --git a/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/swap/SwapScreenContent.tsx b/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/swap/SwapScreenContent.tsx index bdddf776f0a..4a94acf1e5e 100644 --- a/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/swap/SwapScreenContent.tsx +++ b/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/swap/SwapScreenContent.tsx @@ -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, @@ -301,9 +298,14 @@ export function SwapScreenContent(props: { /> ) : ( - - {errorMsg.message || defaultMessage} - +
+ + {errorMsg.title} + + + {errorMsg.message} + +
)} )} @@ -311,7 +313,10 @@ export function SwapScreenContent(props: { {!errorMsg && isNotEnoughBalance && (
- Insufficient funds + Insufficient Funds + + + Select another token or pay with a debit card.
)} diff --git a/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/swap/TokenSelectorScreen.tsx b/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/swap/TokenSelectorScreen.tsx index f1ae7992858..b6e1945eca2 100644 --- a/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/swap/TokenSelectorScreen.tsx +++ b/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/swap/TokenSelectorScreen.tsx @@ -276,7 +276,7 @@ export function TokenSelectorScreen(props: { > - Pay with credit card + Pay with a debit card
@@ -362,7 +362,7 @@ function WalletRowWithBalances(props: { ) : ( - Insufficient funds + Insufficient Funds )} diff --git a/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/swap/TransferConfirmationScreen.tsx b/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/swap/TransferConfirmationScreen.tsx index b8373a060ef..b171270ea65 100644 --- a/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/swap/TransferConfirmationScreen.tsx +++ b/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/swap/TransferConfirmationScreen.tsx @@ -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"; @@ -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; @@ -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 ( @@ -190,15 +227,12 @@ export function TransferConfirmationScreen( )} - {status.id === "error" && ( + {uiErrorMessgae && ( <> - - - {step === "transfer" - ? `${status.error || "Failed to Transfer"}` - : "Failed to Execute"} - - + )} diff --git a/packages/thirdweb/src/react/web/utils/errors.ts b/packages/thirdweb/src/react/web/utils/errors.ts index 84a911d4d60..320eec6725b 100644 --- a/packages/thirdweb/src/react/web/utils/errors.ts +++ b/packages/thirdweb/src/react/web/utils/errors.ts @@ -1,6 +1,7 @@ type ApiError = { code: string; - message?: string; + title: string; + message: string; data?: { minimumAmountUSDCents?: string; requestedAmountUSDCents?: string; @@ -9,19 +10,25 @@ type ApiError = { }; }; -export const defaultMessage = "Unable to get price quote"; // biome-ignore lint/suspicious/noExplicitAny: 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.", }; } From 2cf63bbffdf3bb398e7ef82b0c75999101323bd0 Mon Sep 17 00:00:00 2001 From: gregfromstl Date: Sun, 13 Apr 2025 18:35:40 -0700 Subject: [PATCH 2/2] lint --- .../screens/Buy/swap/ConfirmationScreen.tsx | 10 +++++----- .../screens/Buy/swap/TransferConfirmationScreen.tsx | 10 +++++----- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/swap/ConfirmationScreen.tsx b/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/swap/ConfirmationScreen.tsx index 34ed89eb0d8..5f55674379f 100644 --- a/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/swap/ConfirmationScreen.tsx +++ b/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/swap/ConfirmationScreen.tsx @@ -23,9 +23,9 @@ import { StyledDiv } from "../../../../design-system/elements.js"; import type { ERC20OrNativeToken } from "../../nativeToken.js"; import { Step } from "../Stepper.js"; import type { PayerInfo } from "../types.js"; +import { ErrorText } from "./ErrorText.js"; import { SwapSummary } from "./SwapSummary.js"; import { addPendingTx } from "./pendingSwapTx.js"; -import { ErrorText } from "./ErrorText.js"; /** * @internal @@ -66,7 +66,7 @@ export function SwapConfirmationScreen(props: { const receiver = props.quote.swapDetails.toAddress; const sender = props.quote.swapDetails.fromAddress; - const uiErrorMessgae = useMemo(() => { + const uiErrorMessage = useMemo(() => { if (step === "approval" && status === "error" && error) { if (error.toLowerCase().includes("user rejected")) { return { @@ -160,11 +160,11 @@ export function SwapConfirmationScreen(props: { )} - {uiErrorMessgae && ( + {uiErrorMessage && ( <> diff --git a/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/swap/TransferConfirmationScreen.tsx b/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/swap/TransferConfirmationScreen.tsx index b171270ea65..8242ef509ce 100644 --- a/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/swap/TransferConfirmationScreen.tsx +++ b/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/swap/TransferConfirmationScreen.tsx @@ -34,8 +34,8 @@ import { type ERC20OrNativeToken, isNativeToken } from "../../nativeToken.js"; 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"; +import { SwapSummary } from "./SwapSummary.js"; type TransferConfirmationScreenProps = { title: string; @@ -110,7 +110,7 @@ export function TransferConfirmationScreen( refetchInterval: 30 * 1000, }); - const uiErrorMessgae = useMemo(() => { + const uiErrorMessage = useMemo(() => { if (step === "approve" && status.id === "error" && status.error) { if (status.error.toLowerCase().includes("user rejected")) { return { @@ -227,11 +227,11 @@ export function TransferConfirmationScreen( )} - {uiErrorMessgae && ( + {uiErrorMessage && ( <>