Compare commits

...
Author SHA1 Message Date
Jonas DanielsGitHubgithub-actions[bot] <github-actions[bot]@users.noreply.github.com>
8861982877 Version Packages (#5188)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-10-29 14:13:02 +13:00
Joaquim VergesandGitHub 07ea65bcd8 feat: improve ecosystem smart account handling (#5212) 2024-10-29 13:25:21 +13:00
jnsdls 6cb66c675a remove all farcaster frame logic for the moment (#5213)
<!-- start pr-codex -->

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

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

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

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

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

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

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

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

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

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

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

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

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

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

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

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

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

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

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

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

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

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

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

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

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

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

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

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

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

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

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

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

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

Addresses CNCT-2132

<!-- start pr-codex -->

---

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

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

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

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

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

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

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

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

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

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

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

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

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

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

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

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

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

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

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

<!-- start pr-codex -->

---

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

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

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

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

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

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

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

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

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

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

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

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

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

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

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

<!-- start pr-codex -->

---

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

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

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

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

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

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

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

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

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

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

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

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

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

<!-- end pr-codex -->
2024-10-25 17:10:53 +00:00
9dfe4af679 Biz hackweek - Project Showcase (#5118)
Co-authored-by: samina <[email protected]>
2024-10-25 09:52:30 -07:00
Jonas DanielsGitHubgithub-actions[bot] <github-actions[bot]@users.noreply.github.com>
b5bbecd182 Version Packages (#5173)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-10-25 21:26:53 +13:00
Joaquim VergesandGitHub bafa0e83c3 feat: set initial balance for Anvil proxy in tests (#5174) 2024-10-25 20:58:30 +13:00
kien-ngo 5dc1899541 [SDK] Improve Vote contract tests (#5156)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on enhancing validation for the `minVoteQuorumRequiredPercent` parameter in the `deploy-vote.ts` file and updating corresponding tests to ensure correct functionality.

### Detailed summary
- Added validation for `minVoteQuorumRequiredPercent` in `deploy-vote.ts`.
- Included checks for NaN, range (0-100), and integer-like values.
- Updated tests in `deploy-vote.test.ts` to handle new validation logic.
- Adjusted test descriptions and assertions in `vote.test.ts`.

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

<!-- end pr-codex -->
2024-10-25 03:59:18 +00:00
kien-ngo 4b183518af [SDK] Improve Split contract tests (#5155)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on adding tests for the `split` functionality of a smart contract in the `thirdweb` package, ensuring that the contract behaves as expected when deployed and queried for recipient addresses and their share percentages.

### Detailed summary
- Deleted several test files related to `deploy-split` and recipient functions.
- Introduced a new test suite for the `split` contract in `split.test.ts`.
- Added tests for:
  - Deployment of the split contract.
  - Retrieving all recipient addresses.
  - Retrieving all recipients and their share percentages.
  - Retrieving the split percentage for an individual recipient.

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

<!-- end pr-codex -->
2024-10-25 03:59:17 +00:00
joaquim-verges e8f952a191 feat: always add URI to SIWE payload (#5172)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on ensuring that a URI is always included in the SIWE payload by modifying the logic in the `generate-login-payload.ts` file and updating the corresponding test.

### Detailed summary
- In `generate-login-payload.ts`, the `uri` field is updated to default to `options.domain` if `options.login?.uri` is not provided.
- In `generate-login-payload.test.ts`, the test case for `uri` is changed to use `"example.com"` instead of `undefined`.

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

<!-- end pr-codex -->
2024-10-25 02:45:35 +00:00
Jonas DanielsGitHubgithub-actions[bot] <github-actions[bot]@users.noreply.github.com>
132ccd3535 Version Packages (#5171)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-10-25 13:06:16 +13:00
gregfromstl f23911e1da chore: service utils changeset (#5169)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on reducing the strictness of the parsing for published event actions in the `@thirdweb-dev/service-utils` package.

### Detailed summary
- Updated the `@thirdweb-dev/service-utils` package to a patch version.
- Modified the parsing logic for published event actions to be less strict, allowing for more flexible handling of events.

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

<!-- end pr-codex -->
2024-10-24 23:30:33 +00:00
gregfromstl cd22bca3d8 [Dashboard] Fix: Exclude testnet $ from total sponsored (#5166)
<!-- start pr-codex -->

## PR-Codex overview
This PR focuses on improving the handling of analytics data in the `useUserOpUsageAggregate` function by incorporating chain data and refining the conditional rendering in the `stat.tsx` component.

### Detailed summary
- In `stat.tsx`, changed the conditional rendering to check for `value !== undefined` instead of `value &&`.
- In `useApi.ts`, imported `useAllChainsData` for accessing chain data.
- Added `chainStore` to hold all chains data.
- Modified `userOpStats` type to include `chainId`.
- Implemented a check to skip testnets in aggregated stats based on `chainId`.

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

<!-- end pr-codex -->
2024-10-24 23:05:37 +00:00
Jonas DanielsGitHubgithub-actions[bot] <github-actions[bot]@users.noreply.github.com>
e4b6d87dd8 Version Packages (#5168)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-10-25 12:04:19 +13:00
Joaquim VergesandGitHub 158c2b6e8e feat: respect theme accentColor for default connect UI icons (#5163) 2024-10-25 11:52:08 +13:00
MananTank 52229be96a Revert @sentry/nextjs to 8.34.0 to fix page crash on tx rejection (#5167)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on downgrading the `@sentry/nextjs` package from version `8.35.0` to `8.34.0` and updating the corresponding dependencies in the `pnpm-lock.yaml` file.

### Detailed summary
- Downgraded `@sentry/nextjs` from `8.35.0` to `8.34.0` in `apps/dashboard/package.json`.
- Updated related dependencies in `pnpm-lock.yaml` to reflect the version change for `@sentry/nextjs` and its internal packages.
- Adjusted versions for several other `@sentry` packages to `8.34.0`.
- Removed unnecessary version specifications in `pnpm-lock.yaml`.

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

<!-- end pr-codex -->
2024-10-24 22:37:50 +00:00
gregfromstl 012f3defda Update service-utils event publishing to accept any source (#5162)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR updates the `usageEventSchema` in `packages/service-utils/src/core/usage.ts` by changing the type of the `source` field from an enumeration to a string, allowing for more flexibility in the values that can be assigned to `source`.

### Detailed summary
- Changed the `source` field in `usageEventSchema` from `z.enum` to `z.string()`.

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

<!-- end pr-codex -->
2024-10-24 22:25:10 +00:00
MananTank 8e43e340a7 Handle isContractDeployed error on contract page (#5165)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on improving error handling when checking if a contract is deployed. It ensures that if the contract check fails, it will not throw an error but instead return `false`.

### Detailed summary
- Modified `isValidContract` assignment to handle errors gracefully by using `.catch(() => false)` instead of letting the promise reject.
- Ensured that the flow continues to call `notFound()` if the contract is not valid.

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

<!-- end pr-codex -->
2024-10-24 21:49:22 +00:00
gregfromstl 74e52c000e [Dashboard] Feature: Adds chain ID to total sponsored analytics (#5161)
<!-- start pr-codex -->

## PR-Codex overview
This PR introduces an interface `UserOpStatsByChain` and updates the `TotalSponsoredChartCard` component to handle user operation statistics by blockchain. It enhances chart data handling and visualization, allowing for better representation of sponsored gas across different chains.

### Detailed summary
- Added `UserOpStatsByChain` interface in `useApi.ts`.
- Updated `TotalSponsoredChartCard` to use `UserOpStatsByChain[]`.
- Modified chart data structure to accommodate multiple chains.
- Implemented logic to categorize and display "others" in charts.
- Adjusted CSV export to include unique chain data.
- Improved chart rendering with dynamic colors and data keys based on chains.

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

<!-- end pr-codex -->
2024-10-24 21:02:08 +00:00
274 changed files with 6369 additions and 5350 deletions
-9
View File
@@ -75,15 +75,6 @@ GITHUB_API_TOKEN="ghp_..."
# Upload server url
NEXT_PUBLIC_DASHBOARD_UPLOAD_SERVER="https://storage.thirdweb-preview.com"
# Payments Admin Secret. For dev, get this value from paper-web-dev (on Zeet)'s "HASURA_ADMIN_SECRET_KEY" env var
PAYMENTS_ADMIN_SECRET=""
# Disable telemetry
NEXT_TELEMETRY_DISABLED=1
# Use trench for KYC
NEXT_PUBLIC_TRENCH_2_API_ENDPOINT=""
# Unthread variables - only required for submitting the support form in /support
UNTHREAD_API_KEY=""
UNTHREAD_TRIAGE_CHANNEL_ID=""
+5 -6
View File
@@ -25,7 +25,6 @@
"@chakra-ui/react": "^2.8.2",
"@chakra-ui/styled-system": "^2.9.2",
"@chakra-ui/theme-tools": "^2.1.2",
"@coinbase/onchainkit": "^0.14.2",
"@emotion/react": "11.13.3",
"@emotion/styled": "11.13.0",
"@hookform/resolvers": "^3.9.0",
@@ -62,13 +61,13 @@
"compare-versions": "^6.1.0",
"date-fns": "4.1.0",
"flat": "^6.0.1",
"framer-motion": "11.11.9",
"framer-motion": "11.11.10",
"fuse.js": "7.0.0",
"input-otp": "^1.2.4",
"ioredis": "^5.4.1",
"ipaddr.js": "^2.2.0",
"lottie-react": "^2.4.0",
"lucide-react": "0.453.0",
"lucide-react": "0.454.0",
"next": "14.2.15",
"next-plausible": "^3.12.0",
"next-seo": "^6.5.0",
@@ -108,7 +107,7 @@
"@chromatic-com/storybook": "2.0.2",
"@next/bundle-analyzer": "14.2.15",
"@next/eslint-plugin-next": "14.2.15",
"@playwright/test": "1.47.2",
"@playwright/test": "1.48.2",
"@storybook/addon-essentials": "8.3.6",
"@storybook/addon-interactions": "8.3.6",
"@storybook/addon-links": "8.3.6",
@@ -118,7 +117,7 @@
"@storybook/nextjs": "8.3.6",
"@storybook/react": "8.3.6",
"@storybook/test": "8.3.6",
"@types/color": "^3.0.6",
"@types/color": "4.2.0",
"@types/node": "20.14.9",
"@types/papaparse": "^5.3.15",
"@types/pluralize": "^0.0.33",
@@ -136,7 +135,7 @@
"eslint-config-biome": "1.9.3",
"eslint-plugin-react-compiler": "0.0.0-experimental-fa06e2c-20241014",
"eslint-plugin-storybook": "^0.9.0",
"knip": "^5.33.3",
"knip": "5.34.2",
"next-sitemap": "^4.2.3",
"postcss": "8.4.47",
"storybook": "8.3.6",
Binary file not shown.

After

Width:  |  Height:  |  Size: 551 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 730 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 721 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 631 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 134 KiB

@@ -5,6 +5,7 @@ import {
useQueryClient,
} from "@tanstack/react-query";
import { THIRDWEB_ANALYTICS_API_HOST, THIRDWEB_API_HOST } from "constants/urls";
import { useAllChainsData } from "hooks/chains/allChains";
import invariant from "tiny-invariant";
import { accountKeys, apiKeys, authorizedWallets } from "../cache-keys";
import { useLoggedInUser } from "./useLoggedInUser";
@@ -241,6 +242,14 @@ export interface UserOpStats {
sponsoredUsd: number;
}
export interface UserOpStatsByChain {
date: string;
successful: number;
failed: number;
sponsoredUsd: number;
chainId?: string;
}
interface BillingProduct {
name: string;
id: string;
@@ -434,6 +443,7 @@ export function useUserOpUsageAggregate(args: {
}) {
const { clientId, from, to } = args;
const { user, isLoggedIn } = useLoggedInUser();
const chainStore = useAllChainsData();
return useQuery<UserOpStats>({
queryKey: accountKeys.userOpStats(
@@ -444,16 +454,25 @@ export function useUserOpUsageAggregate(args: {
"all",
),
queryFn: async () => {
const userOpStats: UserOpStats[] = await getUserOpUsage({
clientId,
from,
to,
period: "all",
});
const userOpStats: (UserOpStats & { chainId?: string })[] =
await getUserOpUsage({
clientId,
from,
to,
period: "all",
});
// Aggregate stats across wallet types
return userOpStats.reduce(
(acc, curr) => {
// Skip testnets from the aggregated stats
if (curr.chainId) {
const chain = chainStore.idToChain.get(Number(curr.chainId));
if (chain?.testnet) {
return acc;
}
}
acc.successful += curr.successful;
acc.failed += curr.failed;
acc.sponsoredUsd += curr.sponsoredUsd;
@@ -18,7 +18,7 @@ export function useDashboardContractMetadata(contract: ThirdwebContract) {
});
}
type DashboardContractMetadata = {
export type DashboardContractMetadata = {
name: string;
symbol: string;
image: string;
@@ -1,7 +1,7 @@
"use client";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import type { ResultItem } from "components/engine/system-metrics/components/StatusCodes";
import type { ResultItem } from "app/team/[team_slug]/[project_slug]/engine/(instance)/[engineId]/metrics/components/StatusCodes";
import { THIRDWEB_API_HOST } from "constants/urls";
import type { EngineBackendWalletType } from "lib/engine";
import { useState } from "react";
@@ -0,0 +1,27 @@
"use client";
import type { ThirdwebContract } from "thirdweb";
import { ErrorPage, LoadingPage } from "../../_components/page-skeletons";
import { RedirectToContractOverview } from "../../_components/redirect-contract-overview.client";
import { useContractPageMetadata } from "../../_hooks/useContractPageMetadata";
import { ContractDirectListingsPage } from "./ContractDirectListingsPage";
export function ContractDirectListingsPageClient(props: {
contract: ThirdwebContract;
}) {
const metadataQuery = useContractPageMetadata(props.contract);
if (metadataQuery.isPending) {
return <LoadingPage />;
}
if (metadataQuery.isError) {
return <ErrorPage />;
}
if (!metadataQuery.data.isDirectListingSupported) {
return <RedirectToContractOverview contract={props.contract} />;
}
return <ContractDirectListingsPage contract={props.contract} />;
}
@@ -1,7 +1,9 @@
import { notFound, redirect } from "next/navigation";
import { localhost } from "thirdweb/chains";
import { getContractPageParamsInfo } from "../../_utils/getContractFromParams";
import { getContractPageMetadata } from "../../_utils/getContractPageMetadata";
import { ContractDirectListingsPage } from "./ContractDirectListingsPage";
import { ContractDirectListingsPageClient } from "./ContractDirectListingsPage.client";
export default async function Page(props: {
params: {
@@ -15,6 +17,10 @@ export default async function Page(props: {
notFound();
}
if (info.chainMetadata.chainId === localhost.id) {
return <ContractDirectListingsPageClient contract={info.contract} />;
}
const { isDirectListingSupported } = await getContractPageMetadata(
info.contract,
);
@@ -0,0 +1,27 @@
"use client";
import type { ThirdwebContract } from "thirdweb";
import { ErrorPage, LoadingPage } from "../../_components/page-skeletons";
import { RedirectToContractOverview } from "../../_components/redirect-contract-overview.client";
import { useContractPageMetadata } from "../../_hooks/useContractPageMetadata";
import { ContractEnglishAuctionsPage } from "./ContractEnglishAuctionsPage";
export function ContractEnglishAuctionsPageClient(props: {
contract: ThirdwebContract;
}) {
const metadataQuery = useContractPageMetadata(props.contract);
if (metadataQuery.isPending) {
return <LoadingPage />;
}
if (metadataQuery.isError) {
return <ErrorPage />;
}
if (!metadataQuery.data.isEnglishAuctionSupported) {
return <RedirectToContractOverview contract={props.contract} />;
}
return <ContractEnglishAuctionsPage contract={props.contract} />;
}
@@ -1,7 +1,9 @@
import { notFound, redirect } from "next/navigation";
import { localhost } from "thirdweb/chains";
import { getContractPageParamsInfo } from "../../_utils/getContractFromParams";
import { getContractPageMetadata } from "../../_utils/getContractPageMetadata";
import { ContractEnglishAuctionsPage } from "./ContractEnglishAuctionsPage";
import { ContractEnglishAuctionsPageClient } from "./ContractEnglishAuctionsPage.client";
export default async function Page(props: {
params: {
@@ -15,6 +17,10 @@ export default async function Page(props: {
notFound();
}
if (info.chainMetadata.chainId === localhost.id) {
return <ContractEnglishAuctionsPageClient contract={info.contract} />;
}
const { isEnglishAuctionSupported } = await getContractPageMetadata(
info.contract,
);
@@ -0,0 +1,17 @@
import { Spinner } from "@/components/ui/Spinner/Spinner";
export function LoadingPage() {
return (
<div className="flex min-h-[300px] grow items-center justify-center">
<Spinner className="size-10" />
</div>
);
}
export function ErrorPage() {
return (
<div className="flex min-h-[300px] grow items-center justify-center">
<p className="text-destructive-text">Failed to load contract</p>
</div>
);
}
@@ -0,0 +1,24 @@
"use client";
import { useDashboardRouter } from "@/lib/DashboardRouter";
import { useEffect, useRef } from "react";
import type { ThirdwebContract } from "thirdweb";
import { LoadingPage } from "./page-skeletons";
export function RedirectToContractOverview(props: {
contract: ThirdwebContract;
}) {
const router = useDashboardRouter();
const redirected = useRef(false);
// eslint-disable-next-line no-restricted-syntax
useEffect(() => {
if (redirected.current) {
return;
}
redirected.current = true;
router.replace(`/${props.contract.chain.id}/${props.contract.address}`);
}, [router, props.contract]);
return <LoadingPage />;
}
@@ -0,0 +1,18 @@
"use client";
import { useQuery } from "@tanstack/react-query";
import type { ThirdwebContract } from "thirdweb";
import { getContractPageMetadataSetup } from "../_utils/getContractPageMetadata";
export function useContractPageMetadata(contract: ThirdwebContract) {
return useQuery({
queryKey: ["getContractPageMetadataSetup", contract],
queryFn: () => {
return getContractPageMetadataSetup(contract, () =>
Promise.resolve(false),
);
},
retry: false,
refetchOnWindowFocus: false,
});
}
@@ -0,0 +1,13 @@
import { useQuery } from "@tanstack/react-query";
import { type ThirdwebContract, resolveContractAbi } from "thirdweb/contract";
export function useResolveContractABI(contract: ThirdwebContract) {
return useQuery({
queryKey: ["resolveContractAbi", contract],
queryFn: () => {
return resolveContractAbi(contract);
},
retry: false,
refetchOnWindowFocus: false,
});
}
@@ -4,8 +4,8 @@ import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { Button } from "@/components/ui/button";
import { CheckIcon, CircleAlertIcon, RotateCcwIcon } from "lucide-react";
import { useState } from "react";
import { ConfigureNetworks } from "../../../../../components/configure-networks/ConfigureNetworks";
import { addChainOverrides } from "../../../../../stores/chainStores";
import { addChainOverrides } from "stores/chainStores";
import { ConfigureNetworks } from "../../../../../../components/configure-networks/ConfigureNetworks";
export function ConfigureCustomChain(props: {
chainSlug: string;
@@ -1,5 +1,8 @@
import { getThirdwebClient } from "@/constants/thirdweb.server";
import { fetchDashboardContractMetadata } from "@3rdweb-sdk/react/hooks/useDashboardContractMetadata";
import {
type DashboardContractMetadata,
fetchDashboardContractMetadata,
} from "@3rdweb-sdk/react/hooks/useDashboardContractMetadata";
import { fetchPublishedContractsFromDeploy } from "components/contract-components/fetchPublishedContractsFromDeploy";
import type { ThirdwebContract } from "thirdweb";
import type { ChainMetadata } from "thirdweb/chains";
@@ -8,12 +11,34 @@ import { MetadataHeader } from "./metadata-header";
interface ContractMetadataProps {
contract: ThirdwebContract;
chain: ChainMetadata;
contractMetadata: DashboardContractMetadata | undefined;
externalLinks:
| {
name: string;
url: string;
}[]
| undefined;
}
export async function ContractMetadata({
export function ContractMetadata({
contract,
chain,
contractMetadata,
externalLinks,
}: ContractMetadataProps) {
return (
<MetadataHeader
data={contractMetadata}
chain={chain}
address={contract.address}
externalLinks={externalLinks}
/>
);
}
export async function getContractMetadataHeaderData(
contract: ThirdwebContract,
) {
const settledResults = await Promise.allSettled([
fetchDashboardContractMetadata(contract),
fetchPublishedContractsFromDeploy({
@@ -34,12 +59,8 @@ export async function ContractMetadata({
const latestPublished = publishedContractsFromDeploy?.slice(-1)[0];
return (
<MetadataHeader
data={contractMetadata}
chain={chain}
address={contract.address}
externalLinks={latestPublished?.externalLinks}
/>
);
return {
contractMetadata,
externalLinks: latestPublished?.externalLinks,
};
}
@@ -0,0 +1,49 @@
"use client";
import { useQuery } from "@tanstack/react-query";
import type { ThirdwebContract } from "thirdweb";
import type { ChainMetadata } from "thirdweb/chains";
import { ErrorPage, LoadingPage } from "../_components/page-skeletons";
import { useContractPageMetadata } from "../_hooks/useContractPageMetadata";
import { getContractPageSidebarLinks } from "../_utils/getContractPageSidebarLinks";
import { getContractMetadataHeaderData } from "./contract-metadata";
import { ContractPageLayout } from "./contract-page-layout";
export function ContractPageLayoutClient(props: {
chainMetadata: ChainMetadata;
contract: ThirdwebContract;
children: React.ReactNode;
}) {
const metadataQuery = useContractPageMetadata(props.contract);
const headerMetadataQuery = useQuery({
queryKey: ["getContractMetadataHeaderData", props.contract],
queryFn: async () => {
return await getContractMetadataHeaderData(props.contract);
},
retry: false,
refetchOnWindowFocus: false,
});
if (metadataQuery.isPending) {
return <LoadingPage />;
}
if (metadataQuery.isError) {
return <ErrorPage />;
}
const sidebarLinks = getContractPageSidebarLinks({
chainSlug: props.chainMetadata.slug,
contractAddress: props.contract.address,
metadata: metadataQuery.data,
});
return (
<ContractPageLayout
{...props}
sidebarLinks={sidebarLinks}
dashboardContractMetadata={headerMetadataQuery.data?.contractMetadata}
externalLinks={headerMetadataQuery.data?.externalLinks}
/>
);
}
@@ -0,0 +1,62 @@
import { ChakraProviderSetup } from "@/components/ChakraProviderSetup";
import type { SidebarLink } from "@/components/blocks/Sidebar";
import { SidebarLayout } from "@/components/blocks/SidebarLayout";
import type { DashboardContractMetadata } from "@3rdweb-sdk/react/hooks/useDashboardContractMetadata";
import { DeprecatedAlert } from "components/shared/DeprecatedAlert";
import type { ThirdwebContract } from "thirdweb";
import type { ChainMetadata } from "thirdweb/chains";
import { ContractMetadata } from "./contract-metadata";
import { PrimaryDashboardButton } from "./primary-dashboard-button";
export function ContractPageLayout(props: {
chainMetadata: ChainMetadata;
contract: ThirdwebContract;
children: React.ReactNode;
sidebarLinks: SidebarLink[];
dashboardContractMetadata: DashboardContractMetadata | undefined;
externalLinks:
| {
name: string;
url: string;
}[]
| undefined;
}) {
const {
chainMetadata,
contract,
sidebarLinks,
dashboardContractMetadata,
externalLinks,
} = props;
return (
<ChakraProviderSetup>
<SidebarLayout sidebarLinks={sidebarLinks}>
<div className="border-border border-b pb-8">
<div className="flex flex-col gap-4 ">
<div className="flex flex-col justify-between gap-4 md:flex-row">
<ContractMetadata
contract={contract}
chain={chainMetadata}
contractMetadata={dashboardContractMetadata}
externalLinks={externalLinks}
/>
<PrimaryDashboardButton
contractAddress={contract.address}
chain={contract.chain}
contractInfo={{
chain: chainMetadata,
chainSlug: chainMetadata.slug,
contractAddress: contract.address,
}}
/>
</div>
<DeprecatedAlert chain={chainMetadata} />
</div>
</div>
<div className="h-8" />
<div className="pb-10">{props.children}</div>
</SidebarLayout>
</ChakraProviderSetup>
);
}
@@ -20,27 +20,25 @@ export async function getContractPageParamsInfo(params: {
// move on
}
if (!chainMetadata) {
const cookieStore = cookies();
const localChainStoreValue = cookieStore.get(TW_LOCAL_CHAIN_STORE)?.value;
const cookieStore = cookies();
const localChainStoreValue = cookieStore.get(TW_LOCAL_CHAIN_STORE)?.value;
if (localChainStoreValue) {
try {
const chains = JSON.parse(decodeURIComponent(localChainStoreValue));
if (typeof chains === "object" && Array.isArray(chains)) {
const chainOverrides = chains as ChainMetadata[];
const chain = chainOverrides.find(
(c) =>
c.slug === chainSlugOrId ||
c.chainId === Number.parseInt(chainSlugOrId),
);
if (chain) {
chainMetadata = chain;
}
if (localChainStoreValue) {
try {
const chains = JSON.parse(decodeURIComponent(localChainStoreValue));
if (typeof chains === "object" && Array.isArray(chains)) {
const chainOverrides = chains as ChainMetadata[];
const chain = chainOverrides.find(
(c) =>
c.slug === chainSlugOrId ||
c.chainId === Number.parseInt(chainSlugOrId),
);
if (chain) {
chainMetadata = chain;
}
} catch {
// noop
}
} catch {
// noop
}
}
@@ -46,8 +46,13 @@ export type ContractPageMetadata = {
functionSelectors: string[];
};
export async function getContractPageMetadata(
export async function getContractPageMetadata(contract: ThirdwebContract) {
return getContractPageMetadataSetup(contract, isAnalyticsSupportedForChain);
}
export async function getContractPageMetadataSetup(
contract: ThirdwebContract,
isAnalyticsSupportedFn: (chainId: number) => Promise<boolean>,
): Promise<ContractPageMetadata> {
const [
functionSelectorsResult,
@@ -55,7 +60,7 @@ export async function getContractPageMetadata(
contractTypeResult,
] = await Promise.allSettled([
resolveFunctionSelectors(contract),
isAnalyticsSupportedForChain(contract.chain.id),
isAnalyticsSupportedFn(contract.chain.id),
getContractType({ contract }),
]);
@@ -0,0 +1,27 @@
"use client";
import type { ThirdwebContract } from "thirdweb";
import { ErrorPage, LoadingPage } from "../_components/page-skeletons";
import { RedirectToContractOverview } from "../_components/redirect-contract-overview.client";
import { useContractPageMetadata } from "../_hooks/useContractPageMetadata";
import { AccountSigners } from "./components/account-signers";
export function AccountSignersClient(props: {
contract: ThirdwebContract;
}) {
const metadataQuery = useContractPageMetadata(props.contract);
if (metadataQuery.isPending) {
return <LoadingPage />;
}
if (metadataQuery.isError) {
return <ErrorPage />;
}
if (!metadataQuery.data.isAccountPermissionsSupported) {
return <RedirectToContractOverview contract={props.contract} />;
}
return <AccountSigners contract={props.contract} />;
}
@@ -31,14 +31,19 @@ export const AccountSigners: React.FC<AccountSignersProps> = ({ contract }) => {
);
const data = transformedAdmins.concat(transformedSigners || []);
return (
<div className="flex flex-col gap-6">
{data.map((item) => (
<AccountSigner
key={item.signer}
item={item}
contractChainId={contract.chain.id}
/>
))}
<div>
<h1 className="mb-4 font-semibold text-2xl tracking-tight">
Account Signers
</h1>
<div className="flex flex-col gap-6">
{data.map((item) => (
<AccountSigner
key={item.signer}
item={item}
contractChainId={contract.chain.id}
/>
))}
</div>
</div>
);
};
@@ -1,6 +1,8 @@
import { notFound, redirect } from "next/navigation";
import { localhost } from "thirdweb/chains";
import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
import { getContractPageMetadata } from "../_utils/getContractPageMetadata";
import { AccountSignersClient } from "./AccountSigners.client";
import { AccountSigners } from "./components/account-signers";
export default async function Page(props: {
@@ -15,7 +17,12 @@ export default async function Page(props: {
notFound();
}
const { contract } = info;
const { contract, chainMetadata } = info;
if (chainMetadata.chainId === localhost.id) {
return <AccountSignersClient contract={contract} />;
}
const { isAccountPermissionsSupported } =
await getContractPageMetadata(contract);
@@ -23,12 +30,5 @@ export default async function Page(props: {
redirect(`/${props.params.chain_id}/${props.params.contractAddress}`);
}
return (
<div>
<h1 className="mb-4 font-semibold text-2xl tracking-tight">
Account Signers
</h1>
<AccountSigners contract={contract} />
</div>
);
return <AccountSigners contract={contract} />;
}
@@ -0,0 +1,34 @@
"use client";
import type { ThirdwebContract } from "thirdweb";
import type { ChainMetadata } from "thirdweb/chains";
import { ErrorPage, LoadingPage } from "../_components/page-skeletons";
import { RedirectToContractOverview } from "../_components/redirect-contract-overview.client";
import { useContractPageMetadata } from "../_hooks/useContractPageMetadata";
import { AccountPage } from "./AccountPage";
export function AccountPageClient(props: {
contract: ThirdwebContract;
chainMetadata: ChainMetadata;
}) {
const metadataQuery = useContractPageMetadata(props.contract);
if (metadataQuery.isPending) {
return <LoadingPage />;
}
if (metadataQuery.isError) {
return <ErrorPage />;
}
if (!metadataQuery.data.isAccount) {
return <RedirectToContractOverview contract={props.contract} />;
}
return (
<AccountPage
contract={props.contract}
chainMetadata={props.chainMetadata}
/>
);
}
@@ -1,7 +1,9 @@
import { notFound, redirect } from "next/navigation";
import { localhost } from "thirdweb/chains";
import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
import { getContractPageMetadata } from "../_utils/getContractPageMetadata";
import { AccountPage } from "./AccountPage";
import { AccountPageClient } from "./AccountPage.client";
export default async function Page(props: {
params: {
@@ -16,6 +18,12 @@ export default async function Page(props: {
}
const { contract, chainMetadata } = info;
if (contract.chain.id === localhost.id) {
return (
<AccountPageClient contract={contract} chainMetadata={chainMetadata} />
);
}
const { isAccount } = await getContractPageMetadata(contract);
if (!isAccount) {
@@ -0,0 +1,27 @@
"use client";
import type { ThirdwebContract } from "thirdweb";
import { ErrorPage, LoadingPage } from "../_components/page-skeletons";
import { RedirectToContractOverview } from "../_components/redirect-contract-overview.client";
import { useContractPageMetadata } from "../_hooks/useContractPageMetadata";
import { AccountsPage } from "./AccountsPage";
export function AccountsPageClient(props: {
contract: ThirdwebContract;
}) {
const metadataQuery = useContractPageMetadata(props.contract);
if (metadataQuery.isPending) {
return <LoadingPage />;
}
if (metadataQuery.isError) {
return <ErrorPage />;
}
if (!metadataQuery.data.isAccountFactory) {
return <RedirectToContractOverview contract={props.contract} />;
}
return <AccountsPage contract={props.contract} />;
}
@@ -1,7 +1,9 @@
import { notFound, redirect } from "next/navigation";
import { localhost } from "thirdweb/chains";
import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
import { getContractPageMetadata } from "../_utils/getContractPageMetadata";
import { AccountsPage } from "./AccountsPage";
import { AccountsPageClient } from "./AccountsPage.client";
export default async function Page(props: {
params: {
@@ -15,7 +17,12 @@ export default async function Page(props: {
notFound();
}
const { contract } = info;
const { contract, chainMetadata } = info;
if (chainMetadata.chainId === localhost.id) {
return <AccountsPageClient contract={contract} />;
}
const { isAccountFactory } = await getContractPageMetadata(contract);
if (!isAccountFactory) {
@@ -0,0 +1,27 @@
"use client";
import type { ThirdwebContract } from "thirdweb";
import { ErrorPage, LoadingPage } from "../_components/page-skeletons";
import { RedirectToContractOverview } from "../_components/redirect-contract-overview.client";
import { useContractPageMetadata } from "../_hooks/useContractPageMetadata";
import { ContractAnalyticsPage } from "./ContractAnalyticsPage";
export function ContractAnalyticsPageClient(props: {
contract: ThirdwebContract;
}) {
const metadataQuery = useContractPageMetadata(props.contract);
if (metadataQuery.isPending) {
return <LoadingPage />;
}
if (metadataQuery.isError) {
return <ErrorPage />;
}
if (!metadataQuery.data.isAnalyticsSupported) {
return <RedirectToContractOverview contract={props.contract} />;
}
return <ContractAnalyticsPage contract={props.contract} />;
}
@@ -1,7 +1,9 @@
import { notFound, redirect } from "next/navigation";
import { localhost } from "thirdweb/chains";
import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
import { getContractPageMetadata } from "../_utils/getContractPageMetadata";
import { ContractAnalyticsPage } from "./ContractAnalyticsPage";
import { ContractAnalyticsPageClient } from "./ContractAnalyticsPage.client";
export default async function Page(props: {
params: {
@@ -15,6 +17,10 @@ export default async function Page(props: {
notFound();
}
if (info.chainMetadata.chainId === localhost.id) {
return <ContractAnalyticsPageClient contract={info.contract} />;
}
const { isAnalyticsSupported } = await getContractPageMetadata(info.contract);
if (!isAnalyticsSupported) {
@@ -0,0 +1,38 @@
"use client";
import type { ThirdwebContract } from "thirdweb";
import { ClaimConditions } from "../_components/claim-conditions/claim-conditions";
import { ErrorPage, LoadingPage } from "../_components/page-skeletons";
import { RedirectToContractOverview } from "../_components/redirect-contract-overview.client";
import { useContractPageMetadata } from "../_hooks/useContractPageMetadata";
export function ClaimConditionsClient(props: {
contract: ThirdwebContract;
}) {
const metadataQuery = useContractPageMetadata(props.contract);
if (metadataQuery.isPending) {
return <LoadingPage />;
}
if (metadataQuery.isError) {
return <ErrorPage />;
}
const {
isERC20ClaimConditionsSupported,
isERC721ClaimConditionsSupported,
supportedERCs,
} = metadataQuery.data;
if (!isERC20ClaimConditionsSupported && !isERC721ClaimConditionsSupported) {
return <RedirectToContractOverview contract={props.contract} />;
}
return (
<ClaimConditions
contract={props.contract}
isERC20={supportedERCs.isERC20}
/>
);
}
@@ -1,7 +1,9 @@
import { notFound, redirect } from "next/navigation";
import { localhost } from "thirdweb/chains";
import { ClaimConditions } from "../_components/claim-conditions/claim-conditions";
import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
import { getContractPageMetadata } from "../_utils/getContractPageMetadata";
import { ClaimConditionsClient } from "./ClaimConditions.client";
export default async function Page(props: {
params: {
@@ -14,8 +16,12 @@ export default async function Page(props: {
if (!info) {
notFound();
}
const { contract, chainMetadata } = info;
if (chainMetadata.chainId === localhost.id) {
return <ClaimConditionsClient contract={contract} />;
}
const { contract } = info;
const {
isERC20ClaimConditionsSupported,
isERC721ClaimConditionsSupported,
@@ -0,0 +1,26 @@
"use client";
import type { ChainMetadata } from "thirdweb/chains";
import type { ThirdwebContract } from "thirdweb/contract";
import { LoadingPage } from "../_components/page-skeletons";
import { useResolveContractABI } from "../_hooks/useResolveContractABI";
import { ContractCodePage } from "./contract-code-page";
export function ContractCodePageClient(props: {
contract: ThirdwebContract;
chainMetadata: ChainMetadata;
}) {
const abiQuery = useResolveContractABI(props.contract);
if (abiQuery.isPending) {
return <LoadingPage />;
}
return (
<ContractCodePage
abi={abiQuery.data}
contract={props.contract}
chainMetadata={props.chainMetadata}
/>
);
}
@@ -0,0 +1,33 @@
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import type { Abi } from "abitype";
import { CircleAlertIcon } from "lucide-react";
import type { ChainMetadata } from "thirdweb/chains";
import type { ThirdwebContract } from "thirdweb/contract";
import { CodeOverview } from "../../../../../../contract-ui/tabs/code/components/code-overview";
export function ContractCodePage(props: {
abi: Abi | undefined;
contract: ThirdwebContract;
chainMetadata: ChainMetadata;
}) {
if (props.abi) {
return (
<CodeOverview
abi={props.abi}
contractAddress={props.contract.address}
chainId={props.contract.chain.id}
/>
);
}
return (
<Alert variant="destructive">
<CircleAlertIcon className="size-5" />
<AlertTitle>Failed to resolve contract ABI</AlertTitle>
<AlertDescription>
Please verify that contract address is correct and deployed on "
{props.chainMetadata.name}" chain.
</AlertDescription>
</Alert>
);
}
@@ -1,9 +1,9 @@
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { CircleAlertIcon } from "lucide-react";
import { notFound } from "next/navigation";
import { localhost } from "thirdweb/chains";
import { resolveContractAbi } from "thirdweb/contract";
import { CodeOverview } from "../../../../../../contract-ui/tabs/code/components/code-overview";
import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
import { ContractCodePage } from "./contract-code-page";
import { ContractCodePageClient } from "./contract-code-page.client";
export default async function Page(props: {
params: {
@@ -19,26 +19,22 @@ export default async function Page(props: {
const { contract, chainMetadata } = info;
try {
const abi = await resolveContractAbi(contract);
if (contract.chain.id === localhost.id) {
return (
<CodeOverview
abi={abi}
contractAddress={contract.address}
chainId={contract.chain.id}
<ContractCodePageClient
contract={contract}
chainMetadata={chainMetadata}
/>
);
} catch {
return (
<Alert variant="destructive">
<CircleAlertIcon className="size-5" />
<AlertTitle>Failed to resolve contract ABI</AlertTitle>
<AlertDescription>
Please verify that contract address is correct and deployed on "
{chainMetadata.name}" chain.
</AlertDescription>
</Alert>
);
}
const abi = await resolveContractAbi(contract).then(() => undefined);
return (
<ContractCodePage
abi={abi}
contract={contract}
chainMetadata={chainMetadata}
/>
);
}
@@ -0,0 +1,32 @@
"use client";
import type { ThirdwebContract } from "thirdweb";
import { ErrorPage, LoadingPage } from "../_components/page-skeletons";
import { RedirectToContractOverview } from "../_components/redirect-contract-overview.client";
import { useContractPageMetadata } from "../_hooks/useContractPageMetadata";
import { EmbedSetup } from "./embed-setup";
export function EmbedSetupClient(props: {
contract: ThirdwebContract;
}) {
const metadataQuery = useContractPageMetadata(props.contract);
if (metadataQuery.isPending) {
return <LoadingPage />;
}
if (metadataQuery.isError) {
return <ErrorPage />;
}
if (metadataQuery.data.embedType === null) {
return <RedirectToContractOverview contract={props.contract} />;
}
return (
<EmbedSetup
contract={props.contract}
ercOrMarketplace={metadataQuery.data.embedType}
/>
);
}
@@ -1,19 +1,22 @@
"use client";
import { Button } from "@/components/ui/button";
import { useApiKeys, useCreateApiKey } from "@3rdweb-sdk/react/hooks/useApi";
import { Flex, FormControl, Input, Link, Select } from "@chakra-ui/react";
import { Flex, FormControl, Input, Select } from "@chakra-ui/react";
import { LazyCreateAPIKeyDialog } from "components/settings/ApiKeys/Create/LazyCreateAPIKeyDialog";
import { useTrack } from "hooks/analytics/useTrack";
import { useAllChainsData } from "hooks/chains/allChains";
import { useClipboard } from "hooks/useClipboard";
import { useTxNotifications } from "hooks/useTxNotifications";
import { CheckIcon, CopyIcon } from "lucide-react";
import { useMemo } from "react";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { useMemo, useState } from "react";
import { useForm } from "react-hook-form";
import type { StoredChain } from "stores/chainStores";
import type { ThirdwebContract } from "thirdweb";
import type { ChainMetadata } from "thirdweb/chains";
import { useActiveAccount } from "thirdweb/react";
import {
Button,
Card,
CodeBlock,
FormHelperText,
@@ -211,10 +214,6 @@ export const EmbedSetup: React.FC<EmbedSetupProps> = ({
const apiKeys = useApiKeys();
const createKeyMutation = useCreateApiKey();
const { onSuccess, onError } = useTxNotifications(
"API key created",
"Failed to create API key",
);
const validApiKey = (apiKeys.data || []).find(
(apiKey) =>
@@ -298,14 +297,35 @@ export const EmbedSetup: React.FC<EmbedSetupProps> = ({
);
const { hasCopied, onCopy } = useClipboard(embedCode, 3000);
const [showCreateAPIKeyModal, setShowCreateAPIKeyModal] = useState(false);
const activeAccount = useActiveAccount();
const pathname = usePathname();
return (
<Flex gap={8} direction="column">
<LazyCreateAPIKeyDialog
wording="api-key"
prefill={{
name: "Embed API Key",
domains: "embed.ipfscdn.io",
}}
open={showCreateAPIKeyModal}
onOpenChange={setShowCreateAPIKeyModal}
onCreateAndComplete={() => {
trackEvent({
category: "api-keys",
action: "create",
label: "success",
fromEmbed: true,
});
apiKeys.refetch();
}}
/>
<Flex gap={8} direction={{ base: "column", md: "row" }}>
<Card className="flex w-full flex-col gap-2 md:w-1/2">
<Heading size="title.sm" mb={4}>
Configuration
</Heading>
<Card className="flex w-full flex-col gap-5 md:w-1/2">
<Heading size="title.sm">Configuration</Heading>
{ercOrMarketplace === "marketplace" ? (
<FormControl>
<FormLabel>Listing ID</FormLabel>
@@ -315,6 +335,7 @@ export const EmbedSetup: React.FC<EmbedSetupProps> = ({
</FormHelperText>
</FormControl>
) : null}
{ercOrMarketplace === "marketplace-v3" ? (
<FormControl>
<FormLabel>Listing type</FormLabel>
@@ -327,6 +348,7 @@ export const EmbedSetup: React.FC<EmbedSetupProps> = ({
</FormHelperText>
</FormControl>
) : null}
{ercOrMarketplace === "marketplace-v3" &&
watch("listingType") === "direct-listing" ? (
<FormControl>
@@ -337,6 +359,7 @@ export const EmbedSetup: React.FC<EmbedSetupProps> = ({
</FormHelperText>
</FormControl>
) : null}
{ercOrMarketplace === "marketplace-v3" &&
watch("listingType") === "english-auction" ? (
<FormControl>
@@ -347,6 +370,7 @@ export const EmbedSetup: React.FC<EmbedSetupProps> = ({
</FormHelperText>
</FormControl>
) : null}
{ercOrMarketplace === "erc1155" ? (
<FormControl>
<FormLabel>Token ID</FormLabel>
@@ -356,9 +380,20 @@ export const EmbedSetup: React.FC<EmbedSetupProps> = ({
</FormHelperText>
</FormControl>
) : null}
<FormControl>
<FormLabel>Client ID</FormLabel>
{validApiKey ? (
{!activeAccount ? (
<Button asChild className="w-full">
<Link
href={`/login${
pathname ? `?next=${encodeURIComponent(pathname)}` : ""
}`}
>
Sign in to create a client ID
</Link>
</Button>
) : validApiKey ? (
<Input
readOnly
disabled
@@ -366,11 +401,7 @@ export const EmbedSetup: React.FC<EmbedSetupProps> = ({
/>
) : (
<Button
bgColor="bgBlack"
color="bgWhite"
_hover={{
opacity: 0.8,
}}
className="w-full"
onClick={() => {
trackEvent({
category: "api-keys",
@@ -379,44 +410,7 @@ export const EmbedSetup: React.FC<EmbedSetupProps> = ({
fromEmbed: true,
});
createKeyMutation.mutate(
{
name: "Embed API key",
domains: ["embed.ipfscdn.io"],
services: [
{
name: "rpc",
targetAddresses: ["*"],
},
{
name: "storage",
targetAddresses: ["*"],
actions: ["read"],
},
],
},
{
onSuccess: () => {
onSuccess();
trackEvent({
category: "api-keys",
action: "create",
label: "success",
fromEmbed: true,
});
},
onError: (err) => {
onError(err);
trackEvent({
category: "api-keys",
action: "create",
label: "error",
error: err,
fromEmbed: true,
});
},
},
);
setShowCreateAPIKeyModal(true);
}}
disabled={createKeyMutation.isPending}
>
@@ -428,14 +422,15 @@ export const EmbedSetup: React.FC<EmbedSetupProps> = ({
You need a client ID to use embeds.{" "}
<Link
href="https://portal.thirdweb.com/account/api-keys"
color="primary.500"
isExternal
className="text-link-foreground hover:text-foreground"
target="_blank"
>
Learn more
</Link>
.
</FormHelperText>
</FormControl>
<FormControl>
<FormLabel>RPC Url</FormLabel>
<Input type="url" {...register("rpcUrl")} />
@@ -477,8 +472,8 @@ export const EmbedSetup: React.FC<EmbedSetupProps> = ({
A relayer can be used to make the transaction gasless for the
end user.{" "}
<Link
isExternal
color="blue.500"
target="_blank"
className="text-link-foreground hover:text-foreground"
href="https://blog.thirdweb.com/guides/setup-gasless-transactions"
>
Learn more
@@ -486,6 +481,7 @@ export const EmbedSetup: React.FC<EmbedSetupProps> = ({
</FormHelperText>
</FormControl>
)}
<FormControl>
<Heading size="title.sm" my={4}>
Customization
@@ -501,6 +497,7 @@ export const EmbedSetup: React.FC<EmbedSetupProps> = ({
on the user system&apos;s preferences.
</FormHelperText>
</FormControl>
<FormControl>
<FormLabel>Primary Color</FormLabel>
<Select {...register("primaryColor")}>
@@ -517,6 +514,7 @@ export const EmbedSetup: React.FC<EmbedSetupProps> = ({
Used for the main actions button backgrounds.
</FormHelperText>
</FormControl>
{ercOrMarketplace === "marketplace" ||
ercOrMarketplace === "marketplace-v3" ? (
<FormControl>
@@ -535,6 +533,7 @@ export const EmbedSetup: React.FC<EmbedSetupProps> = ({
</FormControl>
) : null}
</Card>
<Card className="flex w-full flex-col gap-2 md:w-1/2">
<Heading size="title.sm">Embed Code</Heading>
<CodeBlock
@@ -545,8 +544,7 @@ export const EmbedSetup: React.FC<EmbedSetupProps> = ({
language="markup"
/>
<Button
colorScheme="purple"
w="auto"
className="w-auto gap-2"
variant="outline"
onClick={() => {
onCopy();
@@ -558,14 +556,12 @@ export const EmbedSetup: React.FC<EmbedSetupProps> = ({
chainId,
});
}}
leftIcon={
hasCopied ? (
<CheckIcon className="size-4" />
) : (
<CopyIcon className="size-4" />
)
}
>
{hasCopied ? (
<CheckIcon className="size-4" />
) : (
<CopyIcon className="size-4" />
)}
{hasCopied ? "Copied!" : "Copy to clipboard"}
</Button>
</Card>
@@ -1,6 +1,8 @@
import { notFound, redirect } from "next/navigation";
import { localhost } from "thirdweb/chains";
import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
import { getContractPageMetadata } from "../_utils/getContractPageMetadata";
import { EmbedSetupClient } from "./EmbedSetup.client";
import { EmbedSetup } from "./embed-setup";
export default async function Page(props: {
@@ -15,11 +17,16 @@ export default async function Page(props: {
notFound();
}
const { embedType } = await getContractPageMetadata(info.contract);
const { contract } = info;
if (contract.chain.id === localhost.id) {
return <EmbedSetupClient contract={contract} />;
}
const { embedType } = await getContractPageMetadata(contract);
if (embedType === null) {
redirect(`/${props.params.chain_id}/${props.params.contractAddress}`);
}
return <EmbedSetup contract={info.contract} ercOrMarketplace={embedType} />;
return <EmbedSetup contract={contract} ercOrMarketplace={embedType} />;
}
@@ -0,0 +1,31 @@
"use client";
import type { ThirdwebContract } from "thirdweb";
import type { ChainMetadata } from "thirdweb/chains";
import { LoadingPage } from "../_components/page-skeletons";
import { useResolveContractABI } from "../_hooks/useResolveContractABI";
import { ContractExplorerPage } from "./ContractExplorerPage";
interface ContractExplorePageProps {
contract: ThirdwebContract;
chainMetadata: ChainMetadata;
}
export const ContractExplorerPageClient: React.FC<ContractExplorePageProps> = ({
contract,
chainMetadata,
}) => {
const abiQuery = useResolveContractABI(contract);
if (abiQuery.isPending) {
return <LoadingPage />;
}
return (
<ContractExplorerPage
abi={abiQuery.data}
contract={contract}
chainMetadata={chainMetadata}
/>
);
};
@@ -1,21 +1,36 @@
"use client";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { Flex } from "@chakra-ui/react";
import type { Abi } from "abitype";
import { getContractFunctionsFromAbi } from "components/contract-components/getContractFunctionsFromAbi";
import { ContractFunctionsOverview } from "components/contract-functions/contract-functions";
import { CircleAlertIcon } from "lucide-react";
import type { ThirdwebContract } from "thirdweb";
// TODO - figure out why we have to add "use client" here and it does not work without it
import type { ChainMetadata } from "thirdweb/chains";
interface ContractExplorePageProps {
contract: ThirdwebContract;
abi: Abi;
abi: Abi | undefined;
chainMetadata: ChainMetadata;
}
export const ContractExplorerPage: React.FC<ContractExplorePageProps> = ({
contract,
abi,
chainMetadata,
}) => {
if (!abi) {
return (
<Alert variant="destructive">
<CircleAlertIcon className="size-5" />
<AlertTitle>Failed to resolve contract ABI</AlertTitle>
<AlertDescription>
Please verify that contract address is correct and deployed on "
{chainMetadata.name}" chain.
</AlertDescription>
</Alert>
);
}
const functions = getContractFunctionsFromAbi(abi);
return (
<Flex direction="column" h="70vh">
@@ -1,9 +1,9 @@
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { CircleAlertIcon } from "lucide-react";
import { notFound } from "next/navigation";
import { localhost } from "thirdweb/chains";
import { resolveContractAbi } from "thirdweb/contract";
import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
import { ContractExplorerPage } from "./ContractExplorerPage";
import { ContractExplorerPageClient } from "./ContractExplorerPage.client";
export default async function Page(props: {
params: {
@@ -19,19 +19,22 @@ export default async function Page(props: {
const { contract, chainMetadata } = info;
try {
const abi = await resolveContractAbi(contract);
return <ContractExplorerPage contract={contract} abi={abi} />;
} catch {
if (contract.chain.id === localhost.id) {
return (
<Alert variant="destructive">
<CircleAlertIcon className="size-5" />
<AlertTitle>Failed to resolve contract ABI</AlertTitle>
<AlertDescription>
Please verify that contract address is correct and deployed on "
{chainMetadata.name}" chain.
</AlertDescription>
</Alert>
<ContractExplorerPageClient
contract={contract}
chainMetadata={chainMetadata}
/>
);
}
const abi = await resolveContractAbi(contract).catch(() => undefined);
return (
<ContractExplorerPage
contract={contract}
abi={abi}
chainMetadata={chainMetadata}
/>
);
}
@@ -1,15 +1,14 @@
import { ChakraProviderSetup } from "@/components/ChakraProviderSetup";
import { SidebarLayout } from "@/components/blocks/SidebarLayout";
import { ContractMetadata } from "components/custom-contract/contract-header/contract-metadata";
import { DeprecatedAlert } from "components/shared/DeprecatedAlert";
import type { Metadata } from "next";
import { notFound } from "next/navigation";
import { localhost } from "thirdweb/chains";
import { getContractMetadata } from "thirdweb/extensions/common";
import { isAddress, isContractDeployed } from "thirdweb/utils";
import { resolveFunctionSelectors } from "../../../../../lib/selectors";
import { shortenIfAddress } from "../../../../../utils/usedapp-external";
import { ConfigureCustomChain } from "./ConfigureCustomChain";
import { PrimaryDashboardButton } from "./_components/primary-dashboard-button";
import { ConfigureCustomChain } from "./_layout/ConfigureCustomChain";
import { getContractMetadataHeaderData } from "./_layout/contract-metadata";
import { ContractPageLayout } from "./_layout/contract-page-layout";
import { ContractPageLayoutClient } from "./_layout/contract-page-layout.client";
import { supportedERCs } from "./_utils/detectedFeatures/supportedERCs";
import { getContractPageParamsInfo } from "./_utils/getContractFromParams";
import { getContractPageMetadata } from "./_utils/getContractPageMetadata";
@@ -38,44 +37,44 @@ export default async function Layout(props: {
notFound();
}
if (contract.chain.id === localhost.id) {
return (
<ContractPageLayoutClient
chainMetadata={chainMetadata}
contract={contract}
>
{props.children}
</ContractPageLayoutClient>
);
}
// check if the contract exists
const isValidContract = await isContractDeployed(contract);
const isValidContract = await isContractDeployed(contract).catch(() => false);
if (!isValidContract) {
// TODO - replace 404 with a better page to upsale deploy or other thirdweb products
notFound();
}
const contractPageMetadata = await getContractPageMetadata(contract);
const sidebarLinks = getContractPageSidebarLinks({
chainSlug: chainMetadata.slug,
contractAddress: contract.address,
metadata: contractPageMetadata,
});
const { contractMetadata, externalLinks } =
await getContractMetadataHeaderData(contract);
return (
<ChakraProviderSetup>
<SidebarLayout sidebarLinks={sidebarLinks}>
<div className="border-border border-b pb-8">
<div className="flex flex-col gap-4 ">
<div className="flex flex-col justify-between gap-4 md:flex-row">
<ContractMetadata contract={contract} chain={chainMetadata} />
<PrimaryDashboardButton
contractAddress={contract.address}
chain={contract.chain}
contractInfo={{
chain: chainMetadata,
chainSlug: chainMetadata.slug,
contractAddress: contract.address,
}}
/>
</div>
<DeprecatedAlert chain={chainMetadata} />
</div>
</div>
<div className="h-8" />
<div className="pb-10">{props.children}</div>
</SidebarLayout>
</ChakraProviderSetup>
<ContractPageLayout
chainMetadata={chainMetadata}
contract={contract}
sidebarLinks={sidebarLinks}
dashboardContractMetadata={contractMetadata}
externalLinks={externalLinks}
>
{props.children}
</ContractPageLayout>
);
}
@@ -0,0 +1,27 @@
"use client";
import type { ThirdwebContract } from "thirdweb";
import { ErrorPage, LoadingPage } from "../_components/page-skeletons";
import { RedirectToContractOverview } from "../_components/redirect-contract-overview.client";
import { useContractPageMetadata } from "../_hooks/useContractPageMetadata";
import { ContractEditModulesPage } from "./ContractEditModulesPage";
export function ContractEditModulesPageClient(props: {
contract: ThirdwebContract;
}) {
const metadataQuery = useContractPageMetadata(props.contract);
if (metadataQuery.isPending) {
return <LoadingPage />;
}
if (metadataQuery.isError) {
return <ErrorPage />;
}
if (!metadataQuery.data.isModularCore) {
return <RedirectToContractOverview contract={props.contract} />;
}
return <ContractEditModulesPage contract={props.contract} />;
}
@@ -1,35 +1,11 @@
"use client";
import { WalletAddress } from "@/components/blocks/wallet-address";
import { CopyAddressButton } from "@/components/ui/CopyAddressButton";
import { ScrollShadow } from "@/components/ui/ScrollShadow/ScrollShadow";
import { Spinner } from "@/components/ui/Spinner/Spinner";
import { Alert, AlertTitle } from "@/components/ui/alert";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Skeleton } from "@/components/ui/skeleton";
import { ToolTipLabel } from "@/components/ui/tooltip";
import { useMutation } from "@tanstack/react-query";
import { TransactionButton } from "components/buttons/TransactionButton";
import { CircleSlash, TrashIcon } from "lucide-react";
import { useState } from "react";
import { toast } from "sonner";
import {
type ContractOptions,
getContract,
sendTransaction,
waitForReceipt,
} from "thirdweb";
import { uninstallModuleByProxy } from "thirdweb/modules";
import { CircleSlash } from "lucide-react";
import type { ContractOptions } from "thirdweb";
import type { Account } from "thirdweb/wallets";
import { useModuleContractInfo } from "./moduleContractInfo";
import { ModuleCard } from "./module-card";
export const InstalledModulesTable = (props: {
contract: ContractOptions;
@@ -38,7 +14,7 @@ export const InstalledModulesTable = (props: {
isPending: boolean;
};
refetchModules: () => void;
ownerAccount?: Account;
ownerAccount: Account | undefined;
}) => {
const { installedModules, ownerAccount } = props;
@@ -66,249 +42,18 @@ export const InstalledModulesTable = (props: {
<>
{sectionTitle}
<ScrollShadow scrollableClassName="rounded-lg">
<table className="w-full selection:bg-inverted selection:text-inverted-foreground">
<thead>
<TableHeadingRow>
<TableHeading> Module Name </TableHeading>
<TableHeading> Description </TableHeading>
<TableHeading> Publisher Address </TableHeading>
<TableHeading> Module Address </TableHeading>
<TableHeading> Version </TableHeading>
{ownerAccount && <TableHeading> Remove </TableHeading>}
</TableHeadingRow>
</thead>
<tbody>
{installedModules.isPending ? (
<>
<SkeletonRow ownerAccount={ownerAccount} />
<SkeletonRow ownerAccount={ownerAccount} />
<SkeletonRow ownerAccount={ownerAccount} />
</>
) : (
<>
{installedModules.data?.map((e, i) => (
<ModuleRow
// biome-ignore lint/suspicious/noArrayIndexKey: FIXME
key={i}
moduleAddress={e}
contract={props.contract}
onRemoveModule={props.refetchModules}
ownerAccount={ownerAccount}
/>
))}
</>
)}
</tbody>
</table>
<div className="flex flex-col gap-6">
{installedModules.data?.map((moduleAddress) => (
<ModuleCard
key={moduleAddress}
moduleAddress={moduleAddress}
contract={props.contract}
onRemoveModule={props.refetchModules}
ownerAccount={ownerAccount}
/>
))}
</div>
</ScrollShadow>
</>
);
};
function SkeletonRow(props: { ownerAccount?: Account }) {
return (
<TableRow>
<TableData>
<Skeleton className="h-6" />
</TableData>
<TableData>
<Skeleton className="h-6" />
</TableData>
<TableData>
<Skeleton className="h-6" />
</TableData>
<TableData>
<Skeleton className="h-6" />
</TableData>
{/* Version */}
<TableData>
<Skeleton className="h-6" />
</TableData>
{/* Remove */}
{props.ownerAccount && (
<TableData>
<Skeleton className="h-6" />
</TableData>
)}
</TableRow>
);
}
function ModuleRow(props: {
moduleAddress: string;
contract: ContractOptions;
onRemoveModule: () => void;
ownerAccount?: Account;
}) {
const { contract, moduleAddress, ownerAccount } = props;
const [isUninstallModalOpen, setIsUninstallModalOpen] = useState(false);
const contractInfo = useModuleContractInfo(
getContract({
address: moduleAddress,
chain: contract.chain,
client: contract.client,
}),
);
const uninstallMutation = useMutation({
mutationFn: async (account: Account) => {
const uninstallTransaction = uninstallModuleByProxy({
contract,
chain: contract.chain,
client: contract.client,
moduleProxyAddress: moduleAddress,
moduleData: "0x",
});
const txResult = await sendTransaction({
transaction: uninstallTransaction,
account,
});
await waitForReceipt(txResult);
},
onSuccess() {
toast.success("Module Removed successfully");
props.onRemoveModule();
},
onError(error) {
toast.error("Failed to remove module");
console.error("Error during uninstallation:", error);
},
});
const handleRemove = async () => {
if (!ownerAccount) {
return;
}
setIsUninstallModalOpen(false);
uninstallMutation.mutate(ownerAccount);
};
if (!contractInfo) {
return <SkeletonRow ownerAccount={ownerAccount} />;
}
return (
<TableRow>
<TableData>
<p>{contractInfo.name}</p>
</TableData>
<TableData>
<p>{contractInfo.description || "..."}</p>
</TableData>
<TableData>
<WalletAddress address={contractInfo.publisher || ""} />
</TableData>
<TableData>
<CopyAddressButton
className="text-xs"
address={moduleAddress || ""}
copyIconPosition="left"
variant="outline"
/>
</TableData>
{/* Version */}
<TableData>
<p>{contractInfo.version}</p>
</TableData>
{/* Remove */}
{ownerAccount && (
<TableData>
<div>
<ToolTipLabel label="Remove Module">
<Button
onClick={() => setIsUninstallModalOpen(true)}
variant="outline"
className="rounded-xl p-3 text-red-500"
>
{uninstallMutation.isPending ? (
<Spinner className="size-4" />
) : (
<TrashIcon className="size-4" />
)}
</Button>
</ToolTipLabel>
</div>
</TableData>
)}
<Dialog
open={isUninstallModalOpen}
onOpenChange={setIsUninstallModalOpen}
>
<DialogContent className="z-[10001]" dialogOverlayClassName="z-[10000]">
<form
onSubmit={(e) => {
e.preventDefault();
handleRemove();
}}
>
<DialogHeader>
<DialogTitle>Uninstall Module</DialogTitle>
<DialogDescription>
Are you sure you want to uninstall{" "}
<span className="font-medium text-foreground ">
{contractInfo.name}
</span>{" "}
?
</DialogDescription>
</DialogHeader>
<DialogFooter className="mt-10 flex-row justify-end gap-3 md:gap-1">
<Button
type="button"
onClick={() => setIsUninstallModalOpen(false)}
variant="outline"
>
Cancel
</Button>
<TransactionButton
txChainID={contract.chain.id}
transactionCount={1}
isLoading={uninstallMutation.isPending}
type="submit"
colorScheme="red"
className="flex"
>
Uninstall
</TransactionButton>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
</TableRow>
);
}
function TableRow(props: { children: React.ReactNode }) {
return (
<tr className="border-border border-b [&:last-child]:border-b-0">
{props.children}
</tr>
);
}
function TableData({ children }: { children: React.ReactNode }) {
return <td className="px-3 py-4 text-sm">{children}</td>;
}
function TableHeading(props: { children: React.ReactNode }) {
return (
<th className="min-w-[150px] border-border border-b px-3 py-3 text-left font-medium text-muted-foreground text-sm">
{props.children}
</th>
);
}
function TableHeadingRow({ children }: { children: React.ReactNode }) {
return <tr className="relative bg-muted/50">{children}</tr>;
}
@@ -0,0 +1,120 @@
import type { Meta, StoryObj } from "@storybook/react";
import { useMutation } from "@tanstack/react-query";
import { Toaster } from "sonner";
import { BadgeContainer, mobileViewport } from "stories/utils";
import { ThirdwebProvider } from "thirdweb/react";
import { ModuleCardUI } from "./module-card";
const meta = {
title: "Modules/ModuleCard",
component: Component,
parameters: {
layout: "centered",
},
} satisfies Meta<typeof Component>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Desktop: Story = {
args: {},
};
export const Mobile: Story = {
args: {},
parameters: {
viewport: mobileViewport("iphone14"),
},
};
function Component() {
const removeMutation = useMutation({
mutationFn: async () => {
await new Promise((resolve) => setTimeout(resolve, 1000));
},
});
const updateMutation = useMutation({
mutationFn: async () => {
await new Promise((resolve) => setTimeout(resolve, 1000));
},
});
const contractInfo = {
name: "Module Name",
description:
"lorem ipsum dolor sit amet consectetur adipisicing elit sed do eiusmod tempor incididunt ut labore ",
publisher: "0xdd99b75f095d0c4d5112aCe938e4e6ed962fb024",
version: "1.0.0",
};
return (
<ThirdwebProvider>
<div className="container flex max-w-[1150px] flex-col gap-10 py-10">
<BadgeContainer label="No Update, No Children, Owner Account">
<ModuleCardUI
contractInfo={contractInfo}
moduleAddress="0x0000000000000000000000000000000000000000"
uninstallButton={{
onClick: () => removeMutation.mutateAsync(),
isPending: removeMutation.isPending,
}}
isOwnerAccount={true}
/>
</BadgeContainer>
<BadgeContainer label="No Update, No Children, Not Owner Account">
<ModuleCardUI
contractInfo={contractInfo}
moduleAddress="0x0000000000000000000000000000000000000000"
uninstallButton={{
onClick: () => removeMutation.mutateAsync(),
isPending: removeMutation.isPending,
}}
isOwnerAccount={false}
/>
</BadgeContainer>
<BadgeContainer label="Update Button (disabled), No Children, Owner">
<ModuleCardUI
contractInfo={contractInfo}
moduleAddress="0x0000000000000000000000000000000000000000"
uninstallButton={{
onClick: () => removeMutation.mutateAsync(),
isPending: removeMutation.isPending,
}}
updateButton={{
isDisabled: true,
isPending: updateMutation.isPending,
onClick: () => updateMutation.mutateAsync(),
}}
isOwnerAccount={true}
/>
</BadgeContainer>
<BadgeContainer label="Update Button (enabled), Children, Owner">
<ModuleCardUI
contractInfo={contractInfo}
moduleAddress="0x0000000000000000000000000000000000000000"
uninstallButton={{
onClick: () => removeMutation.mutateAsync(),
isPending: removeMutation.isPending,
}}
updateButton={{
isDisabled: false,
isPending: updateMutation.isPending,
onClick: () => updateMutation.mutateAsync(),
}}
isOwnerAccount={true}
>
<div className="flex h-36 items-center justify-center rounded-lg bg-muted text-muted-foreground text-sm">
CHILDREN
</div>
</ModuleCardUI>
</BadgeContainer>
<Toaster richColors />
</div>
</ThirdwebProvider>
);
}
@@ -0,0 +1,288 @@
import { WalletAddress } from "@/components/blocks/wallet-address";
import { CopyAddressButton } from "@/components/ui/CopyAddressButton";
import { Spinner } from "@/components/ui/Spinner/Spinner";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { Skeleton } from "@/components/ui/skeleton";
import { useMutation } from "@tanstack/react-query";
import { TransactionButton } from "components/buttons/TransactionButton";
import { InfoIcon } from "lucide-react";
import { useState } from "react";
import { toast } from "sonner";
import {
type ContractOptions,
getContract,
sendTransaction,
waitForReceipt,
} from "thirdweb";
import { uninstallModuleByProxy } from "thirdweb/modules";
import type { Account } from "thirdweb/wallets";
import { useModuleContractInfo } from "./moduleContractInfo";
type ModuleProps = {
moduleAddress: string;
contract: ContractOptions;
onRemoveModule: () => void;
ownerAccount: Account | undefined;
};
export function ModuleCard(props: ModuleProps) {
const { contract, moduleAddress, ownerAccount } = props;
const [isUninstallModalOpen, setIsUninstallModalOpen] = useState(false);
const contractInfo = useModuleContractInfo(
getContract({
address: moduleAddress,
chain: contract.chain,
client: contract.client,
}),
);
const uninstallMutation = useMutation({
mutationFn: async (account: Account) => {
const uninstallTransaction = uninstallModuleByProxy({
contract,
chain: contract.chain,
client: contract.client,
moduleProxyAddress: moduleAddress,
moduleData: "0x",
});
const txResult = await sendTransaction({
transaction: uninstallTransaction,
account,
});
await waitForReceipt(txResult);
},
onSuccess() {
toast.success("Module uninstalled successfully");
props.onRemoveModule();
},
onError(error) {
toast.error("Failed to uninstall module");
console.error(error);
},
});
const handleRemove = async () => {
if (!ownerAccount) {
toast.error("Wallet is not connected");
return;
}
uninstallMutation.mutate(ownerAccount);
};
if (!contractInfo) {
return <Skeleton className="h-[190px] w-full border border-border" />;
}
return (
<>
<ModuleCardUI
contractInfo={{
name: contractInfo.name,
description: contractInfo.description,
publisher: contractInfo.publisher,
version: contractInfo.version,
}}
isOwnerAccount={!!ownerAccount}
uninstallButton={{
onClick: () => {
setIsUninstallModalOpen(true);
},
isPending: uninstallMutation.isPending,
}}
moduleAddress={moduleAddress}
/>
<Dialog
open={isUninstallModalOpen}
onOpenChange={setIsUninstallModalOpen}
>
<DialogContent className="z-[10001]" dialogOverlayClassName="z-[10000]">
<form
onSubmit={(e) => {
e.preventDefault();
handleRemove();
}}
>
<DialogHeader>
<DialogTitle>Uninstall Module</DialogTitle>
<DialogDescription>
Are you sure you want to uninstall{" "}
<span className="font-medium text-foreground ">
{contractInfo.name}
</span>{" "}
?
</DialogDescription>
</DialogHeader>
<DialogFooter className="mt-10 flex-row justify-end gap-3 md:gap-1">
<Button
type="button"
onClick={() => setIsUninstallModalOpen(false)}
variant="outline"
>
Cancel
</Button>
<TransactionButton
txChainID={contract.chain.id}
transactionCount={1}
isLoading={uninstallMutation.isPending}
type="submit"
colorScheme="red"
className="flex"
>
Uninstall
</TransactionButton>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
</>
);
}
export type ModuleCardUIProps = {
children?: React.ReactNode;
contractInfo: {
name: string;
description?: string;
version?: string;
publisher?: string;
};
moduleAddress: string;
isOwnerAccount: boolean;
uninstallButton: {
onClick: () => void;
isPending: boolean;
};
updateButton?: {
onClick: () => void;
isPending: boolean;
isDisabled: boolean;
};
};
export function ModuleCardUI(props: ModuleCardUIProps) {
return (
<section className="rounded-lg border border-border bg-muted/50">
{/* Header */}
<div className="relative p-4 lg:p-6">
{/* Title */}
<div className="pr-14">
<h3 className="mb-1 gap-2 font-semibold text-xl tracking-tight">
{props.contractInfo.name}
{/* Info Dialog */}
<Dialog>
<DialogTrigger asChild>
<Button
variant="ghost"
className="absolute top-4 right-4 h-auto w-auto p-2 text-muted-foreground"
>
<InfoIcon className="size-5" />
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>{props.contractInfo.name}</DialogTitle>
<DialogDescription>
{props.contractInfo.description}
</DialogDescription>
{/* Avoid adding focus on other elements to prevent tooltips from opening on modal open */}
<input className="sr-only" aria-hidden />
<div className="h-2" />
<div className="flex flex-col gap-4">
{props.contractInfo.version && (
<div>
<p className="text-muted-foreground text-sm">
{" "}
Version{" "}
</p>
<p> {props.contractInfo.version}</p>
</div>
)}
{props.contractInfo.publisher && (
<div>
<p className="text-muted-foreground text-sm">
Published By
</p>
<WalletAddress address={props.contractInfo.publisher} />
</div>
)}
<div>
<p className="mb-1 text-muted-foreground text-sm">
Module Address
</p>
<CopyAddressButton
className="text-xs"
address={props.moduleAddress}
copyIconPosition="left"
variant="outline"
/>
</div>
</div>
</DialogHeader>
</DialogContent>
</Dialog>
</h3>
{/* Description */}
<p className="text-muted-foreground">
{props.contractInfo.description}
</p>
</div>
{props.children ? (
<>
<div className="h-5" />
{props.children}
</>
) : null}
</div>
<div className="flex flex-row justify-end gap-3 border-border border-t p-4 lg:p-6">
<Button
size="sm"
onClick={props.uninstallButton.onClick}
variant="destructive"
className="min-w-24 gap-2"
disabled={!props.isOwnerAccount}
>
{props.uninstallButton.isPending && <Spinner className="size-4" />}
Uninstall
</Button>
{props.isOwnerAccount && props.updateButton && (
<Button
size="sm"
className="min-w-24 gap-2"
type="submit"
onClick={props.updateButton.onClick}
disabled={props.updateButton.isPending || !props.isOwnerAccount}
>
{props.updateButton.isPending && <Spinner className="size-4" />}
Update
</Button>
)}
</div>
</section>
);
}
@@ -1,7 +1,9 @@
import { notFound, redirect } from "next/navigation";
import { localhost } from "thirdweb/chains";
import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
import { getContractPageMetadata } from "../_utils/getContractPageMetadata";
import { ContractEditModulesPage } from "./ContractEditModulesPage";
import { ContractEditModulesPageClient } from "./ContractEditModulesPage.client";
export default async function Page(props: {
params: {
@@ -15,11 +17,16 @@ export default async function Page(props: {
notFound();
}
const { isModularCore } = await getContractPageMetadata(info.contract);
const { contract } = info;
if (contract.chain.id === localhost.id) {
return <ContractEditModulesPageClient contract={contract} />;
}
const { isModularCore } = await getContractPageMetadata(contract);
if (!isModularCore) {
redirect(`/${props.params.chain_id}/${props.params.contractAddress}`);
}
return <ContractEditModulesPage contract={info.contract} />;
return <ContractEditModulesPage contract={contract} />;
}
@@ -0,0 +1,35 @@
"use client";
import type { ThirdwebContract } from "thirdweb";
import { ErrorPage, LoadingPage } from "../_components/page-skeletons";
import { RedirectToContractOverview } from "../_components/redirect-contract-overview.client";
import { useContractPageMetadata } from "../_hooks/useContractPageMetadata";
import { ContractNFTPage } from "./ContractNFTPage";
export function ContractNFTPageClient(props: {
contract: ThirdwebContract;
}) {
const metadataQuery = useContractPageMetadata(props.contract);
if (metadataQuery.isPending) {
return <LoadingPage />;
}
if (metadataQuery.isError) {
return <ErrorPage />;
}
const { supportedERCs, functionSelectors } = metadataQuery.data;
if (!supportedERCs.isERC721 && !supportedERCs.isERC1155) {
return <RedirectToContractOverview contract={props.contract} />;
}
return (
<ContractNFTPage
contract={props.contract}
isErc721={supportedERCs.isERC721}
functionSelectors={functionSelectors}
/>
);
}
@@ -0,0 +1,37 @@
"use client";
import type { ThirdwebContract } from "thirdweb";
import { ErrorPage, LoadingPage } from "../../_components/page-skeletons";
import { RedirectToContractOverview } from "../../_components/redirect-contract-overview.client";
import { useContractPageMetadata } from "../../_hooks/useContractPageMetadata";
import { TokenIdPage } from "./token-id";
export function TokenIdPageClient(props: {
contract: ThirdwebContract;
tokenId: string;
}) {
const { contract } = props;
const metadataQuery = useContractPageMetadata(props.contract);
if (metadataQuery.isPending) {
return <LoadingPage />;
}
if (metadataQuery.isError) {
return <ErrorPage />;
}
const { supportedERCs } = metadataQuery.data;
if (!supportedERCs.isERC721 && !supportedERCs.isERC1155) {
return <RedirectToContractOverview contract={props.contract} />;
}
return (
<TokenIdPage
contract={contract}
isErc721={supportedERCs.isERC721}
tokenId={props.tokenId}
/>
);
}
@@ -3,7 +3,6 @@
import { Flex, FormControl, Input } from "@chakra-ui/react";
import { TransactionButton } from "components/buttons/TransactionButton";
import { useTrack } from "hooks/analytics/useTrack";
import { useTxNotifications } from "hooks/useTxNotifications";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { type ThirdwebContract, ZERO_ADDRESS } from "thirdweb";
@@ -23,13 +22,6 @@ const ClaimTabERC1155: React.FC<ClaimTabProps> = ({ contract, tokenId }) => {
const form = useForm<{ to: string; amount: string }>({
defaultValues: { amount: "1", to: address },
});
const { onSuccess, onError } = useTxNotifications(
"Claimed successfully",
"Failed to claim",
contract,
);
const sendAndConfirmTx = useSendAndConfirmTransaction();
const account = useActiveAccount();
return (
@@ -59,28 +51,34 @@ const ClaimTabERC1155: React.FC<ClaimTabProps> = ({ contract, tokenId }) => {
account,
});
if (approveTx) {
try {
await sendAndConfirmTx.mutateAsync(approveTx);
} catch {
return toast.error("Error approving ERC20 token");
}
const approvalPromise = sendAndConfirmTx.mutateAsync(approveTx);
toast.promise(approvalPromise, {
loading: "Approving ERC20 token for this claim",
success: "Approved succesfully",
error: "Failed to approve ERC20",
});
await approvalPromise;
}
await sendAndConfirmTx.mutateAsync(transaction);
const promise = sendAndConfirmTx.mutateAsync(transaction);
toast.promise(promise, {
loading: "Claiming NFT",
success: "NFT claimed successfully",
error: "Failed to claim NFT",
});
trackEvent({
category: "nft",
action: "claim",
label: "success",
});
onSuccess();
form.reset();
} catch (error) {
console.error(error);
trackEvent({
category: "nft",
action: "claim",
label: "error",
error,
});
onError(error);
}
})}
>
@@ -29,7 +29,7 @@ const mintAdditionalSupplyFormSchema = z.object({
to: z.string().refine((value) => isAddress(value), {
message: "Invalid Ethereum address",
}),
amount: z.number().int().min(1, "Amount must be at least 1"),
amount: z.coerce.number().int().min(1, "Amount must be at least 1"),
});
const MintSupplyTab: React.FC<MintSupplyTabProps> = ({ contract, tokenId }) => {
@@ -96,7 +96,7 @@ const MintSupplyTab: React.FC<MintSupplyTabProps> = ({ contract, tokenId }) => {
<FormItem>
<FormLabel>Amount</FormLabel>
<FormControl>
<Input {...field} />
<Input {...field} type="number" />
</FormControl>
<FormDescription>
How many would you like to mint?
@@ -1,6 +1,8 @@
import { notFound, redirect } from "next/navigation";
import { localhost } from "thirdweb/chains";
import { getContractPageParamsInfo } from "../../_utils/getContractFromParams";
import { getContractPageMetadata } from "../../_utils/getContractPageMetadata";
import { TokenIdPageClient } from "./TokenIdPage.client";
import { TokenIdPage } from "./token-id";
export default async function Page(props: {
@@ -16,18 +18,24 @@ export default async function Page(props: {
notFound();
}
if (!isOnlyNumbers(props.params.tokenId)) {
// redirect to nfts index page
redirect(`/${props.params.chain_id}/${props.params.contractAddress}/nfts`);
}
const { contract } = info;
if (contract.chain.id === localhost.id) {
return (
<TokenIdPageClient contract={contract} tokenId={props.params.tokenId} />
);
}
const { supportedERCs } = await getContractPageMetadata(contract);
if (!supportedERCs.isERC721 && !supportedERCs.isERC1155) {
redirect(`/${props.params.chain_id}/${props.params.contractAddress}`);
}
if (!isOnlyNumbers(props.params.tokenId)) {
// redirect to nfts index page
redirect(`/${props.params.chain_id}/${props.params.contractAddress}/nfts`);
}
return (
<TokenIdPage
contract={contract}
@@ -1,8 +1,9 @@
import { notFound, redirect } from "next/navigation";
import { resolveFunctionSelectors } from "../../../../../../lib/selectors";
import { localhost } from "thirdweb/chains";
import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
import { getContractPageMetadata } from "../_utils/getContractPageMetadata";
import { ContractNFTPage } from "./ContractNFTPage";
import { ContractNFTPageClient } from "./ContractNFTPage.client";
export default async function Page(props: {
params: {
@@ -17,14 +18,17 @@ export default async function Page(props: {
}
const { contract } = info;
const { supportedERCs } = await getContractPageMetadata(contract);
if (contract.chain.id === localhost.id) {
return <ContractNFTPageClient contract={contract} />;
}
const { supportedERCs, functionSelectors } =
await getContractPageMetadata(contract);
if (!supportedERCs.isERC721 && !supportedERCs.isERC1155) {
redirect(`/${props.params.chain_id}/${props.params.contractAddress}`);
}
const functionSelectors = await resolveFunctionSelectors(contract);
return (
<ContractNFTPage
contract={contract}
@@ -1,5 +1,3 @@
import { PublishedBy } from "components/contract-components/shared/published-by";
import { ErrorBoundary } from "react-error-boundary";
import type { ThirdwebContract } from "thirdweb";
import { AnalyticsOverview } from "./components/Analytics";
import { BuildYourApp } from "./components/BuildYourApp";
@@ -21,6 +19,7 @@ interface ContractOverviewPageProps {
chainSlug: string;
isAnalyticsSupported: boolean;
functionSelectors: string[];
publishedBy: React.ReactNode;
}
const TRACKING_CATEGORY = "contract_overview";
@@ -36,6 +35,7 @@ export const ContractOverviewPage: React.FC<ContractOverviewPageProps> = ({
chainSlug,
isAnalyticsSupported,
functionSelectors,
publishedBy,
}) => {
return (
<div className="flex flex-col gap-8 lg:flex-row">
@@ -99,11 +99,7 @@ export const ContractOverviewPage: React.FC<ContractOverviewPageProps> = ({
contractAddress={contract.address}
/>
</div>
<div className="shrink-0 lg:w-[300px]">
<ErrorBoundary fallback={null}>
<PublishedBy contract={contract} />
</ErrorBoundary>
</div>
<div className="shrink-0 lg:w-[300px]">{publishedBy}</div>
</div>
);
};
@@ -1,20 +1,14 @@
import { getThirdwebClient } from "@/constants/thirdweb.server";
import { ContractCard } from "components/explore/contract-card";
import { THIRDWEB_DEPLOYER_ADDRESS } from "constants/addresses";
import type { ThirdwebContract } from "thirdweb";
import type { ThirdwebClient, ThirdwebContract } from "thirdweb";
import { polygon } from "thirdweb/chains";
import { getBytecode, getContract } from "thirdweb/contract";
import { getPublishedUriFromCompilerUri } from "thirdweb/extensions/thirdweb";
import { getInstalledModules } from "thirdweb/modules";
import { download } from "thirdweb/storage";
import { extractIPFSUri } from "thirdweb/utils";
import { getAuthTokenWalletAddress } from "../../../app/api/lib/getAuthToken";
import { isEnsName, resolveEns } from "../../../lib/ens";
import { fetchPublishedContractsFromDeploy } from "../fetchPublishedContractsFromDeploy";
interface PublishedByProps {
contract: ThirdwebContract;
}
import { fetchPublishedContractsFromDeploy } from "../../../../../../../components/contract-components/fetchPublishedContractsFromDeploy";
import { isEnsName, resolveEns } from "../../../../../../../lib/ens";
type ModuleMetadataPickedKeys = {
publisher: string;
@@ -23,15 +17,18 @@ type ModuleMetadataPickedKeys = {
version: string;
};
export const PublishedBy: React.FC<PublishedByProps> = async ({ contract }) => {
const client = getThirdwebClient();
export async function getPublishedByCardProps(params: {
address: string | null;
contract: ThirdwebContract;
client: ThirdwebClient;
}) {
const { address, contract, client } = params;
const publishedContractsFromDeploy = await fetchPublishedContractsFromDeploy({
contract,
client,
});
const address = getAuthTokenWalletAddress();
const reversedPublishedContractsFromDeploy = [
...(publishedContractsFromDeploy || []),
].reverse();
@@ -133,19 +130,35 @@ export const PublishedBy: React.FC<PublishedByProps> = async ({ contract }) => {
}
}
return {
modules,
name: publishedContractToShow.name,
publisher: publisherAddressOrEns,
version: publishedContractToShow.version,
isBeta: (publishedContractToShow.displayName || "")
.toLowerCase()
.includes("beta"),
};
}
export function PublishedByUI(props: {
modules: ModuleMetadataPickedKeys[];
name: string;
publisher: string;
version: string | undefined;
isBeta: boolean;
}) {
return (
<ContractCard
contractId={publishedContractToShow.name}
publisher={publisherAddressOrEns}
version={publishedContractToShow.version}
isBeta={(publishedContractToShow.displayName || "")
.toLowerCase()
.includes("beta")}
modules={modules.map((m) => ({
contractId={props.name}
publisher={props.publisher}
version={props.version}
isBeta={props.isBeta}
modules={props.modules.map((m) => ({
publisher: m.publisher,
moduleId: m.name,
version: m.version,
}))}
/>
);
};
}
@@ -0,0 +1,32 @@
import { getThirdwebClient } from "@/constants/thirdweb.server";
import type { ThirdwebContract } from "thirdweb";
import { getAuthTokenWalletAddress } from "../../../../../../api/lib/getAuthToken";
import { PublishedByUI, getPublishedByCardProps } from "./published-by-ui";
interface PublishedByProps {
contract: ThirdwebContract;
}
export const PublishedBy: React.FC<PublishedByProps> = async ({ contract }) => {
const client = getThirdwebClient();
const address = getAuthTokenWalletAddress();
const props = await getPublishedByCardProps({
address,
contract,
client,
});
if (!props) {
return null;
}
return (
<PublishedByUI
isBeta={props.isBeta}
modules={props.modules}
name={props.name}
publisher={props.publisher}
version={props.version}
/>
);
};
@@ -0,0 +1,43 @@
"use client";
import type { ThirdwebContract } from "thirdweb";
import type { ChainMetadata } from "thirdweb/chains";
import { ErrorPage, LoadingPage } from "../_components/page-skeletons";
import { useContractPageMetadata } from "../_hooks/useContractPageMetadata";
import { ContractOverviewPage } from "./ContractOverviewPage";
export function ContractOverviewPageClient(props: {
contract: ThirdwebContract;
chainMetadata: ChainMetadata;
}) {
const { contract, chainMetadata } = props;
const metadataQuery = useContractPageMetadata(contract);
if (metadataQuery.isPending) {
return <LoadingPage />;
}
if (metadataQuery.isError) {
return <ErrorPage />;
}
const contractPageMetadata = metadataQuery.data;
return (
<ContractOverviewPage
contract={contract}
hasDirectListings={contractPageMetadata.isDirectListingSupported}
hasEnglishAuctions={contractPageMetadata.isEnglishAuctionSupported}
isErc1155={contractPageMetadata.supportedERCs.isERC1155}
isErc20={contractPageMetadata.supportedERCs.isERC20}
isErc721={contractPageMetadata.supportedERCs.isERC721}
isPermissionsEnumerable={
contractPageMetadata.isPermissionsEnumerableSupported
}
chainSlug={chainMetadata.slug}
isAnalyticsSupported={contractPageMetadata.isAnalyticsSupported}
functionSelectors={contractPageMetadata.functionSelectors}
// TODO - create a fully client rendered version of publishedBy and ContractCard and plug it here
publishedBy={undefined}
/>
);
}
@@ -1,7 +1,11 @@
import { notFound } from "next/navigation";
import { ErrorBoundary } from "react-error-boundary";
import { localhost } from "thirdweb/chains";
import { getContractPageParamsInfo } from "./_utils/getContractFromParams";
import { getContractPageMetadata } from "./_utils/getContractPageMetadata";
import { ContractOverviewPage } from "./overview/ContractOverviewPage";
import { PublishedBy } from "./overview/components/published-by.server";
import { ContractOverviewPageClient } from "./overview/contract-overview-page.client";
export default async function Page(props: {
params: {
@@ -16,6 +20,15 @@ export default async function Page(props: {
}
const { contract, chainMetadata } = info;
if (contract.chain.id === localhost.id) {
return (
<ContractOverviewPageClient
chainMetadata={chainMetadata}
contract={contract}
/>
);
}
const contractPageMetadata = await getContractPageMetadata(contract);
return (
@@ -32,6 +45,11 @@ export default async function Page(props: {
chainSlug={chainMetadata.slug}
isAnalyticsSupported={contractPageMetadata.isAnalyticsSupported}
functionSelectors={contractPageMetadata.functionSelectors}
publishedBy={
<ErrorBoundary fallback={null}>
<PublishedBy contract={contract} />
</ErrorBoundary>
}
/>
);
}
@@ -0,0 +1,32 @@
"use client";
import type { ThirdwebContract } from "thirdweb";
import type { ChainMetadata } from "thirdweb/chains";
import { ErrorPage, LoadingPage } from "../_components/page-skeletons";
import { useContractPageMetadata } from "../_hooks/useContractPageMetadata";
import { ContractPermissionsPage } from "./ContractPermissionsPage";
export function ContractPermissionsPageClient(props: {
contract: ThirdwebContract;
chainMetadata: ChainMetadata;
}) {
const metadataQuery = useContractPageMetadata(props.contract);
if (metadataQuery.isPending) {
return <LoadingPage />;
}
if (metadataQuery.isError) {
return <ErrorPage />;
}
return (
<ContractPermissionsPage
contract={props.contract}
chainSlug={props.chainMetadata.slug}
detectedPermissionEnumerable={
metadataQuery.data.isPermissionsEnumerableSupported
}
/>
);
}
@@ -1,7 +1,9 @@
import { notFound } from "next/navigation";
import { localhost } from "thirdweb/chains";
import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
import { getContractPageMetadata } from "../_utils/getContractPageMetadata";
import { ContractPermissionsPage } from "./ContractPermissionsPage";
import { ContractPermissionsPageClient } from "./ContractPermissionsPage.client";
export default async function Page(props: {
params: {
@@ -16,6 +18,15 @@ export default async function Page(props: {
}
const { contract } = info;
if (contract.chain.id === localhost.id) {
return (
<ContractPermissionsPageClient
contract={contract}
chainMetadata={info.chainMetadata}
/>
);
}
const { isPermissionsEnumerableSupported } =
await getContractPageMetadata(contract);
@@ -0,0 +1,27 @@
"use client";
import type { ThirdwebContract } from "thirdweb";
import { ErrorPage, LoadingPage } from "../_components/page-skeletons";
import { RedirectToContractOverview } from "../_components/redirect-contract-overview.client";
import { useContractPageMetadata } from "../_hooks/useContractPageMetadata";
import { ContractProposalsPage } from "./ContractProposalsPage";
export function ContractProposalsPageClient(props: {
contract: ThirdwebContract;
}) {
const metadataQuery = useContractPageMetadata(props.contract);
if (metadataQuery.isPending) {
return <LoadingPage />;
}
if (metadataQuery.isError) {
return <ErrorPage />;
}
if (!metadataQuery.data.isVoteContract) {
return <RedirectToContractOverview contract={props.contract} />;
}
return <ContractProposalsPage contract={props.contract} />;
}
@@ -1,7 +1,9 @@
import { notFound, redirect } from "next/navigation";
import { localhost } from "thirdweb/chains";
import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
import { getContractPageMetadata } from "../_utils/getContractPageMetadata";
import { ContractProposalsPage } from "./ContractProposalsPage";
import { ContractProposalsPageClient } from "./ContractProposalsPage.client";
export default async function Page(props: {
params: {
@@ -15,11 +17,16 @@ export default async function Page(props: {
notFound();
}
const { isVoteContract } = await getContractPageMetadata(info.contract);
const { contract } = info;
if (contract.chain.id === localhost.id) {
return <ContractProposalsPageClient contract={contract} />;
}
const { isVoteContract } = await getContractPageMetadata(contract);
if (!isVoteContract) {
redirect(`/${props.params.chain_id}/${props.params.contractAddress}`);
}
return <ContractProposalsPage contract={info.contract} />;
return <ContractProposalsPage contract={contract} />;
}
@@ -0,0 +1,27 @@
"use client";
import type { ThirdwebContract } from "thirdweb";
import { ErrorPage, LoadingPage } from "../_components/page-skeletons";
import { useContractPageMetadata } from "../_hooks/useContractPageMetadata";
import { ContractSettingsPage } from "./ContractSettingsPage";
export function ContractSettingsPageClient(props: {
contract: ThirdwebContract;
}) {
const metadataQuery = useContractPageMetadata(props.contract);
if (metadataQuery.isPending) {
return <LoadingPage />;
}
if (metadataQuery.isError) {
return <ErrorPage />;
}
return (
<ContractSettingsPage
contract={props.contract}
functionSelectors={metadataQuery.data.functionSelectors}
/>
);
}
@@ -1,6 +1,7 @@
"use client";
import { Flex, GridItem, SimpleGrid } from "@chakra-ui/react";
import type { ThirdwebContract } from "thirdweb";
import * as CommonExt from "thirdweb/extensions/common";
import { SettingsMetadata } from "./components/metadata";
import { SettingsPlatformFees } from "./components/platform-fees";
import { SettingsPrimarySale } from "./components/primary-sale";
@@ -14,7 +15,7 @@ interface ContractSettingsPageProps {
isPlatformFeesSupported: boolean;
}
export const ContractSettingsPage: React.FC<ContractSettingsPageProps> = ({
const ContractSettingsPageInner: React.FC<ContractSettingsPageProps> = ({
contract,
isContractMetadataSupported,
isPrimarySaleSupported,
@@ -66,3 +67,31 @@ export const ContractSettingsPage: React.FC<ContractSettingsPageProps> = ({
</Flex>
);
};
export function ContractSettingsPage(props: {
contract: ThirdwebContract;
functionSelectors: string[];
}) {
const { functionSelectors, contract } = props;
return (
<ContractSettingsPageInner
contract={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)}
/>
);
}
@@ -1,8 +1,9 @@
import { notFound } from "next/navigation";
import * as CommonExt from "thirdweb/extensions/common";
import { localhost } from "thirdweb/chains";
import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
import { getContractPageMetadata } from "../_utils/getContractPageMetadata";
import { ContractSettingsPage } from "./ContractSettingsPage";
import { ContractSettingsPageClient } from "./ContractSettingsPage.client";
export default async function Page(props: {
params: {
@@ -16,27 +17,17 @@ export default async function Page(props: {
notFound();
}
const { contract } = info;
if (contract.chain.id === localhost.id) {
return <ContractSettingsPageClient contract={contract} />;
}
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)}
functionSelectors={functionSelectors}
/>
);
}
@@ -0,0 +1,27 @@
"use client";
import type { ThirdwebContract } from "thirdweb";
import { ErrorPage, LoadingPage } from "../_components/page-skeletons";
import { RedirectToContractOverview } from "../_components/redirect-contract-overview.client";
import { useContractPageMetadata } from "../_hooks/useContractPageMetadata";
import { ContractSplitPage } from "./ContractSplitPage";
export function ContractSplitPageClient(props: {
contract: ThirdwebContract;
}) {
const metadataQuery = useContractPageMetadata(props.contract);
if (metadataQuery.isPending) {
return <LoadingPage />;
}
if (metadataQuery.isError) {
return <ErrorPage />;
}
if (!metadataQuery.data.isSplitSupported) {
return <RedirectToContractOverview contract={props.contract} />;
}
return <ContractSplitPage contract={props.contract} />;
}
@@ -1,7 +1,9 @@
import { notFound, redirect } from "next/navigation";
import { localhost } from "thirdweb/chains";
import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
import { getContractPageMetadata } from "../_utils/getContractPageMetadata";
import { ContractSplitPage } from "./ContractSplitPage";
import { ContractSplitPageClient } from "./ContractSplitPage.client";
export default async function Page(props: {
params: {
@@ -14,12 +16,16 @@ export default async function Page(props: {
if (!info) {
notFound();
}
const { contract } = info;
if (contract.chain.id === localhost.id) {
return <ContractSplitPageClient contract={contract} />;
}
const { isSplitSupported } = await getContractPageMetadata(info.contract);
const { isSplitSupported } = await getContractPageMetadata(contract);
if (!isSplitSupported) {
redirect(`/${props.params.chain_id}/${props.params.contractAddress}`);
}
return <ContractSplitPage contract={info.contract} />;
return <ContractSplitPage contract={contract} />;
}
@@ -0,0 +1,35 @@
"use client";
import type { ThirdwebContract } from "thirdweb";
import {
isClaimToSupported,
isMintToSupported,
} from "thirdweb/extensions/erc20";
import { ErrorPage, LoadingPage } from "../_components/page-skeletons";
import { useContractPageMetadata } from "../_hooks/useContractPageMetadata";
import { ContractTokensPage } from "./ContractTokensPage";
export function ContractTokensPageClient(props: {
contract: ThirdwebContract;
}) {
const metadataQuery = useContractPageMetadata(props.contract);
if (metadataQuery.isPending) {
return <LoadingPage />;
}
if (metadataQuery.isError) {
return <ErrorPage />;
}
const { supportedERCs, functionSelectors } = metadataQuery.data;
return (
<ContractTokensPage
contract={props.contract}
isERC20={supportedERCs.isERC20}
isMintToSupported={isMintToSupported(functionSelectors)}
isClaimToSupported={isClaimToSupported(functionSelectors)}
/>
);
}
@@ -1,4 +1,5 @@
import { notFound } from "next/navigation";
import { localhost } from "thirdweb/chains";
import {
isClaimToSupported,
isMintToSupported,
@@ -6,6 +7,7 @@ import {
import { getContractPageParamsInfo } from "../_utils/getContractFromParams";
import { getContractPageMetadata } from "../_utils/getContractPageMetadata";
import { ContractTokensPage } from "./ContractTokensPage";
import { ContractTokensPageClient } from "./ContractTokensPage.client";
export default async function Page(props: {
params: {
@@ -19,6 +21,10 @@ export default async function Page(props: {
notFound();
}
if (info.contract.chain.id === localhost.id) {
return <ContractTokensPageClient contract={info.contract} />;
}
const { supportedERCs, functionSelectors } = await getContractPageMetadata(
info.contract,
);
@@ -1,5 +1,4 @@
"use client";
import { MultiNetworkSelector } from "@/components/blocks/NetworkSelectors";
import { SettingsCard } from "@/components/blocks/SettingsCard";
import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
@@ -27,6 +26,7 @@ import { zodResolver } from "@hookform/resolvers/zod";
import { PlusIcon } from "lucide-react";
import { useFieldArray, useForm } from "react-hook-form";
import { toast } from "sonner";
import { isAddress } from "thirdweb";
import { getSocialIcon } from "thirdweb/wallets/in-app";
import {
DEFAULT_ACCOUNT_FACTORY_V0_6,
@@ -57,7 +57,7 @@ export function AuthOptionsForm({ ecosystem }: { ecosystem: Ecosystem }) {
customAuthEndpoint: ecosystem.customAuthOptions?.authEndpoint?.url || "",
customHeaders: ecosystem.customAuthOptions?.authEndpoint?.headers || [],
useSmartAccount: !!ecosystem.smartAccountOptions,
chainIds: ecosystem.smartAccountOptions?.chainIds || [],
chainIds: [], // unused - TODO: remove from service
sponsorGas: ecosystem.smartAccountOptions?.sponsorGas || false,
accountFactoryType:
ecosystem.smartAccountOptions?.accountFactoryAddress ===
@@ -92,14 +92,18 @@ export function AuthOptionsForm({ ecosystem }: { ecosystem: Ecosystem }) {
})
.refine(
(data) => {
if (data.useSmartAccount && data.chainIds.length === 0) {
if (
data.useSmartAccount &&
data.customAccountFactoryAddress &&
!isAddress(data.customAccountFactoryAddress)
) {
return false;
}
return true;
},
{
message: "Please select at least one chain for smart accounts",
path: ["chainIds"],
message: "Please enter a valid custom account factory address",
path: ["customAccountFactoryAddress"],
},
)
.refine(
@@ -166,7 +170,7 @@ export function AuthOptionsForm({ ecosystem }: { ecosystem: Ecosystem }) {
}
smartAccountOptions = {
chainIds: data.chainIds,
chainIds: [], // unused - TODO remove from service
sponsorGas: data.sponsorGas,
accountFactoryAddress,
};
@@ -403,27 +407,6 @@ export function AuthOptionsForm({ ecosystem }: { ecosystem: Ecosystem }) {
/>
{form.watch("useSmartAccount") && (
<div className="mt-1 flex flex-col gap-4">
<FormField
control={form.control}
name="chainIds"
render={({ field }) => (
<FormItem>
<FormLabel>Supported Chains</FormLabel>
<FormDescription>
Select the chains you want to support for smart accounts
</FormDescription>
<FormControl>
<div className="w-full bg-background">
<MultiNetworkSelector
selectedChainIds={field.value}
onChange={field.onChange}
/>
</div>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="sponsorGas"
@@ -103,6 +103,7 @@ export function AccountHeaderMobileUI(props: AccountHeaderCompProps) {
</div>
<MobileBurgerMenuButton
type="loggedIn"
email={props.account?.email}
logout={props.logout}
connectButton={props.connectButton}
@@ -11,6 +11,19 @@ export function SecondaryNav(props: {
logout: () => void;
connectButton: React.ReactNode;
}) {
return (
<div className="flex items-center gap-6">
<SecondaryNavLinks />
<AccountButton
logout={props.logout}
connectButton={props.connectButton}
account={props.account}
/>
</div>
);
}
export function SecondaryNavLinks() {
return (
<div className="flex items-center gap-6">
<ToolTipLabel
@@ -45,12 +58,6 @@ export function SecondaryNav(props: {
>
Docs
</Link>
<AccountButton
logout={props.logout}
connectButton={props.connectButton}
account={props.account}
/>
</div>
);
}
@@ -16,12 +16,20 @@ import { useTheme } from "next-themes";
import Link from "next/link";
import { useLayoutEffect, useState } from "react";
import { CmdKSearchModal } from "../../components/cmd-k-search";
import { ThirdwebMiniLogo } from "./ThirdwebMiniLogo";
export function MobileBurgerMenuButton(props: {
email: string | undefined;
logout: () => void;
connectButton: React.ReactNode;
}) {
export function MobileBurgerMenuButton(
props:
| {
type: "loggedIn";
email: string | undefined;
logout: () => void;
connectButton: React.ReactNode;
}
| {
type: "loggedOut";
},
) {
const [isMenuOpen, setIsMenuOpen] = useState(false);
const { setTheme, theme } = useTheme();
const [isCMDSearchModalOpen, setIsCMDSearchModalOpen] = useState(false);
@@ -62,41 +70,51 @@ export function MobileBurgerMenuButton(props: {
<XIcon className="size-7 text-muted-foreground" />
</Button>
<SkeletonContainer
skeletonData="[email protected]"
loadedData={props.email}
className="inline-block self-start"
render={(email) => <p className="text-foreground">{email}</p>}
/>
<Link href="/team">
<ThirdwebMiniLogo className="h-5" />
</Link>
<div className="h-4" />
<div className="h-5" />
<div className="[&>button]:!w-full">{props.connectButton}</div>
{props.type === "loggedIn" && (
<>
<SkeletonContainer
skeletonData="[email protected]"
loadedData={props.email}
className="inline-block self-start"
render={(email) => <p className="text-foreground">{email}</p>}
/>
<div className="h-6" />
<div className="h-3" />
<div className="flex flex-col gap-4">
<Link
href="/account"
className="flex items-center gap-2 text-base text-muted-foreground hover:text-foreground "
>
<SettingsIcon className="size-4" />
Account Settings
</Link>
<div className="[&>button]:!w-full">{props.connectButton}</div>
<Button
variant="link"
className="!h-auto hover:!no-underline justify-start gap-2 px-0 py-1 text-start text-base text-muted-foreground hover:text-foreground"
onClick={props.logout}
>
<LogOutIcon className="size-4" />
Log Out
</Button>
</div>
<div className="h-6" />
<div className="h-6" />
<Separator />
<div className="h-6" />
<div className="flex flex-col gap-4">
<Link
href="/account"
className="flex items-center gap-2 text-base text-muted-foreground hover:text-foreground "
>
<SettingsIcon className="size-4" />
Account Settings
</Link>
<Button
variant="link"
className="!h-auto hover:!no-underline justify-start gap-2 px-0 py-1 text-start text-base text-muted-foreground hover:text-foreground"
onClick={props.logout}
>
<LogOutIcon className="size-4" />
Log Out
</Button>
</div>
<div className="h-6" />
<Separator />
<div className="h-6" />
</>
)}
<div className="flex flex-col gap-5">
<Button
@@ -0,0 +1,95 @@
import { ExternalLink, FileText } from "lucide-react";
import Link from "next/link";
import { Badge } from "../../../@/components/ui/badge";
import { Button } from "../../../@/components/ui/button";
import {
Card,
CardContent,
CardFooter,
CardHeader,
CardTitle,
} from "../../../@/components/ui/card";
import { getThirdwebClient } from "../../../@/constants/thirdweb.server";
import { resolveSchemeWithErrorHandler } from "../../../@/lib/resolveSchemeWithErrorHandler";
import { PROJECT_SHOWCASE_DATA } from "../../../lib/project-showcase-constants";
export default async function DetailPage({
params,
}: { params: { slug: string } }) {
const { slug } = params;
const project = PROJECT_SHOWCASE_DATA.find((p) => p.slug === slug);
if (!project) {
return <div>Project not found</div>;
}
return (
<div className="container mx-auto px-4 py-8">
<Card className="overflow-hidden">
<div className="md:flex">
<div className="md:w-1/2 md:pr-6">
<CardHeader className="border-b md:border-b-0">
<CardTitle className="font-bold text-2xl md:text-3xl">
{project.title}
</CardTitle>
</CardHeader>
<CardContent className="pt-6">
<p className="mb-4 text-lg">{project.description}</p>
<div className="mb-6 flex flex-wrap gap-2">
{project.industries.map((industry) => (
<Badge key={industry} variant="secondary">
{industry}
</Badge>
))}
</div>
</CardContent>
<CardFooter className="flex flex-col gap-4 sm:flex-row">
<Button asChild className="w-full sm:w-auto">
<Link
href={project.link}
target="_blank"
rel="noopener noreferrer"
>
<ExternalLink className="mr-2 h-4 w-4" />
Visit Project Website
</Link>
</Button>
{project.case_study && (
<Button asChild variant="outline" className="w-full sm:w-auto">
<Link
href={project.case_study}
target="_blank"
rel="noopener noreferrer"
>
<FileText className="mr-2 h-4 w-4" />
Link to Case Study
</Link>
</Button>
)}
</CardFooter>
</div>
<div className="md:w-1/2">
<div className="relative aspect-video h-full w-full md:aspect-square">
{/* eslint-disable @next/next/no-img-element */}
<img
src={
project.image?.startsWith("ipfs://")
? (resolveSchemeWithErrorHandler({
client: getThirdwebClient(),
uri: project.image,
}) ?? "")
: (project.image ?? "/assets/showcase/default_image.png")
}
alt={`${project.title} Thumbnail`}
width={500}
height={500}
className="rounded-b-lg object-cover md:rounded-r-lg md:rounded-bl-none"
/>
</div>
</div>
</div>
</Card>
</div>
);
}
@@ -0,0 +1,247 @@
import { FileText } from "lucide-react";
import type { Metadata } from "next";
import Image from "next/image";
import Link from "next/link";
import { Badge } from "../../@/components/ui/badge";
import { Button } from "../../@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "../../@/components/ui/card";
import {
Pagination,
PaginationContent,
PaginationItem,
PaginationLink,
PaginationNext,
PaginationPrevious,
} from "../../@/components/ui/pagination";
import { getThirdwebClient } from "../../@/constants/thirdweb.server";
import { resolveSchemeWithErrorHandler } from "../../@/lib/resolveSchemeWithErrorHandler";
import {
PROJECT_SHOWCASE_DATA,
PROJECT_SHOWCASE_INDUSTRIES,
PROJECT_SHOWCASE_ITEMS_PER_PAGE,
} from "../../lib/project-showcase-constants";
import { getAbsoluteUrl } from "../../lib/vercel-utils";
export const metadata: Metadata = {
title: "Project Showcase | Built on thirdweb",
description: "Discover the latest web3 apps and games built on thirdweb",
openGraph: {
title: "Project Showcase | Built on thirdweb",
description: "Discover the latest web3 apps and games built on thirdweb",
images: [
{
url: `${getAbsoluteUrl()}/assets/showcase/og_image.png`,
width: 1200,
height: 630,
},
],
},
};
export const dynamic = "force-dynamic";
export default function ProjectShowcasePage({
searchParams,
}: {
searchParams: { industry?: string; page?: string };
}) {
const selectedIndustry = searchParams.industry || "All";
const currentPage = Number.parseInt(searchParams.page || "1", 10);
const filteredProjects =
selectedIndustry === "All"
? PROJECT_SHOWCASE_DATA
: PROJECT_SHOWCASE_DATA.filter((project) =>
project.industries?.includes(selectedIndustry),
);
const totalPages = Math.ceil(
filteredProjects.length / PROJECT_SHOWCASE_ITEMS_PER_PAGE,
);
const paginatedProjects = filteredProjects.slice(
(currentPage - 1) * PROJECT_SHOWCASE_ITEMS_PER_PAGE,
currentPage * PROJECT_SHOWCASE_ITEMS_PER_PAGE,
);
return (
<div className="min-h-screen bg-background">
<section className="w-full">
<div className="container px-4 md:px-6">
<div className="grid gap-6 lg:grid-cols-[1fr_400px] lg:gap-12 xl:grid-cols-[1fr_600px]">
<div className="flex flex-col justify-center space-y-4">
<div className="space-y-2">
<h1 className="font-bold text-3xl tracking-tighter sm:text-5xl xl:text-6xl/none">
Built on thirdweb
</h1>
<p className="max-w-[600px] md:text-3xl">
Discover the latest web3 apps and games built on thirdweb.
</p>
</div>
<div className="flex flex-col gap-2 min-[400px]:flex-row">
<Link
href="https://thirdweb.com/login"
className="inline-flex h-10 items-center justify-center rounded-md bg-gray-900 px-8 font-medium text-gray-50 text-sm shadow transition-colors hover:bg-gray-900/90 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-gray-950 disabled:pointer-events-none disabled:opacity-50 dark:bg-gray-50 dark:text-gray-900 dark:focus-visible:ring-gray-300 dark:hover:bg-gray-50/90"
>
Get Started
</Link>
<Link
href="https://blog.thirdweb.com/case-studies/"
className="inline-flex h-10 items-center justify-center rounded-md border border-gray-200 bg-white px-8 font-medium text-sm shadow-sm transition-colors hover:bg-gray-100 hover:text-gray-900 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-gray-950 disabled:pointer-events-none disabled:opacity-50 dark:border-gray-800 dark:bg-gray-950 dark:focus-visible:ring-gray-300 dark:hover:bg-gray-800 dark:hover:text-gray-50"
>
View all case studies
</Link>
</div>
</div>
<Image
alt="Hero"
className="mx-auto overflow-hidden object-cover object-center sm:w-full lg:order-last"
height="550"
src="/assets/showcase/abstract-w.png"
width="550"
/>
</div>
</div>
</section>
<main className="container mx-auto px-4 py-12 md:px-6">
<section>
<div className="mb-8">
<div className="mb-8 flex flex-wrap justify-center gap-2">
{PROJECT_SHOWCASE_INDUSTRIES.map((industry) => (
<Link
key={industry}
href={`/project-showcase?industry=${industry}&page=1`}
className={`inline-flex h-10 items-center justify-center rounded-md px-4 py-2 font-medium text-sm transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring ${
selectedIndustry === industry
? "bg-primary text-primary-foreground shadow hover:bg-primary/90"
: "border border-input bg-background hover:bg-accent hover:text-accent-foreground"
}`}
>
{industry}
</Link>
))}
</div>
</div>
<div className="grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-3">
{paginatedProjects.map((project) => (
<div key={project.id} className="block">
<Card className="flex h-full cursor-pointer flex-col overflow-hidden transition-shadow hover:shadow-lg">
<Link
href={`/project-showcase/${project.slug}`}
className="flex-grow"
>
{/* eslint-disable @next/next/no-img-element */}
<img
src={
project.image?.startsWith("ipfs://")
? (resolveSchemeWithErrorHandler({
client: getThirdwebClient(),
uri: project.image,
}) ?? "")
: (project.image ??
"/assets/showcase/default_image.png")
}
alt={project.title}
width={300}
height={200}
className="h-48 w-full object-cover"
/>
<CardHeader>
<CardTitle className="mb-3">{project.title}</CardTitle>
<CardDescription>{project.description}</CardDescription>
</CardHeader>
</Link>
<CardContent>
<div className="flex items-center justify-between">
<div className="flex flex-wrap gap-2">
{project.industries?.map((industry) => (
<Badge key={industry} variant="secondary">
{industry}
</Badge>
))}
</div>
{project.case_study && (
<Link
href={project.case_study}
target="_blank"
// onClick={(e) => e.stopPropagation()}
>
<Button variant="outline" size="sm">
<FileText className="mr-2 h-4 w-4" /> Case Study
</Button>
</Link>
)}
</div>
</CardContent>
</Card>
</div>
))}
</div>
<div className="mt-8 flex flex-col items-center">
<div className="mb-4 text-muted-foreground text-sm">
Showing {paginatedProjects.length} of {filteredProjects.length}{" "}
projects in{" "}
{selectedIndustry === "All" ? "all categories" : selectedIndustry}
</div>
<Pagination>
<PaginationContent>
<PaginationItem>
<Link
href={`/project-showcase?industry=${selectedIndustry}&page=${
currentPage > 1 ? currentPage - 1 : 1
}`}
passHref
legacyBehavior
>
<PaginationPrevious
className={
currentPage <= 1 ? "pointer-events-none opacity-50" : ""
}
/>
</Link>
</PaginationItem>
{Array.from({ length: totalPages }, (_, i) => i + 1).map(
(pageNumber) => (
<PaginationItem key={`page-${pageNumber}`}>
<Link
href={`/project-showcase?industry=${selectedIndustry}&page=${pageNumber}`}
passHref
legacyBehavior
>
<PaginationLink isActive={currentPage === pageNumber}>
{pageNumber}
</PaginationLink>
</Link>
</PaginationItem>
),
)}
<PaginationItem>
<Link
href={`/project-showcase?industry=${selectedIndustry}&page=${
currentPage < totalPages ? currentPage + 1 : totalPages
}`}
passHref
legacyBehavior
>
<PaginationNext
className={
currentPage >= totalPages
? "pointer-events-none opacity-50"
: ""
}
/>
</Link>
</PaginationItem>
</PaginationContent>
</Pagination>
</div>
</section>
</main>
</div>
);
}
@@ -1,8 +1,8 @@
"use client";
import { WithEngineInstance } from "components/engine/EnginePageLayout";
import { EngineAccessTokens } from "components/engine/permissions/engine-access-tokens";
import type { EngineInstancePageProps } from "../types";
import { EngineAccessTokens } from "./components/engine-access-tokens";
export default function Page(props: EngineInstancePageProps) {
const { params } = props;

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