Compare commits

..
Author SHA1 Message Date
Joaquim VergesGitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
a5345808ec Version Packages (#7640)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-07-21 02:22:54 -07:00
Joaquim VergesandGitHub bfa0a42219 [SDK] Fix TransactionWidget when wallet has enough currency (#7656) 2025-07-21 02:13:17 -07:00
426bb8bbb2 [SDK] Fix: fixes auto-connect on native <ConnectEmbed /> component (#7650)
Co-authored-by: Joaquim Verges <[email protected]>
2025-07-20 18:09:51 -07:00
823af013ae [SDK] Fix: update hooks to use core transaction types (#7647)
Co-authored-by: Joaquim Verges <[email protected]>
2025-07-20 18:02:09 -07:00
gregandGitHub 98aea1e0b9 [Portal] Payments Docs (#7651) 2025-07-20 17:46:20 -07:00
Joaquim VergesandGitHub 620e294f8c [Engine] Update server wallet config types for EOA and EIP-7702 execution (#7652) 2025-07-20 17:43:13 -07:00
Joaquim VergesandGitHub 84eaded152 [Dashboard] Show EOA server wallets by default (#7645) 2025-07-20 16:26:55 -07:00
MananTank 31d7173432 Dashboard: Show upsell card on support page for free plan (#7648)
<!--

## 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 support functionality within the dashboard, particularly for teams on a free billing plan. It adds checks for the billing plan, modifies the `SupportHeader`, and integrates upsell content to encourage users to upgrade their plans.

### Detailed summary
- `SupportHeader` now accepts `isFreePlan` prop to conditionally render buttons.
- Redirects free plan users from creating support cases.
- Displays upsell content for free plan users in the support page.
- Added functionality to show/hide support forms based on billing plan.
- Updated `CreateSupportCase` component to handle dialog state for support case creation.

> ✨ 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**
  * Free plan users now see an upsell prompt encouraging upgrade to access the Support Center, with a clear list of benefits.
  * The Support Center title has been updated for improved clarity.
  * Support case creation is disabled for free plan users, showing an informative message with a billing link instead.
  * Conditional support action buttons are shown only for paid plans, adapting to the user's context.

* **Improvements**
  * Enhanced visual styling and layout for benefit descriptions in upsell prompts.
  * Error messages in the Support Center now display detailed information for easier troubleshooting.
  * Dialogs for creating support cases now open and close with explicit user control for better interaction.
  * The chat button is hidden on the Support Center page to reduce UI clutter.

* **Bug Fixes**
  * Free plan users are prevented from accessing the support case creation page.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-18 21:17:54 +00:00
Yash094 a48329b697 [dashboard] - support in dashboard (#7608)
<!--

## 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 restructuring the support ticketing system by moving components and modifying links to streamline support interactions within the application.

### Detailed summary
- Deleted multiple support-related files and components.
- Changed `getTeamById` function to remove the `export` keyword.
- Updated support links to point to `/team/~/~/support`.
- Added new support ticket types and forms.
- Introduced new components for handling support tickets.
- Implemented ticket status handling and filtering.
- Enhanced the support case list display with filtering options.
- Created API endpoints for support ticket management.
- Integrated dynamic imports for various support forms.

> The following files were skipped due to too many changes: `apps/dashboard/src/@/components/chat/CustomChatContent.tsx`, `apps/dashboard/src/@/components/chat/CustomChats.tsx`, `apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/_components/SupportCaseDetails.tsx`, `apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/_components/CreateSupportCase.tsx`

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

<!-- end pr-codex -->

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

* **New Features**
  * Unified support ticket system integrated into team dashboards with AI chat assistant and dynamic product-specific support forms.
  * Support ticket listing with status filtering, search, detailed conversation views, and reply functionality.
  * Added "Support" link in team sidebar footer; updated support headers, layouts, and navigation flows.
  * Introduced monochrome mode for mini logo and refined chat UI elements.

* **Bug Fixes / Improvements**
  * Enhanced chat component styling and user experience.
  * Removed obsolete "Support" links from navigation menus and headers.
  * Updated button labels, including changing "Ask AI Assistant" to "Get Help".

* **Chores**
  * Removed legacy support ticket creation pages, components, and client/server logic to consolidate support workflows.
  * Cleaned deprecated components, imports, redirects, and updated error messages to point to the new support section.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-18 19:09:16 +00:00
Joaquim VergesandGitHub 6e62a3aa5c [Docs] Add Ecosystem Wallet to sidebar and update documentation (#7643) 2025-07-18 16:47:56 +12:00
Jonas DanielsandGitHub 746b0c9d8b Add mcp to team capabilities (#7636) 2025-07-17 17:46:55 -07:00
gregandGitHub 0f38a1287a [SDK] Feature: Hide the TransactionWidget contract label if unknown (#7642) 2025-07-17 16:32:05 -07:00
gregandGitHub 61bfebf5d8 Chore: Use /search route for routes page (#7639) 2025-07-17 13:40:29 -07:00
170c377445 Handle token balance retrieval failures (#7600)
Co-authored-by: Cursor Agent <[email protected]>
2025-07-17 13:40:15 -07:00
Yash094 4cbf67181d Add etherlink chain (#7634)
<!--

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

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

## Notes for the reviewer

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

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR introduces new blockchain definitions for the Etherlink mainnet and testnet, enhancing the `thirdweb` package with additional chain support.

### Detailed summary
- Added `etherlink` mainnet definition in `packages/thirdweb/src/chains/chain-definitions/etherlink.ts`.
- Added `etherlinkTestnet` definition in `packages/thirdweb/src/chains/chain-definitions/etherlink-testnet.ts`.
- Exported `etherlink` and `etherlinkTestnet` in `packages/thirdweb/src/exports/chains.ts`.

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

<!-- end pr-codex -->

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

## Summary by CodeRabbit

* **New Features**
  * Added support for the Etherlink mainnet and Etherlink testnet blockchain networks.
  * Users can now interact with these new chains within the platform.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-17 19:24:21 +00:00
arcoraven 6bf6108beb chore: webhooks docs (#7604)
<!--

## 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 `Webhooks` section in the application, providing a sidebar navigation item, a dedicated layout, and detailed documentation on webhooks, including their configuration, management, and security practices.

### Detailed summary
- Added `sidebar` configuration for `Webhooks` in `sidebar.tsx`.
- Introduced `SDKCard` for `Webhooks` in `page.tsx`.
- Renamed `BAST_URL` to `BASE_URL` in `metadata.ts`.
- Created a new `Layout` component for `Webhooks` in `layout.tsx`.
- Defined metadata for `Webhooks` in `layout.tsx`.
- Added comprehensive documentation for webhooks in `page.mdx`, covering setup, management, event handling, and security.

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

<!-- end pr-codex -->

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

* **New Features**
  * Introduced a new "Webhooks" section in the backend APIs grid, accessible from the main portal.
  * Added comprehensive documentation for thirdweb Webhooks, including setup, management, security, and troubleshooting guides.
  * Implemented a dedicated sidebar and layout for the Webhooks documentation page for easier navigation.

* **Documentation**
  * Provided detailed instructions and code examples for implementing and verifying webhook integrations.

* **Bug Fixes**
  * Corrected a typo in the base URL constant used for metadata generation.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-17 19:12:38 +00:00
Phillip HoandGitHub ce85be3346 chore: accordion, rename insight topic, test webhook endpoint (#7635) 2025-07-17 20:57:22 +08:00
MananTank e125e743a3 Dashboard: Temporarily remove gas sponsorship option in token creation (#7631)
<!--

## 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 temporarily disabling the gasless feature in two components: `launch-token.tsx` and `launch-nft.tsx`. The condition for enabling gasless functionality has been commented out and replaced with a static `false` value.

### Detailed summary
- In `launch-token.tsx`:
  - `canEnableGasless` is set to `false` with a comment about enabling it later.
  - `showGaslessSection` is set to `false`.

- In `launch-nft.tsx`:
  - `canEnableGasless` is set to `false` with a comment about enabling it later.
  - `showGaslessSection` is set to `false`.

> ✨ 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**
  * The gasless transaction feature and related UI have been temporarily disabled for both NFT and token launch flows.

* **Chores**
  * Updated internal logic to prevent access to the gasless sponsorship section until further notice.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-16 17:35:01 +00:00
Joaquim VergesGitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
05da842b7a Version Packages (#7616)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-07-16 10:14:03 -07:00
MananTank 216b70475e Dashboard, Portal, Playground: Rename leftover usage of 'Universal Bridge' to 'Payments' in UI (#7611)
<!--

## 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 `Payments` across various files in the codebase, reflecting a rebranding initiative.

### Detailed summary
- Changed `name` and `title` from `Universal Bridge` to `Payments` in multiple files.
- Updated labels and descriptions in UI components to reflect the new branding.
- Modified sidebar links and metadata to align with the new `Payments` terminology.
- Adjusted documentation references to `Payments` from `Universal Bridge`.

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

<!-- end pr-codex -->

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

* **Style**
  * Updated all user-facing references of "Universal Bridge" to "Payments" across banners, page titles, headings, navigation links, sidebars, and documentation content for a consistent branding experience.

* **Documentation**
  * Revised documentation text, metadata, and quickstart guides to reflect the new "Payments" terminology and branding.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-16 16:20:48 +00:00
iuwqyir 9d259d0836 add insight filter selection to webhook creation and update (#7598)
<!-- start pr-codex -->

## PR-Codex overview
This PR focuses on enhancing the webhook configuration components by introducing optional support for `ThirdwebClient` and `supportedChainIds`. It improves error handling and validation, particularly in the context of filtering for event and transaction webhooks.

### Detailed summary
- Made `thirdwebClient` optional in `useAbiProcessing` hook.
- Updated `CreateWebhookConfigModal`, `EditWebhookConfigModal`, `WebhooksOverview` to accept `client` and `supportedChainIds`.
- Enhanced error handling in `useQuery` for missing `thirdwebClient`.
- Introduced separate forms for event and transaction filters in `TopicSelectorModal`.
- Improved validation and fetching of ABIs in the filter forms.
- Added support for displaying ABI fetch status and results.
- Enhanced UI elements to accommodate new filter configurations.

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

<!-- end pr-codex -->

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

* **New Features**
  * Enhanced webhook topic selection with advanced, form-driven filtering for contract webhook topics, including multi-network and ABI-based options.
  * Added support for passing client and supported chain information throughout the webhooks dashboard for improved filtering and configuration.

* **Improvements**
  * Increased dialog width for filter configuration to improve usability.
  * Streamlined handling of webhook topic filters for consistency and flexibility.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-16 13:17:01 +00:00
Phillip HoandGitHub ca52cdd3bf chore: /token and /vault landing pages (#7633) 2025-07-16 20:26:13 +08:00
Joaquim VergesandGitHub cf68002922 [Docs] Update page.mdx (#7632)
Signed-off-by: Joaquim Verges <[email protected]>
2025-07-16 02:08:40 -07:00
MananTank 495c7f925d [TOOL-5047] Dashboard: Fix client object missing teamId on contract publish page (#7628)
<!--

## 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 `PublishContractPage` component to utilize the `getUserThirdwebClient` function for client initialization, enhancing user authentication handling during contract publishing.

### Detailed summary
- Added import for `getUserThirdwebClient` from `@/api/auth-token`.
- Removed import for `getClientThirdwebClient` from `@/constants/thirdweb-client.client`.
- Replaced the client initialization in `ContractPublishForm` with `userThirdwebClient` from `getUserThirdwebClient`.

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

<!-- end pr-codex -->

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

## Summary by CodeRabbit

* **Refactor**
  * Updated the way the client is obtained for the contract publish form, improving consistency in client retrieval. No visible changes to user experience.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-15 20:09:58 +00:00
iuwqyir 32add5f602 Display webhook secret in dashboard (#7610)
```
<!--

## title your PR with this format: "[Dashboard] Feature: Display and Mask Webhook Secret in Contracts Webhooks List"

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

## Notes for the reviewer

This PR introduces a new column to the Contracts Webhooks table to display the `webhook_secret`.

Key points:
- The secret is masked for security, showing `**********` followed by the last 3 characters (e.g., `**********xyz`).
- The full secret can be copied to the clipboard using the adjacent copy button.
- No backend changes were required as the `webhook_secret` is already available in the `WebhookResponse`.

## How to test

1. Navigate to a project's webhooks page on the dashboard (`/dashboard/[team_slug]/[project_slug]/webhooks`).
2. Observe the new "Webhook Secret" column in the table.
3. Verify that the secrets are displayed in the masked format (`**********xyz`).
4. Click the copy icon next to a masked secret and confirm that the full secret is copied to your clipboard.

-->
```

---

[Slack Thread](https://thirdwebdev.slack.com/archives/C085X0VQCF3/p1752487882125419?thread_ts=1752487882.125419&cid=C085X0VQCF3)

<!-- start pr-codex -->

---

## PR-Codex overview
This PR introduces a new function to mask webhook secrets for improved security and updates the `WebhooksTable` component to display the masked secret while allowing users to copy the original secret.

### Detailed summary
- Added `maskWebhookSecret` function to mask webhook secrets.
- Updated `WebhooksTable` to include a new column for "Webhook Secret."
- Displayed masked secret in the table with an option to copy the original secret.

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

<!-- end pr-codex -->

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

## Summary by CodeRabbit

* **New Features**
  * Added a new "Webhook Secret" column to the webhooks table, displaying masked webhook secrets for improved privacy.
  * Included a copy button to easily copy the full unmasked webhook secret to the clipboard.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-15 19:59:09 +00:00
gregandGitHub 5f74b3ca3e Chore: Update UB jsdocs to include types (#7629) 2025-07-15 11:50:42 -07:00
GiselleNessi 761cde6a32 [portal] add new support article (#7595)
<!--

## 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 `knowledge-base` section by adding new assets and a comprehensive guide for bulk updating NFT metadata, specifically for ERC-721A contracts.

### Detailed summary
- Added multiple image assets to the `knowledge-base`.
- Introduced a new guide in `page.mdx` for bulk updating NFT metadata.
- Explained concepts of batch and base URI in the context of NFTs.
- Provided step-by-step instructions for updating NFT metadata efficiently.

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

<!-- end pr-codex -->

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

* **New Features**
  * Added a new sidebar entry for "Bulk update NFT metadata" under the "Contracts" section in the Troubleshoot category.
  * Introduced a comprehensive step-by-step guide on how to update metadata for multiple NFTs in bulk, including instructions, images, and helpful resources.

* **Documentation**
  * Expanded the knowledge base with detailed troubleshooting documentation for bulk updating NFT metadata.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-15 16:55:09 +00:00
Joaquim VergesandGitHub 9546b927d9 [Docs] Redesign sidebar and update wallet documentation (#7602) 2025-07-15 01:26:32 -07:00
jnsdls f82f885dad Update nebula rateLimit type to include perSecond and perMinute (#7614)
# Update Nebula Rate Limit Type

This PR updates the Nebula rate limit type to provide more granular control by replacing the single `rateLimit` number with an object containing `perSecond` and `perMinute` properties. The change affects:

- The `TeamCapabilities` type in the service-utils package
- Corresponding mock data and stubs in the dashboard app
- Added a changeset to document this patch update
2025-07-15 00:12:41 +00:00
jnsdls 34d2fddf49 Update error response URL for project settings (#7613)
# Update error response URL in service-utils

This PR updates the error response URLs in the service-utils package to point to the project-specific settings page instead of the generic API key creation page. The new URLs follow the format `https://thirdweb.com/${team.slug}/${project.slug}/settings`, providing users with a direct link to manage restrictions for their specific project.

The changes affect error messages for:
- Unauthorized domains
- Unauthorized bundle IDs
- Unauthorized services
- Unauthorized service actions

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

## Summary by CodeRabbit

* **Bug Fixes**
  * Updated error messages for unauthorized access to provide project-specific URLs, directing users to the relevant project settings page for more accurate guidance.

* **Chores**
  * Added documentation to track the patch update related to error response URLs.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-15 00:12:41 +00:00
a7544f75ce [Portal] Add next button to inner page footers (#7605)
Co-authored-by: Cursor Agent <[email protected]>
2025-07-14 15:46:21 -07:00
gregandGitHub bd3d645be6 [SDK] Fix: Adds BuyWidget fallback when native isn't supported (#7601) 2025-07-14 12:06:18 -07:00
MananTank 5fedc2c040 [TOOL-5037] Dashboard: Remove autoConnect on /login page (#7609)
<!--

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

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

## Notes for the reviewer

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

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR primarily focuses on refactoring import paths for better organization and consistency across the codebase, as well as enhancing the login and onboarding components, including UI adjustments and functionality updates.

### Detailed summary
- Updated import paths to use absolute imports instead of relative paths.
- Modified the `DashboardLayout` to enable auto-connect.
- Enhanced the `LoginPage` with a new footer message regarding phone login support.
- Adjusted the `Toaster` component to simplify its usage.
- Improved onboarding UI components for better user experience.

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

<!-- end pr-codex -->

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

* **New Features**
  * Added an option to enable automatic wallet connection in the app layout.

* **Refactor**
  * Standardized import paths across the dashboard for improved maintainability and clarity.
  * Updated usage of the notification component to a local version with simplified theming.
  * Improved layout and messaging on the login page, moving phone login deprecation notice to a dedicated footer and refining spacing.

* **Documentation**
  * Enhanced SEO and social sharing metadata on the login page.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-14 18:36:51 +00:00
Yash094 6c8a7cf2ba fix playground payments widget (#7607)
<!--

## 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 `CodeGen` component in the `playground-web` application by renaming a prop for consistency and clarity.

### Detailed summary
- Changed the prop name from `token` to `tokenAddress` in the `CodeGen` component.
- Maintained existing conditional rendering for other props such as `seller`, `title`, `image`, `description`, `paymentMethods`, and `currency`.

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

<!-- end pr-codex -->

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

## Summary by CodeRabbit

* **Refactor**
  * Updated the generated code output to use the prop name `tokenAddress` instead of `token` when displaying the token address in the code example. This change does not affect any other visible functionality.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-14 18:01:58 +00:00
Phillip HoandGitHub 4f02f6eeb7 chore: add webhook secret to table, fix api error (#7606) 2025-07-14 15:55:21 +08:00
72f75ea40b Update converter SDK reference (#7603)
Co-authored-by: Cursor Agent <[email protected]>
2025-07-14 12:53:52 +08:00
Joaquim VergesGitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
8b6a953933 Version Packages (#7594)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-07-13 10:27:06 +12:00
Joaquim VergesandGitHub 33b0b6dcb4 [Dashboard] Add server-wallet signAuthorization capability to wallet access token (#7530) 2025-07-12 02:53:53 -07:00
MananTank cdc7d95db2 [TOOL-4959] Dashboard: Asset page UI improvements (#7599)
<!--

## 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 UI components and enhancing the functionality of the `ERC20` public page. It includes improvements in layout, styling, and the introduction of new features related to token price data and analytics.

### Detailed summary
- Deleted unused files: `useTokenPriceData.ts` and `ContractCreatorBadge.tsx`.
- Updated `PageHeader` to change border style.
- Added `tooltip` prop in `CopyAddressButton`.
- Changed API key usage in `fetchTokenInfoFromBridge`.
- Enhanced `ThirdwebAreaChart` with better props handling.
- Introduced `GridPattern` component for background.
- Improved `RecentTransfers` UI and pagination buttons.
- Updated `PriceChart` and `TokenStats` to directly use passed props for token price data.
- Refactored `ContractHeaderUI` to improve layout and social link handling.

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

<!-- end pr-codex -->

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

* **New Features**
  * Introduced a customizable grid background pattern component for enhanced visual design.
  * Added support for displaying token price data and analytics on ERC20 public pages, including conditional rendering of token statistics.
  * Enabled tooltip customization for the copy address button.

* **Bug Fixes**
  * The contract creator badge icon is now visible, and spacing between elements has been improved.

* **Refactor**
  * Simplified and unified chart headers and analytics section layouts.
  * Streamlined fetching and handling of token price data with a new asynchronous function and removed the previous hook.
  * Improved layout and container styling across ERC20 and NFT public pages, including card backgrounds, borders, and spacing.
  * Adjusted component structures for better visual separation and consistency.
  * Removed the contract creator badge component and replaced it with a simpler contract owner link.

* **Style**
  * Updated card and header styles for better visual hierarchy and clarity.
  * Enhanced recent transfers and contract header sections with improved padding, borders, and rounded corners.
  * Removed small shadow effects from cards and refined header background and border styles.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-12 05:50:29 +05:30
Manan TankandGitHub 592bf6f1fd [TOOL-5021] SDK: Fix theme in ConnectButton Details Modal Buy screen (#7589) 2025-07-12 09:21:50 +12:00
MananTank 8973174ad5 Dashboard: Fix erc20 asset page crash when number of tokens claimed is a fraction (#7597)
<!--

## 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 `SupplyClaimedProgress` component to use `claimedSupplyTokens` and `totalSupplyTokens` as `number` types instead of `bigint`, along with minor adjustments in related components and UI elements.

### Detailed summary
- Changed `enable-background` to `enableBackground` in `DiscordIcon.tsx`.
- Updated `SupplyClaimedProgress` props from `claimedSupply` and `totalSupply` (bigint) to `claimedSupplyTokens` and `totalSupplyTokens` (number).
- Adjusted calculations and conditions in `SupplyClaimedProgress` for new props.
- Modified test cases in stories to reflect new prop types.
- Updated UI elements to display supply values correctly based on the new number type.

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

<!-- end pr-codex -->

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

## Summary by CodeRabbit

* **Refactor**
  * Updated supply progress components and related usage to use number types instead of bigints, with new prop names for clarity.
  * Improved handling and display of "unlimited" supply scenarios in supply progress components.
  * Adjusted percentage calculation and formatting for claimed supply progress.

* **Style**
  * Corrected SVG attribute naming in the Discord icon to ensure proper rendering.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-11 20:38:47 +00:00
Phillip HoandGitHub c160e91c08 chore: redirect /universal-bridge -> /payments (#7593) 2025-07-12 04:20:58 +08:00
gregandGitHub f33e31ad6e [SDK] Fix: only require missing token amount for transaction widget (#7591) 2025-07-11 02:01:57 -07:00
Joaquim VergesandGitHub 4111d0e638 [Dashboard] Add error handling and refactor vault account rotation (#7592) 2025-07-10 19:59:42 -07:00
joaquim-verges ffb7b6487b [Dashboard] use BuyWidget on /bridge (#7590)
Fixes TOOL-5023

<!--

## 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 `UniversalBridgeEmbed` component to replace the `PayEmbed` with `BuyWidget` from the `thirdweb` library, modifying how payment options are handled and improving the integration with the `thirdweb` SDK.

### Detailed summary
- Replaced `PayEmbed` component with `BuyWidget`.
- Removed `payOptions` prop and its nested properties.
- Added `amount`, `chain`, and `tokenAddress` props to `BuyWidget`.
- Updated the theme handling to use `getSDKTheme`.

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

<!-- end pr-codex -->

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

## Summary by CodeRabbit

* **New Features**
  * Updated the embedded widget in the dashboard to use a new buy widget, offering a simplified and more direct interface for purchasing tokens.

* **Refactor**
  * Streamlined the widget’s input options for a more straightforward user experience.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-10 23:09:34 +00:00
Joaquim VergesGitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
77063e5c85 Version Packages (#7588)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-07-10 15:51:49 -07:00
Joaquim VergesandGitHub 585eac2a6f [Dashboard] Replace vault access token with project secret key for authentication (#7570) 2025-07-10 15:32:36 -07:00
MananTank c54db522b3 Dashboard: Fix webhooks posthog feature flag conditional rendering (#7585)
<!--

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

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

## Notes for the reviewer

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

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR introduces a centralized webhooks feature, enhancing the sidebar layout and analytics for webhooks. It adds loading states, integrates feature flags, and modifies the handling of webhooks in various components.

### Detailed summary
- Added `Loading` components using `GenericLoadingPage` in multiple webhook pages.
- Updated `ProjectSidebarLayout` to include `isCentralizedWebhooksFeatureFlagEnabled`.
- Enhanced `ProjectLayout` to fetch and utilize the centralized webhooks feature flag.
- Modified `WebhooksAnalyticsPage` to check for the centralized webhooks feature flag before redirecting.
- Adjusted `WebhooksLayout` to conditionally render links based on the centralized webhooks feature flag.
- Refactored `isFeatureFlagEnabled` function for improved feature flag handling.
- Updated `WebhooksPage` to include centralized webhooks feature flag checks and redirection logic.
- Cleaned up imports and ensured consistent use of `params` across 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 multiple loading indicators across webhooks analytics, contracts, universal bridge, and sidebar routes for smoother user experience.
  * Sidebar and tab navigation dynamically update based on the centralized webhooks feature flag.

* **Bug Fixes**
  * Enhanced error handling with 404 responses for missing accounts, projects, or authentication on webhooks pages.
  * Redirects implemented when the centralized webhooks feature flag is disabled.

* **Refactor**
  * Updated feature flag checks to a new cached API accepting structured parameters.
  * Streamlined parameter handling and navigation logic for webhooks pages.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-10 20:15:27 +00:00
Joaquim VergesandGitHub d62e9971ee [SDK] Fix inapp wallets showing up in all wallets list (#7586) 2025-07-10 13:14:57 -07:00
MananTank 7d796f5aa5 Dashboard: Fix theme for BuyWidget in chain page (#7583)
<!--

## 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 `BuyFundsSection` component by integrating theme support for the `BuyWidget` using the `useTheme` hook from `next-themes`. It adjusts the widget's theme based on the current application theme.

### Detailed summary
- Added `useTheme` hook to access the current theme.
- Imported `getSDKTheme` utility for theme handling.
- Passed the correct theme to the `BuyWidget` based on whether the theme is "dark" or "light".

> ✨ 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**
  * The Buy Funds section now adapts its appearance based on your selected theme (light or dark), providing a more consistent and personalized user experience.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-10 19:57:29 +00:00
Joaquim VergesGitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
a5bb36141d Version Packages (#7584)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-07-10 12:50:16 -07:00
Prithvish BaidyaandGitHub f3234c3c41 [thirdweb/server-wallet] fix address used for simulation for ERC4337 (#7581) 2025-07-10 11:55:04 -07:00
91e3781e83 [Dashboard] Improve user filtering functionality (#7572)
Co-authored-by: Cursor Agent <[email protected]>
2025-07-10 11:53:41 -07:00
arcoraven 86e3aa8c04 feat: webhooks list in dashboard (#7505)
# [Dashboard] Feature: Add Webhook Management UI

![image.png](https://graphite-user-uploaded-assets-prod.s3.amazonaws.com/vKjRiOG6EUjD7XH4cppU/38dbe90b-2bd9-47b6-b190-3c257e99d2fe.png)

![image.png](https://graphite-user-uploaded-assets-prod.s3.amazonaws.com/vKjRiOG6EUjD7XH4cppU/d0a40295-c1c7-45c5-9b23-224a2a631b83.png)

![image.png](https://graphite-user-uploaded-assets-prod.s3.amazonaws.com/vKjRiOG6EUjD7XH4cppU/42d5337d-fcfe-4ef7-a349-1f596013e003.png)

![image.png](https://graphite-user-uploaded-assets-prod.s3.amazonaws.com/vKjRiOG6EUjD7XH4cppU/b7b89fe6-a5f7-4961-b927-bfc754c6665f.png)

![image.png](https://graphite-user-uploaded-assets-prod.s3.amazonaws.com/vKjRiOG6EUjD7XH4cppU/88916c15-a501-4ceb-9267-a23290f683b9.png)

## Notes for the reviewer
This PR adds a new webhook management UI to the dashboard, allowing users to create, edit, delete, and monitor webhooks. The implementation includes:

- A new overview page for webhooks with a table to display all configured webhooks
- Create/edit webhook modals with form validation
- Topic selector for webhook event subscriptions
- Delete confirmation with activity warning
- Webhook metrics display showing request counts and error rates
- API integration for webhook configuration management

## How to test
Navigate to the webhooks section in a project to view the new UI. Create a new webhook, edit its configuration, and test the delete functionality. Verify that metrics are displayed correctly for existing webhooks.

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

* **New Features**
  * Introduced a comprehensive webhooks management interface, including creation, editing, deletion, and topic selection with optional JSON filters.
  * Added analytics dashboards for webhooks, featuring charts for request status codes and latency percentiles, with filtering by webhook and date range.
  * Implemented feature flag-based tab navigation for "Overview" and "Analytics" in the webhooks section.
  * Provided detailed webhook metrics, including recent activity and error rates.

* **Bug Fixes**
  * Improved error handling and user feedback for webhook configuration and analytics operations.

* **Chores**
  * Added PostHog server-side analytics integration.
  * Updated environment variable naming for analytics configuration.
  * Added new dependency for analytics support.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR introduces webhook analytics features and enhances the webhook management system, including new types for webhook statistics, UI components for creating and editing webhooks, and improvements in data fetching and handling.

### Detailed summary
- Added `posthog-node` dependency for server-side analytics.
- Updated environment variable from `NEXT_PUBLIC_POSTHOG_API_KEY` to `NEXT_PUBLIC_POSTHOG_KEY`.
- Introduced interfaces for `WebhookRequestStats`, `WebhookLatencyStats`, and `WebhookSummaryStats`.
- Created `CreateWebhookConfigModal` and `EditWebhookConfigModal` components for managing webhooks.
- Implemented `WebhooksAnalytics` and `WebhookAnalyticsServer` components for displaying analytics data.
- Enhanced `WebhookMetrics` to show metrics and error rates.
- Added `isFeatureFlagEnabled` function for feature flag checks.
- Implemented data fetching functions for webhook metrics in `analytics.ts`.
- Updated the webhook configuration management with new API endpoints and improved error handling.
- Created `TopicSelectorModal` for selecting topics with optional filters.
- Improved UI components for better user experience in managing webhooks.

> 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 -->
2025-07-10 17:21:02 +00:00
MananTank aa1035d16c [TOOL-4988] Dashboard, SDK: Add gas sponsorship option for asset creation for in-app wallets (#7566)
<!--

## 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 gasless transactions across various components in the `thirdweb` dashboard, specifically for NFT and token creation processes. It introduces new parameters and logic to support gas sponsorship features.

### Detailed summary
- Added `useAuthToken` option in `bundler.ts` fetch requests.
- Enhanced `fetch.ts` to include authentication headers for bundler requests.
- Updated `CreateTokenFunctions` and `CreateNFTCollectionFunctions` to accept `gasless` parameter.
- Modified multiple functions in the NFT and token creation pages to support gasless transactions.
- Implemented UI elements for enabling gas sponsorship in the `LaunchNFT` and `LaunchTokenStatus` components.
- Adjusted error handling for unsupported gas sponsorship on certain chains.

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

Here are the updated release notes based on the provided summary:

* **New Features**
  * Added support for gasless transactions when launching NFT collections and tokens, available for eligible team plans and in-app wallets.
  * Introduced a toggle in the launch UI to enable or disable gasless mode for NFT and token creation.
  * Updated UI labels and dialogs for launching NFT collections.

* **Refactor**
  * Standardized function signatures for NFT and token creation to accept a unified parameter object, including a new gasless flag.
  * Centralized account and contract retrieval logic for improved maintainability and consistency.
  * Improved error logging in the token creation workflow.

* **Bug Fixes**
  * Enhanced error handling during token launch to detect unsupported gas sponsorship and provide user-friendly retry options.

* **Chores**
  * Updated request headers logic to properly handle authorization tokens and additional headers for bundler URLs.
  * Enabled authentication token usage in bundler request configurations.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-10 15:18:10 +00:00
Joaquim VergesGitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
bd395a3cff Version Packages (#7579)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-07-10 00:51:14 -07:00
Joaquim VergesandGitHub 446a69b1d1 [SDK] update wallet definitions (#7580) 2025-07-09 21:51:41 -07:00
Manan TankandGitHub 826d9c80a8 Dashboard: Remove OP superchain banner, CTAs, credits (#7569) 2025-07-09 20:10:50 -07:00
Joaquim VergesandGitHub 772eb0cc53 [SDK] Handle different fiat currencies in payment widgets (#7578) 2025-07-09 19:59:35 -07:00
Joaquim VergesGitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
5c6dd874e0 Version Packages (#7577)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-07-09 16:55:48 -07:00
a9e47e4759 Replace PayEmbed with BuyWidget code (#7574)
Co-authored-by: Cursor Agent <[email protected]>
2025-07-09 15:47:29 -07:00
bfd728a9d1 Add branding hide prop to widgets (#7575)
Co-authored-by: Cursor Agent <[email protected]>
2025-07-09 15:46:29 -07:00
saminacodes 381a2823d7 [Docs] Remove Nebula from portal navigation (#7576)
```
<!--

## title your PR with this format: "[Portal] Feature: Remove Nebula from portal navigation"

## Notes for the reviewer

This PR removes all user-facing references to Nebula from the portal navigation and homepage, as requested.

-   **Key point:** All Nebula pages and underlying content remain untouched; only navigation links and homepage sections were removed.
-   Removed Nebula from the main header navigation and the APIs dropdown.
-   Removed Nebula sections from the homepage (API Reference and Learning Resources).
-   Updated the Vault section icon on the homepage from `NebulaIcon` (which was removed) to `ConnectIcon`.

## How to test

1.  Run the portal locally.
2.  Verify "Nebula" is no longer present in the main navigation bar or the "APIs" dropdown.
3.  On the homepage (`/`), confirm that the "Nebula" cards are removed from the "API Reference" and "Learning Resources" sections.
4.  Confirm that the "Vault" card in the "Learning Resources" section now uses the `ConnectIcon`.
5.  Run `npm run portal:build` to ensure the project builds successfully.

-->
```

---

[Slack Thread](https://thirdwebdev.slack.com/archives/C04DYC3G5NZ/p1752094214240499?thread_ts=1752094214.240499&cid=C04DYC3G5NZ)

<!-- start pr-codex -->

---

## PR-Codex overview
This PR primarily focuses on removing references to `Nebula` from the application, including links and UI components, while retaining and emphasizing `Insight` and `Vault`.

### Detailed summary
- Removed `Nebula` link from `connectLinks` in `Header.tsx`.
- Deleted `Nebula` API reference card from `ReferenceSection` in `page.tsx`.
- Eliminated `Nebula` card from `LearningResourcesSection` in `page.tsx`.

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

<!-- end pr-codex -->

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

## Summary by CodeRabbit

* **New Features**
  * None.

* **Bug Fixes**
  * None.

* **Refactor**
  * Removed all references to the "Nebula" feature from navigation menus and resource sections.
  * Updated the icon for the "Vault" article card.

* **Chores**
  * Cleaned up unused imports related to "Nebula".

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-09 21:53:40 +00:00
saminacodes 2111b8b12c Rename Universal Bridge to Payments (#7571)
```
<!--

## title your PR with this format: "[Dashboard] Feature: Rename Universal Bridge to Payments"

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

## Notes for the reviewer

This PR renames all instances of "Universal Bridge" to "Payments" across the projects dashboard. This is a branding update and does not change any underlying functionality or URLs.

Changes include:
- Sidebar navigation label
- Page titles and descriptions
- Callouts and sub-text (e.g., analytics, FTUX)
- Webhooks section (tab name and heading)

## How to test

1. Navigate to any project dashboard.
2. Verify the sidebar now shows "Payments" instead of "Universal Bridge".
3. Click on "Payments" in the sidebar and confirm the page title and description are updated.
4. Check the "Analytics" page to ensure "Universal Bridge" references are updated to "Payments".
5. Go to "Webhooks" and then the "Payments" tab to confirm the heading and tab name are correct.
6. Verify the FTUX section also reflects "Payments".

-->
```

---

[Slack Thread](https://thirdwebdev.slack.com/archives/C08GEMP858V/p1752037029611589?thread_ts=1752037029.611589&cid=C08GEMP858V)

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on renaming references from "Universal Bridge" to "Payments" throughout the codebase, reflecting a change in terminology for a feature that facilitates payments.

### Detailed summary
- Updated `name` in `layout.tsx` from "Universal Bridge" to "Payments".
- Changed `label` in `ProjectSidebarLayout.tsx` from "Universal Bridge" to "Payments".
- Modified `title` in `ProjectFTUX.tsx` from "Universal Bridge" to "Payments".
- Changed heading in `page.tsx` from "Universal Bridge Webhooks" to "Payments Webhooks".
- Updated description in `analytics/page.tsx` from "Universal Bridge" to "Payments".
- Changed description in `sidebar/page.tsx` from "Universal Bridge" to "Payments".
- Updated heading in `universal-bridge/page.tsx` from "Get Started with Universal Bridge" to "Get Started with Payments".
- Changed heading in `universal-bridge/layout.tsx` from "Universal Bridge" to "Payments".
- Updated description in `universal-bridge/layout.tsx` to reflect "Payments" instead of "Universal Bridge".

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

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

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

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

## Notes for the reviewer

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

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on enhancing the NFT creation process by adding the ability to set admin addresses and refining various components for better user experience and validation.

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

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

<!-- end pr-codex -->

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

* **New Features**
  * Added the ability to specify multiple admin wallet addresses when creating an NFT collection, with dynamic form fields and validation.
  * NFT launch workflow now includes a step to assign admin roles to specified addresses.
  * NFT collection info and launch dialogs now display the list of assigned admins.

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

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

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

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

## Notes for the reviewer

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

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on improving error handling by adding `try-catch` blocks around asynchronous calls in the `getContractCreator` function. This change ensures that if an error occurs during the execution of `owner` or `getRoleMember`, the function will return `null` instead of throwing an unhandled error.

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

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

<!-- end pr-codex -->

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

## Summary by CodeRabbit

* **Bug Fixes**
  * Improved error handling when retrieving contract creator information, ensuring failures no longer cause crashes and instead return a safe null value.

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

This PR removes the Plausible Analytics integration from our applications:

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

<!-- start pr-codex -->

---

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

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

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

<!-- end pr-codex -->

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

## Summary by CodeRabbit

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

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

Enabled Engine Cloud service in the project creation modal.

### What changed?

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

### How to test?

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

### Why make this change?

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

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

## Summary by CodeRabbit

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

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

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

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

## Notes for the reviewer

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

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR introduces the `teamPlan` property to various components related to NFT and token creation, allowing the application to handle billing plans effectively. It also adds a new component for handling storage limit errors and integrates upsell reporting for plan upgrades.

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

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

<!-- end pr-codex -->

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

* **New Features**
  * Introduced a tailored upsell experience when users on a free plan hit the storage limit during token or NFT creation, including a new prompt to upgrade and real-time detection of plan changes.
  * Added analytics tracking for upsell prompts and upgrade link clicks.

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

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

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

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

## Notes for the reviewer

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

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on improving the handling of wallet addresses and token fetching in the application. It enhances the rendering of wallet address components and modifies the fetching logic for token information, including error handling.

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

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

<!-- end pr-codex -->

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

* **New Features**
  * Improved wallet address display to support external styling and smaller text size for the zero address.

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

<!-- start pr-codex -->

---

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

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

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

<!-- end pr-codex -->

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

## Summary by CodeRabbit

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

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

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

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

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

## Notes for the reviewer

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

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on adding a condition to check if the `maxClaimablePerWallet` is `"0"` and enhancing the validation of the `phase.snapshot` property within the `claim-conditions-form` component.

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

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

<!-- end pr-codex -->

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

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

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

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

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

<!-- end pr-codex -->

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

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

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

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

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

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

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

<!-- end pr-codex -->

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

## Summary by CodeRabbit

* **New Features**
  * Updated team plan features by removing "Invite Team Members" from the "growth" plan feature list.

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

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

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

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

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

## Summary by CodeRabbit

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

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

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

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

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

## Notes for the reviewer

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

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR introduces two new chain definitions for `Core` and `Core Testnet` to the `thirdweb` package, enhancing its blockchain support.

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

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

<!-- end pr-codex -->

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

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

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

### What changed?

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

### How to test?

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

### Why make this change?

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

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

## Summary by CodeRabbit

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

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

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

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

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

## Notes for the reviewer

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

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on refactoring components in the `routes-table`, `routelist-card`, and `routelist-row` files to improve structure and readability, while introducing new components like `TokenName` and `TokenInfo` for better token representation.

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

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

<!-- end pr-codex -->

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

## Summary by CodeRabbit

* **New Features**
  * Added clickable links to ERC20 token pages for improved navigation and clarity in token displays.
  * Introduced new helper components for consistent token information display.

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

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

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

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

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

## Notes for the reviewer

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

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR updates the `page.tsx` file in the `in-app-wallet` directory to improve the formatting of the `wallets` configuration and the `App` function, ensuring consistent indentation and structure.

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

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

<!-- end pr-codex -->

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

## Summary by CodeRabbit

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

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

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

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

## Notes for the reviewer

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

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on enhancing the NFT launch functionality by adding a successful asset creation report feature. It imports a new reporting function and invokes it upon successful NFT launch, improving analytics tracking.

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

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

<!-- end pr-codex -->

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

## Summary by CodeRabbit

* **New Features**
  * Added a success reporting step after completing the NFT launch process, providing improved feedback for successful NFT creation.

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

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

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

## Notes for the reviewer

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

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on refactoring the notifications settings page by renaming components, updating the layout, and improving the user interface for managing notifications.

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

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

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

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

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

## Notes for the reviewer

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

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on modifying the styling and layout of various components within the dashboard, enhancing user interface elements, and removing unused files.

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

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

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

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

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

## Notes for the reviewer

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

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on updating the `EmptyStateCard` component across various files, including changes to its props, styling, and the metrics displayed. It simplifies the component's usage and modifies the metrics for clarity and relevance.

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

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

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

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

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

## Notes for the reviewer

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

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on making minor adjustments to text labels and descriptions in the application, enhancing consistency in naming and improving grammatical accuracy.

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

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

<!-- end pr-codex -->

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

## Summary by CodeRabbit

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

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

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

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

## Notes for the reviewer

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

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR updates the `AnnouncementBanner` component to reflect a rebranding message and a new link to a blog post, replacing the previous announcement about the Nebula App.

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

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

<!-- end pr-codex -->

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

## Summary by CodeRabbit

* **New Features**
  * Updated the announcement banner to highlight the rebranding of Engine, Universal Bridge, and Connect products, with a link to a detailed blog post.

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

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

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

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

## Notes for the reviewer

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

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR primarily focuses on refactoring and updating the billing and settings-related components in the dashboard application, changing URLs from `/settings` to `/billing`, and enhancing the user interface for better clarity and usability.

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

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

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

<!-- end pr-codex -->

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

* **New Features**
  * Introduced a full-width sidebar layout with collapsible submenus and responsive navigation.
  * Added a team sidebar layout with structured navigation for team dashboards.
  * Added collapsible UI components for use in layouts and menus.
  * Added a reusable StaffModeNotice component for staff mode indication.

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

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

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

* **Chores**
  * Added a new dependency for collapsible UI support.
  * Updated import paths and added lint suppression comments for code clarity and maintainability.
  * Added responsive minimum widths to pagination buttons and consistent border styling to pagination links.
  * Updated multiple URLs in links and buttons to reflect new billing paths without `/settings` segment.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-06-28 12:10:06 +00:00
Joaquim VergesandGitHub 143ab6d127 [Portal] simplify get started wallet documentation (#7466) 2025-06-28 04:10:53 -07:00
bc512551ab [SDK] Add enableCard prop to control fiat payment visibility (#7465)
Co-authored-by: Claude <[email protected]>
Co-authored-by: Joaquim Verges <[email protected]>
2025-06-28 04:09:52 -07:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
8e29d48426 chore(deps): bump @vercel/functions from 2.1.0 to 2.2.2 (#7422)
Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-28 01:09:38 -07:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
e7e6446e7d chore(deps-dev): bump checkly from 5.6.0 to 6.0.1 (#7424)
Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-28 01:09:21 -07:00
Joaquim VergesGitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
7113f7c669 Version Packages (#7463)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-06-27 18:56:26 -07:00
f9d7935d84 Update engine API (#7427)
Signed-off-by: Prithvish Baidya <[email protected]>
Co-authored-by: Prithvish Baidya <[email protected]>
2025-06-27 18:41:37 -07:00
FirekeeperandGitHub acd5656d4d [SDK] EIP-7702 Session Keys (#7432) 2025-06-27 18:11:19 -07:00
gregandGitHub 6d1d344c48 Fix: Exclude IAW when smart wallets are used (#7454) 2025-06-27 13:36:11 -07:00
MananTank 26b8c320ae [TOOL-4899] Dashboard: fetch token icon from bridge api as fallback in coin asset page (#7460)
<!--

## 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 utility function to fetch token information from a bridge and integrates it into the `ERC20PublicPage` component to enhance contract metadata with token icons.

### Detailed summary
- Added `fetchTokenInfoFromBridge` function in `fetch-coin-info.ts` to fetch token details from a bridge API.
- Integrated `fetchTokenInfoFromBridge` in the `ERC20PublicPage` component.
- Updated contract metadata to include the token icon if not already present.

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

<!-- end pr-codex -->

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

* **New Features**
  * Enhanced ERC20 public pages to automatically display token icons when available, enriching token metadata with external information.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-06-27 19:46:52 +00:00
Phillip HoandGitHub c7f7f48222 chore: change webhook back to object (#7461) 2025-06-28 02:18:40 +08:00
MananTank e814248d9c [TOOL-4898] Dashboard: Replace PayEmbed with BuyWidget in asset page, add tracking (#7459)
<!--

## 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 `PayEmbed` component to `BuyWidget` in the `BuyTokenEmbed` function, enhancing the functionality with error reporting and success tracking for asset purchases.

### Detailed summary
- Replaced `PayEmbed` with `BuyWidget` in `BuyTokenEmbed`.
- Added `amount`, `chain`, and `className` props to `BuyWidget`.
- Implemented `onError` and `onSuccess` callbacks for analytics reporting.
- Updated the token address prop to ensure correct type.
- Modified chart className in `contract-analytics.tsx`.

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

<!-- end pr-codex -->

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

* **New Features**
  * Updated the buy token section to use an improved widget with enhanced error and success handling, including dynamic titles and analytics reporting.

* **Style**
  * Adjusted the chart's aspect ratio for better display across different screen sizes.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-06-27 16:43:21 +00:00
MananTank d7a0613067 [TOOL-4897] Dashboard: Add Contract creator badge in Coin and NFT asset page (#7458)
<!--

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

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

## Notes for the reviewer

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

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR introduces the `contractCreator` property to multiple components, allowing the display of the contract creator's address in the UI. It also adds a new function, `getContractCreator`, to retrieve the contract creator's information based on the contract's role.

### Detailed summary
- Added `contractCreator` prop to `NFTPublicPageLayout`, `ContractHeaderUI`, and `ContractCreatorBadge`.
- Implemented `getContractCreator` function to fetch the contract creator using `ThirdwebContract`.
- Updated `NFTPublicPage` and `ERC20PublicPage` to call `getContractCreator` and pass the result to the UI.
- Enhanced storybook stories to include `contractCreator` in various scenarios.

> ✨ 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 display of contract creator information on ERC20 and NFT public pages.
  * Introduced a badge showing the contract creator's address in the contract header section.

* **Enhancements**
  * Contract headers now conditionally show the creator badge if creator data is available.
  * Storybook stories for contract headers updated to include contract creator scenarios for improved UI previews.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-06-27 16:31:14 +00:00
MananTank dbc031664d [TOOL-4867] Dashboard: Fix Coin asset page crash on "unlimited" maxClaimableSupply (#7457)
<!--

## 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 `totalSupply` property in the `SupplyClaimedProgress` component and related files to support the string value `"unlimited"` instead of using a constant value from `thirdweb/utils`.

### Detailed summary
- Changed `totalSupply` type from `bigint` to `bigint | "unlimited"` in `SupplyClaimedProgress`.
- Updated the condition to check for `"unlimited"` instead of `maxUint256`.
- Modified the story for `SupplyClaimedProgress` to use `"unlimited"` instead of `maxUint256`.
- Adjusted `totalSupply` handling in `TokenDropClaim` to return `"unlimited"` if `maxClaimableSupply` equals `maxUint256`.

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

<!-- end pr-codex -->

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

## Summary by CodeRabbit

* **New Features**
  * Updated the supply progress display to clearly indicate when the total supply is unlimited.
* **Refactor**
  * Improved internal handling and representation of unlimited supply for better clarity in the UI.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-06-27 16:20:18 +00:00
saminacodes 5640d48854 [Docs] Project Rebrand (#7455)
CORE-0000

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on renaming and rebranding various components and documentation from `Universal Bridge` and `Engine` to `Payments` and `Transactions`, respectively, to better reflect their functionality and purpose.

### Detailed summary
- Renamed `Universal Bridge` to `Payments` across multiple files.
- Renamed `Engine` to `Transactions` across various components and documentation.
- Updated metadata and titles in numerous `.mdx` files to reflect the new branding.
- Changed references in UI components and layout files to match new naming conventions.
- Adjusted descriptions and summaries in documentation to align with the new terms.
- Updated links and API references to point to the correct services.

> ✨ 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**
  * Expanded the learning resources section with updated and additional cards, including new topics like "Contracts" and "Vault".

* **Style**
  * Updated navigation and sidebar labels for improved clarity (e.g., "Connect" to "Wallets", "Bridge" to "Payments", "Engine" to "Transactions").
  * Swapped the order of the GitHub icon and theme switcher in the header for better usability.

* **Documentation**
  * Renamed all references from "Universal Bridge" to "Payments" and from "Engine" to "Transactions" across guides, FAQs, and documentation.
  * Updated instructions, headings, and content to reflect new product names and improved clarity.
  * Added a new FAQ entry explaining the difference between "Transactions Cloud" and "Dedicated" infrastructure.
  * Updated links and navigation paths to match new terminology.
  * Adjusted terminology in wallet-related documentation to replace "Connect" with "Wallets".
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-06-27 15:26:01 +00:00
GiselleNessi 9d6739ea59 updated text support page (#7447)
<!--

## 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 descriptions and titles of various features in the support section of the dashboard, as well as changing a header for improved clarity.

### Detailed summary
- Updated `description` and `title` for the wallets feature.
- Changed `description` and `title` for the universal bridge feature, along with its `viewAllUrl`.
- Modified `description` and `title` for the engine feature, including its `viewAllUrl`.
- Revised `description` for the insight feature.
- Changed the header from "Learning Resources" to "Support Articles".

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

<!-- end pr-codex -->

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

* **Refactor**
  * Updated product names and descriptions for greater clarity in the Support section.
  * Changed the heading above product cards from "Learning Resources" to "Support Articles."
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-06-27 10:52:59 +00:00
GiselleNessi dc49683357 removed next steps chainlist page (#7443)
<!--

## 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 removing unused components and updating product descriptions in the `page.tsx` and `products.ts` files.

### Detailed summary
- Deleted `NextSteps` and `ClaimChainSection` components from `page.tsx`.
- Removed related JSX comments in `page.tsx`.
- Updated product descriptions and names in `products.ts`:
  - Changed "Pre-built contracts & deployment tools" to "Create, deploy and manage smart contracts".
  - Changed "Client-side SDKs for wallets and blockchain interactions" to "Create and manage crypto wallets".
  - Changed "Engine" to "Transactions".
  - Changed "Account factory contracts, bundler & paymaster" to "Enable gas sponsorship for seamless transactions".
  - Changed "Point of sale solution for bridging, onramping & swapping" to "Enable payments in any token on any chain".
  - Changed "Query, transform and analyze blockchain data" to "Query and analyze blockchain 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

* **Refactor**
  * Removed the "Next Steps" and "Claim Chain" sections from the chain page, including related external links and prompts.
* **Documentation**
  * Updated product names and descriptions to use clearer, more user-focused language for better understanding of features.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-06-27 10:42:56 +00:00
Joaquim VergesGitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
30496388c4 Version Packages (#7453)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-06-27 03:40:38 -07:00
Joaquim VergesandGitHub c2ef5eb51c [SDK] Update zod dependency to 3.25.67 (#7451) 2025-06-27 01:11:51 -07:00
gregandGitHub 277c2e2ed9 Feature: Add fallback when we cant fetch the token in new widgets (#7452) 2025-06-26 16:43:46 -07:00
Joaquim VergesGitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
0ac5e59eb9 Version Packages (#7445)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-06-26 14:56:22 -07:00
gregfromstl fa5e13b5d3 Feature: Adds onramp webhook schema to sdk (#7450)
<!--

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

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

## Notes for the reviewer

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

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on adding onramp webhook parsing functionality to the `Universal Bridge` in the `thirdweb` package, along with updates to existing tests to accommodate new payload structures.

### Detailed summary
- Added onramp webhook parsing for `Universal Bridge`.
- Updated `chainId` and `transactionHash` values in tests.
- Modified the structure of `validWebhook` and `validPayload` in `Webhook.test.ts`.
- Changed `destinationAmount` and `originAmount` types to `bigint`.
- Introduced `onchainWebhookSchema` and `onrampWebhookSchema` in `Webhook.ts`.
- Updated `webhookSchema` to include `onchain` and `onramp` transaction types.
- Enhanced the `parse` function to return a `WebhookPayload` that satisfies the new schema.

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

<!-- end pr-codex -->

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

* **New Features**
  * Enhanced webhook parsing to support onramp webhooks for the Universal Bridge.

* **Refactor**
  * Improved validation and structure of webhook payloads for clearer distinction between onchain and onramp types.
  * Updated numeric fields to ensure correct data types and improved payload handling.

* **Documentation**
  * Added a changeset file describing the new onramp webhook parsing capability.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-06-26 21:30:46 +00:00
gregandGitHub 45bb790dd1 Fix: Include correct client in payment link UI (#7449) 2025-06-26 11:21:52 -07:00
0xFirekeeper 102a8c8224 [Portal] EIP-7702 Session Key Docs (.NET) (#7448)
Closes TOOL-4880

<!-- start pr-codex -->

---

## PR-Codex overview
This PR introduces new features and updates for the `EcosystemWallet` in the Thirdweb .NET SDK, focusing on EIP-7702 functionalities, including session management and permissions.

### Detailed summary
- Added new sidebar links for EIP-7702 actions.
- Updated `CreateSessionKey` function to include EIP-7702 functionalities.
- Introduced detailed documentation for new methods: `SignerHasFullPermissions`, `GetCallPoliciesForSigner`, `GetTransferPoliciesForSigner`, `GetSessionExpirationForSigner`, and `GetSessionStateForSigner`.
- Each method includes usage examples, parameter details, return values, and exception handling.

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

<!-- end pr-codex -->

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

## Summary by CodeRabbit

* **New Features**
  * Expanded sidebar navigation with new links for EIP-7702 session key actions under the wallet section.
  * Added documentation for EIP-7702 session key management, including creating session keys, checking signer permissions, retrieving call and transfer policies, session expiration, and session state.

* **Documentation**
  * Introduced detailed guides and usage examples for new EIP-7702 wallet session key features and related methods in the .NET SDK.
  * Clarified sidebar link names to distinguish between EIP-4337 and EIP-7702 session key creation.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-06-26 16:19:05 +00:00
Phillip HoandGitHub 472e737616 chore: webhook payload must be an array (#7444) 2025-06-26 18:38:21 +08:00
1492 changed files with 50840 additions and 41346 deletions
+1 -1
View File
@@ -63,7 +63,7 @@ jobs:
- name: Setup Biome
uses: biomejs/setup-biome@a9763ed3d2388f5746f9dc3e1a55df7f4609bc89 # v2.5.1
with:
version: 2.0.4
version: 2.0.6
- run: pnpm lint
+1 -1
View File
@@ -37,7 +37,7 @@ NEXT_PUBLIC_TYPESENSE_CONTRACT_API_KEY=
# posthog API key
# - not required for prod/staging
NEXT_PUBLIC_POSTHOG_API_KEY="ignored"
NEXT_PUBLIC_POSTHOG_KEY=""
# Stripe Customer portal
NEXT_PUBLIC_STRIPE_KEY=
+1 -1
View File
@@ -1,4 +1,4 @@
{
"$schema": "https://biomejs.dev/schemas/2.0.0/schema.json",
"$schema": "https://biomejs.dev/schemas/2.0.6/schema.json",
"extends": "//"
}
+9 -4
View File
@@ -9,17 +9,20 @@ module.exports = [
// -- build category
"/wallets",
"/account-abstraction",
"/universal-bridge",
"/payments",
"/auth",
"/in-app-wallets",
"/transactions",
// -- end build category
// -- storage
// -- scale category
"/rpc",
"/insight",
"/storage",
// -- end scale category
// -- nebula
"/nebula",
// --insight
"/insight",
// -- contracts
"/contracts",
"/contracts/modular-contracts",
@@ -59,4 +62,6 @@ module.exports = [
// -- universal bridge landing pages --
"/universal-bridge-regions/:region_slug",
"/enterprise",
"/token",
"/vault",
];
+19 -25
View File
@@ -208,35 +208,29 @@ function getConfig(): NextConfig {
const withBundleAnalyzer = require("@next/bundle-analyzer")({
enabled: process.env.ANALYZE === "true",
});
// eslint-disable-next-line @typescript-eslint/no-var-requires
const { withPlausibleProxy } = require("next-plausible");
// eslint-disable-next-line @typescript-eslint/no-var-requires
const { withSentryConfig } = require("@sentry/nextjs");
return withBundleAnalyzer(
withPlausibleProxy({
customDomain: "https://pl.thirdweb.com",
scriptName: "pl",
})(
withSentryConfig(
{
...baseNextConfig,
// @ts-expect-error - this is a valid option
webpack: (config) => {
if (config.cache) {
config.cache = Object.freeze({
type: "memory",
});
}
config.module = {
...config.module,
exprContextCritical: false,
};
// Important: return the modified config
return config;
},
withSentryConfig(
{
...baseNextConfig,
// @ts-expect-error - this is a valid option
webpack: (config) => {
if (config.cache) {
config.cache = Object.freeze({
type: "memory",
});
}
config.module = {
...config.module,
exprContextCritical: false,
};
// Important: return the modified config
return config;
},
SENTRY_OPTIONS,
),
},
SENTRY_OPTIONS,
),
);
}
+28 -27
View File
@@ -4,13 +4,14 @@
"@chakra-ui/styled-system": "^2.9.2",
"@chakra-ui/theme-tools": "^2.1.2",
"@emotion/react": "11.14.0",
"@emotion/styled": "11.14.0",
"@emotion/styled": "11.14.1",
"@hookform/resolvers": "^3.9.1",
"@marsidev/react-turnstile": "^1.1.0",
"@radix-ui/react-accordion": "^1.2.11",
"@radix-ui/react-alert-dialog": "^1.1.14",
"@radix-ui/react-avatar": "^1.1.10",
"@radix-ui/react-checkbox": "^1.3.2",
"@radix-ui/react-collapsible": "^1.1.11",
"@radix-ui/react-dialog": "1.1.14",
"@radix-ui/react-dropdown-menu": "^2.1.15",
"@radix-ui/react-hover-card": "^1.1.14",
@@ -23,14 +24,14 @@
"@radix-ui/react-slot": "^1.2.3",
"@radix-ui/react-switch": "^1.2.5",
"@radix-ui/react-tooltip": "1.2.7",
"@scalar/api-reference-react": "^0.6.19",
"@sentry/nextjs": "9.29.0",
"@scalar/api-reference-react": "0.7.25",
"@sentry/nextjs": "9.34.0",
"@shazow/whatsabi": "0.22.2",
"@tanstack/react-query": "5.80.7",
"@tanstack/react-query": "5.81.5",
"@tanstack/react-table": "^8.21.3",
"@thirdweb-dev/service-utils": "workspace:*",
"@thirdweb-dev/vault-sdk": "workspace:*",
"@vercel/functions": "2.1.0",
"@vercel/functions": "2.2.2",
"@vercel/og": "^0.6.8",
"abitype": "1.0.8",
"chakra-react-select": "^4.7.6",
@@ -40,29 +41,29 @@
"compare-versions": "^6.1.0",
"date-fns": "4.1.0",
"fast-xml-parser": "^5.2.5",
"framer-motion": "12.17.0",
"framer-motion": "12.23.0",
"fuse.js": "7.1.0",
"input-otp": "^1.4.1",
"ioredis": "^5.6.1",
"ipaddr.js": "^2.2.0",
"lucide-react": "0.514.0",
"next": "15.3.3",
"next-plausible": "^3.12.4",
"lucide-react": "0.525.0",
"next": "15.3.5",
"next-themes": "^0.4.6",
"nextjs-toploader": "^1.6.12",
"nuqs": "^2.4.3",
"p-limit": "^6.2.0",
"papaparse": "^5.5.3",
"pluralize": "^8.0.0",
"posthog-js": "1.252.0",
"prettier": "3.5.3",
"posthog-js": "1.256.1",
"posthog-node": "^5.4.0",
"prettier": "3.6.2",
"qrcode": "^1.5.3",
"react": "19.1.0",
"react-children-utilities": "^2.10.0",
"react-day-picker": "^8.10.1",
"react-dom": "19.1.0",
"react-dropzone": "^14.3.8",
"react-error-boundary": "^5.0.0",
"react-error-boundary": "6.0.0",
"react-hook-form": "7.55.0",
"react-markdown": "10.1.0",
"react-table": "^7.8.0",
@@ -71,7 +72,7 @@
"responsive-rsc": "0.0.7",
"server-only": "^0.0.1",
"shiki": "1.27.0",
"sonner": "2.0.5",
"sonner": "2.0.6",
"spdx-correct": "^3.2.0",
"stripe": "17.7.0",
"swagger-ui-react": "^5.24.1",
@@ -81,19 +82,19 @@
"tiny-invariant": "^1.3.3",
"use-debounce": "^10.0.5",
"vaul": "^1.1.2",
"zod": "3.25.62"
"zod": "3.25.75"
},
"devDependencies": {
"@biomejs/biome": "2.0.4",
"@biomejs/biome": "2.0.6",
"@chakra-ui/cli": "^2.4.1",
"@chromatic-com/storybook": "4.0.0",
"@next/bundle-analyzer": "15.3.3",
"@next/eslint-plugin-next": "15.3.3",
"@playwright/test": "1.53.0",
"@storybook/addon-docs": "9.0.8",
"@storybook/addon-links": "9.0.8",
"@storybook/addon-onboarding": "9.0.8",
"@storybook/nextjs": "9.0.8",
"@chromatic-com/storybook": "4.0.1",
"@next/bundle-analyzer": "15.3.5",
"@next/eslint-plugin-next": "15.3.5",
"@playwright/test": "1.53.2",
"@storybook/addon-docs": "9.0.15",
"@storybook/addon-links": "9.0.15",
"@storybook/addon-onboarding": "9.0.15",
"@storybook/nextjs": "9.0.15",
"@types/color": "4.2.0",
"@types/node": "22.14.1",
"@types/papaparse": "^5.3.16",
@@ -107,15 +108,15 @@
"@typescript-eslint/eslint-plugin": "7.14.1",
"@typescript-eslint/parser": "7.14.1",
"autoprefixer": "^10.4.21",
"checkly": "5.6.0",
"checkly": "6.0.1",
"eslint": "8.57.0",
"eslint-config-biome": "1.9.4",
"eslint-plugin-react-compiler": "19.1.0-rc.2",
"eslint-plugin-storybook": "9.0.8",
"eslint-plugin-storybook": "9.0.15",
"knip": "5.60.2",
"next-sitemap": "^4.2.3",
"postcss": "8.5.5",
"storybook": "9.0.8",
"postcss": "8.5.6",
"storybook": "9.0.15",
"tailwindcss": "3.4.17",
"typescript": "5.8.3"
},
+36 -9
View File
@@ -106,9 +106,27 @@ const projectPageRedirects = [
source: `${projectRoute}/nebula/:path*`,
},
{
source: `${projectRoute}/connect/analytics`,
destination: `${projectRoute}`,
permanent: false,
source: `${projectRoute}/connect/analytics`,
},
];
const teamPageRedirects = [
{
destination: "/team/:team_slug/~/billing/:path*",
permanent: false,
source: "/team/:team_slug/~/settings/billing/:path*",
},
{
destination: "/team/:team_slug/~/billing/invoices/:path*",
permanent: false,
source: "/team/:team_slug/~/settings/invoices/:path*",
},
{
destination: "/team/:team_slug/~/billing",
permanent: false,
source: "/team/:team_slug/~/settings/credits",
},
];
@@ -309,13 +327,6 @@ async function redirects() {
permanent: false,
source: "/solutions/chains",
},
// redirect /rpc to portal
{
destination:
"https://portal.thirdweb.com/infrastructure/rpc-edge/overview",
permanent: false,
source: "/rpc-edge",
},
// redirect /sdk to portal
{
destination: "https://portal.thirdweb.com/connect/blockchain-api",
@@ -405,7 +416,7 @@ async function redirects() {
source: "/connect/account-abstraction",
},
{
destination: "/universal-bridge",
destination: "/payments",
permanent: false,
source: "/connect/universal-bridge",
},
@@ -424,8 +435,24 @@ async function redirects() {
permanent: false,
source: "/engine",
},
{
destination: "/rpc",
permanent: false,
source: "/rpc-edge",
},
{
destination: "/payments",
permanent: false,
source: "/universal-bridge",
},
...legacyDashboardToTeamRedirects,
...projectPageRedirects,
...teamPageRedirects,
{
source: "/support/:path*",
destination: "/team/~/~/support",
permanent: false,
},
];
}
@@ -12,7 +12,7 @@ import { COOKIE_ACTIVE_ACCOUNT, COOKIE_PREFIX_TOKEN } from "@/constants/cookie";
import { NEXT_PUBLIC_THIRDWEB_API_HOST } from "@/constants/public-envs";
import { API_SERVER_SECRET } from "@/constants/server-envs";
import { isVercel } from "@/utils/vercel";
import { verifyTurnstileToken } from "./verifyTurnstileToken";
import { verifyTurnstileToken } from "../../app/login/verifyTurnstileToken";
export async function getLoginPayload(
params: GenerateLoginPayloadParams,
+87 -1
View File
@@ -1,7 +1,10 @@
"use server";
import "server-only";
import { getAuthToken } from "@/api/auth-token";
import { NEXT_PUBLIC_THIRDWEB_API_HOST } from "@/constants/public-envs";
import type { ChainInfraSKU } from "@/types/billing";
import { getAbsoluteUrl } from "@/utils/vercel";
export async function reSubscribePlan(options: {
teamId: string;
@@ -14,7 +17,10 @@ export async function reSubscribePlan(options: {
}
const res = await fetch(
`${NEXT_PUBLIC_THIRDWEB_API_HOST}/v1/teams/${options.teamId}/checkout/resubscribe-plan`,
new URL(
`/v1/teams/${options.teamId}/checkout/resubscribe-plan`,
NEXT_PUBLIC_THIRDWEB_API_HOST,
),
{
body: JSON.stringify({}),
headers: {
@@ -35,3 +41,83 @@ export async function reSubscribePlan(options: {
status: 200,
};
}
export async function getChainInfraCheckoutURL(options: {
teamSlug: string;
skus: ChainInfraSKU[];
chainId: number;
annual: boolean;
}) {
const token = await getAuthToken();
if (!token) {
return {
error: "You are not logged in",
status: "error",
} as const;
}
const res = await fetch(
new URL(
`/v1/teams/${options.teamSlug}/checkout/create-link`,
NEXT_PUBLIC_THIRDWEB_API_HOST,
),
{
body: JSON.stringify({
annual: options.annual,
baseUrl: getAbsoluteUrl(),
chainId: options.chainId,
skus: options.skus,
}),
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
method: "POST",
},
);
if (!res.ok) {
const text = await res.text();
console.error("Failed to create checkout link", text, res.status);
switch (res.status) {
case 402: {
return {
error:
"You have outstanding invoices, please pay these first before re-subscribing.",
status: "error",
} as const;
}
case 429: {
return {
error: "Too many requests, please try again later.",
status: "error",
} as const;
}
case 403: {
return {
error: "You are not authorized to deploy infrastructure.",
status: "error",
} as const;
}
default: {
return {
error: "An unknown error occurred, please try again later.",
status: "error",
} as const;
}
}
}
const json = await res.json();
if (!json.result) {
return {
error: "An unknown error occurred, please try again later.",
status: "error",
} as const;
}
return {
data: json.result as string,
status: "success",
} as const;
}
@@ -0,0 +1,57 @@
import "server-only";
import { unstable_cache } from "next/cache";
import { PostHog } from "posthog-node";
let _posthogClient: PostHog | null = null;
function getPostHogServer(): PostHog | null {
if (!_posthogClient && process.env.NEXT_PUBLIC_POSTHOG_KEY) {
_posthogClient = new PostHog(process.env.NEXT_PUBLIC_POSTHOG_KEY, {
host: "https://us.i.posthog.com",
});
}
return _posthogClient;
}
/**
* Check if a feature flag is enabled for a specific user
* @param flagKey - The feature flag key
* @param userEmail - The user's email address for filtering
*/
export const isFeatureFlagEnabled = unstable_cache(
async (params: {
flagKey: string;
accountId: string;
email: string | undefined;
}): Promise<boolean> => {
const posthogClient = getPostHogServer();
if (!posthogClient) {
console.warn("Posthog client not set");
return true;
}
const { flagKey, accountId, email } = params;
try {
if (posthogClient && accountId) {
const isEnabled = await posthogClient.isFeatureEnabled(
flagKey,
accountId,
{
personProperties: email ? { email } : undefined,
},
);
if (isEnabled !== undefined) {
return isEnabled;
}
}
} catch (error) {
console.error(`Error checking feature flag ${flagKey}:`, error);
}
return false;
},
["is-feature-flag-enabled"],
{
revalidate: 3600, // 1 hour
},
);
+34 -1
View File
@@ -1,6 +1,7 @@
import posthog from "posthog-js";
import type { Team } from "@/api/team";
import type { ProductSKU } from "../types/billing";
// ----------------------------
// CONTRACTS
@@ -361,7 +362,11 @@ export function reportAssetCreationFailed(
properties: { contractType: AssetContractType; error: string } & (
| {
assetType: "nft";
step: "deploy-contract" | "mint-nfts" | "set-claim-conditions";
step:
| "deploy-contract"
| "mint-nfts"
| "set-claim-conditions"
| "set-admins";
}
| {
assetType: "coin";
@@ -380,3 +385,31 @@ export function reportAssetCreationFailed(
step: properties.step,
});
}
type UpsellParams = {
content: "storage-limit";
campaign: "create-coin" | "create-nft";
sku: Exclude<ProductSKU, null>;
};
/**
* ### Why do we need to report this event?
* - To track how effective the upsells are in driving users to upgrade
*
* ### Who is responsible for this event?
* @MananTank
*/
export function reportUpsellShown(properties: UpsellParams) {
posthog.capture("upsell shown", properties);
}
/**
* ### Why do we need to report this event?
* - To track how effective the upsells are in driving users to upgrade
*
* ### Who is responsible for this event?
* @MananTank
*/
export function reportUpsellClicked(properties: UpsellParams) {
posthog.capture("upsell clicked", properties);
}
+203 -1
View File
@@ -6,17 +6,54 @@ import type {
EcosystemWalletStats,
EngineCloudStats,
InAppWalletStats,
RpcMethodStats,
TransactionStats,
UniversalBridgeStats,
UniversalBridgeWalletStats,
UserOpStats,
WalletStats,
WalletUserStats,
WebhookLatencyStats,
WebhookRequestStats,
WebhookSummaryStats,
} from "@/types/analytics";
import { getAuthToken } from "./auth-token";
import { getChains } from "./chain";
export interface InsightChainStats {
date: string;
chainId: string;
totalRequests: number;
}
export interface InsightStatusCodeStats {
date: string;
httpStatusCode: number;
totalRequests: number;
}
export interface InsightEndpointStats {
date: string;
endpoint: string;
totalRequests: number;
}
interface InsightUsageStats {
date: string;
totalRequests: number;
}
export interface RpcMethodStats {
date: string;
evmMethod: string;
count: number;
}
export interface RpcUsageTypeStats {
date: string;
usageType: string;
count: number;
}
async function fetchAnalytics(
input: string | URL,
init?: RequestInit,
@@ -76,6 +113,9 @@ function buildSearchParams(params: AnalyticsQueryParams): URLSearchParams {
if (params.period) {
searchParams.append("period", params.period);
}
if (params.limit) {
searchParams.append("limit", params.limit.toString());
}
return searchParams;
}
@@ -225,6 +265,26 @@ export async function getRpcMethodUsage(
return json.data as RpcMethodStats[];
}
export async function getRpcUsageByType(
params: AnalyticsQueryParams,
): Promise<RpcUsageTypeStats[]> {
const searchParams = buildSearchParams(params);
const res = await fetchAnalytics(
`v2/rpc/usage-types?${searchParams.toString()}`,
{
method: "GET",
},
);
if (res?.status !== 200) {
console.error("Failed to fetch RPC usage");
return [];
}
const json = await res.json();
return json.data as RpcUsageTypeStats[];
}
export async function getWalletUsers(
params: AnalyticsQueryParams,
): Promise<WalletUserStats[]> {
@@ -424,3 +484,145 @@ export async function getEngineCloudMethodUsage(
const json = await res.json();
return json.data as EngineCloudStats[];
}
export async function getWebhookSummary(
params: AnalyticsQueryParams & { webhookId: string },
): Promise<{ data: WebhookSummaryStats[] } | { error: string }> {
const searchParams = buildSearchParams(params);
searchParams.append("webhookId", params.webhookId);
const res = await fetchAnalytics(
`v2/webhook/summary?${searchParams.toString()}`,
);
if (!res.ok) {
const reason = await res.text();
return { error: reason };
}
return (await res.json()) as { data: WebhookSummaryStats[] };
}
export async function getWebhookRequests(
params: AnalyticsQueryParams & { webhookId?: string },
): Promise<{ data: WebhookRequestStats[] } | { error: string }> {
const searchParams = buildSearchParams(params);
if (params.webhookId) {
searchParams.append("webhookId", params.webhookId);
}
const res = await fetchAnalytics(
`v2/webhook/requests?${searchParams.toString()}`,
);
if (!res.ok) {
const reason = await res.text();
return { error: reason };
}
return (await res.json()) as { data: WebhookRequestStats[] };
}
export async function getWebhookLatency(
params: AnalyticsQueryParams & { webhookId?: string },
): Promise<{ data: WebhookLatencyStats[] } | { error: string }> {
const searchParams = buildSearchParams(params);
if (params.webhookId) {
searchParams.append("webhookId", params.webhookId);
}
const res = await fetchAnalytics(
`v2/webhook/latency?${searchParams.toString()}`,
);
if (!res.ok) {
const reason = await res.text();
return { error: reason };
}
return (await res.json()) as { data: WebhookLatencyStats[] };
}
export async function getInsightChainUsage(
params: AnalyticsQueryParams,
): Promise<{ data: InsightChainStats[] } | { errorMessage: string }> {
const searchParams = buildSearchParams(params);
const res = await fetchAnalytics(
`v2/insight/usage/by-chain?${searchParams.toString()}`,
{
method: "GET",
},
);
if (res?.status !== 200) {
const reason = await res?.text();
const errMsg = `Failed to fetch Insight chain usage: ${res?.status} - ${res.statusText} - ${reason}`;
console.error(errMsg);
return { errorMessage: errMsg };
}
const json = await res.json();
return { data: json.data as InsightChainStats[] };
}
export async function getInsightStatusCodeUsage(
params: AnalyticsQueryParams,
): Promise<{ data: InsightStatusCodeStats[] } | { errorMessage: string }> {
const searchParams = buildSearchParams(params);
const res = await fetchAnalytics(
`v2/insight/usage/by-status-code?${searchParams.toString()}`,
{
method: "GET",
},
);
if (res?.status !== 200) {
const reason = await res?.text();
const errMsg = `Failed to fetch Insight status code usage: ${res?.status} - ${res.statusText} - ${reason}`;
console.error(errMsg);
return { errorMessage: errMsg };
}
const json = await res.json();
return { data: json.data as InsightStatusCodeStats[] };
}
export async function getInsightEndpointUsage(
params: AnalyticsQueryParams,
): Promise<{ data: InsightEndpointStats[] } | { errorMessage: string }> {
const searchParams = buildSearchParams(params);
const res = await fetchAnalytics(
`v2/insight/usage/by-endpoint?${searchParams.toString()}`,
{
method: "GET",
},
);
if (res?.status !== 200) {
const reason = await res?.text();
const errMsg = `Failed to fetch Insight endpoint usage: ${res?.status} - ${res.statusText} - ${reason}`;
console.error(errMsg);
return { errorMessage: errMsg };
}
const json = await res.json();
return { data: json.data as InsightEndpointStats[] };
}
export async function getInsightUsage(
params: AnalyticsQueryParams,
): Promise<{ data: InsightUsageStats[] } | { errorMessage: string }> {
const searchParams = buildSearchParams(params);
const res = await fetchAnalytics(
`v2/insight/usage?${searchParams.toString()}`,
{
method: "GET",
},
);
if (res?.status !== 200) {
const reason = await res?.text();
const errMsg = `Failed to fetch Insight usage: ${res?.status} - ${res.statusText} - ${reason}`;
console.error(errMsg);
return { errorMessage: errMsg };
}
const json = await res.json();
return { data: json.data as InsightUsageStats[] };
}
-36
View File
@@ -1,44 +1,8 @@
import "server-only";
import type { ChainMetadata } from "thirdweb/chains";
import { NEXT_PUBLIC_THIRDWEB_API_HOST } from "@/constants/public-envs";
import { API_SERVER_SECRET } from "@/constants/server-envs";
import type { ChainService } from "@/types/chain";
export async function getGasSponsoredChains() {
if (!API_SERVER_SECRET) {
throw new Error("API_SERVER_SECRET is not set");
}
const res = await fetch(
`${NEXT_PUBLIC_THIRDWEB_API_HOST}/v1/chains/gas-sponsored`,
{
headers: {
"Content-Type": "application/json",
"x-service-api-key": API_SERVER_SECRET,
},
next: {
revalidate: 15 * 60, //15 minutes
},
},
);
if (!res.ok) {
console.error(
"Failed to fetch gas sponsored chains",
res.status,
res.statusText,
);
res.body?.cancel();
return [];
}
try {
return (await res.json()).data as number[];
} catch (e) {
console.error("Failed to parse gas sponsored chains", e);
return [];
}
}
export function getChains() {
return fetch(
`${NEXT_PUBLIC_THIRDWEB_API_HOST}/v1/chains`,
@@ -1,22 +1,26 @@
export const authOptions = [
"email",
"phone",
"passkey",
"siwe",
"guest",
"google",
"facebook",
"x",
"discord",
"farcaster",
"telegram",
"github",
"twitch",
"steam",
"apple",
"coinbase",
"line",
] as const;
import "server-only";
import { NEXT_PUBLIC_THIRDWEB_API_HOST } from "@/constants/public-envs";
import { getAuthToken } from "./auth-token";
export type AuthOption =
| "email"
| "phone"
| "passkey"
| "siwe"
| "guest"
| "google"
| "facebook"
| "x"
| "discord"
| "farcaster"
| "telegram"
| "github"
| "twitch"
| "steam"
| "apple"
| "coinbase"
| "line";
export type Ecosystem = {
name: string;
@@ -24,7 +28,7 @@ export type Ecosystem = {
id: string;
slug: string;
permission: "PARTNER_WHITELIST" | "ANYONE";
authOptions: (typeof authOptions)[number][];
authOptions: AuthOption[];
customAuthOptions?: {
authEndpoint?: {
url: string;
@@ -47,6 +51,54 @@ export type Ecosystem = {
updatedAt: string;
};
export async function fetchEcosystemList(teamIdOrSlug: string) {
const token = await getAuthToken();
if (!token) {
return [];
}
const res = await fetch(
`${NEXT_PUBLIC_THIRDWEB_API_HOST}/v1/teams/${teamIdOrSlug}/ecosystem-wallet`,
{
headers: {
Authorization: `Bearer ${token}`,
},
},
);
if (!res.ok) {
return [];
}
return (await res.json()).result as Ecosystem[];
}
export async function fetchEcosystem(slug: string, teamIdOrSlug: string) {
const token = await getAuthToken();
if (!token) {
return null;
}
const res = await fetch(
`${NEXT_PUBLIC_THIRDWEB_API_HOST}/v1/teams/${teamIdOrSlug}/ecosystem-wallet/${slug}`,
{
headers: {
Authorization: `Bearer ${token}`,
},
},
);
if (!res.ok) {
const data = await res.json();
console.error(data);
return null;
}
const data = (await res.json()) as { result: Ecosystem };
return data.result;
}
type PartnerPermission = "PROMPT_USER_V1" | "FULL_CONTROL_V1";
export type Partner = {
id: string;
+143
View File
@@ -0,0 +1,143 @@
"use server";
import { NEXT_PUBLIC_THIRDWEB_API_HOST } from "@/constants/public-envs";
import type { SupportTicket } from "../../app/(app)/team/[team_slug]/(team)/~/support/types/tickets";
import { getAuthToken, getAuthTokenWalletAddress } from "./auth-token";
const ESCALATION_FEEDBACK_RATING = 9999;
export async function createSupportTicket(params: {
message: string;
teamSlug: string;
teamId: string;
title: string;
conversationId?: string;
}): Promise<{ data: SupportTicket } | { error: string }> {
const token = await getAuthToken();
if (!token) {
return { error: "No auth token available" };
}
try {
const walletAddress = await getAuthTokenWalletAddress();
const encodedTeamSlug = encodeURIComponent(params.teamSlug);
const apiUrl = `${NEXT_PUBLIC_THIRDWEB_API_HOST}/v1/teams/${encodedTeamSlug}/support-conversations`;
// Build the payload for creating a conversation
// If the message does not already include wallet address, prepend it
let message = params.message;
if (!message.includes("Wallet address:")) {
message = `Wallet address: ${String(walletAddress || "-")}\n${message}`;
}
const payload = {
markdown: message.trim(),
title: params.title,
};
const body = JSON.stringify(payload);
const headers: Record<string, string> = {
Accept: "application/json",
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
"Accept-Encoding": "identity",
};
const response = await fetch(apiUrl, {
body,
headers,
method: "POST",
});
if (!response.ok) {
const errorText = await response.text();
return { error: `API Server error: ${response.status} - ${errorText}` };
}
const createdConversation: SupportTicket = await response.json();
// Escalate to SIWA feedback endpoint if conversationId is provided
if (params.conversationId) {
try {
const siwaUrl = process.env.NEXT_PUBLIC_SIWA_URL;
if (siwaUrl) {
await fetch(`${siwaUrl}/v1/chat/feedback`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
...(params.teamId ? { "x-team-id": params.teamId } : {}),
},
body: JSON.stringify({
conversationId: params.conversationId,
feedbackRating: ESCALATION_FEEDBACK_RATING,
}),
});
}
} catch (error) {
// Log error but don't fail the ticket creation
console.error("Failed to escalate to SIWA feedback:", error);
}
}
return { data: createdConversation };
} catch (error) {
return {
error: `Failed to create support ticket: ${error instanceof Error ? error.message : "Unknown error"}`,
};
}
}
export async function sendMessageToTicket(request: {
ticketId: string;
teamSlug: string;
teamId: string;
message: string;
}): Promise<{ success: true } | { error: string }> {
if (!request.ticketId || !request.teamSlug) {
return { error: "Ticket ID and team slug are required" };
}
const token = await getAuthToken();
if (!token) {
return { error: "No auth token available" };
}
try {
const encodedTeamSlug = encodeURIComponent(request.teamSlug);
const encodedTicketId = encodeURIComponent(request.ticketId);
const apiUrl = `${NEXT_PUBLIC_THIRDWEB_API_HOST}/v1/teams/${encodedTeamSlug}/support-conversations/${encodedTicketId}/messages`;
// Append /unthread send for customer messages to ensure proper routing
const messageWithUnthread = `${request.message.trim()}\n/unthread send`;
const payload = {
markdown: messageWithUnthread,
};
const body = JSON.stringify(payload);
const headers: Record<string, string> = {
Accept: "application/json",
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
"Accept-Encoding": "identity",
...(request.teamId ? { "x-team-id": request.teamId } : {}),
};
const response = await fetch(apiUrl, {
body,
headers,
method: "POST",
});
if (!response.ok) {
const errorText = await response.text();
return { error: `API Server error: ${response.status} - ${errorText}` };
}
return { success: true };
} catch (error) {
return {
error: `Failed to send message: ${error instanceof Error ? error.message : "Unknown error"}`,
};
}
}
+67 -2
View File
@@ -1,6 +1,6 @@
import { getAuthToken } from "@/api/auth-token";
import { NEXT_PUBLIC_THIRDWEB_API_HOST } from "@/constants/public-envs";
import type { ProductSKU } from "@/types/billing";
import type { ChainInfraSKU, ProductSKU } from "@/types/billing";
type InvoiceLine = {
// amount for this line item
@@ -22,7 +22,7 @@ type Invoice = {
export type TeamSubscription = {
id: string;
type: "PLAN" | "USAGE" | "PLAN_ADD_ON" | "PRODUCT";
type: "PLAN" | "USAGE" | "PLAN_ADD_ON" | "PRODUCT" | "CHAIN";
status:
| "incomplete"
| "incomplete_expired"
@@ -37,6 +37,13 @@ export type TeamSubscription = {
trialStart: string | null;
trialEnd: string | null;
upcomingInvoice: Invoice;
skus: (ProductSKU | ChainInfraSKU)[];
};
type ChainTeamSubscription = Omit<TeamSubscription, "skus"> & {
chainId: string;
skus: ChainInfraSKU[];
isLegacy: boolean;
};
export async function getTeamSubscriptions(slug: string) {
@@ -60,3 +67,61 @@ export async function getTeamSubscriptions(slug: string) {
}
return null;
}
const CHAIN_PLAN_TO_INFRA = {
"chain:plan:gold": ["chain:infra:rpc", "chain:infra:account_abstraction"],
"chain:plan:platinum": [
"chain:infra:rpc",
"chain:infra:insight",
"chain:infra:account_abstraction",
],
"chain:plan:ultimate": [
"chain:infra:rpc",
"chain:infra:insight",
"chain:infra:account_abstraction",
],
};
export async function getChainSubscriptions(slug: string) {
const allSubscriptions = await getTeamSubscriptions(slug);
if (!allSubscriptions) {
return null;
}
// first replace any sku that MIGHT match a chain plan
const updatedSubscriptions = allSubscriptions
.filter((s) => s.type === "CHAIN")
.map((s) => {
const skus = s.skus;
const updatedSkus = skus.flatMap((sku) => {
const plan =
CHAIN_PLAN_TO_INFRA[sku as keyof typeof CHAIN_PLAN_TO_INFRA];
return plan ? plan : sku;
});
return {
...s,
isLegacy: updatedSkus.length !== skus.length,
skus: updatedSkus,
};
});
return updatedSubscriptions.filter(
(s): s is ChainTeamSubscription =>
"chainId" in s && typeof s.chainId === "string",
);
}
export async function getChainSubscriptionForChain(
slug: string,
chainId: number,
) {
const chainSubscriptions = await getChainSubscriptions(slug);
if (!chainSubscriptions) {
return null;
}
return (
chainSubscriptions.find((s) => s.chainId === chainId.toString()) ?? null
);
}
+1 -1
View File
@@ -48,7 +48,7 @@ export async function service_getTeamBySlug(slug: string) {
return null;
}
export function getTeamById(id: string) {
function getTeamById(id: string) {
return getTeamBySlug(id);
}
+362
View File
@@ -0,0 +1,362 @@
"use server";
import { getAuthToken } from "@/api/auth-token";
import { NEXT_PUBLIC_THIRDWEB_API_HOST } from "@/constants/public-envs";
export interface WebhookConfig {
id: string;
teamId: string;
projectId: string;
destinationUrl: string;
description: string;
pausedAt: string | null;
webhookSecret: string;
createdAt: string;
updatedAt: string;
deletedAt: string | null;
topics: {
id: string;
serviceName: string;
description: string;
createdAt: string;
updatedAt: string;
deletedAt: string | null;
filters: object | null;
}[];
}
type WebhookConfigsResponse =
| {
data: WebhookConfig[];
status: "success";
}
| {
body: string;
reason: string;
status: "error";
};
interface CreateWebhookConfigRequest {
topicIdsWithFilters: { id: string; filters: object | null }[];
destinationUrl: string;
description: string;
isPaused?: boolean;
}
type CreateWebhookConfigResponse =
| {
data: WebhookConfig;
status: "success";
}
| {
body: string;
reason: string;
status: "error";
};
export interface Topic {
id: string;
serviceName: string;
description: string;
createdAt: string;
updatedAt: string;
deletedAt: string | null;
}
type TopicsResponse =
| {
data: Topic[];
status: "success";
}
| {
body: string;
reason: string;
status: "error";
};
interface UpdateWebhookConfigRequest {
destinationUrl?: string;
topicIdsWithFilters?: { id: string; filters: object | null }[];
description?: string;
isPaused?: boolean;
}
type UpdateWebhookConfigResponse =
| {
data: WebhookConfig;
status: "success";
}
| {
body: string;
reason: string;
status: "error";
};
type DeleteWebhookConfigResponse =
| {
data: WebhookConfig;
status: "success";
}
| {
body: string;
reason: string;
status: "error";
};
export async function getWebhookConfigs(props: {
teamIdOrSlug: string;
projectIdOrSlug: string;
}): Promise<WebhookConfigsResponse> {
const authToken = await getAuthToken();
if (!authToken) {
return {
body: "Authentication required",
reason: "no_auth_token",
status: "error",
};
}
const response = await fetch(
`${NEXT_PUBLIC_THIRDWEB_API_HOST}/v1/teams/${props.teamIdOrSlug}/projects/${props.projectIdOrSlug}/webhook-configs`,
{
headers: {
Authorization: `Bearer ${authToken}`,
"Content-Type": "application/json",
},
method: "GET",
},
);
if (!response.ok) {
const body = await response.text();
return {
body,
reason: "unknown",
status: "error",
};
}
const result = await response.json();
return {
data: result.data,
status: "success",
};
}
export async function createWebhookConfig(props: {
teamIdOrSlug: string;
projectIdOrSlug: string;
config: CreateWebhookConfigRequest;
}): Promise<CreateWebhookConfigResponse> {
const authToken = await getAuthToken();
if (!authToken) {
return {
body: "Authentication required",
reason: "no_auth_token",
status: "error",
};
}
const response = await fetch(
`${NEXT_PUBLIC_THIRDWEB_API_HOST}/v1/teams/${props.teamIdOrSlug}/projects/${props.projectIdOrSlug}/webhook-configs`,
{
body: JSON.stringify(props.config),
headers: {
Authorization: `Bearer ${authToken}`,
"Content-Type": "application/json",
},
method: "POST",
},
);
if (!response.ok) {
const body = await response.text();
return {
body,
reason: "unknown",
status: "error",
};
}
const result = await response.json();
return {
data: result.data,
status: "success",
};
}
export async function getAvailableTopics(): Promise<TopicsResponse> {
const authToken = await getAuthToken();
if (!authToken) {
return {
body: "Authentication required",
reason: "no_auth_token",
status: "error",
};
}
const response = await fetch(
`${NEXT_PUBLIC_THIRDWEB_API_HOST}/v1/webhook-topics`,
{
headers: {
Authorization: `Bearer ${authToken}`,
"Content-Type": "application/json",
},
method: "GET",
},
);
if (!response.ok) {
const body = await response.text();
return {
body,
reason: "unknown",
status: "error",
};
}
const result = await response.json();
return {
data: result.data,
status: "success",
};
}
export async function updateWebhookConfig(props: {
teamIdOrSlug: string;
projectIdOrSlug: string;
webhookConfigId: string;
config: UpdateWebhookConfigRequest;
}): Promise<UpdateWebhookConfigResponse> {
const authToken = await getAuthToken();
if (!authToken) {
return {
body: "Authentication required",
reason: "no_auth_token",
status: "error",
};
}
const response = await fetch(
`${NEXT_PUBLIC_THIRDWEB_API_HOST}/v1/teams/${props.teamIdOrSlug}/projects/${props.projectIdOrSlug}/webhook-configs/${props.webhookConfigId}`,
{
body: JSON.stringify(props.config),
headers: {
Authorization: `Bearer ${authToken}`,
"Content-Type": "application/json",
},
method: "PATCH",
},
);
if (!response.ok) {
const body = await response.text();
return {
body,
reason: "unknown",
status: "error",
};
}
const result = await response.json();
return {
data: result.data,
status: "success",
};
}
export async function deleteWebhookConfig(props: {
teamIdOrSlug: string;
projectIdOrSlug: string;
webhookConfigId: string;
}): Promise<DeleteWebhookConfigResponse> {
const authToken = await getAuthToken();
if (!authToken) {
return {
body: "Authentication required",
reason: "no_auth_token",
status: "error",
};
}
const response = await fetch(
`${NEXT_PUBLIC_THIRDWEB_API_HOST}/v1/teams/${props.teamIdOrSlug}/projects/${props.projectIdOrSlug}/webhook-configs/${props.webhookConfigId}`,
{
headers: {
Authorization: `Bearer ${authToken}`,
"Content-Type": "application/json",
},
method: "DELETE",
},
);
if (!response.ok) {
const body = await response.text();
return {
body,
reason: "unknown",
status: "error",
};
}
const result = await response.json();
return {
data: result.data,
status: "success",
};
}
type TestDestinationUrlResponse =
| {
result: {
httpStatusCode: number;
httpResponseBody: string;
};
status: "success";
}
| {
body: string;
reason: string;
status: "error";
};
export async function testDestinationUrl(props: {
teamIdOrSlug: string;
projectIdOrSlug: string;
destinationUrl: string;
}): Promise<TestDestinationUrlResponse> {
const authToken = await getAuthToken();
if (!authToken) {
return {
body: "Authentication required",
reason: "no_auth_token",
status: "error",
};
}
const resp = await fetch(
`${NEXT_PUBLIC_THIRDWEB_API_HOST}/v1/teams/${props.teamIdOrSlug}/projects/${props.projectIdOrSlug}/webhook-configs/test-destination-url`,
{
body: JSON.stringify({ destinationUrl: props.destinationUrl }),
headers: {
Authorization: `Bearer ${authToken}`,
"Content-Type": "application/json",
},
method: "POST",
},
);
if (!resp.ok) {
return {
body: await resp.text(),
reason: "unknown",
status: "error",
};
}
return await resp.json();
}
@@ -8,11 +8,13 @@ import {
SelectValue,
} from "@/components/ui/select";
import { normalizeTime } from "@/lib/time";
import { cn } from "@/lib/utils";
export function DateRangeSelector(props: {
range: Range;
setRange: (range: Range) => void;
popoverAlign?: "start" | "end" | "center";
className?: string;
}) {
const { range, setRange } = props;
const daysDiff = differenceInCalendarDays(range.to, range.from);
@@ -26,7 +28,7 @@ export function DateRangeSelector(props: {
return (
<DatePickerWithRange
className="w-auto bg-card"
className={cn("w-auto bg-card", props.className)}
from={range.from}
header={
<div className="mb-2 border-border border-b p-4">
@@ -76,8 +76,9 @@ export function RangeSelector({
});
return (
<div className="flex justify-end gap-4">
<div className="flex justify-end gap-3 flex-col lg:flex-row">
<DateRangeSelector
className="rounded-full"
range={localRange}
setRange={(newRange) => {
setRange(newRange);
@@ -89,7 +90,7 @@ export function RangeSelector({
}}
/>
<IntervalSelector
className="bg-card"
className="bg-card rounded-full"
intervalType={localInterval}
setIntervalType={(newInterval) => {
setInterval(newInterval);
@@ -82,7 +82,7 @@ function UnpaidInvoicesWarning({ teamSlug }: { teamSlug: string }) {
<Button asChild className="w-full gap-2 bg-card" variant="outline">
<Link
href={`/team/${teamSlug}/~/settings/invoices?status=open`}
href={`/team/${teamSlug}/~/billing/invoices?status=open`}
rel="noopener noreferrer"
target="_blank"
>
@@ -1,45 +1,18 @@
import { formatDistance } from "date-fns";
import { CircleAlertIcon } from "lucide-react";
import Image from "next/image";
import Link from "next/link";
import type { ThirdwebClient } from "thirdweb";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { Button } from "@/components/ui/button";
import type { Account, BillingCredit } from "@/hooks/useApi";
import { useLocalStorage } from "@/hooks/useLocalStorage";
import { ChainIconClient } from "@/icons/ChainIcon";
import type { BillingCredit } from "@/hooks/useApi";
import { formatToDollars } from "./formatToDollars";
interface CreditsItemProps {
credit?: BillingCredit;
isOpCreditDefault?: boolean;
onClickApply?: () => void;
twAccount: Account;
client: ThirdwebClient;
teamSlug: string;
}
export const CreditsItem: React.FC<CreditsItemProps> = ({
credit,
isOpCreditDefault,
onClickApply,
twAccount,
client,
teamSlug,
}) => {
const [hasAppliedForOpGrant] = useLocalStorage(
`appliedForOpGrant-${twAccount.id}`,
false,
);
const isOpCredit = credit?.name.startsWith("OP -") || isOpCreditDefault;
export const CreditsItem: React.FC<CreditsItemProps> = ({ credit }) => {
const isTwCredit = credit?.name.startsWith("TW -");
const isStartupCredit = credit?.name.startsWith("SU -");
let creditTitle = credit?.name ?? "thirdweb credits";
if (isOpCredit) {
creditTitle = "Optimism sponsorship credits";
} else if (isTwCredit) {
if (isTwCredit) {
creditTitle = "thirdweb credits";
} else if (isStartupCredit) {
creditTitle = "Startup grant credits";
@@ -50,13 +23,7 @@ export const CreditsItem: React.FC<CreditsItemProps> = ({
<div className="flex flex-col gap-3 p-4 lg:p-6">
<div className="relative">
<div className="absolute top-0 right-0">
{isOpCredit ? (
<ChainIconClient
className="size-6"
client={client}
src="ipfs://QmcxZHpyJa8T4i63xqjPYrZ6tKrt55tZJpbXcjSDKuKaf9/optimism/512.png"
/>
) : isTwCredit ? (
{isTwCredit ? (
<Image
alt="tw-credit"
className="size-6"
@@ -112,35 +79,7 @@ export const CreditsItem: React.FC<CreditsItemProps> = ({
</div>
)}
</div>
{hasAppliedForOpGrant && !credit && isOpCredit && (
<Alert variant="info">
<CircleAlertIcon className="size-5" />
<AlertTitle>Grant application pending approval</AlertTitle>
<AlertDescription>
You will receive an email once your application&apos;s status
changes.
</AlertDescription>
</Alert>
)}
</div>
{!hasAppliedForOpGrant && isOpCredit && (
<div className="mt-2 flex justify-end border-t px-4 py-4 lg:px-6">
<Button asChild size="sm" variant="outline">
<Link
href={`/team/${teamSlug}/~/settings/credits`}
onClick={() => {
if (onClickApply) {
onClickApply();
}
}}
>
Apply Now
</Link>
</Button>
</div>
)}
</div>
);
};
@@ -2,41 +2,26 @@
// TODO - convert to RSC
import type { ThirdwebClient } from "thirdweb";
import { type Account, useAccountCredits } from "@/hooks/useApi";
import { useAccountCredits } from "@/hooks/useApi";
import { CreditsItem } from "./CreditsItem";
export const CreditsInfoCard = (props: {
twAccount: Account;
client: ThirdwebClient;
teamSlug: string;
}) => {
export const CreditsInfoCard = () => {
const { data: credits } = useAccountCredits();
if (!credits) {
return null;
}
const opCredit = credits.find((crd) => crd.name.startsWith("OP -"));
const restCredits = credits.filter((crd) => !crd.name.startsWith("OP -"));
if (restCredits.length === 0) {
return null;
}
return (
<section className="flex flex-col gap-4">
<CreditsItem
client={props.client}
credit={opCredit}
isOpCreditDefault={true}
teamSlug={props.teamSlug}
twAccount={props.twAccount}
/>
{restCredits?.map((credit) => (
<CreditsItem
client={props.client}
credit={credit}
key={credit.couponId}
teamSlug={props.teamSlug}
twAccount={props.twAccount}
/>
{restCredits.map((credit) => (
<CreditsItem credit={credit} key={credit.couponId} />
))}
</section>
);
@@ -60,7 +60,7 @@ function BillingWarning({ teamSlug }: { teamSlug: string }) {
You have outstanding invoices. Please{" "}
<Link
className="font-medium text-amber-700 underline transition-colors hover:text-amber-900 dark:text-amber-400 dark:hover:text-amber-300"
href={`/team/${teamSlug}/~/settings/invoices?status=open`}
href={`/team/${teamSlug}/~/billing/invoices?status=open`}
>
pay them
</Link>{" "}
@@ -44,7 +44,7 @@ export const GatedSwitch: React.FC<GatedSwitchProps> = (
<div className="flex w-full flex-col gap-2">
<Button asChild className="justify-start gap-2" size="sm">
<Link
href={`/team/${props.teamSlug}/~/settings/billing?showPlans=true&highlight=${props.requiredPlan}`}
href={`/team/${props.teamSlug}/~/billing?showPlans=true&highlight=${props.requiredPlan}`}
rel="noopener noreferrer"
target="_blank"
>
@@ -53,7 +53,7 @@ export function MobileSidebar(props: {
);
}
export function useActiveSidebarLink(links: SidebarLink[]) {
function useActiveSidebarLink(links: SidebarLink[]) {
const pathname = usePathname();
const activeLink = useMemo(() => {
@@ -152,6 +152,7 @@ export function SingleNetworkSelector(props: {
disableChainId?: boolean;
align?: "center" | "start" | "end";
disableTestnets?: boolean;
disableDeprecated?: boolean;
placeholder?: string;
client: ThirdwebClient;
}) {
@@ -169,8 +170,17 @@ export function SingleNetworkSelector(props: {
chains = chains.filter((chain) => chainIdSet.has(chain.chainId));
}
if (props.disableDeprecated) {
chains = chains.filter((chain) => chain.status !== "deprecated");
}
return chains;
}, [allChains, props.chainIds, props.disableTestnets]);
}, [
allChains,
props.chainIds,
props.disableTestnets,
props.disableDeprecated,
]);
const options = useMemo(() => {
return chainsToShow.map((chain) => {
@@ -1,24 +1,7 @@
import {
Sidebar,
SidebarContent,
SidebarFooter,
SidebarGroup,
SidebarGroupContent,
SidebarGroupLabel,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
SidebarMenuSubItem,
SidebarRail,
SidebarSeparator,
SidebarTrigger,
useSidebar,
} from "@/components/ui/sidebar";
"use client";
import { cn } from "../../lib/utils";
import { AppFooter } from "../footers/app-footer";
import { NavLink } from "../ui/NavLink";
import { Separator } from "../ui/separator";
import { MobileSidebar, useActiveSidebarLink } from "./MobileSidebar";
import { MobileSidebar } from "./MobileSidebar";
import { CustomSidebar, type SidebarLink } from "./Sidebar";
export function SidebarLayout(props: {
@@ -51,129 +34,3 @@ export function SidebarLayout(props: {
</div>
);
}
export function FullWidthSidebarLayout(props: {
contentSidebarLinks: SidebarLink[];
footerSidebarLinks?: SidebarLink[];
children: React.ReactNode;
className?: string;
}) {
const { contentSidebarLinks, children, footerSidebarLinks } = props;
return (
<div
className={cn(
"relative flex w-full flex-1 overflow-y-hidden",
props.className,
)}
>
{/* left - sidebar */}
<Sidebar className="pt-2" collapsible="icon" side="left">
<SidebarContent className="p-2">
<RenderSidebarMenu links={contentSidebarLinks} />
</SidebarContent>
{footerSidebarLinks && (
<SidebarFooter className="pb-3">
<RenderSidebarMenu links={footerSidebarLinks} />
</SidebarFooter>
)}
<SidebarRail />
</Sidebar>
{/* right - content */}
<div className="flex h-full flex-grow flex-col overflow-y-auto">
<MobileSidebarTrigger
links={[...contentSidebarLinks, ...(footerSidebarLinks || [])]}
/>
<main className="flex min-w-0 grow flex-col max-sm:w-full">
{children}
</main>
<AppFooter containerClassName="max-w-7xl" />
</div>
</div>
);
}
function RenderSidebarGroup(props: {
sidebarLinks: SidebarLink[];
groupName: string;
}) {
return (
<SidebarGroup className="p-0">
<SidebarMenuItem>
<SidebarGroupLabel> {props.groupName}</SidebarGroupLabel>
<SidebarGroupContent>
<RenderSidebarMenu links={props.sidebarLinks} />
</SidebarGroupContent>
</SidebarMenuItem>
</SidebarGroup>
);
}
function RenderSidebarMenu(props: { links: SidebarLink[] }) {
const sidebar = useSidebar();
return (
<SidebarMenu className="gap-1.5">
{props.links.map((link, idx) => {
// link
if ("href" in link) {
return (
<SidebarMenuSubItem key={link.href}>
<SidebarMenuButton asChild>
<NavLink
activeClassName="text-foreground bg-accent"
className="flex items-center gap-2 text-muted-foreground text-sm hover:bg-accent"
exactMatch={link.exactMatch}
href={link.href}
isActive={link.isActive}
onClick={() => {
sidebar.setOpenMobile(false);
}}
>
{link.icon && <link.icon className="size-4" />}
<span>{link.label}</span>
</NavLink>
</SidebarMenuButton>
</SidebarMenuSubItem>
);
}
// separator
if ("separator" in link) {
return (
<SidebarSeparator
className="my-1"
key={`separator-${
// biome-ignore lint/suspicious/noArrayIndexKey: index is fine here
idx
}`}
/>
);
}
// group
return (
<RenderSidebarGroup
groupName={link.group}
key={link.group}
sidebarLinks={link.links}
/>
);
})}
</SidebarMenu>
);
}
function MobileSidebarTrigger(props: { links: SidebarLink[] }) {
const activeLink = useActiveSidebarLink(props.links);
return (
<div className="flex items-center gap-3 border-b px-4 py-4 lg:hidden">
<SidebarTrigger className="size-4" />
<Separator className="h-4" orientation="vertical" />
{activeLink && <span className="text-sm">{activeLink.label}</span>}
</div>
);
}
@@ -1,3 +1,4 @@
/** biome-ignore-all lint/a11y/useSemanticElements: FIXME */
"use client";
import {
@@ -164,7 +165,6 @@ export function TWTable<TRowData>(tableProps: TWTableProps<TRowData>) {
return (
<TableRow
key={row.id}
// biome-ignore lint/a11y/useSemanticElements: FIXME
role="group"
{...(tableProps.onRowClick
? {
@@ -45,7 +45,7 @@ export function TeamPlanBadge(props: {
}
e.stopPropagation();
e.preventDefault();
router.push(`/team/${props.teamSlug}/~/settings/billing?showPlans=true`);
router.push(`/team/${props.teamSlug}/~/billing?showPlans=true`);
}
return (
@@ -38,12 +38,18 @@ const ACCENT = {
type UpsellBannerCardProps = {
title: React.ReactNode;
description: React.ReactNode;
cta: {
text: React.ReactNode;
icon?: React.ReactNode;
target?: "_blank";
link: string;
};
cta?:
| {
text: React.ReactNode;
icon?: React.ReactNode;
target?: "_blank";
link: string;
}
| {
text: React.ReactNode;
icon?: React.ReactNode;
onClick: () => void;
};
accentColor?: keyof typeof ACCENT;
icon?: React.ReactNode;
};
@@ -93,25 +99,41 @@ export function UpsellBannerCard(props: UpsellBannerCardProps) {
</div>
</div>
<Button
asChild
className={cn(
"mt-2 gap-2 hover:translate-y-0 hover:shadow-inner sm:mt-0",
color.btn,
)}
size="sm"
>
<Link
href={props.cta.link}
rel={
props.cta.target === "_blank" ? "noopener noreferrer" : undefined
}
target={props.cta.target}
{props.cta && "target" in props.cta ? (
<Button
asChild
className={cn(
"mt-2 gap-2 hover:translate-y-0 hover:shadow-inner sm:mt-0",
color.btn,
)}
size="sm"
>
<Link
href={props.cta.link}
rel={
props.cta.target === "_blank"
? "noopener noreferrer"
: undefined
}
target={props.cta.target}
>
{props.cta.text}
{props.cta.icon && <span className="ml-2">{props.cta.icon}</span>}
</Link>
</Button>
) : props.cta && "onClick" in props.cta ? (
<Button
className={cn(
"mt-2 gap-2 hover:translate-y-0 hover:shadow-inner sm:mt-0",
color.btn,
)}
onClick={props.cta.onClick}
size="sm"
>
{props.cta.text}
{props.cta.icon && <span className="ml-2">{props.cta.icon}</span>}
</Link>
</Button>
</Button>
) : null}
</div>
</div>
);
@@ -29,6 +29,7 @@ type ThirdwebAreaChartProps<TConfig extends ChartConfig> = {
title: string;
description?: string;
titleClassName?: string;
headerClassName?: string;
};
customHeader?: React.ReactNode;
// chart config
@@ -52,6 +53,12 @@ type ThirdwebAreaChartProps<TConfig extends ChartConfig> = {
toolTipLabelFormatter?: (label: string, payload: unknown) => React.ReactNode;
toolTipValueFormatter?: (value: unknown) => React.ReactNode;
emptyChartState?: React.ReactElement;
margin?: {
top?: number;
right?: number;
bottom?: number;
left?: number;
};
};
export function ThirdwebAreaChart<TConfig extends ChartConfig>(
@@ -62,7 +69,7 @@ export function ThirdwebAreaChart<TConfig extends ChartConfig>(
return (
<Card className={props.className}>
{props.header && (
<CardHeader>
<CardHeader className={props.header.headerClassName}>
<CardTitle className={cn("mb-2", props.header.titleClassName)}>
{props.header.title}
</CardTitle>
@@ -85,7 +92,16 @@ export function ThirdwebAreaChart<TConfig extends ChartConfig>(
{props.emptyChartState}
</EmptyChartState>
) : (
<AreaChart accessibilityLayer data={props.data}>
<AreaChart
accessibilityLayer
data={props.data}
margin={{
right: props.margin?.right ?? 0,
left: props.margin?.left ?? 0,
bottom: props.margin?.bottom ?? 10,
top: props.margin?.top ?? 0,
}}
>
<CartesianGrid vertical={false} />
{props.yAxis && <YAxis axisLine={false} tickLine={false} />}
<XAxis
@@ -0,0 +1,304 @@
"use client";
import { ChevronDownIcon, ChevronRightIcon } from "lucide-react";
import { usePathname } from "next/navigation";
import { useMemo } from "react";
import { AppFooter } from "@/components/footers/app-footer";
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from "@/components/ui/collapsible";
import { DynamicHeight } from "@/components/ui/DynamicHeight";
import { NavLink } from "@/components/ui/NavLink";
import { Separator } from "@/components/ui/separator";
import {
Sidebar,
SidebarContent,
SidebarFooter,
SidebarGroup,
SidebarGroupContent,
SidebarGroupLabel,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
SidebarMenuSub,
SidebarMenuSubItem,
SidebarRail,
SidebarSeparator,
SidebarTrigger,
useSidebar,
} from "@/components/ui/sidebar";
import { cn } from "@/lib/utils";
type ShadcnSidebarBaseLink = {
href: string;
label: React.ReactNode;
exactMatch?: boolean;
icon?: React.FC<{ className?: string }>;
isActive?: (pathname: string) => boolean;
};
type ShadcnSidebarLink =
| ShadcnSidebarBaseLink
| {
group: string;
links: ShadcnSidebarBaseLink[];
}
| {
separator: true;
}
| {
subMenu: Omit<ShadcnSidebarBaseLink, "href" | "exactMatch">;
links: Omit<ShadcnSidebarBaseLink, "icon">[];
};
export function FullWidthSidebarLayout(props: {
contentSidebarLinks: ShadcnSidebarLink[];
footerSidebarLinks?: ShadcnSidebarLink[];
children: React.ReactNode;
className?: string;
}) {
const { contentSidebarLinks, children, footerSidebarLinks } = props;
return (
<div
className={cn(
"relative flex w-full flex-1 overflow-y-hidden",
props.className,
)}
>
{/* left - sidebar */}
<Sidebar className="pt-2" collapsible="icon" side="left">
<SidebarContent className="p-2">
<RenderSidebarMenu links={contentSidebarLinks} />
</SidebarContent>
{footerSidebarLinks && (
<SidebarFooter className="pb-3">
<RenderSidebarMenu links={footerSidebarLinks} />
</SidebarFooter>
)}
<SidebarRail />
</Sidebar>
{/* right - content */}
<div className="flex h-full flex-grow flex-col overflow-y-auto">
<MobileSidebarTrigger
links={[...contentSidebarLinks, ...(footerSidebarLinks || [])]}
/>
<main className="flex min-w-0 grow flex-col max-sm:w-full">
{children}
</main>
<AppFooter containerClassName="max-w-7xl" />
</div>
</div>
);
}
function MobileSidebarTrigger(props: { links: ShadcnSidebarLink[] }) {
const activeLink = useActiveShadcnSidebarLink(props.links);
const parentSubNav = props.links.find(
(link) =>
"subMenu" in link && link.links.some((l) => l.href === activeLink?.href),
);
return (
<div className="flex items-center gap-3 border-b px-4 py-4 lg:hidden">
<SidebarTrigger className="size-4" />
<Separator
className="h-4 bg-muted-foreground/50"
orientation="vertical"
/>
{parentSubNav && "subMenu" in parentSubNav && (
<>
<span className="text-sm">{parentSubNav.subMenu.label}</span>
<ChevronRightIcon className="size-4 text-muted-foreground/50 -mx-1.5" />
</>
)}
{activeLink && <span className="text-sm">{activeLink.label}</span>}
</div>
);
}
function useActiveShadcnSidebarLink(links: ShadcnSidebarLink[]) {
const pathname = usePathname();
const activeLink = useMemo(() => {
function isActive(link: ShadcnSidebarBaseLink) {
if (link.exactMatch) {
return link.href === pathname;
}
return pathname?.startsWith(link.href);
}
for (const link of links) {
if ("links" in link) {
for (const subLink of link.links) {
if (isActive(subLink)) {
return subLink;
}
}
} else if ("href" in link) {
if (isActive(link)) {
return link;
}
}
}
}, [links, pathname]);
return activeLink;
}
function useIsSubnavActive(links: ShadcnSidebarBaseLink[]) {
const pathname = usePathname();
const isSubnavActive = useMemo(() => {
function isActive(link: ShadcnSidebarBaseLink) {
if (link.exactMatch) {
return link.href === pathname;
}
return pathname?.startsWith(link.href);
}
return links.some(isActive);
}, [links, pathname]);
return isSubnavActive;
}
function RenderSidebarGroup(props: {
sidebarLinks: ShadcnSidebarLink[];
groupName: string;
}) {
return (
<SidebarGroup className="p-0">
<SidebarMenuItem>
<SidebarGroupLabel> {props.groupName}</SidebarGroupLabel>
<SidebarGroupContent>
<RenderSidebarMenu links={props.sidebarLinks} />
</SidebarGroupContent>
</SidebarMenuItem>
</SidebarGroup>
);
}
function RenderSidebarSubmenu(props: {
links: ShadcnSidebarBaseLink[];
subMenu: Omit<ShadcnSidebarBaseLink, "href" | "exactMatch">;
}) {
const sidebar = useSidebar();
const isSubnavActive = useIsSubnavActive(props.links);
return (
<SidebarMenu>
<DynamicHeight transition="height 200ms ease">
<Collapsible className="group/collapsible" defaultOpen={isSubnavActive}>
<SidebarMenuItem>
<CollapsibleTrigger asChild>
<SidebarMenuButton className="text-sm text-muted-foreground whitespace-nowrap">
{props.subMenu.icon && (
<props.subMenu.icon className="size-4" />
)}
<span>{props.subMenu.label}</span>
<ChevronDownIcon className="ml-auto group-[[data-state=open]]/collapsible:rotate-180 transition-transform" />
</SidebarMenuButton>
</CollapsibleTrigger>
<CollapsibleContent className="mt-0.5">
<SidebarMenuSub className="pr-0 pl-2">
{props.links.map((link) => {
return (
<SidebarMenuSubItem key={link.href}>
<NavLink
activeClassName="text-foreground bg-accent "
className="flex items-center gap-2 text-muted-foreground text-sm hover:bg-accent hover:text-foreground px-2 py-1.5 rounded-lg w-full"
exactMatch={link.exactMatch}
href={link.href}
isActive={link.isActive}
onClick={() => {
sidebar.setOpenMobile(false);
}}
>
{link.icon && <link.icon className="size-4" />}
<span className="whitespace-nowrap">{link.label}</span>
</NavLink>
</SidebarMenuSubItem>
);
})}
</SidebarMenuSub>
</CollapsibleContent>
</SidebarMenuItem>
</Collapsible>
</DynamicHeight>
</SidebarMenu>
);
}
function RenderSidebarMenu(props: { links: ShadcnSidebarLink[] }) {
const sidebar = useSidebar();
return (
<SidebarMenu className="gap-1.5">
{props.links.map((link, idx) => {
// link
if ("href" in link) {
return (
<SidebarMenuSubItem key={link.href}>
<SidebarMenuButton asChild>
<NavLink
activeClassName="text-foreground bg-accent"
className="flex items-center gap-2 text-muted-foreground text-sm hover:bg-accent"
exactMatch={link.exactMatch}
href={link.href}
isActive={link.isActive}
onClick={() => {
sidebar.setOpenMobile(false);
}}
>
{link.icon && <link.icon className="size-4" />}
<span>{link.label}</span>
</NavLink>
</SidebarMenuButton>
</SidebarMenuSubItem>
);
}
// separator
if ("separator" in link) {
return (
<SidebarSeparator
className="my-1"
key={`separator-${
// biome-ignore lint/suspicious/noArrayIndexKey: index is fine here
idx
}`}
/>
);
}
// subnav
if ("subMenu" in link) {
return (
<RenderSidebarSubmenu
key={`submenu_$${
// biome-ignore lint/suspicious/noArrayIndexKey: index is fine here
idx
}`}
links={link.links}
subMenu={link.subMenu}
/>
);
}
// group
return (
<RenderSidebarGroup
groupName={link.group}
key={link.group}
sidebarLinks={link.links}
/>
);
})}
</SidebarMenu>
);
}
@@ -18,7 +18,7 @@ export type MultiStepState<T extends string> = {
}
| {
type: "error";
message: React.ReactNode;
message: string;
};
label: string;
description?: string;
@@ -27,6 +27,10 @@ export type MultiStepState<T extends string> = {
export function MultiStepStatus<T extends string>(props: {
steps: MultiStepState<T>[];
onRetry: (step: MultiStepState<T>) => void;
renderError?: (
step: MultiStepState<T>,
errorMessage: string,
) => React.ReactNode;
}) {
return (
<DynamicHeight>
@@ -66,22 +70,24 @@ export function MultiStepStatus<T extends string>(props: {
</p>
)}
{step.status.type === "error" && (
<div className="mt-1 space-y-2">
<p className="mb-1 text-red-500 text-sm">
{step.status.message}
</p>
<Button
className="gap-2"
onClick={() => props.onRetry(step)}
size="sm"
variant="destructive"
>
<RefreshCwIcon className="size-4" />
Retry
</Button>
</div>
)}
{step.status.type === "error"
? props.renderError?.(step, step.status.message) || (
<div className="mt-1 space-y-2">
<p className="mb-1 text-red-500 text-sm">
{step.status.message}
</p>
<Button
className="gap-2"
onClick={() => props.onRetry(step)}
size="sm"
variant="destructive"
>
<RefreshCwIcon className="size-4" />
Retry
</Button>
</div>
)
: null}
</div>
</div>
))}
@@ -48,7 +48,7 @@ export const PaginationButtons = (props: {
<PaginationContent>
<PaginationItem>
<PaginationPrevious
className="rounded-full"
className="rounded-full lg:min-w-[38px]"
disabled={activePage === 1}
onClick={() => {
setPage(activePage - 1);
@@ -57,7 +57,7 @@ export const PaginationButtons = (props: {
</PaginationItem>
<PaginationItem>
<PaginationNext
className="rounded-full"
className="rounded-full lg:min-w-[38px]"
disabled={activePage === totalPages}
onClick={() => {
setPage(activePage + 1);
@@ -78,7 +78,7 @@ export const PaginationButtons = (props: {
{pages.map((page) => (
<PaginationItem key={page}>
<PaginationLink
className="rounded-full"
className="rounded-full lg:min-w-[38px]"
isActive={activePage === page}
onClick={() => {
setPage(page);
@@ -98,7 +98,7 @@ export const PaginationButtons = (props: {
<PaginationContent>
<PaginationItem>
<PaginationPrevious
className="rounded-full"
className="rounded-full lg:min-w-[38px]"
disabled={activePage === 1}
onClick={() => {
setPage(activePage - 1);
@@ -111,7 +111,7 @@ export const PaginationButtons = (props: {
<>
<PaginationItem>
<PaginationLink
className="rounded-full"
className="rounded-full lg:min-w-[38px]"
onClick={() => {
setPage(1);
}}
@@ -129,7 +129,7 @@ export const PaginationButtons = (props: {
{activePage - 1 > 0 && (
<PaginationItem className="max-sm:hidden">
<PaginationLink
className="rounded-full"
className="rounded-full lg:min-w-[38px]"
onClick={() => {
setPage(activePage - 1);
}}
@@ -140,7 +140,7 @@ export const PaginationButtons = (props: {
)}
<PaginationItem>
<PaginationLink className="rounded-full" isActive>
<PaginationLink className="rounded-full lg:min-w-[38px]" isActive>
{activePage}
</PaginationLink>
</PaginationItem>
@@ -148,7 +148,7 @@ export const PaginationButtons = (props: {
{activePage + 1 <= totalPages && (
<PaginationItem className="max-sm:hidden">
<PaginationLink
className="rounded-full"
className="rounded-full lg:min-w-[38px]"
onClick={() => {
setPage(activePage + 1);
}}
@@ -167,7 +167,7 @@ export const PaginationButtons = (props: {
<PaginationItem>
<PaginationLink
className="rounded-full"
className="rounded-full lg:min-w-[38px]"
onClick={() => {
setPage(totalPages);
}}
@@ -180,7 +180,7 @@ export const PaginationButtons = (props: {
<PaginationItem>
<PaginationNext
className="rounded-full"
className="rounded-full lg:min-w-[38px]"
disabled={activePage === totalPages}
onClick={() => {
setPage(activePage + 1);
@@ -1,3 +1,4 @@
/** biome-ignore-all lint/a11y/useSemanticElements: FIXME */
"use client";
import { CheckIcon, ChevronDownIcon, SearchIcon } from "lucide-react";
@@ -190,7 +191,6 @@ export const SelectWithSearch = React.forwardRef<
ref={
i === optionsToShow.length - 1 ? lastItemRef : undefined
}
// biome-ignore lint/a11y/useSemanticElements: TDOO
role="option"
variant="ghost"
>
@@ -1,6 +1,6 @@
"use client";
import { CrownIcon, LockIcon, SparklesIcon } from "lucide-react";
import { CrownIcon, LockIcon } from "lucide-react";
import Link from "next/link";
import type React from "react";
import type { Team } from "@/api/team";
@@ -111,18 +111,15 @@ export function UpsellContent(props: {
<CardContent className="space-y-6">
{props.benefits && props.benefits.length > 0 && (
<div className="space-y-3">
<h4 className="font-semibold text-muted-foreground text-sm uppercase tracking-wide">
What you'll get:
<h4 className="font-semibold text-foreground text-sm capitalize text-center">
What you'll get
</h4>
<div className="grid gap-2">
<div className="grid gap-1.5">
{props.benefits.map((benefit) => (
<div
className="flex items-center gap-3"
className="flex items-center justify-center gap-3 text-center text-balance"
key={benefit.description}
>
<div className="flex h-5 w-5 flex-shrink-0 items-center justify-center rounded-full bg-accent">
<SparklesIcon className="h-3 w-3 text-success-text" />
</div>
<span className="text-sm">{benefit.description}</span>
{benefit.status === "soon" && (
<Badge className="text-xs" variant="secondary">
@@ -138,7 +135,7 @@ export function UpsellContent(props: {
<div className="flex flex-col gap-3 pt-4 sm:flex-row">
<Button asChild className="flex-1 py-3 font-semibold" size="lg">
<Link
href={`/team/${props.teamSlug}/~/settings/billing?showPlans=true&highlight=${props.requiredPlan}`}
href={`/team/${props.teamSlug}/~/billing?showPlans=true&highlight=${props.requiredPlan}`}
>
<CrownIcon className="mr-2 h-4 w-4" />
Upgrade to{" "}
@@ -146,9 +143,7 @@ export function UpsellContent(props: {
</Link>
</Button>
<Button asChild className="md:flex-1" size="lg" variant="outline">
<Link
href={`/team/${props.teamSlug}/~/settings/billing?showPlans=true`}
>
<Link href={`/team/${props.teamSlug}/~/billing?showPlans=true`}>
View All Plans
</Link>
</Button>
@@ -59,7 +59,11 @@ export function WalletAddress(props: {
// special case for zero address
if (address === ZERO_ADDRESS) {
return <span className="cursor-pointer font-mono">{shortenedAddress}</span>;
return (
<span className={cn("cursor-pointer font-mono", props.className)}>
{shortenedAddress}
</span>
);
}
return (
@@ -116,13 +116,12 @@ export function ChatBar(props: {
) : (
<Button
aria-label="Send"
className="!h-auto w-auto border border-nebula-pink-foreground p-2 disabled:opacity-100"
className="!h-auto w-auto p-2 disabled:opacity-100"
disabled={message.trim() === "" || props.isConnectingWallet}
onClick={() => {
if (message.trim() === "") return;
handleSubmit(message);
}}
variant="pink"
>
<ArrowUpIcon className="size-4" />
</Button>
@@ -1,8 +1,10 @@
"use client";
import { MessageCircleIcon, XIcon } from "lucide-react";
import { useSelectedLayoutSegments } from "next/navigation";
import { useCallback, useRef, useState } from "react";
import { createThirdwebClient } from "thirdweb";
import type { Team } from "@/api/team";
import { Button } from "@/components/ui/button";
import { NEXT_PUBLIC_DASHBOARD_CLIENT_ID } from "@/constants/public-envs";
import { cn } from "@/lib/utils";
@@ -14,22 +16,22 @@ const client = createThirdwebClient({
});
export function CustomChatButton(props: {
isLoggedIn: boolean;
networks: "mainnet" | "testnet" | "all" | null;
isFloating: boolean;
pageType: "chain" | "contract" | "support";
label: string;
examplePrompts: string[];
authToken: string | undefined;
teamId: string | undefined;
authToken: string;
team: Team;
clientId: string | undefined;
requireLogin?: boolean;
}) {
const layoutSegments = useSelectedLayoutSegments();
const [isOpen, setIsOpen] = useState(false);
const [hasBeenOpened, setHasBeenOpened] = useState(false);
const closeModal = useCallback(() => setIsOpen(false), []);
const ref = useRef<HTMLDivElement>(null);
if (layoutSegments[0] === "~" && layoutSegments[1] === "support") {
return null;
}
return (
<>
{/* Inline Button (not floating) */}
@@ -54,14 +56,14 @@ export function CustomChatButton(props: {
ref={ref}
>
{/* Header with close button */}
<div className="flex items-center justify-between border-b px-4 py-2">
<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}
</div>
<Button
aria-label="Close chat"
className="h-auto w-auto p-1 text-muted-foreground"
className="h-auto w-auto p-1 text-muted-foreground rounded-full"
onClick={closeModal}
size="icon"
variant="ghost"
@@ -80,9 +82,7 @@ export function CustomChatButton(props: {
message: prompt,
title: prompt,
}))}
networks={props.networks}
requireLogin={props.requireLogin}
teamId={props.teamId}
team={props.team}
/>
)}
</div>
@@ -1,44 +1,36 @@
"use client";
import { ArrowRightIcon } from "lucide-react";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { useCallback, useState } from "react";
import type { ThirdwebClient } from "thirdweb";
import { useActiveWalletConnectionStatus } from "thirdweb/react";
import type { Team } from "@/api/team";
import { Button } from "@/components/ui/button";
import { NebulaIcon } from "@/icons/NebulaIcon";
import { ThirdwebMiniLogo } from "../../../app/(app)/components/ThirdwebMiniLogo";
import { ChatBar } from "./ChatBar";
import type { UserMessage, UserMessageContent } from "./CustomChats";
import { type CustomChatMessage, CustomChats } from "./CustomChats";
import type { ExamplePrompt, NebulaContext } from "./types";
import type { ExamplePrompt } from "./types";
export default function CustomChatContent(props: {
authToken: string | undefined;
teamId: string | undefined;
team: Team;
clientId: string | undefined;
client: ThirdwebClient;
examplePrompts: ExamplePrompt[];
networks: NebulaContext["networks"];
requireLogin?: boolean;
}) {
if (props.requireLogin !== false && !props.authToken) {
return <LoggedOutStateChatContent />;
}
return (
<CustomChatContentLoggedIn
authToken={props.authToken || ""}
client={props.client}
clientId={props.clientId}
examplePrompts={props.examplePrompts}
teamId={props.teamId}
team={props.team}
/>
);
}
function CustomChatContentLoggedIn(props: {
authToken: string;
teamId: string | undefined;
team: Team;
clientId: string | undefined;
client: ThirdwebClient;
examplePrompts: ExamplePrompt[];
@@ -55,6 +47,9 @@ function CustomChatContentLoggedIn(props: {
const [enableAutoScroll, setEnableAutoScroll] = useState(false);
const connectionStatus = useActiveWalletConnectionStatus();
const [showSupportForm, setShowSupportForm] = useState(false);
const [productLabel, setProductLabel] = useState("");
const handleSendMessage = useCallback(
async (userMessage: UserMessage) => {
const abortController = new AbortController();
@@ -96,7 +91,7 @@ function CustomChatContentLoggedIn(props: {
headers: {
Authorization: `Bearer ${props.authToken}`,
"Content-Type": "application/json",
...(props.teamId ? { "x-team-id": props.teamId } : {}),
"x-team-id": props.team.id,
...(props.clientId ? { "x-client-id": props.clientId } : {}),
},
method: "POST",
@@ -132,7 +127,7 @@ function CustomChatContentLoggedIn(props: {
setEnableAutoScroll(false);
}
},
[props.authToken, props.clientId, props.teamId, sessionId],
[props.authToken, props.clientId, props.team.id, sessionId],
);
const handleFeedback = useCallback(
@@ -165,7 +160,7 @@ function CustomChatContentLoggedIn(props: {
headers: {
Authorization: `Bearer ${props.authToken}`,
"Content-Type": "application/json",
...(props.teamId ? { "x-team-id": props.teamId } : {}),
"x-team-id": props.team.id,
},
method: "POST",
});
@@ -188,9 +183,20 @@ function CustomChatContentLoggedIn(props: {
// Consider implementing retry logic here
}
},
[sessionId, props.authToken, props.teamId, messages],
[sessionId, props.authToken, props.team.id, messages],
);
const handleAddSuccessMessage = useCallback((message: string) => {
setMessages((prev) => [
...prev,
{
type: "assistant" as const,
text: message,
request_id: undefined,
},
]);
}, []);
const showEmptyState = !userHasSubmittedMessage && messages.length === 0;
return (
<div className="flex grow flex-col overflow-hidden">
@@ -212,6 +218,12 @@ function CustomChatContentLoggedIn(props: {
sessionId={sessionId}
setEnableAutoScroll={setEnableAutoScroll}
useSmallText
showSupportForm={showSupportForm}
setShowSupportForm={setShowSupportForm}
productLabel={productLabel}
setProductLabel={setProductLabel}
team={props.team}
addSuccessMessage={handleAddSuccessMessage}
/>
)}
<ChatBar
@@ -245,42 +257,6 @@ function CustomChatContentLoggedIn(props: {
);
}
function LoggedOutStateChatContent() {
const pathname = usePathname();
return (
<div className="flex grow flex-col items-center justify-center p-4">
<div className="mb-4 flex justify-center">
<div className="rounded-full border p-1">
<div className="rounded-full border bg-card p-2">
<NebulaIcon className="size-7 text-muted-foreground" />
</div>
</div>
</div>
<h1 className="px-4 text-center font-semibold text-3xl tracking-tight md:text-4xl">
How can I help you <br className="max-sm:hidden" />
today?
</h1>
<div className="h-3" />
<p className="text-base text-muted-foreground">
Sign in to use AI Assistant
</p>
<div className="h-5" />
<Button asChild>
<Link
className="w-full max-w-96 gap-2"
href={`/login?next=${encodeURIComponent(pathname)}`}
>
Sign in
<ArrowRightIcon className="size-4" />
</Link>
</Button>
</div>
);
}
function EmptyStateChatPageContent(props: {
sendMessage: (message: UserMessage) => void;
examplePrompts: { title: string; message: string }[];
@@ -288,9 +264,12 @@ function EmptyStateChatPageContent(props: {
return (
<div className="flex flex-1 flex-col items-center justify-center overflow-auto p-4 ">
<div className="mb-4 flex justify-center">
<div className="rounded-full border-[1.5px] border-nebula-pink-foreground/20 bg-[hsl(var(--nebula-pink-foreground)/5%)] p-1">
<div className="rounded-full border-[1.5px] border-nebula-pink-foreground/40 bg-[hsl(var(--nebula-pink-foreground)/5%)] p-2">
<NebulaIcon className="size-7 text-nebula-pink-foreground" />
<div className="rounded-full border p-1 bg-muted/20">
<div className="rounded-full border p-2 bg-inverted">
<ThirdwebMiniLogo
isMonoChrome
className="size-7 text-inverted-foreground"
/>
</div>
</div>
</div>
@@ -301,11 +280,12 @@ function EmptyStateChatPageContent(props: {
</h1>
<div className="h-6" />
<div className="flex max-w-lg flex-col flex-wrap justify-center gap-2.5">
<div className="flex flex-col items-center justify-center gap-2.5 overflow-hidden">
{props.examplePrompts.map((prompt) => (
<Button
disabled={false}
key={prompt.title}
className="rounded-full text-xs sm:text-sm truncate bg-card w-fit h-auto py-1.5 whitespace-pre-wrap"
onClick={() =>
props.sendMessage({
content: [
@@ -1,15 +1,26 @@
import {
AlertCircleIcon,
MessageCircleIcon,
ArrowRightIcon,
ThumbsDownIcon,
ThumbsUpIcon,
} from "lucide-react";
import { useEffect, useRef, useState } from "react";
import type { ThirdwebClient } from "thirdweb";
import type { Team } from "@/api/team";
import { MarkdownRenderer } from "@/components/blocks/markdown-renderer";
import { DynamicHeight } from "@/components/ui/DynamicHeight";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { ScrollShadow } from "@/components/ui/ScrollShadow/ScrollShadow";
import { cn } from "@/lib/utils";
import { Reasoning } from "./Reasoning";
import { ThirdwebMiniLogo } from "../../../app/(app)/components/ThirdwebMiniLogo";
import { SupportTicketForm } from "../../../app/(app)/team/[team_slug]/(team)/~/support/_components/SupportTicketForm";
import { Button } from "../ui/button";
import { TextShimmer } from "../ui/text-shimmer";
// Define local types
export type UserMessageContent = { type: "text"; text: string };
@@ -46,11 +57,18 @@ export function CustomChats(props: {
enableAutoScroll: boolean;
useSmallText?: boolean;
sendMessage: (message: UserMessage) => void;
onFeedback?: (messageIndex: number, feedback: 1 | -1) => void;
onFeedback?: (messageIndex: number, feedback: 1 | -1) => Promise<void>;
showSupportForm: boolean;
setShowSupportForm: (v: boolean) => void;
productLabel: string;
setProductLabel: (v: string) => void;
team: Team;
addSuccessMessage?: (message: string) => void;
}) {
const { messages, setEnableAutoScroll, enableAutoScroll } = props;
const scrollAnchorRef = useRef<HTMLDivElement>(null);
const chatContainerRef = useRef<HTMLDivElement>(null);
const [supportTicketCreated, setSupportTicketCreated] = useState(false);
// auto scroll to bottom when messages change
// eslint-disable-next-line no-restricted-syntax
@@ -86,6 +104,8 @@ export function CustomChats(props: {
chatScrollContainer.addEventListener("wheel", disableScroll);
}, [setEnableAutoScroll, enableAutoScroll]);
const [showSupportFormDialog, setShowSupportFormDialog] = useState(false);
return (
<div
className="relative flex max-h-full flex-1 flex-col overflow-hidden"
@@ -123,6 +143,66 @@ export function CustomChats(props: {
sendMessage={props.sendMessage}
sessionId={props.sessionId}
/>
{/* Support Case Button/Form in last assistant message */}
{message.type === "assistant" &&
index === props.messages.length - 1 && (
<>
{/* Only show button/form if ticket not created */}
{!supportTicketCreated && (
<div className="mt-3 pl-12">
<Dialog
open={showSupportFormDialog}
onOpenChange={setShowSupportFormDialog}
>
<Button
onClick={() => setShowSupportFormDialog(true)}
size="sm"
variant="outline"
className="rounded-full bg-card"
>
Create Support Case
<ArrowRightIcon className="w-4 h-4 ml-2" />
</Button>
<DialogContent className="p-0 bg-card">
<DynamicHeight>
<DialogHeader className="p-4 lg:p-6 border-b border-dashed space-y-1">
<DialogTitle className="text-xl font-semibold text-foreground tracking-tight">
Create Support Case
</DialogTitle>
<p className="text-muted-foreground text-sm">
Let's create a detailed support case for
our technical team.
</p>
</DialogHeader>
<SupportTicketForm
team={props.team}
productLabel={props.productLabel}
setProductLabel={props.setProductLabel}
conversationId={props.sessionId}
closeForm={() => {
setShowSupportFormDialog(false);
}}
onSuccess={() => {
props.setShowSupportForm(false);
props.setProductLabel("");
setSupportTicketCreated(true);
// Add success message as a regular assistant message
if (props.addSuccessMessage) {
const supportPortalUrl = `/team/${props.team.slug}/~/support`;
props.addSuccessMessage(
`Your support ticket has been created! Our team will get back to you soon. You can also visit the [support portal](${supportPortalUrl}) to track your case.`,
);
}
}}
/>
</DynamicHeight>
</DialogContent>
</Dialog>
</div>
)}
</>
)}
</div>
);
})}
@@ -143,7 +223,7 @@ function RenderMessage(props: {
nextMessage: CustomChatMessage | undefined;
authToken: string;
sessionId: string | undefined;
onFeedback?: (messageIndex: number, feedback: 1 | -1) => void;
onFeedback?: (messageIndex: number, feedback: 1 | -1) => Promise<void>;
}) {
const { message } = props;
@@ -179,13 +259,16 @@ function RenderMessage(props: {
<div
className={cn(
"flex size-9 items-center justify-center rounded-full",
message.type === "assistant" && "border bg-card",
message.type === "assistant" && "border bg-inverted",
message.type === "error" && "border",
message.type === "presence" && "border bg-card",
message.type === "presence" && "border bg-inverted",
)}
>
{(message.type === "presence" || message.type === "assistant") && (
<MessageCircleIcon className="size-5 text-muted-foreground" />
<ThirdwebMiniLogo
className="size-4 text-inverted-foreground"
isMonoChrome
/>
)}
{message.type === "error" && (
@@ -227,7 +310,7 @@ function RenderMessage(props: {
function CustomFeedbackButtons(props: {
message: CustomChatMessage & { type: "assistant" };
messageIndex: number;
onFeedback: (messageIndex: number, feedback: 1 | -1) => void;
onFeedback: (messageIndex: number, feedback: 1 | -1) => Promise<void>;
className?: string;
}) {
const [isSubmitting, setIsSubmitting] = useState(false);
@@ -240,9 +323,9 @@ function CustomFeedbackButtons(props: {
await props.onFeedback(props.messageIndex, feedback);
} catch (_e) {
// Handle error silently
} finally {
setIsSubmitting(false);
}
setIsSubmitting(false);
};
// Don't show buttons if feedback already given
@@ -251,25 +334,26 @@ function CustomFeedbackButtons(props: {
}
return (
<div className={cn("flex gap-2", props.className)}>
<button
aria-label="Thumbs up"
className="text-muted-foreground transition-colors hover:text-green-500 disabled:opacity-50"
<div className={cn("flex gap-1.5", props.className)}>
<Button
aria-label="Upvote"
variant="outline"
disabled={isSubmitting}
onClick={() => handleFeedback(1)}
type="button"
className="size-8 p-0 rounded-lg bg-card"
>
<ThumbsUpIcon className="size-5" />
</button>
<button
aria-label="Thumbs down"
className="text-muted-foreground transition-colors hover:text-red-500 disabled:opacity-50"
<ThumbsUpIcon className="size-3.5" />
</Button>
<Button
aria-label="Downvote"
variant="outline"
disabled={isSubmitting}
onClick={() => handleFeedback(-1)}
type="button"
className="size-8 p-0 rounded-lg bg-card"
>
<ThumbsDownIcon className="size-5" />
</button>
<ThumbsDownIcon className="size-3.5" />
</Button>
</div>
);
}
@@ -296,7 +380,11 @@ function RenderResponse(props: {
);
case "presence":
return <Reasoning isPending={isMessagePending} texts={message.texts} />;
return (
<div className="h-8 flex items-center">
<TextShimmer text="Reasoning..." />
</div>
);
case "error":
return (
@@ -325,19 +413,15 @@ function StyledMarkdownRenderer(props: {
}) {
return (
<MarkdownRenderer
className="text-foreground [&>*:first-child]:mt-0 [&>*:first-child]:border-none [&>*:first-child]:pb-0 [&>*:last-child]:mb-0"
className="[&>*:first-child]:mt-0 [&>*:first-child]:border-none [&>*:first-child]:pb-0 [&>*:last-child]:mb-0"
code={{
className: "bg-transparent",
className: "bg-card",
ignoreFormattingErrors: true,
}}
inlineCode={{ className: "border-none" }}
li={{ className: "text-foreground" }}
markdownText={props.text}
p={{
className:
props.type === "assistant"
? "text-foreground"
: "text-foreground leading-normal",
className: props.type === "assistant" ? "" : "leading-normal",
}}
skipHtml
/>
@@ -1,62 +0,0 @@
import { ChevronDownIcon } from "lucide-react";
import { useState } from "react";
import { Button } from "@/components/ui/button";
import { DynamicHeight } from "@/components/ui/DynamicHeight";
import { TextShimmer } from "@/components/ui/text-shimmer";
import { cn } from "@/lib/utils";
export function Reasoning(props: { isPending: boolean; texts: string[] }) {
const [_isOpen, setIsOpen] = useState(false);
const isOpen = props.isPending ? true : _isOpen;
const showAll = !props.isPending;
const lastText = props.texts[props.texts.length - 1];
return (
<DynamicHeight>
<Button
className="h-auto w-auto items-center gap-1 px-0 py-1 pb-2 font-normal text-base text-muted-foreground hover:text-foreground hover:no-underline"
onClick={() => setIsOpen(!isOpen)}
size="sm"
variant="link"
>
{props.isPending ? (
<TextShimmer text="Reasoning..." />
) : (
<span className="text-foreground">Reasoning</span>
)}
{!props.isPending && (
<ChevronDownIcon
className={cn(
"size-4 transition-transform",
isOpen && "rotate-180",
)}
/>
)}
</Button>
{isOpen && (
<>
{showAll && props.texts.length > 0 && (
<ul className="list-none space-y-1.5">
{props.texts.map((text) => (
<li
className="fade-in-0 animate-in text-muted-foreground text-sm leading-relaxed duration-300"
key={text}
>
{text.trim()}
</li>
))}
</ul>
)}
{!showAll && lastText && (
<div className="text-muted-foreground text-sm leading-relaxed">
{lastText.trim()}
</div>
)}
</>
)}
</DynamicHeight>
);
}
@@ -6,12 +6,6 @@ export type ExamplePrompt = {
// TODO - remove "Nebula" wording and simplify types
export type NebulaContext = {
chainIds: string[] | null;
walletAddress: string | null;
networks: "mainnet" | "testnet" | "all" | null;
};
type NebulaUserMessageContentItem =
| {
type: "image";
@@ -1,7 +1,6 @@
"use client";
import { useFavoriteChainIds } from "@app/(dashboard)/(chain)/components/client/star-button";
import { doLogout } from "@app/login/auth-actions";
import Image from "next/image";
import Link from "next/link";
import { usePathname } from "next/navigation";
@@ -14,6 +13,7 @@ import {
useActiveAccount,
useConnectModal,
} from "thirdweb/react";
import { doLogout } from "@/actions/auth-actions";
import { resetAnalytics } from "@/analytics/reset";
import { CustomChainRenderer } from "@/components/misc/CustomChainRenderer";
import { LazyConfigureNetworkModal } from "@/components/misc/configure-networks/LazyConfigureNetworkModal";
@@ -120,20 +120,18 @@ export const CustomConnectWallet = (props: {
if ((!isLoggedIn || !account) && loginRequired) {
return (
<>
<Button
asChild
className={props.signInLinkButtonClassName}
size="lg"
variant="default"
<Button
asChild
className={props.signInLinkButtonClassName}
size="lg"
variant="default"
>
<Link
href={`/login${pathname ? `?next=${encodeURIComponent(pathname)}` : ""}`}
>
<Link
href={`/login${pathname ? `?next=${encodeURIComponent(pathname)}` : ""}`}
>
Connect Wallet
</Link>
</Button>
</>
Connect Wallet
</Link>
</Button>
);
}
@@ -3,7 +3,8 @@
import type { StaticImageData } from "next/image";
import Image from "next/image";
import type { FetchDeployMetadataResult } from "thirdweb/contract";
import { serverThirdwebClient } from "@/constants/thirdweb-client.server";
import { DASHBOARD_THIRDWEB_SECRET_KEY } from "@/constants/server-envs";
import { getConfiguredThirdwebClient } from "@/constants/thirdweb.server";
import { replaceIpfsUrl } from "@/lib/sdk";
import generalContractIcon from "../../../../../public/assets/tw-icons/general.png";
@@ -26,7 +27,17 @@ export const ContractIdImage: React.FC<ContractIdImageProps> = ({
<img
alt=""
className="size-8 rounded-full"
src={replaceIpfsUrl(logo, serverThirdwebClient)}
src={
DASHBOARD_THIRDWEB_SECRET_KEY
? replaceIpfsUrl(
logo,
getConfiguredThirdwebClient({
secretKey: DASHBOARD_THIRDWEB_SECRET_KEY,
teamId: undefined,
}),
)
: logo
}
/>
);
}
@@ -314,7 +314,7 @@ const NetworkFilterCell = React.memo(function NetworkFilterCell({
client: ThirdwebClient;
}) {
if (chainIds.length < 2) {
return <> NETWORK </>;
return "NETWORK";
}
return (
@@ -46,9 +46,9 @@ function AnnouncementBannerUI(props: {
export function AnnouncementBanner() {
return (
<AnnouncementBannerUI
href="https://nebula.thirdweb.com/"
label="Announcing Nebula App — the most powerful AI for blockchains! Try it now!"
trackingLabel="nebula-beta-launch"
href="https://blog.thirdweb.com/the-fastest-way-to-build-web3-applications/"
label="We have re-branded our Engine, Payments, and Connect products. Please read the full blog post for details on changes"
trackingLabel="product-rebrand"
/>
);
}
@@ -38,12 +38,10 @@ import { createProjectClient } from "@/hooks/useApi";
import { useDashboardRouter } from "@/lib/DashboardRouter";
import { projectDomainsSchema, projectNameSchema } from "@/schema/validations";
import { toArrFromList } from "@/utils/string";
import { createVaultAccountAndAccessToken } from "../../../../app/(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions/lib/vault.client";
const ALL_PROJECT_SERVICES = SERVICES.filter(
(srv) =>
srv.name !== "relayer" &&
srv.name !== "chainsaw" &&
srv.name !== "engineCloud", // TODO enable once API server is out
(srv) => srv.name !== "relayer" && srv.name !== "chainsaw",
);
export type CreateProjectPrefillOptions = {
@@ -66,6 +64,16 @@ const CreateProjectDialog = (props: CreateProjectDialogProps) => {
<CreateProjectDialogUI
createProject={async (params) => {
const res = await createProjectClient(props.teamId, params);
await createVaultAccountAndAccessToken({
project: res.project,
projectSecretKey: res.secret,
}).catch((error) => {
console.error(
"Failed to create vault account and access token",
error,
);
throw error;
});
return {
project: res.project,
secret: res.secret,
@@ -7,6 +7,7 @@ export function CopyAddressButton(props: {
address: string;
className?: string;
iconClassName?: string;
tooltip?: string;
variant?:
| "primary"
| "default"
@@ -24,7 +25,7 @@ export function CopyAddressButton(props: {
copyIconPosition={props.copyIconPosition}
textToCopy={props.address}
textToShow={shortenedAddress}
tooltip="Copy Address"
tooltip={props.tooltip || "Copy Address"}
variant={props.variant}
/>
);
@@ -1,3 +1,4 @@
import { useId } from "react";
import { cn } from "@/lib/utils";
export function DotsBackgroundPattern(props: { className?: string }) {
@@ -16,3 +17,70 @@ export function DotsBackgroundPattern(props: { className?: string }) {
/>
);
}
interface GridPatternProps extends React.SVGProps<SVGSVGElement> {
width?: number;
height?: number;
x?: number;
y?: number;
squares?: Array<[x: number, y: number]>;
strokeDasharray?: string;
className?: string;
[key: string]: unknown;
}
export function GridPattern({
width = 40,
height = 40,
x = -1,
y = -1,
strokeDasharray = "0",
squares,
className,
...props
}: GridPatternProps) {
const id = useId();
return (
<svg
aria-hidden="true"
className={cn(
"pointer-events-none absolute inset-0 h-full w-full fill-current stroke-current",
className,
)}
{...props}
>
<defs>
<pattern
id={id}
width={width}
height={height}
patternUnits="userSpaceOnUse"
x={x}
y={y}
>
<path
d={`M.5 ${height}V.5H${width}`}
fill="none"
strokeDasharray={strokeDasharray}
/>
</pattern>
</defs>
<rect width="100%" height="100%" strokeWidth={0} fill={`url(#${id})`} />
{squares && (
<svg x={x} y={y} className="overflow-visible" role="presentation">
{squares.map(([x, y]) => (
<rect
strokeWidth="0"
key={`${x}-${y}`}
width={width - 1}
height={height - 1}
x={x * width + 1}
y={y * height + 1}
/>
))}
</svg>
)}
</svg>
);
}
@@ -26,7 +26,6 @@ const buttonVariants = cva(
link: "underline-offset-4 hover:underline",
outline:
"border border-input bg-transparent hover:bg-accent hover:text-accent-foreground",
pink: "border border-nebula-pink-foreground !text-nebula-pink-foreground bg-[hsl(var(--nebula-pink-foreground)/5%)] hover:bg-nebula-pink-foreground/10 dark:!text-foreground dark:bg-nebula-pink-foreground/10 dark:hover:bg-nebula-pink-foreground/20",
primary: "bg-primary hover:bg-primary/90 text-primary-foreground ",
secondary:
"bg-secondary hover:bg-secondary/80 text-secondary-foreground ",
+1 -1
View File
@@ -8,7 +8,7 @@ const Card = React.forwardRef<
>(({ className, ...props }, ref) => (
<div
className={cn(
"rounded-lg border border-border bg-card text-card-foreground shadow-sm",
"rounded-lg border border-border bg-card text-card-foreground",
className,
)}
ref={ref}
@@ -0,0 +1,11 @@
"use client";
import * as CollapsiblePrimitive from "@radix-ui/react-collapsible";
const Collapsible = CollapsiblePrimitive.Root;
const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger;
const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent;
export { Collapsible, CollapsibleTrigger, CollapsibleContent };
@@ -50,9 +50,10 @@ const PaginationLink = ({
<Button
aria-current={isActive ? "page" : undefined}
className={cn(
"h-auto px-3 py-2",
"h-auto px-3 py-2 border border-transparent",
className,
props.disabled && "cursor-not-allowed",
!isActive && "border-border",
)}
variant={!isActive ? "outline" : "default"}
{...props}
@@ -3,7 +3,9 @@ import "server-only";
import { DASHBOARD_THIRDWEB_SECRET_KEY } from "./server-envs";
import { getConfiguredThirdwebClient } from "./thirdweb.server";
// During build time, the secret key might not be available
// Create a client that will work for build but may fail at runtime if secret key is needed
export const serverThirdwebClient = getConfiguredThirdwebClient({
secretKey: DASHBOARD_THIRDWEB_SECRET_KEY,
secretKey: DASHBOARD_THIRDWEB_SECRET_KEY || "dummy-build-time-secret",
teamId: undefined,
});
@@ -76,14 +76,18 @@ export function getConfiguredThirdwebClient(options: {
});
}
// During build time, provide fallbacks if credentials are missing
const clientId = NEXT_PUBLIC_DASHBOARD_CLIENT_ID || "dummy-build-client";
const secretKey = options.secretKey || undefined;
return createThirdwebClient({
clientId: NEXT_PUBLIC_DASHBOARD_CLIENT_ID,
clientId: clientId,
config: {
storage: {
gatewayUrl: NEXT_PUBLIC_IPFS_GATEWAY_URL,
},
},
secretKey: options.secretKey,
secretKey: secretKey,
teamId: options.teamId,
});
}
@@ -150,9 +150,13 @@ export const ErrorProvider: ComponentWithChildren = ({ children }) => {
/>
)}
<Button asChild className="py-2 text-sm" variant="primary">
<Link href="/support" rel="noopener noreferrer" target="_blank">
Visit support site
<Button asChild className="py-2 text-sm" variant="default">
<Link
href="/team/~/~/support"
rel="noopener noreferrer"
target="_blank"
>
Contact Support
</Link>
</Button>
</div>
+20 -12
View File
@@ -2,17 +2,11 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { useActiveAccount } from "thirdweb/react";
import { apiServerProxy } from "@/actions/proxies";
import type { Project } from "@/api/projects";
import { rotateVaultAccountAndAccessToken } from "../../app/(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions/lib/vault.client";
import { accountKeys, authorizedWallets } from "../query-keys/cache-keys";
// FIXME: We keep repeating types, API server should provide them
export const accountPlan = {
enterprise: "enterprise",
free: "free",
growth: "growth",
pro: "pro",
} as const;
export type AuthorizedWallet = {
id: string;
accountId: string;
@@ -318,26 +312,40 @@ export type RotateSecretKeyAPIReturnType = {
data: {
secret: string;
secretMasked: string;
secretHash: string;
};
};
export async function rotateSecretKeyClient(params: {
teamId: string;
projectId: string;
}) {
export async function rotateSecretKeyClient(params: { project: Project }) {
const res = await apiServerProxy<RotateSecretKeyAPIReturnType>({
body: JSON.stringify({}),
headers: {
"Content-Type": "application/json",
},
method: "POST",
pathname: `/v1/teams/${params.teamId}/projects/${params.projectId}/rotate-secret-key`,
pathname: `/v1/teams/${params.project.teamId}/projects/${params.project.id}/rotate-secret-key`,
});
if (!res.ok) {
throw new Error(res.error);
}
try {
// if the project has an encrypted vault admin key, rotate it as well
const service = params.project.services.find(
(service) => service.name === "engineCloud",
);
if (service?.encryptedAdminKey) {
await rotateVaultAccountAndAccessToken({
project: params.project,
projectSecretKey: res.data.data.secret,
projectSecretHash: res.data.data.secretHash,
});
}
} catch (error) {
console.error("Failed to rotate vault admin key", error);
}
return res.data;
}
@@ -7,21 +7,34 @@ import { embeddedWalletsKeys } from "../query-keys/cache-keys";
const fetchAccountList = ({
jwt,
clientId,
ecosystemSlug,
teamId,
pageNumber,
}: {
jwt: string;
clientId: string;
clientId?: string;
ecosystemSlug?: string;
teamId: string;
pageNumber: number;
}) => {
return async () => {
const url = new URL(`${THIRDWEB_EWS_API_HOST}/api/2024-05-05/account/list`);
url.searchParams.append("clientId", clientId);
// Add clientId or ecosystemSlug parameter
if (ecosystemSlug) {
url.searchParams.append("ecosystemSlug", `ecosystem.${ecosystemSlug}`);
} else if (clientId) {
url.searchParams.append("clientId", clientId);
}
url.searchParams.append("page", pageNumber.toString());
const res = await fetch(url.href, {
headers: {
Authorization: `Bearer ${jwt}`,
"Content-Type": "application/json",
"x-thirdweb-team-id": teamId,
...(clientId && { "x-client-id": clientId }),
},
method: "GET",
});
@@ -37,23 +50,27 @@ const fetchAccountList = ({
};
export function useEmbeddedWallets(params: {
clientId: string;
clientId?: string;
ecosystemSlug?: string;
teamId: string;
page: number;
authToken: string;
}) {
const { clientId, page, authToken } = params;
const { clientId, ecosystemSlug, teamId, page, authToken } = params;
const address = useActiveAccount()?.address;
return useQuery({
enabled: !!address && !!clientId,
enabled: !!address && !!(clientId || ecosystemSlug),
queryFn: fetchAccountList({
clientId,
ecosystemSlug,
teamId,
jwt: authToken,
pageNumber: page,
}),
queryKey: embeddedWalletsKeys.embeddedWallets(
address || "",
clientId,
clientId || ecosystemSlug || "",
page,
),
});
@@ -66,7 +83,15 @@ export function useAllEmbeddedWallets(params: { authToken: string }) {
const address = useActiveAccount()?.address;
return useMutation({
mutationFn: async ({ clientId }: { clientId: string }) => {
mutationFn: async ({
clientId,
ecosystemSlug,
teamId,
}: {
clientId?: string;
ecosystemSlug?: string;
teamId: string;
}) => {
const responses: WalletUser[] = [];
let page = 1;
@@ -76,12 +101,14 @@ export function useAllEmbeddedWallets(params: { authToken: string }) {
}>({
queryFn: fetchAccountList({
clientId,
ecosystemSlug,
teamId,
jwt: authToken,
pageNumber: page,
}),
queryKey: embeddedWalletsKeys.embeddedWallets(
address || "",
clientId,
clientId || ecosystemSlug || "",
page,
),
});
+1 -1
View File
@@ -30,7 +30,7 @@ export const ChainIconClient = ({
fallback={<img alt="" src={fallbackChainIcon} />}
key={resolvedSrc}
loading={restProps.loading || "lazy"}
skeleton={<div className="animate-pulse rounded-full bg-border" />}
skeleton={<span className="animate-pulse rounded-full bg-border" />}
src={resolvedSrc}
/>
);
@@ -3,7 +3,7 @@ import type { SVGProps } from "react";
export function DiscordIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg
enable-background="new 0 0 100 100"
enableBackground="new 0 0 100 100"
viewBox="0 0 100 100"
xmlns="http://www.w3.org/2000/svg"
{...props}
+13 -10
View File
@@ -57,7 +57,10 @@ export function teamStub(id: string, billingPlan: Team["billingPlan"]): Team {
},
nebula: {
enabled: true,
rateLimit: 1000,
rateLimit: {
perSecond: 1000,
perMinute: 1000,
},
},
pay: {
enabled: true,
@@ -82,6 +85,10 @@ export function teamStub(id: string, billingPlan: Team["billingPlan"]): Team {
totalFileSizeBytesLimit: 1_000_000_000,
},
},
mcp: {
enabled: true,
rateLimit: 10,
},
},
createdAt: new Date().toISOString(),
dedicatedSupportChannel: null,
@@ -192,6 +199,7 @@ export function teamSubscriptionsStub(
currentPeriodEnd: "2024-12-15T20:56:06.000Z",
currentPeriodStart: "2024-11-15T20:56:06.000Z",
id: "sub-1",
skus: [],
status: "active",
trialEnd: overrides?.trialEnd || null,
trialStart: null,
@@ -212,6 +220,7 @@ export function teamSubscriptionsStub(
currentPeriodEnd: "2024-12-15T20:56:06.000Z",
currentPeriodStart: "2024-11-15T20:56:15.000Z",
id: "sub-2",
skus: [],
status: "active",
trialEnd: null,
trialStart: null,
@@ -229,25 +238,19 @@ export function teamSubscriptionsStub(
// In-App Wallets
{
amount: usage.inAppWalletAmount?.amount || 0,
description: `${
usage.inAppWalletAmount?.quantity || 0
} x In-App Wallets (Tier 1 at $0.00 / month)`,
description: `${usage.inAppWalletAmount?.quantity || 0} x In-App Wallets (Tier 1 at $0.00 / month)`,
thirdwebSku: "usage:in_app_wallet",
},
// AA Sponsorship
{
amount: usage.aaSponsorshipAmount?.amount || 0,
description: `${
usage.aaSponsorshipAmount?.quantity || 0
} x AA Gas Sponsorship (at $0.011 / month)`,
description: `${usage.aaSponsorshipAmount?.quantity || 0} x AA Gas Sponsorship (at $0.011 / month)`,
thirdwebSku: "usage:aa_sponsorship",
},
// OP Grant
{
amount: usage.aaSponsorshipOpGrantAmount?.amount || 0,
description: `${
usage.aaSponsorshipOpGrantAmount?.quantity || 0
} x AA Gas Sponsorship (OP) (at $0.011 / month)`,
description: `${usage.aaSponsorshipOpGrantAmount?.quantity || 0} x AA Gas Sponsorship (OP) (at $0.011 / month)`,
thirdwebSku: "usage:aa_sponsorship_op_grant",
},
],
+26 -6
View File
@@ -39,12 +39,6 @@ export interface TransactionStats {
count: number;
}
export interface RpcMethodStats {
date: string;
evmMethod: string;
count: number;
}
export interface EngineCloudStats {
date: string;
chainId: string;
@@ -72,10 +66,36 @@ export interface UniversalBridgeWalletStats {
developerFeeUsdCents: number;
}
export interface WebhookRequestStats {
date: string;
webhookId: string;
httpStatusCode: number;
totalRequests: number;
}
export interface WebhookLatencyStats {
date: string;
webhookId: string;
p50LatencyMs: number;
p90LatencyMs: number;
p99LatencyMs: number;
}
export interface WebhookSummaryStats {
webhookId: string;
totalRequests: number;
successRequests: number;
errorRequests: number;
successRate: number;
avgLatencyMs: number;
errorBreakdown: Record<string, number>;
}
export interface AnalyticsQueryParams {
teamId: string;
projectId?: string;
from?: Date;
to?: Date;
period?: "day" | "week" | "month" | "year" | "all";
limit?: number;
}
+5
View File
@@ -14,3 +14,8 @@ export type ProductSKU =
| "usage:aa_sponsorship"
| "usage:aa_sponsorship_op_grant"
| null;
export type ChainInfraSKU =
| "chain:infra:rpc"
| "chain:infra:insight"
| "chain:infra:account_abstraction";
+3 -3
View File
@@ -4,14 +4,14 @@ import type { JSX } from "react";
const PLEASE_REACH_OUT_MESSAGE = (
<span>
If you believe this is incorrect or the error persists, please visit our{" "}
If you believe this is incorrect or the error persists, please{" "}
<Link
className="font-semibold underline"
href="/support"
href="/team/~/~/support"
rel="noopener noreferrer"
target="_blank"
>
support site
contact support
</Link>
.
</span>
-1
View File
@@ -23,7 +23,6 @@ export const TEAM_PLANS: Record<
features: [
"Email Support",
"48hr Guaranteed Response",
"Invite Team Members",
"Custom In-App Wallet Auth",
],
price: 99,
@@ -1,5 +1,7 @@
import { defineChain } from "thirdweb";
import { getChainMetadata } from "thirdweb/chains";
import { ExternalLinkIcon } from "lucide-react";
import Link from "next/link";
import { defineChain, getAddress, NATIVE_TOKEN_ADDRESS } from "thirdweb";
import { type ChainMetadata, getChainMetadata } from "thirdweb/chains";
import { Card, CardContent, CardHeader } from "@/components/ui/card";
import { serverThirdwebClient } from "@/constants/thirdweb-client.server";
import { resolveSchemeWithErrorHandler } from "@/utils/resolveSchemeWithErrorHandler";
@@ -53,7 +55,7 @@ export async function RouteListCard({
return (
<div className="relative h-full">
<Card className="h-full w-full transition-colors hover:border-active-border">
<Card className="h-full w-full">
<CardHeader className="flex flex-row items-center justify-between p-4">
<div className="flex flex-row items-center gap-2">
{resolvedOriginTokenIconUri ? (
@@ -80,32 +82,60 @@ export async function RouteListCard({
</CardHeader>
<CardContent className="px-4 pt-0 pb-4">
<table className="w-full">
<tbody className="text-sm [&_td>*]:min-h-[25px]">
<tr>
<th className="text-left font-normal text-base">
{originTokenName === "ETH"
? originChain.nativeCurrency.name
: originTokenName}
</th>
<td className="text-right text-base text-muted-foreground">
{originChain.name}
</td>
</tr>
<tr>
<th className="text-left font-normal text-base">
{destinationTokenName === "ETH"
? destinationChain.nativeCurrency.name
: destinationTokenName}
</th>
<td className="text-right text-base text-muted-foreground">
{destinationChain.name}
</td>
</tr>
</tbody>
</table>
<div className="flex flex-col gap-1">
<div className="flex justify-between gap-2">
<TokenName
chainMetadata={originChain}
tokenAddress={originTokenAddress}
tokenName={originTokenName}
/>
<div className="text-right text-base text-muted-foreground">
{originChain.name}
</div>
</div>
<div className="flex justify-between gap-2">
<TokenName
chainMetadata={destinationChain}
tokenAddress={destinationTokenAddress}
tokenName={destinationTokenName}
/>
<div className="text-right text-base text-muted-foreground">
{destinationChain.name}
</div>
</div>
</div>
</CardContent>
</Card>
</div>
);
}
const nativeTokenAddress = getAddress(NATIVE_TOKEN_ADDRESS);
function TokenName(props: {
tokenAddress: string;
tokenName: string;
chainMetadata: ChainMetadata;
}) {
const isERC20 = getAddress(props.tokenAddress) !== nativeTokenAddress;
if (isERC20) {
return (
<Link
className="flex items-center gap-1.5 font-normal text-base group/link hover:underline underline-offset-4 decoration-muted-foreground/50 decoration-dotted"
href={`https://thirdweb.com/${props.chainMetadata.slug}/${props.tokenAddress}`}
target="_blank"
>
{props.tokenName}
<ExternalLinkIcon className="size-3.5 text-muted-foreground group-hover/link:text-foreground" />
</Link>
);
}
return (
<div className="text-left font-normal text-base">
{props.chainMetadata.nativeCurrency.name}
</div>
);
}
@@ -1,5 +1,14 @@
import { defineChain, getChainMetadata } from "thirdweb/chains";
import { CopyTextButton } from "@/components/ui/CopyTextButton";
import { ExternalLinkIcon } from "lucide-react";
import Link from "next/link";
import { getAddress, NATIVE_TOKEN_ADDRESS } from "thirdweb";
import {
type ChainMetadata,
defineChain,
getChainMetadata,
} from "thirdweb/chains";
import { shortenAddress } from "thirdweb/utils";
import { Img } from "@/components/blocks/Img";
import { Button } from "@/components/ui/button";
import { TableCell, TableRow } from "@/components/ui/table";
import { serverThirdwebClient } from "@/constants/thirdweb-client.server";
import { resolveSchemeWithErrorHandler } from "@/utils/resolveSchemeWithErrorHandler";
@@ -52,37 +61,14 @@ export async function RouteListRow({
]);
return (
<TableRow className="group transition-colors hover:bg-accent/50" linkBox>
<TableRow className="group">
<TableCell>
<div className="flex flex-row items-center gap-3">
<div className="flex items-center gap-2">
{resolvedOriginTokenIconUri ? (
// For now we're using a normal img tag because the domain for these images is unknown
// eslint-disable-next-line @next/next/no-img-element
<img
alt={originTokenAddress}
className="size-7 rounded-full border border-border/50 shadow-sm transition-transform group-hover:scale-105"
src={resolvedOriginTokenIconUri}
/>
) : (
<div className="size-7 rounded-full bg-muted-foreground/20" />
)}
{originTokenSymbol && (
<CopyTextButton
className="relative z-10 font-medium text-base"
copyIconPosition="right"
textToCopy={originTokenAddress}
textToShow={
originTokenSymbol === "ETH"
? originChain.nativeCurrency.symbol
: originTokenSymbol
}
tooltip="Copy Token Address"
variant="ghost"
/>
)}
</div>
</div>
<TokenInfo
chainMetadata={originChain}
tokenAddress={originTokenAddress}
tokenIconUri={resolvedOriginTokenIconUri}
tokenSymbol={originTokenSymbol}
/>
</TableCell>
<TableCell className="text-muted-foreground/90">
@@ -90,34 +76,12 @@ export async function RouteListRow({
</TableCell>
<TableCell>
<div className="flex flex-row items-center gap-3">
<div className="flex items-center gap-2">
{resolvedDestinationTokenIconUri ? (
// eslint-disable-next-line @next/next/no-img-element
<img
alt={destinationTokenAddress}
className="size-7 rounded-full border border-border/50 shadow-sm transition-transform group-hover:scale-105"
src={resolvedDestinationTokenIconUri}
/>
) : (
<div className="size-7 rounded-full bg-muted-foreground/20" />
)}
{destinationTokenSymbol && (
<CopyTextButton
className="relative z-10 font-medium text-base"
copyIconPosition="right"
textToCopy={destinationTokenAddress}
textToShow={
destinationTokenSymbol === "ETH"
? destinationChain.nativeCurrency.symbol
: destinationTokenSymbol
}
tooltip="Copy Token Address"
variant="ghost"
/>
)}
</div>
</div>
<TokenInfo
chainMetadata={destinationChain}
tokenAddress={destinationTokenAddress}
tokenIconUri={resolvedDestinationTokenIconUri}
tokenSymbol={destinationTokenSymbol}
/>
</TableCell>
<TableCell className="text-muted-foreground/90">
@@ -126,3 +90,47 @@ export async function RouteListRow({
</TableRow>
);
}
const nativeTokenAddress = getAddress(NATIVE_TOKEN_ADDRESS);
function TokenInfo(props: {
tokenAddress: string;
tokenSymbol: string | undefined;
chainMetadata: ChainMetadata;
tokenIconUri: string | undefined;
}) {
const isERC20 = getAddress(props.tokenAddress) !== nativeTokenAddress;
return (
<div className="flex items-center gap-1.5">
{props.tokenIconUri ? (
<Img
alt={props.tokenAddress}
className="size-7 rounded-full border border-border/50"
src={props.tokenIconUri}
/>
) : (
<div className="size-7 rounded-full bg-muted-foreground/20" />
)}
{isERC20 ? (
<Button
asChild
className="h-auto w-auto py-1 px-1.5 gap-2"
variant="ghost"
>
<Link
href={`https://thirdweb.com/${props.chainMetadata.slug}/${props.tokenAddress}`}
target="_blank"
>
{props.tokenSymbol || shortenAddress(props.tokenAddress)}
<ExternalLinkIcon className="size-3.5 text-muted-foreground" />
</Link>
</Button>
) : (
<span className="font-medium text-sm px-1.5 py-1">
{props.chainMetadata.nativeCurrency.symbol}
</span>
)}
</div>
);
}
@@ -86,22 +86,14 @@ export async function RoutesData(props: {
<p className="text-2xl">No Results found</p>
</div>
) : props.activeView === "table" ? (
<TableContainer className="overflow-hidden rounded-xl border border-border/50 bg-card/50 shadow-sm transition-all">
<TableContainer>
<Table>
<TableHeader className="z-0">
<TableRow className="border-border/50 border-b bg-muted/50">
<TableHead className="py-4 font-medium text-muted-foreground/80 text-xs uppercase tracking-wider">
Origin Token
</TableHead>
<TableHead className="py-4 font-medium text-muted-foreground/80 text-xs uppercase tracking-wider">
Origin Chain
</TableHead>
<TableHead className="py-4 font-medium text-muted-foreground/80 text-xs uppercase tracking-wider">
Destination Token
</TableHead>
<TableHead className="py-4 font-medium text-muted-foreground/80 text-xs uppercase tracking-wider">
Destination Chain
</TableHead>
<TableHeader>
<TableRow>
<TableHead>Origin Token</TableHead>
<TableHead>Origin Chain</TableHead>
<TableHead>Destination Token</TableHead>
<TableHead>Destination Chain</TableHead>
</TableRow>
</TableHeader>
<TableBody>
@@ -10,7 +10,7 @@ import {
type SearchParams,
} from "./components/server/routes-table";
const title = "Universal Bridge Routes: Swap, Bridge, and Onramp";
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.";
@@ -66,7 +66,7 @@ export default async function RoutesPage(props: {
<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 Universal Bridge
Get Started with thirdweb Payments
</h3>
<p className="text-muted-foreground text-sm">
Simple, instant, and secure payments across any token and chain.
@@ -1,6 +1,5 @@
import "server-only";
import type { Address } from "thirdweb";
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";
@@ -10,21 +9,13 @@ export async function getRoutes({
offset,
originQuery,
destinationQuery,
originChainId,
destinationChainId,
originTokenAddress,
destinationTokenAddress,
}: {
limit?: number;
offset?: number;
originQuery?: string;
destinationQuery?: string;
originChainId?: number;
destinationChainId?: number;
originTokenAddress?: Address;
destinationTokenAddress?: Address;
} = {}) {
const url = new URL(`${NEXT_PUBLIC_THIRDWEB_BRIDGE_HOST}/v1/routes`);
const url = new URL(`${NEXT_PUBLIC_THIRDWEB_BRIDGE_HOST}/v1/routes/search`);
if (limit) {
url.searchParams.set("limit", limit.toString());
}
@@ -37,20 +28,7 @@ export async function getRoutes({
if (destinationQuery) {
url.searchParams.set("destinationQuery", destinationQuery);
}
if (originChainId) {
url.searchParams.set("originChainId", originChainId.toString());
}
if (destinationChainId) {
url.searchParams.set("destinationChainId", destinationChainId.toString());
}
if (originTokenAddress) {
url.searchParams.set("originTokenAddress", originTokenAddress);
}
if (destinationTokenAddress) {
url.searchParams.set("destinationTokenAddress", destinationTokenAddress);
}
url.searchParams.set("sortBy", "popularity");
// It's faster to filter client side, doesn't seem to be a big performance boost to paginate/filter server side
const routesResponse = await fetch(url, {
headers: {
"x-secret-key": DASHBOARD_THIRDWEB_SECRET_KEY,
@@ -1,12 +1,15 @@
"use client";
import { useTheme } from "next-themes";
import { defineChain, type ThirdwebClient } from "thirdweb";
import type { ChainMetadata } from "thirdweb/chains";
import { BuyWidget } from "thirdweb/react";
import { getSDKTheme } from "@/utils/sdk-component-theme";
export function BuyFundsSection(props: {
chain: ChainMetadata;
client: ThirdwebClient;
}) {
const { theme } = useTheme();
return (
<section className="flex flex-col gap-4 items-center justify-center">
<BuyWidget
@@ -14,6 +17,7 @@ export function BuyFundsSection(props: {
// eslint-disable-next-line no-restricted-syntax
chain={defineChain(props.chain.chainId)}
client={props.client}
theme={getSDKTheme(theme === "dark" ? "dark" : "light")}
/>
</section>
);
@@ -1,37 +0,0 @@
"use client";
import { FileTextIcon } from "lucide-react";
import Link from "next/link";
import type { ChainMetadata } from "thirdweb/chains";
import { SectionTitle } from "../server/SectionTitle";
export default function NextSteps(props: { chain: ChainMetadata }) {
const { chain } = props;
return (
<section>
<SectionTitle title="Next Steps" />
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-3">
<div className="relative flex gap-3 rounded-lg border bg-card p-4 pr-8 transition-colors hover:border-active-border">
<FileTextIcon className="mt-0.5 size-5 shrink-0" />
<div>
<h3 className="mb-1.5 font-medium">
<Link
className="before:absolute before:inset-0"
href={
"https://blog.thirdweb.com/supercharge-user-adoption-integrate-embedded-wallets-in-minutes/"
}
rel="noopener noreferrer"
target="_blank"
>
Create a login for {chain.name}
</Link>
</h3>
<p className="text-muted-foreground text-sm">
Supercharge User Adoption—Integrate In-App Wallets in Minutes
</p>
</div>
</div>
</div>
</section>
);
}
@@ -1,31 +0,0 @@
import { ExternalLinkIcon } from "lucide-react";
import Link from "next/link";
import { Button } from "@/components/ui/button";
import { SectionTitle } from "./SectionTitle";
export function ClaimChainSection() {
return (
<section>
<SectionTitle title="Claim Chain" />
<div className="rounded-lg border bg-card p-4">
<p className="mb-1"> This chain page is unclaimed </p>
<p className="mb-5 text-muted-foreground text-sm">
Are you the owner of this ecosystem? Claim this page to add additional
information on this page to help your developers get started!
</p>
<Button asChild variant="outline">
<Link
className="gap-2"
href="https://share.hsforms.com/1qAViW3U5SoKbabNhRZy4Kgea58c"
rel="noopener noreferrer"
target="_blank"
>
<span>Claim Chain</span>
<ExternalLinkIcon className="-mt-[3px] size-4" />
</Link>
</Button>
</div>
</section>
);
}
@@ -3,7 +3,7 @@ import type { ThirdwebClient } from "thirdweb";
import type { ChainMetadata } from "thirdweb/chains";
import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";
import { mapV4ChainToV5Chain } from "../../../../../../../../@/utils/map-chains";
import { mapV4ChainToV5Chain } from "@/utils/map-chains";
import { ChainIcon } from "../../../../components/server/chain-icon";
import { AddChainToWallet } from "../client/add-chain-to-wallet";
@@ -19,7 +19,7 @@ import {
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { getClientThirdwebClient } from "@/constants/thirdweb-client.client";
import { mapV4ChainToV5Chain } from "../../../../../../@/utils/map-chains";
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";
@@ -1,7 +1,8 @@
import { ImageResponse } from "next/og";
import { useId } from "react";
import { download } from "thirdweb/storage";
import { serverThirdwebClient } from "@/constants/thirdweb-client.server";
import { DASHBOARD_THIRDWEB_SECRET_KEY } from "@/constants/server-envs";
import { getConfiguredThirdwebClient } from "@/constants/thirdweb.server";
import { fetchChain } from "@/utils/fetchChain";
// Route segment config
@@ -81,16 +82,29 @@ export default async function Image({
fetch(new URL("og-lib/fonts/inter/700.ttf", import.meta.url)).then((res) =>
res.arrayBuffer(),
),
// download the chain icon if there is one
chain.icon?.url && hasWorkingChainIcon
? download({
client: serverThirdwebClient,
uri: chain.icon.url,
}).then((res) => res.arrayBuffer())
// download the chain icon if there is one and secret key is available
chain.icon?.url && hasWorkingChainIcon && DASHBOARD_THIRDWEB_SECRET_KEY
? (async () => {
try {
const client = getConfiguredThirdwebClient({
secretKey: DASHBOARD_THIRDWEB_SECRET_KEY,
teamId: undefined,
});
const response = await download({
client,
uri: chain.icon?.url || "",
});
return response.arrayBuffer();
} catch (error) {
// If download fails, return undefined to fallback to no icon
console.warn("Failed to download chain icon:", error);
return undefined;
}
})()
: undefined,
// download the background image (based on chain)
fetch(
chain.icon?.url && hasWorkingChainIcon
chain.icon?.url && hasWorkingChainIcon && DASHBOARD_THIRDWEB_SECRET_KEY
? new URL(
"og-lib/assets/chain/bg-with-icon.png",
@@ -118,7 +132,7 @@ export default async function Image({
/>
{/* the actual component starts here */}
{hasWorkingChainIcon && (
{hasWorkingChainIcon && chainIcon && (
<img
alt=""
// @ts-expect-error - TS doesn't know about the ImageResponse component
@@ -3,9 +3,7 @@ import { getClientThirdwebClient } from "@/constants/thirdweb-client.client";
import { getRawAccount } from "../../../../account/settings/getAccount";
import { getChain, getChainMetadata } from "../../utils";
import { BuyFundsSection } from "./components/client/BuyFundsSection";
import NextSteps from "./components/client/NextSteps";
import { ChainOverviewSection } from "./components/server/ChainOverviewSection";
import { ClaimChainSection } from "./components/server/ClaimChainSection";
import { ChainCTA } from "./components/server/cta-card";
import { ExplorersSection } from "./components/server/explorer-section";
import { FaucetSection } from "./components/server/FaucetSection";
@@ -60,11 +58,6 @@ export default async function Page(props: {
{chain.services.filter((s) => s.enabled).length > 0 && (
<SupportedProductsSection services={chain.services} />
)}
{/*Next Steps */}
<NextSteps chain={chain} />
{/* Claim Chain */}
{!chainMetadata && <ClaimChainSection />}
</div>
);
}
@@ -1,4 +1,5 @@
// biome-ignore-all lint/nursery/noNestedComponentDefinitions: TODO
/** biome-ignore-all lint/a11y/useSemanticElements: FIXME */
import {
IconButton,
@@ -256,7 +257,6 @@ export const MarketplaceTable: React.FC<MarketplaceTableProps> = ({
// biome-ignore lint/suspicious/noArrayIndexKey: FIXME
key={rowIndex}
onClick={() => setTokenRow(row.original)}
// biome-ignore lint/a11y/useSemanticElements: FIXME
role="group"
style={{ cursor: "pointer" }}
>
@@ -130,6 +130,7 @@ const getClaimConditionTypeFromPhase = (
if (phase.snapshot) {
if (
phase.maxClaimablePerWallet?.toString() === "0" &&
phase.price === "0" &&
typeof phase.snapshot !== "string" &&
phase.snapshot.length === 1 &&
@@ -464,204 +465,198 @@ export const ClaimConditionsForm: React.FC<ClaimConditionsFormProps> = ({
}
return (
<>
<Flex as="form" direction="column" gap={10} onSubmit={handleFormSubmit}>
<Flex direction="column" gap={6}>
{/* Show the reason why the form is disabled */}
{!isAdmin && (
<Text>Connect with admin wallet to edit claim conditions.</Text>
)}
{controlledFields.map((field, index) => {
const dropType: DropType = field.snapshot
? field.maxClaimablePerWallet?.toString() === "0"
? "specific"
: "overrides"
: "any";
<Flex as="form" direction="column" gap={10} onSubmit={handleFormSubmit}>
<Flex direction="column" gap={6}>
{/* Show the reason why the form is disabled */}
{!isAdmin && (
<Text>Connect with admin wallet to edit claim conditions.</Text>
)}
{controlledFields.map((field, index) => {
const dropType: DropType = field.snapshot
? field.maxClaimablePerWallet?.toString() === "0"
? "specific"
: "overrides"
: "any";
const claimConditionType = getClaimConditionTypeFromPhase(field);
const claimConditionType = getClaimConditionTypeFromPhase(field);
const isActive = activePhaseId === field.id;
const isActive = activePhaseId === field.id;
const snapshotValue = field.snapshot?.map((v) =>
typeof v === "string"
? {
address: v,
currencyAddress: ZERO_ADDRESS,
maxClaimable: "unlimited",
price: "unlimited",
}
: {
...v,
currencyAddress: v?.currencyAddress || ZERO_ADDRESS,
maxClaimable: v?.maxClaimable?.toString() || "unlimited",
price: v?.price?.toString() || "unlimited",
},
);
const snapshotValue = field.snapshot?.map((v) =>
typeof v === "string"
? {
address: v,
currencyAddress: ZERO_ADDRESS,
maxClaimable: "unlimited",
price: "unlimited",
}
: {
...v,
currencyAddress: v?.currencyAddress || ZERO_ADDRESS,
maxClaimable: v?.maxClaimable?.toString() || "unlimited",
price: v?.price?.toString() || "unlimited",
},
);
return (
<Fragment key={`snapshot_${field.id}_${index}`}>
<SnapshotViewerSheet
client={contract.client}
dropType={dropType}
isDisabled={!canEditForm}
isOpen={openSnapshotIndex === index}
onClose={() => {
setOpenSnapshotIndex(-1);
}}
setSnapshot={(snapshot) =>
form.setValue(`phases.${index}.snapshot`, snapshot)
}
value={snapshotValue}
/>
return (
<Fragment key={`snapshot_${field.id}_${index}`}>
<SnapshotViewerSheet
client={contract.client}
dropType={dropType}
isDisabled={!canEditForm}
isOpen={openSnapshotIndex === index}
onClose={() => {
setOpenSnapshotIndex(-1);
}}
setSnapshot={(snapshot) =>
form.setValue(`phases.${index}.snapshot`, snapshot)
}
value={snapshotValue}
/>
<ClaimsConditionFormContext.Provider
value={{
claimConditionType,
dropType,
field,
form,
formDisabled: !canEditForm,
isActive,
isAdmin,
isColumn,
isErc20,
isMultiPhase,
phaseIndex: index,
setOpenSnapshotIndex,
tokenDecimals: tokenDecimals.data,
}}
>
<ClaimConditionsPhase
contract={contract}
isPending={sendTx.isPending}
onRemove={() => {
removePhase(index);
}}
/>
</ClaimsConditionFormContext.Provider>
</Fragment>
);
})}
{phases?.length === 0 && (
<Alert borderRadius="md" status="warning">
<AlertIcon />
<div className="flex flex-col">
<AlertTitle as={Heading} size="label.lg">
{isMultiPhase
? "Missing Claim Phases"
: "Missing Claim Conditions"}
</AlertTitle>
<AlertDescription as={Text}>
{isMultiPhase
? "You need to set at least one claim phase for people to claim this drop."
: "You need to set claim conditions for people to claim this drop."}
</AlertDescription>
</div>
</Alert>
)}
<Flex
flexDir={{ base: "column", md: "row" }}
gap={2}
justifyContent="space-between"
>
<div className="flex flex-row gap-2">
<AdminOnly contract={contract}>
<Menu>
<MenuButton
as={Button}
borderRadius="md"
colorScheme="primary"
isDisabled={
sendTx.isPending || (!isMultiPhase && phases?.length > 0)
}
leftIcon={<PlusIcon className="size-5" />}
size="sm"
variant={phases?.length > 0 ? "outline" : "solid"}
>
Add {isMultiPhase ? "Phase" : "Claim Conditions"}
</MenuButton>
<MenuList
borderRadius="lg"
overflow="hidden"
zIndex="overlay"
>
{Object.keys(ClaimConditionTypeData).map((key) => {
const type = key as ClaimConditionType;
if (type === "custom") {
return null;
}
return (
<MenuItem
key={type}
onClick={() => {
addPhase(type);
// TODO: Automatically start editing the new phase after adding it
}}
>
<div className="flex items-center gap-1">
{ClaimConditionTypeData[type].name}
<TooltipBox
content={
<Text size="body.md">
{ClaimConditionTypeData[type].description}
</Text>
}
/>
</div>
</MenuItem>
);
})}
</MenuList>
</Menu>
</AdminOnly>
{controlledFields.some((field) => field.fromSdk) && (
<ResetClaimEligibility
<ClaimsConditionFormContext.Provider
value={{
claimConditionType,
dropType,
field,
form,
formDisabled: !canEditForm,
isActive,
isAdmin,
isColumn,
isErc20,
isMultiPhase,
phaseIndex: index,
setOpenSnapshotIndex,
tokenDecimals: tokenDecimals.data,
}}
>
<ClaimConditionsPhase
contract={contract}
isErc20={isErc20}
isLoggedIn={isLoggedIn}
isMultiphase={isMultiPhase}
tokenId={tokenId}
isPending={sendTx.isPending}
onRemove={() => {
removePhase(index);
}}
/>
)}
</div>
</ClaimsConditionFormContext.Provider>
</Fragment>
);
})}
<div className="flex flex-row">
<AdminOnly contract={contract} fallback={<Box pb={5} />}>
<Flex alignItems="center" gap={3} justifyContent="center">
{(hasRemovedPhases || hasAddedPhases) && (
<Text color="red.500" fontWeight="bold">
You have unsaved changes
</Text>
)}
{controlledFields.length > 0 ||
hasRemovedPhases ||
!isMultiPhase ? (
<TransactionButton
client={contract.client}
disabled={claimConditionsQuery.isPending}
isLoggedIn={isLoggedIn}
isPending={sendTx.isPending}
transactionCount={1}
txChainID={contract.chain.id}
type="submit"
>
{claimConditionsQuery.isPending
? "Saving Phases"
: "Save Phases"}
</TransactionButton>
) : null}
</Flex>
</AdminOnly>
{phases?.length === 0 && (
<Alert borderRadius="md" status="warning">
<AlertIcon />
<div className="flex flex-col">
<AlertTitle as={Heading} size="label.lg">
{isMultiPhase
? "Missing Claim Phases"
: "Missing Claim Conditions"}
</AlertTitle>
<AlertDescription as={Text}>
{isMultiPhase
? "You need to set at least one claim phase for people to claim this drop."
: "You need to set claim conditions for people to claim this drop."}
</AlertDescription>
</div>
</Flex>
</Alert>
)}
<Flex
flexDir={{ base: "column", md: "row" }}
gap={2}
justifyContent="space-between"
>
<div className="flex flex-row gap-2">
<AdminOnly contract={contract}>
<Menu>
<MenuButton
as={Button}
borderRadius="md"
colorScheme="primary"
isDisabled={
sendTx.isPending || (!isMultiPhase && phases?.length > 0)
}
leftIcon={<PlusIcon className="size-5" />}
size="sm"
variant={phases?.length > 0 ? "outline" : "solid"}
>
Add {isMultiPhase ? "Phase" : "Claim Conditions"}
</MenuButton>
<MenuList borderRadius="lg" overflow="hidden" zIndex="overlay">
{Object.keys(ClaimConditionTypeData).map((key) => {
const type = key as ClaimConditionType;
if (type === "custom") {
return null;
}
return (
<MenuItem
key={type}
onClick={() => {
addPhase(type);
// TODO: Automatically start editing the new phase after adding it
}}
>
<div className="flex items-center gap-1">
{ClaimConditionTypeData[type].name}
<TooltipBox
content={
<Text size="body.md">
{ClaimConditionTypeData[type].description}
</Text>
}
/>
</div>
</MenuItem>
);
})}
</MenuList>
</Menu>
</AdminOnly>
{controlledFields.some((field) => field.fromSdk) && (
<ResetClaimEligibility
contract={contract}
isErc20={isErc20}
isLoggedIn={isLoggedIn}
isMultiphase={isMultiPhase}
tokenId={tokenId}
/>
)}
</div>
<div className="flex flex-row">
<AdminOnly contract={contract} fallback={<Box pb={5} />}>
<Flex alignItems="center" gap={3} justifyContent="center">
{(hasRemovedPhases || hasAddedPhases) && (
<Text color="red.500" fontWeight="bold">
You have unsaved changes
</Text>
)}
{controlledFields.length > 0 ||
hasRemovedPhases ||
!isMultiPhase ? (
<TransactionButton
client={contract.client}
disabled={claimConditionsQuery.isPending}
isLoggedIn={isLoggedIn}
isPending={sendTx.isPending}
transactionCount={1}
txChainID={contract.chain.id}
type="submit"
>
{claimConditionsQuery.isPending
? "Saving Phases"
: "Save Phases"}
</TransactionButton>
) : null}
</Flex>
</AdminOnly>
</div>
</Flex>
</Flex>
</>
</Flex>
);
};
@@ -1,9 +1,10 @@
import { getAddress, getContract, isAddress } from "thirdweb";
import { localhost } from "thirdweb/chains";
import { serverThirdwebClient } from "@/constants/thirdweb-client.server";
import { getUserThirdwebClient } from "@/api/auth-token";
import { DASHBOARD_THIRDWEB_SECRET_KEY } from "@/constants/server-envs";
import { getConfiguredThirdwebClient } from "@/constants/thirdweb.server";
import { fetchChainWithLocalOverrides } from "@/utils/fetchChainWithLocalOverrides";
import { mapV4ChainToV5Chain } from "@/utils/map-chains";
import { getUserThirdwebClient } from "../../../../../../../@/api/auth-token";
import { fetchChainWithLocalOverrides } from "../../../../../../../@/utils/fetchChainWithLocalOverrides";
export async function getContractPageParamsInfo(params: {
contractAddress: string;
@@ -18,13 +19,21 @@ export async function getContractPageParamsInfo(params: {
return undefined;
}
// attempt to get the auth token
// Create server client only if secret key is available
if (!DASHBOARD_THIRDWEB_SECRET_KEY) {
return undefined;
}
const serverClient = getConfiguredThirdwebClient({
secretKey: DASHBOARD_THIRDWEB_SECRET_KEY,
teamId: undefined,
});
const serverContract = getContract({
address: contractAddress,
// eslint-disable-next-line no-restricted-syntax
chain: mapV4ChainToV5Chain(chainMetadata),
client: serverThirdwebClient,
client: serverClient,
});
const clientContract = getContract({
@@ -1,5 +1,5 @@
import { notFound, redirect } from "next/navigation";
import { getAuthTokenWalletAddress } from "../../../../../../../../@/api/auth-token";
import { getAuthTokenWalletAddress } from "@/api/auth-token";
import type { ProjectMeta } from "../../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/types";
import { redirectToContractLandingPage } from "../../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/utils";
import { buildContractPagePath } from "../../_utils/contract-page-path";
@@ -1,4 +1,5 @@
/** biome-ignore-all lint/nursery/noNestedComponentDefinitions: FIXME */
/** biome-ignore-all lint/a11y/useSemanticElements: FIXME */
"use client";
import {
@@ -320,7 +321,6 @@ export const NFTGetAllTable: React.FC<ContractOverviewNFTGetAllProps> = ({
}}
opacity={failedToLoad ? 0.3 : 1}
pointerEvents={failedToLoad ? "none" : "auto"}
// biome-ignore lint/a11y/useSemanticElements: FIXME
role="group"
style={{ cursor: "pointer" }}
>
@@ -1,5 +1,5 @@
import type { ThirdwebContract } from "thirdweb";
import { getAuthTokenWalletAddress } from "../../../../../../../../@/api/auth-token";
import { getAuthTokenWalletAddress } from "@/api/auth-token";
import { getPublishedByCardProps, PublishedByUI } from "./published-by-ui";
interface PublishedByProps {
@@ -6,7 +6,7 @@ import { PublicPageConnectButton } from "./PublicPageConnectButton";
export function PageHeader(props: { containerClassName?: string }) {
return (
<div className="border-b border-dashed">
<div className="border-b bg-card">
<header
className={cn(
"container flex max-w-8xl justify-between py-3",
@@ -0,0 +1,31 @@
import type { ThirdwebContract } from "thirdweb";
import { isOwnerSupported, owner } from "thirdweb/extensions/common";
import {
getRoleMember,
isGetRoleAdminSupported,
} from "thirdweb/extensions/permissions";
export async function getContractCreator(
contract: ThirdwebContract,
functionSelectors: string[],
) {
try {
if (isOwnerSupported(functionSelectors)) {
return await owner({
contract,
});
}
if (isGetRoleAdminSupported(functionSelectors)) {
return await getRoleMember({
contract,
index: BigInt(0),
role: "admin",
});
}
return null;
} catch {
return null;
}
}
@@ -1,6 +1,5 @@
import type { Meta, StoryObj } from "@storybook/nextjs";
import { maxUint256 } from "thirdweb/utils";
import { BadgeContainer } from "../../../../../../../../@/storybook/utils";
import { BadgeContainer } from "@/storybook/utils";
import { SupplyClaimedProgress } from "./supply-claimed-progress";
const meta = {
@@ -18,28 +17,57 @@ export const Variants: Story = {
function StoryVariants() {
return (
<div className="container max-w-md space-y-10 py-10">
<BadgeContainer label="5.5 / Unlimited Supply">
<SupplyClaimedProgress
claimedSupplyTokens={5.5}
totalSupplyTokens="unlimited"
/>
</BadgeContainer>
<BadgeContainer label="10 / Unlimited Supply">
<SupplyClaimedProgress claimedSupply={10n} totalSupply={maxUint256} />
<SupplyClaimedProgress
claimedSupplyTokens={10}
totalSupplyTokens="unlimited"
/>
</BadgeContainer>
<BadgeContainer label="500/1000 Supply">
<SupplyClaimedProgress claimedSupply={500n} totalSupply={1000n} />
<SupplyClaimedProgress
claimedSupplyTokens={500}
totalSupplyTokens={1000}
/>
</BadgeContainer>
<BadgeContainer label="123.45/1000 Supply">
<SupplyClaimedProgress
claimedSupplyTokens={123.45}
totalSupplyTokens={1000}
/>
</BadgeContainer>
<BadgeContainer label="10/1M Supply">
<SupplyClaimedProgress claimedSupply={10n} totalSupply={1000000n} />
<SupplyClaimedProgress
claimedSupplyTokens={10}
totalSupplyTokens={1000000}
/>
</BadgeContainer>
<BadgeContainer label="0/1M Supply">
<SupplyClaimedProgress claimedSupply={0n} totalSupply={1000000n} />
<SupplyClaimedProgress
claimedSupplyTokens={0}
totalSupplyTokens={1000000}
/>
</BadgeContainer>
<BadgeContainer label="10/10 Supply">
<SupplyClaimedProgress claimedSupply={10n} totalSupply={10n} />
<SupplyClaimedProgress
claimedSupplyTokens={10}
totalSupplyTokens={10}
/>
</BadgeContainer>
<BadgeContainer label="0 Supply - don't show anything">
<SupplyClaimedProgress claimedSupply={0n} totalSupply={0n} />
<SupplyClaimedProgress claimedSupplyTokens={0} totalSupplyTokens={0} />
</BadgeContainer>
</div>
);
@@ -1,19 +1,18 @@
import { InfinityIcon } from "lucide-react";
import { maxUint256 } from "thirdweb/utils";
import { Progress } from "@/components/ui/progress";
import { supplyFormatter } from "../nft/format";
export function SupplyClaimedProgress(props: {
claimedSupply: bigint;
totalSupply: bigint;
claimedSupplyTokens: number;
totalSupplyTokens: number | "unlimited";
}) {
// if total supply is unlimited
if (props.totalSupply === maxUint256) {
if (props.totalSupplyTokens === "unlimited") {
return (
<p className="flex items-center justify-between gap-2">
<span className="font-medium text-sm">Claimed Supply </span>
<span className="flex items-center gap-1 font-bold text-sm">
{supplyFormatter.format(props.claimedSupply)} /{" "}
{supplyFormatter.format(props.claimedSupplyTokens)} /{" "}
<InfinityIcon aria-label="Unlimited" className="size-4" />
</span>
</p>
@@ -21,29 +20,29 @@ export function SupplyClaimedProgress(props: {
}
// if total supply is 0 - don't show anything
if (props.totalSupply === 0n) {
if (props.totalSupplyTokens === 0) {
return null;
}
// multiply by 10k to have precision up to 2 decimal places in percentage value
const soldFractionTimes10KBigInt =
(props.claimedSupply * 10000n) / props.totalSupply;
const soldPercentage = Number(soldFractionTimes10KBigInt) / 100;
const soldPercentage =
(props.claimedSupplyTokens / props.totalSupplyTokens) * 100;
const percentToShow = soldPercentage.toFixed(2);
return (
<div className="space-y-2">
<div className="flex items-center justify-between">
<span className="font-medium text-sm">Supply Claimed</span>
<span className="font-bold text-sm">
{supplyFormatter.format(props.claimedSupply)} /{" "}
{supplyFormatter.format(props.totalSupply)}
{supplyFormatter.format(props.claimedSupplyTokens)} /{" "}
{supplyFormatter.format(props.totalSupplyTokens)}
</span>
</div>
<Progress className="h-2.5" value={soldPercentage} />
<p className="font-medium text-muted-foreground text-xs">
{soldPercentage === 0 && props.claimedSupply !== 0n && "~"}
{soldPercentage.toFixed(2)}% Sold
{percentToShow === "0.00" ? "~0.00%" : `${percentToShow}%`} Sold
</p>
</div>
);
@@ -0,0 +1,51 @@
import "server-only";
import { isProd } from "@/constants/env-utils";
import { DASHBOARD_THIRDWEB_SECRET_KEY } from "@/constants/server-envs";
export type TokenPriceData = {
price_usd: number;
price_usd_cents: number;
percent_change_24h: number;
market_cap_usd: number;
volume_24h_usd: number;
volume_change_24h: number;
holders: number;
historical_prices: Array<{
date: string;
price_usd: number;
price_usd_cents: number;
}>;
};
export async function getTokenPriceData(params: {
chainId: number;
contractAddress: string;
}) {
try {
const url = new URL(
`https://insight.${isProd ? "thirdweb" : "thirdweb-dev"}.com/v1/tokens/price`,
);
url.searchParams.set("include_historical_prices", "true");
url.searchParams.set("chain_id", params.chainId.toString());
url.searchParams.set("address", params.contractAddress);
url.searchParams.set("include_holders", "true");
const res = await fetch(url, {
headers: {
"x-secret-key": DASHBOARD_THIRDWEB_SECRET_KEY,
},
});
if (!res.ok) {
console.error("Failed to fetch token price data", await res.text());
return undefined;
}
const json = await res.json();
const priceData = json.data[0] as TokenPriceData | undefined;
return priceData;
} catch {
return undefined;
}
}
@@ -88,6 +88,25 @@ export const WithImageAndMultipleSocialUrls: Story = {
args: {
chainMetadata: ethereumChainMetadata,
clientContract: mockContract,
contractCreator: null,
image: mockTokenImage,
name: "Sample Token",
socialUrls: {
discord: mockSocialUrls.discord,
github: mockSocialUrls.github,
telegram: mockSocialUrls.telegram,
twitter: mockSocialUrls.twitter,
website: mockSocialUrls.website,
},
symbol: "SMPL",
},
};
export const WithContractCreator: Story = {
args: {
chainMetadata: ethereumChainMetadata,
clientContract: mockContract,
contractCreator: "0x1234567890123456789012345678901234567890",
image: mockTokenImage,
name: "Sample Token",
socialUrls: {
@@ -105,6 +124,7 @@ export const WithBrokenImageAndSingleSocialUrl: Story = {
args: {
chainMetadata: ethereumChainMetadata,
clientContract: mockContract,
contractCreator: null,
image: "broken-image.png",
name: "Sample Token",
socialUrls: {
@@ -118,6 +138,7 @@ export const WithoutImageAndNoSocialUrls: Story = {
args: {
chainMetadata: ethereumChainMetadata,
clientContract: mockContract,
contractCreator: null,
image: undefined,
name: "Sample Token",
socialUrls: {},
@@ -129,6 +150,7 @@ export const LongNameAndLotsOfSocialUrls: Story = {
args: {
chainMetadata: ethereumChainMetadata,
clientContract: mockContract,
contractCreator: null,
image: "https://thirdweb.com/chain-icons/ethereum.svg",
name: "This is a very long token name that should wrap to multiple lines",
socialUrls: {
@@ -148,6 +170,7 @@ export const AllSocialUrls: Story = {
args: {
chainMetadata: ethereumChainMetadata,
clientContract: mockContract,
contractCreator: null,
image: "https://thirdweb.com/chain-icons/ethereum.svg",
name: "Sample Token",
socialUrls: {
@@ -171,6 +194,7 @@ export const InvalidSocialUrls: Story = {
args: {
chainMetadata: ethereumChainMetadata,
clientContract: mockContract,
contractCreator: null,
image: "https://thirdweb.com/chain-icons/ethereum.svg",
name: "Sample Token",
socialUrls: {
@@ -188,6 +212,7 @@ export const SomeSocialUrls: Story = {
args: {
chainMetadata: ethereumChainMetadata,
clientContract: mockContract,
contractCreator: null,
image: "https://thirdweb.com/chain-icons/ethereum.svg",
name: "Sample Token",
socialUrls: {
@@ -1,7 +1,12 @@
import { ExternalLinkIcon, GlobeIcon, Settings2Icon } from "lucide-react";
import {
ExternalLinkIcon,
GlobeIcon,
Settings2Icon,
UserIcon,
} from "lucide-react";
import Link from "next/link";
import { useMemo } from "react";
import type { ThirdwebContract } from "thirdweb";
import { type ThirdwebContract, ZERO_ADDRESS } from "thirdweb";
import type { ChainMetadata } from "thirdweb/chains";
import { Img } from "@/components/blocks/Img";
import { Button } from "@/components/ui/button";
@@ -42,6 +47,8 @@ export function ContractHeaderUI(props: {
clientContract: ThirdwebContract;
socialUrls: object;
imageClassName?: string;
contractCreator: string | null;
className?: string;
}) {
const socialUrls = useMemo(() => {
const socialUrlsValue: { name: string; href: string }[] = [];
@@ -62,41 +69,45 @@ export function ContractHeaderUI(props: {
?.replace("Mainnet", "")
.trim();
const explorersToShow = getExplorersToShow(props.chainMetadata);
const validBlockExplorer = getExplorerToShow(props.chainMetadata);
return (
<div className="flex flex-col items-start gap-4 border-b border-dashed py-8 lg:flex-row lg:items-center">
{props.image && (
<Img
className={cn(
"size-20 shrink-0 rounded-full border bg-muted",
props.imageClassName,
)}
fallback={
<div className="flex items-center justify-center font-bold text-3xl text-muted-foreground/80">
{props.name[0]}
</div>
}
src={
props.image
? resolveSchemeWithErrorHandler({
client: props.clientContract.client,
uri: props.image,
})
: ""
}
/>
<div
className={cn(
"flex flex-col lg:flex-row lg:items-center gap-5 py-6 relative",
props.className,
)}
>
<div className="flex">
<div className="border p-1 rounded-full bg-card">
<Img
className={cn("size-28 shrink-0 rounded-full bg-muted")}
fallback={
<div className="flex items-center justify-center font-bold text-5xl text-muted-foreground/50 capitalize">
{props.name[0]}
</div>
}
src={
props.image
? resolveSchemeWithErrorHandler({
client: props.clientContract.client,
uri: props.image,
})
: ""
}
/>
</div>
</div>
<div className="flex flex-col gap-3">
<div className="flex flex-col gap-2.5 flex-1">
{/* top row */}
<div className="flex flex-col gap-3">
<div className="flex flex-col flex-wrap gap-3 lg:flex-row lg:items-center">
<h1 className="line-clamp-2 font-bold text-2xl tracking-tight lg:text-3xl">
<h1 className="font-semibold text-3xl tracking-tighter lg:text-5xl">
{props.name}
</h1>
<div className="flex flex-wrap gap-2">
<div className="flex flex-wrap gap-2 ">
<Link
className="flex w-fit shrink-0 items-center gap-2 rounded-3xl border border-border bg-card px-2.5 py-1.5 hover:bg-accent"
href={`/${props.chainMetadata.slug}`}
@@ -111,12 +122,15 @@ export function ContractHeaderUI(props: {
)}
</Link>
<CopyAddressButton
address={props.clientContract.address}
className="rounded-full bg-card w-[30px] h-[30px] p-0 [&>span]:hidden [&>svg]:text-foreground"
copyIconPosition="left"
variant="outline"
/>
{props.contractCreator &&
validBlockExplorer &&
props.contractCreator !== ZERO_ADDRESS && (
<SocialLink
href={`${validBlockExplorer.url}/address/${props.contractCreator}`}
name="Contract Owner"
icon={UserIcon}
/>
)}
{socialUrls
.toSorted((a, b) => {
@@ -146,7 +160,15 @@ export function ContractHeaderUI(props: {
</div>
{/* bottom row */}
<div className="flex flex-row flex-wrap items-center gap-2">
<div className="flex flex-col items-end sm:flex-row flex-wrap sm:items-center gap-2 absolute top-6 right-0 sm:static w-1/2 sm:w-full">
<CopyAddressButton
address={props.clientContract.address}
className="rounded-full bg-card px-2.5 py-1.5 text-xs"
copyIconPosition="left"
tooltip="Copy contract address"
variant="outline"
/>
<ToolTipLabel
contentClassName="max-w-[300px]"
label={
@@ -171,13 +193,13 @@ export function ContractHeaderUI(props: {
</Button>
</ToolTipLabel>
{explorersToShow?.map((validBlockExplorer) => (
{validBlockExplorer && (
<BadgeLink
href={`${validBlockExplorer.url.endsWith("/") ? validBlockExplorer.url : `${validBlockExplorer.url}/`}address/${props.clientContract.address}`}
key={validBlockExplorer.url}
name={validBlockExplorer.name}
/>
))}
)}
{/* TODO - render social links here */}
</div>
@@ -195,19 +217,22 @@ function isValidUrl(url: string) {
}
}
function getExplorersToShow(chainMetadata: ChainMetadata) {
const validBlockExplorers = chainMetadata.explorers
?.filter((e) => e.standard === "EIP3091")
?.slice(0, 2);
function getExplorerToShow(chainMetadata: ChainMetadata) {
const validBlockExplorers = chainMetadata.explorers?.filter(
(e) => e.standard === "EIP3091",
);
return validBlockExplorers?.slice(0, 1);
return validBlockExplorers?.[0];
}
function BadgeLink(props: { name: string; href: string }) {
function BadgeLink(props: { name: string; href: string; className?: string }) {
return (
<Button
asChild
className="!h-auto gap-2 rounded-full bg-card px-3 py-1.5 text-xs capitalize"
className={cn(
"!h-auto gap-2 rounded-full bg-card px-3 py-1.5 text-xs capitalize",
props.className,
)}
variant="outline"
>
<Link href={props.href} rel="noopener noreferrer" target="_blank">
@@ -2,45 +2,47 @@
import { useTheme } from "next-themes";
import type { Chain, ThirdwebClient } from "thirdweb";
import { PayEmbed } from "thirdweb/react";
import { getSDKTheme } from "../../../../../../../../../@/utils/sdk-component-theme";
import { BuyWidget } from "thirdweb/react";
import {
reportAssetBuyFailed,
reportAssetBuySuccessful,
} from "@/analytics/report";
import { parseError } from "@/utils/errorParser";
import { getSDKTheme } from "@/utils/sdk-component-theme";
export function BuyTokenEmbed(props: {
client: ThirdwebClient;
chain: Chain;
tokenSymbol: string;
tokenName: string;
tokenAddress: string;
}) {
const { theme } = useTheme();
return (
<PayEmbed
<BuyWidget
amount="1"
chain={props.chain}
className="!rounded-xl !w-full"
client={props.client}
connectOptions={{
autoConnect: false,
}}
payOptions={{
metadata: {
name: `Buy ${props.tokenSymbol}`,
},
mode: "fund_wallet",
prefillBuy: {
allowEdits: {
amount: true,
chain: false,
token: false,
},
amount: "1",
chain: props.chain,
token: {
address: props.tokenAddress,
name: props.tokenName,
symbol: props.tokenSymbol,
},
},
onError={(e) => {
const errorMessage = parseError(e);
reportAssetBuyFailed({
assetType: "coin",
chainId: props.chain.id,
contractType: "DropERC20",
error: errorMessage,
});
}}
onSuccess={() => {
reportAssetBuySuccessful({
assetType: "coin",
chainId: props.chain.id,
contractType: "DropERC20",
});
}}
theme={getSDKTheme(theme === "light" ? "light" : "dark")}
tokenAddress={props.tokenAddress as `0x${string}`}
/>
);
}
@@ -6,13 +6,11 @@ import { useMemo, useState } from "react";
import { ThirdwebAreaChart } from "@/components/blocks/charts/area-chart";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { SkeletonContainer } from "@/components/ui/skeleton";
import { ToolTipLabel } from "@/components/ui/tooltip";
import { cn } from "@/lib/utils";
import { useTokenPriceData } from "../_hooks/useTokenPriceData";
import type { TokenPriceData } from "../_apis/token-price-data";
function PriceChartUI(props: {
isPending: boolean;
showTimeOfDay: boolean;
data: Array<{
date: string;
@@ -29,7 +27,7 @@ function PriceChartUI(props: {
<ThirdwebAreaChart
cardContentClassName="p-0"
chartClassName="aspect-[1.5] lg:aspect-[3]"
className="border-none bg-background p-0"
className="border-none bg-transparent p-0"
config={{
price: {
color: "hsl(var(--chart-1))",
@@ -38,7 +36,7 @@ function PriceChartUI(props: {
}}
data={data}
hideLabel={false}
isPending={props.isPending}
isPending={false}
toolTipLabelFormatter={getTooltipLabelFormatter(props.showTimeOfDay)}
toolTipValueFormatter={(value) => {
return tokenPriceUSDFormatter.format(value as number);
@@ -49,7 +47,7 @@ function PriceChartUI(props: {
const tokenPriceUSDFormatter = new Intl.NumberFormat("en-US", {
currency: "USD",
maximumFractionDigits: 10,
maximumFractionDigits: 8,
minimumFractionDigits: 0,
notation: "compact",
roundingMode: "halfEven",
@@ -92,20 +90,14 @@ function getTooltipLabelFormatter(includeTimeOfDay: boolean) {
export function TokenStats(params: {
chainId: number;
contractAddress: string;
tokenPriceData: TokenPriceData;
}) {
const tokenPriceQuery = useTokenPriceData(params);
const [interval, setInterval] = useState<Interval>("max");
const tokenPriceData = tokenPriceQuery.data;
const filteredHistoricalPrices = useMemo(() => {
const currentDate = new Date();
if (tokenPriceData?.type === "no-data") {
return [];
}
return tokenPriceData?.data?.historical_prices.filter((item) => {
return params.tokenPriceData.historical_prices.filter((item) => {
const date = new Date(item.date);
const maxDiff =
interval === "24h"
@@ -120,106 +112,75 @@ export function TokenStats(params: {
return differenceInCalendarDays(currentDate, date) <= maxDiff;
});
}, [tokenPriceData, interval]);
}, [params.tokenPriceData, interval]);
const priceUsd =
tokenPriceData?.type === "no-data" || tokenPriceQuery.isError
? "N/A"
: tokenPriceData?.data?.price_usd;
const priceUsd = params.tokenPriceData.price_usd;
const percentChange24h = params.tokenPriceData.percent_change_24h;
const marketCap = params.tokenPriceData.market_cap_usd;
const holders = params.tokenPriceData.holders;
const percentChange24h =
tokenPriceData?.type === "no-data" || tokenPriceQuery.isError
? "N/A"
: tokenPriceData?.data?.percent_change_24h;
const formattedAbsChange = percentChangeFormatter.format(
Math.abs(percentChange24h),
);
const marketCap =
tokenPriceData?.type === "no-data" || tokenPriceQuery.isError
? "N/A"
: tokenPriceData?.data?.market_cap_usd;
const isAlmostZeroChange =
formattedAbsChange === percentChangeFormatter.format(0);
const holders =
tokenPriceData?.type === "no-data" || tokenPriceQuery.isError
? "N/A"
: tokenPriceData?.data?.holders;
const formattedPriceUSD = tokenPriceUSDFormatter.format(priceUsd);
return (
<div>
<div className="bg-card rounded-lg border">
{/* price and change */}
<div className="flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between">
<div className="flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between p-4 lg:p-6">
<div>
<p className="mb-1 text-muted-foreground text-sm">Current Price</p>
<div className="flex items-center gap-2">
<SkeletonContainer
loadedData={priceUsd}
render={(v) => {
return (
<p className="font-bold text-4xl tracking-tight">
{typeof v === "number"
? tokenPriceUSDFormatter.format(v)
: v}
</p>
);
}}
skeletonData={10.0001}
/>
<SkeletonContainer
loadedData={percentChange24h}
render={(data) => {
if (typeof data === "string") {
return null;
}
const formattedAbsChange = percentChangeFormatter.format(
Math.abs(data),
);
const isAlmostZero =
formattedAbsChange === percentChangeFormatter.format(0);
return (
<Badge
className="gap-2 text-sm"
variant={
isAlmostZero
? "default"
: data > 0
? "success"
: "destructive"
}
>
<div className="flex items-center gap-0.5">
{isAlmostZero ? null : data > 0 ? (
<ArrowUpIcon className="size-3" />
) : (
<ArrowDownIcon className="size-3" />
)}
<span>
{isAlmostZero ? "~0%" : `${formattedAbsChange}%`}
</span>
</div>
<span>(1d)</span>
</Badge>
);
}}
skeletonData={0.001}
/>
<div className="flex items-center gap-3">
<p
className={cn(
"font-bold text-3xl lg:text-4xl tracking-tight",
formattedPriceUSD.length > 8 && "text-2xl lg:text-4xl",
)}
>
{typeof priceUsd === "number" ? formattedPriceUSD : priceUsd}
</p>
<Badge
className="gap-2 lg:text-sm text-xs px-1.5"
variant={
isAlmostZeroChange
? "default"
: percentChange24h > 0
? "success"
: "destructive"
}
>
<div className="flex items-center gap-0.5">
{isAlmostZeroChange ? null : percentChange24h > 0 ? (
<ArrowUpIcon className="size-3" />
) : (
<ArrowDownIcon className="size-3" />
)}
<span>
{isAlmostZeroChange ? "~0%" : `${formattedAbsChange}%`}
</span>
</div>
<span>(1d)</span>
</Badge>
</div>
</div>
<IntervalSelector interval={interval} setInterval={setInterval} />
</div>
<div className="h-4" />
<PriceChartUI
data={filteredHistoricalPrices || []}
isPending={tokenPriceQuery.isPending}
showTimeOfDay={interval === "24h"}
/>
<div className="px-0">
<PriceChartUI
data={filteredHistoricalPrices || []}
showTimeOfDay={interval === "24h"}
/>
</div>
<div className="mt-8 flex gap-6 border-y border-dashed py-4 [&>*:not(:first-child)]:border-l [&>*:not(:first-child)]:border-dashed [&>*:not(:first-child)]:pl-5 [&>*]:grow">
<div className="px-4 lg:px-6 py-4 mt-6 border-t border-dashed flex gap-6 [&>*:not(:first-child)]:border-l [&>*:not(:first-child)]:pl-5 [&>*]:grow">
<TokenStat
label="Market Cap"
skeletonData={"1000000000"}
tooltip="Market capitalization is the total value of all tokens in circulation, calculated by multiplying the current price by the circulating supply"
value={
typeof marketCap === "number"
@@ -230,7 +191,6 @@ export function TokenStats(params: {
<TokenStat
label="Number of Holders"
skeletonData={"1000000000"}
tooltip="The total number of unique wallet addresses that currently hold this token on the blockchain"
value={
// show 0 value as N/A
@@ -246,9 +206,8 @@ export function TokenStats(params: {
);
}
function TokenStat<T extends string | number>(props: {
value: T | undefined;
skeletonData: T;
function TokenStat(props: {
value: string | number;
label: string;
tooltip: string;
}) {
@@ -261,17 +220,9 @@ function TokenStat<T extends string | number>(props: {
</ToolTipLabel>
</div>
<div className="flex">
<SkeletonContainer
loadedData={props.value}
render={(v) => {
return (
<p className="font-semibold text-foreground text-xl tracking-tight">
{v}
</p>
);
}}
skeletonData={props.skeletonData}
/>
<p className="font-semibold text-foreground text-xl tracking-tight">
{props.value}
</p>
</div>
</div>
);
@@ -296,7 +247,7 @@ function IntervalSelector(props: {
{Object.entries(intervals).map(([key, value]) => (
<Button
className={cn(
"rounded-full",
"rounded-full px-2.5 py-1.5 h-auto bg-background text-xs lg:text-sm",
props.interval === key && "border-active-border bg-accent",
)}
key={key}
@@ -47,8 +47,8 @@ function RecentTransfersUI(props: {
}) {
return (
<div>
<div className="mb-4">
<h2 className="font-semibold text-xl tracking-tight">
<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">
Recent Transfers
</h2>
<p className="text-muted-foreground text-sm">
@@ -57,7 +57,7 @@ function RecentTransfersUI(props: {
</p>
</div>
<TableContainer className="rounded-b-none">
<TableContainer className="rounded-b-none rounded-t-none">
<Table>
<TableHeader>
<TableRow>
@@ -159,7 +159,7 @@ function RecentTransfersUI(props: {
<div className="flex items-center justify-end gap-3 rounded-b-lg border-x border-b bg-card px-6 py-5">
<Button
className="gap-1.5 bg-background"
className="gap-1.5 bg-background rounded-full"
disabled={props.page === 0 || props.isPending}
onClick={() => props.setPage(props.page - 1)}
size="sm"
@@ -169,7 +169,7 @@ function RecentTransfersUI(props: {
Previous
</Button>
<Button
className="gap-1.5 bg-background"
className="gap-1.5 bg-background rounded-full"
disabled={props.isPending || props.data.length < props.rowsPerPage}
onClick={() => props.setPage(props.page + 1)}
size="sm"
@@ -222,23 +222,21 @@ export function RecentTransfers(props: {
});
return (
<div>
<RecentTransfersUI
client={props.clientContract.client}
data={tokenQuery.data ?? []}
explorerUrl={
props.chainMetadata.explorers?.[0]?.url ||
`https://thirdweb.com/${props.chainMetadata.slug}`
}
isPending={tokenQuery.isPending}
page={page}
rowsPerPage={rowsPerPage}
setPage={setPage}
tokenMetadata={{
decimals: props.decimals,
symbol: props.tokenSymbol,
}}
/>
</div>
<RecentTransfersUI
client={props.clientContract.client}
data={tokenQuery.data ?? []}
explorerUrl={
props.chainMetadata.explorers?.[0]?.url ||
`https://thirdweb.com/${props.chainMetadata.slug}`
}
isPending={tokenQuery.isPending}
page={page}
rowsPerPage={rowsPerPage}
setPage={setPage}
tokenMetadata={{
decimals: props.decimals,
symbol: props.tokenSymbol,
}}
/>
);
}
@@ -26,7 +26,7 @@ import {
getApprovalForTransaction,
} from "thirdweb/extensions/erc20";
import { useActiveAccount, useSendTransaction } from "thirdweb/react";
import { getClaimParams } from "thirdweb/utils";
import { getClaimParams, maxUint256 } from "thirdweb/utils";
import {
reportAssetBuyFailed,
reportAssetBuySuccessful,
@@ -39,8 +39,8 @@ import { Spinner } from "@/components/ui/Spinner/Spinner";
import { ToolTipLabel } from "@/components/ui/tooltip";
import { cn } from "@/lib/utils";
import { parseError } from "@/utils/errorParser";
import { getSDKTheme } from "@/utils/sdk-component-theme";
import { tryCatch } from "@/utils/try-catch";
import { getSDKTheme } from "../../../../../../../../../../@/utils/sdk-component-theme";
import { PublicPageConnectButton } from "../../../_components/PublicPageConnectButton";
import { SupplyClaimedProgress } from "../../../_components/supply-claimed-progress";
import { TokenPrice } from "../../../_components/token-price";
@@ -325,12 +325,19 @@ export function TokenDropClaim(props: {
<div className="h-4" />
<SupplyClaimedProgress
claimedSupply={BigInt(
claimedSupplyTokens={Number(
toTokens(props.claimCondition.supplyClaimed, props.decimals),
)}
totalSupply={BigInt(
toTokens(props.claimCondition.maxClaimableSupply, props.decimals),
)}
totalSupplyTokens={
props.claimCondition.maxClaimableSupply === maxUint256
? "unlimited"
: Number(
toTokens(
props.claimCondition.maxClaimableSupply,
props.decimals,
),
)
}
/>
<div className="h-4" />
@@ -29,38 +29,38 @@ export function ContractAnalyticsOverview(props: {
});
return (
<div>
<div className="flex flex-col">
<h2 className="font-semibold text-xl tracking-tight">Analytics</h2>
<p className="mb-3 text-muted-foreground text-sm">
View trends in unique wallets, transactions, and events over time for
this contract
</p>
</div>
<ThirdwebAreaChart
cardContentClassName="p-0"
chartClassName="aspect-[1.5] lg:aspect-[3]"
className="border-none bg-background p-0"
config={{
events: {
color: "hsl(var(--chart-3))",
label: "Events",
},
transactions: {
color: "hsl(var(--chart-2))",
label: "Transactions",
},
wallets: {
color: "hsl(var(--chart-1))",
label: "Unique Wallets",
},
}}
data={data || []}
hideLabel={false}
isPending={isPending}
showLegend
toolTipLabelFormatter={toolTipLabelFormatterWithPrecision(precision)}
/>
</div>
<ThirdwebAreaChart
chartClassName="aspect-[1.2] lg:aspect-[3]"
cardContentClassName="px-0 pb-0"
margin={{
bottom: 20,
}}
header={{
headerClassName: "p-4 lg:p-6",
title: "Analytics",
titleClassName: "font-semibold text-2xl mb-0.5 tracking-tight",
description:
"View trends of transactions, events and unique wallets interacting with this contract over time",
}}
config={{
events: {
color: "hsl(var(--chart-5))",
label: "Events",
},
transactions: {
color: "hsl(var(--chart-8))",
label: "Transactions",
},
wallets: {
color: "hsl(var(--chart-1))",
label: "Unique Wallets",
},
}}
data={data || []}
hideLabel={false}
isPending={isPending}
showLegend
toolTipLabelFormatter={toolTipLabelFormatterWithPrecision(precision)}
/>
);
}
@@ -1,56 +0,0 @@
import { useQuery } from "@tanstack/react-query";
import { isProd } from "@/constants/env-utils";
import { NEXT_PUBLIC_DASHBOARD_CLIENT_ID } from "@/constants/public-envs";
type TokenPriceData = {
price_usd: number;
price_usd_cents: number;
percent_change_24h: number;
market_cap_usd: number;
volume_24h_usd: number;
volume_change_24h: number;
holders: number;
historical_prices: Array<{
date: string;
price_usd: number;
price_usd_cents: number;
}>;
};
export function useTokenPriceData(params: {
chainId: number;
contractAddress: string;
}) {
return useQuery({
queryFn: async () => {
const url = new URL(
`https://insight.${isProd ? "thirdweb" : "thirdweb-dev"}.com/v1/tokens/price`,
);
url.searchParams.set("include_historical_prices", "true");
url.searchParams.set("chain_id", params.chainId.toString());
url.searchParams.set("address", params.contractAddress);
url.searchParams.set("include_holders", "true");
url.searchParams.set("clientId", NEXT_PUBLIC_DASHBOARD_CLIENT_ID);
const res = await fetch(url);
if (!res.ok) {
throw new Error(await res.text());
}
const json = await res.json();
const priceData = json.data[0] as TokenPriceData | undefined;
return priceData
? {
data: priceData,
type: "data-found" as const,
}
: { type: "no-data" as const };
},
queryKey: ["token-price-chart", params.chainId, params.contractAddress],
refetchInterval: 5000,
refetchOnWindowFocus: false,
retry: false,
retryOnMount: false,
});
}

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