Compare commits

..
Author SHA1 Message Date
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
Jonas DanielsGitHubgithub-actions[bot] <github-actions[bot]@users.noreply.github.com>
7791a992b6 Version Packages (#5247)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-10-31 21:53:28 +13:00
Joaquim VergesandGitHub 82c8726c10 fix: respect custom paymaster callback (#5246) 2024-10-31 21:35:51 +13:00
Jonas DanielsGitHubgithub-actions[bot] <github-actions[bot]@users.noreply.github.com>
60629e3cf3 Version Packages (#5215)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-10-31 15:43:37 +13:00
Joaquim VergesandGitHub 178d407e6f feat: add support for custom singlePhase drops (#5244) 2024-10-31 15:29:15 +13:00
MananTank 7e1cb6d8d8 Fix in-app wallet analytics chart on legacy dashboard (#5243)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on updating the parameters used in the `Page` component within the `analytics` directory to reflect a change in the expected data structure, specifically changing from `project_slug` to `clientId`.

### Detailed summary
- Updated the `params` type in the `Page` function from `{ team_slug: string; project_slug: string }` to `{ clientId: string }`.
- Changed the prop passed to `<InAppWalletAnalytics />` from `clientId={params.project_slug}` to `clientId={params.clientId}`.

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

<!-- end pr-codex -->
2024-10-30 23:58:20 +00:00
MananTank c2a90542d7 Add loading.tsx for connect pages (#5240)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR primarily focuses on standardizing loading pages across various components in the dashboard by introducing a `GenericLoadingPage`. It also refines data fetching error handling and enhances the `InAppWalletsHeader` and `InAppWallets` pages with improved stats handling.

### Detailed summary
- Deleted multiple `layout.tsx` files.
- Added `"use client"` directive to various loading components.
- Implemented `GenericLoadingPage` for consistent loading UI.
- Improved error handling in `getInAppWalletUsage` function.
- Enhanced `InAppWalletsHeader` with Promise.all for stats fetching.
- Updated `InAppWallets` pages to handle empty stats gracefully.
- Added footer section to several pages for better UX.

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

<!-- end pr-codex -->
2024-10-30 23:40:45 +00:00
Joaquim VergesandGitHub 802d3bfd4b fix: sanitize block explorer URLs (#5237) 2024-10-31 12:36:59 +13:00
MananTank 09b1912eb9 Shorten publisher address in OG image on published contract page (#5242)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR enhances the `publishedContractOGImageTemplate` function to conditionally shorten the display of the `publisher` address if it is a valid blockchain address.

### Detailed summary
- Added import for `isAddress` and `shortenAddress` from `thirdweb`.
- Updated the rendering of the `publisher` in the `<h2>` element to check if it is a valid address using `isAddress`.
- If valid, it displays the shortened address using `shortenAddress`; otherwise, it displays the full `publisher` name.

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

<!-- end pr-codex -->
2024-10-30 22:38:24 +00:00
MananTank 975cd0b384 Add BurnMintERC677 in popular contracts section (#5239)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on updating the `apps/dashboard/src/data/explore.ts` file by adding a new entry to a list of addresses or identifiers related to third-party contracts.

### Detailed summary
- Added the entry `0xaF3202F6bAEbA50d37e0d4B0b870455EDF198D7c/BurnMintERC677` to the existing list of third-party contracts.

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

<!-- end pr-codex -->
2024-10-30 22:03:59 +00:00
MananTank 436f6b79b5 Fix MaskedAvatar and PublisherAvatar sizing issues, improved loading & fallback states (#5238)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR primarily focuses on updating the `PublisherAvatar` component usage across several files, replacing the `boxSize` prop with a `className` prop for styling. Additionally, it enhances the `Img` component with improved loading behavior.

### Detailed summary
- Replaced `boxSize` with `className` in `PublisherAvatar` components in:
  - `apps/dashboard/src/components/explore/publisher/index.tsx`
  - `apps/dashboard/src/pages/profile/[profileAddress].tsx`
  - `apps/dashboard/src/components/contract-components/publisher/publisher-header.tsx`
  - `apps/dashboard/src/pages/community/ambassadors.tsx`
- Updated `MaskedAvatar` component to use `className` instead of `boxSize`.
- Modified `MaskedAvatar` component's rendering logic to use a new `Img` component.
- Enhanced `Img` component with a loading state and error handling using a `ref`.

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

<!-- end pr-codex -->
2024-10-30 22:03:58 +00:00
MananTank df8b2f6f90 Fix enum lint warning in dashboard (#5234)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR refactors the code to replace the usage of `AccountStatus` and `AccountPlan` enums with `accountStatus` and `accountPlan` constants, improving consistency and clarity throughout the codebase.

### Detailed summary
- Replaced `AccountStatus` enum with `accountStatus` constant object.
- Replaced `AccountPlan` enum with `accountPlan` constant object.
- Updated all references to use `accountStatus` and `accountPlan` instead of enums.
- Adjusted conditions and logic to align with the new structure.

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

<!-- end pr-codex -->
2024-10-30 18:48:10 +00:00
950ce60ec2 feat(dashboard): Adds analytics to in-app-wallets page (#5232)
Co-authored-by: gregfromstl <[email protected]>
2024-10-31 07:38:00 +13:00
MananTank 3658b3fece Fix theming class hydration warning on html element (#5233)
This is the correct way to fix this
https://github.com/pacocoursey/next-themes?tab=readme-ov-file#with-app

` suppressHydrationWarning` property only applies one level deep, so it won't block hydration warnings on other child elements of `<html>`
2024-10-30 16:35:51 +00:00
Joaquim VergesandGitHub 686d0c3c05 feat: respect custom bundler URL for gas fees (#5231) 2024-10-30 23:14:17 +13:00
ElasticBottle 9425e9ef42 Winston/don't use iframe for jwt or custom auth endpoint validation (#5229)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR consolidates authentication methods in the `thirdweb` package by introducing a common endpoint for custom JWT and custom auth, enhancing code organization and reducing redundancy.

### Detailed summary
- Changed `getSessionHeaders` to no longer export it.
- Introduced `ecosystem` parameter in various functions.
- Replaced direct `fetch` calls with `getClientFetch` for consistency.
- Simplified the `loginWithAuthToken` method to accept an optional `recoveryCode`.
- Unified handling of authentication strategies in `InAppWebConnector`.

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

<!-- end pr-codex -->
2024-10-30 05:48:20 +00:00
Joaquim VergesandGitHub 556e11373c Revert "[Dashboard] Feature: Adds analytics to in-app-wallets page (#5186) (#5228) 2024-10-30 14:19:12 +13:00
6e87a866b2 [Dashboard] Feature: Adds analytics to in-app-wallets page (#5186) (#5227)
Co-authored-by: gregfromstl <[email protected]>
2024-10-30 14:02:15 +13:00
iuwqyir 043ee92598 unhide insight service (#5190)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR removes an entry from the `HIDDEN_SERVICES` array in the `validations.ts` file, indicating a potential update in the handling of API keys.

### Detailed summary
- Removed `"insight"` from the `HIDDEN_SERVICES` array in the `validations.ts` file.

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

<!-- end pr-codex -->
2024-10-29 22:41:11 +00:00
DustinandGitHub 89df3b5fa9 Soneium manito op grant update (#5222) 2024-10-30 10:18:32 +13:00
kien-ngo c9f52e9239 [Dashboard] chore: Update OS Page (#5223)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR adds new contributors to the list in the `open-source.tsx` file for the dashboard page.

### Detailed summary
- Added new contributors to the list:
  - `iuwqyir`
  - `GWSzeto`
  - `Joe-Thirdweb`
  - `AmineAfia`
  - `catalyst17`
  - `dirtycajunrice`

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

<!-- end pr-codex -->
2024-10-29 20:01:54 +00:00
kien-ngo 519349b5cf [Dashboard] chore: add spacing for TUrnstile captcha (#5182)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on enhancing the styling of the `Turnstile` component within the `FaucetButton.tsx` file by adding a margin-top class.

### Detailed summary
- Added the `className="mt-4"` to the `Turnstile` component in `FaucetButton.tsx` to apply a top margin for improved spacing.

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

<!-- end pr-codex -->
2024-10-29 18:22:54 +00:00
MananTank 71a21e185e Fix page crash caused by BillingAlert component (#5220)
Fixes: DASH-356
2024-10-29 18:12:20 +00:00
kien-ngo c8b5780627 [Dashboard] Fix contract source code page (#5218)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR updates the error handling for resolving the contract ABI in the `page.tsx` file by changing the promise handling from `then` to `catch`, ensuring that any errors in resolving the ABI will result in `undefined` being assigned.

### Detailed summary
- Changed the promise handling for `resolveContractAbi(contract)`:
  - From using `.then(() => undefined)` to `.catch(() => undefined)` to properly handle errors.

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

<!-- end pr-codex -->
2024-10-29 15:01:29 +00:00
ElasticBottle 68ce724ff6 [Dashboard]: Update in app wallet user api (#5187)
## Problem solved

Updates the dashboard so that list of in app wallet will pull from thirdweb RDS.
NOTE: NEED TO UPDATE ENV VAR AFTER DEPLOYEMENT

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on enhancing the `thirdweb` wallet integration by exposing the `WalletUser` type, updating API endpoints, and improving the handling of embedded wallets in the dashboard. It also refines wallet user data display and pagination in the user interface.

### Detailed summary
- Exposed `WalletUser` type in `packages/thirdweb/src/exports/wallets.ts`.
- Updated API host URL in `apps/dashboard/src/constants/urls.ts`.
- Modified `embeddedWallets` function to include `page` parameter.
- Refactored `useEmbeddedWallets` to support pagination and improved error handling.
- Changed data structure in `useEmbeddedWallets` to return `users` and `totalPages`.
- Updated wallet user display logic in `apps/dashboard/src/components/embedded-wallets/Users/index.tsx`.
- Removed deprecated `withinDays` function from `apps/dashboard/src/utils/date-utils.ts`.
- Enhanced CSV download functionality to include all users and relevant details.

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

<!-- end pr-codex -->
2024-10-29 02:36:37 +00:00
Joaquim VergesandGitHub ad4af68d1d perf: optimize passkey authentication for iOS 15 (#5214) 2024-10-29 14:18:43 +13:00
Jonas DanielsGitHubgithub-actions[bot] <github-actions[bot]@users.noreply.github.com>
8861982877 Version Packages (#5188)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-10-29 14:13:02 +13:00
Joaquim VergesandGitHub 07ea65bcd8 feat: improve ecosystem smart account handling (#5212) 2024-10-29 13:25:21 +13:00
jnsdls 6cb66c675a remove all farcaster frame logic for the moment (#5213)
<!-- start pr-codex -->

## PR-Codex overview
This PR primarily focuses on the removal of multiple files related to Farcaster frames and the `@coinbase/onchainkit` package, as well as some metadata updates in the `package.json` and `pnpm-lock.yaml` files.

### Detailed summary
- Deleted various files related to Farcaster frames and their functionality.
- Removed the `shortenAddress` function from `apps/dashboard/src/utils/string.ts`.
- Updated `package.json` to remove `@coinbase/onchainkit` dependency.
- Cleaned up `pnpm-lock.yaml` by removing references to deleted packages and dependencies.

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

<!-- end pr-codex -->
2024-10-28 23:59:34 +00:00
jnsdls ddec3dadcc Refactor Farcaster frame validation (#5211) 2024-10-28 23:24:52 +00:00
jnsdls ac78881711 update dependencies (#5210)
<!-- start pr-codex -->

## PR-Codex overview
This PR primarily focuses on updating various package dependencies across multiple `package.json` files, ensuring compatibility with newer versions and fixing potential issues.

### Detailed summary
- Updated `vitest` from `2.1.3` to `2.1.4`.
- Updated `lucide-react` from `0.453.0` to `0.454.0` in multiple apps.
- Updated `shiki` from `1.22.0` to `1.22.2` in multiple apps.
- Updated AWS SDK packages to `3.679.0` for `client-kms`, `3.680.0` for `client-lambda`, and `3.679.0` for `credential-providers`.
- Updated `tsx` from `4.19.1` to `4.19.2`.
- Updated `@playwright/test` from `1.47.2` to `1.48.2`.
- Updated `@types/color` from `^3.0.6` to `4.2.0`.
- Updated `knip` from `^5.33.3` to `5.34.2`.
- Updated `@vitest/coverage-v8` and `@vitest/ui` to `2.1.4`.
- Updated `msw` from `^2.4.12` to `^2.5.2`.
- Updated `react-native-svg` from `15.7.1` to `15.8.0`.
- Updated `viem` from `2.21.34` to `2.21.35`.
- Adjusted various package resolutions in `pnpm-lock.yaml`.

> The following files were skipped due to too many changes: `pnpm-lock.yaml`

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

<!-- end pr-codex -->
2024-10-28 23:24:50 +00:00
MananTank be0a4fcf90 Remove unused envs from dashboard .env.example (#5209)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on updating the `.env.example` file for the `dashboard` application by removing deprecated or unnecessary environment variable entries.

### Detailed summary
- Removed the comment regarding `PAYMENTS_ADMIN_SECRET` and its associated empty value.
- Deleted the line to disable telemetry (`NEXT_TELEMETRY_DISABLED`).
- Removed the comment and empty value for `NEXT_PUBLIC_TRENCH_2_API_ENDPOINT`.

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

<!-- end pr-codex -->
2024-10-28 22:27:38 +00:00
MananTank 0b3fe51184 Fix turbo env variable warnings for dashboard build on vercel (#5208)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on updating the `env` configuration in the `turbo.json` file to include additional environment variables necessary for the build process.

### Detailed summary
- Added multiple environment variables to the `env` array in the `thirdweb-dashboard#build` section:
  - `MORALIS_API_KEY`
  - `ETHERSCAN_KEY`
  - `SSR_ALCHEMY_KEY`
  - `BEEHIIV_API_KEY`
  - `GITHUB_API_TOKEN`
  - `LUMA_API_KEY`
  - `HUBSPOT_ACCESS_TOKEN`
  - `API_ROUTES_CLIENT_ID`
  - `DASHBOARD_SECRET_KEY`
  - `SIMPLEHASH_API_KEY`
  - `NEYNAR_API_KEY`
  - `THIRDWEB_ENGINE_URL`
  - `THIRDWEB_ACCESS_TOKEN`
  - `THIRDWEB_ENGINE_ACCESS_TOKEN`
  - `DEGEN_THIRDWEB_ENGINE_WALLET`
  - `THIRDWEB_ENGINE_WALLET`
  - `UNTHREAD_TRIAGE_CHANNEL_ID`
  - `UNTHREAD_EMAIL_INBOX_ID`
  - `UNTHREAD_FREE_TIER_ID`
  - `UNTHREAD_GROWTH_TIER_ID`
  - `UNTHREAD_PRO_TIER_ID`
  - `REDIS_URL`
  - `UNTHREAD_API_KEY`
  - `CHAINSAW_API_KEY`
  - `ANALYTICS_SERVICE_URL`
  - `ANALYTICS_SERVICE_API_KEY`
  - `DISABLE_FAUCET_CHAIN_IDS`
  - `API_SERVER_SECRET`
  - `TURNSTILE_SECRET_KEY`

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

<!-- end pr-codex -->
2024-10-28 22:14:56 +00:00
Joaquim VergesandGitHub adeda1ef76 feat: enhance TransactionButton and add buy support check (#5207) 2024-10-29 10:41:01 +13:00
0xFirekeeper fb5115a90f [Docs] Unity as of 5.7.0 (#5195)
<!-- start pr-codex -->

## PR-Codex overview
This PR focuses on updating the documentation for the `Unity SDK`, including changes to wallet options, sidebar navigation, and the introduction of build instructions. It also replaces an image and adds new sections for configuration and debugging.

### Detailed summary
- Removed `thirdwebmanager.png` and updated image imports.
- Changed title in `page.mdx` from "Unity SDK (v5)" to "Unity SDK".
- Expanded wallet options to include `Github` and `Twitch`.
- Added new `Build Instructions` section in `page.mdx`.
- Updated sidebar to include `Build Instructions` and `Migrate from v4`.
- Revised `Client Settings` and added `Preferences`, `Misc`, and `Debug` sections.

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

<!-- end pr-codex -->
2024-10-28 21:28:29 +00:00
MananTank a5ffc54a25 Fix error thrown from PublishedBy card on localhost chain (#5198)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on refactoring the `ContractOverviewPageClient` component by removing the `PublishedByClient` function and its associated logic, while setting the `publishedBy` prop to `undefined`. This prepares for a future implementation of a fully client-rendered version.

### Detailed summary
- Removed the import statements for `useThirdwebClient`, `useQuery`, and `useActiveAccount`.
- Deleted the `PublishedByClient` function and its logic.
- Updated the `publishedBy` prop in `ContractOverviewPageClient` to `undefined`.
- Added a TODO comment for future implementation of a fully client-rendered version of `publishedBy` and `ContractCard`.

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

<!-- end pr-codex -->
2024-10-28 21:16:01 +00:00
MananTank 286aa30bfc Include chain overrides for all chains and not just custom chains in RSCs (#5197)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on refactoring the code in the `getContractFromParams.ts` file to improve readability. It mainly involves reordering and aligning the code without changing the logic.

### Detailed summary
- Removed unnecessary line breaks for better readability.
- Maintained the structure of the `if` statements and the `try-catch` block.
- Ensured consistent formatting for the `chainOverrides.find` function.

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

<!-- end pr-codex -->
2024-10-28 21:15:59 +00:00
joaquim-verges bb4435233f feat: set default domain for thirdweb auth (#5196)
## Problem solved

Addresses CNCT-2132

<!-- start pr-codex -->

---

## PR-Codex overview
This PR updates the `domain` configuration in the `createAuth` function for the `thirdwebAuth` object, changing its default value to "thirdweb.com".

### Detailed summary
- Updated the `domain` property in `createAuth` from an empty string to "thirdweb.com".

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

<!-- end pr-codex -->
2024-10-28 20:49:42 +00:00
Captain UnknownandGitHub aa0f397778 Fixed a redundant return statement in isLoggedIn() & a typo (#5194)
Signed-off-by: Captain Unknown <[email protected]>
2024-10-28 13:47:24 -07:00
MananTank 7185364e4b redirect /team/~/... to /team/<first-team-slug>/... (#5158)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR adds functionality to redirect requests from `/team/~/...` to `/team/<first_team_slug>/...`, enhancing user navigation by ensuring that users are directed to the first team's slug instead of a placeholder.

### Detailed summary
- Added a check for the path starting with `team` and `~`.
- Implemented a call to `getTeams()` to retrieve the list of teams.
- If a team exists, modified the path to use the first team's slug.
- Redirects the request to the new path format.

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

<!-- end pr-codex -->
2024-10-28 18:37:45 +00:00
MananTank efc8bb7fd1 Fix hsl color preview and improved color-picker UI for connect playground (#5193)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on enhancing the color selection functionality in the `ColorInput` component by integrating a new `Popover` UI from `@radix-ui/react-popover` and a color picker from `react-pick-color`. It also updates the dependencies in `package.json`.

### Detailed summary
- Removed the `ColorInput.module.css` file.
- Added `@radix-ui/react-popover` and `react-pick-color` to `package.json`.
- Updated `ColorInput` component to use `Popover` for color selection.
- Integrated `ColorPicker` within the `PopoverContent`.
- Refactored `onChange` handling in `ColorInput`.

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

<!-- end pr-codex -->
2024-10-28 18:25:26 +00:00
MananTank 74d113cf16 Move engine page components to app router folder in teams layout (#5145)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on restructuring the file organization and import paths for various components within the dashboard application, specifically related to engine functionalities. It also includes updates to stylesheets and components for better modularity.

### Detailed summary
- Updated import paths for several components to a new directory structure.
- Refactored components like `EngineAdmins`, `EngineRelayer`, `EngineExplorer`, etc., to use relative imports from the new structure.
- Adjusted CSS imports for `swagger-ui.css` and other styles.
- Updated story files for `RecentEngineAlerts` and `ManageEngineAlerts` to reflect new paths.
- Modified `EngineSystemMetrics` component imports for better organization.

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

<!-- end pr-codex -->
2024-10-28 18:12:36 +00:00
MananTank 486cd4a255 Fix API key creation on embed page (#5192)
Fixes: DASH-340

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on enhancing the `CreateAPIKey` functionality by introducing prefill options for the API key creation process, improving the user interface for creating API keys, and adding a modal for better user experience.

### Detailed summary
- Added `CreateAPIKeyPrefillOptions` type.
- Introduced `DesktopAPIKeyWordingWithPrefill` story.
- Updated `CreateAPIKeyDialogProps` to include `prefill`.
- Modified `CreateAPIKeyDialogUI` to accept and use `prefill`.
- Enhanced `CreateAPIKeyForm` to utilize prefill values.
- Integrated `LazyCreateAPIKeyDialog` in the `EmbedSetup` component.
- Updated button behavior to show modal for creating API keys.
- Improved user prompts for signing in when no active account is detected.

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

<!-- end pr-codex -->
2024-10-28 18:00:39 +00:00
MananTank 68011d5c76 Add Logged Out Variant of Team Header (#5160)
## Problem solved

Short description of the bug fixed or feature added
2024-10-28 17:48:34 +00:00
MananTank 939e7ab820 Render contract pages on client side for localhost chain (#5185)
Fixes: DASH-348
2024-10-28 17:36:30 +00:00
MananTank 59953f714a Fix MintSupplyTab Amount form field error (#5191)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR updates the validation and input handling for the minting amount in the `MintSupplyTab` component. It ensures that the amount is coerced to a number type and specifies the input type for better user experience.

### Detailed summary
- Changed `amount` validation from `z.number()` to `z.coerce.number()` to ensure the input is coerced to a number.
- Updated the `Input` component to include `type="number"` for the amount field, enhancing user input handling.

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

<!-- end pr-codex -->
2024-10-28 16:35:17 +00:00
GWSzeto 7aab5caf98 Change Modules table to cards UI (#5051)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR introduces a new `ModuleCard` component and its associated storybook entries, enhancing the UI for displaying module information in a dashboard. It also refactors the `InstalledModulesTable` to utilize the new `ModuleCard` component for better modularity and readability.

### Detailed summary
- Added `ModuleCard` component in `module-card.tsx`.
- Created storybook entries for `ModuleCard` in `module-card.stories.tsx`.
- Refactored `InstalledModulesTable` to render `ModuleCard` instead of a table.
- Removed unused skeleton and row components from `InstalledModulesTable`.

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

<!-- end pr-codex -->
2024-10-28 14:28:48 +00:00
Joaquim VergesandGitHub 83a2412941 Revert "[Dashboard] Feature: Adds analytics to in-app-wallets page (#5186) (#5189) 2024-10-28 20:47:08 +13:00
Joaquim VergesandGitHub 84571efaf8 feat: redesign payment selection flow for Buy screen (#5176) 2024-10-28 20:15:19 +13:00
771acd1abe Fix op estimation edge case (#5184)
Co-authored-by: Joaquim Verges <[email protected]>
2024-10-28 15:42:14 +13:00
gregfromstl 2bf65c05b2 [Dashboard] Feature: Adds analytics to in-app-wallets page (#5186)
<img width="1106" alt="Screenshot 2024-10-25 at 4 02 59 PM" src="https://github.com/user-attachments/assets/8b559170-2d5e-4810-8a29-55f155448e40">

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on enhancing the in-app wallets feature by adding tracking capabilities, restructuring components, and improving analytics. It introduces new interfaces, updates existing components, and removes unnecessary elements to streamline functionality and improve user experience.

### Detailed summary
- Added `InAppWalletStats` interface.
- Introduced `TRACKING_CATEGORY` constants for analytics.
- Updated components to utilize new tracking categories.
- Removed `AnalyticsCallout` from layout.
- Replaced `Separator` with `<div className="h-5" />` for spacing.
- Enhanced analytics pages to fetch and display wallet usage data.
- Implemented new routing for in-app wallet analytics.
- Updated `InAppWalletUsersChartCard` for better data visualization.
- Improved the structure of the `Tabs` component for navigation.

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

<!-- end pr-codex -->
2024-10-28 01:30:34 +00:00
Edward SunandGitHub a365dd8ee9 Add a case study to project showcase (#5183) 2024-10-25 12:17:05 -07:00
Edward SunandGitHub 363b63384a [Very minor patch] Add case study button to card (#5181) 2024-10-25 11:45:47 -07:00
Edward SunandGitHub 4ec2aa8dbd Project showcase patches (#5178) 2024-10-25 10:45:34 -07:00
MananTank 63bbb4c42a Fix page crash when using useTxNotifications on pages without ChakraProvider, restore latest sentry version (#5179)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on updating dependencies and modifying the error message presentation in the `errorParser.tsx` file. It upgrades several `@sentry` packages and refines the structure of the error messages displayed to users.

### Detailed summary
- Updated `@sentry/nextjs` from `8.34.0` to `8.35.0`.
- Changed the `PLEASE_REACH_OUT_MESSAGE` structure from using `<Text>` to a `<span>`.
- Updated the class name for the `<Link>` from `font-bold` to `font-semibold`.
- Modified error message rendering in `parseErrorToMessage` to use `<span>` instead of `<Text>`.
- Updated several `@sentry` internal dependencies to version `8.35.0` across `pnpm-lock.yaml`.

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

<!-- end pr-codex -->
2024-10-25 17:38:21 +00:00
kien-ngo 4cb7f8f97f [Dashboard] DASH-347 | Claim issue with EditionDrop (#5177)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on enhancing the `ClaimTabERC1155` component by removing the `useTxNotifications` hook and replacing success/error handling with `toast.promise`. This improves user feedback during transaction processes.

### Detailed summary
- Removed the `useTxNotifications` hook and its related success/error callbacks.
- Introduced `toast.promise` for handling promises related to approving ERC20 tokens and claiming NFTs.
- Improved error handling with `console.error` for better debugging.
- Updated success/error messages for user feedback.

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

<!-- end pr-codex -->
2024-10-25 17:10:53 +00:00
9dfe4af679 Biz hackweek - Project Showcase (#5118)
Co-authored-by: samina <[email protected]>
2024-10-25 09:52:30 -07:00
521 changed files with 11943 additions and 8652 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
-9
View File
@@ -75,15 +75,6 @@ GITHUB_API_TOKEN="ghp_..."
# Upload server url
NEXT_PUBLIC_DASHBOARD_UPLOAD_SERVER="https://storage.thirdweb-preview.com"
# Payments Admin Secret. For dev, get this value from paper-web-dev (on Zeet)'s "HASURA_ADMIN_SECRET_KEY" env var
PAYMENTS_ADMIN_SECRET=""
# Disable telemetry
NEXT_TELEMETRY_DISABLED=1
# Use trench for KYC
NEXT_PUBLIC_TRENCH_2_API_ENDPOINT=""
# Unthread variables - only required for submitting the support form in /support
UNTHREAD_API_KEY=""
UNTHREAD_TRIAGE_CHANNEL_ID=""
+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",
];
+6 -7
View File
@@ -25,7 +25,6 @@
"@chakra-ui/react": "^2.8.2",
"@chakra-ui/styled-system": "^2.9.2",
"@chakra-ui/theme-tools": "^2.1.2",
"@coinbase/onchainkit": "^0.14.2",
"@emotion/react": "11.13.3",
"@emotion/styled": "11.13.0",
"@hookform/resolvers": "^3.9.0",
@@ -46,7 +45,7 @@
"@radix-ui/react-slot": "^1.1.0",
"@radix-ui/react-switch": "^1.1.1",
"@radix-ui/react-tooltip": "1.1.3",
"@sentry/nextjs": "8.34.0",
"@sentry/nextjs": "8.35.0",
"@shazow/whatsabi": "^0.15.4",
"@stripe/react-stripe-js": "^2.8.1",
"@stripe/stripe-js": "^3.5.0",
@@ -62,13 +61,13 @@
"compare-versions": "^6.1.0",
"date-fns": "4.1.0",
"flat": "^6.0.1",
"framer-motion": "11.11.9",
"framer-motion": "11.11.10",
"fuse.js": "7.0.0",
"input-otp": "^1.2.4",
"ioredis": "^5.4.1",
"ipaddr.js": "^2.2.0",
"lottie-react": "^2.4.0",
"lucide-react": "0.453.0",
"lucide-react": "0.454.0",
"next": "14.2.15",
"next-plausible": "^3.12.0",
"next-seo": "^6.5.0",
@@ -108,7 +107,7 @@
"@chromatic-com/storybook": "2.0.2",
"@next/bundle-analyzer": "14.2.15",
"@next/eslint-plugin-next": "14.2.15",
"@playwright/test": "1.47.2",
"@playwright/test": "1.48.2",
"@storybook/addon-essentials": "8.3.6",
"@storybook/addon-interactions": "8.3.6",
"@storybook/addon-links": "8.3.6",
@@ -118,7 +117,7 @@
"@storybook/nextjs": "8.3.6",
"@storybook/react": "8.3.6",
"@storybook/test": "8.3.6",
"@types/color": "^3.0.6",
"@types/color": "4.2.0",
"@types/node": "20.14.9",
"@types/papaparse": "^5.3.15",
"@types/pluralize": "^0.0.33",
@@ -136,7 +135,7 @@
"eslint-config-biome": "1.9.3",
"eslint-plugin-react-compiler": "0.0.0-experimental-fa06e2c-20241014",
"eslint-plugin-storybook": "^0.9.0",
"knip": "^5.33.3",
"knip": "5.34.2",
"next-sitemap": "^4.2.3",
"postcss": "8.4.47",
"storybook": "8.3.6",
Binary file not shown.

After

Width:  |  Height:  |  Size: 551 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 730 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 721 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 631 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 134 KiB

+26 -4
View File
@@ -1,6 +1,7 @@
/* eslint-disable @next/next/no-img-element */
"use client";
import { useState } from "react";
import { useRef, useState } from "react";
import { useIsomorphicLayoutEffect } from "../../lib/useIsomorphicLayoutEffect";
import { cn } from "../../lib/utils";
type imgElementProps = React.DetailedHTMLProps<
@@ -25,19 +26,39 @@ export function Img(props: imgElementProps) {
const { className, fallback, skeleton, ...restProps } = props;
const defaultSkeleton = <div className="animate-pulse bg-accent" />;
const defaultFallback = <div className="bg-muted" />;
const imgRef = useRef<HTMLImageElement>(null);
useIsomorphicLayoutEffect(() => {
const imgEl = imgRef.current;
if (!imgEl) {
return;
}
if (imgEl.complete) {
setStatus("loaded");
} else {
function handleLoad() {
setStatus("loaded");
}
imgEl.addEventListener("load", handleLoad);
return () => {
imgEl.removeEventListener("load", handleLoad);
};
}
}, []);
return (
<div className="relative">
<img
{...restProps}
onLoad={() => {
setStatus("loaded");
}}
// avoid setting empty src string to prevent request to the entire page
src={restProps.src || undefined}
ref={imgRef}
onError={() => {
setStatus("fallback");
}}
style={{
opacity: status === "loaded" ? 1 : 0,
...restProps.style,
}}
alt={restProps.alt || ""}
className={cn(
@@ -48,6 +69,7 @@ export function Img(props: imgElementProps) {
{status !== "loaded" && (
<div
style={restProps.style}
className={cn(
"fade-in-0 absolute inset-0 overflow-hidden transition-opacity duration-300 [&>*]:h-full [&>*]:w-full",
className,
@@ -0,0 +1,11 @@
"use client";
import { Spinner } from "@/components/ui/Spinner/Spinner";
export function GenericLoadingPage() {
return (
<div className="flex min-h-[500px] grow items-center justify-center rounded-lg border border-border">
<Spinner className="size-10" />
</div>
);
}
@@ -63,8 +63,11 @@ export const embeddedWalletsKeys = {
all: ["embeddedWallets"] as const,
wallet: (walletAddress: string) =>
[...embeddedWalletsKeys.all, walletAddress] as const,
embeddedWallets: (walletAddress: string, clientId: string | undefined) =>
[...embeddedWalletsKeys.wallet(walletAddress), clientId] as const,
embeddedWallets: (
walletAddress: string,
clientId: string | undefined,
page: number,
) => [...embeddedWalletsKeys.wallet(walletAddress), clientId, page] as const,
};
export const engineKeys = {
@@ -12,21 +12,24 @@ import { useLoggedInUser } from "./useLoggedInUser";
// FIXME: We keep repeating types, API server should provide them
export enum AccountStatus {
NoCustomer = "noCustomer",
NoPayment = "noPayment",
PaymentVerification = "paymentVerification",
ValidPayment = "validPayment",
InvalidPayment = "invalidPayment",
InvalidPaymentMethod = "invalidPaymentMethod",
}
export const accountStatus = {
noCustomer: "noCustomer",
noPayment: "noPayment",
paymentVerification: "paymentVerification",
validPayment: "validPayment",
invalidPayment: "invalidPayment",
invalidPaymentMethod: "invalidPaymentMethod",
} as const;
export enum AccountPlan {
Free = "free",
Growth = "growth",
Pro = "pro",
Enterprise = "enterprise",
}
export const accountPlan = {
free: "free",
growth: "growth",
pro: "pro",
enterprise: "enterprise",
} as const;
export type AccountStatus = (typeof accountStatus)[keyof typeof accountStatus];
export type AccountPlan = (typeof accountPlan)[keyof typeof accountPlan];
export type AuthorizedWallet = {
id: string;
@@ -235,6 +238,14 @@ export interface WalletStats {
walletType: string;
}
export interface InAppWalletStats {
date: string;
authenticationMethod: string;
uniqueWalletsConnected: number;
}
export interface EcosystemWalletStats extends InAppWalletStats {}
export interface UserOpStats {
date: string;
successful: number;
@@ -436,6 +447,43 @@ async function getUserOpUsage(args: {
return json.data;
}
export async function getInAppWalletUsage(args: {
clientId: string;
from?: Date;
to?: Date;
period?: "day" | "week" | "month" | "year" | "all";
}) {
const { clientId, from, to, period } = args;
const searchParams = new URLSearchParams();
searchParams.append("clientId", clientId);
if (from) {
searchParams.append("from", from.toISOString());
}
if (to) {
searchParams.append("to", to.toISOString());
}
if (period) {
searchParams.append("period", period);
}
const res = await fetch(
`${THIRDWEB_ANALYTICS_API_HOST}/v1/wallets/in-app?${searchParams.toString()}`,
{
method: "GET",
headers: {
"Content-Type": "application/json",
},
},
);
const json = await res?.json();
if (!res || res.status !== 200) {
throw new Error(json.message);
}
return json.data;
}
export function useUserOpUsageAggregate(args: {
clientId: string;
from?: Date;
@@ -18,7 +18,7 @@ export function useDashboardContractMetadata(contract: ThirdwebContract) {
});
}
type DashboardContractMetadata = {
export type DashboardContractMetadata = {
name: string;
symbol: string;
image: string;
@@ -1,51 +1,43 @@
import { useQuery } from "@tanstack/react-query";
import { keepPreviousData, useQuery } from "@tanstack/react-query";
import { THIRDWEB_EWS_API_HOST } from "constants/urls";
import type { WalletUser } from "thirdweb/wallets";
import { embeddedWalletsKeys } from "../cache-keys";
import { useLoggedInUser } from "./useLoggedInUser";
// FIXME: Make API to return camelCase or transform
export type EmbeddedWalletUser = {
id: string;
client_id: string;
created_at: string;
last_accessed_at: string;
embedded_wallet?: {
id: string;
address: string;
chain: string;
wallet_user_id: string;
}[];
ews_authed_user: {
id: string;
authed_user_id: string;
email: string;
}[];
};
export function useEmbeddedWallets(clientId: string) {
export function useEmbeddedWallets(clientId: string, page: number) {
const { user, isLoggedIn } = useLoggedInUser();
return useQuery({
queryKey: embeddedWalletsKeys.embeddedWallets(
user?.address as string,
clientId as string,
clientId,
page,
),
queryFn: async () => {
const res = await fetch(
`${THIRDWEB_EWS_API_HOST}/api/thirdweb/embedded-wallet?clientId=${clientId}&lastAccessedAt=0`,
{
method: "GET",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${user?.jwt}`,
},
},
const url = new URL(
`${THIRDWEB_EWS_API_HOST}/api/2024-05-05/account/list`,
);
url.searchParams.append("clientId", clientId);
url.searchParams.append("page", page.toString());
const res = await fetch(url.href, {
method: "GET",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${user?.jwt}`,
},
});
if (!res.ok) {
throw new Error(`Failed to fetch wallets: ${await res.text()}`);
}
const json = await res.json();
return json.walletUsers as EmbeddedWalletUser[];
return json as {
users: WalletUser[];
totalPages: number;
};
},
placeholderData: keepPreviousData,
enabled: !!user?.address && isLoggedIn && !!clientId,
});
}
@@ -1,7 +1,7 @@
"use client";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import type { ResultItem } from "components/engine/system-metrics/components/StatusCodes";
import type { ResultItem } from "app/team/[team_slug]/[project_slug]/engine/(instance)/[engineId]/metrics/components/StatusCodes";
import { THIRDWEB_API_HOST } from "constants/urls";
import type { EngineBackendWalletType } from "lib/engine";
import { useState } from "react";
@@ -208,6 +208,7 @@ export function FaucetButton({
<FormItem>
<FormControl>
<Turnstile
className="mt-4"
siteKey={TURNSTILE_SITE_KEY}
onSuccess={(token) => field.onChange(token)}
/>
@@ -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>
);
};
@@ -0,0 +1,27 @@
"use client";
import type { ThirdwebContract } from "thirdweb";
import { ErrorPage, LoadingPage } from "../../_components/page-skeletons";
import { RedirectToContractOverview } from "../../_components/redirect-contract-overview.client";
import { useContractPageMetadata } from "../../_hooks/useContractPageMetadata";
import { ContractDirectListingsPage } from "./ContractDirectListingsPage";
export function ContractDirectListingsPageClient(props: {
contract: ThirdwebContract;
}) {
const metadataQuery = useContractPageMetadata(props.contract);
if (metadataQuery.isPending) {
return <LoadingPage />;
}
if (metadataQuery.isError) {
return <ErrorPage />;
}
if (!metadataQuery.data.isDirectListingSupported) {
return <RedirectToContractOverview contract={props.contract} />;
}
return <ContractDirectListingsPage contract={props.contract} />;
}
@@ -1,7 +1,9 @@
import { notFound, redirect } from "next/navigation";
import { localhost } from "thirdweb/chains";
import { getContractPageParamsInfo } from "../../_utils/getContractFromParams";
import { getContractPageMetadata } from "../../_utils/getContractPageMetadata";
import { ContractDirectListingsPage } from "./ContractDirectListingsPage";
import { ContractDirectListingsPageClient } from "./ContractDirectListingsPage.client";
export default async function Page(props: {
params: {
@@ -15,6 +17,10 @@ export default async function Page(props: {
notFound();
}
if (info.chainMetadata.chainId === localhost.id) {
return <ContractDirectListingsPageClient contract={info.contract} />;
}
const { isDirectListingSupported } = await getContractPageMetadata(
info.contract,
);
@@ -0,0 +1,27 @@
"use client";
import type { ThirdwebContract } from "thirdweb";
import { ErrorPage, LoadingPage } from "../../_components/page-skeletons";
import { RedirectToContractOverview } from "../../_components/redirect-contract-overview.client";
import { useContractPageMetadata } from "../../_hooks/useContractPageMetadata";
import { ContractEnglishAuctionsPage } from "./ContractEnglishAuctionsPage";
export function ContractEnglishAuctionsPageClient(props: {
contract: ThirdwebContract;
}) {
const metadataQuery = useContractPageMetadata(props.contract);
if (metadataQuery.isPending) {
return <LoadingPage />;
}
if (metadataQuery.isError) {
return <ErrorPage />;
}
if (!metadataQuery.data.isEnglishAuctionSupported) {
return <RedirectToContractOverview contract={props.contract} />;
}
return <ContractEnglishAuctionsPage contract={props.contract} />;
}
@@ -1,7 +1,9 @@
import { notFound, redirect } from "next/navigation";
import { localhost } from "thirdweb/chains";
import { getContractPageParamsInfo } from "../../_utils/getContractFromParams";
import { getContractPageMetadata } from "../../_utils/getContractPageMetadata";
import { ContractEnglishAuctionsPage } from "./ContractEnglishAuctionsPage";
import { ContractEnglishAuctionsPageClient } from "./ContractEnglishAuctionsPage.client";
export default async function Page(props: {
params: {
@@ -15,6 +17,10 @@ export default async function Page(props: {
notFound();
}
if (info.chainMetadata.chainId === localhost.id) {
return <ContractEnglishAuctionsPageClient contract={info.contract} />;
}
const { isEnglishAuctionSupported } = await getContractPageMetadata(
info.contract,
);
@@ -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>
);
};
@@ -0,0 +1,17 @@
import { Spinner } from "@/components/ui/Spinner/Spinner";
export function LoadingPage() {
return (
<div className="flex min-h-[300px] grow items-center justify-center">
<Spinner className="size-10" />
</div>
);
}
export function ErrorPage() {
return (
<div className="flex min-h-[300px] grow items-center justify-center">
<p className="text-destructive-text">Failed to load contract</p>
</div>
);
}
@@ -0,0 +1,24 @@
"use client";
import { useDashboardRouter } from "@/lib/DashboardRouter";
import { useEffect, useRef } from "react";
import type { ThirdwebContract } from "thirdweb";
import { LoadingPage } from "./page-skeletons";
export function RedirectToContractOverview(props: {
contract: ThirdwebContract;
}) {
const router = useDashboardRouter();
const redirected = useRef(false);
// eslint-disable-next-line no-restricted-syntax
useEffect(() => {
if (redirected.current) {
return;
}
redirected.current = true;
router.replace(`/${props.contract.chain.id}/${props.contract.address}`);
}, [router, props.contract]);
return <LoadingPage />;
}
@@ -0,0 +1,18 @@
"use client";
import { useQuery } from "@tanstack/react-query";
import type { ThirdwebContract } from "thirdweb";
import { getContractPageMetadataSetup } from "../_utils/getContractPageMetadata";
export function useContractPageMetadata(contract: ThirdwebContract) {
return useQuery({
queryKey: ["getContractPageMetadataSetup", contract],
queryFn: () => {
return getContractPageMetadataSetup(contract, () =>
Promise.resolve(false),
);
},
retry: false,
refetchOnWindowFocus: false,
});
}
@@ -0,0 +1,13 @@
import { useQuery } from "@tanstack/react-query";
import { type ThirdwebContract, resolveContractAbi } from "thirdweb/contract";
export function useResolveContractABI(contract: ThirdwebContract) {
return useQuery({
queryKey: ["resolveContractAbi", contract],
queryFn: () => {
return resolveContractAbi(contract);
},
retry: false,
refetchOnWindowFocus: false,
});
}
@@ -4,8 +4,8 @@ import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { Button } from "@/components/ui/button";
import { CheckIcon, CircleAlertIcon, RotateCcwIcon } from "lucide-react";
import { useState } from "react";
import { ConfigureNetworks } from "../../../../../components/configure-networks/ConfigureNetworks";
import { addChainOverrides } from "../../../../../stores/chainStores";
import { addChainOverrides } from "stores/chainStores";
import { ConfigureNetworks } from "../../../../../../components/configure-networks/ConfigureNetworks";
export function ConfigureCustomChain(props: {
chainSlug: string;
@@ -1,5 +1,8 @@
import { getThirdwebClient } from "@/constants/thirdweb.server";
import { fetchDashboardContractMetadata } from "@3rdweb-sdk/react/hooks/useDashboardContractMetadata";
import {
type DashboardContractMetadata,
fetchDashboardContractMetadata,
} from "@3rdweb-sdk/react/hooks/useDashboardContractMetadata";
import { fetchPublishedContractsFromDeploy } from "components/contract-components/fetchPublishedContractsFromDeploy";
import type { ThirdwebContract } from "thirdweb";
import type { ChainMetadata } from "thirdweb/chains";
@@ -8,12 +11,34 @@ import { MetadataHeader } from "./metadata-header";
interface ContractMetadataProps {
contract: ThirdwebContract;
chain: ChainMetadata;
contractMetadata: DashboardContractMetadata | undefined;
externalLinks:
| {
name: string;
url: string;
}[]
| undefined;
}
export async function ContractMetadata({
export function ContractMetadata({
contract,
chain,
contractMetadata,
externalLinks,
}: ContractMetadataProps) {
return (
<MetadataHeader
data={contractMetadata}
chain={chain}
address={contract.address}
externalLinks={externalLinks}
/>
);
}
export async function getContractMetadataHeaderData(
contract: ThirdwebContract,
) {
const settledResults = await Promise.allSettled([
fetchDashboardContractMetadata(contract),
fetchPublishedContractsFromDeploy({
@@ -34,12 +59,8 @@ export async function ContractMetadata({
const latestPublished = publishedContractsFromDeploy?.slice(-1)[0];
return (
<MetadataHeader
data={contractMetadata}
chain={chain}
address={contract.address}
externalLinks={latestPublished?.externalLinks}
/>
);
return {
contractMetadata,
externalLinks: latestPublished?.externalLinks,
};
}
@@ -0,0 +1,49 @@
"use client";
import { useQuery } from "@tanstack/react-query";
import type { ThirdwebContract } from "thirdweb";
import type { ChainMetadata } from "thirdweb/chains";
import { ErrorPage, LoadingPage } from "../_components/page-skeletons";
import { useContractPageMetadata } from "../_hooks/useContractPageMetadata";
import { getContractPageSidebarLinks } from "../_utils/getContractPageSidebarLinks";
import { getContractMetadataHeaderData } from "./contract-metadata";
import { ContractPageLayout } from "./contract-page-layout";
export function ContractPageLayoutClient(props: {
chainMetadata: ChainMetadata;
contract: ThirdwebContract;
children: React.ReactNode;
}) {
const metadataQuery = useContractPageMetadata(props.contract);
const headerMetadataQuery = useQuery({
queryKey: ["getContractMetadataHeaderData", props.contract],
queryFn: async () => {
return await getContractMetadataHeaderData(props.contract);
},
retry: false,
refetchOnWindowFocus: false,
});
if (metadataQuery.isPending) {
return <LoadingPage />;
}
if (metadataQuery.isError) {
return <ErrorPage />;
}
const sidebarLinks = getContractPageSidebarLinks({
chainSlug: props.chainMetadata.slug,
contractAddress: props.contract.address,
metadata: metadataQuery.data,
});
return (
<ContractPageLayout
{...props}
sidebarLinks={sidebarLinks}
dashboardContractMetadata={headerMetadataQuery.data?.contractMetadata}
externalLinks={headerMetadataQuery.data?.externalLinks}
/>
);
}
@@ -0,0 +1,62 @@
import { ChakraProviderSetup } from "@/components/ChakraProviderSetup";
import type { SidebarLink } from "@/components/blocks/Sidebar";
import { SidebarLayout } from "@/components/blocks/SidebarLayout";
import type { DashboardContractMetadata } from "@3rdweb-sdk/react/hooks/useDashboardContractMetadata";
import { DeprecatedAlert } from "components/shared/DeprecatedAlert";
import type { ThirdwebContract } from "thirdweb";
import type { ChainMetadata } from "thirdweb/chains";
import { ContractMetadata } from "./contract-metadata";
import { PrimaryDashboardButton } from "./primary-dashboard-button";
export function ContractPageLayout(props: {
chainMetadata: ChainMetadata;
contract: ThirdwebContract;
children: React.ReactNode;
sidebarLinks: SidebarLink[];
dashboardContractMetadata: DashboardContractMetadata | undefined;
externalLinks:
| {
name: string;
url: string;
}[]
| undefined;
}) {
const {
chainMetadata,
contract,
sidebarLinks,
dashboardContractMetadata,
externalLinks,
} = props;
return (
<ChakraProviderSetup>
<SidebarLayout sidebarLinks={sidebarLinks}>
<div className="border-border border-b pb-8">
<div className="flex flex-col gap-4 ">
<div className="flex flex-col justify-between gap-4 md:flex-row">
<ContractMetadata
contract={contract}
chain={chainMetadata}
contractMetadata={dashboardContractMetadata}
externalLinks={externalLinks}
/>
<PrimaryDashboardButton
contractAddress={contract.address}
chain={contract.chain}
contractInfo={{
chain: chainMetadata,
chainSlug: chainMetadata.slug,
contractAddress: contract.address,
}}
/>
</div>
<DeprecatedAlert chain={chainMetadata} />
</div>
</div>
<div className="h-8" />
<div className="pb-10">{props.children}</div>
</SidebarLayout>
</ChakraProviderSetup>
);
}
@@ -20,27 +20,25 @@ export async function getContractPageParamsInfo(params: {
// move on
}
if (!chainMetadata) {
const cookieStore = cookies();
const localChainStoreValue = cookieStore.get(TW_LOCAL_CHAIN_STORE)?.value;
const cookieStore = cookies();
const localChainStoreValue = cookieStore.get(TW_LOCAL_CHAIN_STORE)?.value;
if (localChainStoreValue) {
try {
const chains = JSON.parse(decodeURIComponent(localChainStoreValue));
if (typeof chains === "object" && Array.isArray(chains)) {
const chainOverrides = chains as ChainMetadata[];
const chain = chainOverrides.find(
(c) =>
c.slug === chainSlugOrId ||
c.chainId === Number.parseInt(chainSlugOrId),
);
if (chain) {
chainMetadata = chain;
}
if (localChainStoreValue) {
try {
const chains = JSON.parse(decodeURIComponent(localChainStoreValue));
if (typeof chains === "object" && Array.isArray(chains)) {
const chainOverrides = chains as ChainMetadata[];
const chain = chainOverrides.find(
(c) =>
c.slug === chainSlugOrId ||
c.chainId === Number.parseInt(chainSlugOrId),
);
if (chain) {
chainMetadata = chain;
}
} catch {
// noop
}
} catch {
// noop
}
}
@@ -46,8 +46,13 @@ export type ContractPageMetadata = {
functionSelectors: string[];
};
export async function getContractPageMetadata(
export async function getContractPageMetadata(contract: ThirdwebContract) {
return getContractPageMetadataSetup(contract, isAnalyticsSupportedForChain);
}
export async function getContractPageMetadataSetup(
contract: ThirdwebContract,
isAnalyticsSupportedFn: (chainId: number) => Promise<boolean>,
): Promise<ContractPageMetadata> {
const [
functionSelectorsResult,
@@ -55,7 +60,7 @@ export async function getContractPageMetadata(
contractTypeResult,
] = await Promise.allSettled([
resolveFunctionSelectors(contract),
isAnalyticsSupportedForChain(contract.chain.id),
isAnalyticsSupportedFn(contract.chain.id),
getContractType({ contract }),
]);
@@ -0,0 +1,27 @@
"use client";
import type { ThirdwebContract } from "thirdweb";
import { ErrorPage, LoadingPage } from "../_components/page-skeletons";
import { RedirectToContractOverview } from "../_components/redirect-contract-overview.client";
import { useContractPageMetadata } from "../_hooks/useContractPageMetadata";
import { AccountSigners } from "./components/account-signers";
export function AccountSignersClient(props: {
contract: ThirdwebContract;
}) {
const metadataQuery = useContractPageMetadata(props.contract);
if (metadataQuery.isPending) {
return <LoadingPage />;
}
if (metadataQuery.isError) {
return <ErrorPage />;
}
if (!metadataQuery.data.isAccountPermissionsSupported) {
return <RedirectToContractOverview contract={props.contract} />;
}
return <AccountSigners contract={props.contract} />;
}
@@ -31,14 +31,19 @@ export const AccountSigners: React.FC<AccountSignersProps> = ({ contract }) => {
);
const data = transformedAdmins.concat(transformedSigners || []);
return (
<div className="flex flex-col gap-6">
{data.map((item) => (
<AccountSigner
key={item.signer}
item={item}
contractChainId={contract.chain.id}
/>
))}
<div>
<h1 className="mb-4 font-semibold text-2xl tracking-tight">
Account Signers
</h1>
<div className="flex flex-col gap-6">
{data.map((item) => (
<AccountSigner
key={item.signer}
item={item}
contractChainId={contract.chain.id}
/>
))}
</div>
</div>
);
};
@@ -1,6 +1,8 @@
import { notFound, redirect } from "next/navigation";
import { localhost } from "thirdweb/chains";
import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
import { getContractPageMetadata } from "../_utils/getContractPageMetadata";
import { AccountSignersClient } from "./AccountSigners.client";
import { AccountSigners } from "./components/account-signers";
export default async function Page(props: {
@@ -15,7 +17,12 @@ export default async function Page(props: {
notFound();
}
const { contract } = info;
const { contract, chainMetadata } = info;
if (chainMetadata.chainId === localhost.id) {
return <AccountSignersClient contract={contract} />;
}
const { isAccountPermissionsSupported } =
await getContractPageMetadata(contract);
@@ -23,12 +30,5 @@ export default async function Page(props: {
redirect(`/${props.params.chain_id}/${props.params.contractAddress}`);
}
return (
<div>
<h1 className="mb-4 font-semibold text-2xl tracking-tight">
Account Signers
</h1>
<AccountSigners contract={contract} />
</div>
);
return <AccountSigners contract={contract} />;
}
@@ -0,0 +1,34 @@
"use client";
import type { ThirdwebContract } from "thirdweb";
import type { ChainMetadata } from "thirdweb/chains";
import { ErrorPage, LoadingPage } from "../_components/page-skeletons";
import { RedirectToContractOverview } from "../_components/redirect-contract-overview.client";
import { useContractPageMetadata } from "../_hooks/useContractPageMetadata";
import { AccountPage } from "./AccountPage";
export function AccountPageClient(props: {
contract: ThirdwebContract;
chainMetadata: ChainMetadata;
}) {
const metadataQuery = useContractPageMetadata(props.contract);
if (metadataQuery.isPending) {
return <LoadingPage />;
}
if (metadataQuery.isError) {
return <ErrorPage />;
}
if (!metadataQuery.data.isAccount) {
return <RedirectToContractOverview contract={props.contract} />;
}
return (
<AccountPage
contract={props.contract}
chainMetadata={props.chainMetadata}
/>
);
}
@@ -1,7 +1,9 @@
import { notFound, redirect } from "next/navigation";
import { localhost } from "thirdweb/chains";
import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
import { getContractPageMetadata } from "../_utils/getContractPageMetadata";
import { AccountPage } from "./AccountPage";
import { AccountPageClient } from "./AccountPage.client";
export default async function Page(props: {
params: {
@@ -16,6 +18,12 @@ export default async function Page(props: {
}
const { contract, chainMetadata } = info;
if (contract.chain.id === localhost.id) {
return (
<AccountPageClient contract={contract} chainMetadata={chainMetadata} />
);
}
const { isAccount } = await getContractPageMetadata(contract);
if (!isAccount) {
@@ -0,0 +1,27 @@
"use client";
import type { ThirdwebContract } from "thirdweb";
import { ErrorPage, LoadingPage } from "../_components/page-skeletons";
import { RedirectToContractOverview } from "../_components/redirect-contract-overview.client";
import { useContractPageMetadata } from "../_hooks/useContractPageMetadata";
import { AccountsPage } from "./AccountsPage";
export function AccountsPageClient(props: {
contract: ThirdwebContract;
}) {
const metadataQuery = useContractPageMetadata(props.contract);
if (metadataQuery.isPending) {
return <LoadingPage />;
}
if (metadataQuery.isError) {
return <ErrorPage />;
}
if (!metadataQuery.data.isAccountFactory) {
return <RedirectToContractOverview contract={props.contract} />;
}
return <AccountsPage contract={props.contract} />;
}
@@ -1,7 +1,9 @@
import { notFound, redirect } from "next/navigation";
import { localhost } from "thirdweb/chains";
import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
import { getContractPageMetadata } from "../_utils/getContractPageMetadata";
import { AccountsPage } from "./AccountsPage";
import { AccountsPageClient } from "./AccountsPage.client";
export default async function Page(props: {
params: {
@@ -15,7 +17,12 @@ export default async function Page(props: {
notFound();
}
const { contract } = info;
const { contract, chainMetadata } = info;
if (chainMetadata.chainId === localhost.id) {
return <AccountsPageClient contract={contract} />;
}
const { isAccountFactory } = await getContractPageMetadata(contract);
if (!isAccountFactory) {
@@ -0,0 +1,27 @@
"use client";
import type { ThirdwebContract } from "thirdweb";
import { ErrorPage, LoadingPage } from "../_components/page-skeletons";
import { RedirectToContractOverview } from "../_components/redirect-contract-overview.client";
import { useContractPageMetadata } from "../_hooks/useContractPageMetadata";
import { ContractAnalyticsPage } from "./ContractAnalyticsPage";
export function ContractAnalyticsPageClient(props: {
contract: ThirdwebContract;
}) {
const metadataQuery = useContractPageMetadata(props.contract);
if (metadataQuery.isPending) {
return <LoadingPage />;
}
if (metadataQuery.isError) {
return <ErrorPage />;
}
if (!metadataQuery.data.isAnalyticsSupported) {
return <RedirectToContractOverview contract={props.contract} />;
}
return <ContractAnalyticsPage contract={props.contract} />;
}
@@ -1,7 +1,9 @@
import { notFound, redirect } from "next/navigation";
import { localhost } from "thirdweb/chains";
import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
import { getContractPageMetadata } from "../_utils/getContractPageMetadata";
import { ContractAnalyticsPage } from "./ContractAnalyticsPage";
import { ContractAnalyticsPageClient } from "./ContractAnalyticsPage.client";
export default async function Page(props: {
params: {
@@ -15,6 +17,10 @@ export default async function Page(props: {
notFound();
}
if (info.chainMetadata.chainId === localhost.id) {
return <ContractAnalyticsPageClient contract={info.contract} />;
}
const { isAnalyticsSupported } = await getContractPageMetadata(info.contract);
if (!isAnalyticsSupported) {
@@ -0,0 +1,38 @@
"use client";
import type { ThirdwebContract } from "thirdweb";
import { ClaimConditions } from "../_components/claim-conditions/claim-conditions";
import { ErrorPage, LoadingPage } from "../_components/page-skeletons";
import { RedirectToContractOverview } from "../_components/redirect-contract-overview.client";
import { useContractPageMetadata } from "../_hooks/useContractPageMetadata";
export function ClaimConditionsClient(props: {
contract: ThirdwebContract;
}) {
const metadataQuery = useContractPageMetadata(props.contract);
if (metadataQuery.isPending) {
return <LoadingPage />;
}
if (metadataQuery.isError) {
return <ErrorPage />;
}
const {
isERC20ClaimConditionsSupported,
isERC721ClaimConditionsSupported,
supportedERCs,
} = metadataQuery.data;
if (!isERC20ClaimConditionsSupported && !isERC721ClaimConditionsSupported) {
return <RedirectToContractOverview contract={props.contract} />;
}
return (
<ClaimConditions
contract={props.contract}
isERC20={supportedERCs.isERC20}
/>
);
}
@@ -1,7 +1,9 @@
import { notFound, redirect } from "next/navigation";
import { localhost } from "thirdweb/chains";
import { ClaimConditions } from "../_components/claim-conditions/claim-conditions";
import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
import { getContractPageMetadata } from "../_utils/getContractPageMetadata";
import { ClaimConditionsClient } from "./ClaimConditions.client";
export default async function Page(props: {
params: {
@@ -14,8 +16,12 @@ export default async function Page(props: {
if (!info) {
notFound();
}
const { contract, chainMetadata } = info;
if (chainMetadata.chainId === localhost.id) {
return <ClaimConditionsClient contract={contract} />;
}
const { contract } = info;
const {
isERC20ClaimConditionsSupported,
isERC721ClaimConditionsSupported,
@@ -0,0 +1,26 @@
"use client";
import type { ChainMetadata } from "thirdweb/chains";
import type { ThirdwebContract } from "thirdweb/contract";
import { LoadingPage } from "../_components/page-skeletons";
import { useResolveContractABI } from "../_hooks/useResolveContractABI";
import { ContractCodePage } from "./contract-code-page";
export function ContractCodePageClient(props: {
contract: ThirdwebContract;
chainMetadata: ChainMetadata;
}) {
const abiQuery = useResolveContractABI(props.contract);
if (abiQuery.isPending) {
return <LoadingPage />;
}
return (
<ContractCodePage
abi={abiQuery.data}
contract={props.contract}
chainMetadata={props.chainMetadata}
/>
);
}
@@ -0,0 +1,33 @@
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import type { Abi } from "abitype";
import { CircleAlertIcon } from "lucide-react";
import type { ChainMetadata } from "thirdweb/chains";
import type { ThirdwebContract } from "thirdweb/contract";
import { CodeOverview } from "../../../../../../contract-ui/tabs/code/components/code-overview";
export function ContractCodePage(props: {
abi: Abi | undefined;
contract: ThirdwebContract;
chainMetadata: ChainMetadata;
}) {
if (props.abi) {
return (
<CodeOverview
abi={props.abi}
contractAddress={props.contract.address}
chainId={props.contract.chain.id}
/>
);
}
return (
<Alert variant="destructive">
<CircleAlertIcon className="size-5" />
<AlertTitle>Failed to resolve contract ABI</AlertTitle>
<AlertDescription>
Please verify that contract address is correct and deployed on "
{props.chainMetadata.name}" chain.
</AlertDescription>
</Alert>
);
}
@@ -1,9 +1,9 @@
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { CircleAlertIcon } from "lucide-react";
import { notFound } from "next/navigation";
import { localhost } from "thirdweb/chains";
import { resolveContractAbi } from "thirdweb/contract";
import { CodeOverview } from "../../../../../../contract-ui/tabs/code/components/code-overview";
import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
import { ContractCodePage } from "./contract-code-page";
import { ContractCodePageClient } from "./contract-code-page.client";
export default async function Page(props: {
params: {
@@ -19,26 +19,22 @@ export default async function Page(props: {
const { contract, chainMetadata } = info;
try {
const abi = await resolveContractAbi(contract);
if (contract.chain.id === localhost.id) {
return (
<CodeOverview
abi={abi}
contractAddress={contract.address}
chainId={contract.chain.id}
<ContractCodePageClient
contract={contract}
chainMetadata={chainMetadata}
/>
);
} catch {
return (
<Alert variant="destructive">
<CircleAlertIcon className="size-5" />
<AlertTitle>Failed to resolve contract ABI</AlertTitle>
<AlertDescription>
Please verify that contract address is correct and deployed on "
{chainMetadata.name}" chain.
</AlertDescription>
</Alert>
);
}
const abi = await resolveContractAbi(contract).catch(() => undefined);
return (
<ContractCodePage
abi={abi}
contract={contract}
chainMetadata={chainMetadata}
/>
);
}
@@ -0,0 +1,32 @@
"use client";
import type { ThirdwebContract } from "thirdweb";
import { ErrorPage, LoadingPage } from "../_components/page-skeletons";
import { RedirectToContractOverview } from "../_components/redirect-contract-overview.client";
import { useContractPageMetadata } from "../_hooks/useContractPageMetadata";
import { EmbedSetup } from "./embed-setup";
export function EmbedSetupClient(props: {
contract: ThirdwebContract;
}) {
const metadataQuery = useContractPageMetadata(props.contract);
if (metadataQuery.isPending) {
return <LoadingPage />;
}
if (metadataQuery.isError) {
return <ErrorPage />;
}
if (metadataQuery.data.embedType === null) {
return <RedirectToContractOverview contract={props.contract} />;
}
return (
<EmbedSetup
contract={props.contract}
ercOrMarketplace={metadataQuery.data.embedType}
/>
);
}
@@ -1,19 +1,22 @@
"use client";
import { Button } from "@/components/ui/button";
import { useApiKeys, useCreateApiKey } from "@3rdweb-sdk/react/hooks/useApi";
import { Flex, FormControl, Input, Link, Select } from "@chakra-ui/react";
import { Flex, FormControl, Input, Select } from "@chakra-ui/react";
import { LazyCreateAPIKeyDialog } from "components/settings/ApiKeys/Create/LazyCreateAPIKeyDialog";
import { useTrack } from "hooks/analytics/useTrack";
import { useAllChainsData } from "hooks/chains/allChains";
import { useClipboard } from "hooks/useClipboard";
import { useTxNotifications } from "hooks/useTxNotifications";
import { CheckIcon, CopyIcon } from "lucide-react";
import { useMemo } from "react";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { useMemo, useState } from "react";
import { useForm } from "react-hook-form";
import type { StoredChain } from "stores/chainStores";
import type { ThirdwebContract } from "thirdweb";
import type { ChainMetadata } from "thirdweb/chains";
import { useActiveAccount } from "thirdweb/react";
import {
Button,
Card,
CodeBlock,
FormHelperText,
@@ -211,10 +214,6 @@ export const EmbedSetup: React.FC<EmbedSetupProps> = ({
const apiKeys = useApiKeys();
const createKeyMutation = useCreateApiKey();
const { onSuccess, onError } = useTxNotifications(
"API key created",
"Failed to create API key",
);
const validApiKey = (apiKeys.data || []).find(
(apiKey) =>
@@ -298,14 +297,35 @@ export const EmbedSetup: React.FC<EmbedSetupProps> = ({
);
const { hasCopied, onCopy } = useClipboard(embedCode, 3000);
const [showCreateAPIKeyModal, setShowCreateAPIKeyModal] = useState(false);
const activeAccount = useActiveAccount();
const pathname = usePathname();
return (
<Flex gap={8} direction="column">
<LazyCreateAPIKeyDialog
wording="api-key"
prefill={{
name: "Embed API Key",
domains: "embed.ipfscdn.io",
}}
open={showCreateAPIKeyModal}
onOpenChange={setShowCreateAPIKeyModal}
onCreateAndComplete={() => {
trackEvent({
category: "api-keys",
action: "create",
label: "success",
fromEmbed: true,
});
apiKeys.refetch();
}}
/>
<Flex gap={8} direction={{ base: "column", md: "row" }}>
<Card className="flex w-full flex-col gap-2 md:w-1/2">
<Heading size="title.sm" mb={4}>
Configuration
</Heading>
<Card className="flex w-full flex-col gap-5 md:w-1/2">
<Heading size="title.sm">Configuration</Heading>
{ercOrMarketplace === "marketplace" ? (
<FormControl>
<FormLabel>Listing ID</FormLabel>
@@ -315,6 +335,7 @@ export const EmbedSetup: React.FC<EmbedSetupProps> = ({
</FormHelperText>
</FormControl>
) : null}
{ercOrMarketplace === "marketplace-v3" ? (
<FormControl>
<FormLabel>Listing type</FormLabel>
@@ -327,6 +348,7 @@ export const EmbedSetup: React.FC<EmbedSetupProps> = ({
</FormHelperText>
</FormControl>
) : null}
{ercOrMarketplace === "marketplace-v3" &&
watch("listingType") === "direct-listing" ? (
<FormControl>
@@ -337,6 +359,7 @@ export const EmbedSetup: React.FC<EmbedSetupProps> = ({
</FormHelperText>
</FormControl>
) : null}
{ercOrMarketplace === "marketplace-v3" &&
watch("listingType") === "english-auction" ? (
<FormControl>
@@ -347,6 +370,7 @@ export const EmbedSetup: React.FC<EmbedSetupProps> = ({
</FormHelperText>
</FormControl>
) : null}
{ercOrMarketplace === "erc1155" ? (
<FormControl>
<FormLabel>Token ID</FormLabel>
@@ -356,9 +380,20 @@ export const EmbedSetup: React.FC<EmbedSetupProps> = ({
</FormHelperText>
</FormControl>
) : null}
<FormControl>
<FormLabel>Client ID</FormLabel>
{validApiKey ? (
{!activeAccount ? (
<Button asChild className="w-full">
<Link
href={`/login${
pathname ? `?next=${encodeURIComponent(pathname)}` : ""
}`}
>
Sign in to create a client ID
</Link>
</Button>
) : validApiKey ? (
<Input
readOnly
disabled
@@ -366,11 +401,7 @@ export const EmbedSetup: React.FC<EmbedSetupProps> = ({
/>
) : (
<Button
bgColor="bgBlack"
color="bgWhite"
_hover={{
opacity: 0.8,
}}
className="w-full"
onClick={() => {
trackEvent({
category: "api-keys",
@@ -379,44 +410,7 @@ export const EmbedSetup: React.FC<EmbedSetupProps> = ({
fromEmbed: true,
});
createKeyMutation.mutate(
{
name: "Embed API key",
domains: ["embed.ipfscdn.io"],
services: [
{
name: "rpc",
targetAddresses: ["*"],
},
{
name: "storage",
targetAddresses: ["*"],
actions: ["read"],
},
],
},
{
onSuccess: () => {
onSuccess();
trackEvent({
category: "api-keys",
action: "create",
label: "success",
fromEmbed: true,
});
},
onError: (err) => {
onError(err);
trackEvent({
category: "api-keys",
action: "create",
label: "error",
error: err,
fromEmbed: true,
});
},
},
);
setShowCreateAPIKeyModal(true);
}}
disabled={createKeyMutation.isPending}
>
@@ -428,14 +422,15 @@ export const EmbedSetup: React.FC<EmbedSetupProps> = ({
You need a client ID to use embeds.{" "}
<Link
href="https://portal.thirdweb.com/account/api-keys"
color="primary.500"
isExternal
className="text-link-foreground hover:text-foreground"
target="_blank"
>
Learn more
</Link>
.
</FormHelperText>
</FormControl>
<FormControl>
<FormLabel>RPC Url</FormLabel>
<Input type="url" {...register("rpcUrl")} />
@@ -477,8 +472,8 @@ export const EmbedSetup: React.FC<EmbedSetupProps> = ({
A relayer can be used to make the transaction gasless for the
end user.{" "}
<Link
isExternal
color="blue.500"
target="_blank"
className="text-link-foreground hover:text-foreground"
href="https://blog.thirdweb.com/guides/setup-gasless-transactions"
>
Learn more
@@ -486,6 +481,7 @@ export const EmbedSetup: React.FC<EmbedSetupProps> = ({
</FormHelperText>
</FormControl>
)}
<FormControl>
<Heading size="title.sm" my={4}>
Customization
@@ -501,6 +497,7 @@ export const EmbedSetup: React.FC<EmbedSetupProps> = ({
on the user system&apos;s preferences.
</FormHelperText>
</FormControl>
<FormControl>
<FormLabel>Primary Color</FormLabel>
<Select {...register("primaryColor")}>
@@ -517,6 +514,7 @@ export const EmbedSetup: React.FC<EmbedSetupProps> = ({
Used for the main actions button backgrounds.
</FormHelperText>
</FormControl>
{ercOrMarketplace === "marketplace" ||
ercOrMarketplace === "marketplace-v3" ? (
<FormControl>
@@ -535,6 +533,7 @@ export const EmbedSetup: React.FC<EmbedSetupProps> = ({
</FormControl>
) : null}
</Card>
<Card className="flex w-full flex-col gap-2 md:w-1/2">
<Heading size="title.sm">Embed Code</Heading>
<CodeBlock
@@ -545,8 +544,7 @@ export const EmbedSetup: React.FC<EmbedSetupProps> = ({
language="markup"
/>
<Button
colorScheme="purple"
w="auto"
className="w-auto gap-2"
variant="outline"
onClick={() => {
onCopy();
@@ -558,14 +556,12 @@ export const EmbedSetup: React.FC<EmbedSetupProps> = ({
chainId,
});
}}
leftIcon={
hasCopied ? (
<CheckIcon className="size-4" />
) : (
<CopyIcon className="size-4" />
)
}
>
{hasCopied ? (
<CheckIcon className="size-4" />
) : (
<CopyIcon className="size-4" />
)}
{hasCopied ? "Copied!" : "Copy to clipboard"}
</Button>
</Card>
@@ -1,6 +1,8 @@
import { notFound, redirect } from "next/navigation";
import { localhost } from "thirdweb/chains";
import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
import { getContractPageMetadata } from "../_utils/getContractPageMetadata";
import { EmbedSetupClient } from "./EmbedSetup.client";
import { EmbedSetup } from "./embed-setup";
export default async function Page(props: {
@@ -15,11 +17,16 @@ export default async function Page(props: {
notFound();
}
const { embedType } = await getContractPageMetadata(info.contract);
const { contract } = info;
if (contract.chain.id === localhost.id) {
return <EmbedSetupClient contract={contract} />;
}
const { embedType } = await getContractPageMetadata(contract);
if (embedType === null) {
redirect(`/${props.params.chain_id}/${props.params.contractAddress}`);
}
return <EmbedSetup contract={info.contract} ercOrMarketplace={embedType} />;
return <EmbedSetup contract={contract} ercOrMarketplace={embedType} />;
}
@@ -0,0 +1,31 @@
"use client";
import type { ThirdwebContract } from "thirdweb";
import type { ChainMetadata } from "thirdweb/chains";
import { LoadingPage } from "../_components/page-skeletons";
import { useResolveContractABI } from "../_hooks/useResolveContractABI";
import { ContractExplorerPage } from "./ContractExplorerPage";
interface ContractExplorePageProps {
contract: ThirdwebContract;
chainMetadata: ChainMetadata;
}
export const ContractExplorerPageClient: React.FC<ContractExplorePageProps> = ({
contract,
chainMetadata,
}) => {
const abiQuery = useResolveContractABI(contract);
if (abiQuery.isPending) {
return <LoadingPage />;
}
return (
<ContractExplorerPage
abi={abiQuery.data}
contract={contract}
chainMetadata={chainMetadata}
/>
);
};
@@ -1,21 +1,36 @@
"use client";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { Flex } from "@chakra-ui/react";
import type { Abi } from "abitype";
import { getContractFunctionsFromAbi } from "components/contract-components/getContractFunctionsFromAbi";
import { ContractFunctionsOverview } from "components/contract-functions/contract-functions";
import { CircleAlertIcon } from "lucide-react";
import type { ThirdwebContract } from "thirdweb";
// TODO - figure out why we have to add "use client" here and it does not work without it
import type { ChainMetadata } from "thirdweb/chains";
interface ContractExplorePageProps {
contract: ThirdwebContract;
abi: Abi;
abi: Abi | undefined;
chainMetadata: ChainMetadata;
}
export const ContractExplorerPage: React.FC<ContractExplorePageProps> = ({
contract,
abi,
chainMetadata,
}) => {
if (!abi) {
return (
<Alert variant="destructive">
<CircleAlertIcon className="size-5" />
<AlertTitle>Failed to resolve contract ABI</AlertTitle>
<AlertDescription>
Please verify that contract address is correct and deployed on "
{chainMetadata.name}" chain.
</AlertDescription>
</Alert>
);
}
const functions = getContractFunctionsFromAbi(abi);
return (
<Flex direction="column" h="70vh">
@@ -1,9 +1,9 @@
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { CircleAlertIcon } from "lucide-react";
import { notFound } from "next/navigation";
import { localhost } from "thirdweb/chains";
import { resolveContractAbi } from "thirdweb/contract";
import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
import { ContractExplorerPage } from "./ContractExplorerPage";
import { ContractExplorerPageClient } from "./ContractExplorerPage.client";
export default async function Page(props: {
params: {
@@ -19,19 +19,22 @@ export default async function Page(props: {
const { contract, chainMetadata } = info;
try {
const abi = await resolveContractAbi(contract);
return <ContractExplorerPage contract={contract} abi={abi} />;
} catch {
if (contract.chain.id === localhost.id) {
return (
<Alert variant="destructive">
<CircleAlertIcon className="size-5" />
<AlertTitle>Failed to resolve contract ABI</AlertTitle>
<AlertDescription>
Please verify that contract address is correct and deployed on "
{chainMetadata.name}" chain.
</AlertDescription>
</Alert>
<ContractExplorerPageClient
contract={contract}
chainMetadata={chainMetadata}
/>
);
}
const abi = await resolveContractAbi(contract).catch(() => undefined);
return (
<ContractExplorerPage
contract={contract}
abi={abi}
chainMetadata={chainMetadata}
/>
);
}
@@ -1,15 +1,14 @@
import { ChakraProviderSetup } from "@/components/ChakraProviderSetup";
import { SidebarLayout } from "@/components/blocks/SidebarLayout";
import { ContractMetadata } from "components/custom-contract/contract-header/contract-metadata";
import { DeprecatedAlert } from "components/shared/DeprecatedAlert";
import type { Metadata } from "next";
import { notFound } from "next/navigation";
import { localhost } from "thirdweb/chains";
import { getContractMetadata } from "thirdweb/extensions/common";
import { isAddress, isContractDeployed } from "thirdweb/utils";
import { resolveFunctionSelectors } from "../../../../../lib/selectors";
import { shortenIfAddress } from "../../../../../utils/usedapp-external";
import { ConfigureCustomChain } from "./ConfigureCustomChain";
import { PrimaryDashboardButton } from "./_components/primary-dashboard-button";
import { ConfigureCustomChain } from "./_layout/ConfigureCustomChain";
import { getContractMetadataHeaderData } from "./_layout/contract-metadata";
import { ContractPageLayout } from "./_layout/contract-page-layout";
import { ContractPageLayoutClient } from "./_layout/contract-page-layout.client";
import { supportedERCs } from "./_utils/detectedFeatures/supportedERCs";
import { getContractPageParamsInfo } from "./_utils/getContractFromParams";
import { getContractPageMetadata } from "./_utils/getContractPageMetadata";
@@ -38,44 +37,44 @@ export default async function Layout(props: {
notFound();
}
if (contract.chain.id === localhost.id) {
return (
<ContractPageLayoutClient
chainMetadata={chainMetadata}
contract={contract}
>
{props.children}
</ContractPageLayoutClient>
);
}
// check if the contract exists
const isValidContract = await isContractDeployed(contract).catch(() => false);
if (!isValidContract) {
// TODO - replace 404 with a better page to upsale deploy or other thirdweb products
notFound();
}
const contractPageMetadata = await getContractPageMetadata(contract);
const sidebarLinks = getContractPageSidebarLinks({
chainSlug: chainMetadata.slug,
contractAddress: contract.address,
metadata: contractPageMetadata,
});
const { contractMetadata, externalLinks } =
await getContractMetadataHeaderData(contract);
return (
<ChakraProviderSetup>
<SidebarLayout sidebarLinks={sidebarLinks}>
<div className="border-border border-b pb-8">
<div className="flex flex-col gap-4 ">
<div className="flex flex-col justify-between gap-4 md:flex-row">
<ContractMetadata contract={contract} chain={chainMetadata} />
<PrimaryDashboardButton
contractAddress={contract.address}
chain={contract.chain}
contractInfo={{
chain: chainMetadata,
chainSlug: chainMetadata.slug,
contractAddress: contract.address,
}}
/>
</div>
<DeprecatedAlert chain={chainMetadata} />
</div>
</div>
<div className="h-8" />
<div className="pb-10">{props.children}</div>
</SidebarLayout>
</ChakraProviderSetup>
<ContractPageLayout
chainMetadata={chainMetadata}
contract={contract}
sidebarLinks={sidebarLinks}
dashboardContractMetadata={contractMetadata}
externalLinks={externalLinks}
>
{props.children}
</ContractPageLayout>
);
}
@@ -0,0 +1,27 @@
"use client";
import type { ThirdwebContract } from "thirdweb";
import { ErrorPage, LoadingPage } from "../_components/page-skeletons";
import { RedirectToContractOverview } from "../_components/redirect-contract-overview.client";
import { useContractPageMetadata } from "../_hooks/useContractPageMetadata";
import { ContractEditModulesPage } from "./ContractEditModulesPage";
export function ContractEditModulesPageClient(props: {
contract: ThirdwebContract;
}) {
const metadataQuery = useContractPageMetadata(props.contract);
if (metadataQuery.isPending) {
return <LoadingPage />;
}
if (metadataQuery.isError) {
return <ErrorPage />;
}
if (!metadataQuery.data.isModularCore) {
return <RedirectToContractOverview contract={props.contract} />;
}
return <ContractEditModulesPage contract={props.contract} />;
}
@@ -1,35 +1,11 @@
"use client";
import { WalletAddress } from "@/components/blocks/wallet-address";
import { CopyAddressButton } from "@/components/ui/CopyAddressButton";
import { ScrollShadow } from "@/components/ui/ScrollShadow/ScrollShadow";
import { Spinner } from "@/components/ui/Spinner/Spinner";
import { Alert, AlertTitle } from "@/components/ui/alert";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Skeleton } from "@/components/ui/skeleton";
import { ToolTipLabel } from "@/components/ui/tooltip";
import { useMutation } from "@tanstack/react-query";
import { TransactionButton } from "components/buttons/TransactionButton";
import { CircleSlash, TrashIcon } from "lucide-react";
import { useState } from "react";
import { toast } from "sonner";
import {
type ContractOptions,
getContract,
sendTransaction,
waitForReceipt,
} from "thirdweb";
import { uninstallModuleByProxy } from "thirdweb/modules";
import { CircleSlash } from "lucide-react";
import type { ContractOptions } from "thirdweb";
import type { Account } from "thirdweb/wallets";
import { useModuleContractInfo } from "./moduleContractInfo";
import { ModuleCard } from "./module-card";
export const InstalledModulesTable = (props: {
contract: ContractOptions;
@@ -38,7 +14,7 @@ export const InstalledModulesTable = (props: {
isPending: boolean;
};
refetchModules: () => void;
ownerAccount?: Account;
ownerAccount: Account | undefined;
}) => {
const { installedModules, ownerAccount } = props;
@@ -66,249 +42,18 @@ export const InstalledModulesTable = (props: {
<>
{sectionTitle}
<ScrollShadow scrollableClassName="rounded-lg">
<table className="w-full selection:bg-inverted selection:text-inverted-foreground">
<thead>
<TableHeadingRow>
<TableHeading> Module Name </TableHeading>
<TableHeading> Description </TableHeading>
<TableHeading> Publisher Address </TableHeading>
<TableHeading> Module Address </TableHeading>
<TableHeading> Version </TableHeading>
{ownerAccount && <TableHeading> Remove </TableHeading>}
</TableHeadingRow>
</thead>
<tbody>
{installedModules.isPending ? (
<>
<SkeletonRow ownerAccount={ownerAccount} />
<SkeletonRow ownerAccount={ownerAccount} />
<SkeletonRow ownerAccount={ownerAccount} />
</>
) : (
<>
{installedModules.data?.map((e, i) => (
<ModuleRow
// biome-ignore lint/suspicious/noArrayIndexKey: FIXME
key={i}
moduleAddress={e}
contract={props.contract}
onRemoveModule={props.refetchModules}
ownerAccount={ownerAccount}
/>
))}
</>
)}
</tbody>
</table>
<div className="flex flex-col gap-6">
{installedModules.data?.map((moduleAddress) => (
<ModuleCard
key={moduleAddress}
moduleAddress={moduleAddress}
contract={props.contract}
onRemoveModule={props.refetchModules}
ownerAccount={ownerAccount}
/>
))}
</div>
</ScrollShadow>
</>
);
};
function SkeletonRow(props: { ownerAccount?: Account }) {
return (
<TableRow>
<TableData>
<Skeleton className="h-6" />
</TableData>
<TableData>
<Skeleton className="h-6" />
</TableData>
<TableData>
<Skeleton className="h-6" />
</TableData>
<TableData>
<Skeleton className="h-6" />
</TableData>
{/* Version */}
<TableData>
<Skeleton className="h-6" />
</TableData>
{/* Remove */}
{props.ownerAccount && (
<TableData>
<Skeleton className="h-6" />
</TableData>
)}
</TableRow>
);
}
function ModuleRow(props: {
moduleAddress: string;
contract: ContractOptions;
onRemoveModule: () => void;
ownerAccount?: Account;
}) {
const { contract, moduleAddress, ownerAccount } = props;
const [isUninstallModalOpen, setIsUninstallModalOpen] = useState(false);
const contractInfo = useModuleContractInfo(
getContract({
address: moduleAddress,
chain: contract.chain,
client: contract.client,
}),
);
const uninstallMutation = useMutation({
mutationFn: async (account: Account) => {
const uninstallTransaction = uninstallModuleByProxy({
contract,
chain: contract.chain,
client: contract.client,
moduleProxyAddress: moduleAddress,
moduleData: "0x",
});
const txResult = await sendTransaction({
transaction: uninstallTransaction,
account,
});
await waitForReceipt(txResult);
},
onSuccess() {
toast.success("Module Removed successfully");
props.onRemoveModule();
},
onError(error) {
toast.error("Failed to remove module");
console.error("Error during uninstallation:", error);
},
});
const handleRemove = async () => {
if (!ownerAccount) {
return;
}
setIsUninstallModalOpen(false);
uninstallMutation.mutate(ownerAccount);
};
if (!contractInfo) {
return <SkeletonRow ownerAccount={ownerAccount} />;
}
return (
<TableRow>
<TableData>
<p>{contractInfo.name}</p>
</TableData>
<TableData>
<p>{contractInfo.description || "..."}</p>
</TableData>
<TableData>
<WalletAddress address={contractInfo.publisher || ""} />
</TableData>
<TableData>
<CopyAddressButton
className="text-xs"
address={moduleAddress || ""}
copyIconPosition="left"
variant="outline"
/>
</TableData>
{/* Version */}
<TableData>
<p>{contractInfo.version}</p>
</TableData>
{/* Remove */}
{ownerAccount && (
<TableData>
<div>
<ToolTipLabel label="Remove Module">
<Button
onClick={() => setIsUninstallModalOpen(true)}
variant="outline"
className="rounded-xl p-3 text-red-500"
>
{uninstallMutation.isPending ? (
<Spinner className="size-4" />
) : (
<TrashIcon className="size-4" />
)}
</Button>
</ToolTipLabel>
</div>
</TableData>
)}
<Dialog
open={isUninstallModalOpen}
onOpenChange={setIsUninstallModalOpen}
>
<DialogContent className="z-[10001]" dialogOverlayClassName="z-[10000]">
<form
onSubmit={(e) => {
e.preventDefault();
handleRemove();
}}
>
<DialogHeader>
<DialogTitle>Uninstall Module</DialogTitle>
<DialogDescription>
Are you sure you want to uninstall{" "}
<span className="font-medium text-foreground ">
{contractInfo.name}
</span>{" "}
?
</DialogDescription>
</DialogHeader>
<DialogFooter className="mt-10 flex-row justify-end gap-3 md:gap-1">
<Button
type="button"
onClick={() => setIsUninstallModalOpen(false)}
variant="outline"
>
Cancel
</Button>
<TransactionButton
txChainID={contract.chain.id}
transactionCount={1}
isLoading={uninstallMutation.isPending}
type="submit"
colorScheme="red"
className="flex"
>
Uninstall
</TransactionButton>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
</TableRow>
);
}
function TableRow(props: { children: React.ReactNode }) {
return (
<tr className="border-border border-b [&:last-child]:border-b-0">
{props.children}
</tr>
);
}
function TableData({ children }: { children: React.ReactNode }) {
return <td className="px-3 py-4 text-sm">{children}</td>;
}
function TableHeading(props: { children: React.ReactNode }) {
return (
<th className="min-w-[150px] border-border border-b px-3 py-3 text-left font-medium text-muted-foreground text-sm">
{props.children}
</th>
);
}
function TableHeadingRow({ children }: { children: React.ReactNode }) {
return <tr className="relative bg-muted/50">{children}</tr>;
}
@@ -0,0 +1,120 @@
import type { Meta, StoryObj } from "@storybook/react";
import { useMutation } from "@tanstack/react-query";
import { Toaster } from "sonner";
import { BadgeContainer, mobileViewport } from "stories/utils";
import { ThirdwebProvider } from "thirdweb/react";
import { ModuleCardUI } from "./module-card";
const meta = {
title: "Modules/ModuleCard",
component: Component,
parameters: {
layout: "centered",
},
} satisfies Meta<typeof Component>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Desktop: Story = {
args: {},
};
export const Mobile: Story = {
args: {},
parameters: {
viewport: mobileViewport("iphone14"),
},
};
function Component() {
const removeMutation = useMutation({
mutationFn: async () => {
await new Promise((resolve) => setTimeout(resolve, 1000));
},
});
const updateMutation = useMutation({
mutationFn: async () => {
await new Promise((resolve) => setTimeout(resolve, 1000));
},
});
const contractInfo = {
name: "Module Name",
description:
"lorem ipsum dolor sit amet consectetur adipisicing elit sed do eiusmod tempor incididunt ut labore ",
publisher: "0xdd99b75f095d0c4d5112aCe938e4e6ed962fb024",
version: "1.0.0",
};
return (
<ThirdwebProvider>
<div className="container flex max-w-[1150px] flex-col gap-10 py-10">
<BadgeContainer label="No Update, No Children, Owner Account">
<ModuleCardUI
contractInfo={contractInfo}
moduleAddress="0x0000000000000000000000000000000000000000"
uninstallButton={{
onClick: () => removeMutation.mutateAsync(),
isPending: removeMutation.isPending,
}}
isOwnerAccount={true}
/>
</BadgeContainer>
<BadgeContainer label="No Update, No Children, Not Owner Account">
<ModuleCardUI
contractInfo={contractInfo}
moduleAddress="0x0000000000000000000000000000000000000000"
uninstallButton={{
onClick: () => removeMutation.mutateAsync(),
isPending: removeMutation.isPending,
}}
isOwnerAccount={false}
/>
</BadgeContainer>
<BadgeContainer label="Update Button (disabled), No Children, Owner">
<ModuleCardUI
contractInfo={contractInfo}
moduleAddress="0x0000000000000000000000000000000000000000"
uninstallButton={{
onClick: () => removeMutation.mutateAsync(),
isPending: removeMutation.isPending,
}}
updateButton={{
isDisabled: true,
isPending: updateMutation.isPending,
onClick: () => updateMutation.mutateAsync(),
}}
isOwnerAccount={true}
/>
</BadgeContainer>
<BadgeContainer label="Update Button (enabled), Children, Owner">
<ModuleCardUI
contractInfo={contractInfo}
moduleAddress="0x0000000000000000000000000000000000000000"
uninstallButton={{
onClick: () => removeMutation.mutateAsync(),
isPending: removeMutation.isPending,
}}
updateButton={{
isDisabled: false,
isPending: updateMutation.isPending,
onClick: () => updateMutation.mutateAsync(),
}}
isOwnerAccount={true}
>
<div className="flex h-36 items-center justify-center rounded-lg bg-muted text-muted-foreground text-sm">
CHILDREN
</div>
</ModuleCardUI>
</BadgeContainer>
<Toaster richColors />
</div>
</ThirdwebProvider>
);
}
@@ -0,0 +1,288 @@
import { WalletAddress } from "@/components/blocks/wallet-address";
import { CopyAddressButton } from "@/components/ui/CopyAddressButton";
import { Spinner } from "@/components/ui/Spinner/Spinner";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { Skeleton } from "@/components/ui/skeleton";
import { useMutation } from "@tanstack/react-query";
import { TransactionButton } from "components/buttons/TransactionButton";
import { InfoIcon } from "lucide-react";
import { useState } from "react";
import { toast } from "sonner";
import {
type ContractOptions,
getContract,
sendTransaction,
waitForReceipt,
} from "thirdweb";
import { uninstallModuleByProxy } from "thirdweb/modules";
import type { Account } from "thirdweb/wallets";
import { useModuleContractInfo } from "./moduleContractInfo";
type ModuleProps = {
moduleAddress: string;
contract: ContractOptions;
onRemoveModule: () => void;
ownerAccount: Account | undefined;
};
export function ModuleCard(props: ModuleProps) {
const { contract, moduleAddress, ownerAccount } = props;
const [isUninstallModalOpen, setIsUninstallModalOpen] = useState(false);
const contractInfo = useModuleContractInfo(
getContract({
address: moduleAddress,
chain: contract.chain,
client: contract.client,
}),
);
const uninstallMutation = useMutation({
mutationFn: async (account: Account) => {
const uninstallTransaction = uninstallModuleByProxy({
contract,
chain: contract.chain,
client: contract.client,
moduleProxyAddress: moduleAddress,
moduleData: "0x",
});
const txResult = await sendTransaction({
transaction: uninstallTransaction,
account,
});
await waitForReceipt(txResult);
},
onSuccess() {
toast.success("Module uninstalled successfully");
props.onRemoveModule();
},
onError(error) {
toast.error("Failed to uninstall module");
console.error(error);
},
});
const handleRemove = async () => {
if (!ownerAccount) {
toast.error("Wallet is not connected");
return;
}
uninstallMutation.mutate(ownerAccount);
};
if (!contractInfo) {
return <Skeleton className="h-[190px] w-full border border-border" />;
}
return (
<>
<ModuleCardUI
contractInfo={{
name: contractInfo.name,
description: contractInfo.description,
publisher: contractInfo.publisher,
version: contractInfo.version,
}}
isOwnerAccount={!!ownerAccount}
uninstallButton={{
onClick: () => {
setIsUninstallModalOpen(true);
},
isPending: uninstallMutation.isPending,
}}
moduleAddress={moduleAddress}
/>
<Dialog
open={isUninstallModalOpen}
onOpenChange={setIsUninstallModalOpen}
>
<DialogContent className="z-[10001]" dialogOverlayClassName="z-[10000]">
<form
onSubmit={(e) => {
e.preventDefault();
handleRemove();
}}
>
<DialogHeader>
<DialogTitle>Uninstall Module</DialogTitle>
<DialogDescription>
Are you sure you want to uninstall{" "}
<span className="font-medium text-foreground ">
{contractInfo.name}
</span>{" "}
?
</DialogDescription>
</DialogHeader>
<DialogFooter className="mt-10 flex-row justify-end gap-3 md:gap-1">
<Button
type="button"
onClick={() => setIsUninstallModalOpen(false)}
variant="outline"
>
Cancel
</Button>
<TransactionButton
txChainID={contract.chain.id}
transactionCount={1}
isLoading={uninstallMutation.isPending}
type="submit"
colorScheme="red"
className="flex"
>
Uninstall
</TransactionButton>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
</>
);
}
export type ModuleCardUIProps = {
children?: React.ReactNode;
contractInfo: {
name: string;
description?: string;
version?: string;
publisher?: string;
};
moduleAddress: string;
isOwnerAccount: boolean;
uninstallButton: {
onClick: () => void;
isPending: boolean;
};
updateButton?: {
onClick: () => void;
isPending: boolean;
isDisabled: boolean;
};
};
export function ModuleCardUI(props: ModuleCardUIProps) {
return (
<section className="rounded-lg border border-border bg-muted/50">
{/* Header */}
<div className="relative p-4 lg:p-6">
{/* Title */}
<div className="pr-14">
<h3 className="mb-1 gap-2 font-semibold text-xl tracking-tight">
{props.contractInfo.name}
{/* Info Dialog */}
<Dialog>
<DialogTrigger asChild>
<Button
variant="ghost"
className="absolute top-4 right-4 h-auto w-auto p-2 text-muted-foreground"
>
<InfoIcon className="size-5" />
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>{props.contractInfo.name}</DialogTitle>
<DialogDescription>
{props.contractInfo.description}
</DialogDescription>
{/* Avoid adding focus on other elements to prevent tooltips from opening on modal open */}
<input className="sr-only" aria-hidden />
<div className="h-2" />
<div className="flex flex-col gap-4">
{props.contractInfo.version && (
<div>
<p className="text-muted-foreground text-sm">
{" "}
Version{" "}
</p>
<p> {props.contractInfo.version}</p>
</div>
)}
{props.contractInfo.publisher && (
<div>
<p className="text-muted-foreground text-sm">
Published By
</p>
<WalletAddress address={props.contractInfo.publisher} />
</div>
)}
<div>
<p className="mb-1 text-muted-foreground text-sm">
Module Address
</p>
<CopyAddressButton
className="text-xs"
address={props.moduleAddress}
copyIconPosition="left"
variant="outline"
/>
</div>
</div>
</DialogHeader>
</DialogContent>
</Dialog>
</h3>
{/* Description */}
<p className="text-muted-foreground">
{props.contractInfo.description}
</p>
</div>
{props.children ? (
<>
<div className="h-5" />
{props.children}
</>
) : null}
</div>
<div className="flex flex-row justify-end gap-3 border-border border-t p-4 lg:p-6">
<Button
size="sm"
onClick={props.uninstallButton.onClick}
variant="destructive"
className="min-w-24 gap-2"
disabled={!props.isOwnerAccount}
>
{props.uninstallButton.isPending && <Spinner className="size-4" />}
Uninstall
</Button>
{props.isOwnerAccount && props.updateButton && (
<Button
size="sm"
className="min-w-24 gap-2"
type="submit"
onClick={props.updateButton.onClick}
disabled={props.updateButton.isPending || !props.isOwnerAccount}
>
{props.updateButton.isPending && <Spinner className="size-4" />}
Update
</Button>
)}
</div>
</section>
);
}
@@ -1,7 +1,9 @@
import { notFound, redirect } from "next/navigation";
import { localhost } from "thirdweb/chains";
import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
import { getContractPageMetadata } from "../_utils/getContractPageMetadata";
import { ContractEditModulesPage } from "./ContractEditModulesPage";
import { ContractEditModulesPageClient } from "./ContractEditModulesPage.client";
export default async function Page(props: {
params: {
@@ -15,11 +17,16 @@ export default async function Page(props: {
notFound();
}
const { isModularCore } = await getContractPageMetadata(info.contract);
const { contract } = info;
if (contract.chain.id === localhost.id) {
return <ContractEditModulesPageClient contract={contract} />;
}
const { isModularCore } = await getContractPageMetadata(contract);
if (!isModularCore) {
redirect(`/${props.params.chain_id}/${props.params.contractAddress}`);
}
return <ContractEditModulesPage contract={info.contract} />;
return <ContractEditModulesPage contract={contract} />;
}
@@ -0,0 +1,35 @@
"use client";
import type { ThirdwebContract } from "thirdweb";
import { ErrorPage, LoadingPage } from "../_components/page-skeletons";
import { RedirectToContractOverview } from "../_components/redirect-contract-overview.client";
import { useContractPageMetadata } from "../_hooks/useContractPageMetadata";
import { ContractNFTPage } from "./ContractNFTPage";
export function ContractNFTPageClient(props: {
contract: ThirdwebContract;
}) {
const metadataQuery = useContractPageMetadata(props.contract);
if (metadataQuery.isPending) {
return <LoadingPage />;
}
if (metadataQuery.isError) {
return <ErrorPage />;
}
const { supportedERCs, functionSelectors } = metadataQuery.data;
if (!supportedERCs.isERC721 && !supportedERCs.isERC1155) {
return <RedirectToContractOverview contract={props.contract} />;
}
return (
<ContractNFTPage
contract={props.contract}
isErc721={supportedERCs.isERC721}
functionSelectors={functionSelectors}
/>
);
}
@@ -0,0 +1,37 @@
"use client";
import type { ThirdwebContract } from "thirdweb";
import { ErrorPage, LoadingPage } from "../../_components/page-skeletons";
import { RedirectToContractOverview } from "../../_components/redirect-contract-overview.client";
import { useContractPageMetadata } from "../../_hooks/useContractPageMetadata";
import { TokenIdPage } from "./token-id";
export function TokenIdPageClient(props: {
contract: ThirdwebContract;
tokenId: string;
}) {
const { contract } = props;
const metadataQuery = useContractPageMetadata(props.contract);
if (metadataQuery.isPending) {
return <LoadingPage />;
}
if (metadataQuery.isError) {
return <ErrorPage />;
}
const { supportedERCs } = metadataQuery.data;
if (!supportedERCs.isERC721 && !supportedERCs.isERC1155) {
return <RedirectToContractOverview contract={props.contract} />;
}
return (
<TokenIdPage
contract={contract}
isErc721={supportedERCs.isERC721}
tokenId={props.tokenId}
/>
);
}
@@ -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
@@ -3,7 +3,6 @@
import { Flex, FormControl, Input } from "@chakra-ui/react";
import { TransactionButton } from "components/buttons/TransactionButton";
import { useTrack } from "hooks/analytics/useTrack";
import { useTxNotifications } from "hooks/useTxNotifications";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { type ThirdwebContract, ZERO_ADDRESS } from "thirdweb";
@@ -23,13 +22,6 @@ const ClaimTabERC1155: React.FC<ClaimTabProps> = ({ contract, tokenId }) => {
const form = useForm<{ to: string; amount: string }>({
defaultValues: { amount: "1", to: address },
});
const { onSuccess, onError } = useTxNotifications(
"Claimed successfully",
"Failed to claim",
contract,
);
const sendAndConfirmTx = useSendAndConfirmTransaction();
const account = useActiveAccount();
return (
@@ -59,28 +51,34 @@ const ClaimTabERC1155: React.FC<ClaimTabProps> = ({ contract, tokenId }) => {
account,
});
if (approveTx) {
try {
await sendAndConfirmTx.mutateAsync(approveTx);
} catch {
return toast.error("Error approving ERC20 token");
}
const approvalPromise = sendAndConfirmTx.mutateAsync(approveTx);
toast.promise(approvalPromise, {
loading: "Approving ERC20 token for this claim",
success: "Approved succesfully",
error: "Failed to approve ERC20",
});
await approvalPromise;
}
await sendAndConfirmTx.mutateAsync(transaction);
const promise = sendAndConfirmTx.mutateAsync(transaction);
toast.promise(promise, {
loading: "Claiming NFT",
success: "NFT claimed successfully",
error: "Failed to claim NFT",
});
trackEvent({
category: "nft",
action: "claim",
label: "success",
});
onSuccess();
form.reset();
} catch (error) {
console.error(error);
trackEvent({
category: "nft",
action: "claim",
label: "error",
error,
});
onError(error);
}
})}
>
@@ -29,7 +29,7 @@ const mintAdditionalSupplyFormSchema = z.object({
to: z.string().refine((value) => isAddress(value), {
message: "Invalid Ethereum address",
}),
amount: z.number().int().min(1, "Amount must be at least 1"),
amount: z.coerce.number().int().min(1, "Amount must be at least 1"),
});
const MintSupplyTab: React.FC<MintSupplyTabProps> = ({ contract, tokenId }) => {
@@ -96,7 +96,7 @@ const MintSupplyTab: React.FC<MintSupplyTabProps> = ({ contract, tokenId }) => {
<FormItem>
<FormLabel>Amount</FormLabel>
<FormControl>
<Input {...field} />
<Input {...field} type="number" />
</FormControl>
<FormDescription>
How many would you like to mint?
@@ -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,6 +1,8 @@
import { notFound, redirect } from "next/navigation";
import { localhost } from "thirdweb/chains";
import { getContractPageParamsInfo } from "../../_utils/getContractFromParams";
import { getContractPageMetadata } from "../../_utils/getContractPageMetadata";
import { TokenIdPageClient } from "./TokenIdPage.client";
import { TokenIdPage } from "./token-id";
export default async function Page(props: {
@@ -16,18 +18,24 @@ export default async function Page(props: {
notFound();
}
if (!isOnlyNumbers(props.params.tokenId)) {
// redirect to nfts index page
redirect(`/${props.params.chain_id}/${props.params.contractAddress}/nfts`);
}
const { contract } = info;
if (contract.chain.id === localhost.id) {
return (
<TokenIdPageClient contract={contract} tokenId={props.params.tokenId} />
);
}
const { supportedERCs } = await getContractPageMetadata(contract);
if (!supportedERCs.isERC721 && !supportedERCs.isERC1155) {
redirect(`/${props.params.chain_id}/${props.params.contractAddress}`);
}
if (!isOnlyNumbers(props.params.tokenId)) {
// redirect to nfts index page
redirect(`/${props.params.chain_id}/${props.params.contractAddress}/nfts`);
}
return (
<TokenIdPage
contract={contract}
@@ -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"
@@ -1,8 +1,9 @@
import { notFound, redirect } from "next/navigation";
import { resolveFunctionSelectors } from "../../../../../../lib/selectors";
import { localhost } from "thirdweb/chains";
import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
import { getContractPageMetadata } from "../_utils/getContractPageMetadata";
import { ContractNFTPage } from "./ContractNFTPage";
import { ContractNFTPageClient } from "./ContractNFTPage.client";
export default async function Page(props: {
params: {
@@ -17,14 +18,17 @@ export default async function Page(props: {
}
const { contract } = info;
const { supportedERCs } = await getContractPageMetadata(contract);
if (contract.chain.id === localhost.id) {
return <ContractNFTPageClient contract={contract} />;
}
const { supportedERCs, functionSelectors } =
await getContractPageMetadata(contract);
if (!supportedERCs.isERC721 && !supportedERCs.isERC1155) {
redirect(`/${props.params.chain_id}/${props.params.contractAddress}`);
}
const functionSelectors = await resolveFunctionSelectors(contract);
return (
<ContractNFTPage
contract={contract}
@@ -1,5 +1,3 @@
import { PublishedBy } from "components/contract-components/shared/published-by";
import { ErrorBoundary } from "react-error-boundary";
import type { ThirdwebContract } from "thirdweb";
import { AnalyticsOverview } from "./components/Analytics";
import { BuildYourApp } from "./components/BuildYourApp";
@@ -21,6 +19,7 @@ interface ContractOverviewPageProps {
chainSlug: string;
isAnalyticsSupported: boolean;
functionSelectors: string[];
publishedBy: React.ReactNode;
}
const TRACKING_CATEGORY = "contract_overview";
@@ -36,6 +35,7 @@ export const ContractOverviewPage: React.FC<ContractOverviewPageProps> = ({
chainSlug,
isAnalyticsSupported,
functionSelectors,
publishedBy,
}) => {
return (
<div className="flex flex-col gap-8 lg:flex-row">
@@ -99,11 +99,7 @@ export const ContractOverviewPage: React.FC<ContractOverviewPageProps> = ({
contractAddress={contract.address}
/>
</div>
<div className="shrink-0 lg:w-[300px]">
<ErrorBoundary fallback={null}>
<PublishedBy contract={contract} />
</ErrorBoundary>
</div>
<div className="shrink-0 lg:w-[300px]">{publishedBy}</div>
</div>
);
};
@@ -105,8 +105,9 @@ function Inner({
// writes
ERC721Ext.isSetClaimConditionsSupported(functionSelectors),
ERC721Ext.isResetClaimEligibilitySupported(functionSelectors),
isErc721,
].every(Boolean);
}, [functionSelectors]);
}, [functionSelectors, isErc721]);
const hasERC20ClaimConditions = useMemo(() => {
return [
@@ -118,8 +119,9 @@ function Inner({
// writes
ERC20Ext.isSetClaimConditionsSupported(functionSelectors),
ERC20Ext.isResetClaimEligibilitySupported(functionSelectors),
isErc20,
].every(Boolean);
}, [functionSelectors]);
}, [functionSelectors, isErc20]);
const claimConditions = useReadContract(
isErc721 ? ERC721Ext.getClaimConditions : ERC20Ext.getClaimConditions,
@@ -233,7 +235,7 @@ function Inner({
(erc20Supply.data || 0n) > 0n,
});
}
if (hasERC721ClaimConditions) {
if (hasERC721ClaimConditions && isErc721) {
steps.push({
title: "First NFT claimed",
children: (
@@ -1,20 +1,14 @@
import { getThirdwebClient } from "@/constants/thirdweb.server";
import { ContractCard } from "components/explore/contract-card";
import { THIRDWEB_DEPLOYER_ADDRESS } from "constants/addresses";
import type { ThirdwebContract } from "thirdweb";
import type { ThirdwebClient, ThirdwebContract } from "thirdweb";
import { polygon } from "thirdweb/chains";
import { getBytecode, getContract } from "thirdweb/contract";
import { getPublishedUriFromCompilerUri } from "thirdweb/extensions/thirdweb";
import { getInstalledModules } from "thirdweb/modules";
import { download } from "thirdweb/storage";
import { extractIPFSUri } from "thirdweb/utils";
import { getAuthTokenWalletAddress } from "../../../app/api/lib/getAuthToken";
import { isEnsName, resolveEns } from "../../../lib/ens";
import { fetchPublishedContractsFromDeploy } from "../fetchPublishedContractsFromDeploy";
interface PublishedByProps {
contract: ThirdwebContract;
}
import { fetchPublishedContractsFromDeploy } from "../../../../../../../components/contract-components/fetchPublishedContractsFromDeploy";
import { isEnsName, resolveEns } from "../../../../../../../lib/ens";
type ModuleMetadataPickedKeys = {
publisher: string;
@@ -23,15 +17,18 @@ type ModuleMetadataPickedKeys = {
version: string;
};
export const PublishedBy: React.FC<PublishedByProps> = async ({ contract }) => {
const client = getThirdwebClient();
export async function getPublishedByCardProps(params: {
address: string | null;
contract: ThirdwebContract;
client: ThirdwebClient;
}) {
const { address, contract, client } = params;
const publishedContractsFromDeploy = await fetchPublishedContractsFromDeploy({
contract,
client,
});
const address = getAuthTokenWalletAddress();
const reversedPublishedContractsFromDeploy = [
...(publishedContractsFromDeploy || []),
].reverse();
@@ -133,19 +130,35 @@ export const PublishedBy: React.FC<PublishedByProps> = async ({ contract }) => {
}
}
return {
modules,
name: publishedContractToShow.name,
publisher: publisherAddressOrEns,
version: publishedContractToShow.version,
isBeta: (publishedContractToShow.displayName || "")
.toLowerCase()
.includes("beta"),
};
}
export function PublishedByUI(props: {
modules: ModuleMetadataPickedKeys[];
name: string;
publisher: string;
version: string | undefined;
isBeta: boolean;
}) {
return (
<ContractCard
contractId={publishedContractToShow.name}
publisher={publisherAddressOrEns}
version={publishedContractToShow.version}
isBeta={(publishedContractToShow.displayName || "")
.toLowerCase()
.includes("beta")}
modules={modules.map((m) => ({
contractId={props.name}
publisher={props.publisher}
version={props.version}
isBeta={props.isBeta}
modules={props.modules.map((m) => ({
publisher: m.publisher,
moduleId: m.name,
version: m.version,
}))}
/>
);
};
}
@@ -0,0 +1,32 @@
import { getThirdwebClient } from "@/constants/thirdweb.server";
import type { ThirdwebContract } from "thirdweb";
import { getAuthTokenWalletAddress } from "../../../../../../api/lib/getAuthToken";
import { PublishedByUI, getPublishedByCardProps } from "./published-by-ui";
interface PublishedByProps {
contract: ThirdwebContract;
}
export const PublishedBy: React.FC<PublishedByProps> = async ({ contract }) => {
const client = getThirdwebClient();
const address = getAuthTokenWalletAddress();
const props = await getPublishedByCardProps({
address,
contract,
client,
});
if (!props) {
return null;
}
return (
<PublishedByUI
isBeta={props.isBeta}
modules={props.modules}
name={props.name}
publisher={props.publisher}
version={props.version}
/>
);
};
@@ -0,0 +1,43 @@
"use client";
import type { ThirdwebContract } from "thirdweb";
import type { ChainMetadata } from "thirdweb/chains";
import { ErrorPage, LoadingPage } from "../_components/page-skeletons";
import { useContractPageMetadata } from "../_hooks/useContractPageMetadata";
import { ContractOverviewPage } from "./ContractOverviewPage";
export function ContractOverviewPageClient(props: {
contract: ThirdwebContract;
chainMetadata: ChainMetadata;
}) {
const { contract, chainMetadata } = props;
const metadataQuery = useContractPageMetadata(contract);
if (metadataQuery.isPending) {
return <LoadingPage />;
}
if (metadataQuery.isError) {
return <ErrorPage />;
}
const contractPageMetadata = metadataQuery.data;
return (
<ContractOverviewPage
contract={contract}
hasDirectListings={contractPageMetadata.isDirectListingSupported}
hasEnglishAuctions={contractPageMetadata.isEnglishAuctionSupported}
isErc1155={contractPageMetadata.supportedERCs.isERC1155}
isErc20={contractPageMetadata.supportedERCs.isERC20}
isErc721={contractPageMetadata.supportedERCs.isERC721}
isPermissionsEnumerable={
contractPageMetadata.isPermissionsEnumerableSupported
}
chainSlug={chainMetadata.slug}
isAnalyticsSupported={contractPageMetadata.isAnalyticsSupported}
functionSelectors={contractPageMetadata.functionSelectors}
// TODO - create a fully client rendered version of publishedBy and ContractCard and plug it here
publishedBy={undefined}
/>
);
}
@@ -1,7 +1,11 @@
import { notFound } from "next/navigation";
import { ErrorBoundary } from "react-error-boundary";
import { localhost } from "thirdweb/chains";
import { getContractPageParamsInfo } from "./_utils/getContractFromParams";
import { getContractPageMetadata } from "./_utils/getContractPageMetadata";
import { ContractOverviewPage } from "./overview/ContractOverviewPage";
import { PublishedBy } from "./overview/components/published-by.server";
import { ContractOverviewPageClient } from "./overview/contract-overview-page.client";
export default async function Page(props: {
params: {
@@ -16,6 +20,15 @@ export default async function Page(props: {
}
const { contract, chainMetadata } = info;
if (contract.chain.id === localhost.id) {
return (
<ContractOverviewPageClient
chainMetadata={chainMetadata}
contract={contract}
/>
);
}
const contractPageMetadata = await getContractPageMetadata(contract);
return (
@@ -32,6 +45,11 @@ export default async function Page(props: {
chainSlug={chainMetadata.slug}
isAnalyticsSupported={contractPageMetadata.isAnalyticsSupported}
functionSelectors={contractPageMetadata.functionSelectors}
publishedBy={
<ErrorBoundary fallback={null}>
<PublishedBy contract={contract} />
</ErrorBoundary>
}
/>
);
}

Some files were not shown because too many files have changed in this diff Show More