Skip to content

Commit a20504e

Browse files
committed
Merge branch 'main' of https://github.com/nftlabs/widgets
2 parents 1c7abed + c51c7f4 commit a20504e

File tree

3 files changed

+358
-11
lines changed

3 files changed

+358
-11
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@
5151
"@emotion/styled": "^11",
5252
"@thirdweb-dev/react": "^3.8.0-nightly-f295ec6",
5353
"@thirdweb-dev/sdk": "^3.8.0-nightly-f295ec6",
54-
"@thirdweb-dev/storage": "^1.0.4",
54+
"@thirdweb-dev/storage": "^1.0.7-nightly-f295ec6",
5555
"color": "^4.2.3",
5656
"ethers": "^5.7.0",
5757
"flat": "^5.0.2",

src/embeds/direct-listing.tsx

Lines changed: 356 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,356 @@
1+
import {
2+
Button,
3+
Center,
4+
ChakraProvider,
5+
ColorMode,
6+
Flex,
7+
Heading,
8+
Icon,
9+
LightMode,
10+
NumberDecrementStepper,
11+
NumberIncrementStepper,
12+
NumberInput,
13+
NumberInputField,
14+
NumberInputStepper,
15+
Spinner,
16+
Stack,
17+
Text,
18+
Tooltip,
19+
useColorMode,
20+
useToast,
21+
} from "@chakra-ui/react";
22+
import { css, Global } from "@emotion/react";
23+
import {
24+
ThirdwebProvider,
25+
useAddress,
26+
useAuctionWinner,
27+
useBidBuffer,
28+
useContract,
29+
useDirectListing,
30+
useListing,
31+
useWinningBid,
32+
Web3Button,
33+
} from "@thirdweb-dev/react";
34+
import {
35+
AuctionListing,
36+
DirectListing,
37+
ListingType,
38+
Marketplace,
39+
MarketplaceV3,
40+
} from "@thirdweb-dev/sdk";
41+
import { DirectListingV3 } from "@thirdweb-dev/sdk/dist/declarations/src/evm/types/marketplacev3";
42+
import { ThirdwebStorage } from "@thirdweb-dev/storage";
43+
import { BigNumber, utils } from "ethers";
44+
import React, { useEffect, useMemo, useState } from "react";
45+
import { createRoot } from "react-dom/client";
46+
import { AiFillExclamationCircle } from "react-icons/ai";
47+
import { IoDiamondOutline } from "react-icons/io5";
48+
import { Body } from "src/shared/body";
49+
import { Header } from "src/shared/header";
50+
import { TokenMetadataPage } from "src/shared/token-metadata-page";
51+
import { Footer } from "../shared/footer";
52+
import { useGasless } from "../shared/hooks/useGasless";
53+
import chakraTheme from "../shared/theme";
54+
import { fontsizeCss } from "../shared/theme/typography";
55+
import { parseIpfsGateway } from "../utils/parseIpfsGateway";
56+
57+
interface MarketplaceEmbedProps {
58+
rpcUrl?: string;
59+
contractAddress: string;
60+
listingId: string;
61+
colorScheme: ColorMode;
62+
primaryColor: string;
63+
secondaryColor: string;
64+
}
65+
66+
interface BuyPageProps {
67+
contract?: MarketplaceV3;
68+
listing: DirectListingV3;
69+
isLoading?: boolean;
70+
primaryColor: string;
71+
secondaryColor: string;
72+
colorScheme: ColorMode;
73+
}
74+
75+
interface DirectListingProps extends BuyPageProps {
76+
listing: DirectListingV3;
77+
}
78+
79+
const DirectListingComponent: React.FC<DirectListingProps> = ({
80+
contract,
81+
listing,
82+
primaryColor,
83+
colorScheme,
84+
}) => {
85+
const address = useAddress();
86+
const [quantity, setQuantity] = useState(1);
87+
const [buySuccess, setBuySuccess] = useState(false);
88+
89+
const pricePerToken = listing.currencyValuePerToken.value;
90+
91+
const quantityLimit = useMemo(() => {
92+
return BigNumber.from(listing.quantity || 1);
93+
}, [listing.quantity]);
94+
95+
const formattedPrice = useMemo(() => {
96+
if (!listing.currencyValuePerToken || !quantity) {
97+
return undefined;
98+
}
99+
const formatted = BigNumber.from(
100+
listing.currencyValuePerToken.value,
101+
).mul(BigNumber.from(quantity));
102+
103+
return `${utils.formatUnits(
104+
formatted,
105+
listing.currencyValuePerToken.decimals,
106+
)} ${listing.currencyValuePerToken.symbol}`;
107+
}, [listing.currencyValuePerToken, quantity]);
108+
109+
const toast = useToast();
110+
const isSoldOut = BigNumber.from(listing.quantity).eq(0);
111+
112+
useEffect(() => {
113+
const t = setTimeout(() => setBuySuccess(false), 3000);
114+
return () => clearTimeout(t);
115+
}, [buySuccess]);
116+
117+
const canClaim = !isSoldOut && !!address;
118+
119+
const showQuantityInput =
120+
canClaim && quantityLimit.gt(1) && quantityLimit.lte(1000);
121+
122+
const colors = chakraTheme.colors;
123+
const accentColor = colors[primaryColor as keyof typeof colors][500];
124+
125+
return (
126+
<Stack spacing={4} align="center" w="100%">
127+
{!isSoldOut && (
128+
<Text>
129+
<strong>Available: </strong>
130+
{BigNumber.from(listing.quantity).toString()}
131+
</Text>
132+
)}
133+
<Flex
134+
w="100%"
135+
direction={{ base: "column", sm: "row" }}
136+
gap={2}
137+
justifyContent="center"
138+
alignItems="center"
139+
>
140+
{showQuantityInput && !isSoldOut && (
141+
<NumberInput
142+
inputMode="numeric"
143+
value={quantity}
144+
onChange={(stringValue, value) => {
145+
if (stringValue === "") {
146+
setQuantity(0);
147+
} else {
148+
setQuantity(value);
149+
}
150+
}}
151+
min={1}
152+
max={quantityLimit.toNumber()}
153+
maxW={{ base: "100%", sm: "100px" }}
154+
>
155+
<NumberInputField />
156+
<NumberInputStepper>
157+
<NumberIncrementStepper />
158+
<NumberDecrementStepper />
159+
</NumberInputStepper>
160+
</NumberInput>
161+
)}
162+
<LightMode>
163+
<Web3Button
164+
contractAddress={contract?.getAddress() || ""}
165+
accentColor={accentColor}
166+
colorMode={colorScheme}
167+
isDisabled={!canClaim}
168+
action={() => contract?.directListings.buyFromListing(listing.id, quantity)}
169+
onSuccess={() => {
170+
toast({
171+
title: "Success",
172+
description:
173+
"You have successfully purchased from this listing",
174+
status: "success",
175+
duration: 5000,
176+
isClosable: true,
177+
});
178+
}}
179+
onError={(err) => {
180+
console.error(err);
181+
toast({
182+
title: "Failed to purchase from listing",
183+
status: "error",
184+
duration: 9000,
185+
isClosable: true,
186+
});
187+
}}
188+
>
189+
{isSoldOut
190+
? "Sold Out"
191+
: canClaim
192+
? `Buy${showQuantityInput ? ` ${quantity}` : ""}${
193+
BigNumber.from(pricePerToken).eq(0)
194+
? " (Free)"
195+
: formattedPrice
196+
? ` (${formattedPrice})`
197+
: ""
198+
}`
199+
: "Purchase Unavailable"}
200+
</Web3Button>
201+
</LightMode>
202+
</Flex>
203+
</Stack>
204+
);
205+
};
206+
207+
const BuyPage: React.FC<BuyPageProps> = ({
208+
contract,
209+
listing,
210+
isLoading,
211+
primaryColor,
212+
secondaryColor,
213+
colorScheme,
214+
}) => {
215+
if (isLoading) {
216+
return (
217+
<Center w="100%" h="100%">
218+
<Stack direction="row" align="center">
219+
<Spinner />
220+
<Heading size="label.sm">Loading...</Heading>
221+
</Stack>
222+
</Center>
223+
);
224+
}
225+
226+
if (!listing) {
227+
return (
228+
<Center w="100%" h="100%">
229+
<Button colorScheme="primary" w="100%" isDisabled>
230+
This listing was either cancelled or does not exist.
231+
</Button>
232+
</Center>
233+
);
234+
}
235+
236+
return (
237+
<Center w="100%" h="100%">
238+
<Flex direction="column" align="center" gap={4} w="100%">
239+
<DirectListingComponent
240+
contract={contract}
241+
listing={listing}
242+
primaryColor={primaryColor}
243+
secondaryColor={secondaryColor}
244+
colorScheme={colorScheme}
245+
/>
246+
</Flex>
247+
</Center>
248+
);
249+
};
250+
const MarketplaceEmbed: React.FC<MarketplaceEmbedProps> = ({
251+
contractAddress,
252+
listingId,
253+
colorScheme,
254+
primaryColor,
255+
secondaryColor,
256+
}) => {
257+
const { setColorMode } = useColorMode();
258+
const marketplace = useContract(contractAddress, "marketplace-v3").contract;
259+
260+
const { data: listing, isLoading } = useDirectListing(marketplace, listingId);
261+
useEffect(() => {
262+
setColorMode(colorScheme);
263+
}, [colorScheme, setColorMode]);
264+
265+
return (
266+
<Flex
267+
position="fixed"
268+
top={0}
269+
left={0}
270+
bottom={0}
271+
right={0}
272+
flexDir="column"
273+
borderRadius="1rem"
274+
overflow="hidden"
275+
shadow="0px 1px 1px rgba(0,0,0,0.1)"
276+
border="1px solid"
277+
borderColor="borderColor"
278+
bgColor="backgroundHighlight"
279+
>
280+
<Header primaryColor={primaryColor} colorScheme={colorScheme} />
281+
<Body>
282+
<TokenMetadataPage metadata={listing?.asset} isLoading={isLoading}>
283+
<BuyPage
284+
contract={marketplace}
285+
listing={listing as DirectListingV3}
286+
isLoading={isLoading}
287+
primaryColor={primaryColor}
288+
secondaryColor={secondaryColor}
289+
colorScheme={colorScheme}
290+
/>
291+
</TokenMetadataPage>
292+
</Body>
293+
<Footer />
294+
</Flex>
295+
);
296+
};
297+
298+
const urlParams = new URL(window.location.toString()).searchParams;
299+
300+
const App: React.FC = () => {
301+
const chainId = Number(urlParams.get("chainId"));
302+
const contractAddress = urlParams.get("contract") || "";
303+
const rpcUrl = urlParams.get("rpcUrl") || "";
304+
const listingId = urlParams.get("listingId") || "";
305+
const relayerUrl = urlParams.get("relayUrl") || "";
306+
const biconomyApiKey = urlParams.get("biconomyApiKey") || "";
307+
const biconomyApiId = urlParams.get("biconomyApiId") || "";
308+
const colorScheme = urlParams.get("theme") === "dark" ? "dark" : "light";
309+
const primaryColor = urlParams.get("primaryColor") || "purple";
310+
const secondaryColor = urlParams.get("secondaryColor") || "orange";
311+
312+
const ipfsGateway = parseIpfsGateway(urlParams.get("ipfsGateway") || "");
313+
314+
const sdkOptions = useGasless(relayerUrl, biconomyApiKey, biconomyApiId);
315+
316+
return (
317+
<>
318+
<Global
319+
styles={css`
320+
:host,
321+
:root {
322+
${fontsizeCss};
323+
}
324+
`}
325+
/>
326+
<ChakraProvider theme={chakraTheme}>
327+
<ThirdwebProvider
328+
desiredChainId={chainId}
329+
sdkOptions={sdkOptions}
330+
storageInterface={
331+
ipfsGateway
332+
? new ThirdwebStorage({
333+
gatewayUrls: {
334+
"ipfs://": [ipfsGateway],
335+
},
336+
})
337+
: undefined
338+
}
339+
chainRpc={rpcUrl ? { [chainId]: rpcUrl } : undefined}
340+
>
341+
<MarketplaceEmbed
342+
contractAddress={contractAddress}
343+
listingId={listingId}
344+
colorScheme={colorScheme}
345+
primaryColor={primaryColor}
346+
secondaryColor={secondaryColor}
347+
/>
348+
</ThirdwebProvider>
349+
</ChakraProvider>
350+
</>
351+
);
352+
};
353+
354+
const container = document.getElementById("root") as Element;
355+
const root = createRoot(container);
356+
root.render(<App />);

yarn.lock

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2685,7 +2685,7 @@
26852685
yaml "^2.1.1"
26862686
zod "^3.11.6"
26872687

2688-
"@thirdweb-dev/[email protected]":
2688+
"@thirdweb-dev/[email protected]", "@thirdweb-dev/storage@^1.0.7-nightly-f295ec6":
26892689
version "1.0.7-nightly-f295ec6"
26902690
resolved "https://registry.yarnpkg.com/@thirdweb-dev/storage/-/storage-1.0.7-nightly-f295ec6.tgz#435e4941c5d41774cc222f9191cb56433f364bbe"
26912691
integrity sha512-iKKrqhRbEggbTPU2be3/N07TtsQOebb19jkDV/GYv+P1z26RlKEdvObl0YJ9rUurpXvnY4emxjZy3P6YFE2uYA==
@@ -2694,15 +2694,6 @@
26942694
form-data "^4.0.0"
26952695
uuid "^9.0.0"
26962696

2697-
"@thirdweb-dev/storage@^1.0.4":
2698-
version "1.0.4"
2699-
resolved "https://registry.yarnpkg.com/@thirdweb-dev/storage/-/storage-1.0.4.tgz#acb51ef998ef7bd9488f49b1cbbb94e328f0cc9f"
2700-
integrity sha512-gQohaPDWf5cv/3cfbZESg+c5++eE9dH2C+uwe6LVQukkTZoGxB3J33/qVtSLn7br85f3m1WC31FQ/SkiEht06w==
2701-
dependencies:
2702-
cross-fetch "^3.1.5"
2703-
form-data "^4.0.0"
2704-
uuid "^9.0.0"
2705-
27062697
"@trysound/[email protected]":
27072698
version "0.2.0"
27082699
resolved "https://registry.yarnpkg.com/@trysound/sax/-/sax-0.2.0.tgz#cccaab758af56761eb7bf37af6f03f326dd798ad"

0 commit comments

Comments
 (0)