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
539 changed files with 14316 additions and 9712 deletions
+3 -1
View File
@@ -9,7 +9,7 @@ module.exports = [
// -- build category
"/wallets",
"/account-abstraction",
"/universal-bridge",
"/payments",
"/auth",
"/in-app-wallets",
"/transactions",
@@ -62,4 +62,6 @@ module.exports = [
// -- universal bridge landing pages --
"/universal-bridge-regions/:region_slug",
"/enterprise",
"/token",
"/vault",
];
+11 -1
View File
@@ -416,7 +416,7 @@ async function redirects() {
source: "/connect/account-abstraction",
},
{
destination: "/universal-bridge",
destination: "/payments",
permanent: false,
source: "/connect/universal-bridge",
},
@@ -440,9 +440,19 @@ async function redirects() {
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,
@@ -1,15 +1,16 @@
import "server-only";
import { unstable_cache } from "next/cache";
import { PostHog } from "posthog-node";
let posthogServer: PostHog | null = null;
let _posthogClient: PostHog | null = null;
function getPostHogServer(): PostHog | null {
if (!posthogServer && process.env.NEXT_PUBLIC_POSTHOG_KEY) {
posthogServer = new PostHog(process.env.NEXT_PUBLIC_POSTHOG_KEY, {
if (!_posthogClient && process.env.NEXT_PUBLIC_POSTHOG_KEY) {
_posthogClient = new PostHog(process.env.NEXT_PUBLIC_POSTHOG_KEY, {
host: "https://us.i.posthog.com",
});
}
return posthogServer;
return _posthogClient;
}
/**
@@ -17,29 +18,40 @@ function getPostHogServer(): PostHog | null {
* @param flagKey - The feature flag key
* @param userEmail - The user's email address for filtering
*/
export async function isFeatureFlagEnabled(
flagKey: string,
userEmail?: string,
): Promise<boolean> {
// For localdev environments where Posthog is not running, enable all feature flags.
if (!posthogServer) {
return true;
}
try {
const client = getPostHogServer();
if (client && userEmail) {
const isEnabled = await client.isFeatureEnabled(flagKey, userEmail, {
personProperties: {
email: userEmail,
},
});
if (isEnabled !== undefined) {
return isEnabled;
}
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;
}
} catch (error) {
console.error(`Error checking feature flag ${flagKey}:`, error);
}
return false;
}
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
},
);
+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"}`,
};
}
}
+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);
}
+52 -2
View File
@@ -37,7 +37,7 @@ type WebhookConfigsResponse =
};
interface CreateWebhookConfigRequest {
topics: { id: string; filters: object | null }[];
topicIdsWithFilters: { id: string; filters: object | null }[];
destinationUrl: string;
description: string;
isPaused?: boolean;
@@ -76,7 +76,7 @@ type TopicsResponse =
interface UpdateWebhookConfigRequest {
destinationUrl?: string;
topics?: { id: string; filters: object | null }[];
topicIdsWithFilters?: { id: string; filters: object | null }[];
description?: string;
isPaused?: boolean;
}
@@ -310,3 +310,53 @@ export async function deleteWebhookConfig(props: {
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();
}
@@ -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
@@ -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">
@@ -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";
@@ -47,7 +47,7 @@ export function AnnouncementBanner() {
return (
<AnnouncementBannerUI
href="https://blog.thirdweb.com/the-fastest-way-to-build-web3-applications/"
label="We have re-branded our Engine, Universal Bridge, and Connect products. Please read the full blog post for details on changes"
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,6 +38,7 @@ 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",
@@ -63,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}
@@ -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 -5
View File
@@ -2,6 +2,7 @@ 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
@@ -311,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;
}
@@ -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}
+8 -1
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,
+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>
@@ -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>
);
@@ -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,10 +1,10 @@
import { getAddress, getContract, isAddress } from "thirdweb";
import { localhost } from "thirdweb/chains";
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;
@@ -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,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",
@@ -17,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="unlimited" />
<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>
);
@@ -3,16 +3,16 @@ import { Progress } from "@/components/ui/progress";
import { supplyFormatter } from "../nft/format";
export function SupplyClaimedProgress(props: {
claimedSupply: bigint;
totalSupply: bigint | "unlimited";
claimedSupplyTokens: number;
totalSupplyTokens: number | "unlimited";
}) {
// if total supply is unlimited
if (props.totalSupply === "unlimited") {
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>
@@ -20,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;
}
}
@@ -1,19 +0,0 @@
import type { ThirdwebContract } from "thirdweb";
import { WalletAddress } from "@/components/blocks/wallet-address";
export function ContractCreatorBadge(props: {
contractCreator: string;
clientContract: ThirdwebContract;
}) {
return (
<div className="flex items-center gap-1.5 bg-card rounded-full px-2.5 py-1.5 border hover:bg-accent">
<span className="text-xs text-foreground">By</span>
<WalletAddress
address={props.contractCreator}
className="py-0 text-xs h-auto !no-underline"
client={props.clientContract.client}
iconClassName="size-3.5 hidden"
/>
</div>
);
}
@@ -1,4 +1,9 @@
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, ZERO_ADDRESS } from "thirdweb";
@@ -19,7 +24,6 @@ import { YoutubeIcon } from "@/icons/brand-icons/YoutubeIcon";
import { ChainIconClient } from "@/icons/ChainIcon";
import { cn } from "@/lib/utils";
import { resolveSchemeWithErrorHandler } from "@/utils/resolveSchemeWithErrorHandler";
import { ContractCreatorBadge } from "./ContractCreatorBadge";
const platformToIcons: Record<string, React.FC<{ className?: string }>> = {
discord: DiscordIcon,
@@ -44,6 +48,7 @@ export function ContractHeaderUI(props: {
socialUrls: object;
imageClassName?: string;
contractCreator: string | null;
className?: string;
}) {
const socialUrls = useMemo(() => {
const socialUrlsValue: { name: string; href: string }[] = [];
@@ -64,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}`}
@@ -113,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) => {
@@ -148,13 +160,14 @@ export function ContractHeaderUI(props: {
</div>
{/* bottom row */}
<div className="flex flex-row flex-wrap items-center gap-2">
{props.contractCreator && props.contractCreator !== ZERO_ADDRESS && (
<ContractCreatorBadge
clientContract={props.clientContract}
contractCreator={props.contractCreator}
/>
)}
<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]"
@@ -180,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>
@@ -204,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">
@@ -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,
}}
/>
);
}
@@ -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,13 +325,13 @@ export function TokenDropClaim(props: {
<div className="h-4" />
<SupplyClaimedProgress
claimedSupply={BigInt(
claimedSupplyTokens={Number(
toTokens(props.claimCondition.supplyClaimed, props.decimals),
)}
totalSupply={
totalSupplyTokens={
props.claimCondition.maxClaimableSupply === maxUint256
? "unlimited"
: BigInt(
: Number(
toTokens(
props.claimCondition.maxClaimableSupply,
props.decimals,
@@ -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.2] lg:aspect-[2.6]"
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,
});
}
@@ -1,4 +1,6 @@
import "server-only";
import { isProd } from "@/constants/env-utils";
import { DASHBOARD_THIRDWEB_SECRET_KEY } from "@/constants/server-envs";
export async function fetchTokenInfoFromBridge(params: {
chainId: number;
@@ -10,7 +12,7 @@ export async function fetchTokenInfoFromBridge(params: {
`https://bridge.${isProd ? "thirdweb.com" : "thirdweb-dev.com"}/v1/tokens?chainId=${params.chainId}&tokenAddress=${params.tokenAddress}`,
{
headers: {
"x-client-id": params.clientId,
"x-secret-key": DASHBOARD_THIRDWEB_SECRET_KEY,
},
},
);
@@ -2,9 +2,11 @@ import type { ThirdwebContract } from "thirdweb";
import type { ChainMetadata } from "thirdweb/chains";
import { getContractMetadata } from "thirdweb/extensions/common";
import { decimals, getActiveClaimCondition } from "thirdweb/extensions/erc20";
import { GridPattern } from "@/components/ui/background-patterns";
import { resolveFunctionSelectors } from "@/lib/selectors";
import { getContractCreator } from "../_components/getContractCreator";
import { PageHeader } from "../_components/PageHeader";
import { getTokenPriceData } from "./_apis/token-price-data";
import { ContractHeaderUI } from "./_components/ContractHeader";
import { TokenDropClaim } from "./_components/claim-tokens/claim-tokens-ui";
import { ContractAnalyticsOverview } from "./_components/contract-analytics/contract-analytics";
@@ -25,6 +27,7 @@ export async function ERC20PublicPage(props: {
tokenDecimals,
tokenInfo,
functionSelectors,
tokenPriceData,
] = await Promise.all([
getContractMetadata({
contract: props.serverContract,
@@ -39,6 +42,10 @@ export async function ERC20PublicPage(props: {
tokenAddress: props.serverContract.address,
}),
resolveFunctionSelectors(props.serverContract),
getTokenPriceData({
chainId: props.serverContract.chain.id,
contractAddress: props.serverContract.address,
}),
]);
if (!contractMetadata.image && tokenInfo) {
@@ -78,62 +85,73 @@ export async function ERC20PublicPage(props: {
return (
<div className="flex grow flex-col">
<PageHeader />
<div className="container flex max-w-8xl grow flex-col">
<ContractHeaderUI
chainMetadata={props.chainMetadata}
clientContract={props.clientContract}
contractCreator={contractCreator}
image={contractMetadata.image}
name={contractMetadata.name}
socialUrls={
typeof contractMetadata.social_urls === "object" &&
contractMetadata.social_urls !== null
? contractMetadata.social_urls
: {}
}
symbol={contractMetadata.symbol}
/>
<PageHeader containerClassName="max-w-5xl" />
<div className="h-6" />
<div className="border-b">
<div className="container max-w-5xl">
<ContractHeaderUI
chainMetadata={props.chainMetadata}
clientContract={props.clientContract}
contractCreator={contractCreator}
image={contractMetadata.image}
name={contractMetadata.name}
socialUrls={
typeof contractMetadata.social_urls === "object" &&
contractMetadata.social_urls !== null
? contractMetadata.social_urls
: {}
}
symbol={contractMetadata.symbol}
/>
</div>
</div>
<div className="flex flex-col gap-8 pb-20 xl:flex-row">
<div className="flex grow flex-col gap-8 overflow-hidden">
{activeClaimCondition ? (
<div className="border-b border-dashed pb-6">
<ContractAnalyticsOverview
chainId={props.chainMetadata.chainId}
chainSlug={props.chainMetadata.slug}
contractAddress={props.clientContract.address}
/>
</div>
) : (
<div className="container flex max-w-5xl grow flex-col pt-8 pb-10">
<div className="flex grow flex-col gap-8">
<div className="sm:flex sm:justify-center w-full sm:border sm:border-dashed sm:bg-accent/20 sm:py-12 rounded-lg overflow-hidden relative">
<GridPattern
width={30}
height={30}
x={-1}
y={-1}
strokeDasharray={"4 2"}
className="text-border dark:text-border/70"
style={{
maskImage:
"linear-gradient(to bottom right,white,transparent,transparent)",
}}
/>
<div className="sm:w-[420px] z-10">{buyEmbed}</div>
</div>
{tokenPriceData ? (
<>
<TokenStats
chainId={props.chainMetadata.chainId}
contractAddress={props.clientContract.address}
tokenPriceData={tokenPriceData}
/>
)}
<div className="xl:hidden">{buyEmbed}</div>
<RecentTransfers
chainMetadata={props.chainMetadata}
clientContract={props.clientContract}
decimals={tokenDecimals}
tokenSymbol={contractMetadata.symbol}
/>
{!activeClaimCondition && (
<ContractAnalyticsOverview
chainId={props.chainMetadata.chainId}
chainSlug={props.chainMetadata.slug}
contractAddress={props.clientContract.address}
/>
)}
</div>
<div className="hidden xl:block xl:w-96">
<div className="-mt-6 sticky top-0 pt-6">{buyEmbed}</div>
</div>
</>
) : (
<ContractAnalyticsOverview
chainId={props.chainMetadata.chainId}
chainSlug={props.chainMetadata.slug}
contractAddress={props.clientContract.address}
/>
)}
<RecentTransfers
chainMetadata={props.chainMetadata}
clientContract={props.clientContract}
decimals={tokenDecimals}
tokenSymbol={contractMetadata.symbol}
/>
</div>
</div>
</div>
@@ -17,26 +17,29 @@ export function NFTPublicPageLayout(props: {
return (
<div className="flex grow flex-col">
<PageHeader containerClassName="max-w-8xl" />
<div className="border-b">
<div className="container max-w-8xl">
<ContractHeaderUI
chainMetadata={props.chainMetadata}
clientContract={props.clientContract}
contractCreator={props.contractCreator}
image={
typeof props.contractMetadata.image === "string"
? props.contractMetadata.image
: undefined
}
name={props.contractMetadata.name}
socialUrls={
typeof props.contractMetadata.social_urls === "object" &&
props.contractMetadata.social_urls !== null
? props.contractMetadata.social_urls
: {}
}
symbol={props.contractMetadata.symbol}
/>
</div>
</div>
<div className="container flex max-w-8xl grow flex-col">
<ContractHeaderUI
chainMetadata={props.chainMetadata}
clientContract={props.clientContract}
contractCreator={props.contractCreator}
image={
typeof props.contractMetadata.image === "string"
? props.contractMetadata.image
: undefined
}
imageClassName="rounded-lg"
name={props.contractMetadata.name}
socialUrls={
typeof props.contractMetadata.social_urls === "object" &&
props.contractMetadata.social_urls !== null
? props.contractMetadata.social_urls
: {}
}
symbol={props.contractMetadata.symbol}
/>
{props.children}
</div>
</div>
@@ -10,6 +10,7 @@ import type { ChainMetadata } from "thirdweb/chains";
import { getApprovalForTransaction } from "thirdweb/extensions/erc20";
import { claimTo } from "thirdweb/extensions/erc1155";
import { useActiveAccount, useSendAndConfirmTransaction } from "thirdweb/react";
import { maxUint256 } from "thirdweb/utils";
import * as z from "zod";
import {
reportAssetBuyFailed,
@@ -225,8 +226,12 @@ export function BuyEditionDrop(props: BuyEditionDropProps) {
{claimCondition.data ? (
<SupplyClaimedProgress
claimedSupply={claimCondition.data.supplyClaimed}
totalSupply={claimCondition.data.maxClaimableSupply}
claimedSupplyTokens={Number(claimCondition.data.supplyClaimed)}
totalSupplyTokens={
claimCondition.data.maxClaimableSupply === maxUint256
? "unlimited"
: Number(claimCondition.data.maxClaimableSupply)
}
/>
) : (
<Skeleton className="h-[62px] w-full" />
@@ -231,8 +231,10 @@ export function BuyNFTDropUI(props: BuyNFTDropUIProps) {
)}
<SupplyClaimedProgress
claimedSupply={BigInt(props.totalNFTs) - props.totalUnclaimedSupply}
totalSupply={BigInt(props.totalNFTs)}
claimedSupplyTokens={Number(
BigInt(props.totalNFTs) - props.totalUnclaimedSupply,
)}
totalSupplyTokens={Number(props.totalNFTs)}
/>
<div className="flex flex-col gap-1.5 rounded-lg bg-muted/50 p-3">
@@ -1,6 +1,7 @@
import { ChevronDownIcon } from "lucide-react";
import { headers } from "next/headers";
import Link from "next/link";
import { getAuthToken } from "@/api/auth-token";
import {
Breadcrumb,
BreadcrumbItem,
@@ -14,7 +15,6 @@ import {
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { getAuthToken } from "../../../../../../@/api/auth-token";
import {
AllFilters,
ChainOptionsFilter,
@@ -1,6 +1,6 @@
import type { Metadata } from "next";
import { headers } from "next/headers";
import { getAuthToken } from "../../../../../@/api/auth-token";
import { getAuthToken } from "@/api/auth-token";
import {
AllFilters,
ChainOptionsFilter,
@@ -3,8 +3,8 @@ import "server-only";
import { DASHBOARD_THIRDWEB_SECRET_KEY } from "@/constants/server-envs";
import { getConfiguredThirdwebClient } from "@/constants/thirdweb.server";
import { cn } from "@/lib/utils";
import { fallbackChainIcon } from "@/utils/chain-icons";
import { resolveSchemeWithErrorHandler } from "@/utils/resolveSchemeWithErrorHandler";
import { fallbackChainIcon } from "../../../../../../@/utils/chain-icons";
export async function ChainIcon(props: {
iconUrl?: string;
@@ -2,9 +2,9 @@ import { ChakraProviderSetup } from "chakra/ChakraProviderSetup";
import { revalidatePath } from "next/cache";
import { notFound, redirect } from "next/navigation";
import { fetchDeployMetadata } from "thirdweb/contract";
import { getUserThirdwebClient } from "@/api/auth-token";
import { ContractPublishForm } from "@/components/contract-components/contract-publish-form";
import { getActiveAccountCookie, getJWTCookie } from "@/constants/cookie";
import { getClientThirdwebClient } from "@/constants/thirdweb-client.client";
import { serverThirdwebClient } from "@/constants/thirdweb-client.server";
import { getLatestPublishedContractsWithPublisherMapping } from "../../../published-contract/[publisher]/[contract_id]/utils/getPublishedContractsWithPublisherMapping";
@@ -68,14 +68,15 @@ export default async function PublishContractPage(
redirect(`/login?next=${encodeURIComponent(pathname)}`);
}
const userThirdwebClient = await getUserThirdwebClient({
teamId: undefined,
});
return (
<div className="container flex max-w-[1130px] flex-col gap-8 py-8">
<ChakraProviderSetup>
<ContractPublishForm
client={getClientThirdwebClient({
jwt: token,
teamId: undefined,
})}
client={userThirdwebClient}
isLoggedIn={!!token}
onPublishSuccess={async () => {
"use server";
@@ -1,5 +1,5 @@
import { AppFooter } from "@/components/footers/app-footer";
import { ErrorProvider } from "../../../@/contexts/error-handler";
import { ErrorProvider } from "@/contexts/error-handler";
export default function DashboardLayout(props: { children: React.ReactNode }) {
return (
@@ -1,10 +1,10 @@
import { ChakraProviderSetup } from "chakra/ChakraProviderSetup";
import { notFound } from "next/navigation";
import { getAuthToken } from "@/api/auth-token";
import { PublishedContract } from "@/components/contracts/published-contract";
import { Separator } from "@/components/ui/separator";
import { getClientThirdwebClient } from "@/constants/thirdweb-client.client";
import { serverThirdwebClient } from "@/constants/thirdweb-client.server";
import { getAuthToken } from "../../../../../../@/api/auth-token";
import { getRawAccount } from "../../../../account/settings/getAccount";
import { PublishedActions } from "../../components/contract-actions-published.client";
import { DeployContractHeader } from "../../components/contract-header";
@@ -4,7 +4,7 @@ import { getUserThirdwebClient } from "@/api/auth-token";
import { getProjects } from "@/api/projects";
import { getTeams } from "@/api/team";
import { CustomContractForm } from "@/components/contract-components/contract-deploy-form/custom-contract";
import { loginRedirect } from "../../../login/loginRedirect";
import { loginRedirect } from "@/utils/redirects";
type DeployFormForUriProps = {
contractMetadata: FetchDeployMetadataResult | null;
@@ -1,67 +0,0 @@
import { type ReactElement, useState } from "react";
import { AttachmentForm } from "../shared/SupportForm_AttachmentUploader";
import { DescriptionInput } from "../shared/SupportForm_DescriptionInput";
import { SupportForm_SelectInput } from "../shared/SupportForm_SelectInput";
type ProblemAreaItem = {
label: string;
component: ReactElement;
};
const ACCOUNT_PROBLEM_AREAS: ProblemAreaItem[] = [
{
component: (
<>
<DescriptionInput />
<AttachmentForm />
</>
),
label: "Pricing inquiry",
},
{
component: (
<>
<DescriptionInput />
<AttachmentForm />
</>
),
label: "Billing inquiry",
},
{
component: (
<>
<DescriptionInput />
<AttachmentForm />
</>
),
label: "Usage inquiry",
},
{
component: (
<>
<DescriptionInput />
<AttachmentForm />
</>
),
label: "Other",
},
];
export default function AccountSupportForm() {
const [problemArea, setProblemArea] = useState<string>("");
return (
<>
<SupportForm_SelectInput
formLabel="Problem area"
name="extraInfo_Problem_Area"
onValueChange={setProblemArea}
options={ACCOUNT_PROBLEM_AREAS.map((o) => o.label)}
promptText="Select a problem area"
required={true}
value={problemArea}
/>
{ACCOUNT_PROBLEM_AREAS.find((o) => o.label === problemArea)?.component}
</>
);
}
@@ -1,66 +0,0 @@
import { type ReactElement, useState } from "react";
import { AttachmentForm } from "../shared/SupportForm_AttachmentUploader";
import { DescriptionInput } from "../shared/SupportForm_DescriptionInput";
import { SupportForm_SelectInput } from "../shared/SupportForm_SelectInput";
type ProblemAreaItem = {
label: string;
component: ReactElement;
};
const OTHER_PROBLEM_AREAS: ProblemAreaItem[] = [
{
component: (
<>
<DescriptionInput />
<AttachmentForm />
</>
),
label: "General inquiry",
},
{
component: (
<>
<DescriptionInput />
<AttachmentForm />
</>
),
label: "Security",
},
{
component: (
<>
<DescriptionInput />
<AttachmentForm />
</>
),
label: "Feedback",
},
{
component: (
<>
<DescriptionInput />
<AttachmentForm />
</>
),
label: "Other",
},
];
export default function OtherSupportForm() {
const [problemArea, setProblemArea] = useState<string>("");
return (
<>
<SupportForm_SelectInput
formLabel="Problem area"
name="extraInfo_Problem_Area"
onValueChange={setProblemArea}
options={OTHER_PROBLEM_AREAS.map((o) => o.label)}
promptText="Select a problem area"
required={true}
value={problemArea}
/>
{OTHER_PROBLEM_AREAS.find((o) => o.label === problemArea)?.component}
</>
);
}
@@ -1,17 +0,0 @@
import { Label } from "@/components/ui/label";
export const AttachmentForm = () => {
return (
<div className="flex flex-col items-start gap-2">
<Label className="relative" htmlFor="files">
Attachments
</Label>
<input
accept="image/*,video/*,"
multiple
name="attachments"
type="file"
/>
</div>
);
};
@@ -1,58 +0,0 @@
import { useId } from "react";
import { Label } from "@/components/ui/label";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
type MinimalTeam = {
name: string;
id: string;
};
type Props = {
teams: MinimalTeam[];
selectedTeamId: string | undefined;
onChange: (teamId: string) => void;
};
export const SupportForm_TeamSelection = (props: Props) => {
const selectedTeamName = props.teams.find(
(t) => t.id === props.selectedTeamId,
)?.name;
const teamId = useId();
return (
<div className="flex flex-col items-start gap-2">
<Label className="relative" htmlFor={teamId}>
Select Team
<span className="-top-1.5 -right-2 absolute text-destructive">•</span>
</Label>
<Select
name="teamId"
onValueChange={(selectedId) => {
props.onChange(selectedId);
}}
value={props.selectedTeamId}
>
<SelectTrigger id={teamId}>
<SelectValue placeholder="Select a Team">
{selectedTeamName}
</SelectValue>
</SelectTrigger>
<SelectContent>
{props.teams.map((team) => (
<SelectItem key={team.id} value={team.id}>
{team.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
);
};
@@ -1,27 +0,0 @@
import { Label } from "@/components/ui/label";
type Props = {
placeholder?: string;
};
const defaultDescription = "@YourHandle";
export const SupportForm_TelegramInput = (props: Props) => {
return (
<div className="flex flex-col items-start gap-2 bg-[#0a0a0a]">
<Label className="relative" htmlFor="telegram">
Telegram
<span className="-top-1.5 -right-2 absolute text-destructive">•</span>
</Label>
<input
autoComplete="off"
className="flex h-10 w-full items-center justify-between gap-2 rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1"
maxLength={50}
name="telegram"
placeholder={props.placeholder ?? defaultDescription}
type="text"
/>
</div>
);
};
@@ -1,208 +0,0 @@
"use server";
import "server-only";
import { getTeamById } from "@/api/team";
import { getAuthTokenWalletAddress } from "../../../../../../@/api/auth-token";
import { getRawAccount } from "../../../../account/settings/getAccount";
import { loginRedirect } from "../../../../login/loginRedirect";
type State = {
success: boolean;
message: string;
};
const UNTHREAD_API_KEY = process.env.UNTHREAD_API_KEY || "";
const planToCustomerId = {
accelerate: process.env.UNTHREAD_ACCELERATE_TIER_ID as string,
free: process.env.UNTHREAD_FREE_TIER_ID as string,
growth: process.env.UNTHREAD_GROWTH_TIER_ID as string,
pro: process.env.UNTHREAD_PRO_TIER_ID as string,
scale: process.env.UNTHREAD_SCALE_TIER_ID as string,
// treat starter as free
starter: process.env.UNTHREAD_FREE_TIER_ID as string,
} as const;
function isValidPlan(plan: string): plan is keyof typeof planToCustomerId {
return plan in planToCustomerId;
}
function prepareEmailTitle(
product: string,
problemArea: string,
email: string,
name: string,
) {
const title =
product && problemArea
? `${product}: ${problemArea} (${email})`
: `New ticket from ${name} (${email})`;
return title;
}
function prepareEmailBody(props: {
product: string;
markdownInput: string;
email: string;
name: string;
extraInfoInput: Record<string, string>;
walletAddress: string;
telegramHandle: string;
}) {
const {
extraInfoInput,
email,
walletAddress,
product,
name,
markdownInput,
telegramHandle,
} = props;
// Update `markdown` to include the infos from the form
const extraInfo = Object.keys(extraInfoInput)
.filter((key) => key.startsWith("extraInfo_"))
.map((key) => {
const prettifiedKey = `# ${key
.replace("extraInfo_", "")
.replaceAll("_", " ")}`;
return `${prettifiedKey}: ${extraInfoInput[key] ?? "N/A"}\n`;
})
.join("");
const markdown = `# Email: ${email}
# Name: ${name}
# Telegram: ${telegramHandle}
# Wallet address: ${walletAddress}
# Product: ${product}
${extraInfo}
# Message:
${markdownInput}
`;
return markdown;
}
export async function createTicketAction(
_previousState: State,
formData: FormData,
) {
const teamId = formData.get("teamId")?.toString();
if (!teamId) {
return {
message: "teamId is required",
success: false,
};
}
const team = await getTeamById(teamId);
if (!team) {
return {
message: `Team with id "${teamId}" not found`,
success: false,
};
}
const [walletAddress, account] = await Promise.all([
getAuthTokenWalletAddress(),
getRawAccount(),
]);
if (!walletAddress || !account) {
loginRedirect("/support");
}
const customerId = isValidPlan(team.supportPlan)
? // fall back to "free" tier
planToCustomerId[team.supportPlan] || planToCustomerId.free
: // fallback to "free" tier
planToCustomerId.free;
const product = formData.get("product")?.toString() || "";
const problemArea = formData.get("extraInfo_Problem_Area")?.toString() || "";
const telegramHandle = formData.get("telegram")?.toString() || "";
const title = prepareEmailTitle(
product,
problemArea,
account.email || "",
account.name || "",
);
const keyVal: Record<string, string> = {};
for (const key of formData.keys()) {
keyVal[key] = formData.get(key)?.toString() || "";
}
const markdown = prepareEmailBody({
email: account.email || "",
extraInfoInput: keyVal,
markdownInput: keyVal.markdown || "",
name: account.name || "",
product,
telegramHandle: telegramHandle,
walletAddress: walletAddress,
});
const content = {
customerId,
emailInboxId: process.env.UNTHREAD_EMAIL_INBOX_ID,
markdown,
onBehalfOf: {
email: account.email,
id: account.id,
name: account.name,
},
status: "open",
title,
triageChannelId: process.env.UNTHREAD_TRIAGE_CHANNEL_ID,
type: "email",
};
// check files
const files = formData.getAll("files") as File[];
if (files.length > 10) {
return { message: "You can only attach 10 files at once.", success: false };
}
if (files.some((file) => file.size > 10 * 1024 * 1024)) {
return { message: "The max file size is 20MB.", success: false };
}
// add the content
formData.append("json", JSON.stringify(content));
const KEEP_FIELDS = ["attachments", "json"];
const keys = [...formData.keys()];
// delete everything except attachments off of the form data
for (const key of keys) {
if (!KEEP_FIELDS.includes(key)) {
formData.delete(key);
}
}
// actually create the ticket
const res = await fetch("https://api.unthread.io/api/conversations", {
body: formData,
headers: {
"X-Api-Key": UNTHREAD_API_KEY,
},
method: "POST",
});
if (!res.ok) {
console.error(
"Failed to create ticket",
res.status,
res.statusText,
await res.text(),
);
return {
message: "Failed to create ticket, please try again later.",
success: false,
};
}
return {
message: "Ticket created successfully",
success: true,
};
}
@@ -1,183 +0,0 @@
"use client";
import { SupportForm_SelectInput } from "@app/(dashboard)/support/create-ticket/components/contact-forms/shared/SupportForm_SelectInput";
import { SupportForm_TeamSelection } from "@app/(dashboard)/support/create-ticket/components/contact-forms/shared/SupportForm_TeamSelection";
import { SupportForm_TelegramInput } from "@app/(dashboard)/support/create-ticket/components/contact-forms/shared/SupportForm_TelegramInput";
import dynamic from "next/dynamic";
import {
type ReactElement,
useActionState,
useEffect,
useRef,
useState,
} from "react";
import { useFormStatus } from "react-dom";
import { toast } from "sonner";
import { Button } from "@/components/ui/button";
import { Spinner } from "@/components/ui/Spinner/Spinner";
import { Skeleton } from "@/components/ui/skeleton";
import { cn } from "@/lib/utils";
import { createTicketAction } from "./create-ticket.action";
const ConnectSupportForm = dynamic(() => import("./contact-forms/connect"), {
loading: () => <Skeleton className="h-12" />,
ssr: false,
});
const EngineSupportForm = dynamic(() => import("./contact-forms/engine"), {
loading: () => <Skeleton className="h-12" />,
ssr: false,
});
const ContractSupportForm = dynamic(() => import("./contact-forms/contracts"), {
loading: () => <Skeleton className="h-12" />,
ssr: false,
});
const AccountSupportForm = dynamic(() => import("./contact-forms/account"), {
loading: () => <Skeleton className="h-12" />,
ssr: false,
});
const OtherSupportForm = dynamic(() => import("./contact-forms/other"), {
loading: () => <Skeleton className="h-12" />,
ssr: false,
});
const productOptions: { label: string; component: ReactElement }[] = [
{
component: <ConnectSupportForm />,
label: "Connect",
},
{
component: <EngineSupportForm />,
label: "Engine",
},
{
component: <ContractSupportForm />,
label: "Contracts",
},
{
component: <AccountSupportForm />,
label: "Account",
},
{
component: <OtherSupportForm />,
label: "Other",
},
];
function ProductAreaSelection(props: {
productLabel: string;
setProductLabel: (val: string) => void;
}) {
const { productLabel, setProductLabel } = props;
return (
<div className="flex flex-col gap-6">
<SupportForm_SelectInput
formLabel="What do you need help with?"
name="product"
onValueChange={setProductLabel}
options={productOptions.map((o) => o.label)}
promptText="Select a product"
required={true}
value={productLabel}
/>
{productOptions.find((o) => o.label === productLabel)?.component}
</div>
);
}
export function CreateTicket(props: {
teams: {
name: string;
id: string;
}[];
}) {
const formRef = useRef<HTMLFormElement>(null);
const [selectedTeamId, setSelectedTeamId] = useState<string | undefined>(
props.teams[0]?.id,
);
const [productLabel, setProductLabel] = useState("");
const [state, formAction] = useActionState(createTicketAction, {
message: "",
success: false,
});
// needed here
// eslint-disable-next-line no-restricted-syntax
useEffect(() => {
if (!state.message) {
return;
}
if (state.success) {
toast.success(state.message);
} else {
toast.error(state.message);
}
}, [state.success, state.message]);
return (
<form
action={formAction}
className="rounded-lg border bg-card"
ref={formRef}
>
<div className="px-4 py-6 lg:px-6">
<h2 className="font-semibold text-2xl tracking-tight">Get Support</h2>
<p className="text-muted-foreground">
We are here to help. Ask product questions, report problems, or leave
feedback.
</p>
<div className="h-7" />
<div className="flex flex-col gap-6">
{/* Don't conditionally render this - it has be rendered to submit the input values */}
<div className={cn(props.teams.length === 1 && "hidden")}>
<SupportForm_TeamSelection
onChange={(teamId) => setSelectedTeamId(teamId)}
selectedTeamId={selectedTeamId}
teams={props.teams}
/>
</div>
<SupportForm_TelegramInput />
<ProductAreaSelection
productLabel={productLabel}
setProductLabel={setProductLabel}
/>
</div>
</div>
<div className="h-7" />
<div className="flex justify-end gap-3 border-t px-4 py-6 lg:px-6">
<Button
onClick={() => {
formRef.current?.reset();
setProductLabel("");
}}
variant="outline"
>
Reset
</Button>
<SubmitButton />
</div>
</form>
);
}
function SubmitButton() {
const { pending } = useFormStatus();
return (
<Button
className="flex min-w-24 flex-row gap-2"
disabled={pending}
type="submit"
>
{pending && <Spinner className="size-4" />}
{pending ? "Submitting" : "Submit"}
</Button>
);
}
@@ -1,48 +0,0 @@
import Link from "next/link";
import { getTeams } from "@/api/team";
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator,
} from "@/components/ui/breadcrumb";
import { loginRedirect } from "../../../login/loginRedirect";
import { CreateTicket } from "./components/create-ticket.client";
export default async function Page() {
const teams = await getTeams();
const pagePath = "/support/create-ticket";
if (!teams || teams.length === 0) {
loginRedirect(pagePath);
}
return (
<div>
<Breadcrumb className="border-border border-b px-6 py-4">
<BreadcrumbList>
<BreadcrumbItem>
<BreadcrumbLink asChild>
<Link href="/support">Support</Link>
</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbPage>Get Support</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
<div className="container max-w-[750px] py-10">
<CreateTicket
teams={teams.map((t) => ({
id: t.id,
name: t.name,
}))}
/>
</div>
</div>
);
}
@@ -1,12 +0,0 @@
import { TeamHeader } from "../../team/components/TeamHeader/team-header";
export default function Layout({ children }: { children: React.ReactNode }) {
return (
<div className="flex grow flex-col">
<div className="border-border border-b bg-card">
<TeamHeader />
</div>
{children}
</div>
);
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 849 KiB

@@ -1,155 +0,0 @@
import {
BookOpenIcon,
ChevronRightIcon,
HomeIcon,
WalletIcon,
} from "lucide-react";
import type { Metadata } from "next";
import Link from "next/link";
import { getAuthToken, getAuthTokenWalletAddress } from "@/api/auth-token";
import { getTeams } from "@/api/team";
import { CustomChatButton } from "@/components/chat/CustomChatButton";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { EngineIcon } from "@/icons/EngineIcon";
import { InsightIcon } from "@/icons/InsightIcon";
import { NebulaIcon } from "@/icons/NebulaIcon";
import { PayIcon } from "@/icons/PayIcon";
import { siwaExamplePrompts } from "./definitions";
export const metadata: Metadata = {
openGraph: {
title: "thirdweb Support",
},
title: "thirdweb Support",
};
const HELP_PRODUCTS = [
{
description: "Try out our interactive playground to get started",
icon: HomeIcon,
title: "Playground",
viewAllUrl:
"https://playground.thirdweb.com/connect/sign-in/button?tab=code",
},
{
description: "Create and manage crypto wallets",
icon: WalletIcon,
title: "Wallets",
viewAllUrl: "https://portal.thirdweb.com/connect",
},
{
description: "Enable payments on any tokens on any chain",
icon: PayIcon,
title: "Payments",
viewAllUrl: "https://portal.thirdweb.com/pay/troubleshoot",
},
{
description: "Perform read and write transactions onchain",
icon: EngineIcon,
title: "Transactions",
viewAllUrl: "https://portal.thirdweb.com/engine/v3/troubleshoot",
},
{
description: "Query and analyze blockchain data",
icon: InsightIcon,
title: "Insight",
viewAllUrl: "https://portal.thirdweb.com/insight",
},
{
description: "API interface for LLMs",
icon: NebulaIcon,
title: "Nebula",
viewAllUrl: "https://portal.thirdweb.com/nebula",
},
] as const;
export default async function SupportPage() {
const [authToken, accountAddress] = await Promise.all([
getAuthToken(),
getAuthTokenWalletAddress(),
]);
const teams = await getTeams();
const teamId = teams?.[0]?.id ?? undefined;
return (
<main className="flex flex-col gap-12 pb-12">
<div className="bg-gradient-to-b from-card/0 to-card py-20">
<header className="container flex flex-col items-center gap-8">
<div className="rounded-full bg-gradient-to-r from-[#F213A4] to-[#5204BF] p-2">
<div className="rounded-full bg-background p-6 shadow-md">
<BookOpenIcon className="size-8" />
</div>
</div>
<div className="flex max-w-2xl flex-col items-center gap-2">
<h1 className="text-center font-bold text-4xl md:text-balance md:text-5xl">
How can we help?
</h1>
<p className="text-center text-lg text-muted-foreground">
Get instant answers with Nebula AI, our onchain support assistant.
Still need help? You can also create a support case to reach our
team.
</p>
<div className="mt-6 flex w-full flex-col items-center gap-3">
<CustomChatButton
authToken={authToken || undefined}
clientId={undefined}
examplePrompts={siwaExamplePrompts}
isFloating={false}
isLoggedIn={!!accountAddress}
label="Ask AI for support"
networks="all"
pageType="support"
teamId={teamId}
/>
<Link
className="text-muted-foreground text-sm hover:underline"
href="/support/create-ticket"
rel="noopener noreferrer"
target="_blank"
>
Open a support case
</Link>
</div>
</div>
</header>
</div>
<section className="container flex flex-col gap-6">
<h2 className="font-bold text-3xl">Support Articles</h2>
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 md:gap-6 lg:grid-cols-3">
{HELP_PRODUCTS.map((product) => (
<Card className="relative col-span-1 bg-card" key={product.title}>
<CardHeader className="flex flex-row items-center justify-between gap-2">
<div className="flex flex-row items-center gap-2">
{product.icon && <product.icon className="size-5" />}
<CardTitle className="text-xl">{product.title}</CardTitle>
</div>
<Button
asChild
className="absolute top-4 right-2 flex flex-row gap-1 px-2 text-muted-foreground text-sm hover:text-foreground"
size="sm"
variant="ghost"
>
<Link href={product.viewAllUrl} target="_blank">
<span>View</span>
<ChevronRightIcon className="size-4" />
</Link>
</Button>
</CardHeader>
<CardContent key={product.description}>
{product.description && (
<p className="mb-2 text-muted-foreground text-sm">
{product.description}
</p>
)}
</CardContent>
</Card>
))}
</div>
</section>
</main>
);
}
@@ -63,14 +63,14 @@ export const HexConverter = () => {
<Card className="flex flex-col gap-2 p-4">
<p>
Or use the
Convert values using the{" "}
<a
className="underline"
href="https://portal.thirdweb.com/typescript/v5"
rel="noopener noreferrer"
target="_blank"
>
Connect SDK
thirdweb SDK
</a>
:
</p>
@@ -49,14 +49,14 @@ export const Keccak256Converter = () => {
<Card className="flex flex-col gap-2 p-4">
<p>
Or use the{" "}
Convert values using the{" "}
<a
className="underline"
href="https://portal.thirdweb.com/typescript/v5"
rel="noopener noreferrer"
target="_blank"
>
Connect SDK
thirdweb SDK
</a>
:
</p>
@@ -79,14 +79,14 @@ export const WeiConverter = () => {
<Card className="flex flex-col gap-2 p-4">
<p>
Or use the{" "}
Convert values using the{" "}
<a
className="underline"
href="https://portal.thirdweb.com/typescript/v5"
rel="noopener noreferrer"
target="_blank"
>
Connect SDK
thirdweb SDK
</a>
:
</p>
@@ -4,6 +4,7 @@ import { useCallback, useState } from "react";
import { toast } from "sonner";
import type { ThirdwebClient } from "thirdweb";
import { useActiveWallet, useDisconnect } from "thirdweb/react";
import { doLogout } from "@/actions/auth-actions";
import { createTeam } from "@/actions/createTeam";
import { resetAnalytics } from "@/analytics/reset";
import type { Project } from "@/api/projects";
@@ -12,7 +13,6 @@ import { CustomConnectWallet } from "@/components/connect-wallet";
import { LazyCreateProjectDialog } from "@/components/project/create-project-modal/LazyCreateAPIKeyDialog";
import type { Account } from "@/hooks/useApi";
import { useDashboardRouter } from "@/lib/DashboardRouter";
import { doLogout } from "../../login/auth-actions";
import {
type AccountHeaderCompProps,
AccountHeaderDesktopUI,
@@ -1,8 +1,8 @@
import {
getProjectContracts,
type ProjectContract,
} from "../../../../../@/api/getProjectContracts";
import { fetchChainWithLocalOverrides } from "../../../../../@/utils/fetchChainWithLocalOverrides";
} from "@/api/getProjectContracts";
import { fetchChainWithLocalOverrides } from "@/utils/fetchChainWithLocalOverrides";
export async function getSortedDeployedContracts(params: {
onlyMainnet?: boolean;
@@ -1,5 +1,6 @@
import type React from "react";
import type { ThirdwebClient } from "thirdweb";
import { getAuthToken, getAuthTokenWalletAddress } from "@/api/auth-token";
import { getProjects } from "@/api/projects";
import { getTeams, type Team } from "@/api/team";
import { AppFooter } from "@/components/footers/app-footer";
@@ -7,12 +8,8 @@ import { AnnouncementBanner } from "@/components/misc/AnnouncementBanner";
import { TabPathLinks } from "@/components/ui/tabs";
import { getClientThirdwebClient } from "@/constants/thirdweb-client.client";
import type { Account } from "@/hooks/useApi";
import {
getAuthToken,
getAuthTokenWalletAddress,
} from "../../../@/api/auth-token";
import { loginRedirect } from "@/utils/redirects";
import { TWAutoConnect } from "../components/autoconnect";
import { loginRedirect } from "../login/loginRedirect";
import { AccountHeader } from "./components/AccountHeader";
import { getValidAccount } from "./settings/getAccount";
@@ -1,9 +1,9 @@
import { notFound } from "next/navigation";
import { getAuthToken } from "@/api/auth-token";
import { getTeams } from "@/api/team";
import { getMemberByAccountId } from "@/api/team-members";
import { getClientThirdwebClient } from "@/constants/thirdweb-client.client";
import { getAuthToken } from "../../../@/api/auth-token";
import { loginRedirect } from "../login/loginRedirect";
import { loginRedirect } from "@/utils/redirects";
import { AccountTeamsUI } from "./overview/AccountTeamsUI";
import { getValidAccount } from "./settings/getAccount";
@@ -2,13 +2,13 @@
import type { ThirdwebClient } from "thirdweb";
import { useActiveWallet, useDisconnect } from "thirdweb/react";
import { upload } from "thirdweb/storage";
import { doLogout } from "@/actions/auth-actions";
import { confirmEmailWithOTP } from "@/actions/confirmEmail";
import { apiServerProxy } from "@/actions/proxies";
import { updateAccount } from "@/actions/updateAccount";
import { resetAnalytics } from "@/analytics/reset";
import type { Account } from "@/hooks/useApi";
import { useDashboardRouter } from "@/lib/DashboardRouter";
import { doLogout } from "../../login/auth-actions";
import { AccountSettingsPageUI } from "./AccountSettingsPageUI";
export function AccountSettingsPage(props: {
@@ -285,7 +285,7 @@ function DeleteAccountCard(props: {
variant="outline"
>
<Link
href="/support/create-ticket"
href="/team/~/~/support"
rel="noopener noreferrer"
target="_blank"
>
@@ -1,8 +1,8 @@
import { getAuthToken } from "@/api/auth-token";
import { NEXT_PUBLIC_THIRDWEB_API_HOST } from "@/constants/public-envs";
import type { Account } from "@/hooks/useApi";
import { getAuthToken } from "../../../../@/api/auth-token";
import { loginRedirect } from "../../login/loginRedirect";
import { isAccountOnboardingComplete } from "../../login/onboarding/isOnboardingRequired";
import { loginRedirect } from "@/utils/redirects";
import { isAccountOnboardingComplete } from "../../../login/onboarding/isOnboardingRequired";
/**
* Just get the account object without enforcing onboarding.
@@ -1,7 +1,7 @@
import { getAuthToken } from "@/api/auth-token";
import { getDefaultTeam } from "@/api/team";
import { getClientThirdwebClient } from "@/constants/thirdweb-client.client";
import { getAuthToken } from "../../../../@/api/auth-token";
import { loginRedirect } from "../../login/loginRedirect";
import { loginRedirect } from "@/utils/redirects";
import { AccountSettingsPage } from "./AccountSettingsPage";
import { getValidAccount } from "./getAccount";
@@ -1,7 +1,7 @@
import { getAuthToken } from "@/api/auth-token";
import { getLinkedWallets } from "@/api/linked-wallets";
import { getClientThirdwebClient } from "@/constants/thirdweb-client.client";
import { getAuthToken } from "../../../../@/api/auth-token";
import { loginRedirect } from "../../login/loginRedirect";
import { loginRedirect } from "@/utils/redirects";
import { getValidAccount } from "../settings/getAccount";
import { LinkWallet } from "./LinkWalletUI";
@@ -44,15 +44,6 @@ export function SecondaryNavLinks() {
Docs
</Link>
<Link
className="text-muted-foreground text-sm hover:text-foreground"
href="/support"
rel="noopener noreferrer"
target="_blank"
>
Support
</Link>
<Link
className="text-muted-foreground text-sm hover:text-foreground"
href="https://feedback.thirdweb.com"
@@ -191,15 +191,6 @@ export function MobileBurgerMenuButton(
Docs
</Link>
<Link
className="text-muted-foreground hover:text-foreground "
href="/support"
rel="noopener noreferrer"
target="_blank"
>
Support
</Link>
<Link
className="text-muted-foreground hover:text-foreground"
href="https://feedback.thirdweb.com"
@@ -4,7 +4,10 @@ function prefixId(id: string, prefix?: string) {
return prefix ? `${prefix}_${id}` : id;
}
export function ThirdwebMiniLogo(props: { className?: string }) {
export function ThirdwebMiniLogo(props: {
className?: string;
isMonoChrome?: boolean;
}) {
const id = useId();
return (
<svg className={props.className} fill="none" viewBox="0 0 516 321">
@@ -12,15 +15,27 @@ export function ThirdwebMiniLogo(props: { className?: string }) {
<g clipPath={`url(#${prefixId("clip0_3:35)", id)}`}>
<path
d="M1.40497 27.0011C-3.73633 14.022 5.84519 0 19.8669 0H106.919C115.098 0 122.342 4.86715 125.381 12.3996L194.671 185.299C196.541 189.935 196.541 195.149 194.671 199.901L151.087 308.484C144.427 325.056 120.823 325.056 114.163 308.484L1.40497 27.0011Z"
fill={`url(#${prefixId("paint0_linear_3:35)", id)}`}
fill={
props.isMonoChrome
? "currentColor"
: `url(#${prefixId("paint0_linear_3:35)", id)}`
}
/>
<path
d="M169.547 26.4217C164.873 13.5585 174.454 0 188.242 0H264.077C272.49 0 279.968 5.2148 282.772 12.9791L345.753 185.879C347.272 190.166 347.272 194.918 345.753 199.321L307.894 303.27C301.585 320.652 276.813 320.652 270.503 303.27L169.547 26.4217Z"
fill={`url(#${prefixId("paint1_linear_3:35)", id)}`}
fill={
props.isMonoChrome
? "currentColor"
: `url(#${prefixId("paint1_linear_3:35)", id)}`
}
/>
<path
d="M321.331 27.0011C316.19 14.022 325.771 0 339.793 0H426.845C435.024 0 442.269 4.86715 445.307 12.3996L514.597 185.299C516.467 189.935 516.467 195.149 514.597 199.901L471.013 308.484C464.353 325.056 440.75 325.056 434.089 308.484L321.331 27.0011Z"
fill={`url(#${prefixId("paint2_linear_3:35)", id)}`}
fill={
props.isMonoChrome
? "currentColor"
: `url(#${prefixId("paint2_linear_3:35)", id)}`
}
/>
</g>
<defs>
@@ -1,9 +1,9 @@
import { notFound } from "next/navigation";
import { getAuthToken } from "@/api/auth-token";
import { getTeamBySlug } from "@/api/team";
import { getClientThirdwebClient } from "@/constants/thirdweb-client.client";
import { getAuthToken } from "../../../../../../@/api/auth-token";
import { TeamOnboardingLayout } from "../../../../login/onboarding/onboarding-layout";
import { InviteTeamMembers } from "../../../../login/onboarding/team-onboarding/team-onboarding";
import { TeamOnboardingLayout } from "../../../../../login/onboarding/onboarding-layout";
import { InviteTeamMembers } from "../../../../../login/onboarding/team-onboarding/team-onboarding";
export default async function Page(props: {
params: Promise<{ team_slug: string }>;
@@ -1,17 +1,14 @@
import { differenceInDays } from "date-fns";
import { InfoIcon } from "lucide-react";
import { notFound } from "next/navigation";
import { getAuthToken, getAuthTokenWalletAddress } from "@/api/auth-token";
import { getProjects } from "@/api/projects";
import { getTeamBySlug, getTeams } from "@/api/team";
import { AppFooter } from "@/components/footers/app-footer";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { getClientThirdwebClient } from "@/constants/thirdweb-client.client";
import {
getAuthToken,
getAuthTokenWalletAddress,
} from "../../../../../@/api/auth-token";
import { loginRedirect } from "@/utils/redirects";
import { getValidAccount } from "../../../account/settings/getAccount";
import { loginRedirect } from "../../../login/loginRedirect";
import { TeamHeaderLoggedIn } from "../../../team/components/TeamHeader/team-header-logged-in.client";
export default async function Layout(props: {
@@ -1,10 +1,10 @@
import { notFound } from "next/navigation";
import { getAuthToken } from "@/api/auth-token";
import { getTeamBySlug } from "@/api/team";
import { getClientThirdwebClient } from "@/constants/thirdweb-client.client";
import { getAuthToken } from "../../../../../@/api/auth-token";
import { loginRedirect } from "../../../login/loginRedirect";
import { TeamOnboardingLayout } from "../../../login/onboarding/onboarding-layout";
import { TeamInfoForm } from "../../../login/onboarding/team-onboarding/team-onboarding";
import { loginRedirect } from "@/utils/redirects";
import { TeamOnboardingLayout } from "../../../../login/onboarding/onboarding-layout";
import { TeamInfoForm } from "../../../../login/onboarding/team-onboarding/team-onboarding";
export default async function Page(props: {
params: Promise<{ team_slug: string }>;
@@ -1,7 +1,7 @@
import { notFound } from "next/navigation";
import { getAuthToken } from "@/api/auth-token";
import { getTeamBySlug, type Team } from "@/api/team";
import { getAuthToken } from "../../../../../../@/api/auth-token";
import { TeamOnboardingLayout } from "../../../../login/onboarding/onboarding-layout";
import { TeamOnboardingLayout } from "../../../../../login/onboarding/onboarding-layout";
import { PlanSelector } from "./_components/plan-selector";
export default async function Page(props: {
@@ -2,7 +2,6 @@
import { useMutation } from "@tanstack/react-query";
import { CheckIcon, UsersIcon } from "lucide-react";
import Link from "next/link";
import { toast } from "sonner";
import { acceptInvite } from "@/actions/acceptInvite";
import type { Team } from "@/api/team";
@@ -65,19 +64,7 @@ function Header() {
</span>
</div>
<div className="flex items-center gap-4">
<div className="flex items-center gap-2">
<Link
className="px-2 text-muted-foreground text-sm hover:text-foreground"
href="/support"
rel="noopener noreferrer"
target="_blank"
>
Support
</Link>
</div>
<ToggleThemeButton />
</div>
<ToggleThemeButton />
</header>
</div>
);
+1 -1
View File
@@ -54,7 +54,7 @@ export default function RootLayout({
<body
className={cn("bg-background font-sans antialiased", fontSans.variable)}
>
<AppRouterProviders>{children}</AppRouterProviders>
<AppRouterProviders autoConnect={true}>{children}</AppRouterProviders>
<DashboardRouterTopProgressBar />
<NextTopLoader
color="hsl(var(--foreground))"
+8 -10
View File
@@ -1,16 +1,16 @@
"use client";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { ThemeProvider, useTheme } from "next-themes";
import { ThemeProvider } from "next-themes";
import { NuqsAdapter } from "nuqs/adapters/next/app";
import { useEffect, useMemo } from "react";
import { Toaster } from "sonner";
import {
ThirdwebProvider,
useActiveAccount,
useConnectionManager,
} from "thirdweb/react";
import { CustomConnectWallet } from "@/components/connect-wallet";
import { Toaster } from "@/components/ui/sonner";
import { isSanctionedAddress } from "@/constants/eth-sanctioned-addresses";
import { getClientThirdwebClient } from "@/constants/thirdweb-client.client";
import { useAllChainsData } from "@/hooks/chains/allChains";
@@ -20,21 +20,24 @@ import { TWAutoConnect } from "./components/autoconnect";
const queryClient = new QueryClient();
const thirdwebClient = getClientThirdwebClient();
export function AppRouterProviders(props: { children: React.ReactNode }) {
export function AppRouterProviders(props: {
children: React.ReactNode;
autoConnect: boolean;
}) {
return (
<NuqsAdapter>
<QueryClientProvider client={queryClient}>
<SyncChainStores />
<ThirdwebProvider>
<SyncChainDefinitionsToConnectionManager />
<TWAutoConnect client={thirdwebClient} />
{props.autoConnect && <TWAutoConnect client={thirdwebClient} />}
<ThemeProvider
attribute="class"
defaultTheme="dark"
disableTransitionOnChange
enableSystem={false}
>
<ToasterSetup />
<Toaster richColors />
<SanctionedAddressesChecker>
{props.children}
</SanctionedAddressesChecker>
@@ -45,11 +48,6 @@ export function AppRouterProviders(props: { children: React.ReactNode }) {
);
}
function ToasterSetup() {
const { theme } = useTheme();
return <Toaster richColors theme={theme === "light" ? "light" : "dark"} />;
}
function SyncChainDefinitionsToConnectionManager() {
const { allChainsV5 } = useAllChainsData();
const connectionManager = useConnectionManager();
@@ -6,6 +6,7 @@ import {
DatabaseIcon,
DollarSignIcon,
FileTextIcon,
HelpCircleIcon,
HomeIcon,
SettingsIcon,
WalletCardsIcon,
@@ -89,6 +90,11 @@ export function TeamSidebarLayout(props: {
: []),
]}
footerSidebarLinks={[
{
href: `${layoutPath}/~/support`,
icon: HelpCircleIcon,
label: "Support",
},
{
href: `${layoutPath}/~/billing`,
icon: DollarSignIcon,
@@ -7,9 +7,9 @@ import { getChainSubscriptions } from "@/api/team-subscription";
import { CustomChatButton } from "@/components/chat/CustomChatButton";
import { AnnouncementBanner } from "@/components/misc/AnnouncementBanner";
import { SidebarProvider } from "@/components/ui/sidebar";
import { siwaExamplePrompts } from "@/constants/siwa-example-prompts";
import { getClientThirdwebClient } from "@/constants/thirdweb-client.client";
import { getChain } from "../../../(dashboard)/(chain)/utils";
import { siwaExamplePrompts } from "../../../(dashboard)/support/definitions";
import { getValidAccount } from "../../../account/settings/getAccount";
import { TeamHeaderLoggedIn } from "../../components/TeamHeader/team-header-logged-in.client";
import { StaffModeNotice } from "./_components/StaffModeNotice";
@@ -103,17 +103,13 @@ export default async function TeamLayout(props: {
>
{props.children}
</TeamSidebarLayout>
<div className="fixed right-6 bottom-6 z-50">
<div className="fixed right-4 bottom-4 z-50">
<CustomChatButton
authToken={authToken}
clientId={undefined}
examplePrompts={siwaExamplePrompts}
isFloating={true}
isLoggedIn={true}
label="Ask AI Assistant"
networks="all"
pageType="support"
teamId={team.id}
label="Get Help"
team={team}
/>
</div>
</div>
@@ -1,12 +1,12 @@
import { subDays } from "date-fns";
import { redirect } from "next/navigation";
import { getWalletConnections } from "@/api/analytics";
import { getAuthToken } from "@/api/auth-token";
import { getProjects, type Project } from "@/api/projects";
import { getTeamBySlug } from "@/api/team";
import { DismissibleAlert } from "@/components/blocks/dismissible-alert";
import { getClientThirdwebClient } from "@/constants/thirdweb-client.client";
import { getAuthToken } from "../../../../../@/api/auth-token";
import { loginRedirect } from "../../../login/loginRedirect";
import { loginRedirect } from "@/utils/redirects";
import { Changelog } from "./_components/Changelog";
import { FreePlanUpsellBannerUI } from "./_components/FreePlanUpsellBannerUI";
import { InviteTeamMembersButton } from "./_components/invite-team-members-button";
@@ -2,7 +2,7 @@ import { redirect } from "next/navigation";
import { getAuthToken } from "@/api/auth-token";
import { getTeamBySlug } from "@/api/team";
import { TabPathLinks } from "@/components/ui/tabs";
import { loginRedirect } from "../../../../../login/loginRedirect";
import { loginRedirect } from "@/utils/redirects";
export default async function Layout(props: {
params: Promise<{
@@ -3,7 +3,7 @@ import { getAuthToken } from "@/api/auth-token";
import { fetchEcosystem } from "@/api/ecosystems";
import { getTeamBySlug } from "@/api/team";
import { getClientThirdwebClient } from "@/constants/thirdweb-client.client";
import { loginRedirect } from "../../../../../../../../../login/loginRedirect";
import { loginRedirect } from "@/utils/redirects";
import { AddPartnerForm } from "../components/client/add-partner-form.client";
export default async function AddPartnerPage({
@@ -3,7 +3,7 @@ import { getAuthToken } from "@/api/auth-token";
import { fetchEcosystem } from "@/api/ecosystems";
import { getTeamBySlug } from "@/api/team";
import { getClientThirdwebClient } from "@/constants/thirdweb-client.client";
import { loginRedirect } from "../../../../../../../../../../../login/loginRedirect";
import { loginRedirect } from "@/utils/redirects";
import { UpdatePartnerForm } from "../../../components/client/update-partner-form.client";
import { fetchPartnerDetails } from "../../../hooks/fetchPartnerDetails";
@@ -1,7 +1,7 @@
import { notFound } from "next/navigation";
import { getAuthToken } from "@/api/auth-token";
import { getTeamBySlug } from "@/api/team";
import { getAuthToken } from "../../../../../../../../../@/api/auth-token";
import { loginRedirect } from "../../../../../../../login/loginRedirect";
import { loginRedirect } from "@/utils/redirects";
import { EcosystemPermissionsPage } from "./configuration/components/client/EcosystemPermissionsPage";
export default async function Page(props: {

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