Compare commits

...
Author SHA1 Message Date
Joaquim VergesGitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
c7cb6ae7a4 Version Packages (#6728)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-04-17 18:01:02 -07:00
Joaquim VergesandGitHub 7ecfcb95af [SDK] Add thirdweb branding to PayEmbed (#6760) 2025-04-17 17:55:11 -07:00
Joaquim VergesGitHubCopilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
4cf15a2475 [SDK] Use insight for 1155 getNFTs, getOwnedNFTs and getNFT (#6753)
Signed-off-by: Joaquim Verges <[email protected]>
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
2025-04-17 17:01:20 -07:00
Joaquim VergesandGitHub 62ce05ec35 [SDK] Improve error message for getUser (#6758) 2025-04-17 16:12:42 -07:00
MananTank 563c4c7df1 Dashboard: disable prefetch for footer links (#6756)
<!--

## 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 refactors the `AppFooter` component in `app-footer.tsx` by introducing a new `footerLinks` array to manage footer link data and replaces individual `Link` components with a new `FooterLink` function to enhance maintainability and readability.

### Detailed summary
- Added `footerLinks` array containing link objects with `href` and `label`.
- Replaced individual `Link` components with a mapped `FooterLink` component.
- Introduced `FooterLink` function to streamline link rendering and improve code clarity.

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

<!-- end pr-codex -->
2025-04-17 19:41:27 +00:00
Joe-ThirdwebandGitHub 32c91abc02 [Dashboard] Adding telegram field to support ticket creation (#6754) 2025-04-17 21:41:09 +02:00
Manan TankandGitHub 7a2dc25129 [NEB-167] Add Nebula floating chat button in chain and contract pages (#6750) 2025-04-17 18:00:20 +02:00
jnsdls 32568fd421 Add "Invite Team Members" button to team overview page (#6755)
# Add "Invite Team Members" Button to Team Overview Page

This PR adds a new "Invite Team Members" button to the Team Overview page, replacing the previous implementation. The changes include:

- Created a new reusable `InviteTeamMembersButton` component
- Added the button to the Team Overview page header
- Removed the old "Share Button" component from the TeamProjectsPage
- Improved the layout of the Team Overview page header to accommodate the new button
- Maintained analytics tracking for the invite action

The button links to the team members settings page and includes a user plus icon for better visual indication of its purpose.
2025-04-17 08:06:51 +00:00
GiselleNessi 9091e813ab added invite team button in dash homepage (#6751)
<!--

## 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 `ShareButton` component to the `TeamProjectsPage`, allowing users to invite team members. It also tracks the button click event for analytics and includes a link to the team members settings.

### Detailed summary
- Added `ShareButton` component for inviting team members.
- Integrated `useTrack` hook to track button click events.
- Added a link to the team members settings using `Link`.
- Imported `UserPlus` icon from `lucide-react`.

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

<!-- end pr-codex -->
2025-04-17 05:21:12 +00:00
Joaquim VergesandGitHub 23d375778b [SDK] Simplify RPC request handling (#6752) 2025-04-16 18:07:46 -07:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
e7b0110803 chore(deps-dev): bump msw from 2.7.3 to 2.7.4 (#6723)
Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-16 16:49:50 -07:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
1825ac857e chore(deps-dev): bump vite from 6.2.5 to 6.2.6 in the npm_and_yarn group (#6711)
Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-16 16:09:15 -07:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
3ce2d0602e chore(deps-dev): bump @types/react-dom from 19.1.1 to 19.1.2 (#6721)
Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-16 16:08:43 -07:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
4bde2eb157 chore(deps): bump @abstract-foundation/agw-react from 1.5.4 to 1.6.4 (#6722)
Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-16 16:06:23 -07:00
Joaquim VergesandGitHub 8d4d991e57 [SDK] Use insight for erc721/getNFTs and erc721/getOwnedNFTs (#6741) 2025-04-16 15:53:14 -07:00
MananTank 22090cc400 [NEB-170] Nebula: Various UI changes (#6747)
<!-- start pr-codex -->

## PR-Codex overview
This PR focuses on enhancing the user interface and experience of the `Nebula` app by updating text content, adjusting styles, and introducing a new `StyledMarkdownRenderer` component for better message rendering.

### Detailed summary
- Updated text in `EmptyStateChatPageContent.tsx` and `ContextFilters.tsx` to reflect new terminology.
- Changed section headings from `Context` to `Settings`.
- Improved styling for various components, including `ChatSidebarLink` and buttons.
- Introduced `StyledMarkdownRenderer` for consistent message formatting.
- Adjusted scrollbar styles in `global.css`.
- Modified button states and classes for better responsiveness and appearance.

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

<!-- end pr-codex -->
2025-04-16 21:16:02 +00:00
MananTank 95a6c38703 [NEB-173]: Hide testnets in nebula chain selector (#6746)
<!--

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

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

## Notes for the reviewer

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

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR introduces the ability to hide test networks in the `MultiNetworkSelector` component and updates its props accordingly.

### Detailed summary
- Added `hideTestnets` prop to `MultiNetworkSelector` in `NetworkSelectors.tsx`.
- Implemented filtering of `sortedChains` to exclude test networks if `hideTestnets` is true.
- Updated dependency array in `useEffect` to include `props.hideTestnets`.

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

<!-- end pr-codex -->
2025-04-16 21:16:01 +00:00
MananTank b1060de7da [NEB-172]: Remove Nebula waitlist (#6745)
<!--

## 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 removing components and related functionality for managing the Nebula waitlist in the application. It simplifies the codebase by eliminating unused imports and references to the waitlist, streamlining the `ProjectSidebarLayout`, and adjusting the layout logic.

### Detailed summary
- Deleted files related to the Nebula waitlist functionality.
- Removed `getTeamNebulaWaitList` function and its associated type.
- Updated `ProjectSidebarLayout` to eliminate the `showNebula` prop.
- Simplified `ProjectLayout` by removing waitlist checks and related logic.
- Adjusted `Page` component to remove references to `NebulaWaitListPage`.
- Cleaned up the `Layout` component by removing waitlist handling and related imports.

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

<!-- end pr-codex -->
2025-04-16 21:16:01 +00:00
jnsdls 0aca56317a Fix RPC usage page to handle missing peak date (#6748)
This PR adds a null check for the peak date in the RPC Usage page to handle cases where there are no requests in the last 24 hours. It displays "No Requests in last 24 hours" instead of attempting to format a non-existent date. Additionally, it destructures the `averageRate` directly from the API data to improve code readability when passing it to the RateGraph and CountGraph components.
2025-04-16 16:42:07 +00:00
MananTank 5463baa7da Nebula: Fix chat session randomly showing empty state after first prompt (#6744)
<!--

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

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

## Notes for the reviewer

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

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR addresses an issue with updating the page URL in the `ChatPageContent.tsx` file due to changes in Next.js behavior. The existing method for updating the URL is commented out, and a temporary solution is proposed.

### Detailed summary
- Commented out the line: `window.history.replaceState({}, "", `/chat/${sessionId}`);`
- Added comments explaining that the URL update does not work anymore due to Next.js changes.
- Stated intention to not update the URL until a better solution is found.

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

<!-- end pr-codex -->
2025-04-16 11:34:58 +00:00
MananTank 64ffe199af Dashboard: Remove reo.dev from CSP script-src in next.config (#6740)
<!--

## 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 Content Security Policy (CSP) in the `next.config.ts` file by modifying the `script-src` directive, removing `static.reo.dev` from the list of allowed sources.

### Detailed summary
- Removed `static.reo.dev` from the `script-src` directive in the CSP.
- No other changes to the CSP directives were made.

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

<!-- end pr-codex -->
2025-04-16 10:53:29 +00:00
MananTank 76b522e7bc Portal: Use next-themes for theme management instead of custom code (#6739)
<!--

## 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 theme management in the application by integrating `next-themes` for better theme switching functionality and adding a new `Skeleton` component for loading states.

### Detailed summary
- Removed `theme.tsx` and its usage.
- Added `next-themes` dependency in `package.json`.
- Introduced `Skeleton` component in `skeleton.tsx`.
- Updated `ThemeSwitcher` to use `next-themes` for theme management.
- Implemented `useIsClientMounted` for client-side rendering checks.
- Wrapped the application in `ThemeProvider` in `layout.tsx`.
- Modified CSS for dark and light themes.

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

<!-- end pr-codex -->
2025-04-16 10:43:16 +00:00
Joe-Thirdweb 49786404b3 [Dashboard] Feature : Adding Next Steps guide into chain pages (#6743)
Add actionable next step guide for users who visit a chain page.

Steps to Test:
1. Visit thirdweb.com/{chainID}
2. Scroll to the bottom and you should see
![image](https://github.com/user-attachments/assets/df68ad4b-a0b7-4384-9e2d-1bb207400087)
3. Click on the article
4. Get redirected to a blog post.

<!-- start pr-codex -->

---

## PR-Codex overview
This PR introduces a new component called `NextSteps` to the `page.tsx` file in the dashboard application. This component provides users with actionable links and information related to integrating in-app wallets, enhancing user experience and adoption.

### Detailed summary
- Added `NextSteps` component to `page.tsx`.
- `NextSteps` displays a title and a link for creating a login for the specified `chain`.
- Integrated tracking for link clicks using `useTrack` to monitor user engagement.
- Included a brief description under the link in `NextSteps`.

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

<!-- end pr-codex -->
2025-04-16 05:24:27 +00:00
MananTank 64bd603301 Nebula: Do not override chains set from search params (#6738)
<!--

## 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 refines the logic for updating `updatedContextFilters.chainIds` by ensuring that it only attempts to retrieve last used chains from storage when `chainIds` is empty. It also maintains error handling for local storage access.

### Detailed summary
- Moved the check for `updatedContextFilters.chainIds` length to before retrieving last used chains.
- Retained the logic to fetch last used chain IDs from storage if `chainIds` is empty.
- Preserved error handling for local storage access.

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

<!-- end pr-codex -->
2025-04-15 22:16:26 +00:00
MananTank 60a5b7c3e7 [NEB-123] Nebula: Allow setting context with search params (#6736)
<!--

## 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 chat functionality by introducing `prefillMessage` support across various components and updating the way parameters are handled in the chat interface.

### Detailed summary
- Replaced `initialPrompt` with `initialParams` in chat components.
- Added `prefillMessage` prop to `ChatBar`, `EmptyStateChatPageContent`, and related story files.
- Updated `NebulaLogin` to handle search parameters.
- Modified `ChatPageContent` to utilize `initialParams` for message handling.
- Introduced a new function `getChainIds` for fetching chain IDs based on user input.

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

<!-- end pr-codex -->
2025-04-15 21:44:25 +00:00
jnsdls baf95b9077 Fix RPC usage graphs and improve rate limit visualization (#6737)
# Improve RPC Usage Graphs

This PR enhances the RPC usage graphs in the dashboard:

- Only show rate limited requests in the legend when there are actually rate limited requests
- Fix data processing by removing the `.slice(1, -1)` which was incorrectly trimming data points
- Ensure numeric values by explicitly converting to Number
- Move data slicing to the parent component for better control
- Add a type assertion comment to handle the TypeScript warning
- Add a check to conditionally display relevant chart configuration

These changes improve the accuracy and presentation of the RPC usage data visualization.

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on enhancing the `CountGraph` component by improving the handling of rate-limited data and formatting the `currentRateLimit` value for better readability.

### Detailed summary
- Updated the `currentRateLimit` display to use `toLocaleString()`.
- Added a check for `hasAnyRateLimited` to conditionally configure the chart.
- Modified the `config` object to include `rateLimitedCount` only if there are any rate-limited requests.
- Converted `includedCount` and `rateLimitedCount` to `Number` type during data mapping.

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

<!-- end pr-codex -->
2025-04-15 21:16:37 +00:00
jnsdls e29f749342 Add support for starter, accelerate, and scale tiers (#6735)
fixes: CORE-826

# Add support for new pricing tiers in ticket creation

This PR adds support for the new pricing tiers (starter, accelerate, scale) in the ticket creation system. It updates the `planToCustomerId` mapping to include these new tiers and improves the fallback logic to use the free tier when a customer's plan doesn't have a corresponding Unthread customer ID.
2025-04-15 22:15:28 +02:00
Joaquim VergesandGitHub 2a7df337f7 [SDK] Improve error messages for failed HTTP requests (#6734) 2025-04-15 12:43:54 -07:00
jnsdls 348e3ab4ad Add RPC usage dashboard with rate limit monitoring (#6733)
currently blocked by analytics API needing to go to prod

<!-- start pr-codex -->

---

## PR-Codex overview
This PR introduces a new RPC usage dashboard for monitoring requests over the last 24 hours. It adds new components for visualizing request data and enhances the existing layout to accommodate this functionality.

### Detailed summary
- Added a new `RPC` tab in the usage layout.
- Introduced `CountGraph` and `RateGraph` components for visualizing RPC data.
- Implemented API call to fetch last 24 hours RPC usage data.
- Enhanced `ThirdwebAreaChart` to support max limit display.
- Updated error handling and user feedback for API data fetching.

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

<!-- end pr-codex -->
2025-04-15 19:25:24 +00:00
MananTank e4590a7ada [NEB-168] Nebula: Improve Execute Transaction Card UI (#6731)
![image.png](https://graphite-user-uploaded-assets-prod.s3.amazonaws.com/yNOf1svJ8o3zjO7zQouZ/5807d025-2166-4b28-bedb-2baccd52054c.png)
2025-04-16 00:42:26 +05:30
gregandGitHub a45c558e06 [SDK] Fix: Improve PayEmbed error message parsing (#6732) 2025-04-15 11:45:48 -07:00
a3e7300723 Get events from insight (#6683)
Co-authored-by: Joaquim Verges <[email protected]>
2025-04-15 04:02:59 -07:00
YashandGitHub 4c1f384a63 Fix dynamic contract publish (#6729) 2025-04-15 02:55:20 -07:00
arcoraven ec099a781a chore: Update rate limit to sliding window (10s) (#6730)
Updates the rate limit to use a sliding window

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on updating the rate limiting functionality in the `rateLimit` function to utilize a sliding window approach instead of a fixed window, enhancing how requests are counted over time.

### Detailed summary
- Renamed `RATE_LIMIT_WINDOW_SECONDS` to `SLIDING_WINDOW_SECONDS`.
- Modified Redis interaction to use `mget` for batch fetching request counts.
- Updated the logic to calculate total requests over the last 10 seconds.
- Removed `sampleRate` handling and its related logic.
- Changed error messages for exceeding rate limits.
- Adjusted tests to align with the new sliding window logic.

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

<!-- end pr-codex -->
2025-04-15 09:30:46 +00:00
Joaquim VergesandGitHub 185d2f309c [SDK] Add Insight API client (#6706) 2025-04-14 20:33:40 -07:00
gregandGitHub e8fb41752e [Dashboard] Feature: bridge UI (#6727) 2025-04-14 16:53:46 -07:00
jnsdls 3f42f522b7 Add RPC usage chart to dashboard (#6719)
closes: TOOL-4079

### TL;DR

Added RPC usage chart to the team usage overview page.

### What changed?

- Created a new API endpoint `fetchRPCUsage` to retrieve RPC usage data from the analytics service
- Added a stacked bar chart to visualize RPC usage in the team usage overview page
- The chart displays both regular RPC requests and rate-limited requests
- Replaced the static "Unlimited Requests" display with an interactive visualization

### How to test?

1. Navigate to a team's usage overview page
2. Verify that the RPC Requests section now shows a stacked bar chart
3. Confirm that the chart displays data for the past 7 days
4. Check that the chart properly differentiates between regular RPC requests and rate-limited requests

### Why make this change?

This change provides teams with better visibility into their RPC usage patterns over time, allowing them to monitor both successful requests and rate-limited requests. This visualization helps teams understand their API consumption patterns and make informed decisions about their plan requirements.

<!-- start pr-codex -->

---

## PR-Codex overview
This PR introduces a new feature to fetch and display RPC usage data in the dashboard. It enhances the usage overview by integrating RPC metrics, providing insights into included and rate-limited requests.

### Detailed summary
- Added `RPCUsageDataItem` type and `fetchRPCUsage` function in `rpc.ts`.
- Updated `page.tsx` to fetch RPC usage data alongside account usage and subscriptions.
- Passed `rpcUsage` data to the `Usage` component.
- Modified `Usage` component to process and display RPC usage data using `ThirdwebBarChart`.

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

<!-- end pr-codex -->
2025-04-14 20:27:11 +00:00
MananTank bb29e1f705 [TOOL-4033] Playground: UI revamp and cleanup, Improved engine playgrounds, updated theme (#6720)
<!--

## 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 removing unused assets and files, updating configurations, and enhancing UI components across the `playground-web` application. It also adds new constants and improves code structure for better maintainability.

### Detailed summary
- Deleted numerous unused image files and TypeScript files.
- Updated `postcss.config.js` and added Tailwind CSS configurations.
- Enhanced UI components with consistent styling changes.
- Introduced new constants for `mintExample` and `claimExample`.
- Improved API handling in the `engine` actions.
- Refactored several components to use `PageLayout` for better structure.
- Added code examples for minting and airdrop functionalities.

> The following files were skipped due to too many changes: `apps/playground-web/src/app/engine/airdrop/_components/airdrop-code.tsx`, `apps/playground-web/src/app/connect/pay/commerce/page.tsx`, `apps/playground-web/src/components/code/code-example.tsx`, `apps/playground-web/src/app/connect/in-app-wallet/ecosystem/page.tsx`, `apps/playground-web/src/components/blocks/APIHeader.tsx`, `apps/playground-web/src/app/connect/account-abstraction/connect/page.tsx`, `apps/playground-web/src/app/connect/pay/fund-wallet/page.tsx`, `apps/playground-web/src/app/connect/account-abstraction/sponsor/page.tsx`, `apps/playground-web/src/components/blocks/NetworkSelectors.tsx`, `apps/playground-web/src/app/connect/ui/page.tsx`, `apps/playground-web/src/app/connect/in-app-wallet/sponsor/page.tsx`, `apps/playground-web/src/components/in-app-wallet/custom-login-form.tsx`, `apps/playground-web/src/app/connect/pay/transactions/page.tsx`, `apps/playground-web/src/app/connect/in-app-wallet/page.tsx`, `apps/playground-web/src/components/in-app-wallet/profile-sections.tsx`, `apps/playground-web/src/app/connect/account-abstraction/native-aa/page.tsx`, `apps/playground-web/src/app/engine/airdrop/_components/airdrop-preview.tsx`, `apps/playground-web/src/components/headless-ui/token-examples.tsx`, `apps/playground-web/src/components/headless-ui/nft-examples.tsx`, `apps/playground-web/src/app/connect/sign-in/headless/page.tsx`, `apps/playground-web/src/app/connect/auth/page.tsx`, `apps/playground-web/src/app/globals.css`, `apps/playground-web/src/app/engine/webhooks/_components/webhooks-preview.tsx`, `apps/playground-web/src/app/connect/pay/embed/LeftSection.tsx`, `apps/playground-web/src/app/engine/airdrop/_components/airdrop-card.tsx`, `apps/playground-web/src/app/connect/blockchain-api/page.tsx`, `apps/playground-web/src/app/engine/minting/_components/mint-preview.tsx`, `apps/playground-web/src/components/headless-ui/account-examples.tsx`, `pnpm-lock.yaml`

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

<!-- end pr-codex -->
2025-04-14 19:54:24 +00:00
jnsdls 76822982f3 Add billing warning and open invoice filtering (#6718)
# Add Warning for Teams with Invalid Payment Methods

This PR adds a warning message for teams with invalid payment methods, preventing them from upgrading plans until they resolve outstanding invoices.

## Changes:

- Added a warning component that displays when a team has an `invalidPayment` billing status
- Added a filter to view only open invoices in the invoices page
- Updated the `getTeamInvoices` function to accept an optional `status` parameter
- Disabled checkout buttons for teams with invalid payment methods
- Added a warning in the cancel plan modal for teams with unpaid invoices
- Added a link to direct users to pay their outstanding invoices

These changes help users understand why they can't upgrade their plan and guides them to resolve payment issues.
2025-04-14 14:19:42 +00:00
Joaquim VergesGitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
52261a5740 Version Packages (#6715)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-04-14 00:44:22 -07:00
Joaquim VergesandGitHub cd30ba4f3a [Dashboard] Add partner-based analytics for ecosystem wallet usage (#6717) 2025-04-13 22:13:15 -07:00
gregandGitHub 0ea898712b [SDK] Fix: Error message cleanup on UB embed (#6716) 2025-04-13 19:17:21 -07:00
gregfromstl 1b2caecfc4 [Dashboard] Feature: Improve chainlist bridge copy (#6714)
Closes: tool-4076
![Screenshot 2025-04-13 at 17 24 15](https://github.com/user-attachments/assets/1e6a3f04-cf0d-4dfa-9924-b5b611caaa7a)

<!--

## 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 `BuyFundsSection` component to reflect a shift in terminology from "Buy Funds" to "Bridge Funds," enhancing clarity regarding the functionality offered.

### Detailed summary
- Changed the `SectionTitle` from "Buy Funds" to "Bridge".
- Updated the heading text from "Buy Funds on {sanitizedChainName} using Universal Bridge" to "Bridge Funds to {sanitizedChainName}".
- Modified the paragraph text to clarify the process of obtaining the native currency.
- Altered the `PayModalButton` label from "Buy Funds on {sanitizedChainName}" to "Bridge to {sanitizedChainName}".

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

<!-- end pr-codex -->
2025-04-14 01:28:24 +00:00
Joaquim VergesandGitHub e574114188 [SDK] Don't retry on user connection rejection in EIP1193 connector (#6712) 2025-04-13 17:28:28 -07:00
Joaquim VergesGitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
aa733b7b3e Version Packages (#6709)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-04-13 15:50:19 -07:00
Joaquim VergesandGitHub fa0d1141da [SDK] Add retry logic to predictAccountAddress (#6707) 2025-04-13 15:31:08 -07:00
Joaquim VergesGitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
f618e2bffb Version Packages (#6705)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-04-11 22:40:10 +02:00
Jonas DanielsandGitHub 3ab31c8210 Fix incrBy to incrby in rate limit implementation (#6704) 2025-04-11 22:38:34 +02:00
Joaquim VergesGitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
8eb5d8c01e Version Packages (#6703)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-04-11 22:30:14 +02:00
Jonas DanielsandGitHub d036b87ae1 Refactor rate limiting to use separate read/write Redis operations (#6702) 2025-04-11 22:26:11 +02:00
YashandGitHub c86cac4d61 Fix getClaimConditions (#6701) 2025-04-11 19:19:32 +02:00
MananTank af09576247 Dashboard: Fix contract overpage page overflow when there are too many events in single tx (#6700)
<!--

## 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 `ContractOverviewPage` and `LatestEvents` components in a React application by adding overflow handling and improving the display of event data.

### Detailed summary
- Added `overflow-hidden` class to the `div` in `ContractOverviewPage.tsx`.
- Refactored the mapping of `allEvents` in `LatestEvents.tsx` to use a block structure for better readability.
- Limited the displayed events to 3 and added a button to show additional events if more than 3 exist.

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

<!-- end pr-codex -->
2025-04-11 17:01:47 +00:00
Manan TankandGitHub 316cef1a58 [TOOL-4057] Dashboard: Update Storage pinning card in team usage page (#6697) 2025-04-11 18:08:19 +02:00
Joaquim VergesGitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
1bfcd375e4 Version Packages (#6696)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-04-10 20:51:36 -07:00
Joaquim VergesandGitHub 86070fa309 [Pay] Add feePayer option for direct_payment mode in PayEmbed (#6695) 2025-04-10 20:43:43 -07:00
Joaquim VergesGitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
c5bf5a8594 Version Packages (#6694)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-04-10 14:16:49 -07:00
Joaquim VergesandGitHub c1d6bb7848 [Pay] Add extra gas buffer for quote and transfer transactions (#6693) 2025-04-10 14:10:45 -07:00
Toomas OosaluandGitHub e9db59e8da update insight plaground routes (#6692) 2025-04-10 12:55:59 -07:00
Joaquim VergesGitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
1db216545a Version Packages (#6691)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-04-10 12:55:22 -07:00
Joaquim VergesandGitHub 0a798c58b3 [SDK] Add smartAccountAddress to getUser result (#6690) 2025-04-10 12:17:31 -07:00
Joe-ThirdwebandGitHub cfacfc28a8 [Dashboard] Fix : Adding COTI chain page details (#6689)
Signed-off-by: Joe-Thirdweb <[email protected]>
2025-04-10 19:20:31 +02:00
KimaniandGitHub 2d978aea35 [Portal] Fix: updates broken-links on nebula sessions docs (#6687) 2025-04-10 19:17:19 +02:00
MananTank ca382f442a Dashboard: Remove certain warning logs (#6681)
<!--

## 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 package dependencies and configuration settings for the project, including the addition of `prettier` and adjustments to `@aws-sdk/client-sso-oidc` references in the `pnpm-lock.yaml` file.

### Detailed summary
- Added `public-hoist-pattern` entries in `.npmrc`.
- Updated `next.config.ts` to modify `config.externals`.
- Added `prettier` dependency in `package.json`.
- Updated `pnpm-lock.yaml` to reflect the new `prettier` version.
- Cleaned up references to `@aws-sdk/client-sso-oidc` in `pnpm-lock.yaml`.

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

<!-- end pr-codex -->
2025-04-10 12:59:07 +00:00
0xFirekeeper 840b1a264f [Portal] Hotfix Unity sidebar link (#6688)
<!--

## 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 sidebar navigation in the `sidebar.tsx` file, changing the name and link for a specific menu item.

### Detailed summary
- Changed the menu item name from `"Payments"` to `"Universal Bridge"`.
- Updated the `href` from `"/dotnet/pay/quickstart"` to `"/dotnet/universal-bridge/quickstart"`.

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

<!-- end pr-codex -->
2025-04-10 12:42:27 +00:00
Joaquim VergesGitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
d803d37879 Version Packages (#6656)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-04-09 16:26:39 -07:00
Joaquim VergesandGitHub e10c1e696e [SDK] Pass through desired chain when signing in with wallet for ecosystems (#6686) 2025-04-09 16:03:43 -07:00
kumaryash90 a2305329fd Update deploy config (#6667)
<!--

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

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

## Notes for the reviewer

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

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR introduces support for contracts that have no fees by adding new properties and constants to handle fee exemption logic. It modifies several components to utilize the new metadata and fee configurations, ensuring that contracts can be deployed without fees based on specific versions and chains.

### Detailed summary
- Added `contractMetadataNoFee` prop to `DeployFormForUri` and `CustomContractForm`.
- Introduced `ZERO_FEE_VERSIONS` and `ZERO_FEE_CHAINS` constants in `fee-config.ts`.
- Updated `DeployFormForPublishInfo` to fetch metadata for contracts without fees.
- Adjusted fee logic in `CustomContractForm` to use `ZERO_FEE_CHAINS`.
- Modified deployment logic to conditionally use fee-exempt metadata.

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

<!-- end pr-codex -->
2025-04-09 22:24:46 +00:00
MananTank 60235ade34 [TOOL-4028] Dashboard: Limit team name to 32 chars (#6685)
<!--

## 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 maximum length restriction for the `teamName` input field in the `TeamGeneralSettingsPageUI` component and applies this restriction to the `TeamInfoFormUI` component as well, enhancing user experience by preventing excessively long team names.

### Detailed summary
- Added `maxLength` attribute with a value of `32` to the `teamName` input in `TeamGeneralSettingsPageUI`.
- Updated the `onChange` handler in `TeamGeneralSettingsPageUI` to set the `teamName` directly from the input value.
- Introduced a new constant `maxTeamNameLength` in `TeamInfoForm.tsx` set to `32`.
- Added `maxLength={maxTeamNameLength}` to the input field in `TeamInfoFormUI`.

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

<!-- end pr-codex -->
2025-04-09 21:19:40 +00:00
MananTank 811384b91f [TOOL-4029] Dashboard: Remove create engine page, Various UI udpates in engine instances page (#6684) 2025-04-09 20:57:36 +00:00
Jonas DanielsandGitHub 22dc4d7cd1 update dependencies (#6678) 2025-04-09 20:57:45 +02:00
gregfromstl 3074e7c67d Fix flaky ETH price test (#6682)
<!--

## 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 tests in the `cryptoToFiat` and `Status` files, improving assertions and changing transaction details to reflect new data.

### Detailed summary
- In `cryptoToFiat.test.ts`:
  - Changed assertion from checking if `data.result` is greater than 1500 to ensuring it's greater than 0.

- In `Status.test.ts`:
  - Updated `transactionHash` and related transaction details, including:
    - Modified `destinationAmount`, `destinationChainId`, and `destinationTokenAddress`.
    - Adjusted `originAmount` and `originTokenAddress`.

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

<!-- end pr-codex -->
2025-04-09 17:48:09 +00:00
iuwqyir dd99410ccd update insight playground routes (#6680)
<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on updating the `insightBlueprints` in the `insightBlueprints.ts` file by adding new paths for token and NFT lookups and modifying an existing path name for clarity.

### Detailed summary
- Added a new path for token lookup:
  - `name`: "Token lookup",
  - `path`: "/v1/tokens/lookup"
- Added a new path for getting NFT collections:
  - `name`: "Get collection",
  - `path`: "/v1/nfts/collections/{contract_address}"
- Changed the name of an existing path from "Get NFT by token" to "Get NFT by token ID".

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

<!-- end pr-codex -->
2025-04-09 12:33:24 +00:00
MananTank b4e5dc1f3c [Dashboard] Migrate missing extension overlay from chakra to tailwind css (#6677)
<!--

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

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

## Notes for the reviewer

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

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR primarily focuses on enhancing the UI components related to settings by adding `overflow="hidden"` to `Card` components and refining the `SettingDetectedState` component's structure and styling for improved clarity and responsiveness.

### Detailed summary
- Added `overflow="hidden"` to `Card` components in multiple files.
- Refactored `SettingDetectedState` to use a function declaration instead of a functional component type.
- Improved the layout and styling of the loading and error states in `SettingDetectedState`.
- Replaced `SimpleGrid` with a `div` for better control over layout.
- Updated text structure and classes for better styling consistency.

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

<!-- end pr-codex -->
2025-04-08 22:00:12 +00:00
MananTank 8b4091e848 [Dashboard] Contracts sources page: migrate from chakra to tailwind+shadcn (#6676)
<!--

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

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

## Notes for the reviewer

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

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR primarily focuses on enhancing the `SourcesPanel` and `SourcesAccordion` components by improving their structure and styling. It also introduces a new modal for contract verification and updates the UI elements for better user experience.

### Detailed summary
- Replaced `Text` and `Link` with `p` and `UnderlineLink` in `SourcesPanel`.
- Updated `SourcesAccordion` to use `SourceAccordionItem` for rendering items.
- Modified `VerifyContractModal` to a new functional component, `VerifyContractModalContent`.
- Enhanced loading and error handling in the verification process.
- Changed the layout and styling of the `ContractSourcesPage`.
- Introduced `Dialog` for contract verification instead of using a modal.
- Updated button styles and loading indicators for better UX.

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

<!-- end pr-codex -->
2025-04-08 21:34:47 +00:00
MananTank 2d15e6f509 Dashboard: Update pricing cards, style tweaks (#6675) 2025-04-08 18:41:28 +00:00
MananTank e450d320a0 [TOOL-3969] Dashboard: Update Plans, Add cancel plan, and other related UI updates (#6674)
<!-- start pr-codex -->

## PR-Codex overview
This PR focuses on enhancing the billing and subscription management system by adding new team plans, updating existing components, and improving the user interface for better user experience.

### Detailed summary
- Added new plans: `accelerate` and `scale`.
- Updated `TeamPlan` type to include new plans.
- Modified `PlanInfoCard` to support new plans and improved UI.
- Refactored billing components to use new plan data.
- Enhanced `InviteSection` with new team plan options.
- Updated storybook files for new billing plans.
- Improved `CancelPlanModal` for better user feedback.
- Added helper functions for plan CTA logic.

> The following files were skipped due to too many changes: `apps/dashboard/src/components/onboarding/ApplyForOpCreditsModal.tsx`

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

<!-- end pr-codex -->
2025-04-08 17:58:37 +00:00
MananTank 838f8fa547 Dashboard: Update contracts page header: render on top of sidebar (#6666) 2025-04-07 20:21:39 +00:00
MananTank 1dae74b9d2 Dashboard: Update NFT token details page styles, reduce chakra usage (#6665)
<!-- start pr-codex -->

## PR-Codex overview
This PR focuses on improving the UI components of the dashboard, enhancing the styling and structure of various elements, and adding new features like tooltips and breadcrumbs for better navigation.

### Detailed summary
- Changed `className` from `font-semibold` to `font-bold` in the `h1` element.
- Updated `NftProperty` component to use `div` instead of `Card`.
- Added `toolTip` property to `TabButtons`.
- Wrapped `Button` in `ToolTipLabel` for tooltips.
- Introduced `IPFSLinkGroup` component for displaying IPFS links.
- Enhanced `NFTName` and `NFTDescription` styling.
- Removed unnecessary imports and unused variables for cleaner code.

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

<!-- end pr-codex -->
2025-04-07 16:38:03 +00:00
MananTank 672f7d00bd Dashboard: Contract page style adjustments, fixes, reduce chakra usage (#6664)
<!-- start pr-codex -->

## PR-Codex overview
This PR focuses on enhancing the UI components within the dashboard, improving layout consistency, and updating text for better clarity and user experience.

### Detailed summary
- Updated `Button` styles in `primary-dashboard-button.tsx`.
- Increased gap in layout for `ContractOverviewPage.tsx`.
- Changed headings to `h2` for better accessibility in auction and listing pages.
- Introduced `StatCard` component for consistent stats display.
- Renamed `tokenBalanceQuery` to `ownedTokenBalanceQuery` for clarity.
- Updated `SupplyCards` to use `StatCard` instead of `Stat`.
- Improved layout structure with new divs and classes for better styling.
- Added `ArrowRightIcon` to navigation links for visual cues.

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

<!-- end pr-codex -->
2025-04-07 13:19:42 +00:00
MananTank a9eda9fc8d Dashboard: Migrate contract overview page components to tailwind/shadcn (#6662)
<!--

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

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

## Notes for the reviewer

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

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on refactoring and improving the UI components related to token and NFT details, enhancing the overall structure and user experience in the dashboard application.

### Detailed summary
- Removed unused files: `data.ts`, `types.ts`, and `TokenDetails.tsx`.
- Renamed `StatCard` to `Stat` for consistency.
- Updated UI components to improve layout and styling.
- Enhanced `TokenDetailsCard` and `NFTDetails` for better presentation.
- Added storybook entries for `PermissionsTable` and `LatestEvents`.
- Improved handling of loading states in various components.
- Replaced Chakra UI components with custom UI components for consistency.

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

<!-- end pr-codex -->
2025-04-07 09:23:59 +00:00
Manan TankandGitHub a3b4720f69 Dashboard: Remove chakra usage from contract analytics page, style tweaks (#6661) 2025-04-07 11:22:45 +02:00
Jonas DanielsandGitHub c2a5f65239 update dependencies (#6658) 2025-04-07 11:22:10 +02:00
Joaquim VergesandGitHub 7ab76ac55a [Dashboard] Add team ID to ecosystem API requests (#6659) 2025-04-06 17:04:50 -07:00
MananTank 386138e4f0 Dashboard: Team invoices page style tweaks (#6660)
<!--

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

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

## Notes for the reviewer

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

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on refactoring the rendering of the invoice history page and improving the `BillingHistory` component's layout and functionality, including UI updates and pagination controls.

### Detailed summary
- Replaced `Card` components with a `div` structure for the invoice history.
- Updated headings and descriptions with new styling.
- Changed badge components for invoice statuses to use variants.
- Refined pagination controls with improved styling and layout.

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

<!-- end pr-codex -->
2025-04-05 13:53:12 +00:00
Reza RahemtolaandGitHub 309d1d544b [Docs] Fix: Add missing import (#6657)
Signed-off-by: Reza Rahemtola <[email protected]>
2025-04-05 10:17:51 +02:00
kumaryash90 59be4ea2fb [SDK/Dashboard] fix: ERC1155 single phase claim conditions (#6655)
<!--

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

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

## Notes for the reviewer

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

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on adding single phase functionality for `erc1155` claim conditions in the `thirdweb` dashboard and related utilities, enhancing the ability to manage claim eligibility and conditions for tokens.

### Detailed summary
- Introduced `isMultiphase` and `isSinglePhase` properties in various components.
- Updated `setClaimPhasesTx` to handle single phase options.
- Modified `getClaimConditions` to support both multi-phase and single-phase claims.
- Enhanced `resetClaimEligibility` to work with single phase drops.
- Updated tests to validate new single phase functionality.

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

<!-- end pr-codex -->
2025-04-05 02:26:03 +00:00
MananTank 9c737a1905 Insight Playground: Show error when no chains selected (#6654) 2025-04-04 19:59:22 +00:00
saminacodes 646a5a3788 Ub docs 2 (#6653)
CORE-0000
---

## PR-Codex overview
This PR focuses on updating the terminology and content related to crypto transactions and user identity features in the application. It enhances documentation and descriptions to better reflect the functionality of the components.

### Detailed summary
- Updated `description` in `PayEmbed` to "Prebuilt UI to buy crypto by swapping or fiat onramps".
- Changed title in `fund-wallets` from "Fund wallets" to "Buy Crypto".
- Added new `Details` sections in `faqs` for Universal Bridge operation and KYC requirements.
- Removed user identity links in `sidebar` and reorganized sections.

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

<!-- end pr-codex -->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on updating the terminology and descriptions related to crypto purchases within the application, particularly changing references from "Fund wallets" to "Buy Crypto." It also adds new FAQ sections and modifies existing sidebar navigation.

### Detailed summary
- Updated `description` in `page.mdx` files to reflect "Buy Crypto" instead of "Fund wallets."
- Added new FAQ entries in `faqs/page.mdx` about Universal Bridge operations and KYC requirements.
- Changed sidebar navigation to include new entries and removed outdated user identity links.

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

<!-- end pr-codex -->
2025-04-04 17:55:24 +00:00
MananTank 7597418872 [TOOL-3564] Playground: Select multiple chains in insight playground (#6652)
<!-- start pr-codex -->

## PR-Codex overview
This PR introduces a `MultiSelect` component along with a `useShowMore` hook to enhance the selection of network chains in a UI. It improves the user experience by allowing more items to be displayed as the user scrolls and provides a more flexible selection interface.

### Detailed summary
- Added `useShowMore` hook to manage displaying more items on scroll.
- Introduced `MultiNetworkSelector` for selecting multiple network chains.
- Updated `BlueprintPlayground` to utilize `MultiNetworkSelector`.
- Modified `ParameterSection` to handle array values for chain selection.
- Created `MultiSelect` component for better multi-item selection.
- Enhanced UI with search functionality and option rendering in `MultiSelect`.
- Implemented badge display for selected items in `MultiSelect`.

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

<!-- end pr-codex -->
2025-04-04 17:46:52 +00:00
MananTank 5204bd506f [TOOL-3954] Dashboard: Add tracking in contracts page (#6651) 2025-04-04 16:27:49 +00:00
MananTank 663627715a [TOOL-3876] Dashboard: Merge Framer sitemap.xml to dashboard sitemap.xml (#6648)
<!-- start pr-codex -->

## PR-Codex overview
This PR primarily introduces the `fast-xml-parser` package to the project and updates the `fetchChainsFromApi` function to fetch and parse XML data. It also modifies the sitemap generation logic to utilize the newly added functionality.

### Detailed summary
- Added `fast-xml-parser` to `package.json` and `pnpm-lock.yaml`.
- Updated `next-sitemap.config.js` to require `fast-xml-parser`.
- Introduced a new function `getFramerXML` to fetch and parse XML data.
- Modified `additionalPaths` to use data from `getFramerXML`.
- Adjusted the return structure of URLs in `additionalPaths`.

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

<!-- end pr-codex -->
2025-04-04 14:58:58 +00:00
MananTank 8f7bcaeadc Dashboard: Fix sitemap.xml and other files on public folder not loading (#6647) 2025-04-04 14:20:17 +00:00
Joaquim VergesGitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
ad7f78ca0b Version Packages (#6644)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-04-04 12:18:29 +02:00
Jonas DanielsandGitHub 0967a5d53a Fix newline at end of monad-testnet.ts (#6646) 2025-04-04 12:07:42 +02:00
KrishangandGitHub d93765ac54 [SDK] Feature: Export monadTestnet from thirdweb/chains (#6645) 2025-04-04 02:11:49 -07:00
169ae22a86 Add team capabilities to service-utils (#6641)
Co-authored-by: Phillip Ho <[email protected]>
2025-04-04 12:37:23 +08:00
YashandGitHub 04d2bc72e8 Update deploy config (#6643) 2025-04-04 16:55:41 +13:00
MananTank 1a236fd103 Dashboard: Storybook cleanup (#6642)
<!--

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

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

## Notes for the reviewer

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

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on refactoring Storybook stories across various components by standardizing the export format, updating layout parameters, and removing unnecessary imports. It aims to improve consistency and maintainability in the codebase.

### Detailed summary
- Changed `export const Desktop` to `export const Variants` in multiple stories.
- Removed `parameters: { layout: "centered" }` from many stories.
- Updated container classes for consistent styling.
- Removed `Toaster` components from stories.
- Refactored story decorators for better structure.
- Deleted unused imports like `mobileViewport` from several files.
- Consolidated `BadgeContainer` usage for better readability.

> The following files were skipped due to too many changes: `apps/dashboard/src/app/team/components/Analytics/CombinedBarChartCard.stories.tsx`, `apps/dashboard/src/app/team/[team_slug]/[project_slug]/nebula/components/analytics/nebula-analytics-ui.stories.tsx`, `apps/dashboard/src/app/nebula-app/(app)/components/ChatPageLayout.stories.tsx`, `apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/modules/components/batchMetadata.stories.tsx`, `apps/dashboard/src/components/dashboard/StepsCard.stories.tsx`, `apps/dashboard/src/@/components/blocks/SettingsCard.stories.tsx`

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

<!-- end pr-codex -->
2025-04-03 22:02:08 +00:00
Joaquim VergesGitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
4691343c7d Version Packages (#6638)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-04-03 14:14:43 -07:00
Joaquim VergesandGitHub 93f225b8a3 [SDK] Fix ethers6 provider override (#6640) 2025-04-03 14:13:12 -07:00
Joaquim VergesandGitHub e9649a2b53 Update Next.js to 15.2.4 and other dependencies (#6632) 2025-04-03 12:51:19 -07:00
Joaquim VergesGitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
4d39fb6ee3 Version Packages (#6620)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-04-03 12:28:27 -07:00
MananTank 1e092a7bff Dashboard: Fix timestamp ordering in various charts, migrate them to use ThirdwebBarChart (#6636)
<!-- start pr-codex -->

## PR-Codex overview
This PR primarily focuses on updating various components to improve chart functionalities and data handling in the dashboard, including the introduction of the `ThirdwebBarChart`. It also refines date handling and enhances the user interface for better data representation.

### Detailed summary
- Updated date handling in `stubbedData` from February to December.
- Introduced `BadgeContainer` for displaying time ranges in `TotalSponsoredChartCard`.
- Enhanced `searchParams` structure in the analytics page to include `from` and `to` dates.
- Added `customHeader`, `toolTipValueFormatter`, and `emptyChartState` to `ThirdwebBarChart`.
- Replaced `BarChart` with `ThirdwebBarChart` in multiple components for consistency.
- Improved data mapping and sorting in `TotalSponsoredChartCard`.
- Created new stubs for `EcosystemWalletUsersChartCard` to simulate user data over various days.
- Refactored empty state components to improve user guidance when no data is available.

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

<!-- end pr-codex -->
2025-04-03 18:36:21 +00:00
MananTank 80ee3d6670 [TOOL-3950] Dashboard: link to /explore page for contracts card on chain page (#6637)
<!--

## 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 a link in the `products.ts` file for the dashboard component, changing the URL for a specific product.

### Detailed summary
- Updated the `link` property for the product from `https://thirdweb.com/contracts` to `https://thirdweb.com/explore`.

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

<!-- end pr-codex -->
2025-04-03 17:05:00 +00:00
MananTank a7fd4e51a3 [TOOL-3946] Dashboard: Fix in-app wallet users chart timestamp incorrect ordering (#6635) 2025-04-03 13:09:56 +00:00
MananTank 61583cf6f3 Dashboard: Fix project settings page max-width (#6634)
<!--

## 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 layout of the `settings` page in the project dashboard. It enhances the styling and structure of the header and the content area.

### Detailed summary
- Replaced the existing `<h1>` element with a new structure wrapped in a `<div>` with a class of `border-b py-10`.
- Changed the font size of the `<h1>` from `text-3xl` to `text-2xl` for better responsiveness.
- Added a container with a maximum width of `7xl` around the header.
- Adjusted the padding of the content area to `pt-6 pb-20`.

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

<!-- end pr-codex -->
2025-04-03 12:58:26 +00:00
iuwqyir cf02af06a4 fix insight blueprints navigation (#6633)
## [Dashboard] Feature: Update API path format and add token lookup endpoint

## Notes for the reviewer

This PR updates the API path format in `insightBlueprints.ts` to use curly braces for path parameters (e.g., changing `/v1/transactions/:contractAddress` to `/v1/transactions/{contractAddress}`). This standardizes our path format to match OpenAPI specifications.

## How to test

The changes can be verified by checking the updated paths in the playground interface to ensure they display correctly with the new format.

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on updating the URL path parameters in the `insightBlueprints` to use curly braces `{}` instead of colons `:` for better clarity and consistency in API endpoints.

### Detailed summary
- Changed path parameters from `:paramName` to `{paramName}` for various endpoints:
  - `/v1/transactions/:contractAddress` to `/v1/transactions/{contractAddress}`
  - `/v1/wallets/:wallet_address/transactions` to `/v1/wallets/{wallet_address}/transactions`
  - Updated multiple other endpoints similarly, including:
    - `/v1/events/:contractAddress` to `/v1/events/{contractAddress}`
    - `/v1/tokens/transfers/:contract_address` to `/v1/tokens/transfers/{contract_address}`
    - `/v1/nfts/:contract_address` to `/v1/nfts/{contract_address}`
    - And many more throughout the file.

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

<!-- end pr-codex -->
2025-04-03 12:15:39 +00:00
MananTank 6001f2487e [TOOL-3909] Dashboard: Add footer links to project pages & header style adjustments (#6627)
<!-- start pr-codex -->

## PR-Codex overview
This PR focuses on enhancing the layout and structure of various components in the application, introducing new footer sections, and improving the user interface for better organization and navigation.

### Detailed summary
- Removed the `SupportedPlatformLink.tsx` file.
- Updated `href` in `react-native/v5/page.mdx` from `/installation` to `/getting-started`.
- Enhanced layout in multiple components with added `container` classes for better styling.
- Introduced new `FooterCard` component in various pages for documentation and tutorial links.
- Replaced `AAFooterSection` with `AAFooter` in `AccountAbstractionPage.tsx`.
- Updated `InAppWalletFooterSection` to `InAppWalletsFooter` with a new structure.
- Modified headers and added spacing in several components for improved visual hierarchy.
- Introduced new `InsightFooter` component with relevant links.
- Adjusted various components to use consistent `container` class for layout consistency.

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

<!-- end pr-codex -->
2025-04-03 17:23:24 +05:30
Joaquim VergesandGitHub 3f21445369 [SDK] Emit accountChanged event when smart wallets switch chains (#6626) 2025-04-02 18:19:14 -07:00
MananTank 9e8fd89ec0 [TOOL-3942] Playground: Fix slow dev server - generate insight sidebar links with script (#6631)
<!--

## 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 script for managing insight blueprints, updates the `getSidebarLinks` function to use pre-fetched blueprints, and modifies the `Page` component to display these blueprints. It also refines the utility functions for fetching blueprint data.

### Detailed summary
- Added `update-insight-blueprints` script in `package.json`.
- Created `insightBlueprints.ts` to store auto-generated blueprint data.
- Updated `getSidebarLinks` to use `insightBlueprints` instead of fetching blueprints dynamically.
- Modified `Page` component to render blueprints from `insightBlueprints`.
- Enhanced utility functions in `utils.ts` for fetching blueprint data.

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

<!-- end pr-codex -->
2025-04-02 23:28:15 +00:00
Joaquim VergesandGitHub 95a87530e3 [SDK] Fix updateListing date handling in marketplace direct listings (#6628) 2025-04-02 16:27:41 -07:00
0xFirekeeper 5faaf1be48 [Portal] Universal Bridge .NET (#6625)
<!--

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

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

## Notes for the reviewer

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

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on the removal of several payment-related pages in the `.NET SDK` documentation and the addition of a new section for the `Universal Bridge`, including examples and detailed API usage.

### Detailed summary
- Deleted pages:
  - `quickstart/page.mdx`
  - `buywithfiat/page.mdx`
  - `buywithcrypto/page.mdx`
  - `getbuyhistory/page.mdx`
  - `getbuywithfiatquote/page.mdx`
  - `getbuywithfiatstatus/page.mdx`
  - `getbuywithcryptoquote/page.mdx`
  - `getbuywithcryptostatus/page.mdx`
  - `getbuywithfiatcurrencies/page.mdx`
- Updated `sidebar.tsx` to remove payment links and add a new `Bridge` section.
- Introduced `Universal Bridge` documentation with:
  - Metadata creation
  - Core API examples for buying, selling, and transferring tokens.
  - Code snippets for `C#` usage of the `ThirdwebBridge`.

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

<!-- end pr-codex -->
2025-04-02 18:26:19 +00:00
jnsdls 4847ada819 Add /brand-kit to framer rewrites (#6624)
Added a rewrite rule for the `/brand-kit` path in the Framer rewrites configuration.
2025-04-02 17:53:14 +00:00
MananTank 08eea95f9b [TOOL-3764] Dashboard: Add Nebula FTUX (#6616)
<!--

## 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 `ClientIDSection` component to streamline the display of the Client ID across various components. It also modifies the `WaitingForIntegrationCard` to utilize this new component, improving code organization and reducing redundancy.

### Detailed summary
- Added `ClientIDSection` component to display the Client ID.
- Updated `InsightFTUX` to include `ClientIDSection` instead of inline code.
- Modified `WaitingForIntegrationCard` to accept children for rendering.
- Created `NebulaFTUX` component to integrate Nebula features and display Client ID.
- Adjusted styles in various components for consistency.

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

<!-- end pr-codex -->
2025-04-02 09:12:32 +00:00
MananTank 737155ba21 [TOOL-3765] Dashboard: Add Insight FTUX (#6614)
<!--

## 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 `Insight` page by introducing new components for better integration and user experience, specifically around project insights and blueprints. It refactors existing code and adds new features for displaying API usage examples.

### Detailed summary
- Updated `isProjectActive` function to accept a structured parameter.
- Added `InsightFTUX` component for displaying integration instructions.
- Introduced `BlueprintCard` component with feature descriptions.
- Modified `WaitingForIntegrationCard` to display the client ID without a period.
- Enhanced `page.tsx` to conditionally render `InsightFTUX` or `BlueprintCard` based on project status.

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

<!-- end pr-codex -->
2025-04-02 09:12:31 +00:00
MananTank 73ae5817f2 [TOOL-3763] Dashboard: Update Contracts page (#6611)
<!--

## 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 user interface and functionality of the contracts management section. It introduces new components and modifies existing ones to improve user experience, particularly around contract deployment and management.

### Detailed summary
- Removed the `GetStartedWithContractsDeploy` component.
- Added `DeployViaCLIOrImportCard` component for contract deployment options.
- Updated the `DeployedContractsPage` to include the new `DeployViaCLIOrImportCard`.
- Modified the `ContractTableUI` to display a message and button when no contracts are present.
- Increased the height of the no contracts message section.
- Added a link to discover contracts with an external icon.

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

<!-- end pr-codex -->
2025-04-02 09:12:31 +00:00
Joaquim VergesandGitHub 6a0704a19b Connect to current wallet chain when connecting other wallets in PayEmbed (#6621) 2025-04-02 02:09:39 -07:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2ef8f2a371 chore(deps): bump biomejs/setup-biome from 2.3.0 to 2.5.0 (#6598)
Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-02 01:50:04 -07:00
d6373b2c67 [SDK] Feature: added ru_RU locale to thirdweb package (#6520)
Co-authored-by: Denis Vasilev <[email protected]>
Co-authored-by: Joaquim Verges <[email protected]>
2025-04-02 01:46:15 -07:00
MananTank 7eaf86a607 [TOOL-3762] Dashboard: Update In-app wallet analytics page (#6610)
<!-- start pr-codex -->

## PR-Codex overview
This PR focuses on refactoring components related to in-app wallets and account abstraction analytics. It introduces new loading states, replaces certain components with `StatCard`, and enhances the handling of asynchronous data fetching.

### Detailed summary
- Removed `loading.tsx` files for in-app wallets and account abstraction.
- Updated `InAppWalletsHeader` to no longer accept props.
- Replaced `Stat` with `StatCard` in `EcosystemWalletsSummary` and `AccountAbstractionSummary`.
- Added `isPending` state to `StatCard` for loading indication.
- Refactored `InAppWalletsSummary` and `AccountAbstractionSummary` to utilize async data fetching with `Suspense`.
- Enhanced `InAppWalletAnalytics` to manage loading states and data fetching more effectively.

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

<!-- end pr-codex -->
2025-04-02 07:10:08 +00:00
gregGitHubgraphite-app[bot] <96075541+graphite-app[bot]@users.noreply.github.com>
5788cbdbad [SDK] Feature: Adds action field to Bridge transactions (#6613)
Signed-off-by: greg <[email protected]>
Co-authored-by: graphite-app[bot] <96075541+graphite-app[bot]@users.noreply.github.com>
2025-04-02 00:08:03 -07:00
Joaquim VergesGitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
d9a39cde33 Version Packages (#6619)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-04-01 19:43:56 -07:00
Joaquim VergesandGitHub 62c8e7c0c4 [SDK] show transfer amounts with fees (#6618) 2025-04-01 19:20:15 -07:00
Joaquim VergesandGitHub f618c5308c [SDK] Handle entrypoint resolution inside createAndSignUserOp (#6615) 2025-04-01 18:17:17 -07:00
gregandGitHub 04943d730b [Dashboard] Feature: Token symbol search (#6617) 2025-04-01 15:06:52 -07:00
Joaquim VergesGitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2606da6d87 Version Packages (#6606)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-04-01 13:02:59 -07:00
kumaryash90 9cf137660a Remove custom config for Ronin (#6612)
Create2 deployer restrictions are lifted by the chain. Enable deployments through default create2 factory.

<!--

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

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

## Notes for the reviewer

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

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on removing the custom configuration for the `Ronin` blockchain within the `thirdweb` package.

### Detailed summary
- Removed the `Ronin` configuration from the gas price settings in the `create-2-factory.ts` file.
- Updated the gas price for `Ronin` from `110n * 10n ** 9n` to a static value of `110000n`.

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

<!-- end pr-codex -->
2025-04-01 19:06:06 +00:00
MananTank 58ab8c2ee7 [TOOL-3766] Dashboard: Account Abstraction page updates (#6609)
<!-- start pr-codex -->

## PR-Codex overview
This PR focuses on improving the UI and functionality of various components in the dashboard by modifying styles, enhancing layout elements, and integrating new features related to account abstraction and user operation statistics.

### Detailed summary
- Removed `lg:gap-6` from grid classes in multiple components.
- Increased header sizes from `text-lg` to `text-xl` in `SponsoredTransactionsTableUI.tsx`.
- Adjusted margins and padding for improved spacing in various components.
- Added new `GasCreditAlert` component for displaying gas credit information.
- Integrated `AccountAbstractionSummary` into `AccountAbstractionAnalytics`.
- Updated `userOpStats` handling in `Page` component to use promises for better performance.
- Replaced certain text with more concise phrasing in descriptions.
- Removed unused imports and optimized component structure for clarity.

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

<!-- end pr-codex -->
2025-04-01 18:12:49 +00:00
MananTank 447a519ddc [TOOL-3768] Dashboard: Move Nebula from team to project layout (#6608)
<!-- start pr-codex -->

## PR-Codex overview
This PR focuses on enhancing the `Nebula` components in the dashboard application by improving layout styles, adding project ID handling in analytics, and adjusting waitlist checks.

### Detailed summary
- Updated class names for better styling in `nebula-analytics-filter.tsx` and `nebula-waitlist-page.tsx`.
- Added `projectId` parameter to `fetchNebulaAnalytics`.
- Modified `NebulaWaitListPageUI` layout.
- Integrated `showNebula` prop in `ProjectSidebarLayout`.
- Adjusted project layout and waitlist checks in `layout.tsx`.
- Enhanced `NebulaAnalyticsPage` to pass `projectId`.
- Updated page handling in `page.tsx` to include project fetching and redirects.

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

<!-- end pr-codex -->
2025-04-01 11:56:32 +00:00
Joaquim VergesandGitHub a9b7633d05 [SDK] Show past transactions in transactions modal screen (#6607) 2025-04-01 03:14:28 -07:00
Joaquim VergesandGitHub 38e089a5f3 [SDK] Only onramp for required transaction amount (#6605) 2025-04-01 02:01:11 -07:00
Joaquim VergesandGitHub ef2f86cff0 Version Packages (#6589) 2025-03-31 23:05:33 -07:00
gregfromstl 8fdef82147 [Playground] Fix: Filter UB Backend API for only GETs (#6604)
<!--

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

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

## Notes for the reviewer

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

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on enhancing the layout and functionality of the `Page` component in `apps/playground-web/src/app/connect/pay/backend/page.tsx`. It improves the presentation of the Universal Bridge REST API information and adds a link to view all endpoints.

### Detailed summary
- Wrapped the header and description in a `div` for better layout.
- Added a `Link` component with a button to "View all endpoints".
- Changed the title of the `BlueprintSection` from "Available endpoints" to "Available GET endpoints".
- Updated the filtering of `blueprints` to only include those with defined `get` properties.

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

<!-- end pr-codex -->
2025-04-01 01:02:26 +00:00
MananTank cbb15692a7 [TOOL-3880] Playground: Add utm_source on links (#6597)
<!-- start pr-codex -->

## PR-Codex overview
This PR primarily updates various `docsLink` URLs across multiple components in the `playground-web` application to include a query parameter (`?utm_source=playground`). This change likely aims to track traffic sources more effectively.

### Detailed summary
- Updated `docsLink` in `apps/playground-web/src/app/connect/auth/page.tsx`
- Updated `docsLink` in `apps/playground-web/src/app/connect/pay/page.tsx`
- Updated `docsLink` in `apps/playground-web/src/app/connect/blockchain-api/page.tsx`
- Updated `docsLink` in `apps/playground-web/src/app/connect/pay/commerce/page.tsx`
- Updated `docsLink` in `apps/playground-web/src/app/connect/pay/fund-wallet/page.tsx`
- Updated `docsLink` in `apps/playground-web/src/app/connect/pay/transactions/page.tsx`
- Updated `docsLink` in `apps/playground-web/src/app/connect/sign-in/embed/page.tsx`
- Updated `docsLink` in `apps/playground-web/src/app/connect/sign-in/button/page.tsx`
- Updated `docsLink` in `apps/playground-web/src/app/connect/ui/page.tsx`
- Updated `docsLink` in `apps/playground-web/src/app/connect/sign-in/headless/page.tsx`
- Updated `docsLink` in `apps/playground-web/src/app/connect/in-app-wallet/layout.tsx`
- Updated `docsLink` in `apps/playground-web/src/app/insight/layout.tsx`
- Updated `docsLink` in `apps/playground-web/src/components/blocks/APIHeader.tsx`
- Updated `docsLink` in `apps/playground-web/src/app/connect/pay/backend/layout.tsx`
- Updated `docsLink` in `apps/playground-web/src/app/connect/account-abstraction/connect/page.tsx`
- Updated `docsLink` in `apps/playground-web/src/app/connect/account-abstraction/sponsor/page.tsx`
- Updated `docsLink` in `apps/playground-web/src/app/connect/ui/nft/page.tsx`
- Updated `docsLink` in `apps/playground-web/src/app/otherLinks.ts`
- Updated `docsLink` in `apps/playground-web/src/app/connect/account-abstraction/native-aa/page.tsx`
- Updated `docsLink` in `apps/playground-web/src/app/connect/ui/chain/page.tsx`
- Updated `docsLink` in `apps/playground-web/src/app/connect/ui/token/page.tsx`
- Updated `docsLink` in `apps/playground-web/src/app/connect/social/page.tsx`
- Updated `docsLink` in `apps/playground-web/src/app/connect/ui/wallet/page.tsx`
- Updated `href` in `apps/playground-web/src/app/connect/pay/embed/LeftSection.tsx`
- Updated `href` in `apps/playground-web/src/app/insight/page.tsx`
- Updated `href` in `apps/playground-web/src/components/auth/gated-content.tsx`
- Updated `href` in `apps/playground-web/src/components/engine/minting/erc1155-mint-to.tsx`
- Updated `href` in `apps/playground-web/src/app/connect/sign-in/components/InAppWalletFormGroup.tsx`
- Updated `href` in `apps/playground-web/src/app/connect/sign-in/button/LeftSection.tsx`

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

<!-- end pr-codex -->
2025-03-31 19:37:09 +00:00
MananTank 1d23e5cd9c Dashboard: Fix AA page alert styles (#6596)
<!--

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

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

## Notes for the reviewer

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

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on refactoring the `SmartWalletsBillingAlert` component in the `Alerts.tsx` file. It updates the alert's UI and removes the dismissible functionality, while also changing the import paths for components used in the alert.

### Detailed summary
- Removed unused imports related to `Alert`, `AlertDescription`, `AlertIcon`, `AlertTitle`, `Flex`, and `IconButton`.
- Updated imports for `Alert`, `AlertDescription`, `AlertTitle`, `TrackedUnderlineLink`, and `CircleAlertIcon`.
- Removed the `dismissible` prop and related logic.
- Simplified the alert structure with a new layout.
- Changed the alert icon and styling.

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

<!-- end pr-codex -->
2025-03-31 19:07:57 +00:00
MananTank a17d893811 [TOOL-3873] Fix pageview tracking in dashboard, Add posthog to playground (#6595) 2025-03-31 18:38:58 +00:00
kumaryash90 0dcde077f5 [SDK] Fix bytecode caching (#6593)
<!--

## 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 bytecode caching mechanism in the `thirdweb` package by including additional metadata in the cache key, which improves cache differentiation and management.

### Detailed summary
- Updated the `cacheKey` in `deploy-metadata.ts` to include `compilerMetadata.publisher` and `compilerMetadata.version` along with `compilerMetadata.name` and `chain.id`.
- Removed the `version` and `publisher` fields from the deployment test case in `process-ref-deployments.test.ts`.

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

<!-- end pr-codex -->
2025-03-31 18:30:28 +00:00
jnsdls 05a041fac3 Add glossary slug route to framer rewrites (#6594)
### TL;DR

Added a rewrite rule for glossary detail pages in the dashboard app.

### What changed?

Added a new path `/learn/glossary/:glossary_slug` to the framer-rewrites.js file to support individual glossary term pages.

### How to test?

1. Navigate to a glossary term detail page (e.g., `/learn/glossary/some-term`)
2. Verify that the page loads correctly and the routing works as expected

### Why make this change?

To enable proper routing for individual glossary term pages, allowing users to access specific glossary entries directly through their unique URLs. This complements the existing glossary index page that was already configured.
2025-03-31 18:00:30 +00:00
MananTank 3d140eed97 [TOOL-3874] Portal: Fix pl.js script CSP error (#6591)
<!--

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

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

## Notes for the reviewer

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

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on updating the Content Security Policy in the `next.config.mjs` file to enhance security by modifying the `script-src` directive.

### Detailed summary
- Updated the `script-src` directive to include `thirdweb.com` and `thirdweb-dev.com` without a wildcard prefix.
- Retained existing sources for `self`, `unsafe-eval`, `unsafe-inline`, `wasm-unsafe-eval`, `inline-speculation-rules`, `vercel.live`, and `js.stripe.com`.

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

<!-- end pr-codex -->
2025-03-31 17:43:57 +00:00
jnsdls a5f005fff0 Remove tutorial routes from framer rewrites (#6592)
closes: CORE-802

Redirect `/learn/tutorials` to `/learn/guides`

This PR removes `/learn/tutorials` and `/learn/tutorials/:tutorial_slug` from the framer-rewrites list and adds redirects from these paths to their corresponding `/learn/guides` equivalents.
2025-03-31 17:14:04 +00:00
saminacodes fb70cab6d4 [portal] added module warning (#6590)
CORE-0000

<!-- start pr-codex -->

---

## PR-Codex overview
This PR adds a warning callout to the `page.mdx` file in the `cross-chain/agglayer` directory, informing users that the Agglayer module is in beta and has deployment limitations.

### Detailed summary
- Added a warning callout indicating that the Agglayer module is in beta.
- Specified that deployments are limited to `Sepolia` and `Cardona` due to requirements for Ethereum as L1 and zkEVM as L2.

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

<!-- end pr-codex -->
2025-03-31 14:53:39 +00:00
Joaquim VergesandGitHub cd9ea69c13 [SDK] Add erc20Value for makeOffer extension function (#6588) 2025-03-31 00:24:42 -07:00
Joaquim VergesGitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
dc892bd136 Version Packages (#6585)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-03-30 16:14:40 -07:00
Joaquim VergesandGitHub 3d7c009d72 [CLI] Fix secret key handling (#6584) 2025-03-30 15:49:45 -07:00
YashandGitHub 6dbed0d4fc [SDK] fix: Use default contract params with optional overrides (#6581) 2025-03-30 15:45:13 -07:00
MananTank adee1903d5 [TOOL-3814] Dashboard: Show all active coupons in team billing page (#6568)
<!-- start pr-codex -->

## PR-Codex overview
This PR focuses on refactoring the coupon management system within the billing section of the dashboard. It introduces a new structure for handling coupons, including a client component for API interactions and UI components for displaying and applying coupons.

### Detailed summary
- Deleted obsolete files: `CouponCard.tsx`, `CouponDetails.stories.tsx`, `ApplyCouponCard.stories.tsx`.
- Updated global styles import in `preview.tsx`.
- Replaced `CouponSection` with `Coupons` in `index.tsx`.
- Introduced `CouponsClient` for managing coupon API interactions.
- Created `AsyncCoupons` to fetch coupon data asynchronously.
- Developed `Coupons` component to handle loading states and display.
- Added `CouponsUI` for rendering coupon information and actions.
- Implemented coupon application and deletion logic with error handling.
- Created a story for `CouponsUI` to demonstrate different states and interactions.

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

<!-- end pr-codex -->
2025-03-30 22:18:56 +00:00
gregandGitHub a1d785b8ed Fix: Increase commerce price to avoid minimum purchase errors (#6582) 2025-03-29 20:50:25 -07:00
707 changed files with 28395 additions and 19992 deletions
+17 -6
View File
@@ -16,6 +16,7 @@ env:
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
TW_SECRET_KEY: ${{ secrets.TW_SECRET_KEY }}
TW_CLIENT_ID: ${{ secrets.TW_CLIENT_ID }}
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
jobs:
@@ -60,7 +61,7 @@ jobs:
uses: ./.github/composite-actions/install
- name: Setup Biome
uses: biomejs/setup-biome@c016c38f26f2c4a6eb3662679143614a254263fd # v2.3.0
uses: biomejs/setup-biome@f382a98e582959e6aaac8e5f8b17b31749018780 # v2.5.0
with:
version: latest
@@ -102,7 +103,7 @@ jobs:
runs-on: ubuntu-latest-8
strategy:
matrix:
package_manager: [npm, yarn, pnpm, bun]
package_manager: [pnpm] # TODO, reenable [npm, yarn, pnpm, bun]
bundler: [vite, webpack, esbuild]
steps:
- name: Check out the code
@@ -123,7 +124,14 @@ jobs:
mkdir test-project
cd test-project
npm init -y
${{ matrix.package_manager }} add react react-dom ../packages/thirdweb
# Handle different package managers
if [ "${{ matrix.package_manager }}" = "pnpm" ]; then
# Create pnpm workspace
echo '{"name": "test-project", "private": true, "workspaces": ["."]}' > package.json
echo '{"packages": ["../packages/*"]}' > pnpm-workspace.yaml
pnpm add react react-dom ../packages/thirdweb -w
fi
- name: Create test file
run: |
cd test-project
@@ -133,7 +141,7 @@ jobs:
if: matrix.bundler == 'vite'
run: |
cd test-project
${{matrix.package_manager}} add vite
${{matrix.package_manager}} add vite -w
echo 'import { defineConfig } from "vite"; import {resolve} from "path"; export default defineConfig({ build: { lib: { entry: resolve(__dirname, "index.js"), name: "e2e_test" }, outDir: "dist" }});' > vite.config.js
npx vite build
@@ -141,7 +149,7 @@ jobs:
if: matrix.bundler == 'webpack'
run: |
cd test-project
${{matrix.package_manager}} add webpack webpack-cli
${{matrix.package_manager}} add webpack webpack-cli -w
echo 'const path = require("path"); module.exports = { mode: "production", entry: "./index.js", output: { path: path.resolve(__dirname, "dist"), filename: "bundle.js" }};' > webpack.config.js
npx webpack
@@ -149,7 +157,7 @@ jobs:
if: matrix.bundler == 'esbuild'
run: |
cd test-project
${{matrix.package_manager}} add esbuild
${{matrix.package_manager}} add esbuild -w
npx esbuild index.js --bundle --outdir=dist
- name: Verify bundle
@@ -177,6 +185,9 @@ jobs:
- name: Setup & Install
uses: ./.github/composite-actions/install
- name: Build Packages
run: pnpm build
- name: Report bundle size
uses: andresz1/size-limit-action@94bc357df29c36c8f8d50ea497c3e225c3c95d1d # v1.8.0
with:
+3
View File
@@ -0,0 +1,3 @@
public-hoist-pattern[]=*import-in-the-middle*
public-hoist-pattern[]=*require-in-the-middle*
public-hoist-pattern[]=*pino-pretty*
+9 -14
View File
@@ -1,5 +1,5 @@
import type { Preview } from "@storybook/react";
import "@/styles/globals.css";
import "../src/global.css";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { MoonIcon, SunIcon } from "lucide-react";
import { ThemeProvider, useTheme } from "next-themes";
@@ -81,23 +81,18 @@ function StoryLayout(props: {
return (
<QueryClientProvider client={queryClient}>
<div className="flex min-h-dvh min-w-0 flex-col bg-background text-foreground">
<div className="flex justify-end gap-2 border-b p-4">
<div className="fixed right-0 bottom-0 z-50 flex justify-end gap-2 p-4">
<Button
onClick={() => setTheme("dark")}
onClick={() => setTheme(theme === "dark" ? "light" : "dark")}
size="sm"
variant={theme === "dark" ? "default" : "outline"}
variant="outline"
className="h-auto w-auto shrink-0 rounded-full p-2"
>
<MoonIcon className="size-4" />
</Button>
<Button
onClick={() => setTheme("light")}
size="sm"
variant={theme === "light" ? "default" : "outline"}
className="h-auto w-auto shrink-0 rounded-full p-2"
>
<SunIcon className="size-4" />
{theme === "dark" ? (
<MoonIcon className="size-4" />
) : (
<SunIcon className="size-4" />
)}
</Button>
</div>
+3 -2
View File
@@ -49,12 +49,13 @@ module.exports = [
"/templates/:template_slug",
// -- learn --
"/learn",
"/learn/tutorials",
"/learn/tutorials/:tutorial_slug",
"/learn/guides",
"/learn/guides/:guide_slug",
"/learn/courses",
"/learn/glossary",
"/learn/glossary/:glossary_slug",
// -- faucets --
"/faucets",
// -- brand kit --
"/brand-kit",
];
+2 -1
View File
@@ -12,6 +12,7 @@
"ignoreDependencies": [
"@storybook/blocks",
"@thirdweb-dev/service-utils",
"@types/color"
"@types/color",
"fast-xml-parser"
]
}
+32 -10
View File
@@ -1,7 +1,9 @@
// @ts-check
// eslint-disable-next-line @typescript-eslint/no-var-requires
const { XMLParser } = require("fast-xml-parser");
/**
*
* @returns {Promise<Array<{chainId: number, name: string, slug: string}>>}
*/
async function fetchChainsFromApi() {
const res = await fetch("https://api.thirdweb.com/v1/chains", {
@@ -82,16 +84,20 @@ module.exports = {
};
},
additionalPaths: async (config) => {
// eslint-disable-next-line @typescript-eslint/no-var-requires
const FRAMER_PATHS = require("./framer-rewrites");
const allChains = await fetchChainsFromApi();
const [framerUrls, allChains] = await Promise.all([
getFramerXML(),
fetchChainsFromApi(),
]);
return [
...FRAMER_PATHS.map((path) => ({
loc: path,
changefreq: config.changefreq,
priority: config.priority,
lastmod: config.autoLastmod ? new Date().toISOString() : undefined,
})),
...framerUrls.map((url) => {
return {
loc: url.loc,
changefreq: config.changefreq,
priority: config.priority,
lastmod: config.autoLastmod ? new Date().toISOString() : undefined,
};
}),
...allChains.map((chain) => {
return {
loc: `/${chain.slug}`,
@@ -136,3 +142,19 @@ async function createSearchRecordSitemaps(config) {
// filter out any failed requests
return chainsForLines.filter(Boolean);
}
async function getFramerXML() {
const framerSiteMapText = await fetch(
"https://landing.thirdweb.com/sitemap.xml",
).then((res) => res.text());
const parser = new XMLParser();
const xmlObject = parser.parse(framerSiteMapText);
/**
* @type {Array<{loc: string}>}
*/
const urls = xmlObject.urlset.url;
return urls;
}
+1 -2
View File
@@ -12,7 +12,7 @@ const ContentSecurityPolicy = `
style-src 'self' 'unsafe-inline' vercel.live;
font-src 'self' vercel.live assets.vercel.com framerusercontent.com fonts.gstatic.com;
frame-src * data:;
script-src 'self' 'unsafe-eval' 'unsafe-inline' 'wasm-unsafe-eval' 'inline-speculation-rules' *.thirdweb.com *.thirdweb-dev.com vercel.live js.stripe.com framerusercontent.com events.framer.com challenges.cloudflare.com static.reo.dev;
script-src 'self' 'unsafe-eval' 'unsafe-inline' 'wasm-unsafe-eval' 'inline-speculation-rules' *.thirdweb.com *.thirdweb-dev.com vercel.live js.stripe.com framerusercontent.com events.framer.com challenges.cloudflare.com;
connect-src * data: blob:;
worker-src 'self' blob:;
block-all-mixed-content;
@@ -203,7 +203,6 @@ function getConfig(): NextConfig {
type: "memory",
});
}
config.externals.push("pino-pretty");
config.module = {
...config.module,
exprContextCritical: false,
+50 -48
View File
@@ -29,29 +29,29 @@
"@emotion/styled": "11.14.0",
"@hookform/resolvers": "^3.9.1",
"@marsidev/react-turnstile": "^1.1.0",
"@radix-ui/react-accordion": "^1.2.3",
"@radix-ui/react-alert-dialog": "^1.1.6",
"@radix-ui/react-avatar": "^1.1.3",
"@radix-ui/react-checkbox": "^1.1.4",
"@radix-ui/react-dialog": "1.1.6",
"@radix-ui/react-dropdown-menu": "^2.1.6",
"@radix-ui/react-hover-card": "^1.1.6",
"@radix-ui/react-label": "^2.1.2",
"@radix-ui/react-popover": "^1.1.6",
"@radix-ui/react-progress": "^1.1.2",
"@radix-ui/react-radio-group": "^1.2.3",
"@radix-ui/react-select": "^2.1.6",
"@radix-ui/react-separator": "^1.1.2",
"@radix-ui/react-slot": "^1.1.2",
"@radix-ui/react-switch": "^1.1.3",
"@radix-ui/react-tooltip": "1.1.8",
"@sentry/nextjs": "9.5.0",
"@shazow/whatsabi": "0.20.0",
"@tanstack/react-query": "5.67.3",
"@radix-ui/react-accordion": "^1.2.4",
"@radix-ui/react-alert-dialog": "^1.1.7",
"@radix-ui/react-avatar": "^1.1.4",
"@radix-ui/react-checkbox": "^1.1.5",
"@radix-ui/react-dialog": "1.1.7",
"@radix-ui/react-dropdown-menu": "^2.1.7",
"@radix-ui/react-hover-card": "^1.1.7",
"@radix-ui/react-label": "^2.1.3",
"@radix-ui/react-popover": "^1.1.7",
"@radix-ui/react-progress": "^1.1.3",
"@radix-ui/react-radio-group": "^1.2.4",
"@radix-ui/react-select": "^2.1.7",
"@radix-ui/react-separator": "^1.1.3",
"@radix-ui/react-slot": "^1.2.0",
"@radix-ui/react-switch": "^1.1.4",
"@radix-ui/react-tooltip": "1.2.0",
"@sentry/nextjs": "9.12.0",
"@shazow/whatsabi": "0.21.0",
"@tanstack/react-query": "5.72.1",
"@tanstack/react-table": "^8.21.2",
"@thirdweb-dev/service-utils": "workspace:*",
"@vercel/functions": "2.0.0",
"@vercel/og": "^0.6.5",
"@vercel/og": "^0.6.8",
"abitype": "1.0.8",
"chakra-react-select": "^4.7.6",
"class-variance-authority": "^0.7.1",
@@ -59,6 +59,7 @@
"color": "5.0.0",
"compare-versions": "^6.1.0",
"date-fns": "4.1.0",
"fast-xml-parser": "^5.2.0",
"fetch-event-stream": "0.1.5",
"flat": "^6.0.1",
"framer-motion": "12.5.0",
@@ -67,8 +68,8 @@
"input-otp": "^1.4.1",
"ioredis": "^5.6.0",
"ipaddr.js": "^2.2.0",
"lucide-react": "0.483.0",
"next": "15.2.3",
"lucide-react": "0.487.0",
"next": "15.2.4",
"next-plausible": "^3.12.4",
"next-themes": "^0.4.6",
"nextjs-toploader": "^1.6.12",
@@ -77,25 +78,26 @@
"papaparse": "^5.5.2",
"pluralize": "^8.0.0",
"posthog-js": "1.67.1",
"prettier": "3.5.3",
"qrcode": "^1.5.3",
"react": "19.0.0",
"react": "19.1.0",
"react-children-utilities": "^2.10.0",
"react-day-picker": "^8.10.1",
"react-dom": "19.0.0",
"react-dom": "19.1.0",
"react-dropzone": "^14.3.8",
"react-error-boundary": "^5.0.0",
"react-hook-form": "7.54.2",
"react-hook-form": "7.55.0",
"react-markdown": "^9.0.1",
"react-table": "^7.8.0",
"recharts": "2.15.1",
"recharts": "2.15.2",
"remark-gfm": "4.0.1",
"responsive-rsc": "0.0.7",
"server-only": "^0.0.1",
"shiki": "1.27.0",
"sonner": "2.0.1",
"sonner": "2.0.3",
"spdx-correct": "^3.2.0",
"stripe": "17.7.0",
"swagger-ui-react": "^5.20.1",
"swagger-ui-react": "^5.20.7",
"tailwind-merge": "^2.6.0",
"tailwindcss-animate": "^1.0.7",
"thirdweb": "workspace:*",
@@ -106,41 +108,41 @@
"devDependencies": {
"@chakra-ui/cli": "^2.4.1",
"@chromatic-com/storybook": "3.2.6",
"@next/bundle-analyzer": "15.2.3",
"@next/eslint-plugin-next": "15.2.3",
"@next/bundle-analyzer": "15.2.4",
"@next/eslint-plugin-next": "15.2.4",
"@playwright/test": "1.51.1",
"@storybook/addon-essentials": "8.6.4",
"@storybook/addon-interactions": "8.6.4",
"@storybook/addon-links": "8.6.4",
"@storybook/addon-onboarding": "8.6.4",
"@storybook/addon-viewport": "8.6.4",
"@storybook/blocks": "8.6.4",
"@storybook/nextjs": "8.6.4",
"@storybook/react": "8.6.4",
"@storybook/test": "8.6.4",
"@storybook/addon-essentials": "8.6.12",
"@storybook/addon-interactions": "8.6.12",
"@storybook/addon-links": "8.6.12",
"@storybook/addon-onboarding": "8.6.12",
"@storybook/addon-viewport": "8.6.12",
"@storybook/blocks": "8.6.12",
"@storybook/nextjs": "8.6.12",
"@storybook/react": "8.6.12",
"@storybook/test": "8.6.12",
"@types/color": "4.2.0",
"@types/node": "22.13.10",
"@types/node": "22.14.0",
"@types/papaparse": "^5.3.15",
"@types/pluralize": "^0.0.33",
"@types/qrcode": "^1.5.5",
"@types/react": "19.0.10",
"@types/react-dom": "19.0.4",
"@types/react": "19.1.0",
"@types/react-dom": "19.1.2",
"@types/react-table": "^7.7.20",
"@types/spdx-correct": "^3.1.3",
"@types/swagger-ui-react": "^4.19.0",
"@typescript-eslint/eslint-plugin": "7.14.1",
"@typescript-eslint/parser": "7.14.1",
"autoprefixer": "^10.4.21",
"checkly": "5.0.1",
"checkly": "5.2.0",
"eslint": "8.57.0",
"eslint-config-biome": "1.9.4",
"eslint-plugin-react-compiler": "19.0.0-beta-40c6c23-20250301",
"eslint-plugin-storybook": "0.11.6",
"knip": "5.46.0",
"eslint-plugin-react-compiler": "19.0.0-beta-e993439-20250405",
"eslint-plugin-storybook": "0.12.0",
"knip": "5.47.0",
"next-sitemap": "^4.2.3",
"postcss": "8.5.3",
"storybook": "8.6.4",
"storybook": "8.6.12",
"tailwindcss": "3.4.17",
"typescript": "5.8.2"
"typescript": "5.8.3"
}
}
+13
View File
@@ -361,6 +361,19 @@ async function redirects() {
"/team/:team_slug/:project_slug/connect/universal-bridge/:path*",
permanent: false,
},
// all /learn/tutorials (and sub-routes) -> /learn/guides
{
source: "/learn/tutorials/:path*",
destination: "/learn/guides/:path*",
permanent: false,
},
{
source: "/learn/tutorials",
destination: "/learn/guides",
permanent: false,
},
...legacyDashboardToTeamRedirects,
];
}
@@ -81,6 +81,7 @@ export async function getWalletNFTs(params: {
const result = await transformMoralisResponseToNFT(
await parsedResponse,
owner,
chainId,
);
return { result };
@@ -194,6 +195,8 @@ async function getWalletNFTsFromInsight(params: {
tokenURI: nft.metadata_url,
type: nft.token_type === "erc721" ? "ERC721" : "ERC1155",
supply: nft.balance,
tokenAddress: nft.contract.address,
chainId: nft.contract.chain_id,
};
return walletNFT;
@@ -1,40 +0,0 @@
"use server";
import { getAuthToken } from "../../app/api/lib/getAuthToken";
import { API_SERVER_URL } from "../constants/env";
export async function joinTeamWaitlist(options: {
teamSlug: string;
// currently only 'nebula' is supported
scope: "nebula";
}) {
const { teamSlug, scope } = options;
const token = await getAuthToken();
if (!token) {
return {
errorMessage: "You are not authorized to perform this action",
};
}
const res = await fetch(`${API_SERVER_URL}/v1/teams/${teamSlug}/waitlist`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({
scope,
}),
});
if (!res.ok) {
return {
errorMessage: "Failed to join waitlist",
};
}
return {
success: true,
};
}
@@ -23,7 +23,7 @@ function getStripe() {
export async function getTeamInvoices(
team: Team,
options?: { cursor?: string },
options?: { cursor?: string; status?: "open" },
) {
try {
const customerId = team.stripeCustomerId;
@@ -37,6 +37,8 @@ export async function getTeamInvoices(
customer: customerId,
limit: 10,
starting_after: options?.cursor,
// Only return open invoices if the status is open
status: options?.status,
});
return invoices;
+9 -5
View File
@@ -270,9 +270,10 @@ type ActiveStatus = {
ecosystemWallet: boolean;
};
export async function isProjectActive(
params: AnalyticsQueryParams,
): Promise<ActiveStatus> {
export async function isProjectActive(params: {
teamId: string;
projectId: string;
}): Promise<ActiveStatus> {
const searchParams = buildSearchParams(params);
const res = await fetchAnalytics(
`v2/active-usage?${searchParams.toString()}`,
@@ -345,7 +346,7 @@ export async function getEcosystemWalletUsage(args: {
searchParams.append("period", period);
}
const res = await fetchAnalytics(
`v2/wallets/connects/${ecosystemSlug}?${searchParams.toString()}`,
`v2/wallet/connects?${searchParams.toString()}`,
{
method: "GET",
headers: {
@@ -355,7 +356,10 @@ export async function getEcosystemWalletUsage(args: {
);
if (res?.status !== 200) {
console.error("Failed to fetch ecosystem wallet stats");
const reason = await res?.text();
console.error(
`Failed to fetch ecosystem wallet stats: ${res?.status} - ${res.statusText} - ${reason}`,
);
return null;
}
-28
View File
@@ -74,31 +74,3 @@ export async function getDefaultTeam() {
}
return null;
}
type TeamNebulaWaitList = {
onWaitlist: boolean;
createdAt: null | string;
};
export async function getTeamNebulaWaitList(teamSlug: string) {
const token = await getAuthToken();
if (!token) {
return null;
}
const res = await fetch(
`${API_SERVER_URL}/v1/teams/${teamSlug}/waitlist?scope=nebula`,
{
headers: {
Authorization: `Bearer ${token}`,
},
},
);
if (res.ok) {
return (await res.json()).result as TeamNebulaWaitList;
}
return null;
}
+113
View File
@@ -0,0 +1,113 @@
import "server-only";
import { unstable_cache } from "next/cache";
export type RPCUsageDataItem = {
date: string;
usageType: "included" | "overage" | "rate-limit";
count: string;
};
export const fetchRPCUsage = unstable_cache(
async (params: {
teamId: string;
projectId?: string;
authToken: string;
from: string;
to: string;
period: "day" | "week" | "month" | "year" | "all";
}) => {
const analyticsEndpoint = process.env.ANALYTICS_SERVICE_URL as string;
const url = new URL(`${analyticsEndpoint}/v2/rpc/usage-types`);
url.searchParams.set("teamId", params.teamId);
if (params.projectId) {
url.searchParams.set("projectId", params.projectId);
}
url.searchParams.set("from", params.from);
url.searchParams.set("to", params.to);
url.searchParams.set("period", params.period);
const res = await fetch(url, {
headers: {
Authorization: `Bearer ${params.authToken}`,
},
});
if (!res.ok) {
const error = await res.text();
return {
ok: false as const,
error: error,
};
}
const resData = await res.json();
return {
ok: true as const,
data: resData.data as RPCUsageDataItem[],
};
},
["rpc-usage"],
{
revalidate: 60 * 60, // 1 hour
},
);
type Last24HoursRPCUsageApiResponse = {
peakRate: {
date: string;
peakRPS: number;
};
averageRate: {
date: string;
averageRate: number;
includedCount: number;
rateLimitedCount: number;
overageCount: number;
}[];
totalCounts: {
includedCount: number;
rateLimitedCount: number;
overageCount: number;
};
};
export const getLast24HoursRPCUsage = unstable_cache(
async (params: {
teamId: string;
projectId?: string;
authToken: string;
}) => {
const analyticsEndpoint = process.env.ANALYTICS_SERVICE_URL as string;
const url = new URL(`${analyticsEndpoint}/v2/rpc/24-hours`);
url.searchParams.set("teamId", params.teamId);
if (params.projectId) {
url.searchParams.set("projectId", params.projectId);
}
const res = await fetch(url, {
headers: {
Authorization: `Bearer ${params.authToken}`,
},
});
if (!res.ok) {
const error = await res.text();
return {
ok: false as const,
error: error,
};
}
const resData = await res.json();
return {
ok: true as const,
data: resData.data as Last24HoursRPCUsageApiResponse,
};
},
["rpc-usage-last-24-hours"],
{
revalidate: 60, // 1 minute
},
);
+61 -26
View File
@@ -1,6 +1,8 @@
"use client";
import { useMutation } from "@tanstack/react-query";
import { AlertTriangleIcon } from "lucide-react";
import Link from "next/link";
import { toast } from "sonner";
import type {
GetBillingCheckoutUrlAction,
@@ -8,6 +10,7 @@ import type {
GetBillingPortalUrlAction,
GetBillingPortalUrlOptions,
} from "../actions/billing";
import type { Team } from "../api/team";
import { cn } from "../lib/utils";
import { Spinner } from "./ui/Spinner/Spinner";
import { Button, type ButtonProps } from "./ui/button";
@@ -16,6 +19,7 @@ type CheckoutButtonProps = Omit<GetBillingCheckoutUrlOptions, "redirectUrl"> & {
getBillingCheckoutUrl: GetBillingCheckoutUrlAction;
buttonProps?: Omit<ButtonProps, "children">;
children: React.ReactNode;
billingStatus: Team["billingStatus"];
};
export function CheckoutButton({
@@ -25,6 +29,7 @@ export function CheckoutButton({
getBillingCheckoutUrl,
children,
buttonProps,
billingStatus,
}: CheckoutButtonProps) {
const getUrlMutation = useMutation({
mutationFn: async () => {
@@ -40,35 +45,65 @@ export function CheckoutButton({
const errorMessage = "Failed to open checkout page";
return (
<Button
{...buttonProps}
className={cn(buttonProps?.className, "gap-2")}
disabled={getUrlMutation.isPending || buttonProps?.disabled}
onClick={async (e) => {
buttonProps?.onClick?.(e);
getUrlMutation.mutate(undefined, {
onSuccess: (res) => {
if (!res.url) {
toast.error(errorMessage);
return;
}
<div className="flex w-full flex-col items-center gap-2">
{/* show warning if the team has an invalid payment method */}
{billingStatus === "invalidPayment" && (
<BillingWarning teamSlug={teamSlug} />
)}
<Button
{...buttonProps}
className={cn(buttonProps?.className, "w-full gap-2")}
disabled={
// disable button if the team has an invalid payment method
// api will return 402 error if the team has an invalid payment method
billingStatus === "invalidPayment" ||
getUrlMutation.isPending ||
buttonProps?.disabled
}
onClick={async (e) => {
buttonProps?.onClick?.(e);
getUrlMutation.mutate(undefined, {
onSuccess: (res) => {
if (!res.url) {
toast.error(errorMessage);
return;
}
const tab = window.open(res.url, "_blank");
const tab = window.open(res.url, "_blank");
if (!tab) {
if (!tab) {
toast.error(errorMessage);
return;
}
},
onError: () => {
toast.error(errorMessage);
return;
}
},
onError: () => {
toast.error(errorMessage);
},
});
}}
>
{getUrlMutation.isPending && <Spinner className="size-4" />}
{children}
</Button>
},
});
}}
>
{getUrlMutation.isPending && <Spinner className="size-4" />}
{children}
</Button>
</div>
);
}
function BillingWarning({ teamSlug }: { teamSlug: string }) {
return (
<div className="flex items-center gap-2 rounded-md border border-amber-200 bg-amber-50 p-3 text-amber-800 dark:border-amber-700/50 dark:bg-amber-900/30 dark:text-amber-400">
<AlertTriangleIcon className="h-5 w-5 flex-shrink-0" />
<p className="text-sm">
You have outstanding invoices. Please{" "}
<Link
href={`/team/${teamSlug}/~/settings/invoices?status=open`}
className="font-medium text-amber-700 underline transition-colors hover:text-amber-900 dark:text-amber-400 dark:hover:text-amber-300"
>
pay them
</Link>{" "}
to continue.
</p>
</div>
);
}
@@ -8,13 +8,12 @@ import { GradientAvatar } from "./GradientAvatar";
const meta = {
title: "blocks/Avatars/GradientAvatar",
component: Story,
parameters: {},
} satisfies Meta<typeof Story>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Desktop: Story = {
export const Variants: Story = {
args: {},
};
@@ -22,14 +21,14 @@ const client = getThirdwebClient();
function Story() {
return (
<div className="flex flex-col gap-10 p-10">
<p> All images below are set with size-6 className </p>
<div className="container flex max-w-6xl flex-col gap-10 py-10">
<p> All images below are set with size-20 className </p>
<BadgeContainer label="No Src, No id - Skeleton">
<GradientAvatar
id={undefined}
src={undefined}
className="size-6"
className="size-20"
client={client}
/>
</BadgeContainer>
@@ -38,7 +37,7 @@ function Story() {
<GradientAvatar
id={"foo"}
src={undefined}
className="size-6"
className="size-20"
client={client}
/>
</BadgeContainer>
@@ -47,7 +46,7 @@ function Story() {
<GradientAvatar
id={"foo"}
src={""}
className="size-6"
className="size-20"
client={client}
/>
</BadgeContainer>
@@ -56,7 +55,7 @@ function Story() {
<GradientAvatar
id={"bar"}
src={""}
className="size-6"
className="size-20"
client={client}
/>
</BadgeContainer>
@@ -65,7 +64,7 @@ function Story() {
<GradientAvatar
src="invalid-src"
id={undefined}
className="size-6"
className="size-20"
client={client}
/>
</BadgeContainer>
@@ -74,7 +73,7 @@ function Story() {
<GradientAvatar
src="https://picsum.photos/200/300"
id={undefined}
className="size-6"
className="size-20"
client={client}
/>
</BadgeContainer>
@@ -83,7 +82,7 @@ function Story() {
<GradientAvatar
src="ipfs://QmZbeJYEs7kCJHyQxjxU2SJUtjSAr4m87wzJFJUyWomKdj/Smily.svg"
id={undefined}
className="size-6"
className="size-20"
client={client}
/>
</BadgeContainer>
@@ -123,14 +122,14 @@ function ToggleTest() {
<GradientAvatar
src={data?.src}
id={data?.id}
className="size-6"
className="size-20"
client={client}
/>
</BadgeContainer>
<BadgeContainer label="invalid Src">
<GradientAvatar
className="size-6"
className="size-20"
src={data ? "invalid-src" : undefined}
id={undefined}
client={client}
@@ -14,7 +14,7 @@ const meta = {
export default meta;
type Story = StoryObj<typeof meta>;
export const Desktop: Story = {
export const Variants: Story = {
args: {},
};
@@ -22,7 +22,7 @@ const client = getThirdwebClient();
function Story() {
return (
<div className="flex flex-col gap-10 p-10">
<div className="container flex max-w-6xl flex-col gap-10 py-10">
<p> All images below are set with size-6 className </p>
<BadgeContainer label="No Src - Skeleton">
@@ -1,5 +1,5 @@
import type { Meta, StoryObj } from "@storybook/react";
import { BadgeContainer, mobileViewport } from "../../../stories/utils";
import { BadgeContainer } from "../../../stories/utils";
import { DangerSettingCard } from "./DangerSettingCard";
const meta = {
@@ -15,17 +15,10 @@ const meta = {
export default meta;
type Story = StoryObj<typeof meta>;
export const Desktop: Story = {
export const Variants: Story = {
args: {},
};
export const Mobile: Story = {
args: {},
parameters: {
viewport: mobileViewport("iphone14"),
},
};
function Story() {
return (
<div className="container flex max-w-[1000px] flex-col gap-8 py-10">
@@ -9,13 +9,12 @@ import { Img } from "./Img";
const meta = {
title: "blocks/Img",
component: Story,
parameters: {},
} satisfies Meta<typeof Story>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Desktop: Story = {
export const Variants: Story = {
args: {},
};
@@ -1,6 +1,6 @@
import type { Meta, StoryObj } from "@storybook/react";
import { useState } from "react";
import { BadgeContainer, mobileViewport } from "../../../stories/utils";
import { BadgeContainer } from "../../../stories/utils";
import { MultiNetworkSelector } from "./NetworkSelectors";
const meta = {
@@ -16,20 +16,13 @@ const meta = {
export default meta;
type Story = StoryObj<typeof meta>;
export const Desktop: Story = {
export const Variants: Story = {
args: {},
};
export const Mobile: Story = {
args: {},
parameters: {
viewport: mobileViewport("iphone14"),
},
};
function Story() {
return (
<div className="container flex max-w-[1000px] flex-col gap-8 lg:p-10">
<div className="container flex max-w-6xl flex-col gap-8 py-10">
<Variant label="No Chains selected by default" selectedChainIds={[]} />
<Variant
label="Polygon, Ethereum selected by default"
@@ -4,7 +4,7 @@ import { MultiSelect } from "@/components/blocks/multi-select";
import { SelectWithSearch } from "@/components/blocks/select-with-search";
import { Badge } from "@/components/ui/badge";
import { useCallback, useMemo } from "react";
import { ChainIcon } from "../../../components/icons/ChainIcon";
import { ChainIconClient } from "../../../components/icons/ChainIcon";
import { useAllChainsData } from "../../../hooks/chains/allChains";
function cleanChainName(chainName: string) {
@@ -19,6 +19,7 @@ export function MultiNetworkSelector(props: {
disableChainId?: boolean;
className?: string;
priorityChains?: number[];
hideTestnets?: boolean;
}) {
const { allChains, idToChain } = useAllChainsData();
@@ -44,13 +45,17 @@ export function MultiNetworkSelector(props: {
sortedChains = [...priorityChains, ...otherChains];
}
if (props.hideTestnets) {
sortedChains = sortedChains.filter((chain) => !chain.testnet);
}
return sortedChains.map((chain) => {
return {
label: cleanChainName(chain.name),
value: String(chain.chainId),
};
});
}, [allChains, props.priorityChains, idToChain]);
}, [allChains, props.priorityChains, idToChain, props.hideTestnets]);
const searchFn = useCallback(
(option: Option, searchValue: string) => {
@@ -77,7 +82,7 @@ export function MultiNetworkSelector(props: {
return (
<div className="flex justify-between gap-4">
<span className="flex grow gap-2 truncate text-left">
<ChainIcon
<ChainIconClient
className="size-5"
ipfsSrc={chain.icon?.url}
loading="lazy"
@@ -172,7 +177,7 @@ export function SingleNetworkSelector(props: {
return (
<div className="flex justify-between gap-4">
<span className="flex grow gap-2 truncate text-left">
<ChainIcon
<ChainIconClient
className="size-5"
ipfsSrc={chain.icon?.url}
loading="lazy"
@@ -1,5 +1,5 @@
import type { Meta, StoryObj } from "@storybook/react";
import { BadgeContainer, mobileViewport } from "../../../stories/utils";
import { BadgeContainer } from "../../../stories/utils";
import { SettingsCard } from "./SettingsCard";
const meta = {
@@ -15,139 +15,130 @@ const meta = {
export default meta;
type Story = StoryObj<typeof meta>;
export const Desktop: Story = {
export const Variants: Story = {
args: {},
};
export const Mobile: Story = {
args: {},
parameters: {
viewport: mobileViewport("iphone14"),
},
};
function Story() {
return (
<div className="min-h-dvh bg-background py-6 text-foreground">
<div className="container flex max-w-[1100px] flex-col gap-10 lg:p-10">
<BadgeContainer label="No Header">
<SettingsCard
bottomText="This is bottom text"
header={undefined}
errorText={undefined}
noPermissionText={undefined}
saveButton={undefined}
>
<ChildrenPlaceholder />
</SettingsCard>
</BadgeContainer>
<div className="container flex max-w-6xl flex-col gap-10 py-10">
<BadgeContainer label="No Header">
<SettingsCard
bottomText="This is bottom text"
header={undefined}
errorText={undefined}
noPermissionText={undefined}
saveButton={undefined}
>
<ChildrenPlaceholder />
</SettingsCard>
</BadgeContainer>
<BadgeContainer label="Header">
<SettingsCard
bottomText="This is bottom text"
header={{
title: "Some Title",
description: "This is some description for this card",
}}
errorText={undefined}
noPermissionText={undefined}
saveButton={undefined}
>
<ChildrenPlaceholder />
</SettingsCard>
</BadgeContainer>
<BadgeContainer label="Header">
<SettingsCard
bottomText="This is bottom text"
header={{
title: "Some Title",
description: "This is some description for this card",
}}
errorText={undefined}
noPermissionText={undefined}
saveButton={undefined}
>
<ChildrenPlaceholder />
</SettingsCard>
</BadgeContainer>
<BadgeContainer label="Error">
<SettingsCard
bottomText="This is bottom text"
header={{
title: "Some Title",
description: "This is some description for this card",
}}
errorText="This is error text"
noPermissionText={undefined}
saveButton={undefined}
>
<ChildrenPlaceholder />
</SettingsCard>
</BadgeContainer>
<BadgeContainer label="Error">
<SettingsCard
bottomText="This is bottom text"
header={{
title: "Some Title",
description: "This is some description for this card",
}}
errorText="This is error text"
noPermissionText={undefined}
saveButton={undefined}
>
<ChildrenPlaceholder />
</SettingsCard>
</BadgeContainer>
<BadgeContainer label="No Permission">
<SettingsCard
bottomText="This is bottom text"
header={{
title: "Some Title",
description: "This is some description for this card",
}}
saveButton={{
disabled: false,
isPending: false,
onClick: () => {},
}}
noPermissionText="You do not have permission to edit this"
errorText={undefined}
>
<ChildrenPlaceholder />
</SettingsCard>
</BadgeContainer>
<BadgeContainer label="No Permission">
<SettingsCard
bottomText="This is bottom text"
header={{
title: "Some Title",
description: "This is some description for this card",
}}
saveButton={{
disabled: false,
isPending: false,
onClick: () => {},
}}
noPermissionText="You do not have permission to edit this"
errorText={undefined}
>
<ChildrenPlaceholder />
</SettingsCard>
</BadgeContainer>
<BadgeContainer label="Save">
<SettingsCard
bottomText="This is bottom text"
header={{
title: "Some Title",
description: "This is some description for this card",
}}
saveButton={{
disabled: false,
isPending: false,
onClick: () => {},
}}
noPermissionText={undefined}
errorText={undefined}
>
<ChildrenPlaceholder />
</SettingsCard>
</BadgeContainer>
<BadgeContainer label="Save">
<SettingsCard
bottomText="This is bottom text"
header={{
title: "Some Title",
description: "This is some description for this card",
}}
saveButton={{
disabled: false,
isPending: false,
onClick: () => {},
}}
noPermissionText={undefined}
errorText={undefined}
>
<ChildrenPlaceholder />
</SettingsCard>
</BadgeContainer>
<BadgeContainer label="Save Disabled">
<SettingsCard
bottomText="This is bottom text"
header={{
title: "Some Title",
description: "This is some description for this card",
}}
saveButton={{
disabled: true,
isPending: false,
onClick: () => {},
}}
noPermissionText={undefined}
errorText={undefined}
>
<ChildrenPlaceholder />
</SettingsCard>
</BadgeContainer>
<BadgeContainer label="Save Disabled">
<SettingsCard
bottomText="This is bottom text"
header={{
title: "Some Title",
description: "This is some description for this card",
}}
saveButton={{
disabled: true,
isPending: false,
onClick: () => {},
}}
noPermissionText={undefined}
errorText={undefined}
>
<ChildrenPlaceholder />
</SettingsCard>
</BadgeContainer>
<BadgeContainer label="Save Loading">
<SettingsCard
bottomText="This is bottom text"
header={{
title: "Some Title",
description: "This is some description for this card",
}}
saveButton={{
disabled: false,
isPending: true,
onClick: () => {},
}}
noPermissionText={undefined}
errorText={undefined}
>
<ChildrenPlaceholder />
</SettingsCard>
</BadgeContainer>
</div>
<BadgeContainer label="Save Loading">
<SettingsCard
bottomText="This is bottom text"
header={{
title: "Some Title",
description: "This is some description for this card",
}}
saveButton={{
disabled: false,
isPending: true,
onClick: () => {},
}}
noPermissionText={undefined}
errorText={undefined}
>
<ChildrenPlaceholder />
</SettingsCard>
</BadgeContainer>
</div>
);
}
@@ -1,5 +1,4 @@
import type { Meta, StoryObj } from "@storybook/react";
import { mobileViewport } from "../../../stories/utils";
import { SidebarLayout } from "./SidebarLayout";
const meta = {
@@ -15,17 +14,10 @@ const meta = {
export default meta;
type Story = StoryObj<typeof meta>;
export const Desktop: Story = {
export const Variants: Story = {
args: {},
};
export const Mobile: Story = {
args: {},
parameters: {
viewport: mobileViewport("iphone14"),
},
};
function Story() {
return (
<SidebarLayout
@@ -17,6 +17,7 @@ import { NavLink } from "../ui/NavLink";
import { Separator } from "../ui/separator";
import { MobileSidebar, useActiveSidebarLink } from "./MobileSidebar";
import { CustomSidebar, type SidebarLink } from "./Sidebar";
import { AppFooter } from "./app-footer";
export function SidebarLayout(props: {
sidebarLinks: SidebarLink[];
@@ -54,7 +55,6 @@ export function FullWidthSidebarLayout(props: {
footerSidebarLinks?: SidebarLink[];
children: React.ReactNode;
className?: string;
footer?: React.ReactNode;
}) {
const { contentSidebarLinks, children, footerSidebarLinks } = props;
return (
@@ -65,7 +65,7 @@ export function FullWidthSidebarLayout(props: {
)}
>
{/* left - sidebar */}
<Sidebar collapsible="icon" side="left">
<Sidebar collapsible="icon" side="left" className="pt-2">
<SidebarContent>
<SidebarGroup>
<SidebarGroupContent>
@@ -95,10 +95,10 @@ export function FullWidthSidebarLayout(props: {
links={[...contentSidebarLinks, ...(footerSidebarLinks || [])]}
/>
<main className="container z-0 flex min-w-0 max-w-[1280px] grow flex-col pb-20 max-sm:w-full lg:pt-6">
<main className="z-0 flex min-w-0 grow flex-col max-sm:w-full">
{children}
</main>
{props.footer}
<AppFooter containerClassName="max-w-7xl" />
</div>
</div>
);
@@ -156,7 +156,7 @@ function MobileSidebarTrigger(props: { links: SidebarLink[] }) {
const activeLink = useActiveSidebarLink(props.links);
return (
<div className="mb-4 flex items-center gap-3 border-b px-4 py-4 lg:hidden">
<div className="flex items-center gap-3 border-b px-4 py-4 lg:hidden">
<SidebarTrigger className="size-4" />
<Separator orientation="vertical" className="h-4" />
{activeLink && <span className="text-sm">{activeLink.label}</span>}
@@ -1,6 +1,6 @@
import type { Meta, StoryObj } from "@storybook/react";
import { useState } from "react";
import { BadgeContainer, mobileViewport } from "../../../stories/utils";
import { BadgeContainer } from "../../../stories/utils";
import { SingleNetworkSelector } from "./NetworkSelectors";
const meta = {
@@ -16,20 +16,13 @@ const meta = {
export default meta;
type Story = StoryObj<typeof meta>;
export const Desktop: Story = {
export const Variants: Story = {
args: {},
};
export const Mobile: Story = {
args: {},
parameters: {
viewport: mobileViewport("iphone14"),
},
};
function Story() {
return (
<div className="container flex max-w-[1000px] flex-col gap-8 lg:p-10">
<div className="container flex max-w-6xl flex-col gap-8 py-10">
<Variant label="No Chain ID selected by default" chainId={undefined} />
<Variant label="Polygon selected by default" chainId={137} />
<Variant
@@ -12,17 +12,54 @@ import Link from "next/link";
type AppFooterProps = {
className?: string;
containerClassName?: string;
};
const footerLinks = [
{
href: "/home",
label: "Home",
},
{
href: "https://blog.thirdweb.com",
label: "Blog",
},
{
href: "https://portal.thirdweb.com/changelog",
label: "Changelog",
},
{
href: "https://feedback.thirdweb.com/",
label: "Feedback",
},
{
href: "https://thirdweb.com/privacy-policy",
label: "Privacy Policy",
},
{
href: "https://thirdweb.com/terms",
label: "Terms of Service",
},
{
href: "https://thirdweb.com/chainlist",
label: "Chainlist",
},
];
export function AppFooter(props: AppFooterProps) {
return (
<footer
className={cn(
"w-full border-border border-t bg-card py-6 md:py-8",
"w-full border-border border-t py-6 md:py-8",
props.className,
)}
>
<div className="container flex flex-col gap-4 md:gap-6">
<div
className={cn(
"container flex flex-col gap-4 md:gap-6",
props.containerClassName,
)}
>
{/* top row */}
<div className="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
<div className="flex flex-row items-center gap-3">
@@ -74,57 +111,28 @@ export function AppFooter(props: AppFooterProps) {
</div>
{/* bottom row */}
<div className="grid grid-flow-col grid-cols-2 grid-rows-5 gap-2 md:flex md:flex-row md:justify-between">
<Link
className="px-[10px] py-[6px] text-muted-foreground text-sm hover:underline"
href="/home"
>
Home
</Link>
<Link
className="px-[10px] py-[6px] text-muted-foreground text-sm hover:underline"
href="https://blog.thirdweb.com"
target="_blank"
>
Blog
</Link>
<Link
className="px-[10px] py-[6px] text-muted-foreground text-sm hover:underline"
href="https://portal.thirdweb.com/changelog"
target="_blank"
>
Changelog
</Link>
<Link
className="px-[10px] py-[6px] text-muted-foreground text-sm hover:underline"
href="https://feedback.thirdweb.com/"
target="_blank"
>
Feedback
</Link>
<Link
className="px-[10px] py-[6px] text-muted-foreground text-sm hover:underline"
href="https://thirdweb.com/privacy-policy"
target="_blank"
>
Privacy Policy
</Link>
<Link
className="px-[10px] py-[6px] text-muted-foreground text-sm hover:underline"
href="https://thirdweb.com/terms"
target="_blank"
>
Terms of Service
</Link>
<Link
className="px-[10px] py-[6px] text-muted-foreground text-sm hover:underline"
href="https://thirdweb.com/chainlist"
target="_blank"
>
Chain List
</Link>
{footerLinks.map((link) => (
<FooterLink key={link.href} {...link} />
))}
</div>
</div>
</footer>
);
}
function FooterLink(props: {
href: string;
label: string;
prefetch?: boolean;
}) {
return (
<Link
href={props.href}
prefetch={false}
className="px-0 py-[6px] text-muted-foreground text-sm hover:underline"
target="_blank"
>
{props.label}
</Link>
);
}
@@ -4,6 +4,7 @@ import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/components/ui/card";
@@ -17,7 +18,7 @@ import {
} from "@/components/ui/chart";
import { formatDate } from "date-fns";
import { useMemo } from "react";
import { Area, AreaChart, CartesianGrid, XAxis } from "recharts";
import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from "recharts";
import {
EmptyChartState,
LoadingChartState,
@@ -30,10 +31,17 @@ type ThirdwebAreaChartProps<TConfig extends ChartConfig> = {
description?: string;
titleClassName?: string;
};
footer?: React.ReactNode;
customHeader?: React.ReactNode;
// chart config
config: TConfig;
data: Array<Record<keyof TConfig, number> & { time: number | string | Date }>;
showLegend?: boolean;
maxLimit?: number;
yAxis?: boolean;
xAxis?: {
sameDay?: boolean;
};
// chart className
chartClassName?: string;
@@ -60,6 +68,8 @@ export function ThirdwebAreaChart<TConfig extends ChartConfig>(
</CardHeader>
)}
{props.customHeader && props.customHeader}
<CardContent className={cn(!props.header && "pt-6")}>
<ChartContainer config={props.config} className={props.chartClassName}>
{props.isPending ? (
@@ -67,17 +77,33 @@ export function ThirdwebAreaChart<TConfig extends ChartConfig>(
) : props.data.length === 0 ? (
<EmptyChartState />
) : (
<AreaChart accessibilityLayer data={props.data}>
<AreaChart
accessibilityLayer
data={
props.maxLimit
? props.data.map((d) => ({
...d,
maxLimit: props.maxLimit,
}))
: props.data
}
>
<CartesianGrid vertical={false} />
{props.yAxis && <YAxis tickLine={false} axisLine={false} />}
<XAxis
dataKey="time"
tickLine={false}
axisLine={false}
tickMargin={20}
tickFormatter={(value) => formatDate(new Date(value), "MMM dd")}
tickFormatter={(value) =>
formatDate(
new Date(value),
props.xAxis?.sameDay ? "MMM dd, HH:mm" : "MMM dd",
)
}
/>
<ChartTooltip
cursor={false}
cursor={true}
content={
<ChartTooltipContent
hideLabel={
@@ -121,6 +147,16 @@ export function ThirdwebAreaChart<TConfig extends ChartConfig>(
stackId="a"
/>
))}
{props.maxLimit && (
<Area
type="monotone"
dataKey="maxLimit"
stroke="#ef4444"
strokeWidth={2}
strokeDasharray="5 5"
fill="none"
/>
)}
{props.showLegend && (
<ChartLegend
@@ -131,6 +167,9 @@ export function ThirdwebAreaChart<TConfig extends ChartConfig>(
)}
</ChartContainer>
</CardContent>
{props.footer && (
<CardFooter className="w-full">{props.footer}</CardFooter>
)}
</Card>
);
}
@@ -32,6 +32,7 @@ type ThirdwebBarChartProps<TConfig extends ChartConfig> = {
description?: string;
titleClassName?: string;
};
customHeader?: React.ReactNode;
// chart config
config: TConfig;
data: Array<Record<keyof TConfig, number> & { time: number | string | Date }>;
@@ -41,7 +42,9 @@ type ThirdwebBarChartProps<TConfig extends ChartConfig> = {
chartClassName?: string;
isPending: boolean;
toolTipLabelFormatter?: (label: string, payload: unknown) => React.ReactNode;
toolTipValueFormatter?: (value: unknown) => React.ReactNode;
hideLabel?: boolean;
emptyChartState?: React.ReactElement;
};
export function ThirdwebBarChart<TConfig extends ChartConfig>(
@@ -65,12 +68,14 @@ export function ThirdwebBarChart<TConfig extends ChartConfig>(
</CardHeader>
)}
{props.customHeader && props.customHeader}
<CardContent className={cn(!props.header && "pt-6")}>
<ChartContainer config={props.config} className={props.chartClassName}>
{props.isPending ? (
<LoadingChartState />
) : props.data.length === 0 ? (
<EmptyChartState />
<EmptyChartState>{props.emptyChartState}</EmptyChartState>
) : (
<BarChart accessibilityLayer data={props.data}>
<CartesianGrid vertical={false} />
@@ -88,6 +93,7 @@ export function ThirdwebBarChart<TConfig extends ChartConfig>(
props.hideLabel !== undefined ? props.hideLabel : true
}
labelFormatter={props.toolTipLabelFormatter}
valueFormatter={props.toolTipValueFormatter}
/>
}
/>
@@ -96,25 +102,15 @@ export function ThirdwebBarChart<TConfig extends ChartConfig>(
content={<ChartLegendContent className="pt-5" />}
/>
)}
{configKeys.map((key, idx) => (
{configKeys.map((key) => (
<Bar
key={key}
dataKey={key}
// if stacked then they should all be the same stackId
// if grouped then they should all be unique stackId (so the key works great)
stackId={variant === "stacked" ? "a" : key}
fill={`var(--color-${key})`}
// if stacked then we need to figure out the radius based on the index in the array
// if grouped then we can just use the same radius for all
radius={
variant === "stacked"
? idx === 0
? [0, 0, 4, 4]
: idx === configKeys.length - 1
? [4, 4, 0, 0]
: [0, 0, 0, 0]
: [4, 4, 4, 4]
}
fill={props.config[key]?.color}
radius={[4, 4, 4, 4]}
strokeWidth={1}
className="stroke-background"
/>
@@ -8,7 +8,7 @@ import {
} from "@/components/ui/select";
import type { Meta, StoryObj } from "@storybook/react";
import { useState } from "react";
import { BadgeContainer, mobileViewport } from "../../../stories/utils";
import { BadgeContainer } from "../../../stories/utils";
import { type CodeEnvironment, CodeSegment } from "./code-segment.client";
const meta = {
@@ -24,17 +24,10 @@ const meta = {
export default meta;
type Story = StoryObj<typeof meta>;
export const Desktop: Story = {
export const Variants: Story = {
args: {},
};
export const Mobile: Story = {
args: {},
parameters: {
viewport: mobileViewport("iphone14"),
},
};
type Mode = "default" | "no-tabs" | "only-tabs";
function Story() {
@@ -1,6 +1,6 @@
import type { Meta, StoryObj } from "@storybook/react";
import { useMemo, useState } from "react";
import { BadgeContainer, mobileViewport } from "../../../stories/utils";
import { BadgeContainer } from "../../../stories/utils";
import { MultiSelect } from "./multi-select";
const meta = {
@@ -16,17 +16,10 @@ const meta = {
export default meta;
type Story = StoryObj<typeof meta>;
export const Desktop: Story = {
export const Variants: Story = {
args: {},
};
export const Mobile: Story = {
args: {},
parameters: {
viewport: mobileViewport("iphone14"),
},
};
function createList(len: number) {
return Array.from({ length: len }, (_, i) => ({
value: `${i}`,
@@ -36,7 +29,7 @@ function createList(len: number) {
function Story() {
return (
<div className="mx-auto flex w-full max-w-[600px] flex-col gap-6 px-4 py-6">
<div className="container flex max-w-6xl flex-col gap-6 py-10">
<VariantTest defaultValues={[]} storyLabel="5 items" listLen={5} />
<VariantTest defaultValues={[]} storyLabel="5000 items" listLen={5000} />
<VariantTest
@@ -3,33 +3,36 @@ import type { Team } from "@/api/team";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { ToolTipLabel } from "@/components/ui/tooltip";
import { TrackedLinkTW } from "@/components/ui/tracked-link";
import { cn } from "@/lib/utils";
import { CheckIcon, CircleAlertIcon, CircleDollarSignIcon } from "lucide-react";
import { CheckIcon, CircleDollarSignIcon } from "lucide-react";
import Link from "next/link";
import type React from "react";
import { TEAM_PLANS } from "utils/pricing";
import { useTrack } from "../../../hooks/analytics/useTrack";
import { remainingDays } from "../../../utils/date-utils";
import type { GetBillingCheckoutUrlAction } from "../../actions/billing";
import type { ProductSKU } from "../../lib/billing";
import { CheckoutButton } from "../billing";
type ButtonProps = React.ComponentProps<typeof Button>;
const PRO_CONTACT_US_URL =
"https://meetings.hubspot.com/sales-thirdweb/thirdweb-pro";
type PricingCardCta = {
hint?: string;
title: string;
onClick?: () => void;
} & (
| {
type: "link";
href: string;
}
| {
type: "checkout";
}
);
type PricingCardProps = {
teamSlug: string;
billingPlan: Exclude<Team["billingPlan"], "free">;
cta?: {
hint?: string;
title: string;
tracking: {
category: string;
label?: string;
};
variant?: ButtonProps["variant"];
onClick?: () => void;
};
billingStatus: Team["billingStatus"];
billingPlan: keyof typeof TEAM_PLANS;
cta?: PricingCardCta;
ctaHint?: string;
highlighted?: boolean;
current?: boolean;
@@ -39,6 +42,7 @@ type PricingCardProps = {
export const PricingCard: React.FC<PricingCardProps> = ({
teamSlug,
billingStatus,
billingPlan,
cta,
highlighted = false,
@@ -49,13 +53,23 @@ export const PricingCard: React.FC<PricingCardProps> = ({
const plan = TEAM_PLANS[billingPlan];
const isCustomPrice = typeof plan.price === "string";
const trackEvent = useTrack();
const remainingTrialDays =
(activeTrialEndsAt ? remainingDays(activeTrialEndsAt) : 0) || 0;
const handleCTAClick = () => {
cta?.onClick?.();
trackEvent({
category: "account",
label: `${billingPlan}Plan`,
action: "click",
});
};
return (
<div
className={cn(
"z-10 flex w-full flex-col gap-6 rounded-xl border border-border bg-card p-4 md:p-6",
"z-10 flex w-full flex-col gap-4 rounded-xl border border-border bg-card p-4",
current && "border-blue-500",
highlighted && "border-active-border",
)}
@@ -71,13 +85,13 @@ export const PricingCard: React.FC<PricingCardProps> = ({
<div className="flex flex-col gap-5">
{/* Title + Desc */}
<div>
<div className="mb-2 flex flex-row items-center gap-2">
<div className="mb-1 flex flex-row items-center gap-3">
<h3 className="font-semibold text-2xl capitalize tracking-tight">
{plan.title}
</h3>
{current && <Badge className="capitalize">Current plan</Badge>}
</div>
<p className="max-w-[320px] text-muted-foreground">
<p className="max-w-[320px] text-muted-foreground text-sm">
{plan.description}
</p>
</div>
@@ -85,30 +99,13 @@ export const PricingCard: React.FC<PricingCardProps> = ({
{/* Price */}
<div className="flex flex-col gap-0.5">
<div className="flex items-center gap-2">
<span className="font-semibold text-3xl text-foreground tracking-tight">
<span className="font-semibold text-2xl text-foreground tracking-tight">
${plan.price}
</span>
{!isCustomPrice && (
<span className="text-muted-foreground">/ month</span>
)}
{billingPlan === "starter" && (
<ToolTipLabel
contentClassName="max-w-[320px]"
label="We will place a temporary hold of $25 to verify your card, this will be immediately released back to you after verification."
>
<Button
asChild
variant="ghost"
className="h-auto w-auto p-1 text-muted-foreground hover:text-foreground"
>
<div>
<CircleAlertIcon className="size-5 shrink-0" />
</div>
</Button>
</ToolTipLabel>
)}
</div>
{remainingTrialDays > 0 && (
@@ -124,7 +121,7 @@ export const PricingCard: React.FC<PricingCardProps> = ({
<div className="flex grow flex-col items-start gap-2 text-foreground">
{plan.subTitle && (
<p className="font-medium text-foreground">{plan.subTitle}</p>
<p className="font-medium text-foreground text-sm">{plan.subTitle}</p>
)}
{plan.features.map((f) => (
@@ -134,29 +131,31 @@ export const PricingCard: React.FC<PricingCardProps> = ({
{cta && (
<div className="flex flex-col gap-3">
{billingPlan !== "pro" ? (
{billingPlanToSkuMap[billingPlan] && cta.type === "checkout" && (
<CheckoutButton
billingStatus={billingStatus}
buttonProps={{
variant: cta.variant || "outline",
className: "gap-2",
onClick: cta.onClick,
variant: highlighted ? "default" : "outline",
className: highlighted ? undefined : "bg-background",
onClick: handleCTAClick,
}}
teamSlug={teamSlug}
sku={billingPlan === "starter" ? "plan:starter" : "plan:growth"}
sku={billingPlanToSkuMap[billingPlan]}
getBillingCheckoutUrl={getBillingCheckoutUrl}
>
{cta.title}
</CheckoutButton>
) : (
<Button variant={cta.variant || "outline"} asChild>
<TrackedLinkTW
href={PRO_CONTACT_US_URL}
label={cta.tracking?.label}
category={cta.tracking?.category}
target="_blank"
>
)}
{cta.type === "link" && (
<Button
variant={highlighted ? "default" : "outline"}
className={highlighted ? undefined : "bg-background"}
asChild
>
<Link href={cta.href} target="_blank" onClick={handleCTAClick}>
{cta.title}
</TrackedLinkTW>
</Link>
</Button>
)}
@@ -171,6 +170,19 @@ export const PricingCard: React.FC<PricingCardProps> = ({
);
};
const billingPlanToSkuMap: Record<Team["billingPlan"], ProductSKU | undefined> =
{
starter: "plan:starter",
growth: "plan:growth",
accelerate: "plan:accelerate",
scale: "plan:scale",
// we can't render checkout buttons for these plans:
pro: undefined,
free: undefined,
growth_legacy: undefined,
starter_legacy: undefined,
};
type FeatureItemProps = {
text: string | string[];
};
@@ -1,6 +1,6 @@
import type { Meta, StoryObj } from "@storybook/react";
import { useMemo, useState } from "react";
import { BadgeContainer, mobileViewport } from "../../../stories/utils";
import { BadgeContainer } from "../../../stories/utils";
import { SelectWithSearch } from "./select-with-search";
const meta = {
@@ -16,17 +16,10 @@ const meta = {
export default meta;
type Story = StoryObj<typeof meta>;
export const Desktop: Story = {
export const Variants: Story = {
args: {},
};
export const Mobile: Story = {
args: {},
parameters: {
viewport: mobileViewport("iphone14"),
},
};
function createList(len: number) {
return Array.from({ length: len }, (_, i) => ({
value: `${i}`,
@@ -36,7 +29,7 @@ function createList(len: number) {
function Story() {
return (
<div className="mx-auto flex w-full max-w-[600px] flex-col gap-6 px-4 py-6">
<div className="container flex max-w-6xl flex-col gap-6 py-10">
<VariantTest storyLabel="5 items" listLen={5} />
<VariantTest storyLabel="5000 items" listLen={5000} />
<VariantTest
@@ -23,6 +23,7 @@ export function WalletAddress(props: {
address: string | undefined;
shortenAddress?: boolean;
className?: string;
iconClassName?: string;
}) {
const thirdwebClient = useThirdwebClient();
// default back to zero address if no address provided
@@ -78,6 +79,7 @@ export function WalletAddress(props: {
address={address}
profiles={profiles.data || []}
thirdwebClient={thirdwebClient}
iconClassName={props.iconClassName}
/>
)}
<span className="cursor-pointer font-mono">
@@ -168,6 +170,7 @@ function WalletAvatar(props: {
address: string;
profiles: SocialProfile[];
thirdwebClient: ThirdwebClient;
iconClassName?: string;
}) {
const avatar = useMemo(() => {
return props.profiles.find(
@@ -186,11 +189,20 @@ function WalletAvatar(props: {
: undefined;
return (
<div className="size-6 overflow-hidden rounded-full">
<div
className={cn("size-6 overflow-hidden rounded-full", props.iconClassName)}
>
{resolvedAvatarSrc ? (
<Img src={resolvedAvatarSrc} className="size-6 object-cover" />
<Img
src={resolvedAvatarSrc}
className={cn("size-6 object-cover", props.iconClassName)}
/>
) : (
<Blobbie address={props.address} size={24} />
<Blobbie
address={props.address}
size={24}
className={props.iconClassName}
/>
)}
</div>
);
@@ -1,6 +1,6 @@
import type { Meta, StoryObj } from "@storybook/react";
import { useState } from "react";
import { BadgeContainer, mobileViewport } from "../../stories/utils";
import { BadgeContainer } from "../../stories/utils";
import { PaginationButtons } from "./pagination-buttons";
const meta = {
@@ -16,17 +16,10 @@ const meta = {
export default meta;
type Story = StoryObj<typeof meta>;
export const Desktop: Story = {
export const Variants: Story = {
args: {},
};
export const Mobile: Story = {
args: {},
parameters: {
viewport: mobileViewport("iphone14"),
},
};
function Story() {
return (
<div className="container flex max-w-[1000px] flex-col gap-8 py-10">
@@ -6,9 +6,6 @@ import { BadgeContainer } from "../../../stories/utils";
const meta = {
title: "Shadcn/Buttons",
component: Component,
parameters: {
layout: "centered",
},
} satisfies Meta<typeof Component>;
export default meta;
+3 -1
View File
@@ -243,7 +243,9 @@ const ChartTooltipContent = React.forwardRef<
<div className="grid gap-1.5">
{nestLabel ? tooltipLabel : null}
<span className="text-muted-foreground">
{itemConfig?.label || item.name}
{item.name === "maxLimit"
? "Upper Limit"
: itemConfig?.label || item.name}
</span>
</div>
{item.value !== undefined && (
@@ -1,5 +1,5 @@
import type { Meta, StoryObj } from "@storybook/react";
import { BadgeContainer, mobileViewport } from "stories/utils";
import { BadgeContainer } from "stories/utils";
import { CodeClient } from "./code.client";
const meta = {
@@ -11,17 +11,10 @@ const meta = {
export default meta;
type Story = StoryObj<typeof meta>;
export const Desktop: Story = {
export const Variants: Story = {
args: {},
};
export const Mobile: Story = {
args: {},
parameters: {
viewport: mobileViewport("iphone14"),
},
};
const tsCode = `\
type User = {
name: string;
@@ -86,7 +79,7 @@ const bashCode = "pnpm i thirdweb";
function Component() {
return (
<div className="container flex max-w-[600px] flex-col gap-10 py-10">
<div className="container flex max-w-6xl flex-col gap-10 py-10">
<BadgeContainer label="ts">
<CodeClient code={tsCode} lang="ts" />
</BadgeContainer>
@@ -1,5 +1,5 @@
import type { Meta, StoryObj } from "@storybook/react";
import { BadgeContainer, mobileViewport } from "stories/utils";
import { BadgeContainer } from "stories/utils";
import { PlainTextCodeBlock } from "./plaintext-code";
const meta = {
@@ -11,17 +11,10 @@ const meta = {
export default meta;
type Story = StoryObj<typeof meta>;
export const Desktop: Story = {
export const Variants: Story = {
args: {},
};
export const Mobile: Story = {
args: {},
parameters: {
viewport: mobileViewport("iphone14"),
},
};
const jsCode = `\
import { getContract } from "thirdweb";
import { sepolia } from "thirdweb/chains";
@@ -39,7 +32,7 @@ const overflowText =
function Component() {
return (
<div className="container flex max-w-[600px] flex-col gap-10 py-10">
<div className="container flex max-w-6xl flex-col gap-10 py-10">
<BadgeContainer label="large chunk of code">
<PlainTextCodeBlock code={jsCode} />
</BadgeContainer>
@@ -13,9 +13,6 @@ import { BadgeContainer } from "../../../stories/utils";
const meta = {
title: "Shadcn/Select",
component: Component,
parameters: {
layout: "centered",
},
} satisfies Meta<typeof Component>;
export default meta;
@@ -10,35 +10,25 @@ import {
} from "@/components/ui/table";
import type { Meta, StoryObj } from "@storybook/react";
import Link from "next/link";
import { BadgeContainer, mobileViewport } from "../../../stories/utils";
import { BadgeContainer } from "../../../stories/utils";
import { cn } from "../../lib/utils";
import { Badge } from "./badge";
const meta = {
title: "Shadcn/Table",
component: Component,
parameters: {
layout: "centered",
},
} satisfies Meta<typeof Component>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Desktop: Story = {
export const Variants: Story = {
args: {},
};
export const Mobile: Story = {
args: {},
parameters: {
viewport: mobileViewport("iphone14"),
},
};
function Component() {
return (
<div className="flex min-w-0 flex-col gap-6 px-4 py-6 lg:mx-auto lg:max-w-[1000px]">
<div className="container flex max-w-6xl flex-col gap-10 py-10">
<BadgeContainer label="Normal">
<TableDemo />
</BadgeContainer>
+32 -21
View File
@@ -7,6 +7,7 @@ import { usePathname } from "next/navigation";
import { useCallback, useRef, useState } from "react";
import { ScrollShadow } from "./ScrollShadow/ScrollShadow";
import { Button } from "./button";
import { ToolTipLabel } from "./tooltip";
export type TabLink = {
name: string;
@@ -20,6 +21,7 @@ export function TabLinks(props: {
className?: string;
tabContainerClassName?: string;
shadowColor?: string;
scrollableClassName?: string;
}) {
const { containerRef, lineRef, activeTabRef } =
useUnderline<HTMLAnchorElement>();
@@ -30,7 +32,7 @@ export function TabLinks(props: {
<div className="absolute right-0 bottom-0 left-0 h-[1px] bg-border" />
<ScrollShadow
scrollableClassName="pb-[8px] relative"
scrollableClassName={cn("pb-[8px] relative", props.scrollableClassName)}
shadowColor={props.shadowColor}
>
<div
@@ -81,6 +83,7 @@ export function TabButtons(props: {
isActive: boolean;
isDisabled?: boolean;
icon?: React.FC<{ className?: string }>;
toolTip?: string;
}[];
tabClassName?: string;
activeTabClassName?: string;
@@ -110,26 +113,31 @@ export function TabButtons(props: {
>
{props.tabs.map((tab, index) => {
return (
<Button
// biome-ignore lint/suspicious/noArrayIndexKey: tabs don't change order, so index is stable
key={index}
variant="ghost"
ref={tab.isActive ? activeTabRef : undefined}
className={cn(
"relative inline-flex h-auto items-center gap-1.5 rounded-lg px-2 font-medium text-sm hover:bg-accent lg:px-3 lg:text-base",
!tab.isActive &&
"text-muted-foreground hover:text-foreground",
tab.isDisabled && "cursor-not-allowed opacity-50",
props.tabClassName,
tab.isActive && props.activeTabClassName,
)}
onClick={!tab.isDisabled ? tab.onClick : undefined}
<ToolTipLabel
key={typeof tab.name === "string" ? tab.name : index}
label={tab.toolTip}
>
{tab.icon && (
<tab.icon className={cn("size-6", props.tabIconClassName)} />
)}
{tab.name}
</Button>
<Button
variant="ghost"
ref={tab.isActive ? activeTabRef : undefined}
className={cn(
"relative inline-flex h-auto items-center gap-1.5 rounded-lg px-2 font-medium text-sm hover:bg-accent lg:px-3 lg:text-base",
!tab.isActive &&
"text-muted-foreground hover:text-foreground",
tab.isDisabled && "cursor-not-allowed opacity-50",
props.tabClassName,
tab.isActive && props.activeTabClassName,
)}
onClick={!tab.isDisabled ? tab.onClick : undefined}
>
{tab.icon && (
<tab.icon
className={cn("size-6", props.tabIconClassName)}
/>
)}
{tab.name}
</Button>
</ToolTipLabel>
);
})}
</div>
@@ -159,9 +167,11 @@ function useUnderline<El extends HTMLElement>() {
const containerRect = containerRef.current.getBoundingClientRect();
const lineEl = lineRef.current;
const rect = activeTabEl.getBoundingClientRect();
const containerPaddingLeft =
containerRect.left - containerRef.current.offsetLeft;
lineEl.style.width = `${rect.width}px`;
lineEl.style.transform = `translateX(${
rect.left - containerRect.left
rect.left - containerPaddingLeft
}px)`;
setTimeout(() => {
lineEl.style.transition = "transform 0.3s, width 0.3s";
@@ -205,6 +215,7 @@ export function TabPathLinks(props: {
className?: string;
tabContainerClassName?: string;
shadowColor?: string;
scrollableClassName?: string;
}) {
const pathname = usePathname() || "";
const { links, ...restProps } = props;
+2
View File
@@ -3,6 +3,8 @@ export type ProductSKU =
| "plan:starter"
| "plan:growth"
| "plan:custom"
| "plan:accelerate"
| "plan:scale"
| "product:ecosystem_wallets"
| "product:engine_standard"
| "product:engine_premium"
@@ -15,8 +15,6 @@ import invariant from "tiny-invariant";
import type { EngineStatus } from "../../../app/team/[team_slug]/(team)/~/engine/(instance)/[engineId]/overview/components/transactions-table";
import { engineKeys } from "../cache-keys";
export type EngineTier = "STARTER" | "PREMIUM" | "ENTERPRISE";
// Engine instances
export type EngineInstance = {
id: string;
@@ -24,6 +22,8 @@ export type EngineInstance = {
name: string;
url: string;
lastAccessedAt: string;
isCloudHosted: boolean;
isPlanEngine: boolean;
status:
| "active"
| "pending"
@@ -0,0 +1,25 @@
"use client";
import { getThirdwebClient } from "@/constants/thirdweb.server";
import { getSDKTheme } from "app/components/sdk-component-theme";
import { useV5DashboardChain } from "lib/v5-adapter";
import { useTheme } from "next-themes";
import { PayEmbed } from "thirdweb/react";
export function UniversalBridgeEmbed({ chainId }: { chainId?: number }) {
const { theme } = useTheme();
const chain = useV5DashboardChain(chainId || 1);
return (
<PayEmbed
client={getThirdwebClient()}
payOptions={{
mode: "fund_wallet",
prefillBuy: {
chain,
amount: "0.01",
},
}}
theme={getSDKTheme(theme === "light" ? "light" : "dark")}
/>
);
}
@@ -0,0 +1,35 @@
import type { Metadata } from "next";
import { UniversalBridgeEmbed } from "./components/client/UniversalBridgeEmbed";
const title = "Universal Bridge: Swap, Bridge, and On-Ramp";
const description =
"Swap, bridge, and on-ramp to any EVM chain with thirdweb's Universal Bridge.";
export const metadata: Metadata = {
title,
description,
openGraph: {
title,
description,
},
};
export default async function RoutesPage({
searchParams,
}: { searchParams: Record<string, string | string[]> }) {
const { chainId } = searchParams;
return (
<div className="relative mx-auto flex h-screen w-full flex-col items-center justify-center overflow-hidden border py-10">
<main className="container z-10 flex justify-center">
<UniversalBridgeEmbed chainId={chainId ? Number(chainId) : undefined} />
</main>
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
alt=""
src="/assets/login/background.svg"
className="-bottom-12 -right-12 pointer-events-none absolute lg:right-0 lg:bottom-0"
/>
</div>
);
}
@@ -47,7 +47,7 @@ export const SearchInput: React.FC = () => {
<div className="group relative w-full">
<SearchIcon className="-translate-y-1/2 absolute top-[50%] left-3 size-4 text-muted-foreground" />
<Input
placeholder="Search by token address or chain ID"
placeholder="Search with anything!"
className="h-10 rounded-lg bg-card py-2 pl-9 lg:min-w-[300px]"
defaultValue={searchParams?.get("query") || ""}
onChange={(e) => handleSearch(e.target.value)}
@@ -7,6 +7,7 @@ import {
TableRow,
} from "@/components/ui/table";
import type { Address } from "thirdweb";
import { checksumAddress } from "thirdweb/utils";
import { getRoutes } from "../../../utils";
import { ChainlistPagination } from "../client/pagination";
import { RouteListCard } from "../server/routelist-card";
@@ -34,28 +35,83 @@ async function getRoutesToRender(params: SearchParams) {
originTokenAddress?: Address;
destinationChainId?: number;
destinationTokenAddress?: Address;
originTextQuery?: string;
destinationTextQuery?: string;
}> = {};
if (params.type === "origin" || typeof params.type === "undefined") {
if (params.query?.startsWith("0x")) {
filters.originTokenAddress = params.query as Address;
} else if (params.query) {
} else if (Number.isInteger(Number(params.query))) {
filters.originChainId = Number(params.query);
} else if (params.query) {
filters.originTextQuery = params.query;
}
} else if (params.type === "destination") {
if (params.query?.startsWith("0x")) {
filters.destinationTokenAddress = params.query as Address;
} else if (params.query) {
} else if (Number.isInteger(Number(params.query))) {
filters.destinationChainId = Number(params.query);
} else if (params.query) {
filters.destinationTextQuery = params.query;
}
}
// Temporary, will update this after the /routes endpoint
filters.limit = 50_000;
const routes = await getRoutes(filters);
let routes = await getRoutes({ limit: 100_000 });
const totalCount = routes.length;
if (filters.originChainId) {
routes = routes.filter(
(route) => route.originToken.chainId === filters.originChainId,
);
}
if (filters.originTokenAddress) {
const originTokenAddress = filters.originTokenAddress;
routes = routes.filter(
(route) =>
checksumAddress(route.originToken.address) ===
checksumAddress(originTokenAddress),
);
}
if (filters.destinationChainId) {
routes = routes.filter(
(route) => route.destinationToken.chainId === filters.destinationChainId,
);
}
if (filters.destinationTokenAddress) {
const destinationTokenAddress = filters.destinationTokenAddress;
routes = routes.filter(
(route) =>
checksumAddress(route.destinationToken.address) ===
checksumAddress(destinationTokenAddress),
);
}
if (filters.originTextQuery) {
const originTextQuery = filters.originTextQuery.toLowerCase();
routes = routes.filter((route) => {
return (
route.originToken.name.toLowerCase().includes(originTextQuery) ||
route.originToken.symbol.toLowerCase().includes(originTextQuery)
);
});
}
if (filters.destinationTextQuery) {
const destinationTextQuery = filters.destinationTextQuery.toLowerCase();
routes = routes.filter((route) => {
return (
route.destinationToken.name
.toLowerCase()
.includes(destinationTextQuery) ||
route.destinationToken.symbol
.toLowerCase()
.includes(destinationTextQuery)
);
});
}
return {
routesToRender: routes,
totalCount,
@@ -154,13 +210,21 @@ export async function RoutesData(props: {
out of{" "}
{filteredCount !== totalCount ? (
<>
<span className="text-accent-foreground">{filteredCount}</span>{" "}
<span className="text-accent-foreground">
{filteredCount.toLocaleString()}
</span>{" "}
routes that match filters. (Total:{" "}
<span className="text-accent-foreground">{totalCount}</span>)
<span className="text-accent-foreground">
{totalCount.toLocaleString()}
</span>
)
</>
) : (
<>
<span className="text-accent-foreground">{totalCount}</span> routes.
<span className="text-accent-foreground">
{totalCount.toLocaleString()}
</span>{" "}
routes.
</>
)}
</p>
@@ -2,43 +2,18 @@ import "server-only";
import { BRIDGE_URL, DASHBOARD_THIRDWEB_SECRET_KEY } from "@/constants/env";
import { getAuthToken } from "app/api/lib/getAuthToken";
import type { Address } from "thirdweb";
import type { Route } from "./types/route";
export async function getRoutes({
limit,
offset,
originChainId,
originTokenAddress,
destinationChainId,
destinationTokenAddress,
}: {
limit?: number;
offset?: number;
originChainId?: number;
originTokenAddress?: Address;
destinationChainId?: number;
destinationTokenAddress?: Address;
} = {}) {
const url = new URL(`${BRIDGE_URL}/v1/routes`);
if (limit) {
url.searchParams.set("limit", limit.toString());
}
if (offset) {
url.searchParams.set("offset", offset.toString());
}
if (originChainId) {
url.searchParams.set("originChainId", originChainId.toString());
}
if (originTokenAddress) {
url.searchParams.set("originTokenAddress", originTokenAddress);
}
if (destinationChainId) {
url.searchParams.set("destinationChainId", destinationChainId.toString());
}
if (destinationTokenAddress) {
url.searchParams.set("destinationTokenAddress", destinationTokenAddress);
}
// It's faster to filter client side, doesn't seem to be a big performance boost to paginate/filter server side
const token = await getAuthToken();
const routesResponse = await fetch(url, {
headers: token
@@ -0,0 +1,46 @@
"use client";
import { useTrack } from "hooks/analytics/useTrack";
import { FileText } from "lucide-react";
import Link from "next/link";
import type { ChainMetadata } from "thirdweb/chains";
import { SectionTitle } from "../server/SectionTitle";
export default function NextSteps(props: { chain: ChainMetadata }) {
const { chain } = props;
const trackEvent = useTrack();
return (
<section>
<SectionTitle title="Next Steps" />
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-3">
<div className="relative flex gap-3 rounded-lg border bg-card p-4 pr-8 transition-colors hover:border-active-border">
<FileText className="mt-0.5 size-5 shrink-0" />
<div>
<h3 className="mb-1.5 font-medium">
<Link
href={
"https://blog.thirdweb.com/supercharge-user-adoption-integrate-embedded-wallets-in-minutes/"
}
className="before:absolute before:inset-0"
target="_blank"
onClick={() =>
trackEvent({
category: "nextSteps",
action: "click-inapp",
label: "success",
chain_id: chain.chainId,
})
}
>
Create a login for {chain.name}
</Link>
</h3>
<p className="text-muted-foreground text-sm">
Supercharge User Adoption—Integrate In-App Wallets in Minutes
</p>
</div>
</div>
</div>
</section>
);
}
@@ -11,7 +11,7 @@ export function BuyFundsSection(props: { chain: ChainMetadata }) {
return (
<section>
<SectionTitle title="Buy Funds" />
<SectionTitle title="Bridge" />
<div className="flex justify-center rounded-lg border border-border bg-card px-4 py-10">
<div className="flex max-w-[520px] flex-col items-center ">
<div className="flex items-center">
@@ -28,21 +28,21 @@ export function BuyFundsSection(props: { chain: ChainMetadata }) {
<div className="h-6" />
<h2 className="px-4 text-center font-semibold text-lg tracking-tight">
Buy Funds on {sanitizedChainName} using Universal Bridge
Bridge Funds to {sanitizedChainName}
</h2>
<div className="h-2" />
<p className="max-w-[520px] px-4 text-center text-muted-foreground text-sm">
Instantly onramp, bridge & swap into {sanitizedChainName} with any
token.
Get {props.chain.nativeCurrency.symbol} on {sanitizedChainName} with
fiat or any token on another chain.
</p>
<div className="h-8" />
<PayModalButton
chainId={props.chain.chainId}
label={`Buy Funds on ${sanitizedChainName}`}
label={`Bridge to ${sanitizedChainName}`}
/>
<div className="h-4" />
@@ -13,12 +13,18 @@ import {
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { getThirdwebClient } from "@/constants/thirdweb.server";
import { ChevronDownIcon, TicketCheckIcon } from "lucide-react";
import type { Metadata } from "next";
import Link from "next/link";
import { redirect } from "next/navigation";
import { mapV4ChainToV5Chain } from "../../../../../contexts/map-chains";
import { getAuthToken } from "../../../../api/lib/getAuthToken";
import { getRawAccount } from "../../../../account/settings/getAccount";
import {
getAuthToken,
getAuthTokenWalletAddress,
} from "../../../../api/lib/getAuthToken";
import { NebulaFloatingChatButton } from "../../../../nebula-app/(app)/components/FloatingChat/FloatingChat";
import { StarButton } from "../../components/client/star-button";
import { getChain, getChainMetadata } from "../../utils";
import { AddChainToWallet } from "./components/client/add-chain-to-wallet";
@@ -55,17 +61,58 @@ export default async function ChainPageLayout(props: {
}) {
const params = await props.params;
const { children } = props;
const chain = await getChain(params.chain_id);
const authToken = await getAuthToken();
const [chain, authToken, account, accountAddress] = await Promise.all([
getChain(params.chain_id),
getAuthToken(),
getRawAccount(),
getAuthTokenWalletAddress(),
]);
if (params.chain_id !== chain.slug) {
redirect(chain.slug);
}
const chainMetadata = await getChainMetadata(chain.chainId);
const client = getThirdwebClient(authToken ?? undefined);
const chainPromptPrefix = `\
You are assisting users exploring the chain ${chain.name} (Chain ID: ${chain.chainId}). Provide concise insights into the types of applications and activities prevalent on this chain, such as DeFi protocols, NFT marketplaces, or gaming platforms. Highlight notable projects or trends without delving into technical details like consensus mechanisms or gas fees.
Users may seek comparisons between ${chain.name} and other chains. Provide objective, succinct comparisons focusing on performance, fees, and ecosystem support. Refrain from transaction-specific advice unless requested.
Provide users with an understanding of the unique use cases and functionalities that ${chain.name} supports. Discuss how developers leverage this chain for specific applications, such as scalable dApps, low-cost transactions, or specialized token standards, focusing on practical implementations.
Users may be interested in utilizing thirdweb tools on ${chain.name}. Offer clear guidance on how thirdweb's SDKs, smart contract templates, and deployment tools integrate with this chain. Emphasize the functionalities enabled by thirdweb without discussing transaction execution unless prompted.
Avoid transaction-related actions to be executed by the user unless inquired about.
The following is the user's message:
`;
const examplePrompts: string[] = [
"What are users doing on this chain?",
"What are the most active contracts?",
"Why would I use this chain over others?",
"Can I deploy thirdweb contracts to this chain?",
];
if (chain.chainId !== 1) {
examplePrompts.push("Can I bridge assets from Ethereum to this chain?");
}
return (
<>
<NebulaFloatingChatButton
authToken={authToken ?? undefined}
account={account}
label="Ask AI about this chain"
client={client}
nebulaParams={{
messagePrefix: chainPromptPrefix,
chainIds: [chain.chainId],
wallet: accountAddress ?? undefined,
}}
examplePrompts={examplePrompts.map((prompt) => ({
title: prompt,
message: prompt,
}))}
/>
<div className="flex h-14 border-border border-b pl-7">
<Breadcrumb className="my-auto">
<BreadcrumbList>
@@ -1,6 +1,7 @@
import { CircleAlertIcon } from "lucide-react";
import { getRawAccount } from "../../../../account/settings/getAccount";
import { getChain, getChainMetadata } from "../../utils";
import NextSteps from "./components/client/NextSteps";
import { BuyFundsSection } from "./components/server/BuyFundsSection";
import { ChainOverviewSection } from "./components/server/ChainOverviewSection";
import { ClaimChainSection } from "./components/server/ClaimChainSection";
@@ -57,6 +58,8 @@ export default async function Page(props: {
{chain.services.filter((s) => s.enabled).length > 0 && (
<SupportedProductsSection services={chain.services} />
)}
{/*Next Steps */}
<NextSteps chain={chain} />
{/* Claim Chain */}
{!chainMetadata && <ClaimChainSection />}
@@ -205,6 +205,8 @@ export const CreateListingsForm: React.FC<CreateListingsFormProps> = ({
owner: nft.owner,
type: "ERC721",
tokenURI: nft.tokenURI,
chainId: nft.chainId,
tokenAddress: nft.tokenAddress,
};
}
return {
@@ -216,6 +218,8 @@ export const CreateListingsForm: React.FC<CreateListingsFormProps> = ({
owner: nft.owner,
type: "ERC1155",
tokenURI: nft.tokenURI,
chainId: nft.chainId,
tokenAddress: nft.tokenAddress,
};
}) as WalletNFT[];
}, [ownedNFTs, form]);
@@ -17,7 +17,9 @@ export const ContractDirectListingsPage: React.FC<
return (
<div className="flex flex-col gap-6">
<div className="flex flex-row items-center justify-between">
<p className="text-lg">Contract Listings</p>
<h2 className="font-semibold text-2xl tracking-tight">
Direct Listings
</h2>
<div className="flex flex-row gap-4">
<CreateListingButton
isInsightSupported={isInsightSupported}
@@ -17,7 +17,9 @@ export const ContractEnglishAuctionsPage: React.FC<
return (
<div className="flex flex-col gap-6">
<div className="flex flex-row items-center justify-between">
<p className="text-lg">Contract Auctions</p>
<h2 className="font-semibold text-2xl tracking-tight">
English Auctions
</h2>
<div className="flex flex-row gap-4">
<CreateListingButton
contract={contract}
@@ -1,14 +1,7 @@
import {
Flex,
GridItem,
SimpleGrid,
Skeleton,
SkeletonText,
useBreakpointValue,
} from "@chakra-ui/react";
import { SkeletonContainer } from "@/components/ui/skeleton";
import { TrackedLinkTW } from "@/components/ui/tracked-link";
import { useMemo } from "react";
import { type NFT, ZERO_ADDRESS } from "thirdweb";
import { Card, TrackedLink, type TrackedLinkProps } from "tw-components";
import { NFTMediaWithEmptyState } from "tw-components/nft-media";
type NFTWithContract = NFT & { contractAddress: string; chainId: number };
@@ -20,18 +13,20 @@ const dummyMetadata: (idx: number) => NFTWithContract = (idx) => ({
tokenURI: `1-0x123-${idx}`,
metadata: {
name: "Loading...",
description: "lorem ipsum loading sit amet",
description:
"lorem ipsum loading sit amet consectetur adipisicing elit. Quisquam, quos.",
id: BigInt(idx || 0),
uri: `1-0x123-${idx}`,
},
owner: `0x_fake_${idx}`,
type: "ERC721",
supply: 1n,
tokenAddress: ZERO_ADDRESS,
});
interface NFTCardsProps {
nfts: Array<NFTWithContract>;
trackingCategory: TrackedLinkProps["category"];
trackingCategory: string;
isPending: boolean;
allNfts?: boolean;
}
@@ -42,61 +37,82 @@ export const NFTCards: React.FC<NFTCardsProps> = ({
isPending,
allNfts,
}) => {
const isMobile = useBreakpointValue({ base: true, md: false });
const dummyData = useMemo(() => {
return Array.from({
length: allNfts ? nfts.length : isMobile ? 2 : 3,
length: allNfts ? nfts.length : 3,
}).map((_, idx) => dummyMetadata(idx));
}, [nfts.length, isMobile, allNfts]);
}, [nfts.length, allNfts]);
return (
<SimpleGrid
gap={{ base: 3, md: 6 }}
columns={allNfts ? { base: 2, md: 4 } : { base: 2, md: 3 }}
>
<div className="grid grid-cols-1 gap-4 lg:grid-cols-3">
{(isPending ? dummyData : nfts).map((token) => {
const tokenId = token.id.toString();
return (
<GridItem
<div
key={`${token.chainId}_${token.contractAddress}_${tokenId}`}
as={TrackedLink}
category={trackingCategory}
href={`/${token.chainId}/${token.contractAddress}/nfts/${tokenId}`}
_hover={{ opacity: 0.75, textDecoration: "none" }}
className="hover:-translate-y-0.5 relative flex h-full cursor-pointer flex-col overflow-hidden rounded-lg bg-background duration-200 hover:scale-[1.01]"
>
<Card p={0} h="full">
<div className="relative aspect-square w-full overflow-hidden rounded-xl">
<Skeleton isLoaded={!isPending}>
<NFTMediaWithEmptyState
metadata={token.metadata}
requireInteraction
width="100%"
height="100%"
/>
</Skeleton>
</div>
<Flex p={4} pb={3} gap={1} direction="column">
<Skeleton w={!isPending ? "100%" : "50%"} isLoaded={!isPending}>
<h2 className="font-semibold tracking-tight">
{token.metadata.name}
</h2>
</Skeleton>
<SkeletonText isLoaded={!isPending}>
<p className="line-clamp-3 text-muted-foreground text-sm">
{token.metadata.description ? (
token.metadata.description
) : (
<i>No description</i>
)}
</p>
</SkeletonText>
</Flex>
</Card>
</GridItem>
{/* border */}
<div className="absolute inset-0 rounded-lg border border-border" />
{/* image */}
<div className="relative aspect-square w-full overflow-hidden">
<SkeletonContainer
skeletonData={token.metadata}
loadedData={isPending ? undefined : token.metadata}
className="h-full w-full rounded-lg"
render={(v) => {
return (
<NFTMediaWithEmptyState
metadata={v}
requireInteraction
width="100%"
height="100%"
/>
);
}}
/>
</div>
<div className="p-4">
{/* title */}
<SkeletonContainer
skeletonData={token.metadata}
loadedData={isPending ? undefined : token.metadata}
className="mb-2"
render={(v) => {
return (
<h2 className="font-semibold tracking-tight">
<TrackedLinkTW
category={trackingCategory}
label="view_nft"
href={`/${token.chainId}/${token.contractAddress}/nfts/${tokenId}`}
className="before:absolute before:inset-0"
>
{v.name}
</TrackedLinkTW>
</h2>
);
}}
/>
{/* Description */}
<SkeletonContainer
skeletonData={token.metadata}
loadedData={isPending ? undefined : token.metadata}
render={(v) => {
return (
<p className="line-clamp-3 text-muted-foreground text-sm">
{v.description ? v.description : <i>No description</i>}
</p>
);
}}
/>
</div>
</div>
);
})}
</SimpleGrid>
</div>
);
};
@@ -55,7 +55,7 @@ type Options =
};
export async function getClaimPhasesInLegacyFormat(
options: BaseTransactionOptions<Options>,
options: BaseTransactionOptions<Options> & { isMultiPhase: boolean },
): Promise<CombinedClaimCondition[]> {
const conditions = await (async () => {
switch (options.type) {
@@ -64,7 +64,10 @@ export async function getClaimPhasesInLegacyFormat(
case "erc721":
return ERC721Ext.getClaimConditions(options);
case "erc1155":
return ERC1155Ext.getClaimConditions(options);
return ERC1155Ext.getClaimConditions({
...options,
singlePhaseDrop: !options.isMultiPhase,
});
}
})();
@@ -126,7 +129,7 @@ export async function getClaimPhasesInLegacyFormat(
type PhaseInput = z.input<typeof LegacyClaimConditionInputSchema>;
export function setClaimPhasesTx(
baseOptions: BaseTransactionOptions<Options>,
baseOptions: BaseTransactionOptions<Options> & { isSinglePhase?: boolean },
rawPhases: PhaseInput[],
) {
const phases = rawPhases.map((phase) => {
@@ -172,6 +175,7 @@ export function setClaimPhasesTx(
contract: baseOptions.contract,
phases,
tokenId: baseOptions.tokenId,
singlePhaseDrop: baseOptions.isSinglePhase,
});
}
}
@@ -222,11 +222,15 @@ export const ClaimConditionsForm: React.FC<ClaimConditionsFormProps> = ({
const claimConditionsQuery = useReadContract(getClaimPhasesInLegacyFormat, {
contract,
isMultiPhase,
...(isErc20
? { type: "erc20", decimals: tokenDecimals.data }
: isErc721
? { type: "erc721" }
: { type: "erc1155", tokenId: BigInt(tokenId || 0) }),
: {
type: "erc1155",
tokenId: BigInt(tokenId || 0),
}),
});
const transformedQueryData = useMemo(() => {
@@ -370,6 +374,7 @@ export const ClaimConditionsForm: React.FC<ClaimConditionsFormProps> = ({
const tx = setClaimPhasesTx(
{
contract,
isSinglePhase: !isMultiPhase,
...(isErc20
? { type: "erc20", decimals: tokenDecimals.data }
: isErc721
@@ -635,6 +640,7 @@ export const ClaimConditionsForm: React.FC<ClaimConditionsFormProps> = ({
isErc20={isErc20}
contract={contract}
tokenId={tokenId}
isMultiphase={isMultiPhase}
/>
)}
</div>
@@ -10,6 +10,7 @@ interface ClaimConditionsProps {
isColumn?: true;
isERC20: boolean;
twAccount: Account | undefined;
isMultiphase: boolean;
}
export const ClaimConditions: React.FC<ClaimConditionsProps> = ({
contract,
@@ -17,6 +18,7 @@ export const ClaimConditions: React.FC<ClaimConditionsProps> = ({
isColumn,
isERC20,
twAccount,
isMultiphase,
}) => {
return (
<div className="flex w-full flex-col gap-6">
@@ -37,8 +39,7 @@ export const ClaimConditions: React.FC<ClaimConditionsProps> = ({
contract={contract}
tokenId={tokenId}
isColumn={isColumn}
// always multi phase!
isMultiPhase={true}
isMultiPhase={isMultiphase}
/>
</div>
);
@@ -18,6 +18,7 @@ interface ResetClaimEligibilityProps {
contract: ThirdwebContract;
tokenId?: string;
twAccount: Account | undefined;
isMultiphase: boolean;
}
export const ResetClaimEligibility: React.FC<ResetClaimEligibilityProps> = ({
@@ -25,6 +26,7 @@ export const ResetClaimEligibility: React.FC<ResetClaimEligibilityProps> = ({
tokenId,
isErc20,
twAccount,
isMultiphase,
}) => {
const trackEvent = useTrack();
@@ -55,6 +57,7 @@ export const ResetClaimEligibility: React.FC<ResetClaimEligibilityProps> = ({
return ERC1155Ext.resetClaimEligibility({
contract,
tokenId: BigInt(tokenId),
singlePhaseDrop: !isMultiphase,
});
}
// assume erc 721
@@ -36,33 +36,34 @@ export function ContractPageLayout(props: {
return (
<ChakraProviderSetup>
<SidebarLayout sidebarLinks={sidebarLinks}>
<div className="border-border border-b pb-8">
<div className="flex flex-col gap-4 ">
<div className="flex flex-col justify-between gap-4 md:flex-row">
<ContractMetadata
contract={contract}
chain={chainMetadata}
contractMetadata={dashboardContractMetadata}
externalLinks={externalLinks}
/>
<PrimaryDashboardButton
contractAddress={contract.address}
chain={contract.chain}
contractInfo={{
chain: chainMetadata,
chainSlug: chainMetadata.slug,
contractAddress: contract.address,
}}
teamsAndProjects={teamsAndProjects}
client={client}
/>
</div>
<DeprecatedAlert chain={chainMetadata} />
<div className="border-border border-b py-8">
<div className="container flex flex-col gap-4">
<div className="flex flex-col justify-between gap-4 md:flex-row">
<ContractMetadata
contract={contract}
chain={chainMetadata}
contractMetadata={dashboardContractMetadata}
externalLinks={externalLinks}
/>
<PrimaryDashboardButton
contractAddress={contract.address}
chain={contract.chain}
contractInfo={{
chain: chainMetadata,
chainSlug: chainMetadata.slug,
contractAddress: contract.address,
}}
teamsAndProjects={teamsAndProjects}
client={client}
/>
</div>
<DeprecatedAlert chain={chainMetadata} />
</div>
<div className="h-8" />
<div className="pb-10">{props.children}</div>
</div>
<SidebarLayout sidebarLinks={sidebarLinks}>
{props.children}
<div className="h-20" />
</SidebarLayout>
</ChakraProviderSetup>
);
@@ -3,7 +3,7 @@
import { CopyAddressButton } from "@/components/ui/CopyAddressButton";
import { Button } from "@/components/ui/button";
import { getThirdwebClient } from "@/constants/thirdweb.server";
import { ChainIcon } from "components/icons/ChainIcon";
import { ChainIconClient } from "components/icons/ChainIcon";
import { ExternalLinkIcon } from "lucide-react";
import Link from "next/link";
import type { ChainMetadata } from "thirdweb/chains";
@@ -66,7 +66,7 @@ export const MetadataHeader: React.FC<MetadataHeaderProps> = ({
</div>
)}
<div className="flex grow flex-col gap-4">
<div className="flex grow flex-col gap-3">
{/* Title */}
{!data ? (
<h1 className="font-semibold text-2xl tracking-tight lg:text-3xl">
@@ -75,7 +75,7 @@ export const MetadataHeader: React.FC<MetadataHeaderProps> = ({
) : (
<div className="flex flex-col gap-3 lg:flex-row lg:items-center">
{data?.name && (
<h1 className="line-clamp-2 font-semibold text-2xl tracking-tight lg:text-3xl">
<h1 className="line-clamp-2 font-bold text-2xl tracking-tight lg:text-3xl">
{data.name}
</h1>
)}
@@ -85,7 +85,7 @@ export const MetadataHeader: React.FC<MetadataHeaderProps> = ({
href={`/${chain.slug}`}
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"
>
<ChainIcon ipfsSrc={chain.icon?.url} className="size-4" />
<ChainIconClient ipfsSrc={chain.icon?.url} className="size-4" />
{cleanedChainName && (
<span className="text-xs">{cleanedChainName}</span>
)}
@@ -89,7 +89,7 @@ function AddToProjectButton(props: {
return (
<Dialog>
<DialogTrigger asChild>
<Button className="gap-2" variant="outline">
<Button className="gap-2 bg-card" variant="outline">
<PlusIcon className="size-4" />
Add To Project
</Button>
@@ -33,6 +33,7 @@ export const NftsOwned: React.FC<NftsOwnedProps> = ({
type: nft.type,
contractAddress: nft.contractAddress,
chainId: contract.chain.id,
tokenAddress: nft.tokenAddress,
}))}
allNfts
isPending={isWalletNFTsLoading}
@@ -1,6 +1,6 @@
"use client";
import { ThirdwebBarChart } from "@/components/blocks/charts/bar-chart";
import { Skeleton, Stat, StatLabel, StatNumber } from "@chakra-ui/react";
import { SkeletonContainer } from "@/components/ui/skeleton";
import type { UseQueryResult } from "@tanstack/react-query";
import {
type AnalyticsQueryParams,
@@ -14,9 +14,9 @@ import {
useTotalContractTransactionAnalytics,
useTotalContractUniqueWallets,
} from "data/analytics/hooks";
import { Suspense, useMemo, useState } from "react";
import { formatDate } from "date-fns";
import { useMemo, useState } from "react";
import type { ThirdwebContract } from "thirdweb";
import { Card } from "tw-components";
import {
DateRangeSelector,
type Range,
@@ -121,6 +121,15 @@ type ChartProps = {
startDate: Date;
endDate: Date;
};
function toolTipLabelFormatter(_v: string, item: unknown) {
if (Array.isArray(item)) {
const time = item[0].payload.time as number;
return formatDate(new Date(time), "MMM d, yyyy");
}
return undefined;
}
function UniqueWalletsChart(props: ChartProps) {
const analyticsQuery = useContractUniqueWalletAnalytics(props);
@@ -128,6 +137,7 @@ function UniqueWalletsChart(props: ChartProps) {
<ThirdwebBarChart
header={{
title: "Unique Wallets",
titleClassName: "mb-0.5 text-xl",
description:
"The number of unique wallet addresses that have sent a transaction to this contract.",
}}
@@ -140,6 +150,8 @@ function UniqueWalletsChart(props: ChartProps) {
},
}}
chartClassName="aspect-[1.5] lg:aspect-[4.5]"
hideLabel={false}
toolTipLabelFormatter={toolTipLabelFormatter}
/>
);
}
@@ -156,6 +168,7 @@ function TotalTransactionsChart(props: ChartProps) {
<ThirdwebBarChart
header={{
title: "Total Transactions",
titleClassName: "mb-0.5 text-xl",
description:
"The number of transactions that have been sent to this contract.",
}}
@@ -168,6 +181,8 @@ function TotalTransactionsChart(props: ChartProps) {
},
}}
chartClassName="aspect-[1.5] lg:aspect-[4.5]"
hideLabel={false}
toolTipLabelFormatter={toolTipLabelFormatter}
/>
);
}
@@ -179,6 +194,7 @@ function TotalEventsChart(props: ChartProps) {
<ThirdwebBarChart
header={{
title: "Total Events",
titleClassName: "mb-0.5 text-xl",
description:
"The number of on-chain events that have been emitted from this contract.",
}}
@@ -191,6 +207,8 @@ function TotalEventsChart(props: ChartProps) {
},
}}
chartClassName="aspect-[1.5] lg:aspect-[4.5]"
hideLabel={false}
toolTipLabelFormatter={toolTipLabelFormatter}
/>
);
}
@@ -227,6 +245,7 @@ function FunctionBreakdownChart(
<ThirdwebBarChart
header={{
title: "Function Breakdown",
titleClassName: "mb-0.5 text-xl",
description:
"The breakdown of calls to each write function from transactions.",
}}
@@ -247,6 +266,8 @@ function FunctionBreakdownChart(
)}
chartClassName="aspect-[1.5] lg:aspect-[4.5]"
showLegend
hideLabel={false}
toolTipLabelFormatter={toolTipLabelFormatter}
/>
);
}
@@ -283,6 +304,7 @@ function EventBreakdownChart(
<ThirdwebBarChart
header={{
title: "Event Breakdown",
titleClassName: "mb-0.5 text-xl",
description: "The breakdown of events emitted by this contract.",
}}
data={mappedQueryData || []}
@@ -302,6 +324,8 @@ function EventBreakdownChart(
)}
chartClassName="aspect-[1.5] lg:aspect-[4.5]"
showLegend
hideLabel={false}
toolTipLabelFormatter={toolTipLabelFormatter}
/>
);
}
@@ -320,25 +344,12 @@ const AnalyticsStat: React.FC<AnalyticsStatProps> = ({
useTotal,
}) => {
return (
<Suspense fallback={<AnalyticsSkeleton label={label} />}>
<AnalyticsData
chainId={chainId}
contractAddress={contractAddress}
useTotal={useTotal}
label={label}
/>
</Suspense>
);
};
const AnalyticsSkeleton: React.FC<{ label: string }> = ({ label }) => {
return (
<Card as={Stat} className="bg-card">
<StatLabel mb={{ base: 1, md: 0 }}>{label}</StatLabel>
<Skeleton isLoaded={false}>
<StatNumber>{0}</StatNumber>
</Skeleton>
</Card>
<AnalyticsData
chainId={chainId}
contractAddress={contractAddress}
useTotal={useTotal}
label={label}
/>
);
};
@@ -355,20 +366,23 @@ const AnalyticsData: React.FC<AnalyticsStatProps> = ({
chainId,
});
const data = useMemo(() => {
if (!totalQuery.data) {
return 0;
}
return totalQuery.data.count;
}, [totalQuery.data]);
return (
<Card as={Stat} className="bg-card">
<StatLabel mb={{ base: 1, md: 0 }}>{label}</StatLabel>
<Skeleton isLoaded={totalQuery.isFetched}>
<StatNumber>{data.toLocaleString()}</StatNumber>
</Skeleton>
</Card>
);
return <AnalyticsStatUI label={label} data={totalQuery.data?.count} />;
};
function AnalyticsStatUI(props: {
label: string;
data: number | undefined;
}) {
return (
<dl className="rounded-lg border bg-card p-4">
<dt className="font-semibold">{props.label}</dt>
<SkeletonContainer
skeletonData={10000}
loadedData={props.data}
render={(v) => {
return <dd className="font-normal text-xl">{v.toLocaleString()}</dd>;
}}
/>
</dl>
);
}
@@ -36,6 +36,7 @@ export function ClaimConditionsClient(props: {
contract={props.contract}
isERC20={supportedERCs.isERC20}
twAccount={props.twAccount}
isMultiphase={true}
/>
);
}
@@ -41,6 +41,7 @@ export default async function Page(props: {
contract={contract}
isERC20={supportedERCs.isERC20}
twAccount={account}
isMultiphase={true}
/>
);
}
@@ -2,7 +2,6 @@ import { fetchPublishedContractsFromDeploy } from "components/contract-component
import { notFound } from "next/navigation";
import {
type ContractOptions,
eth_blockNumber,
eth_getTransactionByHash,
eth_getTransactionReceipt,
getContractEvents,
@@ -39,14 +38,17 @@ export default async function Page(props: {
chain_id: string;
}>;
}) {
const params = await props.params;
const info = await getContractPageParamsInfo(params);
const ProxyDeployedEvent = prepareEvent({
signature:
"event ProxyDeployedV2(address indexed implementation, address indexed proxy, address indexed deployer, bytes32 inputSalt, bytes data, bytes extraData)",
filters: {
proxy: params.contractAddress.toLowerCase(),
},
});
const params = await props.params;
const info = await getContractPageParamsInfo(params);
if (!info) {
notFound();
}
@@ -111,16 +113,9 @@ export default async function Page(props: {
let creationBlockNumber: bigint | undefined;
if (twCloneFactoryContract) {
const latestBlockNumber = await eth_blockNumber(
getRpcClient({
client: contract.client,
chain: contract.chain,
}),
);
const events = await getContractEvents({
contract: twCloneFactoryContract,
events: [ProxyDeployedEvent],
blockRange: latestBlockNumber < 100000n ? latestBlockNumber : 100000n,
});
const event = events.find(
(e) =>
@@ -208,7 +203,6 @@ export default async function Page(props: {
const events = await getContractEvents({
contract: contract,
events: [moduleEvent],
blockRange: 123456n,
});
const filteredEvents = events.filter(
@@ -1,6 +1,6 @@
import { SelectWithSearch } from "@/components/blocks/select-with-search";
import { Badge } from "@/components/ui/badge";
import { ChainIcon } from "components/icons/ChainIcon";
import { ChainIconClient } from "components/icons/ChainIcon";
import { useAllChainsData } from "hooks/chains/allChains";
import { useCallback, useMemo } from "react";
@@ -45,7 +45,7 @@ export function SingleNetworkSelector(props: {
return (
<div className="flex justify-between gap-4">
<span className="flex grow gap-2 truncate text-left">
<ChainIcon
<ChainIconClient
className="size-5"
ipfsSrc={chain.icon?.url}
loading="lazy"
@@ -9,6 +9,12 @@ import { isAddress, isContractDeployed } from "thirdweb/utils";
import type { MinimalTeamsAndProjects } from "../../../../../components/contract-components/contract-deploy-form/add-to-project-card";
import { resolveFunctionSelectors } from "../../../../../lib/selectors";
import { shortenIfAddress } from "../../../../../utils/usedapp-external";
import { getRawAccount } from "../../../../account/settings/getAccount";
import {
getAuthToken,
getAuthTokenWalletAddress,
} from "../../../../api/lib/getAuthToken";
import { NebulaFloatingChatButton } from "../../../../nebula-app/(app)/components/FloatingChat/FloatingChat";
import { ConfigureCustomChain } from "./_layout/ConfigureCustomChain";
import { getContractMetadataHeaderData } from "./_layout/contract-metadata";
import { ContractPageLayout } from "./_layout/contract-page-layout";
@@ -42,6 +48,12 @@ export default async function Layout(props: {
notFound();
}
const [authToken, account, accountAddress] = await Promise.all([
getAuthToken(),
getRawAccount(),
getAuthTokenWalletAddress(),
]);
const client = getThirdwebClient();
const teamsAndProjects = await getTeamsAndProjectsIfLoggedIn();
@@ -75,6 +87,25 @@ export default async function Layout(props: {
const { contractMetadata, externalLinks } =
await getContractMetadataHeaderData(contract);
const contractAddress = info.contract.address;
const chainName = info.chainMetadata.name;
const chainId = info.contract.chain.id;
const contractPromptPrefix = `A user is viewing the contract address ${contractAddress} on ${chainName} (Chain ID: ${chainId}). Provide a concise summary of this contract's functionalities, such as token minting, staking, or governance mechanisms. Focus on what the contract enables users to do, avoiding transaction execution details unless requested.
Users may be interested in how to interact with the contract. Outline common interaction patterns, such as claiming rewards, participating in governance, or transferring assets. Emphasize the contract's capabilities without guiding through transaction processes unless asked.
Provide insights into how the contract is being used. Share information on user engagement, transaction volumes, or integration with other dApps, focusing on the contract's role within the broader ecosystem.
Users may be considering integrating the contract into their applications. Discuss how this contract's functionalities can be leveraged within different types of dApps, highlighting potential use cases and benefits.
The following is the user's message:`;
const examplePrompts: string[] = [
"What does this contract do?",
"What permissions or roles exist in this contract?",
"Which functions are used the most?",
"Has this contract been used recently?",
"Who are the largest holders/users of this?",
];
return (
<ContractPageLayout
chainMetadata={chainMetadata}
@@ -85,6 +116,21 @@ export default async function Layout(props: {
teamsAndProjects={teamsAndProjects}
client={client}
>
<NebulaFloatingChatButton
authToken={authToken ?? undefined}
account={account}
label="Ask AI about this contract"
client={client}
nebulaParams={{
messagePrefix: contractPromptPrefix,
chainIds: [info.contract.chain.id],
wallet: accountAddress ?? undefined,
}}
examplePrompts={examplePrompts.map((prompt) => ({
title: prompt,
message: prompt,
}))}
/>
{props.children}
</ContractPageLayout>
);
@@ -3,14 +3,11 @@ import { getThirdwebClient } from "@/constants/thirdweb.server";
import type { Meta, StoryObj } from "@storybook/react";
import { useMutation } from "@tanstack/react-query";
import { useState } from "react";
import { Toaster, toast } from "sonner";
import { BadgeContainer, mobileViewport } from "stories/utils";
import { toast } from "sonner";
import { BadgeContainer } from "stories/utils";
import { ZERO_ADDRESS } from "thirdweb";
import { ConnectButton, ThirdwebProvider } from "thirdweb/react";
import {
ErrorProvider,
type TransactionError,
} from "../../../../../../../contexts/error-handler";
import type { TransactionError } from "../../../../../../../contexts/error-handler";
import { accountStub } from "../../../../../../../stories/stubs";
import {
BatchMetadataModuleUI,
@@ -21,7 +18,6 @@ const meta = {
title: "Modules/BatchMetadata",
component: Component,
parameters: {
layout: "centered",
nextjs: {
appDirectory: true,
},
@@ -31,17 +27,10 @@ const meta = {
export default meta;
type Story = StoryObj<typeof meta>;
export const Desktop: Story = {
export const Variants: Story = {
args: {},
};
export const Mobile: Story = {
args: {},
parameters: {
viewport: mobileViewport("iphone14"),
},
};
function Component() {
const [isOwner, setIsOwner] = useState(true);
@@ -80,38 +69,35 @@ function Component() {
return (
<ThirdwebProvider>
<ErrorProvider>
<div className="container flex max-w-[1150px] flex-col gap-10 py-10">
<div>
<ConnectButton client={getThirdwebClient()} />
</div>
<div className="flex items-center gap-5">
<CheckboxWithLabel
value={isOwner}
onChange={setIsOwner}
id="isOwner"
label="Is Owner"
/>
</div>
<BadgeContainer label="Default">
<BatchMetadataModuleUI
twAccount={accountStub()}
contractInfo={contractInfo}
moduleAddress="0x0000000000000000000000000000000000000000"
uploadMetadata={uploadMetadataStub}
uninstallButton={{
onClick: async () => removeMutation.mutateAsync(),
isPending: removeMutation.isPending,
}}
isOwnerAccount={isOwner}
contractChainId={1}
/>
</BadgeContainer>
<Toaster richColors />
<div className="container flex max-w-6xl flex-col gap-10 py-10">
<div>
<ConnectButton client={getThirdwebClient()} />
</div>
</ErrorProvider>
<div className="flex items-center gap-5">
<CheckboxWithLabel
value={isOwner}
onChange={setIsOwner}
id="isOwner"
label="Is Owner"
/>
</div>
<BadgeContainer label="Default">
<BatchMetadataModuleUI
twAccount={accountStub()}
contractInfo={contractInfo}
moduleAddress="0x0000000000000000000000000000000000000000"
uploadMetadata={uploadMetadataStub}
uninstallButton={{
onClick: async () => removeMutation.mutateAsync(),
isPending: removeMutation.isPending,
}}
isOwnerAccount={isOwner}
contractChainId={1}
/>
</BadgeContainer>
</div>
</ThirdwebProvider>
);
}
@@ -11,8 +11,7 @@ import type { Meta, StoryObj } from "@storybook/react";
import { useMutation } from "@tanstack/react-query";
import { subDays } from "date-fns";
import { useState } from "react";
import { Toaster, toast } from "sonner";
import { mobileViewport } from "stories/utils";
import { toast } from "sonner";
import { NATIVE_TOKEN_ADDRESS, ZERO_ADDRESS } from "thirdweb";
import { ConnectButton, ThirdwebProvider } from "thirdweb/react";
import { checksumAddress } from "thirdweb/utils";
@@ -29,7 +28,6 @@ const meta = {
title: "Modules/Claimable",
component: Component,
parameters: {
layout: "centered",
nextjs: {
appDirectory: true,
},
@@ -39,17 +37,10 @@ const meta = {
export default meta;
type Story = StoryObj<typeof meta>;
export const Desktop: Story = {
export const Variants: Story = {
args: {},
};
export const Mobile: Story = {
args: {},
parameters: {
viewport: mobileViewport("iphone14"),
},
};
const testAddress1 = "0x1F846F6DAE38E1C88D71EAA191760B15f38B7A37";
const claimCondition = {
@@ -67,7 +58,7 @@ const claimCondition = {
function Component() {
const [isOwner, setIsOwner] = useState(true);
const [name, setName] = useState("ClaimableERC721");
const [name, setName] = useState("MintableERC721");
const [isClaimConditionLoading, setIsClaimConditionLoading] = useState(false);
const [isPrimarySaleRecipientLoading, setIsPrimarySaleRecipientLoading] =
useState(false);
@@ -110,7 +101,7 @@ function Component() {
return (
<ThirdwebProvider>
<div className="container flex max-w-[1150px] flex-col gap-10 py-10">
<div className="container flex max-w-6xl flex-col gap-10 py-10">
<div>
<ConnectButton client={getThirdwebClient()} />
</div>
@@ -196,8 +187,6 @@ function Component() {
isValidTokenId={true}
noClaimConditionSet={noClaimConditionSet}
/>
<Toaster richColors />
</div>
</ThirdwebProvider>
);
@@ -11,8 +11,8 @@ import { getThirdwebClient } from "@/constants/thirdweb.server";
import type { Meta, StoryObj } from "@storybook/react";
import { useMutation } from "@tanstack/react-query";
import { useState } from "react";
import { Toaster, toast } from "sonner";
import { BadgeContainer, mobileViewport } from "stories/utils";
import { toast } from "sonner";
import { BadgeContainer } from "stories/utils";
import { ConnectButton, ThirdwebProvider } from "thirdweb/react";
import { accountStub } from "../../../../../../../stories/stubs";
import {
@@ -25,7 +25,6 @@ const meta = {
title: "Modules/Mintable",
component: Component,
parameters: {
layout: "centered",
nextjs: {
appDirectory: true,
},
@@ -35,17 +34,10 @@ const meta = {
export default meta;
type Story = StoryObj<typeof meta>;
export const Desktop: Story = {
export const Variants: Story = {
args: {},
};
export const Mobile: Story = {
args: {},
parameters: {
viewport: mobileViewport("iphone14"),
},
};
const testAddress1 = "0x1F846F6DAE38E1C88D71EAA191760B15f38B7A37";
function Component() {
@@ -84,7 +76,7 @@ function Component() {
return (
<ThirdwebProvider>
<div className="container flex max-w-[1150px] flex-col gap-10 py-10">
<div className="container flex max-w-6xl flex-col gap-10 py-10">
<div>
<ConnectButton client={getThirdwebClient()} />
</div>
@@ -175,8 +167,6 @@ function Component() {
contractChainId={1}
/>
</BadgeContainer>
<Toaster richColors />
</div>
</ThirdwebProvider>
);
@@ -2,33 +2,22 @@ import { Checkbox } from "@/components/ui/checkbox";
import type { Meta, StoryObj } from "@storybook/react";
import { useMutation } from "@tanstack/react-query";
import { useState } from "react";
import { Toaster } from "sonner";
import { BadgeContainer, mobileViewport } from "stories/utils";
import { BadgeContainer } from "stories/utils";
import { ThirdwebProvider } from "thirdweb/react";
import { ModuleCardUI } from "./module-card";
const meta = {
title: "Modules/ModuleCard",
component: Component,
parameters: {
layout: "centered",
},
} satisfies Meta<typeof Component>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Desktop: Story = {
export const Variants: Story = {
args: {},
};
export const Mobile: Story = {
args: {},
parameters: {
viewport: mobileViewport("iphone14"),
},
};
function Component() {
const [isOwner, setIsOwner] = useState(true);
@@ -120,8 +109,6 @@ function Component() {
</div>
</ModuleCardUI>
</BadgeContainer>
<Toaster richColors />
</div>
</ThirdwebProvider>
);
@@ -3,8 +3,8 @@ import { getThirdwebClient } from "@/constants/thirdweb.server";
import type { Meta, StoryObj } from "@storybook/react";
import { useMutation } from "@tanstack/react-query";
import { useState } from "react";
import { Toaster, toast } from "sonner";
import { BadgeContainer, mobileViewport } from "stories/utils";
import { toast } from "sonner";
import { BadgeContainer } from "stories/utils";
import { ConnectButton, ThirdwebProvider } from "thirdweb/react";
import { accountStub } from "../../../../../../../stories/stubs";
import {
@@ -16,7 +16,6 @@ const meta = {
title: "Modules/OpenEditionMetadata",
component: Component,
parameters: {
layout: "centered",
nextjs: {
appDirectory: true,
},
@@ -26,17 +25,10 @@ const meta = {
export default meta;
type Story = StoryObj<typeof meta>;
export const Desktop: Story = {
export const Variants: Story = {
args: {},
};
export const Mobile: Story = {
args: {},
parameters: {
viewport: mobileViewport("iphone14"),
},
};
function Component() {
const [isOwner, setIsOwner] = useState(true);
async function setSharedMetadataStub(values: SetSharedMetadataFormValues) {
@@ -98,8 +90,6 @@ function Component() {
twAccount={accountStub()}
/>
</BadgeContainer>
<Toaster richColors />
</div>
</ThirdwebProvider>
);
@@ -3,8 +3,8 @@ import { getThirdwebClient } from "@/constants/thirdweb.server";
import type { Meta, StoryObj } from "@storybook/react";
import { useMutation } from "@tanstack/react-query";
import { useState } from "react";
import { Toaster, toast } from "sonner";
import { BadgeContainer, mobileViewport } from "stories/utils";
import { toast } from "sonner";
import { BadgeContainer } from "stories/utils";
import { ConnectButton, ThirdwebProvider } from "thirdweb/react";
import { accountStub } from "../../../../../../../stories/stubs";
import {
@@ -18,7 +18,6 @@ const meta = {
title: "Modules/Royalty",
component: Component,
parameters: {
layout: "centered",
nextjs: {
appDirectory: true,
},
@@ -28,17 +27,10 @@ const meta = {
export default meta;
type Story = StoryObj<typeof meta>;
export const Desktop: Story = {
export const Variants: Story = {
args: {},
};
export const Mobile: Story = {
args: {},
parameters: {
viewport: mobileViewport("iphone14"),
},
};
const _testAddress1 = "0x1F846F6DAE38E1C88D71EAA191760B15f38B7A37";
function Component() {
@@ -79,7 +71,7 @@ function Component() {
return (
<ThirdwebProvider>
<div className="container flex max-w-[1150px] flex-col gap-10 py-10">
<div className="container flex max-w-6xl flex-col gap-10 py-10">
<div>
<ConnectButton client={getThirdwebClient()} />
</div>
@@ -168,8 +160,6 @@ function Component() {
twAccount={twAccount}
/>
</BadgeContainer>
<Toaster richColors />
</div>
</ThirdwebProvider>
);
@@ -3,8 +3,8 @@ import { getThirdwebClient } from "@/constants/thirdweb.server";
import type { Meta, StoryObj } from "@storybook/react";
import { useMutation } from "@tanstack/react-query";
import { useState } from "react";
import { Toaster, toast } from "sonner";
import { BadgeContainer, mobileViewport } from "stories/utils";
import { toast } from "sonner";
import { BadgeContainer } from "stories/utils";
import { ConnectButton, ThirdwebProvider } from "thirdweb/react";
import { accountStub } from "../../../../../../../stories/stubs";
import {
@@ -16,7 +16,6 @@ const meta = {
title: "Modules/Transferable",
component: Component,
parameters: {
layout: "centered",
nextjs: {
appDirectory: true,
},
@@ -26,17 +25,10 @@ const meta = {
export default meta;
type Story = StoryObj<typeof meta>;
export const Desktop: Story = {
export const Variants: Story = {
args: {},
};
export const Mobile: Story = {
args: {},
parameters: {
viewport: mobileViewport("iphone14"),
},
};
const testAddress1 = "0x1F846F6DAE38E1C88D71EAA191760B15f38B7A37";
function Component() {
@@ -65,7 +57,7 @@ function Component() {
return (
<ThirdwebProvider>
<div className="container flex max-w-[1150px] flex-col gap-10 py-10">
<div className="container flex max-w-6xl flex-col gap-10 py-10">
<div className="flex gap-2">
<Checkbox
id="terms1"
@@ -139,8 +131,6 @@ function Component() {
twAccount={accountStub()}
/>
</BadgeContainer>
<Toaster richColors />
</div>
</ThirdwebProvider>
);
@@ -60,7 +60,7 @@ export const ContractNFTPage: React.FC<NftOverviewPageProps> = ({
return (
<div className="flex flex-col gap-6">
<div className="flex flex-col gap-3 xl:flex-row xl:items-center xl:justify-between">
<h2 className="font-semibold text-2xl tracking-tight">Contract NFTs</h2>
<h2 className="font-semibold text-2xl tracking-tight">NFTs</h2>
<div className="grid grid-cols-2 gap-2 md:flex">
{isRevealable && (
<NFTRevealButton contract={contract} twAccount={twAccount} />
@@ -4,33 +4,31 @@ import { UnexpectedValueErrorMessage } from "@/components/blocks/error-fallbacks
import { WalletAddress } from "@/components/blocks/wallet-address";
import { CopyTextButton } from "@/components/ui/CopyTextButton";
import { Spinner } from "@/components/ui/Spinner/Spinner";
import { Badge } from "@/components/ui/badge";
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbSeparator,
} from "@/components/ui/breadcrumb";
import { Button } from "@/components/ui/button";
import { CodeClient } from "@/components/ui/code/code.client";
import { TabButtons } from "@/components/ui/tabs";
import { ToolTipLabel } from "@/components/ui/tooltip";
import { useThirdwebClient } from "@/constants/thirdweb.client";
import { useDashboardRouter } from "@/lib/DashboardRouter";
import { resolveSchemeWithErrorHandler } from "@/lib/resolveSchemeWithErrorHandler";
import type { Account } from "@3rdweb-sdk/react/hooks/useApi";
import {
Box,
ButtonGroup,
Divider,
Flex,
GridItem,
IconButton,
SimpleGrid,
Tooltip,
useBreakpointValue,
} from "@chakra-ui/react";
import { useChainSlug } from "hooks/chains/chainSlug";
import { ChevronLeftIcon, ExternalLinkIcon } from "lucide-react";
import { ExternalLinkIcon } from "lucide-react";
import Link from "next/link";
import { useState } from "react";
import type { ThirdwebContract } from "thirdweb";
import type { NFT, ThirdwebClient, ThirdwebContract } from "thirdweb";
import { getNFT as getErc721NFT } from "thirdweb/extensions/erc721";
import { getNFT as getErc1155NFT } from "thirdweb/extensions/erc1155";
import { useReadContract } from "thirdweb/react";
import { Button, Card } from "tw-components";
import { NFTMediaWithEmptyState } from "tw-components/nft-media";
import { shortenString } from "utils/usedapp-external";
import { NftProperty } from "../components/nft-property";
import { useNFTDrawerTabs } from "./useNftDrawerTabs";
@@ -66,7 +64,6 @@ export const TokenIdPage: React.FC<TokenIdPageProps> = ({
twAccount,
}) => {
const [tab, setTab] = useState("Details");
const isMobile = useBreakpointValue({ base: true, md: false });
const router = useDashboardRouter();
const chainId = contract.chain.id;
const chainSlug = useChainSlug(chainId || 1);
@@ -89,15 +86,6 @@ export const TokenIdPage: React.FC<TokenIdPageProps> = ({
},
);
const tokenURIHttpLink = resolveSchemeWithErrorHandler({
client,
uri: nft?.tokenURI,
});
const nftImageLink = resolveSchemeWithErrorHandler({
client,
uri: nft?.metadata.image,
});
if (isPending) {
return (
<div className="flex h-[400px] items-center justify-center">
@@ -123,230 +111,218 @@ export const TokenIdPage: React.FC<TokenIdPageProps> = ({
: undefined,
};
return (
<div>
<div className="flex flex-col gap-6 lg:flex-row">
<div className="relative aspect-square w-full shrink-0 self-start overflow-hidden bg-card lg:w-[350px]">
{/* border */}
<div className="absolute inset-0 z-0 rounded-lg border" />
{/* media */}
<NFTMediaWithEmptyState metadata={nftMetadata} className="z-10" />
</div>
<div className="flex w-full flex-col gap-6">
{/* breadcrumb + title */}
<div>
<Breadcrumb className="mb-3">
<BreadcrumbList>
<BreadcrumbItem>
<BreadcrumbLink
href={`/${chainSlug}/${contract.address}/nfts`}
onClick={(e) => {
e.preventDefault();
router.push(`/${chainSlug}/${contract.address}/nfts`);
}}
>
NFTs
</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbLink>#{tokenId}</BreadcrumbLink>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
<div className="flex flex-col gap-1.5">
<NFTName value={nft.metadata.name} />
{nft.metadata?.description && (
<NFTDescription value={nft.metadata.description} />
)}
</div>
</div>
<TabButtons
tabClassName="!text-sm"
tabs={[
{
name: "Details",
onClick: () => setTab("Details"),
isActive: tab === "Details",
isDisabled: false,
},
...tabs.map((tb) => ({
name: tb.title,
onClick: () => setTab(tb.title),
isActive: tab === tb.title,
isDisabled: tb.isDisabled,
toolTip: tb.isDisabled ? tb.disabledText : undefined,
})),
].sort((a, b) => (a.isDisabled ? 1 : b.isDisabled ? -1 : 0))}
/>
{/* tab contents */}
{tab === "Details" && <NFTDetailsTab nft={nft} client={client} />}
{tabs.map((tb) => {
return (
tb.title === tab && (
<div key={tb.title} className="w-full">
{tb.children}
</div>
)
);
})}
</div>
</div>
</div>
);
};
function NFTDetailsTab(props: {
nft: NFT;
client: ThirdwebClient;
}) {
const { nft, client } = props;
const properties = nft.metadata.attributes || nft.metadata.properties;
return (
<Flex flexDir={{ base: "column", lg: "row" }} gap={6}>
<Card h="full" position="relative" minH="100px">
<Box w="50px" position="absolute" zIndex={1000} top={6} left={6}>
{/* TODO - replace this with breadcrumbs */}
<Card p={1} bgColor="backgroundCardHighlight">
<IconButton
w="inherit"
variant="ghost"
onClick={() =>
router.push(`/${chainSlug}/${contract.address}/nfts`)
<div className="flex flex-col gap-8">
{/* common */}
<div className="flex flex-col gap-3">
<div className="grid grid-cols-1 items-center gap-x-8 gap-y-4 lg:grid-cols-[1fr_2fr]">
{/* token id */}
<div>
<p className="mb-1 text-muted-foreground text-sm">Token ID</p>
<CopyTextButton
textToCopy={nft.id?.toString()}
textToShow={
nft.id?.toString().length > 8
? `${nft.id.toString().slice(0, 4)}...${nft.id.toString().slice(-4)}`
: nft.id?.toString()
}
aria-label="Back"
icon={<ChevronLeftIcon className="size-6" />}
>
Back
</IconButton>
</Card>
</Box>
<NFTMediaWithEmptyState
metadata={nftMetadata}
width={isMobile ? "100%" : "300px"}
height={isMobile ? "100%" : "300px"}
/>
</Card>
<Flex flexDir="column" gap={6} w="full" px={2}>
<Flex flexDir="column" gap={1.5}>
<NFTName value={nft.metadata.name} />
{nft.metadata?.description && (
<NFTDescription value={nft.metadata.description} />
tooltip="Token ID"
copyIconPosition="right"
className="min-w-16 justify-between bg-card"
/>
</div>
{/* owner */}
{nft.owner && (
<div>
<p className="mb-1 text-muted-foreground text-sm">Owner</p>
<WalletAddress
address={nft.owner}
className="h-auto rounded-full border bg-card px-2 py-1 pr-4 text-foreground text-sm"
/>
</div>
)}
</Flex>
<Flex flexDir="column" gap={{ base: 0, md: 4 }}>
<Box
w="full"
overflow={{ base: "auto", md: "hidden" }}
pb={{ base: 4, md: 0 }}
>
<ButtonGroup size="sm" variant="ghost" spacing={2}>
<Button
type="button"
isActive={tab === "Details"}
_active={{
bg: "bgBlack",
color: "bgWhite",
}}
rounded="lg"
onClick={() => setTab("Details")}
{/* type */}
<div>
<p className="mb-1 text-muted-foreground text-sm">Token Standard</p>
<Badge
variant="outline"
className="bg-card py-1 font-normal text-sm"
>
{nft.type}
</Badge>
</div>
{/* supply */}
{nft.type !== "ERC721" && (
<div className="w-full">
<p className="mb-1 text-muted-foreground text-sm">Supply</p>
<ToolTipLabel
label={nft.supply.toLocaleString("en-US")}
contentClassName="text-sm break-all"
>
Details
</Button>
{tabs.map((tb) => (
<Tooltip
key={tb.title}
p={0}
bg="transparent"
boxShadow="none"
label={
tb.isDisabled ? (
<Card py={2} px={4} bgColor="backgroundHighlight">
<p>{tb.disabledText}</p>
</Card>
) : (
""
)
}
>
<Button
isDisabled={tb.isDisabled}
type="button"
isActive={tab === tb.title}
_active={{
bg: "bgBlack",
color: "bgWhite",
}}
rounded="lg"
onClick={() => setTab(tb.title)}
>
{tb.title}
</Button>
</Tooltip>
))}
</ButtonGroup>
</Box>
<Divider />
</Flex>
<p className="max-w-[200px] truncate text-sm">
{nft.supply.toLocaleString("en-US")}
</p>
</ToolTipLabel>
</div>
)}
{tab === "Details" && (
<Flex flexDir="column" gap={4}>
<Card as={Flex} flexDir="column" gap={3}>
<SimpleGrid rowGap={3} columns={12} placeItems="center left">
<GridItem colSpan={4}>
<p className="font-semibold">Token ID</p>
</GridItem>
<GridItem colSpan={8}>
<CopyTextButton
textToCopy={nft.id?.toString()}
textToShow={
nft.id?.toString().length > 8
? `${nft.id.toString().slice(0, 4)}...${nft.id.toString().slice(-4)}`
: nft.id?.toString()
}
tooltip="Token ID"
copyIconPosition="right"
/>
</GridItem>
{/* token uri */}
<div>
<p className="mb-1 text-muted-foreground text-sm">Token URI</p>
<IPFSLinkGroup
ipfsLink={nft.tokenURI}
tooltip="The URI of this NFT"
client={client}
httpsLinkTooltip="View URI"
/>
</div>
{nft.owner && (
<>
<GridItem colSpan={4}>
<p className="font-semibold">Owner</p>
</GridItem>
<GridItem colSpan={8}>
<WalletAddress address={nft.owner} />
</GridItem>
</>
)}
<GridItem colSpan={4}>
<p className="font-semibold">Token Standard</p>
</GridItem>
<GridItem colSpan={8}>{nft.type}</GridItem>
{nft.type !== "ERC721" && (
<>
<GridItem colSpan={4}>
<p className="font-semibold">Supply</p>
</GridItem>
<GridItem colSpan={8}>
<p>{nft.supply.toLocaleString("en-US")}</p>
</GridItem>
</>
)}
<GridItem colSpan={4}>
<p className="font-semibold">Token URI</p>
</GridItem>
<GridItem
colSpan={8}
className="flex flex-row items-center gap-1"
>
<CopyTextButton
textToCopy={nft.tokenURI}
textToShow={shortenString(nft.tokenURI)}
tooltip="The URI of this NFT"
copyIconPosition="right"
/>
{tokenURIHttpLink && (
<Button variant="ghost" size="sm">
<Link href={tokenURIHttpLink} target="_blank">
<ExternalLinkIcon className="size-4" />
</Link>
</Button>
)}
</GridItem>
{nft.metadata.image && (
<>
<GridItem colSpan={4}>
<p className="font-semibold">Media URI</p>
</GridItem>
<GridItem
colSpan={8}
className="flex flex-row items-center gap-1"
>
<CopyTextButton
textToCopy={nft.metadata.image}
textToShow={shortenString(nft.metadata.image)}
tooltip="The media URI of this NFT"
copyIconPosition="right"
/>
{nftImageLink && (
<Button variant="ghost" size="sm">
<Link href={nftImageLink} target="_blank">
<ExternalLinkIcon className="size-4" />
</Link>
</Button>
)}
</GridItem>
</>
)}
</SimpleGrid>
</Card>
{properties ? (
<Card as={Flex} flexDir="column" gap={4}>
<p className="font-semibold">Attributes</p>
{Array.isArray(properties) &&
String(properties[0]?.value) !== "undefined" ? (
<SimpleGrid columns={{ base: 2, md: 4 }} gap={2}>
{/* biome-ignore lint/suspicious/noExplicitAny: FIXME */}
{properties.map((property: any, idx) => (
// biome-ignore lint/suspicious/noArrayIndexKey: FIXME
<NftProperty key={idx} property={property} />
))}
</SimpleGrid>
) : (
<CodeClient
code={JSON.stringify(properties, null, 2) || ""}
lang="json"
/>
)}
</Card>
) : null}
</Flex>
)}
{tabs.map((tb) => {
return (
tb.title === tab && (
<Flex key={tb.title} px={0} w="full">
{tb.children}
</Flex>
)
);
})}
</Flex>
</Flex>
{/* media uri */}
{nft.metadata.image && (
<div>
<p className="mb-1 text-muted-foreground text-sm">Media URI</p>
<IPFSLinkGroup
ipfsLink={nft.metadata.image}
tooltip="The media URI of this NFT"
client={client}
httpsLinkTooltip="View Media"
/>
</div>
)}
</div>
</div>
{/* attributes */}
{properties ? (
<div>
<h3 className="mb-2 flex items-center gap-2 font-medium text-base">
Attributes
{Array.isArray(properties) && properties.length > 0 && (
<Badge
variant="outline"
className="bg-card px-2 text-muted-foreground"
>
{properties.length}
</Badge>
)}
</h3>
<div>
{Array.isArray(properties) &&
String(properties[0]?.value) !== "undefined" ? (
<div className="grid grid-cols-2 gap-3 lg:grid-cols-3">
{/* biome-ignore lint/suspicious/noExplicitAny: FIXME */}
{properties.map((property: any, idx) => (
// biome-ignore lint/suspicious/noArrayIndexKey: FIXME
<NftProperty key={idx} property={property} />
))}
</div>
) : (
<CodeClient
code={JSON.stringify(properties, null, 2) || ""}
lang="json"
/>
)}
</div>
</div>
) : null}
</div>
);
};
}
function NFTName(props: {
value: unknown;
}) {
if (typeof props.value === "string") {
return (
<h2 className="font-semibold text-lg tracking-tight"> {props.value}</h2>
<h2 className="font-bold text-3xl tracking-tight"> {props.value}</h2>
);
}
@@ -360,11 +336,49 @@ function NFTName(props: {
);
}
function IPFSLinkGroup(props: {
ipfsLink: string;
tooltip: string;
client: ThirdwebClient;
httpsLinkTooltip: string;
}) {
const httpLink = resolveSchemeWithErrorHandler({
client: props.client,
uri: props.ipfsLink,
});
return (
<div className="flex flex-row items-center gap-1.5">
<CopyTextButton
textToCopy={props.ipfsLink}
textToShow={props.ipfsLink}
tooltip={props.tooltip}
copyIconPosition="right"
className="max-w-[200px] truncate bg-card"
/>
{httpLink && (
<ToolTipLabel label={props.httpsLinkTooltip}>
<Button
variant="outline"
size="sm"
asChild
className="size-[34px] rounded-full bg-card p-0 text-muted-foreground"
>
<Link href={httpLink} target="_blank">
<ExternalLinkIcon className="size-3" />
</Link>
</Button>
</ToolTipLabel>
)}
</div>
);
}
function NFTDescription(props: {
value: unknown;
}) {
if (typeof props.value === "string") {
return <p className="text-muted-foreground"> {props.value}</p>;
return <p className="text-muted-foreground text-sm"> {props.value}</p>;
}
return (
@@ -68,7 +68,6 @@ export function useNFTDrawerTabs({
const hasERC1155ClaimConditions = (() => {
return [
// reads
ERC1155Ext.isGetClaimConditionByIdSupported(functionSelectors),
ERC1155Ext.isGetClaimConditionsSupported(functionSelectors),
ERC1155Ext.isGetActiveClaimConditionSupported(functionSelectors),
// writes
@@ -77,6 +76,10 @@ export function useNFTDrawerTabs({
].every(Boolean);
})();
const isERC1155Multiphase = (() => {
return ERC1155Ext.isGetClaimConditionByIdSupported(functionSelectors);
})();
const isBurnable = (() => {
if (isERC721) {
return ERC721Ext.isBurnSupported(functionSelectors);
@@ -130,6 +133,7 @@ export function useNFTDrawerTabs({
tokenId={tokenId}
isColumn
twAccount={twAccount}
isMultiphase={isERC1155Multiphase}
/>
),
},
@@ -1,5 +1,3 @@
import { Card } from "@/components/ui/card";
interface NftPropertyProps {
// biome-ignore lint/suspicious/noExplicitAny: FIXME
property: any;
@@ -7,17 +5,17 @@ interface NftPropertyProps {
export const NftProperty: React.FC<NftPropertyProps> = ({ property }) => {
return (
<Card className="flex flex-col gap-2 p-3">
<div className="rounded-lg border bg-card p-3 text-sm">
{property?.trait_type && (
<p className="text-center text-link-foreground text-sm leading-[1.2]">
<p className="mb-0.5 text-muted-foreground text-sm">
{property?.trait_type}
</p>
)}
<p className="text-center text-muted-foreground text-sm">
<p className="text-foreground text-sm">
{typeof property?.value === "object"
? JSON.stringify(property?.value || {})
: property?.value}
</p>
</Card>
</div>
);
};
@@ -35,7 +35,7 @@ export const SupplyCards: React.FC<SupplyCardsProps> = ({ contract }) => {
);
return (
<div className="flex flex-row gap-3 md:gap-6 [&>*]:grow">
<div className="grid grid-cols-1 gap-4 lg:grid-cols-3">
<StatCard
value={realTotalSupply.toString()}
label="Total Supply"
@@ -1,12 +1,12 @@
import type { ThirdwebContract } from "thirdweb";
import { AnalyticsOverview } from "./components/Analytics";
import { TokenDetailsCard } from "../tokens/components/supply";
import { ContractAnalyticsOverviewCard } from "./components/Analytics";
import { BuildYourApp } from "./components/BuildYourApp";
import { ContractChecklist } from "./components/ContractChecklist";
import { LatestEvents } from "./components/LatestEvents";
import { MarketplaceDetails } from "./components/MarketplaceDetails";
import { NFTDetails } from "./components/NFTDetails";
import { PermissionsTable } from "./components/PermissionsTable";
import { TokenDetails } from "./components/TokenDetails";
interface ContractOverviewPageProps {
contract: ThirdwebContract;
@@ -38,8 +38,8 @@ export const ContractOverviewPage: React.FC<ContractOverviewPageProps> = ({
publishedBy,
}) => {
return (
<div className="flex flex-col gap-8 lg:flex-row">
<div className="flex flex-col gap-16">
<div className="flex flex-col gap-10 lg:flex-row lg:gap-8">
<div className="flex grow flex-col gap-10 overflow-hidden">
<ContractChecklist
isErc721={isErc721}
isErc1155={isErc1155}
@@ -50,7 +50,7 @@ export const ContractOverviewPage: React.FC<ContractOverviewPageProps> = ({
/>
{isAnalyticsSupported && (
<AnalyticsOverview
<ContractAnalyticsOverviewCard
contractAddress={contract.address}
chainId={contract.chain.id}
trackingCategory={TRACKING_CATEGORY}
@@ -77,7 +77,7 @@ export const ContractOverviewPage: React.FC<ContractOverviewPageProps> = ({
/>
)}
{isErc20 && <TokenDetails contract={contract} />}
{isErc20 && <TokenDetailsCard contract={contract} />}
<LatestEvents
contract={contract}
@@ -12,19 +12,16 @@ import { ArrowRightIcon } from "lucide-react";
import Link from "next/link";
import { useMemo, useState } from "react";
interface AnalyticsOverviewProps {
chainId: number;
contractAddress: string;
trackingCategory: string;
chainSlug: string;
function getDayKey(date: Date) {
return date.toISOString().split("T")[0];
}
export const AnalyticsOverview: React.FC<AnalyticsOverviewProps> = ({
chainId,
contractAddress,
trackingCategory,
chainSlug,
}) => {
export function ContractAnalyticsOverviewCard(props: {
contractAddress: string;
chainId: number;
trackingCategory: string;
chainSlug: string;
}) {
const trackEvent = useTrack();
const [startDate] = useState(
(() => {
@@ -35,55 +32,27 @@ export const AnalyticsOverview: React.FC<AnalyticsOverviewProps> = ({
);
const [endDate] = useState(new Date());
return (
<div className="relative">
<div className="mb-4 flex items-center justify-between gap-4">
<h2 className="font-semibold text-2xl tracking-tight">Analytics</h2>
<Button
asChild
className="gap-1"
size="sm"
variant="outline"
onClick={() => {
trackEvent({
category: trackingCategory,
action: "click",
label: "view_all_analytics",
});
}}
>
<Link href={`/${chainSlug}/${contractAddress}/analytics`}>
<span>View All</span>
<ArrowRightIcon className="size-4" />
</Link>
</Button>
</div>
const wallets = useContractUniqueWalletAnalytics({
chainId: props.chainId,
contractAddress: props.contractAddress,
startDate,
endDate,
});
<OverviewAnalytics
chainId={chainId}
contractAddress={contractAddress}
endDate={endDate}
startDate={startDate}
/>
</div>
);
};
const transactions = useContractTransactionAnalytics({
chainId: props.chainId,
contractAddress: props.contractAddress,
startDate,
endDate,
});
type ChartProps = {
contractAddress: string;
chainId: number;
startDate: Date;
endDate: Date;
};
const events = useContractEventAnalytics({
chainId: props.chainId,
contractAddress: props.contractAddress,
startDate,
endDate,
});
function getDayKey(date: Date) {
return date.toISOString().split("T")[0];
}
function OverviewAnalytics(props: ChartProps) {
const wallets = useContractUniqueWalletAnalytics(props);
const transactions = useContractTransactionAnalytics(props);
const events = useContractEventAnalytics(props);
const isPending =
wallets.isPending || transactions.isPending || events.isPending;
@@ -135,7 +104,32 @@ function OverviewAnalytics(props: ChartProps) {
data={mergedData || []}
isPending={isPending}
showLegend
chartClassName="aspect-[1.5] lg:aspect-[3.5]"
chartClassName="aspect-[1.5] lg:aspect-[3]"
customHeader={
<div className="flex items-center justify-between gap-4 border-b p-6 py-4">
<h2 className="font-semibold text-xl tracking-tight">Analytics</h2>
<Button
asChild
className="gap-2 bg-background text-muted-foreground"
size="sm"
variant="outline"
onClick={() => {
trackEvent({
category: props.trackingCategory,
action: "click",
label: "view_all_analytics",
});
}}
>
<Link
href={`/${props.chainSlug}/${props.contractAddress}/analytics`}
>
<span>View All</span>
<ArrowRightIcon className="size-4" />
</Link>
</Button>
</div>
}
/>
);
}
@@ -1,79 +1,41 @@
"use client";
import { TrackedLinkTW } from "@/components/ui/tracked-link";
import { ReactIcon } from "../../../../../../../components/icons/brand-icons/ReactIcon";
import { TypeScriptIcon } from "../../../../../../../components/icons/brand-icons/TypeScriptIcon";
import {
Flex,
GridItem,
LinkBox,
LinkOverlay,
SimpleGrid,
} from "@chakra-ui/react";
import { ChakraNextImage as Image } from "components/Image";
import { PRODUCTS } from "components/product-pages/common/nav/data";
import { Card } from "tw-components/card";
import { TrackedLink, type TrackedLinkProps } from "tw-components/link";
import { Text } from "tw-components/text";
const RENDERED_PRODUCTS = ["sdk", "storage", "ui-components", "auth"];
interface BuildYourAppProps {
trackingCategory: TrackedLinkProps["category"];
export function BuildYourApp(props: {
trackingCategory: string;
contractAddress: string;
chainSlug: string;
}
export const BuildYourApp: React.FC<BuildYourAppProps> = ({
trackingCategory,
contractAddress,
chainSlug,
}) => {
}) {
return (
<Card
px={{ base: 4, md: 8 }}
py={{ base: 6, md: 8 }}
as={LinkBox}
transition="all 0.2s"
_hover={{ borderColor: "whiteAlpha.300" }}
>
<SimpleGrid {...{ columns: { base: 1, md: 2 } }} gap={8}>
<GridItem as={Flex} direction="column" gap={4}>
<h2 className="font-semibold text-2xl tracking-tight">
Build your app
</h2>
<Text size="body.md">
<LinkOverlay
as={TrackedLink}
category={trackingCategory}
label="build_your_app"
href={`/${chainSlug}/${contractAddress}/code`}
color="blue.500"
>
Learn more
</LinkOverlay>{" "}
about how you can use thirdweb tools to build apps on top of this
contract.
</Text>
</GridItem>
<GridItem as={Flex} align="center" justify="end" gap={3}>
{RENDERED_PRODUCTS.map((p) => {
const product = PRODUCTS.find((item) => item.label === p);
return (
product?.icon && (
<Flex
key={product.name}
rounded="full"
bg="#0E0E10"
align="center"
justify="center"
w={14}
h={14}
>
<Image boxSize={7} src={product.icon} alt={product.name} />
</Flex>
)
);
})}
</GridItem>
</SimpleGrid>
</Card>
<div className="relative flex flex-col justify-between gap-4 rounded-lg border bg-card p-6 hover:border-active-border lg:flex-row">
{/* left */}
<div className="max-w-sm">
<h2 className="mb-2 font-semibold text-xl leading-none tracking-tight">
Build your app
</h2>
<TrackedLinkTW
category={props.trackingCategory}
label="build_your_app"
href={`/${props.chainSlug}/${props.contractAddress}/code`}
className="block text-balance text-muted-foreground text-sm before:absolute before:inset-0"
>
Learn more about how you can use thirdweb tools to build apps on top
of this contract
</TrackedLinkTW>{" "}
</div>
{/* right */}
<div className="flex items-center justify-end gap-3">
{[TypeScriptIcon, ReactIcon].map((Icon, i) => {
return (
// biome-ignore lint/suspicious/noArrayIndexKey: <explanation>
<div key={i} className="rounded-full border bg-background p-3">
<Icon className="size-5 text-muted-foreground" />
</div>
);
})}
</div>
</div>
);
};
}
@@ -0,0 +1,105 @@
import type { Meta, StoryObj } from "@storybook/react";
import { LatestEventsUI } from "./LatestEvents";
const meta: Meta<typeof LatestEventsUI> = {
title: "Contracts/Overview/LatestEvents",
component: LatestEventsUI,
decorators: [
(Story) => (
<div className="container max-w-4xl py-10">
<Story />
</div>
),
],
args: {
allEvents: [],
autoUpdate: false,
eventsHref: "/ethereum/0x123456789/events",
trackingCategory: "test-category",
},
};
function eventStub(eventName: string) {
return {
logIndex: 0,
eventName,
address: "0x",
args: [],
};
}
const txStub =
"0x9ef44ea701637e0f7c42666b6f99f4aad9aa6b9b51995cd22df1507288e51427";
export default meta;
type Story = StoryObj<typeof LatestEventsUI>;
export const Loading: Story = {
args: {
allEvents: [],
autoUpdate: true,
eventsHref: "/ethereum/0x123456789/events",
},
};
export const NoEvents: Story = {
args: {
allEvents: [],
autoUpdate: false,
eventsHref: "/ethereum/0x123456789/events",
},
};
export const WithEvents: Story = {
args: {
allEvents: [
{
transactionHash: txStub,
events: [eventStub("Transfer"), eventStub("Approval")],
},
{
transactionHash: txStub,
events: [eventStub("Mint")],
},
{
transactionHash: txStub,
events: [
eventStub("Transfer"),
eventStub("Burn"),
eventStub("Approval"),
],
},
],
autoUpdate: false,
eventsHref: "/ethereum/0x123456789/events",
},
};
export const LiveWithEvents: Story = {
args: {
allEvents: [
{
transactionHash: txStub,
events: [eventStub("Transfer"), eventStub("Approval")],
},
{
transactionHash: txStub,
events: [eventStub("Mint")],
},
{
transactionHash: txStub,
events: [
eventStub("Transfer"),
eventStub("Burn"),
eventStub("Approval"),
],
},
{
transactionHash: txStub,
events: [eventStub("RoleGranted"), eventStub("OwnershipTransferred")],
},
],
autoUpdate: true,
eventsHref: "/ethereum/0x123456789/events",
},
};
@@ -1,192 +1,157 @@
"use client";
import { CopyTextButton } from "@/components/ui/CopyTextButton";
import { Spinner } from "@/components/ui/Spinner/Spinner";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { TrackedLinkTW } from "@/components/ui/tracked-link";
import {
type InternalTransaction,
useActivity,
} from "@3rdweb-sdk/react/hooks/useActivity";
import {
Box,
ButtonGroup,
Flex,
List,
SimpleGrid,
Spinner,
Tooltip,
} from "@chakra-ui/react";
import { useClipboard } from "hooks/useClipboard";
import { CopyIcon } from "lucide-react";
import { useState } from "react";
import { toast } from "sonner";
import { ArrowRightIcon } from "lucide-react";
import Link from "next/link";
import type { ThirdwebContract } from "thirdweb";
import {
Button,
Card,
Heading,
LinkButton,
Text,
TrackedLink,
type TrackedLinkProps,
} from "tw-components";
import { shortenString } from "utils/usedapp-external";
interface LatestEventsProps {
trackingCategory: TrackedLinkProps["category"];
export function LatestEvents(props: {
trackingCategory: string;
contract: ThirdwebContract;
chainSlug: string;
}
export const LatestEvents: React.FC<LatestEventsProps> = ({
contract,
trackingCategory,
chainSlug,
}) => {
const [autoUpdate] = useState(true);
const eventsHref = `/${chainSlug}/${contract.address}/events`;
const allEvents = useActivity(contract, autoUpdate);
}) {
const autoUpdate = true;
const allEvents = useActivity(props.contract, autoUpdate);
return (
<Flex gap={6} flexDirection="column">
<Flex align="center" justify="space-between" w="full">
<h2 className="font-semibold text-2xl tracking-tight">Latest Events</h2>
<TrackedLink
category={trackingCategory}
label="view_all_events"
color="blue.400"
_light={{
color: "blue.600",
}}
gap={4}
href={eventsHref}
>
View all -&gt;
</TrackedLink>
</Flex>
<Card p={0} overflow="hidden">
<SimpleGrid
gap={2}
columns={9}
borderBottomWidth="1px"
borderColor="borderColor"
padding={4}
bg="blackAlpha.50"
_dark={{ bg: "whiteAlpha.50" }}
>
<Heading gridColumn="span 3" size="label.md">
Transaction Hash
</Heading>
<Heading gridColumn="span 6" size="label.md">
Events
</Heading>
</SimpleGrid>
<List overflow="auto">
{allEvents.length === 0 ? (
<div className="flex items-center justify-center py-4">
<Flex align="center" gap={2}>
{autoUpdate && <Spinner size="sm" speed="0.69s" />}
<Text size="body.md" fontStyle="italic">
{autoUpdate ? "listening for events" : "no events to show"}
</Text>
</Flex>
</div>
) : null}
<div>
{allEvents?.slice(0, 3).map((e) => (
<EventsFeedItem
key={e.transactionHash}
transaction={e}
contractAddress={contract.address}
chainSlug={chainSlug}
/>
))}
</div>
</List>
</Card>
</Flex>
<LatestEventsUI
allEvents={allEvents}
autoUpdate={autoUpdate}
eventsHref={`/${props.chainSlug}/${props.contract.address}/events`}
trackingCategory={props.trackingCategory}
/>
);
};
interface EventsFeedItemProps {
transaction: InternalTransaction;
contractAddress: string;
chainSlug: string;
}
const EventsFeedItem: React.FC<EventsFeedItemProps> = ({
transaction,
contractAddress,
chainSlug,
}) => {
const { onCopy } = useClipboard(transaction.transactionHash);
const eventsHref = `/${chainSlug}/${contractAddress}/events`;
export function LatestEventsUI(props: {
allEvents: Pick<InternalTransaction, "transactionHash" | "events">[];
autoUpdate: boolean;
eventsHref: string;
trackingCategory: string;
}) {
const { allEvents, autoUpdate, eventsHref } = props;
return (
<div>
<SimpleGrid
columns={9}
gap={2}
as="li"
borderBottomWidth="1px"
borderColor="borderColor"
padding={4}
overflow="hidden"
alignItems="center"
_last={{ borderBottomWidth: 0 }}
>
<Box gridColumn="span 3">
<div className="flex flex-row items-center gap-3">
<Tooltip
p={0}
bg="transparent"
boxShadow="none"
label={
<Card py={2} px={4} bgColor="backgroundHighlight">
<Text size="label.sm">
Copy transaction hash to clipboard
</Text>
</Card>
}
<div className="rounded-lg border bg-card">
{/* header */}
<div className="flex w-full items-center justify-between border-b px-6 py-4">
<div className="flex items-center gap-3">
<h2 className="font-semibold text-xl tracking-tight">
Latest Events
</h2>
{autoUpdate && (
<Badge
variant="outline"
className="hidden gap-2 bg-background text-sm lg:flex"
>
<Button
size="sm"
bg="transparent"
onClick={() => {
onCopy();
toast.info("Transaction hash copied.");
}}
>
<CopyIcon className="size-3" />
</Button>
</Tooltip>
<Text fontFamily="mono" noOfLines={1}>
{shortenString(transaction.transactionHash)}
</Text>
</div>
</Box>
<ButtonGroup
size="sm"
variant="outline"
gridColumn="span 6"
flexWrap="wrap"
gap={2}
spacing={0}
>
{transaction.events.slice(0, 2).map((e, idx) => (
// biome-ignore lint/suspicious/noArrayIndexKey: FIXME
<LinkButton key={idx} href={`${eventsHref}?event=${e.eventName}`}>
<Text color="whiteBg" fontWeight="600" isTruncated>
{e.eventName}
</Text>
</LinkButton>
))}
{transaction.events.length > 2 && (
<Button as="span" pointerEvents="none">
+ {transaction.events.length - 2}
</Button>
<span className="!pointer-events-auto relative flex size-2">
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-sky-400 opacity-75" />
<span className="relative inline-flex size-2 rounded-full bg-primary" />
</span>
Live
</Badge>
)}
</ButtonGroup>
</SimpleGrid>
</div>
<Button asChild variant="outline" size="sm" className="bg-background">
<TrackedLinkTW
category={props.trackingCategory}
label="view_all_events"
href={eventsHref}
className="flex items-center gap-2 text-muted-foreground text-sm"
>
View all <ArrowRightIcon className="size-4" />
</TrackedLinkTW>
</Button>
</div>
{/* table */}
<TableContainer className="w-full border-none">
<Table>
<TableHeader>
<TableRow>
<TableHead className="w-52">Transaction Hash</TableHead>
<TableHead>Events</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{allEvents.slice(0, 3).map((transaction) => {
return (
<TableRow key={transaction.transactionHash}>
<TableCell>
<CopyTextButton
textToShow={shortenString(transaction.transactionHash)}
textToCopy={transaction.transactionHash}
tooltip="Copy transaction hash"
copyIconPosition="left"
variant="ghost"
className="-translate-x-2 font-mono"
/>
</TableCell>
<TableCell>
<div className="flex w-max flex-wrap gap-2">
{transaction.events.slice(0, 3).map((e) => (
<Button
key={e.logIndex + e.address + e.eventName}
variant="outline"
size="sm"
className="h-auto rounded-full py-1"
asChild
>
<Link href={`${eventsHref}?event=${e.eventName}`}>
{e.eventName}
</Link>
</Button>
))}
{transaction.events.length > 3 && (
<Button
variant="outline"
size="sm"
className="h-auto rounded-full py-1 hover:bg-transparent"
asChild
>
<div>+ {transaction.events.length - 3}</div>
</Button>
)}
</div>
</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
</TableContainer>
{allEvents.length === 0 && (
<div className="flex h-48 items-center justify-center text-sm">
{autoUpdate ? (
<div className="flex items-center gap-2">
<Spinner className="size-4" />
<p>Listening for events</p>
</div>
) : (
<p>No events found</p>
)}
</div>
)}
</div>
);
};
}
@@ -2,10 +2,12 @@
import { WalletAddress } from "@/components/blocks/wallet-address";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { SkeletonContainer } from "@/components/ui/skeleton";
import { TrackedLinkTW } from "@/components/ui/tracked-link";
import { ArrowRightIcon } from "lucide-react";
import { useMemo } from "react";
import type { ThirdwebContract } from "thirdweb";
import { type ThirdwebContract, ZERO_ADDRESS } from "thirdweb";
import {
type DirectListing,
type EnglishAuction,
@@ -79,28 +81,37 @@ const DirectListingCards: React.FC<ListingCardsSectionProps> = ({
}
return (
<>
<div className="flex w-full items-center justify-between">
<h2 className="font-semibold text-2xl tracking-tight">
Direct Listing
<div className="rounded-lg border bg-card">
<div className="flex w-full items-center justify-between border-b px-6 py-4">
<h2 className="font-semibold text-xl tracking-tight">
Direct Listings
</h2>
<TrackedLinkTW
category={trackingCategory}
label="view_all_direct_listings"
className="text-link-foreground hover:text-foreground"
href={directListingsHref}
<Button
asChild
variant="outline"
size="sm"
className="gap-2 bg-background text-muted-foreground"
>
View all -&gt;
</TrackedLinkTW>
<TrackedLinkTW
category={trackingCategory}
label="view_all_direct_listings"
href={directListingsHref}
>
View all <ArrowRightIcon className="size-4" />
</TrackedLinkTW>
</Button>
</div>
<ListingCards
listings={listings}
isPending={listingsQuery.isPending}
trackingCategory={trackingCategory}
chainSlug={chainSlug}
contractAddress={contract.address}
/>
</>
<div className="p-6">
<ListingCards
listings={listings}
isPending={listingsQuery.isPending}
trackingCategory={trackingCategory}
chainSlug={chainSlug}
contractAddress={contract.address}
/>
</div>
</div>
);
};
@@ -142,28 +153,36 @@ const EnglishAuctionCards: React.FC<ListingCardsSectionProps> = ({
}
return (
<>
<div className="flex w-full items-center justify-between">
<h2 className="font-semibold text-2xl tracking-tight">
<div className="rounded-lg border bg-card">
<div className="flex w-full items-center justify-between border-b px-6 py-4">
<h2 className="font-semibold text-xl tracking-tight">
English Auctions
</h2>
<TrackedLinkTW
category={trackingCategory}
label="view_all_english_auctions"
className="text-link-foreground hover:text-foreground"
href={englishAuctionsHref}
<Button
asChild
variant="outline"
size="sm"
className="gap-2 bg-background text-muted-foreground"
>
View all -&gt;
</TrackedLinkTW>
<TrackedLinkTW
category={trackingCategory}
label="view_all_english_auctions"
href={englishAuctionsHref}
>
View all <ArrowRightIcon className="size-4" />
</TrackedLinkTW>
</Button>
</div>
<ListingCards
listings={auctions}
isPending={auctionsQuery.isPending}
trackingCategory={trackingCategory}
chainSlug={chainSlug}
contractAddress={contract.address}
/>
</>
<div className="p-6">
<ListingCards
listings={auctions}
isPending={auctionsQuery.isPending}
trackingCategory={trackingCategory}
chainSlug={chainSlug}
contractAddress={contract.address}
/>
</div>
</div>
);
};
@@ -183,8 +202,7 @@ export const MarketplaceDetails: React.FC<MarketplaceDetailsVersionProps> = ({
chainSlug,
}) => {
return (
<div className="flex flex-col gap-6">
<h2 className="font-semibold text-2xl tracking-tight">Listings</h2>
<div className="flex flex-col gap-10">
<ListingStatsV3
contract={contract}
hasDirectListings={hasDirectListings}
@@ -219,6 +237,8 @@ const dummyMetadata: (idx: number) => ListingData = (idx) => ({
supply: BigInt(1),
tokenURI: "",
type: "ERC721",
tokenAddress: ZERO_ADDRESS,
chainId: 1,
},
currencyValuePerToken: {
decimals: 18,
@@ -226,6 +246,8 @@ const dummyMetadata: (idx: number) => ListingData = (idx) => ({
name: "Ether",
symbol: "ETH",
value: 0n,
tokenAddress: ZERO_ADDRESS,
chainId: 1,
},
creatorAddress: "0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef",
type: "direct-listing",
@@ -235,6 +257,8 @@ const dummyMetadata: (idx: number) => ListingData = (idx) => ({
value: 0n,
displayValue: "0.0",
decimals: 18,
tokenAddress: ZERO_ADDRESS,
chainId: 1,
},
});
@@ -262,12 +286,16 @@ const ListingCards: React.FC<ListingCardsProps> = ({
const englishAuctionsHref = `/${chainSlug}/${contractAddress}/english-auctions`;
return (
<div className="grid grid-cols-2 gap-3 md:grid-cols-3 md:gap-6 max-sm:[&>*:nth-child(n+3)]:hidden">
<div className="grid grid-cols-1 gap-4 md:grid-cols-3 ">
{listings.map((listing, index) => (
<div
className="relative rounded-lg border border-border bg-card transition-colors hover:border-active-border"
className="group hover:-translate-y-0.5 relative flex h-full cursor-pointer flex-col rounded-lg bg-background duration-200 hover:scale-[1.01]"
key={`${listing.creatorAddress}-${index}`}
>
{/* border */}
<div className="absolute inset-0 rounded-lg border border-border" />
{/* image */}
<div className="relative aspect-square w-full overflow-hidden rounded-lg">
{/* Image */}
<SkeletonContainer
@@ -324,21 +352,23 @@ const ListingCards: React.FC<ListingCardsProps> = ({
/>
)}
{/* seller */}
<SkeletonContainer
loadedData={isPending ? undefined : listing.creatorAddress}
skeletonData={listing.creatorAddress}
className="mt-4"
className="mt-4 border-t pt-4"
render={(v) => (
<div>
<p className="text-muted-foreground text-sm">Seller</p>
<p className="text-muted-foreground text-xs">Seller</p>
<WalletAddress
className="relative z-[1] self-start"
className="relative z-[1] self-start text-xs"
address={v}
/>
</div>
)}
/>
{/* price */}
{!isPending && (
<Badge
variant="outline"
@@ -1,27 +1,27 @@
"use client";
import { Flex, useBreakpointValue } from "@chakra-ui/react";
import { Button } from "@/components/ui/button";
import { TrackedLinkTW } from "@/components/ui/tracked-link";
import { ArrowRightIcon } from "lucide-react";
import type { ThirdwebContract } from "thirdweb";
import * as ERC721 from "thirdweb/extensions/erc721";
import * as ERC1155 from "thirdweb/extensions/erc1155";
import { useReadContract } from "thirdweb/react";
import { TrackedLink, type TrackedLinkProps } from "tw-components/link";
import { NFTCards } from "../../_components/NFTCards";
interface NFTDetailsProps {
type NFTDetailsProps = {
contract: ThirdwebContract;
trackingCategory: TrackedLinkProps["category"];
trackingCategory: string;
isErc721: boolean;
chainSlug: string;
}
};
export const NFTDetails: React.FC<NFTDetailsProps> = ({
export function NFTDetails({
contract,
trackingCategory,
isErc721,
chainSlug,
}) => {
const isMobile = useBreakpointValue({ base: true, md: false });
}: NFTDetailsProps) {
const nftsHref = `/${chainSlug}/${contract.address}/nfts`;
const nftQuery = useReadContract(
@@ -36,34 +36,37 @@ export const NFTDetails: React.FC<NFTDetailsProps> = ({
const displayableNFTs =
nftQuery.data
?.filter((token) => token.metadata.image || token.metadata.animation_url)
.slice(0, isMobile ? 2 : 3) || [];
.slice(0, 3) || [];
return displayableNFTs.length === 0 && !nftQuery.isPending ? null : (
<Flex direction="column" gap={{ base: 3, md: 6 }}>
<Flex align="center" justify="space-between" w="full">
<h2 className="font-semibold text-2xl tracking-tight">NFT Details</h2>
<TrackedLink
category={trackingCategory}
label="view_all_nfts"
color="blue.400"
_light={{
color: "blue.600",
}}
gap={4}
href={nftsHref}
>
View all -&gt;
</TrackedLink>
</Flex>
<NFTCards
nfts={displayableNFTs.map((t) => ({
...t,
contractAddress: contract.address,
chainId: contract.chain.id,
}))}
trackingCategory={trackingCategory}
isPending={nftQuery.isPending}
/>
</Flex>
<div className="rounded-lg border bg-card">
{/* header */}
<div className="flex items-center justify-between border-b px-6 py-4">
<h2 className="font-semibold text-xl tracking-tight">NFTs</h2>
<Button variant="outline" asChild className="bg-background" size="sm">
<TrackedLinkTW
category={trackingCategory}
label="view_all_nfts"
href={nftsHref}
className="text-muted-foreground"
>
View all <ArrowRightIcon className="ml-2 size-4 " />
</TrackedLinkTW>
</Button>
</div>
{/* cards */}
<div className="p-6">
<NFTCards
nfts={displayableNFTs.map((t) => ({
...t,
contractAddress: contract.address,
chainId: contract.chain.id,
}))}
trackingCategory={trackingCategory}
isPending={nftQuery.isPending}
/>
</div>
</div>
);
};
}
@@ -0,0 +1,84 @@
import type { Meta, StoryObj } from "@storybook/react";
import { ThirdwebProvider } from "thirdweb/react";
import { PermissionsTableUI } from "./PermissionsTable";
const meta: Meta<typeof PermissionsTableUI> = {
title: "Contracts/Overview/PermissionsTable",
component: PermissionsTableUI,
decorators: [
(Story) => (
<ThirdwebProvider>
<div className="container max-w-4xl py-10">
<Story />
</div>
</ThirdwebProvider>
),
],
args: {
viewMoreLink: "#",
trackingCategory: "test-category",
members: [],
isPending: false,
},
};
export default meta;
type Story = StoryObj<typeof PermissionsTableUI>;
export const Loading: Story = {
args: {
isPending: true,
members: [],
},
};
export const NoMembers: Story = {
args: {
isPending: false,
members: [],
},
};
export const OneMember: Story = {
args: {
isPending: false,
members: [
{
member: "0x1F846F6DAE38E1C88D71EAA191760B15f38B7A37",
roles: ["admin"],
},
],
},
};
export const FiveMembers: Story = {
args: {
isPending: false,
members: [
{
member: "0x1F846F6DAE38E1C88D71EAA191760B15f38B7A37",
roles: ["admin", "minter"],
},
{
member: "0x83Dd93fA5D8343094f850f90B3fb90088C1bB425",
roles: ["transfer"],
},
{
member: "0x1f4202eD5c33d229bCda7B9B6AB3B57caDf423e6",
roles: ["admin", "transfer", "minter"],
},
{
member: "0x6844F0056d4C84CA2a894Fcf85473d75d8252b24",
roles: [
"admin",
"minter",
"transfer",
"metadata",
"burner",
"manager",
"lister",
],
},
],
},
};
@@ -1,36 +1,34 @@
"use client";
import { ToolTipLabel } from "@/components/ui/tooltip";
import { Box, Flex, List, SimpleGrid, Tag } from "@chakra-ui/react";
import { WalletAddress } from "@/components/blocks/wallet-address";
import { Spinner } from "@/components/ui/Spinner/Spinner";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { TrackedLinkTW } from "@/components/ui/tracked-link";
import { getAllRoleMembers } from "contract-ui/hooks/permissions";
import { useClipboard } from "hooks/useClipboard";
import { CopyIcon } from "lucide-react";
import { ArrowRightIcon } from "lucide-react";
import { useMemo } from "react";
import { toast } from "sonner";
import { type ThirdwebContract, ZERO_ADDRESS } from "thirdweb";
import { useReadContract } from "thirdweb/react";
import { Button } from "tw-components/button";
import { Card } from "tw-components/card";
import { Heading } from "tw-components/heading";
import { TrackedLink, type TrackedLinkProps } from "tw-components/link";
import { Text } from "tw-components/text";
import { shortenIfAddress } from "utils/usedapp-external";
interface PermissionsTableProps {
export function PermissionsTable(props: {
contract: ThirdwebContract;
trackingCategory: TrackedLinkProps["category"];
trackingCategory: string;
chainSlug: string;
}
export const PermissionsTable: React.FC<PermissionsTableProps> = ({
contract,
trackingCategory,
chainSlug,
}) => {
}) {
const allRoleMembers = useReadContract(getAllRoleMembers, {
contract,
contract: props.contract,
});
const permissionsHref = `/${chainSlug}/${contract.address}/permissions`;
const members = useMemo(() => {
return (
Object.entries(allRoleMembers.data || {}).reduce(
@@ -52,128 +50,82 @@ export const PermissionsTable: React.FC<PermissionsTableProps> = ({
}, [allRoleMembers.data]);
return (
<Flex gap={6} flexDirection="column">
<Flex align="center" justify="space-between" w="full">
<h2 className="font-semibold text-2xl tracking-tight">Permissions</h2>
<TrackedLink
category={trackingCategory}
label="view_all_permissions"
color="blue.400"
_light={{
color: "blue.600",
}}
gap={4}
href={permissionsHref}
>
View all -&gt;
</TrackedLink>
</Flex>
{contract && (
<Card p={0} overflow="hidden">
<SimpleGrid
gap={2}
columns={9}
borderBottomWidth="1px"
borderColor="borderColor"
padding={4}
bg="blackAlpha.50"
_dark={{ bg: "whiteAlpha.50" }}
>
<Heading gridColumn="span 3" size="label.md">
Member
</Heading>
<Heading gridColumn="span 6" size="label.md">
Roles
</Heading>
</SimpleGrid>
<List overflow="auto">
{members.length === 0 && (
<div className="flex items-center justify-center py-4">
<Flex align="center" gap={2}>
<Text size="body.md" fontStyle="italic">
{allRoleMembers.isPending
? "loading permissions"
: "no permissions found"}
</Text>
</Flex>
</div>
)}
<div>
{members.map((e) => (
<PermissionsItem key={e.member} data={e} />
))}
</div>
</List>
</Card>
)}
</Flex>
<PermissionsTableUI
members={members}
isPending={allRoleMembers.isPending}
viewMoreLink={`/${props.chainSlug}/${props.contract.address}/permissions`}
trackingCategory={props.trackingCategory}
/>
);
};
interface PermissionsItemProps {
data: { member: string; roles: string[] };
}
const PermissionsItem: React.FC<PermissionsItemProps> = ({ data }) => {
const { onCopy } = useClipboard(data.member);
export function PermissionsTableUI(props: {
viewMoreLink: string;
trackingCategory: string;
members: { member: string; roles: string[] }[];
isPending: boolean;
}) {
return (
<div>
<SimpleGrid
columns={9}
gap={2}
as="li"
borderBottomWidth="1px"
borderColor="borderColor"
padding={4}
overflow="hidden"
alignItems="center"
_last={{ borderBottomWidth: 0 }}
>
<Box gridColumn="span 2">
<div className="flex flex-row items-center gap-3">
<ToolTipLabel label="Copy address to clipboard">
<Button
size="sm"
bg="transparent"
onClick={() => {
onCopy();
toast.info("Address copied.");
}}
>
<CopyIcon className="size-3" />
</Button>
</ToolTipLabel>
<Text fontFamily="mono" noOfLines={1}>
{shortenIfAddress(data.member)}
</Text>
</div>
</Box>
<div className="rounded-lg border bg-card">
{/* header */}
<div className="flex w-full items-center justify-between border-b px-6 py-4">
<h2 className="font-semibold text-xl tracking-tight">Permissions</h2>
<Button asChild variant="outline" size="sm" className="bg-background">
<TrackedLinkTW
category={props.trackingCategory}
label="view_all_permissions"
href={props.viewMoreLink}
className="flex items-center gap-2 text-muted-foreground text-sm"
>
View all <ArrowRightIcon className="size-4" />
</TrackedLinkTW>
</Button>
</div>
<Box gridColumn="span 1" />
{/* table */}
<TableContainer className="w-full border-none">
<Table>
<TableHeader>
<TableRow>
<TableHead>Member</TableHead>
<TableHead>Roles</TableHead>
</TableRow>
</TableHeader>
<Flex gridColumn="span 6" flexWrap="wrap" gap={2}>
{data.roles.slice(0, 3).map((role) => (
<Tag key={role}>
<Text size="body.md" fontWeight="medium">
{role}
</Text>
</Tag>
))}
{data.roles.length > 3 && (
<Tag
border="2px solid"
borderColor="var(--badge-bg)"
bg="transparent"
>
<Text size="body.md" fontWeight="medium">
+ {data.roles.length - 3}
</Text>
</Tag>
<TableBody>
{props.members.map((data) => (
<TableRow key={data.member}>
<TableCell>
<WalletAddress address={data.member} />
</TableCell>
<TableCell>
<div className="flex w-max flex-wrap gap-2">
{data.roles.map((role) => (
<Badge
variant="outline"
key={role}
className="bg-background py-1 font-normal text-sm capitalize"
>
{role}
</Badge>
))}
</div>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
{props.members.length === 0 && (
<div className="flex h-48 items-center justify-center text-sm">
{props.isPending ? (
<Spinner className="size-8" />
) : (
<p>No Permissions</p>
)}
</Flex>
</SimpleGrid>
</div>
)}
</div>
);
};
}
@@ -1,17 +0,0 @@
import type { ThirdwebContract } from "thirdweb";
import { TokenSupply } from "../../tokens/components/supply";
interface TokenDetailsProps {
contract: ThirdwebContract;
}
export const TokenDetails: React.FC<TokenDetailsProps> = ({ contract }) => {
return (
<div className="flex flex-col gap-6">
<div className="flex w-full flex-row items-center justify-between">
<h2 className="font-semibold text-2xl tracking-tight">Token Details</h2>
</div>
<TokenSupply contract={contract} />
</div>
);
};
@@ -2,7 +2,7 @@ import { useMemo } from "react";
import type { ThirdwebContract } from "thirdweb";
import { totalAuctions, totalListings } from "thirdweb/extensions/marketplace";
import { useReadContract } from "thirdweb/react";
import { StatCard } from "./stat-card";
import { Stat } from "./stat-card";
const TotalListingsStat: React.FC<{ contract: ThirdwebContract }> = ({
contract,
@@ -19,7 +19,7 @@ const TotalListingsStat: React.FC<{ contract: ThirdwebContract }> = ({
);
return (
<StatCard
<Stat
value={combinedListingCount.toString()}
label="Total Listings"
isPending={
@@ -37,7 +37,7 @@ const DirectListingsStat: React.FC<{ contract: ThirdwebContract }> = ({
});
return (
<StatCard
<Stat
value={(directListingsQuery.data || 0n).toString()}
isPending={directListingsQuery.isPending}
label="Direct Listings"
@@ -53,7 +53,7 @@ const EnglishAuctionsStat: React.FC<{ contract: ThirdwebContract }> = ({
});
return (
<StatCard
<Stat
value={(englishAuctionsQuery.data || 0n).toString()}
isPending={englishAuctionsQuery.isPending}
label="English Auctions"
@@ -73,12 +73,17 @@ export const ListingStatsV3: React.FC<ListingStatsV3Props> = ({
hasEnglishAuctions,
}) => {
return (
<div className="flex flex-row gap-3 md:gap-6 [&>*]:grow">
{hasDirectListings && hasEnglishAuctions && contract && (
<TotalListingsStat contract={contract} />
)}
{hasDirectListings && <DirectListingsStat contract={contract} />}
{hasEnglishAuctions && <EnglishAuctionsStat contract={contract} />}
<div className="rounded-lg border bg-card">
<h2 className="border-b px-6 py-5 font-semibold text-xl tracking-tight">
Listing Details
</h2>
<div className="flex flex-col gap-4 p-6 lg:flex-row [&>*]:grow">
{hasDirectListings && hasEnglishAuctions && contract && (
<TotalListingsStat contract={contract} />
)}
{hasDirectListings && <DirectListingsStat contract={contract} />}
{hasEnglishAuctions && <EnglishAuctionsStat contract={contract} />}
</div>
</div>
);
};
@@ -1,17 +1,34 @@
import { SkeletonContainer } from "@/components/ui/skeleton";
export function StatCard(props: {
value: string;
isPending: boolean;
label: string;
}) {
export function Stat({
label,
value,
isPending,
}: { label: string; value: string; isPending: boolean }) {
return (
<dl className="block rounded-lg border border-border bg-card p-4">
<dt className="mb-1.5 text-sm md:text-base">{props.label}</dt>
<dl className="grow">
<dt className="text-muted-foreground text-sm">{label}</dt>
<SkeletonContainer
loadedData={props.isPending ? undefined : props.value}
skeletonData={"0000"}
render={(v) => <dd className="truncate font-semibold text-xl">{v}</dd>}
skeletonData={"000000"}
loadedData={isPending ? undefined : value}
render={(v) => <dd className="font-medium text-lg">{v}</dd>}
/>
</dl>
);
}
export function StatCard({
label,
value,
isPending,
}: { label: string; value: string; isPending: boolean }) {
return (
<dl className="grow rounded-lg border bg-card p-4">
<dt className="text-muted-foreground text-sm">{label}</dt>
<SkeletonContainer
skeletonData={"000000"}
loadedData={isPending ? undefined : value}
render={(v) => <dd className="font-medium text-lg">{v}</dd>}
/>
</dl>
);
@@ -1,7 +1,7 @@
import { Flex, SimpleGrid, Spinner } from "@chakra-ui/react";
import { Spinner } from "@/components/ui/Spinner/Spinner";
import { TrackedUnderlineLink } from "@/components/ui/tracked-link";
import type { ExtensionDetectedState } from "components/buttons/ExtensionDetectedState";
import { ExternalLinkIcon, Grid2x2XIcon } from "lucide-react";
import { Heading, Text, TrackedLink } from "tw-components";
import { Grid2x2XIcon } from "lucide-react";
const settingTypeMap = {
metadata: {
@@ -26,15 +26,13 @@ const settingTypeMap = {
},
} as const;
interface SettingDetectedStateProps {
type: keyof typeof settingTypeMap;
detectedState: ExtensionDetectedState;
}
export const SettingDetectedState: React.FC<SettingDetectedStateProps> = ({
export function SettingDetectedState({
type,
detectedState,
}) => {
}: {
type: keyof typeof settingTypeMap;
detectedState: ExtensionDetectedState;
}) {
if (detectedState === "enabled") {
return null;
}
@@ -42,45 +40,32 @@ export const SettingDetectedState: React.FC<SettingDetectedStateProps> = ({
const metadata = settingTypeMap[type];
return (
<SimpleGrid
position="absolute"
top={0}
left={0}
bottom={0}
right={0}
zIndex={1}
placeItems="center"
px={{ base: 6, md: 0 }}
borderRadius="md"
className="bg-background"
>
<div className="absolute inset-0 z-10 grid place-items-center bg-card p-6">
{detectedState === "loading" ? (
<Spinner size="sm" />
<Spinner className="size-4" />
) : (
<Flex flexDir="column" gap={3}>
<Flex align="center" gap={2}>
<div className="flex flex-col items-center text-center">
<div className="mb-2 flex size-10 items-center justify-center rounded-full border bg-background">
<Grid2x2XIcon className="size-4 text-red-500" />
<Heading size="subtitle.md">Missing extension</Heading>
</Flex>
<Text>
This contract does not implement the required extension for{" "}
<strong>{metadata.name}</strong>.
</Text>
<TrackedLink
category="contract-settings"
label="metadata-extension"
href={metadata.portalLink}
isExternal
display="flex"
flexDir="row"
alignItems="center"
gap={2}
>
Learn how to enable this extension
<ExternalLinkIcon className="size-4" />
</TrackedLink>
</Flex>
</div>
<div>
<h3 className="mb-2 font-semibold text-lg">Missing extension</h3>
<p className="mb-0.5 text-muted-foreground text-sm">
This contract does not implement the required extension for{" "}
<span className="text-foreground">{metadata.name}</span>
</p>
<TrackedUnderlineLink
category="contract-settings"
label="metadata-extension"
href={metadata.portalLink}
target="_blank"
className="text-muted-foreground text-sm"
>
Learn how to enable this extension
</TrackedUnderlineLink>
</div>
</div>
)}
</SimpleGrid>
</div>
);
};
}
@@ -143,7 +143,7 @@ export const SettingsMetadata = ({
);
return (
<Card p={0} position="relative">
<Card p={0} position="relative" overflow="hidden">
<SettingDetectedState type="metadata" detectedState={detectedState} />
<Flex
as="form"
@@ -76,7 +76,7 @@ export const SettingsPlatformFees = ({
);
return (
<Card p={0} position="relative">
<Card p={0} position="relative" overflow="hidden">
<SettingDetectedState type="platformFee" detectedState={detectedState} />
<Flex
as="form"
@@ -68,7 +68,7 @@ export const SettingsPrimarySale = ({
);
return (
<Card p={0} position="relative">
<Card p={0} position="relative" overflow="hidden">
<SettingDetectedState type="primarySale" detectedState={detectedState} />
<Flex
as="form"
@@ -89,7 +89,7 @@ export const SettingsRoyalties = ({
);
return (
<Card p={0} position="relative">
<Card p={0} position="relative" overflow="hidden">
<SettingDetectedState type="royalties" detectedState={detectedState} />
<Flex
as="form"

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