Skip to content

Commit a10b218

Browse files
committed
feat: adds token not supported screen
1 parent c0da29c commit a10b218

File tree

4 files changed

+282
-30
lines changed

4 files changed

+282
-30
lines changed
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import type { Chain } from "../../../../chains/types.js";
2+
import { iconSize } from "../../../core/design-system/index.js";
3+
import { useChainMetadata } from "../../../core/hooks/others/useChainQuery.js";
4+
import { AccentFailIcon } from "../ConnectWallet/icons/AccentFailIcon.js";
5+
import { Spacer } from "../components/Spacer.js";
6+
import { Spinner } from "../components/Spinner.js";
7+
import { Container } from "../components/basic.js";
8+
import { Text } from "../components/text.js";
9+
10+
export interface UnsupportedTokenScreenProps {
11+
/**
12+
* The chain the token is on
13+
*/
14+
chain: Chain;
15+
/**
16+
* Callback when user wants to try a different token
17+
*/
18+
onTryDifferentToken: () => void;
19+
/**
20+
* Optional callback when user wants to contact support
21+
*/
22+
onContactSupport?: () => void;
23+
}
24+
25+
/**
26+
* Screen displayed when a specified token is not supported by the Bridge API
27+
* @internal
28+
*/
29+
export function UnsupportedTokenScreen(props: UnsupportedTokenScreenProps) {
30+
const { chain } = props;
31+
32+
const { data: chainMetadata } = useChainMetadata(chain);
33+
34+
if (chainMetadata?.testnet) {
35+
return (
36+
<Container
37+
animate="fadein"
38+
flex="column"
39+
center="both"
40+
style={{ minHeight: "350px" }}
41+
>
42+
{/* Error Icon */}
43+
<AccentFailIcon size={iconSize["3xl"]} />
44+
<Spacer y="lg" />
45+
46+
{/* Title */}
47+
<Text center color="primaryText" size="lg" weight={600}>
48+
Testnet Not Supported
49+
</Text>
50+
<Spacer y="sm" />
51+
52+
{/* Description */}
53+
<Text
54+
center
55+
color="secondaryText"
56+
size="sm"
57+
style={{ maxWidth: "280px", lineHeight: 1.5 }}
58+
>
59+
The Universal Bridge does not support testnets at this time.
60+
</Text>
61+
</Container>
62+
);
63+
}
64+
65+
return (
66+
<Container
67+
animate="fadein"
68+
flex="column"
69+
center="both"
70+
style={{ minHeight: "350px" }}
71+
>
72+
{/* Loading Spinner */}
73+
<Spinner size="xl" color="accentText" />
74+
<Spacer y="lg" />
75+
76+
{/* Title */}
77+
<Text center color="primaryText" size="lg" weight={600}>
78+
Indexing Token
79+
</Text>
80+
<Spacer y="sm" />
81+
82+
{/* Description */}
83+
<Text
84+
center
85+
color="secondaryText"
86+
size="sm"
87+
style={{ maxWidth: "280px", lineHeight: 1.5 }}
88+
>
89+
This token is being indexed by the Universal Bridge. Please check back
90+
later.
91+
</Text>
92+
</Container>
93+
);
94+
}

packages/thirdweb/src/react/web/ui/Bridge/common/TokenAndChain.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ export function TokenAndChain({
2626
size,
2727
style,
2828
}: {
29-
token: Token;
29+
token: Omit<Token, "priceUsd">;
3030
client: ThirdwebClient;
3131
size: keyof typeof iconSize;
3232
style?: React.CSSProperties;
@@ -99,7 +99,7 @@ export function TokenAndChain({
9999
}
100100

101101
export function TokenIconWithFallback(props: {
102-
token: Token;
102+
token: Omit<Token, "priceUsd">;
103103
size: keyof typeof iconSize;
104104
client: ThirdwebClient;
105105
}) {

packages/thirdweb/src/react/web/ui/PayEmbed.tsx

Lines changed: 91 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import {
3030
BridgeOrchestrator,
3131
type UIOptions,
3232
} from "./Bridge/BridgeOrchestrator.js";
33+
import { UnsupportedTokenScreen } from "./Bridge/UnsupportedTokenScreen.js";
3334
import { EmbedContainer } from "./ConnectWallet/Modal/ConnectEmbed.js";
3435
import { useConnectLocale } from "./ConnectWallet/locale/getConnectLocale.js";
3536
import BuyScreen from "./ConnectWallet/screens/Buy/BuyScreen.js";
@@ -159,6 +160,15 @@ export type PayEmbedProps = {
159160
paymentLinkId?: string;
160161
};
161162

163+
// Enhanced UIOptions to handle unsupported token state
164+
type UIOptionsResult =
165+
| { type: "success"; data: UIOptions }
166+
| {
167+
type: "unsupported_token";
168+
token?: { address: string; symbol?: string; name?: string };
169+
chain: Chain;
170+
};
171+
162172
/**
163173
* Embed a prebuilt UI for funding wallets, purchases or transactions with crypto or fiat.
164174
*
@@ -320,13 +330,16 @@ export function PayEmbed(props: PayEmbedProps) {
320330

321331
const bridgeDataQuery = useQuery({
322332
queryKey: ["bridgeData", props],
323-
queryFn: async (): Promise<UIOptions> => {
333+
queryFn: async (): Promise<UIOptionsResult> => {
324334
if (!props.payOptions?.mode) {
325335
const ETH = await getToken(props.client, NATIVE_TOKEN_ADDRESS, 1);
326336
return {
327-
mode: "fund_wallet",
328-
destinationToken: ETH,
329-
initialAmount: "0.01",
337+
type: "success",
338+
data: {
339+
mode: "fund_wallet",
340+
destinationToken: ETH,
341+
initialAmount: "0.01",
342+
},
330343
};
331344
}
332345

@@ -335,23 +348,38 @@ export function PayEmbed(props: PayEmbedProps) {
335348
if (!prefillInfo) {
336349
const ETH = await getToken(props.client, NATIVE_TOKEN_ADDRESS, 1);
337350
return {
338-
mode: "fund_wallet",
339-
destinationToken: ETH,
351+
type: "success",
352+
data: {
353+
mode: "fund_wallet",
354+
destinationToken: ETH,
355+
},
340356
};
341357
}
342358
const token = await getToken(
343359
props.client,
344360
prefillInfo.token?.address || NATIVE_TOKEN_ADDRESS,
345361
prefillInfo.chain.id,
362+
).catch((err) =>
363+
err.message.includes("not found") ? undefined : Promise.reject(err),
346364
);
347365
if (!token) {
348-
console.error("Token not found for prefillInfo", prefillInfo);
349-
throw new Error("Token not found");
366+
return {
367+
type: "unsupported_token",
368+
token: {
369+
address: prefillInfo.token?.address || NATIVE_TOKEN_ADDRESS,
370+
symbol: prefillInfo.token?.symbol,
371+
name: prefillInfo.token?.name,
372+
},
373+
chain: prefillInfo.chain,
374+
};
350375
}
351376
return {
352-
mode: "fund_wallet",
353-
destinationToken: token,
354-
initialAmount: prefillInfo.amount,
377+
type: "success",
378+
data: {
379+
mode: "fund_wallet",
380+
destinationToken: token,
381+
initialAmount: prefillInfo.amount,
382+
},
355383
};
356384
}
357385

@@ -361,41 +389,66 @@ export function PayEmbed(props: PayEmbedProps) {
361389
props.client,
362390
paymentInfo.token?.address || NATIVE_TOKEN_ADDRESS,
363391
paymentInfo.chain.id,
392+
).catch((err) =>
393+
err.message.includes("not found") ? undefined : Promise.reject(err),
364394
);
365395
if (!token) {
366-
console.error("Token not found for paymentInfo", paymentInfo);
367-
throw new Error("Token not found");
396+
return {
397+
type: "unsupported_token",
398+
token: {
399+
address: paymentInfo.token?.address || NATIVE_TOKEN_ADDRESS,
400+
symbol: paymentInfo.token?.symbol,
401+
name: paymentInfo.token?.name,
402+
},
403+
chain: paymentInfo.chain,
404+
};
368405
}
369406
const amount =
370407
"amount" in paymentInfo
371408
? paymentInfo.amount
372409
: toTokens(paymentInfo.amountWei, token.decimals);
373410
return {
374-
mode: "direct_payment",
375-
paymentInfo: {
376-
token,
377-
amount,
378-
sellerAddress: paymentInfo.sellerAddress as `0x${string}`,
379-
metadata: {
380-
name: props.payOptions?.metadata?.name || "Direct Payment",
381-
image: props.payOptions?.metadata?.image || "",
411+
type: "success",
412+
data: {
413+
mode: "direct_payment",
414+
paymentInfo: {
415+
token,
416+
amount,
417+
sellerAddress: paymentInfo.sellerAddress as `0x${string}`,
418+
metadata: {
419+
name: props.payOptions?.metadata?.name || "Direct Payment",
420+
image: props.payOptions?.metadata?.image || "",
421+
},
422+
feePayer: paymentInfo.feePayer,
382423
},
383-
feePayer: paymentInfo.feePayer,
384424
},
385425
};
386426
}
387427

388428
if (props.payOptions?.mode === "transaction") {
389429
return {
390-
mode: "transaction",
391-
transaction: props.payOptions.transaction,
430+
type: "success",
431+
data: {
432+
mode: "transaction",
433+
transaction: props.payOptions.transaction,
434+
},
392435
};
393436
}
394437

395438
throw new Error("Invalid mode");
396439
},
397440
});
398441

442+
const handleTryDifferentToken = () => {
443+
// Refetch to allow user to try again (they might have changed something)
444+
bridgeDataQuery.refetch();
445+
};
446+
447+
const handleContactSupport = () => {
448+
// Open support link or modal (this could be configurable via props)
449+
window.open("https://support.thirdweb.com", "_blank");
450+
};
451+
399452
let content = null;
400453
if (!localeQuery.data || bridgeDataQuery.isLoading) {
401454
content = (
@@ -410,17 +463,27 @@ export function PayEmbed(props: PayEmbedProps) {
410463
<Spinner size="xl" color="secondaryText" />
411464
</div>
412465
);
413-
} else {
414-
content = bridgeDataQuery.data ? (
466+
} else if (bridgeDataQuery.data?.type === "unsupported_token") {
467+
// Show unsupported token screen
468+
content = (
469+
<UnsupportedTokenScreen
470+
chain={bridgeDataQuery.data.chain}
471+
onTryDifferentToken={handleTryDifferentToken}
472+
onContactSupport={handleContactSupport}
473+
/>
474+
);
475+
} else if (bridgeDataQuery.data?.type === "success") {
476+
// Show normal bridge orchestrator
477+
content = (
415478
<BridgeOrchestrator
416479
client={props.client}
417-
uiOptions={bridgeDataQuery.data}
480+
uiOptions={bridgeDataQuery.data.data}
418481
connectOptions={props.connectOptions}
419482
connectLocale={localeQuery.data}
420483
purchaseData={props.payOptions?.purchaseData}
421484
paymentLinkId={props.paymentLinkId}
422485
/>
423-
) : null;
486+
);
424487
}
425488

426489
return (

0 commit comments

Comments
 (0)