Skip to content

Commit 484a3e1

Browse files
committed
feat: show quotes for onramp providers
1 parent 84a5e8c commit 484a3e1

File tree

5 files changed

+259
-75
lines changed

5 files changed

+259
-75
lines changed

packages/thirdweb/src/pay/buyWithFiat/getQuote.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -288,9 +288,9 @@ export async function getBuyWithFiatQuote(
288288
provider?: FiatProvider,
289289
): "stripe" | "coinbase" | "transak" => {
290290
switch (provider) {
291-
case "STRIPE":
291+
case "stripe":
292292
return "stripe";
293-
case "TRANSAK":
293+
case "transak":
294294
return "transak";
295295
default: // default to coinbase when undefined or any other value
296296
return "coinbase";

packages/thirdweb/src/pay/utils/commonTypes.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,4 +19,4 @@ export type PayOnChainTransactionDetails = {
1919

2020
export type FiatProvider = (typeof FiatProviders)[number];
2121

22-
export const FiatProviders = ["COINBASE", "STRIPE", "TRANSAK"] as const;
22+
export const FiatProviders = ["coinbase", "stripe", "transak"] as const;
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import { type UseQueryOptions, useQueries } from "@tanstack/react-query";
2+
import { prepare as prepareOnramp } from "../../../../bridge/Onramp.js";
3+
import type { ThirdwebClient } from "../../../../client/client.js";
4+
import { getToken } from "../../../../pay/convert/get-token.js";
5+
import type { Address } from "../../../../utils/address.js";
6+
import { toUnits } from "../../../../utils/units.js";
7+
8+
/**
9+
* @internal
10+
*/
11+
export type UseBuyWithFiatQuotesForProvidersParams = {
12+
/**
13+
* A client is the entry point to the thirdweb SDK.
14+
*/
15+
client: ThirdwebClient;
16+
/**
17+
* The destination chain ID.
18+
*/
19+
chainId: number;
20+
/**
21+
* The destination token address.
22+
*/
23+
tokenAddress: Address;
24+
/**
25+
* The address that will receive the tokens.
26+
*/
27+
receiver: Address;
28+
/**
29+
* The desired token amount in wei.
30+
*/
31+
amount: string;
32+
/**
33+
* The fiat currency (e.g., "USD"). Defaults to "USD".
34+
*/
35+
currency?: string;
36+
};
37+
38+
/**
39+
* @internal
40+
*/
41+
export type OnrampQuoteQueryOptions = Omit<
42+
UseQueryOptions<Awaited<ReturnType<typeof prepareOnramp>>>,
43+
"queryFn" | "queryKey" | "enabled"
44+
>;
45+
46+
/**
47+
* @internal
48+
*/
49+
export type UseBuyWithFiatQuotesForProvidersResult = {
50+
data: Awaited<ReturnType<typeof prepareOnramp>> | undefined;
51+
isLoading: boolean;
52+
error: Error | null;
53+
isError: boolean;
54+
isSuccess: boolean;
55+
}[];
56+
57+
/**
58+
* @internal
59+
* Hook to get prepared onramp quotes from Coinbase, Stripe, and Transak providers.
60+
*/
61+
export function useBuyWithFiatQuotesForProviders(
62+
params?: UseBuyWithFiatQuotesForProvidersParams,
63+
queryOptions?: OnrampQuoteQueryOptions,
64+
): UseBuyWithFiatQuotesForProvidersResult {
65+
const providers = ["coinbase", "stripe", "transak"] as const;
66+
67+
const queries = useQueries({
68+
queries: providers.map((provider) => ({
69+
...queryOptions,
70+
queryKey: ["onramp-prepare", provider, params],
71+
queryFn: async () => {
72+
if (!params) {
73+
throw new Error("No params provided");
74+
}
75+
76+
const token = await getToken(
77+
params.client,
78+
params.tokenAddress,
79+
params.chainId,
80+
);
81+
82+
const amountWei = toUnits(params.amount, token.decimals);
83+
84+
return prepareOnramp({
85+
client: params.client,
86+
onramp: provider,
87+
chainId: params.chainId,
88+
tokenAddress: params.tokenAddress,
89+
receiver: params.receiver,
90+
amount: amountWei,
91+
currency: params.currency || "USD",
92+
});
93+
},
94+
enabled: !!params,
95+
retry: false,
96+
})),
97+
});
98+
99+
return queries;
100+
}

packages/thirdweb/src/react/web/ui/Bridge/payment-selection/FiatProviderSelection.tsx

Lines changed: 152 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -1,105 +1,185 @@
11
"use client";
2+
import { useMemo } from "react";
23
import type { ThirdwebClient } from "../../../../../client/client.js";
4+
import { checksumAddress } from "../../../../../utils/address.js";
5+
import { toTokens } from "../../../../../utils/units.js";
36
import { useCustomTheme } from "../../../../core/design-system/CustomThemeProvider.js";
47
import {
58
iconSize,
69
radius,
710
spacing,
811
} from "../../../../core/design-system/index.js";
12+
import { useBuyWithFiatQuotesForProviders } from "../../../../core/hooks/pay/useBuyWithFiatQuotesForProviders.js";
913
import { Img } from "../../components/Img.js";
1014
import { Spacer } from "../../components/Spacer.js";
15+
import { Spinner } from "../../components/Spinner.js";
1116
import { Container } from "../../components/basic.js";
1217
import { Button } from "../../components/buttons.js";
1318
import { Text } from "../../components/text.js";
1419

1520
export interface FiatProviderSelectionProps {
1621
client: ThirdwebClient;
1722
onProviderSelected: (provider: "coinbase" | "stripe" | "transak") => void;
23+
toChainId: number;
24+
toTokenAddress: string;
25+
toAddress: string;
26+
toAmount?: string;
1827
}
1928

29+
const PROVIDERS = [
30+
{
31+
id: "coinbase" as const,
32+
name: "Coinbase",
33+
description: "Fast and secure payments",
34+
iconUri: "https://i.ibb.co/LDJ3Rk2t/Frame-5.png",
35+
},
36+
{
37+
id: "stripe" as const,
38+
name: "Stripe",
39+
description: "Trusted payment processing",
40+
iconUri: "https://i.ibb.co/CpgQC2Lf/images-3.png",
41+
},
42+
{
43+
id: "transak" as const,
44+
name: "Transak",
45+
description: "Global payment solution",
46+
iconUri: "https://i.ibb.co/Xx2r882p/Transak-official-symbol-1.png",
47+
},
48+
];
49+
2050
export function FiatProviderSelection({
21-
client,
2251
onProviderSelected,
52+
client,
53+
toChainId,
54+
toTokenAddress,
55+
toAddress,
56+
toAmount,
2357
}: FiatProviderSelectionProps) {
2458
const theme = useCustomTheme();
2559

26-
const providers = [
27-
{
28-
id: "coinbase" as const,
29-
name: "Coinbase",
30-
description: "Fast and secure payments",
31-
iconUri: "https://i.ibb.co/LDJ3Rk2t/Frame-5.png",
32-
},
33-
{
34-
id: "stripe" as const,
35-
name: "Stripe",
36-
description: "Trusted payment processing",
37-
iconUri: "https://i.ibb.co/CpgQC2Lf/images-3.png",
38-
},
39-
{
40-
id: "transak" as const,
41-
name: "Transak",
42-
description: "Global payment solution",
43-
iconUri: "https://i.ibb.co/Xx2r882p/Transak-official-symbol-1.png",
44-
},
45-
];
60+
// Fetch quotes for all providers
61+
const quoteQueries = useBuyWithFiatQuotesForProviders({
62+
client,
63+
chainId: toChainId,
64+
tokenAddress: checksumAddress(toTokenAddress),
65+
receiver: checksumAddress(toAddress),
66+
amount: toAmount || "0",
67+
currency: "USD",
68+
});
69+
70+
const quotes = useMemo(() => {
71+
return quoteQueries.map((q) => q.data).filter((q) => !!q);
72+
}, [quoteQueries]);
4673

4774
// TODO: add a "remember my choice" checkbox
4875

4976
return (
5077
<>
51-
<Text size="md" color="primaryText">
52-
Select Payment Provider
53-
</Text>
54-
<Spacer y="md" />
5578
<Container flex="column" gap="sm">
56-
{providers.map((provider) => (
57-
<Button
58-
key={provider.id}
59-
variant="secondary"
60-
fullWidth
61-
onClick={() => onProviderSelected(provider.id)}
62-
style={{
63-
border: `1px solid ${theme.colors.borderColor}`,
64-
borderRadius: radius.md,
65-
padding: `${spacing.sm} ${spacing.md}`,
66-
backgroundColor: theme.colors.tertiaryBg,
67-
textAlign: "left",
68-
}}
69-
>
70-
<Container
71-
flex="row"
72-
gap="md"
73-
style={{ width: "100%", alignItems: "center" }}
74-
>
75-
<Container
76-
style={{
77-
width: `${iconSize.md}px`,
78-
height: `${iconSize.md}px`,
79-
borderRadius: "50%",
80-
display: "flex",
81-
alignItems: "center",
82-
justifyContent: "center",
83-
padding: spacing.xs,
84-
overflow: "hidden",
85-
}}
86-
>
87-
<Img
88-
src={provider.iconUri}
89-
alt={provider.name}
90-
width={iconSize.md}
91-
height={iconSize.md}
92-
client={client}
93-
/>
94-
</Container>
95-
<Container flex="column" gap="3xs" style={{ flex: 1 }}>
96-
<Text size="sm" color="primaryText" style={{ fontWeight: 600 }}>
97-
{provider.name}
98-
</Text>
99-
</Container>
100-
</Container>
101-
</Button>
102-
))}
79+
{quotes.length > 0 ? (
80+
quotes
81+
.sort((a, b) => a.currencyAmount - b.currencyAmount)
82+
.map((quote, index) => {
83+
const provider = PROVIDERS.find(
84+
(p) => p.id === quote.intent.onramp,
85+
);
86+
if (!provider) {
87+
return null;
88+
}
89+
90+
return (
91+
<Container
92+
key={provider.id}
93+
animate="fadein"
94+
style={{
95+
animationDelay: `${index * 100}ms`,
96+
}}
97+
>
98+
<Button
99+
variant="secondary"
100+
fullWidth
101+
onClick={() => onProviderSelected(provider.id)}
102+
style={{
103+
border: `1px solid ${theme.colors.borderColor}`,
104+
borderRadius: radius.md,
105+
padding: `${spacing.sm} ${spacing.md}`,
106+
backgroundColor: theme.colors.tertiaryBg,
107+
textAlign: "left",
108+
}}
109+
>
110+
<Container
111+
flex="row"
112+
gap="sm"
113+
style={{ width: "100%", alignItems: "center" }}
114+
>
115+
<Container
116+
style={{
117+
width: `${iconSize.md}px`,
118+
height: `${iconSize.md}px`,
119+
borderRadius: "50%",
120+
display: "flex",
121+
alignItems: "center",
122+
justifyContent: "center",
123+
padding: spacing.xs,
124+
overflow: "hidden",
125+
}}
126+
>
127+
<Img
128+
src={provider.iconUri}
129+
alt={provider.name}
130+
width={iconSize.md}
131+
height={iconSize.md}
132+
client={client}
133+
/>
134+
</Container>
135+
<Container flex="column" gap="3xs" style={{ flex: 1 }}>
136+
<Text
137+
size="md"
138+
color="primaryText"
139+
style={{ fontWeight: 600 }}
140+
>
141+
{provider.name}
142+
</Text>
143+
</Container>
144+
<Container
145+
flex="column"
146+
gap="3xs"
147+
style={{ alignItems: "flex-end" }}
148+
>
149+
<Text
150+
size="sm"
151+
color="primaryText"
152+
style={{ fontWeight: 500 }}
153+
>
154+
$
155+
{quote.currencyAmount.toLocaleString(undefined, {
156+
minimumFractionDigits: 2,
157+
maximumFractionDigits: 2,
158+
})}{" "}
159+
{quote.currency}
160+
</Text>
161+
<Text size="xs" color="secondaryText">
162+
{toTokens(
163+
quote.destinationAmount,
164+
quote.destinationToken.decimals,
165+
)}{" "}
166+
{quote.destinationToken.symbol}
167+
</Text>
168+
</Container>
169+
</Container>
170+
</Button>
171+
</Container>
172+
);
173+
})
174+
) : (
175+
<Container flex="column" center="both" style={{ minHeight: "120px" }}>
176+
<Spinner size="lg" color="secondaryText" />
177+
<Spacer y="sm" />
178+
<Text size="sm" color="secondaryText" center>
179+
Generating quotes...
180+
</Text>
181+
</Container>
182+
)}
103183
</Container>
104184
</>
105185
);

packages/thirdweb/src/react/web/ui/Bridge/payment-selection/PaymentSelection.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,10 @@ export function PaymentSelection({
252252
<FiatProviderSelection
253253
client={client}
254254
onProviderSelected={handleOnrampProviderSelected}
255+
toChainId={destinationToken.chainId}
256+
toTokenAddress={destinationToken.address}
257+
toAddress={receiverAddress || ""}
258+
toAmount={destinationAmount}
255259
/>
256260
)}
257261
</Container>

0 commit comments

Comments
 (0)