Compare commits

..
Author SHA1 Message Date
Joaquim VergesGitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
3862bc7a94 Version Packages (#7150)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-05-23 20:03:03 -07:00
gregfromstl 5277eddb70 [SDK] Fix: Webhook Schema (#7151)
<!--

## 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 `Webhook` schema in the `thirdweb` package to enhance the structure of `originToken` and `destinationToken` by replacing simple address strings with detailed objects containing additional properties.

### Detailed summary
- Updated `originToken` and `destinationToken` to be objects with properties:
  - `chainId`
  - `address`
  - `name`
  - `symbol`
  - `decimals`
  - `priceUsd`
  - `iconUri`
- Adjusted the test file to reflect these changes with appropriate object structures.

> ✨ 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**
  - Enhanced webhook support to include detailed token information such as chain ID, name, symbol, decimals, price, and icon for origin and destination tokens.
- **Tests**
  - Updated test data to match the new detailed token structure in webhook payloads.
- **Chores**
  - Added a changeset to document this patch update.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-05-23 23:10:14 +00:00
jnsdls 6c180b5c8a [Dashboard] migrate solidity inputs to shadcn (#7148)
## Summary
- migrate solidity bool, int, bytes, and string inputs to shadcn/ui and tailwind

## Testing
- `pnpm biome check --apply apps/dashboard/src/contract-ui/components/solidity-inputs/bool-input.tsx apps/dashboard/src/contract-ui/components/solidity-inputs/bytes-input.tsx apps/dashboard/src/contract-ui/components/solidity-inputs/int-input.tsx apps/dashboard/src/contract-ui/components/solidity-inputs/string-input.tsx`
- `pnpm test` *(fails: spawn anvil ENOENT)*

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on refactoring the UI components in the `solidity-inputs` directory to use a consistent styling approach and improve the layout by replacing certain components with more flexible alternatives.

### Detailed summary
- Updated imports from `tw-components` to a local `@/components/ui` for `Button` and `Input`.
- Replaced `ButtonGroup` with a `div` for better styling control in `SolidityBoolInput`.
- Adjusted button variants from `solid` to `default` for consistency.
- Introduced a `showConversionButton` flag in `SolidityIntInput` for conditional rendering.
- Changed `InputGroup` to `div` in `SolidityIntInput` and `SolidityStringInput` for layout flexibility.
- Used `cn` utility for conditional class names in `Input` components.
- Updated button and input placements to enhance responsiveness and styling.

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

<!-- end pr-codex -->

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

## Summary by CodeRabbit

- **Style**
  - Updated input components to use custom UI elements and Tailwind CSS for consistent styling.
  - Replaced Chakra UI and third-party components with locally defined UI components and native HTML elements.
  - Improved button and input layouts for better visual consistency across boolean, integer, byte, and string input fields.

- **New Features**
  - Enhanced integer input with conditional display of a conversion button based on input value format.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-05-23 22:57:37 +00:00
Joaquim VergesandGitHub 3489ecec98 [SDK] Improve quote error handling (#7147) 2025-05-23 15:43:52 -07:00
Joaquim VergesGitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
f9b34ff2f1 Version Packages (#7149)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-05-23 14:04:03 -07:00
Jonas DanielsandGitHub f3abea3276 Add webhook verification functionality for secure payload validation (#7103) 2025-05-23 13:59:46 -07:00
Joaquim VergesGitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
adc738e536 Version Packages (#7116)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-05-23 12:44:58 -07:00
Robby UitbeijerseandGitHub dd2fb1b13d [SDK] Feature: Allow for supported providers to be passed to PayEmbed (#7108) 2025-05-23 12:01:27 -07:00
Joaquim VergesandGitHub 69fdef07f5 [thirdweb] Ensure bigints are stringified before usage with server wallets (#7145) 2025-05-23 11:57:43 -07:00
MananTank 362c086bf6 Dashboard: Move project pages inside (sidebar) layout (#7144)
<!--

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

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

## Notes for the reviewer

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

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR primarily focuses on refactoring the codebase to organize components and hooks into a new `(sidebar)` directory structure, enhancing modularity and maintainability. It also updates import paths to reflect these changes.

### Detailed summary
- Moved various components and hooks to a new `(sidebar)` directory.
- Updated import paths across multiple files.
- Adjusted layout files to accommodate the new directory structure.
- Ensured proper functionality of components post-migration.

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

<!-- end pr-codex -->

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

## Summary by CodeRabbit

- **Chores**
  - Updated and standardized import paths across the dashboard and component files for improved code organization and maintainability.
  - No changes to end-user functionality or visible features.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-05-23 18:12:01 +00:00
jnsdls f9b609e7a2 [Dashboard] replace Chakra fieldset with shadcn (#7143)
Migrates the NetworksFieldset component from Chakra UI to shadcn/ui and Tailwind.

- Drop Chakra Flex, FormControl and Select
- Use shadcn Select primitives with tailwind classes
- Adjust layout with Tailwind via `cn`

Fixes #0

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on refactoring the `NetworksFieldset` component in `networks-fieldset.tsx` to use a new `Select` component from the UI library, improving styling and functionality while maintaining the same overall behavior.

### Detailed summary
- Replaced `Flex` and `FormControl` components with a new `div` structure and `Select` component.
- Updated the `Select` component to use `SelectTrigger`, `SelectContent`, and `SelectItem`.
- Changed event handlers from `onChange` to `onValueChange` for better state management.
- Maintained layout and functionality with updated class names for styling.

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

<!-- end pr-codex -->

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

## Summary by CodeRabbit

- **Refactor**
  - Updated the network selection field to use a custom select component and Tailwind-based layout for a more consistent user interface.
- **Style**
  - Improved the visual appearance and spacing of the network selection field.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-05-23 18:00:04 +00:00
MananTank 916ecf494c [TOOL-4591] Fix resolveImplementation jsdoc code example in jsdoc (#7141)
<!--

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

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

## Notes for the reviewer

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

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR updates the import path for the `resolveImplementation` function to reflect its new location within the `thirdweb/utils` directory.

### Detailed summary
- Changed import statement for `resolveImplementation` from `thirdweb` to `thirdweb/utils`.

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

- **Documentation**
  - Updated example import path in documentation for improved clarity.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-05-23 17:11:46 +00:00
MananTank d1ca9af2f4 Remove SIWA feedback buttons from nebula (#7142)
<!--

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

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

## Notes for the reviewer

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

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR primarily focuses on refactoring the `Chats` component and its related functionalities. It removes unused code and simplifies the props being passed, particularly the `teamId`. Additionally, it eliminates the feedback buttons and associated functionality.

### Detailed summary
- Removed `teamId` prop from various instances of the `Chats` component.
- Deleted the `FeedbackButtons` component and its related logic.
- Simplified imports in the `Chats` component by removing unused imports.
- Cleaned up the `RenderMessage` function by omitting `teamId` from props.

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

<!-- end pr-codex -->

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

## Summary by CodeRabbit

- **New Features**
	- None.

- **Bug Fixes**
	- None.

- **Refactor**
	- Removed the feedback feature, including thumbs up/down buttons and related tracking, from chat components.
	- Simplified chat components by removing the unused or redundant team selection prop.

- **Chores**
	- Cleaned up code by deleting unnecessary feedback-related imports and props.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-05-23 16:02:33 +00:00
jnsdls 317211a48e [Dashboard] migrate CurrencySelector to shadcn (#7137)
## Summary
- migrate shared CurrencySelector from Chakra UI to shadcn/ui
- use tailwindcss classes and Radix-based Select

## Checklist
- [x] `pnpm biome check apps/dashboard/src/components/shared/CurrencySelector.tsx --apply`
- [ ] `pnpm test` *(fails: spawn anvil ENOENT)*

<!-- start pr-codex -->

---

## PR-Codex overview
This PR refactors the `CurrencySelector` component to improve its structure and styling. It replaces certain UI elements with a new `Select` component, enhances props for better functionality, and updates the layout for a more modern appearance.

### Detailed summary
- Changed `CurrencySelectorProps` to include new props: `onChange`, `className`, `isDisabled`.
- Refactored the component to use a `Select` from the UI library instead of native HTML elements.
- Updated event handling to utilize `onValueChange` for the `Select`.
- Improved styling and layout for better user experience.
- Replaced `Flex` components with `div` and applied conditional class names for styling.

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

<!-- end pr-codex -->

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

## Summary by CodeRabbit

- **Refactor**
  - Updated the CurrencySelector component to use custom UI components and Tailwind CSS for styling, replacing Chakra UI elements.
  - Simplified the component's props and adjusted event handling for the new select interface.
  - Maintained existing functionality and behavior while improving consistency with the app's design system.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-05-23 04:52:16 +00:00
gregfromstl 6cd64e7368 [Dashboard] Fix: Routes page pagination beyond 1, add og image (#7136)
<!--

## 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 titles for better clarity and modifying the pagination logic in the routes table component to directly use `routesToRender`, enhancing the user experience in the dashboard.

### Detailed summary
- Updated `title` in `page.tsx` files for clarity.
- Changed the condition from `paginatedRoutes.length` to `routesToRender.length` in the routes table component.
- Removed pagination logic variables (`activePage`, `pageSize`, `startIndex`, `endIndex`, `paginatedRoutes`).
- Updated pagination component to use `Number(props.searchParams.page) || 1`.

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

<!-- end pr-codex -->

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

- **New Features**
  - Updated page titles and metadata for improved clarity and consistency, including changes from "On-Ramp" to "Onramp" and a new title for the routes page.
- **Refactor**
  - Simplified the routes list rendering by removing local pagination logic, now displaying data directly as provided without client-side slicing.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-05-23 04:43:14 +00:00
jnsdls 005acce714 Update PostHog import to use React hook (#7135)
# Update PostHog import to use React hook

This PR updates the PostHog implementation in the `useTrack` hook to use the React-specific hook from PostHog instead of the direct import. The changes include:

- Replaced the direct import of `posthog` with `usePostHog` from `posthog-js/react`
- Added the PostHog instance as a dependency to the `useCallback` hook
- Added a null check before calling `posthog.capture()` to handle cases where PostHog might not be initialized

These changes ensure proper React integration with PostHog and prevent potential issues with the hook's dependency array.
2025-05-23 04:34:30 +00:00
jnsdls cde2dd941f Make free plan badge clickable to upgrade (#7134)
# Add Upgrade Paths for Free Plan Teams

This PR enhances the user experience for teams on the free plan by adding clear upgrade paths throughout the dashboard:

- Makes the free plan badge clickable, linking directly to the billing page with pricing modal open
- Adds analytics tracking to billing-related links for better conversion insights
- Implements a new `FreePlanUpsellBannerUI` component on the team dashboard page
- Ensures only one billing alert banner shows at a time (following priority order)
- Updates all `TeamPlanBadge` components to include the team slug parameter
- Improves billing alert banners with proper analytics tracking

These changes aim to increase conversions from free to paid plans by providing contextual upgrade opportunities throughout the product.

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

- **New Features**
  - Introduced a promotional upsell banner for teams on the free plan, encouraging upgrades with a direct link to view available plans.
  - Added a customizable promotional banner component with multiple accent color themes for enhanced marketing displays.

- **Enhancements**
  - Team plan badges now provide direct links to billing settings for teams on the free plan, with analytics tracking for user interactions.
  - Improved analytics tracking on billing-related banners and links throughout the dashboard.

- **Refactor**
  - Updated multiple components to pass team identifiers to plan badges for more contextual display and interaction.
  - Streamlined the display logic for billing-related banners to prioritize service status and plan type.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on enhancing the `TeamPlanBadge` component by adding a `teamSlug` prop across various files and implementing a new `FreePlanUpsellBannerUI` component to encourage free plan users to upgrade. It also updates links to use `TrackedLinkTW` for better tracking.

### Detailed summary
- Added `teamSlug` prop to `TeamPlanBadge` in multiple components.
- Introduced `FreePlanUpsellBannerUI` to promote upgrades for free plan users.
- Replaced `<Link>` with `<TrackedLinkTW>` for better tracking in several instances.
- Updated conditional rendering logic for billing alerts in the team layout.

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

<!-- end pr-codex -->
2025-05-23 04:21:27 +00:00
7130f9a929 [Dashboard] siwa chat widget (#7128)
Co-authored-by: Joaquim Verges <[email protected]>
2025-05-22 21:20:45 -07:00
1e0b1422f3 [SDK] Implement allowedSmsCountryCodes option (#7090)
Co-authored-by: Jonas Daniels <[email protected]>
2025-05-22 21:14:29 -07:00
jnsdls 70a519ada4 [Dashboard] migrate authorized wallets components to shadcn (#7131)
## Summary
- migrate authorized wallets revoke modal from Chakra to shadcn dialog
- replace Chakra button/text in AuthorizedWalletsTable with shadcn + tailwind
- drop Chakra provider wrapper from account devices page
- cleanup modal title/description and remove node cache

## Testing
- `pnpm biome check --apply apps/dashboard/src/components/settings/AuthorizedWallets/AuthorizedWalletRevokeModal.tsx .gitignore`
- `pnpm test` *(fails: spawn anvil ENOENT)*

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on refactoring the `AccountDevicesPage` and the `AuthorizedWalletRevokeModal` components to replace the `Chakra UI` modal with a new `Dialog` component. It also updates the `AuthorizedWalletsTable` to use local state for modal visibility and modifies UI elements for consistency.

### Detailed summary
- Removed `ChakraProviderSetup` from `AccountDevicesPage`.
- Replaced `Modal` components with `Dialog` components in `AuthorizedWalletRevokeModal`.
- Updated modal structure and styling in `AuthorizedWalletRevokeModal`.
- Changed state management for modal visibility in `AuthorizedWalletsTable`.
- Updated text rendering from `Text` to `span` for consistency in `AuthorizedWalletsTable`.
- Changed button styles in `AuthorizedWalletsTable` for revocation actions.

> ✨ 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**
  - Replaced Chakra UI components with custom UI library components in wallet authorization modals and tables.
  - Updated modal and button styling for a consistent look and feel.
  - Simplified modal state management in the authorized wallets table.

- **Chores**
  - Updated `.gitignore` to exclude dashboard node compilation cache.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-05-23 03:54:27 +00:00
gregandGitHub 678c00e322 Fix: Update routes page to use server-side pagination and filtering (#7133) 2025-05-22 17:26:36 -07:00
MananTank 5ab59ff400 Dashboard: Add token selector in asset creation wizard (#7132)
<!--

## 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 token sale functionality within the application. It introduces new properties, refines existing structures, and improves the token selection process to accommodate native tokens and better handle token sales.

### Detailed summary
- Added `saleTokenAddress` to the form validation schema.
- Updated `CreateTokenAssetPage` to handle native token address logic.
- Changed `TokenSelector` to support selected tokens and improved token address handling.
- Enhanced `TokenSaleSection` to include a currency field for token sales.
- Refined state management in `PaymentLinkForm` for better token handling.
- Modified `TokenInfoFieldset` to trigger updates on chain changes.

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

<!-- end pr-codex -->
2025-05-22 23:15:23 +00:00
MananTank 73629e9f0e [TOOL-4531] Dashboard: Add Token Asset creation wizard (#7081)
<!--

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

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

## Notes for the reviewer

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

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR introduces several enhancements and features related to asset management, including deployment tracking, UI updates, and new forms for creating and managing tokens. It also refines existing components and adds new functionalities for improved user experience.

### Detailed summary
- Added `deploymentType` and `contractType` properties in various components.
- Introduced `revalidatePathAction` for path revalidation.
- Updated UI components for better asset management.
- Enhanced forms for token creation with validation.
- Implemented multi-step status tracking for token launch processes.
- Improved error handling and user feedback in forms.
- Added new charts for token distribution visualization.

> The following files were skipped due to too many changes: `apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/assets/create/launch/launch-token.tsx`, `apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/assets/create/distribution/token-airdrop.tsx`

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

<!-- end pr-codex -->

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

- **New Features**
  - Introduced a comprehensive asset management interface for creating, importing, and managing ERC-20 tokens within team and project dashboards.
  - Added a multi-step token creation flow with form validation, including token info, distribution, sale settings, airdrop CSV upload, and launch process with real-time status tracking.
  - Implemented visual token allocation charts and multi-step progress indicators for enhanced user experience.
  - Added a dedicated "Assets" section in the sidebar with a "New" badge.
  - Added a horizontal segmented distribution bar chart component for visualizing token allocations.
  - Introduced a multi-step status component to track asynchronous step executions with retry capabilities.
  - Added token airdrop CSV upload and validation feature with user-friendly UI and error handling.
  - Added token sale configuration section with price and allocation inputs.
  - Enhanced import modal to support both contract and asset imports with contextual UI and behavior.
  - Added new pages and components to support token asset creation and management workflows, including cards for asset actions.
  - Added server-side path revalidation support for pages and layouts.

- **Improvements**
  - Enhanced contract and asset import workflows with clearer UI, context-aware dialogs, and improved contract table variant handling.
  - Updated number input fields to remove browser-native spinners for a cleaner appearance.
  - Improved analytics event tracking for user interactions and deployment steps.
  - Refined UI components for consistent styling and accessibility.
  - Fixed sidebar badge placement and updated UI text for clarity.
  - Updated dialogs and payment embeds to reference transaction chain instead of active wallet chain and set default payment amounts.
  - Changed token supply label to "Circulating Supply" for clarity.
  - Replaced Chakra UI spinner and styled elements with local components for simplified UI.

- **Bug Fixes**
  - Corrected UI typos and improved analytics event tracking for deployment errors.

- **Documentation**
  - Added Storybook stories for new components to facilitate UI testing and documentation.

- **Chores**
  - Refactored and extended types and props to support new asset and contract features, ensuring future extensibility.
  - Added tracking utilities for token deployment and asset creation analytics.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-05-22 23:15:23 +00:00
23029db9ef Feature: Add image support to payment links (#7129)
Co-authored-by: Claude <[email protected]>
2025-05-22 12:47:05 -07:00
jnsdls 526b6b54cf Add useAuthToken hook for in-app wallet authentication (#7120)
# Add `useAuthToken` hook for in-app wallet authentication

This PR adds a new React hook `useAuthToken()` that returns a JWT authentication token for the currently active in-app wallet. This token can be used to authorize API calls to backend servers.

Key changes:
- Added `useAuthToken()` hook that returns the auth token when an in-app wallet is active
- Modified the `InAppConnector` interface to expose the storage property
- Updated the wallet interface to include a `getAuthToken()` method for in-app wallets
- Implemented token retrieval in the in-app wallet core implementation

The hook ensures the token is only returned when the active wallet is an in-app wallet and matches the active account.

Example usage:
```tsx
function MyComponent() {
  const authToken = useAuthToken();

  const fetchData = async () => {
    const response = await fetch('https://api.example.com/data', {
      headers: {
        'Authorization': `Bearer ${authToken}`
      }
    });
    // ... handle response
  };
}
```

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

- **New Features**
	- Introduced a new hook to retrieve authentication tokens for in-app wallets, enabling secure authorization for API calls.
- **Documentation**
	- Added usage examples and clarifications regarding authentication token availability for wallets.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on enhancing the wallet functionality by introducing an authentication token feature and updating the visibility of certain properties. It adds a method for retrieving an authentication token and modifies the storage property to be public.

### Detailed summary
- Changed `storage` property from `private` to `public` in `native-connector.ts` and `web-connector.ts`.
- Added `getAuthToken` method to the wallet interface in `wallet.ts` with documentation.
- Included `storage` in the `InAppConnector` interface.
- Implemented `authToken` handling in `createInAppWallet` function.
- Created `useAuthToken` hook for retrieving the JWT of the active wallet.

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

<!-- end pr-codex -->
2025-05-22 18:01:48 +00:00
Jonas DanielsandGitHub f31116ea5a [SDK] fix avatar image detection on Node (#7123)
Signed-off-by: Jonas Daniels <[email protected]>
2025-05-22 11:00:48 -07:00
arcoraven c1f7aaadc1 chore: redirect /team to /team/~ to select a team (#7124)
## [Dashboard] Fix: Simplify team redirection logic

## Notes for the reviewer
This PR modifies the team redirection logic by:
1. Renaming `getLastVisitedTeamOrDefaultTeam()` to `getLastVisitedTeam()`
2. Removing the fallback to `getDefaultTeam()` when no last visited team is found
3. Updating the team root page to redirect to `/team/~` when no team is found instead of showing a 404

## How to test
Verify that users are properly redirected to their last visited team when available, and to `/team/~` when no team history exists.

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

## Summary by CodeRabbit

- **Bug Fixes**
	- Improved handling when no last visited team is found by redirecting users to a fallback page instead of showing a not found error.

- **Refactor**
	- Updated team selection logic to remove fallback to a default team and streamline user redirection.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR refactors the `getLastVisitedTeamOrDefaultTeam` function to `getLastVisitedTeam`, changing its behavior to return `null` instead of a default team. It updates the `TeamRootPage` to handle the absence of a team by redirecting to a fallback URL.

### Detailed summary
- Renamed function `getLastVisitedTeamOrDefaultTeam` to `getLastVisitedTeam`.
- Changed return value of `getLastVisitedTeam` from `getDefaultTeam()` to `null`.
- Updated `TeamRootPage` to redirect to `/team/~` if no team is found, instead of calling `notFound()`.

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

<!-- end pr-codex -->
2025-05-22 06:42:40 +00:00
arcoraven 1ad109f3bc chore: update endpoint to rotate secret key (#7122)
## [Dashboard] Fix: Update Secret Key Rotation API Endpoint

## Notes for the reviewer

This PR updates the secret key rotation functionality to use the new team-based API endpoint structure. The `rotateSecretKeyClient` function now requires both `teamId` and `projectId` parameters, and the API endpoint has been updated to follow the `/v1/teams/{teamId}/projects/{projectId}/rotate-secret-key` pattern.

## How to test

Test the secret key rotation functionality in:
1. Project FTUX
2. Nebula FTUX
3. Project General Settings page

Verify that the secret key can be successfully rotated in all these locations.

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

- **Refactor**
  - Updated secret key rotation to require both team and project identifiers for enhanced security and context.
  - Modified components to include team ID alongside project data, improving integration and user experience in project-related sections.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on updating the handling of project and team identifiers in the `NebulaFTUX` and related components, enhancing the functionality for rotating secret keys by including the `teamId` alongside the `projectId`.

### Detailed summary
- Added `teamId` prop to the `NebulaFTUX` component.
- Passed `teamId` from `NebulaFTUX` to `SecretKeySection`.
- Updated `rotateSecretKeyClient` to accept an object with `teamId` and `projectId`.
- Modified the API call in `rotateSecretKeyClient` to use the new parameters.
- Updated the `SecretKeySection` to handle the new `teamId` prop.

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

<!-- end pr-codex -->
2025-05-22 06:25:22 +00:00
376bdb23d2 [Dashboard] Feature: Setup payment link UI (#7121)
Co-authored-by: Claude <[email protected]>
2025-05-21 22:40:59 -07:00
Joaquim VergesandGitHub dcd6b99e67 [SDK] Improve error handling in PayEmbed (#7119) 2025-05-21 21:52:31 -07:00
Jonas DanielsandGitHub 88533d9113 Remove advancedEnabled field from Account interface (#7118) 2025-05-21 16:16:21 -07:00
Jonas DanielsandGitHub 6c3a8d36a7 Remove unused account usage API and interface (#7117) 2025-05-21 16:09:16 -07:00
Robby UitbeijerseandGitHub a31ac0c065 [Docs] Chore: Remove Kado references (#7109) 2025-05-21 15:21:40 -07:00
7b72e88611 [SDK] Fix: Apply accentButtonText theme key to the Spinner inside of PayEmbed Buttons (#7110)
Co-authored-by: Joaquim Verges <[email protected]>
2025-05-21 15:21:14 -07:00
Joaquim VergesandGitHub 4b9a506fdf [service-utils] Improve 403 error messages with team and project details (#7114) 2025-05-21 14:07:40 -07:00
Joaquim VergesandGitHub 77da352366 [Dashboard] Add ecosystem name editing functionality (#7115) 2025-05-21 13:54:12 -07:00
Joaquim VergesandGitHub 72cc27a69a [Dashboard] Add ecosystem logo upload functionality (#7113) 2025-05-21 11:02:18 -07:00
Joaquim VergesGitHubJonas Danielsgraphite-app[bot] <96075541+graphite-app[bot]@users.noreply.github.com>
46d1dab11e [Docs] Add Vault SDK documentation and guides (#7054)
Signed-off-by: Joaquim Verges <[email protected]>
Co-authored-by: Jonas Daniels <[email protected]>
Co-authored-by: graphite-app[bot] <96075541+graphite-app[bot]@users.noreply.github.com>
2025-05-21 11:01:56 -07:00
Joaquim VergesGitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
a1c432ca17 Version Packages (#7105)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-05-21 09:59:10 -07:00
kumaryash90 243c661929 Update deploy config (#7111)
<!--

## 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 value of `DEFAULT_FEE_BPS_NEW` in the `addresses.ts` file to reflect a new default fee setting.

### Detailed summary
- Updated the value of `DEFAULT_FEE_BPS_NEW` from `150` to `200` in `apps/dashboard/src/constants/addresses.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

- **Chores**
  - Updated the default fee value from 150 to 200.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-05-21 16:07:00 +00:00
GiselleNessi b64ffe369a Gi/portal knowledge base (#6993)
<!--

## 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 `thirdweb Knowledge Base` by adding new troubleshooting guides, updating existing resources, and improving documentation structure for better user support.

### Detailed summary
- Added a new troubleshooting guide for `Nebula`.
- Introduced detailed steps for resolving `device link errors` in `thirdweb CLI`.
- Updated support links in `Header.tsx`.
- Created new sections for `batch upload issues` and `airdrop contract troubleshooting`.
- Improved layout in `layout.tsx` with `DocLayout`.
- Removed unused `ContractsIcon` from `page.tsx`.
- Enhanced the `knowledge base` sidebar structure.
- Added visual guides and images in multiple `mdx` files for clarity.
- Improved metadata for better SEO and documentation clarity.

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

<!-- end pr-codex -->

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

- **New Features**
  - Introduced a comprehensive Knowledge Base with structured navigation, guides, and troubleshooting for web3 development and thirdweb support.
  - Added detailed documentation on deploying NFT mint sites, creating soulbound tokens, sharing console errors, and resolving CLI device link errors.
  - Launched troubleshooting guides for airdrop contracts, batch upload issues, embed feature changes, and Nebula-specific support.

- **Improvements**
  - Updated navigation links to add a Nebula API reference and consolidated support links to internal resources.

- **Removals**
  - Removed the "Contracts" article card from the learning resources section.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-05-21 10:32:06 +00:00
Joaquim VergesandGitHub 30fafd16db [SDK] Fix SIWE auth forcing unnecessary chain switch (#7104) 2025-05-20 23:28:53 -07:00
Joaquim VergesGitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
99901dcfd7 Version Packages (#7079)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-05-20 15:48:08 -07:00
jnsdls f5072d882e Rename "Secret Key" to "Webhook Secret" for consistency (#7102)
<!-- This is an auto-generated comment: release notes by coderabbit.ai -->

## Summary by CodeRabbit

- **Style**
  - Updated labels and tooltips to use "Webhook Secret" instead of "Secret Key" for improved clarity in the webhook creation interface.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-05-20 22:10:27 +00:00
iuwqyir e7f7a32dd4 show block gas limit on the chain page (#7101)
## [Dashboard] Feature: Add Block Gas Limit to Chain Live Stats

## Notes for the reviewer

as requested by @0xFirekeeper

This PR enhances the chain live stats component by adding the block gas limit information. The RPC method has been updated from `eth_blockNumber` to `eth_getBlockByNumber` to fetch additional block data including the gas limit.

## How to test

The block gas limit should now be displayed alongside other chain statistics in the dashboard. The component handles loading and error states appropriately.

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

## Summary by CodeRabbit

- **New Features**
  - Added display of the latest block's gas limit in the live stats section, including proper handling of loading, error, and unavailable states.

- **Enhancements**
  - Improved the accuracy of live stats by retrieving and displaying more detailed block information.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR updates the `live-stats.tsx` component to fetch the latest block information using the `eth_getBlockByNumber` method instead of `eth_blockNumber`. It also introduces the display of the block gas limit in the UI.

### Detailed summary
- Changed JSON-RPC method from `eth_blockNumber` to `eth_getBlockByNumber`.
- Updated parameters to fetch the latest block.
- Added parsing for `blockGasLimit` from the response.
- Modified return object to include `blockGasLimit`.
- Introduced a new UI section to display the block gas limit.

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

<!-- end pr-codex -->
2025-05-20 21:55:31 +00:00
d2098ad0bf [SDK] Fix stray keccak256 reference (#7089)
Co-authored-by: Jonas Daniels <[email protected]>
2025-05-20 14:54:41 -07:00
+1
Joaquim VergesGitHubgregfromstlClaudekumaryash90Prithvish BaidyasaminaMaximilian Hubertgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>Jonas Danielsarcoraven
d6239785cf [SDK] Always show pay modal even on non-supported UB chains (#7092)
Signed-off-by: samina <[email protected]>
Signed-off-by: Maximilian Hubert <[email protected]>
Co-authored-by: gregfromstl <[email protected]>
Co-authored-by: Claude <[email protected]>
Co-authored-by: kumaryash90 <[email protected]>
Co-authored-by: Prithvish Baidya <[email protected]>
Co-authored-by: samina <[email protected]>
Co-authored-by: Maximilian Hubert <[email protected]>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Jonas Daniels <[email protected]>
Co-authored-by: arcoraven <[email protected]>
2025-05-20 14:51:58 -07:00
iuwqyir 20fdd5b6d7 show deprecation in playground UI (#7100)
<!-- start pr-codex -->

## PR-Codex overview
This PR focuses on adding a `deprecated` property to various types and components within the application, allowing for better handling and display of deprecated links and features in the UI.

### Detailed summary
- Added `deprecated` property to `pathInfo` in `navLinks.ts`.
- Updated `MinimalBlueprintSpec` type in `utils.ts` to include `deprecated`.
- Modified `SidebarLink` type in `sidebar.tsx` to optionally include `crossedOut`.
- Enhanced UI to visually indicate deprecated items in `page.tsx`.
- Included `deprecated` status for various API endpoints in `insightBlueprints.ts`.

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

<!-- end pr-codex -->
2025-05-20 21:04:28 +00:00
iuwqyir 438c019f01 replace chain with chain_id in insight playground (#7099)
## [Playground] Fix: Update chain parameter handling in blueprint playground

## Notes for the reviewer
This PR updates the blueprint playground to use `chain_id` instead of `chain` parameter throughout the codebase. It also adds logic to remove deprecated parameters from the form schema and specifically removes the `chain` parameter if it's present.

## How to test
Test the blueprint playground with various chain selections to ensure the chain_id parameter is correctly populated and that deprecated parameters are not displayed in the form.

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

- **Refactor**
  - Updated all references from "chain" to "chain_id" for improved consistency across the playground interface.
  - Deprecated parameters are now excluded from form generation and display.

- **New Features**
  - Blueprint listings can now indicate if a blueprint is deprecated.

- **Chores**
  - Made the API domain configurable via environment variables with a default fallback.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR introduces changes to the `BlueprintSection`, `fetchBlueprintSpec`, and other components to enhance the handling of blueprint parameters, including the addition of a `deprecated` flag and renaming of parameters for consistency. It also updates the API URL handling.

### Detailed summary
- Added optional `deprecated` property to `blueprints` in `BlueprintSection`.
- Introduced `THIRDWEB_INSIGHT_API_DOMAIN` for dynamic API URL in `fetchBlueprintSpec`.
- Renamed parameter `chain` to `chain_id` in multiple places for consistency.
- Removed `chain` parameter if present in `modifyParametersForPlayground`.
- Updated form handling to use `chain_id` instead of `chain` in `ParameterSection`.
- Skipped deprecated parameters in schema creation with `createParametersFormSchema`.

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

<!-- end pr-codex -->
2025-05-20 20:46:17 +00:00
Jonas DanielsandGitHub 61044013c8 update dependencies (#7098) 2025-05-20 11:52:26 -07:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
e6213db8af chore(deps-dev): bump @storybook/react-vite from 8.6.12 to 8.6.14 (#7088)
Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-20 11:24:24 -07:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
c46b2597be chore(deps-dev): bump vitest from 3.1.2 to 3.1.4 (#7087)
Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-20 11:24:12 -07:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
9ff97688ce chore(deps-dev): bump vite from 6.3.4 to 6.3.5 in the npm_and_yarn group (#7068)
Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-20 11:20:20 -07:00
gregandGitHub b4f5cc7b5a Fix: Bridge jsdoc to not mention ox (#7097) 2025-05-20 11:15:06 -07:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
256850d1c6 chore(deps): bump @next/mdx from 15.3.1 to 15.3.2 (#7085)
Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-20 11:13:10 -07:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
acb2b9f85f chore(deps): bump github/codeql-action from 3.28.17 to 3.28.18 (#7084)
Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-20 11:12:54 -07:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
6ac62ffe96 chore(deps): bump codecov/codecov-action from 5.4.2 to 5.4.3 (#7083)
Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-20 11:12:40 -07:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
80f4bd4e53 chore(deps): bump changesets/action from 1.5.0 to 1.5.3 (#7082)
Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-20 11:12:25 -07:00
Joe-Thirdweb a3072c4335 [Dashboard] Adding EDU Chain custom info + assets (#7095)
Adding custom chain info for
EDU Mainnet ( 41923 )
EDU Testnet ( 656476 )

<!-- start pr-codex -->

---

## PR-Codex overview
This PR introduces new assets and metadata for an education-focused blockchain initiative, enhancing the dashboard with information about the `EDU Mainnet` and `EDU Testnet`.

### Detailed summary
- Added new image imports: `eduBanner` and `eduCTA`.
- Introduced metadata for `EDU Mainnet` (ID: 41923) with header image, description, and CTA.
- Introduced metadata for `EDU Testnet` (ID: 656476) with similar structure as `EDU Mainnet`.

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

<!-- end pr-codex -->
2025-05-20 17:53:29 +00:00
saminacodes 3277cf6e8e [Docs] Fixed broken links (#7091)
CORE-0000

<!-- start pr-codex -->

---

## PR-Codex overview
This PR primarily focuses on updating references from "backend wallets" to "server wallets" across various files and adding a new `ArticleCardIndex` component in the `page.tsx` file.

### Detailed summary
- In `sidebar.tsx`, updated `href` for the "Overview" link.
- Added a new `ArticleCardIndex` for "Universal Bridge" in `page.tsx`.
- Changed text from "backend wallets" to "server wallets" in `page.mdx`.
- Updated link in `engine-overview.tsx` to point to server wallet documentation.

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

<!-- end pr-codex -->
2025-05-20 17:44:56 +00:00
Jake LooandGitHub 77938c4d5c Update page.mdx for thirdweb-mcp (#7096)
Signed-off-by: Jake Loo <[email protected]>
2025-05-20 10:15:57 -07:00
Vasiliy KirochkaandGitHub 3080a9baba fixing code snippets (#7094) 2025-05-20 09:04:18 -07:00
Joaquim VergesandGitHub 6cf92c6b11 Add AGENTS.md with guidelines (#7093) 2025-05-20 00:18:02 -07:00
+1
Joaquim VergesGitHubgregfromstlClaudekumaryash90Prithvish BaidyasaminaMaximilian Hubertgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>Jonas Danielsarcoraven
89ccc80d9c Add Bridge.Onramp functions + port old functions (#7076)
Signed-off-by: samina <[email protected]>
Signed-off-by: Maximilian Hubert <[email protected]>
Co-authored-by: gregfromstl <[email protected]>
Co-authored-by: Claude <[email protected]>
Co-authored-by: kumaryash90 <[email protected]>
Co-authored-by: Prithvish Baidya <[email protected]>
Co-authored-by: samina <[email protected]>
Co-authored-by: Maximilian Hubert <[email protected]>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Jonas Daniels <[email protected]>
Co-authored-by: arcoraven <[email protected]>
2025-05-19 19:24:32 -07:00
saminacodes 713ebe0a2d [Docs] Update Engine Pricing (#7080)
CORE-0000

<!-- start pr-codex -->

---

## PR-Codex overview
This PR updates the FAQ section in the `page.mdx` file regarding pricing calculations for Engine Cloud. It clarifies the types of requests that incur costs and modifies the note about legacy pricing plans.

### Detailed summary
- Updated pricing calculation to include both write and sign requests.
- Changed the cost description for requests to specify both write and sign requests.
- Revised the note on legacy pricing plans to include a link to the pricing page and clarify the fee specifics.

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

<!-- end pr-codex -->
2025-05-19 15:51:01 +00:00
saminacodes bfdac10dcc [Docs] UB Migration & Other Small Things (#7075)
CORE-0000

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on restructuring the `pay` section of the portal by updating links, modifying sidebar navigation, and adding new assets and pages to improve the user experience and documentation clarity.

### Detailed summary
- Added new links to the `links` array for `Bridge`.
- Updated `href` values in `apisLinks` for the Insight API.
- Modified `FooterLinkItem` labels for consistency.
- Created and adjusted multiple `.mdx` pages for the `pay` section.
- Introduced `payRedirects` for improved navigation.
- Updated the sidebar structure for the `pay` section, including new links and icons.
- Added new layout and metadata to `layout.tsx` for better documentation presentation.

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

<!-- end pr-codex -->
2025-05-19 04:26:51 +00:00
+1
gregGitHubClaudeJoaquim Vergeskumaryash90Prithvish BaidyasaminaMaximilian Hubertgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>Jonas Danielsarcoraven
64964da22e [SDK] Refactor: Update Pay Functions to use UB (#7064)
Signed-off-by: samina <[email protected]>
Signed-off-by: Maximilian Hubert <[email protected]>
Co-authored-by: Claude <[email protected]>
Co-authored-by: Joaquim Verges <[email protected]>
Co-authored-by: kumaryash90 <[email protected]>
Co-authored-by: Prithvish Baidya <[email protected]>
Co-authored-by: samina <[email protected]>
Co-authored-by: Maximilian Hubert <[email protected]>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Jonas Daniels <[email protected]>
Co-authored-by: arcoraven <[email protected]>
2025-05-19 16:05:15 +12:00
Joaquim VergesGitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
260c2fd370 Version Packages (#7078)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-05-18 19:12:58 -07:00
Joaquim VergesandGitHub c61e258ec3 [SDK] Expose getInstalledWallets() utility (#7077) 2025-05-18 19:07:42 -07:00
Joaquim VergesandGitHub fdd344184b [Dashboard] Remove taint for THIRDWEB_ENGINE_URL (#7074) 2025-05-17 00:51:54 -07:00
Joaquim VergesGitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
0c853e8709 Version Packages (#7073)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-05-17 00:36:55 -07:00
arcoraven 7d47b4cde0 chore: Update Accelerate features list (#7053)
<!-- start pr-codex -->

## PR-Codex overview
This PR focuses on updating the pricing plans and features for the embedded wallets in the dashboard application. It adjusts the plan requirements and modifies the features associated with different plans.

### Detailed summary
- Changed `authRequiredPlan` from `"accelerate"` to `"growth"`.
- Added `"Custom Wallet Branding & Auth"` to the features list.
- Removed `"Custom Wallet Branding & Auth"` from the `TEAM_PLANS` features.
- Increased `$400 Usage Credits` to `$500 Usage Credits` in the `TEAM_PLANS` features.

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

<!-- end pr-codex -->
2025-05-17 07:01:00 +00:00
Jonas DanielsandGitHub feae304da2 Prioritize JWT over service API keys in authentication (#7020) 2025-05-16 16:39:45 -07:00
Joaquim VergesandGitHub 8f605d989c [SDK] Update package READMEs with correct naming and documentation links (#7071) 2025-05-16 05:55:22 -07:00
Joaquim VergesGitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
d0c2ac63a9 Version Packages (#7042)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-05-16 05:06:17 -07:00
Joaquim VergesandGitHub 170f14fb54 [Dashboard] Add pagination to server wallets page (#7070) 2025-05-16 05:04:26 -07:00
Joaquim VergesandGitHub da2a2a0e86 [SDK] Fix buyWithCrypto false not respected when returning from quote (#7069) 2025-05-16 04:57:48 -07:00
saminaandGitHub e62fd3fe40 [Docs] Engine Update (#7052)
Signed-off-by: samina <[email protected]>
2025-05-16 04:09:33 -07:00
Joaquim VergesandGitHub 99a7cce624 [Engine] Add all testnets to test transaction options (#7066) 2025-05-16 04:08:17 -07:00
Joaquim VergesandGitHub 61152dd098 [Engine] Add FAILED status to execution result and update error handling (#7065) 2025-05-16 03:34:27 -07:00
Maximilian HubertandGitHub 05519fcdde [DOCS] fix link in contributing.md (#7063)
Signed-off-by: Maximilian Hubert <[email protected]>
2025-05-16 00:11:57 -07:00
saminaandGitHub 7257747520 [Docs] Engine (#7057)
Signed-off-by: samina <[email protected]>
2025-05-16 00:10:35 -07:00
Prithvish BaidyaandGitHub e71e7d70a6 [Vault] default to engine-cloud vault proxy (#7058) 2025-05-15 16:44:03 -07:00
Joaquim VergesandGitHub 8d47864967 [SDK] Export prepareUserOp and add support for new wallets (#7061) 2025-05-15 16:43:11 -07:00
kumaryash90 4a8adc143f [Docs] Stylus dev workflow (#7060)
<!--

## 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 documentation and assets for integrating thirdweb tools into the Stylus contract development workflow, specifically for Rust. It includes setup instructions, publishing and deployment commands, and images to enhance the documentation.

### Detailed summary
- Added new sections in `page.mdx` for Arbitrum Stylus.
- Included prerequisites for Rust and Solidity installations.
- Provided setup commands for creating a Stylus project.
- Added commands for publishing and deploying Stylus contracts.
- Included images (`stylus-publish-page.png`, `stylus-deploy-page.png`) to illustrate the process.

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

<!-- end pr-codex -->
2025-05-15 21:30:32 +00:00
MananTank db538473cf [NEB-248] Show native token balances in assets, show popular tokens on top (#7056)
<!--

## 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 `AssetsSection` component by integrating wallet balance retrieval, modifying how assets are displayed, and implementing new logic for sorting tokens. It also introduces a new constant for native tokens and adjusts rendering based on whether an asset is native.

### Detailed summary
- Added `NATIVE_TOKEN_ADDRESS` and `getWalletBalance` import from `thirdweb`.
- Updated `key` for `AssetItem` to include `chain_id`.
- Introduced `isNativeToken` to conditionally render the `ChainIconClient`.
- Modified the `href` for `Link` based on whether the asset is a native token.
- Added a new set `tokensToShowOnTop` for sorting specific tokens.
- Implemented `nativeBalances` query to fetch wallet balances for various chains.
- Combined `nativeBalances` and `assetsQuery` data for rendering in `AssetsSectionUI`.

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

<!-- end pr-codex -->
2025-05-15 20:52:49 +00:00
Manan TankandGitHub 2057546934 Nebula: Recent Activity improvements in sidebar (#7055) 2025-05-15 13:52:15 -07:00
gregandGitHub ba52e68eab [Dashboard] Fix: Trend markers and UB volume type dropdown (#7059) 2025-05-15 12:05:40 -07:00
0eff64824b [Dashboard] Feature: Add Universal Bridge to analytics dashboard (#7051)
Co-authored-by: Claude <[email protected]>
2025-05-14 18:07:07 -07:00
saminacodes ae7407dca4 [Docs] engine and ub changes (#7049)
<!--

## 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 documentation and user guidance for the `thirdweb` platform, specifically around the Engine v3 features, onramp providers, and pricing details. It introduces new FAQs, clarifies migration steps, and adds visual content.

### Detailed summary
- Added new section for "Onramp Providers" in `sidebar.tsx`.
- Updated Engine FAQs in `page.mdx` with pricing and transaction details.
- Revised migration guide in `page.mdx` with clearer instructions and a callout for wallet migration.
- Enhanced `faqs/page.mdx` with additional questions about Universal Bridge and supported payment methods.
- Updated "Buy With Fiat" availability to 160+ countries.
- Included images for creating a vault and sending transactions in the `get-started/page.mdx`.
- Revised onramp providers section to clarify provider preferences and fees in `onramp-providers/page.mdx`.

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

<!-- end pr-codex -->
2025-05-14 22:53:11 +00:00
Joe-Thirdweb a443b309dc Adding NeoX custom chain info. (#7045)
Adding Custom chain info to

- NeoX Mainnet 47763
- NeoX Testnt 12227332

<!-- start pr-codex -->

---

## PR-Codex overview
This PR introduces new assets and metadata for the `Neo X` blockchain, enhancing the dashboard with images and information about its features and offerings.

### Detailed summary
- Added imports for `NeoXBanner` and `NeoXCTA` images.
- Introduced metadata for `Neo X` (ID: 47763) including:
  - `headerImgUrl`
  - `about` description
  - `cta` object with `backgroundImageUrl`, `title`, `buttonLink`, and `buttonText`.
- Duplicated metadata for `Neo X Testnet` (ID: 12227332) with similar structure.

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

<!-- end pr-codex -->
2025-05-14 22:44:28 +00:00
Joaquim VergesGitHubgraphite-app[bot] <96075541+graphite-app[bot]@users.noreply.github.com>
ae2ff743c0 [Engine] Update OpenAPI spec and configuration (#7050)
Signed-off-by: Joaquim Verges <[email protected]>
Co-authored-by: graphite-app[bot] <96075541+graphite-app[bot]@users.noreply.github.com>
2025-05-14 15:43:30 -07:00
MananTank dfa8603593 [NEB-213] Nebula: Show wallet Assets and Activity in sidebar (#7046)
<!--

## 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 `AssetsSection` and `TransactionsSection` components by adding new features and improving the UI. It also modifies several existing components for better functionality and user experience.

### Detailed summary
- Changed `mergedSessions.push(session)` to `mergedSessions.unshift(session)` in `useSessionsWithLocalOverrides.ts`.
- Updated the width of the sidebar in `ChatPageLayout.tsx` from `280px` to `300px`.
- Added `customDetailsButton` prop to `NebulaConnectWallet` in `NebulaConnectButton.tsx`.
- Implemented conditional rendering for `customDetailsButton` in `NebulaConnectWallet`.
- Created storybook stories for `AssetsSection` and `TransactionsSection` with stubs for tokens and transactions.
- Enhanced `AssetsSection` and `TransactionsSection` components to handle loading states and display messages when there are no assets or transactions.
- Introduced `WalletDetails` component in `ChatSidebar` to manage assets and transactions tabs.
- Refactored `WalletSelector` to integrate `useActiveWallet` for better wallet management and user feedback.
- Improved button components with tooltips for better accessibility and UX.

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

<!-- end pr-codex -->
2025-05-14 21:47:42 +00:00
saminacodes 9f1ca54c26 Update page.mdx (#7048)
CORE-0000

<!-- start pr-codex -->

---

## PR-Codex overview
This PR updates the `preferredProvider` options in the `detailsModal` configuration for the `connectbutton` component, enhancing payment provider flexibility.

### Detailed summary
- Modified `preferredProvider` options in `detailsModal`:
  - Removed `"KADO"`
  - Added `"COINBASE"`

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

<!-- end pr-codex -->
2025-05-14 17:44:23 +00:00
MananTank 7df81b7323 [TOOL-4493] Dashboard: Add starter legacy discontinuation banner (#7047)
<!--

## 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 `StarterLegacyDiscontinuedBanner` component to the `RootTeamLayout` in `layout.tsx`. It displays a notification for users on the "starter_legacy" billing plan, informing them of the discontinuation and providing a link to select a new plan.

### Detailed summary
- Added a conditional rendering for `StarterLegacyDiscontinuedBanner` based on the team's billing plan.
- Created the `StarterLegacyDiscontinuedBanner` component to inform users about the discontinuation of starter legacy plans.
- Included a button with a link to the billing settings for selecting a new plan, featuring an `ArrowRightIcon`.

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

<!-- end pr-codex -->
2025-05-14 16:25:35 +00:00
GiselleNessi ab74fcdaf8 add new apis tab to nav (#7044)
<!--

## 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 adds a new section to the `Header` component in the `Header.tsx` file, introducing `apisLinks` for API documentation links and integrating a `DropdownLinks` component for displaying these links.

### Detailed summary
- Added a new constant `apisLinks` containing links to API documentation.
- Introduced a `DropdownLinks` component to display `apisLinks` in the header.
- Adjusted padding in the header's layout from `xl:px-2` to `xl:px-1` for the new dropdown.

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

<!-- end pr-codex -->
2025-05-14 15:09:12 +00:00
Joaquim VergesandGitHub 1191d5dad9 [Engine] Add EOA read/create permissions for server wallets (#7043) 2025-05-14 22:32:35 +12:00
Joaquim VergesandGitHub 20b5ba943a [SDK] Propagate 401 errors when connecting in-app wallet (#7040) 2025-05-14 02:03:41 -07:00
661 changed files with 18139 additions and 8958 deletions
+1 -1
View File
@@ -14,7 +14,7 @@ We use [Turborepo](https://turbo.build/repo/docs) to manage the repository, and
We use [pnpm](https://pnpm.io) for package management across the repo. `pnpm` is similar to `npm` or `yarn` but with more efficient disk space usage.
**With the v5 SDK, we've consolidated everything into a single project at [/packages/thirdweb](./packages/thirdweb). You can still find the legacy packages at [/legacy_packages](./legacy_packages).**
**With the v5 SDK, we've consolidated everything into a single project at [/packages/thirdweb](../packages/thirdweb). You can still find the legacy packages at [/legacy_packages](../legacy_packages).**
This single package provides a performant & lightweight SDK to interact with any EVM chain across Node, React, and React Native. Learn more about how to use the thirdweb SDK in our [documentation](https://portal.thirdweb.com/typescript/v5).
+1 -1
View File
@@ -89,7 +89,7 @@ jobs:
- run: pnpm test
- name: Code Coverage
uses: codecov/codecov-action@ad3126e916f78f00edff4ed0317cf185271ccc2d # v5.4.2
uses: codecov/codecov-action@18283e04ce6e62d37312384ff67231eb8fd56d24 # v5.4.3
with:
directory: packages/
flags: packages
+3 -3
View File
@@ -46,7 +46,7 @@ jobs:
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@60168efe1c415ce0f5521ea06d5c2062adbeed1b # v3.28.17
uses: github/codeql-action/init@ff0a06e83cb2de871e5a09832bc6a81e7276941f # v3.28.18
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@60168efe1c415ce0f5521ea06d5c2062adbeed1b # v3.28.17
uses: github/codeql-action/autobuild@ff0a06e83cb2de871e5a09832bc6a81e7276941f # v3.28.18
# ℹ️ 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@60168efe1c415ce0f5521ea06d5c2062adbeed1b # v3.28.17
uses: github/codeql-action/analyze@ff0a06e83cb2de871e5a09832bc6a81e7276941f # v3.28.18
+1 -1
View File
@@ -36,7 +36,7 @@ jobs:
- name: Create release Pull Request or publish to NPM
id: changesets
uses: changesets/action@746c25e23caa47dceb6a48ee85b4cbc5a9f5f293 #v1.4.0
uses: changesets/action@e0145edc7d9d8679003495b11f87bd8ef63c0cba #v1.4.0
with:
publish: pnpm release
version: pnpm version-packages
+2 -1
View File
@@ -31,4 +31,5 @@ storybook-static
.aider*
tsconfig.tsbuildinfo
.cursor
.cursor
apps/dashboard/node-compile-cache
+123
View File
@@ -0,0 +1,123 @@
🤖 Codex Agent Guidelines for thirdweb-dev/js
Welcome, AI copilots! This guide captures the coding standards, architectural decisions, and workflow conventions that every automated agent (and human contributor!) must follow. Unless a rule explicitly targets a sub‑project, it applies repo‑wide.
⸻
1. GitHub Workflow & Etiquette
- Pull‑request titles must start with the affected workspace in brackets (e.g. [SDK], [Dashboard], [Portal], [Playground]).
- Begin the PR description with a one‑sentence summary, then add a checklist of changes and reference issues with Fixes #123.
- Keep commits small and topical – one logical change per commit.
- Branch names should follow area/brief-topic (e.g. sdk/fix-gas-estimate). Avoid personal names.
- Request at least one core maintainer review. Do not self‑merge unless you are the sole owner of that package.
- All CI checks (type‑check, Biome, tests) must pass before merging.
⸻
2. Formatting & Linting
- Biome governs formatting and linting; its rules live in biome.json.
- Run pnpm biome check --apply before committing.
- Avoid editor‑specific configs; rely on the shared settings.
⸻
3. TypeScript Style Guide
- Write idiomatic TypeScript: explicit function declarations and return types.
- Limit each file to one stateless, single‑responsibility function for clarity and testability.
- Re‑use shared types from @/types or local types.ts barrels.
- Prefer type aliases over interface except for nominal shapes.
- Avoid any and unknown unless unavoidable; narrow generics whenever possible.
- Choose composition over inheritance; leverage utility types (Partial, Pick, etc.).
⸻
4. Testing Strategy
- Co‑locate tests: foo.ts ↔ foo.test.ts.
- Use real function invocations with stub data; avoid brittle mocks.
- For network interactions, use Mock Service Worker (MSW) to intercept fetch/HTTP calls, mocking only scenarios that are hard to reproduce.
- Keep tests deterministic and side‑effect free; Jest is pre‑configured.
⸻
5. packages/thirdweb
5.1 Public API Surface
- Export everything via the exports/ directory, grouped by feature.
- Every public symbol must have comprehensive TSDoc:
- Include at least one @example block that compiles.
- Tag with one custom annotation (@beta, @internal, @experimental, etc.).
- Comment only ambiguous logic; avoid restating TypeScript in prose.
5.2 Performance
- Lazy‑load heavy dependencies inside async paths to keep the initial bundle lean:
`const { jsPDF } = await import("jspdf");`
⸻
6. apps/dashboard & apps/playground
6.1 Core UI Toolkit
- Import primitives from @/components/ui/\_ (e.g. Button, Input, Select, Tabs, Card, Sidebar, Badge, Separator).
- Use NavLink for internal navigation so active states are handled automatically.
- Group feature‑specific components under feature/components/\_ and expose a barrel index.ts when necessary.
6.2 Styling Conventions
- Tailwind CSS is the styling system – no inline styles or CSS modules.
- Merge class names with cn() from @/lib/utils to keep conditional logic readable.
- Stick to design tokens: backgrounds (bg-card), borders (border-border), muted text (text-muted-foreground), etc.
- Expose a className prop on the root element of every component for overrides.
6.3 Component Patterns
- Server Components (run on the Node edge):
-- Read cookies/headers with next/headers.
-- Access server‑only environment variables or secrets.
-- Perform heavy data fetching that should not ship to the client.
-- Implement redirect logic with redirect() from next/navigation.
-- Start files with import "server-only"; to prevent client bundling.
- Client Components (run in the browser):
-- Begin files with 'use client'; before imports.
-- Handle interactive UI relying on React hooks (useState, useEffect, React Query, wallet hooks).
-- Access browser APIs (localStorage, window, IntersectionObserver, etc.).
-- Support fast transitions where data is prefetched on the client.
6.4 Data Fetching Guidelines
- Server Side
-- Always call getAuthToken() to retrieve the JWT from cookies.
-- Inject the token as an Authorization: Bearer header – never embed it in the URL.
-- 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 requirements (default ≥ 60 s).
-- Keep tokens secret by calling internal API routes or server actions.
⸻
7. Performance & Bundle Size
- Track bundle budgets via package.json#size-limit.
- Lazy‑import optional features; avoid top‑level side‑effects.
- De‑duplicate dependencies across packages through pnpm workspace hoisting.
⸻
8. Documentation & Developer Experience
- Each change in packages/\* should contain a changeset for the appropriate package, with the appropriate version bump
- patch for changes that don't impact the public API
- minor for any new/modified public API
- Surface breaking changes prominently in PR descriptions.
- For new UI components, add Storybook stories (\*.stories.tsx) alongside the code.
+18 -18
View File
@@ -71,7 +71,7 @@
"ioredis": "^5.6.1",
"ipaddr.js": "^2.2.0",
"lucide-react": "0.503.0",
"next": "15.3.1",
"next": "15.3.2",
"next-plausible": "^3.12.4",
"next-themes": "^0.4.6",
"nextjs-toploader": "^1.6.12",
@@ -105,30 +105,30 @@
"thirdweb": "workspace:*",
"tiny-invariant": "^1.3.3",
"use-debounce": "^10.0.4",
"zod": "3.24.3"
"zod": "3.25.24"
},
"devDependencies": {
"@chakra-ui/cli": "^2.4.1",
"@chromatic-com/storybook": "3.2.6",
"@next/bundle-analyzer": "15.3.1",
"@next/eslint-plugin-next": "15.3.1",
"@next/bundle-analyzer": "15.3.2",
"@next/eslint-plugin-next": "15.3.2",
"@playwright/test": "1.52.0",
"@storybook/addon-essentials": "8.6.12",
"@storybook/addon-interactions": "8.6.12",
"@storybook/addon-links": "8.6.12",
"@storybook/addon-onboarding": "8.6.12",
"@storybook/addon-viewport": "8.6.12",
"@storybook/blocks": "8.6.12",
"@storybook/nextjs": "8.6.12",
"@storybook/react": "8.6.12",
"@storybook/test": "8.6.12",
"@storybook/addon-essentials": "8.6.14",
"@storybook/addon-interactions": "8.6.14",
"@storybook/addon-links": "8.6.14",
"@storybook/addon-onboarding": "8.6.14",
"@storybook/addon-viewport": "8.6.14",
"@storybook/blocks": "8.6.14",
"@storybook/nextjs": "8.6.14",
"@storybook/react": "8.6.14",
"@storybook/test": "8.6.14",
"@types/color": "4.2.0",
"@types/node": "22.14.1",
"@types/papaparse": "^5.3.15",
"@types/papaparse": "^5.3.16",
"@types/pluralize": "^0.0.33",
"@types/qrcode": "^1.5.5",
"@types/react": "19.1.2",
"@types/react-dom": "19.1.2",
"@types/react": "19.1.4",
"@types/react-dom": "19.1.5",
"@types/react-table": "^7.7.20",
"@types/spdx-correct": "^3.1.3",
"@types/swagger-ui-react": "^5.18.0",
@@ -140,10 +140,10 @@
"eslint-config-biome": "1.9.4",
"eslint-plugin-react-compiler": "19.0.0-beta-e993439-20250405",
"eslint-plugin-storybook": "0.12.0",
"knip": "5.50.5",
"knip": "5.56.0",
"next-sitemap": "^4.2.3",
"postcss": "8.5.3",
"storybook": "8.6.12",
"storybook": "8.6.14",
"tailwindcss": "3.4.17",
"typescript": "5.8.3"
}
@@ -0,0 +1,10 @@
"use server";
import { revalidatePath } from "next/cache";
export async function revalidatePathAction(
path: string,
type: "page" | "layout",
) {
revalidatePath(path, type);
}
+8 -10
View File
@@ -48,15 +48,15 @@ async function fetchAnalytics(
);
}
// client id DEBUG OVERRIDE
// ANALYTICS_SERVICE_URL.searchParams.delete("projectId");
// ANALYTICS_SERVICE_URL.searchParams.delete("teamId");
// ANALYTICS_SERVICE_URL.searchParams.append(
// analyticsServiceUrl.searchParams.delete("projectId");
// analyticsServiceUrl.searchParams.delete("teamId");
// analyticsServiceUrl.searchParams.append(
// "teamId",
// "team_clmb33q9w00gn1x0u2ri8z0k0",
// "team_cm0lde33r02344w129k5hm2xz",
// );
// ANALYTICS_SERVICE_URL.searchParams.append(
// analyticsServiceUrl.searchParams.append(
// "projectId",
// "prj_clyqwud5y00u1na7nzxnzlz7o",
// "prj_cm4rqwx9b002qrnsnr37wqpo6",
// );
return fetch(analyticsServiceUrl, {
@@ -377,7 +377,7 @@ export async function getEcosystemWalletUsage(args: {
export async function getUniversalBridgeUsage(args: {
teamId: string;
projectId: string;
projectId?: string;
from?: Date;
to?: Date;
period?: "day" | "week" | "month" | "year" | "all";
@@ -395,11 +395,10 @@ export async function getUniversalBridgeUsage(args: {
console.error(
`Failed to fetch universal bridge stats: ${res?.status} - ${res.statusText} - ${reason}`,
);
return null;
return [];
}
const json = await res.json();
return json.data as UniversalBridgeStats[];
}
@@ -430,6 +429,5 @@ export async function getUniversalBridgeWalletUsage(args: {
}
const json = await res.json();
return json.data as UniversalBridgeWalletStats[];
}
+2 -2
View File
@@ -84,7 +84,7 @@ export async function getDefaultTeam() {
return null;
}
export async function getLastVisitedTeamOrDefaultTeam() {
export async function getLastVisitedTeam() {
const token = await getAuthToken();
if (!token) {
return null;
@@ -100,5 +100,5 @@ export async function getLastVisitedTeamOrDefaultTeam() {
}
}
return getDefaultTeam();
return null;
}
@@ -0,0 +1,47 @@
import "server-only";
import { DASHBOARD_THIRDWEB_SECRET_KEY } from "@/constants/server-envs";
import { UB_BASE_URL } from "./constants";
type PaymentLink = {
clientId: string;
title?: string;
imageUrl?: string;
receiver: string;
destinationToken: {
address: string;
symbol: string;
decimals: number;
chainId: number;
};
amount: bigint | undefined;
purchaseData: unknown;
};
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,
},
});
if (!res.ok) {
const text = await res.text();
throw new Error(text);
}
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,
purchaseData: data.purchaseData,
} as PaymentLink;
}
@@ -35,14 +35,28 @@ function Variant(props: {
label: string;
selectedChainId?: number;
}) {
const [tokenAddress, setTokenAddress] = useState<string>("");
const [token, setToken] = useState<
| {
address: string;
chainId: number;
}
| undefined
>(undefined);
return (
<BadgeContainer label={props.label}>
<TokenSelector
tokenAddress={tokenAddress}
addNativeTokenIfMissing={false}
showCheck={false}
selectedToken={token}
chainId={props.selectedChainId}
client={storybookThirdwebClient}
onChange={setTokenAddress}
onChange={(v) => {
setToken({
address: v.address,
chainId: v.chainId,
});
}}
/>
</BadgeContainer>
);
@@ -1,9 +1,15 @@
import { useCallback, useMemo } from "react";
import type { ThirdwebClient } from "thirdweb";
import {
NATIVE_TOKEN_ADDRESS,
type ThirdwebClient,
getAddress,
} from "thirdweb";
import { shortenAddress } from "thirdweb/utils";
import { useAllChainsData } from "../../../hooks/chains/allChains";
import { useTokensData } from "../../../hooks/tokens/tokens";
import { replaceIpfsUrl } from "../../../lib/sdk";
import { fallbackChainIcon } from "../../../utils/chain-icons";
import type { TokenMetadata } from "../../api/universal-bridge/tokens";
import { cn } from "../../lib/utils";
import { Badge } from "../ui/badge";
import { Img } from "./Img";
@@ -11,37 +17,87 @@ import { SelectWithSearch } from "./select-with-search";
type Option = { label: string; value: string };
const checksummedNativeTokenAddress = getAddress(NATIVE_TOKEN_ADDRESS);
export function TokenSelector(props: {
tokenAddress: string | undefined;
onChange: (tokenAddress: string) => void;
selectedToken: { chainId: number; address: string } | undefined;
onChange: (token: TokenMetadata) => void;
className?: string;
popoverContentClassName?: string;
chainId?: number;
side?: "left" | "right" | "top" | "bottom";
disableChainId?: boolean;
disableAddress?: boolean;
align?: "center" | "start" | "end";
placeholder?: string;
client: ThirdwebClient;
disabled?: boolean;
enabled?: boolean;
showCheck: boolean;
addNativeTokenIfMissing: boolean;
}) {
const { tokens, isFetching } = useTokensData({
const tokensQuery = useTokensData({
chainId: props.chainId,
enabled: props.enabled,
});
const { idToChain } = useAllChainsData();
const tokens = useMemo(() => {
if (!tokensQuery.data) {
return [];
}
if (props.addNativeTokenIfMissing) {
const hasNativeToken = tokensQuery.data.some(
(token) => token.address === checksummedNativeTokenAddress,
);
if (!hasNativeToken && props.chainId) {
return [
{
name:
idToChain.get(props.chainId)?.nativeCurrency.name ??
"Native Token",
symbol:
idToChain.get(props.chainId)?.nativeCurrency.symbol ?? "ETH",
decimals: 18,
chainId: props.chainId,
address: checksummedNativeTokenAddress,
} satisfies TokenMetadata,
...tokensQuery.data,
];
}
}
return tokensQuery.data;
}, [
tokensQuery.data,
props.chainId,
props.addNativeTokenIfMissing,
idToChain,
]);
const addressChainToToken = useMemo(() => {
const value = new Map<string, TokenMetadata>();
for (const token of tokens) {
value.set(`${token.chainId}:${token.address}`, token);
}
return value;
}, [tokens]);
const options = useMemo(() => {
return tokens.allTokens.map((token) => {
return {
label: token.symbol,
value: `${token.chainId}:${token.address}`,
};
});
}, [tokens.allTokens]);
return (
tokens.map((token) => {
return {
label: token.symbol,
value: `${token.chainId}:${token.address}`,
};
}) || []
);
}, [tokens]);
const searchFn = useCallback(
(option: Option, searchValue: string) => {
const token = tokens.addressChainToToken.get(option.value);
const token = addressChainToToken.get(option.value);
if (!token) {
return false;
}
@@ -55,12 +111,12 @@ export function TokenSelector(props: {
token.address.toLowerCase().includes(searchValue.toLowerCase())
);
},
[tokens],
[addressChainToToken],
);
const renderOption = useCallback(
(option: Option) => {
const token = tokens.addressChainToToken.get(option.value);
const token = addressChainToToken.get(option.value);
if (!token) {
return option.label;
}
@@ -87,8 +143,8 @@ export function TokenSelector(props: {
{token.symbol}
</span>
{!props.disableChainId && (
<Badge variant="outline" className="gap-2 max-sm:hidden">
{!props.disableAddress && (
<Badge variant="outline" className="gap-2 py-1 max-sm:hidden">
<span className="text-muted-foreground">Address</span>
{shortenAddress(token.address, 4)}
</Badge>
@@ -96,27 +152,37 @@ export function TokenSelector(props: {
</div>
);
},
[tokens, props.disableChainId, props.client],
[addressChainToToken, props.disableAddress, props.client],
);
const selectedValue = props.selectedToken
? `${props.selectedToken.chainId}:${props.selectedToken.address}`
: undefined;
return (
<SelectWithSearch
searchPlaceholder="Search by name or symbol"
value={props.tokenAddress}
value={selectedValue}
options={options}
onValueChange={(tokenAddress) => {
props.onChange(tokenAddress);
const token = addressChainToToken.get(tokenAddress);
if (!token) {
return;
}
props.onChange(token);
}}
closeOnSelect={true}
showCheck={false}
showCheck={props.showCheck}
placeholder={
isFetching ? "Loading Tokens..." : props.placeholder || "Select Token"
tokensQuery.isPending
? "Loading Tokens..."
: props.placeholder || "Select Token"
}
overrideSearchFn={searchFn}
renderOption={renderOption}
className={props.className}
popoverContentClassName={props.popoverContentClassName}
disabled={isFetching || props.disabled}
disabled={tokensQuery.isPending || props.disabled}
side={props.side}
align={props.align}
/>
@@ -0,0 +1,75 @@
import type { Meta, StoryObj } from "@storybook/react";
import { ArrowRightIcon, RocketIcon, StarIcon } from "lucide-react";
import { BadgeContainer } from "../../../stories/utils";
import { UpsellBannerCard } from "./UpsellBannerCard";
function Story() {
return (
<div className="container flex max-w-4xl flex-col gap-8 py-10">
<BadgeContainer label="Green with icon (default)">
<UpsellBannerCard
title="Unlock more with thirdweb"
description="Upgrade to increase limits and access advanced features."
cta={{
text: "View plans",
icon: <ArrowRightIcon className="size-4" />,
link: "#",
}}
trackingCategory="storybook"
trackingLabel="green"
icon={<RocketIcon className="size-5" />}
accentColor="green"
/>
</BadgeContainer>
<BadgeContainer label="Blue accent">
<UpsellBannerCard
title="Need more storage?"
description="Add additional space to your account."
cta={{
text: "Upgrade storage",
icon: <ArrowRightIcon className="size-4" />,
link: "#",
}}
trackingCategory="storybook"
trackingLabel="blue"
icon={<StarIcon className="size-5" />}
accentColor="blue"
/>
</BadgeContainer>
<BadgeContainer label="Purple without leading icon">
<UpsellBannerCard
title="Join the beta"
description="Get early access to experimental features."
cta={{
text: "Request access",
icon: <ArrowRightIcon className="size-4" />,
link: "#",
}}
trackingCategory="storybook"
trackingLabel="purple"
accentColor="purple"
/>
</BadgeContainer>
</div>
);
}
const meta = {
title: "blocks/Banners/UpsellBannerCard",
component: Story,
parameters: {
nextjs: {
appDirectory: true,
},
},
} satisfies Meta<typeof Story>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Variants: Story = {
args: {},
};
@@ -0,0 +1,119 @@
"use client";
import { Button } from "@/components/ui/button";
import { TrackedLinkTW } from "@/components/ui/tracked-link";
import { cn } from "@/lib/utils";
import type React from "react";
const ACCENT = {
green: {
border: "border-green-600 dark:border-green-700",
bgFrom: "from-green-50 dark:from-green-900/20",
blur: "bg-green-600",
title: "text-green-900 dark:text-green-200",
desc: "text-green-800 dark:text-green-300",
iconBg: "bg-green-600 text-white",
btn: "bg-green-600 text-white hover:bg-green-700",
},
blue: {
border: "border-blue-600 dark:border-blue-700",
bgFrom: "from-blue-50 dark:from-blue-900/20",
blur: "bg-blue-600",
title: "text-blue-900 dark:text-blue-200",
desc: "text-blue-800 dark:text-blue-300",
iconBg: "bg-blue-600 text-white",
btn: "bg-blue-600 text-white hover:bg-blue-700",
},
purple: {
border: "border-purple-600 dark:border-purple-700",
bgFrom: "from-purple-50 dark:from-purple-900/20",
blur: "bg-purple-600",
title: "text-purple-900 dark:text-purple-200",
desc: "text-purple-800 dark:text-purple-300",
iconBg: "bg-purple-600 text-white",
btn: "bg-purple-600 text-white hover:bg-purple-700",
},
} as const;
type UpsellBannerCardProps = {
title: React.ReactNode;
description: React.ReactNode;
cta: {
text: React.ReactNode;
icon?: React.ReactNode;
link: string;
};
trackingCategory: string;
trackingLabel: string;
accentColor?: keyof typeof ACCENT;
icon?: React.ReactNode;
};
export function UpsellBannerCard(props: UpsellBannerCardProps) {
const color = ACCENT[props.accentColor || "green"];
return (
<div
className={cn(
"relative overflow-hidden rounded-lg border bg-gradient-to-r p-5",
color.border,
color.bgFrom,
)}
>
{/* Decorative blur */}
<div
className={cn(
"-right-10 -top-10 pointer-events-none absolute size-28 rounded-full opacity-20 blur-2xl",
color.blur,
)}
/>
<div className="relative flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<div className="flex items-start gap-3">
{props.icon ? (
<div
className={cn(
"mt-0.5 flex h-9 w-9 shrink-0 items-center justify-center rounded-full",
color.iconBg,
)}
>
{props.icon}
</div>
) : null}
<div>
<h3
className={cn(
"font-semibold text-lg tracking-tight",
color.title,
)}
>
{props.title}
</h3>
<p className={cn("mt-0.5 text-sm", color.desc)}>
{props.description}
</p>
</div>
</div>
<Button
asChild
size="sm"
className={cn(
"mt-2 gap-2 hover:translate-y-0 hover:shadow-inner sm:mt-0",
color.btn,
)}
>
<TrackedLinkTW
href={props.cta.link}
category={props.trackingCategory}
label={props.trackingLabel}
>
{props.cta.text}
{props.cta.icon && <span className="ml-2">{props.cta.icon}</span>}
</TrackedLinkTW>
</Button>
</div>
</div>
);
}
@@ -0,0 +1,71 @@
import { cn } from "@/lib/utils";
export type Segment = {
label: string;
percent: number;
color: string;
};
type DistributionBarChartProps = {
segments: Segment[];
title: string;
};
export function DistributionBarChart(props: DistributionBarChartProps) {
const totalPercentage = props.segments.reduce(
(sum, segment) => sum + segment.percent,
0,
);
const invalidTotalPercentage = totalPercentage !== 100;
return (
<div>
<div className="mb-2 flex items-center justify-between">
<h3 className="font-medium text-sm">{props.title}</h3>
<div
className={cn(
"font-medium text-muted-foreground text-sm",
invalidTotalPercentage && "text-red-500",
)}
>
Total: {totalPercentage}%
</div>
</div>
{/* Bar */}
<div className="flex h-3 overflow-hidden rounded-lg">
{props.segments.map((segment) => {
return (
<div
key={segment.label}
className="flex h-full items-center justify-center transition-all duration-200"
style={{
width: `${segment.percent}%`,
backgroundColor: segment.color,
}}
/>
);
})}
</div>
{/* Legends */}
<div className="mt-3 flex flex-col gap-1 lg:flex-row lg:gap-6">
{props.segments.map((segment) => {
return (
<div key={segment.label} className="flex items-center gap-1.5">
<div
className="size-3 rounded-full"
style={{
backgroundColor: segment.color,
}}
/>
<p className="text-sm">
{segment.label}: {segment.percent}%
</p>
</div>
);
})}
</div>
</div>
);
}
@@ -0,0 +1,66 @@
import type { Meta, StoryObj } from "@storybook/react";
import { MultiStepStatus } from "./multi-step-status";
const meta = {
title: "Blocks/MultiStepStatus",
component: MultiStepStatus,
decorators: [
(Story) => (
<div className="container w-full max-w-md py-10">
<Story />
</div>
),
],
} satisfies Meta<typeof MultiStepStatus>;
export default meta;
type Story = StoryObj<typeof meta>;
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
export const AllStates: Story = {
args: {
steps: [
{
status: "completed",
label: "Connect Wallet",
retryLabel: "Failed to connect wallet",
execute: async () => {
await sleep(1000);
},
},
{
status: "pending",
label: "Sign Message",
retryLabel: "Failed to sign message",
execute: async () => {
await sleep(1000);
},
},
{
status: "error",
label: "Approve Transaction",
retryLabel: "Transaction approval failed",
execute: async () => {
await sleep(1000);
},
},
{
status: "idle",
label: "Confirm Transaction",
retryLabel: "Transaction confirmation failed",
execute: async () => {
await sleep(1000);
},
},
{
status: "idle",
label: "Finalize",
retryLabel: "Finalization failed",
execute: async () => {
await sleep(1000);
},
},
],
},
};
@@ -0,0 +1,72 @@
"use client";
import { Button } from "@/components/ui/button";
import {
AlertCircleIcon,
CircleCheckIcon,
CircleIcon,
RefreshCwIcon,
} from "lucide-react";
import { DynamicHeight } from "../../ui/DynamicHeight";
import { Spinner } from "../../ui/Spinner/Spinner";
export type MultiStepState = {
status: "idle" | "pending" | "completed" | "error";
retryLabel: string;
label: string;
execute: () => Promise<void>;
};
export function MultiStepStatus(props: {
steps: MultiStepState[];
}) {
return (
<DynamicHeight>
<div className="space-y-4">
{props.steps.map((step) => (
<div key={step.label} className="flex items-start space-x-3 ">
{step.status === "completed" ? (
<CircleCheckIcon className="mt-0.5 size-5 flex-shrink-0 text-green-500" />
) : step.status === "pending" ? (
<Spinner className="mt-0.5 size-5 flex-shrink-0 text-foreground" />
) : step.status === "error" ? (
<AlertCircleIcon className="mt-0.5 size-5 flex-shrink-0 text-red-500" />
) : (
<CircleIcon className="mt-0.5 size-5 flex-shrink-0 text-muted-foreground/70" />
)}
<div className="flex-1">
<p
className={`font-medium ${
step.status === "pending"
? "text-foreground"
: step.status === "completed"
? "text-green-500"
: step.status === "error"
? "text-red-500"
: "text-muted-foreground/70"
}`}
>
{step.label}
</p>
{step.status === "error" && (
<div className="mt-1 space-y-2">
<p className="mb-1 text-red-500 text-sm">{step.retryLabel}</p>
<Button
variant="destructive"
size="sm"
className="gap-2"
onClick={() => step.execute()}
>
<RefreshCwIcon className="size-4" />
Retry
</Button>
</div>
)}
</div>
</div>
))}
</div>
</DynamicHeight>
);
}
@@ -42,7 +42,7 @@ export function CopyTextButton(props: {
variant={props.variant || "outline"}
aria-label={props.tooltip}
className={cn(
"flex h-auto w-auto gap-2 rounded-lg px-2.5 py-1.5 font-normal text-foreground",
"flex h-auto w-auto gap-2 rounded-lg px-1.5 py-0.5 font-normal text-foreground",
props.className,
)}
onClick={(e) => {
@@ -12,6 +12,7 @@ export type ChartConfig = {
[k in string]: {
label?: React.ReactNode;
icon?: React.ComponentType;
isCurrency?: boolean;
} & (
| { color?: string; theme?: never }
| { color?: never; theme: Record<keyof typeof THEMES, string> }
@@ -36,14 +36,6 @@ if (API_SERVER_SECRET) {
export const THIRDWEB_ENGINE_URL = process.env.THIRDWEB_ENGINE_URL || "";
if (THIRDWEB_ENGINE_URL) {
experimental_taintUniqueValue(
"Do not pass THIRDWEB_ENGINE_URL to the client",
process,
THIRDWEB_ENGINE_URL,
);
}
export const THIRDWEB_ACCESS_TOKEN = process.env.THIRDWEB_ACCESS_TOKEN || "";
if (THIRDWEB_ACCESS_TOKEN) {
@@ -3,6 +3,7 @@ import {
NEXT_PUBLIC_IPFS_GATEWAY_URL,
} from "@/constants/public-envs";
import {
THIRDWEB_BRIDGE_URL,
THIRDWEB_BUNDLER_DOMAIN,
THIRDWEB_INAPP_WALLET_DOMAIN,
THIRDWEB_INSIGHT_API_DOMAIN,
@@ -35,6 +36,7 @@ export function getConfiguredThirdwebClient(options: {
social: THIRDWEB_SOCIAL_API_DOMAIN,
bundler: THIRDWEB_BUNDLER_DOMAIN,
insight: THIRDWEB_INSIGHT_API_DOMAIN,
bridge: THIRDWEB_BRIDGE_URL,
});
}
+5 -4
View File
@@ -39,9 +39,10 @@ export function getFiltersFromSearchParams(params: {
};
const defaultInterval =
differenceInCalendarDays(range.to, range.from) > 30
? "week"
: ("day" as const);
params.interval ??
(differenceInCalendarDays(range.to, range.from) > 30
? ("week" as const)
: ("day" as const));
return {
range,
@@ -50,6 +51,6 @@ export function getFiltersFromSearchParams(params: {
? ("day" as const)
: params.interval === "week"
? ("week" as const)
: defaultInterval,
: (defaultInterval as "day" | "week"),
};
}
@@ -32,7 +32,6 @@ export type Account = {
image?: string | null;
name?: string;
email?: string;
advancedEnabled: boolean;
emailConfirmedAt?: string;
unconfirmedEmail?: string;
emailConfirmationWalletAddress?: string;
@@ -41,7 +40,6 @@ export type Account = {
billing: "email" | "none";
updates: "email" | "none";
};
// TODO - add image URL
};
interface UpdateAccountNotificationsInput {
@@ -53,27 +51,6 @@ interface ConfirmEmailInput {
confirmationToken: string;
}
interface UsageStorage {
sumFileSizeBytes: number;
}
export interface UsageBillableByService {
usage: {
storage: UsageStorage;
};
limits: {
storage: number;
};
rateLimits: {
storage: number;
rpc: number;
};
rateLimitedAt: {
storage?: string;
rpc?: string;
};
}
interface BillingProduct {
name: string;
id: string;
@@ -345,13 +322,14 @@ export type RotateSecretKeyAPIReturnType = {
};
};
export async function rotateSecretKeyClient(projectId: string) {
export async function rotateSecretKeyClient(params: {
teamId: string;
projectId: string;
}) {
const res = await apiServerProxy<RotateSecretKeyAPIReturnType>({
pathname: "/v2/keys/rotate-secret-key",
pathname: `/v1/teams/${params.teamId}/projects/${params.projectId}/rotate-secret-key`,
method: "POST",
body: JSON.stringify({
projectId,
}),
body: JSON.stringify({}),
headers: {
"Content-Type": "application/json",
},
@@ -11,8 +11,8 @@ import type { EngineBackendWalletType } from "lib/engine";
import { useState } from "react";
import { useActiveAccount } from "thirdweb/react";
import invariant from "tiny-invariant";
import type { ResultItem } from "../../../app/(app)/team/[team_slug]/[project_slug]/engine/dedicated/(instance)/[engineId]/metrics/components/StatusCodes";
import type { EngineStatus } from "../../../app/(app)/team/[team_slug]/[project_slug]/engine/dedicated/(instance)/[engineId]/overview/components/transactions-table";
import type { ResultItem } from "../../../app/(app)/team/[team_slug]/[project_slug]/(sidebar)/engine/dedicated/(instance)/[engineId]/metrics/components/StatusCodes";
import type { EngineStatus } from "../../../app/(app)/team/[team_slug]/[project_slug]/(sidebar)/engine/dedicated/(instance)/[engineId]/overview/components/transactions-table";
import { engineKeys } from "../cache-keys";
// Engine instances
@@ -1,8 +1,9 @@
import { ArrowUpRightIcon } from "lucide-react";
import type { Metadata } from "next";
import { getClientThirdwebClient } from "../../../../../@/constants/thirdweb-client.client";
import { UniversalBridgeEmbed } from "./components/client/UniversalBridgeEmbed";
const title = "Universal Bridge: Swap, Bridge, and On-Ramp";
const title = "Universal Bridge: Swap, Bridge, and Onramp";
const description =
"Swap, bridge, and on-ramp to any EVM chain with thirdweb's Universal Bridge.";
@@ -35,6 +36,34 @@ export default async function RoutesPage({
src="/assets/login/background.svg"
className="-bottom-12 -right-12 pointer-events-none absolute lg:right-0 lg:bottom-0"
/>
<div className="absolute inset-x-0 bottom-24 z-20">
<div className="container mx-auto px-4">
<div className="relative overflow-hidden rounded-lg border-2 border-green-500/20 bg-gradient-to-br from-card/80 to-card/50 p-4 shadow-[inset_0_1px_2px_0_rgba(0,0,0,0.02)]">
<div className="absolute inset-0 bg-gradient-to-br from-green-500/5 to-transparent" />
<div className="relative flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
<div className="flex flex-col gap-1">
<h3 className="font-medium text-lg">
Get Started with Universal Bridge
</h3>
<p className="text-muted-foreground text-sm">
Simple, instant, and secure payments across any token and
chain.
</p>
</div>
<a
href="https://portal.thirdweb.com/pay"
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-2 rounded-md bg-green-600 px-4 py-2 font-medium text-sm text-white transition-all hover:bg-green-600/90 hover:shadow-sm"
>
Learn More
<ArrowUpRightIcon className="size-4" />
</a>
</div>
</div>
</div>
</div>
</div>
);
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 536 KiB

@@ -47,7 +47,7 @@ export const SearchInput: React.FC = () => {
<div className="group relative w-full">
<SearchIcon className="-translate-y-1/2 absolute top-[50%] left-3 size-4 text-muted-foreground" />
<Input
placeholder="Search with anything!"
placeholder="Search by token name or symbol"
className="h-10 rounded-lg bg-card py-2 pl-9 lg:min-w-[300px]"
defaultValue={searchParams?.get("query") || ""}
onChange={(e) => handleSearch(e.target.value)}
@@ -52,20 +52,20 @@ export async function RouteListRow({
]);
return (
<TableRow linkBox className="hover:bg-accent/50">
<TableRow linkBox className="group transition-colors hover:bg-accent/50">
<TableCell>
<div className="flex flex-row items-center gap-4">
<div className="flex items-center gap-1">
<div className="flex flex-row items-center gap-3">
<div className="flex items-center gap-2">
{resolvedOriginTokenIconUri ? (
// For now we're using a normal img tag because the domain for these images is unknown
// eslint-disable-next-line @next/next/no-img-element
<img
src={resolvedOriginTokenIconUri}
alt={originTokenAddress}
className="size-6 rounded-full border border-muted-foreground"
className="size-7 rounded-full border border-border/50 shadow-sm transition-transform group-hover:scale-105"
/>
) : (
<div className="size-6 rounded-full bg-muted-foreground" />
<div className="size-7 rounded-full bg-muted-foreground/20" />
)}
{originTokenSymbol && (
<CopyTextButton
@@ -76,7 +76,7 @@ export async function RouteListRow({
: originTokenSymbol
}
tooltip="Copy Token Address"
className="relative z-10 text-base"
className="relative z-10 font-medium text-base"
variant="ghost"
copyIconPosition="right"
/>
@@ -85,22 +85,22 @@ export async function RouteListRow({
</div>
</TableCell>
<TableCell className="text-muted-foreground">
{originChain.name}
<TableCell className="text-muted-foreground/90">
<span className="font-medium">{originChain.name}</span>
</TableCell>
<TableCell>
<div className="flex flex-row items-center gap-4">
<div className="flex items-center gap-1">
<div className="flex flex-row items-center gap-3">
<div className="flex items-center gap-2">
{resolvedDestinationTokenIconUri ? (
// eslint-disable-next-line @next/next/no-img-element
<img
src={resolvedDestinationTokenIconUri}
alt={destinationTokenAddress}
className="size-6 rounded-full border border-muted-foreground"
className="size-7 rounded-full border border-border/50 shadow-sm transition-transform group-hover:scale-105"
/>
) : (
<div className="size-6 rounded-full bg-muted-foreground" />
<div className="size-7 rounded-full bg-muted-foreground/20" />
)}
{destinationTokenSymbol && (
<CopyTextButton
@@ -111,7 +111,7 @@ export async function RouteListRow({
: destinationTokenSymbol
}
tooltip="Copy Token Address"
className="relative z-10 text-base"
className="relative z-10 font-medium text-base"
variant="ghost"
copyIconPosition="right"
/>
@@ -120,8 +120,8 @@ export async function RouteListRow({
</div>
</TableCell>
<TableCell className="text-muted-foreground">
{destinationChain.name}
<TableCell className="text-muted-foreground/90">
<span className="font-medium">{destinationChain.name}</span>
</TableCell>
</TableRow>
);
@@ -7,7 +7,6 @@ import {
TableRow,
} from "@/components/ui/table";
import type { Address } from "thirdweb";
import { checksumAddress } from "thirdweb/utils";
import { getRoutes } from "../../../utils";
import { ChainlistPagination } from "../client/pagination";
import { RouteListCard } from "./routelist-card";
@@ -25,18 +24,15 @@ export type SearchParams = Partial<{
// 120 is divisible by 2, 3, and 4 so card layout looks nice
const DEFAULT_PAGE_SIZE = 120;
const DEFAULT_PAGE = 1;
async function getRoutesToRender(params: SearchParams) {
const filters: Partial<{
limit: number;
offset: number;
originQuery?: string;
destinationQuery?: string;
originChainId?: number;
originTokenAddress?: Address;
destinationChainId?: number;
destinationTokenAddress?: Address;
originTextQuery?: string;
destinationTextQuery?: string;
}> = {};
if (params.type === "origin" || typeof params.type === "undefined") {
@@ -45,7 +41,7 @@ async function getRoutesToRender(params: SearchParams) {
} else if (Number.isInteger(Number(params.query))) {
filters.originChainId = Number(params.query);
} else if (params.query) {
filters.originTextQuery = params.query;
filters.originQuery = params.query;
}
} else if (params.type === "destination") {
if (params.query?.startsWith("0x")) {
@@ -53,69 +49,20 @@ async function getRoutesToRender(params: SearchParams) {
} else if (Number.isInteger(Number(params.query))) {
filters.destinationChainId = Number(params.query);
} else if (params.query) {
filters.destinationTextQuery = params.query;
filters.destinationQuery = params.query;
}
}
// Temporary, will update this after the /routes endpoint
let routes = await getRoutes({ limit: 100_000 });
const totalCount = routes.length;
if (filters.originChainId) {
routes = routes.filter(
(route) => route.originToken.chainId === filters.originChainId,
);
}
if (filters.originTokenAddress) {
const originTokenAddress = filters.originTokenAddress;
routes = routes.filter(
(route) =>
checksumAddress(route.originToken.address) ===
checksumAddress(originTokenAddress),
);
}
if (filters.destinationChainId) {
routes = routes.filter(
(route) => route.destinationToken.chainId === filters.destinationChainId,
);
}
if (filters.destinationTokenAddress) {
const destinationTokenAddress = filters.destinationTokenAddress;
routes = routes.filter(
(route) =>
checksumAddress(route.destinationToken.address) ===
checksumAddress(destinationTokenAddress),
);
}
if (filters.originTextQuery) {
const originTextQuery = filters.originTextQuery.toLowerCase();
routes = routes.filter((route) => {
return (
route.originToken.name.toLowerCase().includes(originTextQuery) ||
route.originToken.symbol.toLowerCase().includes(originTextQuery)
);
});
}
if (filters.destinationTextQuery) {
const destinationTextQuery = filters.destinationTextQuery.toLowerCase();
routes = routes.filter((route) => {
return (
route.destinationToken.name
.toLowerCase()
.includes(destinationTextQuery) ||
route.destinationToken.symbol
.toLowerCase()
.includes(destinationTextQuery)
);
});
}
const routes = await getRoutes({
limit: DEFAULT_PAGE_SIZE,
offset: DEFAULT_PAGE_SIZE * ((params.page || 1) - 1),
originQuery: filters.originQuery,
destinationQuery: filters.destinationQuery,
});
return {
routesToRender: routes,
totalCount,
filteredCount: routes.length,
routesToRender: routes.data,
totalCount: routes.meta.totalCount,
filteredCount: routes.meta.filteredCount,
};
}
@@ -128,36 +75,37 @@ export async function RoutesData(props: {
props.searchParams,
);
// pagination
const totalPages = Math.ceil(routesToRender.length / DEFAULT_PAGE_SIZE);
const activePage = Number(props.searchParams.page || DEFAULT_PAGE);
const pageSize = DEFAULT_PAGE_SIZE;
const startIndex = (activePage - 1) * pageSize;
const endIndex = startIndex + pageSize;
const paginatedRoutes = routesToRender.slice(startIndex, endIndex);
const totalPages = Math.ceil(filteredCount / DEFAULT_PAGE_SIZE);
return (
<>
<main>
{/* empty state */}
{paginatedRoutes.length === 0 ? (
{routesToRender.length === 0 ? (
<div className="flex h-[300px] items-center justify-center rounded-lg border p-8 lg:h-[500px]">
<p className="text-2xl">No Results found</p>
</div>
) : props.activeView === "table" ? (
<TableContainer>
<TableContainer className="overflow-hidden rounded-xl border border-border/50 bg-card/50 shadow-sm transition-all">
<Table>
<TableHeader className="z-0">
<TableRow>
<TableHead>Origin Token</TableHead>
<TableHead>Origin Chain</TableHead>
<TableHead>Destination Token</TableHead>
<TableHead>Destination Chain</TableHead>
<TableRow className="border-border/50 border-b bg-muted/50">
<TableHead className="py-4 font-medium text-muted-foreground/80 text-xs uppercase tracking-wider">
Origin Token
</TableHead>
<TableHead className="py-4 font-medium text-muted-foreground/80 text-xs uppercase tracking-wider">
Origin Chain
</TableHead>
<TableHead className="py-4 font-medium text-muted-foreground/80 text-xs uppercase tracking-wider">
Destination Token
</TableHead>
<TableHead className="py-4 font-medium text-muted-foreground/80 text-xs uppercase tracking-wider">
Destination Chain
</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{paginatedRoutes.map((route) => (
{routesToRender.map((route) => (
<RouteListRow
key={`${route.originToken.chainId}:${route.originToken.address}-${route.destinationToken.chainId}:${route.destinationToken.address}`}
originChainId={route.originToken.chainId}
@@ -177,7 +125,7 @@ export async function RoutesData(props: {
</TableContainer>
) : (
<ul className="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">
{paginatedRoutes.map((route) => (
{routesToRender.map((route) => (
<li
key={`${route.originToken.chainId}:${route.originToken.address}-${route.destinationToken.chainId}:${route.destinationToken.address}`}
className="h-full"
@@ -201,12 +149,15 @@ export async function RoutesData(props: {
</main>
<div className="h-10" />
{totalPages > 1 && (
<ChainlistPagination totalPages={totalPages} activePage={activePage} />
<ChainlistPagination
totalPages={totalPages}
activePage={Number(props.searchParams.page) || 1}
/>
)}
<div className="h-4" />
<p className="text-balance text-center text-muted-foreground text-sm">
Showing{" "}
<span className="text-accent-foreground">{paginatedRoutes.length}</span>{" "}
<span className="text-accent-foreground">{routesToRender.length}</span>{" "}
out of{" "}
{filteredCount !== totalCount ? (
<>
@@ -1,3 +1,4 @@
import { ArrowUpRightIcon } from "lucide-react";
import type { Metadata } from "next";
import { headers } from "next/headers";
import { getAuthToken } from "../../../api/lib/getAuthToken";
@@ -9,7 +10,7 @@ import {
type SearchParams,
} from "./components/server/routes-table";
const title = "Routes: Swap, Bridge, and On-Ramp";
const title = "Universal Bridge Routes: Swap, Bridge, and Onramp";
const description =
"A list of token routes for swapping, bridging, and on-ramping between EVM chains with thirdweb.";
@@ -43,15 +44,15 @@ export default async function RoutesPage(props: {
return (
<section className="container mx-auto flex h-full flex-col px-4 py-10">
<header className="flex flex-col gap-4">
<div className="flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between">
<div className="flex flex-row items-center justify-between gap-4 lg:flex-col lg:justify-start">
<header className="flex flex-col gap-6">
<div className="flex flex-col gap-6 lg:flex-row lg:items-center lg:justify-between">
<div className="flex flex-col gap-2">
<h1 className="font-semibold text-4xl tracking-tighter lg:text-5xl">
Routes
</h1>
</div>
<div className="flex flex-row items-end gap-4 lg:flex-col ">
<div className="flex w-full flex-row gap-4">
<div className="flex flex-row items-end gap-4 lg:flex-col">
<div className="flex w-full flex-row items-center gap-4">
<SearchInput />
<QueryType activeType={activeType} />
<RouteListView activeView={activeView} />
@@ -60,6 +61,29 @@ export default async function RoutesPage(props: {
</div>
</header>
<div className="h-10" />
<div className="relative overflow-hidden rounded-lg border-2 border-green-500/20 bg-gradient-to-br from-card/80 to-card/50 p-4 shadow-[inset_0_1px_2px_0_rgba(0,0,0,0.02)]">
<div className="absolute inset-0 bg-gradient-to-br from-green-500/5 to-transparent" />
<div className="relative flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
<div className="flex flex-col gap-1">
<h3 className="font-medium text-lg">
Get Started with Universal Bridge
</h3>
<p className="text-muted-foreground text-sm">
Simple, instant, and secure payments across any token and chain.
</p>
</div>
<a
href="https://portal.thirdweb.com/pay"
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-2 rounded-md bg-green-600 px-4 py-2 font-medium text-sm text-white transition-all hover:bg-green-600/90 hover:shadow-sm"
>
Learn More
<ArrowUpRightIcon className="size-4" />
</a>
</div>
</div>
<div className="h-10" />
<RoutesData
searchParams={searchParams}
activeView={activeView}
@@ -2,35 +2,69 @@ import "server-only";
import { NEXT_PUBLIC_THIRDWEB_BRIDGE_HOST } from "@/constants/public-envs";
import { DASHBOARD_THIRDWEB_SECRET_KEY } from "@/constants/server-envs";
import { getAuthToken } from "app/(app)/api/lib/getAuthToken";
import type { Address } from "thirdweb";
import type { Route } from "./types/route";
export async function getRoutes({
limit,
offset,
originQuery,
destinationQuery,
originChainId,
destinationChainId,
originTokenAddress,
destinationTokenAddress,
}: {
limit?: number;
offset?: number;
originQuery?: string;
destinationQuery?: string;
originChainId?: number;
destinationChainId?: number;
originTokenAddress?: Address;
destinationTokenAddress?: Address;
} = {}) {
const url = new URL(`${NEXT_PUBLIC_THIRDWEB_BRIDGE_HOST}/v1/routes`);
if (limit) {
url.searchParams.set("limit", limit.toString());
}
if (offset) {
url.searchParams.set("offset", offset.toString());
}
if (originQuery) {
url.searchParams.set("originQuery", originQuery);
}
if (destinationQuery) {
url.searchParams.set("destinationQuery", destinationQuery);
}
if (originChainId) {
url.searchParams.set("originChainId", originChainId.toString());
}
if (destinationChainId) {
url.searchParams.set("destinationChainId", destinationChainId.toString());
}
if (originTokenAddress) {
url.searchParams.set("originTokenAddress", originTokenAddress);
}
if (destinationTokenAddress) {
url.searchParams.set("destinationTokenAddress", destinationTokenAddress);
}
url.searchParams.set("sortBy", "popularity");
// It's faster to filter client side, doesn't seem to be a big performance boost to paginate/filter server side
const token = await getAuthToken();
const routesResponse = await fetch(url, {
headers: token
? {
authorization: `Bearer ${token}`,
}
: {
"x-secret-key": DASHBOARD_THIRDWEB_SECRET_KEY,
},
headers: {
"x-secret-key": DASHBOARD_THIRDWEB_SECRET_KEY,
},
next: { revalidate: 60 * 60 },
});
if (!routesResponse.ok) {
throw new Error("Failed to fetch routes");
}
const routes: { data: Route[] } = await routesResponse.json();
const routes: {
data: Route[];
meta: { totalCount: number; filteredCount: number };
} = await routesResponse.json();
return routes.data;
return routes;
}
@@ -32,8 +32,8 @@ function useChainStatswithRPC(_rpcUrl: string) {
method: "POST",
body: JSON.stringify({
jsonrpc: "2.0",
method: "eth_blockNumber",
params: [],
method: "eth_getBlockByNumber",
params: ["latest", false],
id: 1,
}),
});
@@ -41,9 +41,12 @@ function useChainStatswithRPC(_rpcUrl: string) {
const json = await res.json();
const latency = (performance.now() - startTimeStamp).toFixed(0);
const blockNumber = Number.parseInt(json.result.number, 16);
const blockGasLimit = Number.parseInt(json.result.gasLimit, 16);
return {
latency,
blockNumber: Number.parseInt(json.result, 16),
blockNumber,
blockGasLimit,
};
},
refetchInterval: (query) => {
@@ -115,6 +118,21 @@ export function ChainLiveStats(props: { rpc: string }) {
</div>
)}
</PrimaryInfoItem>
{/* Block Gas Limit */}
<PrimaryInfoItem title="Block Gas Limit" titleIcon={<PulseDot />}>
{stats.isError ? (
<p className="fade-in-0 animate-in text-destructive-text">N/A</p>
) : stats.data ? (
<p className="fade-in-0 animate-in">
{stats.data.blockGasLimit ?? "N/A"}
</p>
) : (
<div className="flex h-[28px] w-[140px] py-1">
<Skeleton className="h-full w-full" />
</div>
)}
</PrimaryInfoItem>
</>
);
}
@@ -126,8 +126,10 @@ const getClaimConditionTypeFromPhase = (
if (!phase.snapshot) {
return "public";
}
if (phase.snapshot) {
if (
phase.price === "0" &&
typeof phase.snapshot !== "string" &&
phase.snapshot.length === 1 &&
phase.snapshot.some(
@@ -25,7 +25,7 @@ import { usePathname } from "next/navigation";
import { useState } from "react";
import { toast } from "sonner";
import type { Chain, ThirdwebClient } from "thirdweb";
import { useAddContractToProject } from "../../../../../team/[team_slug]/[project_slug]/hooks/project-contracts";
import { useAddContractToProject } from "../../../../../team/[team_slug]/[project_slug]/(sidebar)/hooks/project-contracts";
const TRACKING_CATEGORY = "add_to_dashboard_upsell";
@@ -147,6 +147,8 @@ function AddToProjectModalContent(props: {
teamId: params.teamId,
projectId: params.projectId,
chainId: props.chainId,
deploymentType: undefined,
contractType: undefined,
},
{
onSuccess: () => {
@@ -8,6 +8,7 @@ import { getContractMetadata } from "thirdweb/extensions/common";
import { isAddress, isContractDeployed } from "thirdweb/utils";
import { shortenIfAddress } from "utils/usedapp-external";
import { NebulaChatButton } from "../../../../../nebula-app/(app)/components/FloatingChat/FloatingChat";
import { examplePrompts } from "../../../../../nebula-app/(app)/data/examplePrompts";
import {
getAuthTokenWalletAddress,
getUserThirdwebClient,
@@ -94,14 +95,6 @@ Users may be considering integrating the contract into their applications. Discu
The following is the user's message:`;
const examplePrompts: string[] = [
"What does this contract do?",
"What permissions or roles exist in this contract?",
"Which functions are used the most?",
"Has this contract been used recently?",
"Who are the largest holders/users of this?",
];
return (
<ContractPageLayout
chainMetadata={chainMetadata}
@@ -124,10 +117,7 @@ The following is the user's message:`;
chainIds: [chainId],
wallet: accountAddress ?? undefined,
}}
examplePrompts={examplePrompts.map((prompt) => ({
title: prompt,
message: prompt,
}))}
examplePrompts={examplePrompts}
/>
{props.children}
</ContractPageLayout>
@@ -14,7 +14,7 @@ export function TokenDetailsCardUI(props: {
</h2>
<div className="flex flex-col gap-5 p-6 lg:flex-row">
<Stat
label="Total Supply"
label="Circulating Supply"
isPending={!tokenSupply}
value={
tokenSupply
Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 123 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

@@ -8,6 +8,8 @@ import alephZeroBaner from "./temp-assets/AlephZeroBanner.jpg";
import alephZeroCTA from "./temp-assets/AlephZeroCTA.jpg";
import cotiBanner from "./temp-assets/COTI_Banner.jpg";
import cotiCTA from "./temp-assets/COTI_CTA.jpg";
import eduBanner from "./temp-assets/EDUBanner.jpeg";
import eduCTA from "./temp-assets/EDUCTA.png";
import hemiSepoliaBanner from "./temp-assets/HemiBanner.png";
import hemiSepoliaCTA from "./temp-assets/HemiCTA.png";
import ancient8Banner from "./temp-assets/ancient8-banner.png";
@@ -41,6 +43,8 @@ import creatorCTA from "./temp-assets/creatorCTA.png";
import lumiaBanner from "./temp-assets/lumiaBanner.png";
import somniaBanner from "./temp-assets/somniaBanner.png";
import NeoXBanner from "./temp-assets/NeoXBanner.jpg";
import NeoXCTA from "./temp-assets/NeoXCTA.jpg";
import flowBanner from "./temp-assets/flowBanner.png";
import flowCTA from "./temp-assets/flowCTA.png";
import mantleBanner from "./temp-assets/mantle.png";
@@ -374,6 +378,31 @@ const chainMetaRecord = {
about:
"Monetize your passion, your way. Discover the freedom to grow with our creator-focused platform.",
},
//EDUMainnet
41923: {
headerImgUrl: eduBanner.src,
about:
"Education On-Chain - A Layer 3 blockchain preparing students and learners for the future of work while advancing onchain education finance (EduFi).",
cta: {
backgroundImageUrl: eduCTA.src,
title: "Education On-Chain",
buttonLink: "https://educhain.xyz/",
buttonText: "Learn More",
},
},
//EDU Testnet <Open Campus Codex>
656476: {
headerImgUrl: eduBanner.src,
about:
"Education On-Chain - A Layer 3 blockchain preparing students and learners for the future of work while advancing onchain education finance (EduFi).",
cta: {
backgroundImageUrl: eduCTA.src,
title: "Education On-Chain",
buttonLink: "https://educhain.xyz/",
buttonText: "Learn More",
},
},
//Etherlink Testnet
128123: {
headerImgUrl: etherlinkBanner.src,
@@ -545,6 +574,30 @@ const chainMetaRecord = {
about:
"Metal L2 is a banking-focused Layer 2 blockchain built on the Optimism Superchain, designed to enable direct on-chain fiat deposits through its connection to The Digital Banking Network—an open-source blockchain banking protocol developed by Metallicus",
},
//NEO X
47763: {
headerImgUrl: NeoXBanner.src,
about:
"Neo X is an EVM-compatible sidechain built by Neo, incorporating Neo's distinctive dBFT consensus mechanism. Serving as a bridge between Neo N3 and the widely used EVM network, Neo X will play a crucial role in expanding the Neo ecosystem and offering developers more opportunities for innovation.",
cta: {
backgroundImageUrl: NeoXCTA.src,
title: "Learn more about Neo X",
buttonLink: "https://x.neo.org/",
buttonText: "Learn More",
},
},
//NEO X Testnet
12227332: {
headerImgUrl: NeoXBanner.src,
about:
"Neo X is an EVM-compatible sidechain built by Neo, incorporating Neo's distinctive dBFT consensus mechanism. Serving as a bridge between Neo N3 and the widely used EVM network, Neo X will play a crucial role in expanding the Neo ecosystem and offering developers more opportunities for innovation.",
cta: {
backgroundImageUrl: NeoXCTA.src,
title: "Learn more about Neo X",
buttonLink: "https://x.neo.org/",
buttonText: "Learn More",
},
},
//0G-Newton-Testnet
16600: {
headerImgUrl: zeroGBanner.src,
@@ -1,6 +1,5 @@
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { getClientThirdwebClient } from "@/constants/thirdweb-client.client";
import { BookOpenIcon, ChevronRightIcon } from "lucide-react";
import type { Metadata } from "next";
import Image from "next/image";
@@ -10,7 +9,8 @@ import contractsIcon from "../../../../../public/assets/support/contracts.png";
import engineIcon from "../../../../../public/assets/support/engine.png";
import miscIcon from "../../../../../public/assets/support/misc.svg";
import connectIcon from "../../../../../public/assets/support/wallets.png";
import { NebulaChatButton } from "../../../nebula-app/(app)/components/FloatingChat/FloatingChat";
import { getTeams } from "../../../../@/api/team";
import { CustomChatButton } from "../../../nebula-app/(app)/components/CustomChat/CustomChatButton";
import {
getAuthToken,
getAuthTokenWalletAddress,
@@ -118,25 +118,22 @@ const HELP_PRODUCTS = [
},
] as const;
export const siwaExamplePrompts = [
"How do I add in-app wallet with sign in with google to my react app?",
"How do I send a transaction in Unity?",
"What does this contract revert error mean?",
"I see thirdweb support id in my console log, can you help me?",
"Here is my code, can you tell me why I'm seeing this error?",
];
export default async function SupportPage() {
const [authToken, accountAddress] = await Promise.all([
getAuthToken(),
getAuthTokenWalletAddress(),
]);
const client = getClientThirdwebClient({
jwt: authToken,
teamId: undefined,
});
const supportPromptPrefix =
"You are a Customer Success Agent at thirdweb, assisting customers with blockchain and Web3-related issues. Use the following details to craft a professional, empathetic response: ";
const examplePrompts = [
"ERC20 - Transfer Amount Exceeds Allowance",
"Replacement transaction underpriced / Replacement fee too low",
"Nonce too low: next nonce #, tx nonce #",
"Nonce too high",
];
const teams = await getTeams();
const teamId = teams?.[0]?.id ?? undefined;
return (
<main className="flex flex-col gap-12 pb-12">
@@ -157,22 +154,16 @@ export default async function SupportPage() {
team.
</p>
<div className="mt-6 flex w-full flex-col items-center gap-3">
<NebulaChatButton
<CustomChatButton
isLoggedIn={!!accountAddress}
networks="all"
isFloating={false}
pageType="support"
label="Ask Nebula AI for support"
client={client}
nebulaParams={{
messagePrefix: supportPromptPrefix,
chainIds: [],
wallet: accountAddress ?? undefined,
}}
examplePrompts={examplePrompts.map((prompt) => ({
title: prompt,
message: prompt,
}))}
label="Ask AI for support"
examplePrompts={siwaExamplePrompts}
authToken={authToken || undefined}
teamId={teamId}
clientId={undefined}
/>
<Link
@@ -26,6 +26,7 @@ export function DeployedContractsPageHeader(props: {
}}
teamId={props.teamId}
projectId={props.projectId}
type="contract"
/>
<div className="container flex max-w-7xl flex-col gap-3 py-10 lg:flex-row lg:items-center lg:justify-between">
@@ -19,6 +19,7 @@ export function DeployViaCLIOrImportCard(props: {
return (
<div className="rounded-lg border bg-card p-4 lg:p-6">
<ImportModal
type="contract"
client={client}
isOpen={importModalOpen}
onClose={() => {
@@ -44,11 +44,13 @@ async function DeployedContractsPageAsync(props: {
teamId: props.teamId,
projectId: props.projectId,
authToken: props.authToken,
deploymentType: undefined,
});
return (
<ClientOnly ssr={<Loading />}>
<ContractTable
variant="contract"
contracts={deployedContracts}
pageSize={10}
teamId={props.teamId}
@@ -7,21 +7,28 @@ export type ProjectContract = {
chainId: string;
createdAt: string;
updatedAt: string;
deploymentType: string | null;
contractType: string | null;
};
export async function getProjectContracts(options: {
teamId: string;
projectId: string;
authToken: string;
deploymentType: string | undefined;
}) {
const res = await fetch(
const url = new URL(
`${NEXT_PUBLIC_THIRDWEB_API_HOST}/v1/teams/${options.teamId}/projects/${options.projectId}/contracts`,
{
headers: {
Authorization: `Bearer ${options.authToken}`,
},
},
);
if (options.deploymentType) {
url.searchParams.set("deploymentType", options.deploymentType);
}
const res = await fetch(url, {
headers: {
Authorization: `Bearer ${options.authToken}`,
},
});
if (!res.ok) {
const errorMessage = await res.text();
@@ -9,11 +9,13 @@ export async function getSortedDeployedContracts(params: {
teamId: string;
projectId: string;
authToken: string;
deploymentType: string | undefined;
}) {
const contracts = await getProjectContracts({
teamId: params.teamId,
projectId: params.projectId,
authToken: params.authToken,
deploymentType: params.deploymentType,
});
const chainIds = Array.from(new Set(contracts.map((c) => c.chainId)));
@@ -1,22 +1,18 @@
"use client";
import { ChakraProviderSetup } from "@/components/ChakraProviderSetup";
import { useAuthorizedWallets } from "@3rdweb-sdk/react/hooks/useApi";
import { AuthorizedWalletsTable } from "components/settings/AuthorizedWallets/AuthorizedWalletsTable";
// TODO - remove ChakraProviderSetup after migrating AuthorizedWalletsTable
// TODO - fetch the authorized wallets server side
export function AccountDevicesPage() {
const authorizedWalletsQuery = useAuthorizedWallets();
return (
<ChakraProviderSetup>
<AuthorizedWalletsTable
authorizedWallets={authorizedWalletsQuery.data || []}
isPending={authorizedWalletsQuery.isPending}
isFetched={authorizedWalletsQuery.isFetched}
/>
</ChakraProviderSetup>
<AuthorizedWalletsTable
authorizedWallets={authorizedWalletsQuery.data || []}
isPending={authorizedWalletsQuery.isPending}
isFetched={authorizedWalletsQuery.isFetched}
/>
);
}
@@ -116,7 +116,7 @@ function TeamRow(props: {
<div>
<div className="flex items-center gap-3">
<p className="font-semibold text-sm">{props.team.name}</p>
<TeamPlanBadge plan={plan} />
<TeamPlanBadge plan={plan} teamSlug={props.team.slug} />
</div>
<p className="text-muted-foreground text-sm capitalize">
{props.role.toLowerCase()}
@@ -1,6 +1,10 @@
"use client";
import type { Team } from "@/api/team";
import { Badge, type BadgeProps } from "@/components/ui/badge";
import { cn } from "@/lib/utils";
import Link from "next/link";
import { useTrack } from "../../../hooks/analytics/useTrack";
const teamPlanToBadgeVariant: Record<
Team["billingPlan"],
@@ -31,11 +35,12 @@ export function getTeamPlanBadgeLabel(plan: Team["billingPlan"]) {
}
export function TeamPlanBadge(props: {
teamSlug: string;
plan: Team["billingPlan"];
className?: string;
postfix?: string;
}) {
return (
const badge = (
<Badge
variant={teamPlanToBadgeVariant[props.plan]}
className={cn("px-1.5 capitalize", props.className)}
@@ -43,4 +48,25 @@ export function TeamPlanBadge(props: {
{`${getTeamPlanBadgeLabel(props.plan)}${props.postfix || ""}`}
</Badge>
);
const track = useTrack();
if (props.plan === "free") {
return (
<Link
href={`/team/${props.teamSlug}/~/settings/billing?showPlans=true`}
onClick={() => {
track({
category: "billing",
action: "show_plans",
label: "team_badge",
});
}}
>
{badge}
</Link>
);
}
return badge;
}
@@ -2,9 +2,9 @@
import { Spinner } from "@/components/ui/Spinner/Spinner";
import { Button } from "@/components/ui/button";
import { TrackedLinkTW } from "@/components/ui/tracked-link";
import { useDashboardRouter } from "@/lib/DashboardRouter";
import { cn } from "@/lib/utils";
import Link from "next/link";
import { useTransition } from "react";
import { useStripeRedirectEvent } from "../../../../(stripe)/stripe-redirect/stripeRedirectChannel";
@@ -54,9 +54,17 @@ function BillingAlertBanner(props: {
"border border-red-600 bg-red-100 text-red-800 hover:bg-red-200 dark:border-red-700 dark:bg-red-900 dark:text-red-100 dark:hover:bg-red-800",
)}
>
<Link href={`/team/${props.teamSlug}/~/settings/invoices`}>
<TrackedLinkTW
href={`/team/${props.teamSlug}/~/settings/invoices`}
category="billingBanner"
label={
props.variant === "warning"
? "pastDue_viewInvoices"
: "serviceCutoff_payNow"
}
>
{props.ctaLabel}
</Link>
</TrackedLinkTW>
</Button>
</div>
);
@@ -0,0 +1,33 @@
"use client";
import type { Team } from "@/api/team";
import { UpsellBannerCard } from "@/components/blocks/UpsellBannerCard";
import { ArrowRightIcon, RocketIcon } from "lucide-react";
/**
* Banner shown to teams on the free plan encouraging them to upgrade.
* It links to the team's billing settings page and automatically opens
* the pricing modal via the `showPlans=true` query param.
*/
export function FreePlanUpsellBannerUI(props: {
teamSlug: string;
highlightPlan: Team["billingPlan"];
}) {
return (
<UpsellBannerCard
title="Unlock more with thirdweb"
description="Upgrade to increase limits and access advanced features."
cta={{
text: "View plans",
icon: <ArrowRightIcon className="size-4" />,
link: `/team/${props.teamSlug}/~/settings/billing?showPlans=true&highlight=${
props.highlightPlan || "growth"
}`,
}}
trackingCategory="billingBanner"
trackingLabel="freePlan_viewPlans"
icon={<RocketIcon className="size-5" />}
accentColor="green"
/>
);
}
@@ -118,9 +118,9 @@ export async function TotalSponsoredChartCardUI({
className={className}
// Get the trend from the last two COMPLETE periods
trendFn={(data, key) =>
data.filter((d) => (d[key] as number) > 0).length >= 3
data.filter((d) => (d[key] as number) > 0).length >= 2
? ((data[data.length - 2]?.[key] as number) ?? 0) /
((data[data.length - 3]?.[key] as number) ?? 0) -
((data[0]?.[key] as number) ?? 0) -
1
: undefined
}
@@ -119,9 +119,9 @@ export async function TransactionsChartCardUI({
className={className}
// Get the trend from the last two COMPLETE periods
trendFn={(data, key) =>
data.filter((d) => (d[key] as number) > 0).length >= 3
data.filter((d) => (d[key] as number) > 0).length >= 2
? ((data[data.length - 2]?.[key] as number) ?? 0) /
((data[data.length - 3]?.[key] as number) ?? 0) -
((data[0]?.[key] as number) ?? 0) -
1
: undefined
}
@@ -4,7 +4,9 @@ import { AppFooter } from "@/components/blocks/app-footer";
import { TabPathLinks } from "@/components/ui/tabs";
import { AnnouncementBanner } from "components/notices/AnnouncementBanner";
import { redirect } from "next/navigation";
import { siwaExamplePrompts } from "../../../(dashboard)/support/page";
import { getClientThirdwebClient } from "../../../../../@/constants/thirdweb-client.client";
import { CustomChatButton } from "../../../../nebula-app/(app)/components/CustomChat/CustomChatButton";
import { getValidAccount } from "../../../account/settings/getAccount";
import {
getAuthToken,
@@ -91,6 +93,19 @@ export default async function TeamLayout(props: {
</div>
<main className="flex grow flex-col">{props.children}</main>
<div className="fixed right-6 bottom-6 z-50">
<CustomChatButton
clientId={undefined}
isLoggedIn={true}
networks="all"
isFloating={true}
pageType="support"
label="Ask AI Assistant"
examplePrompts={siwaExamplePrompts}
teamId={team.id}
authToken={authToken}
/>
</div>
<AppFooter />
</div>
);
@@ -8,6 +8,7 @@ import { redirect } from "next/navigation";
import { getAuthToken } from "../../../api/lib/getAuthToken";
import { loginRedirect } from "../../../login/loginRedirect";
import { Changelog } from "./_components/Changelog";
import { FreePlanUpsellBannerUI } from "./_components/FreePlanUpsellBannerUI";
import { InviteTeamMembersButton } from "./_components/invite-team-members-button";
import {
type ProjectWithAnalytics,
@@ -55,11 +56,18 @@ export default async function Page(props: {
<div className="container flex grow flex-col gap-4 lg:flex-row">
{/* left */}
<div className="flex grow flex-col gap-6 pt-8 lg:pb-20">
<DismissibleAlert
title="Looking for Engines?"
description="Engines, contracts, project settings, and more are now managed within projects. Open or create a project to access them."
localStorageId={`${team.id}-engines-alert`}
/>
{team.billingPlan === "free" ? (
<FreePlanUpsellBannerUI
teamSlug={team.slug}
highlightPlan="growth"
/>
) : (
<DismissibleAlert
title="Looking for Engines?"
description="Engines, contracts, project settings, and more are now managed within projects. Open or create a project to access them."
localStorageId={`${team.id}-engines-alert`}
/>
)}
<TeamProjectsPage
projects={projectsWithTotalWallets}
@@ -1,6 +1,7 @@
import {
getClientTransactions,
getInAppWalletUsage,
getUniversalBridgeUsage,
getUserOpUsage,
getWalletConnections,
getWalletUsers,
@@ -14,6 +15,7 @@ import { redirect } from "next/navigation";
import { type WalletId, getWalletInfo } from "thirdweb/wallets";
import type {
InAppWalletStats,
UniversalBridgeStats,
WalletStats,
WalletUserStats,
} from "types/analytics";
@@ -24,7 +26,10 @@ import { PieChartCard } from "../../../../components/Analytics/PieChartCard";
import { getTeamBySlug } from "@/api/team";
import { GenericLoadingPage } from "@/components/blocks/skeletons/GenericLoadingPage";
import { EmptyStateCard } from "app/(app)/team/components/Analytics/EmptyStateCard";
import {
EmptyStateCard,
EmptyStateContent,
} from "app/(app)/team/components/Analytics/EmptyStateCard";
import { Suspense } from "react";
import { TotalSponsoredChartCardUI } from "../../_components/TotalSponsoredCard";
import { TransactionsChartCardUI } from "../../_components/TransactionsCard";
@@ -100,6 +105,7 @@ async function OverviewPageContent(props: {
userOpUsage,
clientTransactionsTimeSeries,
clientTransactions,
universalBridgeUsage,
] = await Promise.all([
// Aggregated wallet connections
getWalletConnections({
@@ -148,6 +154,13 @@ async function OverviewPageContent(props: {
to: range.to,
period: "all",
}),
// Universal Bridge
getUniversalBridgeUsage({
teamId: teamId,
from: range.from,
to: range.to,
period: interval,
}),
]);
const isEmpty =
@@ -164,8 +177,9 @@ async function OverviewPageContent(props: {
<div className="flex grow flex-col gap-6">
{walletUserStatsTimeSeries.some((w) => w.totalUsers !== 0) ? (
<div className="">
<UsersChartCard
<AppHighlightsCard
userStats={walletUserStatsTimeSeries}
volumeStats={universalBridgeUsage}
searchParams={searchParams}
/>
</div>
@@ -218,80 +232,108 @@ async function OverviewPageContent(props: {
);
}
type UserMetrics = {
totalUsers: number;
type AggregatedMetrics = {
activeUsers: number;
newUsers: number;
returningUsers: number;
totalVolume: number;
feesCollected: number;
};
type TimeSeriesMetrics = UserMetrics & {
type TimeSeriesMetrics = AggregatedMetrics & {
date: string;
};
function processTimeSeriesData(
userStats: WalletUserStats[],
volumeStats: UniversalBridgeStats[],
): TimeSeriesMetrics[] {
const metrics: TimeSeriesMetrics[] = [];
let cumulativeUsers = 0;
for (const stat of userStats) {
cumulativeUsers += stat.newUsers ?? 0;
const volume = volumeStats
.filter((v) => v.date === stat.date && v.status === "completed")
.reduce((acc, curr) => acc + curr.amountUsdCents / 100, 0);
const fees = volumeStats
.filter((v) => v.date === stat.date && v.status === "completed")
.reduce((acc, curr) => acc + curr.developerFeeUsdCents / 100, 0);
metrics.push({
date: stat.date,
activeUsers: stat.totalUsers ?? 0,
returningUsers: stat.returningUsers ?? 0,
newUsers: stat.newUsers ?? 0,
totalUsers: cumulativeUsers,
totalVolume: volume,
feesCollected: fees,
});
}
return metrics;
}
function UsersChartCard({
function AppHighlightsCard({
userStats,
volumeStats,
searchParams,
}: {
userStats: WalletUserStats[];
volumeStats: UniversalBridgeStats[];
searchParams?: { [key: string]: string | string[] | undefined };
}) {
const timeSeriesData = processTimeSeriesData(userStats);
const timeSeriesData = processTimeSeriesData(userStats, volumeStats);
const chartConfig = {
activeUsers: { label: "Active Users", color: "hsl(var(--chart-1))" },
totalUsers: { label: "Total Users", color: "hsl(var(--chart-2))" },
newUsers: { label: "New Users", color: "hsl(var(--chart-3))" },
returningUsers: {
label: "Returning Users",
color: "hsl(var(--chart-4))",
totalVolume: {
label: "Total Volume",
color: "hsl(var(--chart-2))",
isCurrency: true,
emptyContent: (
<EmptyStateContent
metric="Payments"
description="Onramp, swap, and bridge with thirdweb's Universal Bridge."
link="https://portal.thirdweb.com/connect/pay/overview"
/>
),
},
feesCollected: {
label: "Fee Revenue",
color: "hsl(var(--chart-4))",
isCurrency: true,
emptyContent: (
<EmptyStateContent
metric="Fees"
description="Your apps haven't collected any fees yet."
link={"https://portal.thirdweb.com/connect/pay/fees"}
/>
),
},
activeUsers: { label: "Active Users", color: "hsl(var(--chart-1))" },
newUsers: { label: "New Users", color: "hsl(var(--chart-3))" },
} as const;
return (
<CombinedBarChartCard
className="max-md:rounded-none max-md:border-r-0 max-md:border-l-0"
title="Users"
title="App Highlights"
chartConfig={chartConfig}
activeChart={
(searchParams?.usersChart as keyof UserMetrics) ?? "activeUsers"
(searchParams?.appHighlights as keyof AggregatedMetrics) ??
"totalVolume"
}
data={timeSeriesData}
aggregateFn={(_data, key) =>
// If there is only one data point, use that one, otherwise use the previous
timeSeriesData.filter((d) => (d[key] as number) > 0).length >= 2
? timeSeriesData[timeSeriesData.length - 2]?.[key]
: timeSeriesData[timeSeriesData.length - 1]?.[key]
}
// Get the trend from the last two COMPLETE periods
aggregateFn={(_data, key) => {
if (key === "activeUsers") {
return Math.max(...timeSeriesData.map((d) => d[key]));
}
return timeSeriesData.reduce((acc, curr) => acc + curr[key], 0);
}}
trendFn={(data, key) =>
data.filter((d) => (d[key] as number) > 0).length >= 3
data.filter((d) => (d[key] as number) > 0).length >= 2
? ((data[data.length - 2]?.[key] as number) ?? 0) /
((data[data.length - 3]?.[key] as number) ?? 0) -
((data[0]?.[key] as number) ?? 0) -
1
: undefined
}
queryKey="usersChart"
queryKey="appHighlights"
existingQueryParams={searchParams}
/>
);
@@ -1,3 +1,4 @@
import { getTeamBySlug } from "@/api/team";
import { SidebarLayout } from "@/components/blocks/SidebarLayout";
import { redirect } from "next/navigation";
import { getAuthToken } from "../../../../../../../../api/lib/getAuthToken";
@@ -25,6 +26,13 @@ export async function EcosystemLayoutSlug({
params.team_slug,
);
// Fetch team details to obtain team ID for further authenticated updates
const team = await getTeamBySlug(params.team_slug);
if (!team) {
redirect(ecosystemLayoutPath);
}
if (!ecosystem) {
redirect(ecosystemLayoutPath);
}
@@ -35,6 +43,8 @@ export async function EcosystemLayoutSlug({
ecosystem={ecosystem}
ecosystemLayoutPath={ecosystemLayoutPath}
teamIdOrSlug={params.team_slug}
authToken={authToken}
teamId={team.id}
/>
<SidebarLayout
@@ -1,9 +1,19 @@
"use client";
/* eslint-disable */
import { Img } from "@/components/blocks/Img";
import { CopyTextButton } from "@/components/ui/CopyTextButton";
import { Spinner } from "@/components/ui/Spinner/Spinner";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogClose,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import {
DropdownMenu,
DropdownMenuContent,
@@ -12,19 +22,28 @@ import {
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { ImageUpload } from "@/components/ui/image-upload";
import { Input } from "@/components/ui/input";
import { Skeleton } from "@/components/ui/skeleton";
import { useThirdwebClient } from "@/constants/thirdweb.client";
import { useDashboardRouter } from "@/lib/DashboardRouter";
import { resolveSchemeWithErrorHandler } from "@/lib/resolveSchemeWithErrorHandler";
import { cn } from "@/lib/utils";
import { useDashboardStorageUpload } from "@3rdweb-sdk/react/hooks/useDashboardStorageUpload";
import {
AlertTriangleIcon,
CheckIcon,
ChevronsUpDownIcon,
ExternalLinkIcon,
PencilIcon,
PlusCircleIcon,
} from "lucide-react";
import Link from "next/link";
import { useState } from "react";
import { toast } from "sonner";
import { useEcosystemList } from "../../../hooks/use-ecosystem-list";
import type { Ecosystem } from "../../../types";
import { useUpdateEcosystem } from "../configuration/hooks/use-update-ecosystem";
import { useEcosystem } from "../hooks/use-ecosystem";
function EcosystemAlertBanner({ ecosystem }: { ecosystem: Ecosystem }) {
@@ -113,6 +132,8 @@ export function EcosystemHeader(props: {
ecosystem: Ecosystem;
ecosystemLayoutPath: string;
teamIdOrSlug: string;
authToken: string;
teamId: string;
}) {
const { data: fetchedEcosystem } = useEcosystem({
teamIdOrSlug: props.teamIdOrSlug,
@@ -135,6 +156,83 @@ export function EcosystemHeader(props: {
client,
});
// ------------------- Image Upload Logic -------------------
const [isDialogOpen, setIsDialogOpen] = useState(false);
const [selectedFile, setSelectedFile] = useState<File | null>(null);
// Name editing state
const [isNameDialogOpen, setIsNameDialogOpen] = useState(false);
const [tempName, setTempName] = useState(ecosystem.name);
const storageUpload = useDashboardStorageUpload();
const router = useDashboardRouter();
const { mutateAsync: updateEcosystem, isPending: isUpdating } =
useUpdateEcosystem(
{
authToken: props.authToken,
teamId: props.teamId,
},
{
onSuccess: () => {
toast.success("Ecosystem updated");
setIsDialogOpen(false);
router.refresh();
},
onError: (error) => {
const message =
error instanceof Error ? error.message : "Failed to update image";
toast.error(message);
},
},
);
const isUploading = storageUpload.isPending || isUpdating;
async function handleUpload() {
if (!selectedFile) {
toast.error("Please select an image to upload");
return;
}
// Validate file type
const validTypes = ["image/png", "image/jpeg", "image/webp"];
if (!validTypes.includes(selectedFile.type)) {
toast.error("Only PNG, JPG or WEBP images are allowed");
return;
}
try {
const [uri] = await storageUpload.mutateAsync([selectedFile]);
await updateEcosystem({
...ecosystem,
imageUrl: uri,
});
} catch (err) {
console.error(err);
toast.error("Failed to upload image");
}
}
async function handleNameSave() {
const trimmed = tempName.trim();
if (!trimmed || trimmed === ecosystem.name) {
setIsNameDialogOpen(false);
return;
}
try {
await updateEcosystem({
...ecosystem,
name: trimmed,
});
setIsNameDialogOpen(false);
router.refresh();
} catch (err) {
console.error(err);
toast.error("Failed to update name");
}
}
return (
<div className="border-b py-8">
<div className="container flex flex-col gap-8">
@@ -146,11 +244,74 @@ export function EcosystemHeader(props: {
<Skeleton className="size-24" />
) : (
ecosystemImageLink && (
<Img
src={ecosystemImageLink}
alt={ecosystem.name}
className="size-24 rounded-full border object-contain object-center"
/>
<div className="relative">
<Img
src={ecosystemImageLink}
alt={ecosystem.name}
className={cn(
"size-24",
"border",
"rounded-full",
"object-contain object-center",
)}
/>
{/* Upload Dialog */}
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
<DialogTrigger asChild>
<Button
variant="ghost"
size="icon"
className={cn(
"absolute",
"right-0 bottom-0",
"h-6 w-6",
"p-1",
"rounded-full",
"bg-background",
"hover:bg-accent",
)}
aria-label="Change logo"
>
<PencilIcon className="h-4 w-4" />
</Button>
</DialogTrigger>
<DialogContent className="max-w-[480px]">
<DialogHeader>
<DialogTitle>Update Ecosystem Logo</DialogTitle>
</DialogHeader>
<div className="flex flex-col gap-4 py-2">
<ImageUpload
onUpload={(files) => {
if (files?.[0]) {
setSelectedFile(files[0]);
}
}}
accept="image/png,image/jpeg,image/webp"
/>
</div>
<DialogFooter className="mt-4">
<DialogClose asChild>
<Button variant="outline" disabled={isUploading}>
Cancel
</Button>
</DialogClose>
<Button
onClick={handleUpload}
disabled={isUploading || !selectedFile}
>
{isUploading ? (
<Spinner className="h-4 w-4" />
) : (
"Upload"
)}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
)
)}
<div className="flex flex-col gap-2">
@@ -159,6 +320,52 @@ export function EcosystemHeader(props: {
) : (
<h2 className="font-semibold text-3xl text-foreground tracking-tight">
{ecosystem.name}
<Dialog
open={isNameDialogOpen}
onOpenChange={setIsNameDialogOpen}
>
<DialogTrigger asChild>
<Button
variant="ghost"
size="icon"
className="ml-2 h-5 w-5 rounded-full p-1 hover:bg-accent"
aria-label="Edit name"
>
<PencilIcon className="h-4 w-4" />
</Button>
</DialogTrigger>
<DialogContent className="max-w-[480px]">
<DialogHeader>
<DialogTitle>Edit Ecosystem Name</DialogTitle>
</DialogHeader>
<div className="flex flex-col gap-4 py-2">
<Input
value={tempName}
onChange={(e) => setTempName(e.target.value)}
/>
</div>
<DialogFooter className="mt-4">
<DialogClose asChild>
<Button variant="outline" disabled={isUpdating}>
Cancel
</Button>
</DialogClose>
<Button
onClick={handleNameSave}
disabled={isUpdating || !tempName.trim()}
>
{isUpdating ? (
<Spinner className="h-4 w-4" />
) : (
"Save"
)}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</h2>
)}
{!ecosystem.slug ? (
@@ -169,7 +376,7 @@ export function EcosystemHeader(props: {
textToCopy={`ecosystem.${ecosystem.slug}`}
textToShow={`ecosystem.${ecosystem.slug}`}
copyIconPosition="right"
tooltip="Copy Ecosytem slug"
tooltip="Copy Ecosystem slug"
variant="ghost"
className="-translate-x-2 px-2 py-0.5 text-muted-foreground"
/>
@@ -1,26 +0,0 @@
import { NEXT_PUBLIC_THIRDWEB_API_HOST } from "@/constants/public-envs";
import type { UsageBillableByService } from "@3rdweb-sdk/react/hooks/useApi";
import { getAuthToken } from "../../../../../api/lib/getAuthToken";
export async function getAccountUsage() {
const token = await getAuthToken();
if (!token) {
return undefined;
}
const res = await fetch(`${NEXT_PUBLIC_THIRDWEB_API_HOST}/v1/account/usage`, {
method: "GET",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
},
});
if (!res.ok) {
return undefined;
}
const json = await res.json();
return json.data as UsageBillableByService;
}
@@ -4,10 +4,7 @@ import type { TeamSubscription } from "@/api/team-subscription";
import type { RPCUsageDataItem } from "@/api/usage/rpc";
import { ThirdwebBarChart } from "@/components/blocks/charts/bar-chart";
import { Button } from "@/components/ui/button";
import type {
Account,
UsageBillableByService,
} from "@3rdweb-sdk/react/hooks/useApi";
import type { Account } from "@3rdweb-sdk/react/hooks/useApi";
import { InAppWalletUsersChartCardUI } from "components/embedded-wallets/Analytics/InAppWalletUsersChartCard";
import { subMonths } from "date-fns";
import Link from "next/link";
@@ -18,8 +15,6 @@ import { SponsoredTransactionsTable } from "./SponsoredTransactionsTable";
import { UsageCard } from "./UsageCard";
type UsageProps = {
// TODO - remove when we have all the data available from team
usage: UsageBillableByService;
subscriptions: TeamSubscription[];
account: Account;
team: Team;
@@ -39,7 +34,6 @@ const compactNumberFormatter = new Intl.NumberFormat("en-US", {
});
export const Usage: React.FC<UsageProps> = ({
usage: usageData,
subscriptions,
account,
team,
@@ -48,20 +42,18 @@ export const Usage: React.FC<UsageProps> = ({
rpcUsage,
}) => {
const gatewayMetrics = useMemo(() => {
if (!usageData) {
return {};
}
return {
title: "Unlimited Requests",
total: (
<span className="text-muted-foreground">
{compactNumberFormatter.format(usageData.rateLimits.storage)} Requests
Per Second
{compactNumberFormatter.format(
team.capabilities.storage.upload.rateLimit,
)}{" "}
Requests Per Second
</span>
),
};
}, [usageData]);
}, [team.capabilities.storage.upload.rateLimit]);
const usageSub = subscriptions.find((sub) => sub.type === "USAGE");
@@ -138,7 +130,7 @@ export const Usage: React.FC<UsageProps> = ({
<ThirdwebBarChart
header={{
title: "RPC Requests",
description: `Your plan allows for ${usageData.rateLimits.rpc} requests per second`,
description: `Your plan allows for ${team.capabilities.rpc.rateLimit} requests per second`,
titleClassName: "text-xl mb-0.5",
}}
data={rpcUsageData}
@@ -8,7 +8,6 @@ import { getClientThirdwebClient } from "../../../../../../../@/constants/thirdw
import { getValidAccount } from "../../../../../account/settings/getAccount";
import { getAuthToken } from "../../../../../api/lib/getAuthToken";
import { loginRedirect } from "../../../../../login/loginRedirect";
import { getAccountUsage } from "./getAccountUsage";
import { Usage } from "./overview/components/Usage";
export default async function Page(props: {
@@ -31,8 +30,7 @@ export default async function Page(props: {
loginRedirect(`/team/${params.team_slug}/~/usage`);
}
const [accountUsage, subscriptions, projects, rpcUsage] = await Promise.all([
getAccountUsage(),
const [subscriptions, projects, rpcUsage] = await Promise.all([
getTeamSubscriptions(team.slug),
getProjects(team.slug),
fetchRPCUsage({
@@ -48,7 +46,7 @@ export default async function Page(props: {
}),
]);
if (!accountUsage || !subscriptions) {
if (!subscriptions) {
return (
<div className="flex min-h-[350px] items-center justify-center rounded-lg border p-4 text-destructive-text">
Something went wrong. Please try again later.
@@ -63,8 +61,6 @@ export default async function Page(props: {
return (
<Usage
// TODO - remove accountUsage when we have all the data available from team
usage={accountUsage}
subscriptions={subscriptions}
account={account}
team={team}
@@ -86,7 +86,7 @@ export default async function RPCUsage(props: {
<div className="font-bold text-2xl capitalize">
{currentRateLimit.toLocaleString()} RPS
</div>
<TeamPlanBadge plan={currentPlan} />
<TeamPlanBadge plan={currentPlan} teamSlug={team.slug} />
</div>
</CardContent>
</Card>
@@ -0,0 +1,135 @@
"use client";
import { Badge } from "@/components/ui/badge";
import { cn } from "@/lib/utils";
import { ImportModal } from "components/contract-components/import-contract/modal";
import { useTrack } from "hooks/analytics/useTrack";
import { ArrowDownToLineIcon, CoinsIcon, ImagesIcon } from "lucide-react";
import Link from "next/link";
import { useState } from "react";
import type { ThirdwebClient } from "thirdweb";
export function Cards(props: {
teamSlug: string;
projectSlug: string;
client: ThirdwebClient;
teamId: string;
projectId: string;
}) {
const [importModalOpen, setImportModalOpen] = useState(false);
return (
<div className="grid grid-cols-1 gap-4 lg:grid-cols-3">
<ImportModal
client={props.client}
isOpen={importModalOpen}
onClose={() => {
setImportModalOpen(false);
}}
teamId={props.teamId}
projectId={props.projectId}
type="asset"
/>
<CardLink
title="Create Token"
description="Launch your own ERC-20 token"
href={`/team/${props.teamSlug}/${props.projectSlug}/assets/create`}
icon={CoinsIcon}
trackingLabel="create-token"
/>
<CardLink
title="Create NFT Collection"
description="Launch your own ERC-721 NFT collection"
href={undefined}
icon={ImagesIcon}
trackingLabel="create-nft"
badge="Coming Soon"
/>
<CardLink
title="Import Existing Asset"
description="Import tokens or NFTs you own to the project"
href={undefined}
icon={ArrowDownToLineIcon}
trackingLabel="import-asset"
onClick={() => {
setImportModalOpen(true);
}}
/>
</div>
);
}
function CardLink(props: {
title: string;
description: string;
href: string | undefined;
onClick?: () => void;
icon: React.FC<{ className?: string }>;
trackingLabel: string;
badge?: string;
}) {
const { onClick } = props;
const isClickable = !!onClick || !!props.href;
const trackEvent = useTrack();
function handleClick() {
trackEvent({
category: "assets-landing-page",
action: "click",
label: props.trackingLabel,
});
onClick?.();
}
return (
<div
className={cn(
"relative flex flex-col rounded-lg border bg-card p-4",
isClickable && "cursor-pointer hover:border-active-border ",
)}
onClick={handleClick}
role={onClick ? "button" : undefined}
tabIndex={onClick ? 0 : undefined}
onKeyDown={
onClick
? (e) => {
if (e.key === "Enter" || e.key === " ") {
handleClick();
}
}
: undefined
}
>
<div className="mb-4 flex">
<div className="flex items-center justify-center rounded-full border p-2.5">
<props.icon className="size-5 text-muted-foreground" />
</div>
</div>
{props.badge && (
<div className="absolute top-4 right-4">
<Badge
variant="outline"
className="border-dashed py-1 text-muted-foreground"
>
{props.badge}
</Badge>
</div>
)}
<h3 className="mb-0.5 font-semibold text-lg tracking-tight">
{props.href ? (
<Link href={props.href} className="before:absolute before:inset-0">
{props.title}
</Link>
) : (
<span>{props.title}</span>
)}
</h3>
<p className="text-muted-foreground text-sm">{props.description}</p>
</div>
);
}
@@ -0,0 +1,79 @@
import { Button } from "@/components/ui/button";
import { useTrack } from "hooks/analytics/useTrack";
import { ArrowLeftIcon, ArrowRightIcon } from "lucide-react";
import { getStepCardTrackingData } from "./tracking";
export function StepCard(props: {
title: string;
page: "info" | "distribution" | "launch";
prevButton:
| undefined
| {
onClick: () => void;
};
nextButton:
| undefined
| {
type: "submit";
}
| {
type: "custom";
custom: React.ReactNode;
};
children: React.ReactNode;
}) {
const trackEvent = useTrack();
return (
<div className="rounded-lg border bg-card">
<h2 className="border-b px-4 py-5 font-semibold text-xl tracking-tight md:px-6">
{props.title}
</h2>
{props.children}
<div className="flex justify-end gap-3 border-t p-6">
{props.prevButton && (
<Button
variant="outline"
className="gap-2"
onClick={() => {
props.prevButton?.onClick();
trackEvent(
getStepCardTrackingData({
step: props.page,
click: "prev",
}),
);
}}
>
<ArrowLeftIcon className="size-4" />
Back
</Button>
)}
{props.nextButton && props.nextButton.type === "submit" && (
<Button
variant="default"
className="gap-2"
type="submit"
onClick={() => {
trackEvent(
getStepCardTrackingData({
step: props.page,
click: "next",
}),
);
}}
>
Next
<ArrowRightIcon className="size-4" />
</Button>
)}
{props.nextButton &&
props.nextButton.type === "custom" &&
props.nextButton.custom}
</div>
</div>
);
}
@@ -0,0 +1,369 @@
"use client";
import { revalidatePathAction } from "@/actions/revalidate";
import {
DEFAULT_FEE_BPS_NEW,
DEFAULT_FEE_RECIPIENT,
} from "constants/addresses";
import { useTrack } from "hooks/analytics/useTrack";
import { useAllChainsData } from "hooks/chains/allChains";
import { defineDashboardChain } from "lib/defineDashboardChain";
import { useRef } from "react";
import { toast } from "sonner";
import {
NATIVE_TOKEN_ADDRESS,
type ThirdwebClient,
getAddress,
getContract,
sendAndConfirmTransaction,
toUnits,
} from "thirdweb";
import { deployERC20Contract } from "thirdweb/deploys";
import type { ClaimConditionsInput } from "thirdweb/dist/types/utils/extensions/drops/types";
import {
claimTo,
setClaimConditions as setClaimConditionsExtension,
transferBatch,
} from "thirdweb/extensions/erc20";
import { useActiveAccount } from "thirdweb/react";
import { useAddContractToProject } from "../../hooks/project-contracts";
import { CreateTokenAssetPageUI } from "./create-token-page.client";
import type { CreateAssetFormValues } from "./form";
import {
getTokenDeploymentTrackingData,
getTokenStepTrackingData,
} from "./tracking";
export function CreateTokenAssetPage(props: {
accountAddress: string;
client: ThirdwebClient;
teamId: string;
projectId: string;
}) {
const account = useActiveAccount();
const { idToChain } = useAllChainsData();
const addContractToProject = useAddContractToProject();
const contractAddressRef = useRef<string | undefined>(undefined);
const trackEvent = useTrack();
async function deployContract(formValues: CreateAssetFormValues) {
if (!account) {
toast.error("No Connected Wallet");
throw new Error("No Connected Wallet");
}
trackEvent(
getTokenDeploymentTrackingData("attempt", Number(formValues.chain)),
);
const socialUrls = formValues.socialUrls.reduce(
(acc, url) => {
if (url.url && url.platform) {
acc[url.platform] = url.url;
}
return acc;
},
{} as Record<string, string>,
);
try {
const contractAddress = await deployERC20Contract({
account,
// eslint-disable-next-line no-restricted-syntax
chain: defineDashboardChain(
Number(formValues.chain),
idToChain.get(Number(formValues.chain)),
),
client: props.client,
type: "DropERC20",
params: {
// metadata
name: formValues.name,
description: formValues.description,
symbol: formValues.symbol,
image: formValues.image,
// platform fees
platformFeeBps: BigInt(DEFAULT_FEE_BPS_NEW),
platformFeeRecipient: DEFAULT_FEE_RECIPIENT,
// primary sale
saleRecipient: account.address,
social_urls: socialUrls,
},
});
trackEvent(
getTokenDeploymentTrackingData("success", Number(formValues.chain)),
);
// add contract to project in background
addContractToProject.mutateAsync({
teamId: props.teamId,
projectId: props.projectId,
contractAddress: contractAddress,
chainId: formValues.chain,
deploymentType: "asset",
contractType: "DropERC20",
});
contractAddressRef.current = contractAddress;
return {
contractAddress: contractAddress,
};
} catch (e) {
trackEvent(
getTokenDeploymentTrackingData("error", Number(formValues.chain)),
);
throw e;
}
}
async function airdropTokens(formValues: CreateAssetFormValues) {
const contractAddress = contractAddressRef.current;
if (!contractAddress) {
throw new Error("No contract address");
}
if (!account) {
throw new Error("No connected account");
}
// eslint-disable-next-line no-restricted-syntax
const chain = defineDashboardChain(
Number(formValues.chain),
idToChain.get(Number(formValues.chain)),
);
const contract = getContract({
client: props.client,
address: contractAddress,
chain,
});
trackEvent(
getTokenStepTrackingData({
action: "airdrop",
chainId: Number(formValues.chain),
status: "attempt",
}),
);
try {
const airdropTx = transferBatch({
contract,
batch: formValues.airdropAddresses.map((recipient) => ({
to: recipient.address,
amount: recipient.quantity,
})),
});
await sendAndConfirmTransaction({
transaction: airdropTx,
account,
});
trackEvent(
getTokenStepTrackingData({
action: "airdrop",
chainId: Number(formValues.chain),
status: "success",
}),
);
} catch (e) {
trackEvent(
getTokenStepTrackingData({
action: "airdrop",
chainId: Number(formValues.chain),
status: "error",
}),
);
throw e;
}
}
async function mintTokens(formValues: CreateAssetFormValues) {
const contractAddress = contractAddressRef.current;
if (!contractAddress) {
throw new Error("No contract address");
}
if (!account) {
throw new Error("No connected account");
}
// eslint-disable-next-line no-restricted-syntax
const chain = defineDashboardChain(
Number(formValues.chain),
idToChain.get(Number(formValues.chain)),
);
const contract = getContract({
client: props.client,
address: contractAddress,
chain,
});
const totalSupply = Number(formValues.supply);
const salePercent = formValues.saleEnabled
? Number(formValues.saleAllocationPercentage)
: 0;
const ownerAndAirdropPercent = 100 - salePercent;
const ownerSupplyTokens = (totalSupply * ownerAndAirdropPercent) / 100;
trackEvent(
getTokenStepTrackingData({
action: "mint",
chainId: Number(formValues.chain),
status: "attempt",
}),
);
try {
const claimTx = claimTo({
contract,
to: account.address,
quantity: ownerSupplyTokens.toString(),
});
await sendAndConfirmTransaction({
transaction: claimTx,
account,
});
trackEvent(
getTokenStepTrackingData({
action: "mint",
chainId: Number(formValues.chain),
status: "success",
}),
);
} catch (e) {
trackEvent(
getTokenStepTrackingData({
action: "mint",
chainId: Number(formValues.chain),
status: "error",
}),
);
throw e;
}
}
async function setClaimConditions(formValues: CreateAssetFormValues) {
const contractAddress = contractAddressRef.current;
if (!contractAddress) {
throw new Error("No contract address");
}
if (!account) {
throw new Error("No connected account");
}
// eslint-disable-next-line no-restricted-syntax
const chain = defineDashboardChain(
Number(formValues.chain),
idToChain.get(Number(formValues.chain)),
);
const contract = getContract({
client: props.client,
address: contractAddress,
chain,
});
const salePercent = formValues.saleEnabled
? Number(formValues.saleAllocationPercentage)
: 0;
const totalSupply = Number(formValues.supply);
const totalSupplyWei = toUnits(totalSupply.toString(), 18);
const phases: ClaimConditionsInput[] = [
{
maxClaimablePerWallet: formValues.saleEnabled ? undefined : 0n,
maxClaimableSupply: totalSupplyWei,
price:
formValues.saleEnabled && salePercent > 0
? formValues.salePrice
: "0",
currencyAddress:
getAddress(formValues.saleTokenAddress) ===
getAddress(NATIVE_TOKEN_ADDRESS)
? undefined
: formValues.saleTokenAddress,
startTime: new Date(),
metadata: {
name:
formValues.saleEnabled && salePercent > 0
? "Token Sale phase"
: "Only Owner phase",
},
overrideList: [
{
address: account.address,
maxClaimable: "unlimited",
price: "0",
},
],
},
];
const preparedTx = setClaimConditionsExtension({
contract,
phases,
});
trackEvent(
getTokenStepTrackingData({
action: "claim-conditions",
chainId: Number(formValues.chain),
status: "attempt",
}),
);
try {
await sendAndConfirmTransaction({
transaction: preparedTx,
account,
});
trackEvent(
getTokenStepTrackingData({
action: "claim-conditions",
chainId: Number(formValues.chain),
status: "success",
}),
);
} catch (e) {
trackEvent(
getTokenStepTrackingData({
action: "claim-conditions",
chainId: Number(formValues.chain),
status: "error",
}),
);
throw e;
}
}
return (
<CreateTokenAssetPageUI
accountAddress={props.accountAddress}
client={props.client}
onLaunchSuccess={() => {
revalidatePathAction(
`/team/${props.teamId}/project/${props.projectId}/assets`,
"page",
);
}}
createTokenFunctions={{
deployContract,
airdropTokens,
mintTokens,
setClaimConditions,
}}
/>
);
}
@@ -0,0 +1,131 @@
"use client";
import { zodResolver } from "@hookform/resolvers/zod";
import { useState } from "react";
import { useForm } from "react-hook-form";
import {
NATIVE_TOKEN_ADDRESS,
type ThirdwebClient,
getAddress,
} from "thirdweb";
import { TokenDistributionFieldset } from "./distribution/token-distribution";
import {
type CreateAssetFormValues,
type TokenDistributionFormValues,
type TokenInfoFormValues,
tokenDistributionFormSchema,
tokenInfoFormSchema,
} from "./form";
import { LaunchTokenStatus } from "./launch/launch-token";
import { TokenInfoFieldset } from "./token-info-fieldset";
export type CreateTokenFunctions = {
deployContract: (values: CreateAssetFormValues) => Promise<{
contractAddress: string;
}>;
setClaimConditions: (values: CreateAssetFormValues) => Promise<void>;
mintTokens: (values: CreateAssetFormValues) => Promise<void>;
airdropTokens: (values: CreateAssetFormValues) => Promise<void>;
};
const checksummedNativeTokenAddress = getAddress(NATIVE_TOKEN_ADDRESS);
export function CreateTokenAssetPageUI(props: {
accountAddress: string;
client: ThirdwebClient;
createTokenFunctions: CreateTokenFunctions;
onLaunchSuccess: () => void;
}) {
const [step, setStep] = useState<"token-info" | "distribution" | "launch">(
"token-info",
);
const tokenInfoForm = useForm<TokenInfoFormValues>({
resolver: zodResolver(tokenInfoFormSchema),
values: {
name: "",
description: "",
symbol: "",
image: undefined,
chain: "1",
socialUrls: [
{
platform: "Website",
url: "",
},
{
platform: "Twitter",
url: "",
},
],
},
reValidateMode: "onChange",
});
const tokenDistributionForm = useForm<TokenDistributionFormValues>({
resolver: zodResolver(tokenDistributionFormSchema),
values: {
// sale fieldset
saleAllocationPercentage: "0",
saleTokenAddress: checksummedNativeTokenAddress,
salePrice: "0.1",
supply: "1000000",
saleEnabled: false,
// airdrop
airdropEnabled: false,
airdropAddresses: [],
},
reValidateMode: "onChange",
});
return (
<div>
{step === "token-info" && (
<TokenInfoFieldset
onChainUpdated={() => {
// if the chain is updated, set the sale token address to the native token address
tokenDistributionForm.setValue(
"saleTokenAddress",
checksummedNativeTokenAddress,
);
}}
client={props.client}
form={tokenInfoForm}
onNext={() => {
setStep("distribution");
}}
/>
)}
{step === "distribution" && (
<TokenDistributionFieldset
client={props.client}
form={tokenDistributionForm}
accountAddress={props.accountAddress}
chainId={tokenInfoForm.watch("chain")}
onPrevious={() => {
setStep("token-info");
}}
onNext={() => {
setStep("launch");
}}
/>
)}
{step === "launch" && (
<LaunchTokenStatus
client={props.client}
onLaunchSuccess={props.onLaunchSuccess}
onPrevious={() => {
setStep("distribution");
}}
createTokenFunctions={props.createTokenFunctions}
values={{
...tokenInfoForm.getValues(),
...tokenDistributionForm.getValues(),
}}
/>
)}
</div>
);
}
@@ -0,0 +1,63 @@
import type { Meta, StoryObj } from "@storybook/react";
import { storybookThirdwebClient } from "stories/utils";
import { ConnectButton, ThirdwebProvider } from "thirdweb/react";
import { CreateTokenAssetPageUI } from "./create-token-page.client";
const meta = {
title: "Assets/CreateTokenPage",
component: CreateTokenAssetPageUI,
decorators: [
(Story) => (
<ThirdwebProvider>
<div className="container max-w-5xl py-10">
<ConnectButton client={storybookThirdwebClient} />
<div className="h-10" />
<Story />
</div>
</ThirdwebProvider>
),
],
} satisfies Meta<typeof CreateTokenAssetPageUI>;
export default meta;
type Story = StoryObj<typeof meta>;
const mockCreateTokenFunctions = {
deployContract: async () => {
await new Promise((resolve) => setTimeout(resolve, 1000));
return { contractAddress: "0x123" };
},
setClaimConditions: async () => {
await new Promise((resolve) => setTimeout(resolve, 1000));
},
mintTokens: async () => {
await new Promise((resolve) => setTimeout(resolve, 1000));
},
airdropTokens: async () => {
await new Promise((resolve) => setTimeout(resolve, 1000));
},
};
export const Default: Story = {
args: {
accountAddress: "0x1234567890123456789012345678901234567890",
client: storybookThirdwebClient,
createTokenFunctions: mockCreateTokenFunctions,
onLaunchSuccess: () => {},
},
};
export const ErrorOnDeploy: Story = {
args: {
accountAddress: "0x1234567890123456789012345678901234567890",
client: storybookThirdwebClient,
onLaunchSuccess: () => {},
createTokenFunctions: {
...mockCreateTokenFunctions,
deployContract: async () => {
await new Promise((resolve) => setTimeout(resolve, 1000));
throw new Error("Failed to deploy contract");
},
},
},
};
@@ -0,0 +1,437 @@
/* eslint-disable @next/next/no-img-element */
"use client";
import { DynamicHeight } from "@/components/ui/DynamicHeight";
import { Spinner } from "@/components/ui/Spinner/Spinner";
import { Button } from "@/components/ui/button";
import { InlineCode } from "@/components/ui/inline-code";
import {
Sheet,
SheetContent,
SheetDescription,
SheetHeader,
SheetTitle,
SheetTrigger,
} from "@/components/ui/sheet";
import { Switch } from "@/components/ui/switch";
import {
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { cn } from "@/lib/utils";
import { useCsvUpload } from "hooks/useCsvUpload";
import {
ArrowDownToLineIcon,
ArrowRightIcon,
ArrowUpFromLineIcon,
CircleAlertIcon,
FileTextIcon,
RotateCcwIcon,
UploadIcon,
XIcon,
} from "lucide-react";
import { useState } from "react";
import type { TokenDistributionForm } from "../form";
type AirdropAddressInput = {
address: string;
quantity: string;
isValid?: boolean;
resolvedAddress?: string;
};
export function TokenAirdropSection(props: {
form: TokenDistributionForm;
}) {
const airdropAddresses = props.form.watch("airdropAddresses");
const [showUploadSheet, setShowUploadSheet] = useState(false);
const totalAirdropSupply = airdropAddresses.reduce(
(acc, curr) => acc + Number(curr.quantity),
0,
);
const isEnabled = props.form.watch("airdropEnabled");
const removeAirdropAddresses = () => {
props.form.setValue("airdropAddresses", []);
};
return (
<DynamicHeight>
<div className="relative border-t border-dashed">
<div className="flex items-center justify-between gap-3 px-6 py-5">
<div>
<h2 className="font-semibold text-lg">Airdrop</h2>
<p className="text-muted-foreground text-sm">
Airdrop tokens to a list of addresses with each address receiving
a specific quantity
</p>
</div>
<Switch
checked={isEnabled}
onCheckedChange={(checked) => {
props.form.setValue("airdropEnabled", checked);
if (!checked) {
removeAirdropAddresses();
}
}}
/>
</div>
{isEnabled && (
<div className="flex justify-start px-6 pb-6">
{airdropAddresses.length > 0 ? (
<div className="flex w-full flex-col gap-4 rounded-lg border bg-background p-4 md:flex-row lg:items-center lg:justify-between">
{/* left */}
<div>
<h3 className="font-medium text-sm">CSV File Uploaded</h3>
<p className="text-muted-foreground text-sm">
<span className="font-semibold">
{airdropAddresses.length}
</span>{" "}
addresses will receive a total of{" "}
<span className="font-semibold">{totalAirdropSupply}</span>{" "}
tokens
</p>
</div>
{/* right */}
<div className="flex gap-3">
<Sheet>
<SheetTrigger asChild>
<Button size="sm" variant="outline">
<FileTextIcon className="mr-2 size-4" />
View CSV
</Button>
</SheetTrigger>
<SheetContent className="flex h-dvh w-full flex-col gap-0 overflow-hidden lg:max-w-2xl">
<SheetHeader className="mb-3">
<SheetTitle className="text-left">
Airdrop CSV
</SheetTitle>
</SheetHeader>
<AirdropTable
data={airdropAddresses}
className="rounded-b-none"
/>
<div className="flex justify-end gap-3 rounded-b-lg border border-t-0 bg-card p-6">
<Button
variant="outline"
onClick={removeAirdropAddresses}
>
<XIcon className="mr-2 size-4" />
Remove
</Button>
</div>
</SheetContent>
</Sheet>
<Button
size="sm"
variant="outline"
onClick={removeAirdropAddresses}
className="gap-2"
>
<XIcon className="size-4" />
Remove
</Button>
</div>
</div>
) : (
<div>
<Sheet open={showUploadSheet} onOpenChange={setShowUploadSheet}>
<SheetContent className="flex h-dvh w-full flex-col gap-0 overflow-hidden lg:max-w-2xl">
<SheetHeader className="mb-3">
<SheetTitle className="text-left font-semibold text-lg">
Airdrop CSV File
</SheetTitle>
<SheetDescription>
Upload a CSV file to airdrop tokens to a list of
addresses
</SheetDescription>
</SheetHeader>
<AirdropUpload
setAirdrop={(addresses) => {
props.form.setValue("airdropAddresses", addresses);
setShowUploadSheet(false);
}}
onClose={() => setShowUploadSheet(false)}
/>
</SheetContent>
</Sheet>
<Button
variant="outline"
onClick={() => setShowUploadSheet(true)}
className="min-w-44 gap-2 bg-background"
>
<ArrowUpFromLineIcon className="size-4 text-muted-foreground" />
Upload CSV
</Button>
</div>
)}
</div>
)}
</div>
</DynamicHeight>
);
}
type AirdropUploadProps = {
setAirdrop: (airdrop: AirdropAddressInput[]) => void;
onClose: () => void;
};
// CSV parser for airdrop data
const csvParser = (items: AirdropAddressInput[]): AirdropAddressInput[] => {
return items
.map(({ address, quantity }) => ({
address: (address || "").trim(),
quantity: (quantity || "1").trim(),
}))
.filter(({ address }) => address !== "");
};
const AirdropUpload: React.FC<AirdropUploadProps> = ({
setAirdrop,
onClose,
}) => {
const {
normalizeQuery,
getInputProps,
getRootProps,
rawData,
noCsv,
reset,
removeInvalid,
} = useCsvUpload<AirdropAddressInput>({ csvParser });
const normalizeData = normalizeQuery.data;
if (!normalizeData) {
return (
<div className="flex h-[300px] w-full grow items-center justify-center rounded-lg border border-border">
<Spinner className="size-10" />
</div>
);
}
const handleContinue = () => {
setAirdrop(
normalizeData.result.map((o) => ({
address: o.resolvedAddress || o.address,
quantity: o.quantity,
isValid: o.isValid,
})),
);
onClose();
};
return (
<div className="flex w-full grow flex-col gap-6 overflow-hidden">
{normalizeData.result.length && rawData.length > 0 ? (
<div className="flex grow flex-col overflow-hidden outline">
{normalizeQuery.data.invalidFound && (
<p className="mb-3 text-red-500 text-sm">
Invalid addresses found. Please remove them before continuing.
</p>
)}
<AirdropTable
data={normalizeData.result}
className="rounded-b-none"
/>
<div className="flex justify-between gap-3 rounded-b-lg border border-t-0 bg-card p-6">
<Button
variant="outline"
disabled={rawData.length === 0}
onClick={() => {
reset();
}}
>
<RotateCcwIcon className="mr-2 size-4" />
Reset
</Button>
{normalizeQuery.data.invalidFound ? (
<Button
disabled={rawData.length === 0}
onClick={() => {
removeInvalid();
}}
>
<XIcon className="mr-2 size-4" />
Remove invalid addresses
</Button>
) : (
<Button onClick={handleContinue} disabled={rawData.length === 0}>
Continue <ArrowRightIcon className="ml-2 size-4" />
</Button>
)}
</div>
</div>
) : (
<div>
<div className="relative w-full">
<div
className={cn(
"flex h-[300px] cursor-pointer items-center justify-center rounded-md border border-dashed bg-card hover:border-active-border",
noCsv &&
"border-red-500 bg-red-200/30 text-red-500 hover:border-red-600 dark:border-red-900 dark:bg-red-900/30 dark:hover:border-red-800",
)}
{...getRootProps()}
>
<input {...getInputProps()} accept=".csv" />
<div className="flex flex-col items-center justify-center gap-3">
{!noCsv && (
<div className="flex flex-col items-center">
<div className="mb-3 flex size-11 items-center justify-center rounded-full border bg-card">
<UploadIcon className="size-5" />
</div>
<h2 className="mb-0.5 text-center font-medium text-lg">
Upload CSV File
</h2>
<p className="text-center font-medium text-muted-foreground text-sm">
Drag and drop your file or click here to upload
</p>
</div>
)}
{noCsv && (
<div className="flex flex-col items-center">
<div className="mb-3 flex size-11 items-center justify-center rounded-full border border-red-500 bg-red-200/50 text-red-500 dark:border-red-900 dark:bg-red-900/30 dark:text-foreground">
<XIcon className="size-5" />
</div>
<h2 className="mb-0.5 text-center font-medium text-foreground text-lg">
Invalid CSV
</h2>
<p className="text-balance text-center text-sm">
Your CSV does not contain the "address" & "quantity"
columns
</p>
<Button
className="relative z-50 mt-4"
size="sm"
onClick={(e) => {
e.stopPropagation();
reset();
}}
>
Remove Invalid CSV
</Button>
</div>
)}
</div>
</div>
</div>
<div className="h-6" />
<CSVFormatDetails />
</div>
)}
</div>
);
};
function CSVFormatDetails() {
return (
<div>
<h3 className="mb-2 font-semibold">CSV Format</h3>
<ul className="mb-3 list-none space-y-2 text-muted-foreground text-sm">
<li>
CSV file must contain <InlineCode code="address" /> and
<InlineCode code="quantity" /> columns. If the quantity value is not
provided in a record, it will default to 1 token.
</li>
<li>
Repeated addresses will be removed and only the first found will be
kept.
</li>
<li>
ENS Name can be used as an address as well, Address will automatically
be resolved
</li>
</ul>
<DownloadExampleCSVButton />
</div>
);
}
function DownloadExampleCSVButton() {
return (
<Button
size="sm"
onClick={() => {
const link = document.createElement("a");
const exampleData = [
{
address: "0x000000000000000000000000000000000000dEaD",
quantity: "2",
},
{
address: "thirdweb.eth",
quantity: "1",
},
];
const csv = `address,quantity\n${exampleData
.map((o) => `${o.address},${o.quantity}`)
.join("\n")}`;
const blob = new Blob([csv], { type: "text/csv" });
link.href = URL.createObjectURL(blob);
link.download = "airdrop.csv";
link.click();
}}
>
<ArrowDownToLineIcon className="mr-2 size-4" />
Download Example CSV
</Button>
);
}
function AirdropTable(props: {
data: AirdropAddressInput[];
className?: string;
}) {
return (
<TableContainer
className={cn("flex flex-1 flex-col overflow-hidden", props.className)}
>
<Table>
<TableHeader>
<TableRow>
<TableHead>Address</TableHead>
<TableHead>Quantity</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{props.data.map((item, index) => (
// biome-ignore lint/suspicious/noArrayIndexKey: <explanation>
<TableRow key={index} className="!border-b">
<TableCell>
{item.isValid ? (
item.address
) : (
<div className="flex flex-row items-center gap-2">
<CircleAlertIcon className="size-4 text-red-500" />
<span className="font-bold text-red-500">
{item.address}
</span>
</div>
)}
</TableCell>
<TableCell>{item.quantity}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
);
}
@@ -0,0 +1,115 @@
"use client";
import { FormFieldSetup } from "@/components/blocks/FormFieldSetup";
import {
DistributionBarChart,
type Segment,
} from "@/components/blocks/distribution-chart";
import { Form } from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import type { ThirdwebClient } from "thirdweb";
import { StepCard } from "../create-token-card";
import type {
TokenDistributionForm,
TokenDistributionFormValues,
} from "../form";
import { TokenAirdropSection } from "./token-airdrop";
import { TokenSaleSection } from "./token-sale";
export function TokenDistributionFieldset(props: {
accountAddress: string;
onNext: () => void;
onPrevious: () => void;
form: TokenDistributionForm;
chainId: string;
client: ThirdwebClient;
}) {
const { form } = props;
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(props.onNext)}>
<StepCard
title="Token Distribution"
page="distribution"
prevButton={{
onClick: props.onPrevious,
}}
nextButton={{
type: "submit",
}}
>
<div>
<div className="space-y-6 p-4 md:px-6 md:py-6">
<FormFieldSetup
label="Total Supply"
isRequired
htmlFor="supply"
errorMessage={form.formState.errors.supply?.message}
>
<div className="relative">
<Input id="supply" {...form.register("supply")} />
<span className="-translate-y-1/2 absolute top-1/2 right-3 text-muted-foreground text-sm">
Tokens
</span>
</div>
</FormFieldSetup>
<TokenDistributionBarChart
distributionFormValues={form.watch()}
/>
</div>
<TokenAirdropSection form={form} />
<TokenSaleSection
form={form}
chainId={props.chainId}
client={props.client}
/>
</div>
</StepCard>
</form>
</Form>
);
}
export function TokenDistributionBarChart(props: {
distributionFormValues: TokenDistributionFormValues;
}) {
const totalSupply = Number(props.distributionFormValues.supply);
const totalAirdropSupply =
props.distributionFormValues.airdropAddresses.reduce(
(acc, curr) => acc + Number(curr.quantity),
0,
);
const airdropPercentage = (totalAirdropSupply / totalSupply) * 100;
const salePercentage = Number(
props.distributionFormValues.saleAllocationPercentage,
);
const ownerPercentage = 100 - airdropPercentage - salePercentage;
const tokenAllocations: Segment[] = [
{
label: "Owner",
percent: ownerPercentage,
color: "hsl(var(--chart-1))",
},
{
label: "Airdrop",
percent: airdropPercentage,
color: "hsl(var(--chart-3))",
},
{
label: "Sale",
percent: salePercentage,
color: "hsl(var(--chart-4))",
},
];
return (
<DistributionBarChart
segments={tokenAllocations}
title="Token Allocation"
/>
);
}
@@ -0,0 +1,149 @@
"use client";
import { FormFieldSetup } from "@/components/blocks/FormFieldSetup";
import { TokenSelector } from "@/components/blocks/TokenSelector";
import { DynamicHeight } from "@/components/ui/DynamicHeight";
import { Input } from "@/components/ui/input";
import { Switch } from "@/components/ui/switch";
import type { ThirdwebClient } from "thirdweb";
import type { TokenDistributionForm } from "../form";
export function TokenSaleSection(props: {
form: TokenDistributionForm;
chainId: string;
client: ThirdwebClient;
}) {
const totalSupply = Number(props.form.watch("supply"));
const sellSupply = Math.floor(
(totalSupply * Number(props.form.watch("saleAllocationPercentage"))) / 100,
);
const isEnabled = props.form.watch("saleEnabled");
return (
<DynamicHeight>
<div className="relative border-t border-dashed p-6">
<div className="flex items-center justify-between gap-3">
<div>
<h2 className="font-semibold text-lg">Sale</h2>
<p className="text-muted-foreground text-sm">
Make your token available for purchase by setting a price
</p>
</div>
<Switch
checked={isEnabled}
onCheckedChange={(checked) => {
props.form.setValue("saleEnabled", checked);
if (!checked) {
props.form.setValue("saleAllocationPercentage", "0");
props.form.setValue("salePrice", "0");
}
}}
/>
</div>
{isEnabled && (
<div className="mt-4 flex flex-col gap-6">
<FormFieldSetup
label="Sell % of Total Supply"
isRequired
helperText={`${sellSupply} tokens`}
errorMessage={
props.form.formState.errors.saleAllocationPercentage?.message
}
>
<div className="relative">
<DecimalInput
maxValue={100}
value={props.form.watch("saleAllocationPercentage")}
onChange={(value) => {
props.form.setValue("saleAllocationPercentage", value);
}}
/>
<span className="-translate-y-1/2 absolute top-1/2 right-3 text-muted-foreground text-sm">
%
</span>
</div>
</FormFieldSetup>
<div className="grid grid-cols-1 gap-4 lg:grid-cols-2">
<FormFieldSetup
label="Price per Token"
isRequired
errorMessage={props.form.formState.errors.salePrice?.message}
>
<div className="relative">
<DecimalInput
value={props.form.watch("salePrice")}
onChange={(value) => {
props.form.setValue("salePrice", value);
}}
/>
</div>
</FormFieldSetup>
<FormFieldSetup
label="Currency"
isRequired
errorMessage={
props.form.formState.errors.saleTokenAddress?.message
}
>
<TokenSelector
className="bg-background"
addNativeTokenIfMissing={true}
showCheck={true}
disableAddress={true}
selectedToken={{
address: props.form.watch("saleTokenAddress"),
chainId: Number(props.chainId),
}}
onChange={(value) => {
props.form.setValue("saleTokenAddress", value.address);
}}
client={props.client}
chainId={Number(props.chainId)}
/>
</FormFieldSetup>
</div>
</div>
)}
</div>
</DynamicHeight>
);
}
function DecimalInput(props: {
value: string;
onChange: (value: string) => void;
maxValue?: number;
}) {
return (
<Input
type="text"
value={props.value}
inputMode="decimal"
onChange={(e) => {
const number = Number(e.target.value);
// ignore if string becomes invalid number
if (Number.isNaN(number)) {
return;
}
if (props.maxValue && number > props.maxValue) {
return;
}
// replace leading multiple zeros with single zero
let cleanedValue = e.target.value.replace(/^0+/, "0");
// replace leading zero before decimal point
if (!cleanedValue.includes(".")) {
cleanedValue = cleanedValue.replace(/^0+/, "");
}
props.onChange(cleanedValue || "0");
}}
/>
);
}
@@ -0,0 +1,95 @@
import type { UseFormReturn } from "react-hook-form";
import { isAddress } from "thirdweb";
import * as z from "zod";
const addressSchema = z.string().refine(
(value) => {
if (isAddress(value)) {
return true;
}
return false;
},
{
message: "Invalid address",
},
);
const urlSchema = z.string().url();
export const tokenInfoFormSchema = z.object({
// info fieldset
name: z.string().min(1, "Name is required"),
symbol: z
.string()
.min(1, "Symbol is required")
.max(10, "Symbol must be 10 characters or less"),
chain: z.string().min(1, "Chain is required"),
description: z.string().optional(),
image: z.any().optional(),
socialUrls: z.array(
z.object({
platform: z.string(),
url: z.string().refine(
(val) => {
if (val === "") {
return true;
}
const url = val.startsWith("http") ? val : `https://${val}`;
return urlSchema.safeParse(url);
},
{
message: "Invalid URL",
},
),
}),
),
});
export const tokenDistributionFormSchema = z.object({
supply: z.string().min(1, "Supply is required"),
saleAllocationPercentage: z.string().refine(
(value) => {
const number = Number(value);
if (Number.isNaN(number)) {
return false;
}
return number >= 0 && number <= 100;
},
{
message: "Must be a number between 0 and 100",
},
),
saleTokenAddress: z.string(),
salePrice: z.string().refine(
(value) => {
const number = Number(value);
return !Number.isNaN(number) && number >= 0;
},
{
message: "Must be number larger than or equal to 0",
},
),
airdropAddresses: z.array(
z.object({
address: addressSchema,
quantity: z.string(),
}),
),
// UI states
airdropEnabled: z.boolean(),
saleEnabled: z.boolean(),
});
export type TokenDistributionForm = UseFormReturn<
z.infer<typeof tokenDistributionFormSchema>
>;
export type TokenDistributionFormValues = z.infer<
typeof tokenDistributionFormSchema
>;
export type TokenInfoFormValues = z.infer<typeof tokenInfoFormSchema>;
export type TokenInfoForm = UseFormReturn<TokenInfoFormValues>;
export type CreateAssetFormValues = TokenInfoFormValues &
TokenDistributionFormValues;
@@ -0,0 +1,335 @@
"use client";
import { Img } from "@/components/blocks/Img";
import {
type MultiStepState,
MultiStepStatus,
} from "@/components/blocks/multi-step-status/multi-step-status";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { TransactionButton } from "components/buttons/TransactionButton";
import { ChainIconClient } from "components/icons/ChainIcon";
import { useTrack } from "hooks/analytics/useTrack";
import { useAllChainsData } from "hooks/chains/allChains";
import { ArrowRightIcon, ImageOffIcon, RocketIcon } from "lucide-react";
import Link from "next/link";
import { useEffect, useState } from "react";
import type { ThirdwebClient } from "thirdweb";
import { useActiveWallet } from "thirdweb/react";
import { StepCard } from "../create-token-card";
import type { CreateTokenFunctions } from "../create-token-page.client";
import { TokenDistributionBarChart } from "../distribution/token-distribution";
import type { CreateAssetFormValues } from "../form";
import { getLaunchTrackingData } from "../tracking";
export function LaunchTokenStatus(props: {
createTokenFunctions: CreateTokenFunctions;
values: CreateAssetFormValues;
onPrevious: () => void;
client: ThirdwebClient;
onLaunchSuccess: () => void;
}) {
const formValues = props.values;
const { createTokenFunctions } = props;
const [steps, setSteps] = useState<MultiStepState[]>([]);
const [isModalOpen, setIsModalOpen] = useState(false);
const [contractLink, setContractLink] = useState<string | null>(null);
const activeWallet = useActiveWallet();
const walletRequiresApproval = activeWallet?.id !== "inApp";
const trackEvent = useTrack();
async function handleSubmitClick() {
function launchTracking(type: "attempt" | "success" | "error") {
trackEvent(
getLaunchTrackingData({
chainId: Number(formValues.chain),
airdropEnabled: formValues.airdropEnabled,
saleEnabled: formValues.saleEnabled,
type,
}),
);
}
launchTracking("attempt");
function updateStatus(index: number, newStatus: MultiStepState["status"]) {
setSteps((prev) => {
return [
...prev.slice(0, index),
{ ...prev[index], status: newStatus },
...prev.slice(index + 1),
] as MultiStepState[];
});
}
function createSequenceExecutorFn(
index: number,
executeFn: (values: CreateAssetFormValues) => Promise<void>,
) {
return async () => {
updateStatus(index, "pending");
try {
await executeFn(formValues);
updateStatus(index, "completed");
// start next one
const nextStep = initialSteps[index + 1];
if (nextStep) {
// do not use await next step
nextStep.execute();
} else {
launchTracking("success");
props.onLaunchSuccess();
}
} catch (error) {
updateStatus(index, "error");
launchTracking("error");
console.error(error);
}
};
}
const initialSteps: MultiStepState[] = [
{
label: "Deploy contract",
status: "idle",
retryLabel: "Failed to deploy contract",
execute: createSequenceExecutorFn(0, async (values) => {
const result = await createTokenFunctions.deployContract(values);
setContractLink(`/${values.chain}/${result.contractAddress}`);
}),
},
{
label: "Set claim conditions",
status: "idle",
retryLabel: "Failed to set claim conditions",
execute: createSequenceExecutorFn(
1,
createTokenFunctions.setClaimConditions,
),
},
{
label: "Mint tokens",
status: "idle",
retryLabel: "Failed to mint tokens",
execute: createSequenceExecutorFn(2, createTokenFunctions.mintTokens),
},
];
if (formValues.airdropEnabled && formValues.airdropAddresses.length > 0) {
initialSteps.push({
label: "Airdrop tokens",
status: "idle",
retryLabel: "Failed to airdrop tokens",
execute: createSequenceExecutorFn(
3,
createTokenFunctions.airdropTokens,
),
});
}
setSteps(initialSteps);
setIsModalOpen(true);
// start sequence
initialSteps[0]?.execute();
}
const isComplete = steps.every((step) => step.status === "completed");
const isPending = steps.some((step) => step.status === "pending");
return (
<StepCard
page="launch"
title="Launch Token"
prevButton={{
onClick: props.onPrevious,
}}
nextButton={{
type: "custom",
custom: (
<TransactionButton
variant="default"
txChainID={Number(formValues.chain)}
isLoggedIn={true}
isPending={false}
transactionCount={undefined}
onClick={handleSubmitClick}
>
<RocketIcon className="size-4" />
Launch Token
</TransactionButton>
),
}}
>
{/* Token info */}
<div className="flex flex-col gap-6 border-b border-dashed px-4 py-6 pb-6 md:px-6 lg:flex-row">
<OverviewField name="Image" className="shrink-0">
<RenderFileImage file={formValues.image} />
</OverviewField>
<div className="flex grow flex-col gap-4 ">
<div className="flex flex-col gap-4 lg:flex-row lg:gap-6 lg:border-b lg:border-dashed lg:pb-4 lg:[&>*:not(:first-child)]:border-l lg:[&>*:not(:first-child)]:border-dashed lg:[&>*:not(:first-child)]:pl-5">
<OverviewField name="Name">
<OverviewFieldValue value={formValues.name} />
</OverviewField>
<OverviewField name="Symbol">
<OverviewFieldValue value={formValues.symbol} />
</OverviewField>
<OverviewField name="Chain">
<ChainOverview client={props.client} chainId={formValues.chain} />
</OverviewField>
</div>
<OverviewField name="Description">
<OverviewFieldValue
value={formValues.description || "No Description"}
/>
</OverviewField>
</div>
</div>
{/* Token distribution */}
<div className="flex flex-col gap-4 px-4 py-6 md:px-6">
<OverviewField name="Total Supply">
<p className="font-medium text-foreground text-sm">
{compactNumberFormatter.format(Number(formValues.supply))}
</p>
</OverviewField>
<TokenDistributionBarChart distributionFormValues={formValues} />
</div>
<Dialog
open={isModalOpen}
// do not set onOpenChange
>
<DialogContent
className="gap-0 overflow-hidden p-0 md:max-w-[480px]"
dialogCloseClassName="hidden"
>
<div className="flex flex-col gap-6 p-6">
<DialogHeader>
<DialogTitle className="font-semibold text-xl tracking-tight">
Status
</DialogTitle>
{walletRequiresApproval && (
<DialogDescription>
Each step will prompt a signature request in your wallet
</DialogDescription>
)}
</DialogHeader>
<MultiStepStatus steps={steps} />
</div>
<div className="mt-2 flex justify-between gap-4 border-border border-t bg-card p-6">
{isComplete && contractLink ? (
<div>
<Button asChild className="gap-2">
<Link href={contractLink}>
View Launched Token <ArrowRightIcon className="size-4" />
</Link>
</Button>
</div>
) : (
<div />
)}
<Button
variant="outline"
disabled={isPending}
onClick={() => {
setIsModalOpen(false);
// reset steps
setSteps([]);
}}
>
{isComplete ? "Close" : "Cancel"}
</Button>
</div>
</DialogContent>
</Dialog>
</StepCard>
);
}
const compactNumberFormatter = new Intl.NumberFormat("en-US", {
notation: "compact",
maximumFractionDigits: 10,
});
function RenderFileImage(props: {
file: File | undefined;
}) {
const [objectUrl, setObjectUrl] = useState<string>("");
// eslint-disable-next-line no-restricted-syntax
useEffect(() => {
if (props.file) {
const url = URL.createObjectURL(props.file);
setObjectUrl(url);
return () => URL.revokeObjectURL(url);
} else {
setObjectUrl("");
}
}, [props.file]);
return (
<Img
src={objectUrl}
className="size-24 rounded-lg border object-cover"
fallback={
<div className="flex items-center justify-center bg-muted/50">
<ImageOffIcon className="size-5 text-muted-foreground" />
</div>
}
/>
);
}
function OverviewField(props: {
name: string;
children: React.ReactNode;
className?: string;
}) {
return (
<div className={props.className}>
<p className="mb-1 text-muted-foreground text-sm">{props.name}</p>
{props.children}
</div>
);
}
function OverviewFieldValue(props: {
value: string;
}) {
return <p className="text-foreground text-sm">{props.value}</p>;
}
function ChainOverview(props: {
chainId: string;
client: ThirdwebClient;
}) {
const { idToChain } = useAllChainsData();
const chainMetadata = idToChain.get(Number(props.chainId));
return (
<div className="flex items-center gap-2">
<ChainIconClient
className="size-3.5"
client={props.client}
src={chainMetadata?.icon?.url || ""}
/>
<p className="text-foreground text-sm">
{chainMetadata?.name || `Chain ${props.chainId}`}
</p>
</div>
);
}
@@ -0,0 +1,108 @@
import { getProject } from "@/api/projects";
import { getTeamBySlug } from "@/api/team";
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator,
} from "@/components/ui/breadcrumb";
import { getClientThirdwebClient } from "@/constants/thirdweb-client.client";
import Link from "next/link";
import { redirect } from "next/navigation";
import {
getAuthToken,
getAuthTokenWalletAddress,
} from "../../../../../../api/lib/getAuthToken";
import { loginRedirect } from "../../../../../../login/loginRedirect";
import { CreateTokenAssetPage } from "./create-token-page-impl";
export default async function Page(props: {
params: Promise<{ team_slug: string; project_slug: string }>;
}) {
const params = await props.params;
const [authToken, team, project, accountAddress] = await Promise.all([
getAuthToken(),
getTeamBySlug(params.team_slug),
getProject(params.team_slug, params.project_slug),
getAuthTokenWalletAddress(),
]);
if (!authToken || !accountAddress) {
loginRedirect(
`/team/${params.team_slug}/${params.project_slug}/assets/create`,
);
}
if (!team) {
redirect("/team");
}
if (!project) {
redirect(`/team/${params.team_slug}`);
}
const client = getClientThirdwebClient({
jwt: authToken,
teamId: team.id,
});
return (
<div className="flex grow flex-col">
<PageHeader
teamSlug={params.team_slug}
projectSlug={params.project_slug}
/>
<div className="container max-w-5xl pt-8 pb-32">
<CreateTokenAssetPage
accountAddress={accountAddress}
client={client}
teamId={team.id}
projectId={project.id}
/>
</div>
</div>
);
}
function PageHeader(props: {
teamSlug: string;
projectSlug: string;
}) {
return (
<div className="border-b">
<div className="border-b py-3">
<Breadcrumb className="container max-w-5xl">
<BreadcrumbList>
<BreadcrumbItem>
<BreadcrumbLink asChild>
<Link
href={`/team/${props.teamSlug}/${props.projectSlug}/assets`}
>
Assets
</Link>
</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbPage>Create Token</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
</div>
<div className="container flex max-w-5xl flex-col gap-3 py-8 lg:flex-row lg:items-center lg:justify-between">
<div>
<h1 className="font-semibold text-2xl tracking-tight lg:text-3xl">
Create Token
</h1>
<p className="text-muted-foreground">
Launch an ERC-20 token for your project
</p>
</div>
</div>
</div>
);
}
@@ -0,0 +1,221 @@
"use client";
import { FormFieldSetup } from "@/components/blocks/FormFieldSetup";
import { SingleNetworkSelector } from "@/components/blocks/NetworkSelectors";
import { Button } from "@/components/ui/button";
import { Form } from "@/components/ui/form";
import {
FormControl,
FormField,
FormItem,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import { ClientOnly } from "components/ClientOnly/ClientOnly";
import { FileInput } from "components/shared/FileInput";
import { PlusIcon, Trash2Icon } from "lucide-react";
import { useFieldArray } from "react-hook-form";
import type { ThirdwebClient } from "thirdweb";
import { StepCard } from "./create-token-card";
import type { TokenInfoForm } from "./form";
export function TokenInfoFieldset(props: {
client: ThirdwebClient;
onNext: () => void;
form: TokenInfoForm;
onChainUpdated: () => void;
}) {
const { form } = props;
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(props.onNext)}>
<StepCard
page="info"
title="Token Information"
prevButton={undefined}
nextButton={{
type: "submit",
}}
>
<div className="flex flex-col gap-6 px-4 py-6 md:grid md:grid-cols-[280px_1fr] md:px-6">
{/* left */}
<FormFieldSetup
errorMessage={form.formState.errors.image?.message as string}
label="Image"
isRequired={false}
>
<FileInput
accept={{ "image/*": [] }}
value={form.watch("image")}
setValue={(file) =>
form.setValue("image", file, {
shouldTouch: true,
})
}
className="rounded-lg border-border bg-background transition-all duration-200 hover:border-active-border hover:bg-background"
/>
</FormFieldSetup>
{/* right */}
<div className="flex flex-col gap-6">
{/* name + symbol */}
<div className="flex flex-col gap-6 lg:flex-row lg:gap-4">
<FormFieldSetup
className="grow"
label="Name"
isRequired
htmlFor="name"
errorMessage={form.formState.errors.name?.message}
>
<Input
id="name"
placeholder="My Token"
{...form.register("name")}
/>
</FormFieldSetup>
<FormFieldSetup
className="lg:max-w-[200px]"
label="Symbol"
isRequired
htmlFor="symbol"
errorMessage={form.formState.errors.symbol?.message}
>
<Input
id="symbol"
placeholder="XYZ"
{...form.register("symbol")}
/>
</FormFieldSetup>
</div>
{/* chain */}
<FormFieldSetup
label="Chain"
isRequired
htmlFor="chain"
errorMessage={form.formState.errors.chain?.message}
>
<ClientOnly ssr={null}>
<SingleNetworkSelector
className="bg-background"
client={props.client}
chainId={Number(form.watch("chain"))}
onChange={(chain) => {
form.setValue("chain", chain.toString());
props.onChainUpdated();
}}
disableChainId
/>
</ClientOnly>
</FormFieldSetup>
<FormFieldSetup
label="Description"
isRequired={false}
htmlFor="description"
className="flex grow flex-col"
errorMessage={form.formState.errors.description?.message}
>
<Textarea
id="description"
placeholder="Describe your token"
className="grow"
{...form.register("description")}
/>
</FormFieldSetup>
</div>
</div>
<SocialUrls form={form} />
</StepCard>
</form>
</Form>
);
}
function SocialUrls(props: {
form: TokenInfoForm;
}) {
const { form } = props;
const { fields, append, remove } = useFieldArray({
name: "socialUrls",
control: form.control,
});
return (
<div className="border-t border-dashed px-4 py-6 lg:px-6">
<h2 className="mb-2 font-medium text-sm">Social URLs</h2>
{fields.length > 0 && (
<div className="mb-5 space-y-4">
{fields.map((field, index) => (
<div
key={field.id}
className="flex gap-3 max-sm:mb-6 max-sm:border-b max-sm:border-dashed max-sm:pb-6"
>
<div className="flex flex-1 flex-col gap-3 lg:flex-row">
<FormField
control={form.control}
name={`socialUrls.${index}.platform`}
render={({ field }) => (
<FormItem className="lg:max-w-[140px]">
<FormControl>
<Input
{...field}
placeholder="Platform"
aria-label="Platform"
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name={`socialUrls.${index}.url`}
render={({ field }) => (
<FormItem className="flex-1">
<FormControl>
<Input
{...field}
placeholder="https://..."
aria-label="Platform URL"
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<Button
type="button"
variant="outline"
size="icon"
onClick={() => remove(index)}
>
<Trash2Icon className="h-4 w-4" />
<span className="sr-only">Remove</span>
</Button>
</div>
))}
</div>
)}
<Button
type="button"
size="sm"
variant="outline"
onClick={() => append({ platform: "", url: "" })}
className="gap-2"
>
<PlusIcon className="size-4" />
Add Social URL
</Button>
</div>
);
}
@@ -0,0 +1,60 @@
export function getTokenDeploymentTrackingData(
type: "attempt" | "success" | "error",
chainId: number,
) {
// using "custom-contract" because it has to match the main deployment tracking format
return {
category: "custom-contract",
action: "deploy",
label: type,
publisherAndContractName: "deployer.thirdweb.eth/DropERC20",
chainId: chainId,
deploymentType: "asset",
};
}
// example: asset.claim-conditions.attempt
export function getTokenStepTrackingData(params: {
action: "claim-conditions" | "airdrop" | "mint";
chainId: number;
status: "attempt" | "success" | "error";
}) {
return {
category: "asset",
action: params.action,
contractType: "DropERC20",
label: params.status,
chainId: params.chainId,
};
}
// example: asset.launch.attempt
export function getLaunchTrackingData(params: {
chainId: number;
airdropEnabled: boolean;
saleEnabled: boolean;
type: "attempt" | "success" | "error";
}) {
return {
category: "asset",
action: "launch",
label: params.type,
contractType: "DropERC20",
chainId: params.chainId,
airdropEnabled: params.airdropEnabled,
saleEnabled: params.saleEnabled,
};
}
// example: asset.info-page.next-click
export function getStepCardTrackingData(params: {
step: "info" | "distribution" | "launch";
click: "prev" | "next";
}) {
return {
category: "asset",
action: `${params.step}-page`,
label: `${params.click}-click`,
contractType: "DropERC20",
};
}
@@ -0,0 +1,115 @@
import { getProject } from "@/api/projects";
import { getTeamBySlug } from "@/api/team";
import { GenericLoadingPage } from "@/components/blocks/skeletons/GenericLoadingPage";
import { getClientThirdwebClient } from "@/constants/thirdweb-client.client";
import { redirect } from "next/navigation";
import { Suspense } from "react";
import type { ThirdwebClient } from "thirdweb";
import { ClientOnly } from "../../../../../../../components/ClientOnly/ClientOnly";
import { ContractTable } from "../../../../../../../components/contract-components/tables/contract-table";
import { getSortedDeployedContracts } from "../../../../../account/contracts/_components/getSortedDeployedContracts";
import { getAuthToken } from "../../../../../api/lib/getAuthToken";
import { loginRedirect } from "../../../../../login/loginRedirect";
import { Cards } from "./cards";
export default async function Page(props: {
params: Promise<{ team_slug: string; project_slug: string }>;
}) {
const params = await props.params;
const [authToken, team, project] = await Promise.all([
getAuthToken(),
getTeamBySlug(params.team_slug),
getProject(params.team_slug, params.project_slug),
]);
if (!authToken) {
loginRedirect(`/team/${params.team_slug}/${params.project_slug}/assets`);
}
if (!team) {
redirect("/team");
}
if (!project) {
redirect(`/team/${params.team_slug}`);
}
const client = getClientThirdwebClient({
jwt: authToken,
teamId: team.id,
});
return (
<div className="flex grow flex-col">
<AssetsHeader />
<div className="container max-w-7xl pt-8 pb-20">
<Cards
teamSlug={params.team_slug}
projectSlug={params.project_slug}
client={client}
teamId={team.id}
projectId={project.id}
/>
<div className="mt-10 mb-3">
<h2 className="font-semibold text-2xl tracking-tight">Your assets</h2>
<p className="text-muted-foreground">
List of all assets created or imported into this project
</p>
</div>
<Suspense fallback={<GenericLoadingPage />}>
<AssetsPageAsync
teamId={team.id}
projectId={project.id}
authToken={authToken}
client={client}
/>
</Suspense>
</div>
</div>
);
}
function AssetsHeader() {
return (
<div className="border-b">
<div className="container max-w-7xl py-10">
<h1 className="font-semibold text-2xl tracking-tight lg:text-3xl">
Assets
</h1>
<p className="text-muted-foreground">
Create and Manage tokens for your project
</p>
</div>
</div>
);
}
async function AssetsPageAsync(props: {
teamId: string;
projectId: string;
authToken: string;
client: ThirdwebClient;
}) {
const deployedContracts = await getSortedDeployedContracts({
teamId: props.teamId,
projectId: props.projectId,
authToken: props.authToken,
deploymentType: "asset",
});
return (
<ClientOnly ssr={<GenericLoadingPage />}>
<ContractTable
variant="asset"
contracts={deployedContracts}
pageSize={10}
teamId={props.teamId}
projectId={props.projectId}
client={props.client}
/>
</ClientOnly>
);
}
@@ -14,10 +14,10 @@ import {
CircleAlertIcon,
ExternalLinkIcon,
} from "lucide-react";
import { ContractIcon } from "../../../../../(dashboard)/(chain)/components/server/icons/ContractIcon";
import { EngineIcon } from "../../../../../(dashboard)/(chain)/components/server/icons/EngineIcon";
import { InsightIcon } from "../../../../../(dashboard)/(chain)/components/server/icons/InsightIcon";
import { NebulaIcon } from "../../../../../../nebula-app/(app)/icons/NebulaIcon";
import { ContractIcon } from "../../../../../../(dashboard)/(chain)/components/server/icons/ContractIcon";
import { EngineIcon } from "../../../../../../(dashboard)/(chain)/components/server/icons/EngineIcon";
import { InsightIcon } from "../../../../../../(dashboard)/(chain)/components/server/icons/InsightIcon";
import { NebulaIcon } from "../../../../../../../nebula-app/(app)/icons/NebulaIcon";
import { ClientIDSection } from "./ClientIDSection";
import { IntegrateAPIKeyCodeTabs } from "./IntegrateAPIKeyCodeTabs";
import { SecretKeySection } from "./SecretKeySection";
@@ -63,6 +63,7 @@ function IntegrateAPIKeySection({
{secretKeyMasked && (
<SecretKeySection
secretKeyMasked={secretKeyMasked}
teamId={project.teamId}
projectId={project.id}
/>
)}
@@ -6,6 +6,7 @@ import { RotateSecretKeyButton } from "../../settings/ProjectGeneralSettingsPage
export function SecretKeySection(props: {
secretKeyMasked: string;
teamId: string;
projectId: string;
}) {
const [secretKeyMasked, setSecretKeyMasked] = useState(props.secretKeyMasked);
@@ -26,7 +27,10 @@ export function SecretKeySection(props: {
<RotateSecretKeyButton
rotateSecretKey={async () => {
return rotateSecretKeyClient(props.projectId);
return rotateSecretKeyClient({
teamId: props.teamId,
projectId: props.projectId,
});
}}
onSuccess={(data) => {
setSecretKeyMasked(data.data.secretMasked);
@@ -1,19 +1,20 @@
"use client";
import { FullWidthSidebarLayout } from "@/components/blocks/SidebarLayout";
import { Badge } from "@/components/ui/badge";
import {
BookTextIcon,
BoxIcon,
CoinsIcon,
HomeIcon,
SettingsIcon,
WalletIcon,
} from "lucide-react";
import { ContractIcon } from "../../../../(dashboard)/(chain)/components/server/icons/ContractIcon";
import { EngineIcon } from "../../../../(dashboard)/(chain)/components/server/icons/EngineIcon";
import { InsightIcon } from "../../../../(dashboard)/(chain)/components/server/icons/InsightIcon";
import { PayIcon } from "../../../../(dashboard)/(chain)/components/server/icons/PayIcon";
import { SmartAccountIcon } from "../../../../(dashboard)/(chain)/components/server/icons/SmartAccountIcon";
import { Badge } from "../../../../../../@/components/ui/badge";
import { NebulaIcon } from "../../../../../nebula-app/(app)/icons/NebulaIcon";
import { ContractIcon } from "../../../../../(dashboard)/(chain)/components/server/icons/ContractIcon";
import { EngineIcon } from "../../../../../(dashboard)/(chain)/components/server/icons/EngineIcon";
import { InsightIcon } from "../../../../../(dashboard)/(chain)/components/server/icons/InsightIcon";
import { PayIcon } from "../../../../../(dashboard)/(chain)/components/server/icons/PayIcon";
import { SmartAccountIcon } from "../../../../../(dashboard)/(chain)/components/server/icons/SmartAccountIcon";
import { NebulaIcon } from "../../../../../../nebula-app/(app)/icons/NebulaIcon";
export function ProjectSidebarLayout(props: {
layoutPath: string;
@@ -52,11 +53,7 @@ export function ProjectSidebarLayout(props: {
{
href: `${layoutPath}/connect/universal-bridge`,
icon: PayIcon,
label: (
<span className="flex items-center gap-2">
Universal Bridge <Badge>New</Badge>
</span>
),
label: "Universal Bridge",
tracking: tracking("universal-bridge"),
},
{
@@ -65,6 +62,16 @@ export function ProjectSidebarLayout(props: {
icon: ContractIcon,
tracking: tracking("contracts"),
},
{
href: `${layoutPath}/assets`,
label: (
<span className="flex items-center gap-2">
Assets <Badge>New</Badge>
</span>
),
icon: CoinsIcon,
tracking: tracking("assets"),
},
{
href: `${layoutPath}/engine`,
label: (
@@ -15,7 +15,7 @@ import {
XAxis,
} from "recharts";
import type { RpcMethodStats } from "types/analytics";
import { EmptyStateCard } from "../../../../components/Analytics/EmptyStateCard";
import { EmptyStateCard } from "../../../../../components/Analytics/EmptyStateCard";
export function RpcMethodBarChartCardUI({
rawData,
@@ -2,8 +2,8 @@ import { fetchDashboardContractMetadata } from "@3rdweb-sdk/react/hooks/useDashb
import { type ThirdwebClient, defineChain, getContract } from "thirdweb";
import { getChainMetadata } from "thirdweb/chains";
import type { TransactionStats } from "types/analytics";
import { TransactionsChartCardUI } from "../../../(team)/_components/TransactionsCard";
import { PieChartCard } from "../../../../components/Analytics/PieChartCard";
import { TransactionsChartCardUI } from "../../../../(team)/_components/TransactionsCard";
import { PieChartCard } from "../../../../../components/Analytics/PieChartCard";
export function TransactionsChartsUI({
data,
@@ -13,9 +13,9 @@ import { redirect } from "next/navigation";
import { Suspense } from "react";
import { defineChain, getContract } from "thirdweb";
import { getCompilerMetadata } from "thirdweb/contract";
import { getSortedDeployedContracts } from "../../../../../../account/contracts/_components/getSortedDeployedContracts";
import { getAuthToken } from "../../../../../../api/lib/getAuthToken";
import { loginRedirect } from "../../../../../../login/loginRedirect";
import { getSortedDeployedContracts } from "../../../../../../../account/contracts/_components/getSortedDeployedContracts";
import { getAuthToken } from "../../../../../../../api/lib/getAuthToken";
import { loginRedirect } from "../../../../../../../login/loginRedirect";
const trackingCategory = "smart-wallet";
@@ -113,6 +113,7 @@ async function AsyncYourFactories(props: {
teamId: props.teamId,
projectId: props.projectId,
authToken: props.authToken,
deploymentType: undefined,
});
const factories = (
@@ -1,6 +1,7 @@
import { getUserOpUsage } from "@/api/analytics";
import { getProject } from "@/api/projects";
import { getTeamBySlug } from "@/api/team";
import { getClientThirdwebClient } from "@/constants/thirdweb-client.client";
import {
type Range,
getLastNDaysRange,
@@ -9,8 +10,7 @@ import { AccountAbstractionAnalytics } from "components/smart-wallets/AccountAbs
import { AccountAbstractionSummary } from "components/smart-wallets/AccountAbstractionAnalytics/AccountAbstractionSummary";
import { notFound, redirect } from "next/navigation";
import type { SearchParams } from "nuqs/server";
import { getClientThirdwebClient } from "../../../../../../../@/constants/thirdweb-client.client";
import { getAuthToken } from "../../../../../api/lib/getAuthToken";
import { getAuthToken } from "../../../../../../api/lib/getAuthToken";
import { searchParamLoader } from "./search-params";
interface PageParams {
@@ -3,12 +3,12 @@ import { getTeamBySlug } from "@/api/team";
import { ChakraProviderSetup } from "@/components/ChakraProviderSetup";
import { UnderlineLink } from "@/components/ui/UnderlineLink";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { getClientThirdwebClient } from "@/constants/thirdweb-client.client";
import { AccountAbstractionSettingsPage } from "components/smart-wallets/SponsorshipPolicies";
import { CircleAlertIcon } from "lucide-react";
import { redirect } from "next/navigation";
import { getClientThirdwebClient } from "../../../../../../../../@/constants/thirdweb-client.client";
import { getAuthToken } from "../../../../../../api/lib/getAuthToken";
import { getValidTeamPlan } from "../../../../../components/TeamHeader/getValidTeamPlan";
import { getAuthToken } from "../../../../../../../api/lib/getAuthToken";
import { getValidTeamPlan } from "../../../../../../components/TeamHeader/getValidTeamPlan";
export default async function Page(props: {
params: Promise<{ team_slug: string; project_slug: string }>;

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