Skip to content

Commit cf6d447

Browse files
committed
SDK: BuyWidget Improvements
1 parent ef9cf34 commit cf6d447

File tree

25 files changed

+1146
-731
lines changed

25 files changed

+1146
-731
lines changed

.changeset/slow-crews-thank.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
"thirdweb": patch
3+
---
4+
5+
BuyWidget UI improvements and new features:
6+
7+
- `chain`, and `amount` props are now optional
8+
- User can always change the token and chain selection in the widget
9+
- Both fiat and token amounts are editable
10+
- connected wallet can be disconnected from the widget
11+
- current balance displayed in the widget

apps/playground-web/src/app/payments/commerce/CheckoutPlayground.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ const defaultOptions: BridgeComponentsPlaygroundOptions = {
1818
sellerAddress: "0x0000000000000000000000000000000000000000",
1919
title: "",
2020
transactionData: "",
21+
receiverAddress: undefined,
2122
currency: "USD",
2223
showThirdwebBranding: true,
2324
},

apps/playground-web/src/app/payments/components/LeftSection.tsx

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,7 @@ export function LeftSection(props: {
140140
<div className="flex flex-col gap-2">
141141
<Label>Token</Label>
142142
<TokenSelector
143+
disableAddress
143144
chainId={selectedChain}
144145
client={THIRDWEB_CLIENT}
145146
enabled={true}
@@ -154,10 +155,10 @@ export function LeftSection(props: {
154155
)}
155156

156157
{/* Mode-specific form fields */}
157-
<div className="my-2">
158+
<div className="">
158159
{/* Buy Mode - Amount and Payment Methods */}
159160
{props.widget === "buy" && (
160-
<div className="space-y-4">
161+
<div className="space-y-6">
161162
<div className="flex flex-col gap-2">
162163
<Label htmlFor={buyTokenAmountId}>Amount</Label>
163164
<Input
@@ -177,6 +178,30 @@ export function LeftSection(props: {
177178
/>
178179
</div>
179180

181+
{props.widget === "buy" && (
182+
<div className="flex flex-col gap-2">
183+
<Label htmlFor="receiver-address">Receiver Address</Label>
184+
<p className="text-muted-foreground text-sm">
185+
Receive the tokens in a different wallet address
186+
</p>
187+
<Input
188+
className="bg-card"
189+
id="receiver-address"
190+
onChange={(e) => {
191+
setOptions((v) => ({
192+
...v,
193+
payOptions: {
194+
...v.payOptions,
195+
receiverAddress: e.target.value as Address,
196+
},
197+
}));
198+
}}
199+
placeholder="0x..."
200+
value={payOptions.receiverAddress || ""}
201+
/>
202+
</div>
203+
)}
204+
180205
{/* Payment Methods */}
181206
<div className="flex flex-col gap-3">
182207
<Label>Payment Methods</Label>
@@ -206,6 +231,7 @@ export function LeftSection(props: {
206231
/>
207232
<Label htmlFor={cryptoPaymentId}>Crypto</Label>
208233
</div>
234+
209235
<div className="flex items-center space-x-2">
210236
<Checkbox
211237
checked={payOptions.paymentMethods.includes("card")}
@@ -257,7 +283,6 @@ export function LeftSection(props: {
257283
value={payOptions.sellerAddress || ""}
258284
/>
259285
</div>
260-
261286
<div className="flex flex-col gap-2">
262287
<Label htmlFor={paymentAmountId}>Price</Label>
263288
<Input

apps/playground-web/src/app/payments/components/RightSection.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,12 @@ export function RightSection(props: {
6767
tokenAddress={props.options.payOptions.buyTokenAddress}
6868
currency={props.options.payOptions.currency}
6969
showThirdwebBranding={props.options.payOptions.showThirdwebBranding}
70+
receiverAddress={props.options.payOptions.receiverAddress}
71+
key={JSON.stringify({
72+
amount: props.options.payOptions.buyTokenAmount,
73+
chain: props.options.payOptions.buyTokenChain,
74+
tokenAddress: props.options.payOptions.buyTokenAddress,
75+
})}
7076
{...(props.options.payOptions.buttonLabel && {
7177
buttonLabel: props.options.payOptions.buttonLabel,
7278
})}

apps/playground-web/src/app/payments/components/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ export type BridgeComponentsPlaygroundOptions = {
4848

4949
// direct_payment mode options
5050
sellerAddress: Address;
51+
receiverAddress: Address | undefined;
5152

5253
// transaction mode options
5354
transactionData?: string; // Simplified for demo; could be more complex in real implementation

apps/playground-web/src/app/payments/fund-wallet/BuyPlayground.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ const defaultOptions: BridgeComponentsPlaygroundOptions = {
1717
paymentMethods: ["crypto", "card"],
1818
sellerAddress: "0x0000000000000000000000000000000000000000",
1919
title: "",
20+
receiverAddress: undefined,
2021
transactionData: "",
2122
currency: "USD",
2223
showThirdwebBranding: true,

apps/playground-web/src/app/payments/transactions/TransactionPlayground.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ const defaultOptions: BridgeComponentsPlaygroundOptions = {
2020
transactionData: "",
2121
currency: "USD",
2222
showThirdwebBranding: true,
23+
receiverAddress: undefined,
2324
},
2425
theme: {
2526
darkColorOverrides: {},

packages/thirdweb/src/react/web/ui/Bridge/BuyWidget.tsx

Lines changed: 36 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import { trackPayEvent } from "../../../../analytics/track/pay.js";
66
import type { TokenWithPrices } from "../../../../bridge/index.js";
77
import type { Chain } from "../../../../chains/types.js";
88
import type { ThirdwebClient } from "../../../../client/client.js";
9-
import { NATIVE_TOKEN_ADDRESS } from "../../../../constants/addresses.js";
109
import type { SupportedFiatCurrency } from "../../../../pay/convert/type.js";
1110
import type { PurchaseData } from "../../../../pay/types.js";
1211
import type { Address } from "../../../../utils/address.js";
@@ -25,22 +24,18 @@ import type {
2524
import type { CompletedStatusResult } from "../../../core/hooks/useStepExecutor.js";
2625
import type { SupportedTokens } from "../../../core/utils/defaultTokens.js";
2726
import { webWindowAdapter } from "../../adapters/WindowAdapter.js";
28-
import { useConnectLocale } from "../ConnectWallet/locale/getConnectLocale.js";
29-
import type { ConnectLocale } from "../ConnectWallet/locale/types.js";
27+
import connectLocaleEn from "../ConnectWallet/locale/en.js";
3028
import { EmbedContainer } from "../ConnectWallet/Modal/ConnectEmbed.js";
3129
import { DynamicHeight } from "../components/DynamicHeight.js";
32-
import { Spinner } from "../components/Spinner.js";
33-
import type { LocaleId } from "../types.js";
34-
import { useTokenQuery } from "./common/token-query.js";
3530
import { ErrorBanner } from "./ErrorBanner.js";
3631
import { FundWallet } from "./FundWallet.js";
3732
import { PaymentDetails } from "./payment-details/PaymentDetails.js";
3833
import { PaymentSelection } from "./payment-selection/PaymentSelection.js";
3934
import { SuccessScreen } from "./payment-success/SuccessScreen.js";
4035
import { QuoteLoader } from "./QuoteLoader.js";
4136
import { StepRunner } from "./StepRunner.js";
37+
import { useActiveWalletInfo } from "./swap-widget/hooks.js";
4238
import type { PaymentMethod, RequiredParams } from "./types.js";
43-
import { UnsupportedTokenScreen } from "./UnsupportedTokenScreen.js";
4439

4540
export type BuyOrOnrampPrepareResult = Extract<
4641
BridgePrepareResult,
@@ -68,14 +63,7 @@ export type BuyWidgetProps = {
6863
* ```
6964
*/
7065
client: ThirdwebClient;
71-
/**
72-
* By default - ConnectButton UI uses the `en-US` locale for english language users.
73-
*
74-
* You can customize the language used in the ConnectButton UI by setting the `locale` prop.
75-
*
76-
* Refer to the [`LocaleId`](https://portal.thirdweb.com/references/typescript/v5/LocaleId) type for supported locales.
77-
*/
78-
locale?: LocaleId;
66+
7967
/**
8068
* Set the theme for the `BuyWidget` component. By default it is set to `"dark"`
8169
*
@@ -130,7 +118,7 @@ export type BuyWidgetProps = {
130118
/**
131119
* The chain the accepted token is on.
132120
*/
133-
chain: Chain;
121+
chain?: Chain;
134122

135123
/**
136124
* Address of the token to buy. Leave undefined for the native token, or use 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE.
@@ -140,7 +128,7 @@ export type BuyWidgetProps = {
140128
/**
141129
* The amount to buy **(as a decimal string)**, e.g. "1.5" for 1.5 tokens.
142130
*/
143-
amount: string;
131+
amount?: string;
144132

145133
/**
146134
* The title to display in the widget. If `title` is explicity set to an empty string, the title will not be displayed.
@@ -213,6 +201,11 @@ export type BuyWidgetProps = {
213201
* The receiver address for the purchased funds.
214202
*/
215203
receiverAddress?: Address;
204+
205+
/**
206+
* Callback to be called when the user disconnects the active wallet.
207+
*/
208+
onDisconnect?: () => void;
216209
};
217210

218211
/**
@@ -326,31 +319,12 @@ export type BuyWidgetProps = {
326319
* @bridge
327320
*/
328321
export function BuyWidget(props: BuyWidgetProps) {
329-
return (
330-
<BridgeWidgetContainer
331-
theme={props.theme}
332-
className={props.className}
333-
style={props.style}
334-
>
335-
<BridgeWidgetContentWrapper {...props} />
336-
</BridgeWidgetContainer>
337-
);
338-
}
339-
340-
function BridgeWidgetContentWrapper(props: BuyWidgetProps) {
341-
const localQuery = useConnectLocale(props.locale || "en_US");
342-
const tokenQuery = useTokenQuery({
343-
tokenAddress: props.tokenAddress,
344-
chainId: props.chain.id,
345-
client: props.client,
346-
});
347-
348322
useQuery({
349323
queryFn: () => {
350324
trackPayEvent({
351325
client: props.client,
352326
event: "ub:ui:buy_widget:render",
353-
toChainId: props.chain.id,
327+
toChainId: props.chain?.id,
354328
toToken: props.tokenAddress,
355329
});
356330
return true;
@@ -372,33 +346,15 @@ function BridgeWidgetContentWrapper(props: BuyWidgetProps) {
372346
return props.connectOptions;
373347
}, [props.connectOptions, props.showThirdwebBranding]);
374348

375-
if (tokenQuery.isPending || !localQuery.data) {
376-
return (
377-
<div
378-
style={{
379-
alignItems: "center",
380-
display: "flex",
381-
justifyContent: "center",
382-
minHeight: "350px",
383-
}}
384-
>
385-
<Spinner color="secondaryText" size="xl" />
386-
</div>
387-
);
388-
} else if (tokenQuery.data?.type === "unsupported_token") {
389-
return (
390-
<UnsupportedTokenScreen
391-
chain={props.chain}
392-
client={props.client}
393-
tokenAddress={props.tokenAddress || NATIVE_TOKEN_ADDRESS}
394-
/>
395-
);
396-
} else if (tokenQuery.data?.type === "success") {
397-
return (
349+
return (
350+
<BridgeWidgetContainer
351+
theme={props.theme}
352+
className={props.className}
353+
style={props.style}
354+
>
398355
<BridgeWidgetContent
399356
{...props}
400-
connectLocale={localQuery.data}
401-
destinationToken={tokenQuery.data.token}
357+
theme={props.theme || "dark"}
402358
currency={props.currency || "USD"}
403359
paymentMethods={props.paymentMethods || ["crypto", "card"]}
404360
presetOptions={props.presetOptions || [5, 10, 20]}
@@ -409,23 +365,8 @@ function BridgeWidgetContentWrapper(props: BuyWidgetProps) {
409365
: props.showThirdwebBranding
410366
}
411367
/>
412-
);
413-
} else if (tokenQuery.error) {
414-
return (
415-
<ErrorBanner
416-
client={props.client}
417-
error={tokenQuery.error}
418-
onRetry={() => {
419-
tokenQuery.refetch();
420-
}}
421-
onCancel={() => {
422-
props.onCancel?.(undefined);
423-
}}
424-
/>
425-
);
426-
}
427-
428-
return null;
368+
</BridgeWidgetContainer>
369+
);
429370
}
430371

431372
type BuyWidgetScreen =
@@ -475,13 +416,15 @@ type BuyWidgetScreen =
475416
function BridgeWidgetContent(
476417
props: RequiredParams<
477418
BuyWidgetProps,
478-
"currency" | "presetOptions" | "showThirdwebBranding" | "paymentMethods"
479-
> & {
480-
connectLocale: ConnectLocale;
481-
destinationToken: TokenWithPrices;
482-
},
419+
| "currency"
420+
| "presetOptions"
421+
| "showThirdwebBranding"
422+
| "paymentMethods"
423+
| "theme"
424+
>,
483425
) {
484426
const [screen, setScreen] = useState<BuyWidgetScreen>({ id: "1:buy-ui" });
427+
const activeWalletInfo = useActiveWalletInfo();
485428

486429
const handleError = useCallback(
487430
(error: Error, quote: BridgePrepareResult | undefined) => {
@@ -511,9 +454,11 @@ function BridgeWidgetContent(
511454
[props.onCancel],
512455
);
513456

514-
if (screen.id === "1:buy-ui") {
457+
if (screen.id === "1:buy-ui" || !activeWalletInfo) {
515458
return (
516459
<FundWallet
460+
theme={props.theme}
461+
onDisconnect={props.onDisconnect}
517462
client={props.client}
518463
connectOptions={props.connectOptions}
519464
onContinue={(destinationAmount, destinationToken, receiverAddress) => {
@@ -534,8 +479,11 @@ function BridgeWidgetContent(
534479
}}
535480
buttonLabel={props.buttonLabel}
536481
currency={props.currency}
537-
initialAmount={props.amount}
538-
destinationToken={props.destinationToken}
482+
initialSelection={{
483+
tokenAddress: props.tokenAddress,
484+
chainId: props.chain?.id,
485+
amount: props.amount,
486+
}}
539487
/>
540488
);
541489
}
@@ -545,7 +493,7 @@ function BridgeWidgetContent(
545493
<PaymentSelection
546494
// from props
547495
client={props.client}
548-
connectLocale={props.connectLocale}
496+
connectLocale={connectLocaleEn}
549497
connectOptions={props.connectOptions}
550498
paymentMethods={props.paymentMethods}
551499
currency={props.currency}

0 commit comments

Comments
 (0)