Compare commits

..
Author SHA1 Message Date
Joaquim VergesGitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
1665e4d534 Version Packages (#7568)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-07-08 17:18:24 -07:00
Manan TankandGitHub 692cedf921 [TOOL-4987] SDK: Restore CSS resets, remove biome rules for sorting attributes and properties (#7567) 2025-07-08 15:47:22 -07:00
e1f4901b80 [SDK] Remove page and set up redirect (#7565)
Co-authored-by: Cursor Agent <[email protected]>
2025-07-08 12:06:36 -07:00
MananTank 028f871d40 [TOOL-4972] Dashboard: Add field to configure admins in NFT asset creation (#7542)
<!--

## 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 enhancing the NFT creation process by adding the ability to set admin addresses and refining various components for better user experience and validation.

### Detailed summary
- Added `"set-admins"` step to the NFT asset type.
- Introduced `AdminAddressesFieldset` for managing admin addresses.
- Updated forms to include admin validation.
- Improved UI components with consistent styling.
- Enhanced error handling during admin setting.

> ✨ 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 the ability to specify multiple admin wallet addresses when creating an NFT collection, with dynamic form fields and validation.
  * NFT launch workflow now includes a step to assign admin roles to specified addresses.
  * NFT collection info and launch dialogs now display the list of assigned admins.

* **Improvements**
  * The NFT and token creation forms now automatically set the default blockchain network based on the active wallet chain.
  * NFT upload form includes default values for price and supply fields.
  * Visual spacing adjustments in social URL and token launch dialogs for improved UI consistency.

* **Bug Fixes**
  * Ensured at least one admin address is required when creating an NFT collection.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-08 15:37:47 +00:00
2821b4eeb7 Make contract webhook addresses mandatory (#7544)
Co-authored-by: Cursor Agent <[email protected]>
2025-07-08 15:20:49 +02:00
Joaquim VergesGitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
adfa56b2a2 Version Packages (#7555)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-07-07 18:54:19 -07:00
YashandGitHub 415ad90986 feat: add T-Rex testnet chain (#7540) 2025-07-07 18:17:02 -07:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
afcb7bba84 chore(deps): bump zod from 3.25.71 to 3.25.75 (#7552)
Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-07 18:15:59 -07:00
Joaquim VergesandGitHub 10c3b6f27f [SDK] Respect fee payer in token payment selection quotes (#7556) 2025-07-07 18:14:50 -07:00
Joaquim VergesandGitHub c64446306e [SDK] Use new BuyWidget in wallet details modal (#7551) 2025-07-07 17:18:28 -07:00
saminaandGitHub 2136c7ba4a [Docs] Wallets Overview (#7543) 2025-07-07 16:21:33 -07:00
MananTank 67c3d861fe [TOOL-4986] Dashboard: Fix ERC20 asset page crash (#7541)
<!--

## 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 error handling by adding `try-catch` blocks around asynchronous calls in the `getContractCreator` function. This change ensures that if an error occurs during the execution of `owner` or `getRoleMember`, the function will return `null` instead of throwing an unhandled error.

### Detailed summary
- Wrapped the calls to `owner` and `getRoleMember` in a `try-catch` block.
- Changed the calls to `owner` and `getRoleMember` to use `await` for proper asynchronous handling.
- Ensured that if an error occurs, the function returns `null` instead of failing silently.

> ✨ 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 error handling when retrieving contract creator information, ensuring failures no longer cause crashes and instead return a safe null value.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-07 14:15:01 +00:00
d2c12cc6ef Create session key guide and update sidebar (#7538)
Co-authored-by: Cursor Agent <[email protected]>
2025-07-07 03:30:38 -07:00
Joaquim VergesGitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
c1b2e10f34 Version Packages (#7539)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-07-07 03:29:31 -07:00
Joaquim VergesandGitHub f48d8006bc [SDK] Optimize payment token fetching in payment widgets (#7536) 2025-07-07 02:50:25 -07:00
Joaquim VergesandGitHub 20e323d666 [Dashboard] Fix smart account options validation for non-EIP4337 execution modes (#7537) 2025-07-07 01:20:11 -07:00
Joaquim VergesGitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
0ec6142bf8 Version Packages (#7535)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-07-06 19:21:21 -07:00
0ea25bb9ae [SDK] Update walletconnect provider to universal (#7532)
Co-authored-by: Cursor Agent <[email protected]>
2025-07-06 16:39:46 -07:00
Joaquim VergesGitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
67293fc269 Version Packages (#7534)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-07-06 01:17:37 -07:00
Joaquim VergesandGitHub cb77544a46 [SDK] Respect feePayer property in Payment widgets (#7533) 2025-07-06 01:08:46 -07:00
770e7b24da Enhance transaction details with decoded tab (#7531)
Co-authored-by: Cursor Agent <[email protected]>
2025-07-06 00:58:14 -07:00
ff9df497c9 Update branding from Engine Cloud to Transactions (#7516)
Co-authored-by: Cursor Agent <[email protected]>
2025-07-05 13:37:17 -07:00
Joaquim VergesGitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2d6c245b5e Version Packages (#7511)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-07-06 07:21:32 +12:00
37b3d7fa07 Add activity log section to transaction page (#7529)
Co-authored-by: Cursor Agent <[email protected]>
2025-07-05 01:01:24 -07:00
YashandGitHub 8def4f33f6 Stylus CLI updates (#7502) 2025-07-05 07:47:29 +12:00
saminaandGitHub d92ab6fabe [Docs] Portal Images (#7526) 2025-07-03 21:39:03 -07:00
Joaquim VergesandGitHub 198cfd0f95 [Playground] Add TokenSelector component to Pay embed configuration (#7527) 2025-07-03 21:38:44 -07:00
jnsdls 9fba016cff Remove Plausible analytics integration (#7525)
# Remove Plausible Analytics Integration

This PR removes the Plausible Analytics integration from our applications:

- Removed `next-plausible` dependency from the dashboard app
- Removed Plausible script tags from dashboard, playground-web, and portal layouts
- Removed the Plausible proxy configuration from the Next.js config
- Updated the webpack configuration to work without the Plausible proxy wrapper

<!-- start pr-codex -->

---

## PR-Codex overview
This PR removes the usage of `next-plausible` from various files and updates dependencies accordingly. It also cleans up the code by removing unnecessary `<head>` elements and script imports across multiple layout files.

### Detailed summary
- Removed `next-plausible` dependency from `apps/dashboard/package.json`.
- Deleted `<Script>` components related to Plausible from `layout.tsx` files in `apps/playground-web`, `apps/dashboard`, and `apps/portal`.
- Cleaned up `next.config.ts` by removing `withPlausibleProxy`.
- Updated `pnpm-lock.yaml` to reflect the removal of `next-plausible`.

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

* **Chores**
  * Removed Plausible analytics integration and related dependencies from the dashboard.
  * Removed external script loading from the layouts of the playground-web and portal apps.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-04 02:35:21 +00:00
Jonas DanielsandGitHub 4b8badba2e upgrade dependencies (#7524) 2025-07-03 19:22:27 -07:00
Joaquim VergesandGitHub 341325f844 [SDK] Show all tokens in payment selection regardless of balance (#7523) 2025-07-03 19:16:17 -07:00
7500d8723f [Dashboard] Fix validation for smart account options (#7522)
Co-authored-by: Cursor Agent <[email protected]>
2025-07-03 18:35:31 -07:00
Jonas DanielsandGitHub fa45dea07d upgrade biome to 2.0.6 (#7517) 2025-07-03 18:32:04 -07:00
Joaquim VergesandGitHub f94226ad7e [SDK] update wallets, add sh.frame (#7521) 2025-07-03 17:43:39 -07:00
Jonas DanielsandGitHub 0e159f776c [Dashboard] Add chain infrastructure deployment and management (#7456) 2025-07-03 17:32:14 -07:00
jnsdls f1a965b046 Enable engineCloud service in project creation modal (#7520)
### TL;DR

Enabled Engine Cloud service in the project creation modal.

### What changed?

Removed the filter that was excluding the "engineCloud" service from the list of available project services in the create project modal. This change allows users to select Engine Cloud when creating a new project.

### How to test?

1. Open the dashboard application
2. Click on "Create Project" button
3. Verify that Engine Cloud appears as an available service option in the project creation modal

### Why make this change?

The Engine Cloud service was previously excluded with a TODO comment indicating it should be enabled once the API server was ready. This change suggests that the API server is now available, so Engine Cloud can be offered as a service option during project creation.

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

## Summary by CodeRabbit

* **Bug Fixes**
  * Updated project service selection to allow "engineCloud" as an available option.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-03 23:04:16 +00:00
MananTank 9f88b5c2be [TOOL-4951] Dashboard: Show Plan upsell on storage error in asset creation modal (#7512)
<!--

## 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 introduces the `teamPlan` property to various components related to NFT and token creation, allowing the application to handle billing plans effectively. It also adds a new component for handling storage limit errors and integrates upsell reporting for plan upgrades.

### Detailed summary
- Added `teamPlan` prop to components: `CreateNFTPage`, `CreateTokenAssetPage`, `CreateNFTPageUI`, `CreateTokenAssetPageUI`, `LaunchNFT`, `LaunchTokenStatus`.
- Introduced `StorageErrorPlanUpsell` component for storage limit handling.
- Implemented upsell reporting functions: `reportUpsellShown`, `reportUpsellClicked`.
- Updated `MultiStepStatus` to use a render function for error messages.
- Changed error message type from `React.ReactNode` to `string` in `MultiStepStatus`.

> ✨ 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 tailored upsell experience when users on a free plan hit the storage limit during token or NFT creation, including a new prompt to upgrade and real-time detection of plan changes.
  * Added analytics tracking for upsell prompts and upgrade link clicks.

* **Enhancements**
  * Error messages in multi-step workflows can now be customized, allowing for more informative and actionable error displays.
  * Components involved in token and NFT creation now accept and utilize the team's billing plan to enable conditional UI and logic.

* **Storybook**
  * Updated stories to reflect storage limit scenarios and demonstrate the new upsell flow for free plan users.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-02 20:39:36 +00:00
MananTank 7e73c344f5 Dashboard: asset page header: hide zero-address, fix icon (#7514)
<!--

## 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 handling of wallet addresses and token fetching in the application. It enhances the rendering of wallet address components and modifies the fetching logic for token information, including error handling.

### Detailed summary
- Updated rendering of the wallet address in `apps/dashboard/src/@/components/blocks/wallet-address.tsx` and `apps/nebula/src/@/components/blocks/wallet-address.tsx` to include `props.className` and added a `text-xs` class in the latter.
- Enhanced the fetch request in `apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/public-pages/erc20/_utils/fetch-coin-info.ts` by moving `clientId` to headers and added error logging for failed requests.
- Modified the condition for rendering contract creator in `apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/public-pages/erc20/_components/ContractHeader.tsx` to check if `contractCreator` is not equal to `ZERO_ADDRESS`.

> ✨ 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**
  * Improved wallet address display to support external styling and smaller text size for the zero address.

* **Bug Fixes**
  * Updated contract header to ensure the contract creator badge is not shown for zero addresses.
  * Enhanced error logging for token info fetching to aid in troubleshooting.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-02 20:15:47 +00:00
iuwqyir 4bd4efce4b add rpc tab with analytics (#7489)
# [Dashboard] Feature: Add RPC Edge analytics page

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on enhancing the RPC (Remote Procedure Call) analytics feature in the dashboard. It introduces new components, updates existing ones, and adds functionality for fetching and displaying RPC usage data and statistics.

### Detailed summary
- Added `RpcMethodStats` and `RpcUsageTypeStats` interfaces in `analytics.ts`.
- Introduced `getRpcUsageByType` function to fetch RPC usage data.
- Updated `RpcMethodBarChartCardUI` to use new data structure.
- Added new `RPC` link in `ProjectSidebarLayout`.
- Created `RpcAnalyticsFilter` for filtering RPC analytics.
- Implemented `RequestsGraph` to visualize RPC requests over time.
- Developed `TopRPCMethodsTable` to display top EVM methods called.
- Added `RpcFTUX` component for user onboarding in RPC section.
- Updated layout files to integrate new components and improve styling.

> ✨ 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 dedicated "RPC" section in the project sidebar for easy access to RPC analytics.
  * Added a comprehensive RPC analytics dashboard, including:
    * Interactive requests graph and top RPC methods table.
    * Date range and interval filtering for analytics.
    * First-time user experience (FTUX) with ready-to-use code examples and documentation links.
  * Enhanced layout and navigation for the new RPC analytics section.

* **Style**
  * Improved UI spacing, sizing, and visual consistency for analytics components and placeholders.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-02 19:58:55 +00:00
MananTank 84f80437c1 [TOOL-4908] Dashboard: Fix 'Owner only' claim condition phase type condition (#7506)
<!--

## 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 adding a condition to check if the `maxClaimablePerWallet` is `"0"` and enhancing the validation of the `phase.snapshot` property within the `claim-conditions-form` component.

### Detailed summary
- Added a condition to check if `phase.maxClaimablePerWallet` is `"0"`.
- Ensured `phase.price` is `"0"`.
- Checked that `phase.snapshot` is not a string and has a length of 1.

> ✨ 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 claim condition handling to more accurately identify "creator" claim types based on updated criteria.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-02 19:11:32 +00:00
iuwqyir ac48b2409b add analytics to insight page (#7488)
<!-- start pr-codex -->

## PR-Codex overview
This PR introduces enhancements to the `Insight` feature in the application, including new analytics components, improved data fetching, and user interface updates for better user experience.

### Detailed summary
- Added `limit` parameter to analytics type definitions.
- Refactored imports in `insight-ftux.tsx`.
- Introduced `InsightAnalyticsFilter` component for filtering analytics data.
- Created `Layout` component with project validation and redirection.
- Added `InsightFooter` for footer links.
- Implemented `TopInsightChainsTable` and `TopInsightEndpointsTable` for displaying analytics data.
- Added `getInsightChainUsage`, `getInsightStatusCodeUsage`, and `getInsightEndpointUsage` API functions for fetching analytics.
- Enhanced `RequestsByStatusGraph` for visualizing status code requests.
- Updated `Page` component to handle authentication and data fetching more efficiently.

> ✨ 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 comprehensive analytics dashboard for project insights, including interactive charts and tables for usage, errors, top endpoints, and top chains.
  * Added filters for date range and interval selection, with preferences saved for future visits.
  * Enhanced visualization with a stacked bar chart for HTTP status codes and detailed tables for endpoints and chains.
  * Included a streamlined layout with improved navigation, footer resources, and a "Get Started with Insight" call-to-action.
  * Added new analytics data fetching capabilities covering usage by chain, status code, endpoint, and overall metrics.

* **Refactor**
  * Simplified the insights page by removing redundant logic and focusing on authenticated analytics display.

* **Bug Fixes**
  * Improved error handling during data fetching to ensure UI stability when analytics data is unavailable.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-02 19:01:44 +00:00
jnsdls bde53fcf34 Remove "starter" plan checks from team invitation logic (#7513)
<!-- start pr-codex -->

## PR-Codex overview
This PR focuses on refining the invitation feature within the onboarding and settings sections of the application. It simplifies the conditions for inviting team members based on the billing plan.

### Detailed summary
- Removed `"Invite Team Members"` from the pricing options in `pricing.tsx`.
- Updated the conditional rendering for the custom CTA in `InviteTeamMembers.tsx` to check only for the `"free"` billing plan.
- Simplified the `inviteEnabled` condition in `InviteSection.tsx` to exclude the `"starter"` plan.
- Adjusted the conditional check for displaying the bottom section in `InviteSection.tsx` to only check for the `"free"` plan.

> ✨ 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 team plan features by removing "Invite Team Members" from the "growth" plan feature list.

* **Bug Fixes**
  * Adjusted upgrade prompts and invite functionality so that upgrade options and invite restrictions now apply only to teams on the "free" plan, rather than both "free" and "starter" plans.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-02 18:51:42 +00:00
FirekeeperandGitHub 7437289140 Update import path in createSessionKey example (#7510) 2025-07-02 11:12:23 -07:00
Jonas DanielsandGitHub ca1fb84eba Properly type purchaseData as Record<string, unknown> (#7508) 2025-07-02 11:12:06 -07:00
jnsdls 087c29efdd [Dashboard] Rename RPC Edge to RPC and add redirect (#7509)
# Rename `/rpc-edge` to `/rpc`

This PR renames the `/rpc-edge` route to `/rpc` in the dashboard:

- Updates the route in `framer-rewrites.js`
- Adds a redirect from `/rpc-edge` to `/rpc` to maintain backward compatibility

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

## Summary by CodeRabbit

* **New Features**
  * Added a redirect from /rpc-edge to /rpc for dashboard requests.

* **Chores**
  * Updated references from /rpc-edge to /rpc in the scale category configuration.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-02 17:58:07 +00:00
Joaquim VergesGitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
9c2b0f7111 Version Packages (#7501)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-07-02 09:56:04 -07:00
FirekeeperandGitHub fd967ce8e3 Pass purchaseData prop to payment components (#7507) 2025-07-02 09:49:24 -07:00
Joaquim VergesandGitHub 7ab8808b50 [SDK] Handle very large numbers in BuyWidget component (#7503) 2025-07-02 04:18:20 -07:00
Yash094 ec75d9502c feat(chains): add Core testnet and mainnet chains (#7486)
<!--

## 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 introduces two new chain definitions for `Core` and `Core Testnet` to the `thirdweb` package, enhancing its blockchain support.

### Detailed summary
- Added `coreMainnet` definition in `core-mainnet.ts` with block explorer and native currency details.
- Added `coreTestnet` definition in `core-testnet.ts` with block explorer, native currency details, and testnet flag.
- Exported both `coreMainnet` and `coreTestnet` in `chains.ts`.

> ✨ 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 support for the Core mainnet and Core testnet blockchain networks.
  * Users can now interact with these new chains, including viewing block explorer links and using native currency details.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-02 11:03:11 +00:00
85c4ef10ce Added Somnia Testnet (#7474)
Co-authored-by: Joaquim Verges <[email protected]>
2025-07-02 03:20:23 -07:00
Manan TankandGitHub 50e01ce067 SDK: Restore border on embed components (#7487) 2025-07-02 03:19:22 -07:00
Joaquim VergesandGitHub f15f62feec [Porta] fix llm.txt generation (#7500) 2025-07-01 22:45:00 -07:00
Joaquim VergesandGitHub 2d1549bb8b [Portal] Remove unused import from sign-in methods page (#7499) 2025-07-01 22:17:58 -07:00
saminaandGitHub effe60b48f [Docs] Broken Engine Links (#7498)
Signed-off-by: samina <[email protected]>
2025-07-01 21:49:39 -07:00
Joaquim VergesandGitHub 2029869d9c [Dashboard] Add status filter to transactions table (#7497) 2025-07-01 16:03:08 -07:00
jnsdls 02f6f87d08 [Dashboard] Move RPC Edge to scale category and remove redirect (#7495)
### TL;DR

Reorganized navigation structure by adding a "scale category" and removing the RPC Edge redirect.

### What changed?

- Created a new "scale category" in the framer-rewrites.js file that includes:
  - `/rpc-edge`
  - `/insight`
  - `/storage`
- Moved `/storage` from its own category into the new "scale category"
- Moved `/insight` from its own category into the new "scale category"
- Removed the redirect from `/rpc-edge` to the portal URL in redirects.js

### How to test?

1. Verify that the `/rpc-edge`, `/insight`, and `/storage` routes are properly grouped under the "scale category"
2. Confirm that navigating to `/rpc-edge` no longer redirects to the portal URL
3. Check that the other redirects still function as expected

### Why make this change?

This change improves the organization of related services under a logical "scale category" grouping, making the navigation structure more intuitive. It also enables direct access to the RPC Edge page rather than redirecting users to the portal, providing a more seamless user experience.

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

## Summary by CodeRabbit

* **Refactor**
  * Reorganized and regrouped certain dashboard paths under a new "scale category" for improved clarity.

* **Chores**
  * Removed a redirect rule for the /rpc-edge path.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-01 20:38:46 +00:00
MananTank 1305527822 [TOOL-4900] Dashboard: Add ERC20 token page links in /routes (#7493)
<!--

## 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 components in the `routes-table`, `routelist-card`, and `routelist-row` files to improve structure and readability, while introducing new components like `TokenName` and `TokenInfo` for better token representation.

### Detailed summary
- Replaced `TableContainer` with a simpler structure in `routes-table.tsx`.
- Changed table header structure to improve readability.
- Refactored `routelist-card.tsx` to use `TokenName` for token display.
- Introduced `TokenInfo` component in `routelist-row.tsx` for consistent token representation.
- Removed inline token display logic in favor of new components.

> ✨ 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 clickable links to ERC20 token pages for improved navigation and clarity in token displays.
  * Introduced new helper components for consistent token information display.

* **Refactor**
  * Updated token display layouts to use flexbox for better alignment and readability.
  * Consolidated repeated token rendering logic into reusable components.

* **Style**
  * Simplified and removed custom styling from tables and cards for a cleaner appearance.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-01 20:15:55 +00:00
MananTank b882ad7807 Playground: Fix in-app-wallet ConnectEmbed prebuilt ui code snippet (#7492)
<!--

## 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 `page.tsx` file in the `in-app-wallet` directory to improve the formatting of the `wallets` configuration and the `App` function, ensuring consistent indentation and structure.

### Detailed summary
- Reformatted the `wallets` array for improved readability.
- Changed the indentation of the `auth` and `executionMode` properties.
- Updated the `App` function to have consistent spacing around the return statement.

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

* **Style**
  * Improved formatting and readability of code examples and component structure in the in-app wallet integration page. No changes to functionality.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-01 20:10:18 +00:00
MananTank 030c522816 Dashboard: Add missing asset creation successs tracking for nft (#7494)
<!--

## 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 enhancing the NFT launch functionality by adding a successful asset creation report feature. It imports a new reporting function and invokes it upon successful NFT launch, improving analytics tracking.

### Detailed summary
- Added import for `reportAssetCreationSuccessful` from `@/analytics/report`.
- Called `reportAssetCreationSuccessful` with parameters indicating asset type and contract type upon successful NFT launch.

> ✨ 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 success reporting step after completing the NFT launch process, providing improved feedback for successful NFT creation.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-01 20:00:45 +00:00
MananTank c356813852 Dashboard: Remove chakra provider from team/settings/notifications page (#7479)
<!--

## 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 notifications settings page by renaming components, updating the layout, and improving the user interface for managing notifications.

### Detailed summary
- Deleted `SettingsNotificationsPage` and replaced it with `Notifications`.
- Updated the main page layout to include a header and description for notifications.
- Enhanced the `Notifications` component with a new background and border styling.
- Added a separator line in the `Notifications` component.
- Removed unnecessary wrapper from `SettingSwitch` layout.

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

<!-- end pr-codex -->
2025-06-30 16:14:58 +00:00
MananTank 1c638edd80 Dashboard: Charts style tweaks, Remove unused chart component (#7478)
<!--

## 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 modifying the styling and layout of various components within the dashboard, enhancing user interface elements, and removing unused files.

### Detailed summary
- Deleted `CombinedStatBreakdownCard.tsx` and `CombinedStatBreakdownCard.stories.tsx`.
- Updated class names for better styling on `Link` and `Card` components.
- Removed `max-md:border-r-0` and `max-md:border-l-0` classes from several components.
- Adjusted `min-h` properties for `Card` components.
- Centered text in `EmptyStateContent`.
- Streamlined layout in various components for improved spacing.

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

<!-- end pr-codex -->
2025-06-30 16:14:58 +00:00
MananTank 2fd5b84505 [PRO-213] Dashboard: Empty chart state copy changes (#7477)
<!--

## 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 `EmptyStateCard` component across various files, including changes to its props, styling, and the metrics displayed. It simplifies the component's usage and modifies the metrics for clarity and relevance.

### Detailed summary
- Updated `EmptyStateCard` to remove unnecessary props in `RpcMethodBarChartCardUI.tsx`.
- Modified metric and link props in `EngineCloudBarChartCardUI.tsx`.
- Changed `EmptyStateCard` metrics in `EmptyStateCard.stories.tsx` and other files to be more relevant.
- Made `metric` prop optional in `EmptyStateCard.tsx`.
- Enhanced styling for `EmptyStateContent` in `EmptyStateCard.tsx`.
- Adjusted rendering logic for `EmptyStateContent` to improve clarity and user experience.

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

<!-- end pr-codex -->
2025-06-30 16:14:58 +00:00
MananTank 2bfc9b2eb2 [PRO-211, PRO-210] Dashboard: Copy changes (#7476)
<!--

## 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 making minor adjustments to text labels and descriptions in the application, enhancing consistency in naming and improving grammatical accuracy.

### Detailed summary
- In `apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/webhooks/layout.tsx`, changed `name` from "Contract" to "Contracts".
- In `apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/tokens/page.tsx`, updated the phrase from "Create and Manage tokens for your project" to "Create and manage tokens for your project".

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

* **Style**
  * Corrected capitalization in the Assets section header for improved readability.
  * Updated a navigation tab label from "Contract" to "Contracts" for clarity.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-06-30 16:14:57 +00:00
MananTank e9dc2190a1 [PRO-218] Dashboard: Add product rebrand banner (#7475)
<!--

## 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 `AnnouncementBanner` component to reflect a rebranding message and a new link to a blog post, replacing the previous announcement about the Nebula App.

### Detailed summary
- Updated `href` to point to "https://blog.thirdweb.com/the-fastest-way-to-build-web3-applications/"
- Changed `label` to "We have re-branded our Engine, Universal Bridge, and Connect products. Please read the full blog post for details on changes"
- Updated `trackingLabel` to "product-rebrand"

> ✨ 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 the announcement banner to highlight the rebranding of Engine, Universal Bridge, and Connect products, with a link to a detailed blog post.

* **Style**
  * Refreshed banner text and link to reflect the latest product updates.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-06-30 15:47:03 +00:00
Phillip HoandGitHub d0fa1b1e66 chore: Enable telegram support channel, add join link button (#7472) 2025-06-30 18:23:43 +08:00
Joaquim VergesandGitHub 85a5037f50 [Playground] Update product details from hoodie to concert ticket (#7473) 2025-06-29 18:59:11 -07:00
Joaquim VergesandGitHub fd67c5db58 [Dashboard] Add client ID header to embedded wallets fetch request (#7471) 2025-06-29 02:40:26 -07:00
Joaquim VergesGitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
ca65b43566 Version Packages (#7467)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-06-28 23:57:39 -07:00
Corey RiceandGitHub 1f72396d7f docs: Update imports in react v5 adaptor examples (#7469) 2025-06-29 10:25:27 +12:00
MananTank 08834f6385 Dashboard: Update team layout (#7464)
<!--

## 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 refactoring and updating the billing and settings-related components in the dashboard application, changing URLs from `/settings` to `/billing`, and enhancing the user interface for better clarity and usability.

### Detailed summary
- Deleted several unused files related to settings and credits.
- Updated links in various components to redirect from `/settings` to `/billing`.
- Refactored `useActiveSidebarLink` to streamline sidebar link management.
- Enhanced UI elements for better responsiveness and aesthetics.
- Introduced a new `StaffModeNotice` component for staff mode indications.
- Adjusted layout components for consistent styling.
- Reorganized billing-related components into a unified structure.
- Improved pagination button styles for better visibility.

> The following files were skipped due to too many changes: `apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/usage/rpc/page.tsx`

> ✨ 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 full-width sidebar layout with collapsible submenus and responsive navigation.
  * Added a team sidebar layout with structured navigation for team dashboards.
  * Added collapsible UI components for use in layouts and menus.
  * Added a reusable StaffModeNotice component for staff mode indication.

* **Bug Fixes**
  * Improved container sizing and maximum width constraints for better layout consistency across analytics, audit log, and ecosystem pages.

* **Refactor**
  * Simplified and reorganized sidebar layouts, removing complex internal logic and mobile navigation from several components.
  * Updated navigation structures to use new sidebar and tab-based layouts.
  * Streamlined and restyled headers, banners, and button components for a more cohesive look.
  * Replaced inline staff mode banners with the StaffModeNotice component.
  * Removed sidebar navigation from usage and settings layouts for a cleaner interface.
  * Removed gas credits settings and related components from the settings area.
  * Refactored ecosystem pages to redirect directly to creation, removing landing page.
  * Updated billing-related URL paths by removing deprecated `/settings` segments for consistency.
  * Removed account plan constants and unused hooks for cleaner codebase.
  * Cleaned up imports and replaced some relative imports with absolute paths.

* **Style**
  * Enhanced visual styling and responsiveness for range selectors, sidebars, and headers.
  * Adjusted text sizes and spacing in mobile navigation and file sections for improved readability.
  * Added rounded corners and adjusted spacing in date and interval selectors.
  * Added responsive minimum widths and consistent borders to pagination buttons and links.

* **Chores**
  * Added a new dependency for collapsible UI support.
  * Updated import paths and added lint suppression comments for code clarity and maintainability.
  * Added responsive minimum widths to pagination buttons and consistent border styling to pagination links.
  * Updated multiple URLs in links and buttons to reflect new billing paths without `/settings` segment.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-06-28 12:10:06 +00:00
Joaquim VergesandGitHub 143ab6d127 [Portal] simplify get started wallet documentation (#7466) 2025-06-28 04:10:53 -07:00
bc512551ab [SDK] Add enableCard prop to control fiat payment visibility (#7465)
Co-authored-by: Claude <[email protected]>
Co-authored-by: Joaquim Verges <[email protected]>
2025-06-28 04:09:52 -07:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
8e29d48426 chore(deps): bump @vercel/functions from 2.1.0 to 2.2.2 (#7422)
Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-28 01:09:38 -07:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
e7e6446e7d chore(deps-dev): bump checkly from 5.6.0 to 6.0.1 (#7424)
Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-28 01:09:21 -07:00
538 changed files with 20247 additions and 20723 deletions
+1 -1
View File
@@ -63,7 +63,7 @@ jobs:
- name: Setup Biome
uses: biomejs/setup-biome@a9763ed3d2388f5746f9dc3e1a55df7f4609bc89 # v2.5.1
with:
version: 2.0.4
version: 2.0.6
- run: pnpm lint
+1 -1
View File
@@ -1,4 +1,4 @@
{
"$schema": "https://biomejs.dev/schemas/2.0.4/schema.json",
"$schema": "https://biomejs.dev/schemas/2.0.6/schema.json",
"extends": "//"
}
+6 -3
View File
@@ -14,12 +14,15 @@ module.exports = [
"/in-app-wallets",
"/transactions",
// -- end build category
// -- storage
// -- scale category
"/rpc",
"/insight",
"/storage",
// -- end scale category
// -- nebula
"/nebula",
// --insight
"/insight",
// -- contracts
"/contracts",
"/contracts/modular-contracts",
+19 -25
View File
@@ -208,35 +208,29 @@ function getConfig(): NextConfig {
const withBundleAnalyzer = require("@next/bundle-analyzer")({
enabled: process.env.ANALYZE === "true",
});
// eslint-disable-next-line @typescript-eslint/no-var-requires
const { withPlausibleProxy } = require("next-plausible");
// eslint-disable-next-line @typescript-eslint/no-var-requires
const { withSentryConfig } = require("@sentry/nextjs");
return withBundleAnalyzer(
withPlausibleProxy({
customDomain: "https://pl.thirdweb.com",
scriptName: "pl",
})(
withSentryConfig(
{
...baseNextConfig,
// @ts-expect-error - this is a valid option
webpack: (config) => {
if (config.cache) {
config.cache = Object.freeze({
type: "memory",
});
}
config.module = {
...config.module,
exprContextCritical: false,
};
// Important: return the modified config
return config;
},
withSentryConfig(
{
...baseNextConfig,
// @ts-expect-error - this is a valid option
webpack: (config) => {
if (config.cache) {
config.cache = Object.freeze({
type: "memory",
});
}
config.module = {
...config.module,
exprContextCritical: false,
};
// Important: return the modified config
return config;
},
SENTRY_OPTIONS,
),
},
SENTRY_OPTIONS,
),
);
}
+27 -27
View File
@@ -4,13 +4,14 @@
"@chakra-ui/styled-system": "^2.9.2",
"@chakra-ui/theme-tools": "^2.1.2",
"@emotion/react": "11.14.0",
"@emotion/styled": "11.14.0",
"@emotion/styled": "11.14.1",
"@hookform/resolvers": "^3.9.1",
"@marsidev/react-turnstile": "^1.1.0",
"@radix-ui/react-accordion": "^1.2.11",
"@radix-ui/react-alert-dialog": "^1.1.14",
"@radix-ui/react-avatar": "^1.1.10",
"@radix-ui/react-checkbox": "^1.3.2",
"@radix-ui/react-collapsible": "^1.1.11",
"@radix-ui/react-dialog": "1.1.14",
"@radix-ui/react-dropdown-menu": "^2.1.15",
"@radix-ui/react-hover-card": "^1.1.14",
@@ -23,14 +24,14 @@
"@radix-ui/react-slot": "^1.2.3",
"@radix-ui/react-switch": "^1.2.5",
"@radix-ui/react-tooltip": "1.2.7",
"@scalar/api-reference-react": "^0.6.19",
"@sentry/nextjs": "9.29.0",
"@scalar/api-reference-react": "0.7.25",
"@sentry/nextjs": "9.34.0",
"@shazow/whatsabi": "0.22.2",
"@tanstack/react-query": "5.80.7",
"@tanstack/react-query": "5.81.5",
"@tanstack/react-table": "^8.21.3",
"@thirdweb-dev/service-utils": "workspace:*",
"@thirdweb-dev/vault-sdk": "workspace:*",
"@vercel/functions": "2.1.0",
"@vercel/functions": "2.2.2",
"@vercel/og": "^0.6.8",
"abitype": "1.0.8",
"chakra-react-select": "^4.7.6",
@@ -40,29 +41,28 @@
"compare-versions": "^6.1.0",
"date-fns": "4.1.0",
"fast-xml-parser": "^5.2.5",
"framer-motion": "12.17.0",
"framer-motion": "12.23.0",
"fuse.js": "7.1.0",
"input-otp": "^1.4.1",
"ioredis": "^5.6.1",
"ipaddr.js": "^2.2.0",
"lucide-react": "0.514.0",
"next": "15.3.3",
"next-plausible": "^3.12.4",
"lucide-react": "0.525.0",
"next": "15.3.5",
"next-themes": "^0.4.6",
"nextjs-toploader": "^1.6.12",
"nuqs": "^2.4.3",
"p-limit": "^6.2.0",
"papaparse": "^5.5.3",
"pluralize": "^8.0.0",
"posthog-js": "1.252.0",
"prettier": "3.5.3",
"posthog-js": "1.256.1",
"prettier": "3.6.2",
"qrcode": "^1.5.3",
"react": "19.1.0",
"react-children-utilities": "^2.10.0",
"react-day-picker": "^8.10.1",
"react-dom": "19.1.0",
"react-dropzone": "^14.3.8",
"react-error-boundary": "^5.0.0",
"react-error-boundary": "6.0.0",
"react-hook-form": "7.55.0",
"react-markdown": "10.1.0",
"react-table": "^7.8.0",
@@ -71,7 +71,7 @@
"responsive-rsc": "0.0.7",
"server-only": "^0.0.1",
"shiki": "1.27.0",
"sonner": "2.0.5",
"sonner": "2.0.6",
"spdx-correct": "^3.2.0",
"stripe": "17.7.0",
"swagger-ui-react": "^5.24.1",
@@ -81,19 +81,19 @@
"tiny-invariant": "^1.3.3",
"use-debounce": "^10.0.5",
"vaul": "^1.1.2",
"zod": "3.25.67"
"zod": "3.25.75"
},
"devDependencies": {
"@biomejs/biome": "2.0.4",
"@biomejs/biome": "2.0.6",
"@chakra-ui/cli": "^2.4.1",
"@chromatic-com/storybook": "4.0.0",
"@next/bundle-analyzer": "15.3.3",
"@next/eslint-plugin-next": "15.3.3",
"@playwright/test": "1.53.0",
"@storybook/addon-docs": "9.0.8",
"@storybook/addon-links": "9.0.8",
"@storybook/addon-onboarding": "9.0.8",
"@storybook/nextjs": "9.0.8",
"@chromatic-com/storybook": "4.0.1",
"@next/bundle-analyzer": "15.3.5",
"@next/eslint-plugin-next": "15.3.5",
"@playwright/test": "1.53.2",
"@storybook/addon-docs": "9.0.15",
"@storybook/addon-links": "9.0.15",
"@storybook/addon-onboarding": "9.0.15",
"@storybook/nextjs": "9.0.15",
"@types/color": "4.2.0",
"@types/node": "22.14.1",
"@types/papaparse": "^5.3.16",
@@ -107,15 +107,15 @@
"@typescript-eslint/eslint-plugin": "7.14.1",
"@typescript-eslint/parser": "7.14.1",
"autoprefixer": "^10.4.21",
"checkly": "5.6.0",
"checkly": "6.0.1",
"eslint": "8.57.0",
"eslint-config-biome": "1.9.4",
"eslint-plugin-react-compiler": "19.1.0-rc.2",
"eslint-plugin-storybook": "9.0.8",
"eslint-plugin-storybook": "9.0.15",
"knip": "5.60.2",
"next-sitemap": "^4.2.3",
"postcss": "8.5.5",
"storybook": "9.0.8",
"postcss": "8.5.6",
"storybook": "9.0.15",
"tailwindcss": "3.4.17",
"typescript": "5.8.3"
},
+25 -8
View File
@@ -106,9 +106,27 @@ const projectPageRedirects = [
source: `${projectRoute}/nebula/:path*`,
},
{
source: `${projectRoute}/connect/analytics`,
destination: `${projectRoute}`,
permanent: false,
source: `${projectRoute}/connect/analytics`,
},
];
const teamPageRedirects = [
{
destination: "/team/:team_slug/~/billing/:path*",
permanent: false,
source: "/team/:team_slug/~/settings/billing/:path*",
},
{
destination: "/team/:team_slug/~/billing/invoices/:path*",
permanent: false,
source: "/team/:team_slug/~/settings/invoices/:path*",
},
{
destination: "/team/:team_slug/~/billing",
permanent: false,
source: "/team/:team_slug/~/settings/credits",
},
];
@@ -309,13 +327,6 @@ async function redirects() {
permanent: false,
source: "/solutions/chains",
},
// redirect /rpc to portal
{
destination:
"https://portal.thirdweb.com/infrastructure/rpc-edge/overview",
permanent: false,
source: "/rpc-edge",
},
// redirect /sdk to portal
{
destination: "https://portal.thirdweb.com/connect/blockchain-api",
@@ -424,8 +435,14 @@ async function redirects() {
permanent: false,
source: "/engine",
},
{
destination: "/rpc",
permanent: false,
source: "/rpc-edge",
},
...legacyDashboardToTeamRedirects,
...projectPageRedirects,
...teamPageRedirects,
];
}
+87 -1
View File
@@ -1,7 +1,10 @@
"use server";
import "server-only";
import { getAuthToken } from "@/api/auth-token";
import { NEXT_PUBLIC_THIRDWEB_API_HOST } from "@/constants/public-envs";
import type { ChainInfraSKU } from "@/types/billing";
import { getAbsoluteUrl } from "@/utils/vercel";
export async function reSubscribePlan(options: {
teamId: string;
@@ -14,7 +17,10 @@ export async function reSubscribePlan(options: {
}
const res = await fetch(
`${NEXT_PUBLIC_THIRDWEB_API_HOST}/v1/teams/${options.teamId}/checkout/resubscribe-plan`,
new URL(
`/v1/teams/${options.teamId}/checkout/resubscribe-plan`,
NEXT_PUBLIC_THIRDWEB_API_HOST,
),
{
body: JSON.stringify({}),
headers: {
@@ -35,3 +41,83 @@ export async function reSubscribePlan(options: {
status: 200,
};
}
export async function getChainInfraCheckoutURL(options: {
teamSlug: string;
skus: ChainInfraSKU[];
chainId: number;
annual: boolean;
}) {
const token = await getAuthToken();
if (!token) {
return {
error: "You are not logged in",
status: "error",
} as const;
}
const res = await fetch(
new URL(
`/v1/teams/${options.teamSlug}/checkout/create-link`,
NEXT_PUBLIC_THIRDWEB_API_HOST,
),
{
body: JSON.stringify({
annual: options.annual,
baseUrl: getAbsoluteUrl(),
chainId: options.chainId,
skus: options.skus,
}),
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
method: "POST",
},
);
if (!res.ok) {
const text = await res.text();
console.error("Failed to create checkout link", text, res.status);
switch (res.status) {
case 402: {
return {
error:
"You have outstanding invoices, please pay these first before re-subscribing.",
status: "error",
} as const;
}
case 429: {
return {
error: "Too many requests, please try again later.",
status: "error",
} as const;
}
case 403: {
return {
error: "You are not authorized to deploy infrastructure.",
status: "error",
} as const;
}
default: {
return {
error: "An unknown error occurred, please try again later.",
status: "error",
} as const;
}
}
}
const json = await res.json();
if (!json.result) {
return {
error: "An unknown error occurred, please try again later.",
status: "error",
} as const;
}
return {
data: json.result as string,
status: "success",
} as const;
}
+34 -1
View File
@@ -1,6 +1,7 @@
import posthog from "posthog-js";
import type { Team } from "@/api/team";
import type { ProductSKU } from "../types/billing";
// ----------------------------
// CONTRACTS
@@ -361,7 +362,11 @@ export function reportAssetCreationFailed(
properties: { contractType: AssetContractType; error: string } & (
| {
assetType: "nft";
step: "deploy-contract" | "mint-nfts" | "set-claim-conditions";
step:
| "deploy-contract"
| "mint-nfts"
| "set-claim-conditions"
| "set-admins";
}
| {
assetType: "coin";
@@ -380,3 +385,31 @@ export function reportAssetCreationFailed(
step: properties.step,
});
}
type UpsellParams = {
content: "storage-limit";
campaign: "create-coin" | "create-nft";
sku: Exclude<ProductSKU, null>;
};
/**
* ### Why do we need to report this event?
* - To track how effective the upsells are in driving users to upgrade
*
* ### Who is responsible for this event?
* @MananTank
*/
export function reportUpsellShown(properties: UpsellParams) {
posthog.capture("upsell shown", properties);
}
/**
* ### Why do we need to report this event?
* - To track how effective the upsells are in driving users to upgrade
*
* ### Who is responsible for this event?
* @MananTank
*/
export function reportUpsellClicked(properties: UpsellParams) {
posthog.capture("upsell clicked", properties);
}
+146 -1
View File
@@ -6,7 +6,6 @@ import type {
EcosystemWalletStats,
EngineCloudStats,
InAppWalletStats,
RpcMethodStats,
TransactionStats,
UniversalBridgeStats,
UniversalBridgeWalletStats,
@@ -17,6 +16,41 @@ import type {
import { getAuthToken } from "./auth-token";
import { getChains } from "./chain";
export interface InsightChainStats {
date: string;
chainId: string;
totalRequests: number;
}
export interface InsightStatusCodeStats {
date: string;
httpStatusCode: number;
totalRequests: number;
}
export interface InsightEndpointStats {
date: string;
endpoint: string;
totalRequests: number;
}
interface InsightUsageStats {
date: string;
totalRequests: number;
}
export interface RpcMethodStats {
date: string;
evmMethod: string;
count: number;
}
export interface RpcUsageTypeStats {
date: string;
usageType: string;
count: number;
}
async function fetchAnalytics(
input: string | URL,
init?: RequestInit,
@@ -76,6 +110,9 @@ function buildSearchParams(params: AnalyticsQueryParams): URLSearchParams {
if (params.period) {
searchParams.append("period", params.period);
}
if (params.limit) {
searchParams.append("limit", params.limit.toString());
}
return searchParams;
}
@@ -225,6 +262,26 @@ export async function getRpcMethodUsage(
return json.data as RpcMethodStats[];
}
export async function getRpcUsageByType(
params: AnalyticsQueryParams,
): Promise<RpcUsageTypeStats[]> {
const searchParams = buildSearchParams(params);
const res = await fetchAnalytics(
`v2/rpc/usage-types?${searchParams.toString()}`,
{
method: "GET",
},
);
if (res?.status !== 200) {
console.error("Failed to fetch RPC usage");
return [];
}
const json = await res.json();
return json.data as RpcUsageTypeStats[];
}
export async function getWalletUsers(
params: AnalyticsQueryParams,
): Promise<WalletUserStats[]> {
@@ -424,3 +481,91 @@ export async function getEngineCloudMethodUsage(
const json = await res.json();
return json.data as EngineCloudStats[];
}
export async function getInsightChainUsage(
params: AnalyticsQueryParams,
): Promise<{ data: InsightChainStats[] } | { errorMessage: string }> {
const searchParams = buildSearchParams(params);
const res = await fetchAnalytics(
`v2/insight/usage/by-chain?${searchParams.toString()}`,
{
method: "GET",
},
);
if (res?.status !== 200) {
const reason = await res?.text();
const errMsg = `Failed to fetch Insight chain usage: ${res?.status} - ${res.statusText} - ${reason}`;
console.error(errMsg);
return { errorMessage: errMsg };
}
const json = await res.json();
return { data: json.data as InsightChainStats[] };
}
export async function getInsightStatusCodeUsage(
params: AnalyticsQueryParams,
): Promise<{ data: InsightStatusCodeStats[] } | { errorMessage: string }> {
const searchParams = buildSearchParams(params);
const res = await fetchAnalytics(
`v2/insight/usage/by-status-code?${searchParams.toString()}`,
{
method: "GET",
},
);
if (res?.status !== 200) {
const reason = await res?.text();
const errMsg = `Failed to fetch Insight status code usage: ${res?.status} - ${res.statusText} - ${reason}`;
console.error(errMsg);
return { errorMessage: errMsg };
}
const json = await res.json();
return { data: json.data as InsightStatusCodeStats[] };
}
export async function getInsightEndpointUsage(
params: AnalyticsQueryParams,
): Promise<{ data: InsightEndpointStats[] } | { errorMessage: string }> {
const searchParams = buildSearchParams(params);
const res = await fetchAnalytics(
`v2/insight/usage/by-endpoint?${searchParams.toString()}`,
{
method: "GET",
},
);
if (res?.status !== 200) {
const reason = await res?.text();
const errMsg = `Failed to fetch Insight endpoint usage: ${res?.status} - ${res.statusText} - ${reason}`;
console.error(errMsg);
return { errorMessage: errMsg };
}
const json = await res.json();
return { data: json.data as InsightEndpointStats[] };
}
export async function getInsightUsage(
params: AnalyticsQueryParams,
): Promise<{ data: InsightUsageStats[] } | { errorMessage: string }> {
const searchParams = buildSearchParams(params);
const res = await fetchAnalytics(
`v2/insight/usage?${searchParams.toString()}`,
{
method: "GET",
},
);
if (res?.status !== 200) {
const reason = await res?.text();
const errMsg = `Failed to fetch Insight usage: ${res?.status} - ${res.statusText} - ${reason}`;
console.error(errMsg);
return { errorMessage: errMsg };
}
const json = await res.json();
return { data: json.data as InsightUsageStats[] };
}
@@ -1,22 +1,26 @@
export const authOptions = [
"email",
"phone",
"passkey",
"siwe",
"guest",
"google",
"facebook",
"x",
"discord",
"farcaster",
"telegram",
"github",
"twitch",
"steam",
"apple",
"coinbase",
"line",
] as const;
import "server-only";
import { NEXT_PUBLIC_THIRDWEB_API_HOST } from "@/constants/public-envs";
import { getAuthToken } from "./auth-token";
export type AuthOption =
| "email"
| "phone"
| "passkey"
| "siwe"
| "guest"
| "google"
| "facebook"
| "x"
| "discord"
| "farcaster"
| "telegram"
| "github"
| "twitch"
| "steam"
| "apple"
| "coinbase"
| "line";
export type Ecosystem = {
name: string;
@@ -24,7 +28,7 @@ export type Ecosystem = {
id: string;
slug: string;
permission: "PARTNER_WHITELIST" | "ANYONE";
authOptions: (typeof authOptions)[number][];
authOptions: AuthOption[];
customAuthOptions?: {
authEndpoint?: {
url: string;
@@ -47,6 +51,54 @@ export type Ecosystem = {
updatedAt: string;
};
export async function fetchEcosystemList(teamIdOrSlug: string) {
const token = await getAuthToken();
if (!token) {
return [];
}
const res = await fetch(
`${NEXT_PUBLIC_THIRDWEB_API_HOST}/v1/teams/${teamIdOrSlug}/ecosystem-wallet`,
{
headers: {
Authorization: `Bearer ${token}`,
},
},
);
if (!res.ok) {
return [];
}
return (await res.json()).result as Ecosystem[];
}
export async function fetchEcosystem(slug: string, teamIdOrSlug: string) {
const token = await getAuthToken();
if (!token) {
return null;
}
const res = await fetch(
`${NEXT_PUBLIC_THIRDWEB_API_HOST}/v1/teams/${teamIdOrSlug}/ecosystem-wallet/${slug}`,
{
headers: {
Authorization: `Bearer ${token}`,
},
},
);
if (!res.ok) {
const data = await res.json();
console.error(data);
return null;
}
const data = (await res.json()) as { result: Ecosystem };
return data.result;
}
type PartnerPermission = "PROMPT_USER_V1" | "FULL_CONTROL_V1";
export type Partner = {
id: string;
+67 -2
View File
@@ -1,6 +1,6 @@
import { getAuthToken } from "@/api/auth-token";
import { NEXT_PUBLIC_THIRDWEB_API_HOST } from "@/constants/public-envs";
import type { ProductSKU } from "@/types/billing";
import type { ChainInfraSKU, ProductSKU } from "@/types/billing";
type InvoiceLine = {
// amount for this line item
@@ -22,7 +22,7 @@ type Invoice = {
export type TeamSubscription = {
id: string;
type: "PLAN" | "USAGE" | "PLAN_ADD_ON" | "PRODUCT";
type: "PLAN" | "USAGE" | "PLAN_ADD_ON" | "PRODUCT" | "CHAIN";
status:
| "incomplete"
| "incomplete_expired"
@@ -37,6 +37,13 @@ export type TeamSubscription = {
trialStart: string | null;
trialEnd: string | null;
upcomingInvoice: Invoice;
skus: (ProductSKU | ChainInfraSKU)[];
};
type ChainTeamSubscription = Omit<TeamSubscription, "skus"> & {
chainId: string;
skus: ChainInfraSKU[];
isLegacy: boolean;
};
export async function getTeamSubscriptions(slug: string) {
@@ -60,3 +67,61 @@ export async function getTeamSubscriptions(slug: string) {
}
return null;
}
const CHAIN_PLAN_TO_INFRA = {
"chain:plan:gold": ["chain:infra:rpc", "chain:infra:account_abstraction"],
"chain:plan:platinum": [
"chain:infra:rpc",
"chain:infra:insight",
"chain:infra:account_abstraction",
],
"chain:plan:ultimate": [
"chain:infra:rpc",
"chain:infra:insight",
"chain:infra:account_abstraction",
],
};
export async function getChainSubscriptions(slug: string) {
const allSubscriptions = await getTeamSubscriptions(slug);
if (!allSubscriptions) {
return null;
}
// first replace any sku that MIGHT match a chain plan
const updatedSubscriptions = allSubscriptions
.filter((s) => s.type === "CHAIN")
.map((s) => {
const skus = s.skus;
const updatedSkus = skus.flatMap((sku) => {
const plan =
CHAIN_PLAN_TO_INFRA[sku as keyof typeof CHAIN_PLAN_TO_INFRA];
return plan ? plan : sku;
});
return {
...s,
isLegacy: updatedSkus.length !== skus.length,
skus: updatedSkus,
};
});
return updatedSubscriptions.filter(
(s): s is ChainTeamSubscription =>
"chainId" in s && typeof s.chainId === "string",
);
}
export async function getChainSubscriptionForChain(
slug: string,
chainId: number,
) {
const chainSubscriptions = await getChainSubscriptions(slug);
if (!chainSubscriptions) {
return null;
}
return (
chainSubscriptions.find((s) => s.chainId === chainId.toString()) ?? null
);
}
@@ -8,11 +8,13 @@ import {
SelectValue,
} from "@/components/ui/select";
import { normalizeTime } from "@/lib/time";
import { cn } from "@/lib/utils";
export function DateRangeSelector(props: {
range: Range;
setRange: (range: Range) => void;
popoverAlign?: "start" | "end" | "center";
className?: string;
}) {
const { range, setRange } = props;
const daysDiff = differenceInCalendarDays(range.to, range.from);
@@ -26,7 +28,7 @@ export function DateRangeSelector(props: {
return (
<DatePickerWithRange
className="w-auto bg-card"
className={cn("w-auto bg-card", props.className)}
from={range.from}
header={
<div className="mb-2 border-border border-b p-4">
@@ -76,8 +76,9 @@ export function RangeSelector({
});
return (
<div className="flex justify-end gap-4">
<div className="flex justify-end gap-3 flex-col lg:flex-row">
<DateRangeSelector
className="rounded-full"
range={localRange}
setRange={(newRange) => {
setRange(newRange);
@@ -89,7 +90,7 @@ export function RangeSelector({
}}
/>
<IntervalSelector
className="bg-card"
className="bg-card rounded-full"
intervalType={localInterval}
setIntervalType={(newInterval) => {
setInterval(newInterval);
@@ -82,7 +82,7 @@ function UnpaidInvoicesWarning({ teamSlug }: { teamSlug: string }) {
<Button asChild className="w-full gap-2 bg-card" variant="outline">
<Link
href={`/team/${teamSlug}/~/settings/invoices?status=open`}
href={`/team/${teamSlug}/~/billing/invoices?status=open`}
rel="noopener noreferrer"
target="_blank"
>
@@ -60,7 +60,7 @@ function BillingWarning({ teamSlug }: { teamSlug: string }) {
You have outstanding invoices. Please{" "}
<Link
className="font-medium text-amber-700 underline transition-colors hover:text-amber-900 dark:text-amber-400 dark:hover:text-amber-300"
href={`/team/${teamSlug}/~/settings/invoices?status=open`}
href={`/team/${teamSlug}/~/billing/invoices?status=open`}
>
pay them
</Link>{" "}
@@ -44,7 +44,7 @@ export const GatedSwitch: React.FC<GatedSwitchProps> = (
<div className="flex w-full flex-col gap-2">
<Button asChild className="justify-start gap-2" size="sm">
<Link
href={`/team/${props.teamSlug}/~/settings/billing?showPlans=true&highlight=${props.requiredPlan}`}
href={`/team/${props.teamSlug}/~/billing?showPlans=true&highlight=${props.requiredPlan}`}
rel="noopener noreferrer"
target="_blank"
>
@@ -53,7 +53,7 @@ export function MobileSidebar(props: {
);
}
export function useActiveSidebarLink(links: SidebarLink[]) {
function useActiveSidebarLink(links: SidebarLink[]) {
const pathname = usePathname();
const activeLink = useMemo(() => {
@@ -152,6 +152,7 @@ export function SingleNetworkSelector(props: {
disableChainId?: boolean;
align?: "center" | "start" | "end";
disableTestnets?: boolean;
disableDeprecated?: boolean;
placeholder?: string;
client: ThirdwebClient;
}) {
@@ -169,8 +170,17 @@ export function SingleNetworkSelector(props: {
chains = chains.filter((chain) => chainIdSet.has(chain.chainId));
}
if (props.disableDeprecated) {
chains = chains.filter((chain) => chain.status !== "deprecated");
}
return chains;
}, [allChains, props.chainIds, props.disableTestnets]);
}, [
allChains,
props.chainIds,
props.disableTestnets,
props.disableDeprecated,
]);
const options = useMemo(() => {
return chainsToShow.map((chain) => {
@@ -1,24 +1,7 @@
import {
Sidebar,
SidebarContent,
SidebarFooter,
SidebarGroup,
SidebarGroupContent,
SidebarGroupLabel,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
SidebarMenuSubItem,
SidebarRail,
SidebarSeparator,
SidebarTrigger,
useSidebar,
} from "@/components/ui/sidebar";
"use client";
import { cn } from "../../lib/utils";
import { AppFooter } from "../footers/app-footer";
import { NavLink } from "../ui/NavLink";
import { Separator } from "../ui/separator";
import { MobileSidebar, useActiveSidebarLink } from "./MobileSidebar";
import { MobileSidebar } from "./MobileSidebar";
import { CustomSidebar, type SidebarLink } from "./Sidebar";
export function SidebarLayout(props: {
@@ -51,129 +34,3 @@ export function SidebarLayout(props: {
</div>
);
}
export function FullWidthSidebarLayout(props: {
contentSidebarLinks: SidebarLink[];
footerSidebarLinks?: SidebarLink[];
children: React.ReactNode;
className?: string;
}) {
const { contentSidebarLinks, children, footerSidebarLinks } = props;
return (
<div
className={cn(
"relative flex w-full flex-1 overflow-y-hidden",
props.className,
)}
>
{/* left - sidebar */}
<Sidebar className="pt-2" collapsible="icon" side="left">
<SidebarContent className="p-2">
<RenderSidebarMenu links={contentSidebarLinks} />
</SidebarContent>
{footerSidebarLinks && (
<SidebarFooter className="pb-3">
<RenderSidebarMenu links={footerSidebarLinks} />
</SidebarFooter>
)}
<SidebarRail />
</Sidebar>
{/* right - content */}
<div className="flex h-full flex-grow flex-col overflow-y-auto">
<MobileSidebarTrigger
links={[...contentSidebarLinks, ...(footerSidebarLinks || [])]}
/>
<main className="flex min-w-0 grow flex-col max-sm:w-full">
{children}
</main>
<AppFooter containerClassName="max-w-7xl" />
</div>
</div>
);
}
function RenderSidebarGroup(props: {
sidebarLinks: SidebarLink[];
groupName: string;
}) {
return (
<SidebarGroup className="p-0">
<SidebarMenuItem>
<SidebarGroupLabel> {props.groupName}</SidebarGroupLabel>
<SidebarGroupContent>
<RenderSidebarMenu links={props.sidebarLinks} />
</SidebarGroupContent>
</SidebarMenuItem>
</SidebarGroup>
);
}
function RenderSidebarMenu(props: { links: SidebarLink[] }) {
const sidebar = useSidebar();
return (
<SidebarMenu className="gap-1.5">
{props.links.map((link, idx) => {
// link
if ("href" in link) {
return (
<SidebarMenuSubItem key={link.href}>
<SidebarMenuButton asChild>
<NavLink
activeClassName="text-foreground bg-accent"
className="flex items-center gap-2 text-muted-foreground text-sm hover:bg-accent"
exactMatch={link.exactMatch}
href={link.href}
isActive={link.isActive}
onClick={() => {
sidebar.setOpenMobile(false);
}}
>
{link.icon && <link.icon className="size-4" />}
<span>{link.label}</span>
</NavLink>
</SidebarMenuButton>
</SidebarMenuSubItem>
);
}
// separator
if ("separator" in link) {
return (
<SidebarSeparator
className="my-1"
key={`separator-${
// biome-ignore lint/suspicious/noArrayIndexKey: index is fine here
idx
}`}
/>
);
}
// group
return (
<RenderSidebarGroup
groupName={link.group}
key={link.group}
sidebarLinks={link.links}
/>
);
})}
</SidebarMenu>
);
}
function MobileSidebarTrigger(props: { links: SidebarLink[] }) {
const activeLink = useActiveSidebarLink(props.links);
return (
<div className="flex items-center gap-3 border-b px-4 py-4 lg:hidden">
<SidebarTrigger className="size-4" />
<Separator className="h-4" orientation="vertical" />
{activeLink && <span className="text-sm">{activeLink.label}</span>}
</div>
);
}
@@ -1,3 +1,4 @@
/** biome-ignore-all lint/a11y/useSemanticElements: FIXME */
"use client";
import {
@@ -164,7 +165,6 @@ export function TWTable<TRowData>(tableProps: TWTableProps<TRowData>) {
return (
<TableRow
key={row.id}
// biome-ignore lint/a11y/useSemanticElements: FIXME
role="group"
{...(tableProps.onRowClick
? {
@@ -45,7 +45,7 @@ export function TeamPlanBadge(props: {
}
e.stopPropagation();
e.preventDefault();
router.push(`/team/${props.teamSlug}/~/settings/billing?showPlans=true`);
router.push(`/team/${props.teamSlug}/~/billing?showPlans=true`);
}
return (
@@ -38,12 +38,18 @@ const ACCENT = {
type UpsellBannerCardProps = {
title: React.ReactNode;
description: React.ReactNode;
cta: {
text: React.ReactNode;
icon?: React.ReactNode;
target?: "_blank";
link: string;
};
cta?:
| {
text: React.ReactNode;
icon?: React.ReactNode;
target?: "_blank";
link: string;
}
| {
text: React.ReactNode;
icon?: React.ReactNode;
onClick: () => void;
};
accentColor?: keyof typeof ACCENT;
icon?: React.ReactNode;
};
@@ -93,25 +99,41 @@ export function UpsellBannerCard(props: UpsellBannerCardProps) {
</div>
</div>
<Button
asChild
className={cn(
"mt-2 gap-2 hover:translate-y-0 hover:shadow-inner sm:mt-0",
color.btn,
)}
size="sm"
>
<Link
href={props.cta.link}
rel={
props.cta.target === "_blank" ? "noopener noreferrer" : undefined
}
target={props.cta.target}
{props.cta && "target" in props.cta ? (
<Button
asChild
className={cn(
"mt-2 gap-2 hover:translate-y-0 hover:shadow-inner sm:mt-0",
color.btn,
)}
size="sm"
>
<Link
href={props.cta.link}
rel={
props.cta.target === "_blank"
? "noopener noreferrer"
: undefined
}
target={props.cta.target}
>
{props.cta.text}
{props.cta.icon && <span className="ml-2">{props.cta.icon}</span>}
</Link>
</Button>
) : props.cta && "onClick" in props.cta ? (
<Button
className={cn(
"mt-2 gap-2 hover:translate-y-0 hover:shadow-inner sm:mt-0",
color.btn,
)}
onClick={props.cta.onClick}
size="sm"
>
{props.cta.text}
{props.cta.icon && <span className="ml-2">{props.cta.icon}</span>}
</Link>
</Button>
</Button>
) : null}
</div>
</div>
);
@@ -0,0 +1,304 @@
"use client";
import { ChevronDownIcon, ChevronRightIcon } from "lucide-react";
import { usePathname } from "next/navigation";
import { useMemo } from "react";
import { AppFooter } from "@/components/footers/app-footer";
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from "@/components/ui/collapsible";
import { DynamicHeight } from "@/components/ui/DynamicHeight";
import { NavLink } from "@/components/ui/NavLink";
import { Separator } from "@/components/ui/separator";
import {
Sidebar,
SidebarContent,
SidebarFooter,
SidebarGroup,
SidebarGroupContent,
SidebarGroupLabel,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
SidebarMenuSub,
SidebarMenuSubItem,
SidebarRail,
SidebarSeparator,
SidebarTrigger,
useSidebar,
} from "@/components/ui/sidebar";
import { cn } from "@/lib/utils";
type ShadcnSidebarBaseLink = {
href: string;
label: React.ReactNode;
exactMatch?: boolean;
icon?: React.FC<{ className?: string }>;
isActive?: (pathname: string) => boolean;
};
type ShadcnSidebarLink =
| ShadcnSidebarBaseLink
| {
group: string;
links: ShadcnSidebarBaseLink[];
}
| {
separator: true;
}
| {
subMenu: Omit<ShadcnSidebarBaseLink, "href" | "exactMatch">;
links: Omit<ShadcnSidebarBaseLink, "icon">[];
};
export function FullWidthSidebarLayout(props: {
contentSidebarLinks: ShadcnSidebarLink[];
footerSidebarLinks?: ShadcnSidebarLink[];
children: React.ReactNode;
className?: string;
}) {
const { contentSidebarLinks, children, footerSidebarLinks } = props;
return (
<div
className={cn(
"relative flex w-full flex-1 overflow-y-hidden",
props.className,
)}
>
{/* left - sidebar */}
<Sidebar className="pt-2" collapsible="icon" side="left">
<SidebarContent className="p-2">
<RenderSidebarMenu links={contentSidebarLinks} />
</SidebarContent>
{footerSidebarLinks && (
<SidebarFooter className="pb-3">
<RenderSidebarMenu links={footerSidebarLinks} />
</SidebarFooter>
)}
<SidebarRail />
</Sidebar>
{/* right - content */}
<div className="flex h-full flex-grow flex-col overflow-y-auto">
<MobileSidebarTrigger
links={[...contentSidebarLinks, ...(footerSidebarLinks || [])]}
/>
<main className="flex min-w-0 grow flex-col max-sm:w-full">
{children}
</main>
<AppFooter containerClassName="max-w-7xl" />
</div>
</div>
);
}
function MobileSidebarTrigger(props: { links: ShadcnSidebarLink[] }) {
const activeLink = useActiveShadcnSidebarLink(props.links);
const parentSubNav = props.links.find(
(link) =>
"subMenu" in link && link.links.some((l) => l.href === activeLink?.href),
);
return (
<div className="flex items-center gap-3 border-b px-4 py-4 lg:hidden">
<SidebarTrigger className="size-4" />
<Separator
className="h-4 bg-muted-foreground/50"
orientation="vertical"
/>
{parentSubNav && "subMenu" in parentSubNav && (
<>
<span className="text-sm">{parentSubNav.subMenu.label}</span>
<ChevronRightIcon className="size-4 text-muted-foreground/50 -mx-1.5" />
</>
)}
{activeLink && <span className="text-sm">{activeLink.label}</span>}
</div>
);
}
function useActiveShadcnSidebarLink(links: ShadcnSidebarLink[]) {
const pathname = usePathname();
const activeLink = useMemo(() => {
function isActive(link: ShadcnSidebarBaseLink) {
if (link.exactMatch) {
return link.href === pathname;
}
return pathname?.startsWith(link.href);
}
for (const link of links) {
if ("links" in link) {
for (const subLink of link.links) {
if (isActive(subLink)) {
return subLink;
}
}
} else if ("href" in link) {
if (isActive(link)) {
return link;
}
}
}
}, [links, pathname]);
return activeLink;
}
function useIsSubnavActive(links: ShadcnSidebarBaseLink[]) {
const pathname = usePathname();
const isSubnavActive = useMemo(() => {
function isActive(link: ShadcnSidebarBaseLink) {
if (link.exactMatch) {
return link.href === pathname;
}
return pathname?.startsWith(link.href);
}
return links.some(isActive);
}, [links, pathname]);
return isSubnavActive;
}
function RenderSidebarGroup(props: {
sidebarLinks: ShadcnSidebarLink[];
groupName: string;
}) {
return (
<SidebarGroup className="p-0">
<SidebarMenuItem>
<SidebarGroupLabel> {props.groupName}</SidebarGroupLabel>
<SidebarGroupContent>
<RenderSidebarMenu links={props.sidebarLinks} />
</SidebarGroupContent>
</SidebarMenuItem>
</SidebarGroup>
);
}
function RenderSidebarSubmenu(props: {
links: ShadcnSidebarBaseLink[];
subMenu: Omit<ShadcnSidebarBaseLink, "href" | "exactMatch">;
}) {
const sidebar = useSidebar();
const isSubnavActive = useIsSubnavActive(props.links);
return (
<SidebarMenu>
<DynamicHeight transition="height 200ms ease">
<Collapsible className="group/collapsible" defaultOpen={isSubnavActive}>
<SidebarMenuItem>
<CollapsibleTrigger asChild>
<SidebarMenuButton className="text-sm text-muted-foreground whitespace-nowrap">
{props.subMenu.icon && (
<props.subMenu.icon className="size-4" />
)}
<span>{props.subMenu.label}</span>
<ChevronDownIcon className="ml-auto group-[[data-state=open]]/collapsible:rotate-180 transition-transform" />
</SidebarMenuButton>
</CollapsibleTrigger>
<CollapsibleContent className="mt-0.5">
<SidebarMenuSub className="pr-0 pl-2">
{props.links.map((link) => {
return (
<SidebarMenuSubItem key={link.href}>
<NavLink
activeClassName="text-foreground bg-accent "
className="flex items-center gap-2 text-muted-foreground text-sm hover:bg-accent hover:text-foreground px-2 py-1.5 rounded-lg w-full"
exactMatch={link.exactMatch}
href={link.href}
isActive={link.isActive}
onClick={() => {
sidebar.setOpenMobile(false);
}}
>
{link.icon && <link.icon className="size-4" />}
<span className="whitespace-nowrap">{link.label}</span>
</NavLink>
</SidebarMenuSubItem>
);
})}
</SidebarMenuSub>
</CollapsibleContent>
</SidebarMenuItem>
</Collapsible>
</DynamicHeight>
</SidebarMenu>
);
}
function RenderSidebarMenu(props: { links: ShadcnSidebarLink[] }) {
const sidebar = useSidebar();
return (
<SidebarMenu className="gap-1.5">
{props.links.map((link, idx) => {
// link
if ("href" in link) {
return (
<SidebarMenuSubItem key={link.href}>
<SidebarMenuButton asChild>
<NavLink
activeClassName="text-foreground bg-accent"
className="flex items-center gap-2 text-muted-foreground text-sm hover:bg-accent"
exactMatch={link.exactMatch}
href={link.href}
isActive={link.isActive}
onClick={() => {
sidebar.setOpenMobile(false);
}}
>
{link.icon && <link.icon className="size-4" />}
<span>{link.label}</span>
</NavLink>
</SidebarMenuButton>
</SidebarMenuSubItem>
);
}
// separator
if ("separator" in link) {
return (
<SidebarSeparator
className="my-1"
key={`separator-${
// biome-ignore lint/suspicious/noArrayIndexKey: index is fine here
idx
}`}
/>
);
}
// subnav
if ("subMenu" in link) {
return (
<RenderSidebarSubmenu
key={`submenu_$${
// biome-ignore lint/suspicious/noArrayIndexKey: index is fine here
idx
}`}
links={link.links}
subMenu={link.subMenu}
/>
);
}
// group
return (
<RenderSidebarGroup
groupName={link.group}
key={link.group}
sidebarLinks={link.links}
/>
);
})}
</SidebarMenu>
);
}
@@ -18,7 +18,7 @@ export type MultiStepState<T extends string> = {
}
| {
type: "error";
message: React.ReactNode;
message: string;
};
label: string;
description?: string;
@@ -27,6 +27,10 @@ export type MultiStepState<T extends string> = {
export function MultiStepStatus<T extends string>(props: {
steps: MultiStepState<T>[];
onRetry: (step: MultiStepState<T>) => void;
renderError?: (
step: MultiStepState<T>,
errorMessage: string,
) => React.ReactNode;
}) {
return (
<DynamicHeight>
@@ -66,22 +70,24 @@ export function MultiStepStatus<T extends string>(props: {
</p>
)}
{step.status.type === "error" && (
<div className="mt-1 space-y-2">
<p className="mb-1 text-red-500 text-sm">
{step.status.message}
</p>
<Button
className="gap-2"
onClick={() => props.onRetry(step)}
size="sm"
variant="destructive"
>
<RefreshCwIcon className="size-4" />
Retry
</Button>
</div>
)}
{step.status.type === "error"
? props.renderError?.(step, step.status.message) || (
<div className="mt-1 space-y-2">
<p className="mb-1 text-red-500 text-sm">
{step.status.message}
</p>
<Button
className="gap-2"
onClick={() => props.onRetry(step)}
size="sm"
variant="destructive"
>
<RefreshCwIcon className="size-4" />
Retry
</Button>
</div>
)
: null}
</div>
</div>
))}
@@ -48,7 +48,7 @@ export const PaginationButtons = (props: {
<PaginationContent>
<PaginationItem>
<PaginationPrevious
className="rounded-full"
className="rounded-full lg:min-w-[38px]"
disabled={activePage === 1}
onClick={() => {
setPage(activePage - 1);
@@ -57,7 +57,7 @@ export const PaginationButtons = (props: {
</PaginationItem>
<PaginationItem>
<PaginationNext
className="rounded-full"
className="rounded-full lg:min-w-[38px]"
disabled={activePage === totalPages}
onClick={() => {
setPage(activePage + 1);
@@ -78,7 +78,7 @@ export const PaginationButtons = (props: {
{pages.map((page) => (
<PaginationItem key={page}>
<PaginationLink
className="rounded-full"
className="rounded-full lg:min-w-[38px]"
isActive={activePage === page}
onClick={() => {
setPage(page);
@@ -98,7 +98,7 @@ export const PaginationButtons = (props: {
<PaginationContent>
<PaginationItem>
<PaginationPrevious
className="rounded-full"
className="rounded-full lg:min-w-[38px]"
disabled={activePage === 1}
onClick={() => {
setPage(activePage - 1);
@@ -111,7 +111,7 @@ export const PaginationButtons = (props: {
<>
<PaginationItem>
<PaginationLink
className="rounded-full"
className="rounded-full lg:min-w-[38px]"
onClick={() => {
setPage(1);
}}
@@ -129,7 +129,7 @@ export const PaginationButtons = (props: {
{activePage - 1 > 0 && (
<PaginationItem className="max-sm:hidden">
<PaginationLink
className="rounded-full"
className="rounded-full lg:min-w-[38px]"
onClick={() => {
setPage(activePage - 1);
}}
@@ -140,7 +140,7 @@ export const PaginationButtons = (props: {
)}
<PaginationItem>
<PaginationLink className="rounded-full" isActive>
<PaginationLink className="rounded-full lg:min-w-[38px]" isActive>
{activePage}
</PaginationLink>
</PaginationItem>
@@ -148,7 +148,7 @@ export const PaginationButtons = (props: {
{activePage + 1 <= totalPages && (
<PaginationItem className="max-sm:hidden">
<PaginationLink
className="rounded-full"
className="rounded-full lg:min-w-[38px]"
onClick={() => {
setPage(activePage + 1);
}}
@@ -167,7 +167,7 @@ export const PaginationButtons = (props: {
<PaginationItem>
<PaginationLink
className="rounded-full"
className="rounded-full lg:min-w-[38px]"
onClick={() => {
setPage(totalPages);
}}
@@ -180,7 +180,7 @@ export const PaginationButtons = (props: {
<PaginationItem>
<PaginationNext
className="rounded-full"
className="rounded-full lg:min-w-[38px]"
disabled={activePage === totalPages}
onClick={() => {
setPage(activePage + 1);
@@ -1,3 +1,4 @@
/** biome-ignore-all lint/a11y/useSemanticElements: FIXME */
"use client";
import { CheckIcon, ChevronDownIcon, SearchIcon } from "lucide-react";
@@ -190,7 +191,6 @@ export const SelectWithSearch = React.forwardRef<
ref={
i === optionsToShow.length - 1 ? lastItemRef : undefined
}
// biome-ignore lint/a11y/useSemanticElements: TDOO
role="option"
variant="ghost"
>
@@ -138,7 +138,7 @@ export function UpsellContent(props: {
<div className="flex flex-col gap-3 pt-4 sm:flex-row">
<Button asChild className="flex-1 py-3 font-semibold" size="lg">
<Link
href={`/team/${props.teamSlug}/~/settings/billing?showPlans=true&highlight=${props.requiredPlan}`}
href={`/team/${props.teamSlug}/~/billing?showPlans=true&highlight=${props.requiredPlan}`}
>
<CrownIcon className="mr-2 h-4 w-4" />
Upgrade to{" "}
@@ -146,9 +146,7 @@ export function UpsellContent(props: {
</Link>
</Button>
<Button asChild className="md:flex-1" size="lg" variant="outline">
<Link
href={`/team/${props.teamSlug}/~/settings/billing?showPlans=true`}
>
<Link href={`/team/${props.teamSlug}/~/billing?showPlans=true`}>
View All Plans
</Link>
</Button>
@@ -59,7 +59,11 @@ export function WalletAddress(props: {
// special case for zero address
if (address === ZERO_ADDRESS) {
return <span className="cursor-pointer font-mono">{shortenedAddress}</span>;
return (
<span className={cn("cursor-pointer font-mono", props.className)}>
{shortenedAddress}
</span>
);
}
return (
@@ -120,20 +120,18 @@ export const CustomConnectWallet = (props: {
if ((!isLoggedIn || !account) && loginRequired) {
return (
<>
<Button
asChild
className={props.signInLinkButtonClassName}
size="lg"
variant="default"
<Button
asChild
className={props.signInLinkButtonClassName}
size="lg"
variant="default"
>
<Link
href={`/login${pathname ? `?next=${encodeURIComponent(pathname)}` : ""}`}
>
<Link
href={`/login${pathname ? `?next=${encodeURIComponent(pathname)}` : ""}`}
>
Connect Wallet
</Link>
</Button>
</>
Connect Wallet
</Link>
</Button>
);
}
@@ -3,7 +3,8 @@
import type { StaticImageData } from "next/image";
import Image from "next/image";
import type { FetchDeployMetadataResult } from "thirdweb/contract";
import { serverThirdwebClient } from "@/constants/thirdweb-client.server";
import { DASHBOARD_THIRDWEB_SECRET_KEY } from "@/constants/server-envs";
import { getConfiguredThirdwebClient } from "@/constants/thirdweb.server";
import { replaceIpfsUrl } from "@/lib/sdk";
import generalContractIcon from "../../../../../public/assets/tw-icons/general.png";
@@ -26,7 +27,17 @@ export const ContractIdImage: React.FC<ContractIdImageProps> = ({
<img
alt=""
className="size-8 rounded-full"
src={replaceIpfsUrl(logo, serverThirdwebClient)}
src={
DASHBOARD_THIRDWEB_SECRET_KEY
? replaceIpfsUrl(
logo,
getConfiguredThirdwebClient({
secretKey: DASHBOARD_THIRDWEB_SECRET_KEY,
teamId: undefined,
}),
)
: logo
}
/>
);
}
@@ -314,7 +314,7 @@ const NetworkFilterCell = React.memo(function NetworkFilterCell({
client: ThirdwebClient;
}) {
if (chainIds.length < 2) {
return <> NETWORK </>;
return "NETWORK";
}
return (
@@ -46,9 +46,9 @@ function AnnouncementBannerUI(props: {
export function AnnouncementBanner() {
return (
<AnnouncementBannerUI
href="https://nebula.thirdweb.com/"
label="Announcing Nebula App — the most powerful AI for blockchains! Try it now!"
trackingLabel="nebula-beta-launch"
href="https://blog.thirdweb.com/the-fastest-way-to-build-web3-applications/"
label="We have re-branded our Engine, Universal Bridge, and Connect products. Please read the full blog post for details on changes"
trackingLabel="product-rebrand"
/>
);
}
@@ -40,10 +40,7 @@ import { projectDomainsSchema, projectNameSchema } from "@/schema/validations";
import { toArrFromList } from "@/utils/string";
const ALL_PROJECT_SERVICES = SERVICES.filter(
(srv) =>
srv.name !== "relayer" &&
srv.name !== "chainsaw" &&
srv.name !== "engineCloud", // TODO enable once API server is out
(srv) => srv.name !== "relayer" && srv.name !== "chainsaw",
);
export type CreateProjectPrefillOptions = {
@@ -0,0 +1,11 @@
"use client";
import * as CollapsiblePrimitive from "@radix-ui/react-collapsible";
const Collapsible = CollapsiblePrimitive.Root;
const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger;
const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent;
export { Collapsible, CollapsibleTrigger, CollapsibleContent };
@@ -50,9 +50,10 @@ const PaginationLink = ({
<Button
aria-current={isActive ? "page" : undefined}
className={cn(
"h-auto px-3 py-2",
"h-auto px-3 py-2 border border-transparent",
className,
props.disabled && "cursor-not-allowed",
!isActive && "border-border",
)}
variant={!isActive ? "outline" : "default"}
{...props}
@@ -3,7 +3,9 @@ import "server-only";
import { DASHBOARD_THIRDWEB_SECRET_KEY } from "./server-envs";
import { getConfiguredThirdwebClient } from "./thirdweb.server";
// During build time, the secret key might not be available
// Create a client that will work for build but may fail at runtime if secret key is needed
export const serverThirdwebClient = getConfiguredThirdwebClient({
secretKey: DASHBOARD_THIRDWEB_SECRET_KEY,
secretKey: DASHBOARD_THIRDWEB_SECRET_KEY || "dummy-build-time-secret",
teamId: undefined,
});
@@ -76,14 +76,18 @@ export function getConfiguredThirdwebClient(options: {
});
}
// During build time, provide fallbacks if credentials are missing
const clientId = NEXT_PUBLIC_DASHBOARD_CLIENT_ID || "dummy-build-client";
const secretKey = options.secretKey || undefined;
return createThirdwebClient({
clientId: NEXT_PUBLIC_DASHBOARD_CLIENT_ID,
clientId: clientId,
config: {
storage: {
gatewayUrl: NEXT_PUBLIC_IPFS_GATEWAY_URL,
},
},
secretKey: options.secretKey,
secretKey: secretKey,
teamId: options.teamId,
});
}
-7
View File
@@ -6,13 +6,6 @@ import { accountKeys, authorizedWallets } from "../query-keys/cache-keys";
// FIXME: We keep repeating types, API server should provide them
export const accountPlan = {
enterprise: "enterprise",
free: "free",
growth: "growth",
pro: "pro",
} as const;
export type AuthorizedWallet = {
id: string;
accountId: string;
@@ -22,6 +22,7 @@ const fetchAccountList = ({
headers: {
Authorization: `Bearer ${jwt}`,
"Content-Type": "application/json",
"x-client-id": clientId,
},
method: "GET",
});
+1 -1
View File
@@ -30,7 +30,7 @@ export const ChainIconClient = ({
fallback={<img alt="" src={fallbackChainIcon} />}
key={resolvedSrc}
loading={restProps.loading || "lazy"}
skeleton={<div className="animate-pulse rounded-full bg-border" />}
skeleton={<span className="animate-pulse rounded-full bg-border" />}
src={resolvedSrc}
/>
);
+5 -9
View File
@@ -192,6 +192,7 @@ export function teamSubscriptionsStub(
currentPeriodEnd: "2024-12-15T20:56:06.000Z",
currentPeriodStart: "2024-11-15T20:56:06.000Z",
id: "sub-1",
skus: [],
status: "active",
trialEnd: overrides?.trialEnd || null,
trialStart: null,
@@ -212,6 +213,7 @@ export function teamSubscriptionsStub(
currentPeriodEnd: "2024-12-15T20:56:06.000Z",
currentPeriodStart: "2024-11-15T20:56:15.000Z",
id: "sub-2",
skus: [],
status: "active",
trialEnd: null,
trialStart: null,
@@ -229,25 +231,19 @@ export function teamSubscriptionsStub(
// In-App Wallets
{
amount: usage.inAppWalletAmount?.amount || 0,
description: `${
usage.inAppWalletAmount?.quantity || 0
} x In-App Wallets (Tier 1 at $0.00 / month)`,
description: `${usage.inAppWalletAmount?.quantity || 0} x In-App Wallets (Tier 1 at $0.00 / month)`,
thirdwebSku: "usage:in_app_wallet",
},
// AA Sponsorship
{
amount: usage.aaSponsorshipAmount?.amount || 0,
description: `${
usage.aaSponsorshipAmount?.quantity || 0
} x AA Gas Sponsorship (at $0.011 / month)`,
description: `${usage.aaSponsorshipAmount?.quantity || 0} x AA Gas Sponsorship (at $0.011 / month)`,
thirdwebSku: "usage:aa_sponsorship",
},
// OP Grant
{
amount: usage.aaSponsorshipOpGrantAmount?.amount || 0,
description: `${
usage.aaSponsorshipOpGrantAmount?.quantity || 0
} x AA Gas Sponsorship (OP) (at $0.011 / month)`,
description: `${usage.aaSponsorshipOpGrantAmount?.quantity || 0} x AA Gas Sponsorship (OP) (at $0.011 / month)`,
thirdwebSku: "usage:aa_sponsorship_op_grant",
},
],
+1 -6
View File
@@ -39,12 +39,6 @@ export interface TransactionStats {
count: number;
}
export interface RpcMethodStats {
date: string;
evmMethod: string;
count: number;
}
export interface EngineCloudStats {
date: string;
chainId: string;
@@ -78,4 +72,5 @@ export interface AnalyticsQueryParams {
from?: Date;
to?: Date;
period?: "day" | "week" | "month" | "year" | "all";
limit?: number;
}
+5
View File
@@ -14,3 +14,8 @@ export type ProductSKU =
| "usage:aa_sponsorship"
| "usage:aa_sponsorship_op_grant"
| null;
export type ChainInfraSKU =
| "chain:infra:rpc"
| "chain:infra:insight"
| "chain:infra:account_abstraction";
-1
View File
@@ -23,7 +23,6 @@ export const TEAM_PLANS: Record<
features: [
"Email Support",
"48hr Guaranteed Response",
"Invite Team Members",
"Custom In-App Wallet Auth",
],
price: 99,
@@ -1,5 +1,7 @@
import { defineChain } from "thirdweb";
import { getChainMetadata } from "thirdweb/chains";
import { ExternalLinkIcon } from "lucide-react";
import Link from "next/link";
import { defineChain, getAddress, NATIVE_TOKEN_ADDRESS } from "thirdweb";
import { type ChainMetadata, getChainMetadata } from "thirdweb/chains";
import { Card, CardContent, CardHeader } from "@/components/ui/card";
import { serverThirdwebClient } from "@/constants/thirdweb-client.server";
import { resolveSchemeWithErrorHandler } from "@/utils/resolveSchemeWithErrorHandler";
@@ -53,7 +55,7 @@ export async function RouteListCard({
return (
<div className="relative h-full">
<Card className="h-full w-full transition-colors hover:border-active-border">
<Card className="h-full w-full">
<CardHeader className="flex flex-row items-center justify-between p-4">
<div className="flex flex-row items-center gap-2">
{resolvedOriginTokenIconUri ? (
@@ -80,32 +82,60 @@ export async function RouteListCard({
</CardHeader>
<CardContent className="px-4 pt-0 pb-4">
<table className="w-full">
<tbody className="text-sm [&_td>*]:min-h-[25px]">
<tr>
<th className="text-left font-normal text-base">
{originTokenName === "ETH"
? originChain.nativeCurrency.name
: originTokenName}
</th>
<td className="text-right text-base text-muted-foreground">
{originChain.name}
</td>
</tr>
<tr>
<th className="text-left font-normal text-base">
{destinationTokenName === "ETH"
? destinationChain.nativeCurrency.name
: destinationTokenName}
</th>
<td className="text-right text-base text-muted-foreground">
{destinationChain.name}
</td>
</tr>
</tbody>
</table>
<div className="flex flex-col gap-1">
<div className="flex justify-between gap-2">
<TokenName
chainMetadata={originChain}
tokenAddress={originTokenAddress}
tokenName={originTokenName}
/>
<div className="text-right text-base text-muted-foreground">
{originChain.name}
</div>
</div>
<div className="flex justify-between gap-2">
<TokenName
chainMetadata={destinationChain}
tokenAddress={destinationTokenAddress}
tokenName={destinationTokenName}
/>
<div className="text-right text-base text-muted-foreground">
{destinationChain.name}
</div>
</div>
</div>
</CardContent>
</Card>
</div>
);
}
const nativeTokenAddress = getAddress(NATIVE_TOKEN_ADDRESS);
function TokenName(props: {
tokenAddress: string;
tokenName: string;
chainMetadata: ChainMetadata;
}) {
const isERC20 = getAddress(props.tokenAddress) !== nativeTokenAddress;
if (isERC20) {
return (
<Link
className="flex items-center gap-1.5 font-normal text-base group/link hover:underline underline-offset-4 decoration-muted-foreground/50 decoration-dotted"
href={`https://thirdweb.com/${props.chainMetadata.slug}/${props.tokenAddress}`}
target="_blank"
>
{props.tokenName}
<ExternalLinkIcon className="size-3.5 text-muted-foreground group-hover/link:text-foreground" />
</Link>
);
}
return (
<div className="text-left font-normal text-base">
{props.chainMetadata.nativeCurrency.name}
</div>
);
}
@@ -1,5 +1,14 @@
import { defineChain, getChainMetadata } from "thirdweb/chains";
import { CopyTextButton } from "@/components/ui/CopyTextButton";
import { ExternalLinkIcon } from "lucide-react";
import Link from "next/link";
import { getAddress, NATIVE_TOKEN_ADDRESS } from "thirdweb";
import {
type ChainMetadata,
defineChain,
getChainMetadata,
} from "thirdweb/chains";
import { shortenAddress } from "thirdweb/utils";
import { Img } from "@/components/blocks/Img";
import { Button } from "@/components/ui/button";
import { TableCell, TableRow } from "@/components/ui/table";
import { serverThirdwebClient } from "@/constants/thirdweb-client.server";
import { resolveSchemeWithErrorHandler } from "@/utils/resolveSchemeWithErrorHandler";
@@ -52,37 +61,14 @@ export async function RouteListRow({
]);
return (
<TableRow className="group transition-colors hover:bg-accent/50" linkBox>
<TableRow className="group">
<TableCell>
<div className="flex flex-row items-center gap-3">
<div className="flex items-center gap-2">
{resolvedOriginTokenIconUri ? (
// For now we're using a normal img tag because the domain for these images is unknown
// eslint-disable-next-line @next/next/no-img-element
<img
alt={originTokenAddress}
className="size-7 rounded-full border border-border/50 shadow-sm transition-transform group-hover:scale-105"
src={resolvedOriginTokenIconUri}
/>
) : (
<div className="size-7 rounded-full bg-muted-foreground/20" />
)}
{originTokenSymbol && (
<CopyTextButton
className="relative z-10 font-medium text-base"
copyIconPosition="right"
textToCopy={originTokenAddress}
textToShow={
originTokenSymbol === "ETH"
? originChain.nativeCurrency.symbol
: originTokenSymbol
}
tooltip="Copy Token Address"
variant="ghost"
/>
)}
</div>
</div>
<TokenInfo
chainMetadata={originChain}
tokenAddress={originTokenAddress}
tokenIconUri={resolvedOriginTokenIconUri}
tokenSymbol={originTokenSymbol}
/>
</TableCell>
<TableCell className="text-muted-foreground/90">
@@ -90,34 +76,12 @@ export async function RouteListRow({
</TableCell>
<TableCell>
<div className="flex flex-row items-center gap-3">
<div className="flex items-center gap-2">
{resolvedDestinationTokenIconUri ? (
// eslint-disable-next-line @next/next/no-img-element
<img
alt={destinationTokenAddress}
className="size-7 rounded-full border border-border/50 shadow-sm transition-transform group-hover:scale-105"
src={resolvedDestinationTokenIconUri}
/>
) : (
<div className="size-7 rounded-full bg-muted-foreground/20" />
)}
{destinationTokenSymbol && (
<CopyTextButton
className="relative z-10 font-medium text-base"
copyIconPosition="right"
textToCopy={destinationTokenAddress}
textToShow={
destinationTokenSymbol === "ETH"
? destinationChain.nativeCurrency.symbol
: destinationTokenSymbol
}
tooltip="Copy Token Address"
variant="ghost"
/>
)}
</div>
</div>
<TokenInfo
chainMetadata={destinationChain}
tokenAddress={destinationTokenAddress}
tokenIconUri={resolvedDestinationTokenIconUri}
tokenSymbol={destinationTokenSymbol}
/>
</TableCell>
<TableCell className="text-muted-foreground/90">
@@ -126,3 +90,47 @@ export async function RouteListRow({
</TableRow>
);
}
const nativeTokenAddress = getAddress(NATIVE_TOKEN_ADDRESS);
function TokenInfo(props: {
tokenAddress: string;
tokenSymbol: string | undefined;
chainMetadata: ChainMetadata;
tokenIconUri: string | undefined;
}) {
const isERC20 = getAddress(props.tokenAddress) !== nativeTokenAddress;
return (
<div className="flex items-center gap-1.5">
{props.tokenIconUri ? (
<Img
alt={props.tokenAddress}
className="size-7 rounded-full border border-border/50"
src={props.tokenIconUri}
/>
) : (
<div className="size-7 rounded-full bg-muted-foreground/20" />
)}
{isERC20 ? (
<Button
asChild
className="h-auto w-auto py-1 px-1.5 gap-2"
variant="ghost"
>
<Link
href={`https://thirdweb.com/${props.chainMetadata.slug}/${props.tokenAddress}`}
target="_blank"
>
{props.tokenSymbol || shortenAddress(props.tokenAddress)}
<ExternalLinkIcon className="size-3.5 text-muted-foreground" />
</Link>
</Button>
) : (
<span className="font-medium text-sm px-1.5 py-1">
{props.chainMetadata.nativeCurrency.symbol}
</span>
)}
</div>
);
}
@@ -86,22 +86,14 @@ export async function RoutesData(props: {
<p className="text-2xl">No Results found</p>
</div>
) : props.activeView === "table" ? (
<TableContainer className="overflow-hidden rounded-xl border border-border/50 bg-card/50 shadow-sm transition-all">
<TableContainer>
<Table>
<TableHeader className="z-0">
<TableRow className="border-border/50 border-b bg-muted/50">
<TableHead className="py-4 font-medium text-muted-foreground/80 text-xs uppercase tracking-wider">
Origin Token
</TableHead>
<TableHead className="py-4 font-medium text-muted-foreground/80 text-xs uppercase tracking-wider">
Origin Chain
</TableHead>
<TableHead className="py-4 font-medium text-muted-foreground/80 text-xs uppercase tracking-wider">
Destination Token
</TableHead>
<TableHead className="py-4 font-medium text-muted-foreground/80 text-xs uppercase tracking-wider">
Destination Chain
</TableHead>
<TableHeader>
<TableRow>
<TableHead>Origin Token</TableHead>
<TableHead>Origin Chain</TableHead>
<TableHead>Destination Token</TableHead>
<TableHead>Destination Chain</TableHead>
</TableRow>
</TableHeader>
<TableBody>
@@ -1,7 +1,8 @@
import { ImageResponse } from "next/og";
import { useId } from "react";
import { download } from "thirdweb/storage";
import { serverThirdwebClient } from "@/constants/thirdweb-client.server";
import { DASHBOARD_THIRDWEB_SECRET_KEY } from "@/constants/server-envs";
import { getConfiguredThirdwebClient } from "@/constants/thirdweb.server";
import { fetchChain } from "@/utils/fetchChain";
// Route segment config
@@ -81,16 +82,29 @@ export default async function Image({
fetch(new URL("og-lib/fonts/inter/700.ttf", import.meta.url)).then((res) =>
res.arrayBuffer(),
),
// download the chain icon if there is one
chain.icon?.url && hasWorkingChainIcon
? download({
client: serverThirdwebClient,
uri: chain.icon.url,
}).then((res) => res.arrayBuffer())
// download the chain icon if there is one and secret key is available
chain.icon?.url && hasWorkingChainIcon && DASHBOARD_THIRDWEB_SECRET_KEY
? (async () => {
try {
const client = getConfiguredThirdwebClient({
secretKey: DASHBOARD_THIRDWEB_SECRET_KEY,
teamId: undefined,
});
const response = await download({
client,
uri: chain.icon?.url || "",
});
return response.arrayBuffer();
} catch (error) {
// If download fails, return undefined to fallback to no icon
console.warn("Failed to download chain icon:", error);
return undefined;
}
})()
: undefined,
// download the background image (based on chain)
fetch(
chain.icon?.url && hasWorkingChainIcon
chain.icon?.url && hasWorkingChainIcon && DASHBOARD_THIRDWEB_SECRET_KEY
? new URL(
"og-lib/assets/chain/bg-with-icon.png",
@@ -118,7 +132,7 @@ export default async function Image({
/>
{/* the actual component starts here */}
{hasWorkingChainIcon && (
{hasWorkingChainIcon && chainIcon && (
<img
alt=""
// @ts-expect-error - TS doesn't know about the ImageResponse component
@@ -1,4 +1,5 @@
// biome-ignore-all lint/nursery/noNestedComponentDefinitions: TODO
/** biome-ignore-all lint/a11y/useSemanticElements: FIXME */
import {
IconButton,
@@ -256,7 +257,6 @@ export const MarketplaceTable: React.FC<MarketplaceTableProps> = ({
// biome-ignore lint/suspicious/noArrayIndexKey: FIXME
key={rowIndex}
onClick={() => setTokenRow(row.original)}
// biome-ignore lint/a11y/useSemanticElements: FIXME
role="group"
style={{ cursor: "pointer" }}
>
@@ -130,6 +130,7 @@ const getClaimConditionTypeFromPhase = (
if (phase.snapshot) {
if (
phase.maxClaimablePerWallet?.toString() === "0" &&
phase.price === "0" &&
typeof phase.snapshot !== "string" &&
phase.snapshot.length === 1 &&
@@ -464,204 +465,198 @@ export const ClaimConditionsForm: React.FC<ClaimConditionsFormProps> = ({
}
return (
<>
<Flex as="form" direction="column" gap={10} onSubmit={handleFormSubmit}>
<Flex direction="column" gap={6}>
{/* Show the reason why the form is disabled */}
{!isAdmin && (
<Text>Connect with admin wallet to edit claim conditions.</Text>
)}
{controlledFields.map((field, index) => {
const dropType: DropType = field.snapshot
? field.maxClaimablePerWallet?.toString() === "0"
? "specific"
: "overrides"
: "any";
<Flex as="form" direction="column" gap={10} onSubmit={handleFormSubmit}>
<Flex direction="column" gap={6}>
{/* Show the reason why the form is disabled */}
{!isAdmin && (
<Text>Connect with admin wallet to edit claim conditions.</Text>
)}
{controlledFields.map((field, index) => {
const dropType: DropType = field.snapshot
? field.maxClaimablePerWallet?.toString() === "0"
? "specific"
: "overrides"
: "any";
const claimConditionType = getClaimConditionTypeFromPhase(field);
const claimConditionType = getClaimConditionTypeFromPhase(field);
const isActive = activePhaseId === field.id;
const isActive = activePhaseId === field.id;
const snapshotValue = field.snapshot?.map((v) =>
typeof v === "string"
? {
address: v,
currencyAddress: ZERO_ADDRESS,
maxClaimable: "unlimited",
price: "unlimited",
}
: {
...v,
currencyAddress: v?.currencyAddress || ZERO_ADDRESS,
maxClaimable: v?.maxClaimable?.toString() || "unlimited",
price: v?.price?.toString() || "unlimited",
},
);
const snapshotValue = field.snapshot?.map((v) =>
typeof v === "string"
? {
address: v,
currencyAddress: ZERO_ADDRESS,
maxClaimable: "unlimited",
price: "unlimited",
}
: {
...v,
currencyAddress: v?.currencyAddress || ZERO_ADDRESS,
maxClaimable: v?.maxClaimable?.toString() || "unlimited",
price: v?.price?.toString() || "unlimited",
},
);
return (
<Fragment key={`snapshot_${field.id}_${index}`}>
<SnapshotViewerSheet
client={contract.client}
dropType={dropType}
isDisabled={!canEditForm}
isOpen={openSnapshotIndex === index}
onClose={() => {
setOpenSnapshotIndex(-1);
}}
setSnapshot={(snapshot) =>
form.setValue(`phases.${index}.snapshot`, snapshot)
}
value={snapshotValue}
/>
return (
<Fragment key={`snapshot_${field.id}_${index}`}>
<SnapshotViewerSheet
client={contract.client}
dropType={dropType}
isDisabled={!canEditForm}
isOpen={openSnapshotIndex === index}
onClose={() => {
setOpenSnapshotIndex(-1);
}}
setSnapshot={(snapshot) =>
form.setValue(`phases.${index}.snapshot`, snapshot)
}
value={snapshotValue}
/>
<ClaimsConditionFormContext.Provider
value={{
claimConditionType,
dropType,
field,
form,
formDisabled: !canEditForm,
isActive,
isAdmin,
isColumn,
isErc20,
isMultiPhase,
phaseIndex: index,
setOpenSnapshotIndex,
tokenDecimals: tokenDecimals.data,
}}
>
<ClaimConditionsPhase
contract={contract}
isPending={sendTx.isPending}
onRemove={() => {
removePhase(index);
}}
/>
</ClaimsConditionFormContext.Provider>
</Fragment>
);
})}
{phases?.length === 0 && (
<Alert borderRadius="md" status="warning">
<AlertIcon />
<div className="flex flex-col">
<AlertTitle as={Heading} size="label.lg">
{isMultiPhase
? "Missing Claim Phases"
: "Missing Claim Conditions"}
</AlertTitle>
<AlertDescription as={Text}>
{isMultiPhase
? "You need to set at least one claim phase for people to claim this drop."
: "You need to set claim conditions for people to claim this drop."}
</AlertDescription>
</div>
</Alert>
)}
<Flex
flexDir={{ base: "column", md: "row" }}
gap={2}
justifyContent="space-between"
>
<div className="flex flex-row gap-2">
<AdminOnly contract={contract}>
<Menu>
<MenuButton
as={Button}
borderRadius="md"
colorScheme="primary"
isDisabled={
sendTx.isPending || (!isMultiPhase && phases?.length > 0)
}
leftIcon={<PlusIcon className="size-5" />}
size="sm"
variant={phases?.length > 0 ? "outline" : "solid"}
>
Add {isMultiPhase ? "Phase" : "Claim Conditions"}
</MenuButton>
<MenuList
borderRadius="lg"
overflow="hidden"
zIndex="overlay"
>
{Object.keys(ClaimConditionTypeData).map((key) => {
const type = key as ClaimConditionType;
if (type === "custom") {
return null;
}
return (
<MenuItem
key={type}
onClick={() => {
addPhase(type);
// TODO: Automatically start editing the new phase after adding it
}}
>
<div className="flex items-center gap-1">
{ClaimConditionTypeData[type].name}
<TooltipBox
content={
<Text size="body.md">
{ClaimConditionTypeData[type].description}
</Text>
}
/>
</div>
</MenuItem>
);
})}
</MenuList>
</Menu>
</AdminOnly>
{controlledFields.some((field) => field.fromSdk) && (
<ResetClaimEligibility
<ClaimsConditionFormContext.Provider
value={{
claimConditionType,
dropType,
field,
form,
formDisabled: !canEditForm,
isActive,
isAdmin,
isColumn,
isErc20,
isMultiPhase,
phaseIndex: index,
setOpenSnapshotIndex,
tokenDecimals: tokenDecimals.data,
}}
>
<ClaimConditionsPhase
contract={contract}
isErc20={isErc20}
isLoggedIn={isLoggedIn}
isMultiphase={isMultiPhase}
tokenId={tokenId}
isPending={sendTx.isPending}
onRemove={() => {
removePhase(index);
}}
/>
)}
</div>
</ClaimsConditionFormContext.Provider>
</Fragment>
);
})}
<div className="flex flex-row">
<AdminOnly contract={contract} fallback={<Box pb={5} />}>
<Flex alignItems="center" gap={3} justifyContent="center">
{(hasRemovedPhases || hasAddedPhases) && (
<Text color="red.500" fontWeight="bold">
You have unsaved changes
</Text>
)}
{controlledFields.length > 0 ||
hasRemovedPhases ||
!isMultiPhase ? (
<TransactionButton
client={contract.client}
disabled={claimConditionsQuery.isPending}
isLoggedIn={isLoggedIn}
isPending={sendTx.isPending}
transactionCount={1}
txChainID={contract.chain.id}
type="submit"
>
{claimConditionsQuery.isPending
? "Saving Phases"
: "Save Phases"}
</TransactionButton>
) : null}
</Flex>
</AdminOnly>
{phases?.length === 0 && (
<Alert borderRadius="md" status="warning">
<AlertIcon />
<div className="flex flex-col">
<AlertTitle as={Heading} size="label.lg">
{isMultiPhase
? "Missing Claim Phases"
: "Missing Claim Conditions"}
</AlertTitle>
<AlertDescription as={Text}>
{isMultiPhase
? "You need to set at least one claim phase for people to claim this drop."
: "You need to set claim conditions for people to claim this drop."}
</AlertDescription>
</div>
</Flex>
</Alert>
)}
<Flex
flexDir={{ base: "column", md: "row" }}
gap={2}
justifyContent="space-between"
>
<div className="flex flex-row gap-2">
<AdminOnly contract={contract}>
<Menu>
<MenuButton
as={Button}
borderRadius="md"
colorScheme="primary"
isDisabled={
sendTx.isPending || (!isMultiPhase && phases?.length > 0)
}
leftIcon={<PlusIcon className="size-5" />}
size="sm"
variant={phases?.length > 0 ? "outline" : "solid"}
>
Add {isMultiPhase ? "Phase" : "Claim Conditions"}
</MenuButton>
<MenuList borderRadius="lg" overflow="hidden" zIndex="overlay">
{Object.keys(ClaimConditionTypeData).map((key) => {
const type = key as ClaimConditionType;
if (type === "custom") {
return null;
}
return (
<MenuItem
key={type}
onClick={() => {
addPhase(type);
// TODO: Automatically start editing the new phase after adding it
}}
>
<div className="flex items-center gap-1">
{ClaimConditionTypeData[type].name}
<TooltipBox
content={
<Text size="body.md">
{ClaimConditionTypeData[type].description}
</Text>
}
/>
</div>
</MenuItem>
);
})}
</MenuList>
</Menu>
</AdminOnly>
{controlledFields.some((field) => field.fromSdk) && (
<ResetClaimEligibility
contract={contract}
isErc20={isErc20}
isLoggedIn={isLoggedIn}
isMultiphase={isMultiPhase}
tokenId={tokenId}
/>
)}
</div>
<div className="flex flex-row">
<AdminOnly contract={contract} fallback={<Box pb={5} />}>
<Flex alignItems="center" gap={3} justifyContent="center">
{(hasRemovedPhases || hasAddedPhases) && (
<Text color="red.500" fontWeight="bold">
You have unsaved changes
</Text>
)}
{controlledFields.length > 0 ||
hasRemovedPhases ||
!isMultiPhase ? (
<TransactionButton
client={contract.client}
disabled={claimConditionsQuery.isPending}
isLoggedIn={isLoggedIn}
isPending={sendTx.isPending}
transactionCount={1}
txChainID={contract.chain.id}
type="submit"
>
{claimConditionsQuery.isPending
? "Saving Phases"
: "Save Phases"}
</TransactionButton>
) : null}
</Flex>
</AdminOnly>
</div>
</Flex>
</Flex>
</>
</Flex>
);
};
@@ -1,6 +1,7 @@
import { getAddress, getContract, isAddress } from "thirdweb";
import { localhost } from "thirdweb/chains";
import { serverThirdwebClient } from "@/constants/thirdweb-client.server";
import { DASHBOARD_THIRDWEB_SECRET_KEY } from "@/constants/server-envs";
import { getConfiguredThirdwebClient } from "@/constants/thirdweb.server";
import { mapV4ChainToV5Chain } from "@/utils/map-chains";
import { getUserThirdwebClient } from "../../../../../../../@/api/auth-token";
import { fetchChainWithLocalOverrides } from "../../../../../../../@/utils/fetchChainWithLocalOverrides";
@@ -18,13 +19,21 @@ export async function getContractPageParamsInfo(params: {
return undefined;
}
// attempt to get the auth token
// Create server client only if secret key is available
if (!DASHBOARD_THIRDWEB_SECRET_KEY) {
return undefined;
}
const serverClient = getConfiguredThirdwebClient({
secretKey: DASHBOARD_THIRDWEB_SECRET_KEY,
teamId: undefined,
});
const serverContract = getContract({
address: contractAddress,
// eslint-disable-next-line no-restricted-syntax
chain: mapV4ChainToV5Chain(chainMetadata),
client: serverThirdwebClient,
client: serverClient,
});
const clientContract = getContract({
@@ -1,4 +1,5 @@
/** biome-ignore-all lint/nursery/noNestedComponentDefinitions: FIXME */
/** biome-ignore-all lint/a11y/useSemanticElements: FIXME */
"use client";
import {
@@ -320,7 +321,6 @@ export const NFTGetAllTable: React.FC<ContractOverviewNFTGetAllProps> = ({
}}
opacity={failedToLoad ? 0.3 : 1}
pointerEvents={failedToLoad ? "none" : "auto"}
// biome-ignore lint/a11y/useSemanticElements: FIXME
role="group"
style={{ cursor: "pointer" }}
>
@@ -9,19 +9,23 @@ export async function getContractCreator(
contract: ThirdwebContract,
functionSelectors: string[],
) {
if (isOwnerSupported(functionSelectors)) {
return owner({
contract,
});
}
try {
if (isOwnerSupported(functionSelectors)) {
return await owner({
contract,
});
}
if (isGetRoleAdminSupported(functionSelectors)) {
return getRoleMember({
contract,
index: BigInt(0),
role: "admin",
});
}
if (isGetRoleAdminSupported(functionSelectors)) {
return await getRoleMember({
contract,
index: BigInt(0),
role: "admin",
});
}
return null;
return null;
} catch {
return null;
}
}
@@ -1,7 +1,7 @@
import { ExternalLinkIcon, GlobeIcon, Settings2Icon } from "lucide-react";
import Link from "next/link";
import { useMemo } from "react";
import type { ThirdwebContract } from "thirdweb";
import { type ThirdwebContract, ZERO_ADDRESS } from "thirdweb";
import type { ChainMetadata } from "thirdweb/chains";
import { Img } from "@/components/blocks/Img";
import { Button } from "@/components/ui/button";
@@ -149,7 +149,7 @@ export function ContractHeaderUI(props: {
{/* bottom row */}
<div className="flex flex-row flex-wrap items-center gap-2">
{props.contractCreator && (
{props.contractCreator && props.contractCreator !== ZERO_ADDRESS && (
<ContractCreatorBadge
clientContract={props.clientContract}
contractCreator={props.contractCreator}
@@ -7,10 +7,18 @@ export async function fetchTokenInfoFromBridge(params: {
}) {
try {
const res = await fetch(
`https://bridge.${isProd ? "thirdweb.com" : "thirdweb-dev.com"}/v1/tokens?chainId=${params.chainId}&tokenAddress=${params.tokenAddress}&clientId=${params.clientId}`,
`https://bridge.${isProd ? "thirdweb.com" : "thirdweb-dev.com"}/v1/tokens?chainId=${params.chainId}&tokenAddress=${params.tokenAddress}`,
{
headers: {
"x-client-id": params.clientId,
},
},
);
if (!res.ok) {
console.error(
`Failed to fetch token info from bridge: ${await res.text()}`,
);
return null;
}
@@ -1,3 +1,4 @@
/** biome-ignore-all lint/a11y/useSemanticElements: EXPECTED */
"use client";
import { CheckCheckIcon } from "lucide-react";
@@ -210,7 +211,6 @@ function NFTCard(props: {
}
: undefined
}
// biome-ignore lint/a11y/useSemanticElements: FIXME
role="button"
tabIndex={props.onClick ? 0 : undefined}
>
@@ -43,94 +43,92 @@ export const TokenAirdropForm: React.FC<TokenAirdropFormProps> = ({
);
return (
<>
<div className="pt-3">
<form
onSubmit={handleSubmit(async (data) => {
try {
const tx = transferBatch({
batch: data.addresses
.filter((address) => address.quantity !== undefined)
.map((address) => ({
amount: address.quantity,
to: address.address,
})),
contract,
});
await sendTransaction.mutateAsync(tx, {
onError: (error) => {
console.error(error);
},
onSuccess: () => {
// Close the sheet/modal on success
if (toggle) {
toggle(false);
}
},
});
airdropNotifications.onSuccess();
} catch (err) {
airdropNotifications.onError(err);
console.error(err);
}
})}
>
<div className="mb-3 flex w-full flex-col gap-6 md:flex-row">
{airdropFormOpen ? (
<AirdropUpload
client={contract.client}
onClose={() => setAirdropFormOpen(false)}
setAirdrop={(value) =>
setValue("addresses", value, { shouldDirty: true })
<div className="pt-3">
<form
onSubmit={handleSubmit(async (data) => {
try {
const tx = transferBatch({
batch: data.addresses
.filter((address) => address.quantity !== undefined)
.map((address) => ({
amount: address.quantity,
to: address.address,
})),
contract,
});
await sendTransaction.mutateAsync(tx, {
onError: (error) => {
console.error(error);
},
onSuccess: () => {
// Close the sheet/modal on success
if (toggle) {
toggle(false);
}
/>
) : (
<div className="flex flex-col gap-4 md:flex-row">
<Button
borderRadius="md"
colorScheme="primary"
onClick={() => setAirdropFormOpen(true)}
rightIcon={<UploadIcon className="size-4" />}
>
Upload addresses
</Button>
{addresses.length > 0 && (
<div className="flex flex-row items-center justify-center gap-2 text-green-500">
<CircleCheckIcon className="text-green-500" size={16} />
<Text color="inherit" size="body.sm">
<strong>{addresses.length} addresses</strong> ready to be
airdropped
</Text>
</div>
)}
</div>
)}
</div>
{addresses?.length > 0 && !airdropFormOpen && (
<>
{estimateGasCost && (
<Text>
This transaction requires at least {estimateGasCost} gas.
Since each chain has a different gas limit, please split this
operation into multiple transactions if necessary. Usually
under 10M gas is safe.
</Text>
)}
<TransactionButton
className="self-end"
client={contract.client}
isLoggedIn={isLoggedIn}
isPending={sendTransaction.isPending}
transactionCount={1}
txChainID={contract.chain.id}
type="submit"
},
});
airdropNotifications.onSuccess();
} catch (err) {
airdropNotifications.onError(err);
console.error(err);
}
})}
>
<div className="mb-3 flex w-full flex-col gap-6 md:flex-row">
{airdropFormOpen ? (
<AirdropUpload
client={contract.client}
onClose={() => setAirdropFormOpen(false)}
setAirdrop={(value) =>
setValue("addresses", value, { shouldDirty: true })
}
/>
) : (
<div className="flex flex-col gap-4 md:flex-row">
<Button
borderRadius="md"
colorScheme="primary"
onClick={() => setAirdropFormOpen(true)}
rightIcon={<UploadIcon className="size-4" />}
>
Airdrop
</TransactionButton>
</>
Upload addresses
</Button>
{addresses.length > 0 && (
<div className="flex flex-row items-center justify-center gap-2 text-green-500">
<CircleCheckIcon className="text-green-500" size={16} />
<Text color="inherit" size="body.sm">
<strong>{addresses.length} addresses</strong> ready to be
airdropped
</Text>
</div>
)}
</div>
)}
</form>
</div>
</>
</div>
{addresses?.length > 0 && !airdropFormOpen && (
<>
{estimateGasCost && (
<Text>
This transaction requires at least {estimateGasCost} gas. Since
each chain has a different gas limit, please split this
operation into multiple transactions if necessary. Usually under
10M gas is safe.
</Text>
)}
<TransactionButton
className="self-end"
client={contract.client}
isLoggedIn={isLoggedIn}
isPending={sendTransaction.isPending}
transactionCount={1}
txChainID={contract.chain.id}
type="submit"
>
Airdrop
</TransactionButton>
</>
)}
</form>
</div>
);
};
@@ -1,7 +1,7 @@
/* eslint-disable @next/next/no-img-element */
import "server-only";
import { DASHBOARD_THIRDWEB_SECRET_KEY } from "@/constants/server-envs";
import { serverThirdwebClient } from "@/constants/thirdweb-client.server";
import { getConfiguredThirdwebClient } from "@/constants/thirdweb.server";
import { cn } from "@/lib/utils";
import { resolveSchemeWithErrorHandler } from "@/utils/resolveSchemeWithErrorHandler";
import { fallbackChainIcon } from "../../../../../../@/utils/chain-icons";
@@ -13,10 +13,16 @@ export async function ChainIcon(props: {
if (props.iconUrl) {
let imageLink = fallbackChainIcon;
const resolved = resolveSchemeWithErrorHandler({
client: serverThirdwebClient,
uri: props.iconUrl,
});
// Only resolve if we have a secret key available
const resolved = DASHBOARD_THIRDWEB_SECRET_KEY
? resolveSchemeWithErrorHandler({
client: getConfiguredThirdwebClient({
secretKey: DASHBOARD_THIRDWEB_SECRET_KEY,
teamId: undefined,
}),
uri: props.iconUrl,
})
: null;
if (resolved) {
// check if it loads or not
@@ -5,7 +5,7 @@ import { Suspense } from "react";
import {
ContractCard,
ContractCardSkeleton,
} from "../../../../../../@/components/contracts/contract-card";
} from "@/components/contracts/contract-card";
interface ContractRowProps {
category: ExploreCategory;
@@ -182,19 +182,17 @@ function ContractTableRow(props: { row: Row<ContractDataRow> }) {
const { row } = props;
const { key, ...rowProps } = row.getRowProps();
return (
<>
<TableRow
className="cursor-pointer hover:bg-card"
linkBox
{...rowProps}
key={key}
>
{row.cells.map((cell) => (
<TableCell {...cell.getCellProps()} key={cell.getCellProps().key}>
{cell.render("Cell")}
</TableCell>
))}
</TableRow>
</>
<TableRow
className="cursor-pointer hover:bg-card"
linkBox
{...rowProps}
key={key}
>
{row.cells.map((cell) => (
<TableCell {...cell.getCellProps()} key={cell.getCellProps().key}>
{cell.render("Cell")}
</TableCell>
))}
</TableRow>
);
}
@@ -2,7 +2,8 @@ import { notFound } from "next/navigation";
import { ImageResponse } from "next/og";
import { resolveAvatar } from "thirdweb/extensions/ens";
import { GradientBlobbie } from "@/components/blocks/avatar/GradientBlobbie";
import { serverThirdwebClient } from "@/constants/thirdweb-client.server";
import { DASHBOARD_THIRDWEB_SECRET_KEY } from "@/constants/server-envs";
import { getConfiguredThirdwebClient } from "@/constants/thirdweb.server";
/* eslint-disable @next/next/no-img-element */
import { resolveSchemeWithErrorHandler } from "@/utils/resolveSchemeWithErrorHandler";
import { shortenIfAddress } from "@/utils/usedapp-external";
@@ -23,10 +24,18 @@ type PageProps = {
export default async function Image(props: PageProps) {
const params = await props.params;
const resolvedInfo = await resolveAddressAndEns(
params.addressOrEns,
serverThirdwebClient,
);
// Create client only if secret key is available
if (!DASHBOARD_THIRDWEB_SECRET_KEY) {
notFound();
}
const client = getConfiguredThirdwebClient({
secretKey: DASHBOARD_THIRDWEB_SECRET_KEY,
teamId: undefined,
});
const resolvedInfo = await resolveAddressAndEns(params.addressOrEns, client);
if (!resolvedInfo) {
notFound();
@@ -43,14 +52,14 @@ export default async function Image(props: PageProps) {
const ensImage = resolvedInfo.ensName
? await resolveAvatar({
client: serverThirdwebClient,
client,
name: resolvedInfo.ensName,
})
: null;
const resolvedENSImageSrc = ensImage
? resolveSchemeWithErrorHandler({
client: serverThirdwebClient,
client,
uri: ensImage,
})
: null;
@@ -1,6 +1,7 @@
import { format } from "date-fns";
import { getSocialProfiles } from "thirdweb/social";
import { serverThirdwebClient } from "@/constants/thirdweb-client.server";
import { DASHBOARD_THIRDWEB_SECRET_KEY } from "@/constants/server-envs";
import { getConfiguredThirdwebClient } from "@/constants/thirdweb.server";
import { resolveEns } from "@/lib/ens";
import { correctAndUniqueLicenses } from "@/lib/licenses";
import { getPublishedContractsWithPublisherMapping } from "../utils/getPublishedContractsWithPublisherMapping";
@@ -22,17 +23,25 @@ export default async function Image(props: {
}) {
const { publisher, contract_id } = props.params;
// Create client only if secret key is available
if (!DASHBOARD_THIRDWEB_SECRET_KEY) {
return null;
}
const client = getConfiguredThirdwebClient({
secretKey: DASHBOARD_THIRDWEB_SECRET_KEY,
teamId: undefined,
});
const [publishedContracts, socialProfiles] = await Promise.all([
getPublishedContractsWithPublisherMapping({
client: serverThirdwebClient,
client,
contract_id: contract_id,
publisher: publisher,
}),
getSocialProfiles({
address:
(await resolveEns(publisher, serverThirdwebClient)).address ||
publisher,
client: serverThirdwebClient,
address: (await resolveEns(publisher, client)).address || publisher,
client,
}),
]);
@@ -1,6 +1,7 @@
import { format } from "date-fns";
import { getSocialProfiles } from "thirdweb/social";
import { serverThirdwebClient } from "@/constants/thirdweb-client.server";
import { DASHBOARD_THIRDWEB_SECRET_KEY } from "@/constants/server-envs";
import { getConfiguredThirdwebClient } from "@/constants/thirdweb.server";
import { resolveEns } from "@/lib/ens";
import { correctAndUniqueLicenses } from "@/lib/licenses";
import { getLatestPublishedContractsWithPublisherMapping } from "./utils/getPublishedContractsWithPublisherMapping";
@@ -21,17 +22,25 @@ export default async function Image(props: {
}) {
const { publisher, contract_id } = props.params;
// Create client only if secret key is available
if (!DASHBOARD_THIRDWEB_SECRET_KEY) {
return null;
}
const client = getConfiguredThirdwebClient({
secretKey: DASHBOARD_THIRDWEB_SECRET_KEY,
teamId: undefined,
});
const [publishedContract, socialProfiles] = await Promise.all([
getLatestPublishedContractsWithPublisherMapping({
client: serverThirdwebClient,
client,
contract_id: contract_id,
publisher: publisher,
}),
getSocialProfiles({
address:
(await resolveEns(publisher, serverThirdwebClient)).address ||
publisher,
client: serverThirdwebClient,
address: (await resolveEns(publisher, client)).address || publisher,
client,
}),
]);
@@ -4,7 +4,8 @@ import { ImageResponse } from "next/og";
import { isAddress } from "thirdweb";
import { download } from "thirdweb/storage";
import { shortenAddress } from "thirdweb/utils";
import { serverThirdwebClient } from "@/constants/thirdweb-client.server";
import { DASHBOARD_THIRDWEB_SECRET_KEY } from "@/constants/server-envs";
import { getConfiguredThirdwebClient } from "@/constants/thirdweb.server";
const OgBrandIcon: React.FC = () => (
// biome-ignore lint/a11y/noSvgWithoutTitle: not needed
@@ -187,17 +188,41 @@ export async function publishedContractOGImageTemplate(params: {
ibmPlexMono500_,
ibmPlexMono700_,
image,
params.logo
? download({
client: serverThirdwebClient,
uri: params.logo,
}).then((res) => res.arrayBuffer())
params.logo && DASHBOARD_THIRDWEB_SECRET_KEY
? (async () => {
try {
const client = getConfiguredThirdwebClient({
secretKey: DASHBOARD_THIRDWEB_SECRET_KEY,
teamId: undefined,
});
const response = await download({
client,
uri: params.logo || "",
});
return response.arrayBuffer();
} catch (error) {
console.warn("Failed to download logo:", error);
return undefined;
}
})()
: undefined,
params.publisherAvatar
? download({
client: serverThirdwebClient,
uri: params.publisherAvatar,
}).then((res) => res.arrayBuffer())
params.publisherAvatar && DASHBOARD_THIRDWEB_SECRET_KEY
? (async () => {
try {
const client = getConfiguredThirdwebClient({
secretKey: DASHBOARD_THIRDWEB_SECRET_KEY,
teamId: undefined,
});
const response = await download({
client,
uri: params.publisherAvatar || "",
});
return response.arrayBuffer();
} catch (error) {
console.warn("Failed to download avatar:", error);
return undefined;
}
})()
: undefined,
]);
@@ -21,37 +21,33 @@ export const SupportForm_SelectInput = (props: Props) => {
const { options, formLabel, name, required, promptText } = props;
return (
<>
<div className="flex flex-col items-start gap-2">
<Label className="relative" htmlFor={name}>
{formLabel}
{required && (
<span className="-top-1.5 -right-2 absolute text-destructive">
•
</span>
)}
</Label>
<div className="flex flex-col items-start gap-2">
<Label className="relative" htmlFor={name}>
{formLabel}
{required && (
<span className="-top-1.5 -right-2 absolute text-destructive">•</span>
)}
</Label>
<Select
name={name}
onValueChange={(val) => {
props.onValueChange(val);
}}
required={required}
value={props.value}
>
<SelectTrigger>
<SelectValue placeholder={promptText}>{props.value}</SelectValue>
</SelectTrigger>
<SelectContent>
{options.map((option) => (
<SelectItem key={option} value={option}>
{option}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</>
<Select
name={name}
onValueChange={(val) => {
props.onValueChange(val);
}}
required={required}
value={props.value}
>
<SelectTrigger>
<SelectValue placeholder={promptText}>{props.value}</SelectValue>
</SelectTrigger>
<SelectContent>
{options.map((option) => (
<SelectItem key={option} value={option}>
{option}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
);
};
@@ -27,34 +27,32 @@ export const SupportForm_TeamSelection = (props: Props) => {
const teamId = useId();
return (
<>
<div className="flex flex-col items-start gap-2">
<Label className="relative" htmlFor={teamId}>
Select Team
<span className="-top-1.5 -right-2 absolute text-destructive">•</span>
</Label>
<div className="flex flex-col items-start gap-2">
<Label className="relative" htmlFor={teamId}>
Select Team
<span className="-top-1.5 -right-2 absolute text-destructive">•</span>
</Label>
<Select
name="teamId"
onValueChange={(selectedId) => {
props.onChange(selectedId);
}}
value={props.selectedTeamId}
>
<SelectTrigger id={teamId}>
<SelectValue placeholder="Select a Team">
{selectedTeamName}
</SelectValue>
</SelectTrigger>
<SelectContent>
{props.teams.map((team) => (
<SelectItem key={team.id} value={team.id}>
{team.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</>
<Select
name="teamId"
onValueChange={(selectedId) => {
props.onChange(selectedId);
}}
value={props.selectedTeamId}
>
<SelectTrigger id={teamId}>
<SelectValue placeholder="Select a Team">
{selectedTeamName}
</SelectValue>
</SelectTrigger>
<SelectContent>
{props.teams.map((team) => (
<SelectItem key={team.id} value={team.id}>
{team.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
);
};
@@ -76,7 +76,7 @@ export function AccountButton(props: {
className="justify-start px-3 text-muted-foreground text-sm hover:text-foreground"
variant="ghost"
>
<Link href="/team/~/~/settings/billing">Manage Billing</Link>
<Link href="/team/~/~/billing">Manage Billing</Link>
</Button>
<Button
@@ -1,7 +1,8 @@
import { ImageResponse } from "next/og";
import { useId } from "react";
import { download } from "thirdweb/storage";
import { serverThirdwebClient } from "@/constants/thirdweb-client.server";
import { DASHBOARD_THIRDWEB_SECRET_KEY } from "@/constants/server-envs";
import { getConfiguredThirdwebClient } from "@/constants/thirdweb.server";
import { fetchChain } from "@/utils/fetchChain";
import { DROP_PAGES } from "./data";
@@ -84,16 +85,29 @@ export default async function Image({ params }: { params: { slug: string } }) {
fetch(new URL("og-lib/fonts/inter/700.ttf", import.meta.url)).then((res) =>
res.arrayBuffer(),
),
// download the chain icon if there is one
chain.icon?.url && hasWorkingChainIcon
? download({
client: serverThirdwebClient,
uri: chain.icon.url,
}).then((res) => res.arrayBuffer())
// download the chain icon if there is one and secret key is available
chain.icon?.url && hasWorkingChainIcon && DASHBOARD_THIRDWEB_SECRET_KEY
? (async () => {
try {
const client = getConfiguredThirdwebClient({
secretKey: DASHBOARD_THIRDWEB_SECRET_KEY,
teamId: undefined,
});
const response = await download({
client,
uri: chain.icon?.url || "",
});
return response.arrayBuffer();
} catch (error) {
// If download fails, return undefined to fallback to no icon
console.warn("Failed to download chain icon:", error);
return undefined;
}
})()
: undefined,
// download the background image (based on chain)
fetch(
chain.icon?.url && hasWorkingChainIcon
chain.icon?.url && hasWorkingChainIcon && DASHBOARD_THIRDWEB_SECRET_KEY
? new URL(
"og-lib/assets/chain/bg-with-icon.png",
@@ -121,7 +135,7 @@ export default async function Image({ params }: { params: { slug: string } }) {
/>
{/* the actual component starts here */}
{hasWorkingChainIcon && (
{hasWorkingChainIcon && chainIcon && (
<img
alt=""
// @ts-expect-error - TS doesn't know about the ImageResponse component
@@ -11,7 +11,8 @@ import {
getActiveClaimCondition as getActiveClaimCondition1155,
getNFT as getNFT1155,
} from "thirdweb/extensions/erc1155";
import { serverThirdwebClient } from "@/constants/thirdweb-client.server";
import { DASHBOARD_THIRDWEB_SECRET_KEY } from "@/constants/server-envs";
import { getConfiguredThirdwebClient } from "@/constants/thirdweb.server";
import { defineDashboardChain } from "@/lib/defineDashboardChain";
import { DROP_PAGES } from "./data";
import { NftMint } from "./mint-ui";
@@ -41,13 +42,24 @@ export default async function DropPage({
if (!project) {
return notFound();
}
// Create client only if secret key is available
if (!DASHBOARD_THIRDWEB_SECRET_KEY) {
return notFound();
}
const client = getConfiguredThirdwebClient({
secretKey: DASHBOARD_THIRDWEB_SECRET_KEY,
teamId: undefined,
});
// eslint-disable-next-line no-restricted-syntax
const chain = defineDashboardChain(project.chainId, undefined);
const contract = getContract({
address: project.contractAddress,
chain,
client: serverThirdwebClient,
client,
});
const [nft, claimCondition, contractMetadata] = await Promise.all([
@@ -92,7 +104,7 @@ export default async function DropPage({
contract: getContract({
address: claimCondition.currency,
chain,
client: serverThirdwebClient,
client,
}),
})
: undefined;
-9
View File
@@ -1,7 +1,6 @@
import "../../global.css";
import type { Metadata } from "next";
import { Inter } from "next/font/google";
import PlausibleProvider from "next-plausible";
import NextTopLoader from "nextjs-toploader";
import { DashboardRouterTopProgressBar } from "@/lib/DashboardRouter";
import { cn } from "@/lib/utils";
@@ -52,14 +51,6 @@ export default function RootLayout({
}) {
return (
<html lang="en" suppressHydrationWarning>
<head>
<PlausibleProvider
customDomain="https://pl.thirdweb.com"
domain="thirdweb.com"
selfHosted
/>
</head>
<body
className={cn("bg-background font-sans antialiased", fontSans.variable)}
>
@@ -87,8 +87,7 @@ export function InviteTeamMembersUI(props: {
client={props.client}
customCTASection={
<div className="flex gap-3">
{(props.team.billingPlan === "free" ||
props.team.billingPlan === "starter") && (
{props.team.billingPlan === "free" && (
<Button
className="gap-2"
onClick={() => {
@@ -9,7 +9,8 @@ import {
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { serverThirdwebClient } from "@/constants/thirdweb-client.server";
import { DASHBOARD_THIRDWEB_SECRET_KEY } from "@/constants/server-envs";
import { getConfiguredThirdwebClient } from "@/constants/thirdweb.server";
import { PROJECT_SHOWCASE_DATA } from "@/lib/project-showcase-constants";
import { resolveSchemeWithErrorHandler } from "@/utils/resolveSchemeWithErrorHandler";
@@ -78,9 +79,13 @@ export default async function DetailPage(props: {
className="rounded-b-lg object-cover md:rounded-r-lg md:rounded-bl-none"
height={500}
src={
project.image?.startsWith("ipfs://")
project.image?.startsWith("ipfs://") &&
DASHBOARD_THIRDWEB_SECRET_KEY
? (resolveSchemeWithErrorHandler({
client: serverThirdwebClient,
client: getConfiguredThirdwebClient({
secretKey: DASHBOARD_THIRDWEB_SECRET_KEY,
teamId: undefined,
}),
uri: project.image,
}) ?? "")
: (project.image ?? "/assets/showcase/default_image.png")
@@ -19,7 +19,8 @@ import {
PaginationNext,
PaginationPrevious,
} from "@/components/ui/pagination";
import { serverThirdwebClient } from "@/constants/thirdweb-client.server";
import { DASHBOARD_THIRDWEB_SECRET_KEY } from "@/constants/server-envs";
import { getConfiguredThirdwebClient } from "@/constants/thirdweb.server";
import {
PROJECT_SHOWCASE_DATA,
PROJECT_SHOWCASE_INDUSTRIES,
@@ -138,9 +139,13 @@ export default async function ProjectShowcasePage(props: {
className="h-48 w-full object-cover"
height={200}
src={
project.image?.startsWith("ipfs://")
project.image?.startsWith("ipfs://") &&
DASHBOARD_THIRDWEB_SECRET_KEY
? (resolveSchemeWithErrorHandler({
client: serverThirdwebClient,
client: getConfiguredThirdwebClient({
secretKey: DASHBOARD_THIRDWEB_SECRET_KEY,
teamId: undefined,
}),
uri: project.image,
}) ?? "")
: (project.image ??
@@ -0,0 +1,120 @@
"use client";
import {
BookTextIcon,
BoxIcon,
ChartNoAxesColumnIcon,
DatabaseIcon,
DollarSignIcon,
FileTextIcon,
HomeIcon,
SettingsIcon,
WalletCardsIcon,
} from "lucide-react";
import { FullWidthSidebarLayout } from "@/components/blocks/full-width-sidebar-layout";
export function TeamSidebarLayout(props: {
layoutPath: string;
children: React.ReactNode;
ecosystems: Array<{ name: string; slug: string }>;
chainSubscriptions: Array<{
chainId: number;
chainName: string;
slug: string;
}>;
}) {
const { layoutPath, children } = props;
return (
<FullWidthSidebarLayout
contentSidebarLinks={[
{
exactMatch: true,
href: layoutPath,
icon: HomeIcon,
label: "Overview",
},
{
href: `${layoutPath}/~/analytics`,
icon: ChartNoAxesColumnIcon,
label: "Analytics",
},
{
links: [
...props.ecosystems.map((ecosystem) => ({
href: `${layoutPath}/~/ecosystem/${ecosystem.slug}`,
label: ecosystem.name,
})),
{
href: `${layoutPath}/~/ecosystem/create`,
label: "Create Ecosystem",
},
],
subMenu: {
icon: WalletCardsIcon,
label: "Ecosystems",
},
},
{
href: `${layoutPath}/~/audit-log`,
icon: FileTextIcon,
label: "Audit Log",
},
{
href: `${layoutPath}/~/usage`,
icon: DatabaseIcon,
label: "Usage",
},
...(props.chainSubscriptions.length > 0
? [
{
separator: true,
} as const,
{
links: [
...props.chainSubscriptions.map((chainSubscription) => ({
href: `${layoutPath}/~/infrastructure/${chainSubscription.slug}`,
label: chainSubscription.chainName,
})),
{
href: `${layoutPath}/~/infrastructure/deploy`,
label: "Deploy Infrastructure",
},
],
subMenu: {
icon: WalletCardsIcon,
label: "Chain Infrastucture",
},
},
]
: []),
]}
footerSidebarLinks={[
{
href: `${layoutPath}/~/billing`,
icon: DollarSignIcon,
label: "Billing",
},
{
href: `${layoutPath}/~/settings`,
icon: SettingsIcon,
label: "Team Settings",
},
{
separator: true,
},
{
href: "https://portal.thirdweb.com",
icon: BookTextIcon,
label: "Documentation",
},
{
href: "https://playground.thirdweb.com/connect/sign-in/button",
icon: BoxIcon,
label: "Playground",
},
]}
>
{children}
</FullWidthSidebarLayout>
);
}
@@ -54,7 +54,7 @@ function BillingAlertBanner(props: {
"border border-red-600 bg-red-100 text-red-800 hover:bg-red-200 dark:border-red-700 dark:bg-red-900 dark:text-red-100 dark:hover:bg-red-800",
)}
>
<Link href={`/team/${props.teamSlug}/~/settings/invoices`}>
<Link href={`/team/${props.teamSlug}/~/billing/invoices`}>
{props.ctaLabel}
</Link>
</Button>
@@ -18,7 +18,7 @@ export function FreePlanUpsellBannerUI(props: {
accentColor="green"
cta={{
icon: <ArrowRightIcon className="size-4" />,
link: `/team/${props.teamSlug}/~/settings/billing?showPlans=true&highlight=${
link: `/team/${props.teamSlug}/~/billing?showPlans=true&highlight=${
props.highlightPlan || "growth"
}`,
text: "View plans",
@@ -0,0 +1,29 @@
import { ArrowRightIcon } from "lucide-react";
import Link from "next/link";
import { Button } from "@/components/ui/button";
export function StaffModeNotice() {
return (
<div className="dark:bg-orange-800 bg-orange-200 border-b px-5">
<div className="flex items-center justify-between py-3">
<div className="flex flex-col">
<p className="font-bold text-lg tracking-tight">Staff Mode</p>
<p className="text-sm text-foreground/80">
You can only view this team, not take any actions.
</p>
</div>
<Button
asChild
className="rounded-full gap-1.5"
size="sm"
variant="default"
>
<Link href="/team/~">
Leave Staff Mode
<ArrowRightIcon className="size-4" />
</Link>
</Button>
</div>
</div>
);
}
@@ -1,17 +1,19 @@
import Link from "next/link";
import { redirect } from "next/navigation";
import { getAuthToken, getAuthTokenWalletAddress } from "@/api/auth-token";
import { fetchEcosystemList } from "@/api/ecosystems";
import { getProjects } from "@/api/projects";
import { getTeamBySlug, getTeams } from "@/api/team";
import { getChainSubscriptions } from "@/api/team-subscription";
import { CustomChatButton } from "@/components/chat/CustomChatButton";
import { AppFooter } from "@/components/footers/app-footer";
import { AnnouncementBanner } from "@/components/misc/AnnouncementBanner";
import { Button } from "@/components/ui/button";
import { TabPathLinks } from "@/components/ui/tabs";
import { SidebarProvider } from "@/components/ui/sidebar";
import { getClientThirdwebClient } from "@/constants/thirdweb-client.client";
import { getChain } from "../../../(dashboard)/(chain)/utils";
import { siwaExamplePrompts } from "../../../(dashboard)/support/definitions";
import { getValidAccount } from "../../../account/settings/getAccount";
import { TeamHeaderLoggedIn } from "../../components/TeamHeader/team-header-logged-in.client";
import { StaffModeNotice } from "./_components/StaffModeNotice";
import { TeamSidebarLayout } from "./TeamSidebarLayout";
export default async function TeamLayout(props: {
children: React.ReactNode;
@@ -19,105 +21,102 @@ export default async function TeamLayout(props: {
}) {
const params = await props.params;
const [accountAddress, account, teams, authToken, team] = await Promise.all([
const [
accountAddress,
account,
teams,
authToken,
team,
ecosystems,
chainSubscriptions,
] = await Promise.all([
getAuthTokenWalletAddress(),
getValidAccount(`/team/${params.team_slug}`),
getTeams(),
getAuthToken(),
getTeamBySlug(params.team_slug),
fetchEcosystemList(params.team_slug),
getChainSubscriptions(params.team_slug),
]);
if (!teams || !accountAddress || !authToken || !team) {
redirect("/login");
}
const teamsAndProjects = await Promise.all(
teams.map(async (team) => ({
projects: await getProjects(team.slug),
team,
})),
);
const [teamsAndProjects, chainSidebarLinks] = await Promise.all([
Promise.all(
teams.map(async (team) => ({
projects: await getProjects(team.slug),
team,
})),
),
chainSubscriptions
? await Promise.all(
chainSubscriptions.map(async (chainSubscription) => {
if (!chainSubscription.chainId) {
throw new Error("Chain ID is required");
}
const chain = await getChain(chainSubscription.chainId);
return {
chainId: chain.chainId,
chainName: chain.name,
slug: chain.slug,
};
}),
).catch(() => [])
: [],
]);
const client = getClientThirdwebClient({
jwt: authToken,
teamId: team.id,
});
const isStaffMode = !teams.some((t) => t.slug === team.slug);
return (
<div className="flex h-full grow flex-col">
{!teams.some((t) => t.slug === team.slug) && (
<div className="bg-warning-text">
<div className="container flex items-center justify-between py-4">
<div className="flex flex-col gap-2">
<p className="font-bold text-white text-xl">👀 STAFF MODE 👀</p>
<p className="text-sm text-white">
You can only view this team, not take any actions.
</p>
</div>
<Button asChild variant="default">
<Link href="/team/~">Leave Staff Mode</Link>
</Button>
</div>
<SidebarProvider>
<div className="flex h-dvh min-w-0 grow flex-col">
{isStaffMode && <StaffModeNotice />}
<AnnouncementBanner />
<div className="bg-card border-b">
<TeamHeaderLoggedIn
account={account}
accountAddress={accountAddress}
client={client}
currentProject={undefined}
currentTeam={team}
teamsAndProjects={teamsAndProjects}
/>
</div>
)}
<AnnouncementBanner />
<div className="bg-card">
<TeamHeaderLoggedIn
account={account}
accountAddress={accountAddress}
client={client}
currentProject={undefined}
currentTeam={team}
teamsAndProjects={teamsAndProjects}
/>
<TabPathLinks
links={[
{
exactMatch: true,
name: "Overview",
path: `/team/${params.team_slug}`,
},
{
name: "Analytics",
path: `/team/${params.team_slug}/~/analytics`,
},
{
name: "Ecosystems",
path: `/team/${params.team_slug}/~/ecosystem`,
},
{
name: "Usage",
path: `/team/${params.team_slug}/~/usage`,
},
{
name: "Audit Log",
path: `/team/${params.team_slug}/~/audit-log`,
},
{
name: "Settings",
path: `/team/${params.team_slug}/~/settings`,
},
]}
tabContainerClassName="px-4 lg:px-6"
/>
<TeamSidebarLayout
chainSubscriptions={chainSidebarLinks.sort(
(a, b) => a.chainId - b.chainId,
)}
ecosystems={ecosystems.map((ecosystem) => ({
name: ecosystem.name,
slug: ecosystem.slug,
}))}
layoutPath={`/team/${params.team_slug}`}
>
{props.children}
</TeamSidebarLayout>
<div className="fixed right-6 bottom-6 z-50">
<CustomChatButton
authToken={authToken}
clientId={undefined}
examplePrompts={siwaExamplePrompts}
isFloating={true}
isLoggedIn={true}
label="Ask AI Assistant"
networks="all"
pageType="support"
teamId={team.id}
/>
</div>
</div>
<main className="flex grow flex-col">{props.children}</main>
<div className="fixed right-6 bottom-6 z-50">
<CustomChatButton
authToken={authToken}
clientId={undefined}
examplePrompts={siwaExamplePrompts}
isFloating={true}
isLoggedIn={true}
label="Ask AI Assistant"
networks="all"
pageType="support"
teamId={team.id}
/>
</div>
<AppFooter />
</div>
</SidebarProvider>
);
}
@@ -73,7 +73,8 @@ export default async function TeamOverviewPage(props: {
title="Analytics"
/>
</div>
<div className="flex grow flex-col justify-between gap-10 md:container md:pt-8 md:pb-16">
<div className="flex grow flex-col justify-between gap-10 container max-w-7xl pt-8 pb-16">
<div className="flex grow flex-col gap-6">
<Suspense
fallback={<LoadingChartState className="h-[458px] border" />}
@@ -86,7 +87,7 @@ export default async function TeamOverviewPage(props: {
/>
</Suspense>
<div className="grid gap-6 max-md:px-6 md:grid-cols-2">
<div className="grid gap-6 md:grid-cols-2">
<Suspense
fallback={<LoadingChartState className="h-[431px] border" />}
>
@@ -168,7 +169,7 @@ async function AsyncAppHighlightsCard(props: {
return (
<EmptyStateCard
link="https://portal.thirdweb.com/connect/quickstart"
metric="Connect"
metric="Wallets"
/>
);
}
@@ -189,7 +190,7 @@ async function AsyncWalletDistributionCard(props: {
) : (
<EmptyStateCard
link="https://portal.thirdweb.com/connect/quickstart"
metric="Connect"
metric="Wallets"
/>
);
}
@@ -210,7 +211,7 @@ async function AsyncAuthMethodDistributionCard(props: {
) : (
<EmptyStateCard
link="https://portal.thirdweb.com/typescript/v5/inAppWallet"
metric="In-App Wallets"
metric="Wallets"
/>
);
}
@@ -242,14 +243,13 @@ async function AsyncTransactionsChartCard(props: {
clientTransactions.value.length > 0 ? (
<TransactionsChartCardUI
aggregatedData={clientTransactions.value}
className="max-md:rounded-none max-md:border-r-0 max-md:border-l-0"
data={clientTransactionsTimeSeries.value}
searchParams={props.searchParams}
/>
) : (
<EmptyStateCard
link="https://portal.thirdweb.com/connect/quickstart"
metric="Transactions"
metric="Wallets"
/>
);
}
@@ -280,14 +280,13 @@ async function AsyncTotalSponsoredCard(props: {
userOpUsage.value.length > 0 ? (
<TotalSponsoredChartCardUI
aggregatedData={userOpUsage.value}
className="max-md:rounded-none max-md:border-r-0 max-md:border-l-0"
data={userOpUsageTimeSeries.value}
searchParams={props.searchParams}
/>
) : (
<EmptyStateCard
link="https://portal.thirdweb.com/typescript/v5/account-abstraction/get-started"
metric="Gas Sponsored"
metric="Account Abstraction"
/>
);
}
@@ -391,7 +390,6 @@ function AppHighlightsCard({
return timeSeriesData.reduce((acc, curr) => acc + curr[key], 0);
}}
chartConfig={chartConfig}
className="max-md:rounded-none max-md:border-r-0 max-md:border-l-0"
data={timeSeriesData}
existingQueryParams={searchParams}
queryKey="appHighlights"
@@ -24,13 +24,15 @@ export default async function Layout(props: {
>
<div className="flex grow flex-col">
<div className="border-border border-b py-10">
<div className="container flex flex-row justify-between">
<div className="container max-w-5xl flex flex-row justify-between">
<h1 className="font-semibold text-3xl tracking-tight lg:px-2">
Audit Log
</h1>
</div>
</div>
{props.children}
<div className="container max-w-5xl flex flex-col gap-8 pt-6 pb-10">
{props.children}
</div>
</div>
</UpsellWrapper>
);
@@ -47,7 +47,7 @@ export default async function Page(props: {
}
return (
<div className="container flex flex-col gap-8">
<div className="flex flex-col gap-8">
{auditLogs.data.result.length === 0 ? (
<div className="py-12 text-center text-muted-foreground">
<p className="text-sm">No audit events found</p>
@@ -237,7 +237,7 @@ export function PlanInfoCardUI(props: {
size="sm"
variant="outline"
>
<Link href={`/team/${team.slug}/~/settings/invoices`}>
<Link href={`/team/${team.slug}/~/billing/invoices`}>
<FileTextIcon className="size-4 text-muted-foreground" />
View Invoices
</Link>
@@ -16,7 +16,7 @@ import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import { Separator } from "@/components/ui/separator";
import { Skeleton } from "@/components/ui/skeleton";
import { ToolTipLabel } from "@/components/ui/tooltip";
import { ThirdwebMiniLogo } from "../../../../../../../components/ThirdwebMiniLogo";
import { ThirdwebMiniLogo } from "../../../../../../components/ThirdwebMiniLogo";
const predefinedAmounts = [
{ label: "$25", value: "25" },
@@ -19,7 +19,7 @@ export default async function Page(props: {
searchParamLoader(props.searchParams),
]);
const pagePath = `/team/${params.team_slug}/settings/invoices`;
const pagePath = `/team/${params.team_slug}/invoices`;
const account = await getValidAccount(pagePath);
@@ -0,0 +1,56 @@
import { redirect } from "next/navigation";
import { getAuthToken } from "@/api/auth-token";
import { getTeamBySlug } from "@/api/team";
import { TabPathLinks } from "@/components/ui/tabs";
import { loginRedirect } from "../../../../../login/loginRedirect";
export default async function Layout(props: {
params: Promise<{
team_slug: string;
}>;
children: React.ReactNode;
}) {
const params = await props.params;
const [team, authToken] = await Promise.all([
getTeamBySlug(params.team_slug),
getAuthToken(),
]);
if (!authToken) {
loginRedirect(`/team/${params.team_slug}/~/settings`);
}
if (!team) {
redirect("/team");
}
return (
<div className="flex grow flex-col">
<div className="border-border pt-10 pb-6">
<div className="container max-w-6xl">
<h1 className="font-semibold text-3xl tracking-tight">Billing</h1>
</div>
</div>
<TabPathLinks
links={[
{
exactMatch: true,
name: "Overview",
path: `/team/${params.team_slug}/~/billing`,
},
{
name: "Invoices",
path: `/team/${params.team_slug}/~/billing/invoices`,
},
]}
scrollableClassName="container max-w-6xl"
/>
<div className="container max-w-6xl flex flex-col grow gap-8 pt-6 pb-10">
{props.children}
</div>
</div>
);
}
@@ -7,7 +7,7 @@ import { getTeamSubscriptions } from "@/api/team-subscription";
import { CreditsInfoCard } from "@/components/billing/PlanCard";
import { Coupons } from "@/components/billing/SubscriptionCoupons/Coupons";
import { getClientThirdwebClient } from "@/constants/thirdweb-client.client";
import { getValidAccount } from "../../../../../../account/settings/getAccount";
import { getValidAccount } from "../../../../../account/settings/getAccount";
import { CreditBalanceSection } from "./components/credit-balance-section.client";
import { PlanInfoCardClient } from "./components/PlanInfoCard.client";
@@ -24,7 +24,7 @@ export default async function Page(props: {
props.params,
props.searchParams,
]);
const pagePath = `/team/${params.team_slug}/settings/billing`;
const pagePath = `/team/${params.team_slug}/billing`;
const account = await getValidAccount(pagePath);
@@ -64,7 +64,7 @@ export default async function Page(props: {
team.billingStatus === "validPayment" || team.billingStatus === "pastDue";
return (
<div className="flex flex-col gap-12">
<div className="flex flex-col gap-8">
<div>
<PlanInfoCardClient
highlightPlan={highlightPlan}
@@ -1,10 +1,10 @@
import { getEcosystemWalletUsage } from "@/api/analytics";
import type { Partner } from "@/api/ecosystems";
import {
getLastNDaysRange,
type Range,
} from "@/components/analytics/date-range-selector";
import { RangeSelector } from "@/components/analytics/range-selector";
import type { Partner } from "../../../../types";
import { EcosystemWalletUsersChartCard } from "./EcosystemWalletUsersChartCard";
import { EcosystemWalletsSummary } from "./Summary";
@@ -1,6 +1,7 @@
"use client";
import { format } from "date-fns";
import { useMemo } from "react";
import type { Partner } from "@/api/ecosystems";
import { ThirdwebBarChart } from "@/components/blocks/charts/bar-chart";
import { DocLink } from "@/components/blocks/DocLink";
import { ExportToCSVButton } from "@/components/blocks/ExportToCSVButton";
@@ -10,7 +11,6 @@ import { TypeScriptIcon } from "@/icons/brand-icons/TypeScriptIcon";
import { UnityIcon } from "@/icons/brand-icons/UnityIcon";
import type { EcosystemWalletStats } from "@/types/analytics";
import { formatTickerNumber } from "@/utils/format-utils";
import type { Partner } from "../../../../types";
type ChartData = Record<string, number> & {
time: string; // human readable date
@@ -1,7 +1,7 @@
import { redirect } from "next/navigation";
import { getAuthToken } from "@/api/auth-token";
import { fetchEcosystem } from "@/api/ecosystems";
import { getTeamBySlug } from "@/api/team";
import { getAuthToken } from "../../../../../../../../../../@/api/auth-token";
import { fetchEcosystem } from "../../../utils/fetchEcosystem";
import { fetchPartners } from "../configuration/hooks/fetchPartners";
import { EcosystemAnalyticsPage } from "./components/EcosystemAnalyticsPage";
@@ -29,7 +29,7 @@ export default async function Page(props: {
}
const [ecosystem, team] = await Promise.all([
fetchEcosystem(params.slug, authToken, params.team_slug),
fetchEcosystem(params.slug, params.team_slug),
getTeamBySlug(params.team_slug),
]);
@@ -1,9 +1,9 @@
import { redirect } from "next/navigation";
import { getAuthToken } from "@/api/auth-token";
import { fetchEcosystem } from "@/api/ecosystems";
import { getTeamBySlug } from "@/api/team";
import { SidebarLayout } from "@/components/blocks/SidebarLayout";
import { TabPathLinks } from "@/components/ui/tabs";
import { getClientThirdwebClient } from "@/constants/thirdweb-client.client";
import { getAuthToken } from "../../../../../../../../../../@/api/auth-token";
import { fetchEcosystem } from "../../../utils/fetchEcosystem";
import { EcosystemHeader } from "./ecosystem-header.client";
export async function EcosystemLayoutSlug({
@@ -21,11 +21,7 @@ export async function EcosystemLayoutSlug({
redirect(ecosystemLayoutPath);
}
const ecosystem = await fetchEcosystem(
params.slug,
authToken,
params.team_slug,
);
const ecosystem = await fetchEcosystem(params.slug, params.team_slug);
// Fetch team details to obtain team ID for further authenticated updates
const team = await getTeamBySlug(params.team_slug);
@@ -54,25 +50,28 @@ export async function EcosystemLayoutSlug({
teamIdOrSlug={params.team_slug}
/>
<SidebarLayout
sidebarLinks={[
<TabPathLinks
className="pt-2"
links={[
{
exactMatch: true,
href: `${ecosystemLayoutPath}/${ecosystem.slug}`,
label: "Overview",
name: "Overview",
path: `${ecosystemLayoutPath}/${ecosystem.slug}`,
},
{
href: `${ecosystemLayoutPath}/${ecosystem.slug}/analytics`,
label: "Analytics",
name: "Analytics",
path: `${ecosystemLayoutPath}/${ecosystem.slug}/analytics`,
},
{
href: `${ecosystemLayoutPath}/${ecosystem.slug}#`,
label: "Design (coming soon)",
name: "Design (coming soon)",
path: `${ecosystemLayoutPath}/${ecosystem.slug}/#`,
},
]}
>
scrollableClassName="container max-w-7xl"
/>
<div className="container max-w-7xl flex flex-col grow pt-6 pb-10">
{children}
</SidebarLayout>
</div>
</div>
);
}
@@ -1,17 +1,10 @@
"use client";
import {
AlertTriangleIcon,
CheckIcon,
ChevronsUpDownIcon,
ExternalLinkIcon,
PencilIcon,
PlusCircleIcon,
} from "lucide-react";
import { AlertTriangleIcon, ExternalLinkIcon, PencilIcon } from "lucide-react";
import Link from "next/link";
import { useState } from "react";
import { toast } from "sonner";
import type { ThirdwebClient } from "thirdweb";
/* eslint-disable */
import type { Ecosystem } from "@/api/ecosystems";
import { Img } from "@/components/blocks/Img";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { Button } from "@/components/ui/button";
@@ -25,14 +18,6 @@ import {
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { ImageUpload } from "@/components/ui/image-upload";
import { Input } from "@/components/ui/input";
import { Spinner } from "@/components/ui/Spinner/Spinner";
@@ -41,8 +26,6 @@ import { useDashboardStorageUpload } from "@/hooks/useDashboardStorageUpload";
import { useDashboardRouter } from "@/lib/DashboardRouter";
import { cn } from "@/lib/utils";
import { resolveSchemeWithErrorHandler } from "@/utils/resolveSchemeWithErrorHandler";
import { useEcosystemList } from "../../../hooks/use-ecosystem-list";
import type { Ecosystem } from "../../../types";
import { useUpdateEcosystem } from "../configuration/hooks/use-update-ecosystem";
import { useEcosystem } from "../hooks/use-ecosystem";
@@ -78,56 +61,6 @@ function EcosystemAlertBanner({ ecosystem }: { ecosystem: Ecosystem }) {
}
}
function EcosystemSelect(props: {
ecosystem: Ecosystem;
ecosystemLayoutPath: string;
teamIdOrSlug: string;
}) {
const { data: ecosystems, isPending } = useEcosystemList({
teamIdOrSlug: props.teamIdOrSlug,
});
return isPending ? (
<Skeleton className="h-10 w-full md:w-[160px]" />
) : (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
className="relative flex w-full justify-start truncate pr-8 pl-3 md:w-48"
variant="outline"
>
<div className="truncate">{props.ecosystem?.name}</div>
<ChevronsUpDownIcon className="absolute right-2 h-4 w-4 text-muted-foreground" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-full md:w-48">
<DropdownMenuGroup>
{ecosystems?.map((ecosystem) => (
<DropdownMenuItem asChild key={ecosystem.id}>
<Link
className="relative flex cursor-pointer items-center pr-3 pl-8"
href={`${props.ecosystemLayoutPath}/${ecosystem.slug}`}
>
{ecosystem.slug === props.ecosystem.slug && (
<CheckIcon className="absolute left-2 h-4 w-4 text-foreground" />
)}
<div className="truncate">{ecosystem.name}</div>
</Link>
</DropdownMenuItem>
))}
</DropdownMenuGroup>
<DropdownMenuSeparator />
<Link className="" href={`${props.ecosystemLayoutPath}/create`}>
<DropdownMenuItem className="relative flex cursor-pointer items-center pr-3 pl-8">
<PlusCircleIcon className="absolute left-2 h-4 w-4" />
<div className="truncate">New Ecosystem</div>
</DropdownMenuItem>
</Link>
</DropdownMenuContent>
</DropdownMenu>
);
}
export function EcosystemHeader(props: {
ecosystem: Ecosystem;
ecosystemLayoutPath: string;
@@ -236,8 +169,8 @@ export function EcosystemHeader(props: {
}
return (
<div className="border-b py-8">
<div className="container flex flex-col gap-8">
<div className="pt-8 pb-4">
<div className="container max-w-7xl flex flex-col gap-8">
<EcosystemAlertBanner ecosystem={ecosystem} />
<header className="flex flex-col gap-12">
<div className="flex flex-col justify-between gap-4 md:grid-cols-4 md:flex-row">
@@ -402,13 +335,6 @@ export function EcosystemHeader(props: {
)}
</div>
</div>
<div className="flex flex-col justify-between gap-4 md:items-end">
<EcosystemSelect
ecosystem={ecosystem}
ecosystemLayoutPath={props.ecosystemLayoutPath}
teamIdOrSlug={props.teamIdOrSlug}
/>
</div>
</div>
</header>
</div>
@@ -1,10 +1,10 @@
import { notFound } from "next/navigation";
import { getAuthToken } from "@/api/auth-token";
import { fetchEcosystem } from "@/api/ecosystems";
import { getTeamBySlug } from "@/api/team";
import { getClientThirdwebClient } from "@/constants/thirdweb-client.client";
import { getAuthToken } from "../../../../../../../../../../../@/api/auth-token";
import { loginRedirect } from "../../../../../../../../../login/loginRedirect";
import { AddPartnerForm } from "../components/client/add-partner-form.client";
import { fetchEcosystem } from "../hooks/fetchEcosystem";
export default async function AddPartnerPage({
params,
@@ -34,11 +34,10 @@ export default async function AddPartnerPage({
});
try {
const ecosystem = await fetchEcosystem({
authToken,
slug: ecosystemSlug,
teamIdOrSlug: teamSlug,
});
const ecosystem = await fetchEcosystem(ecosystemSlug, teamSlug);
if (!ecosystem) {
throw new Error("Ecosystem not found");
}
return (
<div className="flex flex-col">
@@ -2,8 +2,8 @@
import { PlusIcon } from "lucide-react";
import Link from "next/link";
import type { Ecosystem } from "@/api/ecosystems";
import { Button } from "@/components/ui/button";
import type { Ecosystem } from "../../../../../types";
export function AddPartnerDialogButton(props: {
teamSlug: string;
@@ -2,8 +2,8 @@
import { useParams } from "next/navigation";
import { toast } from "sonner";
import type { ThirdwebClient } from "thirdweb";
import type { Ecosystem, Partner } from "@/api/ecosystems";
import { useDashboardRouter } from "@/lib/DashboardRouter";
import type { Ecosystem, Partner } from "../../../../../types";
import { useAddPartner } from "../../hooks/use-add-partner";
import { PartnerForm, type PartnerFormValues } from "./partner-form.client";
@@ -12,6 +12,7 @@ import {
} from "thirdweb/wallets/smart";
import invariant from "tiny-invariant";
import { z } from "zod";
import type { AuthOption, Ecosystem } from "@/api/ecosystems";
import { SingleNetworkSelector } from "@/components/blocks/NetworkSelectors";
import { SettingsCard } from "@/components/blocks/SettingsCard";
import { Button } from "@/components/ui/button";
@@ -36,9 +37,28 @@ import {
import { Skeleton } from "@/components/ui/skeleton";
import { Switch } from "@/components/ui/switch";
import { cn } from "@/lib/utils";
import { authOptions, type Ecosystem } from "../../../../../types";
import { useUpdateEcosystem } from "../../hooks/use-update-ecosystem";
const authOptions = [
"email",
"phone",
"passkey",
"siwe",
"guest",
"google",
"facebook",
"x",
"discord",
"farcaster",
"telegram",
"github",
"twitch",
"steam",
"apple",
"coinbase",
"line",
] as const satisfies AuthOption[];
type AuthOptionsFormData = {
authOptions: string[];
useCustomAuth: boolean;
@@ -113,6 +133,8 @@ export function AuthOptionsForm({
(data) => {
if (
data.useSmartAccount &&
data.executionMode === "EIP4337" &&
data.accountFactoryType === "custom" &&
data.customAccountFactoryAddress &&
!isAddress(data.customAccountFactoryAddress)
) {
@@ -125,6 +147,23 @@ export function AuthOptionsForm({
path: ["customAccountFactoryAddress"],
},
)
.refine(
(data) => {
if (
data.useSmartAccount &&
data.executionMode === "EIP4337" &&
data.accountFactoryType === "custom" &&
!data.customAccountFactoryAddress
) {
return false;
}
return true;
},
{
message: "Please enter a custom account factory address",
path: ["customAccountFactoryAddress"],
},
)
.refine(
(data) => {
if (data.useSmartAccount && (data.defaultChainId ?? 0) <= 0) {
@@ -193,21 +232,23 @@ export function AuthOptionsForm({
let smartAccountOptions: Ecosystem["smartAccountOptions"] | null = null;
if (data.useSmartAccount) {
let accountFactoryAddress: string;
switch (data.accountFactoryType) {
case "v0.6":
accountFactoryAddress = DEFAULT_ACCOUNT_FACTORY_V0_6;
break;
case "v0.7":
accountFactoryAddress = DEFAULT_ACCOUNT_FACTORY_V0_7;
break;
case "custom":
if (!data.customAccountFactoryAddress) {
toast.error("Please enter a custom account factory address");
return;
}
accountFactoryAddress = data.customAccountFactoryAddress;
break;
let accountFactoryAddress: string | undefined;
if (data.executionMode === "EIP4337") {
switch (data.accountFactoryType) {
case "v0.6":
accountFactoryAddress = DEFAULT_ACCOUNT_FACTORY_V0_6;
break;
case "v0.7":
accountFactoryAddress = DEFAULT_ACCOUNT_FACTORY_V0_7;
break;
case "custom":
if (!data.customAccountFactoryAddress) {
toast.error("Please enter a custom account factory address");
return;
}
accountFactoryAddress = data.customAccountFactoryAddress;
break;
}
}
smartAccountOptions = {
@@ -220,7 +261,7 @@ export function AuthOptionsForm({
updateEcosystem({
...ecosystem,
authOptions: data.authOptions as (typeof authOptions)[number][],
authOptions: data.authOptions as AuthOption[],
customAuthOptions,
smartAccountOptions,
});

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