Compare commits

...
Author SHA1 Message Date
Joaquim VergesGitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
e0ace736a8 Version Packages (#8281)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-10-21 18:31:35 -07:00
Jonas DanielsandGitHub b30369f3cc Change getContractMetadata() return type from any to unknown (#8289) 2025-10-21 17:43:43 -07:00
FirekeeperandGitHub f2121e9519 [Dashboard] Add userId support across wallet APIs and UI (#8290) 2025-10-22 06:35:10 +07:00
Joaquim VergesandGitHub 29d05a50bf [Dashboard] Add nexus to framer-rewrites.js (#8287) 2025-10-20 16:19:28 -07:00
Jonas DanielsandGitHub f00f559a12 Add force-dynamic to Stripe-related pages (#8288) 2025-10-20 16:19:16 -07:00
MananTank ceba6835dd SDK: Fix process not defined error in Vite (#8280)
<!--

## 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 a `process not defined` error that occurs when using the `thirdweb/contract` import in Vite by ensuring that the `process` variable is only accessed when it is defined.

### Detailed summary

- Updated `PUBLISHED_PRIVATE_KEY` export in `packages/thirdweb/src/utils/any-evm/zksync/constants.ts` to check if `process` is defined before accessing `process.env.ZKSYNC_PUBLISHED_PRIVATE_KEY`.

> ✨ 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**
  - Improved robustness of environment configuration handling to avoid runtime issues in non-Node environments.

- **Bug Fixes**
  - Fixed a "process not defined" error that could occur when importing the library in Vite, included in a patch release.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-10-20 13:55:09 +00:00
Joaquim VergesGitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
b33c1a30ac Version Packages (#8272)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-10-19 18:33:02 -07:00
Joaquim VergesandGitHub 72fbb1550f Resolve TokenIcon automatically when available (#8279) 2025-10-19 18:27:35 -07:00
MananTank 1e7df5ad3d [MNY-252] Dashboard: Update WalletAddress component UI (#8276)
<!--

## 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 `WalletAddress` component and its UI by improving the handling of social profiles, adding new story variants, and refining the UI elements for better user experience.

### Detailed summary
- Changed `retry` in `useSocialProfiles` from `3` to `false`.
- Added new Storybook stories for `WalletAddress` and `WalletAddressUI`.
- Introduced a `Skeleton` component for loading states.
- Refactored `WalletAddress` to use `useSocialProfiles` directly.
- Updated UI structure and styling for displaying social profiles.
- Improved the handling of address display and copy functionality.

> ✨ 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 Storybook examples showcasing Wallet Address states (loaded, loading, empty, invalid, zero address, ENS)

* **Refactor**
  * Split wallet address UI for clearer rendering and moved profile-driven rendering to a dedicated UI component
  * Updated copy button styling to use a unified copy control
  * Improved social profiles panel layout and presentation

* **Bug Fixes / Behavior**
  * Adjusted profile loading behavior to avoid automatic retry on failure, surfacing empty/error states sooner
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-10-19 10:54:54 +00:00
MananTank ca16ec783b [MNY-275] Surface the error from bridge API in SwapWidget UI (#8277)
<!--

## 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 in the `swap-ui.tsx` component by providing more detailed error messages when fetching data fails.

### Detailed summary
- Added `multiline` property to a component.
- Updated error message for `preparedResultQuery.error` to display the specific error message if available, falling back to "Failed to get a quote".
- Retained existing error messages for `buyTokenQuery` and `sellTokenQuery`.

> ✨ 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 display in the swap widget: error text now shows a more specific message when available, falls back to a generic failure message otherwise, and supports multiline wrapping so longer errors are fully visible to users.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-10-18 00:05:28 +00:00
MananTank b1a780832e [MNY-274] Dashboard: Generate swap token pages for popular tokens (#8278)
<!--

## 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 `BuyAndSwapEmbed` component and related functionalities to improve the handling of buy and swap operations across different tokens and chains, enhancing the user experience in the bridge application.

### Detailed summary

- Removed `chain` and `tokenAddress` props from `BuyAndSwapEmbed`.
- Introduced `buyTab` and `swapTab` props to manage buy/sell token information.
- Updated `UniversalBridgeEmbed` to accept new props structure.
- Added token pair management in `slug-map.ts`.
- Created utility functions for token pair data retrieval.
- Refactored `BridgePageUI` to accommodate the new props and structure.
- Enhanced error handling and reporting in `BuyAndSwapEmbed`.

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

<!-- end pr-codex -->

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

- **New Features**
  - Added a Bridge UI and dedicated token-pair pages with static routes, metadata, and token-pair utilities for cross-chain slugs.
  - Introduced a Bridge page UI with FAQ, data summary, and an embedded universal bridge widget.

- **Improvements**
  - Unified buy/sell input structure for embeds and prefills, simplifying embed wiring and page rendering.
  - More robust handling of chain/token data and reporting for buy/swap flows; removed legacy testnet flag usage.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-10-17 23:49:31 +00:00
MananTank 9bb65ad5b2 [PRO-48] Dashboard: Bridge page UI updates (#8274)
<!--

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

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

## Notes for the reviewer

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

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on refactoring components related to FAQ sections, modifying link types for analytics, and enhancing the header components in the bridge application.

### Detailed summary
- Changed `linkType` in `reportBridgePageLinkClick` from `"integrate-bridge"` to `"bridge-docs"`.
- Renamed `FaqSection` to `FaqAccordion` and updated its implementation.
- Introduced a new `FaqSection` component wrapping `FaqAccordion`.
- Renamed `PageHeader` to `BridgePageHeader`.
- Updated `PillLink` references to use `FaqAccordion`.
- Enhanced styling and structure of header and FAQ sections.

> ✨ 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**
  * Theme toggle added to dashboard headers
  * Documentation quick-access added to headers and bridge area

* **UI/Layout Changes**
  * Redesigned bridge page with a new hero/heading and data pill layout
  * FAQ section refactored into an accordion-style component
  * Header navigation updated to include trending tokens and documentation controls
  * Removed legacy pill-style external link component
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-10-17 21:53:57 +00:00
MananTank db291d2d38 [MNY-268] Bridge Status UI updates (#8273)
<!--

## 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 UI components in the dashboard by improving styles for better text handling and introducing a new `LinkWithCopyButton` component for copying links and displaying tooltips.

### Detailed summary
- Updated `className` in `CopyTextButton` and `WalletAddress` for better text truncation.
- Introduced `LinkWithCopyButton` component for link handling and copying functionality.
- Adjusted styles in `BridgeStatus` and `TokenInfo` for improved UI consistency.
- Replaced `CopyTextButton` with `LinkWithCopyButton` for token address and transaction hash.

> ✨ 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 combined copy-and-open-link component with tooltip and temporary visual feedback for quick copying and opening external links.

* **Style**
  * Improved text truncation for wallet names, addresses and copy controls to prevent overflow.
  * Updated spacing, typography and section layout across the dashboard for clearer hierarchy.
  * Added external-link arrows for clearer link affordance.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-10-17 20:23:13 +00:00
Jonas DanielsandGitHub 9dd0b17f15 Remove Chain Infrastructure section from team sidebar (#8275) 2025-10-17 15:21:34 -05:00
MananTank 12082fd5fd Dashboard: Fix autoconnect in /bridge page (#8271)
<!--

## 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 simplifies the `TWAutoConnect` component by removing the optional `accountAbstraction` prop and streamlining its usage in the `BridgeProviders` component.

### Detailed summary
- Removed the `accountAbstraction` prop from the `TWAutoConnect` component.
- Updated the `TWAutoConnect` function signature to only require the `client` prop.
- Integrated `TWAutoConnect` into the `BridgeProviders` component, passing the `thirdwebClient`.

> ✨ 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**
  * Streamlined the auto-connect component API so it only requires the client, removing an unused configuration parameter.
  * Integrated the simplified auto-connect into the app provider stack to ensure automatic wallet/client initialization earlier in the startup sequence.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-10-17 16:38:49 +00:00
MananTank 5ff6063961 [MNY-269] SDK: autofocus token search input when token selector modal opens in BuyWidget, SwapWidget and BridgeWidget (#8266)
<!--

## 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 user experience by adding autofocus functionality to token selection inputs in various components and refactoring the `BuyWidget` and `SwapWidget` to utilize a common `Variant` component for rendering with different themes.

### Detailed summary
- Added `autoFocus` to token search inputs in `BuyWidget`, `SwapWidget`, and `BridgeWidget`.
- Refactored `BuyWidget` to use a `Variant` component for theme rendering.
- Refactored `SwapWidget` to use a `Variant` component for theme rendering.
- Introduced `autoFocusCrossIcon` prop in `Modal` component.
- Updated `SearchInput` to accept `autoFocus` prop.

> ✨ 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**
  * Token search input now auto-focuses on desktop when opening token selector modals in Buy, Swap, and Bridge widgets.
  * Modal gained a configurable option to control initial focus of the close (cross) button.

* **Bug Fixes / UX**
  * Prevents unwanted focus on the modal close icon by default when configured, improving keyboard/voicing navigation.

* **Chores**
  * Storybook examples updated for consistent light/dark variants and layout metadata.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-10-17 14:31:13 +00:00
MananTank 3db3982fab [MNY-270] Remove in-app wallet options from bridge page connect modal (#8270)
<!--

## 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 centralized `appMetadata` constant for consistent branding across various components and refactors the code to utilize this constant, reducing redundancy and improving maintainability.

### Detailed summary
- Added `appMetadata` constant in `connect.ts`.
- Refactored `NebulaConnectButton` to use `appMetadata`.
- Updated `PageHeader` to include `wallets` prop in `PublicPageConnectButton`.
- Created `bridgeWallets` array in `UniversalBridgeEmbed` using `createWallet` with `appMetadata`.
- Modified multiple components to utilize `appMetadata` instead of hardcoded values.

> ✨ 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**
  * Wallet selection is now customizable for Buy, Swap, and Bridge flows.
  * App branding metadata centralized for consistent presentation across all wallet connection points.
  * Bridge now includes preset wallet options to streamline onboarding and autoconnect behavior.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-10-17 03:46:41 +00:00
MananTank bf90104330 Dashboard: Remove playwright tests, checkly (#8269)
<!--

## 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 removal of various test files and configurations related to the `dashboard` application, alongside updates to the `package.json` and `pnpm-lock.yaml` files to reflect changes in dependencies.

### Detailed summary
- Deleted multiple test files in `apps/dashboard/tests`.
- Removed configuration files: `setup.ts`, `README.md`, `checkly.config.ts`, and `playwright.config.ts`.
- Updated `package.json` to remove dependencies related to `checkly` and `playwright`.
- Adjusted `pnpm-lock.yaml` to reflect the removal of certain packages and their dependencies.

> 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**
  * Removed end-to-end testing infrastructure, configuration, helper utilities, test runner scripts, and related dev dependencies from the dashboard app.
* **Documentation**
  * Deleted the dashboard E2E testing README and related test documentation.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-10-16 20:57:55 +00:00
Joaquim VergesGitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
8003e93f19 Version Packages (#8264)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-10-16 14:05:56 -05:00
Jonas DanielsandGitHub 51c8318df7 Fetch native currency from chain API when required (#8267) 2025-10-16 14:00:59 -05:00
Joaquim VergesandGitHub f043f1eef4 [Docs] Rename listDiscoveryResources to listPayableServices in MCP tools (#8268) 2025-10-16 11:21:12 -07:00
Manan TankandGitHub d68da2239b [MNY-256] SDK: Update onSuccess prop of BuyWidget, CheckoutWidget, SwapWidget, BridgeWidget components (#8259) 2025-10-16 10:20:44 -07:00
Manan TankandGitHub 6a6b0fba96 [MNY-254] SDK: Preserve token and amount selection in BuyWidget when going back to initial screen (#8260) 2025-10-16 10:20:18 -07:00
Manan TankandGitHub c934102c51 [MNY-253] Dashboard: Add tracking for token row in /tokens page, SDK: BridgeWidget component (#8254) 2025-10-16 09:46:20 -07:00
MananTank 8c58c131ac [MNY-250] Fix Save button not enabled after changing Royalty BPS in contract settings page (#8252)
<!--

## 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 modification to the form handling in two components, adding the `shouldDirty` option to the `setValue` function calls for better form state management.

### Detailed summary
- In `royalties.tsx`, added `shouldDirty: true` to `form.setValue` for `seller_fee_basis_points`.
- In `platform-fees.tsx`, added `shouldDirty: true` to `form.setValue` for `platform_fee_basis_points`.

> ✨ 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**
  * Platform Fees and Royalties settings now mark field edits as dirty immediately, enabling the Save action when values are changed.
  * Unsaved-changes indicators and prompts update right away, reducing the risk of losing edits.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-10-16 11:15:37 +00:00
Joaquim VergesandGitHub 2779afc15f [Docs] Update X402 agent example to use listDiscoveryResources (#8265) 2025-10-16 01:08:03 -07:00
Prithvish BaidyaandGitHub fe50abd5ab Add Solana transactions and wallets tables (#8263) 2025-10-16 04:26:26 +07:00
Joaquim VergesGitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
e830109274 Version Packages (#8258)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-10-15 05:01:50 -07:00
Joaquim VergesandGitHub e7f42e6ac5 [SDK] Fix query cache for block explorers for raw chains (#8257) 2025-10-15 04:59:09 -07:00
Manan TankandGitHub 91ff13b8aa Portal: Fix widget component API reference links (#8255) 2025-10-15 04:46:07 -07:00
Joaquim VergesGitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
47354d5a19 Version Packages (#8243)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-10-14 12:29:02 -07:00
Joaquim VergesandGitHub 8280efb991 [SDK] Fix Coinbase wallet transaction desktop popup for mobile QR login (#8253) 2025-10-14 12:20:50 -07:00
MananTank 5f11ae0cd4 Dashboard: Show Purchase data in bridge tx page, add tx status card in project>bridge page (#8251)
<!--

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

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

## Notes for the reviewer

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

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR primarily focuses on enhancing the transaction status viewing capabilities in the application. It introduces new components and modifies existing ones to improve user experience and functionality related to transaction data.

### Detailed summary
- Added `ViewTxStatus` component for viewing transaction status.
- Updated `Page` component to include `ViewTxStatus` and adjust layout.
- Improved error handling for transaction retrieval.
- Introduced `WithPurchaseData` story for `BridgeStatus`.
- Enhanced `BridgeStatus` to display purchase data if available.
- Modified `RouteDiscovery` to streamline form submission and error handling.

> ✨ 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 “View Transaction Status” tool with chain selector, hash validation, and link to results in a new tab.
  * Transaction page shows a “Purchase Data” section with formatted JSON when available.

* **Bug Fixes**
  * More graceful handling for missing/invalid transactions/receipts with an inline “Transaction not found” message.

* **UI**
  * Reworked Route Discovery into an inline form with clearer error display and updated spacing.

* **Tests**
  * Added a Storybook variant showcasing purchase data on the Completed status.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-10-14 18:37:48 +00:00
d4mr c9a0c23c6c [dashboard] add solana policies to vault access token (#8239)
<!-- start pr-codex -->

## PR-Codex overview
This PR focuses on adding new metadata patterns and required metadata patterns for various transaction types related to `solana`, enhancing the functionality of the `vault.client.ts` file.

### Detailed summary
- Added new `metadataPatterns` for `solana:read`, `solana:create`, `solana:signTransaction`, and `solana:signMessage` types.
- Included `requiredMetadataPatterns` for `solana:create` type.
- Each pattern includes `projectId`, `teamId`, and a fixed `type` of `"server-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

* **New Features**
  * Added Solana-specific permissions to access tokens.
  * Wallet tokens now support solana:read, solana:create, solana:signTransaction, and solana:signMessage.
  * Management tokens now support solana:read and solana:create.
  * Permissions can be scoped via metadata (team, project, server-wallet) for finer control.
  * No changes to existing flows or error handling; new scopes are available during token creation.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-10-14 18:06:44 +00:00
MananTank 43b0680124 [MNY-242] Dashboard: Remove animated background from bridge page (#8219)
<!--

## 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 code cleanup and improvements in the `apps/dashboard` project. It removes unused components and makes adjustments to existing components for better styling and functionality.

### Detailed summary
- Deleted `DarkVeil` and `BridgeVeil` components.
- Adjusted the styling of the `PageHeader` component.
- Modified button size and styles in `BuyAndSwapEmbed`.
- Updated `BridgePage` component layout and header styles.
- Improved `DataPill` component styles.
- Refined `DotsBackgroundPattern` visibility and styles.
- Updated dependency versions in `package.json` and `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

* **Style**
  * Removed animated veil background and simplified header visuals; adjusted responsive typography, spacing, borders, paddings, and colors for improved light/dark contrast.
  * Added a small dot icon beside the first pill on mobile, improved pill layout and muted pill text color on larger screens.
  * Made tab buttons smaller and refined active-state styling.

* **Refactor**
  * Removed legacy veil background components to simplify the UI.

* **Chores**
  * Removed an unused graphics dependency from the dashboard.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-10-13 21:44:06 +00:00
MananTank cccbde9a2e [MNY-240] Dashboard: Show bridge status info on transaction page if available (#8241)
<!--

## 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 tabbed interface for bridge status and overview details in the transaction view. It enhances user experience by allowing easy navigation between different states of a bridge transaction.

### Detailed summary
- Added `BridgeAndOverviewTabs` component for tab navigation.
- Implemented `BridgeStatus` component to display bridge transaction status.
- Created stories for different bridge statuses (`Completed`, `Pending`, `Failed`, `NotFound`).
- Enhanced `Page` component to include bridge status with polling.
- Refactored transaction details layout for better clarity.

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

<!-- end pr-codex -->
2025-10-13 21:28:49 +00:00
MananTank 85a6afc377 [MNY-249] Dashboard: /<chain>/tx/<tx-hash> page UI improvements (#8234)
<!--

## 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 `page.tsx` component for transaction details in the dashboard, improving its layout and user experience by introducing new components and restructuring sections for better clarity and functionality.

### Detailed summary
- Added `Breadcrumb` for navigation.
- Replaced static text with `CopyTextButton` for transaction hash, from, and to addresses.
- Introduced `GridItem` component for consistent layout of transaction details.
- Enhanced transaction details display with tooltips and icons.
- Improved formatting of timestamps and values.
- Added sections for transaction fee, gas prices, and burnt fees.
- Included input data display using `PlainTextCodeBlock`.

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

<!-- end pr-codex -->
2025-10-13 21:28:49 +00:00
MananTank e74aca9c90 [MNY-239] Dashboard: Show webhook sends for Bridge webhooks (#8213)
<!--

## 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 payment and webhook functionalities in the dashboard application by adding `authToken` support across various components and improving the webhook management interface.

### Detailed summary
- Added `authToken` prop to multiple components including `PayAnalytics`, `PaymentHistory`, and `QuickStartSection`.
- Enhanced `getWebhooks`, `createWebhook`, `deleteWebhook`, and `updateWebhook` functions to accept `authToken`.
- Improved the webhook management UI with `WebhookCard` and related functionalities.
- Added dialog components for creating and editing webhooks with validation.
- Updated API calls to include `authToken` for authentication in requests.

> ✨ 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**
  - Card-based webhooks UI with create/edit modal, per-card actions, delete confirmation, webhook detail page, and webhook sends viewer (history, filtering, pagination, resend).
  - Payments area: payment links and payments viewer wired to authenticated CRUD and paginated payments list; resend and webhook-send actions supported.

- **Enhancements**
  - authToken propagated across webhooks, payments, payment links, fees, and related components to enable authenticated operations.
  - Webhook objects no longer expose secret fields; create/update/delete now return usable payloads.

- **Documentation**
  - Storybook stories added for webhook sends UI (loading, results, error, resend scenarios).
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-10-13 21:28:49 +00:00
MananTank b0cac940bd Dashboard: Add block_timestamp_from param to useTokenTransfers (#8121)
<!--

## 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 `useTokenTransfers` functionality by adding a time filter for token transfers and improving the refetching logic based on error states.

### Detailed summary
- Introduced a constant `THIRTY_DAYS_AGO` to represent the timestamp 30 days prior.
- Added a search parameter `block_timestamp_from` to the `url` with the calculated timestamp.
- Changed `refetchInterval` to conditionally return `false` if there's an error in the data.

> ✨ 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**
  * Token transfer history now defaults to the last 30 days for more relevant results.

* **Bug Fixes**
  * Auto-refresh pauses when an error occurs, preventing repeated failures and unnecessary requests.
  * Auto-refresh resumes normal operation once the error clears, keeping data up to date without manual refresh.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-10-13 20:20:30 +00:00
MananTank 1a19e14845 SDK: Fix getNFT tests, Playground: Fix build (#8242)
<!--

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

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

## Notes for the reviewer

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

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR primarily focuses on updating the `tokenAddress` format in the tests and modifying the instantiation of the `thirdwebAI` and `thirdwebClient` to use a consistent `SECRET_KEY` variable for better readability and maintainability.

### Detailed summary
- Updated `tokenAddress` format in `getNFT.test.ts` and `getNFTs.test.ts`.
- Changed `thirdwebAI` instantiation in `route.ts` to use `SECRET_KEY` variable.
- Modified `thirdwebClient` instantiation in `paywall/route.ts` to use `SECRET_KEY` variable.
- Maintained existing environment variable usage for wallet addresses and API URLs.

> ✨ 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 public GET paywall endpoint.

* **Tests**
  * Updated NFT test snapshots to correct token address casing.
  * Removed an obsolete single-line test comment.

* **Chores**
  * API handlers now initialize needed services per request and read required environment secrets.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-10-13 19:58:30 +00:00
YashandGitHub 464af6a54c Stylus CLI updates (#8235) 2025-10-14 00:11:33 +07:00
Joaquim VergesGitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
f3c55d7139 Version Packages (#8240)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-10-11 19:31:58 +07:00
Prithvish BaidyaandGitHub 9545b43675 Engine SDK updates (#8238) 2025-10-11 19:24:30 +07:00
Joaquim VergesGitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
869bbcfa68 Version Packages (#8237)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-10-11 07:42:22 +07:00
Prithvish BaidyaandGitHub 35c7d21e36 [Vault SDK] Add Solana operations support (#8168) 2025-10-11 07:37:27 +07:00
MananTank edf3aad8c4 [BLD-396] Dashboard: Fix changing tabs in /nfts/<id> page shows full page loading indicator (#8224)
<!--

## 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 `TokenIdPage` component by introducing `Suspense` for improved loading state handling and adjusting the layout of the loading spinner.

### Detailed summary
- Added `Suspense` for handling loading states.
- Updated the loading spinner layout with additional padding (`py-20`).
- Wrapped the tab rendering logic within the `Suspense` component for better user experience during 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
  - Introduced a loading experience for NFT tab content using Suspense, providing a fallback UI while content loads.
  - Improves responsiveness when switching tabs by deferring heavy content until ready.

- Style
  - Increased vertical padding for the pending NFT state to enhance visual spacing and readability.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-10-09 22:14:06 +00:00
Joaquim VergesGitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
ab4190e23a Version Packages (#8230)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-10-09 14:19:19 -07:00
Joaquim VergesandGitHub 9086cdba20 [Playground] Add payTo parameter to X402 payment API (#8231) 2025-10-09 14:19:00 -07:00
Joaquim VergesandGitHub 0082e996ca [SDK] Add x402 recipientAddress to payment request and replace deprecated payTo (#8226) 2025-10-09 13:51:19 -07:00
Joaquim VergesGitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
28955e0ac5 Version Packages (#8229)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-10-10 03:07:52 +07:00
FirekeeperandGitHub 05550d045f Changeset for a0c45cb25d (#8228) 2025-10-10 03:01:01 +07:00
FirekeeperandGitHub a0c45cb25d Export ERC7702 MinimalAccount helpers and events (#8227) 2025-10-10 02:57:14 +07:00
MananTank 6fc4d838ff [MNY-246] Dashboard: Add Buy button in tokens table, update supported tokens count (#8222)
<!--

## 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 token-related information and enhancing the `TokensTable` component in the dashboard, including the increase in the number of supported tokens and the addition of a buy button for each token.

### Detailed summary
- Updated token count from `3200+` to `4500+` in `page.tsx`.
- Changed description in `page.tsx` to reflect the new token count.
- Modified `TokensTable` to display the updated token count.
- Added a new button in `TokensTable` for buying tokens, including a link to the token's 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 Buy action column with a button in the tokens table, including proper skeleton alignment for the new column.

- Style
  - Rounded token avatars for a refined look.
  - Simplified token name display by removing inline linking.

- Documentation
  - Updated copy to reflect “4500+ tokens” on the tokens page and bridge page, including Open Graph/description and UI labels.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-10-09 18:42:18 +00:00
MananTank fe940b3c66 [BLD-395] Dashboard: Show error UI when parsing CSV fails (#8223)
<!--

## 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 improves error handling in the `useCsvUpload` hook by adding a `try-catch` block around the CSV parsing logic. It ensures that if an error occurs during parsing or if the parsed data lacks an address, the component sets an error state and logs the error.

### Detailed summary
- Introduced a `try-catch` block around the CSV parsing logic.
- Added error handling to set `noCsv` to `true` if the parsed data's first entry does not have an `address`.
- Ensured that any errors during parsing are logged to the console.

> ✨ 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**
  * Prevents crashes during CSV uploads by handling parse errors gracefully.
  * Ensures invalid or malformed CSV files are detected and flagged without updating data.
  * Maintains existing behavior for valid files, only updating data when required fields are present.
  * Improves reliability of the upload flow with clearer invalid-file handling.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-10-09 18:31:34 +00:00
FirekeeperandGitHub 080d7f2122 Refactor ProjectFTUX API key integration section (#8225) 2025-10-10 00:08:54 +07:00
FirekeeperandGitHub ae75073a4a Refactor project wallet setup and controls UI (#8220) 2025-10-09 22:09:20 +07:00
Joaquim VergesGitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
781a626461 Version Packages (#8217)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-10-09 01:27:39 -07:00
Joaquim VergesandGitHub f630912b9b [SDK] Export missing x402 types (#8218) 2025-10-09 01:14:37 -07:00
163ab8afe1 Project Wallet (Default Server Wallet) (#8212)
Co-authored-by: Joaquim Verges <[email protected]>
2025-10-08 20:57:32 +07:00
MananTank 62cfbb7441 SDK: Show error in BuyWidget and SwapWidget UI if fetching token details fails (#8215)
<!--

## 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 error handling in the `BuyWidget` and `SwapWidget` UIs, displaying appropriate error messages when fetching token details fails.

### Detailed summary
- Added `isError` state to handle errors in token fetching.
- Updated `SelectedTokenButton` to check for errors before rendering.
- Enhanced `SwapUI` to show error messages based on different query states.
- Implemented error display in `FundWallet` for unsupported tokens.
- Modified balance display logic to accommodate new error handling.

> ✨ 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
  - Inline error banners in Buy and Swap widgets when token details fail to load.
  - Clear, specific messages for quote, buy-token, and sell-token fetch failures.
- Improvements
  - Selected token UI now falls back gracefully on errors instead of showing partial data.
  - Balance display uses richer token info (value, decimals, symbol, name) for accuracy.
  - More consistent handling of unsupported tokens across the flow.
- Bug Fixes
  - Prevents stale token icons/details from appearing during error states.
  - Disables auto-retry on token price fetches to reduce UI flicker.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-10-07 21:36:48 +00:00
Joaquim VergesandGitHub d7104f69f4 [Dashboard] Add engineCloud to ActiveStatus and reduce revalidation time to 30 seconds (#8216) 2025-10-07 14:22:32 -07:00
Joaquim VergesandGitHub 23545cb999 [Docs] Add ERC-7702 Smart EOA session keys documentation (#8214) 2025-10-07 13:35:46 -07:00
MananTank f9283f389b Portal: Add missing redirects for bridge pages (#8201)
<!--

## 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 redirect paths in the `redirects.mjs` file to streamline payment-related navigation by adding new routes that point to a `bridge` structure.

### Detailed summary
- Added redirect for `"/payments/sell"` to `"/bridge/sell"`
- Added redirect for `"/payments/swap"` to `"/bridge/swap"`
- Added redirect for `"/payments/tokens"` to `"/bridge/tokens"`
- Added redirect for `"/payments/routes"` to `"/bridge/routes"`

> ✨ 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 redirects so payment URLs now route to corresponding bridge destinations:
    * /payments/sell → /bridge/sell
    * /payments/swap → /bridge/swap
    * /payments/tokens → /bridge/tokens
    * /payments/routes → /bridge/routes
  * Existing bookmarks and shared links to payment pages will continue working and automatically route to the updated locations, with no UI changes required.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-10-07 11:30:51 +00:00
Joaquim VergesGitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
9fbb29dc25 Version Packages (#8202)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-10-07 02:05:50 -07:00
Joaquim VergesandGitHub 71a1e58eaf [AI-SDK-provider] Add default in-memory session store with custom store option (#8211) 2025-10-07 02:00:38 -07:00
MananTank 68b48f4faa [MNY-232] SDK: Fix ExecutingTxScreen missing back button in TransactionWidget (#8193)
<!--

## 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 `onBack` functionality in the `TransactionWidget` and `TransactionModal` components, ensuring that the back navigation behavior is more consistent across these components.

### Detailed summary
- In `TransactionWidget.tsx`, `onBack` now sets the screen to `init-ui`.
- In `TransactionModal.tsx`, `onBack` sets the screen to `deposit`.
- In `ExecutingScreen.tsx`, the `onBack` prop is changed from optional to required.

> ✨ 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 back navigation from the executing transaction screen, allowing users to return to the previous step without closing the modal.
  * Unified back-button behavior across transaction flows for a more consistent experience.
  * Provides clearer control during long-running executions with an explicit back option.
  * Navigation enhancement applies to both bridge and transaction modal flows, improving step-to-step control while keeping transaction processing unchanged.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-10-06 22:33:31 +00:00
MananTank 1c660d4712 [MNY-202] Dashboard: Update resource links in project > bridge page (#8196)
<!--

## 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 `QuickstartSection.client.tsx` and `page.tsx` files in the bridge section of the dashboard. It modifies features, links, and documentation references to align with the new bridge functionality.

### Detailed summary
- Updated `setupTime` to `5` in `QuickstartSection.client.tsx`.
- Changed `features` from `["Swap any token", "Cross-chain swaps"]` to `["Swap any token", "Cross-chain swap"]`.
- Updated link `href` from `https://portal.thirdweb.com/payments/swap` to `https://portal.thirdweb.com/bridge/swap` and changed label to "Setup Swap".
- Added new documentation link to `page.tsx`: `https://portal.thirdweb.com/bridge`.
- Added new playground link to `page.tsx`: `https://playground.thirdweb.com/bridge/swap-widget`.
- Removed old commented-out documentation links related to payments.
- Removed several links under the footer sections for payments and replaced them with new links relevant to the bridge.

> ✨ 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 direct link to Bridge docs in the header for quicker access.
  - Added a Playground link in the header to enable easy testing of the Bridge swap widget.
- Style
  - Removed footer sections from the Bridge page to streamline the layout and reduce clutter.
- Chores
  - Updated the “Setup Swap” feature card and its link to point to the Bridge setup URL for improved navigation.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-10-06 21:22:22 +00:00
MananTank 30b5c30ba0 [PRO-27] Dashboard: copy changes in bridge page (#8191)
<!--

## 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 `description` in the `page.tsx` file for the `bridge` component, enhancing its clarity and accuracy regarding token capabilities. It also modifies the text of a `PillLink` to better reflect its functionality.

### Detailed summary
- Updated the `description` to specify "3,200+ tokens" instead of "9M+ token routes".
- Changed the `PillLink` text from "Integrate Bridge in your apps in minutes, and start generating revenue" to "Accept Payments in Any Token Right Inside Your App".

> ✨ 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 page metadata to reflect 3,200+ tokens across 85+ chains, with clearer phrasing around finality and pricing for improved SEO and share previews.

- **Style**
  - Refined call-to-action on the Bridge page to “Accept Payments in Any Token Right Inside Your App,” improving clarity and alignment with user goals.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-10-06 21:12:26 +00:00
MananTank 86461f9a07 [PRO-25] Dashboard: Update bridge page header to show supported tokens, chains and routes count (#8195)
<!--

## 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 content of the `Page` component in the `page.tsx` file by removing a `Link` element and replacing it with a `p` element that provides information about token and chain support.

### Detailed summary
- Removed the `Link` component that directed users to the thirdweb bridge.
- Added a `p` element stating "85+ chains, 3200+ tokens and 9+ million routes supported".

> ✨ 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**
  - Updated the Tokens dashboard header to display marketing stats: “85+ chains, 3200+ tokens, and 9+ million routes supported.”
  - Replaced the previous header link with a plain text paragraph for a cleaner presentation.
  - No changes to data, performance, or underlying functionality.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-10-06 20:01:56 +00:00
MananTank 2ebba6139a [MNY-235] Dashboard: Fix payment page theme issues (#8199)
<!--

## 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 integrating the `ThemeProvider` from `next-themes` into multiple components within the payment module, enhancing theming support across the application.

### Detailed summary
- Removed `ThemeProvider` from `PayLayout` in `layout.tsx`.
- Added `ThemeProvider` wrapping to `PayPage` in `page.tsx`.
- Integrated `ThemeProvider` in `PayPageWidget` to manage theme settings.
- Updated theme handling logic in `PayPageWidget` to use `getSDKTheme`.
- Wrapped the main structure in `ThemeProvider` in `[id]/page.tsx`.

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

<!-- end pr-codex -->

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

## Summary by CodeRabbit

- New Features
  - Consistent light/dark theme across all Pay pages, with support for forcing a theme per link and reduced visual flicker from system theme changes.
- Style
  - Refined header layout: project image, name, and description grouped; “Secured by thirdweb” badge retained.
  - Improved details on large screens: clearer “Network” label with icon and chain name; seller shown as ENS or shortened address, reordered for readability.
- Refactor
  - Consolidated theming at the page level for consistent behavior across views.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-10-06 19:51:09 +00:00
MananTank f3bacc16cb [MNY-203] Dashboard: ERC20 token page title, metadata and OG updates (#8194)
<!--

## 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 logic for generating `title` and `description` variables in the `shared-layout.tsx` file, particularly for different types of smart contracts. It enhances the information displayed for ERC20 contracts and adds fallback values for when contract metadata is not available.

### Detailed summary
- Changed `const title` to `let title` to allow reassignment.
- Updated `title` and `description` for `ERC20` contracts using `contractMetadata`.
- Added fallback values for `title` and `description` when an error occurs, including `openGraph` properties.

> ✨ 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**
  * Richer link previews for contract pages via Open Graph metadata, improving social sharing and discoverability.
  * More accurate, branded titles and descriptions: ERC20 pages use token name and symbol; non-ERC20 pages use contract name, with references to Buy, Swap, Bridge & Price.
  * Robust fallbacks ensure meaningful titles, descriptions, and previews even if metadata loading fails.
  * Consistent metadata across success and fallback states for a more reliable sharing experience.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-10-06 19:41:13 +00:00
MananTank 968913d55b [MNY-231] SDK: Fix TransactionWidget not updating on currency prop change after initial render (#8192)
<!--

## 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 fixing the `TransactionWidget` to ensure it updates correctly when the `currency` prop changes after the initial render. It also refines how the total cost and display values are calculated based on the selected currency.

### Detailed summary
- Updated `PaymentOverview` to remove the `currency` prop from `TransactionOverViewCompact`.
- Enhanced `TransactionPayment` to calculate `totalFiatCost` and `costToDisplay` based on the selected currency.
- Adjusted the display of USD value to use `costToDisplay`.
- Removed the `currency` type from `UseTransactionDetailsOptions`.
- Simplified `useTransactionDetails` by removing currency-related calculations.

> ✨ 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**
  * Transaction widget now updates correctly when the selected currency changes after initial render.
  * Payment and transaction details display accurate fiat totals, with graceful fallback when pricing data is unavailable.

* **Improvements**
  * Streamlined pricing display to focus on the selected currency, improving clarity and consistency across payment views.

* **Chores**
  * Added a patch-level changeset entry documenting the bug fix.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-10-06 19:22:28 +00:00
MananTank bd70db4a5d [MNY-211] Dashboard: Add chainId and is_tesnet properties in reported events (#8198)
<!--

## 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 handling of testnet-related functionality across various components in the application. It introduces the `is_testnet` and `isTestnet` props to multiple components and functions, ensuring that the application can properly differentiate between mainnet and testnet environments.

### Detailed summary
- Added `isTestnet` prop to `UniversalBridgeEmbed`.
- Introduced `is_testnet` to the `create-nft-page` and `launch-token` components.
- Updated `ChainPageView` to accept `is_testnet`.
- Included `is_testnet` in the `BuyFundsSection` and various asset page components.
- Modified `reportAssetPageview` and `reportChainPageview` to include `is_testnet`.
- Enhanced `reportContractDeployed` to capture `is_testnet` and chain ID.
- Adjusted functions in `report.ts` to utilize `is_testnet` for analytics tracking.

> ✨ 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
  - Analytics now capture testnet status across chain and asset pageviews, buy flows, contract deployments, and token/NFT creation.
  - Asset creation reports now include chain ID on success and failure for improved tracking accuracy.
- Chores
  - Standardized analytics event reporting to pass complete property objects, ensuring consistent payloads.
  - Propagated testnet indicators through relevant components to enrich analytics without altering UI or workflows.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-10-06 19:12:07 +00:00
MananTank d9a138a90a [MNY-236] Portal: Update BridgeWidget script docs to reflect new typescript type (#8200)
<!--

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

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

## Notes for the reviewer

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

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on making the `buy` properties optional in the configuration for the "Buy" tab UI, enhancing flexibility by allowing users to omit certain parameters. It also updates the documentation to reflect these changes.

### Detailed summary
- Changed `buy` properties (`amount`, `chainId`) from required to optional in type definitions.
- Updated documentation to remove "Required" tags for `amount` and `chainId`.
- Clarified descriptions for `amount`, `chainId`, and `tokenAddress` in the 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**
  * Buy configuration is now optional in the bridge widget; amount and chain selection can be omitted and will use defaults.
  * Removed forced preset buy values, enabling more flexible initialization.

* **Documentation**
  * Updated docs to reflect optional Buy fields and default behaviors.
  * Revised examples and type descriptions to indicate optional amount and chain settings.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-10-06 19:06:30 +00:00
Joaquim VergesandGitHub 777aed1e5f [Docs] Add metadata to x402 documentation pages (#8190) 2025-10-06 04:15:16 -07:00
Joaquim VergesandGitHub fbed4be959 [Docs] Update MCP tool filtering to use comma-separated list (#8189) 2025-10-06 04:07:32 -07:00
Joaquim VergesandGitHub 5a26217a43 [Docs] Add MCP tool filtering, agent examples, and available tools list (#8188) 2025-10-06 03:23:24 -07:00
Joaquim VergesGitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
938c1e2f60 Version Packages (#8185)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-10-05 01:36:29 +13:00
Joaquim VergesandGitHub af9a73d930 [SDK] Fix waitUntil facilitator parameter not being respected (#8184) 2025-10-04 05:06:10 -07:00
Joaquim VergesGitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
34c5d24043 Version Packages (#8182)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-10-04 19:33:33 +13:00
MananTank e612f16a85 [MNY-192] SDK: BuyWidget Improvements (#8181)
<!--

## 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 enhancements to the payment features, including optional props for token and chain selection, improvements to UI components, and the addition of a receiver address input for transactions.

### Detailed summary
- Added `receiverAddress` to various components.
- Made `amount` and `chainId` optional in `BuyWidget`.
- Improved UI for `BuyWidget`, allowing token and chain selection.
- Updated `FundWallet` to handle token selection and input.
- Enhanced `DecimalInput` for better user experience.
- Refactored components for better code organization 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

* **New Features**
  * Receiver address input for Buy/Pay flows.
  * Editable fiat and token amounts, current balance visible, and wallet disconnect option.
  * Copy icon supports configurable size.

* **Improvements**
  * Chain and amount can be optional; token and chain remain changeable.
  * Enhanced token selection, preset amount buttons, compact active-wallet header, improved theming and responsive layouts.
  * Added story variants (light/dark).

* **Documentation**
  * Examples updated to reflect buy prefill changes.

* **Style**
  * Simplified address text styling.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-10-03 23:52:49 +00:00
Jonas DanielsandGitHub 4a30b9c89a Add Google Click ID tracking to Stripe checkout (#8180) 2025-10-03 15:05:20 -07:00
MananTank ef9cf3409b SDK: Delete Bridge Orchestrator, Refactor Bridge widget components (#8169)
<!--

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

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

## Notes for the reviewer

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

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR primarily focuses on refactoring and updating various components related to payment and transaction handling in the `thirdweb` library. It introduces new props, modifies existing interfaces, and cleans up the codebase by removing unused files.

### Detailed summary
- Deleted several unused files related to payment and transaction handling.
- Updated `transaction` and `payment` components to use new props and interfaces.
- Replaced `type` definitions with `interface` in some components for consistency.
- Improved error handling and state management in transaction modals.
- Enhanced `SuccessScreen` and `UnsupportedTokenScreen` with new props for better UI.
- Modified various story files to reflect component updates and new props.
- Adjusted payment selection and fund wallet components to align with new data structures.

> The following files were skipped due to too many changes: `packages/thirdweb/src/stories/Bridge/CheckoutWidget.stories.tsx`, `packages/thirdweb/src/stories/Bridge/PaymentSelection.stories.tsx`, `packages/thirdweb/src/stories/Bridge/fixtures.ts`, `packages/thirdweb/src/react/web/ui/Bridge/payment-details/PaymentOverview.tsx`, `packages/thirdweb/src/stories/Bridge/PaymentDetails.stories.tsx`, `packages/thirdweb/src/react/web/ui/Bridge/TransactionWidget.tsx`, `packages/thirdweb/src/react/web/ui/Bridge/BuyWidget.tsx`, `packages/thirdweb/src/react/web/ui/Bridge/CheckoutWidget.tsx`

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

<!-- end pr-codex -->

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

* **New Features**
  * Optional fiat currency display across payment & transaction modals (defaults to USD).
  * Token support detection with a dedicated unsupported-token screen.

* **Refactor**
  * Bridge flows reorganized into modular multi-step UIs (selection, quote, preview, execute, success).
  * Components now use explicit metadata (title/description/image) and explicit currency/mode props; branding handling centralized.

* **Documentation**
  * Storybook reorganized: decorator-based theming, updated/added widget stories, and cleaned-up story exports.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-10-03 15:36:06 +00:00
Joaquim VergesandGitHub fd20a6cabb [SDK] Deprecate payTo parameter and simplify facilitator implementation (#8177) 2025-10-03 02:32:59 -07:00
Jonas DanielsandGitHub 9095041ea7 Fix invoice status logic and refactor billing components (#8175) 2025-10-02 20:44:22 -07:00
Joaquim VergesandGitHub 2dbb9e2de4 [Dashboard] Move server wallet sections to wallets tab (#8174) 2025-10-02 18:00:43 -07:00
Joaquim VergesandGitHub 00b8adf3a2 [Docs] Simplify x402 documentation (#8173) 2025-10-02 15:02:17 -07:00
Joaquim VergesGitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
ae3dfa36c3 Version Packages (#8170)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-10-02 16:08:01 +13:00
Joaquim VergesandGitHub eb603f8908 [Multi] Update dependencies, improve wagmi adapter (#8167) 2025-10-02 15:42:13 +13:00
Joaquim VergesandGitHub e11b0da20e [Dashboard] Update transaction status endpoint path (#8166) 2025-09-30 16:10:00 -07:00
Joaquim VergesandGitHub 4c51fe6a8f [Dashboard] Add transaction filtering by Queue ID to engine v2 (#8163) 2025-09-30 15:53:15 -07:00
MananTank 64034c0ddb Dashboard: Fix Token Rewards UI not accounting for token decimals (#8164)
<!--

## 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 handling of token rewards by adding `decimals` properties for tokens in various components and improving the retrieval of metadata related to tokens.

### Detailed summary
- Added `decimals` property to `token0` and `token1` in `claim-rewards-page.stories.tsx`.
- Introduced `chainMetadata` in `page.tsx`.
- Updated `ClaimRewardsPageUI` to utilize `decimals` for token amounts.
- Refactored `getUnclaimedFees` to fetch `decimals` and `symbol` for tokens.
- Improved token contract retrieval logic in `getUnclaimedFees`.

> ✨ 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
  - Token amounts now display using each token’s specific decimals.
  - Chain metadata is passed to unclaimed-fees retrieval to source native token symbol and decimals.

- Bug Fixes
  - Removed hardcoded 18-decimal assumption so amounts render correctly for tokens with different precisions.

- Tests
  - Story/test stubs updated to include decimals for token objects for more realistic scenarios.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-09-30 22:37:28 +00:00
Jonas DanielsandGitHub 90c1e576ff Add /payments/x402 to framer-rewrites (#8165) 2025-09-30 15:27:56 -07:00
Joaquim VergesandGitHub 27d5a81c49 [Dashboard] Add transaction filtering by Queue ID and wallet address (#8162) 2025-09-30 15:15:10 -07:00
MananTank dbadc23e04 Dashboard: Disable pay modal in ERC1155 set claim conditions if cost estimation is already done in dashboard (#8161)
<!--

## 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 logic for handling transaction confirmations in the `create-nft-page.tsx` file, specifically addressing scenarios with multiple batches.

### Detailed summary
- Added a conditional check for `totalBatches > 1`.
- If true, it uses `sendAndConfirmTxNoPayModal.mutateAsync(tx)` for transaction confirmation.
- If false, it defaults to `sendAndConfirmTx.mutateAsync(tx)` for single batch transactions.

> ✨ 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**
  * Streamlined multi-batch transaction confirmation when configuring NFT claim conditions. When multiple batches are required and a wallet is connected, a no-payment confirmation modal is used to reduce prompts and improve reliability. Single-batch flows continue to use the standard confirmation path.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-09-30 19:01:52 +00:00
Joaquim VergesandGitHub 21d5d0b6af [Nebula] Add permanent redirect to thirdweb.com/ai (#8155) 2025-09-30 11:42:56 -07:00
MananTank b2239c91c9 [MNY-214] Dashboard: ERC1155 set claim conditions step UX improvements (#8146)
<!--

## 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 functionality for handling insufficient funds when creating NFTs, enhancing the user experience by allowing users to buy funds directly if their balance is low.

### Detailed summary
- Added `onNotEnoughFunds` callback to handle insufficient funds.
- Implemented logic to calculate total transaction costs.
- Integrated `BuyWidget` to enable users to purchase funds directly.
- Enhanced error handling and user feedback for fund-related issues.

> ✨ 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
  - Gas-cost–aware NFT minting with total cost estimation and balance pre-checks.
  - Insufficient funds handling with a user-facing prompt showing required vs. available balance.
  - Integrated Buy Funds widget with theme and chain awareness, plus retry and back controls.
  - Optional transaction path that skips the pay modal in eligible scenarios.
  - Enhanced batch processing for ERC1155/721 flows with a callback to surface funding issues.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-09-30 14:51:10 +00:00
Joaquim VergesGitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
ca701a48c2 Version Packages (#8143)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-09-29 18:11:08 -07:00
Joaquim VergesandGitHub 94aae902a1 [SDK] Expose waitUntil parameter in settlePayment() (#8153) 2025-09-29 17:32:59 -07:00
MananTank 0c7149eb44 Portal: remove underlines from links in changelog page (#8148)
<!--

## 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 styles for links within the `changelog-page` in the `styles.css` file, specifically removing certain text decoration properties.

### Detailed summary
- Removed the text decoration properties (`text-decoration`, `text-decoration-color`, `text-decoration-style`, `text-underline-offset`, `transition`) from the `.changelog-page a` selector.

> ✨ 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**
  * Updated link appearance on the changelog page for consistency with global styles. Regular links no longer use custom underlines or color transitions, resulting in a cleaner look and improved readability. Headings remain without underlines, and hover behavior is unchanged. Users should notice a more consistent visual experience across the page without functional changes.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-09-29 23:48:49 +00:00
Joaquim VergesandGitHub ce9220ea00 [Docs] Add comprehensive x402 payment documentation (#8149) 2025-09-30 12:27:40 +13:00
FirekeeperandGitHub 99c2af5e25 Add ecosystem-specific chain config support (#8147) 2025-09-30 04:21:49 +07:00
MananTank 7495f46222 [MNY-213] SDK/SwapWidget: token search improvements (#8145)
<!--

## 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 token search functionality in the `SwapWidget` by incorporating a debounce mechanism to optimize search input handling and refining the token filtering logic.

### Detailed summary
- Updated `use-tokens.ts` to use `query` and `tokenAddress` based on search conditions.
- Added `useDebouncedValue` hook in `select-token-ui.tsx` for debouncing the search input.
- Changed token filtering logic to utilize `debouncedSearch` instead of raw `search` value.

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

<!-- end pr-codex -->
2025-09-29 19:25:57 +00:00
MananTank b8afa98de6 [MNY-212] SDK: Set refetch: false on token list queries in SwapWidget (#8144)
<!--

## 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 loading experience in the `SwapWidget` by removing the retry mechanism when fetching the list of tokens, which reduces the time skeletons are displayed in the UI.

### Detailed summary
- Removed the `retry` option from the token fetching query in `use-tokens.ts`.
- The `retry` option is set to `false` for both the tokens query and the token balances query.
- This change aims to decrease loading times and improve UI responsiveness.

> ✨ 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 SwapWidget responsiveness by removing automatic retries for fetching token lists and balances, reducing how long loading placeholders are shown. Errors surface sooner, enabling quicker user action.

* **Chores**
  * Added a changelog entry documenting the update.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-09-29 19:00:23 +00:00
Joaquim VergesandGitHub 496615feae [SDK] Improve token info discovery for x402 payments (#8142) 2025-09-29 01:56:53 -07:00
Joaquim VergesGitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
e2931df858 Version Packages (#8137)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-09-28 15:03:25 -07:00
Joaquim VergesandGitHub 85bde21dc9 [SDK] Handle 7702 accounts in verifyTypedData (#8141) 2025-09-29 10:32:20 +13:00
FirekeeperandGitHub 2c84ac4089 [Portal] .NET v3 & Unity v6 Documentation (#8140) 2025-09-28 00:44:00 +07:00
MananTank 5ee449fc9c Dashboard: Bridge page UI adjustments, add tracking (#8139)
<!--

## 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 and functionality of the dashboard and bridge components. It includes style updates, component restructuring, and the addition of an event reporting function for link clicks.

### Detailed summary
- Added `bg-background` class to the `canvas` in `DarkVeil.tsx`.
- Wrapped `FaqSection` in a `div` for padding in `page.tsx`.
- Introduced `reportBridgePageLinkClick` function in `report.ts` for event tracking.
- Simplified structure of `faq-section.tsx`.
- Created `PillLink` component in `pill-link.tsx` for better link handling and event tracking.
- Updated `BridgePage` to include new `PillLink` components and adjusted layout with spacing elements.

> ✨ 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 click tracking for Bridge page links.
  - Introduced pill-style external link buttons on the Bridge page.

- Refactor
  - Simplified FAQ markup to a single semantic section.
  - Extracted and reused a PillLink component across the Bridge page.
  - Reorganized Bridge page layout for clearer structure.

- Style
  - Adjusted vertical spacing and rhythm across Bridge, FAQ, and chain pages.
  - Made hero animation snappier.
  - Updated pill text color, background pattern accents, and veil canvas background.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-09-26 23:58:10 +00:00
MananTank a94e9c2396 [MNY-223] Update Bridge Page (#8138)
<!--

## 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 and new features to the `dashboard` application, including updates to the `Bridge` functionality, improvements in the UI components, and new utility functions for handling themes and FAQs.

### Detailed summary
- Added `ogl` dependency for graphics handling.
- Updated `ChainSeo` type in `chain-seo.ts` for better structure.
- Increased blockchain support from 80+ to 85+ in documentation.
- Enhanced `PageHeader` and `ToggleThemeButton` components for improved styling.
- Refactored `FaqSection` to accept a more flexible props structure.
- Introduced `BridgeVeil` component for visual effects based on themes.
- Added `DataPill` and `PillLink` components for better UI elements.
- Created a `BridgeFaqSection` with FAQs related to bridging.
- Improved `TokenPage` to manage filters and sorting more effectively.

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

<!-- end pr-codex -->
2025-09-26 22:09:35 +00:00
MananTank 64f5e474bb [MNY-216] Update SEO metadata on chain pages, Add FAQ section (#8135)
<!--

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

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

## Notes for the reviewer

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

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on refactoring and enhancing the codebase for better readability and functionality. It primarily involves renaming functions, updating component styles, and implementing a new SEO fetching mechanism.

### Detailed summary
- Renamed `getChainMetadata` to `getCustomChainMetadata`.
- Updated styles in several components for consistency.
- Added `fetchChainSeo` function for improved SEO handling.
- Replaced `chainMetadata` with `customChainMetadata` in various components.
- Introduced `FaqSection` component for displaying FAQs.
- Adjusted metadata generation to utilize fetched SEO data.

> ✨ 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
  - Chain pages now show an interactive FAQ section when SEO data includes FAQs.
  - SEO data is fetched and cached externally to provide richer titles, descriptions, and OpenGraph/Twitter previews.

- Bug Fixes / Behavior
  - Metadata generation now depends on external SEO (may be unavailable), affecting preview availability and link consistency.
  - Chain header images and gas-sponsored indicators now use the updated metadata source.

- Style
  - More compact Supported Products cards, adjusted overview spacing/corner radius, lighter explorer/info-item typography, and refined link styling.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-09-26 22:09:34 +00:00
MananTank aa0d23e966 SDK: Fix Embed widget container size, Dashboard: Fix Bridge embed UI width (#8136)
<!--

## 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 fixing the embed container size across various components in the `thirdweb` and `dashboard` applications, ensuring better layout and responsiveness.

### Detailed summary
- Updated `maxWidth` in `ConnectEmbed` to `100%` and adjusted `width` based on `modalSize`.
- Removed redundant `layout: "centered"` parameter in `ConnectEmbed` story.
- Simplified `UniversalBridgeEmbed` structure by removing unnecessary wrapping `div`.
- Added `min-w-0` class to `BuyAndSwapEmbed` and `GridPatternEmbedContainer` for better responsiveness.
- Adjusted layout classes in `GridPatternEmbedContainer` for improved alignment.

> ✨ 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 embed container sizing in the Connect Wallet modal to respect modal width and prevent viewport overflow.
  - Resolved overflow and min-width issues in embed containers for smoother responsive behavior.

- Style
  - Updated Bridge page layout to display components side-by-side on larger screens.
  - Improved responsive rounding and overflow handling around embed containers for cleaner visuals.

- Chores
  - Updated thirdweb dependency to the latest patch release.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-09-26 20:10:28 +00:00
MananTank b888413ca6 Dashboard: Fix BuyAndSwapWidget width (#8134)
<!--

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

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

## Notes for the reviewer

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

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on making minor adjustments to the layout and styling of components in the dashboard, specifically in `grid-pattern-embed-container.tsx` and `BuyAndSwapEmbed.tsx`.

### Detailed summary
- In `grid-pattern-embed-container.tsx`, the width class `sm:w-[420px]` was removed from the `div`, allowing it to take its natural width.
- In `BuyAndSwapEmbed.tsx`, the width class `!w-full` was removed from two instances of `SwapWidget`, maintaining the rounded border style but removing the full width constraint.

> ✨ 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
  * Buy and Swap widgets no longer force full-width, adapting to their container while preserving rounded, borderless styling.
  * Embed container relaxes small-screen width limits, allowing content to use available space more naturally.
  * Improves responsiveness, spacing, and alignment across devices, reducing unnecessary whitespace and preventing overstretched layouts.
  * Visual update only; no changes to widget behavior or controls.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-09-26 16:56:25 +00:00
MananTank 108a79ab3e Playground: Use Geist fonts (#8133)
<!--

## 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 integrating the `geist` font into the `playground-web` application, updating the `tailwind` configuration, and replacing existing font imports with `GeistSans` and `GeistMono`.

### Detailed summary
- Added `geist` dependency in `package.json`.
- Updated `tailwind.config.ts` to extend the theme with `geist` font families.
- Replaced `Fira_Code` and `Inter` font imports in `layout.tsx` with `GeistMono` and `GeistSans`.
- Updated the body class to use `GeistSans` and `GeistMono` 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

- Style
  - Updated global typography to Geist Sans and Geist Mono for a cleaner, consistent look.
  - Replaced previous Inter and Fira Code fonts with new Geist font variables in the app layout.
  - Configured Tailwind to use Geist-based sans and mono font families across the app.

- Chores
  - Added Geist font package as a new dependency.

- Notes
  - No functional changes; visual update limited to typography.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-09-26 11:47:00 +00:00
MananTank 51390f3af0 Playground: Fix ConnectEmbed width (#8132)
<!--

## 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 styling of components in the `playground-web` application by removing unnecessary CSS classes that limit width.

### Detailed summary
- In `styled-connect-embed.tsx`, removed the class `!max-w-full`.
- In `RightSection.tsx`, changed the class from `!max-w-full` to `shadow-xl`, removing the width constraint.

> ✨ 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**
  * Updated Connect modal styling for sign-in: removed restrictive max-width constraint to improve responsiveness and layout in the RightSection.
  * Simplified styling by removing custom class overrides from the styled Connect component, relying on default styles for more consistent appearance.
  * Result: more reliable modal sizing, better visual consistency, and cleaner presentation across themes and wallet variants.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-09-26 10:37:31 +00:00
Joaquim VergesGitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
fb8c95a325 Version Packages (#8130)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-09-25 19:24:09 -07:00
Joaquim VergesandGitHub cf02cbf3af [wagmi-adapter] Add onConnect callback (#8131) 2025-09-25 19:00:35 -07:00
Joaquim VergesandGitHub f152139d73 [SDK] Support ERC-2612 permit for x402 payments (#8128) 2025-09-25 18:21:58 -07:00
MananTank 16307019be Portal: Add BridgeWidget script docs (#8129)
<!--

## 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` functionality, updating UI components, and improving documentation for better user experience and clarity.

### Detailed summary
- Added `BridgeWidget Script` route in sidebar.
- Updated `Paragraph` and `Heading` components' margin classes.
- Revised `Details` component structure.
- Changed text references from `BuyWidget` to `SwapWidget`.
- Enhanced documentation for `BridgeWidget Script` with detailed integration instructions.
- Introduced new options for customizing the widget's appearance and behavior.

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

<!-- end pr-codex -->
2025-09-25 22:50:37 +00:00
MananTank 10fb338877 Portal: UI tweaks 2 (#8127)
<!--

## 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 enhancements and styling updates across various components in the application, including adjustments to CSS, layout changes, and the addition of a new package, `geist`. It aims to improve the overall appearance and functionality of the portal.

### Detailed summary
- Added `font-feature-settings` to `globals.css`.
- Updated `text-rendering`, `-webkit-font-smoothing`, and `text-size-adjust` in `global.css`.
- Included `geist` package in `package.json`.
- Modified layout and spacing in `vault/page.mdx`.
- Adjusted classes for `ApiEndpoint.tsx`, `Paragraph.tsx`, and `InlineCode.tsx`.
- Changed text color in `Subscribe.tsx`.
- Revised spacing and border styles in various components.
- Updated icons in `Anchor.tsx` and `ChatButton.tsx`.
- Enhanced styling of buttons and links across multiple components.
- Improved overall layout structure in `Header.tsx` and sidebar components.
- Refined search modal styles and item presentation in `DocSearch.tsx`.

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

<!-- end pr-codex -->

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

- Style
  - Switched site typography to Geist, enabled ligatures and improved text rendering.
  - Redesigned header with central branding, consolidated controls, mobile menu and subtle backdrop.
  - Refreshed visuals across search, footer, sidebar, platform selector, steps, cards, code/inline code, and mobile sidebar.
  - Updated icons (chat, next-page, anchors), spacing/typography tweaks; vault features now stack vertically.

- Refactor
  - Localized an article card component and reorganized footer/next-page flows; Edit link now wrapped in a button.

- Chores
  - Added Geist dependency and extended Tailwind font settings; global font/rendering tweaks.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-09-25 22:50:36 +00:00
Joaquim VergesGitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
95412c75b9 Version Packages (#8114)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-09-25 10:12:22 -07:00
MananTank 7bc513c770 [BLD-328] Portal: Update changelog page layout (#8124)
<!--

## 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 and code refactoring across various components in the application, enhancing styling consistency, accessibility, and overall user experience.

### Detailed summary
- Updated grid spacing in `Grid.tsx`.
- Increased max-width in `layout.tsx`.
- Modified border styles in multiple components.
- Enhanced button styles and variants in `chat-button.tsx` and `Subscribe.tsx`.
- Improved text styles for better readability.
- Refactored `AuthMethodsTabs.tsx` to use `Button` component.
- Adjusted `DocLayout.tsx` for more responsive design.
- Improved date formatting in `RenderData.tsx`.
- Added animations to `TableOfContents.tsx`.
- Updated feedback dialog in `Feedback.tsx` for better UX.
- General styling updates for consistency across 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

- Style
  - Widespread visual refinements: typography, spacing, tighter headings, dashed borders for dividers/images/code, smaller author avatar, absolute dates, wider content container, updated buttons (ghost/outline), adjusted icons and shadows, updated link/inline-code/grid/card styles, tabs and header/menu visual updates.
  - Footer, sidebar, docs search, subscribe, feedback, and table-of-contents presentational tweaks (including fade-in and conditional hiding).

- Refactor
  - Simplified changelog and post layout to a single-column header-first structure; removed changelog index TOC.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-09-25 08:00:13 +00:00
Joaquim VergesandGitHub 9069351e1a [SDK] Support Chain objects in x402 payment utilities (#8126) 2025-09-25 00:53:22 -07:00
MananTank 8f6363f4e4 SDK: Fix toUnits throwing error for values in scientific notation (#8112)
<!--

## 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 `toUnits` function to handle both scientific notation and fixed-point notation, ensuring accurate conversion of token values with specified decimals. It also adds tests to verify the new functionality.

### Detailed summary
- Modified `toUnits` function to check for scientific notation (e.g., "1e-18").
- Converted scientific notation to fixed-point notation using `Number(tokens).toFixed(decimals)`.
- Added tests for `toUnits` to cover various scientific notation cases, including negative and positive exponents.

> ✨ 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**
  * Unit conversion now accepts scientific notation (positive and negative exponents), normalizing values to the configured decimal precision for consistent fixed-point results.

* **Tests**
  * Added comprehensive tests covering positive and negative exponent cases to validate parsing and conversion from scientific notation.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-09-24 23:07:53 +00:00
MananTank a83104efff [MNY-200] SDK: Fix onSuccess callback not called when tx is sent in TransactionWidget (#8119)
<!--

## 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 simplifies the `handlePostBuyTransactionComplete` function in the `BridgeOrchestrator` component by removing the `quote` parameter and the associated call to `onComplete`. It streamlines the transaction completion process and adjusts the `closeModal` prop accordingly.

### Detailed summary
- Removed the `quote` parameter from the `handlePostBuyTransactionComplete` function.
- Eliminated the call to `onComplete?.(quote)` within the `handlePostBuyTransactionComplete` function.
- Updated the `closeModal` prop in the `ExecutingTxScreen` component to directly use `handlePostBuyTransactionComplete` without arguments.

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

<!-- end pr-codex -->
2025-09-24 21:43:56 +00:00
MananTank 7936ebb6e9 [MNY-180] SDK: Fix TransactionWidget not respecting currency prop (#8118)
<!--

## 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 handling of the `currency` property in various components and adding a new story for a transaction mode in light theme that showcases an ERC20 token transfer.

### Detailed summary
- Changed `currency` from optional to required in `useTransactionDetails.ts`.
- Added `currency` to the props in `PaymentOverview.tsx` and `TransactionPayment.tsx`.
- Introduced a new story `TransactionJPYCurrency` in `BridgeOrchestrator.stories.tsx` for displaying an ERC20 token transfer in light theme.

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

<!-- end pr-codex -->
2025-09-24 21:43:56 +00:00
MananTank 3b986d52e5 [MNY-201] SDK: Rename leftover of Universal Bridge to Bridge (#8117)
<!--

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

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

## Notes for the reviewer

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

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on renaming references from `Universal Bridge` to `Bridge` across multiple files to standardize terminology and improve clarity.

### Detailed summary
- Updated comments and documentation to replace `Universal Bridge` with `Bridge`.
- Modified error messages in `apps/dashboard/src/@/api/analytics.ts` to reflect the new terminology.
- Changed descriptions in various files related to `Chain`, `Token`, `Quote`, and webhooks to use `Bridge`.

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

<!-- end pr-codex -->
2025-09-24 21:43:56 +00:00
MananTank ee4e13e098 [MNY-198] SDK: Fix StepRunner autoStart not working reliably, Remove extra prepare call (#8116)
<!--

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

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

## Notes for the reviewer

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

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR primarily focuses on enhancing the `StepRunner` component in the Bridge UI by integrating a `preparedQuote` prop, improving the handling of swap transactions, and refining the relevant hooks and stories.

### Detailed summary
- Added `preparedQuote` prop to `SwapWidget` and `StepRunner`.
- Updated `StepRunner` to utilize `preparedQuote` for executing steps.
- Modified `useStepExecutor` to accept `preparedQuote` instead of `request`.
- Cleaned up imports in `useStepExecutor`.
- Updated stories to reflect the new `preparedQuote` prop.

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

<!-- end pr-codex -->
2025-09-24 19:29:55 +00:00
MananTank 8dc9e4102d [MNY-197] SDK: SwapWidget chain selection UI improvements (#8115)
<!--

## 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 `SwapUI` and `SelectTokenUI` components by updating callback functions and enhancing token selection logic.

### Detailed summary
- Changed `onBack` prop to `onClose` in `SelectToken` and `SelectTokenUI` components.
- Updated modal state management to close modals on token selection.
- Introduced `initiallySelectedChain` to prioritize the selected chain in `SelectBridgeChainUI`.
- Enhanced filtering logic for token selection to consider chain IDs.

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

<!-- end pr-codex -->
2025-09-24 19:29:55 +00:00
MananTank 33fc3e5226 [MNY-196] SDK: Add SwapWidget render event (#8113)
<!--

## 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 query using `useQuery` from `@tanstack/react-query` in the `SwapWidget` component to track the rendering event of the swap widget.

### Detailed summary
- Added `useQuery` to the `SwapWidget` component.
- Implemented a query function that calls `trackPayEvent` to log the rendering of the swap widget.
- The tracking event includes the `client` from `props` and an event identifier `"ub:ui:swap_widget:render"`.

> ✨ 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**
  * Added background telemetry for the Swap Widget to record render events for analytics. It runs automatically when the widget is displayed, requires no configuration, and does not change the UI or user experience. This helps track usage and adoption to inform future improvements.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-09-24 19:29:54 +00:00
MananTank ed47e93f1d [BLD-338] Dashboard: Fix NFTs table owner row not showing data (#8120)
<!--

## 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 temporary change to the `table.tsx` file in the NFTs component of the dashboard. It sets the `useIndexer` property to `false`, indicating a workaround until a future fix is implemented.

### Detailed summary
- Added `useIndexer: false` to the settings in `table.tsx`.
- Marked the addition as temporary with a comment: `// TEMPORARY, TODO: Remove when this is fixed in insight`.

> ✨ 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 reliability of NFT lists in the dashboard, reducing intermittent errors and missing items when viewing NFTs for specific contracts.
  * Ensures more consistent, up-to-date NFT data is displayed during browsing and refreshes.
  * Enhances stability of data loading in environments with inconsistent indexing, minimizing failed loads and retries.
  * No visual/UI changes; behavior is more dependable without altering the interface or workflows.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-09-24 19:19:38 +00:00
MananTank 1f7cda6698 [MNY-210] SDK: export a script to render BridgeEmbed (#8106)
<!--

## 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 `BridgeWidget` component, allowing users to render a widget for token buying and swapping. It also updates related files for better integration and testing, while modifying build configurations and dependencies.

### Detailed summary
- Added `BridgeWidget` component.
- Created a browser script in `dist/scripts/bridge-widget.js`.
- Updated `BuyOrOnrampPrepareResult` type in `BuyWidget.tsx`.
- Excluded test files in `tsconfig.build.tsup.json`.
- Modified `tsup` configuration for building the widget.
- Enhanced documentation for the `BridgeWidget`.
- Added various usage examples in the README.
- Updated `pnpm-lock.yaml` with new dependencies and versions.

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

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

<!-- end pr-codex -->

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

- **New Features**
  - Introduced a Bridge Widget with Swap and Buy tabs, theming, branding toggle, currency presets, and callback support.
  - Added an embeddable script API with a global render function to mount the Bridge Widget on any webpage and exposed it in React exports.

- **Style**
  - Connect Wallet embed now uses full-width with max-width constraints for responsive display.

- **Documentation**
  - Added usage guide and Storybook examples for the Bridge Widget script.

- **Build**
  - Added browser-targeted bundle config to produce a self-contained Bridge Widget script.

- **Chores**
  - Updated build scripts and dev dependencies.

- **Tests**
  - Skipped a batch transaction test.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-09-24 17:30:52 +00:00
Joaquim VergesGitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
c26fc676c9 Version Packages (#8110)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-09-24 15:59:23 +12:00
gregandGitHub e1cccd7a10 Feature: Displays the buy widget error message (#8108) 2025-09-23 20:29:51 -07:00
Joaquim VergesandGitHub 3646964f2e [SDK] Fix transaction parameter ordering in injected wallet (#8109) 2025-09-23 20:29:24 -07:00
MananTank db145b5621 [BLD-330] Playground: Add missing payments overview page (#8107)
<!--

## 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 `OverviewPage` component for the payments section, updates the layout of the `OverviewPage`, and adds a new payment feature card for the x402 payment protocol.

### Detailed summary
- Updated `OverviewPage` to allow a maximum width of 80% for the description.
- Created a new `Page` component in `page.tsx` that utilizes `OverviewPage` with a title, description, and feature cards.
- Added a new feature card for the x402 payment protocol in `pages-metadata.ts`.

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

<!-- end pr-codex -->

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

## Summary by CodeRabbit

* **New Features**
  * Introduced a Payments page with a clear overview, icon, and quick links to payment options.
  * Added an x402 payment protocol card to the Payments overview, enabling API payments with any web3 wallet and direct navigation.

* **Style**
  * Refined overview layout by limiting description width for improved readability on wide screens.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-09-23 21:05:13 +00:00
Joaquim VergesGitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
db3838032a Version Packages (#8102)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-09-23 03:42:43 -07:00
Joaquim VergesandGitHub 60021b3a44 [SDK] added settlePayment() utility for x402 payments (#8105) 2025-09-23 03:23:31 -07:00
a81899d60a [Dashboard] Add ai usage billing banner to dashboard (#8104)
Co-authored-by: Cursor Agent <[email protected]>
2025-09-22 19:19:09 -07:00
MananTank b366d56039 [MNY-195] Dashboard: Remove Dexscreener on erc20 public page, add token info card (#8103)
<!--

## 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 and functionality in the `ERC20` public page, improving styles, and refactoring the token info fetching logic.

### Detailed summary
- Removed `dex-screener.tsx` and `dex-screener-chains.ts`.
- Adjusted padding in `ContractHeader.tsx`.
- Added shadow to a div in `BuyAndSwapEmbed.tsx`.
- Updated border styles in `ERC20PublicPage.tsx`.
- Introduced `TokenInfoSection` for displaying token info.
- Refactored `fetchTokenInfoFromBridge` to use `ThirdwebClient`.
- Added icons in `ContractAnalyticsOverview` and `RecentTransfers`.
- Improved layout and styles for various components.

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

<!-- end pr-codex -->

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

* **New Features**
  * Added token summary section (Price, Market Cap, 24h Volume) on ERC20 public pages.

* **Style**
  * Visual polish: added shadow, larger rounded corners, adjusted headers, spacing, and rounded chart containers.
  * Chart and analytics headers now support/display icons; some header background styling simplified.

* **Changes**
  * Removed embedded DexScreener integration and related chain-slug mapping.

* **Refactor**
  * Token info fetching switched to a client-based SDK call; formatting helpers added for USD display.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-09-23 00:10:29 +00:00
MananTank e568bcea60 [MNY-193] Dashboard: Update Client Ids for Buy and Swap widgets in chain, bridge and asset pages (#8101)
<!--

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

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

## Notes for the reviewer

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

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on refactoring components and updating environment variable handling for the `BuyAndSwapEmbed` and `BuyFundsSection` components in a dashboard application. It enhances the usage of client IDs and improves the structure of props.

### Detailed summary
- Removed `client` prop from `BuyAndSwapEmbed` and `BuyFundsSection`.
- Introduced `clientId` handling in `getConfiguredThirdwebClient`.
- Updated `BuyAndSwapEmbed` to use `useMemo` for client configuration based on `pageType`.
- Added multiple `NEXT_PUBLIC_*_CLIENT_ID` constants in `public-envs.ts`.
- Cleaned up imports and types in various files 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

- New Features
  - Auto-configured client selection per page type (asset/bridge/chain) for Buy, Swap, and Bridge embeds, improving reliability without manual setup.
- Refactor
  - Simplified components by removing the need to pass a client prop; widgets now use an internal, memoized client.
- Chores
  - Added new public environment variables for page-specific client IDs to enable tailored configuration:
    - NEXT_PUBLIC_ASSET_PAGE_CLIENT_ID
    - NEXT_PUBLIC_BRIDGE_PAGE_CLIENT_ID
    - NEXT_PUBLIC_CHAIN_PAGE_CLIENT_ID

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-09-22 23:41:35 +00:00
MananTank a94f22928a [MNY-204] Dashboard: Add tracking for BuyWidget, SDK: update callback types in BuyWidget (#8100)
<!--

## 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 `BuyWidget` and related components by updating the callback props to include a `quote` object, allowing for better tracking of token buy events and improving error handling.

### Detailed summary
- Updated `onSuccess`, `onError`, and `onCancel` props in `BuyWidget` to accept a `quote` object.
- Enhanced error reporting with detailed information in `reportTokenBuyFailed`, `reportTokenBuyCancelled`, and `reportTokenBuySuccessful`.
- Modified `BuyAndSwapEmbed` component to handle the new callback signatures.
- Adjusted `BridgeOrchestrator` to utilize the `quote` object in its flow.

> ✨ 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
  - Buy and Bridge flows now provide quote details to success, error, and cancel callbacks.
  - Token-level analytics added for buy flows (success, failure, cancel).
- Breaking Changes
  - Callback signatures changed to include quote parameters; client implementations must be updated.
- Documentation
  - Examples updated to show new callback parameters and usage.
- Chores
  - Standardized analytics for asset purchases where contract type may be unspecified.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-09-22 22:53:37 +00:00
Joaquim VergesandGitHub 5249cb7409 [SDK] Add verifyPayment() backend utility for arbitrary chain x402 payments (#8091) 2025-09-22 15:35:05 -07:00
MananTank b37ed57365 Dashboard: Fix broken opengraph-image.tsx (#8094)
<!--

## 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 use of `useId` in several components by replacing it with static string values for IDs, improving consistency and potentially performance in the components related to Open Graph images.

### Detailed summary
- Removed `useId` from `contract-wallet-analytics.ts`, replacing it with a static variable.
- Updated `TWLogo` component in `opengraph-image.tsx` to use static IDs instead of `useId`.
- Changed `OgBrandIcon` component in `opengraph-image.tsx` to use a static gradient ID.
- Updated `TWLogo` component in another `opengraph-image.tsx` file to use static IDs instead of `useId`.

> ✨ 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
  - None.
- Bug Fixes
  - Improved reliability of OpenGraph images by using stable SVG IDs for gradients and clip paths, ensuring consistent rendering across sharing platforms on chain, contract, and drop pages.
- Chores
  - Removed unnecessary debug logging from wallet analytics to reduce console noise.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-09-22 18:59:27 +00:00
Joaquim VergesGitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
c328aa13a6 Version Packages (#8081)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-09-22 15:19:02 +07:00
FirekeeperandGitHub a85ef0b222 Add Zephyr testnet to pre-1559 chains (#8092) 2025-09-22 15:07:13 +07:00
sashaodessaandGitHub 51177fbe17 Fix typos in comments and docs across thirdweb packages (#8033)
Signed-off-by: sashaodessa <[email protected]>
2025-09-19 18:59:29 -07:00
Jonas DanielsandGitHub 1338a0cf2c Remove taint from GROWTH_PLAN_SKU and PAYMENT_METHOD_CONFIGURATION (#8089) 2025-09-19 18:58:47 -07:00
MananTank 96ebf378ff Playground: Redirect corrections (#8087)
<!--

## 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 routing configuration in the `next.config.mjs` file to allow for dynamic path segments in specific routes.

### Detailed summary
- Changed `source` for `/insight` to `/insight/:path*`, allowing for dynamic paths.
- Changed `source` for `/payments/backend` to `/payments/backend/:path*`, enabling dynamic path handling.
- Both routes maintain their respective `destination` and `permanent` settings.

> ✨ 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 redirects for /insight and /payments/backend to handle nested paths, ensuring deep links (e.g., subpages) correctly route to their destinations without 404s.
  * Maintains existing base redirects and leaves other routes untouched, improving navigation reliability without altering overall site behavior.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-09-20 01:10:38 +00:00
MananTank b7732d3a75 Dashboard: Add search in tokens page (#8088)
<!--

## 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 `Token` and `TokenPage` components by adding search functionality for tokens and improving the bridge network selection process.

### Detailed summary
- Added `query` parameter in `tokens` function for searching tokens by name or symbol.
- Updated `Page` component to fetch supported chains asynchronously.
- Modified `TokenPage` to accept `chains` as a prop and integrated search functionality.
- Enhanced `BridgeNetworkSelector` to utilize passed `chains` prop instead of querying.
- Added search input for token filtering in the `TokenPage`.

> ✨ 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 token search on the Tokens page; search by name or symbol with a new inline search field beside sort controls.
  * Search intelligently interacts with sorting: choosing Popular or Trending clears the search.

* **Improvements**
  * Faster, more reliable page load by fetching supported networks server‑side and passing them to the page.
  * Refined network selector UI with a consistent placeholder and smoother option rendering.

* **Refactor**
  * Streamlined components to receive pre-fetched chain data, reducing in-component data fetching and simplifying state.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-09-20 00:54:37 +00:00
MananTank 6f88d38ad2 [BLD-325] Dashboard: Rename floating chat button label (#8086)
<!--

## 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 `CustomChatButton` component and its usage in various layouts by simplifying the button structure, changing icons, and removing unnecessary wrapper elements.

### Detailed summary
- Removed the wrapper `<div>` around the `CustomChatButton` in `layout.tsx` files.
- Changed the icon in `CustomChatButton` from `MessageCircleIcon` to `CircleQuestionMarkIcon`.
- Updated button label from dynamic to static "Need help?".
- Adjusted button class names for styling consistency.

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

<!-- end pr-codex -->
2025-09-20 00:03:01 +00:00
MananTank d53f8a25b4 [MNY-191] Dashboard: Add header in /bridge (#8085)
<!--

## 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 layout and components of the `Bridge` application by improving styling, adding new components, and refactoring existing structures for better usability and aesthetics.

### Detailed summary
- Updated `<body>` class in `layout.tsx` for improved layout.
- Changed width class in `UniversalBridgeEmbed.tsx` for full responsiveness.
- Added `relative z-10` class to the main `<div>` in `BuyAndSwapEmbed.tsx`.
- Introduced a new `PageHeader` component in `header.tsx` with navigation links.
- Refactored `BridgePage` in `page.tsx` to include `PageHeader` and remove unnecessary elements.
- Added `DotsBackgroundPattern` function for a decorative background effect.
- Integrated `AppFooter` in `BridgePage` for consistent footer display.

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

<!-- end pr-codex -->
2025-09-19 23:35:04 +00:00
MananTank dd693169bc Dashboard: Remove /routes (#8084)
<!--

## 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 several files related to the `bridge` feature in the dashboard and the addition of a new `tokens` page with metadata for improved SEO.

### Detailed summary
- Deleted files related to the `bridge` feature:
  - `utils.ts`
  - `layout.tsx`
  - `route.ts`
  - `page.tsx`
  - `opengraph-image.png`
  - Various components under `routes/components/client` and `routes/components/server`
- Added a new `tokens` page in `page.tsx` with:
  - Metadata for SEO, including title and description
  - Import of `PageHeader` and `TokenPage` 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
  - Redirects: Visiting /routes now automatically navigates to /tokens (temporary redirect).
  - Tokens page now includes enriched metadata for improved previews and sharing.

- Refactor
  - Removed the legacy Routes dashboard and associated UI controls, streamlining navigation to the Tokens experience.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-09-19 23:35:04 +00:00
MananTank 93f913c614 [MNY-189] SDK: SwapWidget UI improvements (#8080)
<!--

## 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 `SwapWidget`, enhancing responsiveness, styling, and functionality, particularly for mobile users. It also refines token selection and formatting for better user experience.

### Detailed summary
- Removed a changeset file related to `SwapWidget` UI improvements.
- Adjusted breakpoints and font sizes in `design-system/index.ts`.
- Enhanced `SearchInput` styling in `SearchInput.tsx`.
- Added `tokenAmountFormatter` utility in `utils.ts`.
- Made `children` prop optional in `basic.tsx`.
- Updated color values in `sdk-component-theme.ts`.
- Enhanced `Input` component styles in `formElements.tsx`.
- Improved `BuyAndSwapEmbed.tsx` with better token handling.
- Introduced `useIsMobile` hook for mobile detection.
- Updated `ArrowUpDownIcon.tsx` SVG for better rendering.
- Added hover background support in `buttons.tsx`.
- Renamed `WithData` to `WithDataDesktop` in `SelectChain.stories.tsx`.
- Added mobile versions of `WithData` and `Loading` functions in `SelectChain.stories.tsx`.
- Updated `SwapWidget` stories to manage token selections and themes.
- Refined `select-chain.tsx` for better mobile handling.
- Improved `swap-ui.tsx` with mobile responsiveness and token selection logic.
- Enhanced `select-token-ui.tsx` with mobile UI improvements and token display logic.
- Added `ActiveWalletDetails` for displaying wallet information.
- Updated various button and token display styles for consistency and better UX.

> ✨ 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**
  * Swap widget: onDisconnect callback, details modal, max-fill, improved wallet/account display, clearer loading/insufficient-balance states; stories now disable token-selection persistence.
  * Mobile detection hook enabling separate mobile/desktop flows.

* **UI/Style**
  * Enhanced token/chain visuals (gradients, smaller icon size), adjusted spacing and input sizing, customizable button hover backgrounds.

* **Refactor**
  * Unified token amount formatting and simplified numeric displays.

* **Chores**
  * Added changeset entry for UI improvements.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-09-20 02:22:43 +05:30
MananTank 02502159b6 Dashboard: Add Tokens page (#8073)
<!--

## 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 token management system, focusing on adding price-related fields, sorting options, and improving the user interface for token display and selection.

### Detailed summary
- Added `marketCapUsd` and `volume24hUsd` fields to the `Token` type.
- Introduced `sortBy` option in token fetching.
- Updated `PublicPageConnectButton` to include a `detailsButton`.
- Created a new `Page` component for displaying token information.
- Implemented a `PageHeader` component for navigation.
- Added `TokenPage` component for token management with sorting capabilities.
- Developed `TokensTable` component for listing tokens with price, market cap, and volume.
- Introduced `BridgeNetworkSelector` for selecting blockchain networks.
- Enhanced UI with loading states and better token presentation.

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

<!-- end pr-codex -->

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

## Summary by CodeRabbit

- New Features
  - Introduced a Tokens dashboard page with header and hero section.
  - Added network selector for choosing chains.
  - Implemented token list with columns for Token, Price, Market Cap, and 24h Volume.
  - Enabled sorting by Popular (market cap) and Trending (volume), plus pagination.
  - Improved Connect button with an additional details button style.

- Enhancements
  - Loading states and empty-state messaging for token lists.
  - Backend now supports token sorting and exposes market cap and 24h volume for richer displays.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-09-19 20:02:12 +00:00
Joaquim VergesGitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
e9225c3bad Version Packages (#8079)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-09-19 05:11:46 -07:00
Joaquim VergesandGitHub 5967fb8afa [SDK] Add x402 payment protocol utilities (#8076) 2025-09-19 05:00:05 -07:00
MananTank fddb791f0a Playground: Fix Pay components incomplete code gen, cleanup (#8078)
<!--

## 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 the payment components in the `playground-web` application, updating widget properties and enhancing code generation functions for better modularity and readability.

### Detailed summary
- Changed `lockedWidget` to `widget` in multiple components: `CheckoutPlayground`, `TransactionPlayground`, `BuyPlayground`, `LeftSection`, and `RightSection`.
- Added new functions in `code-gen.ts` for `stringifyProps`, `quotes`, `stringifyImports`, and `stringifyIgnoreFalsy`.
- Updated `CodeGen` components to accept `widget` as a prop.
- Refactored `getCode` function in `CodeGen` to use the new `widget` parameter.
- Removed unused imports and functions from `CodeGen` and `LeftSection`.

> ✨ 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
  - Code generator now supports explicit widget selection (buy, checkout, transaction) with unified, props-driven output.
  - Enhanced theming in generated snippets, including dark/light themes with overrides.

- Refactor
  - Migrated playground code generation to centralized utilities for imports and props.
  - Simplified widget configuration by removing it from options and passing it as a direct prop.
  - Streamlined Payments UI; removed “Sponsor gas fees” section.

- Style
  - Improved code snippet formatting with wider line wrap for cleaner display.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-09-19 10:12:08 +00:00
Joaquim VergesGitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
1732e63bb8 Version Packages (#8075)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-09-19 02:02:04 -07:00
Joaquim VergesandGitHub 7b8ceebb63 [SDK] Show policy error for 7702 transactions (#8077) 2025-09-19 01:53:09 -07:00
MananTank 1d90437d34 Portal: Add SwapWidget in bridge/swap page (#8072)
<!--

## 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 documentation for the `SwapWidget` component, simplifying the example code, and enhancing the user experience by adding new features and usage guidelines.

### Detailed summary
- Added `DocImage` component to display `swap-dark.png`.
- Simplified the swap interface example to use the `SwapWidget`.
- Introduced a live playground section for the `SwapWidget`.
- Added configuration options for default token selection in the widget.
- Removed the complex swap interface code for clarity.
- Included tips for gas optimization and route caching.

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

<!-- end pr-codex -->
2025-09-19 00:32:32 +00:00
Joaquim VergesandGitHub 59616d0082 [Portal] Add documentation for third-party library adapters (#8074) 2025-09-18 17:31:46 -07:00
MananTank 32980f854b [MNY-190] Playground: Add SwapWidget (#8069)
<!--

## 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 significant updates to the `SwapWidget` component, including a new `persistTokenSelections` prop for managing token selection persistence. It also enhances the UI components and integrates a new bridge feature.

### Detailed summary
- Added `persistTokenSelections` prop to `SwapWidget`.
- Updated `SwapWidgetPlayground` for better token management.
- Introduced `BridgeNetworkSelector` and `CurrencySelector` components.
- Added `bridgeFeatureCards` to the feature metadata.
- Enhanced `LeftSection` for improved token and currency selection.
- Updated the sidebar to include the bridge section.
- Refactored network selection logic to use the bridge API.
- Improved code formatting and organization in multiple files.

> ✨ 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
  - SwapWidget gains a persistTokenSelections prop to opt out of saving token choices.
  - Added a Swap Widget Playground with live preview, generated code view, theme/currency/prefill/branding controls, and UI/Code tabs.
  - Playground is integrated into site navigation and feature cards under a new Bridge section.

- Chores
  - Playground now uses bridge-supported chains and improved TypeScript formatting for generated snippets.
  - Added a reusable currency selector for widget configuration.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-09-19 00:17:37 +00:00
MananTank f3b3b89fca Dashboard: Add tracking for SwapWidget impression, chain pageview (#8071)
<!--

## 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 analytics tracking for chain page views and enhances the reporting for swap widgets in the dashboard application.

### Detailed summary
- Added `ChainPageView` component to track impressions of chain pages.
- Integrated `reportChainPageview` function in the analytics module.
- Enhanced `BuyAndSwapEmbed` to report when the swap widget is shown.

> ✨ 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
  - Added one-time telemetry when the Swap widget is displayed to improve usage insights.
  - Tracks the page context (asset, bridge, or chain) when the widget first appears.
  - Added one-time telemetry to record chain pageviews (captures the chain identifier) when visiting chain pages.
  - Integrated with existing UI; no impact on behavior, user action, or performance.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-09-18 21:03:00 +00:00
Jonas DanielsandGitHub 44826925d0 Add Stripe checkout for Growth plan subscription (#8068) 2025-09-17 22:41:55 -07:00
Joaquim VergesGitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
00dcc346bf Version Packages (#8063)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-09-18 10:58:25 +12:00
Manan TankandGitHub e0738d68d2 SDK, Dashboard: Fix SwapWidget setting same token for buy and sell in chain page (#8067) 2025-09-17 15:47:19 -07:00
Joaquim VergesandGitHub e160deb8b7 [SDK] Fix wallet reconnection for previously connected wallets (#8066) 2025-09-17 15:18:13 -07:00
Manan TankandGitHub a6f846fc1e [BLD-286] SDK: add stable classNames to elements in Connect UI (#8065) 2025-09-17 13:34:27 -07:00
Manan TankandGitHub 0e3f3460ad Add SwapWidget in SDK, add in dashboard, BuyWidget UI improvements (#8044) 2025-09-17 12:57:58 -07:00
MananTank 552f7025c1 Dashboard: Fix Import Engine form submit not working (#8064)
<!--

## 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 dialog for importing engine instances in the `ImportEngineButton` component, enhancing user interaction by managing the dialog state and updating UI elements.

### Detailed summary
- Added `useState` to manage the dialog's open state.
- Changed navigation from `router.push` to `router.refresh` after import.
- Wrapped the dialog in a conditional open state.
- Updated the layout and styling of the dialog content and links.
- Modified form field styles for better UI consistency.

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

<!-- end pr-codex -->

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

## Summary by CodeRabbit

* New Features
  * Import dialog now closes automatically after a successful import and refreshes the current page.
  * Added an in-dialog warning beneath the URL field for clearer guidance.
  * Updated “Get started” link destination.

* Style
  * Redesigned import dialog with improved layout, spacing, rounded content, and a bottom action bar; inputs use card styling.
  * Refined labels, placeholders, and header padding for clearer readability.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-09-17 15:58:39 +00:00
MananTank 15f083912c SDK: Fix onSuccess prop not called properly on CheckoutWidget and BuyWidget (#8054)
<!--

## 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 `onSuccess` callback not being triggered properly in the `CheckoutWidget` and `BuyWidget` components, improving the handling of buy completion and post-transaction actions.

### Detailed summary
- Renamed `handleBuyComplete` to `handleDoneOrContinueClick`.
- Adjusted the invocation of `onComplete` to only occur when `uiOptions.mode` is not "transaction".
- Updated the dependency array for `handleExecutionComplete` to include `onComplete` and `uiOptions.mode`.
- Changed the `onDone` prop in `SuccessScreen` to use `handleDoneOrContinueClick`.

> ✨ 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 where the success callback in checkout and buy widgets did not fire reliably.
  * Improved non-transaction flows so completion triggers after execution finishes (instead of on Done), ensuring proper state reset and more consistent behavior.

* **Chores**
  * Added a patch-level changeset entry documenting the fix.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-09-17 10:47:49 +00:00
GiselleNessi 04862de9e4 feat: add feedback collection UI to navigation (#7895)
<!--

## 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 feature for collecting user feedback within the application. It implements feedback submission forms in both desktop and mobile components, capturing user feedback and sending it to PostHog for analysis.

### Detailed summary
- Added `reportProductFeedback` function in `report.ts` to handle feedback submissions.
- Implemented feedback submission in the `SecondaryNavLinks` component.
- Added feedback dropdown with a form in `SecondaryNavLinks`.
- Integrated feedback functionality in the `MobileBurgerMenuButton` component.
- Included success notifications using `toast` for feedback submissions in both 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**
  * Replaced external Feedback link with an in-app feedback panel in the header and mobile menu.
  * Panel includes a textarea (max 1000 chars), Cancel and Submit actions; Submit is disabled when empty, shows a success message, clears input and closes the panel.
  * Adds an in-panel “Contact support” link for technical issues and preserves existing header controls.

* **Style**
  * Expandable/collapsible feedback UI with visual indicators, improved positioning and mobile menu scrolling.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-09-16 16:37:22 +00:00
ea46572947 [Dashboard] Display eip7702 support status (#8060)
Co-authored-by: Cursor Agent <[email protected]>
2025-09-15 17:39:14 -07:00
Joaquim VergesandGitHub f276dcfd76 [SDK] Filter out AGW from searchable wallets (#8062) 2025-09-15 17:38:51 -07:00
Joaquim VergesandGitHub 6495b94910 [SDK] Simplify wallet connection code and add Abstract wallet support (#8061) 2025-09-15 16:29:01 -07:00
635 changed files with 43746 additions and 14063 deletions
+1 -1
View File
@@ -2,7 +2,7 @@ name: Auto Author Assign
on:
pull_request:
types: [opened, reopened, ready_for_review, draft]
types: [opened, reopened, ready_for_review]
permissions:
pull-requests: write
+2
View File
@@ -64,3 +64,5 @@ ANALYTICS_SERVICE_URL=""
# required for billing parts of the dashboard (team -> settings -> billing / invoices)
STRIPE_SECRET_KEY=""
GROWTH_PLAN_SKU=""
PAYMENT_METHOD_CONFIGURATION=""
-30
View File
@@ -1,30 +0,0 @@
import { defineConfig } from "checkly";
import { Frequency } from "checkly/constructs";
export default defineConfig({
checks: {
activated: true,
browserChecks: {
frequency: Frequency.EVERY_24H,
testMatch: "./tests/**/*.spec.ts",
},
checkMatch: "./**/*.check.ts",
frequency: Frequency.EVERY_24H,
ignoreDirectoriesMatch: [],
locations: ["us-east-1", "eu-west-1"],
muted: false,
playwrightConfig: {
use: {
baseURL: process.env.ENVIRONMENT_URL || "https://thirdweb.com",
},
},
runtimeId: "2023.09",
tags: ["website"],
},
cli: {
runLocation: "eu-west-1",
},
logicalId: "thirdweb-www",
projectName: "thirdweb.com",
repoUrl: "https://github.com/thirdweb-dev/dashboard",
});
+2
View File
@@ -10,6 +10,8 @@ module.exports = [
"/wallets",
"/account-abstraction",
"/payments",
"/payments/x402",
"/nexus",
"/auth",
"/in-app-wallets",
"/transactions",
+3 -5
View File
@@ -21,6 +21,8 @@
"@radix-ui/react-tooltip": "1.2.7",
"@sentry/nextjs": "9.34.0",
"@shazow/whatsabi": "0.22.2",
"@stripe/react-stripe-js": "4.0.2",
"@stripe/stripe-js": "7.9.0",
"@tanstack/react-query": "5.81.5",
"@tanstack/react-table": "^8.21.3",
"@thirdweb-dev/api": "workspace:*",
@@ -80,7 +82,6 @@
"@chromatic-com/storybook": "4.0.1",
"@next/bundle-analyzer": "15.3.5",
"@next/eslint-plugin-next": "15.3.5",
"@playwright/test": "1.53.2",
"@storybook/addon-docs": "9.0.15",
"@storybook/addon-links": "9.0.15",
"@storybook/addon-onboarding": "9.0.15",
@@ -97,7 +98,6 @@
"@typescript-eslint/eslint-plugin": "7.14.1",
"@typescript-eslint/parser": "7.14.1",
"autoprefixer": "^10.4.21",
"checkly": "6.0.1",
"eslint": "8.57.0",
"eslint-config-biome": "1.9.4",
"eslint-plugin-react-compiler": "19.1.0-rc.2",
@@ -120,13 +120,11 @@
"format": "biome format ./src --write",
"knip": "knip",
"lint": "biome check ./src && knip && eslint ./src",
"playwright": "playwright test",
"postbuild": "next-sitemap",
"preinstall": "npx only-allow pnpm",
"start": "next start",
"storybook": "storybook dev -p 6006",
"typecheck": "tsc --noEmit",
"update-checkly": "npx checkly deploy"
"typecheck": "tsc --noEmit"
},
"version": "3.0.0"
}
-79
View File
@@ -1,79 +0,0 @@
import { defineConfig, devices } from "@playwright/test";
/**
* Read environment variables from file.
* https://github.com/motdotla/dotenv
*/
// require('dotenv').config();
/**
* See https://playwright.dev/docs/test-configuration.
*/
export default defineConfig({
/* Fail the build on CI if you accidentally left test.only in the source code. */
forbidOnly: !!process.env.CI,
/* Run tests in files in parallel */
fullyParallel: true,
/* Configure projects for major browsers */
projects: [
{
name: "chromium",
use: { ...devices["Desktop Chrome"] },
},
{
name: "firefox",
use: { ...devices["Desktop Firefox"] },
},
{
name: "webkit",
use: { ...devices["Desktop Safari"] },
},
/* Test against mobile viewports. */
// {
// name: 'Mobile Chrome',
// use: { ...devices['Pixel 5'] },
// },
// {
// name: 'Mobile Safari',
// use: { ...devices['iPhone 12'] },
// },
/* Test against branded browsers. */
// {
// name: 'Microsoft Edge',
// use: { ...devices['Desktop Edge'], channel: 'msedge' },
// },
// {
// name: 'Google Chrome',
// use: { ...devices['Desktop Chrome'], channel: 'chrome' },
// },
],
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: "html",
/* Retry on CI only */
retries: process.env.CI ? 2 : 0,
testDir: "./tests",
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
/* Base URL to use in actions like `await page.goto('/')`. */
baseURL: process.env.ENVIRONMENT_URL || "http://127.0.0.1:3000",
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: "on-first-retry",
},
/* Run your local dev server before starting the tests */
webServer: process.env.CI
? undefined
: {
command: "pnpm run start",
reuseExistingServer: !process.env.CI,
url: "http://127.0.0.1:3000",
},
/* Opt out of parallel tests on CI. */
workers: process.env.CI ? 1 : undefined,
});
+5
View File
@@ -474,6 +474,11 @@ async function redirects() {
destination: "/team/~/~/support",
permanent: false,
},
{
source: "/routes",
destination: "/tokens",
permanent: false,
},
];
}
@@ -0,0 +1,70 @@
"use server";
import { createVaultClient, listEoas } from "@thirdweb-dev/vault-sdk";
import { NEXT_PUBLIC_THIRDWEB_VAULT_URL } from "@/constants/public-envs";
import type { ProjectWalletSummary } from "@/lib/server/project-wallet";
interface VaultWalletListItem {
id: string;
address: string;
metadata?: {
label?: string;
projectId?: string;
teamId?: string;
type?: string;
} | null;
}
export async function listProjectServerWallets(params: {
managementAccessToken: string;
projectId: string;
pageSize?: number;
}): Promise<ProjectWalletSummary[]> {
const { managementAccessToken, projectId, pageSize = 100 } = params;
if (!managementAccessToken || !NEXT_PUBLIC_THIRDWEB_VAULT_URL) {
return [];
}
try {
const vaultClient = await createVaultClient({
baseUrl: NEXT_PUBLIC_THIRDWEB_VAULT_URL,
});
const response = await listEoas({
client: vaultClient,
request: {
auth: {
accessToken: managementAccessToken,
},
options: {
page: 0,
// @ts-expect-error - Vault SDK expects snake_case pagination fields
page_size: pageSize,
},
},
});
if (!response.success || !response.data?.items) {
return [];
}
const items = response.data.items as VaultWalletListItem[];
return items
.filter((item) => {
return (
item.metadata?.projectId === projectId &&
(!item.metadata?.type || item.metadata.type === "server-wallet")
);
})
.map<ProjectWalletSummary>((item) => ({
id: item.id,
address: item.address,
label: item.metadata?.label ?? undefined,
}));
} catch (error) {
console.error("Failed to list project server wallets", error);
return [];
}
}
@@ -0,0 +1,74 @@
"use server";
import { configure, sendTokens } from "@thirdweb-dev/api";
import { THIRDWEB_API_HOST } from "@/constants/urls";
configure({
override: {
baseUrl: THIRDWEB_API_HOST,
},
});
export async function sendProjectWalletTokens(options: {
walletAddress: string;
recipientAddress: string;
chainId: number;
quantityWei: string;
publishableKey: string;
teamId: string;
tokenAddress?: string;
secretKey: string;
vaultAccessToken?: string;
}) {
const {
walletAddress,
recipientAddress,
chainId,
quantityWei,
publishableKey,
teamId,
tokenAddress,
secretKey,
vaultAccessToken,
} = options;
if (!secretKey) {
return {
error: "A project secret key is required to send funds.",
ok: false,
} as const;
}
const response = await sendTokens({
body: {
chainId,
from: walletAddress,
recipients: [
{
address: recipientAddress,
quantity: quantityWei,
},
],
...(tokenAddress ? { tokenAddress } : {}),
},
headers: {
"Content-Type": "application/json",
"x-client-id": publishableKey,
"x-secret-key": secretKey,
"x-team-id": teamId,
...(vaultAccessToken ? { "x-vault-access-token": vaultAccessToken } : {}),
},
});
if (response.error || !response.data) {
return {
error: response.error || "Failed to submit transfer request.",
ok: false,
} as const;
}
return {
ok: true,
transactionIds: response.data.result?.transactionIds ?? [],
} as const;
}
+67 -1
View File
@@ -1,8 +1,13 @@
import "server-only";
import { cookies, headers } from "next/headers";
import Stripe from "stripe";
import type { Team } from "@/api/team/get-team";
import { STRIPE_SECRET_KEY } from "@/constants/server-envs";
import {
GROWTH_PLAN_SKU,
PAYMENT_METHOD_CONFIGURATION,
STRIPE_SECRET_KEY,
} from "@/constants/server-envs";
let existingStripe: Stripe | undefined;
@@ -72,3 +77,64 @@ export async function getStripeBalance(customerId: string) {
// Stripe returns a positive balance for credits, so we need to divide by -100 to get the actual balance (as long as the balance is not 0)
return customer.balance === 0 ? 0 : customer.balance / -100;
}
export async function fetchClientSecret(team: Team) {
"use server";
const origin = (await headers()).get("origin");
const stripe = getStripe();
const customerId = team.stripeCustomerId;
if (!customerId) {
throw new Error("No customer ID found");
}
// try to get the gclid cookie
const gclid = (await cookies()).get("gclid")?.value;
// Create Checkout Sessions from body params.
const session = await stripe.checkout.sessions.create({
ui_mode: "embedded",
line_items: [
{
// Provide the exact Price ID (for example, price_1234) of
// the product you want to sell
price: GROWTH_PLAN_SKU,
quantity: 1,
},
],
mode: "subscription",
return_url: `${origin}/get-started/team/${team.slug}/select-plan?session_id={CHECKOUT_SESSION_ID}`,
automatic_tax: { enabled: true },
allow_promotion_codes: true,
customer: customerId,
customer_update: {
address: "auto",
},
payment_method_collection: "always",
payment_method_configuration: PAYMENT_METHOD_CONFIGURATION,
subscription_data: {
trial_period_days: 14,
trial_settings: {
end_behavior: {
missing_payment_method: "cancel",
},
},
// if gclid exists, set it as a metadata field so we can attribute the conversion later
metadata: gclid ? { gclid } : undefined,
},
});
if (!session.client_secret) {
throw new Error("No client secret found");
}
return session.client_secret;
}
export async function getStripeSessionById(sessionId: string) {
const session = await getStripe().checkout.sessions.retrieve(sessionId, {
expand: ["line_items", "payment_intent"],
});
return session;
}
+220 -65
View File
@@ -24,6 +24,7 @@ export function reportContractDeployed(properties: {
publisher: string | undefined;
contractName: string | undefined;
deploymentType?: "asset";
is_testnet: boolean | undefined;
}) {
posthog.capture("contract deployed", properties);
}
@@ -40,6 +41,7 @@ export function reportContractDeployed(properties: {
export function reportContractDeployFailed(properties: {
errorMessage: string;
chainId: number;
is_testnet: boolean | undefined;
publisher: string | undefined;
contractName: string | undefined;
}) {
@@ -81,33 +83,6 @@ export function reportOnboardingStarted() {
posthog.capture("onboarding started");
}
/**
* ### Why do we need to report this event?
* - To track the number of teams that select a paid plan during onboarding
* - To know **which** plan was selected
*
* ### Who is responsible for this event?
* @jnsdls
*
*/
export function reportOnboardingPlanSelected(properties: {
plan: Team["billingPlan"];
}) {
posthog.capture("onboarding plan selected", properties);
}
/**
* ### Why do we need to report this event?
* - To track the number of teams that skip the plan-selection step during onboarding
*
* ### Who is responsible for this event?
* @jnsdls
*
*/
export function reportOnboardingPlanSelectionSkipped() {
posthog.capture("onboarding plan selection skipped");
}
/**
* ### Why do we need to report this event?
* - To track the number of teams that invite members during onboarding
@@ -161,6 +136,18 @@ export function reportOnboardingMembersUpsellPlanSelected(properties: {
posthog.capture("onboarding members upsell plan selected", properties);
}
/**
* ### Why do we need to report this event?
* - To track the number of teams that completed the team member step during onboarding
*
* ### Who is responsible for this event?
* @jnsdls
*
*/
export function reportTeamMemberStepCompleted() {
posthog.capture("onboarding members completed");
}
/**
* ### Why do we need to report this event?
* - To track the number of teams that completed onboarding
@@ -186,9 +173,7 @@ export function reportOnboardingCompleted() {
*
*/
export function reportFaucetUsed(properties: { chainId: number }) {
posthog.capture("faucet used", {
chainId: properties.chainId,
});
posthog.capture("faucet used", properties);
}
// ----------------------------
@@ -212,12 +197,7 @@ export function reportChainConfigurationAdded(properties: {
decimals: number;
};
}) {
posthog.capture("chain configuration added", {
chainId: properties.chainId,
chainName: properties.chainName,
nativeCurrency: properties.nativeCurrency,
rpcURLs: properties.rpcURLs,
});
posthog.capture("chain configuration added", properties);
}
// ----------------------------
@@ -240,19 +220,120 @@ type AssetContractType =
*/
export function reportAssetBuySuccessful(properties: {
chainId: number;
contractType: AssetContractType;
contractType: AssetContractType | undefined;
assetType: "nft" | "coin";
is_testnet: boolean | undefined;
}) {
posthog.capture("asset buy successful", {
assetType: properties.assetType,
chainId: properties.chainId,
contractType: properties.contractType,
});
posthog.capture("asset buy successful", properties);
}
type TokenSwapParams = {
buyTokenChainId: number;
buyTokenAddress: string;
sellTokenChainId: number;
sellTokenAddress: string;
pageType: "asset" | "bridge" | "chain";
};
type TokenBuyParams = {
buyTokenChainId: number | undefined;
buyTokenAddress: string | undefined;
pageType: "asset" | "bridge" | "chain";
};
/**
* ### Why do we need to report this event?
* - To track number of successful token buys
* - To track which tokens are being bought the most
*
* ### Who is responsible for this event?
* @MananTank
*/
export function reportTokenBuySuccessful(properties: TokenBuyParams) {
posthog.capture("token buy successful", properties);
}
/**
* ### Why do we need to report this event?
* - To track number of failed asset purchases from the token page
* - To track number of failed token buys
* - To track which token buys are failing
*
* ### Who is responsible for this event?
* @MananTank
*/
export function reportTokenBuyFailed(properties: TokenBuyParams) {
posthog.capture("token buy failed", properties);
}
/**
* ### Why do we need to report this event?
* - To track number of cancelled token buys
* - To track which token buys are being cancelled
*
* ### Who is responsible for this event?
* @MananTank
*/
export function reportTokenBuyCancelled(properties: TokenBuyParams) {
posthog.capture("token buy cancelled", properties);
}
/**
* ### Why do we need to report this event?
* - To track number of successful token swaps
* - To track which tokens are being swapped the most
*
* ### Who is responsible for this event?
* @MananTank
*/
export function reportTokenSwapSuccessful(properties: TokenSwapParams) {
posthog.capture("token swap successful", properties);
}
/**
* ### Why do we need to report this event?
* - To track impressions of the swap widget
* - To create a funnel "swap widget shown" -> "swap widget successful" to understand the conversion rate
*
* ### Who is responsible for this event?
* @MananTank
*/
export function reportSwapWidgetShown(properties: {
pageType: "asset" | "bridge" | "chain";
}) {
posthog.capture("swap widget shown", properties);
}
/**
* ### Why do we need to report this event?
* - To track number of failed token swaps
* - To track which tokens are being swapped the most
*
* ### Who is responsible for this event?
* @MananTank
*/
export function reportTokenSwapFailed(
properties: TokenSwapParams & {
errorMessage: string;
},
) {
posthog.capture("token swap failed", properties);
}
/**
* ### Why do we need to report this event?
* - To track number of cancelled token swaps
* - To track which tokens are being swapped the most
*
* ### Who is responsible for this event?
* @MananTank
*/
export function reportTokenSwapCancelled(properties: TokenSwapParams) {
posthog.capture("token swap cancelled", properties);
}
/**
* ### Why do we need to report this event?
* - To track number of failed asset purchases
* - To track the errors that users encounter when trying to purchase an asset
*
* ### Who is responsible for this event?
@@ -260,16 +341,29 @@ export function reportAssetBuySuccessful(properties: {
*/
export function reportAssetBuyFailed(properties: {
chainId: number;
contractType: AssetContractType;
is_testnet: boolean | undefined;
contractType: AssetContractType | undefined;
assetType: "nft" | "coin";
error: string;
}) {
posthog.capture("asset buy failed", {
assetType: properties.assetType,
chainId: properties.chainId,
contractType: properties.contractType,
error: properties.error,
});
posthog.capture("asset buy failed", properties);
}
/**
* ### Why do we need to report this event?
* - To track number of cancelled asset purchases from the token page
* - To track the errors that users encounter when trying to purchase an asset
*
* ### Who is responsible for this event?
* @MananTank
*/
export function reportAssetBuyCancelled(properties: {
chainId: number;
is_testnet: boolean | undefined;
contractType: AssetContractType | undefined;
assetType: "nft" | "coin";
}) {
posthog.capture("asset buy cancelled", properties);
}
// Assets Landing Page ----------------------------
@@ -314,10 +408,7 @@ export function reportAssetCreationStepConfigured(
step: "coin-info" | "token-distribution" | "launch-coin";
},
) {
posthog.capture("asset creation step configured", {
assetType: properties.assetType,
step: properties.step,
});
posthog.capture("asset creation step configured", properties);
}
/**
@@ -331,11 +422,10 @@ export function reportAssetCreationStepConfigured(
export function reportAssetCreationSuccessful(properties: {
assetType: "nft" | "coin";
contractType: AssetContractType;
chainId: number;
is_testnet: boolean | undefined;
}) {
posthog.capture("asset creation successful", {
assetType: properties.assetType,
contractType: properties.contractType,
});
posthog.capture("asset creation successful", properties);
}
type CoinCreationStep =
@@ -357,7 +447,12 @@ type CoinCreationStep =
* @MananTank
*/
export function reportAssetCreationFailed(
properties: { contractType: AssetContractType; error: string } & (
properties: {
contractType: AssetContractType;
error: string;
is_testnet: boolean | undefined;
chainId: number;
} & (
| {
assetType: "nft";
step:
@@ -372,12 +467,7 @@ export function reportAssetCreationFailed(
}
),
) {
posthog.capture("asset creation failed", {
assetType: properties.assetType,
contractType: properties.contractType,
error: properties.error,
step: properties.step,
});
posthog.capture("asset creation failed", properties);
}
type UpsellParams = {
@@ -473,10 +563,25 @@ export function reportPaymentLinkBuyFailed(properties: {
export function reportAssetPageview(properties: {
assetType: "nft" | "coin";
chainId: number;
is_testnet: boolean | undefined;
}) {
posthog.capture("asset pageview", properties);
}
/**
* ### Why do we need to report this event?
* - To understand which chains are being viewed the most
*
* ### Who is responsible for this event?
* @MananTank
*/
export function reportChainPageview(properties: {
chainId: number;
is_testnet: boolean | undefined;
}) {
posthog.capture("chain pageview", properties);
}
/**
* ### Why do we need to report this event?
* - To track the usage of fund wallet modal
@@ -547,3 +652,53 @@ export function reportChainInfraRpcOmissionAgreed(properties: {
}) {
posthog.capture("chain infra checkout rpc omission agreed", properties);
}
// ----------------------------
// FEEDBACK
// ----------------------------
/**
* ### Why do we need to report this event?
* - To track user feedback and sentiment about the product
* - To identify common issues or feature requests
* - To measure user satisfaction and engagement
* - To prioritize product improvements based on user input
*
* ### Who is responsible for this event?
* @gisellechacon
*/
export function reportProductFeedback(properties: {
feedback: string;
source: "desktop" | "mobile";
}) {
posthog.capture("product feedback submitted", properties);
}
/**
* ### Why do we need to report this event?
* - To track conversions for the bridge page links
*
* ### Who is responsible for this event?
* @MananTank
*
*/
export function reportBridgePageLinkClick(params: {
linkType: "bridge-docs" | "trending-tokens";
}) {
posthog.capture("bridge page link clicked", params);
}
/**
* ### Why do we need to report this event?
* - To track which tokens are users are clicking on in the token list page
*
* ### Who is responsible for this event?
* @MananTank
*
*/
export function reportTokenRowClicked(params: {
chainId: number;
tokenAddress: string;
}) {
posthog.capture("token row clicked", params);
}
+5 -3
View File
@@ -448,6 +448,7 @@ type ActiveStatus = {
pay: boolean;
inAppWallet: boolean;
ecosystemWallet: boolean;
engineCloud: boolean;
};
export const isProjectActive = unstable_cache(
@@ -479,6 +480,7 @@ export const isProjectActive = unstable_cache(
pay: false,
rpc: false,
sdk: false,
engineCloud: false,
storage: false,
} as ActiveStatus;
}
@@ -488,7 +490,7 @@ export const isProjectActive = unstable_cache(
},
["isProjectActive"],
{
revalidate: 60 * 60, // 1 hour
revalidate: 30, // 30 seconds
},
);
@@ -592,7 +594,7 @@ const cached_getUniversalBridgeUsage = unstable_cache(
if (res?.status !== 200) {
const reason = await res?.text();
console.error(
`Failed to fetch universal bridge stats: ${res?.status} - ${res.statusText} - ${reason}`,
`Failed to fetch bridge stats: ${res?.status} - ${res.statusText} - ${reason}`,
);
return [];
}
@@ -636,7 +638,7 @@ const cached_getUniversalBridgeWalletUsage = unstable_cache(
if (res?.status !== 200) {
const reason = await res?.text();
console.error(
`Failed to fetch universal bridge wallet stats: ${res?.status} - ${res.statusText} - ${reason}`,
`Failed to fetch bridge wallet stats: ${res?.status} - ${res.statusText} - ${reason}`,
);
return [];
}
@@ -82,7 +82,7 @@ const CHAIN_PLAN_TO_INFRA = {
],
};
export async function getChainSubscriptions(slug: string) {
async function getChainSubscriptions(slug: string) {
const allSubscriptions = await getTeamSubscriptions(slug);
if (!allSubscriptions) {
return null;
@@ -1,28 +1,28 @@
"use server";
import type { Address } from "thirdweb";
import { getAuthToken } from "@/api/auth-token";
import { NEXT_PUBLIC_THIRDWEB_BRIDGE_HOST } from "@/constants/public-envs";
const UB_BASE_URL = NEXT_PUBLIC_THIRDWEB_BRIDGE_HOST;
type Webhook = {
export type Webhook = {
url: string;
label: string;
active: boolean;
createdAt: string;
id: string;
secret: string;
version?: number; // TODO (UB) make this mandatory after migration
};
export async function getWebhooks(props: { clientId: string; teamId: string }) {
const authToken = await getAuthToken();
export async function getWebhooks(params: {
clientId: string;
teamId: string;
authToken: string;
}) {
const res = await fetch(`${UB_BASE_URL}/v1/developer/webhooks`, {
headers: {
Authorization: `Bearer ${authToken}`,
Authorization: `Bearer ${params.authToken}`,
"Content-Type": "application/json",
"x-client-id": props.clientId,
"x-team-id": props.teamId,
"x-client-id": params.clientId,
"x-team-id": params.teamId,
},
method: "GET",
});
@@ -36,28 +36,55 @@ export async function getWebhooks(props: { clientId: string; teamId: string }) {
return json.data as Array<Webhook>;
}
export async function createWebhook(props: {
export async function getWebhookById(params: {
clientId: string;
teamId: string;
authToken: string;
webhookId: string;
}) {
const res = await fetch(
`${UB_BASE_URL}/v1/developer/webhooks/${encodeURIComponent(params.webhookId)}`,
{
headers: {
Authorization: `Bearer ${params.authToken}`,
"Content-Type": "application/json",
"x-client-id": params.clientId,
"x-team-id": params.teamId,
},
method: "GET",
},
);
if (!res.ok) {
const text = await res.text();
throw new Error(text);
}
const json = await res.json();
return json.data as Webhook;
}
export async function createWebhook(params: {
clientId: string;
teamId: string;
version?: number;
url: string;
label: string;
secret?: string;
authToken: string;
}) {
const authToken = await getAuthToken();
const res = await fetch(`${UB_BASE_URL}/v1/developer/webhooks`, {
body: JSON.stringify({
label: props.label,
secret: props.secret,
url: props.url,
version: props.version,
label: params.label,
secret: params.secret,
url: params.url,
version: params.version,
}),
headers: {
Authorization: `Bearer ${authToken}`,
Authorization: `Bearer ${params.authToken}`,
"Content-Type": "application/json",
"x-client-id": props.clientId,
"x-team-id": props.teamId,
"x-client-id": params.clientId,
"x-team-id": params.teamId,
},
method: "POST",
});
@@ -67,23 +94,23 @@ export async function createWebhook(props: {
throw new Error(text);
}
return;
return (await res.json()) as Webhook;
}
export async function deleteWebhook(props: {
export async function deleteWebhook(params: {
clientId: string;
teamId: string;
webhookId: string;
authToken: string;
}) {
const authToken = await getAuthToken();
const res = await fetch(
`${UB_BASE_URL}/v1/developer/webhooks/${props.webhookId}`,
`${UB_BASE_URL}/v1/developer/webhooks/${encodeURIComponent(params.webhookId)}`,
{
headers: {
Authorization: `Bearer ${authToken}`,
Authorization: `Bearer ${params.authToken}`,
"Content-Type": "application/json",
"x-client-id": props.clientId,
"x-team-id": props.teamId,
"x-client-id": params.clientId,
"x-team-id": params.teamId,
},
method: "DELETE",
},
@@ -94,7 +121,151 @@ export async function deleteWebhook(props: {
throw new Error(text);
}
return;
return true;
}
export async function updateWebhook(params: {
clientId: string;
teamId: string;
webhookId: string;
authToken: string;
body: {
version?: number;
url: string;
label: string;
};
}) {
const res = await fetch(
`${UB_BASE_URL}/v1/developer/webhooks/${encodeURIComponent(params.webhookId)}`,
{
method: "PUT",
body: JSON.stringify(params.body),
headers: {
Authorization: `Bearer ${params.authToken}`,
"Content-Type": "application/json",
"x-client-id": params.clientId,
"x-team-id": params.teamId,
},
},
);
if (!res.ok) {
const text = await res.text();
throw new Error(text);
}
const json = await res.json();
return json.data as Webhook;
}
export type WebhookSend = {
id: string;
webhookId: string;
webhookUrl: string;
createdAt: string;
onrampId: string | null;
paymentId: string | null;
response: string | null;
responseStatus: number;
status: "PENDING" | "COMPLETED" | "FAILED";
success: boolean;
transactionId: string | null;
body: unknown;
};
type WebhookSendsResponse = {
data: WebhookSend[];
pagination: {
limit: number;
offset: number;
total: number;
};
};
export async function getWebhookSends(options: {
authToken: string;
projectClientId: string;
teamId: string;
limit?: number;
offset?: number;
webhookId: string;
success?: boolean;
}): Promise<WebhookSendsResponse> {
const { limit, offset, success, webhookId, authToken } = options;
const url = new URL(
`${NEXT_PUBLIC_THIRDWEB_BRIDGE_HOST}/v1/developer/webhook-sends`,
);
url.searchParams.set("webhookId", webhookId);
if (limit !== undefined) {
url.searchParams.set("limit", limit.toString());
}
if (offset !== undefined) {
url.searchParams.set("offset", offset.toString());
}
if (success !== undefined) {
url.searchParams.set("success", success.toString());
}
const response = await fetch(url.toString(), {
method: "GET",
headers: {
"x-client-id": options.projectClientId,
"x-team-id": options.teamId,
Authorization: `Bearer ${authToken}`,
},
});
if (!response.ok) {
const errorJson = await response.json();
throw new Error(errorJson.message);
}
return (await response.json()) as WebhookSendsResponse;
}
export async function resendWebhook(
params: {
authToken: string;
projectClientId: string;
teamId: string;
} & (
| {
paymentId: string;
}
| {
onrampId: string;
}
),
) {
const url = new URL(
`${NEXT_PUBLIC_THIRDWEB_BRIDGE_HOST}/v1/developer/webhooks/retry`,
);
const response = await fetch(url.toString(), {
method: "POST",
body: JSON.stringify(
"paymentId" in params
? { paymentId: params.paymentId }
: { onrampId: params.onrampId },
),
headers: {
"Content-Type": "application/json",
"x-client-id": params.projectClientId,
"x-team-id": params.teamId,
Authorization: `Bearer ${params.authToken}`,
},
});
if (!response.ok) {
const errorJson = await response.json();
throw new Error(errorJson.message);
}
return true;
}
type PaymentLink = {
@@ -116,17 +287,17 @@ type PaymentLink = {
amount: bigint;
};
export async function getPaymentLinks(props: {
export async function getPaymentLinks(params: {
clientId: string;
teamId: string;
authToken: string;
}): Promise<Array<PaymentLink>> {
const authToken = await getAuthToken();
const res = await fetch(`${UB_BASE_URL}/v1/developer/links`, {
headers: {
Authorization: `Bearer ${authToken}`,
Authorization: `Bearer ${params.authToken}`,
"Content-Type": "application/json",
"x-client-id": props.clientId,
"x-team-id": props.teamId,
"x-client-id": params.clientId,
"x-team-id": params.teamId,
},
method: "GET",
});
@@ -159,7 +330,7 @@ export async function getPaymentLinks(props: {
}));
}
export async function createPaymentLink(props: {
export async function createPaymentLink(params: {
clientId: string;
teamId: string;
title: string;
@@ -171,26 +342,25 @@ export async function createPaymentLink(props: {
amount: bigint;
purchaseData?: unknown;
};
authToken: string;
}) {
const authToken = await getAuthToken();
const res = await fetch(`${UB_BASE_URL}/v1/developer/links`, {
body: JSON.stringify({
title: props.title,
imageUrl: props.imageUrl,
title: params.title,
imageUrl: params.imageUrl,
intent: {
destinationChainId: props.intent.destinationChainId,
destinationTokenAddress: props.intent.destinationTokenAddress,
receiver: props.intent.receiver,
amount: props.intent.amount.toString(),
purchaseData: props.intent.purchaseData,
destinationChainId: params.intent.destinationChainId,
destinationTokenAddress: params.intent.destinationTokenAddress,
receiver: params.intent.receiver,
amount: params.intent.amount.toString(),
purchaseData: params.intent.purchaseData,
},
}),
headers: {
Authorization: `Bearer ${authToken}`,
Authorization: `Bearer ${params.authToken}`,
"Content-Type": "application/json",
"x-client-id": props.clientId,
"x-team-id": props.teamId,
"x-client-id": params.clientId,
"x-team-id": params.teamId,
},
method: "POST",
});
@@ -207,20 +377,20 @@ export async function createPaymentLink(props: {
return response.data;
}
export async function deletePaymentLink(props: {
export async function deletePaymentLink(params: {
clientId: string;
teamId: string;
paymentLinkId: string;
authToken: string;
}) {
const authToken = await getAuthToken();
const res = await fetch(
`${UB_BASE_URL}/v1/developer/links/${props.paymentLinkId}`,
`${UB_BASE_URL}/v1/developer/links/${encodeURIComponent(params.paymentLinkId)}`,
{
headers: {
Authorization: `Bearer ${authToken}`,
Authorization: `Bearer ${params.authToken}`,
"Content-Type": "application/json",
"x-client-id": props.clientId,
"x-team-id": props.teamId,
"x-client-id": params.clientId,
"x-team-id": params.teamId,
},
method: "DELETE",
},
@@ -231,7 +401,7 @@ export async function deletePaymentLink(props: {
throw new Error(text);
}
return;
return true;
}
export type Fee = {
@@ -241,14 +411,17 @@ export type Fee = {
updatedAt: string;
};
export async function getFees(props: { clientId: string; teamId: string }) {
const authToken = await getAuthToken();
export async function getFees(params: {
clientId: string;
teamId: string;
authToken: string;
}) {
const res = await fetch(`${UB_BASE_URL}/v1/developer/fees`, {
headers: {
Authorization: `Bearer ${authToken}`,
Authorization: `Bearer ${params.authToken}`,
"Content-Type": "application/json",
"x-client-id": props.clientId,
"x-team-id": props.teamId,
"x-client-id": params.clientId,
"x-team-id": params.teamId,
},
method: "GET",
});
@@ -262,23 +435,23 @@ export async function getFees(props: { clientId: string; teamId: string }) {
return json.data as Fee;
}
export async function updateFee(props: {
export async function updateFee(params: {
clientId: string;
teamId: string;
feeRecipient: string;
feeBps: number;
authToken: string;
}) {
const authToken = await getAuthToken();
const res = await fetch(`${UB_BASE_URL}/v1/developer/fees`, {
body: JSON.stringify({
feeBps: props.feeBps,
feeRecipient: props.feeRecipient,
feeBps: params.feeBps,
feeRecipient: params.feeRecipient,
}),
headers: {
Authorization: `Bearer ${authToken}`,
Authorization: `Bearer ${params.authToken}`,
"Content-Type": "application/json",
"x-client-id": props.clientId,
"x-team-id": props.teamId,
"x-client-id": params.clientId,
"x-team-id": params.teamId,
},
method: "PUT",
});
@@ -288,7 +461,7 @@ export async function updateFee(props: {
throw new Error(text);
}
return;
return true;
}
export type PaymentsResponse = {
@@ -345,36 +518,34 @@ export type Payment = {
export type BridgePayment = Extract<Payment, { type: BridgePaymentType }>;
export async function getPayments(props: {
export async function getPayments(params: {
clientId: string;
teamId: string;
paymentLinkId?: string;
limit?: number;
authToken: string;
offset?: number;
}) {
const authToken = await getAuthToken();
// Build URL with query parameters if provided
const url = new URL(`${UB_BASE_URL}/v1/developer/payments`);
if (props.limit) {
url.searchParams.append("limit", props.limit.toString());
if (params.limit) {
url.searchParams.append("limit", params.limit.toString());
}
if (props.offset) {
url.searchParams.append("offset", props.offset.toString());
if (params.offset) {
url.searchParams.append("offset", params.offset.toString());
}
if (props.paymentLinkId) {
url.searchParams.append("paymentLinkId", props.paymentLinkId);
if (params.paymentLinkId) {
url.searchParams.append("paymentLinkId", params.paymentLinkId);
}
const res = await fetch(url.toString(), {
headers: {
Authorization: `Bearer ${authToken}`,
Authorization: `Bearer ${params.authToken}`,
"Content-Type": "application/json",
"x-client-id": props.clientId,
"x-team-id": props.teamId,
"x-client-id": params.clientId,
"x-team-id": params.teamId,
},
method: "GET",
});
@@ -21,7 +21,7 @@ export async function getUniversalBridgeTokens(props: {
headers: {
"Content-Type": "application/json",
"x-client-id": NEXT_PUBLIC_DASHBOARD_CLIENT_ID,
} as Record<string, string>,
},
method: "GET",
});
@@ -21,7 +21,7 @@ export async function addUniversalBridgeTokenRoute(props: {
Authorization: `Bearer ${authToken}`,
"Content-Type": "application/json",
"x-client-id": props.project.publishableKey,
} as Record<string, string>,
},
method: "POST",
});
@@ -0,0 +1,306 @@
/* eslint-disable no-restricted-syntax */
"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 type { Wallet } from "thirdweb/wallets";
import {
reportAssetBuyCancelled,
reportAssetBuyFailed,
reportAssetBuySuccessful,
reportSwapWidgetShown,
reportTokenBuyCancelled,
reportTokenBuyFailed,
reportTokenBuySuccessful,
reportTokenSwapCancelled,
reportTokenSwapFailed,
reportTokenSwapSuccessful,
} from "@/analytics/report";
import { Button } from "@/components/ui/button";
import {
NEXT_PUBLIC_ASSET_PAGE_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";
export type BuyAndSwapEmbedProps = {
buyTab:
| {
buyToken:
| {
tokenAddress: string;
chainId: number;
amount?: string;
}
| undefined;
}
| undefined;
swapTab:
| {
sellToken:
| {
chainId: number;
tokenAddress: string;
amount?: string;
}
| undefined;
buyToken:
| {
chainId: number;
tokenAddress: string;
amount?: string;
}
| undefined;
}
| undefined;
pageType: PageType;
wallets?: Wallet[];
};
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);
// eslint-disable-next-line no-restricted-syntax
useEffect(() => {
if (isMounted.current) {
return;
}
isMounted.current = true;
reportSwapWidgetShown({
pageType: props.pageType,
});
}, [props.pageType]);
const client = useMemo(() => {
return getConfiguredThirdwebClient({
clientId:
props.pageType === "asset"
? NEXT_PUBLIC_ASSET_PAGE_CLIENT_ID
: props.pageType === "bridge"
? NEXT_PUBLIC_BRIDGE_PAGE_CLIENT_ID
: props.pageType === "chain"
? NEXT_PUBLIC_CHAIN_PAGE_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>
{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;
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,
});
}
}}
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>
);
}
@@ -1,7 +1,7 @@
"use client";
import { useCallback, useMemo } from "react";
import type { ThirdwebClient } from "thirdweb";
import type { Bridge, ThirdwebClient } from "thirdweb";
import { MultiSelect } from "@/components/blocks/multi-select";
import { SelectWithSearch } from "@/components/blocks/select-with-search";
import { Badge } from "@/components/ui/badge";
@@ -296,3 +296,89 @@ export function SingleNetworkSelector(props: {
/>
);
}
type BridgeNetworkSelectorProps = {
chainId: number | undefined;
onChange: (chainId: number) => void;
className?: string;
popoverContentClassName?: string;
side?: "left" | "right" | "top" | "bottom";
align?: "center" | "start" | "end";
placeholder?: string;
client: ThirdwebClient;
chains: Bridge.chains.Result;
};
export function BridgeNetworkSelector(props: BridgeNetworkSelectorProps) {
const options = useMemo(() => {
return props.chains.map((chain) => {
return {
label: cleanChainName(chain.name),
value: String(chain.chainId),
};
});
}, [props.chains]);
const searchFn = useCallback(
(option: Option, searchValue: string) => {
const chain = props.chains.find(
(chain) => chain.chainId === Number(option.value),
);
if (!chain) {
return false;
}
if (Number.isInteger(Number.parseInt(searchValue))) {
return String(chain.chainId).startsWith(searchValue);
}
return chain.name.toLowerCase().includes(searchValue.toLowerCase());
},
[props.chains],
);
const renderOption = useCallback(
(option: Option) => {
const chain = props.chains.find(
(chain) => chain.chainId === Number(option.value),
);
if (!chain) {
return option.label;
}
return (
<div className="flex justify-between gap-4">
<span className="flex grow gap-2 truncate text-left">
<ChainIconClient
className="size-5"
client={props.client}
src={chain.icon}
loading="lazy"
/>
{cleanChainName(chain.name)}
</span>
</div>
);
},
[props.chains, props.client],
);
return (
<SelectWithSearch
align={props.align}
className={props.className}
closeOnSelect={true}
onValueChange={(chainId) => {
props.onChange(Number(chainId));
}}
options={options}
overrideSearchFn={searchFn}
placeholder={props.placeholder || "Select Chain"}
popoverContentClassName={props.popoverContentClassName}
renderOption={renderOption}
searchPlaceholder="Search by Name or Chain ID"
showCheck={false}
side={props.side}
value={props.chainId?.toString()}
/>
);
}
@@ -30,6 +30,7 @@ type ThirdwebAreaChartProps<TConfig extends ChartConfig> = {
description?: string;
titleClassName?: string;
headerClassName?: string;
icon?: React.ReactNode;
};
customHeader?: React.ReactNode;
// chart config
@@ -70,6 +71,7 @@ export function ThirdwebAreaChart<TConfig extends ChartConfig>(
<Card className={props.className}>
{props.header && (
<CardHeader className={props.header.headerClassName}>
{props.header.icon}
<CardTitle className={cn("mb-2", props.header.titleClassName)}>
{props.header.title}
</CardTitle>
@@ -4,9 +4,10 @@ import { MoonIcon, SunIcon } from "lucide-react";
import { useTheme } from "next-themes";
import { ClientOnly } from "@/components/blocks/client-only";
import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";
import { Skeleton } from "../ui/skeleton";
export function ToggleThemeButton() {
export function ToggleThemeButton(props: { className?: string }) {
const { setTheme, theme } = useTheme();
return (
@@ -15,7 +16,10 @@ export function ToggleThemeButton() {
>
<Button
aria-label="Toggle theme"
className="h-auto w-auto rounded-full border border-border bg-background p-2"
className={cn(
"h-auto w-auto rounded-full border border-border bg-background p-2",
props.className,
)}
onClick={() => {
setTheme(theme === "dark" ? "light" : "dark");
}}
@@ -0,0 +1,68 @@
"use client";
import { ChevronDownIcon } from "lucide-react";
import { useId, useState } from "react";
import { Button } from "@/components/ui/button";
import { DynamicHeight } from "@/components/ui/DynamicHeight";
import { cn } from "@/lib/utils";
export function FaqAccordion(props: {
faqs: Array<{ title: string; description: string }>;
}) {
return (
<div className="flex flex-col">
{props.faqs.map((faq, faqIndex) => (
<FaqItem
key={faq.title}
title={faq.title}
description={faq.description}
className={cn(faqIndex === props.faqs.length - 1 && "border-b-0")}
/>
))}
</div>
);
}
function FaqItem(props: {
title: string;
description: string;
className?: string;
}) {
const [isOpen, setIsOpenn] = useState(false);
const contentId = useId();
return (
<DynamicHeight>
<div className={cn("border-b border-dashed", props.className)}>
<h3>
<Button
variant="ghost"
onClick={() => setIsOpenn(!isOpen)}
aria-controls={contentId}
aria-expanded={isOpen}
className={cn(
"w-full justify-between h-auto py-5 text-base text-muted-foreground hover:bg-transparent pl-0 text-wrap text-left gap-6",
isOpen && "text-foreground",
)}
>
{props.title}
<ChevronDownIcon
className={cn(
"size-4 shrink-0 transition-transform duration-200",
isOpen && "rotate-180",
)}
/>
</Button>
</h3>
<p
className={cn(
"text-muted-foreground text-base leading-7 pb-6 max-w-4xl",
!isOpen && "hidden",
)}
id={contentId}
>
{props.description}
</p>
</div>
</DynamicHeight>
);
}
@@ -0,0 +1,23 @@
import { GridPattern } from "@/components/ui/background-patterns";
export function GridPatternEmbedContainer(props: {
children: React.ReactNode;
}) {
return (
<div className="flex justify-center w-full sm:border sm:border-dashed sm:bg-accent/20 sm:py-12 sm:rounded-xl sm:overflow-hidden relative">
<GridPattern
width={30}
height={30}
x={-1}
y={-1}
strokeDasharray={"4 2"}
className="text-border dark:text-border/70 hidden lg:block"
style={{
maskImage:
"linear-gradient(to bottom right,white,transparent,transparent)",
}}
/>
<div className="z-10 min-w-0">{props.children}</div>
</div>
);
}
@@ -0,0 +1,83 @@
"use client";
import { CheckIcon, CopyIcon } from "lucide-react";
import { useMemo } from "react";
import { Button } from "@/components/ui/button";
import {
HoverCard,
HoverCardContent,
HoverCardTrigger,
} from "@/components/ui/hover-card";
import { useClipboard } from "@/hooks/useClipboard";
import { cn } from "@/lib/utils";
export function SolanaAddress(props: {
address: string;
shortenAddress?: boolean;
className?: string;
}) {
const shortenedAddress = useMemo(() => {
return props.shortenAddress !== false
? `${props.address.slice(0, 4)}...${props.address.slice(-4)}`
: props.address;
}, [props.address, props.shortenAddress]);
const lessShortenedAddress = useMemo(() => {
return `${props.address.slice(0, 8)}...${props.address.slice(-8)}`;
}, [props.address]);
const { onCopy, hasCopied } = useClipboard(props.address, 2000);
return (
<HoverCard>
<HoverCardTrigger asChild tabIndex={-1}>
<Button
className={cn(
"flex flex-row items-center gap-2 px-0",
props.className,
)}
onClick={(e) => e.stopPropagation()}
variant="link"
>
<div className="flex size-5 items-center justify-center rounded-full bg-gradient-to-br from-purple-500 to-blue-500" />
<span className="cursor-pointer font-mono text-sm">
{shortenedAddress}
</span>
</Button>
</HoverCardTrigger>
<HoverCardContent
className="w-80 border-border"
onClick={(e) => {
// do not close the hover card when clicking anywhere in the content
e.stopPropagation();
}}
>
<div className="space-y-4">
<div className="flex items-center justify-between">
<h3 className="font-semibold text-lg">Solana Public Key</h3>
<Button
className="flex items-center gap-2"
onClick={onCopy}
size="sm"
variant="outline"
>
{hasCopied ? (
<CheckIcon className="h-4 w-4" />
) : (
<CopyIcon className="h-4 w-4" />
)}
{hasCopied ? "Copied!" : "Copy"}
</Button>
</div>
<p className="break-all rounded bg-muted p-3 text-center font-mono text-xs leading-relaxed">
{lessShortenedAddress}
</p>
<div className="rounded-lg bg-muted/50 p-3">
<p className="text-muted-foreground text-xs leading-relaxed">
Solana public key for blockchain transactions.
</p>
</div>
</div>
</HoverCardContent>
</HoverCard>
);
}
@@ -0,0 +1,120 @@
import type { Meta, StoryObj } from "@storybook/nextjs";
import { type ThirdwebClient, ZERO_ADDRESS } from "thirdweb";
import type { SocialProfile } from "thirdweb/react";
import { BadgeContainer, storybookThirdwebClient } from "@/storybook/utils";
import { WalletAddress, WalletAddressUI } from "./wallet-address";
const meta = {
component: Story,
parameters: {
nextjs: {
appDirectory: true,
},
},
title: "blocks/WalletAddress",
} satisfies Meta<typeof Story>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Variants: Story = {
args: {},
};
function Story() {
const client = storybookThirdwebClient as unknown as ThirdwebClient;
return (
<div className="container flex max-w-4xl flex-col gap-8 py-10">
<BadgeContainer label="Social Profiles Loaded">
<WalletAddressUI
address="0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045"
client={client}
profiles={{
data: vitalikEth,
isPending: false,
}}
/>
</BadgeContainer>
<BadgeContainer label="No Social Profiles">
<WalletAddressUI
address="0x83Dd93fA5D8343094f850f90B3fb90088C1bB425"
client={client}
shortenAddress
profiles={{
data: [],
isPending: false,
}}
/>
</BadgeContainer>
<BadgeContainer label="Loading Social Profiles">
<WalletAddressUI
address="0x83Dd93fA5D8343094f850f90B3fb90088C1bB425"
client={client}
shortenAddress
profiles={{
data: [],
isPending: true,
}}
/>
</BadgeContainer>
<BadgeContainer label="Zero address">
<WalletAddressUI
address={ZERO_ADDRESS}
client={client}
profiles={{
data: [],
isPending: false,
}}
/>
</BadgeContainer>
<BadgeContainer label="Real Component - Invalid Address">
<WalletAddress address="not-an-address" client={client} />
</BadgeContainer>
<BadgeContainer label="Real Component - vitalik.eth">
<WalletAddress
address="0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045"
client={client}
/>
</BadgeContainer>
</div>
);
}
const vitalikEth: SocialProfile[] = [
{
type: "ens",
name: "vitalik.eth",
bio: "mi pinxe lo crino tcati",
avatar: "https://euc.li/vitalik.eth",
metadata: {
name: "vitalik.eth",
address: "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045",
avatar: "https://euc.li/vitalik.eth",
description: "mi pinxe lo crino tcati",
url: "https://vitalik.ca",
},
},
{
type: "farcaster",
name: "vitalik.eth",
bio: undefined,
avatar:
"https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/b663cd63-fecf-4d0f-7f87-0e0b6fd42800/original",
metadata: {
fid: 5650,
bio: undefined,
pfp: "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/b663cd63-fecf-4d0f-7f87-0e0b6fd42800/original",
username: "vitalik.eth",
addresses: [
"0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045",
"0x96B6bB2bd2Eba3b4Fbefd7DbAC448ad7B6CBf279",
],
},
},
];
@@ -1,11 +1,10 @@
"use client";
import { CheckIcon, CircleSlashIcon, CopyIcon, XIcon } from "lucide-react";
import { CircleSlashIcon, XIcon } from "lucide-react";
import { useMemo } from "react";
import { isAddress, type ThirdwebClient, ZERO_ADDRESS } from "thirdweb";
import { Blobbie, type SocialProfile, useSocialProfiles } from "thirdweb/react";
import { Img } from "@/components/blocks/Img";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
HoverCard,
@@ -13,22 +12,49 @@ import {
HoverCardTrigger,
} from "@/components/ui/hover-card";
import { ToolTipLabel } from "@/components/ui/tooltip";
import { useClipboard } from "@/hooks/useClipboard";
import { cn } from "@/lib/utils";
import { resolveSchemeWithErrorHandler } from "@/utils/resolveSchemeWithErrorHandler";
import { CopyTextButton } from "../ui/CopyTextButton";
import { Skeleton } from "../ui/skeleton";
export function WalletAddress(props: {
type WalletAddressProps = {
address: string | undefined;
shortenAddress?: boolean;
className?: string;
iconClassName?: string;
client: ThirdwebClient;
fallbackIcon?: React.ReactNode;
}) {
};
export function WalletAddress(props: WalletAddressProps) {
const profiles = useSocialProfiles({
address: props.address,
client: props.client,
});
return (
<WalletAddressUI
{...props}
profiles={{
data: profiles.data || [],
isPending: profiles.isPending,
}}
/>
);
}
export function WalletAddressUI(
props: WalletAddressProps & {
profiles: {
data: SocialProfile[];
isPending: boolean;
};
},
) {
// default back to zero address if no address provided
const address = useMemo(() => props.address || ZERO_ADDRESS, [props.address]);
const [shortenedAddress, lessShortenedAddress] = useMemo(() => {
const [shortenedAddress, _lessShortenedAddress] = useMemo(() => {
return [
props.shortenAddress !== false
? `${address.slice(0, 6)}...${address.slice(-4)}`
@@ -37,17 +63,10 @@ export function WalletAddress(props: {
];
}, [address, props.shortenAddress]);
const profiles = useSocialProfiles({
address: address,
client: props.client,
});
const { onCopy, hasCopied } = useClipboard(address, 2000);
if (!isAddress(address)) {
return (
<ToolTipLabel hoverable label={address}>
<span className="flex items-center gap-2 underline-offset-4 hover:underline">
<span className="flex items-center gap-2 underline-offset-4 hover:underline w-fit">
<div className="flex size-5 items-center justify-center rounded-full border bg-background">
<XIcon className="size-4 text-muted-foreground" />
</div>
@@ -78,7 +97,7 @@ export function WalletAddress(props: {
<HoverCardTrigger asChild tabIndex={-1}>
<Button
className={cn(
"flex flex-row items-center gap-2 px-0",
"flex flex-row items-center gap-2 px-0 max-w-full truncate",
props.className,
)}
onClick={(e) => e.stopPropagation()}
@@ -88,89 +107,82 @@ export function WalletAddress(props: {
<WalletAvatar
address={address}
iconClassName={props.iconClassName}
profiles={profiles.data || []}
profiles={props.profiles.data}
thirdwebClient={props.client}
fallbackIcon={props.fallbackIcon}
/>
)}
<span className="cursor-pointer font-mono">
{profiles.data?.[0]?.name || shortenedAddress}
<span className="cursor-pointer font-mono max-w-full truncate">
{props.profiles.data?.[0]?.name || shortenedAddress}
</span>
</Button>
</HoverCardTrigger>
<HoverCardContent
className="w-80 border-border"
className="w-[calc(100vw-2rem)] lg:w-[450px] border-border rounded-xl p-4 lg:p-6"
onClick={(e) => {
// do not close the hover card when clicking anywhere in the content
e.stopPropagation();
}}
>
<div className="space-y-4">
<div className="flex items-center justify-between">
<h3 className="font-semibold text-lg">Wallet Address</h3>
<Button
className="flex items-center gap-2"
onClick={onCopy}
size="sm"
variant="outline"
>
{hasCopied ? (
<CheckIcon className="h-4 w-4" />
) : (
<CopyIcon className="h-4 w-4" />
)}
{hasCopied ? "Copied!" : "Copy"}
</Button>
</div>
<p className="rounded bg-muted p-2 text-center font-mono text-sm">
{lessShortenedAddress}
</p>
<h3 className="font-semibold text-lg">Social Profiles</h3>
{profiles.isPending ? (
<p className="text-muted-foreground text-sm">Loading profiles...</p>
) : !profiles.data?.length ? (
<p className="text-muted-foreground text-sm">No profiles found</p>
) : (
profiles.data?.map((profile) => {
const walletAvatarLink = resolveSchemeWithErrorHandler({
client: props.client,
uri: profile.avatar,
});
<div className="space-y-1">
<h3 className="font-medium text-sm">Wallet Address</h3>
return (
<div
className="flex flex-row items-center gap-2"
key={profile.type + profile.name}
>
{walletAvatarLink && (
<Avatar>
<AvatarImage
alt={profile.name}
className="object-cover"
src={walletAvatarLink}
/>
{profile.name && (
<AvatarFallback>
{profile.name.slice(0, 2)}
</AvatarFallback>
)}
</Avatar>
)}
<div className="flex w-full flex-col gap-1">
<div className="flex w-full flex-row items-center justify-between gap-4">
<h4 className="font-semibold text-md">{profile.name}</h4>
<Badge variant="outline">{profile.type}</Badge>
<CopyTextButton
textToShow={address}
textToCopy={address}
tooltip="Copy address"
copyIconPosition="right"
variant="ghost"
className="text-muted-foreground -translate-x-1.5"
/>
</div>
<div className="space-y-1 border-t pt-5 border-dashed">
<h3 className="font-medium text-sm">Social Profiles</h3>
{props.profiles.isPending ? (
<Skeleton className="h-4 w-[50%]" />
) : !props.profiles.data?.length ? (
<p className="text-muted-foreground text-sm">No profiles found</p>
) : (
<div className="!mt-2">
{props.profiles.data?.map((profile) => {
const walletAvatarLink = resolveSchemeWithErrorHandler({
client: props.client,
uri: profile.avatar,
});
return (
<div
className="flex flex-row items-center gap-3 py-2"
key={profile.type + profile.name}
>
<Avatar className="size-9">
<AvatarImage
alt={profile.name}
className="object-cover"
src={walletAvatarLink}
/>
{profile.name && (
<AvatarFallback>
{profile.name.slice(0, 2)}
</AvatarFallback>
)}
</Avatar>
<div className="space-y-0.5">
<h4 className="text-sm leading-none">{profile.name}</h4>
<span className="text-muted-foreground text-xs leading-none capitalize">
{profile.type === "ens" ? "ENS" : profile.type}
</span>
</div>
</div>
{profile.bio && (
<p className="line-clamp-1 whitespace-normal text-muted-foreground text-sm">
{profile.bio}
</p>
)}
</div>
</div>
);
})
)}
);
})}
</div>
)}
</div>
</div>
</HoverCardContent>
</HoverCard>
@@ -1,6 +1,6 @@
"use client";
import { MessageCircleIcon, XIcon } from "lucide-react";
import { CircleQuestionMarkIcon, XIcon } from "lucide-react";
import { useSelectedLayoutSegments } from "next/navigation";
import { useCallback, useRef, useState } from "react";
import { createThirdwebClient } from "thirdweb";
@@ -16,7 +16,6 @@ const client = createThirdwebClient({
});
export function CustomChatButton(props: {
label: string;
examplePrompts: string[];
authToken: string;
team: Team;
@@ -39,21 +38,25 @@ export function CustomChatButton(props: {
<>
{/* Inline Button (not floating) */}
<Button
className="gap-2 rounded-full shadow-lg"
className={cn(
"gap-2 rounded-full shadow-lg fixed right-6 bottom-6 z-50 bg-card",
isOpen && "opacity-0",
)}
size="sm"
onClick={() => {
setIsOpen(true);
setHasBeenOpened(true);
}}
variant="default"
variant="outline"
>
<MessageCircleIcon className="size-4" />
{props.label}
<CircleQuestionMarkIcon className="size-3.5 text-muted-foreground" />
Need help?
</Button>
{/* Popup/Modal */}
<div
className={cn(
"slide-in-from-bottom-20 zoom-in-95 fade-in-0 fixed bottom-0 left-0 z-50 flex h-[80vh] w-[100vw] animate-in flex-col overflow-hidden rounded-t-2xl border bg-background shadow-2xl duration-200 lg:right-6 lg:bottom-6 lg:left-auto lg:h-[80vh] lg:max-w-xl lg:rounded-xl",
"slide-in-from-bottom-20 zoom-in-95 fade-in-0 fixed bottom-0 left-0 z-50 flex h-[80vh] w-[100vw] animate-in flex-col overflow-hidden rounded-t-2xl border bg-background shadow-2xl duration-200 lg:right-6 lg:bottom-6 lg:left-auto lg:h-[80vh] lg:max-w-xl lg:rounded-2xl",
!isOpen && "hidden",
)}
ref={ref}
@@ -61,8 +64,7 @@ export function CustomChatButton(props: {
{/* Header with close button */}
<div className="flex items-center justify-between border-b px-4 py-4">
<div className="flex items-center gap-2 font-semibold text-lg">
<MessageCircleIcon className="size-5 text-muted-foreground" />
{props.label}
Need help?
</div>
<Button
aria-label="Close chat"
@@ -17,6 +17,7 @@ import { resetAnalytics } from "@/analytics/reset";
import { CustomChainRenderer } from "@/components/misc/CustomChainRenderer";
import { LazyConfigureNetworkModal } from "@/components/misc/configure-networks/LazyConfigureNetworkModal";
import { Button } from "@/components/ui/button";
import { appMetadata } from "@/constants/connect";
import { popularChains } from "@/constants/popularChains";
import { useAllChainsData } from "@/hooks/chains/allChains";
import { useFavoriteChainIds } from "@/hooks/favorite-chains";
@@ -138,11 +139,7 @@ export const CustomConnectWallet = (props: {
return (
<>
<ConnectButton
appMetadata={{
logoUrl: "https://thirdweb.com/favicon.ico",
name: "thirdweb",
url: "https://thirdweb.com",
}}
appMetadata={appMetadata}
chain={props.chain}
chains={allChainsV5}
client={client}
@@ -280,11 +277,7 @@ export function useCustomConnectModal() {
return useCallback(
(options: { chain?: Chain; client: ThirdwebClient }) => {
return connect({
appMetadata: {
logoUrl: "https://thirdweb.com/favicon.ico",
name: "thirdweb",
url: "https://thirdweb.com",
},
appMetadata,
chain: options?.chain,
client: options.client,
privacyPolicyUrl: "/privacy-policy",
@@ -14,6 +14,15 @@ import {
} from "@/components/ui/select";
import type { SearchType } from "./types";
const searchTypeLabels: Record<SearchType, string> = {
email: "Email",
phone: "Phone",
id: "Auth Identifier",
address: "Address",
externalWallet: "External Wallet",
userId: "User Identifier",
};
export function AdvancedSearchInput(props: {
onSearch: (searchType: SearchType, query: string) => void;
onClear: () => void;
@@ -46,13 +55,14 @@ export function AdvancedSearchInput(props: {
value={searchType}
onValueChange={(value) => setSearchType(value as SearchType)}
>
<SelectTrigger className="w-[140px] bg-card">
<SelectTrigger className="w-[140px] bg-background rounded-full">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="email">Email</SelectItem>
<SelectItem value="phone">Phone</SelectItem>
<SelectItem value="id">ID</SelectItem>
<SelectItem value="id">Auth Identifier</SelectItem>
<SelectItem value="userId">User Identifier</SelectItem>
<SelectItem value="address">Address</SelectItem>
<SelectItem value="externalWallet">External Wallet</SelectItem>
</SelectContent>
@@ -61,8 +71,8 @@ export function AdvancedSearchInput(props: {
<div className="flex flex-1">
<div className="relative flex-1">
<Input
className="bg-card pl-9 border-r-0 rounded-r-none"
placeholder={`Search by ${searchType}...`}
className="bg-background pl-9 border-r-0 rounded-r-none rounded-l-full"
placeholder={`Search by ${searchTypeLabels[searchType]}...`}
value={query}
onChange={(e) => setQuery(e.target.value)}
onKeyDown={handleKeyDown}
@@ -86,7 +96,7 @@ export function AdvancedSearchInput(props: {
onClick={handleSearch}
variant="outline"
disabled={!query.trim() || props.isLoading}
className="rounded-l-none gap-2 bg-card disabled:opacity-100"
className="rounded-l-none rounded-r-full gap-2 bg-background disabled:opacity-100"
>
{props.isLoading && <Spinner className="size-4" />}
Search
@@ -12,18 +12,25 @@ import {
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { CopyTextButton } from "../ui/CopyTextButton";
const getUserIdentifier = (user: WalletUser) => {
const getAuthIdentifier = (user: WalletUser) => {
const mainDetail = user.linkedAccounts[0]?.details;
return (
mainDetail?.id ??
mainDetail?.email ??
mainDetail?.phone ??
mainDetail?.address ??
mainDetail?.id ??
user.id
mainDetail?.address
);
};
const truncateIdentifier = (value: string) => {
if (value.length <= 18) {
return value;
}
return `${value.slice(0, 8)}...${value.slice(-4)}`;
};
export function SearchResults(props: {
results: WalletUser[];
client: ThirdwebClient;
@@ -51,6 +58,8 @@ export function SearchResults(props: {
const mainDetail = user.linkedAccounts?.[0]?.details;
const email = mainDetail?.email as string | undefined;
const phone = mainDetail?.phone as string | undefined;
const authIdentifier = getAuthIdentifier(user);
const userIdentifier = user.id;
// Get external wallet addresses from linkedAccounts where type is 'siwe'
const externalWalletAccounts =
@@ -68,7 +77,34 @@ export function SearchResults(props: {
<p className="text-sm font-medium text-muted-foreground">
User Identifier
</p>
<p className="text-sm">{getUserIdentifier(user)}</p>
{userIdentifier ? (
<CopyTextButton
textToShow={truncateIdentifier(userIdentifier)}
textToCopy={userIdentifier}
tooltip="Copy User Identifier"
copyIconPosition="left"
variant="ghost"
/>
) : (
<p className="text-sm">N/A</p>
)}
</div>
<div>
<p className="text-sm font-medium text-muted-foreground">
Auth Identifier
</p>
{authIdentifier ? (
<CopyTextButton
textToShow={truncateIdentifier(authIdentifier)}
textToCopy={authIdentifier}
tooltip="Copy Auth Identifier"
copyIconPosition="left"
variant="ghost"
/>
) : (
<p className="text-sm">N/A</p>
)}
</div>
{walletAddress && (
@@ -2,7 +2,7 @@
import { createColumnHelper } from "@tanstack/react-table";
import { format } from "date-fns";
import { ArrowLeftIcon, ArrowRightIcon } from "lucide-react";
import { ArrowLeftIcon, ArrowRightIcon, UserIcon } from "lucide-react";
import Papa from "papaparse";
import { useCallback, useMemo, useState } from "react";
import type { ThirdwebClient } from "thirdweb";
@@ -28,22 +28,42 @@ import { SearchResults } from "./SearchResults";
import { searchUsers } from "./searchUsers";
import type { SearchType } from "./types";
const getUserIdentifier = (accounts: WalletUser["linkedAccounts"]) => {
const getAuthIdentifier = (accounts: WalletUser["linkedAccounts"]) => {
const mainDetail = accounts[0]?.details;
return (
mainDetail?.id ??
mainDetail?.email ??
mainDetail?.phone ??
mainDetail?.address ??
mainDetail?.id
mainDetail?.address
);
};
const getExternalWallets = (accounts: WalletUser["linkedAccounts"]) => {
return accounts?.filter((account) => account.type === "siwe") || [];
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;
@@ -56,9 +76,30 @@ export function InAppWalletUsersPageContent(
) {
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 = getUserIdentifier(cell.getValue());
const identifier = getAuthIdentifier(cell.getValue());
if (!identifier) {
return "N/A";
@@ -66,21 +107,17 @@ export function InAppWalletUsersPageContent(
return (
<CopyTextButton
textToShow={
identifier.length > 30
? `${identifier.slice(0, 30)}...`
: identifier
}
textToShow={truncateIdentifier(identifier)}
textToCopy={identifier}
tooltip="Copy User Identifier"
tooltip="Copy Auth Identifier"
copyIconPosition="left"
variant="ghost"
/>
);
},
enableColumnFilter: true,
header: "User Identifier",
id: "user_identifier",
header: "Auth Identifier",
id: "auth_identifier",
}),
columnHelper.accessor("wallets", {
cell: (cell) => {
@@ -94,33 +131,24 @@ export function InAppWalletUsersPageContent(
}),
columnHelper.accessor("linkedAccounts", {
cell: (cell) => {
const externalWallets = getExternalWallets(cell.getValue());
if (externalWallets.length === 0) {
return <span className="text-muted-foreground text-sm">None</span>;
const email = getPrimaryEmail(cell.getValue());
if (!email) {
return <span className="text-muted-foreground text-sm">N/A</span>;
}
return (
<div className="space-y-1">
{externalWallets.slice(0, 2).map((account) => {
const address = account.details?.address as string | undefined;
return address ? (
<div
key={`external-${address}-${account.details?.id}`}
className="text-xs"
>
<WalletAddress address={address} client={props.client} />
</div>
) : null;
})}
{externalWallets.length > 2 && (
<span className="text-muted-foreground text-xs">
+{externalWallets.length - 2} more
</span>
)}
</div>
<CopyTextButton
textToShow={email}
textToCopy={email}
tooltip="Copy Email"
copyIconPosition="left"
variant="ghost"
/>
);
},
header: "External Wallets",
id: "external_wallets",
header: "Email",
id: "email",
}),
columnHelper.accessor("wallets", {
cell: (cell) => {
@@ -230,20 +258,17 @@ export function InAppWalletUsersPageContent(
});
const csv = Papa.unparse(
usersWallets.map((row) => {
const externalWallets = getExternalWallets(row.linkedAccounts);
const externalWalletAddresses = externalWallets
.map((account) => account.details?.address)
.filter(Boolean)
.join(", ");
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",
external_wallets: externalWalletAddresses || "None",
email: email || "N/A",
login_methods: row.linkedAccounts.map((acc) => acc.type).join(", "),
user_identifier: getUserIdentifier(row.linkedAccounts),
auth_identifier: getAuthIdentifier(row.linkedAccounts) || "N/A",
user_identifier: row.id || "N/A",
};
}),
);
@@ -264,11 +289,24 @@ export function InAppWalletUsersPageContent(
return (
<div>
<div className="flex flex-col gap-4">
{/* Top section */}
<div className="flex flex-col gap-4">
<div className="flex flex-col md:flex-row lg:items-center justify-end gap-3">
<div className="w-full max-w-lg">
<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}
@@ -277,7 +315,7 @@ export function InAppWalletUsersPageContent(
/>
</div>
<Button
className="gap-2 bg-card"
className="gap-2 bg-background rounded-full"
disabled={wallets.length === 0 || isPending}
onClick={downloadCSV}
variant="outline"
@@ -298,11 +336,11 @@ export function InAppWalletUsersPageContent(
data={wallets}
isFetched={walletsQuery.isFetched}
isPending={walletsQuery.isPending}
tableContainerClassName="rounded-b-none"
tableContainerClassName="rounded-none border-x-0 border-b-0"
title="in-app wallets"
/>
<div className="flex justify-center gap-3 rounded-b-lg border border-t-0 bg-card p-6">
<div className="flex justify-center gap-3 border-t bg-card p-6">
<Button
className="gap-2 bg-background"
disabled={activePage === 1 || walletsQuery.isPending}
@@ -18,10 +18,10 @@ configure({
type APIWallet = ListUserWalletsResponses[200]["result"]["wallets"][0];
type APIProfile = APIWallet["profiles"][0];
// Transform API response to match existing WalletUser format
// Transform API response to match wallet user format
function transformToWalletUser(apiWallet: APIWallet): WalletUser {
return {
id: getProfileId(apiWallet.profiles[0]) || "",
id: apiWallet.userId || getProfileId(apiWallet.profiles[0]) || "",
linkedAccounts: apiWallet.profiles.map((profile) => {
// Create details object based on the profile data
let details:
@@ -105,6 +105,9 @@ export async function searchUsers(
case "externalWallet":
queryParams.externalWalletAddress = query;
break;
case "userId":
queryParams.userId = query;
break;
}
// Use the generated API function with Bearer authentication
@@ -3,4 +3,5 @@ export type SearchType =
| "phone"
| "id"
| "address"
| "externalWallet";
| "externalWallet"
| "userId";
@@ -5,6 +5,7 @@ import Link from "next/link";
import { Button } from "@/components/ui/button";
import { useLocalStorage } from "@/hooks/useLocalStorage";
// biome-ignore lint/correctness/noUnusedVariables: banner is toggled on-demand via API content changes
function AnnouncementBannerUI(props: {
href: string;
label: string;
@@ -44,11 +45,12 @@ function AnnouncementBannerUI(props: {
}
export function AnnouncementBanner() {
return (
<AnnouncementBannerUI
href="https://blog.thirdweb.com/the-fastest-way-to-build-web3-applications/"
label="We have re-branded our Engine, Payments, and Connect products. Please read the full blog post for details on changes"
trackingLabel="product-rebrand"
/>
);
// return (
// <AnnouncementBannerUI
// href="https://blog.thirdweb.com/the-fastest-way-to-build-web3-applications/"
// label="We have re-branded our Engine, Payments, and Connect products. Please read the full blog post for details on changes"
// trackingLabel="product-rebrand"
// />
// );
return null;
}
@@ -64,7 +64,7 @@ const CreateProjectDialog = (props: CreateProjectDialogProps) => {
<CreateProjectDialogUI
createProject={async (params) => {
const res = await createProjectClient(props.teamId, params);
await createVaultAccountAndAccessToken({
const vaultTokens = await createVaultAccountAndAccessToken({
project: res.project,
projectSecretKey: res.secret,
}).catch((error) => {
@@ -74,6 +74,13 @@ const CreateProjectDialog = (props: CreateProjectDialogProps) => {
);
throw error;
});
const managementAccessToken = vaultTokens.managementToken?.accessToken;
if (!managementAccessToken) {
throw new Error("Missing management access token for project wallet");
}
return {
project: res.project,
secret: res.secret,
@@ -41,7 +41,7 @@ export function CopyTextButton(props: {
<Button
aria-label={props.tooltip}
className={cn(
"flex h-auto w-auto gap-2 rounded-lg px-1.5 py-0.5 font-normal text-foreground",
"flex h-auto w-auto gap-2 rounded-lg px-1.5 py-0.5 font-normal text-foreground max-w-full truncate",
props.className,
)}
onClick={(e) => {
@@ -0,0 +1,45 @@
"use client";
import { ArrowUpRightIcon, CheckIcon, CopyIcon } from "lucide-react";
import Link from "next/link";
import { useState } from "react";
import { cn } from "@/lib/utils";
import { Button } from "./button";
import { ToolTipLabel } from "./tooltip";
export function LinkWithCopyButton(props: {
href: string;
textToShow: string;
textToCopy: string;
copyTooltip: string;
className?: string;
}) {
const [isCopied, setIsCopied] = useState(false);
const Icon = isCopied ? CheckIcon : CopyIcon;
return (
<div className={cn("flex items-center gap-1", props.className)}>
<ToolTipLabel label={props.copyTooltip}>
<Button
variant="ghost"
size="sm"
className="p-1 h-auto shrink-0 opacity-70 hover:opacity-100 text-muted-foreground"
onClick={() => {
navigator.clipboard.writeText(props.textToCopy);
setIsCopied(true);
setTimeout(() => setIsCopied(false), 1000);
}}
>
<Icon className="size-3" />
</Button>
</ToolTipLabel>
<Link
href={props.href}
target="_blank"
className="text-sm text-muted-foreground hover:underline flex items-center gap-1 tabular-nums flex-1 truncate hover:text-foreground group"
>
<span className="max-w-full truncate">{props.textToShow}</span>
<ArrowUpRightIcon className="size-3.5 opacity-70 shrink-0 group-hover:opacity-100" />
</Link>
</div>
);
}
@@ -0,0 +1,5 @@
export const appMetadata = {
logoUrl: "https://thirdweb.com/favicon.ico",
name: "thirdweb",
url: "https://thirdweb.com",
};
@@ -32,3 +32,12 @@ export const NEXT_PUBLIC_DEMO_ENGINE_URL =
export const NEXT_PUBLIC_THIRDWEB_AI_HOST =
process.env.NEXT_PUBLIC_THIRDWEB_AI_HOST || "https://nebula-api.thirdweb.com";
export const NEXT_PUBLIC_BRIDGE_PAGE_CLIENT_ID =
process.env.NEXT_PUBLIC_BRIDGE_PAGE_CLIENT_ID;
export const NEXT_PUBLIC_CHAIN_PAGE_CLIENT_ID =
process.env.NEXT_PUBLIC_CHAIN_PAGE_CLIENT_ID;
export const NEXT_PUBLIC_ASSET_PAGE_CLIENT_ID =
process.env.NEXT_PUBLIC_ASSET_PAGE_CLIENT_ID;
@@ -91,3 +91,8 @@ if (REDIS_URL) {
REDIS_URL,
);
}
// DO NOT TAINT THESE VALUES (for now)
export const GROWTH_PLAN_SKU = process.env.GROWTH_PLAN_SKU || "";
export const PAYMENT_METHOD_CONFIGURATION =
process.env.PAYMENT_METHOD_CONFIGURATION || "";
@@ -25,6 +25,7 @@ import { getVercelEnv } from "@/utils/vercel";
export function getConfiguredThirdwebClient(options: {
secretKey: string | undefined;
teamId: string | undefined;
clientId?: string;
}): ThirdwebClient {
if (getVercelEnv() !== "production") {
// if not on production: run this when creating a client to set the domains
@@ -77,7 +78,8 @@ export function getConfiguredThirdwebClient(options: {
}
// During build time, provide fallbacks if credentials are missing
const clientId = NEXT_PUBLIC_DASHBOARD_CLIENT_ID || "dummy-build-client";
const clientId =
options.clientId || NEXT_PUBLIC_DASHBOARD_CLIENT_ID || "dummy-build-client";
const secretKey = options.secretKey || undefined;
return createThirdwebClient({
+9 -4
View File
@@ -119,12 +119,17 @@ export function useCsvUpload<
}
Papa.parse(csv, {
complete: (results) => {
const data = props.csvParser(results.data as T[]);
if (!data[0]?.address) {
try {
const data = props.csvParser(results.data as T[]);
if (!data[0]?.address) {
setNoCsv(true);
return;
}
setRawData(data);
} catch (error) {
console.error(error);
setNoCsv(true);
return;
}
setRawData(data);
},
header: true,
});
@@ -59,7 +59,10 @@ export async function fetchDashboardContractMetadata(
return {
contractType: compilerMetadata?.name || "",
image: contractMetadata.image || "",
image:
contractMetadata.image && typeof contractMetadata.image === "string"
? contractMetadata.image
: "",
name: contractName,
symbol: contractSymbol,
};
@@ -76,10 +76,10 @@ const fetchAccountList = ({
};
};
// Transform API response to match existing WalletUser format
// Transform API response to match the wallet user format
function transformToWalletUser(apiWallet: APIWallet): WalletUser {
return {
id: getProfileId(apiWallet.profiles[0]) || "",
id: apiWallet.userId || getProfileId(apiWallet.profiles[0]) || "",
linkedAccounts: apiWallet.profiles.map((profile) => {
// Create details object based on the profile data
let details:
+16 -2
View File
@@ -379,12 +379,27 @@ export function useEngineTransactions(params: {
page?: number;
status?: EngineStatus;
};
id?: string;
}) {
const { instanceUrl, autoUpdate, authToken } = params;
const { instanceUrl, autoUpdate, authToken, id } = params;
return useQuery({
placeholderData: keepPreviousData,
queryFn: async () => {
if (id) {
const res = await fetch(`${instanceUrl}transaction/status/${id}`, {
headers: getEngineRequestHeaders(authToken),
method: "GET",
});
const json = await res.json();
const transaction = (json.result as Transaction) || {};
return {
transactions: [transaction],
totalCount: 1,
};
}
const url = new URL(`${instanceUrl}transaction/get-all`);
if (params.queryParams) {
for (const key in params.queryParams) {
@@ -402,7 +417,6 @@ export function useEngineTransactions(params: {
});
const json = await res.json();
return (json.result as TransactionResponse) || {};
},
queryKey: engineKeys.transactions(instanceUrl, params),
@@ -0,0 +1,21 @@
const PROJECT_WALLET_LABEL_SUFFIX = " Wallet";
const PROJECT_WALLET_LABEL_MAX_LENGTH = 32;
/**
* Builds the default label for a project's primary server wallet.
* Ensures a stable naming convention so the wallet can be identified across flows.
*/
export function getProjectWalletLabel(projectName: string | undefined) {
const baseName = projectName?.trim() || "Project";
const maxBaseLength = Math.max(
1,
PROJECT_WALLET_LABEL_MAX_LENGTH - PROJECT_WALLET_LABEL_SUFFIX.length,
);
const normalizedBase =
baseName.length > maxBaseLength
? baseName.slice(0, maxBaseLength).trimEnd()
: baseName;
return `${normalizedBase}${PROJECT_WALLET_LABEL_SUFFIX}`;
}
@@ -0,0 +1,99 @@
import "server-only";
import { createVaultClient, listEoas } from "@thirdweb-dev/vault-sdk";
import type { Project } from "@/api/project/projects";
import { NEXT_PUBLIC_THIRDWEB_VAULT_URL } from "@/constants/public-envs";
import { getProjectWalletLabel } from "@/lib/project-wallet";
export type ProjectWalletSummary = {
id: string;
address: string;
label?: string;
};
type VaultWalletListItem = {
id: string;
address: string;
metadata?: {
label?: string;
projectId?: string;
teamId?: string;
type?: string;
};
};
export async function getProjectWallet(
project: Project,
): Promise<ProjectWalletSummary | undefined> {
const engineCloudService = project.services.find(
(service) => service.name === "engineCloud",
);
const managementAccessToken =
engineCloudService?.managementAccessToken || undefined;
const projectWalletAddress = (
engineCloudService as { projectWalletAddress?: string } | undefined
)?.projectWalletAddress;
if (
!managementAccessToken ||
!NEXT_PUBLIC_THIRDWEB_VAULT_URL ||
!projectWalletAddress
) {
return undefined;
}
try {
const vaultClient = await createVaultClient({
baseUrl: NEXT_PUBLIC_THIRDWEB_VAULT_URL,
});
const response = await listEoas({
client: vaultClient,
request: {
auth: {
accessToken: managementAccessToken,
},
options: {
page: 0,
// @ts-expect-error - SDK expects snake_case for pagination arguments
page_size: 100,
},
},
});
if (!response.success || !response.data) {
return undefined;
}
const items = response.data.items as VaultWalletListItem[] | undefined;
if (!items?.length) {
return undefined;
}
const defaultLabel = getProjectWalletLabel(project.name);
const serverWallets = items.filter(
(item) => item.metadata?.projectId === project.id,
);
const defaultWallet = serverWallets.find(
(item) =>
item.address.toLowerCase() === projectWalletAddress.toLowerCase(),
);
if (!defaultWallet) {
return undefined;
}
return {
id: defaultWallet.id,
address: defaultWallet.address,
label: defaultWallet.metadata?.label ?? defaultLabel,
};
} catch (error) {
console.error("Failed to load project wallet", error);
return undefined;
}
}
@@ -31,9 +31,9 @@ export function getSDKTheme(theme: "light" | "dark"): Theme {
selectedTextBg: "hsl(var(--inverted))",
selectedTextColor: "hsl(var(--inverted-foreground))",
separatorLine: "hsl(var(--border))",
skeletonBg: "hsl(var(--muted))",
skeletonBg: "hsl(var(--secondary-foreground)/15%)",
success: "hsl(var(--success-text))",
tertiaryBg: "hsl(var(--muted)/50%)",
tertiaryBg: "hsl(var(--muted)/30%)",
tooltipBg: "hsl(var(--popover))",
tooltipText: "hsl(var(--popover-foreground))",
},
@@ -1,12 +0,0 @@
import { TeamHeader } from "../../team/components/TeamHeader/team-header";
export default function Layout({ children }: { children: React.ReactNode }) {
return (
<div className="flex grow flex-col">
<div className="border-border border-b bg-card">
<TeamHeader />
</div>
{children}
</div>
);
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 536 KiB

@@ -1,37 +0,0 @@
"use client";
import { usePathname, useSearchParams } from "next/navigation";
import { useCallback } from "react";
import { PaginationButtons } from "@/components/blocks/pagination-buttons";
import { useDashboardRouter } from "@/lib/DashboardRouter";
type ChainlistPaginationProps = {
totalPages: number;
activePage: number;
};
export const ChainlistPagination: React.FC<ChainlistPaginationProps> = ({
activePage,
totalPages,
}) => {
const pathname = usePathname();
const searchParams = useSearchParams();
const router = useDashboardRouter();
const createPageURL = useCallback(
(pageNumber: number) => {
const params = new URLSearchParams(searchParams || undefined);
params.set("page", pageNumber.toString());
return `${pathname}?${params.toString()}`;
},
[pathname, searchParams],
);
return (
<PaginationButtons
activePage={activePage}
onPageClick={(page) => router.push(createPageURL(page))}
totalPages={totalPages}
/>
);
};
@@ -1,74 +0,0 @@
"use client";
import { SearchIcon, XCircleIcon } from "lucide-react";
import { usePathname, useSearchParams } from "next/navigation";
import { useEffect, useRef } from "react";
import { useDebouncedCallback } from "use-debounce";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { useDashboardRouter } from "@/lib/DashboardRouter";
function cleanUrl(url: string) {
if (url.endsWith("?")) {
return url.slice(0, -1);
}
return url;
}
export const SearchInput: React.FC = () => {
const router = useDashboardRouter();
const pathname = usePathname();
const searchParams = useSearchParams();
const inputRef = useRef<HTMLInputElement>(null);
// eslint-disable-next-line no-restricted-syntax
useEffect(() => {
// reset the input if the query param is removed
if (inputRef.current?.value && !searchParams?.get("query")) {
inputRef.current.value = "";
}
}, [searchParams]);
const handleSearch = useDebouncedCallback((term: string) => {
const params = new URLSearchParams(searchParams ?? undefined);
if (term) {
params.set("query", term);
} else {
params.delete("query");
}
// always delete the page number when searching
params.delete("page");
const url = cleanUrl(`${pathname}?${params.toString()}`);
router.replace(url);
}, 300);
return (
<div className="group relative w-full">
<SearchIcon className="-translate-y-1/2 absolute top-[50%] left-3 size-4 text-muted-foreground" />
<Input
className="h-10 rounded-lg bg-card py-2 pl-9 lg:min-w-[300px]"
defaultValue={searchParams?.get("query") || ""}
onChange={(e) => handleSearch(e.target.value)}
placeholder="Search by token name or symbol"
ref={inputRef}
/>
{searchParams?.has("query") && (
<Button
className="-translate-y-1/2 absolute top-[50%] right-0 bg-background text-muted-foreground opacity-0 transition duration-300 ease-in-out group-hover:opacity-100"
onClick={() => {
const params = new URLSearchParams(searchParams ?? undefined);
params.delete("query");
params.delete("page");
const url = cleanUrl(`${pathname}?${params.toString()}`);
router.replace(url);
}}
size="icon"
variant="outline"
>
<XCircleIcon className="size-5" />
</Button>
)}
</div>
);
};
@@ -1,55 +0,0 @@
"use client";
import { ArrowDownLeftIcon, ArrowUpRightIcon } from "lucide-react";
import { usePathname, useSearchParams } from "next/navigation";
import { useCallback } from "react";
import { Button } from "@/components/ui/button";
import { ToolTipLabel } from "@/components/ui/tooltip";
import { useDashboardRouter } from "@/lib/DashboardRouter";
type QueryTypeProps = {
activeType: "origin" | "destination";
};
export const QueryType: React.FC<QueryTypeProps> = ({ activeType }) => {
const pathname = usePathname();
const searchParams = useSearchParams();
const router = useDashboardRouter();
const createPageURL = useCallback(
(type: "origin" | "destination") => {
const params = new URLSearchParams(searchParams || undefined);
params.set("type", type);
return `${pathname}?${params.toString()}`;
},
[pathname, searchParams],
);
return (
<div className="flex flex-row">
<ToolTipLabel contentClassName="w-full" label="Origin">
<Button
className="rounded-r-none"
onClick={() => {
router.replace(createPageURL("origin"));
}}
size="icon"
variant={activeType === "origin" ? "default" : "outline"}
>
<ArrowUpRightIcon strokeWidth={1} />
</Button>
</ToolTipLabel>
<ToolTipLabel contentClassName="w-full" label="Destination">
<Button
className="rounded-l-none"
onClick={() => {
router.replace(createPageURL("destination"));
}}
size="icon"
variant={activeType === "destination" ? "default" : "outline"}
>
<ArrowDownLeftIcon strokeWidth={1} />
</Button>
</ToolTipLabel>
</div>
);
};
@@ -1,50 +0,0 @@
"use client";
import { Grid2X2Icon, ListIcon } from "lucide-react";
import { usePathname, useSearchParams } from "next/navigation";
import { useCallback } from "react";
import { Button } from "@/components/ui/button";
import { useDashboardRouter } from "@/lib/DashboardRouter";
type RouteListViewProps = {
activeView: "grid" | "table";
};
export const RouteListView: React.FC<RouteListViewProps> = ({ activeView }) => {
const pathname = usePathname();
const searchParams = useSearchParams();
const router = useDashboardRouter();
const createPageURL = useCallback(
(view: "grid" | "table") => {
const params = new URLSearchParams(searchParams || undefined);
params.set("view", view);
return `${pathname}?${params.toString()}`;
},
[pathname, searchParams],
);
return (
<div className="flex flex-row">
<Button
className="rounded-r-none"
onClick={() => {
router.replace(createPageURL("table"));
}}
size="icon"
variant={activeView === "table" ? "default" : "outline"}
>
<ListIcon strokeWidth={1} />
</Button>
<Button
className="rounded-l-none"
onClick={() => {
router.replace(createPageURL("grid"));
}}
size="icon"
variant={activeView === "grid" ? "default" : "outline"}
>
<Grid2X2Icon strokeWidth={1} />
</Button>
</div>
);
};
@@ -1,141 +0,0 @@
import { ExternalLinkIcon } from "lucide-react";
import Link from "next/link";
import { defineChain, getAddress, NATIVE_TOKEN_ADDRESS } from "thirdweb";
import { type ChainMetadata, getChainMetadata } from "thirdweb/chains";
import { Card, CardContent, CardHeader } from "@/components/ui/card";
import { serverThirdwebClient } from "@/constants/thirdweb-client.server";
import { resolveSchemeWithErrorHandler } from "@/utils/resolveSchemeWithErrorHandler";
type RouteListCardProps = {
originChainId: number;
originTokenAddress: string;
originTokenIconUri?: string | null;
originTokenSymbol: string;
originTokenName: string;
destinationChainId: number;
destinationTokenAddress: string;
destinationTokenIconUri?: string | null;
destinationTokenSymbol: string;
destinationTokenName: string;
};
export async function RouteListCard({
originChainId,
originTokenAddress,
originTokenIconUri,
originTokenName,
destinationChainId,
destinationTokenAddress,
destinationTokenIconUri,
destinationTokenName,
}: RouteListCardProps) {
const [
originChain,
destinationChain,
resolvedOriginTokenIconUri,
resolvedDestinationTokenIconUri,
] = await Promise.all([
// eslint-disable-next-line no-restricted-syntax
getChainMetadata(defineChain(originChainId)),
// eslint-disable-next-line no-restricted-syntax
getChainMetadata(defineChain(destinationChainId)),
originTokenIconUri
? resolveSchemeWithErrorHandler({
client: serverThirdwebClient,
uri: originTokenIconUri,
})
: undefined,
destinationTokenIconUri
? resolveSchemeWithErrorHandler({
client: serverThirdwebClient,
uri: destinationTokenIconUri,
})
: undefined,
]);
return (
<div className="relative h-full">
<Card className="h-full w-full">
<CardHeader className="flex flex-row items-center justify-between p-4">
<div className="flex flex-row items-center gap-2">
{resolvedOriginTokenIconUri ? (
// eslint-disable-next-line @next/next/no-img-element
<img
alt={originTokenAddress}
className="size-8 rounded-full border border-muted-foreground"
src={resolvedOriginTokenIconUri}
/>
) : (
<div className="size-8 rounded-full bg-muted-foreground" />
)}
{resolvedDestinationTokenIconUri ? (
// eslint-disable-next-line @next/next/no-img-element
<img
alt={destinationTokenAddress}
className="-translate-x-4 size-8 rounded-full border border-muted-foreground ring-2 ring-card"
src={resolvedDestinationTokenIconUri}
/>
) : (
<div className="-translate-x-4 size-8 rounded-full bg-muted-foreground ring-2 ring-card" />
)}
</div>
</CardHeader>
<CardContent className="px-4 pt-0 pb-4">
<div className="flex flex-col gap-1">
<div className="flex justify-between gap-2">
<TokenName
chainMetadata={originChain}
tokenAddress={originTokenAddress}
tokenName={originTokenName}
/>
<div className="text-right text-base text-muted-foreground">
{originChain.name}
</div>
</div>
<div className="flex justify-between gap-2">
<TokenName
chainMetadata={destinationChain}
tokenAddress={destinationTokenAddress}
tokenName={destinationTokenName}
/>
<div className="text-right text-base text-muted-foreground">
{destinationChain.name}
</div>
</div>
</div>
</CardContent>
</Card>
</div>
);
}
const nativeTokenAddress = getAddress(NATIVE_TOKEN_ADDRESS);
function TokenName(props: {
tokenAddress: string;
tokenName: string;
chainMetadata: ChainMetadata;
}) {
const isERC20 = getAddress(props.tokenAddress) !== nativeTokenAddress;
if (isERC20) {
return (
<Link
className="flex items-center gap-1.5 font-normal text-base group/link hover:underline underline-offset-4 decoration-muted-foreground/50 decoration-dotted"
href={`https://thirdweb.com/${props.chainMetadata.slug}/${props.tokenAddress}`}
target="_blank"
>
{props.tokenName}
<ExternalLinkIcon className="size-3.5 text-muted-foreground group-hover/link:text-foreground" />
</Link>
);
}
return (
<div className="text-left font-normal text-base">
{props.chainMetadata.nativeCurrency.name}
</div>
);
}
@@ -1,136 +0,0 @@
import { ExternalLinkIcon } from "lucide-react";
import Link from "next/link";
import { getAddress, NATIVE_TOKEN_ADDRESS } from "thirdweb";
import {
type ChainMetadata,
defineChain,
getChainMetadata,
} from "thirdweb/chains";
import { shortenAddress } from "thirdweb/utils";
import { Img } from "@/components/blocks/Img";
import { Button } from "@/components/ui/button";
import { TableCell, TableRow } from "@/components/ui/table";
import { serverThirdwebClient } from "@/constants/thirdweb-client.server";
import { resolveSchemeWithErrorHandler } from "@/utils/resolveSchemeWithErrorHandler";
type RouteListRowProps = {
originChainId: number;
originTokenAddress: string;
originTokenIconUri?: string | null;
originTokenSymbol?: string;
originTokenName?: string;
destinationChainId: number;
destinationTokenAddress: string;
destinationTokenIconUri?: string | null;
destinationTokenSymbol?: string;
destinationTokenName?: string;
};
export async function RouteListRow({
originChainId,
originTokenAddress,
originTokenIconUri,
originTokenSymbol,
destinationChainId,
destinationTokenAddress,
destinationTokenIconUri,
destinationTokenSymbol,
}: RouteListRowProps) {
const [
originChain,
destinationChain,
resolvedOriginTokenIconUri,
resolvedDestinationTokenIconUri,
] = await Promise.all([
// eslint-disable-next-line no-restricted-syntax
getChainMetadata(defineChain(originChainId)),
// eslint-disable-next-line no-restricted-syntax
getChainMetadata(defineChain(destinationChainId)),
originTokenIconUri
? resolveSchemeWithErrorHandler({
client: serverThirdwebClient,
uri: originTokenIconUri,
})
: undefined,
destinationTokenIconUri
? resolveSchemeWithErrorHandler({
client: serverThirdwebClient,
uri: destinationTokenIconUri,
})
: undefined,
]);
return (
<TableRow className="group">
<TableCell>
<TokenInfo
chainMetadata={originChain}
tokenAddress={originTokenAddress}
tokenIconUri={resolvedOriginTokenIconUri}
tokenSymbol={originTokenSymbol}
/>
</TableCell>
<TableCell className="text-muted-foreground/90">
<span className="font-medium">{originChain.name}</span>
</TableCell>
<TableCell>
<TokenInfo
chainMetadata={destinationChain}
tokenAddress={destinationTokenAddress}
tokenIconUri={resolvedDestinationTokenIconUri}
tokenSymbol={destinationTokenSymbol}
/>
</TableCell>
<TableCell className="text-muted-foreground/90">
<span className="font-medium">{destinationChain.name}</span>
</TableCell>
</TableRow>
);
}
const nativeTokenAddress = getAddress(NATIVE_TOKEN_ADDRESS);
function TokenInfo(props: {
tokenAddress: string;
tokenSymbol: string | undefined;
chainMetadata: ChainMetadata;
tokenIconUri: string | undefined;
}) {
const isERC20 = getAddress(props.tokenAddress) !== nativeTokenAddress;
return (
<div className="flex items-center gap-1.5">
{props.tokenIconUri ? (
<Img
alt={props.tokenAddress}
className="size-7 rounded-full border border-border/50"
src={props.tokenIconUri}
/>
) : (
<div className="size-7 rounded-full bg-muted-foreground/20" />
)}
{isERC20 ? (
<Button
asChild
className="h-auto w-auto py-1 px-1.5 gap-2"
variant="ghost"
>
<Link
href={`https://thirdweb.com/${props.chainMetadata.slug}/${props.tokenAddress}`}
target="_blank"
>
{props.tokenSymbol || shortenAddress(props.tokenAddress)}
<ExternalLinkIcon className="size-3.5 text-muted-foreground" />
</Link>
</Button>
) : (
<span className="font-medium text-sm px-1.5 py-1">
{props.chainMetadata.nativeCurrency.symbol}
</span>
)}
</div>
);
}
@@ -1,176 +0,0 @@
import type { Address } from "thirdweb";
import {
Table,
TableBody,
TableContainer,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { getRoutes } from "../../../utils";
import { ChainlistPagination } from "../client/pagination";
import { RouteListCard } from "./routelist-card";
import { RouteListRow } from "./routelist-row";
export type SearchParams = Partial<{
includeDeprecated: boolean;
query: string;
page: number;
type: "origin" | "destination";
// maybe later we'll have a page size param?
// pageSize: number;
view: "table" | "grid";
}>;
// 120 is divisible by 2, 3, and 4 so card layout looks nice
const DEFAULT_PAGE_SIZE = 120;
async function getRoutesToRender(params: SearchParams) {
const filters: Partial<{
originQuery?: string;
destinationQuery?: string;
originChainId?: number;
originTokenAddress?: Address;
destinationChainId?: number;
destinationTokenAddress?: Address;
}> = {};
if (params.type === "origin" || typeof params.type === "undefined") {
if (params.query?.startsWith("0x")) {
filters.originTokenAddress = params.query as Address;
} else if (Number.isInteger(Number(params.query))) {
filters.originChainId = Number(params.query);
} else if (params.query) {
filters.originQuery = params.query;
}
} else if (params.type === "destination") {
if (params.query?.startsWith("0x")) {
filters.destinationTokenAddress = params.query as Address;
} else if (Number.isInteger(Number(params.query))) {
filters.destinationChainId = Number(params.query);
} else if (params.query) {
filters.destinationQuery = params.query;
}
}
const routes = await getRoutes({
destinationQuery: filters.destinationQuery,
limit: DEFAULT_PAGE_SIZE,
offset: DEFAULT_PAGE_SIZE * ((params.page || 1) - 1),
originQuery: filters.originQuery,
});
return {
filteredCount: routes.meta.filteredCount,
routesToRender: routes.data,
totalCount: routes.meta.totalCount,
};
}
export async function RoutesData(props: {
searchParams: SearchParams;
activeView: "table" | "grid";
isLoggedIn: boolean;
}) {
const { routesToRender, totalCount, filteredCount } = await getRoutesToRender(
props.searchParams,
);
const totalPages = Math.ceil(filteredCount / DEFAULT_PAGE_SIZE);
return (
<>
<main>
{/* empty state */}
{routesToRender.length === 0 ? (
<div className="flex h-[300px] items-center justify-center rounded-lg border p-8 lg:h-[500px]">
<p className="text-2xl">No Results found</p>
</div>
) : props.activeView === "table" ? (
<TableContainer>
<Table>
<TableHeader>
<TableRow>
<TableHead>Origin Token</TableHead>
<TableHead>Origin Chain</TableHead>
<TableHead>Destination Token</TableHead>
<TableHead>Destination Chain</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{routesToRender.map((route) => (
<RouteListRow
destinationChainId={route.destinationToken.chainId}
destinationTokenAddress={route.destinationToken.address}
destinationTokenIconUri={route.destinationToken.iconUri}
destinationTokenName={route.destinationToken.name}
destinationTokenSymbol={route.destinationToken.symbol}
key={`${route.originToken.chainId}:${route.originToken.address}-${route.destinationToken.chainId}:${route.destinationToken.address}`}
originChainId={route.originToken.chainId}
originTokenAddress={route.originToken.address}
originTokenIconUri={route.originToken.iconUri}
originTokenName={route.originToken.name}
originTokenSymbol={route.originToken.symbol}
/>
))}
</TableBody>
</Table>
</TableContainer>
) : (
<ul className="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">
{routesToRender.map((route) => (
<li
className="h-full"
key={`${route.originToken.chainId}:${route.originToken.address}-${route.destinationToken.chainId}:${route.destinationToken.address}`}
>
<RouteListCard
destinationChainId={route.destinationToken.chainId}
destinationTokenAddress={route.destinationToken.address}
destinationTokenIconUri={route.destinationToken.iconUri}
destinationTokenName={route.destinationToken.name}
destinationTokenSymbol={route.destinationToken.symbol}
originChainId={route.originToken.chainId}
originTokenAddress={route.originToken.address}
originTokenIconUri={route.originToken.iconUri}
originTokenName={route.originToken.name}
originTokenSymbol={route.originToken.symbol}
/>
</li>
))}
</ul>
)}
</main>
<div className="h-10" />
{totalPages > 1 && (
<ChainlistPagination
activePage={Number(props.searchParams.page) || 1}
totalPages={totalPages}
/>
)}
<div className="h-4" />
<p className="text-balance text-center text-muted-foreground text-sm">
Showing{" "}
<span className="text-accent-foreground">{routesToRender.length}</span>{" "}
out of{" "}
{filteredCount !== totalCount ? (
<>
<span className="text-accent-foreground">
{filteredCount.toLocaleString()}
</span>{" "}
routes that match filters. (Total:{" "}
<span className="text-accent-foreground">
{totalCount.toLocaleString()}
</span>
)
</>
) : (
<>
<span className="text-accent-foreground">
{totalCount.toLocaleString()}
</span>{" "}
routes.
</>
)}
</p>
</>
);
}
@@ -1,94 +0,0 @@
import { ArrowUpRightIcon } from "lucide-react";
import type { Metadata } from "next";
import { headers } from "next/headers";
import { getAuthToken } from "@/api/auth-token";
import { SearchInput } from "./components/client/search";
import { QueryType } from "./components/client/type";
import { RouteListView } from "./components/client/view";
import {
RoutesData,
type SearchParams,
} from "./components/server/routes-table";
const title = "Payments Routes: Swap, Bridge, and Onramp";
const description =
"A list of token routes for swapping, bridging, and on-ramping between EVM chains with thirdweb.";
export const metadata: Metadata = {
description,
openGraph: {
description,
title,
},
title,
};
export default async function RoutesPage(props: {
searchParams: Promise<SearchParams>;
}) {
const authToken = await getAuthToken();
const headersList = await headers();
const viewportWithHint = Number(
headersList.get("Sec-Ch-Viewport-Width") || 0,
);
const searchParams = await props.searchParams;
const activeType = searchParams.type ?? "origin";
// default is driven by viewport hint
const activeView = searchParams.view
? searchParams.view
: viewportWithHint > 1000
? "table"
: "grid";
return (
<section className="container mx-auto flex h-full flex-col px-4 py-10">
<header className="flex flex-col gap-6">
<div className="flex flex-col gap-6 lg:flex-row lg:items-center lg:justify-between">
<div className="flex flex-col gap-2">
<h1 className="font-semibold text-4xl tracking-tighter lg:text-5xl">
Routes
</h1>
</div>
<div className="flex flex-row items-end gap-4 lg:flex-col">
<div className="flex w-full flex-row items-center gap-4">
<SearchInput />
<QueryType activeType={activeType} />
<RouteListView activeView={activeView} />
</div>
</div>
</div>
</header>
<div className="h-10" />
<div className="relative overflow-hidden rounded-lg border-2 border-green-500/20 bg-gradient-to-br from-card/80 to-card/50 p-4 shadow-[inset_0_1px_2px_0_rgba(0,0,0,0.02)]">
<div className="absolute inset-0 bg-gradient-to-br from-green-500/5 to-transparent" />
<div className="relative flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
<div className="flex flex-col gap-1">
<h3 className="font-medium text-lg">
Get Started with thirdweb Payments
</h3>
<p className="text-muted-foreground text-sm">
Simple, instant, and secure payments across any token and chain.
</p>
</div>
<a
className="inline-flex items-center gap-2 rounded-md bg-green-600 px-4 py-2 font-medium text-sm text-white transition-all hover:bg-green-600/90 hover:shadow-sm"
href="https://portal.thirdweb.com/payments"
rel="noopener noreferrer"
target="_blank"
>
Learn More
<ArrowUpRightIcon className="size-4" />
</a>
</div>
</div>
<div className="h-10" />
<RoutesData
activeView={activeView}
isLoggedIn={!!authToken}
searchParams={searchParams}
/>
</section>
);
}
@@ -1,18 +0,0 @@
import type { Address } from "thirdweb";
export type Route = {
originToken: {
address: Address;
chainId: number;
iconUri?: string;
name: string;
symbol: string;
};
destinationToken: {
address: Address;
chainId: number;
iconUri?: string;
name: string;
symbol: string;
};
};
@@ -1,48 +0,0 @@
import "server-only";
import { NEXT_PUBLIC_THIRDWEB_BRIDGE_HOST } from "@/constants/public-envs";
import { DASHBOARD_THIRDWEB_SECRET_KEY } from "@/constants/server-envs";
import type { Route } from "./types/route";
export async function getRoutes({
limit,
offset,
originQuery,
destinationQuery,
}: {
limit?: number;
offset?: number;
originQuery?: string;
destinationQuery?: string;
} = {}) {
const url = new URL(`${NEXT_PUBLIC_THIRDWEB_BRIDGE_HOST}/v1/routes/search`);
if (limit) {
url.searchParams.set("limit", limit.toString());
}
if (offset) {
url.searchParams.set("offset", offset.toString());
}
if (originQuery) {
url.searchParams.set("originQuery", originQuery);
}
if (destinationQuery) {
url.searchParams.set("destinationQuery", destinationQuery);
}
url.searchParams.set("sortBy", "popularity");
const routesResponse = await fetch(url, {
headers: {
"x-secret-key": DASHBOARD_THIRDWEB_SECRET_KEY,
},
next: { revalidate: 60 * 60 },
});
if (!routesResponse.ok) {
throw new Error("Failed to fetch routes");
}
const routes: {
data: Route[];
meta: { totalCount: number; filteredCount: number };
} = await routesResponse.json();
return routes;
}
@@ -0,0 +1,50 @@
import "server-only";
import { unstable_cache } from "next/cache";
type ChainSeo = {
title: string;
description: string;
og: {
title: string;
description: string;
site_name: string;
url: string;
};
faqs: Array<{
title: string;
description: string;
}>;
chain: {
chainId: number;
name: string;
slug: string;
nativeCurrency: {
name: string;
symbol: string;
decimals: number;
};
testnet: boolean;
is_deprecated: boolean;
};
};
export const fetchChainSeo = unstable_cache(
async (chainId: number) => {
const url = new URL(
`https://seo-pages-generator-5814.zeet-nftlabs.zeet.app/chain/${chainId}`,
);
const res = await fetch(url, {
headers: {
"Content-Type": "application/json",
},
});
if (!res.ok) {
return undefined;
}
return res.json() as Promise<ChainSeo>;
},
["chain-seo"],
{ revalidate: 60 * 60 * 24 }, // 24 hours
);
@@ -1,24 +1,28 @@
"use client";
import { useTheme } from "next-themes";
import { defineChain, type ThirdwebClient } from "thirdweb";
import { NATIVE_TOKEN_ADDRESS } from "thirdweb";
import type { ChainMetadata } from "thirdweb/chains";
import { BuyWidget } from "thirdweb/react";
import { getSDKTheme } from "@/utils/sdk-component-theme";
import { BuyAndSwapEmbed } from "@/components/blocks/BuyAndSwapEmbed";
import { GridPatternEmbedContainer } from "@/components/blocks/grid-pattern-embed-container";
export function BuyFundsSection(props: {
chain: ChainMetadata;
client: ThirdwebClient;
}) {
const { theme } = useTheme();
export function BuyFundsSection(props: { chain: ChainMetadata }) {
return (
<section className="flex flex-col gap-4 items-center justify-center">
<BuyWidget
amount="0"
// eslint-disable-next-line no-restricted-syntax
chain={defineChain(props.chain.chainId)}
client={props.client}
theme={getSDKTheme(theme === "dark" ? "dark" : "light")}
<GridPatternEmbedContainer>
<BuyAndSwapEmbed
swapTab={{
sellToken: {
chainId: props.chain.chainId,
tokenAddress: NATIVE_TOKEN_ADDRESS,
},
buyToken: undefined,
}}
buyTab={{
buyToken: {
chainId: props.chain.chainId,
tokenAddress: NATIVE_TOKEN_ADDRESS,
},
}}
pageType="chain"
/>
</section>
</GridPatternEmbedContainer>
);
}
@@ -0,0 +1,14 @@
"use client";
import { reportChainPageview } from "@/analytics/report";
import { useEffectOnce } from "@/hooks/useEffectOnce";
export function ChainPageView(props: { chainId: number; is_testnet: boolean }) {
useEffectOnce(() => {
reportChainPageview({
chainId: props.chainId,
is_testnet: props.is_testnet,
});
});
return null;
}
@@ -43,9 +43,7 @@ function useChainStatswithRPC(_rpcUrl: string) {
const latency = (performance.now() - startTimeStamp).toFixed(0);
const blockNumber = Number.parseInt(json.result.number, 16);
const blockGasLimit = Number.parseInt(json.result.gasLimit, 16);
return {
blockGasLimit,
blockNumber,
latency,
};
@@ -61,8 +59,70 @@ function useChainStatswithRPC(_rpcUrl: string) {
});
}
function useEIP7702Support(_rpcUrl: string) {
let rpcUrl = _rpcUrl.replace(
// eslint-disable-next-line no-template-curly-in-string
// biome-ignore lint/suspicious/noTemplateCurlyInString: this is expected in this case
"${THIRDWEB_API_KEY}",
NEXT_PUBLIC_DASHBOARD_CLIENT_ID,
);
// based on the environment hit dev or production
if (hostnameEndsWith(rpcUrl, "rpc.thirdweb.com")) {
if (!isProd) {
rpcUrl = rpcUrl.replace("rpc.thirdweb.com", "rpc.thirdweb-dev.com");
}
}
return useQuery({
enabled: !!rpcUrl,
queryFn: async () => {
try {
const res = await fetch(rpcUrl, {
body: JSON.stringify({
id: 1,
jsonrpc: "2.0",
method: "eth_estimateGas",
params: [
{
from: "0xdeadbeef00000000000000000000000000000000",
to: "0xdeadbeef00000000000000000000000000000000",
data: "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
value: "0x0",
},
"latest",
{
"0xdeadbeef00000000000000000000000000000000": {
code: "0xef01000000000000000000000000000000000000000001",
},
},
],
}),
method: "POST",
});
const json = await res.json();
// If the response has a valid result object, EIP-7702 is enabled
return {
isSupported: !!json.result,
};
} catch {
// If the request fails or errors, EIP-7702 is not supported
return {
isSupported: false,
};
}
},
queryKey: ["eip-7702-support", { rpcUrl }],
refetchInterval: false, // Don't refetch this as it's unlikely to change
refetchOnWindowFocus: false,
});
}
export function ChainLiveStats(props: { rpc: string }) {
const stats = useChainStatswithRPC(props.rpc);
const eip7702Support = useEIP7702Support(props.rpc);
return (
<>
@@ -107,8 +167,8 @@ export function ChainLiveStats(props: { rpc: string }) {
)}
</PrimaryInfoItem>
{/* Block Height */}
<PrimaryInfoItem title="Block Height" titleIcon={<PulseDot />}>
{/* Latest Block */}
<PrimaryInfoItem title="Latest Block" titleIcon={<PulseDot />}>
{stats.isError ? (
<p className="fade-in-0 animate-in text-destructive-text">N/A</p>
) : stats.data ? (
@@ -120,16 +180,37 @@ export function ChainLiveStats(props: { rpc: string }) {
)}
</PrimaryInfoItem>
{/* Block Gas Limit */}
<PrimaryInfoItem title="Block Gas Limit" titleIcon={<PulseDot />}>
{stats.isError ? (
<p className="fade-in-0 animate-in text-destructive-text">N/A</p>
) : stats.data ? (
<p className="fade-in-0 animate-in">
{stats.data.blockGasLimit ?? "N/A"}
</p>
{/* EIP-7702 */}
<PrimaryInfoItem
title="EIP-7702"
titleIcon={
eip7702Support.data ? (
eip7702Support.data.isSupported ? (
<ToolTipLabel label="Enabled">
<CircleCheckIcon className="size-4 text-success-text" />
</ToolTipLabel>
) : (
<ToolTipLabel label="Disabled">
<XIcon className="size-4 text-destructive-text" />
</ToolTipLabel>
)
) : eip7702Support.isError ? (
<ToolTipLabel label="Disabled">
<XIcon className="size-4 text-destructive-text" />
</ToolTipLabel>
) : null
}
>
{eip7702Support.data ? (
eip7702Support.data.isSupported ? (
"Available"
) : (
"Not Available"
)
) : eip7702Support.isError ? (
"Not Available"
) : (
<div className="flex h-[28px] w-[140px] py-1">
<div className="flex h-[28px] w-[80px] py-1">
<Skeleton className="h-full w-full" />
</div>
)}
@@ -11,7 +11,7 @@ export function ChainOverviewSection(props: { chain: ChainMetadata }) {
return (
<section>
<SectionTitle title="Chain Overview" />
<div className="grid grid-cols-1 gap-6 rounded-lg border bg-card p-4 md:grid-cols-2 md:p-6 lg:grid-cols-3 lg:gap-8">
<div className="grid grid-cols-1 gap-6 rounded-xl border bg-card p-4 md:grid-cols-2 md:p-6 lg:grid-cols-3 lg:gap-6">
{/* Info */}
{chain.infoURL && (
<PrimaryInfoItem title="Info">
@@ -1,4 +1,3 @@
import { CircleCheckIcon } from "lucide-react";
import Link from "next/link";
import type { ChainMetadataWithServices } from "@/types/chain";
import { products } from "../../../../components/server/products";
@@ -24,15 +23,18 @@ export function SupportedProductsSection(props: {
{enabledProducts.map((product) => {
return (
<div
className="relative flex gap-3 rounded-lg border bg-card p-4 pr-8 transition-colors hover:border-active-border"
className="relative rounded-xl border bg-card p-4 hover:border-active-border"
key={product.id}
>
<CircleCheckIcon className="absolute top-4 right-4 size-5 text-success-text" />
<product.icon className="mt-0.5 size-5 shrink-0" />
<div className="flex mb-4">
<div className="p-2 rounded-full border bg-background">
<product.icon className="size-4 text-muted-foreground" />
</div>
</div>
<div>
<h3 className="mb-1.5 font-medium">
<h3 className="mb-1">
<Link
className="before:absolute before:inset-0"
className="before:absolute before:inset-0 text-base font-medium"
href={product.link}
rel="noopener noreferrer"
target="_blank"
@@ -17,7 +17,7 @@ export function ExplorersSection(props: {
key={explorer.url}
>
<ExternalLinkIcon className="absolute top-4 right-4 size-4 text-muted-foreground" />
<h3 className="mb-1 font-semibold text-base capitalize">
<h3 className="mb-1 font-medium text-base capitalize">
{explorer.name}
</h3>
<Link
@@ -6,9 +6,7 @@ export function PrimaryInfoItem(props: {
return (
<div>
<div className="flex items-center gap-2">
<h3 className="font-medium text-base text-muted-foreground">
{props.title}
</h3>
<h3 className="text-base text-muted-foreground">{props.title}</h3>
{props.titleIcon}
</div>
{props.children}
@@ -22,38 +22,51 @@ import { getClientThirdwebClient } from "@/constants/thirdweb-client.client";
import { mapV4ChainToV5Chain } from "@/utils/map-chains";
import { TeamHeader } from "../../../../team/components/TeamHeader/team-header";
import { StarButton } from "../../components/client/star-button";
import { getChain, getChainMetadata } from "../../utils";
import { getChain, getCustomChainMetadata } from "../../utils";
import { fetchChainSeo } from "./apis/chain-seo";
import { AddChainToWallet } from "./components/client/add-chain-to-wallet";
import { ChainPageView } from "./components/client/chain-pageview";
import { ChainHeader } from "./components/server/chain-header";
// TODO: improve the behavior when clicking "Get started with thirdweb", currently just redirects to the dashboard
type Params = Promise<{ chain_id: string }>;
export async function generateMetadata(props: {
params: Promise<{ chain_id: string }>;
}): Promise<Metadata> {
params: Params;
}): Promise<Metadata | undefined> {
const params = await props.params;
const chain = await getChain(params.chain_id);
const sanitizedChainName = chain.name.replace("Mainnet", "").trim();
const title = `${sanitizedChainName}: RPC and Chain Settings`;
const chainSeo = await fetchChainSeo(Number(chain.chainId)).catch(
() => undefined,
);
const description = `Use the best ${sanitizedChainName} RPC and add to your wallet. Discover the chain ID, native token, explorers, and ${
chain.testnet && chain.faucets?.length ? "faucet options" : "more"
}.`;
if (!chainSeo) {
return undefined;
}
return {
description,
title: chainSeo.title,
description: chainSeo.description,
metadataBase: new URL("https://thirdweb.com"),
openGraph: {
description,
title,
title: chainSeo.og.title,
description: chainSeo.og.description,
siteName: "thirdweb",
type: "website",
url: "https://thirdweb.com",
},
twitter: {
title: chainSeo.og.title,
description: chainSeo.og.description,
card: "summary_large_image",
creator: "@thirdweb",
site: "@thirdweb",
},
title,
};
}
// this is the dashboard layout file
export default async function ChainPageLayout(props: {
children: React.ReactNode;
params: Promise<{ chain_id: string }>;
params: Params;
}) {
const params = await props.params;
const { children } = props;
@@ -66,11 +79,14 @@ export default async function ChainPageLayout(props: {
redirect(chain.slug);
}
const chainMetadata = await getChainMetadata(chain.chainId);
const customChainMetadata = getCustomChainMetadata(chain.chainId);
const chainSeo = await fetchChainSeo(chain.chainId);
const client = getClientThirdwebClient(undefined);
const description = customChainMetadata?.about || chainSeo?.description;
return (
<div className="flex grow flex-col">
<ChainPageView chainId={chain.chainId} is_testnet={chain.testnet} />
<div className="border-border border-b bg-card">
<TeamHeader />
</div>
@@ -121,7 +137,7 @@ export default async function ChainPageLayout(props: {
<ChainHeader
chain={chain}
client={client}
headerImageUrl={chainMetadata?.headerImgUrl}
headerImageUrl={customChainMetadata?.headerImgUrl}
logoUrl={chain.icon?.url}
/>
@@ -129,7 +145,7 @@ export default async function ChainPageLayout(props: {
<div className="flex flex-col gap-3 md:gap-2">
{/* Gas Sponsored badge - Mobile */}
{chainMetadata?.gasSponsored && (
{customChainMetadata?.gasSponsored && (
<div className="flex md:hidden">
<GasSponsoredBadge />
</div>
@@ -151,7 +167,7 @@ export default async function ChainPageLayout(props: {
)}
{/* Gas Sponsored badge - Desktop */}
{chainMetadata?.gasSponsored && (
{customChainMetadata?.gasSponsored && (
<div className="hidden md:block">
<GasSponsoredBadge />
</div>
@@ -159,9 +175,9 @@ export default async function ChainPageLayout(props: {
</div>
{/* description */}
{chainMetadata?.about && (
<p className="mb-2 whitespace-pre-line text-muted-foreground text-sm lg:text-base">
{chainMetadata.about}
{description && (
<p className="mb-2 whitespace-pre-line text-muted-foreground text-sm lg:text-base text-pretty max-w-3xl">
{description}
</p>
)}
@@ -1,5 +1,4 @@
import { ImageResponse } from "next/og";
import { useId } from "react";
import { download } from "thirdweb/storage";
import { DASHBOARD_THIRDWEB_SECRET_KEY } from "@/constants/server-envs";
import { getConfiguredThirdwebClient } from "@/constants/thirdweb.server";
@@ -18,8 +17,8 @@ export const size = {
export const contentType = "image/png";
const TWLogo: React.FC = () => {
const clipPathId = useId();
const linearGradientId = useId();
const clipPathId = "og-tw-logo-clip-path";
const linearGradientId = "og-tw-logo-linear-gradient";
return (
// biome-ignore lint/a11y/noSvgWithoutTitle: not needed
<svg
@@ -1,7 +1,9 @@
import { CircleAlertIcon } from "lucide-react";
import { getRawAccount } from "@/api/account/get-account";
import { FaqAccordion } from "@/components/blocks/faq-section";
import { getClientThirdwebClient } from "@/constants/thirdweb-client.client";
import { getChain, getChainMetadata } from "../../utils";
import { getChain, getCustomChainMetadata } from "../../utils";
import { fetchChainSeo } from "./apis/chain-seo";
import { BuyFundsSection } from "./components/client/BuyFundsSection";
import { ChainOverviewSection } from "./components/server/ChainOverviewSection";
import { ChainCTA } from "./components/server/cta-card";
@@ -9,22 +11,27 @@ import { ExplorersSection } from "./components/server/explorer-section";
import { FaucetSection } from "./components/server/FaucetSection";
import { SupportedProductsSection } from "./components/server/SupportedProductsSection";
export default async function Page(props: {
type Props = {
params: Promise<{ chain_id: string }>;
}) {
};
export default async function Page(props: Props) {
const params = await props.params;
const chain = await getChain(params.chain_id);
const chainMetadata = await getChainMetadata(chain.chainId);
const customChainMetadata = getCustomChainMetadata(Number(params.chain_id));
const chainSeo = await fetchChainSeo(Number(chain.chainId)).catch(
() => undefined,
);
const client = getClientThirdwebClient();
const isDeprecated = chain.status === "deprecated";
const account = await getRawAccount();
return (
<div className="flex flex-col gap-10">
{/* Custom CTA */}
{(chainMetadata?.cta?.title || chainMetadata?.cta?.description) && (
<ChainCTA {...chainMetadata.cta} />
{(customChainMetadata?.cta?.title ||
customChainMetadata?.cta?.description) && (
<ChainCTA {...customChainMetadata.cta} />
)}
{/* Deprecated Alert */}
@@ -44,7 +51,7 @@ export default async function Page(props: {
{chain.testnet ? (
<FaucetSection chain={chain} client={client} isLoggedIn={!!account} />
) : chain.services.find((c) => c.service === "pay" && c.enabled) ? (
<BuyFundsSection chain={chain} client={client} />
<BuyFundsSection chain={chain} />
) : null}
{/* Chain Overview */}
@@ -58,6 +65,23 @@ export default async function Page(props: {
{chain.services.filter((s) => s.enabled).length > 0 && (
<SupportedProductsSection services={chain.services} />
)}
{chainSeo?.faqs && chainSeo.faqs.length > 0 && (
<FaqSection faqs={chainSeo.faqs} />
)}
</div>
);
}
function FaqSection(props: {
faqs: Array<{ title: string; description: string }>;
}) {
return (
<section className="py-10">
<h2 className="text-2xl md:text-3xl font-semibold mb-4 tracking-tight">
Frequently asked questions
</h2>
<FaqAccordion faqs={props.faqs} />
</section>
);
}
@@ -10,8 +10,7 @@ import * as ERC20Ext from "thirdweb/extensions/erc20";
import * as ERC721Ext from "thirdweb/extensions/erc721";
import * as ERC1155Ext from "thirdweb/extensions/erc1155";
import { download } from "thirdweb/storage";
import type { OverrideEntry } from "thirdweb/utils";
import { maxUint256 } from "thirdweb/utils";
import { isRecord, maxUint256, type OverrideEntry } from "thirdweb/utils";
import type { z } from "zod";
import type {
ClaimCondition as LegacyClaimCondition,
@@ -86,7 +85,7 @@ export async function getClaimPhasesInLegacyFormat(
]);
const snapshot = await fetchSnapshot(
condition.merkleRoot,
contractMetadata.merkle,
isRecord(contractMetadata.merkle) ? contractMetadata.merkle : {},
options.contract.client,
);
return {
@@ -67,7 +67,6 @@ export async function getContractUniqueWalletAnalytics(params: {
}
const json = (await res.json()) as InsightResponse;
console.log("wallet analytics json", json);
const aggregations = Object.values(json.aggregations[0]);
const returnValue: TransactionAnalyticsEntry[] = [];
@@ -2,7 +2,7 @@
import { ExternalLinkIcon } from "lucide-react";
import Link from "next/link";
import { useState } from "react";
import { Suspense, useState } from "react";
import type { NFT, ThirdwebClient, ThirdwebContract } from "thirdweb";
import { getNFT as getErc721NFT } from "thirdweb/extensions/erc721";
import { getNFT as getErc1155NFT } from "thirdweb/extensions/erc1155";
@@ -89,7 +89,7 @@ export const TokenIdPage: React.FC<TokenIdPageProps> = ({
if (isPending) {
return (
<div className="flex h-[400px] items-center justify-center">
<div className="flex h-[400px] items-center justify-center py-20">
<Spinner className="size-10" />
</div>
);
@@ -179,15 +179,26 @@ export const TokenIdPage: React.FC<TokenIdPageProps> = ({
<NFTDetailsTab client={contract.client} nft={nft} />
)}
{tabs.map((tb) => {
return (
tb.title === tab && (
<div className="w-full" key={tb.title}>
{tb.children}
<Suspense
fallback={
<div className="h-full">
<div className="flex items-center gap-1.5">
<Spinner className="size-4" />
<p className="text-sm text-muted-foreground"> Loading </p>
</div>
)
);
})}
</div>
}
>
{tabs.map((tb) => {
return (
tb.title === tab && (
<div className="w-full" key={tb.title}>
{tb.children}
</div>
)
);
})}
</Suspense>
</div>
</div>
</div>
@@ -58,6 +58,7 @@ export function NFTGetAllTable({
includeOwners: true,
start: currentPage * pageSize,
tokenByIndex,
useIndexer: false, // TEMPORARY, TODO: Remove when this is fixed in insight
},
);
@@ -1,6 +1,5 @@
/* eslint-disable @next/next/no-img-element */
import { ImageResponse } from "@vercel/og";
import { useId } from "react";
import { getContractMetadata } from "thirdweb/extensions/common";
import { isProd } from "@/constants/env-utils";
import { API_ROUTES_CLIENT_ID } from "@/constants/server-envs";
@@ -48,7 +47,10 @@ export default async function Image({
chainName: info.chainMetadata.name,
contractAddress: info.serverContract.address,
displayName: contractDisplayName,
logo: contractMetadata.image,
logo:
contractMetadata.image && typeof contractMetadata.image === "string"
? contractMetadata.image
: undefined,
});
} catch {
return contractOGImageTemplate({
@@ -62,8 +64,9 @@ function shortenString(str: string) {
return `${str.substring(0, 7)}...${str.substring(str.length - 5)}`;
}
const gradientId = "og-brand-icon-gradient";
const OgBrandIcon: React.FC = () => {
const gradientId = useId();
return (
// biome-ignore lint/a11y/noSvgWithoutTitle: not needed
<svg
@@ -6,7 +6,7 @@ import { PublicPageConnectButton } from "./PublicPageConnectButton";
export function PageHeader(props: { containerClassName?: string }) {
return (
<div className="border-b bg-card">
<div className="border-b">
<header
className={cn(
"container flex max-w-8xl justify-between py-3",
@@ -2,6 +2,8 @@
import { useTheme } from "next-themes";
import { ConnectButton } from "thirdweb/react";
import type { Wallet } from "thirdweb/wallets";
import { appMetadata } from "@/constants/connect";
import { getClientThirdwebClient } from "@/constants/thirdweb-client.client";
import { useAllChainsData } from "@/hooks/chains/allChains";
import { getSDKTheme } from "@/utils/sdk-component-theme";
@@ -10,6 +12,7 @@ const client = getClientThirdwebClient();
export function PublicPageConnectButton(props: {
connectButtonClassName?: string;
wallets?: Wallet[];
}) {
const { theme } = useTheme();
const t = theme === "light" ? "light" : "dark";
@@ -17,17 +20,16 @@ export function PublicPageConnectButton(props: {
return (
<ConnectButton
appMetadata={{
logoUrl: "https://thirdweb.com/favicon.ico",
name: "thirdweb",
url: "https://thirdweb.com",
}}
appMetadata={appMetadata}
autoConnect={false}
chains={allChainsV5}
client={client}
connectButton={{
className: props.connectButtonClassName,
}}
detailsButton={{
className: props.connectButtonClassName,
}}
connectModal={{
privacyPolicyUrl: "/privacy-policy",
showThirdwebBranding: false,
@@ -35,6 +37,7 @@ export function PublicPageConnectButton(props: {
}}
// we have an AutoConnect already added in root layout with AA configuration
theme={getSDKTheme(t)}
wallets={props.wallets}
/>
);
}
@@ -5,11 +5,13 @@ import { useEffectOnce } from "@/hooks/useEffectOnce";
export function AssetPageView(props: {
assetType: "nft" | "coin";
chainId: number;
is_testnet: boolean;
}) {
useEffectOnce(() => {
reportAssetPageview({
assetType: props.assetType,
chainId: props.chainId,
is_testnet: props.is_testnet,
});
});
@@ -76,7 +76,7 @@ export function ContractHeaderUI(props: {
return (
<div
className={cn(
"flex flex-col lg:flex-row lg:items-center gap-5 py-6 relative",
"flex flex-col lg:flex-row lg:items-center gap-5 py-8 relative",
props.className,
)}
>
@@ -1,48 +0,0 @@
"use client";
import { useTheme } from "next-themes";
import type { Chain, ThirdwebClient } from "thirdweb";
import { BuyWidget } from "thirdweb/react";
import {
reportAssetBuyFailed,
reportAssetBuySuccessful,
} from "@/analytics/report";
import { parseError } from "@/utils/errorParser";
import { getSDKTheme } from "@/utils/sdk-component-theme";
export function BuyTokenEmbed(props: {
client: ThirdwebClient;
chain: Chain;
tokenAddress: string;
}) {
const { theme } = useTheme();
return (
<BuyWidget
amount="1"
chain={props.chain}
className="!rounded-xl !w-full"
client={props.client}
connectOptions={{
autoConnect: false,
}}
onError={(e) => {
const errorMessage = parseError(e);
reportAssetBuyFailed({
assetType: "coin",
chainId: props.chain.id,
contractType: "DropERC20",
error: errorMessage,
});
}}
onSuccess={() => {
reportAssetBuySuccessful({
assetType: "coin",
chainId: props.chain.id,
contractType: "DropERC20",
});
}}
theme={getSDKTheme(theme === "light" ? "light" : "dark")}
tokenAddress={props.tokenAddress as `0x${string}`}
/>
);
}
@@ -1,6 +1,7 @@
"use client";
import { formatDistanceToNow } from "date-fns";
import {
ArrowLeftRightIcon,
ArrowRightIcon,
ChevronLeftIcon,
ChevronRightIcon,
@@ -73,8 +74,13 @@ function RecentTransfersUI(props: {
return (
<div>
<div className="p-4 lg:p-6 bg-card border rounded-b-none border-b-0 rounded-lg">
<h2 className="font-semibold text-2xl tracking-tight mb-0.5">
<div className="p-4 lg:p-6 bg-card border rounded-b-none border-b-0 rounded-xl">
<div className="flex mb-3">
<div className="rounded-full border p-2 bg-background">
<ArrowLeftRightIcon className="size-4 text-muted-foreground" />
</div>
</div>
<h2 className="font-semibold text-2xl tracking-tight mb-1">
Recent Transfers
</h2>
<p className="text-muted-foreground text-sm">
@@ -194,7 +200,7 @@ function RecentTransfersUI(props: {
)}
</TableContainer>
<div className="flex items-center justify-end gap-3 rounded-b-lg border-x border-b bg-card px-6 py-5">
<div className="flex items-center justify-end gap-3 rounded-b-xl border-x border-b bg-card px-6 py-5">
<Button
className="gap-1.5 bg-background rounded-full"
disabled={props.page === 0 || props.isPending}
@@ -122,6 +122,7 @@ export function TokenDropClaim(props: {
chainId: props.contract.chain.id,
contractType: "DropERC20",
error: errorMessage,
is_testnet: props.chainMetadata.testnet,
});
toast.error("Failed to approve spending", {
@@ -160,6 +161,7 @@ export function TokenDropClaim(props: {
chainId: props.contract.chain.id,
contractType: "DropERC20",
error: errorMessage,
is_testnet: props.chainMetadata.testnet,
});
toast.error("Failed to buy tokens", {
@@ -172,6 +174,7 @@ export function TokenDropClaim(props: {
assetType: "coin",
chainId: props.contract.chain.id,
contractType: "DropERC20",
is_testnet: props.chainMetadata.testnet,
});
setStepsUI({
@@ -1,5 +1,6 @@
"use client";
import { BarChart3Icon } from "lucide-react";
import { useState } from "react";
import { ThirdwebAreaChart } from "@/components/blocks/charts/area-chart";
import {
@@ -35,10 +36,18 @@ export function ContractAnalyticsOverview(props: {
margin={{
bottom: 20,
}}
className="rounded-xl"
header={{
headerClassName: "p-4 lg:p-6",
title: "Analytics",
titleClassName: "font-semibold text-2xl mb-0.5 tracking-tight",
icon: (
<div className="flex mb-3">
<div className="rounded-full border p-2 bg-background">
<BarChart3Icon className="size-4 text-muted-foreground" />
</div>
</div>
),
description:
"View trends of transactions, events and unique wallets interacting with this contract over time",
}}
@@ -1,77 +0,0 @@
export const mapChainIdToDexScreenerChainSlug = {
8453: "base",
1: "ethereum",
56: "bsc",
369: "pulsechain",
137: "polygon",
2741: "abstract",
43114: "avalanche",
999: "hyperliquid",
480: "worldchain",
42161: "arbitrum",
388: "cronos",
59144: "linea",
1514: "story",
397: "near",
295: "hedera",
146: "sonic",
10: "optimism",
80094: "berachain",
57073: "ink",
130: "unichain",
5000: "mantle",
324: "zksync",
466: "apechain",
1116: "core",
250: "fantom",
1868: "soneium",
81457: "blast",
2000: "dogechain",
14: "flare",
2040: "vana",
4337: "beam",
109: "shibarium",
747: "flowevm",
1088: "metis",
1030: "conflux",
43113: "avalanchedfk",
534352: "scroll",
747474: "katana",
42220: "celo",
1284: "moonbeam",
4200: "merlinchain",
2222: "kava",
39797: "energi",
34443: "mode",
252: "fraxtal",
48900: "zircuit",
20: "elastos",
100: "gnosischain",
204: "opbnb",
169: "manta",
1313161554: "aurora",
3073: "movement",
4689: "iotex",
23294: "oasissapphire",
6001: "bouncebit",
42170: "arbitrumnova",
1101: "polygonzkevm",
40: "telos",
592: "astar",
42262: "oasisemerald",
1285: "moonriver",
245022934: "neonevm",
7777777: "zora",
122: "fuse",
321: "kcc",
1234: "stepnetwork",
106: "velas",
167000: "taiko",
288: "boba",
42766: "zkfair",
32520: "bitgert",
82: "meter",
} as const;
export type DexScreenerChainSlug =
(typeof mapChainIdToDexScreenerChainSlug)[keyof typeof mapChainIdToDexScreenerChainSlug];
@@ -1,58 +0,0 @@
"use client";
import { Spinner } from "@workspace/ui/components/spinner";
import { useTheme } from "next-themes";
import { useMemo } from "react";
import { ClientOnly } from "@/components/blocks/client-only";
import type { DexScreenerChainSlug } from "./dex-screener-chains";
function DexScreenerIframe(props: {
chain: DexScreenerChainSlug;
contractAddress: string;
}) {
const { theme } = useTheme();
const iframeUrl = useMemo(() => {
const resolvedTheme = theme === "light" ? "light" : "dark";
const url = new URL("https://dexscreener.com");
url.pathname = `${props.chain}/${props.contractAddress}`;
url.searchParams.set("embed", "1");
url.searchParams.set("loadChartSettings", "0");
url.searchParams.set("chartTheme", resolvedTheme);
url.searchParams.set("theme", resolvedTheme);
url.searchParams.set("trades", "1");
url.searchParams.set("chartStyle", "1");
url.searchParams.set("chartLeftToolbar", "0");
url.searchParams.set("chartType", "usd");
url.searchParams.set("interval", "15");
url.searchParams.set("chartDefaultOnMobile", "1");
return url.toString();
}, [theme, props.chain, props.contractAddress]);
return (
<iframe
className="w-full h-[500px] lg:h-[1000px] rounded-lg overflow-hidden border bg-card"
src={iframeUrl}
title="DexScreener"
></iframe>
);
}
export function DexScreener(props: {
chain: DexScreenerChainSlug;
contractAddress: string;
}) {
return (
<ClientOnly
ssr={
<div className="w-full h-[500px] lg:h-[1000px] rounded-lg border bg-card items-center justify-center flex">
<Spinner className="size-10" />
</div>
}
>
<DexScreenerIframe
chain={props.chain}
contractAddress={props.contractAddress}
/>
</ClientOnly>
);
}
@@ -1,4 +1,5 @@
import { useQuery } from "@tanstack/react-query";
import { getUnixTime, subDays } from "date-fns";
import { isProd } from "@/constants/env-utils";
import { NEXT_PUBLIC_DASHBOARD_CLIENT_ID } from "@/constants/public-envs";
@@ -35,6 +36,11 @@ export function useTokenTransfers(params: {
url.searchParams.set("page", params.page.toString());
url.searchParams.set("limit", params.limit.toString());
url.searchParams.set("clientId", NEXT_PUBLIC_DASHBOARD_CLIENT_ID);
const THIRTY_DAYS_AGO = subDays(new Date(), 30);
url.searchParams.set(
"block_timestamp_from",
getUnixTime(THIRTY_DAYS_AGO).toString(),
);
const res = await fetch(url);
if (!res.ok) {
@@ -46,7 +52,12 @@ export function useTokenTransfers(params: {
return data;
},
queryKey: ["token-transfers", params],
refetchInterval: 5000,
refetchInterval: (data) => {
if (data?.state.error) {
return false;
}
return 5000;
},
refetchOnWindowFocus: false,
retry: false,
retryOnMount: false,
@@ -1,42 +1,21 @@
import "server-only";
import { isProd } from "@/constants/env-utils";
import { DASHBOARD_THIRDWEB_SECRET_KEY } from "@/constants/server-envs";
import { Bridge, type ThirdwebClient } from "thirdweb";
export async function fetchTokenInfoFromBridge(params: {
chainId: number;
tokenAddress: string;
clientId: string;
client: ThirdwebClient;
}) {
try {
const res = await fetch(
`https://bridge.${isProd ? "thirdweb.com" : "thirdweb-dev.com"}/v1/tokens?chainId=${params.chainId}&tokenAddress=${params.tokenAddress}`,
{
headers: {
"x-secret-key": DASHBOARD_THIRDWEB_SECRET_KEY,
},
},
);
if (!res.ok) {
console.error(
`Failed to fetch token info from bridge: ${await res.text()}`,
);
return null;
}
const data = (await res.json()) as {
data: Array<{
iconUri: string;
address: string;
decimals: number;
name: string;
symbol: string;
priceUsd: number;
}>;
};
return data.data[0];
const res = await Bridge.tokens({
client: params.client,
chainId: params.chainId,
tokenAddress: params.tokenAddress,
includePrices: true,
limit: 1,
});
return res[0];
} catch {
return null;
return undefined;
}
}
@@ -1,9 +1,11 @@
import { BarChart3Icon, DollarSignIcon, TrendingUpIcon } from "lucide-react";
import { cookies } from "next/headers";
import type { ThirdwebContract } from "thirdweb";
import type { ChainMetadata } from "thirdweb/chains";
import { getContractMetadata } from "thirdweb/extensions/common";
import { decimals, getActiveClaimCondition } from "thirdweb/extensions/erc20";
import { GridPattern } from "@/components/ui/background-patterns";
import { BuyAndSwapEmbed } from "@/components/blocks/BuyAndSwapEmbed";
import { GridPatternEmbedContainer } from "@/components/blocks/grid-pattern-embed-container";
import { HAS_USED_DASHBOARD } from "@/constants/cookie";
import { resolveFunctionSelectors } from "@/lib/selectors";
import { AssetPageView } from "../_components/asset-page-view";
@@ -12,9 +14,6 @@ import { PageHeader } from "../_components/PageHeader";
import { ContractHeaderUI } from "./_components/ContractHeader";
import { TokenDropClaim } from "./_components/claim-tokens/claim-tokens-ui";
import { ContractAnalyticsOverview } from "./_components/contract-analytics/contract-analytics";
import { DexScreener } from "./_components/dex-screener";
import { mapChainIdToDexScreenerChainSlug } from "./_components/dex-screener-chains";
import { BuyTokenEmbed } from "./_components/PayEmbedSection";
import { RecentTransfers } from "./_components/RecentTransfers";
import { fetchTokenInfoFromBridge } from "./_utils/fetch-coin-info";
import { getCurrencyMeta } from "./_utils/getCurrencyMeta";
@@ -40,7 +39,7 @@ export async function ERC20PublicPage(props: {
}),
fetchTokenInfoFromBridge({
chainId: props.serverContract.chain.id,
clientId: props.clientContract.client.clientId,
client: props.serverContract.client,
tokenAddress: props.serverContract.address,
}),
resolveFunctionSelectors(props.serverContract),
@@ -77,47 +76,95 @@ export async function ERC20PublicPage(props: {
return (
<div className="flex grow flex-col">
<AssetPageView assetType="coin" chainId={props.chainMetadata.chainId} />
<AssetPageView
assetType="coin"
chainId={props.chainMetadata.chainId}
is_testnet={props.chainMetadata.testnet}
/>
<PageHeader containerClassName="max-w-7xl" />
<div className="border-b">
<div className="border-b border-dashed">
<div className="container max-w-7xl">
<ContractHeaderUI
chainMetadata={props.chainMetadata}
clientContract={props.clientContract}
contractCreator={contractCreator}
image={contractMetadata.image}
image={
contractMetadata.image &&
typeof contractMetadata.image === "string"
? contractMetadata.image
: undefined
}
isDashboardUser={isDashboardUser}
name={contractMetadata.name}
name={
contractMetadata.name && typeof contractMetadata.name === "string"
? contractMetadata.name
: // if we do not have a contract name fall back to the address
props.clientContract.address
}
socialUrls={
typeof contractMetadata.social_urls === "object" &&
contractMetadata.social_urls !== null
? contractMetadata.social_urls
: {}
}
symbol={contractMetadata.symbol}
symbol={
contractMetadata.symbol &&
typeof contractMetadata.symbol === "string"
? contractMetadata.symbol
: undefined
}
/>
</div>
</div>
<div className="h-8" />
{showBuyEmbed && (
<div className="container max-w-7xl pb-10">
<div className=" sm:flex sm:justify-center w-full sm:border sm:border-dashed sm:bg-accent/20 sm:py-12 rounded-lg overflow-hidden relative">
<GridPattern
width={30}
height={30}
x={-1}
y={-1}
strokeDasharray={"4 2"}
className="text-border dark:text-border/70"
style={{
maskImage:
"linear-gradient(to bottom right,white,transparent,transparent)",
}}
{tokenInfoFromUB && (
<div className="container max-w-7xl mb-8">
<div className="grid grid-cols-1 gap-8 lg:grid-cols-3 p-6 border rounded-xl bg-card relative">
<TokenInfoSection
icon={DollarSignIcon}
label="Price"
value={
tokenInfoFromUB.prices.USD
? formatPrice(tokenInfoFromUB.prices.USD)
: "N/A"
}
/>
<div className="sm:w-[420px] z-10">
<TokenInfoSection
className="border-t pt-6 lg:border-l lg:border-t-0 border-dashed lg:pl-8 lg:pt-0"
icon={TrendingUpIcon}
label="Market Cap"
value={
tokenInfoFromUB.marketCapUsd
? formatCompactUSD(tokenInfoFromUB.marketCapUsd)
: "N/A"
}
/>
<TokenInfoSection
className="border-t pt-6 lg:border-l lg:border-t-0 border-dashed lg:pl-8 lg:pt-0"
icon={BarChart3Icon}
label="Volume (24h)"
value={
tokenInfoFromUB.volume24hUsd
? formatCompactUSD(tokenInfoFromUB.volume24hUsd)
: "N/A"
}
/>
</div>
</div>
)}
{showBuyEmbed &&
contractMetadata.name &&
typeof contractMetadata.name === "string" &&
contractMetadata.symbol &&
typeof contractMetadata.symbol === "string" && (
<div className="container max-w-7xl pb-10">
<GridPatternEmbedContainer>
<BuyEmbed
chainMetadata={props.chainMetadata}
claimConditionMeta={claimConditionMeta}
@@ -127,24 +174,9 @@ export async function ERC20PublicPage(props: {
tokenName={contractMetadata.name}
tokenSymbol={contractMetadata.symbol}
/>
</div>
</div>{" "}
</div>
)}
{props.chainMetadata.chainId in mapChainIdToDexScreenerChainSlug && (
<div className="container max-w-7xl pb-10">
<DexScreener
chain={
mapChainIdToDexScreenerChainSlug[
props.chainMetadata
.chainId as keyof typeof mapChainIdToDexScreenerChainSlug
]
}
contractAddress={props.clientContract.address}
/>
</div>
)}
</GridPatternEmbedContainer>
</div>
)}
<div className="container flex max-w-7xl grow flex-col pb-10">
<div className="flex grow flex-col gap-8">
@@ -154,12 +186,15 @@ export async function ERC20PublicPage(props: {
contractAddress={props.clientContract.address}
/>
<RecentTransfers
chainMetadata={props.chainMetadata}
clientContract={props.clientContract}
decimals={tokenDecimals}
tokenSymbol={contractMetadata.symbol}
/>
{contractMetadata.symbol &&
typeof contractMetadata.symbol === "string" && (
<RecentTransfers
chainMetadata={props.chainMetadata}
clientContract={props.clientContract}
decimals={tokenDecimals}
tokenSymbol={contractMetadata.symbol}
/>
)}
</div>
</div>
</div>
@@ -185,10 +220,22 @@ function BuyEmbed(props: {
}) {
if (!props.claimConditionMeta) {
return (
<BuyTokenEmbed
chain={props.clientContract.chain}
client={props.clientContract.client}
tokenAddress={props.clientContract.address}
<BuyAndSwapEmbed
// chain={props.clientContract.chain}
swapTab={{
sellToken: {
chainId: props.clientContract.chain.id,
tokenAddress: props.clientContract.address,
},
buyToken: undefined,
}}
buyTab={{
buyToken: {
chainId: props.clientContract.chain.id,
tokenAddress: props.clientContract.address,
},
}}
pageType="asset"
/>
);
}
@@ -218,3 +265,61 @@ async function getActiveClaimConditionWithErrorHandler(
}
type ActiveClaimCondition = Awaited<ReturnType<typeof getActiveClaimCondition>>;
function TokenInfoSection(props: {
label: string;
value: string;
icon: React.FC<{ className?: string }>;
className?: string;
}) {
return (
<div className={props.className}>
<div className="flex mb-5">
<div className="border rounded-full bg-background p-2">
<props.icon className="size-4 text-muted-foreground" />
</div>
</div>
<dl className="space-y-1.5">
<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">
{props.value}
</dd>
</dl>
</div>
);
}
function formatPrice(value: number): string {
if (value < 100) {
return smallValueUSDFormatter.format(value);
}
return largeValueUSDFormatter.format(value);
}
const smallValueUSDFormatter = new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
maximumFractionDigits: 6,
roundingMode: "halfEven",
});
const largeValueUSDFormatter = new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
maximumFractionDigits: 2,
roundingMode: "halfEven",
});
const compactValueUSDFormatter = new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
notation: "compact",
maximumFractionDigits: 2,
roundingMode: "halfEven",
});
function formatCompactUSD(value: number): string {
return compactValueUSDFormatter.format(value);
}
@@ -18,7 +18,11 @@ export function NFTPublicPageLayout(props: {
}) {
return (
<div className="flex grow flex-col">
<AssetPageView assetType="nft" chainId={props.chainMetadata.chainId} />
<AssetPageView
assetType="nft"
chainId={props.chainMetadata.chainId}
is_testnet={props.chainMetadata.testnet}
/>
<PageHeader containerClassName="max-w-8xl" />
<div className="border-b">
<div className="container max-w-8xl">
@@ -53,6 +53,13 @@ export async function NFTPublicPage(props: {
const _isTokenByIndexSupported =
props.type === "erc721" && isTokenByIndexSupported(functionSelectors);
// FIXME: this is technically a bad fallback but we gotta do what we gotta do
const contractMetadataWithNameAndSymbolFallback = {
...contractMetadata,
// fall back to the contract address if the name is not set
name: contractMetadata.name || props.clientContract.address,
symbol: contractMetadata.symbol || "",
};
const buyNFTDropCard = nftDropClaimParams ? (
<BuyNFTDropCardServer
chainMetadata={props.chainMetadata}
@@ -73,7 +80,7 @@ export async function NFTPublicPage(props: {
<NFTsGrid
chainMetadata={props.chainMetadata}
clientContract={props.clientContract}
collectionMetadata={contractMetadata}
collectionMetadata={contractMetadataWithNameAndSymbolFallback}
gridClassName={
buyNFTDropCard
? "grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4"
@@ -110,7 +117,7 @@ export async function NFTPublicPage(props: {
chainMetadata={props.chainMetadata}
clientContract={props.clientContract}
contractCreator={contractCreator}
contractMetadata={contractMetadata}
contractMetadata={contractMetadataWithNameAndSymbolFallback}
isDashboardUser={isDashboardUser}
>
<ResponsiveLayout
@@ -138,7 +145,7 @@ export async function NFTPublicPage(props: {
<PageLoadTokenViewerSheet
chainMetadata={props.chainMetadata}
clientContract={props.clientContract}
collectionMetadata={contractMetadata}
collectionMetadata={contractMetadataWithNameAndSymbolFallback}
tokenByIndexSupported={_isTokenByIndexSupported}
tokenId={BigInt(props.tokenId)}
type={props.type}
@@ -120,6 +120,7 @@ export function BuyEditionDrop(props: BuyEditionDropProps) {
chainId: props.contract.chain.id,
contractType: "DropERC1155",
error: errorMessage,
is_testnet: props.chainMetadata.testnet,
});
console.error(errorMessage);
@@ -147,6 +148,7 @@ export function BuyEditionDrop(props: BuyEditionDropProps) {
assetType: "nft",
chainId: props.contract.chain.id,
contractType: "DropERC1155",
is_testnet: props.chainMetadata.testnet,
});
props.onSuccess?.();
@@ -163,6 +165,7 @@ export function BuyEditionDrop(props: BuyEditionDropProps) {
chainId: props.contract.chain.id,
contractType: "DropERC1155",
error: errorMessage,
is_testnet: props.chainMetadata.testnet,
});
return;
@@ -180,6 +183,7 @@ export function BuyEditionDrop(props: BuyEditionDropProps) {
chainId: props.contract.chain.id,
contractType: "DropERC1155",
error: errorMessage,
is_testnet: props.chainMetadata.testnet,
});
}
});
@@ -77,6 +77,7 @@ export function BuyNFTDrop(props: BuyNFTDropProps) {
chainId: props.contract.chain.id,
contractType: "DropERC721",
error: errorMessage,
is_testnet: props.chainMetadata.testnet,
});
return;
@@ -103,6 +104,7 @@ export function BuyNFTDrop(props: BuyNFTDropProps) {
assetType: "nft",
chainId: props.contract.chain.id,
contractType: "DropERC721",
is_testnet: props.chainMetadata.testnet,
});
props.onSuccess?.();
@@ -115,6 +117,7 @@ export function BuyNFTDrop(props: BuyNFTDropProps) {
chainId: props.contract.chain.id,
contractType: "DropERC721",
error: errorMessage,
is_testnet: props.chainMetadata.testnet,
});
return;
@@ -131,6 +134,7 @@ export function BuyNFTDrop(props: BuyNFTDropProps) {
chainId: props.contract.chain.id,
contractType: "DropERC721",
error: errorMessage,
is_testnet: props.chainMetadata.testnet,
});
}
};
@@ -37,7 +37,7 @@ export function TokenViewerSheet(
clientContract: ThirdwebContract;
chainMetadata: ChainMetadata;
collectionMetadata: {
name: string;
name: string | null;
image?: string;
};
type: "erc1155" | "erc721";
@@ -57,6 +57,11 @@ export function TokenViewerSheet(
) {
const tokenId = props.variant === "fetch-data" ? props.tokenId : props.nft.id;
const collectionMetadataWithNameFallback = {
...props.collectionMetadata,
name: props.collectionMetadata.name || props.clientContract.address,
};
return (
<Dialog
onOpenChange={(open) => {
@@ -78,7 +83,7 @@ export function TokenViewerSheet(
<FetchAndRenderTokenInfo
chainMetadata={props.chainMetadata}
clientContract={props.clientContract}
collectionMetadata={props.collectionMetadata}
collectionMetadata={collectionMetadataWithNameFallback}
tokenByIndexSupported={props.tokenByIndexSupported}
tokenId={props.tokenId}
type={props.type}
@@ -86,7 +91,7 @@ export function TokenViewerSheet(
) : (
<TokenInfoUI
chainMetadata={props.chainMetadata}
collectionMetadata={props.collectionMetadata}
collectionMetadata={collectionMetadataWithNameFallback}
contract={props.clientContract}
data={props.nft}
tokenId={tokenId}
@@ -84,7 +84,10 @@ export const SettingsMetadata = ({
console.error(err);
}
}
let image: string | undefined = metadata.data?.image;
let image: string | undefined =
metadata.data?.image && typeof metadata.data.image === "string"
? metadata.data.image
: undefined;
try {
image = image
? // eslint-disable-next-line no-restricted-syntax
@@ -137,6 +137,7 @@ export function SettingsPlatformFees({
onChange={(value) =>
form.setValue("platform_fee_basis_points", value, {
shouldValidate: true,
shouldDirty: true,
})
}
value={form.watch("platform_fee_basis_points")}
@@ -151,6 +151,7 @@ export function SettingsRoyalties({
onChange={(value) =>
form.setValue("seller_fee_basis_points", value, {
shouldValidate: true,
shouldDirty: true,
})
}
value={form.watch("seller_fee_basis_points")}
@@ -190,25 +190,37 @@ export async function generateContractLayoutMetadata(params: {
.replace("Testnet", "")
.trim();
const title = `${contractDisplayName} | ${cleanedChainName} Smart Contract`;
let title = `${contractDisplayName} | ${cleanedChainName} Smart Contract`;
let description = "";
if (isERC721 || isERC1155) {
description = `View tokens, source code, transactions, balances, and analytics for the ${contractDisplayName} smart contract on ${cleanedChainName}.`;
} else if (isERC20) {
description = `View ERC20 tokens, transactions, balances, source code, and analytics for the ${contractDisplayName} smart contract on ${cleanedChainName}`;
title = `${contractMetadata.name} (${contractMetadata.symbol}) on ${cleanedChainName} | Buy, Swap, Bridge & Price`;
description = `Buy, swap & bridge ${contractMetadata.name} (${contractMetadata.symbol}) on ${cleanedChainName} with thirdweb Bridge. View contract address, holders, analytics, transactions and live price.`;
} else {
description = `View tokens, transactions, balances, source code, and analytics for the ${contractDisplayName} smart contract on ${cleanedChainName}`;
description = `View tokens, transactions, balances, source code, and analytics for the ${contractMetadata.name} smart contract on ${cleanedChainName}`;
}
return {
description: description,
title: title,
openGraph: {
description: description,
title: title,
},
};
} catch {
const fallbackTitle = `${shortenIfAddress(params.contractAddress)} | ${params.chainIdOrSlug}`;
const fallbackDescription = `View tokens, transactions, balances, source code, and analytics for the smart contract on Chain ID ${params.chainIdOrSlug}`;
return {
description: `View tokens, transactions, balances, source code, and analytics for the smart contract on Chain ID ${params.chainIdOrSlug}`,
title: `${shortenIfAddress(params.contractAddress)} | ${params.chainIdOrSlug}`,
description: fallbackDescription,
title: fallbackTitle,
openGraph: {
description: fallbackDescription,
title: fallbackTitle,
},
};
}
}
@@ -0,0 +1,120 @@
import type { Meta, StoryObj } from "@storybook/nextjs";
import type { Status } from "thirdweb/bridge";
import { storybookThirdwebClient } from "@/storybook/utils";
import { BridgeStatus } from "./bridge-status";
const meta = {
component: BridgeStatus,
title: "chain/tx/bridge-status",
decorators: [
(Story) => (
<div className="container max-w-6xl py-10">
<Story />
</div>
),
],
} satisfies Meta<typeof BridgeStatus>;
export default meta;
type Story = StoryObj<typeof meta>;
const completedStatus: Status = {
status: "COMPLETED",
paymentId:
"0x8e0a0d9c0254a75a3004a28e0fb69bae7f2d659938ddd461753627270930db60",
originAmount: 129935n,
destinationAmount: 100000n,
transactions: [
{
chainId: 42161,
transactionHash:
"0x37daf698878e73590adb2d6f833e810304a9981829ad983fb355245af1183107",
},
{
chainId: 8453,
transactionHash:
"0xc72abba2761bb48f605125dc12c17b15088303db960816fa12316f0c90796417",
},
],
originChainId: 42161,
originTokenAddress: "0xaf88d065e77c8cC2239327C5EDb3A432268e5831",
originToken: {
chainId: 42161,
address: "0xaf88d065e77c8cC2239327C5EDb3A432268e5831",
symbol: "USDC",
name: "USD Coin",
decimals: 6,
iconUri: "https://ethereum-optimism.github.io/data/USDC/logo.png",
},
destinationChainId: 8453,
destinationTokenAddress: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
destinationToken: {
chainId: 8453,
address: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
symbol: "USDC",
name: "USD Coin",
decimals: 6,
iconUri:
"https://coin-images.coingecko.com/coins/images/6319/large/usdc.png?1696506694",
},
sender: "0x2a4f24F935Eb178e3e7BA9B53A5Ee6d8407C0709",
receiver: "0x2a4f24F935Eb178e3e7BA9B53A5Ee6d8407C0709",
};
const pendingStatus: Status = {
...completedStatus,
status: "PENDING",
};
const failedStatus: Status = {
...completedStatus,
status: "FAILED",
};
const notFoundStatus: Status = {
...completedStatus,
status: "NOT_FOUND",
transactions: [],
};
export const Completed: Story = {
args: {
bridgeStatus: completedStatus,
client: storybookThirdwebClient,
},
};
export const Pending: Story = {
args: {
bridgeStatus: pendingStatus,
client: storybookThirdwebClient,
},
};
export const Failed: Story = {
args: {
bridgeStatus: failedStatus,
client: storybookThirdwebClient,
},
};
export const NotFound: Story = {
args: {
bridgeStatus: notFoundStatus,
client: storybookThirdwebClient,
},
};
export const WithPurchaseData: Story = {
args: {
bridgeStatus: {
...completedStatus,
purchaseData: {
userId: "68d645b7ded999651272bf1e",
credits: 32000,
transactionId: "fd2606d1-90df-45c6-bd2c-b19a34764a31",
},
},
client: storybookThirdwebClient,
},
};
@@ -0,0 +1,413 @@
"use client";
import { useQuery } from "@tanstack/react-query";
import { CodeClient } from "@workspace/ui/components/code/code.client";
import { Img } from "@workspace/ui/components/img";
import { Spinner } from "@workspace/ui/components/spinner";
import {
ArrowRightIcon,
ArrowUpRightIcon,
CircleCheckIcon,
CircleXIcon,
} from "lucide-react";
import Link from "next/link";
import { NATIVE_TOKEN_ADDRESS, type ThirdwebClient } from "thirdweb";
import type { Status, Token } from "thirdweb/bridge";
import { status } from "thirdweb/bridge";
import { toTokens } from "thirdweb/utils";
import { WalletAddress } from "@/components/blocks/wallet-address";
import { CopyTextButton } from "@/components/ui/CopyTextButton";
import { LinkWithCopyButton } from "@/components/ui/link-with-copy-button";
import { SkeletonContainer } from "@/components/ui/skeleton";
import { cn } from "@/lib/utils";
import { fetchChain } from "@/utils/fetchChain";
import { resolveSchemeWithErrorHandler } from "@/utils/resolveSchemeWithErrorHandler";
type PurchaseData = Exclude<Status, { status: "NOT_FOUND" }>["purchaseData"];
export function BridgeStatus(props: {
bridgeStatus: Status;
client: ThirdwebClient;
}) {
const { bridgeStatus } = props;
const purchaseDataString =
bridgeStatus.status !== "NOT_FOUND" && bridgeStatus.purchaseData
? getPurchaseData(bridgeStatus.purchaseData)
: undefined;
return (
<div className="bg-card rounded-xl border relative">
{bridgeStatus.status === "FAILED" &&
bridgeStatus.transactions.length > 0 && (
<FailedBridgeStatusContent
transactions={bridgeStatus.transactions}
client={props.client}
/>
)}
{(bridgeStatus.status === "COMPLETED" ||
bridgeStatus.status === "PENDING") && (
<BridgeStatusContent
bridgeStatus={bridgeStatus}
client={props.client}
/>
)}
<div className="px-6 lg:px-8 py-7 space-y-4">
<div className="space-y-1">
<p className="text-sm text-foreground "> Status </p>
<div
className={cn(
"flex items-center gap-1.5 text-sm capitalize",
bridgeStatus.status === "COMPLETED" && "text-green-500",
bridgeStatus.status === "PENDING" && "text-blue-500",
bridgeStatus.status === "FAILED" && "text-red-500",
)}
>
{bridgeStatus.status === "COMPLETED" && (
<CircleCheckIcon className="size-3.5" />
)}
{bridgeStatus.status === "PENDING" && (
<Spinner className="size-3.5" />
)}
{bridgeStatus.status === "FAILED" && (
<CircleXIcon className="size-3.5" />
)}
{bridgeStatus.status === "NOT_FOUND" && (
<CircleXIcon className="size-3.5" />
)}
{bridgeStatus.status.toLowerCase().replace("_", " ")}
</div>
</div>
<div className="space-y-1">
<p className="text-sm text-foreground ">Payment ID</p>
<CopyTextButton
textToCopy={bridgeStatus.paymentId}
textToShow={bridgeStatus.paymentId}
tooltip="Payment ID"
className="text-sm -translate-x-1.5 tabular-nums text-muted-foreground"
copyIconPosition="left"
iconClassName="text-muted-foreground/70"
variant="ghost"
/>
</div>
</div>
{purchaseDataString && (
<div className="px-6 lg:px-8 py-7 space-y-2 border-t border-dashed">
<p className="text-sm text-foreground ">Purchase Data</p>
<CodeClient
code={purchaseDataString}
lang="json"
className="[&_code]:text-xs"
scrollableClassName="p-3"
/>
</div>
)}
</div>
);
}
function TokenInfo(props: {
label: string;
addressLabel: string;
token: Token;
amountWei: bigint | undefined;
walletAddress: string;
client: ThirdwebClient;
txHash: string | undefined;
}) {
const chainQuery = useChainQuery(props.token.chainId);
const isNativeToken =
props.token.address.toLowerCase() === NATIVE_TOKEN_ADDRESS.toLowerCase();
return (
<div className="flex-1 pt-10 pb-9 px-6 lg:px-8">
<div className="flex justify-between items-center">
<h3 className="text-xs font-medium uppercase tracking-wider text-muted-foreground">
{props.label}
</h3>
</div>
<div className="h-6" />
<div>
<div className="flex items-center gap-3">
<div className="relative hover:ring-2 hover:ring-offset-2 hover:ring-offset-card hover:ring-foreground/30 rounded-full">
<Link
href={
props.token.address.toLowerCase() ===
NATIVE_TOKEN_ADDRESS.toLowerCase()
? `/${chainQuery.data?.slug || props.token.chainId}`
: `/${chainQuery.data?.slug || props.token.chainId}/${props.token.address}`
}
target="_blank"
aria-label="View Token"
className="absolute inset-0 z-10"
/>
<Img
src={props.token.iconUri}
alt={props.token.name}
className="size-10 rounded-full"
fallback={
<div className="size-10 from-blue-500 to-foreground rounded-full bg-gradient-to-b" />
}
/>
<div className="absolute -bottom-0.5 -right-0.5 bg-background p-0.5 rounded-full">
<Img
src={resolveSchemeWithErrorHandler({
client: props.client,
uri: chainQuery.data?.icon?.url,
})}
alt={props.token.name}
className="size-4 rounded-full"
fallback={
<div className="size-4 from-blue-500 to-foreground rounded-full bg-gradient-to-b" />
}
/>
</div>
</div>
<div className="space-y-1.5">
<p className="text-lg text-foreground font-medium tracking-tight leading-none">
{props.amountWei ? (
<>
{toTokens(props.amountWei, props.token.decimals)}{" "}
{props.token.symbol}
</>
) : (
"N/A"
)}
</p>
<SkeletonContainer
skeletonData={`Chain ${props.token.chainId}`}
loadedData={chainQuery.data?.name}
className="[&_*]:flex"
render={(data) => (
<Link
href={`/${chainQuery.data?.slug || props.token.chainId}`}
className="text-sm text-muted-foreground leading-none hover:underline flex items-center gap-1"
target="_blank"
>
{data}{" "}
<ArrowUpRightIcon className="size-3.5 text-muted-foreground/70 shrink-0" />
</Link>
)}
/>
</div>
</div>
</div>
<div className="h-8" />
<div className="space-y-4">
<div className="space-y-1">
<p className="text-sm text-foreground ">{props.addressLabel}</p>
<WalletAddress
address={props.walletAddress}
client={props.client}
className="py-0.5 h-auto text-sm [&>*]:!font-sans tabular-nums [&>*]:font-normal text-muted-foreground hover:text-foreground"
iconClassName="size-3"
shortenAddress={false}
/>
</div>
<div className="space-y-1">
<p className="text-sm text-foreground ">Token Address</p>
<LinkWithCopyButton
href={
isNativeToken
? `/${chainQuery.data?.slug || props.token.chainId}`
: `/${chainQuery.data?.slug || props.token.chainId}/${props.token.address}`
}
textToShow={props.token.address}
textToCopy={props.token.address}
copyTooltip="Copy Token Address"
className="-translate-x-1"
/>
</div>
<div className="space-y-1">
<p className="text-sm text-foreground ">Transaction Hash</p>
{props.txHash ? (
<LinkWithCopyButton
className="-translate-x-1"
href={`/${chainQuery.data?.slug || props.token.chainId}/tx/${props.txHash}`}
textToShow={`${props.txHash.slice(0, 12)}...${props.txHash.slice(-4)}`}
textToCopy={props.txHash}
copyTooltip="Copy Transaction Hash"
/>
) : (
<p className="text-sm text-muted-foreground ">N/A</p>
)}
</div>
</div>
</div>
);
}
function BridgeStatusContent(props: {
bridgeStatus: Exclude<Status, { status: "FAILED" } | { status: "NOT_FOUND" }>;
client: ThirdwebClient;
}) {
const { bridgeStatus } = props;
const fromTxHash = bridgeStatus.transactions.find(
(tx) => tx.chainId === bridgeStatus.originChainId,
)?.transactionHash;
const toTxHash = bridgeStatus.transactions.find(
(tx) => tx.chainId === bridgeStatus.destinationChainId,
)?.transactionHash;
return (
<div className="grid grid-cols-1 lg:grid-cols-2 border-b border-dashed">
<TokenInfo
label="From"
token={bridgeStatus.originToken}
amountWei={bridgeStatus.originAmount}
walletAddress={bridgeStatus.sender}
client={props.client}
txHash={fromTxHash}
addressLabel="Sender"
/>
<div className="border-t lg:border-l lg:border-t-0 border-dashed relative">
<div className="p-1.5 rounded-full bg-background absolute top-0 lg:top-1/2 left-1/2 lg:left-0 -translate-y-1/2 -translate-x-1/2 border">
<ArrowRightIcon className="size-4 text-muted-foreground rotate-90 lg:rotate-0" />
</div>
<TokenInfo
label="To"
token={bridgeStatus.destinationToken}
// TODO - destinationAmount is not available in PENDING status, fix it in API
amountWei={
bridgeStatus.status === "COMPLETED"
? bridgeStatus.destinationAmount
: undefined
}
walletAddress={bridgeStatus.receiver}
client={props.client}
txHash={toTxHash}
addressLabel="Receiver"
/>
</div>
</div>
);
}
function FailedBridgeStatusContent(props: {
transactions: { chainId: number; transactionHash: string }[];
client: ThirdwebClient;
}) {
return (
<div className="px-6 lg:px-8 py-7 space-y-1.5 border-b border-dashed">
<h3 className="text-lg font-medium tracking-tight mb-4">Transactions</h3>
<div className="space-y-4">
{props.transactions.map((tx) => {
return (
<TxHashRow
key={tx.chainId}
client={props.client}
chainId={tx.chainId}
txHash={tx.transactionHash}
/>
);
})}
</div>
</div>
);
}
function useChainQuery(chainId: number | undefined) {
return useQuery({
enabled: !!chainId,
queryKey: ["chain-metadata", chainId],
queryFn: async () => {
if (!chainId) {
return null;
}
return fetchChain(chainId);
},
});
}
function TxHashRow(props: {
client: ThirdwebClient;
chainId: number;
txHash: string;
}) {
const chainQuery = useChainQuery(props.chainId);
return (
<div className="space-y-1">
<div className="flex items-center gap-2">
<Img
src={resolveSchemeWithErrorHandler({
client: props.client,
uri: chainQuery.data?.icon?.url,
})}
alt={chainQuery.data?.name}
className="size-3.5 rounded-full"
fallback={
<div className="size-3.5 from-blue-500 to-foreground rounded-full bg-gradient-to-b" />
}
/>
<SkeletonContainer
skeletonData={`Chain ${props.chainId}`}
loadedData={chainQuery.data?.name}
render={(data) => <p className="text-sm text-foreground">{data}</p>}
/>
</div>
<LinkWithCopyButton
href={`/${chainQuery.data?.slug || props.chainId}/tx/${props.txHash}`}
textToShow={props.txHash}
textToCopy={props.txHash}
copyTooltip="Copy Transaction Hash"
className="-translate-x-0.5"
/>
</div>
);
}
export function BridgeStatusWithPolling(props: {
bridgeStatus: Status;
client: ThirdwebClient;
chainId: number;
transactionHash: string;
}) {
const bridgeStatusQuery = useQuery({
enabled:
props.bridgeStatus.status === "PENDING" ||
props.bridgeStatus.status === "NOT_FOUND",
queryKey: ["bridge-status", props.transactionHash, props.chainId],
queryFn: () =>
status({
chainId: props.chainId,
transactionHash: props.transactionHash as `0x${string}`,
client: props.client,
}),
refetchInterval(query) {
const status = query.state.data?.status;
if (status === "COMPLETED" || status === "FAILED") {
return false;
}
return 5000;
},
});
return (
<BridgeStatus
bridgeStatus={bridgeStatusQuery.data || props.bridgeStatus}
client={props.client}
/>
);
}
function getPurchaseData(purchaseData: PurchaseData) {
try {
return JSON.stringify(purchaseData, null, 2);
} catch {
return undefined;
}
}
@@ -1,16 +1,42 @@
import { toTokens, ZERO_ADDRESS } from "thirdweb";
import { Button } from "@workspace/ui/components/button";
import { PlainTextCodeBlock } from "@workspace/ui/components/code/plaintext-code";
import { ToolTipLabel } from "@workspace/ui/components/tooltip";
import { formatDate, formatDistanceToNowStrict, fromUnixTime } from "date-fns";
import {
CircleAlertIcon,
CircleCheckIcon,
Clock4Icon,
InfoIcon,
} from "lucide-react";
import { toTokens } from "thirdweb";
import { status } from "thirdweb/bridge";
import type { ChainMetadata } from "thirdweb/chains";
import {
eth_getBlockByHash,
eth_getTransactionByHash,
eth_getTransactionReceipt,
getRpcClient,
} from "thirdweb/rpc";
import { hexToNumber, shortenAddress, toEther } from "thirdweb/utils";
import type { TransactionReceipt } from "thirdweb/transaction";
import { hexToNumber, toEther } from "thirdweb/utils";
import { Badge } from "@/components/ui/badge";
import { Separator } from "@/components/ui/separator";
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator,
} from "@/components/ui/breadcrumb";
import { CopyTextButton } from "@/components/ui/CopyTextButton";
import { getClientThirdwebClient } from "@/constants/thirdweb-client.client";
import { serverThirdwebClient } from "@/constants/thirdweb-client.server";
import { mapV4ChainToV5Chain } from "@/utils/map-chains";
import { getChain } from "../../../utils";
import { BridgeStatusWithPolling } from "./bridge-status";
import { BridgeAndOverviewTabs } from "./tabs";
type Transaction = Awaited<ReturnType<typeof eth_getTransactionByHash>>;
export default async function Page(props: {
params: Promise<{ chain_id: string; txHash: `0x${string}` }>;
@@ -25,142 +51,342 @@ export default async function Page(props: {
client: serverThirdwebClient,
});
const [transaction, receipt] = await Promise.all([
const [transaction, receipt, bridgeStatus] = await Promise.all([
eth_getTransactionByHash(rpcRequest, {
hash: params.txHash,
}),
}).catch(() => undefined),
eth_getTransactionReceipt(rpcRequest, {
hash: params.txHash,
}),
}).catch(() => undefined),
status({
chainId: chain.chainId,
transactionHash: params.txHash,
client: serverThirdwebClient,
}).catch(() => undefined),
]);
if (!transaction.blockHash) {
return <div>no tx found</div>;
if (!transaction?.blockHash || !receipt) {
return (
<div className="flex flex-col items-center justify-center grow">
<div className="flex flex-col items-center justify-center gap-3">
<div className="p-2 rounded-full bg-background border">
<CircleAlertIcon className="size-5 text-muted-foreground" />
</div>
Transaction not found
</div>
</div>
);
}
const block = await eth_getBlockByHash(rpcRequest, {
blockHash: transaction.blockHash,
});
const overviewContent = (
<GeneericTxDetails
transaction={transaction}
receipt={receipt}
block={block}
chain={chain}
/>
);
return (
<main className="container flex flex-col gap-y-10 py-6">
<header className="flex flex-col gap-y-2">
<p className="font-bold text-3xl">Transaction Details</p>
<div className="flex gap-x-2">
{shortenAddress(transaction.from)} called on{" "}
{shortenAddress(transaction.to || ZERO_ADDRESS)}
</div>
</header>
<main className="pb-20">
<div className="flex h-14 items-center border-border border-b">
<Breadcrumb className="container max-w-6xl">
<BreadcrumbList>
<BreadcrumbItem>
<BreadcrumbLink href="/chainlist">Chainlist</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbLink href={`/${chain.slug}`}>
{chain.name.replace("Mainnet", "")}
</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbPage>Transaction Details</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
</div>
<Separator orientation="horizontal" />
<div className="container max-w-6xl py-10">
<h1 className="font-semibold text-3xl tracking-tight">
Transaction Details
</h1>
</div>
<section className="flex flex-col gap-y-2">
<div className="grid grid-cols-2">
<div className="flex items-center gap-x-2">Transaction hash</div>
<p>{transaction.hash}</p>
</div>
<div className="grid grid-cols-2">
<div className="flex items-center gap-x-2">
Status {/* and method */}
</div>
<div className="flex gap-x-2">
<Badge
className={
receipt.status === "success" ? "bg-green-500" : "bg-red-500"
}
>
{receipt.status}
</Badge>
</div>
</div>
<div className="grid grid-cols-2">
<div className="flex items-center gap-x-2">Block</div>
<p>{Number(transaction.blockNumber)}</p>
</div>
<div className="grid grid-cols-2">
<div className="flex items-center gap-x-2">Timestamp</div>
<p>
{new Date(
Number.parseInt(block.timestamp.toString()) * 1000,
).toLocaleString()}
</p>
</div>
</section>
<Separator orientation="horizontal" />
<section className="flex flex-col gap-y-2">
<div className="grid grid-cols-2">
<div className="flex items-center gap-x-2">From</div>
<p>{transaction.from}</p>
</div>
<div className="grid grid-cols-2">
<div className="flex items-center gap-x-2">
Interacted with contract
</div>
<p>{transaction.to}</p>
</div>
</section>
<Separator orientation="horizontal" />
<section className="flex flex-col gap-y-2">
<div className="grid grid-cols-2">
<div className="flex items-center gap-x-2">Value</div>
<p>{toEther(transaction.value)} ETH</p>
</div>
<div className="grid grid-cols-2">
<div className="flex items-center gap-x-2">Transaction fee</div>
<p>{toEther((transaction.gasPrice || 0n) * receipt.gasUsed)} ETH</p>
</div>
<div className="grid grid-cols-2">
<div className="flex items-center gap-x-2">Gas price</div>
<p>{toEther(transaction.gasPrice || 0n)} ETH</p>
</div>
<div className="grid grid-cols-2">
<div className="flex items-center gap-x-2">Gas usage</div>
<p>{receipt.gasUsed.toString()}</p>
</div>
<div className="grid grid-cols-2">
<div className="flex items-center gap-x-2">Gas fees (Gwei)</div>
<p>
Base: {toTokens(block.baseFeePerGas || 0n, 9)} | Max:{" "}
{toTokens(transaction.maxFeePerGas || 0n, 9)} | Max priority:{" "}
{transaction.maxPriorityFeePerGas?.toString()}
</p>
</div>
<div className="grid grid-cols-2">
<div className="flex items-center gap-x-2">Burnt fees</div>
<p>{toEther(block.baseFeePerGas || 0n * receipt.gasUsed)}</p>
</div>
</section>
<Separator orientation="horizontal" />
<section className="flex flex-col gap-y-2">
<div className="grid grid-cols-2">
<div className="flex items-center gap-x-2">Other</div>
<p>
Txn type: {hexToNumber(transaction.typeHex || "0x0")} (
{transaction.type}) | Nonce: {transaction.nonce} | Position:{" "}
{transaction.transactionIndex}
</p>
</div>
<div className="grid grid-cols-2 items-start">
<div className="flex items-center gap-x-2">Raw Input:</div>
<div className="break-words rounded-lg bg-card p-4 text-sm">
{transaction.input}
</div>
</div>
</section>
<div className="container max-w-6xl">
{bridgeStatus ? (
<BridgeAndOverviewTabs
bridgeStatus={
<BridgeStatusWithPolling
bridgeStatus={bridgeStatus}
client={getClientThirdwebClient()}
chainId={chain.chainId}
transactionHash={params.txHash}
/>
}
overview={overviewContent}
/>
) : (
overviewContent
)}
</div>
</main>
);
}
function GeneericTxDetails(props: {
transaction: Transaction;
receipt: TransactionReceipt;
block: {
timestamp: bigint;
baseFeePerGas: bigint | null;
};
chain: ChainMetadata;
}) {
const { transaction, receipt, chain, block } = props;
const timestamp = getDatefromTimestamp(block.timestamp);
return (
<div className="border rounded-xl p-4 lg:p-6 bg-card">
{/* section 1 */}
<section className="border-b pb-6 space-y-5 lg:space-y-3">
<GridItem
label="Transaction hash"
tooltip="A TxHash or transaction hash is a unique 66-character identifier that is generated whenever a transaction is executed."
>
<CopyTextButton
textToCopy={transaction.hash}
textToShow={transaction.hash}
tooltip="Copy transaction hash"
variant="ghost"
copyIconPosition="right"
className="truncate -translate-x-1.5 max-w-full"
/>
</GridItem>
<GridItem label="Status" tooltip="The status of the transaction.">
<Badge
className="py-0.5 gap-1.5 px-1.5 capitalize"
variant={receipt.status === "success" ? "success" : "destructive"}
>
{receipt.status === "success" ? (
<CircleCheckIcon className="size-3" />
) : (
<CircleAlertIcon className="size-3" />
)}
{receipt.status}
</Badge>
</GridItem>
{transaction.blockNumber && (
<GridItem
label="Block"
tooltip="Number of the block in which the transaction is recorded. Block confirmations indicate how many blocks have been added since the transaction was produced."
>
<p>{Number(transaction.blockNumber)}</p>
</GridItem>
)}
{timestamp && (
<GridItem
label="Timestamp"
tooltip="The date and time at which a transaction is validated."
>
<div className="flex items-center gap-1.5 flex-wrap">
<Clock4Icon className="size-3" />
<p>
{formatDistanceToNowStrict(timestamp, {
addSuffix: true,
})}
</p>
<p>({formatDate(timestamp, "PP pp z")})</p>
</div>
</GridItem>
)}
</section>
{/* section 2 */}
<section className="border-b py-6 space-y-5 lg:space-y-3">
<GridItem label="From" tooltip="The sending party of the transaction.">
<CopyTextButton
textToCopy={transaction.from}
textToShow={transaction.from}
tooltip="Copy from address"
variant="ghost"
copyIconPosition="right"
className="truncate -translate-x-1.5 max-w-full"
/>
</GridItem>
{transaction.to && (
<GridItem
label="Interacted with (To)"
tooltip="The receiving party of the transaction (could be a contract address)."
>
<CopyTextButton
textToCopy={transaction.to}
textToShow={transaction.to}
tooltip="Copy to address"
variant="ghost"
copyIconPosition="right"
className="truncate -translate-x-1.5 max-w-full"
/>
</GridItem>
)}
</section>
{/* section 3 */}
<section className="py-6 space-y-5 lg:space-y-3 border-b">
<GridItem
label="Value"
tooltip="The value being transacted in ETH and fiat value. Note: You can click the fiat value (if available) to see historical value at the time of transaction."
>
<p>
{toEther(transaction.value)} {chain.nativeCurrency?.symbol}
</p>
</GridItem>
<GridItem
label="Transaction Fee"
tooltip="Amount paid to the validator for processing the transaction."
>
<p>
{toEther((transaction.gasPrice || 0n) * receipt.gasUsed)}{" "}
{chain.nativeCurrency?.symbol}
</p>
</GridItem>
<GridItem
label="Gas Price"
tooltip="The price per gas unit the sender is willing to pay"
>
<p>
{toEther(transaction.gasPrice || 0n)} {chain.nativeCurrency?.symbol}
</p>
</GridItem>
<GridItem
label="Gas Usage"
tooltip="The amount of gas used by the transaction."
>
<p>{receipt.gasUsed.toString()}</p>
</GridItem>
<GridItem
label="Gas Fees"
tooltip="Base Fee refers to the network Base Fee at the time of the block, while Max Fee & Max Priority Fee refer to the max amount a user is willing to pay for their tx & to give to the miner respectively."
>
<p className="flex gap-2.5">
<span>Base: {toTokens(block.baseFeePerGas || 0n, 9)} Gwei</span>
{transaction.maxFeePerGas && (
<>
<span className="text-muted-foreground/50">|</span>
<span>
Max: {toTokens(transaction.maxFeePerGas || 0n, 9)} Gwei
</span>
</>
)}
{transaction.maxPriorityFeePerGas && (
<>
<span className="text-muted-foreground/50">|</span>
<span>
Max priority: {transaction.maxPriorityFeePerGas?.toString()}{" "}
Gwei
</span>
</>
)}
</p>
</GridItem>
<GridItem
label="Burnt fees"
tooltip="The amount of fees that were burnt by the transaction."
>
<p>
{toEther((block.baseFeePerGas || 0n) * receipt.gasUsed)}{" "}
{chain.nativeCurrency?.symbol}
</p>
</GridItem>
</section>
{/* section 4 */}
<section className="pt-6 space-y-5 lg:space-y-3">
<GridItem
label="Other Attributes"
tooltip="Other data related to this transaction."
>
<div className="flex gap-2 flex-wrap">
<Badge variant="outline" className="gap-1 py-1">
<span className="text-muted-foreground">Txn type:</span>
<span className="capitalize">
{hexToNumber(transaction.typeHex || "0x0")} ({transaction.type})
</span>
</Badge>
<Badge variant="outline" className="gap-1 py-1">
<span className="text-muted-foreground">Nonce: </span>
<span>{transaction.nonce}</span>
</Badge>
<Badge variant="outline" className="gap-1 py-1">
<span className="text-muted-foreground">Position: </span>
<span>{transaction.transactionIndex}</span>
</Badge>
</div>
</GridItem>
<GridItem
label="Input Data"
tooltip="Additional data included for this transaction. Commonly used as part of contract interaction or as a message sent to the recipient."
>
<PlainTextCodeBlock
code={transaction.input}
className="[&_code]:!whitespace-pre-wrap break-all [&_code]:text-xs text-muted-foreground w-full"
scrollableClassName="max-h-[400px] p-2 min-h-[80px]"
/>
</GridItem>
</section>
</div>
);
}
function GridItem(props: {
label: string;
children: React.ReactNode;
tooltip?: string;
}) {
return (
<div className="flex flex-col gap-1 lg:grid grid-cols-[300px_1fr] items-start">
<div className="flex items-center gap-0.5">
<ToolTipLabel label={props.tooltip}>
<Button
asChild
variant="ghost"
className="h-auto w-auto p-1 hidden lg:block"
>
<div>
<InfoIcon className="size-3.5 text-muted-foreground" />
</div>
</Button>
</ToolTipLabel>
<p className="text-muted-foreground text-sm">{props.label}</p>
</div>
<div className="text-sm w-full">{props.children}</div>
</div>
);
}
function getDatefromTimestamp(timestamp: bigint) {
try {
return fromUnixTime(Number(timestamp));
} catch {
return undefined;
}
}

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