Compare commits

..
Author SHA1 Message Date
Jonas DanielsGitHubgithub-actions[bot] <github-actions[bot]@users.noreply.github.com>
c0b87503a4 Version Packages (#5285)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-11-05 09:28:50 +13:00
joaquim-verges 58fb28de29 fix: respect bundlerUrl in waitForUserReceipt (#5277)
## Problem solved

Fixes CNCT-2217

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on improving the `thirdweb` package by updating configurations, modifying wallet functionalities, enhancing chain checks, and refining tests for wallet balance functionality.

### Detailed summary
- Updated `limit` in `.size-limit.json` from `500 B` to `600 B`.
- Changed `options` to `bundlerOptions` in `waitForUserOpReceipt` call.
- Added additional chain IDs in `isZkSyncChain.ts`.
- Implemented fallback to RPC stack check in `isZkSyncChain.ts`.
- Renamed test case from "should work for un-named token" to "should work for native currency" in `getWalletBalance.test.ts`.

> ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}`

<!-- end pr-codex -->
2024-11-04 20:01:42 +00:00
Jonas DanielsGitHubgithub-actions[bot] <github-actions[bot]@users.noreply.github.com>
322e4496cf Version Packages (#5251)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-11-04 13:39:11 +13:00
gregfromstl 06604162a1 [SDK] Feature: Configure analytics URL (#5270)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR introduces the ability to configure the analytics endpoint for the `thirdweb` package, enhancing flexibility in specifying where analytics data is sent.

### Detailed summary
- Added a new configuration option for `analytics` in `apps/playground-web/src/lib/client.ts`.
- Replaced the hardcoded `ANALYTICS_ENDPOINT` URL with a dynamic URL using `getThirdwebBaseUrl` in `packages/thirdweb/src/analytics/track/index.ts`.
- Introduced a default value for `analytics` in `packages/thirdweb/src/utils/domains.ts`.

> ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}`

<!-- end pr-codex -->
2024-11-02 07:22:53 +00:00
gregfromstl 7d36418e0f [Dashboard] Fix: MAU date range (#5271)
Fixes CNCT-2185

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on updating the date range for fetching in-app wallet usage statistics from the start of the current month to the last 30 days, improving the relevance of the data displayed.

### Detailed summary
- In `layout.tsx`:
  - Changed `from` date in `getInAppWalletUsage` from the start of the current month to 30 days ago using `subDays`.

- In `header.tsx`:
  - Updated `from` date in `getInAppWalletUsage` similarly to 30 days ago with `subDays`.

> ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}`

<!-- end pr-codex -->
2024-11-02 02:23:26 +00:00
kien-ngo 412fb5da08 [Dashboard] Remove Drawer from ERC1155 udpate-metadata form (#5269)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR primarily focuses on updating the user interface components related to NFT actions, enhancing text alignment, and improving metadata update functionality by replacing a `Drawer` with a `Sheet` for better user experience.

### Detailed summary
- Corrected typo from `Aidrop` to `Airdrop` in `airdrop-tab.tsx` and `airdrop-button.tsx`.
- Added `className="text-left"` to `SheetTitle` in several components for consistent text alignment.
- Replaced `Drawer` with `Sheet` in `update-metadata-tab.tsx` to improve UI.
- Modified state management for opening/closing the metadata update sheet.
- Enhanced error handling and user feedback with `toast` notifications in `update-metadata-form.tsx`.

> ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}`

<!-- end pr-codex -->
2024-11-02 01:54:50 +00:00
kien-ngo c3f240489c [Dashboard] Remove Drawer & cleanup marketplace listing form (#5265)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on refactoring the `CreateListingButton` and `CreateListingsForm` components to replace the `Drawer` component with a `Sheet` component for better user interface interaction in the marketplace.

### Detailed summary
- Replaced `Drawer` with `Sheet` in `CreateListingButton`.
- Updated state management for opening/closing the form.
- Modified button and form structure to align with `Sheet` component.
- Added `actionText` and `setOpen` props to `CreateListingsForm`.
- Introduced toast notifications for transaction promises in `CreateListingsForm`.
- Implemented a fixed button section for form submission and cancellation.

> ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}`

<!-- end pr-codex -->
2024-11-02 01:54:48 +00:00
kien-ngo 54840759b8 [Dashboard] Remove Drawer from LazyMint form (#5264)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on refactoring the `NFTLazyMintButton` and `LazyMintNftForm` components to replace the `Drawer` UI component with a `Sheet` component for a better user interface experience during the NFT minting process.

### Detailed summary
- Replaced `Drawer` with `Sheet` in `NFTLazyMintButton`.
- Added `SheetTrigger` and `SheetContent` to manage the button and form display.
- Introduced `setOpen` state in `NFTLazyMintButton` to control the `Sheet`.
- Modified `LazyMintNftForm` to accept `setOpen` prop for closing the `Sheet`.
- Updated success and error handling with `toast` notifications instead of modal context.

> ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}`

<!-- end pr-codex -->
2024-11-02 01:54:47 +00:00
kien-ngo 9d1f75d63a [Dashboard] Remove Drawer from mintTo form (#5263)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on refactoring the `NFTMintButton` and `NFTMintForm` components to replace the `Drawer` component from `@chakra-ui/react` with a `Sheet` component for improved UI consistency and functionality.

### Detailed summary
- Replaced `Drawer` with `Sheet` in `NFTMintButton`.
- Updated `SheetContent` class properties for better responsiveness.
- Changed `SheetTitle` to have `text-left` class.
- Added `setOpen` prop to `NFTMintForm` to control visibility.
- Enhanced error handling with `toast` notifications.
- Refined form structure and validation messages in `NFTMintForm`.

> ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}`

<!-- end pr-codex -->
2024-11-02 01:54:46 +00:00
kien-ngo 21d794c7b8 [Dashboard] Remove chakra: BatchUpload drawer + other minor changes (#5259)
Made the BatchUpload drawer more clean looking basically

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on improving the `ClaimConditions` and `BatchLazyMint` components in the dashboard by refining input handling, restructuring layouts, and enhancing user interaction through better feedback and state management.

### Detailed summary
- Changed `isDisabled` prop to `disabled` in `PhaseNameInput`.
- Restructured layout in `ClaimConditions` for better spacing and organization.
- Updated `BatchLazyMintButton` to use `Sheet` instead of `Drawer`.
- Improved state management in `BatchLazyMintButton` for opening and closing the sheet.
- Refactored `BatchLazyMint` to use a `<form>` instead of `<Container>`.
- Simplified file upload instructions and requirements in `UploadStep`.

> ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}`

<!-- end pr-codex -->
2024-11-02 01:54:45 +00:00
gregfromstl e3c0af28f7 [SDK] Fix: Track chain ID on transaction events (#5272)
Fixes [CNCT-2184](https://linear.app/thirdweb/issue/CNCT-2184)

<!-- start pr-codex -->

---

## PR-Codex overview
This PR introduces modifications to the `trackTransaction` function across multiple files in the `thirdweb` package, adding support for `chainId` in transaction tracking.

### Detailed summary
- Added `chainId: tx.chainId` to `trackTransaction` in:
  - `packages/thirdweb/src/wallets/injected/index.ts`
  - `packages/thirdweb/src/wallets/in-app/core/wallet/enclave-wallet.ts`
  - `packages/thirdweb/src/wallets/in-app/web/lib/iframe-wallet.ts`
  - `packages/thirdweb/src/wallets/coinbase/coinbaseWebSDK.ts`
  - `packages/thirdweb/src/wallets/wallet-connect/controller.ts`

> ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}`

<!-- end pr-codex -->
2024-11-02 01:41:33 +00:00
gregfromstl 66798886cf [Dashboard] Feature: Adds ecosystem analytics (#5254)
<!-- start pr-codex -->

## PR-Codex overview
This PR enhances the `Ecosystem` analytics feature by introducing new components for wallet statistics, improving data fetching, and updating routing. It refines the structure for better maintainability and adds new functionalities for displaying wallet usage insights.

### Detailed summary
- Added `EcosystemWalletStats` interface.
- Introduced `EcosystemPermissionsPage` and `EcosystemAnalyticsPage` components.
- Implemented `RangeSelector` for date range selection.
- Updated routing to redirect to analytics.
- Enhanced data fetching for wallet usage.
- Refactored paths for type imports.
- Improved `InAppWalletUsersChartCard` for better data visualization.
- Added error handling and loading states for analytics components.

> ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}`

<!-- end pr-codex -->
2024-11-02 01:18:06 +00:00
gregfromstl 842ea50cbe [Global] Chore: Automate stale issues and PRs (30 days to stale, then 5 days till closing) (#5262)
<!-- start pr-codex -->

---

## PR-Codex overview
This PR adds a GitHub Actions workflow to automatically close stale issues and pull requests after a specified period of inactivity.

### Detailed summary
- Introduced a new workflow in `.github/workflows/stale.yml` named 'Close stale issues and PRs'.
- Configured the workflow to trigger on a schedule using a cron job.
- Set the job to run on `ubuntu-latest`.
- Utilized `actions/stale@v9` for managing stale issues and PRs.
- Configured messages for stale issues and PRs.
- Defined `days-before-stale` as 30 and `days-before-close` as 5.

> ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}`

<!-- end pr-codex -->
2024-11-02 01:04:26 +00:00
gregfromstl 8bd2a99a22 [Actions] Chore: Rename Issue Check (#5268)
<!-- start pr-codex -->

## PR-Codex overview
This PR introduces a new workflow in the `.github/workflows/linear.yml` file that ensures pull requests contain a linked issue in their body.

### Detailed summary
- Added a new workflow named `Check for linked issue in pull request body`.
- Configured the workflow to trigger on `pull_request` events.

> ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}`

<!-- end pr-codex -->
2024-11-02 00:58:26 +00:00
Jonas DanielsandGitHub 7a9713e877 add /solutions/consumer-apps to framer rewrites (#5266) 2024-11-01 16:23:49 -07:00
Jonas DanielsandGitHub 1c6c1bf890 move /engine and /solutions/gaming to framer (#5261) 2024-11-01 14:14:07 -07:00
edwardysun f81ffa9fa6 Check for linear in PR description (#5216)
## Problem solved

Fixes https://linear.app/thirdweb/issue/ENG-2/better-enforce-linear-use

<!-- start pr-codex -->

---

## PR-Codex overview
This PR introduces a GitHub Actions workflow to check the pull request body for specific keywords ("CNCT" or "DASH"). If these keywords are found, it confirms a linked issue; otherwise, it fails the check.

### Detailed summary
- Added a workflow named `Check for "linear" in pull request body` in `.github/workflows/linear.yml`.
- Configured the workflow to trigger on `pull_request` events (opened and edited).
- Implemented a job `check_linear_in_pr_body` to verify the presence of keywords in the PR body.
- Utilized `curl` to fetch the PR body and `grep` to search for keywords.

> ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}`

<!-- end pr-codex -->
2024-11-01 21:05:21 +00:00
kien-ngo 88e96dee44 [Dashboard] Fix ContractCheckList (#5249)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on enhancing the `ContractChecklist` component by incorporating additional checks for ERC721 and ERC20 claim conditions, ensuring that the logic correctly reflects the type of contract being interacted with.

### Detailed summary
- Added `isErc721` to the dependency array of `useMemo` for `hasERC721ClaimConditions`.
- Added `isErc20` to the dependency array of `useMemo` for `hasERC20ClaimConditions`.
- Updated the conditional check for `hasERC721ClaimConditions` to also verify `isErc721`.

> ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}`

<!-- end pr-codex -->
2024-11-01 20:53:29 +00:00
saminacodes ccbf2d2c22 fixed broken link and broken image (#5241)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on updating the `link` and `image` properties for specific projects in the `PROJECT_SHOWCASE_DATA` array within the `project-showcase-constants.ts` file.

### Detailed summary
- Updated `link` for the gaming project from `@goatgamingbot` to `https://goatgaming.com/`.
- Changed `image` for the `storychain` project from `ipfs://Qmau1mgrzahdfEtJFU9i2pSKMM7AwHav6tQCVEdJARUFpb` to `ipfs://QmUUBdNHa7ZoiRQicNathMY6G8GTBA4S9cRY1RRBXAKpYi`.

> ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}`

<!-- end pr-codex -->
2024-11-01 19:12:24 +00:00
joaquim-verges baf21980be fix: use stringify helper for bigint serialization (#5253)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on replacing `JSON.stringify` with `stringify` in various parts of the codebase to improve bigint serialization handling.

### Detailed summary
- Updated error handling in `bundler.ts` to use `stringify`.
- Modified payload serialization in `verify-jwt.ts`, `native/helpers/errors.ts`, and others to use `stringify`.
- Changed data storage and retrieval methods in various files to utilize `stringify`.
- Adjusted JWT encoding in `encode-jwt.ts` to use `stringify`.
- Enhanced error messages and responses across multiple files by replacing `JSON.stringify` with `stringify`.

> ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}`

<!-- end pr-codex -->
2024-11-01 18:57:21 +00:00
kien-ngo 7a86344941 [SDK] Consolidate more tests - reduce contract deployment (#5258)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on cleaning up and enhancing tests related to `ERC721` and `ERC1155` contracts in the `thirdweb` package. It includes the removal of obsolete tests and the addition of new functionality checks, particularly around contract deployment and NFT minting.

### Detailed summary
- Deleted obsolete test files for `ERC721` and `ERC20`.
- Updated `mintAdditionalSupplyToBatch.test.ts` to include checks for contract names.
- Enhanced tests for `DropERC1155` and `TokenERC1155` to verify deployed names.
- Removed tests for `deployERC721` that were reliant on secret keys.
- Added new tests for minting NFTs in `token721.test.ts`, covering both object and string inputs.
- Ensured compatibility checks for NFT functions in the new tests.

> ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}`

<!-- end pr-codex -->
2024-11-01 18:37:33 +00:00
kien-ngo c621c13f11 [SDK] Options to custonize ViewAssets's tabs (#5126)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR introduces the ability to customize the display order of asset tabs in the `ConnectButton` component, allowing users to prioritize "NFTs" over "Tokens". It also includes updates to the documentation and type definitions to support this feature.

### Detailed summary
- Added customization for asset tab order in `ConnectButton`.
- Default order shows "Tokens" first; can be changed to `["nft", "token"]`.
- Updated type definitions to include `assetTabs` in `ConnectButtonProps`.
- Modified `ViewAssets` to dynamically render tabs based on `assetTabs`.
- Documentation updated to reflect changes in tab customization.

> ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}`

<!-- end pr-codex -->
2024-11-01 18:23:42 +00:00
Jonas DanielsandGitHub 004cac04aa Update free wallets promo code to FREEWALLETS24 (#5260) 2024-11-01 11:23:05 -07:00
gregfromstl 57553b784e [Dashboard] Feature: Breakdown sponsored transactions by chain ID (#5252)
![Screenshot 2024-10-31 at 12 41 52 PM](https://github.com/user-attachments/assets/1eb6234c-9c5a-4e82-9ab7-4252dc1c1a8c)

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on enhancing the `SponsoredTransactionsChartCard` component to handle user operation statistics by chain, improving data visualization and functionality.

### Detailed summary
- Changed `UserOpStats` to `UserOpStatsByChain` in `createUserOpStatsStub`.
- Updated chart data structure to support multiple chains.
- Modified `SponsoredTransactionsChartCard` to visualize data by chain.
- Enhanced data aggregation and display logic.
- Updated chart configuration to include dynamic chains.

> ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}`

<!-- end pr-codex -->
2024-11-01 18:01:28 +00:00
gregfromstl 13fc6f5854 [Dashboard] Docs: Adds basic code organization instructions (#5255)
<!-- start pr-codex -->

## PR-Codex overview
This PR updates the `README.md` file for the `dashboard` application to include guidelines on code structure and style, particularly focusing on component organization and data fetching practices.

### Detailed summary
- Added a section on code structure and style guide.
- Specified that components should be placed in a `components` folder next to their respective app pages.
- Clarified where to place reused components and low-level components.
- Recommended using RSC for data fetching.
- Suggested keeping data fetching functions in the same file as their components unless reused across multiple components.

> ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}`

<!-- end pr-codex -->
2024-11-01 17:49:25 +00:00
kien-ngo b03dc57072 [SDK] Improve ERC721 claimToBatch (#5257)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR refactors the validation logic in the `claimToBatch.ts` file to improve error handling for missing recipient addresses and claim quantities. It replaces the use of `findIndex` with a `for` loop for more straightforward validation.

### Detailed summary
- Removed the use of `findIndex` for checking missing recipient addresses and claim quantities.
- Added a `for` loop to iterate through `options.content` for validation.
- Updated error messages to reflect the index of the item directly within the loop.

> ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}`

<!-- end pr-codex -->
2024-11-01 10:47:05 +00:00
Joaquim VergesandGitHub f40d2474e6 feat: enable smart account chain switching between zk/non-zk (#5250) 2024-10-31 22:25:32 +13:00
Joaquim VergesandGitHub ae217a3a60 feat: add ecosystem URL display in header (#5248) 2024-10-31 22:06:12 +13:00
142 changed files with 2242 additions and 2921 deletions
+26
View File
@@ -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
+15
View File
@@ -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
+14 -1
View File
@@ -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.
+3
View File
@@ -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;
@@ -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>
);
};
@@ -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,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";
@@ -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}
@@ -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>
);
};
@@ -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
@@ -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&apos;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&apos;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>
);
};
@@ -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>
);
};
@@ -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>
);
};
@@ -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">
@@ -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>
);
};
@@ -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&apos;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&apos;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>
</>
);
};
@@ -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>
);
};
@@ -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&apos;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&apos;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>
</>
);
};
@@ -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"
@@ -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: (
@@ -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>
@@ -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">
@@ -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">
@@ -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"
@@ -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">
@@ -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>
);
}
@@ -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>
);
}
@@ -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>
);
}
@@ -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;
}
@@ -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>
@@ -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)",
@@ -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: {
@@ -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,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";
@@ -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 = {
@@ -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({
@@ -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";
@@ -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,4 +1,4 @@
import type { Ecosystem } from "../../../../types";
import type { Ecosystem } from "../../../../../types";
import {
AuthOptionsForm,
AuthOptionsFormSkeleton,
@@ -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,4 +1,4 @@
import type { Ecosystem } from "../../../../types";
import type { Ecosystem } from "../../../../../types";
import {
IntegrationPermissionsToggle,
IntegrationPermissionsToggleSkeleton,
@@ -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 }) {
@@ -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;
@@ -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;
@@ -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">,
@@ -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;
@@ -0,0 +1,5 @@
import { EcosystemPermissionsPage } from "./components/client/EcosystemPermissionsPage";
export default function Page({ params }: { params: { slug: string } }) {
return <EcosystemPermissionsPage params={params} />;
}
@@ -1,4 +1,4 @@
import { EcosystemLayoutSlug } from "./EcosystemSlugLayout";
import { EcosystemLayoutSlug } from "./components/EcosystemSlugLayout";
export default async function Layout({
children,
@@ -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`);
}
@@ -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",
});
@@ -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;
}
@@ -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,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 };
@@ -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`,
);
}
@@ -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",
});
@@ -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",
);
}
@@ -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}
@@ -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)
@@ -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&apos;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&apos;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:
-2
View File
@@ -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",
-394
View File
@@ -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&apos;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;
+1
View File
@@ -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 =
+1 -1
View File
@@ -19,7 +19,7 @@
{
"name": "thirdweb/chains (tree-shaking)",
"path": "./dist/esm/exports/chains.js",
"limit": "500 B",
"limit": "600 B",
"import": "{ ethereum }"
},
{
+37
View File
@@ -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 -1
View File
@@ -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