Compare commits

...
Author SHA1 Message Date
Jonas DanielsGitHubgithub-actions[bot] <github-actions[bot]@users.noreply.github.com>
bfe9753be6 Version Packages (#4881)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-10-03 14:33:47 +13:00
Joaquim VergesandGitHub eeb993df75 feat: optimize zksync contract deployment and bytecode handling (#4902) 2024-10-03 14:09:00 +13:00
ElasticBottle 367a270bba fix: update timeline for GDPR compliance (#4897)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR updates the compliance information regarding GDPR and CCPA in the `page.mdx` file, providing more detailed and accurate descriptions of the data handling practices.

### Detailed summary
- Updated the compliance statement for GDPR and CCPA.
- Changed the text from a general statement about in-app wallets to specify that `thirdweb` complies with GDPR and CCPA frameworks.
- Clarified the timeframe for data deletion requests (30 days for GDPR and 45 for CCPA).

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

<!-- end pr-codex -->
2024-10-03 01:02:51 +00:00
kumaryash90 e91581aec6 Update chain stack type check (#4901)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR introduces a new property `stackType` across various components and utilities, enhancing the handling of blockchain network configurations and metadata.

### Detailed summary
- Added `stackType` property in `types.ts` for chains.
- Updated `utils.ts` to include `stackType` from data.
- Included `stackType` in `embed-setup.tsx` and `ConfigureNetworkForm.tsx`.
- Modified the `isZkSyncChain` function to use `getChainMetadata` for `stackType` validation.
- Removed the obsolete `getChainStack` function.

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

<!-- end pr-codex -->
2024-10-03 00:17:49 +00:00
ElasticBottle 985c4b0804 fix: add missing partner id for ecosystem wallet (#4886)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on fixing the issue of missing `partnerId` when creating an ecosystem wallet in the `thirdweb` package.

### Detailed summary
- Updated `partnerId` in `packages/thirdweb/src/wallets/in-app/web/ecosystem.ts` to use `ecosystem.partnerId`.
- Updated `partnerId` in `packages/thirdweb/src/wallets/in-app/native/ecosystem.ts` to use `createOptions?.partnerId`.

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

<!-- end pr-codex -->
2024-10-02 22:38:54 +00:00
MananTank 9fb5401a93 Add Coupon Card in Billing Page (#4895)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on enhancing the `SettingsBillingPage` component by adding a `teamId` prop to manage billing settings for different teams. It also introduces a new `CouponCard` component that allows users to apply coupon codes with relevant feedback.

### Detailed summary
- Updated `Page` component to pass `teamId` to `SettingsBillingPage`.
- Modified `SettingsBillingPage` to accept `teamId` as a prop.
- Adjusted `Billing` component to use `teamId` in its rendering.
- Introduced `CouponCard` component for applying coupon codes.
- Implemented logic in `CouponCard` to handle coupon submissions and display feedback based on response status.
- Updated `Billing` component to include `CouponCard`.

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

<!-- end pr-codex -->
2024-10-02 21:48:14 +00:00
Joaquim VergesandGitHub e54867550e feat: switch to v2 bundler for all user operations (#4884) 2024-10-03 10:12:43 +13:00
MananTank 514647cb53 Add Billing alerts on /connect layout in app router (#4892)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on enhancing the `BillingAlerts` component by allowing it to accept a `className` prop for better styling flexibility. It also updates the usage of `BillingAlerts` in various components to apply specific padding classes.

### Detailed summary
- Modified `BillingAlerts` to accept an optional `className` prop.
- Updated the `BillingAlerts` usage in `apps/dashboard/src/components/layout/app-shell/index.tsx` to include `className="py-6"`.
- Updated `BillingAlerts` usage in `apps/dashboard/src/app/(dashboard)/dashboard/connect/layout.tsx` to include `className="pb-6"`.
- Adjusted the rendering of `BillingAlertsUI` to apply the `className` prop.

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

<!-- end pr-codex -->
2024-10-02 19:56:29 +00:00
MananTank eb8638e0c2 Spacing fixes on OnboardingGeneral component (#4891)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on refactoring the layout of the `OnboardingGeneral` and `AccountForm` components by replacing `FocusLock` and `Flex` components with `div` elements, enhancing the structure and styling of the components.

### Detailed summary
- Replaced `<FocusLock>` with `<div>` in `OnboardingGeneral`.
- Changed `<Flex>` to `<div className="flex flex-col gap-4">` in `OnboardingGeneral`.
- Updated `<Flex>` to `<div className={cn("flex w-full", ...)}> ` in `AccountForm`.
- Modified closing tags from `</Flex>` to `</div>` in both components.

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

<!-- end pr-codex -->
2024-10-02 18:45:33 +00:00
MananTank a6f85ac949 Replace react-otp-input with shadcn input-otp (#4889)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR introduces a new OTP input component using the `input-otp` library, along with related animations and configuration updates. It replaces the existing `react-otp-input` dependency and enhances the onboarding email confirmation process.

### Detailed summary
- Added `input-otp` dependency to `package.json`.
- Removed `react-otp-input` from `package.json`.
- Updated `tailwind.config.js` to include `caret-blink` animation.
- Created new components: `InputOTP`, `InputOTPGroup`, `InputOTPSlot`, `InputOTPSeparator`.
- Refactored `ConfirmEmail.tsx` to utilize the new OTP input components.
- Improved styling and functionality for OTP input slots.

> 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-02 17:57:34 +00:00
MananTank 726bbde9ca Add privacyPolicyUrl and termsOfServiceUrl on ConnectEmbed in login page (#4890)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on enhancing the `login` component by adding links for the privacy policy and terms of service.

### Detailed summary
- Added `privacyPolicyUrl` prop with the value `"/privacy"` to the component.
- Added `termsOfServiceUrl` prop with the value `"/tos"` to the component.

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

<!-- end pr-codex -->
2024-10-02 17:46:44 +00:00
MananTank 025c581be6 api server url env cleanup (#4887)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR introduces a new constant, `API_SERVER_URL`, to centralize the API server URL configuration. It replaces instances of `process.env.NEXT_PUBLIC_THIRDWEB_API_HOST` throughout the codebase, enhancing maintainability and readability.

### Detailed summary
- Added `API_SERVER_URL` in `env.ts`.
- Replaced `process.env.NEXT_PUBLIC_THIRDWEB_API_HOST` with `API_SERVER_URL` in multiple files:
  - `getAccount.ts`
  - `fetchEcosystemList.ts`
  - `fetchEcosystem.ts`
  - `team-members.ts`
  - `get-auth-token/route.ts`
  - `getAPIKeys.ts`
  - `ensure-login/route.ts`
  - `server-proxy/api/[...paths].tsx`
  - `team.ts`
  - `projects.ts`
  - `create-ticket.action.ts`
  - `auth-actions.ts`
  - `utils.ts`
- Updated fetch requests to use `API_SERVER_URL` for consistent API endpoint access.

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

<!-- end pr-codex -->
2024-10-02 17:39:21 +00:00
arcoraven 522968a505 chore: Update version flow updates deployment (#4852)
## Problem solved

Changes the "update Engine version" flow to query the infra backend to perform the version upgrade.

Previously this would ping Slack for a thirdweb staff to update.

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on improving the code quality and user interface across various components in the dashboard. It includes updates to placeholders, text content, and the handling of engine updates.

### Detailed summary
- Updated `placeholder` in `ip-allowlist.tsx` to use curly braces.
- Added periods to sentences in `EngineImportPage.tsx` and `CreateEnginePage.tsx`.
- Simplified conditional rendering in `engine-instances-table.tsx`.
- Changed `cloudDeployedAt` to `deploymentId` in `useEngine.ts`.
- Renamed `useEngineUpdateVersion` to `useEngineUpdateServerVersion`.
- Updated fetch method to `PUT` for engine updates.
- Changed variable names for clarity in `EngineVersionBadge.tsx`.
- Updated props in `UpdateVersionModal` to use `latestVersion`.
- Adjusted toast messages for better user feedback during engine updates.

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

<!-- end pr-codex -->
2024-10-02 07:09:16 +00:00
joaquim-verges 454f063f98 refactor: optimize zksync chain detection and add Creator Testnet test (#4883)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on refactoring code related to the handling of contract metadata, specifically around bytecode fetching and the integration of `zksync` support. It enhances the clarity and functionality of contract deployment processes.

### Detailed summary
- Removed checks for `compilerType` in multiple files.
- Simplified bytecode fetching by introducing `fetchBytecodeFromCompilerMetadata`.
- Updated contract metadata handling in `fetchDeployMetadata`.
- Adjusted UI components to reflect new metadata structure.
- Added a test for sending transactions on the Creator Testnet.

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

<!-- end pr-codex -->
2024-10-02 03:40:38 +00:00
MananTank 78cbc412ce Migrate Billing related components to tailwind/shadcn - Part 1 (#4879)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR primarily refactors the onboarding and billing components, improving the UI structure and enhancing the modal functionalities. It replaces the `OnboardingTitle` with `TitleAndDescription`, streamlining the code and updating various components for better usability and consistency.

### Detailed summary
- Replaced `<OnboardingTitle>` with `<TitleAndDescription>` in several components.
- Updated `OnboardingModal` to remove redundant `onClose` props.
- Refactored billing alert components to use new UI elements.
- Enhanced the layout of forms and alerts for better UX.
- Improved state management for modal visibility.
- Added new utility functions for account and billing data stubs.
- Updated import statements for consistency and clarity.

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

<!-- end pr-codex -->
2024-10-02 00:07:14 +00:00
kien-ngo 79c50cb311 [Dashboard] Remove react-icons (1) (#4882)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR primarily focuses on replacing various icon components with `PlusIcon` and `UploadIcon` from the `lucide-react` library, enhancing the UI consistency across the application.

### Detailed summary
- Replaced `Upload` component with `UploadIcon` in `airdrop-upload-erc20.tsx` and `upload-step.tsx`.
- Updated `FiPlus` icons to `PlusIcon` in multiple components, including `external-links-fieldset.tsx`, `NFTMintButton.tsx`, and more.
- Removed unused `Icon` imports from several files.
- Adjusted icon classes for consistent styling.

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

<!-- end pr-codex -->
2024-10-01 23:46:56 +00:00
kien-ngo 764ee8ca47 [Dashboard] Remove some chakra icons, adjust IpfsUploadButton (#4880)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR primarily focuses on UI enhancements and component updates across various files in the dashboard application, including adjustments to icons, button properties, and tooltip components.

### Detailed summary
- Updated `InputRightElement` width from `145px` to `160px`.
- Changed `Tooltip` to `ToolTipLabel` in `OpenSeaPropertyBadge`.
- Modified `leftIcon` rendering in `ConnectPlaygroundButton` and `CodeOptionButton` to use class-based icons.
- Replaced `FiUpload` with `UploadIcon` in `IpfsUploadButton`.
- Swapped `BsFillLightningChargeFill` with `ZapIcon` in `ProductButton`.

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

<!-- end pr-codex -->
2024-10-01 23:46:54 +00:00
jnsdls 783d84467d update dependencies (#4874)
<!-- start pr-codex -->

## PR-Codex overview
This PR focuses on updating various dependencies across multiple packages, ensuring compatibility and incorporating improvements.

### Detailed summary
- Updated `turbo` from `2.1.2` to `2.1.3`.
- Updated `@types/react` from `^18.3.8` to `^18.3.10`.
- Updated `tailwindcss` from `3.4.12` to `3.4.13`.
- Updated `@walletconnect/react-native-compat` from `2.13.2` to `2.16.3`.
- Updated `@coinbase/wallet-sdk` from `4.0.4` to `4.1.0`.
- Updated `viem` from `2.21.12` to `2.21.16`.
- Various other dependency updates across multiple packages.

> 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-01 22:10:01 +00:00
jnsdls 687a733d4a Add deprecation warnings to dashboard folders (#4878)
### TL;DR

Added warning messages to deprecated folders in the dashboard to prevent further additions.

### What changed?

Added `DO_NOT_ADD_THINGS_HERE.md` files to several deprecated folders within the dashboard's `src` directory. These folders include:

- @3rdweb-sdk
- classes
- contract-ui
- core-ui
- og-lib
- pages
- tw-components

Each file contains a warning message stating that the folder is deprecated and advising developers not to add new content to these locations.

### How to test?

1. Navigate to each of the mentioned folders in the dashboard's `src` directory.
2. Verify that the `DO_NOT_ADD_THINGS_HERE.md` file exists in each folder.
3. Open the files and confirm they contain the correct warning message.

### Why make this change?

This change aims to prevent developers from adding new content to deprecated parts of the dashboard. By placing clear warning messages in these folders, we reduce the risk of introducing new code or features in areas that are scheduled for removal. This helps maintain code quality, reduces technical debt, and supports the ongoing efforts to modernize and streamline the dashboard's structure.
2024-10-01 21:32:00 +00:00
kien-ngo 96daa2654c [SDK] Update docs for claimTo extensions (#4849)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR enhances the documentation for the `claimTo` function across the `ERC1155`, `ERC20`, and `ERC721` extensions by adding usage examples for drops with allowlists, emphasizing the need to specify the claimer address.

### Detailed summary
- Added a "### Basic usage" section to the documentation for `claimTo` in all three extensions.
- Introduced a "### For Drops with allowlists" section explaining the need for the `from` parameter.
- Provided code examples for using `claimTo` with allowlists in `ERC1155`, `ERC20`, and `ERC721`.

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

<!-- end pr-codex -->
2024-10-01 21:08:24 +00:00
kien-ngo a2ce5f56ab [Dashboard] Remove <Code /> component (ez mode) (#4787)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on enhancing code readability by replacing instances of the `Code` component from Chakra UI with a new `InlineCode` component, which provides a consistent styling for inline code snippets across various files.

### Detailed summary
- Added `InlineCode` component in `inline-code.tsx`.
- Replaced `Code` with `InlineCode` in multiple components for improved consistency:
  - `ChainValidation`
  - `EngineCorsConfig`
  - `EngineIpAllowlistConfig`
  - `InteractiveAbiFunction`
  - `EngineAccessTokens`
  - `SnapshotUpload`
  - `UploadStep`

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

<!-- end pr-codex -->
2024-10-01 21:03:53 +00:00
kumaryash90 6beec7a35e Check zksync stack (#4869)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on updating the `isZkSyncChain` function to be asynchronous and modifying various parts of the codebase to await its result. This change enhances the handling of chain identification, particularly for zkSync-related operations.

### Detailed summary
- Changed `isZkSyncChain` to an async function.
- Updated calls to `isZkSyncChain` to use `await` in several files.
- Modified the logic in `isZkSyncChain` to include additional checks for specific chain IDs.
- Introduced a new async function `getChainStack` for fetching chain stack information.

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

<!-- end pr-codex -->
2024-10-01 20:12:35 +00:00
31f371d065 Superposition Mainnnet Custom Chain Page (#4875)
Co-authored-by: Jonas Daniels <[email protected]>
2024-10-01 12:45:00 -07:00
MananTank fd8674a8a4 Single Header component for App and Pages router (#4877)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on refactoring the dashboard components, enhancing the UI elements, and improving the overall structure of the codebase. It includes updates to navigation, styling, and the introduction of new components for better functionality.

### Detailed summary
- Deleted unused files: `nav-link.client.tsx`, `headerLinks.ts`, `MobileBurgerMenu.tsx`.
- Updated `cmd-k-search` button styling and icon size.
- Changed import paths in `layout.tsx`.
- Added `DashboardHeaderTabs` component to manage navigation.
- Refactored `DashboardHeader` to streamline layout and include new links.
- Updated `ColorModeToggle` and `CreditsButton` for better UI interaction.
- Enhanced `CreditsItem` component layout and functionality.
- Replaced `TrackedLinkButton` with `NavLink` in `UpgradeButton`.

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

<!-- end pr-codex -->
2024-10-01 19:33:06 +00:00
0xFirekeeper d3b0dc6646 [Portal] Fix Unity link (#4876)
<!-- start pr-codex -->

## PR-Codex overview
This PR updates the links in the `page.mdx` file for the Unity SDK to reflect the correct versioning in the URLs.

### Detailed summary
- Updated the link in the text from `/unity/getting-started` to `/unity/v5/getting-started`.
- Changed the `href` attribute of the `ArticleIconCard` component from `/unity/getting-started` to `/unity/v5/getting-started`.

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

<!-- end pr-codex -->
2024-10-01 19:29:36 +00:00
kien-ngo 98c5fd0b79 [SDK} Skip pack test (#4873)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on updating the `createNewPack` test in the `createNewPack.test.ts` file. It enhances the test by ensuring the correct creation and opening of a Pack while managing ERC20 and ERC721 tokens, including minting, approval, and balance checks.

### Detailed summary
- Added a `describe.skip` for `createNewPack` to temporarily skip the test.
- Reformatted the test to ensure proper execution flow.
- Included minting and approval for ERC20 and ERC721 tokens.
- Added assertions to verify balances and pack content after creation.
- Ensured correct metadata checks for the created Pack.

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

<!-- end pr-codex -->
2024-10-01 00:57:50 +00:00
0xFirekeeper 2f11259ccd [Portal] Link new Telegram Unity Template (#4872)
<!-- start pr-codex -->

## PR-Codex overview
This PR updates the `page.mdx` file to modify existing `ArticleCard` entries and add new ones. It enhances the descriptions and removes a duplicate `ArticleCard` for `Pioneer`.

### Detailed summary
- Updated the `description` for the `Take Flight!` and `Blockventure` `ArticleCard` entries.
- Added two new `ArticleCard` entries for `Telegram Mini-App` and `Pioneer`.
- Removed the duplicate `ArticleCard` for `Pioneer`.

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

<!-- end pr-codex -->
2024-10-01 00:47:16 +00:00
Jonas DanielsGitHubgithub-actions[bot] <github-actions[bot]@users.noreply.github.com>
97c09003dc Version Packages (#4842)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-10-01 13:37:54 +13:00
Edward SunandGitHub 5d0505c43c Add purchaseData to transfer transactions (#4871) 2024-10-01 09:14:38 +09:00
Jonas DanielsandGitHub e552fda9e5 fix auth token (#4870) 2024-09-30 16:58:35 -07:00
kien-ngo cddeda4280 [Dashboard] Remove <Stack/> (5) (#4844)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on refactoring the usage of the `Stack` component from `@chakra-ui/react` to standard HTML elements with Flexbox classes for layout consistency and performance improvements across various components in the dashboard.

### Detailed summary
- Removed `Stack` component and replaced it with `div` elements using Flexbox classes in multiple files.
- Updated `as` prop for `FormControl` elements to use `className` for styling.
- Adjusted various components to improve layout and spacing using Tailwind CSS classes.
- Added `StackProps` to `SelectOptionProps` interface, making it more flexible.
- Ensured consistency in component structure and styling throughout the dashboard.

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

<!-- end pr-codex -->
2024-09-30 23:27:38 +00:00
MananTank cb72f67c92 Fix Contract Type shown in Contracts Table, Fix PublishedBy not showing latest version (#4854)
Fixes DASH-274

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on improving the handling of published contracts in the dashboard by reversing and sorting the list of contracts based on the publisher, prioritizing those from `THIRDWEB_DEPLOYER_ADDRESS`.

### Detailed summary
- In `published-by.tsx`, the list of published contracts is reversed.
- In `cells.tsx`, a new sorted array `publishedContractsFromDeploySorted` is created from the original contracts.
- The sorting prioritizes contracts published by `THIRDWEB_DEPLOYER_ADDRESS`.
- The display name retrieval uses the sorted list instead of the original query.

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

<!-- end pr-codex -->
2024-09-30 23:15:51 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
222964dcbb Bump eslint-plugin-storybook from 0.8.0 to 0.9.0 (#4866)
Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-30 16:14:49 -07:00
MananTank ec37c74199 Fix InteractiveAbiFunction component not rendered in published contract page explorer (#4868)
FIXES: DASH-273

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on improving type safety and handling of published contracts in the dashboard application. It modifies type definitions and enhances the rendering logic for contract functions.

### Detailed summary
- Changed `snippet` type from `Record<CodeEnvironment, any>` to `Partial<Record<CodeEnvironment, any>>` in `formatSnippet`.
- Added a check for `publishedContract` in `layout.tsx` to ensure it exists before proceeding.
- Updated the `ContractFunction` component to enforce required `fn` and optional `contract` props.
- Enhanced rendering logic in `ContractFunctionInner` to display function usage and code snippets based on contract availability.

> The following files were skipped due to too many changes: `apps/dashboard/tsconfig.tsbuildinfo`

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

<!-- end pr-codex -->
2024-09-30 22:57:39 +00:00
kien-ngo 2ac1e86b8f [Dashboard] Remove FTUX (#4851)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on removing the `FTUX` component and related files from the project, simplifying the `Dashboard` component by eliminating conditional rendering based on wallet connection status.

### Detailed summary
- Deleted `FTUX.tsx` and associated files: `shared.ts`, `slides.tsx`, `animations.ts`, `Graphics.tsx`, `ContentContainer.tsx`, and `GraphicContainer.tsx`.
- Removed logic for `connectionStatus` and `showFTUX` from `Dashboard`.
- Streamlined rendering in `Dashboard` by directly displaying `OnboardingSteps`.

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

<!-- end pr-codex -->
2024-09-30 22:49:45 +00:00
kien-ngo bdc5964054 [Dashboard] Remove ListItem (2) (#4850)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR primarily focuses on the removal of several files related to the `contract-ui` and `core-ui` components, alongside updates to various components and hooks to include `txChainID` for improved transaction handling.

### Detailed summary
- Deleted multiple files in `contract-ui` and `core-ui`.
- Added `txChainID` prop to `TransactionButton` across various components for transaction context.
- Updated `useWalletNFTs` to accept an object parameter for better clarity.
- Enhanced error handling in API functions.
- Refactored several components to improve code readability and maintainability.

> The following files were skipped due to too many changes: `apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/claim-conditions/page.tsx`, `apps/dashboard/src/contract-ui/tabs/claim-conditions/components/claim-conditions.tsx`, `apps/dashboard/src/contract-ui/tabs/permissions/page.tsx`, `apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/nfts/page.tsx`, `apps/dashboard/src/components/app-layouts/app.tsx`, `apps/dashboard/src/contract-ui/tabs/overview/components/NFTCards.tsx`, `apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/_utils/detectedFeatures/erc4337.ts`, `apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/explorer/page.tsx`, `apps/dashboard/src/contract-ui/tabs/tokens/page.tsx`, `apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/code/page.tsx`, `apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/nfts/[tokenId]/page.tsx`, `apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/page.tsx`, `apps/dashboard/src/contract-ui/tabs/shared-components/cancel-tab.tsx`, `apps/dashboard/src/pages/hackathon/readyplayer3.tsx`, `apps/dashboard/src/contract-ui/tabs/overview/components/PermissionsTable.tsx`, `apps/dashboard/src/contract-ui/tabs/overview/components/BuildYourApp.tsx`, `apps/dashboard/src/stores/chainStores.tsx`, `apps/dashboard/src/contract-ui/tabs/account/page.tsx`, `apps/dashboard/src/contract-ui/tabs/overview/components/NFTDetails.tsx`, `apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/settings/page.tsx`, `apps/dashboard/src/contract-ui/tabs/accounts/components/accounts-table.tsx`, `apps/dashboard/src/contract-ui/tabs/code/components/code-overview.tsx`, `apps/dashboard/src/stores/SyncStoreToStorage.tsx`, `apps/dashboard/src/contract-ui/tabs/overview/components/ContractChecklist.tsx`, `apps/dashboard/src/contract-ui/tabs/nfts/components/table.tsx`, `apps/dashboard/src/pages/contact-us.tsx`, `apps/dashboard/src/contract-ui/tabs/embed/components/embed-setup.tsx`, `apps/dashboard/src/components/contract-functions/contract-function.tsx`, `apps/dashboard/src/contract-ui/tabs/events/components/events-feed.tsx`, `apps/dashboard/src/contract-ui/tabs/nfts/components/airdrop-upload.tsx`, `apps/dashboard/src/@/components/blocks/Sidebar.tsx`, `apps/dashboard/src/components/shared/DeprecatedAlert.tsx`, `apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/_utils/getContractFromParams.ts`, `apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/ConfigureCustomChain.tsx`, `apps/dashboard/src/contract-ui/tabs/overview/components/Analytics.tsx`, `apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/_utils/detectedFeatures/claimConditions.ts`, `apps/dashboard/src/contract-ui/tabs/tokens/components/airdrop-upload-erc20.tsx`, `apps/dashboard/src/contract-ui/tabs/settings/page.tsx`, `apps/dashboard/src/contract-ui/tabs/nfts/components/token-id.tsx`, `apps/dashboard/src/pages/hackathon/consumer-crypto.tsx`, `apps/dashboard/src/@/components/blocks/MobileSidebar.tsx`, `apps/dashboard/src/components/custom-contract/contract-header/contract-metadata.tsx`, `apps/dashboard/src/contract-ui/tabs/explorer/page.tsx`, `apps/dashboard/src/contract-ui/tabs/overview/components/LatestEvents.tsx`, `apps/dashboard/src/contract-ui/tabs/overview/page.tsx`, `apps/dashboard/src/contract-ui/tabs/listings/components/list-form.tsx`, `apps/dashboard/src/components/layout/app-shell/index.tsx`, `apps/dashboard/src/@3rdweb-sdk/react/hooks/useDashboardContractMetadata.tsx`, `apps/dashboard/src/og-lib/url-utils.ts`, `apps/dashboard/src/@3rdweb-sdk/react/hooks/useActiveChainId.tsx`, `apps/dashboard/src/@/components/blocks/charts/area-chart.tsx`, `apps/dashboard/src/contract-ui/components/primary-dashboard-button.tsx`, `apps/dashboard/src/pages/hackathon/earn.tsx`, `apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/_utils/getContractPageSidebarLinks.ts`, `apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/_utils/getContractPageMetadata.ts`, `apps/dashboard/src/contract-ui/tabs/nfts/page.tsx`, `apps/dashboard/src/core-ui/batch-upload/upload-step.tsx`, `apps/dashboard/src/contract-ui/tabs/overview/components/MarketplaceDetails.tsx`, `apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/layout.tsx`, `apps/dashboard/src/components/contract-components/hooks.ts`, `apps/dashboard/src/pages/api/og/contract.tsx`, `apps/dashboard/src/components/configure-networks/ConfigureNetworkForm.tsx`, `apps/dashboard/src/components/custom-contract/contract-header/metadata-header.tsx`, `apps/dashboard/src/@3rdweb-sdk/react/hooks/useApi.ts`, `apps/dashboard/src/components/buttons/MismatchButton.tsx`, `apps/dashboard/src/@3rdweb-sdk/react/hooks/useEngine.ts`

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

<!-- end pr-codex -->
2024-09-30 22:49:44 +00:00
kien-ngoandManan Tank 5fc07f7111 [Dashboard] Remove ListItem (1) and fix some UI issues from the aspect-ratio PR (#4846)
Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

This PR focuses on replacing `List` and `ListItem` components with HTML `<ul>` and `<li>` elements across multiple files to improve semantic HTML structure and potentially enhance accessibility.

- Removed `List` and `ListItem` components in favor of `<ul>` and `<li>` elements in various components.
- Updated `label` in `CreateListingsForm` to use a `Card` with structured `<ul>` content.
- Adjusted lists in `Hackathon`, `HackathonEarn`, and `ContactUs` pages to use `<ul>` and `<li>`.
- Minor styling adjustments for improved layout and alignment.

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

<!-- end pr-codex -->
2024-10-01 03:59:35 +05:30
MananTank 80729e9748 Move contract pages to app router (#4820)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on code cleanup and refactoring, including the removal of deprecated files and the addition of `txChainID` props in various components for better transaction handling.

### Detailed summary
- Deleted multiple unused files in `apps/dashboard/src/contract-ui` and `apps/dashboard/src/core-ui`.
- Added `txChainID` prop to `TransactionButton` in various components for transaction context.
- Updated components to use direct imports and improved overall code structure.
- Enhanced error handling and metadata fetching for contracts.

> The following files were skipped due to too many changes: `apps/dashboard/src/contract-ui/tabs/permissions/page.tsx`, `apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/nfts/page.tsx`, `apps/dashboard/src/components/app-layouts/app.tsx`, `apps/dashboard/src/contract-ui/tabs/overview/components/NFTCards.tsx`, `apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/_utils/detectedFeatures/erc4337.ts`, `apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/explorer/page.tsx`, `apps/dashboard/src/contract-ui/tabs/tokens/page.tsx`, `apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/code/page.tsx`, `apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/nfts/[tokenId]/page.tsx`, `apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/page.tsx`, `apps/dashboard/src/contract-ui/tabs/shared-components/cancel-tab.tsx`, `apps/dashboard/src/contract-ui/tabs/overview/components/PermissionsTable.tsx`, `apps/dashboard/src/contract-ui/tabs/overview/components/BuildYourApp.tsx`, `apps/dashboard/src/stores/chainStores.tsx`, `apps/dashboard/src/contract-ui/tabs/account/page.tsx`, `apps/dashboard/src/contract-ui/tabs/overview/components/NFTDetails.tsx`, `apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/settings/page.tsx`, `apps/dashboard/src/contract-ui/tabs/accounts/components/accounts-table.tsx`, `apps/dashboard/src/components/contract-functions/contract-function.tsx`, `apps/dashboard/src/contract-ui/tabs/code/components/code-overview.tsx`, `apps/dashboard/src/stores/SyncStoreToStorage.tsx`, `apps/dashboard/src/contract-ui/tabs/overview/components/ContractChecklist.tsx`, `apps/dashboard/src/contract-ui/tabs/nfts/components/table.tsx`, `apps/dashboard/src/contract-ui/tabs/embed/components/embed-setup.tsx`, `apps/dashboard/src/contract-ui/tabs/listings/components/list-form.tsx`, `apps/dashboard/src/contract-ui/tabs/events/components/events-feed.tsx`, `apps/dashboard/src/@/components/blocks/Sidebar.tsx`, `apps/dashboard/src/components/shared/DeprecatedAlert.tsx`, `apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/_utils/getContractFromParams.ts`, `apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/ConfigureCustomChain.tsx`, `apps/dashboard/src/contract-ui/tabs/overview/components/Analytics.tsx`, `apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/_utils/detectedFeatures/claimConditions.ts`, `apps/dashboard/src/contract-ui/tabs/settings/page.tsx`, `apps/dashboard/src/contract-ui/tabs/nfts/components/token-id.tsx`, `apps/dashboard/src/@/components/blocks/MobileSidebar.tsx`, `apps/dashboard/src/components/custom-contract/contract-header/contract-metadata.tsx`, `apps/dashboard/src/contract-ui/tabs/explorer/page.tsx`, `apps/dashboard/src/contract-ui/tabs/overview/components/LatestEvents.tsx`, `apps/dashboard/src/contract-ui/tabs/overview/page.tsx`, `apps/dashboard/src/components/layout/app-shell/index.tsx`, `apps/dashboard/src/@3rdweb-sdk/react/hooks/useDashboardContractMetadata.tsx`, `apps/dashboard/src/og-lib/url-utils.ts`, `apps/dashboard/src/@3rdweb-sdk/react/hooks/useActiveChainId.tsx`, `apps/dashboard/src/@/components/blocks/charts/area-chart.tsx`, `apps/dashboard/src/contract-ui/components/primary-dashboard-button.tsx`, `apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/_utils/getContractPageSidebarLinks.ts`, `apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/_utils/getContractPageMetadata.ts`, `apps/dashboard/src/contract-ui/tabs/nfts/page.tsx`, `apps/dashboard/src/contract-ui/tabs/overview/components/MarketplaceDetails.tsx`, `apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/layout.tsx`, `apps/dashboard/src/components/contract-components/hooks.ts`, `apps/dashboard/src/pages/api/og/contract.tsx`, `apps/dashboard/src/components/configure-networks/ConfigureNetworkForm.tsx`, `apps/dashboard/src/components/custom-contract/contract-header/metadata-header.tsx`, `apps/dashboard/src/@3rdweb-sdk/react/hooks/useApi.ts`, `apps/dashboard/src/components/buttons/MismatchButton.tsx`, `apps/dashboard/src/@3rdweb-sdk/react/hooks/useEngine.ts`

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

<!-- end pr-codex -->
2024-09-30 21:43:43 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
8832f87f4a Bump @storybook/addon-interactions from 8.3.2 to 8.3.4 (#4867)
Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-30 14:35:43 -07:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
39a43903ad Bump @aws-sdk/client-lambda from 3.656.0 to 3.659.0 (#4861)
Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-30 14:33:46 -07:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
6396729839 Bump @cloudflare/workers-types from 4.20240919.0 to 4.20240925.0 (#4862)
Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-30 14:32:17 -07:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
30fd225538 Bump @walletconnect/sign-client from 2.16.2 to 2.16.3 (#4864)
Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-30 14:31:13 -07:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
931bbd4952 Bump @storybook/react-vite from 8.3.2 to 8.3.4 (#4863)
Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-30 14:23:30 -07:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
3202ce105c Bump @storybook/blocks from 8.3.2 to 8.3.4 (#4860)
Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-30 14:19:56 -07:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
36aa3f0937 Bump @storybook/react from 8.3.2 to 8.3.4 (#4855)
Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-30 14:05:14 -07:00
MananTank ab43d9426a Fix Code tab not working in playground - revert shiki to 1.16 (#4853)
Fixes CNCT-1958
2024-09-30 18:52:16 +00:00
AmirandGitHub 29938abfe3 Update page.mdx (#4848)
Signed-off-by: Amir <[email protected]>
2024-09-29 21:40:27 -07:00
AmirandGitHub 5bb5a437cd Update page.mdx (#4847)
Signed-off-by: Amir <[email protected]>
2024-09-29 21:14:07 -07:00
kien-ngo 8b681729a1 [Dashboard] Remove <Stack /> (4) (#4843)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on refactoring the layout components in various files from using `Stack` to `div` elements with Flexbox classes for better styling and layout control.

### Detailed summary
- Replaced `<Stack>` components with `<div>` using Flexbox classes in multiple files.
- Updated form structures to use `<form>` tags directly instead of wrapping them in `<Stack>`.
- Adjusted spacing and alignment using Flexbox properties.

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

<!-- end pr-codex -->
2024-09-29 18:29:22 +00:00
kien-ngo 6c26012a71 [SDK] Update smart wallet test (#4845)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR introduces a new `sleep` function in `sleep.ts` for delaying asynchronous operations and replaces existing inline sleep implementations across various files with this unified function.

### Detailed summary
- Added `sleep` function in `packages/thirdweb/src/utils/sleep.ts` for delaying async operations.
- Removed duplicate `sleep` functions from `packages/thirdweb/src/rpc/watchBlockNumber.ts` and `packages/thirdweb/src/wallets/in-app/web/utils/iFrameCommunication/IframeCommunicator.ts`.
- Updated calls to `sleep` in `packages/thirdweb/src/wallets/smart/smart-wallet-integration-v07.test.ts` and `packages/thirdweb/src/wallets/in-app/web/utils/iFrameCommunication/IframeCommunicator.ts` to use the new function.

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

<!-- end pr-codex -->
2024-09-28 23:11:39 +00:00
kien-ngo 90b5495efb [SDK] Add Pack extensions (#4579)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR introduces new functionalities and enhancements for the `Pack` contract in the `thirdweb` project, including event definitions, deployment options, and methods for managing packs and rewards.

### Detailed summary
- Added `initialize` function to `Pack.json`.
- Exported new types and functions in `deploy-pack.ts`.
- Defined new events and functions in `IPackVRFDirect.json`.
- Created tests for deploying `Pack` contracts.
- Updated changelog with Pack extensions.
- Implemented various methods for managing packs, including `openPack`, `addPackContents`, and `claimRewards`.

> The following files were skipped due to too many changes: `packages/thirdweb/src/extensions/pack/createNewPack.test.ts`, `packages/thirdweb/src/extensions/prebuilts/__generated__/Pack/write/initialize.ts`, `packages/thirdweb/src/extensions/pack/__generated__/IPack/write/createPack.ts`, `packages/thirdweb/src/extensions/pack/createNewPack.ts`

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

<!-- end pr-codex -->
2024-09-28 12:46:54 +00:00
Kien NgoandGitHub 71575afef3 [SDK] Avoid out-of-fund in tests (#4840) 2024-09-28 20:59:40 +12:00
Jonas DanielsGitHubgithub-actions[bot] <github-actions[bot]@users.noreply.github.com>
70adb57293 Version Packages (#4841)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-09-28 20:48:14 +12:00
Kien NgoandGitHub 96fa4102ba [SDK] Avoid IPFS uploads in tests whenever possible (#4824) 2024-09-28 20:26:44 +12:00
Joaquim VergesandGitHub fc56142880 fix: createAndSignUserOp with multiple transactions (#4839) 2024-09-28 20:26:06 +12:00
gregfromstl 842baa0513 [SDK] Fix: ensure optional chains are passed to wc (#4838)
<!-- start pr-codex -->

## PR-Codex overview
This PR focuses on enhancing the `wallet-connect` functionality by ensuring that optional chains are correctly handled in the wallet connection request process.

### Detailed summary
- Updated the destructuring of the return value from `getChainsToRequest` to include `requiredChain`.
- Changed how `optionalChains` and `chains` are assigned in `connectWC` and `initProvider` functions.
- Modified the return structure of `getChainsToRequest` to include `requiredChain` and `optionalChains`.

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

<!-- end pr-codex -->
2024-09-28 07:51:20 +00:00
jnsdls 601958d5b7 [Add] Framer rewrite for /framer-test route (#4577)
<!-- start pr-codex -->

## PR-Codex overview
This PR updates the `next.config.js` file to enhance security policies and add new redirect paths for Framer.

### Detailed summary
- Updated `style-src` and `font-src` directives to include `vercel.live`, `assets.vercel.com`, and `framerusercontent.com`.
- Added `framerusercontent.com` and `events.framer.com` to the `script-src` directive.
- Introduced `FRAMER_PATHS` for specific redirect paths.
- Added new redirect mappings for Framer paths to `https://landing.thirdweb.com`.

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

<!-- end pr-codex -->
2024-09-27 22:30:41 +00:00
Jonas DanielsGitHubgithub-actions[bot] <github-actions[bot]@users.noreply.github.com>
bec828de86 Version Packages (#4822)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-09-27 15:30:03 -07:00
gregfromstl a576a5b858 [SDK] Test: Adds storage mocks and basic tests (#4833)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR introduces new features and improvements related to file uploads and downloads using IPFS and Arweave, along with enhancements to testing with `msw`. It also updates dependencies and modifies some configurations.

### Detailed summary
- Updated `.gitignore` to include `.aider*`.
- Modified `web-node.ts` to streamline header handling.
- Added `msw` as a dependency in `package.json`.
- Enhanced `download.ts` to better handle request timeouts.
- Implemented `msw` handlers in `storage.ts` for IPFS and Arweave.
- Expanded tests in `download.test.ts` for various download scenarios.
- Added tests in `web-node.test.ts` for file uploads and error handling.

> 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-09-27 22:04:39 +00:00
MananTank 5e3be734a3 Fix line break in engine Allowlisted Domains textarea placeholder (#4835)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR updates the `placeholder` prop of the `Textarea` component in the `cors.tsx` file to use a JavaScript expression instead of a string literal.

### Detailed summary
- Changed the `placeholder` prop from a string literal (`"https://example.com\nhttp://localhost:3000"`) to a JavaScript expression (`{"https://example.com\nhttp://localhost:3000"}`) for potentially better handling of the string.

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

<!-- end pr-codex -->
2024-09-27 22:00:05 +00:00
MananTank ce6d873ccc Fix Page crash on Module selection (#4834)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on enhancing the handling of `selectedModule` in the `ModuleForm.tsx` component by including additional properties for better data management.

### Detailed summary
- Added `selectedModule?.contractId` to the data array.
- Added `selectedModule?.bytecodeHash` to the data array.

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

<!-- end pr-codex -->
2024-09-27 21:39:17 +00:00
gregfromstl bbe75b0bc8 [SDK] Fix: Only request specified wallet connect chains (#4832)
Falls in line with the WC spec and appeases some picky wallets

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on fixing wallet connection issues in the `thirdweb` package, particularly for wallets with limited chain support. It improves handling of optional chains and modifies how chains are requested.

### Detailed summary
- Updated handling of `optionalChains` to default to `[1]` if empty.
- Changed `chains` to be `undefined` if `chainToRequest` is not provided.
- Modified `setRequestedChainsIds` function to accept `chains` as `undefined`.
- Simplified logic for `chainsToRequest` to use `undefined` when no chains are available.

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

<!-- end pr-codex -->
2024-09-27 19:47:31 +00:00
gregfromstl cefa4eb097 [SDK] Docs: fix social profiles docs (#4821)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR updates the method used to retrieve social profiles in the `profiles.ts` file, changing the function name from `getProfiles` to `getSocialProfiles`.

### Detailed summary
- Changed the import statement from `getProfiles` to `getSocialProfiles` from the `thirdweb/social` module.
- Updated the usage of the function in the example to reflect the new function name.

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

<!-- end pr-codex -->
2024-09-27 16:34:01 +00:00
kien-ngo 63ceb2874c [Dashboard] Remove <Stack/> (3) (#4831)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on replacing instances of the `Stack` component from Chakra UI with `div` elements styled using Tailwind CSS classes. This change aims to improve layout consistency across various components in the application.

### Detailed summary
- Replaced `<Stack>` with `<div className="flex ...">` in multiple components for layout consistency.
- Adjusted spacing and alignment properties to match previous `Stack` behavior.
- Updated components in `airdrop-tab.tsx`, `select-option.tsx`, `airdrop-upload.tsx`, and others.

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

<!-- end pr-codex -->
2024-09-27 16:08:38 +00:00
kien-ngo 509c7981ee [Dashboard] Remove <Stack /> (2) (#4830)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR primarily focuses on replacing instances of the `Stack` component with `div` elements styled using Tailwind CSS classes, enhancing the layout while maintaining functionality across various components in the dashboard application.

### Detailed summary
- Replaced `<Stack>` with `<div className="flex flex-col gap-X">` in multiple components for consistent styling.
- Updated `spacing` properties to `gap` for better alignment with Tailwind CSS practices.
- Maintained functionality across components such as `PermissionEditor`, `EventsFeedItem`, and various modal dialogs.

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

<!-- end pr-codex -->
2024-09-27 15:54:39 +00:00
kien-ngo 8ea9420cc4 [Dashboard] Remove <Stack /> (1) (#4829)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on replacing instances of the `Stack` component with `div` elements styled with Flexbox classes for layout consistency across various components in the application.

### Detailed summary
- Replaced `Stack` components with `div` elements using Flexbox for layout in multiple files.
- Updated spacing and alignment properties to maintain visual consistency.
- Adjusted modal body structures to use Flexbox for better responsiveness.

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

<!-- end pr-codex -->
2024-09-27 15:48:40 +00:00
kien-ngo 8dda1139dc [Dashboard] Remove default <Stack /> (#4828)
@MananTank here's the source code: https://github.com/chakra-ui/chakra-ui/blob/main/packages/react/src/components/stack/stack.tsx

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on replacing `Stack` components with `div` elements styled with `flex` and `gap` classes across various files for a more consistent layout approach in the UI.

### Detailed summary
- Replaced `Stack` with `div className="flex flex-col gap-2"` in multiple components.
- Updated layout in `airdrop-tab.tsx`, `mint-form.tsx`, `lazy-mint-form.tsx`, `split/page.tsx`, `shared-metadata-form.tsx`, `update-metadata-form.tsx`, `transaction-timeline.tsx`, `NewsletterSection.tsx`, and more.
- Enhanced UI consistency and responsiveness.

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

<!-- end pr-codex -->
2024-09-27 11:21:45 +00:00
kien-ngo a637cab051 [Dashboard] Remove @react-icons/all-files (#4819)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on updating icon components in the `dashboard` application by replacing `react-icons` with custom SVG icons for improved performance and maintainability.

### Detailed summary
- Removed `@react-icons/all-files` dependency.
- Added new SVG components: `MediumIcon`, `YoutubeIcon`, `TiktokIcon`, `TelegramIcon`, `RedditIcon`, `MetaIcon`, `InstagramIcon`.
- Updated `SupportPage`, `PublisherSocials`, `AppFooter`, and other components to use new icons.

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

<!-- end pr-codex -->
2024-09-27 03:07:36 +00:00
kien-ngo c31f919cd4 [Dashboard] Replace Linkedin icon (#4818)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR introduces a new `LinkedInIcon` component and replaces instances of the `SiLinkedin` icon with the new `LinkedInIcon` in two files, enhancing the consistency of icon usage across the application.

### Detailed summary
- Created a new `LinkedInIcon` component in `LinkedinIcon.tsx`.
- Replaced `SiLinkedin` with `LinkedInIcon` in `socialLinks.tsx`.
- Replaced `SiLinkedin` with `LinkedInIcon` in `PublisherSocials.tsx`.

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

<!-- end pr-codex -->
2024-09-27 02:41:46 +00:00
kien-ngo ffcd199381 [Dashboard] Replace twitter icon (#4817)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR introduces a new `XIcon` component and replaces existing Twitter icons in various components with this new icon, enhancing consistency and potentially improving the visual design.

### Detailed summary
- Added a new `XIcon` component in `XIcon.tsx`.
- Replaced `SiTwitter` icon with `XIcon` in:
  - `edit-profile.tsx`
  - `socialLinks.tsx`
  - `PublisherSocials.tsx`

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

<!-- end pr-codex -->
2024-09-27 02:41:43 +00:00
kien-ngo b1d78b1f42 [Dashboard] Replace Discord icon (#4816)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on replacing the usage of the `SiDiscord` icon from `@react-icons/all-files` with a custom `DiscordIcon` component in multiple files, enhancing the icon's integration and potentially improving styling consistency.

### Detailed summary
- Replaced `SiDiscord` icon with `DiscordIcon` in:
  - `edit-profile.tsx`
  - `socialLinks.tsx`
  - `PublisherSocials.tsx`
- Introduced `DiscordIcon` component in `DiscordIcon.tsx` with an SVG representation.

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

<!-- end pr-codex -->
2024-09-27 01:52:10 +00:00
kien-ngo bc348ea953 [Dashboard] Replace Unity icon (#4814)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on replacing instances of the `SiUnity` icon from the `react-icons` library with a new custom `UnityIcon` component across multiple files in the dashboard application.

### Detailed summary
- Added a new `UnityIcon` component in `UnityIcon.tsx`.
- Replaced `SiUnity` with `UnityIcon` in:
  - `ConnectSDKCard.tsx`
  - `ConnectPlaygroundButton.tsx`
  - `CodeOptionButton.tsx`
  - `SupportedPlatformLink.tsx`
  - `CodeSegment.tsx`

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

<!-- end pr-codex -->
2024-09-27 01:24:04 +00:00
kien-ngo 43a24e2ed4 [Dashboard] Replace React icon (#4813)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on replacing the usage of `SiReact` icon with a new custom `ReactIcon` component across several files in the dashboard application.

### Detailed summary
- Added `ReactIcon` component in `apps/dashboard/src/components/icons/brand-icons/ReactIcon.tsx`.
- Replaced `SiReact` with `ReactIcon` in:
  - `ConnectSDKCard.tsx`
  - `ConnectPlaygroundButton.tsx`
  - `CodeOptionButton.tsx`
  - `SupportedPlatformLink.tsx`
  - `CodeSegment.tsx`

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

<!-- end pr-codex -->
2024-09-27 01:24:02 +00:00
kien-ngo 7b40f2e8e4 [Dashboard] Replace JS and TS icons (#4812)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on replacing existing icon imports for `JavaScript` and `TypeScript` with custom icon components, enhancing the visual representation in the application.

### Detailed summary
- Replaced `SiJavascript` with `JavaScriptIcon` in `ConnectPlaygroundButton.tsx`, `CodeOptionButton.tsx`, `CodeSegment.tsx`.
- Replaced `SiTypescript` with `TypeScriptIcon` in `SupportedPlatformLink.tsx`, `ConnectSDKCard.tsx`, `CodeSegment.tsx`.
- Added new components: `JavaScriptIcon.tsx` and `TypeScriptIcon.tsx` for custom icons.

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

<!-- end pr-codex -->
2024-09-27 01:24:00 +00:00
kien-ngo c1d94aec4f [Dashboard] Replace SiGithub (#4811)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on replacing the usage of the `SiGithub` icon from `@react-icons` with a custom `GithubIcon` component across several files in the dashboard application.

### Detailed summary
- Replaced `SiGithub` with `GithubIcon` in:
  - `GameCard.tsx`
  - `app-footer.tsx`
  - `edit-profile.tsx`
  - `socialLinks.tsx`
  - `PublisherSocials.tsx`
- Added `GithubIcon` component in `GithubIcon.tsx` with SVG implementation.

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

<!-- end pr-codex -->
2024-09-27 01:23:58 +00:00
kien-ngo ebd5908d4a [Dashboard] Remove non-branding icons (#4810)
<!-- start pr-codex -->

## PR-Codex overview
This PR focuses on replacing various `react-icons` with `lucide-react` icons across multiple components, along with some minor adjustments to properties and imports. It aims to standardize icon usage within the codebase.

### Detailed summary
- Removed `IconType` from multiple interfaces in `types.ts` and `AnimatedCLICommand.tsx`.
- Replaced `react-icons` with `lucide-react` icons in `HackathonFooter`, `NFTClaimButton`, and other components.
- Updated icon rendering logic in multiple components for consistency.
- Adjusted imports and usage of icons in `FileInput`, `BatchLazyMint`, and `MismatchButton`.

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

<!-- end pr-codex -->
2024-09-27 01:11:42 +00:00
Edward SunandGitHub 21b032d1b8 Remove text in UI (#4823) 2024-09-26 16:56:46 -07:00
kumaryash90 d0616c4486 Fix Cronos zkevm deployments (#4815)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR adds support for the `cronos zkevm` by updating the `isZkSyncChain` check to include additional chain IDs for both the testnet and mainnet.

### Detailed summary
- Updated the `isZkSyncChain` function in the file `packages/thirdweb/src/utils/any-evm/zksync/isZkSyncChain.ts`.
- Added chain ID `282` for `cronos zkevm testnet`.
- Added chain ID `388` for `cronos zkevm mainnet`.

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

<!-- end pr-codex -->
2024-09-26 22:13:03 +00:00
MananTank 88db5eaeab Add upload to IPFS on teams & account avatar form fields (#4807)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on enhancing the account and team settings pages by adding image upload functionality for accounts and teams, updating component imports, and improving layout responsiveness.

### Detailed summary
- Added `updateAccountImage` method to `AccountSettingsPage` and `AccountSettingsPageUI`.
- Introduced `updateTeamImage` method in `TeamGeneralSettingsPage` and `TeamGeneralSettingsPageUI`.
- Changed layout from `min-h-full` to `min-h-screen`.
- Updated import statements to reflect new component names.
- Implemented async image upload functionality with IPFS integration.
- Added error handling for image update methods.

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

<!-- end pr-codex -->
2024-09-26 20:43:18 +00:00
MananTank 09b1c28c09 Replace Avatar placeholders with Avatar components in teams and account layout (#4803)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR primarily focuses on updating the account handling in various components to use an `account` object instead of separate email fields, and introduces new avatar components for enhanced visual representation of users and projects.

### Detailed summary
- Replaced `email` with `account` in multiple components.
- Introduced `GradientAvatar` and `ProjectAvatar` components for displaying user and project images.
- Updated story files to showcase new avatar components.
- Added placeholder comments for future image integration.
- Adjusted layout and styling for better alignment with new account structure.

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

<!-- end pr-codex -->
2024-09-26 20:43:14 +00:00
530 changed files with 12414 additions and 10607 deletions
+2 -1
View File
@@ -27,4 +27,5 @@ sitemap*.xml
packages/*/typedoc/*
*storybook.log
storybook-static
storybook-static
.aider*
+4
View File
@@ -60,6 +60,8 @@ module.exports = {
"AspectRatio",
"useToast",
"useClipboard",
"Badge",
"Stack",
// also the types
"ButtonProps",
"BadgeProps",
@@ -71,6 +73,8 @@ module.exports = {
"MenuGroupProps",
"MenuItemProps",
"AspectRatioProps",
"BadgeProps",
"StackProps",
],
message:
'Use the equivalent component from "tw-components" instead.',
+10 -3
View File
@@ -5,10 +5,10 @@ const ContentSecurityPolicy = `
img-src * data: blob:;
media-src * data: blob:;
object-src 'none';
style-src 'self' 'unsafe-inline' https://vercel.live;
font-src 'self' https://vercel.live https://assets.vercel.com;
style-src 'self' 'unsafe-inline' vercel.live;
font-src 'self' vercel.live assets.vercel.com framerusercontent.com;
frame-src * data:;
script-src 'self' 'unsafe-eval' 'unsafe-inline' 'wasm-unsafe-eval' 'inline-speculation-rules' *.thirdweb.com *.thirdweb-dev.com vercel.live js.stripe.com;
script-src 'self' 'unsafe-eval' 'unsafe-inline' 'wasm-unsafe-eval' 'inline-speculation-rules' *.thirdweb.com *.thirdweb-dev.com vercel.live js.stripe.com framerusercontent.com events.framer.com;
connect-src * data: blob:;
worker-src 'self' blob:;
block-all-mixed-content;
@@ -39,6 +39,9 @@ const securityHeaders = [
const redirects = require("./redirects");
// add framer paths here
const FRAMER_PATHS = ["/connect/sign-in", "/contracts/modular-contracts"];
/**
* @returns {import('next').RemotePattern[]}
*/
@@ -128,6 +131,10 @@ const moduleExports = {
source: "/thirdweb.eth/:path*",
destination: "/deployer.thirdweb.eth/:path*",
},
...FRAMER_PATHS.map((path) => ({
source: path,
destination: `https://landing.thirdweb.com${path}`,
})),
];
},
images: {
+17 -18
View File
@@ -45,8 +45,7 @@
"@radix-ui/react-slot": "^1.1.0",
"@radix-ui/react-switch": "^1.1.0",
"@radix-ui/react-tooltip": "1.1.2",
"@react-icons/all-files": "^4.1.0",
"@sentry/nextjs": "8.30.0",
"@sentry/nextjs": "8.32.0",
"@shazow/whatsabi": "^0.14.1",
"@stripe/react-stripe-js": "^2.7.3",
"@stripe/stripe-js": "^3.5.0",
@@ -62,8 +61,9 @@
"compare-versions": "^6.1.0",
"date-fns": "^3.6.0",
"flat": "^6.0.1",
"framer-motion": "11.5.6",
"framer-motion": "11.9.0",
"fuse.js": "7.0.0",
"input-otp": "^1.2.4",
"ioredis": "^5.4.1",
"ipaddr.js": "^2.2.0",
"lottie-react": "^2.4.0",
@@ -88,7 +88,6 @@
"react-icons": "^5.2.1",
"react-intersection-observer": "^9.10.3",
"react-markdown": "^9.0.1",
"react-otp-input": "^3.1.1",
"react-responsive-carousel": "^3.2.23",
"react-table": "^7.8.0",
"recharts": "^2.12.7",
@@ -110,21 +109,21 @@
"@next/bundle-analyzer": "14.2.13",
"@next/eslint-plugin-next": "14.2.13",
"@playwright/test": "1.47.2",
"@storybook/addon-essentials": "8.3.2",
"@storybook/addon-interactions": "8.3.2",
"@storybook/addon-links": "8.3.2",
"@storybook/addon-onboarding": "8.3.2",
"@storybook/addon-viewport": "8.3.2",
"@storybook/blocks": "8.3.2",
"@storybook/nextjs": "8.3.2",
"@storybook/react": "8.3.2",
"@storybook/test": "8.3.2",
"@storybook/addon-essentials": "8.3.4",
"@storybook/addon-interactions": "8.3.4",
"@storybook/addon-links": "8.3.4",
"@storybook/addon-onboarding": "8.3.4",
"@storybook/addon-viewport": "8.3.4",
"@storybook/blocks": "8.3.4",
"@storybook/nextjs": "8.3.4",
"@storybook/react": "8.3.4",
"@storybook/test": "8.3.4",
"@types/color": "^3.0.6",
"@types/node": "20.14.9",
"@types/papaparse": "^5.3.14",
"@types/pluralize": "^0.0.33",
"@types/qrcode": "^1.5.5",
"@types/react": "^18.3.8",
"@types/react": "^18.3.10",
"@types/react-dom": "^18",
"@types/react-table": "^7.7.20",
"@types/spdx-correct": "^3.1.3",
@@ -136,12 +135,12 @@
"eslint": "8.57.0",
"eslint-config-biome": "1.8.4",
"eslint-plugin-react-compiler": "0.0.0-experimental-ca16900-20240916",
"eslint-plugin-storybook": "^0.8.0",
"knip": "^5.30.5",
"eslint-plugin-storybook": "^0.9.0",
"knip": "^5.30.6",
"next-sitemap": "^4.2.3",
"postcss": "8.4.47",
"storybook": "8.3.2",
"tailwindcss": "3.4.12",
"storybook": "8.3.4",
"tailwindcss": "3.4.13",
"typescript": "5.6.2"
}
}
+3 -5
View File
@@ -1,10 +1,8 @@
import "server-only";
import { COOKIE_ACTIVE_ACCOUNT, COOKIE_PREFIX_TOKEN } from "@/constants/cookie";
import { API_SERVER_URL } from "@/constants/env";
import { cookies } from "next/headers";
const THIRDWEB_API_HOST =
process.env.NEXT_PUBLIC_THIRDWEB_API_HOST || "https://api.thirdweb.com";
export type Project = {
id: string;
name: string;
@@ -34,7 +32,7 @@ export async function getProjects(teamSlug: string) {
}
const teamsRes = await fetch(
`${THIRDWEB_API_HOST}/v1/teams/${teamSlug}/projects`,
`${API_SERVER_URL}/v1/teams/${teamSlug}/projects`,
{
headers: {
Authorization: `Bearer ${token}`,
@@ -59,7 +57,7 @@ export async function getProject(teamSlug: string, projectSlug: string) {
}
const teamsRes = await fetch(
`${THIRDWEB_API_HOST}/v1/teams/${teamSlug}/projects/${projectSlug}`,
`${API_SERVER_URL}/v1/teams/${teamSlug}/projects/${projectSlug}`,
{
headers: {
Authorization: `Bearer ${token}`,
+2 -4
View File
@@ -1,10 +1,8 @@
import "server-only";
import { COOKIE_ACTIVE_ACCOUNT, COOKIE_PREFIX_TOKEN } from "@/constants/cookie";
import { API_SERVER_URL } from "@/constants/env";
import { cookies } from "next/headers";
const THIRDWEB_API_HOST =
process.env.NEXT_PUBLIC_THIRDWEB_API_HOST || "https://api.thirdweb.com";
const TeamAccountRole = {
OWNER: "OWNER",
MEMBER: "MEMBER",
@@ -39,7 +37,7 @@ export async function getMembers(teamSlug: string) {
}
const teamsRes = await fetch(
`${THIRDWEB_API_HOST}/v1/teams/${teamSlug}/members`,
`${API_SERVER_URL}/v1/teams/${teamSlug}/members`,
{
headers: {
Authorization: `Bearer ${token}`,
+3 -5
View File
@@ -1,10 +1,8 @@
import "server-only";
import { COOKIE_ACTIVE_ACCOUNT, COOKIE_PREFIX_TOKEN } from "@/constants/cookie";
import { API_SERVER_URL } from "@/constants/env";
import { cookies } from "next/headers";
const THIRDWEB_API_HOST =
process.env.NEXT_PUBLIC_THIRDWEB_API_HOST || "https://api.thirdweb.com";
export type Team = {
id: string;
name: string;
@@ -35,7 +33,7 @@ export async function getTeamBySlug(slug: string) {
return null;
}
const teamRes = await fetch(`${THIRDWEB_API_HOST}/v1/teams/${slug}`, {
const teamRes = await fetch(`${API_SERVER_URL}/v1/teams/${slug}`, {
headers: {
Authorization: `Bearer ${token}`,
},
@@ -57,7 +55,7 @@ export async function getTeams() {
return [];
}
const teamsRes = await fetch(`${THIRDWEB_API_HOST}/v1/teams`, {
const teamsRes = await fetch(`${API_SERVER_URL}/v1/teams`, {
headers: {
Authorization: `Bearer ${token}`,
},
@@ -0,0 +1,89 @@
import type { Meta, StoryObj } from "@storybook/react";
import { useState } from "react";
import { BadgeContainer } from "../../../../stories/utils";
import { Button } from "../../ui/button";
import { GradientAvatar } from "./GradientAvatar";
const meta = {
title: "blocks/Avatars/GradientAvatar",
component: Story,
parameters: {},
} satisfies Meta<typeof Story>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Desktop: Story = {
args: {},
};
function Story() {
return (
<div className="flex flex-col gap-10 p-10">
<p> All images below are set with size-6 className </p>
<BadgeContainer label="No Src, No id - Skeleton">
<GradientAvatar id={undefined} src={undefined} className="size-6" />
</BadgeContainer>
<BadgeContainer label="No Src, id=foo - Skeleton">
<GradientAvatar id={"foo"} src={undefined} className="size-6" />
</BadgeContainer>
<BadgeContainer label="Invalid/Empty Src, id=foo - Gradient">
<GradientAvatar id={"foo"} src={""} className="size-6" />
</BadgeContainer>
<BadgeContainer label="Invalid/Empty Src, id=bar - Gradient">
<GradientAvatar id={"bar"} src={""} className="size-6" />
</BadgeContainer>
<BadgeContainer label="Empty/Invalid Src, No id - default fallback">
<GradientAvatar src="invalid-src" id={undefined} className="size-6" />
</BadgeContainer>
<ToggleTest />
</div>
);
}
function ToggleTest() {
const [data, setData] = useState<undefined | { src: string; id: string }>(
undefined,
);
return (
<div className="relative flex flex-col gap-10 border p-6">
<Button
variant="outline"
onClick={() => {
if (data) {
setData(undefined);
} else {
setData({
id: "foo",
src: "https://picsum.photos/400",
});
}
}}
className="absolute top-6 right-6 inline-flex"
>
Toggle Src
</Button>
<p> Src+ID is: {data ? "set" : "not set"} </p>
<BadgeContainer label="Valid Src">
<GradientAvatar src={data?.src} id={data?.id} className="size-6" />
</BadgeContainer>
<BadgeContainer label="invalid Src">
<GradientAvatar
className="size-6"
src={data ? "invalid-src" : undefined}
id={undefined}
/>
</BadgeContainer>
</div>
);
}
@@ -0,0 +1,58 @@
import { Img } from "@/components/blocks/Img";
import { useMemo } from "react";
import { cn } from "../../../lib/utils";
const gradients = [
["#fca5a5", "#b91c1c"],
["#fdba74", "#c2410c"],
["#fcd34d", "#b45309"],
["#fde047", "#a16207"],
["#a3e635", "#4d7c0f"],
["#86efac", "#15803d"],
["#67e8f9", "#0e7490"],
["#7dd3fc", "#0369a1"],
["#93c5fd", "#1d4ed8"],
["#a5b4fc", "#4338ca"],
["#c4b5fd", "#6d28d9"],
["#d8b4fe", "#7e22ce"],
["#f0abfc", "#a21caf"],
["#f9a8d4", "#be185d"],
["#fda4af", "#be123c"],
];
function getGradientForString(str: string) {
const number = Math.abs(
str.split("").reduce((acc, b, i) => acc + b.charCodeAt(0) * (i + 1), 0),
);
const index = number % gradients.length;
return gradients[index];
}
export function GradientAvatar(props: {
src: string | undefined;
id: string | undefined;
className: string;
}) {
const gradient = useMemo(() => {
if (!props.id) {
return undefined;
}
return getGradientForString(props.id);
}, [props.id]);
return (
<Img
src={props.src}
className={cn("rounded-full", props.className)}
fallback={
gradient ? (
<div
style={{
background: `linear-gradient(45deg, ${gradient[0]} 0%, ${gradient[1]} 100%)`,
}}
/>
) : undefined
}
/>
);
}
@@ -0,0 +1,76 @@
import type { Meta, StoryObj } from "@storybook/react";
import { useState } from "react";
import { BadgeContainer } from "../../../../stories/utils";
import { Button } from "../../ui/button";
import { ProjectAvatar } from "./ProjectAvatar";
const meta = {
title: "blocks/Avatars/ProjectAvatar",
component: Story,
parameters: {},
} satisfies Meta<typeof Story>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Desktop: Story = {
args: {},
};
function Story() {
return (
<div className="flex flex-col gap-10 p-10">
<p> All images below are set with size-6 className </p>
<BadgeContainer label="No Src - Skeleton">
<ProjectAvatar src={undefined} className="size-6" />
</BadgeContainer>
<BadgeContainer label="Invalid/Empty Src - BoxIcon Fallback">
<ProjectAvatar src={""} className="size-6" />
</BadgeContainer>
<ToggleTest />
</div>
);
}
function ToggleTest() {
const [data, setData] = useState<undefined | { src: string; name: string }>(
undefined,
);
return (
<div className="relative flex flex-col gap-10 border p-6">
<Button
variant="outline"
onClick={() => {
if (data) {
setData(undefined);
} else {
setData({
name: "foo",
src: "https://picsum.photos/400",
});
}
}}
className="absolute top-6 right-6 inline-flex"
>
Toggle Src
</Button>
<p> Src+Name is: {data ? "set" : "not set"} </p>
<BadgeContainer label="Valid Src">
<ProjectAvatar src={data?.src} className="size-6" />
</BadgeContainer>
<BadgeContainer label="invalid Src">
<ProjectAvatar
src={data ? "invalid-src" : undefined}
className="size-6"
/>
</BadgeContainer>
</div>
);
}
@@ -0,0 +1,21 @@
import { Img } from "@/components/blocks/Img";
import { BoxIcon } from "lucide-react";
import { cn } from "../../../lib/utils";
export function ProjectAvatar(props: {
src: string | undefined;
className: string | undefined;
}) {
return (
<Img
src={props.src}
className={cn("rounded-lg border", props.className)}
alt={""}
fallback={
<div className="flex items-center justify-center bg-muted/50">
<BoxIcon className="size-[50%] text-muted-foreground" />
</div>
}
/>
);
}
@@ -4,25 +4,45 @@ import { Button } from "@/components/ui/button";
import { Dialog, DialogContent, DialogTrigger } from "@/components/ui/dialog";
import { ChevronDownIcon } from "lucide-react";
import { usePathname } from "next/navigation";
import { useState } from "react";
import { useMemo, useState } from "react";
import { cn } from "../../lib/utils";
import { NavLink } from "../ui/NavLink";
import type { SidebarLink } from "./Sidebar";
import {
RenderSidebarLinks,
type SidebarBaseLink,
type SidebarLink,
} from "./Sidebar";
export function MobileSidebar(props: {
links?: SidebarLink[];
links: SidebarLink[];
footer?: React.ReactNode;
trigger?: React.ReactNode;
triggerClassName?: string;
}) {
const [isOpen, setIsOpen] = useState(false);
const pathname = usePathname();
const activeLink = props.links?.find((link) => {
if (link.exactMatch) {
return link.href === pathname;
const activeLink = useMemo(() => {
function isActive(link: SidebarBaseLink) {
if (link.exactMatch) {
return link.href === pathname;
}
return pathname?.startsWith(link.href);
}
return pathname?.startsWith(link.href);
});
for (const link of props.links) {
if ("group" in link) {
for (const subLink of link.links) {
if (isActive(subLink)) {
return subLink;
}
}
} else {
if (isActive(link)) {
return link;
}
}
}
}, [props.links, pathname]);
const defaultTrigger = (
<Button
@@ -41,7 +61,7 @@ export function MobileSidebar(props: {
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger asChild>{props.trigger || defaultTrigger}</DialogTrigger>
<DialogContent
className="rounded-t-xl rounded-b-none p-4"
className="no-scrollbar max-h-[80vh] overflow-auto rounded-t-xl rounded-b-none p-4"
dialogCloseClassName="hidden"
onClick={(e) => {
if (e.target instanceof HTMLAnchorElement) {
@@ -49,23 +69,7 @@ export function MobileSidebar(props: {
}
}}
>
<div className="flex flex-col gap-2">
{props.links?.map((link) => {
return (
<Button size="sm" variant="ghost" asChild key={link.href}>
<NavLink
href={link.href}
className="!text-left flex h-auto justify-start gap-2 py-3"
activeClassName="bg-accent"
exactMatch={link.exactMatch}
tracking={link.tracking}
>
{link.label}
</NavLink>
</Button>
);
})}
</div>
<RenderSidebarLinks links={props.links} />
{props.footer}
</DialogContent>
</Dialog>
@@ -3,7 +3,7 @@ import type React from "react";
import { cn } from "../../lib/utils";
import { NavLink } from "../ui/NavLink";
export type SidebarLink = {
export type SidebarBaseLink = {
href: string;
label: React.ReactNode;
exactMatch?: boolean;
@@ -14,6 +14,13 @@ export type SidebarLink = {
};
};
export type SidebarLink =
| SidebarBaseLink
| {
group: string;
links: SidebarBaseLink[];
};
type SidebarContentProps = {
header?: React.ReactNode;
links: SidebarLink[];
@@ -31,23 +38,42 @@ export function Sidebar(props: SidebarContentProps) {
<div className="pt-7">
{props.header}
<div className="flex flex-col gap-1">
{props.links?.map((link) => {
const isExternal = link.href.startsWith("http");
return (
<NavLink
key={link.href}
href={link.href}
className="flex items-center gap-2 rounded-md px-3 py-2 text-muted-foreground text-sm hover:bg-muted"
activeClassName="text-foreground"
exactMatch={link.exactMatch}
>
{link.label}
{isExternal && <ExternalLinkIcon className="size-3" />}
</NavLink>
);
})}
<RenderSidebarLinks links={props.links} />
</div>
</div>
</aside>
);
}
export function RenderSidebarLinks(props: { links: SidebarLink[] }) {
return (
<div className="flex flex-col gap-1">
{props.links.map((link, i) => {
if ("group" in link) {
return (
<div className={cn({ "mt-6": i !== 0 })}>
<p className={cn("px-3 py-2 font-medium text-foreground")}>
{link.group}
</p>
<RenderSidebarLinks links={link.links} />
</div>
);
}
const isExternal = link.href.startsWith("http");
return (
<NavLink
key={link.href}
href={link.href}
className="flex items-center gap-2 rounded-md px-3 py-2 text-muted-foreground text-sm hover:bg-muted"
activeClassName="text-foreground"
exactMatch={link.exactMatch}
>
{link.label}
{isExternal && <ExternalLinkIcon className="size-3" />}
</NavLink>
);
})}
</div>
);
}
@@ -1,17 +1,15 @@
import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";
import { ThirdwebMiniLogo } from "app/components/ThirdwebMiniLogo";
import { DiscordIcon } from "components/icons/brand-icons/DiscordIcon";
import { GithubIcon } from "components/icons/brand-icons/GithubIcon";
import { InstagramIcon } from "components/icons/brand-icons/InstagramIcon";
import { LinkedInIcon } from "components/icons/brand-icons/LinkedinIcon";
import { RedditIcon } from "components/icons/brand-icons/RedditIcon";
import { TiktokIcon } from "components/icons/brand-icons/TiktokIcon";
import { XIcon } from "components/icons/brand-icons/XIcon";
import { YoutubeIcon } from "components/icons/brand-icons/YoutubeIcon";
import Link from "next/link";
import {
SiDiscord,
SiGithub,
SiInstagram,
SiLinkedin,
SiReddit,
SiTiktok,
SiX,
SiYoutube,
} from "react-icons/si";
type AppFooterProps = {
className?: string;
@@ -35,17 +33,17 @@ export function AppFooter(props: AppFooterProps) {
<div className="flex flex-row gap-3">
<Button size="icon" variant="ghost" asChild className="size-9">
<Link href="https://github.com/thirdweb-dev" target="_blank">
<SiGithub className="size-5 text-muted-foreground" />
<GithubIcon className="size-5 text-muted-foreground" />
</Link>
</Button>
<Button size="icon" variant="ghost" asChild className="size-9">
<Link href="https://www.tiktok.com/@thirdweb" target="_blank">
<SiTiktok className="size-5 text-muted-foreground" />
<TiktokIcon className="size-5 text-muted-foreground" />
</Link>
</Button>
<Button size="icon" variant="ghost" asChild className="size-9">
<Link href="https://www.instagram.com/thirdweb/" target="_blank">
<SiInstagram className="size-5 text-muted-foreground" />
<InstagramIcon className="size-5 text-muted-foreground" />
</Link>
</Button>
<Button size="icon" variant="ghost" asChild className="size-9">
@@ -53,27 +51,27 @@ export function AppFooter(props: AppFooterProps) {
href="hhttps://www.linkedin.com/company/third-web/"
target="_blank"
>
<SiLinkedin className="size-5 text-muted-foreground" />
<LinkedInIcon className="size-5 text-muted-foreground" />
</Link>
</Button>
<Button size="icon" variant="ghost" asChild className="size-9">
<Link href="https://www.youtube.com/@thirdweb_" target="_blank">
<SiYoutube className="size-5 text-muted-foreground" />
<YoutubeIcon className="size-5 text-muted-foreground" />
</Link>
</Button>
<Button size="icon" variant="ghost" asChild className="size-9">
<Link href="https://discord.gg/thirdweb" target="_blank">
<SiDiscord className="size-5 text-muted-foreground" />
<DiscordIcon className="size-5 text-muted-foreground" />
</Link>
</Button>
<Button size="icon" variant="ghost" asChild className="size-9">
<Link href="https://www.reddit.com/r/thirdweb/" target="_blank">
<SiReddit className="size-5 text-muted-foreground" />
<RedditIcon className="size-5 text-muted-foreground" />
</Link>
</Button>
<Button size="icon" variant="ghost" asChild className="size-9">
<Link href="https://x.com/thirdweb" target="_blank">
<SiX className="size-5 text-muted-foreground" />
<XIcon className="size-5 text-muted-foreground" />
</Link>
</Button>
</div>
@@ -1,14 +1,5 @@
"use client";
import { Area, AreaChart, CartesianGrid, XAxis } from "recharts";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import {
type ChartConfig,
ChartContainer,
@@ -18,11 +9,9 @@ import {
ChartTooltipContent,
} from "@/components/ui/chart";
import { useMemo } from "react";
import { Area, AreaChart, CartesianGrid, XAxis } from "recharts";
type ThirdwebAreaChartProps<TConfig extends ChartConfig> = {
// metadata
title: string;
description?: string;
// chart config
config: TConfig;
data: Array<Record<keyof TConfig, number> & { time: number | string | Date }>;
@@ -37,73 +26,65 @@ export function ThirdwebAreaChart<TConfig extends ChartConfig>(
) {
const configKeys = useMemo(() => Object.keys(props.config), [props.config]);
return (
<Card>
<CardHeader>
<CardTitle>{props.title}</CardTitle>
{props.description && (
<CardDescription>{props.description}</CardDescription>
)}
</CardHeader>
<CardContent>
<ChartContainer config={props.config} className={props.chartClassName}>
<AreaChart
accessibilityLayer
data={props.data}
margin={{
left: 12,
right: 12,
}}
>
<CartesianGrid vertical={false} />
<XAxis
dataKey="time"
tickLine={false}
axisLine={false}
tickMargin={8}
tickFormatter={(value) => new Date(value).toLocaleDateString()}
/>
<ChartTooltip cursor={false} content={<ChartTooltipContent />} />
<defs>
{configKeys.map((key) => (
<linearGradient
key={key}
id={`fill_${key}`}
x1="0"
y1="0"
x2="0"
y2="1"
>
<stop
offset="5%"
stopColor={`var(--color-${key})`}
stopOpacity={0.8}
/>
<stop
offset="95%"
stopColor={`var(--color-${key})`}
stopOpacity={0.1}
/>
</linearGradient>
))}
</defs>
<div className="rounded-lg border border-border bg-muted/50 px-4 pt-10 pb-4">
<ChartContainer config={props.config} className={props.chartClassName}>
<AreaChart
accessibilityLayer
data={props.data}
margin={{
left: 12,
right: 12,
}}
>
<CartesianGrid vertical={false} />
<XAxis
dataKey="time"
tickLine={false}
axisLine={false}
tickMargin={20}
tickFormatter={(value) => new Date(value).toLocaleDateString()}
/>
<ChartTooltip cursor={false} content={<ChartTooltipContent />} />
<defs>
{configKeys.map((key) => (
<Area
<linearGradient
key={key}
dataKey={key}
type="natural"
fill={`url(#fill_${key})`}
fillOpacity={0.4}
stroke={`var(--color-${key})`}
stackId="a"
/>
id={`fill_${key}`}
x1="0"
y1="0"
x2="0"
y2="1"
>
<stop
offset="5%"
stopColor={`var(--color-${key})`}
stopOpacity={0.8}
/>
<stop
offset="95%"
stopColor={`var(--color-${key})`}
stopOpacity={0.1}
/>
</linearGradient>
))}
</defs>
{configKeys.map((key) => (
<Area
key={key}
dataKey={key}
type="natural"
fill={`url(#fill_${key})`}
fillOpacity={0.4}
stroke={`var(--color-${key})`}
stackId="a"
/>
))}
{props.showLegend && (
<ChartLegend content={<ChartLegendContent />} />
)}
</AreaChart>
</ChartContainer>
</CardContent>
</Card>
{props.showLegend && (
<ChartLegend content={<ChartLegendContent />} className="pt-8" />
)}
</AreaChart>
</ChartContainer>
</div>
);
}
@@ -0,0 +1,17 @@
import { cn } from "@/lib/utils";
export function InlineCode({
code,
className,
}: { code: string; className?: string }) {
return (
<code
className={cn(
"inline-block rounded bg-muted px-2 font-mono text-sm",
className,
)}
>
{code}
</code>
);
}
@@ -0,0 +1,72 @@
"use client";
import { OTPInput, OTPInputContext } from "input-otp";
import { Dot } from "lucide-react";
import * as React from "react";
import { cn } from "@/lib/utils";
const InputOTP = React.forwardRef<
React.ElementRef<typeof OTPInput>,
React.ComponentPropsWithoutRef<typeof OTPInput>
>(({ className, containerClassName, ...props }, ref) => (
<OTPInput
ref={ref}
containerClassName={cn(
"flex items-center gap-2 has-[:disabled]:opacity-50",
containerClassName,
)}
className={cn("disabled:cursor-not-allowed", className)}
{...props}
/>
));
InputOTP.displayName = "InputOTP";
const InputOTPGroup = React.forwardRef<
React.ElementRef<"div">,
React.ComponentPropsWithoutRef<"div">
>(({ className, ...props }, ref) => (
<div ref={ref} className={cn("flex items-center", className)} {...props} />
));
InputOTPGroup.displayName = "InputOTPGroup";
const InputOTPSlot = React.forwardRef<
React.ElementRef<"div">,
React.ComponentPropsWithoutRef<"div"> & { index: number }
>(({ index, className, ...props }, ref) => {
const inputOTPContext = React.useContext(OTPInputContext);
const { char, hasFakeCaret, isActive } = inputOTPContext.slots[index];
return (
<div
ref={ref}
className={cn(
"relative flex h-10 w-10 items-center justify-center border-input border-y border-r text-sm transition-all first:rounded-l-md first:border-l last:rounded-r-md",
isActive && "z-10 ring-2 ring-ring ring-offset-background",
className,
)}
{...props}
>
{char}
{hasFakeCaret && (
<div className="pointer-events-none absolute inset-0 flex items-center justify-center">
<div className="h-4 w-px animate-caret-blink bg-foreground duration-1000" />
</div>
)}
</div>
);
});
InputOTPSlot.displayName = "InputOTPSlot";
const InputOTPSeparator = React.forwardRef<
React.ElementRef<"div">,
React.ComponentPropsWithoutRef<"div">
>(({ ...props }, ref) => (
// biome-ignore lint/a11y/useFocusableInteractive: <explanation>
<div ref={ref} role="separator" {...props}>
<Dot />
</div>
));
InputOTPSeparator.displayName = "InputOTPSeparator";
export { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator };
@@ -165,6 +165,8 @@ function useUnderline<El extends HTMLElement>() {
block: "nearest",
inline: "center",
});
} else if (lineRef.current) {
lineRef.current.style.width = "0px";
}
}
+3
View File
@@ -20,3 +20,6 @@ export const PROD_OR_DEV_URL = isProd ? "thirdweb.com" : "thirdweb-dev.com";
export const DASHBOARD_STORAGE_URL =
process.env.NEXT_PUBLIC_DASHBOARD_UPLOAD_SERVER ||
"https://storage.thirdweb.com";
export const API_SERVER_URL =
process.env.NEXT_PUBLIC_THIRDWEB_API_HOST || "https://api.thirdweb.com";
+2
View File
@@ -1,3 +1,5 @@
"use client";
import { useSyncExternalStore } from "react";
export type Store<T> = {
@@ -0,0 +1,3 @@
# DO NOT ADD NEW THINGS INSIDE THIS FOLDER OR SUB_FOLDERS
This part of the dashboard is deprecated, and we are actively working on removing it. Please do not add new things here. If you need to add new things, please add them to the appropriate folder in the src folder.
@@ -1,9 +1,3 @@
export const networkKeys = {
all: ["network"] as const,
chain: (chainId?: number) => [...networkKeys.all, chainId] as const,
multiChainRegistry: ["multi-chain-registry"] as const,
};
export const accountKeys = {
all: ["account"] as const,
wallet: (walletAddress: string) =>
@@ -1,98 +0,0 @@
import { networkKeys, useDashboardEVMChainId } from "@3rdweb-sdk/react";
import {
type MutationFunction,
type QueryFunction,
type QueryKey,
type UseMutationOptions,
type UseQueryOptions,
type UseQueryResult,
useMutation,
useQuery,
useQueryClient,
} from "@tanstack/react-query";
import { useCallback } from "react";
export function useQueryWithNetwork<
TQueryFnData = unknown,
TError = unknown,
TData = TQueryFnData,
TQueryKey extends QueryKey = QueryKey,
>(
queryKey: TQueryKey,
queryFn: QueryFunction<TQueryFnData, TQueryKey>,
options?: Omit<
UseQueryOptions<TQueryFnData, TError, TData, TQueryKey>,
"queryKey" | "queryFn"
>,
): UseQueryResult<TData, TError> {
const activeChainId = useDashboardEVMChainId();
const mergedOptions: Omit<
UseQueryOptions<TQueryFnData, TError, TData, TQueryKey>,
"queryKey" | "queryFn"
> = {
...options,
enabled: !!(activeChainId && options?.enabled),
};
const combinedQueryKey = (
networkKeys.chain(activeChainId) as readonly unknown[]
).concat(queryKey) as unknown as TQueryKey;
return useQuery({
...mergedOptions,
queryKey: combinedQueryKey,
queryFn,
});
}
export function useMutationWithInvalidate<
TData = unknown,
TError = unknown,
TVariables = void,
TContext = unknown,
TQueryKey extends QueryKey = QueryKey,
>(
mutationFn: MutationFunction<TData, TVariables>,
options?: Omit<
UseMutationOptions<TData, TError, TVariables, TContext>,
"mutationFn" | "onSuccess"
> & {
onSuccess?: (
data: TData,
variables: TVariables,
context: TContext | undefined,
wrapCacheKeys: (
cacheKeysToInvalidate: TQueryKey[],
// biome-ignore lint/suspicious/noConfusingVoidType: FIX ME
) => Promise<void[]>,
) => Promise<unknown> | undefined;
},
) {
const activeChainId = useDashboardEVMChainId();
const queryClient = useQueryClient();
const invalidate = useCallback(
(cacheKeysToInvalidate: TQueryKey[]) => {
return Promise.all(
cacheKeysToInvalidate.map((cacheKey) => {
return queryClient.invalidateQueries({
queryKey: (
networkKeys.chain(activeChainId) as readonly unknown[]
).concat(cacheKey) as unknown[],
});
}),
);
},
[queryClient, activeChainId],
);
return useMutation({
...options,
mutationFn,
onSuccess: (...args) => {
if (options?.onSuccess) {
options.onSuccess(...args, invalidate);
}
},
});
}
@@ -1,12 +1,4 @@
import {
createContext,
useCallback,
useContext,
useEffect,
useState,
} from "react";
import type { ChainMetadata } from "thirdweb/chains";
import invariant from "tiny-invariant";
export type EVMContractInfo = {
// using null instead of undefined here so that this type can be JSON stringified
@@ -14,78 +6,3 @@ export type EVMContractInfo = {
chainSlug: string;
contractAddress: string;
};
type SetEVMContractInfo = (info: EVMContractInfo) => void;
// Legacy: will be removed with contract page -> app router refactor
// eslint-disable-next-line no-restricted-syntax
const EVMContractInfoContext = createContext<EVMContractInfo | undefined>(
undefined,
);
// Legacy: will be removed with contract page -> app router refactor
// eslint-disable-next-line no-restricted-syntax
const SetEVMContractInfoContext = createContext<SetEVMContractInfo | undefined>(
undefined,
);
// TODO let's move this out of here eventually
export function EVMContractInfoProvider(props: {
children: React.ReactNode;
value?: EVMContractInfo;
}) {
const [value, setValue] = useState<EVMContractInfo | undefined>(props.value);
// keep the state in sync with the props.value
// this is required when route changes and page goes from fallback to actual page
const _setValue = useCallback((info?: EVMContractInfo) => {
setValue((prevValue) => {
if (
prevValue?.chainSlug === info?.chainSlug &&
prevValue?.contractAddress === info?.contractAddress &&
info?.chain === null
) {
return prevValue;
}
return info;
});
}, []);
// no better way right now - this whole file is going away with RSC migration
// eslint-disable-next-line no-restricted-syntax
useEffect(() => {
_setValue(props.value);
}, [_setValue, props.value]);
// when we move from rendering the fallback page to the actual page, props.value is correct value but state lags behind with undefined value
// in that case we use the props.value instead of providing undefined and update the state with effect afterwards
// this only happens ONCE, when the page is not yet created on server. once it is created and cached. value will always be defined
const providedValue = value || props.value;
return (
<EVMContractInfoContext.Provider value={providedValue}>
<SetEVMContractInfoContext.Provider value={_setValue}>
{props.children}
</SetEVMContractInfoContext.Provider>
</EVMContractInfoContext.Provider>
);
}
export function useEVMContractInfo() {
return useContext(EVMContractInfoContext);
}
export function useSetEVMContractInfo() {
const setter = useContext(SetEVMContractInfoContext);
invariant(
setter,
"useSetEVMContractInfo must be used inside EVMContractInfoProvider",
);
return setter;
}
// for EVM - get network info from context
export function useDashboardEVMChainId() {
const contractInfo = useEVMContractInfo();
return contractInfo?.chain?.chainId;
}
@@ -7,7 +7,6 @@ import {
import { THIRDWEB_ANALYTICS_API_HOST, THIRDWEB_API_HOST } from "constants/urls";
import invariant from "tiny-invariant";
import { accountKeys, apiKeys, authorizedWallets } from "../cache-keys";
import { useMutationWithInvalidate } from "./query/useQueryWithNetwork";
import { useLoggedInUser } from "./useLoggedInUser";
// FIXME: We keep repeating types, API server should provide them
@@ -67,6 +66,7 @@ export type Account = {
paymentFailureCode: string;
serviceCutoffDate: string;
}[];
// TODO - add image URL
};
interface UpdateAccountInput {
@@ -478,8 +478,8 @@ export function useUpdateAccountPlan(waitForWebhook?: boolean) {
const { user } = useLoggedInUser();
const queryClient = useQueryClient();
return useMutationWithInvalidate(
async (input: { plan: string; feedback?: string }) => {
return useMutation({
mutationFn: async (input: { plan: string; feedback?: string }) => {
invariant(user?.address, "walletAddress is required");
const res = await fetch(`${THIRDWEB_API_HOST}/v1/account/plan`, {
@@ -505,27 +505,26 @@ export function useUpdateAccountPlan(waitForWebhook?: boolean) {
return json.data;
},
{
onSuccess: async () => {
onSuccess: async () => {
return Promise.all([
// invalidate usage data as limits are different
await queryClient.invalidateQueries({
queryClient.invalidateQueries({
queryKey: accountKeys.me(user?.address as string),
});
return queryClient.invalidateQueries({
}),
queryClient.invalidateQueries({
queryKey: accountKeys.usage(user?.address as string),
});
},
}),
]);
},
);
});
}
export function useUpdateNotifications() {
const { user } = useLoggedInUser();
const queryClient = useQueryClient();
return useMutationWithInvalidate(
async (input: UpdateAccountNotificationsInput) => {
return useMutation({
mutationFn: async (input: UpdateAccountNotificationsInput) => {
invariant(user?.address, "walletAddress is required");
const res = await fetch(`${THIRDWEB_API_HOST}/v1/account/notifications`, {
@@ -544,14 +543,12 @@ export function useUpdateNotifications() {
return json.data;
},
{
onSuccess: () => {
return queryClient.invalidateQueries({
queryKey: accountKeys.me(user?.address as string),
});
},
onSuccess: () => {
return queryClient.invalidateQueries({
queryKey: accountKeys.me(user?.address as string),
});
},
);
});
}
export function useCreateBillingSession(enabled = false) {
@@ -588,8 +585,8 @@ export function useConfirmEmail() {
const { user } = useLoggedInUser();
const queryClient = useQueryClient();
return useMutationWithInvalidate(
async (input: ConfirmEmailInput) => {
return useMutation({
mutationFn: async (input: ConfirmEmailInput) => {
invariant(user?.address, "walletAddress is required");
const res = await fetch(`${THIRDWEB_API_HOST}/v1/account/confirmEmail`, {
@@ -608,31 +605,29 @@ export function useConfirmEmail() {
return json.data;
},
{
onSuccess: async () => {
// invalidate related cache, since could be relinking account
return Promise.all([
queryClient.invalidateQueries({
queryKey: apiKeys.keys(user?.address as string),
}),
queryClient.invalidateQueries({
queryKey: accountKeys.usage(user?.address as string),
}),
queryClient.invalidateQueries({
queryKey: accountKeys.me(user?.address as string),
}),
]);
},
onSuccess: async () => {
// invalidate related cache, since could be relinking account
return Promise.all([
queryClient.invalidateQueries({
queryKey: apiKeys.keys(user?.address as string),
}),
queryClient.invalidateQueries({
queryKey: accountKeys.usage(user?.address as string),
}),
queryClient.invalidateQueries({
queryKey: accountKeys.me(user?.address as string),
}),
]);
},
);
});
}
export function useResendEmailConfirmation() {
const { user } = useLoggedInUser();
const queryClient = useQueryClient();
return useMutationWithInvalidate(
async () => {
return useMutation({
mutationFn: async () => {
invariant(user?.address, "walletAddress is required");
const res = await fetch(
@@ -654,22 +649,20 @@ export function useResendEmailConfirmation() {
return json.data;
},
{
onSuccess: () => {
return queryClient.invalidateQueries({
queryKey: accountKeys.me(user?.address as string),
});
},
onSuccess: () => {
return queryClient.invalidateQueries({
queryKey: accountKeys.me(user?.address as string),
});
},
);
});
}
export function useCreatePaymentMethod() {
const { user } = useLoggedInUser();
const queryClient = useQueryClient();
return useMutationWithInvalidate(
async (paymentMethodId: string) => {
return useMutation({
mutationFn: async (paymentMethodId: string) => {
invariant(user?.address, "walletAddress is required");
const res = await fetch(`${THIRDWEB_API_HOST}/v1/account/paymentMethod`, {
@@ -690,14 +683,12 @@ export function useCreatePaymentMethod() {
return json.data;
},
{
onSuccess: () => {
return queryClient.invalidateQueries({
queryKey: accountKeys.me(user?.address as string),
});
},
onSuccess: () => {
return queryClient.invalidateQueries({
queryKey: accountKeys.me(user?.address as string),
});
},
);
});
}
export function useApiKeys() {
@@ -760,8 +751,8 @@ export function useUpdateApiKey() {
const { user } = useLoggedInUser();
const queryClient = useQueryClient();
return useMutationWithInvalidate(
async (input: UpdateKeyInput) => {
return useMutation({
mutationFn: async (input: UpdateKeyInput) => {
invariant(user?.address, "walletAddress is required");
const res = await fetch(`${THIRDWEB_API_HOST}/v1/keys/${input.id}`, {
@@ -781,22 +772,20 @@ export function useUpdateApiKey() {
return json.data;
},
{
onSuccess: () => {
return queryClient.invalidateQueries({
queryKey: apiKeys.keys(user?.address as string),
});
},
onSuccess: () => {
return queryClient.invalidateQueries({
queryKey: apiKeys.keys(user?.address as string),
});
},
);
});
}
export function useRevokeApiKey() {
const { user } = useLoggedInUser();
const queryClient = useQueryClient();
return useMutationWithInvalidate(
async (id: string) => {
return useMutation({
mutationFn: async (id: string) => {
invariant(user?.address, "walletAddress is required");
const res = await fetch(`${THIRDWEB_API_HOST}/v1/keys/${id}/revoke`, {
@@ -815,14 +804,12 @@ export function useRevokeApiKey() {
return json.data;
},
{
onSuccess: () => {
return queryClient.invalidateQueries({
queryKey: apiKeys.keys(user?.address as string),
});
},
onSuccess: () => {
return queryClient.invalidateQueries({
queryKey: apiKeys.keys(user?.address as string),
});
},
);
});
}
export const usePolicies = (serviceId?: string) => {
@@ -851,8 +838,11 @@ export const usePolicies = (serviceId?: string) => {
export const useUpdatePolicies = () => {
const queryClient = useQueryClient();
return useMutationWithInvalidate(
async (input: { serviceId: string; data: ApiKeyServicePolicy }) => {
return useMutation({
mutationFn: async (input: {
serviceId: string;
data: ApiKeyServicePolicy;
}) => {
const res = await fetch(`${THIRDWEB_API_HOST}/v1/policies`, {
method: "POST",
@@ -870,22 +860,20 @@ export const useUpdatePolicies = () => {
}
return json.data as ApiKeyServicePolicy;
},
{
onSuccess: (_, variables) => {
return queryClient.invalidateQueries({
queryKey: ["policies", variables.serviceId],
});
},
onSuccess: (_, variables) => {
return queryClient.invalidateQueries({
queryKey: ["policies", variables.serviceId],
});
},
);
});
};
export function useRevokeAuthorizedWallet() {
const { user } = useLoggedInUser();
const queryClient = useQueryClient();
return useMutationWithInvalidate(
async (variables: { authorizedWalletId: string }) => {
return useMutation({
mutationFn: async (variables: { authorizedWalletId: string }) => {
invariant(user?.address, "walletAddress is required");
const { authorizedWalletId } = variables;
@@ -909,16 +897,12 @@ export function useRevokeAuthorizedWallet() {
return json.data;
},
{
onSuccess: () => {
return queryClient.invalidateQueries({
queryKey: authorizedWallets.authorizedWallets(
user?.address as string,
),
});
},
onSuccess: () => {
return queryClient.invalidateQueries({
queryKey: authorizedWallets.authorizedWallets(user?.address as string),
});
},
);
});
}
export function useAuthorizedWallets() {
@@ -14,41 +14,52 @@ import { getContractMetadata, name, symbol } from "thirdweb/extensions/common";
export function useDashboardContractMetadata(contract: ThirdwebContract) {
return useQuery({
queryKey: ["contract-metadata-header", contract.chain.id, contract.address],
queryFn: async () => {
const [contractMetadata, _name, _symbol, compilerMetadata] =
await Promise.all([
getContractMetadata({ contract }).catch(() => ({
image: "",
name: "",
symbol: "",
})),
name({ contract }).catch(() => ""),
symbol({ contract }).catch(() => ""),
getCompilerMetadata(contract).catch(() => undefined),
]);
/**
* Handle the case where the contract.thirdweb.com endpoint returns a very long name
* example url: https://contract.thirdweb.com/metadata/324/0x66E298335c5992d599132582De8eaAF75348e6e6
* In that case, we split the solidity name on ":" and take the second part
*/
if (compilerMetadata?.name.includes(":")) {
const _name = compilerMetadata.name.split(":")[1];
if (_name) {
compilerMetadata.name = compilerMetadata.name.split(":")[1];
}
}
const contractName =
contractMetadata?.name || _name || compilerMetadata?.name || "";
const contractSymbol = contractMetadata?.symbol || _symbol || "";
return {
name: contractName,
symbol: contractSymbol,
image: contractMetadata.image || "",
contractType: compilerMetadata?.name || "",
};
},
queryFn: () => fetchDashboardContractMetadata(contract),
});
}
type DashboardContractMetadata = {
name: string;
symbol: string;
image: string;
contractType: string;
};
export async function fetchDashboardContractMetadata(
contract: ThirdwebContract,
): Promise<DashboardContractMetadata> {
const [contractMetadata, _name, _symbol, compilerMetadata] =
await Promise.all([
getContractMetadata({ contract }).catch(() => ({
image: "",
name: "",
symbol: "",
})),
name({ contract }).catch(() => ""),
symbol({ contract }).catch(() => ""),
getCompilerMetadata(contract).catch(() => undefined),
]);
/**
* Handle the case where the contract.thirdweb.com endpoint returns a very long name
* example url: https://contract.thirdweb.com/metadata/324/0x66E298335c5992d599132582De8eaAF75348e6e6
* In that case, we split the solidity name on ":" and take the second part
*/
if (compilerMetadata?.name.includes(":")) {
const _name = compilerMetadata.name.split(":")[1];
if (_name) {
compilerMetadata.name = compilerMetadata.name.split(":")[1];
}
}
const contractName =
contractMetadata?.name || _name || compilerMetadata?.name || "";
const contractSymbol = contractMetadata?.symbol || _symbol || "";
return {
name: contractName,
symbol: contractSymbol,
image: contractMetadata.image || "",
contractType: compilerMetadata?.name || "",
};
}
@@ -1,3 +1,5 @@
"use client";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import type { ResultItem } from "components/engine/system-metrics/components/StatusCodes";
import { THIRDWEB_API_HOST } from "constants/urls";
@@ -5,7 +7,6 @@ import { useState } from "react";
import { useActiveAccount, useActiveWalletChain } from "thirdweb/react";
import invariant from "tiny-invariant";
import { engineKeys } from "../cache-keys";
import { useMutationWithInvalidate } from "./query/useQueryWithNetwork";
import { useLoggedInUser } from "./useLoggedInUser";
export type EngineTier = "STARTER" | "PREMIUM" | "ENTERPRISE";
@@ -17,7 +18,6 @@ export type EngineInstance = {
name: string;
url: string;
lastAccessedAt: string;
cloudDeployedAt?: string;
status:
| "active"
| "pending"
@@ -25,6 +25,7 @@ export type EngineInstance = {
| "deploying"
| "paymentFailed"
| "deploymentFailed";
deploymentId?: string;
};
// Not checking for null token because the token is required the tanstack useQuery hook
@@ -188,23 +189,26 @@ export function useEngineLatestVersion() {
interface UpdateVersionInput {
engineId: string;
serverVersion: string;
}
export function useEngineUpdateVersion() {
export function useEngineUpdateServerVersion() {
return useMutation({
mutationFn: async (input: UpdateVersionInput) => {
invariant(input.engineId, "engineId is required");
const res = await fetch(`${THIRDWEB_API_HOST}/v1/engine/update-version`, {
method: "POST",
headers: {
"Content-Type": "application/json",
const res = await fetch(
`${THIRDWEB_API_HOST}/v2/engine/${input.engineId}/infrastructure`,
{
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
serverVersion: input.serverVersion,
}),
},
body: JSON.stringify({
engineId: input.engineId,
}),
});
);
// we never use the response body
res.body?.cancel();
if (!res.ok) {
@@ -570,8 +574,8 @@ export function useEngineAddKeypair(instance: string) {
const token = useLoggedInUser().user?.jwt ?? null;
const queryClient = useQueryClient();
return useMutationWithInvalidate(
async (input: AddKeypairInput) => {
return useMutation({
mutationFn: async (input: AddKeypairInput) => {
invariant(instance, "instance is required");
const res = await fetch(`${instance}auth/keypair/add`, {
@@ -587,14 +591,12 @@ export function useEngineAddKeypair(instance: string) {
return json.result;
},
{
onSuccess: () => {
return queryClient.invalidateQueries({
queryKey: engineKeys.keypairs(instance),
});
},
onSuccess: () => {
return queryClient.invalidateQueries({
queryKey: engineKeys.keypairs(instance),
});
},
);
});
}
type RemoveKeypairInput = {
@@ -605,8 +607,8 @@ export function useEngineRemoveKeypair(instance: string) {
const token = useLoggedInUser().user?.jwt ?? null;
const queryClient = useQueryClient();
return useMutationWithInvalidate(
async (input: RemoveKeypairInput) => {
return useMutation({
mutationFn: async (input: RemoveKeypairInput) => {
invariant(instance, "instance is required");
const res = await fetch(`${instance}auth/keypair/remove`, {
@@ -622,14 +624,12 @@ export function useEngineRemoveKeypair(instance: string) {
return json.result;
},
{
onSuccess: () => {
return queryClient.invalidateQueries({
queryKey: engineKeys.keypairs(instance),
});
},
onSuccess: () => {
return queryClient.invalidateQueries({
queryKey: engineKeys.keypairs(instance),
});
},
);
});
}
export type EngineRelayer = {
@@ -672,8 +672,8 @@ export function useEngineCreateRelayer(instance: string) {
const token = useLoggedInUser().user?.jwt ?? null;
const queryClient = useQueryClient();
return useMutationWithInvalidate(
async (input: CreateRelayerInput) => {
return useMutation({
mutationFn: async (input: CreateRelayerInput) => {
invariant(instance, "instance is required");
const res = await fetch(`${instance}relayer/create`, {
@@ -689,14 +689,12 @@ export function useEngineCreateRelayer(instance: string) {
return json.result;
},
{
onSuccess: () => {
return queryClient.invalidateQueries({
queryKey: engineKeys.relayers(instance),
});
},
onSuccess: () => {
return queryClient.invalidateQueries({
queryKey: engineKeys.relayers(instance),
});
},
);
});
}
type RevokeRelayerInput = {
@@ -707,8 +705,8 @@ export function useEngineRevokeRelayer(instance: string) {
const token = useLoggedInUser().user?.jwt ?? null;
const queryClient = useQueryClient();
return useMutationWithInvalidate(
async (input: RevokeRelayerInput) => {
return useMutation({
mutationFn: async (input: RevokeRelayerInput) => {
invariant(instance, "instance is required");
const res = await fetch(`${instance}relayer/revoke`, {
@@ -724,14 +722,12 @@ export function useEngineRevokeRelayer(instance: string) {
return json.result;
},
{
onSuccess: () => {
return queryClient.invalidateQueries({
queryKey: engineKeys.relayers(instance),
});
},
onSuccess: () => {
return queryClient.invalidateQueries({
queryKey: engineKeys.relayers(instance),
});
},
);
});
}
export type UpdateRelayerInput = {
@@ -747,8 +743,8 @@ export function useEngineUpdateRelayer(instance: string) {
const token = useLoggedInUser().user?.jwt ?? null;
const queryClient = useQueryClient();
return useMutationWithInvalidate(
async (input: UpdateRelayerInput) => {
return useMutation({
mutationFn: async (input: UpdateRelayerInput) => {
invariant(instance, "instance is required");
const res = await fetch(`${instance}relayer/update`, {
@@ -764,14 +760,12 @@ export function useEngineUpdateRelayer(instance: string) {
return json.result;
},
{
onSuccess: () => {
return queryClient.invalidateQueries({
queryKey: engineKeys.relayers(instance),
});
},
onSuccess: () => {
return queryClient.invalidateQueries({
queryKey: engineKeys.relayers(instance),
});
},
);
});
}
export interface EngineWebhook {
@@ -827,8 +821,8 @@ export function useEngineSetWalletConfig(instance: string) {
const token = useLoggedInUser().user?.jwt ?? null;
const queryClient = useQueryClient();
return useMutationWithInvalidate(
async (input: SetWalletConfigInput) => {
return useMutation({
mutationFn: async (input: SetWalletConfigInput) => {
invariant(instance, "instance is required");
const res = await fetch(`${instance}configuration/wallets`, {
@@ -844,14 +838,12 @@ export function useEngineSetWalletConfig(instance: string) {
return json.result;
},
{
onSuccess: () => {
return queryClient.invalidateQueries({
queryKey: engineKeys.walletConfig(instance),
});
},
onSuccess: () => {
return queryClient.invalidateQueries({
queryKey: engineKeys.walletConfig(instance),
});
},
);
});
}
export type CreateBackendWalletInput = {
@@ -862,8 +854,8 @@ export function useEngineCreateBackendWallet(instance: string) {
const token = useLoggedInUser().user?.jwt ?? null;
const queryClient = useQueryClient();
return useMutationWithInvalidate(
async (input: CreateBackendWalletInput) => {
return useMutation({
mutationFn: async (input: CreateBackendWalletInput) => {
invariant(instance, "instance is required");
const res = await fetch(`${instance}backend-wallet/create`, {
@@ -879,14 +871,12 @@ export function useEngineCreateBackendWallet(instance: string) {
return json.result;
},
{
onSuccess: () => {
return queryClient.invalidateQueries({
queryKey: engineKeys.backendWallets(instance),
});
},
onSuccess: () => {
return queryClient.invalidateQueries({
queryKey: engineKeys.backendWallets(instance),
});
},
);
});
}
interface UpdateBackendWalletInput {
@@ -898,8 +888,8 @@ export function useEngineUpdateBackendWallet(instance: string) {
const token = useLoggedInUser().user?.jwt ?? null;
const queryClient = useQueryClient();
return useMutationWithInvalidate(
async (input: UpdateBackendWalletInput) => {
return useMutation({
mutationFn: async (input: UpdateBackendWalletInput) => {
invariant(instance, "instance is required");
const res = await fetch(`${instance}backend-wallet/update`, {
@@ -915,14 +905,12 @@ export function useEngineUpdateBackendWallet(instance: string) {
return json.result;
},
{
onSuccess: () => {
return queryClient.invalidateQueries({
queryKey: engineKeys.backendWallets(instance),
});
},
onSuccess: () => {
return queryClient.invalidateQueries({
queryKey: engineKeys.backendWallets(instance),
});
},
);
});
}
export type ImportBackendWalletInput =
@@ -949,8 +937,8 @@ export function useEngineImportBackendWallet(instance: string) {
const token = useLoggedInUser().user?.jwt ?? null;
const queryClient = useQueryClient();
return useMutationWithInvalidate(
async (input: ImportBackendWalletInput) => {
return useMutation({
mutationFn: async (input: ImportBackendWalletInput) => {
invariant(instance, "instance is required");
const res = await fetch(`${instance}backend-wallet/import`, {
@@ -966,22 +954,20 @@ export function useEngineImportBackendWallet(instance: string) {
return json.result;
},
{
onSuccess: () => {
return queryClient.invalidateQueries({
queryKey: engineKeys.backendWallets(instance),
});
},
onSuccess: () => {
return queryClient.invalidateQueries({
queryKey: engineKeys.backendWallets(instance),
});
},
);
});
}
export function useEngineGrantPermissions(instance: string) {
const token = useLoggedInUser().user?.jwt ?? null;
const queryClient = useQueryClient();
return useMutationWithInvalidate(
async (input: EngineAdmin) => {
return useMutation({
mutationFn: async (input: EngineAdmin) => {
invariant(instance, "instance is required");
const res = await fetch(`${instance}auth/permissions/grant`, {
@@ -997,14 +983,12 @@ export function useEngineGrantPermissions(instance: string) {
return json.result;
},
{
onSuccess: () => {
return queryClient.invalidateQueries({
queryKey: engineKeys.permissions(instance),
});
},
onSuccess: () => {
return queryClient.invalidateQueries({
queryKey: engineKeys.permissions(instance),
});
},
);
});
}
type RevokePermissionsInput = {
@@ -1015,8 +999,8 @@ export function useEngineRevokePermissions(instance: string) {
const token = useLoggedInUser().user?.jwt ?? null;
const queryClient = useQueryClient();
return useMutationWithInvalidate(
async (input: RevokePermissionsInput) => {
return useMutation({
mutationFn: async (input: RevokePermissionsInput) => {
invariant(instance, "instance is required");
const res = await fetch(`${instance}auth/permissions/revoke`, {
@@ -1032,14 +1016,12 @@ export function useEngineRevokePermissions(instance: string) {
return json.result;
},
{
onSuccess: () => {
return queryClient.invalidateQueries({
queryKey: engineKeys.permissions(instance),
});
},
onSuccess: () => {
return queryClient.invalidateQueries({
queryKey: engineKeys.permissions(instance),
});
},
);
});
}
type CreateAccessTokenResponse = AccessToken & {
@@ -1050,8 +1032,8 @@ export function useEngineCreateAccessToken(instance: string) {
const token = useLoggedInUser().user?.jwt ?? null;
const queryClient = useQueryClient();
return useMutationWithInvalidate(
async () => {
return useMutation({
mutationFn: async () => {
invariant(instance, "instance is required");
const res = await fetch(`${instance}auth/access-tokens/create`, {
@@ -1067,14 +1049,12 @@ export function useEngineCreateAccessToken(instance: string) {
return json.result as CreateAccessTokenResponse;
},
{
onSuccess: () => {
return queryClient.invalidateQueries({
queryKey: engineKeys.accessTokens(instance),
});
},
onSuccess: () => {
return queryClient.invalidateQueries({
queryKey: engineKeys.accessTokens(instance),
});
},
);
});
}
type RevokeAccessTokenInput = {
@@ -1085,8 +1065,8 @@ export function useEngineRevokeAccessToken(instance: string) {
const token = useLoggedInUser().user?.jwt ?? null;
const queryClient = useQueryClient();
return useMutationWithInvalidate(
async (input: RevokeAccessTokenInput) => {
return useMutation({
mutationFn: async (input: RevokeAccessTokenInput) => {
invariant(instance, "instance is required");
const res = await fetch(`${instance}auth/access-tokens/revoke`, {
@@ -1102,14 +1082,12 @@ export function useEngineRevokeAccessToken(instance: string) {
return json.result;
},
{
onSuccess: () => {
return queryClient.invalidateQueries({
queryKey: engineKeys.accessTokens(instance),
});
},
onSuccess: () => {
return queryClient.invalidateQueries({
queryKey: engineKeys.accessTokens(instance),
});
},
);
});
}
type UpdateAccessTokenInput = {
@@ -1121,8 +1099,8 @@ export function useEngineUpdateAccessToken(instance: string) {
const token = useLoggedInUser().user?.jwt ?? null;
const queryClient = useQueryClient();
return useMutationWithInvalidate(
async (input: UpdateAccessTokenInput) => {
return useMutation({
mutationFn: async (input: UpdateAccessTokenInput) => {
invariant(instance, "instance is required");
const res = await fetch(`${instance}auth/access-tokens/update`, {
@@ -1138,14 +1116,12 @@ export function useEngineUpdateAccessToken(instance: string) {
return json.result;
},
{
onSuccess: () => {
return queryClient.invalidateQueries({
queryKey: engineKeys.accessTokens(instance),
});
},
onSuccess: () => {
return queryClient.invalidateQueries({
queryKey: engineKeys.accessTokens(instance),
});
},
);
});
}
export type CreateWebhookInput = {
@@ -1158,8 +1134,8 @@ export function useEngineCreateWebhook(instance: string) {
const token = useLoggedInUser().user?.jwt ?? null;
const queryClient = useQueryClient();
return useMutationWithInvalidate(
async (input: CreateWebhookInput) => {
return useMutation({
mutationFn: async (input: CreateWebhookInput) => {
invariant(instance, "instance is required");
const res = await fetch(`${instance}webhooks/create`, {
@@ -1175,14 +1151,12 @@ export function useEngineCreateWebhook(instance: string) {
return json.result;
},
{
onSuccess: () => {
return queryClient.invalidateQueries({
queryKey: engineKeys.webhooks(instance),
});
},
onSuccess: () => {
return queryClient.invalidateQueries({
queryKey: engineKeys.webhooks(instance),
});
},
);
});
}
type RevokeWebhookInput = {
@@ -1193,8 +1167,8 @@ export function useEngineRevokeWebhook(instance: string) {
const token = useLoggedInUser().user?.jwt ?? null;
const queryClient = useQueryClient();
return useMutationWithInvalidate(
async (input: RevokeWebhookInput) => {
return useMutation({
mutationFn: async (input: RevokeWebhookInput) => {
invariant(instance, "instance is required");
const res = await fetch(`${instance}webhooks/revoke`, {
@@ -1210,14 +1184,12 @@ export function useEngineRevokeWebhook(instance: string) {
return json.result;
},
{
onSuccess: () => {
return queryClient.invalidateQueries({
queryKey: engineKeys.webhooks(instance),
});
},
onSuccess: () => {
return queryClient.invalidateQueries({
queryKey: engineKeys.webhooks(instance),
});
},
);
});
}
type SendTokensInput = {
@@ -1659,8 +1631,8 @@ export function useEngineCreateNotificationChannel(engineId: string) {
const { isLoggedIn } = useLoggedInUser();
const queryClient = useQueryClient();
return useMutationWithInvalidate(
async (input: CreateNotificationChannelInput) => {
return useMutation({
mutationFn: async (input: CreateNotificationChannelInput) => {
invariant(isLoggedIn, "Must be logged in.");
const res = await fetch(
@@ -1677,22 +1649,20 @@ export function useEngineCreateNotificationChannel(engineId: string) {
const json = await res.json();
return json.data as EngineNotificationChannel;
},
{
onSuccess: () => {
return queryClient.invalidateQueries({
queryKey: engineKeys.notificationChannels(engineId),
});
},
onSuccess: () => {
return queryClient.invalidateQueries({
queryKey: engineKeys.notificationChannels(engineId),
});
},
);
});
}
export function useEngineDeleteNotificationChannel(engineId: string) {
const { isLoggedIn } = useLoggedInUser();
const queryClient = useQueryClient();
return useMutationWithInvalidate(
async (notificationChannelId: string) => {
return useMutation({
mutationFn: async (notificationChannelId: string) => {
invariant(isLoggedIn, "Must be logged in.");
const res = await fetch(
@@ -1704,12 +1674,10 @@ export function useEngineDeleteNotificationChannel(engineId: string) {
}
res.body?.cancel();
},
{
onSuccess: () => {
return queryClient.invalidateQueries({
queryKey: engineKeys.notificationChannels(engineId),
});
},
onSuccess: () => {
return queryClient.invalidateQueries({
queryKey: engineKeys.notificationChannels(engineId),
});
},
);
});
}
@@ -1,3 +1,5 @@
"use client";
import { isLoginRequired } from "@/constants/auth";
import { useDashboardRouter } from "@/lib/DashboardRouter";
import { useQuery } from "@tanstack/react-query";
@@ -1,17 +1,18 @@
import { useQuery } from "@tanstack/react-query";
import type { WalletNFTApiReturn } from "pages/api/wallet/nfts/[chainId]";
import { useQueryWithNetwork } from "./query/useQueryWithNetwork";
export function useWalletNFTs(walletAddress?: string, chainId?: number) {
return useQueryWithNetwork(
["walletNfts", walletAddress],
async () => {
export function useWalletNFTs(params: {
chainId: number;
walletAddress?: string;
}) {
return useQuery({
queryKey: ["walletNfts", params.chainId, params.walletAddress],
queryFn: async () => {
const response = await fetch(
`/api/wallet/nfts/${chainId}?owner=${walletAddress}`,
`/api/wallet/nfts/${params.chainId}?owner=${params.walletAddress}`,
);
return (await response.json()) as WalletNFTApiReturn;
},
{
enabled: !!walletAddress && !!chainId,
},
);
enabled: !!params.walletAddress,
});
}
@@ -0,0 +1,68 @@
"use client";
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";
export function ConfigureCustomChain(props: {
chainSlug: string;
}) {
const { chainSlug } = props;
const isSlugNumber = Number.isInteger(Number.parseInt(chainSlug));
const [isConfigured, setIsConfigured] = useState(false);
if (isConfigured) {
return (
<div className="mx-auto max-w-[600px] py-10">
<Alert variant="info">
<CheckIcon className="size-5" />
<AlertTitle>Network Configured Successfully</AlertTitle>
</Alert>
<div className="h-6" />
<Button
onClick={() => window.location.reload()}
className="w-full gap-2"
>
<RotateCcwIcon className="size-4" />
Reload Page to Continue
</Button>
</div>
);
}
return (
<div className="mx-auto max-w-[600px] py-10">
<Alert variant="warning">
<CircleAlertIcon className="size-5" />
<AlertTitle>
No Chain found with {isSlugNumber ? "chain ID" : "slug"}{" "}
{`"${chainSlug}"`}
</AlertTitle>
<AlertDescription>
Configure a chain with {isSlugNumber ? "chain ID" : "slug"}{" "}
{`"${chainSlug}"`} to continue
</AlertDescription>
</Alert>
<div className="h-6" />
<div className="rounded-lg border border-border">
<ConfigureNetworks
prefillSlug={isSlugNumber ? undefined : chainSlug}
prefillChainId={isSlugNumber ? chainSlug : undefined}
onNetworkAdded={(network) => {
if (
chainSlug === network.slug ||
chainSlug === `${network.chainId}`
) {
addChainOverrides(network);
setIsConfigured(true);
}
}}
editChain={undefined}
/>
</div>
</div>
);
}
@@ -0,0 +1,49 @@
import * as ERC20Ext from "thirdweb/extensions/erc20";
import * as ERC721Ext from "thirdweb/extensions/erc721";
import * as ERC1155Ext from "thirdweb/extensions/erc1155";
export function isERC721ClaimConditionsSupported(functionSelectors: string[]) {
return [
// reads
ERC721Ext.isGetClaimConditionByIdSupported(functionSelectors),
ERC721Ext.isGetActiveClaimConditionIdSupported(functionSelectors),
ERC721Ext.isGetClaimConditionsSupported(functionSelectors),
ERC721Ext.isGetActiveClaimConditionSupported(functionSelectors),
// writes
ERC721Ext.isSetClaimConditionsSupported(functionSelectors),
ERC721Ext.isResetClaimEligibilitySupported(functionSelectors),
].every(Boolean);
}
export function isERC20ClaimConditionsSupported(functionSelectors: string[]) {
return [
// reads
ERC20Ext.isGetClaimConditionByIdSupported(functionSelectors),
ERC20Ext.isGetActiveClaimConditionIdSupported(functionSelectors),
ERC20Ext.isGetClaimConditionsSupported(functionSelectors),
ERC20Ext.isGetActiveClaimConditionSupported(functionSelectors),
// writes
ERC20Ext.isSetClaimConditionsSupported(functionSelectors),
ERC20Ext.isResetClaimEligibilitySupported(functionSelectors),
].every(Boolean);
}
function isERC1155ClaimConditionsSupported(functionSelectors: string[]) {
return [
// reads
ERC1155Ext.isGetClaimConditionByIdSupported(functionSelectors),
ERC1155Ext.isGetClaimConditionsSupported(functionSelectors),
ERC1155Ext.isGetActiveClaimConditionSupported(functionSelectors),
// writes
ERC1155Ext.isSetClaimConditionsSupported(functionSelectors),
ERC1155Ext.isResetClaimEligibilitySupported(functionSelectors),
].every(Boolean);
}
export function isClaimConditionsSupported(functionSelectors: string[]) {
return (
isERC721ClaimConditionsSupported(functionSelectors) ||
isERC20ClaimConditionsSupported(functionSelectors) ||
isERC1155ClaimConditionsSupported(functionSelectors)
);
}
@@ -0,0 +1,26 @@
import * as ERC4337Ext from "thirdweb/extensions/erc4337";
export function isAccountFactoryContract(functionSelectors: string[]) {
return [
ERC4337Ext.isGetAllAccountsSupported(functionSelectors),
ERC4337Ext.isGetAccountsSupported(functionSelectors),
ERC4337Ext.isTotalAccountsSupported(functionSelectors),
ERC4337Ext.isGetAccountsOfSignerSupported(functionSelectors),
ERC4337Ext.isPredictAccountAddressSupported(functionSelectors),
].every(Boolean);
}
export function isAccountContract(functionSelectors: string[]) {
return ERC4337Ext.isValidateUserOpSupported(functionSelectors);
}
export function isAccountPermissionsSupported(functionSelectors: string[]) {
return [
ERC4337Ext.isGetAllActiveSignersSupported(functionSelectors),
ERC4337Ext.isGetAllAdminsSupported(functionSelectors),
ERC4337Ext.isGetAllSignersSupported(functionSelectors),
ERC4337Ext.isIsActiveSignerSupported(functionSelectors),
ERC4337Ext.isIsAdminSupported(functionSelectors),
ERC4337Ext.isAddAdminSupported(functionSelectors),
].every(Boolean);
}
@@ -0,0 +1,17 @@
import * as MarketplaceExt from "thirdweb/extensions/marketplace";
export function isDirectListingSupported(functionSelectors: string[]) {
return [
MarketplaceExt.isCreateListingSupported(functionSelectors),
MarketplaceExt.isCancelListingSupported(functionSelectors),
MarketplaceExt.isBuyFromListingSupported(functionSelectors),
].every(Boolean);
}
export function isEnglishAuctionSupported(functionSelectors: string[]) {
return [
MarketplaceExt.isCreateAuctionSupported(functionSelectors),
MarketplaceExt.isCancelAuctionSupported(functionSelectors),
MarketplaceExt.isBidInAuctionSupported(functionSelectors),
].every(Boolean);
}
@@ -0,0 +1,11 @@
import {
isGetInstalledModulesSupported,
isInstallModuleSupported,
} from "thirdweb/modules";
export function isModularCoreContract(functionSelectors: string[]) {
return [
isGetInstalledModulesSupported(functionSelectors),
isInstallModuleSupported(functionSelectors),
].every(Boolean);
}
@@ -0,0 +1,21 @@
import * as PermissionsExt from "thirdweb/extensions/permissions";
export function isPermissionsSupported(functionSelectors: string[]) {
return [
PermissionsExt.isGetRoleAdminSupported(functionSelectors),
PermissionsExt.isGrantRoleSupported(functionSelectors),
PermissionsExt.isHasRoleSupported(functionSelectors),
PermissionsExt.isRenounceRoleSupported(functionSelectors),
PermissionsExt.isRevokeRoleSupported(functionSelectors),
].every(Boolean);
}
export function isPermissionsEnumerableSupported(functionSelectors: string[]) {
return (
isPermissionsSupported(functionSelectors) &&
[
PermissionsExt.isGetRoleMemberSupported(functionSelectors),
PermissionsExt.isGetRoleMemberCountSupported(functionSelectors),
].every(Boolean)
);
}
@@ -0,0 +1,14 @@
import { isERC20 as isERC20_ERC20 } from "thirdweb/extensions/erc20";
import { isGetNFTSupported as isGetNFTSupported_ERC721 } from "thirdweb/extensions/erc721";
import { isGetNFTSupported as isGetNFTSupported_ERC1155 } from "thirdweb/extensions/erc1155";
export function supportedERCs(functionSelectors: string[]) {
const isERC721 = isGetNFTSupported_ERC721(functionSelectors);
const isERC1155 = isGetNFTSupported_ERC1155(functionSelectors);
const isERC20 = isERC20_ERC20(functionSelectors);
return {
isERC721,
isERC1155,
isERC20,
};
}
@@ -0,0 +1,70 @@
import { getThirdwebClient } from "@/constants/thirdweb.server";
import { mapV4ChainToV5Chain } from "contexts/map-chains";
import { cookies } from "next/headers";
import { TW_LOCAL_CHAIN_STORE } from "stores/storageKeys";
import { getAddress, getContract, isAddress } from "thirdweb";
import type { ChainMetadata } from "thirdweb/chains";
import { fetchChain } from "utils/fetchChain";
import { getAuthToken } from "../../../../../api/lib/getAuthToken";
export async function getContractPageParamsInfo(params: {
contractAddress: string;
chain_id: string;
}) {
const contractAddress = getCheckSummedAddress(params.contractAddress);
const chainSlugOrId = params.chain_id;
let chainMetadata: ChainMetadata | null = null;
try {
chainMetadata = await fetchChain(chainSlugOrId);
} catch {
// move on
}
if (!chainMetadata) {
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;
}
}
} catch {
// noop
}
}
}
if (!chainMetadata) {
return undefined;
}
// attempt to get the auth token
const token = getAuthToken();
const contract = getContract({
address: contractAddress,
// eslint-disable-next-line no-restricted-syntax
chain: mapV4ChainToV5Chain(chainMetadata),
// if we have the auth token pass it into the client
client: getThirdwebClient(token || undefined),
});
return {
chainMetadata,
contract,
};
}
function getCheckSummedAddress(address: string) {
return isAddress(address) ? getAddress(address) : address;
}
@@ -0,0 +1,97 @@
import { resolveFunctionSelectors } from "lib/selectors";
import type { ThirdwebContract } from "thirdweb";
import { contractType as getContractType } from "thirdweb/extensions/thirdweb";
import {
isERC20ClaimConditionsSupported,
isERC721ClaimConditionsSupported,
} from "./detectedFeatures/claimConditions";
import {
isAccountContract,
isAccountFactoryContract,
isAccountPermissionsSupported,
} from "./detectedFeatures/erc4337";
import {
isDirectListingSupported,
isEnglishAuctionSupported,
} from "./detectedFeatures/marketplace";
import { isModularCoreContract } from "./detectedFeatures/modular";
import {
isPermissionsEnumerableSupported,
isPermissionsSupported,
} from "./detectedFeatures/permissions";
import { supportedERCs } from "./detectedFeatures/supportedERCs";
import { type EmbedTypeToShow, getEmbedTypeToShow } from "./getEmbedTypeToShow";
import { isAnalyticsSupportedForChain } from "./isAnalyticsSupportedForChain";
export type ContractPageMetadata = {
supportedERCs: {
isERC721: boolean;
isERC1155: boolean;
isERC20: boolean;
};
isDirectListingSupported: boolean;
isEnglishAuctionSupported: boolean;
isPermissionsSupported: boolean;
isVoteContract: boolean;
isPermissionsEnumerableSupported: boolean;
isModularCore: boolean;
embedType: EmbedTypeToShow;
isAnalyticsSupported: boolean;
isSplitSupported: boolean;
isERC721ClaimConditionsSupported: boolean;
isERC20ClaimConditionsSupported: boolean;
isAccountFactory: boolean;
isAccount: boolean;
isAccountPermissionsSupported: boolean;
functionSelectors: string[];
};
export async function getContractPageMetadata(
contract: ThirdwebContract,
): Promise<ContractPageMetadata> {
const [
functionSelectorsResult,
isAnalyticsSupportedResult,
contractTypeResult,
] = await Promise.allSettled([
resolveFunctionSelectors(contract),
isAnalyticsSupportedForChain(contract.chain.id),
getContractType({ contract }),
]);
const functionSelectors =
functionSelectorsResult.status === "fulfilled"
? functionSelectorsResult.value
: [];
const isAnalyticsSupported =
isAnalyticsSupportedResult.status === "fulfilled"
? isAnalyticsSupportedResult.value
: true;
const contractType =
contractTypeResult.status === "fulfilled" ? contractTypeResult.value : null;
return {
supportedERCs: supportedERCs(functionSelectors),
isDirectListingSupported: isDirectListingSupported(functionSelectors),
isEnglishAuctionSupported: isEnglishAuctionSupported(functionSelectors),
isPermissionsSupported: isPermissionsSupported(functionSelectors),
isPermissionsEnumerableSupported:
isPermissionsEnumerableSupported(functionSelectors),
isModularCore: isModularCoreContract(functionSelectors),
embedType: getEmbedTypeToShow(functionSelectors),
isAnalyticsSupported: isAnalyticsSupported,
isSplitSupported: contractType === "Split",
isVoteContract: contractType === "VoteERC20",
isERC721ClaimConditionsSupported:
isERC721ClaimConditionsSupported(functionSelectors),
isERC20ClaimConditionsSupported:
isERC20ClaimConditionsSupported(functionSelectors),
isAccountFactory: isAccountFactoryContract(functionSelectors),
isAccount: isAccountContract(functionSelectors),
isAccountPermissionsSupported:
isAccountPermissionsSupported(functionSelectors),
functionSelectors,
};
}
@@ -0,0 +1,157 @@
import type { SidebarBaseLink, SidebarLink } from "@/components/blocks/Sidebar";
import type { ContractPageMetadata } from "./getContractPageMetadata";
type SidebarBaseLinkWithHide = SidebarBaseLink & {
hide?: boolean;
};
export function getContractPageSidebarLinks(data: {
metadata: ContractPageMetadata;
contractAddress: string;
chainSlug: string;
}) {
const layoutPrefix = `/${data.chainSlug}/${data.contractAddress}`;
const generalLinks: SidebarBaseLinkWithHide[] = [
{
label: "Overview",
href: layoutPrefix,
exactMatch: true,
},
{
label: "Modules",
href: `${layoutPrefix}/modules`,
hide: !data.metadata.isModularCore,
exactMatch: true,
},
{
label: "Code Snippets",
href: `${layoutPrefix}/code`,
exactMatch: true,
},
{
label: "Explorer",
href: `${layoutPrefix}/explorer`,
exactMatch: true,
},
{
label: "Events",
href: `${layoutPrefix}/events`,
exactMatch: true,
},
{
label: "Embed",
href: `${layoutPrefix}/embed`,
hide: data.metadata.embedType === null,
exactMatch: true,
},
{
label: "Analytics",
href: `${layoutPrefix}/analytics`,
hide: !data.metadata.isAnalyticsSupported,
exactMatch: true,
},
{
label: "Settings",
href: `${layoutPrefix}/settings`,
exactMatch: true,
},
{
label: "Sources",
href: `${layoutPrefix}/sources`,
exactMatch: true,
},
];
const extensionsLinks: SidebarBaseLinkWithHide[] = [
{
label: "NFTs",
href: `${layoutPrefix}/nfts`,
hide:
!data.metadata.supportedERCs.isERC721 &&
!data.metadata.supportedERCs.isERC1155,
},
{
label: "Tokens",
href: `${layoutPrefix}/tokens`,
hide: !data.metadata.supportedERCs.isERC20,
exactMatch: true,
},
{
label: "Direct Listings",
href: `${layoutPrefix}/direct-listings`,
hide: !data.metadata.isDirectListingSupported,
exactMatch: true,
},
{
label: "English Auctions",
href: `${layoutPrefix}/english-auctions`,
hide: !data.metadata.isEnglishAuctionSupported,
exactMatch: true,
},
{
label: "Balances",
href: `${layoutPrefix}/split`,
hide: !data.metadata.isSplitSupported,
exactMatch: true,
},
{
label: "Proposals",
href: `${layoutPrefix}/proposals`,
hide: !data.metadata.isVoteContract,
exactMatch: true,
},
{
label: "Claim Conditions",
href: `${layoutPrefix}/claim-conditions`,
hide: !(
data.metadata.isERC721ClaimConditionsSupported ||
data.metadata.isERC20ClaimConditionsSupported
),
exactMatch: true,
},
{
label: "Accounts",
href: `${layoutPrefix}/accounts`,
hide: !data.metadata.isAccountFactory,
exactMatch: true,
},
{
label: "Balance",
href: `${layoutPrefix}/account`,
hide: !data.metadata.isAccount,
exactMatch: true,
},
{
label: "Account Permissions",
href: `${layoutPrefix}/account-permissions`,
hide: !data.metadata.isAccountPermissionsSupported,
exactMatch: true,
},
{
label: "Permissions",
href: `${layoutPrefix}/permissions`,
exactMatch: true,
hide: !data.metadata.isPermissionsSupported,
},
];
const extensionsToShow = extensionsLinks.filter((l) => !l.hide);
const generalLinksToShow = generalLinks.filter((l) => !l.hide);
const sidebarLinks: SidebarLink[] = [
{
group: "General",
links: generalLinksToShow,
},
];
if (extensionsToShow.length > 0) {
sidebarLinks.push({
group: "Extensions",
links: extensionsToShow,
});
}
return sidebarLinks;
}
@@ -0,0 +1,40 @@
import { isClaimConditionsSupported } from "./detectedFeatures/claimConditions";
import {
isDirectListingSupported,
isEnglishAuctionSupported,
} from "./detectedFeatures/marketplace";
import { supportedERCs } from "./detectedFeatures/supportedERCs";
export type EmbedTypeToShow =
| "marketplace-v3"
| "erc20"
| "erc1155"
| "erc721"
| null;
export function getEmbedTypeToShow(
functionSelectors: string[],
): EmbedTypeToShow {
const { isERC1155, isERC20, isERC721 } = supportedERCs(functionSelectors);
if (
isEnglishAuctionSupported(functionSelectors) &&
isDirectListingSupported(functionSelectors)
) {
return "marketplace-v3";
}
if (isClaimConditionsSupported(functionSelectors)) {
if (isERC721) {
return "erc721";
}
if (isERC1155) {
return "erc1155";
}
if (isERC20) {
return "erc20";
}
}
return null;
}
@@ -0,0 +1,35 @@
import { isProd } from "@/constants/env";
export async function isAnalyticsSupportedForChain(
chainId: number,
): Promise<boolean> {
try {
if (!process.env.CHAINSAW_API_KEY) {
throw new Error("Missing CHAINSAW_API_KEY env var");
}
const res = await fetch(
`https://chainsaw.${isProd ? "thirdweb" : "thirdweb-dev"}.com/service/chains/${chainId}`,
{
method: "GET",
headers: {
"content-type": "application/json",
// pass the shared secret
"x-service-api-key": process.env.CHAINSAW_API_KEY || "",
},
},
);
if (!res.ok) {
// assume not supported if we get a non-200 response
return false;
}
const { data } = await res.json();
return data;
} catch (e) {
console.error("Error checking if analytics is supported for chain", e);
}
return false;
}
@@ -0,0 +1,34 @@
import { notFound, redirect } from "next/navigation";
import { AccountSigners } from "../../../../../../contract-ui/tabs/account-permissions/components/account-signers";
import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
import { getContractPageMetadata } from "../_utils/getContractPageMetadata";
export default async function Page(props: {
params: {
contractAddress: string;
chain_id: string;
};
}) {
const info = await getContractPageParamsInfo(props.params);
if (!info) {
notFound();
}
const { contract } = info;
const { isAccountPermissionsSupported } =
await getContractPageMetadata(contract);
if (!isAccountPermissionsSupported) {
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>
);
}
@@ -0,0 +1,26 @@
import { notFound, redirect } from "next/navigation";
import { AccountPage } from "../../../../../../contract-ui/tabs/account/page";
import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
import { getContractPageMetadata } from "../_utils/getContractPageMetadata";
export default async function Page(props: {
params: {
contractAddress: string;
chain_id: string;
};
}) {
const info = await getContractPageParamsInfo(props.params);
if (!info) {
notFound();
}
const { contract, chainMetadata } = info;
const { isAccount } = await getContractPageMetadata(contract);
if (!isAccount) {
redirect(`/${props.params.chain_id}/${props.params.contractAddress}`);
}
return <AccountPage contract={contract} chainMetadata={chainMetadata} />;
}
@@ -0,0 +1,26 @@
import { notFound, redirect } from "next/navigation";
import { AccountsPage } from "../../../../../../contract-ui/tabs/accounts/page";
import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
import { getContractPageMetadata } from "../_utils/getContractPageMetadata";
export default async function Page(props: {
params: {
contractAddress: string;
chain_id: string;
};
}) {
const info = await getContractPageParamsInfo(props.params);
if (!info) {
notFound();
}
const { contract } = info;
const { isAccountFactory } = await getContractPageMetadata(contract);
if (!isAccountFactory) {
redirect(`/${props.params.chain_id}/${props.params.contractAddress}`);
}
return <AccountsPage contract={contract} />;
}
@@ -0,0 +1,25 @@
import { notFound, redirect } from "next/navigation";
import { ContractAnalyticsPage } from "../../../../../../contract-ui/tabs/analytics/page";
import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
import { getContractPageMetadata } from "../_utils/getContractPageMetadata";
export default async function Page(props: {
params: {
contractAddress: string;
chain_id: string;
};
}) {
const info = await getContractPageParamsInfo(props.params);
if (!info) {
notFound();
}
const { isAnalyticsSupported } = await getContractPageMetadata(info.contract);
if (!isAnalyticsSupported) {
redirect(`/${props.params.chain_id}/${props.params.contractAddress}`);
}
return <ContractAnalyticsPage contract={info.contract} />;
}
@@ -0,0 +1,32 @@
import { notFound, redirect } from "next/navigation";
import { ClaimConditions } from "../../../../../../contract-ui/tabs/claim-conditions/components/claim-conditions";
import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
import { getContractPageMetadata } from "../_utils/getContractPageMetadata";
export default async function Page(props: {
params: {
contractAddress: string;
chain_id: string;
};
}) {
const info = await getContractPageParamsInfo(props.params);
if (!info) {
notFound();
}
const { contract } = info;
const {
isERC20ClaimConditionsSupported,
isERC721ClaimConditionsSupported,
supportedERCs,
} = await getContractPageMetadata(contract);
if (!isERC20ClaimConditionsSupported && !isERC721ClaimConditionsSupported) {
redirect(`/${props.params.chain_id}/${props.params.contractAddress}`);
}
return (
<ClaimConditions contract={contract} isERC20={supportedERCs.isERC20} />
);
}
@@ -0,0 +1,44 @@
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { CircleAlertIcon } from "lucide-react";
import { notFound } from "next/navigation";
import { resolveContractAbi } from "thirdweb/contract";
import { CodeOverview } from "../../../../../../contract-ui/tabs/code/components/code-overview";
import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
export default async function Page(props: {
params: {
contractAddress: string;
chain_id: string;
};
}) {
const info = await getContractPageParamsInfo(props.params);
if (!info) {
notFound();
}
const { contract, chainMetadata } = info;
try {
const abi = await resolveContractAbi(contract);
return (
<CodeOverview
abi={abi}
contractAddress={contract.address}
chainId={contract.chain.id}
/>
);
} 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>
);
}
}
@@ -0,0 +1,27 @@
import { notFound, redirect } from "next/navigation";
import { ContractDirectListingsPage } from "../../../../../../contract-ui/tabs/direct-listings/page";
import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
import { getContractPageMetadata } from "../_utils/getContractPageMetadata";
export default async function Page(props: {
params: {
contractAddress: string;
chain_id: string;
};
}) {
const info = await getContractPageParamsInfo(props.params);
if (!info) {
notFound();
}
const { isDirectListingSupported } = await getContractPageMetadata(
info.contract,
);
if (!isDirectListingSupported) {
redirect(`/${props.params.chain_id}/${props.params.contractAddress}`);
}
return <ContractDirectListingsPage contract={info.contract} />;
}
@@ -0,0 +1,25 @@
import { notFound, redirect } from "next/navigation";
import { EmbedSetup } from "../../../../../../contract-ui/tabs/embed/components/embed-setup";
import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
import { getContractPageMetadata } from "../_utils/getContractPageMetadata";
export default async function Page(props: {
params: {
contractAddress: string;
chain_id: string;
};
}) {
const info = await getContractPageParamsInfo(props.params);
if (!info) {
notFound();
}
const { embedType } = await getContractPageMetadata(info.contract);
if (embedType === null) {
redirect(`/${props.params.chain_id}/${props.params.contractAddress}`);
}
return <EmbedSetup contract={info.contract} ercOrMarketplace={embedType} />;
}
@@ -0,0 +1,27 @@
import { notFound, redirect } from "next/navigation";
import { ContractEnglishAuctionsPage } from "../../../../../../contract-ui/tabs/english-auctions/page";
import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
import { getContractPageMetadata } from "../_utils/getContractPageMetadata";
export default async function Page(props: {
params: {
contractAddress: string;
chain_id: string;
};
}) {
const info = await getContractPageParamsInfo(props.params);
if (!info) {
notFound();
}
const { isEnglishAuctionSupported } = await getContractPageMetadata(
info.contract,
);
if (!isEnglishAuctionSupported) {
redirect(`/${props.params.chain_id}/${props.params.contractAddress}`);
}
return <ContractEnglishAuctionsPage contract={info.contract} />;
}
@@ -0,0 +1,18 @@
import { notFound } from "next/navigation";
import { EventsFeed } from "../../../../../../contract-ui/tabs/events/components/events-feed";
import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
export default async function Page(props: {
params: {
contractAddress: string;
chain_id: string;
};
}) {
const info = await getContractPageParamsInfo(props.params);
if (!info) {
notFound();
}
return <EventsFeed contract={info.contract} />;
}
@@ -0,0 +1,37 @@
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { CircleAlertIcon } from "lucide-react";
import { notFound } from "next/navigation";
import { resolveContractAbi } from "thirdweb/contract";
import { ContractExplorerPage } from "../../../../../../contract-ui/tabs/explorer/page";
import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
export default async function Page(props: {
params: {
contractAddress: string;
chain_id: string;
};
}) {
const info = await getContractPageParamsInfo(props.params);
if (!info) {
notFound();
}
const { contract, chainMetadata } = info;
try {
const abi = await resolveContractAbi(contract);
return <ContractExplorerPage contract={contract} abi={abi} />;
} 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>
);
}
}
@@ -0,0 +1,122 @@
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 { PrimaryDashboardButton } from "contract-ui/components/primary-dashboard-button";
import type { Metadata } from "next";
import { getContractMetadata } from "thirdweb/extensions/common";
import { resolveFunctionSelectors } from "../../../../../lib/selectors";
import { shortenIfAddress } from "../../../../../utils/usedapp-external";
import { ConfigureCustomChain } from "./ConfigureCustomChain";
import { supportedERCs } from "./_utils/detectedFeatures/supportedERCs";
import { getContractPageParamsInfo } from "./_utils/getContractFromParams";
import { getContractPageMetadata } from "./_utils/getContractPageMetadata";
import { getContractPageSidebarLinks } from "./_utils/getContractPageSidebarLinks";
export default async function Layout(props: {
params: {
contractAddress: string;
chain_id: string;
};
children: React.ReactNode;
}) {
const info = await getContractPageParamsInfo(props.params);
if (!info) {
return <ConfigureCustomChain chainSlug={props.params.chain_id} />;
}
const { contract, chainMetadata } = info;
const contractPageMetadata = await getContractPageMetadata(contract);
const sidebarLinks = getContractPageSidebarLinks({
chainSlug: chainMetadata.slug,
contractAddress: contract.address,
metadata: contractPageMetadata,
});
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>
);
}
export async function generateMetadata({
params,
}: {
params: {
contractAddress: string;
chain_id: string;
};
}): Promise<Metadata> {
try {
const info = await getContractPageParamsInfo(params);
if (!info) {
throw new Error();
}
const [functionSelectors, contractMetadata] = await Promise.all([
resolveFunctionSelectors(info.contract),
getContractMetadata({
contract: info.contract,
}),
]);
if (contractMetadata.name === null) {
throw new Error();
}
const { isERC1155, isERC20, isERC721 } = supportedERCs(functionSelectors);
const contractDisplayName = `${contractMetadata.name}${
contractMetadata.symbol ? ` (${contractMetadata.symbol})` : ""
}`;
const cleanedChainName = info?.chainMetadata?.name
.replace("Mainnet", "")
.replace("Testnet", "")
.trim();
const title = `${contractDisplayName} | ${cleanedChainName} Smart Contract`;
let description = "";
if (isERC721 || isERC1155) {
description = `View tokens, source code, transactions, balances, and analytics for the ${contractDisplayName} smart contract on ${cleanedChainName}.`;
} else if (isERC20) {
description = `View ERC20 tokens, transactions, balances, source code, and analytics for the ${contractDisplayName} smart contract on ${cleanedChainName}`;
} else {
description = `View tokens, transactions, balances, source code, and analytics for the ${contractDisplayName} smart contract on ${cleanedChainName}`;
}
return {
title: title,
description: description,
};
} catch {
return {
title: `${shortenIfAddress(params.contractAddress)} | ${params.chain_id}`,
description: `View tokens, transactions, balances, source code, and analytics for the smart contract on Chain ID ${params.chain_id}`,
};
}
}
@@ -0,0 +1,25 @@
import { notFound, redirect } from "next/navigation";
import { ContractEditModulesPage } from "../../../../../../contract-ui/tabs/manage/page";
import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
import { getContractPageMetadata } from "../_utils/getContractPageMetadata";
export default async function Page(props: {
params: {
contractAddress: string;
chain_id: string;
};
}) {
const info = await getContractPageParamsInfo(props.params);
if (!info) {
notFound();
}
const { isModularCore } = await getContractPageMetadata(info.contract);
if (!isModularCore) {
redirect(`/${props.params.chain_id}/${props.params.contractAddress}`);
}
return <ContractEditModulesPage contract={info.contract} />;
}
@@ -0,0 +1,42 @@
import { notFound, redirect } from "next/navigation";
import { TokenIdPage } from "../../../../../../../contract-ui/tabs/nfts/components/token-id";
import { getContractPageParamsInfo } from "../../_utils/getContractFromParams";
import { getContractPageMetadata } from "../../_utils/getContractPageMetadata";
export default async function Page(props: {
params: {
contractAddress: string;
chain_id: string;
tokenId: string;
};
}) {
const info = await getContractPageParamsInfo(props.params);
if (!info) {
notFound();
}
const { contract } = info;
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}
isErc721={supportedERCs.isERC721}
tokenId={props.params.tokenId}
/>
);
}
function isOnlyNumbers(str: string) {
return /^\d+$/.test(str);
}
@@ -0,0 +1,35 @@
import { notFound, redirect } from "next/navigation";
import { ContractNFTPage } from "../../../../../../contract-ui/tabs/nfts/page";
import { resolveFunctionSelectors } from "../../../../../../lib/selectors";
import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
import { getContractPageMetadata } from "../_utils/getContractPageMetadata";
export default async function Page(props: {
params: {
contractAddress: string;
chain_id: string;
};
}) {
const info = await getContractPageParamsInfo(props.params);
if (!info) {
notFound();
}
const { contract } = info;
const { supportedERCs } = 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}
isErc721={supportedERCs.isERC721}
functionSelectors={functionSelectors}
/>
);
}
@@ -1,27 +1,60 @@
/* eslint-disable @next/next/no-img-element */
import { isProd } from "@/constants/env";
import { ImageResponse } from "@vercel/og";
import type { NextRequest } from "next/server";
import { ContractOG } from "og-lib/url-utils";
import { getContractMetadata } from "thirdweb/extensions/common";
import { getContractPageParamsInfo } from "./_utils/getContractFromParams";
export function shortenString(str: string) {
return `${str.substring(0, 7)}...${str.substring(str.length - 5)}`;
}
export const runtime = "edge";
// Make sure the font exists in the specified path:
export const config = {
runtime: "edge",
export const size = {
width: 1200,
height: 630,
};
const inter400_ = fetch(
new URL("og-lib/fonts/inter/400.ttf", import.meta.url),
).then((res) => res.arrayBuffer());
const inter500_ = fetch(
new URL("og-lib/fonts/inter/500.ttf", import.meta.url),
).then((res) => res.arrayBuffer());
const inter700_ = fetch(
new URL("og-lib/fonts/inter/700.ttf", import.meta.url),
).then((res) => res.arrayBuffer());
export default async function Image({
params,
}: {
params: {
contractAddress: string;
chain_id: string;
};
}) {
try {
const info = await getContractPageParamsInfo(params);
if (!info) {
throw new Error();
}
const contractMetadata = await getContractMetadata({
contract: info.contract,
});
if (contractMetadata.name === null) {
throw new Error();
}
const contractDisplayName = `${contractMetadata.name}${
contractMetadata.symbol ? ` (${contractMetadata.symbol})` : ""
}`;
return contractOGImageTemplate({
displayName: contractDisplayName,
contractAddress: info.contract.address,
chainName: info.chainMetadata.name,
logo: contractMetadata.image,
});
} catch {
return contractOGImageTemplate({
contractAddress: params.contractAddress,
chainName: params.chain_id,
});
}
}
function shortenString(str: string) {
return `${str.substring(0, 7)}...${str.substring(str.length - 5)}`;
}
const OgBrandIcon: React.FC = () => (
// biome-ignore lint/a11y/noSvgWithoutTitle: not needed
@@ -60,9 +93,8 @@ const OgBrandIcon: React.FC = () => (
</svg>
);
const MAX_LENGTH = 50;
function textShortener(text: string) {
const MAX_LENGTH = 50;
const words = [];
let currentLength = 0;
let shortened = false;
@@ -108,12 +140,21 @@ function replaceAnyIpfsUrlWithGateway(url: string) {
return url;
}
export default async function handler(req: NextRequest) {
if (req.method !== "GET") {
return new Response("Method not allowed", { status: 405 });
}
const contractData = ContractOG.fromUrl(new URL(req.url));
async function contractOGImageTemplate(params: {
logo?: string;
displayName?: string;
contractAddress: string;
chainName: string;
}) {
const inter400_ = fetch(
new URL("../../../../../og-lib/fonts/inter/400.ttf", import.meta.url),
).then((res) => res.arrayBuffer());
const inter500_ = fetch(
new URL("../../../../../og-lib/fonts/inter/500.ttf", import.meta.url),
).then((res) => res.arrayBuffer());
const inter700_ = fetch(
new URL("../../../../../og-lib/fonts/inter/700.ttf", import.meta.url),
).then((res) => res.arrayBuffer());
const [inter400, inter500, inter700] = await Promise.all([
inter400_,
@@ -121,6 +162,10 @@ export default async function handler(req: NextRequest) {
inter700_,
]);
const title =
(params.displayName ? textShortener(params.displayName) : "") ||
shortenString(params.contractAddress);
return new ImageResponse(
<div
tw="w-full h-full flex justify-center py-8 px-8 "
@@ -133,31 +178,33 @@ export default async function handler(req: NextRequest) {
<div tw="flex flex-col my-auto w-full">
{/* if we have a logo render it */}
{contractData.logo && (
{params.logo && (
<img
alt=""
tw="w-50 h-50 rounded-lg mb-8 flex mx-auto"
src={replaceAnyIpfsUrlWithGateway(contractData.logo)}
src={replaceAnyIpfsUrlWithGateway(params.logo)}
/>
)}
{contractData.displayName &&
contractData.displayName !== contractData.contractAddress ? (
{params.displayName && params.displayName !== params.contractAddress ? (
<h3
tw="text-2xl font-bold flex text-center my-0 mb-4 mx-auto"
style={{
color: "#626262",
}}
>
{shortenString(contractData.contractAddress)}
{shortenString(params.contractAddress)}
</h3>
) : null}
<h1 tw="text-6xl text-white font-bold flex text-center my-0 mx-auto">
{textShortener(contractData.displayName) ||
shortenString(contractData.contractAddress)}
</h1>
{title && (
<h1 tw="text-6xl text-white font-bold flex text-center my-0 mx-auto">
{title}
</h1>
)}
<h2 tw="text-4xl text-white opacity-80 font-medium flex text-center my-0 mt-10 mx-auto">
{contractData.chainName}
{params.chainName}
</h2>
</div>
@@ -0,0 +1,36 @@
import { ContractOverviewPage } from "contract-ui/tabs/overview/page";
import { notFound } from "next/navigation";
import { getContractPageParamsInfo } from "./_utils/getContractFromParams";
import { getContractPageMetadata } from "./_utils/getContractPageMetadata";
export default async function Page(props: {
params: {
contractAddress: string;
chain_id: string;
};
}) {
const info = await getContractPageParamsInfo(props.params);
if (!info) {
notFound();
}
const { contract, chainMetadata } = info;
const contractPageMetadata = await getContractPageMetadata(contract);
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}
/>
);
}
@@ -0,0 +1,29 @@
import { notFound } from "next/navigation";
import { ContractPermissionsPage } from "../../../../../../contract-ui/tabs/permissions/page";
import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
import { getContractPageMetadata } from "../_utils/getContractPageMetadata";
export default async function Page(props: {
params: {
contractAddress: string;
chain_id: string;
};
}) {
const info = await getContractPageParamsInfo(props.params);
if (!info) {
notFound();
}
const { contract } = info;
const { isPermissionsEnumerableSupported } =
await getContractPageMetadata(contract);
return (
<ContractPermissionsPage
contract={contract}
chainSlug={info.chainMetadata.slug}
detectedPermissionEnumerable={isPermissionsEnumerableSupported}
/>
);
}
@@ -0,0 +1,25 @@
import { notFound, redirect } from "next/navigation";
import { ContractProposalsPage } from "../../../../../../contract-ui/tabs/proposals/page";
import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
import { getContractPageMetadata } from "../_utils/getContractPageMetadata";
export default async function Page(props: {
params: {
contractAddress: string;
chain_id: string;
};
}) {
const info = await getContractPageParamsInfo(props.params);
if (!info) {
notFound();
}
const { isVoteContract } = await getContractPageMetadata(info.contract);
if (!isVoteContract) {
redirect(`/${props.params.chain_id}/${props.params.contractAddress}`);
}
return <ContractProposalsPage contract={info.contract} />;
}
@@ -0,0 +1,42 @@
import { notFound } from "next/navigation";
import * as CommonExt from "thirdweb/extensions/common";
import { ContractSettingsPage } from "../../../../../../contract-ui/tabs/settings/page";
import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
import { getContractPageMetadata } from "../_utils/getContractPageMetadata";
export default async function Page(props: {
params: {
contractAddress: string;
chain_id: string;
};
}) {
const info = await getContractPageParamsInfo(props.params);
if (!info) {
notFound();
}
const { functionSelectors } = await getContractPageMetadata(info.contract);
return (
<ContractSettingsPage
contract={info.contract}
isContractMetadataSupported={[
CommonExt.isGetContractMetadataSupported(functionSelectors),
CommonExt.isSetContractMetadataSupported(functionSelectors),
].every(Boolean)}
isPrimarySaleSupported={[
CommonExt.isPrimarySaleRecipientSupported(functionSelectors),
CommonExt.isSetPrimarySaleRecipientSupported(functionSelectors),
].every(Boolean)}
isRoyaltiesSupported={[
CommonExt.isGetDefaultRoyaltyInfoSupported(functionSelectors),
CommonExt.isSetDefaultRoyaltyInfoSupported(functionSelectors),
].every(Boolean)}
isPlatformFeesSupported={[
CommonExt.isGetPlatformFeeInfoSupported(functionSelectors),
CommonExt.isSetPlatformFeeInfoSupported(functionSelectors),
].every(Boolean)}
/>
);
}
@@ -0,0 +1,18 @@
import { notFound } from "next/navigation";
import { ContractSourcesPage } from "../../../../../../contract-ui/tabs/sources/page";
import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
export default async function Page(props: {
params: {
contractAddress: string;
chain_id: string;
};
}) {
const info = await getContractPageParamsInfo(props.params);
if (!info) {
notFound();
}
return <ContractSourcesPage contract={info.contract} />;
}
@@ -0,0 +1,25 @@
import { notFound, redirect } from "next/navigation";
import { ContractSplitPage } from "../../../../../../contract-ui/tabs/split/page";
import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
import { getContractPageMetadata } from "../_utils/getContractPageMetadata";
export default async function Page(props: {
params: {
contractAddress: string;
chain_id: string;
};
}) {
const info = await getContractPageParamsInfo(props.params);
if (!info) {
notFound();
}
const { isSplitSupported } = await getContractPageMetadata(info.contract);
if (!isSplitSupported) {
redirect(`/${props.params.chain_id}/${props.params.contractAddress}`);
}
return <ContractSplitPage contract={info.contract} />;
}
@@ -0,0 +1,34 @@
import { notFound } from "next/navigation";
import {
isClaimToSupported,
isMintToSupported,
} from "thirdweb/extensions/erc20";
import { ContractTokensPage } from "../../../../../../contract-ui/tabs/tokens/page";
import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
import { getContractPageMetadata } from "../_utils/getContractPageMetadata";
export default async function Page(props: {
params: {
contractAddress: string;
chain_id: string;
};
}) {
const info = await getContractPageParamsInfo(props.params);
if (!info) {
notFound();
}
const { supportedERCs, functionSelectors } = await getContractPageMetadata(
info.contract,
);
return (
<ContractTokensPage
contract={info.contract}
isERC20={supportedERCs.isERC20}
isMintToSupported={isMintToSupported(functionSelectors)}
isClaimToSupported={isClaimToSupported(functionSelectors)}
/>
);
}
@@ -37,6 +37,7 @@ import zytronCTA from "./temp-assets/zytronCTA.jpg";
// END TEMPORARY
import { API_SERVER_URL } from "@/constants/env";
import type { ChainMetadata } from "thirdweb/chains";
import type {
ChainMetadataWithServices,
@@ -44,18 +45,15 @@ import type {
ChainServices,
} from "./types/chain";
const THIRDWEB_API_HOST =
process.env.NEXT_PUBLIC_THIRDWEB_API_HOST || "https://api.thirdweb.com";
export async function getChains() {
const [chains, chainServices] = await Promise.all([
fetch(
`${THIRDWEB_API_HOST}/v1/chains`,
`${API_SERVER_URL}/v1/chains`,
// revalidate every 60 minutes
{ next: { revalidate: 60 * 60 } },
).then((res) => res.json()) as Promise<{ data: ChainMetadata[] }>,
fetch(
`${THIRDWEB_API_HOST}/v1/chains/services`,
`${API_SERVER_URL}/v1/chains/services`,
// revalidate every 60 minutes
{ next: { revalidate: 60 * 60 } },
).then((res) => res.json()) as Promise<{
@@ -77,12 +75,12 @@ export async function getChain(
): Promise<ChainMetadataWithServices> {
const [chain, chainServices] = await Promise.all([
fetch(
`${THIRDWEB_API_HOST}/v1/chains/${chainIdOrSlug}`,
`${API_SERVER_URL}/v1/chains/${chainIdOrSlug}`,
// revalidate every 15 minutes
{ next: { revalidate: 15 * 60 } },
).then((res) => res.json()) as Promise<{ data: ChainMetadata }>,
fetch(
`${THIRDWEB_API_HOST}/v1/chains/${chainIdOrSlug}/services`,
`${API_SERVER_URL}/v1/chains/${chainIdOrSlug}/services`,
// revalidate every 15 minutes
{ next: { revalidate: 15 * 60 } },
).then((res) => res.json()) as Promise<{ data: ChainServices }>,
@@ -237,6 +235,18 @@ const chainMetaRecord = {
buttonText: "Learn more",
},
},
// Superposition Mainnet
55244: {
headerImgUrl: superpositionBanner.src,
about:
"Superposition is the first blockchain that pays you to use it. It is a DeFi native Layer-3 that focuses on novel incentives and order-flow for growth and value capture for developers and users alike.\n\nSuperpositions includes a native on-chain orderbook with faster execution speeds through Stylus, providing shared and permissionless liquidity for all apps onchain, and Super Assets, which pay yield when you both hold and use them.",
cta: {
backgroundImageUrl: superpositionCTA.src,
title: "The Blockchain that pays you to use it",
buttonLink: "https://superposition.so/",
buttonText: "Learn more",
},
},
// Aleph 0 Mainnet
41455: {
headerImgUrl: alephZeroBaner.src,
@@ -1,14 +1,12 @@
import { API_SERVER_URL } from "@/constants/env";
import type { Ecosystem } from "../types";
export async function fetchEcosystem(slug: string, authToken: string) {
const res = await fetch(
`${process.env.NEXT_PUBLIC_THIRDWEB_API_HOST || "https://api.thirdweb.com"}/v1/ecosystem-wallet/${slug}`,
{
headers: {
Authorization: `Bearer ${authToken}`,
},
const res = await fetch(`${API_SERVER_URL}/v1/ecosystem-wallet/${slug}`, {
headers: {
Authorization: `Bearer ${authToken}`,
},
);
});
if (!res.ok) {
const data = await res.json();
console.error(data);
@@ -1,14 +1,12 @@
import { API_SERVER_URL } from "@/constants/env";
import type { Ecosystem } from "../types";
export async function fetchEcosystemList(authToken: string) {
const res = await fetch(
`${process.env.NEXT_PUBLIC_THIRDWEB_API_HOST || "https://api.thirdweb.com"}/v1/ecosystem-wallet/list`,
{
headers: {
Authorization: `Bearer ${authToken}`,
},
const res = await fetch(`${API_SERVER_URL}/v1/ecosystem-wallet/list`, {
headers: {
Authorization: `Bearer ${authToken}`,
},
);
});
if (!res.ok) {
const data = await res.json();
@@ -1,7 +1,13 @@
import { BillingAlerts } from "../../../../components/settings/Account/Billing/alerts/Alert";
import { ConnectSidebarLayout } from "./DashboardConnectLayout";
export default function Layout(props: {
children: React.ReactNode;
}) {
return <ConnectSidebarLayout>{props.children}</ConnectSidebarLayout>;
return (
<ConnectSidebarLayout>
<BillingAlerts className="pb-6" />
{props.children}
</ConnectSidebarLayout>
);
}
@@ -1,6 +1,6 @@
import { AppFooter } from "@/components/blocks/app-footer";
import { DashboardHeader } from "app/components/Header/DashboardHeader";
import { ErrorProvider } from "../../contexts/error-handler";
import { DashboardHeader } from "../components/Header/DashboardHeader";
export default function DashboardLayout(props: { children: React.ReactNode }) {
return (
@@ -25,6 +25,13 @@ export async function generateMetadata({ params }: { params: Params }) {
const publishedContract = publishedContracts[0];
if (!publishedContract) {
return {
title: `${contract_id} | Published Smart Contract`,
description: `Deploy ${contract_id} Smart Contract in one click with thirdweb.`,
};
}
const publishedContractName =
publishedContract?.displayName || publishedContract?.name;
@@ -1,5 +1,5 @@
import { ChakraProviderSetup } from "@/components/ChakraProviderSetup";
import { Separator } from "@/components/ui/separator";
import { ChakraProviderSetup } from "../../../../../@/components/ChakraProviderSetup";
import { PublishedContract } from "../../../../../components/contract-components/published-contract";
import { PublishedActions } from "../../components/contract-actions-published.client";
import { DeployContractHeader } from "../../components/contract-header";
@@ -2,6 +2,7 @@
import "server-only";
import { COOKIE_ACTIVE_ACCOUNT, COOKIE_PREFIX_TOKEN } from "@/constants/cookie";
import { API_SERVER_URL } from "@/constants/env";
import { cookies } from "next/headers";
import { redirect } from "next/navigation";
@@ -11,8 +12,6 @@ type State = {
};
const UNTHREAD_API_KEY = process.env.UNTHREAD_API_KEY || "";
const THIRDWEB_API_HOST =
process.env.NEXT_PUBLIC_THIRDWEB_API_HOST || "https://api.thirdweb.com";
const planToCustomerId = {
free: process.env.UNTHREAD_FREE_TIER_ID as string,
@@ -77,7 +76,7 @@ export async function createTicketAction(
// user is not logged in, make them log in
redirect(`/login?next=${encodeURIComponent("/support")}`);
}
const accountRes = await fetch(`${THIRDWEB_API_HOST}/v1/account/me`, {
const accountRes = await fetch(`${API_SERVER_URL}/v1/account/me`, {
method: "GET",
headers: {
Authorization: `Bearer ${token}`,
@@ -6,11 +6,11 @@ import {
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { DiscordIcon } from "components/icons/brand-icons/DiscordIcon";
import { BookOpenIcon, ChevronRightIcon } from "lucide-react";
import type { Metadata } from "next";
import Image from "next/image";
import Link from "next/link";
import { SiDiscord } from "react-icons/si";
import accountIcon from "../../../../public/assets/support/account.svg";
import contractsIcon from "../../../../public/assets/support/contracts.png";
import discordIllustration from "../../../../public/assets/support/discord-illustration.png";
@@ -213,7 +213,7 @@ export default function SupportPage() {
asChild
>
<Link href="https://discord.gg/thirdweb">
<SiDiscord />
<DiscordIcon className="size-4" />
<span>Join Discord</span>
</Link>
</Button>
@@ -35,10 +35,10 @@ export function AccountHeader(props: {
const headerProps: AccountHeaderCompProps = {
teamsAndProjects: props.teamsAndProjects,
email: myAccountQuery.data?.email,
logout: logout,
connectButton: <CustomConnectWallet />,
createProject: () => setIsCreateProjectDialogOpen(true),
account: myAccountQuery.data,
};
return (
@@ -45,13 +45,26 @@ function Variants(props: {
return (
<ThirdwebProvider>
<div className="flex min-h-screen flex-col gap-6 bg-gray-700 px-4 py-10">
<BadgeContainer label="Logged in">
<BadgeContainer label="Account Loaded">
<Comp
teamsAndProjects={teamsAndProjectsStub}
email="[email protected]"
logout={() => {}}
connectButton={<ConnectButtonStub />}
createProject={() => {}}
account={{
id: "foo",
email: "[email protected]",
}}
/>
</BadgeContainer>
<BadgeContainer label="Account Loading">
<Comp
teamsAndProjects={teamsAndProjectsStub}
logout={() => {}}
connectButton={<ConnectButtonStub />}
createProject={() => {}}
account={undefined}
/>
</BadgeContainer>
</div>
@@ -1,6 +1,8 @@
import type { Project } from "@/api/projects";
import type { Team } from "@/api/team";
import { GradientAvatar } from "@/components/blocks/Avatars/GradientAvatar";
import { cn } from "@/lib/utils";
import type { Account } from "@3rdweb-sdk/react/hooks/useApi";
import Link from "next/link";
import { SecondaryNav } from "../../components/Header/SecondaryNav/SecondaryNav";
import { MobileBurgerMenuButton } from "../../components/MobileBurgerMenuButton";
@@ -10,11 +12,11 @@ import { TeamSelectorMobileMenuButton } from "../../team/components/TeamHeader/T
export type AccountHeaderCompProps = {
className?: string;
email: string | undefined;
logout: () => void;
connectButton: React.ReactNode;
teamsAndProjects: Array<{ team: Team; projects: Project[] }>;
createProject: () => void;
account: Pick<Account, "email" | "id"> | undefined;
};
export function AccountHeaderDesktopUI(props: AccountHeaderCompProps) {
@@ -37,8 +39,12 @@ export function AccountHeaderDesktopUI(props: AccountHeaderCompProps) {
href="/account"
className="flex flex-row items-center gap-2 font-normal text-sm"
>
{/* TODO - replace with account image */}
<div className="size-6 rounded-full border border-border bg-muted" />
{/* TODO - set account Image */}
<GradientAvatar
id={props.account?.id}
src={props.account ? "" : undefined}
className="size-6"
/>
<span> My Account </span>
</Link>
@@ -48,12 +54,13 @@ export function AccountHeaderDesktopUI(props: AccountHeaderCompProps) {
teamsAndProjects={props.teamsAndProjects}
focus="team-selection"
createProject={props.createProject}
account={props.account}
/>
</div>
</div>
<SecondaryNav
email={props.email}
account={props.account}
logout={props.logout}
connectButton={props.connectButton}
/>
@@ -77,8 +84,12 @@ export function AccountHeaderMobileUI(props: AccountHeaderCompProps) {
"flex flex-row items-center gap-2 font-normal text-foreground text-sm",
)}
>
{/* TODO - replace with account image */}
<div className="size-7 rounded-full border border-border bg-muted" />
{/* TODO - set account image */}
<GradientAvatar
id={props.account?.id}
src={props.account ? "" : undefined}
className="size-6"
/>
<span> My Account </span>
</Link>
@@ -86,12 +97,13 @@ export function AccountHeaderMobileUI(props: AccountHeaderCompProps) {
currentTeam={undefined}
teamsAndProjects={props.teamsAndProjects}
upgradeTeamLink={undefined}
account={props.account}
/>
</div>
</div>
<MobileBurgerMenuButton
email={props.email}
email={props.account?.email}
logout={props.logout}
connectButton={props.connectButton}
/>
+1 -1
View File
@@ -10,7 +10,7 @@ export default async function AccountLayout(props: {
children: React.ReactNode;
}) {
return (
<div className="flex min-h-full flex-col bg-background">
<div className="flex min-h-screen flex-col bg-background">
<div className="flex grow flex-col">
<HeaderAndNav />
<div className="border-border border-b py-10">
@@ -2,6 +2,7 @@
import type { Team } from "@/api/team";
import type { TeamAccountRole } from "@/api/team-members";
import { GradientAvatar } from "@/components/blocks/Avatars/GradientAvatar";
import { Button } from "@/components/ui/button";
import {
DropdownMenu,
@@ -102,8 +103,8 @@ function TeamRow(props: {
<div className="flex items-center justify-between gap-2">
{/* start */}
<div className="flex items-center gap-4">
{/* TODO - render team avatar */}
<div className="size-8 rounded-full border bg-muted" />
{/* TODO - set image */}
<GradientAvatar className="size-8" src={""} id={props.team.id} />
<div>
<div className="flex items-center gap-3">
@@ -0,0 +1,33 @@
"use client";
import type { Account } from "@3rdweb-sdk/react/hooks/useApi";
import type { ThirdwebClient } from "thirdweb";
import { upload } from "thirdweb/storage";
import { AccountSettingsPageUI } from "./AccountSettingsPageUI";
export function AccountSettingsPage(props: {
account: Account;
client: ThirdwebClient;
}) {
return (
<AccountSettingsPageUI
account={props.account}
updateAccountImage={async (file) => {
if (file) {
// upload to IPFS
const ipfsUri = await upload({
client: props.client,
files: [file],
});
// TODO - Implement updating the account image with uri
console.log(ipfsUri);
} else {
// TODO - Implement deleting the account image
}
throw new Error("Not implemented");
}}
/>
);
}
@@ -41,6 +41,9 @@ function Variants() {
name: "John Doe",
email: "[email protected]",
}}
updateAccountImage={async () => {
await new Promise((resolve) => setTimeout(resolve, 1000));
}}
/>
</div>
<Toaster richColors />
@@ -42,10 +42,11 @@ type MinimalAccount = Pick<
export function AccountSettingsPageUI(props: {
account: MinimalAccount;
updateAccountImage: (file: File | undefined) => Promise<void>;
}) {
return (
<div className="flex flex-col gap-8">
<AccountAvatarFormControl />
<AccountAvatarFormControl updateAccountImage={props.updateAccountImage} />
<AccountNameFormControl name={props.account.name || ""} />
<AccountEmailFormControl
email={props.account.email || ""}
@@ -58,16 +59,14 @@ export function AccountSettingsPageUI(props: {
);
}
function AccountAvatarFormControl() {
function AccountAvatarFormControl(props: {
updateAccountImage: (file: File | undefined) => Promise<void>;
}) {
const [avatar, setAvatar] = useState<File>(); // TODO: prefill with account avatar
// TODO - implement
const updateAvatarMutation = useMutation({
mutationFn: async (_avatar: File | undefined) => {
// Fake loading
await new Promise((resolve) => setTimeout(resolve, 3000));
console.log("Updating account image to", _avatar);
throw new Error("Not implemented");
await props.updateAccountImage(_avatar);
},
});
@@ -1,11 +1,10 @@
import { API_SERVER_URL } from "@/constants/env";
import type { Account } from "@3rdweb-sdk/react/hooks/useApi";
import { getAuthToken } from "../../api/lib/getAuthToken";
export async function getAccount() {
const authToken = getAuthToken();
const apiServerURL = new URL(
process.env.NEXT_PUBLIC_THIRDWEB_API_HOST || "https://api.thirdweb.com",
);
const apiServerURL = new URL(API_SERVER_URL);
apiServerURL.pathname = "/v1/account/me";
@@ -1,13 +1,18 @@
import { getThirdwebClient } from "@/constants/thirdweb.server";
import { redirect } from "next/navigation";
import { AccountSettingsPageUI } from "./AccountSettingsPageUI";
import { getAuthToken } from "../../api/lib/getAuthToken";
import { AccountSettingsPage } from "./AccountSettingsPage";
import { getAccount } from "./getAccount";
export default async function Page() {
const account = await getAccount();
const token = getAuthToken();
if (!account) {
if (!account || !token) {
redirect(`/login?next=${encodeURIComponent("/account")}`);
}
return <AccountSettingsPageUI account={account} />;
return (
<AccountSettingsPage account={account} client={getThirdwebClient(token)} />
);
}
@@ -1,11 +1,9 @@
import { COOKIE_ACTIVE_ACCOUNT, COOKIE_PREFIX_TOKEN } from "@/constants/cookie";
import { API_SERVER_URL } from "@/constants/env";
import { cookies } from "next/headers";
import { type NextRequest, NextResponse } from "next/server";
import { getAddress } from "thirdweb/utils";
const THIRDWEB_API_HOST =
process.env.NEXT_PUBLIC_THIRDWEB_API_HOST || "https://api.thirdweb.com";
export type EnsureLoginPayload = {
pathname: string;
address?: string;
@@ -56,7 +54,7 @@ export const GET = async (req: NextRequest) => {
}
// check that the token is valid by checking for the user account
const accountRes = await fetch(`${THIRDWEB_API_HOST}/v1/account/me`, {
const accountRes = await fetch(`${API_SERVER_URL}/v1/account/me`, {
method: "GET",
headers: {
Authorization: `Bearer ${token}`,
@@ -1,11 +1,9 @@
import { COOKIE_PREFIX_TOKEN } from "@/constants/cookie";
import { API_SERVER_URL } from "@/constants/env";
import { cookies } from "next/headers";
import { type NextRequest, NextResponse } from "next/server";
import { getAddress } from "thirdweb/utils";
const THIRDWEB_API_HOST =
process.env.NEXT_PUBLIC_THIRDWEB_API_HOST || "https://api.thirdweb.com";
export type GetAuthTokenResponse = {
jwt: string | null;
};
@@ -43,7 +41,7 @@ export const GET = async (req: NextRequest) => {
}
// check token validity
const accountRes = await fetch(`${THIRDWEB_API_HOST}/v1/account/me`, {
const accountRes = await fetch(`${API_SERVER_URL}/v1/account/me`, {
method: "GET",
headers: {
Authorization: `Bearer ${token}`,
+7 -9
View File
@@ -1,3 +1,4 @@
import { API_SERVER_URL } from "@/constants/env";
import type { ApiKey } from "@3rdweb-sdk/react/hooks/useApi";
import { getAuthToken } from "./getAuthToken";
@@ -36,17 +37,14 @@ export async function getAPIKey(apiKeyId: string) {
export async function getApiKeys() {
const authToken = getAuthToken();
const res = await fetch(
`${process.env.NEXT_PUBLIC_THIRDWEB_API_HOST || "https://api.thirdweb.com"}/v1/keys`,
{
method: "GET",
const res = await fetch(`${API_SERVER_URL}/v1/keys`, {
method: "GET",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${authToken}`,
},
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${authToken}`,
},
);
});
const json = await res.json();
if (json.error) {
@@ -1,68 +1,101 @@
import { ColorModeToggle } from "@/components/color-mode-toggle";
import { Spinner } from "@/components/ui/Spinner/Spinner";
import { NavLink } from "@/components/ui/NavLink";
import { Button } from "@/components/ui/button";
import { CustomConnectWallet } from "@3rdweb-sdk/react/components/connect-wallet";
import { ClientOnly } from "components/ClientOnly/ClientOnly";
import { CmdKSearch } from "components/cmd-k-search";
import { ColorModeToggle } from "components/color-mode/color-mode-toggle";
import { Logo } from "components/logo";
import { CreditsButton } from "components/settings/Account/Billing/CreditsButton";
import { UpgradeButton } from "components/settings/Account/Billing/UpgradeButton";
import { CircleHelpIcon } from "lucide-react";
import Link from "next/link";
import { ThirdwebMiniLogo } from "../ThirdwebMiniLogo";
import { NavLink } from "../nav-link.client";
import { MobileBurgerMenu } from "./MobileBurgerMenu";
import { headerLinks } from "./headerLinks";
import { Suspense } from "react";
import { DashboardHeaderTabs } from "./DashboardHeaderTabs";
export function DashboardHeader() {
export const DashboardHeader: React.FC = () => {
return (
// the "h-24" avoids layout shift when connecting wallet (connected wallet button is taller than disconnected...)
<header className="flex h-20 flex-shrink-0 flex-row items-center justify-between border-border border-b bg-muted/50 px-4 py-4 md:h-20 lg:px-6">
{/* Left */}
<div className="flex items-center gap-6">
<div className="flex items-center gap-2">
<MobileBurgerMenu />
<div className="bg-muted/50">
<header className="flex items-center justify-between px-4 py-3 lg:px-6">
{/* left */}
<div className="flex items-center gap-4">
<Link href="/dashboard">
<ThirdwebMiniLogo className="h-6" />
<Logo hideWordmark />
</Link>
<CmdKSearch />
</div>
<div className="hidden items-center gap-2 md:flex">
{headerLinks.left.map((link) => (
<NavLink
key={link.href}
href={link.href}
target={link.href.startsWith("https") ? "_blank" : undefined}
className="px-[10px] py-2 font-normal text-muted-foreground text-sm hover:text-foreground"
activeClassName="font-medium text-foreground"
>
{link.name}
</NavLink>
))}
</div>
</div>
{/* Right */}
<div className="flex items-center gap-4">
<div className="items-center gap-2 lg:flex">
<div className="hidden items-center gap-2 lg:flex">
{headerLinks.right.map((link) => (
<Link
key={link.href}
href={link.href}
target={link.href.startsWith("https") ? "_blank" : undefined}
className="px-[10px] py-2 font-normal text-muted-foreground text-sm hover:text-foreground"
>
{link.name}
</Link>
))}
{/* Right */}
<div className="flex items-center gap-2">
<div className="hidden gap-2 md:flex">
<Suspense fallback={null}>
<CreditsButton />
</Suspense>
<UpgradeButton />
</div>
<ColorModeToggle />
</div>
<ClientOnly
ssr={
<div className="flex h-[48px] w-[144px] items-center justify-center rounded-lg border bg-muted/50">
<Spinner className="size-4" />
</div>
}
>
<CustomConnectWallet />
</ClientOnly>
</div>
</header>
<HeaderNavLink
trackingLabel="chainlist"
href="/chainlist"
label="Chainlist"
/>
<HeaderNavLink
trackingLabel="docs"
href="https://portal.thirdweb.com"
label="Docs"
/>
<HeaderNavLink
trackingLabel="support"
href="/support"
label="Support"
/>
<div className="md:hidden">
<Button asChild variant="ghost">
<NavLink
className="fade-in-0 !h-auto !w-auto p-2"
href="/support"
tracking={{
category: "header",
action: "click",
label: "support",
}}
>
<CircleHelpIcon className="size-5" />
</NavLink>
</Button>
</div>
<ColorModeToggle />
<div className="md:ml-2">
<CustomConnectWallet />
</div>
</div>
</header>
<DashboardHeaderTabs />
</div>
);
};
function HeaderNavLink(props: {
label: string;
trackingLabel: string;
href: string;
}) {
return (
<NavLink
href={props.href}
tracking={{
category: "header",
action: "click",
label: props.trackingLabel,
}}
className="hidden px-2.5 text-muted-foreground text-sm hover:text-foreground md:block"
activeClassName="text-foreground"
>
{props.label}
</NavLink>
);
}
@@ -0,0 +1,42 @@
"use client";
import { TabLinks } from "@/components/ui/tabs";
import { usePathname } from "next/navigation";
export function DashboardHeaderTabs() {
const pathname = usePathname() || "";
return (
<TabLinks
className="w-full"
tabContainerClassName="px-4"
shadowColor="transparent"
links={[
{
href: "/dashboard",
name: "Home",
isActive: pathname === "/dashboard",
},
{
href: "/dashboard/connect/analytics",
name: "Connect",
isActive: pathname.startsWith("/dashboard/connect"),
},
{
href: "/dashboard/contracts/deploy",
name: "Contracts",
isActive: pathname.startsWith("/dashboard/contracts"),
},
{
href: "/dashboard/engine",
name: "Engine",
isActive: pathname.startsWith("/dashboard/engine"),
},
{
href: "/dashboard/settings/api-keys",
name: "Settings",
isActive: pathname.startsWith("/dashboard/settings"),
},
]}
/>
);
}
@@ -1,66 +0,0 @@
"use client";
import { Button } from "@/components/ui/button";
import { useIsomorphicLayoutEffect } from "@/lib/useIsomorphicLayoutEffect";
import { Menu, XIcon } from "lucide-react";
import Link from "next/link";
import { useState } from "react";
import { headerLinks } from "./headerLinks";
export function MobileBurgerMenu() {
const [showBurgerMenu, setShowBurgerMenu] = useState(false);
useIsomorphicLayoutEffect(() => {
if (showBurgerMenu) {
// eslint-disable-next-line react-compiler/react-compiler
document.body.style.overflow = "hidden";
} else {
document.body.style.overflow = "auto";
}
}, [showBurgerMenu]);
useIsomorphicLayoutEffect(() => {
return () => {
document.body.style.overflow = "auto";
};
});
return (
<>
<Button
variant="outline"
size="icon"
className="md:hidden"
onClick={() => setShowBurgerMenu(!showBurgerMenu)}
>
<Menu strokeWidth={1} />
</Button>
{showBurgerMenu && (
<nav className="fade-in-20 slide-in-from-top-3 fixed inset-0 top-[80px] z-50 animate-in border-t bg-background p-6">
<Button
variant="ghost"
size="icon"
className="absolute top-6 right-6"
onClick={() => setShowBurgerMenu(false)}
>
<XIcon strokeWidth={1} />
</Button>
<div className="flex flex-col gap-7">
{[...headerLinks.left, ...headerLinks.right].map((link) => (
<Link
key={link.href}
href={link.href}
className="text-lg "
target={link.href.startsWith("https") ? "_blank" : undefined}
>
{link.name}
</Link>
))}
</div>
</nav>
)}
</>
);
}
@@ -1,10 +1,11 @@
import type { Account } from "@3rdweb-sdk/react/hooks/useApi";
import Link from "next/link";
import type React from "react";
import { ResourcesDropdownButton } from "./ResourcesDropdownButton";
import { AccountButton } from "./account-button.client";
export function SecondaryNav(props: {
email: string | undefined;
account: Pick<Account, "email" | "id"> | undefined;
logout: () => void;
connectButton: React.ReactNode;
}) {
@@ -27,9 +28,9 @@ export function SecondaryNav(props: {
</Link>
<AccountButton
email={props.email}
logout={props.logout}
connectButton={props.connectButton}
account={props.account}
/>
</div>
);
@@ -1,5 +1,6 @@
"use client";
import { GradientAvatar } from "@/components/blocks/Avatars/GradientAvatar";
import { Button } from "@/components/ui/button";
import {
DropdownMenu,
@@ -8,14 +9,15 @@ import {
} from "@/components/ui/dropdown-menu";
import { SkeletonContainer } from "@/components/ui/skeleton";
import { cn } from "@/lib/utils";
import type { Account } from "@3rdweb-sdk/react/hooks/useApi";
import { LogOutIcon, Moon, Sun } from "lucide-react";
import { useTheme } from "next-themes";
import Link from "next/link";
export function AccountButton(props: {
email: string | undefined;
logout: () => void;
connectButton: React.ReactNode;
account?: Pick<Account, "email" | "id">;
}) {
const { setTheme, theme } = useTheme();
@@ -27,15 +29,15 @@ export function AccountButton(props: {
className="!p-0 !h-auto size-10 rounded-full hover:ring-2 hover:ring-offset-2"
variant="ghost"
>
{/* TODO - replace with account image */}
<div className="size-9 rounded-full border border-border bg-muted" />
{/* TODO - set account image */}
<GradientAvatar id={props.account?.id} src={""} className="size-9" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-[300px] rounded-lg p-0" align="end">
<div className="border-border border-b p-4 pb-5">
<SkeletonContainer
skeletonData="[email protected]"
loadedData={props.email}
loadedData={props.account?.email}
render={(v) => <p className="text-muted-foreground text-sm">{v}</p>}
/>
@@ -1,34 +0,0 @@
export const headerLinks = {
left: [
{
name: "Connect",
href: "/dashboard/connect/analytics",
},
{
name: "Contracts",
href: "/dashboard/contracts",
},
{
name: "Engine",
href: "/dashboard/engine",
},
{
name: "Settings",
href: "/dashboard/settings/api-keys",
},
],
right: [
{
name: "Chainlist",
href: "/chainlist",
},
{
name: "Docs",
href: "https://portal.thirdweb.com",
},
{
name: "Support",
href: "https://thirdweb.com/support",
},
],
};
@@ -1,29 +0,0 @@
"use client";
import { cn } from "@/lib/utils";
import Link, { type LinkProps } from "next/link";
import { usePathname } from "next/navigation";
type NavLinkProps = Omit<
React.AnchorHTMLAttributes<HTMLAnchorElement>,
keyof LinkProps
> &
Omit<LinkProps, "href"> & {
href: string;
activeClassName: string;
};
export const NavLink: React.FC<React.PropsWithChildren<NavLinkProps>> = ({
children,
className,
activeClassName,
...props
}) => {
const pathname = usePathname();
const isActive = !!pathname?.startsWith(props.href);
return (
<Link {...props} className={cn(className, isActive && activeClassName)}>
{children}
</Link>
);
};
+4 -6
View File
@@ -2,6 +2,7 @@
import "server-only";
import { COOKIE_ACTIVE_ACCOUNT, COOKIE_PREFIX_TOKEN } from "@/constants/cookie";
import { API_SERVER_URL } from "@/constants/env";
import { cookies } from "next/headers";
import { getAddress } from "thirdweb";
import type {
@@ -10,9 +11,6 @@ import type {
VerifyLoginPayloadParams,
} from "thirdweb/auth";
const THIRDWEB_API_HOST =
process.env.NEXT_PUBLIC_THIRDWEB_API_HOST || "https://api.thirdweb.com";
const THIRDWEB_API_SECRET = process.env.API_SERVER_SECRET || "";
export async function getLoginPayload(
@@ -21,7 +19,7 @@ export async function getLoginPayload(
if (!THIRDWEB_API_SECRET) {
throw new Error("API_SERVER_SECRET is not set");
}
const res = await fetch(`${THIRDWEB_API_HOST}/v2/siwe/payload`, {
const res = await fetch(`${API_SERVER_URL}/v2/siwe/payload`, {
method: "POST",
headers: {
"Content-Type": "application/json",
@@ -48,7 +46,7 @@ export async function doLogin(payload: VerifyLoginPayloadParams) {
const cookieStore = cookies();
// forward the request to the API server
const res = await fetch(`${THIRDWEB_API_HOST}/v2/siwe/login`, {
const res = await fetch(`${API_SERVER_URL}/v2/siwe/login`, {
method: "POST",
headers: {
"Content-Type": "application/json",
@@ -144,7 +142,7 @@ export async function isLoggedIn(address: string) {
return false;
}
const res = await fetch(`${THIRDWEB_API_HOST}/v1/account/me`, {
const res = await fetch(`${API_SERVER_URL}/v1/account/me`, {
method: "GET",
headers: {
"Content-Type": "application/json",
+2
View File
@@ -78,6 +78,8 @@ function CustomConnectEmmbed() {
modalSize={isLG ? "wide" : "compact"}
theme={getSDKTheme(theme === "light" ? "light" : "dark")}
className="shadow-lg"
privacyPolicyUrl="/privacy"
termsOfServiceUrl="/tos"
/>
);
}
@@ -13,6 +13,7 @@ import { ChevronDownIcon, SearchIcon } from "lucide-react";
import Link from "next/link";
import { useState } from "react";
import { ProjectAvatar } from "@/components/blocks/Avatars/ProjectAvatar";
import { CopyButton } from "@/components/ui/CopyButton";
import {
Select,
@@ -119,8 +120,8 @@ function ProjectCard(props: {
key={project.id}
className="relative flex items-center gap-4 rounded-lg border border-border bg-muted/50 p-4 transition-colors hover:bg-muted/70"
>
{/* TODO - replace with project image */}
<div className="size-10 shrink-0 rounded-full bg-border" />
{/* TODO - set image */}
<ProjectAvatar className="size-10 rounded-full" src="" />
<div>
<Link
@@ -0,0 +1,62 @@
"use client";
import type { Team } from "@/api/team";
import { cn } from "@/lib/utils";
import { useAccount } from "@3rdweb-sdk/react/hooks/useApi";
import { usePathname } from "next/navigation";
import { useState } from "react";
import { TeamSettingsSidebar } from "./_components/sidebar/TeamSettingsSidebar";
import { TeamSettingsMobileNav } from "./_components/sidebar/TeamsMobileNav";
import { getTeamSettingsLinks } from "./_components/sidebar/getTeamSettingsLinks";
// on the /~/settings page
// - On desktop: show the general settings as usual
// - On mobile: show the full nav instead of page content and when user clicks on the "General Settings" ( first link ) - hide the full nav and show the page content
export function SettingsLayout(props: {
team: Team;
children: React.ReactNode;
}) {
const [_showFullNavOnMobile, setShowFullNavOnMobile] = useState(true);
const pathname = usePathname();
const isSettingsOverview = (pathname || "").endsWith("/~/settings");
const showFullNavOnMobile = _showFullNavOnMobile && isSettingsOverview;
const links = getTeamSettingsLinks(props.team.slug);
const activeLink = links.find((link) => pathname === link.href);
const accountQuery = useAccount();
return (
<div>
{/* Huge page title */}
<div className="border-border border-b py-10">
<div className="container">
<h1 className="font-semibold text-3xl tracking-tight">
Team Settings
</h1>
</div>
</div>
<div className="md:hidden">
<TeamSettingsMobileNav
teamSlug={props.team.slug}
showFull={showFullNavOnMobile}
setShowFull={setShowFullNavOnMobile}
activeLink={activeLink}
/>
</div>
<div className="container flex grow gap-8 lg:min-h-[900px] [&>*]:py-8 lg:[&>*]:py-10">
<TeamSettingsSidebar team={props.team} account={accountQuery.data} />
<div
className={cn(
"grow",
// if showing full nav on mobile - hide the page content
showFullNavOnMobile && "max-sm:hidden",
)}
>
{props.children}
</div>
</div>
</div>
);
}
@@ -1,10 +1,14 @@
import type { Team } from "@/api/team";
import { GradientAvatar } from "@/components/blocks/Avatars/GradientAvatar";
import type { Account } from "@3rdweb-sdk/react/hooks/useApi";
import { SidebarLink } from "./SidebarLink";
import { getTeamSettingsLinks } from "./getTeamSettingsLinks";
export function TeamSettingsSidebar(props: {
teamSlug: string;
team: Team;
account: Pick<Account, "id"> | undefined;
}) {
const teamLinks = getTeamSettingsLinks(props.teamSlug);
const teamLinks = getTeamSettingsLinks(props.team.slug);
const teamGroupLinks = teamLinks.filter((link) => link.group === "team");
const accountGroupLinks = teamLinks.filter(
(link) => link.group === "account",
@@ -12,8 +16,24 @@ export function TeamSettingsSidebar(props: {
return (
<aside className="lg:-ml-2 sticky top-0 hidden w-[250px] shrink-0 grow-0 flex-col gap-4 self-start lg:flex">
<RenderLinkGroup links={teamGroupLinks} title="Team" />
<RenderLinkGroup links={accountGroupLinks} title="Account" />
<RenderLinkGroup
links={teamGroupLinks}
title="Team"
team={props.team}
titleAvatarIcon={{
id: props.team.id,
src: "", // TODO - set team image
}}
/>
<RenderLinkGroup
links={accountGroupLinks}
title="Account"
team={props.team}
titleAvatarIcon={{
id: props.account?.id,
src: "", // TODO - set team image
}}
/>
</aside>
);
}
@@ -24,12 +44,20 @@ function RenderLinkGroup(props: {
href: string;
}>;
title: string;
team: Team;
titleAvatarIcon: {
id: string | undefined;
src: string;
};
}) {
return (
<>
<div className="flex items-center gap-1.5 px-4">
{/* TODO - using placeholder for now */}
<div className="size-4 rounded-full border border-border bg-muted" />
<GradientAvatar
src={props.titleAvatarIcon.src}
className="size-4"
id={props.titleAvatarIcon.id}
/>
<p className="text-muted-foreground text-xs">{props.title}</p>
</div>
@@ -4,7 +4,9 @@ import { Spinner } from "@/components/ui/Spinner/Spinner";
import { AccountStatus, useAccount } from "@3rdweb-sdk/react/hooks/useApi";
import { Billing } from "components/settings/Account/Billing";
export const SettingsBillingPage = () => {
export const SettingsBillingPage = (props: {
teamId: string | undefined;
}) => {
const meQuery = useAccount({
refetchInterval: (query) =>
[
@@ -25,5 +27,5 @@ export const SettingsBillingPage = () => {
);
}
return <Billing account={account} />;
return <Billing account={account} teamId={props.teamId} />;
};
@@ -1,10 +1,22 @@
import { getTeamBySlug } from "@/api/team";
import { ChakraProviderSetup } from "@/components/ChakraProviderSetup";
import { notFound } from "next/navigation";
import { SettingsBillingPage } from "./BillingSettingsPage";
export default function Page() {
export default async function Page(props: {
params: {
team_slug: string;
};
}) {
const team = await getTeamBySlug(props.params.team_slug);
if (!team) {
notFound();
}
return (
<ChakraProviderSetup>
<SettingsBillingPage />
<SettingsBillingPage teamId={team.id} />
</ChakraProviderSetup>
);
}

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