Compare commits

...
Author SHA1 Message Date
Joaquim VergesGitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
7113f7c669 Version Packages (#7463)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-06-27 18:56:26 -07:00
f9d7935d84 Update engine API (#7427)
Signed-off-by: Prithvish Baidya <[email protected]>
Co-authored-by: Prithvish Baidya <[email protected]>
2025-06-27 18:41:37 -07:00
FirekeeperandGitHub acd5656d4d [SDK] EIP-7702 Session Keys (#7432) 2025-06-27 18:11:19 -07:00
gregandGitHub 6d1d344c48 Fix: Exclude IAW when smart wallets are used (#7454) 2025-06-27 13:36:11 -07:00
MananTank 26b8c320ae [TOOL-4899] Dashboard: fetch token icon from bridge api as fallback in coin asset page (#7460)
<!--

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

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

## Notes for the reviewer

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

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR introduces a new utility function to fetch token information from a bridge and integrates it into the `ERC20PublicPage` component to enhance contract metadata with token icons.

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

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

<!-- end pr-codex -->

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

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

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

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

## Notes for the reviewer

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

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on updating the `PayEmbed` component to `BuyWidget` in the `BuyTokenEmbed` function, enhancing the functionality with error reporting and success tracking for asset purchases.

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

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

<!-- end pr-codex -->

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

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

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

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

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

## Notes for the reviewer

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

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

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

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

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

<!-- end pr-codex -->

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

## Summary by CodeRabbit

* **New Features**
  * Added display of contract creator information on ERC20 and NFT public pages.
  * Introduced a badge showing the contract creator's address in the contract header section.

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

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

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

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

## Notes for the reviewer

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

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on updating the handling of the `totalSupply` property in the `SupplyClaimedProgress` component and related files to support the string value `"unlimited"` instead of using a constant value from `thirdweb/utils`.

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

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

<!-- end pr-codex -->

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

## Summary by CodeRabbit

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

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

<!-- start pr-codex -->

---

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

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

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

<!-- end pr-codex -->

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

* **New Features**
  * Expanded the learning resources section with updated and additional cards, including new topics like "Contracts" and "Vault".

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

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

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

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

## Notes for the reviewer

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

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on updating the descriptions and titles of various features in the support section of the dashboard, as well as changing a header for improved clarity.

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

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

<!-- end pr-codex -->

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

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

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

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

## Notes for the reviewer

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

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on removing unused components and updating product descriptions in the `page.tsx` and `products.ts` files.

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

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

<!-- end pr-codex -->

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

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

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

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

## Notes for the reviewer

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

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

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

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

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

<!-- end pr-codex -->

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

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

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

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

<!-- start pr-codex -->

---

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

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

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

<!-- end pr-codex -->

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

## Summary by CodeRabbit

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

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

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-06-26 16:19:05 +00:00
Phillip HoandGitHub 472e737616 chore: webhook payload must be an array (#7444) 2025-06-26 18:38:21 +08:00
Joaquim VergesGitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
0345492e99 Version Packages (#7433)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-06-26 03:01:11 -07:00
gregandGitHub e650434061 [SDK] Fix: Chain name label (#7442) 2025-06-25 15:47:17 -07:00
gregandGitHub 6712911950 [SDK] Chore: Remove onrampTokenAddress set to ETH (#7441) 2025-06-25 14:22:20 -07:00
orbuloandGitHub 1387337955 [SDK] Feature: update hey-api version to 0.76.0 (#7431) 2025-06-25 12:30:12 -07:00
Phillip HoandGitHub a2b737e144 fix: typo on a field in webhook event helper (#7440) 2025-06-25 23:19:02 +08:00
Phillip HoandGitHub c5f9921dc9 feat: add webhook producer to service-utils (#7439) 2025-06-25 13:35:19 +08:00
Joaquim VergesandGitHub 830125c920 Add CLAUDE.md (#7438) 2025-06-24 19:11:06 -07:00
Joaquim VergesandGitHub af89836b6c [SDK] Handle sponsored gas on TransactionWidget (#7437) 2025-06-24 19:10:47 -07:00
MananTank d85ee5fac3 Dashboard: add "Contracts" link in contract layout header (#7436)
![image.png](https://graphite-user-uploaded-assets-prod.s3.amazonaws.com/yNOf1svJ8o3zjO7zQouZ/05f5e0d2-cfee-4604-8567-a630e13f8232.png)

<!--

## 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 optional property, `currentProjectSubpath`, to enhance the `TeamHeader` components by allowing the display of additional project navigation details, including a label, href, and icon.

### Detailed summary
- Added `currentProjectSubpath` property to `TeamHeaderLoggedIn` and `TeamHeaderDesktopUI`.
- Updated `TeamHeaderUI` to render project subpath details if provided.
- Introduced a new story in `TeamHeaderUI.stories.tsx` to showcase the `currentProjectSubpath` feature.
- Integrated `currentProjectSubpath` into the `ContractLayout` component for enhanced navigation.

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

<!-- end pr-codex -->

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

- **New Features**
  - Added support for displaying an active project subpage link with an icon in the team header, enhancing breadcrumb navigation for projects.
  - Updated the team header to visually indicate the current subpage within a project.

- **Documentation**
  - Added a new Storybook variant to demonstrate the team header with an active subpage link.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-06-24 23:37:56 +00:00
MananTank 8bb4d4bc11 Dashboard: remove date selector from UB FTUX, improved skeletons (#7435)
<!--

## 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 `Page` component in the `loading.tsx` file and the `PayAnalytics` component in the `PayAnalytics.tsx` file, enhancing the structure and user experience by modifying the rendering of analytics and loading states.

### Detailed summary
- Removed `ResponsiveSuspense` and `PayAnalyticsFilter` from `loading.tsx`.
- Integrated `PayAnalytics` directly into the `Page` component.
- Added `ResponsiveSuspense` with a skeleton loader in `PayAnalytics`.
- Reorganized analytics display in `PayAnalytics`, including filtering and layout adjustments.

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

<!-- end pr-codex -->

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

## Summary by CodeRabbit

- **Refactor**
  - Improved the loading experience in the PayAnalytics section with responsive skeleton placeholders during data loading.
  - Updated the layout to display analytics filters above the loading state for better usability.

- **Chores**
  - Removed unused loading and suspense logic from related pages to streamline the user interface.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-06-24 22:27:52 +00:00
MananTank 3c6668b6c8 [TOOL-4833] Add Manage Contract button in new public contract pages (#7434)
<!--

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

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

## Notes for the reviewer

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

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on enhancing the handling of `projectSlug` and `teamSlug` properties across various components in the application, improving navigation and data management related to teams and projects.

### Detailed summary
- Added `projectSlug` and `teamSlug` props in several components including `cards.tsx`, `DeployedContractsPage.tsx`, and `layout.tsx`.
- Updated links to include `teamSlug` and `projectSlug` for better navigation.
- Introduced new API function `getContractImportedProjects` to fetch projects with imported contracts.
- Created `TeamSelectorCard` and `ProjectAndTeamSelectorCard` components for selecting teams and projects.
- Improved UI elements such as buttons and tooltips in `ContractHeaderUI` and `ImportModal`.
- Enhanced type definitions for better type safety and clarity in components.

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

<!-- end pr-codex -->

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

- **New Features**
  - Introduced a team and project selector UI, enabling users to easily select teams or projects from visual cards.
  - Added a project and team selector for associating contracts, allowing users to view, select, or import contracts into projects across teams.
  - Enhanced contract management by providing direct navigation to contract pages based on project selection.
  - Improved contract view links to include team and project information in URLs for clearer navigation.
  - Added functionality to fetch projects that have imported specific contracts within teams.

- **UI Improvements**
  - Updated button styles with rounded edges for a polished look.
  - Added copy address and manage contract buttons with icons and tooltips for enhanced usability.

- **Storybook/Documentation**
  - Added Storybook stories to visually test and demonstrate the new team and project selector components under various scenarios.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-06-24 21:58:48 +00:00
gregandGitHub 6aa2765a34 Feature: UB Widget Tracking and Docs (#7426) 2025-06-24 13:19:01 -07:00
Joaquim VergesGitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
bbff67db0c Version Packages (#7414)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-06-23 20:31:12 -07:00
Prithvish BaidyaandGitHub ef2895fa2c Implement support for EOA execution options in server wallets (#7429) 2025-06-23 20:27:59 -07:00
Prithvish BaidyaandGitHub cf44dae9ed add eoa execution options type (#7428) 2025-06-23 19:51:33 -07:00
Joaquim VergesandGitHub c26be2d958 Fix wallet row alignment in wide connect modal (#7425) 2025-06-23 16:38:44 -07:00
MananTank 029741126f [TOOL-4856] Engine Server wallets table UI adjustment, Fix Vault Portal Link (#7419)
<!--

## 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 user interface in the dashboard by improving the handling of server wallets and updating the link for the Vault.

### Detailed summary
- Updated the `href` in the `Link` component to point to `https://portal.thirdweb.com/vault` and added a "Learn more about Vault" text.
- Refactored the rendering logic for wallets:
  - Removed the conditional rendering for "No server wallets found" and replaced it with a new div that displays this message when there are no wallets.
  - Streamlined the mapping of `wallets` to display each wallet's details within a `TableRow`.

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

<!-- end pr-codex -->

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

## Summary by CodeRabbit

- **Bug Fixes**
	- Improved the display of the "No server wallets found" message, which now appears below the wallets table instead of inside it for better clarity.

- **Style**
	- Updated the "Learn more about Vault" link to display clear link text and corrected its URL for improved user navigation.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-06-23 23:14:31 +00:00
MananTank b5114dfe22 [TOOL-4858] Update Team Usage Overview page UI for free plan (#7423)
<!--

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

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

## Notes for the reviewer

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

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on improving the handling of team usage and upsell features in the dashboard. It introduces new components and logic to display upsell content based on the team's billing plan.

### Detailed summary
- Updated import paths for `getTeamBySlug` and `UpsellWrapper`.
- Added `UpsellContent` component to display upsell information.
- Implemented logic to show upsell content if the team is on a "free" plan.
- Refactored the `UpsellWrapper` to include `UpsellContent`.
- Enhanced rendering of benefits in `UpsellContent`.

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

<!-- end pr-codex -->

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

## Summary by CodeRabbit

- **Refactor**
  - Improved code structure by extracting upsell card UI into a dedicated component, resulting in cleaner and more maintainable code.
  - Updated import paths for consistency and clarity.

- **Style**
  - Adjusted layout classes for better visual alignment of upsell content.

- **Bug Fixes**
  - Unified error handling for usage page, ensuring a consistent message for all error scenarios. Free plan users now see a clear upgrade prompt.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-06-23 23:02:36 +00:00
MananTank 66270f7933 Dashboard: group project sidebar links (#7417)
<!--

## 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 sidebar layout by introducing new components for better organization and rendering of sidebar links. It refactors the sidebar structure to use `RenderSidebarMenu` and `RenderSidebarGroup`, allowing for grouped sidebar items.

### Detailed summary
- Added `SidebarGroupLabel` and `SidebarMenuSubItem` components.
- Refactored `FullWidthSidebarLayout` to use `RenderSidebarMenu` instead of `RenderSidebarGroup`.
- Updated `RenderSidebarGroup` to include `SidebarGroupLabel` and `SidebarMenuItem`.
- Introduced grouping for sidebar links with `group` and `links` properties.
- Enhanced sidebar link structure to support separators and improved organization.

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

<!-- end pr-codex -->

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

## Summary by CodeRabbit

- **Refactor**
  - Improved sidebar navigation by organizing links into hierarchical groups ("Build," "Monetize," "Scale") for better clarity and usability.
  - Updated sidebar rendering for greater modularity and maintainability, with enhanced grouping and link management.

- **Style**
  - Sidebar appearance and structure are updated for a more organized and user-friendly experience.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-06-24 02:42:29 +05:30
AmineAfia 11b4d9e780 Add support for tuple types in ABI event and function signatures (#7415)
# [Dashboard] Fix: Improve ABI type handling for tuple types in webhook signatures

## Notes for the reviewer
This PR adds support for properly handling tuple types in ABI event and function signatures. Previously, we were only extracting the basic type string, which doesn't correctly represent tuple structures. Now we recursively build the canonical type representation for tuples, including nested tuples and array dimensions.

## How to test
The changes can be tested by creating webhooks with contracts that use tuple types in their events or functions. The signature extraction should now correctly represent the canonical format for these complex types.

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

- **Bug Fixes**
  - Improved handling of tuple types in ABI inputs to ensure accurate event and function signature generation, especially for nested tuple structures.
- **New Features**
  - Function signature labels in transaction filters are now truncated for better readability without affecting underlying data.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR primarily enhances the handling of function signatures in the `FilterDetailsStep.tsx` component and improves the extraction of ABI input types in `abiUtils.ts`. It introduces a helper function to manage tuple types and modifies how function signatures are displayed.

### Detailed summary
- In `FilterDetailsStep.tsx`, updated the label for function signatures to use `truncateMiddle(sig.name, 30, 15)` for better readability.
- Added a new helper function `getCanonicalType` in `abiUtils.ts` to recursively handle tuple types in ABI inputs.
- Modified the extraction of canonical input types in both `extractEventSignatures` and `extractFunctionSignatures` to use `getCanonicalType` instead of directly mapping the type.

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

<!-- end pr-codex -->
2025-06-23 13:49:17 +00:00
kilavvyandGitHub ad5aabcb7c Docs: Fix typos in modular contracts guide (#7408)
Signed-off-by: kilavvy <[email protected]>
2025-06-22 18:12:29 -07:00
Joaquim VergesandGitHub 3107ba1c37 [SDK] Handle non-URLs for metadata images in payment widgets (#7413) 2025-06-22 17:54:33 -07:00
MananTank 897bdc75d5 Dashboard: Remove unused assets #2 (#7412)
<!--

## 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 image files from the project, specifically logos and images used in the dashboard and product pages.

### Detailed summary
- Deleted image files from `apps/dashboard/src/components/partners/logos/`:
  - `aws.png`
  - `xai.png`
  - `ztx.png`
  - `myna.png`
  - `nyfw.png`
  - `paima.png`
  - `layer3.png`
  - `mirror.png`
  - `pixels.png`
  - `torque.png`
  - `unlock.png`
  - `animoca.png`
  - `polygon.png`
  - `rarible.png`
  - `shopify.png`
  - `avacloud.png`
  - `coinbase.png`
  - `coolcats.png`
  - `paradigm.png`
  - `treasure.png`
  - `mcfarlane.png`
  - `aavegotchi.png`
  - `gala_games.png`
  - `infinigods.png`
- Deleted image files from `apps/dashboard/src/components/product-pages/homepage/multi-chain/`:
  - `mobile.svg`
  - `desktop.svg`
  - `mobile-gray.svg`
  - `desktop-gray.svg`
- Deleted `blur.png` from `apps/dashboard/src/components/product-pages/homepage/`

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

<!-- end pr-codex -->
2025-06-22 19:29:45 +00:00
MananTank 87c645d633 Dashboard: file organization refactor (no code change) (#7410)
<!--

## 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.

-->
2025-06-22 19:29:44 +00:00
MananTank 15b4b4b2f9 Dashboard: Remove unused files (#7409)
<!--

## 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 unused asset files and CSS modules from the `apps/dashboard` directory, streamlining the project's file structure.

### Detailed summary
- Deleted `misc.svg` from `apps/dashboard/public/assets/support/`
- Deleted `engine.png` from `apps/dashboard/public/assets/support/`
- Deleted `engine.png` from `apps/dashboard/public/assets/og-image/`
- Deleted `account.svg` from `apps/dashboard/public/assets/support/`
- Deleted `wallets.png` from `apps/dashboard/public/assets/support/`
- Deleted `contracts.png` from `apps/dashboard/public/assets/support/`
- Deleted `contracts.png` from `apps/dashboard/public/assets/og-image/`
- Deleted `engine.png` from `apps/dashboard/public/assets/product-icons/`
- Deleted `contracts.png` from `apps/dashboard/public/assets/landingpage/`
- Deleted `wallets.png` from `apps/dashboard/public/assets/product-icons/`
- Deleted `contracts.png` from `apps/dashboard/public/assets/product-icons/`
- Deleted `partner-logo.module.css` from `apps/dashboard/src/components/partners/`
- Deleted `categories.module.css` from `apps/dashboard/src/components/homepage/category/`

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

<!-- end pr-codex -->

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

## Summary by CodeRabbit

- **Style**
  - Removed custom styles for homepage category components, affecting layout, interactivity, and responsiveness.
  - Removed marquee animation and spacing styles for partner logo elements.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-06-22 19:29:43 +00:00
arcoraven 18ad1afbde fix: bug with no results on storage page (#7411)
## [Dashboard] Fix: Add optional chaining to prevent undefined errors in YourFilesSection

## Notes for the reviewer

Added optional chaining to prevent potential undefined errors when accessing `pinnedFilesQuery.data.result` properties in the YourFilesSection component. This ensures the component handles cases where the data or result might be undefined more gracefully.

## How to test

Verify that the YourFilesSection component renders correctly when data is loading or when the API returns undefined values.

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

## Summary by CodeRabbit

- **Bug Fixes**
	- Improved reliability when displaying pinned files by preventing errors if data is missing or incomplete.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-06-22 19:19:54 +00:00
e70e9b9fcc Fix comment for max claimable tokens test in drop721.test.ts (#7336)
Signed-off-by: fuder.eth <[email protected]>
Co-authored-by: Jonas Daniels <[email protected]>
2025-06-21 13:19:18 -07:00
Jonas DanielsandGitHub dfe913e2a9 Upgrade Biome from 2.0.0 to 2.0.4 (#7407) 2025-06-21 13:16:45 -07:00
Jonas DanielsandGitHub 5bc50eb339 [Dashboard] Replace Sentry Replay with PostHog integration (#7406) 2025-06-21 13:11:41 -07:00
jnsdls b61ce679e1 Move Sentry config to instrumentation-client.ts (#7405)
<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **New Features**
  - Integrated Sentry error monitoring and Session Replay for improved error tracking and diagnostics in the dashboard.
  - Enhanced filtering of irrelevant errors and noise from third-party scripts and browser extensions.

- **Chores**
  - Updated analytics setup to include Sentry alongside existing PostHog integration.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on integrating `Sentry` for error tracking and monitoring within the `instrumentation-client.ts` file, alongside an existing `PostHog` setup for analytics.

### Detailed summary
- Removed `sentry.client.config.ts`.
- Added `Sentry` initialization with configuration settings including `allowUrls`, `denyUrls`, and `ignoreErrors`.
- Configured `Sentry` for session replay with `integrations`, `replaysOnErrorSampleRate`, and `tracesSampleRate`.
- Exported `onRouterTransitionStart` for router transition tracking.

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

<!-- end pr-codex -->
2025-06-21 19:22:40 +00:00
Joaquim VergesGitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
56495a1217 Version Packages (#7404)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-06-21 11:45:49 -07:00
MananTank c9b4730ecb Dashboard: Team Overview page UI improvements (#7400)
<!--

## 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 UI components in the dashboard application, including pagination buttons, team member invitations, and changelog displays. It also refines layout and styling for better user experience.

### Detailed summary
- Updated `pagination-buttons.stories.tsx` to correct page labels.
- Modified `InviteTeamMembersButton` styling for a more rounded appearance.
- Enhanced layout in `page.tsx` for better responsiveness and spacing.
- Changed `Changelog` component structure for improved visual hierarchy.
- Adjusted `TeamProjectsPage` to refine project display and sorting options.
- Replaced `Select` component with a `DropdownMenu` for sorting.

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

<!-- end pr-codex -->

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

- **New Features**
	- Changelog section now displays feature images for each entry, with fallback icons if unavailable.
	- Changelog items are shown in a responsive grid layout for improved readability.
	- Changelog header includes a title, description, and "View All" button.

- **Enhancements**
	- Team projects page features a redesigned search input, improved sorting with a dropdown menu, and a more prominent subtitle.
	- "Create Project" and other action buttons have updated styling for better visibility.
	- Invite Team Members button now has a rounded appearance and enhanced background styling.
	- Dashboard layout is streamlined with sequential content flow and consistent maximum width for better usability.
	- Pagination controls updated with fully rounded styling for a modern look.
	- Pagination storybook examples corrected and expanded to accurately represent page counts.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-06-21 18:19:28 +00:00
MananTank ea3c702c4f Fix lint errors (#7401)
<!--

## 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 `sidebar.tsx` file by modifying the `href` property of a menu item and enhancing `globals.css` to improve code styling, particularly for code spans in both light and dark themes.

### Detailed summary
- In `sidebar.tsx`, the `href` property for the menu item "Get Dedicated Support" was removed.
- In `globals.css`, new styles were added for `code span` to set colors for light and dark themes.
- Redundant `.shiki` styles were removed.
- Existing styles for autofilled inputs and button elements remain unchanged.

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

<!-- end pr-codex -->

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

- **Style**
  - Consolidated and reorganized CSS rules for code highlighting to improve maintainability. No visual changes were made.

- **Refactor**
  - Adjusted the order of properties within the sidebar configuration for improved code consistency. No changes to functionality or user experience.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-06-21 18:19:28 +00:00
Jonas DanielsandGitHub 51798f09be [Changeset] Remove thirdweb-login from ignore list (#7403) 2025-06-21 11:19:01 -07:00
Jonas DanielsandGitHub 387a739544 [Changeset] Add nebula-app to ignore list and reorder config (#7402) 2025-06-21 11:15:56 -07:00
Phillip HoandGitHub 6fd630af1f docs: Add docs about dedicated support channel (#7399) 2025-06-21 12:13:34 +08:00
Phillip HoandGitHub ff6e2453d0 feat: dedicated support self-serve (#7381) 2025-06-21 10:46:25 +08:00
MananTank eeb672d092 [TOOL-4842] Dashboard navigation changes (#7395)
<!--

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

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

## Notes for the reviewer

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

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on restructuring and renaming various components and pages within the application, particularly transitioning terminology from "assets" to "tokens" and reorganizing file paths for clarity. It also includes the deletion of unused files and adjustments in imports.

### Detailed summary
- Deleted unused files related to `nebula` and `in-app-wallets`.
- Renamed components and pages from "assets" to "tokens".
- Updated file paths to reflect the new structure.
- Adjusted imports for consistency with new paths.
- Enhanced UI elements for better user experience.

> The following files were skipped due to too many changes: `apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/engine/cloud/vault/components/create-vault-account.client.tsx`, `apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/contracts/layout.tsx`, `apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/engine/cloud/analytics/ftux.client.tsx`, `apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/engine/dedicated/(general)/layout.tsx`, `apps/dashboard/redirects.js`, `apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/webhooks/contract-webhooks/contract-webhooks-page.tsx`, `apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/engine/cloud/server-wallets/components/try-it-out.tsx`, `apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/engine/cloud/layout.tsx`, `apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/layout.tsx`, `apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/vault/components/key-management.tsx`, `apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/components/ProjectSidebarLayout.tsx`, `apps/dashboard/src/components/pay/PayAnalytics/PayEmbedFTUX.tsx`, `apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/components/ProjectFTUX/ProjectFTUX.tsx`, `apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/engine/cloud/server-wallets/wallet-table/wallet-table-ui.client.tsx`, `apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/webhooks/page.tsx`, `packages/nebula/package.json`, `pnpm-lock.yaml`

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

<!-- end pr-codex -->

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

- **New Features**
  - Introduced a Vault section for secure key management, including a new layout and key management UI.
  - Added contract webhooks management with improved pages and navigation.

- **Improvements**
  - Renamed "Assets" to "Tokens" and "In-App Wallets" to "Wallets" throughout the dashboard.
  - Updated sidebar navigation labels, icons, and link destinations for clarity and consistency.
  - Enhanced "Transactions" section, replacing previous "Engine" references and updating related routes and UI.
  - Refined onboarding and FTUX experiences with clearer terminology and improved instructional content.
  - Improved contract import and table UI with updated labels and empty state messages.
  - Enhanced Pay analytics with richer code examples and a more flexible FTUX component.
  - Streamlined redirects and updated URL paths to remove deprecated segments for smoother navigation.
  - Improved pagination display and active link detection in navigation components.
  - Adjusted layout spacing, button sizes, and typography for improved visual consistency.
  - Updated footer and sidebar styling for better user experience.

- **Bug Fixes**
  - Fixed various redirect and navigation paths to align with new route structures.
  - Improved handling of pagination and active link states in navigation components.

- **Removals**
  - Removed Nebula analytics pages, components, and related tracking utilities.
  - Deleted unused tracking properties and constants.
  - Removed outdated Vault key management component replaced by new implementation.

- **Documentation**
  - Updated terminology in documentation and UI to reflect the shift from "assets" to "tokens".
  - Revised analytics event descriptions to use "token page" terminology.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-06-21 00:36:00 +00:00
PaoloRollo 9667c74981 feat: add @thirdweb-dev/nebula package. (#7389)
<!--

## 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.

-->
This PR adds the new `@thirdweb-dev/nebula` package. It's an OpenAPI Typescript Wrapper for the new Nebula APIs.

<!-- start pr-codex -->

---

## PR-Codex overview
This PR introduces the initial release of the `@thirdweb-dev/nebula` TypeScript SDK, providing a wrapper for the Nebula API with configuration options and client functionalities.

### Detailed summary
- Added `@thirdweb-dev/nebula` package with initial release.
- Created configuration functions: `configure`, `configureWithClient`, and `getNebulaClient`.
- Generated client and types using `@hey-api/openapi-ts`.
- Included comprehensive README with usage examples.
- Established TypeScript configuration files.

> The following files were skipped due to too many changes: `packages/nebula/src/client/types.gen.ts`

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

<!-- end pr-codex -->

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

- **New Features**
  - Introduced the Nebula TypeScript SDK as a new package, providing a client for interacting with the Nebula OpenAPI AI agent service.
  - Added configuration utilities for flexible authentication and integration with existing clients.
  - Exposed a comprehensive set of API methods for managing agents, conversations, models, and user data.
  - Extended build scripts to include the Nebula package in relevant workspace commands.

- **Documentation**
  - Added detailed README with setup instructions and usage examples for the Nebula SDK.

- **Chores**
  - Added configuration and build files to support development and compilation of the new package.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-06-20 23:32:30 +00:00
Jonas DanielsandGitHub 997beeac3a biome 2 (#7398) 2025-06-20 16:14:38 -07:00
gregandGitHub 9fec4ccec4 [SDK] Fix: Deprecate test mode (#7396) 2025-06-20 13:07:52 -07:00
gregandGitHub 9c420c0c9d [SDK] Feature: Update PayEmbed to use new widgets (#7394) 2025-06-20 12:50:06 -07:00
Jonas DanielsandGitHub ffea73c719 [Dashboard] Disable 7-day trial for growth plan (#7392) 2025-06-20 00:22:21 -07:00
Jonas DanielsandGitHub f56cc00187 [Dashboard] Remove phone login option and add info alert (#7391) 2025-06-20 00:09:21 -07:00
gregandGitHub 6be9459361 Refactor: UB widget inputs (#7390) 2025-06-19 16:47:38 -07:00
MananTank 492c6d0194 Dashboard: NFT asset page UI updates (#7388)
<!--

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

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

## Notes for the reviewer

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

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR enhances the `TokenInfoUI` component by adding a new claim condition check, improving the UI for displaying NFT purchase availability, and restructuring the layout for better clarity. It also introduces a tabbed interface for viewing traits and purchasing options for ERC1155 tokens.

### Detailed summary
- Added a new check for claim conditions in `TokenInfoUI`.
- Introduced a "Not available for purchase" message when no claim conditions are set.
- Implemented a tabbed interface for ERC1155 tokens to switch between traits and purchase options.
- Reorganized the layout of the NFT details section for improved readability.
- Adjusted spacing in the `BuyEditionDrop` form for consistency.

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

<!-- end pr-codex -->

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

- **New Features**
	- Introduced a tabbed interface for ERC1155 tokens, allowing users to switch between viewing traits and purchasing options.
	- Added badges displaying token type, token ID, and chain metadata for improved clarity.

- **Improvements**
	- Enhanced handling of claim conditions, now clearly indicating when an NFT is not available for purchase.
	- Refined the display of NFT traits, showing a "No Traits" message if none exist.
	- Updated form and input styling for a cleaner and more consistent appearance.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-06-19 18:13:48 +00:00
Yash094 787118d367 feat: add berachain and berachain-bepolia chains (#7343)
<!--

## 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 `berachain` and `berachainBepolia` blockchain definitions to the `thirdweb` package, enhancing its chain support. It also updates the `chains` export file to include these new chains.

### Detailed summary
- Added `berachain` and `berachainBepolia` definitions in their respective files.
- Updated `packages/thirdweb/src/exports/chains.ts` to export `berachain` and `berachainBepolia`.
- Defined `berachain` with an ID of 80094 and a native currency of `BERA`.
- Defined `berachainBepolia` with an ID of 80069, marked as a testnet.

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

<!-- end pr-codex -->

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

- **New Features**
  - Added support for the Berachain mainnet and Berachain Bepolia testnet networks, including native currency details and integrated block explorers for enhanced network visibility.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-06-19 18:03:17 +00:00
MananTank 70e024f43e [TOOL-4831] SDK: Fix MediaRenderer not showing poster for 3d models (#7387)
<!--

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

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

## Notes for the reviewer

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

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on fixing the display of the `poster` in the `MediaRenderer` component for 3D models and adding tests to ensure expected behavior.

### Detailed summary
- Updated the `MediaRenderer` component to show a `poster` image for 3D models.
- Added a new test case to verify rendering of the `poster` image when a 3D model is provided.
- Replaced hardcoded image URL with a constant `imageLink` for clarity in tests.

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

<!-- end pr-codex -->

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

- **Bug Fixes**
	- Fixed an issue where the poster image was not displayed when rendering 3D models in the MediaRenderer component. Now, if a poster image is available, it will be shown as a fallback for unsupported 3D models.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-06-19 17:16:55 +00:00
Phillip HoandGitHub 4c2988623f chore: add dedicatedSupportChannel to TeamResponse (#7382) 2025-06-19 21:24:50 +08:00
Jonas DanielsandGitHub 5492331263 [Dashboard] Add chain, publisher, and contract data to deploy failure analytics (#7376) 2025-06-18 21:14:01 -07:00
jnsdls 1397adf32e posthog migration: part 5 (#7367)
<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **New Features**
  - Added event reporting for contract publishing, faucet usage, and custom chain configuration additions.
  - Introduced detailed event reporting for asset purchases, creations, imports, and configuration steps.

- **Refactor**
  - Replaced legacy analytics tracking hooks with centralized reporting functions for key user actions.
  - Updated asset creation flows to report step transitions and outcomes with enhanced detail.

- **Bug Fixes**
  - Clarified faucet claim restriction text to include the "Scale" plan alongside other eligible plans.

- **Chores**
  - Removed all legacy analytics tracking hooks and event calls across the dashboard.
  - Simplified event reporting by eliminating unused tracking utilities and hooks without affecting user workflows.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on removing the `useTrack` analytics hook from various components and replacing it with direct logging or removing tracking functionality entirely.

### Detailed summary
- Deleted `useTrack` imports and instances from multiple files.
- Removed tracking-related code from components such as `LinkWalletPrompt`, `LoginOrSignup`, and `StepCard`.
- Replaced tracking calls with direct logging in error handling.
- Adjusted various components to eliminate unnecessary tracking.

> The following files were skipped due to too many changes: `apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/engine/cloud/vault/components/list-access-tokens.client.tsx`, `apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/settings/components/primary-sale.tsx`, `apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/nfts/components/mint-form.tsx`, `apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/nfts/[tokenId]/components/mint-supply-tab.tsx`, `apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/_components/claim-conditions/reset-claim-eligibility.tsx`, `apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/engine/dedicated/(instance)/[engineId]/wallet-credentials/components/edit-wallet-credential-button.tsx`, `apps/dashboard/src/@/components/blocks/pricing-card.tsx`, `apps/dashboard/src/components/embedded-wallets/Configure/index.tsx`, `apps/dashboard/src/components/onboarding/ApplyForOpCreditsForm.tsx`, `apps/dashboard/src/components/settings/AuthorizedWallets/AuthorizedWalletsTable.tsx`, `apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/settings/components/platform-fees.tsx`, `apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/nfts/[tokenId]/components/update-metadata-form.tsx`, `apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/nfts/[tokenId]/components/burn-tab.tsx`, `apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/nfts/components/shared-metadata-form.tsx`, `apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/nfts/components/batch-lazy-mint-button.tsx`, `apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/nfts/[tokenId]/components/airdrop-tab.tsx`, `apps/dashboard/src/components/configure-networks/ConfigureNetworks.tsx`, `apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/tokens/components/airdrop-form.tsx`, `apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/tokens/components/burn-button.tsx`, `apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/tokens/components/transfer-button.tsx`, `apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/engine/dedicated/(instance)/[engineId]/overview/components/backend-wallets-table.tsx`, `apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/nfts/components/claim-button.tsx`, `apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/engine/dedicated/(instance)/[engineId]/admins/components/admins-table.tsx`, `apps/dashboard/src/components/smart-wallets/SponsorshipPolicies/index.tsx`, `apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/engine/dedicated/(instance)/[engineId]/relayers/components/relayers-table.tsx`, `apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/engine/dedicated/(instance)/[engineId]/access-tokens/components/access-tokens-table.tsx`, `apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/tokens/components/claim-button.tsx`, `apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/(chainPage)/components/client/PayModal.tsx`, `apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/settings/ProjectGeneralSettingsPage.tsx`, `apps/dashboard/src/app/(app)/login/onboarding/account-onboarding-ui.tsx`, `apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/_components/claim-conditions/claim-conditions-form/index.tsx`, `apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/assets/create/nft/upload-nfts/upload-nfts.tsx`, `apps/dashboard/src/components/CustomChat/CustomChatContent.tsx`, `apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/_layout/primary-dashboard-button.tsx`, `apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/(chainPage)/components/client/FaucetButton.tsx`, `apps/dashboard/src/components/contract-components/contract-publish-form/index.tsx`, `apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/assets/cards.tsx`, `apps/dashboard/src/app/(app)/login/onboarding/VerifyEmail/VerifyEmail.tsx`, `apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/assets/create/nft/launch/launch-nft.tsx`, `apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/assets/create/token/launch/launch-token.tsx`, `apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/public-pages/nft/overview/buy-nft-drop/buy-nft-drop.client.tsx`, `apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/public-pages/erc20/_components/claim-tokens/claim-tokens-ui.tsx`, `apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/public-pages/nft/overview/buy-edition-drop/buy-edition-drop.client.tsx`, `apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/assets/create/token/create-token-page-impl.tsx`, `apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/assets/create/nft/create-nft-page.tsx`, `apps/dashboard/src/@/analytics/report.ts`

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

<!-- end pr-codex -->
2025-06-19 00:53:58 +00:00
jnsdls 2aa9ce5f78 posthog migration: part 4 (#7366)
# Analytics Event Reporting System

This PR introduces a structured analytics event reporting system for the dashboard app using PostHog. It provides clear guidelines and standardized patterns for tracking user actions.

## Key Features

- Added a centralized analytics reporting system in `src/@/analytics/report.ts`
- Implemented standardized naming conventions for events (`<subject> <verb>`) and reporting functions (`report<Subject><Verb>`)
- Created comprehensive documentation in `apps/dashboard/src/@/analytics/README.md`
- Added mandatory JSDoc requirements to document the purpose and ownership of each event
- Integrated the first event: `reportContractDeployed` to track contract deployments
- Updated the contract deployment form to use the new analytics system

## Guidelines

- Events should only be added when they answer concrete product/business questions
- All events must include typed properties and proper documentation
- Analytics code is client-side only (never import `posthog-js` in server components)
- Developers should notify #core-services before renaming or removing events

This standardized approach will make our analytics more maintainable, discoverable, and purposeful.

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

- **Documentation**
  - Added comprehensive guidelines and a README for analytics event reporting, including naming conventions, usage examples, and requirements for event tracking in the dashboard app.

- **New Features**
  - Introduced new analytics events covering contract deployments and multiple stages of team onboarding, such as plan selection, member invitations, and onboarding completion.

- **Refactor**
  - Centralized analytics reporting by replacing generic event tracking with dedicated reporting functions across contract deployment and team onboarding flows.
  - Removed redundant and granular event tracking calls, simplifying analytics integration and improving consistency.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on enhancing analytics event reporting for onboarding and contract deployment processes across the application, ensuring better tracking of user interactions and feedback.

### Detailed summary
- Added `.eslintignore` entries for markdown files.
- Updated `InviteTeamMembers` and `InviteSection` to report success counts on invites.
- Introduced new reporting functions in `report.ts` for onboarding and contract events.
- Removed unused `trackEvent` calls, replacing them with specific reporting functions.
- Enhanced documentation for analytics guidelines and event reporting in `README.md` files.

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

<!-- end pr-codex -->
2025-06-19 00:53:58 +00:00
jnsdls d9eee50dc1 posthog migration: part 3 (#7365)
# Remove Analytics Tracking Components

This PR removes analytics tracking components and properties from the dashboard app. Key changes include:

- Removed `TrackedLinkTW` and `TrackedUnderlineLink` components, replacing them with standard `Link` and `UnderlineLink` components
- Removed tracking properties from sidebar links and navigation components
- Eliminated tracking metadata from contract cards, NFT cards, and marketplace components
- Removed tracking-related props from various UI components
- Simplified component interfaces by removing tracking-specific parameters
- Replaced legacy tracked button components with standard button components

These changes simplify the codebase by removing the analytics tracking layer while maintaining all core functionality.

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on enhancing security by adding `rel="noopener noreferrer"` to various `Link` components throughout the codebase to prevent potential security vulnerabilities associated with opening links in new tabs.

### Detailed summary
- Added `rel="noopener noreferrer"` to multiple `Link` components for security.
- Removed `tracking` properties from several components.
- Replaced `TrackedLinkTW` with `Link` or `UnderlineLink` in various files.
- Updated imports to use `Link` from `next/link` instead of custom tracking links.

> The following files were skipped due to too many changes: `apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/engine/dedicated/(instance)/[engineId]/metrics/components/EngineSystemMetrics.tsx`, `apps/dashboard/src/tw-components/link.tsx`, `apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/_components/NFTCards.tsx`, `apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/engine/dedicated/(general)/import/EngineImportPage.tsx`, `apps/dashboard/src/components/explore/upsells/deploy-your-own.tsx`, `apps/dashboard/src/components/contract-functions/interactive-abi-function.tsx`, `apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/overview/components/LatestEvents.tsx`, `apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/engine/dedicated/(instance)/[engineId]/contract-subscriptions/components/engine-contract-subscription.tsx`, `apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/engine/dedicated/(general)/_components.tsx`, `apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/engine/dedicated/(instance)/[engineId]/_components/version.tsx`, `apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/overview/components/NFTDetails.tsx`, `apps/dashboard/src/@3rdweb-sdk/react/components/connect-wallet/index.tsx`, `apps/dashboard/src/components/contract-components/publisher/publisher-header.tsx`, `apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/overview/ContractOverviewPage.tsx`, `apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/overview/components/PermissionsTable.tsx`, `apps/dashboard/src/app/(app)/(dashboard)/profile/[addressOrEns]/components/PublishedContractTable.tsx`, `apps/dashboard/src/components/explore/contract-card/index.tsx`, `apps/dashboard/src/contract-ui/tabs/code/components/code-overview.tsx`, `apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/connect/account-abstraction/factories/page.tsx`, `apps/dashboard/src/components/embedded-wallets/Configure/index.tsx`, `apps/dashboard/src/@/components/blocks/app-footer.tsx`, `apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/components/footer/FooterLinksSection.tsx`, `apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/connect/account-abstraction/AccountAbstractionPage.tsx`, `apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/components/ProjectSidebarLayout.tsx`, `apps/dashboard/src/components/buttons/MismatchButton.tsx`, `apps/dashboard/src/tw-components/button.tsx`, `apps/dashboard/src/components/smart-wallets/SponsorshipPolicies/index.tsx`, `apps/dashboard/src/components/cmd-k-search/index.tsx`, `apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/components/ProjectFTUX/ProjectFTUX.tsx`, `apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/overview/components/MarketplaceDetails.tsx`

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

<!-- end pr-codex -->

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

- **Refactor**
  - Removed all analytics tracking from links and buttons across the dashboard.
  - Replaced custom tracking link and button components with standard or simplified alternatives.
  - Updated component props and interfaces to eliminate tracking-related fields.
  - Streamlined UI components by removing tracking props and logic, resulting in cleaner navigation and interactions.

- **Style**
  - Added `rel="noopener noreferrer"` attribute to all external links opening in new tabs for improved security and privacy.
  - Minor adjustments to link styling in some components to accommodate new link components.

No changes to core functionality or user workflows; navigation and UI remain consistent.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-06-19 00:53:58 +00:00
jnsdls 5f2bca8371 posthog migration: part 2 (#7364)
# Add PostHog Analytics Identification for Accounts and Teams

This PR adds analytics tracking capabilities to identify users and teams in PostHog:

- Created two new hooks:
  - `useIdentifyAccount`: Identifies users by their account ID and optional email
  - `useIdentifyTeam`: Identifies teams by their team ID using PostHog's group functionality

- Implemented these hooks in the `TeamHeaderLoggedIn` component to track both the current user and team

Both hooks include safety checks to ensure PostHog is properly loaded before attempting to identify users or teams.

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on integrating `posthog-js` analytics functionalities into various components of the application, specifically for resetting analytics on logout and identifying users and teams.

### Detailed summary
- Added `resetAnalytics` function in `reset.ts` to reset analytics.
- Implemented `useIdentifyTeam` hook in `identify-team.ts` for team identification.
- Implemented `useIdentifyAccount` hook in `identify-account.ts` for account identification.
- Integrated `resetAnalytics` in logout processes across multiple components:
  - `CustomConnectWallet`
  - `AccountHeader`
  - `AccountSettingsPage`
  - `LoginPage`
  - `TeamHeaderLoggedIn`
- Updated ESLint rules to allow direct imports of `posthog-js` within analytics helpers.

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

<!-- end pr-codex -->

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

- **New Features**
	- Introduced automatic analytics identification for user accounts and teams within the dashboard.
	- Added a function to reset analytics data, now triggered on logout, wallet disconnection, and account deletion.

- **Chores**
	- Updated internal configuration to ensure analytics tracking is consistently managed and restricted to designated areas.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-06-19 00:53:57 +00:00
jnsdls 6b24fed21f posthog migration: part 1 (#7363)
# Migrate to PostHog US Instance

This PR updates our PostHog integration across all apps to use the US-based PostHog instance instead of the previous self-hosted solution. Key changes include:

- Upgraded PostHog JS SDK from 1.67.1 to 1.252.0
- Added API proxying via Next.js rewrites to route PostHog requests through `/_ph` endpoints
- Implemented a new initialization approach using `instrumentation-client.ts` files
- Removed custom PostHog components and providers in favor of the official SDK's auto-capture functionality
- Configured PostHog to capture page views on history changes automatically
- Disabled session recording and exception capturing for privacy and performance
- Removed the `flat` dependency which is no longer needed

These changes will provide more reliable analytics while improving performance by using PostHog's official CDN and modern SDK features.

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

- **New Features**
  - Added support for proxying analytics-related requests to external services across multiple apps, enabling seamless integration with analytics providers.

- **Chores**
  - Upgraded the analytics library dependency to the latest version in several apps.
  - Updated environment variable examples to support new analytics configuration.

- **Refactor**
  - Replaced in-app analytics instrumentation with new centralized clients for improved control and consistency.

- **Bug Fixes**
  - Minor CSS z-index syntax adjustments for improved styling consistency.

- **Removed**
  - Eliminated legacy analytics components and tracking from layouts, providers, hooks, and feedback forms, simplifying application structure and reducing external tracking.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on the removal of several `Posthog` components and their related files across multiple applications, as well as updating the `posthog-js` library version to `1.252.0`. It also introduces new `rewrites` in various configuration files to accommodate the updated tracking.

### Detailed summary
- Deleted files related to `Posthog` from:
  - `apps/portal/src/lib/env.ts`
  - `apps/portal/src/lib/posthog/Posthog.tsx`
  - `apps/dashboard/src/lib/posthog/Posthog.tsx`
  - `apps/playground-web/src/lib/posthog/Posthog.tsx`
  - `apps/dashboard/src/components/wallets/PosthogIdentifier.tsx`
- Updated `posthog-js` version to `1.252.0` in:
  - `apps/nebula/package.json`
  - `apps/playground-web/package.json`
  - `apps/portal/package.json`
  - `apps/dashboard/package.json`
- Added `NEXT_PUBLIC_POSTHOG_KEY` and `NEXT_PUBLIC_POSTHOG_HOST` to `apps/nebula/.env.example`.
- Introduced new `rewrites` in `next.config.mjs` for `apps/playground-web`, `apps/dashboard`, `apps/nebula`, and `apps/portal` to direct traffic for `Posthog`.
- Removed `Posthog` components from various layouts and providers, simplifying the structure in `apps/dashboard/src/app/pay/components/client/Providers.client.tsx` and `apps/dashboard/src/app/bridge/components/client/Providers.client.tsx`.

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

<!-- end pr-codex -->
2025-06-19 00:53:57 +00:00
gregandGitHub cc3fbba895 Fix: Fallback insight owned tokens request to mainnet (#7375) 2025-06-18 15:38:58 -07:00
Toomas OosaluandGitHub 381e423d59 insight playground - allow array items to be single item (#7373) 2025-06-18 15:14:12 -07:00
Manan TankandGitHub c70d5ab013 [PRO-199] Playground sidebar changes (#7369) 2025-06-18 15:13:30 -07:00
gregandGitHub 3dbf9e16fd Fix: Bridge Playground Chain Input Form (#7371) 2025-06-18 11:58:21 -07:00
Joaquim VergesGitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
be49798e99 Version Packages (#7361)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-06-18 09:37:52 -07:00
AmineAfia 167f9213ff Add SignatureSelector component for custom signature input in webhooks (#7370)
# [INFRA-1454][Dashboard] Feature: Add SignatureSelector component for improved signature selection

## Notes for the reviewer

This PR introduces a new `SignatureSelector` component that enhances the signature selection experience in the webhooks UI. The component allows users to either select from predefined signatures or enter custom ones, with appropriate UI feedback.

Key improvements:
- Users can now enter custom signatures directly in the input field
- Warning message appears when a custom signature is entered
- Automatic ABI detection for known signatures
- Better handling of signature formats (automatically converts text signatures to hash format)

## How to test

1. Navigate to the webhooks creation flow
2. Test selecting predefined event/function signatures
3. Test entering custom signatures and verify the warning appears
4. Verify that ABIs are properly loaded for known signatures
5. Test the reset functionality

The component is fully integrated with the existing form validation system.

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

- **New Features**
  - Introduced a new signature selector with search and custom input support for selecting or entering function signatures.
  - Enhanced dropdown components to allow custom search input elements.

- **Refactor**
  - Replaced the previous signature dropdown with the new selector for improved usability and consistency.
  - Improved ABI input handling, including automatic detection and reset options.

- **Bug Fixes**
  - Improved normalization of signature hash values and ABI fallback logic for webhook payloads.
  - Enhanced address parsing for ABI reset functionality.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR introduces enhancements to the `webhookPayloadUtils`, `MultiSelect`, and `SignatureSelector` components, improving signature handling and custom input functionality. It also updates the `FilterDetailsStep` to utilize the new `SignatureSelector` and remove the deprecated `SignatureDropdown`.

### Detailed summary
- Updated `buildEventWebhookPayload` and `buildTransactionWebhookPayload` functions to handle `sigHash` and `abi` more robustly.
- Added `customSearchInput` prop to `MultiSelect` for enhanced search functionality.
- Implemented `SignatureSelector` component for better signature selection with custom input support.
- Replaced `SignatureDropdown` with `SignatureSelector` in `FilterDetailsStep`.
- Improved handling of custom signatures and ABI management in `FilterDetailsStep`.

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

<!-- end pr-codex -->
2025-06-18 15:49:37 +00:00
Jonas DanielsandGitHub bb09e20bcb [Dashboard] Update rewrites and redirects for Connect to Build migration (#7362) 2025-06-17 16:51:05 -07:00
gregandGitHub 78b9c8e334 [SDK] Fix: stringify bigint erc20Value in useTransactionDetails (#7360) 2025-06-17 15:32:04 -07:00
Manan TankandGitHub a5163c0c7f Fix z-index issue in NFT asset page (#7359) 2025-06-17 15:04:09 -07:00
gregandGitHub 55baa997f0 Fix: Throw error if amount and gas is more than balance (#7358) 2025-06-17 14:39:55 -07:00
Manan TankandGitHub c75e434498 [TOOL-4827] Move Nebula out of dashboard (#7356) 2025-06-17 14:30:24 -07:00
YashandGitHub 2b5941ff7d Replace CLI login article link (#7355) 2025-06-17 10:12:20 -07:00
Manan TankandGitHub 85b21ec65c Add Empty Nebula project shell (#7357) 2025-06-17 09:38:46 -07:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
508b4c48e5 chore(deps): bump github/codeql-action from 3.28.19 to 3.29.0 (#7348)
Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-16 22:19:42 -07:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
a5e086f173 chore(deps): bump @sentry/nextjs from 9.28.1 to 9.29.0 (#7351)
Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-16 22:19:26 -07:00
4682 changed files with 97462 additions and 81094 deletions
+9 -9
View File
@@ -1,5 +1,11 @@
{
"___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH": {
"onlyUpdatePeerDependentsWhenOutOfRange": true,
"updateInternalDependents": "always"
},
"$schema": "https://unpkg.com/@changesets/[email protected]/schema.json",
"access": "public",
"baseBranch": "main",
"changelog": [
"@changesets/changelog-github",
{
@@ -7,21 +13,15 @@
}
],
"commit": false,
"access": "public",
"baseBranch": "main",
"updateInternalDependencies": "patch",
"ignore": [
"playground-web",
"thirdweb-dashboard",
"wallet-ui",
"portal",
"thirdweb-login"
"nebula-app"
],
"___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH": {
"updateInternalDependents": "always",
"onlyUpdatePeerDependentsWhenOutOfRange": true
},
"snapshot": {
"prereleaseTemplate": "{tag}-{commit}-{datetime}"
}
},
"updateInternalDependencies": "patch"
}
+26 -1
View File
@@ -3,7 +3,6 @@ description: Rules for writing features in apps/dashboard
globs: dashboard
alwaysApply: false
---
# Reusable Core UI Components
- Always import from the central UI library under `@/components/ui/*` – e.g. `import { Button } from "@/components/ui/button"`.
@@ -101,3 +100,29 @@ Guidelines:
- Keep `queryKey` stable and descriptive for cache hits.
- Prefer API routes or server actions to keep tokens secret; the browser only sees relative paths.
- Configure `staleTime` / `cacheTime` according to freshness requirements.
# Analytics Event Reporting
- **Add events intentionally** – only when they answer a concrete product/business question.
- **Event name**: human-readable `<subject> <verb>` phrase (e.g. `"contract deployed"`).
- **Reporting helper**: `report<Subject><Verb>` (PascalCase); all live in `src/@/analytics/report.ts`.
- **Mandatory JSDoc**: explain *Why* the event exists and *Who* owns it (`@username`).
- **Typed properties**: accept a single `properties` object and pass it unchanged to `posthog.capture`.
- **Client-side only**: never import `posthog-js` in server components.
- **Housekeeping**: ping **#core-services** before renaming or removing an event.
```ts
/**
* ### Why do we need to report this event?
* - Tracks number of contracts deployed
*
* ### Who is responsible for this event?
* @jnsdls
*/
export function reportContractDeployed(properties: {
address: string;
chainId: number;
}) {
posthog.capture("contract deployed", properties);
}
```
+1 -1
View File
@@ -63,7 +63,7 @@ jobs:
- name: Setup Biome
uses: biomejs/setup-biome@a9763ed3d2388f5746f9dc3e1a55df7f4609bc89 # v2.5.1
with:
version: latest
version: 2.0.4
- run: pnpm lint
+3 -3
View File
@@ -46,7 +46,7 @@ jobs:
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@fca7ace96b7d713c7035871441bd52efbe39e27e # v3.28.19
uses: github/codeql-action/init@ce28f5bb42b7a9f2c824e633a3f6ee835bab6858 # v3.29.0
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
@@ -59,7 +59,7 @@ jobs:
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@fca7ace96b7d713c7035871441bd52efbe39e27e # v3.28.19
uses: github/codeql-action/autobuild@ce28f5bb42b7a9f2c824e633a3f6ee835bab6858 # v3.29.0
# ℹ️ Command-line programs to run using the OS shell.
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
@@ -72,4 +72,4 @@ jobs:
# ./location_of_script_within_repo/buildscript.sh
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@fca7ace96b7d713c7035871441bd52efbe39e27e # v3.28.19
uses: github/codeql-action/analyze@ce28f5bb42b7a9f2c824e633a3f6ee835bab6858 # v3.29.0
+26 -27
View File
@@ -1,11 +1,32 @@
{
"editor.formatOnSave": true,
"[css]": {
"editor.defaultFormatter": "biomejs.biome"
},
"[javascript]": {
"editor.defaultFormatter": "biomejs.biome"
},
"[json]": {
"editor.defaultFormatter": "biomejs.biome"
},
"[markdown]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[typescript]": {
"editor.defaultFormatter": "biomejs.biome"
},
"[typescriptreact]": {
"editor.defaultFormatter": "biomejs.biome"
},
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit",
"source.organizeImports.biome": "explicit",
"quickfix.biome": "explicit"
"quickfix.biome": "explicit",
"source.fixAll.biome": "explicit"
},
"editor.defaultFormatter": "biomejs.biome",
"editor.formatOnSave": true,
"eslint.workingDirectories": [
{ "pattern": "./packages/*/" },
{ "pattern": "./apps/*/" }
],
"typescript.preferences.autoImportFileExcludePatterns": [
"./packages/thirdweb/src/exports"
],
@@ -15,27 +36,5 @@
"next/dist",
"^lucide-react/dist/lucide-react.suffixed$"
],
"typescript.tsdk": "node_modules/typescript/lib",
"[typescriptreact]": {
"editor.defaultFormatter": "biomejs.biome"
},
"[typescript]": {
"editor.defaultFormatter": "biomejs.biome"
},
"[json]": {
"editor.defaultFormatter": "biomejs.biome"
},
"[markdown]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[javascript]": {
"editor.defaultFormatter": "biomejs.biome"
},
"[css]": {
"editor.defaultFormatter": "biomejs.biome"
},
"eslint.workingDirectories": [
{ "pattern": "./packages/*/" },
{ "pattern": "./apps/*/" }
]
"typescript.tsdk": "node_modules/typescript/lib"
}
+35
View File
@@ -104,6 +104,41 @@ Welcome, AI copilots! This guide captures the coding standards, architectural de
-- Configure staleTime / cacheTime based on freshness requirements (default ≥ 60 s).
-- Keep tokens secret by calling internal API routes or server actions.
6.5 Analytics Event Reporting
- **When to create a new event**
-- Only add events that answer a clear product or business question.
-- Check `src/@/analytics/report.ts` first; avoid duplicates.
- **Naming conventions**
-- **Event name**: human-readable phrase in the form `<subject> <verb>` (e.g. "contract deployed").
-- **Reporting function**: `report<Subject><Verb>` (PascalCase).
-- All reporting helpers currently live in the shared `report.ts` file.
- **Boilerplate template**
-- Add a JSDoc header explaining **Why** the event exists and **Who** owns it (`@username`).
-- Accept a single typed `properties` object and forward it unchanged to `posthog.capture`.
-- Example:
```ts
/**
* ### Why do we need to report this event?
* - Tracks number of contracts deployed
*
* ### Who is responsible for this event?
* @jnsdls
*/
export function reportContractDeployed(properties: {
address: string;
chainId: number;
}) {
posthog.capture("contract deployed", properties);
}
```
- **Client-side only**: never import `posthog-js` in server components.
- **Housekeeping**: Inform **#eng-core-services** before renaming or removing an existing event.
⸻
7. Performance & Bundle Size
+290
View File
@@ -0,0 +1,290 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Development Commands
### Package Management
- Use `pnpm install` (or `pnpm install --ignore-scripts` on Windows)
- Monorepo managed with Turborepo and pnpm workspaces
### Building
```bash
# Build all packages
pnpm build
# Build specific packages with dependencies
turbo run build --filter=./packages/*
# Development with watch mode
pnpm dev # Core thirdweb package
pnpm dashboard # Run dashboard + dependencies
pnpm playground # Run playground + dependencies
pnpm portal # Run portal docs + dependencies
```
### Testing
```bash
# Run all tests
pnpm test
# Interactive testing (thirdweb package)
cd packages/thirdweb && pnpm test:devr
# Test specific file
pnpm test:dev <path-to-test-file>
# E2E testing (dashboard)
cd apps/dashboard && pnpm playwright
```
### Code Quality
```bash
# Lint all packages
pnpm lint
# Auto-fix linting issues
pnpm fix
# Format code
turbo run format
# Biome is the primary linter/formatter
```
### Development Workflow
```bash
# Start development server for dashboard
pnpm dashboard
# Start playground for SDK testing
pnpm playground
# Generate changeset for releases
pnpm changeset
# Version packages
pnpm version-packages
```
## Repository Architecture
### Monorepo Structure
This is a Turborepo monorepo with the main thirdweb v5 SDK consolidated into `/packages/thirdweb/`. Legacy packages are in `/legacy_packages/`.
### Core Package (`/packages/thirdweb/`)
**Main Modules:**
- `client/` - ThirdwebClient foundation
- `chains/` - 50+ supported blockchain definitions
- `contract/` - Contract interaction with automatic ABI resolution
- `transaction/` - Transaction management and execution
- `wallets/` - Comprehensive wallet integration system
- `extensions/` - Modular contract extensions (ERC20, ERC721, etc.)
- `auth/` - SIWE authentication and signature verification
- `pay/` - Fiat and crypto payment infrastructure
- `storage/` - IPFS integration for decentralized storage
- `rpc/` - Low-level blockchain communication
**Exports Structure:**
The SDK uses modular exports from `src/exports/` including:
- `thirdweb.ts` - Core client and utilities
- `chains.ts` - Chain definitions
- `wallets.ts` - Wallet connectors
- `react.ts` - React hooks and components
- `extensions/` - Contract standards and protocols
### Applications (`/apps/`)
- **dashboard** - Web-based developer console (Next.js, Chakra UI)
- **playground-web** - Interactive SDK testing environment
- **portal** - Documentation site with MDX
- **nebula** - Account abstraction and smart wallet management
- **wallet-ui** - Wallet interface and testing
### Key Packages (`/packages/`)
- **thirdweb** - Main SDK (TypeScript, React, React Native)
- **engine** - thirdweb Engine API client
- **insight** - Analytics and data APIs
- **nebula** - Account abstraction client
- **service-utils** - Shared utilities across services
## Development Practices
### GitHub Workflow & Pull Requests
- **PR titles**: Must start with affected workspace in brackets (e.g. `[SDK]`, `[Dashboard]`, `[Portal]`, `[Playground]`)
- **PR descriptions**: Begin with one-sentence summary, add checklist of changes, reference issues with `Fixes #123`
- **Commits**: Keep small and topical – one logical change per commit
- **Branch naming**: Use `area/brief-topic` format (e.g. `sdk/fix-gas-estimate`). Avoid personal names
- **Reviews**: Request at least one core maintainer review. Do not self-merge unless sole package owner
- **CI requirements**: All checks (type-check, Biome, tests) must pass before merging
### Code Quality & Formatting
- **Biome**: Primary linter/formatter (rules in `biome.json`)
- **Pre-commit**: Run `pnpm biome check --apply` before committing
- **Build verification**: Run `pnpm build` after each file change to ensure everything builds
- Avoid editor-specific configs; rely on shared settings
### TypeScript Guidelines
- **Style**: Write idiomatic TypeScript with explicit function declarations and return types
- **File structure**: Limit each file to one stateless, single-responsibility function for clarity
- **Types**: Re-use shared types from `@/types` or local `types.ts` barrels
- **Interfaces vs Types**: Prefer type aliases over interface except for nominal shapes
- **Type safety**: Avoid `any` and `unknown` unless unavoidable; narrow generics when possible
- **Architecture**: Choose composition over inheritance; leverage utility types (`Partial`, `Pick`, etc.)
### Testing Strategy
- **Co-location**: Place tests alongside code: `foo.ts` ↔ `foo.test.ts`
- **Test approach**: Use real function invocations with stub data; avoid brittle mocks
- **Network mocking**: Use Mock Service Worker (MSW) for fetch/HTTP call interception
- **Test quality**: Keep tests deterministic and side-effect free
- **Running tests**: `cd packages/thirdweb && pnpm test:dev <filename>`
- **Test accounts**: Predefined accounts in `test/src/test-wallets.ts`
- **Chain forking**: Use `FORKED_ETHEREUM_CHAIN` for mainnet interactions, `ANVIL_CHAIN` for isolated tests
### SDK Development (`packages/thirdweb`)
#### Public API Guidelines
- **Exports**: Export everything via `exports/` directory, grouped by feature
- **Documentation**: Every public symbol must have comprehensive TSDoc with:
- At least one `@example` block that compiles
- Custom annotation tags (`@beta`, `@internal`, `@experimental`)
- **Comments**: Comment only ambiguous logic; avoid restating TypeScript in prose
#### Performance Optimization
- **Lazy loading**: Load heavy dependencies inside async paths to keep initial bundle lean:
```typescript
const { jsPDF } = await import("jspdf");
```
- **Bundle budgets**: Track via `package.json#size-limit`
- **Dependencies**: De-duplicate across packages through pnpm workspace hoisting
### Dashboard & Playground Development
#### UI Component Standards
- **Core components**: Import primitives from `@/components/ui/*` (Button, Input, Select, Tabs, Card, Sidebar, Badge, Separator)
- **Navigation**: Use `NavLink` for internal navigation with automatic active states
- **Organization**: Group feature-specific components under `feature/components/*` with barrel `index.ts`
#### Styling Conventions
- **CSS framework**: Tailwind CSS only – no inline styles or CSS modules
- **Class merging**: Use `cn()` from `@/lib/utils` for conditional logic
- **Design tokens**: Use design system tokens (backgrounds: `bg-card`, borders: `border-border`, muted text: `text-muted-foreground`)
- **Component API**: Expose `className` prop on root element for overrides
#### Component Architecture
- **Server Components** (Node edge):
- Start files with `import "server-only";`
- Read cookies/headers with `next/headers`
- Access server-only environment variables
- Perform heavy data fetching
- Implement redirect logic with `redirect()` from `next/navigation`
- **Client Components** (browser):
- Begin files with `'use client';`
- Handle interactive UI with React hooks (`useState`, `useEffect`, React Query, wallet hooks)
- Access browser APIs (`localStorage`, `window`, `IntersectionObserver`)
- Support fast transitions with prefetched data
#### Data Fetching Patterns
- **Server Side**:
- Always call `getAuthToken()` to retrieve JWT from cookies
- Use `Authorization: Bearer` header – never embed tokens in URLs
- Return typed results (`Project[]`, `User[]`) – avoid `any`
- **Client Side**:
- Wrap calls in React Query (`@tanstack/react-query`)
- Use descriptive, stable `queryKeys` for cache hits
- Configure `staleTime`/`cacheTime` based on freshness (default ≥ 60s)
- Keep tokens secret via internal API routes or server actions
#### Analytics Event Guidelines
- **When to add**: Only create events that answer clear product/business questions
- **Check duplicates**: Review `src/@/analytics/report.ts` first
- **Naming**:
- Event name: human-readable `<subject> <verb>` (e.g. "contract deployed")
- Function: `report<Subject><Verb>` (PascalCase)
- **Template**:
```typescript
/**
* ### Why do we need to report this event?
* - Tracks number of contracts deployed
*
* ### Who is responsible for this event?
* @username
*/
export function reportContractDeployed(properties: {
address: string;
chainId: number;
}) {
posthog.capture("contract deployed", properties);
}
```
- **Client-side only**: Never import `posthog-js` in server components
- **Changes**: Inform **#eng-core-services** before renaming/removing events
### Extension Development
- Extensions follow modular pattern in `src/extensions/`
- Auto-generated contracts from ABI definitions
- Composable functions with TypeScript safety
- Support for read/write operations
### Wallet Architecture
- Unified `Wallet` and `Account` interfaces
- Support for in-app wallets (social/email login)
- Smart wallets with account abstraction
- EIP-1193, EIP-5792, EIP-7702 standard support
## Contribution Workflow
1. **Fork and Clone**: Create fork, clone, create feature branch
2. **Install**: `pnpm install` (use `--ignore-scripts` on Windows)
3. **Develop**: Use appropriate dev commands above
4. **Test**: Write unit tests, run linting, test on demo apps
5. **Changeset**: Run `pnpm changeset` for semantic versioning
6. **PR**: Submit pull request to main branch
### Release Testing
Comment `/release-pr` on PR to trigger dev release to npm for testing.
### Changeset Guidelines
Each change in `packages/*` should contain a changeset for the appropriate package with the appropriate version bump:
- **patch**: Changes that don't impact the public API
- **minor**: Any new/modified public API
- **major**: Breaking changes (surface prominently in PR descriptions)
### Documentation Standards
- For new UI components, add Storybook stories (`*.stories.tsx`) alongside the code
- Surface breaking changes prominently in PR descriptions
- Keep documentation focused on developer experience and practical usage
## Multi-Platform Support
The SDK supports:
- **Web**: React hooks and components
- **React Native**: Mobile-specific exports and components
- **Node.js**: Server-side functionality
- **Framework Adapters**: Wagmi, Ethers compatibility layers
Key files:
- `src/exports/react.native.ts` - React Native specific exports
- `packages/react-native-adapter/` - Mobile platform shims
- `packages/wagmi-adapter/` - Wagmi ecosystem integration
-11
View File
@@ -44,9 +44,6 @@ NEXT_PUBLIC_STRIPE_KEY=
NEXT_PUBLIC_STRIPE_PAYMENT_METHOD_CFG_ID=
# Needed for contract analytics / blockchain data information
CHAINSAW_API_KEY=
#
# Private (server)
#
@@ -65,17 +62,11 @@ MORALIS_API_KEY=
# - not required to build (unless testing wallet NFTs)>
SSR_ALCHEMY_KEY=
# beehiiv.com API key (used for newsletter signups)
# - not required to build (unless testing newsletter signups)>
BEEHIIV_API_KEY=
# Hubspot Access Token (used for contact us form)
# - not required to build (unless testing contact us form)>
HUBSPOT_ACCESS_TOKEN=
# Github API Token (used for /open-source)
GITHUB_API_TOKEN="ghp_..."
# Upload server url
NEXT_PUBLIC_DASHBOARD_UPLOAD_SERVER="https://storage.thirdweb-preview.com"
@@ -100,8 +91,6 @@ REDIS_URL=""
ANALYTICS_SERVICE_URL=""
# Required for Nebula Chat
NEXT_PUBLIC_NEBULA_URL=""
# required for billing parts of the dashboard (team -> settings -> billing / invoices)
STRIPE_SECRET_KEY=""
+124 -110
View File
@@ -1,52 +1,98 @@
module.exports = {
env: {
browser: true,
node: true,
},
extends: [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"plugin:@next/next/recommended",
"plugin:storybook/recommended",
],
overrides: [
// disable restricted imports in chakra
{
files: "src/chakra/**/*",
rules: {
"no-restricted-imports": ["off"],
},
},
// allow direct PostHog imports inside analytics helpers
{
files: "src/@/analytics/**/*",
rules: {
"no-restricted-imports": ["off"],
},
},
// enable rule specifically for TypeScript files
{
files: ["*.ts", "*.tsx"],
rules: {
"@typescript-eslint/explicit-module-boundary-types": ["off"],
},
},
// in test files, allow null assertions and anys and eslint is sometimes weird about the react-scope thing
{
files: ["*test.ts?(x)"],
rules: {
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-non-null-assertion": "off",
"react/display-name": "off",
},
},
// allow requires in non-transpiled JS files and logical key ordering in config files
{
files: [
"babel-node.js",
"*babel.config.js",
"env.config.js",
"next.config.js",
"webpack.config.js",
"packages/mobile-web/package-builder/**",
],
rules: {},
},
// setupTests can have separated imports for logical grouping
{
files: ["setupTests.ts"],
rules: {
"import/newline-after-import": "off",
},
},
// turn OFF unused vars via eslint
{
files: ["*.ts", "*.tsx"],
rules: {
"@next/next/no-img-element": "off",
"@typescript-eslint/no-unused-vars": "off",
},
},
// THIS NEEDS TO GO LAST!
{
extends: ["biome"],
files: ["*.ts", "*.js", "*.tsx", "*.jsx"],
},
],
parser: "@typescript-eslint/parser",
parserOptions: {
ecmaFeatures: {
impliedStrict: true,
jsx: true,
},
ecmaVersion: 2019,
warnOnUnsupportedTypeScriptVersion: true,
},
plugins: ["@typescript-eslint", "react-compiler"],
rules: {
"react-compiler/react-compiler": "error",
"no-restricted-syntax": [
"error",
{
selector: "CallExpression[callee.name='useEffect']",
message:
'Are you *sure* you need to use "useEffect" here? If you loading any async function prefer using "useQuery".',
},
{
selector: "CallExpression[callee.name='createContext']",
message:
'Are you *sure* you need to use a "Context"? In almost all cases you should prefer passing props directly.',
},
{
selector: "CallExpression[callee.name='defineChain']",
message:
"Use useV5DashboardChain instead if you are using it inside a component",
},
{
selector: "CallExpression[callee.name='defineDashboardChain']",
message:
"Use useV5DashboardChain instead if you are using it inside a component",
},
{
selector: "CallExpression[callee.name='mapV4ChainToV5Chain']",
message:
"Use useV5DashboardChain instead if you are using it inside a component",
},
{
selector: "CallExpression[callee.name='resolveScheme']",
message:
"resolveScheme can throw error if resolution fails. Either catch the error and ignore the lint warning or Use `resolveSchemeWithErrorHandler` / `replaceIpfsUrl` utility in dashboard instead",
},
],
"no-restricted-imports": [
"error",
{
paths: [
{
name: "@chakra-ui/react",
// these are provided by tw-components, so we don't want to import them from chakra directly
// these are provided by chakra folder, so we don't want to import them from chakra directly
importNames: [
"Card",
"Button",
@@ -81,48 +127,72 @@ module.exports = {
"StackProps",
],
message:
'Use the equivalent component from "tw-components" instead.',
'import component from "chakra" folder instead if you have to use chakra component, But use shadcn component otherwise',
name: "@chakra-ui/react",
},
{
name: "@chakra-ui/layout",
message:
"Import from `@chakra-ui/react` instead of `@chakra-ui/layout`.",
name: "@chakra-ui/layout",
},
{
name: "@chakra-ui/button",
message:
"Import from `@chakra-ui/react` instead of `@chakra-ui/button`.",
name: "@chakra-ui/button",
},
{
name: "@chakra-ui/menu",
message:
"Import from `@chakra-ui/react` instead of `@chakra-ui/menu`.",
name: "@chakra-ui/menu",
},
{
name: "next/navigation",
importNames: ["useRouter"],
message:
'Use `import { useDashboardRouter } from "@/lib/DashboardRouter";` instead',
name: "next/navigation",
},
{
name: "lucide-react",
importNames: ["Link", "Table", "Sidebar"],
message:
'This is likely a mistake. If you really want to import this - postfix the imported name with Icon. Example - "LinkIcon"',
'Import "posthog-js" directly only within the analytics helpers ("src/@/analytics/*"). Use the exported helpers from "@/analytics/track" elsewhere.',
name: "posthog-js",
},
],
},
],
},
parser: "@typescript-eslint/parser",
plugins: ["@typescript-eslint", "react-compiler"],
parserOptions: {
ecmaVersion: 2019,
ecmaFeatures: {
impliedStrict: true,
jsx: true,
},
warnOnUnsupportedTypeScriptVersion: true,
"no-restricted-syntax": [
"error",
{
message:
'Are you *sure* you need to use "useEffect" here? If you loading any async function prefer using "useQuery".',
selector: "CallExpression[callee.name='useEffect']",
},
{
message:
'Are you *sure* you need to use a "Context"? In almost all cases you should prefer passing props directly.',
selector: "CallExpression[callee.name='createContext']",
},
{
message:
"Use useV5DashboardChain instead if you are using it inside a component",
selector: "CallExpression[callee.name='defineChain']",
},
{
message:
"Use useV5DashboardChain instead if you are using it inside a component",
selector: "CallExpression[callee.name='defineDashboardChain']",
},
{
message:
"Use useV5DashboardChain instead if you are using it inside a component",
selector: "CallExpression[callee.name='mapV4ChainToV5Chain']",
},
{
message:
"resolveScheme can throw error if resolution fails. Either catch the error and ignore the lint warning or Use `resolveSchemeWithErrorHandler` / `replaceIpfsUrl` utility in dashboard instead",
selector: "CallExpression[callee.name='resolveScheme']",
},
],
"react-compiler/react-compiler": "error",
},
settings: {
react: {
@@ -131,60 +201,4 @@ module.exports = {
version: "detect",
},
},
overrides: [
// disable restricted imports in tw-components
{
files: "src/tw-components/**/*",
rules: {
"no-restricted-imports": ["off"],
},
},
// enable rule specifically for TypeScript files
{
files: ["*.ts", "*.tsx"],
rules: {
"@typescript-eslint/explicit-module-boundary-types": ["off"],
},
},
// in test files, allow null assertions and anys and eslint is sometimes weird about the react-scope thing
{
files: ["*test.ts?(x)"],
rules: {
"@typescript-eslint/no-non-null-assertion": "off",
"@typescript-eslint/no-explicit-any": "off",
"react/display-name": "off",
},
},
// allow requires in non-transpiled JS files and logical key ordering in config files
{
files: [
"babel-node.js",
"*babel.config.js",
"env.config.js",
"next.config.js",
"webpack.config.js",
"packages/mobile-web/package-builder/**",
],
rules: {},
},
// setupTests can have separated imports for logical grouping
{
files: ["setupTests.ts"],
rules: {
"import/newline-after-import": "off",
},
},
// THIS NEEDS TO GO LAST!
{
files: ["*.ts", "*.js", "*.tsx", "*.jsx"],
extends: ["biome"],
},
],
env: {
browser: true,
node: true,
},
};
+2 -14
View File
@@ -1,16 +1,4 @@
{
"$schema": "https://biomejs.dev/schemas/1.9.2/schema.json",
"extends": ["../../biome.json"],
"overrides": [
{
"include": ["src/css/swagger-ui.css"],
"linter": {
"rules": {
"suspicious": {
"noImportantInKeyframe": "off"
}
}
}
}
]
"$schema": "https://biomejs.dev/schemas/2.0.4/schema.json",
"extends": "//"
}
+12 -12
View File
@@ -2,29 +2,29 @@ import { defineConfig } from "checkly";
import { Frequency } from "checkly/constructs";
export default defineConfig({
projectName: "thirdweb.com",
logicalId: "thirdweb-www",
repoUrl: "https://github.com/thirdweb-dev/dashboard",
checks: {
activated: true,
muted: false,
runtimeId: "2023.09",
frequency: Frequency.EVERY_24H,
locations: ["us-east-1", "eu-west-1"],
tags: ["website"],
browserChecks: {
frequency: Frequency.EVERY_24H,
testMatch: "./tests/**/*.spec.ts",
},
checkMatch: "./**/*.check.ts",
frequency: Frequency.EVERY_24H,
ignoreDirectoriesMatch: [],
locations: ["us-east-1", "eu-west-1"],
muted: false,
playwrightConfig: {
use: {
baseURL: process.env.ENVIRONMENT_URL || "https://thirdweb.com",
},
},
browserChecks: {
frequency: Frequency.EVERY_24H,
testMatch: "./tests/**/*.spec.ts",
},
runtimeId: "2023.09",
tags: ["website"],
},
cli: {
runLocation: "eu-west-1",
},
logicalId: "thirdweb-www",
projectName: "thirdweb.com",
repoUrl: "https://github.com/thirdweb-dev/dashboard",
});
+12 -12
View File
@@ -1,17 +1,17 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "default",
"rsc": true,
"tsx": true,
"tailwind": {
"config": "tailwind.config.js",
"css": "src/global.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils"
}
}
},
"rsc": true,
"style": "default",
"tailwind": {
"baseColor": "neutral",
"config": "tailwind.config.js",
"css": "src/global.css",
"cssVariables": true,
"prefix": ""
},
"tsx": true
}
+9 -9
View File
@@ -6,13 +6,14 @@ module.exports = [
"/bounties",
"/contact-us",
// -- product landing pages --
// -- connect
"/connect",
"/connect/sign-in",
"/connect/account-abstraction",
"/connect/universal-bridge",
"/connect/auth",
"/connect/in-app-wallets",
// -- build category
"/wallets",
"/account-abstraction",
"/universal-bridge",
"/auth",
"/in-app-wallets",
"/transactions",
// -- end build category
// -- storage
"/storage",
// -- nebula
@@ -24,8 +25,7 @@ module.exports = [
"/contracts/modular-contracts",
"/contracts/explore",
"/contracts/deployment-tool",
// -- engine
"/engine",
// -- solutions pages --
"/solutions/:solution_slug",
// -- campaigns --
+7 -7
View File
@@ -1,18 +1,18 @@
{
"$schema": "https://unpkg.com/knip@5/schema.json",
"next": true,
"ignore": [
"src/@/components/ui/**",
"src/components/notices/AnnouncementBanner.tsx",
"src/components/cmd-k-search/index.tsx",
"src/lib/search.ts"
"src/@/components/misc/AnnouncementBanner.tsx",
"src/@/components/cmd-k-search/index.tsx",
"src/@/lib/search.ts"
],
"project": ["src/**"],
"ignoreBinaries": ["only-allow", "biome"],
"ignoreBinaries": ["only-allow"],
"ignoreDependencies": [
"@thirdweb-dev/service-utils",
"@thirdweb-dev/vault-sdk",
"@types/color",
"fast-xml-parser"
]
],
"next": true,
"project": ["src/**"]
}
+34 -34
View File
@@ -44,12 +44,37 @@ async function getSingleChain(chainIdOrSlug) {
/** @type {import('next-sitemap').IConfig} */
module.exports = {
siteUrl: process.env.SITE_URL || "https://thirdweb.com",
additionalPaths: async (config) => {
const [framerUrls, allChains] = await Promise.all([
getFramerXML(),
fetchChainsFromApi(),
]);
return [
...framerUrls.map((url) => {
return {
changefreq: config.changefreq,
lastmod: config.autoLastmod ? new Date().toISOString() : undefined,
loc: url.loc,
priority: config.priority,
};
}),
...allChains.map((chain) => {
return {
changefreq: config.changefreq,
lastmod: config.autoLastmod ? new Date().toISOString() : undefined,
loc: `/${chain.slug}`,
priority: config.priority,
};
}),
...(await createSearchRecordSitemaps(config)),
];
},
exclude: ["/chain/validate"],
generateRobotsTxt: true,
robotsTxtOptions: {
policies: [
{
userAgent: "*",
// allow all if production
allow: process.env.VERCEL_ENV === "production" ? ["/"] : [],
// disallow all if not production
@@ -58,10 +83,11 @@ module.exports = {
? ["/"]
: // disallow `/team` and `/team/*` if production
["/team", "/team/*"],
userAgent: "*",
},
],
},
exclude: ["/chain/validate"],
siteUrl: process.env.SITE_URL || "https://thirdweb.com",
transform: async (config, _path) => {
let path = _path;
@@ -75,40 +101,14 @@ module.exports = {
path = path.replace("deployer.thirdweb.eth", "thirdweb.eth");
}
return {
alternateRefs: config.alternateRefs ?? [],
changefreq: config.changefreq,
lastmod: config.autoLastmod ? new Date().toISOString() : undefined,
// => this will be exported as http(s)://<config.siteUrl>/<path>
loc: path,
changefreq: config.changefreq,
priority: config.priority,
lastmod: config.autoLastmod ? new Date().toISOString() : undefined,
alternateRefs: config.alternateRefs ?? [],
};
},
additionalPaths: async (config) => {
const [framerUrls, allChains] = await Promise.all([
getFramerXML(),
fetchChainsFromApi(),
]);
return [
...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}`,
changefreq: config.changefreq,
priority: config.priority,
lastmod: config.autoLastmod ? new Date().toISOString() : undefined,
};
}),
...(await createSearchRecordSitemaps(config)),
];
},
};
/**
* @param {{ changefreq?: any; priority?: any; autoLastmod?: any; }} config
@@ -131,10 +131,10 @@ async function createSearchRecordSitemaps(config) {
parsedLines.map((parsedLine) => {
return getSingleChain(parsedLine.chain_id)
.then((parsedLineChain) => ({
loc: `/${parsedLineChain.slug}/${parsedLine.contract_address}`,
changefreq: config.changefreq,
priority: config.priority,
lastmod: config.autoLastmod ? new Date().toISOString() : undefined,
loc: `/${parsedLineChain.slug}/${parsedLine.contract_address}`,
priority: config.priority,
}))
.catch(() => null);
}),
+64 -52
View File
@@ -46,91 +46,90 @@ function determineIpfsGateways() {
const remotePatterns: RemotePattern[] = [];
if (process.env.API_ROUTES_CLIENT_ID) {
remotePatterns.push({
protocol: "https",
hostname: `${process.env.API_ROUTES_CLIENT_ID}.ipfscdn.io`,
protocol: "https",
});
remotePatterns.push({
protocol: "https",
hostname: `${process.env.API_ROUTES_CLIENT_ID}.thirdwebstorage-staging.com`,
protocol: "https",
});
remotePatterns.push({
protocol: "https",
hostname: `${process.env.API_ROUTES_CLIENT_ID}.thirdwebstorage-dev.com`,
protocol: "https",
});
} else {
// this should only happen in development
remotePatterns.push({
protocol: "https",
hostname: "ipfs.io",
protocol: "https",
});
}
// also add the dashboard clientId ipfs gateway if it is set
if (process.env.NEXT_PUBLIC_DASHBOARD_CLIENT_ID) {
remotePatterns.push({
protocol: "https",
hostname: `${process.env.NEXT_PUBLIC_DASHBOARD_CLIENT_ID}.ipfscdn.io`,
protocol: "https",
});
remotePatterns.push({
protocol: "https",
hostname: `${process.env.NEXT_PUBLIC_DASHBOARD_CLIENT_ID}.thirdwebstorage-staging.com`,
protocol: "https",
});
remotePatterns.push({
protocol: "https",
hostname: `${process.env.NEXT_PUBLIC_DASHBOARD_CLIENT_ID}.thirdwebstorage-dev.com`,
protocol: "https",
});
}
return remotePatterns;
}
const SENTRY_OPTIONS: SentryBuildOptions = {
// For all available options, see:
// https://github.com/getsentry/sentry-webpack-plugin#options
org: "thirdweb-dev",
project: "dashboard",
// An auth token is required for uploading source maps.
authToken: process.env.SENTRY_AUTH_TOKEN,
// Suppresses source map uploading logs during build
silent: true,
// For all available options, see:
// https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/
// Upload a larger set of source maps for prettier stack traces (increases build time)
widenClientFileUpload: true,
// Routes browser requests to Sentry through a Next.js rewrite to circumvent ad-blockers (increases server load)
tunnelRoute: "/err",
// Automatically tree-shake Sentry logger statements to reduce bundle size
disableLogger: true,
// Enables automatic instrumentation of Vercel Cron Monitors.
// See the following for more information:
// https://docs.sentry.io/product/crons/
// https://vercel.com/docs/cron-jobs
automaticVercelMonitors: false,
// Automatically tree-shake Sentry logger statements to reduce bundle size
disableLogger: true,
// For all available options, see:
// https://github.com/getsentry/sentry-webpack-plugin#options
org: "thirdweb-dev",
project: "dashboard",
// Suppresses source map uploading logs during build
silent: true,
// Routes browser requests to Sentry through a Next.js rewrite to circumvent ad-blockers (increases server load)
tunnelRoute: "/err",
// For all available options, see:
// https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/
// Upload a larger set of source maps for prettier stack traces (increases build time)
widenClientFileUpload: true,
};
// add additional languages to the framer rewrite paths here (english is already included by default)
const FRAMER_ADDITIONAL_LANGUAGES = ["es"];
const baseNextConfig: NextConfig = {
compiler: {
emotion: true,
},
eslint: {
ignoreDuringBuilds: true,
},
productionBrowserSourceMaps: false,
experimental: {
webpackBuildWorker: true,
webpackMemoryOptimizations: true,
serverSourceMaps: false,
taint: true,
webpackBuildWorker: true,
webpackMemoryOptimizations: true,
},
serverExternalPackages: ["pino-pretty"],
async headers() {
return [
{
// Apply these headers to all routes in your application.
source: "/(.*)",
headers: [
...securityHeaders,
{
@@ -138,56 +137,69 @@ const baseNextConfig: NextConfig = {
value: "sec-ch-viewport-width",
},
],
// Apply these headers to all routes in your application.
source: "/(.*)",
},
];
},
images: {
contentSecurityPolicy: "default-src 'self'; script-src 'none'; sandbox;",
dangerouslyAllowSVG: true,
remotePatterns: [
{
hostname: "**.thirdweb.com",
protocol: "https",
},
...determineIpfsGateways(),
],
},
productionBrowserSourceMaps: false,
reactStrictMode: true,
async redirects() {
return getRedirects();
},
async rewrites() {
return [
{
source: "/thirdweb.eth",
destination: "/deployer.thirdweb.eth",
destination: "https://us-assets.i.posthog.com/static/:path*",
source: "/_ph/static/:path*",
},
{
destination: "https://us.i.posthog.com/:path*",
source: "/_ph/:path*",
},
{
destination: "https://us.i.posthog.com/decide",
source: "/_ph/decide",
},
{
destination: "/deployer.thirdweb.eth",
source: "/thirdweb.eth",
},
{
source: "/thirdweb.eth/:path*",
destination: "/deployer.thirdweb.eth/:path*",
source: "/thirdweb.eth/:path*",
},
// re-write /home to / (this is so that logged in users will be able to go to /home and NOT be redirected to the logged in app)
{
source: "/home",
destination: "https://landing.thirdweb.com",
source: "/home",
},
// flatmap the framer paths for the default language and the additional languages
...FRAMER_PATHS.flatMap((path) => [
{
source: path,
destination: `https://landing.thirdweb.com${path}`,
source: path,
},
// this is for additional languages
...FRAMER_ADDITIONAL_LANGUAGES.map((lang) => ({
source: `/${lang}${path}`,
destination: `https://landing.thirdweb.com/${lang}${path}`,
source: `/${lang}${path}`,
})),
]),
];
},
images: {
dangerouslyAllowSVG: true,
contentSecurityPolicy: "default-src 'self'; script-src 'none'; sandbox;",
remotePatterns: [
{
protocol: "https",
hostname: "**.thirdweb.com",
},
...determineIpfsGateways(),
],
},
compiler: {
emotion: true,
},
reactStrictMode: true,
serverExternalPackages: ["pino-pretty"],
};
function getConfig(): NextConfig {
+27 -28
View File
@@ -1,26 +1,4 @@
{
"name": "thirdweb-dashboard",
"version": "3.0.0",
"private": true,
"scripts": {
"preinstall": "npx only-allow pnpm",
"dev": "next dev --turbopack",
"build": "NODE_OPTIONS=--max-old-space-size=6144 next build",
"start": "next start",
"format": "biome format ./src --write",
"lint": "biome check ./src && knip && eslint ./src",
"fix": "biome check ./src --fix && eslint ./src --fix",
"typecheck": "tsc --noEmit",
"gen:theme-typings": "chakra-cli tokens src/theme/index.ts",
"postinstall": "pnpm run gen:theme-typings",
"postbuild": "next-sitemap",
"build:analyze": "ANALYZE=true pnpm run build",
"knip": "knip",
"playwright": "playwright test",
"update-checkly": "npx checkly deploy",
"storybook": "storybook dev -p 6006",
"build-storybook": "storybook build"
},
"dependencies": {
"@chakra-ui/react": "^2.8.2",
"@chakra-ui/styled-system": "^2.9.2",
@@ -46,7 +24,7 @@
"@radix-ui/react-switch": "^1.2.5",
"@radix-ui/react-tooltip": "1.2.7",
"@scalar/api-reference-react": "^0.6.19",
"@sentry/nextjs": "9.28.1",
"@sentry/nextjs": "9.29.0",
"@shazow/whatsabi": "0.22.2",
"@tanstack/react-query": "5.80.7",
"@tanstack/react-table": "^8.21.3",
@@ -62,8 +40,6 @@
"compare-versions": "^6.1.0",
"date-fns": "4.1.0",
"fast-xml-parser": "^5.2.5",
"fetch-event-stream": "0.1.5",
"flat": "^6.0.1",
"framer-motion": "12.17.0",
"fuse.js": "7.1.0",
"input-otp": "^1.4.1",
@@ -78,7 +54,7 @@
"p-limit": "^6.2.0",
"papaparse": "^5.5.3",
"pluralize": "^8.0.0",
"posthog-js": "1.67.1",
"posthog-js": "1.252.0",
"prettier": "3.5.3",
"qrcode": "^1.5.3",
"react": "19.1.0",
@@ -105,9 +81,10 @@
"tiny-invariant": "^1.3.3",
"use-debounce": "^10.0.5",
"vaul": "^1.1.2",
"zod": "3.25.62"
"zod": "3.25.67"
},
"devDependencies": {
"@biomejs/biome": "2.0.4",
"@chakra-ui/cli": "^2.4.1",
"@chromatic-com/storybook": "4.0.0",
"@next/bundle-analyzer": "15.3.3",
@@ -141,5 +118,27 @@
"storybook": "9.0.8",
"tailwindcss": "3.4.17",
"typescript": "5.8.3"
}
},
"name": "thirdweb-dashboard",
"private": true,
"scripts": {
"build": "NODE_OPTIONS=--max-old-space-size=6144 next build",
"build-storybook": "storybook build",
"build:analyze": "ANALYZE=true pnpm run build",
"dev": "next dev --turbopack",
"fix": "biome check ./src --fix && eslint ./src --fix",
"format": "biome format ./src --write",
"gen:theme-typings": "chakra-cli tokens src/chakra/theme/index.ts",
"knip": "knip",
"lint": "biome check ./src && knip && eslint ./src",
"playwright": "playwright test",
"postbuild": "next-sitemap",
"postinstall": "pnpm run gen:theme-typings",
"preinstall": "npx only-allow pnpm",
"start": "next start",
"storybook": "storybook dev -p 6006",
"typecheck": "tsc --noEmit",
"update-checkly": "npx checkly deploy"
},
"version": "3.0.0"
}
+18 -18
View File
@@ -10,25 +10,10 @@ import { defineConfig, devices } from "@playwright/test";
* See https://playwright.dev/docs/test-configuration.
*/
export default defineConfig({
testDir: "./tests",
/* Run tests in files in parallel */
fullyParallel: true,
/* Fail the build on CI if you accidentally left test.only in the source code. */
forbidOnly: !!process.env.CI,
/* Retry on CI only */
retries: process.env.CI ? 2 : 0,
/* Opt out of parallel tests on CI. */
workers: process.env.CI ? 1 : undefined,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: "html",
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
/* Base URL to use in actions like `await page.goto('/')`. */
baseURL: process.env.ENVIRONMENT_URL || "http://127.0.0.1:3000",
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: "on-first-retry",
},
/* Run tests in files in parallel */
fullyParallel: true,
/* Configure projects for major browsers */
projects: [
@@ -67,13 +52,28 @@ export default defineConfig({
// use: { ...devices['Desktop Chrome'], channel: 'chrome' },
// },
],
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: "html",
/* Retry on CI only */
retries: process.env.CI ? 2 : 0,
testDir: "./tests",
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
/* Base URL to use in actions like `await page.goto('/')`. */
baseURL: process.env.ENVIRONMENT_URL || "http://127.0.0.1:3000",
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: "on-first-retry",
},
/* Run your local dev server before starting the tests */
webServer: process.env.CI
? undefined
: {
command: "pnpm run start",
url: "http://127.0.0.1:3000",
reuseExistingServer: !process.env.CI,
url: "http://127.0.0.1:3000",
},
/* Opt out of parallel tests on CI. */
workers: process.env.CI ? 1 : undefined,
});
+1 -1
View File
@@ -1,6 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
tailwindcss: {},
},
};
Binary file not shown.

Before

Width:  |  Height:  |  Size: 157 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 626 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 187 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.0 KiB

@@ -1,3 +0,0 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M9.33106 4.3C8.33109 4.3 7.52045 5.11064 7.52045 6.11061C7.52045 7.11058 8.33109 7.92121 9.33106 7.92121C10.331 7.92121 11.1417 7.11058 11.1417 6.11061C11.1417 5.11064 10.331 4.3 9.33106 4.3ZM5.22045 6.11061C5.22045 3.84038 7.06084 2 9.33106 2C11.6013 2 13.4417 3.84038 13.4417 6.11061C13.4417 8.38083 11.6013 10.2212 9.33106 10.2212C7.06084 10.2212 5.22045 8.38083 5.22045 6.11061ZM4.20397 12.0858C4.97486 11.3149 6.0204 10.8818 7.11061 10.8818H11.5515C12.6417 10.8818 13.6873 11.3149 14.4581 12.0858C14.9073 12.5349 14.9073 13.263 14.4581 13.7121C14.009 14.1612 13.2809 14.1612 12.8318 13.7121C12.4922 13.3726 12.0317 13.1818 11.5515 13.1818H7.11061C6.6304 13.1818 6.16987 13.3726 5.83031 13.7121C5.49076 14.0517 5.3 14.5122 5.3 14.9924V16.4727C5.3 17.1079 4.78513 17.6227 4.15 17.6227C3.51487 17.6227 3 17.1079 3 16.4727V14.9924C3 13.9022 3.43308 12.8567 4.20397 12.0858ZM17.5457 14.9598C17.9948 14.5107 17.9948 13.7825 17.5457 13.3334C17.0966 12.8843 16.3685 12.8843 15.9194 13.3334L13.7719 15.4808L13.1048 14.8137C12.6557 14.3646 11.9276 14.3646 11.4785 14.8137C11.0294 15.2628 11.0294 15.991 11.4785 16.4401L12.9588 17.9204C13.4079 18.3695 14.136 18.3695 14.5851 17.9204L17.5457 14.9598Z" fill="#646D7A"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

@@ -1,4 +0,0 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M15.4444 3H4.55556C3.69645 3 3 3.69645 3 4.55556V15.4444C3 16.3036 3.69645 17 4.55556 17H15.4444C16.3036 17 17 16.3036 17 15.4444V4.55556C17 3.69645 16.3036 3 15.4444 3Z" stroke="#646D7A" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M12.3333 7.66666L7.66663 12.3333" stroke="#646D7A" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 492 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.0 KiB

+152 -102
View File
@@ -1,64 +1,114 @@
const legacyDashboardToTeamRedirects = [
{
source: "/dashboard",
destination: "/team",
permanent: false,
source: "/dashboard",
},
{
source: "/dashboard/contracts/:path*",
destination: "/team/~/~/contracts",
permanent: false,
source: "/dashboard/contracts/:path*",
},
{
source: "/dashboard/connect/ecosystem/:path*",
destination: "/team/~/~/ecosystem/:path*",
permanent: false,
source: "/dashboard/connect/ecosystem/:path*",
},
{
source: "/dashboard/engine/:path*",
destination: "/team/~/~/engine/:path*",
permanent: false,
source: "/dashboard/engine/:path*",
},
{
source: "/dashboard/settings/api-keys",
destination: "/team",
permanent: false,
source: "/dashboard/settings/api-keys",
},
{
source: "/dashboard/settings/devices",
destination: "/account/devices",
permanent: false,
source: "/dashboard/settings/devices",
},
{
source: "/dashboard/settings/billing",
destination: "/team/~/~/settings/billing",
permanent: false,
source: "/dashboard/settings/billing",
},
{
source: "/dashboard/settings/gas-credits",
destination: "/team/~/~/settings/credits",
permanent: false,
source: "/dashboard/settings/gas-credits",
},
{
source: "/dashboard/settings/usage",
destination: "/team/~/~/usage",
permanent: false,
source: "/dashboard/settings/usage",
},
{
source: "/dashboard/settings/storage",
destination: "/team/~/~/usage/storage",
permanent: false,
source: "/dashboard/settings/storage",
},
{
source: "/dashboard/settings/notifications",
destination: "/team/~/~/settings/notifications",
permanent: false,
source: "/dashboard/settings/notifications",
},
// rest of the /dashboard/* routes
{
source: "/dashboard/:path*",
destination: "/team",
permanent: false,
source: "/dashboard/:path*",
},
];
const projectRoute = "/team/:team_slug/:project_slug";
const projectPageRedirects = [
{
destination: `${projectRoute}/universal-bridge/:path*`,
permanent: false,
source: `${projectRoute}/connect/pay/:path*`,
},
{
destination: `${projectRoute}/universal-bridge/:path*`,
permanent: false,
source: `${projectRoute}/connect/universal-bridge/:path*`,
},
{
destination: `${projectRoute}/account-abstraction/:path*`,
permanent: false,
source: `${projectRoute}/connect/account-abstraction/:path*`,
},
{
destination: `${projectRoute}/wallets/:path*`,
permanent: false,
source: `${projectRoute}/connect/in-app-wallets/:path*`,
},
{
destination: `${projectRoute}/vault/:path*`,
permanent: false,
source: `${projectRoute}/engine/cloud/vault/:path*`,
},
{
destination: `${projectRoute}/transactions/:path*`,
permanent: false,
source: `${projectRoute}/engine/cloud/:path*`,
},
{
destination: `${projectRoute}/tokens/:path*`,
permanent: false,
source: `${projectRoute}/assets/:path*`,
},
{
destination: projectRoute,
permanent: false,
source: `${projectRoute}/nebula/:path*`,
},
{
source: `${projectRoute}/connect/analytics`,
destination: `${projectRoute}`,
permanent: false,
},
];
@@ -66,316 +116,316 @@ const legacyDashboardToTeamRedirects = [
async function redirects() {
return [
{
source: "/portal/:match*",
destination: "https://portal.thirdweb.com/:match*",
permanent: true,
source: "/portal/:match*",
},
{
source: "/solutions/appchain-api",
destination: "/solutions/chains",
permanent: true,
source: "/solutions/appchain-api",
},
{
source: "/contracts/release",
destination: "/contracts/publish",
permanent: false,
source: "/contracts/release",
},
{
source: "/contracts/release/:path*",
destination: "/contracts/publish/:path*",
permanent: false,
source: "/contracts/release/:path*",
},
{
source: "/release",
destination: "/publish",
permanent: false,
source: "/release",
},
{
source: "/release/:path*",
destination: "/publish/:path*",
permanent: false,
source: "/release/:path*",
},
{
source: "/authentication",
destination: "/auth",
permanent: false,
source: "/authentication",
},
{
source: "/extensions",
destination: "/build",
permanent: false,
source: "/extensions",
},
{
source: "/contractkit",
destination: "/build",
permanent: true,
source: "/contractkit",
},
// old (deprecated) routes
{
source:
"/:network/(edition|nft-collection|token|nft-drop|signature-drop|edition-drop|token-drop|vote)/:address",
destination: "/:network/:address",
permanent: false,
source:
"/:network/(edition|nft-collection|token|nft-drop|signature-drop|edition-drop|token-drop|vote)/:address",
},
// prebuilt contract deploys
{
source: "/contracts/new/:slug*",
destination: "/explore",
permanent: false,
source: "/contracts/new/:slug*",
},
// deployer to non-deployer url
// handled directly in SSR as well
{
source: "/deployer.thirdweb.eth",
destination: "/thirdweb.eth",
permanent: true,
source: "/deployer.thirdweb.eth",
},
{
source: "/deployer.thirdweb.eth/:path*",
destination: "/thirdweb.eth/:path*",
permanent: true,
source: "/deployer.thirdweb.eth/:path*",
},
{
source: "/chains",
destination: "/chainlist",
permanent: true,
source: "/chains",
},
// polygon zkevm beta to non-beta
{
source: "/polygon-zkevm-beta",
destination: "/polygon-zkevm",
permanent: false,
source: "/polygon-zkevm-beta",
},
// backwards compat: page moved to pages/settings/devices
{
source: "/template/nft-drop",
destination: "/template/erc721",
permanent: false,
source: "/template/nft-drop",
},
{
destination: "/team",
permanent: false,
source: "/create-api-key",
destination: "/team",
permanent: false,
},
{
destination: "/team",
permanent: false,
source: "/dashboard/settings",
destination: "/team",
permanent: false,
},
{
source: "/dashboard/connect/playground",
destination: "https://playground.thirdweb.com/connect/sign-in/button",
permanent: false,
source: "/dashboard/connect/playground",
},
{
source: "/dashboard/infrastructure/storage",
destination: "/dashboard/settings/storage",
permanent: false,
source: "/dashboard/infrastructure/storage",
},
{
source: "/dashboard/infrastructure/rpc-edge",
destination: "/chainlist",
permanent: false,
source: "/dashboard/infrastructure/rpc-edge",
},
{
source: "/solutions/commerce",
destination: "/solutions/loyalty",
permanent: false,
source: "/solutions/commerce",
},
{
source: "/hackathon/base-consumer-crypto",
destination: "/hackathon/consumer-crypto",
permanent: false,
source: "/hackathon/base-consumer-crypto",
},
{
source: "/bear-market-airdrop",
destination: "/",
permanent: false,
source: "/bear-market-airdrop",
},
{
source: "/drops/optimism",
destination: "/optimism",
permanent: false,
source: "/drops/optimism",
},
// Redirecting as ambassadors lives in community now
{
source: "/ambassadors",
destination: "/community/ambassadors",
permanent: false,
source: "/ambassadors",
},
{
source: "/embedded-wallets",
destination: "/in-app-wallets",
permanent: false,
source: "/embedded-wallets",
},
// temporarily redirect cli login to support page
{
source: "/cli/login",
destination:
"https://support.thirdweb.com/troubleshooting-errors/7Y1BqKNvtLdBv5fZkRZZB3/issue-linking-device-on-the-authorization-page-via-thirdweb-cli/cn9LRA3ax7XCP6uxwRYdvx",
"https://portal.thirdweb.com/knowledge-base/onchain-common-errors/thirdweb-cli/device-link-error",
permanent: false,
source: "/cli/login",
},
// temporary redirect gas -> explore page
{
source: "/gas",
destination: "/explore",
permanent: false,
source: "/gas",
},
{
destination: "/contracts/deployment-tool",
permanent: false,
source: "/deploy",
destination: "/contracts/deployment-tool",
permanent: false,
},
{
destination: "/contracts/deployment-tool",
permanent: false,
source: "/publish",
destination: "/contracts/deployment-tool",
permanent: false,
},
{
source: "/smart-contracts",
destination: "/contracts/explore",
permanent: false,
source: "/smart-contracts",
},
{
source: "/build",
destination: "/contracts/modular-contracts",
destination: "/sdk",
permanent: false,
},
{
source: "/ui-components",
destination: "/sdk",
permanent: false,
},
{
destination: "/sdk",
permanent: false,
source: "/interact",
destination: "/sdk",
permanent: false,
},
{
source: "/sponsored-transactions",
destination: "/account-abstraction",
permanent: false,
source: "/sponsored-transactions",
},
// redirect /solutions/chains to /solutions/ecosystem
{
source: "/solutions/chains",
destination: "/solutions/ecosystem",
permanent: false,
source: "/solutions/chains",
},
// redirect /rpc to portal
{
source: "/rpc-edge",
destination:
"https://portal.thirdweb.com/infrastructure/rpc-edge/overview",
permanent: false,
source: "/rpc-edge",
},
// redirect /sdk to portal
{
source: "/sdk",
destination: "https://portal.thirdweb.com/connect/blockchain-api",
permanent: false,
source: "/sdk",
},
// redirect `/events` to homepage
{
source: "/events",
destination: "/",
permanent: false,
source: "/events",
},
// redirect /community to /community/ambassadors
{
source: "/community",
destination: "/community/ambassadors",
permanent: false,
source: "/community",
},
// redirect `/tos` to `/terms`
{
source: "/tos",
destination: "/terms",
permanent: false,
source: "/tos",
},
// redirect `/privacy` to `/privacy-policy`
{
source: "/privacy",
destination: "/privacy-policy",
permanent: false,
source: "/privacy",
},
// redirect `/mission` to `/home`
{
source: "/mission",
destination: "/home",
permanent: false,
source: "/mission",
},
// redirect "/open-source" to "/bounties"
{
source: "/open-source",
destination: "/bounties",
permanent: false,
source: "/open-source",
},
// redirect /template/<slug> to /templates/<slug>
{
source: "/template/:slug",
destination: "/templates/:slug",
permanent: false,
source: "/template/:slug",
},
// redirect /account-abstraction to /connect/account-abstraction
// redirect /connect/pay to /universal-bridge
{
source: "/account-abstraction",
destination: "/connect/account-abstraction",
destination: "/universal-bridge",
permanent: false,
},
// redirect /connect/pay to /connect/universal-bridge
{
source: "/connect/pay",
destination: "/connect/universal-bridge",
permanent: false,
},
// redirect /auth to /connect/auth
{
source: "/auth",
destination: "/connect/auth",
permanent: false,
},
// redirect /in-app-wallets to /connect/in-app-wallets
{
source: "/in-app-wallets",
destination: "/connect/in-app-wallets",
permanent: false,
},
// PREVIOUS CAMPAIGNS
{
source: "/unlimited-wallets",
destination: "/",
permanent: false,
source: "/unlimited-wallets",
},
// pay > universal-bridge redirect
{
source: "/team/:team_slug/:project_slug/connect/pay/:path*",
destination:
"/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/:path*",
},
{
source: "/learn/tutorials",
destination: "/learn/guides",
permanent: false,
source: "/learn/tutorials",
},
// redirect to /grant/superchain to /superchain
{
source: "/grant/superchain",
destination: "/superchain",
permanent: false,
source: "/grant/superchain",
},
// connect -> build redirects
{
destination: "/wallets",
permanent: false,
source: "/connect",
},
{
destination: "/account-abstraction",
permanent: false,
source: "/connect/account-abstraction",
},
{
destination: "/universal-bridge",
permanent: false,
source: "/connect/universal-bridge",
},
{
destination: "/auth",
permanent: false,
source: "/connect/auth",
},
{
destination: "/in-app-wallets",
permanent: false,
source: "/connect/in-app-wallets",
},
{
destination: "/transactions",
permanent: false,
source: "/engine",
},
...legacyDashboardToTeamRedirects,
...projectPageRedirects,
];
}
+9 -9
View File
@@ -1,7 +1,7 @@
"use server";
import { getAuthToken } from "../../app/(app)/api/lib/getAuthToken";
import { NEXT_PUBLIC_THIRDWEB_API_HOST } from "../constants/public-envs";
import { getAuthToken } from "@/api/auth-token";
import { NEXT_PUBLIC_THIRDWEB_API_HOST } from "@/constants/public-envs";
export async function acceptInvite(options: {
teamId: string;
@@ -11,20 +11,20 @@ export async function acceptInvite(options: {
if (!token) {
return {
ok: false,
errorMessage: "You are not authorized to perform this action",
ok: false,
};
}
const res = await fetch(
`${NEXT_PUBLIC_THIRDWEB_API_HOST}/v1/teams/${options.teamId}/invites/${options.inviteId}/accept`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({}),
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
method: "POST",
},
);
@@ -42,8 +42,8 @@ export async function acceptInvite(options: {
} catch {}
return {
ok: false,
errorMessage,
ok: false,
};
}
+7 -7
View File
@@ -1,7 +1,7 @@
"use server";
import { getAuthToken } from "../../app/(app)/api/lib/getAuthToken";
import { NEXT_PUBLIC_THIRDWEB_API_HOST } from "../constants/public-envs";
import { getAuthToken } from "@/api/auth-token";
import { NEXT_PUBLIC_THIRDWEB_API_HOST } from "@/constants/public-envs";
export async function reSubscribePlan(options: {
teamId: string;
@@ -16,12 +16,12 @@ export async function reSubscribePlan(options: {
const res = await fetch(
`${NEXT_PUBLIC_THIRDWEB_API_HOST}/v1/teams/${options.teamId}/checkout/resubscribe-plan`,
{
method: "PUT",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({}),
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
method: "PUT",
},
);
+7 -7
View File
@@ -1,7 +1,7 @@
"use server";
import { getAuthToken } from "../../app/(app)/api/lib/getAuthToken";
import { NEXT_PUBLIC_THIRDWEB_API_HOST } from "../constants/public-envs";
import { getAuthToken } from "@/api/auth-token";
import { NEXT_PUBLIC_THIRDWEB_API_HOST } from "@/constants/public-envs";
export async function confirmEmailWithOTP(otp: string) {
const token = await getAuthToken();
@@ -15,14 +15,14 @@ export async function confirmEmailWithOTP(otp: string) {
const res = await fetch(
`${NEXT_PUBLIC_THIRDWEB_API_HOST}/v1/account/confirmEmail`,
{
method: "PUT",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({
confirmationToken: otp,
}),
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
method: "PUT",
},
);
+18 -21
View File
@@ -3,62 +3,59 @@ import "server-only";
// biome-ignore lint/style/useNodejsImportProtocol: breaks storybook if it's `node:` prefixed
import { randomBytes } from "crypto";
import type { Team } from "@/api/team";
import { format } from "date-fns";
import { getAuthToken } from "../../app/(app)/api/lib/getAuthToken";
import { NEXT_PUBLIC_THIRDWEB_API_HOST } from "../constants/public-envs";
import { getAuthToken } from "@/api/auth-token";
import type { Team } from "@/api/team";
import { NEXT_PUBLIC_THIRDWEB_API_HOST } from "@/constants/public-envs";
export async function createTeam(options?: {
name?: string;
slug?: string;
}) {
export async function createTeam(options?: { name?: string; slug?: string }) {
const token = await getAuthToken();
if (!token) {
return {
status: "error",
errorMessage: "You are not authorized to perform this action",
status: "error",
} as const;
}
const res = await fetch(`${NEXT_PUBLIC_THIRDWEB_API_HOST}/v1/teams`, {
method: "POST",
body: JSON.stringify({
billingEmail: null,
image: null,
name:
options?.name ?? `Your Projects ${format(new Date(), "MMM d yyyy")}`,
slug: options?.slug ?? randomBytes(20).toString("hex"),
}),
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
name:
options?.name ?? `Your Projects ${format(new Date(), "MMM d yyyy")}`,
slug: options?.slug ?? randomBytes(20).toString("hex"),
billingEmail: null,
image: null,
}),
method: "POST",
});
if (!res.ok) {
const reason = await res.text();
console.error("failed to create team", {
status: res.status,
reason,
status: res.status,
});
switch (res.status) {
case 400: {
return {
status: "error",
errorMessage: "Invalid team name or slug.",
status: "error",
} as const;
}
case 401: {
return {
status: "error",
errorMessage: "You are not authorized to perform this action.",
status: "error",
} as const;
}
default: {
return {
status: "error",
errorMessage: "An unknown error occurred.",
status: "error",
} as const;
}
}
@@ -69,7 +66,7 @@ export async function createTeam(options?: {
};
return {
status: "success",
data: json.result,
status: "success",
} as const;
}
+11 -13
View File
@@ -1,65 +1,63 @@
"use server";
import "server-only";
import { getAuthToken } from "../../app/(app)/api/lib/getAuthToken";
import { NEXT_PUBLIC_THIRDWEB_API_HOST } from "../constants/public-envs";
import { getAuthToken } from "@/api/auth-token";
import { NEXT_PUBLIC_THIRDWEB_API_HOST } from "@/constants/public-envs";
export async function deleteTeam(options: {
teamId: string;
}) {
export async function deleteTeam(options: { teamId: string }) {
const token = await getAuthToken();
if (!token) {
return {
status: "error",
errorMessage: "You are not authorized to perform this action.",
status: "error",
} as const;
}
const res = await fetch(
`${NEXT_PUBLIC_THIRDWEB_API_HOST}/v1/teams/${options.teamId}`,
{
method: "DELETE",
headers: {
Authorization: `Bearer ${token}`,
},
method: "DELETE",
},
);
// handle errors
if (!res.ok) {
const reason = await res.text();
console.error("failed to delete team", {
status: res.status,
reason,
status: res.status,
});
switch (res.status) {
case 400: {
return {
status: "error",
errorMessage: "Invalid team ID.",
status: "error",
} as const;
}
case 401: {
return {
status: "error",
errorMessage: "You are not authorized to perform this action.",
status: "error",
} as const;
}
case 403: {
return {
status: "error",
errorMessage: "You do not have permission to delete this team.",
status: "error",
} as const;
}
case 404: {
return {
status: "error",
errorMessage: "Team not found.",
status: "error",
} as const;
}
default: {
return {
status: "error",
errorMessage: "An unknown error occurred.",
status: "error",
} as const;
}
}
@@ -1,9 +1,9 @@
"use server";
import { defineDashboardChain } from "lib/defineDashboardChain";
import { ZERO_ADDRESS, isAddress, toTokens } from "thirdweb";
import { isAddress, toTokens, ZERO_ADDRESS } from "thirdweb";
import { getWalletBalance } from "thirdweb/wallets";
import { MORALIS_API_KEY } from "../constants/server-envs";
import { serverThirdwebClient } from "../constants/thirdweb-client.server";
import { MORALIS_API_KEY } from "@/constants/server-envs";
import { serverThirdwebClient } from "@/constants/thirdweb-client.server";
import { defineDashboardChain } from "@/lib/defineDashboardChain";
type BalanceQueryResponse = Array<{
balance: string;
@@ -43,12 +43,12 @@ export async function getTokenBalancesFromMoralis(params: {
});
return [
{
token_address: ZERO_ADDRESS,
symbol: balance.symbol,
name: "Native Token",
decimals: balance.decimals,
balance: balance.value.toString(),
decimals: balance.decimals,
display_balance: toTokens(balance.value, balance.decimals),
name: "Native Token",
symbol: balance.symbol,
token_address: ZERO_ADDRESS,
},
];
};
@@ -59,10 +59,10 @@ export async function getTokenBalancesFromMoralis(params: {
const tokenBalanceEndpoint = `https://deep-index.moralis.io/api/v2/${_address}/erc20?chain=${_chain}`;
const resp = await fetch(tokenBalanceEndpoint, {
method: "GET",
headers: {
"x-api-key": MORALIS_API_KEY,
},
method: "GET",
});
if (!resp.ok) {
@@ -83,7 +83,7 @@ export async function getTokenBalancesFromMoralis(params: {
]);
return {
error: undefined,
data: [...nativeBalance, ...tokenBalances],
error: undefined,
};
}
+24 -24
View File
@@ -1,19 +1,19 @@
"use server";
import { NEXT_PUBLIC_DASHBOARD_CLIENT_ID } from "@/constants/public-envs";
import { MORALIS_API_KEY } from "@/constants/server-envs";
import {
generateAlchemyUrl,
transformAlchemyResponseToNFT,
} from "lib/wallet/nfts/alchemy";
} from "@/lib/wallet/nfts/alchemy";
import {
generateMoralisUrl,
transformMoralisResponseToNFT,
} from "lib/wallet/nfts/moralis";
import type { WalletNFT } from "lib/wallet/nfts/types";
import { getVercelEnv } from "../../lib/vercel-utils";
import { isAlchemySupported } from "../../lib/wallet/nfts/isAlchemySupported";
import { isMoralisSupported } from "../../lib/wallet/nfts/isMoralisSupported";
import { NEXT_PUBLIC_DASHBOARD_CLIENT_ID } from "../constants/public-envs";
import { MORALIS_API_KEY } from "../constants/server-envs";
} from "@/lib/wallet/nfts/moralis";
import type { WalletNFT } from "@/lib/wallet/nfts/types";
import { getVercelEnv } from "@/utils/vercel";
import { isAlchemySupported } from "../lib/wallet/nfts/isAlchemySupported";
import { isMoralisSupported } from "../lib/wallet/nfts/isMoralisSupported";
type WalletNFTApiReturn =
| { result: WalletNFT[]; error?: undefined }
@@ -53,7 +53,7 @@ export async function getWalletNFTs(params: {
const parsedResponse = await response.json();
const result = await transformAlchemyResponseToNFT(parsedResponse, owner);
return { result, error: undefined };
return { error: undefined, result };
} catch (err) {
console.error("Error fetching NFTs", err);
return { error: "error parsing response" };
@@ -64,10 +64,10 @@ export async function getWalletNFTs(params: {
const url = generateMoralisUrl({ chainId, owner });
const response = await fetch(url, {
method: "GET",
headers: {
"X-API-Key": MORALIS_API_KEY,
},
method: "GET",
next: {
revalidate: 10, // cache for 10 seconds
},
@@ -156,8 +156,8 @@ async function getWalletNFTsFromInsight(params: {
if (!response.ok) {
const errorMessage = await response.text();
return {
ok: false,
error: errorMessage,
ok: false,
};
}
@@ -172,32 +172,32 @@ async function getWalletNFTsFromInsight(params: {
const walletNFTs = nftsResponse.data.map((nft) => {
const walletNFT: WalletNFT = {
id: nft.token_id,
chainId: nft.contract.chain_id,
contractAddress: nft.contract.address,
id: nft.token_id,
metadata: {
uri: isDev
? nft.metadata_url.replace("ipfscdn.io/", "thirdwebstorage-dev.com/")
: nft.metadata_url,
name: nft.name,
description: nft.description,
image: isDev
? nft.image_url.replace("ipfscdn.io/", "thirdwebstorage-dev.com/")
: nft.image_url,
animation_url: isDev
? nft.extra_metadata.animation_original_url?.replace(
"ipfscdn.io/",
"thirdwebstorage-dev.com/",
)
: nft.extra_metadata.animation_original_url,
external_url: nft.external_url,
background_color: nft.background_color,
description: nft.description,
external_url: nft.external_url,
image: isDev
? nft.image_url.replace("ipfscdn.io/", "thirdwebstorage-dev.com/")
: nft.image_url,
name: nft.name,
uri: isDev
? nft.metadata_url.replace("ipfscdn.io/", "thirdwebstorage-dev.com/")
: nft.metadata_url,
},
owner: params.owner,
tokenURI: nft.metadata_url,
type: nft.token_type === "erc721" ? "ERC721" : "ERC1155",
supply: nft.balance,
tokenAddress: nft.contract.address,
chainId: nft.contract.chain_id,
tokenURI: nft.metadata_url,
type: nft.token_type === "erc721" ? "ERC721" : "ERC1155",
};
return walletNFT;
+11 -11
View File
@@ -1,11 +1,11 @@
"use server";
import { getAuthToken } from "../../app/(app)/api/lib/getAuthToken";
import { getAuthToken } from "@/api/auth-token";
import {
NEXT_PUBLIC_ENGINE_CLOUD_URL,
NEXT_PUBLIC_THIRDWEB_API_HOST,
} from "../constants/public-envs";
import { ANALYTICS_SERVICE_URL } from "../constants/server-envs";
} from "@/constants/public-envs";
import { ANALYTICS_SERVICE_URL } from "@/constants/server-envs";
type ProxyActionParams = {
pathname: string;
@@ -47,35 +47,35 @@ async function proxy<T>(
}
const res = await fetch(url, {
method: params.method,
body: params.body,
headers: {
...params.headers,
...(authToken ? { Authorization: `Bearer ${authToken}` } : {}),
},
body: params.body,
method: params.method,
});
if (!res.ok) {
try {
const errorMessage = await res.text();
return {
status: res.status,
ok: false,
error: errorMessage || res.statusText,
ok: false,
status: res.status,
};
} catch {
return {
status: res.status,
ok: false,
error: res.statusText,
ok: false,
status: res.status,
};
}
}
return {
status: res.status,
ok: true,
data: params.parseAsText ? await res.text() : await res.json(),
ok: true,
status: res.status,
};
}
@@ -1,7 +1,7 @@
"use server";
import { getAuthToken } from "../../app/(app)/api/lib/getAuthToken";
import { NEXT_PUBLIC_THIRDWEB_API_HOST } from "../constants/public-envs";
import { getAuthToken } from "@/api/auth-token";
import { NEXT_PUBLIC_THIRDWEB_API_HOST } from "@/constants/public-envs";
export async function sendTeamInvites(options: {
teamId: string;
@@ -20,8 +20,8 @@ export async function sendTeamInvites(options: {
if (!token) {
return {
ok: false,
errorMessage: "You are not authorized to perform this action",
ok: false,
};
}
@@ -43,15 +43,15 @@ async function sendInvite(
const res = await fetch(
`${NEXT_PUBLIC_THIRDWEB_API_HOST}/v1/teams/${teamId}/invites`,
{
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
inviteEmail: invite.email,
inviteRole: invite.role,
}),
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
method: "POST",
},
);
@@ -59,8 +59,8 @@ async function sendInvite(
const errorMessage = await res.text();
return {
email: invite.email,
ok: false,
errorMessage,
ok: false,
};
}
@@ -1,8 +1,8 @@
import "server-only";
import Stripe from "stripe";
import type { Team } from "../api/team";
import { STRIPE_SECRET_KEY } from "../constants/server-envs";
import type { Team } from "@/api/team";
import { STRIPE_SECRET_KEY } from "@/constants/server-envs";
let existingStripe: Stripe | undefined;
@@ -1,6 +1,6 @@
"use server";
import { getAuthToken } from "../../app/(app)/api/lib/getAuthToken";
import { NEXT_PUBLIC_THIRDWEB_API_HOST } from "../constants/public-envs";
import { getAuthToken } from "@/api/auth-token";
import { NEXT_PUBLIC_THIRDWEB_API_HOST } from "@/constants/public-envs";
export async function updateAccount(values: {
name?: string;
@@ -14,12 +14,12 @@ export async function updateAccount(values: {
}
const res = await fetch(`${NEXT_PUBLIC_THIRDWEB_API_HOST}/v1/account`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
},
body: JSON.stringify(values),
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
method: "PUT",
});
if (!res.ok) {
+67
View File
@@ -0,0 +1,67 @@
# Analytics Guidelines
This folder centralises the **PostHog** tracking logic for the dashboard app.
Most developers will only need to add or extend _event-reporting_ functions in `report.ts`.
---
## 1. When to add an event
1. Ask yourself if the data will be **actionable**. Every event should have a clear product or business question it helps answer.
2. Check if a similar event already exists in `report.ts`. Avoid duplicates.
---
## 2. Naming conventions
| Concept | Convention | Example |
|---------|------------|---------|
| **Event name** (string sent to PostHog) | Human-readable phrase formatted as `<subject> <verb>` | `"contract deployed"` |
| **Reporting function** | `report<Subject><Verb>` (PascalCase) | `reportContractDeployed` |
| **File** | All event functions live in the shared `report.ts` file (for now) | — |
> Keeping names predictable makes it easy to search both code and analytics.
---
## 3. Boilerplate / template
Add a new function to `report.ts` following this pattern:
```ts
/**
* ### Why do we need to report this event?
* - _Add bullet points explaining the product metrics/questions this event answers._
*
* ### Who is responsible for this event?
* @your-github-handle
*/
export function reportExampleEvent(properties: {
/* Add typed properties here */
}) {
posthog.capture("example event", {
/* Pass the same properties here */
});
}
```
Guidelines:
1. **Explain the "why".** The JSDoc block is mandatory so future contributors know the purpose.
2. **Type everything.** The `properties` object should be fully typed—this doubles as documentation.
3. **Client-side only.** `posthog-js` must never run on the server. Call these reporting helpers from client components, event handlers, etc.
---
## 4. Editing or removing events
1. Update both the function and the PostHog event definition (if required).
2. Inform the core services team before removing or renaming an event.
---
## 5. Identification & housekeeping (FYI)
Most devs can ignore this section, but for completeness:
- `hooks/identify-account.ts` and `hooks/identify-team.ts` wrap `posthog.identify`/`group` calls.
- `resetAnalytics` clears identity state (used on logout).
---
## 6. Need help?
Ping #eng-core-services in slack.
@@ -0,0 +1,20 @@
"use client";
import posthog from "posthog-js";
import { useEffect } from "react";
export function useIdentifyAccount(opts?: {
accountId: string;
email?: string;
}) {
// eslint-disable-next-line no-restricted-syntax
useEffect(() => {
// if no accountId, don't identify
if (!opts?.accountId) {
return;
}
// if email is provided, add it to the identify
posthog.identify(opts.accountId, opts.email ? { email: opts.email } : {});
}, [opts?.accountId, opts?.email]);
}
@@ -0,0 +1,17 @@
"use client";
import posthog from "posthog-js";
import { useEffect } from "react";
export function useIdentifyTeam(opts?: { teamId: string }) {
// eslint-disable-next-line no-restricted-syntax
useEffect(() => {
// if no teamId, don't identify
if (!opts?.teamId) {
return;
}
// identify the team
posthog.group("team", opts.teamId);
}, [opts?.teamId]);
}
+382
View File
@@ -0,0 +1,382 @@
import posthog from "posthog-js";
import type { Team } from "@/api/team";
// ----------------------------
// CONTRACTS
// ----------------------------
/**
* ### Why do we need to report this event?
* - To track the number of contracts deployed
* - To track the number of contracts deployed on each chain
* - To track if the contract was deployed on the token page vs on the deploy page
*
* ### Who is responsible for this event?
* @jnsdls
*
*/
export function reportContractDeployed(properties: {
address: string;
chainId: number;
publisher: string | undefined;
contractName: string | undefined;
deploymentType?: "asset";
}) {
posthog.capture("contract deployed", properties);
}
/**
* ### Why do we need to report this event?
* - To track the number of contracts that failed to deploy
* - To track the error message of the failed contract deployment (so we can fix it / add workarounds)
*
* ### Who is responsible for this event?
* @jnsdls
*
*/
export function reportContractDeployFailed(properties: {
errorMessage: string;
chainId: number;
publisher: string | undefined;
contractName: string | undefined;
}) {
posthog.capture("contract deploy failed", properties);
}
/**
* ### Why do we need to report this event?
* - To track the number of contracts published
* - To understand the type of contracts published
* - To understand who publishes contracts
*
* ### Who is responsible for this event?
* @jnsdls
*
*/
export function reportContractPublished(properties: {
publisher: string;
contractName: string;
version: string;
deployType: string | undefined;
}) {
posthog.capture("contract published", properties);
}
// ----------------------------
// ONBOARDING (TEAM)
// ----------------------------
/**
* ### Why do we need to report this event?
* - To track the number of teams that enter the onboarding flow
*
* ### Who is responsible for this event?
* @jnsdls
*
*/
export function reportOnboardingStarted() {
posthog.capture("onboarding started");
}
/**
* ### Why do we need to report this event?
* - To track the number of teams that select a paid plan during onboarding
* - To know **which** plan was selected
*
* ### Who is responsible for this event?
* @jnsdls
*
*/
export function reportOnboardingPlanSelected(properties: {
plan: Team["billingPlan"];
}) {
posthog.capture("onboarding plan selected", properties);
}
/**
* ### Why do we need to report this event?
* - To track the number of teams that skip the plan-selection step during onboarding
*
* ### Who is responsible for this event?
* @jnsdls
*
*/
export function reportOnboardingPlanSelectionSkipped() {
posthog.capture("onboarding plan selection skipped");
}
/**
* ### Why do we need to report this event?
* - To track the number of teams that invite members during onboarding
* - To track **how many** members were invited
*
* ### Who is responsible for this event?
* @jnsdls
*
*/
export function reportOnboardingMembersInvited(properties: { count: number }) {
posthog.capture("onboarding members invited", {
count: properties.count,
});
}
/**
* ### Why do we need to report this event?
* - To track the number of teams that skip inviting members during onboarding
*
* ### Who is responsible for this event?
* @jnsdls
*
*/
export function reportOnboardingMembersSkipped() {
posthog.capture("onboarding members skipped");
}
/**
* ### Why do we need to report this event?
* - To track how many teams click the upsell (upgrade) button on the member-invite step during onboarding
*
* ### Who is responsible for this event?
* @jnsdls
*
*/
export function reportOnboardingMembersUpsellButtonClicked() {
posthog.capture("onboarding members upsell clicked");
}
/**
* ### Why do we need to report this event?
* - To track which plan is selected from the members-step upsell during onboarding
*
* ### Who is responsible for this event?
* @jnsdls
*
*/
export function reportOnboardingMembersUpsellPlanSelected(properties: {
plan: Team["billingPlan"];
}) {
posthog.capture("onboarding members upsell plan selected", properties);
}
/**
* ### Why do we need to report this event?
* - To track the number of teams that completed onboarding
*
* ### Who is responsible for this event?
* @jnsdls
*
*/
export function reportOnboardingCompleted() {
posthog.capture("onboarding completed");
}
// ----------------------------
// FAUCET
// ----------------------------
/**
* ### Why do we need to report this event?
* - To track which chain the faucet was used on
* - To track how popular specific faucets are
*
* ### Who is responsible for this event?
* @jnsdls
*
*/
export function reportFaucetUsed(properties: { chainId: number }) {
posthog.capture("faucet used", {
chainId: properties.chainId,
});
}
// ----------------------------
// CHAIN CONFIGURATION
// ----------------------------
/**
* ### Why do we need to report this event?
* - To track which custom chains customers are adding that we may want to add to the app
*
* ### Who is responsible for this event?
* @jnsdls
*
*/
export function reportChainConfigurationAdded(properties: {
chainId: number;
chainName: string;
rpcURLs: readonly string[];
nativeCurrency: {
name: string;
symbol: string;
decimals: number;
};
}) {
posthog.capture("chain configuration added", {
chainId: properties.chainId,
chainName: properties.chainName,
nativeCurrency: properties.nativeCurrency,
rpcURLs: properties.rpcURLs,
});
}
// ----------------------------
// ASSETS
// ----------------------------
type AssetContractType = "DropERC20" | "DropERC1155" | "DropERC721";
/**
* ### Why do we need to report this event?
* - To track number of successful asset purchases from the token page
* - To track which asset and contract types are being purchased the most
*
* ### Who is responsible for this event?
* @MananTank
*/
export function reportAssetBuySuccessful(properties: {
chainId: number;
contractType: AssetContractType;
assetType: "nft" | "coin";
}) {
posthog.capture("asset buy successful", {
assetType: properties.assetType,
chainId: properties.chainId,
contractType: properties.contractType,
});
}
/**
* ### Why do we need to report this event?
* - To track number of failed asset purchases from the token page
* - To track the errors that users encounter when trying to purchase an asset
*
* ### Who is responsible for this event?
* @MananTank
*/
export function reportAssetBuyFailed(properties: {
chainId: number;
contractType: AssetContractType;
assetType: "nft" | "coin";
error: string;
}) {
posthog.capture("asset buy failed", {
assetType: properties.assetType,
chainId: properties.chainId,
contractType: properties.contractType,
error: properties.error,
});
}
// Assets Landing Page ----------------------------
/**
* ### Why do we need to report this event?
* - To track number of asset creation started from the assets page
* - To track which asset types are being created the most
*
* ### Who is responsible for this event?
* @MananTank
*/
export function reportAssetCreationStarted(properties: {
assetType: "nft" | "coin";
}) {
posthog.capture("asset creation started", {
assetType: properties.assetType,
});
}
/**
* ### Why do we need to report this event?
* - To track number of assets imported successfully from the assets page
*
* ### Who is responsible for this event?
* @MananTank
*/
export function reportAssetImportSuccessful() {
posthog.capture("asset import successful");
}
/**
* ### Why do we need to report this event?
* - To track number of asset import started in the assets page
*
* ### Who is responsible for this event?
* @MananTank
*/
export function reportAssetImportStarted() {
posthog.capture("asset import started");
}
/**
* ### Why do we need to report this event?
* - To track the steps users are configuring in the asset creation to understand if there are any drop-offs
*
* ### Who is responsible for this event?
* @MananTank
*/
export function reportAssetCreationStepConfigured(
properties:
| {
assetType: "nft";
step: "collection-info" | "upload-assets" | "sales-settings";
}
| {
assetType: "coin";
step: "coin-info" | "token-distribution" | "launch-coin";
},
) {
posthog.capture("asset creation step configured", {
assetType: properties.assetType,
step: properties.step,
});
}
/**
* ### Why do we need to report this event?
* - To track number of successful asset creations
* - To track which asset types are being created the most
*
* ### Who is responsible for this event?
* @MananTank
*/
export function reportAssetCreationSuccessful(properties: {
assetType: "nft" | "coin";
contractType: AssetContractType;
}) {
posthog.capture("asset creation successful", {
assetType: properties.assetType,
contractType: properties.contractType,
});
}
/**
* ### Why do we need to report this event?
* - To track number of failed asset creations
* - To track the errors that users encounter when trying to create an asset
* - To track the step that is failing in the asset creation
*
* ### Who is responsible for this event?
* @MananTank
*/
export function reportAssetCreationFailed(
properties: { contractType: AssetContractType; error: string } & (
| {
assetType: "nft";
step: "deploy-contract" | "mint-nfts" | "set-claim-conditions";
}
| {
assetType: "coin";
step:
| "deploy-contract"
| "set-claim-conditions"
| "mint-tokens"
| "airdrop-tokens";
}
),
) {
posthog.capture("asset creation failed", {
assetType: properties.assetType,
contractType: properties.contractType,
error: properties.error,
step: properties.step,
});
}
+7
View File
@@ -0,0 +1,7 @@
"use client";
import posthog from "posthog-js";
export function resetAnalytics() {
posthog.reset();
}
+11 -11
View File
@@ -1,5 +1,6 @@
import "server-only";
import { ANALYTICS_SERVICE_URL } from "@/constants/server-envs";
import type {
AnalyticsQueryParams,
EcosystemWalletStats,
@@ -12,9 +13,8 @@ import type {
UserOpStats,
WalletStats,
WalletUserStats,
} from "types/analytics";
import { getAuthToken } from "../../app/(app)/api/lib/getAuthToken";
import { ANALYTICS_SERVICE_URL } from "../constants/server-envs";
} from "@/types/analytics";
import { getAuthToken } from "./auth-token";
import { getChains } from "./chain";
async function fetchAnalytics(
@@ -175,9 +175,9 @@ export async function getAggregateUserOpUsage(
},
{
date: (params.from || new Date()).toISOString(),
successful: 0,
failed: 0,
sponsoredUsd: 0,
successful: 0,
},
);
}
@@ -279,14 +279,14 @@ export async function isProjectActive(params: {
);
return {
bundler: false,
storage: false,
rpc: false,
nebula: false,
sdk: false,
insight: false,
pay: false,
inAppWallet: false,
ecosystemWallet: false,
inAppWallet: false,
insight: false,
nebula: false,
pay: false,
rpc: false,
sdk: false,
storage: false,
} as ActiveStatus;
}
+9 -9
View File
@@ -1,8 +1,8 @@
"use server";
import "server-only";
import { getAuthToken } from "../../app/(app)/api/lib/getAuthToken";
import { NEXT_PUBLIC_THIRDWEB_API_HOST } from "../constants/public-envs";
import { NEXT_PUBLIC_THIRDWEB_API_HOST } from "@/constants/public-envs";
import { getAuthToken } from "./auth-token";
export type AuditLogEntry = {
who: {
@@ -58,34 +58,34 @@ export async function getAuditLogs(teamSlug: string, cursor?: string) {
url.searchParams.set("take", "15");
const response = await fetch(url, {
headers: {
Authorization: `Bearer ${authToken}`,
},
next: {
// revalidate this query once per 10 seconds (does not need to be more granular than that)
revalidate: 10,
},
headers: {
Authorization: `Bearer ${authToken}`,
},
});
if (!response.ok) {
// if the status is 402, the most likely reason is that the team is on a free plan
if (response.status === 402) {
return {
status: "error",
reason: "higher_plan_required",
status: "error",
} as const;
}
const body = await response.text();
return {
status: "error",
reason: "unknown",
body,
reason: "unknown",
status: "error",
} as const;
}
const data = (await response.json()) as AuditLogApiResponse;
return {
status: "success",
data,
status: "success",
} as const;
}
@@ -1,7 +1,7 @@
import { COOKIE_ACTIVE_ACCOUNT, COOKIE_PREFIX_TOKEN } from "@/constants/cookie";
import { getClientThirdwebClient } from "@/constants/thirdweb-client.client";
import { LAST_USED_TEAM_ID } from "constants/cookies";
import { cookies } from "next/headers";
import { COOKIE_ACTIVE_ACCOUNT, COOKIE_PREFIX_TOKEN } from "@/constants/cookie";
import { LAST_USED_TEAM_ID } from "@/constants/cookies";
import { getClientThirdwebClient } from "@/constants/thirdweb-client.client";
export async function getAuthToken() {
const cookiesManager = await cookies();
+3 -3
View File
@@ -1,8 +1,8 @@
import "server-only";
import type { ChainMetadata } from "thirdweb/chains";
import type { ChainService } from "../../app/(app)/(dashboard)/(chain)/types/chain";
import { NEXT_PUBLIC_THIRDWEB_API_HOST } from "../constants/public-envs";
import { API_SERVER_SECRET } from "../constants/server-envs";
import { NEXT_PUBLIC_THIRDWEB_API_HOST } from "@/constants/public-envs";
import { API_SERVER_SECRET } from "@/constants/server-envs";
import type { ChainService } from "@/types/chain";
export async function getGasSponsoredChains() {
if (!API_SERVER_SECRET) {
@@ -0,0 +1,40 @@
"use server";
import "server-only";
import { NEXT_PUBLIC_THIRDWEB_API_HOST } from "@/constants/public-envs";
import { getAuthToken } from "./auth-token";
export async function createDedicatedSupportChannel(
teamIdOrSlug: string,
channelType: "slack" | "telegram",
): Promise<{ error: string | null }> {
const token = await getAuthToken();
if (!token) {
return { error: "Unauthorized" };
}
const res = await fetch(
new URL(
`/v1/teams/${teamIdOrSlug}/dedicated-support-channel`,
NEXT_PUBLIC_THIRDWEB_API_HOST,
),
{
body: JSON.stringify({
type: channelType,
}),
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
method: "POST",
},
);
if (!res.ok) {
const json = await res.json();
return {
error:
json.error?.message ?? "Failed to create dedicated support channel.",
};
}
return { error: null };
}
@@ -1,4 +1,5 @@
import "server-only";
import { getAddress } from "thirdweb";
import { NEXT_PUBLIC_THIRDWEB_API_HOST } from "@/constants/public-envs";
export type ProjectContract = {
@@ -43,3 +44,43 @@ export async function getProjectContracts(options: {
return data.result;
}
export type PartialProject = {
id: string;
name: string;
slug: string;
image: string | null;
};
/**
* get a list of projects within a team that have a given contract imported
*/
export async function getContractImportedProjects(options: {
teamId: string;
authToken: string;
chainId: number;
contractAddress: string;
}) {
const url = new URL(
`${NEXT_PUBLIC_THIRDWEB_API_HOST}/v1/teams/${options.teamId}/projects/contracts/lookup?chainId=${options.chainId}&contractAddress=${getAddress(options.contractAddress)}`,
);
const res = await fetch(url, {
headers: {
Authorization: `Bearer ${options.authToken}`,
},
});
if (!res.ok) {
const errorMessage = await res.text();
console.error("Error fetching: /projects/contracts/lookup");
console.error(errorMessage);
return [];
}
const data = (await res.json()) as {
result: PartialProject[];
};
return data.result;
}
+15 -15
View File
@@ -1,8 +1,8 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
"use server";
import { getAuthToken } from "app/(app)/api/lib/getAuthToken";
import { THIRDWEB_INSIGHT_API_DOMAIN } from "constants/urls";
import { getAuthToken } from "@/api/auth-token";
import { THIRDWEB_INSIGHT_API_DOMAIN } from "@/constants/urls";
export interface WebhookResponse {
id: string;
@@ -80,13 +80,13 @@ export async function createWebhook(
const response = await fetch(
`https://${THIRDWEB_INSIGHT_API_DOMAIN}/v1/webhooks`,
{
method: "POST",
body: JSON.stringify(payload),
headers: {
Authorization: `Bearer ${authToken}`,
"Content-Type": "application/json",
"x-client-id": clientId,
Authorization: `Bearer ${authToken}`,
},
body: JSON.stringify(payload),
method: "POST",
},
);
@@ -115,11 +115,11 @@ export async function getWebhooks(
const response = await fetch(
`https://${THIRDWEB_INSIGHT_API_DOMAIN}/v1/webhooks`,
{
method: "GET",
headers: {
"x-client-id": clientId,
Authorization: `Bearer ${authToken}`,
"x-client-id": clientId,
},
method: "GET",
},
);
@@ -149,11 +149,11 @@ export async function deleteWebhook(
const response = await fetch(
`https://${THIRDWEB_INSIGHT_API_DOMAIN}/v1/webhooks/${encodeURIComponent(webhookId)}`,
{
method: "DELETE",
headers: {
"x-client-id": clientId,
Authorization: `Bearer ${authToken}`,
"x-client-id": clientId,
},
method: "DELETE",
},
);
@@ -183,29 +183,29 @@ export async function testWebhook(
const response = await fetch(
`https://${THIRDWEB_INSIGHT_API_DOMAIN}/v1/webhooks/test`,
{
method: "POST",
body: JSON.stringify(payload),
headers: {
Authorization: `Bearer ${authToken}`,
"Content-Type": "application/json",
"x-client-id": clientId,
Authorization: `Bearer ${authToken}`,
},
body: JSON.stringify(payload),
method: "POST",
},
);
if (!response.ok) {
const errorText = await response.text();
return {
success: false,
error: `Failed to test webhook: ${errorText}`,
success: false,
};
}
return (await response.json()) as TestWebhookResponse;
} catch (error) {
return {
success: false,
error: `Network or parsing error: ${error instanceof Error ? error.message : "Unknown error"}`,
success: false,
};
}
}
@@ -215,10 +215,10 @@ export async function getSupportedWebhookChains(): Promise<SupportedWebhookChain
const response = await fetch(
`https://${THIRDWEB_INSIGHT_API_DOMAIN}/service/chains`,
{
method: "GET",
headers: {
"x-service-api-key": process.env.INSIGHT_SERVICE_API_KEY || "",
},
method: "GET",
},
);
+2 -2
View File
@@ -1,5 +1,5 @@
import { getAuthToken } from "../../app/(app)/api/lib/getAuthToken";
import { NEXT_PUBLIC_THIRDWEB_API_HOST } from "../constants/public-envs";
import { NEXT_PUBLIC_THIRDWEB_API_HOST } from "@/constants/public-envs";
import { getAuthToken } from "./auth-token";
export type LinkedWallet = {
createdAt: string;
+16 -16
View File
@@ -1,8 +1,8 @@
"use server";
import "server-only";
import { getAuthToken } from "../../app/(app)/api/lib/getAuthToken";
import { NEXT_PUBLIC_THIRDWEB_API_HOST } from "../constants/public-envs";
import { NEXT_PUBLIC_THIRDWEB_API_HOST } from "@/constants/public-envs";
import { getAuthToken } from "./auth-token";
export type Notification = {
id: string;
@@ -41,17 +41,17 @@ export async function getUnreadNotifications(cursor?: string) {
if (!response.ok) {
const body = await response.text();
return {
status: "error",
reason: "unknown",
body,
reason: "unknown",
status: "error",
} as const;
}
const data = (await response.json()) as NotificationsApiResponse;
return {
status: "success",
data,
status: "success",
} as const;
}
@@ -77,17 +77,17 @@ export async function getArchivedNotifications(cursor?: string) {
if (!response.ok) {
const body = await response.text();
return {
status: "error",
reason: "unknown",
body,
reason: "unknown",
status: "error",
} as const;
}
const data = (await response.json()) as NotificationsApiResponse;
return {
status: "success",
data,
status: "success",
} as const;
}
@@ -109,9 +109,9 @@ export async function getUnreadNotificationsCount() {
if (!response.ok) {
const body = await response.text();
return {
status: "error",
reason: "unknown",
body,
reason: "unknown",
status: "error",
} as const;
}
const data = (await response.json()) as {
@@ -120,8 +120,8 @@ export async function getUnreadNotificationsCount() {
};
};
return {
status: "success",
data,
status: "success",
} as const;
}
@@ -135,20 +135,20 @@ export async function markNotificationAsRead(notificationId?: string) {
NEXT_PUBLIC_THIRDWEB_API_HOST,
);
const response = await fetch(url, {
method: "PUT",
// if notificationId is provided, mark it as read, otherwise mark all as read
body: JSON.stringify(notificationId ? { notificationId } : {}),
headers: {
Authorization: `Bearer ${authToken}`,
"Content-Type": "application/json",
},
// if notificationId is provided, mark it as read, otherwise mark all as read
body: JSON.stringify(notificationId ? { notificationId } : {}),
method: "PUT",
});
if (!response.ok) {
const body = await response.text();
return {
status: "error",
reason: "unknown",
body,
reason: "unknown",
status: "error",
} as const;
}
return {
+2 -2
View File
@@ -1,7 +1,7 @@
import "server-only";
import { NEXT_PUBLIC_THIRDWEB_API_HOST } from "@/constants/public-envs";
import type { ProjectResponse } from "@thirdweb-dev/service-utils";
import { getAuthToken } from "../../app/(app)/api/lib/getAuthToken";
import { NEXT_PUBLIC_THIRDWEB_API_HOST } from "@/constants/public-envs";
import { getAuthToken } from "./auth-token";
export type Project = ProjectResponse;
+2 -2
View File
@@ -1,6 +1,6 @@
import "server-only";
import { NEXT_PUBLIC_THIRDWEB_API_HOST } from "../constants/public-envs";
import { API_SERVER_SECRET } from "../constants/server-envs";
import { NEXT_PUBLIC_THIRDWEB_API_HOST } from "@/constants/public-envs";
import { API_SERVER_SECRET } from "@/constants/server-envs";
export type SMSCountryTiers = {
tier1: string[];
+2 -2
View File
@@ -1,5 +1,5 @@
import { getAuthToken } from "../../app/(app)/api/lib/getAuthToken";
import { NEXT_PUBLIC_THIRDWEB_API_HOST } from "../constants/public-envs";
import { NEXT_PUBLIC_THIRDWEB_API_HOST } from "@/constants/public-envs";
import { getAuthToken } from "./auth-token";
export type TeamInvite = {
id: string;
+2 -2
View File
@@ -1,10 +1,10 @@
import "server-only";
import { NEXT_PUBLIC_THIRDWEB_API_HOST } from "@/constants/public-envs";
import { getAuthToken } from "../../app/(app)/api/lib/getAuthToken";
import { getAuthToken } from "./auth-token";
const TeamAccountRole = {
OWNER: "OWNER",
MEMBER: "MEMBER",
OWNER: "OWNER",
} as const;
export type TeamAccountRole =
@@ -1,6 +1,6 @@
import { getAuthToken } from "../../app/(app)/api/lib/getAuthToken";
import { NEXT_PUBLIC_THIRDWEB_API_HOST } from "../constants/public-envs";
import type { ProductSKU } from "../lib/billing";
import { getAuthToken } from "@/api/auth-token";
import { NEXT_PUBLIC_THIRDWEB_API_HOST } from "@/constants/public-envs";
import type { ProductSKU } from "@/types/billing";
type InvoiceLine = {
// amount for this line item
+4 -4
View File
@@ -1,11 +1,11 @@
import "server-only";
import { NEXT_PUBLIC_THIRDWEB_API_HOST } from "@/constants/public-envs";
import { API_SERVER_SECRET } from "@/constants/server-envs";
import type { TeamResponse } from "@thirdweb-dev/service-utils";
import { cookies } from "next/headers";
import { LAST_USED_TEAM_ID } from "@/constants/cookies";
import { NEXT_PUBLIC_THIRDWEB_API_HOST } from "@/constants/public-envs";
import { API_SERVER_SECRET } from "@/constants/server-envs";
import { getValidAccount } from "../../app/(app)/account/settings/getAccount";
import { getAuthToken } from "../../app/(app)/api/lib/getAuthToken";
import { LAST_USED_TEAM_ID } from "../../constants/cookies";
import { getAuthToken } from "./auth-token";
import { getMemberByAccountId } from "./team-members";
export type Team = TeamResponse & { stripeCustomerId: string | null };
@@ -1,6 +1,6 @@
"use server";
import { getAuthToken } from "app/(app)/api/lib/getAuthToken";
import { NEXT_PUBLIC_THIRDWEB_BRIDGE_HOST } from "../../constants/public-envs";
import { getAuthToken } from "@/api/auth-token";
import { NEXT_PUBLIC_THIRDWEB_BRIDGE_HOST } from "@/constants/public-envs";
const UB_BASE_URL = NEXT_PUBLIC_THIRDWEB_BRIDGE_HOST;
@@ -14,19 +14,16 @@ type Webhook = {
version?: number; // TODO (UB) make this mandatory after migration
};
export async function getWebhooks(props: {
clientId: string;
teamId: string;
}) {
export async function getWebhooks(props: { clientId: string; teamId: string }) {
const authToken = await getAuthToken();
const res = await fetch(`${UB_BASE_URL}/v1/developer/webhooks`, {
method: "GET",
headers: {
Authorization: `Bearer ${authToken}`,
"Content-Type": "application/json",
"x-client-id": props.clientId,
"x-team-id": props.teamId,
Authorization: `Bearer ${authToken}`,
},
method: "GET",
});
if (!res.ok) {
@@ -49,19 +46,19 @@ export async function createWebhook(props: {
const authToken = await getAuthToken();
const res = await fetch(`${UB_BASE_URL}/v1/developer/webhooks`, {
method: "POST",
body: JSON.stringify({
url: props.url,
label: props.label,
version: props.version,
secret: props.secret,
url: props.url,
version: props.version,
}),
headers: {
Authorization: `Bearer ${authToken}`,
"Content-Type": "application/json",
"x-client-id": props.clientId,
"x-team-id": props.teamId,
Authorization: `Bearer ${authToken}`,
},
method: "POST",
});
if (!res.ok) {
@@ -81,13 +78,13 @@ export async function deleteWebhook(props: {
const res = await fetch(
`${UB_BASE_URL}/v1/developer/webhooks/${props.webhookId}`,
{
method: "DELETE",
headers: {
Authorization: `Bearer ${authToken}`,
"Content-Type": "application/json",
"x-client-id": props.clientId,
"x-team-id": props.teamId,
Authorization: `Bearer ${authToken}`,
},
method: "DELETE",
},
);
@@ -106,19 +103,16 @@ export type Fee = {
updatedAt: string;
};
export async function getFees(props: {
clientId: string;
teamId: string;
}) {
export async function getFees(props: { clientId: string; teamId: string }) {
const authToken = await getAuthToken();
const res = await fetch(`${UB_BASE_URL}/v1/developer/fees`, {
method: "GET",
headers: {
"Content-Type": "application/json",
"x-team-id": props.teamId,
"x-client-id": props.clientId,
Authorization: `Bearer ${authToken}`,
"Content-Type": "application/json",
"x-client-id": props.clientId,
"x-team-id": props.teamId,
},
method: "GET",
});
if (!res.ok) {
@@ -138,17 +132,17 @@ export async function updateFee(props: {
}) {
const authToken = await getAuthToken();
const res = await fetch(`${UB_BASE_URL}/v1/developer/fees`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
"x-team-id": props.teamId,
"x-client-id": props.clientId,
Authorization: `Bearer ${authToken}`,
},
body: JSON.stringify({
feeRecipient: props.feeRecipient,
feeBps: props.feeBps,
feeRecipient: props.feeRecipient,
}),
headers: {
Authorization: `Bearer ${authToken}`,
"Content-Type": "application/json",
"x-client-id": props.clientId,
"x-team-id": props.teamId,
},
method: "PUT",
});
if (!res.ok) {
@@ -225,13 +219,13 @@ export async function getPayments(props: {
}
const res = await fetch(url, {
method: "GET",
headers: {
Authorization: `Bearer ${authToken}`,
"Content-Type": "application/json",
"x-client-id": props.clientId,
"x-team-id": props.teamId,
Authorization: `Bearer ${authToken}`,
},
method: "GET",
});
if (!res.ok) {
@@ -18,15 +18,13 @@ type PaymentLink = {
purchaseData: Record<string, unknown> | undefined;
};
export async function getPaymentLink(props: {
paymentId: string;
}) {
export async function getPaymentLink(props: { paymentId: string }) {
const res = await fetch(`${UB_BASE_URL}/v1/links/${props.paymentId}`, {
method: "GET",
headers: {
"Content-Type": "application/json",
"x-secret-key": DASHBOARD_THIRDWEB_SECRET_KEY,
},
method: "GET",
});
if (!res.ok) {
@@ -36,12 +34,12 @@ export async function getPaymentLink(props: {
const { data } = await res.json();
return {
clientId: data.clientId,
title: data.title,
imageUrl: data.imageUrl,
receiver: data.receiver,
destinationToken: data.destinationToken,
amount: data.amount ? BigInt(data.amount) : undefined,
clientId: data.clientId,
destinationToken: data.destinationToken,
imageUrl: data.imageUrl,
purchaseData: data.purchaseData,
receiver: data.receiver,
title: data.title,
} as PaymentLink;
}
@@ -1,7 +1,7 @@
"use server";
import { DASHBOARD_THIRDWEB_SECRET_KEY } from "@/constants/server-envs";
import type { ProjectResponse } from "@thirdweb-dev/service-utils";
import { getAuthToken } from "app/(app)/api/lib/getAuthToken";
import { getAuthToken } from "@/api/auth-token";
import { DASHBOARD_THIRDWEB_SECRET_KEY } from "@/constants/server-envs";
import { UB_BASE_URL } from "./constants";
export type TokenMetadata = {
@@ -13,9 +13,7 @@ export type TokenMetadata = {
iconUri?: string;
};
export async function getUniversalBridgeTokens(props: {
chainId?: number;
}) {
export async function getUniversalBridgeTokens(props: { chainId?: number }) {
const url = new URL(`${UB_BASE_URL}/v1/tokens`);
if (props.chainId) {
@@ -24,11 +22,11 @@ export async function getUniversalBridgeTokens(props: {
url.searchParams.append("limit", "1000");
const res = await fetch(url.toString(), {
method: "GET",
headers: {
"Content-Type": "application/json",
"x-secret-key": DASHBOARD_THIRDWEB_SECRET_KEY,
} as Record<string, string>,
method: "GET",
});
if (!res.ok) {
@@ -49,16 +47,16 @@ export async function addUniversalBridgeTokenRoute(props: {
const url = new URL(`${UB_BASE_URL}/v1/tokens`);
const res = await fetch(url.toString(), {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${authToken}`,
"x-client-id": props.project.publishableKey,
} as Record<string, string>,
body: JSON.stringify({
chainId: props.chainId,
tokenAddress: props.tokenAddress,
}),
headers: {
Authorization: `Bearer ${authToken}`,
"Content-Type": "application/json",
"x-client-id": props.project.publishableKey,
} as Record<string, string>,
method: "POST",
});
if (!res.ok) {
@@ -1,7 +1,7 @@
import "server-only";
import { getAuthToken } from "@/api/auth-token";
import { NEXT_PUBLIC_THIRDWEB_API_HOST } from "@/constants/public-envs";
import { getAuthToken } from "../../../app/(app)/api/lib/getAuthToken";
type LineItem = {
quantity: number;
@@ -34,33 +34,33 @@ export async function getBilledUsage(teamSlug: string) {
NEXT_PUBLIC_THIRDWEB_API_HOST,
),
{
headers: {
Authorization: `Bearer ${authToken}`,
},
next: {
// revalidate this query once per minute (does not need to be more granular than that)
revalidate: 60,
},
headers: {
Authorization: `Bearer ${authToken}`,
},
},
);
if (!response.ok) {
// if the status is 404, the most likely reason is that the team is on a free plan
if (response.status === 404) {
return {
status: "error",
reason: "free_plan",
status: "error",
} as const;
}
const body = await response.text();
return {
status: "error",
reason: "unknown",
body,
reason: "unknown",
status: "error",
} as const;
}
const data = (await response.json()) as UsageApiResponse;
return {
status: "success",
data,
status: "success",
} as const;
}
+4 -8
View File
@@ -1,6 +1,6 @@
import "server-only";
import { unstable_cache } from "next/cache";
import { ANALYTICS_SERVICE_URL } from "../../constants/server-envs";
import { ANALYTICS_SERVICE_URL } from "@/constants/server-envs";
type Last24HoursRPCUsageApiResponse = {
peakRate: {
@@ -23,11 +23,7 @@ type Last24HoursRPCUsageApiResponse = {
};
export const getLast24HoursRPCUsage = unstable_cache(
async (params: {
teamId: string;
projectId?: string;
authToken: string;
}) => {
async (params: { teamId: string; projectId?: string; authToken: string }) => {
const analyticsEndpoint = ANALYTICS_SERVICE_URL;
const url = new URL(`${analyticsEndpoint}/v2/rpc/24-hours`);
url.searchParams.set("teamId", params.teamId);
@@ -44,16 +40,16 @@ export const getLast24HoursRPCUsage = unstable_cache(
if (!res.ok) {
const error = await res.text();
return {
ok: false as const,
error: error,
ok: false as const,
};
}
const resData = await res.json();
return {
ok: true as const,
data: resData.data as Last24HoursRPCUsageApiResponse,
ok: true as const,
};
},
["rpc-usage-last-24-hours:v2"],
+4 -4
View File
@@ -1,8 +1,8 @@
"use server";
import "server-only";
import { getAuthToken } from "../../app/(app)/api/lib/getAuthToken";
import { NEXT_PUBLIC_THIRDWEB_API_HOST } from "../constants/public-envs";
import { NEXT_PUBLIC_THIRDWEB_API_HOST } from "@/constants/public-envs";
import { getAuthToken } from "./auth-token";
export type VerifiedDomainResponse =
| {
@@ -56,12 +56,12 @@ export async function createDomainVerification(
const res = await fetch(
`${NEXT_PUBLIC_THIRDWEB_API_HOST}/v1/teams/${teamIdOrSlug}/verified-domain`,
{
method: "POST",
body: JSON.stringify({ domain }),
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
method: "POST",
},
);
@@ -1,3 +1,4 @@
import { differenceInCalendarDays, format, subDays } from "date-fns";
import { DatePickerWithRange } from "@/components/ui/DatePickerWithRange";
import {
Select,
@@ -7,7 +8,6 @@ import {
SelectValue,
} from "@/components/ui/select";
import { normalizeTime } from "@/lib/time";
import { differenceInCalendarDays, format, subDays } from "date-fns";
export function DateRangeSelector(props: {
range: Range;
@@ -26,30 +26,15 @@ export function DateRangeSelector(props: {
return (
<DatePickerWithRange
className="w-auto bg-card"
from={range.from}
to={range.to}
popoverAlign={props.popoverAlign}
setFrom={(from) =>
setRange({
from,
to: range.to,
type: "custom",
})
}
setTo={(to) =>
setRange({
from: range.from,
to,
type: "custom",
})
}
header={
<div className="mb-2 border-border border-b p-4">
<Select
value={rangeType}
onValueChange={(id: DurationId) => {
setRange(getLastNDaysRange(id));
}}
value={rangeType}
>
<SelectTrigger className="flex bg-transparent">
<SelectValue placeholder="Select" />
@@ -72,7 +57,22 @@ export function DateRangeSelector(props: {
</div>
}
labelOverride={rangeLabel}
className="w-auto bg-card"
popoverAlign={props.popoverAlign}
setFrom={(from) =>
setRange({
from,
to: range.to,
type: "custom",
})
}
setTo={(to) =>
setRange({
from: range.from,
to,
type: "custom",
})
}
to={range.to}
/>
);
}
@@ -86,10 +86,10 @@ export function getLastNDaysRange(id: DurationId) {
const todayDate = new Date(Date.now() + 1000 * 60 * 60 * 24); // add 1 day to avoid timezone issues
const value: Range = {
type: id,
from: subDays(todayDate, durationInfo.days),
to: todayDate,
label: durationInfo.name,
to: todayDate,
type: id,
};
return value;
@@ -97,24 +97,24 @@ export function getLastNDaysRange(id: DurationId) {
const durationPresets = [
{
name: "Last 7 Days",
id: "last-7",
days: 7,
id: "last-7",
name: "Last 7 Days",
},
{
name: "Last 30 Days",
id: "last-30",
days: 30,
id: "last-30",
name: "Last 30 Days",
},
{
name: "Last 60 Days",
id: "last-60",
days: 60,
id: "last-60",
name: "Last 60 Days",
},
{
name: "Last 120 Days",
id: "last-120",
days: 120,
id: "last-120",
name: "Last 120 Days",
},
] as const;
@@ -1,9 +1,9 @@
"use client";
import { LoadingDots } from "@/components/ui/LoadingDots";
import { type ChartConfig, ChartContainer } from "@/components/ui/chart";
import { cn } from "@/lib/utils";
import { useMemo } from "react";
import { useId, useMemo } from "react";
import { Area, AreaChart, Bar, BarChart } from "recharts";
import { type ChartConfig, ChartContainer } from "@/components/ui/chart";
import { LoadingDots } from "@/components/ui/LoadingDots";
import { cn } from "@/lib/utils";
type FakeCartData = {
value: number;
@@ -17,15 +17,18 @@ function generateRandomData(): FakeCartData[] {
const skeletonChartConfig = {
value: {
label: "Value",
color: "hsl(var(--muted)/90%)",
label: "Value",
},
} satisfies ChartConfig;
export function EmptyChartState({
children,
type,
}: { children?: React.ReactNode; type: "bar" | "area" }) {
}: {
children?: React.ReactNode;
type: "bar" | "area";
}) {
const barChartData = useMemo(() => generateRandomData(), []);
return (
@@ -55,10 +58,11 @@ function SkeletonBarChart(props: {
data: FakeCartData[];
type: "bar" | "area";
}) {
const fillAreaSkeletonId = useId();
return (
<ChartContainer
config={skeletonChartConfig}
className="pointer-events-none h-full w-full blur-[5px]"
config={skeletonChartConfig}
>
{props.type === "bar" ? (
<BarChart
@@ -77,7 +81,7 @@ function SkeletonBarChart(props: {
}}
>
<defs>
<linearGradient id="fill_area_skeleton" x1="0" y1="0" x2="0" y2="1">
<linearGradient id={fillAreaSkeletonId} x1="0" x2="0" y1="0" y2="1">
<stop
offset="5%"
stopColor={"hsl(var(--muted-foreground)/0.5)"}
@@ -87,11 +91,11 @@ function SkeletonBarChart(props: {
</linearGradient>
</defs>
<Area
type="natural"
dataKey="value"
stroke="hsl(var(--muted-foreground))"
fill="url(#fill_area_skeleton)"
fill={`url(#${fillAreaSkeletonId})`}
radius={8}
stroke="hsl(var(--muted-foreground))"
type="natural"
/>
</AreaChart>
)}
@@ -14,10 +14,10 @@ export function IntervalSelector(props: {
}) {
return (
<Select
value={props.intervalType}
onValueChange={(value: "day" | "week") => {
props.setIntervalType(value);
}}
value={props.intervalType}
>
<SelectTrigger
className={cn("w-auto bg-card md:w-[120px]", props.className)}
@@ -1,22 +1,25 @@
"use client";
import { useDashboardRouter } from "@/lib/DashboardRouter";
import { useQuery } from "@tanstack/react-query";
import {
DateRangeSelector,
getLastNDaysRange,
} from "components/analytics/date-range-selector";
import { usePathname, useSearchParams } from "next/navigation";
import { useState } from "react";
import type {
DurationId,
Range,
} from "components/analytics/date-range-selector";
import { IntervalSelector } from "components/analytics/interval-selector";
import { usePathname, useSearchParams } from "next/navigation";
import { useState } from "react";
} from "@/components/analytics/date-range-selector";
import {
DateRangeSelector,
getLastNDaysRange,
} from "@/components/analytics/date-range-selector";
import { IntervalSelector } from "@/components/analytics/interval-selector";
import { useDashboardRouter } from "@/lib/DashboardRouter";
export function RangeSelector({
range,
interval,
}: { range?: Range; interval: "day" | "week" }) {
}: {
range?: Range;
interval: "day" | "week";
}) {
const pathname = usePathname();
const searchParams = useSearchParams();
const router = useDashboardRouter();
@@ -26,7 +29,6 @@ export function RangeSelector({
const [localInterval, setInterval] = useState<"day" | "week">(interval);
useQuery({
queryKey: ["analytics-range", searchParams?.toString(), range],
queryFn: async () => {
if (range) {
setRange(range);
@@ -46,11 +48,11 @@ export function RangeSelector({
}
return getLastNDaysRange("last-120");
},
queryKey: ["analytics-range", searchParams?.toString(), range],
});
// prefetch for each interval and default range
useQuery({
queryKey: ["analytics-range", searchParams?.toString()],
queryFn: async () => {
const newSearchParams = new URLSearchParams(searchParams || {});
for (const interval of ["day", "week"] as const) {
@@ -70,6 +72,7 @@ export function RangeSelector({
}
return true;
},
queryKey: ["analytics-range", searchParams?.toString()],
});
return (
@@ -1,6 +1,5 @@
"use client";
import { Checkbox, CheckboxWithLabel } from "@/components/ui/checkbox";
import {
Alert,
AlertIcon,
@@ -12,8 +11,10 @@ import {
Textarea,
} from "@chakra-ui/react";
import { zodResolver } from "@hookform/resolvers/zod";
import { TransactionButton } from "components/buttons/TransactionButton";
import { FileInput } from "components/shared/FileInput";
import { Button } from "chakra/button";
import { FormErrorMessage, FormHelperText, FormLabel } from "chakra/form";
import { Heading } from "chakra/heading";
import { Text } from "chakra/text";
import { ChevronLeftIcon, EyeIcon, EyeOffIcon } from "lucide-react";
import { useRef, useState } from "react";
import { useDropzone } from "react-dropzone";
@@ -21,18 +22,13 @@ import { useForm } from "react-hook-form";
import type { ThirdwebClient } from "thirdweb";
import type { CreateDelayedRevealBatchParams } from "thirdweb/extensions/erc721";
import type { NFTInput } from "thirdweb/utils";
import {
Button,
FormErrorMessage,
FormHelperText,
FormLabel,
Heading,
Text,
TrackedLink,
} from "tw-components";
import type { ComponentWithChildren } from "types/component-with-children";
import { processInputData, shuffleData } from "utils/batch";
import { z } from "zod";
import { FileInput } from "@/components/blocks/FileInput";
import { TransactionButton } from "@/components/tx-button";
import { Checkbox, CheckboxWithLabel } from "@/components/ui/checkbox";
import { UnderlineLink } from "@/components/ui/UnderlineLink";
import type { ComponentWithChildren } from "@/types/component-with-children";
import { processInputData, shuffleData } from "@/utils/batch";
import { BatchTable } from "./batch-table";
import { SelectOption } from "./lazy-mint-form/select-option";
import { UploadStep } from "./upload-step";
@@ -60,27 +56,27 @@ type BatchLazyMintProps = BatchLazyMintEVMProps;
const BatchLazyMintFormSchema = z
.object({
// delayed reveal placeholder
placeHolder: z
.object({
name: z.string().min(1, "A name is required"),
image: z.any().optional(),
description: z.string().or(z.string().length(0)).optional(),
})
.optional(),
// delayed reveal password logic
password: z.string().min(1, "A password is required.").optional(),
confirmPassword: z
.string()
.min(1, "Please confirm your password.")
.optional(),
// shared logic
shuffle: z.boolean().default(false),
revealType: z.literal("instant").or(z.literal("delayed")).optional(),
// metadata
metadatas: z.array(z.any()),
// delayed reveal password logic
password: z.string().min(1, "A password is required.").optional(),
// delayed reveal placeholder
placeHolder: z
.object({
description: z.string().or(z.string().length(0)).optional(),
image: z.any().optional(),
name: z.string().min(1, "A name is required"),
})
.optional(),
revealType: z.literal("instant").or(z.literal("delayed")).optional(),
// shared logic
shuffle: z.boolean().default(false),
})
.refine((data) => data.password === data.confirmPassword, {
message: "Passwords don't match",
@@ -93,12 +89,12 @@ type BatchLazyMintFormType = z.output<typeof BatchLazyMintFormSchema> & {
function useBatchLazyMintForm() {
return useForm<BatchLazyMintFormType>({
resolver: zodResolver(BatchLazyMintFormSchema),
defaultValues: {
metadatas: [],
revealType: undefined,
shuffle: false,
},
resolver: zodResolver(BatchLazyMintFormSchema),
});
}
@@ -150,8 +146,8 @@ export const BatchLazyMint: ComponentWithChildren<
// check submit is instant
if (data.revealType === "instant") {
return props.onSubmit({
revealType: "instant",
data: { metadatas: shuffledMetadatas },
revealType: "instant",
});
}
// validate password
@@ -171,16 +167,16 @@ export const BatchLazyMint: ComponentWithChildren<
}
// submit
return props.onSubmit({
revealType: "delayed",
data: {
metadata: shuffledMetadatas,
password: data.password,
placeholderMetadata: {
name: data.placeHolder?.name,
description: data.placeHolder?.description,
image: data.placeHolder?.image,
name: data.placeHolder?.name,
},
},
revealType: "delayed",
});
})}
>
@@ -189,10 +185,10 @@ export const BatchLazyMint: ComponentWithChildren<
{nftMetadatas.length > 0 ? (
<>
<BatchTable
portalRef={paginationPortalRef}
client={props.client}
data={nftMetadatas}
nextTokenIdToMint={props.nextTokenIdToMint}
client={props.client}
portalRef={paginationPortalRef}
/>
<div className="border-border border-t">
<div className="flex flex-col items-center justify-between p-0 md:flex-row md:p-4">
@@ -222,8 +218,8 @@ export const BatchLazyMint: ComponentWithChildren<
</>
) : (
<UploadStep
getRootProps={getRootProps}
getInputProps={getInputProps}
getRootProps={getRootProps}
hasFailed={hasError}
isDragActive={isDragActive}
/>
@@ -231,31 +227,31 @@ export const BatchLazyMint: ComponentWithChildren<
</div>
) : (
<>
<Flex align="center" justify="space-between" py={4} w="100%" mb={2}>
<Flex align="center" justify="space-between" mb={2} py={4} w="100%">
<div className="flex flex-row items-center gap-2">
<Button
className="text-muted-foreground"
variant="ghost"
onClick={() => setStep(0)}
variant="ghost"
>
<ChevronLeftIcon className="size-5 cursor-pointer" />
</Button>
<Heading size="title.md" className="my-auto">
<Heading className="my-auto" size="title.md">
When will you reveal your NFTs?
</Heading>
</div>
</Flex>
<SelectReveal
form={form}
canCreateDelayedRevealBatch={props.canCreateDelayedRevealBatch}
client={props.client}
form={form}
/>
{form.watch("revealType") && (
<>
<CheckboxWithLabel>
<Checkbox
className="mt-3"
checked={form.watch("shuffle")}
className="mt-3"
onCheckedChange={(val) => form.setValue("shuffle", !!val)}
/>
<div className="flex flex-col items-center gap-1 md:flex-row">
@@ -265,14 +261,14 @@ export const BatchLazyMint: ComponentWithChildren<
</CheckboxWithLabel>
<div className="w-full md:w-[61%]">
<TransactionButton
client={props.client}
txChainID={props.chainId}
className="mt-4 w-full"
transactionCount={1}
client={props.client}
disabled={!nftMetadatas.length}
type="submit"
isPending={form.formState.isSubmitting}
isLoggedIn={props.isLoggedIn}
isPending={form.formState.isSubmitting}
transactionCount={1}
txChainID={props.chainId}
type="submit"
>
{form.formState.isSubmitting
? `Uploading ${nftMetadatas.length} NFTs`
@@ -280,15 +276,15 @@ export const BatchLazyMint: ComponentWithChildren<
</TransactionButton>
{props.children}
</div>
<Text size="body.sm" mt={2}>
<TrackedLink
<Text mt={2} size="body.sm">
<UnderlineLink
className="text-primary-500"
href="https://support.thirdweb.com/dashboard/n5evQ4EfEjEifczEQaZ1hL/batch-upload-troubleshooting/5WMQFqfaUTU1C8NM8FtJ2X"
isExternal
category="batch-upload"
label="issues"
rel="noopener noreferrer"
target="_blank"
>
Experiencing issues uploading your files?
</TrackedLink>
</UnderlineLink>
</Text>
</>
)}
@@ -316,30 +312,30 @@ const SelectReveal: React.FC<SelectRevealProps> = ({
return (
<Flex flexDir="column">
<Flex
flexDir={{ base: "column", md: "row" }}
gap={{ base: 3, md: 6 }}
mb={6}
flexDir={{ base: "column", md: "row" }}
>
<SelectOption
name="Reveal upon mint"
description="Collectors will immediately see the final NFT when they complete the minting"
isActive={form.watch("revealType") === "instant"}
name="Reveal upon mint"
onClick={() => form.setValue("revealType", "instant")}
/>
<SelectOption
name="Delayed Reveal"
description="Collectors will mint your placeholder image, then you reveal at a later time"
isActive={form.watch("revealType") === "delayed"}
onClick={() => form.setValue("revealType", "delayed")}
disabled={!canCreateDelayedRevealBatch}
disabledText="This contract doesn't implement Delayed Reveal"
isActive={form.watch("revealType") === "delayed"}
name="Delayed Reveal"
onClick={() => form.setValue("revealType", "delayed")}
/>
</Flex>
<div className="flex flex-col gap-3">
{form.watch("revealType") === "delayed" && (
<>
<Heading size="title.sm">Let&apos;s set a password</Heading>
<Alert status="warning" borderRadius="lg">
<Alert borderRadius="lg" status="warning">
<AlertIcon />
You&apos;ll need this password to reveal your NFTs. Please save it
somewhere safe.
@@ -350,10 +346,10 @@ const SelectReveal: React.FC<SelectRevealProps> = ({
gap={{ base: 4, md: 0 }}
>
<FormControl
isRequired
isInvalid={
!!form.getFieldState("password", form.formState).error
}
isRequired
mr={4}
>
<FormLabel>Password</FormLabel>
@@ -366,13 +362,13 @@ const SelectReveal: React.FC<SelectRevealProps> = ({
<InputRightElement cursor="pointer">
{show ? (
<EyeIcon
onClick={() => setShow(!show)}
className="size-3"
onClick={() => setShow(!show)}
/>
) : (
<EyeOffIcon
onClick={() => setShow(!show)}
className="size-3"
onClick={() => setShow(!show)}
/>
)}
</InputRightElement>
@@ -386,10 +382,10 @@ const SelectReveal: React.FC<SelectRevealProps> = ({
</FormErrorMessage>
</FormControl>
<FormControl
isRequired
isInvalid={
!!form.getFieldState("confirmPassword", form.formState).error
}
isRequired
>
<FormLabel>Confirm password</FormLabel>
<Input
@@ -416,14 +412,14 @@ const SelectReveal: React.FC<SelectRevealProps> = ({
<FormLabel>Image</FormLabel>
<div className="w-auto md:w-[350px]">
<FileInput
client={client}
accept={{ "image/*": [] }}
value={imageUrl}
showUploadButton
className="rounded border border-border transition-all duration-200"
client={client}
setValue={(file) =>
form.setValue("placeHolder.image", file)
}
className="rounded border border-border transition-all duration-200"
showUploadButton
value={imageUrl}
/>
</div>
<FormHelperText>
@@ -437,10 +433,10 @@ const SelectReveal: React.FC<SelectRevealProps> = ({
</FormErrorMessage>
</FormControl>
<FormControl
isRequired
isInvalid={
!!form.getFieldState("placeHolder.name", form.formState).error
}
isRequired
>
<FormLabel>Name</FormLabel>
<Input
@@ -1,5 +1,6 @@
import { CodeClient } from "@/components/ui/code/code.client";
import { ToolTipLabel } from "@/components/ui/tooltip";
/** biome-ignore-all lint/nursery/noNestedComponentDefinitions: FIXME */
import { FilePreview } from "@app/team/[team_slug]/[project_slug]/(sidebar)/tokens/create/_common/file-preview";
import {
Flex,
IconButton,
@@ -23,7 +24,8 @@ import { useMemo } from "react";
import { type Column, usePagination, useTable } from "react-table";
import type { ThirdwebClient } from "thirdweb";
import type { NFTInput } from "thirdweb/utils";
import { FilePreview } from "../../app/(app)/team/[team_slug]/[project_slug]/(sidebar)/assets/create/_common/file-preview";
import { CodeClient } from "@/components/ui/code/code.client";
import { ToolTipLabel } from "@/components/ui/tooltip";
interface BatchTableProps {
data: NFTInput[];
@@ -42,37 +44,36 @@ export const BatchTable: React.FC<BatchTableProps> = ({
let cols: Column<NFTInput>[] = [];
if (nextTokenIdToMint !== undefined) {
cols = cols.concat({
Header: "Token ID",
accessor: (_row, index) => String(nextTokenIdToMint + BigInt(index)),
Header: "Token ID",
});
}
cols = cols.concat([
{
Header: "Image",
accessor: (row) => row.image,
Cell: ({ cell: { value } }: { cell: { value?: string } }) => (
<FilePreview
className="size-24 shrink-0 rounded-lg object-contain"
srcOrFile={value}
client={client}
srcOrFile={value}
/>
),
Header: "Image",
},
{
Header: "Animation Url",
accessor: (row) => row.animation_url,
Cell: ({ cell: { value } }: { cell: { value?: string } }) => (
<FilePreview
className="size-24 shrink-0 rounded-lg"
srcOrFile={value}
client={client}
srcOrFile={value}
/>
),
Header: "Animation Url",
},
{ Header: "Name", accessor: (row) => row.name },
{ accessor: (row) => row.name, Header: "Name" },
{
Header: "Description",
accessor: (row) => (
<ToolTipLabel label={row.description}>
<p className="line-clamp-6 whitespace-pre-wrap">
@@ -80,9 +81,9 @@ export const BatchTable: React.FC<BatchTableProps> = ({
</p>
</ToolTipLabel>
),
Header: "Description",
},
{
Header: "Attributes",
accessor: (row) => row.attributes || row.properties,
// biome-ignore lint/suspicious/noExplicitAny: FIXME
Cell: ({ cell }: { cell: any }) =>
@@ -93,9 +94,10 @@ export const BatchTable: React.FC<BatchTableProps> = ({
scrollableClassName="max-w-[300px]"
/>
) : null,
Header: "Attributes",
},
{ Header: "External URL", accessor: (row) => row.external_url },
{ Header: "Background Color", accessor: (row) => row.background_color },
{ accessor: (row) => row.external_url, Header: "External URL" },
{ accessor: (row) => row.background_color, Header: "Background Color" },
]);
return cols;
}, [nextTokenIdToMint, client]);
@@ -124,8 +126,8 @@ export const BatchTable: React.FC<BatchTableProps> = ({
columns,
data,
initialState: {
pageSize: 50,
pageIndex: 0,
pageSize: 50,
},
},
// will be fixed with @tanstack/react-table v8
@@ -136,7 +138,7 @@ export const BatchTable: React.FC<BatchTableProps> = ({
// Render the UI for your table
return (
<Flex flexGrow={1} overflow="auto">
<TableContainer maxW="100%" className="w-full">
<TableContainer className="w-full" maxW="100%">
<Table {...getTableProps()}>
<Thead>
{headerGroups.map((headerGroup, index) => (
@@ -159,8 +161,8 @@ export const BatchTable: React.FC<BatchTableProps> = ({
return (
<Tr
{...row.getRowProps()}
borderBottomWidth={1}
_last={{ borderBottomWidth: 0 }}
borderBottomWidth={1}
// biome-ignore lint/suspicious/noArrayIndexKey: FIXME
key={rowIndex}
>
@@ -187,15 +189,15 @@ export const BatchTable: React.FC<BatchTableProps> = ({
<div className="flex w-full items-center justify-center">
<div className="flex flex-row items-center gap-2">
<IconButton
isDisabled={!canPreviousPage}
aria-label="first page"
icon={<ChevronFirstIcon className="size-4" />}
isDisabled={!canPreviousPage}
onClick={() => gotoPage(0)}
/>
<IconButton
isDisabled={!canPreviousPage}
aria-label="previous page"
icon={<ChevronLeftIcon className="size-4" />}
isDisabled={!canPreviousPage}
onClick={() => previousPage()}
/>
<p className="whitespace-nowrap">
@@ -203,15 +205,15 @@ export const BatchTable: React.FC<BatchTableProps> = ({
<strong>{pageOptions.length}</strong>
</p>
<IconButton
isDisabled={!canNextPage}
aria-label="next page"
icon={<ChevronRightIcon className="size-4" />}
isDisabled={!canNextPage}
onClick={() => nextPage()}
/>
<IconButton
isDisabled={!canNextPage}
aria-label="last page"
icon={<ChevronLastIcon className="size-4" />}
isDisabled={!canNextPage}
onClick={() => gotoPage(pageCount - 1)}
/>
@@ -1,8 +1,8 @@
import { InfoIcon } from "lucide-react";
import type { JSX, MouseEventHandler } from "react";
import { Card } from "@/components/ui/card";
import { ToolTipLabel } from "@/components/ui/tooltip";
import { cn } from "@/lib/utils";
import { InfoIcon } from "lucide-react";
import type { JSX, MouseEventHandler } from "react";
interface SelectOptionProps extends React.HTMLAttributes<HTMLDivElement> {
name: string;
@@ -1,7 +1,7 @@
import { UnorderedList } from "@/components/ui/List/List";
import { InlineCode } from "@/components/ui/inline-code";
import { cn } from "@/lib/utils";
import { UploadIcon } from "lucide-react";
import { InlineCode } from "@/components/ui/inline-code";
import { UnorderedList } from "@/components/ui/List/List";
import { cn } from "@/lib/utils";
interface UploadStepProps {
// biome-ignore lint/suspicious/noExplicitAny: FIXME
@@ -61,16 +61,16 @@ export const UploadStep: React.FC<UploadStepProps> = ({
<li>
Files <em>must</em> contain one .csv or .json file with metadata. -{" "}
<a
download
className="text-link-foreground hover:text-foreground"
download
href="/assets/examples/example.csv"
>
Download example.csv
</a>
.{" "}
<a
download
className="text-link-foreground hover:text-foreground"
download
href="/assets/examples/example.json"
>
Download example.json
@@ -111,8 +111,8 @@ export const UploadStep: React.FC<UploadStepProps> = ({
<InlineCode code="animation_url" /> column and add the IPFS hashes
there.{" "}
<a
download
className="text-link-foreground hover:text-foreground"
download
href="/assets/examples/example-with-ipfs.csv"
>
Download example.csv
@@ -123,8 +123,8 @@ export const UploadStep: React.FC<UploadStepProps> = ({
the name of your files to the <InlineCode code="image" /> and
<InlineCode code="animation_url" /> column.{" "}
<a
download
className="text-link-foreground hover:text-foreground"
download
href="/assets/examples/example-with-maps.csv"
>
Download example.csv
@@ -1,7 +1,11 @@
"use client";
import { CircleXIcon, ExternalLinkIcon } from "lucide-react";
import Link from "next/link";
import { useState, useTransition } from "react";
import { toast } from "sonner";
import type { Team } from "@/api/team";
import { Spinner } from "@/components/ui/Spinner/Spinner";
import { Button } from "@/components/ui/button";
import { Spinner } from "@/components/ui/Spinner/Spinner";
import {
Sheet,
SheetContent,
@@ -9,16 +13,12 @@ import {
SheetTitle,
SheetTrigger,
} from "@/components/ui/sheet";
import { PRO_CONTACT_US_URL } from "@/constants/pro";
import { useStripeRedirectEvent } from "@/hooks/stripe/redirect-event";
import { useDashboardRouter } from "@/lib/DashboardRouter";
import { CircleXIcon, ExternalLinkIcon } from "lucide-react";
import Link from "next/link";
import { useState, useTransition } from "react";
import { toast } from "sonner";
import { useStripeRedirectEvent } from "../../../../../app/(app)/(stripe)/stripe-redirect/stripeRedirectChannel";
import { buildCancelPlanUrl } from "../../../../../app/(app)/(stripe)/utils/build-url";
import { PRO_CONTACT_US_URL } from "../../../../../constants/pro";
import { pollWithTimeout } from "../../../../../utils/pollWithTimeout";
import { tryCatch } from "../../../../../utils/try-catch";
import { pollWithTimeout } from "@/utils/pollWithTimeout";
import { buildCancelPlanUrl } from "@/utils/stripe/build-url";
import { tryCatch } from "@/utils/try-catch";
export function CancelPlanButton(props: {
teamId: string;
@@ -32,9 +32,9 @@ export function CancelPlanButton(props: {
if (props.billingStatus !== "invalidPayment" && props.currentPlan !== "pro") {
return (
<ImmediateCancelPlanButton
teamId={props.teamId}
getTeam={props.getTeam}
disabled={props.disabled}
getTeam={props.getTeam}
teamId={props.teamId}
/>
);
}
@@ -43,10 +43,10 @@ export function CancelPlanButton(props: {
<Sheet>
<SheetTrigger asChild>
<Button
variant="outline"
size="sm"
className="gap-2 bg-background"
disabled={props.disabled}
size="sm"
variant="outline"
>
<CircleXIcon className="size-4 text-muted-foreground" />
Cancel Plan
@@ -80,9 +80,10 @@ function UnpaidInvoicesWarning({ teamSlug }: { teamSlug: string }) {
You have unpaid invoices. Please pay them before cancelling your plan.
</p>
<Button variant="outline" asChild className="w-full gap-2 bg-card">
<Button asChild className="w-full gap-2 bg-card" variant="outline">
<Link
href={`/team/${teamSlug}/~/settings/invoices?status=open`}
rel="noopener noreferrer"
target="_blank"
>
See Invoices
@@ -102,7 +103,7 @@ function ProPlanCancelPlanSheetContent() {
Please contact us to cancel your Pro plan
</p>
<Button variant="outline" asChild className="w-full gap-2 bg-card">
<Button asChild className="w-full gap-2 bg-card" variant="outline">
<Link href={PRO_CONTACT_US_URL} target="_blank">
Contact Us <ExternalLinkIcon className="size-4" />
</Link>
@@ -149,11 +150,11 @@ function ImmediateCancelPlanButton(props: {
return (
<Button
variant="outline"
size="sm"
asChild
className="gap-2 bg-background"
disabled={showPlanSpinner || props.disabled}
asChild
size="sm"
variant="outline"
>
<Link href={buildCancelPlanUrl({ teamId: props.teamId })} target="_blank">
{showPlanSpinner ? (
@@ -1,14 +1,13 @@
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { Button } from "@/components/ui/button";
import type { Account, BillingCredit } from "@3rdweb-sdk/react/hooks/useApi";
import { ChainIconClient } from "components/icons/ChainIcon";
import { formatDistance } from "date-fns";
import { useTrack } from "hooks/analytics/useTrack";
import { useLocalStorage } from "hooks/useLocalStorage";
import { CircleAlertIcon } from "lucide-react";
import Image from "next/image";
import Link from "next/link";
import type { ThirdwebClient } from "thirdweb";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { Button } from "@/components/ui/button";
import type { Account, BillingCredit } from "@/hooks/useApi";
import { useLocalStorage } from "@/hooks/useLocalStorage";
import { ChainIconClient } from "@/icons/ChainIcon";
import { formatToDollars } from "./formatToDollars";
interface CreditsItemProps {
@@ -28,8 +27,6 @@ export const CreditsItem: React.FC<CreditsItemProps> = ({
client,
teamSlug,
}) => {
const trackEvent = useTrack();
const [hasAppliedForOpGrant] = useLocalStorage(
`appliedForOpGrant-${twAccount.id}`,
false,
@@ -55,23 +52,23 @@ export const CreditsItem: React.FC<CreditsItemProps> = ({
<div className="absolute top-0 right-0">
{isOpCredit ? (
<ChainIconClient
src="ipfs://QmcxZHpyJa8T4i63xqjPYrZ6tKrt55tZJpbXcjSDKuKaf9/optimism/512.png"
className="size-6"
client={client}
src="ipfs://QmcxZHpyJa8T4i63xqjPYrZ6tKrt55tZJpbXcjSDKuKaf9/optimism/512.png"
/>
) : isTwCredit ? (
<Image
src={require("../../../../../public/brand/thirdweb-icon.svg")}
alt="tw-credit"
className="size-6"
objectFit="contain"
src={require("../../../../public/brand/thirdweb-icon.svg")}
/>
) : isStartupCredit ? (
<Image
src={require("../../../../../public/brand/thirdweb-icon.svg")}
alt="tw-credit"
className="size-6"
objectFit="contain"
src={require("../../../../public/brand/thirdweb-icon.svg")}
/>
) : null}
</div>
@@ -134,11 +131,6 @@ export const CreditsItem: React.FC<CreditsItemProps> = ({
<Link
href={`/team/${teamSlug}/~/settings/credits`}
onClick={() => {
trackEvent({
category: "op-sponsorship",
action: "click",
label: "apply-now",
});
if (onClickApply) {
onClickApply();
}
@@ -2,11 +2,8 @@
// TODO - convert to RSC
import {
type Account,
useAccountCredits,
} from "@3rdweb-sdk/react/hooks/useApi";
import type { ThirdwebClient } from "thirdweb";
import { type Account, useAccountCredits } from "@/hooks/useApi";
import { CreditsItem } from "./CreditsItem";
export const CreditsInfoCard = (props: {
@@ -26,19 +23,19 @@ export const CreditsInfoCard = (props: {
return (
<section className="flex flex-col gap-4">
<CreditsItem
client={props.client}
credit={opCredit}
isOpCreditDefault={true}
twAccount={props.twAccount}
client={props.client}
teamSlug={props.teamSlug}
twAccount={props.twAccount}
/>
{restCredits?.map((credit) => (
<CreditsItem
key={credit.couponId}
credit={credit}
twAccount={props.twAccount}
client={props.client}
credit={credit}
key={credit.couponId}
teamSlug={props.teamSlug}
twAccount={props.twAccount}
/>
))}
</section>
@@ -1,24 +1,23 @@
"use client";
import { useTransition } from "react";
import type { Team } from "@/api/team";
import { PricingCard } from "@/components/blocks/pricing-card";
import { Spinner } from "@/components/ui/Spinner/Spinner";
import { PRO_CONTACT_US_URL } from "@/constants/pro";
import { useStripeRedirectEvent } from "@/hooks/stripe/redirect-event";
import { useDashboardRouter } from "@/lib/DashboardRouter";
import { useTransition } from "react";
import { useStripeRedirectEvent } from "../../../../app/(app)/(stripe)/stripe-redirect/stripeRedirectChannel";
import { getValidTeamPlan } from "../../../../app/(app)/team/components/TeamHeader/getValidTeamPlan";
import { PRO_CONTACT_US_URL } from "../../../../constants/pro";
import { getValidTeamPlan } from "@/utils/getValidTeamPlan";
// this is used to determine whether to show "Upgrade" or "Downgrade" label based on tier level
const planToTierRecord: Record<Team["billingPlan"], number> = {
free: 0,
starter: 2,
growth_legacy: 3,
growth: 4,
accelerate: 5,
scale: 6,
free: 0,
growth: 4,
growth_legacy: 3,
pro: 7,
scale: 6,
starter: 2,
};
interface BillingPricingProps {
@@ -100,63 +99,63 @@ export const BillingPricing: React.FC<BillingPricingProps> = ({
<PricingCard
billingPlan="starter"
billingStatus={team.billingStatus}
current={validTeamPlan === "starter"}
cta={getPlanCta(
validTeamPlan,
"starter",
isCurrentPlanScheduledToCancel,
)}
teamSlug={team.slug}
teamId={team.id}
highlighted={highlightStarterPlan}
current={validTeamPlan === "starter"}
getTeam={getTeam}
highlighted={highlightStarterPlan}
teamId={team.id}
teamSlug={team.slug}
/>
{/* Growth */}
<PricingCard
billingPlan="growth"
billingStatus={team.billingStatus}
activeTrialEndsAt={
validTeamPlan === "growth" ? trialPeriodEndedAt : undefined
}
current={validTeamPlan === "growth"}
billingPlan="growth"
billingStatus={team.billingStatus}
cta={getPlanCta(
validTeamPlan,
"growth",
isCurrentPlanScheduledToCancel,
)}
highlighted={highlightGrowthPlan}
teamSlug={team.slug}
teamId={team.id}
current={validTeamPlan === "growth"}
getTeam={getTeam}
highlighted={highlightGrowthPlan}
teamId={team.id}
teamSlug={team.slug}
/>
{/* Scale */}
<PricingCard
billingPlan="scale"
billingStatus={team.billingStatus}
teamSlug={team.slug}
teamId={team.id}
current={validTeamPlan === "scale"}
cta={getPlanCta(
validTeamPlan,
"scale",
isCurrentPlanScheduledToCancel,
)}
highlighted={highlightScalePlan}
current={validTeamPlan === "scale"}
getTeam={getTeam}
highlighted={highlightScalePlan}
teamId={team.id}
teamSlug={team.slug}
/>
{/* Pro */}
<PricingCard
billingPlan="pro"
billingStatus={team.billingStatus}
teamSlug={team.slug}
teamId={team.id}
current={validTeamPlan === "pro"}
cta={getPlanCta(validTeamPlan, "pro", isCurrentPlanScheduledToCancel)}
highlighted={highlightProPlan}
current={validTeamPlan === "pro"}
getTeam={getTeam}
highlighted={highlightProPlan}
teamId={team.id}
teamSlug={team.slug}
/>
</div>
</div>
@@ -171,8 +170,8 @@ function getPlanCta(
// if the CURRENT plan is pro, show contact us link
if (currentPlan === "pro") {
return {
label: "Contact us",
href: PRO_CONTACT_US_URL,
label: "Contact us",
type: "link",
};
}
@@ -1,13 +1,13 @@
import type { Meta, StoryObj } from "@storybook/nextjs";
import { useId, useMemo, useState } from "react";
import { Label } from "@/components/ui/label";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import { Switch } from "@/components/ui/switch";
import type { Meta, StoryObj } from "@storybook/nextjs";
import { useMemo, useState } from "react";
import { type CouponData, CouponsUI } from "./CouponsUI";
const meta = {
title: "Billing/Coupons",
component: Story,
title: "Billing/Coupons",
} satisfies Meta<typeof CouponsUI>;
export default meta;
@@ -16,54 +16,54 @@ type Story = StoryObj<typeof CouponsUI>;
const couponStubs: CouponData[] = [
// Forever
{
id: "di_1QAE7jCQUO4TBFqFQgnS4Qj4",
start: 1729011691,
end: null,
coupon: {
id: "forever_active",
name: "Example 1",
duration: "forever",
duration_in_months: null,
id: "forever_active",
name: "Example 1",
},
end: null,
id: "di_1QAE7jCQUO4TBFqFQgnS4Qj4",
start: 1729011691,
},
// Once
{
id: "once",
start: 1733391052,
end: null,
coupon: {
id: "once_active",
name: "Example 2",
duration: "once",
duration_in_months: null,
id: "once_active",
name: "Example 2",
},
end: null,
id: "once",
start: 1733391052,
},
// Repeating - 3 months
{
id: "repeating-3",
start: 1733391629,
end: 1743391629, // Will end in the future
coupon: {
id: "repeating_active",
name: "Example 3",
duration: "repeating",
duration_in_months: 3,
id: "repeating_active",
name: "Example 3",
},
end: 1743391629,
id: "repeating-3", // Will end in the future
start: 1733391629,
},
// Repeating - 1 month
{
id: "repeating-1",
start: 1733391713,
end: 1736391713,
coupon: {
id: "repeating_single",
name: "Example 4",
duration: "repeating",
duration_in_months: 1,
id: "repeating_single",
name: "Example 4",
},
end: 1736391713,
id: "repeating-1",
start: 1733391713,
},
];
@@ -102,6 +102,9 @@ function Story() {
200 | 400 | 401 | 409 | 429 | 500
>(200);
const hasAccountCouponId = useId();
const paymentSetupId = useId();
return (
<div className="container max-w-[1100px] py-10">
<div className="mb-8 space-y-6">
@@ -109,22 +112,22 @@ function Story() {
<div className="space-y-3">
<h3 className="font-semibold text-lg">Status</h3>
<RadioGroup
value={status}
className="flex gap-4"
onValueChange={(value) =>
setStatus(value as "pending" | "error" | "success")
}
className="flex gap-4"
value={status}
>
<div className="flex items-center space-x-2">
<RadioGroupItem value="pending" id="status-pending" />
<RadioGroupItem value="pending" />
<Label htmlFor="status-pending">Pending</Label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem value="error" id="status-error" />
<RadioGroupItem value="error" />
<Label htmlFor="status-error">Error</Label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem value="success" id="status-success" />
<RadioGroupItem value="success" />
<Label htmlFor="status-success">Success</Label>
</div>
</RadioGroup>
@@ -133,22 +136,22 @@ function Story() {
<div className="space-y-3">
<h3 className="font-semibold text-lg">Active Coupons</h3>
<RadioGroup
value={activeCouponsType}
className="flex gap-4"
onValueChange={(value) =>
setActiveCouponsType(value as "multiple" | "zero" | "one")
}
className="flex gap-4"
value={activeCouponsType}
>
<div className="flex items-center space-x-2">
<RadioGroupItem value="multiple" id="coupons-multiple" />
<RadioGroupItem value="multiple" />
<Label htmlFor="coupons-multiple">Multiple</Label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem value="zero" id="coupons-zero" />
<RadioGroupItem value="zero" />
<Label htmlFor="coupons-zero">Zero</Label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem value="one" id="coupons-one" />
<RadioGroupItem value="one" />
<Label htmlFor="coupons-one">One</Label>
</div>
</RadioGroup>
@@ -157,19 +160,19 @@ function Story() {
<div className="space-y-3">
<h3 className="font-semibold text-lg">Apply Coupon Status</h3>
<RadioGroup
value={applyCouponStatus.toString()}
className="flex gap-4"
onValueChange={(value) =>
setApplyCouponStatus(
Number.parseInt(value) as 200 | 400 | 401 | 409 | 429 | 500,
)
}
value={applyCouponStatus.toString()}
>
{[200, 400, 401, 409, 429, 500].map((code) => (
<div key={code} className="flex items-center space-x-2">
<div className="flex items-center space-x-2" key={code}>
<RadioGroupItem
value={code.toString()}
id={`status-${code}`}
value={code.toString()}
/>
<Label htmlFor={`status-${code}`}>{code}</Label>
</div>
@@ -183,52 +186,52 @@ function Story() {
<div className="space-y-4">
<div className="flex items-center gap-4">
<Switch
id="has-account-coupon"
checked={hasAccountCoupon}
id={hasAccountCouponId}
onCheckedChange={setHasAccountCoupon}
/>
<Label htmlFor="has-account-coupon">Has Account Coupon</Label>
<Label htmlFor={hasAccountCouponId}>Has Account Coupon</Label>
</div>
<div className="flex items-center gap-4">
<Switch
id="payment-setup"
checked={isPaymentSetup}
id={paymentSetupId}
onCheckedChange={setIsPaymentSetup}
/>
<Label htmlFor="payment-setup">Payment Setup</Label>
<Label htmlFor={paymentSetupId}>Payment Setup</Label>
</div>
</div>
</div>
</div>
<CouponsUI
status={status}
activeCoupons={activeCoupons}
accountCouponId={hasAccountCoupon ? activeCoupons[0]?.id : undefined}
isPaymentSetup={isPaymentSetup}
activeCoupons={activeCoupons}
applyCoupon={async () => {
await new Promise((resolve) => setTimeout(resolve, 1000));
return {
status: applyCouponStatus,
data:
applyCouponStatus === 200
? {
id: "xyz",
start: 1727992716,
end: 1759528716,
coupon: {
id: "test",
name: "TEST COUPON",
duration: "repeating",
duration_in_months: 12,
id: "test",
name: "TEST COUPON",
},
end: 1759528716,
id: "xyz",
start: 1727992716,
}
: null,
status: applyCouponStatus,
};
}}
deleteCoupon={async () => {
await new Promise((resolve) => setTimeout(resolve, 1000));
}}
isPaymentSetup={isPaymentSetup}
status={status}
/>
</div>
);
@@ -1,6 +1,6 @@
import { NEXT_PUBLIC_THIRDWEB_API_HOST } from "@/constants/public-envs";
import { Suspense } from "react";
import { getAuthToken } from "../../../../../app/(app)/api/lib/getAuthToken";
import { getAuthToken } from "@/api/auth-token";
import { NEXT_PUBLIC_THIRDWEB_API_HOST } from "@/constants/public-envs";
import { CouponsClient } from "./CouponsClient";
import type { CouponData } from "./CouponsUI";
@@ -39,10 +39,10 @@ async function AsyncCoupons(props: {
) {
return (
<CouponsClient
status="error"
activeCoupons={[]}
accountCouponId={undefined}
activeCoupons={[]}
isPaymentSetup={props.isPaymentSetup}
status="error"
teamId={props.teamId}
/>
);
@@ -58,34 +58,31 @@ async function AsyncCoupons(props: {
return (
<CouponsClient
status="success"
activeCoupons={allCouponsData.data}
accountCouponId={accountCouponData.data?.id}
activeCoupons={allCouponsData.data}
isPaymentSetup={props.isPaymentSetup}
status="success"
teamId={props.teamId}
/>
);
}
export function Coupons(props: {
teamId: string;
isPaymentSetup: boolean;
}) {
export function Coupons(props: { teamId: string; isPaymentSetup: boolean }) {
return (
<Suspense
fallback={
<CouponsClient
status="pending"
activeCoupons={[]}
accountCouponId={undefined}
activeCoupons={[]}
isPaymentSetup={true}
status="pending"
teamId={props.teamId}
/>
}
>
<AsyncCoupons
teamId={props.teamId}
isPaymentSetup={props.isPaymentSetup}
teamId={props.teamId}
/>
</Suspense>
);
@@ -16,6 +16,35 @@ export function CouponsClient(
return (
<CouponsUI
{...props}
applyCoupon={async (promoCode: string) => {
const res = await apiServerProxy<{ data: CouponData }>({
body: JSON.stringify({
promoCode,
teamId: props.teamId,
}),
headers: {
"Content-Type": "application/json",
},
method: "POST",
pathname: "/v1/coupons/redeem",
});
if (!res.ok) {
return {
data: null,
status: res.status,
};
}
const json = res.data;
router.refresh();
return {
data: json.data,
status: 200,
};
}}
deleteCoupon={async () => {
const res = await apiServerProxy({
method: "DELETE",
@@ -29,35 +58,6 @@ export function CouponsClient(
router.refresh();
}}
applyCoupon={async (promoCode: string) => {
const res = await apiServerProxy<{ data: CouponData }>({
method: "POST",
pathname: "/v1/coupons/redeem",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
promoCode,
teamId: props.teamId,
}),
});
if (!res.ok) {
return {
status: res.status,
data: null,
};
}
const json = res.data;
router.refresh();
return {
status: 200,
data: json.data,
};
}}
/>
);
}
@@ -1,17 +1,5 @@
"use client";
import { Spinner } from "@/components/ui/Spinner/Spinner";
import { Button } from "@/components/ui/button";
import {
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { ToolTipLabel } from "@/components/ui/tooltip";
import { useMutation } from "@tanstack/react-query";
import { format, fromUnixTime } from "date-fns";
import {
@@ -22,6 +10,18 @@ import {
Trash2Icon,
} from "lucide-react";
import { toast } from "sonner";
import { Button } from "@/components/ui/button";
import { Spinner } from "@/components/ui/Spinner/Spinner";
import {
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { ToolTipLabel } from "@/components/ui/tooltip";
type Coupon = {
id: string;
@@ -211,17 +211,17 @@ function DeleteCouponButton(props: {
return (
<Button
variant="outline"
size="sm"
aria-label="Remove coupon"
disabled={props.disabled}
onClick={() => {
const promise = deleteCoupon.mutateAsync();
toast.promise(promise, {
success: "Coupon removed successfully",
error: "Failed to remove coupon",
success: "Coupon removed successfully",
});
}}
aria-label="Remove coupon"
size="sm"
variant="outline"
>
{deleteCoupon.isPending ? (
<Spinner className="size-4" />
@@ -1,14 +1,14 @@
"use client";
import { AlertTriangleIcon } from "lucide-react";
import Link from "next/link";
import type { Team } from "@/api/team";
import { Button, type ButtonProps } from "@/components/ui/button";
import { cn } from "@/lib/utils";
import type { ProductSKU } from "@/types/billing";
import {
buildBillingPortalUrl,
buildCheckoutUrl,
} from "../../app/(app)/(stripe)/utils/build-url";
import type { Team } from "../api/team";
import type { ProductSKU } from "../lib/billing";
import { cn } from "../lib/utils";
import { Button, type ButtonProps } from "./ui/button";
} from "@/utils/stripe/build-url";
export function CheckoutButton(props: {
buttonProps?: Omit<ButtonProps, "children">;
@@ -38,11 +38,12 @@ export function CheckoutButton(props: {
}}
>
<Link
target="_blank"
href={buildCheckoutUrl({
teamSlug: props.teamSlug,
sku: props.sku,
teamSlug: props.teamSlug,
})}
rel="noopener noreferrer"
target="_blank"
>
{props.children}
</Link>
@@ -58,8 +59,8 @@ function BillingWarning({ teamSlug }: { teamSlug: string }) {
<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"
href={`/team/${teamSlug}/~/settings/invoices?status=open`}
>
pay them
</Link>{" "}
@@ -77,17 +78,17 @@ export function BillingPortalButton(props: {
return (
<Button
{...props.buttonProps}
asChild
className={cn(props.buttonProps?.className, "gap-2")}
disabled={props.buttonProps?.disabled}
asChild
onClick={async (e) => {
props.buttonProps?.onClick?.(e);
}}
>
<a
href={buildBillingPortalUrl({ teamSlug: props.teamSlug })}
rel="noopener noreferrer"
target="_blank"
rel="noreferrer"
>
{props.children}
</a>
@@ -1,7 +1,7 @@
export const formatToDollars = (cents: number) => {
const dollars = cents / 100;
return new Intl.NumberFormat(undefined, {
style: "currency",
currency: "USD",
style: "currency",
}).format(dollars);
};
@@ -1,16 +1,16 @@
"use client";
import { reSubscribePlan } from "@/actions/billing";
import type { Team } from "@/api/team";
import { Spinner } from "@/components/ui/Spinner/Spinner";
import { Button } from "@/components/ui/button";
import { useDashboardRouter } from "@/lib/DashboardRouter";
import { useMutation } from "@tanstack/react-query";
import { RefreshCwIcon } from "lucide-react";
import { useState, useTransition } from "react";
import { toast } from "sonner";
import { pollWithTimeout } from "../../../../../utils/pollWithTimeout";
import { tryCatch } from "../../../../../utils/try-catch";
import { reSubscribePlan } from "@/actions/billing";
import type { Team } from "@/api/team";
import { Button } from "@/components/ui/button";
import { Spinner } from "@/components/ui/Spinner/Spinner";
import { useDashboardRouter } from "@/lib/DashboardRouter";
import { pollWithTimeout } from "../../../utils/pollWithTimeout";
import { tryCatch } from "../../../utils/try-catch";
export function RenewSubscriptionButton(props: {
teamId: string;
@@ -68,11 +68,11 @@ export function RenewSubscriptionButton(props: {
return (
<Button
onClick={handleRenewSubscription}
variant="default"
size="sm"
className="gap-2"
disabled={showSpinner || props.disabled}
onClick={handleRenewSubscription}
size="sm"
variant="default"
>
{showSpinner ? (
<Spinner className="size-4" />
@@ -1,8 +1,9 @@
/* eslint-disable no-restricted-syntax */
import { Input } from "@/components/ui/input";
import { cn } from "@/lib/utils";
import { PercentIcon } from "lucide-react";
import { useEffect, useState } from "react";
import { Input } from "@/components/ui/input";
import { cn } from "@/lib/utils";
type InputProps = React.ComponentProps<typeof Input>;
@@ -51,7 +52,7 @@ export const BasisPointsInput: React.FC<BasisPointsInputProps> = ({
"rounded-r-none border-none focus-visible:ring-0 focus-visible:ring-offset-0",
restInputProps.className,
)}
onChange={(e) => setStringValue(e.target.value)}
maxLength={5}
onBlur={(e) => {
const val = e.target.value;
const validValue = val.match(
@@ -63,7 +64,7 @@ export const BasisPointsInput: React.FC<BasisPointsInputProps> = ({
setStringValue("0.00");
}
}}
maxLength={5}
onChange={(e) => setStringValue(e.target.value)}
/>
<div className="flex items-center justify-center border-border border-l bg-card px-3 font-medium">
<PercentIcon className="size-4" />
@@ -1,3 +1,5 @@
import { useMemo, useState } from "react";
import { isAddress, NATIVE_TOKEN_ADDRESS, ZERO_ADDRESS } from "thirdweb";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import {
@@ -7,11 +9,9 @@ import {
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { CURRENCIES, type CurrencyMetadata } from "@/constants/currencies";
import { useAllChainsData } from "@/hooks/chains/allChains";
import { cn } from "@/lib/utils";
import { CURRENCIES, type CurrencyMetadata } from "constants/currencies";
import { useAllChainsData } from "hooks/chains/allChains";
import { useMemo, useState } from "react";
import { NATIVE_TOKEN_ADDRESS, ZERO_ADDRESS, isAddress } from "thirdweb";
interface CurrencySelectorProps {
value: string;
@@ -113,15 +113,15 @@ export function CurrencySelector({
</Button>
<Input
className="w-full rounded-none"
required
placeholder="ERC20 Address"
value={editCustomCurrency}
onChange={(e) => setEditCustomCurrency(e.target.value)}
placeholder="ERC20 Address"
required
value={editCustomCurrency}
/>
<Button
className="rounded-r-md rounded-l-none"
onClick={addCustomCurrency}
disabled={!isAddress(editCustomCurrency)}
onClick={addCustomCurrency}
>
Save
</Button>
@@ -139,13 +139,6 @@ export function CurrencySelector({
>
<Select
disabled={isDisabled}
value={
isPaymentsSelector
? value
: value?.toLowerCase() === ZERO_ADDRESS.toLowerCase()
? NATIVE_TOKEN_ADDRESS.toLowerCase()
: value?.toLowerCase()
}
onValueChange={(val) => {
if (val === "custom") {
setIsAddingCurrency(true);
@@ -155,6 +148,13 @@ export function CurrencySelector({
} as React.ChangeEvent<HTMLSelectElement>);
}
}}
value={
isPaymentsSelector
? value
: value?.toLowerCase() === ZERO_ADDRESS.toLowerCase()
? NATIVE_TOKEN_ADDRESS.toLowerCase()
: value?.toLowerCase()
}
>
<SelectTrigger>
<SelectValue placeholder="Select Currency" />
@@ -1,15 +1,15 @@
import type { Meta, StoryObj } from "@storybook/nextjs";
import { BadgeContainer } from "../../../stories/utils";
import { BadgeContainer } from "@/storybook/utils";
import { DangerSettingCard } from "./DangerSettingCard";
const meta = {
title: "blocks/Cards/DangerSettingCard",
component: Story,
parameters: {
nextjs: {
appDirectory: true,
},
},
title: "blocks/Cards/DangerSettingCard",
} satisfies Meta<typeof Story>;
export default meta;
@@ -24,29 +24,29 @@ function Story() {
<div className="container flex max-w-[1000px] flex-col gap-8 py-10">
<BadgeContainer label="Base">
<DangerSettingCard
title="This is a title"
description="This is a description"
buttonLabel="Some Action"
buttonOnClick={() => {}}
isPending={false}
confirmationDialog={{
title: "This is confirmation title",
description: "This is confirmation description",
title: "This is confirmation title",
}}
description="This is a description"
isPending={false}
title="This is a title"
/>
</BadgeContainer>
<BadgeContainer label="Loading">
<DangerSettingCard
title="This is a title"
description="This is a description"
buttonLabel="Some Action"
buttonOnClick={() => {}}
isPending={true}
confirmationDialog={{
title: "This is confirmation title",
description: "This is confirmation description",
title: "This is confirmation title",
}}
description="This is a description"
isPending={true}
title="This is a title"
/>
</BadgeContainer>
</div>
@@ -1,4 +1,4 @@
import { Spinner } from "@/components/ui/Spinner/Spinner";
import { useState } from "react";
import { Button } from "@/components/ui/button";
import {
@@ -10,7 +10,7 @@ import {
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { useState } from "react";
import { Spinner } from "@/components/ui/Spinner/Spinner";
import { cn } from "../../lib/utils";
import { DynamicHeight } from "../ui/DynamicHeight";
@@ -58,19 +58,19 @@ export function DangerSettingCard(props: {
)}
>
<Dialog
open={isConfirmationDialogOpen}
onOpenChange={(v) => {
setIsConfirmationDialogOpen(v);
if (!v) {
props.confirmationDialog.onClose?.();
}
}}
open={isConfirmationDialogOpen}
>
<DialogTrigger asChild>
<Button
variant="destructive"
className="gap-2 bg-red-600 font-semibold text-white hover:bg-red-600/80"
disabled={props.isDisabled || props.isPending}
variant="destructive"
>
{props.isPending && <Spinner className="size-3" />}
{props.buttonLabel}
@@ -99,10 +99,10 @@ export function DangerSettingCard(props: {
</DialogClose>
<Button
variant="destructive"
className="gap-2 bg-red-600 font-semibold text-white hover:bg-red-600/80"
onClick={props.buttonOnClick}
disabled={props.isPending}
onClick={props.buttonOnClick}
variant="destructive"
>
{props.isPending && <Spinner className="size-3" />}
{props.buttonLabel}
@@ -7,9 +7,10 @@ export function DocLink(props: {
}) {
return (
<Link
href={props.link}
target="_blank"
className="flex items-center gap-2 whitespace-nowrap text-muted-foreground text-sm hover:text-foreground"
href={props.link}
rel="noopener noreferrer"
target="_blank"
>
<props.icon className="size-4" />
{props.label}
@@ -1,9 +1,9 @@
import { Spinner } from "@/components/ui/Spinner/Spinner";
import { Button } from "@/components/ui/button";
import { useMutation } from "@tanstack/react-query";
import { DownloadIcon } from "lucide-react";
import Papa from "papaparse";
import { toast } from "sonner";
import { Button } from "@/components/ui/button";
import { Spinner } from "@/components/ui/Spinner/Spinner";
import { cn } from "../../lib/utils";
export function ExportToCSVButton(props: {
@@ -34,12 +34,12 @@ export function ExportToCSVButton(props: {
return (
<Button
variant="outline"
disabled={props.disabled || exportMutation.isPending}
className={cn("flex items-center gap-2 border text-xs", props.className)}
disabled={props.disabled || exportMutation.isPending}
onClick={async () => {
exportMutation.mutate();
}}
variant="outline"
>
{exportMutation.isPending ? (
<>
@@ -58,8 +58,8 @@ export function ExportToCSVButton(props: {
function convertToCSVFormat(data: { header: string[]; rows: string[][] }) {
return Papa.unparse({
fields: data.header,
data: data.rows,
fields: data.header,
});
}
@@ -1,6 +1,4 @@
import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";
import { FilePreview } from "@app/team/[team_slug]/[project_slug]/(sidebar)/assets/create/_common/file-preview";
import { FilePreview } from "@app/team/[team_slug]/[project_slug]/(sidebar)/tokens/create/_common/file-preview";
import { FilePlusIcon, UploadIcon } from "lucide-react";
import { useCallback, useEffect, useRef } from "react";
import {
@@ -10,6 +8,8 @@ import {
useDropzone,
} from "react-dropzone";
import type { ThirdwebClient } from "thirdweb";
import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";
interface IFileInputProps {
accept?: Accept;
@@ -60,8 +60,8 @@ export const FileInput: React.FC<IFileInputProps> = ({
[setValue],
);
const { getRootProps, getInputProps } = useDropzone({
onDrop,
accept,
onDrop,
});
const helperTextOrFile = helperText
@@ -98,15 +98,15 @@ export const FileInput: React.FC<IFileInputProps> = ({
{showPreview &&
(isDisabled ? (
<div
style={{
aspectRatio: 1,
maxWidth: previewMaxWidth,
}}
className={cn(
"cursor-not-allowed bg-card opacity-50",
"flex w-full items-center justify-center border border-border hover:bg-accent hover:ring-2 hover:ring-ring",
className,
)}
style={{
aspectRatio: 1,
maxWidth: previewMaxWidth,
}}
>
<div className="flex flex-col items-center gap-2 text-muted-foreground">
<UploadIcon className="size-6" />
@@ -115,23 +115,23 @@ export const FileInput: React.FC<IFileInputProps> = ({
</div>
) : (
<div
style={{
aspectRatio: 1,
maxWidth: previewMaxWidth,
}}
className={cn(
"relative cursor-pointer overflow-hidden",
"flex w-full items-center justify-center border border-border hover:bg-accent hover:ring-2 hover:ring-ring",
value ? "bg-transparent" : "bg-card",
className,
)}
style={{
aspectRatio: 1,
maxWidth: previewMaxWidth,
}}
>
{value ? (
<FilePreview
srcOrFile={value}
client={client}
className="h-full w-full"
client={client}
imgContainerClassName="h-full w-full"
srcOrFile={value}
/>
) : (
<div className="flex flex-col items-center gap-2.5 text-muted-foreground">
@@ -151,7 +151,7 @@ export const FileInput: React.FC<IFileInputProps> = ({
{showUploadButton ? (
<div className="flex flex-col gap-2">
{showUploadButton && (
<Button variant="outline" disabled={isDisabled} className="gap-2">
<Button className="gap-2" disabled={isDisabled} variant="outline">
<FilePlusIcon className="size-4" /> {selectOrUpload}{" "}
{helperTextOrFile}
</Button>
@@ -1,7 +1,7 @@
import { Label } from "@/components/ui/label";
import { ToolTipLabel } from "@/components/ui/tooltip";
import { AsteriskIcon, InfoIcon } from "lucide-react";
import type React from "react";
import { Label } from "@/components/ui/label";
import { ToolTipLabel } from "@/components/ui/tooltip";
export function FormFieldSetup(props: {
htmlFor?: string;
@@ -1,3 +1,5 @@
import type { Meta, StoryObj } from "@storybook/nextjs";
import { useState } from "react";
import type { Team } from "@/api/team";
import { Label } from "@/components/ui/label";
import {
@@ -7,14 +9,12 @@ import {
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import type { Meta, StoryObj } from "@storybook/nextjs";
import { useState } from "react";
import { BadgeContainer } from "../../../../stories/utils";
import { BadgeContainer } from "@/storybook/utils";
import { GatedSwitch } from "./GatedSwitch";
const meta = {
title: "Billing/GatedSwitch",
component: Variants,
title: "Billing/GatedSwitch",
} satisfies Meta<typeof Variants>;
export default meta;
@@ -46,10 +46,10 @@ function Variants() {
<div className="flex items-center gap-2">
<Label>Required Plan</Label>
<Select
value={requiredPlan}
onValueChange={(value) =>
setRequiredPlan(value as typeof requiredPlan)
}
value={requiredPlan}
>
<SelectTrigger className="w-[180px]">
<SelectValue placeholder="Select plan" />
@@ -66,8 +66,8 @@ function Variants() {
{plans.map((currentPlan) => (
<BadgeContainer key={currentPlan} label={`plan: ${currentPlan}`}>
<GatedSwitch
key={currentPlan}
currentPlan={currentPlan}
key={currentPlan}
requiredPlan={requiredPlan}
teamSlug="foo"
/>
@@ -1,15 +1,12 @@
import { ExternalLinkIcon } from "lucide-react";
import Link from "next/link";
import type { Team } from "@/api/team";
import { Button } from "@/components/ui/button";
import { Switch } from "@/components/ui/switch";
import { ToolTipLabel } from "@/components/ui/tooltip";
import { TrackedLinkTW } from "@/components/ui/tracked-link";
import { planToTierRecordForGating } from "@/constants/planToTierRecord";
import { cn } from "@/lib/utils";
import { ExternalLinkIcon } from "lucide-react";
import {
TeamPlanBadge,
getTeamPlanBadgeLabel,
} from "../../../../app/(app)/components/TeamPlanBadge";
import { planToTierRecordForGating } from "./planToTierRecord";
import { getTeamPlanBadgeLabel, TeamPlanBadge } from "./TeamPlanBadge";
type SwitchProps = React.ComponentProps<typeof Switch>;
@@ -45,16 +42,15 @@ export const GatedSwitch: React.FC<GatedSwitchProps> = (
</p>
<div className="flex w-full flex-col gap-2">
<Button asChild size="sm" className="justify-start gap-2">
<TrackedLinkTW
target="_blank"
<Button asChild className="justify-start gap-2" size="sm">
<Link
href={`/team/${props.teamSlug}/~/settings/billing?showPlans=true&highlight=${props.requiredPlan}`}
category="advancedFeature"
label="checkout"
rel="noopener noreferrer"
target="_blank"
>
{`Upgrade to ${getTeamPlanBadgeLabel(props.requiredPlan)} plan`}
Upgrade to {getTeamPlanBadgeLabel(props.requiredPlan)} plan
<ExternalLinkIcon className="size-4" />
</TrackedLinkTW>
</Link>
</Button>
</div>
</div>
@@ -65,15 +61,15 @@ export const GatedSwitch: React.FC<GatedSwitchProps> = (
{isUpgradeRequired && (
<TeamPlanBadge
plan={props.requiredPlan}
teamSlug={props.teamSlug}
postfix="+"
teamSlug={props.teamSlug}
/>
)}
<Switch
{...props.switchProps}
checked={props.switchProps?.checked && !isUpgradeRequired}
disabled={props.switchProps?.disabled || isUpgradeRequired}
className={cn("disabled:opacity-100", props.switchProps?.className)}
disabled={props.switchProps?.disabled || isUpgradeRequired}
/>
</div>
</ToolTipLabel>
@@ -1,14 +1,14 @@
import type { Meta, StoryObj } from "@storybook/nextjs";
import { ImageIcon } from "lucide-react";
import { useState } from "react";
import { BadgeContainer } from "../../../stories/utils";
import { Spinner } from "../ui/Spinner/Spinner";
import { BadgeContainer } from "@/storybook/utils";
import { Button } from "../ui/button";
import { Spinner } from "../ui/Spinner/Spinner";
import { Img } from "./Img";
const meta = {
title: "blocks/Img",
component: Story,
title: "blocks/Img",
} satisfies Meta<typeof Story>;
export default meta;
@@ -51,24 +51,24 @@ function Story() {
<BadgeContainer label="Custom Skeleton">
<Img
src={undefined}
className="size-20"
skeleton={
<div className="flex items-center justify-center rounded-lg border">
<Spinner className="size-6" />
</div>
}
className="size-20"
src={undefined}
/>
</BadgeContainer>
<BadgeContainer label="Custom Fallback">
<Img
className="size-20"
fallback={
<div className="flex items-center justify-center rounded-lg border">
<ImageIcon className="size-10 text-muted-foreground" />
</div>
}
className="size-20"
src="invalid-src"
/>
</BadgeContainer>
@@ -82,7 +82,7 @@ function ToggleTest() {
return (
<div className="relative flex flex-col gap-10 border p-6">
<Button
variant="outline"
className="absolute top-6 right-6 inline-flex"
onClick={() => {
if (src) {
setSrc(undefined);
@@ -90,7 +90,7 @@ function ToggleTest() {
setSrc("https://picsum.photos/400");
}
}}
className="absolute top-6 right-6 inline-flex"
variant="outline"
>
Toggle Src
</Button>

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