Compare commits

..
Author SHA1 Message Date
Joaquim VergesGitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
01dce0e581 Version Packages (#8536)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-12-10 17:43:25 -08:00
Joaquim VergesandGitHub 18425f9d74 [SDK] Add minPrice property for x402 payments using 'upto' schema (#8534) 2025-12-10 17:36:03 -08:00
Joaquim VergesandGitHub bd2761af15 [Portal] Improve documentation for upto payment scheme (#8531) 2025-12-10 16:06:05 -08:00
FirekeeperandGitHub f3d2739df1 Improve relayer analytics (#8533) 2025-12-11 06:47:03 +07:00
Joaquim VergesGitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
1aa4912b17 Version Packages (#8532)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-12-11 04:36:33 +07:00
7967d7f17e Dedicated Relayer (#8479)
Co-authored-by: Manan Tank <[email protected]>
2025-12-11 04:26:03 +07:00
Joaquim VergesandGitHub 1487de6808 Fix typo in "Create Project" button text (#8530) 2025-12-10 10:53:06 -08:00
MananTank 18917b632a Fix bridge iframe config (#8529)
<!--

## 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 references from `/bridge/embed` to `/bridge/widget` in the configuration files, reflecting a change in the iframe page used in the application.

### Detailed summary
- Updated `PAYMENT_METHOD_CONFIGURATION` comment in `.env.example` from `/bridge/embed` to `/bridge/widget`.
- Changed `source` in `next.config.ts` from `/bridge/embed` to `/bridge/widget`.
- Updated `source` path pattern in `next.config.ts` from `/bridge/embed/:path*` to `/bridge/widget/:path*`.

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

<!-- end pr-codex -->

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

## Summary by CodeRabbit

* **Documentation**
  * Updated configuration documentation.

* **Chores**
  * Updated internal routing paths in application configuration.

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

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-12-10 18:35:11 +00:00
MananTank 41919024c3 Add bridge widget iframe docs, update path, link to docs (#8528)
<!--

## 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 `Bridge` and `Bridge Widget` functionalities, improving UI elements, adding new features, and updating documentation. It includes modifications to components, styling adjustments, and expanded capabilities for currency selection.

### Detailed summary
- Updated `Sidebar.tsx` to remove padding.
- Changed `page.mdx` headers from "Key Features" to "Features".
- Enhanced `reportBridgePageLinkClick` to include "integrate-bridge-widget".
- Modified `page.tsx` to include client ID instructions.
- Adjusted header font sizes in `header.tsx`.
- Improved `bridgeRedirects` to include new paths.
- Updated `bridge-widget` links in `sidebar.tsx`.
- Added `persistTokenSelections` and `currency` props to `UniversalBridgeEmbed`.
- Created `IframeCodePreview` component for iframe integration documentation.
- Added a new `BadgeLink` component for better user interaction.
- Expanded `BuyAndSwapEmbed` props to support currency and token selections.
- Updated documentation for `BridgeWidget` and `Iframe` integrations with examples.
- Enhanced `BridgePageUI` to include the new `AddBridgeWidgetLink`.
- Introduced multiple currency options for fiat values in widget interfaces.

> ✨ 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**
  * Bridge widget available via Iframe, Script, and React component
  * Fiat currency selection for buy/swap widgets
  * Token selection persistence option
  * "Add Bridge widget" integration link added to Bridge page

* **Documentation**
  * New detailed guides and examples for Iframe, Script, and React integration
  * Code preview and embedding examples added

* **Style**
  * Typography, spacing, and layout refinements across Bridge pages
  * Navigation reorganized into a Bridge Widget group

* **Chores**
  * Added redirects for Bridge widget paths

<sub>✏️ Tip: You can customize this high-level summary in your review settings.</sub>
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-12-10 17:53:09 +00:00
github-actions[bot]andJonas Daniels 56eb1d2491 Version Packages 2025-12-10 01:08:35 -08:00
Jonas Daniels 70ff608c72 Add UTF-8 JSON string parsing support 2025-12-10 00:20:56 -08:00
MananTank 7e442a4c66 [MNY-328] Dashboard: Add Bridge iframe page (#8525)
<!--

## 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 enhances the `bridge` functionality by introducing iframe support, updating component structures, and modifying environment variables. It also includes changes in layout and security policies related to the `bridge` feature.

### Detailed summary
- Added `NEXT_PUBLIC_BRIDGE_IFRAME_CLIENT_ID` to environment variables.
- Modified `BridgeProviders` to accept `clientId` and `forcedTheme`.
- Updated `UniversalBridgeEmbed` to use dynamic `pageType`.
- Created a new layout for `BridgeEmbedLayout`.
- Enhanced security headers for `/bridge/embed`.
- Refactored imports and paths for `data` and components.
- Added `bridge-iframe` to `pageType` types across multiple files.
- Implemented new parsing logic for search parameters in the iframe page.

> ✨ 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 new /bridge/embed page to support an embeddable bridge iframe and a "bridge-iframe" flow.

* **Security**
  * Updated content security headers and frame-ancestor rules to allow safe embedding of the bridge iframe.

* **Chores**
  * Added environment variable for the bridge iframe client ID.
  * Simplified theme behavior to rely on the default dark theme.

* **Analytics**
  * Added "bridge-iframe" page type to reporting to track iframe usage.

<sub>✏️ Tip: You can customize this high-level summary in your review settings.</sub>
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-12-09 22:05:25 +00:00
MananTank a5b888576f Dashboard: Login page UI tweaks, remove phone login notice (#8524)
<!--

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

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

## Notes for the reviewer

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

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR enhances the `LoginPage.tsx` component by adding a theme toggle button, updating the layout, and improving the UI elements. It incorporates new components and refines the existing structure for better aesthetics and functionality.

### Detailed summary
- Added imports for `MoonIcon`, `SunIcon`, `DotsBackgroundPattern`, `Button`, and `Skeleton`.
- Updated header background and layout, adjusting padding and margins.
- Introduced a `ToggleThemeButton` component for theme switching.
- Removed deprecated login message and background image.
- Enhanced the `LoginPageContainer` structure for cleaner rendering.

> ✨ 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 decorative dots background pattern to enhance the login page visual design.
  * Added theme toggle button enabling light and dark mode switching.
  * Enhanced header with brand text display alongside the logo.

* **Style**
  * Streamlined login page layout with a simplified and cleaner container structure.
  * Adjusted spacing, padding, and overall dimensions for improved visual hierarchy.

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

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-12-09 17:49:25 +00:00
FirekeeperandGitHub 53b6d1b4d6 bal override path (#8516) 2025-12-09 02:36:38 +07:00
MananTank 9856ee4010 [PRO-138] Dashboard: Update Project highlights card revenue tab (#8515)
<!--

## 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 analytics components by adding new functionalities and refining existing code for better performance and readability. It introduces new data handling for `x402Settlements`, updates chart configurations, and improves conditional rendering in various components.

### Detailed summary
- Added `x402Settlements` data handling in `AsyncAppHighlightsCard`.
- Updated `CombinedBarChartConfig` to include `stackedKeys`.
- Improved conditional rendering for the `EmptyChartStateGetStartedCTA`.
- Refined the `BarChart` component for better data processing.
- Enhanced `ProjectHighlightsCard` to include `bridgeRevenue` and `x402Revenue`.
- Introduced utility functions for generating mock data in stories.
- Updated types in various components for better type safety.

> ✨ 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 stacked data visualization support to charts.
  * Integrated x402 revenue tracking into analytics highlights.
  * Added ability to hide specific chart tabs from display.

* **Improvements**
  * Refined USD currency formatting to display 2 decimal places for cleaner presentation.
  * Made analytics CTA buttons conditionally displayed based on configuration.

* **Refactor**
  * Simplified settlement data model with inheritance to reduce redundancy.

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

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-12-08 18:55:16 +00:00
Joaquim VergesGitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
3c768a0ef9 Version Packages (#8503)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-12-05 23:07:48 -08:00
FirekeeperandGitHub a388439022 Add Solana facilitator guide to docs (#8511) 2025-12-06 05:53:27 +07:00
MananTank 7b873d38db [MNY-322] SDK: Update success message shown in SwapWidget component (#8509)
<!--

## 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 success messages in the `SuccessScreen` component across various widgets in the `thirdweb` package, specifically for payment and swap transactions.

### Detailed summary
- Updated success message type in `SwapWidget` to `"swap-success"`.
- Updated success message type in `BuyWidget` and `CheckoutWidget` to `"payment-success"`.
- Added a new story `BasicSwapSuccess` in `SuccessScreen.stories.tsx`.
- Modified `SuccessScreen` to handle different message types.
- Updated displayed titles and descriptions based on the success type.
- Changed button text from "View Payment Receipt" to "View Transaction Receipt".

> ✨ 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**
  * Success screens now display dynamic, context-appropriate messaging tailored to transaction type, with distinct titles and descriptions for swap versus payment transactions.
  * Enhanced user experience with improved terminology—the option to "View Transaction Receipt" now displays consistently across all payment flows for better clarity and consistency.

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

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-12-05 20:46:01 +00:00
Jonas Daniels d33a943176 Add AI privacy policy and terms to framer-rewrites.js 2025-12-05 12:38:02 -08:00
FirekeeperandGitHub d5e0b6eb73 Update API reference links in dashboard and portal (#8508) 2025-12-06 02:57:42 +07:00
Joaquim VergesandGitHub 75e9be0358 [SDK] Update token balances query to use client instead of clientId (#8507) 2025-12-04 18:32:44 -08:00
joaquim-verges a704c3f2b0 [SDK] Allow passing activeWallet to SwapWidget (#8504)
<!--

## 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 `SwapWidget` by allowing the passing of an `activeWallet` prop and improving the project wallet functionality in the dashboard. It also includes updates to the VSCode settings and introduces new components for swapping tokens.

### Detailed summary
- Added `activeWallet` prop to `SwapWidgetProps`.
- Modified `useActiveWalletInfo` to accept an `activeWalletOverride`.
- Introduced `SwapProjectWalletModalContent` for token swapping.
- Updated `ProjectWalletDetailsSection` to include swap functionality.
- Adjusted VSCode settings for formatting preferences.

> ✨ 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 in-app token swap functionality for project wallets, allowing users to exchange tokens directly from the wallet interface with credentials management.
  * SwapWidget now supports wallet pre-selection, enabling faster and more streamlined token swap initiation.

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

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-12-05 00:03:49 +00:00
MananTank b0eccf365a [MNY-318] SDK: Remove tabs from token selection ui in SwapWidget (#8501)
<!--

## 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 `SelectToken` component by simplifying token limit management, improving UI consistency, and refactoring token display logic. It also removes unnecessary tabs and optimizes the selection process for better user experience.

### Detailed summary
- Replaced constants `ALL_TOKENS_LIMIT` and `OWNED_TOKENS_LIMIT` with `INITIAL_LIMIT`.
- Unified token limit state management into a single `limit` state.
- Refactored token fetching and display logic to streamline data handling.
- Removed unnecessary tab UI for better clarity.
- Simplified the structure of token data handling in `SelectTokenUI`.
- Updated the rendering logic for owned and other tokens to improve performance and readability.

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

<!-- end pr-codex -->

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

* **UI Improvements**
  * Redesigned token selection UI in SwapWidget: removed tab navigation and consolidated token lists into dedicated "Your Tokens" and "Other Tokens" sections for a clearer, more intuitive experience.

* **Bug Fixes**
  * Enhanced search functionality to properly reset token filtering, improving responsiveness.

<sub>✏️ Tip: You can customize this high-level summary in your review settings.</sub>
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-12-04 22:09:50 +00:00
MananTank f3525c7c01 [PRO-123] Dashboard: Move project webhooks (#8500)
<!--

## 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 reorganizing the `webhooks` feature within the application, including changes to routing and component structure, as well as the introduction of a `bridge` section for webhooks.

### Detailed summary
- Deleted multiple files related to `webhooks` under the previous structure.
- Updated routing paths from `/webhooks/payments` to `/bridge/webhooks`.
- Refactored components to align with the new `bridge` structure.
- Adjusted imports to reflect new file locations.
- Removed unused `WebhookIcon` and streamlined sidebar links.
- Introduced new layout and page components under `tokens/(general)/webhooks`.

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

## Release Notes

* **Navigation & Organization**
  - Webhooks functionality relocated to the Bridge section for improved contextual organization
  - Updated navigation paths and sidebar structure accordingly

* **UI Improvements**
  - Redesigned empty state for webhooks with enhanced visual styling and icon display

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

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-12-04 21:35:46 +00:00
MananTank e516b4ddc1 [PRO-147] Dashboard: Update empty state for project analytics charts (#8499)
<!--

## 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 empty chart states across various components in the dashboard by standardizing the `EmptyChartState` usage and enhancing the user experience with informative content.

### Detailed summary
- Updated `EmptyChartState` components to use `content` prop instead of `type`.
- Enhanced `EmptyChartState` with more descriptive text and links.
- Introduced `EmptyChartStateGetStartedCTA` for better guidance in empty states.
- Adjusted multiple charts to incorporate the new empty state handling.
- Simplified chart rendering logic when no data is available.

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

* **UI/UX Improvements**
  * Unified and simplified empty-chart visuals (smaller message, cleaner layout, adjusted loading background) and consistent spacing across analytics cards.
  * Empty states now show contextual CTAs linking to relevant configuration/details pages.

* **New Features**
  * Reusable "Get Started" CTA for empty charts with title, description and action.

* **Refactor**
  * Charts can now accept and render provided empty-state content for a more consistent UX.

<sub>✏️ Tip: You can customize this high-level summary in your review settings.</sub>
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-12-04 18:38:25 +00:00
Joaquim VergesGitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
dffa457a39 Version Packages (#8498)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-12-03 19:50:37 -08:00
Joaquim VergesandGitHub 859032dc17 [SDK] Skip swap review step when coming from swap widget (#8496) 2025-12-03 19:23:42 -08:00
Joaquim VergesGitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
e0ee69b061 Version Packages (#8494)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-12-03 17:47:50 -08:00
Joaquim VergesandGitHub b6be8d67ef [SDK] Faster token balance load query (#8495) 2025-12-03 17:38:16 -08:00
Joaquim VergesandGitHub 6ddb15cf02 [SDK] Optimize payment methods query by removing quotes and improving balance checks (#8493) 2025-12-03 17:00:25 -08:00
joaquim-verges 551ec680df [SDK] Add payment selection tracking (#8490)
<!--

## 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 payment selection tracking throughout the `thirdweb` package by replacing `useQuery` calls with `useEffect` hooks to ensure that payment events are tracked only once per component lifecycle.

### Detailed summary
- Replaced `useQuery` for tracking payment events with `useEffect` in multiple components.
- Added `useRef` to prevent multiple event firings.
- Updated tracking events in:
  - `UnsupportedTokenScreen`
  - `BridgeWidget`
  - `ErrorBanner`
  - `QuoteLoader`
  - `PaymentSelection`
  - `PaymentDetails`
  - `SwapWidget`
  - `SuccessScreen`
  - `CheckoutWidget`
  - `TransactionWidget`
  - `BuyWidget`
  - `BridgeWidget`

> ✨ 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**
  * Refined Bridge telemetry: replaced reactive query-based tracking with guarded one-time render and interaction events to reduce duplicates.
  * Expanded analytics coverage for widgets, payment selection, loading/quote stages, payment details, swaps, unsupported-token and success flows.
  * Cleaned up and simplified tracking behavior: some legacy query-driven trackers were removed or consolidated for clearer, more reliable events.

<sub>✏️ Tip: You can customize this high-level summary in your review settings.</sub>
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-12-04 00:32:15 +00:00
Jonas Daniels 7b9ce493c8 upgrade next and react 2025-12-03 14:38:01 -08:00
0xFirekeeper b27fcd3ba0 Add Solana wallet docs and sidebar link (#8489)
Added a new sidebar entry for Solana Transactions in the wallets section and introduced a comprehensive Solana server wallet documentation page covering wallet management, balances, transactions, signing, swaps, and example API workflows.

Closes BLD-414

<!-- start pr-codex -->

---

## PR-Codex overview
This PR introduces support for managing Solana transactions in the application, including wallet creation, balance checking, token transfers, and swaps. It enhances the user interface with detailed documentation and examples for developers.

### Detailed summary
- Added a new sidebar entry for `Solana Transactions`.
- Created a detailed `page.mdx` for Solana server wallets.
- Included metadata for the `Solana Server Wallets` page.
- Documented capabilities: wallet management, balance checking, signing, token swaps, and custom transactions.
- Provided example workflows using `curl` commands for various operations.
- Defined network identifiers for `Mainnet` and `Devnet`.

> ✨ 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 Solana Transactions link in Server Wallets navigation for streamlined access to Solana wallet operations

* **Documentation**
  * New Solana server wallets documentation page with getting started guides, API workflows for wallet creation, balance retrieval, token transfers, transaction signing, and token swap operations

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

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-12-03 19:08:53 +00:00
MananTank c40603f36e [PRO-134] Add Bridge Volume pie chart in project overview page (#8488)
<!--

## 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 analytics components by refining data formatting, adding currency support, and improving the UI for better data visualization in the dashboard.

### Detailed summary
- Added `maximumFractionDigits` to formatting utilities.
- Integrated currency formatting in `PieChart` and `TotalValueChartHeader`.
- Updated `ProjectHighlightsCard` to remove total volume and adjust labels.
- Introduced `BridgeChartCard` for displaying bridge usage data.
- Enhanced `TotalVolumePieChart` to conditionally display total volume and adjusted data handling.

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

<!-- end pr-codex -->
2025-12-03 18:46:13 +00:00
MananTank 458246fea1 Dashboard: Project analytics code organization refactor (#8487)
<!--

## 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 several analytics components for a dashboard application, enhancing the project's data visualization capabilities. It includes new types for page parameters and search parameters, as well as multiple asynchronous chart components for different metrics.

### Detailed summary
- Added `PageParams` and `PageSearchParams` types for handling routing parameters.
- Implemented `AsyncRPCRequestsChartCard` and `RPCRequestsChartCard` for RPC usage analytics.
- Created `AsyncAiAnalytics` and `AIAnalyticsChartCard` for AI usage analytics.
- Introduced `AsyncIndexerRequestsChartCard` and `IndexerRequestsChartCard` for indexer request analytics.
- Developed `AsyncX402RequestsChart` and `X402RequestsChartCard` for x402 settlements analytics.
- Added `AsyncAppHighlightsCard` and `ProjectHighlightCard` for displaying project highlights.
- Refactored `ProjectAnalytics` function to integrate new chart components.
- Removed redundant code and improved structure for better maintainability.

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

## Release Notes

* **New Features**
  * Added AI token volume analytics chart to project dashboard
  * Added indexer/gateway request analytics chart
  * Added RPC usage analytics chart
  * Added X402 settlements analytics chart
  * Introduced project highlights card displaying wallet and bridge usage metrics

* **Refactor**
  * Reorganized project analytics dashboard structure for improved layout and user experience

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

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-12-03 18:46:13 +00:00
Joaquim VergesGitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
1b3e938b06 Version Packages (#8470)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-12-03 01:13:36 -08:00
Joaquim VergesandGitHub 3ebca18f9e [SDK] Add error tracking for payment execution failures (#8486) 2025-12-03 00:55:49 -08:00
Joaquim VergesandGitHub 4c5696e414 [SDK] Add caching and timeout for fetching capabilities (#8485) 2025-12-02 17:50:58 -08:00
Joaquim VergesandGitHub 9e43da4ec0 [SDK] Support ERC5792 batch transactions for swaps, add slippage option to Bridge API (#8484) 2025-12-02 15:49:37 -08:00
MananTank 22cea5bb8f Dashboard: Update bridge stats (#8483)
<!--

## 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 `supportedTokens` values in the `data.ts` file to reflect an increase in the number of supported tokens for the bridge feature.

### Detailed summary
- Updated `supportedTokens` from `"10,000+"` to `"14,000+"` (string format).
- Updated `supportedTokens` from `10000` to `14000` (numeric format).

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

* **Updates**
  * Bridge now supports 14,000+ tokens, increased from 10,000+.

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

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-12-02 21:43:51 +00:00
MananTank d0e4db26ce [PRO-136] Add x402 requests chart in project overview (#8482)
<!--

## 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 chart components and their presentation in the dashboard, including layout adjustments, new chart configurations, and improved user feedback for empty states.

### Detailed summary
- Updated `Card` component in `bar-chart.tsx` to include additional class names.
- Adjusted `aspect-auto` for `ThirdwebBarChart` in `AiTokenUsageChartCard`.
- Modified `WaitingForIntegrationCard` to conditionally render `TabButtons`.
- Enhanced empty state in `EmptyChartState` with a visual indicator and message.
- Added `aspect-auto` class to `SkeletonBarChart`.
- Introduced `X402RequestsChartCardUI` for handling X402 request data.
- Refactored project analytics page to include `X402RequestsChartCardUI` and manage its data fetching.

> ✨ 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 X402 requests analytics chart with per-period totals to the project dashboard.

* **Bug Fixes**
  * Tab selector now appears only when multiple code tabs exist.
  * Time filters are shown only when relevant data is present.
  * Empty-chart state now shows a clear "No data available" display.

* **Style**
  * Header styling refined for improved visuals on large screens.
  * Chart containers adjusted for better aspect/overflow handling.

<sub>✏️ Tip: You can customize this high-level summary in your review settings.</sub>
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-12-02 21:32:12 +00:00
MananTank 1056696696 [PRO-145] Dashboard: Remove Engine Cloud Requests chart from project overview (#8481)
<!--

## 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 the removal of the `EngineCloudChartCard` component and its related files, as well as the `EngineCloudStats` interface from the analytics types. It also updates the `ProjectAnalytics` component to eliminate the usage of `EngineCloudChartCardAsync`.

### Detailed summary
- Deleted files related to `EngineCloudChartCard`:
  - `index.tsx`
  - `EngineCloudBarChartCardUI.tsx`
  - `EngineCloudBarChartCard.stories.tsx`
- Removed `EngineCloudStats` interface from `analytics.ts`.
- Eliminated `EngineCloudChartCardAsync` import and usage in `ProjectAnalytics`.
- Added revalidation settings to cache functions in `analytics.ts`.

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

<!-- end pr-codex -->
2025-12-02 17:55:23 +00:00
MananTank 48297993d8 [PRO-132] Dashboard: Update Wallet Connection charts in project (#8480)
<!--

## 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 analytics and chart components related to wallet connections in the application. It introduces new types, refactors existing components, and updates links and chart functionalities to improve usability and data representation.

### Detailed summary
- Deleted unused files related to wallet analytics charts.
- Introduced `WalletStatsWithName` type for wallet statistics.
- Updated links to point to new endpoints.
- Refactored chart components to improve data handling and display.
- Added new `EOAConnectionsChart` and `UserWalletConnectionsChart` components.
- Replaced `InAppWalletAnalytics` with `UserWalletConnectionsChart`.
- Enhanced `RPCRequestsChartUI` to include an `isPending` prop.
- Updated metrics and labels in various charts for better clarity.
- Improved error handling in API calls for wallet connections.
- Removed deprecated code and streamlined analytics data fetching.

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

<!-- end pr-codex -->
2025-12-02 17:55:23 +00:00
MananTank cd9e42911e [PRO-135] Add AI token volume chart in project overview page (#8472)
<!--

## 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 AI features in the dashboard by updating UI components, improving navigation, and adding analytics capabilities for AI token usage.

### Detailed summary
- Changed `variant` of a `Button` in `ChatBar.tsx` from `primary` to `default`.
- Added `viewMoreLink` prop to `AiTokenUsageChartCardUI` in `index.tsx`.
- Updated sidebar structure in `ProjectSidebarLayout.tsx` to include submenus and links for AI.
- Modified redirection logic in `page.tsx` for missing sessions.
- Adjusted styling and structure of `ExportToCSVButton` in `ExportToCSVButton.tsx`.
- Removed the `Analytics` link from `ChatSidebar.tsx`.
- Removed unused state for token symbol in `project-wallet-details.tsx`.
- Introduced `AsyncAiAnalytics` function to fetch AI usage data in `page.tsx`.
- Updated `AiTokenUsageChartCardUI` to handle new props and improve chart presentation.
- Enhanced empty state message in `AiTokenUsageChartCardUI` with a button linking to AI chat documentation.

> ✨ 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**
  * AI token usage analytics card added to project dashboard with totals and chart.
  * AI sidebar reorganized into Chat and Analytics entries; AI analytics accessible from project pages.

* **Bug Fixes**
  * Missing chat session now redirects back to the AI section instead of showing an error.

* **Style**
  * Button and spinner visuals refined (export and send controls); chart sizing and empty-state text adjusted; chat sidebar streamlined.

<sub>✏️ Tip: You can customize this high-level summary in your review settings.</sub>
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-12-02 17:55:23 +00:00
Jonas Daniels 41e2ccb67d Add isLegacyPlan flag to Team type and UI components 2025-12-01 14:22:34 -08:00
Joaquim VergesandGitHub 68a1dcac26 [Playground] Improve token selection and UI for x402 playground (#8471) 2025-11-30 17:07:30 -08:00
Joaquim VergesandGitHub 98d8f292db [Dashboard] Show project server wallet balance for any token (#8469) 2025-11-30 15:01:14 -08:00
FirekeeperandGitHub 4f2dbccba4 small fixes to dotnet docs (#8468) 2025-11-27 17:38:46 +07:00
MananTank 908142f8fa [PRO-116] Move Server wallets table to separate page (#8467)
<!--

## 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 a new `Wallets` section to the application, enhancing the wallet management interface. It updates the routing and adds functionality to handle EVM and Solana wallets, including error handling and data fetching improvements.

### Detailed summary
- Added a new route for `Wallets` in `layout.tsx`.
- Updated the redirect path in `page.tsx` for unauthorized access.
- Removed deprecated error handling for EVM wallets.
- Implemented new logic for fetching and displaying wallets, including Solana accounts.
- Enhanced error handling for wallet retrieval and connection issues.

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

<!-- end pr-codex -->
2025-11-26 22:17:40 +00:00
MananTank 4d065c3774 [PRO-137] Add indexer and RPC charts in project overview page (#8464)
<!--

## 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 chart components in the dashboard by updating properties for better data visualization and integrating new functionalities for displaying RPC and status code requests.

### Detailed summary
- Updated `xAxis` property from `sameDay` to `showHour` in `rate-graph.tsx` and `count-graph.tsx`.
- Increased height of `Card` in `RpcMethodBarChartCardUI.tsx`.
- Changed title and description handling in `RequestsByStatusGraph.tsx`.
- Added `className` prop to `ThirdwebBarChart` component.
- Replaced `RequestsGraph` with `RPCRequestsChartUI` in `RPCAnalytics.tsx`.
- Introduced a new component `TotalValueChartHeader` for displaying total values.
- Updated `RequestsGraph` to include a breakdown by hour.
- Added asynchronous functions for fetching indexer and RPC request data in `page.tsx`.

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

<!-- end pr-codex -->
2025-11-26 22:05:19 +00:00
MananTank 426229050c [PRO-129] Dashboard: Remove AA analytics from project overview (#8463)
<!--

## 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 renaming and updating references from `Account Abstraction` to `Gas Sponsorship` across various components in the codebase, as well as removing the `TotalSponsoredCard` functionality.

### Detailed summary
- Removed `total-sponsored.tsx`.
- Updated `EmptyStateCard` metric from `Account Abstraction` to `Gas Sponsorship`.
- Modified `featureDescription` in `UpsellContent` to include `Gas Sponsorship`.
- Changed alert title from `Account Abstraction service is disabled` to `Gas Sponsorship is disabled`.
- Increased padding in `usage/layout.tsx`.
- Renamed usage path from `Account Abstraction` to `Gas Sponsorship`.
- Removed `TotalSponsoredCard` and its related functionality from `page.tsx`.

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

<!-- end pr-codex -->
2025-11-26 22:05:18 +00:00
MananTank 0fd55a2a13 [PRO-130] Move RPC methods chart from project overview to RPC tab (#8462)
<!--

## 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 UI components of the RPC analytics dashboard by refining styles, improving layout, and optimizing the rendering of charts and tables.

### Detailed summary
- Added `titleClassName` to `RequestsGraph` for improved styling.
- Removed unnecessary `showLegend` from `RequestsGraph`.
- Simplified `CardHeader` and adjusted padding in `RpcMethodBarChartCardCardUI`.
- Reintroduced `RpcMethodBarChartCardAsync` in `RPCAnalytics` with proper parameters.
- Updated layout and styling in `TopRPCMethodsTable`, replacing `Card` with a `div` and enhancing header styles.

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

<!-- end pr-codex -->
2025-11-26 22:05:18 +00:00
MananTank 59ad7e8b09 [PRO-127] Dashboard: Move user wallets table to separate tab (#8460)
<!--

## 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 user wallet components in the dashboard application, replacing the `InAppWalletUsersPageContent` with `UserWalletsTable` and enhancing the user wallet functionalities.

### Detailed summary
- Added a new route for `Users` in `layout.tsx`.
- Replaced `InAppWalletUsersPageContent` with `UserWalletsTable` in `page.tsx`.
- Introduced a new `Page` component for user wallets with authentication and project validation.
- Updated `UserWalletsTable` to include necessary props and improve layout.

> ✨ 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 "Users" tab in the wallets area and a dedicated Users page showing user wallets (search, download and pagination available).

* **Refactor**
  * Consolidated and simplified the user-wallets layout and table for clearer hierarchy and tighter spacing.
  * Moved action controls (search/download) into the header area and adjusted alignment for improved usability.

<sub>✏️ Tip: You can customize this high-level summary in your review settings.</sub>
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-11-26 22:05:18 +00:00
Joaquim VergesGitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
fda2284036 Version Packages (#8466)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-11-26 14:03:39 -08:00
Joaquim VergesandGitHub 629efc13b6 [SDK] Add Monad chain definition (#8465) 2025-11-26 13:46:02 -08:00
Joaquim VergesandGitHub 512f9dd3c9 [Docs] Fix AI model response handling in server example (#8459) 2025-11-26 11:05:01 -08:00
jnsdls 919230f0c8 Add daily revalidation to TypeScript pages (#8456)
Added daily revalidation to TypeScript reference pages

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

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

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

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

<!-- start pr-codex -->

---

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

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

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

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

Closes BLD-500

<!-- start pr-codex -->

---

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

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

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

<!-- end pr-codex -->

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

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

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

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

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

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

## Notes for the reviewer

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

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

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

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

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

<!-- end pr-codex -->

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

## Summary by CodeRabbit

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

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

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

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

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

## Notes for the reviewer

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

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

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

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

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

<!-- end pr-codex -->

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

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

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

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

## Notes for the reviewer

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

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

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

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

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

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

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

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

## Notes for the reviewer

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

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

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

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

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

<!-- end pr-codex -->

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

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

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

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

## Notes for the reviewer

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

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

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

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

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

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

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

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

## Notes for the reviewer

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

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

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

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

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

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

<!-- end pr-codex -->

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

## Summary by CodeRabbit

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

* **Chores**
  * Updated project dependencies.

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

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

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

## Notes for the reviewer

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

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

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

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

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

<!-- end pr-codex -->

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

## Summary by CodeRabbit

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

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

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-11-18 16:30:46 +00:00
344 changed files with 13405 additions and 6739 deletions
+5 -2
View File
@@ -1,4 +1,6 @@
{
"editor.insertSpaces": true,
"editor.tabSize": 2,
"[css]": {
"editor.defaultFormatter": "biomejs.biome"
},
@@ -18,8 +20,9 @@
"editor.defaultFormatter": "biomejs.biome"
},
"editor.codeActionsOnSave": {
"quickfix.biome": "explicit",
"source.fixAll.biome": "explicit"
"quickfix.biome": "always",
"source.fixAll.biome": "always",
"source.organizeImports.biome": "always"
},
"editor.defaultFormatter": "biomejs.biome",
"editor.formatOnSave": true,
+3
View File
@@ -66,3 +66,6 @@ ANALYTICS_SERVICE_URL=""
STRIPE_SECRET_KEY=""
GROWTH_PLAN_SKU=""
PAYMENT_METHOD_CONFIGURATION=""
# required for bridge iframe (/bridge/widget) page
NEXT_PUBLIC_BRIDGE_IFRAME_CLIENT_ID=""
+3
View File
@@ -68,4 +68,7 @@ module.exports = [
"/token",
"/vault",
"/monetize/bridge",
// ai
"/ai-privacy-policy",
"/ai-terms",
];
+23
View File
@@ -18,6 +18,11 @@ const ContentSecurityPolicy = `
block-all-mixed-content;
`;
const EmbedContentSecurityPolicy = `
${ContentSecurityPolicy}
frame-ancestors *;
`;
const securityHeaders = [
{
key: "X-DNS-Prefetch-Control",
@@ -138,6 +143,24 @@ const baseNextConfig: NextConfig = {
// Apply these headers to all routes in your application.
source: "/(.*)",
},
{
headers: [
{
key: "Content-Security-Policy",
value: EmbedContentSecurityPolicy.replace(/\s{2,}/g, " ").trim(),
},
],
source: "/bridge/widget",
},
{
headers: [
{
key: "Content-Security-Policy",
value: EmbedContentSecurityPolicy.replace(/\s{2,}/g, " ").trim(),
},
],
source: "/bridge/widget/:path*",
},
];
},
images: {
+130 -129
View File
@@ -1,131 +1,132 @@
{
"dependencies": {
"@hookform/resolvers": "^3.9.1",
"@marsidev/react-turnstile": "^1.1.0",
"@number-flow/react": "^0.5.10",
"@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",
"@radix-ui/react-label": "^2.1.7",
"@radix-ui/react-popover": "^1.1.14",
"@radix-ui/react-progress": "^1.1.7",
"@radix-ui/react-radio-group": "^1.3.7",
"@radix-ui/react-select": "^2.2.5",
"@radix-ui/react-separator": "^1.1.7",
"@radix-ui/react-slot": "^1.2.3",
"@radix-ui/react-switch": "^1.2.5",
"@radix-ui/react-tooltip": "1.2.7",
"@sentry/nextjs": "9.34.0",
"@shazow/whatsabi": "0.22.2",
"@stripe/react-stripe-js": "4.0.2",
"@stripe/stripe-js": "7.9.0",
"@tanstack/react-query": "5.81.5",
"@tanstack/react-table": "^8.21.3",
"@thirdweb-dev/api": "workspace:*",
"@thirdweb-dev/service-utils": "workspace:*",
"@thirdweb-dev/vault-sdk": "workspace:*",
"@vercel/functions": "2.2.2",
"@vercel/og": "^0.6.8",
"@workspace/ui": "workspace:*",
"abitype": "1.0.8",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"compare-versions": "^6.1.0",
"date-fns": "4.1.0",
"fast-xml-parser": "^5.2.5",
"fetch-event-stream": "0.1.5",
"fuse.js": "7.1.0",
"input-otp": "^1.4.1",
"ioredis": "^5.6.1",
"ipaddr.js": "^2.2.0",
"lucide-react": "0.525.0",
"next": "15.3.5",
"next-themes": "^0.4.6",
"nextjs-toploader": "^1.6.12",
"nuqs": "^2.4.3",
"papaparse": "^5.5.3",
"pluralize": "^8.0.0",
"posthog-js": "1.256.1",
"prettier": "3.6.2",
"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": "6.0.0",
"react-hook-form": "7.55.0",
"react-markdown": "10.1.0",
"react-table": "^7.8.0",
"recharts": "2.15.3",
"remark-gfm": "4.0.1",
"responsive-rsc": "0.0.7",
"server-only": "^0.0.1",
"shiki": "3.12.0",
"sonner": "2.0.6",
"spdx-correct": "^3.2.0",
"stripe": "17.7.0",
"swagger-ui-react": "^5.24.1",
"tailwind-merge": "^2.6.0",
"tailwindcss-animate": "^1.0.7",
"thirdweb": "workspace:*",
"tiny-invariant": "^1.3.3",
"use-debounce": "^10.0.5",
"vaul": "^1.1.2",
"zod": "3.25.75"
},
"devDependencies": {
"@biomejs/biome": "2.0.6",
"@chromatic-com/storybook": "4.0.1",
"@next/bundle-analyzer": "15.3.5",
"@next/eslint-plugin-next": "15.3.5",
"@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",
"@types/pluralize": "^0.0.33",
"@types/react": "19.1.8",
"@types/react-dom": "19.1.6",
"@types/react-table": "^7.7.20",
"@types/spdx-correct": "^3.1.3",
"@types/swagger-ui-react": "^5.18.0",
"@typescript-eslint/eslint-plugin": "7.14.1",
"@typescript-eslint/parser": "7.14.1",
"autoprefixer": "^10.4.21",
"eslint": "8.57.0",
"eslint-config-biome": "1.9.4",
"eslint-plugin-react-compiler": "19.1.0-rc.2",
"eslint-plugin-storybook": "9.0.15",
"knip": "5.60.2",
"next-sitemap": "^4.2.3",
"postcss": "8.5.6",
"storybook": "9.0.15",
"tailwindcss": "3.4.17",
"typescript": "5.8.3"
},
"name": "thirdweb-dashboard",
"private": true,
"scripts": {
"build": "NODE_OPTIONS=--max-old-space-size=6144 next build",
"build-storybook": "storybook build",
"build:analyze": "ANALYZE=true pnpm run build",
"dev": "next dev --turbopack",
"fix": "biome check ./src --fix && eslint ./src --fix",
"format": "biome format ./src --write",
"knip": "knip",
"lint": "biome check ./src && knip && eslint ./src",
"postbuild": "next-sitemap",
"preinstall": "npx only-allow pnpm",
"start": "next start",
"storybook": "storybook dev -p 6006",
"typecheck": "tsc --noEmit"
},
"version": "3.0.0"
"dependencies": {
"@hookform/resolvers": "^3.9.1",
"@marsidev/react-turnstile": "^1.1.0",
"@number-flow/react": "^0.5.10",
"@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",
"@radix-ui/react-label": "^2.1.7",
"@radix-ui/react-popover": "^1.1.14",
"@radix-ui/react-progress": "^1.1.7",
"@radix-ui/react-radio-group": "^1.3.7",
"@radix-ui/react-select": "^2.2.5",
"@radix-ui/react-separator": "^1.1.7",
"@radix-ui/react-slot": "^1.2.3",
"@radix-ui/react-switch": "^1.2.5",
"@radix-ui/react-tooltip": "1.2.7",
"@sentry/nextjs": "9.34.0",
"@shazow/whatsabi": "0.22.2",
"@stripe/react-stripe-js": "4.0.2",
"@stripe/stripe-js": "7.9.0",
"@tanstack/react-query": "5.81.5",
"@tanstack/react-table": "^8.21.3",
"@thirdweb-dev/api": "workspace:*",
"@thirdweb-dev/service-utils": "workspace:*",
"@thirdweb-dev/vault-sdk": "workspace:*",
"@vercel/functions": "2.2.2",
"@vercel/og": "^0.6.8",
"@workspace/ui": "workspace:*",
"abitype": "1.0.8",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"compare-versions": "^6.1.0",
"date-fns": "4.1.0",
"fast-xml-parser": "^5.2.5",
"fetch-event-stream": "0.1.5",
"fuse.js": "7.1.0",
"input-otp": "^1.4.1",
"ioredis": "^5.6.1",
"ipaddr.js": "^2.2.0",
"lucide-react": "0.525.0",
"next": "15.3.6",
"next-themes": "^0.4.6",
"nextjs-toploader": "^1.6.12",
"nuqs": "^2.4.3",
"p-limit": "^7.2.0",
"papaparse": "^5.5.3",
"pluralize": "^8.0.0",
"posthog-js": "1.256.1",
"prettier": "3.6.2",
"react": "19.2.1",
"react-children-utilities": "^2.10.0",
"react-day-picker": "^8.10.1",
"react-dom": "19.2.1",
"react-dropzone": "^14.3.8",
"react-error-boundary": "6.0.0",
"react-hook-form": "7.55.0",
"react-markdown": "10.1.0",
"react-table": "^7.8.0",
"recharts": "2.15.3",
"remark-gfm": "4.0.1",
"responsive-rsc": "0.0.7",
"server-only": "^0.0.1",
"shiki": "3.12.0",
"sonner": "2.0.6",
"spdx-correct": "^3.2.0",
"stripe": "17.7.0",
"swagger-ui-react": "^5.24.1",
"tailwind-merge": "^2.6.0",
"tailwindcss-animate": "^1.0.7",
"thirdweb": "workspace:*",
"tiny-invariant": "^1.3.3",
"use-debounce": "^10.0.5",
"vaul": "^1.1.2",
"zod": "3.25.75"
},
"devDependencies": {
"@biomejs/biome": "2.0.6",
"@chromatic-com/storybook": "4.0.1",
"@next/bundle-analyzer": "15.3.6",
"@next/eslint-plugin-next": "15.3.6",
"@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",
"@types/pluralize": "^0.0.33",
"@types/react": "19.2.7",
"@types/react-dom": "19.2.3",
"@types/react-table": "^7.7.20",
"@types/spdx-correct": "^3.1.3",
"@types/swagger-ui-react": "^5.18.0",
"@typescript-eslint/eslint-plugin": "7.14.1",
"@typescript-eslint/parser": "7.14.1",
"autoprefixer": "^10.4.21",
"eslint": "8.57.0",
"eslint-config-biome": "1.9.4",
"eslint-plugin-react-compiler": "19.1.0-rc.2",
"eslint-plugin-storybook": "9.0.15",
"knip": "5.60.2",
"next-sitemap": "^4.2.3",
"postcss": "8.5.6",
"storybook": "9.0.15",
"tailwindcss": "3.4.17",
"typescript": "5.8.3"
},
"name": "thirdweb-dashboard",
"private": true,
"scripts": {
"build": "NODE_OPTIONS=--max-old-space-size=6144 next build",
"build-storybook": "storybook build",
"build:analyze": "ANALYZE=true pnpm run build",
"dev": "next dev --turbopack",
"fix": "biome check ./src --fix && eslint ./src --fix",
"format": "biome format ./src --write",
"knip": "knip",
"lint": "biome check ./src && knip && eslint ./src",
"postbuild": "next-sitemap",
"preinstall": "npx only-allow pnpm",
"start": "next start",
"storybook": "storybook dev -p 6006",
"typecheck": "tsc --noEmit"
},
"version": "3.0.0"
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

+5 -1
View File
@@ -1,6 +1,6 @@
"use server";
import { revalidatePath } from "next/cache";
import { revalidatePath, revalidateTag } from "next/cache";
export async function revalidatePathAction(
path: string,
@@ -8,3 +8,7 @@ export async function revalidatePathAction(
) {
revalidatePath(path, type);
}
export async function revalidateCacheTagAction(tag: string) {
revalidateTag(tag);
}
+4 -4
View File
@@ -232,13 +232,13 @@ type TokenSwapParams = {
buyTokenAddress: string;
sellTokenChainId: number;
sellTokenAddress: string;
pageType: "asset" | "bridge" | "chain";
pageType: "asset" | "bridge" | "chain" | "bridge-iframe";
};
type TokenBuyParams = {
buyTokenChainId: number | undefined;
buyTokenAddress: string | undefined;
pageType: "asset" | "bridge" | "chain";
pageType: "asset" | "bridge" | "chain" | "bridge-iframe";
};
/**
@@ -298,7 +298,7 @@ export function reportTokenSwapSuccessful(properties: TokenSwapParams) {
* @MananTank
*/
export function reportSwapWidgetShown(properties: {
pageType: "asset" | "bridge" | "chain";
pageType: "asset" | "bridge" | "chain" | "bridge-iframe";
}) {
posthog.capture("swap widget shown", properties);
}
@@ -683,7 +683,7 @@ export function reportProductFeedback(properties: {
*
*/
export function reportBridgePageLinkClick(params: {
linkType: "bridge-docs" | "trending-tokens";
linkType: "bridge-docs" | "trending-tokens" | "integrate-bridge-widget";
}) {
posthog.capture("bridge page link clicked", params);
}
+139 -47
View File
@@ -1,19 +1,20 @@
import "server-only";
import { unstable_cache } from "next/cache";
import { getWalletInfo, type WalletId } from "thirdweb/wallets";
import { ANALYTICS_SERVICE_URL } from "@/constants/server-envs";
import { normalizeTime } from "@/lib/time";
import type {
AIUsageStats,
AnalyticsQueryParams,
EcosystemWalletStats,
EngineCloudStats,
InAppWalletStats,
TransactionStats,
UniversalBridgeStats,
UniversalBridgeWalletStats,
UserOpStats,
WalletStats,
WalletStatsWithName,
WebhookLatencyStats,
WebhookSummaryStats,
X402QueryParams,
@@ -148,13 +149,7 @@ const cached_getWalletConnections = unstable_cache(
}
const json = await res.json();
return (json.data as WalletStats[]).filter(
(w) =>
w.walletType !== "smart" &&
w.walletType !== "smartWallet" &&
w.walletType !== "inApp" &&
w.walletType !== "inAppWallet",
);
return json.data as WalletStats[];
},
["getWalletConnections"],
{
@@ -162,11 +157,116 @@ const cached_getWalletConnections = unstable_cache(
},
);
export function getWalletConnections(
const cache_getEOAWalletConnections = unstable_cache(
async (
params: AnalyticsQueryParams,
authToken: string,
): Promise<WalletStatsWithName[]> => {
const result = await cached_getWalletConnections(
normalizedParams(params),
authToken,
);
const filteredResult = result.filter(
(w) =>
w.walletType !== "smart" &&
w.walletType !== "smartWallet" &&
w.walletType !== "inApp" &&
w.walletType !== "inAppWallet",
);
const modifiedResult = await Promise.all(
filteredResult.map(async (w) => {
const wallet = await getWalletInfo(w.walletType as WalletId).catch(
() => undefined,
);
if (!wallet) {
return {
...w,
walletName: w.walletType,
};
}
return {
...w,
walletName: wallet.name,
};
}),
);
return modifiedResult;
},
["getEOAWalletConnections"],
{
revalidate: 60 * 60, // 1 hour
},
);
export async function getEOAWalletConnections(
params: AnalyticsQueryParams,
authToken: string,
) {
return cached_getWalletConnections(normalizedParams(params), authToken);
return cache_getEOAWalletConnections(normalizedParams(params), authToken);
}
const cache_getEOAAndInAppWalletConnections = unstable_cache(
async (
params: AnalyticsQueryParams,
authToken: string,
): Promise<WalletStatsWithName[]> => {
const result = await cached_getWalletConnections(
normalizedParams(params),
authToken,
);
const modifiedResult = await Promise.all(
result.map(async (w) => {
if (
w.walletType === "inApp" ||
w.walletType === "inAppWallet" ||
w.walletType === "smart" ||
w.walletType === "smartWallet"
) {
return {
...w,
walletName: "User wallet",
};
}
const wallet = await getWalletInfo(w.walletType as WalletId).catch(
() => undefined,
);
if (!wallet) {
return {
...w,
walletName: w.walletType,
};
}
return {
...w,
walletName: wallet.name,
};
}),
);
return modifiedResult;
},
["getEOAAndInAppWalletConnections"],
{
revalidate: 60 * 60, // 1 hour
},
);
export async function getEOAAndInAppWalletConnections(
params: AnalyticsQueryParams,
authToken: string,
): Promise<WalletStatsWithName[]> {
return cache_getEOAAndInAppWalletConnections(
normalizedParams(params),
authToken,
);
}
const cached_getInAppWalletUsage = unstable_cache(
@@ -291,8 +391,11 @@ const cached_getAggregateUserOpUsage = unstable_cache(
]);
// Aggregate stats across wallet types
return userOpStats.reduce(
(acc, curr) => {
const aggregated = userOpStats.reduce(
(
acc: UserOpStats & { gasPriceSum: number; gasPriceCount: number },
curr,
) => {
// Skip testnets from the aggregated stats
if (curr.chainId) {
const chain = chains.data.find(
@@ -306,6 +409,10 @@ const cached_getAggregateUserOpUsage = unstable_cache(
acc.successful += curr.successful;
acc.failed += curr.failed;
acc.sponsoredUsd += curr.sponsoredUsd;
acc.gasUnits += curr.gasUnits;
// For avgGasPrice, we'll track sum and count for proper averaging
acc.gasPriceSum += curr.avgGasPrice * curr.successful;
acc.gasPriceCount += curr.successful;
return acc;
},
{
@@ -313,8 +420,28 @@ const cached_getAggregateUserOpUsage = unstable_cache(
failed: 0,
sponsoredUsd: 0,
successful: 0,
gasUnits: 0,
avgGasPrice: 0,
gasPriceSum: 0,
gasPriceCount: 0,
},
);
// Calculate the proper average gas price
aggregated.avgGasPrice =
aggregated.gasPriceCount > 0
? aggregated.gasPriceSum / aggregated.gasPriceCount
: 0;
// Return only the UserOpStats fields
return {
date: aggregated.date,
failed: aggregated.failed,
sponsoredUsd: aggregated.sponsoredUsd,
successful: aggregated.successful,
gasUnits: aggregated.gasUnits,
avgGasPrice: aggregated.avgGasPrice,
};
},
["getAggregateUserOpUsage"],
{
@@ -664,41 +791,6 @@ export function getUniversalBridgeWalletUsage(
);
}
const cached_getEngineCloudMethodUsage = unstable_cache(
async (
params: AnalyticsQueryParams,
authToken: string,
): Promise<EngineCloudStats[]> => {
const searchParams = buildSearchParams(params);
const res = await fetchAnalytics({
authToken,
url: `v2/engine-cloud/requests?${searchParams.toString()}`,
init: {
method: "GET",
},
});
if (res?.status !== 200) {
console.error("Failed to fetch Engine Cloud method usage");
return [];
}
const json = await res.json();
return json.data as EngineCloudStats[];
},
["getEngineCloudMethodUsage"],
{
revalidate: 60 * 60, // 1 hour
},
);
export function getEngineCloudMethodUsage(
params: AnalyticsQueryParams,
authToken: string,
) {
return cached_getEngineCloudMethodUsage(normalizedParams(params), authToken);
}
const _cached_getWebhookSummary = unstable_cache(
async (
params: AnalyticsQueryParams & { webhookId: string },
@@ -0,0 +1,6 @@
export function projectContractsCacheTag(params: {
teamId: string;
projectId: string;
}) {
return `${params.teamId}/${params.projectId}/project-contracts`;
}
@@ -1,58 +1,111 @@
import { unstable_cache } from "next/cache";
import pLimit from "p-limit";
import { defineChain } from "thirdweb";
import { getContract } from "thirdweb/contract";
import {
getProjectContracts,
type ProjectContract,
} from "@/api/project/getProjectContracts";
import { fetchChainWithLocalOverrides } from "@/utils/fetchChainWithLocalOverrides";
import { supportedERCs } from "@/utils/supportedERCs";
import { serverThirdwebClient } from "../../constants/thirdweb-client.server";
import { resolveFunctionSelectors } from "../../lib/selectors";
import { projectContractsCacheTag } from "./cache-tag";
export async function getSortedDeployedContracts(params: {
onlyMainnet?: boolean;
type GetFilteredProjectContractsParams = {
teamId: string;
projectId: string;
authToken: string;
deploymentType: string | undefined;
}) {
};
type ProjectContractWithIsToken = ProjectContract & { isToken: boolean };
async function getProjectContractsWithIsTokenInfo(
params: GetFilteredProjectContractsParams,
): Promise<ProjectContractWithIsToken[]> {
const contracts = await getProjectContracts({
authToken: params.authToken,
deploymentType: params.deploymentType,
deploymentType: undefined, // don't pass it to api to fetch all contracts, we'll filter/merge manually here
projectId: params.projectId,
teamId: params.teamId,
});
const chainIds = Array.from(new Set(contracts.map((c) => c.chainId)));
const chains = (
await Promise.allSettled(
chainIds.map((chainId) =>
fetchChainWithLocalOverrides(chainId.toString()),
),
)
)
.filter((c) => c.status === "fulfilled")
.map((c) => c.value)
.filter((c) => c !== null);
const assetContracts = contracts.filter((c) => c.deploymentType === "asset");
const otherContracts = contracts.filter((c) => c.deploymentType !== "asset");
const mainnetContracts: ProjectContract[] = [];
const testnetContracts: ProjectContract[] = [];
// "asset" deployment type contract is assumed to be a "token" type contract already
// for others, check if its a "token" type contract (isERC20, isERC721 or isERC1155)
for (const contract of contracts) {
const chain = chains.find(
(chain) => contract.chainId === chain.chainId.toString(),
const limit = pLimit(20);
const othersContractWithIsTokenInfo: ProjectContractWithIsToken[] =
await Promise.all(
otherContracts.map(async (c) => {
return limit(async () => {
try {
const contract = getContract({
address: c.contractAddress,
// eslint-disable-next-line no-restricted-syntax
chain: defineChain(Number(c.chainId)),
client: serverThirdwebClient,
});
const functionSelectors = await resolveFunctionSelectors(contract);
const ercs = supportedERCs(functionSelectors);
if (ercs.isERC20 || ercs.isERC721 || ercs.isERC1155) {
return {
...c,
isToken: true,
};
}
return {
...c,
isToken: false,
};
} catch {
return {
...c,
isToken: false,
};
}
});
}),
);
if (chain && chain.status !== "deprecated") {
if (chain.testnet) {
testnetContracts.push(contract);
} else {
mainnetContracts.push(contract);
}
for (const c of othersContractWithIsTokenInfo) {
if (c.deploymentType === "asset") {
assetContracts.push(c);
} else {
otherContracts.push(c);
}
}
mainnetContracts.sort((a, b) => Number(a.chainId) - Number(b.chainId));
if (params.onlyMainnet) {
return mainnetContracts;
}
testnetContracts.sort((a, b) => Number(a.chainId) - Number(b.chainId));
return [...mainnetContracts, ...testnetContracts];
return [
...assetContracts.map(
(c) => ({ ...c, isToken: true }) as ProjectContractWithIsToken,
),
...othersContractWithIsTokenInfo,
];
}
export async function getFilteredProjectContracts(
params: GetFilteredProjectContractsParams & {
type: "token-contracts" | "non-token-contracts";
},
) {
const cached_getProjectContractsWithIsTokenInfo = unstable_cache(
getProjectContractsWithIsTokenInfo,
["get-project-contracts-with-is-token-info"],
{
revalidate: 3600, // 1 hour
tags: [projectContractsCacheTag(params)],
},
);
const contracts = await cached_getProjectContractsWithIsTokenInfo(params);
if (params.type === "token-contracts") {
return contracts.filter((c) => c.isToken);
} else {
return contracts.filter((c) => !c.isToken);
}
}
+4 -1
View File
@@ -8,7 +8,10 @@ import { NEXT_PUBLIC_THIRDWEB_API_HOST } from "@/constants/public-envs";
import { API_SERVER_SECRET } from "@/constants/server-envs";
import { getMemberByAccountId } from "./team-members";
export type Team = TeamResponse & { stripeCustomerId: string | null };
export type Team = TeamResponse & {
stripeCustomerId: string | null;
isLegacyPlan: boolean;
};
export async function getTeamBySlug(slug: string) {
const token = await getAuthToken();
+27
View File
@@ -0,0 +1,27 @@
import type { Project } from "@/api/project/projects";
type X402Fee = {
feeRecipient: string;
feeBps: number;
};
/**
* Extract x402 fee configuration from project's engineCloud service
*/
export function getX402Fees(project: Project): X402Fee {
const engineCloudService = project.services.find(
(service) => service.name === "engineCloud",
);
if (!engineCloudService) {
return {
feeRecipient: "",
feeBps: 0,
};
}
return {
feeRecipient: engineCloudService.x402FeeRecipient || "",
feeBps: engineCloudService.x402FeeBPS || 0,
};
}
@@ -1,42 +1,23 @@
"use client";
import { useId, useMemo } from "react";
import { Area, AreaChart, Bar, BarChart } from "recharts";
import { type ChartConfig, ChartContainer } from "@/components/ui/chart";
import { ArrowUpRightIcon, XIcon } from "lucide-react";
import Link from "next/link";
import { LoadingDots } from "@/components/ui/LoadingDots";
import { cn } from "@/lib/utils";
import { Button } from "../ui/button";
type FakeCartData = {
value: number;
};
function generateRandomData(): FakeCartData[] {
return Array.from({ length: 30 }, () => ({
value: Math.floor(Math.random() * 100 + 30),
}));
}
const skeletonChartConfig = {
value: {
color: "hsl(var(--muted)/90%)",
label: "Value",
},
} satisfies ChartConfig;
export function EmptyChartState({
children,
type,
}: {
children?: React.ReactNode;
type: "bar" | "area";
}) {
const barChartData = useMemo(() => generateRandomData(), []);
export function EmptyChartState({ content }: { content?: React.ReactNode }) {
return (
<div className="relative z-0 h-full w-full">
<div className="absolute inset-0 z-[1] flex flex-col items-center justify-center font-medium text-base text-muted-foreground">
{children ?? "No data available"}
<div className="absolute inset-0 z-[1] flex flex-col items-center justify-center text-base text-muted-foreground">
{content || (
<div className="flex items-center gap-3 flex-col">
<div className="rounded-full border p-2 bg-background">
<XIcon className="size-4" />
</div>
<p className="text-sm"> No data available </p>
</div>
)}
</div>
<SkeletonBarChart data={barChartData} type={type} />
</div>
);
}
@@ -45,7 +26,7 @@ export function LoadingChartState({ className }: { className?: string }) {
return (
<div
className={cn(
"pointer-events-none flex h-full w-full items-center justify-center rounded-lg bg-muted/30",
"pointer-events-none flex h-full w-full items-center justify-center bg-card rounded-lg",
className,
)}
>
@@ -54,51 +35,42 @@ export function LoadingChartState({ className }: { className?: string }) {
);
}
function SkeletonBarChart(props: {
data: FakeCartData[];
type: "bar" | "area";
export function EmptyChartStateGetStartedCTA(props: {
link?: {
label: string;
href: string;
};
title: string;
description?: string;
}) {
const fillAreaSkeletonId = useId();
return (
<ChartContainer
className="pointer-events-none h-full w-full blur-[5px]"
config={skeletonChartConfig}
>
{props.type === "bar" ? (
<BarChart
data={props.data}
margin={{
top: 20,
}}
<div className="flex items-center flex-col">
<div className="rounded-full border p-2 mb-4">
<XIcon className="size-4 text-foreground" />
</div>
<div className="mb-5 space-y-1">
<h3 className="text-base text-foreground text-center font-medium">
{props.title}
</h3>
{props.description && (
<p className="text-center text-sm text-muted-foreground">
{props.description}
</p>
)}
</div>
{props.link && (
<Button
asChild
className="rounded-full gap-2"
variant="default"
size="sm"
>
<Bar dataKey="value" fill="var(--color-value)" radius={8} />
</BarChart>
) : (
<AreaChart
data={props.data}
margin={{
top: 20,
}}
>
<defs>
<linearGradient id={fillAreaSkeletonId} x1="0" x2="0" y1="0" y2="1">
<stop
offset="5%"
stopColor={"hsl(var(--muted-foreground)/0.5)"}
stopOpacity={0.8}
/>
<stop offset="95%" stopColor="transparent" stopOpacity={0.1} />
</linearGradient>
</defs>
<Area
dataKey="value"
fill={`url(#${fillAreaSkeletonId})`}
radius={8}
stroke="hsl(var(--muted-foreground))"
type="monotone"
/>
</AreaChart>
<Link href={props.link.href}>
{props.link.label} <ArrowUpRightIcon className="size-4" />
</Link>
</Button>
)}
</ChartContainer>
</div>
);
}
@@ -4,7 +4,11 @@
import { useTheme } from "next-themes";
import { useEffect, useMemo, useRef, useState } from "react";
import { defineChain } from "thirdweb";
import { BuyWidget, SwapWidget } from "thirdweb/react";
import {
BuyWidget,
type SupportedFiatCurrency,
SwapWidget,
} from "thirdweb/react";
import type { Wallet } from "thirdweb/wallets";
import {
reportAssetBuyCancelled,
@@ -21,6 +25,7 @@ import {
import { Button } from "@/components/ui/button";
import {
NEXT_PUBLIC_ASSET_PAGE_CLIENT_ID,
NEXT_PUBLIC_BRIDGE_IFRAME_CLIENT_ID,
NEXT_PUBLIC_BRIDGE_PAGE_CLIENT_ID,
NEXT_PUBLIC_CHAIN_PAGE_CLIENT_ID,
} from "@/constants/public-envs";
@@ -30,9 +35,10 @@ import { getSDKTheme } from "@/utils/sdk-component-theme";
import { appMetadata } from "../../constants/connect";
import { getConfiguredThirdwebClient } from "../../constants/thirdweb.server";
type PageType = "asset" | "bridge" | "chain";
type PageType = "asset" | "bridge" | "chain" | "bridge-iframe";
export type BuyAndSwapEmbedProps = {
persistTokenSelections?: boolean;
buyTab:
| {
buyToken:
@@ -64,6 +70,7 @@ export type BuyAndSwapEmbedProps = {
| undefined;
pageType: PageType;
wallets?: Wallet[];
currency?: SupportedFiatCurrency;
};
export function BuyAndSwapEmbed(props: BuyAndSwapEmbedProps) {
@@ -92,7 +99,9 @@ export function BuyAndSwapEmbed(props: BuyAndSwapEmbedProps) {
? NEXT_PUBLIC_BRIDGE_PAGE_CLIENT_ID
: props.pageType === "chain"
? NEXT_PUBLIC_CHAIN_PAGE_CLIENT_ID
: undefined,
: props.pageType === "bridge-iframe"
? NEXT_PUBLIC_BRIDGE_IFRAME_CLIENT_ID
: undefined,
secretKey: undefined,
teamId: undefined,
});
@@ -115,6 +124,7 @@ export function BuyAndSwapEmbed(props: BuyAndSwapEmbedProps) {
{tab === "buy" && (
<BuyWidget
currency={props.currency || "USD"}
amount={props.buyTab?.buyToken?.amount || "1"}
chain={
props.buyTab?.buyToken?.chainId
@@ -238,6 +248,8 @@ export function BuyAndSwapEmbed(props: BuyAndSwapEmbedProps) {
{tab === "swap" && (
<SwapWidget
currency={props.currency || "USD"}
persistTokenSelections={props.persistTokenSelections}
client={client}
theme={themeObj}
className="!rounded-2xl !border-none"
@@ -34,7 +34,8 @@ export function ExportToCSVButton(props: {
return (
<Button
className={cn("flex items-center gap-2 border text-xs", props.className)}
className={cn("gap-2 rounded-full", props.className)}
size="sm"
disabled={props.disabled || exportMutation.isPending}
onClick={async () => {
exportMutation.mutate();
@@ -42,16 +43,11 @@ export function ExportToCSVButton(props: {
variant="outline"
>
{exportMutation.isPending ? (
<>
Downloading
<Spinner className="size-3" />
</>
<Spinner className="size-3.5 text-muted-foreground" />
) : (
<>
<DownloadIcon className="size-3" />
Export as CSV
</>
<DownloadIcon className="size-3.5 text-muted-foreground" />
)}
Export
</Button>
);
}
@@ -70,6 +70,7 @@ function Variants() {
key={currentPlan}
requiredPlan={requiredPlan}
teamSlug="foo"
isLegacyPlan={false}
/>
</BadgeContainer>
))}
@@ -14,6 +14,7 @@ type GatedSwitchProps = {
trackingLabel?: string;
currentPlan: Team["billingPlan"];
requiredPlan: Team["billingPlan"];
isLegacyPlan: boolean;
teamSlug: string;
switchProps?: SwitchProps;
};
@@ -33,7 +34,7 @@ export const GatedSwitch: React.FC<GatedSwitchProps> = (
<div className="w-full min-w-[280px]">
<h3 className="font-medium text-base">
<span className="capitalize">
{getTeamPlanBadgeLabel(props.requiredPlan)}+
{getTeamPlanBadgeLabel(props.requiredPlan, false)}+
</span>{" "}
plan required
</h3>
@@ -48,7 +49,8 @@ export const GatedSwitch: React.FC<GatedSwitchProps> = (
rel="noopener noreferrer"
target="_blank"
>
Upgrade to {getTeamPlanBadgeLabel(props.requiredPlan)} plan
Upgrade to {getTeamPlanBadgeLabel(props.requiredPlan, false)}{" "}
plan
<ExternalLinkIcon className="size-4" />
</Link>
</Button>
@@ -61,6 +63,7 @@ export const GatedSwitch: React.FC<GatedSwitchProps> = (
{isUpgradeRequired && (
<TeamPlanBadge
plan={props.requiredPlan}
isLegacyPlan={props.isLegacyPlan}
postfix="+"
teamSlug={props.teamSlug}
/>
@@ -23,28 +23,42 @@ const teamPlanToBadgeVariant: Record<
starter: "warning",
};
export function getTeamPlanBadgeLabel(plan: Team["billingPlan"]) {
export function getTeamPlanBadgeLabel(
plan: Team["billingPlan"],
isLegacyPlan: boolean,
) {
if (plan === "growth_legacy") {
return "Growth - Legacy";
}
if (isLegacyPlan) {
return `${plan} - Legacy`;
}
return plan;
}
export function TeamPlanBadge(props: {
teamSlug: string;
plan: Team["billingPlan"];
isLegacyPlan: boolean;
className?: string;
postfix?: string;
}) {
const router = useDashboardRouter();
function handleNavigateToBilling(e: React.MouseEvent | React.KeyboardEvent) {
e.stopPropagation();
e.preventDefault();
if (props.isLegacyPlan) {
router.push(`/team/${props.teamSlug}/~/billing`);
return;
}
if (props.plan !== "free") {
return;
}
e.stopPropagation();
e.preventDefault();
router.push(`/team/${props.teamSlug}/~/billing?showPlans=true`);
}
@@ -57,11 +71,13 @@ export function TeamPlanBadge(props: {
handleNavigateToBilling(e);
}
}}
role={props.plan === "free" ? "button" : undefined}
tabIndex={props.plan === "free" ? 0 : undefined}
variant={teamPlanToBadgeVariant[props.plan]}
role={props.plan === "free" || props.isLegacyPlan ? "button" : undefined}
tabIndex={props.plan === "free" || props.isLegacyPlan ? 0 : undefined}
variant={
props.isLegacyPlan ? "warning" : teamPlanToBadgeVariant[props.plan]
}
>
{`${getTeamPlanBadgeLabel(props.plan)}${props.postfix || ""}`}
{`${getTeamPlanBadgeLabel(props.plan, props.isLegacyPlan)}${props.postfix || ""}`}
</Badge>
);
}
@@ -40,7 +40,7 @@ type ThirdwebAreaChartProps<TConfig extends ChartConfig> = {
yAxis?: boolean;
xAxis?: {
sameDay?: boolean;
showHour?: boolean;
};
variant?: "stacked" | "individual";
@@ -90,9 +90,7 @@ export function ThirdwebAreaChart<TConfig extends ChartConfig>(
{props.isPending ? (
<LoadingChartState />
) : props.data.length === 0 ? (
<EmptyChartState type="area">
{props.emptyChartState}
</EmptyChartState>
<EmptyChartState content={props.emptyChartState} />
) : (
<AreaChart
accessibilityLayer
@@ -112,7 +110,7 @@ export function ThirdwebAreaChart<TConfig extends ChartConfig>(
tickFormatter={(value) =>
format(
new Date(value),
props.xAxis?.sameDay ? "MMM dd, HH:mm" : "MMM dd",
props.xAxis?.showHour ? "MMM dd, HH:mm" : "MMM dd",
)
}
tickLine={false}
@@ -1,12 +1,11 @@
import type { Meta, StoryObj } from "@storybook/nextjs";
import type { InAppWalletAuth } from "thirdweb/wallets";
import { BadgeContainer } from "@/storybook/utils";
import type { InAppWalletStats } from "@/types/analytics";
import { InAppWalletUsersChartCardUI } from "./InAppWalletUsersChartCard";
import { AutoMergeBarChart, type StatData } from "./automerge-barchart";
const meta = {
component: Component,
title: "Charts/InAppWallets",
title: "Charts/AutoMergeBarChart",
} satisfies Meta<typeof Component>;
export default meta;
@@ -22,57 +21,106 @@ function Component() {
"This is an example of a description about user wallet usage chart";
return (
<div className="container flex max-w-6xl flex-col gap-10 py-10">
<BadgeContainer label="30 days">
<InAppWalletUsersChartCardUI
<BadgeContainer label="10 days, view more variant">
<AutoMergeBarChart
viewMoreLink={"#"}
description={description}
inAppWalletStats={createInAppWalletStatsStub(30)}
stats={createStatsStub(10)}
isPending={false}
title={title}
maxLabelsToShow={5}
emptyChartState={undefined}
exportButton={undefined}
/>
</BadgeContainer>
<BadgeContainer label="30 days">
<AutoMergeBarChart
viewMoreLink={undefined}
description={description}
stats={createStatsStub(30)}
isPending={false}
title={title}
maxLabelsToShow={5}
emptyChartState={undefined}
exportButton={{
fileName: "in-app-wallets",
}}
/>
</BadgeContainer>
<BadgeContainer label="60 days">
<InAppWalletUsersChartCardUI
<AutoMergeBarChart
viewMoreLink={undefined}
description={description}
inAppWalletStats={createInAppWalletStatsStub(60)}
stats={createStatsStub(60)}
isPending={false}
title={title}
maxLabelsToShow={5}
emptyChartState={undefined}
exportButton={{
fileName: "in-app-wallets",
}}
/>
</BadgeContainer>
<BadgeContainer label="120 days">
<InAppWalletUsersChartCardUI
<AutoMergeBarChart
viewMoreLink={undefined}
description={description}
inAppWalletStats={createInAppWalletStatsStub(120)}
stats={createStatsStub(120)}
isPending={false}
title={title}
maxLabelsToShow={5}
emptyChartState={undefined}
exportButton={{
fileName: "in-app-wallets",
}}
/>
</BadgeContainer>
<BadgeContainer label="10 days">
<InAppWalletUsersChartCardUI
<AutoMergeBarChart
viewMoreLink={undefined}
description={description}
inAppWalletStats={createInAppWalletStatsStub(10)}
stats={createStatsStub(10)}
isPending={false}
title={title}
maxLabelsToShow={5}
emptyChartState={undefined}
exportButton={{
fileName: "in-app-wallets",
}}
/>
</BadgeContainer>
<BadgeContainer label="Empty List">
<InAppWalletUsersChartCardUI
<AutoMergeBarChart
viewMoreLink={undefined}
description={description}
inAppWalletStats={createInAppWalletStatsStub(0)}
stats={createStatsStub(0)}
isPending={false}
title={title}
maxLabelsToShow={5}
emptyChartState={undefined}
exportButton={{
fileName: "in-app-wallets",
}}
/>
</BadgeContainer>
<BadgeContainer label="Pending">
<InAppWalletUsersChartCardUI
<AutoMergeBarChart
viewMoreLink={undefined}
description={description}
inAppWalletStats={[]}
stats={[]}
isPending={true}
title={title}
maxLabelsToShow={5}
emptyChartState={undefined}
exportButton={{
fileName: "in-app-wallets",
}}
/>
</BadgeContainer>
</div>
@@ -111,17 +159,16 @@ const pickRandomAuthMethod = () => {
return capitalized;
};
function createInAppWalletStatsStub(days: number): InAppWalletStats[] {
const stubbedData: InAppWalletStats[] = [];
function createStatsStub(days: number): StatData[] {
const stubbedData: StatData[] = [];
let d = days;
while (d !== 0) {
const uniqueWallets = Math.floor(Math.random() * 100);
stubbedData.push({
authenticationMethod: pickRandomAuthMethod(),
label: pickRandomAuthMethod(),
date: new Date(2024, 11, d).toLocaleString(),
uniqueWalletsConnected: uniqueWallets,
newUsers: Math.floor(Math.random() * 100) + 1,
count: uniqueWallets,
});
if (Math.random() > 0.7) {
@@ -0,0 +1,173 @@
"use client";
import { format } from "date-fns";
import { useMemo } from "react";
import { ThirdwebBarChart } from "@/components/blocks/charts/bar-chart";
import { ExportToCSVButton } from "@/components/blocks/ExportToCSVButton";
import type { ChartConfig } from "@/components/ui/chart";
import { TotalValueChartHeader } from "./chart-header";
export type StatData = {
date: string;
count: number;
label: string;
};
type ChartData = Record<string, number> & {
time: string; // human readable date
};
const defaultLabel = "Unknown";
// merges the multiple stats in single bar if they share the same date
// shows the top `maxLabelsToShow` labels, merge the rest into "Others"
export function AutoMergeBarChart(props: {
stats: StatData[];
isPending: boolean;
title: string;
emptyChartState: React.ReactElement | undefined;
description: string | undefined;
maxLabelsToShow: number;
exportButton:
| {
fileName: string;
}
| undefined;
viewMoreLink: string | undefined;
}) {
const { stats, maxLabelsToShow } = props;
const { chartConfig, chartData } = useMemo(() => {
const _chartConfig: ChartConfig = {};
const _chartDataMap: Map<string, ChartData> = new Map();
const labelToCountMap: Map<string, number> = new Map();
// for each stat, add it in _chartDataMap
for (const stat of stats) {
const chartData = _chartDataMap.get(stat.date);
// if no data for current day - create new entry
if (!chartData && stat.count > 0) {
_chartDataMap.set(stat.date, {
time: stat.date,
[stat.label || defaultLabel]: stat.count,
} as ChartData);
} else if (chartData) {
chartData[stat.label || defaultLabel] =
(chartData[stat.label || defaultLabel] || 0) + stat.count;
}
labelToCountMap.set(
stat.label || defaultLabel,
stat.count + (labelToCountMap.get(stat.label || defaultLabel) || 0),
);
}
const labelsSorted = Array.from(labelToCountMap.entries())
.sort((a, b) => b[1] - a[1])
.map((w) => w[0]);
const labelsToShow = labelsSorted.slice(0, maxLabelsToShow);
const labelsToShowAsOther = labelsSorted.slice(maxLabelsToShow);
// replace chainIdsToTagAsOther chainId with "other"
for (const data of _chartDataMap.values()) {
for (const dataKey in data) {
if (labelsToShowAsOther.includes(dataKey)) {
data.others = (data.others || 0) + (data[dataKey] || 0);
delete data[dataKey];
}
}
}
labelsToShow.forEach((walletType, i) => {
_chartConfig[walletType] = {
color: `hsl(var(--chart-${(i % 10) + 1}))`,
label: labelsToShow[i],
};
});
// Add Other
if (labelsToShow.length >= maxLabelsToShow) {
labelsToShow.push("others");
_chartConfig.others = {
color: "hsl(var(--muted-foreground))",
label: "Others",
};
}
return {
chartConfig: _chartConfig,
chartData: Array.from(_chartDataMap.values()).sort(
(a, b) => new Date(a.time).getTime() - new Date(b.time).getTime(),
),
};
}, [stats, maxLabelsToShow]);
const uniqueLabels = Object.keys(chartConfig);
const total = useMemo(() => {
return props.stats.reduce((acc, stat) => acc + stat.count, 0);
}, [props.stats]);
return (
<ThirdwebBarChart
chartClassName="h-[275px] w-full"
config={chartConfig}
customHeader={
props.viewMoreLink ? (
<TotalValueChartHeader
total={total}
title={props.title}
isPending={props.isPending}
viewMoreLink={props.viewMoreLink}
/>
) : (
<div className="relative px-6 pt-6">
<div className="space-y-1">
<h3 className="font-semibold text-lg tracking-tight leading-none">
{props.title}
</h3>
{props.description && (
<p className="text-muted-foreground text-sm">
{props.description}
</p>
)}
</div>
{props.exportButton && props.stats.length > 0 && (
<ExportToCSVButton
className="top-6 right-6 mb-4 w-full bg-background md:absolute md:mb-0 md:flex md:w-auto"
fileName={props.exportButton.fileName}
getData={async () => {
// Shows the number of each type of wallet connected on all dates
const header = ["Time", ...uniqueLabels];
const rows = chartData.map((data) => {
const { time, ...rest } = data;
return [
new Date(time).toISOString(),
...uniqueLabels.map((w) => (rest[w] || 0).toString()),
];
});
return { header, rows };
}}
/>
)}
</div>
)
}
data={chartData}
emptyChartState={props.emptyChartState}
hideLabel={false}
isPending={props.isPending}
showLegend
toolTipLabelFormatter={(_v, item) => {
if (Array.isArray(item)) {
const time = item[0].payload.time as number;
return format(new Date(time), "MMM d, yyyy");
}
return undefined;
}}
variant="stacked"
/>
);
}
@@ -44,6 +44,10 @@ type ThirdwebBarChartProps<TConfig extends ChartConfig> = {
toolTipValueFormatter?: (value: unknown) => React.ReactNode;
hideLabel?: boolean;
emptyChartState?: React.ReactElement;
className?: string;
xAxis?: {
showHour?: boolean;
};
};
export function ThirdwebBarChart<TConfig extends ChartConfig>(
@@ -55,7 +59,7 @@ export function ThirdwebBarChart<TConfig extends ChartConfig>(
props.variant || configKeys.length > 4 ? "stacked" : "grouped";
return (
<Card>
<Card className={cn("overflow-hidden", props.className)}>
{props.header && (
<CardHeader>
<CardTitle className={cn("mb-2", props.header.titleClassName)}>
@@ -74,16 +78,19 @@ export function ThirdwebBarChart<TConfig extends ChartConfig>(
{props.isPending ? (
<LoadingChartState />
) : props.data.length === 0 ? (
<EmptyChartState type="bar">
{props.emptyChartState}
</EmptyChartState>
<EmptyChartState content={props.emptyChartState} />
) : (
<BarChart accessibilityLayer data={props.data}>
<CartesianGrid vertical={false} />
<XAxis
axisLine={false}
dataKey="time"
tickFormatter={(value) => format(new Date(value), "MMM d")}
tickFormatter={(value) =>
format(
new Date(value),
props.xAxis?.showHour ? "MMM dd, HH:mm" : "MMM dd",
)
}
tickLine={false}
tickMargin={10}
/>
@@ -0,0 +1,60 @@
import { ArrowUpRightIcon } from "lucide-react";
import Link from "next/link";
import { Button } from "@/components/ui/button";
import { SkeletonContainer } from "@/components/ui/skeleton";
export function TotalValueChartHeader(props: {
total: number;
title: string;
isUsd?: boolean;
isPending: boolean;
viewMoreLink: string | undefined;
}) {
return (
<div className="space-y-1 p-6 pb-0 flex justify-between items-start gap-3">
<div>
<SkeletonContainer
loadedData={!props.isPending ? props.total : undefined}
skeletonData={100}
render={(value) => {
return (
<p className="text-3xl font-semibold tracking-tight">
{props.isUsd
? compactUSDFormatter.format(value)
: compactNumberFormatter.format(value)}
</p>
);
}}
/>
<h3 className="text-muted-foreground">{props.title}</h3>
</div>
{props.viewMoreLink && (
<Button
asChild
size="sm"
variant="outline"
className="gap-2 rounded-full text-muted-foreground hover:text-foreground"
>
<Link href={props.viewMoreLink}>
<span>View More</span>
<ArrowUpRightIcon className="size-4" />
</Link>
</Button>
)}
</div>
);
}
const compactNumberFormatter = new Intl.NumberFormat("en-US", {
notation: "compact",
maximumFractionDigits: 2,
});
const compactUSDFormatter = new Intl.NumberFormat("en-US", {
notation: "compact",
maximumFractionDigits: 2,
style: "currency",
currency: "USD",
});
@@ -22,6 +22,7 @@ interface UpsellWrapperProps {
isLocked?: boolean;
requiredPlan: Team["billingPlan"];
currentPlan?: Team["billingPlan"];
isLegacyPlan?: boolean;
featureName: string;
featureDescription: string;
benefits?: {
@@ -41,6 +42,7 @@ export function UpsellWrapper({
featureDescription,
benefits = [],
className,
isLegacyPlan = false,
}: UpsellWrapperProps) {
if (!isLocked) {
return <>{children}</>;
@@ -66,6 +68,7 @@ export function UpsellWrapper({
featureDescription={featureDescription}
featureName={featureName}
requiredPlan={requiredPlan}
isLegacyPlan={isLegacyPlan}
teamSlug={teamSlug}
/>
</div>
@@ -79,6 +82,7 @@ export function UpsellContent(props: {
featureDescription: string;
requiredPlan: Team["billingPlan"];
currentPlan: Team["billingPlan"];
isLegacyPlan: boolean;
benefits?: {
description: string;
status: "available" | "soon";
@@ -96,6 +100,7 @@ export function UpsellContent(props: {
plan={props.requiredPlan}
postfix=" Feature"
teamSlug={props.teamSlug}
isLegacyPlan={props.isLegacyPlan}
/>
<div className="space-y-1">
<CardTitle className="font-bold text-2xl text-foreground md:text-3xl">
@@ -62,6 +62,8 @@ function Story() {
client={storybookThirdwebClient}
contracts={[]}
pageSize={10}
projectId="project-id"
teamId="team-id"
projectSlug={projectSlug}
removeContractFromProject={removeContractStub}
teamSlug={teamSlug}
@@ -74,6 +76,8 @@ function Story() {
client={storybookThirdwebClient}
contracts={[popularPolygonNFTs[0] as ProjectContract]}
pageSize={10}
projectId="project-id"
teamId="team-id"
projectSlug={projectSlug}
removeContractFromProject={removeContractStub}
teamSlug={teamSlug}
@@ -86,6 +90,8 @@ function Story() {
client={storybookThirdwebClient}
contracts={popularPolygonNFTs}
pageSize={10}
projectId="project-id"
teamId="team-id"
projectSlug={projectSlug}
removeContractFromProject={removeContractStub}
teamSlug={teamSlug}
@@ -95,6 +101,8 @@ function Story() {
<BadgeContainer label="10 Contracts, 2 chains">
<ContractTableUI
projectId="project-id"
teamId="team-id"
client={storybookThirdwebClient}
contracts={[...popularPolygonNFTs, ...EthereumPopularNFTs]}
pageSize={10}
@@ -114,6 +122,8 @@ function Story() {
...basePopularTokens,
]}
pageSize={10}
projectId="project-id"
teamId="team-id"
projectSlug={projectSlug}
removeContractFromProject={removeContractStub}
teamSlug={teamSlug}
@@ -32,6 +32,8 @@ import { useAllChainsData } from "@/hooks/chains/allChains";
import { removeContractFromProject } from "@/hooks/project-contracts";
import { ChainIconClient } from "@/icons/ChainIcon";
import { cn } from "@/lib/utils";
import { revalidateCacheTagAction } from "../../../actions/revalidate";
import { projectContractsCacheTag } from "../../../api/project/cache-tag";
import {
ContractNameCell,
ContractTypeCell,
@@ -57,6 +59,8 @@ export function ContractTable(props: {
client={props.client}
contracts={props.contracts}
pageSize={props.pageSize}
projectId={props.projectId}
teamId={props.teamId}
projectSlug={props.projectSlug}
removeContractFromProject={async (contractId) => {
await removeContractFromProject({
@@ -77,6 +81,8 @@ export function ContractTableUI(props: {
removeContractFromProject: (contractId: string) => Promise<void>;
client: ThirdwebClient;
variant: "asset" | "contract";
teamId: string;
projectId: string;
teamSlug: string;
projectSlug: string;
}) {
@@ -243,6 +249,13 @@ export function ContractTableUI(props: {
<ContractActionsCell
contractId={contract.id}
onContractRemoved={() => {
revalidateCacheTagAction(
projectContractsCacheTag({
teamId: props.teamId,
projectId: props.projectId,
}),
);
setDeletedContractIds((v) => {
return [...v, contract.id];
});
@@ -69,7 +69,7 @@ export default function Component() {
// Get your contract
var contract = await ThirdwebManager.Instance.GetContract(
address: "{{contract_address}}",
address: "{{contract_address}}",
chainId: {{chainId}},
);
@@ -124,7 +124,7 @@ export default function Component() {
// Get your contract
var contract = await ThirdwebManager.Instance.GetContract(
address: "{{contract_address}}",
address: "{{contract_address}}",
chainId: {{chainId}}
);
@@ -214,7 +214,7 @@ function App() {
// Get your contract
var contract = await ThirdwebManager.Instance.GetContract(
address: "{{contract_address}}",
address: "{{contract_address}}",
chainId: {{chainId}}
);`,
dotnet: `using Thirdweb;
@@ -272,7 +272,7 @@ export default function Component() {
// Get your contract
var contract = await ThirdwebManager.Instance.GetContract(
address: "{{contract_address}}",
address: "{{contract_address}}",
chainId: {{chainId}}
);
@@ -1103,7 +1103,7 @@ function AccountFactorySection(props: {
<AlertDescription>
The recommended way to use account factories is to integrate the{" "}
<UnderlineLink
href="https://portal.thirdweb.com/transactions/sponsor"
href="https://portal.thirdweb.com/wallets/sponsor-gas"
rel="noopener noreferrer"
target="_blank"
>
@@ -178,7 +178,7 @@ export async function ContractCard({
{auditLink && !isBeta && (
<>
<Link
className="relative z-1 flex items-center gap-1 font-medium text-sm text-success-text hover:underline"
className="relative z-10 flex items-center gap-1 font-medium text-sm text-success-text hover:underline"
href={auditLink}
rel="noopener noreferrer"
target="_blank"
@@ -1,11 +1,23 @@
"use client";
import { zodResolver } from "@hookform/resolvers/zod";
import { ArrowDownToLineIcon, ExternalLinkIcon } from "lucide-react";
import { UnderlineLink } from "@workspace/ui/components/UnderlineLink";
import {
ArrowDownToLineIcon,
CircleAlertIcon,
ExternalLinkIcon,
} from "lucide-react";
import Link from "next/link";
import { useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { getAddress, isAddress, type ThirdwebClient } from "thirdweb";
import {
getAddress,
getContract,
isAddress,
type ThirdwebClient,
} from "thirdweb";
import { defineChain } from "thirdweb/chains";
import { useActiveWalletChain } from "thirdweb/react";
import { z } from "zod";
import { SingleNetworkSelector } from "@/components/blocks/NetworkSelectors";
@@ -31,6 +43,11 @@ import { Spinner } from "@/components/ui/Spinner";
import { useChainSlug } from "@/hooks/chains/chainSlug";
import { useAddContractToProject } from "@/hooks/project-contracts";
import { useDashboardRouter } from "@/lib/DashboardRouter";
import { revalidateCacheTagAction } from "../../../actions/revalidate";
import { projectContractsCacheTag } from "../../../api/project/cache-tag";
import { resolveFunctionSelectors } from "../../../lib/selectors";
import { supportedERCs } from "../../../utils/supportedERCs";
import { Alert, AlertDescription, AlertTitle } from "../../ui/alert";
type ImportModalProps = {
isOpen: boolean;
@@ -42,6 +59,7 @@ type ImportModalProps = {
client: ThirdwebClient;
type: "contract" | "asset";
onSuccess?: () => void;
allowedContractType: "token" | "non-token";
};
export const ImportModal: React.FC<ImportModalProps> = (props) => {
@@ -75,6 +93,7 @@ export const ImportModal: React.FC<ImportModalProps> = (props) => {
teamId={props.teamId}
teamSlug={props.teamSlug}
type={props.type}
allowedContractType={props.allowedContractType}
/>
</DialogContent>
</Dialog>
@@ -105,6 +124,7 @@ function ImportForm(props: {
client: ThirdwebClient;
type: "contract" | "asset";
onSuccess?: () => void;
allowedContractType: "token" | "non-token";
}) {
const router = useDashboardRouter();
const activeChainId = useActiveWalletChain()?.id;
@@ -119,9 +139,14 @@ function ImportForm(props: {
const chainSlug = useChainSlug(form.watch("chainId"));
const addContractToProject = useAddContractToProject();
const [notAllowedError, setNotAllowedError] = useState(false);
return (
<Form {...form}>
<form
onChange={() => {
setNotAllowedError(false);
}}
onSubmit={form.handleSubmit(async (data) => {
const { chainId } = data;
let contractAddress: string;
@@ -158,6 +183,27 @@ function ImportForm(props: {
return;
}
// check if the contract matches the requirement
const contract = getContract({
address: contractAddress,
// eslint-disable-next-line no-restricted-syntax
chain: defineChain(chainId),
client: props.client,
});
const functionSelectors = await resolveFunctionSelectors(contract);
const ercs = supportedERCs(functionSelectors);
const isToken = ercs.isERC20 || ercs.isERC721 || ercs.isERC1155;
if (
(props.allowedContractType === "token" && !isToken) ||
(props.allowedContractType === "non-token" && isToken)
) {
setNotAllowedError(true);
return;
}
addContractToProject.mutate(
{
chainId: chainId.toString(),
@@ -177,6 +223,12 @@ function ImportForm(props: {
}
},
onSuccess: () => {
revalidateCacheTagAction(
projectContractsCacheTag({
teamId: props.teamId,
projectId: props.projectId,
}),
);
router.refresh();
toast.success("Contract imported successfully");
props.onSuccess?.();
@@ -217,6 +269,34 @@ function ImportForm(props: {
</div>
</div>
{notAllowedError && (
<div className="px-6 pt-6 text-sm text-destructive-text">
<Alert variant="destructive">
<CircleAlertIcon className="size-5" />
<AlertTitle> Invalid Contract </AlertTitle>
<AlertDescription>
{props.allowedContractType === "token" && (
<span>
This contract is not a token contract. <br /> Only ERC20,
ERC721 and ERC1155 contracts can be imported as tokens.
</span>
)}
{props.allowedContractType === "non-token" && (
<span>
This contract is a token contract. <br /> Go to the{" "}
<UnderlineLink
href={`/team/${props.teamSlug}/${props.projectSlug}/tokens`}
>
Tokens
</UnderlineLink>{" "}
page to import in dashboard.
</span>
)}
</AlertDescription>
</Alert>
</div>
)}
<div className="mt-8 flex justify-end border-t bg-card p-6">
{addContractToProject.isSuccess &&
addContractToProject.data?.result ? (
@@ -64,7 +64,7 @@ const truncateIdentifier = (value: string) => {
return `${value.slice(0, 8)}...${value.slice(-4)}`;
};
export function InAppWalletUsersPageContent(
export function UserWalletsTable(
props: {
authToken: string;
client: ThirdwebClient;
@@ -288,83 +288,81 @@ export function InAppWalletUsersPageContent(
]);
return (
<div>
<div className="overflow-hidden rounded-xl border border-border bg-card">
<div className="flex flex-col lg:flex-row lg:justify-between p-4 lg:px-6 py-5 lg:items-center gap-5">
<div>
<div className="flex mb-3">
<div className="p-2 rounded-full bg-background border border-border">
<UserIcon className="size-5 text-muted-foreground" />
</div>
</div>
<h2 className="font-semibold text-2xl tracking-tight">
User Wallets
</h2>
<p className="text-muted-foreground text-sm">
View and manage your project's users
</p>
</div>
<div className="flex flex-col items-start lg:items-end gap-2.5 border-t lg:border-t-0 pt-5 lg:pt-0 border-dashed">
<div className="w-full lg:w-auto lg:min-w-[320px]">
<AdvancedSearchInput
onSearch={handleSearch}
onClear={handleClearSearch}
isLoading={isSearching}
hasResults={hasSearchResults}
/>
</div>
<Button
className="gap-2 bg-background rounded-full"
disabled={wallets.length === 0 || isPending}
onClick={downloadCSV}
variant="outline"
>
{isPending && <Spinner className="size-4" />}
Download as .csv
</Button>
</div>
</div>
<div className="overflow-hidden rounded-xl border border-border bg-card">
<div className="flex flex-col lg:flex-row lg:justify-between p-4 lg:px-6 py-5 lg:items-center gap-5">
<div>
{hasSearchResults ? (
<SearchResults results={searchResults} client={props.client} />
) : (
<>
<TWTable
columns={columns}
data={wallets}
isFetched={walletsQuery.isFetched}
isPending={walletsQuery.isPending}
tableContainerClassName="rounded-none border-x-0 border-b-0"
title="User wallets"
/>
<div className="flex justify-center gap-3 border-t bg-card p-6">
<Button
className="gap-2 bg-background"
disabled={activePage === 1 || walletsQuery.isPending}
onClick={() => setActivePage((p) => Math.max(1, p - 1))}
size="sm"
variant="outline"
>
<ArrowLeftIcon className="size-4" />
Previous
</Button>
<Button
className="gap-2 bg-background"
disabled={wallets.length === 0 || walletsQuery.isPending}
onClick={() => setActivePage((p) => p + 1)}
size="sm"
variant="outline"
>
Next
<ArrowRightIcon className="size-4" />
</Button>
</div>
</>
)}
<div className="flex mb-3">
<div className="p-2 rounded-full bg-background border border-border">
<UserIcon className="size-5 text-muted-foreground" />
</div>
</div>
<h2 className="font-semibold text-2xl tracking-tight">
User Wallets
</h2>
<p className="text-muted-foreground text-sm">
View and manage your project's users
</p>
</div>
<div className="flex flex-col items-start lg:items-end gap-2.5 border-t lg:border-t-0 pt-5 lg:pt-0 border-dashed">
<div className="w-full lg:w-auto lg:min-w-[320px]">
<AdvancedSearchInput
onSearch={handleSearch}
onClear={handleClearSearch}
isLoading={isSearching}
hasResults={hasSearchResults}
/>
</div>
<Button
className="gap-2 bg-background rounded-full"
disabled={wallets.length === 0 || isPending}
onClick={downloadCSV}
variant="outline"
>
{isPending && <Spinner className="size-4" />}
Download as .csv
</Button>
</div>
</div>
<div>
{hasSearchResults ? (
<SearchResults results={searchResults} client={props.client} />
) : (
<>
<TWTable
columns={columns}
data={wallets}
isFetched={walletsQuery.isFetched}
isPending={walletsQuery.isPending}
tableContainerClassName="rounded-none border-x-0 border-b-0"
title="User wallets"
/>
<div className="flex justify-center gap-3 border-t bg-card p-6">
<Button
className="gap-2 bg-background"
disabled={activePage === 1 || walletsQuery.isPending}
onClick={() => setActivePage((p) => Math.max(1, p - 1))}
size="sm"
variant="outline"
>
<ArrowLeftIcon className="size-4" />
Previous
</Button>
<Button
className="gap-2 bg-background"
disabled={wallets.length === 0 || walletsQuery.isPending}
onClick={() => setActivePage((p) => p + 1)}
size="sm"
variant="outline"
>
Next
<ArrowRightIcon className="size-4" />
</Button>
</div>
</>
)}
</div>
</div>
);
@@ -41,3 +41,6 @@ export const NEXT_PUBLIC_CHAIN_PAGE_CLIENT_ID =
export const NEXT_PUBLIC_ASSET_PAGE_CLIENT_ID =
process.env.NEXT_PUBLIC_ASSET_PAGE_CLIENT_ID;
export const NEXT_PUBLIC_BRIDGE_IFRAME_CLIENT_ID =
process.env.NEXT_PUBLIC_BRIDGE_IFRAME_CLIENT_ID;
@@ -13,6 +13,7 @@ import {
import {
THIRDWEB_BRIDGE_URL,
THIRDWEB_BUNDLER_DOMAIN,
THIRDWEB_ENGINE_CLOUD_URL,
THIRDWEB_INAPP_WALLET_DOMAIN,
THIRDWEB_INSIGHT_API_DOMAIN,
THIRDWEB_PAY_DOMAIN,
@@ -38,6 +39,7 @@ export function getConfiguredThirdwebClient(options: {
rpc: THIRDWEB_RPC_DOMAIN,
social: THIRDWEB_SOCIAL_API_DOMAIN,
storage: THIRDWEB_STORAGE_DOMAIN,
engineCloud: new URL(THIRDWEB_ENGINE_CLOUD_URL).hostname,
});
}
+3
View File
@@ -25,3 +25,6 @@ export const THIRDWEB_INSIGHT_API_DOMAIN =
export const THIRDWEB_BRIDGE_URL =
process.env.NEXT_PUBLIC_BRIDGE_URL || "bridge.thirdweb-dev.com";
export const THIRDWEB_ENGINE_CLOUD_URL =
process.env.NEXT_PUBLIC_ENGINE_CLOUD_URL || "https://engine.thirdweb-dev.com";
+1
View File
@@ -30,6 +30,7 @@ export function teamStub(id: string, billingPlan: Team["billingPlan"]): Team {
billingPlan: billingPlan,
billingStatus: "validPayment",
canCreatePublicChains: null,
isLegacyPlan: false,
capabilities: {
bundler: {
enabled: true,
+10 -27
View File
@@ -7,6 +7,10 @@ export interface WalletStats {
walletType: string;
}
export type WalletStatsWithName = WalletStats & {
walletName: string;
};
export interface InAppWalletStats {
date: string;
authenticationMethod: string;
@@ -23,6 +27,8 @@ export interface UserOpStats {
successful: number;
failed: number;
sponsoredUsd: number;
gasUnits: number;
avgGasPrice: number;
chainId?: string;
}
@@ -33,13 +39,6 @@ export interface TransactionStats {
count: number;
}
export interface EngineCloudStats {
date: string;
chainId: string;
pathname: string;
totalRequests: number;
}
export interface UniversalBridgeStats {
date: string;
chainId: number;
@@ -102,36 +101,20 @@ export interface X402SettlementsOverall {
totalValueUSD: number;
}
export interface X402SettlementsByChainId {
date: string;
export interface X402SettlementsByChainId extends X402SettlementsOverall {
chainId: string;
totalRequests: number;
totalValue: number;
totalValueUSD: number;
}
export interface X402SettlementsByPayer {
date: string;
export interface X402SettlementsByPayer extends X402SettlementsOverall {
payer: string;
totalRequests: number;
totalValue: number;
totalValueUSD: number;
}
interface X402SettlementsByReceiver {
date: string;
interface X402SettlementsByReceiver extends X402SettlementsOverall {
receiver: string;
totalRequests: number;
totalValue: number;
totalValueUSD: number;
}
export interface X402SettlementsByResource {
date: string;
export interface X402SettlementsByResource extends X402SettlementsOverall {
resource: string;
totalRequests: number;
totalValue: number;
totalValueUSD: number;
}
interface X402SettlementsByAsset {
+7
View File
@@ -13,9 +13,16 @@ export type ProductSKU =
| "usage:in_app_wallet"
| "usage:aa_sponsorship"
| "usage:aa_sponsorship_op_grant"
// dedicated relayer SKUs
| DedicatedRelayerSKU
| null;
export type ChainInfraSKU =
| "chain:infra:rpc"
| "chain:infra:insight"
| "chain:infra:account_abstraction";
export type DedicatedRelayerSKU =
| "product:dedicated_relayer_standard"
| "product:dedicated_relayer_premium"
| "product:dedicated_relayer_enterprise";
@@ -1,5 +1,6 @@
const compactNumberFormatter = new Intl.NumberFormat("en-US", {
notation: "compact",
maximumFractionDigits: 2,
});
export const formatTickerNumber = (value: number) => {
+5 -5
View File
@@ -1,10 +1,10 @@
const usdCurrencyFormatter = new Intl.NumberFormat("en-US", {
currency: "USD",
maximumFractionDigits: 6, // prefix with $
minimumFractionDigits: 0, // don't show decimal places if value is a whole number
notation: "compact", // at max 2 decimal places
roundingMode: "halfEven", // round to nearest even number, standard practice for financial calculations
style: "currency", // shows 1.2M instead of 1,200,000, 1.2B instead of 1,200,000,000
maximumFractionDigits: 2,
minimumFractionDigits: 0,
notation: "compact",
roundingMode: "halfEven",
style: "currency",
});
export const toUSD = (value: number) => {
@@ -3,6 +3,7 @@ import { zodResolver } from "@hookform/resolvers/zod";
import { Turnstile } from "@marsidev/react-turnstile";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import type { CanClaimResponseType } from "app/(app)/api/testnet-faucet/can-claim/CanClaimResponseType";
import { ArrowUpRightIcon } from "lucide-react";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { useForm } from "react-hook-form";
@@ -192,6 +193,20 @@ export function FaucetButton({
// Can not claim
if (canClaimFaucetQuery.data && canClaimFaucetQuery.data.canClaim === false) {
if (canClaimFaucetQuery.data.type === "paid-plan-required") {
return (
<Button
asChild
className="w-full gap-2 whitespace-pre-wrap h-auto min-h-10 text-center bg-background"
variant="outline"
>
<Link href="/team/~/~/billing">
Faucet is not available on Free plan. Upgrade your plan to continue
<ArrowUpRightIcon className="size-4 shrink-0 text-muted-foreground" />
</Link>
</Button>
);
}
return (
<Button className="!opacity-100 w-full " disabled variant="outline">
{canClaimFaucetQuery.data.type === "throttle" && (
@@ -203,10 +218,6 @@ export function FaucetButton({
{canClaimFaucetQuery.data.type === "unsupported-chain" &&
"Faucet is empty right now"}
{/* TODO: add an upsell path here to subscribe to one of these plans */}
{canClaimFaucetQuery.data.type === "paid-plan-required" &&
"Faucet is only available on Starter, Growth, Scale and Pro plans."}
</Button>
);
}
@@ -2,6 +2,7 @@ import type { ThirdwebContract } from "thirdweb";
import { contractType as getContractType } from "thirdweb/extensions/thirdweb";
import { getDeployedEntrypointERC20 } from "thirdweb/tokens";
import { resolveFunctionSelectors } from "@/lib/selectors";
import { supportedERCs } from "@/utils/supportedERCs";
import { getValidReward } from "../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/rewards/utils/rewards";
import {
isERC20ClaimConditionsSupported,
@@ -21,7 +22,6 @@ import {
isPermissionsEnumerableSupported,
isPermissionsSupported,
} from "./detectedFeatures/permissions";
import { supportedERCs } from "./detectedFeatures/supportedERCs";
import { type EmbedTypeToShow, getEmbedTypeToShow } from "./getEmbedTypeToShow";
type ContractPageMetadata = {
@@ -1,9 +1,9 @@
import { supportedERCs } from "@/utils/supportedERCs";
import { isClaimConditionsSupported } from "./detectedFeatures/claimConditions";
import {
isDirectListingSupported,
isEnglishAuctionSupported,
} from "./detectedFeatures/marketplace";
import { supportedERCs } from "./detectedFeatures/supportedERCs";
export type EmbedTypeToShow =
| "marketplace-v3"
@@ -2,7 +2,7 @@ import type { ThirdwebContract } from "thirdweb";
import { isGetNFTsSupported as ERC721_isGetNFTsSupported } from "thirdweb/extensions/erc721";
import { isGetNFTsSupported as ERC1155_isGetNFTsSupported } from "thirdweb/extensions/erc1155";
import { resolveFunctionSelectors } from "@/lib/selectors";
import { supportedERCs } from "./detectedFeatures/supportedERCs";
import { supportedERCs } from "@/utils/supportedERCs";
export type NewPublicPageType = "erc20" | "erc1155" | "erc721";
@@ -22,7 +22,7 @@ export function AccountsPage(props: {
View list of smart accounts that have been created for this
contract.{" "}
<UnderlineLink
href="https://portal.thirdweb.com/transactions/sponsor"
href="https://portal.thirdweb.com/wallets/sponsor-gas"
target="_blank"
rel="noopener noreferrer"
>
@@ -7,6 +7,7 @@ import { getTeams } from "@/api/team/get-team";
import type { MinimalTeamsAndProjects } from "@/components/contracts/import-contract/types";
import { getClientThirdwebClient } from "@/constants/thirdweb-client.client";
import { resolveFunctionSelectors } from "@/lib/selectors";
import { supportedERCs } from "@/utils/supportedERCs";
import { shortenIfAddress } from "@/utils/usedapp-external";
import type { ProjectMeta } from "../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/types";
import { TeamHeader } from "../../../../team/components/TeamHeader/team-header";
@@ -14,7 +15,6 @@ import { ConfigureCustomChain } from "./_layout/ConfigureCustomChain";
import { getContractMetadataHeaderData } from "./_layout/contract-metadata";
import { ContractPageLayout } from "./_layout/contract-page-layout";
import { ContractPageLayoutClient } from "./_layout/contract-page-layout.client";
import { supportedERCs } from "./_utils/detectedFeatures/supportedERCs";
import { getContractPageParamsInfo } from "./_utils/getContractFromParams";
import { getContractPageMetadata } from "./_utils/getContractPageMetadata";
import { getContractPageSidebarLinks } from "./_utils/getContractPageSidebarLinks";
@@ -1,5 +1,6 @@
import { BotIcon, CoinsIcon, FuelIcon, HardDriveIcon } from "lucide-react";
import { BridgeIcon } from "@/icons/BridgeIcon";
import { InsightIcon } from "@/icons/InsightIcon";
import { PayIcon } from "@/icons/PayIcon";
import { RPCIcon } from "@/icons/RPCIcon";
import { WalletProductIcon } from "@/icons/WalletProductIcon";
@@ -54,6 +55,12 @@ export const services = [
link: "https://thirdweb.com/x402",
name: "x402",
},
{
icon: InsightIcon,
id: "insight",
link: "https://thirdweb.com/gateway",
name: "Indexer",
},
] satisfies Array<{
name: string;
id: ChainSupportedService;
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 MiB

@@ -1,130 +0,0 @@
import type { Metadata } from "next";
import Link from "next/link";
import { notFound } from "next/navigation";
import { Suspense } from "react";
import {
ContractCard,
ContractCardSkeleton,
} from "@/components/contracts/contract-card/contract-card";
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator,
} from "@/components/ui/breadcrumb";
import { DeployUpsellCard } from "../components/upsells/deploy-your-own";
import { getCategory } from "../data";
type ExploreCategoryPageProps = {
params: Promise<{
category: string;
}>;
};
export async function generateMetadata(
props: ExploreCategoryPageProps,
): Promise<Metadata> {
const params = await props.params;
const category = getCategory(params.category);
if (!category) {
notFound();
}
return {
description: `${category.description} Deploy with one click to Ethereum, Polygon, Optimism, and other EVM blockchains with thirdweb.`,
title: `${category.name} Smart Contracts | Explore`,
};
}
export default async function ExploreCategoryPage(
props: ExploreCategoryPageProps,
) {
const params = await props.params;
const category = getCategory(params.category);
if (!category) {
notFound();
}
return (
<div className="flex flex-col">
<Breadcrumb className="border-border border-b px-6 py-4">
<BreadcrumbList>
<BreadcrumbItem>
<BreadcrumbLink asChild>
<Link href="/explore">Explore</Link>
</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbPage>
{category.displayName || category.name}
</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
<div className="container flex flex-col gap-4 py-8">
<h1 className="mb-3 font-bold text-3xl tracking-tighter lg:text-5xl">
{category.displayName || category.name}
</h1>
<p className="max-w-screen-md text-base text-muted-foreground lg:text-lg">
{category.description}{" "}
{category.learnMore && (
<Link href={category.learnMore}>Learn more</Link>
)}
</p>
<div className="h-10" />
<div className="grid grid-cols-1 gap-5 md:grid-cols-3">
{category.contracts.map((publishedContractId) => {
const publisher: string | undefined = Array.isArray(
publishedContractId,
)
? publishedContractId[0].split("/")[0]
: publishedContractId.split("/")[0];
const contractId: string | undefined = Array.isArray(
publishedContractId,
)
? publishedContractId[0].split("/")[1]
: publishedContractId.split("/")[1];
const modules = Array.isArray(publishedContractId)
? publishedContractId[1]
: undefined;
const overrides = Array.isArray(publishedContractId)
? publishedContractId[2]
: undefined;
if (!publisher || !contractId) {
return null;
}
return (
<Suspense
fallback={<ContractCardSkeleton />}
key={publisher + contractId + overrides?.title}
>
<ContractCard
contractId={contractId}
descriptionOverride={overrides?.description}
isBeta={category.isBeta}
modules={
modules?.length
? modules.map((m) => ({
moduleId: m.split("/")[1] || "",
publisher: m.split("/")[0] || "",
}))
: undefined
}
publisher={publisher}
titleOverride={overrides?.title}
/>
</Suspense>
);
})}
</div>
<div className="h-16" />
<DeployUpsellCard />
</div>
</div>
);
}
@@ -1,4 +1,3 @@
import { ArrowRightIcon } from "lucide-react";
import Link from "next/link";
import { Suspense } from "react";
import {
@@ -7,48 +6,45 @@ import {
} from "@/components/contracts/contract-card/contract-card";
import type { ExploreCategory } from "../../data";
export function ContractRow({ category }: { category: ExploreCategory }) {
export function ContractRow({
category,
prefixCard,
}: {
category: ExploreCategory;
prefixCard?: React.ReactNode;
}) {
return (
<section>
{/* Title, Description + View all link */}
<div className="flex items-center justify-between gap-4">
<header className="flex flex-col gap-1.5">
<Link href={`/explore/${category.id}`}>
<h2 className="font-semibold text-2xl tracking-tight">
{category.displayName || category.name}
</h2>
</Link>
<header className="flex flex-col gap-1">
<h2 className="font-semibold text-2xl tracking-tight">
{category.displayName || category.name}
</h2>
<p className="text-muted-foreground">
{category.description}{" "}
{category.learnMore && (
<Link
className="inline text-link-foreground"
href={category.learnMore}
rel="noopener noreferrer"
target="_blank"
>
Learn more
</Link>
)}
</p>
{(category.description || category.learnMore) && (
<p className="text-muted-foreground">
{category.description}{" "}
{category.learnMore && (
<Link
className="inline text-link-foreground"
href={category.learnMore}
rel="noopener noreferrer"
target="_blank"
>
Learn more
</Link>
)}
</p>
)}
</header>
{category.contracts.length > 6 && (
<Link
className="flex shrink-0 items-center gap-1 text-base text-link-foreground hover:text-foreground"
href={`/explore/${category.id}`}
>
View all
<ArrowRightIcon className="size-4" />
</Link>
)}
</div>
<div className="h-5" />
<div className="relative z-0 grid grid-cols-1 gap-4 md:grid-cols-3">
{category.contracts.slice(0, 6).map((publishedContractId) => {
{prefixCard}
{category.contracts.map((publishedContractId) => {
const publisher: string | undefined = Array.isArray(
publishedContractId,
)
@@ -0,0 +1,103 @@
"use client";
import { RocketIcon, ShieldCheckIcon } from "lucide-react";
import Link from "next/link";
import { reportTokenUpsellClicked } from "@/analytics/report";
import { Img } from "@/components/blocks/Img";
import { Button } from "@/components/ui/button";
const thirdwebEns = {
name: "thirdweb.eth",
image: "https://euc.li/thirdweb.eth",
};
function TokenContractCard(props: {
name: string;
description: string;
link: string;
trackingType: "nft" | "coin";
}) {
return (
<article className="relative flex min-h-[220px] flex-col rounded-lg border border-border bg-card p-4 hover:border-active-border">
<div className="flex items-center gap-1.5">
<div className="flex items-center gap-1 font-medium text-sm text-success-text">
<ShieldCheckIcon className="size-4" />
Audited
</div>
</div>
<div className="h-3.5" />
{/* Title */}
<h3 className="font-semibold text-lg tracking-tight">
<Link
className="cursor-pointer before:absolute before:inset-0 before:z-0"
href={props.link}
onClick={() => {
reportTokenUpsellClicked({
assetType: props.trackingType,
pageType: "explore",
});
}}
>
{props.name}
</Link>
</h3>
<p className="mt-1 text-muted-foreground text-sm leading-5">
{props.description}
</p>
<div className="mt-auto relative z-1 flex justify-between gap-2 pt-3">
<Link
className="flex shrink-0 items-center gap-1.5 hover:underline"
href={`/${thirdwebEns.name}`}
target="_blank"
>
<Img
className="size-5 rounded-full object-cover"
src={thirdwebEns.image}
/>
<span className="text-xs"> {thirdwebEns.name}</span>
</Link>
<div className="flex items-center justify-between">
<Button
asChild
className="relative z-10 h-auto gap-1.5 px-2.5 py-1.5 text-xs hover:bg-inverted hover:text-inverted-foreground"
size="sm"
variant="outline"
>
<Link href={props.link}>
<RocketIcon className="size-3" />
Deploy
</Link>
</Button>
</div>
</div>
</article>
);
}
export function NFTCollectionCard() {
return (
<TokenContractCard
name="NFT Collection"
description="Launch an ERC-721 or ERC-1155 NFT collection"
link="/team/~/~project/tokens/create/nft"
trackingType="nft"
/>
);
}
export function CoinCard() {
return (
<TokenContractCard
name="Coin"
description="Launch an ERC-20 token to create a cryptocurrency"
link="/team/~/~project/tokens/create/token"
trackingType="coin"
/>
);
}
@@ -5,31 +5,12 @@ import { PlainTextCodeBlock } from "@/components/ui/code/plaintext-code";
export const DeployUpsellCard: React.FC = () => {
return (
<section className="rounded-xl border border-border px-4 py-6 shadow-lg md:px-8 md:py-14">
<h3 className="font-bold text-2xl tracking-tight">
<h3 className="font-semibold text-2xl tracking-tight">
Didn't find what you're looking for? Build your own!
</h3>
<div className="h-8" />
<p className="text-muted-foreground ">
Create a contract with a single command.{" "}
<Link
className="inline-flex items-center gap-1.5 text-link-foreground"
href="https://portal.thirdweb.com/contracts/build/overview"
rel="noopener noreferrer"
target="_blank"
>
Learn more about the Solidity SDK
<ExternalLinkIcon className="size-4" />
</Link>
</p>
<div className="h-2" />
<PlainTextCodeBlock code="npx thirdweb create contract" />
<div className="h-8" />
<p className="text-muted-foreground ">
Deploy a contract with a single command.{" "}
<Link
@@ -1,70 +0,0 @@
import Image from "next/image";
import Link from "next/link";
import { Button } from "@/components/ui/button";
import exploreFeatureImage from "../../../../../../../public/assets/landingpage/explore-featured.png";
import heroIcon1 from "../../../../../../../public/assets/product-pages/publish/hero-icon-1.png";
import heroIcon2 from "../../../../../../../public/assets/product-pages/publish/hero-icon-2.png";
export const PublishUpsellCard: React.FC = () => {
return (
<div className="flex gap-10 rounded-xl border border-border bg-card p-8 shadow-lg md:p-10">
<div className="flex flex-col gap-6">
<h2 className="font-bold text-3xl tracking-tighter">
Accelerate your protocol's growth
</h2>
<p className="text-muted-foreground">
Publishing your contract is the best way to get your contracts in
front of our 70k+ community of web3 developers.
</p>
<div className="flex gap-2">
<Image alt="" className="hidden size-6 md:block" src={heroIcon1} />
<p className="text-muted-foreground">
<span className="font-semibold text-foreground">
Save development time.{" "}
</span>
Focus on protocol development and save time by not having to build
middleware layer yourself.
</p>
</div>
<div className="flex gap-2">
<Image alt="" className="hidden size-6 md:block" src={heroIcon2} />
<p className="text-muted-foreground">
<span className="font-semibold text-foreground">
Shareable landing page.{" "}
</span>
By publishing your contract, your contracts become easily shareable
with a landing page for your contract.
</p>
</div>
<div className="mt-auto flex gap-2 pt-4">
<Button asChild>
<Link href="/contact-us" rel="noopener noreferrer" target="_blank">
Get In Touch
</Link>
</Button>
<Button asChild variant="outline">
<Link
href="https://portal.thirdweb.com/contracts/publish/overview"
rel="noopener noreferrer"
target="_blank"
>
Learn More
</Link>
</Button>
</div>
</div>
<Image
alt=""
className="hidden w-[40%] grayscale invert md:block dark:filter-none"
draggable={false}
src={exploreFeatureImage}
/>
</div>
);
};
@@ -1,151 +1,55 @@
type PublishedContractID = `${string}/${string}`;
export interface ExploreCategory {
id: string;
name: string;
displayName?: string;
description: string;
description: string | undefined;
learnMore?: string;
contracts:
| Array<PublishedContractID>
| Array<
[
PublishedContractID,
Array<PublishedContractID>,
{ title: string; description: string },
]
>;
showInExplore?: boolean;
contracts: Array<
| PublishedContractID
| [
PublishedContractID,
Array<PublishedContractID>,
{ title: string; description: string },
]
>;
isBeta?: boolean;
}
const POPULAR = {
export const NFTS = {
contracts: [
"thirdweb.eth/DropERC721",
"thirdweb.eth/MarketplaceV3",
"thirdweb.eth/AccountFactory",
"0xaF3202F6bAEbA50d37e0d4B0b870455EDF198D7c/BurnMintERC677",
"thirdweb.eth/DropERC1155",
"thirdweb.eth/TokenERC20",
"thirdweb.eth/NFTStake",
"unlock-protocol.eth/PublicLock",
],
description: "A collection of our most deployed contracts.",
id: "popular",
name: "Popular",
} satisfies ExploreCategory;
const NFTS = {
contracts: [
"thirdweb.eth/DropERC721",
"thirdweb.eth/OpenEditionERC721",
"thirdweb.eth/TokenERC721",
"thirdweb.eth/DropERC1155",
"thirdweb.eth/TokenERC1155",
"flairsdk.eth/ERC721CommunityStream",
"thirdweb.eth/LoyaltyCard",
"doubledev.eth/ERC4907",
"thirdweb.eth/Multiwrap",
"kronickatz.eth/ERC721NESDrop",
],
description:
"NFT Collections, Editions, Drops and everything else NFT-related.",
displayName: "NFTs",
id: "nft",
name: "NFT",
} satisfies ExploreCategory;
const GOVERNANCE = {
contracts: [
"thirdweb.eth/VoteERC20",
"thirdweb.eth/TokenERC20",
"thirdweb.eth/Split",
],
export const GOVERNANCE = {
contracts: ["thirdweb.eth/VoteERC20", "thirdweb.eth/Split"],
description: "Create your own DAO, vote on proposals, and manage a treasury.",
id: "daos-governance",
name: "DAOs & Governance",
} satisfies ExploreCategory;
const DROPS = {
contracts: [
"thirdweb.eth/DropERC721",
"thirdweb.eth/DropERC1155",
"thirdweb.eth/DropERC20",
],
description: "Release NFTs and Tokens based on preset Claim Conditions.",
displayName: "Drops",
id: "drops",
name: "Drop",
} satisfies ExploreCategory;
const MARKETS = {
contracts: [
"thirdweb.eth/MarketplaceV3",
"thirdweb.eth/TokenERC20",
"thirdweb.eth/Split",
],
export const MARKETS = {
contracts: ["thirdweb.eth/MarketplaceV3", "thirdweb.eth/Split"],
description: "Quickly spin up your own on-chain marketplace for NFTs.",
displayName: "Marketplaces",
id: "marketplace",
name: "Marketplace",
} satisfies ExploreCategory;
const AIRDROP = {
export const AIRDROP = {
contracts: ["thirdweb.eth/Airdrop"],
description:
"Efficiently transfer large numbers of on-chain assets to a large number of recipients.",
displayName: "Airdrops",
id: "airdrop",
name: "Airdrop",
} satisfies ExploreCategory;
const GAMING = {
contracts: [
"thirdweb.eth/MarketplaceV3",
"thirdweb.eth/DropERC721",
"thirdweb.eth/TokenERC20",
"thirdweb.eth/TokenERC1155",
"thirdweb.eth/Multiwrap",
"thirdweb.eth/NFTStake",
],
description:
"A collection of contracts that are popular for building play-to-earn and free-to-own web3 games.",
displayName: "Gaming",
id: "gaming",
name: "Gaming",
showInExplore: false,
} satisfies ExploreCategory;
const LOYALTY = {
contracts: [
"thirdweb.eth/LoyaltyCard",
"thirdweb.eth/MarketplaceV3",
"thirdweb.eth/TokenERC20",
],
description:
"A collection of contracts that are popular for building loyalty programs.",
displayName: "Loyalty",
id: "loyalty",
name: "Loyalty",
showInExplore: false,
} satisfies ExploreCategory;
const COMMERCE = {
contracts: [
"thirdweb.eth/TokenERC721",
"thirdweb.eth/TokenERC1155",
"thirdweb.eth/TokenERC20",
"thirdweb.eth/Split",
],
description:
"Most popular contracts for building web3 commerce apps. Reward loyal customers and sell NFTs through your storefront.",
displayName: "Commerce",
id: "commerce",
name: "Commerce",
showInExplore: false,
} satisfies ExploreCategory;
const STAKING = {
export const STAKING = {
contracts: [
"thirdweb.eth/NFTStake",
"thirdweb.eth/EditionStake",
@@ -153,12 +57,10 @@ const STAKING = {
],
description: "Stake your NFTs or tokens to earn ERC20 tokens in return.",
displayName: "Staking",
id: "staking",
name: "Staking",
showInExplore: true,
} satisfies ExploreCategory;
const SMART_WALLET = {
export const SMART_WALLET = {
contracts: [
"thirdweb.eth/AccountFactory",
"thirdweb.eth/ManagedAccountFactory",
@@ -168,30 +70,16 @@ const SMART_WALLET = {
description:
"Account factories that let you spin up Account Abstraction (ERC-4337) wallets for your users. Not sure which factory is right for you?",
displayName: "Account Abstraction",
id: "smart-wallet",
learnMore:
"https://portal.thirdweb.com/wallets/smart-wallet/get-started#1-deploy-a-smart-wallet-factory-contract",
name: "Account Abstraction",
showInExplore: true,
} satisfies ExploreCategory;
const STYLUS = {
export const STYLUS = {
contracts: [
"0x6453a486d52e0EB6E79Ec4491038E2522a926936/StylusAirdropERC1155",
"0x6453a486d52e0EB6E79Ec4491038E2522a926936/StylusAirdropERC721",
"0x6453a486d52e0EB6E79Ec4491038E2522a926936/StylusAirdropERC20",
],
description:
"Airdrop your NFTs or tokens to a large number of recipients. Built with Arbitrum Stylus.",
displayName: "Airdrop - Arbitrum Stylus",
id: "stylus",
name: "Stylus",
showInExplore: true,
isBeta: true,
} satisfies ExploreCategory;
const MODULAR_CONTRACTS_STYLUS = {
contracts: [
// erc721 token
[
"thirdweb.eth/ERC721CoreInitializable",
@@ -232,44 +120,8 @@ const MODULAR_CONTRACTS_STYLUS = {
},
],
],
description:
"Collection of highly customizable and upgradeable smart contracts built with the modular contracts framework.",
displayName: "Modular Contracts - Arbitrum Stylus",
id: "modular-contracts",
name: "modular",
showInExplore: true,
description: undefined,
displayName: "Arbitrum Stylus",
name: "Stylus",
isBeta: true,
} satisfies ExploreCategory;
const CATEGORIES: Record<string, ExploreCategory> = {
[POPULAR.id]: POPULAR,
[NFTS.id]: NFTS,
[MARKETS.id]: MARKETS,
[STYLUS.id]: STYLUS,
[DROPS.id]: DROPS,
[SMART_WALLET.id]: SMART_WALLET,
[AIRDROP.id]: AIRDROP,
[GAMING.id]: GAMING,
[LOYALTY.id]: LOYALTY,
[COMMERCE.id]: COMMERCE,
[STAKING.id]: STAKING,
[GOVERNANCE.id]: GOVERNANCE,
[MODULAR_CONTRACTS_STYLUS.id]: MODULAR_CONTRACTS_STYLUS,
};
export function getCategory(id: string) {
if (isExploreCategory(id)) {
return CATEGORIES[id];
}
return null;
}
type ExploreCategoryName = keyof typeof CATEGORIES;
function isExploreCategory(category: string): category is ExploreCategoryName {
return category in CATEGORIES;
}
export const EXPLORE_PAGE_DATA = Object.values(CATEGORIES).filter((v) =>
"showInExplore" in v ? v.showInExplore !== false : true,
);
@@ -1,14 +1,20 @@
import type { Metadata } from "next";
import { Fragment } from "react";
import { ContractRow } from "./components/contract-row";
import { CoinCard, NFTCollectionCard } from "./components/token-cards";
import { DeployUpsellCard } from "./components/upsells/deploy-your-own";
import { PublishUpsellCard } from "./components/upsells/publish-submit";
import { EXPLORE_PAGE_DATA } from "./data";
import { TokensSection } from "./tokens-section";
import {
AIRDROP,
GOVERNANCE,
MARKETS,
NFTS,
SMART_WALLET,
STAKING,
STYLUS,
} from "./data";
const title = "List of smart contracts for EVM Developers";
const description =
"A list of Ethereum smart contract templates for web3 developers, including the most popular evm smart contracts for dapps, NFTs and more.";
"A list of smart contract templates for web3 developers, including the most popular evm smart contracts for dapps, NFTs and more.";
export const metadata: Metadata = {
description,
@@ -22,9 +28,9 @@ export const metadata: Metadata = {
export default async function ExplorePage() {
return (
<div className="flex flex-col">
<div className="border-b py-10">
<div className="border-b py-12">
<div className="container max-w-7xl">
<h1 className="mb-2 font-bold text-4xl lg:text-5xl tracking-tight">
<h1 className="mb-2 font-semibold text-4xl lg:text-5xl tracking-tight">
Explore
</h1>
<p className="max-w-screen-sm text-sm lg:text-base text-muted-foreground">
@@ -35,24 +41,34 @@ export default async function ExplorePage() {
</div>
</div>
<div className="container max-w-7xl pt-8 pb-4">
<TokensSection />
</div>
<div className="container max-w-7xl flex flex-col gap-8 py-10">
<div className="container max-w-7xl flex flex-col py-10">
<div className="flex flex-col gap-14">
{EXPLORE_PAGE_DATA.map((category, idx) => (
<Fragment key={category.id}>
{Math.floor(EXPLORE_PAGE_DATA.length / 2) === idx && (
<PublishUpsellCard />
)}
<ContractRow category={category} />
</Fragment>
))}
<PopularSection />
<ContractRow category={MARKETS} />
<ContractRow category={NFTS} prefixCard={<NFTCollectionCard />} />
<ContractRow category={SMART_WALLET} />
<ContractRow category={AIRDROP} />
<ContractRow category={STAKING} />
<ContractRow category={GOVERNANCE} />
<ContractRow category={STYLUS} />
<DeployUpsellCard />
</div>
<DeployUpsellCard />
</div>
</div>
);
}
function PopularSection() {
return (
<section>
<h2 className="font-semibold text-2xl tracking-tight mb-1">Popular</h2>
<p className="text-muted-foreground mb-4">
A collection of our most deployed contracts.
</p>
<div className="relative z-0 grid grid-cols-1 gap-4 md:grid-cols-3">
<CoinCard />
<NFTCollectionCard />
</div>
</section>
);
}
@@ -1,111 +0,0 @@
"use client";
import { ZapIcon } from "lucide-react";
import Link from "next/link";
import { reportTokenUpsellClicked } from "@/analytics/report";
import { GridPattern } from "@/components/ui/background-patterns";
import { Badge } from "@/components/ui/badge";
import { cn } from "@/lib/utils";
export function TokensSection() {
return (
<div className="bg-card p-4 lg:py-8 lg:px-6 border rounded-xl relative w-full overflow-hidden">
<GridPattern
width={30}
height={30}
strokeDasharray={"4 2"}
className="text-border dark:text-border/70 hidden lg:block translate-x-5"
style={{
maskImage:
"linear-gradient(to bottom left,white,transparent,transparent)",
}}
/>
<Badge
variant="outline"
className="text-sm bg-background h-auto px-3 py-1 gap-2 mb-4"
>
<div className="rounded-full bg-primary size-2" />
New
</Badge>
<h2 className="font-semibold text-2xl tracking-tight mb-1">
Launch Your Tokens Effortlessly
</h2>
<p className="text-muted-foreground mb-6 text-sm lg:text-base">
Deploy contract and configure all settings you need to launch your token
in one seamless flow
</p>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 max-w-4xl">
<CardLink
assetType="coin"
title="Launch Coin"
description="Launch an ERC-20 token to create a cryptocurrency"
href="/team/~/~project/tokens/create/token"
bullets={[
"Deploy Contract",
"Configure Price and Supply",
"Airdrop Tokens",
]}
/>
<CardLink
assetType="nft"
title="Launch NFT Collection"
description="Launch an ERC-721 or ERC-1155 NFT collection"
href="/team/~/~project/tokens/create/nft"
bullets={[
"Deploy Contract",
"Upload NFTs",
"Configure Price and Supply",
]}
/>
</div>
</div>
);
}
function CardLink(props: {
title: string;
description: string;
href: string;
assetType: "nft" | "coin";
bullets: string[];
}) {
return (
<div
className={cn(
"relative flex flex-col rounded-lg border p-4 cursor-pointer hover:border-active-border bg-background",
)}
>
<div className="mb-3 flex">
<div className="flex items-center justify-center rounded-full border p-2 bg-card">
<ZapIcon className="size-4 text-muted-foreground" />
</div>
</div>
<h3 className="mb-0.5 font-semibold text-lg tracking-tight">
<Link
className="before:absolute before:inset-0"
href={props.href}
onClick={() => {
reportTokenUpsellClicked({
assetType: props.assetType,
pageType: "explore",
});
}}
>
{props.title}
</Link>
</h3>
<p className="text-muted-foreground text-sm">{props.description}</p>
<ul className="mt-4 space-y-1 text-sm list-disc list-inside text-muted-foreground">
{props.bullets.map((bullet) => (
<li key={bullet}>{bullet}</li>
))}
</ul>
</div>
);
}
@@ -3,7 +3,7 @@ import type { Metadata } from "next";
import { unstable_cache } from "next/cache";
import { Bridge } from "thirdweb";
import { serverThirdwebClient } from "@/constants/thirdweb-client.server";
import { bridgeStats } from "../../../bridge/data";
import { bridgeStats } from "../../../bridge/(general)/data";
import { PageHeader } from "./components/header";
import { TokenPage } from "./components/token-page";
@@ -17,15 +17,21 @@ export default async function CheckoutPage(props: {
searchParams: Promise<{
amount?: string;
invoice_id?: string;
project_id?: string;
chain_id?: string | string[];
}>;
}) {
const params = await props.params;
const [params, searchParams] = await Promise.all([
props.params,
props.searchParams,
]);
console.log("params", params);
console.log("searchParams", searchParams);
switch (params.sku) {
case "topup": {
const amountUSD = Number.parseInt(
(await props.searchParams).amount || "10",
);
const amountUSD = Number.parseInt(searchParams.amount || "10");
if (Number.isNaN(amountUSD)) {
return <StripeRedirectErrorPage errorMessage="Invalid amount" />;
}
@@ -43,7 +49,7 @@ export default async function CheckoutPage(props: {
break;
}
case "invoice": {
const invoiceId = (await props.searchParams).invoice_id;
const invoiceId = searchParams.invoice_id;
if (!invoiceId) {
return <StripeRedirectErrorPage errorMessage="Invalid invoice ID" />;
}
@@ -59,10 +65,12 @@ export default async function CheckoutPage(props: {
redirect(invoice);
break;
}
default: {
const response = await getBillingCheckoutUrl({
sku: decodeURIComponent(params.sku) as Exclude<ProductSKU, null>,
teamSlug: params.team_slug,
params: searchParams,
});
if (response.status === "error") {
@@ -7,6 +7,10 @@ import { getAbsoluteUrl } from "@/utils/vercel";
export async function getBillingCheckoutUrl(options: {
teamSlug: string;
sku: Exclude<ProductSKU, null>;
params: {
project_id?: string;
chain_id?: string | string[];
};
}) {
const token = await getAuthToken();
@@ -17,13 +21,26 @@ export async function getBillingCheckoutUrl(options: {
} as const;
}
const chainIds = options.params.chain_id
? (Array.isArray(options.params.chain_id)
? options.params.chain_id
: [options.params.chain_id]
)
.map((value) => Number(value))
.filter((value) => Number.isFinite(value))
: undefined;
const body = {
redirectTo: getAbsoluteStripeRedirectUrl(),
sku: options.sku,
projectId: options.params.project_id,
chainIds: chainIds,
};
const res = await fetch(
`${NEXT_PUBLIC_THIRDWEB_API_HOST}/v1/teams/${options.teamSlug}/checkout/create-link`,
{
body: JSON.stringify({
redirectTo: getAbsoluteStripeRedirectUrl(),
sku: options.sku,
}),
body: JSON.stringify(body),
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
@@ -18,7 +18,6 @@ import {
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { useDashboardRouter } from "@/lib/DashboardRouter";
import { getValidTeamPlan } from "@/utils/getValidTeamPlan";
import { SearchInput } from "../components/SearchInput";
export function AccountTeamsUI(props: {
@@ -117,8 +116,6 @@ function TeamRow(props: {
role: TeamAccountRole;
client: ThirdwebClient;
}) {
const plan = getValidTeamPlan(props.team);
return (
<div className="flex items-center justify-between gap-2">
{/* start */}
@@ -133,7 +130,11 @@ function TeamRow(props: {
<div>
<div className="flex items-center gap-3">
<p className="font-semibold text-sm">{props.team.name}</p>
<TeamPlanBadge plan={plan} teamSlug={props.team.slug} />
<TeamPlanBadge
plan={props.team.billingPlan}
teamSlug={props.team.slug}
isLegacyPlan={props.team.isLegacyPlan}
/>
</div>
<p className="text-muted-foreground text-sm capitalize">
{props.role.toLowerCase()}
@@ -312,7 +312,7 @@ function CreateProjectForm(props: {
type="submit"
>
{createProject.isPending && <Spinner className="size-4" />}
Create Projecct
Create Project
</Button>
</footer>
</form>
@@ -1,7 +1,6 @@
"use client";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { usePathname } from "next/navigation";
import { ThemeProvider } from "next-themes";
import { NuqsAdapter } from "nuqs/adapters/next/app";
import { useEffect, useMemo } from "react";
@@ -25,9 +24,6 @@ export function AppRouterProviders(props: {
children: React.ReactNode;
autoConnect: boolean;
}) {
// get the current pathname
const pathname = usePathname();
return (
<NuqsAdapter>
<QueryClientProvider client={queryClient}>
@@ -40,9 +36,6 @@ export function AppRouterProviders(props: {
defaultTheme="dark"
disableTransitionOnChange
enableSystem={false}
forcedTheme={
pathname.startsWith("/get-started") ? "dark" : undefined
}
>
<Toaster richColors />
<SanctionedAddressesChecker>
@@ -6,13 +6,12 @@ import {
} from "responsive-rsc";
import { defineChain } from "thirdweb";
import { type ChainMetadata, getChainMetadata } from "thirdweb/chains";
import { getWalletInfo, type WalletId } from "thirdweb/wallets";
import {
getClientTransactions,
getEOAWalletConnections,
getInAppWalletUsage,
getUniversalBridgeUsage,
getUserOpUsage,
getWalletConnections,
} from "@/api/analytics";
import { getAuthToken } from "@/api/auth-token";
import { getTeamBySlug } from "@/api/team/get-team";
@@ -26,7 +25,7 @@ import { getFiltersFromSearchParams } from "@/lib/time";
import type {
InAppWalletStats,
UserOpStats,
WalletStats,
WalletStatsWithName,
} from "@/types/analytics";
import { loginRedirect } from "@/utils/redirects";
import { PieChartCard } from "../../../../components/Analytics/PieChartCard";
@@ -240,7 +239,7 @@ async function AsyncWalletDistributionCard(props: {
range: Range;
authToken: string;
}) {
const walletConnections = await getWalletConnections(
const walletConnections = await getEOAWalletConnections(
{
from: props.range.from,
period: "all",
@@ -373,29 +372,19 @@ async function AsyncTotalSponsoredCard(props: {
) : (
<EmptyStateCard
link="https://portal.thirdweb.com/typescript/v5/account-abstraction/get-started"
metric="Account Abstraction"
metric="Gas Sponsorship"
/>
);
}
async function WalletDistributionCard({ data }: { data: WalletStats[] }) {
const formattedData = await Promise.all(
data.map(async (w) => {
const wallet = await getWalletInfo(w.walletType as WalletId).catch(
() => ({ name: w.walletType }),
);
return {
totalConnections: w.totalConnections,
uniqueWalletsConnected: w.uniqueWalletsConnected,
walletName: wallet.name,
walletType: w.walletType,
};
}),
);
async function WalletDistributionCard({
data,
}: {
data: WalletStatsWithName[];
}) {
return (
<PieChartCard
data={formattedData.map(({ walletName, uniqueWalletsConnected }) => {
data={data.map(({ walletName, uniqueWalletsConnected }) => {
return {
label: walletName,
value: uniqueWalletsConnected,
@@ -73,21 +73,24 @@ export function PlanInfoCardUI(props: {
<h3 className="font-semibold text-2xl capitalize tracking-tight">
{validPlan === "growth_legacy" ? "Growth" : validPlan} Plan
</h3>
{validPlan.includes("legacy") && (
{props.team.isLegacyPlan && (
<Badge variant="warning">Legacy</Badge>
)}
{trialEndsInFuture && <Badge variant="default">Trial</Badge>}
</div>
{validPlan.includes("legacy") && (
{props.team.isLegacyPlan && (
<p className="text-sm text-yellow-600">
You are on the legacy plan. You may save by upgrading to new
plan.{" "}
Legacy plans will be upgraded to new plans automatically on
January 1st, 2026.
<br />
<UnderlineLink
className="decoration-yellow-600/50"
href="/pricing"
href="https://blog.thirdweb.com/retiring-legacy-pricing-plans-on-january-1st-2026"
rel="noopener noreferrer"
target="_blank"
>
Learn More
Learn more about changes to legacy plans
</UnderlineLink>
</p>
)}
@@ -2,7 +2,7 @@ import { redirect } from "next/navigation";
import { getAuthToken } from "@/api/auth-token";
import { fetchEcosystem } from "@/api/team/ecosystems";
import { getTeamBySlug } from "@/api/team/get-team";
import { InAppWalletUsersPageContent } from "@/components/in-app-wallet-users-content/in-app-wallet-users-content";
import { UserWalletsTable } from "@/components/in-app-wallet-users-content/user-wallets-table";
import { getClientThirdwebClient } from "@/constants/thirdweb-client.client";
import { loginRedirect } from "@/utils/redirects";
@@ -30,7 +30,7 @@ export default async function EcosystemUsersPage(props: {
});
return (
<InAppWalletUsersPageContent
<UserWalletsTable
authToken={authToken}
client={client}
ecosystemSlug={ecosystem.slug}
@@ -63,15 +63,17 @@ function MetricPlaceholders({
{metric.label}
</span>
<div className="h-32 w-full">
<EmptyChartState type="area">
{status === "active" ? (
<Badge>Coming Soon</Badge>
) : status === "pending" ? (
<p className="text-xs">Activation in progress.</p>
) : (
<p className="text-xs">Activate service to view metrics.</p>
)}
</EmptyChartState>
<EmptyChartState
content={
status === "active" ? (
<Badge>Coming Soon</Badge>
) : status === "pending" ? (
<p className="text-xs">Activation in progress.</p>
) : (
<p className="text-xs">Activate service to view metrics.</p>
)
}
/>
</div>
</Card>
))}
@@ -28,6 +28,7 @@ export default async function Page(props: {
<div className="flex flex-col grow justify-center items-center">
<UpsellContent
currentPlan={team.billingPlan}
isLegacyPlan={team.isLegacyPlan}
featureDescription="Create support cases for your projects"
featureName="Support Center"
requiredPlan="starter"
@@ -1,20 +1,29 @@
import { redirect } from "next/navigation";
import type { SearchParams } from "nuqs/server";
import { getUserOpUsage } from "@/api/analytics";
import { getAuthToken } from "@/api/auth-token";
import { getProjects } from "@/api/project/projects";
import { getTeamBySlug } from "@/api/team/get-team";
import { getTeamSubscriptions } from "@/api/team/team-subscription";
import { SponsoredTransactionsTable } from "@/components/sponsored-transactions-table/SponsoredTransactionsTable";
import {
getLastNDaysRange,
type Range,
} from "@/components/analytics/date-range-selector";
import { getClientThirdwebClient } from "@/constants/thirdweb-client.client";
import { loginRedirect } from "@/utils/redirects";
import { AccountAbstractionSummary } from "../../../../[project_slug]/(sidebar)/account-abstraction/AccountAbstractionAnalytics/AccountAbstractionSummary";
import { AccountAbstractionAnalytics } from "../../../../[project_slug]/(sidebar)/account-abstraction/aa-analytics";
import { searchParamLoader } from "../../../../[project_slug]/(sidebar)/account-abstraction/search-params";
export default async function Page(props: {
params: Promise<{
team_slug: string;
}>;
searchParams: Promise<SearchParams>;
}) {
const params = await props.params;
const [team, authToken] = await Promise.all([
getTeamBySlug(params.team_slug),
const [params, parsedSearchParams, authToken] = await Promise.all([
props.params,
searchParamLoader(props.searchParams),
getAuthToken(),
]);
@@ -22,6 +31,8 @@ export default async function Page(props: {
loginRedirect(`/team/${params.team_slug}/~/usage/account-abstraction`);
}
const team = await getTeamBySlug(params.team_slug);
if (!team) {
redirect("/team");
}
@@ -44,20 +55,51 @@ export default async function Page(props: {
);
}
const interval = parsedSearchParams.interval ?? "week";
const rangeType = parsedSearchParams.range || "last-120";
const range: Range = {
from:
rangeType === "custom"
? parsedSearchParams.from
: getLastNDaysRange(rangeType).from,
to:
rangeType === "custom"
? parsedSearchParams.to
: getLastNDaysRange(rangeType).to,
type: rangeType,
};
const userOpStats = await getUserOpUsage(
{
from: range.from,
period: interval,
teamId: team.id,
to: range.to,
},
authToken,
);
const client = getClientThirdwebClient({
jwt: authToken,
teamId: team.id,
});
return (
<div className="flex flex-col gap-14">
<SponsoredTransactionsTable
<div className="flex flex-col gap-10">
<AccountAbstractionSummary
authToken={authToken}
teamId={team.id}
from={range.from}
to={range.to}
/>
<AccountAbstractionAnalytics
client={client}
from={usageSubscription.currentPeriodStart}
projects={projects}
teamId={team.id}
teamSlug={team.slug}
to={usageSubscription.currentPeriodEnd}
userOpStats={userOpStats}
variant="team"
/>
</div>
@@ -13,7 +13,7 @@ export default async function Layout(props: {
return (
<div className="flex grow flex-col">
<div className="border-border pt-10 pb-4">
<div className="border-border pt-10 pb-6">
<div className="container max-w-7xl flex flex-col gap-4 lg:flex-row lg:justify-between">
<h1 className="font-semibold text-3xl tracking-tight">Usage</h1>
<div className="items-center gap-3 hidden lg:flex">
@@ -47,7 +47,7 @@ export default async function Layout(props: {
path: `${usagePath}/storage`,
},
{
name: "Account Abstraction",
name: "Gas Sponsorship",
path: `${usagePath}/account-abstraction`,
},
]}
@@ -43,7 +43,8 @@ export default async function Page(props: {
<div className="grow flex flex-col justify-center items-center">
<UpsellContent
currentPlan={team.billingPlan}
featureDescription="View RPC, Wallet, Storage, Account Abstraction, Engine Cloud, Webhooks usage and more"
isLegacyPlan={team.isLegacyPlan}
featureDescription="View RPC, Wallet, Storage, Gas Sponsorship, Engine Cloud, Webhooks usage and more"
featureName="Usage"
requiredPlan="starter"
teamSlug={params.team_slug}
@@ -57,7 +57,7 @@ export function CountGraph(props: {
return format(label, "MMM dd, HH:mm");
}}
xAxis={{
sameDay: true,
showHour: true,
}}
yAxis
/>
@@ -57,7 +57,7 @@ export function RateGraph(props: {
return format(label, "MMM dd, HH:mm");
}}
xAxis={{
sameDay: true,
showHour: true,
}}
yAxis
/>
@@ -33,7 +33,6 @@ export default async function RPCUsage(props: {
redirect("/team");
}
const currentPlan = team.billingPlan;
const currentRateLimit = team.capabilities.rpc.rateLimit;
const apiData = await getLast24HoursRPCUsage({
@@ -102,7 +101,11 @@ export default async function RPCUsage(props: {
<div className="font-bold text-2xl capitalize">
{currentRateLimit.toLocaleString()} RPS
</div>
<TeamPlanBadge plan={currentPlan} teamSlug={team.slug} />
<TeamPlanBadge
plan={team.billingPlan}
teamSlug={team.slug}
isLegacyPlan={team.isLegacyPlan}
/>
</div>
</CardContent>
</Card>
@@ -0,0 +1,84 @@
import { ResponsiveSuspense } from "responsive-rsc";
import { getAiUsage } from "@/api/analytics";
import { EmptyChartStateGetStartedCTA } from "@/components/analytics/empty-chart-state";
import { AiTokenUsageChartCardUI } from "../ai/analytics/chart/AiTokenUsageChartCard";
async function AsyncAiAnalytics(props: {
from: Date;
to: Date;
interval: "day" | "week";
projectId: string;
teamId: string;
authToken: string;
teamSlug: string;
projectSlug: string;
}) {
const stats = await getAiUsage(
{
from: props.from,
period: props.interval,
projectId: props.projectId,
teamId: props.teamId,
to: props.to,
},
props.authToken,
).catch((error) => {
console.error(error);
return [];
});
return (
<AiTokenUsageChartCardUI
title="AI token volume"
isPending={false}
aiUsageStats={stats}
viewMoreLink={`/team/${props.teamSlug}/${props.projectSlug}/ai/analytics`}
emptyChartState={
<EmptyChartStateGetStartedCTA
link={{
label: "View more details",
href: `/team/${props.teamSlug}/${props.projectSlug}/ai/analytics`,
}}
title="No data available"
description="No AI usage found in selected time period"
/>
}
/>
);
}
export function AIAnalyticsChartCard(props: {
from: Date;
to: Date;
interval: "day" | "week";
projectId: string;
teamId: string;
authToken: string;
teamSlug: string;
projectSlug: string;
}) {
return (
<ResponsiveSuspense
fallback={
<AiTokenUsageChartCardUI
isPending={true}
title="AI token volume"
viewMoreLink={`/team/${props.teamSlug}/${props.projectSlug}/ai/analytics`}
aiUsageStats={[]}
/>
}
searchParamsUsed={["from", "to", "interval"]}
>
<AsyncAiAnalytics
teamSlug={props.teamSlug}
projectSlug={props.projectSlug}
from={props.from}
to={props.to}
interval={props.interval}
projectId={props.projectId}
teamId={props.teamId}
authToken={props.authToken}
/>
</ResponsiveSuspense>
);
}
@@ -0,0 +1,92 @@
import { ResponsiveSuspense } from "responsive-rsc";
import { getEOAAndInAppWalletConnections } from "@/api/analytics";
import { EmptyChartStateGetStartedCTA } from "@/components/analytics/empty-chart-state";
import {
AutoMergeBarChart,
type StatData,
} from "@/components/blocks/charts/automerge-barchart";
type AllWalletConnectionsChartProps = {
teamId: string;
projectId: string;
authToken: string;
from: Date;
to: Date;
interval: "day" | "week";
teamSlug: string;
projectSlug: string;
};
export function AllWalletConnectionsChart(
props: AllWalletConnectionsChartProps,
) {
return (
<ResponsiveSuspense
searchParamsUsed={["from", "to", "interval"]}
fallback={
<AllWalletConnectionsChartUI {...props} isPending={true} stats={[]} />
}
>
<AsyncAllWalletConnectionsChart {...props} />
</ResponsiveSuspense>
);
}
async function AsyncAllWalletConnectionsChart(
props: AllWalletConnectionsChartProps,
) {
const stats = await getEOAAndInAppWalletConnections(
{
from: props.from,
period: props.interval,
projectId: props.projectId,
teamId: props.teamId,
to: props.to,
},
props.authToken,
).catch((error) => {
console.error(error);
return [];
});
return (
<AllWalletConnectionsChartUI
{...props}
isPending={false}
stats={stats.map((stat) => ({
date: stat.date,
count: stat.uniqueWalletsConnected,
label: stat.walletName,
}))}
/>
);
}
function AllWalletConnectionsChartUI(props: {
stats: StatData[];
teamSlug: string;
projectSlug: string;
isPending: boolean;
}) {
return (
<AutoMergeBarChart
title="Wallet connections"
description={undefined}
stats={props.stats || []}
isPending={props.isPending}
exportButton={undefined}
maxLabelsToShow={5}
emptyChartState={
<EmptyChartStateGetStartedCTA
link={{
label: "View Configuration",
href: `/team/${props.teamSlug}/${props.projectSlug}/wallets/user-wallets/configure`,
}}
title="No data available"
description="No wallet connections found in selected time period"
/>
}
viewMoreLink={`/team/${props.teamSlug}/${props.projectSlug}/wallets/user-wallets`}
/>
);
}
@@ -0,0 +1,102 @@
import { ResponsiveSuspense } from "responsive-rsc";
import { getUniversalBridgeUsage } from "@/api/analytics";
import {
EmptyChartState,
EmptyChartStateGetStartedCTA,
LoadingChartState,
} from "@/components/analytics/empty-chart-state";
import { TotalValueChartHeader } from "@/components/blocks/charts/chart-header";
import type { UniversalBridgeStats } from "@/types/analytics";
import { TotalVolumePieChart } from "../payments/components/TotalVolumePieChart";
async function AsyncBridgeCard(props: {
from: Date;
to: Date;
interval: "day" | "week";
projectId: string;
teamId: string;
authToken: string;
teamSlug: string;
projectSlug: string;
}) {
const data = await getUniversalBridgeUsage(
{
from: props.from,
period: props.interval,
projectId: props.projectId,
teamId: props.teamId,
to: props.to,
},
props.authToken,
).catch((error) => {
console.error(error);
return [];
});
return (
<BridgeChartCardUI
data={data}
teamSlug={props.teamSlug}
projectSlug={props.projectSlug}
/>
);
}
export function BridgeChartCard(props: {
from: Date;
to: Date;
interval: "day" | "week";
projectId: string;
teamId: string;
authToken: string;
teamSlug: string;
projectSlug: string;
}) {
return (
<ResponsiveSuspense
fallback={<LoadingChartState className="min-h-[40px] h-full border" />}
searchParamsUsed={["from", "to", "interval"]}
>
<AsyncBridgeCard {...props} />
</ResponsiveSuspense>
);
}
function BridgeChartCardUI(props: {
data: UniversalBridgeStats[];
teamSlug: string;
projectSlug: string;
}) {
const total =
props.data.reduce((acc, curr) => acc + curr.amountUsdCents, 0) / 100;
return (
<div className="flex flex-col bg-card border rounded-lg">
<TotalValueChartHeader
title="Bridge Volume"
total={total}
isUsd={true}
isPending={false}
viewMoreLink={`/team/${props.teamSlug}/${props.projectSlug}/bridge`}
/>
<div className="p-6 grow flex items-center justify-center">
{total === 0 ? (
<EmptyChartState
content={
<EmptyChartStateGetStartedCTA
link={{
label: "View Configuration",
href: `/team/${props.teamSlug}/${props.projectSlug}/bridge/configuration`,
}}
title="No data available"
description="No bridge volume found in selected time period"
/>
}
/>
) : (
<TotalVolumePieChart data={props.data} hideTotal={true} />
)}
</div>
</div>
);
}
@@ -0,0 +1,158 @@
import type { Meta } from "@storybook/nextjs";
import { ResponsiveSearchParamsProvider } from "responsive-rsc";
import type {
InAppWalletStats,
UniversalBridgeStats,
X402SettlementsOverall,
} from "@/types/analytics";
import { ProjectHighlightsCard } from "./highlights-card-ui";
const meta = {
component: ProjectHighlightsCard,
decorators: [
(Story) => (
<ResponsiveSearchParamsProvider value={{}}>
<div className="container max-w-6xl py-10">
<Story />
</div>
</ResponsiveSearchParamsProvider>
),
],
title: "Analytics/ProjectHighlightsCard",
parameters: {
nextjs: {
appDirectory: true,
},
},
} satisfies Meta<typeof ProjectHighlightsCard>;
export default meta;
function generateDateSeries(days: number) {
const dates: string[] = [];
const today = new Date();
for (let i = days - 1; i >= 0; i--) {
const d = new Date(today);
d.setDate(today.getDate() - i);
dates.push(d.toISOString());
}
return dates;
}
function inAppWalletStub(days: number): InAppWalletStats[] {
const dates = generateDateSeries(days);
return dates.map((date) => {
return {
// main data
newUsers: randomValue(50, 100),
uniqueWalletsConnected: randomValue(500, 1200),
// others
authenticationMethod: "email",
date,
};
});
}
// a few aggregated entries that will be summed for the "Active Users" stat
function aggregatedInAppWalletsStub(): InAppWalletStats[] {
return Array.from({ length: 3 }).map(() => {
return {
// main data
newUsers: randomValue(50, 100),
uniqueWalletsConnected: randomValue(500, 1200),
// others
date: new Date().toISOString(),
authenticationMethod: "email",
};
});
}
function bridgeVolumeStub(days: number): UniversalBridgeStats[] {
const dates = generateDateSeries(days);
return dates.map((date) => {
return {
// main data
developerFeeUsdCents: randomValue(500 * 100, 2500 * 100),
status: "completed",
// others
chainId: 0,
count: 0,
date,
amountUsdCents: 0,
type: "onchain",
};
});
}
function randomValue(min: number, max: number) {
return Math.max(0, Math.round(min + Math.random() * (max - min)));
}
function generateX402Settlements(days: number): X402SettlementsOverall[] {
const dates = generateDateSeries(days);
return dates.map((date) => {
return {
date,
totalRequests: randomValue(10, 100),
totalValue: randomValue(500, 2500),
totalValueUSD: randomValue(500, 2500),
};
});
}
export function ThirtyDays() {
const days = 30;
const data = {
aggregatedUserStats: aggregatedInAppWalletsStub(),
userStats: inAppWalletStub(days),
bridgeVolumeStats: bridgeVolumeStub(days),
x402Settlements: generateX402Settlements(days),
};
return (
<ProjectHighlightsCard
aggregatedUserStats={data.aggregatedUserStats}
userStats={data.userStats}
volumeStats={data.bridgeVolumeStats}
x402Settlements={data.x402Settlements}
/>
);
}
export function SingleDayRevenue() {
const days = 1;
const data = {
aggregatedUserStats: [],
userStats: [],
bridgeVolumeStats: bridgeVolumeStub(days),
x402Settlements: generateX402Settlements(days),
};
return (
<ProjectHighlightsCard
aggregatedUserStats={data.aggregatedUserStats}
userStats={data.userStats}
volumeStats={data.bridgeVolumeStats}
x402Settlements={data.x402Settlements}
/>
);
}
export function SingleDayRevenueNoX402() {
const days = 1;
const data = {
aggregatedUserStats: [],
userStats: [],
bridgeVolumeStats: bridgeVolumeStub(days),
x402Settlements: [],
};
return (
<ProjectHighlightsCard
aggregatedUserStats={data.aggregatedUserStats}
userStats={data.userStats}
volumeStats={data.bridgeVolumeStats}
x402Settlements={data.x402Settlements}
/>
);
}
@@ -1,67 +1,66 @@
"use client";
import { EmptyStateContent } from "app/(app)/team/components/Analytics/EmptyStateCard";
import { useSetResponsiveSearchParams } from "responsive-rsc";
import type { InAppWalletStats, UniversalBridgeStats } from "@/types/analytics";
import { CombinedBarChartCard } from "../../../../components/Analytics/CombinedBarChartCard";
import { useState } from "react";
import type {
InAppWalletStats,
UniversalBridgeStats,
X402SettlementsOverall,
} from "@/types/analytics";
import {
CombinedBarChartCard,
type CombinedBarChartConfig,
} from "../../../../components/Analytics/CombinedBarChartCard";
type AggregatedMetrics = {
activeUsers: number;
newUsers: number;
totalVolume: number;
feesCollected: number;
bridgeRevenue: number;
x402Revenue: number;
};
export function ProjectHighlightsCard(props: {
selectedChart: string | undefined;
aggregatedUserStats: InAppWalletStats[];
userStats: InAppWalletStats[];
volumeStats: UniversalBridgeStats[];
teamSlug: string;
projectSlug: string;
selectedChartQueryParam: string;
x402Settlements: X402SettlementsOverall[];
}) {
const {
selectedChart,
aggregatedUserStats,
const { aggregatedUserStats, userStats, volumeStats, x402Settlements } =
props;
const [selectedChart, setSelectedChart] = useState<string | undefined>(
"activeUsers",
);
const timeSeriesData = processTimeSeriesData(
userStats,
volumeStats,
teamSlug,
projectSlug,
selectedChartQueryParam,
} = props;
x402Settlements,
);
const timeSeriesData = processTimeSeriesData(userStats, volumeStats);
const setResponsiveSearchParams = useSetResponsiveSearchParams();
const chartConfig = {
const chartConfig: CombinedBarChartConfig<keyof AggregatedMetrics> = {
activeUsers: { color: "hsl(var(--chart-1))", label: "Active Users" },
newUsers: { color: "hsl(var(--chart-3))", label: "New Users" },
totalVolume: {
color: "hsl(var(--chart-2))",
emptyContent: (
<EmptyStateContent
description="Onramp, swap, and bridge with thirdweb's Payments."
link="https://portal.thirdweb.com/payments"
metric="Payments"
/>
),
newUsers: { color: "hsl(var(--chart-2))", label: "New Users" },
bridgeRevenue: {
color: "hsl(var(--chart-3))",
isCurrency: true,
label: "Total Volume",
label: "Bridge",
hideAsTab: true,
},
x402Revenue: {
color: "hsl(var(--chart-4))",
isCurrency: true,
label: "x402",
hideAsTab: true,
},
feesCollected: {
color: "hsl(var(--chart-4))",
emptyContent: (
<EmptyStateContent
description="Your app hasn't collected any fees yet."
link={`/team/${teamSlug}/${projectSlug}/payments/settings`}
metric="Fees"
/>
),
color: "hsl(var(--chart-3))",
emptyContent: undefined,
isCurrency: true,
label: "Fee Revenue",
label: "Revenue",
stackedKeys: ["bridgeRevenue", "x402Revenue"],
},
} as const;
};
return (
<CombinedBarChartCard
@@ -83,12 +82,7 @@ export function ProjectHighlightsCard(props: {
chartConfig={chartConfig}
data={timeSeriesData}
onSelect={(key) => {
setResponsiveSearchParams((v) => {
return {
...v,
[selectedChartQueryParam]: key,
};
});
setSelectedChart(key);
}}
trendFn={(data, key) =>
data.filter((d) => (d[key] as number) > 0).length >= 2
@@ -103,6 +97,8 @@ export function ProjectHighlightsCard(props: {
type TimeSeriesMetrics = AggregatedMetrics & {
date: string;
bridgeRevenue: number;
x402Revenue: number;
};
/**
@@ -111,48 +107,51 @@ type TimeSeriesMetrics = AggregatedMetrics & {
function processTimeSeriesData(
userStats: InAppWalletStats[],
volumeStats: UniversalBridgeStats[],
x402Settlements: X402SettlementsOverall[],
): TimeSeriesMetrics[] {
const metrics: TimeSeriesMetrics[] = [];
const dates = [
...new Set([
...userStats.map((a) => new Date(a.date).toISOString().slice(0, 10)),
...volumeStats.map((a) => new Date(a.date).toISOString().slice(0, 10)),
...userStats.map((a) => ignoreTimePeriod(a.date)),
...volumeStats.map((a) => ignoreTimePeriod(a.date)),
...x402Settlements.map((a) => ignoreTimePeriod(a.date)),
]),
];
for (const date of dates) {
const activeUsers = userStats
.filter((u) => new Date(u.date).toISOString().slice(0, 10) === date)
.filter((u) => ignoreTimePeriod(u.date) === ignoreTimePeriod(date))
.reduce((acc, curr) => acc + curr.uniqueWalletsConnected, 0);
const newUsers = userStats
.filter((u) => new Date(u.date).toISOString().slice(0, 10) === date)
.filter((u) => ignoreTimePeriod(u.date) === ignoreTimePeriod(date))
.reduce((acc, curr) => acc + curr.newUsers, 0);
const volume = volumeStats
const bridgeRevenue = volumeStats
.filter(
(v) =>
new Date(v.date).toISOString().slice(0, 10) === date &&
v.status === "completed",
)
.reduce((acc, curr) => acc + curr.amountUsdCents / 100, 0);
const fees = volumeStats
.filter(
(v) =>
new Date(v.date).toISOString().slice(0, 10) === date &&
ignoreTimePeriod(v.date) === ignoreTimePeriod(date) &&
v.status === "completed",
)
.reduce((acc, curr) => acc + curr.developerFeeUsdCents / 100, 0);
const x402Revenue = x402Settlements
.filter((x) => ignoreTimePeriod(x.date) === ignoreTimePeriod(date))
.reduce((acc, curr) => acc + curr.totalValueUSD, 0);
metrics.push({
activeUsers: activeUsers,
date: date,
feesCollected: fees,
feesCollected: bridgeRevenue + x402Revenue,
bridgeRevenue: bridgeRevenue,
x402Revenue: x402Revenue,
newUsers: newUsers,
totalVolume: volume,
});
}
return metrics;
}
function ignoreTimePeriod(date: string) {
return new Date(date).toISOString().slice(0, 10);
}
@@ -0,0 +1,131 @@
import { EmptyStateCard } from "app/(app)/team/components/Analytics/EmptyStateCard";
import { ResponsiveSuspense } from "responsive-rsc";
import type { ThirdwebClient } from "thirdweb";
import {
getInAppWalletUsage,
getUniversalBridgeUsage,
getX402Settlements,
} from "@/api/analytics";
import type { Project } from "@/api/project/projects";
import type { Range } from "@/components/analytics/date-range-selector";
import { LoadingChartState } from "@/components/analytics/empty-chart-state";
import { ProjectHighlightsCard } from "./highlights-card-ui";
import type { PageParams, PageSearchParams } from "./types";
export function ProjectHighlightCard(props: {
authToken: string;
client: ThirdwebClient;
interval: "day" | "week";
params: PageParams;
range: Range;
searchParams: PageSearchParams;
project: Project;
}) {
return (
<ResponsiveSuspense
fallback={<LoadingChartState className="h-[458px] border" />}
searchParamsUsed={["from", "to", "interval"]}
>
<AsyncAppHighlightsCard
authToken={props.authToken}
client={props.client}
interval={props.interval}
params={props.params}
project={props.project}
range={props.range}
/>
</ResponsiveSuspense>
);
}
async function AsyncAppHighlightsCard(props: {
project: Project;
range: Range;
interval: "day" | "week";
client: ThirdwebClient;
params: PageParams;
authToken: string;
}) {
const [
aggregatedUserStats,
walletUserStatsTimeSeries,
universalBridgeUsage,
x402Settlements,
] = await Promise.allSettled([
getInAppWalletUsage(
{
from: props.range.from,
period: "all",
projectId: props.project.id,
teamId: props.project.teamId,
to: props.range.to,
},
props.authToken,
),
getInAppWalletUsage(
{
from: props.range.from,
period: props.interval,
projectId: props.project.id,
teamId: props.project.teamId,
to: props.range.to,
},
props.authToken,
),
getUniversalBridgeUsage(
{
from: props.range.from,
period: props.interval,
projectId: props.project.id,
teamId: props.project.teamId,
to: props.range.to,
},
props.authToken,
),
getX402Settlements(
{
from: props.range.from,
period: props.interval,
projectId: props.project.id,
teamId: props.project.teamId,
to: props.range.to,
},
props.authToken,
),
]);
if (
walletUserStatsTimeSeries.status === "fulfilled" &&
universalBridgeUsage.status === "fulfilled"
) {
return (
<ProjectHighlightsCard
aggregatedUserStats={
aggregatedUserStats.status === "fulfilled"
? aggregatedUserStats.value
: []
}
userStats={
walletUserStatsTimeSeries.status === "fulfilled"
? walletUserStatsTimeSeries.value
: []
}
volumeStats={
universalBridgeUsage.status === "fulfilled"
? universalBridgeUsage.value
: []
}
x402Settlements={
x402Settlements.status === "fulfilled" ? x402Settlements.value : []
}
/>
);
}
return (
<EmptyStateCard
link="https://portal.thirdweb.com/wallets"
metric="Wallets"
/>
);
}
@@ -0,0 +1,79 @@
import { ResponsiveSuspense } from "responsive-rsc";
import { getInsightStatusCodeUsage } from "@/api/analytics";
import { EmptyChartStateGetStartedCTA } from "@/components/analytics/empty-chart-state";
import { RequestsByStatusGraph } from "../gateway/indexer/components/RequestsByStatusGraph";
export function IndexerRequestsChartCard(props: {
from: Date;
to: Date;
interval: "day" | "week";
projectId: string;
teamId: string;
authToken: string;
teamSlug: string;
projectSlug: string;
}) {
return (
<ResponsiveSuspense
fallback={
<RequestsByStatusGraph
data={[]}
isPending={true}
viewMoreLink={`/team/${props.teamSlug}/${props.projectSlug}/gateway/indexer`}
/>
}
searchParamsUsed={["from", "to", "interval"]}
>
<AsyncIndexerRequestsChartCard
teamSlug={props.teamSlug}
projectSlug={props.projectSlug}
from={props.from}
to={props.to}
interval={props.interval}
projectId={props.projectId}
teamId={props.teamId}
authToken={props.authToken}
/>
</ResponsiveSuspense>
);
}
async function AsyncIndexerRequestsChartCard(props: {
from: Date;
to: Date;
interval: "day" | "week";
projectId: string;
teamId: string;
authToken: string;
teamSlug: string;
projectSlug: string;
}) {
const requestsData = await getInsightStatusCodeUsage(
{
from: props.from,
period: props.interval,
projectId: props.projectId,
teamId: props.teamId,
to: props.to,
},
props.authToken,
).catch(() => undefined);
return (
<RequestsByStatusGraph
data={requestsData && "data" in requestsData ? requestsData.data : []}
isPending={false}
viewMoreLink={`/team/${props.teamSlug}/${props.projectSlug}/gateway/indexer`}
emptyChartState={
<EmptyChartStateGetStartedCTA
link={{
label: "View more details",
href: `/team/${props.teamSlug}/${props.projectSlug}/gateway/indexer`,
}}
title="No data available"
description="No indexer requests found in selected time period"
/>
}
/>
);
}
@@ -0,0 +1,144 @@
import { ResponsiveSuspense } from "responsive-rsc";
import type { ThirdwebClient } from "thirdweb";
import type { Project } from "@/api/project/projects";
import type { Range } from "@/components/analytics/date-range-selector";
import { LoadingChartState } from "@/components/analytics/empty-chart-state";
import { TransactionsChartCardAsync } from "../components/Transactions";
import { AIAnalyticsChartCard } from "./ai-card";
import { AllWalletConnectionsChart } from "./all-wallet-connections-chart";
import { BridgeChartCard } from "./bridge-card";
import { ProjectHighlightCard } from "./highlights-card";
import { IndexerRequestsChartCard } from "./indexer-card";
import { RPCRequestsChartCard } from "./rpc-card";
import type { PageParams, PageSearchParams } from "./types";
import { X402RequestsChartCard } from "./x402-card";
export async function ProjectAnalytics(props: {
project: Project;
params: PageParams;
range: Range;
interval: "day" | "week";
searchParams: PageSearchParams;
client: ThirdwebClient;
authToken: string;
}) {
const { project, params, range, interval, searchParams, client, authToken } =
props;
return (
<div className="flex grow flex-col gap-6">
{/* highlights */}
<ProjectHighlightCard
authToken={authToken}
client={client}
interval={interval}
params={params}
project={project}
range={range}
searchParams={searchParams}
/>
<div className="grid gap-6 md:grid-cols-2">
{/* wallets */}
<AllWalletConnectionsChart
teamSlug={params.team_slug}
projectSlug={params.project_slug}
authToken={authToken}
projectId={project.id}
from={range.from}
to={range.to}
interval={interval}
teamId={project.teamId}
/>
{/* bridge */}
<BridgeChartCard
teamSlug={params.team_slug}
projectSlug={params.project_slug}
from={range.from}
to={range.to}
interval={interval}
projectId={project.id}
teamId={project.teamId}
authToken={authToken}
/>
</div>
<div className="grid gap-6 md:grid-cols-2">
{/* Indexer */}
<IndexerRequestsChartCard
teamSlug={params.team_slug}
projectSlug={params.project_slug}
from={range.from}
to={range.to}
interval={interval}
projectId={project.id}
teamId={project.teamId}
authToken={authToken}
/>
{/* RPC */}
<RPCRequestsChartCard
teamSlug={params.team_slug}
projectSlug={params.project_slug}
from={range.from}
to={range.to}
interval={interval}
projectId={project.id}
teamId={project.teamId}
authToken={authToken}
/>
</div>
<div className="grid gap-6 md:grid-cols-2">
{/* x402 */}
<X402RequestsChartCard
teamSlug={params.team_slug}
projectSlug={params.project_slug}
from={range.from}
to={range.to}
interval={interval}
projectId={project.id}
teamId={project.teamId}
authToken={authToken}
/>
{/* AI */}
<AIAnalyticsChartCard
teamSlug={params.team_slug}
projectSlug={params.project_slug}
from={range.from}
to={range.to}
interval={interval}
projectId={project.id}
teamId={project.teamId}
authToken={authToken}
/>
</div>
{/* Transactions, Chains, Contracts */}
<ResponsiveSuspense
fallback={<LoadingChartState className="h-[458px] border" />}
searchParamsUsed={["from", "to", "interval", "client_transactions"]}
>
<TransactionsChartCardAsync
client={client}
params={{
from: range.from,
period: interval,
projectId: project.id,
teamId: project.teamId,
to: range.to,
}}
authToken={authToken}
selectedChart={
typeof searchParams.client_transactions === "string"
? searchParams.client_transactions
: undefined
}
selectedChartQueryParam="client_transactions"
/>
</ResponsiveSuspense>
</div>
);
}
@@ -0,0 +1,81 @@
import { ResponsiveSuspense } from "responsive-rsc";
import { getRpcUsageByType } from "@/api/analytics";
import { EmptyChartStateGetStartedCTA } from "@/components/analytics/empty-chart-state";
import { RPCRequestsChartUI } from "../gateway/rpc/components/RequestsGraph";
async function AsyncRPCRequestsChartCard(props: {
from: Date;
to: Date;
interval: "day" | "week";
projectId: string;
teamId: string;
authToken: string;
teamSlug: string;
projectSlug: string;
}) {
const requestsData = await getRpcUsageByType(
{
from: props.from,
period: props.interval,
projectId: props.projectId,
teamId: props.teamId,
to: props.to,
},
props.authToken,
).catch(() => undefined);
const isEmpty = requestsData?.every((d) => d.count === 0);
return (
<RPCRequestsChartUI
isPending={false}
data={isEmpty ? [] : requestsData || []}
viewMoreLink={`/team/${props.teamSlug}/${props.projectSlug}/gateway/rpc`}
emptyChartState={
<EmptyChartStateGetStartedCTA
link={{
label: "View more details",
href: `/team/${props.teamSlug}/${props.projectSlug}/gateway/rpc`,
}}
title="No data available"
description="No RPC requests found in selected time period"
/>
}
/>
);
}
export function RPCRequestsChartCard(props: {
from: Date;
to: Date;
interval: "day" | "week";
projectId: string;
teamId: string;
authToken: string;
teamSlug: string;
projectSlug: string;
}) {
return (
<ResponsiveSuspense
fallback={
<RPCRequestsChartUI
isPending={true}
data={[]}
viewMoreLink={`/team/${props.teamSlug}/${props.projectSlug}/gateway/rpc`}
/>
}
searchParamsUsed={["from", "to", "interval"]}
>
<AsyncRPCRequestsChartCard
teamSlug={props.teamSlug}
projectSlug={props.projectSlug}
from={props.from}
to={props.to}
interval={props.interval}
projectId={props.projectId}
teamId={props.teamId}
authToken={props.authToken}
/>
</ResponsiveSuspense>
);
}
@@ -0,0 +1,13 @@
export type PageParams = {
team_slug: string;
project_slug: string;
};
export type PageSearchParams = {
from: string | undefined | string[];
to: string | undefined | string[];
type: string | undefined | string[];
interval: string | undefined | string[];
client_transactions: string | undefined | string[];
totalSponsored: string | undefined | string[];
};
@@ -0,0 +1,93 @@
import { ResponsiveSuspense } from "responsive-rsc";
import { getX402Settlements } from "@/api/analytics";
import { EmptyChartStateGetStartedCTA } from "@/components/analytics/empty-chart-state";
import { X402RequestsChartCardUI } from "./x402-requests-chart";
async function AsyncX402RequestsChart(props: {
from: Date;
to: Date;
interval: "day" | "week";
projectId: string;
teamId: string;
authToken: string;
teamSlug: string;
projectSlug: string;
}) {
const stats = await getX402Settlements(
{
from: props.from,
period: props.interval,
projectId: props.projectId,
teamId: props.teamId,
to: props.to,
},
props.authToken,
).catch((error) => {
console.error(error);
return [];
});
const isAllEmpty = stats.every((stat) => stat.totalRequests === 0);
return (
<X402RequestsChartCardUI
stats={
isAllEmpty
? []
: stats.map((stat) => ({
requests: stat.totalRequests,
time: stat.date,
}))
}
isPending={false}
teamSlug={props.teamSlug}
projectSlug={props.projectSlug}
emptyChartState={
<EmptyChartStateGetStartedCTA
link={{
label: "View Configuration",
href: `/team/${props.teamSlug}/${props.projectSlug}/x402/configuration`,
}}
title="No data available"
description="No X402 requests found in selected time period"
/>
}
/>
);
}
export function X402RequestsChartCard(props: {
from: Date;
to: Date;
interval: "day" | "week";
projectId: string;
teamId: string;
authToken: string;
teamSlug: string;
projectSlug: string;
}) {
return (
<ResponsiveSuspense
searchParamsUsed={["from", "to", "interval"]}
fallback={
<X402RequestsChartCardUI
stats={[]}
isPending={true}
teamSlug={props.teamSlug}
projectSlug={props.projectSlug}
/>
}
>
<AsyncX402RequestsChart
from={props.from}
teamSlug={props.teamSlug}
projectSlug={props.projectSlug}
to={props.to}
interval={props.interval}
projectId={props.projectId}
teamId={props.teamId}
authToken={props.authToken}
/>
</ResponsiveSuspense>
);
}
@@ -0,0 +1,53 @@
"use client";
import { format } from "date-fns";
import { useMemo } from "react";
import { ThirdwebBarChart } from "@/components/blocks/charts/bar-chart";
import { TotalValueChartHeader } from "@/components/blocks/charts/chart-header";
export function X402RequestsChartCardUI(props: {
stats: Array<{ requests: number; time: string }>;
isPending: boolean;
teamSlug: string;
projectSlug: string;
emptyChartState?: React.ReactElement | undefined;
}) {
const total = useMemo(() => {
return props.stats.reduce((acc, curr) => acc + curr.requests, 0);
}, [props.stats]);
return (
<ThirdwebBarChart
chartClassName="w-full h-[275px]"
data={props.stats}
config={{
requests: {
color: "hsl(var(--chart-1))",
label: "Requests",
},
}}
isPending={props.isPending}
hideLabel={false}
toolTipLabelFormatter={(label) => {
return format(label, "MMM dd");
}}
toolTipValueFormatter={(value) => {
return compactNumberFormatter.format(value as number);
}}
customHeader={
<TotalValueChartHeader
total={total}
isPending={props.isPending}
title="X402 Requests"
viewMoreLink={`/team/${props.teamSlug}/${props.projectSlug}/x402`}
/>
}
emptyChartState={props.emptyChartState}
/>
);
}
const compactNumberFormatter = new Intl.NumberFormat("en-US", {
notation: "compact",
maximumFractionDigits: 2,
});
@@ -34,13 +34,17 @@ function AccountAbstractionSummaryInner(props: {
async function AsyncAccountAbstractionSummary(props: {
teamId: string;
projectId: string;
projectId?: string;
authToken: string;
from?: Date;
to?: Date;
}) {
const aggregateUserOpStats = await getAggregateUserOpUsage(
{
projectId: props.projectId,
teamId: props.teamId,
from: props.from,
to: props.to,
},
props.authToken,
);
@@ -55,8 +59,10 @@ async function AsyncAccountAbstractionSummary(props: {
export function AccountAbstractionSummary(props: {
teamId: string;
projectId: string;
projectId?: string;
authToken: string;
from?: Date;
to?: Date;
}) {
return (
<Suspense
@@ -71,6 +77,8 @@ export function AccountAbstractionSummary(props: {
projectId={props.projectId}
teamId={props.teamId}
authToken={props.authToken}
from={props.from}
to={props.to}
/>
</Suspense>
);
@@ -0,0 +1,302 @@
"use client";
import { format } from "date-fns";
import { useMemo, useState } from "react";
import { ThirdwebBarChart } from "@/components/blocks/charts/bar-chart";
import { DocLink } from "@/components/blocks/DocLink";
import { ExportToCSVButton } from "@/components/blocks/ExportToCSVButton";
import { Button } from "@/components/ui/button";
import type { ChartConfig } from "@/components/ui/chart";
import { useAllChainsData } from "@/hooks/chains/allChains";
import { DotNetIcon } from "@/icons/brand-icons/DotNetIcon";
import { ReactIcon } from "@/icons/brand-icons/ReactIcon";
import { TypeScriptIcon } from "@/icons/brand-icons/TypeScriptIcon";
import { UnityIcon } from "@/icons/brand-icons/UnityIcon";
import { UnrealIcon } from "@/icons/brand-icons/UnrealIcon";
import type { UserOpStats } from "@/types/analytics";
import { formatTickerNumber } from "@/utils/format-utils";
type ChartData = Record<string, number> & {
time: string;
};
type MetricType =
| "sponsoredTransactions"
| "gasUnits"
| "avgGasPrice"
| "costPerGasUnit";
export function GasMetricsChartCard(props: {
userOpStats: UserOpStats[];
isPending: boolean;
}) {
const { userOpStats } = props;
const topChainsToShow = 10;
const chainsStore = useAllChainsData();
const [metricType, setMetricType] = useState<MetricType>(
"sponsoredTransactions",
);
const { chartConfig, chartData } = useMemo(() => {
const _chartConfig: ChartConfig = {};
const _chartDataMap: Map<string, ChartData> = new Map();
const chainIdToVolumeMap: Map<string, number> = new Map();
// for each stat, add it in _chartDataMap
for (const stat of userOpStats) {
const chartData = _chartDataMap.get(stat.date);
const { chainId } = stat;
const chain = chainsStore.idToChain.get(Number(chainId));
const chainName = chain?.name || chainId || "Unknown";
let value: number;
if (metricType === "sponsoredTransactions") {
value = stat.successful;
} else if (metricType === "gasUnits") {
value = stat.gasUnits;
} else if (metricType === "avgGasPrice") {
value = stat.avgGasPrice;
} else {
// costPerGasUnit: USD spent per gas unit (helps identify price spike impact)
value = stat.gasUnits > 0 ? stat.sponsoredUsd / stat.gasUnits : 0;
}
// if no data for current day - create new entry
if (!chartData) {
_chartDataMap.set(stat.date, {
time: stat.date,
[chainName]: value,
} as ChartData);
} else {
chartData[chainName] = (chartData[chainName] || 0) + value;
}
chainIdToVolumeMap.set(
chainName,
value + (chainIdToVolumeMap.get(chainName) || 0),
);
}
const chainsSorted = Array.from(chainIdToVolumeMap.entries())
.sort((a, b) => b[1] - a[1])
.map((w) => w[0]);
const chainsToShow = chainsSorted.slice(0, topChainsToShow);
const chainsToTagAsOthers = chainsSorted.slice(topChainsToShow);
// replace chainIdsToTagAsOther chainId with "other"
for (const data of _chartDataMap.values()) {
for (const chainId in data) {
if (chainsToTagAsOthers.includes(chainId)) {
data.others = (data.others || 0) + (data[chainId] || 0);
delete data[chainId];
}
}
}
chainsToShow.forEach((chainName, i) => {
_chartConfig[chainName] = {
color: `hsl(var(--chart-${(i % 10) + 1}))`,
label: chainName,
};
});
// Add Other
if (chainsToTagAsOthers.length > 0) {
chainsToShow.push("others");
_chartConfig.others = {
color: "hsl(var(--muted-foreground))",
label: "Others",
};
}
return {
chartConfig: _chartConfig,
chartData: Array.from(_chartDataMap.values()).sort((a, b) => {
return new Date(a.time).getTime() - new Date(b.time).getTime();
}),
};
}, [userOpStats, chainsStore, metricType]);
const uniqueChainIds = Object.keys(chartConfig);
const disableActions =
props.isPending ||
chartData.length === 0 ||
chartData.every((data) => {
return Object.entries(data).every(([key, value]) => {
return key === "time" || value === 0;
});
});
const metricConfig = {
sponsoredTransactions: {
title: "Sponsored Transactions",
description: "Total number of sponsored transactions",
fileName: "Sponsored Transactions",
formatter: (value: number) => formatTickerNumber(value),
},
gasUnits: {
title: "Gas Units Consumed",
description: "Total gas units consumed by sponsored transactions",
fileName: "Gas Units Consumed",
formatter: (value: number) => formatTickerNumber(value),
},
avgGasPrice: {
title: "Average Gas Price",
description: "Average gas price in Gwei for sponsored transactions",
fileName: "Average Gas Price",
formatter: (value: number) => {
// Convert from wei to Gwei
const gwei = value / 1_000_000_000;
return `${formatTickerNumber(gwei)} Gwei`;
},
},
costPerGasUnit: {
title: "Cost per Gas Unit",
description:
"USD spent per gas unit - spikes indicate gas price increases",
fileName: "Cost per Gas Unit",
formatter: (value: number) => {
// Show in micro-USD for readability
return `$${(value * 1_000_000).toFixed(4)}µ`;
},
},
};
const config =
metricConfig[metricType as keyof typeof metricConfig] ||
metricConfig.sponsoredTransactions;
return (
<ThirdwebBarChart
chartClassName="aspect-[1] lg:aspect-[3.5]"
config={chartConfig}
customHeader={
<div className="relative px-6 pt-6">
<div className="flex items-start justify-between gap-4">
<div>
<h3 className="mb-0.5 font-semibold text-xl tracking-tight">
{config.title}
</h3>
<p className="text-muted-foreground text-sm">
{config.description}
</p>
</div>
<div className="flex gap-2 flex-wrap">
<Button
onClick={() => setMetricType("sponsoredTransactions")}
size="sm"
variant={
metricType === "sponsoredTransactions" ? "default" : "outline"
}
>
Transactions
</Button>
<Button
onClick={() => setMetricType("gasUnits")}
size="sm"
variant={metricType === "gasUnits" ? "default" : "outline"}
>
Gas Units
</Button>
<Button
onClick={() => setMetricType("avgGasPrice")}
size="sm"
variant={metricType === "avgGasPrice" ? "default" : "outline"}
>
Gas Price
</Button>
<Button
onClick={() => setMetricType("costPerGasUnit")}
size="sm"
variant={
metricType === "costPerGasUnit" ? "default" : "outline"
}
>
Cost Efficiency
</Button>
</div>
</div>
<div className="top-6 right-6 mt-4 mb-4 grid grid-cols-2 items-center gap-2 md:absolute md:my-0 md:flex md:mt-10">
<ExportToCSVButton
className="bg-background"
disabled={disableActions}
fileName={config.fileName}
getData={async () => {
const header = ["Date", ...uniqueChainIds];
const rows = chartData.map((data) => {
const { time, ...rest } = data;
return [
time,
...uniqueChainIds.map((w) => (rest[w] || 0).toString()),
];
});
return { header, rows };
}}
/>
</div>
</div>
}
data={chartData}
emptyChartState={<GasMetricsChartCardEmptyChartState />}
hideLabel={false}
isPending={props.isPending}
showLegend
toolTipLabelFormatter={(_v, item) => {
if (Array.isArray(item)) {
const time = item[0].payload.time as number;
return format(new Date(time), "MMM d, yyyy");
}
return undefined;
}}
toolTipValueFormatter={(value) => {
if (typeof value !== "number") {
return "";
}
return config.formatter(value);
}}
variant="stacked"
/>
);
}
function GasMetricsChartCardEmptyChartState() {
return (
<div className="flex flex-col items-center justify-center">
<span className="mb-6 text-lg">Sponsor gas for your users</span>
<div className="flex max-w-md flex-wrap items-center justify-center gap-x-6 gap-y-4">
<DocLink
icon={TypeScriptIcon}
label="TypeScript"
link="https://portal.thirdweb.com/typescript/v5/account-abstraction/get-started"
/>
<DocLink
icon={ReactIcon}
label="React"
link="https://portal.thirdweb.com/react/v5/account-abstraction/get-started"
/>
<DocLink
icon={ReactIcon}
label="React Native"
link="https://portal.thirdweb.com/react/v5/account-abstraction/get-started"
/>
<DocLink
icon={UnityIcon}
label="Unity"
link="https://portal.thirdweb.com/unity/v5/wallets/account-abstraction"
/>
<DocLink
icon={UnrealIcon}
label="Unreal Engine"
link="https://portal.thirdweb.com/unreal-engine/blueprints/smart-wallet"
/>
<DocLink
icon={DotNetIcon}
label=".NET"
link="https://portal.thirdweb.com/dotnet/wallets/providers/account-abstraction"
/>
</div>
</div>
);
}
@@ -9,6 +9,8 @@ export function createUserOpStatsStub(days: number): UserOpStats[] {
const successful = Math.random() * 100;
const failed = Math.random() * 100;
const sponsoredUsd = Math.random() * 100;
const gasUnits = Math.random() * 1000000; // Random gas units between 0-1M
const avgGasPrice = Math.random() * 100000000000; // Random gas price in wei (0-100 Gwei)
stubbedData.push({
chainId: Math.floor(Math.random() * 100).toString(),
@@ -16,6 +18,8 @@ export function createUserOpStatsStub(days: number): UserOpStats[] {
failed,
sponsoredUsd,
successful,
gasUnits,
avgGasPrice,
});
if (Math.random() > 0.7) {
@@ -11,17 +11,34 @@ import {
import { IntervalSelector } from "@/components/analytics/interval-selector";
import { SponsoredTransactionsTable } from "@/components/sponsored-transactions-table/SponsoredTransactionsTable";
import type { UserOpStats } from "@/types/analytics";
import { SponsoredTransactionsChartCard } from "./AccountAbstractionAnalytics/SponsoredTransactionsChartCard";
import { GasMetricsChartCard } from "./AccountAbstractionAnalytics/GasMetricsChartCard";
import { TotalSponsoredChartCard } from "./AccountAbstractionAnalytics/TotalSponsoredChartCard";
import { searchParams } from "./search-params";
export function AccountAbstractionAnalytics(props: {
type AccountAbstractionAnalyticsProps = {
userOpStats: UserOpStats[];
teamId: string;
teamSlug: string;
client: ThirdwebClient;
projectId: string;
}) {
} & (
| {
variant: "project";
projectId: string;
}
| {
variant: "team";
projects: {
id: string;
name: string;
image: string | null;
slug: string;
}[];
}
);
export function AccountAbstractionAnalytics(
props: AccountAbstractionAnalyticsProps,
) {
const [isLoading, startTransition] = useTransition();
const [rangeType, setRangeType] = useQueryState(
@@ -62,6 +79,8 @@ export function AccountAbstractionAnalytics(props: {
to: rangeType === "custom" ? to : getLastNDaysRange(rangeType).to,
type: rangeType,
};
const resolvedFrom = range.from;
const resolvedTo = range.to;
return (
<div>
@@ -100,20 +119,23 @@ export function AccountAbstractionAnalytics(props: {
userOpStats={props.userOpStats}
/>
<SponsoredTransactionsChartCard
<GasMetricsChartCard
isPending={isLoading}
userOpStats={props.userOpStats}
/>
<SponsoredTransactionsTable
client={props.client}
from={from.toISOString()}
projectId={props.projectId}
teamId={props.teamId}
teamSlug={props.teamSlug}
to={to.toISOString()}
variant="project"
/>
{resolvedFrom && resolvedTo && (
<SponsoredTransactionsTable
client={props.client}
from={resolvedFrom.toISOString()}
teamId={props.teamId}
teamSlug={props.teamSlug}
to={resolvedTo.toISOString()}
{...(props.variant === "project"
? { projectId: props.projectId, variant: "project" as const }
: { projects: props.projects, variant: "team" as const })}
/>
)}
</div>
</div>
);
@@ -3,7 +3,7 @@ import Link from "next/link";
import { Suspense } from "react";
import { defineChain, getContract, type ThirdwebClient } from "thirdweb";
import { getCompilerMetadata } from "thirdweb/contract";
import { getSortedDeployedContracts } from "@/api/project/getSortedDeployedContracts";
import { getFilteredProjectContracts } from "@/api/project/getSortedDeployedContracts";
import { ClientOnly } from "@/components/blocks/client-only";
import { GenericLoadingPage } from "@/components/blocks/skeletons/GenericLoadingPage";
import { Button } from "@/components/ui/button";
@@ -60,9 +60,9 @@ async function AsyncYourFactories(props: {
projectSlug: string;
clientThirdwebClient: ThirdwebClient;
}) {
const deployedContracts = await getSortedDeployedContracts({
const deployedContracts = await getFilteredProjectContracts({
authToken: props.authToken,
deploymentType: undefined,
type: "non-token-contracts",
projectId: props.projectId,
teamId: props.teamId,
});
@@ -44,6 +44,7 @@ type AccountAbstractionSettingsPageProps = {
teamId: string;
teamSlug: string;
validTeamPlan: Team["billingPlan"];
isLegacyPlan: boolean;
client: ThirdwebClient;
};
@@ -565,6 +566,7 @@ export function AccountAbstractionSettingsPage(
<GatedSwitch
currentPlan={props.validTeamPlan}
requiredPlan="starter"
isLegacyPlan={props.isLegacyPlan}
switchProps={{
checked: field.value.enabled,
id: "server-verifier-switch",
@@ -1,10 +1,12 @@
"use client";
import { format } from "date-fns";
import { BotIcon } from "lucide-react";
import { ArrowUpRightIcon } from "lucide-react";
import Link from "next/link";
import { useMemo } from "react";
import { ThirdwebBarChart } from "@/components/blocks/charts/bar-chart";
import { DocLink } from "@/components/blocks/DocLink";
import { TotalValueChartHeader } from "@/components/blocks/charts/chart-header";
import { ExportToCSVButton } from "@/components/blocks/ExportToCSVButton";
import { Button } from "@/components/ui/button";
import type { ChartConfig } from "@/components/ui/chart";
import type { AIUsageStats } from "@/types/analytics";
@@ -17,7 +19,8 @@ export function AiTokenUsageChartCardUI(props: {
aiUsageStats: AIUsageStats[];
isPending: boolean;
title: string;
description: string;
viewMoreLink: string | undefined;
emptyChartState?: React.ReactElement | undefined;
}) {
const { aiUsageStats } = props;
@@ -52,36 +55,46 @@ export function AiTokenUsageChartCardUI(props: {
chartData.length === 0 ||
chartData.every((data) => data.tokens === 0);
const total = useMemo(() => {
return chartData.reduce((acc, curr) => acc + curr.tokens, 0);
}, [chartData]);
return (
<ThirdwebBarChart
chartClassName="aspect-[1.5] lg:aspect-[3.5]"
chartClassName="h-[275px] w-full aspect-auto"
config={chartConfig}
customHeader={
<div className="relative px-6 pt-6">
<h3 className="mb-0.5 font-semibold text-xl tracking-tight">
{props.title}
</h3>
<p className="mb-3 text-muted-foreground text-sm">
{props.description}
</p>
<ExportToCSVButton
className="top-6 right-6 mb-4 w-full bg-background md:absolute md:mb-0 md:flex md:w-auto"
disabled={disableActions}
fileName="AI Token Usage"
getData={async () => {
const header = ["Date", "Tokens"];
const rows = chartData.map((data) => [
data.time,
data.tokens.toString(),
]);
return { header, rows };
}}
props.viewMoreLink ? (
<TotalValueChartHeader
isPending={props.isPending}
total={total}
title={props.title}
viewMoreLink={props.viewMoreLink}
/>
</div>
) : (
<div className="relative px-6 pt-6">
<h3 className="font-semibold text-xl tracking-tight">
{props.title}
</h3>
<ExportToCSVButton
className="top-6 right-6 mb-4 w-full bg-background md:absolute md:mb-0 md:flex md:w-auto"
disabled={disableActions}
fileName="AI Token Usage"
getData={async () => {
const header = ["Date", "Tokens"];
const rows = chartData.map((data) => [
data.time,
data.tokens.toString(),
]);
return { header, rows };
}}
/>
</div>
)
}
data={chartData}
emptyChartState={<AiTokenUsageEmptyChartState />}
emptyChartState={props.emptyChartState}
hideLabel={false}
isPending={props.isPending}
showLegend={false}
@@ -97,20 +110,27 @@ export function AiTokenUsageChartCardUI(props: {
);
}
function AiTokenUsageEmptyChartState() {
function _AiTokenUsageEmptyChartState() {
return (
<div className="flex flex-col items-center justify-center px-4">
<span className="mb-6 text-center text-lg">
<h3 className="text-base font-semibold text-foreground mb-1 text-center">
No data available
</h3>
<p className="mb-6 text-center text-sm">
Integrate thirdweb AI to interact with any EVM chain using natural
language
</span>
<div className="flex max-w-md flex-wrap items-center justify-center gap-x-6 gap-y-4">
<DocLink
icon={BotIcon}
label="Get Started"
link="https://portal.thirdweb.com/ai/chat"
/>
</div>
</p>
<Button
asChild
size="sm"
variant="default"
className="rounded-full gap-2"
>
<Link href="https://portal.thirdweb.com/ai/chat" target="_blank">
Get Started
<ArrowUpRightIcon className="size-4" />
</Link>
</Button>
</div>
);
}
@@ -18,9 +18,9 @@ function AiAnalyticsUI({ stats, isPending }: AiAnalyticsProps) {
return (
<AiTokenUsageChartCardUI
title="Token Usage"
description="The total number of tokens used for AI interactions on your project."
aiUsageStats={stats || []}
isPending={isPending}
viewMoreLink={undefined}
/>
);
}
@@ -1,4 +1,4 @@
import { notFound } from "next/navigation";
import { notFound, redirect } from "next/navigation";
import { getAuthToken, getUserThirdwebClient } from "@/api/auth-token";
import { getProject } from "@/api/project/projects";
import { getSessionById, getSessions } from "../../api/session";
@@ -39,7 +39,7 @@ export default async function Page(props: {
]);
if (!session) {
notFound();
redirect(`/team/${params.team_slug}/${params.project_slug}/ai`);
}
return (
@@ -410,7 +410,7 @@ export function ChatBar(props: {
if (message.trim() === "" && images.length === 0) return;
handleSubmit(message);
}}
variant="primary"
variant="default"
>
<ArrowUpIcon className="size-4" />
</Button>

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