Compare commits

..
Author SHA1 Message Date
Joaquim VergesGitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
fda2284036 Version Packages (#8466)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-11-26 14:03:39 -08:00
Joaquim VergesandGitHub 629efc13b6 [SDK] Add Monad chain definition (#8465) 2025-11-26 13:46:02 -08:00
Joaquim VergesandGitHub 512f9dd3c9 [Docs] Fix AI model response handling in server example (#8459) 2025-11-26 11:05:01 -08:00
jnsdls 919230f0c8 Add daily revalidation to TypeScript pages (#8456)
Added daily revalidation to TypeScript reference pages

This PR adds a revalidation period of 24 hours (86400 seconds) to TypeScript reference pages to ensure content stays fresh while minimizing unnecessary rebuilds.

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

* **Chores**
  * Introduced daily automatic revalidation across multiple portal pages and layouts so content refreshes every 24 hours. This enables incremental static regeneration for affected pages, improving cache freshness and helping ensure users see up-to-date content with reduced stale data risk.

<sub>✏️ Tip: You can customize this high-level summary in your review settings.</sub>
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR introduces a `revalidate` constant set to `86400` seconds (1 day) in multiple files, which specifies that the cached data should be revalidated every day.

### Detailed summary
- Added `export const revalidate = 86400; // revalidate every day` to:
  - `apps/portal/src/app/contracts/extensions/page.tsx`
  - `apps/portal/src/app/react/v5/[...slug]/page.tsx`
  - `apps/portal/src/app/contracts/layout.tsx`
  - `apps/portal/src/app/typescript/v5/[...slug]/page.tsx`
  - `apps/portal/src/app/react/v5/layout.tsx`
  - `apps/portal/src/app/typescript/v5/layout.tsx`
  - `apps/portal/src/app/references/typescript/[version]/[[...slug]]/page.tsx`

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

<!-- end pr-codex -->
2025-11-25 17:23:17 -08:00
Joaquim VergesGitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
3ac8cba2e8 Version Packages (#8458)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-11-25 17:04:57 -08:00
Joaquim VergesandGitHub 35aaf24e8a [SDK] Add payment scheme and improve type documentation for x402 (#8457) 2025-11-25 16:49:40 -08:00
Joaquim VergesGitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
ad2d1757e2 Version Packages (#8452)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-11-24 15:18:50 -08:00
Joaquim VergesandGitHub 52aba0ffb8 [SDK] Skip chain switching for Coinbase Wallet if already on correct chain (#8453) 2025-11-24 15:13:30 -08:00
Joaquim VergesandGitHub 9809d5cf66 [SDK] Add useFetchWithPayment React hook for x402 payment handling (#8444) 2025-11-24 14:42:50 -08:00
0xFirekeeper 8b10674481 Port gas sponsorship analytics to Team usage tab (#8445)
Updated AccountAbstractionAnalytics to support both team and project variants, allowing for flexible rendering based on context. Refactored usage and summary components to accept optional projectId, and updated the team usage page to use the new analytics and summary components. Adjusted SponsoredTransactionsTable usage to match the new props structure.

Closes BLD-500

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on enhancing the `AccountAbstractionAnalytics` and `AccountAbstractionSummary` components to support additional parameters for date ranges and project identification, improving data handling for user operation statistics.

### Detailed summary
- Added `from` and `to` props to `AccountAbstractionSummary` and `AccountAbstractionAnalytics`.
- Made `projectId` optional in `AsyncAccountAbstractionSummary` and `AccountAbstractionSummary`.
- Updated `AccountAbstractionAnalytics` to handle different variants (`project` and `team`).
- Introduced date range handling in the main page logic to fetch user operation statistics based on selected ranges.

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

<!-- end pr-codex -->

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

* **New Features**
  * Account abstraction views now support team- and project-level variants and explicit date-range (from/to) filtering.
  * Analytics components expose flexible props to render project or team analytics based on resolved ranges.

* **Improvements**
  * Data loading sequence refined to fetch authentication, team lookup, and usage metrics before render.
  * UI updated to show a summary section followed by analytics with improved spacing and conditional rendering.

<sub>✏️ Tip: You can customize this high-level summary in your review settings.</sub>
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-11-24 15:45:06 +00:00
Joaquim VergesGitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
a3c76d5f7d Version Packages (#8435)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-11-19 17:59:14 -08:00
Joaquim VergesandGitHub f5f6848ead [SDK] Add fundWalletLink prop to x402 responses (#8443) 2025-11-19 17:56:53 -08:00
FirekeeperandGitHub 82ba2b20fc Add gas metrics chart to account abstraction analytics (#8442) 2025-11-20 07:29:20 +07:00
FirekeeperandGitHub b0e69b8794 General doc improvements + gaming sdk snippets (#8441) 2025-11-20 04:38:59 +07:00
MananTank dd983dab8f Dashboard: Update payment links (#8440)
<!--

## title your PR with this format: "[SDK/Dashboard/Portal] Feature/Fix: Concise title for the changes"

If you did not copy the branch name from Linear, paste the issue tag here (format is TEAM-0000):

## Notes for the reviewer

Anything important to call out? Be sure to also clarify these in your comments.

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on updating URLs and labels related to the payment and bridge functionalities in the dashboard application.

### Detailed summary
- Updated `href` in `layout.tsx` from "https://portal.thirdweb.com/payments/webhooks" to "https://portal.thirdweb.com/bridge/webhooks".
- Changed `href` in `QuickstartSection.client.tsx` from "https://portal.thirdweb.com/payments/products" to "https://portal.thirdweb.com/bridge/products".
- Modified `link` in `highlights-card.tsx` to point to `/bridge/configuration` instead of `/payments/settings`.
- Changed label from "Fee Revenue" to "Bridge Fee Revenue".

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

<!-- end pr-codex -->

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->

## Summary by CodeRabbit

* **Documentation**
  * Updated dashboard links and documentation references to point to Bridge resources instead of Payments.
  * Updated fee-related labels to reflect Bridge branding terminology.

<sub>✏️ Tip: You can customize this high-level summary in your review settings.</sub>

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-11-19 20:51:21 +00:00
MananTank 068cb592dc Dashboard: Add Indexer service filter in chainlist (#8439)
<!--

## title your PR with this format: "[SDK/Dashboard/Portal] Feature/Fix: Concise title for the changes"

If you did not copy the branch name from Linear, paste the issue tag here (format is TEAM-0000):

## Notes for the reviewer

Anything important to call out? Be sure to also clarify these in your comments.

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR adds a new service entry for "Indexer" in the `services` array within the `products.ts` file, including an associated icon.

### Detailed summary
- Added import for `InsightIcon`.
- Introduced a new service object:
  - `icon`: `InsightIcon`
  - `id`: "insight"
  - `link`: "https://thirdweb.com/gateway"
  - `name`: "Indexer"

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

<!-- end pr-codex -->

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

* **New Features**
  * Introduced a new Indexer service now available in the dashboard, delivering enhanced data insights and advanced blockchain indexing capabilities. This integration provides users with streamlined gateway access, enabling efficient data querying, improved management workflows, and better access to blockchain data for comprehensive analysis and operations.<sub>✏️ Tip: You can customize this high-level summary in your review settings.</sub>
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-11-19 19:56:25 +00:00
MananTank 359d008d38 [PRO-106] Dashboard: Update Bridge stats (#8430)
<!--

## title your PR with this format: "[SDK/Dashboard/Portal] Feature/Fix: Concise title for the changes"

If you did not copy the branch name from Linear, paste the issue tag here (format is TEAM-0000):

## Notes for the reviewer

Anything important to call out? Be sure to also clarify these in your comments.

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR updates the statistics related to supported chains, tokens, and routes in the bridge application. It enhances the data representation by introducing a new object for numerical values and modifies the `bridge-page` component to utilize these updated statistics.

### Detailed summary
- Updated `supportedChains` from "90+" to "95+".
- Updated `supportedTokens` from "6700+" to "10,000+".
- Updated `supportedRoutes` from "14M+" to "15M+".
- Introduced a new constant `bridgeStatsNumbers` with numerical values for chains, tokens, and routes.
- Modified `bridge-page.tsx` to use `bridgeStatsNumbers` for displaying data in `DataSquare` components.

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

<!-- end pr-codex -->
2025-11-19 19:36:32 +00:00
MananTank 7b5682ce27 Link to team billing page in faucet button on free plan (#8429)
<!--

## title your PR with this format: "[SDK/Dashboard/Portal] Feature/Fix: Concise title for the changes"

If you did not copy the branch name from Linear, paste the issue tag here (format is TEAM-0000):

## Notes for the reviewer

Anything important to call out? Be sure to also clarify these in your comments.

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR enhances the `FaucetButton` component by introducing a new condition to handle cases where a paid plan is required to access the faucet. It provides a link to upgrade the plan if the user is on a free plan.

### Detailed summary
- Added a check for `canClaimFaucetQuery.data.type` being `"paid-plan-required"`.
- If true, renders a button linking to the billing page with an upgrade message.
- Removed a TODO comment regarding upsell paths for subscription plans.
- Cleaned up the conditional rendering logic for the button.

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

<!-- end pr-codex -->

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

* **New Features**
  * Enhanced faucet button for paid-plan scenarios: when claiming is blocked by plan limits, the button now links directly to billing/subscription management and shows an inline arrow indicator for a clearer, actionable path.
* **Bug Fixes**
  * Preserved existing disabled states for throttle and unsupported-chain cases to avoid regressions.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-11-19 19:36:32 +00:00
MananTank 51f46d34cf [PRO-120] Dashboard: Update Explore page (#8437)
<!--

## title your PR with this format: "[SDK/Dashboard/Portal] Feature/Fix: Concise title for the changes"

If you did not copy the branch name from Linear, paste the issue tag here (format is TEAM-0000):

## Notes for the reviewer

Anything important to call out? Be sure to also clarify these in your comments.

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on refactoring and enhancing the `Explore` page in the dashboard application, including the removal of obsolete files, updates to UI components, and the introduction of new contract card components to improve usability and visual consistency.

### Detailed summary
- Deleted obsolete files: `tokens-section.tsx`, `page.tsx`, `opengraph-image.png`, `publish-submit.tsx`.
- Updated class names for improved z-index in links.
- Changed heading style from `font-bold` to `font-semibold`.
- Removed unnecessary paragraphs and added new `PopularSection` component.
- Updated `ContractRow` to accept an optional `prefixCard` prop.
- Introduced `TokenContractCard`, `NFTCollectionCard`, and `CoinCard` components.
- Adjusted `ExploreCategory` interface to allow optional `description`.
- Simplified contract categories and removed unused ones.
- Enhanced overall layout and spacing on the `ExplorePage`.

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

<!-- end pr-codex -->
2025-11-19 19:23:39 +00:00
MananTank 63a45e21cd [PRO-121] Dashboard: show all token contracts in tokens tab, conditionally show contracts tab in sidebar (#8436)
<!--

## title your PR with this format: "[SDK/Dashboard/Portal] Feature/Fix: Concise title for the changes"

If you did not copy the branch name from Linear, paste the issue tag here (format is TEAM-0000):

## Notes for the reviewer

Anything important to call out? Be sure to also clarify these in your comments.

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR primarily focuses on enhancing the contract import functionality by introducing contract type validation (token vs non-token) and improving contract fetching methods, along with various updates to the project structure and dependencies.

### Detailed summary
- Added `allowedContractType` prop to `ImportModal` for contract type validation.
- Introduced `projectContractsCacheTag` function for caching project contracts.
- Updated `revalidatePathAction` to include `revalidateTag`.
- Modified contract fetching methods to filter by type.
- Refactored components to use new contract fetching logic.
- Updated `ContractTable` to include project and team IDs.
- Enhanced error handling in `ImportForm` for invalid contract types.
- Various dependency updates including `p-limit` and others.

> 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 -->

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->

## Summary by CodeRabbit

* **New Features**
  * Implemented type-based contract filtering to distinguish between token contracts and non-token contracts within projects.
  * Added new Tokens section to project dashboard for dedicated token asset management.
  * Enhanced contract import process with validation to ensure contracts are imported to the correct section based on type.

* **Chores**
  * Updated project dependencies.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-11-19 19:23:39 +00:00
Joaquim VergesandGitHub c11fe95416 [Dashboard] Add x402 fee configuration (#8434) 2025-11-18 17:02:33 -08:00
Joaquim VergesGitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
851fee1c83 Version Packages (#8433)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-11-18 14:19:27 -08:00
Joaquim VergesandGitHub 9983e6ccfd [SDK] Improve SIWE chain management for external wallets (#8432) 2025-11-18 14:03:34 -08:00
MananTank 73c924b440 [PRO-112] Dashboard: Project FTUX copy changes (#8428)
<!--

## title your PR with this format: "[SDK/Dashboard/Portal] Feature/Fix: Concise title for the changes"

If you did not copy the branch name from Linear, paste the issue tag here (format is TEAM-0000):

## Notes for the reviewer

Anything important to call out? Be sure to also clarify these in your comments.

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on updating the titles and text in the `ProjectFTUX` components to reflect changes in the API endpoint and improve the clarity of the secret key information.

### Detailed summary
- Changed the `title` in `ProjectFTUX.tsx` from "POST /v1/auth/initiate" to "POST /v1/transactions".
- Updated the text in `SecretKeySection.tsx` to clarify the reference to the full secret key, removing a comma for better readability.

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

<!-- end pr-codex -->

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->

## Summary by CodeRabbit

* **Bug Fixes**
  * Corrected punctuation in descriptive text within the project setup guide

* **Documentation**
  * Updated API endpoint example in the getting started section

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-11-18 16:30:46 +00:00
Joaquim VergesGitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2976af3a63 Version Packages (#8426)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-11-17 15:56:04 -08:00
Joaquim VergesandGitHub 419fb31601 [SDK] Better eip5792 handling (#8425) 2025-11-17 15:49:57 -08:00
c76fe10dd0 started portal docs (#8410)
Co-authored-by: 0xFirekeeper <[email protected]>
2025-11-17 23:37:34 +07:00
MananTank c63ef1c4e9 [MNY-310] Dashboard: Improve login redirect path validation (#8422)
<!--

## title your PR with this format: "[SDK/Dashboard/Portal] Feature/Fix: Concise title for the changes"

If you did not copy the branch name from Linear, paste the issue tag here (format is TEAM-0000):

## Notes for the reviewer

Anything important to call out? Be sure to also clarify these in your comments.

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR modifies the `isValidEncodedRedirectPath` function to enhance its validation logic for decoded paths, ensuring they start with a single slash and belong to the `thirdweb.com` domain.

### Detailed summary
- Removed comments about decoding URI components and path validation.
- Added a check to ensure `decodedPath` starts with a single slash.
- Introduced a `URL` object to validate that the hostname is `thirdweb.com`.
- Simplified the return logic for invalid paths.

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

<!-- end pr-codex -->

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

* **Bug Fixes**
  * Enhanced login redirect validation to ensure redirects are properly verified and authenticated for the correct domain.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-11-17 13:09:54 +00:00
Jonas Daniels c7ff3b1020 Add /gateway to framer-rewrites.js 2025-11-14 15:00:19 -08:00
MananTank b06a9184c1 [PRO-105] Dashboard: Update service names (#8419)
<!--

## title your PR with this format: "[SDK/Dashboard/Portal] Feature/Fix: Concise title for the changes"

If you did not copy the branch name from Linear, paste the issue tag here (format is TEAM-0000):

## Notes for the reviewer

Anything important to call out? Be sure to also clarify these in your comments.

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on renaming and refactoring the usage of `products` to `services` across various components, updating service definitions, and improving the rendering of product cards in the UI.

### Detailed summary
- Renamed `products` to `services` in relevant components.
- Updated service definitions with new titles and descriptions.
- Modified the `ChainListRow` and `ChainServiceFilter` components to use `services`.
- Enhanced `SupportedProductsSection` to conditionally render `ProductCard`.
- Added new `ProductCard` component for UI rendering.

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

<!-- end pr-codex -->
2025-11-14 21:08:47 +00:00
MananTank 5080b4707f [PRO-98] Update Project FTUX UI (#8418)
<!--

## title your PR with this format: "[SDK/Dashboard/Portal] Feature/Fix: Concise title for the changes"

If you did not copy the branch name from Linear, paste the issue tag here (format is TEAM-0000):

## Notes for the reviewer

Anything important to call out? Be sure to also clarify these in your comments.

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR primarily focuses on updating the UI components by replacing the `NebulaIcon` with `BotIcon`, enhancing styling, and refactoring code for better readability and functionality across various components.

### Detailed summary
- Replaced `NebulaIcon` with `BotIcon` in multiple components.
- Improved styles for headings and paragraphs in `ClientIDSection` and `SecretKeySection`.
- Updated layout properties in `ProjectWalletSection` and other components.
- Added a new `CodeShowcase` component to display code examples.
- Enhanced `ProjectFTUX` to accept a `projectWalletSection` prop.
- Refactored several components for better structure and readability.
- Adjusted margins and paddings for improved UI consistency.

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

<!-- end pr-codex -->

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

* **New Features**
  * Multi-language code showcase with a language selector for integration examples
  * Configurable wallet layouts and ability to inject a wallet area into onboarding/FTUX

* **UI/UX Improvements**
  * Refined spacing, typography, and responsive layouts across project pages and sidebar
  * Added popular framework/product brand icons; replaced several AI/sidebar icons with a unified Bot icon
  * Simplified sidebar footer links and adjusted paddings and avatar sizes

* **Chores**
  * Updated story examples to include new layout args
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-11-14 21:08:47 +00:00
Yash094 d6bc43c34d support form changes (#8414)
<!--

## title your PR with this format: "[SDK/Dashboard/Portal] Feature/Fix: Concise title for the changes"

If you did not copy the branch name from Linear, paste the issue tag here (format is TEAM-0000):

## Notes for the reviewer

Anything important to call out? Be sure to also clarify these in your comments.

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on refactoring and enhancing various support forms in the application, including the removal of outdated components and the introduction of new fields to improve user input for support tickets.

### Detailed summary
- Deleted `payments`, `contracts`, and `AffectedAreaInput` components.
- Updated `OtherSupportForm` with a new label.
- Modified `TokensMarketplaceSupportForm` to include a new input for "Support ID in logs".
- Removed dynamic imports for `ContractSupportForm` and `PaymentsSupportForm`.
- Updated `productOptions` labels for clarity.
- Replaced `ENGINE_TYPES` and `ENGINE_PROBLEM_AREAS` with `API_SDK_OPTIONS`.
- Enhanced `EngineSupportForm` with new inputs and logic based on selected SDK.
- Introduced new components for user wallet issues and account abstraction.
- Updated `PROBLEM_AREAS` to include new components for social/email login and user wallet custom auth.

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

<!-- end pr-codex -->

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

* **New Features**
  * Added "Agents/x402 and AI" support option and an optional "Support ID in logs" field.

* **Improvements**
  * Reorganized and simplified support forms: Wallets → Working with User Wallets, Transactions → API/SDKs, Tokens/Marketplace → Creating, Bridging and swapping tokens; token input accepts address or symbol.
  * Streamlined Connect and Engine flows with focused issue categories (social/email login, custom auth, account abstraction, third‑party wallets, API/SDK selection).

* **Chores**
  * Removed legacy Contracts, Payments and affected-area form screens.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-11-14 20:55:30 +00:00
github-actions[bot]andJonas Daniels ef577028fd Version Packages 2025-11-14 08:28:40 -08:00
MananTank 304700f318 Improve Mobile detection in WC connection (#8415)
<!--

## title your PR with this format: "[SDK/Dashboard/Portal] Feature/Fix: Concise title for the changes"

If you did not copy the branch name from Linear, paste the issue tag here (format is TEAM-0000):

## Notes for the reviewer

Anything important to call out? Be sure to also clarify these in your comments.

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR enhances the `isMobile` function to include viewport detection for mobile devices and refines the QR overlay creation logic for desktop environments by ensuring existing overlays are cleaned up before creating new ones.

### Detailed summary
- Added viewport detection in `isMobile` function to return `true` if `window.innerWidth` is less than 640.
- Refactored QR overlay creation in `create-wallet.ts` to ensure existing overlays are destroyed before creating a new one.

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

<!-- end pr-codex -->

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->

## Summary by CodeRabbit

* **Bug Fixes**
  * Improved mobile device detection based on viewport size.
  * Enhanced QR code overlay display with better error handling and resource cleanup.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-11-14 16:10:19 +00:00
kumaryash90 6cd681c9ce Fix resolve implementation (#8413)
<!--

## title your PR with this format: "[SDK/Dashboard/Portal] Feature/Fix: Concise title for the changes"

If you did not copy the branch name from Linear, paste the issue tag here (format is TEAM-0000):

## Notes for the reviewer

Anything important to call out? Be sure to also clarify these in your comments.

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on improving the `resolveImplementation` function in the `thirdweb` package by enhancing the implementation resolution process for proxy contracts.

### Detailed summary
- Replaced the existing implementation resolution logic with a more robust approach using `Promise.all` to fetch implementations from both storage and contract calls.
- Introduced a loop to handle multiple implementations, ensuring that valid addresses are checked before proceeding.
- Updated the return logic to ensure correct addresses and bytecode are returned based on the resolved implementation.

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

<!-- end pr-codex -->

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->

## Summary by CodeRabbit

* **Bug Fixes**
  * Improved the reliability of contract implementation resolution to handle edge cases more robustly when detecting implementations across proxy contracts and various contract architecture types. Processing is now more efficient through concurrent implementation detection and optimized fallback handling, ensuring more consistent and faster results in complex contract scenarios.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-11-14 15:29:13 +00:00
FirekeeperandGitHub 144bb33ffd [Dashboard] Move Contracts back to top level sidebar (#8412) 2025-11-14 01:47:06 +07:00
jnsdls 01116bb650 consolidate gateway navbar (#8411)
# [Dashboard] Feature: Reorganize Gateway section in project sidebar

## Notes for the reviewer

This PR reorganizes the project sidebar by:
- Creating a new "Gateway" submenu that contains both RPC and Indexer (formerly Insight)
- Moving the Insight page to Gateway/Indexer
- Simplifying the Indexer FTUX component
- Updating all related file paths and imports

## How to test

Navigate to the project sidebar and verify:
- The Gateway submenu appears with RPC and Indexer options
- The former Insight page now appears under Gateway/Indexer
- All functionality continues to work as expected

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

* **New Features**
  * Added Indexer page and onboarding card; introduced Gateway submenu grouping RPC and Indexer.
  * Introduced a Wallets submenu consolidating wallet-related links; added a Bridge link and highlighted a monetization item with a "New" badge.
  * Sidebar now shows an Engine link when dedicated engines exist.

* **Refactor**
  * Removed standalone Insight product/page and its onboarding card.
  * Simplified engine onboarding and empty-state flow.

* **Chores**
  * Added redirects routing old insight/rpc paths to gateway equivalents.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on restructuring the `dashboard` application by reorganizing components and updating routes. It removes unused files, modifies imports, and adjusts the sidebar layout to improve navigation and functionality.

### Detailed summary
- Deleted several unused components related to `insight`.
- Moved `RpcAnalytics` and `MethodsTable` to a new path under `gateway`.
- Updated `loginRedirect` paths in several components.
- Added a new `InsightFTUX` component.
- Adjusted sidebar links and layout to reflect new component structure.
- Removed references to `InsightIcon` and related logic in various files.

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

<!-- end pr-codex -->
2025-11-13 13:00:24 +00:00
joaquim-verges f07b4b56c5 [SDK] Rename otherWallet to allConnectedWallets in autoConnect callback (#8408)
<!--

## title your PR with this format: "[SDK/Dashboard/Portal] Feature/Fix: Concise title for the changes"

If you did not copy the branch name from Linear, paste the issue tag here (format is TEAM-0000):

## Notes for the reviewer

Anything important to call out? Be sure to also clarify these in your comments.

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on renaming the `otherWallet` parameter to `allConnectedWallets` in the `onConnect` callback to improve clarity and consistency in the code.

### Detailed summary
- Updated the `onConnect` callback type in `packages/thirdweb/src/wallets/connection/types.ts` to use `OnConnectCallback`.
- Renamed `otherWallet` to `allConnectedWallets` in the `onConnect` function signature.

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

<!-- end pr-codex -->
2025-11-12 22:46:33 +00:00
joaquim-verges 9442dc7729 [SDK] Rename otherWallets to allConnectedWallets in onConnect callback (#8407)
<!--

## title your PR with this format: "[SDK/Dashboard/Portal] Feature/Fix: Concise title for the changes"

If you did not copy the branch name from Linear, paste the issue tag here (format is TEAM-0000):

## Notes for the reviewer

Anything important to call out? Be sure to also clarify these in your comments.

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on updating the `onConnect` callback in the `AutoConnect` component to improve clarity in variable naming, changing `otherWallets` to `allConnectedWallets`.

### Detailed summary
- Changed the parameter name in `onConnect` from `otherWallets` to `allConnectedWallets`.
- Updated the console log statement to reflect the new parameter name, changing it from "other wallets that were also connected" to "all connected wallets".

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

<!-- end pr-codex -->

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->

## Summary by CodeRabbit

* **Breaking Changes**
  * Updated parameter naming in the AutoConnect callback configuration to align with updated documentation.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-11-12 22:46:32 +00:00
MananTank 6c4e684ee6 [PRO-90] Dashboard: Project > Bridge page changes (#8406)
<!--

## title your PR with this format: "[SDK/Dashboard/Portal] Feature/Fix: Concise title for the changes"

If you did not copy the branch name from Linear, paste the issue tag here (format is TEAM-0000):

## Notes for the reviewer

Anything important to call out? Be sure to also clarify these in your comments.

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on refactoring the payments and bridge components in the dashboard application. It updates routes, modifies component structures, and enhances the user interface for better functionality and clarity.

### Detailed summary
- Deleted `view-tx-status.tsx` and `project-settings-breadcrumb.tsx`.
- Updated payment-related paths to bridge configuration.
- Refactored `FeatureCard` and `QuickstartSection` components.
- Changed `TableRow` to `PurchaseTableRow` in `PaymentsTableRow`.
- Modified layout and routing in `bridge/layout.tsx` and `bridge/page.tsx`.
- Enhanced UI components for better responsiveness and clarity.

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

<!-- end pr-codex -->

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

* **New Features**
  * Added a Bridge configuration page with Overview and Configuration tabs.
  * Transaction hashes now show as copyable links in the payments table.

* **Improvements**
  * Updated Quickstart cards and labels to streamline onboarding and emphasize bridge setup.
  * Navigation paths updated to the Bridge configuration flow.
  * Added a styling hook for link icons and refined payments table layout and semantics.

* **Removals**
  * Removed a legacy breadcrumb component and an old transaction status widget.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-11-12 22:35:49 +00:00
nischitpra 68978711f8 default sort desc for tokens (#8405)
<!-- start pr-codex -->

## PR-Codex overview
This PR introduces a new sorting option for retrieving tokens, allowing results to be ordered in descending order based on their balance.

### Detailed summary
- Added a new property `sort_order` with the value `"desc"` to the object passed to `getV1Tokens`.

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

<!-- end pr-codex -->

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->

## Summary by CodeRabbit

* **Improvements**
  * Token balances are now displayed in consistent descending order.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-11-12 21:53:55 +00:00
MananTank eca4ce2b82 Dashboard: Update announcement banner (#8404)
<!--

## title your PR with this format: "[SDK/Dashboard/Portal] Feature/Fix: Concise title for the changes"

If you did not copy the branch name from Linear, paste the issue tag here (format is TEAM-0000):

## Notes for the reviewer

Anything important to call out? Be sure to also clarify these in your comments.

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on updating the `label` prop of the `AnnouncementBannerUI` component to provide more detailed information about recent changes related to Wallets in the Project Dashboard.

### Detailed summary
- Updated the `label` prop of the `AnnouncementBannerUI` component from:
  - "Project View Update - Wallets"
  - To: "Transactions, Vault, and Account Abstraction are now part of Wallets in Project Dashboard. View changes"

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

<!-- end pr-codex -->

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->

## Summary by CodeRabbit

* **New Features**
  * Updated dashboard announcement banner with information about recent Wallets feature changes, including details on Transactions, Vault, and Account Abstraction integration.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-11-12 20:59:56 +00:00
289 changed files with 6757 additions and 10294 deletions
+1
View File
@@ -21,6 +21,7 @@ module.exports = [
"/rpc",
"/insight",
"/storage",
"/gateway",
// -- end scale category
// -- ai
+1
View File
@@ -48,6 +48,7 @@
"next-themes": "^0.4.6",
"nextjs-toploader": "^1.6.12",
"nuqs": "^2.4.3",
"p-limit": "^7.2.0",
"papaparse": "^5.5.3",
"pluralize": "^8.0.0",
"posthog-js": "1.256.1",
+10
View File
@@ -115,6 +115,16 @@ const projectPageRedirects = [
permanent: false,
source: `${projectRoute}/connect/analytics`,
},
{
destination: `${projectRoute}/gateway/indexer/:path*`,
permanent: false,
source: `${projectRoute}/insight/:path*`,
},
{
destination: `${projectRoute}/gateway/rpc/:path*`,
permanent: false,
source: `${projectRoute}/rpc/:path*`,
},
];
const teamPageRedirects = [
+5 -1
View File
@@ -1,6 +1,6 @@
"use server";
import { revalidatePath } from "next/cache";
import { revalidatePath, revalidateTag } from "next/cache";
export async function revalidatePathAction(
path: string,
@@ -8,3 +8,7 @@ export async function revalidatePathAction(
) {
revalidatePath(path, type);
}
export async function revalidateCacheTagAction(tag: string) {
revalidateTag(tag);
}
+29 -2
View File
@@ -291,8 +291,11 @@ const cached_getAggregateUserOpUsage = unstable_cache(
]);
// Aggregate stats across wallet types
return userOpStats.reduce(
(acc, curr) => {
const aggregated = userOpStats.reduce(
(
acc: UserOpStats & { gasPriceSum: number; gasPriceCount: number },
curr,
) => {
// Skip testnets from the aggregated stats
if (curr.chainId) {
const chain = chains.data.find(
@@ -306,6 +309,10 @@ const cached_getAggregateUserOpUsage = unstable_cache(
acc.successful += curr.successful;
acc.failed += curr.failed;
acc.sponsoredUsd += curr.sponsoredUsd;
acc.gasUnits += curr.gasUnits;
// For avgGasPrice, we'll track sum and count for proper averaging
acc.gasPriceSum += curr.avgGasPrice * curr.successful;
acc.gasPriceCount += curr.successful;
return acc;
},
{
@@ -313,8 +320,28 @@ const cached_getAggregateUserOpUsage = unstable_cache(
failed: 0,
sponsoredUsd: 0,
successful: 0,
gasUnits: 0,
avgGasPrice: 0,
gasPriceSum: 0,
gasPriceCount: 0,
},
);
// Calculate the proper average gas price
aggregated.avgGasPrice =
aggregated.gasPriceCount > 0
? aggregated.gasPriceSum / aggregated.gasPriceCount
: 0;
// Return only the UserOpStats fields
return {
date: aggregated.date,
failed: aggregated.failed,
sponsoredUsd: aggregated.sponsoredUsd,
successful: aggregated.successful,
gasUnits: aggregated.gasUnits,
avgGasPrice: aggregated.avgGasPrice,
};
},
["getAggregateUserOpUsage"],
{
@@ -0,0 +1,6 @@
export function projectContractsCacheTag(params: {
teamId: string;
projectId: string;
}) {
return `${params.teamId}/${params.projectId}/project-contracts`;
}
@@ -1,58 +1,111 @@
import { unstable_cache } from "next/cache";
import pLimit from "p-limit";
import { defineChain } from "thirdweb";
import { getContract } from "thirdweb/contract";
import {
getProjectContracts,
type ProjectContract,
} from "@/api/project/getProjectContracts";
import { fetchChainWithLocalOverrides } from "@/utils/fetchChainWithLocalOverrides";
import { supportedERCs } from "@/utils/supportedERCs";
import { serverThirdwebClient } from "../../constants/thirdweb-client.server";
import { resolveFunctionSelectors } from "../../lib/selectors";
import { projectContractsCacheTag } from "./cache-tag";
export async function getSortedDeployedContracts(params: {
onlyMainnet?: boolean;
type GetFilteredProjectContractsParams = {
teamId: string;
projectId: string;
authToken: string;
deploymentType: string | undefined;
}) {
};
type ProjectContractWithIsToken = ProjectContract & { isToken: boolean };
async function getProjectContractsWithIsTokenInfo(
params: GetFilteredProjectContractsParams,
): Promise<ProjectContractWithIsToken[]> {
const contracts = await getProjectContracts({
authToken: params.authToken,
deploymentType: params.deploymentType,
deploymentType: undefined, // don't pass it to api to fetch all contracts, we'll filter/merge manually here
projectId: params.projectId,
teamId: params.teamId,
});
const chainIds = Array.from(new Set(contracts.map((c) => c.chainId)));
const chains = (
await Promise.allSettled(
chainIds.map((chainId) =>
fetchChainWithLocalOverrides(chainId.toString()),
),
)
)
.filter((c) => c.status === "fulfilled")
.map((c) => c.value)
.filter((c) => c !== null);
const assetContracts = contracts.filter((c) => c.deploymentType === "asset");
const otherContracts = contracts.filter((c) => c.deploymentType !== "asset");
const mainnetContracts: ProjectContract[] = [];
const testnetContracts: ProjectContract[] = [];
// "asset" deployment type contract is assumed to be a "token" type contract already
// for others, check if its a "token" type contract (isERC20, isERC721 or isERC1155)
for (const contract of contracts) {
const chain = chains.find(
(chain) => contract.chainId === chain.chainId.toString(),
const limit = pLimit(20);
const othersContractWithIsTokenInfo: ProjectContractWithIsToken[] =
await Promise.all(
otherContracts.map(async (c) => {
return limit(async () => {
try {
const contract = getContract({
address: c.contractAddress,
// eslint-disable-next-line no-restricted-syntax
chain: defineChain(Number(c.chainId)),
client: serverThirdwebClient,
});
const functionSelectors = await resolveFunctionSelectors(contract);
const ercs = supportedERCs(functionSelectors);
if (ercs.isERC20 || ercs.isERC721 || ercs.isERC1155) {
return {
...c,
isToken: true,
};
}
return {
...c,
isToken: false,
};
} catch {
return {
...c,
isToken: false,
};
}
});
}),
);
if (chain && chain.status !== "deprecated") {
if (chain.testnet) {
testnetContracts.push(contract);
} else {
mainnetContracts.push(contract);
}
for (const c of othersContractWithIsTokenInfo) {
if (c.deploymentType === "asset") {
assetContracts.push(c);
} else {
otherContracts.push(c);
}
}
mainnetContracts.sort((a, b) => Number(a.chainId) - Number(b.chainId));
if (params.onlyMainnet) {
return mainnetContracts;
}
testnetContracts.sort((a, b) => Number(a.chainId) - Number(b.chainId));
return [...mainnetContracts, ...testnetContracts];
return [
...assetContracts.map(
(c) => ({ ...c, isToken: true }) as ProjectContractWithIsToken,
),
...othersContractWithIsTokenInfo,
];
}
export async function getFilteredProjectContracts(
params: GetFilteredProjectContractsParams & {
type: "token-contracts" | "non-token-contracts";
},
) {
const cached_getProjectContractsWithIsTokenInfo = unstable_cache(
getProjectContractsWithIsTokenInfo,
["get-project-contracts-with-is-token-info"],
{
revalidate: 3600, // 1 hour
tags: [projectContractsCacheTag(params)],
},
);
const contracts = await cached_getProjectContractsWithIsTokenInfo(params);
if (params.type === "token-contracts") {
return contracts.filter((c) => c.isToken);
} else {
return contracts.filter((c) => !c.isToken);
}
}
+27
View File
@@ -0,0 +1,27 @@
import type { Project } from "@/api/project/projects";
type X402Fee = {
feeRecipient: string;
feeBps: number;
};
/**
* Extract x402 fee configuration from project's engineCloud service
*/
export function getX402Fees(project: Project): X402Fee {
const engineCloudService = project.services.find(
(service) => service.name === "engineCloud",
);
if (!engineCloudService) {
return {
feeRecipient: "",
feeBps: 0,
};
}
return {
feeRecipient: engineCloudService.x402FeeRecipient || "",
feeBps: engineCloudService.x402FeeBPS || 0,
};
}
@@ -67,7 +67,7 @@ export function FullWidthSidebarLayout(props: {
)}
>
{/* left - sidebar */}
<Sidebar className="pt-2" collapsible="icon" side="left">
<Sidebar collapsible="icon" side="left">
<SidebarContent className="p-2">
<RenderSidebarMenu links={contentSidebarLinks} />
</SidebarContent>
@@ -66,13 +66,16 @@ export type ProjectPageHeaderProps = {
href: string;
};
className?: string;
// TODO: add task card component
task?: never;
};
export function ProjectPageHeader(props: ProjectPageHeaderProps) {
return (
<header className="container max-w-7xl py-6 relative">
<header
className={cn("container max-w-7xl py-6 relative", props.className)}
>
{/* top row */}
<div className="flex justify-between items-start mb-4">
{/* left - icon */}
@@ -13,13 +13,19 @@ type ProjectPageProps = {
header: ProjectPageHeaderProps;
footer?: ProjectPageFooterProps;
tabs?: TabPathLink[];
containerClassName?: string;
};
export function ProjectPage(props: React.PropsWithChildren<ProjectPageProps>) {
return (
<div className="flex flex-col grow">
<div className={cn(!props.tabs && "border-b")}>
<ProjectPageHeader {...props.header} />
<ProjectPageHeader
{...props.header}
className={cn(
props.tabs && props.tabs.length > 0 ? "pt-8 pb-6" : "py-8",
)}
/>
{props.tabs && (
<TabPathLinks
scrollableClassName="container max-w-7xl"
@@ -28,7 +34,11 @@ export function ProjectPage(props: React.PropsWithChildren<ProjectPageProps>) {
)}
</div>
<main className="container max-w-7xl pt-6">{props.children}</main>
<main
className={cn("container max-w-7xl pt-6", props.containerClassName)}
>
{props.children}
</main>
<div className="h-20" />
{props.footer && (
<div className="border-t mt-auto">
@@ -62,6 +62,8 @@ function Story() {
client={storybookThirdwebClient}
contracts={[]}
pageSize={10}
projectId="project-id"
teamId="team-id"
projectSlug={projectSlug}
removeContractFromProject={removeContractStub}
teamSlug={teamSlug}
@@ -74,6 +76,8 @@ function Story() {
client={storybookThirdwebClient}
contracts={[popularPolygonNFTs[0] as ProjectContract]}
pageSize={10}
projectId="project-id"
teamId="team-id"
projectSlug={projectSlug}
removeContractFromProject={removeContractStub}
teamSlug={teamSlug}
@@ -86,6 +90,8 @@ function Story() {
client={storybookThirdwebClient}
contracts={popularPolygonNFTs}
pageSize={10}
projectId="project-id"
teamId="team-id"
projectSlug={projectSlug}
removeContractFromProject={removeContractStub}
teamSlug={teamSlug}
@@ -95,6 +101,8 @@ function Story() {
<BadgeContainer label="10 Contracts, 2 chains">
<ContractTableUI
projectId="project-id"
teamId="team-id"
client={storybookThirdwebClient}
contracts={[...popularPolygonNFTs, ...EthereumPopularNFTs]}
pageSize={10}
@@ -114,6 +122,8 @@ function Story() {
...basePopularTokens,
]}
pageSize={10}
projectId="project-id"
teamId="team-id"
projectSlug={projectSlug}
removeContractFromProject={removeContractStub}
teamSlug={teamSlug}
@@ -32,6 +32,8 @@ import { useAllChainsData } from "@/hooks/chains/allChains";
import { removeContractFromProject } from "@/hooks/project-contracts";
import { ChainIconClient } from "@/icons/ChainIcon";
import { cn } from "@/lib/utils";
import { revalidateCacheTagAction } from "../../../actions/revalidate";
import { projectContractsCacheTag } from "../../../api/project/cache-tag";
import {
ContractNameCell,
ContractTypeCell,
@@ -57,6 +59,8 @@ export function ContractTable(props: {
client={props.client}
contracts={props.contracts}
pageSize={props.pageSize}
projectId={props.projectId}
teamId={props.teamId}
projectSlug={props.projectSlug}
removeContractFromProject={async (contractId) => {
await removeContractFromProject({
@@ -77,6 +81,8 @@ export function ContractTableUI(props: {
removeContractFromProject: (contractId: string) => Promise<void>;
client: ThirdwebClient;
variant: "asset" | "contract";
teamId: string;
projectId: string;
teamSlug: string;
projectSlug: string;
}) {
@@ -243,6 +249,13 @@ export function ContractTableUI(props: {
<ContractActionsCell
contractId={contract.id}
onContractRemoved={() => {
revalidateCacheTagAction(
projectContractsCacheTag({
teamId: props.teamId,
projectId: props.projectId,
}),
);
setDeletedContractIds((v) => {
return [...v, contract.id];
});
@@ -178,7 +178,7 @@ export async function ContractCard({
{auditLink && !isBeta && (
<>
<Link
className="relative z-1 flex items-center gap-1 font-medium text-sm text-success-text hover:underline"
className="relative z-10 flex items-center gap-1 font-medium text-sm text-success-text hover:underline"
href={auditLink}
rel="noopener noreferrer"
target="_blank"
@@ -1,11 +1,23 @@
"use client";
import { zodResolver } from "@hookform/resolvers/zod";
import { ArrowDownToLineIcon, ExternalLinkIcon } from "lucide-react";
import { UnderlineLink } from "@workspace/ui/components/UnderlineLink";
import {
ArrowDownToLineIcon,
CircleAlertIcon,
ExternalLinkIcon,
} from "lucide-react";
import Link from "next/link";
import { useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { getAddress, isAddress, type ThirdwebClient } from "thirdweb";
import {
getAddress,
getContract,
isAddress,
type ThirdwebClient,
} from "thirdweb";
import { defineChain } from "thirdweb/chains";
import { useActiveWalletChain } from "thirdweb/react";
import { z } from "zod";
import { SingleNetworkSelector } from "@/components/blocks/NetworkSelectors";
@@ -31,6 +43,11 @@ import { Spinner } from "@/components/ui/Spinner";
import { useChainSlug } from "@/hooks/chains/chainSlug";
import { useAddContractToProject } from "@/hooks/project-contracts";
import { useDashboardRouter } from "@/lib/DashboardRouter";
import { revalidateCacheTagAction } from "../../../actions/revalidate";
import { projectContractsCacheTag } from "../../../api/project/cache-tag";
import { resolveFunctionSelectors } from "../../../lib/selectors";
import { supportedERCs } from "../../../utils/supportedERCs";
import { Alert, AlertDescription, AlertTitle } from "../../ui/alert";
type ImportModalProps = {
isOpen: boolean;
@@ -42,6 +59,7 @@ type ImportModalProps = {
client: ThirdwebClient;
type: "contract" | "asset";
onSuccess?: () => void;
allowedContractType: "token" | "non-token";
};
export const ImportModal: React.FC<ImportModalProps> = (props) => {
@@ -75,6 +93,7 @@ export const ImportModal: React.FC<ImportModalProps> = (props) => {
teamId={props.teamId}
teamSlug={props.teamSlug}
type={props.type}
allowedContractType={props.allowedContractType}
/>
</DialogContent>
</Dialog>
@@ -105,6 +124,7 @@ function ImportForm(props: {
client: ThirdwebClient;
type: "contract" | "asset";
onSuccess?: () => void;
allowedContractType: "token" | "non-token";
}) {
const router = useDashboardRouter();
const activeChainId = useActiveWalletChain()?.id;
@@ -119,9 +139,14 @@ function ImportForm(props: {
const chainSlug = useChainSlug(form.watch("chainId"));
const addContractToProject = useAddContractToProject();
const [notAllowedError, setNotAllowedError] = useState(false);
return (
<Form {...form}>
<form
onChange={() => {
setNotAllowedError(false);
}}
onSubmit={form.handleSubmit(async (data) => {
const { chainId } = data;
let contractAddress: string;
@@ -158,6 +183,27 @@ function ImportForm(props: {
return;
}
// check if the contract matches the requirement
const contract = getContract({
address: contractAddress,
// eslint-disable-next-line no-restricted-syntax
chain: defineChain(chainId),
client: props.client,
});
const functionSelectors = await resolveFunctionSelectors(contract);
const ercs = supportedERCs(functionSelectors);
const isToken = ercs.isERC20 || ercs.isERC721 || ercs.isERC1155;
if (
(props.allowedContractType === "token" && !isToken) ||
(props.allowedContractType === "non-token" && isToken)
) {
setNotAllowedError(true);
return;
}
addContractToProject.mutate(
{
chainId: chainId.toString(),
@@ -177,6 +223,12 @@ function ImportForm(props: {
}
},
onSuccess: () => {
revalidateCacheTagAction(
projectContractsCacheTag({
teamId: props.teamId,
projectId: props.projectId,
}),
);
router.refresh();
toast.success("Contract imported successfully");
props.onSuccess?.();
@@ -217,6 +269,34 @@ function ImportForm(props: {
</div>
</div>
{notAllowedError && (
<div className="px-6 pt-6 text-sm text-destructive-text">
<Alert variant="destructive">
<CircleAlertIcon className="size-5" />
<AlertTitle> Invalid Contract </AlertTitle>
<AlertDescription>
{props.allowedContractType === "token" && (
<span>
This contract is not a token contract. <br /> Only ERC20,
ERC721 and ERC1155 contracts can be imported as tokens.
</span>
)}
{props.allowedContractType === "non-token" && (
<span>
This contract is a token contract. <br /> Go to the{" "}
<UnderlineLink
href={`/team/${props.teamSlug}/${props.projectSlug}/tokens`}
>
Tokens
</UnderlineLink>{" "}
page to import in dashboard.
</span>
)}
</AlertDescription>
</Alert>
</div>
)}
<div className="mt-8 flex justify-end border-t bg-card p-6">
{addContractToProject.isSuccess &&
addContractToProject.data?.result ? (
@@ -47,7 +47,7 @@ export function AnnouncementBanner() {
return (
<AnnouncementBannerUI
href="https://blog.thirdweb.com/changelog/project-view-update-wallets/"
label="Project View Update - Wallets"
label="Transactions, Vault, and Account Abstraction are now part of Wallets in Project Dashboard. View changes"
trackingLabel="project-view-update-wallets"
/>
);
@@ -12,6 +12,7 @@ export function LinkWithCopyButton(props: {
textToCopy: string;
copyTooltip: string;
className?: string;
linkIconClassName?: string;
}) {
const [isCopied, setIsCopied] = useState(false);
const Icon = isCopied ? CheckIcon : CopyIcon;
@@ -37,8 +38,15 @@ export function LinkWithCopyButton(props: {
target="_blank"
className="text-sm text-muted-foreground hover:underline flex items-center gap-1 tabular-nums flex-1 truncate hover:text-foreground group"
>
<span className="max-w-full truncate">{props.textToShow}</span>
<ArrowUpRightIcon className="size-3.5 opacity-70 shrink-0 group-hover:opacity-100" />
<span className="max-w-full truncate tabular-nums">
{props.textToShow}
</span>
<ArrowUpRightIcon
className={cn(
"size-3.5 opacity-70 shrink-0 group-hover:opacity-100",
props.linkIconClassName,
)}
/>
</Link>
</div>
);
@@ -1,21 +0,0 @@
export function ConnectSDKIcon(props: { className?: string }) {
return (
<svg
className={props.className}
fill="none"
height="24"
role="presentation"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M16 8.00007V4.50073C16 3.66899 16 3.25312 15.8248 2.99755C15.6717 2.77425 15.4346 2.62257 15.1678 2.57715C14.8623 2.52517 14.4847 2.69944 13.7295 3.04799L4.85901 7.14206C4.18551 7.45291 3.84875 7.60834 3.60211 7.84939C3.38406 8.06249 3.21762 8.32262 3.1155 8.6099C3 8.93486 3 9.30575 3 10.0475V15.0001M16.5 14.5001H16.51M3 11.2001L3 17.8001C3 18.9202 3 19.4802 3.21799 19.9081C3.40973 20.2844 3.71569 20.5903 4.09202 20.7821C4.51984 21.0001 5.07989 21.0001 6.2 21.0001H17.8C18.9201 21.0001 19.4802 21.0001 19.908 20.7821C20.2843 20.5903 20.5903 20.2844 20.782 19.9081C21 19.4802 21 18.9202 21 17.8001V11.2001C21 10.08 21 9.51992 20.782 9.09209C20.5903 8.71577 20.2843 8.40981 19.908 8.21806C19.4802 8.00007 18.9201 8.00007 17.8 8.00007L6.2 8.00007C5.0799 8.00007 4.51984 8.00007 4.09202 8.21806C3.7157 8.40981 3.40973 8.71577 3.21799 9.09209C3 9.51991 3 10.08 3 11.2001ZM17 14.5001C17 14.7762 16.7761 15.0001 16.5 15.0001C16.2239 15.0001 16 14.7762 16 14.5001C16 14.2239 16.2239 14.0001 16.5 14.0001C16.7761 14.0001 17 14.2239 17 14.5001Z"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
/>
</svg>
);
}
-24
View File
@@ -1,24 +0,0 @@
export function NebulaIcon(props: { className?: string }) {
return (
<svg
className={props.className}
fill="none"
height="24"
role="presentation"
viewBox="0 0 153 153"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M76.3162 90.8232C82.4613 90.8232 87.4429 85.8416 87.4429 79.6965C87.4429 73.5514 82.4613 68.5698 76.3162 68.5698C70.171 68.5698 65.1895 73.5514 65.1895 79.6965C65.1895 85.8416 70.171 90.8232 76.3162 90.8232Z"
fill="currentColor"
/>
<path
clipRule="evenodd"
d="M91.9481 29.4892C78.7023 32.4916 63.6745 40.7425 50.4663 53.9508C46.781 57.636 43.4817 61.4629 40.5746 65.3532C44.5585 68.8492 47.0735 73.9786 47.0735 79.6955C47.0735 90.2299 38.5336 98.7698 27.9992 98.7698C27.1107 98.7698 26.2364 98.7091 25.3803 98.5915C23.3955 110.414 25.9579 119.384 31.3537 124.78C37.2202 130.646 47.3123 133.164 60.701 130.129C73.9468 127.126 88.9745 118.875 102.183 105.667C105.888 101.962 109.204 98.1131 112.122 94.2007C108.103 90.7037 105.562 85.551 105.562 79.8048C105.562 69.2704 114.102 60.7305 124.636 60.7305C125.536 60.7305 126.421 60.7928 127.288 60.9133C129.237 49.147 126.674 40.2167 121.295 34.8381C115.429 28.9716 105.337 26.4545 91.9481 29.4892ZM16.3791 94.8229C13.4047 109.493 15.9004 122.818 24.608 131.526C42.6147 149.532 80.3662 140.975 108.928 112.413C113.381 107.96 117.348 103.284 120.803 98.494C122.042 98.7466 123.324 98.8792 124.636 98.8792C135.171 98.8792 143.711 90.3393 143.711 79.8048C143.711 73.6582 140.803 68.1906 136.289 64.7023C139.234 50.0687 136.73 36.7817 128.041 28.0925C110.034 10.0859 72.2829 18.6429 43.7206 47.2051C39.2988 51.627 35.3564 56.2691 31.9179 61.024C30.6531 60.7599 29.3423 60.6211 27.9992 60.6211C17.4647 60.6211 8.9248 69.161 8.9248 79.6955C8.9248 85.8567 11.8461 91.3357 16.3791 94.8229ZM124.636 70.2677C119.369 70.2677 115.099 74.5376 115.099 79.8048C115.099 85.0721 119.369 89.342 124.636 89.342C129.904 89.342 134.174 85.0721 134.174 79.8048C134.174 74.5376 129.904 70.2677 124.636 70.2677ZM18.462 79.6955C18.462 74.4282 22.7319 70.1583 27.9992 70.1583C33.2664 70.1583 37.5363 74.4282 37.5363 79.6955C37.5363 84.9627 33.2664 89.2326 27.9992 89.2326C22.7319 89.2326 18.462 84.9627 18.462 79.6955Z"
fill="currentColor"
fillRule="evenodd"
/>
</svg>
);
}
@@ -0,0 +1,21 @@
export function ExpoIcon(props: { className?: string }) {
return (
<svg
width="800px"
height="800px"
viewBox="0 -12.5 256 256"
xmlns="http://www.w3.org/2000/svg"
xmlnsXlink="http://www.w3.org/1999/xlink"
preserveAspectRatio="xMidYMid"
className={props.className}
aria-hidden="true"
>
<g>
<path
d="M121.309004,84.6732585 C123.402504,81.5874152 125.694292,81.1950171 127.553451,81.1950171 C129.41261,81.1950171 132.509843,81.5874152 134.604162,84.6732585 C151.106348,107.339593 178.345607,152.492 198.439108,185.798721 C211.542532,207.519499 221.6069,224.201947 223.671721,226.324944 C231.422996,234.294992 242.053551,229.327949 248.230809,220.287799 C254.312201,211.387762 256.000111,205.138399 256.000111,198.471155 C256.000111,193.930186 167.895315,30.0714244 159.022317,16.4322117 C150.48936,3.31359639 147.710044,0 133.105527,0 L122.176721,0 C107.615631,0 105.511479,3.31359639 96.9777022,16.4322117 C88.1055238,30.0714244 0.0001105152,193.930186 0.0001105152,198.471155 C0.0001105152,205.138399 1.68839227,211.387762 7.76991495,220.287799 C13.9471241,229.327949 24.5775965,234.294992 32.3286259,226.324944 C34.3936934,224.201947 44.4580605,207.519499 57.5616485,185.798721 C77.654822,152.492 104.806818,107.339593 121.309004,84.6732585 Z"
fill="currentColor"
/>
</g>
</svg>
);
}
@@ -0,0 +1,59 @@
export function NextjsIcon(props: { className?: string }) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
data-testid="geist-icon"
strokeLinejoin="round"
style={{
color: "currentColor",
}}
viewBox="3 3 10 10"
aria-hidden="true"
className={props.className}
>
<g clipPath="url(#clip0_53_108)">
<path
d="M10.63 11V5"
stroke="url(#paint0_linear_53_108vsxrmxu21)"
strokeWidth={1.25}
strokeMiterlimit={Math.SQRT2}
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M5.995 5.00087V5H4.745V11H5.995V6.96798L12.3615 14.7076C12.712 14.4793 13.0434 14.2242 13.353 13.9453L5.99527 5.00065L5.995 5.00087Z"
fill="url(#paint1_linear_53_108vsxrmxu21)"
/>
</g>
<defs>
<linearGradient
id="paint0_linear_53_108vsxrmxu21"
x1={11.13}
y1={5}
x2={11.13}
y2={11}
gradientUnits="userSpaceOnUse"
>
<stop stopColor="currentColor" />
<stop offset={0.609375} stopColor="currentColor" stopOpacity={0.57} />
<stop offset={0.796875} stopColor="currentColor" stopOpacity={0} />
<stop offset={1} stopColor="currentColor" stopOpacity={0} />
</linearGradient>
<linearGradient
id="paint1_linear_53_108vsxrmxu21"
x1={9.9375}
y1={9.0625}
x2={13.5574}
y2={13.3992}
gradientUnits="userSpaceOnUse"
>
<stop stopColor="currentColor" />
<stop offset={1} stopColor="currentColor" stopOpacity={0} />
</linearGradient>
<clipPath id="clip0_53_108">
<rect width={16} height={16} fill="red" />
</clipPath>
</defs>
</svg>
);
}
@@ -0,0 +1,20 @@
export function NodeJSIcon(props: { className?: string }) {
return (
<svg
viewBox="0 0 32 32"
fill="none"
xmlns="http://www.w3.org/2000/svg"
aria-hidden="true"
className={props.className}
>
<path
d="M17.1725 2.29872C16.4627 1.89953 15.5373 1.90132 14.8269 2.29872C11.2689 4.26227 7.71082 6.22641 4.15216 8.18906C3.45969 8.55335 2.99264 9.29698 3.00009 10.0688V21.9328C2.99509 22.7197 3.48622 23.4705 4.19655 23.8298C5.21871 24.3736 6.2118 24.9726 7.25244 25.4802C8.45451 26.0709 9.95843 26.2015 11.1752 25.5855C12.1629 25.075 12.6016 23.9395 12.6003 22.896C12.6083 18.9806 12.6016 15.0651 12.6034 11.1496C12.6269 10.9756 12.4962 10.7896 12.3064 10.7938C11.8517 10.7866 11.3964 10.7896 10.9417 10.7926C10.7699 10.7764 10.6022 10.9191 10.6152 11.0918C10.6091 14.982 10.6164 18.8734 10.6115 22.7642C10.6214 23.3024 10.2578 23.8196 9.73913 24.0014C8.5412 24.4213 5.12198 22.2012 5.12198 22.2012C4.9965 22.1431 4.91682 22.007 4.92912 21.8718C4.92912 17.9576 4.92973 14.0433 4.92912 10.1297C4.91187 9.97191 5.00912 9.8298 5.15402 9.76538C8.70033 7.8134 12.2448 5.85654 15.7911 3.90336C15.9143 3.82115 16.086 3.8214 16.2089 3.90396C19.7552 5.85654 23.3003 7.81161 26.8472 9.76368C26.9926 9.828 27.0857 9.9725 27.0709 10.1297C27.0703 14.0433 27.0721 17.9576 27.0697 21.8713C27.0802 22.0098 27.0086 22.144 26.8793 22.2048C23.3661 24.1462 19.8129 26.025 16.3315 28.0228C16.1796 28.1099 16.0075 28.2086 15.8373 28.1126C14.9218 27.6062 14.0174 27.0801 13.1049 26.5688C13.0057 26.5069 12.8794 26.4803 12.7759 26.5496C12.3668 26.7652 11.982 26.9398 11.5122 27.1258C10.8524 27.387 10.9578 27.4938 11.5529 27.8405C12.62 28.4444 13.6889 29.0459 14.756 29.6504C15.4585 30.0888 16.4024 30.12 17.1275 29.7149C20.6861 27.7538 24.2436 25.7904 27.8029 23.8293C28.5113 23.468 29.0049 22.7202 28.9999 21.9327V10.0688C29.0068 9.31264 28.5576 8.58227 27.886 8.21259C24.3156 6.23947 20.7435 4.27064 17.1725 2.29872Z"
fill="currentColor"
/>
<path
d="M22.5419 11.2062C21.1452 10.459 19.4836 10.4192 17.9315 10.5169C16.8102 10.6277 15.6309 10.9371 14.814 11.7409C13.9761 12.5489 13.7937 13.8537 14.1917 14.9085C14.4769 15.6539 15.1948 16.1386 15.9372 16.395C16.8935 16.7326 17.8979 16.837 18.9026 16.9414C19.819 17.0366 20.7357 17.1319 21.6165 17.4042C21.9763 17.5234 22.3953 17.7058 22.5055 18.0973C22.6073 18.5609 22.4957 19.0998 22.1193 19.4219C20.9237 20.3682 17.5979 20.2232 16.4166 19.4784C15.939 19.1611 15.7332 18.5994 15.6495 18.0641C15.6402 17.8973 15.5059 17.7443 15.3248 17.757C14.8713 17.7516 14.4178 17.7528 13.9643 17.7564C13.8061 17.7431 13.6416 17.8557 13.6329 18.0172C13.5397 20.4689 15.7914 21.5377 17.9039 21.773C19.1108 21.888 20.3442 21.8814 21.5327 21.6224C22.4261 21.419 23.3219 21.0444 23.9369 20.3563C24.6953 19.52 24.8444 18.2749 24.5043 17.2332C24.2443 16.4559 23.5012 15.9573 22.7416 15.7008C21.7086 15.3466 20.4844 15.1562 19.5488 15.0671C18.1889 14.9376 16.5729 14.9905 16.188 14.0969C16.0345 13.629 16.1651 13.048 16.5951 12.7602C17.7328 11.9885 20.0483 12.091 21.2265 12.6675C21.7675 12.9384 22.081 13.4948 22.2104 14.0565C22.2344 14.2215 22.3454 14.3937 22.5364 14.3865C22.9868 14.3955 23.4372 14.3889 23.8875 14.3895C24.0422 14.4003 24.2116 14.313 24.2418 14.1546C24.2227 12.9806 23.6232 11.7788 22.5419 11.2062Z"
fill="currentColor"
/>
</svg>
);
}
@@ -0,0 +1,23 @@
export function ViteIcon(props: { className?: string }) {
return (
<svg
viewBox="-0.5 0 257 257"
xmlns="http://www.w3.org/2000/svg"
xmlnsXlink="http://www.w3.org/1999/xlink"
preserveAspectRatio="xMidYMid"
aria-hidden="true"
className={props.className}
>
<g>
<path
d="M255.152904,37.937763 L134.896865,252.97646 C132.413943,257.416178 126.035075,257.442321 123.5149,253.02417 L0.87443175,37.9584812 C-1.87111536,33.1438084 2.24595371,27.3119153 7.70191187,28.2871109 L128.086639,49.8052023 C128.854587,49.9424525 129.640835,49.9411454 130.408783,49.8012155 L248.276014,28.3179595 C253.713738,27.3268821 257.850198,33.1136134 255.152904,37.937763 Z"
fill="currentColor"
/>
<path
d="M185.432401,0.0631038902 L96.4393502,17.500942 C94.9766549,17.7875335 93.8936852,19.0270992 93.8054529,20.5146956 L88.3311293,112.971419 C88.2023755,115.149123 90.2023075,116.839261 92.3277254,116.349082 L117.10466,110.630976 C119.422882,110.096354 121.517582,112.138114 121.041128,114.469407 L113.67994,150.515893 C113.184532,152.941955 115.462232,155.016394 117.831433,154.29681 L133.134834,149.647295 C135.507302,148.927059 137.786963,151.00738 137.285019,153.435402 L125.586724,210.056351 C124.854723,213.598061 129.565674,215.529368 131.530313,212.49287 L132.842687,210.464834 L205.359174,65.745575 C206.573511,63.3224548 204.479465,60.5594769 201.818118,61.0730542 L176.31441,65.9952397 C173.91776,66.4573155 171.878614,64.2253653 172.555061,61.8805431 L189.2009,4.17570253 C189.878001,1.82692623 187.831665,-0.406957894 185.432401,0.0631038902 Z"
fill="hsl(var(--background))"
/>
</g>
</svg>
);
}
+2
View File
@@ -23,6 +23,8 @@ export interface UserOpStats {
successful: number;
failed: number;
sponsoredUsd: number;
gasUnits: number;
avgGasPrice: number;
chainId?: string;
}
@@ -3,6 +3,7 @@ import { zodResolver } from "@hookform/resolvers/zod";
import { Turnstile } from "@marsidev/react-turnstile";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import type { CanClaimResponseType } from "app/(app)/api/testnet-faucet/can-claim/CanClaimResponseType";
import { ArrowUpRightIcon } from "lucide-react";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { useForm } from "react-hook-form";
@@ -192,6 +193,20 @@ export function FaucetButton({
// Can not claim
if (canClaimFaucetQuery.data && canClaimFaucetQuery.data.canClaim === false) {
if (canClaimFaucetQuery.data.type === "paid-plan-required") {
return (
<Button
asChild
className="w-full gap-2 whitespace-pre-wrap h-auto min-h-10 text-center bg-background"
variant="outline"
>
<Link href="/team/~/~/billing">
Faucet is not available on Free plan. Upgrade your plan to continue
<ArrowUpRightIcon className="size-4 shrink-0 text-muted-foreground" />
</Link>
</Button>
);
}
return (
<Button className="!opacity-100 w-full " disabled variant="outline">
{canClaimFaucetQuery.data.type === "throttle" && (
@@ -203,10 +218,6 @@ export function FaucetButton({
{canClaimFaucetQuery.data.type === "unsupported-chain" &&
"Faucet is empty right now"}
{/* TODO: add an upsell path here to subscribe to one of these plans */}
{canClaimFaucetQuery.data.type === "paid-plan-required" &&
"Faucet is only available on Starter, Growth, Scale and Pro plans."}
</Button>
);
}
@@ -1,55 +1,112 @@
import { BotIcon, CodeIcon, CoinsIcon } from "lucide-react";
import Link from "next/link";
import type { ChainMetadataWithServices } from "@/types/chain";
import { products } from "../../../../components/server/products";
import { useMemo } from "react";
import { BridgeIcon } from "@/icons/BridgeIcon";
import { PayIcon } from "@/icons/PayIcon";
import { WalletProductIcon } from "@/icons/WalletProductIcon";
import type {
ChainMetadataWithServices,
ChainSupportedService,
} from "@/types/chain";
import { SectionTitle } from "./SectionTitle";
export function SupportedProductsSection(props: {
services: ChainMetadataWithServices["services"];
}) {
const enabledProducts = products.filter((product) => {
return props.services.find(
(service) => service.service === product.id && service.enabled,
const enabledServices = useMemo(() => {
return props.services.reduce(
(acc, service) => {
acc[service.service] = service.enabled;
return acc;
},
{} as Record<ChainSupportedService, boolean>,
);
});
if (enabledProducts.length === 0) {
return null;
}
}, [props.services]);
return (
<section>
<SectionTitle title="thirdweb Products" />
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-3">
{enabledProducts.map((product) => {
return (
<div
className="relative rounded-xl border bg-card p-4 hover:border-active-border"
key={product.id}
>
<div className="flex mb-4">
<div className="p-2 rounded-full border bg-background">
<product.icon className="size-4 text-muted-foreground" />
</div>
</div>
<div>
<h3 className="mb-1">
<Link
className="before:absolute before:inset-0 text-base font-medium"
href={product.link}
rel="noopener noreferrer"
target="_blank"
>
{product.name}
</Link>
</h3>
<p className="text-muted-foreground text-sm">
{product.description}
</p>
</div>
</div>
);
})}
{enabledServices["connect-sdk"] && (
<ProductCard
icon={WalletProductIcon}
name="Wallets"
description="Create wallets to read and transact"
link="https://thirdweb.com/wallets"
/>
)}
{enabledServices["account-abstraction"] && (
<ProductCard
icon={PayIcon}
name="x402"
description="Create internet native payments with x402"
link="https://thirdweb.com/x402"
/>
)}
{enabledServices.pay && (
<ProductCard
icon={BridgeIcon}
name="Bridge"
description="Bridge and swap tokens across chains"
link="https://thirdweb.com/monetize/bridge"
/>
)}
{enabledServices.contracts && (
<ProductCard
icon={CoinsIcon}
name="Tokens"
description="Launch tokens and markets"
link="https://thirdweb.com/token"
/>
)}
<ProductCard
icon={BotIcon}
name="AI"
description="Read and write onchain via natural language"
link="https://thirdweb.com/ai"
/>
<ProductCard
icon={CodeIcon}
name="HTTP API"
description="Build products with our HTTP API"
link="https://portal.thirdweb.com/reference"
/>
</div>
</section>
);
}
function ProductCard(props: {
icon: React.FC<{ className?: string }>;
link: string;
name: string;
description: string;
}) {
return (
<div className="relative rounded-xl border bg-card p-4 hover:border-active-border">
<div className="flex mb-4">
<div className="p-2 rounded-full border bg-background">
<props.icon className="size-4 text-muted-foreground" />
</div>
</div>
<div>
<h3 className="mb-1">
<Link
className="before:absolute before:inset-0 text-base font-medium"
href={props.link}
rel="noopener noreferrer"
target="_blank"
>
{props.name}
</Link>
</h3>
<p className="text-muted-foreground text-sm">{props.description}</p>
</div>
</div>
);
}
@@ -2,6 +2,7 @@ import type { ThirdwebContract } from "thirdweb";
import { contractType as getContractType } from "thirdweb/extensions/thirdweb";
import { getDeployedEntrypointERC20 } from "thirdweb/tokens";
import { resolveFunctionSelectors } from "@/lib/selectors";
import { supportedERCs } from "@/utils/supportedERCs";
import { getValidReward } from "../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/rewards/utils/rewards";
import {
isERC20ClaimConditionsSupported,
@@ -21,7 +22,6 @@ import {
isPermissionsEnumerableSupported,
isPermissionsSupported,
} from "./detectedFeatures/permissions";
import { supportedERCs } from "./detectedFeatures/supportedERCs";
import { type EmbedTypeToShow, getEmbedTypeToShow } from "./getEmbedTypeToShow";
type ContractPageMetadata = {
@@ -1,9 +1,9 @@
import { supportedERCs } from "@/utils/supportedERCs";
import { isClaimConditionsSupported } from "./detectedFeatures/claimConditions";
import {
isDirectListingSupported,
isEnglishAuctionSupported,
} from "./detectedFeatures/marketplace";
import { supportedERCs } from "./detectedFeatures/supportedERCs";
export type EmbedTypeToShow =
| "marketplace-v3"
@@ -2,7 +2,7 @@ import type { ThirdwebContract } from "thirdweb";
import { isGetNFTsSupported as ERC721_isGetNFTsSupported } from "thirdweb/extensions/erc721";
import { isGetNFTsSupported as ERC1155_isGetNFTsSupported } from "thirdweb/extensions/erc1155";
import { resolveFunctionSelectors } from "@/lib/selectors";
import { supportedERCs } from "./detectedFeatures/supportedERCs";
import { supportedERCs } from "@/utils/supportedERCs";
export type NewPublicPageType = "erc20" | "erc1155" | "erc721";
@@ -7,6 +7,7 @@ import { getTeams } from "@/api/team/get-team";
import type { MinimalTeamsAndProjects } from "@/components/contracts/import-contract/types";
import { getClientThirdwebClient } from "@/constants/thirdweb-client.client";
import { resolveFunctionSelectors } from "@/lib/selectors";
import { supportedERCs } from "@/utils/supportedERCs";
import { shortenIfAddress } from "@/utils/usedapp-external";
import type { ProjectMeta } from "../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/types";
import { TeamHeader } from "../../../../team/components/TeamHeader/team-header";
@@ -14,7 +15,6 @@ 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";
import { getContractPageSidebarLinks } from "./_utils/getContractPageSidebarLinks";
@@ -15,7 +15,7 @@ import {
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import { Separator } from "@/components/ui/separator";
import { useDashboardRouter } from "@/lib/DashboardRouter";
import { products } from "../../../components/server/products";
import { services } from "../../../components/server/products";
function cleanUrl(url: string) {
if (url.endsWith("?")) {
@@ -345,7 +345,7 @@ export const ChainServiceFilter: React.FC<ChainServiceFilterProps> = ({
const section = (
<FilterSection title="Services">
{products.map((product) => (
{services.map((product) => (
<div className="group flex items-center gap-2" key={product.id}>
<Checkbox
checked={isServiceActive(mutableSearchParams, product.id)}
@@ -393,7 +393,7 @@ export const ChainServiceFilter: React.FC<ChainServiceFilterProps> = ({
}
const firstFilter = allFilters[0];
const name = products.find((p) => p.id === firstFilter)?.name;
const name = services.find((p) => p.id === firstFilter)?.name;
const plus = allFilters.length > 1 ? ` +${allFilters.length - 1}` : "";
return [`${name}${plus}`, true];
@@ -12,7 +12,7 @@ import { ToolTipLabel } from "@/components/ui/tooltip";
import { cn } from "@/lib/utils";
import type { ChainSupportedService } from "@/types/chain";
import { ChainIcon } from "../../../components/server/chain-icon";
import { products } from "../../../components/server/products";
import { services } from "../../../components/server/products";
import { getCustomChainMetadata } from "../../../utils";
type ChainListRowProps = {
@@ -83,7 +83,7 @@ export function ChainListRow({
<TableCell>
<div className="flex w-[520px] flex-row items-center gap-14 ">
<div className="z-10 flex items-center gap-4">
{products.map((p) => {
{services.map((p) => {
return (
<ProductIcon
href={p.link}
@@ -1,74 +1,69 @@
import { ConnectSDKIcon } from "@/icons/ConnectSDKIcon";
import { ContractIcon } from "@/icons/ContractIcon";
import { EngineIcon } from "@/icons/EngineIcon";
import { BotIcon, CoinsIcon, FuelIcon, HardDriveIcon } from "lucide-react";
import { BridgeIcon } from "@/icons/BridgeIcon";
import { InsightIcon } from "@/icons/InsightIcon";
import { NebulaIcon } from "@/icons/NebulaIcon";
import { PayIcon } from "@/icons/PayIcon";
import { RPCIcon } from "@/icons/RPCIcon";
import { SmartAccountIcon } from "@/icons/SmartAccountIcon";
import { WalletProductIcon } from "@/icons/WalletProductIcon";
import type { ChainSupportedService } from "@/types/chain";
export const products = [
export const services = [
{
description: "Create, deploy and manage smart contracts",
icon: ContractIcon,
icon: CoinsIcon,
id: "contracts",
link: "https://thirdweb.com/explore",
name: "Contracts",
link: "https://thirdweb.com/token",
name: "Tokens",
},
{
description: "Create and manage crypto wallets",
icon: ConnectSDKIcon,
icon: WalletProductIcon,
id: "connect-sdk",
link: "https://thirdweb.com/connect",
name: "Wallets",
link: "https://thirdweb.com/wallets",
name: "User Wallets",
},
{
description: "Performant and scalable RPC service",
icon: RPCIcon,
id: "rpc-edge",
link: "https://portal.thirdweb.com/infrastructure/rpc-edge/overview",
name: "RPC Edge",
link: "https://thirdweb.com/rpc",
name: "RPC",
},
{
description: "Backend server that reads, writes, and deploys contracts",
icon: EngineIcon,
icon: HardDriveIcon,
id: "engine",
link: "https://thirdweb.com/engine",
name: "Transactions",
link: "https://thirdweb.com/wallets",
name: "Server Wallets",
},
{
description: "Enable gas sponsorship for seamless transactions",
icon: SmartAccountIcon,
icon: FuelIcon,
id: "account-abstraction",
link: "https://portal.thirdweb.com/wallets/sponsor-gas",
name: "Account Abstraction",
link: "https://thirdweb.com/wallets",
name: "Gas Sponsorship",
},
{
description: "Enable payments in any token on any chain",
icon: PayIcon,
icon: BridgeIcon,
id: "pay",
link: "https://portal.thirdweb.com/payments",
name: "Payments",
link: "https://thirdweb.com/monetize/bridge",
name: "Bridge",
},
{
description: "Query and analyze blockchain data",
icon: InsightIcon,
id: "insight",
link: "https://thirdweb.com/insight",
name: "Insight",
},
{
description: "The most powerful AI for interacting with the blockchain",
icon: NebulaIcon,
icon: BotIcon,
id: "nebula",
link: "https://thirdweb.com/ai",
name: "thirdweb AI",
name: "AI",
},
{
icon: PayIcon,
id: "account-abstraction",
link: "https://thirdweb.com/x402",
name: "x402",
},
{
icon: InsightIcon,
id: "insight",
link: "https://thirdweb.com/gateway",
name: "Indexer",
},
] satisfies Array<{
name: string;
id: ChainSupportedService;
icon: React.FC<{ className?: string }>;
description: string;
link: string;
}>;
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 MiB

@@ -1,130 +0,0 @@
import type { Metadata } from "next";
import Link from "next/link";
import { notFound } from "next/navigation";
import { Suspense } from "react";
import {
ContractCard,
ContractCardSkeleton,
} from "@/components/contracts/contract-card/contract-card";
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator,
} from "@/components/ui/breadcrumb";
import { DeployUpsellCard } from "../components/upsells/deploy-your-own";
import { getCategory } from "../data";
type ExploreCategoryPageProps = {
params: Promise<{
category: string;
}>;
};
export async function generateMetadata(
props: ExploreCategoryPageProps,
): Promise<Metadata> {
const params = await props.params;
const category = getCategory(params.category);
if (!category) {
notFound();
}
return {
description: `${category.description} Deploy with one click to Ethereum, Polygon, Optimism, and other EVM blockchains with thirdweb.`,
title: `${category.name} Smart Contracts | Explore`,
};
}
export default async function ExploreCategoryPage(
props: ExploreCategoryPageProps,
) {
const params = await props.params;
const category = getCategory(params.category);
if (!category) {
notFound();
}
return (
<div className="flex flex-col">
<Breadcrumb className="border-border border-b px-6 py-4">
<BreadcrumbList>
<BreadcrumbItem>
<BreadcrumbLink asChild>
<Link href="/explore">Explore</Link>
</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbPage>
{category.displayName || category.name}
</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
<div className="container flex flex-col gap-4 py-8">
<h1 className="mb-3 font-bold text-3xl tracking-tighter lg:text-5xl">
{category.displayName || category.name}
</h1>
<p className="max-w-screen-md text-base text-muted-foreground lg:text-lg">
{category.description}{" "}
{category.learnMore && (
<Link href={category.learnMore}>Learn more</Link>
)}
</p>
<div className="h-10" />
<div className="grid grid-cols-1 gap-5 md:grid-cols-3">
{category.contracts.map((publishedContractId) => {
const publisher: string | undefined = Array.isArray(
publishedContractId,
)
? publishedContractId[0].split("/")[0]
: publishedContractId.split("/")[0];
const contractId: string | undefined = Array.isArray(
publishedContractId,
)
? publishedContractId[0].split("/")[1]
: publishedContractId.split("/")[1];
const modules = Array.isArray(publishedContractId)
? publishedContractId[1]
: undefined;
const overrides = Array.isArray(publishedContractId)
? publishedContractId[2]
: undefined;
if (!publisher || !contractId) {
return null;
}
return (
<Suspense
fallback={<ContractCardSkeleton />}
key={publisher + contractId + overrides?.title}
>
<ContractCard
contractId={contractId}
descriptionOverride={overrides?.description}
isBeta={category.isBeta}
modules={
modules?.length
? modules.map((m) => ({
moduleId: m.split("/")[1] || "",
publisher: m.split("/")[0] || "",
}))
: undefined
}
publisher={publisher}
titleOverride={overrides?.title}
/>
</Suspense>
);
})}
</div>
<div className="h-16" />
<DeployUpsellCard />
</div>
</div>
);
}
@@ -1,4 +1,3 @@
import { ArrowRightIcon } from "lucide-react";
import Link from "next/link";
import { Suspense } from "react";
import {
@@ -7,48 +6,45 @@ import {
} from "@/components/contracts/contract-card/contract-card";
import type { ExploreCategory } from "../../data";
export function ContractRow({ category }: { category: ExploreCategory }) {
export function ContractRow({
category,
prefixCard,
}: {
category: ExploreCategory;
prefixCard?: React.ReactNode;
}) {
return (
<section>
{/* Title, Description + View all link */}
<div className="flex items-center justify-between gap-4">
<header className="flex flex-col gap-1.5">
<Link href={`/explore/${category.id}`}>
<h2 className="font-semibold text-2xl tracking-tight">
{category.displayName || category.name}
</h2>
</Link>
<header className="flex flex-col gap-1">
<h2 className="font-semibold text-2xl tracking-tight">
{category.displayName || category.name}
</h2>
<p className="text-muted-foreground">
{category.description}{" "}
{category.learnMore && (
<Link
className="inline text-link-foreground"
href={category.learnMore}
rel="noopener noreferrer"
target="_blank"
>
Learn more
</Link>
)}
</p>
{(category.description || category.learnMore) && (
<p className="text-muted-foreground">
{category.description}{" "}
{category.learnMore && (
<Link
className="inline text-link-foreground"
href={category.learnMore}
rel="noopener noreferrer"
target="_blank"
>
Learn more
</Link>
)}
</p>
)}
</header>
{category.contracts.length > 6 && (
<Link
className="flex shrink-0 items-center gap-1 text-base text-link-foreground hover:text-foreground"
href={`/explore/${category.id}`}
>
View all
<ArrowRightIcon className="size-4" />
</Link>
)}
</div>
<div className="h-5" />
<div className="relative z-0 grid grid-cols-1 gap-4 md:grid-cols-3">
{category.contracts.slice(0, 6).map((publishedContractId) => {
{prefixCard}
{category.contracts.map((publishedContractId) => {
const publisher: string | undefined = Array.isArray(
publishedContractId,
)
@@ -0,0 +1,103 @@
"use client";
import { RocketIcon, ShieldCheckIcon } from "lucide-react";
import Link from "next/link";
import { reportTokenUpsellClicked } from "@/analytics/report";
import { Img } from "@/components/blocks/Img";
import { Button } from "@/components/ui/button";
const thirdwebEns = {
name: "thirdweb.eth",
image: "https://euc.li/thirdweb.eth",
};
function TokenContractCard(props: {
name: string;
description: string;
link: string;
trackingType: "nft" | "coin";
}) {
return (
<article className="relative flex min-h-[220px] flex-col rounded-lg border border-border bg-card p-4 hover:border-active-border">
<div className="flex items-center gap-1.5">
<div className="flex items-center gap-1 font-medium text-sm text-success-text">
<ShieldCheckIcon className="size-4" />
Audited
</div>
</div>
<div className="h-3.5" />
{/* Title */}
<h3 className="font-semibold text-lg tracking-tight">
<Link
className="cursor-pointer before:absolute before:inset-0 before:z-0"
href={props.link}
onClick={() => {
reportTokenUpsellClicked({
assetType: props.trackingType,
pageType: "explore",
});
}}
>
{props.name}
</Link>
</h3>
<p className="mt-1 text-muted-foreground text-sm leading-5">
{props.description}
</p>
<div className="mt-auto relative z-1 flex justify-between gap-2 pt-3">
<Link
className="flex shrink-0 items-center gap-1.5 hover:underline"
href={`/${thirdwebEns.name}`}
target="_blank"
>
<Img
className="size-5 rounded-full object-cover"
src={thirdwebEns.image}
/>
<span className="text-xs"> {thirdwebEns.name}</span>
</Link>
<div className="flex items-center justify-between">
<Button
asChild
className="relative z-10 h-auto gap-1.5 px-2.5 py-1.5 text-xs hover:bg-inverted hover:text-inverted-foreground"
size="sm"
variant="outline"
>
<Link href={props.link}>
<RocketIcon className="size-3" />
Deploy
</Link>
</Button>
</div>
</div>
</article>
);
}
export function NFTCollectionCard() {
return (
<TokenContractCard
name="NFT Collection"
description="Launch an ERC-721 or ERC-1155 NFT collection"
link="/team/~/~project/tokens/create/nft"
trackingType="nft"
/>
);
}
export function CoinCard() {
return (
<TokenContractCard
name="Coin"
description="Launch an ERC-20 token to create a cryptocurrency"
link="/team/~/~project/tokens/create/token"
trackingType="coin"
/>
);
}
@@ -5,31 +5,12 @@ import { PlainTextCodeBlock } from "@/components/ui/code/plaintext-code";
export const DeployUpsellCard: React.FC = () => {
return (
<section className="rounded-xl border border-border px-4 py-6 shadow-lg md:px-8 md:py-14">
<h3 className="font-bold text-2xl tracking-tight">
<h3 className="font-semibold text-2xl tracking-tight">
Didn't find what you're looking for? Build your own!
</h3>
<div className="h-8" />
<p className="text-muted-foreground ">
Create a contract with a single command.{" "}
<Link
className="inline-flex items-center gap-1.5 text-link-foreground"
href="https://portal.thirdweb.com/contracts/build/overview"
rel="noopener noreferrer"
target="_blank"
>
Learn more about the Solidity SDK
<ExternalLinkIcon className="size-4" />
</Link>
</p>
<div className="h-2" />
<PlainTextCodeBlock code="npx thirdweb create contract" />
<div className="h-8" />
<p className="text-muted-foreground ">
Deploy a contract with a single command.{" "}
<Link
@@ -1,70 +0,0 @@
import Image from "next/image";
import Link from "next/link";
import { Button } from "@/components/ui/button";
import exploreFeatureImage from "../../../../../../../public/assets/landingpage/explore-featured.png";
import heroIcon1 from "../../../../../../../public/assets/product-pages/publish/hero-icon-1.png";
import heroIcon2 from "../../../../../../../public/assets/product-pages/publish/hero-icon-2.png";
export const PublishUpsellCard: React.FC = () => {
return (
<div className="flex gap-10 rounded-xl border border-border bg-card p-8 shadow-lg md:p-10">
<div className="flex flex-col gap-6">
<h2 className="font-bold text-3xl tracking-tighter">
Accelerate your protocol's growth
</h2>
<p className="text-muted-foreground">
Publishing your contract is the best way to get your contracts in
front of our 70k+ community of web3 developers.
</p>
<div className="flex gap-2">
<Image alt="" className="hidden size-6 md:block" src={heroIcon1} />
<p className="text-muted-foreground">
<span className="font-semibold text-foreground">
Save development time.{" "}
</span>
Focus on protocol development and save time by not having to build
middleware layer yourself.
</p>
</div>
<div className="flex gap-2">
<Image alt="" className="hidden size-6 md:block" src={heroIcon2} />
<p className="text-muted-foreground">
<span className="font-semibold text-foreground">
Shareable landing page.{" "}
</span>
By publishing your contract, your contracts become easily shareable
with a landing page for your contract.
</p>
</div>
<div className="mt-auto flex gap-2 pt-4">
<Button asChild>
<Link href="/contact-us" rel="noopener noreferrer" target="_blank">
Get In Touch
</Link>
</Button>
<Button asChild variant="outline">
<Link
href="https://portal.thirdweb.com/contracts/publish/overview"
rel="noopener noreferrer"
target="_blank"
>
Learn More
</Link>
</Button>
</div>
</div>
<Image
alt=""
className="hidden w-[40%] grayscale invert md:block dark:filter-none"
draggable={false}
src={exploreFeatureImage}
/>
</div>
);
};
@@ -1,151 +1,55 @@
type PublishedContractID = `${string}/${string}`;
export interface ExploreCategory {
id: string;
name: string;
displayName?: string;
description: string;
description: string | undefined;
learnMore?: string;
contracts:
| Array<PublishedContractID>
| Array<
[
PublishedContractID,
Array<PublishedContractID>,
{ title: string; description: string },
]
>;
showInExplore?: boolean;
contracts: Array<
| PublishedContractID
| [
PublishedContractID,
Array<PublishedContractID>,
{ title: string; description: string },
]
>;
isBeta?: boolean;
}
const POPULAR = {
export const NFTS = {
contracts: [
"thirdweb.eth/DropERC721",
"thirdweb.eth/MarketplaceV3",
"thirdweb.eth/AccountFactory",
"0xaF3202F6bAEbA50d37e0d4B0b870455EDF198D7c/BurnMintERC677",
"thirdweb.eth/DropERC1155",
"thirdweb.eth/TokenERC20",
"thirdweb.eth/NFTStake",
"unlock-protocol.eth/PublicLock",
],
description: "A collection of our most deployed contracts.",
id: "popular",
name: "Popular",
} satisfies ExploreCategory;
const NFTS = {
contracts: [
"thirdweb.eth/DropERC721",
"thirdweb.eth/OpenEditionERC721",
"thirdweb.eth/TokenERC721",
"thirdweb.eth/DropERC1155",
"thirdweb.eth/TokenERC1155",
"flairsdk.eth/ERC721CommunityStream",
"thirdweb.eth/LoyaltyCard",
"doubledev.eth/ERC4907",
"thirdweb.eth/Multiwrap",
"kronickatz.eth/ERC721NESDrop",
],
description:
"NFT Collections, Editions, Drops and everything else NFT-related.",
displayName: "NFTs",
id: "nft",
name: "NFT",
} satisfies ExploreCategory;
const GOVERNANCE = {
contracts: [
"thirdweb.eth/VoteERC20",
"thirdweb.eth/TokenERC20",
"thirdweb.eth/Split",
],
export const GOVERNANCE = {
contracts: ["thirdweb.eth/VoteERC20", "thirdweb.eth/Split"],
description: "Create your own DAO, vote on proposals, and manage a treasury.",
id: "daos-governance",
name: "DAOs & Governance",
} satisfies ExploreCategory;
const DROPS = {
contracts: [
"thirdweb.eth/DropERC721",
"thirdweb.eth/DropERC1155",
"thirdweb.eth/DropERC20",
],
description: "Release NFTs and Tokens based on preset Claim Conditions.",
displayName: "Drops",
id: "drops",
name: "Drop",
} satisfies ExploreCategory;
const MARKETS = {
contracts: [
"thirdweb.eth/MarketplaceV3",
"thirdweb.eth/TokenERC20",
"thirdweb.eth/Split",
],
export const MARKETS = {
contracts: ["thirdweb.eth/MarketplaceV3", "thirdweb.eth/Split"],
description: "Quickly spin up your own on-chain marketplace for NFTs.",
displayName: "Marketplaces",
id: "marketplace",
name: "Marketplace",
} satisfies ExploreCategory;
const AIRDROP = {
export const AIRDROP = {
contracts: ["thirdweb.eth/Airdrop"],
description:
"Efficiently transfer large numbers of on-chain assets to a large number of recipients.",
displayName: "Airdrops",
id: "airdrop",
name: "Airdrop",
} satisfies ExploreCategory;
const GAMING = {
contracts: [
"thirdweb.eth/MarketplaceV3",
"thirdweb.eth/DropERC721",
"thirdweb.eth/TokenERC20",
"thirdweb.eth/TokenERC1155",
"thirdweb.eth/Multiwrap",
"thirdweb.eth/NFTStake",
],
description:
"A collection of contracts that are popular for building play-to-earn and free-to-own web3 games.",
displayName: "Gaming",
id: "gaming",
name: "Gaming",
showInExplore: false,
} satisfies ExploreCategory;
const LOYALTY = {
contracts: [
"thirdweb.eth/LoyaltyCard",
"thirdweb.eth/MarketplaceV3",
"thirdweb.eth/TokenERC20",
],
description:
"A collection of contracts that are popular for building loyalty programs.",
displayName: "Loyalty",
id: "loyalty",
name: "Loyalty",
showInExplore: false,
} satisfies ExploreCategory;
const COMMERCE = {
contracts: [
"thirdweb.eth/TokenERC721",
"thirdweb.eth/TokenERC1155",
"thirdweb.eth/TokenERC20",
"thirdweb.eth/Split",
],
description:
"Most popular contracts for building web3 commerce apps. Reward loyal customers and sell NFTs through your storefront.",
displayName: "Commerce",
id: "commerce",
name: "Commerce",
showInExplore: false,
} satisfies ExploreCategory;
const STAKING = {
export const STAKING = {
contracts: [
"thirdweb.eth/NFTStake",
"thirdweb.eth/EditionStake",
@@ -153,12 +57,10 @@ const STAKING = {
],
description: "Stake your NFTs or tokens to earn ERC20 tokens in return.",
displayName: "Staking",
id: "staking",
name: "Staking",
showInExplore: true,
} satisfies ExploreCategory;
const SMART_WALLET = {
export const SMART_WALLET = {
contracts: [
"thirdweb.eth/AccountFactory",
"thirdweb.eth/ManagedAccountFactory",
@@ -168,30 +70,16 @@ const SMART_WALLET = {
description:
"Account factories that let you spin up Account Abstraction (ERC-4337) wallets for your users. Not sure which factory is right for you?",
displayName: "Account Abstraction",
id: "smart-wallet",
learnMore:
"https://portal.thirdweb.com/wallets/smart-wallet/get-started#1-deploy-a-smart-wallet-factory-contract",
name: "Account Abstraction",
showInExplore: true,
} satisfies ExploreCategory;
const STYLUS = {
export const STYLUS = {
contracts: [
"0x6453a486d52e0EB6E79Ec4491038E2522a926936/StylusAirdropERC1155",
"0x6453a486d52e0EB6E79Ec4491038E2522a926936/StylusAirdropERC721",
"0x6453a486d52e0EB6E79Ec4491038E2522a926936/StylusAirdropERC20",
],
description:
"Airdrop your NFTs or tokens to a large number of recipients. Built with Arbitrum Stylus.",
displayName: "Airdrop - Arbitrum Stylus",
id: "stylus",
name: "Stylus",
showInExplore: true,
isBeta: true,
} satisfies ExploreCategory;
const MODULAR_CONTRACTS_STYLUS = {
contracts: [
// erc721 token
[
"thirdweb.eth/ERC721CoreInitializable",
@@ -232,44 +120,8 @@ const MODULAR_CONTRACTS_STYLUS = {
},
],
],
description:
"Collection of highly customizable and upgradeable smart contracts built with the modular contracts framework.",
displayName: "Modular Contracts - Arbitrum Stylus",
id: "modular-contracts",
name: "modular",
showInExplore: true,
description: undefined,
displayName: "Arbitrum Stylus",
name: "Stylus",
isBeta: true,
} satisfies ExploreCategory;
const CATEGORIES: Record<string, ExploreCategory> = {
[POPULAR.id]: POPULAR,
[NFTS.id]: NFTS,
[MARKETS.id]: MARKETS,
[STYLUS.id]: STYLUS,
[DROPS.id]: DROPS,
[SMART_WALLET.id]: SMART_WALLET,
[AIRDROP.id]: AIRDROP,
[GAMING.id]: GAMING,
[LOYALTY.id]: LOYALTY,
[COMMERCE.id]: COMMERCE,
[STAKING.id]: STAKING,
[GOVERNANCE.id]: GOVERNANCE,
[MODULAR_CONTRACTS_STYLUS.id]: MODULAR_CONTRACTS_STYLUS,
};
export function getCategory(id: string) {
if (isExploreCategory(id)) {
return CATEGORIES[id];
}
return null;
}
type ExploreCategoryName = keyof typeof CATEGORIES;
function isExploreCategory(category: string): category is ExploreCategoryName {
return category in CATEGORIES;
}
export const EXPLORE_PAGE_DATA = Object.values(CATEGORIES).filter((v) =>
"showInExplore" in v ? v.showInExplore !== false : true,
);
@@ -1,14 +1,20 @@
import type { Metadata } from "next";
import { Fragment } from "react";
import { ContractRow } from "./components/contract-row";
import { CoinCard, NFTCollectionCard } from "./components/token-cards";
import { DeployUpsellCard } from "./components/upsells/deploy-your-own";
import { PublishUpsellCard } from "./components/upsells/publish-submit";
import { EXPLORE_PAGE_DATA } from "./data";
import { TokensSection } from "./tokens-section";
import {
AIRDROP,
GOVERNANCE,
MARKETS,
NFTS,
SMART_WALLET,
STAKING,
STYLUS,
} from "./data";
const title = "List of smart contracts for EVM Developers";
const description =
"A list of Ethereum smart contract templates for web3 developers, including the most popular evm smart contracts for dapps, NFTs and more.";
"A list of smart contract templates for web3 developers, including the most popular evm smart contracts for dapps, NFTs and more.";
export const metadata: Metadata = {
description,
@@ -22,9 +28,9 @@ export const metadata: Metadata = {
export default async function ExplorePage() {
return (
<div className="flex flex-col">
<div className="border-b py-10">
<div className="border-b py-12">
<div className="container max-w-7xl">
<h1 className="mb-2 font-bold text-4xl lg:text-5xl tracking-tight">
<h1 className="mb-2 font-semibold text-4xl lg:text-5xl tracking-tight">
Explore
</h1>
<p className="max-w-screen-sm text-sm lg:text-base text-muted-foreground">
@@ -35,24 +41,34 @@ export default async function ExplorePage() {
</div>
</div>
<div className="container max-w-7xl pt-8 pb-4">
<TokensSection />
</div>
<div className="container max-w-7xl flex flex-col gap-8 py-10">
<div className="container max-w-7xl flex flex-col py-10">
<div className="flex flex-col gap-14">
{EXPLORE_PAGE_DATA.map((category, idx) => (
<Fragment key={category.id}>
{Math.floor(EXPLORE_PAGE_DATA.length / 2) === idx && (
<PublishUpsellCard />
)}
<ContractRow category={category} />
</Fragment>
))}
<PopularSection />
<ContractRow category={MARKETS} />
<ContractRow category={NFTS} prefixCard={<NFTCollectionCard />} />
<ContractRow category={SMART_WALLET} />
<ContractRow category={AIRDROP} />
<ContractRow category={STAKING} />
<ContractRow category={GOVERNANCE} />
<ContractRow category={STYLUS} />
<DeployUpsellCard />
</div>
<DeployUpsellCard />
</div>
</div>
);
}
function PopularSection() {
return (
<section>
<h2 className="font-semibold text-2xl tracking-tight mb-1">Popular</h2>
<p className="text-muted-foreground mb-4">
A collection of our most deployed contracts.
</p>
<div className="relative z-0 grid grid-cols-1 gap-4 md:grid-cols-3">
<CoinCard />
<NFTCollectionCard />
</div>
</section>
);
}
@@ -1,111 +0,0 @@
"use client";
import { ZapIcon } from "lucide-react";
import Link from "next/link";
import { reportTokenUpsellClicked } from "@/analytics/report";
import { GridPattern } from "@/components/ui/background-patterns";
import { Badge } from "@/components/ui/badge";
import { cn } from "@/lib/utils";
export function TokensSection() {
return (
<div className="bg-card p-4 lg:py-8 lg:px-6 border rounded-xl relative w-full overflow-hidden">
<GridPattern
width={30}
height={30}
strokeDasharray={"4 2"}
className="text-border dark:text-border/70 hidden lg:block translate-x-5"
style={{
maskImage:
"linear-gradient(to bottom left,white,transparent,transparent)",
}}
/>
<Badge
variant="outline"
className="text-sm bg-background h-auto px-3 py-1 gap-2 mb-4"
>
<div className="rounded-full bg-primary size-2" />
New
</Badge>
<h2 className="font-semibold text-2xl tracking-tight mb-1">
Launch Your Tokens Effortlessly
</h2>
<p className="text-muted-foreground mb-6 text-sm lg:text-base">
Deploy contract and configure all settings you need to launch your token
in one seamless flow
</p>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 max-w-4xl">
<CardLink
assetType="coin"
title="Launch Coin"
description="Launch an ERC-20 token to create a cryptocurrency"
href="/team/~/~project/tokens/create/token"
bullets={[
"Deploy Contract",
"Configure Price and Supply",
"Airdrop Tokens",
]}
/>
<CardLink
assetType="nft"
title="Launch NFT Collection"
description="Launch an ERC-721 or ERC-1155 NFT collection"
href="/team/~/~project/tokens/create/nft"
bullets={[
"Deploy Contract",
"Upload NFTs",
"Configure Price and Supply",
]}
/>
</div>
</div>
);
}
function CardLink(props: {
title: string;
description: string;
href: string;
assetType: "nft" | "coin";
bullets: string[];
}) {
return (
<div
className={cn(
"relative flex flex-col rounded-lg border p-4 cursor-pointer hover:border-active-border bg-background",
)}
>
<div className="mb-3 flex">
<div className="flex items-center justify-center rounded-full border p-2 bg-card">
<ZapIcon className="size-4 text-muted-foreground" />
</div>
</div>
<h3 className="mb-0.5 font-semibold text-lg tracking-tight">
<Link
className="before:absolute before:inset-0"
href={props.href}
onClick={() => {
reportTokenUpsellClicked({
assetType: props.assetType,
pageType: "explore",
});
}}
>
{props.title}
</Link>
</h3>
<p className="text-muted-foreground text-sm">{props.description}</p>
<ul className="mt-4 space-y-1 text-sm list-disc list-inside text-muted-foreground">
{props.bullets.map((bullet) => (
<li key={bullet}>{bullet}</li>
))}
</ul>
</div>
);
}
@@ -26,14 +26,6 @@ const EngineSupportForm = dynamic(
ssr: false,
},
);
const ContractSupportForm = dynamic(
() =>
import("./contact-forms/contracts").then((mod) => mod.ContractSupportForm),
{
loading: () => <Skeleton className="h-12" />,
ssr: false,
},
);
const AccountSupportForm = dynamic(
() => import("./contact-forms/account").then((mod) => mod.AccountSupportForm),
{
@@ -48,14 +40,6 @@ const OtherSupportForm = dynamic(
ssr: false,
},
);
const PaymentsSupportForm = dynamic(
() =>
import("./contact-forms/payments").then((mod) => mod.PaymentsSupportForm),
{
loading: () => <Skeleton className="h-12" />,
ssr: false,
},
);
const TokensMarketplaceSupportForm = dynamic(
() =>
import("./contact-forms/tokens-marketplace").then(
@@ -68,13 +52,14 @@ const TokensMarketplaceSupportForm = dynamic(
);
const productOptions = [
{ component: <ConnectSupportForm />, label: "Wallets" },
{ component: <EngineSupportForm />, label: "Transactions" },
{ component: <PaymentsSupportForm />, label: "Payments" },
{ component: <ContractSupportForm />, label: "Contracts" },
{ component: <ConnectSupportForm />, label: "Working with User Wallets" },
{
component: <EngineSupportForm />,
label: "Using API/SDK’s, errored transactions",
},
{
component: <TokensMarketplaceSupportForm />,
label: "Tokens / Marketplace",
label: "Creating, Bridging and swapping tokens",
},
{ component: <AccountSupportForm />, label: "Account" },
{ component: <OtherSupportForm />, label: "Other" },
@@ -1,67 +0,0 @@
"use client";
import { useState } from "react";
import { DescriptionInput } from "../../shared/SupportForm_DescriptionInput";
import { SupportForm_SelectInput } from "../../shared/SupportForm_SelectInput";
import { SupportForm_TextInput } from "../../shared/SupportForm_TextInput";
import { UnitySupportForm } from "../../shared/SupportForm_UnityInput";
const AFFECTED_AREAS = ["Dashboard", "Application"];
export const AffectedAreaInput = () => {
const [selectedAffectedArea, setSelectedAffectedArea] = useState<string>("");
const [selectedSDK, setSelectedSDK] = useState<string>("");
const [description, setDescription] = useState<string>("");
const [sdkDescription, setSdkDescription] = useState<string>("");
return (
<>
<SupportForm_SelectInput
formLabel="Affected area"
name="extraInfo_Affected_Area"
onValueChange={setSelectedAffectedArea}
options={AFFECTED_AREAS}
promptText="Select an affected area"
required={true}
value={selectedAffectedArea}
/>
{selectedAffectedArea &&
(selectedAffectedArea === "Application" ? (
<>
<SupportForm_SelectInput
formLabel="SDK"
name="extraInfo_SDK"
onValueChange={setSelectedSDK}
options={["TypeScript", "React", "React Native", "Unity"]}
promptText="Select SDK"
required={true}
value={selectedSDK}
/>
{selectedSDK && (
<>
{selectedSDK === "Unity" && <UnitySupportForm />}
<SupportForm_TextInput
formLabel="SDK Version"
formValue="extraInfo_SDK_Version"
inputType="text"
required={true}
/>
<SupportForm_TextInput
formLabel="Application URL"
formValue="extraInfo_Application_URL"
inputType="url"
required={false}
/>
<DescriptionInput
value={sdkDescription}
onChange={setSdkDescription}
/>
</>
)}
</>
) : (
<DescriptionInput value={description} onChange={setDescription} />
))}
</>
);
};
@@ -1,116 +1,91 @@
"use client";
import { type ReactElement, useState } from "react";
import { DescriptionInput } from "../../shared/SupportForm_DescriptionInput";
import { SupportForm_SelectInput } from "../../shared/SupportForm_SelectInput";
import { SupportForm_TextInput } from "../../shared/SupportForm_TextInput";
import { UnitySupportForm } from "../../shared/SupportForm_UnityInput";
import { AffectedAreaInput } from "./AffectedAreaInput";
type ProblemAreaItem = {
label: string;
component: ReactElement;
};
const SDKVersionInput = () => (
<SupportForm_TextInput
formLabel="SDK Version"
formValue="extraInfo_SDK_Version"
inputType="text"
required={true}
/>
);
const OSSelect = () => {
const [selectedOS, setSelectedOS] = useState<string>("");
const SocialEmailLoginIssuesComponent = () => {
const [description, setDescription] = useState<string>("");
return (
<SupportForm_SelectInput
formLabel="OS"
name="extraInfo_OS"
onValueChange={setSelectedOS}
options={["Windows", "MacOS", "Linux", "Other"]}
promptText="Select an operating system"
required={true}
value={selectedOS}
/>
<>
<SupportForm_TextInput
formLabel="User ID/Email with issue"
formValue="extraInfo_User_ID_Email"
inputType="text"
placeholder="Enter user ID or email"
required={true}
/>
<DescriptionInput value={description} onChange={setDescription} />
</>
);
};
const DescriptionInputWrapper = () => {
const UserWalletCustomAuthComponent = () => {
const [description, setDescription] = useState<string>("");
return (
<>
<SupportForm_TextInput
formLabel="Custom auth endpoint"
formValue="extraInfo_Custom_Auth_Endpoint"
inputType="text"
placeholder="Enter custom auth endpoint"
required={true}
/>
<SupportForm_TextInput
formLabel="Support ID"
formValue="extraInfo_Support_ID"
inputType="text"
placeholder="Enter support ID (optional)"
required={false}
/>
<DescriptionInput value={description} onChange={setDescription} />
</>
);
};
const AccountAbstractionComponent = () => {
const [description, setDescription] = useState<string>("");
return (
<>
<SupportForm_TextInput
formLabel="Support ID"
formValue="extraInfo_Support_ID"
inputType="text"
placeholder="Enter support ID (optional)"
required={false}
/>
<DescriptionInput value={description} onChange={setDescription} />
</>
);
};
const ThirdPartyEOAWalletsComponent = () => {
const [description, setDescription] = useState<string>("");
return <DescriptionInput value={description} onChange={setDescription} />;
};
const PROBLEM_AREAS: ProblemAreaItem[] = [
{
component: <AffectedAreaInput />,
label: "In-app wallet login issues",
component: <SocialEmailLoginIssuesComponent />,
label: "Social/Email login issues",
},
{
component: <AffectedAreaInput />,
label: "In-app wallet transaction issues",
component: <UserWalletCustomAuthComponent />,
label: "User wallet with custom auth",
},
{
component: <AffectedAreaInput />,
label: "In-app wallet Custom Auth",
},
{
component: <AffectedAreaInput />,
component: <AccountAbstractionComponent />,
label: "Account Abstraction",
},
{
component: (
<>
<SDKVersionInput />
<SupportForm_TextInput
formLabel="Application URL"
formValue="extraInfo_Application_URL"
inputType="url"
required={false}
/>
<DescriptionInputWrapper />
</>
),
label: "thirdweb SDKs",
},
{
component: (
<>
<UnitySupportForm />
<SDKVersionInput />
<DescriptionInputWrapper />
</>
),
label: "Unity SDK",
},
{
component: (
<>
<SDKVersionInput />
<OSSelect />
<SupportForm_TextInput
formLabel="Framework"
formValue="extraInfo_dotNET_Framework"
inputType="text"
placeholder="MAUI | Blazor | Godot, etc"
required={false}
/>
<SupportForm_TextInput
formLabel="Target OS"
formValue="extraInfo_dotNET_Target_OS"
inputType="text"
required={false}
/>
<DescriptionInputWrapper />
</>
),
label: ".NET SDK",
},
{
component: <AffectedAreaInput />,
label: "Pay",
},
{
component: <AffectedAreaInput />,
label: "Auth",
component: <ThirdPartyEOAWalletsComponent />,
label: "Third party/EOA wallets",
},
];
@@ -1,138 +0,0 @@
import { type ReactElement, useState } from "react";
import { DescriptionInput } from "../../shared/SupportForm_DescriptionInput";
import { SupportForm_SelectInput } from "../../shared/SupportForm_SelectInput";
import { SupportForm_TextInput } from "../../shared/SupportForm_TextInput";
type ProblemAreaItem = {
label: string;
component: ReactElement;
};
const NetworkInput = () => (
<SupportForm_TextInput
formLabel="Network"
formValue="extraInfo_Network"
inputType="text"
placeholder="Network name or chain ID"
required={true}
/>
);
const ContractAddressInput = () => (
<SupportForm_TextInput
formLabel="Contract address"
formValue="extraInfo_Contract_Address"
inputType="text"
required={true}
/>
);
const ContractFunctionInput = () => (
<SupportForm_TextInput
formLabel="Contract function"
formValue="extraInfo_Contract_Function"
inputType="text"
required={true}
/>
);
const ContractTypeInput = () => (
<SupportForm_TextInput
formLabel="Contract type"
formValue="extraInfo_Contract_Type"
inputType="text"
placeholder={`"MarketplaceV3 | "NFTDrop" | ...`}
required={false}
/>
);
const ContractAffectedAreaInput = () => {
const [selectedAffectedArea, setSelectedAffectedArea] = useState<string>("");
return (
<SupportForm_SelectInput
formLabel="Contract affected area"
name="extraInfo_Affected_Area"
onValueChange={setSelectedAffectedArea}
options={["Dashboard", "SDK"]}
promptText="Select an affected area"
required={true}
value={selectedAffectedArea}
/>
);
};
const DescriptionInputWrapper = () => {
const [description, setDescription] = useState<string>("");
return <DescriptionInput value={description} onChange={setDescription} />;
};
const CONTRACT_PROBLEM_AREAS: ProblemAreaItem[] = [
{
component: (
<>
<NetworkInput />
<ContractTypeInput />
<ContractAffectedAreaInput />
<DescriptionInputWrapper />
</>
),
label: "Deploying a contract",
},
{
component: (
<>
<NetworkInput />
<ContractAddressInput />
<ContractTypeInput />
<DescriptionInputWrapper />
</>
),
label: "Contract verification",
},
{
component: (
<>
<NetworkInput />
<ContractAddressInput />
<ContractFunctionInput />
<ContractAffectedAreaInput />
<DescriptionInputWrapper />
</>
),
label: "Calling a function in my contract",
},
{
component: (
<>
<DescriptionInputWrapper />
</>
),
label: "Developing a custom contract",
},
{
component: (
<>
<DescriptionInputWrapper />
</>
),
label: "Other",
},
];
export function ContractSupportForm() {
const [problemArea, setProblemArea] = useState<string>("");
return (
<>
<SupportForm_SelectInput
formLabel="Problem area"
name="extraInfo_Problem_Area"
onValueChange={setProblemArea}
options={CONTRACT_PROBLEM_AREAS.map((o) => o.label)}
promptText="Select a problem area"
required={true}
value={problemArea}
/>
{CONTRACT_PROBLEM_AREAS.find((o) => o.label === problemArea)?.component}
</>
);
}
@@ -2,56 +2,62 @@ import { useState } from "react";
import { DescriptionInput } from "../../shared/SupportForm_DescriptionInput";
import { SupportForm_SelectInput } from "../../shared/SupportForm_SelectInput";
import { SupportForm_TextInput } from "../../shared/SupportForm_TextInput";
import { UnitySupportForm } from "../../shared/SupportForm_UnityInput";
const ENGINE_TYPES = ["Cloud (V3)", "Dedicated (V2)"];
const ENGINE_PROBLEM_AREAS = [
"SSL Issues",
"Transaction queueing issues",
"401 - Unauthorized",
"404 - Endpoint Not Found",
"Other",
const API_SDK_OPTIONS = [
"API endpoint",
"React/TypeScript",
"Unity/.NET",
"React Native",
];
export function EngineSupportForm() {
const [selectedEngineType, setSelectedEngineType] = useState<string>("");
const [problemArea, setProblemArea] = useState<string>("");
const [selectedSDK, setSelectedSDK] = useState<string>("");
const [description, setDescription] = useState<string>("");
return (
<>
<SupportForm_SelectInput
formLabel="Type"
name="extraInfo_Engine_Type"
onValueChange={setSelectedEngineType}
options={ENGINE_TYPES}
promptText="Select Engine type"
formLabel="Which API/SDK are you using?"
name="extraInfo_API_SDK"
onValueChange={setSelectedSDK}
options={API_SDK_OPTIONS}
promptText="Select a problem area"
required={true}
value={selectedEngineType}
/>{" "}
{selectedEngineType && (
value={selectedSDK}
/>
{selectedSDK && (
<>
<SupportForm_SelectInput
formLabel="Problem area"
name="extraInfo_Problem_Area"
onValueChange={setProblemArea}
options={ENGINE_PROBLEM_AREAS}
promptText="Select a problem area"
required={true}
value={problemArea}
/>
{problemArea && (
<>
{selectedSDK === "Unity/.NET" ? (
<UnitySupportForm />
) : (
selectedSDK !== "API endpoint" && (
<SupportForm_TextInput
formLabel="Engine URL"
formValue="extraInfo_Engine_URL"
inputType="url"
placeholder="https://your-engine-url.com"
formLabel="Version of SDK"
formValue="extraInfo_SDK_Version"
inputType="text"
placeholder="e.g. 3.12.0"
required={true}
/>
<DescriptionInput value={description} onChange={setDescription} />
</>
)
)}
<SupportForm_TextInput
formLabel="Queue/Support ID in logs"
formValue="extraInfo_Queue_Support_ID"
inputType="text"
placeholder="Enter support ID from logs"
required={false}
/>
{selectedSDK === "API endpoint" && (
<SupportForm_TextInput
formLabel="API endpoint URL with parameters"
formValue="extraInfo_API_Endpoint_URL"
inputType="url"
placeholder="https://api.example.com/endpoint?param=value"
required={false}
/>
)}
<DescriptionInput value={description} onChange={setDescription} />
</>
)}
</>
@@ -14,6 +14,7 @@ const OTHER_PROBLEM_AREAS = [
"Bug report",
"Documentation",
"Integration help",
"Agents/x402 and AI",
"Other",
];
export function OtherSupportForm() {
@@ -1,55 +0,0 @@
"use client";
import { useState } from "react";
import { DescriptionInput } from "../../shared/SupportForm_DescriptionInput";
import { SupportForm_SelectInput } from "../../shared/SupportForm_SelectInput";
import { SupportForm_TextInput } from "../../shared/SupportForm_TextInput";
const PAYMENT_AREAS = ["Dashboard", "Application"];
export function PaymentsSupportForm() {
const [area, setArea] = useState<string>("");
const [description, setDescription] = useState<string>("");
return (
<>
<SupportForm_SelectInput
formLabel="Affected Area"
name="extraInfo_Payments_Area"
onValueChange={setArea}
options={PAYMENT_AREAS}
promptText="Select affected area"
required={true}
value={area}
/>
{area === "Application" && (
<>
<SupportForm_TextInput
formLabel="SDK"
formValue="extraInfo_Payments_SDK"
inputType="text"
placeholder="e.g. React, Unity, iOS, etc."
required={true}
/>
<SupportForm_TextInput
formLabel="SDK Version"
formValue="extraInfo_Payments_SDK_Version"
inputType="text"
placeholder="e.g. 3.12.0"
required={true}
/>
<SupportForm_TextInput
formLabel="Application URL"
formValue="extraInfo_Payments_App_URL"
inputType="url"
placeholder="https://your-app-url.com"
required={true}
/>
</>
)}
{(area === "Application" || area === "Dashboard") && (
<DescriptionInput value={description} onChange={setDescription} />
)}
</>
);
}
@@ -8,10 +8,10 @@ export function TokensMarketplaceSupportForm() {
return (
<>
<SupportForm_TextInput
formLabel="Address"
formValue="extraInfo_TokensMarketplace_Address"
formLabel="Token address or symbol"
formValue="extraInfo_Token_Address_Symbol"
inputType="text"
placeholder="Enter the relevant address"
placeholder="Enter token address or symbol (e.g. 0x... or ETH)"
required={true}
/>
<SupportForm_TextInput
@@ -21,6 +21,13 @@ export function TokensMarketplaceSupportForm() {
placeholder="e.g. Ethereum, Polygon, etc."
required={true}
/>
<SupportForm_TextInput
formLabel="Support ID in logs"
formValue="extraInfo_Support_ID_Logs"
inputType="text"
placeholder="Enter support ID from logs"
required={false}
/>
<DescriptionInput value={description} onChange={setDescription} />
</>
);
@@ -1,20 +1,29 @@
import { redirect } from "next/navigation";
import type { SearchParams } from "nuqs/server";
import { getUserOpUsage } from "@/api/analytics";
import { getAuthToken } from "@/api/auth-token";
import { getProjects } from "@/api/project/projects";
import { getTeamBySlug } from "@/api/team/get-team";
import { getTeamSubscriptions } from "@/api/team/team-subscription";
import { SponsoredTransactionsTable } from "@/components/sponsored-transactions-table/SponsoredTransactionsTable";
import {
getLastNDaysRange,
type Range,
} from "@/components/analytics/date-range-selector";
import { getClientThirdwebClient } from "@/constants/thirdweb-client.client";
import { loginRedirect } from "@/utils/redirects";
import { AccountAbstractionSummary } from "../../../../[project_slug]/(sidebar)/account-abstraction/AccountAbstractionAnalytics/AccountAbstractionSummary";
import { AccountAbstractionAnalytics } from "../../../../[project_slug]/(sidebar)/account-abstraction/aa-analytics";
import { searchParamLoader } from "../../../../[project_slug]/(sidebar)/account-abstraction/search-params";
export default async function Page(props: {
params: Promise<{
team_slug: string;
}>;
searchParams: Promise<SearchParams>;
}) {
const params = await props.params;
const [team, authToken] = await Promise.all([
getTeamBySlug(params.team_slug),
const [params, parsedSearchParams, authToken] = await Promise.all([
props.params,
searchParamLoader(props.searchParams),
getAuthToken(),
]);
@@ -22,6 +31,8 @@ export default async function Page(props: {
loginRedirect(`/team/${params.team_slug}/~/usage/account-abstraction`);
}
const team = await getTeamBySlug(params.team_slug);
if (!team) {
redirect("/team");
}
@@ -44,20 +55,51 @@ export default async function Page(props: {
);
}
const interval = parsedSearchParams.interval ?? "week";
const rangeType = parsedSearchParams.range || "last-120";
const range: Range = {
from:
rangeType === "custom"
? parsedSearchParams.from
: getLastNDaysRange(rangeType).from,
to:
rangeType === "custom"
? parsedSearchParams.to
: getLastNDaysRange(rangeType).to,
type: rangeType,
};
const userOpStats = await getUserOpUsage(
{
from: range.from,
period: interval,
teamId: team.id,
to: range.to,
},
authToken,
);
const client = getClientThirdwebClient({
jwt: authToken,
teamId: team.id,
});
return (
<div className="flex flex-col gap-14">
<SponsoredTransactionsTable
<div className="flex flex-col gap-10">
<AccountAbstractionSummary
authToken={authToken}
teamId={team.id}
from={range.from}
to={range.to}
/>
<AccountAbstractionAnalytics
client={client}
from={usageSubscription.currentPeriodStart}
projects={projects}
teamId={team.id}
teamSlug={team.slug}
to={usageSubscription.currentPeriodEnd}
userOpStats={userOpStats}
variant="team"
/>
</div>
@@ -34,13 +34,17 @@ function AccountAbstractionSummaryInner(props: {
async function AsyncAccountAbstractionSummary(props: {
teamId: string;
projectId: string;
projectId?: string;
authToken: string;
from?: Date;
to?: Date;
}) {
const aggregateUserOpStats = await getAggregateUserOpUsage(
{
projectId: props.projectId,
teamId: props.teamId,
from: props.from,
to: props.to,
},
props.authToken,
);
@@ -55,8 +59,10 @@ async function AsyncAccountAbstractionSummary(props: {
export function AccountAbstractionSummary(props: {
teamId: string;
projectId: string;
projectId?: string;
authToken: string;
from?: Date;
to?: Date;
}) {
return (
<Suspense
@@ -71,6 +77,8 @@ export function AccountAbstractionSummary(props: {
projectId={props.projectId}
teamId={props.teamId}
authToken={props.authToken}
from={props.from}
to={props.to}
/>
</Suspense>
);
@@ -0,0 +1,302 @@
"use client";
import { format } from "date-fns";
import { useMemo, useState } from "react";
import { ThirdwebBarChart } from "@/components/blocks/charts/bar-chart";
import { DocLink } from "@/components/blocks/DocLink";
import { ExportToCSVButton } from "@/components/blocks/ExportToCSVButton";
import { Button } from "@/components/ui/button";
import type { ChartConfig } from "@/components/ui/chart";
import { useAllChainsData } from "@/hooks/chains/allChains";
import { DotNetIcon } from "@/icons/brand-icons/DotNetIcon";
import { ReactIcon } from "@/icons/brand-icons/ReactIcon";
import { TypeScriptIcon } from "@/icons/brand-icons/TypeScriptIcon";
import { UnityIcon } from "@/icons/brand-icons/UnityIcon";
import { UnrealIcon } from "@/icons/brand-icons/UnrealIcon";
import type { UserOpStats } from "@/types/analytics";
import { formatTickerNumber } from "@/utils/format-utils";
type ChartData = Record<string, number> & {
time: string;
};
type MetricType =
| "sponsoredTransactions"
| "gasUnits"
| "avgGasPrice"
| "costPerGasUnit";
export function GasMetricsChartCard(props: {
userOpStats: UserOpStats[];
isPending: boolean;
}) {
const { userOpStats } = props;
const topChainsToShow = 10;
const chainsStore = useAllChainsData();
const [metricType, setMetricType] = useState<MetricType>(
"sponsoredTransactions",
);
const { chartConfig, chartData } = useMemo(() => {
const _chartConfig: ChartConfig = {};
const _chartDataMap: Map<string, ChartData> = new Map();
const chainIdToVolumeMap: Map<string, number> = new Map();
// for each stat, add it in _chartDataMap
for (const stat of userOpStats) {
const chartData = _chartDataMap.get(stat.date);
const { chainId } = stat;
const chain = chainsStore.idToChain.get(Number(chainId));
const chainName = chain?.name || chainId || "Unknown";
let value: number;
if (metricType === "sponsoredTransactions") {
value = stat.successful;
} else if (metricType === "gasUnits") {
value = stat.gasUnits;
} else if (metricType === "avgGasPrice") {
value = stat.avgGasPrice;
} else {
// costPerGasUnit: USD spent per gas unit (helps identify price spike impact)
value = stat.gasUnits > 0 ? stat.sponsoredUsd / stat.gasUnits : 0;
}
// if no data for current day - create new entry
if (!chartData) {
_chartDataMap.set(stat.date, {
time: stat.date,
[chainName]: value,
} as ChartData);
} else {
chartData[chainName] = (chartData[chainName] || 0) + value;
}
chainIdToVolumeMap.set(
chainName,
value + (chainIdToVolumeMap.get(chainName) || 0),
);
}
const chainsSorted = Array.from(chainIdToVolumeMap.entries())
.sort((a, b) => b[1] - a[1])
.map((w) => w[0]);
const chainsToShow = chainsSorted.slice(0, topChainsToShow);
const chainsToTagAsOthers = chainsSorted.slice(topChainsToShow);
// replace chainIdsToTagAsOther chainId with "other"
for (const data of _chartDataMap.values()) {
for (const chainId in data) {
if (chainsToTagAsOthers.includes(chainId)) {
data.others = (data.others || 0) + (data[chainId] || 0);
delete data[chainId];
}
}
}
chainsToShow.forEach((chainName, i) => {
_chartConfig[chainName] = {
color: `hsl(var(--chart-${(i % 10) + 1}))`,
label: chainName,
};
});
// Add Other
if (chainsToTagAsOthers.length > 0) {
chainsToShow.push("others");
_chartConfig.others = {
color: "hsl(var(--muted-foreground))",
label: "Others",
};
}
return {
chartConfig: _chartConfig,
chartData: Array.from(_chartDataMap.values()).sort((a, b) => {
return new Date(a.time).getTime() - new Date(b.time).getTime();
}),
};
}, [userOpStats, chainsStore, metricType]);
const uniqueChainIds = Object.keys(chartConfig);
const disableActions =
props.isPending ||
chartData.length === 0 ||
chartData.every((data) => {
return Object.entries(data).every(([key, value]) => {
return key === "time" || value === 0;
});
});
const metricConfig = {
sponsoredTransactions: {
title: "Sponsored Transactions",
description: "Total number of sponsored transactions",
fileName: "Sponsored Transactions",
formatter: (value: number) => formatTickerNumber(value),
},
gasUnits: {
title: "Gas Units Consumed",
description: "Total gas units consumed by sponsored transactions",
fileName: "Gas Units Consumed",
formatter: (value: number) => formatTickerNumber(value),
},
avgGasPrice: {
title: "Average Gas Price",
description: "Average gas price in Gwei for sponsored transactions",
fileName: "Average Gas Price",
formatter: (value: number) => {
// Convert from wei to Gwei
const gwei = value / 1_000_000_000;
return `${formatTickerNumber(gwei)} Gwei`;
},
},
costPerGasUnit: {
title: "Cost per Gas Unit",
description:
"USD spent per gas unit - spikes indicate gas price increases",
fileName: "Cost per Gas Unit",
formatter: (value: number) => {
// Show in micro-USD for readability
return `$${(value * 1_000_000).toFixed(4)}µ`;
},
},
};
const config =
metricConfig[metricType as keyof typeof metricConfig] ||
metricConfig.sponsoredTransactions;
return (
<ThirdwebBarChart
chartClassName="aspect-[1] lg:aspect-[3.5]"
config={chartConfig}
customHeader={
<div className="relative px-6 pt-6">
<div className="flex items-start justify-between gap-4">
<div>
<h3 className="mb-0.5 font-semibold text-xl tracking-tight">
{config.title}
</h3>
<p className="text-muted-foreground text-sm">
{config.description}
</p>
</div>
<div className="flex gap-2 flex-wrap">
<Button
onClick={() => setMetricType("sponsoredTransactions")}
size="sm"
variant={
metricType === "sponsoredTransactions" ? "default" : "outline"
}
>
Transactions
</Button>
<Button
onClick={() => setMetricType("gasUnits")}
size="sm"
variant={metricType === "gasUnits" ? "default" : "outline"}
>
Gas Units
</Button>
<Button
onClick={() => setMetricType("avgGasPrice")}
size="sm"
variant={metricType === "avgGasPrice" ? "default" : "outline"}
>
Gas Price
</Button>
<Button
onClick={() => setMetricType("costPerGasUnit")}
size="sm"
variant={
metricType === "costPerGasUnit" ? "default" : "outline"
}
>
Cost Efficiency
</Button>
</div>
</div>
<div className="top-6 right-6 mt-4 mb-4 grid grid-cols-2 items-center gap-2 md:absolute md:my-0 md:flex md:mt-10">
<ExportToCSVButton
className="bg-background"
disabled={disableActions}
fileName={config.fileName}
getData={async () => {
const header = ["Date", ...uniqueChainIds];
const rows = chartData.map((data) => {
const { time, ...rest } = data;
return [
time,
...uniqueChainIds.map((w) => (rest[w] || 0).toString()),
];
});
return { header, rows };
}}
/>
</div>
</div>
}
data={chartData}
emptyChartState={<GasMetricsChartCardEmptyChartState />}
hideLabel={false}
isPending={props.isPending}
showLegend
toolTipLabelFormatter={(_v, item) => {
if (Array.isArray(item)) {
const time = item[0].payload.time as number;
return format(new Date(time), "MMM d, yyyy");
}
return undefined;
}}
toolTipValueFormatter={(value) => {
if (typeof value !== "number") {
return "";
}
return config.formatter(value);
}}
variant="stacked"
/>
);
}
function GasMetricsChartCardEmptyChartState() {
return (
<div className="flex flex-col items-center justify-center">
<span className="mb-6 text-lg">Sponsor gas for your users</span>
<div className="flex max-w-md flex-wrap items-center justify-center gap-x-6 gap-y-4">
<DocLink
icon={TypeScriptIcon}
label="TypeScript"
link="https://portal.thirdweb.com/typescript/v5/account-abstraction/get-started"
/>
<DocLink
icon={ReactIcon}
label="React"
link="https://portal.thirdweb.com/react/v5/account-abstraction/get-started"
/>
<DocLink
icon={ReactIcon}
label="React Native"
link="https://portal.thirdweb.com/react/v5/account-abstraction/get-started"
/>
<DocLink
icon={UnityIcon}
label="Unity"
link="https://portal.thirdweb.com/unity/v5/wallets/account-abstraction"
/>
<DocLink
icon={UnrealIcon}
label="Unreal Engine"
link="https://portal.thirdweb.com/unreal-engine/blueprints/smart-wallet"
/>
<DocLink
icon={DotNetIcon}
label=".NET"
link="https://portal.thirdweb.com/dotnet/wallets/providers/account-abstraction"
/>
</div>
</div>
);
}
@@ -9,6 +9,8 @@ export function createUserOpStatsStub(days: number): UserOpStats[] {
const successful = Math.random() * 100;
const failed = Math.random() * 100;
const sponsoredUsd = Math.random() * 100;
const gasUnits = Math.random() * 1000000; // Random gas units between 0-1M
const avgGasPrice = Math.random() * 100000000000; // Random gas price in wei (0-100 Gwei)
stubbedData.push({
chainId: Math.floor(Math.random() * 100).toString(),
@@ -16,6 +18,8 @@ export function createUserOpStatsStub(days: number): UserOpStats[] {
failed,
sponsoredUsd,
successful,
gasUnits,
avgGasPrice,
});
if (Math.random() > 0.7) {
@@ -11,17 +11,34 @@ import {
import { IntervalSelector } from "@/components/analytics/interval-selector";
import { SponsoredTransactionsTable } from "@/components/sponsored-transactions-table/SponsoredTransactionsTable";
import type { UserOpStats } from "@/types/analytics";
import { SponsoredTransactionsChartCard } from "./AccountAbstractionAnalytics/SponsoredTransactionsChartCard";
import { GasMetricsChartCard } from "./AccountAbstractionAnalytics/GasMetricsChartCard";
import { TotalSponsoredChartCard } from "./AccountAbstractionAnalytics/TotalSponsoredChartCard";
import { searchParams } from "./search-params";
export function AccountAbstractionAnalytics(props: {
type AccountAbstractionAnalyticsProps = {
userOpStats: UserOpStats[];
teamId: string;
teamSlug: string;
client: ThirdwebClient;
projectId: string;
}) {
} & (
| {
variant: "project";
projectId: string;
}
| {
variant: "team";
projects: {
id: string;
name: string;
image: string | null;
slug: string;
}[];
}
);
export function AccountAbstractionAnalytics(
props: AccountAbstractionAnalyticsProps,
) {
const [isLoading, startTransition] = useTransition();
const [rangeType, setRangeType] = useQueryState(
@@ -62,6 +79,8 @@ export function AccountAbstractionAnalytics(props: {
to: rangeType === "custom" ? to : getLastNDaysRange(rangeType).to,
type: rangeType,
};
const resolvedFrom = range.from;
const resolvedTo = range.to;
return (
<div>
@@ -100,20 +119,23 @@ export function AccountAbstractionAnalytics(props: {
userOpStats={props.userOpStats}
/>
<SponsoredTransactionsChartCard
<GasMetricsChartCard
isPending={isLoading}
userOpStats={props.userOpStats}
/>
<SponsoredTransactionsTable
client={props.client}
from={from.toISOString()}
projectId={props.projectId}
teamId={props.teamId}
teamSlug={props.teamSlug}
to={to.toISOString()}
variant="project"
/>
{resolvedFrom && resolvedTo && (
<SponsoredTransactionsTable
client={props.client}
from={resolvedFrom.toISOString()}
teamId={props.teamId}
teamSlug={props.teamSlug}
to={resolvedTo.toISOString()}
{...(props.variant === "project"
? { projectId: props.projectId, variant: "project" as const }
: { projects: props.projects, variant: "team" as const })}
/>
)}
</div>
</div>
);
@@ -3,7 +3,7 @@ import Link from "next/link";
import { Suspense } from "react";
import { defineChain, getContract, type ThirdwebClient } from "thirdweb";
import { getCompilerMetadata } from "thirdweb/contract";
import { getSortedDeployedContracts } from "@/api/project/getSortedDeployedContracts";
import { getFilteredProjectContracts } from "@/api/project/getSortedDeployedContracts";
import { ClientOnly } from "@/components/blocks/client-only";
import { GenericLoadingPage } from "@/components/blocks/skeletons/GenericLoadingPage";
import { Button } from "@/components/ui/button";
@@ -60,9 +60,9 @@ async function AsyncYourFactories(props: {
projectSlug: string;
clientThirdwebClient: ThirdwebClient;
}) {
const deployedContracts = await getSortedDeployedContracts({
const deployedContracts = await getFilteredProjectContracts({
authToken: props.authToken,
deploymentType: undefined,
type: "non-token-contracts",
projectId: props.projectId,
teamId: props.teamId,
});
@@ -1,11 +1,11 @@
"use client";
import { format } from "date-fns";
import { BotIcon } from "lucide-react";
import { useMemo } from "react";
import { ThirdwebBarChart } from "@/components/blocks/charts/bar-chart";
import { DocLink } from "@/components/blocks/DocLink";
import { ExportToCSVButton } from "@/components/blocks/ExportToCSVButton";
import type { ChartConfig } from "@/components/ui/chart";
import { NebulaIcon } from "@/icons/NebulaIcon";
import type { AIUsageStats } from "@/types/analytics";
type ChartData = Record<string, number> & {
@@ -106,7 +106,7 @@ function AiTokenUsageEmptyChartState() {
</span>
<div className="flex max-w-md flex-wrap items-center justify-center gap-x-6 gap-y-4">
<DocLink
icon={NebulaIcon}
icon={BotIcon}
label="Get Started"
link="https://portal.thirdweb.com/ai/chat"
/>
@@ -1,3 +1,4 @@
import { BotIcon } from "lucide-react";
import { redirect } from "next/navigation";
import { ResponsiveSearchParamsProvider } from "responsive-rsc";
import { getAuthToken } from "@/api/auth-token";
@@ -6,7 +7,6 @@ import type { DurationId } from "@/components/analytics/date-range-selector";
import { ResponsiveTimeFilters } from "@/components/analytics/responsive-time-filters";
import { ProjectPage } from "@/components/blocks/project-page/project-page";
import { getClientThirdwebClient } from "@/constants/thirdweb-client.client";
import { NebulaIcon } from "@/icons/NebulaIcon";
import { getFiltersFromSearchParams } from "@/lib/time";
import { loginRedirect } from "@/utils/redirects";
import { AiAnalytics } from "./chart";
@@ -58,7 +58,7 @@ export default async function Page(props: {
<ResponsiveSearchParamsProvider value={searchParams}>
<ProjectPage
header={{
icon: NebulaIcon,
icon: BotIcon,
client,
title: "AI Analytics",
description: "Track your AI app usage and performance",
@@ -1,5 +1,6 @@
"use client";
import {
BotIcon,
ChartLineIcon,
ChevronDownIcon,
ChevronRightIcon,
@@ -25,7 +26,6 @@ import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { DynamicHeight } from "@/components/ui/DynamicHeight";
import { TabButtons } from "@/components/ui/tabs";
import { NebulaIcon } from "@/icons/NebulaIcon";
import { useDashboardRouter } from "@/lib/DashboardRouter";
import { cn } from "@/lib/utils";
import type { TruncatedSessionInfo } from "../api/types";
@@ -52,7 +52,7 @@ export function ChatSidebar(props: {
<div className="flex h-full flex-col">
<div className="flex items-center justify-start gap-3 p-4 lg:justify-between">
<div className="flex items-center gap-2">
<NebulaIcon aria-label="AI" className="size-6 text-foreground" />
<BotIcon aria-label="AI" className="size-6 text-foreground" />
<span className="font-medium text-lg">thirdweb AI</span>
</div>
@@ -1,10 +1,9 @@
import { MarkdownRenderer } from "@workspace/ui/components/markdown-renderer";
import { ScrollShadow } from "@workspace/ui/components/scroll-shadow";
import { AlertCircleIcon } from "lucide-react";
import { AlertCircleIcon, BotIcon } from "lucide-react";
import { useEffect, useRef } from "react";
import type { ThirdwebClient } from "thirdweb";
import type { Project } from "@/api/project/projects";
import { NebulaIcon } from "@/icons/NebulaIcon";
import { cn } from "@/lib/utils";
import type {
NebulaSwapData,
@@ -238,11 +237,11 @@ function RenderMessage(props: {
)}
>
{message.type === "presence" && (
<NebulaIcon className="size-5 text-muted-foreground" />
<BotIcon className="size-5 text-muted-foreground" />
)}
{message.type === "assistant" && (
<NebulaIcon className="size-5 text-muted-foreground" />
<BotIcon className="size-5 text-muted-foreground" />
)}
{message.type === "error" && (
@@ -1,9 +1,8 @@
"use client";
import { ArrowUpRightIcon, InfoIcon } from "lucide-react";
import { ArrowUpRightIcon, BotIcon, InfoIcon } from "lucide-react";
import type { ThirdwebClient } from "thirdweb";
import { Button } from "@/components/ui/button";
import { NebulaIcon } from "@/icons/NebulaIcon";
import { cn } from "@/lib/utils";
import type { NebulaContext, NebulaUserMessage } from "../api/types";
import { examplePrompts } from "../data/examplePrompts";
@@ -33,7 +32,7 @@ export function EmptyStateChatPageContent(props: {
<div className="flex justify-center">
<div className="rounded-full border-[1.5px] border-primary-foreground/20 bg-[hsl(var(--primary-foreground)/5%)] p-1">
<div className="rounded-full border-[1.5px] border-primary-foreground/40 bg-[hsl(var(--primary-foreground)/5%)] p-2">
<NebulaIcon className="size-7 text-primary-foreground" />
<BotIcon className="size-7 text-primary-foreground" />
</div>
</div>
</div>
@@ -1,10 +1,6 @@
"use client";
import {
ArrowRightLeftIcon,
BadgeDollarSignIcon,
WebhookIcon,
} from "lucide-react";
import { CodeIcon, WebhookIcon } from "lucide-react";
import { FeatureCard } from "../payments/components/FeatureCard.client";
export function QuickStartSection(props: {
@@ -24,32 +20,36 @@ export function QuickStartSection(props: {
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
<FeatureCard
title="Cross-chain Swap Tokens"
icon={ArrowRightLeftIcon}
setupTime={5}
title="Buy Widget Component"
icon={CodeIcon}
id="swap_tokens"
features={["Swap any token", "Cross-chain swap"]}
description="Swap tokens cross-chain with dedicated swapping endpoints."
features={[
"React component",
"Cross-chain token swaps across 85+ blockchains",
"Fiat onramp support to buy tokens with credit/debit cards",
"Customizable UI",
]}
description={undefined}
link={{
href: `https://portal.thirdweb.com/bridge/swap`,
label: "Setup Swap",
label: "Get Started",
}}
/>
<FeatureCard
title="Earn Fees"
description="Setup fees to earn any time a user swaps or bridges funds."
icon={BadgeDollarSignIcon}
title="Bridge Widget Script"
description={undefined}
icon={CodeIcon}
id="fees"
setupTime={1}
features={[
"Fees on every purchase",
"Custom percentage",
"Directly to your wallet",
"Integrate with a script tag",
"Cross-chain token swaps across 85+ blockchains",
"Fiat onramp support to buy tokens with credit/debit cards",
"Customizable UI",
]}
link={{
href: `/team/${props.teamSlug}/${props.projectSlug}/settings/payments`,
label: "Configure Fees",
href: `/team/${props.teamSlug}/${props.projectSlug}/bridge/configuration`,
label: "Get Started",
}}
/>
@@ -57,7 +57,6 @@ export function QuickStartSection(props: {
title="Webhooks"
description="Create Webhooks to get notified on each purchase or transaction."
icon={WebhookIcon}
setupTime={5}
id="webhooks"
features={["Instant events", "Transaction verification"]}
link={{
@@ -3,9 +3,10 @@ import { getAuthToken } from "@/api/auth-token";
import { getProject } from "@/api/project/projects";
import { getTeamBySlug } from "@/api/team/get-team";
import { getFees } from "@/api/universal-bridge/developer";
import { getClientThirdwebClient } from "@/constants/thirdweb-client.client";
import { getProjectWallet } from "@/lib/server/project-wallet";
import { loginRedirect } from "@/utils/redirects";
import { ProjectSettingsBreadcrumb } from "../_components/project-settings-breadcrumb";
import { RouteDiscovery } from "../RouteDiscovery";
import { PayConfig } from "./PayConfig";
export default async function Page(props: {
@@ -21,7 +22,7 @@ export default async function Page(props: {
]);
if (!authToken) {
loginRedirect(`/team/${team_slug}/${project_slug}/settings/payments`);
loginRedirect(`/team/${team_slug}/${project_slug}/bridge/configuration`);
}
if (!team) {
@@ -56,14 +57,13 @@ export default async function Page(props: {
};
}
return (
<div className="flex flex-col gap-5">
<ProjectSettingsBreadcrumb
teamSlug={team_slug}
projectSlug={project_slug}
page="Payments"
/>
const client = getClientThirdwebClient({
jwt: authToken,
teamId: project.teamId,
});
return (
<div className="flex flex-col gap-6">
<PayConfig
fees={fees}
project={project}
@@ -72,6 +72,8 @@ export default async function Page(props: {
teamSlug={team_slug}
authToken={authToken}
/>
<RouteDiscovery client={client} project={project} />
</div>
);
}
@@ -0,0 +1,83 @@
import { WebhookIcon } from "lucide-react";
import { redirect } from "next/navigation";
import { getAuthToken } from "@/api/auth-token";
import { getProject } from "@/api/project/projects";
import { ProjectPage } from "@/components/blocks/project-page/project-page";
import { getClientThirdwebClient } from "@/constants/thirdweb-client.client";
import { BridgeIcon } from "@/icons/BridgeIcon";
import { loginRedirect } from "@/utils/redirects";
export default async function Layout(props: {
params: Promise<{
team_slug: string;
project_slug: string;
}>;
children: React.ReactNode;
}) {
const [params, authToken] = await Promise.all([props.params, getAuthToken()]);
const project = await getProject(params.team_slug, params.project_slug);
if (!authToken) {
loginRedirect(`/team/${params.team_slug}/${params.project_slug}/bridge`);
}
if (!project) {
redirect(`/team/${params.team_slug}`);
}
const client = getClientThirdwebClient({
jwt: authToken,
teamId: project.teamId,
});
return (
<ProjectPage
header={{
client,
title: "Bridge",
icon: BridgeIcon,
description: (
<>
Bridge lets developers swap and transfer any token across any chain
instantly
</>
),
actions: {
secondary: {
href: `/team/${params.team_slug}/${params.project_slug}/webhooks/payments`,
label: "Webhooks",
icon: <WebhookIcon className="size-3.5 text-muted-foreground" />,
},
},
links: [
{
type: "docs",
href: "https://portal.thirdweb.com/bridge",
},
{
type: "playground",
href: "https://playground.thirdweb.com/bridge/swap-widget",
},
{
type: "api",
href: "https://api.thirdweb.com/reference#tag/bridge",
},
],
}}
tabs={[
{
name: "Overview",
path: `/team/${params.team_slug}/${params.project_slug}/bridge`,
exactMatch: true,
},
{
name: "Configuration",
path: `/team/${params.team_slug}/${params.project_slug}/bridge/configuration`,
},
]}
>
{props.children}
</ProjectPage>
);
}
@@ -1,17 +1,12 @@
import { WebhookIcon } from "lucide-react";
import { redirect } from "next/navigation";
import { ResponsiveSearchParamsProvider } from "responsive-rsc";
import { getAuthToken } from "@/api/auth-token";
import { getProject } from "@/api/project/projects";
import { ProjectPage } from "@/components/blocks/project-page/project-page";
import { getClientThirdwebClient } from "@/constants/thirdweb-client.client";
import { BridgeIcon } from "@/icons/BridgeIcon";
import { loginRedirect } from "@/utils/redirects";
import { PayAnalytics } from "../payments/components/PayAnalytics";
import { getUniversalBridgeFiltersFromSearchParams } from "../payments/components/time";
import { QuickStartSection } from "./QuickstartSection.client";
import { RouteDiscovery } from "./RouteDiscovery";
import { ViewTxStatus } from "./view-tx-status";
export default async function Page(props: {
params: Promise<{
@@ -51,69 +46,27 @@ export default async function Page(props: {
});
return (
<ProjectPage
header={{
client,
title: "Bridge",
icon: BridgeIcon,
description: (
<>
Bridge lets developers swap and transfer any token across any chain
instantly
</>
),
actions: {
secondary: {
href: `/team/${params.team_slug}/${params.project_slug}/webhooks/payments`,
label: "Webhooks",
icon: <WebhookIcon className="size-3.5 text-muted-foreground" />,
},
},
settings: {
href: `/team/${params.team_slug}/${params.project_slug}/settings/payments`,
},
links: [
{
type: "docs",
href: "https://portal.thirdweb.com/bridge",
},
{
type: "playground",
href: "https://playground.thirdweb.com/bridge/swap-widget",
},
{
type: "api",
href: "https://api.thirdweb.com/reference#tag/bridge",
},
],
}}
>
<div className="flex flex-col gap-6">
<ResponsiveSearchParamsProvider value={searchParams}>
<PayAnalytics
client={client}
interval={interval}
projectClientId={project.publishableKey}
projectId={project.id}
range={range}
teamId={project.teamId}
authToken={authToken}
/>
</ResponsiveSearchParamsProvider>
<div className="flex flex-col gap-6">
<ResponsiveSearchParamsProvider value={searchParams}>
<PayAnalytics
client={client}
interval={interval}
projectClientId={project.publishableKey}
projectId={project.id}
range={range}
teamId={project.teamId}
authToken={authToken}
/>
</ResponsiveSearchParamsProvider>
<RouteDiscovery client={client} project={project} />
<ViewTxStatus client={client} />
<div className="pt-4">
<QuickStartSection
projectSlug={params.project_slug}
teamSlug={params.team_slug}
clientId={project.publishableKey}
teamId={project.teamId}
/>
</div>
<div className="pt-4">
<QuickStartSection
projectSlug={params.project_slug}
teamSlug={params.team_slug}
clientId={project.publishableKey}
teamId={project.teamId}
/>
</div>
</ProjectPage>
</div>
);
}
@@ -1,72 +0,0 @@
"use client";
import { Button } from "@workspace/ui/components/button";
import { ArrowUpRightIcon } from "lucide-react";
import Link from "next/link";
import { useState } from "react";
import type { ThirdwebClient } from "thirdweb";
import { SingleNetworkSelector } from "@/components/blocks/NetworkSelectors";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
export function ViewTxStatus(props: { client: ThirdwebClient }) {
const [txHash, setTxHash] = useState("");
const [chainId, setChainId] = useState<number>(1);
const isValidTxHash = txHash.length === 66 && txHash.startsWith("0x");
const isEnabled = !!txHash && !!chainId && isValidTxHash;
return (
<div className="bg-card rounded-xl border">
<div className="p-4 lg:p-6 mb-3">
<h3 className="font-semibold text-xl tracking-tight mb-1">
View transaction status
</h3>
<p className="text-sm text-muted-foreground mb-4">
Select chain and enter the transaction hash to view the transaction
status.
</p>
<div className="grid grid-cols-1 gap-4 lg:grid-cols-2 max-w-3xl">
<div className="space-y-2">
<Label>Chain</Label>
<SingleNetworkSelector
chainId={chainId}
onChange={setChainId}
client={props.client}
disableChainId
className="bg-background"
/>
</div>
<div className="space-y-2">
<Label htmlFor="view-tx-hash">Transaction Hash</Label>
<Input
id="view-tx-hash"
value={txHash}
onChange={(e) => setTxHash(e.target.value)}
placeholder="0x.."
/>
{txHash && !isValidTxHash && (
<p className="text-sm text-destructive-text">
Invalid transaction hash
</p>
)}
</div>
</div>
</div>
<div className="px-4 lg:px-6 py-4 border-t border-dashed justify-end items-center flex">
<Button
asChild
disabled={!isEnabled}
size="sm"
variant={isEnabled ? "default" : "outline"}
className="gap-1.5 rounded-full"
>
<Link href={`/${chainId.toString()}/tx/${txHash}`} target="_blank">
View Transaction Status <ArrowUpRightIcon className="size-4" />
</Link>
</Button>
</div>
</div>
);
}
@@ -3,8 +3,8 @@ import { CopyTextButton } from "@/components/ui/CopyTextButton";
export function ClientIDSection(props: { clientId: string }) {
return (
<div>
<h3>Client ID</h3>
<p className="mb-2 text-muted-foreground text-sm">
<h3 className="mb-1 text-base text-foreground font-medium">Client ID</h3>
<p className="mb-3 text-muted-foreground text-sm">
Identifies your application
</p>
@@ -0,0 +1,45 @@
"use client";
import { useState } from "react";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
export function CodeShowcase(props: {
title: string;
tabs: Array<{
label: string;
code: React.ReactNode;
}>;
}) {
const [selectedTab, setSelectedTab] = useState(props.tabs[0]?.label || "");
return (
<div>
<div className="flex items-center gap-2 justify-between border px-5 py-3 rounded-t-xl bg-card">
<h3 className="text-sm font-mono">{props.title}</h3>
<Select
value={selectedTab}
onValueChange={(val) => setSelectedTab(val)}
>
<SelectTrigger className="w-[160px] rounded-full">
<SelectValue placeholder="Select language" />
</SelectTrigger>
<SelectContent>
{props.tabs.map((tab) => (
<SelectItem key={tab.label} value={tab.label}>
{tab.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{props.tabs.find((tab) => tab.label === selectedTab)?.code}
</div>
);
}
@@ -24,6 +24,7 @@ type Story = StoryObj<typeof meta>;
export const Default: Story = {
args: {
projectWalletSection: undefined,
project: {
...projectStub("foo", "bar"),
secretKeys: [
@@ -1,27 +1,40 @@
import { Button } from "@workspace/ui/components/button";
import { CodeServer } from "@workspace/ui/components/code/code.server";
import {
ArrowLeftRightIcon,
ArrowUpRightIcon,
BotIcon,
ChevronRightIcon,
ExternalLinkIcon,
CoinsIcon,
DoorOpenIcon,
} from "lucide-react";
import Link from "next/link";
import type { Project } from "@/api/project/projects";
import { CodeServer } from "@/components/ui/code/code.server";
import { BridgeIcon } from "@/icons/BridgeIcon";
import { DotNetIcon } from "@/icons/brand-icons/DotNetIcon";
import { GithubIcon } from "@/icons/brand-icons/GithubIcon";
import { ExpoIcon } from "@/icons/brand-icons/ExpoIcon";
import { NextjsIcon } from "@/icons/brand-icons/NextjsIcon";
import { NodeJSIcon } from "@/icons/brand-icons/NodeJSIcon";
import { ReactIcon } from "@/icons/brand-icons/ReactIcon";
import { TypeScriptIcon } from "@/icons/brand-icons/TypeScriptIcon";
import { UnityIcon } from "@/icons/brand-icons/UnityIcon";
import { UnrealIcon } from "@/icons/brand-icons/UnrealIcon";
import { ContractIcon } from "@/icons/ContractIcon";
import { InsightIcon } from "@/icons/InsightIcon";
import { ViteIcon } from "@/icons/brand-icons/ViteIcon";
import { PayIcon } from "@/icons/PayIcon";
import { WalletProductIcon } from "@/icons/WalletProductIcon";
import { ClientIDSection } from "./ClientIDSection";
import { CodeShowcase } from "./CodeSelector";
import { SecretKeySection } from "./SecretKeySection";
export function ProjectFTUX(props: { project: Project; teamSlug: string }) {
export function ProjectFTUX(props: {
project: Project;
teamSlug: string;
projectWalletSection: React.ReactNode;
}) {
return (
<div className="flex flex-col gap-10">
<div className="flex flex-col gap-14">
<IntegrateAPIKeySection project={props.project} />
{props.projectWalletSection}
<GetStartedSection project={props.project} />
<ProductsSection
projectSlug={props.project.slug}
teamSlug={props.teamSlug}
@@ -39,12 +52,29 @@ function IntegrateAPIKeySection({ project }: { project: Project }) {
const clientId = project.publishableKey;
return (
<section>
<h2 className="mb-3 font-semibold text-xl tracking-tight">
Integrate API key
</h2>
<section className="grid grid-cols-1 gap-4 lg:grid-cols-2">
<div>
<h2 className="mb-1 font-semibold text-xl tracking-tight">API Keys</h2>
<p className="text-muted-foreground text-sm mb-4">
Your API key is used to authenticate and integrate your application.
</p>
<Button
variant="outline"
className="bg-card rounded-lg gap-2"
size="sm"
asChild
>
<Link
href="https://portal.thirdweb.com/account/api-keys"
target="_blank"
>
Learn more about API keys
<ArrowUpRightIcon className="size-4" />
</Link>
</Button>
</div>
<div className="rounded-lg border border-border bg-card p-4">
<div className="rounded-xl border bg-card p-5">
<div className="flex flex-col gap-6 ">
<ClientIDSection clientId={clientId} />
{secretKeyMasked && (
@@ -54,23 +84,74 @@ function IntegrateAPIKeySection({ project }: { project: Project }) {
/>
)}
</div>
<div className="h-5" />
<div className="flex flex-col gap-3">
<p className="text-muted-foreground text-sm">
Run this command in your terminal to send a test transaction using
your Project Wallet.
</p>
<CodeServer
className="bg-background"
code={curlCodeExample(project)}
lang="bash"
/>
</div>
</div>
</section>
);
}
function GetStartedSection({ project }: { project: Project }) {
return (
<section className="grid grid-cols-1 gap-4 lg:grid-cols-2">
<div>
<h2 className="mb-1 font-semibold text-xl tracking-tight">
Get Started
</h2>
<p className="text-muted-foreground text-sm mb-4">
Send a test transaction using your project wallet
</p>
<Button
variant="outline"
className="bg-card rounded-lg gap-2"
size="sm"
asChild
>
<Link href="https://portal.thirdweb.com/reference" target="_blank">
View API Reference
<ArrowUpRightIcon className="size-4" />
</Link>
</Button>
</div>
<CodeShowcase
title="POST /v1/transactions"
tabs={[
{
label: "Curl",
code: (
<CodeServer
code={curlCodeExample(project)}
lang="bash"
className="border-t-0 rounded-t-none"
/>
),
},
{
label: "JavaScript",
code: (
<CodeServer
code={fetchJSCodeExample(project)}
lang="js"
className="border-t-0 rounded-t-none"
/>
),
},
{
label: "Python",
code: (
<CodeServer
code={pythonCodeExample(project)}
lang="python"
className="border-t-0 rounded-t-none"
/>
),
},
]}
/>
</section>
);
}
const curlCodeExample = (project: Project): string => `\
curl https://api.thirdweb.com/v1/transactions \\
--request POST \\
@@ -88,6 +169,40 @@ curl https://api.thirdweb.com/v1/transactions \\
}'
`;
const fetchJSCodeExample = (project: Project): string => `\
fetch("https://api.thirdweb.com/v1/transactions", {
method: "POST",
headers: {
"Content-Type": "application/json",
"x-secret-key": "${project.secretKeys[0]?.masked ?? "<YOUR_SECRET_KEY>"}",
},
body: JSON.stringify({
chainId: 421614,
transactions: [
{ data: "0x", to: "vitalik.eth", value: "0" },
],
}),
});
`;
const pythonCodeExample = (project: Project): string => `\
import requests
url = "https://api.thirdweb.com/v1/transactions"
headers = {
"Content-Type": "application/json",
"x-secret-key": "${project.secretKeys[0]?.masked ?? "<YOUR_SECRET_KEY>"}",
}
payload = {
"chainId": 421614,
"transactions": [
{ "data": "0x", "to": "vitalik.eth", "value": "0" },
],
}
response = requests.post(url, headers=headers, json=payload)
result = response.json()
`;
// products section ------------------------------------------------------------
function ProductsSection(props: { teamSlug: string; projectSlug: string }) {
@@ -98,56 +213,60 @@ function ProductsSection(props: { teamSlug: string; projectSlug: string }) {
icon: React.FC<{ className?: string }>;
}> = [
{
description:
"Scale your application with a backend server to read, write, and deploy contracts at production-grade.",
href: `/team/${props.teamSlug}/${props.projectSlug}/transactions`,
icon: ArrowLeftRightIcon,
title: "Transactions",
icon: WalletProductIcon,
title: "Wallets",
description: "Wallets to read, write and transact",
href: `/team/${props.teamSlug}/${props.projectSlug}/wallets/user-wallets`,
},
{
description:
"Deploy your own contracts or leverage existing solutions for onchain implementation",
href: `/team/${props.teamSlug}/${props.projectSlug}/contracts`,
icon: ContractIcon,
title: "Contracts",
icon: BridgeIcon,
title: "Bridge",
description: "Swap and bridge tokens",
href: `/team/${props.teamSlug}/${props.projectSlug}/bridge`,
},
{
description:
"Add indexing capabilities to retrieve real-time onchain data",
href: `/team/${props.teamSlug}/${props.projectSlug}/insight`,
icon: InsightIcon,
title: "Insight",
},
{
description:
"Bridge, swap, and purchase cryptocurrencies with any fiat options or tokens via cross-chain routing",
href: `/team/${props.teamSlug}/${props.projectSlug}/payments`,
icon: PayIcon,
title: "Payments",
title: "x402",
description: "Native internet payments",
href: `/team/${props.teamSlug}/${props.projectSlug}/x402`,
},
{
icon: DoorOpenIcon,
title: "Gateway",
description: "Blockchain connectivity and data access",
href: `/team/${props.teamSlug}/${props.projectSlug}/gateway/rpc`,
},
{
icon: CoinsIcon,
title: "Tokens",
description: "Launch tokens and markets",
href: `/team/${props.teamSlug}/${props.projectSlug}/tokens`,
},
{
icon: BotIcon,
title: "AI",
description: "Read and write onchain via AI agents",
href: `/team/${props.teamSlug}/${props.projectSlug}/ai`,
},
];
return (
<div className="">
<h2 className="mb-0.5 font-semibold text-xl tracking-tight">
Complete your full-stack application
</h2>
<p className="text-muted-foreground">
Tools to build frontend, backend, and onchain with built-in
infrastructure and analytics.
<h2 className="mb-1 font-semibold text-xl tracking-tight">Products</h2>
<p className="text-muted-foreground text-sm">
Everything you need to build full-stack applications, games, and agents.
</p>
<div className="h-4" />
{/* Feature Cards */}
<section className="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-4">
<section className="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">
{products.map((product) => (
<ProductCard
<IconCard
description={product.description}
href={product.href}
icon={product.icon}
key={product.title}
title={product.title}
name={product.title}
/>
))}
</section>
@@ -155,72 +274,51 @@ function ProductsSection(props: { teamSlug: string; projectSlug: string }) {
);
}
function ProductCard(props: {
title: string;
description: string;
href: string;
icon: React.FC<{ className?: string }>;
}) {
return (
<div className="relative flex flex-col rounded-lg border bg-card p-4 hover:border-active-border">
<div className="mb-3 flex size-9 items-center justify-center rounded-full border p-1">
<props.icon className="size-5 text-muted-foreground" />
</div>
<h3 className="mb-0.5 font-semibold text-lg tracking-tight">
<Link className="before:absolute before:inset-0" href={props.href}>
{props.title}
</Link>
</h3>
<p className="text-muted-foreground text-sm">{props.description}</p>
</div>
);
}
// sdk section ------------------------------------------------------------
type SDKCardProps = {
type IconCardProps = {
name: string;
description: string | undefined;
href: string;
icon: React.FC<{ className?: string }>;
trackingLabel: string;
};
const sdks: SDKCardProps[] = [
const sdks: IconCardProps[] = [
{
href: "https://portal.thirdweb.com/sdk/typescript",
icon: TypeScriptIcon,
name: "TypeScript",
trackingLabel: "typescript",
description: undefined,
},
{
href: "https://portal.thirdweb.com/react/v5",
icon: ReactIcon,
name: "React",
trackingLabel: "react",
description: undefined,
},
{
href: "https://portal.thirdweb.com/react-native/v5",
icon: ReactIcon,
name: "React Native",
trackingLabel: "react_native",
description: undefined,
},
{
href: "https://portal.thirdweb.com/unity/v5",
icon: UnityIcon,
name: "Unity",
trackingLabel: "unity",
description: undefined,
},
{
href: "https://portal.thirdweb.com/unreal-engine",
icon: UnrealIcon,
name: "Unreal Engine",
trackingLabel: "unreal",
description: undefined,
},
{
href: "https://portal.thirdweb.com/dotnet",
icon: DotNetIcon,
name: ".NET",
trackingLabel: "dotnet",
description: undefined,
},
];
@@ -228,14 +326,14 @@ function SDKSection() {
return (
<section>
<h2 className="mb-3 font-semibold text-xl tracking-tight">Client SDKs</h2>
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4">
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-3">
{sdks.map((sdk) => (
<SDKCard
<IconCard
href={sdk.href}
icon={sdk.icon}
key={sdk.name}
name={sdk.name}
trackingLabel={sdk.trackingLabel}
description={sdk.description}
/>
))}
</div>
@@ -243,10 +341,10 @@ function SDKSection() {
);
}
function SDKCard(props: SDKCardProps) {
function IconCard(props: IconCardProps) {
return (
<div className="relative flex items-center gap-3 rounded-lg border bg-card p-4 hover:border-active-border">
<div className="flex size-9 items-center justify-center rounded-full border">
<div className="relative flex items-center gap-3 rounded-xl border bg-card p-4 hover:border-active-border">
<div className="flex items-center justify-center rounded-full border p-2 bg-background">
<props.icon className="size-4 text-muted-foreground" />
</div>
<div className="flex flex-col gap-0.5">
@@ -254,16 +352,14 @@ function SDKCard(props: SDKCardProps) {
<Link
className="before:absolute before:inset-0"
href={props.href}
rel="noopener noreferrer"
target="_blank"
target={props.href.startsWith("http") ? "_blank" : undefined}
>
{props.name}
</Link>
</p>
<p className="inline-flex items-center gap-1.5 text-muted-foreground text-xs">
View Docs
<ExternalLinkIcon className="size-3" />
</p>
{props.description && (
<p className="text-muted-foreground text-sm">{props.description}</p>
)}
</div>
</div>
);
@@ -271,42 +367,47 @@ function SDKCard(props: SDKCardProps) {
// starter kits section ------------------------------------------------------------
type StartedKitCardProps = {
name: string;
href: string;
trackingLabel: string;
};
const startedKits: StartedKitCardProps[] = [
const startedKits: IconCardProps[] = [
{
href: "https://github.com/thirdweb-example/next-starter",
name: "Next Starter",
trackingLabel: "next_starter",
icon: NextjsIcon,
description: undefined,
},
{
href: "https://github.com/thirdweb-example/vite-starter",
name: "Vite Starter",
trackingLabel: "vite_starter",
icon: ViteIcon,
description: undefined,
},
{
href: "https://github.com/thirdweb-example/expo-starter",
name: "Expo Starter",
trackingLabel: "expo_starter",
icon: ExpoIcon,
description: undefined,
},
{
href: "https://github.com/thirdweb-example/node-starter",
name: "Node Starter",
trackingLabel: "node_starter",
icon: NodeJSIcon,
description: undefined,
},
];
function StarterKitsSection() {
return (
<section>
<div className="mb-3 flex items-center justify-between gap-2">
<h2 className="font-semibold text-xl tracking-tight">Starter Kits</h2>
<div className="mb-3 flex items-start justify-between gap-2">
<div>
<h2 className="font-semibold text-xl tracking-tight mb-1">
Starters
</h2>
<p className="text-muted-foreground text-sm">
Kickstart your development process with ready-to-ship repositories.
</p>
</div>
<Link
className="inline-flex items-center gap-1 text-muted-foreground text-sm hover:text-foreground"
className="inline-flex items-center gap-1 text-muted-foreground text-sm hover:text-foreground shrink-0"
href="https://github.com/thirdweb-example"
rel="noopener noreferrer"
target="_blank"
@@ -315,41 +416,17 @@ function StarterKitsSection() {
</Link>
</div>
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4">
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-3">
{startedKits.map((kit) => (
<StarterKitCard
<IconCard
href={kit.href}
icon={kit.icon}
key={kit.name}
name={kit.name}
trackingLabel={kit.trackingLabel}
description={kit.description}
/>
))}
</div>
</section>
);
}
function StarterKitCard(props: StartedKitCardProps) {
return (
<div className="relative flex items-center gap-3 rounded-lg border bg-card p-4 hover:border-active-border">
<div className="flex size-9 items-center justify-center rounded-full border">
<GithubIcon className="size-4 text-muted-foreground" />
</div>
<div className="flex flex-col gap-0.5">
<Link
className="before:absolute before:inset-0"
href={props.href}
rel="noopener noreferrer"
target="_blank"
>
{props.name}
</Link>
<p className="inline-flex items-center gap-1.5 text-muted-foreground text-xs">
View Repo
<ExternalLinkIcon className="size-3" />
</p>
</div>
</div>
);
}
@@ -13,15 +13,15 @@ export function SecretKeySection(props: {
return (
<div>
<h3>Secret Key</h3>
<p className="mb-2 text-muted-foreground text-sm">
<h3 className="mb-1 text-base text-foreground font-medium">Secret Key</h3>
<p className="mb-3 text-muted-foreground text-sm">
Identifies and authenticates your application from a backend. <br />{" "}
This is not the full secret key, Refer to your saved secret key at the
This is not the full secret key. Refer to your saved secret key at the
time of creation for the full secret key.
</p>
<div className="flex flex-col gap-3 lg:flex-row lg:items-center">
<div className="rounded-lg border border-border bg-background px-4 py-3 font-mono text-sm lg:w-[350px]">
<div className="flex flex-col gap-3 lg:flex-row lg:items-center lg:w-[400px]">
<div className="rounded-lg border border-border bg-background px-4 py-3 font-mono text-sm grow">
{secretKeyMasked}
</div>
@@ -1,11 +1,10 @@
"use client";
import { Badge } from "@workspace/ui/components/badge";
import {
BookTextIcon,
BoxIcon,
BotIcon,
DatabaseIcon,
DoorOpenIcon,
HomeIcon,
RssIcon,
Settings2Icon,
WebhookIcon,
} from "lucide-react";
@@ -15,8 +14,6 @@ import {
} from "@/components/blocks/full-width-sidebar-layout";
import { BridgeIcon } from "@/icons/BridgeIcon";
import { ContractIcon } from "@/icons/ContractIcon";
import { InsightIcon } from "@/icons/InsightIcon";
import { NebulaIcon } from "@/icons/NebulaIcon";
import { PayIcon } from "@/icons/PayIcon";
import { TokenIcon } from "@/icons/TokenIcon";
import { WalletProductIcon } from "@/icons/WalletProductIcon";
@@ -24,6 +21,8 @@ import { WalletProductIcon } from "@/icons/WalletProductIcon";
export function ProjectSidebarLayout(props: {
layoutPath: string;
children: React.ReactNode;
hasEngines: boolean;
showContracts: boolean;
}) {
const contentSidebarLinks = [
{
@@ -36,91 +35,84 @@ export function ProjectSidebarLayout(props: {
separator: true,
},
{
group: "Build",
subMenu: {
icon: WalletProductIcon,
label: "Wallets",
},
links: [
{
subMenu: {
icon: WalletProductIcon,
label: "Wallets",
href: `${props.layoutPath}/wallets/user-wallets`,
label: "User Wallets",
},
{
href: `${props.layoutPath}/wallets/server-wallets`,
label: "Server Wallets",
},
{
href: `${props.layoutPath}/wallets/sponsored-gas`,
label: "Gas Sponsorship",
},
],
},
...(props.showContracts
? [
{
href: `${props.layoutPath}/contracts`,
icon: ContractIcon,
label: "Contracts",
},
links: [
{
href: `${props.layoutPath}/wallets/user-wallets`,
label: "User Wallets",
},
{
href: `${props.layoutPath}/wallets/server-wallets`,
label: "Server Wallets",
},
{
href: `${props.layoutPath}/wallets/sponsored-gas`,
label: "Gas Sponsorship",
},
],
},
{
href: `${props.layoutPath}/contracts`,
icon: ContractIcon,
label: "Contracts",
},
{
href: `${props.layoutPath}/ai`,
icon: NebulaIcon,
label: "AI",
},
],
]
: []),
{
href: `${props.layoutPath}/x402`,
icon: PayIcon,
label: (
<span className="flex items-center gap-2">
x402 <Badge>New</Badge>
</span>
),
},
{
separator: true,
href: `${props.layoutPath}/bridge`,
icon: BridgeIcon,
label: "Bridge",
},
{
group: "Monetize",
href: `${props.layoutPath}/tokens`,
icon: TokenIcon,
label: "Tokens",
},
{
href: `${props.layoutPath}/ai`,
icon: BotIcon,
label: "AI",
},
{
subMenu: {
icon: DoorOpenIcon,
label: "Gateway",
},
links: [
{
href: `${props.layoutPath}/x402`,
icon: PayIcon,
label: (
<span className="flex items-center gap-2">
x402 <Badge>New</Badge>
</span>
),
},
{
href: `${props.layoutPath}/bridge`,
icon: BridgeIcon,
label: "Bridge",
},
{
href: `${props.layoutPath}/tokens`,
icon: TokenIcon,
label: "Tokens",
},
],
},
{
separator: true,
},
{
group: "Scale",
links: [
{
href: `${props.layoutPath}/insight`,
icon: InsightIcon,
label: "Insight",
},
{
href: `${props.layoutPath}/rpc`,
icon: RssIcon,
href: `${props.layoutPath}/gateway/rpc`,
label: "RPC",
},
// linkely want to move this to `team` level eventually
{
href: `${props.layoutPath}/engine`,
icon: DatabaseIcon,
label: "Engine",
href: `${props.layoutPath}/gateway/indexer`,
label: "Indexer",
},
],
},
// only show engine link if there the user already has an engine instance
...(props.hasEngines
? [
{
href: `${props.layoutPath}/engine`,
icon: DatabaseIcon,
label: "Engine",
},
]
: []),
] satisfies ShadcnSidebarLink[];
const footerSidebarLinks = [
@@ -140,19 +132,6 @@ export function ProjectSidebarLayout(props: {
icon: Settings2Icon,
label: "Project Settings",
},
{
separator: true,
},
{
href: "https://portal.thirdweb.com",
icon: BookTextIcon,
label: "Documentation",
},
{
href: "https://playground.thirdweb.com/wallets/sign-in/button",
icon: BoxIcon,
label: "Playground",
},
] satisfies ShadcnSidebarLink[];
return (
@@ -177,7 +177,7 @@ export function ProjectWalletDetailsSection(props: ProjectWalletControlsProps) {
</DropdownMenuContent>
</DropdownMenu>
<div className="p-4 relative flex flex-col gap-5">
<div className="p-5 relative flex flex-col gap-5">
<div>
<p className="text-sm text-foreground mb-1">Wallet Address</p>
<CopyTextButton
@@ -200,7 +200,7 @@ export function ProjectWalletDetailsSection(props: ProjectWalletControlsProps) {
</div>
</div>
<div className="p-4 border-t border-dashed flex justify-between items-center">
<div className="p-5 border-t border-dashed flex justify-between items-center">
<div>
<p className="text-sm text-foreground mb-1">Balance</p>
<div className="flex items-center gap-1">
@@ -49,6 +49,7 @@ const projectWallet2: ProjectWalletSummary = {
export const NoProjectWalletSetNoManagedAccessToken: Story = {
args: {
layout: "column",
project: projectWithoutManagedAccessToken,
client: storybookThirdwebClient,
teamSlug: "bar",
@@ -62,6 +63,7 @@ export const NoProjectWalletSetNoManagedAccessToken: Story = {
export const NoProjectWalletSetWithManagedAccessToken: Story = {
args: {
layout: "column",
project: projectWithManagedAccessToken,
client: storybookThirdwebClient,
teamSlug: "bar",
@@ -75,6 +77,7 @@ export const NoProjectWalletSetWithManagedAccessToken: Story = {
export const NoProjectWalletSetWithManagedAccessTokenAndServerWallets: Story = {
args: {
layout: "column",
project: projectWithManagedAccessToken,
teamSlug: "bar",
client: storybookThirdwebClient,
@@ -88,6 +91,7 @@ export const NoProjectWalletSetWithManagedAccessTokenAndServerWallets: Story = {
export const NoProjectWalletSetLoading: Story = {
args: {
layout: "column",
project: projectWithManagedAccessToken,
teamSlug: "bar",
client: storybookThirdwebClient,
@@ -101,6 +105,7 @@ export const NoProjectWalletSetLoading: Story = {
export const ProjectWalletSetMultipleServerWallets: Story = {
args: {
layout: "column",
project: projectWithManagedAccessToken,
teamSlug: "bar",
client: storybookThirdwebClient,
@@ -114,6 +119,7 @@ export const ProjectWalletSetMultipleServerWallets: Story = {
export const ProjectWalletSetSingleServerWallet: Story = {
args: {
layout: "column",
project: projectWithManagedAccessToken,
teamSlug: "bar",
projectWallet: projectWallet1,
@@ -2,7 +2,8 @@
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { UnderlineLink } from "@workspace/ui/components/UnderlineLink";
import { ChevronDownIcon, XIcon } from "lucide-react";
import { ArrowUpRightIcon, ChevronDownIcon, XIcon } from "lucide-react";
import Link from "next/link";
import { useMemo, useState } from "react";
import { toast } from "sonner";
import type { ThirdwebClient } from "thirdweb";
@@ -29,6 +30,7 @@ import { Skeleton } from "@/components/ui/skeleton";
import { getClientThirdwebClient } from "@/constants/thirdweb-client.client";
import { useDashboardRouter } from "@/lib/DashboardRouter";
import type { ProjectWalletSummary } from "@/lib/server/project-wallet";
import { cn } from "@/lib/utils";
import { updateDefaultProjectWallet } from "../../transactions/lib/vault.client";
import { CreateServerWallet } from "../../transactions/server-wallets/components/create-server-wallet.client";
import { ProjectWalletDetailsSection } from "./project-wallet-details";
@@ -131,11 +133,11 @@ function CreateProjectWalletSection(props: {
Boolean(managementAccessToken) && serverWallets.length > 0;
return (
<div className="rounded-xl border p-4 bg-card">
<div className="rounded-xl border p-5 bg-card">
<div className="rounded-full p-2 border bg-background inline-flex mb-4">
<XIcon className="size-3.5 text-muted-foreground" />
<XIcon className="size-4 text-muted-foreground" />
</div>
<h2 className="text-sm font-medium text-foreground mb-0.5">
<h2 className="text-base font-medium text-foreground mb-0.5">
No Project Wallet set
</h2>
<p className="text-sm text-muted-foreground">
@@ -276,10 +278,17 @@ export function ProjectWalletSectionUI(props: {
projectWallet: ProjectWalletSummary | undefined;
getProjectServerWallets: GetProjectServerWallets;
client: ThirdwebClient;
layout: "row" | "column";
}) {
return (
<div>
<div className="mb-3">
<div
className={cn(
props.layout === "row"
? "grid grid-cols-1 gap-4 lg:grid-cols-2"
: "flex flex-col gap-4",
)}
>
<div>
<h2 className="mb-1 font-semibold text-xl tracking-tight">
Project Wallet
</h2>
@@ -292,6 +301,24 @@ export function ProjectWalletSectionUI(props: {
thirdweb API
</UnderlineLink>
</p>
{props.layout === "row" && (
<div className="mt-4 hidden lg:block">
<Button
variant="outline"
asChild
className="rounded-lg gap-2 bg-card"
size="sm"
>
<Link
href="https://portal.thirdweb.com/reference"
target="_blank"
>
View API Reference
<ArrowUpRightIcon className="size-4" />
</Link>
</Button>
</div>
)}
</div>
{props.projectWallet ? (
@@ -318,6 +345,7 @@ export function ProjectWalletSection(props: {
teamSlug: string;
projectWallet: ProjectWalletSummary | undefined;
client: ThirdwebClient;
layout: "row" | "column";
}) {
return (
<ProjectWalletSectionUI
@@ -326,6 +354,7 @@ export function ProjectWalletSection(props: {
client={props.client}
projectWallet={props.projectWallet}
getProjectServerWallets={listProjectServerWallets}
layout={props.layout}
/>
);
}
@@ -26,6 +26,7 @@ export function DeployViaCLIOrImportCard(props: {
</div>
</div>
<ImportModal
allowedContractType="non-token"
client={props.client}
isOpen={importModalOpen}
onClose={() => {
@@ -1,10 +1,10 @@
import { Suspense } from "react";
import type { ThirdwebClient } from "thirdweb";
import { getSortedDeployedContracts } from "@/api/project/getSortedDeployedContracts";
import { getFilteredProjectContracts } from "@/api/project/getSortedDeployedContracts";
import { ClientOnly } from "@/components/blocks/client-only";
import { ContractTable } from "@/components/contract-components/tables/contract-table";
import { Spinner } from "@/components/ui/Spinner";
import { DeployViaCLIOrImportCard } from "../../../../../account/contracts/_components/DeployViaCLIOrImportCard";
import { DeployViaCLIOrImportCard } from "./DeployViaCLIOrImportCard";
export function DeployedContractsPage(props: {
teamId: string;
@@ -39,9 +39,9 @@ async function DeployedContractsPageAsync(props: {
teamSlug: string;
projectSlug: string;
}) {
const deployedContracts = await getSortedDeployedContracts({
const deployedContracts = await getFilteredProjectContracts({
authToken: props.authToken,
deploymentType: undefined,
type: "non-token-contracts",
projectId: props.projectId,
teamId: props.teamId,
});
@@ -18,6 +18,7 @@ export function ImportContractButton(props: {
return (
<>
<ImportModal
allowedContractType="non-token"
client={props.client}
isOpen={importModalOpen}
onClose={() => {
@@ -1,4 +1,5 @@
import { ArrowUpFromLineIcon } from "lucide-react";
import Link from "next/link";
import { redirect } from "next/navigation";
import { getAuthToken } from "@/api/auth-token";
import { getProject } from "@/api/project/projects";
@@ -39,111 +40,129 @@ export default async function Page(props: {
});
return (
<ProjectPage
header={{
client,
title: "Contracts",
icon: ContractIcon,
description: (
<>
Read, write, and deploy smart contracts on any EVM compatible
blockchain. <br className="max-sm:hidden" /> Deploy contracts from
templates, or build your own from scratch
</>
),
actions: {
primary: {
label: "Deploy Contract",
href: "/explore",
icon: <ArrowUpFromLineIcon className="size-3.5" />,
<div className="flex flex-col grow">
{/* banner */}
<div className="py-5 bg-blue-700 text-white font-medium">
<div className="container max-w-7xl">
<div>
<p>
ERC20, ERC721, ERC1155 token contracts are moved to the{" "}
<Link
href={`/team/${params.team_slug}/${params.project_slug}/tokens`}
className="underline underline-offset-4 decoration-dotted hover:decoration-solid"
>
Tokens
</Link>{" "}
section.
</p>
<p>Go to the Tokens section to view and deploy token contracts.</p>
</div>
</div>
</div>
<ProjectPage
header={{
client,
title: "Contracts",
icon: ContractIcon,
description: (
<>
Read, write, and deploy smart contracts on any EVM compatible
blockchain. <br className="max-sm:hidden" /> Deploy contracts from
templates, or build your own from scratch
</>
),
actions: {
primary: {
label: "Deploy Contract",
href: "/explore",
icon: <ArrowUpFromLineIcon className="size-3.5" />,
},
secondary: {
component: (
<ImportContractButton
teamId={team.id}
projectId={project.id}
projectSlug={params.project_slug}
teamSlug={params.team_slug}
client={client}
/>
),
},
},
secondary: {
component: (
<ImportContractButton
teamId={team.id}
projectId={project.id}
projectSlug={params.project_slug}
teamSlug={params.team_slug}
client={client}
/>
),
},
},
links: [
{
type: "docs",
href: "https://portal.thirdweb.com/contracts",
},
{
type: "playground",
href: "https://playground.thirdweb.com/contracts/read",
},
{
type: "api",
href: "https://api.thirdweb.com/reference#tag/contracts",
},
{
type: "webhooks",
href: `/team/${params.team_slug}/${params.project_slug}/webhooks/contracts`,
},
],
}}
footer={{
left: {
links: [
{
href: "https://portal.thirdweb.com/contracts/deploy/overview",
label: "Deployment Tools",
type: "docs",
href: "https://portal.thirdweb.com/contracts",
},
{
href: "https://portal.thirdweb.com/contracts/modular-contracts/overview",
label: "Modular Contracts",
type: "api",
href: "https://api.thirdweb.com/reference#tag/contracts",
},
{
href: "https://portal.thirdweb.com/contracts/explore/overview",
label: "Pre-built Contracts",
type: "webhooks",
href: `/team/${params.team_slug}/${params.project_slug}/webhooks/contracts`,
},
],
title: "Documentation",
},
center: {
links: [
{
href: "https://thirdweb.com/templates/hardhat-starter",
label: "Hardhat Starter",
},
{
href: "https://thirdweb.com/templates/forge-starter",
label: "Forge Starter",
},
],
title: "Templates",
},
right: {
links: [
{
href: "https://www.youtube.com/watch?v=cZt-CkzxrNM",
label:
"Everything you need to know about upgradeable smart contracts",
},
{
href: "https://www.youtube.com/watch?v=ZoOk41y4f_k",
label:
"Modular Contracts SDK: Build Core & Modules from Scratch (Advanced Guide)",
},
],
title: "Tutorials",
},
}}
>
<DeployedContractsPage
authToken={authToken}
client={client}
projectId={project.id}
projectSlug={params.project_slug}
teamId={team.id}
teamSlug={params.team_slug}
/>
</ProjectPage>
}}
footer={{
left: {
links: [
{
href: "https://portal.thirdweb.com/contracts/deploy/overview",
label: "Deployment Tools",
},
{
href: "https://portal.thirdweb.com/contracts/modular-contracts/overview",
label: "Modular Contracts",
},
{
href: "https://portal.thirdweb.com/contracts/explore/overview",
label: "Pre-built Contracts",
},
],
title: "Documentation",
},
center: {
links: [
{
href: "https://thirdweb.com/templates/hardhat-starter",
label: "Hardhat Starter",
},
{
href: "https://thirdweb.com/templates/forge-starter",
label: "Forge Starter",
},
],
title: "Templates",
},
right: {
links: [
{
href: "https://www.youtube.com/watch?v=cZt-CkzxrNM",
label:
"Everything you need to know about upgradeable smart contracts",
},
{
href: "https://www.youtube.com/watch?v=ZoOk41y4f_k",
label:
"Modular Contracts SDK: Build Core & Modules from Scratch (Advanced Guide)",
},
],
title: "Tutorials",
},
}}
>
<DeployedContractsPage
authToken={authToken}
client={client}
projectId={project.id}
projectSlug={params.project_slug}
teamId={team.id}
teamSlug={params.team_slug}
/>
</ProjectPage>
</div>
);
}
@@ -6,15 +6,10 @@ import {
ArrowRightIcon,
CheckIcon,
CircleAlertIcon,
DatabaseIcon,
InfinityIcon,
InfoIcon,
MoreHorizontalIcon,
PencilIcon,
PlusIcon,
ShieldCheckIcon,
Trash2Icon,
WalletIcon,
} from "lucide-react";
import Link from "next/link";
import { useState } from "react";
@@ -22,7 +17,6 @@ import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
import type { Team } from "@/api/team/get-team";
import { CheckoutButton } from "@/components/billing/billing";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { Badge, type BadgeProps } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
@@ -31,20 +25,10 @@ import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import {
Drawer,
DrawerContent,
DrawerDescription,
DrawerFooter,
DrawerHeader,
DrawerTitle,
DrawerTrigger,
} from "@/components/ui/drawer";
import {
DropdownMenu,
DropdownMenuContent,
@@ -72,7 +56,6 @@ import {
TableRow,
} from "@/components/ui/table";
import { Textarea } from "@/components/ui/textarea";
import { useIsMobile } from "@/hooks/use-mobile";
import {
type DeleteCloudHostedEngineParams,
deleteCloudHostedEngine,
@@ -84,7 +67,6 @@ import {
} from "@/hooks/useEngine";
import { EngineIcon } from "@/icons/EngineIcon";
import { useDashboardRouter } from "@/lib/DashboardRouter";
import { cn } from "@/lib/utils";
type DeletedCloudHostedEngine = (
params: DeleteCloudHostedEngineParams,
@@ -126,96 +108,6 @@ export function EngineInstancesTable(props: {
);
}
export function DedicatedEngineSubscriptionButton(props: { team: Team }) {
const [open, setOpen] = useState(false);
const isMobile = useIsMobile();
const trigger = (
<Button className="gap-2 rounded-full" size="sm">
<PlusIcon className="size-3.5" />
Deploy Dedicated Engine
</Button>
);
const title = (
<div className="flex flex-row items-center gap-2">
<div className="grid size-8 place-items-center rounded-full bg-warning-text">
<DatabaseIcon className="size-4 text-white" />
</div>
<span className="font-semibold text-lg">Dedicated Engine</span>
</div>
);
const content = (
<div className="flex flex-col items-start justify-center gap-4 px-4 md:px-0">
<ul className="flex flex-col gap-1 self-start text-muted-foreground text-sm">
<li className="flex items-center gap-2">
<ShieldCheckIcon className="size-4 text-foreground" />
Isolated environment
</li>
<li className="flex items-center gap-2">
<WalletIcon className="size-4 text-foreground" />
EOA or Smart Wallets
</li>
<li className="flex items-center gap-2">
<InfinityIcon className="size-4 text-foreground" />
No transaction usage limits or charges
</li>
</ul>
</div>
);
if (isMobile) {
return (
<Drawer onOpenChange={setOpen} open={open}>
<DrawerTrigger asChild>{trigger}</DrawerTrigger>
<DrawerContent className="max-h-[90vh]">
<DrawerHeader>
<DrawerTitle>{title}</DrawerTitle>
<DrawerDescription>
Instantly deploy a dedicated engine instance for your team.
</DrawerDescription>
</DrawerHeader>
{content}
<DrawerFooter>
<CheckoutButton
billingStatus={props.team.billingStatus}
sku="product:engine_standard"
teamSlug={props.team.slug}
>
Deploy Now · $299 / month
</CheckoutButton>
</DrawerFooter>
</DrawerContent>
</Drawer>
);
}
return (
<Dialog onOpenChange={setOpen} open={open}>
<DialogTrigger asChild>{trigger}</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>{title}</DialogTitle>
<DialogDescription>
Instantly deploy a dedicated engine instance for your team.
</DialogDescription>
</DialogHeader>
{content}
<DialogFooter>
<CheckoutButton
billingStatus={props.team.billingStatus}
sku="product:engine_standard"
teamSlug={props.team.slug}
>
Deploy Now · $299 / month
</CheckoutButton>
</DialogFooter>
</DialogContent>
</Dialog>
);
}
export function EngineInstancesTableUI(props: {
instances: EngineInstance[];
engineLinkPrefix: string;
@@ -896,10 +788,6 @@ function DeleteEngineSubscriptionModalContent(props: {
}
function EmptyEngineState(props: { team: Team; projectSlug: string }) {
const [selectedTab, setSelectedTab] = useState<
"self-hosted" | "cloud-hosted"
>("cloud-hosted");
return (
<div className="flex flex-col items-center justify-center rounded-lg border bg-card px-4 py-12 lg:px-6">
<div className="mb-4 rounded-full border bg-card p-2">
@@ -908,75 +796,34 @@ function EmptyEngineState(props: { team: Team; projectSlug: string }) {
<h2 className="mb-1 font-semibold text-xl">No Engine instances found</h2>
<p className="mb-6 max-w-lg text-center text-muted-foreground text-sm">
Get started with one of the options below to add your first engine
instance.
Import an Engine instance running on your own infrastructure.
</p>
<div className="w-full max-w-md">
<div className="mx-auto max-w-sm">
<div className="grid grid-cols-2 gap-1 rounded-lg border p-1">
<Button
className={cn(selectedTab === "cloud-hosted" && "bg-accent")}
onClick={() => setSelectedTab("cloud-hosted")}
size="sm"
variant="ghost"
>
Managed
</Button>
<div className="h-5" />
<div className="flex flex-col text-center">
<h3 className="mb-0.5 font-semibold text-base">Self-Hosted Engine</h3>
<p className="text-muted-foreground text-sm">
Add engine instance running on your own infrastructure.
</p>
<div className="h-4" />
<div className="mt-auto">
<Button
className={cn(selectedTab === "self-hosted" && "bg-accent")}
onClick={() => setSelectedTab("self-hosted")}
asChild
className="w-full gap-2"
size="sm"
variant="ghost"
variant="default"
>
Self-hosted
<Link
href={`/team/${props.team.slug}/${props.projectSlug}/engine`}
>
Import self-hosted Engine
<ArrowRightIcon size={16} />
</Link>
</Button>
</div>
</div>
<div className="h-5" />
{selectedTab === "self-hosted" && (
<div className="flex flex-col text-center">
<h3 className="mb-0.5 font-semibold text-base">
Self-Hosted Engine
</h3>
<p className="text-muted-foreground text-sm">
Add engine instance running on your own infrastructure.
</p>
<div className="h-4" />
<div className="mt-auto">
<Button
asChild
className="w-full gap-2"
size="sm"
variant="default"
>
<Link
href={`/team/${props.team.slug}/${props.projectSlug}/engine`}
>
Import self-hosted Engine
<ArrowRightIcon size={16} />
</Link>
</Button>
</div>
</div>
)}
{selectedTab === "cloud-hosted" && (
<div className="flex flex-col text-center">
<h3 className="mb-0.5 font-semibold text-base">Managed Engine</h3>
<p className="text-muted-foreground text-sm">
Deploy a managed engine instance to your team. <br /> We recommend
using Engine Cloud in most cases.
</p>
<div className="h-4" />
<div className="mt-auto">
<DedicatedEngineSubscriptionButton team={props.team} />
</div>
</div>
)}
</div>
<div className="mt-4 text-center text-muted-foreground text-sm">
@@ -7,7 +7,6 @@ import { getClientThirdwebClient } from "@/constants/thirdweb-client.client";
import { loginRedirect } from "@/utils/redirects";
import { getEngineInstances } from "../_utils/getEngineInstances";
import { ImportEngineButton } from "./import/import-engine-dialog";
import { DedicatedEngineSubscriptionButton } from "./overview/engine-instances-table";
import { EngineInstancesList } from "./overview/engine-list";
export default async function Page(props: {
@@ -61,9 +60,6 @@ export default async function Page(props: {
title: "Engine",
description: "Manage your deployed Engine instances.",
actions: {
primary: {
component: <DedicatedEngineSubscriptionButton team={team} />,
},
secondary: {
component: (
<ImportEngineButton
@@ -13,7 +13,7 @@ import {
TableHeader,
TableRow,
} from "@/components/ui/table";
import { CardHeading } from "../../payments/components/common";
import { CardHeading } from "../../../payments/components/common";
export function TopInsightChainsTable(props: {
data: InsightChainStats[];
@@ -13,7 +13,7 @@ import {
TableHeader,
TableRow,
} from "@/components/ui/table";
import { CardHeading } from "../../payments/components/common";
import { CardHeading } from "../../../payments/components/common";
export function TopInsightEndpointsTable(props: {
data: InsightEndpointStats[];
@@ -0,0 +1,30 @@
export function InsightFTUX(_props: { clientId: string }) {
return (
<div className="rounded-lg border bg-card">
<div className="border-b px-4 py-4 lg:px-6">
<h2 className="font-semibold text-xl tracking-tight">Indexer</h2>
</div>
<div className="px-4 py-6 lg:p-6">
<p className="text-muted-foreground text-sm">
Indexed data is automatically used when using the thirdweb SDKs and
API where appropriate.
</p>
<p className="text-muted-foreground text-sm">
Once you have integrated thirdweb into your application you will start
seeing usage patterns here.
</p>
</div>
<div className="flex flex-col gap-3 border-t p-4 lg:flex-row lg:items-center lg:justify-between lg:p-6">
<div className="flex gap-3"></div>
<p className="flex items-center gap-2 rounded-full border bg-background px-3.5 py-1.5 text-sm">
<span className="!pointer-events-auto relative flex size-2">
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-sky-400 opacity-75" />
<span className="relative inline-flex size-2 rounded-full bg-primary" />
</span>
Waiting for integration
</p>
</div>
</div>
);
}
@@ -0,0 +1,84 @@
import { redirect } from "next/navigation";
import { ResponsiveSearchParamsProvider } from "responsive-rsc";
import { getAuthToken } from "@/api/auth-token";
import { getProject } from "@/api/project/projects";
import { ProjectPage } from "@/components/blocks/project-page/project-page";
import { getClientThirdwebClient } from "@/constants/thirdweb-client.client";
import { InsightIcon } from "@/icons/InsightIcon";
import { getFiltersFromSearchParams } from "@/lib/time";
import { loginRedirect } from "@/utils/redirects";
import { InsightAnalytics } from "./components/InsightAnalytics";
export default async function Page(props: {
params: Promise<{
team_slug: string;
project_slug: string;
}>;
searchParams: Promise<{
from?: string | undefined | string[];
to?: string | undefined | string[];
interval?: string | undefined | string[];
}>;
}) {
const [params, authToken] = await Promise.all([props.params, getAuthToken()]);
const project = await getProject(params.team_slug, params.project_slug);
if (!authToken) {
loginRedirect(
`/team/${params.team_slug}/${params.project_slug}/gateway/indexer`,
);
}
if (!project) {
redirect(`/team/${params.team_slug}`);
}
const searchParams = await props.searchParams;
const { range, interval } = getFiltersFromSearchParams({
defaultRange: "last-30",
from: searchParams.from,
interval: searchParams.interval,
to: searchParams.to,
});
const client = getClientThirdwebClient({
jwt: authToken,
teamId: project.teamId,
});
return (
<ProjectPage
header={{
client,
icon: InsightIcon,
title: "Indexer",
description:
"Indexed data for EVM chains, available automatically via the thirdweb SDKs and API.",
actions: null,
links: [
{
type: "docs",
href: "https://portal.thirdweb.com/contracts/events",
},
{
type: "api",
href: "https://portal.thirdweb.com/reference",
},
],
}}
>
<ResponsiveSearchParamsProvider value={searchParams}>
<InsightAnalytics
client={client}
interval={interval}
projectClientId={project.publishableKey}
projectId={project.id}
range={range}
teamId={project.teamId}
authToken={authToken}
/>
</ResponsiveSearchParamsProvider>
</ProjectPage>
);
}
@@ -15,7 +15,7 @@ import {
TableHeader,
TableRow,
} from "@/components/ui/table";
import { CardHeading } from "../../payments/components/common";
import { CardHeading } from "../../../payments/components/common";
export function TopRPCMethodsTable(props: {
data: RpcMethodStats[];
@@ -1,7 +1,7 @@
import { CodeServer } from "@/components/ui/code/code.server";
import { isProd } from "@/constants/env-utils";
import { ClientIDSection } from "../../components/ProjectFTUX/ClientIDSection";
import { WaitingForIntegrationCard } from "../../components/WaitingForIntegrationCard/WaitingForIntegrationCard";
import { ClientIDSection } from "../../../components/ProjectFTUX/ClientIDSection";
import { WaitingForIntegrationCard } from "../../../components/WaitingForIntegrationCard/WaitingForIntegrationCard";
export function RpcFTUX(props: { clientId: string }) {
return (
@@ -25,7 +25,9 @@ export default async function Page(props: {
const project = await getProject(params.team_slug, params.project_slug);
if (!authToken) {
loginRedirect(`/team/${params.team_slug}/${params.project_slug}/rpc`);
loginRedirect(
`/team/${params.team_slug}/${params.project_slug}/gateway/rpc`,
);
}
if (!project) {
@@ -1,74 +0,0 @@
import { CodeServer } from "@/components/ui/code/code.server";
import { isProd } from "@/constants/env-utils";
import { ClientIDSection } from "../../components/ProjectFTUX/ClientIDSection";
import { WaitingForIntegrationCard } from "../../components/WaitingForIntegrationCard/WaitingForIntegrationCard";
export function InsightFTUX(props: { clientId: string }) {
return (
<WaitingForIntegrationCard
codeTabs={[
{
code: (
<CodeServer
className="bg-background"
code={jsCode(props.clientId)}
lang="ts"
/>
),
label: "JavaScript",
},
{
code: (
<CodeServer
className="bg-background"
code={pythonCode(props.clientId)}
lang="python"
/>
),
label: "Python",
},
{
code: (
<CodeServer
className="bg-background"
code={curlCode(props.clientId)}
lang="bash"
/>
),
label: "Curl",
},
]}
ctas={[
{
href: "https://portal.thirdweb.com/insight",
label: "View Docs",
},
]}
title="Integrate Insight"
>
<ClientIDSection clientId={props.clientId} />
<div className="h-4" />
</WaitingForIntegrationCard>
);
}
const twDomain = isProd ? "thirdweb" : "thirdweb-dev";
const jsCode = (clientId: string) => `\
// Example: Get latest 10 transactions on Ethereum
const res = await fetch("https://insight.${twDomain}.com/v1/transactions?chain=1&limit=10&clientId=${clientId}");
const data = await res.json();
`;
const curlCode = (clientId: string) => `\
# Example: Get latest 10 transactions on Ethereum
curl -X GET "https://insight.${twDomain}.com/v1/transactions?chain=1&limit=10&clientId=${clientId}"
`;
const pythonCode = (clientId: string) => `\
# Example: Get latest 10 transactions on Ethereum
import requests
response = requests.get("https://insight.${twDomain}.com/v1/transactions?chain=1&limit=10&clientId=${clientId}")
data = response.json()
`;
@@ -1,113 +0,0 @@
import { ArrowUpRightIcon } from "lucide-react";
import { redirect } from "next/navigation";
import { ResponsiveSearchParamsProvider } from "responsive-rsc";
import { getAuthToken } from "@/api/auth-token";
import { getProject } from "@/api/project/projects";
import { ProjectPage } from "@/components/blocks/project-page/project-page";
import { getClientThirdwebClient } from "@/constants/thirdweb-client.client";
import { InsightIcon } from "@/icons/InsightIcon";
import { getFiltersFromSearchParams } from "@/lib/time";
import { loginRedirect } from "@/utils/redirects";
import { InsightAnalytics } from "./components/InsightAnalytics";
export default async function Page(props: {
params: Promise<{
team_slug: string;
project_slug: string;
}>;
searchParams: Promise<{
from?: string | undefined | string[];
to?: string | undefined | string[];
interval?: string | undefined | string[];
}>;
}) {
const [params, authToken] = await Promise.all([props.params, getAuthToken()]);
const project = await getProject(params.team_slug, params.project_slug);
if (!authToken) {
loginRedirect(`/team/${params.team_slug}/${params.project_slug}/insight`);
}
if (!project) {
redirect(`/team/${params.team_slug}`);
}
const searchParams = await props.searchParams;
const { range, interval } = getFiltersFromSearchParams({
defaultRange: "last-30",
from: searchParams.from,
interval: searchParams.interval,
to: searchParams.to,
});
const client = getClientThirdwebClient({
jwt: authToken,
teamId: project.teamId,
});
return (
<ProjectPage
header={{
client,
icon: InsightIcon,
title: "Insight",
description:
"APIs to retrieve blockchain data from any EVM chain, enrich it with metadata, and transform it using custom logic.",
actions: null,
links: [
{
type: "docs",
href: "https://portal.thirdweb.com/insight",
},
{
type: "api",
href: "https://insight-api.thirdweb.com/reference",
},
{
type: "webhooks",
href: `/team/${params.team_slug}/${params.project_slug}/webhooks/contracts`,
},
],
}}
>
<ResponsiveSearchParamsProvider value={searchParams}>
<div>
<InsightAnalytics
client={client}
interval={interval}
projectClientId={project.publishableKey}
projectId={project.id}
range={range}
teamId={project.teamId}
authToken={authToken}
/>
<div className="h-10" />
<div className="relative overflow-hidden rounded-lg border-2 border-green-500/20 bg-gradient-to-br from-card/80 to-card/50 p-4 shadow-[inset_0_1px_2px_0_rgba(0,0,0,0.02)]">
<div className="absolute inset-0 bg-gradient-to-br from-green-500/5 to-transparent" />
<div className="relative flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
<div className="flex flex-col gap-1">
<h3 className="font-medium text-lg">
Get Started with Insight
</h3>
<p className="text-muted-foreground text-sm">
A cross-chain API for historic blockchain data.
</p>
</div>
<a
className="inline-flex items-center gap-2 rounded-md bg-green-600 px-4 py-2 font-medium text-sm text-white transition-all hover:bg-green-600/90 hover:shadow-sm"
href="https://portal.thirdweb.com/insight"
rel="noopener noreferrer"
target="_blank"
>
Learn More
<ArrowUpRightIcon className="size-4" />
</a>
</div>
</div>
</div>
</ResponsiveSearchParamsProvider>
</ProjectPage>
);
}
@@ -1,6 +1,7 @@
import { redirect } from "next/navigation";
import { getValidAccount } from "@/api/account/get-account";
import { getAuthToken, getAuthTokenWalletAddress } from "@/api/auth-token";
import { getFilteredProjectContracts } from "@/api/project/getSortedDeployedContracts";
import { getProject, getProjects } from "@/api/project/projects";
import { getTeamBySlug, getTeams } from "@/api/team/get-team";
import { CustomChatButton } from "@/components/chat/CustomChatButton";
@@ -12,6 +13,7 @@ import { TeamHeaderLoggedIn } from "../../../components/TeamHeader/team-header-l
import { StaffModeNotice } from "../../(team)/_components/StaffModeNotice";
import { ProjectSidebarLayout } from "./components/ProjectSidebarLayout";
import { SaveLastUsedProject } from "./components/SaveLastUsedProject";
import { getEngineInstances } from "./engine/_utils/getEngineInstances";
export default async function ProjectLayout(props: {
children: React.ReactNode;
@@ -20,15 +22,34 @@ export default async function ProjectLayout(props: {
}) {
const params = await props.params;
const [accountAddress, teams, account, authToken, team, project] =
await Promise.all([
getAuthTokenWalletAddress(),
getTeams(),
getValidAccount(`/team/${params.team_slug}/${params.project_slug}`),
getAuthToken(),
getTeamBySlug(params.team_slug),
getProject(params.team_slug, params.project_slug),
]);
const [
accountAddress,
teams,
account,
authToken,
team,
project,
hasLegacyDedicatedEngines,
] = await Promise.all([
getAuthTokenWalletAddress(),
getTeams(),
getValidAccount(`/team/${params.team_slug}/${params.project_slug}`),
getAuthToken(),
getTeamBySlug(params.team_slug),
getProject(params.team_slug, params.project_slug),
(async () => {
const authToken = await getAuthToken();
if (!authToken) {
return false;
}
const instances = await getEngineInstances({
authToken,
teamIdOrSlug: params.team_slug,
});
// if there are any engine instances, return true, otherwise false
return !!instances?.data?.length;
})(),
]);
if (!teams || !accountAddress || !authToken) {
redirect("/login");
@@ -58,6 +79,15 @@ export default async function ProjectLayout(props: {
const isStaffMode = !teams.some((t) => t.slug === team.slug);
const nonTokenContracts = await getFilteredProjectContracts({
authToken,
projectId: project.id,
teamId: team.id,
type: "non-token-contracts",
}).catch(() => []);
const showContracts = nonTokenContracts.length > 0;
return (
<SidebarProvider>
<div className="flex h-dvh min-w-0 grow flex-col">
@@ -73,7 +103,11 @@ export default async function ProjectLayout(props: {
teamsAndProjects={teamsAndProjects}
/>
</div>
<ProjectSidebarLayout layoutPath={layoutPath}>
<ProjectSidebarLayout
layoutPath={layoutPath}
hasEngines={hasLegacyDedicatedEngines}
showContracts={showContracts}
>
{props.children}
</ProjectSidebarLayout>
</div>
@@ -54,12 +54,12 @@ export function ProjectHighlightsCard(props: {
emptyContent: (
<EmptyStateContent
description="Your app hasn't collected any fees yet."
link={`/team/${teamSlug}/${projectSlug}/payments/settings`}
link={`/team/${teamSlug}/${projectSlug}/bridge/configuration`}
metric="Fees"
/>
),
isCurrency: true,
label: "Fee Revenue",
label: "Bridge Fee Revenue",
},
} as const;

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