Compare commits

...
Author SHA1 Message Date
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
309 changed files with 14630 additions and 5494 deletions
+1
View File
@@ -10,6 +10,7 @@ module.exports = [
"/wallets",
"/account-abstraction",
"/payments",
"/payments/x402",
"/auth",
"/in-app-wallets",
"/transactions",
+1
View File
@@ -47,6 +47,7 @@
"next-themes": "^0.4.6",
"nextjs-toploader": "^1.6.12",
"nuqs": "^2.4.3",
"ogl": "^1.0.11",
"papaparse": "^5.5.3",
"pluralize": "^8.0.0",
"posthog-js": "1.256.1",
+5
View File
@@ -474,6 +474,11 @@ async function redirects() {
destination: "/team/~/~/support",
permanent: false,
},
{
source: "/routes",
destination: "/tokens",
permanent: false,
},
];
}
+63 -7
View File
@@ -225,7 +225,7 @@ type AssetContractType =
*/
export function reportAssetBuySuccessful(properties: {
chainId: number;
contractType: AssetContractType;
contractType: AssetContractType | undefined;
assetType: "nft" | "coin";
}) {
posthog.capture("asset buy successful", {
@@ -243,9 +243,51 @@ type TokenSwapParams = {
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 swaps from the token page
* - 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 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?
@@ -271,7 +313,7 @@ export function reportSwapWidgetShown(properties: {
/**
* ### Why do we need to report this event?
* - To track number of failed token swaps from the token page
* - To track number of failed token swaps
* - To track which tokens are being swapped the most
*
* ### Who is responsible for this event?
@@ -287,7 +329,7 @@ export function reportTokenSwapFailed(
/**
* ### Why do we need to report this event?
* - To track number of cancelled token swaps from the token page
* - To track number of cancelled token swaps
* - To track which tokens are being swapped the most
*
* ### Who is responsible for this event?
@@ -299,7 +341,7 @@ export function reportTokenSwapCancelled(properties: TokenSwapParams) {
/**
* ### Why do we need to report this event?
* - To track number of failed asset purchases from the token page
* - 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?
@@ -307,7 +349,7 @@ export function reportTokenSwapCancelled(properties: TokenSwapParams) {
*/
export function reportAssetBuyFailed(properties: {
chainId: number;
contractType: AssetContractType;
contractType: AssetContractType | undefined;
assetType: "nft" | "coin";
error: string;
}) {
@@ -329,7 +371,7 @@ export function reportAssetBuyFailed(properties: {
*/
export function reportAssetBuyCancelled(properties: {
chainId: number;
contractType: AssetContractType;
contractType: AssetContractType | undefined;
assetType: "nft" | "coin";
}) {
posthog.capture("asset buy cancelled", {
@@ -649,3 +691,17 @@ export function reportProductFeedback(properties: {
source: properties.source,
});
}
/**
* ### 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: "integrate-bridge" | "trending-tokens";
}) {
posthog.capture("bridge page link clicked", params);
}
+2 -2
View File
@@ -592,7 +592,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 +636,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 [];
}
@@ -0,0 +1,192 @@
"use client";
/* eslint-disable no-restricted-syntax */
import { Mesh, Program, Renderer, Triangle, Vec2 } from "ogl";
import { useEffect, useRef } from "react";
const vertex = `
attribute vec2 position;
void main(){gl_Position=vec4(position,0.0,1.0);}
`;
const fragment = `
#ifdef GL_ES
precision lowp float;
#endif
uniform vec2 uResolution;
uniform float uTime;
uniform float uHueShift;
uniform float uNoise;
uniform float uScan;
uniform float uScanFreq;
uniform float uWarp;
uniform vec3 uBackgroundColor;
uniform float uBackgroundOpacity;
uniform float uPatternLightness;
#define iTime uTime
#define iResolution uResolution
vec4 buf[8];
float rand(vec2 c){return fract(sin(dot(c,vec2(12.9898,78.233)))*43758.5453);}
mat3 rgb2yiq=mat3(0.299,0.587,0.114,0.596,-0.274,-0.322,0.211,-0.523,0.312);
mat3 yiq2rgb=mat3(1.0,0.956,0.621,1.0,-0.272,-0.647,1.0,-1.106,1.703);
vec3 hueShiftRGB(vec3 col,float deg){
vec3 yiq=rgb2yiq*col;
float rad=radians(deg);
float cosh=cos(rad),sinh=sin(rad);
vec3 yiqShift=vec3(yiq.x,yiq.y*cosh-yiq.z*sinh,yiq.y*sinh+yiq.z*cosh);
return clamp(yiq2rgb*yiqShift,0.0,1.0);
}
vec4 sigmoid(vec4 x){return 1./(1.+exp(-x));}
vec4 cppn_fn(vec2 coordinate,float in0,float in1,float in2){
buf[6]=vec4(coordinate.x,coordinate.y,0.3948333106474662+in0,0.36+in1);
buf[7]=vec4(0.14+in2,sqrt(coordinate.x*coordinate.x+coordinate.y*coordinate.y),0.,0.);
buf[0]=mat4(vec4(6.5404263,-3.6126034,0.7590882,-1.13613),vec4(2.4582713,3.1660357,1.2219609,0.06276096),vec4(-5.478085,-6.159632,1.8701609,-4.7742867),vec4(6.039214,-5.542865,-0.90925294,3.251348))*buf[6]+mat4(vec4(0.8473259,-5.722911,3.975766,1.6522468),vec4(-0.24321538,0.5839259,-1.7661959,-5.350116),vec4(0.,0.,0.,0.),vec4(0.,0.,0.,0.))*buf[7]+vec4(0.21808943,1.1243913,-1.7969975,5.0294676);
buf[1]=mat4(vec4(-3.3522482,-6.0612736,0.55641043,-4.4719114),vec4(0.8631464,1.7432913,5.643898,1.6106541),vec4(2.4941394,-3.5012043,1.7184316,6.357333),vec4(3.310376,8.209261,1.1355612,-1.165539))*buf[6]+mat4(vec4(5.24046,-13.034365,0.009859298,15.870829),vec4(2.987511,3.129433,-0.89023495,-1.6822904),vec4(0.,0.,0.,0.),vec4(0.,0.,0.,0.))*buf[7]+vec4(-5.9457836,-6.573602,-0.8812491,1.5436668);
buf[0]=sigmoid(buf[0]);buf[1]=sigmoid(buf[1]);
buf[2]=mat4(vec4(-15.219568,8.095543,-2.429353,-1.9381982),vec4(-5.951362,4.3115187,2.6393783,1.274315),vec4(-7.3145227,6.7297835,5.2473326,5.9411426),vec4(5.0796127,8.979051,-1.7278991,-1.158976))*buf[6]+mat4(vec4(-11.967154,-11.608155,6.1486754,11.237008),vec4(2.124141,-6.263192,-1.7050359,-0.7021966),vec4(0.,0.,0.,0.),vec4(0.,0.,0.,0.))*buf[7]+vec4(-4.17164,-3.2281182,-4.576417,-3.6401186);
buf[3]=mat4(vec4(3.1832156,-13.738922,1.879223,3.233465),vec4(0.64300746,12.768129,1.9141049,0.50990224),vec4(-0.049295485,4.4807224,1.4733979,1.801449),vec4(5.0039253,13.000481,3.3991797,-4.5561905))*buf[6]+mat4(vec4(-0.1285731,7.720628,-3.1425676,4.742367),vec4(0.6393625,3.714393,-0.8108378,-0.39174938),vec4(0.,0.,0.,0.),vec4(0.,0.,0.,0.))*buf[7]+vec4(-1.1811101,-21.621881,0.7851888,1.2329718);
buf[2]=sigmoid(buf[2]);buf[3]=sigmoid(buf[3]);
buf[4]=mat4(vec4(5.214916,-7.183024,2.7228765,2.6592617),vec4(-5.601878,-25.3591,4.067988,0.4602802),vec4(-10.57759,24.286327,21.102104,37.546658),vec4(4.3024497,-1.9625226,2.3458803,-1.372816))*buf[0]+mat4(vec4(-17.6526,-10.507558,2.2587414,12.462782),vec4(6.265566,-502.75443,-12.642513,0.9112289),vec4(-10.983244,20.741234,-9.701768,-0.7635988),vec4(5.383626,1.4819539,-4.1911616,-4.8444734))*buf[1]+mat4(vec4(12.785233,-16.345072,-0.39901125,1.7955981),vec4(-30.48365,-1.8345358,1.4542528,-1.1118771),vec4(19.872723,-7.337935,-42.941723,-98.52709),vec4(8.337645,-2.7312303,-2.2927687,-36.142323))*buf[2]+mat4(vec4(-16.298317,3.5471997,-0.44300047,-9.444417),vec4(57.5077,-35.609753,16.163465,-4.1534753),vec4(-0.07470326,-3.8656476,-7.0901804,3.1523974),vec4(-12.559385,-7.077619,1.490437,-0.8211543))*buf[3]+vec4(-7.67914,15.927437,1.3207729,-1.6686112);
buf[5]=mat4(vec4(-1.4109162,-0.372762,-3.770383,-21.367174),vec4(-6.2103205,-9.35908,0.92529047,8.82561),vec4(11.460242,-22.348068,13.625772,-18.693201),vec4(-0.3429052,-3.9905605,-2.4626114,-0.45033523))*buf[0]+mat4(vec4(7.3481627,-4.3661838,-6.3037653,-3.868115),vec4(1.5462853,6.5488915,1.9701879,-0.58291394),vec4(6.5858274,-2.2180402,3.7127688,-1.3730392),vec4(-5.7973905,10.134961,-2.3395722,-5.965605))*buf[1]+mat4(vec4(-2.5132585,-6.6685553,-1.4029363,-0.16285264),vec4(-0.37908727,0.53738135,4.389061,-1.3024765),vec4(-0.70647055,2.0111287,-5.1659346,-3.728635),vec4(-13.562562,10.487719,-0.9173751,-2.6487076))*buf[2]+mat4(vec4(-8.645013,6.5546675,-6.3944063,-5.5933375),vec4(-0.57783127,-1.077275,36.91025,5.736769),vec4(14.283112,3.7146652,7.1452246,-4.5958776),vec4(2.7192075,3.6021907,-4.366337,-2.3653464))*buf[3]+vec4(-5.9000807,-4.329569,1.2427121,8.59503);
buf[4]=sigmoid(buf[4]);buf[5]=sigmoid(buf[5]);
buf[6]=mat4(vec4(-1.61102,0.7970257,1.4675229,0.20917463),vec4(-28.793737,-7.1390953,1.5025433,4.656581),vec4(-10.94861,39.66238,0.74318546,-10.095605),vec4(-0.7229728,-1.5483948,0.7301322,2.1687684))*buf[0]+mat4(vec4(3.2547753,21.489103,-1.0194173,-3.3100595),vec4(-3.7316632,-3.3792162,-7.223193,-0.23685838),vec4(13.1804495,0.7916005,5.338587,5.687114),vec4(-4.167605,-17.798311,-6.815736,-1.6451967))*buf[1]+mat4(vec4(0.604885,-7.800309,-7.213122,-2.741014),vec4(-3.522382,-0.12359311,-0.5258442,0.43852118),vec4(9.6752825,-22.853785,2.062431,0.099892326),vec4(-4.3196306,-17.730087,2.5184598,5.30267))*buf[2]+mat4(vec4(-6.545563,-15.790176,-6.0438633,-5.415399),vec4(-43.591583,28.551912,-16.00161,18.84728),vec4(4.212382,8.394307,3.0958717,8.657522),vec4(-5.0237565,-4.450633,-4.4768,-5.5010443))*buf[3]+mat4(vec4(1.6985557,-67.05806,6.897715,1.9004834),vec4(1.8680354,2.3915145,2.5231109,4.081538),vec4(11.158006,1.7294737,2.0738268,7.386411),vec4(-4.256034,-306.24686,8.258898,-17.132736))*buf[4]+mat4(vec4(1.6889864,-4.5852966,3.8534803,-6.3482175),vec4(1.3543309,-1.2640043,9.932754,2.9079645),vec4(-5.2770967,0.07150358,-0.13962056,3.3269649),vec4(28.34703,-4.918278,6.1044083,4.085355))*buf[5]+vec4(6.6818056,12.522166,-3.7075126,-4.104386);
buf[7]=mat4(vec4(-8.265602,-4.7027016,5.098234,0.7509808),vec4(8.6507845,-17.15949,16.51939,-8.884479),vec4(-4.036479,-2.3946867,-2.6055532,-1.9866527),vec4(-2.2167742,-1.8135649,-5.9759874,4.8846445))*buf[0]+mat4(vec4(6.7790847,3.5076547,-2.8191125,-2.7028968),vec4(-5.743024,-0.27844876,1.4958696,-5.0517144),vec4(13.122226,15.735168,-2.9397483,-4.101023),vec4(-14.375265,-5.030483,-6.2599335,2.9848232))*buf[1]+mat4(vec4(4.0950394,-0.94011575,-5.674733,4.755022),vec4(4.3809423,4.8310084,1.7425908,-3.437416),vec4(2.117492,0.16342592,-104.56341,16.949184),vec4(-5.22543,-2.994248,3.8350096,-1.9364246))*buf[2]+mat4(vec4(-5.900337,1.7946124,-13.604192,-3.8060522),vec4(6.6583457,31.911177,25.164474,91.81147),vec4(11.840538,4.1503043,-0.7314397,6.768467),vec4(-6.3967767,4.034772,6.1714606,-0.32874924))*buf[3]+mat4(vec4(3.4992442,-196.91893,-8.923708,2.8142626),vec4(3.4806502,-3.1846354,5.1725626,5.1804223),vec4(-2.4009497,15.585794,1.2863957,2.0252278),vec4(-71.25271,-62.441242,-8.138444,0.50670296))*buf[4]+mat4(vec4(-12.291733,-11.176166,-7.3474145,4.390294),vec4(10.805477,5.6337385,-0.9385842,-4.7348723),vec4(-12.869276,-7.039391,5.3029537,7.5436664),vec4(1.4593618,8.91898,3.5101583,5.840625))*buf[5]+vec4(2.2415268,-6.705987,-0.98861027,-2.117676);
buf[6]=sigmoid(buf[6]);buf[7]=sigmoid(buf[7]);
buf[0]=mat4(vec4(1.6794263,1.3817469,2.9625452,0.),vec4(-1.8834411,-1.4806935,-3.5924516,0.),vec4(-1.3279216,-1.0918057,-2.3124623,0.),vec4(0.2662234,0.23235129,0.44178495,0.))*buf[0]+mat4(vec4(-0.6299101,-0.5945583,-0.9125601,0.),vec4(0.17828953,0.18300213,0.18182953,0.),vec4(-2.96544,-2.5819945,-4.9001055,0.),vec4(1.4195864,1.1868085,2.5176322,0.))*buf[1]+mat4(vec4(-1.2584374,-1.0552157,-2.1688404,0.),vec4(-0.7200217,-0.52666044,-1.438251,0.),vec4(0.15345335,0.15196142,0.272854,0.),vec4(0.945728,0.8861938,1.2766753,0.))*buf[2]+mat4(vec4(-2.4218085,-1.968602,-4.35166,0.),vec4(-22.683098,-18.0544,-41.954372,0.),vec4(0.63792,0.5470648,1.1078634,0.),vec4(-1.5489894,-1.3075932,-2.6444845,0.))*buf[3]+mat4(vec4(-0.49252132,-0.39877754,-0.91366625,0.),vec4(0.95609266,0.7923952,1.640221,0.),vec4(0.30616966,0.15693925,0.8639857,0.),vec4(1.1825981,0.94504964,2.176963,0.))*buf[4]+mat4(vec4(0.35446745,0.3293795,0.59547555,0.),vec4(-0.58784515,-0.48177817,-1.0614829,0.),vec4(2.5271258,1.9991658,4.6846647,0.),vec4(0.13042648,0.08864098,0.30187556,0.))*buf[5]+mat4(vec4(-1.7718065,-1.4033192,-3.3355875,0.),vec4(3.1664357,2.638297,5.378702,0.),vec4(-3.1724713,-2.6107926,-5.549295,0.),vec4(-2.851368,-2.249092,-5.3013067,0.))*buf[6]+mat4(vec4(1.5203838,1.2212278,2.8404984,0.),vec4(1.5210563,1.2651345,2.683903,0.),vec4(2.9789467,2.4364579,5.2347264,0.),vec4(2.2270417,1.8825914,3.8028636,0.))*buf[7]+vec4(-1.5468478,-3.6171484,0.24762098,0.);
buf[0]=sigmoid(buf[0]);
return vec4(buf[0].x,buf[0].y,buf[0].z,1.);
}
void mainImage(out vec4 fragColor,in vec2 fragCoord){
vec2 uv=fragCoord/uResolution.xy*2.-1.;
uv.y*=-1.;
uv+=uWarp*vec2(sin(uv.y*6.283+uTime*0.5),cos(uv.x*6.283+uTime*0.5))*0.05;
fragColor=cppn_fn(uv,0.1*sin(0.3*uTime),0.1*sin(0.69*uTime),0.1*sin(0.44*uTime));
}
void main(){
vec4 col;mainImage(col,gl_FragCoord.xy);
col.rgb=hueShiftRGB(col.rgb,uHueShift);
float scanline_val=sin(gl_FragCoord.y*uScanFreq)*0.5+0.5;
col.rgb*=1.-(scanline_val*scanline_val)*uScan;
col.rgb+=(rand(gl_FragCoord.xy+uTime)-0.5)*uNoise;
// Apply lightness control to pattern
col.rgb = col.rgb * uPatternLightness;
// Use background color as base, then add pattern on top
vec3 finalColor = uBackgroundColor + col.rgb * uBackgroundOpacity;
gl_FragColor=vec4(clamp(finalColor,0.0,1.0),1.0);
}
`;
type Props = {
hueShift?: number;
noiseIntensity?: number;
scanlineIntensity?: number;
speed?: number;
scanlineFrequency?: number;
warpAmount?: number;
resolutionScale?: number;
backgroundColor?: [number, number, number];
backgroundOpacity?: number;
patternLightness?: number;
};
export function DarkVeil({
hueShift = 0,
noiseIntensity = 0,
scanlineIntensity = 0,
speed = 0.5,
scanlineFrequency = 0,
warpAmount = 0,
resolutionScale = 1,
backgroundColor = [0, 0, 0],
backgroundOpacity = 1,
patternLightness = 1.0,
}: Props) {
const ref = useRef<HTMLCanvasElement>(null);
useEffect(() => {
const canvas = ref.current as HTMLCanvasElement;
const parent = canvas.parentElement as HTMLElement;
const renderer = new Renderer({
dpr: Math.min(window.devicePixelRatio, 2),
canvas,
});
const gl = renderer.gl;
const geometry = new Triangle(gl);
const program = new Program(gl, {
vertex,
fragment,
uniforms: {
uTime: { value: 0 },
uResolution: { value: new Vec2() },
uHueShift: { value: hueShift },
uNoise: { value: noiseIntensity },
uScan: { value: scanlineIntensity },
uScanFreq: { value: scanlineFrequency },
uWarp: { value: warpAmount },
uBackgroundColor: { value: backgroundColor },
uBackgroundOpacity: { value: backgroundOpacity },
uPatternLightness: { value: patternLightness },
},
});
const mesh = new Mesh(gl, { geometry, program });
const resize = () => {
const w = parent.clientWidth,
h = parent.clientHeight;
renderer.setSize(w * resolutionScale, h * resolutionScale);
program.uniforms.uResolution.value.set(w, h);
};
window.addEventListener("resize", resize);
resize();
const start = performance.now();
let frame = 0;
const loop = () => {
program.uniforms.uTime.value =
((performance.now() - start) / 1000) * speed;
program.uniforms.uHueShift.value = hueShift;
program.uniforms.uNoise.value = noiseIntensity;
program.uniforms.uScan.value = scanlineIntensity;
program.uniforms.uScanFreq.value = scanlineFrequency;
program.uniforms.uWarp.value = warpAmount;
program.uniforms.uBackgroundColor.value = backgroundColor;
program.uniforms.uBackgroundOpacity.value = backgroundOpacity;
program.uniforms.uPatternLightness.value = patternLightness;
renderer.render({ scene: mesh });
frame = requestAnimationFrame(loop);
};
loop();
return () => {
cancelAnimationFrame(frame);
window.removeEventListener("resize", resize);
};
}, [
hueShift,
noiseIntensity,
scanlineIntensity,
speed,
scanlineFrequency,
warpAmount,
resolutionScale,
backgroundColor,
backgroundOpacity,
patternLightness,
]);
return <canvas ref={ref} className="w-full h-full block bg-background" />;
}
@@ -1,29 +1,39 @@
"use client";
import { useTheme } from "next-themes";
import { useEffect, useRef, useState } from "react";
import type { Chain, ThirdwebClient } from "thirdweb";
import { useEffect, useMemo, useRef, useState } from "react";
import type { Chain } from "thirdweb";
import { BuyWidget, SwapWidget } from "thirdweb/react";
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 { getConfiguredThirdwebClient } from "../../constants/thirdweb.server";
type PageType = "asset" | "bridge" | "chain";
export function BuyAndSwapEmbed(props: {
client: ThirdwebClient;
chain: Chain;
tokenAddress: string | undefined;
buyAmount: string | undefined;
pageType: "asset" | "bridge" | "chain";
pageType: PageType;
}) {
const { theme } = useTheme();
const [tab, setTab] = useState<"buy" | "swap">("swap");
@@ -41,8 +51,23 @@ export function BuyAndSwapEmbed(props: {
});
}, [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">
<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"
@@ -60,38 +85,87 @@ export function BuyAndSwapEmbed(props: {
<BuyWidget
amount={props.buyAmount || "1"}
chain={props.chain}
className="!rounded-2xl !w-full !border-none"
className="!rounded-2xl !border-none"
title=""
client={props.client}
client={client}
connectOptions={{
autoConnect: false,
}}
onError={(e) => {
onError={(e, quote) => {
const errorMessage = parseError(e);
reportTokenBuyFailed({
buyTokenChainId:
quote?.type === "buy"
? quote.intent.destinationChainId
: quote?.type === "onramp"
? quote.intent.chainId
: undefined,
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: props.chain.id,
contractType: "DropERC20",
error: errorMessage,
contractType: undefined,
});
}
}}
onCancel={() => {
onCancel={(quote) => {
reportTokenBuyCancelled({
buyTokenChainId:
quote?.type === "buy"
? quote.intent.destinationChainId
: quote?.type === "onramp"
? quote.intent.chainId
: undefined,
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: props.chain.id,
contractType: "DropERC20",
contractType: undefined,
});
}
}}
onSuccess={() => {
onSuccess={(quote) => {
reportTokenBuySuccessful({
buyTokenChainId:
quote.type === "buy"
? quote.intent.destinationChainId
: quote.type === "onramp"
? quote.intent.chainId
: undefined,
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: props.chain.id,
contractType: "DropERC20",
contractType: undefined,
});
}
}}
@@ -103,16 +177,17 @@ export function BuyAndSwapEmbed(props: {
{tab === "swap" && (
<SwapWidget
client={props.client}
client={client}
theme={themeObj}
className="!rounded-2xl !border-none !w-full"
className="!rounded-2xl !border-none"
prefill={{
sellToken: {
// buy this token by default
buyToken: {
chainId: props.chain.id,
tokenAddress: props.tokenAddress,
},
// only set `buyToken` as "Native token" if `sellToken` is not a "native token" already
buyToken: props.tokenAddress
// sell the native token by default (but if buytoken is a native token, don't set)
sellToken: props.tokenAddress
? {
chainId: props.chain.id,
}
@@ -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,73 @@
"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 FaqSection(props: {
faqs: Array<{ title: string; description: string }>;
}) {
return (
<section>
<h2 className="text-2xl md:text-3xl font-semibold mb-4 tracking-tight">
Frequently asked questions
</h2>
<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>
</section>
);
}
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>
);
}
@@ -4,7 +4,7 @@ export function GridPatternEmbedContainer(props: {
children: React.ReactNode;
}) {
return (
<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">
<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}
@@ -17,7 +17,7 @@ export function GridPatternEmbedContainer(props: {
"linear-gradient(to bottom right,white,transparent,transparent)",
}}
/>
<div className="sm:w-[420px] z-10">{props.children}</div>
<div className="z-10 min-w-0">{props.children}</div>
</div>
);
}
@@ -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"
@@ -44,11 +44,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;
}
@@ -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;
+1 -17
View File
@@ -92,23 +92,7 @@ if (REDIS_URL) {
);
}
// DO NOT TAINT THESE VALUES (for now)
export const GROWTH_PLAN_SKU = process.env.GROWTH_PLAN_SKU || "";
if (GROWTH_PLAN_SKU) {
experimental_taintUniqueValue(
"Do not pass GROWTH_PLAN_SKU to the client",
process,
GROWTH_PLAN_SKU,
);
}
export const PAYMENT_METHOD_CONFIGURATION =
process.env.PAYMENT_METHOD_CONFIGURATION || "";
if (PAYMENT_METHOD_CONFIGURATION) {
experimental_taintUniqueValue(
"Do not pass PAYMENT_METHOD_CONFIGURATION to the client",
process,
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({
+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),
@@ -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,18 +1,13 @@
"use client";
import type { ThirdwebClient } from "thirdweb";
import type { ChainMetadata } from "thirdweb/chains";
import { BuyAndSwapEmbed } from "@/components/blocks/BuyAndSwapEmbed";
import { GridPatternEmbedContainer } from "@/components/blocks/grid-pattern-embed-container";
import { defineDashboardChain } from "@/lib/defineDashboardChain";
export function BuyFundsSection(props: {
chain: ChainMetadata;
client: ThirdwebClient;
}) {
export function BuyFundsSection(props: { chain: ChainMetadata }) {
return (
<GridPatternEmbedContainer>
<BuyAndSwapEmbed
client={props.client}
// eslint-disable-next-line no-restricted-syntax
chain={defineDashboardChain(props.chain.chainId, props.chain)}
buyAmount={undefined}
@@ -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,39 +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;
@@ -67,8 +79,10 @@ 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">
@@ -123,7 +137,7 @@ export default async function ChainPageLayout(props: {
<ChainHeader
chain={chain}
client={client}
headerImageUrl={chainMetadata?.headerImgUrl}
headerImageUrl={customChainMetadata?.headerImgUrl}
logoUrl={chain.icon?.url}
/>
@@ -131,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>
@@ -153,7 +167,7 @@ export default async function ChainPageLayout(props: {
)}
{/* Gas Sponsored badge - Desktop */}
{chainMetadata?.gasSponsored && (
{customChainMetadata?.gasSponsored && (
<div className="hidden md:block">
<GasSponsoredBadge />
</div>
@@ -161,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 { FaqSection } 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,12 @@ export default async function Page(props: {
{chain.services.filter((s) => s.enabled).length > 0 && (
<SupportedProductsSection services={chain.services} />
)}
{chainSeo?.faqs && chainSeo.faqs.length > 0 && (
<div className="py-10">
<FaqSection faqs={chainSeo.faqs} />
</div>
)}
</div>
);
}
@@ -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[] = [];
@@ -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";
@@ -62,8 +61,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",
@@ -28,6 +28,9 @@ export function PublicPageConnectButton(props: {
connectButton={{
className: props.connectButtonClassName,
}}
detailsButton={{
className: props.connectButtonClassName,
}}
connectModal={{
privacyPolicyUrl: "/privacy-policy",
showThirdwebBranding: false,
@@ -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,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}
@@ -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,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,3 +1,4 @@
import { BarChart3Icon, DollarSignIcon, TrendingUpIcon } from "lucide-react";
import { cookies } from "next/headers";
import type { ThirdwebContract } from "thirdweb";
import type { ChainMetadata } from "thirdweb/chains";
@@ -13,8 +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 { 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),
@@ -80,7 +79,7 @@ export async function ERC20PublicPage(props: {
<AssetPageView assetType="coin" chainId={props.chainMetadata.chainId} />
<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}
@@ -102,6 +101,44 @@ export async function ERC20PublicPage(props: {
<div className="h-8" />
{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"
}
/>
<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 && (
<div className="container max-w-7xl pb-10">
<GridPatternEmbedContainer>
@@ -118,20 +155,6 @@ export async function ERC20PublicPage(props: {
</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>
)}
<div className="container flex max-w-7xl grow flex-col pb-10">
<div className="flex grow flex-col gap-8">
<ContractAnalyticsOverview
@@ -173,7 +196,6 @@ function BuyEmbed(props: {
return (
<BuyAndSwapEmbed
chain={props.clientContract.chain}
client={props.clientContract.client}
tokenAddress={props.clientContract.address}
buyAmount={undefined}
pageType="asset"
@@ -206,3 +228,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);
}
@@ -5,7 +5,7 @@ import { CopyTextButton } from "@/components/ui/CopyTextButton";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import type { ChainSupportedService } from "@/types/chain";
import { ChainIcon } from "../../../components/server/chain-icon";
import { getChainMetadata } from "../../../utils";
import { getCustomChainMetadata } from "../../../utils";
type ChainListCardProps = {
favoriteButton: JSX.Element | undefined;
@@ -18,7 +18,7 @@ type ChainListCardProps = {
iconUrl?: string;
};
export async function ChainListCard({
export function ChainListCard({
isDeprecated,
chainId,
chainName,
@@ -28,7 +28,7 @@ export async function ChainListCard({
favoriteButton,
iconUrl,
}: ChainListCardProps) {
const chainMetadata = await getChainMetadata(chainId);
const customChainMetadata = getCustomChainMetadata(chainId);
return (
<div className="relative h-full">
@@ -90,9 +90,9 @@ export async function ChainListCard({
</tbody>
</table>
{(isDeprecated || chainMetadata?.gasSponsored) && (
{(isDeprecated || customChainMetadata?.gasSponsored) && (
<div className="mt-5 flex gap-5 border-t pt-4">
{!isDeprecated && chainMetadata?.gasSponsored && (
{!isDeprecated && customChainMetadata?.gasSponsored && (
<div className="flex items-center gap-1.5">
<TicketCheckIcon className="size-5 text-foreground" />
<p className="text-sm">Gas Sponsored</p>
@@ -13,7 +13,7 @@ import { cn } from "@/lib/utils";
import type { ChainSupportedService } from "@/types/chain";
import { ChainIcon } from "../../../components/server/chain-icon";
import { products } from "../../../components/server/products";
import { getChainMetadata } from "../../../utils";
import { getCustomChainMetadata } from "../../../utils";
type ChainListRowProps = {
favoriteButton: JSX.Element | undefined;
@@ -26,7 +26,7 @@ type ChainListRowProps = {
iconUrl?: string;
};
export async function ChainListRow({
export function ChainListRow({
isDeprecated,
chainId,
chainName,
@@ -36,7 +36,7 @@ export async function ChainListRow({
favoriteButton,
iconUrl,
}: ChainListRowProps) {
const chainMetadata = await getChainMetadata(chainId);
const chainMetadata = getCustomChainMetadata(chainId);
return (
<TableRow className="hover:bg-accent/50" linkBox>
{/* Name */}
@@ -914,9 +914,9 @@ const chainMetaRecord = {
} satisfies Record<number, ExtraChainMetadata>;
// END TEMPORARY
export async function getChainMetadata(
export function getCustomChainMetadata(
chainId: number,
): Promise<(ExtraChainMetadata & { gasSponsored?: true }) | null> {
): (ExtraChainMetadata & { gasSponsored?: true }) | null {
// TODO: fetch this from the API
if (chainId in chainMetaRecord) {
return {
@@ -0,0 +1,39 @@
import Link from "next/link";
import { ToggleThemeButton } from "@/components/blocks/color-mode-toggle";
import { cn } from "@/lib/utils";
import { ThirdwebMiniLogo } from "../../../components/ThirdwebMiniLogo";
import { PublicPageConnectButton } from "../../(chain)/[chain_id]/[contractAddress]/public-pages/_components/PublicPageConnectButton";
export function PageHeader(props: { containerClassName?: string }) {
return (
<div className="border-b">
<header
className={cn(
"container flex max-w-7xl justify-between py-3",
props.containerClassName,
)}
>
<div className="flex items-center gap-4">
<Link className="flex items-center gap-2" href="/team">
<ThirdwebMiniLogo className="h-5" />
<span className="hidden font-bold text-2xl tracking-tight lg:block">
thirdweb
</span>
</Link>
</div>
<div className="flex items-center gap-5">
<Link
href="https://portal.thirdweb.com/bridge"
target="_blank"
className="text-sm text-muted-foreground hover:text-foreground"
>
Docs
</Link>
<ToggleThemeButton />
<PublicPageConnectButton connectButtonClassName="!rounded-full" />
</div>
</header>
</div>
);
}
@@ -0,0 +1,142 @@
"use client";
import { useQuery } from "@tanstack/react-query";
import { Input } from "@workspace/ui/components/input";
import { cn } from "@workspace/ui/lib/utils";
import { ActivityIcon, SearchIcon, TrendingUpIcon } from "lucide-react";
import { useCallback, useState } from "react";
import { Bridge } from "thirdweb";
import { BridgeNetworkSelector } from "@/components/blocks/NetworkSelectors";
import { Button } from "@/components/ui/button";
import { getClientThirdwebClient } from "@/constants/thirdweb-client.client";
import { TokensTable } from "./tokens-table";
const client = getClientThirdwebClient();
const pageSize = 20;
type Filter =
| { type: "filter"; value: "volume" | "market_cap" }
| { type: "search"; value: string };
export function TokenPage(props: { chains: Bridge.chains.Result }) {
const [page, setPage] = useState(1);
const [chainId, setChainId] = useState(1);
const [filter, _setFilter] = useState<Filter>({
type: "filter",
value: "volume",
});
const setFilter = useCallback((filter: Filter) => {
_setFilter(filter);
setPage(1);
}, []);
const tokensQuery = useQuery({
queryKey: [
"tokens",
{
page,
chainId,
filter,
},
],
queryFn: () => {
return Bridge.tokens({
client: client,
chainId: chainId,
limit: pageSize,
offset: (page - 1) * pageSize,
sortBy: filter.type === "filter" ? filter.value : undefined,
query: filter.type === "search" ? filter.value : undefined,
});
},
refetchOnMount: false,
refetchOnWindowFocus: false,
});
return (
<div className="pb-20 pt-8">
<div className="container max-w-7xl">
<div className="mb-4 flex gap-4 lg:gap-3 flex-col lg:flex-row">
<BridgeNetworkSelector
client={client}
chainId={chainId}
onChange={setChainId}
className="rounded-xl bg-card lg:w-fit min-w-[280px]"
popoverContentClassName="!w-[300px] rounded-xl overflow-hidden"
chains={props.chains}
/>
<div className="flex gap-3">
<SortButton
label="Popular"
onClick={() => {
setFilter({ type: "filter", value: "market_cap" });
}}
isSelected={
filter.type === "filter" && filter.value === "market_cap"
}
icon={ActivityIcon}
/>
<SortButton
label="Trending"
onClick={() => {
setFilter({ type: "filter", value: "volume" });
}}
isSelected={filter.type === "filter" && filter.value === "volume"}
icon={TrendingUpIcon}
/>
</div>
<div className="relative w-full lg:w-[420px] ml-auto">
<SearchIcon className="size-4 absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground" />
<Input
placeholder="Search Tokens"
value={filter.type === "search" ? filter.value : ""}
className="rounded-xl bg-card flex-1 pl-9"
onChange={(e) =>
setFilter({ type: "search", value: e.target.value })
}
/>
</div>
</div>
<TokensTable
pageSize={pageSize}
tokens={tokensQuery.data ?? []}
isFetching={tokensQuery.isFetching}
pagination={{
onNext: () => setPage(page + 1),
onPrevious: () => setPage(page - 1),
nextDisabled: !!(
tokensQuery.data && tokensQuery.data.length < pageSize
),
previousDisabled: page === 1,
}}
/>
</div>
</div>
);
}
function SortButton(props: {
label: string;
onClick: () => void;
isSelected: boolean;
icon: React.FC<{ className?: string }>;
}) {
return (
<Button
variant="outline"
onClick={props.onClick}
className={cn(
"rounded-xl gap-2 bg-card",
props.isSelected && "bg-accent border-active-border",
)}
>
<props.icon className="size-3.5" />
{props.label}
</Button>
);
}
@@ -0,0 +1,195 @@
"use client";
import { ArrowLeftIcon, ArrowRightIcon } from "lucide-react";
import Link from "next/link";
import { type Bridge, NATIVE_TOKEN_ADDRESS } from "thirdweb";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { Button } from "@/components/ui/button";
import { Skeleton } from "@/components/ui/skeleton";
import {
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
export function TokensTable(props: {
tokens: Bridge.TokenWithPrices[];
pageSize: number;
isFetching: boolean;
pagination: {
onNext: () => void;
onPrevious: () => void;
nextDisabled: boolean;
previousDisabled: boolean;
};
}) {
const { tokens, isFetching } = props;
return (
<div className="border rounded-xl overflow-hidden">
<TableContainer className="border-0">
<Table>
<TableHeader>
<TableRow>
<TableHead>Token</TableHead>
<TableHead>Price</TableHead>
<TableHead>Market cap</TableHead>
<TableHead>Volume (24h)</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{isFetching
? new Array(props.pageSize).fill(0).map((_, i) => (
// biome-ignore lint/suspicious/noArrayIndexKey: ok
<TableRow key={i}>
<TableCell>
<div className="flex items-center gap-3">
<Skeleton className="size-10 rounded-full" />
<div className="flex flex-col gap-1.5">
<Skeleton className="h-4 w-16" />
<Skeleton className="h-3 w-36" />
</div>
</div>
</TableCell>
<TableCell>
<Skeleton className="ml-auto h-4 w-16" />
</TableCell>
<TableCell>
<Skeleton className="ml-auto h-4 w-20" />
</TableCell>
<TableCell>
<Skeleton className="ml-auto h-4 w-20" />
</TableCell>
</TableRow>
))
: tokens.length > 0
? tokens.map((token) => {
const price = token.prices.USD;
return (
<TableRow
key={`${token.chainId}-${token.address}`}
className="hover:bg-accent/50"
linkBox
>
<TableCell>
<div className="flex items-center gap-3">
<Avatar className="size-10">
<AvatarImage
src={token.iconUri ?? undefined}
alt={token.symbol}
/>
<AvatarFallback>
{token.symbol?.slice(0, 2)?.toUpperCase()}
</AvatarFallback>
</Avatar>
<div className="flex flex-col gap-1.5">
<span className="font-medium leading-none text-base">
{token.symbol}
</span>
<Link
href={
token.address.toLowerCase() ===
NATIVE_TOKEN_ADDRESS.toLowerCase()
? `/${token.chainId}`
: `/${token.chainId}/${token.address}`
}
prefetch={false}
className="before:absolute before:inset-0"
>
<span className="text-sm text-muted-foreground leading-none">
{token.name}
</span>
</Link>
</div>
</div>
</TableCell>
<TableCell>
{price ? formatPrice(price) : "N/A"}
</TableCell>
<TableCell>
{token.marketCapUsd
? formatUsdCompact(token.marketCapUsd)
: "N/A"}
</TableCell>
<TableCell>
{token.volume24hUsd
? formatUsdCompact(token.volume24hUsd)
: "N/A"}
</TableCell>
</TableRow>
);
})
: null}
</TableBody>
</Table>
</TableContainer>
{tokens.length === 0 && !isFetching && (
<div className="flex items-center justify-center py-20 text-muted-foreground w-full text-sm ">
No tokens found
</div>
)}
<div className="flex justify-end p-4 gap-3 border-t bg-card">
<Button
variant="outline"
className="gap-2 rounded-full"
size="sm"
onClick={() => props.pagination.onPrevious()}
disabled={props.pagination.previousDisabled || isFetching}
>
<ArrowLeftIcon className="size-4" />
Previous
</Button>
<Button
variant="outline"
className="gap-2 rounded-full"
size="sm"
onClick={() => props.pagination.onNext()}
disabled={props.pagination.nextDisabled || isFetching}
>
Next
<ArrowRightIcon className="size-4" />
</Button>
</div>
</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 formatUsdCompact(value: number): string {
return compactValueUSDFormatter.format(value);
}
@@ -0,0 +1,61 @@
import { BringToFrontIcon } from "lucide-react";
import type { Metadata } from "next";
import { unstable_cache } from "next/cache";
import Link from "next/link";
import { Bridge } from "thirdweb";
import { serverThirdwebClient } from "@/constants/thirdweb-client.server";
import { PageHeader } from "./components/header";
import { TokenPage } from "./components/token-page";
const title = "Tokens | thirdweb";
const description = "Discover and swap any tokens on any chain, instantly";
export const metadata: Metadata = {
description,
openGraph: {
description,
title,
},
title,
};
export default async function Page() {
const chains = await getBridgeSupportedChains();
return (
<div>
<PageHeader />
<div className="border-b py-10 ">
<div className="container max-w-7xl">
<div className="flex mb-4">
<div className="rounded-full bg-card p-2.5 border">
<BringToFrontIcon className="size-6 text-muted-foreground" />
</div>
</div>
<h1 className="text-4xl font-bold tracking-tighter mb-1">
Discover and swap any tokens on any chain, instantly
</h1>
<Link
className="text-muted-foreground hover:text-foreground"
href="https://thirdweb.com/monetize/bridge"
target="_blank"
>
Powered by thirdweb bridge
</Link>
</div>
</div>
<TokenPage chains={chains} />
</div>
);
}
const getBridgeSupportedChains = unstable_cache(
async () => {
const chains = await Bridge.chains({ client: serverThirdwebClient });
return chains;
},
["bridge-supported-chains"],
{
revalidate: 60 * 60, // 1 hour
},
);
@@ -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";
@@ -19,8 +18,8 @@ export const size = {
export const contentType = "image/png";
const TWLogo: React.FC = () => {
const cipId = useId();
const linearGradientId = useId();
const cipId = "og-tw-logo-clip-path";
const linearGradientId = "og-tw-logo-linear-gradient";
return (
// biome-ignore lint/a11y/noSvgWithoutTitle: not needed
@@ -103,15 +103,12 @@ export default async function TeamLayout(props: {
>
{props.children}
</TeamSidebarLayout>
<div className="fixed right-4 bottom-4 z-50">
<CustomChatButton
authToken={authToken}
clientId={undefined}
examplePrompts={siwaExamplePrompts}
label="Get Help"
team={team}
/>
</div>
<CustomChatButton
authToken={authToken}
clientId={undefined}
examplePrompts={siwaExamplePrompts}
team={team}
/>
</div>
</SidebarProvider>
);
@@ -1,6 +1,6 @@
"use client";
import { ArrowUpRightIcon } from "lucide-react";
import { ArrowUpRightIcon, InfoIcon } from "lucide-react";
import type { ThirdwebClient } from "thirdweb";
import { Button } from "@/components/ui/button";
import { NebulaIcon } from "@/icons/NebulaIcon";
@@ -24,6 +24,7 @@ export function EmptyStateChatPageContent(props: {
}) {
return (
<div className="overflow-hidden py-10 lg:py-16">
<AIUsageBanner />
{props.showAurora && (
<Aurora className="top-0 left-1/2 h-[800px] w-[1000px] text-[hsl(var(--primary-foreground)/8%)] lg:w-[150%] dark:text-[hsl(var(--primary-foreground)/10%)]" />
)}
@@ -165,3 +166,35 @@ const Aurora: React.FC<AuroraProps> = ({ className }) => {
/>
);
};
function AIUsageBanner() {
return (
<div className="absolute top-0 left-0 right-0 z-10 flex justify-center px-4 pt-4">
<div className="max-w-4xl">
<div className="relative overflow-hidden rounded-lg border border-blue-200 bg-gradient-to-r from-blue-50 to-blue-50/50 p-3 shadow-sm dark:border-blue-800 dark:from-blue-950/50 dark:to-blue-950/20">
<div className="relative flex items-center gap-3 px-2">
<div className="mt-0.5 flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-blue-100 dark:bg-blue-900">
<InfoIcon className="size-3.5 text-blue-600 dark:text-blue-400" />
</div>
<div className="flex-1 text-sm">
<p className="text-blue-800 dark:text-blue-200">
thirdweb AI usage is billed based on number of tokens used. See
the{" "}
<a
href="https://thirdweb.com/pricing"
target="_blank"
rel="noopener noreferrer"
className="font-medium text-blue-700 underline decoration-blue-300 underline-offset-2 transition-colors hover:text-blue-800 hover:decoration-blue-500 dark:text-blue-300 dark:decoration-blue-600 dark:hover:text-blue-200 dark:hover:decoration-blue-400"
>
pricing page
</a>
.
</p>
</div>
</div>
</div>
</div>
</div>
);
}
@@ -1,3 +1,4 @@
import { Input } from "@workspace/ui/components/input";
import { format, formatDistanceToNowStrict } from "date-fns";
import {
ArrowLeftIcon,
@@ -110,6 +111,7 @@ export function TransactionsTable(props: {
const [autoUpdate, setAutoUpdate] = useState(true);
const [page, setPage] = useState(1);
const [status, setStatus] = useState<EngineStatus | undefined>(undefined);
const [filterId, setFilterId] = useState<string | undefined>(undefined);
const autoUpdateId = useId();
const pageSize = 10;
const transactionsQuery = useEngineTransactions({
@@ -121,6 +123,7 @@ export function TransactionsTable(props: {
page: page,
status,
},
id: filterId,
});
const transactions = transactionsQuery.data?.transactions ?? [];
@@ -160,6 +163,16 @@ export function TransactionsTable(props: {
onCheckedChange={(v) => setAutoUpdate(!!v)}
/>
</div>
<Input
className="max-w-[250px]"
onChange={(e) => {
const value = e.target.value.trim();
setFilterId(value || undefined);
setPage(1);
}}
placeholder="Filter by Queue ID"
value={filterId || ""}
/>
<StatusSelector
setStatus={(v) => {
setStatus(v);
@@ -77,15 +77,12 @@ export default async function ProjectLayout(props: {
{props.children}
</ProjectSidebarLayout>
</div>
<div className="fixed right-4 bottom-4 z-50">
<CustomChatButton
authToken={authToken}
clientId={project.publishableKey}
examplePrompts={siwaExamplePrompts}
label="Ask AI Assistant"
team={team}
/>
</div>
<CustomChatButton
authToken={authToken}
clientId={project.publishableKey}
examplePrompts={siwaExamplePrompts}
team={team}
/>
<SaveLastUsedProject projectId={project.id} teamId={team.id} />
</SidebarProvider>
);
@@ -74,6 +74,10 @@ export type CreateNFTCollectionFunctions = {
count: number;
};
gasless: boolean;
onNotEnoughFunds: (data: {
requiredAmount: string;
balance: string;
}) => void;
}) => Promise<void>;
lazyMintNFTs: (params: {
values: CreateNFTCollectionAllValues;
@@ -1,6 +1,14 @@
"use client";
import { useRef } from "react";
import { encode, getContract, type ThirdwebClient } from "thirdweb";
import {
encode,
estimateGasCost,
getContract,
getGasPrice,
type PreparedTransaction,
type ThirdwebClient,
toEther,
} from "thirdweb";
import { deployERC721Contract, deployERC1155Contract } from "thirdweb/deploys";
import { multicall } from "thirdweb/extensions/common";
import {
@@ -14,7 +22,8 @@ import {
} from "thirdweb/extensions/erc1155";
import { grantRole } from "thirdweb/extensions/permissions";
import { useActiveAccount } from "thirdweb/react";
import { maxUint256 } from "thirdweb/utils";
import { maxUint256, resolvePromisedValue } from "thirdweb/utils";
import { getWalletBalance } from "thirdweb/wallets";
import { create7702MinimalAccount } from "thirdweb/wallets/smart";
import { revalidatePathAction } from "@/actions/revalidate";
import { reportContractDeployed } from "@/analytics/report";
@@ -39,6 +48,10 @@ export function CreateNFTPage(props: {
const contractAddressRef = useRef<string | undefined>(undefined);
const getChain = useGetV5DashboardChain();
const sendAndConfirmTx = useSendAndConfirmTx();
const sendAndConfirmTxNoPayModal = useSendAndConfirmTx({
payModal: false,
});
function getAccount(params: { gasless: boolean }) {
if (!activeAccount) {
throw new Error("Wallet is not connected");
@@ -218,6 +231,10 @@ export function CreateNFTPage(props: {
count: number;
};
gasless: boolean;
onNotEnoughFunds: (data: {
requiredAmount: string;
balance: string;
}) => void;
}) {
const { values, batch } = params;
const contract = getDeployedContract({
@@ -277,7 +294,46 @@ export function CreateNFTPage(props: {
data: encodedTransactions,
});
await sendAndConfirmTx.mutateAsync(tx);
// if there are more than one batches, on the first batch, we check if the user has enough funds to cover the cost of all the batches ->
// calculate the cost of one batch, multiply by the number of batches to get the total cost
// if the user does not have enough funds, call the onNotEnoughFunds callback
const totalBatches = Math.ceil(values.nfts.length / batch.count);
if (batch.startIndex === 0 && totalBatches > 1 && !params.gasless) {
if (!activeAccount) {
throw new Error("Wallet is not connected");
}
const costPerBatch = await getTotalTransactionCost({
tx: tx,
from: activeAccount.address,
});
const totalCost = costPerBatch * BigInt(totalBatches);
const walletBalance = await getWalletBalance({
address: activeAccount.address,
chain: contract.chain,
client: contract.client,
});
if (walletBalance.value < totalCost) {
params.onNotEnoughFunds({
balance: toEther(walletBalance.value),
requiredAmount: toEther(totalCost),
});
throw new Error(
`Not enough funds: Required ${toEther(totalCost)}, Balance ${toEther(walletBalance.value)}`,
);
}
await sendAndConfirmTxNoPayModal.mutateAsync(tx);
} else if (totalBatches > 1) {
await sendAndConfirmTxNoPayModal.mutateAsync(tx);
} else {
await sendAndConfirmTx.mutateAsync(tx);
}
}
async function handleSetAdmins(params: {
@@ -390,3 +446,41 @@ function transformSocialUrls(
{} as Record<string, string>,
);
}
async function getTransactionGasCost(tx: PreparedTransaction, from?: string) {
try {
const gasCost = await estimateGasCost({
from,
transaction: tx,
});
const bufferCost = gasCost.wei / 10n;
// Note: get tx.value AFTER estimateGasCost
// add 10% extra gas cost to the estimate to ensure user buys enough to cover the tx cost
return gasCost.wei + bufferCost;
} catch {
if (from) {
// try again without passing from
return await getTransactionGasCost(tx);
}
// fallback if both fail, use the tx value + 1M * gas price
const gasPrice = await getGasPrice({
chain: tx.chain,
client: tx.client,
});
return 1_000_000n * gasPrice;
}
}
async function getTotalTransactionCost(params: {
tx: PreparedTransaction;
from?: string;
}) {
const [txValue, txGasCost] = await Promise.all([
resolvePromisedValue(params.tx.value),
getTransactionGasCost(params.tx, params.from),
]);
return (txValue || 0n) + txGasCost;
}
@@ -1,14 +1,19 @@
"use client";
import {
ArrowLeftIcon,
ArrowRightIcon,
ArrowUpFromLineIcon,
ImageOffIcon,
RefreshCcwIcon,
ShoppingBagIcon,
} from "lucide-react";
import Link from "next/link";
import { useTheme } from "next-themes";
import { useMemo, useRef, useState } from "react";
import type { ThirdwebClient } from "thirdweb";
import {
BuyWidget,
TokenProvider,
TokenSymbol,
useActiveAccount,
@@ -37,6 +42,7 @@ import {
import { Skeleton } from "@/components/ui/skeleton";
import { useV5DashboardChain } from "@/hooks/chains/v5-adapter";
import { parseError } from "@/utils/errorParser";
import { getSDKTheme } from "@/utils/sdk-component-theme";
import { ChainOverview } from "../../_common/chain-overview";
import { StepCard } from "../../_common/step-card";
import { StorageErrorPlanUpsell } from "../../_common/storage-error-upsell";
@@ -71,6 +77,10 @@ export function LaunchNFT(props: {
const walletRequiresApproval = activeWallet?.id !== "inApp";
const account = useActiveAccount();
const contractAddressRef = useRef<string | null>(null);
const { theme } = useTheme();
const chainMeta = useV5DashboardChain(
Number(formValues.collectionInfo.chain),
);
function updateStatus(
index: number,
@@ -161,6 +171,18 @@ export function LaunchNFT(props: {
const [isGasless, setIsGasless] = useState(canEnableGasless);
const showGaslessSection = false; // activeWallet?.id === "inApp";
const [notEnoughFunds, setNotEnoughFunds] = useState<
| false
| {
values: {
requiredAmount: string;
balance: string;
};
isBuySuccess: boolean;
showBuyWidget: boolean;
}
>(false);
const contractLink = contractAddressRef.current
? `/team/${props.teamSlug}/${props.projectSlug}/contract/${formValues.collectionInfo.chain}/${contractAddressRef.current}`
: null;
@@ -204,6 +226,16 @@ export function LaunchNFT(props: {
},
gasless: isGasless,
values: formValues,
onNotEnoughFunds: (data) => {
setNotEnoughFunds({
values: {
balance: data.balance,
requiredAmount: data.requiredAmount,
},
isBuySuccess: false,
showBuyWidget: false,
});
},
});
batchesProcessedRef.current += 1;
@@ -216,6 +248,16 @@ export function LaunchNFT(props: {
},
gasless: isGasless,
values: formValues,
onNotEnoughFunds: (data) => {
setNotEnoughFunds({
values: {
balance: data.balance,
requiredAmount: data.requiredAmount,
},
isBuySuccess: false,
showBuyWidget: false,
});
},
});
}
}
@@ -357,41 +399,143 @@ export function LaunchNFT(props: {
className="gap-0 overflow-hidden p-0 md:max-w-[480px]"
dialogCloseClassName="hidden"
>
<div className="flex flex-col gap-6 p-6">
<DialogHeader className="space-y-0.5">
<DialogTitle className="font-semibold text-xl tracking-tight">
Status
</DialogTitle>
{walletRequiresApproval && (
<DialogDescription>
Each step will prompt a signature request in your wallet
</DialogDescription>
)}
</DialogHeader>
{/** biome-ignore lint/complexity/useOptionalChain: can't change it to optional chain */}
{notEnoughFunds && notEnoughFunds.showBuyWidget ? (
<div className="bg-card">
<div className="border-b py-2 px-2">
<Button
variant="ghost"
size="sm"
onClick={() =>
setNotEnoughFunds({
...notEnoughFunds,
showBuyWidget: false,
})
}
className="gap-2 px-2 text-muted-foreground hover:text-foreground"
>
<ArrowLeftIcon className="size-4" />
Back
</Button>
</div>
<BuyWidget
client={props.client}
chain={chain}
amount={String(
Number(notEnoughFunds.values.requiredAmount) -
Number(notEnoughFunds.values.balance),
)}
buttonLabel={`Buy ${chainMeta?.nativeCurrency?.symbol || "ETH"}`}
theme={getSDKTheme(theme === "dark" ? "dark" : "light")}
className="!border-none !w-full !rounded-none"
onSuccess={() => {
setNotEnoughFunds({
...notEnoughFunds,
isBuySuccess: true,
showBuyWidget: false,
});
}}
/>
</div>
) : (
<div className="flex flex-col gap-6 p-6 mb-2">
<DialogHeader className="space-y-0.5">
<DialogTitle className="font-semibold text-xl tracking-tight">
Status
</DialogTitle>
{walletRequiresApproval && (
<DialogDescription>
Each step will prompt a signature request in your wallet
</DialogDescription>
)}
</DialogHeader>
<MultiStepStatus
onRetry={handleRetry}
renderError={(step, errorMessage) => {
if (
props.teamPlan === "free" &&
errorMessage.toLowerCase().includes("storage limit")
) {
return (
<StorageErrorPlanUpsell
onRetry={() => handleRetry(step)}
teamSlug={props.teamSlug}
trackingCampaign="create-nft"
/>
);
}
<MultiStepStatus
onRetry={handleRetry}
renderError={(step, errorMessage) => {
if (
props.teamPlan === "free" &&
errorMessage.toLowerCase().includes("storage limit")
) {
return (
<StorageErrorPlanUpsell
onRetry={() => handleRetry(step)}
teamSlug={props.teamSlug}
trackingCampaign="create-nft"
/>
);
}
return null;
}}
steps={steps}
/>
</div>
if (notEnoughFunds) {
return (
<div>
{notEnoughFunds.isBuySuccess ? (
<p className="text-success-text text-sm mt-1.5 mb-2">
Funds bought successfully
</p>
) : (
<div className="mt-2 mb-2 space-y-1">
<p className="text-destructive-text text-sm">
Not enough funds
</p>
<p className="text-muted-foreground text-sm">
Required Funds:{" "}
{notEnoughFunds.values.requiredAmount}{" "}
{chainMeta?.nativeCurrency?.symbol || "ETH"}
</p>
<p className="text-muted-foreground text-sm">
Balance: {notEnoughFunds.values.balance}{" "}
{chainMeta?.nativeCurrency?.symbol || "ETH"}
</p>
</div>
)}
<div className="mt-2 flex justify-end gap-4 border-border border-t bg-card p-6">
<div className="flex gap-2">
<Button
onClick={() => {
handleRetry(step);
setNotEnoughFunds(false);
}}
size="sm"
variant={
notEnoughFunds.isBuySuccess
? "default"
: "outline"
}
className="gap-2"
>
<RefreshCcwIcon className="size-3.5" />
Retry
</Button>
{!notEnoughFunds.isBuySuccess && (
<Button
onClick={() => {
setNotEnoughFunds({
...notEnoughFunds,
showBuyWidget: true,
});
}}
size="sm"
className="gap-2"
>
<ShoppingBagIcon className="size-3.5" />
Buy Funds
</Button>
)}
</div>
</div>
);
}
return null;
}}
steps={steps}
/>
</div>
)}
<div className="flex justify-end gap-4 border-border border-t bg-card p-6">
{!isComplete ? (
<Button
onClick={() => {
@@ -13,6 +13,7 @@ import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { CopyAddressButton } from "@/components/ui/CopyAddressButton";
import { CopyTextButton } from "@/components/ui/CopyTextButton";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import {
Select,
@@ -49,6 +50,8 @@ export function TransactionsTableUI(props: {
getData: (params: {
page: number;
status: TransactionStatus | undefined;
id: string | undefined;
from: string | undefined;
}) => Promise<TransactionsResponse>;
project: Project;
teamSlug: string;
@@ -60,13 +63,15 @@ export function TransactionsTableUI(props: {
const [status, setStatus] = useState<TransactionStatus | undefined>(
undefined,
);
const [id, setId] = useState<string | undefined>(undefined);
const [from, setFrom] = useState<string | undefined>(undefined);
const [page, setPage] = useState(1);
const pageSize = 10;
const transactionsQuery = useQuery({
placeholderData: keepPreviousData,
queryFn: () => props.getData({ page, status }),
queryKey: ["transactions", props.project.id, page, status],
queryFn: () => props.getData({ page, status, id, from }),
queryKey: ["transactions", props.project.id, page, status, id, from],
refetchInterval: autoUpdate ? 4_000 : false,
});
@@ -84,18 +89,18 @@ export function TransactionsTableUI(props: {
return (
<div className="overflow-hidden rounded-lg border border-border bg-card">
<div className="flex flex-col gap-4 rounded-lg rounded-b-none px-6 py-6 lg:flex-row lg:justify-between">
<div>
<h2 className="font-semibold text-2xl tracking-tight">
Transaction History
</h2>
<p className="text-muted-foreground text-sm">
Transactions sent from server wallets
</p>
</div>
<div className="flex flex-col gap-4 rounded-lg rounded-b-none px-6 py-6">
<div className="flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between">
<div>
<h2 className="font-semibold text-2xl tracking-tight">
Transaction History
</h2>
<p className="text-muted-foreground text-sm">
Transactions sent from server wallets
</p>
</div>
<div className="flex items-center justify-end gap-5 border-border border-t pt-4 lg:border-none lg:pt-0">
<div className="flex shrink-0 items-center gap-2">
<div className="flex shrink-0 items-center gap-2 border-border border-t pt-4 lg:border-none lg:pt-0">
<Label htmlFor={autoUpdateId}>Auto Update</Label>
<Switch
checked={autoUpdate}
@@ -103,14 +108,39 @@ export function TransactionsTableUI(props: {
onCheckedChange={(v) => setAutoUpdate(!!v)}
/>
</div>
<StatusSelector
setStatus={(v) => {
setStatus(v);
// reset page
</div>
<div className="flex flex-wrap items-center justify-end gap-3">
<Input
className="max-w-[250px]"
onChange={(e) => {
const value = e.target.value.trim();
setId(value || undefined);
setPage(1);
}}
status={status}
placeholder="Filter by Queue ID"
value={id || ""}
/>
<Input
className="max-w-[250px]"
onChange={(e) => {
const value = e.target.value.trim();
setFrom(value || undefined);
setPage(1);
}}
placeholder="Filter by wallet address"
value={from || ""}
/>
<div>
<StatusSelector
setStatus={(v) => {
setStatus(v);
// reset page
setPage(1);
}}
status={status}
/>
</div>
</div>
</div>
@@ -16,11 +16,13 @@ export function TransactionsTable(props: {
return (
<TransactionsTableUI
client={props.client}
getData={async ({ page, status }) => {
getData={async ({ page, status, id, from }) => {
return await getTransactions({
page,
project: props.project,
status,
id,
from,
});
}}
project={props.project}
@@ -34,10 +36,14 @@ async function getTransactions({
project,
page,
status,
id,
from,
}: {
project: Project;
page: number;
status: TransactionStatus | undefined;
id: string | undefined;
from: string | undefined;
}) {
const transactions = await engineCloudProxy<{ result: TransactionsResponse }>(
{
@@ -52,6 +58,8 @@ async function getTransactions({
limit: "20",
page: page.toString(),
status: status ?? undefined,
id: id ?? undefined,
from: from ?? undefined,
},
},
);
@@ -215,7 +215,7 @@ export function ServerWalletsTableUI({
<PaginationContent>
<PaginationItem>
<Link
href={`/team/${teamSlug}/${project.slug}/transactions/server-wallets?page=${
href={`/team/${teamSlug}/${project.slug}/transactions?page=${
currentPage > 1 ? currentPage - 1 : 1
}`}
legacyBehavior
@@ -232,7 +232,7 @@ export function ServerWalletsTableUI({
(pageNumber) => (
<PaginationItem key={`page-${pageNumber}`}>
<Link
href={`/team/${teamSlug}/${project.slug}/transactions/server-wallets?page=${pageNumber}`}
href={`/team/${teamSlug}/${project.slug}/transactions?page=${pageNumber}`}
passHref
>
<PaginationLink isActive={currentPage === pageNumber}>
@@ -244,7 +244,7 @@ export function ServerWalletsTableUI({
)}
<PaginationItem>
<Link
href={`/team/${teamSlug}/${project.slug}/transactions/server-wallets?page=${
href={`/team/${teamSlug}/${project.slug}/transactions?page=${
currentPage < totalPages ? currentPage + 1 : totalPages
}`}
passHref
@@ -30,11 +30,13 @@ function unclaimedFeesStub(token0Amount: bigint, token1Amount: bigint) {
address: "0x1234567890123456789012345678901234567890",
amount: token0Amount,
symbol: "FOO",
decimals: 18,
},
token1: {
address: "0x0987654321098765432109876543210987654321",
amount: token1Amount,
symbol: "BAR",
decimals: 18,
},
};
}
@@ -29,11 +29,13 @@ export function ClaimRewardsPage(props: {
address: string;
amount: bigint;
symbol: string;
decimals: number;
};
token1: {
address: string;
amount: bigint;
symbol: string;
decimals: number;
};
};
chainSlug: string;
@@ -98,11 +100,13 @@ export function ClaimRewardsPageUI(props: {
address: string;
amount: bigint;
symbol: string;
decimals: number;
};
token1: {
address: string;
amount: bigint;
symbol: string;
decimals: number;
};
};
recipient: string;
@@ -275,6 +279,7 @@ function TokenReward(props: {
address: string;
amount: bigint;
symbol: string;
decimals: number;
};
client: ThirdwebClient;
chain: Chain;
@@ -303,7 +308,8 @@ function TokenReward(props: {
</div>
<div className="space-y-0.5">
<p className="font-bold text-sm">
{toTokens(props.token.amount, 18)} {props.token.symbol}
{toTokens(props.token.amount, props.token.decimals)}{" "}
{props.token.symbol}
</p>
<Link
target="_blank"
@@ -31,6 +31,7 @@ export default async function Page(props: {
const chain = info.clientContract.chain;
const assetContractServer = info.serverContract;
const serverClient = assetContractServer.client;
const chainMetadata = info.chainMetadata;
const { address: entrypointContractAddress } =
await getDeployedEntrypointERC20({
@@ -56,6 +57,7 @@ export default async function Page(props: {
// Note: must use server contract/client here
const unclaimedFees = await getUnclaimedFees({
chainMetadata,
positionManager: getContract({
address: reward.positionManager,
chain,
@@ -1,11 +1,14 @@
import type { ThirdwebContract } from "thirdweb";
import { getContract, readContract } from "thirdweb";
import { getAddress, getContract, readContract, ZERO_ADDRESS } from "thirdweb";
import type { ChainMetadata } from "thirdweb/chains";
import { symbol } from "thirdweb/extensions/common";
import { decimals } from "thirdweb/extensions/erc20";
const maxUint128 = 2n ** 128n - 1n;
export async function getUnclaimedFees(params: {
positionManager: ThirdwebContract;
chainMetadata: ChainMetadata;
reward: {
tokenId: bigint;
recipient: string;
@@ -56,33 +59,60 @@ export async function getUnclaimedFees(params: {
const token0Address = positionsResult[2];
const token1Address = positionsResult[3];
const [token0Symbol, token1Symbol] = await Promise.all([
symbol({
contract: getContract({
address: token0Address,
chain,
client,
}),
}),
symbol({
contract: getContract({
address: token1Address,
chain,
client,
}),
}),
]);
const token0Contract = getContract({
address: token0Address,
chain,
client,
});
const token1Contract = getContract({
address: token1Address,
chain,
client,
});
const isToken0Native = getAddress(token0Address) === getAddress(ZERO_ADDRESS);
const isToken1Native = getAddress(token1Address) === getAddress(ZERO_ADDRESS);
const nativeSymbol = params.chainMetadata.nativeCurrency.symbol;
const nativeDecimals = params.chainMetadata.nativeCurrency.decimals;
const [token0Symbol, token1Symbol, token0Decimals, token1Decimals] =
await Promise.all([
isToken0Native
? nativeSymbol
: symbol({
contract: token0Contract,
}),
isToken1Native
? nativeSymbol
: symbol({
contract: token1Contract,
}),
isToken0Native
? nativeDecimals
: decimals({
contract: token0Contract,
}),
isToken1Native
? nativeDecimals
: decimals({
contract: token1Contract,
}),
]);
return {
token0: {
address: token0Address,
amount: collectResult[0],
symbol: token0Symbol,
decimals: token0Decimals,
},
token1: {
address: token1Address,
amount: collectResult[1],
symbol: token1Symbol,
decimals: token1Decimals,
},
};
}
@@ -0,0 +1,36 @@
"use client";
import { useTheme } from "next-themes";
import { DarkVeil } from "@/components/DarkVeil";
const bgBackground: Record<string, [number, number, number]> = {
dark: [0, 0, 0],
light: [250 / 255, 250 / 255, 250 / 255],
};
const hues = {
light: 65,
dark: -85,
};
const patternLightnesss = {
light: -1,
dark: 0.8,
};
export function BridgeVeil() {
const theme = useTheme();
const isLight = theme.theme === "light";
const patternLightness = isLight
? patternLightnesss.light
: patternLightnesss.dark;
const hueShift = isLight ? hues.light : hues.dark;
return (
<DarkVeil
backgroundColor={bgBackground[isLight ? "light" : "dark"]}
patternLightness={patternLightness}
hueShift={hueShift}
warpAmount={0.5}
/>
);
}
@@ -3,7 +3,6 @@
import type { TokenInfo } from "thirdweb/react";
import { BuyAndSwapEmbed } from "@/components/blocks/BuyAndSwapEmbed";
import { useV5DashboardChain } from "@/hooks/chains/v5-adapter";
import { bridgeAppThirdwebClient } from "../../constants";
export function UniversalBridgeEmbed({
chainId,
@@ -17,14 +16,11 @@ export function UniversalBridgeEmbed({
const chain = useV5DashboardChain(chainId || 1);
return (
<div className="lg:w-[400px]">
<BuyAndSwapEmbed
client={bridgeAppThirdwebClient}
chain={chain}
buyAmount={amount}
tokenAddress={token?.address}
pageType="bridge"
/>
</div>
<BuyAndSwapEmbed
chain={chain}
buyAmount={amount}
tokenAddress={token?.address}
pageType="bridge"
/>
);
}
@@ -0,0 +1,33 @@
"use client";
import { ChevronRightIcon, DollarSignIcon, TrendingUpIcon } from "lucide-react";
import Link from "next/link";
import { reportBridgePageLinkClick } from "@/analytics/report";
import { cn } from "@/lib/utils";
export function PillLink(props: {
children: React.ReactNode;
href: string;
linkType: "integrate-bridge" | "trending-tokens";
}) {
const Icon =
props.linkType === "integrate-bridge" ? DollarSignIcon : TrendingUpIcon;
return (
<Link
href={props.href}
target="_blank"
onClick={() => reportBridgePageLinkClick({ linkType: props.linkType })}
className={cn(
"shadow-sm text-center justify-center inline-flex items-center gap-4 text-foreground group",
"text-sm bg-card/50 backdrop-blur-lg border border-border/70",
"rounded-full px-4 py-2.5 transition-colors duration-300 text-pretty leading-5",
"hover:bg-pink-300/20 dark:hover:bg-pink-950/30 hover:text-foreground hover:border-pink-400 dark:hover:border-pink-800",
)}
>
<div className="flex items-center gap-2.5">
<Icon className="size-4 shrink-0 text-pink-600" />
{props.children}
</div>
<ChevronRightIcon className="size-4 shrink-0 text-muted-foreground/50 group-hover:translate-x-1 transition-all duration-300 group-hover:text-foreground" />
</Link>
);
}
@@ -0,0 +1,39 @@
import Link from "next/link";
import { ToggleThemeButton } from "@/components/blocks/color-mode-toggle";
import { cn } from "@/lib/utils";
import { PublicPageConnectButton } from "../../(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/public-pages/_components/PublicPageConnectButton";
import { ThirdwebMiniLogo } from "../../(app)/components/ThirdwebMiniLogo";
export function PageHeader(props: { containerClassName?: string }) {
return (
<div className="border-b border-border/50 border-dashed">
<header
className={cn(
"container flex max-w-7xl justify-between py-4",
props.containerClassName,
)}
>
<div className="flex items-center gap-4">
<Link className="flex items-center gap-2" href="/team">
<ThirdwebMiniLogo className="h-5" />
<span className="hidden font-bold text-2xl tracking-tight lg:block">
thirdweb
</span>
</Link>
</div>
<div className="flex items-center gap-5">
<Link
href="https://portal.thirdweb.com/bridge"
target="_blank"
className="text-sm text-muted-foreground hover:text-foreground"
>
Docs
</Link>
<ToggleThemeButton className="bg-transparent" />
<PublicPageConnectButton connectButtonClassName="!rounded-full" />
</div>
</header>
</div>
);
}
+1 -1
View File
@@ -18,7 +18,7 @@ export default function BridgeLayout({
<html lang="en" suppressHydrationWarning>
<body
className={cn(
"h-screen w-screen bg-background font-sans antialiased",
"min-h-dvh bg-background font-sans antialiased flex flex-col",
fontSans.variable,
)}
>
Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

+134 -48
View File
@@ -1,15 +1,20 @@
import { ArrowUpRightIcon } from "lucide-react";
import { cn } from "@workspace/ui/lib/utils";
import type { Metadata } from "next";
import type { Address } from "thirdweb";
import { defineChain } from "thirdweb/chains";
import { getContract } from "thirdweb/contract";
import { getCurrencyMetadata } from "thirdweb/extensions/erc20";
import { FaqSection } from "@/components/blocks/faq-section";
import { AppFooter } from "@/components/footers/app-footer";
import { BridgeVeil } from "./components/BridgeVeil";
import { PillLink } from "./components/client/pill-link";
import { UniversalBridgeEmbed } from "./components/client/UniversalBridgeEmbed";
import { PageHeader } from "./components/header";
import { bridgeAppThirdwebClient } from "./constants";
const title = "thirdweb Payments: Swap, Bridge, and Onramp";
const title = "thirdweb Bridge: Buy, Bridge & Swap Crypto on 85+ Chains";
const description =
"Swap, bridge, and on-ramp to any EVM chain with thirdweb's Payments.";
"Bridge, swap, and on-ramp across 9M+ token routes on 85+ chains (Ethereum, Base, Optimism, Arbitrum, BNB & more). Best-price routing and near-instant fast finality";
export const metadata: Metadata = {
description,
@@ -49,57 +54,138 @@ export default async function BridgePage({
}
return (
<div className="relative mx-auto flex h-dvh w-full flex-col items-center justify-center overflow-hidden border py-10">
<main className="container z-10 flex justify-center">
<UniversalBridgeEmbed
amount={amount as string}
chainId={chainId ? Number(chainId) : undefined}
token={
symbol && decimals && tokenName
? {
address: tokenAddress as Address,
name: tokenName,
symbol,
}
: undefined
}
/>
</main>
<div className="grow flex flex-col relative overflow-hidden">
<div className="absolute top-0 left-0 right-0 h-dvh md:h-[1200px] fade-in-0 animate-in duration-700">
<BridgeVeil />
</div>
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
alt=""
className="-bottom-12 -right-12 pointer-events-none absolute lg:right-0 lg:bottom-0 opacity-20"
src="/assets/login/background.svg"
/>
<div className="relative z-10">
<PageHeader />
<div className="absolute inset-x-0 bottom-8 z-20">
<div className="container mx-auto px-4">
<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-semibold text-base tracking-tight">
Get Started with thirdweb Bridge
</h3>
<p className="text-muted-foreground text-sm">
thirdweb Bridge allows developers to easily bridge and swap
tokens between chains and wallets.
</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/bridge"
rel="noopener noreferrer"
target="_blank"
>
Learn More
<ArrowUpRightIcon className="size-4" />
</a>
<div className="h-20" />
<div className="relative z-10">
<div className="container">
<h1 className="text-5xl md:text-6xl font-bold mb-6 tracking-tighter text-center leading-none text-pretty">
Bridge and Swap tokens <br className="max-sm:hidden" /> across any
chain, instantly
</h1>
<div className="flex-wrap flex gap-3 items-center justify-center">
<DataPill>85+ Chains Supported</DataPill>
<DataPill>3200+ Tokens Supported</DataPill>
<DataPill>9+ Million Routes Available</DataPill>
</div>
</div>
<div className="h-16" />
<div className="flex grow items-center justify-center px-4 relative">
<DotsBackgroundPattern />
<UniversalBridgeEmbed
amount={amount as string}
chainId={chainId ? Number(chainId) : undefined}
token={
symbol && decimals && tokenName
? {
address: tokenAddress as Address,
name: tokenName,
symbol,
}
: undefined
}
/>
</div>
</div>
<div className="h-32" />
<div className="flex flex-col gap-4 items-center container">
<PillLink
href="https://portal.thirdweb.com/bridge"
linkType="integrate-bridge"
>
Integrate Bridge in your apps in minutes, and start generating
revenue
</PillLink>
<PillLink
href="https://thirdweb.com/tokens"
linkType="trending-tokens"
>
Discover Trending Tokens
</PillLink>
</div>
<div className="h-32" />
<div className="container max-w-2xl">
<BridgeFaqSection />
</div>
<div className="h-32" />
<div className="relative">
<AppFooter />
</div>
</div>
</div>
);
}
function DataPill(props: { children: React.ReactNode }) {
return (
<p className="flex items-center gap-1.5 text-foreground/50 text-xs bg-accent/30 backdrop-blur-lg border border-border/70 rounded-full px-3 py-1.5 hover:text-foreground transition-colors duration-300">
{props.children}
</p>
);
}
function DotsBackgroundPattern(props: { className?: string }) {
return (
<div
className={cn(
"pointer-events-none absolute -inset-x-36 -inset-y-24 text-pink-700/30 dark:text-pink-500/20",
props.className,
)}
style={{
backgroundImage: "radial-gradient(currentColor 1px, transparent 1px)",
backgroundSize: "24px 24px",
maskImage:
"radial-gradient(ellipse 100% 100% at 50% 50%, black 30%, transparent 50%)",
}}
/>
);
}
const bridgeFaqs: Array<{ title: string; description: string }> = [
{
title: "What is bridging in crypto?",
description:
"Crypto bridging (cross-chain bridging) moves tokens between blockchains so you can use assets across networks. In thirdweb Bridge, connect your wallet, choose the source token/network and destination token/network, review the route and price, then confirm. Assets arrive after finality, often under ~10 seconds on fast routes, though timing depends on networks and congestion.",
},
{
title: "How does crypto bridging work?",
description:
"Bridge smart contracts lock or burn tokens on the source chain and mint or release equivalents on the destination via verified cross-chain providers. thirdweb Bridge automatically finds the fastest, lowest-cost route and may use different mechanisms based on networks and liquidity. Arrival can range from seconds to minutes depending on finality; many routes complete in ~10 seconds",
},
{
title: "What is a crypto asset swap?",
description:
"A crypto swap exchanges one token for another via a DEX or aggregator. thirdweb Bridge lets you bridge + swap in one step. For example, ETH on Ethereum to USDC on Base, by selecting your start and end tokens/networks and confirming.",
},
{
title: "How can I get stablecoins like USDC or USDT?",
description:
"Use thirdweb Bridge to convert assets you hold into USDC or USDT on your chosen network: select your current token/network, pick the stablecoin (USDC, USDT, etc) on the destination, and confirm. You can also buy stablecoins with fiat in the Buy flow and bridge if needed. Always verify official token contract addresses.",
},
{
title: "What is the cost of bridging and swapping?",
description:
"Costs include gas on each chain, bridge/liquidity provider fees, and any DEX swap fees or price impact. thirdweb Bridge compares routes and selects the best price route. Save by using lower-gas times or combining bridge + swap in one flow.",
},
];
function BridgeFaqSection() {
return <FaqSection faqs={bridgeFaqs} />;
}
+9
View File
@@ -64,6 +64,15 @@ const baseNextConfig: NextConfig = {
},
productionBrowserSourceMaps: false,
reactStrictMode: true,
async redirects() {
return [
{
source: "/:path*",
destination: "https://thirdweb.com/ai",
permanent: true,
},
];
},
async rewrites() {
return [
{
@@ -23,7 +23,7 @@ export function EmptyStateChatPageContent(props: {
onLoginClick: undefined | (() => void);
}) {
return (
<div className="overflow-hidden py-10 lg:py-16">
<div className="relative overflow-hidden py-10 lg:py-16">
{props.showAurora && (
<Aurora className="top-0 left-1/2 h-[800px] w-[1000px] text-[hsl(var(--nebula-pink-foreground)/8%)] lg:w-[150%] dark:text-[hsl(var(--nebula-pink-foreground)/10%)]" />
)}
+2 -2
View File
@@ -148,12 +148,12 @@ const nextConfig = {
permanent: false,
},
{
source: "/insight",
source: "/insight/:path*",
destination: "https://insight.thirdweb.com/reference",
permanent: false,
},
{
source: "/payments/backend",
source: "/payments/backend/:path*",
destination: "/reference#tag/payments",
permanent: false,
},
+1 -1
View File
@@ -26,6 +26,7 @@
"clsx": "^2.1.1",
"date-fns": "4.1.0",
"fetch-event-stream": "0.1.5",
"geist": "^1.5.1",
"lucide-react": "0.525.0",
"next": "15.3.5",
"next-themes": "^0.4.6",
@@ -48,7 +49,6 @@
"thirdweb": "workspace:*",
"use-debounce": "^10.0.5",
"use-stick-to-bottom": "^1.1.1",
"x402-next": "^0.6.1",
"zod": "3.25.75"
},
"devDependencies": {
@@ -5,6 +5,6 @@ export const maxDuration = 300;
export async function GET(_req: Request) {
return NextResponse.json({
success: true,
message: "Congratulations! You have accessed the protected route.",
message: "Payment successful. You have accessed the protected route.",
});
}
@@ -59,7 +59,9 @@ export function RightSection(props: { options: SwapWidgetPlaygroundOptions }) {
prefill={props.options.prefill}
currency={props.options.currency}
showThirdwebBranding={props.options.showThirdwebBranding}
key={JSON.stringify(props.options)}
key={JSON.stringify({
prefill: props.options.prefill,
})}
persistTokenSelections={false}
/>
)}
@@ -4,6 +4,7 @@ import {
BotIcon,
BoxIcon,
BringToFrontIcon,
CircleDollarSignIcon,
CircleUserIcon,
CreditCardIcon,
DollarSignIcon,
@@ -186,6 +187,13 @@ export const paymentsFeatureCards: FeatureCardMetadata[] = [
description:
"Enable users to pay for onchain transactions with fiat or crypto",
},
{
icon: CircleDollarSignIcon,
title: "x402",
link: "/payments/x402",
description:
"Use the x402 payment protocol to pay for API calls using any web3 wallet",
},
];
export const accountAbstractionsFeatureCards: FeatureCardMetadata[] = [
+4 -15
View File
@@ -1,26 +1,15 @@
import type { Metadata } from "next";
import { Fira_Code, Inter } from "next/font/google";
import { metadataBase } from "@/lib/constants";
import { cn } from "@/lib/utils";
import { AppSidebarLayout } from "./AppSidebar";
import { Providers } from "./providers";
import "@workspace/ui/global.css";
import { GeistMono } from "geist/font/mono";
import { GeistSans } from "geist/font/sans";
import { ThemeProvider } from "next-themes";
import NextTopLoader from "nextjs-toploader";
import { Toaster } from "sonner";
const sansFont = Inter({
subsets: ["latin"],
variable: "--font-sans",
weight: "variable",
});
const monoFont = Fira_Code({
subsets: ["latin"],
variable: "--font-mono",
weight: "variable",
});
export const metadata: Metadata = {
description: "thirdweb playground",
metadataBase,
@@ -37,8 +26,8 @@ export default async function RootLayout({
<body
className={cn(
"bg-background font-sans antialiased ",
sansFont.variable,
monoFont.variable,
GeistSans.variable,
GeistMono.variable,
)}
>
<ThemeProvider
@@ -0,0 +1,14 @@
import { OverviewPage } from "@/components/blocks/OverviewPage";
import { PayIcon } from "@/icons/PayIcon";
import { paymentsFeatureCards } from "../data/pages-metadata";
export default function Page() {
return (
<OverviewPage
title="Payments"
description="thirdweb Payments allow developers to create advanced payment flows to monetize their apps through product sales, peer to peer payments, token sales, and more"
featureCards={paymentsFeatureCards}
icon={PayIcon}
/>
);
}
@@ -0,0 +1,192 @@
"use client";
import type React from "react";
import { useId, useState } from "react";
import { defineChain } from "thirdweb/chains";
import { BridgeNetworkSelector } from "@/components/blocks/NetworkSelectors";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { TokenSelector } from "@/components/ui/TokenSelector";
import { THIRDWEB_CLIENT } from "@/lib/client";
import type { TokenMetadata } from "@/lib/types";
import type { X402PlaygroundOptions } from "./types";
export function X402LeftSection(props: {
options: X402PlaygroundOptions;
setOptions: React.Dispatch<React.SetStateAction<X402PlaygroundOptions>>;
}) {
const { options, setOptions } = props;
// Local state for chain and token selection
const [selectedChain, setSelectedChain] = useState<number | undefined>(() => {
return options.chain?.id;
});
const [selectedToken, setSelectedToken] = useState<
{ chainId: number; address: string } | undefined
>(() => {
if (options.tokenAddress && options.chain?.id) {
return {
address: options.tokenAddress,
chainId: options.chain.id,
};
}
return undefined;
});
const chainId = useId();
const tokenId = useId();
const amountId = useId();
const payToId = useId();
const waitUntilId = useId();
const handleChainChange = (chainId: number) => {
setSelectedChain(chainId);
// Clear token selection when chain changes
setSelectedToken(undefined);
setOptions((v) => ({
...v,
chain: defineChain(chainId),
tokenAddress: "0x0000000000000000000000000000000000000000" as const,
tokenSymbol: "",
tokenDecimals: 18,
}));
};
const handleTokenChange = (token: TokenMetadata) => {
setSelectedToken({
address: token.address,
chainId: selectedChain!,
});
setOptions((v) => ({
...v,
tokenAddress: token.address as `0x${string}`,
tokenSymbol: token.symbol ?? "",
tokenDecimals: token.decimals ?? 18,
}));
};
const handleAmountChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setOptions((v) => ({
...v,
amount: e.target.value,
}));
};
const handlePayToChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setOptions((v) => ({
...v,
payTo: e.target.value as `0x${string}`,
}));
};
const handleWaitUntilChange = (
value: "simulated" | "submitted" | "confirmed",
) => {
setOptions((v) => ({
...v,
waitUntil: value,
}));
};
return (
<div className="space-y-6">
<div>
<h2 className="mb-4 text-xl font-semibold">Configuration</h2>
<div className="space-y-4">
{/* Chain selection */}
<div className="flex flex-col gap-2">
<Label htmlFor={chainId}>Chain</Label>
<BridgeNetworkSelector
chainId={selectedChain}
onChange={handleChainChange}
placeholder="Select a chain"
className="bg-card"
/>
</div>
{/* Token selection - only show if chain is selected */}
{selectedChain && (
<div className="flex flex-col gap-2">
<Label htmlFor={tokenId}>Token</Label>
<TokenSelector
chainId={selectedChain}
client={THIRDWEB_CLIENT}
enabled={true}
onChange={handleTokenChange}
placeholder="Select a token"
selectedToken={selectedToken}
className="bg-card"
/>
</div>
)}
{/* Amount input */}
<div className="flex flex-col gap-2">
<Label htmlFor={amountId}>Amount</Label>
<Input
id={amountId}
type="text"
placeholder="0.01"
value={options.amount}
onChange={handleAmountChange}
className="bg-card"
/>
{options.tokenSymbol && (
<p className="text-sm text-muted-foreground">
Amount in {options.tokenSymbol}
</p>
)}
</div>
{/* Pay To input */}
<div className="flex flex-col gap-2">
<Label htmlFor={payToId}>Pay To Address</Label>
<Input
id={payToId}
type="text"
placeholder="0x..."
value={options.payTo}
onChange={handlePayToChange}
className="bg-card"
/>
<p className="text-sm text-muted-foreground">
The wallet address that will receive the payment
</p>
</div>
{/* Wait Until selection */}
<div className="flex flex-col gap-2">
<Label htmlFor={waitUntilId}>Wait Until</Label>
<Select
value={options.waitUntil}
onValueChange={handleWaitUntilChange}
>
<SelectTrigger className="bg-card">
<SelectValue placeholder="Select wait condition" />
</SelectTrigger>
<SelectContent>
<SelectItem value="simulated">Simulated</SelectItem>
<SelectItem value="submitted">Submitted</SelectItem>
<SelectItem value="confirmed">Confirmed</SelectItem>
</SelectContent>
</Select>
<p className="text-sm text-muted-foreground">
When to consider the payment settled: simulated (fastest),
submitted (medium), or confirmed (most secure)
</p>
</div>
</div>
</div>
</div>
);
}
@@ -0,0 +1,45 @@
"use client";
import React, { useState } from "react";
import { useActiveAccount } from "thirdweb/react";
import { chain, token } from "./constants";
import type { X402PlaygroundOptions } from "./types";
import { X402LeftSection } from "./X402LeftSection";
import { X402RightSection } from "./X402RightSection";
const defaultOptions: X402PlaygroundOptions = {
chain: chain,
tokenAddress: token.address as `0x${string}`,
tokenSymbol: token.symbol,
tokenDecimals: token.decimals,
amount: "0.01",
payTo: "0x0000000000000000000000000000000000000000",
waitUntil: "simulated",
};
export function X402Playground() {
const [options, setOptions] = useState<X402PlaygroundOptions>(defaultOptions);
const activeAccount = useActiveAccount();
// Update payTo address when wallet connects, but only if it's still the default
React.useEffect(() => {
if (
activeAccount?.address &&
options.payTo === "0x0000000000000000000000000000000000000000"
) {
setOptions((prev) => ({
...prev,
payTo: activeAccount.address as `0x${string}`,
}));
}
}, [activeAccount?.address, options.payTo]);
return (
<div className="relative flex flex-col-reverse gap-6 xl:min-h-[900px] xl:flex-row xl:gap-6">
<div className="grow border-b pb-10 xl:mb-0 xl:border-r xl:border-b-0 xl:pr-6">
<X402LeftSection options={options} setOptions={setOptions} />
</div>
<X402RightSection options={options} />
</div>
);
}
@@ -0,0 +1,307 @@
"use client";
import { useMutation } from "@tanstack/react-query";
import { Badge } from "@workspace/ui/components/badge";
import { CodeClient } from "@workspace/ui/components/code/code.client";
import { CodeIcon, LockIcon } from "lucide-react";
import { usePathname } from "next/navigation";
import { useState } from "react";
import {
ConnectButton,
useActiveAccount,
useActiveWallet,
} from "thirdweb/react";
import { wrapFetchWithPayment } from "thirdweb/x402";
import { Button } from "@/components/ui/button";
import { Card } from "@/components/ui/card";
import { THIRDWEB_CLIENT } from "@/lib/client";
import { cn } from "@/lib/utils";
import type { X402PlaygroundOptions } from "./types";
type Tab = "ui" | "client-code" | "server-code";
export function X402RightSection(props: { options: X402PlaygroundOptions }) {
const pathname = usePathname();
const [previewTab, _setPreviewTab] = useState<Tab>(() => {
return "ui";
});
function setPreviewTab(tab: "ui" | "client-code" | "server-code") {
_setPreviewTab(tab);
window.history.replaceState({}, "", `${pathname}?tab=${tab}`);
}
const activeWallet = useActiveWallet();
const activeAccount = useActiveAccount();
const paidApiCall = useMutation({
mutationFn: async () => {
if (!activeWallet) {
throw new Error("No active wallet");
}
const fetchWithPay = wrapFetchWithPayment(
fetch,
THIRDWEB_CLIENT,
activeWallet,
BigInt(1 * 10 ** 18),
);
const searchParams = new URLSearchParams();
searchParams.set("chainId", props.options.chain.id.toString());
searchParams.set("payTo", props.options.payTo);
searchParams.set("amount", props.options.amount);
searchParams.set("tokenAddress", props.options.tokenAddress);
searchParams.set("decimals", props.options.tokenDecimals.toString());
searchParams.set("waitUntil", props.options.waitUntil);
const url =
"/api/paywall" +
(searchParams.size > 0 ? "?" + searchParams.toString() : "");
const response = await fetchWithPay(url.toString());
return response.json();
},
});
const handlePayClick = async () => {
paidApiCall.mutate();
};
const clientCode = `import { createThirdwebClient } from "thirdweb";
import { wrapFetchWithPayment } from "thirdweb/x402";
import { useActiveWallet } from "thirdweb/react";
const client = createThirdwebClient({ clientId: "your-client-id" });
export default function Page() {
const wallet = useActiveWallet();
const onClick = async () => {
const fetchWithPay = wrapFetchWithPayment(fetch, client, wallet);
const response = await fetchWithPay('/api/paid-endpoint');
}
return (
<Button onClick={onClick}>Pay Now</Button>
);
}`;
const serverCode = `// Usage in a Next.js API route
import { settlePayment, facilitator } from "thirdweb/x402";
import { createThirdwebClient } from "thirdweb";
import { defineChain } from "thirdweb/chains";
const client = createThirdwebClient({
secretKey: process.env.THIRDWEB_SECRET_KEY,
});
const thirdwebFacilitator = facilitator({
client,
serverWalletAddress: "0xYourServerWalletAddress",
waitUtil: "${props.options.waitUntil}",
});
export async function POST(request: Request) {
const paymentData = request.headers.get("x-payment");
// verify and process the payment
const result = await settlePayment({
resourceUrl: "https://api.example.com/premium-content",
method: "POST",
paymentData,
payTo: "${props.options.payTo}",
network: defineChain(${props.options.chain.id}),
price: {
amount: "${Number(props.options.amount) * 10 ** props.options.tokenDecimals}",
asset: {
address: "${props.options.tokenAddress}",
},
},
facilitator: thirdwebFacilitator,
});
if (result.status === 200) {
// Payment verified and settled successfully
return Response.json({ data: "premium content" });
} else {
// Payment required
return Response.json(result.responseBody, {
status: result.status,
headers: result.responseHeaders,
});
}
}`;
return (
<div className="flex shrink-0 flex-col gap-4 xl:sticky xl:top-0 xl:w-[764px]">
<TabButtons
tabs={[
{
isActive: previewTab === "ui",
name: "UI",
onClick: () => setPreviewTab("ui"),
},
{
isActive: previewTab === "client-code",
name: "Client Code",
onClick: () => setPreviewTab("client-code"),
},
{
isActive: previewTab === "server-code",
name: "Server Code",
onClick: () => setPreviewTab("server-code"),
},
]}
/>
<div
className={cn(
"relative flex min-h-[200px] grow justify-center rounded-lg",
previewTab === "ui" && "items-center",
)}
>
<BackgroundPattern />
{previewTab === "ui" && (
<div className="flex flex-col gap-4 w-full p-4 md:p-12 max-w-lg mx-auto">
<ConnectButton
client={THIRDWEB_CLIENT}
chain={props.options.chain}
detailsButton={{
displayBalanceToken:
props.options.tokenAddress !==
"0x0000000000000000000000000000000000000000"
? {
[props.options.chain.id]: props.options.tokenAddress,
}
: undefined,
}}
supportedTokens={
props.options.tokenAddress !==
"0x0000000000000000000000000000000000000000"
? {
[props.options.chain.id]: [
{
address: props.options.tokenAddress,
symbol: props.options.tokenSymbol,
name: props.options.tokenSymbol,
},
],
}
: undefined
}
/>
<Card className="p-6">
<div className="flex items-center gap-3 mb-4">
<LockIcon className="w-5 h-5 text-muted-foreground" />
<span className="text-lg font-medium">Paid API Call</span>
<Badge variant="success">
<span className="text-xl font-bold">
{props.options.amount}{" "}
{props.options.tokenSymbol || "tokens"}
</span>
</Badge>
</div>
<Button
onClick={handlePayClick}
className="w-full mb-4"
size="lg"
disabled={paidApiCall.isPending || !activeAccount}
>
Access Premium Content
</Button>
<p className="text-sm text-muted-foreground">
Pay for access with {props.options.tokenSymbol || "tokens"} on{" "}
{props.options.chain.name || `chain ${props.options.chain.id}`}
</p>
</Card>
<Card className="p-6">
<div className="flex items-center gap-3 mb-2">
<CodeIcon className="w-5 h-5 text-muted-foreground" />
<span className="text-lg font-medium">API Call Response</span>
</div>
{paidApiCall.isPending && (
<div className="text-center">Loading...</div>
)}
{paidApiCall.isError && (
<div className="text-center">
Error: {paidApiCall.error.message}
</div>
)}
{paidApiCall.data && (
<CodeClient
code={JSON.stringify(paidApiCall.data, null, 2)}
lang="json"
/>
)}
</Card>
</div>
)}
{previewTab === "client-code" && (
<div className="w-full">
<CodeClient
className="h-full rounded-none border-none"
code={clientCode}
lang="tsx"
/>
</div>
)}
{previewTab === "server-code" && (
<div className="w-full">
<CodeClient
className="h-full rounded-none border-none"
code={serverCode}
lang="typescript"
/>
</div>
)}
</div>
</div>
);
}
function BackgroundPattern() {
const color = "hsl(var(--foreground)/15%)";
return (
<div
className="absolute inset-0 z-[-1]"
style={{
backgroundImage: `radial-gradient(${color} 1px, transparent 1px)`,
backgroundSize: "24px 24px",
maskImage:
"radial-gradient(ellipse 100% 100% at 50% 50%, black 30%, transparent 60%)",
}}
/>
);
}
function TabButtons(props: {
tabs: Array<{
name: string;
isActive: boolean;
onClick: () => void;
}>;
}) {
return (
<div>
<div className="flex justify-start gap-1 rounded-lg border bg-card p-2 shadow-md md:inline-flex">
{props.tabs.map((tab) => (
<Button
className={cn(
"gap-2 px-4 text-base",
tab.isActive
? "bg-accent text-foreground"
: "bg-transparent text-muted-foreground",
)}
key={tab.name}
onClick={tab.onClick}
variant="ghost"
>
{tab.name}
</Button>
))}
</div>
</div>
);
}
@@ -0,0 +1,33 @@
import { base } from "thirdweb/chains";
export const chain = base;
export const token = {
address: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
name: "USD Coin",
symbol: "USDC",
decimals: 6,
};
// export const chain = base;
// export const token = {
// address: "0x0578d8A44db98B23BF096A382e016e29a5Ce0ffe",
// decimals: 18,
// name: "Higher",
// symbol: "HIGHER",
// version: "1",
// };
// export const token = {
// address: "0xfdcC3dd6671eaB0709A4C0f3F53De9a333d80798",
// decimals: 18,
// name: "Stable Coin",
// symbol: "SBC",
// version: "1",
// // primaryType: "Permit",
// }
// export const chain = defineChain(3338);
// export const token = {
// address: "0xbbA60da06c2c5424f03f7434542280FCAd453d10",
// decimals: 6,
// name: "USDC",
// symbol: "USDC",
// version: "2",
// }
@@ -0,0 +1,12 @@
import type { Chain } from "thirdweb/chains";
import type { Address } from "thirdweb/utils";
export type X402PlaygroundOptions = {
chain: Chain;
tokenAddress: Address;
tokenSymbol: string;
tokenDecimals: number;
amount: string;
payTo: Address;
waitUntil: "simulated" | "submitted" | "confirmed";
};
@@ -1,102 +0,0 @@
"use client";
import { useMutation } from "@tanstack/react-query";
import { CodeClient } from "@workspace/ui/components/code/code.client";
import { CodeIcon, LockIcon } from "lucide-react";
import { baseSepolia } from "thirdweb/chains";
import {
ConnectButton,
getDefaultToken,
useActiveAccount,
useActiveWallet,
} from "thirdweb/react";
import { wrapFetchWithPayment } from "thirdweb/x402";
import { Button } from "@/components/ui/button";
import { Card } from "@/components/ui/card";
import { THIRDWEB_CLIENT } from "../../../../lib/client";
const chain = baseSepolia;
const token = getDefaultToken(chain, "USDC");
export function X402ClientPreview() {
const activeWallet = useActiveWallet();
const activeAccount = useActiveAccount();
const paidApiCall = useMutation({
mutationFn: async () => {
if (!activeWallet) {
throw new Error("No active wallet");
}
const fetchWithPay = wrapFetchWithPayment(
fetch,
THIRDWEB_CLIENT,
activeWallet,
);
const response = await fetchWithPay("/api/paywall");
return response.json();
},
});
const handlePayClick = async () => {
paidApiCall.mutate();
};
return (
<div className="flex flex-col gap-4 w-full p-4 md:p-12 max-w-lg mx-auto">
<ConnectButton
client={THIRDWEB_CLIENT}
chain={chain}
detailsButton={{
displayBalanceToken: {
[chain.id]: token!.address,
},
}}
supportedTokens={{
[chain.id]: [token!],
}}
/>
<Card className="p-6">
<div className="flex items-center gap-3 mb-4">
<LockIcon className="w-5 h-5 text-muted-foreground" />
<span className="text-lg font-medium">Paid API Call</span>
<span className="text-xl font-bold text-red-600">$0.01</span>
</div>
<Button
onClick={handlePayClick}
className="w-full mb-4"
size="lg"
disabled={paidApiCall.isPending || !activeAccount}
>
Pay Now
</Button>
<p className="text-sm text-muted-foreground">
{" "}
<a
className="underline"
href={"https://faucet.circle.com/"}
target="_blank"
rel="noopener noreferrer"
>
Click here to get USDC on {chain.name}
</a>
</p>
</Card>
<Card className="p-6">
<div className="flex items-center gap-3 mb-2">
<CodeIcon className="w-5 h-5 text-muted-foreground" />
<span className="text-lg font-medium">API Call Response</span>
</div>
{paidApiCall.isPending && <div className="text-center">Loading...</div>}
{paidApiCall.isError && (
<div className="text-center">Error: {paidApiCall.error.message}</div>
)}
{paidApiCall.data && (
<CodeClient
code={JSON.stringify(paidApiCall.data, null, 2)}
lang="json"
/>
)}
</Card>
</div>
);
}
@@ -1,10 +1,8 @@
import { CodeServer } from "@workspace/ui/components/code/code.server";
import { CircleDollarSignIcon, Code2Icon } from "lucide-react";
import { CodeExample, TabName } from "@/components/code/code-example";
import { CircleDollarSignIcon } from "lucide-react";
import ThirdwebProvider from "@/components/thirdweb-provider";
import { PageLayout } from "../../../components/blocks/APIHeader";
import { createMetadata } from "../../../lib/metadata";
import { X402ClientPreview } from "./components/x402-client-preview";
import { X402Playground } from "./components/X402Playground";
const title = "x402 Payments";
const description =
@@ -30,98 +28,8 @@ export default function Page() {
description={description}
docsLink="https://portal.thirdweb.com/payments/x402?utm_source=playground"
>
<X402Example />
<div className="h-8" />
<ServerCodeExample />
<X402Playground />
</PageLayout>
</ThirdwebProvider>
);
}
function ServerCodeExample() {
return (
<>
<div className="mb-4">
<h2 className="font-semibold text-xl tracking-tight">
Next.js Server Code Example
</h2>
<p className="max-w-4xl text-muted-foreground text-balance text-sm md:text-base">
Use any x402 middleware + the thirdweb facilitator to settle
transactions with our server wallet.
</p>
</div>
<div className="overflow-hidden rounded-lg border bg-card">
<div className="flex grow flex-col border-b md:border-r md:border-b-0">
<TabName icon={Code2Icon} name="Server Code" />
<CodeServer
className="h-full rounded-none border-none"
code={`// src/middleware.ts
import { facilitator } from "thirdweb/x402";
import { createThirdwebClient } from "thirdweb";
import { paymentMiddleware } from "x402-next";
const client = createThirdwebClient({ secretKey: "your-secret-key" });
export const middleware = paymentMiddleware(
"0xYourWalletAddress",
{
"/api/paid-endpoint": {
price: "$0.01",
network: "base-sepolia",
config: {
description: "Access to paid content",
},
},
},
facilitator({
client,
serverWalletAddress: "0xYourServerWalletAddress",
}),
);
// Configure which paths the middleware should run on
export const config = {
matcher: ["/api/paid-endpoint"],
};
`}
lang="tsx"
/>
</div>
</div>
</>
);
}
function X402Example() {
return (
<CodeExample
header={{
title: "Client Code Example",
description:
"Wrap your fetch requests with the `wrapFetchWithPayment` function to enable x402 payments.",
}}
code={`import { createThirdwebClient } from "thirdweb";
import { wrapFetchWithPayment } from "thirdweb/x402";
import { useActiveWallet } from "thirdweb/react";
const client = createThirdwebClient({ clientId: "your-client-id" });
export default function Page() {
const wallet = useActiveWallet();
const onClick = async () => {
const fetchWithPay = wrapFetchWithPayment(fetch, client, wallet);
const response = await fetchWithPay('/api/paid-endpoint');
}
return (
<Button onClick={onClick}>Pay Now</Button>
);
}`}
lang="tsx"
preview={<X402ClientPreview />}
/>
);
}
@@ -197,7 +197,7 @@ export function RightSection(props: {
}
auth={connectOptions.enableAuth ? playgroundAuth : undefined}
autoConnect={false}
className="shadow-xl !max-w-full"
className="shadow-xl"
client={THIRDWEB_CLIENT}
header={{
title: connectOptions.modalTitle,
@@ -19,7 +19,7 @@ export function OverviewPage(props: {
<h1 className="mb-2 font-bold text-5xl tracking-tight">
{props.title}
</h1>
<p className="text-base text-muted-foreground leading-normal text-pretty">
<p className="text-base text-muted-foreground leading-normal text-pretty max-w-[80%]">
{props.description}
</p>
</div>
@@ -49,7 +49,6 @@ export function StyledConnectEmbed(
abstract,
]}
client={THIRDWEB_CLIENT}
className="!max-w-full"
theme={theme === "light" ? "light" : "dark"}
wallets={WALLETS}
{...props}

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