Compare commits
28
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c0b87503a4 | ||
|
|
58fb28de29 | ||
|
|
322e4496cf | ||
|
|
06604162a1 | ||
|
|
7d36418e0f | ||
|
|
412fb5da08 | ||
|
|
c3f240489c | ||
|
|
54840759b8 | ||
|
|
9d1f75d63a | ||
|
|
21d794c7b8 | ||
|
|
e3c0af28f7 | ||
|
|
66798886cf | ||
|
|
842ea50cbe | ||
|
|
8bd2a99a22 | ||
|
|
7a9713e877 | ||
|
|
1c6c1bf890 | ||
|
|
f81ffa9fa6 | ||
|
|
88e96dee44 | ||
|
|
ccbf2d2c22 | ||
|
|
baf21980be | ||
|
|
7a86344941 | ||
|
|
c621c13f11 | ||
|
|
004cac04aa | ||
|
|
57553b784e | ||
|
|
13fc6f5854 | ||
|
|
b03dc57072 | ||
|
|
f40d2474e6 | ||
|
|
ae217a3a60 |
@@ -0,0 +1,26 @@
|
||||
name: Check for linked issue in pull request body
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types:
|
||||
- opened
|
||||
- edited
|
||||
|
||||
jobs:
|
||||
check_linear_in_pr_body:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check for linked issue in pull request body
|
||||
run: |
|
||||
pr_body=$(curl -s \
|
||||
-H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
|
||||
https://api.github.com/repos/${{ github.repository }}/pulls/${{ github.event.number }} | jq -r .body)
|
||||
|
||||
echo "Pull Request Body: $pr_body"
|
||||
|
||||
if echo "$pr_body" | grep -iE "CNCT|DASH"; then
|
||||
echo "Linked issue found in the pull request body."
|
||||
else
|
||||
echo "No linked issue found in the pull request body."
|
||||
exit 1
|
||||
fi
|
||||
@@ -0,0 +1,15 @@
|
||||
name: 'Close stale issues and PRs'
|
||||
on:
|
||||
schedule:
|
||||
- cron: '30 1 * * *'
|
||||
|
||||
jobs:
|
||||
stale:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/stale@v9
|
||||
with:
|
||||
stale-issue-message: 'This issue has been inactive for 30 days. It is now marked as stale and will be closed in 5 days if no further activity occurs.'
|
||||
stale-pr-message: 'This PR has been inactive for 30 days. It is now marked as stale and will be closed in 5 days if no further activity occurs.'
|
||||
days-before-stale: 30
|
||||
days-before-close: 5
|
||||
@@ -23,4 +23,17 @@ Some env vars can be overridden that are required for some external services to
|
||||
|
||||
To define env vars please create a `.env` file based on the `.env.example` template at the root level of the project. This file is ignored by git so you can safely add it to your local copy of the project.
|
||||
|
||||
**Add your thirdweb clientID and secret key to build a basic functioning version of the site.**
|
||||
**Add your thirdweb clientID and secret key to build a basic functioning version of the site.**
|
||||
|
||||
## Code Structure and Style Guide
|
||||
|
||||
### Components
|
||||
- Components should go in a `components` folder next to the app page they are used in.
|
||||
- If a component is reused in multiple pages, it should go in the lowest level route folder it is used in. For example, if a chart component is used in all analytics subroutes `/analytics/...`, it should go in the `/analytics/components` folder.
|
||||
- All low-level components such as buttons, inputs, etc. should be shadcn components found in `@/ui`.
|
||||
- All composed components reused across all page routes are `blocks`, and should also go in the `@/ui` folder.
|
||||
|
||||
### Data fetching
|
||||
- Use RSC wherever possible.
|
||||
- Write data fetching code in its own function in the same file as the component it is used in, not exported.
|
||||
- If the same data fetching function is used in multiple components, place it in a file in an `api` folder at the lowest level possible (just like components). If you need to do this you probable aren't organizing your components properly.
|
||||
|
||||
@@ -4,4 +4,7 @@ module.exports = [
|
||||
"/connect/sign-in",
|
||||
"/contracts/modular-contracts",
|
||||
"/unlimited-wallets",
|
||||
"/engine",
|
||||
"/solutions/gaming",
|
||||
"/solutions/consumer-apps",
|
||||
];
|
||||
|
||||
@@ -244,6 +244,8 @@ export interface InAppWalletStats {
|
||||
uniqueWalletsConnected: number;
|
||||
}
|
||||
|
||||
export interface EcosystemWalletStats extends InAppWalletStats {}
|
||||
|
||||
export interface UserOpStats {
|
||||
date: string;
|
||||
successful: number;
|
||||
|
||||
+27
-55
@@ -1,17 +1,20 @@
|
||||
"use client";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Sheet,
|
||||
SheetContent,
|
||||
SheetHeader,
|
||||
SheetTitle,
|
||||
SheetTrigger,
|
||||
} from "@/components/ui/sheet";
|
||||
import { ListerOnly } from "@3rdweb-sdk/react/components/roles/lister-only";
|
||||
import { useDisclosure } from "@chakra-ui/react";
|
||||
import { TransactionButton } from "components/buttons/TransactionButton";
|
||||
import { PlusIcon } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import type { ThirdwebContract } from "thirdweb";
|
||||
import { useActiveAccount } from "thirdweb/react";
|
||||
import { Button, Drawer } from "tw-components";
|
||||
import { CreateListingsForm } from "./list-form";
|
||||
|
||||
const LIST_FORM_ID = "marketplace-list-form";
|
||||
|
||||
interface CreateListingButtonProps {
|
||||
contract: ThirdwebContract;
|
||||
createText?: string;
|
||||
@@ -25,59 +28,28 @@ export const CreateListingButton: React.FC<CreateListingButtonProps> = ({
|
||||
...restButtonProps
|
||||
}) => {
|
||||
const address = useActiveAccount()?.address;
|
||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||
const [isFormLoading, setIsFormLoading] = useState(false);
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<ListerOnly contract={contract}>
|
||||
<Drawer
|
||||
allowPinchZoom
|
||||
preserveScrollBarGap
|
||||
size="lg"
|
||||
onClose={onClose}
|
||||
isOpen={isOpen}
|
||||
header={{ children: createText }}
|
||||
footer={{
|
||||
children: (
|
||||
<>
|
||||
<Button
|
||||
isDisabled={isFormLoading}
|
||||
variant="outline"
|
||||
mr={3}
|
||||
onClick={onClose}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<TransactionButton
|
||||
txChainID={contract.chain.id}
|
||||
isLoading={isFormLoading}
|
||||
transactionCount={2}
|
||||
form={LIST_FORM_ID}
|
||||
type="submit"
|
||||
colorScheme="primary"
|
||||
>
|
||||
{createText}
|
||||
</TransactionButton>
|
||||
</>
|
||||
),
|
||||
}}
|
||||
>
|
||||
<CreateListingsForm
|
||||
contract={contract}
|
||||
formId={LIST_FORM_ID}
|
||||
type={type}
|
||||
setIsFormLoading={setIsFormLoading}
|
||||
/>
|
||||
</Drawer>
|
||||
<Button
|
||||
colorScheme="primary"
|
||||
leftIcon={<PlusIcon className="size-5" />}
|
||||
{...restButtonProps}
|
||||
onClick={onOpen}
|
||||
isDisabled={!address}
|
||||
>
|
||||
{createText}
|
||||
</Button>
|
||||
<Sheet open={open} onOpenChange={setOpen}>
|
||||
<SheetTrigger asChild>
|
||||
<Button variant="primary" {...restButtonProps} disabled={!address}>
|
||||
{createText} <PlusIcon className="ml-2 size-5" />
|
||||
</Button>
|
||||
</SheetTrigger>
|
||||
<SheetContent className="w-full overflow-y-auto sm:min-w-[540px] lg:min-w-[700px]">
|
||||
<SheetHeader>
|
||||
<SheetTitle className="mb-5 text-left">{createText}</SheetTitle>
|
||||
</SheetHeader>
|
||||
<CreateListingsForm
|
||||
contract={contract}
|
||||
type={type}
|
||||
actionText={createText}
|
||||
setOpen={setOpen}
|
||||
/>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
</ListerOnly>
|
||||
);
|
||||
};
|
||||
|
||||
+57
-33
@@ -1,4 +1,5 @@
|
||||
import { Alert, AlertTitle } from "@/components/ui/alert";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card } from "@/components/ui/card";
|
||||
import { useDashboardOwnedNFTs } from "@3rdweb-sdk/react/hooks/useDashboardOwnedNFTs";
|
||||
import { useWalletNFTs } from "@3rdweb-sdk/react/hooks/useWalletNFTs";
|
||||
@@ -10,19 +11,18 @@ import {
|
||||
Select,
|
||||
Spinner,
|
||||
Tooltip,
|
||||
useModalContext,
|
||||
} from "@chakra-ui/react";
|
||||
import { TransactionButton } from "components/buttons/TransactionButton";
|
||||
import { CurrencySelector } from "components/shared/CurrencySelector";
|
||||
import { SolidityInput } from "contract-ui/components/solidity-inputs";
|
||||
import { useTrack } from "hooks/analytics/useTrack";
|
||||
import { useAllChainsData } from "hooks/chains/allChains";
|
||||
import { useTxNotifications } from "hooks/useTxNotifications";
|
||||
import { isAlchemySupported } from "lib/wallet/nfts/alchemy";
|
||||
import { isMoralisSupported } from "lib/wallet/nfts/moralis";
|
||||
import { isSimpleHashSupported } from "lib/wallet/nfts/simpleHash";
|
||||
import type { WalletNFT } from "lib/wallet/nfts/types";
|
||||
import { CircleAlertIcon, InfoIcon } from "lucide-react";
|
||||
import { useMemo } from "react";
|
||||
import { type Dispatch, type SetStateAction, useMemo, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import {
|
||||
@@ -58,6 +58,8 @@ import {
|
||||
import { NFTMediaWithEmptyState } from "tw-components/nft-media";
|
||||
import { shortenIfAddress } from "utils/usedapp-external";
|
||||
|
||||
const LIST_FORM_ID = "marketplace-list-form";
|
||||
|
||||
type ListForm =
|
||||
| (Omit<CreateListingParams, "quantity" | "currencyContractAddress"> & {
|
||||
currencyContractAddress: string;
|
||||
@@ -79,9 +81,9 @@ type ListForm =
|
||||
|
||||
type CreateListingsFormProps = {
|
||||
contract: ThirdwebContract;
|
||||
formId: string;
|
||||
actionText: string;
|
||||
setOpen: Dispatch<SetStateAction<boolean>>;
|
||||
type?: "direct-listings" | "english-auctions";
|
||||
setIsFormLoading: (loading: boolean) => void;
|
||||
};
|
||||
|
||||
const auctionTimes = [
|
||||
@@ -96,14 +98,15 @@ const auctionTimes = [
|
||||
|
||||
export const CreateListingsForm: React.FC<CreateListingsFormProps> = ({
|
||||
contract,
|
||||
formId,
|
||||
type,
|
||||
setIsFormLoading,
|
||||
actionText,
|
||||
setOpen,
|
||||
}) => {
|
||||
const trackEvent = useTrack();
|
||||
const chainId = contract.chain.id;
|
||||
const { idToChain } = useAllChainsData();
|
||||
const network = idToChain.get(chainId);
|
||||
const [isFormLoading, setIsFormLoading] = useState(false);
|
||||
|
||||
const isSupportedChain =
|
||||
chainId &&
|
||||
@@ -203,17 +206,10 @@ export const CreateListingsForm: React.FC<CreateListingsFormProps> = ({
|
||||
|
||||
const noNfts = !nfts?.length;
|
||||
|
||||
const modalContext = useModalContext();
|
||||
|
||||
const { onSuccess, onError } = useTxNotifications(
|
||||
"NFT listed successfully",
|
||||
"Failed to list NFT",
|
||||
);
|
||||
|
||||
return (
|
||||
<form
|
||||
className="flex flex-col gap-6"
|
||||
id={formId}
|
||||
className="flex flex-col gap-6 pb-16"
|
||||
id={LIST_FORM_ID}
|
||||
onSubmit={form.handleSubmit(async (formData) => {
|
||||
if (!formData.selected || !selectedContract) {
|
||||
return;
|
||||
@@ -247,12 +243,13 @@ export const CreateListingsForm: React.FC<CreateListingsFormProps> = ({
|
||||
approved: true,
|
||||
});
|
||||
|
||||
try {
|
||||
await sendAndConfirmTx.mutateAsync(approveTx);
|
||||
} catch {
|
||||
setIsFormLoading(false);
|
||||
return toast.error("Failed to approve NFT for marketplace");
|
||||
}
|
||||
const promise = sendAndConfirmTx.mutateAsync(approveTx);
|
||||
toast.promise(promise, {
|
||||
loading: "Approving NFT for listing",
|
||||
success: "NFT approved succesfully",
|
||||
error: "Failed to approve NFT",
|
||||
});
|
||||
await promise;
|
||||
}
|
||||
|
||||
if (formData.listingType === "direct") {
|
||||
@@ -270,12 +267,14 @@ export const CreateListingsForm: React.FC<CreateListingsFormProps> = ({
|
||||
pricePerToken: String(formData.pricePerToken),
|
||||
endTimestamp,
|
||||
});
|
||||
await sendAndConfirmTx.mutateAsync(transaction, {
|
||||
onSuccess: () => {
|
||||
onSuccess();
|
||||
modalContext.onClose();
|
||||
},
|
||||
onError,
|
||||
|
||||
const promise = sendAndConfirmTx.mutateAsync(transaction, {
|
||||
onSuccess: () => setOpen(false),
|
||||
});
|
||||
toast.promise(promise, {
|
||||
loading: "Listing NFT",
|
||||
success: "NFT listed successfully",
|
||||
error: "Failed to list NFT",
|
||||
});
|
||||
} else if (formData.listingType === "auction") {
|
||||
let minimumBidAmountWei: bigint;
|
||||
@@ -323,16 +322,15 @@ export const CreateListingsForm: React.FC<CreateListingsFormProps> = ({
|
||||
buyoutBidAmountWei * BigInt(formData.quantity),
|
||||
});
|
||||
|
||||
await sendAndConfirmTx.mutateAsync(transaction, {
|
||||
const promise = sendAndConfirmTx.mutateAsync(transaction, {
|
||||
onSuccess: () => {
|
||||
onSuccess();
|
||||
trackEvent({
|
||||
category: "marketplace",
|
||||
action: "add-listing",
|
||||
label: "success",
|
||||
network,
|
||||
});
|
||||
modalContext.onClose();
|
||||
setOpen(false);
|
||||
},
|
||||
onError: (error) => {
|
||||
trackEvent({
|
||||
@@ -342,11 +340,16 @@ export const CreateListingsForm: React.FC<CreateListingsFormProps> = ({
|
||||
network,
|
||||
error,
|
||||
});
|
||||
onError(error);
|
||||
},
|
||||
});
|
||||
toast.promise(promise, {
|
||||
loading: "Creating auction",
|
||||
success: "Auction created successfully",
|
||||
error: "Failed to create auction",
|
||||
});
|
||||
}
|
||||
} catch {
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
toast.error("Failed to list NFT");
|
||||
}
|
||||
|
||||
@@ -540,6 +543,27 @@ export const CreateListingsForm: React.FC<CreateListingsFormProps> = ({
|
||||
<AlertTitle>No NFT selected</AlertTitle>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{/* Need to pin these at the bottom because this is a very long form */}
|
||||
<div className="fixed right-6 bottom-4 flex flex-row items-center justify-end gap-3">
|
||||
<Button
|
||||
disabled={isFormLoading}
|
||||
variant="default"
|
||||
onClick={() => setOpen(false)}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<TransactionButton
|
||||
txChainID={contract.chain.id}
|
||||
isLoading={isFormLoading}
|
||||
transactionCount={2}
|
||||
form={LIST_FORM_ID}
|
||||
type="submit"
|
||||
colorScheme="primary"
|
||||
>
|
||||
{actionText}
|
||||
</TransactionButton>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import { Input } from "@chakra-ui/react";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { useActiveAccount } from "thirdweb/react";
|
||||
import { CustomFormControl } from "../common";
|
||||
import { useClaimConditionsFormContext } from "../index";
|
||||
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
import { Input } from "@chakra-ui/react";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { CustomFormControl } from "../common";
|
||||
import { useClaimConditionsFormContext } from "../index";
|
||||
|
||||
@@ -18,7 +18,7 @@ export const PhaseNameInput: React.FC = () => {
|
||||
helperText="This does not affect how your claim phase functions and is for organizational purposes only."
|
||||
>
|
||||
<Input
|
||||
isDisabled={formDisabled}
|
||||
disabled={formDisabled}
|
||||
type="text"
|
||||
value={inputValue}
|
||||
placeholder={inputPlaceholder}
|
||||
|
||||
+19
-25
@@ -1,6 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import { Flex } from "@chakra-ui/react";
|
||||
import type { ThirdwebContract } from "thirdweb";
|
||||
import { ClaimConditionsForm } from "./claim-conditions-form/index";
|
||||
|
||||
@@ -17,31 +16,26 @@ export const ClaimConditions: React.FC<ClaimConditionsProps> = ({
|
||||
isERC20,
|
||||
}) => {
|
||||
return (
|
||||
<div className="flex w-full flex-col gap-8">
|
||||
<Flex p={0} position="relative">
|
||||
<Flex direction="column" gap={6} w="full">
|
||||
{/* Info */}
|
||||
<section>
|
||||
<h2 className="mb-1 font-semibold text-xl tracking-tight">
|
||||
Set Claim Conditions
|
||||
</h2>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
Control when the {isERC20 ? "tokens" : "NFTs"} get dropped, how
|
||||
much they cost, and more.
|
||||
</p>
|
||||
</section>
|
||||
<div className="flex w-full flex-col gap-6">
|
||||
<section>
|
||||
<h2 className="mb-1 font-semibold text-xl tracking-tight">
|
||||
Set Claim Conditions
|
||||
</h2>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
Control when the {isERC20 ? "tokens" : "NFTs"} get dropped, how much
|
||||
they cost, and more.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
{/* Set Claim Conditions */}
|
||||
<ClaimConditionsForm
|
||||
isErc20={isERC20}
|
||||
contract={contract}
|
||||
tokenId={tokenId}
|
||||
isColumn={isColumn}
|
||||
// always multi phase!
|
||||
isMultiPhase={true}
|
||||
/>
|
||||
</Flex>
|
||||
</Flex>
|
||||
{/* Set Claim Conditions */}
|
||||
<ClaimConditionsForm
|
||||
isErc20={isERC20}
|
||||
contract={contract}
|
||||
tokenId={tokenId}
|
||||
isColumn={isColumn}
|
||||
// always multi phase!
|
||||
isMultiPhase={true}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
+1
-1
@@ -129,7 +129,7 @@ const AirdropTab: React.FC<AirdropTabProps> = ({ contract, tokenId }) => {
|
||||
<SheetContent className="w-full overflow-y-auto sm:min-w-[540px] lg:min-w-[700px]">
|
||||
<SheetHeader>
|
||||
<SheetTitle className="mb-5 text-left">
|
||||
Aidrop NFTs
|
||||
Airdrop NFTs
|
||||
</SheetTitle>
|
||||
</SheetHeader>
|
||||
<AirdropUpload
|
||||
|
||||
+216
-230
@@ -7,13 +7,9 @@ import {
|
||||
AccordionItem,
|
||||
AccordionPanel,
|
||||
Divider,
|
||||
DrawerBody,
|
||||
DrawerFooter,
|
||||
DrawerHeader,
|
||||
FormControl,
|
||||
Input,
|
||||
Textarea,
|
||||
useModalContext,
|
||||
} from "@chakra-ui/react";
|
||||
import { OpenSeaPropertyBadge } from "components/badges/opensea";
|
||||
import { TransactionButton } from "components/buttons/TransactionButton";
|
||||
@@ -21,9 +17,9 @@ import { PropertiesFormControl } from "components/contract-pages/forms/propertie
|
||||
import { FileInput } from "components/shared/FileInput";
|
||||
import { useTrack } from "hooks/analytics/useTrack";
|
||||
import { useImageFileOrUrl } from "hooks/useImageFileOrUrl";
|
||||
import { useTxNotifications } from "hooks/useTxNotifications";
|
||||
import { useMemo } from "react";
|
||||
import { type Dispatch, type SetStateAction, useMemo } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import type { NFT, ThirdwebContract } from "thirdweb";
|
||||
import {
|
||||
updateMetadata as updateMetadata721,
|
||||
@@ -50,7 +46,7 @@ const UPDATE_METADATA_FORM_ID = "nft-update-metadata-form";
|
||||
type UpdateNftMetadataForm = {
|
||||
contract: ThirdwebContract;
|
||||
nft: NFT;
|
||||
|
||||
setOpen: Dispatch<SetStateAction<boolean>>;
|
||||
/**
|
||||
* If useUpdateMetadata (NFT Drop, Edition Drop) -> use `updateMetadata`
|
||||
* else (NFT Collection, Edition) -> use `setTokenURI`
|
||||
@@ -62,6 +58,7 @@ export const UpdateNftMetadata: React.FC<UpdateNftMetadataForm> = ({
|
||||
contract,
|
||||
nft,
|
||||
useUpdateMetadata,
|
||||
setOpen,
|
||||
}) => {
|
||||
const trackEvent = useTrack();
|
||||
const address = useActiveAccount()?.address;
|
||||
@@ -101,14 +98,6 @@ export const UpdateNftMetadata: React.FC<UpdateNftMetadataForm> = ({
|
||||
values: transformedQueryData,
|
||||
});
|
||||
|
||||
const modalContext = useModalContext();
|
||||
|
||||
const { onSuccess, onError } = useTxNotifications(
|
||||
"NFT Metadata updated successfully",
|
||||
"Metadata update failed",
|
||||
contract,
|
||||
);
|
||||
|
||||
const setFile = (file: File) => {
|
||||
if (file.type.includes("image")) {
|
||||
// image files
|
||||
@@ -173,237 +162,234 @@ export const UpdateNftMetadata: React.FC<UpdateNftMetadataForm> = ({
|
||||
const showCoverImageUpload =
|
||||
watch("animation_url") instanceof File ||
|
||||
watch("external_url") instanceof File;
|
||||
const { mutate, isPending } = useSendAndConfirmTransaction();
|
||||
const sendAndConfirmTx = useSendAndConfirmTransaction();
|
||||
|
||||
return (
|
||||
<>
|
||||
<DrawerHeader>
|
||||
<Heading>Update NFT Metadata</Heading>
|
||||
</DrawerHeader>
|
||||
<DrawerBody>
|
||||
<form
|
||||
className="flex flex-col gap-6"
|
||||
id={UPDATE_METADATA_FORM_ID}
|
||||
onSubmit={handleSubmit((data) => {
|
||||
if (!address) {
|
||||
onError("Please connect your wallet to update metadata.");
|
||||
return;
|
||||
}
|
||||
trackEvent({
|
||||
category: "nft",
|
||||
action: "update-metadata",
|
||||
label: "attempt",
|
||||
});
|
||||
<form
|
||||
className="flex flex-col gap-6"
|
||||
id={UPDATE_METADATA_FORM_ID}
|
||||
onSubmit={handleSubmit((data) => {
|
||||
if (!address) {
|
||||
toast.error("Please connect your wallet to update metadata.");
|
||||
return;
|
||||
}
|
||||
trackEvent({
|
||||
category: "nft",
|
||||
action: "update-metadata",
|
||||
label: "attempt",
|
||||
});
|
||||
|
||||
const newMetadata = parseAttributes({
|
||||
...data,
|
||||
image: data.image || data.customImage || nft.metadata.image,
|
||||
animation_url:
|
||||
data.animation_url ||
|
||||
data.customAnimationUrl ||
|
||||
nft.metadata.animation_url,
|
||||
});
|
||||
try {
|
||||
const newMetadata = parseAttributes({
|
||||
...data,
|
||||
image: data.image || data.customImage || nft.metadata.image,
|
||||
animation_url:
|
||||
data.animation_url ||
|
||||
data.customAnimationUrl ||
|
||||
nft.metadata.animation_url,
|
||||
});
|
||||
|
||||
const transaction = useUpdateMetadata
|
||||
? // For Drop contracts, we need to call the `updateBatchBaseURI` method
|
||||
nft.type === "ERC721"
|
||||
? updateMetadata721({
|
||||
contract,
|
||||
targetTokenId: BigInt(nft.id),
|
||||
newMetadata,
|
||||
})
|
||||
: updateMetadata1155({
|
||||
contract,
|
||||
targetTokenId: BigInt(nft.id),
|
||||
newMetadata,
|
||||
})
|
||||
: // For Collection contracts, we need to call the `setTokenURI` method
|
||||
nft.type === "ERC721"
|
||||
? updateTokenURI721({
|
||||
contract,
|
||||
tokenId: BigInt(nft.id),
|
||||
newMetadata,
|
||||
})
|
||||
: updateTokenURI1155({
|
||||
contract,
|
||||
tokenId: BigInt(nft.id),
|
||||
newMetadata,
|
||||
});
|
||||
mutate(transaction, {
|
||||
onSuccess: () => {
|
||||
trackEvent({
|
||||
category: "nft",
|
||||
action: "update-metadata",
|
||||
label: "success",
|
||||
const transaction = useUpdateMetadata
|
||||
? // For Drop contracts, we need to call the `updateBatchBaseURI` method
|
||||
nft.type === "ERC721"
|
||||
? updateMetadata721({
|
||||
contract,
|
||||
targetTokenId: BigInt(nft.id),
|
||||
newMetadata,
|
||||
})
|
||||
: updateMetadata1155({
|
||||
contract,
|
||||
targetTokenId: BigInt(nft.id),
|
||||
newMetadata,
|
||||
})
|
||||
: // For Collection contracts, we need to call the `setTokenURI` method
|
||||
nft.type === "ERC721"
|
||||
? updateTokenURI721({
|
||||
contract,
|
||||
tokenId: BigInt(nft.id),
|
||||
newMetadata,
|
||||
})
|
||||
: updateTokenURI1155({
|
||||
contract,
|
||||
tokenId: BigInt(nft.id),
|
||||
newMetadata,
|
||||
});
|
||||
onSuccess();
|
||||
modalContext.onClose();
|
||||
},
|
||||
// biome-ignore lint/suspicious/noExplicitAny: FIXME
|
||||
onError: (error: any) => {
|
||||
trackEvent({
|
||||
category: "nft",
|
||||
action: "update-metadata",
|
||||
label: "error",
|
||||
error,
|
||||
});
|
||||
onError(error);
|
||||
},
|
||||
});
|
||||
})}
|
||||
>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Heading size="subtitle.md">Metadata</Heading>
|
||||
<Divider />
|
||||
</div>
|
||||
<FormControl isRequired isInvalid={!!errors.name}>
|
||||
<FormLabel>Name</FormLabel>
|
||||
<Input autoFocus {...register("name")} />
|
||||
<FormErrorMessage>{errors?.name?.message}</FormErrorMessage>
|
||||
</FormControl>
|
||||
<FormControl isInvalid={!!mediaFileError}>
|
||||
<FormLabel>Media</FormLabel>
|
||||
{nft?.metadata && !mediaFileUrl && (
|
||||
<div className="flex flex-row">
|
||||
<NFTMediaWithEmptyState
|
||||
metadata={nft.metadata}
|
||||
width="200px"
|
||||
height="200px"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<FileInput
|
||||
previewMaxWidth="200px"
|
||||
value={mediaFileUrl as File | string}
|
||||
showUploadButton
|
||||
showPreview={nft?.metadata ? !!mediaFileUrl : true}
|
||||
setValue={setFile}
|
||||
className="rounded border border-border transition-all duration-200"
|
||||
selectOrUpload="Upload"
|
||||
helperText={nft?.metadata ? "New Media" : "Media"}
|
||||
/>
|
||||
</div>
|
||||
<FormHelperText>
|
||||
You can upload image, audio, video, html, text, pdf, and 3d model
|
||||
files here.
|
||||
</FormHelperText>
|
||||
<FormErrorMessage>
|
||||
{mediaFileError?.message as unknown as string}
|
||||
</FormErrorMessage>
|
||||
</FormControl>
|
||||
{showCoverImageUpload && (
|
||||
<FormControl isInvalid={!!errors.image}>
|
||||
<FormLabel>Cover Image</FormLabel>
|
||||
<FileInput
|
||||
previewMaxWidth="200px"
|
||||
accept={{ "image/*": [] }}
|
||||
value={imageUrl}
|
||||
showUploadButton
|
||||
setValue={(file) => setValue("image", file)}
|
||||
className="rounded border border-border transition-all"
|
||||
/>
|
||||
const promise = sendAndConfirmTx.mutateAsync(transaction, {
|
||||
onSuccess: () => {
|
||||
trackEvent({
|
||||
category: "nft",
|
||||
action: "update-metadata",
|
||||
label: "success",
|
||||
});
|
||||
setOpen(false);
|
||||
},
|
||||
// biome-ignore lint/suspicious/noExplicitAny: FIXME
|
||||
onError: (error: any) => {
|
||||
trackEvent({
|
||||
category: "nft",
|
||||
action: "update-metadata",
|
||||
label: "error",
|
||||
error,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
toast.promise(promise, {
|
||||
loading: "Updating NFT metadata",
|
||||
error: "Failed to update NFT metadata",
|
||||
success: "NFT metadata updated successfully",
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
toast.error("Failed to update NFT metadata");
|
||||
}
|
||||
})}
|
||||
>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Heading size="subtitle.md">Metadata</Heading>
|
||||
<Divider />
|
||||
</div>
|
||||
<FormControl isRequired isInvalid={!!errors.name}>
|
||||
<FormLabel>Name</FormLabel>
|
||||
<Input autoFocus {...register("name")} />
|
||||
<FormErrorMessage>{errors?.name?.message}</FormErrorMessage>
|
||||
</FormControl>
|
||||
<FormControl isInvalid={!!mediaFileError}>
|
||||
<FormLabel>Media</FormLabel>
|
||||
<div className="flex flex-row flex-wrap gap-3">
|
||||
{nft?.metadata && !mediaFileUrl && (
|
||||
<NFTMediaWithEmptyState
|
||||
metadata={nft.metadata}
|
||||
width="200px"
|
||||
height="200px"
|
||||
/>
|
||||
)}
|
||||
<FileInput
|
||||
previewMaxWidth="200px"
|
||||
value={mediaFileUrl as File | string}
|
||||
showUploadButton
|
||||
showPreview={nft?.metadata ? !!mediaFileUrl : true}
|
||||
setValue={setFile}
|
||||
className="rounded border border-border transition-all duration-200"
|
||||
selectOrUpload="Upload"
|
||||
helperText={nft?.metadata ? "New Media" : "Media"}
|
||||
/>
|
||||
</div>
|
||||
<FormHelperText>
|
||||
You can upload image, audio, video, html, text, pdf, and 3d model
|
||||
files here.
|
||||
</FormHelperText>
|
||||
<FormErrorMessage>
|
||||
{mediaFileError?.message as unknown as string}
|
||||
</FormErrorMessage>
|
||||
</FormControl>
|
||||
{showCoverImageUpload && (
|
||||
<FormControl isInvalid={!!errors.image}>
|
||||
<FormLabel>Cover Image</FormLabel>
|
||||
<FileInput
|
||||
previewMaxWidth="200px"
|
||||
accept={{ "image/*": [] }}
|
||||
value={imageUrl}
|
||||
showUploadButton
|
||||
setValue={(file) => setValue("image", file)}
|
||||
className="rounded border border-border transition-all"
|
||||
/>
|
||||
<FormHelperText>
|
||||
You can optionally upload an image as the cover of your NFT.
|
||||
</FormHelperText>
|
||||
<FormErrorMessage>
|
||||
{errors?.image?.message as unknown as string}
|
||||
</FormErrorMessage>
|
||||
</FormControl>
|
||||
)}
|
||||
<FormControl isInvalid={!!errors.description}>
|
||||
<FormLabel>Description</FormLabel>
|
||||
<Textarea {...register("description")} />
|
||||
<FormErrorMessage>{errors?.description?.message}</FormErrorMessage>
|
||||
</FormControl>
|
||||
<PropertiesFormControl
|
||||
watch={watch}
|
||||
// biome-ignore lint/suspicious/noExplicitAny: FIXME
|
||||
errors={errors as any}
|
||||
control={control}
|
||||
register={register}
|
||||
setValue={setValue}
|
||||
/>
|
||||
<Accordion
|
||||
allowToggle={!(errors.background_color || errors.external_url)}
|
||||
index={errors.background_color || errors.external_url ? [0] : undefined}
|
||||
>
|
||||
<AccordionItem>
|
||||
<AccordionButton px={0} justifyContent="space-between">
|
||||
<Heading size="subtitle.md">Advanced Options</Heading>
|
||||
<AccordionIcon />
|
||||
</AccordionButton>
|
||||
<AccordionPanel className="flex flex-col gap-6 px-0">
|
||||
<FormControl isInvalid={!!errors.background_color}>
|
||||
<FormLabel>
|
||||
Background Color <OpenSeaPropertyBadge />
|
||||
</FormLabel>
|
||||
<Input max="6" {...register("background_color")} />
|
||||
<FormHelperText>
|
||||
You can optionally upload an image as the cover of your NFT.
|
||||
Must be a six-character hexadecimal with a pre-pended #.
|
||||
</FormHelperText>
|
||||
<FormErrorMessage>
|
||||
{errors?.image?.message as unknown as string}
|
||||
{errors?.background_color?.message}
|
||||
</FormErrorMessage>
|
||||
</FormControl>
|
||||
)}
|
||||
<FormControl isInvalid={!!errors.description}>
|
||||
<FormLabel>Description</FormLabel>
|
||||
<Textarea {...register("description")} />
|
||||
<FormErrorMessage>{errors?.description?.message}</FormErrorMessage>
|
||||
</FormControl>
|
||||
<PropertiesFormControl
|
||||
watch={watch}
|
||||
// biome-ignore lint/suspicious/noExplicitAny: FIXME
|
||||
errors={errors as any}
|
||||
control={control}
|
||||
register={register}
|
||||
setValue={setValue}
|
||||
/>
|
||||
<Accordion
|
||||
allowToggle={!(errors.background_color || errors.external_url)}
|
||||
index={
|
||||
errors.background_color || errors.external_url ? [0] : undefined
|
||||
}
|
||||
>
|
||||
<AccordionItem>
|
||||
<AccordionButton px={0} justifyContent="space-between">
|
||||
<Heading size="subtitle.md">Advanced Options</Heading>
|
||||
<AccordionIcon />
|
||||
</AccordionButton>
|
||||
<AccordionPanel className="flex flex-col gap-6 px-0">
|
||||
<FormControl isInvalid={!!errors.background_color}>
|
||||
<FormLabel>
|
||||
Background Color <OpenSeaPropertyBadge />
|
||||
</FormLabel>
|
||||
<Input max="6" {...register("background_color")} />
|
||||
<FormHelperText>
|
||||
Must be a six-character hexadecimal with a pre-pended #.
|
||||
</FormHelperText>
|
||||
<FormErrorMessage>
|
||||
{errors?.background_color?.message}
|
||||
</FormErrorMessage>
|
||||
</FormControl>
|
||||
{!externalIsTextFile && (
|
||||
<FormControl isInvalid={!!errors.external_url}>
|
||||
<FormLabel>
|
||||
External URL <OpenSeaPropertyBadge />
|
||||
</FormLabel>
|
||||
<Input {...register("external_url")} />
|
||||
<FormHelperText>
|
||||
This is the URL that will appear below the asset's
|
||||
image on OpenSea and will allow users to leave OpenSea and
|
||||
view the item on your site.
|
||||
</FormHelperText>
|
||||
<FormErrorMessage>
|
||||
{errors?.external_url?.message as unknown as string}
|
||||
</FormErrorMessage>
|
||||
</FormControl>
|
||||
)}
|
||||
<FormControl isInvalid={!!errors.customImage}>
|
||||
<FormLabel>Image URL</FormLabel>
|
||||
<Input max="6" {...register("customImage")} />
|
||||
<FormHelperText>
|
||||
If you already have your NFT image preuploaded, you can set
|
||||
the URL or URI here.
|
||||
</FormHelperText>
|
||||
<FormErrorMessage>
|
||||
{errors?.customImage?.message}
|
||||
</FormErrorMessage>
|
||||
</FormControl>
|
||||
<FormControl isInvalid={!!errors.customAnimationUrl}>
|
||||
<FormLabel>Animation URL</FormLabel>
|
||||
<Input max="6" {...register("customAnimationUrl")} />
|
||||
<FormHelperText>
|
||||
If you already have your NFT Animation URL preuploaded, you
|
||||
can set the URL or URI here.
|
||||
</FormHelperText>
|
||||
<FormErrorMessage>
|
||||
{errors?.customAnimationUrl?.message}
|
||||
</FormErrorMessage>
|
||||
</FormControl>
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
</form>
|
||||
</DrawerBody>
|
||||
<DrawerFooter>
|
||||
{!externalIsTextFile && (
|
||||
<FormControl isInvalid={!!errors.external_url}>
|
||||
<FormLabel>
|
||||
External URL <OpenSeaPropertyBadge />
|
||||
</FormLabel>
|
||||
<Input {...register("external_url")} />
|
||||
<FormHelperText>
|
||||
This is the URL that will appear below the asset's image
|
||||
on OpenSea and will allow users to leave OpenSea and view the
|
||||
item on your site.
|
||||
</FormHelperText>
|
||||
<FormErrorMessage>
|
||||
{errors?.external_url?.message as unknown as string}
|
||||
</FormErrorMessage>
|
||||
</FormControl>
|
||||
)}
|
||||
<FormControl isInvalid={!!errors.customImage}>
|
||||
<FormLabel>Image URL</FormLabel>
|
||||
<Input max="6" {...register("customImage")} />
|
||||
<FormHelperText>
|
||||
If you already have your NFT image preuploaded, you can set the
|
||||
URL or URI here.
|
||||
</FormHelperText>
|
||||
<FormErrorMessage>
|
||||
{errors?.customImage?.message}
|
||||
</FormErrorMessage>
|
||||
</FormControl>
|
||||
<FormControl isInvalid={!!errors.customAnimationUrl}>
|
||||
<FormLabel>Animation URL</FormLabel>
|
||||
<Input max="6" {...register("customAnimationUrl")} />
|
||||
<FormHelperText>
|
||||
If you already have your NFT Animation URL preuploaded, you can
|
||||
set the URL or URI here.
|
||||
</FormHelperText>
|
||||
<FormErrorMessage>
|
||||
{errors?.customAnimationUrl?.message}
|
||||
</FormErrorMessage>
|
||||
</FormControl>
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
<div className="mt-8 flex flex-row justify-end gap-3">
|
||||
<Button
|
||||
isDisabled={isPending}
|
||||
isDisabled={sendAndConfirmTx.isPending}
|
||||
variant="outline"
|
||||
mr={3}
|
||||
onClick={modalContext.onClose}
|
||||
onClick={() => setOpen(false)}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<TransactionButton
|
||||
txChainID={contract.chain.id}
|
||||
transactionCount={1}
|
||||
isLoading={isPending || false}
|
||||
isLoading={sendAndConfirmTx.isPending}
|
||||
form={UPDATE_METADATA_FORM_ID}
|
||||
type="submit"
|
||||
colorScheme="primary"
|
||||
@@ -411,7 +397,7 @@ export const UpdateNftMetadata: React.FC<UpdateNftMetadataForm> = ({
|
||||
>
|
||||
Update NFT
|
||||
</TransactionButton>
|
||||
</DrawerFooter>
|
||||
</>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
+21
-25
@@ -1,8 +1,14 @@
|
||||
import { Flex, useDisclosure } from "@chakra-ui/react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Sheet,
|
||||
SheetContent,
|
||||
SheetHeader,
|
||||
SheetTitle,
|
||||
SheetTrigger,
|
||||
} from "@/components/ui/sheet";
|
||||
import { useState } from "react";
|
||||
import type { NFT, ThirdwebContract } from "thirdweb";
|
||||
import { Button, Drawer, Text } from "tw-components";
|
||||
import { UpdateNftMetadata } from "./update-metadata-form";
|
||||
|
||||
interface UpdateMetadataTabProps {
|
||||
contract: ThirdwebContract;
|
||||
nft: NFT;
|
||||
@@ -19,35 +25,25 @@ const UpdateMetadataTab: React.FC<UpdateMetadataTabProps> = ({
|
||||
nft,
|
||||
useUpdateMetadata,
|
||||
}) => {
|
||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Drawer
|
||||
allowPinchZoom
|
||||
preserveScrollBarGap
|
||||
size="lg"
|
||||
onClose={onClose}
|
||||
isOpen={isOpen}
|
||||
>
|
||||
<Sheet open={open} onOpenChange={setOpen}>
|
||||
<SheetTrigger asChild>
|
||||
<Button variant="primary">Update Metadata</Button>
|
||||
</SheetTrigger>
|
||||
<SheetContent className="w-full overflow-y-auto sm:min-w-[540px] lg:min-w-[700px]">
|
||||
<SheetHeader>
|
||||
<SheetTitle className="text-left">Mint NFT</SheetTitle>
|
||||
</SheetHeader>
|
||||
<UpdateNftMetadata
|
||||
contract={contract}
|
||||
nft={nft}
|
||||
useUpdateMetadata={useUpdateMetadata}
|
||||
setOpen={setOpen}
|
||||
/>
|
||||
</Drawer>
|
||||
<Flex direction="column" gap={6}>
|
||||
<Text>
|
||||
You can update the metadata of this NFT at any time, this will only
|
||||
change the representation of the NFT, not the owner or tokenId.
|
||||
</Text>
|
||||
<Flex justifyContent="right">
|
||||
<Button colorScheme="primary" onClick={onOpen}>
|
||||
Update Metadata
|
||||
</Button>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
+86
-79
@@ -1,16 +1,23 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
Sheet,
|
||||
SheetContent,
|
||||
SheetHeader,
|
||||
SheetTitle,
|
||||
SheetTrigger,
|
||||
} from "@/components/ui/sheet";
|
||||
import { MinterOnly } from "@3rdweb-sdk/react/components/roles/minter-only";
|
||||
import { useDisclosure } from "@chakra-ui/react";
|
||||
import { BatchLazyMint } from "core-ui/batch-upload/batch-lazy-mint";
|
||||
import { useTrack } from "hooks/analytics/useTrack";
|
||||
import { useTxNotifications } from "hooks/useTxNotifications";
|
||||
import { FileStackIcon } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import type { ThirdwebContract } from "thirdweb";
|
||||
import * as ERC721Ext from "thirdweb/extensions/erc721";
|
||||
import * as ERC1155Ext from "thirdweb/extensions/erc1155";
|
||||
import { useReadContract, useSendAndConfirmTransaction } from "thirdweb/react";
|
||||
import { Button, Drawer } from "tw-components";
|
||||
import { Button } from "tw-components";
|
||||
|
||||
interface BatchLazyMintButtonProps {
|
||||
canCreateDelayedRevealBatch: boolean;
|
||||
@@ -24,7 +31,7 @@ export const BatchLazyMintButton: React.FC<BatchLazyMintButtonProps> = ({
|
||||
isErc721,
|
||||
}) => {
|
||||
const trackEvent = useTrack();
|
||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const nextTokenIdToMintQuery = useReadContract(
|
||||
isErc721 ? ERC721Ext.nextTokenIdToMint : ERC1155Ext.nextTokenIdToMint,
|
||||
@@ -43,88 +50,88 @@ export const BatchLazyMintButton: React.FC<BatchLazyMintButtonProps> = ({
|
||||
if (!contract) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<MinterOnly contract={contract}>
|
||||
<Drawer
|
||||
allowPinchZoom
|
||||
preserveScrollBarGap
|
||||
size="full"
|
||||
onClose={onClose}
|
||||
isOpen={isOpen}
|
||||
>
|
||||
<BatchLazyMint
|
||||
chainId={contract.chain.id}
|
||||
onSubmit={async ({ revealType, data }) => {
|
||||
// nice, we can set up everything the same for both the only thing that changes is the action string
|
||||
const action = `batch-upload-${revealType}` as const;
|
||||
|
||||
trackEvent({
|
||||
category: "nft",
|
||||
action,
|
||||
label: "attempt",
|
||||
});
|
||||
try {
|
||||
const tx = (() => {
|
||||
switch (true) {
|
||||
// lazy mint erc721
|
||||
case revealType === "instant" && isErc721: {
|
||||
return ERC721Ext.lazyMint({
|
||||
contract,
|
||||
nfts: data.metadatas,
|
||||
});
|
||||
}
|
||||
// lazy mint erc1155
|
||||
case revealType === "instant" && !isErc721: {
|
||||
return ERC1155Ext.lazyMint({
|
||||
contract,
|
||||
nfts: data.metadatas,
|
||||
});
|
||||
}
|
||||
// delayed reveal erc721
|
||||
case revealType === "delayed": {
|
||||
return ERC721Ext.createDelayedRevealBatch({
|
||||
contract,
|
||||
metadata: data.metadata,
|
||||
password: data.password,
|
||||
placeholderMetadata: data.placeholderMetadata,
|
||||
});
|
||||
}
|
||||
default: {
|
||||
throw new Error("Invalid reveal type");
|
||||
}
|
||||
}
|
||||
})();
|
||||
|
||||
await sendTxMutation.mutateAsync(tx);
|
||||
|
||||
<Sheet onOpenChange={setOpen} open={open}>
|
||||
<SheetTrigger asChild>
|
||||
<Button
|
||||
colorScheme="primary"
|
||||
leftIcon={<FileStackIcon className="size-4" />}
|
||||
>
|
||||
Batch Upload
|
||||
</Button>
|
||||
</SheetTrigger>
|
||||
<SheetContent className="min-w-full overflow-y-auto">
|
||||
<SheetHeader>
|
||||
<SheetTitle className="text-left">Upload NFTs</SheetTitle>
|
||||
</SheetHeader>
|
||||
<BatchLazyMint
|
||||
chainId={contract.chain.id}
|
||||
onSubmit={async ({ revealType, data }) => {
|
||||
// nice, we can set up everything the same for both the only thing that changes is the action string
|
||||
const action = `batch-upload-${revealType}` as const;
|
||||
trackEvent({
|
||||
category: "nft",
|
||||
action,
|
||||
label: "success",
|
||||
label: "attempt",
|
||||
});
|
||||
txNotifications.onSuccess();
|
||||
onClose();
|
||||
} catch (error) {
|
||||
trackEvent({
|
||||
category: "nft",
|
||||
action,
|
||||
label: "error",
|
||||
error,
|
||||
});
|
||||
txNotifications.onError(error);
|
||||
}
|
||||
}}
|
||||
nextTokenIdToMint={nextTokenIdToMintQuery.data || 0n}
|
||||
canCreateDelayedRevealBatch={canCreateDelayedRevealBatch}
|
||||
/>
|
||||
</Drawer>
|
||||
<Button
|
||||
colorScheme="primary"
|
||||
leftIcon={<FileStackIcon className="size-4" />}
|
||||
onClick={onOpen}
|
||||
>
|
||||
Batch Upload
|
||||
</Button>
|
||||
try {
|
||||
const tx = (() => {
|
||||
switch (true) {
|
||||
// lazy mint erc721
|
||||
case revealType === "instant" && isErc721: {
|
||||
return ERC721Ext.lazyMint({
|
||||
contract,
|
||||
nfts: data.metadatas,
|
||||
});
|
||||
}
|
||||
// lazy mint erc1155
|
||||
case revealType === "instant" && !isErc721: {
|
||||
return ERC1155Ext.lazyMint({
|
||||
contract,
|
||||
nfts: data.metadatas,
|
||||
});
|
||||
}
|
||||
// delayed reveal erc721
|
||||
case revealType === "delayed": {
|
||||
return ERC721Ext.createDelayedRevealBatch({
|
||||
contract,
|
||||
metadata: data.metadata,
|
||||
password: data.password,
|
||||
placeholderMetadata: data.placeholderMetadata,
|
||||
});
|
||||
}
|
||||
default: {
|
||||
throw new Error("Invalid reveal type");
|
||||
}
|
||||
}
|
||||
})();
|
||||
|
||||
await sendTxMutation.mutateAsync(tx);
|
||||
|
||||
trackEvent({
|
||||
category: "nft",
|
||||
action,
|
||||
label: "success",
|
||||
});
|
||||
txNotifications.onSuccess();
|
||||
setOpen(false);
|
||||
} catch (error) {
|
||||
trackEvent({
|
||||
category: "nft",
|
||||
action,
|
||||
label: "error",
|
||||
error,
|
||||
});
|
||||
txNotifications.onError(error);
|
||||
}
|
||||
}}
|
||||
nextTokenIdToMint={nextTokenIdToMintQuery.data || 0n}
|
||||
canCreateDelayedRevealBatch={canCreateDelayedRevealBatch}
|
||||
/>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
</MinterOnly>
|
||||
);
|
||||
};
|
||||
|
||||
+2
-2
@@ -49,9 +49,9 @@ export const NFTClaimButton: React.FC<NFTClaimButtonProps> = ({ contract }) => {
|
||||
Claim
|
||||
</Button>
|
||||
</SheetTrigger>
|
||||
<SheetContent className="z-[10000] overflow-y-auto sm:w-[540px] sm:max-w-[90%] lg:w-[700px]">
|
||||
<SheetContent className="overflow-y-auto sm:w-[540px] sm:max-w-[90%] lg:w-[700px]">
|
||||
<SheetHeader>
|
||||
<SheetTitle>Claim NFTs</SheetTitle>
|
||||
<SheetTitle className="text-left">Claim NFTs</SheetTitle>
|
||||
</SheetHeader>
|
||||
<form className="mt-8 flex w-full flex-col gap-3 md:flex-row">
|
||||
<div className="flex w-full flex-col gap-6 md:flex-row">
|
||||
|
||||
+31
-20
@@ -1,9 +1,16 @@
|
||||
"use client";
|
||||
import {
|
||||
Sheet,
|
||||
SheetContent,
|
||||
SheetHeader,
|
||||
SheetTitle,
|
||||
SheetTrigger,
|
||||
} from "@/components/ui/sheet";
|
||||
import { MinterOnly } from "@3rdweb-sdk/react/components/roles/minter-only";
|
||||
import { useDisclosure } from "@chakra-ui/react";
|
||||
import { PlusIcon } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import type { ThirdwebContract } from "thirdweb";
|
||||
import { Button, Drawer } from "tw-components";
|
||||
import { Button } from "tw-components";
|
||||
import { LazyMintNftForm } from "./lazy-mint-form";
|
||||
|
||||
interface NFTLazyMintButtonProps {
|
||||
@@ -16,26 +23,30 @@ export const NFTLazyMintButton: React.FC<NFTLazyMintButtonProps> = ({
|
||||
isErc721,
|
||||
...restButtonProps
|
||||
}) => {
|
||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||
const [open, setOpen] = useState(false);
|
||||
return (
|
||||
<MinterOnly contract={contract}>
|
||||
<Drawer
|
||||
allowPinchZoom
|
||||
preserveScrollBarGap
|
||||
size="lg"
|
||||
onClose={onClose}
|
||||
isOpen={isOpen}
|
||||
>
|
||||
<LazyMintNftForm contract={contract} isErc721={isErc721} />
|
||||
</Drawer>
|
||||
<Button
|
||||
colorScheme="primary"
|
||||
leftIcon={<PlusIcon className="size-5" />}
|
||||
{...restButtonProps}
|
||||
onClick={onOpen}
|
||||
>
|
||||
Single Upload
|
||||
</Button>
|
||||
<Sheet open={open} onOpenChange={setOpen}>
|
||||
<SheetTrigger asChild>
|
||||
<Button
|
||||
colorScheme="primary"
|
||||
leftIcon={<PlusIcon className="size-5" />}
|
||||
{...restButtonProps}
|
||||
>
|
||||
Single Upload
|
||||
</Button>
|
||||
</SheetTrigger>
|
||||
<SheetContent className="w-full overflow-y-auto sm:min-w-[540px] lg:min-w-[700px]">
|
||||
<SheetHeader>
|
||||
<SheetTitle className="text-left">Mint NFT</SheetTitle>
|
||||
</SheetHeader>
|
||||
<LazyMintNftForm
|
||||
contract={contract}
|
||||
isErc721={isErc721}
|
||||
setOpen={setOpen}
|
||||
/>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
</MinterOnly>
|
||||
);
|
||||
};
|
||||
|
||||
+150
-154
@@ -7,13 +7,9 @@ import {
|
||||
AccordionItem,
|
||||
AccordionPanel,
|
||||
Divider,
|
||||
DrawerBody,
|
||||
DrawerFooter,
|
||||
DrawerHeader,
|
||||
FormControl,
|
||||
Input,
|
||||
Textarea,
|
||||
useModalContext,
|
||||
} from "@chakra-ui/react";
|
||||
import { OpenSeaPropertyBadge } from "components/badges/opensea";
|
||||
import { TransactionButton } from "components/buttons/TransactionButton";
|
||||
@@ -21,8 +17,9 @@ import { PropertiesFormControl } from "components/contract-pages/forms/propertie
|
||||
import { FileInput } from "components/shared/FileInput";
|
||||
import { useTrack } from "hooks/analytics/useTrack";
|
||||
import { useImageFileOrUrl } from "hooks/useImageFileOrUrl";
|
||||
import { useTxNotifications } from "hooks/useTxNotifications";
|
||||
import type { Dispatch, SetStateAction } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import type { ThirdwebContract } from "thirdweb";
|
||||
import { lazyMint as lazyMint721 } from "thirdweb/extensions/erc721";
|
||||
import { lazyMint as lazyMint1155 } from "thirdweb/extensions/erc1155";
|
||||
@@ -42,15 +39,17 @@ const LAZY_MINT_FORM_ID = "nft-lazy-mint-form";
|
||||
type LazyMintNftFormParams = {
|
||||
contract: ThirdwebContract;
|
||||
isErc721: boolean;
|
||||
setOpen: Dispatch<SetStateAction<boolean>>;
|
||||
};
|
||||
|
||||
export const LazyMintNftForm: React.FC<LazyMintNftFormParams> = ({
|
||||
contract,
|
||||
isErc721,
|
||||
setOpen,
|
||||
}) => {
|
||||
const trackEvent = useTrack();
|
||||
const address = useActiveAccount()?.address;
|
||||
const { mutate, isPending } = useSendAndConfirmTransaction();
|
||||
const sendAndConfirmTx = useSendAndConfirmTransaction();
|
||||
|
||||
const {
|
||||
setValue,
|
||||
@@ -67,13 +66,6 @@ export const LazyMintNftForm: React.FC<LazyMintNftFormParams> = ({
|
||||
}
|
||||
>();
|
||||
|
||||
const modalContext = useModalContext();
|
||||
const { onSuccess, onError } = useTxNotifications(
|
||||
"NFT Lazy minted successfully",
|
||||
"Failed to lazy mint NFT",
|
||||
contract,
|
||||
);
|
||||
|
||||
const setFile = (file: File) => {
|
||||
if (file.type.includes("image")) {
|
||||
// image files
|
||||
@@ -141,19 +133,15 @@ export const LazyMintNftForm: React.FC<LazyMintNftFormParams> = ({
|
||||
|
||||
return (
|
||||
<>
|
||||
<DrawerHeader>
|
||||
<Heading>Mint NFT</Heading>
|
||||
</DrawerHeader>
|
||||
<DrawerBody>
|
||||
<form
|
||||
className="flex flex-col gap-6"
|
||||
id={LAZY_MINT_FORM_ID}
|
||||
onSubmit={handleSubmit(async (data) => {
|
||||
if (!address) {
|
||||
onError("Please connect your wallet to mint.");
|
||||
return;
|
||||
}
|
||||
|
||||
<form
|
||||
className="mt-6 flex flex-col gap-6"
|
||||
id={LAZY_MINT_FORM_ID}
|
||||
onSubmit={handleSubmit(async (data) => {
|
||||
if (!address) {
|
||||
toast.error("Please connect your wallet to mint.");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
trackEvent({
|
||||
category: "nft",
|
||||
action: "lazy-mint",
|
||||
@@ -163,15 +151,15 @@ export const LazyMintNftForm: React.FC<LazyMintNftFormParams> = ({
|
||||
const transaction = isErc721
|
||||
? lazyMint721({ contract, nfts })
|
||||
: lazyMint1155({ contract, nfts });
|
||||
mutate(transaction, {
|
||||
|
||||
const promise = sendAndConfirmTx.mutateAsync(transaction, {
|
||||
onSuccess: () => {
|
||||
trackEvent({
|
||||
category: "nft",
|
||||
action: "lazy-mint",
|
||||
label: "success",
|
||||
});
|
||||
onSuccess();
|
||||
modalContext.onClose();
|
||||
setOpen(false);
|
||||
},
|
||||
onError: (error) => {
|
||||
trackEvent({
|
||||
@@ -180,154 +168,162 @@ export const LazyMintNftForm: React.FC<LazyMintNftFormParams> = ({
|
||||
label: "error",
|
||||
error,
|
||||
});
|
||||
onError(error);
|
||||
},
|
||||
});
|
||||
})}
|
||||
>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Heading size="subtitle.md">Metadata</Heading>
|
||||
<Divider />
|
||||
|
||||
toast.promise(promise, {
|
||||
loading: "Lazy minting NFT",
|
||||
error: "Failed to lazy mint NFT",
|
||||
success: "Lazy minted successfully",
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
toast.error("Failed to lazy mint NFT");
|
||||
}
|
||||
})}
|
||||
>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Heading size="subtitle.md">Metadata</Heading>
|
||||
<Divider />
|
||||
</div>
|
||||
<FormControl isRequired isInvalid={!!errors.name}>
|
||||
<FormLabel>Name</FormLabel>
|
||||
<Input autoFocus {...register("name")} />
|
||||
<FormErrorMessage>{errors?.name?.message}</FormErrorMessage>
|
||||
</FormControl>
|
||||
<FormControl isInvalid={!!mediaFileError}>
|
||||
<FormLabel>Media</FormLabel>
|
||||
<div>
|
||||
<FileInput
|
||||
previewMaxWidth="200px"
|
||||
value={mediaFileUrl as File | string}
|
||||
showUploadButton
|
||||
showPreview={true}
|
||||
setValue={setFile}
|
||||
className="rounded border border-border transition-all duration-200"
|
||||
selectOrUpload="Upload"
|
||||
helperText="Media"
|
||||
/>
|
||||
</div>
|
||||
<FormControl isRequired isInvalid={!!errors.name}>
|
||||
<FormLabel>Name</FormLabel>
|
||||
<Input autoFocus {...register("name")} />
|
||||
<FormErrorMessage>{errors?.name?.message}</FormErrorMessage>
|
||||
</FormControl>
|
||||
<FormControl isInvalid={!!mediaFileError}>
|
||||
<FormLabel>Media</FormLabel>
|
||||
<div>
|
||||
<FileInput
|
||||
previewMaxWidth="200px"
|
||||
value={mediaFileUrl as File | string}
|
||||
showUploadButton
|
||||
showPreview={true}
|
||||
setValue={setFile}
|
||||
className="rounded border border-border transition-all duration-200"
|
||||
selectOrUpload="Upload"
|
||||
helperText="Media"
|
||||
/>
|
||||
</div>
|
||||
<FormHelperText>
|
||||
You can upload image, audio, video, html, text, pdf, and 3d model
|
||||
files here.
|
||||
</FormHelperText>
|
||||
<FormErrorMessage>
|
||||
{mediaFileError?.message as unknown as string}
|
||||
</FormErrorMessage>
|
||||
</FormControl>
|
||||
{showCoverImageUpload && (
|
||||
<FormControl isInvalid={!!errors.image}>
|
||||
<FormLabel>Cover Image</FormLabel>
|
||||
<FileInput
|
||||
previewMaxWidth="200px"
|
||||
accept={{ "image/*": [] }}
|
||||
value={imageUrl}
|
||||
showUploadButton
|
||||
setValue={(file) => setValue("image", file)}
|
||||
className="rounded border border-border transition-all"
|
||||
/>
|
||||
<FormHelperText>
|
||||
You can upload image, audio, video, html, text, pdf, and 3d model
|
||||
files here.
|
||||
You can optionally upload an image as the cover of your NFT.
|
||||
</FormHelperText>
|
||||
<FormErrorMessage>
|
||||
{mediaFileError?.message as unknown as string}
|
||||
{errors?.image?.message as unknown as string}
|
||||
</FormErrorMessage>
|
||||
</FormControl>
|
||||
{showCoverImageUpload && (
|
||||
<FormControl isInvalid={!!errors.image}>
|
||||
<FormLabel>Cover Image</FormLabel>
|
||||
<FileInput
|
||||
previewMaxWidth="200px"
|
||||
accept={{ "image/*": [] }}
|
||||
value={imageUrl}
|
||||
showUploadButton
|
||||
setValue={(file) => setValue("image", file)}
|
||||
className="rounded border border-border transition-all"
|
||||
/>
|
||||
<FormHelperText>
|
||||
You can optionally upload an image as the cover of your NFT.
|
||||
</FormHelperText>
|
||||
<FormErrorMessage>
|
||||
{errors?.image?.message as unknown as string}
|
||||
</FormErrorMessage>
|
||||
</FormControl>
|
||||
)}
|
||||
<FormControl isInvalid={!!errors.description}>
|
||||
<FormLabel>Description</FormLabel>
|
||||
<Textarea {...register("description")} />
|
||||
<FormErrorMessage>{errors?.description?.message}</FormErrorMessage>
|
||||
</FormControl>
|
||||
<PropertiesFormControl
|
||||
watch={watch}
|
||||
// biome-ignore lint/suspicious/noExplicitAny: FIXME
|
||||
errors={errors as any}
|
||||
control={control}
|
||||
register={register}
|
||||
setValue={setValue}
|
||||
/>
|
||||
<Accordion
|
||||
allowToggle={!(errors.background_color || errors.external_url)}
|
||||
index={
|
||||
errors.background_color || errors.external_url ? [0] : undefined
|
||||
}
|
||||
>
|
||||
<AccordionItem>
|
||||
<AccordionButton px={0} justifyContent="space-between">
|
||||
<Heading size="subtitle.md">Advanced Options</Heading>
|
||||
<AccordionIcon />
|
||||
</AccordionButton>
|
||||
<AccordionPanel className="flex flex-col gap-6 px-0">
|
||||
<FormControl isInvalid={!!errors.background_color}>
|
||||
)}
|
||||
<FormControl isInvalid={!!errors.description}>
|
||||
<FormLabel>Description</FormLabel>
|
||||
<Textarea {...register("description")} />
|
||||
<FormErrorMessage>{errors?.description?.message}</FormErrorMessage>
|
||||
</FormControl>
|
||||
<PropertiesFormControl
|
||||
watch={watch}
|
||||
// biome-ignore lint/suspicious/noExplicitAny: FIXME
|
||||
errors={errors as any}
|
||||
control={control}
|
||||
register={register}
|
||||
setValue={setValue}
|
||||
/>
|
||||
<Accordion
|
||||
allowToggle={!(errors.background_color || errors.external_url)}
|
||||
index={
|
||||
errors.background_color || errors.external_url ? [0] : undefined
|
||||
}
|
||||
>
|
||||
<AccordionItem>
|
||||
<AccordionButton px={0} justifyContent="space-between">
|
||||
<Heading size="subtitle.md">Advanced Options</Heading>
|
||||
<AccordionIcon />
|
||||
</AccordionButton>
|
||||
<AccordionPanel className="flex flex-col gap-6 px-0">
|
||||
<FormControl isInvalid={!!errors.background_color}>
|
||||
<FormLabel>
|
||||
Background Color <OpenSeaPropertyBadge />
|
||||
</FormLabel>
|
||||
<Input max="6" {...register("background_color")} />
|
||||
<FormHelperText>
|
||||
Must be a six-character hexadecimal with a pre-pended #.
|
||||
</FormHelperText>
|
||||
<FormErrorMessage>
|
||||
{errors?.background_color?.message}
|
||||
</FormErrorMessage>
|
||||
</FormControl>
|
||||
{!externalIsTextFile && (
|
||||
<FormControl isInvalid={!!errors.external_url}>
|
||||
<FormLabel>
|
||||
Background Color <OpenSeaPropertyBadge />
|
||||
External URL <OpenSeaPropertyBadge />
|
||||
</FormLabel>
|
||||
<Input max="6" {...register("background_color")} />
|
||||
<Input {...register("external_url")} />
|
||||
<FormHelperText>
|
||||
Must be a six-character hexadecimal with a pre-pended #.
|
||||
This is the URL that will appear below the asset's
|
||||
image on OpenSea and will allow users to leave OpenSea and
|
||||
view the item on your site.
|
||||
</FormHelperText>
|
||||
<FormErrorMessage>
|
||||
{errors?.background_color?.message}
|
||||
{errors?.external_url?.message as unknown as string}
|
||||
</FormErrorMessage>
|
||||
</FormControl>
|
||||
{!externalIsTextFile && (
|
||||
<FormControl isInvalid={!!errors.external_url}>
|
||||
<FormLabel>
|
||||
External URL <OpenSeaPropertyBadge />
|
||||
</FormLabel>
|
||||
<Input {...register("external_url")} />
|
||||
<FormHelperText>
|
||||
This is the URL that will appear below the asset's
|
||||
image on OpenSea and will allow users to leave OpenSea and
|
||||
view the item on your site.
|
||||
</FormHelperText>
|
||||
<FormErrorMessage>
|
||||
{errors?.external_url?.message as unknown as string}
|
||||
</FormErrorMessage>
|
||||
</FormControl>
|
||||
)}
|
||||
<FormControl isInvalid={!!errors.customImage}>
|
||||
<FormLabel>Image URL</FormLabel>
|
||||
<Input max="6" {...register("customImage")} />
|
||||
<FormHelperText>
|
||||
If you already have your NFT image preuploaded, you can set
|
||||
the URL or URI here.
|
||||
</FormHelperText>
|
||||
<FormErrorMessage>
|
||||
{errors?.customImage?.message}
|
||||
</FormErrorMessage>
|
||||
</FormControl>
|
||||
<FormControl isInvalid={!!errors.customAnimationUrl}>
|
||||
<FormLabel>Animation URL</FormLabel>
|
||||
<Input max="6" {...register("customAnimationUrl")} />
|
||||
<FormHelperText>
|
||||
If you already have your NFT Animation URL preuploaded, you
|
||||
can set the URL or URI here.
|
||||
</FormHelperText>
|
||||
<FormErrorMessage>
|
||||
{errors?.customAnimationUrl?.message}
|
||||
</FormErrorMessage>
|
||||
</FormControl>
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
</form>
|
||||
</DrawerBody>
|
||||
<DrawerFooter>
|
||||
)}
|
||||
<FormControl isInvalid={!!errors.customImage}>
|
||||
<FormLabel>Image URL</FormLabel>
|
||||
<Input max="6" {...register("customImage")} />
|
||||
<FormHelperText>
|
||||
If you already have your NFT image preuploaded, you can set
|
||||
the URL or URI here.
|
||||
</FormHelperText>
|
||||
<FormErrorMessage>
|
||||
{errors?.customImage?.message}
|
||||
</FormErrorMessage>
|
||||
</FormControl>
|
||||
<FormControl isInvalid={!!errors.customAnimationUrl}>
|
||||
<FormLabel>Animation URL</FormLabel>
|
||||
<Input max="6" {...register("customAnimationUrl")} />
|
||||
<FormHelperText>
|
||||
If you already have your NFT Animation URL preuploaded, you
|
||||
can set the URL or URI here.
|
||||
</FormHelperText>
|
||||
<FormErrorMessage>
|
||||
{errors?.customAnimationUrl?.message}
|
||||
</FormErrorMessage>
|
||||
</FormControl>
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
</form>
|
||||
<div className="mt-8 flex flex-row justify-end gap-3">
|
||||
<Button
|
||||
isDisabled={isPending}
|
||||
isDisabled={sendAndConfirmTx.isPending}
|
||||
variant="outline"
|
||||
mr={3}
|
||||
onClick={modalContext.onClose}
|
||||
onClick={() => setOpen(false)}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<TransactionButton
|
||||
txChainID={contract.chain.id}
|
||||
transactionCount={1}
|
||||
isLoading={isPending || false}
|
||||
isLoading={sendAndConfirmTx.isPending}
|
||||
form={LAZY_MINT_FORM_ID}
|
||||
type="submit"
|
||||
colorScheme="primary"
|
||||
@@ -335,7 +331,7 @@ export const LazyMintNftForm: React.FC<LazyMintNftFormParams> = ({
|
||||
>
|
||||
Lazy Mint NFT
|
||||
</TransactionButton>
|
||||
</DrawerFooter>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
+32
-20
@@ -1,10 +1,17 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
Sheet,
|
||||
SheetContent,
|
||||
SheetHeader,
|
||||
SheetTitle,
|
||||
SheetTrigger,
|
||||
} from "@/components/ui/sheet";
|
||||
import { MinterOnly } from "@3rdweb-sdk/react/components/roles/minter-only";
|
||||
import { useDisclosure } from "@chakra-ui/react";
|
||||
import { PlusIcon } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import type { ThirdwebContract } from "thirdweb";
|
||||
import { Button, Drawer } from "tw-components";
|
||||
import { Button } from "tw-components";
|
||||
import { NFTMintForm } from "./mint-form";
|
||||
|
||||
interface NFTMintButtonProps {
|
||||
@@ -17,26 +24,31 @@ export const NFTMintButton: React.FC<NFTMintButtonProps> = ({
|
||||
isErc721,
|
||||
...restButtonProps
|
||||
}) => {
|
||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<MinterOnly contract={contract}>
|
||||
<Drawer
|
||||
allowPinchZoom
|
||||
preserveScrollBarGap
|
||||
size="lg"
|
||||
onClose={onClose}
|
||||
isOpen={isOpen}
|
||||
>
|
||||
<NFTMintForm contract={contract} isErc721={isErc721} />
|
||||
</Drawer>
|
||||
<Button
|
||||
colorScheme="primary"
|
||||
leftIcon={<PlusIcon className="size-5" />}
|
||||
{...restButtonProps}
|
||||
onClick={onOpen}
|
||||
>
|
||||
Mint
|
||||
</Button>
|
||||
<Sheet open={open} onOpenChange={setOpen}>
|
||||
<SheetTrigger asChild>
|
||||
<Button
|
||||
colorScheme="primary"
|
||||
leftIcon={<PlusIcon className="size-5" />}
|
||||
{...restButtonProps}
|
||||
>
|
||||
Mint
|
||||
</Button>
|
||||
</SheetTrigger>
|
||||
<SheetContent className="w-full overflow-y-auto sm:min-w-[540px] lg:min-w-[700px]">
|
||||
<SheetHeader>
|
||||
<SheetTitle className="text-left">Mint NFT</SheetTitle>
|
||||
</SheetHeader>
|
||||
<NFTMintForm
|
||||
contract={contract}
|
||||
isErc721={isErc721}
|
||||
setOpen={setOpen}
|
||||
/>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
</MinterOnly>
|
||||
);
|
||||
};
|
||||
|
||||
+164
-166
@@ -7,13 +7,9 @@ import {
|
||||
AccordionItem,
|
||||
AccordionPanel,
|
||||
Divider,
|
||||
DrawerBody,
|
||||
DrawerFooter,
|
||||
DrawerHeader,
|
||||
FormControl,
|
||||
Input,
|
||||
Textarea,
|
||||
useModalContext,
|
||||
} from "@chakra-ui/react";
|
||||
import { OpenSeaPropertyBadge } from "components/badges/opensea";
|
||||
import { TransactionButton } from "components/buttons/TransactionButton";
|
||||
@@ -21,8 +17,9 @@ import { PropertiesFormControl } from "components/contract-pages/forms/propertie
|
||||
import { FileInput } from "components/shared/FileInput";
|
||||
import { useTrack } from "hooks/analytics/useTrack";
|
||||
import { useImageFileOrUrl } from "hooks/useImageFileOrUrl";
|
||||
import { useTxNotifications } from "hooks/useTxNotifications";
|
||||
import type { Dispatch, SetStateAction } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import type { ThirdwebContract } from "thirdweb";
|
||||
import { mintTo as erc721MintTo } from "thirdweb/extensions/erc721";
|
||||
import { mintTo as erc1155MintTo } from "thirdweb/extensions/erc1155";
|
||||
@@ -42,9 +39,14 @@ const MINT_FORM_ID = "nft-mint-form";
|
||||
type NFTMintForm = {
|
||||
contract: ThirdwebContract;
|
||||
isErc721: boolean;
|
||||
setOpen: Dispatch<SetStateAction<boolean>>;
|
||||
};
|
||||
|
||||
export const NFTMintForm: React.FC<NFTMintForm> = ({ contract, isErc721 }) => {
|
||||
export const NFTMintForm: React.FC<NFTMintForm> = ({
|
||||
contract,
|
||||
isErc721,
|
||||
setOpen,
|
||||
}) => {
|
||||
const trackEvent = useTrack();
|
||||
const address = useActiveAccount()?.address;
|
||||
const {
|
||||
@@ -62,14 +64,6 @@ export const NFTMintForm: React.FC<NFTMintForm> = ({ contract, isErc721 }) => {
|
||||
}
|
||||
>();
|
||||
|
||||
const modalContext = useModalContext();
|
||||
|
||||
const { onSuccess, onError } = useTxNotifications(
|
||||
"NFT minted successfully",
|
||||
"Failed to mint NFT",
|
||||
contract,
|
||||
);
|
||||
|
||||
const setFile = (file: File) => {
|
||||
if (file.type.includes("image")) {
|
||||
// image files
|
||||
@@ -135,23 +129,20 @@ export const NFTMintForm: React.FC<NFTMintForm> = ({ contract, isErc721 }) => {
|
||||
watch("animation_url") instanceof File ||
|
||||
watch("external_url") instanceof File;
|
||||
|
||||
const { mutate, isPending } = useSendAndConfirmTransaction();
|
||||
const sendAndConfirmTx = useSendAndConfirmTransaction();
|
||||
|
||||
return (
|
||||
<>
|
||||
<DrawerHeader>
|
||||
<Heading>Mint NFT</Heading>
|
||||
</DrawerHeader>
|
||||
<DrawerBody>
|
||||
<form
|
||||
className="flex flex-col gap-6"
|
||||
id={MINT_FORM_ID}
|
||||
onSubmit={handleSubmit((data) => {
|
||||
if (!address) {
|
||||
onError("Please connect your wallet to mint.");
|
||||
return;
|
||||
}
|
||||
<form
|
||||
className="mt-6 flex flex-col gap-6"
|
||||
id={MINT_FORM_ID}
|
||||
onSubmit={handleSubmit((data) => {
|
||||
if (!address) {
|
||||
toast.error("Please connect your wallet to mint.");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const dataWithCustom = {
|
||||
...data,
|
||||
image: data.image || data.customImage,
|
||||
@@ -172,15 +163,14 @@ export const NFTMintForm: React.FC<NFTMintForm> = ({ contract, isErc721 }) => {
|
||||
nft,
|
||||
supply: BigInt(data.supply),
|
||||
});
|
||||
mutate(transaction, {
|
||||
const promise = sendAndConfirmTx.mutateAsync(transaction, {
|
||||
onSuccess: () => {
|
||||
trackEvent({
|
||||
category: "nft",
|
||||
action: "mint",
|
||||
label: "success",
|
||||
});
|
||||
onSuccess();
|
||||
modalContext.onClose();
|
||||
setOpen(false);
|
||||
},
|
||||
// biome-ignore lint/suspicious/noExplicitAny: FIXME
|
||||
onError: (error: any) => {
|
||||
@@ -190,166 +180,174 @@ export const NFTMintForm: React.FC<NFTMintForm> = ({ contract, isErc721 }) => {
|
||||
label: "error",
|
||||
error,
|
||||
});
|
||||
onError(error);
|
||||
},
|
||||
});
|
||||
})}
|
||||
>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Heading size="subtitle.md">Metadata</Heading>
|
||||
<Divider />
|
||||
|
||||
toast.promise(promise, {
|
||||
loading: "Minting NFT",
|
||||
success: "NFT minted successfully",
|
||||
error: "Failed to mint NFT",
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
toast.error("Failed to mint NFT");
|
||||
}
|
||||
})}
|
||||
>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Heading size="subtitle.md">Metadata</Heading>
|
||||
<Divider />
|
||||
</div>
|
||||
<FormControl isRequired isInvalid={!!errors.name}>
|
||||
<FormLabel>Name</FormLabel>
|
||||
<Input autoFocus {...register("name")} />
|
||||
<FormErrorMessage>{errors?.name?.message}</FormErrorMessage>
|
||||
</FormControl>
|
||||
<FormControl isInvalid={!!mediaFileError}>
|
||||
<FormLabel>Media</FormLabel>
|
||||
<div>
|
||||
<FileInput
|
||||
previewMaxWidth="200px"
|
||||
value={mediaFileUrl as File | string}
|
||||
showUploadButton
|
||||
showPreview={true}
|
||||
setValue={setFile}
|
||||
className="rounded border border-border transition-all duration-200"
|
||||
selectOrUpload="Upload"
|
||||
helperText="Media"
|
||||
/>
|
||||
</div>
|
||||
<FormControl isRequired isInvalid={!!errors.name}>
|
||||
<FormLabel>Name</FormLabel>
|
||||
<Input autoFocus {...register("name")} />
|
||||
<FormErrorMessage>{errors?.name?.message}</FormErrorMessage>
|
||||
</FormControl>
|
||||
<FormControl isInvalid={!!mediaFileError}>
|
||||
<FormLabel>Media</FormLabel>
|
||||
<div>
|
||||
<FileInput
|
||||
previewMaxWidth="200px"
|
||||
value={mediaFileUrl as File | string}
|
||||
showUploadButton
|
||||
showPreview={true}
|
||||
setValue={setFile}
|
||||
className="rounded border border-border transition-all duration-200"
|
||||
selectOrUpload="Upload"
|
||||
helperText="Media"
|
||||
/>
|
||||
</div>
|
||||
<FormHelperText>
|
||||
You can upload image, audio, video, html, text, pdf, and 3d model
|
||||
files here.
|
||||
</FormHelperText>
|
||||
<FormErrorMessage>
|
||||
{mediaFileError?.message as unknown as string}
|
||||
</FormErrorMessage>
|
||||
</FormControl>
|
||||
{showCoverImageUpload && (
|
||||
<FormControl isInvalid={!!errors.image}>
|
||||
<FormLabel>Cover Image</FormLabel>
|
||||
<FileInput
|
||||
previewMaxWidth="200px"
|
||||
accept={{ "image/*": [] }}
|
||||
value={imageUrl}
|
||||
showUploadButton
|
||||
setValue={(file) => setValue("image", file)}
|
||||
className="rounded border border-border transition-all"
|
||||
/>
|
||||
<FormHelperText>
|
||||
You can upload image, audio, video, html, text, pdf, and 3d model
|
||||
files here.
|
||||
You can optionally upload an image as the cover of your NFT.
|
||||
</FormHelperText>
|
||||
<FormErrorMessage>
|
||||
{mediaFileError?.message as unknown as string}
|
||||
{errors?.image?.message as unknown as string}
|
||||
</FormErrorMessage>
|
||||
</FormControl>
|
||||
{showCoverImageUpload && (
|
||||
<FormControl isInvalid={!!errors.image}>
|
||||
<FormLabel>Cover Image</FormLabel>
|
||||
<FileInput
|
||||
previewMaxWidth="200px"
|
||||
accept={{ "image/*": [] }}
|
||||
value={imageUrl}
|
||||
showUploadButton
|
||||
setValue={(file) => setValue("image", file)}
|
||||
className="rounded border border-border transition-all"
|
||||
/>
|
||||
<FormHelperText>
|
||||
You can optionally upload an image as the cover of your NFT.
|
||||
</FormHelperText>
|
||||
<FormErrorMessage>
|
||||
{errors?.image?.message as unknown as string}
|
||||
</FormErrorMessage>
|
||||
</FormControl>
|
||||
)}
|
||||
<FormControl isInvalid={!!errors.description}>
|
||||
<FormLabel>Description</FormLabel>
|
||||
<Textarea {...register("description")} />
|
||||
<FormErrorMessage>{errors?.description?.message}</FormErrorMessage>
|
||||
)}
|
||||
<FormControl isInvalid={!!errors.description}>
|
||||
<FormLabel>Description</FormLabel>
|
||||
<Textarea {...register("description")} />
|
||||
<FormErrorMessage>{errors?.description?.message}</FormErrorMessage>
|
||||
</FormControl>
|
||||
{!isErc721 && (
|
||||
<FormControl isRequired isInvalid={!!errors.supply}>
|
||||
<FormLabel>Initial Supply</FormLabel>
|
||||
<Input
|
||||
type="number"
|
||||
step="1"
|
||||
pattern="[0-9]"
|
||||
{...register("supply")}
|
||||
/>
|
||||
<FormErrorMessage>{errors?.supply?.message}</FormErrorMessage>
|
||||
</FormControl>
|
||||
{!isErc721 && (
|
||||
<FormControl isRequired isInvalid={!!errors.supply}>
|
||||
<FormLabel>Initial Supply</FormLabel>
|
||||
<Input
|
||||
type="number"
|
||||
step="1"
|
||||
pattern="[0-9]"
|
||||
{...register("supply")}
|
||||
/>
|
||||
<FormErrorMessage>{errors?.supply?.message}</FormErrorMessage>
|
||||
</FormControl>
|
||||
)}
|
||||
<PropertiesFormControl
|
||||
watch={watch}
|
||||
// biome-ignore lint/suspicious/noExplicitAny: FIXME
|
||||
errors={errors as any}
|
||||
control={control}
|
||||
register={register}
|
||||
setValue={setValue}
|
||||
/>
|
||||
<Accordion
|
||||
allowToggle={!(errors.background_color || errors.external_url)}
|
||||
index={
|
||||
errors.background_color || errors.external_url ? [0] : undefined
|
||||
}
|
||||
>
|
||||
<AccordionItem>
|
||||
<AccordionButton px={0} justifyContent="space-between">
|
||||
<Heading size="subtitle.md">Advanced Options</Heading>
|
||||
<AccordionIcon />
|
||||
</AccordionButton>
|
||||
<AccordionPanel className="flex flex-col gap-6 px-0">
|
||||
<FormControl isInvalid={!!errors.background_color}>
|
||||
)}
|
||||
<PropertiesFormControl
|
||||
watch={watch}
|
||||
// biome-ignore lint/suspicious/noExplicitAny: FIXME
|
||||
errors={errors as any}
|
||||
control={control}
|
||||
register={register}
|
||||
setValue={setValue}
|
||||
/>
|
||||
<Accordion
|
||||
allowToggle={!(errors.background_color || errors.external_url)}
|
||||
index={
|
||||
errors.background_color || errors.external_url ? [0] : undefined
|
||||
}
|
||||
>
|
||||
<AccordionItem>
|
||||
<AccordionButton px={0} justifyContent="space-between">
|
||||
<Heading size="subtitle.md">Advanced Options</Heading>
|
||||
<AccordionIcon />
|
||||
</AccordionButton>
|
||||
<AccordionPanel className="flex flex-col gap-6 px-0">
|
||||
<FormControl isInvalid={!!errors.background_color}>
|
||||
<FormLabel>
|
||||
Background Color <OpenSeaPropertyBadge />
|
||||
</FormLabel>
|
||||
<Input max="6" {...register("background_color")} />
|
||||
<FormHelperText>
|
||||
Must be a six-character hexadecimal with a pre-pended #.
|
||||
</FormHelperText>
|
||||
<FormErrorMessage>
|
||||
{errors?.background_color?.message}
|
||||
</FormErrorMessage>
|
||||
</FormControl>
|
||||
{!externalIsTextFile && (
|
||||
<FormControl isInvalid={!!errors.external_url}>
|
||||
<FormLabel>
|
||||
Background Color <OpenSeaPropertyBadge />
|
||||
External URL <OpenSeaPropertyBadge />
|
||||
</FormLabel>
|
||||
<Input max="6" {...register("background_color")} />
|
||||
<Input {...register("external_url")} />
|
||||
<FormHelperText>
|
||||
Must be a six-character hexadecimal with a pre-pended #.
|
||||
This is the URL that will appear below the asset's
|
||||
image on OpenSea and will allow users to leave OpenSea and
|
||||
view the item on your site.
|
||||
</FormHelperText>
|
||||
<FormErrorMessage>
|
||||
{errors?.background_color?.message}
|
||||
{errors?.external_url?.message as unknown as string}
|
||||
</FormErrorMessage>
|
||||
</FormControl>
|
||||
{!externalIsTextFile && (
|
||||
<FormControl isInvalid={!!errors.external_url}>
|
||||
<FormLabel>
|
||||
External URL <OpenSeaPropertyBadge />
|
||||
</FormLabel>
|
||||
<Input {...register("external_url")} />
|
||||
<FormHelperText>
|
||||
This is the URL that will appear below the asset's
|
||||
image on OpenSea and will allow users to leave OpenSea and
|
||||
view the item on your site.
|
||||
</FormHelperText>
|
||||
<FormErrorMessage>
|
||||
{errors?.external_url?.message as unknown as string}
|
||||
</FormErrorMessage>
|
||||
</FormControl>
|
||||
)}
|
||||
<FormControl isInvalid={!!errors.customImage}>
|
||||
<FormLabel>Image URL</FormLabel>
|
||||
<Input max="6" {...register("customImage")} />
|
||||
<FormHelperText>
|
||||
If you already have your NFT image preuploaded, you can set
|
||||
the URL or URI here.
|
||||
</FormHelperText>
|
||||
<FormErrorMessage>
|
||||
{errors?.customImage?.message}
|
||||
</FormErrorMessage>
|
||||
</FormControl>
|
||||
<FormControl isInvalid={!!errors.customAnimationUrl}>
|
||||
<FormLabel>Animation URL</FormLabel>
|
||||
<Input max="6" {...register("customAnimationUrl")} />
|
||||
<FormHelperText>
|
||||
If you already have your NFT Animation URL preuploaded, you
|
||||
can set the URL or URI here.
|
||||
</FormHelperText>
|
||||
<FormErrorMessage>
|
||||
{errors?.customAnimationUrl?.message}
|
||||
</FormErrorMessage>
|
||||
</FormControl>
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
</form>
|
||||
</DrawerBody>
|
||||
<DrawerFooter>
|
||||
)}
|
||||
<FormControl isInvalid={!!errors.customImage}>
|
||||
<FormLabel>Image URL</FormLabel>
|
||||
<Input max="6" {...register("customImage")} />
|
||||
<FormHelperText>
|
||||
If you already have your NFT image preuploaded, you can set
|
||||
the URL or URI here.
|
||||
</FormHelperText>
|
||||
<FormErrorMessage>
|
||||
{errors?.customImage?.message}
|
||||
</FormErrorMessage>
|
||||
</FormControl>
|
||||
<FormControl isInvalid={!!errors.customAnimationUrl}>
|
||||
<FormLabel>Animation URL</FormLabel>
|
||||
<Input max="6" {...register("customAnimationUrl")} />
|
||||
<FormHelperText>
|
||||
If you already have your NFT Animation URL preuploaded, you
|
||||
can set the URL or URI here.
|
||||
</FormHelperText>
|
||||
<FormErrorMessage>
|
||||
{errors?.customAnimationUrl?.message}
|
||||
</FormErrorMessage>
|
||||
</FormControl>
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
</form>
|
||||
<div className="mt-8 flex flex-row justify-end gap-3">
|
||||
<Button
|
||||
isDisabled={isPending}
|
||||
isDisabled={sendAndConfirmTx.isPending}
|
||||
variant="outline"
|
||||
mr={3}
|
||||
onClick={modalContext.onClose}
|
||||
onClick={() => setOpen(false)}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<TransactionButton
|
||||
txChainID={contract.chain.id}
|
||||
transactionCount={1}
|
||||
isLoading={isPending}
|
||||
isLoading={sendAndConfirmTx.isPending}
|
||||
form={MINT_FORM_ID}
|
||||
type="submit"
|
||||
colorScheme="primary"
|
||||
@@ -357,7 +355,7 @@ export const NFTMintForm: React.FC<NFTMintForm> = ({ contract, isErc721 }) => {
|
||||
>
|
||||
Mint NFT
|
||||
</TransactionButton>
|
||||
</DrawerFooter>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
+2
-2
@@ -55,9 +55,9 @@ export const NFTRevealButton: React.FC<NFTRevealButtonProps> = ({
|
||||
Reveal NFTs
|
||||
</Button>
|
||||
</SheetTrigger>
|
||||
<SheetContent className="z-[10000] overflow-y-auto sm:w-[540px] sm:max-w-[90%] lg:w-[700px]">
|
||||
<SheetContent className="w-full overflow-y-auto sm:min-w-[540px] lg:min-w-[700px]">
|
||||
<SheetHeader>
|
||||
<SheetTitle>Reveal batch</SheetTitle>
|
||||
<SheetTitle className="text-left">Reveal batch</SheetTitle>
|
||||
</SheetHeader>
|
||||
<form
|
||||
className="mt-10 flex flex-col gap-6"
|
||||
|
||||
+5
-3
@@ -105,8 +105,9 @@ function Inner({
|
||||
// writes
|
||||
ERC721Ext.isSetClaimConditionsSupported(functionSelectors),
|
||||
ERC721Ext.isResetClaimEligibilitySupported(functionSelectors),
|
||||
isErc721,
|
||||
].every(Boolean);
|
||||
}, [functionSelectors]);
|
||||
}, [functionSelectors, isErc721]);
|
||||
|
||||
const hasERC20ClaimConditions = useMemo(() => {
|
||||
return [
|
||||
@@ -118,8 +119,9 @@ function Inner({
|
||||
// writes
|
||||
ERC20Ext.isSetClaimConditionsSupported(functionSelectors),
|
||||
ERC20Ext.isResetClaimEligibilitySupported(functionSelectors),
|
||||
isErc20,
|
||||
].every(Boolean);
|
||||
}, [functionSelectors]);
|
||||
}, [functionSelectors, isErc20]);
|
||||
|
||||
const claimConditions = useReadContract(
|
||||
isErc721 ? ERC721Ext.getClaimConditions : ERC20Ext.getClaimConditions,
|
||||
@@ -233,7 +235,7 @@ function Inner({
|
||||
(erc20Supply.data || 0n) > 0n,
|
||||
});
|
||||
}
|
||||
if (hasERC721ClaimConditions) {
|
||||
if (hasERC721ClaimConditions && isErc721) {
|
||||
steps.push({
|
||||
title: "First NFT claimed",
|
||||
children: (
|
||||
|
||||
+1
-1
@@ -43,7 +43,7 @@ export const TokenAirdropButton: React.FC<TokenAirdropButtonProps> = ({
|
||||
</SheetTrigger>
|
||||
<SheetContent className="w-full overflow-y-auto sm:min-w-[540px] lg:min-w-[700px]">
|
||||
<SheetHeader>
|
||||
<SheetTitle>Aidrop tokens</SheetTitle>
|
||||
<SheetTitle className="text-left">Airdrop tokens</SheetTitle>
|
||||
</SheetHeader>
|
||||
<TokenAirdropForm contract={contract} toggle={setOpen} />
|
||||
</SheetContent>
|
||||
|
||||
+1
-1
@@ -69,7 +69,7 @@ export const TokenBurnButton: React.FC<TokenBurnButtonProps> = ({
|
||||
</SheetTrigger>
|
||||
<SheetContent className="z-[10000]">
|
||||
<SheetHeader>
|
||||
<SheetTitle>Burn tokens</SheetTitle>
|
||||
<SheetTitle className="text-left">Burn tokens</SheetTitle>
|
||||
</SheetHeader>
|
||||
<form className="mt-10 flex flex-col gap-3">
|
||||
<div className="flex w-full flex-col gap-6 md:flex-row">
|
||||
|
||||
+1
-1
@@ -62,7 +62,7 @@ export const TokenClaimButton: React.FC<TokenClaimButtonProps> = ({
|
||||
</SheetTrigger>
|
||||
<SheetContent className="z-[10000]">
|
||||
<SheetHeader>
|
||||
<SheetTitle>Claim tokens</SheetTitle>
|
||||
<SheetTitle className="text-left">Claim tokens</SheetTitle>
|
||||
</SheetHeader>
|
||||
<form>
|
||||
<div className="mt-10 flex flex-col gap-6">
|
||||
|
||||
+3
-1
@@ -60,7 +60,9 @@ export const TokenMintButton: React.FC<TokenMintButtonProps> = ({
|
||||
</SheetTrigger>
|
||||
<SheetContent className="z-[10000]">
|
||||
<SheetHeader>
|
||||
<SheetTitle>Mint additional tokens</SheetTitle>
|
||||
<SheetTitle className="text-left">
|
||||
Mint additional tokens
|
||||
</SheetTitle>
|
||||
</SheetHeader>
|
||||
<form
|
||||
className="mt-10 flex flex-col gap-6"
|
||||
|
||||
+1
-1
@@ -67,7 +67,7 @@ export const TokenTransferButton: React.FC<TokenTransferButtonProps> = ({
|
||||
</SheetTrigger>
|
||||
<SheetContent className="z-[10000]">
|
||||
<SheetHeader>
|
||||
<SheetTitle>Transfer tokens</SheetTitle>
|
||||
<SheetTitle className="text-left">Transfer tokens</SheetTitle>
|
||||
</SheetHeader>
|
||||
<form className="mt-10">
|
||||
<div className="flex flex-col gap-6">
|
||||
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
import {
|
||||
type Range,
|
||||
getLastNDaysRange,
|
||||
} from "components/analytics/date-range-selector";
|
||||
import { RangeSelector } from "components/analytics/range-selector";
|
||||
import { getEcosystemWalletUsage } from "data/analytics/wallets/ecosystem";
|
||||
import { EcosystemWalletUsersChartCard } from "./EcosystemWalletUsersChartCard";
|
||||
|
||||
export async function EcosystemAnalyticsPage({
|
||||
ecosystemId,
|
||||
interval,
|
||||
range,
|
||||
}: { ecosystemId: string; interval: "day" | "week"; range?: Range }) {
|
||||
if (!range) {
|
||||
range = getLastNDaysRange("last-120");
|
||||
}
|
||||
|
||||
const stats = await getEcosystemWalletUsage({
|
||||
ecosystemId,
|
||||
from: range.from,
|
||||
to: range.to,
|
||||
period: interval,
|
||||
}).catch(() => null);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<RangeSelector range={range} interval={interval} />
|
||||
|
||||
<div className="h-6" />
|
||||
|
||||
<div className="flex flex-col gap-4 lg:gap-6">
|
||||
<EcosystemWalletUsersChartCard
|
||||
ecosystemWalletStats={stats || []}
|
||||
isPending={false}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+241
@@ -0,0 +1,241 @@
|
||||
"use client";
|
||||
import { ExportToCSVButton } from "@/components/blocks/ExportToCSVButton";
|
||||
import {
|
||||
type ChartConfig,
|
||||
ChartContainer,
|
||||
ChartLegend,
|
||||
ChartLegendContent,
|
||||
ChartTooltip,
|
||||
ChartTooltipContent,
|
||||
} from "@/components/ui/chart";
|
||||
import type { EcosystemWalletStats } from "@3rdweb-sdk/react/hooks/useApi";
|
||||
import {
|
||||
EmptyChartState,
|
||||
LoadingChartState,
|
||||
} from "components/analytics/empty-chart-state";
|
||||
import { ReactIcon } from "components/icons/brand-icons/ReactIcon";
|
||||
import { TypeScriptIcon } from "components/icons/brand-icons/TypeScriptIcon";
|
||||
import { UnityIcon } from "components/icons/brand-icons/UnityIcon";
|
||||
import { DocLink } from "components/shared/DocLink";
|
||||
import { format } from "date-fns";
|
||||
import { formatTickerNumber } from "lib/format-utils";
|
||||
import { useMemo } from "react";
|
||||
import { Bar, BarChart, CartesianGrid, XAxis, YAxis } from "recharts";
|
||||
|
||||
type ChartData = Record<string, number> & {
|
||||
time: string; // human readable date
|
||||
};
|
||||
const defaultLabel = "Unknown Auth";
|
||||
|
||||
export function EcosystemWalletUsersChartCard(props: {
|
||||
ecosystemWalletStats: EcosystemWalletStats[];
|
||||
isPending: boolean;
|
||||
}) {
|
||||
const { ecosystemWalletStats } = props;
|
||||
|
||||
const topChainsToShow = 10;
|
||||
|
||||
const { chartConfig, chartData } = useMemo(() => {
|
||||
const _chartConfig: ChartConfig = {};
|
||||
const _chartDataMap: Map<string, ChartData> = new Map();
|
||||
const authMethodToVolumeMap: Map<string, number> = new Map();
|
||||
// for each stat, add it in _chartDataMap
|
||||
for (const stat of ecosystemWalletStats) {
|
||||
const chartData = _chartDataMap.get(stat.date);
|
||||
const { authenticationMethod } = stat;
|
||||
|
||||
// if no data for current day - create new entry
|
||||
if (!chartData) {
|
||||
_chartDataMap.set(stat.date, {
|
||||
time: format(new Date(stat.date), "MMM dd"),
|
||||
[authenticationMethod || defaultLabel]: stat.uniqueWalletsConnected,
|
||||
} as ChartData);
|
||||
} else if (chartData) {
|
||||
chartData[authenticationMethod || defaultLabel] =
|
||||
(chartData[authenticationMethod || defaultLabel] || 0) +
|
||||
stat.uniqueWalletsConnected;
|
||||
}
|
||||
|
||||
authMethodToVolumeMap.set(
|
||||
authenticationMethod || defaultLabel,
|
||||
stat.uniqueWalletsConnected +
|
||||
(authMethodToVolumeMap.get(authenticationMethod || defaultLabel) ||
|
||||
0),
|
||||
);
|
||||
}
|
||||
|
||||
const authMethodsSorted = Array.from(authMethodToVolumeMap.entries())
|
||||
.sort((a, b) => b[1] - a[1])
|
||||
.map((w) => w[0]);
|
||||
|
||||
const authMethodsToShow = authMethodsSorted.slice(0, topChainsToShow);
|
||||
const authMethodsAsOther = authMethodsSorted.slice(topChainsToShow);
|
||||
|
||||
// replace chainIdsToTagAsOther chainId with "other"
|
||||
for (const data of _chartDataMap.values()) {
|
||||
for (const authMethod in data) {
|
||||
if (authMethodsAsOther.includes(authMethod)) {
|
||||
data.others = (data.others || 0) + (data[authMethod] || 0);
|
||||
delete data[authMethod];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
authMethodsToShow.forEach((walletType, i) => {
|
||||
_chartConfig[walletType] = {
|
||||
label: authMethodsToShow[i],
|
||||
color: `hsl(var(--chart-${(i % 10) + 1}))`,
|
||||
};
|
||||
});
|
||||
|
||||
if (authMethodsToShow.length > topChainsToShow) {
|
||||
// Add Other
|
||||
authMethodsToShow.push("others");
|
||||
_chartConfig.others = {
|
||||
label: "Others",
|
||||
color: "hsl(var(--muted-foreground))",
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
chartData: Array.from(_chartDataMap.values()).sort(
|
||||
(a, b) => new Date(a.time).getTime() - new Date(b.time).getTime(),
|
||||
),
|
||||
chartConfig: _chartConfig,
|
||||
};
|
||||
}, [ecosystemWalletStats]);
|
||||
|
||||
const uniqueAuthMethods = Object.keys(chartConfig);
|
||||
const disableActions =
|
||||
props.isPending ||
|
||||
chartData.length === 0 ||
|
||||
uniqueAuthMethods.every((authMethod) =>
|
||||
chartData.every((data) => data[authMethod] === 0),
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="relative w-full rounded-lg border border-border bg-muted/50 p-4 md:p-6">
|
||||
<h3 className="mb-1 font-semibold text-xl tracking-tight md:text-2xl">
|
||||
Unique Users
|
||||
</h3>
|
||||
<p className="mb-3 text-muted-foreground text-sm">
|
||||
The total number of active users in your ecosystem for each period.
|
||||
</p>
|
||||
|
||||
<div className="top-6 right-6 mb-4 grid grid-cols-2 items-center gap-2 md:absolute md:mb-0 md:flex">
|
||||
<ExportToCSVButton
|
||||
className="bg-background"
|
||||
fileName="Connect Wallets"
|
||||
disabled={disableActions}
|
||||
getData={async () => {
|
||||
// Shows the number of each type of wallet connected on all dates
|
||||
const header = ["Date", ...uniqueAuthMethods];
|
||||
const rows = chartData.map((data) => {
|
||||
const { time, ...rest } = data;
|
||||
return [
|
||||
time,
|
||||
...uniqueAuthMethods.map((w) => (rest[w] || 0).toString()),
|
||||
];
|
||||
});
|
||||
return { header, rows };
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Chart */}
|
||||
<ChartContainer
|
||||
config={chartConfig}
|
||||
className="h-[250px] w-full md:h-[350px]"
|
||||
>
|
||||
{props.isPending ? (
|
||||
<LoadingChartState />
|
||||
) : chartData.length === 0 ||
|
||||
uniqueAuthMethods.every((authMethod) =>
|
||||
chartData.every((data) => data[authMethod] === 0),
|
||||
) ? (
|
||||
<EmptyChartState>
|
||||
<div className="flex flex-col items-center justify-center">
|
||||
<span className="mb-6 text-lg">
|
||||
Connect users to your app with social logins
|
||||
</span>
|
||||
<div className="flex max-w-md flex-wrap items-center justify-center gap-x-6 gap-y-4">
|
||||
<DocLink
|
||||
link="https://portal.thirdweb.com/typescript/v5/ecosystemWallet"
|
||||
label="TypeScript"
|
||||
icon={TypeScriptIcon}
|
||||
/>
|
||||
<DocLink
|
||||
link="https://portal.thirdweb.com/react/v5/ecosystem-wallet/get-started"
|
||||
label="React"
|
||||
icon={ReactIcon}
|
||||
/>
|
||||
<DocLink
|
||||
link="https://portal.thirdweb.com/react/v5/ecosystem-wallet/get-started"
|
||||
label="React Native"
|
||||
icon={ReactIcon}
|
||||
/>
|
||||
<DocLink
|
||||
link="https://portal.thirdweb.com/unity/v5/wallets/ecosystem-wallet"
|
||||
label="Unity"
|
||||
icon={UnityIcon}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</EmptyChartState>
|
||||
) : (
|
||||
<BarChart
|
||||
accessibilityLayer
|
||||
data={chartData}
|
||||
margin={{
|
||||
top: 20,
|
||||
}}
|
||||
>
|
||||
<CartesianGrid vertical={false} />
|
||||
|
||||
<XAxis
|
||||
dataKey="time"
|
||||
tickLine={false}
|
||||
tickMargin={10}
|
||||
axisLine={false}
|
||||
/>
|
||||
|
||||
<YAxis
|
||||
dataKey={(data) =>
|
||||
Object.entries(data)
|
||||
.filter(([key]) => key !== "time")
|
||||
.map(([, value]) => value)
|
||||
.reduce((acc, current) => Number(acc) + Number(current), 0)
|
||||
}
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
tickFormatter={(value) => formatTickerNumber(value)}
|
||||
/>
|
||||
|
||||
<ChartTooltip
|
||||
cursor={true}
|
||||
content={
|
||||
<ChartTooltipContent
|
||||
valueFormatter={(value) => formatTickerNumber(Number(value))}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<ChartLegend content={<ChartLegendContent />} />
|
||||
{uniqueAuthMethods.map((authMethod) => {
|
||||
return (
|
||||
<Bar
|
||||
key={authMethod}
|
||||
dataKey={authMethod}
|
||||
fill={chartConfig[authMethod]?.color}
|
||||
radius={4}
|
||||
stackId="a"
|
||||
strokeWidth={1.5}
|
||||
className="stroke-muted"
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</BarChart>
|
||||
)}
|
||||
</ChartContainer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
import type { EcosystemWalletStats } from "@3rdweb-sdk/react/hooks/useApi";
|
||||
import { Stat } from "components/analytics/stat";
|
||||
import { ActivityIcon, UserIcon } from "lucide-react";
|
||||
|
||||
export function EcosystemWalletsSummary(props: {
|
||||
allTimeStats: EcosystemWalletStats[] | undefined;
|
||||
monthlyStats: EcosystemWalletStats[] | undefined;
|
||||
}) {
|
||||
const allTimeStats = props.allTimeStats?.reduce(
|
||||
(acc, curr) => {
|
||||
acc.uniqueWalletsConnected += curr.uniqueWalletsConnected;
|
||||
return acc;
|
||||
},
|
||||
{
|
||||
uniqueWalletsConnected: 0,
|
||||
},
|
||||
);
|
||||
|
||||
const monthlyStats = props.monthlyStats?.reduce(
|
||||
(acc, curr) => {
|
||||
acc.uniqueWalletsConnected += curr.uniqueWalletsConnected;
|
||||
return acc;
|
||||
},
|
||||
{
|
||||
uniqueWalletsConnected: 0,
|
||||
},
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-2 gap-4 lg:gap-6">
|
||||
<Stat
|
||||
label="Total Users"
|
||||
value={allTimeStats?.uniqueWalletsConnected || 0}
|
||||
icon={ActivityIcon}
|
||||
/>
|
||||
<Stat
|
||||
label="Monthly Active Users"
|
||||
value={monthlyStats?.uniqueWalletsConnected || 0}
|
||||
icon={UserIcon}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
import type { Range } from "components/analytics/date-range-selector";
|
||||
import { fetchApiServer } from "data/analytics/fetch-api-server";
|
||||
import { FetchError } from "utils/error";
|
||||
import type { Ecosystem } from "../../../types";
|
||||
import { EcosystemAnalyticsPage } from "./components/EcosystemAnalyticsPage";
|
||||
|
||||
export default async function Page({
|
||||
params,
|
||||
searchParams,
|
||||
}: {
|
||||
params: { slug: string };
|
||||
searchParams: {
|
||||
interval?: "day" | "week";
|
||||
range?: Range;
|
||||
};
|
||||
}) {
|
||||
const ecosystem = await getEcosystem(params.slug);
|
||||
|
||||
return (
|
||||
<EcosystemAnalyticsPage
|
||||
ecosystemId={ecosystem.id}
|
||||
interval={searchParams.interval || "week"}
|
||||
range={searchParams.range}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
async function getEcosystem(ecosystemSlug: string) {
|
||||
const res = await fetchApiServer(`v1/ecosystem-wallet/${ecosystemSlug}`);
|
||||
|
||||
if (!res.ok) {
|
||||
const data = await res.json();
|
||||
console.error(data);
|
||||
throw new FetchError(
|
||||
res,
|
||||
data?.message ?? data?.error?.message ?? "Failed to fetch ecosystems",
|
||||
);
|
||||
}
|
||||
|
||||
const data = (await res.json()) as { result: Ecosystem };
|
||||
return data.result;
|
||||
}
|
||||
+24
-2
@@ -1,9 +1,10 @@
|
||||
import { COOKIE_ACTIVE_ACCOUNT, COOKIE_PREFIX_TOKEN } from "@/constants/cookie";
|
||||
import { getEcosystemWalletUsage } from "data/analytics/wallets/ecosystem";
|
||||
import { cookies } from "next/headers";
|
||||
import { redirect } from "next/navigation";
|
||||
import { getAddress } from "thirdweb";
|
||||
import { fetchEcosystem } from "../../utils/fetchEcosystem";
|
||||
import { EcosystemHeader } from "./components/client/ecosystem-header.client";
|
||||
import { fetchEcosystem } from "../../../utils/fetchEcosystem";
|
||||
import { EcosystemHeader } from "./ecosystem-header.client";
|
||||
|
||||
export async function EcosystemLayoutSlug({
|
||||
children,
|
||||
@@ -30,11 +31,32 @@ export async function EcosystemLayoutSlug({
|
||||
redirect(ecosystemLayoutPath);
|
||||
}
|
||||
|
||||
const allTimeStatsPromise = getEcosystemWalletUsage({
|
||||
ecosystemId: ecosystem.id,
|
||||
from: new Date(2022, 0, 1),
|
||||
to: new Date(),
|
||||
period: "all",
|
||||
});
|
||||
|
||||
const monthlyStatsPromise = getEcosystemWalletUsage({
|
||||
ecosystemId: ecosystem.id,
|
||||
from: new Date(new Date().getFullYear(), new Date().getMonth(), 1),
|
||||
to: new Date(),
|
||||
period: "month",
|
||||
});
|
||||
|
||||
const [allTimeStats, monthlyStats] = await Promise.all([
|
||||
allTimeStatsPromise,
|
||||
monthlyStatsPromise,
|
||||
]);
|
||||
|
||||
return (
|
||||
<div className="flex w-full flex-col gap-10">
|
||||
<EcosystemHeader
|
||||
ecosystem={ecosystem}
|
||||
ecosystemLayoutPath={ecosystemLayoutPath}
|
||||
allTimeStats={allTimeStats || []}
|
||||
monthlyStats={monthlyStats || []}
|
||||
/>
|
||||
{children}
|
||||
</div>
|
||||
+34
-16
@@ -15,6 +15,7 @@ import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { TabLinks } from "@/components/ui/tabs";
|
||||
import { useThirdwebClient } from "@/constants/thirdweb.client";
|
||||
import { resolveSchemeWithErrorHandler } from "@/lib/resolveSchemeWithErrorHandler";
|
||||
import type { EcosystemWalletStats } from "@3rdweb-sdk/react/hooks/useApi";
|
||||
import {
|
||||
AlertTriangleIcon,
|
||||
CheckIcon,
|
||||
@@ -24,9 +25,10 @@ import {
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { useEcosystemList } from "../../../../hooks/use-ecosystem-list";
|
||||
import type { Ecosystem } from "../../../../types";
|
||||
import { useEcosystem } from "../../hooks/use-ecosystem";
|
||||
import { useEcosystemList } from "../../../hooks/use-ecosystem-list";
|
||||
import type { Ecosystem } from "../../../types";
|
||||
import { EcosystemWalletsSummary } from "../analytics/components/Summary";
|
||||
import { useEcosystem } from "../hooks/use-ecosystem";
|
||||
|
||||
function EcosystemAlertBanner({ ecosystem }: { ecosystem: Ecosystem }) {
|
||||
switch (ecosystem.status) {
|
||||
@@ -110,6 +112,8 @@ function EcosystemSelect(props: {
|
||||
export function EcosystemHeader(props: {
|
||||
ecosystem: Ecosystem;
|
||||
ecosystemLayoutPath: string;
|
||||
allTimeStats: EcosystemWalletStats[];
|
||||
monthlyStats: EcosystemWalletStats[];
|
||||
}) {
|
||||
const pathname = usePathname();
|
||||
const { data: fetchedEcosystem } = useEcosystem({
|
||||
@@ -165,11 +169,23 @@ export function EcosystemHeader(props: {
|
||||
{!ecosystem.slug ? (
|
||||
<Skeleton className="h-6 w-[300px]" />
|
||||
) : (
|
||||
<div className="flex items-center gap-1">
|
||||
<p className="text-muted-foreground">
|
||||
ecosystem.{ecosystem.slug}
|
||||
</p>
|
||||
<CopyButton text={`ecosystem.${ecosystem.slug}`} />
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="flex items-center gap-1">
|
||||
<p className="text-muted-foreground">
|
||||
ecosystem.{ecosystem.slug}
|
||||
</p>
|
||||
<CopyButton text={`ecosystem.${ecosystem.slug}`} />
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<a
|
||||
href={`https://${ecosystem.slug}.ecosystem.thirdweb.com`}
|
||||
className="text-link-foreground"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
{`${ecosystem.slug}.ecosystem.thirdweb.com`}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -181,19 +197,21 @@ export function EcosystemHeader(props: {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<EcosystemWalletsSummary
|
||||
allTimeStats={props.allTimeStats}
|
||||
monthlyStats={props.monthlyStats}
|
||||
/>
|
||||
<TabLinks
|
||||
links={[
|
||||
{
|
||||
name: "Permissions",
|
||||
href: `${props.ecosystemLayoutPath}/${ecosystem.slug}`,
|
||||
isActive:
|
||||
pathname === `${props.ecosystemLayoutPath}/${ecosystem.slug}`,
|
||||
name: "Analytics",
|
||||
href: `${props.ecosystemLayoutPath}/${ecosystem.slug}/analytics`,
|
||||
isActive: !!pathname && pathname.endsWith("/analytics"),
|
||||
},
|
||||
{
|
||||
name: "Analytics (Coming Soon)",
|
||||
href: "#",
|
||||
isActive: false,
|
||||
isDisabled: true,
|
||||
name: "Configuration",
|
||||
href: `${props.ecosystemLayoutPath}/${ecosystem.slug}/configuration`,
|
||||
isActive: !!pathname && pathname.endsWith("/configuration"),
|
||||
},
|
||||
{
|
||||
name: "Design (Coming Soon)",
|
||||
+1
-1
@@ -10,7 +10,7 @@ import {
|
||||
} from "@/components/ui/dialog";
|
||||
import { PlusIcon } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import type { Ecosystem } from "../../../../types";
|
||||
import type { Ecosystem } from "../../../../../types";
|
||||
import { AddPartnerForm } from "./add-partner-form.client";
|
||||
|
||||
export function AddPartnerDialogButton(props: {
|
||||
+4
-4
@@ -1,8 +1,8 @@
|
||||
"use client";
|
||||
import { AuthOptionsSection } from "./components/server/auth-options-section";
|
||||
import { EcosystemPartnersSection } from "./components/server/ecosystem-partners-section";
|
||||
import { IntegrationPermissionsSection } from "./components/server/integration-permissions-section";
|
||||
import { useEcosystem } from "./hooks/use-ecosystem";
|
||||
import { useEcosystem } from "../../../hooks/use-ecosystem";
|
||||
import { AuthOptionsSection } from "../server/auth-options-section";
|
||||
import { EcosystemPartnersSection } from "../server/ecosystem-partners-section";
|
||||
import { IntegrationPermissionsSection } from "../server/integration-permissions-section";
|
||||
|
||||
export function EcosystemPermissionsPage({
|
||||
params,
|
||||
+1
-2
@@ -1,4 +1,3 @@
|
||||
"use client";
|
||||
import { Spinner } from "@/components/ui/Spinner/Spinner";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
@@ -16,7 +15,7 @@ import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import type { z } from "zod";
|
||||
import type { Ecosystem } from "../../../../types";
|
||||
import type { Ecosystem } from "../../../../../types";
|
||||
import { partnerFormSchema } from "../../constants";
|
||||
import { useAddPartner } from "../../hooks/use-add-partner";
|
||||
|
||||
+1
-1
@@ -34,7 +34,7 @@ import {
|
||||
} from "thirdweb/wallets/smart";
|
||||
import invariant from "tiny-invariant";
|
||||
import { z } from "zod";
|
||||
import { type Ecosystem, authOptions } from "../../../../types";
|
||||
import { type Ecosystem, authOptions } from "../../../../../types";
|
||||
import { useUpdateEcosystem } from "../../hooks/use-update-ecosystem";
|
||||
|
||||
type AuthOptionsFormData = {
|
||||
+1
-1
@@ -6,7 +6,7 @@ import { cn } from "@/lib/utils";
|
||||
import { useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import invariant from "tiny-invariant";
|
||||
import type { Ecosystem } from "../../../../types";
|
||||
import type { Ecosystem } from "../../../../../types";
|
||||
import { useUpdateEcosystem } from "../../hooks/use-update-ecosystem";
|
||||
|
||||
export function IntegrationPermissionsToggle({
|
||||
+1
-1
@@ -16,7 +16,7 @@ import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import type { z } from "zod";
|
||||
import type { Ecosystem, Partner } from "../../../../types";
|
||||
import type { Ecosystem, Partner } from "../../../../../types";
|
||||
import { partnerFormSchema } from "../../constants";
|
||||
import { useUpdatePartner } from "../../hooks/use-update-partner";
|
||||
|
||||
+1
-1
@@ -7,7 +7,7 @@ import {
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import { useState } from "react";
|
||||
import type { Ecosystem, Partner } from "../../../../types";
|
||||
import type { Ecosystem, Partner } from "../../../../../types";
|
||||
import { UpdatePartnerForm } from "./update-partner-form.client";
|
||||
|
||||
export function UpdatePartnerModal({
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import type { Ecosystem } from "../../../../types";
|
||||
import type { Ecosystem } from "../../../../../types";
|
||||
import {
|
||||
AuthOptionsForm,
|
||||
AuthOptionsFormSkeleton,
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import type { Ecosystem } from "../../../../types";
|
||||
import type { Ecosystem } from "../../../../../types";
|
||||
import { AddPartnerDialogButton } from "../client/AddPartnerDialogButton";
|
||||
import { PartnersTable } from "./partners-table";
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import type { Ecosystem } from "../../../../types";
|
||||
import type { Ecosystem } from "../../../../../types";
|
||||
import {
|
||||
IntegrationPermissionsToggle,
|
||||
IntegrationPermissionsToggleSkeleton,
|
||||
+2
-2
@@ -16,9 +16,9 @@ import { cn } from "@/lib/utils";
|
||||
import { Pencil, Trash2 } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { toast } from "sonner";
|
||||
import type { Ecosystem, Partner } from "../../../../types";
|
||||
import type { Ecosystem, Partner } from "../../../../../types";
|
||||
import { usePartners } from "../../../hooks/use-partners";
|
||||
import { useDeletePartner } from "../../hooks/use-delete-partner";
|
||||
import { usePartners } from "../../hooks/use-partners";
|
||||
import { UpdatePartnerModal } from "../client/update-partner-modal.client";
|
||||
|
||||
export function PartnersTable({ ecosystem }: { ecosystem: Ecosystem }) {
|
||||
+1
-1
@@ -4,7 +4,7 @@ import {
|
||||
useMutation,
|
||||
useQueryClient,
|
||||
} from "@tanstack/react-query";
|
||||
import type { Ecosystem, Partner } from "../../../types";
|
||||
import type { Ecosystem, Partner } from "../../../../types";
|
||||
|
||||
type AddPartnerParams = {
|
||||
ecosystem: Ecosystem;
|
||||
+1
-1
@@ -4,7 +4,7 @@ import {
|
||||
useMutation,
|
||||
useQueryClient,
|
||||
} from "@tanstack/react-query";
|
||||
import type { Ecosystem } from "../../../types";
|
||||
import type { Ecosystem } from "../../../../types";
|
||||
|
||||
type DeletePartnerParams = {
|
||||
ecosystem: Ecosystem;
|
||||
+1
-1
@@ -4,7 +4,7 @@ import {
|
||||
useMutation,
|
||||
useQueryClient,
|
||||
} from "@tanstack/react-query";
|
||||
import type { Ecosystem } from "../../../types";
|
||||
import type { Ecosystem } from "../../../../types";
|
||||
|
||||
export function useUpdateEcosystem(
|
||||
options?: Omit<UseMutationOptions<boolean, unknown, Ecosystem>, "mutationFn">,
|
||||
+1
-1
@@ -4,7 +4,7 @@ import {
|
||||
useMutation,
|
||||
useQueryClient,
|
||||
} from "@tanstack/react-query";
|
||||
import type { Ecosystem, Partner } from "../../../types";
|
||||
import type { Ecosystem, Partner } from "../../../../types";
|
||||
|
||||
type UpdatePartnerParams = {
|
||||
partnerId: string;
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
import { EcosystemPermissionsPage } from "./components/client/EcosystemPermissionsPage";
|
||||
|
||||
export default function Page({ params }: { params: { slug: string } }) {
|
||||
return <EcosystemPermissionsPage params={params} />;
|
||||
}
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import { EcosystemLayoutSlug } from "./EcosystemSlugLayout";
|
||||
import { EcosystemLayoutSlug } from "./components/EcosystemSlugLayout";
|
||||
|
||||
export default async function Layout({
|
||||
children,
|
||||
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
import { EcosystemPermissionsPage } from "./EcosystemPermissionsPage";
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
export default function Page({ params }: { params: { slug: string } }) {
|
||||
return <EcosystemPermissionsPage params={params} />;
|
||||
redirect(`/dashboard/connect/ecosystem/${params.slug}/analytics`);
|
||||
}
|
||||
|
||||
+2
-1
@@ -1,5 +1,6 @@
|
||||
import { InAppWalletsSummary } from "components/embedded-wallets/Analytics/Summary";
|
||||
import { getInAppWalletUsage } from "data/analytics/wallets/in-app";
|
||||
import { subDays } from "date-fns";
|
||||
import { redirect } from "next/navigation";
|
||||
import { getAuthToken } from "../../../../../api/lib/getAuthToken";
|
||||
import { InAppWaletFooterSection } from "../../../../../team/[team_slug]/[project_slug]/connect/in-app-wallets/_components/footer";
|
||||
@@ -42,7 +43,7 @@ export default async function Page(props: {
|
||||
|
||||
const monthlyStatsPromise = getInAppWalletUsage({
|
||||
clientId,
|
||||
from: new Date(new Date().getFullYear(), new Date().getMonth(), 1),
|
||||
from: subDays(new Date(), 30),
|
||||
to: new Date(),
|
||||
period: "month",
|
||||
});
|
||||
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
import type { Ecosystem } from "app/(dashboard)/dashboard/connect/ecosystem/types";
|
||||
import type { Range } from "components/analytics/date-range-selector";
|
||||
import { fetchApiServer } from "data/analytics/fetch-api-server";
|
||||
import { FetchError } from "utils/error";
|
||||
import { EcosystemAnalyticsPage } from "../../../../../../../../(dashboard)/dashboard/connect/ecosystem/[slug]/(active)/analytics/components/EcosystemAnalyticsPage";
|
||||
|
||||
export default async function Page({
|
||||
params,
|
||||
searchParams,
|
||||
}: {
|
||||
params: {
|
||||
slug: string;
|
||||
team_slug: string;
|
||||
project_slug: string;
|
||||
};
|
||||
searchParams: {
|
||||
interval?: "day" | "week";
|
||||
range?: Range;
|
||||
};
|
||||
}) {
|
||||
const ecosystem = await getEcosystem(params.slug);
|
||||
return (
|
||||
<EcosystemAnalyticsPage
|
||||
ecosystemId={ecosystem.id}
|
||||
interval={searchParams.interval || "week"}
|
||||
range={searchParams.range}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
async function getEcosystem(ecosystemSlug: string) {
|
||||
const res = await fetchApiServer(`/v1/ecosystem-wallet/${ecosystemSlug}`);
|
||||
|
||||
if (!res.ok) {
|
||||
const data = await res.json();
|
||||
console.error(data);
|
||||
throw new FetchError(
|
||||
res,
|
||||
data?.message ?? data?.error?.message ?? "Failed to fetch ecosystems",
|
||||
);
|
||||
}
|
||||
|
||||
const data = (await res.json()) as { result: Ecosystem };
|
||||
return data.result;
|
||||
}
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
import { EcosystemPermissionsPage } from "../../../../../../../../(dashboard)/dashboard/connect/ecosystem/[slug]/(active)/configuration/components/client/EcosystemPermissionsPage";
|
||||
|
||||
export default function Page({ params }: { params: { slug: string } }) {
|
||||
return <EcosystemPermissionsPage params={params} />;
|
||||
}
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import { EcosystemLayoutSlug } from "../../../../../../../(dashboard)/dashboard/connect/ecosystem/[slug]/(active)/EcosystemSlugLayout";
|
||||
import { EcosystemLayoutSlug } from "../../../../../../../(dashboard)/dashboard/connect/ecosystem/[slug]/(active)/components/EcosystemSlugLayout";
|
||||
|
||||
export default async function Layout(props: {
|
||||
params: { team_slug: string; project_slug: string; slug: string };
|
||||
|
||||
+7
-3
@@ -1,5 +1,9 @@
|
||||
import { EcosystemPermissionsPage } from "../../../../../../../(dashboard)/dashboard/connect/ecosystem/[slug]/(active)/EcosystemPermissionsPage";
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
export default function Page({ params }: { params: { slug: string } }) {
|
||||
return <EcosystemPermissionsPage params={params} />;
|
||||
export default function Page({
|
||||
params,
|
||||
}: { params: { team_slug: string; project_slug: string; slug: string } }) {
|
||||
redirect(
|
||||
`/team/${params.team_slug}/${params.project_slug}/connect/ecosystem/${params.slug}/analytics`,
|
||||
);
|
||||
}
|
||||
|
||||
+2
-1
@@ -1,6 +1,7 @@
|
||||
import { TrackedLinkTW } from "@/components/ui/tracked-link";
|
||||
import { InAppWalletsSummary } from "components/embedded-wallets/Analytics/Summary";
|
||||
import { getInAppWalletUsage } from "data/analytics/wallets/in-app";
|
||||
import { subDays } from "date-fns";
|
||||
import { TRACKING_CATEGORY } from "../_constants";
|
||||
|
||||
export async function InAppWalletsHeader({ clientId }: { clientId: string }) {
|
||||
@@ -13,7 +14,7 @@ export async function InAppWalletsHeader({ clientId }: { clientId: string }) {
|
||||
|
||||
const monthlyStatsPromise = getInAppWalletUsage({
|
||||
clientId,
|
||||
from: new Date(new Date().getFullYear(), new Date().getMonth(), 1),
|
||||
from: subDays(new Date(), 30),
|
||||
to: new Date(),
|
||||
period: "month",
|
||||
});
|
||||
|
||||
+15
-11
@@ -26,6 +26,7 @@ import { formatTickerNumber } from "../../../lib/format-utils";
|
||||
type ChartData = Record<string, number> & {
|
||||
time: string; // human readable date
|
||||
};
|
||||
const defaultLabel = "Unknown Auth";
|
||||
|
||||
export function InAppWalletUsersChartCard(props: {
|
||||
inAppWalletStats: InAppWalletStats[];
|
||||
@@ -47,18 +48,19 @@ export function InAppWalletUsersChartCard(props: {
|
||||
if (!chartData && stat.uniqueWalletsConnected > 0) {
|
||||
_chartDataMap.set(stat.date, {
|
||||
time: format(new Date(stat.date), "MMM dd"),
|
||||
[authenticationMethod || "Unknown"]: stat.uniqueWalletsConnected,
|
||||
[authenticationMethod || defaultLabel]: stat.uniqueWalletsConnected,
|
||||
} as ChartData);
|
||||
} else if (chartData) {
|
||||
chartData[authenticationMethod || "Unknown"] =
|
||||
(chartData[authenticationMethod || "Unknown"] || 0) +
|
||||
chartData[authenticationMethod || defaultLabel] =
|
||||
(chartData[authenticationMethod || defaultLabel] || 0) +
|
||||
stat.uniqueWalletsConnected;
|
||||
}
|
||||
|
||||
authMethodToVolumeMap.set(
|
||||
authenticationMethod || "Unknown",
|
||||
authenticationMethod || defaultLabel,
|
||||
stat.uniqueWalletsConnected +
|
||||
(authMethodToVolumeMap.get(authenticationMethod || "Unknown") || 0),
|
||||
(authMethodToVolumeMap.get(authenticationMethod || defaultLabel) ||
|
||||
0),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -86,12 +88,14 @@ export function InAppWalletUsersChartCard(props: {
|
||||
};
|
||||
});
|
||||
|
||||
// Add Other
|
||||
authMethodsToShow.push("others");
|
||||
_chartConfig.others = {
|
||||
label: "Others",
|
||||
color: "hsl(var(--muted-foreground))",
|
||||
};
|
||||
if (authMethodsToShow.length > topChainsToShow) {
|
||||
// Add Other
|
||||
authMethodsToShow.push("others");
|
||||
_chartConfig.others = {
|
||||
label: "Others",
|
||||
color: "hsl(var(--muted-foreground))",
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
chartData: Array.from(_chartDataMap.values()).sort(
|
||||
|
||||
@@ -3,8 +3,8 @@ import {
|
||||
getLastNDaysRange,
|
||||
} from "components/analytics/date-range-selector";
|
||||
import { getInAppWalletUsage } from "data/analytics/wallets/in-app";
|
||||
import { RangeSelector } from "../../analytics/range-selector";
|
||||
import { InAppWalletUsersChartCard } from "./InAppWalletUsersChartCard";
|
||||
import { RangeSelector } from "./RangeSelector";
|
||||
|
||||
export async function InAppWalletAnalytics({
|
||||
clientId,
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
import { TrackedLink } from "tw-components";
|
||||
import styles from "./opensource.module.css";
|
||||
|
||||
const thirdwebRepoUrl = "https://github.com/thirdweb-dev";
|
||||
|
||||
const GithubIcon = () => {
|
||||
return (
|
||||
<svg
|
||||
width="21"
|
||||
height="21"
|
||||
viewBox="0 0 21 21"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={styles.githubIcon}
|
||||
>
|
||||
<title>GitHub</title>
|
||||
<g id="github-mark-white 1" clipPath="url(#clip0_3418_4084)">
|
||||
<path
|
||||
id="Vector"
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M10.3538 0.537842C4.62843 0.537842 0 5.20039 0 10.9686C0 15.5794 2.96559 19.4824 7.07966 20.8638C7.59402 20.9676 7.78243 20.6394 7.78243 20.3632C7.78243 20.1214 7.76547 19.2925 7.76547 18.4289C4.88529 19.0507 4.28552 17.1855 4.28552 17.1855C3.82265 15.9766 3.13684 15.6659 3.13684 15.6659C2.19415 15.0269 3.2055 15.0269 3.2055 15.0269C4.25118 15.096 4.79988 16.0976 4.79988 16.0976C5.7254 17.6863 7.21678 17.2374 7.81676 16.961C7.90238 16.2875 8.17684 15.8212 8.46825 15.5623C6.17109 15.3204 3.7542 14.4225 3.7542 10.4159C3.7542 9.27609 4.16535 8.34358 4.81684 7.61834C4.71405 7.35936 4.35397 6.28846 4.91984 4.85515C4.91984 4.85515 5.79406 4.57879 7.76526 5.92584C8.6092 5.69752 9.47954 5.58137 10.3538 5.58039C11.2281 5.58039 12.1192 5.7014 12.9422 5.92584C14.9136 4.57879 15.7878 4.85515 15.7878 4.85515C16.3537 6.28846 15.9934 7.35936 15.8906 7.61834C16.5592 8.34358 16.9534 9.27609 16.9534 10.4159C16.9534 14.4225 14.5366 15.3031 12.2222 15.5623C12.5995 15.8903 12.925 16.5119 12.925 17.4964C12.925 18.8951 12.9081 20.0178 12.9081 20.363C12.9081 20.6394 13.0967 20.9676 13.6108 20.864C17.7249 19.4822 20.6905 15.5794 20.6905 10.9686C20.7074 5.20039 16.0621 0.537842 10.3538 0.537842Z"
|
||||
fill="white"
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_3418_4084">
|
||||
<rect
|
||||
width="20.7695"
|
||||
height="20.3457"
|
||||
fill="white"
|
||||
transform="translate(0 0.537842)"
|
||||
/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
type OpenSourceProps = {
|
||||
TRACKING_CATEGORY: string;
|
||||
};
|
||||
|
||||
export const OpenSource: React.FC<OpenSourceProps> = ({
|
||||
TRACKING_CATEGORY,
|
||||
}) => {
|
||||
return (
|
||||
<TrackedLink
|
||||
label="open-source"
|
||||
category={TRACKING_CATEGORY}
|
||||
href={thirdwebRepoUrl}
|
||||
isExternal
|
||||
>
|
||||
<span className={styles.animatedGradient}>
|
||||
open-source <GithubIcon />
|
||||
</span>
|
||||
</TrackedLink>
|
||||
);
|
||||
};
|
||||
@@ -1,57 +0,0 @@
|
||||
@keyframes slideGradient {
|
||||
from {
|
||||
background-position: 0% 50%;
|
||||
}
|
||||
to {
|
||||
background-position: -200% 50%;
|
||||
}
|
||||
}
|
||||
|
||||
.animatedGradient {
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
display: inline-block; /* Use inline-block for better control */
|
||||
background: linear-gradient(88deg, #fff 0%, #fff 50%, #9786df 60%, #fff 90%);
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
color: transparent;
|
||||
background-size: 200% 100%;
|
||||
animation: slideGradient 4s linear infinite;
|
||||
}
|
||||
|
||||
.animatedGradient:hover {
|
||||
background: none;
|
||||
color: #9786df;
|
||||
}
|
||||
|
||||
.githubIcon {
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
right: -24px;
|
||||
}
|
||||
|
||||
.animatedGradient:hover > .githubIcon > g > path {
|
||||
fill: #9786df;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1166px) {
|
||||
.githubIcon {
|
||||
top: -10px;
|
||||
right: -12px;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 611px) {
|
||||
.githubIcon {
|
||||
top: -10px;
|
||||
right: -12px;
|
||||
}
|
||||
}
|
||||
@media screen and (max-width: 479px) {
|
||||
.githubIcon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
top: -8px;
|
||||
right: -8px;
|
||||
}
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
import { Box, Container, Flex, SimpleGrid } from "@chakra-ui/react";
|
||||
import { EngineTierCard } from "components/engine/create/tier-card";
|
||||
import { useTrack } from "hooks/analytics/useTrack";
|
||||
import { useRouter } from "next/router";
|
||||
import { Heading } from "tw-components";
|
||||
|
||||
interface PricingSectionProps {
|
||||
trackingCategory: string;
|
||||
}
|
||||
|
||||
export const PricingEngineHomepage: React.FC<PricingSectionProps> = ({
|
||||
trackingCategory,
|
||||
}) => {
|
||||
const track = useTrack();
|
||||
const router = useRouter();
|
||||
return (
|
||||
<Container maxW="6xl">
|
||||
<Flex flexDir="column" pt={{ base: 8, lg: 24 }} gap={{ base: 8, md: 16 }}>
|
||||
<Flex flexDir="column" gap={6}>
|
||||
<Heading
|
||||
as="h2"
|
||||
size="display.sm"
|
||||
textAlign="center"
|
||||
px={{ base: 2, md: 0 }}
|
||||
>
|
||||
Simple, transparent & flexible{" "}
|
||||
<Box bgGradient="linear(to-r, #4DABEE, #692AC1)" bgClip="text">
|
||||
pricing for every team.
|
||||
</Box>
|
||||
</Heading>
|
||||
</Flex>
|
||||
<SimpleGrid columns={{ base: 1, lg: 3 }} gap={6}>
|
||||
<EngineTierCard
|
||||
tier="STARTER"
|
||||
ctaText="Get Started"
|
||||
onClick={() => {
|
||||
track({
|
||||
category: trackingCategory,
|
||||
action: "click",
|
||||
label: "clicked-cloud-hosted",
|
||||
tier: "STANDARD",
|
||||
});
|
||||
router.push("/dashboard/engine");
|
||||
}}
|
||||
/>
|
||||
|
||||
<EngineTierCard
|
||||
tier="PREMIUM"
|
||||
isPrimaryCta
|
||||
ctaText="Get Started"
|
||||
onClick={() => {
|
||||
track({
|
||||
category: trackingCategory,
|
||||
action: "click",
|
||||
label: "clicked-cloud-hosted",
|
||||
tier: "PREMIUM",
|
||||
});
|
||||
router.push("/dashboard/engine");
|
||||
}}
|
||||
/>
|
||||
|
||||
<EngineTierCard
|
||||
tier="ENTERPRISE"
|
||||
previousTier="Premium Engine"
|
||||
onClick={() => {
|
||||
track({
|
||||
category: trackingCategory,
|
||||
action: "click",
|
||||
label: "clicked-cloud-hosted",
|
||||
tier: "ENTERPRISE",
|
||||
});
|
||||
router.push("/contact-us");
|
||||
}}
|
||||
/>
|
||||
</SimpleGrid>
|
||||
</Flex>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
@@ -1,51 +0,0 @@
|
||||
import { Flex, type FlexProps } from "@chakra-ui/react";
|
||||
import type { StaticImageData } from "next/image";
|
||||
import { Text } from "tw-components";
|
||||
import { LandingDesktopMobileImage } from "./desktop-mobile-image";
|
||||
|
||||
type LandingImageSectionItemProps = {
|
||||
image: StaticImageData;
|
||||
mobileImage: StaticImageData;
|
||||
title: string;
|
||||
description: string;
|
||||
maxHeightImage?: FlexProps["maxHeight"];
|
||||
} & FlexProps;
|
||||
|
||||
const LandingImageSectionItem = ({
|
||||
mobileImage,
|
||||
image,
|
||||
title,
|
||||
description,
|
||||
maxHeightImage,
|
||||
...rest
|
||||
}: LandingImageSectionItemProps) => {
|
||||
return (
|
||||
<Flex flexDir="column" gap={6}>
|
||||
<Flex
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
borderRadius="12px"
|
||||
background="#131418"
|
||||
minH={{ base: "auto", md: "333px" }}
|
||||
{...rest}
|
||||
>
|
||||
<LandingDesktopMobileImage
|
||||
image={image}
|
||||
mobileImage={mobileImage}
|
||||
w="100%"
|
||||
maxHeight={maxHeightImage}
|
||||
alt=""
|
||||
/>
|
||||
</Flex>
|
||||
<Flex flexDir="column" gap={4}>
|
||||
<Text size="body.xl" color="white" fontWeight="bold">
|
||||
{title}
|
||||
</Text>
|
||||
|
||||
{description && <Text size="body.lg">{description}</Text>}
|
||||
</Flex>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default LandingImageSectionItem;
|
||||
@@ -1,89 +0,0 @@
|
||||
import { Box, Container, Flex } from "@chakra-ui/react";
|
||||
import type { StaticImageData } from "next/image";
|
||||
import { Heading, Text } from "tw-components";
|
||||
import { LandingCTAButtons } from "./cta-buttons";
|
||||
import { LandingDesktopMobileImage } from "./desktop-mobile-image";
|
||||
|
||||
interface SplashImageProps {
|
||||
title: string;
|
||||
titleWithGradient: string;
|
||||
subtitle: string;
|
||||
trackingCategory: string;
|
||||
ctaText?: string;
|
||||
ctaLink?: string;
|
||||
// biome-ignore lint/complexity/noBannedTypes: FIXME
|
||||
lottie?: {};
|
||||
gradient: string;
|
||||
image?: StaticImageData;
|
||||
mobileImage?: StaticImageData;
|
||||
noCta?: boolean;
|
||||
contactUsTitle?: string;
|
||||
contactUsLink?: string;
|
||||
}
|
||||
|
||||
export const SplashImage: React.FC<SplashImageProps> = ({
|
||||
title,
|
||||
titleWithGradient,
|
||||
subtitle,
|
||||
trackingCategory,
|
||||
ctaText,
|
||||
ctaLink,
|
||||
gradient,
|
||||
lottie,
|
||||
image,
|
||||
mobileImage,
|
||||
noCta,
|
||||
contactUsTitle,
|
||||
contactUsLink,
|
||||
}) => {
|
||||
return (
|
||||
<Flex flexDir="column" gap={{ base: 2, md: 4 }}>
|
||||
<Container maxW="container.lg" paddingX={{ base: 0, md: 4 }}>
|
||||
<Flex flexDir="column" gap={{ base: 6, md: 8 }}>
|
||||
<Flex flexDir="column" gap={2}>
|
||||
<Heading
|
||||
as="h2"
|
||||
size="display.sm"
|
||||
textAlign="center"
|
||||
px={{ base: 2, md: 0 }}
|
||||
>
|
||||
{title}{" "}
|
||||
<Box as="span" bgGradient={gradient} bgClip="text">
|
||||
{titleWithGradient}
|
||||
</Box>
|
||||
</Heading>
|
||||
</Flex>
|
||||
<Text
|
||||
color="white"
|
||||
opacity={0.8}
|
||||
textAlign="center"
|
||||
fontSize={[16, 20]}
|
||||
fontWeight={500}
|
||||
size="body.lg"
|
||||
>
|
||||
{subtitle}
|
||||
</Text>
|
||||
</Flex>
|
||||
<Flex maxH="1000px" my={4} pointerEvents="none">
|
||||
<LandingDesktopMobileImage
|
||||
lottie={lottie}
|
||||
image={image}
|
||||
mobileImage={mobileImage}
|
||||
/>
|
||||
</Flex>
|
||||
<Flex gap={2} justifyContent="center" alignItems="center">
|
||||
<LandingCTAButtons
|
||||
noCta={noCta}
|
||||
noContactUs
|
||||
ctaText={ctaText}
|
||||
ctaLink={ctaLink}
|
||||
trackingCategory={trackingCategory}
|
||||
contactUsTitle={contactUsTitle}
|
||||
contactUsLink={contactUsLink}
|
||||
/>
|
||||
</Flex>
|
||||
</Container>
|
||||
<LandingDesktopMobileImage image={image} mobileImage={mobileImage} />
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
@@ -53,8 +53,8 @@ function AnnouncementBanner(props: {
|
||||
export function UnlimitedWalletsBanner() {
|
||||
return (
|
||||
<AnnouncementBanner
|
||||
href="/dashboard/settings/billing?coupon=FREEWALLETS"
|
||||
label='Claim 12 months of free in-app wallets. Use code "FREEWALLETS". Redeem offer by October 31st!'
|
||||
href="/dashboard/settings/billing?coupon=FREEWALLETS24"
|
||||
label='Claim 12 months of free in-app wallets. Use code "FREEWALLETS24". Redeem offer by December 31st!'
|
||||
trackingLabel="unlimited-wallets"
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -1,85 +0,0 @@
|
||||
import { Box, Flex } from "@chakra-ui/react";
|
||||
import { ArrowRightIcon } from "lucide-react";
|
||||
import NextImage, { type StaticImageData } from "next/image";
|
||||
import { Card, Heading, TrackedLink } from "tw-components";
|
||||
import type { ComponentWithChildren } from "types/component-with-children";
|
||||
|
||||
interface ImageCardProps {
|
||||
title: string;
|
||||
image: StaticImageData;
|
||||
TRACKING_CATEGORY: string;
|
||||
linkTitle?: string;
|
||||
category?: string;
|
||||
label: string;
|
||||
href: string;
|
||||
}
|
||||
|
||||
export const ImageCard: ComponentWithChildren<ImageCardProps> = ({
|
||||
title,
|
||||
image,
|
||||
href,
|
||||
linkTitle,
|
||||
category,
|
||||
TRACKING_CATEGORY,
|
||||
label,
|
||||
children,
|
||||
}) => {
|
||||
return (
|
||||
<TrackedLink
|
||||
category={TRACKING_CATEGORY || href}
|
||||
href={href}
|
||||
label={label}
|
||||
isExternal
|
||||
color="blue.400"
|
||||
_hover={{ textDecor: "none" }}
|
||||
role="group"
|
||||
textDecoration="none"
|
||||
>
|
||||
<Card as={Flex} flexDir="column" bg="black" padding={0} overflow="hidden">
|
||||
<NextImage src={image} alt="" priority />
|
||||
<Flex
|
||||
flexDir="column"
|
||||
gap={6}
|
||||
p={8}
|
||||
justifyContent="space-between"
|
||||
h="full"
|
||||
>
|
||||
<Flex alignItems="center" flexWrap="wrap" gap={4}>
|
||||
<Heading size="title.sm">{title}</Heading>
|
||||
|
||||
{category && (
|
||||
<Box
|
||||
py={1}
|
||||
px={3}
|
||||
borderColor="#383838"
|
||||
borderWidth={1}
|
||||
maxW="fit-content"
|
||||
rounded={8}
|
||||
fontSize="12px"
|
||||
>
|
||||
{category}
|
||||
</Box>
|
||||
)}
|
||||
</Flex>
|
||||
{children && (
|
||||
<Flex
|
||||
direction="column"
|
||||
fontSize="body.lg"
|
||||
color="paragraph"
|
||||
lineHeight={1.6}
|
||||
h="100%"
|
||||
>
|
||||
{children}
|
||||
</Flex>
|
||||
)}
|
||||
{href && (
|
||||
<Flex alignItems="center">
|
||||
{linkTitle || <span>Visit website</span>}
|
||||
<ArrowRightIcon className="-rotate-45 ml-2 size-4 transition-transform group-hover:translate-x-1 group-hover:rotate-45" />
|
||||
</Flex>
|
||||
)}
|
||||
</Flex>
|
||||
</Card>
|
||||
</TrackedLink>
|
||||
);
|
||||
};
|
||||
@@ -151,6 +151,8 @@ export function ApplyCouponCardUI(props: {
|
||||
res.data,
|
||||
// prod & dev
|
||||
values.promoCode === "FREEWALLETS" ||
|
||||
// new prod code
|
||||
values.promoCode === "FREEWALLETS24" ||
|
||||
values.promoCode === "TESTFREEWALLETS",
|
||||
);
|
||||
}
|
||||
|
||||
+91
-43
@@ -2,13 +2,14 @@
|
||||
|
||||
import { ExportToCSVButton } from "@/components/blocks/ExportToCSVButton";
|
||||
import {
|
||||
type ChartConfig,
|
||||
ChartContainer,
|
||||
ChartLegend,
|
||||
ChartLegendContent,
|
||||
ChartTooltip,
|
||||
ChartTooltipContent,
|
||||
} from "@/components/ui/chart";
|
||||
import type { UserOpStats } from "@3rdweb-sdk/react/hooks/useApi";
|
||||
import type { UserOpStatsByChain } from "@3rdweb-sdk/react/hooks/useApi";
|
||||
import {
|
||||
EmptyChartState,
|
||||
LoadingChartState,
|
||||
@@ -20,53 +21,94 @@ import { UnityIcon } from "components/icons/brand-icons/UnityIcon";
|
||||
import { UnrealIcon } from "components/icons/brand-icons/UnrealIcon";
|
||||
import { DocLink } from "components/shared/DocLink";
|
||||
import { format } from "date-fns";
|
||||
import { useAllChainsData } from "hooks/chains/allChains";
|
||||
import { useMemo } from "react";
|
||||
import { Bar, BarChart, CartesianGrid, XAxis, YAxis } from "recharts";
|
||||
import { formatTickerNumber } from "../../../lib/format-utils";
|
||||
|
||||
type ChartData = {
|
||||
type ChartData = Record<string, number> & {
|
||||
time: string; // human readable date
|
||||
failed: number;
|
||||
successful: number;
|
||||
};
|
||||
|
||||
const chartConfig = {
|
||||
successful: {
|
||||
label: "Successful",
|
||||
color: "hsl(var(--chart-1))",
|
||||
},
|
||||
failed: {
|
||||
label: "Failed",
|
||||
color: "red",
|
||||
},
|
||||
};
|
||||
export function SponsoredTransactionsChartCard(props: {
|
||||
userOpStats: UserOpStats[];
|
||||
userOpStats: UserOpStatsByChain[];
|
||||
isPending: boolean;
|
||||
}) {
|
||||
const { userOpStats } = props;
|
||||
const topChainsToShow = 10;
|
||||
const chainsStore = useAllChainsData();
|
||||
|
||||
const barChartData: ChartData[] = useMemo(() => {
|
||||
const chartDataMap: Map<string, ChartData> = new Map();
|
||||
const { chartConfig, chartData } = useMemo(() => {
|
||||
const _chartConfig: ChartConfig = {};
|
||||
const _chartDataMap: Map<string, ChartData> = new Map();
|
||||
const chainIdToVolumeMap: Map<string, number> = new Map();
|
||||
// for each stat, add it in _chartDataMap
|
||||
for (const stat of userOpStats) {
|
||||
const chartData = _chartDataMap.get(stat.date);
|
||||
const { chainId } = stat;
|
||||
const chain = chainsStore.idToChain.get(Number(chainId));
|
||||
|
||||
for (const data of userOpStats) {
|
||||
const chartData = chartDataMap.get(data.date);
|
||||
// if no data for current day - create new entry
|
||||
if (!chartData) {
|
||||
chartDataMap.set(data.date, {
|
||||
time: format(new Date(data.date), "MMM dd"),
|
||||
successful: data.successful,
|
||||
failed: data.failed,
|
||||
});
|
||||
_chartDataMap.set(stat.date, {
|
||||
time: format(new Date(stat.date), "MMM dd"),
|
||||
[chain?.name || chainId || "Unknown"]:
|
||||
Math.round(stat.sponsoredUsd * 100) / 100,
|
||||
} as ChartData);
|
||||
} else {
|
||||
chartData.successful += data.successful;
|
||||
chartData.failed += data.failed;
|
||||
chartData[chain?.name || chainId || "Unknown"] =
|
||||
(chartData[chain?.name || chainId || "Unknown"] || 0) +
|
||||
Math.round(stat.sponsoredUsd * 100) / 100;
|
||||
}
|
||||
|
||||
chainIdToVolumeMap.set(
|
||||
chain?.name || chainId || "Unknown",
|
||||
stat.sponsoredUsd + (chainIdToVolumeMap.get(chainId || "Unknown") || 0),
|
||||
);
|
||||
}
|
||||
|
||||
const chainsSorted = Array.from(chainIdToVolumeMap.entries())
|
||||
.sort((a, b) => b[1] - a[1])
|
||||
.map((w) => w[0]);
|
||||
|
||||
const chainsToShow = chainsSorted.slice(0, topChainsToShow);
|
||||
const chainsToTagAsOthers = chainsSorted.slice(topChainsToShow);
|
||||
|
||||
// replace chainIdsToTagAsOther chainId with "other"
|
||||
for (const data of _chartDataMap.values()) {
|
||||
for (const chainId in data) {
|
||||
if (chainsToTagAsOthers.includes(chainId)) {
|
||||
data.others = (data.others || 0) + (data[chainId] || 0);
|
||||
delete data[chainId];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Array.from(chartDataMap.values());
|
||||
}, [userOpStats]);
|
||||
chainsToShow.forEach((walletType, i) => {
|
||||
_chartConfig[walletType] = {
|
||||
label: chainsToShow[i],
|
||||
color: `hsl(var(--chart-${(i % 10) + 1}))`,
|
||||
};
|
||||
});
|
||||
|
||||
const disableActions = props.isPending || barChartData.length === 0;
|
||||
// Add Other
|
||||
chainsToShow.push("others");
|
||||
_chartConfig.others = {
|
||||
label: "Others",
|
||||
color: "hsl(var(--muted-foreground))",
|
||||
};
|
||||
|
||||
return {
|
||||
chartData: Array.from(_chartDataMap.values()),
|
||||
chartConfig: _chartConfig,
|
||||
};
|
||||
}, [userOpStats, chainsStore]);
|
||||
|
||||
const uniqueChainIds = Object.keys(chartConfig);
|
||||
const disableActions =
|
||||
props.isPending ||
|
||||
chartData.length === 0 ||
|
||||
chartData.every((data) => data.transactions === 0);
|
||||
|
||||
return (
|
||||
<div className="relative w-full rounded-lg border border-border bg-muted/50 p-4 md:p-6">
|
||||
@@ -83,10 +125,13 @@ export function SponsoredTransactionsChartCard(props: {
|
||||
fileName="Sponsored Transactions"
|
||||
disabled={disableActions}
|
||||
getData={async () => {
|
||||
const header = ["Date", "Successful", "Failed"];
|
||||
const rows = barChartData.map((data) => {
|
||||
const { time, successful, failed } = data;
|
||||
return [time, successful.toString(), failed.toString()];
|
||||
const header = ["Date", ...uniqueChainIds];
|
||||
const rows = chartData.map((data) => {
|
||||
const { time, ...rest } = data;
|
||||
return [
|
||||
time,
|
||||
...uniqueChainIds.map((w) => (rest[w] || 0).toString()),
|
||||
];
|
||||
});
|
||||
return { header, rows };
|
||||
}}
|
||||
@@ -97,10 +142,8 @@ export function SponsoredTransactionsChartCard(props: {
|
||||
<ChartContainer config={chartConfig} className="h-[400px] w-full">
|
||||
{props.isPending ? (
|
||||
<LoadingChartState />
|
||||
) : barChartData.length === 0 ||
|
||||
barChartData.every(
|
||||
(data) => data.failed === 0 && data.successful === 0,
|
||||
) ? (
|
||||
) : chartData.length === 0 ||
|
||||
chartData.every((data) => data.transactions === 0) ? (
|
||||
<EmptyChartState>
|
||||
<div className="flex flex-col items-center justify-center">
|
||||
<span className="mb-6 text-lg">
|
||||
@@ -143,7 +186,7 @@ export function SponsoredTransactionsChartCard(props: {
|
||||
) : (
|
||||
<BarChart
|
||||
accessibilityLayer
|
||||
data={barChartData}
|
||||
data={chartData}
|
||||
margin={{
|
||||
top: 20,
|
||||
}}
|
||||
@@ -158,7 +201,12 @@ export function SponsoredTransactionsChartCard(props: {
|
||||
/>
|
||||
|
||||
<YAxis
|
||||
dataKey={(data) => data.successful + data.failed}
|
||||
dataKey={(data) => {
|
||||
return Object.entries(data)
|
||||
.filter(([key]) => key !== "time")
|
||||
.map(([, value]) => value)
|
||||
.reduce((acc, current) => Number(acc) + Number(current), 0);
|
||||
}}
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
tickFormatter={(value) => formatTickerNumber(value)}
|
||||
@@ -173,12 +221,12 @@ export function SponsoredTransactionsChartCard(props: {
|
||||
}
|
||||
/>
|
||||
<ChartLegend content={<ChartLegendContent />} />
|
||||
{(["failed", "successful"] as const).map((result) => {
|
||||
{uniqueChainIds.map((chainId) => {
|
||||
return (
|
||||
<Bar
|
||||
key={result}
|
||||
dataKey={result}
|
||||
fill={chartConfig[result].color}
|
||||
key={chainId}
|
||||
dataKey={chainId}
|
||||
fill={chartConfig[chainId]?.color}
|
||||
radius={4}
|
||||
stackId="a"
|
||||
strokeWidth={1.5}
|
||||
|
||||
-1
@@ -204,7 +204,6 @@ export function TotalSponsoredChartCard(props: {
|
||||
|
||||
<YAxis
|
||||
dataKey={(data) => {
|
||||
console.log(data);
|
||||
return Object.entries(data)
|
||||
.filter(([key]) => key !== "time")
|
||||
.map(([, value]) => value)
|
||||
|
||||
+4
-3
@@ -1,7 +1,7 @@
|
||||
import type { UserOpStats } from "@3rdweb-sdk/react/hooks/useApi";
|
||||
import type { UserOpStatsByChain } from "@3rdweb-sdk/react/hooks/useApi";
|
||||
|
||||
export function createUserOpStatsStub(days: number): UserOpStats[] {
|
||||
const stubbedData: UserOpStats[] = [];
|
||||
export function createUserOpStatsStub(days: number): UserOpStatsByChain[] {
|
||||
const stubbedData: UserOpStatsByChain[] = [];
|
||||
|
||||
let d = days;
|
||||
while (d !== 0) {
|
||||
@@ -13,6 +13,7 @@ export function createUserOpStatsStub(days: number): UserOpStats[] {
|
||||
successful,
|
||||
failed,
|
||||
sponsoredUsd,
|
||||
chainId: Math.floor(Math.random() * 100).toString(),
|
||||
});
|
||||
|
||||
if (Math.random() > 0.7) {
|
||||
|
||||
@@ -4,7 +4,6 @@ import {
|
||||
Alert,
|
||||
AlertIcon,
|
||||
Box,
|
||||
Container,
|
||||
Flex,
|
||||
FormControl,
|
||||
Input,
|
||||
@@ -24,7 +23,6 @@ import type { CreateDelayedRevealBatchParams } from "thirdweb/extensions/erc721"
|
||||
import type { NFTInput } from "thirdweb/utils";
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
Checkbox,
|
||||
FormErrorMessage,
|
||||
FormHelperText,
|
||||
@@ -139,12 +137,8 @@ export const BatchLazyMint: ComponentWithChildren<BatchLazyMintProps> = (
|
||||
const paginationPortalRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
return (
|
||||
<Container
|
||||
maxW="container.page"
|
||||
borderRadius={{ base: 0, md: "2xl" }}
|
||||
my={{ base: 0, md: 12 }}
|
||||
p={{ base: 0, md: 4 }}
|
||||
as="form"
|
||||
<form
|
||||
className="mt-4 flex w-full flex-col"
|
||||
onSubmit={form.handleSubmit((data) => {
|
||||
// first shuffle
|
||||
const shuffledMetadatas = data.shuffle
|
||||
@@ -188,146 +182,111 @@ export const BatchLazyMint: ComponentWithChildren<BatchLazyMintProps> = (
|
||||
});
|
||||
})}
|
||||
>
|
||||
<Card bg="backgroundCardHighlight">
|
||||
<Flex flexDir="column" width="100%" p={4}>
|
||||
{step === 0 ? (
|
||||
{step === 0 ? (
|
||||
<div className="flex flex-col gap-6">
|
||||
{nftMetadatas.length > 0 ? (
|
||||
<>
|
||||
<Flex
|
||||
align="center"
|
||||
justify="space-between"
|
||||
py={{ base: 2, md: 4 }}
|
||||
w="100%"
|
||||
mb={2}
|
||||
>
|
||||
<Heading size="title.md">Upload your NFTs</Heading>
|
||||
</Flex>
|
||||
<Flex direction="column" gap={6} h="100%">
|
||||
{nftMetadatas.length > 0 ? (
|
||||
<BatchTable
|
||||
portalRef={paginationPortalRef}
|
||||
data={nftMetadatas}
|
||||
nextTokenIdToMint={props.nextTokenIdToMint}
|
||||
/>
|
||||
) : (
|
||||
<UploadStep
|
||||
getRootProps={getRootProps}
|
||||
getInputProps={getInputProps}
|
||||
hasFailed={hasError}
|
||||
isDragActive={isDragActive}
|
||||
/>
|
||||
)}
|
||||
<Flex borderTop="1px solid" borderTopColor="borderColor">
|
||||
<Container maxW="container.page">
|
||||
<Flex
|
||||
align="center"
|
||||
justify="space-between"
|
||||
p={{ base: 0, md: 4 }}
|
||||
flexDir={{ base: "column", md: "row" }}
|
||||
mt={{ base: 4, md: 0 }}
|
||||
<BatchTable
|
||||
portalRef={paginationPortalRef}
|
||||
data={nftMetadatas}
|
||||
nextTokenIdToMint={props.nextTokenIdToMint}
|
||||
/>
|
||||
<div className="border-border border-t">
|
||||
<div className="flex flex-col items-center justify-between p-0 md:flex-row md:p-4">
|
||||
<Box ref={paginationPortalRef} />
|
||||
<div className="mt-4 flex w-full flex-row items-center gap-2 md:mt-0 md:w-auto">
|
||||
<Button
|
||||
borderRadius="md"
|
||||
isDisabled={!hasError}
|
||||
onClick={() => {
|
||||
form.reset();
|
||||
}}
|
||||
w={{ base: "100%", md: "auto" }}
|
||||
>
|
||||
<Box ref={paginationPortalRef} />
|
||||
<Flex
|
||||
gap={2}
|
||||
align="center"
|
||||
mt={{ base: 4, md: 0 }}
|
||||
w={{ base: "100%", md: "auto" }}
|
||||
>
|
||||
<Button
|
||||
borderRadius="md"
|
||||
isDisabled={nftMetadatas.length === 0 || !hasError}
|
||||
onClick={() => {
|
||||
form.reset();
|
||||
}}
|
||||
w={{ base: "100%", md: "auto" }}
|
||||
>
|
||||
Reset
|
||||
</Button>
|
||||
<Button
|
||||
borderRadius="md"
|
||||
colorScheme="primary"
|
||||
isDisabled={nftMetadatas.length === 0}
|
||||
onClick={() => setStep(1)}
|
||||
w={{ base: "100%", md: "auto" }}
|
||||
>
|
||||
Next
|
||||
</Button>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Container>
|
||||
</Flex>
|
||||
</Flex>
|
||||
Reset
|
||||
</Button>
|
||||
<Button
|
||||
borderRadius="md"
|
||||
colorScheme="primary"
|
||||
onClick={() => setStep(1)}
|
||||
w={{ base: "100%", md: "auto" }}
|
||||
>
|
||||
Next
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Flex
|
||||
align="center"
|
||||
justify="space-between"
|
||||
py={4}
|
||||
w="100%"
|
||||
mb={2}
|
||||
<UploadStep
|
||||
getRootProps={getRootProps}
|
||||
getInputProps={getInputProps}
|
||||
hasFailed={hasError}
|
||||
isDragActive={isDragActive}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<Flex align="center" justify="space-between" py={4} w="100%" mb={2}>
|
||||
<div className="flex flex-row items-center gap-2">
|
||||
<Button
|
||||
className="text-muted-foreground"
|
||||
variant="ghost"
|
||||
onClick={() => setStep(0)}
|
||||
>
|
||||
<div className="flex flex-row items-center gap-2">
|
||||
<Button
|
||||
className="text-muted-foreground"
|
||||
variant="ghost"
|
||||
onClick={() => setStep(0)}
|
||||
>
|
||||
<ChevronLeftIcon className="size-5 cursor-pointer" />
|
||||
</Button>
|
||||
<Heading size="title.md" className="my-auto">
|
||||
When will you reveal your NFTs?
|
||||
</Heading>
|
||||
</div>
|
||||
</Flex>
|
||||
<SelectReveal
|
||||
form={form}
|
||||
canCreateDelayedRevealBatch={props.canCreateDelayedRevealBatch}
|
||||
/>
|
||||
{form.watch("revealType") && (
|
||||
<>
|
||||
<Checkbox {...form.register("shuffle")} mt={3}>
|
||||
<Flex gap={1} flexDir={{ base: "column", md: "row" }}>
|
||||
<Text>
|
||||
Shuffle the order of the NFTs before uploading.
|
||||
</Text>
|
||||
<Text fontStyle="italic">
|
||||
This is an off-chain operation and is not provable.
|
||||
</Text>
|
||||
</Flex>
|
||||
</Checkbox>
|
||||
<Box maxW={{ base: "100%", md: "61%" }}>
|
||||
<TransactionButton
|
||||
txChainID={props.chainId}
|
||||
mt={4}
|
||||
colorScheme="primary"
|
||||
transactionCount={1}
|
||||
isDisabled={!nftMetadatas.length}
|
||||
type="submit"
|
||||
isLoading={form.formState.isSubmitting}
|
||||
loadingText={`Uploading ${nftMetadatas.length} NFTs...`}
|
||||
w="full"
|
||||
>
|
||||
Upload {nftMetadatas.length} NFTs
|
||||
</TransactionButton>
|
||||
{props.children}
|
||||
</Box>
|
||||
<Text size="body.sm" mt={2}>
|
||||
<TrackedLink
|
||||
href="https://support.thirdweb.com/dashboard/n5evQ4EfEjEifczEQaZ1hL/batch-upload-troubleshooting/5WMQFqfaUTU1C8NM8FtJ2X"
|
||||
isExternal
|
||||
category="batch-upload"
|
||||
label="issues"
|
||||
>
|
||||
Experiencing issues uploading your files?
|
||||
</TrackedLink>
|
||||
<ChevronLeftIcon className="size-5 cursor-pointer" />
|
||||
</Button>
|
||||
<Heading size="title.md" className="my-auto">
|
||||
When will you reveal your NFTs?
|
||||
</Heading>
|
||||
</div>
|
||||
</Flex>
|
||||
<SelectReveal
|
||||
form={form}
|
||||
canCreateDelayedRevealBatch={props.canCreateDelayedRevealBatch}
|
||||
/>
|
||||
{form.watch("revealType") && (
|
||||
<>
|
||||
<Checkbox {...form.register("shuffle")} mt={3}>
|
||||
<Flex gap={1} flexDir={{ base: "column", md: "row" }}>
|
||||
<Text>Shuffle the order of the NFTs before uploading.</Text>
|
||||
<Text fontStyle="italic">
|
||||
This is an off-chain operation and is not provable.
|
||||
</Text>
|
||||
</>
|
||||
)}
|
||||
</Flex>
|
||||
</Checkbox>
|
||||
<Box maxW={{ base: "100%", md: "61%" }}>
|
||||
<TransactionButton
|
||||
txChainID={props.chainId}
|
||||
mt={4}
|
||||
colorScheme="primary"
|
||||
transactionCount={1}
|
||||
isDisabled={!nftMetadatas.length}
|
||||
type="submit"
|
||||
isLoading={form.formState.isSubmitting}
|
||||
loadingText={`Uploading ${nftMetadatas.length} NFTs...`}
|
||||
w="full"
|
||||
>
|
||||
Upload {nftMetadatas.length} NFTs
|
||||
</TransactionButton>
|
||||
{props.children}
|
||||
</Box>
|
||||
<Text size="body.sm" mt={2}>
|
||||
<TrackedLink
|
||||
href="https://support.thirdweb.com/dashboard/n5evQ4EfEjEifczEQaZ1hL/batch-upload-troubleshooting/5WMQFqfaUTU1C8NM8FtJ2X"
|
||||
isExternal
|
||||
category="batch-upload"
|
||||
label="issues"
|
||||
>
|
||||
Experiencing issues uploading your files?
|
||||
</TrackedLink>
|
||||
</Text>
|
||||
</>
|
||||
)}
|
||||
</Flex>
|
||||
</Card>
|
||||
</Container>
|
||||
</>
|
||||
)}
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { UnorderedList } from "@/components/ui/List/List";
|
||||
import { InlineCode } from "@/components/ui/inline-code";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Container, Flex, Link, UnorderedList } from "@chakra-ui/react";
|
||||
import { UploadIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { Heading, Text } from "tw-components";
|
||||
|
||||
interface UploadStepProps {
|
||||
@@ -20,117 +21,111 @@ export const UploadStep: React.FC<UploadStepProps> = ({
|
||||
getInputProps,
|
||||
}) => {
|
||||
return (
|
||||
<Flex flexGrow={1} align="center" overflow="auto">
|
||||
<Container maxW="container.page">
|
||||
<Flex gap={8} flexDir={{ base: "column", md: "row" }}>
|
||||
<div className="relative aspect-square w-full md:w-1/2">
|
||||
<div
|
||||
<div className="flex flex-col items-center gap-8 md:flex-row">
|
||||
<div className="relative aspect-square w-full md:w-1/2">
|
||||
<div
|
||||
className={cn(
|
||||
"flex h-full cursor-pointer rounded-md border border-border hover:border-blue-500",
|
||||
hasFailed ? "bg-red-200" : "bg-card",
|
||||
)}
|
||||
{...getRootProps()}
|
||||
>
|
||||
<input {...getInputProps()} />
|
||||
<div className="m-auto flex flex-col p-6">
|
||||
<UploadIcon
|
||||
className={cn(
|
||||
"flex h-full cursor-pointer rounded-md border border-border hover:border-blue-500",
|
||||
hasFailed ? "bg-red-200" : "bg-card",
|
||||
"mx-auto mb-2 size-8",
|
||||
hasFailed ? "text-red-500" : "text-gray-600",
|
||||
)}
|
||||
{...getRootProps()}
|
||||
>
|
||||
<input {...getInputProps()} />
|
||||
<div className="m-auto flex flex-col p-6">
|
||||
<UploadIcon
|
||||
className={cn(
|
||||
"mx-auto mb-2 size-8",
|
||||
hasFailed ? "text-red-500" : "text-gray-600",
|
||||
)}
|
||||
/>
|
||||
{isDragActive ? (
|
||||
<Heading
|
||||
as={Text}
|
||||
size="label.md"
|
||||
color="gray.600"
|
||||
textAlign="center"
|
||||
>
|
||||
Drop the files here
|
||||
</Heading>
|
||||
) : (
|
||||
<Heading
|
||||
as={Text}
|
||||
size="label.md"
|
||||
lineHeight={1.2}
|
||||
color={hasFailed ? "red.500" : "gray.600"}
|
||||
textAlign="center"
|
||||
>
|
||||
{hasFailed
|
||||
? `No valid CSV or JSON file found. Please make sure your NFT metadata includes at least a "name" field and try again.`
|
||||
: "Drag & Drop files or folders here, or click to select files"}
|
||||
</Heading>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
/>
|
||||
{isDragActive ? (
|
||||
<Heading
|
||||
as={Text}
|
||||
size="label.md"
|
||||
color="gray.600"
|
||||
textAlign="center"
|
||||
>
|
||||
Drop the files here
|
||||
</Heading>
|
||||
) : (
|
||||
<Heading
|
||||
as={Text}
|
||||
size="label.md"
|
||||
lineHeight={1.2}
|
||||
color={hasFailed ? "red.500" : "gray.600"}
|
||||
textAlign="center"
|
||||
>
|
||||
{hasFailed
|
||||
? `No valid CSV or JSON file found. Please make sure your NFT metadata includes at least a "name" field and try again.`
|
||||
: "Drag & Drop files or folders here, or click to select files"}
|
||||
</Heading>
|
||||
)}
|
||||
</div>
|
||||
<Flex gap={2} flexDir="column" w={{ base: "100%", md: "50%" }}>
|
||||
<Heading size="subtitle.sm">Requirements</Heading>
|
||||
<UnorderedList>
|
||||
<li>
|
||||
Files <em>must</em> contain one .csv or .json file with
|
||||
metadata. -{" "}
|
||||
<Link download color="blue.500" href="/example.csv">
|
||||
Download example.csv
|
||||
</Link>
|
||||
.{" "}
|
||||
<Link download color="blue.500" href="/example.json">
|
||||
Download example.json
|
||||
</Link>
|
||||
.
|
||||
</li>
|
||||
<li>
|
||||
The csv <em>must</em> have a <InlineCode code="name" /> column,
|
||||
which defines the name of the NFT.
|
||||
</li>
|
||||
<li>
|
||||
Asset names <em>must</em> be sequential 0,1,2,3...n.[extension].
|
||||
It doesn't matter at what number you begin. (Example:{" "}
|
||||
<InlineCode code="131.png" />, <InlineCode code="132.png" />
|
||||
).
|
||||
</li>
|
||||
<li>
|
||||
Make sure to drag and drop the CSV/JSON and the images{" "}
|
||||
<strong>at the same time</strong>.
|
||||
</li>
|
||||
</UnorderedList>
|
||||
<Heading size="subtitle.sm" mt={4}>
|
||||
Options
|
||||
</Heading>
|
||||
<UnorderedList>
|
||||
<li>
|
||||
Images and other file types can be used in combination.
|
||||
<br />
|
||||
<small>
|
||||
They both have to follow the asset naming convention above.
|
||||
(Example: <InlineCode code="0.png" /> and{" "}
|
||||
<InlineCode code="0.mp4" />,<InlineCode code="1.png" /> and{" "}
|
||||
<InlineCode code="1.glb" />, etc.)
|
||||
</small>
|
||||
</li>
|
||||
<li>
|
||||
When uploading files, we will upload them and pin them to IPFS
|
||||
automatically for you. If you already have the files uploaded,
|
||||
you can add an <InlineCode code="image" /> and/or
|
||||
<InlineCode code="animation_url" /> column and add the IPFS
|
||||
hashes there.
|
||||
<Link download color="blue.500" href="/example-with-ipfs.csv">
|
||||
Download example.csv
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
If you want to make your media files map to your NFTs, you can
|
||||
add add the name of your files to the{" "}
|
||||
<InlineCode code="image" /> and
|
||||
<InlineCode code="animation_url" /> column.
|
||||
<Link download color="blue.500" href="/example-with-maps.csv">
|
||||
Download example.csv
|
||||
</Link>
|
||||
</li>
|
||||
</UnorderedList>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Container>
|
||||
</Flex>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex w-full flex-col gap-2 md:w-1/2">
|
||||
<Heading size="subtitle.sm">Requirements</Heading>
|
||||
<UnorderedList>
|
||||
<li>
|
||||
Files <em>must</em> contain one .csv or .json file with metadata. -{" "}
|
||||
<Link download color="blue.500" href="/example.csv">
|
||||
Download example.csv
|
||||
</Link>
|
||||
.{" "}
|
||||
<Link download color="blue.500" href="/example.json">
|
||||
Download example.json
|
||||
</Link>
|
||||
.
|
||||
</li>
|
||||
<li>
|
||||
The csv <em>must</em> have a <InlineCode code="name" /> column,
|
||||
which defines the name of the NFT.
|
||||
</li>
|
||||
<li>
|
||||
Asset names <em>must</em> be sequential 0,1,2,3...n.[extension]. It
|
||||
doesn't matter at what number you begin. (Example:{" "}
|
||||
<InlineCode code="131.png" />, <InlineCode code="132.png" />
|
||||
).
|
||||
</li>
|
||||
<li>
|
||||
Make sure to drag and drop the CSV/JSON and the images{" "}
|
||||
<strong>at the same time</strong>.
|
||||
</li>
|
||||
</UnorderedList>
|
||||
<Heading size="subtitle.sm" mt={4}>
|
||||
Options
|
||||
</Heading>
|
||||
<UnorderedList>
|
||||
<li>
|
||||
Images and other file types can be used in combination.
|
||||
<br />
|
||||
<small>
|
||||
They both have to follow the asset naming convention above.
|
||||
(Example: <InlineCode code="0.png" /> and{" "}
|
||||
<InlineCode code="0.mp4" />,<InlineCode code="1.png" /> and{" "}
|
||||
<InlineCode code="1.glb" />, etc.)
|
||||
</small>
|
||||
</li>
|
||||
<li>
|
||||
When uploading files, we will upload them and pin them to IPFS
|
||||
automatically for you. If you already have the files uploaded, you
|
||||
can add an <InlineCode code="image" /> and/or
|
||||
<InlineCode code="animation_url" /> column and add the IPFS hashes
|
||||
there.
|
||||
<Link download color="blue.500" href="/example-with-ipfs.csv">
|
||||
Download example.csv
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
If you want to make your media files map to your NFTs, you can add
|
||||
the name of your files to the <InlineCode code="image" /> and
|
||||
<InlineCode code="animation_url" /> column.
|
||||
<Link download color="blue.500" href="/example-with-maps.csv">
|
||||
Download example.csv
|
||||
</Link>
|
||||
</li>
|
||||
</UnorderedList>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,28 +1,31 @@
|
||||
import "server-only";
|
||||
|
||||
export async function fetchAnalytics(input: string | URL, init?: RequestInit) {
|
||||
export async function fetchAnalytics(
|
||||
input: string | URL,
|
||||
init?: RequestInit,
|
||||
): Promise<Response> {
|
||||
const [pathname, searchParams] = input.toString().split("?");
|
||||
if (!pathname) {
|
||||
throw new Error("Invalid input, no pathname provided");
|
||||
}
|
||||
|
||||
// create a new URL object for the analytics server
|
||||
const API_SERVER_URL = new URL(
|
||||
const ANALYTICS_SERVICE_URL = new URL(
|
||||
process.env.ANALYTICS_SERVICE_URL || "https://analytics.thirdweb.com",
|
||||
);
|
||||
API_SERVER_URL.pathname = pathname;
|
||||
ANALYTICS_SERVICE_URL.pathname = pathname;
|
||||
for (const param of searchParams?.split("&") || []) {
|
||||
const [key, value] = param.split("=");
|
||||
if (!key || !value) {
|
||||
return;
|
||||
throw new Error("Invalid input, no key or value provided");
|
||||
}
|
||||
API_SERVER_URL.searchParams.append(
|
||||
ANALYTICS_SERVICE_URL.searchParams.append(
|
||||
decodeURIComponent(key),
|
||||
decodeURIComponent(value),
|
||||
);
|
||||
}
|
||||
|
||||
return await fetch(API_SERVER_URL, {
|
||||
return fetch(ANALYTICS_SERVICE_URL, {
|
||||
...init,
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import { COOKIE_ACTIVE_ACCOUNT, COOKIE_PREFIX_TOKEN } from "@/constants/cookie";
|
||||
import { cookies } from "next/headers";
|
||||
import { getAddress } from "thirdweb";
|
||||
import "server-only";
|
||||
|
||||
export async function fetchApiServer(
|
||||
input: string | URL,
|
||||
init?: RequestInit,
|
||||
): Promise<Response> {
|
||||
const activeAccount = cookies().get(COOKIE_ACTIVE_ACCOUNT)?.value;
|
||||
const authToken = activeAccount
|
||||
? cookies().get(COOKIE_PREFIX_TOKEN + getAddress(activeAccount))?.value
|
||||
: null;
|
||||
|
||||
const [pathname, searchParams] = input.toString().split("?");
|
||||
if (!pathname) {
|
||||
throw new Error("Invalid input, no pathname provided");
|
||||
}
|
||||
|
||||
// create a new URL object for the analytics server
|
||||
const API_SERVER_URL = new URL(
|
||||
process.env.NEXT_PUBLIC_THIRDWEB_API_HOST || "https://api.thirdweb.com",
|
||||
);
|
||||
API_SERVER_URL.pathname = pathname;
|
||||
for (const param of searchParams?.split("&") || []) {
|
||||
const [key, value] = param.split("=");
|
||||
if (!key || !value) {
|
||||
throw new Error("Invalid input, no key or value provided");
|
||||
}
|
||||
API_SERVER_URL.searchParams.append(
|
||||
decodeURIComponent(key),
|
||||
decodeURIComponent(value),
|
||||
);
|
||||
}
|
||||
|
||||
return fetch(API_SERVER_URL, {
|
||||
...init,
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
...(authToken ? { authorization: `Bearer ${authToken}` } : {}),
|
||||
...init?.headers,
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import type { EcosystemWalletStats } from "@3rdweb-sdk/react/hooks/useApi";
|
||||
import { fetchAnalytics } from "../fetch-analytics";
|
||||
|
||||
export async function getEcosystemWalletUsage(args: {
|
||||
ecosystemId: string;
|
||||
from?: Date;
|
||||
to?: Date;
|
||||
period?: "day" | "week" | "month" | "year" | "all";
|
||||
}) {
|
||||
const { ecosystemId, from, to, period } = args;
|
||||
|
||||
const searchParams = new URLSearchParams();
|
||||
if (from) {
|
||||
searchParams.append("from", from.toISOString());
|
||||
}
|
||||
if (to) {
|
||||
searchParams.append("to", to.toISOString());
|
||||
}
|
||||
if (period) {
|
||||
searchParams.append("period", period);
|
||||
}
|
||||
const res = await fetchAnalytics(
|
||||
`v1/wallets/ecosystem/${ecosystemId}?${searchParams.toString()}`,
|
||||
{
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
if (res?.status !== 200) {
|
||||
console.error("Failed to fetch in-app wallet stats");
|
||||
return null;
|
||||
}
|
||||
|
||||
const json = await res.json();
|
||||
|
||||
return json.data as EcosystemWalletStats[];
|
||||
}
|
||||
@@ -204,7 +204,7 @@ export const PROJECT_SHOWCASE_DATA = [
|
||||
description:
|
||||
"Gaming community and platform for connecting gamers across various titles and events.",
|
||||
industries: ["Games"],
|
||||
link: "@goatgamingbot",
|
||||
link: "https://goatgaming.com/",
|
||||
case_study:
|
||||
"https://blog.thirdweb.com/case-studies/how-goat-gaming-grew-by-250k-players-with-mini-games-on-telegram-with-thirdweb/",
|
||||
},
|
||||
@@ -393,7 +393,7 @@ export const PROJECT_SHOWCASE_DATA = [
|
||||
slug: "storychain",
|
||||
description:
|
||||
"Collaborative storytelling platform that uses blockchain to create, share, and own narrative content.",
|
||||
image: "ipfs://Qmau1mgrzahdfEtJFU9i2pSKMM7AwHav6tQCVEdJARUFpb",
|
||||
image: "ipfs://QmUUBdNHa7ZoiRQicNathMY6G8GTBA4S9cRY1RRBXAKpYi",
|
||||
industries: ["Social"],
|
||||
link: "https://storychain.io",
|
||||
case_study:
|
||||
|
||||
@@ -66,8 +66,6 @@ export enum PageId {
|
||||
// thirdweb.com/rpc-edge
|
||||
RPCEdgeLanding = "rpc-edge-landing",
|
||||
|
||||
// thirdweb.com/engine
|
||||
EngineLanding = "engine-landing",
|
||||
// thirdweb.com/web3-dashboard
|
||||
DashboardLanding = "dashboard-landing",
|
||||
|
||||
|
||||
@@ -1,394 +0,0 @@
|
||||
import { Box, Container, Flex } from "@chakra-ui/react";
|
||||
import { PricingEngineHomepage } from "components/homepage/sections/PricingEngine";
|
||||
import { LandingCardWithImage } from "components/landing-pages/card-with-image";
|
||||
import LandingCardWithMetrics from "components/landing-pages/card-with-metrics";
|
||||
import { LandingEndCTA } from "components/landing-pages/end-cta";
|
||||
import { Gradients } from "components/landing-pages/gradients";
|
||||
import { LandingGridSection } from "components/landing-pages/grid-section";
|
||||
import { LandingGuidesShowcase } from "components/landing-pages/guide-showcase";
|
||||
import { LandingHeroWithSideImage } from "components/landing-pages/hero-with-side-image";
|
||||
import LandingImageSectionItem from "components/landing-pages/image-section-item";
|
||||
import { LandingLayout } from "components/landing-pages/layout";
|
||||
import { LandingSectionHeading } from "components/landing-pages/section-heading";
|
||||
import { SplashImage } from "components/landing-pages/splash-image";
|
||||
import { getAbsoluteUrl } from "lib/vercel-utils";
|
||||
import { PageId } from "page-id";
|
||||
import { Heading, Text } from "tw-components";
|
||||
import type { ThirdwebNextPage } from "utils/types";
|
||||
import { OpenSource } from "../components/homepage/open-source/OpenSource";
|
||||
|
||||
const TRACKING_CATEGORY = "engine-landing";
|
||||
|
||||
export const metrics = [
|
||||
{
|
||||
title: "Stand With Crypto",
|
||||
description:
|
||||
"Championing clear crypto regulation to foster greater economic freedom via a grassroots advocacy hub.",
|
||||
image: require("../../public/assets/product-pages/engine/casestudy-image-standwithcrypto.png"),
|
||||
mobileImage: require("../../public/assets/product-pages/engine/casestudy-image-standwithcrypto.png"),
|
||||
items: [
|
||||
{
|
||||
title: "$87M+",
|
||||
description: "Raised",
|
||||
},
|
||||
{
|
||||
title: "515k+",
|
||||
description: "Crypto Advocates",
|
||||
colSpan: 2,
|
||||
},
|
||||
{
|
||||
title: "165k+",
|
||||
description: "Contacts",
|
||||
},
|
||||
],
|
||||
href: "https://www.standwithcrypto.org/",
|
||||
hoverBackground: "#0053FF",
|
||||
},
|
||||
{
|
||||
title: "InfiniGods",
|
||||
description:
|
||||
"Building a free-to-play web3 game universe — with interoperable onchain assets that players truly own.",
|
||||
image: require("../../public/assets/product-pages/engine/casestudy-image-infinigods.png"),
|
||||
mobileImage: require("../../public/assets/product-pages/engine/casestudy-image-infinigods.png"),
|
||||
items: [
|
||||
{
|
||||
title: "100k+",
|
||||
description: "Daily Users",
|
||||
},
|
||||
{
|
||||
title: "1.5M+",
|
||||
description: "Monthly Transactions",
|
||||
colSpan: 2,
|
||||
},
|
||||
{
|
||||
title: "10k+",
|
||||
description: "VIP Members",
|
||||
},
|
||||
],
|
||||
href: "https://blog.thirdweb.com/case-studies/king-of-destiny-launches-avatar-nfts-thirdweb-engine/",
|
||||
hoverBackground: "#25262B",
|
||||
},
|
||||
{
|
||||
title: "Coinbase Wallet",
|
||||
description:
|
||||
"Bringing onchain experiences to the real world — with seamless NFT creation, delivery, & transaction management.",
|
||||
image: require("../../public/assets/product-pages/engine/casestudy-image-coinbase-wallet.png"),
|
||||
mobileImage: require("../../public/assets/product-pages/engine/casestudy-image-coinbase-wallet.png"),
|
||||
items: [
|
||||
{
|
||||
title: "1,000+",
|
||||
description: "Real-World Transactions",
|
||||
colSpan: 2,
|
||||
},
|
||||
{
|
||||
title: "4 Weeks",
|
||||
description: "Total Development Time",
|
||||
colSpan: 2,
|
||||
},
|
||||
],
|
||||
href: "https://blog.thirdweb.com/case-studies/coinbase-brings-onchain-experiences-to-life/",
|
||||
hoverBackground: "#0053FF",
|
||||
},
|
||||
];
|
||||
|
||||
const GUIDES = [
|
||||
{
|
||||
title: "The Quick-Start Guide to thirdweb Engine",
|
||||
image: require("../../public/assets/product-pages/engine/guide-get-started.png"),
|
||||
link: "https://portal.thirdweb.com/engine/get-started",
|
||||
},
|
||||
{
|
||||
title: "Airdrop Tokens and NFTs to Users",
|
||||
image: require("../../public/assets/product-pages/engine/guide-airdrop.png"),
|
||||
link: "https://portal.thirdweb.com/engine/guides/airdrop-nfts",
|
||||
},
|
||||
{
|
||||
title: "Create a Gasless NFT Mint Farcaster Frame",
|
||||
image: require("../../public/assets/product-pages/engine/guide-farcaster-frame.png"),
|
||||
link: "https://blog.thirdweb.com/guides/create-an-nft-mint-farcaster-frame/",
|
||||
},
|
||||
];
|
||||
|
||||
const EngineLanding: ThirdwebNextPage = () => {
|
||||
return (
|
||||
<LandingLayout
|
||||
bgColor="#0F0F0F"
|
||||
py={0}
|
||||
seo={{
|
||||
title: "Engine: Open-Source APIs for Web3 Apps and Games",
|
||||
description:
|
||||
"Scalable smart contract APIs backed by secure wallets, with automatic nonce queuing & gas-optimized retries—on any EVM blockchain. Get started.",
|
||||
openGraph: {
|
||||
images: [
|
||||
{
|
||||
url: `${getAbsoluteUrl()}/assets/og-image/engine.png`,
|
||||
width: 1200,
|
||||
height: 630,
|
||||
alt: "thirdweb Engine",
|
||||
},
|
||||
],
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Flex
|
||||
position="relative"
|
||||
flexDir="column"
|
||||
width="100%"
|
||||
overflow="hidden"
|
||||
py={{ base: "120px", md: "80px" }}
|
||||
>
|
||||
<Container
|
||||
zIndex={3}
|
||||
position="relative"
|
||||
maxW="container.page"
|
||||
as={Flex}
|
||||
flexDir="column"
|
||||
gap={{ base: "80px", md: "180px" }}
|
||||
>
|
||||
<LandingHeroWithSideImage
|
||||
miniTitle="Engine"
|
||||
miniImage={require("../../public/assets/landingpage/engine-icon.png")}
|
||||
title={
|
||||
<>
|
||||
<Box
|
||||
as="div"
|
||||
display="inline-block"
|
||||
textTransform="capitalize"
|
||||
mr={6}
|
||||
>
|
||||
<OpenSource TRACKING_CATEGORY={TRACKING_CATEGORY} />
|
||||
</Box>
|
||||
APIs for web3 apps & games
|
||||
</>
|
||||
}
|
||||
titleWithGradient=""
|
||||
subtitle="Scalable smart contract APIs backed by secure wallets, with automatic nonce queuing & gas-optimized retries."
|
||||
trackingCategory={TRACKING_CATEGORY}
|
||||
ctaLink="/dashboard/engine"
|
||||
ctaText="Get started"
|
||||
contactUsTitle="Book a demo"
|
||||
gradient="linear(to-r, #9786DF, #9786DF)"
|
||||
lottie={require("../../public/assets/product-pages/engine/lottie.json")}
|
||||
contactUsBg="#0E0E0E"
|
||||
contactUsHover={{ background: "#0E0E0E" }}
|
||||
/>
|
||||
<SplashImage
|
||||
title="Autoscale"
|
||||
titleWithGradient="your app"
|
||||
subtitle="Mint tokens and perform onchain actions with robust backend wallets — equipped with automatic nonce management, transaction queueing, and gas-optimized retries."
|
||||
trackingCategory={TRACKING_CATEGORY}
|
||||
ctaText="Deploy an instance"
|
||||
ctaLink="/dashboard/engine"
|
||||
gradient="linear(to-r, #FFFFFF, #FFFFFF)"
|
||||
lottie={require("../../public/assets/product-pages/engine/lottie2.json")}
|
||||
/>
|
||||
<LandingGridSection
|
||||
desktopColumns={3}
|
||||
title={
|
||||
<Box
|
||||
display="flex"
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
marginBottom={{ base: "35px", md: "55px" }}
|
||||
>
|
||||
<Box width="full" maxWidth="1000px" textAlign="center">
|
||||
<LandingSectionHeading
|
||||
title="What can you build with Engine?"
|
||||
blackToWhiteTitle=""
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
}
|
||||
>
|
||||
<LandingImageSectionItem
|
||||
image={require("../../public/assets/landingpage/desktop/scale-your-app-to-millions.png")}
|
||||
mobileImage={require("../../public/assets/landingpage/desktop/scale-your-app-to-millions.png")}
|
||||
title="Scale your app to millions"
|
||||
description="Eliminate stuck transactions and scale your app with automatic nonce management and gas-optimized transaction retries."
|
||||
justifyContent="flex-end"
|
||||
/>
|
||||
|
||||
<LandingImageSectionItem
|
||||
image={require("../../public/assets/landingpage/desktop/create-the-best-ux.png")}
|
||||
mobileImage={require("../../public/assets/landingpage/desktop/create-the-best-ux.png")}
|
||||
title="Create the best user experience"
|
||||
description="Abstract the blockchain away from users — removing wallet creation, gas fees, & signing. Powered by Account Abstraction."
|
||||
/>
|
||||
|
||||
<LandingImageSectionItem
|
||||
image={require("../../public/assets/landingpage/desktop/trigger-onchain-actions.png")}
|
||||
mobileImage={require("../../public/assets/landingpage/desktop/trigger-onchain-actions.png")}
|
||||
title="Trigger onchain actions with webhooks"
|
||||
description="Mint tokens, read & write to smart contracts, & perform onchain transactions with purpose-built webhooks."
|
||||
/>
|
||||
</LandingGridSection>
|
||||
|
||||
<LandingGridSection
|
||||
title={
|
||||
<Box
|
||||
display="flex"
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
w="full"
|
||||
marginBottom="38px"
|
||||
>
|
||||
<LandingSectionHeading
|
||||
title="Solutions for every web3-powered feature"
|
||||
blackToWhiteTitle=""
|
||||
/>
|
||||
</Box>
|
||||
}
|
||||
desktopColumns={4}
|
||||
>
|
||||
<LandingCardWithImage
|
||||
title="Wallet Management"
|
||||
description="Create backend wallets you can programatically use with automatic nonce and gas management. Eliminate gas spikes, stuck transactions and network instability."
|
||||
image={require("../../public/assets/landingpage/account-abstraction-desktop.png")}
|
||||
mobileImage={require("../../public/assets/landingpage/account-abstraction-mobile.png")}
|
||||
TRACKING_CATEGORY={TRACKING_CATEGORY}
|
||||
href="https://portal.thirdweb.com/engine/features/backend-wallets"
|
||||
direction="horizontal"
|
||||
/>
|
||||
<LandingCardWithImage
|
||||
title="Smart Contracts"
|
||||
description="Deploy, read, & write to any smart contract across any EVM-compatible blockchain — and build with thirdweb's audited smart contracts."
|
||||
image={require("../../public/assets/landingpage/smart-contract-audits-desktop.png")}
|
||||
mobileImage={require("../../public/assets/landingpage/smart-contract-audits-mobile.png")}
|
||||
TRACKING_CATEGORY={TRACKING_CATEGORY}
|
||||
href="https://portal.thirdweb.com/engine/features/contracts"
|
||||
colSpan={2}
|
||||
/>
|
||||
|
||||
<LandingCardWithImage
|
||||
title="Web3 Auth"
|
||||
description="Create permissions to enable users' wallets to directly interact with certain endpoints on the thirdweb Engine."
|
||||
image={require("../../public/assets/product-pages/connect/desktop-auth.png")}
|
||||
mobileImage={require("../../public/assets/product-pages/connect/mobile-auth.png")}
|
||||
TRACKING_CATEGORY={TRACKING_CATEGORY}
|
||||
href="/connect"
|
||||
colSpan={1}
|
||||
/>
|
||||
<LandingCardWithImage
|
||||
title="Account Abstraction"
|
||||
description="Deploy and manage smart wallets, use session keys for access controls, and transact on behalf of your users."
|
||||
image={require("../../public/assets/landingpage/account-desktop.png")}
|
||||
mobileImage={require("../../public/assets/landingpage/account-mobile.png")}
|
||||
TRACKING_CATEGORY={TRACKING_CATEGORY}
|
||||
href="https://portal.thirdweb.com/engine/features/account-abstraction"
|
||||
direction="horizontal"
|
||||
/>
|
||||
<LandingCardWithImage
|
||||
title="Gasless Transactions"
|
||||
description="Sponsor user transactions with gasless relayers and user operations."
|
||||
image={require("../../public/assets/landingpage/transaction-fee-desktop.png")}
|
||||
mobileImage={require("../../public/assets/landingpage/transaction-fee-mobile.png")}
|
||||
TRACKING_CATEGORY={TRACKING_CATEGORY}
|
||||
href="https://portal.thirdweb.com/engine/features/gasless-transactions"
|
||||
colSpan={1}
|
||||
/>
|
||||
<LandingCardWithImage
|
||||
title="High transaction throughput"
|
||||
description="Blockchain transactions are processed in parallel with nonce management, and stuck transactions are automatically retried."
|
||||
image={require("../../public/assets/landingpage/desktop/happy-people.png")}
|
||||
mobileImage={require("../../public/assets/landingpage/mobile/happy-people.png")}
|
||||
TRACKING_CATEGORY={TRACKING_CATEGORY}
|
||||
href="/dashboard/engine"
|
||||
/>
|
||||
<LandingCardWithImage
|
||||
title="Advanced analytics"
|
||||
description="View transaction history trends, event logs for each transaction, a ledger of backend wallet funds, and more."
|
||||
image={require("../../public/assets/landingpage/desktop/analytics-v3.png")}
|
||||
mobileImage={require("../../public/assets/landingpage/mobile/analytics-v3.png")}
|
||||
TRACKING_CATEGORY={TRACKING_CATEGORY}
|
||||
href="/dashboard/engine"
|
||||
direction="horizontal"
|
||||
colSpan={2}
|
||||
/>
|
||||
<LandingCardWithImage
|
||||
title="Infrastructure"
|
||||
description="Built-in infrastructure so you don't have to worry about RPCs, storage, bundlers or paymasters."
|
||||
image={require("../../public/assets/landingpage/infastructure-desktop.png")}
|
||||
mobileImage={require("../../public/assets/landingpage/infastructure-mobile.png")}
|
||||
TRACKING_CATEGORY={TRACKING_CATEGORY}
|
||||
href="/rpc-edge"
|
||||
direction="horizontal"
|
||||
/>
|
||||
<LandingCardWithImage
|
||||
title="Self-hosted or Managed"
|
||||
description="Set up on your own server for free with minimal configuration or use our managed service."
|
||||
image={require("../../public/assets/landingpage/selfhost.png")}
|
||||
mobileImage={require("../../public/assets/landingpage/selfhost.png")}
|
||||
TRACKING_CATEGORY={TRACKING_CATEGORY}
|
||||
href="/dashboard/engine"
|
||||
/>
|
||||
</LandingGridSection>
|
||||
|
||||
<LandingCardWithMetrics
|
||||
title={
|
||||
<div className="flex flex-col items-center justify-center text-center">
|
||||
<Heading size="display.sm" color="white">
|
||||
Trusted by the best
|
||||
</Heading>
|
||||
|
||||
<Text fontSize={[16, 20]} mt={6}>
|
||||
thirdweb Engine powers the best onchain apps, integrating into
|
||||
any backend at scale.
|
||||
</Text>
|
||||
</div>
|
||||
}
|
||||
desktopColumns={3}
|
||||
TRACKING_CATEGORY={TRACKING_CATEGORY}
|
||||
metrics={metrics}
|
||||
/>
|
||||
|
||||
<PricingEngineHomepage trackingCategory={TRACKING_CATEGORY} />
|
||||
|
||||
<LandingGuidesShowcase
|
||||
title="Get started with thirdweb Engine"
|
||||
category={TRACKING_CATEGORY}
|
||||
description="Integrate powerful web3 infrastructure that scales as you grow."
|
||||
solution="Engine"
|
||||
guides={GUIDES}
|
||||
/>
|
||||
<LandingEndCTA
|
||||
title="Start building with"
|
||||
titleWithGradient="thirdweb Engine."
|
||||
trackingCategory={TRACKING_CATEGORY}
|
||||
ctaText="Get started"
|
||||
ctaLink="/dashboard/engine"
|
||||
contactUsTitle="Book a demo"
|
||||
gradient="linear(to-r, #FFFFFF, #FFFFFF)"
|
||||
contactUsBg="#0E0E0E"
|
||||
contactUsHover={{ background: "#0E0E0E" }}
|
||||
contactUsBorder="none"
|
||||
/>
|
||||
</Container>
|
||||
|
||||
<Gradients
|
||||
top={{
|
||||
top: 100,
|
||||
left: 0,
|
||||
right: 0,
|
||||
height: "2286px",
|
||||
width: "100%",
|
||||
background: `url("/assets/landingpage/top-gradient.svg")`,
|
||||
backgroundSize: { base: "cover", lg: "100% 100%" },
|
||||
}}
|
||||
bottom={{
|
||||
bottom: -200,
|
||||
left: 0,
|
||||
right: 0,
|
||||
height: "1386px",
|
||||
width: "100%",
|
||||
background: `url("/assets/landingpage/bottom-gradient.png")`,
|
||||
backgroundSize: { base: "cover", lg: "100% 100%" },
|
||||
}}
|
||||
/>
|
||||
</Flex>
|
||||
</LandingLayout>
|
||||
);
|
||||
};
|
||||
|
||||
EngineLanding.pageId = PageId.EngineLanding;
|
||||
|
||||
export default EngineLanding;
|
||||
@@ -1,449 +0,0 @@
|
||||
import { Container, Flex, SimpleGrid } from "@chakra-ui/react";
|
||||
import LandingCardWithMetrics from "components/landing-pages/card-with-metrics";
|
||||
import { LandingEndCTA } from "components/landing-pages/end-cta";
|
||||
import { LandingGridSection } from "components/landing-pages/grid-section";
|
||||
import { LandingHero } from "components/landing-pages/hero";
|
||||
import { LandingIconSectionItem } from "components/landing-pages/icon-section-item";
|
||||
import { LandingLayout } from "components/landing-pages/layout";
|
||||
import { LandingOptionSelector } from "components/landing-pages/option-selector";
|
||||
import { ImageCard } from "components/product-pages/common/ImageCard";
|
||||
import { getAbsoluteUrl } from "lib/vercel-utils";
|
||||
import { PageId } from "page-id";
|
||||
import type { ReactNode } from "react";
|
||||
import {
|
||||
Card,
|
||||
type CardProps,
|
||||
Heading,
|
||||
Text,
|
||||
TrackedLink,
|
||||
} from "tw-components";
|
||||
import type { ThirdwebNextPage } from "utils/types";
|
||||
|
||||
const TRACKING_CATEGORY = "gaming_kit";
|
||||
|
||||
const SELECTOR_ITEMS = [
|
||||
{
|
||||
title: "Frictionless onboarding experience",
|
||||
description:
|
||||
"Get users onboarded quickly with familiar email and social log-ins— we create game wallets on behalf of users. Purchases can be made via credit card, Apple/Google Pay.",
|
||||
steps: [
|
||||
"User sign-in to app with their email or social log-in",
|
||||
"Wallet is created on behalf of the user abstracting away crypto entirely",
|
||||
"User interacts with app seamlessly— without disruptive transaction pop-ups",
|
||||
],
|
||||
products: ["connect", "embedded-wallets", "smart-wallet"],
|
||||
},
|
||||
{
|
||||
title: "Uninterrupted game play",
|
||||
description:
|
||||
"Give users uninterrupted game play by removing disruptive transaction pop-ups. Generate on-chain transactions reliably and at scale on behalf of users.",
|
||||
steps: [
|
||||
"User completes a level in game and is rewarded with an in-game digital asset",
|
||||
"Developer sponsors the transaction so that user does not have to pay for gas fees",
|
||||
"User receives in-game asset without any interruption to game play",
|
||||
],
|
||||
products: [
|
||||
"embedded-wallets",
|
||||
"smart-wallet",
|
||||
"sponsored-transactions",
|
||||
"engine",
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Subscription and All Access passes",
|
||||
description:
|
||||
"Create digital loyalty passes that provide access to early gameplay content, exclusive NFT drops, and in-game boosts",
|
||||
steps: [
|
||||
"User selects a digital loyalty pass NFT when they login",
|
||||
"Loyalty pass is used to unlock in-game experiences",
|
||||
"User collects rewards to upgrade their loyalty pass",
|
||||
],
|
||||
products: ["explore", "interact"],
|
||||
},
|
||||
{
|
||||
title: "Interoperable game ecosystems",
|
||||
description:
|
||||
"Give a unified interface to users to login across your gaming ecosystem with the flexibility to import/export their digital assets",
|
||||
steps: [
|
||||
"User logs in using existing game login",
|
||||
"Game links User's gaming wallet to game issued account",
|
||||
"User selects assets they want to import and gives game scoped access to use assets for duration of game play",
|
||||
],
|
||||
products: ["auth", "smart-wallet", "engine"],
|
||||
},
|
||||
{
|
||||
title: "Game appchain",
|
||||
description:
|
||||
"Connect seamlessly to any EVM compatible L1, L2 blockchains as well as build your game on your own app chain.",
|
||||
steps: [
|
||||
"Game devs can add their app chain to thirdweb Dashboard and SDK's",
|
||||
"An app chain landing page is generated with links to SDKs, contract deployment and infrastructure for their app chain",
|
||||
"Developers can easily deploy contracts to their app chain in just a few clicks and get code snippets to integrate contracts into their games",
|
||||
],
|
||||
products: ["rpc-edge", "explore", "interact", "engine"],
|
||||
},
|
||||
{
|
||||
title: "Marketplace for digital assets",
|
||||
description:
|
||||
"Generate revenue through the launch of your own on-chain marketplace",
|
||||
steps: [
|
||||
"Deploy marketplace contract",
|
||||
"Set % platform fee (% collected by game dev on every in-game asset sale, e.g. when buyer buys tokens from listing)",
|
||||
"Players can list and trade NFTs for sale at a fixed price",
|
||||
],
|
||||
products: ["explore", "interact", "engine", "storage"],
|
||||
},
|
||||
];
|
||||
|
||||
export const metrics = [
|
||||
{
|
||||
title: "Pixels",
|
||||
description:
|
||||
"Creating an open-world web3 game with a thriving ecosystem — with in-game tokens, VIP memberships, and digital assets that users own onchain.",
|
||||
image: require("../../../public/assets/landingpage/pixels.png"),
|
||||
mobileImage: require("../../../public/assets/landingpage/mobile-pixels.png"),
|
||||
items: [],
|
||||
href: "https://blog.thirdweb.com/case-studies/pixels-builds-an-onchain-ecosystem-for-its-web3-game",
|
||||
hoverBackground: "#622AFF",
|
||||
},
|
||||
{
|
||||
title: "Gala Games",
|
||||
description:
|
||||
"Partnering with DreamWorks to integrate their brand IP into the VOXverse — and bringing fans onchain with interoperable digital collectibles.",
|
||||
image: require("../../../public/assets/solutions-pages/gaming/case-study-gala.png"),
|
||||
mobileImage: require("../../../public/assets/solutions-pages/gaming/mobile-case-study-gala.png"),
|
||||
items: [],
|
||||
href: "https://blog.thirdweb.com/case-studies/dreamworks-launches-nft-avatars-for-web3-games-with-gala-and-the-sims-creators-voxverse/",
|
||||
hoverBackground: "#0053FF",
|
||||
},
|
||||
{
|
||||
title: "Paima Studios",
|
||||
description:
|
||||
"Bringing onchain games to all players — with instant onboarding, an `invisible` wallet experience, and the smoothest gameplay with signless & gasless UX.",
|
||||
image: require("../../../public/assets/solutions-pages/gaming/case-study-ztx.png"),
|
||||
mobileImage: require("../../../public/assets/solutions-pages/gaming/mobile-case-study-ztx.png"),
|
||||
items: [],
|
||||
href: "https://paimastudios.com",
|
||||
hoverBackground: "#00B477",
|
||||
},
|
||||
];
|
||||
|
||||
const SolutionsGamingCard = ({
|
||||
children,
|
||||
href,
|
||||
label,
|
||||
...rest
|
||||
}: { children: ReactNode; href: string; label: string } & CardProps) => {
|
||||
return (
|
||||
<TrackedLink
|
||||
category={TRACKING_CATEGORY}
|
||||
href={href}
|
||||
label={label}
|
||||
isExternal
|
||||
color="blue.400"
|
||||
_hover={{ textDecor: "none" }}
|
||||
role="group"
|
||||
h="full"
|
||||
textDecoration="none"
|
||||
>
|
||||
<Card
|
||||
p={8}
|
||||
transition="border-color 200ms ease, box-shadow 200ms ease, transform 200ms ease"
|
||||
_hover={{
|
||||
borderColor: "blue.500",
|
||||
boxShadow: "0 0 16px hsl(215deg 100% 60% / 30%)",
|
||||
transform: "scale(1.01)",
|
||||
}}
|
||||
cursor="pointer"
|
||||
h="full"
|
||||
{...rest}
|
||||
>
|
||||
{children}
|
||||
</Card>
|
||||
</TrackedLink>
|
||||
);
|
||||
};
|
||||
|
||||
const SolutionsGaming: ThirdwebNextPage = () => {
|
||||
return (
|
||||
<LandingLayout
|
||||
bgColor="#0F0F0F"
|
||||
seo={{
|
||||
title: "The complete stack for scalable web3 games",
|
||||
description:
|
||||
"Add web3 features to your game on all platforms, including: Native, Mobile, Console, Browser and VR.",
|
||||
openGraph: {
|
||||
images: [
|
||||
{
|
||||
url: `${getAbsoluteUrl()}/assets/og-image/gaming-solutions.png`,
|
||||
width: 1200,
|
||||
height: 630,
|
||||
alt: "thirdweb gaming",
|
||||
},
|
||||
],
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Container
|
||||
maxW="container.page"
|
||||
as={Flex}
|
||||
flexDir="column"
|
||||
gap={{ base: "80px", md: "120px" }}
|
||||
>
|
||||
<LandingHero
|
||||
title="The complete stack for"
|
||||
titleWithGradient="scalable web3 games"
|
||||
subtitle="Everything you need to build seamless onchain games with web3-powered features — on Unity, Unreal Engine, & any platform."
|
||||
trackingCategory={TRACKING_CATEGORY}
|
||||
ctaLink="https://portal.thirdweb.com/gaming"
|
||||
gradient="linear(to-r, #F213A4, #F97CCE)"
|
||||
image={require("../../../public/assets/product-pages/hero/desktop-hero-gaming.png")}
|
||||
mobileImage={require("../../../public/assets/product-pages/hero/mobile-hero-gaming.png")}
|
||||
contactUsTitle="Book Demo"
|
||||
/>
|
||||
|
||||
<LandingGridSection>
|
||||
<LandingIconSectionItem
|
||||
icon={require("../../../public/assets/solutions-pages/icons/icon-fee.svg")}
|
||||
title="Unlock new revenue streams"
|
||||
description="Primary sales and royalty fees from secondary sales for in-game assets represented as NFTs."
|
||||
/>
|
||||
<LandingIconSectionItem
|
||||
icon={require("../../../public/assets/solutions-pages/icons/icon-scale.svg")}
|
||||
title="Increase user retention"
|
||||
description="Reward your players with in game currency and digital assets."
|
||||
/>
|
||||
<LandingIconSectionItem
|
||||
icon={require("../../../public/assets/solutions-pages/icons/icon-guest.svg")}
|
||||
title="Engage with your community"
|
||||
description="Enable access to other games within the studio seamlessly by creating an immersive ecosystem."
|
||||
/>
|
||||
</LandingGridSection>
|
||||
<Flex alignItems="center" flexDirection="column">
|
||||
<Heading
|
||||
as="h2"
|
||||
size="display.sm"
|
||||
textAlign="center"
|
||||
mb={12}
|
||||
maxW={800}
|
||||
>
|
||||
Build games on any platform
|
||||
</Heading>
|
||||
<SimpleGrid
|
||||
justifyContent="flex-start"
|
||||
columns={{ base: 1, md: 3 }}
|
||||
gap={{ base: 12, md: 6 }}
|
||||
>
|
||||
<ImageCard
|
||||
title="Unity"
|
||||
image={require("../../../public/assets/solutions-pages/gaming/gaming-unity.png")}
|
||||
TRACKING_CATEGORY={TRACKING_CATEGORY}
|
||||
href="https://portal.thirdweb.com/unity"
|
||||
linkTitle="Read the Unity SDK docs"
|
||||
label="unity"
|
||||
/>
|
||||
<ImageCard
|
||||
title="Unreal Engine"
|
||||
image={require("../../../public/assets/solutions-pages/gaming/gaming-unreal.png")}
|
||||
TRACKING_CATEGORY={TRACKING_CATEGORY}
|
||||
href="https://portal.thirdweb.com/solutions/gaming/unreal-engine/quickstart"
|
||||
linkTitle="Read the Unreal Engine docs"
|
||||
label="unreal-engine"
|
||||
/>
|
||||
<ImageCard
|
||||
title="Mobile"
|
||||
image={require("../../../public/assets/solutions-pages/gaming/gaming-phone.png")}
|
||||
TRACKING_CATEGORY={TRACKING_CATEGORY}
|
||||
linkTitle="Read the React Native docs"
|
||||
href="https://portal.thirdweb.com/react-native/latest"
|
||||
label="mobile"
|
||||
/>
|
||||
</SimpleGrid>
|
||||
</Flex>
|
||||
|
||||
<Flex alignItems="center" flexDirection="column">
|
||||
<Heading
|
||||
as="h2"
|
||||
size="display.sm"
|
||||
textAlign="center"
|
||||
mb={12}
|
||||
maxW={800}
|
||||
>
|
||||
Create new gaming universes
|
||||
</Heading>
|
||||
<SimpleGrid
|
||||
justifyContent="flex-start"
|
||||
columns={{ base: 1, md: 3 }}
|
||||
gap={{ base: 12, md: 6 }}
|
||||
>
|
||||
<ImageCard
|
||||
title="Web3 Warriors"
|
||||
image={require("../../../public/assets/solutions-pages/gaming/web3warriors.png")}
|
||||
TRACKING_CATEGORY={TRACKING_CATEGORY}
|
||||
href="https://web3warriors.thirdweb.com"
|
||||
category="Unity"
|
||||
linkTitle="Play Web3 Warriors"
|
||||
label="web3warriors"
|
||||
>
|
||||
A full-scale action RPG — built in 3 weeks using thirdweb's
|
||||
Unity SDK.
|
||||
</ImageCard>
|
||||
|
||||
<ImageCard
|
||||
title="Cat Attack"
|
||||
image={require("../../../public/assets/solutions-pages/gaming/catattack-square.png")}
|
||||
TRACKING_CATEGORY={TRACKING_CATEGORY}
|
||||
href="https://catattack.thirdweb.com"
|
||||
category="Mobile"
|
||||
linkTitle="Play Cat Attack"
|
||||
label="cat-attack"
|
||||
>
|
||||
A simple web3 mobile game — gone viral & built in 2 days using
|
||||
thirdweb.
|
||||
</ImageCard>
|
||||
|
||||
<ImageCard
|
||||
title="Play TappyFowl"
|
||||
image={require("../../../public/assets/solutions-pages/gaming/gaming-tappyfowl.png")}
|
||||
TRACKING_CATEGORY={TRACKING_CATEGORY}
|
||||
href="https://tappyfowl.thirdweb.com/"
|
||||
category="Unreal Engine"
|
||||
linkTitle="Play TappyFowl"
|
||||
label="tappyfowl"
|
||||
>
|
||||
Guide your fowl through a fantastical landscape that requires
|
||||
quick reflexes and precise timing.
|
||||
</ImageCard>
|
||||
</SimpleGrid>
|
||||
</Flex>
|
||||
|
||||
<LandingOptionSelector
|
||||
items={SELECTOR_ITEMS}
|
||||
TRACKING_CATEGORY={TRACKING_CATEGORY}
|
||||
blackToWhiteTitle=""
|
||||
title="What You Can Build"
|
||||
/>
|
||||
|
||||
<LandingGridSection
|
||||
title={
|
||||
<div className="flex flex-col items-center justify-center text-center">
|
||||
<Heading size="display.sm" color="white">
|
||||
The complete stack for web3 games
|
||||
</Heading>
|
||||
|
||||
<Text size="body.lg" mt={6}>
|
||||
Everything you need to build full-featured onchain games.
|
||||
</Text>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<SolutionsGamingCard
|
||||
p={8}
|
||||
label="unreal-engine"
|
||||
href="https://portal.thirdweb.com/gaming"
|
||||
>
|
||||
<LandingIconSectionItem
|
||||
icon={require("../../../public/assets/solutions-pages/icons/icon-tool.svg")}
|
||||
title="SDKs & APIs"
|
||||
description="Integrate web3-powered features into your game with best-in-class SDKs and APIs for any platform — including Unity & Unreal Engine."
|
||||
/>
|
||||
</SolutionsGamingCard>
|
||||
<SolutionsGamingCard p={8} label="connect-wallet" href="/connect">
|
||||
<LandingIconSectionItem
|
||||
icon={require("../../../public/assets/solutions-pages/icons/icon-smart-wallet.svg")}
|
||||
title="Connect"
|
||||
description="Onboard all of your users with a beautiful Connect Wallet modal, flexible sign-in options for web2 & web3, and powerful hooks for full customizability."
|
||||
/>
|
||||
</SolutionsGamingCard>
|
||||
<SolutionsGamingCard
|
||||
p={8}
|
||||
label="embedded-wallet"
|
||||
href="/embedded-wallets"
|
||||
>
|
||||
<LandingIconSectionItem
|
||||
icon={require("../../../public/assets/solutions-pages/icons/icon-email-signin.svg")}
|
||||
title="In-App Wallet"
|
||||
description="Give any user the keys to web3 with familiar web2 login flows. Choose from non-custodial or custodial solutions & enable users to sign in with an email, phone number, or social account."
|
||||
/>
|
||||
</SolutionsGamingCard>
|
||||
<SolutionsGamingCard
|
||||
p={8}
|
||||
label="smart-wallet"
|
||||
href="/account-abstraction"
|
||||
>
|
||||
<LandingIconSectionItem
|
||||
icon={require("../../../public/assets/solutions-pages/icons/icon-verified.svg")}
|
||||
title="Account Abstraction"
|
||||
description="Transform your app's user experience with signless transactions, multi-signature security, account recovery and more."
|
||||
/>
|
||||
</SolutionsGamingCard>
|
||||
<SolutionsGamingCard
|
||||
p={8}
|
||||
label="sponsored-transaction"
|
||||
href="/sponsored-transactions"
|
||||
>
|
||||
<LandingIconSectionItem
|
||||
icon={require("../../../public/assets/solutions-pages/icons/icon-fee.svg")}
|
||||
title="Sponsored Transactions"
|
||||
description="Create seamless web3 UX by sponsoring users' gas fees — for any & all transactions. No more disruptive transaction popups or bridging & swapping funds."
|
||||
/>
|
||||
</SolutionsGamingCard>
|
||||
<SolutionsGamingCard p={8} label="api" href="/engine">
|
||||
<LandingIconSectionItem
|
||||
icon={require("../../../public/assets/solutions-pages/icons/icon-build.svg")}
|
||||
title="Engine"
|
||||
description="Power your Web3 app with production-grade APIs, including
|
||||
auth, smart contracts, backend wallets, gasless transactions,
|
||||
and managed infrastructure."
|
||||
/>
|
||||
</SolutionsGamingCard>
|
||||
<SolutionsGamingCard p={8} label="web3-wallets" href="/connect">
|
||||
<LandingIconSectionItem
|
||||
icon={require("../../../public/assets/solutions-pages/icons/icon-verified-user.svg")}
|
||||
title="Auth"
|
||||
description="Easy auth for the most popular web3 wallets and web2 login flows — so you can verify your users' identities & prove wallet ownership to off-chain systems."
|
||||
/>
|
||||
</SolutionsGamingCard>
|
||||
<SolutionsGamingCard p={8} label="smart-contract" href="/explore">
|
||||
<LandingIconSectionItem
|
||||
icon={require("../../../public/assets/solutions-pages/icons/icon-tool.svg")}
|
||||
title="Interact"
|
||||
description="The complete toolkit to add any smart contract into your apps — and call functions for any type of onchain interaction."
|
||||
/>
|
||||
</SolutionsGamingCard>
|
||||
</LandingGridSection>
|
||||
|
||||
<LandingCardWithMetrics
|
||||
title={
|
||||
<div className="flex flex-col items-center justify-center text-center">
|
||||
<Heading size="display.sm" color="white">
|
||||
Trusted by the best
|
||||
</Heading>
|
||||
|
||||
<Text size="body.lg" mt={6}>
|
||||
Powering web3 apps across verticals — from onchain games to
|
||||
creator platforms.
|
||||
</Text>
|
||||
</div>
|
||||
}
|
||||
desktopColumns={3}
|
||||
TRACKING_CATEGORY={TRACKING_CATEGORY}
|
||||
metrics={metrics}
|
||||
/>
|
||||
|
||||
<LandingEndCTA
|
||||
title="Start building"
|
||||
titleWithGradient="today."
|
||||
trackingCategory={TRACKING_CATEGORY}
|
||||
ctaLink="https://portal.thirdweb.com/gaming"
|
||||
gradient="linear(to-r, #F213A4, #F97CCE)"
|
||||
contactUsTitle="Book Demo"
|
||||
/>
|
||||
</Container>
|
||||
</LandingLayout>
|
||||
);
|
||||
};
|
||||
|
||||
SolutionsGaming.pageId = PageId.SolutionsGaming;
|
||||
|
||||
export default SolutionsGaming;
|
||||
@@ -8,6 +8,7 @@ setThirdwebDomains({
|
||||
storage: process.env.NEXT_PUBLIC_STORAGE_URL,
|
||||
bundler: process.env.NEXT_PUBLIC_BUNDLER_URL,
|
||||
pay: process.env.NEXT_PUBLIC_PAY_URL,
|
||||
analytics: process.env.NEXT_PUBLIC_ANALYTICS_URL,
|
||||
});
|
||||
|
||||
const isDev =
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
{
|
||||
"name": "thirdweb/chains (tree-shaking)",
|
||||
"path": "./dist/esm/exports/chains.js",
|
||||
"limit": "500 B",
|
||||
"limit": "600 B",
|
||||
"import": "{ ethereum }"
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,5 +1,42 @@
|
||||
# thirdweb
|
||||
|
||||
## 5.65.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#5277](https://github.com/thirdweb-dev/js/pull/5277) [`58fb28d`](https://github.com/thirdweb-dev/js/commit/58fb28de297e5a81be18a185d495425e29913a0b) Thanks [@joaquim-verges](https://github.com/joaquim-verges)! - Respect bundlerUrl in waitForUserReceipt
|
||||
|
||||
## 5.65.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- [#5126](https://github.com/thirdweb-dev/js/pull/5126) [`c621c13`](https://github.com/thirdweb-dev/js/commit/c621c13f11166a5ff8aa1fbd9e5e78dd83cbaef5) Thanks [@kien-ngo](https://github.com/kien-ngo)! - Allow to customize the display order of Asset tabs
|
||||
|
||||
When you click on "View Assets", by default the "Tokens" tab is shown first.
|
||||
|
||||
If you want to show the "NFTs" tab first, change the order of the asset tabs to: ["nft", "token"]
|
||||
|
||||
Note: If an empty array is passed, the [View Funds] button will be hidden
|
||||
|
||||
```tsx
|
||||
<ConnectButton
|
||||
client={client}
|
||||
detailsModal={{
|
||||
assetTabs: ["nft", "token"],
|
||||
}}
|
||||
/>
|
||||
```
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#5253](https://github.com/thirdweb-dev/js/pull/5253) [`baf2198`](https://github.com/thirdweb-dev/js/commit/baf21980be291d037797ce17fcd2e8a64e3b7814) Thanks [@joaquim-verges](https://github.com/joaquim-verges)! - Use stringify instead of JSON.stringify in most places to handle bigint serialization
|
||||
|
||||
- [#5272](https://github.com/thirdweb-dev/js/pull/5272) [`e3c0af2`](https://github.com/thirdweb-dev/js/commit/e3c0af28f7531bccbcc38f3f4ffb3516151636de) Thanks [@gregfromstl](https://github.com/gregfromstl)! - Tracks transaction chain IDs
|
||||
|
||||
- [#5250](https://github.com/thirdweb-dev/js/pull/5250) [`f40d247`](https://github.com/thirdweb-dev/js/commit/f40d2474e6cc1227a8d05151210ef4793beb6142) Thanks [@joaquim-verges](https://github.com/joaquim-verges)! - Allow smart accounts to switch chains between zk and non zk chains
|
||||
|
||||
- [#5270](https://github.com/thirdweb-dev/js/pull/5270) [`0660416`](https://github.com/thirdweb-dev/js/commit/06604162a11d99ff119bbf427a24b9a7d82c1575) Thanks [@gregfromstl](https://github.com/gregfromstl)! - Enable configuring the analytics endpoint
|
||||
|
||||
## 5.64.5
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "thirdweb",
|
||||
"version": "5.64.5",
|
||||
"version": "5.65.1",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/thirdweb-dev/js.git#main"
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import type { ThirdwebClient } from "../../client/client.js";
|
||||
import { getThirdwebBaseUrl } from "../../utils/domains.js";
|
||||
import { getClientFetch } from "../../utils/fetch.js";
|
||||
import { stringify } from "../../utils/json.js";
|
||||
import type { Ecosystem } from "../../wallets/in-app/core/wallet/types.js";
|
||||
|
||||
const ANALYTICS_ENDPOINT = "https://c.thirdweb.com/event";
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
@@ -23,7 +22,7 @@ export async function track({
|
||||
...data,
|
||||
};
|
||||
|
||||
return fetch(ANALYTICS_ENDPOINT, {
|
||||
return fetch(`${getThirdwebBaseUrl("analytics")}/event`, {
|
||||
method: "POST",
|
||||
body: stringify(event),
|
||||
}).catch(() => {});
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { stringify } from "../../utils/json.js";
|
||||
import { decodeJWT } from "../../utils/jwt/decode-jwt.js";
|
||||
import type { JWTPayload } from "../../utils/jwt/types.js";
|
||||
import { verifyEOASignature } from "../verify-signature.js";
|
||||
@@ -82,7 +83,7 @@ export function verifyJWT(options: AuthOptions) {
|
||||
}
|
||||
|
||||
const verified = await verifyEOASignature({
|
||||
message: JSON.stringify(payload),
|
||||
message: stringify(payload),
|
||||
signature,
|
||||
address: issuerAddress,
|
||||
});
|
||||
|
||||
@@ -16,6 +16,7 @@ import { sendAndConfirmTransaction } from "../../transaction/actions/send-and-co
|
||||
import { resolvePromisedValue } from "../../utils/promise/resolve-promised-value.js";
|
||||
import { toEther } from "../../utils/units.js";
|
||||
import { generateMerkleTreeInfoERC1155 } from "../airdrop/write/merkleInfoERC1155.js";
|
||||
import { name } from "../common/read/name.js";
|
||||
import { deployERC20Contract } from "../prebuilts/deploy-erc20.js";
|
||||
import { deployERC1155Contract } from "../prebuilts/deploy-erc1155.js";
|
||||
import { balanceOf } from "./__generated__/IERC1155/read/balanceOf.js";
|
||||
@@ -45,12 +46,22 @@ describe.runIf(process.env.TW_SECRET_KEY)(
|
||||
chain: ANVIL_CHAIN,
|
||||
client: TEST_CLIENT,
|
||||
params: {
|
||||
name: "Test DropERC1155",
|
||||
name: "EditionDrop",
|
||||
contractURI: TEST_CONTRACT_URI,
|
||||
},
|
||||
type: "DropERC1155",
|
||||
});
|
||||
|
||||
expect(contractAddress).toBeDefined();
|
||||
const deployedName = await name({
|
||||
contract: getContract({
|
||||
client: TEST_CLIENT,
|
||||
chain: ANVIL_CHAIN,
|
||||
address: contractAddress,
|
||||
}),
|
||||
});
|
||||
expect(deployedName).toBe("EditionDrop");
|
||||
|
||||
contract = getContract({
|
||||
address: contractAddress,
|
||||
chain: ANVIL_CHAIN,
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
import { resolveContractAbi } from "../../contract/actions/resolve-abi.js";
|
||||
import { type ThirdwebContract, getContract } from "../../contract/contract.js";
|
||||
import { sendAndConfirmTransaction } from "../../transaction/actions/send-and-confirm-transaction.js";
|
||||
import { name } from "../common/read/name.js";
|
||||
import { deployERC1155Contract } from "../prebuilts/deploy-erc1155.js";
|
||||
import { balanceOf } from "./__generated__/IERC1155/read/balanceOf.js";
|
||||
import { totalSupply } from "./__generated__/IERC1155/read/totalSupply.js";
|
||||
@@ -37,12 +38,22 @@ describe.runIf(process.env.TW_SECRET_KEY)("TokenERC1155", () => {
|
||||
chain: ANVIL_CHAIN,
|
||||
client: TEST_CLIENT,
|
||||
params: {
|
||||
name: "Test TokenERC1155",
|
||||
name: "Edition",
|
||||
contractURI: TEST_CONTRACT_URI,
|
||||
},
|
||||
type: "TokenERC1155",
|
||||
});
|
||||
|
||||
expect(contractAddress).toBeDefined();
|
||||
const deployedName = await name({
|
||||
contract: getContract({
|
||||
client: TEST_CLIENT,
|
||||
chain: ANVIL_CHAIN,
|
||||
address: contractAddress,
|
||||
}),
|
||||
});
|
||||
expect(deployedName).toBe("Edition");
|
||||
|
||||
contract = getContract({
|
||||
address: contractAddress,
|
||||
chain: ANVIL_CHAIN,
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import {} from "viem";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { TEST_ACCOUNT_C } from "~test/test-wallets.js";
|
||||
import { optimizeMintBatchContent } from "./mintAdditionalSupplyToBatch.js";
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { beforeAll, describe, expect, it } from "vitest";
|
||||
import { TEST_CONTRACT_URI } from "~test/ipfs-uris.js";
|
||||
import { VITALIK_WALLET } from "../../../test/src/addresses.js";
|
||||
import { ANVIL_CHAIN } from "../../../test/src/chains.js";
|
||||
import { TEST_CLIENT } from "../../../test/src/test-clients.js";
|
||||
@@ -13,6 +12,7 @@ import { type ThirdwebContract, getContract } from "../../contract/contract.js";
|
||||
import { sendAndConfirmTransaction } from "../../transaction/actions/send-and-confirm-transaction.js";
|
||||
import { resolvePromisedValue } from "../../utils/promise/resolve-promised-value.js";
|
||||
import { toEther } from "../../utils/units.js";
|
||||
import { name } from "../common/read/name.js";
|
||||
import { deployERC20Contract } from "../prebuilts/deploy-erc20.js";
|
||||
import { getClaimConditions } from "./drops/read/getClaimConditions.js";
|
||||
import { claimTo } from "./drops/write/claimTo.js";
|
||||
@@ -35,11 +35,20 @@ describe.runIf(process.env.TW_SECRET_KEY)(
|
||||
client: TEST_CLIENT,
|
||||
params: {
|
||||
name: "Test DropERC20",
|
||||
contractURI: TEST_CONTRACT_URI,
|
||||
},
|
||||
type: "DropERC20",
|
||||
});
|
||||
|
||||
expect(contractAddress).toBeDefined();
|
||||
const deployedName = await name({
|
||||
contract: getContract({
|
||||
client: TEST_CLIENT,
|
||||
chain: ANVIL_CHAIN,
|
||||
address: contractAddress,
|
||||
}),
|
||||
});
|
||||
expect(deployedName).toBe("Test DropERC20");
|
||||
|
||||
contract = getContract({
|
||||
address: contractAddress,
|
||||
chain: ANVIL_CHAIN,
|
||||
|
||||
@@ -13,6 +13,7 @@ import { type ThirdwebContract, getContract } from "../../contract/contract.js";
|
||||
import { sendAndConfirmTransaction } from "../../transaction/actions/send-and-confirm-transaction.js";
|
||||
import { resolvePromisedValue } from "../../utils/promise/resolve-promised-value.js";
|
||||
import { toEther } from "../../utils/units.js";
|
||||
import { name } from "../common/read/name.js";
|
||||
import { deployERC20Contract } from "../prebuilts/deploy-erc20.js";
|
||||
import { deployERC721Contract } from "../prebuilts/deploy-erc721.js";
|
||||
import { balanceOf } from "./__generated__/IERC721A/read/balanceOf.js";
|
||||
@@ -40,11 +41,19 @@ describe.runIf(process.env.TW_SECRET_KEY)(
|
||||
client: TEST_CLIENT,
|
||||
params: {
|
||||
name: "Test DropERC721",
|
||||
contractURI: TEST_CONTRACT_URI,
|
||||
},
|
||||
type: "DropERC721",
|
||||
});
|
||||
|
||||
contract = getContract({
|
||||
address: contractAddress,
|
||||
chain: ANVIL_CHAIN,
|
||||
client: TEST_CLIENT,
|
||||
});
|
||||
|
||||
const deployedName = await name({ contract });
|
||||
expect(deployedName).toBe("Test DropERC721");
|
||||
|
||||
const erc20ContractAddress = await deployERC20Contract({
|
||||
account: TEST_ACCOUNT_A,
|
||||
chain: ANVIL_CHAIN,
|
||||
@@ -61,12 +70,6 @@ describe.runIf(process.env.TW_SECRET_KEY)(
|
||||
chain: ANVIL_CHAIN,
|
||||
client: TEST_CLIENT,
|
||||
});
|
||||
|
||||
contract = getContract({
|
||||
address: contractAddress,
|
||||
chain: ANVIL_CHAIN,
|
||||
client: TEST_CLIENT,
|
||||
});
|
||||
// this deploys a contract, it may take some time
|
||||
}, 120_000);
|
||||
|
||||
|
||||
@@ -59,17 +59,15 @@ export function claimToBatch(
|
||||
async function getClaimToBatchParams(
|
||||
options: BaseTransactionOptions<ClaimToBatchParams>,
|
||||
) {
|
||||
const errorIndexTo = options.content.findIndex((o) => !o.to);
|
||||
if (errorIndexTo !== -1) {
|
||||
throw new Error(
|
||||
`Error: Item at index ${errorIndexTo} is missing recipient address ("to")`,
|
||||
);
|
||||
}
|
||||
const errorIndexQuantity = options.content.findIndex((o) => !o.quantity);
|
||||
if (errorIndexQuantity !== -1) {
|
||||
throw new Error(
|
||||
`Error: Item at index ${errorIndexQuantity} is missing claim quantity`,
|
||||
);
|
||||
for (let i = 0; i < options.content.length; i++) {
|
||||
if (!options.content[i]?.quantity) {
|
||||
throw new Error(`Error: Item at index ${i} is missing claim quantity`);
|
||||
}
|
||||
if (!options.content[i]?.to) {
|
||||
throw new Error(
|
||||
`Error: Item at index ${i} is missing recipient address ("to")`,
|
||||
);
|
||||
}
|
||||
}
|
||||
const content = optimizeClaimContent(options.content);
|
||||
const data = await Promise.all(
|
||||
@@ -89,6 +87,7 @@ async function getClaimToBatchParams(
|
||||
pricePerToken: claimParams.pricePerToken,
|
||||
allowlistProof: claimParams.allowlistProof,
|
||||
data: claimParams.data,
|
||||
overrides: claimParams.overrides,
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -1,16 +1,5 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { ANVIL_CHAIN } from "~test/chains.js";
|
||||
import { TEST_CONTRACT_URI } from "~test/ipfs-uris.js";
|
||||
import { TEST_CLIENT } from "~test/test-clients.js";
|
||||
import { DOODLES_CONTRACT } from "~test/test-contracts.js";
|
||||
import { TEST_ACCOUNT_A } from "~test/test-wallets.js";
|
||||
import { getContract } from "../../../contract/contract.js";
|
||||
import { deployERC721Contract } from "../../../extensions/prebuilts/deploy-erc721.js";
|
||||
import { sendAndConfirmTransaction } from "../../../transaction/actions/send-and-confirm-transaction.js";
|
||||
import { parseNFT } from "../../../utils/nft/parseNft.js";
|
||||
import { setTokenURI } from "../__generated__/INFTMetadata/write/setTokenURI.js";
|
||||
import { mintTo } from "../write/mintTo.js";
|
||||
import { getNFT } from "./getNFT.js";
|
||||
|
||||
describe.runIf(process.env.TW_SECRET_KEY)("erc721.getNFT", () => {
|
||||
@@ -99,60 +88,4 @@ describe.runIf(process.env.TW_SECRET_KEY)("erc721.getNFT", () => {
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it("should return a default value if the URI of the token doesn't exist", async () => {
|
||||
/**
|
||||
* To create this test scenario, we first deploy an NFTCollection/Edition contract,
|
||||
* mint a token, then purposefully change that token's URI to an empty string, using setTokenURI
|
||||
*/
|
||||
const contract = getContract({
|
||||
address: await deployERC721Contract({
|
||||
chain: ANVIL_CHAIN,
|
||||
client: TEST_CLIENT,
|
||||
account: TEST_ACCOUNT_A,
|
||||
type: "TokenERC721",
|
||||
params: {
|
||||
name: "",
|
||||
contractURI: TEST_CONTRACT_URI,
|
||||
},
|
||||
}),
|
||||
chain: ANVIL_CHAIN,
|
||||
client: TEST_CLIENT,
|
||||
});
|
||||
|
||||
await sendAndConfirmTransaction({
|
||||
transaction: mintTo({
|
||||
contract,
|
||||
nft: { name: "token 0" },
|
||||
to: TEST_ACCOUNT_A.address,
|
||||
}),
|
||||
account: TEST_ACCOUNT_A,
|
||||
});
|
||||
|
||||
await sendAndConfirmTransaction({
|
||||
transaction: setTokenURI({
|
||||
contract,
|
||||
tokenId: 0n,
|
||||
// Need to have some spaces because NFTMetadata.sol does not allow to update an empty valud
|
||||
uri: " ",
|
||||
}),
|
||||
account: TEST_ACCOUNT_A,
|
||||
});
|
||||
|
||||
expect(await getNFT({ contract, tokenId: 0n })).toStrictEqual(
|
||||
parseNFT(
|
||||
{
|
||||
id: 0n,
|
||||
type: "ERC721",
|
||||
uri: "",
|
||||
},
|
||||
{
|
||||
tokenId: 0n,
|
||||
tokenUri: "",
|
||||
type: "ERC721",
|
||||
owner: null,
|
||||
},
|
||||
),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,17 +1,8 @@
|
||||
import { type Abi, toFunctionSelector } from "viem";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { ANVIL_CHAIN } from "~test/chains.js";
|
||||
import { TEST_CONTRACT_URI } from "~test/ipfs-uris.js";
|
||||
import { TEST_CLIENT } from "~test/test-clients.js";
|
||||
import {
|
||||
DOODLES_CONTRACT,
|
||||
UNISWAPV3_FACTORY_CONTRACT,
|
||||
} from "~test/test-contracts.js";
|
||||
import { TEST_ACCOUNT_B } from "~test/test-wallets.js";
|
||||
import { resolveContractAbi } from "../../../contract/actions/resolve-abi.js";
|
||||
import { getContract } from "../../../contract/contract.js";
|
||||
import { deployERC721Contract } from "../../../extensions/prebuilts/deploy-erc721.js";
|
||||
import { isGetNFTSupported } from "./getNFT.js";
|
||||
import { getNFTs } from "./getNFTs.js";
|
||||
|
||||
describe.runIf(process.env.TW_SECRET_KEY)("erc721.getNFTs", () => {
|
||||
@@ -197,29 +188,6 @@ describe.runIf(process.env.TW_SECRET_KEY)("erc721.getNFTs", () => {
|
||||
// TODO find a contract that we can use that has "1 indexed" NFTs, then re-enable this test
|
||||
});
|
||||
|
||||
it("isGetNFTsSupported should work", async () => {
|
||||
const contract = getContract({
|
||||
address: await deployERC721Contract({
|
||||
chain: ANVIL_CHAIN,
|
||||
client: TEST_CLIENT,
|
||||
type: "TokenERC721",
|
||||
params: {
|
||||
name: "",
|
||||
contractURI: TEST_CONTRACT_URI,
|
||||
},
|
||||
account: TEST_ACCOUNT_B,
|
||||
}),
|
||||
chain: ANVIL_CHAIN,
|
||||
client: TEST_CLIENT,
|
||||
});
|
||||
|
||||
const abi = await resolveContractAbi<Abi>(contract);
|
||||
const selectors = abi
|
||||
.filter((f) => f.type === "function")
|
||||
.map((f) => toFunctionSelector(f));
|
||||
expect(isGetNFTSupported(selectors)).toBe(true);
|
||||
});
|
||||
|
||||
it("should throw error if totalSupply and nextTokenIdToMint are not supported", async () => {
|
||||
await expect(() =>
|
||||
getNFTs({ contract: UNISWAPV3_FACTORY_CONTRACT }),
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
import { type Abi, toFunctionSelector } from "viem";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { ANVIL_CHAIN } from "~test/chains.js";
|
||||
import { TEST_CONTRACT_URI } from "~test/ipfs-uris.js";
|
||||
import { TEST_CLIENT } from "~test/test-clients.js";
|
||||
import { TEST_ACCOUNT_B } from "~test/test-wallets.js";
|
||||
import { resolveContractAbi } from "../../contract/actions/resolve-abi.js";
|
||||
import { type ThirdwebContract, getContract } from "../../contract/contract.js";
|
||||
import { name } from "../../extensions/common/read/name.js";
|
||||
import { sendAndConfirmTransaction } from "../../transaction/actions/send-and-confirm-transaction.js";
|
||||
import { parseNFT } from "../../utils/nft/parseNft.js";
|
||||
import { deployERC721Contract } from "../prebuilts/deploy-erc721.js";
|
||||
import { tokenURI } from "./__generated__/IERC721A/read/tokenURI.js";
|
||||
import { setTokenURI } from "./__generated__/INFTMetadata/write/setTokenURI.js";
|
||||
import { getNFT, isGetNFTSupported } from "./read/getNFT.js";
|
||||
import { mintTo } from "./write/mintTo.js";
|
||||
|
||||
const client = TEST_CLIENT;
|
||||
const chain = ANVIL_CHAIN;
|
||||
const account = TEST_ACCOUNT_B;
|
||||
|
||||
let token721Contract: ThirdwebContract;
|
||||
|
||||
describe.runIf(process.env.TW_SECRET_KEY)("deployERC721", () => {
|
||||
it("should deploy ERC721 token", async () => {
|
||||
const address = await deployERC721Contract({
|
||||
client,
|
||||
chain,
|
||||
account,
|
||||
type: "TokenERC721",
|
||||
params: {
|
||||
name: "NFTCollection",
|
||||
contractURI: TEST_CONTRACT_URI,
|
||||
},
|
||||
});
|
||||
expect(address).toBeDefined();
|
||||
token721Contract = getContract({
|
||||
client: TEST_CLIENT,
|
||||
chain: ANVIL_CHAIN,
|
||||
address,
|
||||
});
|
||||
const deployedName = await name({ contract: token721Contract });
|
||||
expect(deployedName).toBe("NFTCollection");
|
||||
});
|
||||
|
||||
it("should mint an nft when passed an object", async () => {
|
||||
const transaction = mintTo({
|
||||
contract: token721Contract,
|
||||
nft: { name: "token 0" },
|
||||
to: account.address,
|
||||
});
|
||||
await sendAndConfirmTransaction({
|
||||
transaction,
|
||||
account,
|
||||
});
|
||||
|
||||
const nft = await getNFT({ contract: token721Contract, tokenId: 0n });
|
||||
expect(nft.metadata.name).toBe("token 0");
|
||||
});
|
||||
|
||||
it("should mint an nft when passed a string", async () => {
|
||||
const transaction = mintTo({
|
||||
contract: token721Contract,
|
||||
nft: "ipfs://fake-token-uri",
|
||||
to: account.address,
|
||||
});
|
||||
await sendAndConfirmTransaction({
|
||||
transaction,
|
||||
account,
|
||||
});
|
||||
const _uri = await tokenURI({ contract: token721Contract, tokenId: 1n });
|
||||
expect(_uri).toBe("ipfs://fake-token-uri");
|
||||
});
|
||||
|
||||
it("isGetNFTsSupported should work", async () => {
|
||||
const abi = await resolveContractAbi<Abi>(token721Contract);
|
||||
const selectors = abi
|
||||
.filter((f) => f.type === "function")
|
||||
.map((f) => toFunctionSelector(f));
|
||||
expect(isGetNFTSupported(selectors)).toBe(true);
|
||||
});
|
||||
|
||||
it("should return a default value if the URI of the token doesn't exist", async () => {
|
||||
/**
|
||||
* mint a token, then purposefully change that token's URI to an empty string, using setTokenURI
|
||||
*/
|
||||
await sendAndConfirmTransaction({
|
||||
transaction: setTokenURI({
|
||||
contract: token721Contract,
|
||||
tokenId: 1n,
|
||||
// Need to have some spaces because NFTMetadata.sol does not allow to update an empty value
|
||||
uri: " ",
|
||||
}),
|
||||
account,
|
||||
});
|
||||
|
||||
expect(
|
||||
await getNFT({ contract: token721Contract, tokenId: 1n }),
|
||||
).toStrictEqual(
|
||||
parseNFT(
|
||||
{
|
||||
id: 1n,
|
||||
type: "ERC721",
|
||||
uri: "",
|
||||
},
|
||||
{
|
||||
tokenId: 1n,
|
||||
tokenUri: "",
|
||||
type: "ERC721",
|
||||
owner: null,
|
||||
},
|
||||
),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1,69 +0,0 @@
|
||||
import { beforeEach, describe, expect, it } from "vitest";
|
||||
import { ANVIL_CHAIN } from "~test/chains.js";
|
||||
import { TEST_CLIENT } from "~test/test-clients.js";
|
||||
import { TEST_ACCOUNT_A } from "~test/test-wallets.js";
|
||||
|
||||
import { TEST_CONTRACT_URI } from "~test/ipfs-uris.js";
|
||||
import {
|
||||
type ThirdwebContract,
|
||||
getContract,
|
||||
} from "../../../contract/contract.js";
|
||||
import { deployERC721Contract } from "../../../extensions/prebuilts/deploy-erc721.js";
|
||||
import { sendAndConfirmTransaction } from "../../../transaction/actions/send-and-confirm-transaction.js";
|
||||
import { tokenURI } from "../__generated__/IERC721A/read/tokenURI.js";
|
||||
import { getNFT } from "../read/getNFT.js";
|
||||
import { mintTo } from "./mintTo.js";
|
||||
|
||||
let contract: ThirdwebContract;
|
||||
const account = TEST_ACCOUNT_A;
|
||||
const client = TEST_CLIENT;
|
||||
const chain = ANVIL_CHAIN;
|
||||
|
||||
describe.runIf(!!process.env.TW_SECRET_KEY)("erc721 mintTo extension", () => {
|
||||
beforeEach(async () => {
|
||||
const address = await deployERC721Contract({
|
||||
client,
|
||||
chain,
|
||||
account,
|
||||
type: "TokenERC721",
|
||||
params: {
|
||||
name: "NFTDrop",
|
||||
contractURI: TEST_CONTRACT_URI,
|
||||
},
|
||||
});
|
||||
contract = getContract({
|
||||
address,
|
||||
client,
|
||||
chain,
|
||||
});
|
||||
});
|
||||
|
||||
it("should mint an nft when passed an object", async () => {
|
||||
const transaction = mintTo({
|
||||
contract,
|
||||
nft: { name: "token 0" },
|
||||
to: account.address,
|
||||
});
|
||||
await sendAndConfirmTransaction({
|
||||
transaction,
|
||||
account,
|
||||
});
|
||||
|
||||
const nft = await getNFT({ contract, tokenId: 0n });
|
||||
expect(nft.metadata.name).toBe("token 0");
|
||||
});
|
||||
|
||||
it("should mint an nft when passed a string", async () => {
|
||||
const transaction = mintTo({
|
||||
contract,
|
||||
nft: "ipfs://fake-token-uri",
|
||||
to: account.address,
|
||||
});
|
||||
await sendAndConfirmTransaction({
|
||||
transaction,
|
||||
account,
|
||||
});
|
||||
const _uri = await tokenURI({ contract, tokenId: 0n });
|
||||
expect(_uri).toBe("ipfs://fake-token-uri");
|
||||
});
|
||||
});
|
||||
@@ -1,54 +0,0 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { ANVIL_CHAIN } from "../../../test/src/chains.js";
|
||||
import { TEST_CLIENT } from "../../../test/src/test-clients.js";
|
||||
import { TEST_ACCOUNT_B } from "../../../test/src/test-wallets.js";
|
||||
import { getContract } from "../../contract/contract.js";
|
||||
import { name } from "../common/read/name.js";
|
||||
import { deployERC1155Contract } from "./deploy-erc1155.js";
|
||||
|
||||
const account = TEST_ACCOUNT_B;
|
||||
|
||||
describe.runIf(process.env.TW_SECRET_KEY)("deployERC1155", () => {
|
||||
it("should deploy ERC1155 drop", async () => {
|
||||
const address = await deployERC1155Contract({
|
||||
client: TEST_CLIENT,
|
||||
chain: ANVIL_CHAIN,
|
||||
account,
|
||||
type: "DropERC1155",
|
||||
params: {
|
||||
name: "EditionDrop",
|
||||
symbol: "NFTD",
|
||||
},
|
||||
});
|
||||
expect(address).toBeDefined();
|
||||
const deployedName = await name({
|
||||
contract: getContract({
|
||||
client: TEST_CLIENT,
|
||||
chain: ANVIL_CHAIN,
|
||||
address,
|
||||
}),
|
||||
});
|
||||
expect(deployedName).toBe("EditionDrop");
|
||||
});
|
||||
|
||||
it("should deploy ERC1155 token", async () => {
|
||||
const address = await deployERC1155Contract({
|
||||
client: TEST_CLIENT,
|
||||
chain: ANVIL_CHAIN,
|
||||
account,
|
||||
type: "TokenERC1155",
|
||||
params: {
|
||||
name: "Edition",
|
||||
},
|
||||
});
|
||||
expect(address).toBeDefined();
|
||||
const deployedName = await name({
|
||||
contract: getContract({
|
||||
client: TEST_CLIENT,
|
||||
chain: ANVIL_CHAIN,
|
||||
address,
|
||||
}),
|
||||
});
|
||||
expect(deployedName).toBe("Edition");
|
||||
});
|
||||
});
|
||||
@@ -1,56 +0,0 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { ANVIL_CHAIN } from "../../../test/src/chains.js";
|
||||
import { TEST_CLIENT } from "../../../test/src/test-clients.js";
|
||||
import { TEST_ACCOUNT_D } from "../../../test/src/test-wallets.js";
|
||||
import { getContract } from "../../contract/contract.js";
|
||||
import { name } from "../common/read/name.js";
|
||||
import { deployERC20Contract } from "./deploy-erc20.js";
|
||||
|
||||
const account = TEST_ACCOUNT_D;
|
||||
|
||||
// skip this test suite if there is no secret key available to test with
|
||||
// TODO: remove reliance on secret key during unit tests entirely
|
||||
describe.runIf(process.env.TW_SECRET_KEY)("deployERC20", () => {
|
||||
it("should deploy ERC20 drop", async () => {
|
||||
const address = await deployERC20Contract({
|
||||
client: TEST_CLIENT,
|
||||
chain: ANVIL_CHAIN,
|
||||
account,
|
||||
type: "DropERC20",
|
||||
params: {
|
||||
name: "TokenDrop",
|
||||
symbol: "NFTD",
|
||||
},
|
||||
});
|
||||
expect(address).toBeDefined();
|
||||
const deployedName = await name({
|
||||
contract: getContract({
|
||||
client: TEST_CLIENT,
|
||||
chain: ANVIL_CHAIN,
|
||||
address,
|
||||
}),
|
||||
});
|
||||
expect(deployedName).toBe("TokenDrop");
|
||||
});
|
||||
|
||||
it("should deploy ERC20 token", async () => {
|
||||
const address = await deployERC20Contract({
|
||||
client: TEST_CLIENT,
|
||||
chain: ANVIL_CHAIN,
|
||||
account,
|
||||
type: "TokenERC20",
|
||||
params: {
|
||||
name: "Token",
|
||||
},
|
||||
});
|
||||
expect(address).toBeDefined();
|
||||
const deployedName = await name({
|
||||
contract: getContract({
|
||||
client: TEST_CLIENT,
|
||||
chain: ANVIL_CHAIN,
|
||||
address,
|
||||
}),
|
||||
});
|
||||
expect(deployedName).toBe("Token");
|
||||
});
|
||||
});
|
||||
@@ -11,48 +11,6 @@ const account = TEST_ACCOUNT_B;
|
||||
// skip this test suite if there is no secret key available to test with
|
||||
// TODO: remove reliance on secret key during unit tests entirely
|
||||
describe.runIf(process.env.TW_SECRET_KEY)("deployERC721", () => {
|
||||
it("should deploy ERC721 drop", async () => {
|
||||
const address = await deployERC721Contract({
|
||||
client: TEST_CLIENT,
|
||||
chain: ANVIL_CHAIN,
|
||||
account,
|
||||
type: "DropERC721",
|
||||
params: {
|
||||
name: "NFTDrop",
|
||||
},
|
||||
});
|
||||
expect(address).toBeDefined();
|
||||
const deployedName = await name({
|
||||
contract: getContract({
|
||||
client: TEST_CLIENT,
|
||||
chain: ANVIL_CHAIN,
|
||||
address,
|
||||
}),
|
||||
});
|
||||
expect(deployedName).toBe("NFTDrop");
|
||||
});
|
||||
|
||||
it("should deploy ERC721 token", async () => {
|
||||
const address = await deployERC721Contract({
|
||||
client: TEST_CLIENT,
|
||||
chain: ANVIL_CHAIN,
|
||||
account,
|
||||
type: "TokenERC721",
|
||||
params: {
|
||||
name: "NFTCollection",
|
||||
},
|
||||
});
|
||||
expect(address).toBeDefined();
|
||||
const deployedName = await name({
|
||||
contract: getContract({
|
||||
client: TEST_CLIENT,
|
||||
chain: ANVIL_CHAIN,
|
||||
address,
|
||||
}),
|
||||
});
|
||||
expect(deployedName).toBe("NFTCollection");
|
||||
});
|
||||
|
||||
it("should deploy ERC721 open edition", async () => {
|
||||
const address = await deployERC721Contract({
|
||||
client: TEST_CLIENT,
|
||||
|
||||
@@ -5,6 +5,7 @@ import { getContract } from "../../contract/contract.js";
|
||||
import { approve } from "../../extensions/erc20/write/approve.js";
|
||||
import type { PrepareTransactionOptions } from "../../transaction/prepare-transaction.js";
|
||||
import { getClientFetch } from "../../utils/fetch.js";
|
||||
import { stringify } from "../../utils/json.js";
|
||||
import { getPayBuyWithCryptoQuoteEndpoint } from "../utils/definitions.js";
|
||||
import type {
|
||||
QuoteApprovalInfo,
|
||||
@@ -223,7 +224,7 @@ export async function getBuyWithCryptoQuote(
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
body: stringify({
|
||||
fromAddress: params.fromAddress,
|
||||
toAddress: params.toAddress,
|
||||
fromChainId: params.fromChainId.toString(),
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user