Compare commits

...
Author SHA1 Message Date
Joaquim VergesGitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
1d28988253 Version Packages (#8687)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-02-24 22:39:15 +13:00
5352152835 Connect modal approval changeset (#8686)
Co-authored-by: Cursor Agent <[email protected]>
Co-authored-by: Yash <[email protected]>
2026-02-24 22:36:53 +13:00
decbbb8f7a Connect modal require approval (#8683)
Co-authored-by: Cursor Agent <[email protected]>
Co-authored-by: Yash <[email protected]>
2026-02-24 22:01:29 +13:00
Joaquim VergesGitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
ecd5ae1feb Version Packages (#8672)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-02-13 23:39:01 +07:00
FirekeeperandGitHub 1caf6dd460 changeset / fmt (#8671) 2026-02-13 23:34:21 +07:00
19204e4654 [SDK] Fix Safe wallet WalletConnect connection on Berachain (#8670)
Co-authored-by: Claude Opus 4.6 <[email protected]>
2026-02-13 23:24:37 +07:00
Manan TankandGitHub 674c8d5e6c portal: Fix sidebar spacing on top (#8666) 2026-02-11 17:09:29 +13:00
1eb0cd9314 Feat/partner logo branding (#8668)
Co-authored-by: Claude Opus 4.6 <[email protected]>
2026-02-10 14:52:06 -08:00
Joaquim VergesGitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
b7dfde2d68 Version Packages (#8665)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-02-04 19:54:15 -08:00
Joaquim VergesandGitHub b50e304e0c changeset (#8664) 2026-02-04 17:56:41 -08:00
9baa251bb1 [SDK] Fix SiteLink and SiteEmbed stripping URL hash fragments (#8663)
Co-authored-by: Claude Opus 4.5 <[email protected]>
2026-02-04 17:55:11 -08:00
Manan TankandGitHub 7f36303b0b portal: OpenApiEndpoint card UI tweaks (#8661) 2026-02-04 17:50:30 -08:00
Manan TankandGitHub 13a46b7ff6 portal: Update SDK Reference breadcrumbs UI (#8662) 2026-02-04 17:50:07 -08:00
Manan TankandGitHub b9d93e44ca portal: landing page ui improvements (#8647) 2026-01-30 12:09:55 -08:00
Joaquim VergesGitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
fc9e81f99a Version Packages (#8644)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-01-20 16:27:12 -08:00
281759f06a Add base account sdk (#8634)
Co-authored-by: Joaquim Verges <[email protected]>
2026-01-20 16:04:13 -08:00
Joaquim VergesandGitHub 2d98fe2799 Support linking accounts with redirect mode (#8643) 2026-01-20 14:40:33 -08:00
Joaquim VergesGitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
93bcdc557c Version Packages (#8640)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-01-15 17:26:04 -08:00
Joaquim VergesandGitHub d38deecaa1 Fix getCallStatus via EIP1193 provider (#8639) 2026-01-15 17:20:25 -08:00
Joaquim VergesGitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
a3ea002afd Version Packages (#8638)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-01-14 22:56:41 +07:00
FirekeeperandGitHub 6390052f8b Add fallback for gas fee estimation in zkSync (#8637) 2026-01-14 22:54:24 +07:00
Joaquim VergesGitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
621f187a8a Version Packages (#8622)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-01-13 13:36:58 -08:00
Joaquim VergesandGitHub 36ff327744 support x402 v2 headers (#8623) 2026-01-13 12:59:11 -08:00
Manan TankandGitHub f68c2c62a5 [MNY-233] SDK: Add Last Used badge in Connect UI (#8631) 2026-01-13 10:45:05 -08:00
5766c90fb4 [SDK] Fix: Parse x402 payment header (#8617)
Co-authored-by: Joaquim Verges <[email protected]>
2026-01-12 11:20:43 -08:00
MananTank 8d4b6a1fea [MNY-356] Add tokenEditable and amountEditable props on BuyWidget (#8621)
<!--

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

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

## Notes for the reviewer

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

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR introduces two new properties, `amountEditable` and `tokenEditable`, to enhance the `BuyWidget` component, allowing control over editing options for token selection and amount. This improves user experience by providing flexibility in the widget's functionality.

### Detailed summary
- Added `amountEditable` and `tokenEditable` properties to `BuyWidget`.
- Updated `BuyPlayground.tsx`, `CheckoutPlayground.tsx`, and `TransactionPlayground.tsx` to use new props.
- Modified `CodeGen.tsx` to incorporate editability options.
- Updated URL generation in `buildBuyIframeUrl.ts` for new props.
- Added editability options in `LeftSection.tsx`.
- Enhanced `BuyWidgetEmbed` to accept new props.
- Implemented stories in `BuyWidget.stories.tsx` for editable and non-editable states.
- Added documentation in `iframe/page.mdx` for new features.
- Adjusted `FundWallet.tsx` and `TokenSection.tsx` to respect new editability options.

> ✨ 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 `amountEditable` and `tokenEditable` props to the BuyWidget component, allowing developers to disable token selection and/or amount editing in the buy flow.
  * Enhanced UI controls to prevent interaction with locked fields when these options are disabled.
  * Updated documentation with examples of how to use the new editability options.

* **Tests**
  * Added story variants demonstrating token and amount lock scenarios.

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

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-01-12 18:12:38 +00:00
FirekeeperandGitHub 49b80fe87f Add self-managed vault recovery option for lost secret key (#8619) 2026-01-12 23:16:45 +07:00
FirekeeperandGitHub 5ca4d7497b Preserve wallet address on vault credential rotation (#8618) 2026-01-12 22:27:08 +07:00
kumaryash90 0382d42a99 Enable multiple ref params for bytes input (#8610)
<!--

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

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

## Notes for the reviewer

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

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on modifying the `Button` component in the `decoded-input-set.tsx` file to enhance its functionality based on certain conditions related to `param.type` and `fields`.

### Detailed summary
- Updated the `Button` component to disable it when `param.type` is "bytes" and `fields.length` is 1 or more.
- Maintained the existing `onClick` functionality to append a new input with a default value of an empty string.

> ✨ 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**
  * Fixed an issue in the contract publish form where the Add Parameter button was incorrectly disabled when working with bytes parameters. Users can now add multiple bytes parameters without restriction.

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

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-01-08 15:23:52 +00:00
MananTank d35cad8655 [MNY-355] Playground: Add Buy Widget iframe playground (#8615)
<!--

## 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 `buy` widget functionality by introducing iframe support and improving the handling of integration types. It also adds new components for rendering iframe previews and updates the URL management for better integration experience.

### Detailed summary
- Updated `LeftSection.tsx` to allow both `checkout` and `buy` widgets for iframe integration.
- Modified `Page` component in `page.tsx` to handle async `searchParams` and pass `defaultTab` to `BuyPlayground`.
- Enhanced `CodeGen` to support widget type in iframe code generation.
- Added `CheckoutIframePreview` and `BuyWidgetIframePreview` components in `RightSection.tsx` for rendering respective iframes.
- Created `buildBuyIframeUrl` function in `buildBuyIframeUrl.ts` for generating URLs for the buy widget.
- Updated `BuyPlayground` to manage state and URL for integration type selection using tabs.

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

<!-- end pr-codex -->
2026-01-07 20:23:15 +00:00
MananTank c1275a5e38 [MNY-354] Portal: Add Buy Widget iframe docs (#8614)
<!--

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

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

## Notes for the reviewer

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

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR introduces the `Buy Widget`, enhancing the portal with a new iframe component for purchasing crypto. It includes documentation, features, and examples for users to integrate the widget easily.

### Detailed summary
- Added `IframeCodePreview` component in `iframe-code-preview.tsx`.
- Updated sidebar links to reflect the new `Buy Widget` and `Swap Widget`.
- Created documentation for the `Buy Widget` in `page.mdx`, detailing features and integration.
- Added example usage of the `BuyWidget` component in `react/page.mdx`.
- Developed `iframe/page.mdx` with customization options and event listening for the widget.

> ✨ 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 an iframe embed preview with code/preview tabs for trying the Buy widget.

* **Documentation**
  * Added comprehensive Buy widget docs (overview, React and iframe guides, examples, customization, event listening, live playgrounds).

* **Chores**
  * Updated site navigation: Widgets section reorganized to list Swap, Buy, and Checkout widgets with updated links.

<sub>✏️ Tip: You can customize this high-level summary in your review settings.</sub>
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-01-07 20:23:14 +00:00
MananTank 277874594b SDK: token details screen UI tweaks (#8612)
<!--

## 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 user interface of the `TokenInfoScreen` component in the `select-token-ui.tsx` file by enhancing loading states, adjusting layout heights, and refining token display logic.

### Detailed summary
- Removed initial loading and token not found states.
- Increased `Container` minHeight from `350px` to `450px`.
- Refactored conditional rendering for token details.
- Maintained structure for displaying token name, symbol, price, market cap, volume, and contract address.

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

<!-- end pr-codex -->

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

* **Refactor**
  * Consolidated rendering logic for the token information view to centralize pending, not-found, and content states.
  * Reorganized layout so detailed token sections render only when token data is present.
  * Adjusted minimum container height for token information screens in the bridge swap widget.

<sub>✏️ Tip: You can customize this high-level summary in your review settings.</sub>
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-01-07 19:26:05 +00:00
MananTank 4fc02b3618 [MNY-342] Dashboard: Add Buy Widget iframe (#8613)
<!--

## 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 `BuyWidget` feature, allowing users to purchase crypto tokens using various payment methods. It adds necessary environment variables, updates configuration for security policies, and implements the layout and functionality for the `BuyWidgetEmbed` component.

### Detailed summary
- Added `NEXT_PUBLIC_BUY_IFRAME_CLIENT_ID` to `public-envs.ts`.
- Updated `next.config.ts` with new Content Security Policy headers for `/bridge/buy-widget`.
- Created `BuyWidgetLayout` component in `layout.tsx` for the buy widget page.
- Implemented `BuyWidgetEmbed` component in `BuyWidgetEmbed.client.tsx` for handling crypto purchases.
- Enhanced `page.tsx` to manage search parameters and integrate `BuyWidgetEmbed`.

> ✨ 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 a new buy widget enabling direct cryptocurrency purchases with configurable payment methods (crypto and card) and multi-currency support
  * Implemented personalization options including custom titles, descriptions, images, and button labels
  * Enhanced security infrastructure to support the new widget functionality

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

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-01-07 19:16:44 +00:00
Yash094 26a91537d2 Etherlink shadownet chain (#8611)
<!--

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

-->
[SDK] Feature: Add Etherlink Shadownet Testnet chain

## Notes for the reviewer

Adds the Etherlink Shadownet testnet chain (ID 127823) to the predefined chains in the SDK. This includes its native currency (XTZ) and block explorer URL.

## How to test

- Verify the `etherlinkShadownet` chain can be imported from `thirdweb/chains`.
- Check that the chain object contains the correct `id`, `name`, `nativeCurrency`, and `blockExplorers` properties.
- Run `pnpm build` and `pnpm lint` to ensure no new issues are introduced.

---
[Slack Thread](https://thirdwebdev.slack.com/archives/C09DS2CKGP2/p1767789368607579?thread_ts=1767789368.607579&cid=C09DS2CKGP2)

<a href="https://cursor.com/background-agent?bcId=bc-a0f088a7-057b-46f0-b6a6-3ba957d1e261"><picture><source media="(prefers-color-scheme: dark)" srcset="https://cursor.com/open-in-cursor-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://cursor.com/open-in-cursor-light.svg"><img alt="Open in Cursor" src="https://cursor.com/open-in-cursor.svg"></picture></a>&nbsp;<a href="https://cursor.com/agents?id=bc-a0f088a7-057b-46f0-b6a6-3ba957d1e261"><picture><source media="(prefers-color-scheme: dark)" srcset="https://cursor.com/open-in-web-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://cursor.com/open-in-web-light.svg"><img alt="Open in Web" src="https://cursor.com/open-in-web.svg"></picture></a>

<!-- start pr-codex -->

---

## PR-Codex overview
This PR introduces the `Etherlink Shadownet` testnet chain to the project, defining its properties and adding it to the exports.

### Detailed summary
- Added a new chain definition for `etherlinkShadownet` in `etherlink-shadownet.ts`.
- Defined properties such as `id`, `name`, `nativeCurrency`, and `blockExplorers`.
- Exported `etherlinkShadownet` in `chains.ts` for use in the application.

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

<!-- end pr-codex -->

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

## Summary by CodeRabbit

* **New Features**
  * Added support for Etherlink Shadownet testnet chain, enabling development and testing on this network with proper block explorer integration and native currency configuration (XTZ).

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

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-01-07 14:06:28 +00:00
MananTank a3ff7572d6 [MNY-319] SDK: Add token details screen in token selection UI (#8608)
<!--

## 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 token selection UI by adding a token details screen and improving the display of token information within the `SwapWidget` and `BridgeWidget`.

### Detailed summary
- Added a token details screen in the token selection UI.
- Enhanced the `SelectedTokenButton` to include a more flexible display of the token symbol.
- Introduced `useTokenPrice` hook for fetching token prices.
- Updated `TokenButton` to include an info button for showing token details.
- Created `TokenInfoScreen` and `TokenInfoRow` components for displaying detailed token information.
- Integrated token info functionality into `TokenSelectionScreen`.

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

<!-- end pr-codex -->
2026-01-06 21:15:08 +00:00
MananTank 01004e8408 [MNY-353] SDK: TransactionWidget UI improvements (#8607)
<!--

## 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 UI improvements for the `TransactionWidget` in the `thirdweb` package, enhancing user experience with better layout, spacing, and loading indicators.

### Detailed summary
- Changed the `chain` in `simpleBuyRequest` from `baseSepolia` to `base`.
- Updated the story names and added new stories for `TransactionWidget`.
- Improved layout and spacing in `TransactionPayment`.
- Replaced static loading elements with `Skeleton` components for better loading states.
- Enhanced button styles and added a loading spinner.
- Refined contract info and address display for better clarity.

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

<!-- end pr-codex -->

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

## Summary by CodeRabbit

* **UI Improvements**
  * Improved TransactionWidget loading states with clearer visual spinner components
  * Enhanced visual presentation and layout of transaction details, addresses, and network information
  * Refined typography, spacing, and styling throughout the widget for better readability 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 -->
2026-01-06 21:15:08 +00:00
MananTank f497b614cb [MNY-352] Playground: add swap widget iframe (#8605)
<!--

## 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 enhancements to the `SwapWidgetPlayground`, allowing for integration with both `iframe` and `react` types. It modifies how themes and options are handled, improves URL management, and updates iframe handling for better user experience.

### Detailed summary
- Added `integrationType` to `SwapWidgetPlaygroundOptions`.
- Conditional rendering of `ColorFormGroup` based on `integrationType`.
- Created `getPreviewSrc` function to manage iframe URLs.
- Updated `persistTokenSelections` handling in various components.
- Introduced `SwapWidgetPlaygroundAsync` for async loading.
- Refactored `RightSection` to conditionally render `iframe` or `SwapWidget`.
- Implemented URL update logic based on selected integration type.
- Enhanced code generation for iframe and react integration types.

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

<!-- end pr-codex -->
2026-01-05 21:40:36 +00:00
MananTank f344fb5fbd [MNY-351] Portal: Add swap widget iframe docs (#8604)
<!--

## 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 `Swap Widget` functionality by adding an `iframe` integration option and a new `React Component`. It updates documentation and examples for better usability and expands features like dark/light mode support and customizable token selections.

### Detailed summary
- Added `links` for `React Component` and `Iframe` in `sidebar.tsx`.
- Introduced `IframeCodePreview` component in `iframe-code-preview.tsx`.
- Updated `page.mdx` for `iframe` integration with features and examples.
- Revised `page.mdx` for `react` with improved descriptions and features.
- Enhanced API reference and examples for token selection configurations.
- Added iframe event listening instructions for swap results.

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

<!-- end pr-codex -->
2026-01-05 21:40:36 +00:00
MananTank 9f265bf734 [MNY-344] Dashboard: Add swap-widget iframe (#8603)
<!--

## 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 `NEXT_PUBLIC_SWAP_IFRAME_CLIENT_ID` constant and enhances the swap functionality with a `SwapWidgetEmbed` component, while refactoring the existing `Providers` component to `BridgeProvidersLite`. It also adds necessary headers for security and updates layout components.

### Detailed summary
- Added `NEXT_PUBLIC_SWAP_IFRAME_CLIENT_ID` constant in `public-envs.ts`.
- Updated `next.config.ts` to include security headers for swap widget routes.
- Introduced `BridgeProvidersLite` component in `Providers.client.tsx`.
- Created `SwapWidgetEmbed` component for swap functionality.
- Refactored `page.tsx` to utilize `BridgeProvidersLite` and `SwapWidgetEmbed`.
- Removed the old `Providers` component logic.

> ✨ 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 cross‑chain swap widget with page, embed component and layout, URL-prefill support, theming, and fiat currency options.
  * Added a lightweight provider wrapper for embed usage.

* **Changes**
  * Checkout widget no longer auto-connects preconfigured wallets; provider usage unified to the lightweight wrapper.

* **Chores**
  * Added CSP headers for swap widget routes.
  * Introduced public env var for swap iframe client ID.

<sub>✏️ Tip: You can customize this high-level summary in your review settings.</sub>
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-01-05 20:26:31 +00:00
MananTank e5519eaef9 Dashboard: Update bridge supported tokens count (#8602)
<!--

## 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` value in the `data.ts` file for the bridge statistics, reflecting an increase in the number of supported tokens.

### Detailed summary
- Updated `supportedTokens` from `"14,000+"` to `"17,000+"`.
- Changed `supportedTokens` from `14000` to `17000` in the `bridgeStatsNumbers` object.

> ✨ 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**
  * Increased supported tokens count from 14,000+ to 17,000+ on the bridge dashboard.

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

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-01-05 16:11:01 +00:00
MananTank 805b838fd3 [MNY-346] Playground: Add CheckoutWidget iframe (#8591)
<!--

## 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 support for customizable payment methods in the `checkout` widget, allowing users to select between "crypto" and "card" options. It also refines the integration type handling and enhances the iframe functionality for the checkout experience.

### Detailed summary
- Added `integrationType` option to `BridgeComponentsPlaygroundOptions`.
- Introduced `paymentMethods` parsing in `page.tsx` for checkout widget.
- Updated `LeftSection` to conditionally render `ColorFormGroup`.
- Enhanced `CheckoutWidgetEmbed` to accept `paymentMethods`.
- Modified `RightSection` to support iframe rendering for checkout.
- Created `buildCheckoutIframeUrl` function to generate URLs with payment methods.
- Updated `CodeGen` to handle iframe code generation.
- Added documentation for payment methods in `iframe` page.
- Adjusted `CheckoutPlayground` to manage integration types and theme changes.

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

<!-- end pr-codex -->

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

* **New Features**
  * Checkout widget: optional paymentMethods filter (crypto or card) and iframe integration.
  * Playground: tab-based switcher (React / Iframe), URL state persistence, and theme-aware preview.
  * Preview/code generation: iframe-specific preview and HTML snippet export for iframe integration.

* **Documentation**
  * Added "Payment Methods" docs for iframe integration with examples.

<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-30 11:17:26 +00:00
Yash094 ec73f1ff83 X402 playground chainlist (#8588)
## [Playground] Fix: Exclude deprecated chains from x402 network selector

## Notes for the reviewer

This change enables the existing `disableDeprecated` functionality of the `SingleNetworkSelector` component on the x402 playground page. This ensures that deprecated chains are no longer displayed in the network selection dropdown, consistent with patterns used elsewhere in the codebase.

## How to test

1.  Navigate to `https://playground.thirdweb.com/x402`.
2.  Open the network selector dropdown.
3.  Verify that deprecated chains (e.g., Mumbai) are not listed.

---
[Slack Thread](https://thirdwebdev.slack.com/archives/C09DS2CKGP2/p1766589810150189?thread_ts=1766589810.150189&cid=C09DS2CKGP2)

<a href="https://cursor.com/background-agent?bcId=bc-662b659b-43e5-4760-8634-cf73b25f1a02"><picture><source media="(prefers-color-scheme: dark)" srcset="https://cursor.com/open-in-cursor-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://cursor.com/open-in-cursor-light.svg"><img alt="Open in Cursor" src="https://cursor.com/open-in-cursor.svg"></picture></a>&nbsp;<a href="https://cursor.com/agents?id=bc-662b659b-43e5-4760-8634-cf73b25f1a02"><picture><source media="(prefers-color-scheme: dark)" srcset="https://cursor.com/open-in-web-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://cursor.com/open-in-web-light.svg"><img alt="Open in Web" src="https://cursor.com/open-in-web.svg"></picture></a>

<!-- start pr-codex -->

---

## PR-Codex overview
This PR introduces a new property to a component, enhancing its functionality by allowing it to disable deprecated features.

### Detailed summary
- Added the `disableDeprecated` property to the `X402LeftSection` component.
- This change modifies the component's behavior, likely affecting how deprecated features are handled.

> ✨ 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**
  * Updated component configuration to disable deprecated options in the network selector.

<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-25 10:26:16 +00:00
saminacodes 245e9a688c delete outdated webhook docs and move webhooks to tokens (#8587)
<!--

## 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 the addition and enhancement of webhook documentation for both `Tokens` and `Bridge` functionalities in the application, while removing outdated files.

### Detailed summary
- Deleted files related to `webhooks` in `portal/src/app`.
- Added `webhooks` section in `tokens` documentation with use cases and configuration instructions.
- Updated `bridge` documentation to reflect the correct title and description.
- Enhanced explanations for creating webhooks in both `tokens` and `bridge` contexts.
- Included images for `TokenWebhooksScreen` and `BridgeWebhookScreen`.

> ✨ 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**
  * Reorganized webhooks docs into product-specific guides: Bridge (renamed from Payments Webhooks) and Tokens.
  * Added new Token Webhooks guide with use cases, setup steps, and an illustrative image.
  * Updated Bridge Webhooks content and dashboard guidance; added visuals and clarified structure and headings.
  * Removed the generic Webhooks overview in favor of the new guides.
  * Minor wording improvements in Wallets security docs.

<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-23 20:49:33 +00:00
saminacodes 8bdc0c0c27 Expand FAQ with Dedicated Relayer FAQs (#8585)
Added information about gas sponsorship and dedicated relayer features.

<!--

## 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 new content related to `Gas Sponsorship` in the `page.mdx` file, adding FAQs about the dedicated relayer service offered by thirdweb.

### Detailed summary
- Added a new section titled `Gas Sponsorship`.
- Included multiple FAQs regarding the dedicated relayer:
  - Its status as an add-on service.
  - Expected performance benchmarks.
  - Comparison to other alternatives.
  - Management of scaling on multiple relayer instances.
  - Transaction handling capacity.

> ✨ 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**
  * Expanded wallets FAQ with a new "Gas Sponsorship" section covering dedicated relayer plan/pricing, throughput benchmarks for single vs. multi-executor setups, comparisons with alternative queue approaches, scaling guidance for multiple relayer instances, and transaction-per-second expectations. Reordered content so Gas Sponsorship appears before Security & Recoverability; Ecosystem Wallets content retained.

<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-23 18:52:17 +00:00
MananTank 9c04b3f180 [MNY-338] Add filters when fetching chains in chain selection ui (#8573)
<!--

## 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 `SwapWidget` and related components by improving the chain selection logic and UI responsiveness, introducing a more reliable token selection process, and adjusting the timeout for modal unmounting.

### Detailed summary
- Updated `setTimeout` duration in `onModalUnmount` to 200ms.
- Changed `useBridgeChains` to `useBridgeChain` for better chain fetching.
- Added `type` and `selections` props to various components for handling buy/sell logic.
- Implemented a new `useBridgeChainsWithFilters` function for filtered chain queries.
- Enhanced UI components to accommodate new selection logic.
- Improved loading states and mobile responsiveness in `SelectBridgeChain` and `SelectToken`.

> ✨ 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 filtering now supports testnet and mutually exclusive origin/destination filters.
  - Token selection accepts operation context (buy/sell) and selected-chain hints.

* **Improvements**
  - Filter-driven chain lists yield more relevant token choices.
  - Loading skeletons adapt sizing for mobile and desktop.
  - Deferred buy-token assignment reduces redundant fetches during modal close.
  - Slightly longer modal unmount delay for smoother transitions.

<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-19 22:46:35 +00:00
MananTank 800503da68 [MNY-345] Portal: Add Checkout widget iframe docs (#8581)
<!--

## 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 `Checkout Widget` and `Swap Widget` components to enhance their functionality and improve user experience, including new image formats, additional features, and documentation updates.

### Detailed summary
- Replaced `.png` images with `.jpg` for `swap-dark` and `swap-light`.
- Added `IframeCodePreview` component for iframe integration in `Checkout Widget`.
- Updated `sendMessageToParent` function to use a single object parameter.
- Enhanced sidebar with new links for `Checkout Widget` and `Swap Widget`.
- Expanded documentation for `Checkout Widget` and `Swap Widget` with features, examples, and API references.

> ✨ 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 an iframe code-preview component with "Code" and "Preview" tabs for live iframe examples.

* **Documentation**
  * New Checkout Widget docs (React + iframe) with integration guides, examples, options, and event-listening examples.
  * New Swap Widget docs with usage examples and API reference.
  * Swap page asset updates (image format changes).

* **Improvements**
  * Reorganized sidebar to surface Widgets (Bridge, Checkout, Swap).
  * Updated widget postMessage event payload shape — consumers should verify event format.

<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-19 22:19:15 +00:00
MananTank 0ff8b9ff60 [MNY-316] SDK: directly show token selection after wallet connection in bridge widgets (#8580)
<!--

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

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

## Notes for the reviewer

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

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR modifies the behavior of the wallet connection process in the `WalletSwitcherConnectionScreen`. After a wallet is connected, it skips the "Choose Payment" screen and directly moves to the token selection screen, enhancing user experience in bridge widgets.

### Detailed summary
- Updated the `onConnect` function to remove the call to `props.onBack()`, preventing the return to the "Choose Payment" screen.
- Changed the flow to directly transition to the token selection screen after wallet connection.

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

<!-- end pr-codex -->

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

## Summary by CodeRabbit

* **Bug Fixes**
  * Improved wallet connection flow in bridge widgets. After connecting a wallet on the Choose Payment screen, users are now directed directly to token selection instead of returning to the payment screen.

<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-19 20:06:41 +00:00
MananTank 304e4e5be8 [MNY-348] SDK: Remove in-app wallet from default wallet list for bridge widgets (#8579)
<!--

## 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 wallet integration in bridge components by introducing custom wallet options and modifying default wallet selections for various widgets.

### Detailed summary
- Removed in-app wallet from default wallet list for bridge widgets.
- Added `CustomWallets` component in `BuyWidget.stories.tsx` and `SwapWidget.stories.tsx`.
- Implemented `getDefaultWalletsForBridgeComponents` to return a list of wallets.
- Updated `FundWallet`, `SwapUI`, and `TransactionPayment` components to use custom wallet options.
- Introduced new wallet options in `TransactionWidget.stories.tsx`.
- Added `CustomWallets` functionality in `BridgeWidget` stories.

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

<!-- end pr-codex -->
2025-12-19 19:29:48 +00:00
MananTank fdd2557663 [MNY-343] Add checkout widget iframe (#8578)
<!--

## 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 `CheckoutWidget` and related components in the `thirdweb` application, including the removal of duplicate fiat price displays, adding new environment constants, and refining query parameter parsing for improved functionality and usability.

### Detailed summary
- Removed duplicate fiat price display in `CheckoutWidget`.
- Added `NEXT_PUBLIC_CHECKOUT_IFRAME_CLIENT_ID` constant.
- Introduced `parseQueryParams` utility for better query handling.
- Updated `CheckoutWidgetEmbed` to utilize new constants and improved query parsing.
- Added validation for required parameters in the checkout page.
- Refactored currency validation to use `isValidCurrency`.
- Modified tests to skip certain cases based on environment variables.

> ✨ 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**
  * Embeddable checkout widget with a dedicated checkout page/layout and a new public env var for iframe client ID.
  * Content-Security-Policy headers applied to checkout widget routes.

* **Bug Fixes**
  * Removed duplicate fiat price display in the checkout button.

* **Chores / Refactor**
  * Centralized query-parameter parsing and currency validation; added input sanitization utilities.
  * Added a patch-level changeset entry.

* **Tests**
  * Marked several integration tests as skipped by default; enabled one parallel-execution test.

<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-19 19:29:48 +00:00
MananTank 56a1c0a467 [MNY-341] Payment Details UI improvements (#8576)
<!--

## 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 Payment Details UI for bridge widget components in the `thirdweb` package, enhancing layout, styling, and functionality.

### Detailed summary
- Adjusted `padding` in `TokenBalanceRow.tsx`.
- Replaced `OutlineWalletIcon` with a styled `Container` in `WalletRow.tsx`.
- Updated `borderRadius` and `borderBottom` styles in `PaymentOverview.tsx`.
- Enhanced `StepConnectorArrow` styles and added `iconSize` for `ChevronDownIcon`.
- Modified `OutlineWalletIcon` definition to accept `size`, `color`, and `style` props.
- Refactored `cryptoPaymentMethod` and `ethCryptoPaymentMethod` structures for clarity.
- Updated `PaymentDetails.tsx` styles, including `gap`, `borderRadius`, and button styles.

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

<!-- end pr-codex -->

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

## Summary by CodeRabbit

* **Bug Fixes**
  * Improved bridge payment details UI with enhanced spacing, borders, and styling
  * Updated wallet row display with better visual presentation when disconnected
  * Refined typography and color usage across payment interface components
  * Enhanced border radius and padding consistency throughout bridge widget

<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-19 12:57:51 +00:00
saminacodes 6e87b9205f added audits (#8577)
<!--

## 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 documentation for building and deploying tokens programmatically, generating bridge links, and auditing wallets. It enhances the user experience by providing clear instructions and examples for developers.

### Detailed summary
- Added a section titled `Build with tokens` in `page.mdx`.
- Introduced `Deploy Programmatically through API` section with details on ERC-20, ERC-721, and ERC-1155 tokens in `deploy-programmatically/page.mdx`.
- Included a guide on generating bridge links with steps in `link/page.mdx`.
- Added `Wallets Audits` section with links to audit reports in `security/page.mdx`.

> ✨ 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**
  * Added Bridge Link guide with step-by-step instructions for generating payment links and hosting them on websites
  * Added Build with Tokens guide
  * Added Deploying Tokens Programmatically guide with API examples for ERC-20, ERC-721, and ERC-1155 tokens
  * Added Wallet Security Audits section highlighting audit reports

<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-18 20:59:07 +00:00
MananTank 9383ccdf4e Portal: Add bridge widget playground links (#8575)
<!--

## 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 `Bridge Widget` to enhance its integration options and improve user experience by adding a live playground feature. It modifies the integration types, updates the code logic, and introduces new UI components for better functionality.

### Detailed summary
- Changed `href` in `sidebar.tsx` for the Playground link.
- Updated `integrationType` options in `types.ts` to include "react".
- Adjusted code language conditions in `code.tsx` based on `integrationType`.
- Added `PlayIcon` and `ArticleIconCard` components in multiple `.mdx` files.
- Introduced a "Try it out" section in several `.mdx` files for the Playground.
- Modified the `Page` component to accept `searchParams` and determine the valid tab.
- Enhanced `BridgeWidgetPlayground` to manage URL updates based on the selected integration type.

> ✨ 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**
  * Playground now supports URL parameters to pre-select integration tabs and keeps the browser URL in sync when switching tabs
  * React integration tab standardized and labeled consistently with other tabs

* **Documentation**
  * Added "Try it out" call-to-action cards throughout docs linking to the live playground

* **Chores**
  * Sidebar playground link updated to point to the bridge-specific playground URL

<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-18 18:08:11 +00:00
eabdelmoneim ac326e24f1 [Portal] Fix Transactions link in Archived Documentation section (#8574)
https://linear.app/thirdweb/issue/PRO-149/fix-archive-link-for-transactions

Update the Transactions link to point to /engine instead of /wallets/server/send-transactions.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

<!--

## 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 `href` property of the `ArticleCardIndex` component in the `page.tsx` file to redirect users to a new location.

### Detailed summary
- Changed the `href` property of the `ArticleCardIndex` from `"/wallets/server/send-transactions"` to `"/engine"`

> ✨ 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**
  * Updated the Transactions card navigation link destination in the Archive section.

<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-18 17:23:51 +00:00
FirekeeperandGitHub 7ebc32a0c4 Improve portfolio fetch concurrency and error handling (#8572) 2025-12-17 06:06:03 +07:00
FirekeeperandGitHub 5f0e25b5a8 Add wallet portfolio fetching and display to user table (#8570) 2025-12-17 05:31:39 +07:00
MananTank aad3e6477e [MNY-226] Add Bridge Widget playgrounds (script, iframe, react) (#8571)
<!--

## 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 `BridgeWidget` component within the playground by introducing new features, improving styling, and refining existing functionalities. It also includes updates to metadata and the handling of token selections and amounts.

### Detailed summary
- Removed `PayIcon` and replaced with `BringToFrontIcon`.
- Added `showThirdwebBranding` option to `UniversalBridgeEmbed`.
- Updated `BridgeWidget` to support token amounts and selections.
- Enhanced sorting of bridge chains in `chains.ts`.
- Improved styling for various components, including `FeatureCard` and `ChatPageContent`.
- Updated documentation for widget parameters.
- Refactored code for better readability and maintainability in multiple components.

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

<!-- end pr-codex -->

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

* **New Features**
  * Bridge Widget playground: interactive preview, code generation (script/component/iframe), theme, token/amount prefills, and preview modes.
  * Unified buy/swap experience under a single Bridge widget and a new branding toggle (hide/show branding).
  * Expanded connection options for wallet/connect flows.

* **Documentation**
  * Added docs for token amounts, branding, and iframe examples.

* **Style**
  * Minor typography and spacing refinements.

* **Chores**
  * Navigation and feature card updates; small test adjustment (one image test skipped).

<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-16 20:42:47 +00:00
MananTank 4bab6f1ae8 [PRO-125] Dashboard: Update token creation modal UI (#8561)
<!--

## 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 in the dashboard by improving dialog descriptions, layout adjustments, and introducing images for token cards, along with some styling updates.

### Detailed summary
- Added `DialogDescription` to provide context in the dialog.
- Increased `DialogContent` max width from `!max-w-md` to `!max-w-2xl`.
- Changed layout of `Cards` from a column to a grid.
- Introduced `images` for token cards with light and dark themes.
- Updated descriptions for token creation.
- Enhanced styling of `CardLink` components, including rounded corners.

> ✨ 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 theme-aware image rendering with light and dark mode support for card components.
  * Implemented responsive 2-column grid layout on larger screens.

* **Style**
  * Increased border radius on cards and dialog components for a more refined appearance.
  * Enhanced card title typography sizing.
  * Expanded dialog width for improved content display.
  * Improved responsive behavior with compact icons on smaller viewports.

<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-15 18:38:38 +00:00
MananTank f9a435051d [MNY-336] Portal: Fix spacing issue on menu with link (#8560)
<!--

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

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

## Notes for the reviewer

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

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on modifying the sidebar configuration and adjusting styles in the `Sidebar` component for better layout and design.

### Detailed summary
- In `apps/portal/src/app/bridge/sidebar.tsx`:
  - Added an entry for `name: "Guides"` in the sidebar configuration.
- In `apps/portal/src/components/others/Sidebar.tsx`:
  - Updated the `className` of a `div` to include `py-1.5` and `px-3`.
  - Modified the `className` of a `Link` to remove padding classes `py-1.5` and `px-3`.

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

<!-- end pr-codex -->

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

## Summary by CodeRabbit

* **Style**
  * Improved sidebar category spacing with enhanced padding distribution
  * Expanded interactive click target area for sidebar navigation items
  * Better visual organization of the Guides section

<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-15 17:55:21 +00:00
MananTank 27d72e5d3e [PRO-124] Dashboard: Project Wallet UI tweaks (#8559)
<!--

## 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 refining the user interface and functionality of the `ProjectWallet` component in the dashboard application. It includes style adjustments, component updates, and the introduction of new dropdown menu options for better user interaction.

### Detailed summary
- Adjusted the size of `ChevronDownIcon` in `select-with-search.tsx`.
- Removed `XIcon` from `project-wallet.tsx` and updated the heading style.
- Renamed `projectWallet` variables to `serverWallet` in `project-wallet.stories.tsx`.
- Introduced a new `Variant` function for rendering `ProjectWalletSectionUI`.
- Added new icons in `project-wallet-details.tsx` and refined the dropdown menu.
- Enhanced wallet detail presentation with better layout and added new dropdown options for actions like Deposit, Withdraw, and Change Project Wallet.

> ✨ 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 dropdown menu with wallet action options: Deposit Funds, Withdraw Funds, Swap Tokens, View Transactions, and Change Project Wallet.
  * Added copy-address button in the wallet header.

* **Style**
  * Redesigned wallet header layout with improved visual structure.
  * Refined icon sizing and spacing throughout the wallet interface.

<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-15 17:44:00 +00:00
MananTank 151127d668 [PRO-144] SDK: Update in-app wallet icon in wide connect ui (#8556)
<!--

## 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 in-app wallet feature by updating icons, adding new components for different authentication methods, and improving the UI for wallet connection options.

### Detailed summary
- Updated the in-app wallet icon in the wide connect UI.
- Changed `defaultAuthOptions` from a constant to an exported variable.
- Modified `EmailIcon` component for better styling and structure.
- Added new functions for various modal configurations (`ConfiguredInAppWalletWideModal`, `GoogleLoginWideModal`, `GithubLoginWideModal`).
- Introduced `InAppWalletIcon` component to handle multiple authentication methods visually.
- Created comprehensive storybook examples for `InAppWalletIcon` with various authentication options.

> ✨ 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 an in-app wallet icon that displays available authentication methods and adapts layout for single vs. multiple options.

* **UI/UX Improvements**
  * Updated wallet icon visuals and dynamic color handling for more consistent rendering in the connect modal.
  * Simplified email icon visuals for clearer strokes.

* **Stories**
  * Added Storybook examples showcasing the in-app wallet icon across themes and auth-option permutations.

<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-12 21:03:26 +00:00
MananTank 759f41b11e Dashboard: Update Bridge get started section (#8555)
<!--

## 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 `QuickstartSection.client.tsx` file to improve the presentation of the bridge widget feature. It modifies the `FeatureCard` components to enhance descriptions, titles, and links for better clarity and usability.

### Detailed summary
- Changed the title of the first `FeatureCard` from "Buy Widget Component" to "Bridge Widget".
- Updated the description of the first `FeatureCard` to "Integrate cross-chain swaps and fiat onramp into your app using bridge widget".
- Altered features in the first `FeatureCard` to include "Integrate with iframe, script, or React component".
- Updated the link URL in the first `FeatureCard` to point to the bridge widget documentation.
- Removed the "Customizable UI" feature from the first `FeatureCard`.

> ✨ 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 Widget quickstart section now supports integration via iframe, script tag, and React component.
  * Enhanced feature card descriptions for improved clarity in the onboarding flow.

<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-12 19:22:00 +00:00
MananTank 5ec84bd3b6 [MNY-314] SDK: Token Selection UX improvements in SwapWidget (#8552)
<!--

## 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 user experience in the `SwapWidget` by automatically selecting the native token when no sell or buy token is selected.

### Detailed summary
- Added logic to set the `sellToken` as the native token of the buy token's chain if no sell token is selected.
- Added logic to set the `buyToken` as the native token of the sell token's chain if no buy token is selected.

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

<!-- end pr-codex -->
2025-12-12 18:49:07 +00:00
MananTank 38f4a89076 [MNY-334] SDK: Fix Spinner animation missing sometimes in Next.js (#8553)
<!--

## 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 addresses an issue with the `Spinner` component's animation in a Next.js environment, ensuring that the animation works consistently.

### Detailed summary
- Updated the `Spinner` component to use a `<circle>` element instead of a `Circle` styled component.
- Removed the `StyledCircle` import from `elements.js`.
- Added animation styles directly to the `<circle>` element.
- Defined the `tw-spinner-circle-dash` keyframe animation within the component.

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

<!-- end pr-codex -->
2025-12-12 18:35:49 +00:00
MananTank 31e24f1c58 [MNY-331] SDK: Alphabetically sort chains in SwapWidget UI (#8551)
<!--

## 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 `SwapWidget` UI by alphabetically sorting the chains returned by the `use-bridge-chains.ts` module, ensuring that chains with names starting with numbers are placed at the end of the list.

### Detailed summary
- Updated the `queryFn` in `use-bridge-chains.ts` to be asynchronous.
- Introduced a copy of the fetched data to sort it.
- Implemented sorting logic to place chains starting with numbers at the end.
- Used `localeCompare` for alphabetical sorting of chain names.

> ✨ 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**
  * Chains in the SwapWidget are now alphabetically sorted for improved navigation; chain names beginning with digits appear after alphabetic entries.

* **Chores**
  * Patch release entry added to the changelog.

<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-12 18:35:49 +00:00
MananTank 2bb6a4f9a3 [MNY-332] Dashboard: Checkout link error message improvements (#8550)
<!--

## 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 error handling in the `billing.ts` files by checking for a specific error structure in the JSON response. If the error is present and formatted correctly, it returns a detailed error message instead of a generic one.

### Detailed summary
- Added error handling for JSON responses in `apps/dashboard/src/@/actions/billing.ts`:
  - Checks if `error` and `message` exist in the response.
  - Validates that `json.error.message` is a string.
  - Returns a structured error object with the message and status.

- Similar changes were made in `apps/dashboard/src/app/(app)/(stripe)/utils/billing.ts`.

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

<!-- end pr-codex -->

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

## Summary by CodeRabbit

## Bug Fixes
* Improved error message handling for billing operations to display API-provided error messages to users instead of generic fallback errors, enabling clearer feedback when checkout transactions encounter issues.

<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-12 17:56:56 +00:00
eabdelmoneim e281ed185e [Playground] Fix bridge/payments documentation links (#8549)
https://linear.app/thirdweb/issue/PRO-148/fix-playground-doc-links-for-bridge-components

Update documentation links for bridge/payments widgets to follow consistent naming convention:
- Buy Widget: wallets/sponsor-gas → references/typescript/v5/BuyWidget
- Checkout Widget: payments → references/typescript/v5/CheckoutWidget
- Transaction Widget: wallets/sponsor-gas → references/typescript/v5/TransactionWidget
- Transaction Button: Add missing utm_source=playground parameter

All links now point to /references/typescript/v5/{WidgetName} with utm_source tracking.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

<!--

## 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 `docsLink` properties in several components to point to the correct documentation pages for various widgets in the `playground-web` application.

### Detailed summary
- Updated `docsLink` in `apps/playground-web/src/app/bridge/buy-widget/page.tsx` to point to `BuyWidget`.
- Updated `docsLink` in `apps/playground-web/src/app/bridge/checkout-widget/page.tsx` to point to `CheckoutWidget`.
- Updated `docsLink` in `apps/playground-web/src/app/bridge/transaction-button/page.tsx` to include `?utm_source=playground`.
- Updated `docsLink` in `apps/playground-web/src/app/bridge/transaction-widget/page.tsx` to point to `TransactionWidget`.

> ✨ 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 documentation links across playground widgets to reference current TypeScript v5 documentation resources.

<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-12 17:52:12 +00:00
MananTank 116975286f [MNY-327] SDK: Display error returned from API in login with phone number UI (#8548)
<!--

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

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

## Notes for the reviewer

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

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on improving error handling and state management in the OTP authentication process. It enhances the way error messages are displayed and modifies the structure of `AccountStatus` to accommodate additional information.

### Detailed summary
- In `otp.ts`, enhanced error handling by parsing the response for a detailed message.
- Updated `AccountStatus` type in `OTPLoginUI.tsx` to an object structure, allowing for an error message.
- Modified state management for `accountStatus` to use the new object structure.
- Adjusted UI rendering logic to handle the new `AccountStatus` 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

* **Bug Fixes**
  * Enhanced error message handling in OTP verification to display detailed server error messages to users instead of generic messages.
  * Improved error status display in OTP login interface.

* **Style**
  * Updated OTP login UI layout and spacing for better status presentation.

<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-12 17:08:22 +00:00
FirekeeperandGitHub ff8f4130e9 Add Dedicated Relayer docs and sidebar link (#8546) 2025-12-12 23:54:19 +07:00
MananTank ffd8c402f4 Dashboard: Fix webhooks link in project/contracts page (#8547)
<!--

## 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 URL structure for webhooks in the `page.tsx` file, changing the path from `/webhooks/contracts` to `/tokens/webhooks`.

### Detailed summary
- Modified the `href` property in the object from:
  - `href: `/team/${params.team_slug}/${params.project_slug}/webhooks/contracts``
  - to `href: `/team/${params.team_slug}/${params.project_slug}/tokens/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

* **Bug Fixes**
  * Fixed the "webhooks" navigation link in the Contracts page to correctly redirect to the tokens section instead of the contracts section.

<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-12 16:32:05 +00:00
MananTank d365efe997 Update Next.js version (#8545)
<!--

## 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 updates various dependencies across multiple `package.json` files, particularly upgrading the `next` version from `15.3.6` to `15.3.8` and `@next/eslint-plugin-next` from `15.3.6` to `15.3.8`. It also modifies the `pnpm-lock.yaml` to reflect these updates.

### Detailed summary
- Updated `next` from `15.3.6` to `15.3.8` in:
  - `packages/ui/package.json`
  - `apps/wallet-ui/package.json`
  - `apps/portal/package.json`
  - `apps/playground-web/package.json`
  - `apps/nebula/package.json`
  - `apps/dashboard/package.json`
- Updated `@next/eslint-plugin-next` from `15.3.6` to `15.3.8` in:
  - `apps/wallet-ui/package.json`
  - `apps/nebula/package.json`
  - `apps/dashboard/package.json`
- Updated `@next/mdx` from `15.3.6` to `15.3.8` in `apps/portal/package.json`.
- Adjusted `pnpm-lock.yaml` to reflect the above dependency updates.

> 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

* **Chores**
  * Bumped Next.js and related Next packages (minor/patch updates) across multiple apps and packages for stability and consistency.
  * Normalized package manifest formatting across projects; no functional or public API changes introduced.

<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-12 11:06:15 +00:00
FirekeeperandGitHub 1558b48056 relayer prod (#8535) 2025-12-12 06:14:14 +07:00
FirekeeperandGitHub 3fba7e4e88 relayer updates (#8541) 2025-12-12 05:41:59 +07:00
Joaquim VergesGitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
57ccadbef2 Version Packages (#8539)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-12-11 02:52:55 -08:00
Joaquim VergesandGitHub 20e41ee164 [SDK] Fix null options handling in useFetchWithPayment hook (#8540) 2025-12-11 02:47:50 -08:00
Joaquim VergesandGitHub f5ab57f365 [SDK] Cache and reuse x402 permit signatures for upto schemes (#8538) 2025-12-11 02:40:55 -08:00
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
643 changed files with 26745 additions and 9968 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",
];
+77
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,78 @@ 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*",
},
{
headers: [
{
key: "Content-Security-Policy",
value: EmbedContentSecurityPolicy.replace(/\s{2,}/g, " ").trim(),
},
],
source: "/bridge/checkout-widget",
},
{
headers: [
{
key: "Content-Security-Policy",
value: EmbedContentSecurityPolicy.replace(/\s{2,}/g, " ").trim(),
},
],
source: "/bridge/checkout-widget/:path*",
},
{
headers: [
{
key: "Content-Security-Policy",
value: EmbedContentSecurityPolicy.replace(/\s{2,}/g, " ").trim(),
},
],
source: "/bridge/swap-widget",
},
{
headers: [
{
key: "Content-Security-Policy",
value: EmbedContentSecurityPolicy.replace(/\s{2,}/g, " ").trim(),
},
],
source: "/bridge/swap-widget/:path*",
},
{
headers: [
{
key: "Content-Security-Policy",
value: EmbedContentSecurityPolicy.replace(/\s{2,}/g, " ").trim(),
},
],
source: "/bridge/buy-widget",
},
{
headers: [
{
key: "Content-Security-Policy",
value: EmbedContentSecurityPolicy.replace(/\s{2,}/g, " ").trim(),
},
],
source: "/bridge/buy-widget/:path*",
},
];
},
images: {
+7 -7
View File
@@ -44,7 +44,7 @@
"ioredis": "^5.6.1",
"ipaddr.js": "^2.2.0",
"lucide-react": "0.525.0",
"next": "15.3.5",
"next": "15.3.8",
"next-themes": "^0.4.6",
"nextjs-toploader": "^1.6.12",
"nuqs": "^2.4.3",
@@ -53,10 +53,10 @@
"pluralize": "^8.0.0",
"posthog-js": "1.256.1",
"prettier": "3.6.2",
"react": "19.1.0",
"react": "19.2.1",
"react-children-utilities": "^2.10.0",
"react-day-picker": "^8.10.1",
"react-dom": "19.1.0",
"react-dom": "19.2.1",
"react-dropzone": "^14.3.8",
"react-error-boundary": "6.0.0",
"react-hook-form": "7.55.0",
@@ -82,8 +82,8 @@
"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",
"@next/bundle-analyzer": "15.3.8",
"@next/eslint-plugin-next": "15.3.8",
"@storybook/addon-docs": "9.0.15",
"@storybook/addon-links": "9.0.15",
"@storybook/addon-onboarding": "9.0.15",
@@ -92,8 +92,8 @@
"@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": "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",
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

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

+12
View File
@@ -109,6 +109,18 @@ export async function getChainInfraCheckoutURL(options: {
}
const json = await res.json();
if (
"error" in json &&
"message" in json.error &&
typeof json.error.message === "string"
) {
return {
error: json.error.message,
status: "error",
} as const;
}
if (!json.result) {
return {
error: "An unknown error occurred, please try again later.",
+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);
}
+110 -45
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(
@@ -691,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 },
@@ -105,6 +105,7 @@ type PartnerPermission = "PROMPT_USER_V1" | "FULL_CONTROL_V1";
export type Partner = {
id: string;
name: string;
imageUrl?: string;
allowlistedDomains: string[];
allowlistedBundleIds: string[];
permissions: [PartnerPermission];
+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();
@@ -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>
);
}
@@ -6,13 +6,16 @@ export const StatCard: React.FC<{
icon: React.FC<{ className?: string }>;
formatter?: (value: number) => string;
isPending: boolean;
}> = ({ label, value, formatter, icon: Icon, isPending }) => {
emptyText?: string;
}> = ({ label, value, formatter, icon: Icon, isPending, emptyText }) => {
return (
<dl className="flex items-center justify-between gap-4 rounded-lg border border-border bg-card p-4 pr-6">
<div>
<dd className="mb-0.5 font-semibold text-2xl tracking-tight">
{isPending ? (
<Skeleton className="h-8 w-20" />
) : emptyText ? (
<span className="text-muted-foreground">{emptyText}</span>
) : value !== undefined && formatter ? (
formatter(value)
) : (
@@ -2,9 +2,8 @@
"use client";
import { useTheme } from "next-themes";
import { useEffect, useMemo, useRef, useState } from "react";
import { defineChain } from "thirdweb";
import { BuyWidget, SwapWidget } from "thirdweb/react";
import { useEffect, useMemo, useRef } from "react";
import { BridgeWidget, type SupportedFiatCurrency } from "thirdweb/react";
import type { Wallet } from "thirdweb/wallets";
import {
reportAssetBuyCancelled,
@@ -18,21 +17,21 @@ import {
reportTokenSwapFailed,
reportTokenSwapSuccessful,
} from "@/analytics/report";
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";
import { cn } from "@/lib/utils";
import { parseError } from "@/utils/errorParser";
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,11 +63,12 @@ export type BuyAndSwapEmbedProps = {
| undefined;
pageType: PageType;
wallets?: Wallet[];
currency?: SupportedFiatCurrency;
showThirdwebBranding?: boolean;
};
export function BuyAndSwapEmbed(props: BuyAndSwapEmbedProps) {
const { theme } = useTheme();
const [tab, setTab] = useState<"buy" | "swap">("swap");
const themeObj = getSDKTheme(theme === "light" ? "light" : "dark");
const isMounted = useRef(false);
@@ -92,215 +92,170 @@ 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,
});
}, [props.pageType]);
return (
<div className="bg-card rounded-2xl border overflow-hidden flex flex-col relative z-10 shadow-xl min-w-0">
<div className="flex gap-2.5 p-4 border-b border-dashed">
<TabButton
label="Swap"
onClick={() => setTab("swap")}
isActive={tab === "swap"}
/>
<TabButton
label="Buy"
onClick={() => setTab("buy")}
isActive={tab === "buy"}
/>
</div>
<BridgeWidget
client={client}
theme={themeObj}
className="z-10 min-w-0 shadow-xl"
connectOptions={{
autoConnect: false,
wallets: props.wallets,
appMetadata: appMetadata,
}}
buy={
props.buyTab
? {
amount: props.buyTab.buyToken?.amount,
chainId: props.buyTab.buyToken?.chainId,
tokenAddress: props.buyTab.buyToken?.tokenAddress,
onError: (e, quote) => {
const errorMessage = parseError(e);
{tab === "buy" && (
<BuyWidget
amount={props.buyTab?.buyToken?.amount || "1"}
chain={
props.buyTab?.buyToken?.chainId
? defineChain(props.buyTab.buyToken.chainId)
: undefined
}
tokenAddress={
props.buyTab?.buyToken?.tokenAddress as `0x${string}` | undefined
}
className="!rounded-2xl !border-none"
title=""
client={client}
connectOptions={{
autoConnect: false,
wallets: props.wallets,
appMetadata: appMetadata,
}}
onError={(e, quote) => {
const errorMessage = parseError(e);
const buyChainId =
quote?.type === "buy"
? quote.intent.destinationChainId
: quote?.type === "onramp"
? quote.intent.chainId
: undefined;
const buyChainId =
quote?.type === "buy"
? quote.intent.destinationChainId
: quote?.type === "onramp"
? quote.intent.chainId
: undefined;
if (!buyChainId) {
return;
}
if (!buyChainId) {
return;
reportTokenBuyFailed({
buyTokenChainId: buyChainId,
buyTokenAddress:
quote?.type === "buy"
? quote.intent.destinationTokenAddress
: quote?.type === "onramp"
? quote.intent.tokenAddress
: undefined,
pageType: props.pageType,
});
if (props.pageType === "asset") {
reportAssetBuyFailed({
assetType: "coin",
chainId: buyChainId,
error: errorMessage,
contractType: undefined,
is_testnet: false,
});
}
},
onCancel: (quote) => {
const buyChainId =
quote?.type === "buy"
? quote.intent.destinationChainId
: quote?.type === "onramp"
? quote.intent.chainId
: undefined;
if (!buyChainId) {
return;
}
reportTokenBuyCancelled({
buyTokenChainId: buyChainId,
buyTokenAddress:
quote?.type === "buy"
? quote.intent.destinationTokenAddress
: quote?.type === "onramp"
? quote.intent.tokenAddress
: undefined,
pageType: props.pageType,
});
if (props.pageType === "asset") {
reportAssetBuyCancelled({
assetType: "coin",
chainId: buyChainId,
contractType: undefined,
is_testnet: false,
});
}
},
onSuccess: ({ quote }) => {
const buyChainId =
quote?.type === "buy"
? quote.intent.destinationChainId
: quote?.type === "onramp"
? quote.intent.chainId
: undefined;
if (!buyChainId) {
return;
}
reportTokenBuySuccessful({
buyTokenChainId: buyChainId,
buyTokenAddress:
quote?.type === "buy"
? quote.intent.destinationTokenAddress
: quote?.type === "onramp"
? quote.intent.tokenAddress
: undefined,
pageType: props.pageType,
});
if (props.pageType === "asset") {
reportAssetBuySuccessful({
assetType: "coin",
chainId: buyChainId,
contractType: undefined,
is_testnet: false,
});
}
},
}
reportTokenBuyFailed({
buyTokenChainId: buyChainId,
buyTokenAddress:
quote?.type === "buy"
? quote.intent.destinationTokenAddress
: quote?.type === "onramp"
? quote.intent.tokenAddress
: undefined,
pageType: props.pageType,
});
if (props.pageType === "asset") {
reportAssetBuyFailed({
assetType: "coin",
chainId: buyChainId,
error: errorMessage,
contractType: undefined,
is_testnet: false,
});
}
}}
onCancel={(quote) => {
const buyChainId =
quote?.type === "buy"
? quote.intent.destinationChainId
: quote?.type === "onramp"
? quote.intent.chainId
: undefined;
if (!buyChainId) {
return;
}
reportTokenBuyCancelled({
buyTokenChainId: buyChainId,
buyTokenAddress:
quote?.type === "buy"
? quote.intent.destinationTokenAddress
: quote?.type === "onramp"
? quote.intent.tokenAddress
: undefined,
pageType: props.pageType,
});
if (props.pageType === "asset") {
reportAssetBuyCancelled({
assetType: "coin",
chainId: buyChainId,
contractType: undefined,
is_testnet: false,
});
}
}}
onSuccess={({ quote }) => {
const buyChainId =
quote?.type === "buy"
? quote.intent.destinationChainId
: quote?.type === "onramp"
? quote.intent.chainId
: undefined;
if (!buyChainId) {
return;
}
reportTokenBuySuccessful({
buyTokenChainId: buyChainId,
buyTokenAddress:
quote?.type === "buy"
? quote.intent.destinationTokenAddress
: quote?.type === "onramp"
? quote.intent.tokenAddress
: undefined,
pageType: props.pageType,
});
if (props.pageType === "asset") {
reportAssetBuySuccessful({
assetType: "coin",
chainId: buyChainId,
contractType: undefined,
is_testnet: false,
});
}
}}
theme={themeObj}
paymentMethods={["card"]}
/>
)}
{tab === "swap" && (
<SwapWidget
client={client}
theme={themeObj}
className="!rounded-2xl !border-none"
connectOptions={{
autoConnect: false,
wallets: props.wallets,
appMetadata: appMetadata,
}}
prefill={{
buyToken: props.swapTab?.buyToken,
sellToken: props.swapTab?.sellToken,
}}
onError={(error, quote) => {
const errorMessage = parseError(error);
reportTokenSwapFailed({
errorMessage: errorMessage,
buyTokenChainId: quote.intent.destinationChainId,
buyTokenAddress: quote.intent.destinationTokenAddress,
sellTokenChainId: quote.intent.originChainId,
sellTokenAddress: quote.intent.originTokenAddress,
pageType: props.pageType,
});
}}
onSuccess={({ quote }) => {
reportTokenSwapSuccessful({
buyTokenChainId: quote.intent.destinationChainId,
buyTokenAddress: quote.intent.destinationTokenAddress,
sellTokenChainId: quote.intent.originChainId,
sellTokenAddress: quote.intent.originTokenAddress,
pageType: props.pageType,
});
}}
onCancel={(quote) => {
reportTokenSwapCancelled({
buyTokenChainId: quote.intent.destinationChainId,
buyTokenAddress: quote.intent.destinationTokenAddress,
sellTokenChainId: quote.intent.originChainId,
sellTokenAddress: quote.intent.originTokenAddress,
pageType: props.pageType,
});
}}
/>
)}
</div>
);
}
function TabButton(props: {
label: string;
onClick: () => void;
isActive: boolean;
}) {
return (
<Button
onClick={props.onClick}
size="sm"
className={cn(
"rounded-full text-muted-foreground px-4 bg-accent",
props.isActive && "text-foreground border-foreground/50",
)}
variant="outline"
>
{props.label}
</Button>
: undefined
}
swap={{
persistTokenSelections: props.persistTokenSelections,
prefill: {
buyToken: props.swapTab?.buyToken,
sellToken: props.swapTab?.sellToken,
},
onError: (error, quote) => {
const errorMessage = parseError(error);
reportTokenSwapFailed({
errorMessage: errorMessage,
buyTokenChainId: quote.intent.destinationChainId,
buyTokenAddress: quote.intent.destinationTokenAddress,
sellTokenChainId: quote.intent.originChainId,
sellTokenAddress: quote.intent.originTokenAddress,
pageType: props.pageType,
});
},
onSuccess: ({ quote }) => {
reportTokenSwapSuccessful({
buyTokenChainId: quote.intent.destinationChainId,
buyTokenAddress: quote.intent.destinationTokenAddress,
sellTokenChainId: quote.intent.originChainId,
sellTokenAddress: quote.intent.originTokenAddress,
pageType: props.pageType,
});
},
onCancel: (quote) => {
reportTokenSwapCancelled({
buyTokenChainId: quote.intent.destinationChainId,
buyTokenAddress: quote.intent.destinationTokenAddress,
sellTokenChainId: quote.intent.originChainId,
sellTokenAddress: quote.intent.originTokenAddress,
pageType: props.pageType,
});
},
}}
currency={props.currency}
showThirdwebBranding={props.showThirdwebBranding}
/>
);
}
@@ -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",
});
@@ -125,7 +125,7 @@ export const SelectWithSearch = React.forwardRef<
? renderOption(selectedOption)
: selectedOption?.label || placeholder}
</span>
<ChevronDownIcon className="size-4 cursor-pointer text-muted-foreground" />
<ChevronDownIcon className="size-3.5 cursor-pointer text-muted-foreground shrink-0" />
</div>
</Button>
</PopoverTrigger>
@@ -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">
@@ -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"
>
@@ -1,371 +0,0 @@
"use client";
import { createColumnHelper } from "@tanstack/react-table";
import { format } from "date-fns";
import { ArrowLeftIcon, ArrowRightIcon, UserIcon } from "lucide-react";
import Papa from "papaparse";
import { useCallback, useMemo, useState } from "react";
import type { ThirdwebClient } from "thirdweb";
import type { WalletUser } from "thirdweb/wallets";
import { TWTable } from "@/components/blocks/TWTable";
import { WalletAddress } from "@/components/blocks/wallet-address";
import { Button } from "@/components/ui/button";
import { Spinner } from "@/components/ui/Spinner";
import {
ToolTipLabel,
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import {
useAllEmbeddedWallets,
useEmbeddedWallets,
} from "@/hooks/useEmbeddedWallets";
import { CopyTextButton } from "../ui/CopyTextButton";
import { AdvancedSearchInput } from "./AdvancedSearchInput";
import { SearchResults } from "./SearchResults";
import { searchUsers } from "./searchUsers";
import type { SearchType } from "./types";
const getAuthIdentifier = (accounts: WalletUser["linkedAccounts"]) => {
const mainDetail = accounts[0]?.details;
return (
mainDetail?.id ??
mainDetail?.email ??
mainDetail?.phone ??
mainDetail?.address
);
};
const getPrimaryEmail = (accounts: WalletUser["linkedAccounts"]) => {
const emailFromPrimary = accounts[0]?.details?.email;
if (emailFromPrimary) {
return emailFromPrimary;
}
const emailAccount = accounts.find((account) => {
return typeof account.details?.email === "string" && account.details.email;
});
if (emailAccount && typeof emailAccount.details.email === "string") {
return emailAccount.details.email;
}
return undefined;
};
const columnHelper = createColumnHelper<WalletUser>();
const truncateIdentifier = (value: string) => {
if (value.length <= 18) {
return value;
}
return `${value.slice(0, 8)}...${value.slice(-4)}`;
};
export function InAppWalletUsersPageContent(
props: {
authToken: string;
client: ThirdwebClient;
teamId: string;
} & (
| { projectClientId: string; ecosystemSlug?: never }
| { ecosystemSlug: string; projectClientId?: never }
),
) {
const columns = useMemo(() => {
return [
columnHelper.accessor("id", {
cell: (cell) => {
const userId = cell.getValue();
if (!userId) {
return "N/A";
}
return (
<CopyTextButton
textToShow={truncateIdentifier(userId)}
textToCopy={userId}
tooltip="Copy User Identifier"
copyIconPosition="left"
variant="ghost"
/>
);
},
header: "User Identifier",
id: "user_identifier",
}),
columnHelper.accessor("linkedAccounts", {
cell: (cell) => {
const identifier = getAuthIdentifier(cell.getValue());
if (!identifier) {
return "N/A";
}
return (
<CopyTextButton
textToShow={truncateIdentifier(identifier)}
textToCopy={identifier}
tooltip="Copy Auth Identifier"
copyIconPosition="left"
variant="ghost"
/>
);
},
enableColumnFilter: true,
header: "Auth Identifier",
id: "auth_identifier",
}),
columnHelper.accessor("wallets", {
cell: (cell) => {
const address = cell.getValue()[0]?.address;
return address ? (
<WalletAddress address={address} client={props.client} />
) : null;
},
header: "Address",
id: "address",
}),
columnHelper.accessor("linkedAccounts", {
cell: (cell) => {
const email = getPrimaryEmail(cell.getValue());
if (!email) {
return <span className="text-muted-foreground text-sm">N/A</span>;
}
return (
<CopyTextButton
textToShow={email}
textToCopy={email}
tooltip="Copy Email"
copyIconPosition="left"
variant="ghost"
/>
);
},
header: "Email",
id: "email",
}),
columnHelper.accessor("wallets", {
cell: (cell) => {
const value = cell.getValue()[0]?.createdAt;
if (!value) {
return;
}
return (
<ToolTipLabel
label={format(new Date(value), "MMM dd, yyyy 'at' h:mm:ss a zzz")}
hoverable
>
<span className="text-sm">
{format(new Date(value), "MMM dd, yyyy")}
</span>
</ToolTipLabel>
);
},
header: "Created",
id: "created_at",
}),
columnHelper.accessor("linkedAccounts", {
cell: (cell) => {
const value = cell.getValue();
const loginMethodsDisplay = value.reduce((acc, curr) => {
if (acc.length === 2) {
acc.push("...");
}
if (acc.length < 2) {
acc.push(curr.type);
}
return acc;
}, [] as string[]);
const loginMethods = value.map((v) => v.type).join(", ");
return (
<TooltipProvider>
<Tooltip>
<TooltipTrigger className="text-sm">
{loginMethodsDisplay.join(", ")}
</TooltipTrigger>
<TooltipContent>
<span className="text-sm">{loginMethods}</span>
</TooltipContent>
</Tooltip>
</TooltipProvider>
);
},
header: "Login Methods",
id: "login_methods",
}),
];
}, [props.client]);
const [activePage, setActivePage] = useState(1);
const [searchResults, setSearchResults] = useState<WalletUser[]>([]);
const [isSearching, setIsSearching] = useState(false);
const [hasSearchResults, setHasSearchResults] = useState(false);
const walletsQuery = useEmbeddedWallets({
authToken: props.authToken,
clientId: props.projectClientId,
ecosystemSlug: props.ecosystemSlug,
teamId: props.teamId,
page: activePage,
});
const wallets = walletsQuery?.data?.users || [];
const { mutateAsync: getAllEmbeddedWallets, isPending } =
useAllEmbeddedWallets({
authToken: props.authToken,
});
const handleSearch = async (searchType: SearchType, query: string) => {
setIsSearching(true);
try {
const results = await searchUsers(
props.authToken,
props.projectClientId,
props.ecosystemSlug,
props.teamId,
searchType,
query,
);
setSearchResults(results);
setHasSearchResults(true);
} catch (error) {
console.error("Search failed:", error);
setSearchResults([]);
setHasSearchResults(true);
} finally {
setIsSearching(false);
}
};
const handleClearSearch = () => {
setSearchResults([]);
setHasSearchResults(false);
};
const downloadCSV = useCallback(async () => {
if (wallets.length === 0 || !getAllEmbeddedWallets) {
return;
}
const usersWallets = await getAllEmbeddedWallets({
clientId: props.projectClientId,
ecosystemSlug: props.ecosystemSlug,
teamId: props.teamId,
});
const csv = Papa.unparse(
usersWallets.map((row) => {
const email = getPrimaryEmail(row.linkedAccounts);
return {
address: row.wallets[0]?.address || "Uninitialized",
created: row.wallets[0]?.createdAt
? new Date(row.wallets[0].createdAt).toISOString()
: "Wallet not created yet",
email: email || "N/A",
login_methods: row.linkedAccounts.map((acc) => acc.type).join(", "),
auth_identifier: getAuthIdentifier(row.linkedAccounts) || "N/A",
user_identifier: row.id || "N/A",
};
}),
);
const csvUrl = URL.createObjectURL(
new Blob([csv], { type: "text/csv;charset=utf-8;" }),
);
const tempLink = document.createElement("a");
tempLink.href = csvUrl;
tempLink.setAttribute("download", "download.csv");
tempLink.click();
}, [
wallets,
props.projectClientId,
getAllEmbeddedWallets,
props.teamId,
props.ecosystemSlug,
]);
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>
{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>
</div>
);
}
@@ -0,0 +1,612 @@
"use client";
import { createColumnHelper } from "@tanstack/react-table";
import { format } from "date-fns";
import {
ArrowLeftIcon,
ArrowRightIcon,
DollarSignIcon,
RefreshCwIcon,
UserIcon,
WalletIcon,
} from "lucide-react";
import Papa from "papaparse";
import { useCallback, useMemo, useState } from "react";
import type { ThirdwebClient } from "thirdweb";
import type { WalletUser } from "thirdweb/wallets";
import { StatCard } from "@/components/analytics/stat";
import { MultiNetworkSelector } from "@/components/blocks/NetworkSelectors";
import { TWTable } from "@/components/blocks/TWTable";
import { WalletAddress } from "@/components/blocks/wallet-address";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Progress } from "@/components/ui/progress";
import { Spinner } from "@/components/ui/Spinner";
import {
ToolTipLabel,
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import {
useAllEmbeddedWallets,
useEmbeddedWallets,
} from "@/hooks/useEmbeddedWallets";
import {
useFetchAllPortfolios,
type WalletPortfolioData,
} from "@/hooks/useWalletPortfolio";
import { CopyTextButton } from "../ui/CopyTextButton";
import { AdvancedSearchInput } from "./AdvancedSearchInput";
import { SearchResults } from "./SearchResults";
import { searchUsers } from "./searchUsers";
import type { SearchType } from "./types";
const getAuthIdentifier = (accounts: WalletUser["linkedAccounts"]) => {
const mainDetail = accounts[0]?.details;
return (
mainDetail?.id ??
mainDetail?.email ??
mainDetail?.phone ??
mainDetail?.address
);
};
const getPrimaryEmail = (accounts: WalletUser["linkedAccounts"]) => {
const emailFromPrimary = accounts[0]?.details?.email;
if (emailFromPrimary) {
return emailFromPrimary;
}
const emailAccount = accounts.find((account) => {
return typeof account.details?.email === "string" && account.details.email;
});
if (emailAccount && typeof emailAccount.details.email === "string") {
return emailAccount.details.email;
}
return undefined;
};
const columnHelper = createColumnHelper<WalletUser>();
const truncateIdentifier = (value: string) => {
if (value.length <= 18) {
return value;
}
return `${value.slice(0, 8)}...${value.slice(-4)}`;
};
export function UserWalletsTable(
props: {
authToken: string;
client: ThirdwebClient;
teamId: string;
} & (
| { projectClientId: string; ecosystemSlug?: never }
| { ecosystemSlug: string; projectClientId?: never }
),
) {
const [activePage, setActivePage] = useState(1);
const [searchResults, setSearchResults] = useState<WalletUser[]>([]);
const [isSearching, setIsSearching] = useState(false);
const [hasSearchResults, setHasSearchResults] = useState(false);
// Portfolio state
const [selectedChains, setSelectedChains] = useState<number[]>([1]); // Default to Ethereum
const [portfolioMap, setPortfolioMap] = useState<
Map<string, WalletPortfolioData>
>(new Map());
const [portfolioLoaded, setPortfolioLoaded] = useState(false);
const [fetchProgress, setFetchProgress] = useState({
completed: 0,
total: 0,
});
const walletsQuery = useEmbeddedWallets({
authToken: props.authToken,
clientId: props.projectClientId,
ecosystemSlug: props.ecosystemSlug,
teamId: props.teamId,
page: activePage,
});
const wallets = walletsQuery?.data?.users || [];
const { mutateAsync: getAllEmbeddedWallets, isPending: isLoadingAllWallets } =
useAllEmbeddedWallets({
authToken: props.authToken,
});
const fetchPortfoliosMutation = useFetchAllPortfolios();
const handleFetchBalances = useCallback(async () => {
if (selectedChains.length === 0) return;
try {
// First get all wallets
const allWallets = await getAllEmbeddedWallets({
clientId: props.projectClientId,
ecosystemSlug: props.ecosystemSlug,
teamId: props.teamId,
});
const allAddresses = allWallets
.map((w) => w.wallets[0]?.address)
.filter((a): a is string => !!a);
if (allAddresses.length === 0) {
setPortfolioLoaded(true);
return;
}
setFetchProgress({ completed: 0, total: allAddresses.length });
const results = await fetchPortfoliosMutation.mutateAsync({
addresses: allAddresses,
chainIds: selectedChains,
authToken: props.authToken,
teamId: props.teamId,
clientId: props.projectClientId,
ecosystemSlug: props.ecosystemSlug,
onProgress: (completed, total) => {
setFetchProgress({ completed, total });
},
});
setPortfolioMap(results);
setPortfolioLoaded(true);
} catch (error) {
console.error("Failed to fetch balances:", error);
}
}, [
selectedChains,
getAllEmbeddedWallets,
props.projectClientId,
props.ecosystemSlug,
props.teamId,
props.authToken,
fetchPortfoliosMutation,
]);
const isFetchingBalances =
isLoadingAllWallets || fetchPortfoliosMutation.isPending;
const aggregatedStats = useMemo(() => {
let fundedWallets = 0;
let totalValue = 0;
portfolioMap.forEach((data) => {
if (data.totalUsdValue > 0) {
fundedWallets++;
totalValue += data.totalUsdValue;
}
});
return { fundedWallets, totalValue };
}, [portfolioMap]);
const columns = useMemo(() => {
return [
columnHelper.accessor("id", {
cell: (cell) => {
const userId = cell.getValue();
if (!userId) {
return "N/A";
}
return (
<CopyTextButton
textToShow={truncateIdentifier(userId)}
textToCopy={userId}
tooltip="Copy User Identifier"
copyIconPosition="left"
variant="ghost"
/>
);
},
header: "User Identifier",
id: "user_identifier",
}),
columnHelper.accessor("linkedAccounts", {
cell: (cell) => {
const identifier = getAuthIdentifier(cell.getValue());
if (!identifier) {
return "N/A";
}
return (
<CopyTextButton
textToShow={truncateIdentifier(identifier)}
textToCopy={identifier}
tooltip="Copy Auth Identifier"
copyIconPosition="left"
variant="ghost"
/>
);
},
enableColumnFilter: true,
header: "Auth Identifier",
id: "auth_identifier",
}),
columnHelper.accessor("wallets", {
cell: (cell) => {
const address = cell.getValue()[0]?.address;
return address ? (
<WalletAddress address={address} client={props.client} />
) : null;
},
header: "Address",
id: "address",
}),
columnHelper.accessor("wallets", {
id: "total_balance",
header: "Total Balance",
cell: (cell) => {
const address = cell.getValue()[0]?.address;
if (!address) return "N/A";
if (!portfolioLoaded) {
return <span className="text-muted-foreground text-sm">—</span>;
}
const data = portfolioMap.get(address);
if (!data) {
return <span className="text-muted-foreground text-sm">—</span>;
}
return (
<span className="text-sm">
{new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
}).format(data.totalUsdValue)}
</span>
);
},
}),
columnHelper.accessor("wallets", {
id: "tokens",
header: "Tokens",
cell: (cell) => {
const address = cell.getValue()[0]?.address;
if (!address) return "N/A";
if (!portfolioLoaded) {
return <span className="text-muted-foreground text-sm">—</span>;
}
const data = portfolioMap.get(address);
if (!data || data.tokens.length === 0) {
return <span className="text-muted-foreground text-sm">None</span>;
}
const topTokens = data.tokens
.sort((a, b) => (b.usdValue || 0) - (a.usdValue || 0))
.slice(0, 3)
.map((t) => t.symbol)
.join(", ");
return (
<TooltipProvider>
<Tooltip>
<TooltipTrigger className="text-sm">
{topTokens}
{data.tokens.length > 3 ? "..." : ""}
</TooltipTrigger>
<TooltipContent>
<div className="flex flex-col gap-1">
{data.tokens.map((t) => (
<div
key={`${t.tokenAddress}-${t.chainId}`}
className="flex justify-between gap-4 text-xs"
>
<span>{t.symbol}</span>
<span>
{new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
}).format(t.usdValue || 0)}
</span>
</div>
))}
</div>
</TooltipContent>
</Tooltip>
</TooltipProvider>
);
},
}),
columnHelper.accessor("linkedAccounts", {
cell: (cell) => {
const email = getPrimaryEmail(cell.getValue());
if (!email) {
return <span className="text-muted-foreground text-sm">N/A</span>;
}
return (
<CopyTextButton
textToShow={email}
textToCopy={email}
tooltip="Copy Email"
copyIconPosition="left"
variant="ghost"
/>
);
},
header: "Email",
id: "email",
}),
columnHelper.accessor("wallets", {
cell: (cell) => {
const value = cell.getValue()[0]?.createdAt;
if (!value) {
return;
}
return (
<ToolTipLabel
label={format(new Date(value), "MMM dd, yyyy 'at' h:mm:ss a zzz")}
hoverable
>
<span className="text-sm">
{format(new Date(value), "MMM dd, yyyy")}
</span>
</ToolTipLabel>
);
},
header: "Created",
id: "created_at",
}),
columnHelper.accessor("linkedAccounts", {
cell: (cell) => {
const value = cell.getValue();
const loginMethodsDisplay = value.reduce((acc, curr) => {
if (acc.length === 2) {
acc.push("...");
}
if (acc.length < 2) {
acc.push(curr.type);
}
return acc;
}, [] as string[]);
const loginMethods = value.map((v) => v.type).join(", ");
return (
<TooltipProvider>
<Tooltip>
<TooltipTrigger className="text-sm">
{loginMethodsDisplay.join(", ")}
</TooltipTrigger>
<TooltipContent>
<span className="text-sm">{loginMethods}</span>
</TooltipContent>
</Tooltip>
</TooltipProvider>
);
},
header: "Login Methods",
id: "login_methods",
}),
];
}, [props.client, portfolioMap, portfolioLoaded]);
const handleSearch = async (searchType: SearchType, query: string) => {
setIsSearching(true);
try {
const results = await searchUsers(
props.authToken,
props.projectClientId,
props.ecosystemSlug,
props.teamId,
searchType,
query,
);
setSearchResults(results);
setHasSearchResults(true);
} catch (error) {
console.error("Search failed:", error);
setSearchResults([]);
setHasSearchResults(true);
} finally {
setIsSearching(false);
}
};
const handleClearSearch = () => {
setSearchResults([]);
setHasSearchResults(false);
};
const downloadCSV = useCallback(async () => {
if (wallets.length === 0 || !getAllEmbeddedWallets) {
return;
}
const usersWallets = await getAllEmbeddedWallets({
clientId: props.projectClientId,
ecosystemSlug: props.ecosystemSlug,
teamId: props.teamId,
});
const csv = Papa.unparse(
usersWallets.map((row) => {
const email = getPrimaryEmail(row.linkedAccounts);
return {
address: row.wallets[0]?.address || "Uninitialized",
created: row.wallets[0]?.createdAt
? new Date(row.wallets[0].createdAt).toISOString()
: "Wallet not created yet",
email: email || "N/A",
login_methods: row.linkedAccounts.map((acc) => acc.type).join(", "),
auth_identifier: getAuthIdentifier(row.linkedAccounts) || "N/A",
user_identifier: row.id || "N/A",
};
}),
);
const csvUrl = URL.createObjectURL(
new Blob([csv], { type: "text/csv;charset=utf-8;" }),
);
const tempLink = document.createElement("a");
tempLink.href = csvUrl;
tempLink.setAttribute("download", "download.csv");
tempLink.click();
}, [
wallets,
props.projectClientId,
getAllEmbeddedWallets,
props.teamId,
props.ecosystemSlug,
]);
return (
<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 || isLoadingAllWallets}
onClick={downloadCSV}
variant="outline"
>
{isLoadingAllWallets && <Spinner className="size-4" />}
Download as .csv
</Button>
</div>
</div>
<div>
{hasSearchResults ? (
<SearchResults results={searchResults} client={props.client} />
) : (
<>
{/* Chain Selector and Fetch Button */}
<div className="flex items-center gap-3 px-4 lg:px-6 pb-4 border-b border-border">
<div className="flex items-center gap-2">
<MultiNetworkSelector
client={props.client}
selectedChainIds={selectedChains}
onChange={setSelectedChains}
disableChainId
hideTestnets
popoverContentClassName="max-h-[300px]"
/>
<Button
onClick={() => handleFetchBalances()}
disabled={
isFetchingBalances ||
selectedChains.length === 0 ||
walletsQuery.isPending
}
className="gap-2"
>
{isFetchingBalances ? (
<Spinner className="size-4" />
) : (
<RefreshCwIcon className="size-4" />
)}
{isFetchingBalances
? `Fetching... ${fetchProgress.total > 0 ? Math.round((fetchProgress.completed / fetchProgress.total) * 100) : 0}%`
: "Fetch All Balances"}
</Button>
</div>
{isFetchingBalances && (
<div className="flex flex-col gap-1 flex-1 min-w-[150px]">
{fetchProgress.total > 0 && (
<Progress
value={
(fetchProgress.completed / fetchProgress.total) * 100
}
className="h-2"
/>
)}
<p className="text-xs text-muted-foreground">
This may take a few minutes
</p>
</div>
)}
{portfolioLoaded && !isFetchingBalances && (
<Badge variant="success" className="ml-auto">
Balances loaded for {portfolioMap.size} wallets
</Badge>
)}
</div>
{/* Stats Section */}
<div className="grid grid-cols-2 gap-4 px-4 lg:px-6 py-4">
<StatCard
label="Funded Wallets"
value={portfolioLoaded ? aggregatedStats.fundedWallets : 0}
icon={WalletIcon}
isPending={isFetchingBalances}
emptyText={!portfolioLoaded ? "—" : undefined}
/>
<StatCard
label="Total Value"
value={portfolioLoaded ? aggregatedStats.totalValue : 0}
icon={DollarSignIcon}
formatter={(value) =>
new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
}).format(value)
}
isPending={isFetchingBalances}
emptyText={!portfolioLoaded ? "—" : undefined}
/>
</div>
<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>
);
}
@@ -42,7 +42,7 @@ const DialogContent = React.forwardRef<
<DialogOverlay className={dialogOverlayClassName} />
<DialogPrimitive.Content
className={cn(
"fixed z-50 grid w-full gap-4 rounded-t-xl border border-border bg-background p-6 shadow-lg duration-300 md:max-w-lg md:rounded-lg",
"fixed z-50 grid w-full gap-4 rounded-t-xl border border-border bg-background p-6 shadow-lg duration-300 md:max-w-lg md:rounded-xl",
// on mobile - put the dialog at the bottom of the screen, animate - slide up and fade in
"right-0 bottom-0 left-0",
"data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
@@ -41,3 +41,15 @@ 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;
export const NEXT_PUBLIC_CHECKOUT_IFRAME_CLIENT_ID =
process.env.NEXT_PUBLIC_CHECKOUT_IFRAME_CLIENT_ID;
export const NEXT_PUBLIC_SWAP_IFRAME_CLIENT_ID =
process.env.NEXT_PUBLIC_SWAP_IFRAME_CLIENT_ID;
export const NEXT_PUBLIC_BUY_IFRAME_CLIENT_ID =
process.env.NEXT_PUBLIC_BUY_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";
+10 -14
View File
@@ -330,20 +330,16 @@ export async function rotateSecretKeyClient(params: { project: Project }) {
throw new Error(res.error);
}
try {
// if the project has an encrypted vault admin key, rotate it as well
const service = params.project.services.find(
(service) => service.name === "engineCloud",
);
if (service?.encryptedAdminKey) {
await rotateVaultAccountAndAccessToken({
project: params.project,
projectSecretKey: res.data.data.secret,
projectSecretHash: res.data.data.secretHash,
});
}
} catch (error) {
console.error("Failed to rotate vault admin key", error);
// if the project has an encrypted vault admin key, rotate it as well
const service = params.project.services.find(
(service) => service.name === "engineCloud",
);
if (service?.encryptedAdminKey) {
await rotateVaultAccountAndAccessToken({
project: params.project,
projectSecretKey: res.data.data.secret,
projectSecretHash: res.data.data.secretHash,
});
}
return res.data;
@@ -177,33 +177,63 @@ export function useAllEmbeddedWallets(params: { authToken: string }) {
}) => {
const responses: WalletUser[] = [];
let page = 1;
let consecutiveFailures = 0;
const maxConsecutiveFailures = 3;
while (true) {
const res = await queryClient.fetchQuery<{
users: WalletUser[];
hasMore: boolean;
}>({
queryFn: fetchAccountList({
clientId,
ecosystemSlug,
teamId,
jwt: authToken,
pageNumber: page,
}),
queryKey: embeddedWalletsKeys.embeddedWallets(
address || "",
clientId || ecosystemSlug || "",
page,
),
});
while (consecutiveFailures < maxConsecutiveFailures) {
try {
const res = await queryClient.fetchQuery<{
users: WalletUser[];
hasMore: boolean;
}>({
queryFn: fetchAccountList({
clientId,
ecosystemSlug,
teamId,
jwt: authToken,
pageNumber: page,
}),
queryKey: embeddedWalletsKeys.embeddedWallets(
address || "",
clientId || ecosystemSlug || "",
page,
),
retry: 3,
retryDelay: (attemptIndex) =>
Math.min(1000 * 2 ** attemptIndex, 10000),
});
responses.push(...res.users);
responses.push(...res.users);
consecutiveFailures = 0; // Reset on success
if (!res.hasMore) {
break;
if (!res.hasMore) {
break;
}
page++;
} catch (error) {
consecutiveFailures++;
console.warn(
`Failed to fetch page ${page}, attempt ${consecutiveFailures}/${maxConsecutiveFailures}:`,
error,
);
if (consecutiveFailures >= maxConsecutiveFailures) {
// If we have some data, return it instead of throwing
if (responses.length > 0) {
console.warn(
`Returning partial results (${responses.length} users) after ${maxConsecutiveFailures} consecutive failures`,
);
break;
}
throw error;
}
// Wait before retrying the same page
await new Promise((resolve) =>
setTimeout(resolve, 1000 * consecutiveFailures),
);
}
page++;
}
return responses;
@@ -0,0 +1,231 @@
import { useMutation } from "@tanstack/react-query";
import type { GetBalanceResult } from "thirdweb/extensions/erc20";
import { THIRDWEB_API_HOST } from "@/constants/urls";
type WalletPortfolioToken = GetBalanceResult & {
usdValue?: number;
priceUsd?: number;
};
export type WalletPortfolioData = {
totalUsdValue: number;
tokens: WalletPortfolioToken[];
};
// Retry helper with exponential backoff - returns null on failure instead of throwing
async function fetchWithRetry(
url: string,
options: RequestInit,
maxRetries = 3,
): Promise<Response | null> {
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
const response = await fetch(url, options);
// Retry on rate limit (429) or server errors (5xx)
if (response.status === 429 || response.status >= 500) {
const delay = Math.min(1000 * 2 ** attempt, 10000); // Max 10s delay
await new Promise((resolve) => setTimeout(resolve, delay));
continue;
}
return response;
} catch (_e) {
// Network error - retry with backoff
const delay = Math.min(1000 * 2 ** attempt, 10000);
await new Promise((resolve) => setTimeout(resolve, delay));
}
}
// All retries failed - return null instead of throwing
return null;
}
// Fetch tokens for a single address on a single chain
async function fetchTokensForChain(
address: string,
chainId: number,
authToken: string,
teamId: string,
clientId?: string,
ecosystemSlug?: string,
): Promise<WalletPortfolioToken[]> {
try {
const url = new URL(`${THIRDWEB_API_HOST}/v1/wallets/${address}/tokens`);
url.searchParams.set("chainId", chainId.toString());
url.searchParams.set("limit", "50");
url.searchParams.set("metadata", "true");
url.searchParams.set("includeSpam", "false");
url.searchParams.set("includeNative", "true");
url.searchParams.set("sortBy", "usd_value");
url.searchParams.set("sortOrder", "desc");
const response = await fetchWithRetry(url.toString(), {
headers: {
Authorization: `Bearer ${authToken}`,
"Content-Type": "application/json",
"x-thirdweb-team-id": teamId,
...(clientId && { "x-client-id": clientId }),
...(ecosystemSlug && {
"x-ecosystem-id": `ecosystem.${ecosystemSlug}`,
}),
},
});
// If all retries failed or response not ok, return empty (balance = 0)
if (!response || !response.ok) {
return [];
}
const data = await response.json();
const rawTokens = data.result?.tokens || [];
return rawTokens.map(
(t: {
balance: string;
decimals: number;
name: string;
symbol: string;
token_address: string;
chain_id: number;
price_data?: { usd_value?: number; price_usd?: number };
}): WalletPortfolioToken => ({
value: BigInt(t.balance),
decimals: t.decimals,
displayValue: (Number(t.balance) / 10 ** t.decimals).toString(),
name: t.name,
symbol: t.symbol,
tokenAddress: t.token_address,
chainId: t.chain_id,
usdValue: t.price_data?.usd_value,
priceUsd: t.price_data?.price_usd,
}),
);
} catch (e) {
// Silent fail for individual chain - continue with others
console.warn(
`Failed to fetch tokens for ${address} on chain ${chainId}:`,
e,
);
return [];
}
}
// Fetch tokens for a single address on selected chains
async function fetchWalletPortfolio(
address: string,
chainIds: number[],
authToken: string,
teamId: string,
clientId?: string,
ecosystemSlug?: string,
): Promise<WalletPortfolioData> {
// Fetch all chains concurrently for this address
const results = await Promise.all(
chainIds.map((chainId) =>
fetchTokensForChain(
address,
chainId,
authToken,
teamId,
clientId,
ecosystemSlug,
),
),
);
const allTokens = results.flat();
const totalUsdValue = allTokens.reduce(
(acc, token) => acc + (token.usdValue || 0),
0,
);
return {
totalUsdValue,
tokens: allTokens,
};
}
// Batch fetch portfolios for all addresses with progress callback
async function fetchAllPortfolios(
addresses: string[],
chainIds: number[],
authToken: string,
teamId: string,
clientId?: string,
ecosystemSlug?: string,
onProgress?: (completed: number, total: number) => void,
): Promise<Map<string, WalletPortfolioData>> {
const results = new Map<string, WalletPortfolioData>();
const concurrency = 50; // Process up to 50 addresses concurrently
let completed = 0;
let index = 0;
// Worker function that processes one address at a time
async function worker() {
while (index < addresses.length) {
const currentIndex = index++;
const address = addresses[currentIndex];
if (!address) continue;
try {
const data = await fetchWalletPortfolio(
address,
chainIds,
authToken,
teamId,
clientId,
ecosystemSlug,
);
results.set(address, data);
} catch (e) {
// Silent fail - continue with others
console.warn(`Failed to fetch portfolio for ${address}:`, e);
}
completed++;
onProgress?.(completed, addresses.length);
}
}
// Start concurrent workers
const workers = Array.from(
{ length: Math.min(concurrency, addresses.length) },
() => worker(),
);
await Promise.all(workers);
return results;
}
export function useFetchAllPortfolios() {
return useMutation({
mutationFn: async ({
addresses,
chainIds,
authToken,
teamId,
clientId,
ecosystemSlug,
onProgress,
}: {
addresses: string[];
chainIds: number[];
authToken: string;
teamId: string;
clientId?: string;
ecosystemSlug?: string;
onProgress?: (completed: number, total: number) => void;
}) => {
return fetchAllPortfolios(
addresses,
chainIds,
authToken,
teamId,
clientId,
ecosystemSlug,
onProgress,
);
},
});
}
+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,
+8 -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;
@@ -35,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;
@@ -104,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) => {
@@ -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"
>
@@ -283,7 +283,7 @@ function TokenInfoSection(props: {
<dt className="text-xs font-medium text-muted-foreground tracking-wider uppercase">
{props.label}
</dt>
<dd className="text-3xl font-bold text-foreground tracking-tight">
<dd className="text-3xl font-semibold text-foreground tracking-tight">
{props.value}
</dd>
</dl>
@@ -72,7 +72,6 @@ export const DecodedInputSet: React.FC<DecodedInputSetProps> = ({
<div className="mt-6 flex justify-start">
<Button
className="gap-2"
disabled={param.type === "bytes" && fields.length >= 1}
onClick={() =>
append({
defaultValue: "",
@@ -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",
@@ -58,6 +75,18 @@ export async function getBillingCheckoutUrl(options: {
}
const json = await res.json();
if (
"error" in json &&
"message" in json.error &&
typeof json.error.message === "string"
) {
return {
error: json.error.message,
status: "error",
} as const;
}
if (!json.result) {
return {
error: "An unknown error occurred, please try again later.",
@@ -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>
)}
@@ -3,6 +3,7 @@ import { useParams } from "next/navigation";
import { toast } from "sonner";
import type { ThirdwebClient } from "thirdweb";
import type { Ecosystem, Partner } from "@/api/team/ecosystems";
import { useDashboardStorageUpload } from "@/hooks/useDashboardStorageUpload";
import { useDashboardRouter } from "@/lib/DashboardRouter";
import { useAddPartner } from "../../hooks/use-add-partner";
import { PartnerForm, type PartnerFormValues } from "./partner-form.client";
@@ -23,6 +24,8 @@ export function AddPartnerForm({
const teamSlug = params.team_slug as string;
const ecosystemSlug = params.slug as string;
const storageUpload = useDashboardStorageUpload({ client });
const { mutateAsync: addPartner, isPending } = useAddPartner(
{
authToken,
@@ -48,10 +51,23 @@ export function AddPartnerForm({
},
);
const handleSubmit = (
const isUploading = storageUpload.isPending;
const handleSubmit = async (
values: PartnerFormValues,
finalAccessControl: Partner["accessControl"] | null,
) => {
let imageUrl: string | undefined;
if (values.logo) {
try {
const [uri] = await storageUpload.mutateAsync([values.logo]);
imageUrl = uri;
} catch {
toast.error("Failed to upload logo");
return;
}
}
addPartner({
accessControl: finalAccessControl,
allowlistedBundleIds: values.bundleIds
@@ -61,6 +77,7 @@ export function AddPartnerForm({
.split(/,| /)
.filter((d) => d.length > 0),
ecosystem,
imageUrl,
name: values.name,
});
};
@@ -68,7 +85,7 @@ export function AddPartnerForm({
return (
<PartnerForm
client={client}
isSubmitting={isPending}
isSubmitting={isPending || isUploading}
onSubmit={handleSubmit}
submitLabel="Add"
/>
@@ -1,12 +1,13 @@
"use client";
import { zodResolver } from "@hookform/resolvers/zod";
import { PlusIcon, Trash2Icon } from "lucide-react";
import { useId } from "react";
import { PencilIcon, PlusIcon, Trash2Icon, XIcon } from "lucide-react";
import { useId, useRef } from "react";
import { useFieldArray, useForm } from "react-hook-form";
import type { ThirdwebClient } from "thirdweb";
import type { z } from "zod";
import type { Partner } from "@/api/team/ecosystems";
import { Img } from "@/components/blocks/Img";
import { Button } from "@/components/ui/button";
import {
Form,
@@ -17,11 +18,13 @@ import {
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { ImageUpload } from "@/components/ui/image-upload";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Spinner } from "@/components/ui/Spinner";
import { Switch } from "@/components/ui/switch";
import { cn } from "@/lib/utils";
import { resolveSchemeWithErrorHandler } from "@/utils/resolveSchemeWithErrorHandler";
import { partnerFormSchema } from "../../constants";
import { AllowedOperationsSection } from "./allowed-operations-section";
@@ -118,6 +121,7 @@ export function PartnerForm({
const accessControlId = useId();
const serverVerifierId = useId();
const fileInputRef = useRef<HTMLInputElement>(null);
return (
<Form {...form}>
@@ -150,6 +154,101 @@ export function PartnerForm({
</FormItem>
)}
/>
<FormField
control={form.control}
name="logo"
render={() => {
const removeLogo = form.watch("removeLogo");
const logoFile = form.watch("logo");
const newFilePreview = logoFile
? URL.createObjectURL(logoFile)
: undefined;
const existingImageUrl =
partner?.imageUrl && !removeLogo
? resolveSchemeWithErrorHandler({
client,
uri: partner.imageUrl,
})
: undefined;
const displayUrl = newFilePreview || existingImageUrl;
return (
<FormItem>
<FormLabel>Partner Logo</FormLabel>
<FormControl>
<div>
<input
ref={fileInputRef}
accept="image/png,image/jpeg,image/webp"
className="hidden"
onChange={(e) => {
const file = e.target.files?.[0];
if (file) {
form.setValue("logo", file, {
shouldValidate: true,
});
form.setValue("removeLogo", false);
}
e.target.value = "";
}}
type="file"
/>
{displayUrl ? (
<div className="relative inline-block">
<Img
alt={partner?.name ?? "Partner logo"}
className="size-20 rounded-md border object-contain object-center"
src={displayUrl}
/>
<Button
aria-label="Remove logo"
className="absolute -top-2 -right-2 size-6 rounded-full bg-background p-0 hover:bg-accent"
onClick={() => {
form.setValue("logo", undefined);
form.setValue("removeLogo", true);
}}
size="icon"
type="button"
variant="ghost"
>
<XIcon className="size-3" />
</Button>
<Button
aria-label="Change logo"
className="absolute -bottom-2 -right-2 size-6 rounded-full bg-background p-0 hover:bg-accent"
onClick={() => fileInputRef.current?.click()}
size="icon"
type="button"
variant="ghost"
>
<PencilIcon className="size-3" />
</Button>
</div>
) : (
<ImageUpload
accept="image/png, image/jpeg, image/webp"
className="bg-background"
onUpload={(files) => {
if (files[0]) {
form.setValue("logo", files[0], {
shouldValidate: true,
});
form.setValue("removeLogo", false);
}
}}
/>
)}
</div>
</FormControl>
<FormDescription>
Optional logo for this partner. Used in OTP emails sent to
users authenticating through this partner.
</FormDescription>
<FormMessage />
</FormItem>
);
}}
/>
<FormField
control={form.control}
defaultValue=""
@@ -3,6 +3,7 @@ import { useParams } from "next/navigation";
import { toast } from "sonner";
import type { ThirdwebClient } from "thirdweb";
import type { Ecosystem, Partner } from "@/api/team/ecosystems";
import { useDashboardStorageUpload } from "@/hooks/useDashboardStorageUpload";
import { useDashboardRouter } from "@/lib/DashboardRouter";
import { useUpdatePartner } from "../../hooks/use-update-partner";
import { PartnerForm, type PartnerFormValues } from "./partner-form.client";
@@ -25,6 +26,8 @@ export function UpdatePartnerForm({
const teamSlug = params.team_slug as string;
const ecosystemSlug = params.slug as string;
const storageUpload = useDashboardStorageUpload({ client });
const { mutateAsync: updatePartner, isPending } = useUpdatePartner(
{
authToken,
@@ -50,10 +53,31 @@ export function UpdatePartnerForm({
},
);
const handleSubmit = (
const isUploading = storageUpload.isPending;
const handleSubmit = async (
values: PartnerFormValues,
finalAccessControl: Partner["accessControl"] | null,
) => {
// Determine imageUrl based on three states:
// 1. New file uploaded → upload and use new URI
// 2. Explicit removal → send null to clear
// 3. No change → preserve existing partner imageUrl
let imageUrl: string | null | undefined;
if (values.logo) {
try {
const [uri] = await storageUpload.mutateAsync([values.logo]);
imageUrl = uri;
} catch {
toast.error("Failed to upload logo");
return;
}
} else if (values.removeLogo) {
imageUrl = null;
} else {
imageUrl = partner.imageUrl;
}
updatePartner({
accessControl: finalAccessControl,
allowlistedBundleIds: values.bundleIds
@@ -63,6 +87,7 @@ export function UpdatePartnerForm({
.split(/,| /)
.filter((d) => d.length > 0),
ecosystem,
imageUrl,
name: values.name,
partnerId: partner.id,
});
@@ -71,7 +96,7 @@ export function UpdatePartnerForm({
return (
<PartnerForm
client={client}
isSubmitting={isPending}
isSubmitting={isPending || isUploading}
onSubmit={handleSubmit}
partner={partner}
submitLabel="Update"
@@ -154,6 +154,21 @@ const allowedOperationsSchema = z.discriminatedUnion("signMethod", [
export const partnerFormSchema = z
.object({
logo: z
.instanceof(File, {
message: "Please select an image file",
})
.refine(
(file) => ["image/png", "image/jpeg", "image/webp"].includes(file.type),
{
message: "Only PNG, JPG or WEBP images are allowed",
},
)
.refine((file) => file.size <= 500 * 1024, {
message: "Logo size must be less than 500KB",
})
.optional(),
removeLogo: z.boolean().default(false),
accessControl: z
.object({
allowedOperations: z
@@ -8,6 +8,7 @@ import type { Ecosystem, Partner } from "@/api/team/ecosystems";
type AddPartnerParams = {
ecosystem: Ecosystem;
name: string;
imageUrl?: string;
allowlistedDomains: string[];
allowlistedBundleIds: string[];
accessControl?: Partner["accessControl"] | null;
@@ -37,6 +38,7 @@ export function useAddPartner(
accessControl: params.accessControl ?? undefined,
allowlistedBundleIds: params.allowlistedBundleIds,
allowlistedDomains: params.allowlistedDomains,
imageUrl: params.imageUrl,
name: params.name,
// TODO - remove the requirement for permissions in API endpoint
permissions: ["FULL_CONTROL_V1"],
@@ -9,6 +9,7 @@ type UpdatePartnerParams = {
partnerId: string;
ecosystem: Ecosystem;
name: string;
imageUrl?: string | null;
allowlistedDomains: string[];
allowlistedBundleIds: string[];
accessControl?: {
@@ -43,6 +44,7 @@ export function useUpdatePartner(
accessControl: params.accessControl,
allowlistedBundleIds: params.allowlistedBundleIds,
allowlistedDomains: params.allowlistedDomains,
imageUrl: params.imageUrl,
name: params.name,
}),
@@ -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"
@@ -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}/bridge/configuration`}
metric="Fees"
/>
),
color: "hsl(var(--chart-3))",
emptyContent: undefined,
isCurrency: true,
label: "Bridge 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,
});
@@ -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>
@@ -1,7 +1,6 @@
"use client";
import {
BotIcon,
ChartLineIcon,
ChevronDownIcon,
ChevronRightIcon,
FileCode2Icon,
@@ -83,16 +82,6 @@ export function ChatSidebar(props: {
<div className="h-5" />
<div className="flex flex-col gap-3 border-t border-dashed px-4 py-4">
<Link
className="flex items-center gap-1 rounded-full text-foreground text-sm hover:underline justify-between"
href={`/team/${props.team_slug}/${props.project.slug}/ai/analytics`}
>
<div className="flex items-center gap-2">
<ChartLineIcon className="size-3.5 text-muted-foreground" />
Analytics
</div>
<ChevronRightIcon className="size-3.5 text-muted-foreground" />
</Link>
<Link
className="flex items-center gap-1 rounded-full text-foreground text-sm hover:underline justify-between"
href={"https://portal.thirdweb.com/ai/chat"}
@@ -20,35 +20,17 @@ export function QuickStartSection(props: {
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
<FeatureCard
title="Buy Widget Component"
icon={CodeIcon}
id="swap_tokens"
features={[
"React component",
"Cross-chain token swaps across 85+ blockchains",
"Fiat onramp support to buy tokens with credit/debit cards",
"Customizable UI",
]}
description={undefined}
link={{
href: `https://portal.thirdweb.com/bridge/swap`,
label: "Get Started",
}}
/>
<FeatureCard
title="Bridge Widget Script"
description={undefined}
title="Bridge Widget"
description="Integrate cross-chain swaps and fiat onramp into your app using bridge widget"
icon={CodeIcon}
id="fees"
features={[
"Integrate with a script tag",
"Integrate with iframe, script, or React component",
"Cross-chain token swaps across 85+ blockchains",
"Fiat onramp support to buy tokens with credit/debit cards",
"Customizable UI",
]}
link={{
href: `/team/${props.teamSlug}/${props.projectSlug}/bridge/configuration`,
href: `https://portal.thirdweb.com/bridge/bridge-widget`,
label: "Get Started",
}}
/>
@@ -60,7 +42,7 @@ export function QuickStartSection(props: {
id="webhooks"
features={["Instant events", "Transaction verification"]}
link={{
href: `/team/${props.teamSlug}/${props.projectSlug}/webhooks/payments`,
href: `/team/${props.teamSlug}/${props.projectSlug}/bridge/webhooks`,
label: "Setup Webhooks",
}}
/>
@@ -1,4 +1,3 @@
import { WebhookIcon } from "lucide-react";
import { redirect } from "next/navigation";
import { getAuthToken } from "@/api/auth-token";
import { getProject } from "@/api/project/projects";
@@ -43,13 +42,7 @@ export default async function Layout(props: {
instantly
</>
),
actions: {
secondary: {
href: `/team/${params.team_slug}/${params.project_slug}/webhooks/payments`,
label: "Webhooks",
icon: <WebhookIcon className="size-3.5 text-muted-foreground" />,
},
},
actions: null,
links: [
{
type: "docs",
@@ -75,6 +68,10 @@ export default async function Layout(props: {
name: "Configuration",
path: `/team/${params.team_slug}/${params.project_slug}/bridge/configuration`,
},
{
name: "Webhooks",
path: `/team/${params.team_slug}/${params.project_slug}/bridge/webhooks`,
},
]}
>
{props.children}
@@ -46,7 +46,7 @@ export default async function Page(props: {
<BreadcrumbItem>
<BreadcrumbLink asChild>
<Link
href={`/team/${params.team_slug}/${params.project_slug}/webhooks/payments`}
href={`/team/${params.team_slug}/${params.project_slug}/bridge/webhooks`}
>
Webhooks
</Link>
@@ -12,6 +12,7 @@ import {
PlusIcon,
TrashIcon,
WebhookIcon,
XIcon,
} from "lucide-react";
import Link from "next/link";
import { type PropsWithChildren, useMemo, useState } from "react";
@@ -95,9 +96,14 @@ export function PayWebhooksPage(props: PayWebhooksPageProps) {
if (!webhooksQuery.data?.length) {
return (
<div className="flex flex-col items-center gap-6 rounded-lg border border-border bg-card p-8 text-center">
<div className="flex flex-col gap-2">
<h2 className="font-semibold text-xl">No webhooks configured</h2>
<p className="text-sm text-muted-foreground">
<div className="flex flex-col items-center">
<div className="rounded-full p-2 border bg-background inline-flex mb-3">
<XIcon className="size-4 text-muted-foreground" />
</div>
<h2 className="font-semibold text-base mb-1">
No webhooks configured
</h2>
<p className="text-muted-foreground text-sm">
Create a webhook to receive notifications for bridge, swap or onramp
events.
</p>
@@ -126,7 +132,7 @@ export function PayWebhooksPage(props: PayWebhooksPageProps) {
webhook={webhook}
clientId={props.clientId}
teamId={props.teamId}
layoutPath={`/team/${props.teamSlug}/${props.projectSlug}/webhooks/payments`}
layoutPath={`/team/${props.teamSlug}/${props.projectSlug}/bridge/webhooks`}
/>
))}
</div>
@@ -1,7 +1,7 @@
import { notFound } from "next/navigation";
import { getAuthToken } from "@/api/auth-token";
import { getProject } from "@/api/project/projects";
import { PayWebhooksPage } from "../../payments/webhooks/components/webhooks.client";
import { PayWebhooksPage } from "./components/webhooks.client";
export default async function Page(props: {
params: Promise<{
@@ -1,56 +0,0 @@
import type { Meta, StoryObj } from "@storybook/nextjs";
import { BadgeContainer } from "@/storybook/utils";
import type { EngineCloudStats } from "@/types/analytics";
import { EngineCloudBarChartCardUI } from "./EngineCloudBarChartCardUI";
const meta = {
component: Component,
title: "Analytics/EngineCloudBarChartCard",
} satisfies Meta<typeof Component>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Variants: Story = {
parameters: {
viewport: { defaultViewport: "desktop" },
},
};
const generateTimeSeriesData = (days: number, pathnames: string[]) => {
const data: EngineCloudStats[] = [];
const today = new Date();
for (let i = days - 1; i >= 0; i--) {
const date = new Date(today);
date.setDate(date.getDate() - i);
const dateStr = date.toISOString().split("T")[0];
for (const pathname of pathnames) {
data.push({
chainId: "84532",
// biome-ignore lint/style/noNonNullAssertion: we know this is not null
date: dateStr!,
pathname,
totalRequests: Math.floor(Math.random() * 1000) + 100,
});
}
}
return data;
};
function Component() {
return (
<div className="container max-w-6xl space-y-10 py-10">
<BadgeContainer label="Multiple Pathnames">
<EngineCloudBarChartCardUI
rawData={generateTimeSeriesData(30, [
"/v1/write/transaction",
"/v1/sign/transaction",
"/v1/sign/message",
])}
/>
</BadgeContainer>
</div>
);
}

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